data stage "events" for new and modified recipe prototypes
Posted: Mon Jul 22, 2019 5:47 pm
G'day. I was recently politely horrified to see someone catching recipe creation in `data-fixes-final` by setting the metatable of the data.raw recipes table. They then used the __newindex event to catch recipe creation, and take their own actions. That'll work, demonstrably, but it really seemed off.
On the other hand, I was noodling with an idea recently and found myself in the same position: I wanted to catch any change to the ingredients of a recipe, as well as creation, including in the `data-fixes-final` stage, without having to coordinate with other mods.
Which is a subbing an empty object in place, with events like __index, __newindex, and __pairs, so you can intercept reads and writes in your proxy, and forward to/from the real table. Untested, but ... unless the C++ side uses raw get operations, it should work, and without it should at least theoretically be possible to make the horror work.
At the end of days these, and I suspect many uses of `data-fixes-final`, are really about ensuring I can run some code in response to these events. If I could do that it'd be a trivial declaration at the start of `data` and I'd never have to care again, because I know it'll do the right thing.
So: I suggest y'all consider adding events, ike the ones during runtime, in the data phase. If I can do this, I don't need dream of horrible hacks, or petition for a `data-fixes-final-2-ex` phase.
Obvious thing: recursion might happen. I'd suggest breaking the loop after a limit of cycles, and reporting incompatible a mod incompatibility for everything involved. Like, someone responds to my hiding landfill by showing it.
Alternatively: just stop events after the first 20 or something, and let the last one win. This is probably not deterministic without lots of care, though, so refusing to work at all seems smart.
I'd probably *not* treat any reentrancy as a problem, just excessive, because not all reentrance will cycle forever. A huge kindness would be to detect actual change rather than just assignment, and only trigger the next round if something did change.
If I fight over a field with someone, boom, but if I only change a field once each time they change prototype , and they change an unrelated field when I change the prototype, if we assigned the same value it'd terminate the loop quickly and correctly.
The spec or lua wiki have more details about how this works, but the principal is trivial enough: once __metatable is present in the actual metatable, you only get that value back. Without the real metatable table, no adding metatable stuff. Cruel persons might consider tormenting people with __metatable = {}, a fresh table, so it *looks* like you can set methods, but they just don't work.
On the other hand, I was noodling with an idea recently and found myself in the same position: I wanted to catch any change to the ingredients of a recipe, as well as creation, including in the `data-fixes-final` stage, without having to coordinate with other mods.
Which is a subbing an empty object in place, with events like __index, __newindex, and __pairs, so you can intercept reads and writes in your proxy, and forward to/from the real table. Untested, but ... unless the C++ side uses raw get operations, it should work, and without it should at least theoretically be possible to make the horror work.
At the end of days these, and I suspect many uses of `data-fixes-final`, are really about ensuring I can run some code in response to these events. If I could do that it'd be a trivial declaration at the start of `data` and I'd never have to care again, because I know it'll do the right thing.
So: I suggest y'all consider adding events, ike the ones during runtime, in the data phase. If I can do this, I don't need dream of horrible hacks, or petition for a `data-fixes-final-2-ex` phase.
Code: Select all
-- I typed this fresh in the post, so syntax errors everywhere probably
data.on_event({defines.on_recipe_prototype_created, defines.on_recipe_prototype_modified}, function(event)
if event.prototype.name == "landfill" then
-- obviously contrived example is obviously contrived
event.prototype.hidden = true
end
end)
Alternatively: just stop events after the first 20 or something, and let the last one win. This is probably not deterministic without lots of care, though, so refusing to work at all seems smart.
I'd probably *not* treat any reentrancy as a problem, just excessive, because not all reentrance will cycle forever. A huge kindness would be to detect actual change rather than just assignment, and only trigger the next round if something did change.
If I fight over a field with someone, boom, but if I only change a field once each time they change prototype , and they change an unrelated field when I change the prototype, if we assigned the same value it'd terminate the loop quickly and correctly.
Alternative: ban metatable stuff
The following will make it impossible to mutate the metatable of a table every again, completely forbidding anyone from doing this. Which is the other reasonable response to the situation, I think. Once you make that setmetatable call (from C or lua) it becomes impossible to get the metatable, because you get the value of __metatable instead from getmetatable.The spec or lua wiki have more details about how this works, but the principal is trivial enough: once __metatable is present in the actual metatable, you only get that value back. Without the real metatable table, no adding metatable stuff. Cruel persons might consider tormenting people with __metatable = {}, a fresh table, so it *looks* like you can set methods, but they just don't work.
Code: Select all
local so_controversial_yet_so_brave= { __metatable = "this is too awful to live" }
data.raw = setmetatable({}, so_controversial_yet_so_brave)
data.raw.armor = setmetatable({}, so_controversial_yet_so_brave)
-- etc, etc