Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lightning collectors #397

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Docs/Fulgora lightning model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
This is the math used to calculate `requiredChargeMw` (called "_cap_" here) in `BuildAccumulatorView`.
The first two equations are the starting point, and the remainder are solving for _cap_.

_chargeTime_ is per lightning strike.
_eff_ is the efficiency of the lightning attractor.

$$chargeTime=\frac{1000MJ\times eff}{drain+cap+load}$$
$$cap\times chargeTime\times numStrikes-load\times(stormTime-chargeTime\times numStrikes)=reqMj$$
$$\frac{cap\times 1000MJ\times eff\times numStrikes}{drain+cap+load}-load\times\left(stormTime-\frac{1000MJ\times eff\times numStrikes}{drain+cap+load}\right)=reqMj$$
$$\frac{cap\times 1000MJ\times eff\times numStrikes}{drain+cap+load}-load\times stormTime+\left(\frac{load\times 1000MJ\times eff\times numStrikes}{drain+cap+load}\right)=reqMj$$
$$\frac{cap\times 1000MJ\times eff\times numStrikes-load\times stormTime\times(drain+cap+load)+load\times 1000MJ\times eff\times numStrikes}{drain+cap+load}=reqMj$$
$$cap\times 1000MJ\times eff\times numStrikes-load\times stormTime\times(drain+cap+load)+load\times 1000MJ\times eff\times numStrikes\\
=reqMj\times(drain+cap+load)$$
$$cap\times 1000MJ\times eff\times numStrikes-load\times stormTime\times drain-load\times stormTime\times cap\\
-\ load\times stormTime\times load+load\times 1000MJ\times eff\times numStrikes\\
=reqMj\times drain+reqMj\times cap+reqMj\times load$$
$$cap\times 1000MJ\times eff\times numStrikes-load\times stormTime\times cap-reqMj\times cap\\
\begin{aligned}
=\ &reqMj\times drain+reqMj\times load+load\times stormTime\times drain\\
&+load\times stormTime\times load-load\times 1000MJ\times eff\times numStrikes
\end{aligned}$$
$$\begin{aligned}
cap\times(&1000MJ\times eff\times numStrikes-load\times stormTime-reqMj)\\
&=reqMj\times drain+load\times(reqMj+stormTime\times drain+stormTime\times load-1000MJ\times eff\times numStrikes)
\end{aligned}$$
$$cap=\frac{reqMj\times drain+load\times(reqMj+stormTime\times(drain+load)-1000MJ\times eff\times numStrikes)}{1000MJ\times eff\times numStrikes-load\times stormTime-reqMj}$$
52 changes: 48 additions & 4 deletions Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ public class Entity : FactorioObject {
public float mapGenDensity { get; internal set; }
public float basePower { get; internal set; }
public float Power(Quality quality)
=> factorioType is "boiler" or "reactor" or "generator" or "burner-generator" ? quality.ApplyStandardBonus(basePower)
=> factorioType is "boiler" or "reactor" or "generator" or "burner-generator" or "accumulator" ? quality.ApplyStandardBonus(basePower)
: factorioType is "beacon" ? basePower * quality.BeaconConsumptionFactor
: basePower;
public EntityEnergy energy { get; internal set; } = null!; // TODO: Prove that this is always properly initialized. (Do we need an EntityWithEnergy type?)
Expand Down Expand Up @@ -671,15 +671,58 @@ public class EntityCrafter : EntityWithModules {
public Goods[]? inputs { get; internal set; }
public RecipeOrTechnology[] recipes { get; internal set; } = null!; // null-forgiving: Set in the first step of CalculateMaps
private float _craftingSpeed = 1;
public float baseCraftingSpeed {
public virtual float baseCraftingSpeed {
// The speed of a lab is baseSpeed * (1 + researchSpeedBonus) * Math.Min(0.2, 1 + moduleAndBeaconSpeedBonus)
get => _craftingSpeed * (1 + (factorioType == "lab" ? Project.current.settings.researchSpeedBonus : 0));
internal set => _craftingSpeed = value;
}
public float CraftingSpeed(Quality quality) => factorioType is "agricultural-tower" or "electric-energy-interface" ? baseCraftingSpeed : quality.ApplyStandardBonus(baseCraftingSpeed);
public virtual float CraftingSpeed(Quality quality) => factorioType is "agricultural-tower" or "electric-energy-interface" ? baseCraftingSpeed : quality.ApplyStandardBonus(baseCraftingSpeed);
public EffectReceiver effectReceiver { get; internal set; } = null!;
}

public class EntityAttractor : EntityCrafter {
// TODO(multi-planet): Read PlanetPrototype::lightning_properties.search_radius
private const int LightningSearchRange = 10;
// TODO(multi-planet): Read PlanetPrototype::lightning_properties.lightning_types and
// LightningPrototype::lightnings_per_chunk_per_tick * 60 * LightningPrototype::energy / 1024 * 0.3
private const float MwPerTile = 0.0293f;
public override float baseCraftingSpeed {
get => CraftingSpeed(Quality.Normal);
internal set => throw new NotSupportedException("To set lightning attractor crafting speed, set the range and efficiency fields.");
}
public float drain { get; internal set; }

internal float range;
internal float efficiency;
/// <summary>
/// Gets the size of the (square) grid for this lightning attractor. The grid is sized to be as large as possible while protecting the
/// entire areas within the grid: the collection areas of diagonally-adjacent attractors should overlap as little as possible.
/// </summary>
public int ConstructionGrid(Quality quality) => (int)MathF.Floor((Range(quality) + LightningSearchRange) * MathF.Sqrt(2));
public float Range(Quality quality) => quality.ApplyStandardBonus(range);
public float Efficiency(Quality quality) => quality.ApplyStandardBonus(efficiency);

public override float CraftingSpeed(Quality quality) {
// Maximum distance between attractors for full protection, in a square grid:
float distance = ConstructionGrid(quality);
float efficiency = Efficiency(quality);
// Production is coverage area times efficiency times lightning power density
// Peak coverage area is (π*range²), but (distance²) allows full protection with a simple square grid.
float area = distance * distance;
// Assume 90% of the captured energy is lost to the attractor's internal drain.
return area * efficiency * MwPerTile / 10;
}

public float StormPotentialPerTick(Quality quality) {
// Maximum distance between attractors for full protection, in a square grid:
float distance = ConstructionGrid(quality);
// Storm potential is coverage area times lightning power density / .3
// Peak coverage area is (π*range²), but (distance²) allows full protection with a simple square grid.
float area = distance * distance;
return area * MwPerTile / 60 / 0.3f;
}
}

internal class EntityProjectile : Entity {
internal HashSet<string> placeEntities { get; } = [];
}
Expand Down Expand Up @@ -839,7 +882,8 @@ public class EntityInserter : Entity {
}

public class EntityAccumulator : Entity {
public float baseAccumulatorCapacity { get; internal set; }
internal float baseAccumulatorCapacity { get; set; }
internal float baseAccumulatorCurrent;
public float AccumulatorCapacity(Quality quality) => quality.ApplyAccumulatorCapacityBonus(baseAccumulatorCapacity);
}

Expand Down
28 changes: 27 additions & 1 deletion Yafc.Model/Model/QualityExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
namespace Yafc.Model;
using System.Diagnostics.CodeAnalysis;

namespace Yafc.Model;

public static class QualityExtensions {
public static float GetCraftingSpeed(this IObjectWithQuality<EntityCrafter> crafter) => crafter.target.CraftingSpeed(crafter.quality);

public static float GetPower(this IObjectWithQuality<Entity> entity) => entity.target.Power(entity.quality);

public static float GetBeaconEfficiency(this IObjectWithQuality<EntityBeacon> beacon) => beacon.target.BeaconEfficiency(beacon.quality);

public static float GetAttractorEfficiency(this IObjectWithQuality<EntityAttractor> attractor)
=> attractor.target.Efficiency(attractor.quality);

public static float StormPotentialPerTick(this IObjectWithQuality<EntityAttractor> attractor)
=> attractor.target.StormPotentialPerTick(attractor.quality);

/// <summary>
/// If possible, converts an <see cref="IObjectWithQuality{T}"/> into one with a different generic parameter.
/// </summary>
/// <typeparam name="T">The desired type parameter for the output <see cref="IObjectWithQuality{T}"/>.</typeparam>
/// <param name="obj">The input <see cref="IObjectWithQuality{T}"/> to be converted.</param>
/// <param name="result">If <c><paramref name="obj"/>?.target is <typeparamref name="T"/></c>, an <see cref="IObjectWithQuality{T}"/> with
/// the same target and quality as <paramref name="obj"/>. Otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the conversion was successful, or <see langword="false"/> if it was not.</returns>
public static bool Is<T>(this IObjectWithQuality<FactorioObject>? obj, [NotNullWhen(true)] out IObjectWithQuality<T>? result) where T : FactorioObject {
if (obj is null or IObjectWithQuality<T>) {
result = obj as IObjectWithQuality<T>;
return result is not null;
}
// Use the conversion because it permits a null target. The constructor does not.
result = (ObjectWithQuality<T>?)(obj.target as T, obj.quality);
return result is not null;
}
}
4 changes: 4 additions & 0 deletions Yafc.Model/Model/RecipeParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum WarningFlags {
ReactorsNeighborsFromPrefs = 1 << 1,
FuelUsageInputLimited = 1 << 2,
AsteroidCollectionNotModelled = 1 << 3,
AssumesFulgoraAndModel = 1 << 4,

// Static errors
EntityNotSpecified = 1 << 8,
Expand Down Expand Up @@ -150,6 +151,9 @@ public static RecipeParameters CalculateParameters(RecipeRow row) {
if (entity.target.factorioType == "solar-panel") {
warningFlags |= WarningFlags.AssumesNauvisSolarRatio;
}
else if (entity.target.factorioType == "lightning-attractor") {
warningFlags |= WarningFlags.AssumesFulgoraAndModel;
}
else if (entity.target.factorioType == "asteroid-collector") {
warningFlags |= WarningFlags.AsteroidCollectionNotModelled;
}
Expand Down
46 changes: 36 additions & 10 deletions Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Yafc.Parser;

internal partial class FactorioDataDeserializer {
private const float EstimationDistanceFromCenter = 3000f;
private bool GetFluidBoxFilter(LuaTable table, string fluidBoxName, int temperature, [NotNullWhen(true)] out Fluid? fluid, out TemperatureRange range) {
private bool GetFluidBoxFilter(LuaTable? table, string fluidBoxName, int temperature, [NotNullWhen(true)] out Fluid? fluid, out TemperatureRange range) {
fluid = null;
range = default;

Expand Down Expand Up @@ -40,7 +40,7 @@ private static int CountFluidBoxes(LuaTable list, bool input) {
return count;
}

private void ReadFluidEnergySource(LuaTable energySource, Entity entity) {
private void ReadFluidEnergySource(LuaTable? energySource, Entity entity) {
var energy = entity.energy;
_ = energySource.Get("burns_fluid", out bool burns, false);
energy.type = burns ? EntityEnergyType.FluidFuel : EntityEnergyType.FluidHeat;
Expand Down Expand Up @@ -69,7 +69,7 @@ private void ReadFluidEnergySource(LuaTable energySource, Entity entity) {
}
}

private void ReadEnergySource(LuaTable energySource, Entity entity, float defaultDrain = 0f) {
private void ReadEnergySource(LuaTable? energySource, Entity entity, float defaultDrain = 0f) {
_ = energySource.Get("type", out string type, "burner");

if (type == "void") {
Expand Down Expand Up @@ -162,6 +162,19 @@ private Recipe CreateLaunchRecipe(EntityCrafter entity, Recipe recipe, int parts
return launchRecipe;
}

// TODO: Work with AAI-I to support offshore pumps that consume energy.
private static readonly HashSet<string> noDefaultEnergyParsing = [
// Has custom parsing:
"generator",
"burner-generator",
// Doesn't consume energy:
"offshore-pump",
"solar-panel",
"accumulator",
"electric-energy-interface",
"lightning-attractor",
];

private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) {
string factorioType = table.Get("type", "");
string name = table.Get("name", "");
Expand All @@ -186,8 +199,13 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) {
case "accumulator":
var accumulator = GetObject<Entity, EntityAccumulator>(table);

if (table.Get("energy_source", out LuaTable? accumulatorEnergy) && accumulatorEnergy.Get("buffer_capacity", out string? capacity)) {
accumulator.baseAccumulatorCapacity = ParseEnergy(capacity);
if (table.Get("energy_source", out LuaTable? accumulatorEnergy)) {
if (accumulatorEnergy.Get("buffer_capacity", out string? capacity)) {
accumulator.baseAccumulatorCapacity = ParseEnergy(capacity);
}
if (accumulatorEnergy.Get("input_flow_limit", out string? inputPower)) {
accumulator.basePower = ParseEnergy(inputPower);
}
}
break;
case "agricultural-tower":
Expand Down Expand Up @@ -406,6 +424,18 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) {
break;
case "logistic-container":
goto case "container";
case "lightning-attractor":
if (table.Get("range_elongation", out int range) && table.Get("efficiency", out float efficiency) && efficiency > 0) {
EntityAttractor attractor = GetObject<Entity, EntityAttractor>(table);
attractor.energy = voidEntityEnergy;
attractor.range = range;
attractor.efficiency = efficiency;
if (table.Get("energy_source", out LuaTable? energy) && energy.Get("drain", out string? drain)) {
attractor.drain = ParseEnergy(drain) * 60; // Drain is listed as MJ/tick, not MW
}
recipeCrafters.Add(attractor, SpecialNames.GeneratorRecipe);
}
break;
case "mining-drill":
var drill = GetObject<Entity, EntityCrafter>(table);
_ = table.Get("energy_usage", out usesPower);
Expand Down Expand Up @@ -555,11 +585,7 @@ void parseEffect(LuaTable effect) {

_ = table.Get("energy_source", out LuaTable? energySource);

// These types have already called ReadEnergySource/ReadFluidEnergySource (generator, burner generator) or don't consume energy from YAFC's point of view (pump to EII).
// TODO: Work with AAI-I to support offshore pumps that consume energy.
if (factorioType is not "generator" and not "burner-generator" and not "offshore-pump" and not "solar-panel" and not "accumulator" and not "electric-energy-interface"
&& energySource != null) {

if (energySource != null && !noDefaultEnergyParsing.Contains(factorioType)) {
ReadEnergySource(energySource, entity, defaultDrain);
}

Expand Down
13 changes: 11 additions & 2 deletions Yafc/Widgets/ObjectTooltip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,18 +331,26 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) {
break;
case EntityAccumulator accumulator:
miscText = "Accumulator charge: " + DataUtils.FormatAmount(accumulator.AccumulatorCapacity(quality), UnitOfMeasure.Megajoule);
break;
case EntityAttractor attractor:
if (attractor.baseCraftingSpeed > 0f) {
miscText = "Power production (average usable): " + DataUtils.FormatAmount(attractor.CraftingSpeed(quality), UnitOfMeasure.Megawatt);
miscText += $"\n Build in a {attractor.ConstructionGrid(quality)}-tile square grid";
miscText += "\nProtection range: " + DataUtils.FormatAmount(attractor.Range(quality), UnitOfMeasure.None);
miscText += "\nCollection efficiency: " + DataUtils.FormatAmount(attractor.Efficiency(quality), UnitOfMeasure.Percent);
}

break;
case EntityCrafter solarPanel:
if (solarPanel.baseCraftingSpeed > 0f && entity.factorioType == "solar-panel") {
miscText = "Power production (average): " + DataUtils.FormatAmount(solarPanel.CraftingSpeed(quality), UnitOfMeasure.Megawatt);
}

break;
}

if (miscText != null) {
using (gui.EnterGroup(contentPadding)) {
gui.BuildText(miscText);
gui.BuildText(miscText, TextBlockDisplayStyle.WrappedText);
}
}
}
Expand Down Expand Up @@ -671,6 +679,7 @@ private static void BuildQuality(Quality quality, ImGui gui) {
("Module effects:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent) + '*'),
("Beacon transmission efficiency:", '+' + DataUtils.FormatAmount(quality.BeaconTransmissionBonus, UnitOfMeasure.None)),
("Time before spoiling:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)),
("Lightning attractor range & efficiency:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)),
];

float rightWidth = text.Max(t => gui.GetTextDimensions(out _, t.right).X);
Expand Down
Loading