From b781731eccf8359b65406ac2531f04b70a7dd3ba Mon Sep 17 00:00:00 2001 From: ILLISIS Date: Fri, 13 Nov 2026 22:14:46 +1100 Subject: [PATCH 1/2] Sync the page shown on each quick bar row player_sync restored the content of the quick bar slots but not which page each row shows, which every save keeps per player on its own. After switching servers the rows could show other pages, and with them another set of slots. Fixes #1201 Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01JyNnVzRW4jPWaoWTxCyhM7 --- plugins/player_sync/README.md | 2 +- plugins/player_sync/components.ts | 2 +- plugins/player_sync/messages.ts | 2 ++ plugins/player_sync/module/serialize.lua | 27 ++++++++++++++++++++++++ plugins/player_sync/test/plugin.js | 3 +++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/plugins/player_sync/README.md b/plugins/player_sync/README.md index 5f7c16c3..c44c2706 100644 --- a/plugins/player_sync/README.md +++ b/plugins/player_sync/README.md @@ -166,7 +166,7 @@ Defaults to `true`. ### player_sync.sync_quick_bar -Sync the quick bar. +Sync the quick bar and the page shown on each of its rows. Defaults to `true`. diff --git a/plugins/player_sync/components.ts b/plugins/player_sync/components.ts index 1e1c1b03..133e039f 100644 --- a/plugins/player_sync/components.ts +++ b/plugins/player_sync/components.ts @@ -9,7 +9,7 @@ export const components = { // inventories holds the god inventory and the hidden ghost inventory inventories: ["character", "crafting_queue", "inventories"], logistics: ["personal_logistic_slots"], - quick_bar: ["quick_bar", "hotbar"], + quick_bar: ["quick_bar", "quick_bar_pages", "hotbar"], settings: ["flashlight", "shortcuts", "game_view_settings"], recipe_notifications: ["recipe_notifications"], } as const; diff --git a/plugins/player_sync/messages.ts b/plugins/player_sync/messages.ts index 5a92591d..a3a1ce9b 100644 --- a/plugins/player_sync/messages.ts +++ b/plugins/player_sync/messages.ts @@ -23,6 +23,7 @@ export type IpcPlayerData = { character?: any, inventories?: any, quick_bar?: any, + quick_bar_pages?: number[], hotbar?: Record, personal_logistic_slots?: any, crafting_queue?: any, @@ -181,6 +182,7 @@ const jsonImportedPlayerData = Type.Object({ character: Type.Optional(jsonCharacter), inventories: Type.Optional(jsonInventories), quick_bar: Type.Optional(Type.Record(Type.String(), jsonQuickBarSlot)), + quick_bar_pages: Type.Optional(Type.Array(Type.Integer({ minimum: 1, maximum: 10 }), { maxItems: 10 })), hotbar: Type.Optional(Type.Record(Type.String(), Type.String())), personal_logistic_slots: Type.Optional(jsonPersonalLogisticSlots), crafting_queue: Type.Optional(jsonCraftingQueue), diff --git a/plugins/player_sync/module/serialize.lua b/plugins/player_sync/module/serialize.lua index 7a79d9ed..df6dec79 100644 --- a/plugins/player_sync/module/serialize.lua +++ b/plugins/player_sync/module/serialize.lua @@ -713,6 +713,30 @@ function serialize.deserialize_quick_bar(player, serialized) end end +--- Quick bar page shown on each quick bar row of the screen, kept by every save on its own +--- @param player LuaPlayer +--- @return integer[]? +function serialize.serialize_quick_bar_pages(player) + local pages = {} + for screen = 1, 10 do + -- pcall: the number of rows the API knows is not documented for older versions + local ok, page = pcall(player.get_active_quick_bar_page, screen) + if not ok or not page then + break + end + pages[screen] = page + end + return pages[1] and pages or nil +end + +--- @param player LuaPlayer +--- @param pages integer[]? Absent in data from before pages were synced, the rows then stay as they are here +function serialize.deserialize_quick_bar_pages(player, pages) + for screen, page in ipairs(pages or {}) do + player.set_active_quick_bar_page(screen, page) + end +end + -- Crafting queue is a table with the following fields: -- crafting_queue -- ingredients @@ -974,6 +998,7 @@ end --- @field inventories table? --- @field hotbar table? --- @field quick_bar table? +--- @field quick_bar_pages integer[]? Page shown on each quick bar row --- @field personal_logistic_slots table? --- @field crafting_queue table? --- @field recipe_notifications string? @@ -1178,6 +1203,7 @@ function serialize.serialize_player(player, failed_deserialization, components) -- Serialize quick bar if syncs(components, "quick_bar") then serialized.quick_bar = serialize.serialize_quick_bar(player) + serialized.quick_bar_pages = serialize.serialize_quick_bar_pages(player) end -- Serialize crafting queue @@ -1444,6 +1470,7 @@ function serialize.deserialize_player(player, serialized, components, local_cont -- Deserialize quick bar (named hotbar in old data) if syncs(components, "quick_bar") then serialize.deserialize_quick_bar(player, serialized.quick_bar or serialized.hotbar) + serialize.deserialize_quick_bar_pages(player, serialized.quick_bar_pages) end -- Deserialize recipe notifications diff --git a/plugins/player_sync/test/plugin.js b/plugins/player_sync/test/plugin.js index 77516d93..2bc67904 100644 --- a/plugins/player_sync/test/plugin.js +++ b/plugins/player_sync/test/plugin.js @@ -117,6 +117,7 @@ describe("player_sync plugin", function() { 3: { t: "i", n: "main", s: 4, b: [2] }, 4: { t: "r", p: [1], y: "blueprint" }, }, + quick_bar_pages: [2, 1, 3], crafting_queue: { crafting_queue: [{ recipe: "iron-gear-wheel", count: 5 }], crafting_queue_progress: 0.25, @@ -172,6 +173,8 @@ describe("player_sync plugin", function() { ["quick_bar", { 1: { t: "i", n: "main" } }], ["quick_bar", { 1: { t: "r", y: "blueprint" } }], ["quick_bar", { 1: 5 }], + ["quick_bar_pages", [0]], + ["quick_bar_pages", [11]], ["personal_logistic_slots", { enabled: true, sections: [] }], ["personal_logistic_slots", { enabled: true, trash_not_requested: false, sections: [{}] }], ["personal_logistic_slots", { 1: { min: 50 } }], From 87bdbd520f4049310d300b2ed261c072a4d59c64 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Fri, 13 Nov 2026 22:49:12 +0100 Subject: [PATCH 2/2] Sync every quick bar slot when the pages are wider than 10 From Factorio 2.1 a quick bar page is as wide as the player set it, up to 100 slots. Only the first 100 slots were synced, so with wider pages the later pages kept the content of the destination, and restoring the page shown on a row could show those. --- plugins/player_sync/module/serialize.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/player_sync/module/serialize.lua b/plugins/player_sync/module/serialize.lua index df6dec79..88d22b23 100644 --- a/plugins/player_sync/module/serialize.lua +++ b/plugins/player_sync/module/serialize.lua @@ -662,13 +662,20 @@ function serialize.deserialize_quick_bar_slot(entry, player) return entry --[[@as string]] end +--- Slots over all 10 quick bar pages, a page is as wide as the player set it from 2.1 and 10 slots before +--- @param player LuaPlayer +--- @return integer +local function quick_bar_slot_count(player) + return 10 * (v2_1_quick_bar_api and player.quick_bar_width or 10) +end + --- @param player LuaPlayer --- @return table? function serialize.serialize_quick_bar(player) local serialized local width = v2_1_quick_bar_api and player.quick_bar_width - for i = 1, 100 do + for i = 1, quick_bar_slot_count(player) do local slot if v2_1_quick_bar_api then @@ -699,7 +706,7 @@ function serialize.deserialize_quick_bar(player, serialized) -- previous server does not come back from this server's old copy serialized = serialized or {} local width = v2_1_quick_bar_api and player.quick_bar_width - for i = 1, 100 do + for i = 1, quick_bar_slot_count(player) do local entry = serialized[tostring(i)] local slot = entry ~= nil and serialize.deserialize_quick_bar_slot(entry, player) or nil if v2_1_quick_bar_api then