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)