1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-03-17 21:08:08 +02:00

Add config-restart command.

- change Restart command to use server.start_game (offers setting the mod pack).
- Change danger ores and crashsite maps to use the new restart command.
This commit is contained in:
James Gillham 2021-04-19 21:28:58 +01:00
parent 66883a87b3
commit ca381aa289
24 changed files with 1215 additions and 373 deletions

View File

@ -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'

View File

@ -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 = '<server>', 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<table> {type<string:'scenario'|'save'>, name<string>, mod_pack<string?>}
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<table|string> {type<string?:'scenario'|'save'='scenario'>, name<string>, mod_pack<string?>}
-- 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<string> | {type<string?>, name<string>, mod_pack<string?>} 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

View File

@ -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)

View File

@ -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<scenario|save> 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

View File

@ -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 = '<server>', 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'},

View File

@ -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 = '<server>', 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

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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'})

View File

@ -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 '<server>'
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

View File

@ -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)

9
utils/string.lua Normal file
View File

@ -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

View File

@ -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,

View File

@ -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