Сборка под игру 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
+4 -4
View File
@@ -18,7 +18,7 @@ namespace NecromancerTome
///
/// ПОЧЕМУ Minor / 10. Игра хранит версию двузначным числом: 3.2.0 = 20, 3.2.1 = 21, 3.3.0 = 30
/// (VersionInformation печатает его как {Minor/10}.{Minor%10}). Проверка по десяткам пропускает
/// любые хотфиксы 3.2.x и не пропускает 3.3 и всё, что будет после.
/// любые хотфиксы 3.3.x и не пропускает ни 3.2, ни 3.4 и всё, что будет после.
///
/// ВСЁ, ЧЕМ ПОЛЬЗУЕТСЯ ЭТОТ ФАЙЛ, СВЕРЕНО С ДЕКОМПИЛЯЦИЕЙ 3.3.0 (build 25485021): Constants,
/// VersionInformation, XUiC_MainMenu.OnOpen, XUiC_MessageBoxWindowGroup (класс совпадает целиком),
@@ -29,10 +29,10 @@ namespace NecromancerTome
/// </summary>
public static class GameVersionGuard
{
/// <summary>Под какую версию собран мод: Major и десяток Minor (3.2.x = 3 и 2).</summary>
/// <summary>Под какую версию собран мод: Major и десяток Minor (3.3.x = 3 и 3; сборка 1.4.x была под 3.2).</summary>
public const int SupportedMajor = 3;
public const int SupportedMinorTens = 2;
public const string SupportedText = "3.2";
public const int SupportedMinorTens = 3;
public const string SupportedText = "3.3";
private static bool? supported;
+6 -4
View File
@@ -130,17 +130,19 @@ namespace NecromancerTome
public static bool HasAnyKnife(EntityPlayerLocal player)
{
return ContainsKnife(player.inventory?.GetSlots()) || ContainsKnife(player.bag?.GetSlots());
return ContainsKnife(player.inventory?.ItemGrid) || ContainsKnife(player.bag?.ItemGrid);
}
private static bool ContainsKnife(ItemStack[] slots)
// В 3.3 у пояса и рюкзака нет GetSlots: предметы лежат в ItemStackGrid.
private static bool ContainsKnife(ItemStackGrid grid)
{
if (slots == null)
if (grid == null)
{
return false;
}
foreach (ItemStack stack in slots)
for (int i = 0; i < grid.Length; i++)
{
ItemStack stack = grid.GetItem(i);
if (stack == null || stack.IsEmpty())
{
continue;
+2 -2
View File
@@ -207,13 +207,13 @@ namespace NecromancerTome
/// user's "как проще" - this is a brand-new feature, nothing to migrate from yet;
/// PooledBinaryReader.ReadBoolean() below has no such compile restriction, confirmed
/// separately, so Read() needs no equivalent workaround.</summary>
public override void Write(PooledBinaryWriter _bw, TileEntity.StreamModeWrite _eStreamMode)
public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode)
{
base.Write(_bw, _eStreamMode);
PyramidWardWriteHelper.Write(_bw, EffectOn, ZoneShown);
}
public override void Read(PooledBinaryReader _br, TileEntity.StreamModeRead _eStreamMode)
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
{
base.Read(_br, _eStreamMode);
EffectOn = _br.ReadBoolean();
+4 -8
View File
@@ -139,7 +139,7 @@ namespace NecromancerTome
// the safety net for when that chain does not complete: opening the bracelet must
// never be what silently starts an empty vault over a saved one. Only then is a
// genuinely new bag created.
bag = SpatialVaultPersistence.LastLoadedVault ?? new Bag(slotCount);
bag = SpatialVaultPersistence.LastLoadedVault ?? new Bag(new Vector2i(slotCount, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
if (bag == SpatialVaultPersistence.LastLoadedVault)
{
Debug.Log("[NecromancerTome] SpatialVaultPatch: session cache was empty, adopted the last loaded vault (" +
@@ -149,13 +149,9 @@ namespace NecromancerTome
}
else if (bag.SlotCount < slotCount)
{
// Grow, never shrink - the skill level only ever goes up, so this only ever
// copies existing stacks into a bigger array, same shape
// EntityLootContainer.SetContent itself uses when it needs to resize a bag.
ItemStack[] oldSlots = bag.GetSlots();
ItemStack[] newSlots = ItemStack.CreateArray(slotCount);
Array.Copy(oldSlots, newSlots, oldSlots.Length);
bag.SetSlots(newSlots);
// Grow, never shrink - the skill level only ever goes up. В 3.3 это Bag.Resize:
// ItemStackGrid.Resize переносит все стопки в новый массив и досоздаёт слоты.
bag.Resize(new Vector2i(slotCount, 1));
}
Debug.Log("[NecromancerTome] SpatialVaultPatch: owner=" + player.entityId + " opened vault, " + slotCount + " slots (Necromancy level " + level + ")");
+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;
+7 -10
View File
@@ -457,16 +457,16 @@ namespace NecromancerTome
public static float SpendCharge(PickupJob _job)
{
ItemValue bracelet = _job.Bracelet;
if (bracelet == null || bracelet.Modifications == null || _job.ChannelSeconds <= 0f)
if (bracelet == null || bracelet.ModificationCount == 0 || _job.ChannelSeconds <= 0f)
{
return 0f;
}
float spent = 0f;
bool emptied = false;
for (int i = 0; i < bracelet.Modifications.Length; i++)
for (int i = 0; i < bracelet.ModificationCount; i++)
{
ItemValue mod = bracelet.Modifications[i];
ItemValue mod = bracelet.GetModification(i);
if (mod == null || mod.IsEmpty())
{
continue;
@@ -497,7 +497,7 @@ namespace NecromancerTome
// ItemValue.None is what an empty mod slot holds - type 0, which is exactly
// what IsEmpty() tests for, so the slot reads as free to every other piece of
// code including the gate in Begin.
bracelet.Modifications[i] = ItemValue.None;
bracelet.SetModification(i, ItemValue.None);
emptied = true;
Debug.Log("[NecromancerTome] SpatialVaultPickup: sphere in slot " + i +
" ran out (" + mod.UseTimes.ToString("0.#") + "/" + max +
@@ -585,7 +585,7 @@ namespace NecromancerTome
// a composite tile entity with a storage FEATURE rather than as their own class, so
// the question has to be asked of the feature - the same TryGetSelfOrFeature call the
// engine's own storage code uses.
if (tileEntity.TryGetSelfOrFeature(out ITileEntityLootable lootable) && !lootable.IsEmpty())
if (tileEntity.TryGetSelfOrFeature(out TEFeatureStorage storage) && !storage.IsEmpty())
{
Deny(_player, MsgNotEmpty);
return false;
@@ -616,15 +616,12 @@ namespace NecromancerTome
if (!Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.TryGetValue(_player.entityId, out Bag bag))
{
bag = SpatialVaultPersistence.LastLoadedVault ?? new Bag(slotCount);
bag = SpatialVaultPersistence.LastLoadedVault ?? new Bag(new Vector2i(slotCount, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults[_player.entityId] = bag;
}
if (bag.SlotCount < slotCount)
{
ItemStack[] oldSlots = bag.GetSlots();
ItemStack[] newSlots = ItemStack.CreateArray(slotCount);
System.Array.Copy(oldSlots, newSlots, oldSlots.Length);
bag.SetSlots(newSlots);
bag.Resize(new Vector2i(slotCount, 1));
}
return bag;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.