From a02251e486795b206b40954b5a30a9125380b61f Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Fri, 20 Feb 2026 22:57:40 +0100 Subject: [PATCH] fix(channelserver): mitigate house theme corruption on save (#92) The game client sometimes writes -1 (0xFF bytes) into the house_tier field during save, which causes the house theme to vanish on next login. Snapshot the house tier before applying the save delta and restore it if the incoming value is corrupted. --- server/channelserver/handlers_data.go | 19 +++++++++++++++++++ server/channelserver/model_character.go | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/server/channelserver/handlers_data.go b/server/channelserver/handlers_data.go index 3a2264ce1..17f11ccca 100644 --- a/server/channelserver/handlers_data.go +++ b/server/channelserver/handlers_data.go @@ -25,6 +25,11 @@ func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } + // Snapshot current house tier before applying the update so we can + // restore it if the incoming data is corrupted (issue #92). + prevHouseTier := make([]byte, len(characterSaveData.HouseTier)) + copy(prevHouseTier, characterSaveData.HouseTier) + // Var to hold the decompressed savedata for updating the launcher response fields. if pkt.SaveType == 1 { // Diff-based update. @@ -55,6 +60,20 @@ func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) { } characterSaveData.updateStructWithSaveData() + // Mitigate house theme corruption (issue #92): the game client + // sometimes sends house_tier as -1 (all 0xFF bytes), which causes + // the house theme to vanish on next login. If the new value looks + // corrupted, restore the previous value in both the struct and the + // decompressed blob so Save() persists consistent data. + if len(prevHouseTier) > 0 && characterSaveData.isHouseTierCorrupted() { + s.logger.Warn("Detected corrupted house_tier in save data, restoring previous value", + zap.Binary("corrupted", characterSaveData.HouseTier), + zap.Binary("restored", prevHouseTier), + zap.Uint32("charID", s.charID), + ) + characterSaveData.restoreHouseTier(prevHouseTier) + } + s.playtime = characterSaveData.Playtime s.playtimeTime = time.Now() diff --git a/server/channelserver/model_character.go b/server/channelserver/model_character.go index f732b1042..055d728bd 100644 --- a/server/channelserver/model_character.go +++ b/server/channelserver/model_character.go @@ -202,3 +202,27 @@ func (save *CharacterSaveData) updateStructWithSaveData() { } } } + +// isHouseTierCorrupted checks whether the house tier field contains 0xFF +// bytes, which indicates an uninitialized or -1 value from the game client. +// The game uses small positive integers for theme IDs; 0xFF is never valid. +func (save *CharacterSaveData) isHouseTierCorrupted() bool { + for _, b := range save.HouseTier { + if b == 0xFF { + return true + } + } + return false +} + +// restoreHouseTier replaces the current house tier with the given value in +// both the struct field and the underlying decompressed save blob, keeping +// them consistent for Save(). +func (save *CharacterSaveData) restoreHouseTier(valid []byte) { + save.HouseTier = make([]byte, len(valid)) + copy(save.HouseTier, valid) + offset, ok := save.Pointers[pHouseTier] + if ok && offset+len(valid) <= len(save.decompSave) { + copy(save.decompSave[offset:offset+len(valid)], valid) + } +}