1.4.2: проверка версии игры и хранилище, которое не стирается

Повод: 1.4.1 на игре 3.3.0 experimental не смогла прочитать Пространственное
хранилище (Bag переписан) и при сохранении записала пустой маркер - вещи
пропали навсегда.

GameVersionGuard: версия из Constants.cVersionInformation (не из const
cVersionMinor - та вшивается в DLL), поддерживается Major 3 и Minor/10 == 2,
то есть любая 3.2.x. На другой версии в главном меню окно «мод несовместим,
обновите» с единственной кнопкой «Выйти из игры» (решение пользователя), а
вместо PatchAll ставятся по отдельности только окно и четыре патча
сохранения хранилища. Всё, чем пользуется защита, сверено с декомпиляцией 3.3.

Хранилище: непрочитанные байты (чужая версия или упавший разбор) проходят
Read -> ToPlayer -> FromPlayer -> Write нетронутыми, ключ - entityId, сброс
при открытии главного меню (последнее сохранение игрока делается раньше,
внутри SaveAndCleanupWorld - проверено по коду игры). Пока байты не
прочитаны, хранилище не открывается и не пополняется, с подсказкой.

Проверено в игре на 3.2 через отладочные файлы FORCE_VAULT_UNREAD и
FORCE_VERSION_GUARD (в .gitignore, в архив не попадают): 603 байта
хранилища прошли сохранение и прочитались обратно совпадающими с исходными
(26 ячеек, 24 предмета); окно появляется, Esc его не закрывает, кнопка
закрывает игру. На настоящей 3.3 не проверено.

Localization: 4 новых ключа на 13 языках, всего 165; README - статус и
число ключей (было 149).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-09-25 00:09:01 +03:00
co-authored by Claude Opus 5.5
parent 77b56be9ae
commit f4470b784f
13 changed files with 296 additions and 6 deletions
+83 -1
View File
@@ -89,6 +89,37 @@ namespace NecromancerTome
public class VaultSlot
{
public Bag Bag;
/// <summary>Байты хранилища, которые эта сборка не прочитала: версия игры не та, под
/// которую собран мод, или разбор упал. Пишутся обратно в файл байт в байт.</summary>
public byte[] RawPayload;
}
/// <summary>ХРАНИЛИЩЕ НЕ ДОЛЖНО ТЕРЯТЬСЯ НА ЧУЖОЙ ВЕРСИИ ИГРЫ, 1.4.2 (2026-09-24). В 1.4.1 на
/// игре 3.3 разбор хранилища падал, и при сохранении мод писал вместо него ПУСТОЙ маркер -
/// вещи пропадали навсегда. Теперь непрочитанные байты проходят всю цепочку сохранения
/// нетронутыми: Read кладёт их в слот файла, ToPlayer переносит к игроку (сюда), FromPlayer
/// отдаёт новому файлу (игра на каждом сохранении создаёт НОВЫЙ PlayerDataFile), Write
/// пишет их как есть. Сборка под нужную версию потом прочтёт их обычным путём.
///
/// Ключ - entityId. Очищается при каждом открытии главного меню (GameVersionGuard.cs), чтобы
/// байты одного мира не перешли к персонажу другого.
///
/// Пока байты здесь, хранилище не открывается и не пополняется (SpatialVaultPatch.OpenVault,
/// SpatialVaultPickup.Begin): вещи, положенные в новое пустое хранилище, всё равно не
/// сохранились бы - запись отдаёт приоритет нетронутым байтам.</summary>
public static readonly System.Collections.Generic.Dictionary<int, byte[]> UnreadableByEntity =
new System.Collections.Generic.Dictionary<int, byte[]>();
public static bool IsLocked(EntityPlayer _player)
{
return _player != null && UnreadableByEntity.ContainsKey(_player.entityId);
}
public static void AttachRaw(PlayerDataFile _file, byte[] _raw)
{
AttachedVaults.Remove(_file);
AttachedVaults.Add(_file, new VaultSlot { RawPayload = _raw });
}
/// <summary>Vault attached to a PlayerDataFile while it is being written, read or
@@ -215,6 +246,17 @@ namespace NecromancerTome
{
return;
}
if (SpatialVaultPersistence.UnreadableByEntity.TryGetValue(_player.entityId, out byte[] raw))
{
SpatialVaultPersistence.AttachRaw(__instance, raw);
string rawLine = "FromPlayer entity " + _player.entityId + " - carrying " + raw.Length + " unread byte(s) unchanged";
if (rawLine != SpatialVaultPersistence.lastSaveLogged)
{
SpatialVaultPersistence.lastSaveLogged = rawLine;
Debug.Log("[NecromancerTome] SpatialVaultPersistence: " + rawLine);
}
return;
}
Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.TryGetValue(_player.entityId, out Bag bag);
string source = bag != null ? "session cache" : null;
if (bag == null && _player is EntityPlayerLocal && SpatialVaultPersistence.LastLoadedVault != null)
@@ -260,6 +302,21 @@ namespace NecromancerTome
string note;
Bag bag;
if (SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot rawSlot) &&
rawSlot.RawPayload != null)
{
SpatialVaultPersistence.UnreadableByEntity[_player.entityId] = rawSlot.RawPayload;
Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.Remove(_player.entityId);
if (isLocal)
{
SpatialVaultPersistence.LastLoadedVault = null;
}
Debug.LogWarning("[NecromancerTome] SpatialVaultPersistence: ToPlayer entity " + _player.entityId +
" - vault kept as " + rawSlot.RawPayload.Length + " unread byte(s); it stays locked and is saved unchanged");
return;
}
SpatialVaultPersistence.UnreadableByEntity.Remove(_player.entityId);
if (SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot slot))
{
bag = slot.Bag;
@@ -309,6 +366,11 @@ namespace NecromancerTome
try
{
SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot slot);
if (slot != null && slot.RawPayload != null)
{
SpatialVaultBlobIO.Write(_bw, slot.RawPayload);
return;
}
Bag bag = slot != null ? slot.Bag : null;
SpatialVaultBlobIO.Write(_bw, SpatialVaultPersistence.BuildPayload(bag));
if (bag == null)
@@ -346,7 +408,27 @@ namespace NecromancerTome
return;
}
Bag bag = SpatialVaultPersistence.ParsePayload(payload);
if (!GameVersionGuard.IsSupported || GameVersionGuard.ForceVaultUnread)
{
SpatialVaultPersistence.AttachRaw(__instance, payload);
Debug.LogWarning("[NecromancerTome] SpatialVaultPersistence: Read - game version not supported, " +
payload.Length + " byte(s) kept unread and will be saved unchanged");
return;
}
Bag bag;
try
{
bag = SpatialVaultPersistence.ParsePayload(payload);
}
catch (Exception parseError)
{
// Упал сам разбор (на 3.3 это был MissingMethodException на Bag.Read). Байты не
// выбрасываем: они уйдут в файл как были.
SpatialVaultPersistence.AttachRaw(__instance, payload);
Debug.LogError("[NecromancerTome] SpatialVaultPersistence: Read - could not parse the vault, " +
payload.Length + " byte(s) kept unread and will be saved unchanged: " + parseError);
return;
}
// Attached even when null: a blob that says "no vault" IS an answer, and ToPlayer
// uses it to clear a stale cache when a different save is loaded.
SpatialVaultPersistence.Attach(__instance, bag);