- Блоки логиста на модели кадила: по центру на подставке (necroCinerariumLogist) и новый вид у стены на кронштейне (necroCinerariumLogistWall/+Skull); выбор вида - R с урной в руке. - Дух логиста втрое меньше, в покое сидит в кадиле, на перевозку вылетает через окно крышки (у стены - не в стену) и возвращается тем же окном. - Застрял (ящик «куда» полон) - урна мигает (CinerariumBlink) вместо почерневшего духа; прежняя зелёная подмена свечения старой модели убрана. - Команды разработки без мыши: necrospawn, necroexit [quit], necrolook, necroaim, necroplace, necrohold, necrobox, necrourn, necroclick. Проверено в игре (test8): перевозка обоими видами, вылет духа, вид у стены от стены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
580 lines
19 KiB
C#
580 lines
19 KiB
C#
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;
|
||
|
||
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
|
||
{
|
||
_stuck = false;
|
||
LastFrom = null;
|
||
// Урна с нитями - логист (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();
|
||
}
|
||
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();
|
||
}
|
||
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;
|
||
}
|
||
return z.Where[w];
|
||
}
|
||
}
|
||
// 3. Грядки.
|
||
foreach (Vector3i p in z.Ripe)
|
||
{
|
||
if (Harvest(_world, p, z, ref blocked))
|
||
{
|
||
return p;
|
||
}
|
||
}
|
||
if (z.Empty.Count > 0)
|
||
{
|
||
Vector3i? planted = PlantRandom(_world, z);
|
||
if (planted.HasValue)
|
||
{
|
||
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();
|
||
}
|
||
}
|
||
}
|