Files
Erupe/server/channelserver/model_character.go
Houmgaor a02251e486 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.
2026-02-20 22:57:40 +01:00

229 lines
7.1 KiB
Go

package channelserver
import (
"encoding/binary"
"erupe-ce/common/bfutil"
"erupe-ce/common/stringsupport"
_config "erupe-ce/config"
"erupe-ce/server/channelserver/compression/nullcomp"
)
// SavePointer identifies a section within the character save data blob.
type SavePointer int
const (
pGender = iota
pRP
pHouseTier
pHouseData
pBookshelfData
pGalleryData
pToreData
pGardenData
pPlaytime
pWeaponType
pWeaponID
pHR
pGRP
pKQF
lBookshelfData
)
// CharacterSaveData holds a character's save data and its parsed fields.
type CharacterSaveData struct {
CharID uint32
Name string
IsNewCharacter bool
Mode _config.Mode
Pointers map[SavePointer]int
Gender bool
RP uint16
HouseTier []byte
HouseData []byte
BookshelfData []byte
GalleryData []byte
ToreData []byte
GardenData []byte
Playtime uint32
WeaponType uint8
WeaponID uint16
HR uint16
GR uint16
KQF []byte
compSave []byte
decompSave []byte
}
func getPointers(mode _config.Mode) map[SavePointer]int {
pointers := map[SavePointer]int{pGender: 81, lBookshelfData: 5576}
switch mode {
case _config.ZZ:
pointers[pPlaytime] = 128356
pointers[pWeaponID] = 128522
pointers[pWeaponType] = 128789
pointers[pHouseTier] = 129900
pointers[pToreData] = 130228
pointers[pHR] = 130550
pointers[pGRP] = 130556
pointers[pHouseData] = 130561
pointers[pBookshelfData] = 139928
pointers[pGalleryData] = 140064
pointers[pGardenData] = 142424
pointers[pRP] = 142614
pointers[pKQF] = 146720
case _config.Z2, _config.Z1, _config.G101, _config.G10, _config.G91, _config.G9, _config.G81, _config.G8,
_config.G7, _config.G61, _config.G6, _config.G52, _config.G51, _config.G5, _config.GG, _config.G32, _config.G31,
_config.G3, _config.G2, _config.G1:
pointers[pPlaytime] = 92356
pointers[pWeaponID] = 92522
pointers[pWeaponType] = 92789
pointers[pHouseTier] = 93900
pointers[pToreData] = 94228
pointers[pHR] = 94550
pointers[pGRP] = 94556
pointers[pHouseData] = 94561
pointers[pBookshelfData] = 89118 // TODO: fix bookshelf data pointer
pointers[pGalleryData] = 104064
pointers[pGardenData] = 106424
pointers[pRP] = 106614
pointers[pKQF] = 110720
case _config.F5, _config.F4:
pointers[pPlaytime] = 60356
pointers[pWeaponID] = 60522
pointers[pWeaponType] = 60789
pointers[pHouseTier] = 61900
pointers[pToreData] = 62228
pointers[pHR] = 62550
pointers[pHouseData] = 62561
pointers[pBookshelfData] = 57118 // TODO: fix bookshelf data pointer
pointers[pGalleryData] = 72064
pointers[pGardenData] = 74424
pointers[pRP] = 74614
case _config.S6:
pointers[pPlaytime] = 12356
pointers[pWeaponID] = 12522
pointers[pWeaponType] = 12789
pointers[pHouseTier] = 13900
pointers[pToreData] = 14228
pointers[pHR] = 14550
pointers[pHouseData] = 14561
pointers[pBookshelfData] = 9118 // TODO: fix bookshelf data pointer
pointers[pGalleryData] = 24064
pointers[pGardenData] = 26424
pointers[pRP] = 26614
}
if mode == _config.G5 {
pointers[lBookshelfData] = 5548
} else if mode <= _config.GG {
pointers[lBookshelfData] = 4520
}
return pointers
}
func (save *CharacterSaveData) Compress() error {
var err error
save.compSave, err = nullcomp.Compress(save.decompSave)
if err != nil {
return err
}
return nil
}
func (save *CharacterSaveData) Decompress() error {
var err error
save.decompSave, err = nullcomp.Decompress(save.compSave)
if err != nil {
return err
}
return nil
}
// This will update the character save with the values stored in the save struct
func (save *CharacterSaveData) updateSaveDataWithStruct() {
rpBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(rpBytes, save.RP)
if save.Mode >= _config.F4 {
copy(save.decompSave[save.Pointers[pRP]:save.Pointers[pRP]+saveFieldRP], rpBytes)
}
if save.Mode >= _config.G10 {
copy(save.decompSave[save.Pointers[pKQF]:save.Pointers[pKQF]+saveFieldKQF], save.KQF)
}
}
// This will update the save struct with the values stored in the character save
// Save data field sizes
const (
saveFieldRP = 2
saveFieldHouseTier = 5
saveFieldHouseData = 195
saveFieldGallery = 1748
saveFieldTore = 240
saveFieldGarden = 68
saveFieldPlaytime = 4
saveFieldWeaponID = 2
saveFieldHR = 2
saveFieldGRP = 4
saveFieldKQF = 8
saveFieldNameOffset = 88
saveFieldNameLen = 12
)
func (save *CharacterSaveData) updateStructWithSaveData() {
save.Name, _ = stringsupport.SJISToUTF8(bfutil.UpToNull(save.decompSave[saveFieldNameOffset : saveFieldNameOffset+saveFieldNameLen]))
if save.decompSave[save.Pointers[pGender]] == 1 {
save.Gender = true
} else {
save.Gender = false
}
if !save.IsNewCharacter {
if save.Mode >= _config.S6 {
save.RP = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pRP] : save.Pointers[pRP]+saveFieldRP])
save.HouseTier = save.decompSave[save.Pointers[pHouseTier] : save.Pointers[pHouseTier]+saveFieldHouseTier]
save.HouseData = save.decompSave[save.Pointers[pHouseData] : save.Pointers[pHouseData]+saveFieldHouseData]
save.BookshelfData = save.decompSave[save.Pointers[pBookshelfData] : save.Pointers[pBookshelfData]+save.Pointers[lBookshelfData]]
save.GalleryData = save.decompSave[save.Pointers[pGalleryData] : save.Pointers[pGalleryData]+saveFieldGallery]
save.ToreData = save.decompSave[save.Pointers[pToreData] : save.Pointers[pToreData]+saveFieldTore]
save.GardenData = save.decompSave[save.Pointers[pGardenData] : save.Pointers[pGardenData]+saveFieldGarden]
save.Playtime = binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pPlaytime] : save.Pointers[pPlaytime]+saveFieldPlaytime])
save.WeaponType = save.decompSave[save.Pointers[pWeaponType]]
save.WeaponID = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pWeaponID] : save.Pointers[pWeaponID]+saveFieldWeaponID])
save.HR = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pHR] : save.Pointers[pHR]+saveFieldHR])
if save.Mode >= _config.G1 {
if save.HR == uint16(999) {
save.GR = grpToGR(int(binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pGRP] : save.Pointers[pGRP]+saveFieldGRP])))
}
}
if save.Mode >= _config.G10 {
save.KQF = save.decompSave[save.Pointers[pKQF] : save.Pointers[pKQF]+saveFieldKQF]
}
}
}
}
// 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)
}
}