Skip to content

Commit

Permalink
Space age spoilage (#352)
Browse files Browse the repository at this point in the history
This loads and displays all the spoiling information from #313, except
"Mark spoil result items in GUI", which I don't think is necessary; and
"Support spoilable science packs", which can be accomplished for now by
setting the table to have (excess) desired output.

Perishable items and entities get a clock overlay on their icon, and a
new "Perishable" section in their tooltip. If they are recipe outputs
they may also get a "This recipe output ..." string.

![image](https://github.com/user-attachments/assets/12d49fcc-aba5-47c3-a171-fbf9162aa4b9)

Recipes that have non-default spoil behavior describe that behavior in
their tooltip.

![image](https://github.com/user-attachments/assets/bf2ae9cd-cd40-4108-854d-9586678a1060)
  • Loading branch information
shpaass authored Nov 9, 2024
2 parents ae5dbb2 + d8b59bb commit 73732f2
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 158 deletions.
68 changes: 67 additions & 1 deletion Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ public class Recipe : RecipeOrTechnology {
public string[]? allowedModuleCategories { get; internal set; }
public Technology[] technologyUnlock { get; internal set; } = [];
public Dictionary<Technology, float> technologyProductivity { get; internal set; } = [];
public bool preserveProducts { get; internal set; }

public bool HasIngredientVariants() {
foreach (var ingredient in ingredients) {
if (ingredient.variants != null) {
Expand Down Expand Up @@ -248,6 +250,11 @@ public class Product : IFactorioObjectWrapper {
internal readonly float amountMax;
internal readonly float probability;
public readonly float amount; // This is average amount including probability and range
/// <summary>
/// Gets or sets the fixed freshness of this product: 0 if the recipe has result_is_always_fresh, or percent_spoiled from the
/// ItemProductPrototype, or <see langword="null"/> if neither of those values are set.
/// </summary>
public float? percentSpoiled { get; internal set; }
internal float productivityAmount { get; private set; }

public void SetCatalyst(float catalyst) {
Expand Down Expand Up @@ -283,7 +290,10 @@ public Product(Goods goods, float min, float max, float probability) {
amount = productivityAmount = probability * (min + max) / 2;
}

public bool IsSimple => amountMin == amountMax && probability == 1f;
/// <summary>
/// Gets <see langword="true"/> if this product is one item with 100% probability and default spoilage behavior.
/// </summary>
public bool IsSimple => amountMin == amountMax && amount == 1 && probability == 1 && percentSpoiled == null;

FactorioObject IFactorioObjectWrapper.target => goods;

Expand All @@ -301,6 +311,16 @@ string IFactorioObjectWrapper.text {
if (probability != 1f) {
text = DataUtils.FormatAmount(probability, UnitOfMeasure.Percent) + " " + text;
}
else if (amountMin == 1 && amountMax == 1) {
text = "1x " + text;
}

if (percentSpoiled == 0) {
text += ", always fresh";
}
else if (percentSpoiled != null) {
text += ", " + DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent) + " spoiled";
}

return text;
}
Expand Down Expand Up @@ -329,6 +349,12 @@ public virtual bool HasSpentFuel([MaybeNullWhen(false)] out Item spent) {
}

public class Item : Goods {
public Item() {
getSpoilResult = new(() => getSpoilRecipe()?.products[0].goods);
getBaseSpoilTime = new(() => getSpoilRecipe()?.time ?? 0);
Recipe? getSpoilRecipe() => Database.recipes.all.OfType<Mechanics>().SingleOrDefault(r => r.name == "spoil." + name);
}

/// <summary>
/// The prototypes in this array will be loaded in order, before any other prototypes.
/// This should correspond to the prototypes for subclasses of item, with more derived classes listed before their base classes.
Expand All @@ -347,6 +373,29 @@ public class Item : Goods {
public override string type => "Item";
internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Items;
public override UnitOfMeasure flowUnitOfMeasure => UnitOfMeasure.ItemPerSecond;
/// <summary>
/// Gets the result when this item spoils, or <see langword="null"/> if this item doesn't spoil.
/// </summary>
public FactorioObject? spoilResult => getSpoilResult.Value;
/// <summary>
/// Gets the time it takes for a base-quality item to spoil, in seconds, or 0 if this item doesn't spoil.
/// </summary>
public float baseSpoilTime => getBaseSpoilTime.Value;
/// <summary>
/// Gets the <see cref="Quality"/>-adjusted spoilage time for this item, in seconds, or 0 if this item doesn't spoil.
/// </summary>
public float GetSpoilTime(Quality quality) => quality.ApplyStandardBonus(baseSpoilTime);

/// <summary>
/// The lazy store for getting the spoilage result. By default it searches for and reads a $"Mechanics.spoil.{name}" recipe,
/// but it can be overridden for items that spoil into Entities.
/// </summary>
internal Lazy<FactorioObject?> getSpoilResult;
/// <summary>
/// The lazy store for getting the normal-quality spoilage time. By default it searches for and reads a $"Mechanics.spoil.{name}" recipe,
/// but it can be overridden for items that spoil into Entities.
/// </summary>
internal Lazy<float> getBaseSpoilTime;

public override bool HasSpentFuel([NotNullWhen(true)] out Item? spent) {
spent = fuelResult;
Expand Down Expand Up @@ -438,6 +487,23 @@ public float Power(Quality quality)
public int size { get; internal set; }
internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Entities;
public override string type => "Entity";
/// <summary>
/// Gets the result when this entity spoils (possibly <see langword="null"/>, if the entity burns out with no replacement),
/// or <see langword="null"/> if this entity doesn't spoil.
/// </summary>
public Entity? spoilResult => getSpoilResult?.Value;
/// <summary>
/// Gets the time it takes for a base-quality entity to spoil, in seconds, or 0 if this entity doesn't spoil.
/// </summary>
public float baseSpoilTime { get; internal set; }
/// <summary>
/// Gets the <see cref="Quality"/>-adjusted spoilage time for this entity, in seconds, or 0 if this entity doesn't spoil.
/// </summary>
public float GetSpoilTime(Quality quality) => quality.ApplyStandardBonus(baseSpoilTime);
/// <summary>
/// The lazy store for getting the spoilage result, if this entity spoils.
/// </summary>
internal Lazy<Entity?>? getSpoilResult;

public override void GetDependencies(IDependencyCollector collector, List<FactorioObject> temp) {
if (energy != null) {
Expand Down
14 changes: 8 additions & 6 deletions Yafc.Model/Model/ProductionTableContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,15 @@ public IEnumerable<RecipeRowProduct> Products {
handledFuel = true;
}

yield return (product.goods, amount, links.products[i++]);
yield return (product.goods, amount, links.products[i++], product.percentSpoiled);
}
else {
yield return (null, 0, null);
yield return (null, 0, null, null);
}
}

if (!handledFuel) {
yield return hierarchyEnabled ? (spentFuel, fuelUsagePerSecond, links.spentFuel) : (null, 0, null);
yield return hierarchyEnabled ? (spentFuel, fuelUsagePerSecond, links.spentFuel, null) : (null, 0, null, null);
}
}
}
Expand Down Expand Up @@ -745,7 +745,9 @@ public static implicit operator RecipeRowIngredient((Goods? Goods, float Amount,
=> new(value.Goods, value.Amount, value.Link, value.Variants);
}

public record RecipeRowProduct(Goods? Goods, float Amount, ProductionLink? Link) {
public static implicit operator (Goods? Goods, float Amount, ProductionLink? Link)(RecipeRowProduct value) => (value.Goods, value.Amount, value.Link);
public static implicit operator RecipeRowProduct((Goods? Goods, float Amount, ProductionLink? Link) value) => new(value.Goods, value.Amount, value.Link);
public record RecipeRowProduct(Goods? Goods, float Amount, ProductionLink? Link, float? PercentSpoiled) {
public static implicit operator (Goods? Goods, float Amount, ProductionLink? Link, float? PercentSpoiled)(RecipeRowProduct value)
=> (value.Goods, value.Amount, value.Link, value.PercentSpoiled);
public static implicit operator RecipeRowProduct((Goods? Goods, float Amount, ProductionLink? Link, float? PercentSpoiled) value)
=> new(value.Goods, value.Amount, value.Link, value.PercentSpoiled);
}
2 changes: 2 additions & 0 deletions Yafc.Model/Model/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ public class ProjectSettings(Project project) : ModelObject<Project>(project) {
public int reactorSizeX { get; set; } = 2;
public int reactorSizeY { get; set; } = 2;
public float PollutionCostModifier { get; set; } = 0;
public float spoilingRate { get; set; } = 1;

public event Action<bool>? changed;
protected internal override void ThisChanged(bool visualOnly) => changed?.Invoke(visualOnly);

Expand Down
18 changes: 12 additions & 6 deletions Yafc.Parser/Data/DataParserUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,33 @@ namespace Yafc.Parser;

internal static class DataParserUtils {
private static class ConvertersFromLua<T> {
public static Func<object, T, T>? convert;
public static Converter? convert;

[return: NotNullIfNotNull(nameof(@default))]
public delegate T Converter(object value, T @default);
}

static DataParserUtils() {
ConvertersFromLua<int>.convert = (o, def) => o is long l ? (int)l : o is double d ? (int)d : o is string s && int.TryParse(s, out int res) ? res : def;
ConvertersFromLua<int?>.convert = (o, def) => o is long l ? (int)l : o is double d ? (int)d : o is string s && int.TryParse(s, out int res) ? res : def;
ConvertersFromLua<float>.convert = (o, def) => o is long l ? l : o is double d ? (float)d : o is string s && float.TryParse(s, out float res) ? res : def;
ConvertersFromLua<bool>.convert = delegate (object src, bool def) {
ConvertersFromLua<float?>.convert = (o, def) => o is long l ? l : o is double d ? (float)d : o is string s && float.TryParse(s, out float res) ? res : def;
ConvertersFromLua<bool>.convert = (o, def) => ConvertersFromLua<bool?>.convert!(o, def).Value;
ConvertersFromLua<bool?>.convert = (o, def) => {
if (src is bool b) {
if (o is bool b) {
return b;
}
if (src == null) {
if (o == null) {
return def;
}
if (src.Equals("true")) {
if (o.Equals("true")) {
return true;
}
if (src.Equals("false")) {
if (o.Equals("false")) {
return false;
}
Expand Down
30 changes: 28 additions & 2 deletions Yafc.Parser/Data/FactorioDataDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ private static EffectReceiver ParseEffectReceiver(LuaTable? table) {
};
}

private void DeserializeItem(LuaTable table, ErrorCollector _) {
private void DeserializeItem(LuaTable table, ErrorCollector _1) {
string name = table.Get("name", "");
if (table.Get("type", "") == "module" && table.Get("effect", out LuaTable? moduleEffect)) {
Module module = GetObject<Item, Module>(name);
Expand Down Expand Up @@ -447,12 +447,38 @@ void readTrigger(LuaTable table) {
recipe.time = 0f; // TODO what to put here?
}

if (GetRef<Item>(table, "spoil_result", out Item? spoiled)) {
if (GetRef(table, "spoil_result", out Item? spoiled)) {
var recipe = CreateSpecialRecipe(item, SpecialNames.SpoilRecipe, "spoiling");
recipe.ingredients = [new Ingredient(item, 1)];
recipe.products = [new Product(spoiled, 1)];
recipe.time = table.Get("spoil_ticks", 0) / 60f;
}
// Read spoil-into-entity triggers
else if (table["spoil_to_trigger_result"] is LuaTable spoil && spoil["trigger"] is LuaTable triggers) {
triggers.ReadObjectOrArray(readTrigger);

void readTrigger(LuaTable trigger) {
if (trigger.Get<string>("type") == "direct" && trigger["action_delivery"] is LuaTable delivery) {
delivery.ReadObjectOrArray(readDelivery);
}
}
void readDelivery(LuaTable delivery) {
if (delivery.Get<string>("type") == "instant" && delivery["source_effects"] is LuaTable effects) {
effects.ReadObjectOrArray(readEffect);
}
}
void readEffect(LuaTable effect) {
if (effect.Get<string>("type") == "create-entity") {
float spoilTime = table.Get("spoil_ticks", 0) / 60f;
string entityName = "Entity." + effect.Get<string>("entity_name");
item.getBaseSpoilTime = new(() => spoilTime);
item.getSpoilResult = new(() => {
_ = Database.objectsByTypeName.TryGetValue(entityName, out FactorioObject? result);
return result;
});
}
}
}

if (table.Get("plant_result", out string? plantResult) && !string.IsNullOrEmpty(plantResult)) {
plantResults[item] = plantResult;
Expand Down
Loading

0 comments on commit 73732f2

Please sign in to comment.