-- 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 if player.crafting_queue_size > 0 then p("Can't port between worlds while crafting", c) return end 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 debugrenameworld_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('/debugrenameworld ', c) return end local bits = split_whitespace(cmd.parameter) if #bits ~= 2 then p('/debugrenameworld ', c) return end local old_name = bits[1] local new_name = bits[2] if old_name == 'nauvis' or old_name == 'spawn' then p('Sorry, renaming the spawn world is not permitted', c) return end local surface = get_surface_by_name(old_name) if surface == nil then p('No such world ' .. old_name, c) return end -- We need to get some stuff before we rename the surface, since it's name dependent. local force = force_for_surface(surface) -- Rename the surface. surface.name = new_name p('Renamed surface ' .. old_name .. ' to ' .. new_name, c) -- Check if we need to rename the force. local new_force_name = force_name_for_surface(surface) if force == nil then p('Creating new force ' .. new_force_name) force = game.create_force(new_force_name) 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 elseif force.name ~= new_force_name then p('Renaming force ' .. force.name .. ' to ' .. new_force_name) local new_force = game.create_force(new_force_name) game.merge_forces(force.name, new_force.name) force = new_force end -- Move inventories and locations, if any, from the old name to the new name. if global.inventories == nil then global.inventories = {} end if global.locations == nil then global.locations = {} end local old_surface_inventories = global.inventories[old_name] if old_surface_inventories == nil then old_surface_inventories = {} end local old_surface_locations = global.locations[old_name] if old_surface_locations == nil then old_surface_locations = {} end global.inventories[new_name] = old_surface_inventories global.locations[new_name] = old_surface_locations 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('debugrenameworld', 'Renames a surface and remaps corresponding state', function(cmd) xpcall(function() debugrenameworld_command(cmd) end, error_handler("/debugrenameworld", 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