From 5ca10d579d2bc58c0c51a21d51f7cff8c37f4b73 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:25:29 +0000 Subject: [PATCH] Restore pre-2.0 logistic slots and bound imported stats Logistic slots saved before 2.0 are keyed by index strings, which json_to_table keeps as strings, so the migration branch never ran and the 2.0 branch failed on the missing sections. Detect the old format by the missing sections and convert the keys to numbers. The import schema now uses the minimums Factorio enforces for character stats, checks the filter value type and the quick bar comparator, and allows filters without quality or comparator. Co-Authored-By: Claude Opus 5.5 --- plugins/player_sync/messages.ts | 29 ++++++++++++++---------- plugins/player_sync/module/serialize.lua | 6 ++--- plugins/player_sync/test/plugin.js | 15 +++++++++++- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/plugins/player_sync/messages.ts b/plugins/player_sync/messages.ts index 4b246f01..53251537 100644 --- a/plugins/player_sync/messages.ts +++ b/plugins/player_sync/messages.ts @@ -23,8 +23,8 @@ export type IpcPlayerData = { character?: any, inventories?: any, quick_bar?: any, - hotbar?: string[], - personal_logistic_slots?: {name:string, min:number, max:number}[], + hotbar?: Record, + personal_logistic_slots?: any, crafting_queue?: any, recipe_notifications?: string, // Components the upload was serialized with, all when missing @@ -54,22 +54,22 @@ const jsonColor = Type.Union([ Type.Object({ r: Type.Number(), g: Type.Number(), b: Type.Number(), a: Type.Optional(Type.Number()) }), Type.Array(Type.Number(), { minItems: 3, maxItems: 4 }), ]); -// module/define_player_stat_keys.lua, missing stats are left as they are +// Missing stats are left as they are, the minimums are the ones Factorio enforces const jsonCharacter = Type.Object({ - character_crafting_speed_modifier: Type.Optional(Type.Number()), - character_mining_speed_modifier: Type.Optional(Type.Number()), + character_crafting_speed_modifier: Type.Optional(Type.Number({ minimum: -1 })), + character_mining_speed_modifier: Type.Optional(Type.Number({ minimum: -1 })), character_additional_mining_categories: Type.Optional(jsonList(Type.String())), - character_running_speed_modifier: Type.Optional(Type.Number()), + character_running_speed_modifier: Type.Optional(Type.Number({ minimum: -1 })), character_build_distance_bonus: Type.Optional(Type.Number()), character_item_drop_distance_bonus: Type.Optional(Type.Number()), character_reach_distance_bonus: Type.Optional(Type.Number()), character_resource_reach_distance_bonus: Type.Optional(Type.Number()), character_item_pickup_distance_bonus: Type.Optional(Type.Number()), character_loot_pickup_distance_bonus: Type.Optional(Type.Number()), - character_inventory_slots_bonus: Type.Optional(Type.Number()), - character_trash_slot_count_bonus: Type.Optional(Type.Number()), - character_maximum_following_robot_count_bonus: Type.Optional(Type.Number()), - character_health_bonus: Type.Optional(Type.Number()), + character_inventory_slots_bonus: Type.Optional(Type.Integer({ minimum: 0 })), + character_trash_slot_count_bonus: Type.Optional(Type.Integer({ minimum: 0 })), + character_maximum_following_robot_count_bonus: Type.Optional(Type.Integer({ minimum: 0 })), + character_health_bonus: Type.Optional(Type.Number({ minimum: 0 })), character_personal_logistic_requests_enabled: Type.Optional(Type.Boolean()), allow_dispatching_robots: Type.Optional(Type.Boolean()), inhibit_movement_bonus: Type.Optional(Type.Boolean()), @@ -88,7 +88,7 @@ const jsonPersonalLogisticSlots = Type.Union([ group: Type.String(), active: Type.Boolean(), multiplier: Type.Number({ minimum: 0 }), - filters: jsonList(Type.Object({})), + filters: jsonList(Type.Object({ value: Type.Optional(Type.Object({})) })), })), }), // From before 2.0, slot index to request @@ -98,7 +98,12 @@ const jsonPersonalLogisticSlots = Type.Union([ // QuickBarSlotEncoded in module/serialize.lua const jsonQuickBarSlot = Type.Union([ Type.String(), - Type.Object({ t: Type.Literal("f"), n: Type.String(), q: Type.String(), c: Type.String() }), + Type.Object({ + t: Type.Literal("f"), + n: Type.String(), + q: Type.Optional(Type.String()), + c: Type.Optional(StringEnum(["=", ">", "<", "≥", ">=", "≤", "<=", "≠", "!="])), + }), Type.Object({ t: Type.Literal("i"), n: Type.String(), diff --git a/plugins/player_sync/module/serialize.lua b/plugins/player_sync/module/serialize.lua index 40b351e0..867f82cd 100644 --- a/plugins/player_sync/module/serialize.lua +++ b/plugins/player_sync/module/serialize.lua @@ -346,13 +346,13 @@ function serialize.deserialize_personal_logistic_slots(player, serialized) logistic_point.remove_section(i) end - -- If this is an array instead of a table, migrate to v2 format + -- Data from before 2.0 maps slot indices to requests, migrate it to a section if next(serialized) == nil then return - elseif serialized[1] ~= nil then + elseif serialized.sections == nil then local section = logistic_point.add_section() for i, slot in pairs(serialized) do - section.set_slot(i, { + section.set_slot(tonumber(i), { value = { name = slot.name, quality = "normal", diff --git a/plugins/player_sync/test/plugin.js b/plugins/player_sync/test/plugin.js index e04d69bb..57d7b4a4 100644 --- a/plugins/player_sync/test/plugin.js +++ b/plugins/player_sync/test/plugin.js @@ -75,7 +75,12 @@ describe("player_sync plugin", function() { personal_logistic_slots: { enabled: true, trash_not_requested: false, - sections: [{ group: "", active: true, multiplier: 1, filters: [{ index: 1, min: 50 }] }], + sections: [{ + group: "", + active: true, + multiplier: 1, + filters: [{ value: { name: "iron-plate", type: "item" }, min: 3 }], + }], }, quick_bar: { 1: "iron-plate", @@ -118,6 +123,9 @@ describe("player_sync plugin", function() { ["character", { inventories: { main: { i: [{ s: 0 }] } } }], ["shortcuts", { "toggle-personal-roboport": "yes" }], ["character", { character_health_bonus: "100" }], + ["character", { character_inventory_slots_bonus: -1 }], + ["character", { character_crafting_speed_modifier: -5 }], + ["quick_bar", { 1: { t: "f", n: "iron-plate", q: "normal", c: "~" } }], ["crafting_queue", {}], ["crafting_queue", { crafting_queue: [], crafting_queue_progress: 0 }], ["crafting_queue", { @@ -129,6 +137,11 @@ describe("player_sync plugin", function() { ["personal_logistic_slots", { enabled: true, sections: [] }], ["personal_logistic_slots", { enabled: true, trash_not_requested: false, sections: [{}] }], ["personal_logistic_slots", { 1: { min: 50 } }], + ["personal_logistic_slots", { + enabled: true, + trash_not_requested: false, + sections: [{ group: "", active: true, multiplier: 1, filters: [{ value: 5 }] }], + }], ]) { assert(!accepts({ ...exported, [field]: value }), `accepted ${field} ${JSON.stringify(value)}`); }