Files
necromants-tome-7d2d-3-2/HarmonySrc/GeoOssuaryPatch.cs
T
AlexCubeandClaude Opus 5.5 c2610da59a Оссуарий-котёл: одна ячейка образца, хранилище 6, перенос старых ячеек, свечение, огонёк, рецепт в кузне (#66, #69)
- blocks.xml: 6 ячеек хранилища (цикл вдвое дольше - добыча прежняя);
  видно одну ячейку образца и одну топлива, остальные спрятаны (в данных
  их по-прежнему 3 и 6 - сейвы не режутся).
- Перенос: содержимое спрятанных ячеек у уже поставленных оссуариев
  уходит в хранилище (проверено на test8: уголь, камень, сферы).
- Свечение рун и окошек и сфера на крышке - пока работает (OssuaryGlow).
- Огонёк дикого духа: вылет из окошка с уходом в землю и затуханием,
  возврат с другой стороны в противоположное окошко; только пока работает.
  В игре пока не увиден - разобрать (necroossuary wisp on|off).
- Рецепт - кузня, железо вместо дерева и юкки (плавильня / лом).
- Описание на 13 языках под котёл и вольных духов.
- Консоль: necroossuary info / put (отладка ячеек).
- DLL включает и #72, #74.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:56:41 +03:00

366 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Гео-оссуарий (1.6.0, 2026-09-30): станция добычи ресурсов на кровавой сфере. Блок -
/// necroGeoOssuary в blocks.xml, наследник ванильного коллектора росы; всё, что коллектор
/// умеет сам (окно, топливо, таймеры, работа без игрока рядом), остаётся его. Здесь только то,
/// чего ваниль не умеет, - разобрано по декомпилятору 3.3:
///
/// - ЯЧЕЙКА МОДИФИКАЦИИ ПРИНИМАЕТ ОДИН ПРЕДМЕТ. XUiC_DewCollectorModGrid.SetTileEntity
/// зовёт SetAllowedItemClassSingle(RequiredMods[i]). У оссуария в любую из трёх ячеек идёт
/// любой из пяти ресурсов, по одной штуке (OverrideStackCount = 1). Окно у всех
/// коллекторов одно и то же, поэтому для чужих коллекторов предел штук сбрасывается
/// обратно - иначе коллектор росы после оссуария тоже брал бы по одной.
/// - ВАНИЛЬ НЕ СМОТРИТ, ЧТО ЛЕЖИТ В ЯЧЕЙКЕ. ModTypes=Type - просто флаг HasModConvert
/// (OutputItem -> OutputItemModded), одна замена на весь блок. У оссуария три OutputType
/// os1..os3, по одному на ячейку продукции; os(i+1) выдаёт полный стак ресурса из ячейки
/// модификации i. Пустая или чужая модификация - этот OutputType выключен (isDisabled),
/// значит таймер стоит и сферу он не ест.
/// - ПРОВЕРКА НЕБА. HandleSkyCheck (7 блоков вверх) у ванили безусловна для всех
/// коллекторов; оссуарий работает где угодно, в том числе под землёй (решение 30.09).
/// </summary>
public static class GeoOssuary
{
public const string BlockName = "necroGeoOssuary";
/// <summary>Камень, уголь, селитра, железо (его дают железные жилы), сланец.</summary>
public static readonly string[] ResourceNames =
{
"resourceRockSmall", "resourceCoal", "resourcePotassiumNitratePowder",
"resourceScrapIron", "resourceOilShale",
};
static ItemClass[] resourceClasses;
public static ItemClass[] ResourceClasses
{
get
{
if (resourceClasses == null)
{
resourceClasses = new ItemClass[ResourceNames.Length];
for (int i = 0; i < ResourceNames.Length; i++)
resourceClasses[i] = ItemClass.GetItemClass(ResourceNames[i]);
}
return resourceClasses;
}
}
public static bool Is(Block block) => block != null && block.GetBlockName() == BlockName;
public static bool Is(TileEntityCollector te) => te != null && Is(te.blockValue.Block);
/// <summary>Ячейка образца для выхода: у котла (#66, 22.10) образец один - все выходы os1..os6 берут ресурс из
/// ячейки 0 («Ячейка под образец - одна»). Не наш выход - -1.</summary>
public static int ModSlotOf(BlockCollector.OutputType outputType)
{
string name = outputType?.Name;
if (name == null || name.Length != 3 || !name.StartsWith("os"))
return -1;
return 0;
}
/// <summary>Видимых в окне ячеек образца и топлива (#66, 22.10: «Ячейка под образец - одна; ячейка под кровавую
/// сферу (или камень) - одна»). В данных их по-прежнему 3 и 6: длины пишутся в сейв, а при загрузке игра обрезает
/// лишнее по конфигу - уменьшение молча выбросило бы содержимое. Лишние прячутся в окне, их старое содержимое
/// переносит в хранилище Patch_TileEntityCollector_UpdateTick_GeoOssuaryMigrate.</summary>
public const int VisibleModSlots = 1;
public const int VisibleFuelSlots = 1;
/// <summary>Ресурс в ячейке модификации, если он из списка; иначе null.</summary>
public static ItemClass ResourceIn(TileEntityCollector te, int modSlot)
{
ItemStack[] mods = te.ModSlots;
if (modSlot < 0 || mods == null || modSlot >= mods.Length)
return null;
ItemStack stack = mods[modSlot];
if (stack == null || stack.IsEmpty())
return null;
ItemClass cls = stack.itemValue.ItemClass;
return cls != null && System.Array.IndexOf(ResourceNames, cls.Name) >= 0 ? cls : null;
}
}
[HarmonyPatch(typeof(XUiC_DewCollectorModGrid), nameof(XUiC_DewCollectorModGrid.SetTileEntity))]
public static class Patch_XUiC_DewCollectorModGrid_SetTileEntity_GeoOssuary
{
public static void Postfix(XUiC_DewCollectorModGrid __instance, TileEntityCollector te)
{
bool ossuary = GeoOssuary.Is(te);
for (int i = 0; i < __instance.itemControllers.Length; i++)
{
XUiC_ItemStack slot = __instance.itemControllers[i];
// окно у всех коллекторов общее - чужим возвращаем все ячейки
slot.ViewComponent.IsVisible = !ossuary || i < GeoOssuary.VisibleModSlots;
if (!(slot is XUiC_RequiredItemStack required))
continue;
required.OverrideStackCount = ossuary ? 1 : -1;
if (ossuary)
{
required.SetAllowedItemClasses(GeoOssuary.ResourceClasses);
required.RequiredItemOnly = true;
}
}
}
}
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.isDisabled))]
public static class Patch_TileEntityCollector_isDisabled_GeoOssuary
{
public static void Postfix(TileEntityCollector __instance, BlockCollector.OutputType _outputType, ref bool __result)
{
if (__result || !GeoOssuary.Is(__instance))
return;
if (GeoOssuary.ResourceIn(__instance, GeoOssuary.ModSlotOf(_outputType)) == null)
__result = true;
}
}
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.newItem))]
public static class Patch_TileEntityCollector_newItem_GeoOssuary
{
public static void Postfix(TileEntityCollector __instance, BlockCollector.OutputType outputType, ref ItemStack __result)
{
if (__result == null || !GeoOssuary.Is(__instance))
return;
ItemClass resource = GeoOssuary.ResourceIn(__instance, GeoOssuary.ModSlotOf(outputType));
if (resource == null)
{
// isDisabled не пускает сюда без ресурса; если всё же дошло - сфера уже списана,
// и отдать хотя бы заглушку честнее, чем молча съесть топливо.
Debug.LogWarning("[NecromancerTome] GeoOssuary: " + outputType.Name + " сработал без ресурса в ячейке модификации");
return;
}
__result = new ItemStack(new ItemValue(resource.Id), resource.MaxCount);
Debug.Log("[NecromancerTome] GeoOssuary: " + outputType.Name + " -> " + resource.Name + " x" + resource.MaxCount + " в " + __instance.ToWorldPos());
}
}
/// <summary>
/// Шейдер модели. Своя модель оссуария лежит в нашем бандле с запасным шейдером
/// NecromancerTome/OssuaryProxy (Standard-поверхность), а у пирамиды 10.09 именно
/// Standard из бандла на расстоянии рисовался маджентой. Поэтому при первой загрузке модели
/// материалу ставится тот шейдер, которым игра рисует ванильный коллектор росы: имена текстур
/// у них общие (_MainTex, _Normal, _RMOM, _Emissive), модель выросла из его префаба. Префаб из
/// бандла - общий ассет, так что правка одна на все экземпляры.
/// </summary>
[HarmonyPatch(typeof(BlockShapeModelEntity), nameof(BlockShapeModelEntity.getPrefab))]
public static class Patch_BlockShapeModelEntity_getPrefab_GeoOssuaryShader
{
const string VanillaModel = "@:Entities/Furniture/collectorDewPrefab.prefab";
// Пирамида отрицания (01.10) собрана из копии префаба оссуария - те же имена объектов,
// тот же запасной шейдер, поэтому лечится тем же постфиксом.
static readonly System.Collections.Generic.HashSet<string> models =
new System.Collections.Generic.HashSet<string> { "necroOssuaryPrefab", "necroWardPrefab" };
static readonly System.Collections.Generic.HashSet<string> done = new System.Collections.Generic.HashSet<string>();
public static void Postfix(Transform __result)
{
if (__result == null || !models.Contains(__result.name) || !done.Add(__result.name))
return;
Transform vanilla = DataLoader.LoadAsset<Transform>(VanillaModel);
Renderer src = vanilla != null ? vanilla.GetComponentInChildren<Renderer>(true) : null;
Shader shader = src != null && src.sharedMaterial != null ? src.sharedMaterial.shader : null;
if (shader == null)
{
Debug.LogWarning("[NecromancerTome] GeoOssuary: шейдер коллектора росы не найден - остаётся запасной");
return;
}
int n = 0;
foreach (Renderer r in __result.GetComponentsInChildren<Renderer>(true))
foreach (Material m in r.sharedMaterials)
if (m != null && m.shader != shader) { m.shader = shader; m.enableInstancing = true; n++; }
Debug.Log("[NecromancerTome] GeoOssuary: материалам модели (" + n + ") поставлен шейдер '" + shader.name + "'");
CopyTags(vanilla, __result);
}
/// <summary>Теги коллайдера. У ванильного префаба корень и объект collision помечены
/// игровым тегом (AssetRipper записал его как unknown_20004 - имени он не знал), и именно
/// по тегу игра понимает, что луч попал в блок, с которым можно взаимодействовать. В нашем
/// Unity-проекте такого тега нет, при сборке он стал Untagged - и оссуарий не реагировал
/// на наведение (тест 30.09). Берём теги у ванильного коллектора по именам объектов.</summary>
static void CopyTags(Transform vanilla, Transform ours)
{
var byName = new System.Collections.Generic.Dictionary<string, Transform>();
foreach (Transform t in vanilla.GetComponentsInChildren<Transform>(true))
byName[t == vanilla ? "<root>" : t.name] = t;
foreach (Transform t in ours.GetComponentsInChildren<Transform>(true))
{
Transform src;
if (!byName.TryGetValue(t == ours ? "<root>" : t.name, out src))
continue;
if (t.tag != src.tag || t.gameObject.layer != src.gameObject.layer)
{
Debug.Log("[NecromancerTome] GeoOssuary: '" + t.name + "' тег " + t.tag + " -> " + src.tag +
", слой " + t.gameObject.layer + " -> " + src.gameObject.layer);
t.tag = src.tag;
t.gameObject.layer = src.gameObject.layer;
}
}
}
}
/// <summary>
/// Расход сферы - по прочности, как в пространственном хранилище (указание 01.10: «одна
/// сфера на стак - это жирно; 1/10 прочности на стак»). Ваниль в removeFuel вычитает ШТУКИ
/// топлива, то есть съедала бы сферу целиком за каждый стак. Здесь вместо этого у первой
/// сферы в отсеке списывается ChargePerStack прочности (DegradationMax сферы 500 -> 10 стаков
/// со сферы); выработанная до нуля сфера исчезает из ячейки, как из браслета. Наличие топлива
/// ваниль по-прежнему считает штуками (getFuelCount) - любая сфера с остатком годится.
/// </summary>
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.removeFuel))]
public static class Patch_TileEntityCollector_removeFuel_GeoOssuary
{
public const int ChargePerStack = 50;
public static bool Prefix(TileEntityCollector __instance)
{
if (!GeoOssuary.Is(__instance))
return true;
ItemStack[] fuel = __instance.FuelSlots;
// Кровавый камень в отсеке - вечное топливо (01.10: «полная замена сферы»): не тратится
// ни он, ни сферы рядом.
for (int i = 0; i < fuel.Length; i++)
if (fuel[i] != null && !fuel[i].IsEmpty() && fuel[i].itemValue.ItemClass != null &&
fuel[i].itemValue.ItemClass.Name == BoneSteedFuel.StoneItem)
return false;
for (int i = 0; i < fuel.Length; i++)
{
ItemStack stack = fuel[i];
if (stack == null || stack.IsEmpty() || stack.itemValue.ItemClass == null ||
stack.itemValue.ItemClass.Name != SpatialVaultPickup.ChargeItemName)
continue;
int max = stack.itemValue.MaxUseTimes;
if (max <= 0)
{
// Прочность не задалась (DegradationMax) - ведём себя как ваниль: сфера за стак.
fuel[i] = ItemStack.Empty;
Debug.LogWarning("[NecromancerTome] GeoOssuary: у сферы MaxUseTimes 0 - списана целиком");
return false;
}
stack.itemValue.UseTimes += ChargePerStack;
if (stack.itemValue.UseTimes >= max)
{
fuel[i] = ItemStack.Empty;
Debug.Log("[NecromancerTome] GeoOssuary: сфера в ячейке " + i + " выработана и исчезла");
}
return false;
}
return false;
}
}
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.HandleSkyCheck))]
public static class Patch_TileEntityCollector_HandleSkyCheck_GeoOssuary
{
public static void Postfix(TileEntityCollector __instance, ref bool __result)
{
if (__result && GeoOssuary.Is(__instance))
__result = false;
}
}
/// <summary>Одна ячейка топлива в окне (GeoOssuary.VisibleFuelSlots): остальные ячейки сетки и их подсказки-картинки
/// спрятаны. Сетка общая для всех коллекторов (и для катализатора) - чужим всё показываем обратно.</summary>
public static class GeoOssuaryFuelGrid
{
public static void Apply(XUiC_CollectorFuelGrid _grid)
{
if (_grid?.tileEntity == null || _grid.fuelBehavior != XUiC_CollectorFuelGrid.FuelBehavior.Fuel)
return;
bool ossuary = GeoOssuary.Is(_grid.tileEntity);
int n = _grid.tileEntity.FuelGridLength;
for (int i = 0; i < _grid.itemControllers.Length; i++)
{
bool show = i < n && (!ossuary || i < GeoOssuary.VisibleFuelSlots);
if (_grid.itemControllers[i].ViewComponent.IsVisible != show && (ossuary || i < n))
_grid.itemControllers[i].ViewComponent.IsVisible = show;
if (_grid.previewSprites != null && i < _grid.previewSprites.Length && _grid.previewSprites[i] != null && ossuary && i >= GeoOssuary.VisibleFuelSlots)
_grid.previewSprites[i].IsVisible = false;
}
}
}
[HarmonyPatch(typeof(XUiC_CollectorFuelGrid), nameof(XUiC_CollectorFuelGrid.SetTileEntity))]
public static class Patch_XUiC_CollectorFuelGrid_SetTileEntity_GeoOssuary
{
public static void Postfix(XUiC_CollectorFuelGrid __instance) => GeoOssuaryFuelGrid.Apply(__instance);
}
[HarmonyPatch(typeof(XUiC_CollectorFuelGrid), nameof(XUiC_CollectorFuelGrid.OnTileEntityChanged))]
public static class Patch_XUiC_CollectorFuelGrid_OnTileEntityChanged_GeoOssuary
{
public static void Postfix(XUiC_CollectorFuelGrid __instance) => GeoOssuaryFuelGrid.Apply(__instance);
}
/// <summary>Shift-клик топливом: у оссуария - только в видимую ячейку (ваниль раскладывает по всей сетке).</summary>
[HarmonyPatch(typeof(XUiC_CollectorFuelGrid), nameof(XUiC_CollectorFuelGrid.TryAddFuel))]
public static class Patch_XUiC_CollectorFuelGrid_TryAddFuel_GeoOssuary
{
public static bool Prefix(XUiC_CollectorFuelGrid __instance, ItemClass newItemClass, ItemStack newItemStack, ref bool __result)
{
if (__instance.fuelBehavior != XUiC_CollectorFuelGrid.FuelBehavior.Fuel || !GeoOssuary.Is(__instance.tileEntity))
return true;
if (System.Array.IndexOf(__instance.tileEntity.GetFuelTypes(), newItemClass.Name) >= 0)
{
XUiC_ItemStack slot = __instance.itemControllers[0];
if (slot.ItemStack.IsEmpty())
{
slot.ItemStack = newItemStack.Clone();
newItemStack.count = 0;
}
else
{
int n = slot.ItemStack.StackTransferCount(newItemStack);
slot.ItemStack.count += n;
newItemStack.count -= n;
}
__instance.UpdateBackend(__instance.getUISlots());
}
__result = newItemStack.count == 0;
return false;
}
}
/// <summary>Переход на котёл (#66, 22.10: «у уже поставленных оссуариев при первой загрузке новой версии всё лишнее из
/// старых ячеек образцов и топлива переложить в новое хранилище - ничего не теряется»). Сервер на каждом тике смотрит
/// спрятанные ячейки (образцы 2-3, топливо 2-6) и перекладывает их содержимое в свободные ячейки хранилища; нет места -
/// ждёт, пока игрок заберёт добычу.</summary>
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.UpdateTick))]
public static class Patch_TileEntityCollector_UpdateTick_GeoOssuaryMigrate
{
public static void Postfix(TileEntityCollector __instance)
{
if (!GeoOssuary.Is(__instance) || !SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer)
return;
bool moved = Move(__instance, __instance.ModSlots, GeoOssuary.VisibleModSlots, "образца");
moved |= Move(__instance, __instance.FuelSlots, GeoOssuary.VisibleFuelSlots, "топлива");
if (moved)
__instance.SetModified();
}
static bool Move(TileEntityCollector _te, ItemStack[] _from, int _keep, string _what)
{
bool moved = false;
ItemStack[] store = _te.Items;
for (int i = _keep; i < _from.Length; i++)
{
if (_from[i] == null || _from[i].IsEmpty())
continue;
int free = System.Array.FindIndex(store, s => s == null || s.IsEmpty());
if (free < 0)
return moved;
store[free] = _from[i].Clone();
_from[i] = ItemStack.Empty.Clone();
moved = true;
Debug.Log("[NecromancerTome] GeoOssuary: " + store[free].itemValue.ItemClass?.Name + " x" + store[free].count +
" из ячейки " + _what + " " + (i + 1) + " -> хранилище " + (free + 1) + " в " + _te.ToWorldPos());
}
return moved;
}
}
}