Сборка под игру 3.3: хранилище, Пирамида, проверка версии

- Пирамида: StreamModeWrite/Read теперь глобальные перечисления (в 3.2 были внутри TileEntity).
- Хранилище: Bag поверх ItemStackGrid (конструктор с местом хранения, Resize, ItemGrid),
  запись и чтение через PooledBinaryWriter/Reader. Формат хранилища 2; формат 1 из сейвов 3.2
  читается. Незнакомый номер формата больше не считается «хранилища нет» - байты сохраняются.
- Сфера на браслете: ModificationCount/GetModification/SetModification.
- Непустой сундук: TEFeatureStorage.IsEmpty (ITileEntityLootable в 3.3 нет).
- Поиск ножа в инвентаре через ItemGrid.
- GameVersionGuard пускает только 3.3.

Проверено в игре на V 3.3.0 b14: хранилище из сейва 3.2 (24 стопки) прочиталось, сохранилось
и перечиталось; Пирамида включается; «Жертва», обыск трупа и мешки работают.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-09-26 17:44:11 +03:00
co-authored by Claude Opus 5.5
parent f4470b784f
commit e8e21d5a58
9 changed files with 64 additions and 54 deletions
+41 -26
View File
@@ -79,8 +79,16 @@ namespace NecromancerTome
{
/// <summary>Payload layout version, independent of the blob framing in
/// SpatialVaultBlobIO. An unknown version is skipped, not guessed at - the framing's
/// explicit length means we can always step over a payload we do not understand.</summary>
public const byte PayloadVersion = 1;
/// explicit length means we can always step over a payload we do not understand.
///
/// 2 = игра 3.3 (мод 1.5.0): сумка пишется новым Bag.Write (свой байт версии 2 и
/// ItemStackGrid). 1 = игра 3.2, старый формат сумки; 3.3 читает его сам (Bag.readInto,
/// ветка версии &lt; 2), поэтому хранилище из сейва 3.2 переезжает без потерь.
/// Номер поднят, чтобы сборка под 3.2 не приняла новый формат за свой.</summary>
public const byte PayloadVersion = 2;
/// <summary>Старейший номер, который эта сборка ещё читает.</summary>
public const byte OldestReadablePayloadVersion = 1;
/// <summary>What a PlayerDataFile carries. A class rather than a bare Bag because its mere
/// PRESENCE is information: "this file has been read/filled, and the answer - including a
@@ -147,30 +155,34 @@ namespace NecromancerTome
/// which is the part that had diagnostic value.</summary>
public static string lastSaveLogged;
/// <summary>Builds the opaque payload SpatialVaultBlobIO wraps. Uses netstandard's own
/// BinaryWriter over a MemoryStream, which is why Bag serialization can stay in this
/// project instead of the satellite assembly.</summary>
/// <summary>Builds the opaque payload SpatialVaultBlobIO wraps. Заголовок (версия и
/// «есть ли сумка») пишется в MemoryStream напрямую, а сама сумка - через
/// PooledBinaryWriter поверх того же потока: в 3.3 Bag.Write принимает только его.
/// Сами Write-перегрузки PooledBinaryWriter из этого проекта не вызываются (см.
/// NecromancerTEPersistence.csproj), поэтому заголовок и не идёт через него.</summary>
public static byte[] BuildPayload(Bag _bag)
{
using (MemoryStream ms = new MemoryStream())
using (BinaryWriter bw = new BinaryWriter(ms))
{
bw.Write(PayloadVersion);
ms.WriteByte(PayloadVersion);
bool hasBag = _bag != null;
bw.Write(hasBag);
ms.WriteByte(hasBag ? (byte)1 : (byte)0);
if (hasBag)
{
// Plain BinaryWriter is enough: Bag.Write only demands a PooledBinaryWriter
// when bag.preferences != null, and vault bags come from `new Bag(int)`, whose
// constructor sets nothing but the item array.
_bag.Write(bw);
using (PooledBinaryWriter pw = MemoryPools.poolBinaryWriter.AllocSync(_bReset: false))
{
pw.SetBaseStream(ms);
_bag.Write(pw, StreamModeWrite.Persistency);
pw.Flush();
}
}
bw.Flush();
return ms.ToArray();
}
}
/// <summary>Null when the payload holds no vault or is a version we do not know.</summary>
/// <summary>Null when the payload holds no vault. Незнакомый номер версии - исключение,
/// а НЕ null: null значит «хранилища нет», и следующее сохранение стёрло бы вещи.
/// Исключение ловит Read-постфикс и сохраняет байты нетронутыми (AttachRaw).</summary>
public static Bag ParsePayload(byte[] _payload)
{
if (_payload == null || _payload.Length == 0)
@@ -178,22 +190,25 @@ namespace NecromancerTome
return null;
}
using (MemoryStream ms = new MemoryStream(_payload, false))
using (BinaryReader br = new BinaryReader(ms))
{
byte version = br.ReadByte();
if (version != PayloadVersion)
int version = ms.ReadByte();
if (version < OldestReadablePayloadVersion || version > PayloadVersion)
{
Debug.LogWarning("[NecromancerTome] SpatialVaultPersistence: vault payload version " + version + ", expected " + PayloadVersion + " - skipped");
return null;
throw new InvalidDataException("vault payload version " + version + ", this build reads " +
OldestReadablePayloadVersion + ".." + PayloadVersion);
}
if (!br.ReadBoolean())
if (ms.ReadByte() != 1)
{
return null;
}
// Bag.Read is the STATIC one and returns a new Bag; ReadInto is the instance
// version. Symmetric with BuildPayload: preferences were written as absent, so no
// PooledBinaryReader is needed here either.
return Bag.Read(br);
// Сумка сама пишет свой номер формата, и Bag.readInto 3.3 понимает оба: и старый
// (из сейва 3.2), и новый. Место хранения - как у сумки дрона (то же окно), владельца нет:
// так же игра сама создаёт сумку из старого сейва (PlayerDataFile, legacyBag).
using (PooledBinaryReader pr = MemoryPools.poolBinaryReader.AllocSync(_bReset: false))
{
pr.SetBaseStream(ms);
return Bag.Read(pr, XUiC_ItemStack.StackLocationTypes.LootContainer, StreamModeRead.Persistency, null);
}
}
}
@@ -212,14 +227,14 @@ namespace NecromancerTome
{
return "NONE";
}
ItemStack[] slots = _bag.GetSlots();
ItemStackGrid slots = _bag.ItemGrid;
StringBuilder sb = new StringBuilder();
sb.Append(_bag.SlotCount).Append(" slots, ").Append(_bag.GetUsedSlotCount()).Append(" used");
if (slots != null)
{
for (int i = 0; i < slots.Length; i++)
{
ItemStack stack = slots[i];
ItemStack stack = slots.GetItem(i);
if (stack == null || stack.IsEmpty())
{
continue;