--- See documentation at https://github.com/Refactorio/RedMew/pull/469 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 serialize = serpent.serialize local concat = table.concat local remove = table.remove local tostring = tostring local serialize_options = {sparse = true, compact = true} local Public = {} local raw_print = print function print(str) raw_print('[PRINT] ' .. str) end 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 'features.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 message to send. -- @usage -- local Server = require 'features.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 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 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 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 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 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 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 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 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 The name of the scenario as appears in the scenario table on http://redmew.com/admin -- @usage -- local Server = require 'features.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 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 -- @param key -- @param value Any type that is not a function. set to nil to remove the data. -- @usage -- local Server = require 'features.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 -- @param key -- @param callback_token -- @usage -- local Server = require 'features.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 -- @param callback_token -- @usage -- local Server = require 'features.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 -- @param handler -- @usage -- local Server = require 'features.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 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 -- @param reason defaults to empty string. -- @param admin admin's name, defaults to '