From e63f1246da5b11ec657efd581f98220c27b1c865 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 29 Jul 2021 23:55:48 +0200 Subject: [PATCH] Offer the player a temporary inventory while waiting for master --- plugins/inventory_sync/instance.js | 3 + .../module/download_inventory.lua | 13 +++- .../module/gui/dialog_failed_download.lua | 60 +++++++++++++++++++ .../module/gui/handle_gui_events.lua | 23 +++++++ .../module/{ => gui}/progress_dialog.lua | 0 .../module/{ => gui}/welcome_new_player.lua | 0 .../inventory_sync/module/inventory_sync.lua | 46 +++++++++++++- .../module/script/clean_dirty_inventory.lua | 29 +++++++++ 8 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 plugins/inventory_sync/module/gui/dialog_failed_download.lua create mode 100644 plugins/inventory_sync/module/gui/handle_gui_events.lua rename plugins/inventory_sync/module/{ => gui}/progress_dialog.lua (100%) rename plugins/inventory_sync/module/{ => gui}/welcome_new_player.lua (100%) create mode 100644 plugins/inventory_sync/module/script/clean_dirty_inventory.lua diff --git a/plugins/inventory_sync/instance.js b/plugins/inventory_sync/instance.js index 450eba69..7dbc83ff 100644 --- a/plugins/inventory_sync/instance.js +++ b/plugins/inventory_sync/instance.js @@ -17,6 +17,7 @@ function chunkify(chunkSize, string) { class InstancePlugin extends libPlugin.BaseInstancePlugin { async init() { + // Handle IPC from scenario script this.instance.server.on("ipc-inventory_sync_upload", content => this.handleUpload(content) .catch(err => this.logger.error(`Error handling ipc-inventory_sync_upload:\n${err.stack}`))); this.instance.server.on("ipc-inventory_sync_download", content => this.handleDownload(content) @@ -35,6 +36,8 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin { async handleDownload(player) { this.logger.verbose(`Downloading ${player.player_name}`); + + // Retrieves inventory data. If the master is offline it waits until it comes back online. let response = await this.info.messages.download.send(this.instance, { player_name: player.player_name, }); diff --git a/plugins/inventory_sync/module/download_inventory.lua b/plugins/inventory_sync/module/download_inventory.lua index 310126b8..590b8156 100644 --- a/plugins/inventory_sync/module/download_inventory.lua +++ b/plugins/inventory_sync/module/download_inventory.lua @@ -1,4 +1,4 @@ -local progress_dialog = require("modules/inventory_sync/progress_dialog") +local progress_dialog = require("modules/inventory_sync/gui/progress_dialog") local serialize = require("modules/clusterio/serialize") local load_crafting_queue = require("modules/inventory_sync/load_crafting_queue") local inventories = require("modules/inventory_sync/define_player_inventories") @@ -27,7 +27,18 @@ function download_inventory(player_name, data, number, total) -- Request next segment rcon.print("Segment downloaded") + + -- Reset sync_start_tick to indicate master connection + global.inventory_sync.players[player_name].sync_start_tick = game.tick else + -- Stop tracking master connection status, sync is complete + global.inventory_sync.players[player_name].sync_start_tick = 0 + + -- Handle dirty inventory + if global.inventory_sync.players[player_name].dirty_inventory then + clean_dirty_inventory(player) + end + -- Recreate player character ensure_character(player) diff --git a/plugins/inventory_sync/module/gui/dialog_failed_download.lua b/plugins/inventory_sync/module/gui/dialog_failed_download.lua new file mode 100644 index 00000000..50074622 --- /dev/null +++ b/plugins/inventory_sync/module/gui/dialog_failed_download.lua @@ -0,0 +1,60 @@ +-- Show inventory download progressbar +function dialog_failed_download(player) + if player.gui.screen.dialog_failed_download then + player.gui.screen.dialog_failed_download.destroy() + end + + local frame = player.gui.screen.add { + name = "dialog_failed_download", + type = "frame", + direction = "vertical", + } + + local titlebar = frame.add { + type = "flow", + } + titlebar.drag_target = frame + + local title = titlebar.add { + type = "label", + style = "frame_title", + caption = "Inventory sync download failed", + } + title.drag_target = frame + + local filler = titlebar.add { + type = "empty-widget", + style = "draggable_space_header", + } + filler.style.horizontally_stretchable = "on" + filler.style.right_margin = 4 + filler.style.height = 24 + filler.drag_target = frame + + local content = frame.add { + name = "inventory_sync_failed_download_content", + type = "frame", + direction = "vertical", + style = "inside_shallow_frame_with_padding", + } + content.style.width = 400 + + local p1 = content.add { + type = "label", + caption = "The inventory download couldn't complete. You can choose to keep playing with an empty inventory until the connection is reestablished and the inventory download completes. At that point your temporary inventory will be put in a chest.", + } + p1.style.single_line = false + p1.style.bottom_margin = 8 + + local abort = content.add { + name = "inventory_sync_failed_download_abort", + type = "button", + caption = "Continue with new inventory", + tooltip = "Create a new empty temporary inventory. Items you pick up will be placed in a chest when the sync completes.", + } + + frame.force_auto_center() + player.opened = frame +end + +return dialog_failed_download diff --git a/plugins/inventory_sync/module/gui/handle_gui_events.lua b/plugins/inventory_sync/module/gui/handle_gui_events.lua new file mode 100644 index 00000000..259cc0a7 --- /dev/null +++ b/plugins/inventory_sync/module/gui/handle_gui_events.lua @@ -0,0 +1,23 @@ +local ensure_character = require("modules/inventory_sync/ensure_character") + +-- on_gui_click +local handle_gui_events = function(event) + local player = game.get_player(event.player_index) + local element = event.element + if element.name == "inventory_sync_failed_download_abort" then + -- Give the player a new inventory and mark it as dirty + -- Show dirty status and an explanation in a GUI + + -- Remove GUI element + player.gui.screen.dialog_failed_download.destroy() + -- Handle inventory dirtying + if not global.inventory_sync.players[player.name].dirty_inventory then + global.inventory_sync.players[player.name].dirty_inventory = true + + -- Restore player character + ensure_character(player) + end + end +end + +return handle_gui_events diff --git a/plugins/inventory_sync/module/progress_dialog.lua b/plugins/inventory_sync/module/gui/progress_dialog.lua similarity index 100% rename from plugins/inventory_sync/module/progress_dialog.lua rename to plugins/inventory_sync/module/gui/progress_dialog.lua diff --git a/plugins/inventory_sync/module/welcome_new_player.lua b/plugins/inventory_sync/module/gui/welcome_new_player.lua similarity index 100% rename from plugins/inventory_sync/module/welcome_new_player.lua rename to plugins/inventory_sync/module/gui/welcome_new_player.lua diff --git a/plugins/inventory_sync/module/inventory_sync.lua b/plugins/inventory_sync/module/inventory_sync.lua index 23c77f1c..7a7019d3 100644 --- a/plugins/inventory_sync/module/inventory_sync.lua +++ b/plugins/inventory_sync/module/inventory_sync.lua @@ -14,7 +14,10 @@ local clusterio_api = require("modules/clusterio/api") local add_commands = require("modules/inventory_sync/commands") local upload_inventory = require("modules/inventory_sync/upload_inventory") local download_inventory = require("modules/inventory_sync/download_inventory") -local welcome_new_player = require("modules/inventory_sync/welcome_new_player") +local welcome_new_player = require("modules/inventory_sync/gui/welcome_new_player") +local handle_gui_events = require("modules/inventory_sync/gui/handle_gui_events") +local dialog_failed_download = require("modules/inventory_sync/gui/dialog_failed_download") +local clean_dirty_inventory = require("modules/inventory_sync/script/clean_dirty_inventory") inventory_sync = {} inventory_sync.welcome_new_player = welcome_new_player @@ -33,6 +36,7 @@ inventory_sync.events[clusterio_api.events.on_server_startup] = function(event) saved_crafting_queue = {}, download_cache = {}, players_waiting_for_download = {}, + players = {}, } end end @@ -45,8 +49,20 @@ inventory_sync.events[defines.events.on_player_removed] = function(event) global.inventory_sync.download_start_tick[player.name] = nil -- Remove stored crafting queue global.inventory_sync.saved_crafting_queue[player.name] = nil + -- Remove other player data + global.inventory_sync.players[player.name] = nil end +inventory_sync.events[defines.events.on_player_created] = function(event) + local player = game.get_player(event.player_index) + global.inventory_sync.players[player.name] = { + dirty_inventory = false, -- Player has a temporary non-synced inventory that should be persisted + sync_start_tick = 0, -- To track download failure timeout + } +end + +inventory_sync.events[defines.events.on_gui_click] = handle_gui_events + -- Download inventory from master inventory_sync.events[defines.events.on_player_joined_game] = function(event) -- Add player to download queue. Inventory won't be downloaded before the player has a character @@ -57,7 +73,11 @@ end inventory_sync.on_nth_tick = {} inventory_sync.on_nth_tick[33] = function(event) + -- Periodically check if player has a character to download to. False in cutscenes. inventory_sync.check_player_character_before_download() + + -- Check if download has failed and we should create a dirty inventory + inventory_sync.check_inventory_download_failed() end -- Upload inventory when a player leaves the game. Triggers on restart after crash if player was online during crash. @@ -97,12 +117,36 @@ function inventory_sync.initiate_inventory_download(player) player_name = player.name }) + -- Handle dirty inventories. A dirty inventory is a temporary inventory that has been used while waiting + -- for sync. It should be taken care of in some way, like putting it in a corpse or a chest. + if global.inventory_sync.players[player.name].dirty_inventory then + clean_dirty_inventory(player) + end + -- Set player into ghost mode for duration of download local character = player.character player.set_controller { type = defines.controllers.ghost, } if character ~= nil then character.destroy() end + + -- Start timeout for detecting master connection failure + global.inventory_sync.players[player.name].sync_start_tick = game.tick +end +function inventory_sync.check_inventory_download_failed() + -- Used for letting the master report the inventory download failing, like when the master is offline or an + -- error occurs somewhere in the server code. The player will be returned to a playable state. + for name, data in pairs(global.inventory_sync.players) do + if data.sync_start_tick ~= 0 and data.sync_start_tick <= game.tick - 600 then + -- Stop refreshing timer + data.sync_start_tick = 0 + + local player = game.get_player(name) + player.print("Inventory download failed due to master connection") + -- Show GUI with option to retry or abort download + dialog_failed_download(player) + end + end end return inventory_sync diff --git a/plugins/inventory_sync/module/script/clean_dirty_inventory.lua b/plugins/inventory_sync/module/script/clean_dirty_inventory.lua new file mode 100644 index 00000000..9a7b7c72 --- /dev/null +++ b/plugins/inventory_sync/module/script/clean_dirty_inventory.lua @@ -0,0 +1,29 @@ +local inventories = require("modules/inventory_sync/define_player_inventories") + +--[[ + +Take a player with a dirty inventory, create chests with all the items, clear the player, +put them in ghost mode and mark the inventory as clean. + +]] + +function clean_dirty_inventory(player) + global.inventory_sync.players[player.name].dirty_inventory = false + + local surface = player.surface + local position = player.position + local character = player.character + player.disassociate_character(character) + character.die() + player.ticks_to_respawn = nil + player.teleport(position, surface) + + -- Remove player character + local character = player.character + player.set_controller { + type = defines.controllers.ghost, + } + if character ~= nil then character.destroy() end +end + +return clean_dirty_inventory