659 lines
19 KiB
Lua
659 lines
19 KiB
Lua
-- SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
|
--
|
|
-- 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 <name>', c)
|
|
return
|
|
end
|
|
|
|
local bits = split_whitespace(cmd.parameter)
|
|
if #bits ~= 1 then
|
|
p('/destroyworld <name>', 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 <name> <map exchange string>', c)
|
|
return
|
|
end
|
|
|
|
local bits = split_whitespace(cmd.parameter)
|
|
if #bits <= 2 then
|
|
p('/createworld <name> <map exchange string>', 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 <name>', c)
|
|
return
|
|
end
|
|
|
|
local bits = split_whitespace(cmd.parameter)
|
|
if #bits ~= 2 then
|
|
p('/debugrenameworld <name>', 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 <name>', c)
|
|
return
|
|
end
|
|
|
|
local bits = split_whitespace(cmd.parameter)
|
|
if #bits ~= 1 then
|
|
p('/debugresetworldstate <name>', 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
|