This issue is raised for the purpose of discussing how the Trains
module should work in conjunction with the Entity
module. I'll summarise the problem below and pose a potential solution. Sorry it's such a long post, I just wanted to document all the things I've already looked at.
As it stands, the Entity
module is designed to work with LuaEntity
objects (or instances of any class that extends it). More generally though, it will work with any table with name
and valid
keys. This absolutely fine in the vast majority of scenarios, but trains are a special case.
Trains are instances of LuaTrain
which doesn't extend LuaEntity
, nor does it contain the right fields to be treated as such. This means we can't directly save data about a train; rather modders would save data against the lead locomotive. Whilst this would work, and the on_train_id_changed
event would allow them to perform the necessary book keeping, I think it's unintuitive, tedious and likely prone to error.
Instead, we should enable modders to directly save data about trains.
An obvious solution .. that doesn't quite work
The quickest and most obvious sounding solution is provide a duck type for LuaEntity
that enables LuaTrain
to be used with the Entity
module
-- stdlib/trains/trains.lua
function Trains.to_entity(train)
return {
name = "train-"..Trains.get_train_id(train)
valid = train.valid
}
end
-- control.lua
Entity.set_data(Trains.to_entity(a_train), data)
However this doesn't work at first blush, because the Entity
module compares the entity given as the first argument with the one in the global data by reference.
-- stdlib/entity/entity.lua#L56
if entity_data.entity == entity then
return entity_data.data
end
This appears to be fine when handling LuaEntity
references; I guess this is because the game can properly (de)serialize those and always returns the same references from global data and surface queries. but I don't know for sure.
However, the Trains.to_entity
method above always returns a new table, so the references will never match. To get around that, we could modify Trains._registry
and other Trains
internals to keep track of the faux-entities and have to_entity
return that reference.
for _, trainInfo in pairs(all_trains) do
registry[tonumber(trainInfo.id)] = {
train = trainInfo.train,
entity = {
name = "train-"..trainInfo.id
valid = trainInfo.train.valid
}
}
end
However that approach falls down in the following scenario:
- I have locomotive 1000, locomotive 2000 and wagon 1
- I connect locomotive 1000 and locomotive 2000 to either end of wagon 1 to form an L-C-L train called train-1000
- I store data about train-1000
- I disconnect locomotive 1000 from train-1000, and the train id changes to train-2000.
- I reconnect locomotive 1000 to train-1000, thus re-creating train-1000. In the mean time, train-1000 was dropped from the registry and the entity reference lost. Thus, the train-1000 data is inaccessible.
To this end, I don't think comparing by reference will ever suit the Trains
module
Field-wise equality
In reality, as far as the Trains
module is concerned, two tables with the same name
value are equal and referring to the same train. To that end, I would propose making a slight change to the Entity
module to have it call an equals
method on the input entity if there is such a method and the entity references don't match.
-- for brevity's sake, checking the function exists is omitted
if ((entity_data.entity == entity) or (entity.equals(entity_data.entity)) then
return entity_data.data
end
This would preserve the behaviour of the Entity
module, but crucially allow a simple integration with the Trains
module, eg
function Trains.to_entity(train)
local self = {
name = "train-"..Trains.get_train_id(train),
valid = train.valid,
}
-- perhaps creating a new function for each call isn't
-- sensible, bit here it is for clarity
self.equals = function(ent)
return ent.name == self.name
end
return self
end
Entity.set_data(Trains.to_entity(train), { working = true })
data = Entity.get_data(Trains.to_entity(train))
-- { working = true }
Of course, I think no matter what solution we choose, there will need to be some internal book keeping to shuffle stored data about when a train's id changes.