From e005a5fd6ec2b16204683f46ffc00449a70e079d Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sat, 5 Feb 2022 17:40:03 +0000 Subject: [PATCH] ops/factorio/multiworld: add debugrenameworld --- ops/factorio/multiworld/README.md | 9 +- ops/factorio/multiworld/multiworld.lua | 1138 ++++++++++++------------ 2 files changed, 567 insertions(+), 580 deletions(-) diff --git a/ops/factorio/multiworld/README.md b/ops/factorio/multiworld/README.md index 5673ce3976..4aa0bcad12 100644 --- a/ops/factorio/multiworld/README.md +++ b/ops/factorio/multiworld/README.md @@ -46,8 +46,13 @@ achievement disabling flag. This is intended for use for admins to import existing save files as new surfaces. -Note that once you've done so, you'll need to use the `/debugresetworldstate` -command in order to properly set up the extra metadata required by Multiworld. +Note that once you've done so, you'll need to use the `/debugrenameworld` or +`/debugresetworldstate` commands in order to properly set up the extra metadata +required by Multiworld. + +### `/debugrenameworld ` - rename a surface (admins only) + +This is useful for renaming worlds imported by the `/debugeditor`. ### `/debugresetworldstate ` - reset book-keeping metadata for a world (admins only) diff --git a/ops/factorio/multiworld/multiworld.lua b/ops/factorio/multiworld/multiworld.lua index ee55bbb43f..e00229d0ee 100644 --- a/ops/factorio/multiworld/multiworld.lua +++ b/ops/factorio/multiworld/multiworld.lua @@ -1,578 +1,560 @@ --- SPDX-FileCopyrightText: 2022 Luke Granger-Brown --- --- SPDX-License-Identifier: Apache-2.0 -local c = {r = 1, g = 0.5, b = 0.1} -local c_fatal = {r = 1, g = 0.5, b = 0.5} - -local function split_whitespace(s) - chunks = {} - for substring in s:gmatch("%S+") do - table.insert(chunks, substring) - end - return chunks -end - -local function error_handler(doing_what, player_cause) - return function(err) - local full_err = '[' .. player_cause.name .. ' caused error while ' .. - doing_what .. '] ' .. err .. '\n' .. - debug.traceback() - print(full_err) - if not player_cause.admin then - player_cause.print('Oops, something went wrong when performing ' .. - doing_what .. ': ' .. err, c_fatal) - end - for _, player in pairs(game.connected_players) do - if player.admin then - player.print(full_err, c_fatal) - end - end - end -end - -local starter_items = { - ["iron-plate"] = 8, - ["wood"] = 1, - ["burner-mining-drill"] = 1, - ["stone-furnace"] = 1 -} - -local starter_ammo_items = {["pistol"] = 1, ["firearm-magazine"] = 10} - -local function get_surface_by_name(name) - -- Yeet. - if name == 'spawn' then - return game.get_surface('nauvis') - elseif name == 'nauvis' then - return nil - end - return game.get_surface(name) -end - -local function get_surface_display_name(surface) - if surface.name == 'nauvis' then - return 'spawn' - end - return surface.name -end - -local function force_name_for_surface(surface) - if surface.name == 'nauvis' then - return 'player' - end - return 'player_' .. surface.name -end - -local function force_for_surface(surface) - return game.forces[force_name_for_surface(surface)] -end - -local inventory_types = { - -- The order of this dictates the load order from the top. - -- The save order starts from the bottom. - - -- Armor first, because it grants inventory size buffs. - defines.inventory.character_armor, defines.inventory.character_guns, - defines.inventory.character_ammo, defines.inventory.character_vehicle, - defines.inventory.character_trash, defines.inventory.character_main -} - -local function save_inventory(player, inventories, inventory_type) - if inventories[inventory_type] ~= nil then - inventories[inventory_type].destroy() - end - inventories[inventory_type] = nil - - local src_inventory = player.character.get_inventory(inventory_type) - if src_inventory == nil then - return - end - - local dst_inventory = game.create_inventory(#src_inventory) - inventories[inventory_type] = dst_inventory - for i = 1, #src_inventory do - dst_inventory[i].swap_stack(src_inventory[i]) - end - src_inventory.clear() -end - -local function save_inventories(player, inventories) - for i = #inventory_types, 1, -1 do - save_inventory(player, inventories, inventory_types[i]) - end -end - -local function restore_inventory(player, inventories, inventory_type) - local dst_inventory = player.character.get_inventory(inventory_type) - if dst_inventory == nil then - return true - end - - dst_inventory.clear() - - if inventories[inventory_type] == nil then - -- Special case: we have no inventory to restore from, so give them default spawn stuff. - if inventory_type == defines.inventory.character_main then - player.print( - 'Giving you the starter items, since you seem to be new to the world...', - c) - for item_name, count in pairs(starter_items) do - player.insert({name = item_name, count = count}) - end - elseif inventory_type == defines.inventory.character_ammo then - for item_name, count in pairs(starter_ammo_items) do - player.insert({name = item_name, count = count}) - end - end - return false - end - local src_inventory = inventories[inventory_type] - for i = 1, #src_inventory do - dst_inventory[i].swap_stack(src_inventory[i]) - end - src_inventory.clear() - return true -end - -local function restore_inventories(player, inventories) - for i = 1, #inventory_types do - restore_inventory(player, inventories, inventory_types[i]) - end -end - -local function get_saved_inventory(surface, player) - local surface_inventories = global.inventories[surface.name] - if surface_inventories == nil then - surface_inventories = {} - global.inventories[surface.name] = surface_inventories - end - local inventories = surface_inventories[player.index] - if inventories == nil then - inventories = {} - surface_inventories[player.index] = inventories - end - return inventories -end - -local function get_surface_locations(surface) - if global.locations == nil then - global.locations = {} - end - local surface_locations = global.locations[surface.name] - if surface_locations == nil then - surface_locations = {} - global.locations[surface.name] = surface_locations - end - return surface_locations -end - -local function port_player_to_world(player, surface) - local p = player.print - - player.clear_cursor() - save_inventories(player, get_saved_inventory(player.surface, player)) - - local src_locations = get_surface_locations(player.surface) - local dst_locations = get_surface_locations(surface) - src_locations[player.index] = player.character.position - local force = force_for_surface(surface) - local dst_location = dst_locations[player.index] - if dst_location == nil then - dst_location = force.get_spawn_position(surface) - end - local safe_dst_location = surface.find_non_colliding_position("character", - dst_location, - surface.get_starting_area_radius(), - 2) - if safe_dst_location ~= nil then - dst_location = safe_dst_location - end - player.teleport(dst_location, surface) - player.force = force - - restore_inventories(player, get_saved_inventory(surface, player)) -end - -local function port_player_to_world_by_name(player, name) - local p = player.print - local surface = get_surface_by_name(name) - if surface == nil then - p("No such world " .. name, c) - return - end - if player.character.surface == surface then - p("Already on world " .. name, c) - return - end - port_player_to_world(player, surface) -end - -local function show_gui(player) - local p = player.print - if player.gui.screen["lukegb-gui"] then - player.gui.screen["lukegb-gui"].destroy() - end - local outer = player.gui.screen.add { - type = "frame", - name = "lukegb-gui", - direction = "vertical" - } - - local title_flow = outer.add { - type = "flow", - name = "title_flow", - direction = "horizontal" - } - title_flow.style.horizontally_stretchable = true - title_flow.style.horizontal_spacing = 8 - - local title = title_flow.add { - type = "label", - caption = "World Selector", - style = "frame_title" - } - title.drag_target = outer - - local title_pusher = title_flow.add { - type = "empty-widget", - style = "draggable_space_header" - } - title_pusher.style.height = 24 - title_pusher.style.horizontally_stretchable = true - title_pusher.drag_target = outer - - local close_button = title_flow.add { - type = "sprite-button", - style = "frame_action_button", - sprite = "utility/close_white", - tags = {lukegb_close_btn = true} - } - - local cont = outer.add {type = "frame", style = "inside_shallow_frame"} - local surf_scroll = cont.add { - type = "scroll-pane", - style = "scroll_pane_under_subheader" - } - for _, surf in pairs(game.surfaces) do - local btn = surf_scroll.add { - type = "button", - caption = get_surface_display_name(surf), - style = "menu_button", - enabled = surf ~= player.surface, - tags = {lukegb_dst_world = surf.index} - } - end - outer.force_auto_center() - player.opened = outer -end - -local function handle_gui_click(e) - local p - local player = game.players[e.player_index] - - if player == nil then - print('Sorry, need to be in game') - return - end - p = player.print - - if e.element.tags.lukegb_dst_world ~= nil then - port_player_to_world(player, - game.surfaces[e.element.tags.lukegb_dst_world]) - player.opened = nil - elseif e.element.tags.lukegb_close_btn then - player.opened = nil - end -end - -local function handle_gui_closed(e) - if e.gui_type ~= defines.gui_type.custom then - return - end - if e.element == nil then - return - end - if e.element.name ~= "lukegb-gui" then - return - end - e.element.destroy() -end - -local function ws_command(cmd) - local p - local player = game.player - - if player == nil then - print('Sorry, need to be in game') - return - end - p = player.print - - if cmd.parameter ~= nil then - -- If we got a parameter, put them there. - port_player_to_world_by_name(player, cmd.parameter) - return - end - - -- Otherwise, show the gui. - show_gui(player) -end - -local function reset_inventories(surface, p) - if global.inventories == nil then - global.inventories = {} - end - if global.inventories[surface.name] ~= nil then - p('Destroying inventories for surface ' .. surface.name, c) - for player_idx, inventories in pairs(global.inventories[surface.name]) do - p(' .. ' .. game.players[player_idx].name, c) - for inventory_type, inventory in pairs(inventories) do - inventory.destroy() - end - end - end - global.inventories[surface.name] = {} -end - -local function reset_locations(surface, p) - if global.locations == nil then - global.locations = {} - end - if global.locations[surface.name] ~= nil then - p('Destroying locations for surface ' .. surface.name, c) - for player_idx, location in pairs(global.locations[surface.name]) do - p(' .. ' .. game.players[player_idx].name .. ' (' .. location.x .. - ', ' .. location.y .. ')', c) - -- We don't do anything here, this is just for logging purposes. - end - end - global.locations[surface.name] = {} -end - -local function destroy_world(name, p) - local surface = get_surface_by_name(name) - if surface == nil then - p('No such world ' .. name, c) - return - end - if surface.name == 'nauvis' then - p('Spawn world cannot be destroyed', c) - return - end - - if force_for_surface(surface) ~= nil then - p('Destroying force ' .. force_name_for_surface(surface), c) - game.merge_forces(force_name_for_surface(surface), 'player') - end - - p('Destroying surface ' .. surface.name, c) - game.delete_surface(surface) - - reset_inventories(surface, p) - reset_locations(surface, p) -end - -local function create_world(name, map_gen_settings, p) - local surface = get_surface_by_name(name) - if surface ~= nil then - p('World named ' .. name .. ' already exists', c) - return - end - - p('Creating surface ' .. name, c) - local surface = game.create_surface(name, map_gen_settings) - p('Creating force ' .. force_name_for_surface(surface), c) - local force = game.create_force(force_name_for_surface(surface)) - - reset_inventories(surface, p) - reset_locations(surface, p) -end - -local function destroyworld_command(cmd) - local p - local player = game.player - - if player == nil then - print('Sorry, need to be in game') - return - end - p = player.print - - if player.admin ~= true then - p('Need to be an admin') - return - end - - if cmd.parameter == nil then - p('/destroyworld ', c) - return - end - - local bits = split_whitespace(cmd.parameter) - if #bits ~= 1 then - p('/destroyworld ', c) - return - end - - local map_name = bits[1] - - destroy_world(map_name, p) -end - -local function createworld_command(cmd) - local p - local player = game.player - - if player == nil then - print('Sorry, need to be in game') - return - end - p = player.print - - if player.admin ~= true then - p('Need to be an admin', c) - return - end - - if cmd.parameter == nil then - p('/createworld ', c) - return - end - - local bits = split_whitespace(cmd.parameter) - if #bits <= 2 then - p('/createworld ', c) - return - end - local map_exchange_string_bits = {} - for k, v in pairs({table.unpack(bits, 2)}) do - map_exchange_string_bits[k] = v - end - local map_exchange_string = table.concat(map_exchange_string_bits, " ") - - local map_name = bits[1] - local map_exchange = game.parse_map_exchange_string(map_exchange_string) - - create_world(map_name, map_exchange.map_gen_settings, p) -end - -local function debugeditor_command(cmd) - local p - local player = game.player - - if player == nil then - print('Sorry, need to be in game') - return - end - p = player.print - - if player.admin ~= true then - p('Need to be an admin', c) - return - end - - player.toggle_map_editor() -end - -local function debugresetworldstate_command(cmd) - local p - local player = game.player - - if player == nil then - print('Sorry, need to be in game') - return - end - p = player.print - - if player.admin ~= true then - p('Need to be an admin', c) - return - end - - if cmd.parameter == nil then - p('/debugresetworldstate ', c) - return - end - - local bits = split_whitespace(cmd.parameter) - if #bits ~= 1 then - p('/debugresetworldstate ', c) - return - end - - local map_name = bits[1] - local surface = get_surface_by_name(map_name) - if surface == nil then - p('No such world ' .. map_name, c) - return - end - - if force_for_surface(surface) ~= nil then - p('Destroying force ' .. force_name_for_surface(surface), c) - game.merge_forces(force_name_for_surface(surface), 'player') - end - p('Creating force ' .. force_name_for_surface(surface), c) - local force = game.create_force(force_name_for_surface(surface)) - - reset_inventories(surface, p) - reset_locations(surface, p) - - local force = force_for_surface(surface) - local player_owned_entities = surface.find_entities_filtered { - force = "player" - } - p('Moving everything (' .. #player_owned_entities .. - ' entities) from the player force to the ' .. force.name .. ' force') - for _, entity in pairs(player_owned_entities) do - entity.force = force - end -end - -local function make_event_handler(name, handler) - return function(e) - xpcall(function() handler(e) end, - error_handler(name, game.players[e.player_index])) - end -end - -local multiworld = {} - -multiworld.events = { - [defines.events.on_gui_click] = make_event_handler("on_gui_click", - handle_gui_click), - [defines.events.on_gui_closed] = make_event_handler("on_gui_closed", - handle_gui_closed) -} - -multiworld.add_commands = function() - commands.add_command('ws', 'View the list of worlds', ws_command) - - commands.add_command('destroyworld', 'Destroy a world by name', - function(cmd) - xpcall(function() destroyworld_command(cmd) end, - error_handler("/destroyworld", game.player)) - end) - - commands.add_command('createworld', - 'Create a new world given a name and a map exchange string', - function(cmd) - xpcall(function() createworld_command(cmd) end, - error_handler("/createworld", game.player)) - end) - - commands.add_command('debugeditor', - 'Toggles into or out of the map editor without disabling achievements', - function(cmd) - xpcall(function() debugeditor_command(cmd) end, - error_handler("/debugeditor", game.player)) - end) - - commands.add_command('debugresetworldstate', - 'Resets the multiworld state for a surface without touching the surface itself', - function(cmd) - xpcall(function() debugresetworldstate_command(cmd) end, - error_handler("/debugresetworldstate", game.player)) - end) -end - -return multiworld +-- SPDX-FileCopyrightText: 2022 Luke Granger-Brown +-- +-- SPDX-License-Identifier: Apache-2.0 +local c = {r = 1, g = 0.5, b = 0.1} +local c_fatal = {r = 1, g = 0.5, b = 0.5} + +local function split_whitespace(s) + chunks = {} + for substring in s:gmatch("%S+") do + table.insert(chunks, substring) + end + return chunks +end + +local function error_handler(doing_what, player_cause) + return function(err) + local full_err = '[' .. player_cause.name .. ' caused error while ' .. + doing_what .. '] ' .. err .. '\n' .. + debug.traceback() + print(full_err) + if not player_cause.admin then + player_cause.print('Oops, something went wrong when performing ' .. + doing_what .. ': ' .. err, c_fatal) + end + for _, player in pairs(game.connected_players) do + if player.admin then + player.print(full_err, c_fatal) + end + end + end +end + +local starter_items = { + ["iron-plate"] = 8, + ["wood"] = 1, + ["burner-mining-drill"] = 1, + ["stone-furnace"] = 1 +} + +local starter_ammo_items = {["pistol"] = 1, ["firearm-magazine"] = 10} + +local function get_surface_by_name(name) + -- Yeet. + if name == 'spawn' then + return game.get_surface('nauvis') + elseif name == 'nauvis' then + return nil + end + return game.get_surface(name) +end + +local function get_surface_display_name(surface) + if surface.name == 'nauvis' then + return 'spawn' + end + return surface.name +end + +local function force_name_for_surface(surface) + if surface.name == 'nauvis' then + return 'player' + end + return 'player_' .. surface.name +end + +local function force_for_surface(surface) + return game.forces[force_name_for_surface(surface)] +end + +local inventory_types = { + -- The order of this dictates the load order from the top. + -- The save order starts from the bottom. + + -- Armor first, because it grants inventory size buffs. + defines.inventory.character_armor, defines.inventory.character_guns, + defines.inventory.character_ammo, defines.inventory.character_vehicle, + defines.inventory.character_trash, defines.inventory.character_main +} + +local function save_inventory(player, inventories, inventory_type) + if inventories[inventory_type] ~= nil then + inventories[inventory_type].destroy() + end + inventories[inventory_type] = nil + + local src_inventory = player.character.get_inventory(inventory_type) + if src_inventory == nil then + return + end + + local dst_inventory = game.create_inventory(#src_inventory) + inventories[inventory_type] = dst_inventory + for i = 1, #src_inventory do + dst_inventory[i].swap_stack(src_inventory[i]) + end + src_inventory.clear() +end + +local function save_inventories(player, inventories) + for i = #inventory_types, 1, -1 do + save_inventory(player, inventories, inventory_types[i]) + end +end + +local function restore_inventory(player, inventories, inventory_type) + local dst_inventory = player.character.get_inventory(inventory_type) + if dst_inventory == nil then + return true + end + + dst_inventory.clear() + + if inventories[inventory_type] == nil then + -- Special case: we have no inventory to restore from, so give them default spawn stuff. + if inventory_type == defines.inventory.character_main then + player.print( + 'Giving you the starter items, since you seem to be new to the world...', + c) + for item_name, count in pairs(starter_items) do + player.insert({name = item_name, count = count}) + end + elseif inventory_type == defines.inventory.character_ammo then + for item_name, count in pairs(starter_ammo_items) do + player.insert({name = item_name, count = count}) + end + end + return false + end + local src_inventory = inventories[inventory_type] + for i = 1, #src_inventory do + dst_inventory[i].swap_stack(src_inventory[i]) + end + src_inventory.clear() + return true +end + +local function restore_inventories(player, inventories) + for i = 1, #inventory_types do + restore_inventory(player, inventories, inventory_types[i]) + end +end + +local function get_saved_inventory(surface, player) + local surface_inventories = global.inventories[surface.name] + if surface_inventories == nil then + surface_inventories = {} + global.inventories[surface.name] = surface_inventories + end + local inventories = surface_inventories[player.index] + if inventories == nil then + inventories = {} + surface_inventories[player.index] = inventories + end + return inventories +end + +local function get_surface_locations(surface) + if global.locations == nil then + global.locations = {} + end + local surface_locations = global.locations[surface.name] + if surface_locations == nil then + surface_locations = {} + global.locations[surface.name] = surface_locations + end + return surface_locations +end + +local function port_player_to_world(player, surface) + local p = player.print + + player.clear_cursor() + save_inventories(player, get_saved_inventory(player.surface, player)) + + local src_locations = get_surface_locations(player.surface) + local dst_locations = get_surface_locations(surface) + src_locations[player.index] = player.character.position + local force = force_for_surface(surface) + local dst_location = dst_locations[player.index] + if dst_location == nil then + dst_location = force.get_spawn_position(surface) + end + local safe_dst_location = surface.find_non_colliding_position("character", + dst_location, + surface.get_starting_area_radius(), + 2) + if safe_dst_location ~= nil then + dst_location = safe_dst_location + end + player.teleport(dst_location, surface) + player.force = force + + restore_inventories(player, get_saved_inventory(surface, player)) +end + +local function port_player_to_world_by_name(player, name) + local p = player.print + local surface = get_surface_by_name(name) + if surface == nil then + p("No such world " .. name, c) + return + end + if player.character.surface == surface then + p("Already on world " .. name, c) + return + end + port_player_to_world(player, surface) +end + +local function show_gui(player) + local p = player.print + if player.gui.screen["lukegb-gui"] then + player.gui.screen["lukegb-gui"].destroy() + end + local outer = player.gui.screen.add { + type = "frame", + name = "lukegb-gui", + direction = "vertical" + } + + local title_flow = outer.add { + type = "flow", + name = "title_flow", + direction = "horizontal" + } + title_flow.style.horizontally_stretchable = true + title_flow.style.horizontal_spacing = 8 + + local title = title_flow.add { + type = "label", + caption = "World Selector", + style = "frame_title" + } + title.drag_target = outer + + local title_pusher = title_flow.add { + type = "empty-widget", + style = "draggable_space_header" + } + title_pusher.style.height = 24 + title_pusher.style.horizontally_stretchable = true + title_pusher.drag_target = outer + + local close_button = title_flow.add { + type = "sprite-button", + style = "frame_action_button", + sprite = "utility/close_white", + tags = {lukegb_close_btn = true} + } + + local cont = outer.add {type = "frame", style = "inside_shallow_frame"} + local surf_scroll = cont.add { + type = "scroll-pane", + style = "scroll_pane_under_subheader" + } + for _, surf in pairs(game.surfaces) do + local btn = surf_scroll.add { + type = "button", + caption = get_surface_display_name(surf), + style = "menu_button", + enabled = surf ~= player.surface, + tags = {lukegb_dst_world = surf.index} + } + end + outer.force_auto_center() + player.opened = outer +end + +local function handle_gui_click(e) + local p + local player = game.players[e.player_index] + + if player == nil then + print('Sorry, need to be in game') + return + end + p = player.print + + if e.element.tags.lukegb_dst_world ~= nil then + port_player_to_world(player, + game.surfaces[e.element.tags.lukegb_dst_world]) + player.opened = nil + elseif e.element.tags.lukegb_close_btn then + player.opened = nil + end +end + +local function handle_gui_closed(e) + if e.gui_type ~= defines.gui_type.custom then + return + end + if e.element == nil then + return + end + if e.element.name ~= "lukegb-gui" then + return + end + e.element.destroy() +end + +local function ws_command(cmd) + local p + local player = game.player + + if player == nil then + print('Sorry, need to be in game') + return + end + p = player.print + + if cmd.parameter ~= nil then + -- If we got a parameter, put them there. + port_player_to_world_by_name(player, cmd.parameter) + return + end + + -- Otherwise, show the gui. + show_gui(player) +end + +local function reset_inventories(surface, p) + if global.inventories == nil then + global.inventories = {} + end + if global.inventories[surface.name] ~= nil then + p('Destroying inventories for surface ' .. surface.name, c) + for player_idx, inventories in pairs(global.inventories[surface.name]) do + p(' .. ' .. game.players[player_idx].name, c) + for inventory_type, inventory in pairs(inventories) do + inventory.destroy() + end + end + end + global.inventories[surface.name] = {} +end + +local function destroy_world(name, p) + local surface = get_surface_by_name(name) + if surface == nil then + p('No such world ' .. name, c) + return + end + if surface.name == 'nauvis' then + p('Spawn world cannot be destroyed', c) + return + end + + if force_for_surface(surface) ~= nil then + p('Destroying force ' .. force_name_for_surface(surface), c) + game.merge_forces(force_name_for_surface(surface), 'player') + end + + p('Destroying surface ' .. surface.name, c) + game.delete_surface(surface) + + reset_inventories(surface, p) +end + +local function create_world(name, map_gen_settings, p) + local surface = get_surface_by_name(name) + if surface ~= nil then + p('World named ' .. name .. ' already exists', c) + return + end + + p('Creating surface ' .. name, c) + local surface = game.create_surface(name, map_gen_settings) + p('Creating force ' .. force_name_for_surface(surface), c) + local force = game.create_force(force_name_for_surface(surface)) + + reset_inventories(surface, p) +end + +local function destroyworld_command(cmd) + local p + local player = game.player + + if player == nil then + print('Sorry, need to be in game') + return + end + p = player.print + + if player.admin ~= true then + p('Need to be an admin') + return + end + + if cmd.parameter == nil then + p('/destroyworld ', c) + return + end + + local bits = split_whitespace(cmd.parameter) + if #bits ~= 1 then + p('/destroyworld ', c) + return + end + + local map_name = bits[1] + + destroy_world(map_name, p) +end + +local function createworld_command(cmd) + local p + local player = game.player + + if player == nil then + print('Sorry, need to be in game') + return + end + p = player.print + + if player.admin ~= true then + p('Need to be an admin', c) + return + end + + if cmd.parameter == nil then + p('/createworld ', c) + return + end + + local bits = split_whitespace(cmd.parameter) + if #bits <= 2 then + p('/createworld ', c) + return + end + local map_exchange_string_bits = {} + for k, v in pairs({table.unpack(bits, 2)}) do + map_exchange_string_bits[k] = v + end + local map_exchange_string = table.concat(map_exchange_string_bits, " ") + + local map_name = bits[1] + local map_exchange = game.parse_map_exchange_string(map_exchange_string) + + create_world(map_name, map_exchange.map_gen_settings, p) +end + +local function debugeditor_command(cmd) + local p + local player = game.player + + if player == nil then + print('Sorry, need to be in game') + return + end + p = player.print + + if player.admin ~= true then + p('Need to be an admin', c) + return + end + + player.toggle_map_editor() +end + +local function debugresetworldstate_command(cmd) + local p + local player = game.player + + if player == nil then + print('Sorry, need to be in game') + return + end + p = player.print + + if player.admin ~= true then + p('Need to be an admin', c) + return + end + + if cmd.parameter == nil then + p('/debugresetworldstate ', c) + return + end + + local bits = split_whitespace(cmd.parameter) + if #bits ~= 1 then + p('/debugresetworldstate ', c) + return + end + + local map_name = bits[1] + local surface = get_surface_by_name(map_name) + if surface == nil then + p('No such world ' .. map_name, c) + return + end + + if force_for_surface(surface) ~= nil then + p('Destroying force ' .. force_name_for_surface(surface), c) + game.merge_forces(force_name_for_surface(surface), 'player') + end + p('Creating force ' .. force_name_for_surface(surface), c) + local force = game.create_force(force_name_for_surface(surface)) + + reset_inventories(surface, p) + + local force = force_for_surface(surface) + local player_owned_entities = surface.find_entities_filtered { + force = "player" + } + p('Moving everything (' .. #player_owned_entities .. + ' entities) from the player force to the ' .. force.name .. ' force') + for _, entity in pairs(player_owned_entities) do + entity.force = force + end +end + +local function make_event_handler(name, handler) + return function(e) + xpcall(function() handler(e) end, + error_handler(name, game.players[e.player_index])) + end +end + +local multiworld = {} + +multiworld.events = { + [defines.events.on_gui_click] = make_event_handler("on_gui_click", + handle_gui_click), + [defines.events.on_gui_closed] = make_event_handler("on_gui_closed", + handle_gui_closed) +} + +multiworld.add_commands = function() + commands.add_command('ws', 'View the list of worlds', ws_command) + + commands.add_command('destroyworld', 'Destroy a world by name', + function(cmd) + xpcall(function() destroyworld_command(cmd) end, + error_handler("/destroyworld", game.player)) + end) + + commands.add_command('createworld', + 'Create a new world given a name and a map exchange string', + function(cmd) + xpcall(function() createworld_command(cmd) end, + error_handler("/createworld", game.player)) + end) + + commands.add_command('debugeditor', + 'Toggles into or out of the map editor without disabling achievements', + function(cmd) + xpcall(function() debugeditor_command(cmd) end, + error_handler("/debugeditor", game.player)) + end) + + commands.add_command('debugresetworldstate', + 'Resets the multiworld state for a surface without touching the surface itself', + function(cmd) + xpcall(function() debugresetworldstate_command(cmd) end, + error_handler("/debugresetworldstate", game.player)) + end) +end + +return multiworld