Bug Report: MIR Crash on on_init() Caused by External Mods Mutating Hidden Carrier Technologies
1. Summary
In Factorio 2.1 / Space Age, More Infinite Research (e.g. version 5.0.21014) crashes during world initialization (on_init) when played alongside mods that iterate over data.raw.technology and inject extra productivity recipes without checking if a technology is hidden or internal.
This was observed with Cerys-Moon-of-Fulgora (and previously encountered with rigor-module before it was patched).
2. Exact Error & Stack Trace
Error while running event more-infinite-research::on_init()
__more-infinite-research__/runtime/native_credits.lua:1050: MIR native family credit has invalid main or carrier [MIR-FAMILY-CREDIT family="research_low_density_structure"; recipe="scrap-recycling"; force=1; expected_change=0.1; scope_valid=true; main_recipe_change=nil; main.id="recipe-prod-research_low_density_structure-1"; main.name="recipe-prod-research_low_density_structure-1"; main.level=1; main.researched=false; main.prototype_name="recipe-prod-research_low_density_structure-1"; main.first=1; main.max=4294967295; main.hidden=true; main.enabled=true; main.visible_disabled=false; main.effects_type="table"; main.effects_count=0; positive.id="mir-native-credit-research_low_density_structure-scrap-recycling-positive"; positive.name="mir-native-credit-research_low_density_structure-scrap-recycling-positive"; positive.level=1; positive.researched=false; positive.prototype_name="mir-native-credit-research_low_density_structure-scrap-recycling-positive"; positive.first=1; positive.max=4294967295; positive.hidden=true; positive.enabled=false; positive.visible_disabled=false; positive.effects_type="table"; positive.effects_count=2; positive.effect_type="change-recipe-productivity"; positive.recipe="scrap-recycling"; positive.change=0.1; negative.id="mir-native-credit-research_low_density_structure-scrap-recycling-negative"; negative.name="mir-native-credit-research_low_density_structure-scrap-recycling-negative"; negative.level=1; negative.researched=false; negative.prototype_name="mir-native-credit-research_low_density_structure-scrap-recycling-negative"; negative.first=1; negative.max=4294967295; negative.hidden=true; negative.enabled=false; negative.visible_disabled=false; negative.effects_type="table"; negative.effects_count=2; negative.effect_type="change-recipe-productivity"; negative.recipe="scrap-recycling"; negative.change=-0.1]
stack traceback:
[C]: in function 'error'
__more-infinite-research__/runtime/native_credits.lua:417: in function 'family_observations'
__more-infinite-research__/runtime/native_credits.lua:819: in function 'preflight_family_transition'
__more-infinite-research__/runtime/native_credits.lua:1050: in function 'on_init'
__more-infinite-research__/runtime/bootstrap.lua:346: in function <__more-infinite-research__/runtime/bootstrap.lua:338>
Key detail from the error message:
* positive.effects_count=2
* negative.effects_count=2
3. Root Cause Analysis
How More Infinite Research Works
In research/native_credit_definitions.lua, MIR creates internal carrier technologies:
* positive_id = "mir-native-credit-" .. family .. "-" .. recipe .. "-positive"
* negative_id = "mir-native-credit-" .. family .. "-" .. recipe .. "-negative"
These carriers are created with:
* hidden = true
* enabled = false
* Exactly 1 effect in effects: {type = "change-recipe-productivity", recipe = recipe, change = change}.
At runtime (runtime/native_credits.lua), carrier_scalar inspects each carrier:
local function carrier_scalar(technology, descriptor, direction)
local effects = carrier_effects(technology, descriptor, direction)
if effects == nil or #effects ~= 1 or next(effects) ~= 1 or next(effects,1) ~= nil then return nil end
...
MIR expects #effects == 1. If #effects > 1, carrier_scalar returns nil, causing MIR to fall back to carrier_vector. carrier_vector then looks for a public reference technology matching the combined vector (e.g. scrap-recycling-productivity), which does not exist for arbitrary foreign recipe combinations, ultimately raising the unrecoverable error.
What Other Mods Do
In data-final-fixes.lua, certain mods iterate blindly across all technologies in data.raw.technology.
For example, in Cerys-Moon-of-Fulgora (prototypes/override-final/technology.lua):
for _, technology in pairs(data.raw.technology) do
local processing_unit_productivity_change = 0
local scrap_recycling_productivity_change = 0
...
if technology.effects then
for _, effect in ipairs(technology.effects) do
if effect.type == "change-recipe-productivity" and effect.recipe == "scrap-recycling" then
scrap_recycling_productivity_change = scrap_recycling_productivity_change + effect.change
end
...
end
if scrap_recycling_productivity_change ~= 0 and cerys_scrap_productivity_change == 0 then
table.insert(technology.effects, {
type = "change-recipe-productivity",
recipe = "cerys-nuclear-scrap-recycling",
change = scrap_recycling_productivity_change
})
end
end
end
Because Cerys does not check if technology.hidden, it injects "cerys-nuclear-scrap-recycling" directly into MIR's mir-native-credit-research_low_density_structure-scrap-recycling-positive and -negative carriers.
This changes #effects from 1 to 2, directly causing MIR to fail validation at runtime.
(A similar loop previously occurred in rigor-module, where it injected extra variants into all technology effects until fixed).
4. Proposed Solutions
Solution 1: Ordering dependency in info.json (For More Infinite Research)
Add content and planet mods running late data-final-fixes.lua scripts to dependencies with (?) in MIR's info.json:
"(?) Cerys-Moon-of-Fulgora"
When Cerys loads before MIR, MIR creates its carriers after Cerys's override-final/technology.lua has already finished executing, so the carriers remain untouched.
Solution 2: Carrier sanitization / self-defense in MIR (Recommended for MIR author)
In data-final-fixes.lua right before calling generator.install(technologies, data), or at the end of the data stage:
Strip any foreign-injected effects from technologies whose names match "^mir%-native%-credit%-", ensuring only the single original effect matching the carrier's target recipe is retained:
for _, technology in ipairs(technologies) do
if technology.name and technology.name:find("^mir%-native%-credit%-") and technology.effects and #technology.effects > 1 then
-- Retain only the intended primary effect for this carrier
local primary_effect = technology.effects[1]
technology.effects = { primary_effect }
end
end
Alternatively, in runtime/native_credits.lua:
Have carrier_scalar filter and locate the specific effect matching descriptor.recipe rather than rejecting #effects ~= 1 if third-party mods appended extra effects.
Solution 3: Proper filtering in external mods (For Cerys / other mod authors)
Any mod that iterates over data.raw.technology to propagate recipe productivity must skip hidden and internal carrier technologies:
for _, technology in pairs(data.raw.technology) do
if not technology.hidden
and technology.enabled ~= false
and not (technology.name and (technology.name:find("^mir%-") or technology.name:find("^recipe%-prod%-")))
and technology.effects then
-- proceed with effect propagation
end
end
(This is the exact filter that rigor-module implemented in its data-final-fixes.lua line 712).