diff --git a/nimbus-proxy.kvp b/nimbus-proxy.kvp new file mode 100644 index 000000000..bf0af64f1 --- /dev/null +++ b/nimbus-proxy.kvp @@ -0,0 +1,96 @@ +Meta.DisplayName=Nimbus Proxy +Meta.Description=Player-facing Nimbus proxy with an embedded Registry by default +Meta.OS=Windows, Linux +Meta.AarchSupport=None +Meta.Arch=x86_64 +Meta.Author=HuguesDePayens +Meta.URL=https://github.com/StratumServer/Nimbus +Meta.DisplayImageSource=url:https://wiki.vintagestory.at/images/thumb/8/8a/Gamelogo-vintagestory.png/340px-Gamelogo-vintagestory.png +Meta.EndpointURIFormat= +Meta.ConfigManifest=nimbus-proxyconfig.json +Meta.MetaConfigManifest= +Meta.ConfigRoot=nimbus-proxy.kvp +Meta.MinAMPVersion=2.4.6.0 +Meta.SpecificDockerImage=cubecoders/ampbase:debian +Meta.DockerRequired=False +Meta.ContainerPolicy=Supported +Meta.ExtraContainerPackages=["jq","unzip","curl","ca-certificates"] +Meta.Prerequisites=[] +Meta.ConfigReleaseState=NotSpecified +Meta.NoCommercialUsage=False +Meta.ConfigVersion=1 +Meta.AppConfigId=6ce2a583-1448-46f5-9f9d-b505d5eab311 +Meta.AppIsMultiIPAware=True +Meta.ResourceUsageInfo=Lightweight network service; size the backend servers separately +Meta.ImportableExtensions=[] +App.DisplayName=Nimbus Proxy +App.RootDir=./nimbus/ +App.BaseDirectory=./nimbus/ +App.ExecutableLinux=dotnet/dotnet +App.ExecutableWin=dotnet\dotnet.exe +App.WorkingDir=Nimbus +App.LinuxCommandLineArgs="{{$FullRootDir}}Nimbus/Nimbus.Proxy.dll" +App.WindowsCommandLineArgs="{{$FullRootDir}}Nimbus\Nimbus.Proxy.dll" +App.CommandLineArgs={{$PlatformArgs}} +App.UseLinuxIOREDIR=False +App.AppSettings={} +App.EnvironmentVariables={"DOTNET_ROOT":"{{$FullRootDir}}dotnet","DOTNET_CLI_TELEMETRY_OPTOUT":"1","DOTNET_NOLOGO":"1"} +App.CommandLineParameterFormat={0} {1} +App.CommandLineParameterDelimiter= +App.ExitMethod=OS_CLOSE +App.ExitTimeout=30 +App.ExitString= +App.ExitFile= +App.HasWriteableConsole=False +App.HasReadableConsole=True +App.SupportsLiveSettingsChanges=False +App.LiveSettingChangeCommandFormat= +App.ApplicationIPBinding=0.0.0.0 +App.Ports=@IncludeJson[nimbus-proxyports.json] +App.AdminPortRef=AdminPort +App.PrimaryApplicationPortRef=ServerPort +App.UniversalSleepApplicationUDPPortRef=ServerPort +App.UniversalSleepSteamQueryPortRef=ServerPort +App.MaxUsers={{$MaxUsers}} +App.UseRandomAdminPassword=False +App.RemoteAdminPassword= +App.AdminMethod=STDIO +App.AdminLoginTransform=None +App.RCONConnectDelaySeconds=5 +App.RCONConnectRetrySeconds=5 +App.RCONHeartbeatMinutes=0 +App.RCONHeartbeatCommand= +App.TelnetLoginFormat={0} +App.PreStartStages=@IncludeJson[nimbus-proxystart.json] +App.UpdateSources=@IncludeJson[nimbus-proxyupdates.json] +App.Compatibility=None +App.SteamUpdateAnonymousLogin=True +App.SteamForceLoginPrompt=False +App.RapidStartup=False +App.SmartExcludeSupported=True +App.SmartExcludeExemptions=["*.toml","*.json","plugins/*"] +App.MonitorChildProcess=True +App.MonitorChildProcessWaitMs=100 +App.MonitorChildProcessName= +App.SupportsUniversalSleep=False +App.WakeupMode=Any +App.ApplicationReadyMode=RegexMatch +App.StripANSIControlCodes=True +Console.FilterMatchRegex= +Console.FilterMatchReplacement= +Console.ThrowawayMessageRegex= +Console.AppReadyRegex=^[\d:]+ \[nimbus\] listening on .+ -> (?:pool of \d+ backend\(s\)|backend .+)$ +Console.UserJoinRegex= +Console.UserLeaveRegex= +Console.UserChatRegex= +Console.UpdateAvailableRegex= +Console.MetricsRegex= +Console.SuppressLogAtStart=False +Console.ActivateLogRegex= +Console.UserActions={} +Limits.SleepMode=False +Limits.SleepOnStart=False +Limits.SleepDelayMinutes=5 +Limits.DozeDelay=2 +Limits.AutoRetryCount=2 +Limits.SleepStartThresholdSeconds=25 diff --git a/nimbus-proxyconfig.json b/nimbus-proxyconfig.json new file mode 100644 index 000000000..91d5d6095 --- /dev/null +++ b/nimbus-proxyconfig.json @@ -0,0 +1,20 @@ +[ + {"DisplayName":"Player Port","Category":"Network:share","Subcategory":"Ports:dns:1","Description":"Public player-facing Vintage Story port.","Keywords":"player,port","FieldName":"$ServerPort","InputType":"number","Hidden":true,"ParamFieldName":"ServerPort","IncludeInCommandLine":false,"DefaultValue":"42420","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Registry Port","Category":"Network:share","Subcategory":"Ports:dns:1","Description":"Embedded Registry HTTP port used by backends.","Keywords":"registry,port","FieldName":"$RegistryPort","InputType":"number","Hidden":true,"ParamFieldName":"RegistryPort","IncludeInCommandLine":false,"DefaultValue":"8765","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Admin Port","Category":"Network:share","Subcategory":"Ports:dns:1","Description":"Loopback administration port.","Keywords":"admin,port","FieldName":"$AdminPort","InputType":"number","Hidden":true,"ParamFieldName":"AdminPort","IncludeInCommandLine":false,"DefaultValue":"42499","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Metrics Port","Category":"Network:share","Subcategory":"Ports:dns:1","Description":"Loopback metrics and status API port.","Keywords":"metrics,status,port","FieldName":"$MetricsPort","InputType":"number","Hidden":true,"ParamFieldName":"MetricsPort","IncludeInCommandLine":false,"DefaultValue":"42500","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Nimbus Release Tag","Category":"Updates:update","Subcategory":"Version:verified_user:1","Description":"Optional exact Nimbus release tag. Leave blank for the latest stable release.","Keywords":"nimbus,version","FieldName":"NimbusVersionTag","InputType":"text","ParamFieldName":"NimbusVersionTag","IncludeInCommandLine":false,"DefaultValue":"","Placeholder":"v0.5.0","EnumValues":{}}, + {"DisplayName":"Backend Servers","Category":"Routing:swap_horiz","Subcategory":"Backends:dns:1","Description":"Semicolon-separated id=host:port map. Each ID must match that backend's Nimbus Server ID.","Keywords":"backend,servers,map","FieldName":"ProxyServers","InputType":"text","ParamFieldName":"ProxyServers","IncludeInCommandLine":false,"Required":true,"DefaultValue":"hub=127.0.0.1:42421","Placeholder":"hub=127.0.0.1:42421;survival=127.0.0.1:42422","EnumValues":{}}, + {"DisplayName":"Try Order","Category":"Routing:swap_horiz","Subcategory":"Backends:dns:1","Description":"Comma-separated backend IDs tried for a new connection.","Keywords":"backend,order","FieldName":"ProxyTryList","InputType":"text","ParamFieldName":"ProxyTryList","IncludeInCommandLine":false,"Required":true,"DefaultValue":"hub","EnumValues":{}}, + {"DisplayName":"Registry Mode","Category":"Registry:hub","Subcategory":"Topology:account_tree:1","Description":"Embedded is recommended for one Proxy. Remote uses the optional standalone Registry.","Keywords":"registry,mode","FieldName":"ProxyRegistryMode","InputType":"enum","ParamFieldName":"ProxyRegistryMode","IncludeInCommandLine":false,"DefaultValue":"embedded","EnumValues":{"embedded":"Embedded (recommended)","remote":"Remote standalone Registry"}}, + {"DisplayName":"Embedded Registry Access","Category":"Registry:hub","Subcategory":"Topology:account_tree:1","Description":"Same host binds Registry to loopback. Other hosts exposes it on all instance interfaces.","Keywords":"registry,access","FieldName":"ProxyRegistryAccess","InputType":"enum","ParamFieldName":"ProxyRegistryAccess","IncludeInCommandLine":false,"DefaultValue":"same-host","EnumValues":{"same-host":"Backends on same host","other-hosts":"Backends on other hosts"}}, + {"DisplayName":"Remote Registry URL","Category":"Registry:hub","Subcategory":"Topology:account_tree:1","Description":"Required only in remote mode, for example http://registry-host:8765.","Keywords":"registry,url","FieldName":"ProxyRegistryUrl","InputType":"text","ParamFieldName":"ProxyRegistryUrl","IncludeInCommandLine":false,"DefaultValue":"","Placeholder":"http://registry-host:8765","EnumValues":{}}, + {"DisplayName":"Registry Shared Secret","Category":"Registry:hub","Subcategory":"Security:lock:2","Description":"Blank generates and then preserves a secret in embedded mode. Remote mode requires the standalone Registry secret.","Keywords":"registry,secret","FieldName":"ProxySharedSecret","InputType":"password","ParamFieldName":"ProxySharedSecret","IncludeInCommandLine":false,"DefaultValue":"","EnumValues":{}}, + {"DisplayName":"Proxy ID","Category":"Registry:hub","Subcategory":"Topology:account_tree:1","Description":"Stable identifier for this Proxy in the Registry.","Keywords":"proxy,id","FieldName":"ProxyId","InputType":"text","ParamFieldName":"ProxyId","IncludeInCommandLine":false,"Required":true,"DefaultValue":"nimbus-proxy","EnumValues":{}}, + {"DisplayName":"Network Name","Category":"Status:info","Subcategory":"Server List:list:1","Description":"Name returned by the Proxy status response.","Keywords":"status,name","FieldName":"ProxyStatusName","InputType":"text","ParamFieldName":"ProxyStatusName","IncludeInCommandLine":false,"DefaultValue":"Nimbus","EnumValues":{}}, + {"DisplayName":"Network Message","Category":"Status:info","Subcategory":"Server List:list:1","Description":"Message returned by the Proxy status response.","Keywords":"status,motd","FieldName":"ProxyStatusMotd","InputType":"text","ParamFieldName":"ProxyStatusMotd","IncludeInCommandLine":false,"DefaultValue":"Vintage Story proxy","EnumValues":{}}, + {"DisplayName":"Player Limit","Category":"Status:info","Subcategory":"Server List:list:1","Description":"Maximum player count reported by the Proxy.","Keywords":"players,limit","FieldName":"$MaxUsers","InputType":"number","ParamFieldName":"MaxUsers","IncludeInCommandLine":false,"DefaultValue":"100","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Network Whitelist","Category":"Access:lock","Subcategory":"Whitelist:verified_user:1","Description":"Require Registry-synchronized whitelist access across the network.","Keywords":"network,whitelist","FieldName":"ProxyWhitelistNetwork","InputType":"checkbox","ParamFieldName":"ProxyWhitelistNetwork","IncludeInCommandLine":false,"DefaultValue":"false","EnumValues":{"True":"true","False":"false"}}, + {"DisplayName":"Whitelisted Backends","Category":"Access:lock","Subcategory":"Whitelist:verified_user:1","Description":"Optional comma-separated backend IDs subject to the network whitelist.","Keywords":"backend,whitelist","FieldName":"ProxyWhitelistServers","InputType":"text","ParamFieldName":"ProxyWhitelistServers","IncludeInCommandLine":false,"DefaultValue":"","EnumValues":{}}, + {"DisplayName":"Verbose Logging","Category":"Logging:description","Subcategory":"Detail:manage_search:1","Description":"Enable extra Nimbus diagnostic messages. Packet sniffing and traffic-byte logs remain off.","Keywords":"verbose,logging","FieldName":"ProxyVerboseLogging","InputType":"checkbox","ParamFieldName":"ProxyVerboseLogging","IncludeInCommandLine":false,"DefaultValue":"false","EnumValues":{"True":"true","False":"false"}} +] diff --git a/nimbus-proxyports.json b/nimbus-proxyports.json new file mode 100644 index 000000000..853e5a44a --- /dev/null +++ b/nimbus-proxyports.json @@ -0,0 +1,6 @@ +[ + {"Protocol":"Both","Name":"Player Port","Description":"Public Vintage Story port players connect to","Port":42420,"Ref":"ServerPort"}, + {"Protocol":"TCP","Name":"Registry Port","Description":"Embedded Registry HTTP endpoint used by backends","Port":8765,"Ref":"RegistryPort"}, + {"Protocol":"TCP","Name":"Admin Port","Description":"Loopback Nimbus administration endpoint","Port":42499,"Ref":"AdminPort"}, + {"Protocol":"TCP","Name":"Metrics and Status Port","Description":"Loopback metrics and status endpoint","Port":42500,"Ref":"MetricsPort"} +] diff --git a/nimbus-proxystart.json b/nimbus-proxystart.json new file mode 100644 index 000000000..578909085 --- /dev/null +++ b/nimbus-proxystart.json @@ -0,0 +1,18 @@ +[ + { + "UpdateStageName":"Render Nimbus Proxy configuration", + "UpdateSourcePlatform":"Linux", + "UpdateSource":"Executable", + "UpdateSourceData":"/bin/bash", + "UpdateSourceArgs":"-c \"NP_BIND_ADDRESS='{{$ApplicationIPBinding}}' NP_SERVER_PORT='{{$ServerPort}}' NP_REGISTRY_PORT='{{$RegistryPort}}' NP_ADMIN_PORT='{{$AdminPort}}' NP_METRICS_PORT='{{$MetricsPort}}' NP_SERVERS='{{ProxyServers}}' NP_TRY='{{ProxyTryList}}' NP_REGISTRY_MODE='{{ProxyRegistryMode}}' NP_REGISTRY_ACCESS='{{ProxyRegistryAccess}}' NP_REGISTRY_URL='{{ProxyRegistryUrl}}' NP_SHARED_SECRET='{{ProxySharedSecret}}' NP_PROXY_ID='{{ProxyId}}' NP_STATUS_NAME='{{ProxyStatusName}}' NP_STATUS_MOTD='{{ProxyStatusMotd}}' NP_MAX_USERS='{{$MaxUsers}}' NP_WHITELIST_NETWORK='{{ProxyWhitelistNetwork}}' NP_WHITELIST_SERVERS='{{ProxyWhitelistServers}}' NP_VERBOSE='{{ProxyVerboseLogging}}' bash '{{$FullRootDir}}scripts/write-proxy-toml.sh' '{{$FullRootDir}}Nimbus/nimbus.proxy.toml'\"", + "SkipOnFailure":false + }, + { + "UpdateStageName":"Render Nimbus Proxy configuration", + "UpdateSourcePlatform":"Windows", + "UpdateSource":"Executable", + "UpdateSourceData":"powershell.exe", + "UpdateSourceArgs":"-NoProfile -ExecutionPolicy Bypass -Command \"$env:NP_BIND_ADDRESS='{{$ApplicationIPBinding}}'; $env:NP_SERVER_PORT='{{$ServerPort}}'; $env:NP_REGISTRY_PORT='{{$RegistryPort}}'; $env:NP_ADMIN_PORT='{{$AdminPort}}'; $env:NP_METRICS_PORT='{{$MetricsPort}}'; $env:NP_SERVERS='{{ProxyServers}}'; $env:NP_TRY='{{ProxyTryList}}'; $env:NP_REGISTRY_MODE='{{ProxyRegistryMode}}'; $env:NP_REGISTRY_ACCESS='{{ProxyRegistryAccess}}'; $env:NP_REGISTRY_URL='{{ProxyRegistryUrl}}'; $env:NP_SHARED_SECRET='{{ProxySharedSecret}}'; $env:NP_PROXY_ID='{{ProxyId}}'; $env:NP_STATUS_NAME='{{ProxyStatusName}}'; $env:NP_STATUS_MOTD='{{ProxyStatusMotd}}'; $env:NP_MAX_USERS='{{$MaxUsers}}'; $env:NP_WHITELIST_NETWORK='{{ProxyWhitelistNetwork}}'; $env:NP_WHITELIST_SERVERS='{{ProxyWhitelistServers}}'; $env:NP_VERBOSE='{{ProxyVerboseLogging}}'; & '{{$FullRootDir}}scripts\\write-proxy-toml.ps1' '{{$FullRootDir}}Nimbus\\nimbus.proxy.toml'\"", + "SkipOnFailure":false + } +] diff --git a/nimbus-proxyupdates.json b/nimbus-proxyupdates.json new file mode 100644 index 000000000..c31412784 --- /dev/null +++ b/nimbus-proxyupdates.json @@ -0,0 +1,75 @@ +[ + { + "UpdateStageName": "Create dotnet directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}dotnet", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create Nimbus directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}Nimbus", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create scripts directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}scripts", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install local .NET 10 runtime", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; if [ -x \\\"$root/dotnet/dotnet\\\" ] \u0026\u0026 \\\"$root/dotnet/dotnet\\\" --list-runtimes | grep -q \u0027^Microsoft.AspNetCore.App 10\\.\u0027; then echo \u0027[AMP] .NET 10 ASP.NET Core runtime already installed\u0027; else tmp=\\\"$(mktemp)\\\"; curl -fsSL https://dot.net/v1/dotnet-install.sh -o \\\"$tmp\\\"; bash \\\"$tmp\\\" --channel 10.0 --runtime aspnetcore --install-dir \\\"$root/dotnet\\\" --no-path; rm -f \\\"$tmp\\\"; fi\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install local .NET 10 runtime", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $root=\u0027{{$FullRootDir}}\u0027; $dotnet=Join-Path $root \u0027dotnet\\dotnet.exe\u0027; $ready=$false; if (Test-Path -LiteralPath $dotnet) { $ready=[bool]((\u0026 $dotnet --list-runtimes) -match \u0027^Microsoft.AspNetCore.App 10\\.\u0027) }; if ($ready) { Write-Output \u0027[AMP] .NET 10 ASP.NET Core runtime already installed\u0027 } else { $script=Join-Path $env:TEMP (\u0027dotnet-install-\u0027+[guid]::NewGuid().ToString(\u0027N\u0027)+\u0027.ps1\u0027); try { Invoke-WebRequest -UseBasicParsing -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile $script; \u0026 $script -Channel 10.0 -Runtime aspnetcore -InstallDir (Join-Path $root \u0027dotnet\u0027) -NoPath } finally { Remove-Item -LiteralPath $script -Force -ErrorAction SilentlyContinue } }\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Nimbus Proxy", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; tag=\u0027{{NimbusVersionTag}}\u0027; if [ -z \\\"$tag\\\" ]; then tag=\\\"$(curl -fsSL -H \u0027User-Agent: AMP-Clean-Template\u0027 https://api.github.com/repos/StratumServer/Nimbus/releases/latest | jq -r .tag_name)\\\"; fi; case \\\"$tag\\\" in v*) ;; *) tag=\\\"v$tag\\\";; esac; stage=\\\"$root/.amp-nimbus-stage\\\"; archive=\\\"$root/.amp-nimbus.zip\\\"; old=\\\"$root/.amp-nimbus-old\\\"; rm -rf \\\"$stage\\\" \\\"$old\\\"; mkdir -p \\\"$stage\\\"; curl -fL --retry 3 -o \\\"$archive\\\" \\\"https://github.com/StratumServer/Nimbus/releases/download/$tag/Nimbus-$tag.zip\\\"; unzip -q \\\"$archive\\\" -d \\\"$stage\\\"; rm -f \\\"$archive\\\"; fresh_dll=\\\"$(find \\\"$stage\\\" -type f -name Nimbus.Proxy.dll -print -quit)\\\"; [ -n \\\"$fresh_dll\\\" ]; fresh=\\\"$(dirname \\\"$fresh_dll\\\")\\\"; if [ -d \\\"$root/Nimbus/plugins\\\" ]; then rm -rf \\\"$fresh/plugins\\\"; cp -a \\\"$root/Nimbus/plugins\\\" \\\"$fresh/plugins\\\"; fi; if [ -f \\\"$root/Nimbus/nimbus.proxy.toml\\\" ]; then cp \\\"$root/Nimbus/nimbus.proxy.toml\\\" \\\"$fresh/nimbus.proxy.toml\\\"; fi; if [ -d \\\"$root/Nimbus\\\" ]; then mv \\\"$root/Nimbus\\\" \\\"$old\\\"; fi; mv \\\"$fresh\\\" \\\"$root/Nimbus\\\"; rm -rf \\\"$stage\\\" \\\"$old\\\"; printf \u0027%s\u0027 \\\"$tag\\\" \u003e \\\"$root/Nimbus/.amp-nimbus-release\\\"\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Nimbus Proxy", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $ProgressPreference=\u0027SilentlyContinue\u0027; $root=\u0027{{$FullRootDir}}\u0027; $tag=\u0027{{NimbusVersionTag}}\u0027.Trim(); $headers=@{\u0027User-Agent\u0027=\u0027AMP-Clean-Template\u0027}; if (-not $tag) { $tag=(Invoke-RestMethod -Headers $headers -Uri https://api.github.com/repos/StratumServer/Nimbus/releases/latest).tag_name }; if (-not $tag.StartsWith(\u0027v\u0027)) { $tag=\u0027v\u0027+$tag }; $stage=Join-Path $root \u0027.amp-nimbus-stage\u0027; $archive=Join-Path $root \u0027.amp-nimbus.zip\u0027; $target=Join-Path $root \u0027Nimbus\u0027; $old=Join-Path $root \u0027.amp-nimbus-old\u0027; Remove-Item -LiteralPath $stage,$old,$archive -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $stage | Out-Null; Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri (\u0027https://github.com/StratumServer/Nimbus/releases/download/\u0027+$tag+\u0027/Nimbus-\u0027+$tag+\u0027.zip\u0027) -OutFile $archive; Expand-Archive -LiteralPath $archive -DestinationPath $stage -Force; Remove-Item -LiteralPath $archive -Force; $freshDll=Get-ChildItem -LiteralPath $stage -Filter \u0027Nimbus.Proxy.dll\u0027 -File -Recurse | Select-Object -First 1; if ($null -eq $freshDll) { throw \u0027Nimbus archive did not contain Nimbus.Proxy.dll\u0027 }; $fresh=$freshDll.Directory.FullName; if (Test-Path -LiteralPath (Join-Path $target \u0027plugins\u0027)) { Copy-Item -LiteralPath (Join-Path $target \u0027plugins\u0027) -Destination (Join-Path $fresh \u0027plugins\u0027) -Recurse -Force }; if (Test-Path -LiteralPath (Join-Path $target \u0027nimbus.proxy.toml\u0027)) { Copy-Item -LiteralPath (Join-Path $target \u0027nimbus.proxy.toml\u0027) -Destination (Join-Path $fresh \u0027nimbus.proxy.toml\u0027) -Force }; if (Test-Path -LiteralPath $target) { Move-Item -LiteralPath $target -Destination $old }; try { Move-Item -LiteralPath $fresh -Destination $target; Remove-Item -LiteralPath $stage,$old -Recurse -Force -ErrorAction SilentlyContinue } catch { if ((Test-Path -LiteralPath $old) -and -not (Test-Path -LiteralPath $target)) { Move-Item -LiteralPath $old -Destination $target }; throw }; [IO.File]::WriteAllText((Join-Path $target \u0027.amp-nimbus-release\u0027),$tag,(New-Object Text.UTF8Encoding $false))\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Deploy Proxy renderer (Linux)", + "UpdateSourcePlatform": "Linux", + "UpdateSource": "CreateFile", + "UpdateSourceArgs": "{{$FullRootDir}}scripts/write-proxy-toml.sh", + "UpdateSourceData": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${1:?target path required}\"\n\nTomlQuote() {\n local value=\"${1-}\"\n value=${value//\\/\\\\}\n value=${value//\\\"/\\\\\\\"}\n value=${value//$\u0027\\r\u0027/\\\\r}\n value=${value//$\u0027\\n\u0027/\\\\n}\n value=${value//$\u0027\\t\u0027/\\\\t}\n printf \u0027\"%s\"\u0027 \"$value\"\n}\n\nParseBackendMap() {\n BACKEND_LINES=\"\"\n BACKEND_IDS=\"\"\n local raw entry id endpoint host port\n IFS=\u0027;\u0027 read -ra entries \u003c\u003c\u003c \"${NP_SERVERS:-hub=127.0.0.1:42421}\"\n for raw in \"${entries[@]}\"; do\n entry=\"$(printf \u0027%s\u0027 \"$raw\" | sed \u0027s/^[[:space:]]*//;s/[[:space:]]*$//\u0027)\"\n [ -z \"$entry\" ] \u0026\u0026 continue\n [[ \"$entry\" == *=* ]] || { echo \"Malformed backend entry \u0027$entry\u0027; expected id=host:port\" \u003e\u00262; return 1; }\n id=\"${entry%%=*}\"; endpoint=\"${entry#*=}\"\n id=\"$(printf \u0027%s\u0027 \"$id\" | sed \u0027s/^[[:space:]]*//;s/[[:space:]]*$//\u0027)\"\n endpoint=\"$(printf \u0027%s\u0027 \"$endpoint\" | sed \u0027s/^[[:space:]]*//;s/[[:space:]]*$//\u0027)\"\n [[ \"$id\" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || { echo \"Invalid backend ID \u0027$id\u0027\" \u003e\u00262; return 1; }\n case \",$BACKEND_IDS,\" in *\",$id,\"*) echo \"Duplicate backend ID \u0027$id\u0027\" \u003e\u00262; return 1;; esac\n if [[ \"$endpoint\" =~ ^(\\[[^]]+\\]|[^:[:space:]]+):([0-9]+)$ ]]; then port=\"${BASH_REMATCH[2]}\"; else echo \"Invalid backend endpoint \u0027$endpoint\u0027\" \u003e\u00262; return 1; fi\n (( port \u003e= 1 \u0026\u0026 port \u003c= 65535 )) || { echo \"Backend port is outside 1-65535 in \u0027$endpoint\u0027\" \u003e\u00262; return 1; }\n BACKEND_IDS=\"${BACKEND_IDS:+$BACKEND_IDS,}$id\"\n BACKEND_LINES+=\"$(TomlQuote \"$id\") = $(TomlQuote \"$endpoint\")\"$\u0027\\n\u0027\n done\n [ -n \"$BACKEND_IDS\" ] || { echo \u0027At least one backend is required\u0027 \u003e\u00262; return 1; }\n}\n\nParseIdList() {\n local value=\"$1\" allow_empty=\"$2\" result=\"\" raw id\n IFS=\u0027,\u0027 read -ra ids \u003c\u003c\u003c \"$value\"\n for raw in \"${ids[@]}\"; do\n id=\"$(printf \u0027%s\u0027 \"$raw\" | sed \u0027s/^[[:space:]]*//;s/[[:space:]]*$//\u0027)\"\n [ -z \"$id\" ] \u0026\u0026 continue\n case \",$BACKEND_IDS,\" in *\",$id,\"*) ;; *) echo \"Unknown backend ID \u0027$id\u0027\" \u003e\u00262; return 1;; esac\n case \",$result,\" in *\",$id,\"*) ;; *) result=\"${result:+$result,}$id\";; esac\n done\n if [ \"$allow_empty\" != true ] \u0026\u0026 [ -z \"$result\" ]; then echo \u0027At least one backend ID is required\u0027 \u003e\u00262; return 1; fi\n printf \u0027%s\u0027 \"$result\"\n}\n\nReadExistingSecret() {\n local path=\"$1\" mode=\"$2\" key\n [ \"$mode\" = embedded ] \u0026\u0026 key=embedded_shared_secret || key=shared_secret\n [ -f \"$path\" ] || return 0\n sed -n \"s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*\\\"\\([0-9A-Fa-f]*\\)\\\"[[:space:]]*$/\\1/p\" \"$path\" | head -n 1\n}\n\nNewRandomSecret() { od -An -N32 -tx1 /dev/urandom | tr -d \u0027 \\n\u0027; }\n\nResolveSecret() {\n local panel=\"$1\" existing=\"$2\" mode=\"$3\" lower\n panel=\"$(printf \u0027%s\u0027 \"$panel\" | sed \u0027s/^[[:space:]]*//;s/[[:space:]]*$//\u0027)\"\n lower=\"$(printf \u0027%s\u0027 \"$panel\" | tr \u0027[:upper:]\u0027 \u0027[:lower:]\u0027)\"\n case \"$lower\" in change-me*|paste*secret*|shared-secret|shared_secret|\"shared secret\"|your-secret|your_secret|\"your secret\"|replace-me*) echo \u0027The shared secret is a documented placeholder, not a usable secret\u0027 \u003e\u00262; return 1;; esac\n if [ -n \"$panel\" ]; then printf \u0027%s\u0027 \"$panel\"; elif [ -n \"$existing\" ]; then printf \u0027%s\u0027 \"$existing\"; elif [ \"$mode\" = embedded ]; then NewRandomSecret; else echo \u0027Remote Registry mode requires an explicit or previously saved shared secret\u0027 \u003e\u00262; return 1; fi\n}\n\nParsePort() { local value=\"$1\" name=\"$2\"; [[ \"$value\" =~ ^[0-9]+$ ]] \u0026\u0026 (( value \u003e= 1 \u0026\u0026 value \u003c= 65535 )) || { echo \"$name must be from 1 through 65535\" \u003e\u00262; return 1; }; printf \u0027%s\u0027 \"$value\"; }\nParseBool() { local value=\"${1,,}\" name=\"$2\"; case \"$value\" in true|false) printf \u0027%s\u0027 \"$value\";; *) echo \"$name must be true or false\" \u003e\u00262; return 1;; esac; }\nTomlArray() { local value=\"$1\" result=\"\" raw; IFS=\u0027,\u0027 read -ra values \u003c\u003c\u003c \"$value\"; for raw in \"${values[@]}\"; do [ -z \"$raw\" ] \u0026\u0026 continue; result=\"${result:+$result, }$(TomlQuote \"$raw\")\"; done; printf \u0027[%s]\u0027 \"$result\"; }\n\nWriteAtomic() {\n local path=\"$1\" content=\"$2\" directory temporary\n directory=\"$(dirname \"$path\")\"; mkdir -p \"$directory\"\n temporary=\"$directory/.$(basename \"$path\").$$.$RANDOM.tmp\"\n trap \u0027rm -f \"$temporary\"\u0027 RETURN\n printf \u0027%s\u0027 \"$content\" \u003e \"$temporary\"\n mv -f \"$temporary\" \"$path\"\n trap - RETURN\n}\n\nParseBackendMap\ntry_ids=\"$(ParseIdList \"${NP_TRY:-hub}\" false)\"\nwhitelist_ids=\"$(ParseIdList \"${NP_WHITELIST_SERVERS:-}\" true)\"\nmode=\"${NP_REGISTRY_MODE:-embedded}\"; mode=\"${mode,,}\"\n[[ \"$mode\" = embedded || \"$mode\" = remote ]] || { echo \u0027Registry mode must be embedded or remote\u0027 \u003e\u00262; exit 1; }\naccess=\"${NP_REGISTRY_ACCESS:-same-host}\"; access=\"${access,,}\"\n[[ \"$access\" = same-host || \"$access\" = other-hosts ]] || { echo \u0027Registry access must be same-host or other-hosts\u0027 \u003e\u00262; exit 1; }\nregistry_url=\"${NP_REGISTRY_URL:-}\"\nif [ \"$mode\" = remote ] \u0026\u0026 [[ ! \"$registry_url\" =~ ^https?://[^[:space:]]+$ ]]; then echo \u0027Remote Registry mode requires an HTTP or HTTPS URL\u0027 \u003e\u00262; exit 1; fi\nexisting=\"$(ReadExistingSecret \"$target\" \"$mode\")\"\nsecret=\"$(ResolveSecret \"${NP_SHARED_SECRET:-}\" \"$existing\" \"$mode\")\"\nserver_port=\"$(ParsePort \"${NP_SERVER_PORT:-42420}\" \u0027Server port\u0027)\"\nregistry_port=\"$(ParsePort \"${NP_REGISTRY_PORT:-8765}\" \u0027Registry port\u0027)\"\nadmin_port=\"$(ParsePort \"${NP_ADMIN_PORT:-42499}\" \u0027Admin port\u0027)\"\nmetrics_port=\"$(ParsePort \"${NP_METRICS_PORT:-42500}\" \u0027Metrics port\u0027)\"\nmax_users=\"$(ParsePort \"${NP_MAX_USERS:-100}\" \u0027Player limit\u0027)\"\nwhitelist=\"$(ParseBool \"${NP_WHITELIST_NETWORK:-false}\" \u0027Network whitelist\u0027)\"\nverbose=\"$(ParseBool \"${NP_VERBOSE:-false}\" \u0027Verbose logging\u0027)\"\n[ \"$access\" = same-host ] \u0026\u0026 embedded_host=127.0.0.1 || embedded_host=0.0.0.0\nif [ \"$mode\" = embedded ]; then remote_secret=\"\"; embedded_secret=\"$secret\"; embedded_bind=\"http://${embedded_host}:${registry_port}\"; registry_url=\"\"; else remote_secret=\"$secret\"; embedded_secret=\"\"; embedded_bind=\"\"; fi\n\ncontent=\"bind = $(TomlQuote \"${NP_BIND_ADDRESS:-0.0.0.0}:${server_port}\")\ntry = $(TomlArray \"$try_ids\")\n\n[servers]\n${BACKEND_LINES}\n[transfers]\ndefault_mode = \\\"redirect\\\"\nallow_seamless = false\nrequire_seamless_capability = true\nfallback_to_redirect_when_seamless_unavailable = true\nenable_unsafe_seamless_splice = false\n\n[admin]\nbind = $(TomlQuote \"127.0.0.1:${admin_port}\")\nsecret = \\\"\\\"\nenabled = true\ngranted_permissions = [\\\"*\\\"]\n\n[registry]\nmode = $(TomlQuote \"$mode\")\nproxy_id = $(TomlQuote \"${NP_PROXY_ID:-nimbus-proxy}\")\nurl = $(TomlQuote \"$registry_url\")\nshared_secret = $(TomlQuote \"$remote_secret\")\nembedded_bind = $(TomlQuote \"$embedded_bind\")\nembedded_shared_secret = $(TomlQuote \"$embedded_secret\")\nbackend_stale_seconds = 20\nbackend_drop_seconds = 120\nnonce_window_seconds = 90\nmax_reservation_ttl_seconds = 300\nfail_on_error = true\ntransfer_intent_poll_ms = 1000\nadvertise_on_master_server = false\n\n[whitelist]\nnetwork = ${whitelist}\nservers = $(TomlArray \"$whitelist_ids\")\nfail_open_until_first_sync = false\n\n[logging]\nverbose = ${verbose}\nsniff_frames = false\nlog_traffic_bytes = false\n\n[metrics]\nenabled = true\nbind = $(TomlQuote \"http://127.0.0.1:${metrics_port}\")\npath = \\\"/metrics\\\"\nstatus_api = true\nstatus_api_token = \\\"\\\"\n\n[status]\nenabled = true\nname = $(TomlQuote \"${NP_STATUS_NAME:-Nimbus}\")\nmotd = $(TomlQuote \"${NP_STATUS_MOTD:-Vintage Story proxy}\")\ngame_mode = \\\"survival\\\"\npassword = false\nserver_version = \\\"\\\"\nmax_players = ${max_users}\nquery_timeout_ms = 1500\n\n[plugins]\nenabled = true\ndirectory = \\\"plugins\\\"\ndisabled = []\n\n[persistence]\npersist_drain_flags = true\ndrain_flags_file = \\\"nimbus.drain-state.json\\\"\n\n[advanced]\nconnect_timeout_ms = 5000\nbuffer_size = 16384\n\"\nWriteAtomic \"$target\" \"$content\"\nprintf \u0027wrote %s\\n\u0027 \"$target\"\n", + "OverwriteExistingFiles": true, + "SkipOnFailure": false + }, + { + "UpdateStageName": "Deploy Proxy renderer (Windows)", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "CreateFile", + "UpdateSourceArgs": "{{$FullRootDir}}scripts/write-proxy-toml.ps1", + "UpdateSourceData": "param([Parameter(Mandatory = $true)][string]$Target)\n\n$ErrorActionPreference = \u0027Stop\u0027\n\nfunction Get-Value([string]$Name, [string]$Default = \u0027\u0027) {\n $value = [Environment]::GetEnvironmentVariable($Name)\n if ($null -eq $value) { return $Default }\n return $value\n}\n\nfunction TomlQuote([string]$Value) {\n if ($null -eq $Value) { $Value = \u0027\u0027 }\n $Value = $Value.Replace(\u0027\\\u0027, \u0027\\\\\u0027).Replace(\u0027\"\u0027, \u0027\\\"\u0027)\n $Value = $Value.Replace(\"`r\", \u0027\\r\u0027).Replace(\"`n\", \u0027\\n\u0027).Replace(\"`t\", \u0027\\t\u0027)\n return \u0027\"\u0027 + $Value + \u0027\"\u0027\n}\n\nfunction ParseBackendMap([string]$Value) {\n $result = [ordered]@{}\n foreach ($raw in $Value.Split(\u0027;\u0027)) {\n $entry = $raw.Trim()\n if ($entry -eq \u0027\u0027) { continue }\n $split = $entry.IndexOf(\u0027=\u0027)\n if ($split -lt 1 -or $split -eq $entry.Length - 1) { throw \"Malformed backend entry \u0027$entry\u0027; expected id=host:port\" }\n $id = $entry.Substring(0, $split).Trim()\n $endpoint = $entry.Substring($split + 1).Trim()\n if ($id -notmatch \u0027^[A-Za-z0-9][A-Za-z0-9_.-]*$\u0027) { throw \"Invalid backend ID \u0027$id\u0027\" }\n if ($result.Contains($id)) { throw \"Duplicate backend ID \u0027$id\u0027\" }\n $match = [regex]::Match($endpoint, \u0027^(\\[[^\\]]+\\]|[^:\\s]+):([0-9]+)$\u0027)\n if (-not $match.Success) { throw \"Invalid backend endpoint \u0027$endpoint\u0027\" }\n $port = 0\n if (-not [int]::TryParse($match.Groups[2].Value, [ref]$port) -or $port -lt 1 -or $port -gt 65535) {\n throw \"Backend port is outside 1-65535 in \u0027$endpoint\u0027\"\n }\n $result[$id] = $endpoint\n }\n if ($result.Count -eq 0) { throw \u0027At least one backend is required\u0027 }\n return $result\n}\n\nfunction ParseIdList([string]$Value, [System.Collections.IDictionary]$Known, [bool]$AllowEmpty = $false) {\n $result = New-Object System.Collections.Generic.List[string]\n foreach ($raw in $Value.Split(\u0027,\u0027)) {\n $id = $raw.Trim()\n if ($id -eq \u0027\u0027) { continue }\n if (-not $Known.Contains($id)) { throw \"Unknown backend ID \u0027$id\u0027\" }\n if (-not $result.Contains($id)) { $result.Add($id) }\n }\n if (-not $AllowEmpty -and $result.Count -eq 0) { throw \u0027At least one backend ID is required\u0027 }\n return @($result)\n}\n\nfunction ReadExistingSecret([string]$Path, [string]$RegistryMode) {\n if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return \u0027\u0027 }\n $key = if ($RegistryMode -eq \u0027embedded\u0027) { \u0027embedded_shared_secret\u0027 } else { \u0027shared_secret\u0027 }\n $pattern = \u0027^\\s*\u0027 + [regex]::Escape($key) + \u0027\\s*=\\s*\"([0-9A-Fa-f]+)\"\\s*$\u0027\n foreach ($line in [IO.File]::ReadAllLines($Path)) {\n $match = [regex]::Match($line, $pattern)\n if ($match.Success) { return $match.Groups[1].Value }\n }\n return \u0027\u0027\n}\n\nfunction NewRandomSecret() {\n $bytes = New-Object byte[] 32\n $rng = [Security.Cryptography.RandomNumberGenerator]::Create()\n try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }\n return (($bytes | ForEach-Object { $_.ToString(\u0027x2\u0027) }) -join \u0027\u0027)\n}\n\nfunction ResolveSecret([string]$PanelValue, [string]$ExistingValue, [string]$RegistryMode) {\n $value = $PanelValue.Trim()\n $placeholder = \u0027^(?i)(change-me.*|paste.*secret.*|shared[-_ ]?secret|your[-_ ]?secret|replace[-_ ]?me.*)$\u0027\n if ($value -match $placeholder) { throw \u0027The shared secret is a documented placeholder, not a usable secret\u0027 }\n if ($value -ne \u0027\u0027) { return $value }\n if ($ExistingValue -ne \u0027\u0027) { return $ExistingValue }\n if ($RegistryMode -eq \u0027embedded\u0027) { return NewRandomSecret }\n throw \u0027Remote Registry mode requires an explicit or previously saved shared secret\u0027\n}\n\nfunction ParseBool([string]$Value, [string]$Name) {\n switch ($Value.Trim().ToLowerInvariant()) {\n \u0027true\u0027 { return \u0027true\u0027 }\n \u0027false\u0027 { return \u0027false\u0027 }\n default { throw \"$Name must be true or false\" }\n }\n}\n\nfunction ParsePort([string]$Value, [string]$Name) {\n $port = 0\n if (-not [int]::TryParse($Value, [ref]$port) -or $port -lt 1 -or $port -gt 65535) { throw \"$Name must be from 1 through 65535\" }\n return $port\n}\n\nfunction TomlArray([string[]]$Values) {\n return \u0027[\u0027 + (($Values | ForEach-Object { TomlQuote $_ }) -join \u0027, \u0027) + \u0027]\u0027\n}\n\nfunction WriteAtomic([string]$Path, [string]$Content) {\n $directory = Split-Path -Parent $Path\n if ($directory -and -not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }\n $temporary = Join-Path $directory (\u0027.\u0027 + [IO.Path]::GetFileName($Path) + \u0027.\u0027 + [guid]::NewGuid().ToString(\u0027N\u0027) + \u0027.tmp\u0027)\n try {\n [IO.File]::WriteAllText($temporary, $Content, (New-Object Text.UTF8Encoding $false))\n Move-Item -LiteralPath $temporary -Destination $Path -Force\n }\n finally {\n if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force }\n }\n}\n\n$bindAddress = Get-Value \u0027NP_BIND_ADDRESS\u0027 \u00270.0.0.0\u0027\n$serverPort = ParsePort (Get-Value \u0027NP_SERVER_PORT\u0027 \u002742420\u0027) \u0027Server port\u0027\n$registryPort = ParsePort (Get-Value \u0027NP_REGISTRY_PORT\u0027 \u00278765\u0027) \u0027Registry port\u0027\n$adminPort = ParsePort (Get-Value \u0027NP_ADMIN_PORT\u0027 \u002742499\u0027) \u0027Admin port\u0027\n$metricsPort = ParsePort (Get-Value \u0027NP_METRICS_PORT\u0027 \u002742500\u0027) \u0027Metrics port\u0027\n$maxPlayers = ParsePort (Get-Value \u0027NP_MAX_USERS\u0027 \u0027100\u0027) \u0027Player limit\u0027\n$backends = ParseBackendMap (Get-Value \u0027NP_SERVERS\u0027 \u0027hub=127.0.0.1:42421\u0027)\n$tryIds = ParseIdList (Get-Value \u0027NP_TRY\u0027 \u0027hub\u0027) $backends\n$whitelistIds = ParseIdList (Get-Value \u0027NP_WHITELIST_SERVERS\u0027 \u0027\u0027) $backends $true\n$mode = (Get-Value \u0027NP_REGISTRY_MODE\u0027 \u0027embedded\u0027).Trim().ToLowerInvariant()\nif (@(\u0027embedded\u0027, \u0027remote\u0027) -notcontains $mode) { throw \u0027Registry mode must be embedded or remote\u0027 }\n$access = (Get-Value \u0027NP_REGISTRY_ACCESS\u0027 \u0027same-host\u0027).Trim().ToLowerInvariant()\nif (@(\u0027same-host\u0027, \u0027other-hosts\u0027) -notcontains $access) { throw \u0027Registry access must be same-host or other-hosts\u0027 }\n$registryUrl = (Get-Value \u0027NP_REGISTRY_URL\u0027 \u0027\u0027).Trim()\nif ($mode -eq \u0027remote\u0027 -and $registryUrl -notmatch \u0027^https?://[^\\s]+$\u0027) { throw \u0027Remote Registry mode requires an HTTP or HTTPS URL\u0027 }\n$existingSecret = ReadExistingSecret $Target $mode\n$secret = ResolveSecret (Get-Value \u0027NP_SHARED_SECRET\u0027 \u0027\u0027) $existingSecret $mode\n$embeddedBindHost = if ($access -eq \u0027same-host\u0027) { \u0027127.0.0.1\u0027 } else { \u00270.0.0.0\u0027 }\n$remoteSecret = if ($mode -eq \u0027remote\u0027) { $secret } else { \u0027\u0027 }\n$embeddedSecret = if ($mode -eq \u0027embedded\u0027) { $secret } else { \u0027\u0027 }\n$embeddedBind = if ($mode -eq \u0027embedded\u0027) { \"http://${embeddedBindHost}:$registryPort\" } else { \u0027\u0027 }\n$whitelistNetwork = ParseBool (Get-Value \u0027NP_WHITELIST_NETWORK\u0027 \u0027false\u0027) \u0027Network whitelist\u0027\n$verbose = ParseBool (Get-Value \u0027NP_VERBOSE\u0027 \u0027false\u0027) \u0027Verbose logging\u0027\n\n$lines = New-Object System.Collections.Generic.List[string]\n$lines.Add(\u0027bind = \u0027 + (TomlQuote \"${bindAddress}:$serverPort\"))\n$lines.Add(\u0027try = \u0027 + (TomlArray $tryIds))\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[servers]\u0027)\nforeach ($entry in $backends.GetEnumerator()) { $lines.Add((TomlQuote $entry.Key) + \u0027 = \u0027 + (TomlQuote $entry.Value)) }\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[transfers]\u0027)\n$lines.Add(\u0027default_mode = \"redirect\"\u0027)\n$lines.Add(\u0027allow_seamless = false\u0027)\n$lines.Add(\u0027require_seamless_capability = true\u0027)\n$lines.Add(\u0027fallback_to_redirect_when_seamless_unavailable = true\u0027)\n$lines.Add(\u0027enable_unsafe_seamless_splice = false\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[admin]\u0027)\n$lines.Add(\u0027bind = \u0027 + (TomlQuote \"127.0.0.1:$adminPort\"))\n$lines.Add(\u0027secret = \"\"\u0027)\n$lines.Add(\u0027enabled = true\u0027)\n$lines.Add(\u0027granted_permissions = [\"*\"]\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[registry]\u0027)\n$lines.Add(\u0027mode = \u0027 + (TomlQuote $mode))\n$lines.Add(\u0027proxy_id = \u0027 + (TomlQuote (Get-Value \u0027NP_PROXY_ID\u0027 \u0027nimbus-proxy\u0027)))\n$lines.Add(\u0027url = \u0027 + (TomlQuote $(if ($mode -eq \u0027remote\u0027) { $registryUrl } else { \u0027\u0027 })))\n$lines.Add(\u0027shared_secret = \u0027 + (TomlQuote $remoteSecret))\n$lines.Add(\u0027embedded_bind = \u0027 + (TomlQuote $embeddedBind))\n$lines.Add(\u0027embedded_shared_secret = \u0027 + (TomlQuote $embeddedSecret))\n$lines.Add(\u0027backend_stale_seconds = 20\u0027)\n$lines.Add(\u0027backend_drop_seconds = 120\u0027)\n$lines.Add(\u0027nonce_window_seconds = 90\u0027)\n$lines.Add(\u0027max_reservation_ttl_seconds = 300\u0027)\n$lines.Add(\u0027fail_on_error = true\u0027)\n$lines.Add(\u0027transfer_intent_poll_ms = 1000\u0027)\n$lines.Add(\u0027advertise_on_master_server = false\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[whitelist]\u0027)\n$lines.Add(\u0027network = \u0027 + $whitelistNetwork)\n$lines.Add(\u0027servers = \u0027 + (TomlArray $whitelistIds))\n$lines.Add(\u0027fail_open_until_first_sync = false\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[logging]\u0027)\n$lines.Add(\u0027verbose = \u0027 + $verbose)\n$lines.Add(\u0027sniff_frames = false\u0027)\n$lines.Add(\u0027log_traffic_bytes = false\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[metrics]\u0027)\n$lines.Add(\u0027enabled = true\u0027)\n$lines.Add(\u0027bind = \u0027 + (TomlQuote \"http://127.0.0.1:$metricsPort\"))\n$lines.Add(\u0027path = \"/metrics\"\u0027)\n$lines.Add(\u0027status_api = true\u0027)\n$lines.Add(\u0027status_api_token = \"\"\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[status]\u0027)\n$lines.Add(\u0027enabled = true\u0027)\n$lines.Add(\u0027name = \u0027 + (TomlQuote (Get-Value \u0027NP_STATUS_NAME\u0027 \u0027Nimbus\u0027)))\n$lines.Add(\u0027motd = \u0027 + (TomlQuote (Get-Value \u0027NP_STATUS_MOTD\u0027 \u0027Vintage Story proxy\u0027)))\n$lines.Add(\u0027game_mode = \"survival\"\u0027)\n$lines.Add(\u0027password = false\u0027)\n$lines.Add(\u0027server_version = \"\"\u0027)\n$lines.Add(\u0027max_players = \u0027 + $maxPlayers)\n$lines.Add(\u0027query_timeout_ms = 1500\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[plugins]\u0027)\n$lines.Add(\u0027enabled = true\u0027)\n$lines.Add(\u0027directory = \"plugins\"\u0027)\n$lines.Add(\u0027disabled = []\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[persistence]\u0027)\n$lines.Add(\u0027persist_drain_flags = true\u0027)\n$lines.Add(\u0027drain_flags_file = \"nimbus.drain-state.json\"\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[advanced]\u0027)\n$lines.Add(\u0027connect_timeout_ms = 5000\u0027)\n$lines.Add(\u0027buffer_size = 16384\u0027)\n\nWriteAtomic $Target (($lines -join \"`n\") + \"`n\")\nWrite-Output \"wrote $Target\"\n", + "OverwriteExistingFiles": true, + "SkipOnFailure": false + } +] diff --git a/nimbus-registry.kvp b/nimbus-registry.kvp new file mode 100644 index 000000000..370ff935e --- /dev/null +++ b/nimbus-registry.kvp @@ -0,0 +1,96 @@ +Meta.DisplayName=Nimbus Registry (Optional) +Meta.Description=Optional shared Nimbus control plane for multi-proxy or multi-host networks +Meta.OS=Windows, Linux +Meta.AarchSupport=None +Meta.Arch=x86_64 +Meta.Author=HuguesDePayens +Meta.URL=https://github.com/StratumServer/Nimbus +Meta.DisplayImageSource=url:https://wiki.vintagestory.at/images/thumb/8/8a/Gamelogo-vintagestory.png/340px-Gamelogo-vintagestory.png +Meta.EndpointURIFormat= +Meta.ConfigManifest=nimbus-registryconfig.json +Meta.MetaConfigManifest= +Meta.ConfigRoot=nimbus-registry.kvp +Meta.MinAMPVersion=2.4.6.0 +Meta.SpecificDockerImage=cubecoders/ampbase:debian +Meta.DockerRequired=False +Meta.ContainerPolicy=Supported +Meta.ExtraContainerPackages=["jq","unzip","curl","ca-certificates"] +Meta.Prerequisites=[] +Meta.ConfigReleaseState=NotSpecified +Meta.NoCommercialUsage=False +Meta.ConfigVersion=1 +Meta.AppConfigId=b0ed14fe-3b73-4044-9339-71bd3df1ced3 +Meta.AppIsMultiIPAware=True +Meta.ResourceUsageInfo=Optional lightweight HTTP control plane; one Proxy can use its embedded Registry instead +Meta.ImportableExtensions=[] +App.DisplayName=Nimbus Registry +App.RootDir=./nimbus-registry/ +App.BaseDirectory=./nimbus-registry/ +App.ExecutableLinux=dotnet/dotnet +App.ExecutableWin=dotnet\dotnet.exe +App.WorkingDir=Nimbus.Registry +App.LinuxCommandLineArgs="{{$FullRootDir}}Nimbus.Registry/Nimbus.Registry.dll" "{{$FullRootDir}}Nimbus.Registry/nimbus.registry.toml" +App.WindowsCommandLineArgs="{{$FullRootDir}}Nimbus.Registry\Nimbus.Registry.dll" "{{$FullRootDir}}Nimbus.Registry\nimbus.registry.toml" +App.CommandLineArgs={{$PlatformArgs}} +App.UseLinuxIOREDIR=False +App.AppSettings={} +App.EnvironmentVariables={"DOTNET_ROOT":"{{$FullRootDir}}dotnet","DOTNET_CLI_TELEMETRY_OPTOUT":"1","DOTNET_NOLOGO":"1"} +App.CommandLineParameterFormat={0} {1} +App.CommandLineParameterDelimiter= +App.ExitMethod=OS_CLOSE +App.ExitTimeout=30 +App.ExitString= +App.ExitFile= +App.HasWriteableConsole=False +App.HasReadableConsole=True +App.SupportsLiveSettingsChanges=False +App.LiveSettingChangeCommandFormat= +App.ApplicationIPBinding=0.0.0.0 +App.Ports=@IncludeJson[nimbus-registryports.json] +App.AdminPortRef=RegistryPort +App.PrimaryApplicationPortRef=RegistryPort +App.UniversalSleepApplicationUDPPortRef= +App.UniversalSleepSteamQueryPortRef= +App.MaxUsers=0 +App.UseRandomAdminPassword=False +App.RemoteAdminPassword= +App.AdminMethod=STDIO +App.AdminLoginTransform=None +App.RCONConnectDelaySeconds=5 +App.RCONConnectRetrySeconds=5 +App.RCONHeartbeatMinutes=0 +App.RCONHeartbeatCommand= +App.TelnetLoginFormat={0} +App.PreStartStages=@IncludeJson[nimbus-registrystart.json] +App.UpdateSources=@IncludeJson[nimbus-registryupdates.json] +App.Compatibility=None +App.SteamUpdateAnonymousLogin=True +App.SteamForceLoginPrompt=False +App.RapidStartup=False +App.SmartExcludeSupported=True +App.SmartExcludeExemptions=["*.toml","nimbus.bans.json*","nimbus.whitelist.json*","nimbus.tokens.json*"] +App.MonitorChildProcess=True +App.MonitorChildProcessWaitMs=100 +App.MonitorChildProcessName= +App.SupportsUniversalSleep=False +App.WakeupMode=Any +App.ApplicationReadyMode=RegexMatch +App.StripANSIControlCodes=True +Console.FilterMatchRegex= +Console.FilterMatchReplacement= +Console.ThrowawayMessageRegex= +Console.AppReadyRegex=^\[Nimbus\] registry listening on .+$ +Console.UserJoinRegex= +Console.UserLeaveRegex= +Console.UserChatRegex= +Console.UpdateAvailableRegex= +Console.MetricsRegex= +Console.SuppressLogAtStart=False +Console.ActivateLogRegex= +Console.UserActions={} +Limits.SleepMode=False +Limits.SleepOnStart=False +Limits.SleepDelayMinutes=5 +Limits.DozeDelay=2 +Limits.AutoRetryCount=2 +Limits.SleepStartThresholdSeconds=25 diff --git a/nimbus-registryconfig.json b/nimbus-registryconfig.json new file mode 100644 index 000000000..2a908b0f1 --- /dev/null +++ b/nimbus-registryconfig.json @@ -0,0 +1,13 @@ +[ + {"DisplayName":"Registry Port","Category":"Registry:hub","Subcategory":"Network:dns:1","Description":"HTTP port used by Nimbus Proxy and backends.","Keywords":"registry,port","FieldName":"$RegistryPort","InputType":"number","Hidden":true,"ParamFieldName":"RegistryPort","IncludeInCommandLine":false,"DefaultValue":"8765","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Nimbus Release Tag","Category":"Updates:update","Subcategory":"Version:verified_user:1","Description":"Optional exact Nimbus release tag. Leave blank for the latest stable release.","Keywords":"nimbus,version","FieldName":"NimbusVersionTag","InputType":"text","ParamFieldName":"NimbusVersionTag","IncludeInCommandLine":false,"DefaultValue":"","Placeholder":"v0.5.0","EnumValues":{}}, + {"DisplayName":"Registry Shared Secret","Category":"Registry:hub","Subcategory":"Security:lock:2","Description":"Blank generates a strong secret once and preserves it. Copy the resulting value to Proxy and every backend.","Keywords":"registry,secret","FieldName":"RegistrySharedSecret","InputType":"password","ParamFieldName":"RegistrySharedSecret","IncludeInCommandLine":false,"DefaultValue":"","EnumValues":{}}, + {"DisplayName":"Advertise on Vintage Story Master Server","Category":"Public Listing:public","Subcategory":"Advertising:campaign:1","Description":"Publish the Proxy endpoint on the Vintage Story public server list.","Keywords":"master,server,advertise","FieldName":"RegistryAdvertise","InputType":"checkbox","ParamFieldName":"RegistryAdvertise","IncludeInCommandLine":false,"DefaultValue":"false","EnumValues":{"True":"true","False":"false"}}, + {"DisplayName":"Proxy Public Host","Category":"Public Listing:public","Subcategory":"Identity:badge:2","Description":"Public hostname or IP of Nimbus Proxy, not this Registry HTTP endpoint.","Keywords":"proxy,public,host","FieldName":"RegistryPublicHost","InputType":"text","ParamFieldName":"RegistryPublicHost","IncludeInCommandLine":false,"DefaultValue":"127.0.0.1","Placeholder":"play.example.com","EnumValues":{}}, + {"DisplayName":"Proxy Public Port","Category":"Public Listing:public","Subcategory":"Identity:badge:2","Description":"Public player port of Nimbus Proxy, not the Registry port.","Keywords":"proxy,public,port","FieldName":"RegistryPublicPort","InputType":"number","ParamFieldName":"RegistryPublicPort","IncludeInCommandLine":false,"DefaultValue":"42420","MinValue":1,"MaxValue":65535,"EnumValues":{}}, + {"DisplayName":"Network Name","Category":"Public Listing:public","Subcategory":"Identity:badge:2","Description":"Name shown on the Vintage Story public server list.","Keywords":"server,name","FieldName":"RegistryServerName","InputType":"text","ParamFieldName":"RegistryServerName","IncludeInCommandLine":false,"DefaultValue":"Nimbus Network","EnumValues":{}}, + {"DisplayName":"Network Description","Category":"Public Listing:public","Subcategory":"Identity:badge:2","Description":"Description shown on the Vintage Story public server list.","Keywords":"server,description","FieldName":"RegistryServerDescription","InputType":"text","ParamFieldName":"RegistryServerDescription","IncludeInCommandLine":false,"DefaultValue":"","EnumValues":{}}, + {"DisplayName":"Vintage Story Version","Category":"Public Listing:public","Subcategory":"Compatibility:verified_user:3","Description":"Game version reported publicly. Keep this equal to the Vintage Story version owned by the selected Stratum release.","Keywords":"game,version,stratum","FieldName":"RegistryGameVersion","InputType":"text","ParamFieldName":"RegistryGameVersion","IncludeInCommandLine":false,"Required":true,"DefaultValue":"1.22.6","EnumValues":{}}, + {"DisplayName":"List as Password Protected","Category":"Public Listing:public","Subcategory":"Access:lock:4","Description":"Report that the network requires a player password.","Keywords":"listing,password","FieldName":"RegistryListingHasPassword","InputType":"checkbox","ParamFieldName":"RegistryListingHasPassword","IncludeInCommandLine":false,"DefaultValue":"false","EnumValues":{"True":"true","False":"false"}}, + {"DisplayName":"List as Whitelisted","Category":"Public Listing:public","Subcategory":"Access:lock:4","Description":"Report that the network uses a whitelist.","Keywords":"listing,whitelist","FieldName":"RegistryListingWhitelisted","InputType":"checkbox","ParamFieldName":"RegistryListingWhitelisted","IncludeInCommandLine":false,"DefaultValue":"false","EnumValues":{"True":"true","False":"false"}} +] diff --git a/nimbus-registryports.json b/nimbus-registryports.json new file mode 100644 index 000000000..12962d2dd --- /dev/null +++ b/nimbus-registryports.json @@ -0,0 +1,3 @@ +[ + {"Protocol":"TCP","Name":"Registry Port","Description":"Nimbus Registry HTTP endpoint used by Proxy and backends","Port":8765,"Ref":"RegistryPort"} +] diff --git a/nimbus-registrystart.json b/nimbus-registrystart.json new file mode 100644 index 000000000..ec4a61519 --- /dev/null +++ b/nimbus-registrystart.json @@ -0,0 +1,18 @@ +[ + { + "UpdateStageName":"Render Nimbus Registry configuration", + "UpdateSourcePlatform":"Linux", + "UpdateSource":"Executable", + "UpdateSourceData":"/bin/bash", + "UpdateSourceArgs":"-c \"NR_BIND_ADDRESS='{{$ApplicationIPBinding}}' NR_REGISTRY_PORT='{{$RegistryPort}}' NR_SHARED_SECRET='{{RegistrySharedSecret}}' NR_ADVERTISE='{{RegistryAdvertise}}' NR_PUBLIC_HOST='{{RegistryPublicHost}}' NR_PUBLIC_PORT='{{RegistryPublicPort}}' NR_SERVER_NAME='{{RegistryServerName}}' NR_SERVER_DESCRIPTION='{{RegistryServerDescription}}' NR_GAME_VERSION='{{RegistryGameVersion}}' NR_HAS_PASSWORD='{{RegistryListingHasPassword}}' NR_WHITELISTED='{{RegistryListingWhitelisted}}' bash '{{$FullRootDir}}scripts/write-registry-toml.sh' '{{$FullRootDir}}Nimbus.Registry/nimbus.registry.toml'\"", + "SkipOnFailure":false + }, + { + "UpdateStageName":"Render Nimbus Registry configuration", + "UpdateSourcePlatform":"Windows", + "UpdateSource":"Executable", + "UpdateSourceData":"powershell.exe", + "UpdateSourceArgs":"-NoProfile -ExecutionPolicy Bypass -Command \"$env:NR_BIND_ADDRESS='{{$ApplicationIPBinding}}'; $env:NR_REGISTRY_PORT='{{$RegistryPort}}'; $env:NR_SHARED_SECRET='{{RegistrySharedSecret}}'; $env:NR_ADVERTISE='{{RegistryAdvertise}}'; $env:NR_PUBLIC_HOST='{{RegistryPublicHost}}'; $env:NR_PUBLIC_PORT='{{RegistryPublicPort}}'; $env:NR_SERVER_NAME='{{RegistryServerName}}'; $env:NR_SERVER_DESCRIPTION='{{RegistryServerDescription}}'; $env:NR_GAME_VERSION='{{RegistryGameVersion}}'; $env:NR_HAS_PASSWORD='{{RegistryListingHasPassword}}'; $env:NR_WHITELISTED='{{RegistryListingWhitelisted}}'; & '{{$FullRootDir}}scripts\\write-registry-toml.ps1' '{{$FullRootDir}}Nimbus.Registry\\nimbus.registry.toml'\"", + "SkipOnFailure":false + } +] diff --git a/nimbus-registryupdates.json b/nimbus-registryupdates.json new file mode 100644 index 000000000..e2db834a7 --- /dev/null +++ b/nimbus-registryupdates.json @@ -0,0 +1,75 @@ +[ + { + "UpdateStageName": "Create dotnet directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}dotnet", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create Nimbus Registry directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}Nimbus.Registry", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create scripts directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}scripts", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install local .NET 10 runtime", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; if [ -x \\\"$root/dotnet/dotnet\\\" ] \u0026\u0026 \\\"$root/dotnet/dotnet\\\" --list-runtimes | grep -q \u0027^Microsoft.AspNetCore.App 10\\.\u0027; then echo \u0027[AMP] .NET 10 ASP.NET Core runtime already installed\u0027; else tmp=\\\"$(mktemp)\\\"; curl -fsSL https://dot.net/v1/dotnet-install.sh -o \\\"$tmp\\\"; bash \\\"$tmp\\\" --channel 10.0 --runtime aspnetcore --install-dir \\\"$root/dotnet\\\" --no-path; rm -f \\\"$tmp\\\"; fi\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install local .NET 10 runtime", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $root=\u0027{{$FullRootDir}}\u0027; $dotnet=Join-Path $root \u0027dotnet\\dotnet.exe\u0027; $ready=$false; if (Test-Path -LiteralPath $dotnet) { $ready=[bool]((\u0026 $dotnet --list-runtimes) -match \u0027^Microsoft.AspNetCore.App 10\\.\u0027) }; if ($ready) { Write-Output \u0027[AMP] .NET 10 ASP.NET Core runtime already installed\u0027 } else { $script=Join-Path $env:TEMP (\u0027dotnet-install-\u0027+[guid]::NewGuid().ToString(\u0027N\u0027)+\u0027.ps1\u0027); try { Invoke-WebRequest -UseBasicParsing -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile $script; \u0026 $script -Channel 10.0 -Runtime aspnetcore -InstallDir (Join-Path $root \u0027dotnet\u0027) -NoPath } finally { Remove-Item -LiteralPath $script -Force -ErrorAction SilentlyContinue } }\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Nimbus Registry", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; tag=\u0027{{NimbusVersionTag}}\u0027; if [ -z \\\"$tag\\\" ]; then tag=\\\"$(curl -fsSL -H \u0027User-Agent: AMP-Clean-Template\u0027 https://api.github.com/repos/StratumServer/Nimbus/releases/latest | jq -r .tag_name)\\\"; fi; case \\\"$tag\\\" in v*) ;; *) tag=\\\"v$tag\\\";; esac; stage=\\\"$root/.amp-registry-stage\\\"; archive=\\\"$root/.amp-registry.zip\\\"; old=\\\"$root/.amp-registry-old\\\"; rm -rf \\\"$stage\\\" \\\"$old\\\"; mkdir -p \\\"$stage\\\"; curl -fL --retry 3 -o \\\"$archive\\\" \\\"https://github.com/StratumServer/Nimbus/releases/download/$tag/Nimbus-$tag.zip\\\"; unzip -q \\\"$archive\\\" -d \\\"$stage\\\"; rm -f \\\"$archive\\\"; fresh_dll=\\\"$(find \\\"$stage\\\" -type f -name Nimbus.Registry.dll -print -quit)\\\"; [ -n \\\"$fresh_dll\\\" ]; fresh=\\\"$(dirname \\\"$fresh_dll\\\")\\\"; if [ -f \\\"$root/Nimbus.Registry/nimbus.registry.toml\\\" ]; then cp \\\"$root/Nimbus.Registry/nimbus.registry.toml\\\" \\\"$fresh/nimbus.registry.toml\\\"; fi; for name in nimbus.bans.json nimbus.whitelist.json nimbus.tokens.json; do for file in \\\"$root/Nimbus.Registry/$name\\\"*; do [ -f \\\"$file\\\" ] \u0026\u0026 cp \\\"$file\\\" \\\"$fresh/\\\"; done; done; if [ -d \\\"$root/Nimbus.Registry\\\" ]; then mv \\\"$root/Nimbus.Registry\\\" \\\"$old\\\"; fi; mv \\\"$fresh\\\" \\\"$root/Nimbus.Registry\\\"; rm -rf \\\"$stage\\\" \\\"$old\\\"; printf \u0027%s\u0027 \\\"$tag\\\" \u003e \\\"$root/Nimbus.Registry/.amp-nimbus-release\\\"\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Nimbus Registry", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $ProgressPreference=\u0027SilentlyContinue\u0027; $root=\u0027{{$FullRootDir}}\u0027; $tag=\u0027{{NimbusVersionTag}}\u0027.Trim(); $headers=@{\u0027User-Agent\u0027=\u0027AMP-Clean-Template\u0027}; if (-not $tag) { $tag=(Invoke-RestMethod -Headers $headers -Uri https://api.github.com/repos/StratumServer/Nimbus/releases/latest).tag_name }; if (-not $tag.StartsWith(\u0027v\u0027)) { $tag=\u0027v\u0027+$tag }; $stage=Join-Path $root \u0027.amp-registry-stage\u0027; $archive=Join-Path $root \u0027.amp-registry.zip\u0027; $target=Join-Path $root \u0027Nimbus.Registry\u0027; $old=Join-Path $root \u0027.amp-registry-old\u0027; Remove-Item -LiteralPath $stage,$old,$archive -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $stage | Out-Null; Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri (\u0027https://github.com/StratumServer/Nimbus/releases/download/\u0027+$tag+\u0027/Nimbus-\u0027+$tag+\u0027.zip\u0027) -OutFile $archive; Expand-Archive -LiteralPath $archive -DestinationPath $stage -Force; Remove-Item -LiteralPath $archive -Force; $freshDll=Get-ChildItem -LiteralPath $stage -Filter \u0027Nimbus.Registry.dll\u0027 -File -Recurse | Select-Object -First 1; if ($null -eq $freshDll) { throw \u0027Nimbus archive did not contain Nimbus.Registry.dll\u0027 }; $fresh=$freshDll.Directory.FullName; foreach ($pattern in @(\u0027nimbus.registry.toml\u0027,\u0027nimbus.bans.json*\u0027,\u0027nimbus.whitelist.json*\u0027,\u0027nimbus.tokens.json*\u0027)) { Get-ChildItem -LiteralPath $target -Filter $pattern -File -ErrorAction SilentlyContinue | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $fresh -Force } }; if (Test-Path -LiteralPath $target) { Move-Item -LiteralPath $target -Destination $old }; try { Move-Item -LiteralPath $fresh -Destination $target; Remove-Item -LiteralPath $stage,$old -Recurse -Force -ErrorAction SilentlyContinue } catch { if ((Test-Path -LiteralPath $old) -and -not (Test-Path -LiteralPath $target)) { Move-Item -LiteralPath $old -Destination $target }; throw }; [IO.File]::WriteAllText((Join-Path $target \u0027.amp-nimbus-release\u0027),$tag,(New-Object Text.UTF8Encoding $false))\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Deploy Registry renderer (Linux)", + "UpdateSourcePlatform": "Linux", + "UpdateSource": "CreateFile", + "UpdateSourceArgs": "{{$FullRootDir}}scripts/write-registry-toml.sh", + "UpdateSourceData": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${1:?target path required}\"\n\nTomlQuote() {\n local value=\"${1-}\"\n value=${value//\\/\\\\}; value=${value//\\\"/\\\\\\\"}\n value=${value//$\u0027\\r\u0027/\\\\r}; value=${value//$\u0027\\n\u0027/\\\\n}; value=${value//$\u0027\\t\u0027/\\\\t}\n printf \u0027\"%s\"\u0027 \"$value\"\n}\nReadExistingSecret() { [ -f \"$1\" ] || return 0; sed -n \u0027s/^[[:space:]]*shared_secret[[:space:]]*=[[:space:]]*\"\\([0-9A-Fa-f]*\\)\"[[:space:]]*$/\\1/p\u0027 \"$1\" | head -n 1; }\nNewRandomSecret() { od -An -N32 -tx1 /dev/urandom | tr -d \u0027 \\n\u0027; }\nResolveSecret() {\n local panel=\"$1\" existing=\"$2\" lower\n panel=\"$(printf \u0027%s\u0027 \"$panel\" | sed \u0027s/^[[:space:]]*//;s/[[:space:]]*$//\u0027)\"; lower=\"$(printf \u0027%s\u0027 \"$panel\" | tr \u0027[:upper:]\u0027 \u0027[:lower:]\u0027)\"\n case \"$lower\" in change-me*|paste*secret*|shared-secret|shared_secret|\"shared secret\"|your-secret|your_secret|\"your secret\"|replace-me*) echo \u0027The shared secret is a documented placeholder, not a usable secret\u0027 \u003e\u00262; return 1;; esac\n if [ -n \"$panel\" ]; then printf \u0027%s\u0027 \"$panel\"; elif [ -n \"$existing\" ]; then printf \u0027%s\u0027 \"$existing\"; else NewRandomSecret; fi\n}\nParsePort() { local value=\"$1\" name=\"$2\"; [[ \"$value\" =~ ^[0-9]+$ ]] \u0026\u0026 (( value \u003e= 1 \u0026\u0026 value \u003c= 65535 )) || { echo \"$name must be from 1 through 65535\" \u003e\u00262; return 1; }; printf \u0027%s\u0027 \"$value\"; }\nParseBool() { local value=\"${1,,}\" name=\"$2\"; case \"$value\" in true|false) printf \u0027%s\u0027 \"$value\";; *) echo \"$name must be true or false\" \u003e\u00262; return 1;; esac; }\nWriteAtomic() { local path=\"$1\" content=\"$2\" directory temporary; directory=\"$(dirname \"$path\")\"; mkdir -p \"$directory\"; temporary=\"$directory/.$(basename \"$path\").$$.$RANDOM.tmp\"; trap \u0027rm -f \"$temporary\"\u0027 RETURN; printf \u0027%s\u0027 \"$content\" \u003e \"$temporary\"; mv -f \"$temporary\" \"$path\"; trap - RETURN; }\n\nregistry_port=\"$(ParsePort \"${NR_REGISTRY_PORT:-8765}\" \u0027Registry port\u0027)\"\npublic_port=\"$(ParsePort \"${NR_PUBLIC_PORT:-42420}\" \u0027Proxy public port\u0027)\"\nadvertise=\"$(ParseBool \"${NR_ADVERTISE:-false}\" \u0027Master-server advertising\u0027)\"\nhas_password=\"$(ParseBool \"${NR_HAS_PASSWORD:-false}\" \u0027Password listing flag\u0027)\"\nwhitelisted=\"$(ParseBool \"${NR_WHITELISTED:-false}\" \u0027Whitelist listing flag\u0027)\"\npublic_host=\"${NR_PUBLIC_HOST-}\"; game_version=\"${NR_GAME_VERSION:-1.22.6}\"\nif [ \"$advertise\" = true ]; then [ -n \"$public_host\" ] || { echo \u0027Advertising requires the public hostname or IP of Nimbus Proxy\u0027 \u003e\u00262; exit 1; }; [ -n \"$game_version\" ] || { echo \u0027Advertising requires the Vintage Story game version supported by the selected Stratum release\u0027 \u003e\u00262; exit 1; }; fi\nsecret=\"$(ResolveSecret \"${NR_SHARED_SECRET:-}\" \"$(ReadExistingSecret \"$target\")\")\"\n\ncontent=\"bind_url = $(TomlQuote \"http://${NR_BIND_ADDRESS:-0.0.0.0}:${registry_port}\")\nshared_secret = $(TomlQuote \"$secret\")\naccepted_secrets = []\nbackend_stale_seconds = 20\nbackend_drop_seconds = 120\nnonce_window_seconds = 90\nmax_reservation_ttl_seconds = 300\nlog_heartbeats = false\nstate_dir = \\\".\\\"\n\n[api_tokens]\nenabled = false\nrate_limit_per_minute = 60\ntrust_forwarded_proto = false\n\n[identity]\nadvertise_on_master_server = ${advertise}\nmaster_server_url = \\\"http://masterserver.vintagestory.at/api/v1/servers/\\\"\npublic_host = $(TomlQuote \"$public_host\")\npublic_port = ${public_port}\nserver_name = $(TomlQuote \"${NR_SERVER_NAME:-Nimbus Network}\")\nserver_description = $(TomlQuote \"${NR_SERVER_DESCRIPTION:-}\")\nserver_url = \\\"\\\"\nserver_icon = \\\"\\\"\nvh_identifier = \\\"\\\"\ngame_version = $(TomlQuote \"$game_version\")\nmax_players_override = 0\nmod_source = \\\"aggregate\\\"\nexplicit_mods = []\nwhitelisted = ${whitelisted}\nhas_password = ${has_password}\nheartbeat_interval_seconds = 120\n\n[identity.playstyle]\nid = \\\"surviveandbuild\\\"\nlang_code = \\\"preset-surviveandbuild\\\"\n\"\nWriteAtomic \"$target\" \"$content\"\nprintf \u0027wrote %s\\n\u0027 \"$target\"\n", + "OverwriteExistingFiles": true, + "SkipOnFailure": false + }, + { + "UpdateStageName": "Deploy Registry renderer (Windows)", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "CreateFile", + "UpdateSourceArgs": "{{$FullRootDir}}scripts/write-registry-toml.ps1", + "UpdateSourceData": "param([Parameter(Mandatory = $true)][string]$Target)\n\n$ErrorActionPreference = \u0027Stop\u0027\n\nfunction Get-Value([string]$Name, [string]$Default = \u0027\u0027) {\n $value = [Environment]::GetEnvironmentVariable($Name)\n if ($null -eq $value) { return $Default }\n return $value\n}\n\nfunction TomlQuote([string]$Value) {\n if ($null -eq $Value) { $Value = \u0027\u0027 }\n $Value = $Value.Replace(\u0027\\\u0027, \u0027\\\\\u0027).Replace(\u0027\"\u0027, \u0027\\\"\u0027)\n $Value = $Value.Replace(\"`r\", \u0027\\r\u0027).Replace(\"`n\", \u0027\\n\u0027).Replace(\"`t\", \u0027\\t\u0027)\n return \u0027\"\u0027 + $Value + \u0027\"\u0027\n}\n\nfunction ReadExistingSecret([string]$Path) {\n if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return \u0027\u0027 }\n foreach ($line in [IO.File]::ReadAllLines($Path)) {\n $match = [regex]::Match($line, \u0027^\\s*shared_secret\\s*=\\s*\"([0-9A-Fa-f]+)\"\\s*$\u0027)\n if ($match.Success) { return $match.Groups[1].Value }\n }\n return \u0027\u0027\n}\n\nfunction NewRandomSecret() {\n $bytes = New-Object byte[] 32\n $rng = [Security.Cryptography.RandomNumberGenerator]::Create()\n try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }\n return (($bytes | ForEach-Object { $_.ToString(\u0027x2\u0027) }) -join \u0027\u0027)\n}\n\nfunction ResolveSecret([string]$PanelValue, [string]$ExistingValue) {\n $value = $PanelValue.Trim()\n $placeholder = \u0027^(?i)(change-me.*|paste.*secret.*|shared[-_ ]?secret|your[-_ ]?secret|replace[-_ ]?me.*)$\u0027\n if ($value -match $placeholder) { throw \u0027The shared secret is a documented placeholder, not a usable secret\u0027 }\n if ($value -ne \u0027\u0027) { return $value }\n if ($ExistingValue -ne \u0027\u0027) { return $ExistingValue }\n return NewRandomSecret\n}\n\nfunction ParseBool([string]$Value, [string]$Name) {\n switch ($Value.Trim().ToLowerInvariant()) {\n \u0027true\u0027 { return \u0027true\u0027 }\n \u0027false\u0027 { return \u0027false\u0027 }\n default { throw \"$Name must be true or false\" }\n }\n}\n\nfunction ParsePort([string]$Value, [string]$Name) {\n $port = 0\n if (-not [int]::TryParse($Value, [ref]$port) -or $port -lt 1 -or $port -gt 65535) { throw \"$Name must be from 1 through 65535\" }\n return $port\n}\n\nfunction WriteAtomic([string]$Path, [string]$Content) {\n $directory = Split-Path -Parent $Path\n if ($directory -and -not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }\n $temporary = Join-Path $directory (\u0027.\u0027 + [IO.Path]::GetFileName($Path) + \u0027.\u0027 + [guid]::NewGuid().ToString(\u0027N\u0027) + \u0027.tmp\u0027)\n try {\n [IO.File]::WriteAllText($temporary, $Content, (New-Object Text.UTF8Encoding $false))\n Move-Item -LiteralPath $temporary -Destination $Path -Force\n }\n finally {\n if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force }\n }\n}\n\n$bindAddress = Get-Value \u0027NR_BIND_ADDRESS\u0027 \u00270.0.0.0\u0027\n$registryPort = ParsePort (Get-Value \u0027NR_REGISTRY_PORT\u0027 \u00278765\u0027) \u0027Registry port\u0027\n$publicPort = ParsePort (Get-Value \u0027NR_PUBLIC_PORT\u0027 \u002742420\u0027) \u0027Proxy public port\u0027\n$advertise = ParseBool (Get-Value \u0027NR_ADVERTISE\u0027 \u0027false\u0027) \u0027Master-server advertising\u0027\n$hasPassword = ParseBool (Get-Value \u0027NR_HAS_PASSWORD\u0027 \u0027false\u0027) \u0027Password listing flag\u0027\n$whitelisted = ParseBool (Get-Value \u0027NR_WHITELISTED\u0027 \u0027false\u0027) \u0027Whitelist listing flag\u0027\n$publicHost = (Get-Value \u0027NR_PUBLIC_HOST\u0027 \u0027\u0027).Trim()\n$gameVersion = (Get-Value \u0027NR_GAME_VERSION\u0027 \u00271.22.6\u0027).Trim()\nif ($advertise -eq \u0027true\u0027) {\n if ($publicHost -eq \u0027\u0027) { throw \u0027Advertising requires the public hostname or IP of Nimbus Proxy\u0027 }\n if ($gameVersion -eq \u0027\u0027) { throw \u0027Advertising requires the Vintage Story game version supported by the selected Stratum release\u0027 }\n}\n$secret = ResolveSecret (Get-Value \u0027NR_SHARED_SECRET\u0027 \u0027\u0027) (ReadExistingSecret $Target)\n\n$lines = New-Object System.Collections.Generic.List[string]\n$lines.Add(\u0027bind_url = \u0027 + (TomlQuote \"http://${bindAddress}:$registryPort\"))\n$lines.Add(\u0027shared_secret = \u0027 + (TomlQuote $secret))\n$lines.Add(\u0027accepted_secrets = []\u0027)\n$lines.Add(\u0027backend_stale_seconds = 20\u0027)\n$lines.Add(\u0027backend_drop_seconds = 120\u0027)\n$lines.Add(\u0027nonce_window_seconds = 90\u0027)\n$lines.Add(\u0027max_reservation_ttl_seconds = 300\u0027)\n$lines.Add(\u0027log_heartbeats = false\u0027)\n$lines.Add(\u0027state_dir = \".\"\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[api_tokens]\u0027)\n$lines.Add(\u0027enabled = false\u0027)\n$lines.Add(\u0027rate_limit_per_minute = 60\u0027)\n$lines.Add(\u0027trust_forwarded_proto = false\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[identity]\u0027)\n$lines.Add(\u0027advertise_on_master_server = \u0027 + $advertise)\n$lines.Add(\u0027master_server_url = \"http://masterserver.vintagestory.at/api/v1/servers/\"\u0027)\n$lines.Add(\u0027public_host = \u0027 + (TomlQuote $publicHost))\n$lines.Add(\u0027public_port = \u0027 + $publicPort)\n$lines.Add(\u0027server_name = \u0027 + (TomlQuote (Get-Value \u0027NR_SERVER_NAME\u0027 \u0027Nimbus Network\u0027)))\n$lines.Add(\u0027server_description = \u0027 + (TomlQuote (Get-Value \u0027NR_SERVER_DESCRIPTION\u0027 \u0027\u0027)))\n$lines.Add(\u0027server_url = \"\"\u0027)\n$lines.Add(\u0027server_icon = \"\"\u0027)\n$lines.Add(\u0027vh_identifier = \"\"\u0027)\n$lines.Add(\u0027game_version = \u0027 + (TomlQuote $gameVersion))\n$lines.Add(\u0027max_players_override = 0\u0027)\n$lines.Add(\u0027mod_source = \"aggregate\"\u0027)\n$lines.Add(\u0027explicit_mods = []\u0027)\n$lines.Add(\u0027whitelisted = \u0027 + $whitelisted)\n$lines.Add(\u0027has_password = \u0027 + $hasPassword)\n$lines.Add(\u0027heartbeat_interval_seconds = 120\u0027)\n$lines.Add(\u0027\u0027)\n$lines.Add(\u0027[identity.playstyle]\u0027)\n$lines.Add(\u0027id = \"surviveandbuild\"\u0027)\n$lines.Add(\u0027lang_code = \"preset-surviveandbuild\"\u0027)\n\nWriteAtomic $Target (($lines -join \"`n\") + \"`n\")\nWrite-Output \"wrote $Target\"\n", + "OverwriteExistingFiles": true, + "SkipOnFailure": false + } +] diff --git a/vintage-story-stratum.kvp b/vintage-story-stratum.kvp new file mode 100644 index 000000000..5538ab8a3 --- /dev/null +++ b/vintage-story-stratum.kvp @@ -0,0 +1,96 @@ +Meta.DisplayName=Vintage Story (Stratum + Nimbus Backend) +Meta.Description=Vintage Story backend using Stratum and Nimbus.ServerMod. Players connect through Nimbus Proxy. +Meta.OS=Windows, Linux +Meta.AarchSupport=None +Meta.Arch=x86_64 +Meta.Author=HuguesDePayens +Meta.URL=https://github.com/StratumServer/Stratum +Meta.DisplayImageSource=url:https://wiki.vintagestory.at/images/thumb/8/8a/Gamelogo-vintagestory.png/340px-Gamelogo-vintagestory.png +Meta.EndpointURIFormat= +Meta.ConfigManifest=vintage-story-stratumconfig.json +Meta.MetaConfigManifest=vintage-story-stratummetaconfig.json +Meta.ConfigRoot=vintage-story-stratum.kvp +Meta.MinAMPVersion=2.4.6.0 +Meta.SpecificDockerImage=cubecoders/ampbase:debian +Meta.DockerRequired=False +Meta.ContainerPolicy=Supported +Meta.ExtraContainerPackages=["jq","unzip","curl","ca-certificates"] +Meta.Prerequisites=[] +Meta.ConfigReleaseState=NotSpecified +Meta.NoCommercialUsage=False +Meta.ConfigVersion=4 +Meta.AppConfigId=ac764031-4749-46d1-b662-d3e9de8d9356 +Meta.AppIsMultiIPAware=True +Meta.ResourceUsageInfo=4 GB RAM minimum; 8 GB recommended for active worlds +Meta.ImportableExtensions=[] +App.DisplayName=Vintage Story (Stratum Backend) +App.RootDir=./vintage-story/ +App.BaseDirectory=./vintage-story/ +App.ExecutableWin=server\StratumServer.exe +App.ExecutableLinux=server/StratumServer +App.WorkingDir=server +App.LinuxCommandLineArgs= +App.WindowsCommandLineArgs= +App.CommandLineArgs={{$PlatformArgs}} --port {{$ServerPort}} --dataPath "../data" {{$FormattedArgs}} +App.UseLinuxIOREDIR=False +App.AppSettings={} +App.EnvironmentVariables={"DOTNET_ROOT":"{{$FullRootDir}}dotnet","DOTNET_CLI_TELEMETRY_OPTOUT":"1","DOTNET_NOLOGO":"1"} +App.CommandLineParameterFormat=--{0} {1} +App.CommandLineParameterDelimiter= +App.ExitMethod=String +App.ExitTimeout=45 +App.ExitString=/stop +App.ExitFile=lock +App.HasWriteableConsole=True +App.HasReadableConsole=True +App.SupportsLiveSettingsChanges=False +App.LiveSettingChangeCommandFormat=set {0} "{1}" +App.ApplicationIPBinding=0.0.0.0 +App.Ports=@IncludeJson[vintage-story-stratumports.json] +App.AdminPortRef=ServerPort +App.PrimaryApplicationPortRef=ServerPort +App.UniversalSleepApplicationUDPPortRef=ServerPort +App.UniversalSleepSteamQueryPortRef=ServerPort +App.MaxUsers={{$MaxUsers}} +App.UseRandomAdminPassword=False +App.RemoteAdminPassword= +App.AdminMethod=STDIO +App.AdminLoginTransform=None +App.RCONConnectDelaySeconds=5 +App.RCONConnectRetrySeconds=5 +App.RCONHeartbeatMinutes=0 +App.RCONHeartbeatCommand=ping +App.TelnetLoginFormat={0} +App.UpdateSources=@IncludeJson[vintage-story-stratumupdates.json] +App.PreStartStages=@IncludeJson[vintage-story-stratumstart.json] +App.Compatibility=None +App.SteamUpdateAnonymousLogin=True +App.SteamForceLoginPrompt=False +App.RapidStartup=False +App.SmartExcludeSupported=True +App.SmartExcludeExemptions=["*.cfg","*.conf","*.config","*.ini","*.json","*.xml","*.properties","*.kvp","*.yml","*.yaml","*.toml","*.lua","*.vcdbs"] +App.MonitorChildProcess=True +App.MonitorChildProcessWaitMs=100 +App.MonitorChildProcessName= +App.SupportsUniversalSleep=False +App.WakeupMode=Any +App.ApplicationReadyMode=RegexMatch +App.StripANSIControlCodes=True +Console.FilterMatchRegex=^.*(?:|).*$ +Console.FilterMatchReplacement= +Console.ThrowawayMessageRegex=^(WARNING|ERROR): Shader.+$ +Console.AppReadyRegex=^[\d.]*\s*[\d:.]+ \[.*Event\] (?:Dedicated Server|Serveur dédié|Dedizierter Server) now running on Port \d+ and (?:all ips|ip .*)!$ +Console.UserJoinRegex=^[\d.]*\s*[\d:.]+ \[.*Event\] (?.+?) [\[\]a-f\d:.]+ joins\.$ +Console.UserLeaveRegex=^[\d.]*\s*[\d:.]+ \[.*Event\] Player (?.+?) (left|got removed\. Reason: Server shutting down - Shutdown via server command).*$ +Console.UserChatRegex=^[\d.]*\s*[\d:.]+ \[.*Chat\] (?.+?): (?.+?): (?.+)$ +Console.UpdateAvailableRegex=^.*(?:Stratum update available:|A new server update is available!|UPDATE AVAILABLE: running ).*$ +Console.MetricsRegex= +Console.SuppressLogAtStart=False +Console.ActivateLogRegex= +Console.UserActions={} +Limits.SleepMode=True +Limits.SleepOnStart=False +Limits.SleepDelayMinutes=5 +Limits.DozeDelay=2 +Limits.AutoRetryCount=2 +Limits.SleepStartThresholdSeconds=25 diff --git a/vintage-story-stratumconfig.json b/vintage-story-stratumconfig.json new file mode 100644 index 000000000..c2b9d23d4 --- /dev/null +++ b/vintage-story-stratumconfig.json @@ -0,0 +1,354 @@ +[ + { + "DisplayName": "Server Port", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Network:dns:1", + "Description": "Private game port used by Nimbus Proxy.", + "Keywords": "port,backend", + "FieldName": "$ServerPort", + "InputType": "number", + "Hidden": true, + "ParamFieldName": "$.Port", + "IncludeInCommandLine": false, + "DefaultValue": "42420", + "MinValue": 1, + "MaxValue": 65535, + "EnumValues": { + + } + }, + { + "DisplayName": "Player Limit", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Players:group:2", + "Description": "Maximum simultaneous players on this backend.", + "Keywords": "players,limit", + "FieldName": "$MaxUsers", + "InputType": "number", + "ParamFieldName": "$.MaxClients", + "IncludeInCommandLine": false, + "DefaultValue": "16", + "MinValue": 1, + "MaxValue": 1000, + "EnumValues": { + + } + }, + { + "DisplayName": "Server Name", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Identity:badge:1", + "Description": "Name shown by the Vintage Story server.", + "Keywords": "server,name", + "FieldName": "VS_ServerName", + "InputType": "text", + "ParamFieldName": "$.ServerName", + "IncludeInCommandLine": false, + "DefaultValue": "Vintage Story Server", + "EnumValues": { + + } + }, + { + "DisplayName": "Server Description", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Identity:badge:1", + "Description": "Short description shown to players.", + "Keywords": "server,description", + "FieldName": "VS_ServerDescription", + "InputType": "text", + "ParamFieldName": "$.ServerDescription", + "IncludeInCommandLine": false, + "DefaultValue": "", + "EnumValues": { + + } + }, + { + "DisplayName": "Welcome Message", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Identity:badge:1", + "Description": "Message shown after a player joins.", + "Keywords": "welcome,message", + "FieldName": "VS_WelcomeMessage", + "InputType": "text", + "ParamFieldName": "$.WelcomeMessage", + "IncludeInCommandLine": false, + "DefaultValue": "Welcome, {0}, may you survive well and prosper", + "EnumValues": { + + } + }, + { + "DisplayName": "Server Password", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Access:lock:3", + "Description": "Optional password required to join. Leave blank for none.", + "Keywords": "password,access", + "FieldName": "VS_Password", + "InputType": "password", + "ParamFieldName": "$.Password", + "IncludeInCommandLine": false, + "DefaultValue": "", + "EnumValues": { + + } + }, + { + "DisplayName": "Whitelist Mode", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Access:lock:3", + "Description": "Default follows Vintage Story dedicated-server policy.", + "Keywords": "whitelist,access", + "FieldName": "VS_WhitelistMode", + "InputType": "enum", + "ParamFieldName": "$.WhitelistMode", + "IncludeInCommandLine": false, + "DefaultValue": "0", + "EnumValues": { + "0": "Default (on for dedicated servers)", + "1": "Off", + "2": "On" + } + }, + { + "DisplayName": "Allow PvP", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Rules:gavel:4", + "Description": "Allow players to damage other players.", + "Keywords": "pvp,rules", + "FieldName": "VS_AllowPvP", + "InputType": "checkbox", + "ParamFieldName": "$.AllowPvP", + "IncludeInCommandLine": false, + "DefaultValue": "true", + "EnumValues": { + "True": "true", + "False": "false" + } + }, + { + "DisplayName": "Pass Time When Empty", + "Category": "Vintage Story:stadia_controller", + "Subcategory": "Rules:gavel:4", + "Description": "Advance world time while no players are online.", + "Keywords": "time,empty", + "FieldName": "VS_PassTimeWhenEmpty", + "InputType": "checkbox", + "ParamFieldName": "$.PassTimeWhenEmpty", + "IncludeInCommandLine": false, + "DefaultValue": "false", + "EnumValues": { + "True": "true", + "False": "false" + } + }, + { + "DisplayName": "Enable Nimbus Backend", + "Category": "Nimbus Backend:share", + "Subcategory": "Status:power_settings_new:0", + "Description": "Enable Nimbus backend networking. Leave disabled to run Stratum without Nimbus. Network settings below are required only when enabled.", + "Keywords": "nimbus,enable,disable,standalone", + "FieldName": "NimbusEnabled", + "InputType": "checkbox", + "ParamFieldName": "$.Enabled", + "IncludeInCommandLine": false, + "DefaultValue": "false", + "EnumValues": { + "True": "true", + "False": "false" + } + }, + { + "DisplayName": "Nimbus Server ID", + "Category": "Nimbus Backend:share", + "Subcategory": "Connection:dns:1", + "Description": "Required only when Nimbus is enabled. Unique ID that must exactly match this backend\u0027s key in the Proxy Backend Servers map.", + "Keywords": "nimbus,id", + "FieldName": "NimbusServerId", + "InputType": "text", + "ParamFieldName": "$.ServerId", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "hub", + "EnumValues": { + + } + }, + { + "DisplayName": "Nimbus Display Name", + "Category": "Nimbus Backend:share", + "Subcategory": "Connection:dns:1", + "Description": "Friendly name for this backend.", + "Keywords": "nimbus,name", + "FieldName": "NimbusDisplayName", + "InputType": "text", + "ParamFieldName": "$.DisplayName", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "Hub", + "EnumValues": { + + } + }, + { + "DisplayName": "Backend Reachable Host", + "Category": "Nimbus Backend:share", + "Subcategory": "Connection:dns:1", + "Description": "Required only when Nimbus is enabled. Hostname or IP the Nimbus Proxy uses to reach this backend.", + "Keywords": "nimbus,backend,host", + "FieldName": "NimbusPublicHost", + "InputType": "text", + "ParamFieldName": "$.PublicHost", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "192.168.1.20", + "EnumValues": { + + } + }, + { + "DisplayName": "Backend Reachable Port", + "Category": "Nimbus Backend:share", + "Subcategory": "Connection:dns:1", + "Description": "Port the Nimbus Proxy uses to reach this backend. Required only when Nimbus is enabled.", + "Keywords": "nimbus,backend,port", + "FieldName": "NimbusPublicPort", + "InputType": "number", + "ParamFieldName": "$.PublicPort", + "IncludeInCommandLine": false, + "DefaultValue": "42420", + "MinValue": 1, + "MaxValue": 65535, + "EnumValues": { + + } + }, + { + "DisplayName": "Nimbus Registry URL", + "Category": "Nimbus Backend:share", + "Subcategory": "Registry:hub:2", + "Description": "Required only when Nimbus is enabled. HTTP URL reachable from this backend for the Proxy\u0027s embedded Registry or the optional standalone Registry.", + "Keywords": "registry,url", + "FieldName": "NimbusRegistryUrl", + "InputType": "text", + "ParamFieldName": "$.RegistryUrl", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "http://proxy-host:8765", + "EnumValues": { + + } + }, + { + "DisplayName": "Nimbus Shared Secret", + "Category": "Nimbus Backend:share", + "Subcategory": "Registry:hub:2", + "Description": "Required only when Nimbus is enabled. Copy the generated shared secret from the Proxy or standalone Registry. Backends never generate it.", + "Keywords": "registry,secret", + "FieldName": "NimbusSharedSecret", + "InputType": "password", + "ParamFieldName": "$.SharedSecret", + "IncludeInCommandLine": false, + "DefaultValue": "", + "EnumValues": { + + } + }, + { + "DisplayName": "Maintenance Mode", + "Category": "Nimbus Backend:share", + "Subcategory": "Routing:swap_horiz:3", + "Description": "Nimbus networks only. Keep this configured backend online and heartbeating but remove it from normal routing. This does not disable Nimbus.", + "Keywords": "maintenance,routing", + "FieldName": "NimbusMaintenance", + "InputType": "checkbox", + "ParamFieldName": "$.Maintenance", + "IncludeInCommandLine": false, + "DefaultValue": "false", + "EnumValues": { + "True": "true", + "False": "false" + } + }, + { + "DisplayName": "Allow Player Server Command", + "Category": "Nimbus Backend:share", + "Subcategory": "Routing:swap_horiz:3", + "Description": "Allow players to use Nimbus server-switch commands.", + "Keywords": "server,command", + "FieldName": "NimbusAllowPlayerServerCommand", + "InputType": "checkbox", + "ParamFieldName": "$.AllowPlayerServerCommand", + "IncludeInCommandLine": false, + "DefaultValue": "true", + "EnumValues": { + "True": "true", + "False": "false" + } + }, + { + "DisplayName": "Anonymous Stratum Statistics", + "Category": "Stratum:security", + "Subcategory": "Privacy:visibility:1", + "Description": "Send Stratum\u0027s anonymous version and player-count statistics.", + "Keywords": "stratum,statistics", + "FieldName": "StratumServerStats", + "InputType": "checkbox", + "ParamFieldName": "$.ServerStats.Enabled", + "IncludeInCommandLine": false, + "DefaultValue": "true", + "EnumValues": { + "True": "true", + "False": "false" + } + }, + { + "DisplayName": "Stratum Release Tag", + "Category": "Updates:update", + "Subcategory": "Version:verified_user:1", + "Description": "Optional exact Stratum release tag. Leave blank for the latest stable release; the release selects the compatible Vintage Story version.", + "Keywords": "stratum,version", + "FieldName": "StratumVersionTag", + "InputType": "text", + "ParamFieldName": "StratumVersionTag", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "v1.22.6-stratum.2", + "EnumValues": { + + } + }, + { + "DisplayName": "Nimbus Release Tag", + "Category": "Updates:update", + "Subcategory": "Version:verified_user:1", + "Description": "Optional exact Nimbus release tag. Leave blank for the latest stable release.", + "Keywords": "nimbus,version", + "FieldName": "NimbusVersionTag", + "InputType": "text", + "ParamFieldName": "NimbusVersionTag", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "v0.5.0", + "EnumValues": { + + } + }, + { + "DisplayName": "RedirectFix Release Tag", + "Category": "Updates:update", + "Subcategory": "Version:verified_user:1", + "Description": "Optional exact RedirectFix release tag. Leave blank to install and track the latest stable release.", + "Keywords": "redirectfix,version,client,mod", + "FieldName": "RedirectFixVersionTag", + "InputType": "text", + "ParamFieldName": "RedirectFixVersionTag", + "IncludeInCommandLine": false, + "DefaultValue": "", + "Placeholder": "v1.0.1", + "EnumValues": { + + } + } +] diff --git a/vintage-story-stratummetaconfig.json b/vintage-story-stratummetaconfig.json new file mode 100644 index 000000000..df80c95c2 --- /dev/null +++ b/vintage-story-stratummetaconfig.json @@ -0,0 +1,5 @@ +[ + {"ConfigFile":"data/serverconfig.json","AutoMap":true,"ConfigType":"json"}, + {"ConfigFile":"data/stratum.json","AutoMap":true,"ConfigType":"json"}, + {"ConfigFile":"data/ModConfig/nimbus-server.json","AutoMap":true,"ConfigType":"json"} +] diff --git a/vintage-story-stratumports.json b/vintage-story-stratumports.json new file mode 100644 index 000000000..0a83adc08 --- /dev/null +++ b/vintage-story-stratumports.json @@ -0,0 +1,9 @@ +[ + { + "Protocol": "Both", + "Name": "Server Port", + "Description": "Private game port used by Nimbus Proxy to reach this backend", + "Port": 42420, + "Ref": "ServerPort" + } +] diff --git a/vintage-story-stratumstart.json b/vintage-story-stratumstart.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/vintage-story-stratumstart.json @@ -0,0 +1 @@ +[] diff --git a/vintage-story-stratumupdates.json b/vintage-story-stratumupdates.json new file mode 100644 index 000000000..e2ce5f9ca --- /dev/null +++ b/vintage-story-stratumupdates.json @@ -0,0 +1,164 @@ +[ + { + "UpdateStageName": "Create server directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}server", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create data directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}data", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create tools directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}tools", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create Mods directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}data/Mods", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create ModConfig directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}data/ModConfig", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Create dotnet directory", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateDirectory", + "UpdateSourceArgs": "{{$FullRootDir}}dotnet", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install local .NET 10 runtime", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; if [ -x \\\"$root/dotnet/dotnet\\\" ] \u0026\u0026 \\\"$root/dotnet/dotnet\\\" --list-runtimes | grep -q \u0027^Microsoft.NETCore.App 10\\.\u0027; then echo \u0027[AMP] .NET 10 runtime already installed\u0027; else tmp=\\\"$(mktemp)\\\"; curl -fsSL https://dot.net/v1/dotnet-install.sh -o \\\"$tmp\\\"; bash \\\"$tmp\\\" --channel 10.0 --runtime dotnet --install-dir \\\"$root/dotnet\\\" --no-path; rm -f \\\"$tmp\\\"; fi\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install local .NET 10 runtime", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $root=\u0027{{$FullRootDir}}\u0027; $dotnet=Join-Path $root \u0027dotnet\\dotnet.exe\u0027; $ready=$false; if (Test-Path -LiteralPath $dotnet) { $ready=[bool]((\u0026 $dotnet --list-runtimes) -match \u0027^Microsoft.NETCore.App 10\\.\u0027) }; if ($ready) { Write-Output \u0027[AMP] .NET 10 runtime already installed\u0027 } else { $script=Join-Path $env:TEMP (\u0027dotnet-install-\u0027+[guid]::NewGuid().ToString(\u0027N\u0027)+\u0027.ps1\u0027); try { Invoke-WebRequest -UseBasicParsing -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile $script; \u0026 $script -Channel 10.0 -Runtime dotnet -InstallDir (Join-Path $root \u0027dotnet\u0027) -NoPath } finally { Remove-Item -LiteralPath $script -Force -ErrorAction SilentlyContinue } }\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Stratum release", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; tag=\u0027{{StratumVersionTag}}\u0027; if [ -z \\\"$tag\\\" ]; then tag=\\\"$(curl -fsSL -H \u0027User-Agent: AMP-Clean-Template\u0027 https://api.github.com/repos/StratumServer/Stratum/releases/latest | jq -r .tag_name)\\\"; fi; case \\\"$tag\\\" in v*) ;; *) tag=\\\"v$tag\\\";; esac; version=\\\"${tag#v}\\\"; asset=\\\"stratum-${version}-linux-x64.zip\\\"; stage=\\\"$root/.amp-stratum-stage\\\"; archive=\\\"$root/.amp-stratum.zip\\\"; rm -rf \\\"$stage\\\"; mkdir -p \\\"$stage\\\"; curl -fL --retry 3 -o \\\"$archive\\\" \\\"https://github.com/StratumServer/Stratum/releases/download/$tag/$asset\\\"; unzip -q \\\"$archive\\\" -d \\\"$stage\\\"; rm -f \\\"$archive\\\"; test -x \\\"$stage/StratumServer\\\" || chmod +x \\\"$stage/StratumServer\\\"; test -x \\\"$stage/StratumServer\\\"; old=\\\"$root/.amp-stratum-old\\\"; rm -rf \\\"$old\\\"; if [ -d \\\"$root/server\\\" ]; then mv \\\"$root/server\\\" \\\"$old\\\"; fi; mv \\\"$stage\\\" \\\"$root/server\\\"; rm -rf \\\"$old\\\"; printf \u0027%s\u0027 \\\"$tag\\\" \u003e \\\"$root/server/.amp-stratum-release\\\"\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Stratum release", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $ProgressPreference=\u0027SilentlyContinue\u0027; $root=\u0027{{$FullRootDir}}\u0027; $tag=\u0027{{StratumVersionTag}}\u0027.Trim(); $headers=@{\u0027User-Agent\u0027=\u0027AMP-Clean-Template\u0027}; if (-not $tag) { $tag=(Invoke-RestMethod -Headers $headers -Uri https://api.github.com/repos/StratumServer/Stratum/releases/latest).tag_name }; if (-not $tag.StartsWith(\u0027v\u0027)) { $tag=\u0027v\u0027+$tag }; $version=$tag.TrimStart(\u0027v\u0027); $asset=\u0027stratum-\u0027+$version+\u0027-win-x64.zip\u0027; $stage=Join-Path $root \u0027.amp-stratum-stage\u0027; $archive=Join-Path $root \u0027.amp-stratum.zip\u0027; $server=Join-Path $root \u0027server\u0027; $old=Join-Path $root \u0027.amp-stratum-old\u0027; Remove-Item -LiteralPath $stage,$old,$archive -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $stage | Out-Null; Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri (\u0027https://github.com/StratumServer/Stratum/releases/download/\u0027+$tag+\u0027/\u0027+$asset) -OutFile $archive; Expand-Archive -LiteralPath $archive -DestinationPath $stage -Force; Remove-Item -LiteralPath $archive -Force; if (-not (Test-Path -LiteralPath (Join-Path $stage \u0027StratumServer.exe\u0027))) { throw \u0027Stratum archive did not contain StratumServer.exe\u0027 }; if (Test-Path -LiteralPath $server) { Move-Item -LiteralPath $server -Destination $old }; try { Move-Item -LiteralPath $stage -Destination $server; Remove-Item -LiteralPath $old -Recurse -Force -ErrorAction SilentlyContinue } catch { if ((Test-Path -LiteralPath $old) -and -not (Test-Path -LiteralPath $server)) { Move-Item -LiteralPath $old -Destination $server }; throw }; [IO.File]::WriteAllText((Join-Path $server \u0027.amp-stratum-release\u0027),$tag,(New-Object Text.UTF8Encoding $false))\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Prepare compatible Vintage Story server assets", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; export DOTNET_ROOT=\u0027{{$FullRootDir}}dotnet\u0027; cd \u0027{{$FullRootDir}}server\u0027; ./StratumServer --stratum-prepare-only --stratum-no-banner --dataPath \u0027../data\u0027\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Prepare compatible Vintage Story server assets", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -Command \"$env:DOTNET_ROOT=\u0027{{$FullRootDir}}dotnet\u0027; Set-Location \u0027{{$FullRootDir}}server\u0027; \u0026 \u0027.\\StratumServer.exe\u0027 --stratum-prepare-only --stratum-no-banner --dataPath \u0027../data\u0027; exit $LASTEXITCODE\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Generate official Vintage Story configuration when missing", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; if [ ! -f \u0027{{$FullRootDir}}data/serverconfig.json\u0027 ]; then export DOTNET_ROOT=\u0027{{$FullRootDir}}dotnet\u0027; cd \u0027{{$FullRootDir}}server\u0027; ./StratumServer --genconfig --dataPath \u0027../data\u0027; fi\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Generate official Vintage Story configuration when missing", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -Command \"$cfg=\u0027{{$FullRootDir}}data\\serverconfig.json\u0027; if (-not (Test-Path -LiteralPath $cfg)) { $env:DOTNET_ROOT=\u0027{{$FullRootDir}}dotnet\u0027; Set-Location \u0027{{$FullRootDir}}server\u0027; \u0026 \u0027.\\StratumServer.exe\u0027 --genconfig --dataPath \u0027../data\u0027; exit $LASTEXITCODE }\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Seed Stratum configuration when missing", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateFile", + "UpdateSourceArgs": "{{$FullRootDir}}data/stratum.json", + "UpdateSourceData": "{\n \"ServerStats\": {\n \"Enabled\": true\n }\n}\n", + "OverwriteExistingFiles": false, + "SkipOnFailure": false + }, + { + "UpdateStageName": "Seed Nimbus backend configuration when missing", + "UpdateSourcePlatform": "All", + "UpdateSource": "CreateFile", + "UpdateSourceArgs": "{{$FullRootDir}}data/ModConfig/nimbus-server.json", + "UpdateSourceData": "{\n \"Enabled\": false,\n \"ServerId\": \"\",\n \"DisplayName\": \"\",\n \"PublicHost\": \"\",\n \"PublicPort\": 42420,\n \"RegistryUrl\": \"\",\n \"SharedSecret\": \"\",\n \"HeartbeatIntervalSeconds\": 5,\n \"RegistryHttpTimeoutSeconds\": 5,\n \"Maintenance\": false,\n \"ReservationRequired\": true,\n \"FailClosedWhenRegistryUnreachable\": false,\n \"AllowPlayerServerCommand\": true,\n \"TransferMode\": \"redirect\",\n \"SeamlessPrepareAckTimeoutSeconds\": 8,\n \"ShortcutCommands\": [],\n \"Tags\": []\n}\n", + "OverwriteExistingFiles": false, + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Nimbus ServerMod", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; tag=\u0027{{NimbusVersionTag}}\u0027; if [ -z \\\"$tag\\\" ]; then tag=\\\"$(curl -fsSL -H \u0027User-Agent: AMP-Clean-Template\u0027 https://api.github.com/repos/StratumServer/Nimbus/releases/latest | jq -r .tag_name)\\\"; fi; case \\\"$tag\\\" in v*) ;; *) tag=\\\"v$tag\\\";; esac; stage=\\\"$root/.amp-nimbus-mod-stage\\\"; archive=\\\"$root/.amp-nimbus-mod.zip\\\"; target=\\\"$root/data/Mods/Nimbus.ServerMod\\\"; rm -rf \\\"$stage\\\"; mkdir -p \\\"$stage\\\"; curl -fL --retry 3 -o \\\"$archive\\\" \\\"https://github.com/StratumServer/Nimbus/releases/download/$tag/Nimbus.ServerMod-$tag.zip\\\"; unzip -q \\\"$archive\\\" -d \\\"$stage\\\"; rm -f \\\"$archive\\\"; find \\\"$stage\\\" -type f | grep -q .; old=\\\"$root/.amp-nimbus-mod-old\\\"; rm -rf \\\"$old\\\"; if [ -d \\\"$target\\\" ]; then mv \\\"$target\\\" \\\"$old\\\"; fi; mv \\\"$stage\\\" \\\"$target\\\"; rm -rf \\\"$old\\\"; printf \u0027%s\u0027 \\\"$tag\\\" \u003e \\\"$target/.amp-nimbus-release\\\"\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install Nimbus ServerMod", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $ProgressPreference=\u0027SilentlyContinue\u0027; $root=\u0027{{$FullRootDir}}\u0027; $tag=\u0027{{NimbusVersionTag}}\u0027.Trim(); $headers=@{\u0027User-Agent\u0027=\u0027AMP-Clean-Template\u0027}; if (-not $tag) { $tag=(Invoke-RestMethod -Headers $headers -Uri https://api.github.com/repos/StratumServer/Nimbus/releases/latest).tag_name }; if (-not $tag.StartsWith(\u0027v\u0027)) { $tag=\u0027v\u0027+$tag }; $stage=Join-Path $root \u0027.amp-nimbus-mod-stage\u0027; $archive=Join-Path $root \u0027.amp-nimbus-mod.zip\u0027; $target=Join-Path $root \u0027data\\Mods\\Nimbus.ServerMod\u0027; $old=Join-Path $root \u0027.amp-nimbus-mod-old\u0027; Remove-Item -LiteralPath $stage,$old,$archive -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $stage | Out-Null; Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri (\u0027https://github.com/StratumServer/Nimbus/releases/download/\u0027+$tag+\u0027/Nimbus.ServerMod-\u0027+$tag+\u0027.zip\u0027) -OutFile $archive; Expand-Archive -LiteralPath $archive -DestinationPath $stage -Force; Remove-Item -LiteralPath $archive -Force; if (-not (Get-ChildItem -LiteralPath $stage -File -Recurse | Select-Object -First 1)) { throw \u0027Nimbus ServerMod archive was empty\u0027 }; if (Test-Path -LiteralPath $target) { Move-Item -LiteralPath $target -Destination $old }; try { Move-Item -LiteralPath $stage -Destination $target; Remove-Item -LiteralPath $old -Recurse -Force -ErrorAction SilentlyContinue } catch { if ((Test-Path -LiteralPath $old) -and -not (Test-Path -LiteralPath $target)) { Move-Item -LiteralPath $old -Destination $target }; throw }; [IO.File]::WriteAllText((Join-Path $target \u0027.amp-nimbus-release\u0027),$tag,(New-Object Text.UTF8Encoding $false))\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install RedirectFix", + "UpdateSourcePlatform": "Linux", + "UpdateSourceArch": "x86_64", + "UpdateSource": "Executable", + "UpdateSourceData": "/bin/bash", + "UpdateSourceArgs": "-c \"set -euo pipefail; root=\u0027{{$FullRootDir}}\u0027; requested=\u0027{{RedirectFixVersionTag}}\u0027; api=\u0027https://api.github.com/repos/StratumServer/redirectfix/releases\u0027; if [ -z \\\"$requested\\\" ]; then release=\\\"$(curl -fsSL -H \u0027User-Agent: AMP-Clean-Template\u0027 \\\"$api/latest\\\")\\\"; else case \\\"$requested\\\" in v*) tag=\\\"$requested\\\";; *) tag=\\\"v$requested\\\";; esac; release=\\\"$(curl -fsSL -H \u0027User-Agent: AMP-Clean-Template\u0027 \\\"$api/tags/$tag\\\")\\\"; fi; tag=\\\"$(printf \u0027%s\u0027 \\\"$release\\\" | jq -r \u0027.tag_name // empty\u0027)\\\"; test -n \\\"$tag\\\"; version=\\\"\u0024{tag#v}\\\"; asset=\\\"redirectfix-$version.zip\\\"; url=\\\"$(printf \u0027%s\u0027 \\\"$release\\\" | jq -r --arg name \\\"$asset\\\" \u0027.assets[] | select(.name == $name) | .browser_download_url\u0027 | head -n 1)\\\"; test -n \\\"$url\\\"; mods=\\\"$root/data/Mods\\\"; mkdir -p \\\"$mods\\\"; target=\\\"$mods/redirectfix.zip\\\"; marker=\\\"$mods/.amp-redirectfix-version\\\"; installed=\u0027\u0027; valid=false; if [ -f \\\"$target\\\" ] && unzip -Z1 \\\"$target\\\" | grep -qx \u0027modinfo.json\u0027 && unzip -Z1 \\\"$target\\\" | grep -qx \u0027RedirectFix.dll\u0027; then installed=\\\"$(unzip -p \\\"$target\\\" modinfo.json | jq -r \u0027.Version // empty\u0027 2>/dev/null || true)\\\"; valid=true; fi; echo \\\"[AMP] RedirectFix installed=\u0024{installed:-\u003cnone\u003e} requested=$version\\\"; if [ \\\"$valid\\\" = true ] && [ \\\"$installed\\\" = \\\"$version\\\" ]; then printf \u0027%s\\n\u0027 \\\"$version\\\" \u003e \\\"$marker.tmp\\\"; mv -f \\\"$marker.tmp\\\" \\\"$marker\\\"; echo \\\"[AMP] RedirectFix $version is already current\\\"; exit 0; fi; tmp=\\\"$(mktemp \\\"$mods/.redirectfix.XXXXXX.zip\\\")\\\"; trap \u0027rm -f \\\"$tmp\\\" \\\"$marker.tmp\\\"\u0027 EXIT; curl -fL --retry 3 -o \\\"$tmp\\\" \\\"$url\\\"; unzip -Z1 \\\"$tmp\\\" | grep -qx \u0027modinfo.json\u0027; unzip -Z1 \\\"$tmp\\\" | grep -qx \u0027RedirectFix.dll\u0027; archive_version=\\\"$(unzip -p \\\"$tmp\\\" modinfo.json | jq -r \u0027.Version // empty\u0027)\\\"; [ \\\"$archive_version\\\" = \\\"$version\\\" ]; mv -f \\\"$tmp\\\" \\\"$target\\\"; printf \u0027%s\\n\u0027 \\\"$version\\\" \u003e \\\"$marker.tmp\\\"; mv -f \\\"$marker.tmp\\\" \\\"$marker\\\"; trap - EXIT; echo \\\"[AMP] RedirectFix updated to $version\\\"\"", + "SkipOnFailure": false + }, + { + "UpdateStageName": "Install RedirectFix", + "UpdateSourcePlatform": "Windows", + "UpdateSource": "Executable", + "UpdateSourceData": "powershell.exe", + "UpdateSourceArgs": "-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference=\u0027Stop\u0027; $ProgressPreference=\u0027SilentlyContinue\u0027; Add-Type -AssemblyName System.IO.Compression.FileSystem; $root=\u0027{{$FullRootDir}}\u0027; $requested=\u0027{{RedirectFixVersionTag}}\u0027.Trim(); $headers=@{\u0027User-Agent\u0027=\u0027AMP-Clean-Template\u0027}; $api=\u0027https://api.github.com/repos/StratumServer/redirectfix/releases\u0027; if ($requested) { $tag=if ($requested.StartsWith(\u0027v\u0027)) {$requested} else {\u0027v\u0027+$requested}; $release=Invoke-RestMethod -Headers $headers -Uri ($api+\u0027/tags/\u0027+$tag) } else { $release=Invoke-RestMethod -Headers $headers -Uri ($api+\u0027/latest\u0027) }; $tag=[string]$release.tag_name; if (-not $tag) { throw \u0027RedirectFix release has no tag\u0027 }; $version=$tag.TrimStart(\u0027v\u0027); $assetName=\u0027redirectfix-\u0027+$version+\u0027.zip\u0027; $asset=@($release.assets | Where-Object name -CEQ $assetName | Select-Object -First 1); if ($asset.Count -ne 1) { throw (\u0027RedirectFix release has no \u0027+$assetName) }; function Read-RedirectFixArchive([string]$path) { if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }; $zip=[IO.Compression.ZipFile]::OpenRead($path); try { $info=@($zip.Entries | Where-Object FullName -CEQ \u0027modinfo.json\u0027); $dll=@($zip.Entries | Where-Object FullName -CEQ \u0027RedirectFix.dll\u0027); if ($info.Count -ne 1 -or $dll.Count -ne 1) { return $null }; $reader=New-Object IO.StreamReader($info[0].Open()); try { $meta=$reader.ReadToEnd() | ConvertFrom-Json } finally { $reader.Dispose() }; return [pscustomobject]@{Version=[string]$meta.Version} } finally { $zip.Dispose() } }; $mods=Join-Path $root \u0027data\\Mods\u0027; New-Item -ItemType Directory -Path $mods -Force | Out-Null; $target=Join-Path $mods \u0027redirectfix.zip\u0027; $marker=Join-Path $mods \u0027.amp-redirectfix-version\u0027; $current=Read-RedirectFixArchive $target; $installed=if ($null -eq $current -or -not $current.Version) {\u0027\u003cnone\u003e\u0027} else {$current.Version}; Write-Output (\u0027[AMP] RedirectFix installed=\u0027+$installed+\u0027 requested=\u0027+$version); if ($null -ne $current -and $current.Version -CEQ $version) { [IO.File]::WriteAllText($marker,$version+[Environment]::NewLine,(New-Object Text.UTF8Encoding $false)); Write-Output (\u0027[AMP] RedirectFix \u0027+$version+\u0027 is already current\u0027); exit 0 }; $tmp=Join-Path $mods (\u0027.redirectfix.\u0027+[guid]::NewGuid().ToString(\u0027N\u0027)+\u0027.zip\u0027); $backup=$target+\u0027.amp-backup\u0027; try { Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri ([string]$asset.browser_download_url) -OutFile $tmp; $candidate=Read-RedirectFixArchive $tmp; if ($null -eq $candidate) { throw \u0027RedirectFix archive must contain root modinfo.json and RedirectFix.dll\u0027 }; if ($candidate.Version -CNE $version) { throw (\u0027RedirectFix archive version \u0027+$candidate.Version+\u0027 does not match \u0027+$version) }; Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue; if (Test-Path -LiteralPath $target) { [IO.File]::Replace($tmp,$target,$backup,$true); Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue } else { [IO.File]::Move($tmp,$target) }; [IO.File]::WriteAllText($marker,$version+[Environment]::NewLine,(New-Object Text.UTF8Encoding $false)); Write-Output (\u0027[AMP] RedirectFix updated to \u0027+$version) } finally { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }\"", + "SkipOnFailure": false + } +]