Skip to content

Reading Runtime Data

Achimobil edited this page Oct 7, 2026 · 1 revision

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.

How to detect Revamp

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)
end

This 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.

Recipe lines

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.

Mix groups

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.
end

Two 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 in production.mixGroupWinnerType[mix].
  • Which member wins is decided by production.mixMode (ASC the first, DESC the last, MOST the best stocked, LEAST the 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.

Boosters and master boosters

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. Without limitBoosters every sufficiently stocked booster contributes its factor and several multiply with each other; with limitBoosters only one counts, selected by boostMode. The result is in production.boosterFactor.
  • Master boosters (mix == 99) act on the whole line — consumption and production, just not on their own consumption. The result is in production.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).

What actually flows per cycle

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.

Storage: the shared pool

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.capacity is the size of the shared pool, not the capacity of a single type.
  • storage.capacities[fillType] is nil for every type that shares the pool — this nil means "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.

Output mode and distribution targets

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.

Extended vanilla constants

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.

Vanilla functions overwritten by Revamp

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.

What an admin may have switched off

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
end

As 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.

Clone this wiki locally