mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 08:33:41 +01:00
updateSaveDataWithStruct only wrote RP and KQF into the blob, leaving the playtime field stale. On each reconnect, GetCharacterSaveData read the old in-game counter from the blob and reset s.playtime to it, rolling back all progress accumulated during the previous session. Playtime is now persisted into the blob alongside RP, using the same S6 mode guard as the read path in updateStructWithSaveData.
234 lines
7.1 KiB
Go
234 lines
7.1 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"encoding/binary"
|
|
|
|
"erupe-ce/common/bfutil"
|
|
"erupe-ce/common/stringsupport"
|
|
cfg "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 cfg.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 cfg.Mode) map[SavePointer]int {
|
|
pointers := map[SavePointer]int{pGender: 81, lBookshelfData: 5576}
|
|
switch mode {
|
|
case cfg.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 cfg.Z2, cfg.Z1, cfg.G101, cfg.G10, cfg.G91, cfg.G9, cfg.G81, cfg.G8,
|
|
cfg.G7, cfg.G61, cfg.G6, cfg.G52, cfg.G51, cfg.G5, cfg.GG, cfg.G32, cfg.G31,
|
|
cfg.G3, cfg.G2, cfg.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] = 103928
|
|
pointers[pGalleryData] = 104064
|
|
pointers[pGardenData] = 106424
|
|
pointers[pRP] = 106614
|
|
pointers[pKQF] = 110720
|
|
case cfg.F5, cfg.F4:
|
|
pointers[pPlaytime] = 60356
|
|
pointers[pWeaponID] = 60522
|
|
pointers[pWeaponType] = 60789
|
|
pointers[pHouseTier] = 61900
|
|
pointers[pToreData] = 62228
|
|
pointers[pHR] = 62550
|
|
pointers[pHouseData] = 62561
|
|
pointers[pBookshelfData] = 71928
|
|
pointers[pGalleryData] = 72064
|
|
pointers[pGardenData] = 74424
|
|
pointers[pRP] = 74614
|
|
case cfg.S6:
|
|
pointers[pPlaytime] = 12356
|
|
pointers[pWeaponID] = 12522
|
|
pointers[pWeaponType] = 12789
|
|
pointers[pHouseTier] = 13900
|
|
pointers[pToreData] = 14228
|
|
pointers[pHR] = 14550
|
|
pointers[pHouseData] = 14561
|
|
pointers[pBookshelfData] = 23928
|
|
pointers[pGalleryData] = 24064
|
|
pointers[pGardenData] = 26424
|
|
pointers[pRP] = 26614
|
|
}
|
|
if mode == cfg.G5 {
|
|
pointers[lBookshelfData] = 5548
|
|
} else if mode <= cfg.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.DecompressWithLimit(save.compSave, saveDataMaxDecompressedPayload)
|
|
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 >= cfg.F4 {
|
|
copy(save.decompSave[save.Pointers[pRP]:save.Pointers[pRP]+saveFieldRP], rpBytes)
|
|
}
|
|
if save.Mode >= cfg.S6 {
|
|
playtimeBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(playtimeBytes, save.Playtime)
|
|
copy(save.decompSave[save.Pointers[pPlaytime]:save.Pointers[pPlaytime]+saveFieldPlaytime], playtimeBytes)
|
|
}
|
|
if save.Mode >= cfg.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.SJISToUTF8Lossy(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 >= cfg.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 >= cfg.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 >= cfg.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)
|
|
}
|
|
}
|