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) + } +}