forked from Mirrors/openclonk
729 lines
19 KiB
C
729 lines
19 KiB
C
/**
|
|
Producer
|
|
Library for production facilities. This library handles the automatic production of
|
|
items in structures. The library provides the interface for the player, checks for
|
|
components, need for liquids or fuel and power. Then handles the production process and may in the future
|
|
provide an interface with other systems (e.g. railway).
|
|
|
|
@author Maikel
|
|
*/
|
|
|
|
/*
|
|
Properties of producers:
|
|
* Storage of producers:
|
|
* Producers can store sufficient amounts of raw material they need to produce their products.
|
|
* Producers can store the products they can produce.
|
|
* Nothing more, nothing less.
|
|
* Production queue:
|
|
* Producers automatically produce the items in the production queue.
|
|
* Producible items can be added to the queue, with an amount specified.
|
|
* Also an infinite amount is possible, equals indefinite production.
|
|
Possible interaction with cable network:
|
|
* Producers request the cable network for raw materials.
|
|
|
|
*/
|
|
|
|
#include Library_PowerConsumer
|
|
|
|
// Production queue, a list of items to be produced.
|
|
local queue;
|
|
|
|
|
|
protected func Initialize()
|
|
{
|
|
queue = [];
|
|
AddEffect("ProcessQueue", this, 100, 5, this);
|
|
return _inherited(...);
|
|
}
|
|
|
|
/*-- Player interface --*/
|
|
|
|
// All producers are accessible.
|
|
public func IsContainer() { return true; }
|
|
|
|
public func IsInteractable() { return GetCon() >= 100; }
|
|
|
|
public func GetInteractionMetaInfo(object clonk)
|
|
{
|
|
return { Description = "$DescInteraction$", IconName = nil, IconID = nil };
|
|
}
|
|
|
|
// On interaction the production menu should be opened.
|
|
public func Interact(object clonk)
|
|
{
|
|
var open_menu = clonk->GetMenu();
|
|
// First try to close any other menu, which is open in the clonk.
|
|
if (open_menu)
|
|
{
|
|
var is_prod_menu = open_menu->~IsProductionMenu();
|
|
// Remove the open menu.
|
|
open_menu->RemoveObject();
|
|
clonk->SetMenu(nil);
|
|
// If open_menu is production menu, return and don't open a new one.
|
|
if (is_prod_menu)
|
|
return true;
|
|
}
|
|
// Open production menu for the caller.
|
|
clonk->CreateProductionMenu(this);
|
|
return true;
|
|
}
|
|
|
|
protected func OnProductSelection(id product, int par, bool alt)
|
|
{
|
|
if (!product)
|
|
return;
|
|
|
|
var amount = nil;
|
|
if (!alt)
|
|
amount = 1;
|
|
|
|
AddToQueue(product, amount);
|
|
return;
|
|
}
|
|
|
|
|
|
/*-- Production properties --*/
|
|
|
|
// This function may be overloaded by the actual producer.
|
|
// If set to true, the producer will show every product which is assigned to it instead of checking the knowledge base of its owner.
|
|
private func IgnoreKnowledge() { return false; }
|
|
|
|
/** Determines whether the product specified can be produced. Should be overloaded by the producer.
|
|
@param product_id item's id of which to determine if it is producible.
|
|
@return \c true if the item can be produced, \c false otherwise.
|
|
*/
|
|
private func IsProduct(id product_id)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/** Returns an array with the ids of products which can be produced at this producer.
|
|
@return array with products.
|
|
*/
|
|
public func GetProducts(object for_clonk)
|
|
{
|
|
var for_plr = GetOwner();
|
|
if (for_clonk)
|
|
for_plr = for_clonk-> GetOwner();
|
|
var products = [];
|
|
// Cycle through all definitions to find the ones this producer can produce.
|
|
var index = 0, product;
|
|
if (!IgnoreKnowledge() && for_plr != NO_OWNER)
|
|
{
|
|
while (product = GetPlrKnowledge(for_plr, nil, index, C4D_Object))
|
|
{
|
|
if (IsProduct(product))
|
|
products[GetLength(products)] = product;
|
|
index++;
|
|
}
|
|
index = 0;
|
|
while (product = GetPlrKnowledge(for_plr, nil, index, C4D_Vehicle))
|
|
{
|
|
if (IsProduct(product))
|
|
products[GetLength(products)] = product;
|
|
index++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (product = GetDefinition(index))
|
|
{
|
|
if (IsProduct(product))
|
|
products[GetLength(products)] = product;
|
|
index++;
|
|
}
|
|
}
|
|
return products;
|
|
}
|
|
|
|
/** Determines whether the raw material specified is needed for production. Should be overloaded by the producer.
|
|
@param rawmat_id id of raw material for which to determine if it is needed for production.
|
|
@return \c true if the raw material is needed, \c false otherwise.
|
|
*/
|
|
public func NeedRawMaterial(id rawmat_id)
|
|
{
|
|
return false; // Obsolete for now.
|
|
}
|
|
|
|
/**
|
|
Determines the production costs for an item.
|
|
@param item_id id of the item under consideration.
|
|
@return a list of objects and their respective amounts.
|
|
*/
|
|
public func ProductionCosts(id item_id)
|
|
{
|
|
/* NOTE: This may be overloaded by the producer */
|
|
var comp_list = [];
|
|
var comp_id, index = 0;
|
|
while (comp_id = GetComponent(nil, index, nil, item_id))
|
|
{
|
|
var amount = GetComponent(comp_id, index, nil, item_id);
|
|
comp_list[index] = [comp_id, amount];
|
|
index++;
|
|
}
|
|
return comp_list;
|
|
}
|
|
|
|
/*-- Production queue --*/
|
|
|
|
/** Adds an item to the production queue.
|
|
@param product_id id of the item.
|
|
@param amount the amount of items of \c item_id which should be produced.
|
|
@return current position of the item in the production queue.
|
|
*/
|
|
public func AddToQueue(id product_id, int amount)
|
|
{
|
|
// Check if this producer can produce the requested item.
|
|
if (!IsProduct(product_id))
|
|
return nil;
|
|
var pos = GetLength(queue);
|
|
|
|
// Check if the same product is in the position before, cause of possible redundancy.
|
|
if (amount != nil && pos > 0 && queue[pos-1].Product == product_id)
|
|
queue[pos-1].Amount += amount;
|
|
// Otherwise create a new entry in the queue.
|
|
else
|
|
queue[pos] = { Product = product_id, Amount = amount };
|
|
// Notify all production menus open for this producer.
|
|
for (var menu in FindObjects(Find_ID(Library_ProductionMenu), Find_Func("HasCommander", this)))
|
|
menu->UpdateMenuQueue(this);
|
|
return pos;
|
|
}
|
|
|
|
/** Inserts an item into the production queue at the specified position.
|
|
@param product_id id of the item.
|
|
@param amount the amount of items of \c item_id which should be inserted.
|
|
@param pos the position at which the object should be inserted, the rest of the queue is shifted downwards.
|
|
@return current position of the item in the production queue.
|
|
*/
|
|
public func InsertIntoQueue(id product_id, int amount, int pos)
|
|
{
|
|
// Check if this producer can produce the requested item.
|
|
if (!IsProduct(product_id))
|
|
return nil;
|
|
|
|
// Make sure the position is valid.
|
|
BoundBy(pos, 0, GetLength(queue) - 1);
|
|
|
|
// Check if there is the same product at that position, for a possible merge.
|
|
if (amount != nil && queue[pos].Product == product_id)
|
|
queue[pos].Amount += amount;
|
|
// Check if there is the same product at the position before, for a possible merge.
|
|
else if (amount != nil && pos > 0 && queue[pos-1].Product == product_id)
|
|
queue[pos-1].Amount += amount;
|
|
// Otherwise insert a new item into the queue.
|
|
else
|
|
{
|
|
// First shift all queue items from that position up by one.
|
|
var length = GetLength(queue);
|
|
for (var i = length; i > pos; i--)
|
|
queue[i] = queue[i-1];
|
|
// Then create new item.
|
|
queue[pos] = { Product = product_id, Amount = amount };
|
|
}
|
|
// Notify all production menus open for this producer.
|
|
for (var menu in FindObjects(Find_ID(Library_ProductionMenu), Find_Func("HasCommander", this)))
|
|
menu->UpdateMenuQueue(this);
|
|
return pos;
|
|
}
|
|
|
|
/** Removes a item or some of it from from the production queue.
|
|
@param pos position of the item in the queue.
|
|
@param amount the amount of this item which should be removed.
|
|
@return \c nil.
|
|
*/
|
|
public func RemoveFromQueue(int pos, int amount)
|
|
{
|
|
var length = GetLength(queue);
|
|
// Safety, pos out of reach.
|
|
if (pos > length - 1)
|
|
return;
|
|
// Reduce and check amount.
|
|
queue[pos].Amount -= amount;
|
|
// If amount <= 0, remove item from queue.
|
|
// From pos onwards queue items should be shift downwards.
|
|
if (queue[pos].Amount <= 0)
|
|
{
|
|
for (var i = pos; i < GetLength(queue); i++)
|
|
queue[i] = queue[i+1];
|
|
// Reduce queue size by one.
|
|
SetLength(queue, length - 1);
|
|
// Check if the removed product was in between equal products, cause of a new possible redundancy.
|
|
if (pos > 0 && pos <= length - 2)
|
|
{
|
|
if (queue[pos-1].Product == queue[pos].Product)
|
|
{
|
|
queue[pos-1].Amount += queue[pos].Amount;
|
|
for (var i = pos; i < GetLength(queue); i++)
|
|
queue[i] = queue[i+1];
|
|
// Reduce queue size by one.
|
|
SetLength(queue, length - 2);
|
|
}
|
|
}
|
|
}
|
|
// Notify all production menus open for this producer.
|
|
for (var menu in FindObjects(Find_ID(Library_ProductionMenu), Find_Func("HasCommander", this)))
|
|
menu->UpdateMenuQueue(this);
|
|
return;
|
|
}
|
|
|
|
/** Clears the complete production queue.
|
|
@param abort determines whether to abort the current production process.
|
|
@return \c nil.
|
|
*/
|
|
public func ClearQueue(bool abort)
|
|
{
|
|
if (abort)
|
|
{
|
|
queue = [];
|
|
return;
|
|
}
|
|
//
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
/** Returns the current queue.
|
|
@return an array containing the queue elements (.Product for id, .Amount for amount).
|
|
*/
|
|
public func GetQueue()
|
|
{
|
|
return queue;
|
|
}
|
|
|
|
protected func FxProcessQueueStart()
|
|
{
|
|
|
|
return 1;
|
|
}
|
|
|
|
protected func FxProcessQueueTimer(object target, proplist effect)
|
|
{
|
|
// If target is currently producing, don't do anything.
|
|
if (IsProducing())
|
|
return 1;
|
|
|
|
// Wait if there are no items in the queue.
|
|
if (!queue[0])
|
|
return 1;
|
|
|
|
// Produce first item in the queue.
|
|
var product_id = queue[0].Product;
|
|
var amount = queue[0].Amount;
|
|
// Check raw material need.
|
|
if (!CheckMaterial(product_id))
|
|
{
|
|
// No material available? request from cable network.
|
|
RequestMaterial(product_id);
|
|
return 1;
|
|
}
|
|
// Start the item production.
|
|
if (!Produce(product_id))
|
|
return 1;
|
|
|
|
// Update amount and or queue.
|
|
if (amount == nil)
|
|
return 1;
|
|
|
|
// Update queue, reduce amount.
|
|
RemoveFromQueue(0, 1);
|
|
|
|
// Done with production checks.
|
|
return 1;
|
|
}
|
|
|
|
/*-- Production --*/
|
|
|
|
// These functions may be overloaded by the actual producer.
|
|
private func ProductionTime(id product) { return product->~GetProductionTime(); }
|
|
private func FuelNeed(id product) { return product->~GetFuelNeed(); }
|
|
private func LiquidNeed(id product) { return product->~GetLiquidNeed(); }
|
|
private func MaterialNeed(id product) { return product->~GetMaterialNeed(); }
|
|
|
|
public func PowerNeed() { return 80; }
|
|
|
|
public func GetConsumerPriority() { return 50; }
|
|
|
|
private func Produce(id product)
|
|
{
|
|
// Already producing? Wait a little.
|
|
if (IsProducing())
|
|
return false;
|
|
|
|
// Check if components are available.
|
|
if (!CheckComponents(product))
|
|
return false;
|
|
// Check need for fuel.
|
|
if (!CheckFuel(product))
|
|
return false;
|
|
// Check need for liquids.
|
|
if (!CheckLiquids(product))
|
|
return false;
|
|
// Check need for materials.
|
|
if (!CheckMaterials(product))
|
|
return false;
|
|
// Check need for power.
|
|
if (!CheckForPower())
|
|
return false;
|
|
|
|
// Everything available? Start production.
|
|
// Remove needed components, fuel and liquid.
|
|
// Power will be substracted during the production process.
|
|
CheckComponents(product, true);
|
|
CheckFuel(product, true);
|
|
CheckLiquids(product, true);
|
|
CheckMaterials(product, true);
|
|
|
|
// Add production effect.
|
|
AddEffect("ProcessProduction", this, 100, 2, this, nil, product);
|
|
|
|
return true;
|
|
}
|
|
|
|
private func CheckComponents(id product, bool remove)
|
|
{
|
|
for (var item in ProductionCosts(product))
|
|
{
|
|
var mat_id = item[0];
|
|
var mat_cost = item[1];
|
|
if (!CheckComponent(mat_id, mat_cost))
|
|
return false; // Components missing.
|
|
else if (remove)
|
|
{
|
|
for (var i = 0; i < mat_cost; i++)
|
|
FindObject(Find_Container(this), Find_ID(mat_id))->RemoveObject();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public func CheckComponent(id component, int amount)
|
|
{
|
|
// check if at least the given amount of the given component is available to be used for production
|
|
return (ObjectCount(Find_Container(this), Find_ID(component)) >= amount);
|
|
}
|
|
|
|
public func CheckFuel(id product, bool remove)
|
|
{
|
|
if (FuelNeed(product) > 0)
|
|
{
|
|
var fuel_amount = 0;
|
|
// Find fuel in this producer.
|
|
for (var fuel in FindObjects(Find_Container(this), Find_Func("IsFuel")))
|
|
fuel_amount += fuel->~GetFuelAmount(false);
|
|
if (fuel_amount < FuelNeed(product))
|
|
return false;
|
|
else if (remove)
|
|
{
|
|
// Remove the fuel needed.
|
|
fuel_amount = 0;
|
|
for (var fuel in FindObjects(Find_Container(this), Find_Func("IsFuel")))
|
|
{
|
|
fuel_amount += fuel->~GetFuelAmount(false);
|
|
fuel->RemoveObject();
|
|
if (fuel_amount >= FuelNeed(product))
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public func CheckLiquids(id product, bool remove)
|
|
{
|
|
var liq_need = LiquidNeed(product);
|
|
if (liq_need)
|
|
{
|
|
var liquid_amount = 0;
|
|
var liquid = liq_need[0];
|
|
var need = liq_need[1];
|
|
// Find liquid containers in this producer.
|
|
for (var liq_container in FindObjects(Find_Container(this), Find_Func("IsLiquidContainer")))
|
|
if (liq_container->~GetBarrelMaterial() == liquid)
|
|
liquid_amount += liq_container->~GetFillLevel();
|
|
// Find objects that "are" liquid (e.g. ice)
|
|
for (var liq_object in FindObjects(Find_Container(this), Find_Func("IsLiquid")))
|
|
if (liq_object->~IsLiquid() == liquid)
|
|
liquid_amount += liq_object->~GetLiquidAmount();
|
|
if (liquid_amount < need)
|
|
return false;
|
|
else if (remove)
|
|
{
|
|
// Remove the liquid needed.
|
|
var extracted = 0;
|
|
for (var liq_container in FindObjects(Find_Container(this), Find_Func("IsLiquidContainer")))
|
|
{
|
|
var val = liq_container->~GetLiquid(liquid, need - extracted);
|
|
extracted += val[1];
|
|
if (extracted >= need)
|
|
return true;
|
|
}
|
|
for (var liq_object in FindObjects(Find_Container(this), Find_Func("IsLiquid")))
|
|
{
|
|
if (liq_object->~IsLiquid() != liquid) continue;
|
|
extracted += liq_object->~GetLiquidAmount();
|
|
liq_object->RemoveObject();
|
|
if (extracted >= need)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public func CheckMaterials(id product, bool remove)
|
|
{
|
|
var mat_need = MaterialNeed(product);
|
|
if (mat_need)
|
|
{
|
|
var material_amount = 0;
|
|
var material = mat_need[0];
|
|
var need = mat_need[1];
|
|
// Find liquid containers in this producer.
|
|
for (var mat_container in FindObjects(Find_Container(this), Find_Func("IsMaterialContainer")))
|
|
if (mat_container->~GetContainedMaterial() == material)
|
|
material_amount += mat_container->~GetFillLevel();
|
|
if (material_amount < need)
|
|
return false;
|
|
else if (remove)
|
|
{
|
|
// Remove the material needed.
|
|
var extracted = 0;
|
|
for (var mat_container in FindObjects(Find_Container(this), Find_Func("IsMaterialContainer")))
|
|
{
|
|
var val = mat_container->~RemoveContainedMaterial(material, need - extracted);
|
|
extracted += val;
|
|
if (extracted >= need)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private func CheckForPower()
|
|
{
|
|
return true; // always assume that power is available
|
|
}
|
|
|
|
private func IsProducing()
|
|
{
|
|
if (GetEffect("ProcessProduction", this))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
protected func FxProcessProductionStart(object target, proplist effect, int temporary, id product)
|
|
{
|
|
if (temporary)
|
|
return 1;
|
|
|
|
// Set product.
|
|
effect.Product = product;
|
|
|
|
// Set production duration to zero.
|
|
effect.Duration = 0;
|
|
|
|
// Production is active.
|
|
effect.Active = true;
|
|
|
|
// Callback to the producer.
|
|
this->~OnProductionStart(effect.Product);
|
|
|
|
// Consume power by registering as a consumer for the needed amount.
|
|
// But first hold the production until the power system gives it ok.
|
|
// Always register the power request even if power need is zero. The
|
|
// power network handles this correctly and a producer may decide to
|
|
// change its power need during production.
|
|
RegisterPowerRequest(this->PowerNeed());
|
|
|
|
return 1;
|
|
}
|
|
|
|
public func OnNotEnoughPower()
|
|
{
|
|
var effect = GetEffect("ProcessProduction", this);
|
|
if (effect)
|
|
{
|
|
effect.Active = false;
|
|
this->~OnProductionHold(effect.Product, effect.Duration);
|
|
}
|
|
else
|
|
FatalError("Producer effect removed when power still active!");
|
|
return _inherited(...);
|
|
}
|
|
|
|
public func OnEnoughPower()
|
|
{
|
|
var effect = GetEffect("ProcessProduction", this);
|
|
if (effect)
|
|
{
|
|
effect.Active = true;
|
|
this->~OnProductionContinued(effect.Product, effect.Duration);
|
|
}
|
|
else
|
|
FatalError("Producer effect removed when power still active!");
|
|
return _inherited(...);
|
|
}
|
|
|
|
protected func FxProcessProductionTimer(object target, proplist effect, int time)
|
|
{
|
|
if (!effect.Active)
|
|
return 1;
|
|
|
|
// Add effect interval to production duration.
|
|
effect.Duration += effect.Interval;
|
|
|
|
//Log("Production in progress on %i, %d frames, %d time", effect.Product, effect.Duration, time);
|
|
|
|
// Check if production time has been reached.
|
|
if (effect.Duration >= ProductionTime(effect.Product))
|
|
return -1;
|
|
|
|
return 1;
|
|
}
|
|
|
|
protected func FxProcessProductionStop(object target, proplist effect, int reason, bool temp)
|
|
{
|
|
if(temp) return;
|
|
|
|
// no need to consume power anymore
|
|
UnregisterPowerRequest();
|
|
|
|
if (reason != 0)
|
|
return 1;
|
|
|
|
// Callback to the producer.
|
|
//Log("Production finished on %i after %d frames", effect.Product, effect.Duration);
|
|
this->~OnProductionFinish(effect.Product);
|
|
// Create product.
|
|
var product = CreateObjectAbove(effect.Product);
|
|
OnProductEjection(product);
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Standard behaviour for product ejection.
|
|
public func OnProductEjection(object product)
|
|
{
|
|
// Safety for the product removing itself on construction.
|
|
if (!product)
|
|
return;
|
|
// Vehicles in front of buildings, and objects with special needs as well.
|
|
if (product->GetCategory() & C4D_Vehicle || product->~OnCompletionEjectProduct())
|
|
{
|
|
var x = GetX();
|
|
var y = GetY() + GetDefHeight()/2 - product->GetDefHeight()/2;
|
|
product->SetPosition(x, y);
|
|
// Sometimes, there is material in front of the building. Move vehicles upwards in that case
|
|
var max_unstick_range = Max(GetDefHeight()/5,5); // 8 pixels for tools workshop
|
|
var y_off = 0;
|
|
while (product->Stuck() && y_off < max_unstick_range)
|
|
product->SetPosition(x, y-++y_off);
|
|
}
|
|
// Items should stay inside.
|
|
else
|
|
product->Enter(this);
|
|
return;
|
|
}
|
|
|
|
/*-- --*/
|
|
|
|
/**
|
|
Determines whether there is sufficient material to produce an item.
|
|
*/
|
|
private func CheckMaterial(id item_id)
|
|
{
|
|
for (var item in ProductionCosts(item_id))
|
|
{
|
|
var mat_id = item[0];
|
|
var mat_cost = item[1];
|
|
var mat_av = ObjectCount(Find_Container(this), Find_ID(mat_id));
|
|
if (mat_av < mat_cost)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
Requests the necessary material from the cable network if available.
|
|
*/
|
|
private func RequestMaterial(id item_id)
|
|
{
|
|
for (var item in ProductionCosts(item_id))
|
|
{
|
|
var mat_id = item[0];
|
|
var mat_cost = item[1];
|
|
var mat_av = ObjectCount(Find_Container(this), Find_ID(mat_id));
|
|
if (mat_av < mat_cost)
|
|
RequestObject(mat_id, mat_cost - mat_av);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Must exist if Library_CableStation is not included by either this
|
|
// library or the structure including this library.
|
|
public func RequestObject(id obj_id, int amount)
|
|
{
|
|
return _inherited(obj_id, amount, ...);
|
|
}
|
|
|
|
/*-- Storage --*/
|
|
|
|
protected func RejectCollect(id item, object obj)
|
|
{
|
|
// Just return RejectEntrance for this object.
|
|
return RejectEntrance(obj);
|
|
}
|
|
|
|
protected func RejectEntrance(object obj)
|
|
{
|
|
var obj_id = obj->GetID();
|
|
// Products itself may be collected.
|
|
if (IsProduct(obj_id))
|
|
return false;
|
|
|
|
// Components of products may be collected.
|
|
for (var product in GetProducts())
|
|
{
|
|
var i = 0, comp_id;
|
|
while (comp_id = GetComponent(nil, i, nil, product))
|
|
{
|
|
if (comp_id == obj_id)
|
|
return false;
|
|
i++;
|
|
}
|
|
}
|
|
// Fuel for products may be collected.
|
|
if (obj->~IsFuel())
|
|
{
|
|
for (var product in GetProducts())
|
|
if (FuelNeed(product) > 0)
|
|
return false;
|
|
}
|
|
// Liquid objects may be collected if a product needs them.
|
|
if (obj->~IsLiquid())
|
|
{
|
|
for (var product in GetProducts())
|
|
if (LiquidNeed(product))
|
|
if (LiquidNeed(product)[0] == obj->~IsLiquid())
|
|
return false;
|
|
}
|
|
// Liquid containers may be collected if a product needs them.
|
|
if (obj->~IsLiquidContainer())
|
|
{
|
|
for (var product in GetProducts())
|
|
if (LiquidNeed(product))
|
|
return false;
|
|
}
|
|
// Material containers may be collected if a product needs them.
|
|
if (obj->~IsMaterialContainer())
|
|
{
|
|
for (var product in GetProducts())
|
|
if (MaterialNeed(product))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|