Bad User Property Value

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
Post Reply
User avatar
Zo Kath Ra
Posts: 931
Joined: Sat Apr 21, 2012 9:57 am
Location: Germany

Bad User Property Value

Post by Zo Kath Ra »

script_entity_1

Code: Select all

array = {}
script_entity_2

Code: Select all

function test()
	script_entity_1.script.array[#script_entity_1.script.array + 1] = 1
	print(script_entity_1.script.array[#script_entity_1.script.array])
	script_entity_1.script.array = {}
end
Stepping on a pressure plate calls test()
The line "script_entity_1.script.array = {}" produces the error "bad user property value".
What does this error mean?

Everything works fine if I put the test() function in script_entity_1 and change it like this:

Code: Select all

function test()
	array[#array + 1] = 1
	print(array[#array])
	array = {}
end
User avatar
Isaac
Posts: 3172
Joined: Fri Mar 02, 2012 10:02 pm

Re: Bad User Property Value

Post by Isaac »

You should use a setter function in script_entity_1.

Code: Select all

--script_entity_1
array = {}

function resetArray()
	array = {}
end

Code: Select all

--script_entity_2
function test()
   script_entity_1.script.array[#script_entity_1.script.array + 1] = 1
   print(script_entity_1.script.array[#script_entity_1.script.array])
   script_entity_1.script:resetArray()
end
User avatar
Zo Kath Ra
Posts: 931
Joined: Sat Apr 21, 2012 9:57 am
Location: Germany

Re: Bad User Property Value

Post by Zo Kath Ra »

Isaac wrote:You should use a setter function in script_entity_1.

Code: Select all

--script_entity_1
array = {}

function resetArray()
	array = {}
end

Code: Select all

--script_entity_2
function test()
   script_entity_1.script.array[#script_entity_1.script.array + 1] = 1
   print(script_entity_1.script.array[#script_entity_1.script.array])
   script_entity_1.script:resetArray()
end
Thanks, that works.
But why can't you reset the array directly from a different script entity?
You can add elements, so the array and its reference are accessible from outside the script entity it's defined in.
User avatar
Isaac
Posts: 3172
Joined: Fri Mar 02, 2012 10:02 pm

Re: Bad User Property Value

Post by Isaac »

That's a question for minmay, or Petri. The LoG script interface is not fully compliant 5.1 Lua.

Afaik (in the LoG games) the interface between different user scripts is read only, except for parameters given to a function.
minmay
Posts: 2768
Joined: Mon Sep 23, 2013 2:24 am

Re: Bad User Property Value

Post by minmay »

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:

Code: Select all

goat
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:

Code: Select all

party.party.getChampion = "goat"
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.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
User avatar
Isaac
Posts: 3172
Joined: Fri Mar 02, 2012 10:02 pm

Re: Bad User Property Value

Post by Isaac »

Thank you for this very in depth explanation. 8-) 8-)
minmay wrote:
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.
Ah ha... I had been thinking of the apparent lack of global variables in user scripts—afaik, for thinking it only partially compliant.
Post Reply