Transport Fever 2

Transport Fever 2

Create your own game world!
Give your game a personal touch and change it to your liking. Create, share and install mods. Customize the game with new landscapes, vehicles, stations, assets and more.
Scripting (postRunFn) How to copy a resource without manually typing the full key list?
Hey, I think I've asked something loosely related to this on a previous project, and I'm bumping into it again...

When I try to dynamically copy resources (tracks or streets especially) in postRunFn using a for-pairs-do loop on the k,v in the table, I get "userdata" errors. It refuses even simple stuff like:
for k, v in pairs(track) do print(k,v) end
without saying nope it's userdata no touchies! 🙄 Yet I can directly ref track.key's easily.

So far I haven't found any examples in API ref or other mods that do this without having to manually type key by key... newTrack.keyx = oldTrack.keyx for dozens of keys 😣 Which is silly and complete error fodder (especially if key lists vary or get updated). And doing so links the copies so changing newTrack.keyx also changes oldTrack.keyx..., which is the whole reason for using a for loop to recursively copy the table by k,v pairs.

Hoping someone can point me to a better way please... 🙏

Thanks!

Edit: BTW print(table.toString( )) or debugprint( ) on these "userdata tables" also doesn't work. I'm guessing this comes from the C++ side of the code.
Last edited by RadiKyle; 3 Mar @ 6:42am
< >
Showing 1-15 of 23 comments
Have you checked out the freestyle station? I do something like that. I think it's in mod.lua.
I did but there's so much code in there I get completely lost, so I didn't find any ideas that I understand how to use.

It appears that the API simply does not permit for-pairs-do loops on these tables, I don't know why 🤷

So I did the same as other mods: copy every key-value pair by explicitly naming it. For track tables it's ridiculous, 70+ hard-coded lines to copy a table 😖
Last edited by RadiKyle; 21 Mar @ 10:07am
eis_os 2 21 Mar @ 9:28am 
No direct useble code, as I mostly flatten the object to a lua table,
but here are some the ingredients, I copied the sample code from several code pieces.

Technical for sol objects you want to get the metatable.

local function tryMetatablePairsAtUserdata(mt) if (mt.pairs ~= nil) then return true end return false end local function tryUserdataObjGet(o, k) return o[k] end Say you have object o: local t = type(o) if ( t == "table" ) then -- "normal" lua table code here elseif ( t == "userdata" ) then -- maybe sol object, get the metatable local metatable = getmetatable(o) if (metatable.__name ~= nil) then -- should have a name local statuspairsallowed, pairsallowed = pcall(tryMetatablePairsAtUserdata, metatable) if (statuspairsallowed and metatable.pairs ~= nil) then for k, v in pairs(o) do newobj[k] = dosomethingwithobj(v) end end elseif(metatable.__members ~= nil) then local members = metatable.__members for i = 1, #members do local key = members
if (key ~= "new") then
local ok, v = pcall(tryUserdataObjGet, o, key)
if (ok) then
newobj[key] = dosomethingwithobj(v)
end
end
end
end
end
end[/code]


CommonAPI2 has a buildin dumper, and will be loaded technical in most lua states:

commonapi.dmp(object) So you can see the technical details
lollus 15 21 Mar @ 10:12am 
local trackFileNames = api.res.trackTypeRep.getAll()
for trackTypeIndex, trackFileName in pairs(trackFileNames) do
local trackType = api.res.trackTypeRep.get(trackTypeIndex)
...
RadiKyle 6 21 Mar @ 10:31am 
Originally posted by lollus:
local trackFileNames = api.res.trackTypeRep.getAll()
for trackTypeIndex, trackFileName in pairs(trackFileNames) do
local trackType = api.res.trackTypeRep.get(trackTypeIndex)
...

Thanks, yes I was just looking at that mod.lua a few minutes ago, but it looks like you did the same thing I did. When making a new track or module from existing ones, you copy each key-value pair explictly line by line. For example:
module.availability.yearFrom = props.yearFrom
module.availability.yearTo = props.yearTo
module.description.icon = props.icon
instead of something like: for k, v in pairs(props) do module[k] = v end, which is what I want to do. But the API rejects that kind of code on these tables.
RadiKyle 6 21 Mar @ 10:38am 
Originally posted by eis_os:
No direct useble code, as I mostly flatten the object to a lua table,
but here are some the ingredients, I copied the sample code from several code pieces.

Technical for sol objects you want to get the metatable.

Thanks, I think there is a lot of new info here that I need to learn...

I did not know that "userdata" is a Lua concept. I thought it was something unique to TpF2. I should have googled it 🤦‍♂️ I see not that userdata is described in the Lua programming manual so I need to study that...

I thought all these api.res.xTypeRep.get() results were already Lua tables, but you are saying they are not. This explains a lot of mysteries for me, because I am trying to use them like Lua tables.

What does "sol object" mean? I googled it but didn't find an answer for this context.

I read about Lua metatables before but I have not used them. I need to study this more. Thanks for the example.
lollus 15 21 Mar @ 10:39am 
You want to loop over the module properties? Not so much of a gain, there are not so many.
RadiKyle 6 21 Mar @ 11:01am 
Originally posted by lollus:
You want to loop over the module properties? Not so much of a gain, there are not so many.

Tracks have 70+ properties... That's 70+ hard-coded lines to do a 1-line copy.

But based on other reply it looks like I misunderstood what api.res.trackTypeRep.get() returns, I thought it was a Lua table, but apparently it's something else and I just need to learn the correct Lua commands to process it.
eis_os 2 21 Mar @ 11:24am 
If you have commonapi2 you can use commonapi.dmp(somevalueorobj) mostly everywhere.

It will tell you the metatable name of the thing.

userdata => a opaque object created from the other side (binary, c interface)
metatable => a table with metafunctions and data that tells lua what to do with a table or object
SOL2 => wraps c++ objects into lua userdata together with some helpers ( https://github.com/ThePhD/sol2 )

The binary game is a compiled C++ binary, most languages uses 0 as first array index. LUA uses 1 as first array index.
SOL2 objects represent a pointer to some c++ object in TPF2 memory.

This means, most api functions will return some kind of object and you won't get simple lua tables.

>> commonapi.dmp(api.res.trackTypeRep.getAll()) { [0] = "high_speed.lua", [1] = "standard.lua", [2] = "eis_os_1000mm.lua", [3] = "eis_os_1000mm_5_5m.lua", [4] = "eis_os_750mm.lua", <metatable> = <__name: sol.std::map<int,std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::less<int>,std::allocator<std::pair<int const ,std::basic_string<char,std::char_traits<char>,std::allocator<char> > > > >> }

Any pointer you received from a map to a the object will get invalid, if you alter the map or vector, like adding something. So special care need to taken.

Good example, it's not possible to hold entity components between game_script handleEvent function calls.
Last edited by eis_os; 21 Mar @ 11:24am
lollus 15 21 Mar @ 12:33pm 
I don't copy the track properties one by one. I don't know what you want to do, somehow I think there is a simple solution if you expand on it :-)
With the metatable, I tried this in the console, it works:
tr = api.res.trackTypeRep.get(0)
mt = getmetatable(tr)
for _, v in pairs(mt.__members) do print(v, tr[v]) end -- print track keys and values
I hate (the abuse of) metatables, but this could be a use case for them.
Last edited by lollus; 21 Mar @ 4:25pm
Ok this is interesting!... I think getting somewhere now...

The original question about looping over a long list of pairs is only part of the topic now... "userdata" has always been a brick wall for me, couldn't even find read access to it, and didn't find any answers when I asked UG. But this thread is showing me that userdata can be accessed if I know the right syntax...

Ok so using the track example, here's its metatable:
>> a = getmetatable(api.res.trackTypeRep.get(0)) >> a { __eq = <function>, __gc = <function>, __index = <function>, __name = "sol.TrackType", __newindex = <function>, __pairs = <function>, __type = { is = <function>, name = "TrackType", }, new = <function>, ["🌲.index"] = <function>, ["🌲.new_index"] = <function>, }

First thing I noted is that __members is not in the list, but I can still access it:
>> a.__members { [1] = "new", [2] = "name", [3] = "desc", [4] = "icon", [5] = "yearFrom", [6] = "yearTo", ....etc

I don't know why __members is "hidden". This is one of the most frustrating things about this API for me. I would not know it exists if @eis_os didn't mention it.

Anyhow using this then I could use code similar to @lollus' example. Note some of the values are also userdata, so maybe some nested ops are required here to access everything.

But what's really catching my eye is this in the metatable: __pairs = <function>

Based on what little I have learned about Lua, that looks like a custom pairs function defined for that table. So I think maybe I can call that __pairs function on the track table from api...get() to loop over its key-value pairs. But I haven't found the correct syntax. For example these don't work:
>> tr = api.res.trackTypeRep.get(0) >> for k,v in a.__pairs(tr) do print(k,v) end error: [string "for k,v in a.__pairs(tr) do print(k,v) end"]:1: sol: cannot call '__pairs/pairs' on type 'sol::as_container_t<TrackType>': it is not recognized as a container stack traceback: =[C](-1): ? =[C](-1): __pairs for k,v in a.__pairs(tr) do print(k,v) end(1): ? >> for k,v in a.__pairs(api.res.trackTypeRep.get(0)) do print(k,v) end error: [string "for k,v in a.__pairs(api.res.trackTypeRep.get(0)) do print(k,v) end"]:1: sol: cannot call '__pairs/pairs' on type 'sol::as_container_t<TrackType>': it is not recognized as a container stack traceback: =[C](-1): ? =[C](-1): __pairs for k,v in a.__pairs(api.res.trackTypeRep.get(0)) do print(k,v) end(1): ?

I realize @lollus' code works, but I am trying to discover what that __pairs meta function does, because it could be more direct access to the data.
Last edited by RadiKyle; 21 Mar @ 5:31pm
I would also like to know.
eis_os 2 22 Mar @ 3:30am 
Technical using metatables is a core lua language feature. CommonAPI uses them for a lot of plain lua code too. (like lazy loading, special sorted maps)

SOL2 glue code is not 100% perfect.
Not all containers even if they look like having pairs will be useable with pairs.

The reason my sample code uses pcall to decide if pairs works, and if not uses members.
You can't be 100% be sure of the behaviour of userdata objects.

And if you doubt my code, look into comonapi2 utils deepcopy2table.

In helperscript.lua of commonapi2 I do create real (deep) copies of objects, but this is limited really for this specific use case and only works with the tested parts for the vehiclescript system.

If you have more questions, you can use transportfever.net as I really don't want to use the steam discussion system.

Don't be afraid to ask, but sometimes the answer is simply more complex then using one simple function. I am sorry...
eis_os 2 22 Mar @ 4:16am 
Some additonal note I couldn't add to my last post.

A TrackType is a c++ object / structure not a iterable container in c++. So it has __members...

Not all members are bound or useable from lua for all objects

__members is something UG uses for console autocompletion.
< >
Showing 1-15 of 23 comments
Per page: 1530 50