
Welcome to the Metruvia Content Creator Scripting Border Guide. You have already mastered the “physical” side of modding: the file structures, the polygon budgets, and the IP compliance. Now, we enter the “digital” side—the code that breathes life into your assets.
In the localized, PC-only ecosystem of the Steam Workshop, the Transport Fever 2 engine allows for remarkable flexibility. PC modders have historically used Lua to completely overhaul UI systems, inject custom shaders, and read external files. However, on PlayStation 5 and Xbox Series X|S, this level of access is a critical security vulnerability.
When you check the “Console Compatible” box on Mod.io, your code is subjected to a strict Sandbox. Sony and Microsoft do not permit arbitrary code execution on their closed hardware. If your script attempts to call a restricted OS-level function, or if it introduces a custom UI element that a gamepad controller cannot navigate, your mod will be instantly rejected.
This masterclass will deconstruct the Transport Fever 2 Lua Sandbox. We will define the exact denylist of prohibited functions, explore the limitations of the UI API, and provide the architectural knowledge needed to write safe, console-compliant logic that enhances the game without violating hardware security protocols.
1. The Architecture of the Lua Sandbox
To understand why your code might be rejected, you must understand how Transport Fever 2 handles scripts. The game utilizes Lua 5.3 as its embedded scripting language. However, the environment your mod executes within is not standard Lua; it is a heavily restricted virtual container.
1.1 The Principle of Least Privilege
The sandbox operates on the “Principle of Least Privilege.” Your mod is only granted access to the specific internal game APIs necessary to function (game.config, api.engine, api.res). It is entirely blinded to the outside world. It cannot know what operating system it is running on, it cannot see the player’s network connection, and it cannot access the local file system beyond its own designated Virtual File System (VFS) boundaries.
1.2 Deterministic Execution
On consoles, stability is paramount. The engine requires scripts to be deterministic. This means that given the same input, your code must produce the same output every single time.
Non-Deterministic Trap: Using standard Lua functions to get the current system time or random numbers from the OS.
The Solution: You must use the internal game-clock and the game’s seeded random number generator. If your mod creates a “random” chance for an industry to spawn, but that randomness varies between a PC player and a Console player in the same save-file context, the simulation will desync and crash.
2. The Denylist: Triggers for Immediate Security Rejection
The Urban Games API proactively strips access to standard Lua libraries that pose security threats. Attempting to call any of the following functions will result in a fatal nil value error, an engine crash, and a permanent rejection from the Mod.io console pool.
2.1 The os.* Library (Operating System Access)
The os library interacts directly with the operating system. On a console, it is a vector for system-level exploitation.
| Restricted Function | Why it is Banned |
os.execute | Can be used to run external malicious binaries. |
os.remove / os.rename | Could potentially delete or corrupt system files. |
os.getenv | Could be used to scrape user information or system IDs. |
os.tmpname | Accesses temporary directories outside the game sandbox. |
2.2 The io.* Library (File Input/Output)
While you can read configuration files packaged within your mod’s folder structure using the Transport Fever 2 specific file handlers, the standard Lua I/O library is restricted to prevent unauthorized data exfiltration.
Prohibited: io.open, io.popen, io.tmpfile.
The Consequence: You cannot write dynamic log files to a console’s SSD, nor can you read external configuration data from outside your specific mod directory.
2.3 Binary Extensions and package.loadlib
PC modders sometimes use .dll (Windows) or .so (Linux) files to extend Lua’s capabilities.
The Rule: Consoles do not run external binary code. Any attempt to use package.loadlib or require on a non-Lua file is an immediate violation of Sony and Microsoft developer agreements. Your mod must be pure Lua.
3. Algorithmic Efficiency: The Watchdog Timer
Consoles have strict “Watchdog Timers” to prevent hard lockups. If your Lua script contains an inefficient loop that takes too long to execute, the console OS will assume the game has frozen and forcefully terminate the application.
3.1 Time Complexity and the CPU Budget
When parsing massive arrays (like checking every single vehicle on a map), you must optimize for complexity.
Safe ($O(n)$): A single loop through the vehicle list.
Dangerous ($O(n^2)$): A loop inside a loop (e.g., checking every vehicle against every other vehicle).
If you have 1,000 vehicles, an $O(n^2)$ script requires $1,000,000$ operations. On a console CPU already handling the game’s complex pathfinding, this spike will trigger the watchdog.
3.2 Garbage Collection (GC) Spikes
Lua uses automatic memory management. If you create and destroy thousands of temporary tables in a single frame, you trigger a “GC Spike.” This causes a rhythmic stutter in the framerate.
The Strategy: Pre-allocate your tables. Instead of creating a new local myData = {} inside a loop, create it once outside and clear it for re-use.
4. The UI Barrier: Gamepad Navigation Meshes
The most common reason for mod rejection on Mod.io is the mishandling of the UI API.
4.1 Mouse vs. Controller
PC players use a mouse with free-roaming pixel accuracy. Console players use a controller, which relies on UI Nav-Meshes. The highlight (focus) jumps from one button to the next based on predefined paths.
4.2 The “Unreachable Button”
If you use Lua to inject a custom “Auto-Schedule” button into the vehicle management window, a PC player simply clicks it. However, the console’s UI Nav-Mesh does not know your button exists. The controller highlight will skip right over it.
4.3 The Strict UI Prohibition
Because Urban Games cannot dynamically generate controller support for arbitrary UI elements created by modders, all custom UI alterations are prohibited for Console Compatible mods.
Custom windows Prohibited.
No custom buttons in vanilla menus.
Interception of mouse-click events Prohibited.
5. The mod.lua Lifecycle: State and Persistence
Understanding exactly when your code executes is the difference between a stable mod and a save-game breaker.
5.1 runFn(settings, modParams)
This runs once when the save game is loaded. It is used to modify global game configuration tables.
Caution: Changes made here are “baked” into the save. If you change a vanilla value here, that change persists as long as the mod is active.
5.2 postRunFn(settings, modParams)
This executes after all other mods have completed their runFn.
Why it Matters: If you want to modify a vehicle added by another mod, you must do it in postRunFn. If you do it in runFn, your script might run before the other mod has even loaded its vehicles.
5.3 The guiInit and guiUpdate Trap
These functions are used for UI logic. For console mods, these should remain empty. Any logic placed here that attempts to draw to the screen will cause a rejection.
6. Safe Scripting: Utilizing the api.engine
If you cannot use external libraries or custom UI, how do you add features? The answer lies in the Component System.
6.1 Modifier Scripts
You can safely iterate through the game’s resource repository to apply changes without risking security.
— Example: Safe maintenance cost modifier
api.res.modelRep.addModifier(“my_modifier”, function (fileName, data)
if data.metadata and data.metadata.maintenance then
data.metadata.maintenance.runningCosts = data.metadata.maintenance.runningCosts * 0.8
end
return data
end)
6.2 The Scripting Component (api.type.Component)
Transport Fever 2 allows you to attach custom data to entities using components. This is the “safe” way to store mod-specific data (like a vehicle’s custom mileage or an industry’s specialty) without breaking the save file structure.
7. Save-Game Integrity: severityRemove
Because scripts can alter the deep logic of a save file, you must tell the game how dangerous it is to remove your mod.
NONE: Removing the mod has no effect (e.g., a simple texture swap).
WARNING: Removing it might delete a vehicle but won’t crash the game.
CRITICAL: Mandatory for economy overhauls or mods that add new cargo types. If this is removed, the save will crash.
8. The Diagnostic Matrix: Troubleshooting Console Crashes
When your mod crashes locally, you must learn to read the stdout.txt log file.
| Error Message | Meaning | Solution |
attempt to index a nil value | Your code tried to access a table that doesn’t exist. | Add a “nil-check”: if myTable then... |
stack overflow | Infinite loop detected. | Check your recursive functions. |
attempt to call a nil value (global 'os') | You used a banned library. | Remove all os.* and io.* calls. |
Out of memory (Lua) | Too many temporary tables created. | Refactor for memory efficiency. |
9. Final Audit: The Scripting Certification Checklist
Before you hit “Publish” on Mod.io, run your codebase through this final security audit:
Banned Libraries: Search for os., io., debug., and require. Delete any instances.
UI Check: Does your mod require a custom button? If so, it is PC-only. Strip the UI for the console version.
Complexity Check: Are you running a loop-inside-a-loop on the entire entity list? Optimize to $O(n)$.
Deterministic Logic: Are you using math.random? Switch to the game’s seeded random generator to prevent desyncs.
Metadata: Is your severityRemove set correctly?
By respecting the Sandbox and focusing on algorithmic efficiency, you ensure that your technical modifications are as stable as they are innovative. You have bridged the gap between PC complexity and console reliability.

