Откат крови и Кровавая сфера вместо неё
Чинит поломку сейвов, внесённую предыдущим коммитомe362c62, и заменяет её механику на безопасную. ЧТО БЫЛО СЛОМАНО.e362c62перенёс resourceNecromancerBlood из items.xml в item_modifiers.xml, чтобы кровь вставлялась в Пространственный браслет. Класс предмета определяет БАЙТОВУЮ РАСКЛАДКУ каждого его стака в сейве: ItemValue.Read строка 1094: if ((version > 4 || HasQuality) && !(itemClass is ItemClassModifier)) ItemValue.Write строка 1228: if (!(ItemClass is ItemClassModifier)) Обычный предмет пишет байт числа модификаций и байт числа косметических слотов; ItemClassModifier не пишет ни того, ни другого. Сейв, записанный до переноса, после переноса читается со сдвигом - поток съезжает на первом же стаке крови, и PlayerDataFile.Load падает. Бэкап .ttp.bak умирает вместе с основным файлом: он того же формата. В тестовом мире персонаж потерян вместе с бэкапом, игра откатилась на Respawning: NewGame. В коммитеe362c62написано "сейв цел" со ссылкой на assignIdsFromMapping. Айди действительно берутся из сохранённого name->id мэппинга - но ломается не айди, а раскладка байтов, и к мэппингу это отношения не имеет. Была проверена не та вещь. ОТКАТ. Кровь вернулась в items.xml обычным <item>. Набор её свойств сверен с7172681и совпадает посимвольно: убраны и Stacknumber=1, и прочность (ShowQuality + DegradationBreaksAfter + effect_group DegradationMax). Обе правки были безвредны для сейва, но существовали ради отменённой механики - по указанию пользователя откат доведён до "как было", а не до "как было плюс безвредное". Стак снова 15, наследуется от medicalBloodBag. В комментарии у предмета оставлено предупреждение с номерами строк Read/Write - единственное, что вынесено из аварии, и единственное, ради чего стоит читать этот комментарий целиком. КРОВАВАЯ СФЕРА. Расходник браслета теперь отдельный предмет, и это ровно то, что делает правку безопасной: resourceBloodSphere - НОВОЕ имя, в старых сейвах его нет, значит нет и ни одного стака, который читался бы по другой раскладке. Общее правило, выведенное из аварии: предмет, который уже мог попасть в чужой инвентарь, нельзя переводить между ItemClass и ItemClassModifier ни в какую сторону - нужна модификация, заводи новый предмет. Продиктовано: доступна на первом грейде, без станка, 1 кровь некроманта + 5 праха зомби дают две сферы, прочность 500. В руке - камень с алым тинтом. - item_modifiers.xml: installable_tags="necroBracelet", свой modifier_tags, type="attachment", DegradationMax 500 в tiered="false" группе. Extends=modGeneralMaster, а НЕ resourceRockSmall: вместе с камнем приезжали бы Action0 ThrowAway и ThrowableDecoy, то есть сферу можно было бы метать. Меш берётся строкой Meshfile, наследовать ради него весь предмет не нужно. - Вид в руке: HoldType 40 и все три меша (Meshfile/HandMeshfile/DropMeshfile) на rock_smallPrefab плюс TintColor "220, 30, 45". Путь проверенный - на этом же меше с таким же тинтом в моде живут Камень духов и оба портальных камня. - recipes.xml: count=2, без craft_area и без тега разблокировки (группа 1 открыта с уровня 1, рецепт без тега доступен всегда - как у Камня духов). - progression.xml: сфера в unlock_entry группы 1, unlock_tier="1". - SpatialVaultPickupPatch: SpendBlood -> SpendCharge, имя из новой константы ChargeItemName. Гейт пустого слота в Begin снова включён - после отката он был временно закомментирован, потому что вставлять было нечего. ИКОНКИ. Свои, от пользователя: BloodSphere.png и BloodStone.png, 160x160 RGBA, в ItemIconAtlas и ItemIconAtlasGreyscale. Серая копия обязательна - без неё у заблокированной записи в скиллах не будет картинки вообще. Способ её получения подобран сверкой с существующими файлами и совпал ПОБИТОВО: convert('L') на RGB без альфы, альфа приклеивается обратно отдельным каналом. Записано в BACKLOG.md, раньше это нигде не было зафиксировано. Иконка камня положена заранее - самого предмета ещё нет, он запланирован. ПРОВЕРЕНО, ЧТО НИЧЕГО БОЛЬШЕ НЕ ЕДЕТ. Сверка с7172681: набор <item> в items.xml не изменился, в item_modifiers.xml единственное добавление - resourceBloodSphere, и ни у одного существующего предмета не менялись Tags, ShowQuality и Stacknumber. То есть ни один предмет не сменил класс и не сменил раскладку. В игре: мир грузится без ошибок, сфера крафтится и тратится - в логе "sphere in slot 0 now 12/500 used" за 12-секундный канал. --- Revert the blood, and a Blood Sphere in its place Fixes the save corruption introduced bye362c62and replaces the mechanic behind it with a safe one. WHAT WAS BROKEN.e362c62moved resourceNecromancerBlood from items.xml into item_modifiers.xml so it could be installed in the Spatial Bracelet. An item's CLASS decides the BYTE LAYOUT of every stack of it in a save: ItemValue.Read line 1094: if ((version > 4 || HasQuality) && !(itemClass is ItemClassModifier)) ItemValue.Write line 1228: if (!(ItemClass is ItemClassModifier)) A plain item writes a modifications count and a cosmetic-slot count; an ItemClassModifier writes neither. A save written before the move reads out of step after it - the stream slips on the first blood stack and PlayerDataFile.Load throws. The .ttp.bak backup dies with the main file, being the same format. In the test world the character was lost along with its backup and the game fell back to Respawning: NewGame. e362c62's message claimed "saves are safe", citing assignIdsFromMapping. Item ids really do come from the stored name->id mapping - but what breaks is not the id, it is the byte layout, and the mapping has nothing to do with it. The wrong thing was verified. THE REVERT. The blood is a plain <item> in items.xml again. Its property set was diffed against7172681and matches character for character: both Stacknumber=1 and the durability (ShowQuality + DegradationBreaksAfter + the DegradationMax effect_group) are gone. Both were harmless to the save format, but both existed only to serve the cancelled mechanic - on the user's instruction the revert goes back to "as it was", not "as it was plus whatever I judged harmless". The stack is 15 again, inherited from medicalBloodBag. A warning carrying the Read/Write line numbers stays in the item's comment - the one thing worth keeping out of this accident. THE BLOOD SPHERE. The bracelet's charge is its own item now, and that is precisely what makes this safe: resourceBloodSphere is a NEW name, absent from every existing save, so no stack of it can be read under the wrong layout. The general rule the accident produced: an item that may already sit in someone's inventory must never be moved between ItemClass and ItemClassModifier in either direction - if a modifier is wanted, make a new item. Dictated: available at the first grade, no workstation, 1 Necromancer's Blood + 5 zombie ash makes two spheres, durability 500. Held, it is a stone with a scarlet tint. - item_modifiers.xml: installable_tags="necroBracelet", its own modifier_tags, type="attachment", DegradationMax 500 in a tiered="false" group. Extends=modGeneralMaster, NOT resourceRockSmall: the rock would have brought Action0 ThrowAway and ThrowableDecoy with it, making the sphere throwable. The mesh comes from the Meshfile line; inheriting a whole item for it is not needed. - Held look: HoldType 40 and all three meshes (Meshfile/HandMeshfile/ DropMeshfile) on rock_smallPrefab, plus TintColor "220, 30, 45". A proven path - the Spirit Stone and both portal stones already live on that mesh with that same kind of tint. - recipes.xml: count=2, no craft_area and no unlock tag (group 1 opens at level 1, and a recipe with no tag is simply always available, as with the Spirit Stone). - progression.xml: the sphere joins group 1's unlock_entry at unlock_tier="1". - SpatialVaultPickupPatch: SpendBlood -> SpendCharge, the name coming from a new ChargeItemName constant. The empty-slot gate in Begin is switched back on - it was commented out during the revert because nothing could be installed. ICONS. The user's own art: BloodSphere.png and BloodStone.png, 160x160 RGBA, in both ItemIconAtlas and ItemIconAtlasGreyscale. The greyscale copy is mandatory - without it a locked skill entry has no picture at all. How those copies are made was worked out by diffing against the existing files and matched BIT FOR BIT: convert('L') over RGB without the alpha, with the alpha merged back as its own channel. Written up in BACKLOG.md; it had never been recorded anywhere. The stone's icon is filed ahead of the item, which is still only planned. VERIFIED THAT NOTHING ELSE SHIFTS. Diffed against7172681: the set of <item> entries in items.xml is unchanged, the only addition to item_modifiers.xml is resourceBloodSphere, and no existing item had its Tags, ShowQuality or Stacknumber changed. No item changed class, and no item changed layout. In game: the world loads clean, and the sphere crafts and drains - the log shows "sphere in slot 0 now 12/500 used" for a 12-second channel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MnwP2Dt1vk8bUPJ452EoVL
This commit is contained in:
co-authored by
Claude Opus 5
parent
e362c627e7
commit
896501dcd8
@@ -135,6 +135,22 @@ namespace NecromancerTome
|
||||
/// <summary>The denial sound vanilla plays with these tooltips.</summary>
|
||||
public const string DeniedSound = "ui_denied";
|
||||
|
||||
/// <summary>What the bracelet burns to pull a block: the Кровавая сфера, dictated
|
||||
/// 2026-09-15. Its own definition is in Config/item_modifiers.xml.
|
||||
///
|
||||
/// WHY A SEPARATE ITEM AND NOT THE BLOOD ITSELF - this is the scar of the 15.09 accident
|
||||
/// and the reason not to "simplify" it back. The blood was moved into item_modifiers.xml
|
||||
/// so it could be installed here, and that destroyed a character's save along with its
|
||||
/// backup: an item's CLASS decides the byte layout of every stack of it
|
||||
/// (ItemValue.Read:1094 / Write:1228), so a save written before the move became
|
||||
/// unreadable. The sphere is a NEW name that no old save contains, which is what makes it
|
||||
/// safe. Full write-up in BACKLOG.md.
|
||||
///
|
||||
/// The Кровавый камень, when it exists, goes in the same slot and is NOT charged here -
|
||||
/// it is the infinite one. Nothing to add for it: this check names the sphere, so
|
||||
/// anything else in the slot simply pays nothing.</summary>
|
||||
public const string ChargeItemName = "resourceBloodSphere";
|
||||
|
||||
/// <summary>Unscaled time at which a cancel last opened the vault, or -1. Exists to stop
|
||||
/// ONE press from opening the vault TWICE: the cancel reacts to the button going down,
|
||||
/// while the bracelet's ordinary power attack reacts to it coming back up, and those are
|
||||
@@ -399,7 +415,7 @@ namespace NecromancerTome
|
||||
// AFTER the block is gone and the item is in the vault, never before: every refusal
|
||||
// above returns early, and blood paid for a pull that was then refused would be blood
|
||||
// charged for nothing. This is the only place the flask is spent.
|
||||
float spent = SpendBlood(job);
|
||||
float spent = SpendCharge(job);
|
||||
|
||||
// The vault lives in memory and is written out with the player's own save data; this
|
||||
// is the same commit point closing the vault window uses, so a block taken and then
|
||||
@@ -409,22 +425,17 @@ namespace NecromancerTome
|
||||
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPickup: owner=" + job.Player.entityId + " took " +
|
||||
blockValue.Block.GetBlockName() + " at " + job.Position + " into the vault for " +
|
||||
spent.ToString("0.#") + " of blood");
|
||||
spent.ToString("0.#") + " of charge");
|
||||
}
|
||||
|
||||
/// <summary>Charges this pull to the Necromancer's Blood in the bracelet's mod slot, one
|
||||
/// point of durability per second of channel (user request 2026-09-15). Returns what was
|
||||
/// actually taken, which is not always what was asked for - see the clamp.
|
||||
/// <summary>Charges this pull to the Кровавая сфера in the bracelet's mod slot, one point
|
||||
/// of durability per second of channel (user request 2026-09-15). Returns what was
|
||||
/// actually taken.
|
||||
///
|
||||
/// WHY THE NAME CHECK AND NOT "whatever is in the slot". The instruction is explicit -
|
||||
/// "если эта модификация кровь некроманта" - and it has to stay that way: the slot is
|
||||
/// meant to take other things later (the message the empty slot prints already promises a
|
||||
/// Blood Stone), and those will have their own price, or none. A mod that is not blood
|
||||
/// pays nothing here and the pull still happens - deliberately, because refusing it would
|
||||
/// be a second rule nobody asked for.
|
||||
///
|
||||
/// The name comes from NecromancerBloodPatch rather than a second literal in this file,
|
||||
/// so the two cannot drift apart if the item is ever renamed.
|
||||
/// WHY THE NAME CHECK AND NOT "whatever is in the slot". The slot is meant to take the
|
||||
/// Кровавый камень too, and that one is explicitly the infinite version - it must pay
|
||||
/// nothing. Naming the sphere here gets that for free: anything else installed is simply
|
||||
/// not charged, and the pull still happens.
|
||||
///
|
||||
/// NOT ENOUGH BLOOD IS NOT A REFUSAL (user request 2026-09-15: "пусть поглощение всё-равно
|
||||
/// сработает, но флакон крови некроманта после этого пусть исчезнет из слота", clarified to
|
||||
@@ -443,7 +454,7 @@ namespace NecromancerTome
|
||||
///
|
||||
/// WHY THE NAME CHECK AND NOT "whatever is in the slot" - see above; a mod that is not
|
||||
/// blood pays nothing, is not emptied, and the pull still happens.</summary>
|
||||
public static float SpendBlood(PickupJob _job)
|
||||
public static float SpendCharge(PickupJob _job)
|
||||
{
|
||||
ItemValue bracelet = _job.Bracelet;
|
||||
if (bracelet == null || bracelet.Modifications == null || _job.ChannelSeconds <= 0f)
|
||||
@@ -460,7 +471,7 @@ namespace NecromancerTome
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (mod.ItemClass == null || mod.ItemClass.Name != NecromancerBloodPatch.BloodItemName)
|
||||
if (mod.ItemClass == null || mod.ItemClass.Name != ChargeItemName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -475,7 +486,7 @@ namespace NecromancerTome
|
||||
// said out loud instead.
|
||||
if (max <= 0)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] SpatialVaultPickup: blood in slot " + i +
|
||||
Debug.LogWarning("[NecromancerTome] SpatialVaultPickup: sphere in slot " + i +
|
||||
" has MaxUseTimes 0 - nothing to spend, flask kept. Check the " +
|
||||
"DegradationMax passive_effect in Config/item_modifiers.xml");
|
||||
continue;
|
||||
@@ -488,13 +499,13 @@ namespace NecromancerTome
|
||||
// code including the gate in Begin.
|
||||
bracelet.Modifications[i] = ItemValue.None;
|
||||
emptied = true;
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPickup: blood in slot " + i +
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPickup: sphere in slot " + i +
|
||||
" ran out (" + mod.UseTimes.ToString("0.#") + "/" + max +
|
||||
") - flask removed from the bracelet");
|
||||
") - sphere removed from the bracelet");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPickup: blood in slot " + i + " now " +
|
||||
Debug.Log("[NecromancerTome] SpatialVaultPickup: sphere in slot " + i + " now " +
|
||||
mod.UseTimes.ToString("0.#") + "/" + max + " used");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user