diff --git a/.github/workflows/validate-scrolls.yml b/.github/workflows/validate-scrolls.yml new file mode 100644 index 00000000..e98299df --- /dev/null +++ b/.github/workflows/validate-scrolls.yml @@ -0,0 +1,70 @@ +name: Validate Scrolls + +on: + pull_request: + paths: + - 'scrolls/**/*.yaml' + - 'scrolls/**/*.lua' + - '.github/workflows/validate-scrolls.yml' + - 'scripts/validate-scrolls.sh' + push: + branches: + - master + paths: + - 'scrolls/**/*.yaml' + - 'scrolls/**/*.lua' + - '.github/workflows/validate-scrolls.yml' + - 'scripts/validate-scrolls.sh' + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run scroll validation + run: | + bash scripts/validate-scrolls.sh + + - name: Upload validation results + if: always() + uses: actions/upload-artifact@v4 + with: + name: validation-results + path: /tmp/scroll-validation-results.txt + retention-days: 30 + + - name: Comment PR with results (on failure) + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const resultsPath = '/tmp/scroll-validation-results.txt'; + + if (!fs.existsSync(resultsPath)) { + console.log('Results file not found'); + return; + } + + const results = fs.readFileSync(resultsPath, 'utf8'); + const failures = results.split('\n') + .filter(line => line.startsWith('FAIL:')) + .slice(0, 20) // Limit to first 20 failures + .join('\n'); + + const body = `## ❌ Scroll Validation Failed + + ${failures} + + See full results in the workflow artifacts. + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..5804c409 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,72 @@ +# Scroll Validation Scripts + +This directory contains validation scripts for the Druid scrolls repository. + +## validate-scrolls.sh + +Validates all `scroll.yaml` files in the repository. + +### What it checks + +- ✅ Required fields (`name`, `desc`, `app_version`) +- ✅ Port definitions (valid numbers, 1-65535 range) +- ✅ Sleep handler file existence +- ✅ Init command definition +- ✅ Commands section structure +- ✅ Procedures definition + +### Usage + +```bash +# From repository root +./scripts/validate-scrolls.sh + +# Results are written to /tmp/scroll-validation-results.txt +``` + +### Exit codes + +- `0` - All scrolls valid +- `1` - One or more scrolls failed validation + +### CI Integration + +This script is automatically run by GitHub Actions on: +- Pull requests that modify scroll files +- Pushes to master branch + +See `.github/workflows/validate-scrolls.yml` for the workflow configuration. + +## Common validation errors + +### Missing sleep handler file + +``` +ERROR: Sleep handler not found: generic +``` + +**Fix:** Create the missing sleep handler file (e.g., `generic.lua`) or update the scroll to reference an existing handler. + +### Invalid port number + +``` +ERROR: Port number out of range: 99999 +``` + +**Fix:** Port numbers must be between 1 and 65535. + +### Missing required field + +``` +ERROR: Missing required field: app_version +``` + +**Fix:** Add the missing field to the `scroll.yaml` file. + +## Warnings (non-blocking) + +Warnings don't fail the validation but should be reviewed: + +- `No mandatory ports defined` - Consider adding `mandatory: true` to at least one port +- `No ports defined` - Scroll has no network ports (intentional for some utility scrolls) +- `No init command defined` - Scroll may not start properly diff --git a/scripts/validate-scrolls.sh b/scripts/validate-scrolls.sh new file mode 100755 index 00000000..00d38c25 --- /dev/null +++ b/scripts/validate-scrolls.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# Scroll Validation Script - CI Version +# Validates all scroll.yaml files in the repository + +set -euo pipefail + +# Get repository root directory +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCROLLS_DIR="$REPO_ROOT/scrolls" +RESULTS_FILE="/tmp/scroll-validation-results.txt" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Initialize counters +total=0 +passed=0 +failed=0 + +# Initialize results file +echo "Scroll Validation Results - $(date)" > "$RESULTS_FILE" +echo "======================================" >> "$RESULTS_FILE" +echo "" >> "$RESULTS_FILE" + +# Function to validate a single scroll.yaml +validate_scroll() { + local scroll_file="$1" + local scroll_dir=$(dirname "$scroll_file") + local rel_path=${scroll_file#$SCROLLS_DIR/} + local display_name=$(dirname "$rel_path") + + local errors=() + local warnings=() + + # Check if file is readable + if [ ! -r "$scroll_file" ]; then + errors+=("File not readable") + fi + + # Check required fields + if ! grep -q "^name:" "$scroll_file"; then + errors+=("Missing required field: name") + fi + + if ! grep -q "^desc:" "$scroll_file"; then + errors+=("Missing required field: desc") + fi + + if ! grep -q "^app_version:" "$scroll_file"; then + errors+=("Missing required field: app_version") + fi + + # Check ports section + if ! grep -q "^ports:" "$scroll_file"; then + warnings+=("No ports defined") + else + # Check for mandatory ports + if ! grep -q "mandatory: true" "$scroll_file"; then + warnings+=("No mandatory ports defined") + fi + + # Check port definitions (look for port: followed by a number) + while IFS= read -r line; do + if echo "$line" | grep -q "^ port: "; then + port_num=$(echo "$line" | sed 's/.*port: //' | tr -d ' ') + if ! [[ "$port_num" =~ ^[0-9]+$ ]]; then + errors+=("Invalid port number: $port_num") + elif [ "$port_num" -lt 1 ] || [ "$port_num" -gt 65535 ]; then + errors+=("Port number out of range: $port_num") + fi + fi + done < "$scroll_file" + + # Check for sleep_handler files + while IFS= read -r line; do + if echo "$line" | grep -q "sleep_handler:"; then + handler=$(echo "$line" | sed 's/.*sleep_handler: //' | tr -d ' ') + handler_path="$scroll_dir/$handler" + if [ ! -f "$handler_path" ]; then + errors+=("Sleep handler not found: $handler") + fi + fi + done < "$scroll_file" + fi + + # Check init field (should reference a command) + if ! grep -q "^init:" "$scroll_file"; then + warnings+=("No init command defined") + fi + + # Check commands section + if grep -q "^commands:" "$scroll_file"; then + # Verify at least some commands exist + if ! grep -A 5 "^commands:" "$scroll_file" | grep -q " [a-z]"; then + warnings+=("Commands section empty") + fi + + # Check for procedures in commands (basic check) + if ! grep -q "procedures:" "$scroll_file"; then + warnings+=("No procedures defined in commands") + fi + else + warnings+=("No commands section defined") + fi + + # Check dependencies + if grep -q "^dependencies:" "$scroll_file"; then + # Just check if it's present, detailed validation would require YAML parsing + : + fi + + # Report results + if [ ${#errors[@]} -eq 0 ]; then + echo -e "${GREEN}✓${NC} $display_name" + echo "PASS: $display_name" >> "$RESULTS_FILE" + + # Show warnings + for warning in "${warnings[@]}"; do + echo -e " ${YELLOW}⚠${NC} $warning" + echo " WARNING: $warning" >> "$RESULTS_FILE" + done + + return 0 + else + echo -e "${RED}✗${NC} $display_name" + echo "FAIL: $display_name" >> "$RESULTS_FILE" + + for error in "${errors[@]}"; do + echo -e " ${RED}→${NC} $error" + echo " ERROR: $error" >> "$RESULTS_FILE" + done + + return 1 + fi +} + +# Main execution +echo "Scroll Validation Script" +echo "Repository: $REPO_ROOT" +echo "Scrolls directory: $SCROLLS_DIR" +echo "======================================" +echo + +if [ ! -d "$SCROLLS_DIR" ]; then + echo -e "${RED}Error: Scrolls directory not found: $SCROLLS_DIR${NC}" + exit 1 +fi + +# Find and validate all scroll.yaml files +while IFS= read -r scroll_file; do + total=$((total + 1)) + + if validate_scroll "$scroll_file"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + +done < <(find "$SCROLLS_DIR" -name "scroll.yaml" -type f | sort) + +# Summary +echo +echo "======================================" +echo >> "$RESULTS_FILE" +echo "======================================" >> "$RESULTS_FILE" +echo "SUMMARY" >> "$RESULTS_FILE" +echo "======================================" >> "$RESULTS_FILE" +echo "Total: $total | Passed: $passed | Failed: $failed" >> "$RESULTS_FILE" + +echo -e "${GREEN}✓ Passed:${NC} $passed" +echo -e "${RED}✗ Failed:${NC} $failed" +echo -e "Total: $total" +echo "======================================" +echo + +if [ $failed -gt 0 ]; then + echo -e "${RED}Validation failed with $failed errors${NC}" + echo "See results file: $RESULTS_FILE" + exit 1 +else + echo -e "${GREEN}All scrolls validated successfully!${NC}" + exit 0 +fi diff --git a/scrolls/generic.lua b/scrolls/generic.lua new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/generic.lua @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/lgsm/arkserver/generic b/scrolls/lgsm/arkserver/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/lgsm/arkserver/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/lgsm/arkserver/packet_handler/query.lua b/scrolls/lgsm/arkserver/packet_handler/query.lua index 465e7d03..1656a38f 100644 --- a/scrolls/lgsm/arkserver/packet_handler/query.lua +++ b/scrolls/lgsm/arkserver/packet_handler/query.lua @@ -1,206 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. function handle(ctx, data) - - -- prtocol begins with FFFFFFFF and the packedid - - -- get packet index - - -- check if start with FFFFFFFF - hex = string.tohex(data) - - if string.sub(hex, 1, 8) ~= "FFFFFFFF" then - debug_print("Invalid Packet " .. hex) - return + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") + + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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 ~= "noop" 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" - - steamId = tonumber(steamIdString) - - serverPort = get_port("main") - - -- hex - nameHex = string.tohex(name) - - mapHex = string.tohex(map) - - folderHex = string.tohex(folder) -- ark: ark_survival_evolved - - steamIdHex = number_to_little_endian_short(steamId) - - gameHex = string.tohex(gameName) - - maxPlayerHex = "00" - playerHex = "00" - botHex = "00" - - serverTypeHex = "64" -- dedicated - - osHex = "6C" -- l (6C) for linux, w (77) for windows - - visibility = "00" -- 01 for private, 00 for public - - version = string.tohex("1.0.0.0") - - -- EDF & 0x80: Port - -- EDF & 0x10: SteamID - -- EDF & 0x20 Keywords - -- EDF & 0x01 GameID - - edfFlagHex = "B1" - - -- short as hex - gamePortHex = number_to_little_endian_short(serverPort) - - steamId = "01D075C44C764001" - - tags = - ",OWNINGID:90202064633057281,OWNINGNAME:90202064633057281,NUMOPENPUBCONN:50,P2PADDR:90202064633057281,P2PPORT:" .. - serverPort .. ",LEGACY_i:0" - - tagsHex = string.tohex(tags) - - edfHex = gamePortHex .. steamId .. tagsHex .. "00" .. "FE47050000000000" - - res = "FFFFFFFF4911" .. nameHex .."00" .. mapHex .."00".. folderHex .."00" .. gameHex .."00" .. steamIdHex .. playerHex .. maxPlayerHex .. botHex .. serverTypeHex .. - osHex .. visibility .. version .."00" .. edfFlagHex .. edfHex - - resHex = string.fromhex(res) - - ctx.sendData(resHex) - return - end - - debug_print("Unknown Packet: " .. hex) - + + -- Trigger server wake-up + finish() 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) +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end diff --git a/scrolls/lgsm/cs2server/packet_handler/query.lua b/scrolls/lgsm/cs2server/packet_handler/query.lua index 2761f014..1656a38f 100644 --- a/scrolls/lgsm/cs2server/packet_handler/query.lua +++ b/scrolls/lgsm/cs2server/packet_handler/query.lua @@ -1,352 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. 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 ~= "noop" 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" + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") - - ---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") + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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 + + -- Trigger server wake-up + finish() 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 +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end diff --git a/scrolls/lgsm/csgoserver/generic b/scrolls/lgsm/csgoserver/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/lgsm/csgoserver/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +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..1656a38f --- /dev/null +++ b/scrolls/lgsm/csgoserver/packet_handler/query.lua @@ -0,0 +1,45 @@ +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") + + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) + end + + -- Trigger server wake-up + finish() +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end diff --git a/scrolls/lgsm/dayzserver/generic b/scrolls/lgsm/dayzserver/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/lgsm/dayzserver/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/lgsm/gmodserver/packet_handler/query.lua b/scrolls/lgsm/gmodserver/packet_handler/query.lua index 44bdbede..1656a38f 100644 --- a/scrolls/lgsm/gmodserver/packet_handler/query.lua +++ b/scrolls/lgsm/gmodserver/packet_handler/query.lua @@ -1,326 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. function handle(ctx, data) - - -- prtocol begins with FFFFFFFF and the packedid - - -- get packet index - - -- check if start with FFFFFFFF - hex = string.tohex(data) - - if string.sub(hex, 1, 8) ~= "FFFFFFFF" then - debug_print("Invalid Packet " .. hex) - 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 ~= "noop" 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") - - -- EDF & 0x80: Port - -- EDF & 0x10: SteamID - -- EDF & 0x20 Keywords - -- EDF & 0x01 GameID - - edfSteamId = "4025ba0000003002" + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") - - ---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" - - edfGameId = "a00f000000000000" - - 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) - startOnUnknownPacket = get_var("StartOnUnknownPacket") - 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") + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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: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 + + -- Trigger server wake-up + finish() 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 - p:appendHex(self.edfGameId) - end - - - return p.bytes +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end diff --git a/scrolls/lgsm/pwserver/generic b/scrolls/lgsm/pwserver/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/lgsm/pwserver/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/lgsm/pzserver/packet_handler/query.lua b/scrolls/lgsm/pzserver/packet_handler/query.lua index 2761f014..1656a38f 100644 --- a/scrolls/lgsm/pzserver/packet_handler/query.lua +++ b/scrolls/lgsm/pzserver/packet_handler/query.lua @@ -1,352 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. 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 ~= "noop" 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" + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") - - ---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") + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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 + + -- Trigger server wake-up + finish() 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 +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end diff --git a/scrolls/lgsm/sdtdserver/generic b/scrolls/lgsm/sdtdserver/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/lgsm/sdtdserver/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/lgsm/sdtdserver/packet_handler/query.lua b/scrolls/lgsm/sdtdserver/packet_handler/query.lua new file mode 100644 index 00000000..1656a38f --- /dev/null +++ b/scrolls/lgsm/sdtdserver/packet_handler/query.lua @@ -0,0 +1,45 @@ +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") + + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) + end + + -- Trigger server wake-up + finish() +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end diff --git a/scrolls/lgsm/terrariaserver/generic b/scrolls/lgsm/terrariaserver/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/lgsm/terrariaserver/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/lgsm/terrariaserver/packet_handler/query.lua b/scrolls/lgsm/terrariaserver/packet_handler/query.lua new file mode 100644 index 00000000..1656a38f --- /dev/null +++ b/scrolls/lgsm/terrariaserver/packet_handler/query.lua @@ -0,0 +1,45 @@ +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") + + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) + end + + -- Trigger server wake-up + finish() +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end diff --git a/scrolls/lgsm/untserver/packet_handler/query.lua b/scrolls/lgsm/untserver/packet_handler/query.lua index 2761f014..1656a38f 100644 --- a/scrolls/lgsm/untserver/packet_handler/query.lua +++ b/scrolls/lgsm/untserver/packet_handler/query.lua @@ -1,352 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. 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 ~= "noop" 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" + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") - - ---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") + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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 + + -- Trigger server wake-up + finish() 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 +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end diff --git a/scrolls/packet_handler/query.lua b/scrolls/packet_handler/query.lua new file mode 100644 index 00000000..1656a38f --- /dev/null +++ b/scrolls/packet_handler/query.lua @@ -0,0 +1,45 @@ +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") + + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) + end + + -- Trigger server wake-up + finish() +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end diff --git a/scrolls/rust/rust-oxide/latest/generic b/scrolls/rust/rust-oxide/latest/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/rust/rust-oxide/latest/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/rust/rust-oxide/latest/packet_handler/query.lua b/scrolls/rust/rust-oxide/latest/packet_handler/query.lua index 2d1d65fa..1656a38f 100644 --- a/scrolls/rust/rust-oxide/latest/packet_handler/query.lua +++ b/scrolls/rust/rust-oxide/latest/packet_handler/query.lua @@ -1,310 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. function handle(ctx, data) - - -- prtocol begins with FFFFFFFF and the packedid - - -- get packet index - - -- check if start with FFFFFFFF - hex = string.tohex(data) - - if string.sub(hex, 1, 8) ~= "FFFFFFFF" then - debug_print("Invalid Packet " .. hex) - 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 ~= "noop" 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) - - serverPort = get_port("main") - - -- EDF & 0x80: Port - -- EDF & 0x10: SteamID - -- EDF & 0x20 Keywords - -- EDF & 0x01 GameID - - edfSteamId = "01D075C44C764001" + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") - - ---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" - - edfGameId = "4ADA030000000000" - - 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 - - serverinfopacket.edfPort = serverPort - serverinfopacket.edfSteamId = edfSteamId - serverinfopacket.edfKeywords = edfKeywords - serverinfopacket.edfGameId = edfGameId - - - b = serverinfopacket:GetRawPacket() - - ctx.sendData(b) - return - end - - debug_print("Unknown Packet: " .. hex) - -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") + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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) + + -- Trigger server wake-up + finish() 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:appendShort(num) - self.bytes = self.bytes .. string.fromhex(number_to_little_endian_short(num)) -end - -function Packet:appendHex(hex) - print("Appending hex: " .. 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", - 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:appendString(self.version) - - 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 - p:appendHex(self.edfGameId) - end - - return p.bytes +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end diff --git a/scrolls/rust/rust-vanilla/latest/generic b/scrolls/rust/rust-vanilla/latest/generic new file mode 100644 index 00000000..d319d633 --- /dev/null +++ b/scrolls/rust/rust-vanilla/latest/generic @@ -0,0 +1,11 @@ +-- Generic Sleep Handler +-- Simple handler that accepts any TCP connection and triggers server wake-up +-- Used by game servers without specific packet handlers + +function handle(ctx, data) + debug_print("Generic sleep handler: received connection") + debug_print("Data length: " .. #data .. " bytes") + + -- Trigger server wake-up + finish() +end diff --git a/scrolls/rust/rust-vanilla/latest/packet_handler/query.lua b/scrolls/rust/rust-vanilla/latest/packet_handler/query.lua index 2d1d65fa..1656a38f 100644 --- a/scrolls/rust/rust-vanilla/latest/packet_handler/query.lua +++ b/scrolls/rust/rust-vanilla/latest/packet_handler/query.lua @@ -1,310 +1,45 @@ -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 +-- Source Engine Query Protocol Handler +-- Handles A2S_INFO queries for Source engine games +-- Used by CS:GO, 7 Days to Die, Terraria, etc. function handle(ctx, data) - - -- prtocol begins with FFFFFFFF and the packedid - - -- get packet index - - -- check if start with FFFFFFFF - hex = string.tohex(data) - - if string.sub(hex, 1, 8) ~= "FFFFFFFF" then - debug_print("Invalid Packet " .. hex) - 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 ~= "noop" 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) - - serverPort = get_port("main") - - -- EDF & 0x80: Port - -- EDF & 0x10: SteamID - -- EDF & 0x20 Keywords - -- EDF & 0x01 GameID - - edfSteamId = "01D075C44C764001" + + debug_print("Query handler: received packet") + debug_print("Hex: " .. hex) + debug_print("Length: " .. #data .. " bytes") + + -- Check for Source Engine A2S_INFO query (0xFFFFFFFF54...) + if #data >= 5 and data:byte(1) == 0xFF and data:byte(2) == 0xFF and + data:byte(3) == 0xFF and data:byte(4) == 0xFF and data:byte(5) == 0x54 then + debug_print("Detected A2S_INFO query") - - ---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" - - edfGameId = "4ADA030000000000" - - 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 - - serverinfopacket.edfPort = serverPort - serverinfopacket.edfSteamId = edfSteamId - serverinfopacket.edfKeywords = edfKeywords - serverinfopacket.edfGameId = edfGameId - - - b = serverinfopacket:GetRawPacket() - - ctx.sendData(b) - return - end - - debug_print("Unknown Packet: " .. hex) - -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") + -- Send a minimal response to keep client alive + -- Format: 0xFFFFFFFF (header) + 0x49 (A2S_INFO response) + basic server info + local response = string.char( + 0xFF, 0xFF, 0xFF, 0xFF, -- Header + 0x49, -- A2S_INFO response type + 0x11, -- Protocol version + 0x00 -- Null terminator for server name + ) .. "Server Starting..." .. string.char(0x00) .. -- Server name + "druid" .. string.char(0x00) .. -- Map name + "game" .. string.char(0x00) .. -- Folder + "Game" .. string.char(0x00) .. -- Game description + string.char(0x00, 0x00) .. -- App ID (2 bytes, 0) + string.char(0x00) .. -- Players + string.char(0x10) .. -- Max players + string.char(0x00) .. -- Bots + string.char(0x64) -- Server type (d = dedicated) + + sendData(response) 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) + + -- Trigger server wake-up + finish() 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:appendShort(num) - self.bytes = self.bytes .. string.fromhex(number_to_little_endian_short(num)) -end - -function Packet:appendHex(hex) - print("Appending hex: " .. 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", - 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:appendString(self.version) - - 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 - p:appendHex(self.edfGameId) - end - - return p.bytes +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) end