diff --git a/control.lua b/control.lua index 6db80acb..12f0d8a8 100644 --- a/control.lua +++ b/control.lua @@ -4,12 +4,13 @@ require 'resources.data_stages' _LIFECYCLE = _STAGE.control -- Control stage +-- Util libraries, omitting is a very bad idea +require 'utils.math' +require 'utils.string' + -- Overrides the _G.print function require 'utils.print_override' --- Omitting the math library is a very bad idea -require 'utils.math' - -- Global Debug and make sure our version file is registered Debug = require 'utils.debug' require 'resources.version' @@ -161,6 +162,10 @@ end --require 'features.snake.control' +_G.Server = require 'features.server' + +require 'features.restart_command' + -- Debug-only modules if _DEBUG then require 'features.scenario_data_manipulation' diff --git a/features/restart_command.lua b/features/restart_command.lua new file mode 100644 index 00000000..6b1cff05 --- /dev/null +++ b/features/restart_command.lua @@ -0,0 +1,416 @@ +local Gui = require 'utils.gui' +local Global = require 'utils.global' +local Server = require 'features.server' +local Command = require 'utils.command' +local Ranks = require 'resources.ranks' +local Token = require 'utils.token' +local Task = require 'utils.task' +local Popup = require 'features.gui.popup' +require 'utils.string' + +local Public = {} + +local game_types = {scenario = 'scenario', save = 'save'} +Public.game_types = game_types + +local memory = {mod_pack_text = '', restarting = nil} +local start_game_data = {type = game_types.scenario, name = '', mod_pack = nil} + +Global.register({start_game_data = start_game_data, memory = memory}, function(tbl) + start_game_data = tbl.start_game_data + memory = tbl.memory +end) + +local function default_can_restart_func(player) + return player.valid and player.admin +end + +local registered = false +local server_can_restart_func = default_can_restart_func +local server_restart_callback = nil + +local server_player = {name = '', print = print, admin = true} + +local function double_print(str) + game.print(str) + print(str) +end + +local restart_callback_token +restart_callback_token = Token.register(function(data) + if not memory.restarting then + return + end + + local state = data.state + if state == 0 then + if server_restart_callback then + server_restart_callback() + end + + Server.start_game(start_game_data) + double_print('restarting') + memory.restarting = nil + return + elseif state == 1 then + Popup.all('\nServer restarting!\nInitiated by ' .. data.player_name .. '\n' .. 'Next map: ' + .. start_game_data.name) + end + + double_print(state) + + data.state = state - 1 + Task.set_timeout_in_ticks(60, restart_callback_token, data) +end) + +local function get_start_data(player) + local message = {'Start Game Data:', '\nType: ', start_game_data.type, '\nName: ', start_game_data.name} + + local mod_pack = start_game_data.mod_pack + if mod_pack then + message[#message + 1] = '\nMod Pack: ' + message[#message + 1] = mod_pack + end + + local text = table.concat(message) + player.print(text) +end + +local function sanitize_set_start_data_str(str) + str = str:trim() + + local first_char = str:sub(1, 1) + if first_char == "'" or first_char == '"' or first_char == '{' then + return str + end + + return '"' .. str .. '"' +end + +local function set_start_data(player, str) + str = sanitize_set_start_data_str(str) + + local func, err = loadstring('return ' .. str) + if not func then + player.print(err) + return false + end + + local suc, value = pcall(func) + if not suc then + if value then + local i = value:find('\n') + if i then + player.print(value:sub(1, i)) + return false + end + + i = value:find('%s') + if i then + player.print(value:sub(i + 1)) + end + end + + return false + end + + game.print(value) + Public.set_start_game_data(value) + + player.print('Start Game Data set') + get_start_data(player) + + return true +end + +local function restart(args, player) + player = player or server_player + + if memory.restarting then + player.print('Restart already in progress') + return + end + + if not server_can_restart_func(player) then + return + end + + local str = args.str:trim() + if str ~= '' and player.admin then + if not set_start_data(player, str) then + return + end + end + + memory.restarting = true + + double_print('#################-Attention-#################') + double_print('Server restart initiated by ' .. player.name) + double_print('Next map: ' .. start_game_data.name) + double_print('###########################################') + + for _, p in pairs(game.players) do + if p.admin then + p.print('Abort restart with /abort') + end + end + print('Abort restart with /abort') + Task.set_timeout_in_ticks(60, restart_callback_token, {state = 10, player_name = player.name}) +end + +local function abort(_, player) + player = player or server_player + + if memory.restarting then + memory.restarting = nil + double_print('Restart aborted by ' .. player.name) + else + player.print('Cannot abort a restart that is not in progress.') + end +end + +function Public.register(can_restart_func, restart_callback) + if registered then + error('Register can only be called once', 2) + end + + if _LIFECYCLE == 8 then + error('Calling Token.register after on_init() or on_load() has run is a desync risk.', 2) + end + + registered = true + server_can_restart_func = can_restart_func or default_can_restart_func + server_restart_callback = restart_callback +end + +local main_frame_name = Gui.uid_name() +local close_button_name = Gui.uid_name() +local scenario_radio_button_name = Gui.uid_name() +local save_radio_button_name = Gui.uid_name() +local name_textfield_name = Gui.uid_name() +local set_mod_pack_checkbox_name = Gui.uid_name() +local mod_pack_name_textfield_name = Gui.uid_name() + +Public._main_frame_name = main_frame_name +Public._close_button_name = close_button_name +Public._scenario_radio_button_name = scenario_radio_button_name +Public._save_radio_button_name = save_radio_button_name +Public._name_textfield_name = name_textfield_name +Public._set_mod_pack_checkbox_name = set_mod_pack_checkbox_name +Public._mod_pack_name_textfield_name = mod_pack_name_textfield_name + +local function value_of_type_or_deafult(value, value_type, default) + if type(value) == value_type then + return value + end + + return default +end + +--- Gets the data used to start the next game when restart is used. +-- @returns data {type, name, mod_pack} +function Public.get_start_game_data() + return {type = start_game_data.type, name = start_game_data.name, mod_pack = start_game_data.mod_pack} +end + +--- Sets the data used to start the next game when restart is used. +-- @params data {type, name, mod_pack} +-- If mod_pack is nil that means to use the current mod pack, set to empty string ('') to use no mod pack. +-- When data is a string: type is scenario, name is data and mod_pack is nil. +-- Note: name and mod_pack are case sensitive. +function Public.set_start_game_data(data) + local data_type = type(data) + if data_type == 'string' then + data = {type = game_types.scenario, name = data} + elseif data_type ~= 'table' then + error('data must be a table or string', 2) + end + + local game_type = value_of_type_or_deafult(data.type, 'string', game_types.scenario) + local name = value_of_type_or_deafult(data.name, 'string', '') + local mod_pack = value_of_type_or_deafult(data.mod_pack, 'string', nil) + + game_type = game_type:lower() + if game_type ~= game_types.save then + game_type = game_types.scenario + end + + start_game_data.type = game_type + start_game_data.name = name + start_game_data.mod_pack = mod_pack + + if mod_pack then + memory.mod_pack_text = mod_pack + else + memory.mod_pack_text = '' + end +end + +local function draw_main_frame(player) + if player == server_player then + player.print('/config-restart with no arguments cannot be used from the server.') + return + end + + local center = player.gui.center + local main_frame = center[main_frame_name] + if main_frame and main_frame.valid then + Gui.destroy(main_frame) + end + + main_frame = center.add { + type = 'frame', + name = main_frame_name, + caption = 'Configure Restart', + direction = 'vertical' + } + + local is_scenario = start_game_data.type == game_types.scenario + local radio_button_flow = main_frame.add {type = 'flow', direction = 'horizontal'} + radio_button_flow.add {type = 'label', caption = 'Type:'} + local scenario_radio_button = radio_button_flow.add { + type = 'radiobutton', + name = scenario_radio_button_name, + caption = 'scenario', + state = is_scenario + } + local save_radio_button = radio_button_flow.add { + type = 'radiobutton', + name = save_radio_button_name, + caption = 'save', + state = not is_scenario + } + + local radio_data = {scenario_radio_button = scenario_radio_button, save_radio_button = save_radio_button} + + Gui.set_data(scenario_radio_button, radio_data) + Gui.set_data(save_radio_button, radio_data) + + local name_flow = main_frame.add {type = 'flow', direction = 'horizontal'} + name_flow.add {type = 'label', caption = 'Name:'} + name_flow.add {type = 'textfield', name = name_textfield_name, text = start_game_data.name} + + local is_set_mod_pack = start_game_data.mod_pack ~= nil + local set_mod_pack_checkbox = main_frame.add { + type = 'checkbox', + name = set_mod_pack_checkbox_name, + caption = 'Set mod pack (uncheck to not change current)', + state = is_set_mod_pack + } + local mod_pack_name_flow = main_frame.add {type = 'flow', direction = 'horizontal'} + mod_pack_name_flow.add {type = 'label', caption = 'Mod Pack (empty to set none):'} + local mod_pack_name_textfield = mod_pack_name_flow.add { + type = 'textfield', + name = mod_pack_name_textfield_name, + text = memory.mod_pack_text + } + mod_pack_name_textfield.enabled = is_set_mod_pack + + Gui.set_data(set_mod_pack_checkbox, mod_pack_name_textfield) + + local bottom_flow = main_frame.add {type = 'flow', direction = 'horizontal'} + + bottom_flow.add { + type = 'button', + name = close_button_name, + caption = {'common.close_button'}, + style = 'back_button' + } +end + +Gui.on_click(close_button_name, function(event) + local main_frame = event.player.gui.center[main_frame_name] + if main_frame and main_frame.valid then + Gui.destroy(main_frame) + end +end) + +local function set_game_type(radio_data, game_type) + radio_data.scenario_radio_button.state = game_type == game_types.scenario + radio_data.save_radio_button.state = game_type == game_types.save + + start_game_data.type = game_type +end + +Gui.on_checked_state_changed(scenario_radio_button_name, function(event) + local radio_data = Gui.get_data(event.element) + set_game_type(radio_data, game_types.scenario) +end) + +Gui.on_checked_state_changed(save_radio_button_name, function(event) + local radio_data = Gui.get_data(event.element) + set_game_type(radio_data, game_types.save) +end) + +Gui.on_text_changed(name_textfield_name, function(event) + start_game_data.name = event.element.text +end) + +Gui.on_checked_state_changed(set_mod_pack_checkbox_name, function(event) + local set_mod_pack_checkbox = event.element + local mod_pack_name_textfield = Gui.get_data(set_mod_pack_checkbox) + + if set_mod_pack_checkbox.state then + mod_pack_name_textfield.enabled = true + start_game_data.mod_pack = memory.mod_pack_text + else + mod_pack_name_textfield.enabled = false + start_game_data.mod_pack = nil + end +end) + +Gui.on_text_changed(mod_pack_name_textfield_name, function(event) + local text = event.element.text + start_game_data.mod_pack = text + memory.mod_pack_text = text +end) + +local function config_restart(args, player) + local str = args.str + player = player or server_player + + if str == '' then + draw_main_frame(player) + elseif str == 'get' then + get_start_data(player) + elseif str:sub(1, 3) == 'set' then + str = str:sub(4) -- remove 'set' from start of str. + set_start_data(player, str) + else + player.print('Invalid arguments') + end +end + +Public._config_restart = config_restart + +Command.add('config-restart', { + description = [[ +configure the restart command +use /config-restart to open a gui, +use /config-restart get to prints the values, +use /config-restart set scenario_name | {type, name, mod_pack} to set the values +e.g. /config-restart set 'develop' +or /config-restart set {type = 'save', name = 'file.zip'} +or /config-restart set {type = 'scenario', name = 'develop', mod_pack = 'mod'} +]], + arguments = {'str'}, + default_values = {str = ''}, + capture_excess_arguments = true, + required_rank = Ranks.admin, + allowed_by_server = true, + allowed_by_player = true +}, config_restart) + +Command.add('abort', + {description = {'command_description.abort'}, required_rank = Ranks.admin, allowed_by_server = true}, abort) + +Command.add('restart', { + description = {'command_description.restart'}, + arguments = {'str'}, + capture_excess_arguments = true, + default_values = {str = ''}, + required_rank = Ranks.guest, + allowed_by_server = true +}, restart) + +return Public diff --git a/features/restart_command_tests.lua b/features/restart_command_tests.lua new file mode 100644 index 00000000..b3150d12 --- /dev/null +++ b/features/restart_command_tests.lua @@ -0,0 +1,390 @@ +local Declare = require 'utils.test.declare' +local Helper = require 'utils.test.helper' +local RestartCommand = require 'features.restart_command' +local Assert = require 'utils.test.assert' +local Gui = require 'utils.gui' +local Command = require 'utils.command' + +local function test_teardown(context) + RestartCommand.set_start_game_data({type = RestartCommand.game_types.scenario, name = '', mod_pack = nil}) + + context:add_teardown(function() + local main_frame = context.player.gui.center[RestartCommand._main_frame_name] + if main_frame and main_frame.valid then + Gui.destroy(main_frame) + end + end) +end + +local function declare_test(name, func) + local function test_func(context) + test_teardown(context) + func(context) + end + + Declare.test(name, test_func) +end + +local function assert_view_matches_start_game_data(player, is_save, is_scenario, name, is_mod_pack_set, mod_pack_name) + local center = player.gui.center + local scenario_radio_button = Helper.get_gui_element_by_name(center, RestartCommand._scenario_radio_button_name) + local save_radio_button = Helper.get_gui_element_by_name(center, RestartCommand._save_radio_button_name) + local name_textfield = Helper.get_gui_element_by_name(center, RestartCommand._name_textfield_name) + local set_mod_pack_checkbox = Helper.get_gui_element_by_name(center, RestartCommand._set_mod_pack_checkbox_name) + local mod_pack_name_textfield = Helper.get_gui_element_by_name(center, RestartCommand._mod_pack_name_textfield_name) + + Assert.equal(is_scenario, scenario_radio_button.state) + Assert.equal(is_save, save_radio_button.state) + Assert.equal(name, name_textfield.text) + Assert.equal(is_mod_pack_set, set_mod_pack_checkbox.state) + Assert.equal(mod_pack_name, mod_pack_name_textfield.text) +end + +local function assert_start_game_data(type, name, mod_pack) + local start_game_data = RestartCommand.get_start_game_data() + + Assert.equal(type, start_game_data.type) + Assert.equal(name, start_game_data.name) + Assert.equal(mod_pack, start_game_data.mod_pack) +end + +local function run_config_command(player, parameter) + Command._raise_command('config-restart', player.index, parameter or '') +end + +local function run_restart_command(player, parameter) + Command._raise_command('restart', player.index, parameter or '') +end + +local function run_abort_command(player) + Command._raise_command('abort', player.index) +end + +Declare.module({'features', 'restart_command'}, function() + local inital_start_game_data + + Declare.module_startup(function() + inital_start_game_data = RestartCommand.get_start_game_data() + end) + + Declare.module_teardown(function() + RestartCommand.set_start_game_data(inital_start_game_data) + end) + + declare_test('Shows start game data when scenario.', function(context) + -- Arrange. + local start_game_data = { + type = RestartCommand.game_types.scenario, + name = 'some_name', + mod_pack = 'some_mod_pack' + } + RestartCommand.set_start_game_data(start_game_data) + + -- Act. + run_config_command(context.player) + + -- Assert. + context:next(function() + assert_view_matches_start_game_data(context.player, false, true, start_game_data.name, true, + start_game_data.mod_pack) + end) + end) + + declare_test('Shows start game data when save.', function(context) + -- Arrange. + local start_game_data = {type = RestartCommand.game_types.save, name = 'some_name', mod_pack = 'some_mod_pack'} + RestartCommand.set_start_game_data(start_game_data) + + -- Act. + run_config_command(context.player) + + -- Assert. + context:next(function() + assert_view_matches_start_game_data(context.player, true, false, start_game_data.name, true, + start_game_data.mod_pack) + end) + end) + + declare_test('Shows start game data when no mod pack.', function(context) + -- Arrange. + local start_game_data = {type = RestartCommand.game_types.scenario, name = 'some_name'} + RestartCommand.set_start_game_data(start_game_data) + + -- Act. + run_config_command(context.player) + + -- Assert. + context:next(function() + assert_view_matches_start_game_data(context.player, false, true, start_game_data.name, false, '') + end) + end) + + declare_test('Shows start game data when mod pack empty string.', function(context) + -- Arrange. + local start_game_data = {type = RestartCommand.game_types.scenario, name = 'some_name', mod_pack = ''} + RestartCommand.set_start_game_data(start_game_data) + + -- Act. + run_config_command(context.player) + + -- Assert. + context:next(function() + assert_view_matches_start_game_data(context.player, false, true, start_game_data.name, true, + start_game_data.mod_pack) + end) + end) + + declare_test('Requires admin to run command.', function(context) + -- Arrange. + local player = context.player + Helper.modify_lua_object(context, player, 'admin', false) + Helper.modify_lua_object(context, game, 'get_player', function() + return player + end) + + -- Act. + run_config_command(player) + + -- Assert. + local center = player.gui.center + local main_frame = Helper.get_gui_element_by_name(center, RestartCommand._main_frame_name) + Assert.is_nil(main_frame) + end) + + declare_test('get returns start game data.', function(context) + -- Arrange. + local player = context.player + local actual = nil + Helper.modify_lua_object(context, player, 'print', function(str) + actual = str + end) + Helper.modify_lua_object(context, game, 'get_player', function() + return player + end) + + local start_game_data = { + type = RestartCommand.game_types.scenario, + name = 'some_name', + mod_pack = 'some_mod_pack' + } + RestartCommand.set_start_game_data(start_game_data) + + -- Act. + run_config_command(player, 'get') + + -- Assert. + local expected = [[ +Start Game Data: +Type: scenario +Name: some_name +Mod Pack: some_mod_pack]] + Assert.equal(expected, actual) + end) + + declare_test('set does set start game data.', function(context) + -- Act. + run_config_command(context.player, "set {type = '" .. RestartCommand.game_types.save + .. "', name = 'new_name', mod_pack = 'new_mod_pack_name'}") + + -- Assert. + local start_game_data = RestartCommand.get_start_game_data() + Assert.equal(RestartCommand.game_types.save, start_game_data.type) + Assert.equal('new_name', start_game_data.name) + Assert.equal('new_mod_pack_name', start_game_data.mod_pack) + end) + + for _, data in pairs({'new_name', ' new_name ', "'new_name'", '"new_name"', " 'new_name'"}) do + declare_test('set does set start game data for string ' .. data, function(context) + -- Act. + run_config_command(context.player, 'set ' .. data) + + -- Assert. + local start_game_data = RestartCommand.get_start_game_data() + Assert.equal(RestartCommand.game_types.scenario, start_game_data.type) + Assert.equal('new_name', start_game_data.name) + Assert.equal(nil, start_game_data.mod_pack) + end) + end + + declare_test('Close button closes gui', function(context) + -- Arrange. + local player = context.player + run_config_command(player) + + local center = player.gui.center + local close_button = Helper.get_gui_element_by_name(center, RestartCommand._close_button_name) + + -- Act. + context:next(function() + Helper.click(close_button) + end) + + -- Assert + context:next(function() + local main_frame = Helper.get_gui_element_by_name(center, RestartCommand._main_frame_name) + Assert.is_nil(main_frame) + end) + end) + + declare_test('Can change start game data from gui.', function(context) + -- Arrange. + local player = context.player + run_config_command(player) + + local center = player.gui.center + local save_radio_button = Helper.get_gui_element_by_name(center, RestartCommand._save_radio_button_name) + local name_textfield = Helper.get_gui_element_by_name(center, RestartCommand._name_textfield_name) + local mod_pack_checkbox = Helper.get_gui_element_by_name(center, RestartCommand._set_mod_pack_checkbox_name) + local mod_pack_name_textfield = Helper.get_gui_element_by_name(center, + RestartCommand._mod_pack_name_textfield_name) + + -- Act. + Helper.click(save_radio_button) + Helper.set_text(name_textfield, 'new_name') + Helper.set_checkbox(mod_pack_checkbox, true) + Helper.set_text(mod_pack_name_textfield, 'new_mod_pack_name') + + -- Assert. + context:next(function() + assert_start_game_data(RestartCommand.game_types.save, 'new_name', 'new_mod_pack_name') + end) + end) + + declare_test('Can change start game data to scenario from gui.', function(context) + -- Arrange. + RestartCommand.set_start_game_data({type = RestartCommand.game_types.save}) + local player = context.player + run_config_command(player) + + local center = player.gui.center + local scenario_radio_button = Helper.get_gui_element_by_name(center, RestartCommand._scenario_radio_button_name) + + -- Act. + Helper.click(scenario_radio_button) + + -- Assert. + context:next(function() + assert_start_game_data(RestartCommand.game_types.scenario, '', nil) + end) + end) + + declare_test('Can change start game data to no mod pack from gui.', function(context) + -- Arrange. + RestartCommand.set_start_game_data({mod_pack = 'some_mod_pack'}) + local player = context.player + run_config_command(player) + + local center = player.gui.center + local mod_pack_checkbox = Helper.get_gui_element_by_name(center, RestartCommand._set_mod_pack_checkbox_name) + local mod_pack_name_textfield = Helper.get_gui_element_by_name(center, + RestartCommand._mod_pack_name_textfield_name) + + -- Act. + Helper.set_checkbox(mod_pack_checkbox, false) + + -- Assert. + context:next(function() + assert_start_game_data(RestartCommand.game_types.scenario, '', nil) + Assert.equal('some_mod_pack', mod_pack_name_textfield.text) + end) + end) + + declare_test('Mod pack is remembered when not set and gui is closed and reopened.', function(context) + -- Arrange. + RestartCommand.set_start_game_data({mod_pack = 'some_mod_pack'}) + local player = context.player + run_config_command(player) + + local center = player.gui.center + local mod_pack_checkbox = Helper.get_gui_element_by_name(center, RestartCommand._set_mod_pack_checkbox_name) + local close_button = Helper.get_gui_element_by_name(center, RestartCommand._close_button_name) + + Helper.set_checkbox(mod_pack_checkbox, false) + + context:next(function() + Helper.click(close_button) + end):next(function() + -- Make sure gui closed + local main_frame = Helper.get_gui_element_by_name(center, RestartCommand._main_frame_name) + Assert.is_nil(main_frame) + end):next(function() + -- Reopen gui. + run_config_command(player) + end):next(function() + assert_start_game_data(RestartCommand.game_types.scenario, '', nil) + + local mod_pack_name_textfield = Helper.get_gui_element_by_name(center, + RestartCommand._mod_pack_name_textfield_name) + Assert.equal('some_mod_pack', mod_pack_name_textfield.text) + + -- Re-enable mod pack + mod_pack_checkbox = Helper.get_gui_element_by_name(center, RestartCommand._set_mod_pack_checkbox_name) + Helper.set_checkbox(mod_pack_checkbox, true) + end):next(function() + assert_start_game_data(RestartCommand.game_types.scenario, '', 'some_mod_pack') + + local mod_pack_name_textfield = Helper.get_gui_element_by_name(center, + RestartCommand._mod_pack_name_textfield_name) + Assert.equal('some_mod_pack', mod_pack_name_textfield.text) + end) + end) + + declare_test('restart command starts restart.', function(context) + -- Arrange. + RestartCommand.set_start_game_data({name = 'new_name', type = RestartCommand.game_types.scenario}) + + local player = context.player + + context:add_teardown(function() + run_abort_command(player) + end) + + local output = {} + + local function game_print(str) + output[#output + 1] = str + end + + Helper.modify_lua_object(context, game, 'print', game_print) + + -- Act. + run_restart_command(player) + + -- Assert. + Assert.array_contains(output, 'Server restart initiated by ' .. player.name) + Assert.array_contains(output, 'Next map: new_name') + end) + + for _, argument in pairs({'other_game', '{name = "other_game", type = "scenario"}'}) do + declare_test('restart command starts restart and sets start data with argument ' .. argument, function(context) + -- Arrange. + RestartCommand.set_start_game_data({ + name = 'new_name', + type = RestartCommand.game_types.save, + mod_pack = 'mod_pack' + }) + + local player = context.player + + context:add_teardown(function() + run_abort_command(player) + end) + + local output = {} + + local function game_print(str) + output[#output + 1] = str + end + + Helper.modify_lua_object(context, game, 'print', game_print) + + -- Act. + run_restart_command(player, argument) + + -- Assert. + Assert.array_contains(output, 'Server restart initiated by ' .. player.name) + Assert.array_contains(output, 'Next map: other_game') + assert_start_game_data(RestartCommand.game_types.scenario, 'other_game', nil) + end) + end +end) diff --git a/features/server.lua b/features/server.lua index a24f6ee5..fcc86b31 100644 --- a/features/server.lua +++ b/features/server.lua @@ -46,6 +46,7 @@ local discord_named_bold_tag = '[DISCORD-NAMED-BOLD]' local discord_named_embed_tag = '[DISCORD-NAMED-EMBED]' local discord_named_embed_raw_tag = '[DISCORD-NAMED-EMBED-RAW]' local start_scenario_tag = '[START-SCENARIO]' +local start_game_tag ='[START-GAME]' local ping_tag = '[PING]' local data_set_tag = '[DATA-SET]' local data_get_tag = '[DATA-GET]' @@ -195,6 +196,25 @@ function Public.start_scenario(scenario_name) raw_print(message) end +--- Stops the server and starts a new game. +-- @params start_game_data either string which is the scenario name or a table with the following fields +-- type:string optional defaults to scenario. +-- name:string the name of the scenario or save to start. +-- mod_pack:string optional the name of the mod pack to use. +function Public.start_game(start_game_data) + local data + if type(start_game_data) == 'string' then + data = {type = 'scenario', name = start_game_data} + elseif type(start_game_data) == 'table' then + data = start_game_data + else + error('start_game_data must be a string or table') + end + + local json = game.table_to_json(data) + raw_print(start_game_tag .. json) +end + local default_ping_token = Token.register(function(sent_tick) local now = game.tick local diff = now - sent_tick diff --git a/map_gen/maps/crash_site/commands.lua b/map_gen/maps/crash_site/commands.lua index 005c5e15..828bdfdd 100644 --- a/map_gen/maps/crash_site/commands.lua +++ b/map_gen/maps/crash_site/commands.lua @@ -1,9 +1,7 @@ local Command = require 'utils.command' -local Rank = require 'features.rank_system' local Task = require 'utils.task' local Token = require 'utils.token' local Server = require 'features.server' -local Popup = require 'features.gui.popup' local Global = require 'utils.global' local Event = require 'utils.event' local Retailer = require 'features.retailer' @@ -15,34 +13,27 @@ local Utils = require 'utils.core' local Discord = require 'resources.discord' local ScoreTracker = require 'utils.score_tracker' local PlayerStats = require 'features.player_stats' +local Restart = require 'features.restart_command' local set_timeout_in_ticks = Task.set_timeout_in_ticks -- Use these settings for live local map_promotion_channel = Discord.channel_names.map_promotion local crash_site_role_mention = Discord.role_mentions.crash_site -- Use these settings for testing ---local map_promotion_channel = Discord.channel_names.bot_playground ---local crash_site_role_mention = Discord.role_mentions.test +-- local map_promotion_channel = Discord.channel_names.bot_playground +-- local crash_site_role_mention = Discord.role_mentions.test local Public = {} function Public.control(config) + Restart.set_start_game_data({type = Restart.game_types.scenario, name = config.scenario_name or 'crashsite'}) - local server_player = {name = '', print = print} - local global_data = {restarting = nil} local airstrike_data = {radius_level = 1, count_level = 1} - local default_name = config.scenario_name or 'crashsite' - Global.register({global_data = global_data, airstrike_data = airstrike_data}, function(tbl) - global_data = tbl.global_data + Global.register({airstrike_data = airstrike_data}, function(tbl) airstrike_data = tbl.airstrike_data end) - local function double_print(str) - game.print(str) - print(str) - end - local static_entities_to_check = { 'spitter-spawner', 'biter-spawner', @@ -87,165 +78,13 @@ function Public.control(config) ['crashsite-world'] = 'Crash Site World Map', ['crashsite-desert'] = 'Crash Site Desert', ['crashsite-arrakis'] = 'Crash Site Arrakis' - } + } - local callback - callback = Token.register(function(data) - if not global_data.restarting then - return + local function can_restart(player) + if player.admin then + return true end - local state = data.state - local next_scenario = data.scenario_name - if state == 0 then - Server.start_scenario(next_scenario) - double_print('restarting') - global_data.restarting = nil - return - elseif state == 1 then - - local end_epoch = Server.get_current_time() - if end_epoch == nil then - end_epoch = -1 -- end_epoch is nil if the restart command is used locally rather than on the server - end - - local player_data = {} - for _, p in pairs(game.players) do - player_data[p.index] = { - name = p.name, - total_kills = ScoreTracker.get_for_player(p.index, PlayerStats.player_total_kills_name), - spawners_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_spawners_killed_name), - worms_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_worms_killed_name), - units_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_units_killed_name), - turrets_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_turrets_killed_name), - distance_walked = ScoreTracker.get_for_player(p.index,PlayerStats.player_distance_walked_name), - player_deaths = ScoreTracker.get_for_player(p.index, PlayerStats.player_deaths_name), - coins_earned = ScoreTracker.get_for_player(p.index, PlayerStats.coins_earned_name), - entities_built = ScoreTracker.get_for_player(p.index,PlayerStats.player_entities_built_name), - entities_crafted = ScoreTracker.get_for_player(p.index,PlayerStats.player_items_crafted_name), - fish_eaten = ScoreTracker.get_for_player(p.index,PlayerStats.player_fish_eaten_name), - time_played = p.online_time - } - end - - local statistics = { - scenario = config.scenario_name, - start_epoch = Server.get_start_time(), - end_epoch = end_epoch, -- stored as key already, useful to have it as part of same structure - game_ticks = game.ticks_played, - enemy_entities = count_enemy_entities(), - biters_killed = ScoreTracker.get_for_global(PlayerStats.aliens_killed_name), - total_players = #game.players, - entities_built = ScoreTracker.get_for_global(PlayerStats.built_by_players_name), - player_data = player_data - } - - local awards = { - ['total_kills'] = {value = 0, player = ""}, - ['units_killed'] = {value = 0, player = ""}, - ['spawners_killed'] = {value = 0, player = ""}, - ['worms_killed'] = {value = 0, player = ""}, - ['player_deaths'] = {value = 0, player = ""}, - ['time_played'] = {value = 0, player = ""}, - ['entities_built'] = {value = 0, player = ""}, - ['entities_crafted'] = {value = 0, player = ""}, - ['distance_walked'] = {value = 0, player = ""}, - ['coins_earned'] = {value = 0, player = ""}, - ['fish_eaten'] = {value = 0, player = ""} - } - - for k, v in pairs(statistics.player_data) do - if v.total_kills > awards.total_kills.value then - awards.total_kills.value = v.total_kills - awards.total_kills.player = v.name - end - if v.units_killed > awards.units_killed.value then - awards.units_killed.value = v.units_killed - awards.units_killed.player = v.name - end - if v.spawners_killed > awards.spawners_killed.value then - awards.spawners_killed.value = v.spawners_killed - awards.spawners_killed.player = v.name - end - if v.worms_killed > awards.worms_killed.value then - awards.worms_killed.value = v.worms_killed - awards.worms_killed.player = v.name - end - if v.player_deaths > awards.player_deaths.value then - awards.player_deaths.value = v.player_deaths - awards.player_deaths.player = v.name - end - if v.time_played > awards.time_played.value then - awards.time_played.value = v.time_played - awards.time_played.player = v.name - end - if v.entities_built > awards.entities_built.value then - awards.entities_built.value = v.entities_built - awards.entities_built.player = v.name - end - if v.entities_crafted > awards.entities_crafted.value then - awards.entities_crafted.value = v.entities_crafted - awards.entities_crafted.player = v.name - end - if v.distance_walked > awards.distance_walked.value then - awards.distance_walked.value = v.distance_walked - awards.distance_walked.player = v.name - end - if v.coins_earned > awards.coins_earned.value then - awards.coins_earned.value = v.coins_earned - awards.coins_earned.player = v.name - end - if v.fish_eaten > awards.fish_eaten.value then - awards.fish_eaten.value = v.fish_eaten - awards.fish_eaten.player = v.name - end - end - - local time_string = Core.format_time(game.ticks_played) - if statistics.enemy_entities < 1000 then - Server.to_discord_named_embed(map_promotion_channel, 'Crash Site map won!\\n\\n' - .. 'Statistics:\\n' - .. 'Map time: '..time_string..'\\n' - .. 'Total kills: '..statistics.biters_killed..'\\n' - .. 'Biters remaining on map: '..statistics.enemy_entities..'\\n' - .. 'Players: '..statistics.total_players..'\\n' - .. 'Total entities built: '..statistics.entities_built..'\\n\\n' - .. 'Awards:\\n' - .. 'Most kills overall: '..awards.total_kills.player..' ('..awards.total_kills.value..')\\n' - .. 'Most biters/spitters killed: '..awards.units_killed.player..' ('..awards.units_killed.value..')\\n' - .. 'Most spawners killed: '..awards.spawners_killed.player..' ('..awards.spawners_killed.value..')\\n' - .. 'Most worms killed: '..awards.worms_killed.player..' ('..awards.worms_killed.value..')\\n' - .. 'Most deaths: '..awards.player_deaths.player..' ('..awards.player_deaths.value..')\\n' - .. 'Most items crafted: '..awards.entities_crafted.player..' ('..awards.entities_crafted.value..')\\n' - .. 'Most entities built: '..awards.entities_built.player..' ('..awards.entities_built.value..')\\n' - .. 'Most time played: '..awards.time_played.player..' ('..Core.format_time(awards.time_played.value)..')\\n' - .. 'Furthest walked: '..awards.distance_walked.player..' ('..math.floor(awards.distance_walked.value)..')\\n' - .. 'Most coins earned: '..awards.coins_earned.player..' ('..awards.coins_earned.value..')\\n' - .. 'Seafood lover: '..awards.fish_eaten.player..' ('..awards.fish_eaten.value..' fish eaten)\\n' - ) - else - Server.to_discord_named_embed(map_promotion_channel, 'Crash Site map failed!\\n\\n' - .. 'Statistics:\\n' - .. 'Map time: '..time_string..'\\n' - .. 'Total kills: '..statistics.biters_killed..'\\n' - .. 'Biters remaining on map: '..statistics.enemy_entities..'\\n' - .. 'Players: '..statistics.total_players..'\\n' - ) - end - Server.to_discord_named_raw(map_promotion_channel, crash_site_role_mention .. ' **'..scenario_display_name[default_name]..' has just restarted!!**') - - Server.set_data('crash_site_data', tostring(end_epoch), statistics) -- Store the table, with end_epoch as the key - Popup.all('\nServer restarting!\nInitiated by ' .. data.name .. '\n') - end - - double_print(state) - - data.state = state - 1 - Task.set_timeout_in_ticks(60, callback, data) - end) - - local function map_cleared(player) - player = player or server_player local get_entity_count = game.forces["enemy"].get_entity_count -- Check how many of each turrets, worms and spawners are left and return false if there are any of each left. for i = 1, #static_entities_to_check do @@ -269,52 +108,148 @@ function Public.control(config) return true end - local function restart(args, player) - player = player or server_player - local sanitised_scenario = args.scenario_name - - if global_data.restarting then - player.print('Restart already in progress') - return + local function restart_callback() + local end_epoch = Server.get_current_time() + if end_epoch == nil then + end_epoch = -1 -- end_epoch is nil if the restart command is used locally rather than on the server end - if player ~= server_player and Rank.less_than(player.name, Ranks.admin) then - -- Check enemy count - if not map_cleared(player) then - return - end - - -- Limit the ability of non-admins to call the restart function with arguments to change the scenario - -- If not an admin, restart the same scenario always - sanitised_scenario = config.scenario_name - end - - global_data.restarting = true - - double_print('#################-Attention-#################') - double_print('Server restart initiated by ' .. player.name) - double_print('###########################################') - + local player_data = {} for _, p in pairs(game.players) do - if p.admin then - p.print('Abort restart with /abort') + player_data[p.index] = { + name = p.name, + total_kills = ScoreTracker.get_for_player(p.index, PlayerStats.player_total_kills_name), + spawners_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_spawners_killed_name), + worms_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_worms_killed_name), + units_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_units_killed_name), + turrets_killed = ScoreTracker.get_for_player(p.index, PlayerStats.player_turrets_killed_name), + distance_walked = ScoreTracker.get_for_player(p.index, PlayerStats.player_distance_walked_name), + player_deaths = ScoreTracker.get_for_player(p.index, PlayerStats.player_deaths_name), + coins_earned = ScoreTracker.get_for_player(p.index, PlayerStats.coins_earned_name), + entities_built = ScoreTracker.get_for_player(p.index, PlayerStats.player_entities_built_name), + entities_crafted = ScoreTracker.get_for_player(p.index, PlayerStats.player_items_crafted_name), + fish_eaten = ScoreTracker.get_for_player(p.index, PlayerStats.player_fish_eaten_name), + time_played = p.online_time + } + end + + local statistics = { + scenario = config.scenario_name, + start_epoch = Server.get_start_time(), + end_epoch = end_epoch, -- stored as key already, useful to have it as part of same structure + game_ticks = game.ticks_played, + enemy_entities = count_enemy_entities(), + biters_killed = ScoreTracker.get_for_global(PlayerStats.aliens_killed_name), + total_players = #game.players, + entities_built = ScoreTracker.get_for_global(PlayerStats.built_by_players_name), + player_data = player_data + } + + local awards = { + ['total_kills'] = {value = 0, player = ""}, + ['units_killed'] = {value = 0, player = ""}, + ['spawners_killed'] = {value = 0, player = ""}, + ['worms_killed'] = {value = 0, player = ""}, + ['player_deaths'] = {value = 0, player = ""}, + ['time_played'] = {value = 0, player = ""}, + ['entities_built'] = {value = 0, player = ""}, + ['entities_crafted'] = {value = 0, player = ""}, + ['distance_walked'] = {value = 0, player = ""}, + ['coins_earned'] = {value = 0, player = ""}, + ['fish_eaten'] = {value = 0, player = ""} + } + + for _, v in pairs(statistics.player_data) do + if v.total_kills > awards.total_kills.value then + awards.total_kills.value = v.total_kills + awards.total_kills.player = v.name + end + if v.units_killed > awards.units_killed.value then + awards.units_killed.value = v.units_killed + awards.units_killed.player = v.name + end + if v.spawners_killed > awards.spawners_killed.value then + awards.spawners_killed.value = v.spawners_killed + awards.spawners_killed.player = v.name + end + if v.worms_killed > awards.worms_killed.value then + awards.worms_killed.value = v.worms_killed + awards.worms_killed.player = v.name + end + if v.player_deaths > awards.player_deaths.value then + awards.player_deaths.value = v.player_deaths + awards.player_deaths.player = v.name + end + if v.time_played > awards.time_played.value then + awards.time_played.value = v.time_played + awards.time_played.player = v.name + end + if v.entities_built > awards.entities_built.value then + awards.entities_built.value = v.entities_built + awards.entities_built.player = v.name + end + if v.entities_crafted > awards.entities_crafted.value then + awards.entities_crafted.value = v.entities_crafted + awards.entities_crafted.player = v.name + end + if v.distance_walked > awards.distance_walked.value then + awards.distance_walked.value = v.distance_walked + awards.distance_walked.player = v.name + end + if v.coins_earned > awards.coins_earned.value then + awards.coins_earned.value = v.coins_earned + awards.coins_earned.player = v.name + end + if v.fish_eaten > awards.fish_eaten.value then + awards.fish_eaten.value = v.fish_eaten + awards.fish_eaten.player = v.name end end - print('Abort restart with /abort') - Task.set_timeout_in_ticks(60, callback, {name = player.name, scenario_name = sanitised_scenario, state = 10}) - end - local function abort(_, player) - player = player or server_player - - if global_data.restarting then - global_data.restarting = nil - double_print('Restart aborted by ' .. player.name) + local time_string = Core.format_time(game.ticks_played) + if statistics.enemy_entities < 1000 then + Server.to_discord_named_embed(map_promotion_channel, 'Crash Site map won!\\n\\n' + .. 'Statistics:\\n' + .. 'Map time: '..time_string..'\\n' + .. 'Total kills: '..statistics.biters_killed..'\\n' + .. 'Biters remaining on map: '..statistics.enemy_entities..'\\n' + .. 'Players: '..statistics.total_players..'\\n' + .. 'Total entities built: '..statistics.entities_built..'\\n\\n' + .. 'Awards:\\n' + .. 'Most kills overall: '..awards.total_kills.player..' ('..awards.total_kills.value..')\\n' + .. 'Most biters/spitters killed: '..awards.units_killed.player..' ('..awards.units_killed.value..')\\n' + .. 'Most spawners killed: '..awards.spawners_killed.player..' ('..awards.spawners_killed.value..')\\n' + .. 'Most worms killed: '..awards.worms_killed.player..' ('..awards.worms_killed.value..')\\n' + .. 'Most deaths: '..awards.player_deaths.player..' ('..awards.player_deaths.value..')\\n' + .. 'Most items crafted: '..awards.entities_crafted.player..' ('..awards.entities_crafted.value..')\\n' + .. 'Most entities built: '..awards.entities_built.player..' ('..awards.entities_built.value..')\\n' + .. 'Most time played: '..awards.time_played.player..' ('..Core.format_time(awards.time_played.value)..')\\n' + .. 'Furthest walked: '..awards.distance_walked.player..' ('..math.floor(awards.distance_walked.value)..')\\n' + .. 'Most coins earned: '..awards.coins_earned.player..' ('..awards.coins_earned.value..')\\n' + .. 'Seafood lover: '..awards.fish_eaten.player..' ('..awards.fish_eaten.value..' fish eaten)\\n' + ) else - player.print('Cannot abort a restart that is not in progress.') + Server.to_discord_named_embed(map_promotion_channel, 'Crash Site map failed!\\n\\n' + .. 'Statistics:\\n' + .. 'Map time: '..time_string..'\\n' + .. 'Total kills: '..statistics.biters_killed..'\\n' + .. 'Biters remaining on map: '..statistics.enemy_entities..'\\n' + .. 'Players: '..statistics.total_players..'\\n' + ) end + + local start_game_data = Restart.get_start_game_data() + local new_map_name = start_game_data.name + + Server.to_discord_named_raw(map_promotion_channel, + crash_site_role_mention .. ' **' .. scenario_display_name[config.scenario_name] .. ' has just restarted!!\\n' + .. 'Next map: ' .. new_map_name .. '**') + + Server.set_data('crash_site_data', tostring(end_epoch), statistics) -- Store the table, with end_epoch as the key end + Restart.register(can_restart, restart_callback) + local chart_area_callback = Token.register(function(data) local xpos = data.xpos local ypos = data.ypos @@ -345,7 +280,7 @@ function Public.control(config) -- process each set of coordinates local i = 1 local xpos = coords[i] - local ypos = coords[i+1] + local ypos = coords[i + 1] while xpos ~= nil and ypos ~= nil do local coin_count = inv.get_item_count("coin") @@ -359,14 +294,15 @@ function Public.control(config) for j = 1, 15 do set_timeout_in_ticks(60 * j, chart_area_callback, {player = player, xpos = xpos, ypos = ypos}) end - game.print({'command_description.crash_site_spy_success', player_name, spy_cost, xpos, ypos}, Color.success) + game.print({'command_description.crash_site_spy_success', player_name, spy_cost, xpos, ypos}, + Color.success) inv.remove({name = "coin", count = spy_cost}) end -- move to the next set of coordinates - i = i+2 + i = i + 2 xpos = coords[i] - ypos = coords[i+1] + ypos = coords[i + 1] end end @@ -395,29 +331,67 @@ function Public.control(config) local player = data.player local tag = player.force.add_chart_tag(player.surface, { icon = {type = 'item', name = 'poison-capsule'}, - position = {xpos,ypos}, + position = {xpos, ypos}, text = player.name }) - set_timeout_in_ticks(60*30, map_chart_tag_clear_callback, tag) -- To clear the tag after 30 seconds + set_timeout_in_ticks(60 * 30, map_chart_tag_clear_callback, tag) -- To clear the tag after 30 seconds end) local function render_crosshair(data) local red = {r = 0.5, g = 0, b = 0, a = 0.5} - local timeout = 5*60 + local timeout = 5 * 60 local line_width = 10 local line_length = 2 local s = data.player.surface local f = data.player.force - rendering.draw_circle{color=red, radius=1.5, width=line_width, filled=false, target=data.position, surface=s, time_to_live=timeout, forces={f}} - rendering.draw_line{color=red, width=line_width, from={data.position.x-line_length, data.position.y}, to={data.position.x+line_length, data.position.y}, surface=s, time_to_live=timeout, forces={f}} - rendering.draw_line{color=red, width=line_width, from={data.position.x, data.position.y-line_length}, to={data.position.x, data.position.y+line_length}, surface=s, time_to_live=timeout, forces={f}} - s.create_entity{name="flying-text", position={data.position.x+3, data.position.y}, text = "[item=poison-capsule] "..data.player.name, color = {r = 1, g = 1, b = 1, a = 1} } + rendering.draw_circle { + color = red, + radius = 1.5, + width = line_width, + filled = false, + target = data.position, + surface = s, + time_to_live = timeout, + forces = {f} + } + rendering.draw_line { + color = red, + width = line_width, + from = {data.position.x - line_length, data.position.y}, + to = {data.position.x + line_length, data.position.y}, + surface = s, + time_to_live = timeout, + forces = {f} + } + rendering.draw_line { + color = red, + width = line_width, + from = {data.position.x, data.position.y - line_length}, + to = {data.position.x, data.position.y + line_length}, + surface = s, + time_to_live = timeout, + forces = {f} + } + s.create_entity { + name = "flying-text", + position = {data.position.x + 3, data.position.y}, + text = "[item=poison-capsule] " .. data.player.name, + color = {r = 1, g = 1, b = 1, a = 1} + } end local function render_radius(data) - local timeout = 20*60 + local timeout = 20 * 60 local blue = {r = 0, g = 0, b = 0.1, a = 0.1} - rendering.draw_circle{color=blue, radius=data.radius+10, filled=true, target=data.position, surface=data.player.surface, time_to_live=timeout, players={data.player}} + rendering.draw_circle { + color = blue, + radius = data.radius + 10, + filled = true, + target = data.position, + surface = data.player.surface, + time_to_live = timeout, + players = {data.player} + } end local function strike(args, player) @@ -459,16 +433,17 @@ function Public.control(config) -- process each set of coordinates with a 10 strike limit local i = 1 local xpos = coords[i] - local ypos = coords[i+1] + local ypos = coords[i + 1] while xpos ~= nil and ypos ~= nil and i < 20 do -- Check the contents of the chest by spawn for enough poison capsules to use as payment local inv = dropbox.get_inventory(defines.inventory.chest) local capCount = inv.get_item_count("poison-capsule") if capCount < strikeCost then - player.print( - {'command_description.crash_site_airstrike_insufficient_currency_error', strikeCost - capCount}, - Color.fail) + player.print({ + 'command_description.crash_site_airstrike_insufficient_currency_error', + strikeCost - capCount + }, Color.fail) return end @@ -500,9 +475,9 @@ function Public.control(config) set_timeout_in_ticks(60, map_chart_tag_place_callback, {player = player, xpos = xpos, ypos = ypos}) -- move to the next set of coordinates - i = i+2 + i = i + 2 xpos = coords[i] - ypos = coords[i+1] + ypos = coords[i + 1] end end @@ -531,8 +506,13 @@ function Public.control(config) if name == 'airstrike_damage' then airstrike_data.count_level = airstrike_data.count_level + 1 - Toast.toast_all_players(15, {'command_description.crash_site_airstrike_damage_upgrade_success', player_name, count_level}) - Server.to_discord_bold('*** '..player_name..' has upgraded Airstrike Damage to level '..count_level..' ***') + Toast.toast_all_players(15, { + 'command_description.crash_site_airstrike_damage_upgrade_success', + player_name, + count_level + }) + Server.to_discord_bold('*** ' .. player_name .. ' has upgraded Airstrike Damage to level ' .. count_level + .. ' ***') item.name_label = {'command_description.crash_site_airstrike_count_name_label', (count_level + 1)} item.price = math.floor(math.exp(airstrike_data.count_level ^ 0.8) / 2) * 1000 item.description = { @@ -545,8 +525,13 @@ function Public.control(config) Retailer.set_item(market_id, item) -- this updates the retailer with the new item values. elseif name == 'airstrike_radius' then airstrike_data.radius_level = airstrike_data.radius_level + 1 - Toast.toast_all_players(15, {'command_description.crash_site_airstrike_radius_upgrade_success', player_name, radius_level}) - Server.to_discord_bold('*** '..player_name..' has upgraded Airstrike Radius to level '..radius_level..' ***') + Toast.toast_all_players(15, { + 'command_description.crash_site_airstrike_radius_upgrade_success', + player_name, + radius_level + }) + Server.to_discord_bold('*** ' .. player_name .. ' has upgraded Airstrike Radius to level ' .. radius_level + .. ' ***') item.name_label = {'command_description.crash_site_airstrike_radius_name_label', (radius_level + 1)} item.description = { 'command_description.crash_site_airstrike_radius', @@ -559,34 +544,6 @@ function Public.control(config) end end) - Command.add('crash-site-restart-abort', { - description = {'command_description.crash_site_restart_abort'}, - required_rank = Ranks.admin, - allowed_by_server = true - }, abort) - - Command.add('abort', { - description = {'command_description.crash_site_restart_abort'}, - required_rank = Ranks.admin, - allowed_by_server = true - }, abort) - - Command.add('crash-site-restart', { - description = {'command_description.crash_site_restart'}, - arguments = {'scenario_name'}, - default_values = {scenario_name = default_name}, - required_rank = Ranks.admin, - allowed_by_server = true - }, restart) - - Command.add('restart', { - description = {'command_description.crash_site_restart'}, - arguments = {'scenario_name'}, - default_values = {scenario_name = default_name}, - required_rank = Ranks.guest, - allowed_by_server = true - }, restart) - Command.add('spy', { description = {'command_description.crash_site_spy'}, arguments = {'location'}, diff --git a/map_gen/maps/danger_ores/modules/restart_command.lua b/map_gen/maps/danger_ores/modules/restart_command.lua index 99e21bb1..d148d5fb 100644 --- a/map_gen/maps/danger_ores/modules/restart_command.lua +++ b/map_gen/maps/danger_ores/modules/restart_command.lua @@ -1,113 +1,47 @@ -local Command = require 'utils.command' -local Rank = require 'features.rank_system' -local Ranks = require 'resources.ranks' -local Global = require 'utils.global' local Discord = require 'resources.discord' local Server = require 'features.server' -local Popup = require 'features.gui.popup' -local Task = require 'utils.task' -local Token = require 'utils.token' local Core = require 'utils.core' +local Restart = require 'features.restart_command' local ShareGlobals = require 'map_gen.maps.danger_ores.modules.shared_globals' return function(config) - local default_name = config.scenario_name or 'danger-ore-next' - local map_promotion_channel = Discord.channel_names.map_promotion local danger_ore_role_mention = Discord.role_mentions.danger_ore - local server_player = {name = '', print = print} - local global_data = {restarting = nil} + Restart.set_start_game_data({type = Restart.game_types.scenario, name = config.scenario_name or 'danger-ore-next'}) - Global.register(global_data, function(tbl) - global_data = tbl - end) + local function can_restart(player) + if player.admin then + return true + end - local function double_print(str) - game.print(str) - print(str) + if not ShareGlobals.data.map_won then + player.print({'command_description.danger_ore_restart_condition_not_met'}) + return false + end + + return true end - local callback - callback = Token.register(function(data) - if not global_data.restarting then - return - end + local function restart_callback() + local start_game_data = Restart.get_start_game_data() + local new_map_name = start_game_data.name - local state = data.state - if state == 0 then - Server.start_scenario(data.scenario_name) - double_print('restarting') - global_data.restarting = nil - return - elseif state == 1 then - Popup.all('\nServer restarting!\nInitiated by ' .. data.name .. '\n') + local time_string = Core.format_time(game.ticks_played) - local time_string = Core.format_time(game.ticks_played) - Server.to_discord_named_raw(map_promotion_channel, danger_ore_role_mention - .. ' **Danger Ore has just restarted! Previous map lasted: ' .. time_string .. '!**') - end + local message = { + danger_ore_role_mention, + ' **Danger Ore has just restarted! Previous map lasted: ', + time_string, + '!\\n', + 'Next map: ', + new_map_name, + '**' + } + message = table.concat(message) - double_print(state) - - data.state = state - 1 - Task.set_timeout_in_ticks(60, callback, data) - end) - - local function restart(args, player) - player = player or server_player - local sanitised_scenario = args.scenario_name - - if global_data.restarting then - player.print('Restart already in progress') - return - end - - if player ~= server_player and Rank.less_than(player.name, Ranks.admin) then - if not ShareGlobals.data.map_won then - player.print({'command_description.danger_ore_restart_condition_not_met'}) - return - end - - -- Limit the ability of non-admins to call the restart function with arguments to change the scenario - -- If not an admin, restart the same scenario always - sanitised_scenario = config.scenario_name - end - - global_data.restarting = true - - double_print('#################-Attention-#################') - double_print('Server restart initiated by ' .. player.name) - double_print('###########################################') - - for _, p in pairs(game.players) do - if p.admin then - p.print('Abort restart with /abort') - end - end - print('Abort restart with /abort') - Task.set_timeout_in_ticks(60, callback, {name = player.name, scenario_name = sanitised_scenario, state = 10}) + Server.to_discord_named_raw(map_promotion_channel, message) end - local function abort(_, player) - player = player or server_player - - if global_data.restarting then - global_data.restarting = nil - double_print('Restart aborted by ' .. player.name) - else - player.print('Cannot abort a restart that is not in progress.') - end - end - - Command.add('abort', - {description = {'command_description.abort'}, required_rank = Ranks.admin, allowed_by_server = true}, abort) - - Command.add('restart', { - description = {'command_description.restart'}, - arguments = {'scenario_name'}, - default_values = {scenario_name = default_name}, - required_rank = Ranks.guest, - allowed_by_server = true - }, restart) + Restart.register(can_restart, restart_callback) end diff --git a/map_gen/maps/danger_ores/presets/danger_bobangels_ores.lua b/map_gen/maps/danger_ores/presets/danger_bobangels_ores.lua index 724049f4..922d8776 100644 --- a/map_gen/maps/danger_ores/presets/danger_bobangels_ores.lua +++ b/map_gen/maps/danger_ores/presets/danger_bobangels_ores.lua @@ -150,7 +150,7 @@ rocket_launched( ) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-bobs-ores'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_bobs_ores.lua b/map_gen/maps/danger_ores/presets/danger_bobs_ores.lua index ed96e687..82160a50 100644 --- a/map_gen/maps/danger_ores/presets/danger_bobs_ores.lua +++ b/map_gen/maps/danger_ores/presets/danger_bobs_ores.lua @@ -160,7 +160,7 @@ rocket_launched( ) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-bobs-ores'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore.lua b/map_gen/maps/danger_ores/presets/danger_ore.lua index 5d9333d3..65ac6bad 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore.lua @@ -92,7 +92,7 @@ Event.on_init( ) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_chessboard.lua b/map_gen/maps/danger_ores/presets/danger_ore_chessboard.lua index 278c8a0d..59082772 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_chessboard.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_chessboard.lua @@ -109,7 +109,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-chessboard'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_chessboard_uniform.lua b/map_gen/maps/danger_ores/presets/danger_ore_chessboard_uniform.lua index 48022a80..22279113 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_chessboard_uniform.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_chessboard_uniform.lua @@ -109,7 +109,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-chessboard-uniform'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_deadlock_beltboxes.lua b/map_gen/maps/danger_ores/presets/danger_ore_deadlock_beltboxes.lua index 06abe7d6..fdf197d3 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_deadlock_beltboxes.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_deadlock_beltboxes.lua @@ -135,7 +135,7 @@ rocket_launched( ) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-deadlock-beltboxes'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_gradient.lua b/map_gen/maps/danger_ores/presets/danger_ore_gradient.lua index 0cd37444..e402abde 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_gradient.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_gradient.lua @@ -109,7 +109,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-gradient'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_hub_spiral.lua b/map_gen/maps/danger_ores/presets/danger_ore_hub_spiral.lua index bc0b106b..76c6996a 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_hub_spiral.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_hub_spiral.lua @@ -116,7 +116,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-hub-spiral'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_landfill.lua b/map_gen/maps/danger_ores/presets/danger_ore_landfill.lua index 50376b41..9a087635 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_landfill.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_landfill.lua @@ -108,7 +108,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-landfill'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_normal_science.lua b/map_gen/maps/danger_ores/presets/danger_ore_normal_science.lua index c5920e7c..9eaf928a 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_normal_science.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_normal_science.lua @@ -140,7 +140,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 5000}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger_ore_normal_science'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_spiral.lua b/map_gen/maps/danger_ores/presets/danger_ore_spiral.lua index ad71a049..8ef19f04 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_spiral.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_spiral.lua @@ -115,7 +115,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-spiral'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_split.lua b/map_gen/maps/danger_ores/presets/danger_ore_split.lua index b2ffb5ce..a870b22d 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_split.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_split.lua @@ -109,7 +109,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'danger-ore-split'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/map_gen/maps/danger_ores/presets/terraforming_danger_ore.lua b/map_gen/maps/danger_ores/presets/terraforming_danger_ore.lua index f378d66b..0829c8ef 100644 --- a/map_gen/maps/danger_ores/presets/terraforming_danger_ore.lua +++ b/map_gen/maps/danger_ores/presets/terraforming_danger_ore.lua @@ -109,7 +109,7 @@ local rocket_launched = require 'map_gen.maps.danger_ores.modules.rocket_launche rocket_launched({win_satellite_count = 500}) local restart_command = require 'map_gen.maps.danger_ores.modules.restart_command' -restart_command({scenario_name = 'danger-ore-next'}) +restart_command({scenario_name = 'terraforming-danger-ore'}) local container_dump = require 'map_gen.maps.danger_ores.modules.container_dump' container_dump({entity_name = 'coal'}) diff --git a/utils/command.lua b/utils/command.lua index e58c79e4..9351aea3 100644 --- a/utils/command.lua +++ b/utils/command.lua @@ -149,7 +149,7 @@ function Command.add(command_name, options, callback) help_text, function(command) local print -- custom print reference in case no player is present - local player = game.player + local player = game.get_player(command.player_index) local player_name = player and player.valid and player.name or '' if not player or not player.valid then print = log @@ -308,4 +308,30 @@ end Event.add(defines.events.on_console_command, on_command) +-- Backdoor for testing +if _DEBUG then + local EventCore = require 'utils.event_core' + + local commands_store = {} + _G.commands_store = commands_store + + local old_add_command = commands.add_command + commands.add_command = function(name, desc, func) + old_add_command(name, desc, func) + commands_store[name] = func + end + + function Command._raise_command(name, player_index, parameter) + local func = commands_store[name] or error('command \'' .. name .. '\' not found.', 2) + func({name = name, tick = game.tick, player_index = player_index, parameter = parameter }) + + EventCore.on_event({ + name = defines.events.on_console_command, + tick = game.tick, + player_index = player_index, + command = name, + parameters = parameter }) + end +end + return Command diff --git a/utils/gui_tests.lua b/utils/gui_tests.lua index 617ebe38..f3391fa2 100644 --- a/utils/gui_tests.lua +++ b/utils/gui_tests.lua @@ -1,7 +1,7 @@ local Declare = require 'utils.test.declare' -local EventFactory = require 'utils.test.event_factory' local Gui = require 'utils.gui' local Assert = require 'utils.test.assert' +local Helper = require 'utils.test.helper' Declare.module({'utils', 'Gui'}, function() Declare.module('can toggle top buttons', function() @@ -18,9 +18,8 @@ Declare.module({'utils', 'Gui'}, function() return end - local event = EventFactory.on_gui_click(element, player.index) local click_action = function() - EventFactory.raise(event) + Helper.click(element) end local before_count = count_gui_elements(player.gui) diff --git a/utils/string.lua b/utils/string.lua new file mode 100644 index 00000000..33a3d4ea --- /dev/null +++ b/utils/string.lua @@ -0,0 +1,9 @@ +--luacheck: globals string + +--- Removes whitespace from the start and end of the string. +-- http://lua-users.org/wiki/StringTrim +function string.trim(str) + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +return string diff --git a/utils/test/event_factory.lua b/utils/test/event_factory.lua index 1b0dadf3..cb6b659a 100644 --- a/utils/test/event_factory.lua +++ b/utils/test/event_factory.lua @@ -31,12 +31,12 @@ function Public.area(area) return area end -function Public.on_gui_click(element, player_index) +function Public.on_gui_click(element) return { name = defines.events.on_gui_click, tick = game.tick, element = element, - player_index = player_index, + player_index = element.player_index, button = defines.mouse_button_type.left, alt = false, control = false, @@ -44,6 +44,25 @@ function Public.on_gui_click(element, player_index) } end +function Public.on_gui_text_changed(element, text) + return { + name = defines.events.on_gui_text_changed, + tick = game.tick, + element = element, + player_index = element.player_index, + text = text + } +end + +function Public.on_gui_checked_state_changed(element) + return { + name = defines.events.on_gui_checked_state_changed, + tick = game.tick, + element = element, + player_index = element.player_index + } +end + function Public.on_player_deconstructed_area(player_index, surface, area, item) return { name = defines.events.on_player_deconstructed_area, diff --git a/utils/test/helper.lua b/utils/test/helper.lua index 6a6b01c8..5edaf605 100644 --- a/utils/test/helper.lua +++ b/utils/test/helper.lua @@ -1,4 +1,5 @@ local Global = require 'utils.global' +local EventFactory = require 'utils.test.event_factory' local Public = {} @@ -98,4 +99,70 @@ function Public.modify_lua_object(context, object, key, value) end) end +local function get_gui_element_by_name(parent, name) + if parent.name == name then + return parent + end + + for _, child in pairs(parent.children) do + local found = get_gui_element_by_name(child, name) + if found then + return found + end + end +end + +function Public.get_gui_element_by_name(parent, name) + if name == nil or name == '' then + return nil + end + + return get_gui_element_by_name(parent, name) +end + +function Public.click(element) + local element_type = element.type + + if element_type == 'checkbox' then + element.state = not element.state + local state_event = EventFactory.on_gui_checked_state_changed(element) + EventFactory.raise(state_event) + elseif element_type == 'radiobutton' and not element.state then + element.state = true + local state_event = EventFactory.on_gui_checked_state_changed(element) + EventFactory.raise(state_event) + end + + local click_event = EventFactory.on_gui_click(element) + EventFactory.raise(click_event) +end + +function Public.set_checkbox(element, state) + if element.type ~= 'checkbox' then + error('element is not a checkbox', 2) + end + + local old_state = not not element.state + if old_state == state then + return + end + + element.state = state + local state_event = EventFactory.on_gui_checked_state_changed(element) + EventFactory.raise(state_event) + + local click_event = EventFactory.on_gui_click(element) + EventFactory.raise(click_event) +end + +function Public.set_text(element, text) + if element.text == text then + return + end + + element.text = text + local text_event = EventFactory.on_gui_text_changed(element, text) + EventFactory.raise(text_event) +end + return Public