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;
|
twitternuke = depot.go.twitternuke;
|
||||||
systemPathJSON = depot.ops.nixos.systemPathJSON;
|
systemPathJSON = depot.ops.nixos.systemPathJSON;
|
||||||
};
|
};
|
||||||
|
factorio = depot.ops.factorio;
|
||||||
};
|
};
|
||||||
x86_64-darwin = {
|
x86_64-darwin = {
|
||||||
home-manager = depot.ops.home-manager-ext.built;
|
home-manager = depot.ops.home-manager-ext.built;
|
||||||
|
|
|
@ -7,6 +7,7 @@ args: {
|
||||||
maint = import ./maint args;
|
maint = import ./maint args;
|
||||||
secrets = import ./secrets args;
|
secrets = import ./secrets args;
|
||||||
raritan = import ./raritan args;
|
raritan = import ./raritan args;
|
||||||
|
factorio = import ./factorio args;
|
||||||
|
|
||||||
home-manager-ext = import ./home-manager-ext.nix 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