using System.IO; namespace NecromancerTome { /// /// Raw byte-level half of the Spatial Bracelet's vault persistence. Lives in this satellite /// assembly for exactly the reason PyramidWardWriteHelper.cs documents: PooledBinaryWriter's /// Write overload set cannot be resolved from the main project at all (CS7069), so anything /// that actually touches a PooledBinaryWriter/PooledBinaryReader has to be compiled here, /// against the game's own mscorlib. /// /// The split is deliberately drawn so that ONLY primitives cross it: this file knows about /// byte arrays and stream positions, nothing else. Bag/ItemStack serialization stays in the /// main project, where `Bag.Write(BinaryWriter)` against netstandard's own BinaryWriter /// already compiles fine (proven - that is how the vault blob is built). Keeping Bag out of /// here also keeps UnityEngine out of here, which this project's reference setup (NoStdLib + /// the game's mscorlib, no UnityEngine at all) cannot tolerate. /// /// BLOB LAYOUT, appended after everything vanilla PlayerDataFile.Write produces: /// /// int64 Magic "NECROVLT" /// int32 payloadLength /// byte[] payload (opaque here; the main project builds and parses it) /// /// The magic plus the explicit length is what makes this safe to append to somebody else's /// format. On read we remember the stream position first: if the magic is not there (an old /// save written before this feature, or a player-data packet from a party that does not have /// the mod) the position is put back exactly where it was and the caller is told "no vault" - /// so whatever the game reads next still reads the right bytes. That matters concretely: /// PlayerDataFile.ReadNetwork calls Read and then goes on to read PlayerMetaInfo from the /// same stream, and PlayerDataFile.Load treats ANY exception out of Read as "file is broken, /// roll back to the .bak". Neither may be disturbed, so nothing here throws. /// public static class SpatialVaultBlobIO { /// ASCII "NECROVLT" as one int64 - distinctive enough that stray bytes will not /// be mistaken for our block. public const long Magic = 0x4E4543524F564C54L; /// Magic (8) + length (4). public const int HeaderSize = 12; public static void Write(PooledBinaryWriter _bw, byte[] _payload) { if (_bw == null || _payload == null) { return; } _bw.Write(Magic); _bw.Write(_payload.Length); _bw.Write(_payload); } /// Returns the payload, or null when this stream carries no vault block. Never /// throws, and never leaves the stream anywhere the caller did not expect: either just /// past our whole block, or exactly back where it started. public static byte[] TryRead(PooledBinaryReader _br) { if (_br == null) { return null; } Stream stream = _br.BaseStream; if (stream == null || !stream.CanSeek) { return null; } long startPosition = stream.Position; try { if (stream.Length - startPosition < HeaderSize) { return null; } if (_br.ReadInt64() != Magic) { stream.Position = startPosition; return null; } int length = _br.ReadInt32(); if (length < 0 || stream.Length - stream.Position < length) { stream.Position = startPosition; return null; } return _br.ReadBytes(length); } catch { stream.Position = startPosition; return null; } } } }