using System.Collections.Generic; using UnityEngine; namespace NecromancerTome { /// /// Работа духа урны рабочего (18.10.2026, BACKLOG «Урна рабочего»): изготовить предмет из ячейки образца, пока его в запасе /// меньше, чем в стопке образца (23.10 - без счёта: пока хватает ресурсов и есть куда класть). Решения 17.10: нити ящик -> урна - откуда ингредиенты и топливо, урна -> станция - где /// изготавливать, урна -> ящик - куда готовое (нет такой нити - готовое остаётся в выходе станции, выход полон - станция /// гаснет); только прямые рецепты (цепочки - несколько урн); качество 1; топливо - костру, химстанции, кузне, и станция /// гасится, когда делать нечего (верстаку, ступке, бетономешалке топливо не нужно). Изготавливает сама станция игры: дух /// только кладёт рецепт в её очередь, забрав ингредиенты, - время, огонь и выход считает игра. /// Запас = такие же предметы в ящиках «куда», в выходах станций и в их очередях. /// Одно дело за шаг, как у прочих духов (CinerariumWork.Step), _dry - пробный шаг без изменений. /// public static class CinerariumCraft { const int FuelModule = 3, MaterialModule = 4; sealed class Station { public Vector3i Pos; public TileEntityWorkstation Te; public string[] Areas; } public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, bool _dry, out bool _stuck, out Vector3i? _from) { _stuck = false; _from = null; ItemStack sample = _urn.Sample; List<(Vector3i p, TEFeatureStorage box)> src = new List<(Vector3i, TEFeatureStorage)>(); foreach (Vector3i p in _urn.ThreadFrom) { TEFeatureStorage b = CinerariumWork.BoxAt(_world, p); if (b != null) { src.Add((p, b)); } } List stations = new List(); List<(Vector3i p, TEFeatureStorage box)> outs = new List<(Vector3i, TEFeatureStorage)>(); foreach (Vector3i p in _urn.ThreadOut) { TileEntityWorkstation te = CinerariumThreads.StationAt(_world, p); if (te != null) { if (!te.IsUserAccessing()) { stations.Add(new Station { Pos = p, Te = te, Areas = AreasOf(_world.GetBlock(p).Block) }); } continue; } TEFeatureStorage b = CinerariumWork.BoxAt(_world, p); if (b != null) { outs.Add((p, b)); } } bool hasSample = sample != null && !sample.IsEmpty(); // 1. Готовое (только предмет образца - чужое изготовленное игроком не трогаем) - из выхода станции в ящики «куда». if (hasSample && outs.Count > 0) { foreach (Station st in stations) { ItemStack[] output = st.Te.Output; for (int i = 0; i < output.Length; i++) { ItemStack s = output[i]; if (s == null || s.IsEmpty() || s.itemValue.type != sample.itemValue.type) { continue; } Vector3i? dst = null; foreach ((Vector3i p, TEFeatureStorage box) o in outs) { if (CinerariumWork.Fits(o.box, s)) { dst = o.p; break; } } if (!dst.HasValue) { _stuck = true; // ящики «куда» полны continue; } _from = st.Pos; if (_dry) { return dst; } ItemStack moving = s.Clone(); List boxes = new List(); foreach ((Vector3i p, TEFeatureStorage box) o in outs) { boxes.Add(o.box); } CinerariumWork.Place(moving, boxes); output[i] = moving.count > 0 ? moving : ItemStack.Empty.Clone(); st.Te.Output = output; Debug.Log("[NecromancerTome] Cinerarium: рабочий перенёс " + (s.count - moving.count) + " " + s.itemValue.ItemClass?.GetItemName() + " из станции " + st.Pos); return dst; } } } // 2. Изготовить. 23.10, пользователь: «Рабочий должен делать бесконечное количество по образцу. Я положил 1 цемент, // а его задача наделать цемента столько, сколько возможно с учётом текущего количества ресурсов» - образец задаёт что, // не сколько (было: пока запас меньше стопки образца). Стоп - нет ингредиентов или готовое класть некуда. EntityPlayer owner = OwnerPlayer(_world, _urn); bool outRoom = false; foreach ((Vector3i p, TEFeatureStorage box) o in outs) { if (CinerariumWork.Fits(o.box, sample)) { outRoom = true; break; } } if (hasSample) { string name = sample.itemValue.ItemClass?.GetItemName(); foreach (Station st in stations) { int slot = FreeQueueSlot(st.Te); if (slot < 0) { continue; } if (!OutputHasRoom(st.Te, sample) && !outRoom) { continue; // выход полон и ящики «куда» полны (или их нет) - ниже станция погаснет } foreach (Recipe r in CraftingManager.GetRecipes(name)) { if (r.IsScrap || !AreaFits(r, st.Areas) || !ToolFits(r, st.Te) || owner != null && !r.IsUnlocked(owner)) { continue; } if (!Ingredients(r, st.Te, src, out Vector3i? srcAt, out string shortUnit)) { // Кузня: не хватает переплавленного материала - положить сырьё во вход (плавит сама, с огнём). if (shortUnit != null && FeedMaterial(st, shortUnit, src, _dry, out Vector3i? rawAt)) { _from = rawAt; if (!_dry) { Ignite(st.Te, src, false); } return st.Pos; } continue; } _from = srcAt; if (_dry) { return st.Pos; } TakeIngredients(r, st.Te, src); Enqueue(st.Te, slot, r); Ignite(st.Te, src, false); Debug.Log("[NecromancerTome] Cinerarium: рабочий поставил " + name + " на станцию " + st.Pos); return st.Pos; } } } // 3. Огонь: работа есть - топливо и огонь; нет - погасить (экономия топлива). foreach (Station st in stations) { if (!st.Te.isModuleUsed[FuelModule]) { continue; } bool busy = st.Te.hasRecipeInQueue() || RawInInput(st.Te); bool full = hasSample && !OutputHasRoom(st.Te, sample) && !outRoom; if (busy && !full) { if (st.Te.BurnTotalTimeLeft <= 0f) { Vector3i? fuelAt = FuelSource(src); if (fuelAt.HasValue) { _from = fuelAt; if (_dry) { return st.Pos; } Ignite(st.Te, src, true); return st.Pos; } } else if (!_dry && !st.Te.IsBurning) { st.Te.IsBurning = true; } } else if (st.Te.IsBurning) { // Погасить - тоже дело: дух летит к станции. Пробный шаг должен его найти - иначе настоящий шаг не // запускается и огонь горел до конца топлива (проверка 18.10: костёр, химстанция, кузня). if (_dry) { return st.Pos; } st.Te.IsBurning = false; Debug.Log("[NecromancerTome] Cinerarium: рабочий погасил станцию " + st.Pos + (full ? " - выход полон" : " - делать нечего")); if (full) { _stuck = true; } return st.Pos; } if (full) { _stuck = true; } } return null; } /// Чьи рецепты изучены: хозяин урны, если он в игре; в одиночной - игрок. Нет никого - не проверяем. static EntityPlayer OwnerPlayer(World _world, TEFeatureCinerarium _urn) { PlatformUserIdentifierAbs id = _urn.Parent?.Owner; if (id != null) { PersistentPlayerData ppd = GameManager.Instance.persistentPlayers?.GetPlayerData(id); if (ppd != null && _world.GetEntity(ppd.EntityId) is EntityPlayer p) { return p; } } return _world.GetPrimaryPlayer(); } /// Какие рецепты станция делает: Workstation.CraftingAreaRecipes или имя блока («player» - рецепты без станции). static string[] AreasOf(Block _b) { if (_b == null) { return new string[0]; } string a = _b.Properties.Contains("Workstation", "CraftingAreaRecipes") ? _b.Properties.GetString("Workstation", "CraftingAreaRecipes") : _b.GetBlockName(); return a.Replace(" ", "").Split(','); } static bool AreaFits(Recipe _r, string[] _areas) { string area = string.IsNullOrEmpty(_r.craftingArea) ? "player" : _r.craftingArea; foreach (string a in _areas) { if (a.EqualsCaseInsensitive(area)) { return true; } } return false; } static bool ToolFits(Recipe _r, TileEntityWorkstation _te) { if (_r.craftingToolType == 0) { return true; } foreach (ItemStack t in _te.Tools) { if (t != null && !t.IsEmpty() && t.itemValue.type == _r.craftingToolType) { return true; } } return false; } /// Свободная ячейка очереди: самая близкая к работающей (последней). static int FreeQueueSlot(TileEntityWorkstation _te) { RecipeQueueItem[] q = _te.Queue; for (int i = q.Length - 1; i >= 0; i--) { if (q[i] == null || q[i].Recipe == null || q[i].Multiplier <= 0) { return i; } } return -1; } static bool OutputHasRoom(TileEntityWorkstation _te, ItemStack _sample) { foreach (ItemStack s in _te.Output) { if (s == null || s.IsEmpty()) { return true; } int n = 1; if (s.itemValue.type == _sample.itemValue.type && s.CanStackPartly(ref n)) { return true; } } return false; } /// Индекс ячейки переплавленного материала кузни для unit_X (вход 3 + номер материала), или -1. static int UnitSlot(TileEntityWorkstation _te, string _itemName) { if (!_te.isModuleUsed[MaterialModule] || !_itemName.StartsWith("unit_")) { return -1; } string mat = _itemName.Substring(5); for (int j = 0; j < _te.materialNames.Length; j++) { if (_te.materialNames[j].EqualsCaseInsensitive(mat) && 3 + j < _te.Input.Length) { return 3 + j; } } return -1; } /// Хватает ли всего: переплавленное - в кузне, прочее - в ящиках «откуда». _srcAt - ящик, откуда первое; /// _shortUnit - какого переплавленного не хватило (кузне можно подложить сырья). static bool Ingredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, out Vector3i? _srcAt, out string _shortUnit) { _srcAt = null; _shortUnit = null; foreach (ItemStack ing in _r.ingredients) { if (ing == null || ing.IsEmpty() || ing.count <= 0) { continue; } string iname = ing.itemValue.ItemClass?.GetItemName() ?? ""; int us = UnitSlot(_te, iname); if (us >= 0) { if (_te.Input[us].count < ing.count) { _shortUnit = iname; return false; } continue; } int have = 0; foreach ((Vector3i p, TEFeatureStorage box) s in _src) { ItemStack[] grid = s.box.ItemGrid.items; for (int k = 0; k < grid.Length; k++) { if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k)) { have += grid[k].count; if (!_srcAt.HasValue) { _srcAt = s.p; } } } } if (have < ing.count) { return false; } } return true; } static void TakeIngredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src) { ItemStack[] input = _te.Input; bool inputChanged = false; foreach (ItemStack ing in _r.ingredients) { if (ing == null || ing.IsEmpty() || ing.count <= 0) { continue; } int us = UnitSlot(_te, ing.itemValue.ItemClass?.GetItemName() ?? ""); if (us >= 0) { input[us].count -= ing.count; inputChanged = true; continue; } int need = ing.count; foreach ((Vector3i p, TEFeatureStorage box) s in _src) { ItemStack[] grid = s.box.ItemGrid.items; for (int k = 0; k < grid.Length && need > 0; k++) { if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k)) { int take = Mathf.Min(need, grid[k].count); CinerariumWork.Take(s.box, k, take); need -= take; } } if (need <= 0) { break; } } } if (inputChanged) { _te.Input = input; } } static void Enqueue(TileEntityWorkstation _te, int _slot, Recipe _r) { RecipeQueueItem[] q = _te.Queue; float t = _r.craftingTime > 0f ? _r.craftingTime : 1f; ItemClass outClass = ItemClass.GetForId(_r.itemValueType); q[_slot] = new RecipeQueueItem { Recipe = _r, Multiplier = 1, CraftingTimeLeft = t, OneItemCraftTime = t, IsCrafting = _slot == q.Length - 1, Quality = (byte)(outClass != null && outClass.HasQuality ? 1 : 0), StartingEntityId = -1 }; _te.Queue = q; } static bool RawInInput(TileEntityWorkstation _te) { if (!_te.isModuleUsed[MaterialModule]) { return false; } for (int i = 0; i < 3 && i < _te.Input.Length; i++) { if (_te.Input[i] != null && !_te.Input[i].IsEmpty()) { return true; } } return false; } /// Сырьё для переплавки в unit_X: предмет из ящика «откуда», сделанный из этого материала, - во вход кузни. static bool FeedMaterial(Station _st, string _unit, List<(Vector3i p, TEFeatureStorage box)> _src, bool _dry, out Vector3i? _at) { _at = null; string mat = _unit.Substring(5); ItemStack[] input = _st.Te.Input; // 23.10 («вместо камней вплавляет булыжник»): сначала сырьё, у которого нет рецепта (камни, лом), сделанное руками // (булыжник) - только если сырья нет. Раньше - первое подходящее по порядку ячеек. foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, g => { ItemClass c = g.itemValue.ItemClass; if (c?.MadeOfMaterial?.ForgeCategory == null || !c.MadeOfMaterial.ForgeCategory.EqualsCaseInsensitive(mat) || c.GetItemName().StartsWith("unit_")) { return -1; } return CraftingManager.GetRecipe(c.GetItemName()) == null ? 0 : 1; })) { { int k = s.k; ItemStack g = s.box.ItemGrid.items[k]; ItemClass ic = g.itemValue.ItemClass; for (int i = 0; i < 3 && i < input.Length; i++) { bool empty = input[i] == null || input[i].IsEmpty(); int room = empty ? ic.MaxCount : input[i].itemValue.type == g.itemValue.type ? ic.MaxCount - input[i].count : 0; if (room <= 0) { continue; } _at = s.p; if (_dry) { return true; } int take = Mathf.Min(room, g.count); if (empty) { input[i] = new ItemStack(g.itemValue.Clone(), take); } else { input[i].count += take; } CinerariumWork.Take(s.box, k, take); _st.Te.Input = input; Debug.Log("[NecromancerTome] Cinerarium: рабочий положил в кузню " + _st.Pos + " " + take + " " + ic.GetItemName()); return true; } } } return false; } static Vector3i? FuelSource(List<(Vector3i p, TEFeatureStorage box)> _src) { foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, FuelRank)) { return s.p; } return null; } /// Порядок топлива (23.10: «вместо камней жжёт в печке сланец»): древесина, потом уголь, потом прочее горючее /// (сланец, бензин - ценное сырьё, только если больше нечего). -1 - не горит. static int FuelRank(ItemStack _g) { if (ItemClass.GetFuelValue(_g.itemValue) <= 0) { return -1; } string n = _g.itemValue.ItemClass?.GetItemName() ?? ""; return n.StartsWith("resourceWood") ? 0 : n.StartsWith("resourceCoal") ? 1 : 2; } /// Незакреплённые непустые ячейки ящиков «откуда», у которых _rank >= 0, по возрастанию ранга (при равном - /// в прежнем порядке ящиков и ячеек). static List<(Vector3i p, TEFeatureStorage box, int k)> Ordered(List<(Vector3i p, TEFeatureStorage box)> _src, System.Func _rank) { List<(Vector3i p, TEFeatureStorage box, int k, int r, int i)> all = new List<(Vector3i, TEFeatureStorage, int, int, int)>(); foreach ((Vector3i p, TEFeatureStorage box) s in _src) { ItemStack[] grid = s.box.ItemGrid.items; for (int k = 0; k < grid.Length; k++) { if (grid[k] == null || grid[k].IsEmpty() || CinerariumWork.Locked(s.box, k)) { continue; } int r = _rank(grid[k]); if (r >= 0) { all.Add((s.p, s.box, k, r, all.Count)); } } } all.Sort((a, b) => a.r != b.r ? a.r.CompareTo(b.r) : a.i.CompareTo(b.i)); List<(Vector3i p, TEFeatureStorage box, int k)> res = new List<(Vector3i, TEFeatureStorage, int)>(all.Count); foreach (var a in all) { res.Add((a.p, a.box, a.k)); } return res; } /// Огонь станции с топливом: нет топлива - стопку горючего из ящиков «откуда» (_addFuel), затем зажечь. static void Ignite(TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, bool _addFuel) { if (!_te.isModuleUsed[FuelModule]) { return; } if (_addFuel || _te.BurnTotalTimeLeft <= 0f) { ItemStack[] fuel = _te.Fuel; foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, FuelRank)) { { int k = s.k; ItemStack g = s.box.ItemGrid.items[k]; for (int i = 0; i < fuel.Length; i++) { bool empty = fuel[i] == null || fuel[i].IsEmpty(); int max = g.itemValue.ItemClass.MaxCount; int room = empty ? max : fuel[i].itemValue.type == g.itemValue.type ? max - fuel[i].count : 0; if (room <= 0) { continue; } int take = Mathf.Min(room, g.count); if (empty) { fuel[i] = new ItemStack(g.itemValue.Clone(), take); } else { fuel[i].count += take; } string fuelName = g.itemValue.ItemClass?.GetItemName(); // до Take: опустевшая ячейка очищает стопку (было NRE, 18.10) CinerariumWork.Take(s.box, k, take); _te.Fuel = fuel; Debug.Log("[NecromancerTome] Cinerarium: рабочий подбросил топливо " + take + " " + fuelName); goto lit; } } } } lit: if (_te.BurnTotalTimeLeft > 0f && !_te.IsBurning) { _te.IsBurning = true; } } } }