diff --git a/scripts/prebuild/main.go b/scripts/prebuild/main.go index 1cd8e845..38f9375e 100644 --- a/scripts/prebuild/main.go +++ b/scripts/prebuild/main.go @@ -10,7 +10,9 @@ import ( "os/exec" "path/filepath" "sort" + "strconv" "strings" + "syscall" cp "github.com/otiai10/copy" "gopkg.in/yaml.v3" @@ -23,6 +25,7 @@ type prebuildSpec struct { Image string Ports []string MinDisk string + BuildDisk string MinRAM string MinCPU string Category string @@ -84,6 +87,9 @@ func runSpec(spec prebuildSpec) error { if err := validateRequiredEnv(spec); err != nil { return err } + if err := validateBuildDisk(spec); err != nil { + return err + } root, err := createPrebuildRoot(spec.Target) if err != nil { @@ -226,6 +232,79 @@ func validateRequiredEnv(spec prebuildSpec) error { return nil } +func validateBuildDisk(spec prebuildSpec) error { + if spec.BuildDisk == "" { + return nil + } + required, err := parseSizeBytes(spec.BuildDisk) + if err != nil { + return fmt.Errorf("invalid build disk requirement %q: %w", spec.BuildDisk, err) + } + paths := []string{"/var/lib/docker"} + if parent := os.Getenv("PREBUILD_TMP_PARENT"); parent != "" { + paths = append(paths, parent) + } else if cwd, err := os.Getwd(); err == nil { + paths = append(paths, filepath.Join(cwd, ".prebuild-tmp")) + } + for _, path := range paths { + available, err := availableBytes(path) + if err != nil { + return fmt.Errorf("check free disk for %s: %w", path, err) + } + if available < required { + return fmt.Errorf("need at least %s free for %s prebuild at %s, have %s", spec.BuildDisk, spec.Target, path, formatBytes(available)) + } + } + return nil +} + +func parseSizeBytes(raw string) (uint64, error) { + value := strings.TrimSpace(raw) + if value == "" { + return 0, errors.New("empty size") + } + units := []struct { + suffix string + factor uint64 + }{ + {"Gi", 1024 * 1024 * 1024}, + {"Mi", 1024 * 1024}, + {"G", 1000 * 1000 * 1000}, + {"M", 1000 * 1000}, + } + for _, unit := range units { + if strings.HasSuffix(value, unit.suffix) { + number := strings.TrimSpace(strings.TrimSuffix(value, unit.suffix)) + parsed, err := strconv.ParseFloat(number, 64) + if err != nil { + return 0, err + } + return uint64(parsed * float64(unit.factor)), nil + } + } + return strconv.ParseUint(value, 10, 64) +} + +func availableBytes(path string) (uint64, error) { + if err := os.MkdirAll(path, 0o755); err != nil { + return 0, err + } + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil { + return 0, err + } + return stat.Bavail * uint64(stat.Bsize), nil +} + +func formatBytes(bytes uint64) string { + const gib = 1024 * 1024 * 1024 + if bytes >= gib { + return fmt.Sprintf("%.1fGi", float64(bytes)/gib) + } + const mib = 1024 * 1024 + return fmt.Sprintf("%.1fMi", float64(bytes)/mib) +} + func isShell(value string) bool { base := filepath.Base(value) return base == "sh" || base == "bash" @@ -567,11 +646,11 @@ func allSpecs() []prebuildSpec { {Target: "untserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:untserver-prebuild", Source: "./scrolls/lgsm/untserver", Image: steamImage, Ports: []string{"main=27015/udp", "mainv6=27016"}, MinDisk: "7Gi", MinRAM: "1Gi", MinCPU: "0.5", Category: "unturned", Smart: true, PackMeta: true}, {Target: "sdtdserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:sdtdserver-prebuild", Source: "./scrolls/lgsm/sdtdserver", Image: steamImage, Ports: []string{"query=26900/udp", "main=26900/udp", "main2=26902/udp", "maintcp=26900"}, MinDisk: "20Gi", MinRAM: "2Gi", MinCPU: "0.5", Category: "7days", PackMeta: true}, {Target: "gmodserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:gmodserver-prebuild", Source: "./scrolls/lgsm/gmodserver", Image: steamImage, Ports: []string{"query=27005/udp", "main=27015/udp", "sourcetv=27020/udp", "steam=27015"}, MinDisk: "8Gi", MinRAM: "512Mi", MinCPU: "0.25", Category: "gmod", Smart: true, PackMeta: true}, - {Target: "cs2server", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:cs2server-prebuild", Source: "./scrolls/lgsm/cs2server", Image: steamImage, Ports: []string{"main=27015/udp", "rcon=27015"}, MinDisk: "38Gi", MinRAM: "1Gi", MinCPU: "0.5", Category: "cs2", Smart: true, PackMeta: true}, + {Target: "cs2server", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:cs2server-prebuild", Source: "./scrolls/lgsm/cs2server", Image: steamImage, Ports: []string{"main=27015/udp", "rcon=27015"}, MinDisk: "38Gi", BuildDisk: "95Gi", MinRAM: "1Gi", MinCPU: "0.5", Category: "cs2", Smart: true, PackMeta: true}, {Target: "pzserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:pzserver-prebuild", Source: "./scrolls/lgsm/pzserver", Image: steamImage, Ports: []string{"main=16261/udp", "main2=16262/udp", "maintcp=16261"}, MinDisk: "3Gi", MinRAM: "512Mi", MinCPU: "0.25", Category: "zomboid", Smart: true, PackMeta: true}, - {Target: "csgoserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:csgoserver-prebuild", Source: "./scrolls/lgsm/csgoserver", Image: steamImage, Ports: []string{"query=27005/udp", "main=27015/udp", "sourcetv=27020/udp", "steam=27015"}, Category: "csgo", Smart: true, PackMeta: true}, - {Target: "rust-vanilla", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest-prebuild", Source: "./scrolls/rust/rust-vanilla/latest", Image: steamImage, Ports: []string{"main=/udp", "query=/udp", "rcon", "rustplus"}, MinDisk: "10Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, - {Target: "rust-oxide", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-oxide:latest-prebuild", Source: "./scrolls/rust/rust-oxide/latest", Image: steamImage, Ports: []string{"main=/udp", "query=/udp", "rcon", "rustplus"}, MinDisk: "10Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, + {Target: "csgoserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:csgoserver-prebuild", Source: "./scrolls/lgsm/csgoserver", Image: steamImage, Ports: []string{"query=27005/udp", "main=27015/udp", "sourcetv=27020/udp", "steam=27015"}, BuildDisk: "45Gi", Category: "csgo", Smart: true, PackMeta: true}, + {Target: "rust-vanilla", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest-prebuild", Source: "./scrolls/rust/rust-vanilla/latest", Image: steamImage, Ports: []string{"main=28015/udp", "query=28017/udp", "rcon=28016", "rustplus=28082"}, MinDisk: "10Gi", BuildDisk: "25Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, + {Target: "rust-oxide", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-oxide:latest-prebuild", Source: "./scrolls/rust/rust-oxide/latest", Image: steamImage, Ports: []string{"main=28015/udp", "query=28017/udp", "rcon=28016", "rustplus=28082"}, MinDisk: "10Gi", BuildDisk: "25Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, } sort.Slice(specs, func(i, j int) bool { return specs[i].Target < specs[j].Target }) return specs diff --git a/scripts/prebuild/main_test.go b/scripts/prebuild/main_test.go index 49fd586e..4392aa8e 100644 --- a/scripts/prebuild/main_test.go +++ b/scripts/prebuild/main_test.go @@ -89,6 +89,20 @@ func TestDayZPrebuildRequiresSteamCredentials(t *testing.T) { } } +func TestRustPrebuildPortsAreConcrete(t *testing.T) { + specs, err := selectSpecs("rust-vanilla,rust-oxide") + if err != nil { + t.Fatal(err) + } + for _, spec := range specs { + for _, port := range spec.Ports { + if port == "main=/udp" || port == "query=/udp" || port == "rcon" || port == "rustplus" { + t.Fatalf("%s has non-concrete port %q", spec.Target, port) + } + } + } +} + func TestValidateRequiredEnvFailsBeforePrebuild(t *testing.T) { t.Setenv("PREBUILD_TEST_REQUIRED", "") err := validateRequiredEnv(prebuildSpec{Target: "test", RequiredEnv: []string{"PREBUILD_TEST_REQUIRED"}}) @@ -97,6 +111,17 @@ func TestValidateRequiredEnvFailsBeforePrebuild(t *testing.T) { } } +func TestParseSizeBytesSupportsBinaryUnits(t *testing.T) { + got, err := parseSizeBytes("1.5Gi") + if err != nil { + t.Fatal(err) + } + want := uint64(1610612736) + if got != want { + t.Fatalf("bytes = %d, want %d", got, want) + } +} + func TestValidateRequiredEnvAcceptsPresentEnv(t *testing.T) { t.Setenv("PREBUILD_TEST_REQUIRED", "present") if err := validateRequiredEnv(prebuildSpec{Target: "test", RequiredEnv: []string{"PREBUILD_TEST_REQUIRED"}}); err != nil { diff --git a/scrolls/lgsm/.build/vars.json b/scrolls/lgsm/.build/vars.json index 8605a5f0..9f5c6895 100644 --- a/scrolls/lgsm/.build/vars.json +++ b/scrolls/lgsm/.build/vars.json @@ -78,7 +78,8 @@ "lua_query_folder": "csgo", "lua_query_map": "server idle", "lua_query_servername": "Druid.gg Server (idle) - join to start", - "main_port_protocol": "udp", + "lua_query_port": "main", + "lua_query_start_on_unknown_packet": "yes", "ppm": "600", "lua_steam_app_id": "730", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" diff --git a/scrolls/lgsm/.build/versions/csgoserver/packet_handler/query.lua b/scrolls/lgsm/.build/versions/csgoserver/packet_handler/query.lua new file mode 100644 index 00000000..8c7c855a --- /dev/null +++ b/scrolls/lgsm/.build/versions/csgoserver/packet_handler/query.lua @@ -0,0 +1,352 @@ +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +function pack_uint64_le(n) + local bytes = {} + for i = 1, 8 do + bytes[i] = string.char(n % 256) + n = math.floor(n / 256) + end + return table.concat(bytes) +end + +function handle(ctx, data) + + -- prtocol begins with FFFFFFFF and the packedid + + -- get packet index + + -- check if start with FFFFFFFF + + hex = string.tohex(data) + + startOnUnknownPacket = get_var("StartOnUnknownPacket") + if string.sub(hex, 1, 8) ~= "FFFFFFFF" then + debug_print("Invalid Packet " .. hex) + + if startOnUnknownPacket == "yes" then + print("Starting server on invalid packet: " .. hex) + finish() + end + return + end + + packetId = string.sub(hex, 9, 10) + + payload = string.sub(hex, 11) + + -- check if packet is 54 + + debug_print("Packet ID: " .. packetId) + + if packetId == "55" then + + if payload == "FFFFFFFF" or payload == "00000000" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex("FFFFFFFF414BA1D522") -- this is not good, as we allways pass the same key for the challenge + ctx.sendData(resHex) + return + end + + if payload == "4BA1D522" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex("FFFFFFFF4400") -- this is not good to be hardcoded, but fine for now + + ctx.sendData(resHex) + return + end + debug_print("Bad challenge: " .. hex) + return + end + + if packetId == "56" then + + if payload == "FFFFFFFF" or payload == "00000000" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex("FFFFFFFF414BA1D522") -- this is not good, as we allways pass the same key for the challenge + ctx.sendData(resHex) + return + end + + if payload == "4BA1D522" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex( + "FFFFFFFF451A00414C4C4F57444F574E4C4F414443484152535F69003100414C4C4F57444F574E4C4F41444954454D535F69003100436C757374657249645F73004B4150323032326E76637738393233386E3332726677653900435553544F4D5345525645524E414D455F73006B617020707670202F20342D6D616E202F2078352D783235202F20776F726B65727320667269656E646C79207365727665720044617954696D655F730037360047616D654D6F64655F73005465737447616D654D6F64655F43004841534143544956454D4F44535F690031004C45474143595F690030004D4154434854494D454F55545F66003132302E303030303030004D4F44305F7300323839373838353837383A4544393730443545343845324143433334333545374339373345434135373637004D4F44315F7300323536343534363435353A3934413336414236343933453241443335364631343142313932383633453445004D4F44325F7300333034363539363536343A3832453245393730343446444139463642464237353439443730433337423133004D4F44335F7300313939393434373137323A3836453432424644343646453430363338443639344141384342453634344134004D6F6449645F6C0030004E6574776F726B696E675F690030004E554D4F50454E505542434F4E4E003530004F4646494349414C5345525645525F690030004F574E494E474944003930323032313035363131373133353337004F574E494E474E414D45003930323032313035363131373133353337005032504144445200393032303231303536313137313335333700503250504F52540037373837005345415243484B4559574F5244535F7300437573746F6D0053657276657250617373776F72645F620066616C73650053455256455255534553424154544C4559455F6200747275650053455353494F4E464C41475300313730370053455353494F4E49535056455F69003000") -- this is not good to be hardcoded, but fine for now + + ctx.sendData(resHex) + return + end + debug_print("Bad challenge: " .. hex) + return + end + + if packetId == "54" then + + + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + + queue = get_queue() + name = get_var("ServerListName") or "Coldstarter is cool (server is idle, join to start)" + + map = get_var("MapName") or "server idle" + + local finishSec = get_finish_sec() + + if finishSec ~= nil then + finishSec = math.ceil(finishSec) + end + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + name = get_var("ServerListNameRestoring") or "EXTRACTING snapshot, this might take a moment" + map = get_var("MapNameRestoring") or "extracting snapshot" + else + name = get_var("ServerListNameRestoring") or "DOWNLOADING snapshot - " .. string.format("%.2f", snapshotPercentage) .. "%" + map = get_var("MapNameRestoring") or "downloading snapshot" + end + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + name = get_var("ServerListNameBackingUp") or "BACKING UP, this might take a moment" + else + name = get_var("ServerListNameBackingUp") or "BACKING UP - " .. string.format("%.2f", snapshotPercentage) .. "%" + end + map = get_var("MapNameBackingUp") or "backing up server" + end + elseif queue ~= nil and queue["install"] == "running" then + if finishSec ~= nil then + -- finish sec is not necissary applicable, but it's better to show something I guess + name = get_var("ServerListNameInstalling") or + string.format("INSTALLING, this might take a moment - %ds", finishSec) + else + name = get_var("ServerListNameInstalling") or "INSTALLING, this might take a moment" + end + + map = get_var("MapNameInstalling") or "installing server" + elseif finishSec ~= nil then + nameTemplate = get_var("ServerListNameStarting") or "Druid Gameserver (starting) - %ds" + name = string.format(nameTemplate, finishSec) + end + + folder = get_var("GameSteamFolder") or "ark_survival_evolved" + + gameName = get_var("GameName") or "ARK: Survival Evolved" + + steamIdString = get_var("GameSteamId") or "0" + gameVersion = get_var("GameVersion") or "1.0.0" + + steamId = tonumber(steamIdString) + steamIdNum = tonumber(steamIdString) + versionPrefix = get_var("GameVersionPrefix") + serverPort = get_port("main") + + + edfGameIdStr = get_var("SteamAppId") + edfGameId = nil + if edfGameIdStr ~= nil then + edfGameId = tonumber(edfGameIdStr) + end + + + -- EDF & 0x80: Port + -- EDF & 0x10: SteamID + -- EDF & 0x20 Keywords + -- EDF & 0x01 GameID + + edfSteamId = "4025ba0000003002" + + + ---rust: "mp0,cp0,ptrak,qp0,$r?,v2592,born0,gmrust,cs1337420" + edfKeywords = get_var("GameKeywords") or ",OWNINGID:90202064633057281,OWNINGNAME:90202064633057281,NUMOPENPUBCONN:50,P2PADDR:90202064633057281,P2PPORT:" .. + serverPort .. ",LEGACY_i:0" + + + serverinfopacket = ServeInfoPacket:new() + serverinfopacket.name = name + serverinfopacket.map = map + serverinfopacket.folder = folder + serverinfopacket.gameName = gameName + serverinfopacket.steamId = steamIdNum + serverinfopacket.player = 0x00 + serverinfopacket.maxPlayer = 0x00 + serverinfopacket.bot = 0x00 + serverinfopacket.serverType = 0x64 -- 64 for dedicated server + serverinfopacket.os = 0x6C -- 6C for linux, 77 for windows + serverinfopacket.visibility = 0x00 + serverinfopacket.version = gameVersion + if versionPrefix ~= nil then + serverinfopacket.versionPrefix = versionPrefix + else + serverinfopacket.versionPrefix = nil + end + + serverinfopacket.edfPort = serverPort + serverinfopacket.edfSteamId = edfSteamId + serverinfopacket.edfKeywords = edfKeywords + serverinfopacket.edfGameId = edfGameId + + + b = serverinfopacket:GetRawPacket() + + ctx.sendData(b) + return + end + + print("Unknown Packet: " .. hex) + if startOnUnknownPacket == "yes" then + print("Starting server on unknown packet: " .. hex) + finish() + end + +end + +function number_to_little_endian_short(num) + -- Ensure the number is in the 16-bit range for unsigned short + if num < 0 or num > 65535 then + error("Number " .. num .. " out of range for 16-bit unsigned short") + end + + -- Convert the number to two bytes in little-endian format + local low_byte = num % 256 -- Least significant byte + local high_byte = math.floor(num / 256) % 256 -- Most significant byte + + -- Format as hexadecimal string + return string.format("%02X%02X", low_byte, high_byte) +end + +Packet = { + bytes = "" +} + + +function Packet:new (packetId) + local o = {} + setmetatable(o, self) + self.__index = self + o.bytes = string.fromhex("FFFFFFFF") .. packetId -- 0xFFFFFFFF + packetId + return o +end + +function Packet:appendString(data) + self.bytes = self.bytes .. data .. string.char(0) +end + +function Packet:appendByte(data) + self.bytes = self.bytes .. string.char(data) +end + +function Packet:appendRawBytes(data) + self.bytes = self.bytes .. data +end + +function Packet:appendShort(num) + self.bytes = self.bytes .. string.fromhex(number_to_little_endian_short(num)) +end + +function Packet:appendHex(hex) + self.bytes = self.bytes .. string.fromhex(hex) +end + +ServeInfoPacket = { + name = "", + map = "", + folder = "", + gameName = "", + steamId = 0, + player = 0x00, + maxPlayer = 0x00, + bot = 0x00, + serverType = 0x64, + os = 0x6C, -- 6C for linux, 77 for windows + visibility = 0x00, -- 01 for private, 00 for public + version = "1.0.0", + versionPrefix = nil, + edfPort = nil, + edfSteamId = nil, + edfSourceTv = nil, + edfKeywords = nil, + edfGameId = nil +} + +function ServeInfoPacket:new () + o = {} + setmetatable(o, self) + self.__index = self + return o +end + + +function ServeInfoPacket:GetRawPacket() + + p = Packet:new(string.fromhex("4911")) -- 0x49 0x11 is the packet id for server info + p:appendString(self.name) + p:appendString(self.map) + p:appendString(self.folder) + p:appendString(self.gameName) + p:appendShort(self.steamId) + p:appendByte(self.player) + p:appendByte(self.maxPlayer) + p:appendByte(self.bot) + p:appendByte(self.serverType) + p:appendByte(self.os) + p:appendByte(self.visibility) -- 01 for private, 00 for public + --p:appendHex("01323032352E30332E323600") + if self.versionPrefix ~= nil then + debug_print("Using version prefix: " .. self.versionPrefix) + p:appendHex(self.versionPrefix) + end + p:appendString(self.version) + debug_print(string.tohex(p.bytes)) + + edfByte = 0x00 + + if self.edfPort ~= nil then + edfByte = edfByte + 0x80 + end + if self.edfSteamId ~= nil then + edfByte = edfByte + 0x10 + end + if self.edfSourceTv ~= nil then + edfByte = edfByte + 0x40 + end + if self.edfKeywords ~= nil then + edfByte = edfByte + 0x20 + end + if self.edfGameId ~= nil then + edfByte = edfByte + 0x01 + end + + p:appendByte(edfByte) + + if self.edfPort ~= nil then + p:appendShort(self.edfPort) + end + if self.edfSteamId ~= nil then + p:appendHex(self.edfSteamId) + end + if self.edfSourceTv ~= nil then + p:appendHex(self.edfSourceTv) + end + if self.edfKeywords ~= nil then + p:appendString(self.edfKeywords) + end + if self.edfGameId ~= nil then + local bytes = pack_uint64_le(self.edfGameId) + p:appendRawBytes(bytes) + end + + + return p.bytes +end diff --git a/scrolls/lgsm/csgoserver/packet_handler/query.lua b/scrolls/lgsm/csgoserver/packet_handler/query.lua new file mode 100644 index 00000000..8c7c855a --- /dev/null +++ b/scrolls/lgsm/csgoserver/packet_handler/query.lua @@ -0,0 +1,352 @@ +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +function pack_uint64_le(n) + local bytes = {} + for i = 1, 8 do + bytes[i] = string.char(n % 256) + n = math.floor(n / 256) + end + return table.concat(bytes) +end + +function handle(ctx, data) + + -- prtocol begins with FFFFFFFF and the packedid + + -- get packet index + + -- check if start with FFFFFFFF + + hex = string.tohex(data) + + startOnUnknownPacket = get_var("StartOnUnknownPacket") + if string.sub(hex, 1, 8) ~= "FFFFFFFF" then + debug_print("Invalid Packet " .. hex) + + if startOnUnknownPacket == "yes" then + print("Starting server on invalid packet: " .. hex) + finish() + end + return + end + + packetId = string.sub(hex, 9, 10) + + payload = string.sub(hex, 11) + + -- check if packet is 54 + + debug_print("Packet ID: " .. packetId) + + if packetId == "55" then + + if payload == "FFFFFFFF" or payload == "00000000" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex("FFFFFFFF414BA1D522") -- this is not good, as we allways pass the same key for the challenge + ctx.sendData(resHex) + return + end + + if payload == "4BA1D522" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex("FFFFFFFF4400") -- this is not good to be hardcoded, but fine for now + + ctx.sendData(resHex) + return + end + debug_print("Bad challenge: " .. hex) + return + end + + if packetId == "56" then + + if payload == "FFFFFFFF" or payload == "00000000" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex("FFFFFFFF414BA1D522") -- this is not good, as we allways pass the same key for the challenge + ctx.sendData(resHex) + return + end + + if payload == "4BA1D522" then + debug_print("Received Packet: " .. hex) + resHex = string.fromhex( + "FFFFFFFF451A00414C4C4F57444F574E4C4F414443484152535F69003100414C4C4F57444F574E4C4F41444954454D535F69003100436C757374657249645F73004B4150323032326E76637738393233386E3332726677653900435553544F4D5345525645524E414D455F73006B617020707670202F20342D6D616E202F2078352D783235202F20776F726B65727320667269656E646C79207365727665720044617954696D655F730037360047616D654D6F64655F73005465737447616D654D6F64655F43004841534143544956454D4F44535F690031004C45474143595F690030004D4154434854494D454F55545F66003132302E303030303030004D4F44305F7300323839373838353837383A4544393730443545343845324143433334333545374339373345434135373637004D4F44315F7300323536343534363435353A3934413336414236343933453241443335364631343142313932383633453445004D4F44325F7300333034363539363536343A3832453245393730343446444139463642464237353439443730433337423133004D4F44335F7300313939393434373137323A3836453432424644343646453430363338443639344141384342453634344134004D6F6449645F6C0030004E6574776F726B696E675F690030004E554D4F50454E505542434F4E4E003530004F4646494349414C5345525645525F690030004F574E494E474944003930323032313035363131373133353337004F574E494E474E414D45003930323032313035363131373133353337005032504144445200393032303231303536313137313335333700503250504F52540037373837005345415243484B4559574F5244535F7300437573746F6D0053657276657250617373776F72645F620066616C73650053455256455255534553424154544C4559455F6200747275650053455353494F4E464C41475300313730370053455353494F4E49535056455F69003000") -- this is not good to be hardcoded, but fine for now + + ctx.sendData(resHex) + return + end + debug_print("Bad challenge: " .. hex) + return + end + + if packetId == "54" then + + + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + + queue = get_queue() + name = get_var("ServerListName") or "Coldstarter is cool (server is idle, join to start)" + + map = get_var("MapName") or "server idle" + + local finishSec = get_finish_sec() + + if finishSec ~= nil then + finishSec = math.ceil(finishSec) + end + + if snapshotMode ~= "idle" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + name = get_var("ServerListNameRestoring") or "EXTRACTING snapshot, this might take a moment" + map = get_var("MapNameRestoring") or "extracting snapshot" + else + name = get_var("ServerListNameRestoring") or "DOWNLOADING snapshot - " .. string.format("%.2f", snapshotPercentage) .. "%" + map = get_var("MapNameRestoring") or "downloading snapshot" + end + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + name = get_var("ServerListNameBackingUp") or "BACKING UP, this might take a moment" + else + name = get_var("ServerListNameBackingUp") or "BACKING UP - " .. string.format("%.2f", snapshotPercentage) .. "%" + end + map = get_var("MapNameBackingUp") or "backing up server" + end + elseif queue ~= nil and queue["install"] == "running" then + if finishSec ~= nil then + -- finish sec is not necissary applicable, but it's better to show something I guess + name = get_var("ServerListNameInstalling") or + string.format("INSTALLING, this might take a moment - %ds", finishSec) + else + name = get_var("ServerListNameInstalling") or "INSTALLING, this might take a moment" + end + + map = get_var("MapNameInstalling") or "installing server" + elseif finishSec ~= nil then + nameTemplate = get_var("ServerListNameStarting") or "Druid Gameserver (starting) - %ds" + name = string.format(nameTemplate, finishSec) + end + + folder = get_var("GameSteamFolder") or "ark_survival_evolved" + + gameName = get_var("GameName") or "ARK: Survival Evolved" + + steamIdString = get_var("GameSteamId") or "0" + gameVersion = get_var("GameVersion") or "1.0.0" + + steamId = tonumber(steamIdString) + steamIdNum = tonumber(steamIdString) + versionPrefix = get_var("GameVersionPrefix") + serverPort = get_port("main") + + + edfGameIdStr = get_var("SteamAppId") + edfGameId = nil + if edfGameIdStr ~= nil then + edfGameId = tonumber(edfGameIdStr) + end + + + -- EDF & 0x80: Port + -- EDF & 0x10: SteamID + -- EDF & 0x20 Keywords + -- EDF & 0x01 GameID + + edfSteamId = "4025ba0000003002" + + + ---rust: "mp0,cp0,ptrak,qp0,$r?,v2592,born0,gmrust,cs1337420" + edfKeywords = get_var("GameKeywords") or ",OWNINGID:90202064633057281,OWNINGNAME:90202064633057281,NUMOPENPUBCONN:50,P2PADDR:90202064633057281,P2PPORT:" .. + serverPort .. ",LEGACY_i:0" + + + serverinfopacket = ServeInfoPacket:new() + serverinfopacket.name = name + serverinfopacket.map = map + serverinfopacket.folder = folder + serverinfopacket.gameName = gameName + serverinfopacket.steamId = steamIdNum + serverinfopacket.player = 0x00 + serverinfopacket.maxPlayer = 0x00 + serverinfopacket.bot = 0x00 + serverinfopacket.serverType = 0x64 -- 64 for dedicated server + serverinfopacket.os = 0x6C -- 6C for linux, 77 for windows + serverinfopacket.visibility = 0x00 + serverinfopacket.version = gameVersion + if versionPrefix ~= nil then + serverinfopacket.versionPrefix = versionPrefix + else + serverinfopacket.versionPrefix = nil + end + + serverinfopacket.edfPort = serverPort + serverinfopacket.edfSteamId = edfSteamId + serverinfopacket.edfKeywords = edfKeywords + serverinfopacket.edfGameId = edfGameId + + + b = serverinfopacket:GetRawPacket() + + ctx.sendData(b) + return + end + + print("Unknown Packet: " .. hex) + if startOnUnknownPacket == "yes" then + print("Starting server on unknown packet: " .. hex) + finish() + end + +end + +function number_to_little_endian_short(num) + -- Ensure the number is in the 16-bit range for unsigned short + if num < 0 or num > 65535 then + error("Number " .. num .. " out of range for 16-bit unsigned short") + end + + -- Convert the number to two bytes in little-endian format + local low_byte = num % 256 -- Least significant byte + local high_byte = math.floor(num / 256) % 256 -- Most significant byte + + -- Format as hexadecimal string + return string.format("%02X%02X", low_byte, high_byte) +end + +Packet = { + bytes = "" +} + + +function Packet:new (packetId) + local o = {} + setmetatable(o, self) + self.__index = self + o.bytes = string.fromhex("FFFFFFFF") .. packetId -- 0xFFFFFFFF + packetId + return o +end + +function Packet:appendString(data) + self.bytes = self.bytes .. data .. string.char(0) +end + +function Packet:appendByte(data) + self.bytes = self.bytes .. string.char(data) +end + +function Packet:appendRawBytes(data) + self.bytes = self.bytes .. data +end + +function Packet:appendShort(num) + self.bytes = self.bytes .. string.fromhex(number_to_little_endian_short(num)) +end + +function Packet:appendHex(hex) + self.bytes = self.bytes .. string.fromhex(hex) +end + +ServeInfoPacket = { + name = "", + map = "", + folder = "", + gameName = "", + steamId = 0, + player = 0x00, + maxPlayer = 0x00, + bot = 0x00, + serverType = 0x64, + os = 0x6C, -- 6C for linux, 77 for windows + visibility = 0x00, -- 01 for private, 00 for public + version = "1.0.0", + versionPrefix = nil, + edfPort = nil, + edfSteamId = nil, + edfSourceTv = nil, + edfKeywords = nil, + edfGameId = nil +} + +function ServeInfoPacket:new () + o = {} + setmetatable(o, self) + self.__index = self + return o +end + + +function ServeInfoPacket:GetRawPacket() + + p = Packet:new(string.fromhex("4911")) -- 0x49 0x11 is the packet id for server info + p:appendString(self.name) + p:appendString(self.map) + p:appendString(self.folder) + p:appendString(self.gameName) + p:appendShort(self.steamId) + p:appendByte(self.player) + p:appendByte(self.maxPlayer) + p:appendByte(self.bot) + p:appendByte(self.serverType) + p:appendByte(self.os) + p:appendByte(self.visibility) -- 01 for private, 00 for public + --p:appendHex("01323032352E30332E323600") + if self.versionPrefix ~= nil then + debug_print("Using version prefix: " .. self.versionPrefix) + p:appendHex(self.versionPrefix) + end + p:appendString(self.version) + debug_print(string.tohex(p.bytes)) + + edfByte = 0x00 + + if self.edfPort ~= nil then + edfByte = edfByte + 0x80 + end + if self.edfSteamId ~= nil then + edfByte = edfByte + 0x10 + end + if self.edfSourceTv ~= nil then + edfByte = edfByte + 0x40 + end + if self.edfKeywords ~= nil then + edfByte = edfByte + 0x20 + end + if self.edfGameId ~= nil then + edfByte = edfByte + 0x01 + end + + p:appendByte(edfByte) + + if self.edfPort ~= nil then + p:appendShort(self.edfPort) + end + if self.edfSteamId ~= nil then + p:appendHex(self.edfSteamId) + end + if self.edfSourceTv ~= nil then + p:appendHex(self.edfSourceTv) + end + if self.edfKeywords ~= nil then + p:appendString(self.edfKeywords) + end + if self.edfGameId ~= nil then + local bytes = pack_uint64_le(self.edfGameId) + p:appendRawBytes(bytes) + end + + + return p.bytes +end diff --git a/scrolls/lgsm/csgoserver/scroll.yaml b/scrolls/lgsm/csgoserver/scroll.yaml index 654e3235..748c832a 100644 --- a/scrolls/lgsm/csgoserver/scroll.yaml +++ b/scrolls/lgsm/csgoserver/scroll.yaml @@ -27,8 +27,6 @@ commands: - id: coldstart image: artifacts.druid.gg/druid-team/druid:v0.1.248 expectedPorts: - - name: query - keepAliveTraffic: 10kb/5m - name: main keepAliveTraffic: 10kb/5m mounts: @@ -37,21 +35,19 @@ commands: working_dir: "/runtime" env: DRUID_ROOT: "/runtime" - DRUID_PORT_QUERY_COLDSTARTER: "packet_handler/query.lua" + DRUID_PORT_MAIN_COLDSTARTER: "packet_handler/query.lua" DRUID_COLDSTARTER_VAR_GAME_NAME: "Counter-Strike: Global Offensive" DRUID_COLDSTARTER_VAR_GAME_STEAM_FOLDER: "csgo" DRUID_COLDSTARTER_VAR_GAME_STEAM_ID: "0" DRUID_COLDSTARTER_VAR_MAP_NAME: "server idle" DRUID_COLDSTARTER_VAR_SERVER_LIST_NAME: "Druid.gg Server (idle) - join to start" + DRUID_COLDSTARTER_VAR_START_ON_UNKNOWN_PACKET: "yes" DRUID_COLDSTARTER_VAR_STEAM_APP_ID: "730" - DRUID_PORT_MAIN_COLDSTARTER: "generic" command: - druid-coldstarter - id: start image: artifacts.druid.gg/druid-team/druid:v0.1.248-steamcmd expectedPorts: - - name: query - keepAliveTraffic: 10kb/5m - name: main keepAliveTraffic: 10kb/5m mounts: