using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Гео-оссуарий (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). /// public static class GeoOssuary { public const string BlockName = "necroGeoOssuary"; /// Камень, уголь, селитра, железо (его дают железные жилы), сланец. 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); /// os1 -> 0, os2 -> 1, os3 -> 2; иначе -1. public static int ModSlotOf(BlockCollector.OutputType outputType) { string name = outputType?.Name; if (name == null || name.Length != 3 || !name.StartsWith("os")) return -1; return name[2] - '1'; } /// Ресурс в ячейке модификации, если он из списка; иначе null. 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); foreach (XUiC_ItemStack slot in __instance.itemControllers) { 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()); } } /// /// Шейдер модели. Своя модель оссуария лежит в нашем бандле с запасным шейдером /// NecromancerTome/OssuaryProxy (Standard-поверхность), а у пирамиды 10.09 именно /// Standard из бандла на расстоянии рисовался маджентой. Поэтому при первой загрузке модели /// материалу ставится тот шейдер, которым игра рисует ванильный коллектор росы: имена текстур /// у них общие (_MainTex, _Normal, _RMOM, _Emissive), модель выросла из его префаба. Префаб из /// бандла - общий ассет, так что правка одна на все экземпляры. /// [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 models = new System.Collections.Generic.HashSet { "necroOssuaryPrefab", "necroWardPrefab" }; static readonly System.Collections.Generic.HashSet done = new System.Collections.Generic.HashSet(); public static void Postfix(Transform __result) { if (__result == null || !models.Contains(__result.name) || !done.Add(__result.name)) return; Transform vanilla = DataLoader.LoadAsset(VanillaModel); Renderer src = vanilla != null ? vanilla.GetComponentInChildren(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(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); } /// Теги коллайдера. У ванильного префаба корень и объект collision помечены /// игровым тегом (AssetRipper записал его как unknown_20004 - имени он не знал), и именно /// по тегу игра понимает, что луч попал в блок, с которым можно взаимодействовать. В нашем /// Unity-проекте такого тега нет, при сборке он стал Untagged - и оссуарий не реагировал /// на наведение (тест 30.09). Берём теги у ванильного коллектора по именам объектов. static void CopyTags(Transform vanilla, Transform ours) { var byName = new System.Collections.Generic.Dictionary(); foreach (Transform t in vanilla.GetComponentsInChildren(true)) byName[t == vanilla ? "" : t.name] = t; foreach (Transform t in ours.GetComponentsInChildren(true)) { Transform src; if (!byName.TryGetValue(t == ours ? "" : 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; } } } } /// /// Расход сферы - по прочности, как в пространственном хранилище (указание 01.10: «одна /// сфера на стак - это жирно; 1/10 прочности на стак»). Ваниль в removeFuel вычитает ШТУКИ /// топлива, то есть съедала бы сферу целиком за каждый стак. Здесь вместо этого у первой /// сферы в отсеке списывается ChargePerStack прочности (DegradationMax сферы 500 -> 10 стаков /// со сферы); выработанная до нуля сфера исчезает из ячейки, как из браслета. Наличие топлива /// ваниль по-прежнему считает штуками (getFuelCount) - любая сфера с остатком годится. /// [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; } } }