Temporary wrappers — the golden rule¶
RitnLib*runtime classes are views, not entities. Create one in your handler, use it, throw it away.
This is the rule to know before touching RitnLibPlayer, RitnLibEntity, RitnLibSurface, RitnLibForce, RitnLibEvent, RitnLibRecipe, RitnLibTechnology, RitnLibGui. Ignore it and you'll run into strange, intermittent bugs that survive across saves.
Why¶
When you instantiate a wrapper, its constructor takes a static snapshot of the Factorio properties. Example with RitnLibPlayer:
RitnLibPlayer = ritnlib.classFactory.newclass(function(self, LuaPlayer)
self.player = LuaPlayer
self.index = LuaPlayer.index
self.surface = LuaPlayer.surface -- ← snapshot
self.force = LuaPlayer.force -- ← snapshot
self.controller_type = LuaPlayer.controller_type -- ← snapshot
self.controller_name = getControllerName(LuaPlayer)
self.character = LuaPlayer.character -- ← snapshot
self.driving = LuaPlayer.driving -- ← snapshot
self.vehicle = LuaPlayer.vehicle -- ← snapshot
self.connected = LuaPlayer.connected -- ← snapshot
-- ...
end)
All of these properties change during play: a player switches surface, enters editor mode (controller_type changes), boards a vehicle, disconnects. The wrapper keeps its initial values.
If you reuse the wrapper later, you'll read stale values — without any error being raised. self.player stays valid (it's a LuaPlayer reference) but self.surface, self.driving, etc. no longer reflect reality.
Good practice¶
Instantiate the wrapper inside the handler that needs it, just before you use it:
script.on_event(defines.events.on_player_changed_surface, function(event)
local player = RitnLibPlayer(game.get_player(event.player_index))
if player.isPresent then
player:print("You're now on " .. player.surface.name)
end
end)
The wrapper dies at end-of-function. At the next event, you create a fresh one reflecting the current state.
What not to do¶
❌ Storing the wrapper in storage¶
-- DON'T DO THIS
script.on_init(function()
storage.player_wrapper = RitnLibPlayer(game.get_player(1))
end)
Why it's broken:
1. At save time, Factorio knows how to serialize the inner LuaPlayer (managed userdata). But the scalar copies (self.surface, self.controller_type…) are serialized as values frozen at init time.
2. At load time, Factorio restores those scalars as-is — even if the player has changed surface ten times since.
3. You end up with an object whose self.surface points at a LuaSurface that's no longer the player's current surface.
❌ Keeping the wrapper between two events¶
-- DON'T DO THIS
local cached_wrapper
script.on_event(defines.events.on_player_created, function(event)
cached_wrapper = RitnLibPlayer(game.get_player(event.player_index))
end)
script.on_event(defines.events.on_player_changed_surface, function(event)
cached_wrapper:print("New surface: " .. cached_wrapper.surface.name)
-- ⚠ cached_wrapper.surface is the OLD surface, not the new one
end)
The cached_wrapper variable survives between events (it lives in control.lua's upvalue), but its scalar fields are frozen at instantiation time.
❌ Reusing a RitnLibEvent past its event¶
-- DON'T DO THIS
local last_event
script.on_event(defines.events.on_built_entity, function(event)
last_event = RitnLibEvent(event)
end)
script.on_event(defines.events.on_tick, function()
if last_event then
log("Last entity: " .. last_event.entity.name)
-- ⚠ last_event.entity may have been destroyed since
end
end)
The inner LuaEntity may have been invalidated (entity.valid == false) — any operation on the wrapper will raise.
When caching is OK¶
You can store primitive values extracted from the wrapper in storage, never the wrapper itself:
-- ✅ OK
script.on_event(defines.events.on_player_created, function(event)
local p = RitnLibPlayer(game.get_player(event.player_index))
storage.player_names = storage.player_names or {}
storage.player_names[p.index] = p.name -- storing the string, not the wrapper
end)
Or store the raw LuaPlayer (Factorio knows how to persist it) and re-wrap as needed:
-- ✅ Also OK
script.on_event(defines.events.on_player_created, function(event)
storage.tracked_players = storage.tracked_players or {}
table.insert(storage.tracked_players, game.get_player(event.player_index))
end)
script.on_event(defines.events.on_tick, function()
for _, raw_player in pairs(storage.tracked_players or {}) do
if raw_player.valid then
local p = RitnLibPlayer(raw_player) -- fresh wrapper every tick
-- ...
end
end
end)
Summary¶
| ✅ Do | ❌ Avoid |
|---|---|
RitnLibPlayer(player) inside the handler |
storage.wrapper = RitnLibPlayer(...) |
| Re-instantiate on demand | Reuse a wrapper across events |
Store the raw LuaPlayer |
Store the wrapper |
| Store extracted primitives (string, index, position) | Store the wrapper object |
Classes covered by this rule¶
Every class under classes/LuaClass/:
- RitnLibEvent
- RitnLibPlayer
- RitnLibSurface
- RitnLibForce
- RitnLibEntity
- RitnLibRecipe
- RitnLibTechnology
- RitnLibGui
- RitnLibInformatron
Exception: RitnLibInventory is a special case — the table it receives as second argument is persistent (that's the whole point). See Delegated persistence for details.