ops/factorio/multiworld: init
This commit is contained in:
parent
cc6fd576e7
commit
ee8ec5263a
8 changed files with 830 additions and 0 deletions
|
@ -28,6 +28,7 @@ let
|
|||
twitternuke = depot.go.twitternuke;
|
||||
systemPathJSON = depot.ops.nixos.systemPathJSON;
|
||||
};
|
||||
factorio = depot.ops.factorio;
|
||||
};
|
||||
x86_64-darwin = {
|
||||
home-manager = depot.ops.home-manager-ext.built;
|
||||
|
|
|
@ -7,6 +7,7 @@ args: {
|
|||
maint = import ./maint args;
|
||||
secrets = import ./secrets args;
|
||||
raritan = import ./raritan args;
|
||||
factorio = import ./factorio args;
|
||||
|
||||
home-manager-ext = import ./home-manager-ext.nix args;
|
||||
}
|
||||
|
|
7
ops/factorio/default.nix
Normal file
7
ops/factorio/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
args: {
|
||||
multiworld = import ./multiworld args;
|
||||
}
|
1
ops/factorio/multiworld/.lua-format
Normal file
1
ops/factorio/multiworld/.lua-format
Normal file
|
@ -0,0 +1 @@
|
|||
keep_simple_control_block_one_line: false
|
61
ops/factorio/multiworld/README.md
Normal file
61
ops/factorio/multiworld/README.md
Normal file
|
@ -0,0 +1,61 @@
|
|||
# Factorio Multiworld
|
||||
|
||||
This is a very simple hack to allow Factorio vanilla servers to run with
|
||||
multiple-world support.
|
||||
|
||||
It enables the use of a `/ws` command which opens a GUI to switch between
|
||||
worlds. If you already know the name of the world, then `/ws <WORLDNAME>` can
|
||||
be used to quickly switch.
|
||||
|
||||
Different worlds have separate research progress, and players have separate
|
||||
inventories as they transition between worlds.
|
||||
|
||||
Worlds can be created from a map exchange string with the `/createworld`
|
||||
command by admins, and destroyed using `/destroyworld`.
|
||||
|
||||
## Commands
|
||||
|
||||
### `/ws` - world switcher
|
||||
|
||||
The `/ws` is the bulk of the multiworld functionality, and the only command accessible by non-admins.
|
||||
|
||||
It has two forms:
|
||||
|
||||
* `/ws` - opens a world selection GUI with a button per world
|
||||
* `/ws <WORLD NAME>` - goes directly to the world
|
||||
|
||||
### `/createworld <WORLD NAME> <MAP EXCHANGE STRING>` - create a new world (admins only)
|
||||
|
||||
`/createworld` is one way of creating a new, empty world based on a given map
|
||||
exchange string.
|
||||
|
||||
Note that only the parameters of the map exchange string that pertain to
|
||||
generation settings will be respected: difficulty, and other settings that
|
||||
Factorio retains per-map rather than per-surface will be ignored.
|
||||
|
||||
### `/destroyworld <WORLD NAME>` - destroy a world (admins only)
|
||||
|
||||
`/destroyworld` deletes a world by name, and deletes all the inventories
|
||||
associated with it.
|
||||
|
||||
### `/debugeditor` - open the editor (admins only)
|
||||
|
||||
`/debugeditor` opens the map editor without tripping the "editor opened"
|
||||
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.
|
||||
|
||||
### `/debugresetworldstate <WORLD NAME>` - reset book-keeping metadata for a world (admins only)
|
||||
|
||||
This resets various internal metadata:
|
||||
|
||||
* The per-world player force
|
||||
* Any inventories held for players for that world
|
||||
* Any locations held for players for that world
|
||||
|
||||
It should usually be used only after importing an existing save file as a new
|
||||
surface using the `/debugeditor`.
|
40
ops/factorio/multiworld/default.nix
Normal file
40
ops/factorio/multiworld/default.nix
Normal file
|
@ -0,0 +1,40 @@
|
|||
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
python3Env = pkgs.python3.withPackages (pm: with pm; [
|
||||
absl-py
|
||||
]);
|
||||
|
||||
src = pkgs.runCommand "factorio-multiworld-infect-src" {
|
||||
nativeBuildInputs = [
|
||||
(pkgs.python3.withPackages (pm: with pm; [
|
||||
black
|
||||
]))
|
||||
pkgs.luaformatter
|
||||
];
|
||||
src = ./.;
|
||||
} ''
|
||||
mkdir $out
|
||||
cp $src/infect.py $out/infect.py
|
||||
black --check $out/infect.py
|
||||
cp $src/multiworld.lua $out/multiworld.lua
|
||||
cp $src/.lua-format ./.
|
||||
lua-format --check $out/multiworld.lua
|
||||
'';
|
||||
in pkgs.runCommand "factorio-multiworld-infect" {
|
||||
python3 = "${python3Env}/bin/python";
|
||||
|
||||
infectPy = "${src}/infect.py";
|
||||
multiworldlua = "${src}/multiworld.lua";
|
||||
} ''
|
||||
mkdir -p $out/bin
|
||||
|
||||
substitute $infectPy $out/bin/factorio-multiworld-infect \
|
||||
--subst-var python3 \
|
||||
--subst-var multiworldlua
|
||||
chmod +x $out/bin/factorio-multiworld-infect
|
||||
''
|
141
ops/factorio/multiworld/infect.py
Executable file
141
ops/factorio/multiworld/infect.py
Executable file
|
@ -0,0 +1,141 @@
|
|||
#!@python3@
|
||||
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
USAGE: %s [flags] <zip to patch>
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
from absl import app
|
||||
from absl import flags
|
||||
from absl import logging
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
flags.DEFINE_boolean(
|
||||
"backup",
|
||||
True,
|
||||
"Back up world before modifying it to file with .pre-multiworld-infect suffix",
|
||||
)
|
||||
flags.DEFINE_boolean(
|
||||
"dry_run",
|
||||
False,
|
||||
"Don't actually overwrite the input file with the output",
|
||||
)
|
||||
flags.DEFINE_boolean(
|
||||
"uninfect",
|
||||
False,
|
||||
"Removes the multiworld Lua rather than injecting it; note that this won't remove any of the other persisted save elements, and may leave players stranded on other surfaces.",
|
||||
)
|
||||
|
||||
_CONTROL_LUA_INFECT_LINE = b'handler.add_lib(require("multiworld"))'
|
||||
|
||||
|
||||
def for_each_in_zip(in_zip, out_zip, callback, skip_files=None):
|
||||
skip_files = skip_files or frozenset()
|
||||
ret = None
|
||||
for zi in in_zip.infolist():
|
||||
file_basename = pathlib.PurePosixPath(zi.filename).name
|
||||
if file_basename in skip_files:
|
||||
logging.info("Found %s at %s: skipping", file_basename, zi.filename)
|
||||
continue
|
||||
with in_zip.open(zi, "r") as fsrc:
|
||||
zi.header_offset = None
|
||||
with out_zip.open(zi, "w") as fdst:
|
||||
cbret = callback(zi, fsrc, fdst)
|
||||
ret = ret or cbret
|
||||
return ret
|
||||
|
||||
|
||||
def handle_control_lua(fsrc, fdst):
|
||||
# Look to see if we've already infected it...
|
||||
buf = fsrc.read()
|
||||
lines = buf.split(b"\n")
|
||||
if FLAGS.uninfect:
|
||||
if _CONTROL_LUA_INFECT_LINE in lines:
|
||||
logging.info("Removing infection line from control.lua")
|
||||
lines.remove(_CONTROL_LUA_INFECT_LINE)
|
||||
else:
|
||||
logging.info("control.lua not infected, carrying on")
|
||||
else:
|
||||
if _CONTROL_LUA_INFECT_LINE not in lines:
|
||||
logging.info("control.lua not yet infected, adding our line...")
|
||||
last_handler_add_lib = None
|
||||
for n, line in enumerate(lines):
|
||||
if line.startswith(b"handler.add_lib("):
|
||||
last_handler_add_lib = n
|
||||
if last_handler_add_lib is None:
|
||||
raise ValueError("Can't find handler.add_lib( lines in control.lua!")
|
||||
lines.insert(last_handler_add_lib + 1, _CONTROL_LUA_INFECT_LINE)
|
||||
else:
|
||||
logging.info("control.lua already infected, carrying on")
|
||||
|
||||
for line in lines:
|
||||
fdst.write(line + b"\n")
|
||||
|
||||
|
||||
def handle_file(zi, fsrc, fdst):
|
||||
filepath = pathlib.PurePosixPath(zi.filename)
|
||||
if filepath.name == "control.lua":
|
||||
logging.info("Handling control.lua at %s", filepath)
|
||||
handle_control_lua(fsrc, fdst)
|
||||
return filepath.parent
|
||||
|
||||
logging.info("Copying %s", filepath)
|
||||
shutil.copyfileobj(fsrc, fdst, 1024 * 8)
|
||||
return None
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 2:
|
||||
raise app.UsageError("Need exactly one argument")
|
||||
|
||||
file_path = pathlib.Path(argv[1])
|
||||
|
||||
if FLAGS.backup:
|
||||
backup_path = file_path.with_name(f"{file_path.name}.pre-multiworld-infect")
|
||||
logging.info("Backing up %s to %s", file_path, backup_path)
|
||||
shutil.copy(file_path, backup_path)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpd:
|
||||
tmpd_path = pathlib.Path(tmpd)
|
||||
tmp_file_path = tmpd_path / file_path.name
|
||||
with zipfile.ZipFile(
|
||||
tmp_file_path, "w", compression=zipfile.ZIP_DEFLATED
|
||||
) as out_zip, zipfile.ZipFile(file_path, "r") as in_zip:
|
||||
parent = for_each_in_zip(
|
||||
in_zip, out_zip, handle_file, skip_files={"multiworld.lua"}
|
||||
)
|
||||
|
||||
if not FLAGS.uninfect:
|
||||
# Add our multiworld.lua.
|
||||
arcname = parent / "multiworld.lua"
|
||||
logging.info(
|
||||
"Adding multiworld.lua from %s to %s", "@multiworldlua@", arcname
|
||||
)
|
||||
zi = zipfile.ZipInfo.from_file(
|
||||
filename="@multiworldlua@",
|
||||
arcname=parent / "multiworld.lua",
|
||||
strict_timestamps=False,
|
||||
)
|
||||
with open("@multiworldlua@", "rb") as src, out_zip.open(
|
||||
zi, "w"
|
||||
) as dest:
|
||||
shutil.copyfileobj(src, dest, 1024 * 8)
|
||||
if FLAGS.dry_run:
|
||||
logging.warning(
|
||||
"In dry-run mode: not overwriting output file %s with %s",
|
||||
file_path,
|
||||
tmp_file_path,
|
||||
)
|
||||
else:
|
||||
shutil.move(tmp_file_path, file_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(main)
|
578
ops/factorio/multiworld/multiworld.lua
Normal file
578
ops/factorio/multiworld/multiworld.lua
Normal file
|
@ -0,0 +1,578 @@
|
|||
-- 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
|
||||
|
||||
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 <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 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)
|
||||
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
|
Loading…
Reference in a new issue