Compare commits

..
Author SHA1 Message Date
ILLISISandClaude Fable 5.2 a0c42225a4 Leave missing quick bar records alone once the quick bar is not synced
An instance whose quick bar sync was turned off while a record entry was
pending still set the slot when the library arrived.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyNnVzRW4jPWaoWTxCyhM7
2026-11-14 14:57:44 +11:00
ILLISISandClaude Fable 5.2 0b0e2fd377 Keep quick bar slots whose record is not in the blueprint library yet
A save gets a player's blueprint library from the client some seconds
after the join. The data of a player new to the save is applied before
that, so quick bar slots pointing at library records were cleared, and
the next upload stored the quick bar without them.

The entries are now kept. They are set once the record shows up, and an
upload before that stores them again as they came.

Fixes #1219

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyNnVzRW4jPWaoWTxCyhM7
2026-11-14 13:24:10 +11:00
2 changed files with 95 additions and 15 deletions

No files matched your search

@@ -634,6 +634,7 @@ player_sync.on_nth_tick[33] = function(event)
player_sync.check_players_in_cutscene()
player_sync.check_active_downloads()
player_sync.check_active_uploads()
player_sync.check_missing_quick_bar_records()
end
player_sync.events[defines.events.on_cutscene_waypoint_reached] = function(event)
@@ -781,6 +782,25 @@ function player_sync.check_active_downloads()
end
end
-- Quick bar slots pointing into the blueprint library are set once the save has the player's library
function player_sync.check_missing_quick_bar_records()
local script_data = get_script_data()
-- Not once the quick bar stopped being synced here. The entries stay for the upload, which goes by the
-- components of the player's session.
if not serialize.syncs(script_data.components, "quick_bar") then
return
end
for player_name, failed in pairs(script_data.failed_deserialization) do
local player = failed.quick_bar and game.get_player(player_name)
if player and player.connected then
failed.quick_bar = serialize.restore_missing_quick_bar_records(player, failed.quick_bar)
if not next(failed) then
script_data.failed_deserialization[player_name] = nil
end
end
end
end
function player_sync.check_active_uploads()
local script_data = get_script_data()
for player_name, record in pairs(script_data.active_uploads) do
+75 -15
View File
@@ -494,6 +494,27 @@ local function find_item_path(inventory, item_number)
return nil
end
--- The record a quick bar slot entry points at, if the player's library has it at that path
--- @param player LuaPlayer
--- @param entry QuickBarSlotEncoded.record
--- @return LuaRecord?
local function find_record(player, entry)
-- The record must still be what was stored, a shifted library must not set the wrong one
local records = player.blueprints
local record --[[@as LuaRecord?]]
for _, index in ipairs(entry.p) do
record = records[index]
if not (record and record.valid) then
return nil
end
records = record.type == "blueprint-book" and record.contents or {}
end
if record and record.type == entry.y and (record.label or "") == (entry.l or "") then
return record
end
return nil
end
--- @param filter ItemFilter
--- @return QuickBarSlotEncoded
local function serialize_filter(filter)
@@ -611,18 +632,8 @@ function serialize.deserialize_quick_bar_slot(entry, player)
end
if entry.t == "r" then
-- The record must still be what was stored, a shifted library must not set the wrong one
local records = player.blueprints
local record --[[@as LuaRecord?]]
for _, index in ipairs(entry.p) do
record = records[index]
if not (record and record.valid) then
record = nil
break
end
records = record.type == "blueprint-book" and record.contents or {}
end
if record and record.valid and record.type == entry.y and (record.label or "") == (entry.l or "") then
local record = find_record(player, entry)
if record then
return { type = "record", record = record } --[[@as QuickBarSlot]]
end
@@ -670,8 +681,9 @@ local function quick_bar_slot_count(player)
end
--- @param player LuaPlayer
--- @param missing table<string, QuickBarSlotEncoded.record>? Entries deserialize_quick_bar could not find the record of
--- @return table<string, QuickBarSlotEncoded>?
function serialize.serialize_quick_bar(player)
function serialize.serialize_quick_bar(player, missing)
local serialized
local width = v2_1_quick_bar_api and player.quick_bar_width
@@ -696,19 +708,63 @@ function serialize.serialize_quick_bar(player)
end
end
-- A record still missing here is stored again as it came, unless the player put something else in its slot
for index, entry in pairs(missing or {}) do
serialized = serialized or {}
serialized[index] = serialized[index] or entry
end
return serialized
end
--- Slot index to page and slot on it
--- @param player LuaPlayer
--- @param i integer
local function quick_bar_location(player, i)
local width = player.quick_bar_width
return math.floor((i - 1) / width) + 1, ((i - 1) % width) + 1
end
--- Set the slots whose records were not in the player's library when the data was applied. A save gets the library
--- of a player from the client some seconds after the join, later than the data of a player new to it is applied.
--- @param player LuaPlayer
--- @param missing table<string, QuickBarSlotEncoded.record>
--- @return table<string, QuickBarSlotEncoded.record>? missing Entries whose record is still not there
function serialize.restore_missing_quick_bar_records(player, missing)
for index, entry in pairs(missing) do
local page, page_slot = quick_bar_location(player, tonumber(index) --[[@as integer]])
if player.get_quick_bar_slot(page, page_slot) ~= nil then
-- The player put something else there
missing[index] = nil
else
local record = find_record(player, entry)
if record then
player.set_quick_bar_slot(page, page_slot, { type = "record", record = record })
missing[index] = nil
end
end
end
return next(missing) and missing or nil
end
--- @param player LuaPlayer
--- @param serialized table<string, string | QuickBarSlotEncoded>?
--- @return table<string, QuickBarSlotEncoded.record>? missing Entries whose record is not in the player's library
function serialize.deserialize_quick_bar(player, serialized)
-- Every slot is set, and the ones not in the data are cleared, so a slot moved or cleared on the
-- 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
local missing
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 slot == nil and v2_1_quick_bar_api and type(entry) == "table" and entry.t == "r" then
-- ponytail: kept until the record shows up or the slot is used for something else, so the entry of a
-- record deleted from the library is carried along. Drop them by age if that ever matters.
missing = missing or {}
missing[tostring(i)] = entry
end
if v2_1_quick_bar_api then
local page = math.floor((i - 1) / width) + 1
local page_slot = ((i - 1) % width) + 1
@@ -718,6 +774,7 @@ function serialize.deserialize_quick_bar(player, serialized)
player.set_quick_bar_slot(i, slot)
end
end
return missing
end
--- Quick bar page shown on each quick bar row of the screen, kept by every save on its own
@@ -1212,7 +1269,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 = serialize.serialize_quick_bar(player, failed_deserialization.quick_bar)
serialized.quick_bar_pages = serialize.serialize_quick_bar_pages(player)
end
@@ -1333,6 +1390,7 @@ end
--- @class FailedDeserializationPlayerData
--- @field recipe_notifications string[]?
--- @field quick_bar table<string, QuickBarSlotEncoded.record>? Slots whose record is not in the library (yet)
--- @param player LuaPlayer
--- @param serialized SerializedPlayerData
@@ -1495,7 +1553,9 @@ 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)
failed_deserialization.quick_bar = serialize.deserialize_quick_bar(
player, serialized.quick_bar or serialized.hotbar
)
serialize.deserialize_quick_bar_pages(player, serialized.quick_bar_pages)
end