From 8ef576af6aa6a2c3514c97659eedee506d80b83b Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 26 Jul 2022 17:09:30 +0000 Subject: [PATCH] Add lobby server functionality --- .gitattributes | 1 + info.js | 17 +- instance.js | 18 +- master.js | 69 ++- module/flib/LICENSE | 21 + module/flib/README.md | 3 + module/flib/gui.lua | 522 +++++++++++++++++++++++ module/gridworld.lua | 35 +- module/lobby.lua | 18 + module/lobby/gui.lua | 11 + module/lobby/gui/draw_welcome.lua | 137 ++++++ module/lobby/gui/events/on_gui_click.lua | 9 + module/lobby/register_lobby_server.lua | 8 + module/lobby/register_map_data.lua | 9 + module/lobby/worldgen/create_spawn.lua | 9 + 15 files changed, 866 insertions(+), 21 deletions(-) create mode 100644 .gitattributes create mode 100644 module/flib/LICENSE create mode 100644 module/flib/README.md create mode 100644 module/flib/gui.lua create mode 100644 module/lobby.lua create mode 100644 module/lobby/gui.lua create mode 100644 module/lobby/gui/draw_welcome.lua create mode 100644 module/lobby/gui/events/on_gui_click.lua create mode 100644 module/lobby/register_lobby_server.lua create mode 100644 module/lobby/register_map_data.lua create mode 100644 module/lobby/worldgen/create_spawn.lua diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e5111b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +module/flib/* linguist-vendored diff --git a/info.js b/info.js index 3bc2066..c89b2c2 100644 --- a/info.js +++ b/info.js @@ -66,6 +66,20 @@ InstanceConfigGroup.define({ type: "number", initial_value: 512, }); +InstanceConfigGroup.define({ + name: "is_lobby_server", + title: "Server is a lobby server", + description: "Make this instance act as a lobby server for a gridworld", + type: "boolean", + initial_value: false, +}); +InstanceConfigGroup.define({ + name: "grid_id", + title: "Grid ID", + description: "Grid identifier - used to run multiple gridworlds on the same cluster", + type: "number", + initial_value: 0, +}); InstanceConfigGroup.finalize(); libUsers.definePermission({ @@ -121,8 +135,9 @@ module.exports = { }), getMapData: new libLink.Request({ type: "gridworld:get_map_data", - links: ["control-master"], + links: ["control-master", "instance-slave", "slave-master"], permission: "gridworld.overview.view", + forwardTo: "master", responseProperties: { map_data: { type: "array", diff --git a/instance.js b/instance.js index 41e8a6d..4484eab 100644 --- a/instance.js +++ b/instance.js @@ -35,8 +35,15 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin { world_x: this.instance.config.get("gridworld.grid_x_position"), world_y: this.instance.config.get("gridworld.grid_y_position"), }; - await this.sendRcon(`/c gridworld.create_world_limit("${data.x_size}","${data.y_size}","${data.world_x}","${data.world_y}", false)`, true); - await this.sendRcon(`/c gridworld.create_spawn("${data.x_size}","${data.y_size}","${data.world_x}","${data.world_y}", false)`, true); + if (this.instance.config.get("gridworld.is_lobby_server")) { + await this.sendRcon("/sc gridworld.register_lobby_server(true)"); + // Get gridworld data + const { map_data } = await this.info.messages.getMapData.send(this.instance); + await this.sendRcon(`/sc gridworld.register_map_data('${JSON.stringify(map_data)}')`); + } else { + await this.sendRcon(`/sc gridworld.create_world_limit("${data.x_size}","${data.y_size}","${data.world_x}","${data.world_y}", false)`, true); + await this.sendRcon(`/sc gridworld.create_spawn("${data.x_size}","${data.y_size}","${data.world_x}","${data.world_y}", false)`, true); + } } async onStop() { @@ -59,13 +66,6 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin { onMasterConnectionEvent(event) { if (event === "connect") { this.disconnecting = false; - (async () => { - if (this.disconnecting) { - - } - })().catch( - err => this.logger.error(`Unexpected error:\n${err.stack}`) - ); } } diff --git a/master.js b/master.js index ffbbc91..39d955e 100644 --- a/master.js +++ b/master.js @@ -195,6 +195,7 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { let instances = []; if (!message.data.use_edge_transports) { return; } + const lobby_server = await this.createLobbyServer(message.data.slave); try { for (let x = 1; x <= message.data.x_count; x++) { for (let y = 1; y <= message.data.y_count; y++) { @@ -205,7 +206,8 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { x, y, message.data.x_size, - message.data.y_size + message.data.y_size, + lobby_server.config.get("gridworld.grid_id"), ), x, y, @@ -263,7 +265,66 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { } } - async createInstance(name, x, y, x_size, y_size) { + async createLobbyServer(slaveId) { + // Create instance + this.logger.info("Creating lobby server"); + const name = "Gridworld lobby server"; + let instanceConfig = new libConfig.InstanceConfig("master"); + await instanceConfig.init(); + instanceConfig.set("instance.name", name); + instanceConfig.set("instance.auto_start", true); + instanceConfig.set("gridworld.is_lobby_server", true); + instanceConfig.set("gridworld.grid_id", Math.ceil(Math.random()*1000)); + + let instanceId = instanceConfig.get("instance.id"); + if (this.master.instances.has(instanceId)) { + throw new libErrors.RequestError(`Instance with ID ${instanceId} already exists`); + } + + // Add common settings for the Factorio server + let settings = { + ...instanceConfig.get("factorio.settings"), + + "name": `${this.master.config.get("master.name")} - ${name}`, + "description": `Clusterio instance for ${this.master.config.get("master.name")}`, + "tags": ["clusterio", "gridworld"], + "max_players": 0, + "visibility": { "public": true, "lan": true }, + "username": "", + "token": "", + "game_password": "", + "require_user_verification": true, + "max_upload_in_kilobytes_per_second": 0, + "max_upload_slots": 5, + "ignore_player_limit_for_returning_players": false, + "allow_commands": "admins-only", + "autosave_interval": 10, + "autosave_slots": 1, + "afk_autokick_interval": 0, + "auto_pause": false, + "only_admins_can_pause_the_game": true, + "autosave_only_on_server": true, + }; + instanceConfig.set("factorio.settings", settings); + + let instance = { config: instanceConfig, status: "unassigned" }; + this.master.instances.set(instanceId, instance); + await libPlugin.invokeHook(this.master.plugins, "onInstanceStatusChanged", instance, null); + this.master.addInstanceHooks(instance); + const instance_id = instanceConfig.get("instance.id"); + // Assign instance to a slave (using first slave as a placeholder) + await this.assignInstance(instance_id, instance.slaveId); + + // Create map + await this.createSave( + instance_id, + this.master.config.get("gridworld.gridworld_seed"), + this.master.config.get("gridworld.gridworld_map_exchange_string") + ); + return instance; + } + + async createInstance(name, x, y, x_size, y_size, grid_id) { this.logger.info("Creating instance", name); let instanceConfig = new libConfig.InstanceConfig("master"); await instanceConfig.init(); @@ -280,6 +341,8 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { // Add common settings for the Factorio server let settings = { + ...instanceConfig.get("factorio.settings"), + "name": `${this.master.config.get("master.name")} - ${name}`, "description": `Clusterio instance for ${this.master.config.get("master.name")}`, "tags": ["clusterio"], @@ -299,8 +362,6 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { "auto_pause": false, "only_admins_can_pause_the_game": true, "autosave_only_on_server": true, - - ...instanceConfig.get("factorio.settings"), }; instanceConfig.set("factorio.settings", settings); diff --git a/module/flib/LICENSE b/module/flib/LICENSE new file mode 100644 index 0000000..ba4cee1 --- /dev/null +++ b/module/flib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 raiguard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/module/flib/README.md b/module/flib/README.md new file mode 100644 index 0000000..1c70fc8 --- /dev/null +++ b/module/flib/README.md @@ -0,0 +1,3 @@ +# Fork of Factorio Library + +This is a GUI only fork of Raiguards [flib](https://github.com/factoriolib/flib). diff --git a/module/flib/gui.lua b/module/flib/gui.lua new file mode 100644 index 0000000..1bce30c --- /dev/null +++ b/module/flib/gui.lua @@ -0,0 +1,522 @@ +local mod_name = script.mod_name +local gui_event_defines = {} + +local event_id_to_string_mapping = {} +for name, id in pairs(defines.events) do + if string.find(name, "^on_gui") then + gui_event_defines[name] = id + event_id_to_string_mapping[id] = string.gsub(name, "^on_gui", "on") + end +end + +--- GUI structuring tools and event handling. +local flib_gui = {} + +--- Provide a callback to be run for GUI events. +--- +--- # Examples +--- +--- ```lua +--- gui.hook_events(function(e) +--- local msg = gui.read_action(e) +--- if msg then +--- -- read the action to determine what to do +--- end +--- end) +--- ``` +--- @param callback function +function flib_gui.hook_events(callback) + local on_event = script.on_event + for _, id in pairs(gui_event_defines) do + on_event(id, callback) + end +end + +--- Retrieve the action message from a GUI element's tags. +--- +--- # Examples +--- +--- ```lua +--- event.on_gui_click(function(e) +--- local action = gui.read_action(e) +--- if action then +--- -- do stuff +--- end +--- end) +--- ``` +--- @param event_data EventData +--- @return any? action The element's action for this GUI event. +function flib_gui.read_action(event_data) + local elem = event_data.element + if not elem or not elem.valid then + return + end + + local mod_tags = elem.tags[mod_name] + if not mod_tags then + return + end + + local elem_actions = mod_tags.flib + if not elem_actions then + return + end + + local event_name = event_id_to_string_mapping[event_data.name] + local msg = elem_actions[event_name] + + return msg +end + +--- Navigate a structure to build a GUI +--- @param parent LuaGuiElement +--- @param structure GuiBuildStructure +--- @param refs table +local function recursive_build(parent, structure, refs) + -- If the structure has no type, just ignore it + -- This is to make it possible to pass unit types `{}` to represent "no element" without breaking things + if not structure.type then + return + end + + -- Prepare tags + local original_tags = structure.tags + local tags = original_tags or {} + local actions = structure.actions + local tags_flib = tags.flib + tags.flib = actions + structure.tags = { [mod_name] = tags } + + -- Make the game not convert these into a property tree for no reason + structure.actions = nil + -- Substructures can be defined in special tables or as the array portion of this structure + local substructures + local substructures_len = #structure + if substructures_len > 0 then + if structure.children or structure.tabs then + error("Children or tab-and-content pairs must ALL be in the array portion, or a subtable. Not both at once!") + end + substructures = {} + for i = 1, substructures_len do + substructures[i] = structure[i] + structure[i] = nil + end + else + substructures = structure.children or structure.tabs + structure.children = nil + structure.tabs = nil + end + + -- Create element + local elem = parent.add(structure) + + -- Restore structure + structure.tags = original_tags + structure.actions = actions + tags.flib = tags_flib + + local style_mods = structure.style_mods + if style_mods then + for k, v in pairs(style_mods) do + elem.style[k] = v + end + end + + local elem_mods = structure.elem_mods + if elem_mods then + for k, v in pairs(elem_mods) do + elem[k] = v + end + end + + local ref = structure.ref + if ref then + -- Recursively create tables as needed + local prev = refs + local ref_length = #ref + for i = 1, ref_length - 1 do + local current_key = ref[i] + local current = prev[current_key] + if not current then + current = {} + prev[current_key] = current + end + prev = current + end + prev[ref[ref_length]] = elem + end + + -- Substructures + if substructures then + if structure.type == "tabbed-pane" then + local add_tab = elem.add_tab + for i = 1, #substructures do + local tab_and_content = substructures[i] + if not (tab_and_content.tab and tab_and_content.content) then + error("TabAndContent must have `tab` and `content` fields") + end + local tab = recursive_build(elem, tab_and_content.tab, refs) + local content = recursive_build(elem, tab_and_content.content, refs) + add_tab(tab, content) + end + else + for i = 1, #substructures do + recursive_build(elem, substructures[i], refs) + end + end + end + + return elem +end + +--- Build a GUI based on the given structure(s). +--- @param parent LuaGuiElement The parent GUI element where the new GUI will be located. +--- @param structures GuiBuildStructure[] The GUIs to build. +--- @return table refs `LuaGuiElement` references and subtables, built based on the values of `ref` throughout the `GuiBuildStructure`. +function flib_gui.build(parent, structures) + local refs = {} + for i = 1, #structures do + recursive_build(parent, structures[i], refs) + end + return refs +end + +--- Build a single element based on a GuiStructure. +--- +--- This is to allow use of `style_mods`, `actions` and `tags` without needing to use `gui.build()` for a single element. +--- +--- Unlike `gui.build()`, the element will be automatically returned from the function without needing to use `ref`. If +--- you need to obtain references to children of this element, use `gui.build()` instead. +--- @param parent LuaGuiElement The parent GUI element where this new element will be located. +--- @param structure GuiBuildStructure The element to build. +--- @return LuaGuiElement elem A reference to the element that was created. +function flib_gui.add(parent, structure) + -- Just in case they had a ref in the structure already, extract it + local previous_ref = structure.ref + -- Put in a known ref that we can use later + structure.ref = { "FLIB_ADD_ROOT" } + -- Build the element + local refs = {} + recursive_build(parent, structure, refs) + -- Restore the previous ref + structure.ref = previous_ref + -- Return the element + return refs.FLIB_ADD_ROOT +end + +--- @param elem LuaGuiElement +--- @param updates GuiUpdateStructure +local function recursive_update(elem, updates) + if updates.cb then + updates.cb(elem) + end + + if updates.style then + elem.style = updates.style + end + + if updates.style_mods then + for key, value in pairs(updates.style_mods) do + elem.style[key] = value + end + end + + if updates.elem_mods then + for key, value in pairs(updates.elem_mods) do + elem[key] = value + end + end + + if updates.tags then + flib_gui.update_tags(elem, updates.tags) + end + + -- TODO: This could be a lot better + if updates.actions then + for event_name, payload in pairs(updates.actions) do + flib_gui.set_action(elem, event_name, payload) + end + end + + local substructures + local substructures_len = #updates + if substructures_len > 0 then + if updates.children or updates.tabs then + error("Children or tab-and-content pairs must ALL be in the array portion, or a subtable. Not both at once!") + end + substructures = {} + for i = 1, substructures_len do + substructures[i] = updates[i] + updates[i] = nil + end + else + substructures = updates.children or updates.tabs + updates.children = nil + updates.tabs = nil + end + local subelements + if elem.type == "tabbed-pane" then + subelements = elem.tabs + else + subelements = elem.children + end + + if substructures then + for i, substructure in pairs(substructures) do + if substructure.tab or substructure.content then + local elem_tab_and_content = subelements[i] + if elem_tab_and_content then + local tab = elem_tab_and_content.tab + local tab_updates = substructures.tab + if tab and tab_updates then + recursive_update(tab, tab_updates) + end + local content = elem_tab_and_content.content + local content_updates = substructures.content + if content and content_updates then + recursive_update(content, content_updates) + end + end + elseif subelements[i] then + recursive_update(subelements[i], substructure) + end + end + end +end + +--- Update an existing GUI based on a given structure. +--- @param elem LuaGuiElement The element to update. +--- @param updates GuiUpdateStructure The updates to perform. +function flib_gui.update(elem, updates) + recursive_update(elem, updates) +end + +--- Retrieve a GUI element's tags. +--- +--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts. +--- +--- If no tags exist, this function will return an empty table. +--- @param elem LuaGuiElement +--- @return table +function flib_gui.get_tags(elem) + return elem.tags[mod_name] or {} +end + +--- Set (override) a GUI element's tags. +--- +--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts. +--- @param elem LuaGuiElement +--- @param tags table +function flib_gui.set_tags(elem, tags) + local elem_tags = elem.tags + elem_tags[mod_name] = tags + elem.tags = elem_tags +end + +--- Delete a GUI element's tags. +--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts. +--- +--- @param elem LuaGuiElement +function flib_gui.delete_tags(elem) + local elem_tags = elem.tags + elem_tags[mod_name] = nil + elem.tags = elem_tags +end + +--- Perform a shallow merge on a GUI element's tags. +--- +--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts. +--- +--- Only the top level will be updated. If deep updating is needed, use `gui.get_tags` and `table.deep_merge`, then +--- `gui.set_tags`. +--- @param elem LuaGuiElement +--- @param updates table +function flib_gui.update_tags(elem, updates) + local elem_tags = elem.tags + local existing = elem_tags[mod_name] + + if not existing then + existing = {} + elem_tags[mod_name] = existing + end + + for k, v in pairs(updates) do + existing[k] = v + end + + elem.tags = elem_tags +end + +--- Set (overwrite) the specified action message for this GUI element. +--- @param elem LuaGuiElement +--- @param event_name string The GUI event name for this action, with the `_gui` portion omitted (i.e. `on_click`). +--- @param msg any? The action message, or `nil` to clear the action. +function flib_gui.set_action(elem, event_name, msg) + local elem_tags = elem.tags + local existing = elem_tags[mod_name] + + if not existing then + existing = {} + elem_tags[mod_name] = existing + end + + local actions = existing.flib + if not actions then + actions = {} + existing.flib = actions + end + + actions[event_name] = msg or nil + + elem.tags = elem_tags +end + +--- Retrieve the specified action message for this GUI element. +--- @param elem LuaGuiElement +--- @param event_name string The GUI event name to get the action message for, with the `_gui` portion omitted (i.e. `on_click`). +--- @return any|nil msg The action message, if there is one. +function flib_gui.get_action(elem, event_name) + local elem_tags = elem.tags + local existing = elem_tags[mod_name] + + if not existing then + return + end + + local actions = existing.flib + if not actions then + return + end + + return actions[event_name] +end + +--- A series of nested tables used to build a GUI. +--- +--- This is an extension of `LuaGuiElement`, providing new features and options. +--- +--- This inherits all required properties from its base `LuaGuiElement`, i.e. if the `type` field is +--- `sprite-button`, the `GuiBuildStructure` must contain all the fields that a `sprite-button` `LuaGuiElement` +--- requires. +--- +--- There are a number of new fields that can be applied to a `GuiBuildStructure` depending on the type. +--- +--- # Example +--- +--- ```lua +--- gui.build(player.gui.screen, { +--- { +--- type = "frame", +--- direction = "vertical", +--- ref = {"window"}, +--- actions = { +--- on_closed = {gui = "demo", action = "close"} +--- }, +--- -- Titlebar +--- {type = "flow", ref = {"titlebar", "flow"}, +--- {type = "label", style = "frame_title", caption = "Menu", ignored_by_interaction = true}, +--- {type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true}, +--- { +--- type = "sprite-button", +--- style = "frame_action_button", +--- sprite = "utility/close_white", +--- hovered_sprite = "utility/close_black", +--- clicked_sprite = "utility/close_black", +--- ref = {"titlebar", "close_button"}, +--- actions = { +--- on_click = {gui = "demo", action = "close"} +--- } +--- } +--- }, +--- -- Content +--- {type = "frame", style = "inside_deep_frame_for_tabs", +--- {type = "tabbed-pane", +--- { +--- tab = {type = "tab", caption = "1"}, +--- content = {type = "table", style = "slot_table", column_count = 10, ref = {"tables", 1}} +--- }, +--- { +--- tab = {type = "tab", caption = "2"}, +--- content = {type = "table", style = "slot_table", column_count = 10, ref = {"tables", 2}} +--- } +--- } +--- } +--- } +--- }) +--- ``` +--- @class GuiBuildStructure +--- @field style_mods? table +--- @field elem_mods? table +--- @field tags? table +--- @field actions? GuiElementActions +--- @field ref? string[] +--- @field children? GuiBuildStructure[] +--- @field tabs? TabAndContent[] + +---- A series of nested tables used to update a GUI. +--- +--- # Examples +--- +--- ```lua +--- gui.update( +--- my_frame, +--- { +--- elem_mods = {caption = "Hello there!"}, +--- tags = {subject = "General Kenobi"}, +--- actions = {on_click = "everybody_say_hey"}, +--- { +--- { +--- {tab = {elem_mods = {badge_text = "69"}}, content = {...}}, +--- {content = {...}} +--- } +--- } +--- } +--- ) +--- ``` +--- @class GuiUpdateStructure +--- @field cb? function A callback to run on this GUI element. The callback will be passed a `LuaGuiElement` as its first parameter. +--- @field style? string The new style that the element should use. +--- @field style_mods? table A key -> value dictionary defining modifications to make to the element's style. Available properties are listed in `LuaStyle`. +--- @field elem_mods? table A key –> value dictionary defining modifications to make to the element. Available properties are listed in LuaGuiElement. +--- @field tags? table Tags that should be added to the element. This is identical to calling `gui.update_tags` on the element. +--- @field actions? table Actions that should be added to the element. The format is identical to `actions` in a `GuiBuildStructure`. This is identical to calling `set_action` for each action on this element. +--- @field children? GuiUpdateStructure[] `GuiUpdateStructure`s to apply to the children of this `LuaGuiElement`. This may alternatively be defined in the array portion of the parent structure to improve readability. +--- @field tabs? TabAndContent[] `TabAndContent`s to apply to the tabs of this `LuaGuiElement`. This may alternatively be defined in the array portion of the parent structure to improve readability. + +--- A mapping of GUI event name -> action message. +--- +--- Each key is a GUI event name (`on_gui_click`, `on_gui_elem_changed`, etc.) with the `_gui` part removed. For example, `on_gui_click` will become `on_click`. +--- +--- Each value is a custom set of data that `gui.read_action` will return when that GUI event is fired and passes +--- this GUI element. This data may be of any type, as long as it is truthy. +--- +--- Actions are kept under a `flib` subtable in the element's mod-specific tags subtable, retrievable with +--- `gui.get_tags`. Because of this, there is no chance of accidental mod action overlaps, so feel free to use +--- generic actions such as "close" or "open". +--- +--- A common format for a mod with multiple GUIs might be to give each GUI a name, and write the actions as shown below. +--- +--- # Example +--- +--- ```lua +--- gui.build(player.gui.screen, { +--- { +--- type = "frame", +--- caption = "My frame", +--- actions = { +--- on_click = {gui = "my_gui", action = "handle_click"}, +--- on_closed = {gui = "my_gui", action = "close"} +--- } +--- } +--- }) +--- ``` +--- @class GuiElementActions + +--- A table representing a tab <-> content pair. +--- +--- When used in `gui.build`, both fields are required. When used in `gui.update`, both fields are optional. +--- @class TabAndContent +--- @field tab GuiBuildStructure|GuiUpdateStructure +--- @field content GuiBuildStructure|GuiUpdateStructure + +return flib_gui diff --git a/module/gridworld.lua b/module/gridworld.lua index abe48a8..7bf4eab 100644 --- a/module/gridworld.lua +++ b/module/gridworld.lua @@ -6,6 +6,10 @@ When a player enters a map, generate neighboring maps and connections ]] +gridworld = {} + +-- flib.gui +local gui = require("modules/gridworld/flib/gui") local clusterio_api = require("modules/clusterio/api") local out_of_bounds = require("modules/gridworld/util/out_of_bounds") local edge_teleport = require("modules/gridworld/edge_teleport") @@ -14,8 +18,8 @@ local create_world_limit = require("modules/gridworld/worldgen/create_world_limi local create_spawn = require("modules/gridworld/worldgen/create_spawn") local populate_neighbor_data = require("modules/gridworld/populate_neighbor_data") local map = require("modules/gridworld/map/map") +local lobby = require("modules/gridworld/lobby") -gridworld = {} -- Declare globals to make linter happy game = game global = global @@ -47,8 +51,13 @@ gridworld.events[defines.events.on_player_joined_game] = function(event) if global.gridworld.players[player.name] == nil then global.gridworld.players[player.name] = {} end - edge_teleport.receive_teleport(player) - player_tracking.send_player_position(player) + + if not global.gridworld.lobby_server then + edge_teleport.receive_teleport(player) + player_tracking.send_player_position(player) + else + lobby.gui.draw_welcome(player) + end end gridworld.events[defines.events.on_player_left_game] = function(event) local player = game.get_player(event.player_index) @@ -81,14 +90,24 @@ gridworld.events[defines.events.on_built_entity] = function(event) end end end +gridworld.events[defines.events.on_gui_click] = function(event) + local action = gui.read_action(event) + if action then + lobby.gui.on_gui_click(event, action) + end +end gridworld.on_nth_tick = {} gridworld.on_nth_tick[37] = function() - -- Periodically check players position for cross server teleport - edge_teleport.check_player_position() + if not global.gridworld.lobby_server then + -- Periodically check players position for cross server teleport + edge_teleport.check_player_position() + end end gridworld.on_nth_tick[121] = function() - -- Update player positions on the map - player_tracking.check_player_positions() + if not global.gridworld.lobby_server then + -- Update player positions on the map + player_tracking.check_player_positions() + end end -- Plugin API @@ -98,5 +117,7 @@ gridworld.populate_neighbor_data = populate_neighbor_data gridworld.receive_teleport_data = edge_teleport.receive_teleport_data gridworld.dump_mapview = map.dump_mapview gridworld.ask_for_teleport = edge_teleport.ask_for_teleport +gridworld.register_lobby_server = lobby.register_lobby_server +gridworld.register_map_data = lobby.register_map_data return gridworld diff --git a/module/lobby.lua b/module/lobby.lua new file mode 100644 index 0000000..e33ca35 --- /dev/null +++ b/module/lobby.lua @@ -0,0 +1,18 @@ +--[[ + The lobby server functionality is implemented here. + The lobby server is always online and has auto_start enabled by default. When players join the lobby server they will be presented with a GUI explaining how the server works and how to play on it. + A new player will be presented with a "New game" button. Upon pressing the button a new profile will be generated. + On the profile view you can see your current character, player statistics and your location on the grid. To start, press "Join server". The server will start and a dialog prompting to connect to the server will appear. The player can click "Confirm" or press E to connect to the server. +]] + +local register_lobby_server = require("modules/gridworld/lobby/register_lobby_server") +local register_map_data = require("modules/gridworld/lobby/register_map_data") +local gui = require("modules/gridworld/lobby/gui") + +gridworld.gui = gui + +return { + register_lobby_server = register_lobby_server, + register_map_data = register_map_data, + gui = gui, +} diff --git a/module/lobby/gui.lua b/module/lobby/gui.lua new file mode 100644 index 0000000..ec9e60b --- /dev/null +++ b/module/lobby/gui.lua @@ -0,0 +1,11 @@ +--[[ + GUI handlers for the lobby server. +]] + +local draw_welcome = require("modules/gridworld/lobby/gui/draw_welcome") +local on_gui_click = require("modules/gridworld/lobby/gui/events/on_gui_click") + +return { + draw_welcome = draw_welcome, + on_gui_click = on_gui_click, +} diff --git a/module/lobby/gui/draw_welcome.lua b/module/lobby/gui/draw_welcome.lua new file mode 100644 index 0000000..387e239 --- /dev/null +++ b/module/lobby/gui/draw_welcome.lua @@ -0,0 +1,137 @@ +local gui = require("modules/gridworld/flib/gui") + +local function draw_welcome(player) + if player == nil then player = game.player end + player.gui.center.clear() + gui.build(player.gui.center, { + { + type = "frame", + direction = "vertical", + ref = {"window"}, + -- Header + { + type = "flow", ref = {"titlebar", "flow"}, + { + type = "label", style = "frame_title", caption = "Menu", ignored_by_interaction = true, + }, + { + type = "empty-widget", style = "draggable_space_header", ignored_by_interaction = true, + }, + }, + -- Content + { + type = "flow", + { + type = "tabbed-pane", + style_mods = { + maximal_width = 750, + }, + -- Welcome + { + tab = { type = "tab", caption = "Welcome" }, + content = { + type = "scroll-pane", + direction = "vertical", + style_mods = { + height = 200, + }, + { + type = "label", + caption = "Welcome to the Gridworld lobby server!", + style = "heading_1_label", + }, + { + type = "label", + caption = "This server is used to host a game of Gridworld. You can join the server by pressing the \"Join server\" button below. You can also create a new game by pressing the \"New game\" button below. Starting a new game will clear your inventory and reset your spawnpoint.", + style_mods = { + single_line = false, + }, + }, + { + type = "flow", + style = "horizontal_flow", + { + type = "button", + caption = "Join server", + style = "green_button", + actions = { + on_click = { + action = "join_latest_server", + }, + }, + }, + { + type = "button", + caption = "New game", + style = "button", + actions = { + on_click = { + action = "open_new_game_dialog", + }, + }, + }, + }, + } + }, + -- About gridworld + { + tab = { type = "tab", caption = "About" }, + content = { + type = "scroll-pane", + direction = "vertical", + style_mods = { + height = 200, + }, + { + type = "label", + caption = "What is gridworld?", + style = "heading_1_label", + }, + { + type = "label", + caption = "Gridworld is an infinitely scaleable factorio MMO concept. Using clusterio it divides a single factorio world into multiple limited size servers. Each server simulates a limited part of the game world. When a player moves to the edge of one world they will be prompted to join the next server. This allows us to work around the scaling limitations of the factorio engine.", + style_mods = { + single_line = false, + }, + }, + { + type = "label", + caption = "How do I join a server?", + style = "heading_1_label", + }, + { + type = "label", + caption = "To preserve player positions, it is recommended to join a gridworld cluster via a lobby server (such as this one). This will ensure that you are placed on the same server as your previous character.", + style_mods = { + single_line = false, + }, + }, + { + type = "label", + caption = "How do I host my own cluster?", + style = "heading_1_label", + }, + { + type = "label", + caption = "The source code and instructions for hosting your own cluster can be found on the github repository at https://github.com/Danielv123/gridworld", + style_mods = { + single_line = false, + }, + }, + { + type = "label", + caption = "Gridworld is a clusterio plugin, so it requires a working clusterio cluster. You can find the instructions for installing clusterio at https://github.com/clusterio/clusterio", + style_mods = { + single_line = false, + }, + }, + } + } + } + } + } + }) +end +-- /c game.player.gui.screen.clear() + +return draw_welcome diff --git a/module/lobby/gui/events/on_gui_click.lua b/module/lobby/gui/events/on_gui_click.lua new file mode 100644 index 0000000..52ab565 --- /dev/null +++ b/module/lobby/gui/events/on_gui_click.lua @@ -0,0 +1,9 @@ +local function on_gui_click(event, action) + local player = event.player + if player == nil then return end + if action.action == "join_latest_server" then + game.print("Joining server...") + end +end + +return on_gui_click diff --git a/module/lobby/register_lobby_server.lua b/module/lobby/register_lobby_server.lua new file mode 100644 index 0000000..55d5ae6 --- /dev/null +++ b/module/lobby/register_lobby_server.lua @@ -0,0 +1,8 @@ +local create_spawn = require("modules/gridworld/lobby/worldgen/create_spawn") + +local function register_lobby_server() + global.gridworld.lobby_server = true + create_spawn() +end + +return register_lobby_server diff --git a/module/lobby/register_map_data.lua b/module/lobby/register_map_data.lua new file mode 100644 index 0000000..4574fba --- /dev/null +++ b/module/lobby/register_map_data.lua @@ -0,0 +1,9 @@ +--[[ + Receive the current gridworld map state from RCON to visualize on the lobby server +]] +local function register_map_data(map_string) + global.gridworld.map_data = game.json_to_table(map_string) + log(global.gridworld.map_data) +end + +return register_map_data diff --git a/module/lobby/worldgen/create_spawn.lua b/module/lobby/worldgen/create_spawn.lua new file mode 100644 index 0000000..7b2c84b --- /dev/null +++ b/module/lobby/worldgen/create_spawn.lua @@ -0,0 +1,9 @@ +local generation_version = require("modules/gridworld/constants").generation_version + +local function create_spawn(force) + if not force and global.gridworld.world_limit_version >= generation_version then return end + + game.forces.player.set_spawn_position({x = 0, y = 0}, game.surfaces[1]) +end + +return create_spawn