Skip to content

Commit 3ceb47e

Browse files
FIX: calculate max power manually
max power is incorrect in core satisfactory for variable rate buildings. we should calculate max power for each building that is affected: converter, particle accelerators, quantum encoders, portals. calculate grid breakdown (per building/type) here. need to also calculate power grid total. should also use this value in the building stats if applicable. see also https://questions.satisfactorygame.com/post/67050eedddb9d97e071f63a2 for context Add somersloop to buildings schema for sloop power calculations
1 parent 9ed8a49 commit 3ceb47e

9 files changed

+300
-7
lines changed

Companion/exporter/drone_station_collector.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type DroneStationDetails struct {
3030
AvgIncRate float64 `json:"AvgIncRate"`
3131
AvgIncStack float64 `json:"AvgIncStack"`
3232
AvgOutRate float64 `json:"AvgOutRate"`
33-
AvgOutStack float64 `json"AvgOutStack"`
33+
AvgOutStack float64 `json:"AvgOutStack"`
3434
AvgRndTrip string `json:"AvgRndTrip"`
3535
AvgTotalIncRate float64 `json:"AvgTotalIncRate"`
3636
AvgTotalIncStack float64 `json:"AvgTotalIncStack"`
@@ -106,6 +106,10 @@ func (c *DroneStationCollector) Collect(frmAddress string, sessionName string) {
106106
cid := strconv.FormatFloat(circuitId, 'f', -1, 64)
107107
DronePortPower.WithLabelValues(cid, frmAddress, sessionName).Set(powerConsumed)
108108
}
109+
for circuitId, powerConsumed := range maxPowerInfo {
110+
cid := strconv.FormatFloat(circuitId, 'f', -1, 64)
111+
DronePortPowerMax.WithLabelValues(cid, frmAddress, sessionName).Set(powerConsumed)
112+
}
109113

110114
c.metricsDropper.DropStaleMetricLabels()
111115
}

Companion/exporter/factory_build_detail.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type BuildingDetail struct {
1212
IsPaused bool `json:"IsPaused"`
1313
CircuitGroupId int `json:"CircuitGroupID"`
1414
PowerInfo PowerInfo `json:"PowerInfo"`
15+
Somersloops float64 `json:"Somersloops"`
1516
}
1617

1718
type Production struct {

Companion/exporter/factory_building_collector.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,20 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string
6666
powerInfo[building.PowerInfo.CircuitGroupId] = building.PowerInfo.PowerConsumed
6767
}
6868
val, ok = maxPowerInfo[building.PowerInfo.CircuitGroupId]
69+
70+
// TODO: max factory power is bugged in the base game
71+
// for converters, quantum encoders, and particle accelerators.
72+
// Replace with reported values when they are correct.
73+
sloops := building.Somersloops
6974
maxBuildingPower := building.PowerInfo.MaxPowerConsumed
75+
switch building.Building {
76+
case "Converter":
77+
maxBuildingPower = powerMultiplier(building.ManuSpeed, sloops, 2.0) * MaxConverterPower
78+
case "Quantum Encoder":
79+
maxBuildingPower = powerMultiplier(building.ManuSpeed, sloops, 4.0) * MaxQuantumEncoderPower
80+
case "Particle Accelerator":
81+
maxBuildingPower = powerMultiplier(building.ManuSpeed, sloops, 4.0) * MaxParticleAcceleratorPower(building.Recipe)
82+
}
7083

7184
if ok {
7285
maxPowerInfo[building.PowerInfo.CircuitGroupId] = val + maxBuildingPower

Companion/exporter/factory_building_collector_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55

66
. "github.com/onsi/ginkgo/v2"
77
. "github.com/onsi/gomega"
8+
9+
"math"
810
)
911

1012
var _ = Describe("FactoryBuildingCollector", func() {
@@ -139,5 +141,152 @@ var _ = Describe("FactoryBuildingCollector", func() {
139141
Expect(ironNothing).To(Equal(float64(0.25)))
140142
})
141143
})
144+
145+
Describe("with power particle accelerator making diamonds", func() {
146+
BeforeEach(func() {
147+
FRMServer.Reset()
148+
url = FRMServer.server.URL
149+
collector = exporter.NewFactoryBuildingCollector("/getFactory")
150+
151+
FRMServer.ReturnsFactoryBuildings([]exporter.BuildingDetail{
152+
{
153+
Building: "Particle Accelerator",
154+
Recipe: "Diamonds",
155+
ManuSpeed: 100.0,
156+
CircuitGroupId: 0,
157+
PowerInfo: exporter.PowerInfo{
158+
CircuitGroupId: 1,
159+
PowerConsumed: 23,
160+
// value will be ignored for this - the recipe here is set to 750 in power_info.go
161+
MaxPowerConsumed: 4,
162+
},
163+
},
164+
})
165+
})
166+
It("recalculates max power use", func() {
167+
collector.Collect(url, sessionName)
168+
169+
val, err := gaugeValue(exporter.FactoryPowerMax, "1", url, sessionName)
170+
Expect(err).ToNot(HaveOccurred())
171+
Expect(val).To(Equal(750.0))
172+
})
173+
})
174+
Describe("with an overclocked accelerator", func() {
175+
BeforeEach(func() {
176+
FRMServer.Reset()
177+
url = FRMServer.server.URL
178+
collector = exporter.NewFactoryBuildingCollector("/getFactory")
179+
180+
FRMServer.ReturnsFactoryBuildings([]exporter.BuildingDetail{
181+
{
182+
Building: "Particle Accelerator",
183+
Recipe: "Nuclear Pasta",
184+
ManuSpeed: 250.0,
185+
CircuitGroupId: 0,
186+
PowerInfo: exporter.PowerInfo{
187+
CircuitGroupId: 1,
188+
PowerConsumed: 23,
189+
// value will be ignored for this - the recipe here is set to 750 in power_info.go
190+
MaxPowerConsumed: 4,
191+
},
192+
},
193+
})
194+
})
195+
It("recalculates max power use", func() {
196+
collector.Collect(url, sessionName)
197+
198+
val, err := gaugeValue(exporter.FactoryPowerMax, "1", url, sessionName)
199+
Expect(err).ToNot(HaveOccurred())
200+
Expect(val).To(Equal(math.Pow((250.0/100), exporter.ClockspeedExponent) * 1500.0))
201+
})
202+
})
203+
Describe("with an underclocked converter", func() {
204+
BeforeEach(func() {
205+
FRMServer.Reset()
206+
url = FRMServer.server.URL
207+
collector = exporter.NewFactoryBuildingCollector("/getFactory")
208+
209+
FRMServer.ReturnsFactoryBuildings([]exporter.BuildingDetail{
210+
{
211+
Building: "Converter",
212+
Recipe: "Coal",
213+
ManuSpeed: 50.0,
214+
CircuitGroupId: 0,
215+
PowerInfo: exporter.PowerInfo{
216+
CircuitGroupId: 1,
217+
PowerConsumed: 23,
218+
// value will be ignored for this - the recipe here is set to 400 * clockspeed in power_info.go
219+
MaxPowerConsumed: 4,
220+
},
221+
},
222+
})
223+
})
224+
It("recalculates max power use", func() {
225+
collector.Collect(url, sessionName)
226+
227+
val, err := gaugeValue(exporter.FactoryPowerMax, "1", url, sessionName)
228+
Expect(err).ToNot(HaveOccurred())
229+
Expect(val).To(Equal(math.Pow((50.0/100), exporter.ClockspeedExponent) * 400.0))
230+
})
231+
})
232+
Describe("with an overclocked quantum encoder", func() {
233+
BeforeEach(func() {
234+
FRMServer.Reset()
235+
url = FRMServer.server.URL
236+
collector = exporter.NewFactoryBuildingCollector("/getFactory")
237+
238+
FRMServer.ReturnsFactoryBuildings([]exporter.BuildingDetail{
239+
{
240+
Building: "Quantum Encoder",
241+
Recipe: "Power Shard",
242+
ManuSpeed: 250.0,
243+
CircuitGroupId: 0,
244+
PowerInfo: exporter.PowerInfo{
245+
CircuitGroupId: 1,
246+
PowerConsumed: 23,
247+
// value will be ignored for this - the recipe here is set to 2000 * clockspeed in power_info.go
248+
MaxPowerConsumed: 4,
249+
},
250+
},
251+
})
252+
})
253+
It("recalculates max power use", func() {
254+
collector.Collect(url, sessionName)
255+
256+
val, err := gaugeValue(exporter.FactoryPowerMax, "1", url, sessionName)
257+
Expect(err).ToNot(HaveOccurred())
258+
Expect(val).To(Equal(math.Pow((250.0/100), exporter.ClockspeedExponent) * 2000.0))
259+
})
260+
})
261+
Describe("with a somerslooped quantum encoder", func() {
262+
BeforeEach(func() {
263+
FRMServer.Reset()
264+
url = FRMServer.server.URL
265+
collector = exporter.NewFactoryBuildingCollector("/getFactory")
266+
267+
FRMServer.ReturnsFactoryBuildings([]exporter.BuildingDetail{
268+
{
269+
Building: "Quantum Encoder",
270+
Recipe: "Power Shard",
271+
ManuSpeed: 100.0,
272+
CircuitGroupId: 0,
273+
Somersloops: 4,
274+
PowerInfo: exporter.PowerInfo{
275+
CircuitGroupId: 1,
276+
PowerConsumed: 23,
277+
// value will be ignored for this - the recipe here is set to 2000 * clockspeed in power_info.go
278+
MaxPowerConsumed: 4,
279+
},
280+
},
281+
})
282+
})
283+
It("recalculates max power use", func() {
284+
collector.Collect(url, sessionName)
285+
286+
val, err := gaugeValue(exporter.FactoryPowerMax, "1", url, sessionName)
287+
Expect(err).ToNot(HaveOccurred())
288+
Expect(val).To(Equal(4 * 2000.0))
289+
})
290+
})
142291
})
143292
})

Companion/exporter/portal_collector.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ func (c *PortalCollector) Collect(frmAddress string, sessionName string) {
5656
powerInfo[d.PowerInfo.CircuitGroupId] = d.PowerInfo.PowerConsumed
5757
}
5858
val, ok = maxPowerInfo[d.PowerInfo.CircuitGroupId]
59+
// TODO: max portal power is bugged in the base game.
60+
// Replace with reported values when they are correct.
5961
if ok {
60-
maxPowerInfo[d.PowerInfo.CircuitGroupId] = val + d.PowerInfo.MaxPowerConsumed
62+
maxPowerInfo[d.PowerInfo.CircuitGroupId] = val + MaxPortalPower
6163
} else {
62-
maxPowerInfo[d.PowerInfo.CircuitGroupId] = d.PowerInfo.MaxPowerConsumed
64+
maxPowerInfo[d.PowerInfo.CircuitGroupId] = MaxPortalPower
6365
}
6466
}
6567
for circuitId, powerConsumed := range powerInfo {

Companion/exporter/portal_collector_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ var _ = Describe("PortalCollector", func() {
6060
val, err := gaugeValue(exporter.PortalPowerMax, "1", url, sessionName)
6161

6262
Expect(err).ToNot(HaveOccurred())
63-
Expect(val).To(Equal(90.0))
63+
// max is overridden here to 1000 per portal. see power_info.go.
64+
Expect(val).To(Equal(3000.0))
6465
})
6566
})
6667
})

Companion/exporter/power_collector.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strconv"
66

77
"github.com/prometheus/client_golang/prometheus"
8+
dto "github.com/prometheus/client_model/go"
89
)
910

1011
type PowerInfo struct {
@@ -31,6 +32,47 @@ type PowerDetails struct {
3132
FuseTriggered bool `json:"FuseTriggered"`
3233
}
3334

35+
// power max calculated via aggregate.
36+
// actual max is either reported max, or calculated max. Take the higher one.
37+
// see power_info.go for details about the bug we're working around.
38+
func calculateMaxPowerCategory(gauge *prometheus.GaugeVec, circuitId string, frmAddress string, sessionName string) float64 {
39+
var m = &dto.Metric{}
40+
err := gauge.WithLabelValues(circuitId, frmAddress, sessionName).Write(m)
41+
if err != nil {
42+
return 0
43+
}
44+
return m.Gauge.GetValue()
45+
}
46+
func calculateMaxPower(reportedMaxPower float64, circuitId string, frmAddress string, sessionName string) float64 {
47+
maxConsumed := reportedMaxPower
48+
factoryPowerMax := calculateMaxPowerCategory(FactoryPowerMax, circuitId, frmAddress, sessionName)
49+
extractorPowerMax := calculateMaxPowerCategory(ExtractorPowerMax, circuitId, frmAddress, sessionName)
50+
dronePowerMax := calculateMaxPowerCategory(DronePortPowerMax, circuitId, frmAddress, sessionName)
51+
frackingPowerMax := calculateMaxPowerCategory(FrackingPowerMax, circuitId, frmAddress, sessionName)
52+
hypertubePowerMax := calculateMaxPowerCategory(HypertubePowerMax, circuitId, frmAddress, sessionName)
53+
portalPowerMax := calculateMaxPowerCategory(PortalPowerMax, circuitId, frmAddress, sessionName)
54+
pumpPowerMax := calculateMaxPowerCategory(PumpPowerMax, circuitId, frmAddress, sessionName)
55+
resourceSinkPowerMax := calculateMaxPowerCategory(ResourceSinkPowerMax, circuitId, frmAddress, sessionName)
56+
trainPowerMax := calculateMaxPowerCategory(TrainCircuitPowerMax, circuitId, frmAddress, sessionName)
57+
trainStationPowerMax := calculateMaxPowerCategory(TrainStationPowerMax, circuitId, frmAddress, sessionName)
58+
vehicleStationPowerMax := calculateMaxPowerCategory(VehicleStationPowerMax, circuitId, frmAddress, sessionName)
59+
calculatedMaxConsumed := factoryPowerMax +
60+
extractorPowerMax +
61+
dronePowerMax +
62+
frackingPowerMax +
63+
hypertubePowerMax +
64+
portalPowerMax +
65+
pumpPowerMax +
66+
resourceSinkPowerMax +
67+
trainPowerMax +
68+
trainStationPowerMax +
69+
vehicleStationPowerMax
70+
if calculatedMaxConsumed > maxConsumed {
71+
maxConsumed = calculatedMaxConsumed
72+
}
73+
return maxConsumed
74+
}
75+
3476
func NewPowerCollector(endpoint string) *PowerCollector {
3577
return &PowerCollector{
3678
endpoint: endpoint,
@@ -84,7 +126,10 @@ func (c *PowerCollector) Collect(frmAddress string, sessionName string) {
84126
c.metricsDropper.CacheFreshMetricLabel(prometheus.Labels{"url": frmAddress, "session_name": sessionName, "circuit_id": circuitId})
85127
PowerConsumed.WithLabelValues(circuitId, frmAddress, sessionName).Set(d.PowerConsumed)
86128
PowerCapacity.WithLabelValues(circuitId, frmAddress, sessionName).Set(d.PowerCapacity)
87-
PowerMaxConsumed.WithLabelValues(circuitId, frmAddress, sessionName).Set(d.PowerMaxConsumed)
129+
130+
maxConsumed := calculateMaxPower(d.PowerMaxConsumed, circuitId, frmAddress, sessionName)
131+
PowerMaxConsumed.WithLabelValues(circuitId, frmAddress, sessionName).Set(maxConsumed)
132+
88133
BatteryDifferential.WithLabelValues(circuitId, frmAddress, sessionName).Set(d.BatteryDifferential)
89134
BatteryPercent.WithLabelValues(circuitId, frmAddress, sessionName).Set(d.BatteryPercent)
90135
BatteryCapacity.WithLabelValues(circuitId, frmAddress, sessionName).Set(d.BatteryCapacity)

Companion/exporter/power_collector_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ var _ = Describe("PowerCollector", func() {
7373
Expect(err).ToNot(HaveOccurred())
7474
Expect(val).To(Equal(float64(0)))
7575

76-
val2, err := gaugeValue(exporter.FuseTriggered, "2", url, sessionName)
76+
val2, _ := gaugeValue(exporter.FuseTriggered, "2", url, sessionName)
7777
Expect(val2).To(Equal(float64(1)))
7878
})
7979
It("sets the 'battery_differential' metric with the right labels", func() {
@@ -84,8 +84,27 @@ var _ = Describe("PowerCollector", func() {
8484
Expect(err).ToNot(HaveOccurred())
8585
Expect(val).To(Equal(float64(12)))
8686

87-
val2, err := gaugeValue(exporter.BatteryDifferential, "2", url, sessionName)
87+
val2, _ := gaugeValue(exporter.BatteryDifferential, "2", url, sessionName)
8888
Expect(val2).To(Equal(float64(-12)))
8989
})
90+
It("corrects max power when calculated is higher", func() {
91+
exporter.FactoryPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
92+
exporter.ExtractorPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
93+
exporter.DronePortPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
94+
exporter.FrackingPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
95+
exporter.HypertubePowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
96+
exporter.PortalPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
97+
exporter.PumpPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
98+
exporter.ResourceSinkPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
99+
exporter.TrainCircuitPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
100+
exporter.TrainStationPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
101+
exporter.VehicleStationPowerMax.WithLabelValues("1", url, sessionName).Set(100.0)
102+
collector.Collect(url, sessionName)
103+
104+
val, err := gaugeValue(exporter.PowerMaxConsumed, "1", url, sessionName)
105+
106+
Expect(err).ToNot(HaveOccurred())
107+
Expect(val).To(Equal(1100.0))
108+
})
90109
})
91110
})

0 commit comments

Comments
 (0)