You've already forked ComfyFactorio
mirror of
https://github.com/ComfyFactory/ComfyFactorio.git
synced 2026-06-20 16:32:28 +02:00
server utils
This commit is contained in:
@@ -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
|
||||
+4
-3
@@ -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"
|
||||
|
||||
+21
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user