Files
necromants-tome-7d2d-3-2/HarmonySrc/CinerariumWork.cs
T

603 lines
21 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 System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Работа духа урны - этап 4 некротехнолога (указание 16.10: дух - УНИВЕРСАЛЬНЫЙ работник зоны; BACKLOG «Некротехнолог v2»).
/// Раз в StepSeconds - одно дело, по порядку:
/// 1. пополнить расходники коллекторов из ящиков зоны: ячейки топлива (банки, цветы, корм, сферы - что коллектор
/// принимает, GetFuelTypes) и катализатора (куры - GetCatalystTypes; по одной в пустую ячейку). Модификации
/// коллекторов не трогаются;
/// 2. забрать готовое из коллекторов (Items) и станций (TileEntityWorkstation.Output - верстак, кузня, химстанция,
/// костёр, бетономешалка) и сложить в ящики зоны: сначала к таким же предметам, потом в свободное место; ящики полны -
/// в сумку урны; некуда - дух чернеет и замирает над урной, оживает сам, когда место появится;
/// 3. грядки - последними: собрать созревшее (блок с меткой crops и добычей Harvest) с «идеальными бонусами»
/// (06.10: все бонусные доли урожая и семя - всегда), семя сразу посадить на то же место; затем засадить пустые
/// грядки случайными семенами из ящиков и сумки (семя - предмет-блок стадии 1, BlockPlantGrowing; можно ли сажать,
/// решает сам блок семени - CanPlaceBlockAt проверяет плодородие под ним, то есть грядку).
/// Коллектор или станция подходят любые - будущий крольчатник обслуживается тем же кодом. Ящик - составной блок игрока
/// с хранилищем (TEFeatureStorage, PlayerPlaced), кроме урн. Открытое у игрока (IsUserAccessing) не трогается.
/// Работает на сервере: зовётся из TEFeatureCinerarium.UpdateTick.
/// </summary>
public static class CinerariumWork
{
public static float StepSeconds = 3f;
/// <summary>Что в зоне: собирается заново на каждом шаге (150 клеток - дёшево, а ящики могут переставить).</summary>
sealed class Zone
{
public readonly List<TEFeatureStorage> Boxes = new List<TEFeatureStorage>();
public readonly List<TileEntityCollector> Collectors = new List<TileEntityCollector>();
public readonly List<TileEntityWorkstation> Stations = new List<TileEntityWorkstation>();
public readonly Dictionary<TileEntity, Vector3i> Where = new Dictionary<TileEntity, Vector3i>();
public TEFeatureStorage Bag;
public readonly List<Vector3i> Ripe = new List<Vector3i>();
public readonly List<Vector3i> Empty = new List<Vector3i>();
}
static readonly FastTags<TagGroup.Global> CropTag = FastTags<TagGroup.Global>.Parse("crops");
static readonly GameRandom _rnd = GameRandomManager.Instance.CreateGameRandom();
static readonly HashSet<TileEntity> _seen = new HashSet<TileEntity>();
/// <summary>Пробный шаг: найти дело и место, ничего не меняя.</summary>
static bool _dry;
/// <summary>Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда.
/// _dryRun - только узнать, где будет дело (дух летит туда заранее), ничего не трогая.</summary>
public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck, bool _dryRun)
{
_dry = _dryRun;
try
{
return DoStep(_world, _urn, out _stuck);
}
finally
{
_dry = false;
}
}
/// <summary>У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.</summary>
public static Vector3i? LastFrom;
/// <summary>Что выбрал последний шаг (для necrourn probe, 23.10: «стою в 19 м - не работают»).</summary>
public static string LastWhat;
/// <summary>Пробный шаг урны без изменений: что в зоне и что дух выбрал бы сейчас (команда necrourn x y z probe).</summary>
public static string Probe(World _world, TEFeatureCinerarium _urn)
{
string zone = "";
if (!_urn.IsLogist && !_urn.IsWorker)
{
Zone z = Scan(_world, _urn);
zone = "коллекторов " + z.Collectors.Count + ", станций " + z.Stations.Count + ", ящиков " + z.Boxes.Count +
", сумка " + (z.Bag != null) + ", спелых " + z.Ripe.Count + (z.Ripe.Count > 0 ? " (" + z.Ripe[0] + ")" : "") +
", пустых клеток " + z.Empty.Count + "; ";
}
Vector3i? at = Step(_world, _urn, out bool stuck, true);
return zone + "шаг: " + (at.HasValue ? LastWhat + " в " + at.Value : "нечего делать") + (stuck ? ", класть некуда" : "");
}
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
LastFrom = null;
LastWhat = "?";
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
if (_urn.IsLogist)
{
return Ferry(_world, _urn, out _stuck);
}
// Урна рабочего - изготовление по образцу на станциях по нитям (CinerariumCraft.cs).
if (_urn.IsWorker)
{
Vector3i? at = CinerariumCraft.Step(_world, _urn, _dry, out _stuck, out Vector3i? from);
LastFrom = from;
return at;
}
Zone z = Scan(_world, _urn);
// 1. Расходники.
foreach (TileEntityCollector c in z.Collectors)
{
if (Refill(c, c.FuelSlots, c.GetFuelTypes(), false, z) || Refill(c, c.CatalystSlots, c.GetCatalystTypes(), true, z))
{
if (!_dry)
{
c.SetModified();
}
LastWhat = "расходники коллектору";
return z.Where[c];
}
}
// 2. Готовое.
bool blocked = false;
foreach (TileEntityCollector c in z.Collectors)
{
ItemStack[] items = c.Items;
if (Unload(items, z, ref blocked))
{
if (!_dry)
{
c.Items = items;
c.SetModified();
}
LastWhat = "готовое из коллектора";
return z.Where[c];
}
}
foreach (TileEntityWorkstation w in z.Stations)
{
ItemStack[] output = w.Output;
if (Unload(output, z, ref blocked))
{
if (!_dry)
{
w.Output = output;
}
LastWhat = "готовое со станции";
return z.Where[w];
}
}
// 3. Грядки.
foreach (Vector3i p in z.Ripe)
{
if (Harvest(_world, p, z, ref blocked))
{
LastWhat = "сбор урожая";
return p;
}
}
if (z.Empty.Count > 0)
{
Vector3i? planted = PlantRandom(_world, z);
if (planted.HasValue)
{
LastWhat = "посадка";
return planted;
}
}
_stuck = blocked;
return null;
}
/// <summary>Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». Только предметы включённых в урне групп
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём, _stuck - урна
/// мигает (18.10). Открытые у игрока ящики и закреплённые ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
if (!_urn.HasThreadTo)
{
return null;
}
TEFeatureStorage dst = BoxAt(_world, _urn.ThreadTo);
if (dst == null)
{
return null;
}
foreach (Vector3i f in _urn.ThreadFrom)
{
if (f == _urn.ThreadTo)
{
continue;
}
TEFeatureStorage src = BoxAt(_world, f);
if (src == null)
{
continue;
}
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack s = src.ItemGrid[k];
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)))
{
continue;
}
if (!Fits(dst, s))
{
_stuck = true; // есть что везти, но некуда
continue;
}
LastFrom = f;
_stuck = false; // везти есть куда - не застрял
if (_dry)
{
return _urn.ThreadTo;
}
ItemStack moving = s.Clone();
Place(moving, new[] { dst });
int moved = s.count - moving.count;
if (moved <= 0)
{
continue;
}
Take(src, k, moved);
dst.SetModified();
Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo);
return _urn.ThreadTo;
}
}
return null;
}
internal static TEFeatureStorage BoxAt(World _world, Vector3i _p)
{
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
c.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage s) ? s : null;
}
static bool IsRipe(Block _b)
{
return _b != null && !(_b is BlockPlantGrowing) && _b.HasAnyFastTags(CropTag) &&
_b.itemsToDrop.TryGetValue(EnumDropEvent.Harvest, out List<Block.SItemDropProb> l) && l != null && l.Count > 0;
}
static bool IsSeed(ItemValue _iv, out BlockValue _bv)
{
_bv = BlockValue.Air;
if (_iv == null || _iv.type == 0 || !_iv.ItemClass.IsBlock())
{
return false;
}
_bv = _iv.ToBlockValue();
return _bv.Block is BlockPlantGrowing;
}
/// <summary>Урожай: все доли добычи Harvest (бонусы - тоже, вероятность только у обычных долей), множители сервера на
/// урожай и семена. Одно семя - сразу в землю на то же место, остальное - в ящики/сумку. Нет места ни для чего -
/// грядку не трогаем, дух чернеет.</summary>
static bool Harvest(World _world, Vector3i _p, Zone _z, ref bool _blocked)
{
Block crop = _world.GetBlock(_p).Block;
List<ItemStack> got = new List<ItemStack>();
BlockValue replant = BlockValue.Air;
foreach (Block.SItemDropProb d in crop.itemsToDrop[EnumDropEvent.Harvest])
{
ItemValue iv = ItemClass.GetItem(d.name);
if (iv == null || iv.type == 0)
{
continue;
}
bool seed = IsSeed(iv, out BlockValue seedBv);
bool bonus = d.tag != null && (d.tag.Contains("Bonus") || d.tag.Contains("bonus"));
if (!bonus && d.prob < 1f && _rnd.RandomFloat > d.prob)
{
continue;
}
int n = _rnd.RandomRange(d.minCount, d.maxCount + 1);
n = Mathf.RoundToInt(n * (seed ? XUiM_Recipes.SeedDropOutputModifier : XUiM_Recipes.CropOutputModifier));
if (seed && replant.isair && n > 0)
{
replant = seedBv;
n--;
}
if (n > 0)
{
got.Add(new ItemStack(iv, n));
}
}
if (got.Count > 0 && !HasRoom(got[0], _z))
{
_blocked = true;
return false;
}
if (_dry)
{
return true;
}
_world.SetBlockRPC(_p, BlockValue.Air);
if (!replant.isair)
{
replant.rotation = (byte)_rnd.RandomRange(0, 4);
_world.SetBlockRPC(_p, replant);
}
foreach (ItemStack s in got)
{
Place(s, _z.Boxes);
if (s.count > 0 && _z.Bag != null)
{
Place(s, new[] { _z.Bag });
}
if (s.count > 0)
{
// Влезло не всё (проверяли только первую долю) - остаток падает у урны, не пропадает.
GameManager.Instance.ItemDropServer(s.Clone(), World.blockToTransformPos(_p) + Vector3.up * 0.5f, Vector3.zero);
}
}
Debug.Log("[NecromancerTome] Cinerarium: дух собрал " + crop.GetBlockName() + " в " + _p + (replant.isair ? "" : ", посадил снова"));
return true;
}
static bool HasRoom(ItemStack _s, Zone _z)
{
foreach (TEFeatureStorage t in _z.Boxes)
{
if (Fits(t, _s))
{
return true;
}
}
return _z.Bag != null && Fits(_z.Bag, _s);
}
internal static bool Fits(TEFeatureStorage _t, ItemStack _s)
{
for (int i = 0; i < _t.ItemGrid.Length; i++)
{
ItemStack x = _t.ItemGrid[i];
if (x == null || x.IsEmpty())
{
return true;
}
int n = _s.count;
if (x.itemValue.type == _s.itemValue.type && x.CanStackPartly(ref n))
{
return true;
}
}
return false;
}
/// <summary>Засадить одну пустую грядку случайным семенем из сумки и ящиков (06.10: «случайно сажает имеющиеся семена»).</summary>
static Vector3i? PlantRandom(World _world, Zone _z)
{
List<(TEFeatureStorage src, int slot, BlockValue bv)> seeds = new List<(TEFeatureStorage, int, BlockValue)>();
foreach (TEFeatureStorage src in Sources(_z))
{
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack st = src.ItemGrid[k];
if (st != null && !st.IsEmpty() && !Locked(src, k) && IsSeed(st.itemValue, out BlockValue bv))
{
seeds.Add((src, k, bv));
}
}
}
if (seeds.Count == 0)
{
return null;
}
foreach (Vector3i p in _z.Empty)
{
var pick = seeds[_rnd.RandomRange(0, seeds.Count)];
BlockValue bv = pick.bv;
bv.rotation = (byte)_rnd.RandomRange(0, 4);
if (!bv.Block.CanPlaceBlockAt(_world, p, bv))
{
continue;
}
if (_dry)
{
return p;
}
Take(pick.src, pick.slot, 1);
_world.SetBlockRPC(p, bv);
Debug.Log("[NecromancerTome] Cinerarium: дух посадил " + bv.Block.GetBlockName() + " в " + p);
return p;
}
return null;
}
static Zone Scan(World _world, TEFeatureCinerarium _urn)
{
Zone z = new Zone();
_seen.Clear();
Vector3i c = _urn.ToWorldPos();
if (_urn.Parent != null)
{
_urn.Parent.TryGetSelfOrFeature<TEFeatureStorage>(out z.Bag);
_seen.Add(_urn.Parent);
}
int w = TEFeatureCinerarium.HalfWidth;
for (int dy = -TEFeatureCinerarium.Below; dy <= TEFeatureCinerarium.Above; dy++)
{
for (int dx = -w; dx <= w; dx++)
{
for (int dz = -w; dz <= w; dz++)
{
Vector3i p = new Vector3i(c.x + dx, c.y + dy, c.z + dz);
BlockValue bv = _world.GetBlock(p);
if (bv.isair)
{
// Пустая клетка над плодородным блоком - кандидат в грядку (окончательно решит блок семени).
if (_world.GetBlock(p - Vector3i.up).Block?.blockMaterial?.FertileLevel > 0)
{
z.Empty.Add(p);
}
continue;
}
if (!bv.ischild && IsRipe(bv.Block))
{
z.Ripe.Add(p);
continue;
}
Vector3i owner = bv.ischild ? p + bv.parent : p; // часть составного блока (верстак 2x1) - к главной клетке
TileEntity te = _world.GetTileEntity(owner);
if (te == null || !_seen.Add(te) || te.IsUserAccessing())
{
continue;
}
if (te is TileEntityCollector col)
{
z.Collectors.Add(col);
z.Where[col] = owner;
}
else if (te is TileEntityWorkstation ws)
{
z.Stations.Add(ws);
z.Where[ws] = owner;
}
else if (te is TileEntityComposite comp && comp.PlayerPlaced &&
!comp.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage box))
{
z.Boxes.Add(box);
z.Where[comp] = owner;
}
}
}
}
return z;
}
/// <summary>Дополнить ячейки коллектора тем, что он принимает, из ящиков и сумки. _single - по одному предмету в
/// пустую ячейку (куры), иначе до полного стака. true - что-то положено.</summary>
static bool Refill(TileEntityCollector _c, ItemStack[] _slots, string[] _allowed, bool _single, Zone _z)
{
if (_slots == null || _allowed == null || _allowed.Length == 0)
{
return false;
}
for (int i = 0; i < _slots.Length; i++)
{
ItemStack slot = _slots[i];
ItemClass want = null;
int room;
if (slot == null || slot.IsEmpty())
{
room = -1; // любой допустимый
}
else
{
if (_single)
{
continue;
}
want = slot.itemValue.ItemClass;
room = want != null ? want.MaxCount - slot.count : 0;
if (room <= 0)
{
continue;
}
}
foreach (TEFeatureStorage src in Sources(_z))
{
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack s = src.ItemGrid[k];
if (s == null || s.IsEmpty() || Locked(src, k))
{
continue;
}
ItemClass ic = s.itemValue.ItemClass;
if (ic == null || (want != null ? ic != want : System.Array.IndexOf(_allowed, ic.GetItemName()) < 0))
{
continue;
}
int take = _single ? 1 : (room < 0 ? Mathf.Min(s.count, ic.MaxCount) : Mathf.Min(s.count, room));
if (take <= 0)
{
continue;
}
if (_dry)
{
return true;
}
if (want == null)
{
_slots[i] = new ItemStack(s.itemValue.Clone(), take);
}
else
{
_slots[i].count += take;
}
Take(src, k, take);
return true;
}
}
}
return false;
}
/// <summary>Одна стопка готового - в ящики (к таким же, потом в пустое), затем в сумку урны. true - что-то перенесено;
/// _blocked - была стопка, которой не нашлось места.</summary>
static bool Unload(ItemStack[] _items, Zone _z, ref bool _blocked)
{
if (_items == null)
{
return false;
}
for (int i = 0; i < _items.Length; i++)
{
ItemStack s = _items[i];
if (s == null || s.IsEmpty())
{
continue;
}
if (_dry)
{
if (HasRoom(s, _z))
{
return true;
}
_blocked = true;
continue;
}
int before = s.count;
// Сначала ящики зоны (к таким же, потом в пустое), они полны - тогда сумка урны (решение 06.10).
Place(s, _z.Boxes);
if (s.count > 0 && _z.Bag != null)
{
Place(s, new[] { _z.Bag });
}
if (s.count == 0)
{
_items[i] = ItemStack.Empty.Clone();
return true;
}
if (s.count != before)
{
return true;
}
_blocked = true;
}
return false;
}
internal static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
{
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
{
_dst[i].TryStackItem(0, _s);
}
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
{
if (_dst[i].AddItem(_s.Clone()))
{
_s.count = 0;
}
}
}
/// <summary>Откуда брать расходники: сумка урны (там то, что дух накопил), потом ящики зоны.</summary>
static IEnumerable<TEFeatureStorage> Sources(Zone _z)
{
if (_z.Bag != null)
{
yield return _z.Bag;
}
foreach (TEFeatureStorage b in _z.Boxes)
{
yield return b;
}
}
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
internal static bool Locked(TEFeatureStorage _src, int _slot)
{
PackedBoolArray locks = _src.ItemGrid.SlotLocks;
return locks != null && _slot < locks.Length && locks[_slot];
}
internal static void Take(TEFeatureStorage _src, int _slot, int _count)
{
ItemStack s = _src.ItemGrid[_slot].Clone();
s.count -= _count;
_src.UpdateSlot(_slot, s.count > 0 ? s : ItemStack.Empty.Clone());
_src.SetModified();
}
}
}