Repository navigation
Reading Runtime Data
This page is aimed at mods that only read Revamp's data — HUD displays, info extensions, statistics and planning tools. If you want to bring your own productions, buildings or goods, the XML reference and Connecting other mods are the right places; this page is exclusively about what is in memory at runtime and how to read it correctly.
The guiding principle: Revamp changes the meaning of vanilla data, not just its content. A tool that evaluates the recipes of a production point the way it does in the vanilla game will reliably calculate wrong in three places as soon as a Revamp production is among them — with mix groups, with boosters and with shared storage. The sections on this follow further below.
Whether Revamp is loaded at all is described under Compatibility — the mod name is fixed as FS25_0_ProductionRevamp.
Everything that sits on vanilla classes (ProductionPoint, Storage, the entries in productions, inputs, outputs) can be read without further effort: these are the same tables the game itself uses, Revamp only attaches its own fields to them. For the sections below, a foreign mod therefore as a rule needs no access to Revamp's own code.
Revamp's own globals (Revamp.*), on the other hand, live in Revamp's mod environment and are not directly visible from outside. The game stores every mod environment under its mod name in _G, so the way there is:
local revampEnv = _G["FS25_0_ProductionRevamp"]
local revamp = revampEnv ~= nil and revampEnv.Revamp or nil
if revamp ~= nil then
-- e.g. revamp.MIX_BOOST, revamp.getIsSharedStorageFillType(storage, fillType)
endThis is rarely necessary. The few numeric values you really need are written out below — they are part of the saved data model and will not change anymore.
A recipe line is still an entry from productionPoint.productions (or productionPoint.productionsIdToObj[id]), with vanilla's inputs, outputs, cyclesPerMinute, status and id. Revamp attaches additional fields to every line:
| Field | Meaning |
|---|---|
master |
The line has at least one master booster |
limitBoosters |
Only a single booster counts, not all at the same time |
mixMode / boostMode
|
Selection method ASC/DESC/MOST/LEAST — can be changed by the player at runtime
|
hasMixChoice / hasBoostChoice
|
Whether this choice has any effect at all on this line |
hideFromMenu / autoStart
|
Hidden or self-starting line |
sharedThroughputCapacity |
Whether the line takes part in the shared throughput capacity |
modes, hoursTable, seasonsList, monthsList
|
Weather and time conditions, see Weather and time |
hoursText, seasonText, monthsText
|
The same conditions as ready-made display text |
weatherFactor, minPower, maxPower
|
Static weather parameters from the XML |
weatherFactorCurrent |
Current weather share, nil if the line has no weather modes |
modeStatus |
nil if the line is allowed to run, otherwise the status with which it is currently stopped |
boosterFactor, masterFactor
|
Result of the last cycle, see below |
mixGroupSatisfied, mixGroupWinnerType
|
Per mix group: whether it is satisfied and which type is currently winning |
mixGroupTitles |
Custom heading of a mix group, if the XML assigns one |
primaryProductFillType |
Main product enforced by the recipe |
productionPoint.openingHoursTable combines the opening hours of all lines: the business counts as open as long as at least one line is allowed to run.
Alternative ingredients are not nested, but are completely ordinary entries in production.inputs, linked via a shared field mix:
input.mix |
Meaning |
|---|---|
0 or nil
|
Mandatory ingredient, exactly as in vanilla |
1 to 97
|
Member of the mix group with this number — per cycle one of them is needed and deducted |
98 |
Booster (optional) |
99 |
Master booster (optional) |
Anyone who naively iterates over inputs considers every alternative to be individually mandatory and reports a shortage even though the group has long been satisfied. The correct way is to group them:
local MIX_BOOST, MIX_MASTER = 98, 99
local required, groups = {}, {}
for _, input in ipairs(production.inputs) do
local mix = input.mix or 0
if mix == 0 then
table.insert(required, input)
elseif mix ~= MIX_BOOST and mix ~= MIX_MASTER then
groups[mix] = groups[mix] or {}
table.insert(groups[mix], input)
end
-- Boosters and master boosters are deliberately left out here: missing stock
-- never stops the production, it only makes the bonus drop out.
endTwo things that are easily overlooked:
- A group blocks the line if none of its members is sufficiently stocked — so it behaves like a mandatory ingredient, just with a choice. Whether it is currently satisfied is in
production.mixGroupSatisfied[mix], the currently chosen type inproduction.mixGroupWinnerType[mix]. - Which member wins is decided by
production.mixMode(ASCthe first,DESCthe last,MOSTthe best stocked,LEASTthe least stocked). The value is not a static XML value: the player changes it in the manage dialog, so it can change during a running game.
An ingredient that was specified in the XML via fillTypeCategory can no longer be recognized as such at runtime — Revamp already resolves it into ordinary entries of the same mix group while loading. For a reading mod this is the convenient case: it only ever sees real fill types.
Both are optional ingredients that change the throughput. Their input.boostFactor is a pure multiplier (neutral value 1, which is why it is not allowed as an XML value); input.boostSatisfied tells whether the stock was sufficient for it in the last cycle.
-
Boosters (
mix == 98) act on the products. WithoutlimitBoostersevery sufficiently stocked booster contributes its factor and several multiply with each other; withlimitBoostersonly one counts, selected byboostMode. The result is inproduction.boosterFactor. -
Master boosters (
mix == 99) act on the whole line — consumption and production, just not on their own consumption. The result is inproduction.masterFactor.
On the individual product, output.boost decides whether the booster applies at all: true multiplies by boosterFactor, false leaves the amount untouched, "reverse" uses the reciprocal (a booster then reduces this output).
The numbers in input.amount and output.amount are nominal recipe values and not what a running business actually converts:
takt = cyclesPerMinute
* (weatherFactorCurrent or 1)
* masterFactor
/ (number of lines currently sharing the throughput capacity)
consumption per ingredient = input.amount * takt (the master input itself without masterFactor)
production per product = output.amount * takt * booster share of this output
The last divisor only applies if productionPoint.sharedThroughputCapacity and production.sharedThroughputCapacity are set. rngAffected on an ingredient or a product additionally multiplies by a random value between 0 and 1 — an exact prediction is then fundamentally impossible, an average of 50 % comes closest.
boosterFactor, masterFactor and weatherFactorCurrent come from the respective last production cycle. They are not set at all before the first cycle and stay at their old value as long as a line is stopped via modeStatus — so always guard them with or 1 and do not treat them as current while modeStatus is set.
A storage can have a total capacity from which all fill types without their own capacity line are served. A grain storage of 60,000 liters holds 60,000 liters, no matter how many types they are spread across.
For this, Revamp corrects Storage:getCapacity and Storage:getFreeCapacity (vanilla contradicts itself here and, depending on the case, reports too little free space or the full pool for every type). Anyone calling these two methods automatically gets the correct values and does not need to do anything else.
It goes wrong as soon as a mod reads the raw fields instead:
-
storage.capacityis the size of the shared pool, not the capacity of a single type. -
storage.capacities[fillType]isnilfor every type that shares the pool — thisnilmeans "shared pool", not "no space".
This has a consequence for every remaining-time or utilisation calculation: the space for a product depends on the fill level of other types and changes without anything happening to the product itself. A bar that calculates fillLevel / storage.capacity shows a utilisation that does not exist like that; storage:getCapacity(fillType), on the other hand, returns what is actually reachable for this type right now.
Whether a type is affected can be checked without Revamp access: storage.supportsMultipleFillTypes == true and storage.fillLevels[fillType] ~= nil and storage.capacities[fillType] == nil.
Revamp adds a fourth output mode to vanilla's output modes (KEEP, DIRECT_SELL, AUTO_DELIVER):
ProductionPoint.OUTPUT_MODE.STORE = 3"Keep" (STORE) suppresses the automatic pallet and bale spawning for a fill type. productionPoint:getOutputDistributionMode(fillType) returns it as usual, and the affected fill types are additionally listed in productionPoint.outputFillTypeIdsStorage. A tool that translates the modes into text therefore needs a fourth case — otherwise it shows nothing or something wrong for stored goods.
The distribution targets of a production point are located under productionPoint.distributionTargets[fillTypeIndex][uniqueId] = {level = "PRIMARY"|"SECONDARY"|"REST"|"NEVER", limit = <liters or nil>}. The key is the uniqueId of the target placeable; the level REST without a limit is the default case and is not saved at all. What the levels mean is described under Distribution.
Revamp changes some vanilla constants for the whole savegame. A reading mod that sends its own values over the network or translates status values into text needs to know this:
| Constant | Vanilla | With Revamp |
|---|---|---|
ProductionPoint.PROD_STATUS |
INACTIVE, RUNNING, MISSING_INPUTS, NO_OUTPUT_SPACE
|
additionally OUTSIDE_OF_SEASONS (4), OUTSIDE_OF_MONTHS (5), OUTSIDE_OF_HOURS (6), NO_WEATHER (7) |
ProductionPoint.PROD_STATUS_NUM_BITS |
2 | 3 |
FillTypeManager.SEND_NUM_BITS |
8 (255 fill types) | 10 (1024 fill types) |
ProductionChainManager.NUM_MAX_PRODUCTION_POINTS |
Vanilla limit | 360 |
SlotSystem.NUM_OBJECT_LIMITS |
Bales and pallets limited | unlimited on PC, still limited on consoles |
The new statuses have matching texts in ProductionPoint.PROD_STATUS_TO_L10N — anyone who uses this table instead of their own mapping gets them without any extra effort.
If another mod overwrites the same function, both sides must call superFunc, otherwise one pushes the other out. With Utils.prependedFunction and Utils.appendedFunction the question does not arise, they are compatible by themselves — the critical line is every "overwritten".
| Function | Type |
|---|---|
FSBaseMission.onConnectionFinishedLoading |
appended |
FSCareerMissionInfo.saveToXMLFile |
appended |
FillTypeDesc.loadFromXMLFile |
overwritten |
FillTypeManager.loadModFillTypes |
appended |
FinanceStats.new |
overwritten |
InGameMenuProductionFrame.getCellTypeForItemInSection |
overwritten |
InGameMenuProductionFrame.getNumberOfItemsInSection |
overwritten |
InGameMenuProductionFrame.getNumberOfSections |
overwritten |
InGameMenuProductionFrame.getTitleForSectionHeader |
overwritten |
InGameMenuProductionFrame.populateCellForItemInSection |
overwritten and appended |
InGameMenuProductionFrame.updateMenuButtons |
appended |
InGameMenuSettingsFrame.onFrameOpen |
appended |
InGameMenuSettingsFrame.updateGameSettings |
appended |
Mission00.loadMission00Finished |
appended |
PalletSpawner.getSupportedFillTypes |
overwritten |
PalletSpawner.update |
overwritten |
ProductionChainManager.distributeGoods |
overwritten |
ProductionPoint.delete |
prepended |
ProductionPoint.getOutputDistributionMode |
overwritten |
ProductionPoint.load |
overwritten |
ProductionPoint.loadFromXMLFile |
overwritten |
ProductionPoint.loadSpecValueInputFillTypes |
completely replaced, without calling the original version |
ProductionPoint.readStream |
prepended |
ProductionPoint.registerSavegameXMLPaths |
prepended |
ProductionPoint.registerXMLPaths |
prepended |
ProductionPoint.saveToXMLFile |
prepended |
ProductionPoint.setOutputDistributionMode |
overwritten |
ProductionPoint.setProductionState |
overwritten |
ProductionPoint.toggleOutputDistributionMode |
overwritten |
ProductionPoint.updateProduction |
overwritten |
ProductionPoint.writeStream |
prepended |
ProductionPointActivatable.run |
overwritten |
SmoothListElement.updateView |
overwritten |
Storage.getCapacity |
overwritten |
Storage.getFreeCapacity |
overwritten |
Storage.load |
overwritten |
StorageSystem.addUnloadingStation |
overwritten |
StoreManager.loadMapData |
prepended |
ProductionPoint.load, ProductionPoint.loadFromXMLFile and ProductionPoint.updateProduction carry several Revamp layers on top of each other — a further layer from a foreign mod is therefore no problem, as long as it calls superFunc.
In addition, new methods are added to ProductionPoint (setDistributionTarget, setRecipeSelectionModes, transportAnimals, calculateAnimals, getIsSubTypeAllowedForTrigger, getPalletCallback) as well as a new money type MoneyType.PRODUCTION_DELIVERY_COSTS for the delivery costs. None of these overwrite anything, a name conflict would only arise with a mod that uses the same names.
Several Revamp areas can be switched off per savegame (see Settings). If that has happened, vanilla behavior applies again for the affected area, even though the data is still saved and in memory — so a distribution or "Keep" state that has been read out may currently have no effect. Affected are distribution, "Keep", buying, selling, the weather and time modes, and the selection modes. For the output mode Revamp handles this itself: with the storage extension switched off, getOutputDistributionMode returns vanilla's value again.
How to detect it: ProductionPoint.REVAMP_SETTINGS contains the current state of all settings under the same names that the Settings page uses. Without Revamp the field does not exist.
| Field | Values |
|---|---|
distributionActive |
Distribution to other production points |
distributionCostFactor |
Factor on the delivery costs, 1 = unchanged |
storageActive |
Storage extension including the output mode "Keep" |
buyingActive |
Buy ingredients |
sellingActive |
Sell products |
tradeRestriction |
"everyone", "farmManager" or "productionAgent"
|
directSellingPriceFactor |
Factor on the revenue for direct selling |
weatherModesActive |
Weather and time conditions of the recipe lines |
recipeSettingsActive |
Selection modes for mix groups and boosters |
The values are present on the client just as on the server: until the server's settings arrive, the start values apply; a half-filled state does not occur.
local revampSettings = ProductionPoint.REVAMP_SETTINGS
if revampSettings ~= nil and not revampSettings.weatherModesActive then
-- hoursTable, seasonsList and monthsList are present, but currently do not stop any line
endAs of: version 0.0.0.33. This reference describes the built development state. The fields on production, input, output and productionPoint as well as the constants above are part of the data model and are deliberately kept stable; everything else may still change.