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 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-10-17 20:25:29 +00:00
1 parent c06555c5da
commit 5ca10d579d
3 files changed
+34 -16

No files matched your search

+17 -12
View File
@@ -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<string, string>,
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(),
+3 -3
View File
@@ -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",
+14 -1
View File
@@ -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)}`);
}