diff --git a/plugins/inventory_sync/module/commands.lua b/plugins/inventory_sync/module/commands.lua new file mode 100644 index 00000000..914c961b --- /dev/null +++ b/plugins/inventory_sync/module/commands.lua @@ -0,0 +1,17 @@ +local save_crafts = require("modules/inventory_sync/save_crafts") +local restore_crafts = require("modules/inventory_sync/restore_crafts") + +function add_commands() + --[[ + Command to serialize crafting queue for transport. + When serialized, the crafting queue is cleared and stored in global. The items received are cleared as well. + Upon leaving, the value in global is cleared and sent with the inventory to the master. + Upon joining a game the crafting queue is added to the end of the existing crafting queue. + ]] + commands.add_command("save-crafts", "Save crafting queue for transport to a different server", save_crafts) + commands.add_command("csc", "Shorthand for /save-crafts", save_crafts) + commands.add_command("restore-crafts", "Load crafting queue from global", restore_crafts) + commands.add_command("crc", "Shorthand for /restore-crafts", restore_crafts) +end + +return add_commands diff --git a/plugins/inventory_sync/module/define_player_inventories.lua b/plugins/inventory_sync/module/define_player_inventories.lua new file mode 100644 index 00000000..86569406 --- /dev/null +++ b/plugins/inventory_sync/module/define_player_inventories.lua @@ -0,0 +1,10 @@ +local inventories = { + defines.inventory.character_main, + defines.inventory.character_guns, + defines.inventory.character_ammo, + defines.inventory.character_armor, + defines.inventory.character_vehicle, + defines.inventory.character_trash, +} + +return inventories diff --git a/plugins/inventory_sync/module/download_inventory.lua b/plugins/inventory_sync/module/download_inventory.lua new file mode 100644 index 00000000..0110a0af --- /dev/null +++ b/plugins/inventory_sync/module/download_inventory.lua @@ -0,0 +1,95 @@ +local progress_dialog = require("modules/inventory_sync/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") + +function download_inventory(playerName, data, number, total) + local player = game.players[playerName] + if player == nil then return end + if number then + if global.download_cache[playerName] == nil then + global.download_cache[playerName] = "" + end + -- Append data to download cache + global.download_cache[playerName] = global.download_cache[playerName] .. data + end + + if number ~= total then + -- Show progress in console + -- game.players[playerName].print("Downloaded "..(number + 1).."/"..total.." parts") + -- Show progress in GUI + progress_dialog(player, number+1, total) + + -- Request next segment + rcon.print("Segment downloaded") + else + -- Recreate player character + player.set_controller { + type = defines.controllers.god, + } + player.create_character() + -- Load downloaded inventory + local serialized_player = game.json_to_table(global.download_cache[playerName]) + global.download_cache[playerName] = nil -- remove data to lower mapsize + local player = game.players[playerName] + -- Load inventories + for _, inv in pairs(inventories) do + local inventory = player.get_inventory(inv) + if inventory ~= nil and serialized_player.inventories[tostring(inv)] ~= nil then + serialize.deserialize_inventory(inventory, serialized_player.inventories[tostring(inv)]) + end + end + + -- Load personal logistics slots + for i = 1, 200 do + local slot = serialized_player.personal_logistic_slots[tostring(i)] + if slot ~= nil then + player.set_personal_logistic_slot(i, slot) + end + end + + -- Load hotbar, don't overwrite empty slots + for i = 1, 100 do + -- if serialized_player.hotbar[i] ~= nil then + player.set_quick_bar_slot(i, game.item_prototypes[serialized_player.hotbar[i]]) + -- end + end + + -- Load crafting queue + load_crafting_queue(serialized_player.crafting_queue, player) + + -- Misc + player.cheat_mode = serialized_player.cheat_mode + player.force = serialized_player.force -- Force by name as string + player.tag = serialized_player.tag + player.color = serialized_player.color + player.chat_color = serialized_player.chat_color + + player.character_crafting_speed_modifier = serialized_player.character_crafting_speed_modifier + player.character_mining_speed_modifier = serialized_player.character_mining_speed_modifier + player.character_additional_mining_categories = serialized_player.character_additional_mining_categories + player.character_running_speed_modifier = serialized_player.character_running_speed_modifier + player.character_build_distance_bonus = serialized_player.character_build_distance_bonus + player.character_item_drop_distance_bonus = serialized_player.character_item_drop_distance_bonus + player.character_reach_distance_bonus = serialized_player.character_reach_distance_bonus + player.character_resource_reach_distance_bonus = serialized_player.character_resource_reach_distance_bonus + player.character_item_pickup_distance_bonus = serialized_player.character_item_pickup_distance_bonus + player.character_loot_pickup_distance_bonus = serialized_player.character_loot_pickup_distance_bonus + player.character_inventory_slots_bonus = serialized_player.character_inventory_slots_bonus + player.character_trash_slot_count_bonus = serialized_player.character_trash_slot_count_bonus + player.character_maximum_following_robot_count_bonus = serialized_player.character_maximum_following_robot_count_bonus + player.character_health_bonus = serialized_player.character_health_bonus + player.character_personal_logistic_requests_enabled = serialized_player.character_personal_logistic_requests_enabled + + if serialized_player.flashlight then + player.enable_flashlight() + else + player.disable_flashlight() + end + + local startTick = global["inv_sync_download_start_tick "..game.players[playerName].name] + game.print("Imported inventory for "..playerName.." in "..game.tick - startTick.." ticks") + end +end + +return download_inventory diff --git a/plugins/inventory_sync/module/inventory_sync.lua b/plugins/inventory_sync/module/inventory_sync.lua index c412c641..396c9afe 100644 --- a/plugins/inventory_sync/module/inventory_sync.lua +++ b/plugins/inventory_sync/module/inventory_sync.lua @@ -8,28 +8,38 @@ When the master sends an inventory, parse it and give it to the player. ]] -local serialize = require("modules/clusterio/serialize") local clusterio_api = require("modules/clusterio/api") -local progress_dialog = require("modules/inventory_sync/progress_dialog") - -local inventories = { - defines.inventory.character_main, - defines.inventory.character_guns, - defines.inventory.character_ammo, - defines.inventory.character_armor, - defines.inventory.character_vehicle, - defines.inventory.character_trash, -} +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") inventory_sync = {} +inventory_sync.add_commands = add_commands inventory_sync.events = {} +-- Cleanup +inventory_sync.events[defines.events.on_player_removed] = function(event) + local player = game.players[event.player_index] + + -- Remove inventory download performance counter + global["inv_sync_download_start_tick "..player.name] = nil + -- Remove stored crafting queue + global["saved_crafting_queue_"..player.name] = nil +end + -- Download inventory from master inventory_sync.events[defines.events.on_player_joined_game] = function(event) local playerIndex = event.player_index inventory_sync.initiateInventoryDownload(playerIndex) end +-- Upload inventory when a player leaves the game. Triggers on restart after crash if player was online during crash. +inventory_sync.events[defines.events.on_player_left_game] = function(event) + local playerIndex = event.player_index + inventory_sync.uploadInventory(playerIndex) + global.download_cache[playerIndex] = "" -- Clear cache to avoid leaking memory to people quitting during download +end + function inventory_sync.initiateInventoryDownload(playerIndex) local player = game.players[playerIndex] player.print("Initiating inventory download...") @@ -50,176 +60,9 @@ function inventory_sync.initiateInventoryDownload(playerIndex) if character ~= nil then character.destroy() end end --- This function is called by instance.js as /sc inventory_sync.downloadInventory("danielv", Escaped JSON string) with data from the master -function inventory_sync.downloadInventory(playerName, data, number, total) - local player = game.players[playerName] - if player == nil then return end - if number then - if global.download_cache[playerName] == nil then - global.download_cache[playerName] = "" - end - -- Append data to download cache - global.download_cache[playerName] = global.download_cache[playerName] .. data - end - - if number ~= total then - -- Show progress in console - -- game.players[playerName].print("Downloaded "..(number + 1).."/"..total.." parts") - -- Show progress in GUI - progress_dialog(player, number+1, total) - - -- Request next segment - rcon.print("Segment downloaded") - else - -- Recreate player character - player.set_controller { - type = defines.controllers.god, - } - player.create_character() - -- Load downloaded inventory - local serialized_player = game.json_to_table(global.download_cache[playerName]) - global.download_cache[playerName] = nil -- remove data to lower mapsize - local player = game.players[playerName] - -- Load inventories - for _, inv in pairs(inventories) do - local inventory = player.get_inventory(inv) - if inventory ~= nil and serialized_player.inventories[tostring(inv)] ~= nil then - serialize.deserialize_inventory(inventory, serialized_player.inventories[tostring(inv)]) - end - end - - -- Load personal logistics slots - for i = 1, 200 do - local slot = serialized_player.personal_logistic_slots[tostring(i)] - if slot ~= nil then - player.set_personal_logistic_slot(i, slot) - end - end - - -- Load hotbar, don't overwrite empty slots - for i = 1, 100 do - -- if serialized_player.hotbar[i] ~= nil then - player.set_quick_bar_slot(i, game.item_prototypes[serialized_player.hotbar[i]]) - -- end - end - - -- Load crafting queue - for _, queueItem in pairs(serialized_player.crafting_queue) do - player.begin_crafting { - count = queueItem.count, - recipe = queueItem.recipe, - silent = true, -- Fail silently if items are missing - } - end - - -- Misc - player.cheat_mode = serialized_player.cheat_mode - player.force = serialized_player.force -- Force by name as string - player.tag = serialized_player.tag - player.color = serialized_player.color - player.chat_color = serialized_player.chat_color - - player.character_crafting_speed_modifier = serialized_player.character_crafting_speed_modifier - player.character_mining_speed_modifier = serialized_player.character_mining_speed_modifier - player.character_additional_mining_categories = serialized_player.character_additional_mining_categories - player.character_running_speed_modifier = serialized_player.character_running_speed_modifier - player.character_build_distance_bonus = serialized_player.character_build_distance_bonus - player.character_item_drop_distance_bonus = serialized_player.character_item_drop_distance_bonus - player.character_reach_distance_bonus = serialized_player.character_reach_distance_bonus - player.character_resource_reach_distance_bonus = serialized_player.character_resource_reach_distance_bonus - player.character_item_pickup_distance_bonus = serialized_player.character_item_pickup_distance_bonus - player.character_loot_pickup_distance_bonus = serialized_player.character_loot_pickup_distance_bonus - player.character_inventory_slots_bonus = serialized_player.character_inventory_slots_bonus - player.character_trash_slot_count_bonus = serialized_player.character_trash_slot_count_bonus - player.character_maximum_following_robot_count_bonus = serialized_player.character_maximum_following_robot_count_bonus - player.character_health_bonus = serialized_player.character_health_bonus - player.character_personal_logistic_requests_enabled = serialized_player.character_personal_logistic_requests_enabled - - if serialized_player.flashlight then - player.enable_flashlight() - else - player.disable_flashlight() - end - - local startTick = global["inv_sync_download_start_tick "..game.players[playerName].name] - game.print("Imported inventory for "..playerName.." in "..game.tick - startTick.." ticks") - end -end - --- Upload inventory when a player leaves the game. Triggers on restart after crash if player was online during crash. -inventory_sync.events[defines.events.on_player_left_game] = function(event) - local playerIndex = event.player_index - inventory_sync.uploadInventory(playerIndex) - global.download_cache[playerIndex] = "" -- Clear cache to avoid leaking memory to people quitting during download -end - -function inventory_sync.uploadInventory(playerIndex) - local player = game.players[playerIndex] - - local serialized_player = { - name = player.name, - inventories = {}, - hotbar = {}, - color = player.color, - chat_color = player.chat_color, - tag = player.tag, - -- admin = player.admin, -- This is handled by other parts of clusterio - personal_logistic_slots = {}, - force = player.force.name, - cheat_mode = player.cheat_mode, - crafting_queue = {}, - character_crafting_speed_modifier = player.character_crafting_speed_modifier, - character_mining_speed_modifier = player.character_mining_speed_modifier, - character_additional_mining_categories = player.character_additional_mining_categories, - character_running_speed_modifier = player.character_running_speed_modifier, - character_build_distance_bonus = player.character_build_distance_bonus, - character_item_drop_distance_bonus = player.character_item_drop_distance_bonus, - character_reach_distance_bonus = player.character_reach_distance_bonus, - character_resource_reach_distance_bonus = player.character_resource_reach_distance_bonus, - character_item_pickup_distance_bonus = player.character_item_pickup_distance_bonus, - character_loot_pickup_distance_bonus = player.character_loot_pickup_distance_bonus, - character_inventory_slots_bonus = player.character_inventory_slots_bonus, - character_trash_slot_count_bonus = player.character_trash_slot_count_bonus, - character_maximum_following_robot_count_bonus = player.character_maximum_following_robot_count_bonus, - character_health_bonus = player.character_health_bonus, - character_personal_logistic_requests_enabled = player.character_personal_logistic_requests_enabled, - - flashlight = player.is_flashlight_enabled(), - } - - -- Serialize hotbar - for i = 1, 100 do - serialized_player.hotbar[i] = {} - local slot = player.get_quick_bar_slot(i) - if slot ~= nil and slot.name ~= nil then - serialized_player.hotbar[i] = slot.name - end - end - - -- Serialize personal logistics slots - for i = 1, 200 do -- Nobody will have more than 200 logistics slots, right? - local slot = player.get_personal_logistic_slot(i) - if slot.name ~= nil then - -- We leave [1] empty to force the JSON function to parse it as an object. - -- When its parsed as an array we get inconsistent key datatypes depending - -- on whether or not it is sparse. - serialized_player.personal_logistic_slots[i+1] = { - name = slot.name, - min = slot.min, - max = slot.max, - } - end - end - - -- Serialize inventories - for _, inv in pairs(inventories) do - local inventory = player.get_inventory(inv) - if inventory ~= nil then - local serialized_inventory = serialize.serialize_inventory(inventory) - serialized_player.inventories[inv] = serialized_inventory - end - end - clusterio_api.send_json("inventory_sync_upload", serialized_player) -end +-- This function is called by instance.js as /sc inventory_sync.downloadInventory("danielv", Escaped JSON string, package_number, total_packages_count) with data from the master +inventory_sync.downloadInventory = download_inventory +-- This function is called internally when a player leaves the game for their inventory to upload to the master +inventory_sync.uploadInventory = upload_inventory return inventory_sync diff --git a/plugins/inventory_sync/module/load_crafting_queue.lua b/plugins/inventory_sync/module/load_crafting_queue.lua new file mode 100644 index 00000000..edec5cb6 --- /dev/null +++ b/plugins/inventory_sync/module/load_crafting_queue.lua @@ -0,0 +1,35 @@ +-- Load crafting queue from a table +function load_crafting_queue(crafting_queue, player) + local inventory = player.get_main_inventory() + + -- Give player some more inventory space to avoid duplicating items + player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 100 + + -- Load crafting queue + for _, queueItem in pairs(crafting_queue) do + local recipe = game.recipe_prototypes[queueItem.recipe] + game.print("Crafting "..queueItem.recipe) + -- Add items to inventory + for _, item in pairs(recipe.ingredients) do + local count = math.floor(item.amount * queueItem.count / math.max(recipe.main_product.amount, 1)) + if count > 0 then + inventory.insert({ + name = item.name, + count = count, + }) + end + end + + -- Start crafting (consume items) + player.begin_crafting { + count = queueItem.count, + recipe = queueItem.recipe, + silent = true, -- Fail silently if items are missing + } + end + + -- Remove extra inventory slots + player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 100 +end + +return load_crafting_queue diff --git a/plugins/inventory_sync/module/restore_crafts.lua b/plugins/inventory_sync/module/restore_crafts.lua new file mode 100644 index 00000000..5b7650a9 --- /dev/null +++ b/plugins/inventory_sync/module/restore_crafts.lua @@ -0,0 +1,19 @@ +local load_crafting_queue = require("modules/inventory_sync/load_crafting_queue") + +function restore_crafts(event) + if event.player_index then + local player = game.players[event.player_index] + local crafting_queue = global["saved_crafting_queue_"..player.name] + + if crafting_queue == nil then + player.print("No saved crafting queue") + return + end + + load_crafting_queue(crafting_queue, player) + + global["saved_crafting_queue_"..player.name] = nil + end +end + +return restore_crafts diff --git a/plugins/inventory_sync/module/save_crafts.lua b/plugins/inventory_sync/module/save_crafts.lua new file mode 100644 index 00000000..b11beab1 --- /dev/null +++ b/plugins/inventory_sync/module/save_crafts.lua @@ -0,0 +1,55 @@ +function save_crafts(event) + if event.player_index then + local player = game.players[event.player_index] + local crafting_queue = {} + + -- Avoid overwriting old save + if global["saved_crafting_queue_"..player.name] ~= nil then + player.print("You already have a saved crafting queue, restore it with /restore-crafts") + return + end + + -- Serialize crafting queue + if player.crafting_queue == nil then + player.print("You can't save nothing, you silly goose") + return + end + for _, queueItem in pairs(player.crafting_queue) do + crafting_queue[queueItem.index] = { + count = queueItem.count, + recipe = queueItem.recipe, + } + end + + -- Give player some more inventory space to avoid duplicating items + player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 100 + -- Cancel old crafts to get the items back + while player.crafting_queue_size > 0 do + local queueItem = player.crafting_queue[1] + local recipe = game.recipe_prototypes[queueItem.recipe] + + -- Cancel craft + player.cancel_crafting { + index = 1, + count = 1, + } + + -- Remove ingredients from inventory + for _, item in pairs(recipe.ingredients) do + local inventory = player.get_main_inventory() + inventory.remove({ + name = item.name, + count = item.amount, + }) + end + game.print("Canceled craft "..recipe.name) + end + + -- Remove extra inventory slots + player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 100 + + global["saved_crafting_queue_"..player.name] = crafting_queue + end +end + +return save_crafts diff --git a/plugins/inventory_sync/module/upload_inventory.lua b/plugins/inventory_sync/module/upload_inventory.lua new file mode 100644 index 00000000..0394fc6c --- /dev/null +++ b/plugins/inventory_sync/module/upload_inventory.lua @@ -0,0 +1,83 @@ +local clusterio_api = require("modules/clusterio/api") +local serialize = require("modules/clusterio/serialize") +local inventories = require("modules/inventory_sync/define_player_inventories") + +function upload_inventory(playerIndex) + local player = game.players[playerIndex] + + local serialized_player = { + name = player.name, + inventories = {}, + hotbar = {}, + color = player.color, + chat_color = player.chat_color, + tag = player.tag, + -- admin = player.admin, -- This is handled by other parts of clusterio + personal_logistic_slots = {}, + force = player.force.name, + cheat_mode = player.cheat_mode, + character_crafting_speed_modifier = player.character_crafting_speed_modifier, + character_mining_speed_modifier = player.character_mining_speed_modifier, + character_additional_mining_categories = player.character_additional_mining_categories, + character_running_speed_modifier = player.character_running_speed_modifier, + character_build_distance_bonus = player.character_build_distance_bonus, + character_item_drop_distance_bonus = player.character_item_drop_distance_bonus, + character_reach_distance_bonus = player.character_reach_distance_bonus, + character_resource_reach_distance_bonus = player.character_resource_reach_distance_bonus, + character_item_pickup_distance_bonus = player.character_item_pickup_distance_bonus, + character_loot_pickup_distance_bonus = player.character_loot_pickup_distance_bonus, + character_inventory_slots_bonus = player.character_inventory_slots_bonus, + character_trash_slot_count_bonus = player.character_trash_slot_count_bonus, + character_maximum_following_robot_count_bonus = player.character_maximum_following_robot_count_bonus, + character_health_bonus = player.character_health_bonus, + character_personal_logistic_requests_enabled = player.character_personal_logistic_requests_enabled, + + flashlight = player.is_flashlight_enabled(), + } + + -- Serialize hotbar + for i = 1, 100 do + serialized_player.hotbar[i] = {} + local slot = player.get_quick_bar_slot(i) + if slot ~= nil and slot.name ~= nil then + serialized_player.hotbar[i] = slot.name + end + end + + -- Serialize personal logistics slots + for i = 1, 200 do -- Nobody will have more than 200 logistics slots, right? + local slot = player.get_personal_logistic_slot(i) + if slot.name ~= nil then + -- We leave [1] empty to force the JSON function to parse it as an object. + -- When its parsed as an array we get inconsistent key datatypes depending + -- on whether or not it is sparse. + serialized_player.personal_logistic_slots[i+1] = { + name = slot.name, + min = slot.min, + max = slot.max, + } + end + end + + -- Serialize inventories + for _, inv in pairs(inventories) do + local inventory = player.get_inventory(inv) + if inventory ~= nil then + local serialized_inventory = serialize.serialize_inventory(inventory) + serialized_player.inventories[inv] = serialized_inventory + end + end + + -- Serialize crafting queue saved by /csc or /save-crafts + serialized_player.crafting_queue = global["saved_crafting_queue_"..player.name] + if serialized_player.crafting_queue == nil then + serialized_player.crafting_queue = {} + end + + clusterio_api.send_json("inventory_sync_upload", serialized_player) + + -- Clear saved crafting queue + global["saved_crafting_queue_"..player.name] = nil +end + +return upload_inventory