diff --git a/HarmonySrc/GameVersionGuard.cs b/HarmonySrc/GameVersionGuard.cs index 58d3d5f..8ecfe59 100644 --- a/HarmonySrc/GameVersionGuard.cs +++ b/HarmonySrc/GameVersionGuard.cs @@ -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 /// public static class GameVersionGuard { - /// Под какую версию собран мод: Major и десяток Minor (3.2.x = 3 и 2). + /// Под какую версию собран мод: Major и десяток Minor (3.3.x = 3 и 3; сборка 1.4.x была под 3.2). 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; diff --git a/HarmonySrc/NecromancerBloodPatch.cs b/HarmonySrc/NecromancerBloodPatch.cs index d0841ee..af901eb 100644 --- a/HarmonySrc/NecromancerBloodPatch.cs +++ b/HarmonySrc/NecromancerBloodPatch.cs @@ -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; diff --git a/HarmonySrc/PyramidWardPatch.cs b/HarmonySrc/PyramidWardPatch.cs index ad04be9..b041868 100644 --- a/HarmonySrc/PyramidWardPatch.cs +++ b/HarmonySrc/PyramidWardPatch.cs @@ -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. - 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(); diff --git a/HarmonySrc/SpatialVaultPatch.cs b/HarmonySrc/SpatialVaultPatch.cs index fd0ba89..20fd7bf 100644 --- a/HarmonySrc/SpatialVaultPatch.cs +++ b/HarmonySrc/SpatialVaultPatch.cs @@ -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 + ")"); diff --git a/HarmonySrc/SpatialVaultPersistence.cs b/HarmonySrc/SpatialVaultPersistence.cs index 02e999e..15f1270 100644 --- a/HarmonySrc/SpatialVaultPersistence.cs +++ b/HarmonySrc/SpatialVaultPersistence.cs @@ -79,8 +79,16 @@ namespace NecromancerTome { /// 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. - 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, + /// ветка версии < 2), поэтому хранилище из сейва 3.2 переезжает без потерь. + /// Номер поднят, чтобы сборка под 3.2 не приняла новый формат за свой. + public const byte PayloadVersion = 2; + + /// Старейший номер, который эта сборка ещё читает. + public const byte OldestReadablePayloadVersion = 1; /// 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. public static string lastSaveLogged; - /// 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. + /// Builds the opaque payload SpatialVaultBlobIO wraps. Заголовок (версия и + /// «есть ли сумка») пишется в MemoryStream напрямую, а сама сумка - через + /// PooledBinaryWriter поверх того же потока: в 3.3 Bag.Write принимает только его. + /// Сами Write-перегрузки PooledBinaryWriter из этого проекта не вызываются (см. + /// NecromancerTEPersistence.csproj), поэтому заголовок и не идёт через него. 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(); } } - /// Null when the payload holds no vault or is a version we do not know. + /// Null when the payload holds no vault. Незнакомый номер версии - исключение, + /// а НЕ null: null значит «хранилища нет», и следующее сохранение стёрло бы вещи. + /// Исключение ловит Read-постфикс и сохраняет байты нетронутыми (AttachRaw). 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; diff --git a/HarmonySrc/SpatialVaultPickupPatch.cs b/HarmonySrc/SpatialVaultPickupPatch.cs index 6182e14..b10483c 100644 --- a/HarmonySrc/SpatialVaultPickupPatch.cs +++ b/HarmonySrc/SpatialVaultPickupPatch.cs @@ -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; } diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll index c2e2a06..681260c 100644 Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ diff --git a/NecromancerHarmony.pdb b/NecromancerHarmony.pdb index aa1279e..512db90 100644 Binary files a/NecromancerHarmony.pdb and b/NecromancerHarmony.pdb differ diff --git a/NecromancerTEPersistence.dll b/NecromancerTEPersistence.dll index 2a1a9d3..c493e6e 100644 Binary files a/NecromancerTEPersistence.dll and b/NecromancerTEPersistence.dll differ