Хранилище браслета переживает перезапуск; торговцы чёрно-белые
Исправляет первый баг-репорт мода на Nexus (youkia96581, 11.09.2026): "Items stored in the space bracelet will disappear after leaving the game and going online again". Причина была записана в коде как нерешённая: PlayerVaults - обычный статический Dictionary, save/load не существовало. ХРАНИЛИЩЕ ТЕПЕРЬ ЖИВЁТ В PlayerDataFile, рядом с рюкзаком игрока. Так решено после вопроса пользователя "почему не сделать принцип как у ящика?": ящик хранит вещи тем, что они лежат в чанке (у TileEntity единственный конструктор TileEntity(Chunk)), а браслету нужен был дом в чём-то, что движок и так сохраняет. Четыре постфикса - FromPlayer/Write/Read/ToPlayer, блоб с магией "NECROVLT" и явной длиной дописывается после всего ванильного. Байтовая часть - в сателлитной сборке: PooledBinaryWriter.Write не резолвится из основного проекта (CS7069), как и у PyramidWardWriteHelper. Два дефекта, найденные и убитые по дороге живыми тестами: 1. ModEvents.WorldShuttingDown приходит ПЕРЕД финальным сохранением игрока (GameManager.SaveAndCleanupWorld: событие на IL_0026, SaveLocalPlayerData на IL_00c4). Обработчик, чистивший там кэш, затирал хранилище на каждом корректном выходе. Обработчик убран; свежесть решает авторитетность ToPlayer, а не таймер. 2. Пустой сессионный кэш трактовался как "хранилища нет" и записывался поверх настоящего. Путь восстановления имеет право не сработать, удалять он права не имеет - добавлена страховка LastLoadedVault. Проверено в игре: положил -> вышел -> запустил заново -> вещи на месте, блоб на 54 байта сверен в .ttp побайтово. ТОРГОВЦЫ (npcTraderJoel/Rekt/Bob/Hugh/Jen) - чёрно-белые. Шейдер НЕ подменяется: материал клонируется со своим шейдером, меняется только текстура альбедо на обесцвеченную копию, так что свет, нормали и скиннинг остаются движковыми. Альбедо ищется обходом свойств шейдера, а не по имени: тело - Game/Character/_Albedo, волосы - Game/Autodesk/_MainTex. Плюс 1% прозрачности с сохранением _ZWrite. Опрос раз в 2 с, потому что торговцы стримятся на подходе, а Джен собирается в рантайме. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
This commit is contained in:
co-authored by
Claude Opus 5
parent
a5f8592903
commit
29431990f6
@@ -30,19 +30,15 @@ namespace NecromancerTome
|
||||
/// this rounds to 0 - deliberately left as-is, not special-cased away, matching the
|
||||
/// Knife's own "0 at 0 kills is a feature, not a bug" precedent - a tooltip explains it
|
||||
/// instead of silently opening a useless empty window.
|
||||
/// - PERSISTENCE - the one thing NOT fully solved here, flagged rather than silently
|
||||
/// assumed: the Bag backing each player's vault lives in a plain in-memory
|
||||
/// Dictionary<int, Bag> in this file (PlayerVaults below), keyed by entityId. This
|
||||
/// is reliable for as long as the game process keeps running (survives death/respawn/
|
||||
/// relogging within one play session, confirmed by how a static field behaves) but has
|
||||
/// NOT been wired into any save/load system - closing the game entirely and reloading the
|
||||
/// save later will NOT bring the vault's contents back (no persistence file, no hook into
|
||||
/// PersistentPlayerData or a world-save event). Building real cross-session persistence
|
||||
/// (a custom save file + ModEvents.GameSave/Load hooks, or piggybacking on an owned
|
||||
/// world entity the way the summoned pets do - unconfirmed whether THOSE actually survive
|
||||
/// a full restart either) is real, separate follow-up work, not attempted here. Treat
|
||||
/// this like a session-scoped stash until that's built and confirmed - don't rely on it
|
||||
/// across game restarts yet.
|
||||
/// - PERSISTENCE - solved 2026-09-13, see SpatialVaultPersistence.cs. It was NOT solved
|
||||
/// when this item shipped, and that shortfall is exactly what became the mod's first
|
||||
/// Nexus bug report (youkia96581, 11 Sep 2026: "Items stored in the space bracelet will
|
||||
/// disappear after leaving the game and going online again"). PlayerVaults below is still
|
||||
/// the in-memory, entityId-keyed Dictionary it always was, but it is now only the session
|
||||
/// cache: the durable copy is written into the player's own PlayerDataFile, alongside the
|
||||
/// backpack, by four postfixes on FromPlayer/ToPlayer/Write/Read. Read that file's comment
|
||||
/// for why there ("почему не сделать принцип как у ящика?" - because a chest's items live
|
||||
/// in a chunk, and the bracelet's closest equivalent home is its owner's save data).
|
||||
///
|
||||
/// REGULAR ATTACK (index 0) - knock back + slow whatever zombie the crosshair is aimed at:
|
||||
/// - Same raycast mechanism HarmonySrc/ThiefLoopPatch.cs already established for
|
||||
@@ -71,8 +67,9 @@ namespace NecromancerTome
|
||||
public const float MaxRange = 50f;
|
||||
public const float ShoveDistance = 6f;
|
||||
|
||||
/// <summary>See the class-level comment above for exactly what this does and doesn't
|
||||
/// guarantee - session-scoped only, not yet saved/loaded across game restarts.</summary>
|
||||
/// <summary>Session cache only - the durable copy lives on disk, see
|
||||
/// SpatialVaultPersistence.cs. Cleared on WorldShuttingDown so a different save loaded
|
||||
/// afterwards cannot inherit this world's vault through a recycled entityId.</summary>
|
||||
public static readonly Dictionary<int, Bag> PlayerVaults = new Dictionary<int, Bag>();
|
||||
|
||||
public static bool Prefix(ItemActionData _actionData, bool _bReleased)
|
||||
@@ -118,7 +115,17 @@ namespace NecromancerTome
|
||||
|
||||
if (!PlayerVaults.TryGetValue(player.entityId, out Bag bag))
|
||||
{
|
||||
bag = new Bag(slotCount);
|
||||
// Normally a restored vault is already here - the ToPlayer postfix puts it in
|
||||
// when the game applies the save file to the spawning player. LastLoadedVault is
|
||||
// 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);
|
||||
if (bag == SpatialVaultPersistence.LastLoadedVault)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPatch: session cache was empty, adopted the last loaded vault (" +
|
||||
bag.SlotCount + " slots, " + bag.GetUsedSlotCount() + " used)");
|
||||
}
|
||||
PlayerVaults[player.entityId] = bag;
|
||||
}
|
||||
else if (bag.SlotCount < slotCount)
|
||||
@@ -134,7 +141,21 @@ namespace NecromancerTome
|
||||
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPatch: owner=" + player.entityId + " opened vault, " + slotCount + " slots (Necromancy level " + level + ")");
|
||||
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player);
|
||||
XUiC_BagStorageWindowGroup.Open(playerUI.xui, player, bag, LootContainer.GetLootContainer("roboticDrone"), Localization.Get("braceletSpatialVaultWindowTitle"));
|
||||
// The trailing callbacks are vanilla's own optional parameters (_onModified, _onClose).
|
||||
// _onModified is not needed: the vault lives in PlayerVaults, and PlayerDataFile's
|
||||
// FromPlayer postfix reads it fresh every time the game saves the player, so there is
|
||||
// nothing to flush per item move. _onClose asks for a player-data save right away, so
|
||||
// closing the window is a commit point rather than waiting for the next autosave -
|
||||
// SaveLocalPlayerData is the game's own routine call and no-ops when saving is not
|
||||
// active (which is the correct behaviour on a client, where the server owns the file).
|
||||
XUiC_BagStorageWindowGroup.Open(
|
||||
playerUI.xui,
|
||||
player,
|
||||
bag,
|
||||
LootContainer.GetLootContainer("roboticDrone"),
|
||||
Localization.Get("braceletSpatialVaultWindowTitle"),
|
||||
null,
|
||||
() => GameManager.Instance.SaveLocalPlayerData());
|
||||
}
|
||||
|
||||
public static void ShoveZombieAtCrosshair(EntityPlayerLocal player)
|
||||
|
||||
Reference in New Issue
Block a user