Documentation
¶
Index ¶
- Constants
- Variables
- func AddItemsToSlot(slot *SaveSlot, itemIDs []uint32, invQty, storageQty int, forceStackable bool) error
- func AddItemsToSlotBatch(slot *SaveSlot, items []ItemToAdd) error
- func AppendTutorialID(slot *SaveSlot, id uint32) error
- func ApplyGaItemRepackPlan(slot *SaveSlot, plan GaItemRepackPlan) error
- func AttachExistingWeaponAoW(slot *SaveSlot, weaponHandle, aowHandle uint32) error
- func ClearProfileSummaryRegion(data []byte, idx int)
- func ClearWeaponAoW(slot *SaveSlot, weaponHandle uint32) error
- func ComputeMD5(data []byte) [16]byte
- func ComputeSlotHash(slot *SaveSlot) [HashSize]byte
- func CreateBackup(path string) (string, error)
- func CreateWeaponAoWCopy(slot *SaveSlot, weaponHandle, aowItemID uint32) error
- func CurrentWeaponAoWItemID(slot *SaveSlot, weaponHandle uint32) (uint32, bool)
- func DecryptSave(data []byte) ([]byte, error)
- func EffectiveQuantityCap(rec ResolvedRecord, containerOwned map[uint32]uint64) (limit uint64, applies bool)
- func EncryptSave(data []byte, iv []byte) ([]byte, error)
- func FingerprintRecordAt(slot *SaveSlot, scope string, row int) (string, bool)
- func FlushGaItems(slot *SaveSlot) errordeprecated
- func GaItemRecordSize(itemID uint32) int
- func HasTutorialID(slot *SaveSlot, id uint32) (bool, error)
- func IsHandleEquipped(slot *SaveSlot, handle uint32) bool
- func IsNoCustomAoWHandle(h uint32) bool
- func IssueKeyID(key IssueKey) string
- func PatchEquippedSpell(slot *SaveSlot, slotIndex int, spellID uint32) error
- func PatchNetworkParams(ud11 []byte, patch NetworkParamValues) ([]byte, error)
- func PatchWeaponAoW(slot *SaveSlot, weaponHandle, newAoWItemID uint32) error
- func PatchWeaponAoWHandle(slot *SaveSlot, weaponHandle uint32, newAoWHandle uint32) error
- func PatchWeaponItemID(slot *SaveSlot, handle, expectedCurrentItemID, newItemID uint32) error
- func PruneBackups(path string, max int) error
- func ReadTutorialIDs(slot *SaveSlot) ([]uint32, error)
- func RebuildSlot(slot *SaveSlot) ([]byte, error)
- func RebuildSlotFull(slot *SaveSlot) ([]byte, error)
- func RecalculateSlotHash(slot *SaveSlot)
- func ReconcileInventoryHeader(slot *SaveSlot)
- func ReconcileStorageHeader(slot *SaveSlot)
- func RemoveInventoryRecordAt(slot *SaveSlot, scope string, row int, fingerprint string) error
- func RemoveItemByBaseID(slot *SaveSlot, itemID uint32)
- func RemoveItemFromSlot(slot *SaveSlot, handle uint32, fromInventory, fromStorage bool) error
- func RepairDLCSection(slot *SaveSlot) bool
- func RepairDuplicateWondrousPhysick(slot *SaveSlot) (int, error)
- func RepairGaItemDataCount(slot *SaveSlot) bool
- func RepairGaItemDuplicate(slot *SaveSlot, handle uint32, keepIndex int) error
- func RepairOrphanedGaItems(slot *SaveSlot) int
- func RepairSlot(slot *SaveSlot) (fixed, skipped []string)
- func RepairStats(slot *SaveSlot) []string
- func RepairStorageCountHeader(slot *SaveSlot) bool
- func RestoreSlot(slot *SaveSlot, snap SlotSnapshot)
- func ScanRepairIssuesWithCoverage(slotIndex int, slot *SaveSlot, records []ResolvedRecord) ([]RepairIssue, ValidationCoverage)
- func SetUnlockedRegions(slot *SaveSlot, ids []uint32) error
- func UTF16ToString(u16 []uint16) string
- func ValidateNetworkParams(p NetworkParamValues) error
- func ValidateSlotIntegrity(slot *SaveSlot) error
- type AoWCopyRaw
- type BaseVersion
- type BloodStain
- type CSMenuSystemSaveLoad
- type CapacityReport
- type DLCSection
- type DiagnosticIssue
- type DiagnosticSeverity
- type DuplicateInventoryIndexIssue
- type EquipInventoryData
- type EquipmentSlotKind
- type EquipmentWrite
- type EventFlagsBlock
- type FloatVector3
- type FloatVector4
- type GaItem
- type GaItemCapacity
- type GaItemDuplicateAnalysis
- type GaItemDuplicateCandidate
- type GaItemFull
- type GaItemRepackAnalysis
- type GaItemRepackBlocker
- type GaItemRepackPlan
- type GaItemRepackPreflight
- type GaItemRepackResult
- type GaitemGameData
- type GaitemGameDataEntry
- type IdentityClass
- type IntegrityError
- type InventoryIndexRepairChange
- type InventoryIndexRepairReport
- type InventoryItem
- type IssueKey
- type ItemToAdd
- type MapID
- type MenuSaveLoad
- type NetMan
- type NetworkParamValues
- func NetworkParamAggressiveBlue() NetworkParamValues
- func NetworkParamAggressiveHost() NetworkParamValues
- func NetworkParamAggressiveHunter() NetworkParamValues
- func NetworkParamAggressiveReds() NetworkParamValues
- func NetworkParamAggressiveSummonGuest() NetworkParamValues
- func NetworkParamAggressiveSummonHost() NetworkParamValues
- func NetworkParamAggressiveSummons() NetworkParamValues
- func NetworkParamDefaults() NetworkParamValues
- func NetworkParamFast() NetworkParamValues
- func NetworkParamFastBlue() NetworkParamValues
- func NetworkParamFastInvasions() NetworkParamValues
- func NetworkParamFastSummons() NetworkParamValues
- func NetworkParamFasterBlue() NetworkParamValues
- func NetworkParamFasterHunter() NetworkParamValues
- func NetworkParamFasterReds() NetworkParamValues
- func NetworkParamFasterSummonGuest() NetworkParamValues
- func NetworkParamFasterSummonHost() NetworkParamValues
- func NetworkParamFasterSummons() NetworkParamValues
- func NetworkParamLightInvasions() NetworkParamValues
- func ReadNetworkParams(ud11 []byte) (*NetworkParamValues, error)
- type PS5Activity
- type Platform
- type PlayerCoordinates
- type PlayerGameData
- type PlayerGameDataHash
- type PreEventFlagsScalars
- type ProfileSummary
- type QuantityClampChange
- type Reader
- func (r *Reader) FindPattern(pattern []byte) int
- func (r *Reader) Len() int
- func (r *Reader) Pos() int
- func (r *Reader) ReadAt(p []byte, off int64) (n int, err error)
- func (r *Reader) ReadBytes(n int) ([]byte, error)
- func (r *Reader) ReadF32() (float32, error)
- func (r *Reader) ReadI32() (int32, error)
- func (r *Reader) ReadSizedBytes(maxSize int, fieldName string) ([]byte, error)
- func (r *Reader) ReadU8() (uint8, error)
- func (r *Reader) ReadU16() (uint16, error)
- func (r *Reader) ReadU32() (uint32, error)
- func (r *Reader) ReadU64() (uint64, error)
- func (r *Reader) Seek(offset int64, whence int) (int64, error)
- type RehandleChange
- type RepairIssue
- type Resolution
- type ResolvedRecord
- type RideGameData
- type SaveFile
- type SaveSlot
- func (s *SaveSlot) CalculateDynamicOffsets() error
- func (s *SaveSlot) FaceDataStart() int
- func (s *SaveSlot) Read(r *Reader, platform string) error
- func (s *SaveSlot) SyncPlayerToData()
- func (s *SaveSlot) Write(platform string) []byte
- func (s *SaveSlot) WriteEquipment(writes []EquipmentWrite) error
- func (s *SaveSlot) WriteSpells(writes []SpellWrite) error
- type SectionRange
- type SectionWriter
- func (w *SectionWriter) Bytes() []byte
- func (w *SectionWriter) Len() int
- func (w *SectionWriter) PadZeros(n int)
- func (w *SectionWriter) WriteBytes(b []byte)
- func (w *SectionWriter) WriteF32(v float32)
- func (w *SectionWriter) WriteI32(v int32)
- func (w *SectionWriter) WriteSizedBytes(data []byte)
- func (w *SectionWriter) WriteU8(v uint8)
- func (w *SectionWriter) WriteU16(v uint16)
- func (w *SectionWriter) WriteU32(v uint32)
- func (w *SectionWriter) WriteU64(v uint64)
- type SizePrefixedBlob
- type SlotAccessor
- func (sa *SlotAccessor) CheckBounds(off, size int, label string) error
- func (sa *SlotAccessor) ReadDynamicSize(off int, maxSize int, name string) (int, error)
- func (sa *SlotAccessor) ReadU8(off int) (uint8, error)
- func (sa *SlotAccessor) ReadU16(off int) (uint16, error)
- func (sa *SlotAccessor) ReadU32(off int) (uint32, error)
- func (sa *SlotAccessor) ReadU64(off int) (uint64, error)
- func (sa *SlotAccessor) WriteU8(off int, val uint8) error
- func (sa *SlotAccessor) WriteU16(off int, val uint16) error
- func (sa *SlotAccessor) WriteU32(off int, val uint32) error
- func (sa *SlotAccessor) WriteU64(off int, val uint64) error
- type SlotDiagnostics
- type SlotSnapshot
- type SlotUsage
- type SpawnPointBlock
- type SpellWrite
- type TrailingFixedBlock
- type TransferDirection
- type TransferOptions
- type TransferResult
- type TransferSkip
- type TrophyEquipData
- type TutorialData
- type UnknownReason
- type ValidationCoverage
- type WondrousPhysickOccurrence
- type WorldAreaTime
- type WorldAreaWeather
- type WorldGeomBlock
- type WorldHead
- type Writer
Constants ¶
const ( HashMagic = 0x80078071 // Magic constant for ComputeHashedValue HashSize = 0x80 // 128 bytes = 32 u32 entries (12 used + padding) HashOffset = SlotSize - HashSize // 0x27FF80 — start of hash block in slot HashEntries = 12 // Number of meaningful hash entries )
Hash algorithm constants.
const ( OffHumanity = -347 // PGD+0x54 OffSoulMemory = -327 // PGD+0x68 OffPGD0xB8 = -247 // PGD+0xB8 (1 byte, used for hash index 3) )
PlayerGameData field offsets relative to MagicOffset, for hash computation. These correspond to the PGD struct layout documented in the audit.
const ( OffLevel = -335 OffVigor = -379 OffMind = -375 OffEndurance = -371 OffStrength = -367 OffDexterity = -363 OffIntelligence = -359 OffFaith = -355 OffArcane = -351 OffSouls = -331 OffGender = -249 OffClass = -248 OffGreatRuneOn = -184 // GreatRuneActive (PGD 0xF7, u8 bool, 0=off 1=on) OffTalismanSlots = -241 // AdditionalTalismanSlotsCount (PGD 0xBE, u8, range 0-3) OffScadutreeBlessing = -187 OffShadowRealmBlessing = -186 OffVoiceType = -245 // Voice type (0=Young1, 1=Young2, 2=Mature1, 3=Mature2, 4=Aged1, 5=Aged2) OffCharacterName = -0x11B // 16 x uint16 UTF-16LE // MagicOffset must be at least this value; otherwise negative stat offsets // would access memory before the start of the slot buffer. MinMagicOffset = 400 // abs(OffVigor) + margin )
Offsets relative to MagicOffset (negative = before the pattern). Source: SPEC.md §5.2 PlayerGameData.
const ( GaRecordWeapon = 21 GaRecordArmor = 16 GaRecordAccessory = 8 GaRecordItem = 8 GaRecordAoW = 8 )
GaItem record sizes by handle type prefix (upper nibble). Source: SPEC.md §5.3 GaItems.
const ( GaHandleEmpty = 0x00000000 GaHandleInvalid = 0xFFFFFFFF GaHandleTypeMask = 0xF0000000 // upper nibble = item type )
GaItem handle constants.
const ( InvStartFromMagic = 505 // MagicOffset + 505 — points to first common item (common_count header at -4) CommonItemCount = 0xA80 // 2688 common item slots KeyItemCount = 0x180 // 384 key item slots StorageItemCount = 2048 // storage box capacity (read limit for ReadStorage) StorageCommonCount = 0x780 // 1920 actual common item slots in storage StorageKeyCount = 0x80 // 128 key item slots in storage InvRecordLen = 12 // bytes per inventory record (handle + qty + index) InvSafetyMargin = 0x9000 // max distance from invStart to validate section StorageSafetyMarg = 0x6000 // max distance from storageStart to validate section StorageHeaderSkip = 4 // skip 4-byte header at StorageBoxOffset InvKeyCountHeader = 4 // 4-byte key_count header between common and key items // Offsets of trailing counters relative to (StorageBoxOffset + StorageHeaderSkip). // Layout: StorageCommonCount×12 + key_count(4) + StorageKeyCount×12 + next_equip_index(4) + next_acq_sort_id(4) StorageNextEquipIdxRel = StorageCommonCount*InvRecordLen + InvKeyCountHeader + StorageKeyCount*InvRecordLen StorageNextAcqSortRel = StorageNextEquipIdxRel + 4 )
Inventory layout (relative to MagicOffset). Source: SPEC.md §5.4 Dynamic Offsets.
const ( DynPlayerData = 0x1B0 DynSpEffect = 0xD0 DynEquipedItemIndex = 0x58 // these three share the same offset value by coincidence of the save layout DynActiveEquipedItems = 0x1C DynEquipedItemsID = 0x58 // same value as DynEquipedItemIndex — different struct contexts DynActiveEquipedItemsGa = 0x58 // same value — GaItem variant of the same offset DynInventoryHeld = 0x9011 DynEquipedSpells = 0x74 DynEquipedItems = 0x8C DynEquipedGestures = 0x18 DynEquipedArmaments = 0x9C DynEquipePhysics = 0x0C DynFaceData = 0x12F DynStorageBox = 0x6010 DynStorageToGestures = 0x100 DynHorse = 0x29 DynEquipGreatRune = 0x28 // offset within EquippedItemsItemIds to Great Rune slot (u32) DynClearCount = 0x44 // offset from horse to ClearCount (NG+ cycle, uint32) DynBloodStain = 0x4C DynMenuProfile = 0x103C DynGaItemsOther = 0x1B588 DynTutorialData = 0x40B DynIngameTimer = 0x1A DynEventFlags = 0 )
Dynamic offset chain constants (relative to InventoryEnd). Source: SPEC.md §5.4 Dynamic Offsets.
const ( FavBaseOffset = 0x154 // first preset slot in UserData10.Data (same for PC and PS4) FavSlotSize = 0x130 // 304 bytes per preset slot FavSlotCount = 15 // total preset slots in CSMenuSystemSaveLoad // Offsets within a single 0x130-byte preset slot FavOffBodyFlag = 0x08 // u8: body flag FavOffBodyType = 0x09 // u8: 0=female, 1=male FavOffMarker = 0x14 // i32: -1 (0xFFFFFFFF) = empty, 0 = active (per er-save-manager) FavOffMagic = 0x18 // "FACE" (4 bytes) — indicates slot is populated FavOffAlignment = 0x1C // u32: 4 FavOffInnerSize = 0x20 // u32: 0x120 (288) FavOffModelIDs = 0x24 // 8 × u32: model IDs (same layout as FaceData blob) FavOffFaceShape = 0x44 // 64 bytes: face shape sliders FavOffUnkBlock = 0x84 // 64 bytes: unk0x6c — opaque, preserved on apply (game ignores preset's value) FavOffBody = 0xC4 // 7 bytes: body proportions (head, chest, abdomen, arm_r, leg_r, arm_l, leg_l) FavOffSkin = 0xCB // 91 bytes: skin & cosmetics (same length as slot's FaceData skin block) )
CSMenuSystemSaveLoad / Favorites preset constants. Located in UserData10.Data. The game's Mirror at Roundtable Hold reads presets from here. All 15 slots (0..14) are safe for use after the ProfileSummary offset fix (ProfileSummary now correctly writes at 0x195E + i*0x24C, well past all preset slots which span 0x154..0x1323). See spec/23-user-data-10.md and spec/31-appearance-presets.md.
const ( FavHeaderUnk = 0x11D0 FavHeaderMagicU16 = 0xFACE )
FavHeaderUnk is the constant u32 at preset header offset 0x04 (observed in all active presets). FavHeaderMagicU16 is the u16 written at offset 0x00 of the Favorites slot header.
const ( FoWBlobStart = 0x087E FoWBlobEnd = 0x10B0 )
Fog of War bitfield offsets relative to afterRegs. See spec/27-map-reveal.md §4.
const ( FaceDataBlobSize = 0x12F // 303 bytes total // Header (16 bytes) FDOffMarker = 0x00 // u32 = 0xFFFFFFFF FDOffMagic = 0x04 // "FACE" FDOffAlignment = 0x08 // u32 = 4 FDOffInnerSize = 0x0C // u32 = 0x120 (288) // Model IDs (8 × u32, effective u8 + 3 padding each) FDOffFaceModel = 0x10 FDOffHairModel = 0x14 FDOffEyeModel = 0x18 FDOffEyebrowModel = 0x1C FDOffBeardModel = 0x20 FDOffEyepatchModel = 0x24 FDOffDecalModel = 0x28 // tattoo/mark FDOffEyelashModel = 0x2C // Face shape parameters (64 × u8, 0x30-0x6F) FDOffFaceShape = 0x30 // Unknown block (64 bytes, 0x70-0xAF) — leave unchanged FDOffUnknownBlock = 0x70 // Body proportions (7 × u8, 0xB0-0xB6) FDOffHead = 0xB0 FDOffChest = 0xB1 FDOffAbdomen = 0xB2 FDOffArmR = 0xB3 FDOffLegR = 0xB4 FDOffArmL = 0xB5 FDOffLegL = 0xB6 // Skin & cosmetics (91 bytes, 0xB7-0x111) FDOffSkinR = 0xB7 FDOffSkinG = 0xB8 FDOffSkinB = 0xB9 FDOffSkinLuster = 0xBA FDOffPores = 0xBB FDOffStubble = 0xBC FDOffDarkCircles = 0xBD FDOffDarkCircleR = 0xBE FDOffDarkCircleG = 0xBF FDOffDarkCircleB = 0xC0 FDOffCheeksInt = 0xC1 FDOffCheekR = 0xC2 FDOffCheekG = 0xC3 FDOffCheekB = 0xC4 FDOffEyeliner = 0xC5 FDOffEyelinerR = 0xC6 FDOffEyelinerG = 0xC7 FDOffEyelinerB = 0xC8 FDOffEyeShadLow = 0xC9 FDOffEyeShadLowR = 0xCA FDOffEyeShadLowG = 0xCB FDOffEyeShadLowB = 0xCC FDOffEyeShadUp = 0xCD FDOffEyeShadUpR = 0xCE FDOffEyeShadUpG = 0xCF FDOffEyeShadUpB = 0xD0 FDOffLipstick = 0xD1 FDOffLipstickR = 0xD2 FDOffLipstickG = 0xD3 FDOffLipstickB = 0xD4 FDOffTattooH = 0xD5 FDOffTattooV = 0xD6 FDOffTattooAngle = 0xD7 FDOffTattooExp = 0xD8 FDOffTattooR = 0xD9 FDOffTattooG = 0xDA FDOffTattooB = 0xDB FDOffTattooUnk = 0xDC FDOffTattooFlip = 0xDD FDOffBodyHair = 0xDE FDOffBodyHairR = 0xDF FDOffBodyHairG = 0xE0 FDOffBodyHairB = 0xE1 // Right eye FDOffRIrisR = 0xE2 FDOffRIrisG = 0xE3 FDOffRIrisB = 0xE4 FDOffRIrisSize = 0xE5 FDOffRClouding = 0xE6 FDOffRCloudR = 0xE7 FDOffRCloudG = 0xE8 FDOffRCloudB = 0xE9 FDOffRWhiteR = 0xEA FDOffRWhiteG = 0xEB FDOffRWhiteB = 0xEC FDOffREyePos = 0xED // Left eye FDOffLIrisR = 0xEE FDOffLIrisG = 0xEF FDOffLIrisB = 0xF0 FDOffLIrisSize = 0xF1 FDOffLClouding = 0xF2 FDOffLCloudR = 0xF3 FDOffLCloudG = 0xF4 FDOffLCloudB = 0xF5 FDOffLWhiteR = 0xF6 FDOffLWhiteG = 0xF7 FDOffLWhiteB = 0xF8 FDOffLEyePos = 0xF9 // Hair colors FDOffHairR = 0xFA FDOffHairG = 0xFB FDOffHairB = 0xFC FDOffHairLuster = 0xFD FDOffHairRoot = 0xFE FDOffHairWhite = 0xFF // Beard colors FDOffBeardR = 0x100 FDOffBeardG = 0x101 FDOffBeardB = 0x102 FDOffBeardLuster = 0x103 FDOffBeardRoot = 0x104 FDOffBeardWhite = 0x105 // Eyebrow colors FDOffBrowR = 0x106 FDOffBrowG = 0x107 FDOffBrowB = 0x108 FDOffBrowLuster = 0x109 FDOffBrowRoot = 0x10A FDOffBrowWhite = 0x10B // Eyelash colors FDOffLashR = 0x10C FDOffLashG = 0x10D FDOffLashB = 0x10E // Eyepatch colors FDOffPatchR = 0x10F FDOffPatchG = 0x110 FDOffPatchB = 0x111 )
FaceData blob layout constants. FaceData is a 303-byte (0x12F) block stored at FaceDataOffset-FaceDataBlobSize. All offsets below are relative to the start of the FaceData blob.
const ( MaxProjCount = 200000 // max acquired_projectiles count (projSkip = count*8+4; observed: 67584 PC, 103168 PS4) MaxUnlockedRegCnt = 20000 // max unlocked_regions count (regSkip = count*4+4) MaxHandleAttempts = 10000 // max iterations for generateUniqueHandle )
Sanity limits for dynamic size reads from untrusted save data.
const ( GaItemDataActiveEntryLen = 8 // active record: itemID(4) + flag(4) GaItemDataArrayOff = 8 // array starts after distinct_count(4) + unk1(4) GaItemDataMaxCount = 7000 // 0x1B58 max active records )
GaItemData section (distinct_acquired_items_count + GaItem2 array). Source: ER-Save-Editor save_slot.rs, GaItemData struct. GaItemData records every weapon/AoW ID ever acquired. The game looks up weapon properties (reinforce_type etc.) from this list on load. Missing entry → crash.
const ( // Offsets relative to afterRegs (end of unlocked regions array). DLCTileZeroStart = 0x0088 // start of range to zero out before writing coords DLCTileZeroEnd = 0x0110 // end of range (exclusive) // Record 1: DLC map center (2 floats + 1 flag byte) DLCTileRec1X = 0x008D // f32 X coordinate DLCTileRec1Y = 0x0091 // f32 Y coordinate DLCTileRec1Flag = 0x0095 // u8 visited flag // Record 2: DLC area anchor (4 floats + 1 flag byte) DLCTileRec2X = 0x00C5 // f32 X DLCTileRec2Y = 0x00C9 // f32 Y DLCTileRec2Z = 0x00CD // f32 Z DLCTileRec2W = 0x00D1 // f32 W DLCTileRec2Flag = 0x00D5 // u8 visited flag )
DLC black tile removal constants. Two position records in the BloodStain section control the DLC map cover layer. Writing DLC-area coordinates here removes the black tile overlay. See spec/29-dlc-black-tiles.md for details.
const ( DlcSectionSize = 0x32 // 50 bytes DlcSectionOffset = SlotSize - HashSize - DlcSectionSize // SlotSize - 0xB2 DlcEntryFlagByte = 1 // byte index within DLC section for SotE entry flag )
DLC section constants. CSDlc is 0x32 (50) bytes located at SlotSize - 0xB2 (before PlayerGameDataHash). Byte[0] = pre-order gesture "The Ring" Byte[1] = Shadow of the Erdtree entry flag (non-zero = entered DLC; causes infinite loading without DLC) Bytes[2] = pre-order gesture "Ring of Miquella" Bytes[3-49] = must be 0x00
const ( GaItemCountOld = 5118 // 0x13FE — version ≤ 81 GaItemCountNew = 5120 // 0x1400 — version > 81 GaItemVersionBreak = 81 // version threshold for GaItem count change )
GaItem entry counts by slot version. Source: ER-Save-Editor save_slot.rs, er-save-manager user_data_x.py
const ( RepairCodeDuplicateHandle = "duplicate_handle" RepairCodeDuplicateUID = "duplicate_uid" RepairCodeUnknownItemID = "unknown_item_id" RepairCodeUnknownHandleType = "unknown_handle_type" RepairCodeMissingGaItemMapping = "missing_gaitem_mapping" RepairCodeQuantityZero = "quantity_zero" RepairCodeQuantityAboveMax = "quantity_above_max" // RepairCodeItemNotAllowedInContainer is emitted instead of // quantity_above_max when the effective container cap is zero: the item is // not permitted in that container at all, which is a distinct defect from an // excessive-but-legal quantity. Splitting the codes keeps the clamp repair // from ever driving a quantity down to zero (which would manufacture a new // quantity_zero defect). RepairCodeItemNotAllowedInContainer = "item_not_allowed_in_container" // RepairCodePassThroughRecords is retained for JSON/action-map // compatibility but is no longer emitted as an aggregate issue — // pass-through is a write strategy, not a defect. Per-record resolution // status is reported via the coverage model instead. RepairCodePassThroughRecords = "pass_through_records" RepairCodeCurrentAoWMissing = "current_aow_missing" RepairCodeCurrentAoWNonAoWCategory = "current_aow_non_aow_category" RepairCodeDuplicateAcquisitionIndex = "duplicate_acquisition_index" RepairCodeStatsFormula = "stats_formula" // RepairCodeContainerOveruse is a REPORT-ONLY aggregate: the total quantity // of pot/aromatic craftables mapped to one container (e.g. all Throwing Pots // against Cracked Pot) exceeds the number of that container the slot owns. // Per-record caps cannot see this — each individual pot stack may sit within // the owned count while their sum overflows the shared container. It carries // no mutating action (no safe generic auto-repair; the user chooses what to // trim), so its default action is no_action. RepairCodeContainerOveruse = "container_overuse" // RepairCodeDuplicatePhysicalHandle flags two non-empty records in the // physical slot.GaItems table sharing one handle. This is a DISTINCT defect // from RepairCodeDuplicateHandle (duplicate Inventory/Storage container // records): here the collision is between physical GaItem records that may // carry different ItemIDs, which the GaItem repack preflight refuses. It is // REPORT-ONLY — no safe generic auto-repair exists (which ItemID to retain is // the user's call), so its only action is no_action. RepairCodeDuplicatePhysicalHandle = "duplicate_physical_gaitem_handle" )
Issue code constants — match the codes used in backend/editor/validate.go and in the UI so the frontend can use a single stable key for each problem.
const ( RepairActionCreateCopy = "create_copy" RepairActionRemoveRecord = "remove_record" RepairActionClearAoW = "clear_aow" RepairActionPickAoW = "pick_aow" RepairActionRepairIndex = "repair_index" RepairActionFixLevel = "fix_level" RepairActionNoAction = "no_action" // RepairActionClampQuantity clamps an over-cap record down to its // authoritative effective cap (ClampInventoryQuantityAt). RepairActionClampQuantity = "clamp_quantity" )
Repair action identifiers — proposed by the scanner, executed by the apply endpoint.
const ( SectionEmptySlot = "empty_slot" SectionPreUnlockedRegs = "pre_unlocked_regions" SectionUnlockedRegs = "unlocked_regions" SectionPostUnlockedRegs = "post_unlocked_regions" SectionDLC = "dlc" SectionHash = "player_data_hash" )
Section names emitted by buildSectionMap. Stable identifiers so callers (rebuild logic, tests, diagnostics) can reference sections by name.
const ( ActiveSlotsOffset = 0x1954 // 10 × u8 active-slot flags ProfileSummaryOffset = 0x195E // ProfileSummary[i] at base + i*ProfileSummaryStride ProfileSummaryStride = 0x24C // 588 bytes per summary )
UserData10 per-slot metadata offsets (see spec/23-user-data-10.md).
const ( ItemTypeWeapon = 0x80000000 ItemTypeArmor = 0x90000000 ItemTypeAccessory = 0xA0000000 ItemTypeItem = 0xB0000000 ItemTypeAow = 0xC0000000 )
const ( // NoCustomAoWHandle is the canonical value emitted by writers when a // weapon GaItem has no custom Ash of War attached. Matches the value // the in-game save writes. NoCustomAoWHandle uint32 = 0x00000000 // LegacyNoCustomAoWHandle is the value historical SaveForge releases // (and the GaItemFull zero-value placeholder for empty slots) wrote // for the same semantic state. Readers must continue to recognize it // so previously edited saves keep working. LegacyNoCustomAoWHandle uint32 = 0xFFFFFFFF )
AoWGaItemHandle sentinel values for "no custom Ash of War attached".
Forensic comparison of in-game vanilla saves (ER0000-kro55-vanilla.sl2 and fresh fields of ER0000.sl2) versus SaveForge-bulk-edited saves shows the game writes 0x00000000 for weapons that have no external AoW gem attached; SaveForge historically wrote 0xFFFFFFFF. The game tolerates both values, but vanilla-aligned output minimizes anti-cheat/validation risk so the writer canonicalizes to NoCustomAoWHandle.
const ( SkipReasonInvalidHandle = "invalid_handle" SkipReasonNotFound = "not_found" SkipReasonEquipped = "equipped" SkipReasonDestFull = "dest_full" SkipReasonDestAtCap = "dest_at_cap" SkipReasonMissingCap = "missing_cap" SkipReasonHandleAllocFailed = "handle_alloc_failed" // SkipReasonDestDuplicate is reserved. Instance-move handles that // encounter the same handle on the destination side trigger a rehandle // path (materializeRehandledInstance) and do NOT return this reason. It // is kept as a public constant for callers that want to surface a // duplicate-handle condition in future direct-write APIs (e.g. preset // import) and for backwards compatibility with the prior transfer // semantics. The transfer core no longer emits it. SkipReasonDestDuplicate = "dest_duplicate" )
Skip reason constants.
const ( TutorialDataHeaderLen = 8 // unk0x0 + unk0x2 + size TutorialDataCountOff = 8 // u32 count after header TutorialDataIDsOff = 12 // first ID after count TutorialDataMaxIDs = 0xFF // (size - 4) / 4 = (0x400 - 4) / 4 = 255 )
TutorialData layout in slot.Data at slot.TutorialDataOffset:
+0x00 (u16): unk0x0 +0x02 (u16): unk0x2 +0x04 (u32): size — total chunk size in bytes (typically 0x400) +0x08 (u32): count — number of tutorial IDs in the list +0x0C: count × u32 tutorial IDs (TutorialParam row IDs)
When the game first triggers a tutorial popup (or first hands a tutorial-bound item like an "About *" or Crafting Kit pickup), it appends the corresponding TutorialParam row ID to this list. Subsequent triggers check the list and skip if the ID is already present — so pre-populating the list lets the editor prevent the "About item drops on ground" pickup duplicate.
Source: er-save-manager src/er_save_manager/parser/world.py (TutorialDataChunk). Verified empirically by save diff: buying Crafting Kit at Kalé added ID 2010.
const BaseVersionSize = 16
const BloodStainSize = 68
const ChrAsmEquipmentSize = ChrAsmFieldCount * 4
ChrAsmEquipmentSize is the byte size of the ChrAsmEquipment header (22 × 4 = 0x58).
const ChrAsmFieldCount = 22
ChrAsmEquipment field count — 22 u32 values per equipment section (0x58 bytes).
const DLCSectionSerializedSize = 50
const EquippedSpellEmptySentinel uint32 = 0xFFFFFFFF
EquippedSpellEmptySentinel marks an empty spell slot. Both spell_id == 0xFFFFFFFF AND follower == 0x00000000 are required by the game; mixing these is a corrupt state.
const EquippedSpellOccupiedFollower uint32 = 0xFFFFFFFF
EquippedSpellOccupiedFollower is written into the follower/unk u32 for any occupied slot. The lower-level meaning of this field (follower toggle / unk) is not relevant here; vanilla saves consistently use 0xFFFFFFFF for every occupied spell slot regardless of which spell is equipped.
const EquippedSpellSlotCount = 14
EquippedSpellSlotCount is the number of spell slots in the EquippedSpells section (memory + skills shortcuts). Mirrors the 14-slot constant used by readSpellIDs in hash.go.
const EquippedSpellSlotSize = 8
EquippedSpellSlotSize is the per-slot byte stride: spell_id u32 LE followed by follower/unk u32 LE.
const EventFlagsBlockSize = EventFlagsByteCount + 1
const EventFlagsByteCount = 0x1BF99F
EventFlagsByteCount is the fixed length of the event_flags bitfield in every save slot. Reference: tmp/repos/er-save-manager/parser/user_data_x.py:374
const FallbackMagicBase = 0x15420 + 432
FallbackMagicBase is the hardcoded base used when MagicPattern is not found.
const (
GaItemsStart = 0x20 // scan starts here
)
GaItems section. Source: SPEC.md §5.3 GaItems.
const GaitemGameDataSize = 8 + gaitemEntryCount*gaitemEntrySize // 0x1B458
const InvEquipReservedMax = 432
InvEquipReservedMax is a conservative lower bound for SaveForge-generated fresh indices. New records written by the editor should stay above this floor and above existing indices to avoid collisions with game-managed entries.
This is not a validation rule for existing records: genuine saves can contain game-created inventory/storage records with Index <= InvEquipReservedMax.
const InvUnarmedBaseID = uint32(0x0001ADB0)
InvUnarmedBaseID is the "Unarmed" placeholder weapon base itemID the game keeps as a technical inventory slot (row 110000 in EquipParamWeapon). It is a resolved technical placeholder, never an unknown item.
const MinSaveFileSize = 10*SlotSize + 0x60000
MinSaveFileSize is the minimum valid save file size: 10 slots × 0x280000 + UserData10 (0x60000).
const NetManSize = 4 + 0x20000
const PS5ActivitySize = 0x20
const PlayerCoordinatesSize = 12 + 4 + 16 + 1 + 12 + 16
const PlayerGameDataHashSize = 11*4 + 0x54 // 128
const PreEventFlagsScalarsSize = 3 + 4 + 4 + 1 + 4 + 4 + 1 + 4 + 4 // 29
const RideGameDataSize = 40
const SlotSize = 0x280000
SlotSize is the fixed size of each save slot in bytes (2,621,440 = 0x280000). Source: SPEC.md §3.1 BND4 Container.
const TrailingFixedBlockSize = WorldAreaWeatherSize + WorldAreaTimeSize + BaseVersionSize + 8 + PS5ActivitySize + DLCSectionSerializedSize
const TrophyEquipDataSize = 52
const UnsupportedContainerCode = "ERR_UNSUPPORTED_CONTAINER"
UnsupportedContainerCode is a stable, machine-matchable discriminator prefixed to ErrUnsupportedContainer's message so the frontend can reliably detect this specific failure (and show a dedicated modal) without matching prose.
const WorldAreaTimeSize = 12
const WorldAreaWeatherSize = 12
const WorldHeadSize = RideGameDataSize + 1 + BloodStainSize + 4 + 4
Variables ¶
var ErrUnsupportedContainer = fmt.Errorf("%s: this file's save format could not be identified safely (not a native PC or PS4 save). It will not be opened, to avoid writing it back in the wrong platform format. Format conversion is currently unavailable", UnsupportedContainerCode)
ErrUnsupportedContainer is returned by LoadSave when the input container is not an unambiguous native save (raw BND4 for PC, raw PS4 magic for PS4). User-facing so the frontend can explain why the file was not opened.
var MagicPattern = []byte{
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
MagicPattern matches the 192-byte pattern used in the Python editor for reliability. First block: 0x00 + 0xFFFFFFFF + 12 zeros (17 bytes) Subsequent blocks: 0xFFFFFFFF + 12 zeros (16 bytes each)
var ( // SaveKey is the standard Elden Ring PC Save AES-128-CBC key. SaveKey = []byte{0x99, 0xad, 0x2d, 0x50, 0xed, 0xf2, 0xfb, 0x01, 0xc5, 0xf3, 0xec, 0x3a, 0x2b, 0xca, 0xb6, 0x9d} )
Functions ¶
func AddItemsToSlot ¶
func AddItemsToSlot(slot *SaveSlot, itemIDs []uint32, invQty, storageQty int, forceStackable bool) error
AddItemsToSlot adds multiple items to a specific save slot. invQty and storageQty control quantities: 0 = skip, -1 = use provided max from caller, >0 = exact qty. forceStackable treats items as stackable (reuse existing GaMap handle) regardless of type. Used for arrows/bolts which have weapon-like IDs but are stackable in inventory.
Algorithm (Plan D — GaItems Section Re-serialization):
Phase 1: Allocate GaItem entries in-memory array + write GaItemData at old offsets. Phase 2: FlushGaItems — serialize array, compute size delta, single shift, update offsets. Phase 3: Add to inventory/storage (offsets now correct after flush).
func AddItemsToSlotBatch ¶
AddItemsToSlotBatch adds a batch of items with per-item qty/stackable settings. All GaItem allocations happen in Phase 1, then ONE RebuildSlotFull in Phase 2, then all inventory/storage writes in Phase 3. This is O(1) rebuilds instead of O(N).
func AppendTutorialID ¶
AppendTutorialID adds a tutorial ID to the slot's TutorialData list. Idempotent: if the ID already exists, returns nil without modification. Returns an error if the list is full (255 IDs) or offset chain failed.
func ApplyGaItemRepackPlan ¶ added in v1.5.0
func ApplyGaItemRepackPlan(slot *SaveSlot, plan GaItemRepackPlan) error
ApplyGaItemRepackPlan replaces only slot.GaItems with a private copy of the planned stable layout. It intentionally does not rebuild bytes, reparse, update cursors, or touch GaMap; those atomic transaction steps belong to the caller. A no-op plan does not replace the existing slice.
func AttachExistingWeaponAoW ¶ added in v1.1.0
AttachExistingWeaponAoW attaches an existing, free AoW GaItem (identified by its 0xC0 handle) to the weapon. Backs attach_existing_aow. Compatibility is validated against the exact weapon before mutation; PatchWeaponAoWHandle then enforces that the AoW GaItem exists and is not already referenced by another weapon, and writes exactly 4 bytes on success — so a rejection never mutates.
func ClearProfileSummaryRegion ¶ added in v1.0.0
ClearProfileSummaryRegion zeroes the FULL ProfileSummary region for slot idx in UserData10.Data — name, level, AND the opaque face/equipment snapshot (ProfileSummaryStride bytes). ProfileSummary.Serialize only rewrites name+level, so without this a deleted slot would keep the previous occupant's face/equipment snapshot, which the character-select menu could still render as a phantom.
func ClearWeaponAoW ¶ added in v1.1.0
ClearWeaponAoW removes the Ash of War from a weapon, writing the canonical no-custom sentinel in-place. Backs the clear_aow action for all three current_aow_* codes. Always legal — no compatibility check needed.
func ComputeMD5 ¶
ComputeMD5 calculates the MD5 checksum of the given data.
func ComputeSlotHash ¶
ComputeSlotHash calculates the full CSPlayerGameDataHash (0x80 bytes) for a slot. The hash block is written at SlotSize - 0x80.
The offset chain for equipment sections mirrors calculateDynamicOffsets() in structures.go, including the dynamic projSize field. This ensures hash entries [7]-[10] read from the correct positions in the slot data.
Hash entries:
[0] Level [1] Stats (with Int/Faith swapped) [2] ArcheType (Class) [3] PGD+0xB8 byte [4] padding (0) [5] Souls [6] SoulMemory [7] EquippedWeapons (10 IDs) [8] EquippedArmors (4 armor + 5 talismans = 9 IDs) [9] EquippedItems (16 quick/pouch IDs, lower 28 bits) [10] EquippedSpells (14 spell IDs) [11] padding (0)
func CreateBackup ¶
CreateBackup creates a copy of the file at the given path. The backup file is named: original_filename.YYYYMMDD_HHMMSS.bak Returns the path to the created backup file.
func CreateWeaponAoWCopy ¶ added in v1.1.0
CreateWeaponAoWCopy allocates a fresh AoW GaItem for aowItemID and attaches it to the weapon. Backs create_new_aow_copy (default for current_aow_shared). Compatibility and GaItems/GaItemData capacity are prechecked read-only before PatchWeaponAoW runs, so an incompatible AoW or a full slot reports an error with no partial mutation.
func CurrentWeaponAoWItemID ¶ added in v1.1.0
CurrentWeaponAoWItemID resolves the itemID of the Ash of War currently attached to weaponHandle, via the weapon GaItem's AoWGaItemHandle → GaMap. ok=false when the weapon is absent, carries no custom AoW, or the AoW handle is unmapped. Used by the repair apply endpoint to derive the copy source for create_new_aow_copy on a shared AoW without asking the UI to supply it.
func DecryptSave ¶
DecryptSave decrypts the AES-128-CBC encrypted payload from a PC save. The first 16 bytes of the encrypted data are used as the IV.
func EffectiveQuantityCap ¶ added in v1.1.0
func EffectiveQuantityCap(rec ResolvedRecord, containerOwned map[uint32]uint64) (limit uint64, applies bool)
EffectiveQuantityCap returns the authoritative per-record quantity cap for a resolved record. It is the single source of quantity-cap semantics for both the scanner and the clamp repair primitive, so the two can never disagree.
applies is false — and limit is 0 — for any record that carries no authoritative cap: a record that did not resolve to a DB entry (unknown / technical placeholder) or one in an unrecognised scope. Callers must not category-check or clamp such a record.
For a KnownDB record, storage uses GameMaxStorage. Inventory uses GameMaxInventory EXCEPT for pot/aromatic craftables listed in data.RequiredContainer: the game caps those by the runtime container limit, not by their raw maxNum. For those, the inventory cap is how many of the required container the slot currently owns (containerOwned[containerID]), which is 0 when the container is missing. Storage is never container-capped.
containerOwned maps containerItemID -> owned inventory quantity; pass nil when no record in scope is container-gated (the map lookup then yields 0). This deliberately does not use the conservative Normal Mode caps or scales_with_ng: editor policy and single-playthrough availability are not save-integrity truths.
A known zero is a legitimate value (item not permitted in the container, or no container owned) and still returns applies=true.
func EncryptSave ¶
EncryptSave encrypts the payload using AES-128-CBC. It returns the IV prepended to the encrypted data.
func FingerprintRecordAt ¶ added in v1.1.0
FingerprintRecordAt returns the fingerprint of the inventory/storage record currently at scope+row, so a repair apply endpoint can stale-check a target against the state captured at scan time before dispatching a primitive. ok=false when scope is unknown or row is out of range.
func FlushGaItems
deprecated
FlushGaItems serializes the entire in-memory GaItems array back to slot.Data. If the total byte size changed (e.g. empty 8B slot replaced by 21B weapon), shifts all data after the GaItems section and updates all downstream offsets.
Deprecated: this function uses an in-place data shift that overwrites the last `delta` bytes of slot.Data — including DLC section (50 B at SlotSize-0xB2) and PlayerGameDataHash (128 B at SlotSize-0x80) — whenever delta > 0x132. Use RebuildSlotFull (called from AddItemsToSlot Phase 2) instead. Kept for backward compatibility with any external callers; will be removed once no callers remain. See CHANGELOG entry for the FlushGaItems DLC+Hash overwrite post-mortem.
func GaItemRecordSize ¶
GaItemRecordSize returns the byte size of a GaItem record based on item_id. Uses item_id (not handle) for size determination, matching Rust ER-Save-Editor.
func HasTutorialID ¶
HasTutorialID returns true if the given tutorial ID is already in the list.
func IsHandleEquipped ¶ added in v1.0.0
IsHandleEquipped reports whether the given handle is referenced by any slot of ChrAsmEquipment (equipped weapons / armor / talismans / arrows). The equipped block stores item-form IDs, so the check matches against multiple candidate representations of the handle:
- handle itself (defensive — some saves may store handles directly)
- GaMap[handle] (true item ID for weapons, armor, AoW)
- lower 28 bits with item-ID prefix (talismans 0xA0→0x20, goods 0xB0→0x40)
Returns false when EquipItemsIDOffset is not parsed.
func IsNoCustomAoWHandle ¶ added in v1.0.0
IsNoCustomAoWHandle reports whether the given AoWGaItemHandle field value means "no external Ash of War attached" — covering both the canonical vanilla sentinel (0x00000000) and the legacy SaveForge sentinel (0xFFFFFFFF). Use this in every reader and availability scan; any check that compares against a single sentinel will misclassify half of the save population.
func IssueKeyID ¶ added in v1.1.0
IssueKeyID computes the stable issueID for a given IssueKey (hex SHA-256[:8]).
func PatchEquippedSpell ¶ added in v1.0.0
PatchEquippedSpell writes a single spell slot in the EquippedSpells section of slot.Data.
spellID is the raw MagicParam ID (e.g. Catch Flame = 0x1770), NOT a full item ID (the 0x40XXXXXX / 0x60XXXXXX prefixed form used elsewhere in the save). Passing EquippedSpellEmptySentinel (0xFFFFFFFF) clears the slot.
Semantics:
spellID == 0xFFFFFFFF → write (spell_id=0xFFFFFFFF, follower=0x00000000) spellID != 0xFFFFFFFF → write (spell_id=spellID, follower=0xFFFFFFFF)
Out of scope: this writer does NOT touch the slot hash block. Callers that want the in-save hash refreshed must invoke ComputeSlotHash separately, the same way other low-level core writers (PatchWeaponItemID, etc.) leave the hash update to the apply layer.
Errors are returned WITHOUT mutating slot.Data. Idempotent writes (target bytes already match) are skipped.
func PatchNetworkParams ¶
func PatchNetworkParams(ud11 []byte, patch NetworkParamValues) ([]byte, error)
PatchNetworkParams modifies NetworkParam in UserData11 and returns the patched UserData11.
func PatchWeaponAoW ¶ added in v1.0.0
PatchWeaponAoW sets or removes the Ash of War attached to a weapon GaItem.
newAoWItemID == 0: removes AoW — patches AoWGaItemHandle to the canonical NoCustomAoWHandle (0x00000000) in-place. No GaItem allocation, no RebuildSlotFull.
newAoWItemID != 0: allocates a fresh AoW GaItem (never reuses an existing handle — sharing an AoW handle between two weapons causes EXCEPTION_ACCESS_VIOLATION), upserts GaItemData, calls RebuildSlotFull + parseFromData, then patches the weapon's AoWGaItemHandle field after the rebuild settles offsets.
Old AoW GaItems are intentionally left in place — the game tolerates orphaned entries.
This is the allocate path: it mints a fresh AoW GaItem rather than reusing an existing handle. It is invoked by the active workspace save flow (backend/editor/save.go) when an AoW change requires a new record; the in-place strict path (PatchWeaponAoWHandle) handles reuse of a pre-existing free copy.
func PatchWeaponAoWHandle ¶ added in v1.0.0
PatchWeaponAoWHandle patches the AoWGaItemHandle field of a weapon GaItem in-place. newAoWHandle indicates no-custom-AoW (per IsNoCustomAoWHandle): removes AoW attachment. Always allowed; the canonical NoCustomAoWHandle value is written regardless of which sentinel the caller supplied — keeps disk output vanilla-aligned even when legacy callers still pass 0xFFFFFFFF. newAoWHandle is a valid 0xC0... handle: attaches an existing AoW GaItem — validates that the handle identifies an existing AoW GaItem and is not already referenced by a different weapon. No GaItem allocation, no RebuildSlotFull — exactly 4 bytes at [weaponOff+16] are overwritten.
func PatchWeaponItemID ¶ added in v1.0.0
PatchWeaponItemID changes the ItemID of a single weapon GaItem in-place. Both IDs must be in the weapon range (prefix 0x00000000 → record size 21 B). Since the record size doesn't change, no RebuildSlotFull is needed — we overwrite exactly 4 bytes at the ItemID field and update derived state.
Caller guarantees:
- handle identifies one weapon instance in slot.GaItems / slot.GaMap
- expectedCurrentItemID matches what the slot currently stores (stale-data guard)
- newItemID encodes the same base weapon + same upgrade level, different infusion
func PruneBackups ¶
PruneBackups removes oldest timestamped backups for a given file, keeping at most max versions. Backup filenames are expected to match the pattern: <path>.<YYYYMMDD_HHMMSS>.bak
func ReadTutorialIDs ¶
ReadTutorialIDs returns the list of tutorial IDs currently registered in the slot's TutorialData block. Returns empty slice + error if offset is invalid.
func RebuildSlot ¶
RebuildSlot serializes a SaveSlot into a fresh 0x280000-byte buffer.
Sequential rebuild strategy (Option B / R-1 final):
- Copy bytes [0, UnlockedRegionsOffset) verbatim (pre-regions blob).
- Reserialize `unlocked_regions` from slot.UnlockedRegions (count u32 + N×u32). This is the only section whose size may change.
- Re-parse every section after `unlocked_regions` from slot.Data starting at the *original* regions end, then write each one back via its typed Write method. Sections written: WorldHead, MenuSaveLoad, TrophyEquipData, GaitemGameData, TutorialData, PreEventFlagsScalars, EventFlagsBlock, WorldGeomBlock, PlayerCoordinates, SpawnPointBlock, NetMan, TrailingFixedBlock (weather/time/base/steam/ps5/dlc), PlayerGameDataHash.
- Pad the remainder of the slot with zeros up to SlotSize. This tail padding absorbs the unlocked_regions delta on saves that have slack (PC saves observed with ~419KB rest; PS4 saves with 0).
For an unmodified slot this produces byte-for-byte identical output to slot.Data. For a mutated UnlockedRegions slice it produces a save where every other section retains its original bytes, only the regions block shifts size, and the tail rest absorbs the delta.
Reference: tmp/repos/er-save-manager/src/er_save_manager/parser/slot_rebuild.py
func RebuildSlotFull ¶
RebuildSlotFull rebuilds the slot from scratch, including a fresh GaItems section serialized from slot.GaItems. This replaces the in-place data shift in FlushGaItems which overwrote the last `delta` bytes of slot.Data — the DLC section (50 B) and PlayerGameDataHash (128 B) — whenever GaItems grew by more than 0x132 bytes.
Layout produced (all written into a fresh SlotSize buffer):
- Header : slot.Data[0:GaItemsStart] (32 bytes)
- GaItems : serialized slot.GaItems (variable)
- PreRegs : slot.Data[oldGaLimit:slot.UnlockedRegionsOffset] (variable, verbatim)
- Regions : count u32 + N×u32 (variable)
- PostRegs: WorldHead..Hash (typed Write sequence) (~263 KB)
- TailPad : zeros up to SlotSize
DLC + PlayerGameDataHash are preserved because they're written via TrailingFixedBlock.Write / PlayerGameDataHash.Write from struct fields parsed at slot.Read() time. As long as those parsed values are intact in the in-memory SaveSlot, the rebuild restores them at the correct fixed end-of-slot positions.
Caller responsibility: after calling this function and copying the result over slot.Data, the caller MUST refresh derived state via:
slot.calculateDynamicOffsets(); slot.mapInventory(); slot.buildSectionMap()
Reference: tmp/repos/ER-Save-Editor/src/save/common/save_slot.rs (Rust reference uses a similar full rebuild on every mutation).
func RecalculateSlotHash ¶
func RecalculateSlotHash(slot *SaveSlot)
RecalculateSlotHash computes and writes the CSPlayerGameDataHash into slot data.
func ReconcileInventoryHeader ¶
func ReconcileInventoryHeader(slot *SaveSlot)
ReconcileInventoryHeader sets the held-inventory common_item_count header to the actual number of non-empty common item slots. Mirrors ReconcileStorageHeader. Call this after loading a save that was edited by another tool (er-save-manager, Rust ER-Save-Editor) to guarantee the counter matches what the game will see.
func ReconcileStorageHeader ¶
func ReconcileStorageHeader(slot *SaveSlot)
ReconcileStorageHeader sets the storage header count to the actual number of non-empty items in the storage array. Fixes mismatch from blind +1 increments in addToInventory when the header was already wrong.
func RemoveInventoryRecordAt ¶ added in v1.1.0
RemoveInventoryRecordAt deletes exactly ONE inventory or storage record, identified structurally by scope + row (matching the RepairIssue IssueKey) plus a fingerprint stale-check.
Unlike RemoveItemFromSlot — which zeroes EVERY record sharing a handle — this targets a single record by position. Duplicate talisman copies (0xA0) and any other record that happens to share the same handle in a different row are left untouched.
scope is one of the repairScope* constants (inventory_common, inventory_key, storage_common). row is the index into the matching in-memory list exactly as the scanner produced it. fingerprint must equal fingerprintInventoryItem of the record currently at that row; a mismatch means the slot changed since the scan, so the removal is refused with no mutation.
GaItem records are intentionally NOT garbage-collected: another record may still reference the same handle/GaItem. Call RepairOrphanedGaItems separately when GC is explicitly wanted.
func RemoveItemByBaseID ¶
RemoveItemByBaseID removes an item from inventory by its base item ID (e.g. 0x40002198). Editor-added goods use computed handle (e.g. 0xB0XXXXXX for 0x40XXXXXX items). Game-placed key items in the KeyItems section use the raw item ID as handle.
func RemoveItemFromSlot ¶
RemoveItemFromSlot zeroes out inventory/storage slots for the given handle. Inventory: fixed pre-allocated array — zero the matching slot(s). Storage: dynamic list — zero the matching slot(s); game stops reading at handle==0. GaMap entry is removed only when the handle is absent from both lists after removal.
func RepairDLCSection ¶ added in v1.0.0
RepairDLCSection zeros the reserved trailing bytes (indices 3–49) of the DLC section. Returns true if any byte was changed.
func RepairDuplicateWondrousPhysick ¶ added in v1.0.0
func RepairGaItemDataCount ¶ added in v1.0.0
RepairGaItemDataCount caps the GaItemData count header to the allowed maximum. Returns true if the value was changed.
func RepairGaItemDuplicate ¶ added in v1.5.0
RepairGaItemDuplicate removes only the unselected physical GaItem record for a verified duplicate pair, preserving the user-selected record unchanged. It runs as one transaction: any failed postcondition restores the complete slot, so a refused or failed call leaves the slot byte-for-byte and structurally unchanged. It never saves a file, never creates a backup, and never runs a repack.
func RepairOrphanedGaItems ¶
RepairOrphanedGaItems clears GaItem records whose handles are not present in either held inventory or storage. These orphans accumulate when RemoveItemFromSlot zeros the inventory slot but does not clear the backing GaItem binary record — scanGaItems() then re-adds them to GaMap on every load.
Returns the number of entries cleared.
func RepairSlot ¶ added in v1.0.0
RepairSlot applies all available automated repairs to the slot. Returns lists of what was fixed and what was skipped (unrepairable).
func RepairStats ¶ added in v1.0.0
RepairStats clamps player level and attributes to valid game ranges. Calls SyncPlayerToData on any change. Returns list of applied fixes.
func RepairStorageCountHeader ¶ added in v1.0.0
RepairStorageCountHeader recalculates the storage box item count header. Returns true if the header was corrected.
func RestoreSlot ¶
func RestoreSlot(slot *SaveSlot, snap SlotSnapshot)
RestoreSlot overwrites all mutable slot state from a snapshot.
func ScanRepairIssuesWithCoverage ¶ added in v1.1.0
func ScanRepairIssuesWithCoverage(slotIndex int, slot *SaveSlot, records []ResolvedRecord) ([]RepairIssue, ValidationCoverage)
ScanRepairIssuesWithCoverage runs the full scan over a pre-resolved record collection and returns both the issues and a coverage report whose StructuralChecksApplied reflects the records the structural scanner actually processed — not a count the coverage builder assumed. This is the pipeline entry point: it guarantees structural coverage is only reported AFTER the scanner has run. Read-only.
func SetUnlockedRegions ¶
SetUnlockedRegions replaces the slot's unlocked-regions list with the given IDs (deduplicated, sorted ascending), rebuilds the slot data via RebuildSlot, and refreshes dynamic offsets so subsequent reads/writes see the new layout.
This is the write entry point for the Invasion Regions feature. The rebuild path means the call always succeeds regardless of how much "slack" exists at the slot tail (full struct rebuild + tail zero pad).
func UTF16ToString ¶
func ValidateNetworkParams ¶
func ValidateNetworkParams(p NetworkParamValues) error
ValidateNetworkParams checks all field boundaries. Returns nil if valid.
func ValidateSlotIntegrity ¶
ValidateSlotIntegrity performs write-ahead validation on a slot before saving. It re-checks the offset chain, inventory bounds, data length and stat sanity to prevent writing a corrupted save file.
Types ¶
type AoWCopyRaw ¶ added in v1.0.0
type AoWCopyRaw struct {
ItemID uint32 // AoW item ID (upper nibble 0x8)
Handle uint32 // AoW GaItem handle (upper nibble 0xC)
UsedByWeaponHandle uint32 // 0 if this copy is free; weapon handle if attached
}
AoWCopyRaw is one Ash of War GaItem found in the save slot.
func ScanAoWAvailability ¶ added in v1.0.0
func ScanAoWAvailability(slot *SaveSlot) []AoWCopyRaw
ScanAoWAvailability scans slot.GaItems and returns one AoWCopyRaw per AoW GaItem found.
Pass 1 collects every AoW GaItem (handle prefix 0xC0000000) and every weapon's AoWGaItemHandle reference. Pass 2 cross-references them to determine which copies are free and whether any handle is shared by multiple weapons (save corruption indicator).
Assumptions:
- One AoW itemID may have multiple copies (different handles).
- A handle must not be shared between two weapons; if it is, both copies are flagged.
- Only entries where !g.IsEmpty() are considered.
type BaseVersion ¶
type BaseVersion struct {
BaseVersionCopy uint32
BaseVersion uint32
IsLatestVersion uint32
Unk0xc uint32
}
BaseVersion — 16 bytes (4×u32).
func (*BaseVersion) Read ¶
func (b *BaseVersion) Read(r *Reader) error
func (*BaseVersion) Write ¶
func (b *BaseVersion) Write(sw *SectionWriter)
type BloodStain ¶
type BloodStain struct {
Coordinates FloatVector3
Angle FloatVector4
Unk1c uint32
Unk20 uint32
Unk24 uint32
Unk28 uint32
Unk2c uint32
Unk30 int32
Runes int32
MapID MapID
Unk3c uint32
Unk38 uint32
}
BloodStain — death drop. 0x44 = 68 bytes. Layout: coordinates(12) | angle(16) | 5×u32 | i32×2 | map_id(4) | u32×2 Reference: tmp/repos/er-save-manager/parser/world.py:BloodStain
func (*BloodStain) Read ¶
func (b *BloodStain) Read(r *Reader) error
func (*BloodStain) Write ¶
func (b *BloodStain) Write(w *SectionWriter)
type CSMenuSystemSaveLoad ¶
type CSMenuSystemSaveLoad struct {
Data []byte
}
func (*CSMenuSystemSaveLoad) Read ¶
func (c *CSMenuSystemSaveLoad) Read(r *Reader)
type CapacityReport ¶
type CapacityReport struct {
CanFitAll bool
CapHit string // "" | "inventory_full" | "storage_full" | "gaitem_full" | "gaitemdata_full"
FreeInv int
FreeStorage int
FreeGaItems int
FreeGaItemCursor int // allocator cursor room: len(GaItems) - NextArmamentIndex
FreeGaItemData int
NeededInv int
NeededStorage int
NeededGaItems int
NeededGaItemData int
}
CapacityReport describes why items don't fit.
func CheckAddCapacity ¶
func CheckAddCapacity(slot *SaveSlot, items []ItemToAdd) CapacityReport
CheckAddCapacity verifies that ALL items can be added without exceeding any container limit. Returns a report indicating whether everything fits.
type DLCSection ¶
type DLCSection struct {
PreorderTheRing uint8
ShadowOfErdtreeFlag uint8 // SotE entry flag (non-zero = entered DLC)
PreorderRingOfMiquella uint8
Unused [47]byte
}
DLCSection — 50 bytes. Layout: pre_order_the_ring u8 | shadow_of_erdtree u8 |
pre_order_ring_of_miquella u8 | unused [47]byte
func (*DLCSection) Read ¶
func (d *DLCSection) Read(r *Reader) error
func (*DLCSection) Write ¶
func (d *DLCSection) Write(sw *SectionWriter)
type DiagnosticIssue ¶
type DiagnosticIssue struct {
Severity DiagnosticSeverity `json:"severity"`
Category string `json:"category"` // "offset_chain", "gaitem", "inventory", "stats", "dlc"
Description string `json:"description"`
}
DiagnosticIssue represents a single corruption finding.
type DiagnosticSeverity ¶
type DiagnosticSeverity string
DiagnosticSeverity classifies corruption findings.
const ( SeverityCritical DiagnosticSeverity = "critical" // save will crash the game SeverityWarning DiagnosticSeverity = "warning" // save may behave unexpectedly SeverityInfo DiagnosticSeverity = "info" // observation, not necessarily harmful )
type DuplicateInventoryIndexIssue ¶ added in v1.0.0
type DuplicateInventoryIndexIssue struct {
Index uint32 `json:"index"`
Scope string `json:"scope"` // "inventory_common" | "inventory_key"
FirstRow int `json:"firstRow"`
FirstHandle uint32 `json:"firstHandle"`
DuplicateRow int `json:"duplicateRow"`
DuplicateHandle uint32 `json:"duplicateHandle"`
}
DuplicateInventoryIndexIssue describes a single Index collision discovered by ScanDuplicateInventoryIndices. Used by pre-flight guards to abort mutations on already-corrupt saves with a precise diagnostic instead of a misleading post-mutation rollback.
func ScanDuplicateInventoryIndices ¶ added in v1.0.0
func ScanDuplicateInventoryIndices(slot *SaveSlot) []DuplicateInventoryIndexIssue
ScanDuplicateInventoryIndices walks Inventory.CommonItems and Inventory.KeyItems and reports every Index value that appears more than once across the combined inventory list. Empty / invalid handles are ignored. Storage is not scanned — duplicate post-mutation validation only covers inventory.
Read-only: never modifies slot. Safe to call before snapshot/mutation as a pre-flight guard.
type EquipInventoryData ¶
type EquipInventoryData struct {
CommonItems []InventoryItem
KeyItems []InventoryItem
NextEquipIndex uint32
NextAcquisitionSortId uint32
// contains filtered or unexported fields
}
func (*EquipInventoryData) Clone ¶
func (e *EquipInventoryData) Clone() EquipInventoryData
Clone returns a deep copy of EquipInventoryData, including unexported offset fields.
func (*EquipInventoryData) NextEquipIndexOff ¶
func (e *EquipInventoryData) NextEquipIndexOff() int
NextEquipIndexOff returns the absolute byte offset of NextEquipIndex in slot.Data. Used by tests to exclude intentionally-corrected bytes from round-trip comparison.
func (*EquipInventoryData) Read ¶
func (e *EquipInventoryData) Read(r *Reader, commonCount, keyCount int) error
func (*EquipInventoryData) ReadStorage ¶
func (e *EquipInventoryData) ReadStorage(r *Reader, count int) error
type EquipmentSlotKind ¶ added in v1.0.0
type EquipmentSlotKind int
EquipmentSlotKind identifies a writable equipment slot within ChrAsmEquipment.
Phase 7b.0 — backend-only foundation for weapon/ammo slots (0–9, hash 7) and armor slots (12–15, hash 8). Phase 7c — extends the writer to talisman slots (17–21, hash 8). Talisman5 (index 21) accepts only the clear sentinel because vanilla Elden Ring caps the Talisman Pouch at 4 active slots; non-empty Talisman5 writes are rejected by the resolver at the apply layer (see app_templates_v2_apply.go). The unknown slots 10/11/16 and EquippedGreatRune remain out of scope.
const ( EquipSlotLeftHandArmament1 EquipmentSlotKind = iota EquipSlotRightHandArmament1 EquipSlotLeftHandArmament2 EquipSlotRightHandArmament2 EquipSlotLeftHandArmament3 EquipSlotRightHandArmament3 EquipSlotArrows1 EquipSlotBolts1 EquipSlotArrows2 EquipSlotBolts2 EquipSlotHead EquipSlotChest EquipSlotArms EquipSlotLegs EquipSlotTalisman1 EquipSlotTalisman2 EquipSlotTalisman3 EquipSlotTalisman4 EquipSlotTalisman5 )
type EquipmentWrite ¶ added in v1.0.0
type EquipmentWrite struct {
Slot EquipmentSlotKind
Handle uint32
}
EquipmentWrite is one entry in a WriteEquipment batch. Handle == 0 clears the slot (writes 0xFFFFFFFF).
type EventFlagsBlock ¶
EventFlagsBlock — fixed-size 0x1BF99F bitfield + 1-byte terminator.
func (*EventFlagsBlock) Read ¶
func (e *EventFlagsBlock) Read(r *Reader) error
func (*EventFlagsBlock) Write ¶
func (e *EventFlagsBlock) Write(w *SectionWriter)
type FloatVector3 ¶
type FloatVector3 struct {
X, Y, Z float32
}
FloatVector3 — 12 bytes (3×f32, little-endian).
func (*FloatVector3) Read ¶
func (v *FloatVector3) Read(r *Reader) error
func (*FloatVector3) Write ¶
func (v *FloatVector3) Write(w *SectionWriter)
type FloatVector4 ¶
type FloatVector4 struct {
X, Y, Z, W float32
}
FloatVector4 — 16 bytes (4×f32). Used for quaternion-style angles in the save format despite the name; we keep the 4 components as-is.
func (*FloatVector4) Read ¶
func (v *FloatVector4) Read(r *Reader) error
func (*FloatVector4) Write ¶
func (v *FloatVector4) Write(w *SectionWriter)
type GaItemCapacity ¶ added in v1.5.0
GaItemCapacity describes the usable GaItem allocation capacity at one point in time. PhysicalEmpty is the number of empty records in the table, while CursorRoom is what the current allocator cursor can reach. Usable is always the smaller of those two values.
type GaItemDuplicateAnalysis ¶ added in v1.5.0
type GaItemDuplicateAnalysis struct {
Handle uint32
Candidates [2]GaItemDuplicateCandidate
Repairable bool
RefusalCode string
RefusalMsg string
}
GaItemDuplicateAnalysis is the read-only preflight for one requested duplicate physical GaItem handle. Repairable is true only when every safety condition holds; the caller must still pick which physical index to keep. This analysis never mutates the slot and never picks a candidate on the user's behalf.
func AnalyzeGaItemDuplicate ¶ added in v1.5.0
func AnalyzeGaItemDuplicate(slot *SaveSlot, handle uint32) GaItemDuplicateAnalysis
AnalyzeGaItemDuplicate inspects one requested handle and decides whether a single, unambiguous physical duplicate pair can be safely deduplicated. It is fail-closed: any missing condition or ambiguity returns Repairable=false with a stable RefusalCode and never mutates slot. It deliberately makes no choice between the two candidates.
type GaItemDuplicateCandidate ¶ added in v1.5.0
GaItemDuplicateCandidate is one physical GaItem record that shares a handle with another physical record. The two candidates always carry different ItemID values; which one to keep is a decision only the user can make.
type GaItemFull ¶
type GaItemFull struct {
Handle uint32
ItemID uint32
Unk2 int32 // weapon/armor: default -1 (0xFFFFFFFF)
Unk3 int32 // weapon/armor: default -1 (0xFFFFFFFF)
AoWGaItemHandle uint32 // weapon only: NoCustomAoWHandle when no AoW attached (see IsNoCustomAoWHandle for compat)
Unk5 uint8 // weapon only: default 0
}
GaItemFull represents a complete GaItem entry with all variable-length fields. Size depends on item_id type: weapon=21B, armor=16B, everything else=8B. Matches Rust ER-Save-Editor save_slot.rs GaItem struct.
func (*GaItemFull) ByteSize ¶
func (g *GaItemFull) ByteSize() int
ByteSize returns the serialized byte size of this entry.
func (*GaItemFull) IsEmpty ¶
func (g *GaItemFull) IsEmpty() bool
IsEmpty returns true if this GaItem slot is unused.
func (*GaItemFull) Serialize ¶
func (g *GaItemFull) Serialize(buf []byte) int
Serialize writes the GaItem entry into buf and returns bytes written. buf must have at least g.ByteSize() bytes available.
type GaItemRepackAnalysis ¶ added in v1.5.0
type GaItemRepackAnalysis struct {
Before GaItemCapacity
ProjectedAfter GaItemCapacity
Recovered int
NonEmptyRecords int
}
GaItemRepackAnalysis is a read-only forecast of stable GaItem compaction. It deliberately makes no safety decision: validation/refusal belongs to the repack pre-flight. The caller can use this report to decide whether there is capacity to recover before any mutation is attempted.
func AnalyzeGaItemRepack ¶ added in v1.5.0
func AnalyzeGaItemRepack(slot *SaveSlot) GaItemRepackAnalysis
AnalyzeGaItemRepack calculates the capacity effect of the canonical stable compaction without modifying slot. Non-empty records preserve their physical order; after compaction scanGaItems would place the max-counter record at its compacted position, which determines the projected allocator cursor.
type GaItemRepackBlocker ¶ added in v1.5.0
type GaItemRepackBlocker struct {
Code string
Message string
Handle uint32
// contains filtered or unexported fields
}
GaItemRepackBlocker is one fail-closed reason why a GaItem repack cannot be attempted. Code is stable for callers; Message is concise and user-facing. Handle carries the offending GaItem handle for blockers that identify one structurally (currently only "duplicate_handle" for a physical GaItem duplicate); it is 0 for blockers that do not name a handle.
type GaItemRepackPlan ¶ added in v1.5.0
type GaItemRepackPlan struct {
GaItems []GaItemFull
NonEmptyRecords int
Changes bool
}
GaItemRepackPlan is the deterministic in-memory layout produced by stable compaction. It is valid only for the exact, already preflighted slot from which it was built; the transaction layer is responsible for checking freshness before applying it to a candidate slot.
func BuildGaItemRepackPlan ¶ added in v1.5.0
func BuildGaItemRepackPlan(slot *SaveSlot) GaItemRepackPlan
BuildGaItemRepackPlan creates the canonical stable-compaction layout without modifying slot. It preserves every field and the relative order of non-empty records, then leaves a zero-value empty suffix of the original table length. Callers must run PreflightGaItemRepack before using the plan.
type GaItemRepackPreflight ¶ added in v1.5.0
type GaItemRepackPreflight struct {
Analysis GaItemRepackAnalysis
Blockers []GaItemRepackBlocker
}
GaItemRepackPreflight is the read-only result of checking whether a slot is safe to repack. Analysis is populated only when all refusal gates pass.
func PreflightGaItemRepack ¶ added in v1.5.0
func PreflightGaItemRepack(slot *SaveSlot) GaItemRepackPreflight
PreflightGaItemRepack validates only invariants required to preserve a slot during stable GaItem compaction. It never repairs or mutates slot. Structural and record-identity failures stop later phases; reference failures are aggregated in deterministic order for a useful refusal report.
type GaItemRepackResult ¶ added in v1.5.0
type GaItemRepackResult struct {
Before GaItemCapacity
After GaItemCapacity
Recovered int
Changed bool
}
GaItemRepackResult describes a completed repack attempt. Repack never writes a save file; it only updates the passed in-memory candidate slot.
func RepackGaItems ¶ added in v1.5.0
func RepackGaItems(slot *SaveSlot) (GaItemRepackResult, error)
RepackGaItems applies the canonical stable compaction to slot as one transaction. A caller that must keep its live slot untouched should pass a CloneSlot candidate and publish that candidate only after success.
RepackGaItems refuses unsafe input before mutating it. Any error after the first mutation restores the complete SlotSnapshot, so the passed slot is left unchanged. It does not save a file or create a backup.
type GaitemGameData ¶
type GaitemGameData struct {
Count int64
Entries [gaitemEntryCount]GaitemGameDataEntry
}
GaitemGameData — header (i64 count) + 7000 entries × 16 bytes = 0x1B458 (112,008 bytes).
func (*GaitemGameData) Read ¶
func (g *GaitemGameData) Read(r *Reader) error
func (*GaitemGameData) Write ¶
func (g *GaitemGameData) Write(w *SectionWriter)
type GaitemGameDataEntry ¶
type GaitemGameDataEntry struct {
ID uint32
Unk0x4 uint8
Pad0x5 [3]byte
NextItemID uint32
Unk0xc uint8
Pad0x0d [3]byte
}
GaitemGameDataEntry — 16 bytes per entry. Layout: id u32 | unk0x4 u8 | pad0x5 [3]byte | next_item_id u32 | unk0xc u8 | pad0x0d [3]byte
func (*GaitemGameDataEntry) Read ¶
func (e *GaitemGameDataEntry) Read(r *Reader) error
func (*GaitemGameDataEntry) Write ¶
func (e *GaitemGameDataEntry) Write(w *SectionWriter)
type IdentityClass ¶ added in v1.1.0
type IdentityClass string
IdentityClass classifies how a record's identity is derived. It is orthogonal to Resolution — a record's class is known even when it fails DB resolution.
const ( IdentityInstanceBacked IdentityClass = "instance_backed" // weapon/armor/aow with a per-instance GaItem IdentityHandleEncoded IdentityClass = "handle_encoded" // goods/talismans (itemID derived from handle) IdentityStackableAmmo IdentityClass = "stackable_ammo" // arrows/bolts — weapon prefix, but stackable IdentityTechnicalPlaceholder IdentityClass = "technical_placeholder" // Unarmed, naked armor IdentityUnknown IdentityClass = "unknown" // unresolved )
type IntegrityError ¶
IntegrityError describes a single post-mutation invariant violation.
func ValidatePostMutation ¶
func ValidatePostMutation(slot *SaveSlot) []IntegrityError
ValidatePostMutation performs fast invariant checks after a slot mutation. Only checks crash-causing conditions — not full diagnostic scan. Returns nil if all checks pass, or a slice of violations.
Fail-closed: every duplicate acquisition Index across Inventory.CommonItems + KeyItems is reported. Callers run a fail-closed pre-flight (ScanDuplicateInventoryIndices) before mutating and refuse to proceed when the slot already holds duplicates, so any duplicate observed here was introduced by the mutation itself and must roll back.
func (IntegrityError) Error ¶
func (e IntegrityError) Error() string
type InventoryIndexRepairChange ¶ added in v1.0.0
type InventoryIndexRepairChange struct {
Scope string `json:"scope"` // "inventory_common" | "inventory_key"
Row int `json:"row"`
Handle uint32 `json:"handle"`
OldIndex uint32 `json:"oldIndex"`
NewIndex uint32 `json:"newIndex"`
}
InventoryIndexRepairChange describes one Index reassignment performed by RepairDuplicateInventoryIndices. NewIndex is guaranteed unique across the combined Inventory.CommonItems + KeyItems set after the repair completes.
func AssignFreshInventoryIndex ¶ added in v1.1.0
func AssignFreshInventoryIndex(slot *SaveSlot, scope string, row int) (InventoryIndexRepairChange, error)
AssignFreshInventoryIndex assigns a new, safe acquisition index to exactly one inventory record identified by scope + row. The new index is:
- greater than all existing indices across inventory_common and inventory_key;
- greater than InvEquipReservedMax, as a conservative floor for newly generated editor indices;
- unique within the combined index space.
Both the in-memory InventoryItem and the raw slot.Data bytes are updated. NextAcquisitionSortId is advanced if the new index exceeds the current value. NextEquipIndex is never touched (see CE-108255-1).
Scope must be "inventory_common" or "inventory_key". This primitive is the building block for both duplicate-index repair and single-record index repair; the batch RepairDuplicateInventoryIndices uses its own counter loop for efficiency.
type InventoryIndexRepairReport ¶ added in v1.0.0
type InventoryIndexRepairReport struct {
Changed int `json:"changed"`
Changes []InventoryIndexRepairChange `json:"changes"`
}
InventoryIndexRepairReport is the outcome of one repair invocation. Changed == 0 means the slot was already clean (no-op, idempotent).
func RepairDuplicateInventoryIndices ¶ added in v1.0.0
func RepairDuplicateInventoryIndices(slot *SaveSlot) (InventoryIndexRepairReport, error)
RepairDuplicateInventoryIndices reassigns the Index of every duplicate occurrence in Inventory.CommonItems + KeyItems so that all non-empty entries share a globally-unique Index. The first occurrence of each value is kept; every subsequent occurrence gets a fresh Index > all existing values, taken from a counter seeded at max(NextAcquisitionSortId, max(existing Index)+1).
Updates both the in-memory InventoryItem and the matching uint32 in slot.Data so a subsequent WriteSave (or any direct raw read) sees the corrected Index. Also advances NextAcquisitionSortId / NextEquipIndex to stay > all assigned indices, with the matching slot.Data counters written back when their offsets are known.
Read scope is identical to ScanDuplicateInventoryIndices: empty / invalid handles are ignored, storage is not touched.
Idempotent: a second call on the repaired slot returns Changed=0.
type InventoryItem ¶
type IssueKey ¶ added in v1.1.0
type IssueKey struct {
Slot int `json:"slot"`
Domain string `json:"domain"`
Code string `json:"code"`
Scope string `json:"scope"`
Row int `json:"row"`
Handle uint32 `json:"handle"`
Field string `json:"field,omitempty"`
Value string `json:"value,omitempty"`
}
IssueKey uniquely and structurally identifies one repair issue. issueID = hex(SHA-256(canonical JSON of IssueKey)[:8]) — 16 hex chars. debugKey is the human-readable pipe-separated form for logs only; never parse it.
type ItemToAdd ¶
ItemToAdd describes a single item intended for batch addition. How it consumes slot resources (merge vs per-copy records, GaItem or not) is derived from the item ID via classifyItemAdd — callers must not pre-classify it. ForceStackable is the one explicit override: it marks weapon-prefixed ammo (arrows) that stack.
type MapID ¶
type MapID [4]byte
MapID — 4 raw bytes encoding (area, block, x, y) in some order. The game treats them as opaque tuples; we store the bytes verbatim.
func (*MapID) Write ¶
func (m *MapID) Write(w *SectionWriter)
type MenuSaveLoad ¶
type MenuSaveLoad struct {
Unk0x0 uint16
Unk0x2 uint16
Data []byte // serialized length is len(Data); Size header is rewritten on Write
}
MenuSaveLoad — variable-size menu profile data block. Layout: unk0x0 u16 | unk0x2 u16 | size u32 | data[size] Reference: tmp/repos/er-save-manager/parser/world.py:MenuSaveLoad
func (*MenuSaveLoad) ByteSize ¶
func (m *MenuSaveLoad) ByteSize() int
ByteSize returns the serialized length: 8-byte header + payload.
func (*MenuSaveLoad) Read ¶
func (m *MenuSaveLoad) Read(r *Reader) error
func (*MenuSaveLoad) Write ¶
func (m *MenuSaveLoad) Write(w *SectionWriter)
type NetMan ¶
NetMan — network manager block, 131,076 bytes total (0x20004). Layout: unk0x0 u32 | data [0x20000]byte Reference: tmp/repos/er-save-manager/parser/world.py:NetMan
func (*NetMan) Write ¶
func (n *NetMan) Write(w *SectionWriter)
type NetworkParamValues ¶
type NetworkParamValues struct {
// --- Invader role ---
MaxBreakInTargetListCount int32 `json:"maxBreakInTargetListCount"`
BreakInRequestIntervalTimeSec float32 `json:"breakInRequestIntervalTimeSec"`
BreakInRequestTimeOutSec float32 `json:"breakInRequestTimeOutSec"`
BreakInRequestAreaCount int32 `json:"breakInRequestAreaCount"`
// --- Summon Host (host waiting for phantom) ---
SummonTimeoutTime float32 `json:"summonTimeoutTime"`
// --- Cooperator role (summon signs) ---
ReloadSignIntervalTime2 float32 `json:"reloadSignIntervalTime2"`
ReloadSignTotalCount int32 `json:"reloadSignTotalCount"`
ReloadSignCellCount int32 `json:"reloadSignCellCount"`
UpdateSignIntervalTime float32 `json:"updateSignIntervalTime"`
SingGetMax int32 `json:"singGetMax"`
SignDownloadSpan float32 `json:"signDownloadSpan"`
SignUpdateSpan float32 `json:"signUpdateSpan"`
// --- Blue role (Blue Cipher Ring) ---
ReloadVisitListCoolTime float32 `json:"reloadVisitListCoolTime"`
MaxCoopBlueSummonCount int32 `json:"maxCoopBlueSummonCount"`
MaxVisitListCount int32 `json:"maxVisitListCount"`
ReloadSearchCoopBlueMin float32 `json:"reloadSearchCoopBlueMin"`
ReloadSearchCoopBlueMax float32 `json:"reloadSearchCoopBlueMax"`
AllAreaSearchRateCoopBlue int32 `json:"allAreaSearchRateCoopBlue"`
AllAreaSearchRateVsBlue int32 `json:"allAreaSearchRateVsBlue"`
// --- Host role (Taunter's Tongue / visitor) ---
VisitorListMax int32 `json:"visitorListMax"`
VisitorTimeOutTime float32 `json:"visitorTimeOutTime"`
VisitorDownloadSpan float32 `json:"visitorDownloadSpan"`
}
NetworkParamValues holds tunable PvP/multiplayer parameters grouped by player role.
func NetworkParamAggressiveBlue ¶ added in v1.0.0
func NetworkParamAggressiveBlue() NetworkParamValues
NetworkParamAggressiveBlue returns the "Aggressive Blue / Hunter" preset (Blue role). Fastest, widest co-op blue search for hunter-side testing with an active Blue Cipher Ring. MaxCoopBlueSummonCount (Blue Search Parallelism) and AllAreaSearchRateVsBlue (Retribution) stay vanilla — they are Experimental.
func NetworkParamAggressiveHost ¶
func NetworkParamAggressiveHost() NetworkParamValues
NetworkParamAggressiveHost returns the "Aggressive Host" preset (Host role). Experimental.
func NetworkParamAggressiveHunter ¶ added in v1.0.0
func NetworkParamAggressiveHunter() NetworkParamValues
func NetworkParamAggressiveReds ¶ added in v1.0.0
func NetworkParamAggressiveReds() NetworkParamValues
NetworkParamAggressiveReds returns the "Aggressive Reds" preset (Invader role). Aggressive red invasion search: broad 12/12 area/target scan, 10s retry interval, and 7s timeout. TODO: add a separate Very Aggressive preset only after validating that tighter overlapping cycles do not starve matchmaking requests.
func NetworkParamAggressiveSummonGuest ¶ added in v1.0.0
func NetworkParamAggressiveSummonGuest() NetworkParamValues
func NetworkParamAggressiveSummonHost ¶ added in v1.0.0
func NetworkParamAggressiveSummonHost() NetworkParamValues
func NetworkParamAggressiveSummons ¶ added in v1.0.0
func NetworkParamAggressiveSummons() NetworkParamValues
NetworkParamAggressiveSummons returns the "Aggressive Summon Signs" preset (Cooperator role). Fastest sign refresh/upload and the largest internally consistent sign buffer (cellCount <= totalCount <= singGetMax: 32 <= 64 <= 96). Controls the summon-sign network path only; Summoning Pool activation is a separate World/Exploration feature. cellGroup* ranges are left untouched.
func NetworkParamDefaults ¶
func NetworkParamDefaults() NetworkParamValues
NetworkParamDefaults returns the vanilla game defaults for all fields.
func NetworkParamFast ¶
func NetworkParamFast() NetworkParamValues
NetworkParamFast returns the legacy "Fast Invasions" preset for backward compatibility.
func NetworkParamFastBlue ¶
func NetworkParamFastBlue() NetworkParamValues
NetworkParamFastBlue returns the "Fast Blue" preset (Blue role). Experimental.
func NetworkParamFastInvasions ¶
func NetworkParamFastInvasions() NetworkParamValues
NetworkParamFastInvasions returns the "Fast Invasions" preset (Invader role).
func NetworkParamFastSummons ¶
func NetworkParamFastSummons() NetworkParamValues
NetworkParamFastSummons returns the "Fast Summons" preset (Cooperator role). Experimental.
func NetworkParamFasterBlue ¶ added in v1.0.0
func NetworkParamFasterBlue() NetworkParamValues
NetworkParamFasterBlue returns the "Faster Blue / Hunter" preset (Blue role). Faster and wider co-op blue search. MaxCoopBlueSummonCount stays at vanilla 2 (the server caps actual joins; raising it only inflates client-side search). AllAreaSearchRateVsBlue stays at vanilla 30 (retribution blue likely legacy in ER).
func NetworkParamFasterHunter ¶ added in v1.0.0
func NetworkParamFasterHunter() NetworkParamValues
func NetworkParamFasterReds ¶ added in v1.0.0
func NetworkParamFasterReds() NetworkParamValues
NetworkParamFasterReds returns the "Faster Reds" preset (Invader role). Faster red invasion search: wider 8/8 area/target scan, 12s retry interval, and 8s timeout. This keeps a small processing gap before the next cycle.
func NetworkParamFasterSummonGuest ¶ added in v1.0.0
func NetworkParamFasterSummonGuest() NetworkParamValues
func NetworkParamFasterSummonHost ¶ added in v1.0.0
func NetworkParamFasterSummonHost() NetworkParamValues
func NetworkParamFasterSummons ¶ added in v1.0.0
func NetworkParamFasterSummons() NetworkParamValues
NetworkParamFasterSummons returns the "Faster Summons & Pools" preset (Cooperator role). Faster sign download/upload and a larger, internally consistent sign buffer (cellCount <= totalCount <= singGetMax). Spatial cellGroup ranges are left at vanilla — they are an Experimental option, not part of this preset.
func NetworkParamLightInvasions ¶ added in v0.13.0
func NetworkParamLightInvasions() NetworkParamValues
NetworkParamLightInvasions returns the "Light / Safer" invasions preset. Moderate speed-up with a slightly lower detection surface than Fast Invasions.
func ReadNetworkParams ¶
func ReadNetworkParams(ud11 []byte) (*NetworkParamValues, error)
ReadNetworkParams extracts current NetworkParam values from UserData11.
type PS5Activity ¶
type PS5Activity struct {
Data [0x20]byte
}
PS5Activity — 32 opaque bytes.
func (*PS5Activity) Read ¶
func (p *PS5Activity) Read(r *Reader) error
func (*PS5Activity) Write ¶
func (p *PS5Activity) Write(sw *SectionWriter)
type Platform ¶
type Platform string
func ClassifyContainer ¶ added in v1.3.0
ClassifyContainer returns the platform of an unambiguous native save container, or "" when the container is ambiguous/unsupported. Detection is purely by leading container magic — never by decryption, and never by filename. An AES-encrypted PC save (no raw BND4 prefix) is intentionally classified as unsupported.
type PlayerCoordinates ¶
type PlayerCoordinates struct {
Coordinates FloatVector3
MapID MapID
Angle FloatVector4
GameMan0xbf0 uint8
UnkCoords FloatVector3
UnkAngle FloatVector4
}
PlayerCoordinates — current player position block, 61 bytes.
Layout: coords (12) | map_id (4) | angle (16) | game_man_0xbf0 (1) |
unk_coords (12) | unk_angle (16)
(er-save-manager labels this 57 bytes in a comment — the actual struct
is 61 bytes; the comment is stale.)
Reference: tmp/repos/er-save-manager/parser/world.py:PlayerCoordinates
func (*PlayerCoordinates) Read ¶
func (p *PlayerCoordinates) Read(r *Reader) error
func (*PlayerCoordinates) Write ¶
func (p *PlayerCoordinates) Write(w *SectionWriter)
type PlayerGameData ¶
type PlayerGameData struct {
Level uint32
Vigor uint32
Mind uint32
Endurance uint32
Strength uint32
Dexterity uint32
Intelligence uint32
Faith uint32
Arcane uint32
Souls uint32
SoulMemory uint32
CharacterName [16]uint16
Gender uint8
VoiceType uint8 // 0=Young1, 1=Young2, 2=Mature1, 3=Mature2, 4=Aged1, 5=Aged2
Class uint8
TalismanSlots uint8 // additional talisman slots (0-3), total = 1 + this value
ClearCount uint32 // NG+ cycle (0=NG, 1=NG+1, ..., 7=NG+7)
GreatRuneOn uint8 // Great Rune buff active (0=off, 1=on)
EquippedGreatRune uint32 // equipped Great Rune item ID (0=none)
ScadutreeBlessing uint8
ShadowRealmBlessing uint8
}
type PlayerGameDataHash ¶
type PlayerGameDataHash struct {
Level uint32
Stats uint32
Archetype uint32
PlayerGameData0xc0 uint32
Padding uint32
Runes uint32
RunesMemory uint32
EquippedWeapons uint32
EquippedArmorsAndTalismans uint32
EquippedItems uint32
EquippedSpells uint32
Rest [0x54]byte
}
PlayerGameDataHash — 128 bytes (0x80), 11×u32 + 0x54 raw bytes.
Reference: tmp/repos/er-save-manager/parser/world.py:PlayerGameDataHash
Spec/22 describes the length as `slot_end - position`, but in practice every save we have observed places this 128-byte block at SlotSize - 0x80 and any remaining tail bytes are zero padding (captured separately as SaveSlot.RestPadding by the rebuild parser).
func (*PlayerGameDataHash) Read ¶
func (h *PlayerGameDataHash) Read(r *Reader) error
func (*PlayerGameDataHash) Write ¶
func (h *PlayerGameDataHash) Write(w *SectionWriter)
type PreEventFlagsScalars ¶
type PreEventFlagsScalars struct {
GameMan0x8c uint8
GameMan0x8d uint8
GameMan0x8e uint8
TotalDeathsCount uint32
CharacterType int32
InOnlineSessionFlag uint8
CharacterTypeOnline uint32
LastRestedGrace uint32
NotAloneFlag uint8
InGameCountdownTimer uint32
UnkGameDataMan0x124 uint32
}
PreEventFlagsScalars — block of scalar fields between TutorialData and the event_flags bitfield. Layout (all little-endian):
gameman_0x8c u8 gameman_0x8d u8 gameman_0x8e u8 total_deaths_count u32 character_type i32 in_online_session_flag u8 character_type_online u32 last_rested_grace u32 not_alone_flag u8 in_game_countdown_timer u32 unk_gamedataman_0x124 u32
Total: 3 + 4 + 4 + 1 + 4 + 4 + 1 + 4 + 4 = 29 bytes. Reference: tmp/repos/er-save-manager/parser/user_data_x.py:358-371
func (*PreEventFlagsScalars) Read ¶
func (s *PreEventFlagsScalars) Read(r *Reader) error
func (*PreEventFlagsScalars) Write ¶
func (s *PreEventFlagsScalars) Write(w *SectionWriter)
type ProfileSummary ¶
func (*ProfileSummary) Read ¶
func (p *ProfileSummary) Read(r *Reader) error
func (*ProfileSummary) Serialize ¶
func (p *ProfileSummary) Serialize(data []byte, offset int)
type QuantityClampChange ¶ added in v1.1.0
type QuantityClampChange struct {
Scope string `json:"scope"`
Row int `json:"row"`
Handle uint32 `json:"handle"`
OldQuantity uint32 `json:"oldQuantity"`
NewQuantity uint32 `json:"newQuantity"`
Cap uint64 `json:"cap"`
}
QuantityClampChange describes one ClampInventoryQuantityAt result. OldQuantity and NewQuantity are the RAW quantity fields (high bit preserved), so a caller can tell exactly what changed on the wire.
func ClampInventoryQuantityAt ¶ added in v1.1.0
func ClampInventoryQuantityAt(slot *SaveSlot, scope string, row int, fingerprint string) (QuantityClampChange, error)
ClampInventoryQuantityAt clamps the quantity of the single record at scope+row down to its authoritative effective cap, recomputed from the item DB at apply time. It never accepts a caller-supplied cap.
The record is re-resolved, its fingerprint stale-checked against the scan-time value, and the raw/in-memory state verified for consistency before any write. Records without an applicable cap, with a zero cap (item not permitted in the container — removal, not clamping, is the correct repair), or already at/below the cap are rejected with no mutation. The high quantity bit (0x80000000) is preserved.
Every fallible check runs before the single write, so no failure is possible afterwards and the primitive needs no snapshot of its own (the App repair wrapper still snapshots for defense in depth). Exactly one 4-byte raw quantity field and its in-memory counterpart change; handle, acquisition index, headers, GaItems and every other record are left untouched.
scope is one of the repairScope* constants; row is the index into the matching in-memory list exactly as the scanner produced it.
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
func (*Reader) FindPattern ¶
FindPattern searches for a byte pattern in the data starting from the current position.
func (*Reader) ReadSizedBytes ¶
ReadSizedBytes mirrors WriteSizedBytes: reads an i32 size, then `size` raw bytes. Returns the data slice (referencing the underlying buffer — copy if you need to retain it past the next read).
Sanity-checks `size` against the provided maximum (matches er-save-manager behaviour: write size as 0 if unreasonable; here we surface as an error).
type RehandleChange ¶ added in v1.1.0
type RehandleChange struct {
Scope string `json:"scope"`
Row int `json:"row"`
OldHandle uint32 `json:"oldHandle"`
NewHandle uint32 `json:"newHandle"`
ItemID uint32 `json:"itemID"`
}
RehandleChange describes the outcome of one RehandleInventoryRecord call.
func RehandleInventoryRecord ¶ added in v1.1.0
func RehandleInventoryRecord(slot *SaveSlot, scope string, row int) (RehandleChange, error)
RehandleInventoryRecord assigns a fresh unique handle to the record at scope+row, leaving the record in its container. ItemID, quantity, and acquisition index are preserved.
Weapons (0x80), armor (0x90), and AoW (0xC0): a new GaItem entry is allocated. If GaItems is full, the function returns an error with no partial mutation — slot.Data and the in-memory list are unchanged. Talismans (0xA0) and goods (0xB0): no GaItem is allocated; only GaMap and the binary handle field are updated.
GaMap gains an entry for the new handle. The old handle's GaMap entry is left in place — other records may still reference it.
Scope: "inventory_common" | "inventory_key" | "storage_common".
type RepairIssue ¶ added in v1.1.0
type RepairIssue struct {
IssueID string `json:"issueID"`
DebugKey string `json:"debugKey"`
Fingerprint string `json:"fingerprint"`
Key IssueKey `json:"key"`
Description string `json:"description"`
Severity string `json:"severity"`
Actions []string `json:"actions"`
DefaultAction string `json:"defaultAction"`
}
RepairIssue is one problem found by ScanRepairIssues. It is read-only — no slot mutation occurs during scanning.
func ScanRepairIssues ¶ added in v1.1.0
func ScanRepairIssues(slotIndex int, slot *SaveSlot) []RepairIssue
ScanRepairIssues returns all repair issues found in slot. Read-only. It resolves the physical record collection once and delegates to ScanRepairIssuesFromRecords so coverage and scanning share identical record semantics.
func ScanRepairIssuesFromRecords ¶ added in v1.1.0
func ScanRepairIssuesFromRecords(slotIndex int, slot *SaveSlot, records []ResolvedRecord) []RepairIssue
ScanRepairIssuesFromRecords scans a pre-resolved record collection. Callers that also build a coverage report should resolve once (ResolveInventoryRecords) and pass the same slice to both this function and BuildCoverageReport to guarantee the two never diverge. Read-only.
type Resolution ¶ added in v1.1.0
type Resolution string
Resolution is the terminal resolution status of a physical record. The three values form an exhaustive partition of every non-empty record:
TotalPhysical = KnownDB + TechnicalPlaceholder + Unknown
"Resolved" in the coverage report means KnownDB + TechnicalPlaceholder. A record being Resolved says nothing about whether it is *valid* — that is the scanner's job (structural / category checks), reported separately as issues.
const ( ResolutionKnownDB Resolution = "known_db" ResolutionTechnicalPlaceholder Resolution = "technical_placeholder" ResolutionUnknown Resolution = "unknown" )
type ResolvedRecord ¶ added in v1.1.0
type ResolvedRecord struct {
Scope string // repairScope* — inventory_common / inventory_key / storage_common
Row int // compacted/action row within the scope — index into slot.*.CommonItems, addressed by repair primitives
PhysicalRow int // physical slot index in the raw binary array; differs from Row when storage has gaps (== Row for inventory / when raw data is unavailable)
IndexDedup bool // participates in duplicate_acquisition_index dedup (inventory, not storage)
Handle uint32 // raw GaItemHandle as stored
HandleType uint32 // handle & GaHandleTypeMask
ItemID uint32 // raw itemID (GaMap, else db.HandleToItemID) — never rewritten
DisplayID uint32 // normalized display itemID (Wondrous Physick raw→display alias)
BaseID uint32 // DB base itemID (upgrade/infusion stripped) when resolved
Name string // DB name when resolved; placeholder name for naked armor
Category string // DB category when resolved
MaxInventory uint32 // conservative Normal Mode DB cap; meaningful only when Resolution == KnownDB
MaxStorage uint32 // conservative Normal Mode DB cap; meaningful only when Resolution == KnownDB
GameMaxInventory uint32 // technical game cap; meaningful only when GameMaxInventoryKnown
GameMaxStorage uint32 // technical game cap; meaningful only when GameMaxStorageKnown
GameMaxInventoryKnown bool // distinguishes a known zero (prohibited) from missing limit data
GameMaxStorageKnown bool // distinguishes a known zero (prohibited) from missing limit data
ScalesWithNG bool // conservative Normal Mode flag; never used as save-integrity truth
Quantity uint32 // record quantity as stored
AcquisitionIndex uint32 // record acquisition/sort index as stored
HasGaItem bool // a per-instance GaItem actually exists for this handle. slot.GaMap is built 1:1 from the non-empty slot.GaItems entries (structures.go scanGaItems), so GaMap membership is equivalent to GaItem existence — this is not a mere numeric coincidence.
Identity IdentityClass // instance-backed / handle-encoded / stackable ammo / placeholder / unknown
Resolution Resolution // KnownDB / TechnicalPlaceholder / Unknown
UnknownReason UnknownReason // set only when Resolution == Unknown
Fingerprint string // record-state fingerprint (handle+qty+index) for stale-checks
}
ResolvedRecord is the canonical, read-only view of one physical inventory or storage record, shared by the core scanner, the coverage report, and the editor workspace. It is produced once per record by the resolver so all three consumers see identical semantics.
func ResolveInventoryRecords ¶ added in v1.1.0
func ResolveInventoryRecords(slot *SaveSlot) []ResolvedRecord
ResolveInventoryRecords resolves every non-empty physical record across the inventory-common, key-item, and storage-common sections exactly once, in a stable order. Empty/invalid handles are skipped. This is the single source of record semantics for both BuildCoverageReport and the scanner.
func ResolveRecord ¶ added in v1.1.0
func ResolveRecord(slot *SaveSlot, scope string, row int, handle, qty, acq uint32) ResolvedRecord
ResolveRecord resolves a single physical record into its canonical form. It never mutates the slot and never rewrites the raw itemID; DisplayID carries the normalized ID used for DB lookups (e.g. Wondrous Physick).
type RideGameData ¶
type RideGameData struct {
Coordinates FloatVector3
MapID MapID
Angle FloatVector4
HP int32
State uint32 // HorseState enum (3=DEAD, 13=ACTIVE)
}
RideGameData — Torrent / horse state. 0x28 = 40 bytes. Layout: coordinates(12) | map_id(4) | angle(16) | hp(i32) | state(u32) Reference: tmp/repos/er-save-manager/parser/world.py:RideGameData
func (*RideGameData) Read ¶
func (h *RideGameData) Read(r *Reader) error
func (*RideGameData) Write ¶
func (h *RideGameData) Write(w *SectionWriter)
type SaveFile ¶
type SaveFile struct {
Platform Platform
Encrypted bool
IV []byte
Header []byte
Slots [10]SaveSlot
SteamID uint64
UserData10 CSMenuSystemSaveLoad
ActiveSlots [10]bool
ProfileSummaries [10]ProfileSummary
UserData11 []byte
}
func (*SaveFile) CleanResidualSlots ¶ added in v1.0.0
CleanResidualSlots zeroes every slot flagged inactive that still carries residual character data (see SlotHasResidualData). Returns the number of slots cleaned. Active slots are never touched. Idempotent: a second call returns 0.
func (*SaveFile) ClearSlot ¶ added in v1.0.0
ClearSlot zeroes slot idx entirely IN PLACE — data block, active flag, and the full ProfileSummary region. This mirrors the game's positional deletion model: slots 0-9 keep their positions and only the target slot is cleared; subsequent slots are NOT shifted down. (Confirmed by the independent per-slot active-flag array at 0x1954 — gaps between active slots are valid — and by the reference ER-Save-Editor, which is purely positional.)
func (*SaveFile) SlotHasResidualData ¶ added in v1.0.0
SlotHasResidualData reports whether slot idx carries leftover character data (a slot-data name or a ProfileSummary name) while its active flag is cleared. This is the "phantom" state produced when a character is deleted in-game (the game clears the active flag but does not zero the data block / summary): the game ignores the slot (flag=0), but a name-based UI would show it as a duplicate. Active slots are never residual.
type SaveSlot ¶
type SaveSlot struct {
Data []byte
Version uint32 // slot format version (offset 0x00); 0 = empty slot
Player PlayerGameData
GaMap map[uint32]uint32
GaItems []GaItemFull // parsed GaItem array (5118 or 5120 entries)
Inventory EquipInventoryData
Storage EquipInventoryData
SteamID uint64
Warnings []string // non-fatal issues detected during parsing
MagicOffset int
InventoryEnd int
EventFlagsOffset int
// Dynamic offsets from Python logic
PlayerDataOffset int
FaceDataOffset int
StorageBoxOffset int
IngameTimerOffset int
GaItemDataOffset int // start of GaItemData section (distinct_acquired_items_count header)
TutorialDataOffset int // start of TutorialData block (header at offset, per er-save-manager world.py)
ClearCountOffset int // NG+ cycle counter (uint32) — after BloodStain in dynamic chain
EquipItemsIDOffset int // start of EquippedItemsItemIds section
EquippedSpellsOffset int // start of EquippedSpells section (14×8 + 4 active_index = 0x74)
UnlockedRegionsOffset int // start of unlocked_regions struct (count u32 + count*4 IDs)
UnlockedRegions []uint32 // parsed unlocked region IDs (drives invasion / blue-summon eligibility)
// SectionMap holds the section boundaries used by RebuildSlot. Populated
// after parsing finishes (or in a degraded form for empty / unparseable slots).
// Sections cover [0, SlotSize) contiguously with no gaps or overlaps.
SectionMap []SectionRange
// Tracked indices for type-segregated GaItem placement.
// The game expects AoW entries at low indices, then armor, then weapons.
// Matching Rust ER-Save-Editor's next_aow_index / next_armament_or_armor_index.
NextAoWIndex int // next free index for AoW entries (after last AoW + 1)
NextArmamentIndex int // next free index for weapon/armor entries (after highest-counter entry + 1)
NextGaItemHandle uint32 // global handle counter (lower 16 bits), next value to assign
PartGaItemHandle uint8 // part_id (bits 16-23 of handle), extracted from first entry
}
func CloneSlot ¶ added in v1.5.0
CloneSlot returns a deep, independent copy of slot. It is intended for candidate-only mutations: callers can run a transaction on the clone and publish it only after all validation succeeds.
func (*SaveSlot) CalculateDynamicOffsets ¶ added in v1.0.0
CalculateDynamicOffsets is the exported wrapper over the package-internal calculateDynamicOffsets. Phase 7d.3 introduces it so the apply-spells test fixtures (in package main) can materialise a calibrated SaveSlot from a hand-built buffer without running the full Read pipeline. The production code paths still call the unexported form via parseFromData; this wrapper exists solely as a test seam.
func (*SaveSlot) FaceDataStart ¶
FaceDataStart returns the byte offset in slot.Data where the FaceData blob begins. FaceData is FaceDataBlobSize (303) bytes ending at FaceDataOffset.
func (*SaveSlot) SyncPlayerToData ¶
func (s *SaveSlot) SyncPlayerToData()
func (*SaveSlot) WriteEquipment ¶ added in v1.0.0
func (s *SaveSlot) WriteEquipment(writes []EquipmentWrite) error
WriteEquipment applies a batch of equipment slot writes atomically.
Semantics:
- All writes are validated before any byte is mutated. If any write fails validation, the slot data and hash bytes remain unchanged.
- Handle == 0 clears the slot to 0xFFFFFFFF.
- Non-zero handles must exist in slot.GaMap and match the slot's class (weapon / ammo / armor). The 0xC0 AoW handles are rejected for weapon slots in Phase 7b.0 — equipping an Ash of War as a weapon is out of scope, even though the read-side encoding rule would technically accept it.
- After a successful write, hash 7 is recomputed if any slot 0–9 was touched, and hash 8 is recomputed if any slot 12–15 was touched. Other hash entries are not modified.
Concurrency: callers that share a SaveSlot across goroutines must hold the slot-level lock for the entire WriteEquipment call.
func (*SaveSlot) WriteSpells ¶ added in v1.0.0
func (s *SaveSlot) WriteSpells(writes []SpellWrite) error
WriteSpells is the batch equivalent of PatchEquippedSpell with one extra responsibility: it recomputes hash entry [10] (the EquippedSpells hash) so the in-save hash block stays consistent with the new spell loadout. Mirrors the WriteEquipment pattern (hash[7] / hash[8] inline recompute) and is deliberately the ONLY production-code path that touches hash[10].
Atomicity: every write is structurally validated (slot index range + duplicate detection) BEFORE any byte is mutated. Any validation failure returns the error without touching slot.Data, matching WriteEquipment's no-partial-write invariant. Per-write semantic validation (offset bounds, nil slot, etc.) is delegated to PatchEquippedSpell, which itself never mutates on failure.
Hash discipline: only hash[10] is touched. hash[7] (weapons), hash[8] (armor/talismans) and every other hash entry are left untouched — this writer never invalidates work done by WriteEquipment or by any future per-section writer.
Concurrency: callers that share a SaveSlot across goroutines must hold the slot-level lock for the entire WriteSpells call.
type SectionRange ¶
SectionRange describes a contiguous byte range inside slot.Data. End is exclusive. Size returns End - Start.
func (SectionRange) Size ¶
func (r SectionRange) Size() int
Size returns the byte length of the section.
type SectionWriter ¶
type SectionWriter struct {
// contains filtered or unexported fields
}
SectionWriter is a small append-only writer used by section serializers when rebuilding a slot. It mirrors the primitive helpers on Reader and keeps an explicit cursor so callers can track the resulting offset.
Unlike io.Writer it cannot fail — writes always succeed against an internal []byte buffer that grows on demand.
func NewSectionWriter ¶
func NewSectionWriter(hint int) *SectionWriter
NewSectionWriter returns a writer pre-allocated to the given hint capacity. hint may be 0; the buffer will grow as needed.
func (*SectionWriter) Bytes ¶
func (w *SectionWriter) Bytes() []byte
Bytes returns the underlying buffer (no copy).
func (*SectionWriter) Len ¶
func (w *SectionWriter) Len() int
Len returns the current write cursor (== len(Bytes())).
func (*SectionWriter) PadZeros ¶
func (w *SectionWriter) PadZeros(n int)
PadZeros appends n zero bytes. n must be >= 0.
func (*SectionWriter) WriteBytes ¶
func (w *SectionWriter) WriteBytes(b []byte)
WriteBytes appends raw bytes verbatim.
func (*SectionWriter) WriteF32 ¶
func (w *SectionWriter) WriteF32(v float32)
func (*SectionWriter) WriteI32 ¶
func (w *SectionWriter) WriteI32(v int32)
func (*SectionWriter) WriteSizedBytes ¶
func (w *SectionWriter) WriteSizedBytes(data []byte)
WriteSizedBytes serializes a size-prefixed blob: i32 size followed by data. Used for `field_area`, `world_area`, `world_geom_man`, `world_geom_man2`, `rend_man` (see tmp/repos/er-save-manager/parser/slot_rebuild.py).
func (*SectionWriter) WriteU8 ¶
func (w *SectionWriter) WriteU8(v uint8)
func (*SectionWriter) WriteU16 ¶
func (w *SectionWriter) WriteU16(v uint16)
func (*SectionWriter) WriteU32 ¶
func (w *SectionWriter) WriteU32(v uint32)
func (*SectionWriter) WriteU64 ¶
func (w *SectionWriter) WriteU64(v uint64)
type SizePrefixedBlob ¶
type SizePrefixedBlob struct {
Size int32 // raw header value
Data []byte // length matches Size when Size is in valid range
}
SizePrefixedBlob represents a `(size: i32, data: bytes[size])` section. The raw `Size` header is preserved verbatim even when out of range (matching er-save-manager behaviour); in that case `Data` is empty and the section serializes as just the 4-byte header.
func (*SizePrefixedBlob) ByteSize ¶
func (b *SizePrefixedBlob) ByteSize() int
ByteSize returns the serialized length: 4-byte header + payload bytes.
func (*SizePrefixedBlob) Read ¶
func (b *SizePrefixedBlob) Read(r *Reader, maxSize int32, fieldName string) error
func (*SizePrefixedBlob) Write ¶
func (b *SizePrefixedBlob) Write(w *SectionWriter)
type SlotAccessor ¶
SlotAccessor provides bounds-checked read/write access to a save slot's raw byte buffer. It collects non-fatal warnings (e.g. clamped dynamic sizes) separately from fatal errors.
func NewSlotAccessor ¶
func NewSlotAccessor(data []byte) *SlotAccessor
func (*SlotAccessor) CheckBounds ¶
func (sa *SlotAccessor) CheckBounds(off, size int, label string) error
CheckBounds validates that a write of `size` bytes at `off` is safe.
func (*SlotAccessor) ReadDynamicSize ¶
ReadDynamicSize reads a uint32 size value from untrusted save data and clamps it to a sane maximum. Returns 0 (not error) when clamped, but appends a warning. This is the correct behavior for PS4 saves which often have garbage in size fields.
func (*SlotAccessor) ReadU8 ¶
func (sa *SlotAccessor) ReadU8(off int) (uint8, error)
ReadU8 reads a single byte at the given offset with bounds checking.
func (*SlotAccessor) ReadU16 ¶
func (sa *SlotAccessor) ReadU16(off int) (uint16, error)
ReadU16 reads a little-endian uint16 at the given offset with bounds checking.
func (*SlotAccessor) ReadU32 ¶
func (sa *SlotAccessor) ReadU32(off int) (uint32, error)
ReadU32 reads a little-endian uint32 at the given offset with bounds checking.
func (*SlotAccessor) ReadU64 ¶
func (sa *SlotAccessor) ReadU64(off int) (uint64, error)
ReadU64 reads a little-endian uint64 at the given offset with bounds checking.
func (*SlotAccessor) WriteU8 ¶
func (sa *SlotAccessor) WriteU8(off int, val uint8) error
WriteU8 writes a single byte at the given offset with bounds checking.
func (*SlotAccessor) WriteU16 ¶
func (sa *SlotAccessor) WriteU16(off int, val uint16) error
WriteU16 writes a little-endian uint16 at the given offset with bounds checking.
type SlotDiagnostics ¶
type SlotDiagnostics struct {
SlotIndex int `json:"slotIndex"`
Issues []DiagnosticIssue `json:"issues"`
}
SlotDiagnostics holds the results of a comprehensive slot corruption scan.
func DiagnoseSaveCorruption ¶
func DiagnoseSaveCorruption(slot *SaveSlot, slotIndex int) SlotDiagnostics
DiagnoseSaveCorruption performs a comprehensive corruption scan on a slot. Returns all found issues sorted by severity.
type SlotSnapshot ¶
type SlotSnapshot struct {
Data []byte
Version uint32
Player PlayerGameData
GaMap map[uint32]uint32
GaItems []GaItemFull
Inventory EquipInventoryData
Storage EquipInventoryData
SteamID uint64
Warnings []string
MagicOffset int
InventoryEnd int
EventFlagsOffset int
PlayerDataOffset int
FaceDataOffset int
StorageBoxOffset int
IngameTimerOffset int
GaItemDataOffset int
TutorialDataOffset int
ClearCountOffset int
EquipItemsIDOffset int
EquippedSpellsOffset int
UnlockedRegionsOffset int
UnlockedRegions []uint32
SectionMap []SectionRange
NextAoWIndex int
NextArmamentIndex int
NextGaItemHandle uint32
PartGaItemHandle uint8
}
SlotSnapshot holds a deep copy of all mutable SaveSlot state for rollback.
func SnapshotSlot ¶
func SnapshotSlot(slot *SaveSlot) SlotSnapshot
SnapshotSlot creates a deep copy of all mutable slot state.
type SlotUsage ¶
type SlotUsage struct {
GaItemsUsed int
GaItemsMax int
GaItemDataUsed int
GaItemDataMax int
InventoryUsed int
InventoryMax int
StorageUsed int
StorageMax int
}
SlotUsage holds the count of used vs max slots for each container.
func CountSlotUsage ¶
CountSlotUsage counts used entries in all slot containers by scanning binary data.
type SpawnPointBlock ¶
type SpawnPointBlock struct {
PadAfterCoords [2]byte
SpawnPointEntityID uint32
GameMan0xb64 uint32
HasTempSpawnPoint bool // version >= 65
TempSpawnPointEntityID uint32 // populated only if HasTempSpawnPoint
HasGameMan0xcb3 bool // version >= 66
GameMan0xcb3 uint8 // populated only if HasGameMan0xcb3
}
SpawnPointBlock — fields after PlayerCoordinates and its 2-byte padding.
Layout:
pad_after_coords [2]byte (always zero, preserved verbatim) spawn_point_entity_id u32 game_man_0xb64 u32 temp_spawn_point_entity_id u32 (only when slot Version >= 65) game_man_0xcb3 u8 (only when slot Version >= 65 — see note)
Note: er-save-manager gates the two trailing fields on version >= 65 and >= 66 separately. Our saves are always version >= 230, so both fire; we pass the slot version explicitly so the rebuild matches whatever the slot was originally read with.
Reference: tmp/repos/er-save-manager/parser/user_data_x.py:386-395
func (*SpawnPointBlock) ByteSize ¶
func (s *SpawnPointBlock) ByteSize() int
ByteSize reflects the version-gated layout chosen at Read time.
func (*SpawnPointBlock) Write ¶
func (s *SpawnPointBlock) Write(w *SectionWriter)
type SpellWrite ¶ added in v1.0.0
SpellWrite is a single equipped-spell mutation request. SlotIndex is 0..13 (matches the save's spell slot ordering); SpellID is a raw MagicParam ID (e.g. 0x1770 for Catch Flame) or EquippedSpellEmptySentinel (0xFFFFFFFF) to clear the slot.
Templates v2 stores spells with full DB-style item IDs (0x40XXXXXX); the apply layer is responsible for stripping the prefix via db.ItemIDToMagicParamID before constructing SpellWrite entries.
type TrailingFixedBlock ¶
type TrailingFixedBlock struct {
Weather WorldAreaWeather
Time WorldAreaTime
BaseVersion BaseVersion
SteamID uint64
PS5Activity PS5Activity
DLC DLCSection
}
TrailingFixedBlock — Weather + Time + BaseVersion + SteamID + PS5Activity + DLC. Total 12 + 12 + 16 + 8 + 32 + 50 = 130 bytes.
func (*TrailingFixedBlock) Read ¶
func (b *TrailingFixedBlock) Read(r *Reader) error
func (*TrailingFixedBlock) Write ¶
func (b *TrailingFixedBlock) Write(sw *SectionWriter)
type TransferDirection ¶ added in v1.0.0
type TransferDirection int
TransferDirection identifies the source→destination direction of a move between Inventory.CommonItems and Storage.CommonItems.
const ( TransferToStorage TransferDirection = iota // Inventory.CommonItems → Storage.CommonItems TransferToInventory // Storage.CommonItems → Inventory.CommonItems )
type TransferOptions ¶ added in v1.0.0
TransferOptions carries optional parameters for MoveItemsBetweenContainers. DestCaps provides per-handle quantity caps for the destination container (e.g. MaxStorage for to-storage direction). When a stackable handle is missing from DestCaps or maps to 0, the move is rejected with SkipReasonMissingCap rather than silently exceeding the in-game cap. Non-stackable items ignore the cap (qty=1 per record).
type TransferResult ¶ added in v1.0.0
type TransferResult struct {
Moved int `json:"moved"`
Skipped []TransferSkip `json:"skipped"`
}
TransferResult is the outcome of a batch transfer.
func MoveItemsBetweenContainers ¶ added in v1.0.0
func MoveItemsBetweenContainers(slot *SaveSlot, handles []uint32, direction TransferDirection, opts *TransferOptions) (TransferResult, error)
MoveItemsBetweenContainers physically relocates item records between Inventory.CommonItems and Storage.CommonItems. The GaItem array and GaMap are NOT touched — the handle stays alive on the destination side and the underlying GaItem record (with all its metadata: upgrade level, infusion, attached AoW gem) is preserved across the transfer.
For non-stackable items (handle prefixes 0x80 Weapon, 0x90 Armor, 0xC0 AoW): the source record is cleared and an empty destination slot receives the same handle, the original quantity, and a freshly-assigned Index (per add-like rules for the dest side). Caps are ignored (records hold qty=1).
For stackable items (handle prefixes 0xA0 Accessory/Talisman and 0xB0 Goods), and for any case where the destination already has a record with the same handle: the move is cap-aware. The destination cap is read from opts.DestCaps[handle]; if missing or 0, the handle is skipped with reason SkipReasonMissingCap (no silent unbounded merge). When the source quantity exceeds the available space (cap - existing_dst_qty), the move is partial: destination is filled to cap, source retains the remainder, the handle is reported in both Moved (>0 qty moved) and Skipped (reason SkipReasonDestAtCap with MovedQty/RemainingQty populated). When the destination is already at cap, no movement happens and the skip reason is SkipReasonDestAtCap with MovedQty=0.
Equipped items (handles referenced by ChrAsmEquipment) are rejected when moving Inventory → Storage with skip reason SkipReasonEquipped. No equipped check is performed for Storage → Inventory.
The function never partial-fails the batch: invalid handles are recorded in Skipped, valid handles are processed independently. After the batch, both common_item_count headers are reconciled defensively, and slot.Storage.CommonItems is rebuilt from binary to drop stale entries.
type TransferSkip ¶ added in v1.0.0
type TransferSkip struct {
Handle uint32 `json:"handle"`
Reason string `json:"reason"` // see SkipReason* constants
MovedQty uint32 `json:"movedQty,omitempty"` // qty actually transferred (partial-cap only)
RemainingQty uint32 `json:"remainingQty,omitempty"` // qty left in source (partial-cap only)
}
TransferSkip describes a single handle outcome that was not (fully) moved. MovedQty and RemainingQty are populated only for partial transfers (reason "dest_at_cap" with non-zero MovedQty) — other reasons leave them at 0.
type TrophyEquipData ¶
TrophyEquipData — fixed 52-byte block of opaque equipment metadata. Layout: unk0x0 u32 | unk0x4 [16]byte | unk0x14 [16]byte | unk0x24 [16]byte Reference: tmp/repos/er-save-manager/parser/equipment.py:TrophyEquipData
func (*TrophyEquipData) Read ¶
func (t *TrophyEquipData) Read(r *Reader) error
func (*TrophyEquipData) Write ¶
func (t *TrophyEquipData) Write(w *SectionWriter)
type TutorialData ¶
type TutorialData struct {
Unk0x0 uint16
Unk0x2 uint16
Data []byte // serialized length is len(Data); Size header is rewritten on Write
}
TutorialData — variable-size tutorial progress block. Layout: unk0x0 u16 | unk0x2 u16 | size u32 | data[size]
Inside `data`: u32 count followed by remaining bytes treated as u32 IDs by er-save-manager. We keep the inner block as raw bytes — the count is encoded as the first 4 bytes of Data and we don't need to interpret it. Reference: tmp/repos/er-save-manager/parser/world.py:TutorialData
func (*TutorialData) ByteSize ¶
func (t *TutorialData) ByteSize() int
ByteSize returns the serialized length: 8-byte header + payload.
func (*TutorialData) Read ¶
func (t *TutorialData) Read(r *Reader) error
func (*TutorialData) Write ¶
func (t *TutorialData) Write(w *SectionWriter)
type UnknownReason ¶ added in v1.1.0
type UnknownReason string
UnknownReason explains why a record ended up with ResolutionUnknown. It is its own type (not Resolution) because the two describe different axes: Resolution is the terminal status, UnknownReason is the cause behind an Unknown status. Each reason maps to a distinct repair code and must not be collapsed.
const ( UnknownReasonUnknownHandleType UnknownReason = "unknown_handle_type" // handle prefix is not a known GaItem type UnknownReasonMissingDBEntry UnknownReason = "missing_db_entry" // known prefix, but itemID absent from DB )
type ValidationCoverage ¶ added in v1.1.0
type ValidationCoverage struct {
TotalPhysical int `json:"totalPhysical"`
Resolved int `json:"resolved"`
KnownDB int `json:"knownDB"`
TechnicalPlaceholder int `json:"technicalPlaceholder"`
Unknown int `json:"unknown"`
ResolutionChecksApplied int `json:"resolutionChecksApplied"`
StructuralChecksApplied int `json:"structuralChecksApplied"`
CategoryChecksApplied int `json:"categoryChecksApplied"`
PerCategory map[string]int `json:"perCategory"`
UnknownByReason map[string]int `json:"unknownByReason"`
}
ValidationCoverage is a measurable, issue-independent report of how many physical records the scanner read and to what depth they were checked.
Invariants (Prompt 12):
TotalPhysical = KnownDB + TechnicalPlaceholder + Unknown
Resolved = KnownDB + TechnicalPlaceholder
ResolutionChecksApplied = TotalPhysical (the resolver classified every
physical record — this is what
BuildCoverageReport itself did)
StructuralChecksApplied = 0 until the scanner runs; the pipeline fills it
with the count the structural scanner actually
processed (see ScanRepairIssuesWithCoverage). The
builder never claims structural checks it did not
execute.
CategoryChecksApplied = 0 from the builder; the pipeline fills it with the
number of KnownDB records the container/quantity
validator actually executed (see
ScanRepairIssuesWithCoverage). Only KnownDB records
are category-checked, so this is ≤ KnownDB.
PerCategory counts only records that resolved to a recognised DB category, so its values sum to KnownDB minus any KnownDB record with an empty category — it is NOT expected to sum to TotalPhysical.
func BuildCoverageReport ¶ added in v1.1.0
func BuildCoverageReport(records []ResolvedRecord) ValidationCoverage
BuildCoverageReport tallies a resolved record collection into a coverage report. It reads only the already-resolved records — it never rescans the slot, so its semantics can never diverge from the scanner's.
It reports ResolutionChecksApplied (work the resolver genuinely did) but leaves StructuralChecksApplied at 0: the structural scanner has not run at this point, and the builder must not claim checks it did not execute. The pipeline sets StructuralChecksApplied afterwards (ScanRepairIssuesWithCoverage).
type WondrousPhysickOccurrence ¶ added in v1.0.0
type WondrousPhysickOccurrence struct {
Scope string `json:"scope"`
Row int `json:"row"`
Handle uint32 `json:"handle"`
ItemID uint32 `json:"itemId"`
}
func ScanDuplicateWondrousPhysick ¶ added in v1.0.0
func ScanDuplicateWondrousPhysick(slot *SaveSlot) []WondrousPhysickOccurrence
type WorldAreaTime ¶
WorldAreaTime — 12 bytes (hour, minute, second as u32 each).
func (*WorldAreaTime) Read ¶
func (t *WorldAreaTime) Read(r *Reader) error
func (*WorldAreaTime) Write ¶
func (t *WorldAreaTime) Write(sw *SectionWriter)
type WorldAreaWeather ¶
WorldAreaWeather — 12 bytes. Layout: area_id u16 | weather_type u16 | timer u32 | padding u32
func (*WorldAreaWeather) Read ¶
func (w *WorldAreaWeather) Read(r *Reader) error
func (*WorldAreaWeather) Write ¶
func (w *WorldAreaWeather) Write(sw *SectionWriter)
type WorldGeomBlock ¶
type WorldGeomBlock struct {
FieldArea SizePrefixedBlob
WorldArea SizePrefixedBlob
WorldGeomMan SizePrefixedBlob
WorldGeomMan2 SizePrefixedBlob
RendMan SizePrefixedBlob
}
WorldGeomBlock — combined post-event_flags world block: field_area + world_area + world_geom_man + world_geom_man2 + rend_man. Each is a SizePrefixedBlob with its own sanity ceiling. Reference: tmp/repos/er-save-manager/parser/world.py + slot_rebuild.py
func (*WorldGeomBlock) ByteSize ¶
func (b *WorldGeomBlock) ByteSize() int
ByteSize returns the total serialized size of the 5 size-prefixed blobs.
func (*WorldGeomBlock) Read ¶
func (b *WorldGeomBlock) Read(r *Reader) error
func (*WorldGeomBlock) Write ¶
func (b *WorldGeomBlock) Write(w *SectionWriter)
type WorldHead ¶
type WorldHead struct {
Horse RideGameData
ControlByteMaybe uint8
BloodStain BloodStain
UnkGameDataMan120 uint32 // gamedataman_0x120 or _0x130 depending on version
UnkGameDataMan88 uint32 // gamedataman_0x88
}
WorldHead — first block after `unlocked_regions`: horse(40) + control_byte(1) + blood_stain(68) + unk_gdm_120(4) + unk_gdm_88(4) Total 117 bytes. Field naming follows er-save-manager.
func (*WorldHead) Write ¶
func (h *WorldHead) Write(w *SectionWriter)
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
func (*Writer) WriteBytes ¶
Source Files
¶
- aow_availability.go
- aow_repair.go
- backup.go
- capacity.go
- crypto.go
- diagnostics.go
- equipment_writer.go
- gaitem_duplicate_repair.go
- gaitem_repack.go
- gaitem_repack_execute.go
- gaitem_repack_preflight.go
- hash.go
- inventory_index_repair.go
- item_add_plan.go
- offset_defs.go
- quantity_clamp.go
- reader.go
- record_removal.go
- record_resolver.go
- regulation.go
- rehandle.go
- repair_scanner.go
- save_manager.go
- section_eventflags.go
- section_hash.go
- section_io.go
- section_menu.go
- section_netman.go
- section_player_coords.go
- section_trailing.go
- section_types.go
- section_world.go
- section_world_geom.go
- slot_access.go
- slot_rebuild.go
- slots.go
- snapshot.go
- spell_writer.go
- structures.go
- transfer.go
- tutorial_data.go
- wondrous_physick_repair.go
- writer.go