Isaac wrote:The LoG script interface is not fully compliant 5.1 Lua.
Yes it is, as I've told you before. Grimrock uses (presumably) unmodified LuaJIT 2.0.0-beta9.
The short answer is: don't try to assign to ScriptComponents like that. It won't do what you want.
Indexing them is fine, but don't try to assign to them. If you want to alter a ScriptComponent's environment from somewhere else, use a setter like Isaac suggested.
Now, I'm aware that's not a real answer to your question. So if you want the long answer, keep reading. But I'm not kidding when I say it's the long answer, and you will need a fair amount of pre-existing Lua knowledge to understand it.
To understand what's going on here you need a rudimentary understanding of
metatables and metamethods. We're really only concerned about two specific metamethods here:
__index, which overrides the behaviour for indexing a table, and
__newindex, which overrides the behaviour for assigning to a table.
Components, GameObjects, and almost everything else you'll encounter are just tables, but they have metatables that give the illusion that there's something more. If you try to iterate over a Component or GameObject using
pairs, you'll find that it appears to have no fields. That's because it really doesn't have any fields! But it does have a metatable with an __index method. That __index method is what allows you to use "party.party:getChampion(1)". party.party doesn't have a field called getChampion, so it falls through to the __index method, which returns the getChampion method.
This is great because it lets you have dedicated methods without having to reference every single one in every single Component. If you have 5000 ModelComponents, you don't need to store a reference to disable(), enable(), setStaticShadow(), etc. in every ModelComponent - you just need to store a reference to the metatable in every ModelComponent. Storing one reference per ModelComponent is a lot cheaper than storing 31.
The metatable for the Component also has a __newindex method, and this one is less intuitive. Look at this code:
Code: Select all
party.party.goatString = "goat"
for k,v in pairs(party.party) do print(k,v) end
print(party.party.goatString)
This would give you the result:
According to
pairs the table still doesn't have anything in it, but the second print statement clearly shows that goatString is there.
This is because the __newindex method stopped the assignment to the actual party.party table, and instead assigned the value to a separate, hidden "user properties" table. When the __index method sees you're trying to get whatever is at the index "goatString", it first checks if the Component has a goatString() method (it doesn't) and then returns the user property with the key "goatString". It checks for the method first, so you can't overwrite methods:
will appear to have no effect (it actually assigns a user property that you'll never be able to read back).
Nobody actually uses this user property functionality because 1. it's undocumented and 2. it doesn't provide any power that a variable in a ScriptComponent does not (it's not even more efficient to serialize). But it exists, and you need to know about it to really understand what's going on here.
Those __index and __newindex methods do a couple more things: throwing errors to prevent you from blowing things up. If the "Component" you're indexing no longer corresponds to a real Component (i.e. it's been destroyed and you're using a leftover reference to its table), it'll give you an error ("bad object"). If the key you're assigning to is not a string, it'll give you an error ("bad user property key"). If the value you're assigning to the key is not a string, number, or boolean, it'll give you an error ("bad user property value"). If the key you're assigning to is important for some other reason, it'll give you an error ("attempt to modify a read only property").
When you run "script_entity_1.script.array = {}", you're trying to assign a table as a user property value. That's not a string, number, or boolean, so you get an error.
So why does "array = {}" in script_entity_1 work? Because ScriptComponent is special. Every ScriptComponent has an environment table, which is
not the ScriptComponent itself; the __index method just makes it
seem like it is. When you ask for script_entity_1.script.array, you actually get script_entity_1.script.environment_table.array. But if you try to
assign to script_entity_1.script.array, you don't go through the __index method, you go through the __newindex method - which is the same as the __newindex method for other Components, so it goes to the user properties table, not the ScriptComponent environment table.
That ScriptComponent environment table is the fenv for all functions defined in the ScriptComponent, including the main function (i.e. the entire script). So any global index/assignment doesn't go to the global environment, it goes to that ScriptComponent's environment table. "array = {}" is really like "environment_table.array = {}".
This environment table has a metatable with an __index method too. That metamethod gives you access to Config, DamageFlags, vec, etc. without needing to copy references to them to every environment table. This metamethod is also why "script_entity_1.script.array = {}" doesn't error with "attempt to index a nil value" even though script_entity_1 isn't an actual variable in the environment: the __index metamethod returns the result of findEntity(key). So the code is functionally equivalent to "findEntity('script_entity_1').script.array = {}".
Important: using "self" in a ScriptComponent script will give you the ScriptComponent,
not the environment table. I am not aware of any way to obtain a direct reference to the environment table from the user scripting interface. This makes writing a "general setter" function very unpleasant.
Finally, this is the source of a common serialization bug. Functions in Lua carry a reference to their environment with them. It doesn't matter where the function is, it will keep its environment until changed with setfenv(). When Grimrock serializes functions, it doesn't serialize that environment reference. It just serializes the function, and when the saved game gets loaded later, Grimrock assumes that the ScriptComponent containing the function must be the ScriptComponent with the right environment for the function. So if you define a function inside script_entity_1, then move it to script_entity_2, it will keep having script_entity_1's environment until the game is saved and loaded, after which it will have script_entity_2's. Be careful about passing functions around.