diff --git a/app/Filament/Admin/Resources/Eggs/Pages/CreateEgg.php b/app/Filament/Admin/Resources/Eggs/Pages/CreateEgg.php index 4ad98e366..b591848e8 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/CreateEgg.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/CreateEgg.php @@ -83,14 +83,16 @@ class CreateEgg extends CreateRecord ->rows(2) ->columnSpanFull() ->helperText(trans('admin/egg.description_help')), - Textarea::make('startup') - ->label(trans('admin/egg.startup')) - ->rows(3) + KeyValue::make('startup_commands') + ->label(trans('admin/egg.startup_commands')) + ->live() ->columnSpanFull() ->required() - ->placeholder(implode("\n", [ - 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}', - ])) + ->addActionLabel(trans('admin/egg.add_startup')) + ->keyLabel(trans('admin/egg.startup_name')) + ->keyPlaceholder('Default') + ->valueLabel(trans('admin/egg.startup_command')) + ->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}') ->helperText(trans('admin/egg.startup_help')), TagsInput::make('file_denylist') ->label(trans('admin/egg.file_denylist')) diff --git a/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php b/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php index c92b14251..cf756de95 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php @@ -80,11 +80,14 @@ class EditEgg extends EditRecord ->disabled() ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) ->helperText(trans('admin/egg.author_help_edit')), - Textarea::make('startup') - ->label(trans('admin/egg.startup')) - ->rows(3) + KeyValue::make('startup_commands') + ->label(trans('admin/egg.startup_commands')) + ->live() ->columnSpanFull() ->required() + ->addActionLabel(trans('admin/egg.add_startup')) + ->keyLabel(trans('admin/egg.startup_name')) + ->valueLabel(trans('admin/egg.startup_command')) ->helperText(trans('admin/egg.startup_help')), TagsInput::make('file_denylist') ->label(trans('admin/egg.file_denylist')) diff --git a/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php b/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php index 70937c635..e123f3e90 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php @@ -328,7 +328,7 @@ class CreateServer extends CreateRecord ->live() ->afterStateUpdated(function ($state, Set $set, Get $get, $old) { $egg = Egg::query()->find($state); - $set('startup', $egg->startup ?? ''); + $set('startup', ''); $set('image', ''); $variables = $egg->variables ?? []; @@ -402,24 +402,45 @@ class CreateServer extends CreateRecord ]) ->inline(), - Textarea::make('startup') - ->hintIcon('tabler-code') + Select::make('select_startup') ->label(trans('admin/server.startup_cmd')) ->hidden(fn (Get $get) => $get('egg_id') === null) + ->live() + ->afterStateUpdated(fn (Set $set, $state) => $set('startup', $state)) + ->options(function ($state, Get $get, Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $startups = $egg->startup_commands ?? []; + + $currentStartup = $get('startup'); + if (!$currentStartup && $startups) { + $currentStartup = collect($startups)->first(); + $set('startup', $currentStartup); + $set('select_startup', $currentStartup); + } + + return array_flip($startups) + ['' => 'Custom Startup']; + }) + ->selectablePlaceholder(false) + ->columnSpanFull(), + + Textarea::make('startup') + ->hiddenLabel() + ->hidden(fn (Get $get) => $get('egg_id') === null) ->required() ->live() - ->rows(function ($state) { - return str($state)->explode("\n")->reduce( - fn (int $carry, $line) => $carry + floor(strlen($line) / 125), - 1 - ); + ->autosize() + ->afterStateUpdated(function ($state, Get $get, Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $startups = $egg->startup_commands ?? []; + + if (in_array($state, $startups)) { + $set('select_startup', $state); + } else { + $set('select_startup', ''); + } }) - ->columnSpan([ - 'default' => 1, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]), + ->placeholder(trans('admin/server.startup_placeholder')) + ->columnSpanFull(), Hidden::make('environment')->default([]), diff --git a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php index 53cd2047a..879a015c7 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php @@ -609,26 +609,51 @@ class EditServer extends EditRecord 1 => 'tabler-code-off', ]) ->required(), + Hidden::make('previewing') ->default(false), - Textarea::make('startup') + + Select::make('select_startup') ->label(trans('admin/server.startup_cmd')) - ->required() - ->columnSpan(6) - ->autosize() + ->live() + ->afterStateUpdated(function (Set $set, $state) { + $set('startup', $state); + $set('previewing', false); + }) + ->options(function ($state, Get $get, Set $set) { + $egg = Egg::find($get('egg_id')); + $startups = $egg->startup_commands ?? []; + + $currentStartup = $get('startup'); + if (!$currentStartup && $startups) { + $currentStartup = collect($startups)->first(); + $set('startup', $currentStartup); + $set('select_startup', $currentStartup); + } + + return array_flip($startups) + ['' => 'Custom Startup']; + }) + ->selectablePlaceholder(false) + ->columnSpanFull() ->hintAction(PreviewStartupAction::make('preview')), - Textarea::make('defaultStartup') - ->hintCopy() - ->label(trans('admin/server.default_startup')) - ->disabled() + Textarea::make('startup') + ->hiddenLabel() + ->required() + ->live() ->autosize() - ->columnSpan(6) - ->formatStateUsing(function ($state, Get $get) { - $egg = Egg::query()->find($get('egg_id')); + ->afterStateUpdated(function ($state, Get $get, Set $set) { + $egg = Egg::find($get('egg_id')); + $startups = $egg->startup_commands ?? []; - return $egg->startup; - }), + if (in_array($state, $startups)) { + $set('select_startup', $state); + } else { + $set('select_startup', ''); + } + }) + ->placeholder(trans('admin/server.startup_placeholder')) + ->columnSpanFull(), Repeater::make('server_variables') ->hiddenLabel() diff --git a/app/Filament/Components/Actions/PreviewStartupAction.php b/app/Filament/Components/Actions/PreviewStartupAction.php index 9782b547b..a7d7b4a45 100644 --- a/app/Filament/Components/Actions/PreviewStartupAction.php +++ b/app/Filament/Components/Actions/PreviewStartupAction.php @@ -15,19 +15,17 @@ class PreviewStartupAction extends Action return 'preview'; } - public function getLabel(): string - { - return trans('server/startup.preview'); - } - protected function setUp(): void { parent::setUp(); + $this->label(fn (Get $get) => $get('previewing') ? trans('server/startup.disable_preview') : trans('server/startup.enable_preview')); + $this->action(function (Get $get, Set $set, Server $server) { - $active = $get('previewing'); - $set('previewing', !$active); - $set('startup', $active ? $server->startup : fn (Server $server, StartupCommandService $service) => $service->handle($server)); + $previewing = !$get('previewing'); + + $set('previewing', $previewing); + $set('startup', !$previewing ? $server->startup : fn (Server $server, StartupCommandService $service) => $service->handle($server, $server->startup)); }); } } diff --git a/app/Filament/Server/Pages/Startup.php b/app/Filament/Server/Pages/Startup.php index 887d0feda..386df191c 100644 --- a/app/Filament/Server/Pages/Startup.php +++ b/app/Filament/Server/Pages/Startup.php @@ -17,6 +17,7 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Validator; @@ -35,35 +36,51 @@ class Startup extends ServerFormPage return parent::form($schema) ->columns([ 'default' => 1, - 'sm' => 1, - 'md' => 4, - 'lg' => 6, + 'md' => 2, ]) ->components([ Hidden::make('previewing') ->default(false), - Textarea::make('startup') + TextInput::make('custom_startup') ->label(trans('server/startup.command')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 4, - ]) - ->autosize() - ->hintAction(PreviewStartupAction::make()) - ->readOnly(), + ->readOnly() + ->visible(fn (Server $server) => !in_array($server->startup, $server->egg->startup_commands)) + ->formatStateUsing(fn () => 'Custom Startup') + ->hintAction(PreviewStartupAction::make()), + Select::make('startup_select') + ->label(trans('server/startup.command')) + ->live() + ->visible(fn (Server $server) => in_array($server->startup, $server->egg->startup_commands)) + ->disabled(fn (Server $server) => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) + ->formatStateUsing(fn (Server $server) => $server->startup) + ->afterStateUpdated(function ($state, Server $server, Set $set) { + $original = $server->startup; + $server->forceFill(['startup' => $state])->saveOrFail(); + + $set('startup', $state); + $set('previewing', false); + + if ($original !== $server->startup) { + $startups = array_flip($server->egg->startup_commands); + Activity::event('server:startup.command') + ->property(['old' => $startups[$original], 'new' => $startups[$state]]) + ->log(); + } + + Notification::make() + ->title(trans('server/startup.notification_startup')) + ->body(trans('server/startup.notification_startup_body')) + ->success() + ->send(); + }) + ->options(fn (Server $server) => array_flip($server->egg->startup_commands)) + ->selectablePlaceholder(false) + ->hintAction(PreviewStartupAction::make()), TextInput::make('custom_image') ->label(trans('server/startup.docker_image')) ->readOnly() ->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images)) - ->formatStateUsing(fn (Server $server) => $server->image) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), + ->formatStateUsing(fn (Server $server) => $server->image), Select::make('image') ->label(trans('server/startup.docker_image')) ->live() @@ -89,14 +106,12 @@ class Startup extends ServerFormPage $images = $server->egg->docker_images; return array_flip($images); - }) - ->selectablePlaceholder(false) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), + }), + Textarea::make('startup') + ->hiddenLabel() + ->columnSpanFull() + ->autosize() + ->readOnly(), Section::make(trans('server/startup.variables')) ->columnSpanFull() ->schema([ diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 709a80535..103e82eec 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -22,8 +22,7 @@ use Illuminate\Support\Str; * @property string $name * @property string|null $description * @property string[]|null $features - * @property string $docker_image -- deprecated, use $docker_images - * @property array $docker_images + * @property array $docker_images * @property string|null $update_url * @property bool $force_outgoing_ip * @property string[]|null $file_denylist @@ -32,7 +31,7 @@ use Illuminate\Support\Str; * @property string|null $config_logs * @property string|null $config_stop * @property int|null $config_from - * @property string|null $startup + * @property array $startup_commands * @property bool $script_is_privileged * @property string|null $script_install * @property string $script_entry @@ -71,7 +70,7 @@ class Egg extends Model implements Validatable /** * Defines the current egg export version. */ - public const EXPORT_VERSION = 'PLCN_v2'; + public const EXPORT_VERSION = 'PLCN_v3'; /** * Fields that are not mass assignable. @@ -90,7 +89,7 @@ class Egg extends Model implements Validatable 'config_logs', 'config_stop', 'config_from', - 'startup', + 'startup_commands', 'update_url', 'script_is_privileged', 'script_install', @@ -111,7 +110,8 @@ class Egg extends Model implements Validatable 'file_denylist.*' => ['string'], 'docker_images' => ['required', 'array', 'min:1'], 'docker_images.*' => ['required', 'string'], - 'startup' => ['required', 'nullable', 'string'], + 'startup_commands' => ['required', 'array', 'min:1'], + 'startup_commands.*' => ['required', 'string', 'distinct'], 'config_from' => ['sometimes', 'bail', 'nullable', 'numeric', 'exists:eggs,id'], 'config_stop' => ['required_without:config_from', 'nullable', 'string', 'max:255'], 'config_startup' => ['required_without:config_from', 'nullable', 'json'], @@ -143,6 +143,7 @@ class Egg extends Model implements Validatable 'features' => 'array', 'docker_images' => 'array', 'file_denylist' => 'array', + 'startup_commands' => 'array', 'tags' => 'array', ]; } diff --git a/app/Services/Eggs/EggChangerService.php b/app/Services/Eggs/EggChangerService.php index 3b0f30bf6..52abad1dc 100644 --- a/app/Services/Eggs/EggChangerService.php +++ b/app/Services/Eggs/EggChangerService.php @@ -5,6 +5,7 @@ namespace App\Services\Eggs; use App\Models\Egg; use App\Models\Server; use App\Models\ServerVariable; +use Illuminate\Support\Arr; class EggChangerService { @@ -21,8 +22,8 @@ class EggChangerService // Change egg id, default image and startup command $server->forceFill([ 'egg_id' => $newEgg->id, - 'image' => array_values($newEgg->docker_images)[0], - 'startup' => $newEgg->startup, + 'image' => Arr::first($newEgg->docker_images), + 'startup' => Arr::first($newEgg->startup_commands), ])->saveOrFail(); $oldVariables = []; diff --git a/app/Services/Eggs/Sharing/EggExporterService.php b/app/Services/Eggs/Sharing/EggExporterService.php index ce9d6d152..339da8475 100644 --- a/app/Services/Eggs/Sharing/EggExporterService.php +++ b/app/Services/Eggs/Sharing/EggExporterService.php @@ -33,7 +33,7 @@ class EggExporterService 'features' => $egg->features, 'docker_images' => $egg->docker_images, 'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(fn ($v) => !empty($v))->values(), - 'startup' => $egg->startup, + 'startup_commands' => $egg->startup_commands, 'config' => [ 'files' => $egg->inherit_config_files, 'startup' => $egg->inherit_config_startup, diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 18e0ad1bc..de3054a5f 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -133,8 +133,9 @@ class EggImporterService $version = $parsed['meta']['version'] ?? ''; $parsed = match ($version) { - 'PTDL_v1' => $this->convertToV2($parsed), - 'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $parsed, + 'PTDL_v1' => $this->convertToV3($this->convertLegacy($parsed)), + 'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $this->convertToV3($parsed), + Egg::EXPORT_VERSION => $parsed, default => throw new InvalidFileUploadException('The file format is not recognized.'), }; @@ -180,9 +181,9 @@ class EggImporterService if ($forbidden->count()) { $parsed['variables'] = $allowed->merge($updatedVariables)->all(); - if (!empty($parsed['startup'])) { + foreach ($parsed['startup_commands'] ?? [] as $name => $startup) { $pattern = '/\b(' . collect($forbidden)->map(fn ($variable) => preg_quote($variable['env_variable']))->join('|') . ')\b/'; - $parsed['startup'] = preg_replace($pattern, 'SERVER_$1', $parsed['startup']) ?? $parsed['startup']; + $parsed['startup_commands'][$name] = preg_replace($pattern, 'SERVER_$1', $startup) ?? $startup; } } @@ -206,7 +207,7 @@ class EggImporterService 'config_startup' => json_encode(json_decode(Arr::get($parsed, 'config.startup')), JSON_PRETTY_PRINT), 'config_logs' => json_encode(json_decode(Arr::get($parsed, 'config.logs')), JSON_PRETTY_PRINT), 'config_stop' => Arr::get($parsed, 'config.stop'), - 'startup' => Arr::get($parsed, 'startup'), + 'startup_commands' => Arr::get($parsed, 'startup_commands'), 'script_install' => Arr::get($parsed, 'scripts.installation.script'), 'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'), 'script_container' => Arr::get($parsed, 'scripts.installation.container'), @@ -217,7 +218,7 @@ class EggImporterService * @param array $parsed * @return array */ - protected function convertToV2(array $parsed): array + protected function convertLegacy(array $parsed): array { if (!isset($parsed['images'])) { $images = [Arr::get($parsed, 'image') ?? 'nil']; @@ -234,4 +235,21 @@ class EggImporterService return $parsed; } + + /** + * @param array $parsed + * @return array + */ + protected function convertToV3(array $parsed): array + { + $startup = $parsed['startup']; + + unset($parsed['startup']); + + $parsed['startup_commands'] = [ + 'Default' => $startup, + ]; + + return $parsed; + } } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index a3719a997..f2483c5f6 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -61,8 +61,8 @@ class ServerCreationService $egg = Egg::query()->findOrFail($data['egg_id']); // Fill missing fields from egg - $data['image'] ??= collect($egg->docker_images)->first(); - $data['startup'] ??= $egg->startup; + $data['image'] ??= Arr::first($egg->docker_images); + $data['startup'] ??= Arr::first($egg->startup_commands); // If a deployment object has been passed we need to get the allocation and node that the server should use. if ($deployment) { diff --git a/app/Services/Servers/StartupCommandService.php b/app/Services/Servers/StartupCommandService.php index c29f3e8bb..6cc2af4af 100644 --- a/app/Services/Servers/StartupCommandService.php +++ b/app/Services/Servers/StartupCommandService.php @@ -9,8 +9,10 @@ class StartupCommandService /** * Generates a startup command for a given server instance. */ - public function handle(Server $server, bool $hideAllValues = false): string + public function handle(Server $server, ?string $startup = null, bool $hideAllValues = false): string { + $startup ??= $server->startup; + $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; $replace = [ (string) $server->memory, @@ -23,6 +25,6 @@ class StartupCommandService $replace[] = ($variable->user_viewable && !$hideAllValues) ? ($variable->server_value ?? $variable->default_value) : '[hidden]'; } - return str_replace($find, $replace, $server->startup); + return str_replace($find, $replace, $startup); } } diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 2e5267920..f83bc2a9d 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -85,8 +85,8 @@ class StartupModificationService ]); // Fill missing fields from egg - $data['docker_image'] = $data['docker_image'] ?? collect($egg->docker_images)->first(); - $data['startup'] = $data['startup'] ?? $egg->startup; + $data['docker_image'] ??= Arr::first($egg->docker_images); + $data['startup'] ??= Arr::first($egg->startup_commands); } $server->fill([ diff --git a/app/Transformers/Api/Application/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php index 0ae247194..548e1e70a 100644 --- a/app/Transformers/Api/Application/EggTransformer.php +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -48,10 +48,7 @@ class EggTransformer extends BaseTransformer 'description' => $model->description, 'features' => $model->features, 'tags' => $model->tags, - // "docker_image" is deprecated, but left here to avoid breaking too many things at once - // in external software. We'll remove it down the road once things have gotten the chance - // to upgrade to using "docker_images". - 'docker_image' => count($model->docker_images) > 0 ? Arr::first($model->docker_images) : '', + 'docker_image' => Arr::first($model->docker_images, default: ''), // docker_images, use startup_commands 'docker_images' => $model->docker_images, 'config' => [ 'files' => $files, @@ -61,7 +58,8 @@ class EggTransformer extends BaseTransformer 'file_denylist' => $model->inherit_file_denylist, 'extends' => $model->config_from, ], - 'startup' => $model->startup, + 'startup' => Arr::first($model->startup_commands, default: ''), // deprecated, use startup_commands + 'startup_commands' => $model->startup_commands, 'script' => [ 'privileged' => $model->script_is_privileged, 'install' => $model->copy_script_install, diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 8424db69e..0e52b0e9f 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -60,7 +60,7 @@ class ServerTransformer extends BaseClientTransformer 'oom_disabled' => !$server->oom_killer, 'oom_killer' => $server->oom_killer, ], - 'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)), + 'invocation' => $service->handle($server, hideAllValues: !$user->can(Permission::ACTION_STARTUP_READ, $server)), 'docker_image' => $server->image, 'egg_features' => $server->egg->inherit_features, 'feature_limits' => [ diff --git a/database/Factories/EggFactory.php b/database/Factories/EggFactory.php index 3f265c476..0a0d2a238 100644 --- a/database/Factories/EggFactory.php +++ b/database/Factories/EggFactory.php @@ -30,7 +30,7 @@ class EggFactory extends Factory 'config_files' => '{}', 'name' => $this->faker->name(), 'description' => implode(' ', $this->faker->sentences()), - 'startup' => 'java -jar test.jar', + 'startup_commands' => ['java -jar test.jar'], ]; } } diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.yaml b/database/Seeders/eggs/minecraft/egg-bungeecord.yaml index d079a6bf5..ed03ecf86 100644 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.yaml +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-bungeecord.yaml' -exported_at: '2025-07-25T13:32:34+00:00' +exported_at: '2025-09-05T08:54:34+00:00' name: Bungeecord author: panel@example.com uuid: 9e6b409e-4028-4947-aea8-50a2c404c271 @@ -26,7 +26,8 @@ docker_images: 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' file_denylist: { } -startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' +startup_commands: + Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' config: files: config.yml: diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml b/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml index ed8b3a5ff..008726b93 100644 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml' -exported_at: '2025-08-05T21:00:17+00:00' +exported_at: '2025-09-05T08:54:45+00:00' name: 'Forge Minecraft' author: panel@example.com uuid: ed072427-f209-4603-875c-f540c6dd5a65 @@ -22,7 +22,8 @@ docker_images: 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' file_denylist: { } -startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s "-jar {{SERVER_JARFILE}}" || printf %s "@unix_args.txt" )' +startup_commands: + Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s "-jar {{SERVER_JARFILE}}" || printf %s "@unix_args.txt" )' config: files: server.properties: diff --git a/database/Seeders/eggs/minecraft/egg-paper.yaml b/database/Seeders/eggs/minecraft/egg-paper.yaml index 0431c3373..a4db77af8 100644 --- a/database/Seeders/eggs/minecraft/egg-paper.yaml +++ b/database/Seeders/eggs/minecraft/egg-paper.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-paper.yaml' -exported_at: '2025-08-05T21:00:17+00:00' +exported_at: '2025-09-05T08:54:10+00:00' name: Paper author: parker@example.com uuid: 5da37ef6-58da-4169-90a6-e683e1721247 @@ -20,7 +20,8 @@ docker_images: 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' file_denylist: { } -startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}' +startup_commands: + Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}' config: files: server.properties: diff --git a/database/Seeders/eggs/minecraft/egg-sponge.yaml b/database/Seeders/eggs/minecraft/egg-sponge.yaml index 2fc5630e7..f24afab91 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge.yaml +++ b/database/Seeders/eggs/minecraft/egg-sponge.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 - update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.yaml' -exported_at: '2025-08-05T21:00:17+00:00' + version: PLCN_v3 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge.yaml' +exported_at: '2025-09-12T08:38:42+00:00' name: Sponge author: panel@example.com uuid: f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d @@ -20,7 +20,8 @@ docker_images: 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' file_denylist: { } -startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' +startup_commands: + Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' config: files: server.properties: diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml index 7328ca196..3f1408735 100644 --- a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml +++ b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml' -exported_at: '2025-08-05T20:59:51+00:00' +exported_at: '2025-09-05T08:55:10+00:00' name: 'Vanilla Minecraft' author: panel@example.com uuid: 9ac39f3d-0c34-4d93-8174-c52ab9e6c57b @@ -24,7 +24,8 @@ docker_images: 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' file_denylist: { } -startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' +startup_commands: + Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' config: files: server.properties: diff --git a/database/Seeders/eggs/rust/egg-rust.yaml b/database/Seeders/eggs/rust/egg-rust.yaml index f65fcb2ba..e080e864a 100644 --- a/database/Seeders/eggs/rust/egg-rust.yaml +++ b/database/Seeders/eggs/rust/egg-rust.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/rust/egg-rust.yaml' -exported_at: '2025-07-25T13:30:56+00:00' +exported_at: '2025-09-05T08:56:17+00:00' name: Rust author: panel@example.com uuid: bace2dfb-209c-452a-9459-7d6f340b07ae @@ -17,9 +17,10 @@ tags: features: - steam_disk_space docker_images: - 'ghcr.io/parkervcp/games:rust': 'ghcr.io/parkervcp/games:rust' + Rust: 'ghcr.io/parkervcp/games:rust' file_denylist: { } -startup: './RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity "rust" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \"{{SERVER_HOSTNAME}}\" +server.level \"{{LEVEL}}\" +server.description \"{{DESCRIPTION}}\" +server.url \"{{SERVER_URL}}\" +server.headerimage \"{{SERVER_IMG}}\" +server.logoimage \"{{SERVER_LOGO}}\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \"{{RCON_PASS}}\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s "+server.worldsize \"{{WORLD_SIZE}}\" +server.seed \"{{WORLD_SEED}}\"" || printf %s "+server.levelurl {{MAP_URL}}" ) {{ADDITIONAL_ARGS}}' +startup_commands: + Default: './RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity "rust" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \"{{SERVER_HOSTNAME}}\" +server.level \"{{LEVEL}}\" +server.description \"{{DESCRIPTION}}\" +server.url \"{{SERVER_URL}}\" +server.headerimage \"{{SERVER_IMG}}\" +server.logoimage \"{{SERVER_LOGO}}\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \"{{RCON_PASS}}\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s "+server.worldsize \"{{WORLD_SIZE}}\" +server.seed \"{{WORLD_SEED}}\"" || printf %s "+server.levelurl {{MAP_URL}}" ) {{ADDITIONAL_ARGS}}' config: files: { } startup: diff --git a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml index ee871c043..9ebe20c8d 100644 --- a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml +++ b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml' -exported_at: '2025-07-25T13:30:23+00:00' +exported_at: '2025-09-05T08:55:22+00:00' name: 'Custom Source Engine Game' author: panel@example.com uuid: 2a42d0c2-c0ba-4067-9a0a-9b95d77a3490 @@ -15,9 +15,10 @@ tags: features: - steam_disk_space docker_images: - 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' + Source: 'ghcr.io/parkervcp/games:source' file_denylist: { } -startup: './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart' +startup_commands: + Default: './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart' config: files: { } startup: diff --git a/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml b/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml index 7c8761599..547059ab6 100644 --- a/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml +++ b/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml' -exported_at: '2025-07-25T13:30:30+00:00' +exported_at: '2025-09-05T08:54:21+00:00' name: 'Garrys Mod' author: panel@example.com uuid: 60ef81d4-30a2-4d98-ab64-f59c69e2f915 @@ -16,9 +16,10 @@ features: - gsl_token - steam_disk_space docker_images: - 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' + Source: 'ghcr.io/parkervcp/games:source' file_denylist: { } -startup: './srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ "$LUA_REFRESH" == "1" ] || printf %s ''-disableluarefresh'' )' +startup_commands: + Default: './srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ "$LUA_REFRESH" == "1" ] || printf %s ''-disableluarefresh'' )' config: files: { } startup: diff --git a/database/Seeders/eggs/source-engine/egg-insurgency.yaml b/database/Seeders/eggs/source-engine/egg-insurgency.yaml index 0c5b0515b..e9be67468 100644 --- a/database/Seeders/eggs/source-engine/egg-insurgency.yaml +++ b/database/Seeders/eggs/source-engine/egg-insurgency.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-insurgency.yaml' -exported_at: '2025-07-25T13:30:35+00:00' +exported_at: '2025-09-05T08:55:34+00:00' name: Insurgency author: panel@example.com uuid: a5702286-655b-4069-bf1e-925c7300b61a @@ -16,9 +16,10 @@ tags: features: - steam_disk_space docker_images: - 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' + Source: 'ghcr.io/parkervcp/games:source' file_denylist: { } -startup: './srcds_run -game insurgency -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart' +startup_commands: + Default: './srcds_run -game insurgency -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart' config: files: { } startup: diff --git a/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml b/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml index dafcad0b3..e27c4df2e 100644 --- a/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml +++ b/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml' -exported_at: '2025-07-25T13:30:44+00:00' +exported_at: '2025-09-05T08:55:44+00:00' name: 'Team Fortress 2' author: panel@example.com uuid: 7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d @@ -16,9 +16,10 @@ features: - gsl_token - steam_disk_space docker_images: - 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' + Source: 'ghcr.io/parkervcp/games:source' file_denylist: { } -startup: './srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}' +startup_commands: + Default: './srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}' config: files: { } startup: diff --git a/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml b/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml index 6efe64c4c..2905f2289 100644 --- a/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml +++ b/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml' -exported_at: '2025-07-25T13:30:48+00:00' +exported_at: '2025-09-05T08:55:54+00:00' name: 'Mumble Server' author: panel@example.com uuid: 727ee758-7fb2-4979-972b-d3eba4e1e9f0 @@ -15,7 +15,8 @@ features: { } docker_images: Mumble: 'ghcr.io/parkervcp/yolks:voice_mumble' file_denylist: { } -startup: 'mumble-server -fg -ini murmur.ini' +startup_commands: + Default: 'mumble-server -fg -ini murmur.ini' config: files: murmur.ini: diff --git a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml index 15fe93290..eb4c925dd 100644 --- a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml +++ b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml @@ -1,8 +1,8 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' meta: - version: PLCN_v2 + version: PLCN_v3 update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml' -exported_at: '2025-07-25T13:30:53+00:00' +exported_at: '2025-09-05T08:56:06+00:00' name: 'Teamspeak3 Server' author: panel@example.com uuid: 983b1fac-d322-4d5f-a636-436127326b37 @@ -15,7 +15,8 @@ features: { } docker_images: Debian: 'ghcr.io/parkervcp/yolks:debian' file_denylist: { } -startup: './ts3server default_voice_port={{SERVER_PORT}} query_port={{QUERY_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} query_http_port={{QUERY_HTTP}} query_ssh_port={{QUERY_SSH}} query_protocols={{QUERY_PROTOCOLS_VAR}} license_accepted=1' +startup_commands: + Default: './ts3server default_voice_port={{SERVER_PORT}} query_port={{QUERY_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} query_http_port={{QUERY_HTTP}} query_ssh_port={{QUERY_SSH}} query_protocols={{QUERY_PROTOCOLS_VAR}} license_accepted=1' config: files: { } startup: diff --git a/database/migrations/2025_09_03_090706_support_multiple_startup_commands.php b/database/migrations/2025_09_03_090706_support_multiple_startup_commands.php new file mode 100644 index 000000000..4ff264769 --- /dev/null +++ b/database/migrations/2025_09_03_090706_support_multiple_startup_commands.php @@ -0,0 +1,50 @@ +json('startup_commands')->after('startup')->nullable(); + }); + + DB::table('eggs')->select(['id', 'startup'])->cursor()->each(function ($egg) { + DB::table('eggs')->where('id', $egg->id)->update(['startup_commands' => json_encode(['Default' => $egg->startup], JSON_UNESCAPED_SLASHES)]); + }); + + Schema::table('eggs', function (Blueprint $table) { + $table->dropColumn('startup'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::transaction(function () { + Schema::table('eggs', function (Blueprint $table) { + $table->text('startup')->after('startup_commands'); + }); + + DB::table('eggs')->select(['id', 'startup_commands'])->cursor()->each(function ($egg) { + DB::table('eggs')->where('id', $egg->id)->update([ + 'startup' => Arr::first(json_decode($egg->startup_commands, true, 512, JSON_THROW_ON_ERROR)), + ]); + }); + + Schema::table('eggs', function (Blueprint $table) { + $table->dropColumn('startup_commands'); + }); + }); + } +}; diff --git a/lang/en/activity.php b/lang/en/activity.php index a6f743dce..4b3cdb339 100644 --- a/lang/en/activity.php +++ b/lang/en/activity.php @@ -112,6 +112,7 @@ return [ 'startup' => [ 'edit' => 'Changed the :variable variable from ":old" to ":new"', 'image' => 'Updated the Docker Image for the server from :old to :new', + 'command' => 'Updated the Startup Command for the server from :old to :new', ], 'subuser' => [ 'create' => 'Added :email as a subuser', diff --git a/lang/en/admin/egg.php b/lang/en/admin/egg.php index 600c9cb4b..3f475718b 100644 --- a/lang/en/admin/egg.php +++ b/lang/en/admin/egg.php @@ -37,8 +37,11 @@ return [ 'author_help_edit' => 'The author of this version of the Egg. Uploading a new configuration from a different author will change this.', 'description' => 'Description', 'description_help' => 'A description of this Egg that will be displayed throughout the Panel as needed.', - 'startup' => 'Startup Command', - 'startup_help' => 'The default startup command that should be used for new servers using this Egg.', + 'add_startup' => 'Add Startup Command', + 'startup_command' => 'Command', + 'startup_commands' => 'Startup Commands', + 'startup_name' => 'Display Name', + 'startup_help' => 'The startup commands available to servers using this Egg. The first one is the default.', 'file_denylist' => 'File Denylist', 'file_denylist_help' => 'A list of files that the end user is not allowed to edit.', 'features' => 'Features', @@ -51,7 +54,7 @@ return [ 'docker_images' => 'Docker Images', 'docker_name' => 'Image Name', 'docker_uri' => 'Image URI', - 'docker_help' => 'The docker images available to servers using this Egg.', + 'docker_help' => 'The docker images available to servers using this Egg. The first one is the default.', 'stop_command' => 'Stop Command', 'stop_command_help' => 'The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.', diff --git a/lang/en/admin/server.php b/lang/en/admin/server.php index a3ebb092f..cd74c4159 100644 --- a/lang/en/admin/server.php +++ b/lang/en/admin/server.php @@ -26,7 +26,9 @@ return [ 'already_primary' => 'Already Primary', 'make_primary' => 'Make Primary', 'startup_cmd' => 'Startup Command', + 'startup_name' => 'Startup Name', 'default_startup' => 'Default Startup Command', + 'startup_placeholder' => 'Enter a custom startup command', 'variables' => 'Variables', 'resource_limits' => 'Resource Limits', 'cpu' => 'CPU', diff --git a/lang/en/server/startup.php b/lang/en/server/startup.php index 030ffc738..49ff30f7e 100644 --- a/lang/en/server/startup.php +++ b/lang/en/server/startup.php @@ -3,7 +3,10 @@ return [ 'title' => 'Startup', 'command' => 'Startup Command', - 'preview' => 'Preview', + 'notification_startup' => 'Startup Command Updated', + 'notification_startup_body' => 'Restart the server to use the new startup command.', + 'enable_preview' => 'Enable Preview', + 'disable_preview' => 'Disable Preview', 'docker_image' => 'Docker Image', 'notification_docker' => 'Docker Image Updated', 'notification_docker_body' => 'Restart the server to use the new image.',