feat(savedata): write back zenny/gzenny/CP to ZZ save blob

Mirrors the read path added in 47277c7: updateSaveDataWithStruct now
flushes Zenny/GZenny/CP back to the blob for ZZ, using the same
`ok && off > 0 && off+size <= len(blob)` guard so unmapped modes remain
inert.

Tests lock down byte-level idempotence — the most important invariant
for save data. Parsing a live kirito ZZ blob and immediately writing
the struct back produces a byte-identical blob, so enabling these
fields cannot silently corrupt existing player saves on the next save
cycle. Additional coverage: round-trip through both paths, non-ZZ
modes never touch the blob bytes, and truncated blobs don't panic on
write.
This commit is contained in:
Houmgaor
2026-04-17 23:06:16 +02:00
parent 47277c712d
commit b1972e3c96
3 changed files with 208 additions and 1 deletions

View File

@@ -171,6 +171,18 @@ func (save *CharacterSaveData) updateSaveDataWithStruct() {
if save.Mode >= cfg.G10 {
copy(save.decompSave[save.Pointers[pKQF]:save.Pointers[pKQF]+saveFieldKQF], save.KQF)
}
// Write zenny / gzenny / CP only when a validated pointer exists for the
// current mode. Same guards as the read path: absent or zero offsets are
// never written, so unmapped versions cannot corrupt unrelated bytes.
if off, ok := save.Pointers[pZenny]; ok && off > 0 && off+saveFieldZenny <= len(save.decompSave) {
binary.LittleEndian.PutUint32(save.decompSave[off:off+saveFieldZenny], save.Zenny)
}
if off, ok := save.Pointers[pGZenny]; ok && off > 0 && off+saveFieldGZenny <= len(save.decompSave) {
binary.LittleEndian.PutUint32(save.decompSave[off:off+saveFieldGZenny], save.GZenny)
}
if off, ok := save.Pointers[pCP]; ok && off > 0 && off+saveFieldCP <= len(save.decompSave) {
binary.LittleEndian.PutUint32(save.decompSave[off:off+saveFieldCP], save.CP)
}
}
// This will update the save struct with the values stored in the character save