Example Scripts

Update server automatically on update.

--- auto_updater.lua ---
-- note this requires you to have two things:
--   1) configured the server to restart when it shuts down
--   2) update the server right before the server is actually started via steamcmd (via shell script for example)
local json = require("dkjson") -- Ensure you have a JSON library like dkjson or rapidjson
local cs_coroutine = (require 'util/cs_coroutine')

-- state --
update_tick_timer = 0
started_shutdown = false

--- configuration ---
ACE_SQUARED_DEDICATED_SERVER_STEAM_APP_ID = gServerProxy:GetAppId()
OUR_VERSION = gServerProxy:GetVersion()
update_tick_interval = 60
SHUTDOWN_TIME_ON_UPDATE = 60
MESSAGE_ON_SHUTDOWN = "Restarting due to new update."
BUILD_ID_FILENAME = CS.System.IO.Path.Combine(_STREAMING_ASSETS_PATH, "script_data", "auto_updater_current_buildid.txt")

--- helper functions ---
function print_helper(text)
    if (text == nil) then
        text = "nil"
    end
    print("[auto_updater]: " .. text)
end

function read_local_build_id()
    if not CS.System.IO.File.Exists(BUILD_ID_FILENAME) then
        print_helper("Local build ID file not found. Assuming first run.")
        return nil
    end

    local success, content = pcall(CS.System.IO.File.ReadAllText, BUILD_ID_FILENAME)
    if not success then
        print_helper("ERROR: Failed to read local build ID file. Content: " .. tostring(content))
        return nil
    end

    return content
end

function write_local_build_id(id)
    local success, err = pcall(CS.System.IO.File.WriteAllText, BUILD_ID_FILENAME, tostring(id))
    if not success then
        print_helper("ERROR: Could not write build ID file. Details: " .. tostring(err))
        return
    end
    print_helper("Successfully wrote new build ID to disk: " .. tostring(id))
end

function Process()
    if (started_shutdown) then
        print_helper("Shutdown already initiated, skipping check.")
        return
    end

    print_helper("Checking for updates for App ID: " .. ACE_SQUARED_DEDICATED_SERVER_STEAM_APP_ID)

    -- New request string for the steamcmd.net API
    local request_string = "https://api.steamcmd.net/v1/info/" .. ACE_SQUARED_DEDICATED_SERVER_STEAM_APP_ID
    print_helper("Request string: " .. request_string)

    local uwr = CS.UnityEngine.Networking.UnityWebRequest.Get(request_string)
    coroutine.yield(uwr:SendWebRequest())

    if (uwr.result ~= CS.UnityEngine.Networking.UnityWebRequest.Result.Success) then
        print_helper("Request failed: " .. tostring(uwr.error))
        uwr:Dispose()
        return
    end

    local response_text = uwr.downloadHandler.text
    uwr:Dispose() -- Dispose as soon as we have the text

    if (response_text == nil or response_text == "") then
        print_helper("ERROR: API response text is nil or empty.")
        return
    end

    local success, response_object = pcall(json.decode, response_text)
    if not success then
        print_helper("ERROR: Failed to decode JSON response.")
        return
    end

    -- Navigate the new JSON structure to get the buildid
    local app_data = response_object.data[tostring(ACE_SQUARED_DEDICATED_SERVER_STEAM_APP_ID)]
    if not app_data or not app_data.depots or not app_data.depots.branches or not app_data.depots.branches.public then
        print_helper("ERROR: Could not find build ID in API response. JSON structure may have changed.")
        return
    end

    local latest_build_id = app_data.depots.branches.public.buildid
    print_helper("Latest build ID from Steam: " .. latest_build_id)

    local local_build_id = read_local_build_id()
    print_helper("Local build ID from disk: " .. tostring(local_build_id))

    if not local_build_id then
        -- First run: save the current build ID and do nothing.
        write_local_build_id(latest_build_id)
        print_helper("First run detected. Saved latest build ID. No restart will be triggered.")
        return
    end

    if (tostring(latest_build_id) == tostring(local_build_id)) then
        print_helper("Server is up to date.")
        return
    end

    print_helper("New update found! Local: " .. local_build_id .. ", Latest: " .. latest_build_id)
    started_shutdown = true

    -- Write the new build ID before shutting down
    write_local_build_id(latest_build_id)

    gServerProxy:SendServerMessage("Server is not up to date. " .. MESSAGE_ON_SHUTDOWN .. " (" .. tostring(SHUTDOWN_TIME_ON_UPDATE) .. " seconds).")
    gServerProxy:BeginShutdown(SHUTDOWN_TIME_ON_UPDATE)
    gServerProxy:SetShutdownReason(MESSAGE_ON_SHUTDOWN)

    print_helper("Shutdown sequence initiated.")
end

function HandleTick()
    update_tick_timer = update_tick_timer - CS.UnityEngine.Time.deltaTime
    if (update_tick_timer <= 0) then
        update_tick_timer = update_tick_interval
        cs_coroutine.start(Process)
    end
end

RegisterScriptCallback("Tick", HandleTick)

directory = CS.System.IO.Path.GetDirectoryName(BUILD_ID_FILENAME)
print_helper("attempting to create directory... path: " .. directory)
CS.System.IO.Directory.CreateDirectory(directory)

print("[auto_updater]: Script loaded. Update check running every " .. update_tick_interval .. " seconds.")

Send automatic server messages periodically

--- chat_announcer.lua ---
messages = {
    "Want to find teammates, join community events, or just chat? Join our official Discord! Type /discord or find the link in the main menu!",
    "Looking to start a match? Ping some players in Discord with role @ping-for-playtest!",
    "Stay up-to-date with the latest news, patches, and connect with the devs! Join the community on Discord with command /discord"
}

tick_timer = 0
send_message_interval = 180

message_index = 1

function HandleTick()
    tick_timer = tick_timer - CS.UnityEngine.Time.deltaTime
    if (tick_timer <= 0) then
        tick_timer = send_message_interval
        gServerProxy:SendServerMessage(messages[message_index])
        message_index = message_index + 1
        if (message_index > #messages) then
            message_index = 1
        end
    end
end

RegisterScriptCallback("Tick", HandleTick)

Some useful chat commands

--- chat_commands.lua ---

function OnHandleChatCommand(clientProxy, args)
    -- set nickname
    if (args[0] == "nick") then
        if (args.Count < 2) then
            gServerProxy:SendChatMessageToSingle(clientProxy:GetId(), "invalid args, usage: /nick [string]")
            return 1
        end

        local display_username = clientProxy:GetDisplayUsername()
        display_username.username = args[1]
        display_username:SetDirty()
        gServerProxy:SendChatMessageToSingle(clientProxy:GetId(), "nickname updated")
        return 1
    end    

    -- set custom prefix
    if (args[0] == "prefix") then
        if (args.Count < 5) then
            gServerProxy:SendChatMessageToSingle(clientProxy:GetId(), "invalid args, usage: /prefix [string] [r] [g] [b]")
            return 1
        end

        local prefix = args[1]
        local r = tonumber(args[2])
        local g = tonumber(args[3])
        local b = tonumber(args[4])
        local prefix_color = CS.UnityEngine.Color32(r, g, b, 255)

        -- clear previous prefixes
        local display_username = clientProxy:GetDisplayUsername()
        display_username.prefixes:Clear()

        -- create new prefix and add it
        local new_prefix = CS.AceSquared.color_string_t(prefix, prefix_color)
        display_username.prefixes:Add(new_prefix)
        display_username:SetDirty()
        gServerProxy:SendChatMessageToSingle(clientProxy:GetId(), "prefix updated")
        return 1
    end
end

RegisterScriptCallback("OnHandleChatCommand", OnHandleChatCommand)

Set map light blocks

--- voxel_colors.lua ---

-- this sets light blocks on the map based on the lighting data in the map json file.
-- // example map config file
--  /* ...other settings... */
--    "lighting": [
--      "275 27 176 250 250 200 254",
--      "275 27 166 255 0 0 254"
--    ]
--/* ...other settings... */

-- decode helper
function DecodeLightingData(lightingArray)
    local results = {}

    for _, entry in ipairs(lightingArray) do
        -- Split the string into components
        local components = {}
        for value in string.gmatch(entry, "%S+") do
            table.insert(components, tonumber(value))
        end

        -- Ensure the entry has exactly 7 components (X, Y, Z, R, G, B, A)
        if #components == 7 then
            local position = {
                x = components[1],
                y = components[2],
                z = components[3]
            }

            local color = {
                r = components[4],
                g = components[5],
                b = components[6],
                a = components[7]
            }

            table.insert(results, { position = position, color = color })
        else
            print("Invalid lighting entry: " .. entry)
        end
    end

    return results
end

function try_set_lighting(mapName)
    local path = _STREAMING_ASSETS_PATH .. "/Maps/" .. mapName .. ".json"
    print("map_name: " .. mapName)
    package.path = package.path .. ";" .. _STREAMING_ASSETS_PATH .. "/xlua/?.lua"

    local json = require("dkjson") -- Ensure you have a JSON library like dkjson or rapidjson

    local content = CS.System.IO.File.ReadAllText(path)
    local lightcfg = json.decode(content).lighting

    if (lightcfg == nil) then
        print("no lighting data for this map: " .. mapName)
        return
    end

    local decoded_lighting = DecodeLightingData(lightcfg)

    local cs_list = CS.System.Collections.Generic.List(CS.UnityEngine.Vector3Int)()
    for _, data in ipairs(decoded_lighting) do
        cs_list:Clear()
        print(string.format("setting lighting: (%d, %d, %d), Color: (R: %d, G: %d, B: %d, A: %d)",
            data.position.x, data.position.y, data.position.z,
            data.color.r, data.color.g, data.color.b, data.color.a))

        cs_list:Add(CS.UnityEngine.Vector3Int(data.position.x, data.position.y, data.position.z))

        gWorldProxy:PlaceVoxels(cs_list, CS.UnityEngine.Color32(data.color.r, data.color.g, data.color.b, data.color.a))
    end
end

function HandleOnWorldLoaded(mapName)
    try_set_lighting(mapName)
end

RegisterScriptCallback("OnWorldLoaded", HandleOnWorldLoaded)