diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b02b2cc..3806b3589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dashboard channel ports now reflect the actual configured `Entrance.Entries[].Channels[].Port` instead of a hardcoded `54000 + server_id`. - Fixed backup recovery panic: `recoverFromBackups` now rejects decompressed backup data smaller than the minimum save layout size, preventing a slice-bounds panic when nullcomp passes through garbage bytes as "already decompressed" data ([#182](https://github.com/Mezeporta/Erupe/pull/182)). +- Fixed save-time panic and character rollback on Forward.5 / Forward.4 / Season 6.0 clients: bookshelf was introduced after Forward.5 (verified against the F5 client binary), so the configured pointers overshoot the smaller save blob. The bookshelf read is now bounds-checked and skipped when absent; persistence via house packets is unaffected. ## [9.3.2] - 2026-04-06 diff --git a/server/channelserver/model_character.go b/server/channelserver/model_character.go index 488c6fbf5..832796d13 100644 --- a/server/channelserver/model_character.go +++ b/server/channelserver/model_character.go @@ -188,7 +188,16 @@ func (save *CharacterSaveData) updateStructWithSaveData() { 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]] + // Bookshelf was introduced after Forward.5 (verified: F5 mhfo.dll + // contains no Bookshelf symbols, while modern clients export + // .?AVBookshelfForm@@). For F4/F5/S6 the configured pointers + // place the bookshelf region past the end of the save blob, so + // skip the read entirely on those versions. Bookshelf state is + // persisted via house packets into user_binary.bookshelf, not + // from this blob, so leaving BookshelfData nil is safe. + if bsEnd := save.Pointers[pBookshelfData] + save.Pointers[lBookshelfData]; bsEnd <= len(save.decompSave) { + save.BookshelfData = save.decompSave[save.Pointers[pBookshelfData]:bsEnd] + } 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]