1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-10-30 23:47:41 +02:00

server utils

This commit is contained in:
MewMew
2019-02-20 11:43:29 +01:00
parent 38c4cd1b3f
commit 3e63fa3362
13 changed files with 1090 additions and 57 deletions

46
bot.lua
View File

@@ -1,46 +0,0 @@
local Event = require "utils.event"
Event.add(defines.events.on_player_died, function (event)
local player = event.player_index
if game.players[player].name ~= nil then
print("PLAYER$die," .. player .. "," .. game.players[player].name .. "," .. game.players[player].force.name)
end
end)
Event.add(defines.events.on_player_respawned, function (event)
local player = event.player_index
if game.players[player].name ~= nil then
print("PLAYER$respawn," .. player .. "," .. game.players[player].name .. "," .. game.players[player].force.name)
end
end)
Event.add(defines.events.on_player_joined_game, function (event)
local player = event.player_index
if game.players[player].name ~= nil then
print("PLAYER$join," .. player .. "," .. game.players[player].name .. "," .. game.players[player].force.name)
end
end)
Event.add(defines.events.on_player_left_game, function (event)
local player = event.player_index
if game.players[player].name ~= nil then
print("PLAYER$leave," .. player .. "," .. game.players[player].name .. "," .. game.players[player].force.name)
end
end)
function heartbeat()
--Do nothing, this is just so managepgm can call something as a heartbeat without any errors occurring
end
function playerQuery()
if #game.connected_players == 0 then
print("output$pquery$none")
else
local response = "output&pquery$"
for _,player in pairs(game.connected_players) do
local playerdata = player.name .. "-" .. player.force.name
response = response .. playerdata .. ","
end
print(response:sub(1,#str-1))
end
end

View File

@@ -1,6 +1,7 @@
server_commands = require 'utils.server'
require "utils.server_commands"
require "utils.utils"
require "utils.corpse_util"
require "bot"
require "chatbot"
require "commands"
require "session_tracker"
@@ -39,8 +40,8 @@ require "score"
--require "maps.tank_battles"
--require "maps.spiral_troopers"
--require "maps.fish_defender"
require "maps.mountain_fortress"
--require "maps.stoneblock"
--require "maps.mountain_fortress"
require "maps.stoneblock"
--require "maps.deep_jungle"
--require "maps.crossing"
--require "maps.anarchy"

View File

@@ -812,6 +812,21 @@ local function is_game_lost()
local l = t.add({type = "label", caption = mvp.deaths.name .. " died " .. mvp.deaths.score .. " times"})
l.style.font = "default-bold"
l.style.font_color = {r=0.33, g=0.66, b=0.9}
if not global.results_sent then
local result = {}
insert(result, 'MVP Defender: \\n')
insert(result, mvp.killscore.name .. " with a score of " .. mvp.killscore.score .. "\\n" )
insert(result, '\\n')
insert(result, 'MVP Builder: \\n')
insert(result, mvp.built_entities.name .. " built " .. mvp.built_entities.score .. " things\\n" )
insert(result, '\\n')
insert(result, 'MVP Deaths: \\n')
insert(result, mvp.deaths.name .. " died " .. mvp.deaths.score .. " times" )
local message = table.concat(result)
server_commands.to_discord_embed(message)
global.results_sent = true
end
end
for _, player in pairs(game.connected_players) do
@@ -1341,7 +1356,12 @@ local function on_tick()
if global.game_restart_timer > 0 then game.print("Map will restart in " .. global.game_restart_timer / 60 .. " seconds!", { r=0.22, g=0.88, b=0.22}) end
if global.game_restart_timer == 0 then
game.print("Map is restarting!", { r=0.22, g=0.88, b=0.22})
game.write_file("commandPipe", ":loadscenario --force", false, 0)
--game.write_file("commandPipe", ":loadscenario --force", false, 0)
local message = 'Map is restarting! '
server_commands.to_discord_bold(table.concat{'*** ', message, ' ***'})
server_commands.start_scenario('Fish_Defender')
end
end
end

View File

@@ -3,9 +3,9 @@
local event = require 'utils.event'
local biter_values = {
["medium-biter"] = {"blood-explosion-big", 25, 1.5},
["big-biter"] = {"blood-explosion-huge", 50, 2},
["behemoth-biter"] = {"blood-explosion-huge", 75, 2.5}
["medium-biter"] = {"blood-explosion-big", 20, 1.5},
["big-biter"] = {"blood-explosion-huge", 40, 2},
["behemoth-biter"] = {"blood-explosion-huge", 60, 2.5}
}
local function damage_entities_in_radius(surface, position, radius, damage)
@@ -16,8 +16,7 @@ local function damage_entities_in_radius(surface, position, radius, damage)
if entity.name == "player" then
entity.damage(damage, "enemy")
else
entity.health = entity.health - damage
--entity.surface.create_entity({name = "blood-explosion-big", position = entity.position})
entity.health = entity.health - damage
if entity.health <= 0 then entity.die("enemy") end
end
end
@@ -30,7 +29,12 @@ local function on_entity_died(event)
if biter_values[event.entity.name] then
local entity = event.entity
entity.surface.create_entity({name = biter_values[entity.name][1], position = entity.position})
damage_entities_in_radius(entity.surface, entity.position, biter_values[entity.name][3], biter_values[entity.name][2])
damage_entities_in_radius(
entity.surface,
entity.position,
biter_values[entity.name][3],
math.random(math.ceil(biter_values[entity.name][2] * 0.75), math.ceil(biter_values[entity.name][2] * 1.25))
)
end
end

View File

@@ -5,7 +5,7 @@ require "maps.modules.biters_double_damage"
require "maps.modules.biters_double_hp"
require "maps.modules.biters_yield_coins"
require "maps.modules.dynamic_landfill"
require "maps.modules.dynamic_player_spawn"
--require "maps.modules.dynamic_player_spawn"
require "maps.modules.explosive_biters"
require "maps.modules.rocks_broken_paint_tiles"
require "maps.modules.rocks_heal_over_time"

View File

@@ -349,6 +349,32 @@ local function on_marked_for_deconstruction(event)
end
end
local function on_tick(event)
if game.tick % 3600 ~= 1 then return end
if math_random(1,8) ~= 1 then return end
local surface = game.surfaces["mountain_fortress"]
local spawners = surface.find_entities_filtered({force = "enemy", type = "unit-spawner"})
if not spawners[1] then return end
local target = surface.find_nearest_enemy({position = spawners[math_random(1, #spawners)].position, max_distance=1500, force="enemy"})
if not target then return end
surface.set_multi_command({
command={
type=defines.command.attack_area,
destination=target.position,
radius=16,
distraction=defines.distraction.by_anything
},
unit_count = math_random(6,12),
force = "enemy",
unit_search_distance=1024
})
end
event.add(defines.events.on_tick, on_tick)
event.add(defines.events.on_chunk_charted, on_chunk_charted)
event.add(defines.events.on_entity_damaged, on_entity_damaged)
event.add(defines.events.on_marked_for_deconstruction, on_marked_for_deconstruction)

125
utils/event_core.lua Normal file
View File

@@ -0,0 +1,125 @@
-- This module exists to break the circular dependency between event.lua and global.lua.
-- It is not expected that any user code would require this module instead event.lua should be required.
local Public = {}
local init_event_name = -1
local load_event_name = -2
-- map of event_name to handlers[]
local event_handlers = {}
-- map of nth_tick to handlers[]
local on_nth_tick_event_handlers = {}
local function call_handlers(handlers, event)
if _DEBUG then
for _, handler in ipairs(handlers) do
handler(event)
end
else
for _, handler in ipairs(handlers) do
local success, error = pcall(handler, event)
if not success then
log(error)
end
end
end
end
local function on_event(event)
local handlers = event_handlers[event.name]
call_handlers(handlers, event)
end
local function on_init()
_LIFECYCLE = 5 -- on_init
local handlers = event_handlers[init_event_name]
call_handlers(handlers)
event_handlers[init_event_name] = nil
event_handlers[load_event_name] = nil
_LIFECYCLE = 8 -- Runtime
end
local function on_load()
_LIFECYCLE = 6 -- on_load
local handlers = event_handlers[load_event_name]
call_handlers(handlers)
event_handlers[init_event_name] = nil
event_handlers[load_event_name] = nil
_LIFECYCLE = 8 -- Runtime
end
local function on_nth_tick_event(event)
local handlers = on_nth_tick_event_handlers[event.nth_tick]
call_handlers(handlers, event)
end
--- Do not use this function, use Event.add instead as it has safety checks.
function Public.add(event_name, handler)
local handlers = event_handlers[event_name]
if not handlers then
event_handlers[event_name] = {handler}
script.on_event(event_name, on_event)
else
table.insert(handlers, handler)
if #handlers == 1 then
script.on_event(event_name, on_event)
end
end
end
--- Do not use this function, use Event.on_init instead as it has safety checks.
function Public.on_init(handler)
local handlers = event_handlers[init_event_name]
if not handlers then
event_handlers[init_event_name] = {handler}
script.on_init(on_init)
else
table.insert(handlers, handler)
if #handlers == 1 then
script.on_init(on_init)
end
end
end
--- Do not use this function, use Event.on_load instead as it has safety checks.
function Public.on_load(handler)
local handlers = event_handlers[load_event_name]
if not handlers then
event_handlers[load_event_name] = {handler}
script.on_load(on_load)
else
table.insert(handlers, handler)
if #handlers == 1 then
script.on_load(on_load)
end
end
end
--- Do not use this function, use Event.on_nth_tick instead as it has safety checks.
function Public.on_nth_tick(tick, handler)
local handlers = on_nth_tick_event_handlers[tick]
if not handlers then
on_nth_tick_event_handlers[tick] = {handler}
script.on_nth_tick(tick, on_nth_tick_event)
else
table.insert(handlers, handler)
if #handlers == 1 then
script.on_nth_tick(tick, on_nth_tick_event)
end
end
end
function Public.get_event_handlers()
return event_handlers
end
function Public.get_on_nth_tick_event_handlers()
return on_nth_tick_event_handlers
end
return Public

116
utils/game.lua Normal file
View File

@@ -0,0 +1,116 @@
local Global = require 'utils.global'
local pairs = pairs
local Game = {}
local bad_name_players = {}
Global.register(
bad_name_players,
function(tbl)
bad_name_players = tbl
end
)
--[[
Due to a bug in the Factorio api the following expression isn't guaranteed to be true.
game.players[player.index] == player
get_player_by_index(index) will always return the correct player.
When looking up players by name or iterating through all players use game.players instead.
]]
function Game.get_player_by_index(index)
local p = game.players[index]
if not p then
return nil
end
if p.index == index then
return p
end
p = bad_name_players[index]
if p then
if p.valid then
return p
else
return nil
end
end
for k, v in pairs(game.players) do
if k == index then
bad_name_players[index] = v
return v
end
end
end
--- Returns a valid LuaPlayer if given a number, string, or LuaPlayer. Returns nil otherwise.
-- obj <number|string|LuaPlayer>
function Game.get_player_from_any(obj)
local o_type = type(obj)
local p
if type == 'number' then
p = Game.get_player_by_index(obj)
elseif o_type == 'string' then
p = game.players[obj]
elseif o_type == 'table' and obj.valid and obj.is_player() then
return obj
end
if p and p.valid then
return p
end
end
--- Prints to player or console.
function Game.player_print(str)
if game.player then
game.player.print(str)
else
print(str)
end
end
--[[
@param Position String to display at
@param text String to display
@param color table in {r = 0~1, g = 0~1, b = 0~1}, defaults to white.
@param surface LuaSurface
@return the created entity
]]
function Game.print_floating_text(surface, position, text, color)
color = color
return surface.create_entity {
name = 'tutorial-flying-text',
color = color,
text = text,
position = position
}
end
--[[
Creates a floating text entity at the player location with the specified color in {r, g, b} format.
Example: "+10 iron" or "-10 coins"
@param text String to display
@param color table in {r = 0~1, g = 0~1, b = 0~1}, defaults to white.
@return the created entity
]]
function Game.print_player_floating_text_position(player_index, text, color, x_offset, y_offset)
local player = Game.get_player_by_index(player_index)
if not player or not player.valid then
return
end
local position = player.position
return Game.print_floating_text(player.surface, {x = position.x + x_offset, y = position.y + y_offset}, text, color)
end
function Game.print_player_floating_text(player_index, text, color)
Game.print_player_floating_text_position(player_index, text, color, 0, -1.5)
end
return Game

13
utils/print_override.lua Normal file
View File

@@ -0,0 +1,13 @@
local Public = {}
local locale_string = {'', '[PRINT] ', nil}
local raw_print = print
function print(str)
locale_string[3] = str
log(locale_string)
end
Public.raw_print = raw_print
return Public

539
utils/server.lua Normal file
View File

@@ -0,0 +1,539 @@
local Token = require 'utils.token'
local Global = require 'utils.global'
local Event = require 'utils.event'
local Game = require 'utils.game'
local Timestamp = require 'utils.timestamp'
local Print = require('utils.print_override')
local serialize = serpent.serialize
local concat = table.concat
local remove = table.remove
local tostring = tostring
local raw_print = Print.raw_print
local serialize_options = {sparse = true, compact = true}
local Public = {}
local server_time = {secs = nil, tick = 0}
Global.register(
server_time,
function(tbl)
server_time = tbl
end
)
local discord_tag = '[DISCORD]'
local discord_raw_tag = '[DISCORD-RAW]'
local discord_bold_tag = '[DISCORD-BOLD]'
local discord_admin_tag = '[DISCORD-ADMIN]'
local discord_admin_raw_tag = '[DISCORD-ADMIN-RAW]'
local discord_embed_tag = '[DISCORD-EMBED]'
local discord_embed_raw_tag = '[DISCORD-EMBED-RAW]'
local discord_admin_embed_tag = '[DISCORD-ADMIN-EMBED]'
local discord_admin_embed_raw_tag = '[DISCORD-ADMIN-EMBED-RAW]'
local start_scenario_tag = '[START-SCENARIO]'
local ping_tag = '[PING]'
local data_set_tag = '[DATA-SET]'
local data_get_tag = '[DATA-GET]'
local data_get_all_tag = '[DATA-GET-ALL]'
local data_tracked_tag = '[DATA-TRACKED]'
local ban_sync_tag = '[BAN-SYNC]'
local unbanned_sync_tag = '[UNBANNED-SYNC]'
local query_players_tag = '[QUERY-PLAYERS]'
local player_join_tag = '[PLAYER-JOIN]'
local player_leave_tag = '[PLAYER-LEAVE]'
Public.raw_print = raw_print
local data_set_handlers = {}
--- The event id for the on_server_started event.
-- The event is raised whenever the server goes from the starting state to the running state.
-- It provides a good opportunity to request data from the web server.
-- Note that if the server is stopped then started again, this event will be raised again.
-- @usage
-- local Server = require 'utils.server'
-- local Event = require 'utils.event'
--
-- Event.add(Server.events.on_server_started,
-- function()
-- Server.try_get_all_data('regulars', callback)
-- end)
Public.events = {on_server_started = script.generate_event_name()}
--- Sends a message to the linked discord channel. The message is sanitized of markdown server side.
-- @param message<string> message to send.
-- @usage
-- local Server = require 'utils.server'
-- Server.to_discord('Hello from scenario script!')
function Public.to_discord(message)
raw_print(discord_tag .. message)
end
--- Sends a message to the linked discord channel. The message is not sanitized of markdown.
-- @param message<string> message to send.
function Public.to_discord_raw(message)
raw_print(discord_raw_tag .. message)
end
--- Sends a message to the linked discord channel. The message is sanitized of markdown server side, then made bold.
-- @param message<string> message to send.
function Public.to_discord_bold(message)
raw_print(discord_bold_tag .. message)
end
--- Sends a message to the linked admin discord channel. The message is sanitized of markdown server side.
-- @param message<string> message to send.
function Public.to_admin(message)
raw_print(discord_admin_tag .. message)
end
--- Sends a message to the linked admin discord channel. The message is not sanitized of markdown.
-- @param message<string> message to send.
function Public.to_admin_raw(message)
raw_print(discord_admin_raw_tag .. message)
end
--- Sends a embed message to the linked discord channel. The message is sanitized of markdown server side.
-- @param message<string> the content of the embed.
function Public.to_discord_embed(message)
raw_print(discord_embed_tag .. message)
end
--- Sends a embed message to the linked discord channel. The message is not sanitized of markdown.
-- @param message<string> the content of the embed.
function Public.to_discord_embed_raw(message)
raw_print(discord_embed_raw_tag .. message)
end
--- Sends a embed message to the linked admin discord channel. The message is sanitized of markdown server side.
-- @param message<string> the content of the embed.
function Public.to_admin_embed(message)
raw_print(discord_admin_embed_tag .. message)
end
--- Sends a embed message to the linked admin discord channel. The message is not sanitized of markdown.
-- @param message<string> the content of the embed.
function Public.to_admin_embed_raw(message)
raw_print(discord_admin_embed_raw_tag .. message)
end
--- Stops and saves the factorio server and starts the named scenario.
-- @param scenario_name<string> The name of the scenario as appears in the scenario table on the panel.
-- @usage
-- local Server = require 'utils.server'
-- Server.start_scenario('my_scenario_name')
function Public.start_scenario(scenario_name)
if type(scenario_name) ~= 'string' then
game.print('start_scenario - scenario_name ' .. tostring(scenario_name) .. ' must be a string.')
return
end
local message = start_scenario_tag .. scenario_name
raw_print(message)
end
local default_ping_token =
Token.register(
function(sent_tick)
local now = game.tick
local diff = now - sent_tick
local message = concat({'Pong in ', diff, ' tick(s) ', 'sent tick: ', sent_tick, ' received tick: ', now})
game.print(message)
end
)
--- Pings the web server.
-- @param func_token<token> The function that is called when the web server replies.
-- The function is passed the tick that the ping was sent.
function Public.ping(func_token)
local message = concat({ping_tag, func_token or default_ping_token, ' ', game.tick})
raw_print(message)
end
local function double_escape(str)
-- Excessive escaping because the data is serialized twice.
return str:gsub('\\', '\\\\\\\\'):gsub('"', '\\\\\\"'):gsub('\n', '\\\\n')
end
--- Sets the web server's persistent data storage. If you pass nil for the value removes the data.
-- Data set this will by synced in with other server if they choose to.
-- There can only be one key for each data_set.
-- @param data_set<string>
-- @param key<string>
-- @param value<nil|boolean|number|string|table> Any type that is not a function. set to nil to remove the data.
-- @usage
-- local Server = require 'utils.server'
-- Server.set_data('my data set', 'key 1', 123)
-- Server.set_data('my data set', 'key 2', 'abc')
-- Server.set_data('my data set', 'key 3', {'some', 'data', ['is_set'] = true})
--
-- Server.set_data('my data set', 'key 1', nil) -- this will remove 'key 1'
-- Server.set_data('my data set', 'key 2', 'def') -- this will change the value for 'key 2' to 'def'
function Public.set_data(data_set, key, value)
if type(data_set) ~= 'string' then
error('data_set must be a string', 2)
end
if type(key) ~= 'string' then
error('key must be a string', 2)
end
data_set = double_escape(data_set)
key = double_escape(key)
local message
local vt = type(value)
if vt == 'nil' then
message = concat({data_set_tag, '{data_set:"', data_set, '",key:"', key, '"}'})
elseif vt == 'string' then
value = double_escape(value)
message = concat({data_set_tag, '{data_set:"', data_set, '",key:"', key, '",value:"\\"', value, '\\""}'})
elseif vt == 'number' then
message = concat({data_set_tag, '{data_set:"', data_set, '",key:"', key, '",value:"', value, '"}'})
elseif vt == 'boolean' then
message = concat({data_set_tag, '{data_set:"', data_set, '",key:"', key, '",value:"', tostring(value), '"}'})
elseif vt == 'function' then
error('value cannot be a function', 2)
else -- table
value = serialize(value, serialize_options)
-- Less escaping than the string case as serpent provides one level of escaping.
-- Need to escape single quotes as serpent uses double quotes for strings.
value = value:gsub('\\', '\\\\'):gsub("'", "\\'")
message = concat({data_set_tag, '{data_set:"', data_set, '",key:"', key, "\",value:'", value, "'}"})
end
raw_print(message)
end
--- Gets data from the web server's persistent data storage.
-- The callback is passed a table {data_set: string, key: string, value: any}.
-- If the value is nil, it means there is no stored data for that data_set key pair.
-- @param data_set<string>
-- @param key<string>
-- @param callback_token<token>
-- @usage
-- local Server = require 'utils.server'
-- local Token = require 'utils.token'
--
-- local callback =
-- Token.register(
-- function(data)
-- local data_set = data.data_set
-- local key = data.key
-- local value = data.value -- will be nil if no data
--
-- game.print(data_set .. ':' .. key .. ':' .. tostring(value))
-- end
-- )
--
-- Server.try_get_data('my data set', 'key 1', callback)
function Public.try_get_data(data_set, key, callback_token)
if type(data_set) ~= 'string' then
error('data_set must be a string', 2)
end
if type(key) ~= 'string' then
error('key must be a string', 2)
end
if type(callback_token) ~= 'number' then
error('callback_token must be a number', 2)
end
data_set = double_escape(data_set)
key = double_escape(key)
local message = concat {data_get_tag, callback_token, ' {', 'data_set:"', data_set, '",key:"', key, '"}'}
raw_print(message)
end
--- Gets all the data for the data_set from the web server's persistent data storage.
-- The callback is passed a table {data_set: string, entries: {dictionary key -> value}}.
-- If there is no data stored for the data_set entries will be nil.
-- @param data_set<string>
-- @param callback_token<token>
-- @usage
-- local Server = require 'utils.server'
-- local Token = require 'utils.token'
--
-- local callback =
-- Token.register(
-- function(data)
-- local data_set = data.data_set
-- local entries = data.entries -- will be nil if no data
-- local value2 = entries['key 2']
-- local value3 = entries['key 3']
-- end
-- )
--
-- Server.try_get_all_data('my data set', callback)
function Public.try_get_all_data(data_set, callback_token)
if type(data_set) ~= 'string' then
error('data_set must be a string', 2)
end
if type(callback_token) ~= 'number' then
error('callback_token must be a number', 2)
end
data_set = double_escape(data_set)
local message = concat {data_get_all_tag, callback_token, ' {', 'data_set:"', data_set, '"}'}
raw_print(message)
end
local function data_set_changed(data)
local handlers = data_set_handlers[data.data_set]
if handlers == nil then
return
end
if _DEBUG then
for _, handler in ipairs(handlers) do
local success, err = pcall(handler, data)
if not success then
log(err)
error(err, 2)
end
end
else
for _, handler in ipairs(handlers) do
local success, err = pcall(handler, data)
if not success then
log(err)
end
end
end
end
--- Register a handler to be called when the data_set changes.
-- The handler is passed a table {data_set:string, key:string, value:any}
-- If value is nil that means the key was removed.
-- The handler may be called even if the value hasn't changed. It's up to the implementer
-- to determine if the value has changed, or not care.
-- To prevent desyncs the same handlers must be registered for all clients. The easiest way to do this
-- is in the control stage, i.e before on_init or on_load would be called.
-- @param data_set<string>
-- @param handler<function>
-- @usage
-- local Server = require 'utils.server'
-- Server.on_data_set_changed(
-- 'my data set',
-- function(data)
-- local data_set = data.data_set
-- local key = data.key
-- local value = data.value -- will be nil if data was removed.
-- end
-- )
function Public.on_data_set_changed(data_set, handler)
if _LIFECYCLE == _STAGE.runtime then
error('cannot call during runtime', 2)
end
if type(data_set) ~= 'string' then
error('data_set must be a string', 2)
end
local handlers = data_set_handlers[data_set]
if handlers == nil then
handlers = {handler}
data_set_handlers[data_set] = handlers
else
handlers[#handlers + 1] = handler
end
end
--- Called by the web server to notify the client that a data_set has changed.
Public.raise_data_set = data_set_changed
--- Called by the web server to determine which data_sets are being tracked.
function Public.get_tracked_data_sets()
local message = {data_tracked_tag, '['}
for k, _ in pairs(data_set_handlers) do
k = double_escape(k)
local message_length = #message
message[message_length + 1] = '"'
message[message_length + 2] = k
message[message_length + 3] = '"'
message[message_length + 4] = ','
end
if message[#message] == ',' then
remove(message)
end
message[#message + 1] = ']'
message = concat(message)
raw_print(message)
end
local function escape(str)
return str:gsub('\\', '\\\\'):gsub('"', '\\"')
end
--- If the player exists bans the player.
-- Regardless of whether or not the player exists the name is synchronized with other servers
-- and stored in the database.
-- @param username<string>
-- @param reason<string?> defaults to empty string.
-- @param admin<string?> admin's name, defaults to '<script>'
function Public.ban_sync(username, reason, admin)
if type(username) ~= 'string' then
error('username must be a string', 2)
end
if reason == nil then
reason = ''
elseif type(reason) ~= 'string' then
error('reason must be a string or nil', 2)
end
if admin == nil then
admin = '<script>'
elseif type(admin) ~= 'string' then
error('admin must be a string or nil', 2)
end
-- game.ban_player errors if player not found.
-- However we may still want to use this function to ban player names.
local player = game.players[username]
if player then
game.ban_player(player, reason)
end
username = escape(username)
reason = escape(reason)
admin = escape(admin)
local message = concat({ban_sync_tag, '{username:"', username, '",reason:"', reason, '",admin:"', admin, '"}'})
raw_print(message)
end
--- If the player exists bans the player else throws error.
-- The ban is not synchronized with other servers or stored in the database.
-- @param PlayerSpecification
-- @param reason<string?> defaults to empty string.
function Public.ban_non_sync(PlayerSpecification, reason)
game.ban_player(PlayerSpecification, reason)
end
--- If the player exists unbans the player.
-- Regardless of whether or not the player exists the name is synchronized with other servers
-- and removed from the database.
-- @param username<string>
-- @param admin<string?> admin's name, defaults to '<script>'. This name is stored in the logs for who removed the ban.
function Public.unban_sync(username, admin)
if type(username) ~= 'string' then
error('username must be a string', 2)
end
if admin == nil then
admin = '<script>'
elseif type(admin) ~= 'string' then
error('admin must be a string or nil', 2)
end
-- game.unban_player errors if player not found.
-- However we may still want to use this function to unban player names.
local player = game.players[username]
if player then
game.unban_player(username)
end
username = escape(username)
admin = escape(admin)
local message = concat({unbanned_sync_tag, '{username:"', username, '",admin:"', admin, '"}'})
raw_print(message)
end
--- If the player exists unbans the player else throws error.
-- The ban is not synchronized with other servers or removed from the database.
-- @param PlayerSpecification
function Public.unban_non_sync(PlayerSpecification)
game.unban_player(PlayerSpecification)
end
--- Called by the web server to set the server time.
-- @param secs<number> unix epoch timestamp
function Public.set_time(secs)
server_time.secs = secs
server_time.tick = game.tick
end
--- Gets a table {secs:number?, tick:number} with secs being the unix epoch timestamp
-- for the server time and ticks the number of game ticks ago it was set.
-- @return table
function Public.get_time_data_raw()
return server_time
end
--- Gets an estimate of the current server time as a unix epoch timestamp.
-- If the server time has not been set returns nil.
-- The estimate may be slightly off if within the last minute the game has been paused, saving or overwise,
-- or the game speed has been changed.
-- @return number?
function Public.get_current_time()
local secs = server_time.secs
if secs == nil then
return nil
end
local diff = game.tick - server_time.tick
return math.floor(secs + diff / game.speed / 60)
end
--- Called be the web server to re sync which players are online.
function Public.query_online_players()
local message = {query_players_tag, '['}
for _, p in ipairs(game.connected_players) do
message[#message + 1] = '"'
local name = escape(p.name)
message[#message + 1] = name
message[#message + 1] = '",'
end
if message[#message] == '",' then
message[#message] = '"'
end
message[#message + 1] = ']'
message = concat(message)
raw_print(message)
end
--- The [JOIN] nad [LEAVE] messages Factorio sends to stdout aren't sent in all cases of
-- players joining or leaving. So we send our own [PLAYER-JOIN] and [PLAYER-LEAVE] tags.
Event.add(
defines.events.on_player_joined_game,
function(event)
local player = Game.get_player_by_index(event.player_index)
if not player then
return
end
raw_print(player_join_tag .. player.name)
end
)
Event.add(
defines.events.on_player_left_game,
function(event)
local player = Game.get_player_by_index(event.player_index)
if not player then
return
end
raw_print(player_leave_tag .. player.name)
end
)
return Public

28
utils/server_commands.lua Normal file
View File

@@ -0,0 +1,28 @@
local Poll = {send_poll_result_to_discord = function () end}
local Token = require 'utils.token'
local Server = require 'utils.server'
--- This module is for the web server to call functions and raise events.
-- Not intended to be called by scripts.
-- Needs to be in the _G table so it can be accessed by the web server.
ServerCommands = {}
ServerCommands.get_poll_result = Poll.send_poll_result_to_discord
function ServerCommands.raise_callback(func_token, data)
local func = Token.get(func_token)
func(data)
end
ServerCommands.raise_data_set = Server.raise_data_set
ServerCommands.get_tracked_data_sets = Server.get_tracked_data_sets
function ServerCommands.server_started()
script.raise_event(Server.events.on_server_started, {})
end
ServerCommands.set_time = Server.set_time
ServerCommands.query_online_players = Server.query_online_players
return ServerCommands

152
utils/timestamp.lua Normal file
View File

@@ -0,0 +1,152 @@
--- source https://github.com/daurnimator/luatz/blob/master/luatz/timetable.lua
-- edited down to just what is needed.
local Public = {}
local floor = math.floor
local strformat = string.format
local function borrow(tens, units, base)
local frac = tens % 1
units = units + frac * base
tens = tens - frac
return tens, units
end
local function carry(tens, units, base)
if units >= base then
tens = tens + floor(units / base)
units = units % base
elseif units < 0 then
tens = tens + floor(units / base)
units = (base + units) % base
end
return tens, units
end
local function is_leap(y)
if (y % 4) ~= 0 then
return false
elseif (y % 100) ~= 0 then
return true
else
return (y % 400) == 0
end
end
local mon_lengths = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
-- Number of days in year until start of month; not corrected for leap years
local months_to_days_cumulative = {0}
for i = 2, 12 do
months_to_days_cumulative[i] = months_to_days_cumulative[i - 1] + mon_lengths[i - 1]
end
local function month_length(m, y)
if m == 2 then
return is_leap(y) and 29 or 28
else
return mon_lengths[m]
end
end
local function day_of_year(day, month, year)
local yday = months_to_days_cumulative[month]
if month > 2 and is_leap(year) then
yday = yday + 1
end
return yday + day
end
local function leap_years_since(year)
return floor(year / 4) - floor(year / 100) + floor(year / 400)
end
local leap_years_since_1970 = leap_years_since(1970)
local function normalise(year, month, day, hour, min, sec)
-- `month` and `day` start from 1, need -1 and +1 so it works modulo
month, day = month - 1, day - 1
-- Convert everything (except seconds) to an integer
-- by propagating fractional components down.
year, month = borrow(year, month, 12)
-- Carry from month to year first, so we get month length correct in next line around leap years
year, month = carry(year, month, 12)
month, day = borrow(month, day, month_length(floor(month + 1), year))
day, hour = borrow(day, hour, 24)
hour, min = borrow(hour, min, 60)
min, sec = borrow(min, sec, 60)
-- Propagate out of range values up
-- e.g. if `min` is 70, `hour` increments by 1 and `min` becomes 10
-- This has to happen for all columns after borrowing, as lower radixes may be pushed out of range
min, sec = carry(min, sec, 60) -- TODO: consider leap seconds?
hour, min = carry(hour, min, 60)
day, hour = carry(day, hour, 24)
-- Ensure `day` is not underflowed
-- Add a whole year of days at a time, this is later resolved by adding months
-- TODO[OPTIMIZE]: This could be slow if `day` is far out of range
while day < 0 do
month = month - 1
if month < 0 then
year = year - 1
month = 11
end
day = day + month_length(month + 1, year)
end
year, month = carry(year, month, 12)
-- TODO[OPTIMIZE]: This could potentially be slow if `day` is very large
while true do
local i = month_length(month + 1, year)
if day < i then
break
end
day = day - i
month = month + 1
if month >= 12 then
month = 0
year = year + 1
end
end
-- Now we can place `day` and `month` back in their normal ranges
-- e.g. month as 1-12 instead of 0-11
month, day = month + 1, day + 1
return {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
end
--- Converts unix epoch timestamp into table {year: number, month: number, day: number, hour: number, min: number, sec: number}
-- @param sec<number> unix epoch timestamp
-- @return {year: number, month: number, day: number, hour: number, min: number, sec: number}
function Public.to_timetable(secs)
return normalise(1970, 1, 1, 0, 0, secs)
end
--- Converts timetable into unix epoch timestamp
-- @param timetable<table> {year: number, month: number, day: number, hour: number, min: number, sec: number}
-- @return number
function Public.from_timetable(timetable)
local tt = normalise(timetable.year, timetable.month, timetable.day, timetable.hour, timetable.min, timetable.sec)
local year, month, day, hour, min, sec = tt.year, tt.month, tt.day, tt.hour, tt.min, tt.sec
local days_since_epoch =
day_of_year(day, month, year) + 365 * (year - 1970) + -- Each leap year adds one day
(leap_years_since(year - 1) - leap_years_since_1970) -
1
return days_since_epoch * (60 * 60 * 24) + hour * (60 * 60) + min * 60 + sec
end
--- Converts unix epoch timestamp into human readable string.
-- @param secs<type> unix epoch timestamp
-- @return string
function Public.to_string(secs)
local tt = normalise(1970, 1, 1, 0, 0, secs)
return strformat('%04u-%02u-%02u %02u:%02u:%02d', tt.year, tt.month, tt.day, tt.hour, tt.min, tt.sec)
end
return Public

55
utils/token.lua Normal file
View File

@@ -0,0 +1,55 @@
local Token = {}
local tokens = {}
local counter = 0
--- Assigns a unquie id for the given var.
-- This function cannot be called after on_init() or on_load() has run as that is a desync risk.
-- Typically this is used to register functions, so the id can be stored in the global table
-- instead of the function. This is becasue closures cannot be safely stored in the global table.
-- @param var<any>
-- @return number the unique token for the variable.
function Token.register(var)
if _LIFECYCLE == 8 then
error('Calling Token.register after on_init() or on_load() has run is a desync risk.', 2)
end
counter = counter + 1
tokens[counter] = var
return counter
end
function Token.get(token_id)
return tokens[token_id]
end
global.tokens = {}
function Token.register_global(var)
local c = #global.tokens + 1
global.tokens[c] = var
return c
end
function Token.get_global(token_id)
return global.tokens[token_id]
end
function Token.set_global(token_id, var)
global.tokens[token_id] = var
end
local uid_counter = 0
function Token.uid()
uid_counter = uid_counter + 1
return uid_counter
end
return Token