Custom item tracker (lua)

I’m having trouble adding a custom item tracker in lua. I haven’t gotten to the part where I can actually test it yet, because I can’t find the right time to add it to the stonehearth.inventory service/controller. I can’t add it immediately on 'radiant:required_loading' because that happens as soon as the main menu loads but before the inventory controller is instantiated (because a game hasn’t been loaded yet). Is there an event I can listen for that would be the right time to add it, or is there a better way to do this?

Well, I decided to just monkey patch it for now, which is working.

Now on to the next problem: I get an error saying my function is nil when I try to call the original (saved) version of it. I’m doing this exactly the same as SmartCrafter does it. Are “libs” treated differently than “controllers” or “services” for this sort of thing? I don’t understand how my function can get called by the game calling use_consumable and then have the original use_consumable be nil.

local Consumables = radiant.mods.require('stonehearth.ai.lib.consumables_lib')
local MyConsumables = class()

MyConsumables._ot_old_use_consumable = Consumables.use_consumable

function MyConsumables:use_consumable(consumable, user, target_entity)
    local used = self:_ot_old_use_consumable(consumable, user, target_entity)

    if used then
		radiant.events.trigger(self, 'stonehearth:consumable:used', consumable)
	end
end

return MyConsumables

The problem is that you’re patching the class, but self might be an (earlier) instance that does not have that method set. I wouldn’t do it like that, the safer way is to save the old func in a local, and then call local ret = { old_func(self, consumable, user, target_entity) }. I’ve recently had a post about monkey patching where I did pretty much that.

Actually, nevermind. You are calling a method of another class that is not an ancestor of your class and it borks. That’s a big no-go. Is Consumables.use_consumable by chance using any local, or self-based field? Those are not available to you within MyConsumables, because that one’s pretty empty. What you’re doing also isn’t monkey patching, you’re doing some kind of facade/proxy.

However, I don’t think this is an appropriate case for a monkey patch. If it’s possible to add new inventory controllers (i.e. if the game’s been designed for that), then I’d search for a hook that’s executed upon map creation, or game loading. I’m sure there’s some kind of hook for it. If you need to have the modification done before any inventories or so are created, then we’re in weirdo territory again of course, where things get complicated (especially with savegames).

I didn’t list the monkey patching part, since it’s the exact same as I’ve done in the past:

tracker_init = {}

function tracker_init:_on_required_loaded()
	local myInventory = require('inventory')
	local inventory = radiant.mods.require('stonehearth.services.server.inventory.inventory')
	radiant.mixin(inventory, myInventory)
   
	local myConsumables = require('consumables_lib')
	local consumables = radiant.mods.require('stonehearth.ai.lib.consumables_lib')
	radiant.mixin(consumables, myConsumables)
end

radiant.events.listen_once(radiant, 'radiant:required_loaded', tracker_init, tracker_init._on_required_loaded)

return tracker_init

I’m doing virtually the exact same thing that Smart Crafter does to override the CraftOrderList:add_order function. From what I understand of your comment, the reason it doesn’t work for ConsumablesLib (and the main significant difference between their usages, as far as I can tell) is that consumables_lib.lua has two locals defined outside of that function that then get used in that function:

local entity_forms_lib = require 'stonehearth.lib.entity_forms.entity_forms_lib'
local SCRIPTS_CACHE = {}

So basically I’d have to copy and override the entire function if I wanted to put my event trigger in there, since I don’t have access to those locals.

Edit:
I would love to hook into the right event, I just don’t know what it would be or really where to look for it, which was the feedback I was hoping for from my first post. But I also wanted to test out other aspects of my mod, and I was already using the monkey patch for the consumables part, it just wasn’t reaching that part because my attempt to add an inventory tracker without monkey patching was failing.

Also, I understand if accessing those locals outside of the consumables function is something I can’t do and produces an error. What I’m confused about is why the error comes in the form it does, suggesting that my old_use_consumable method itself is nil:

release-869 (x64)[M]
obligationtracker/consumables_lib.lua:7: attempt to call method 'old_use_consumable' (a nil value)
stack traceback:
    [C]: in function 'old_use_consumable'
    obligationtracker/consumables_lib.lua:7: in function 'use_consumable'
    stonehearth/call_handlers/town_call_handler.lua:10: in function <stonehearth/call_handlers/town_call_handler.lua:6>

Edit2:
Ignore the different function name for it. Obviously when I changed it, I changed it in both places at the same time. That was an attempt to name it something else in case old_use_consumable was somehow getting set to nil somewhere else.

Nope, that can’t be right. Because monkey patching the inventory service to override Inventory:_pre_activate() to add my own item tracker worked fine, and I did that in exactly the same way, and the original Inventory:_pre_activate() function contains the line SvTable.lock_field(self._sv, '_container_for'), referencing the local SvTable which is declared outside the function.

I’m trying a different strategy now, which I think is worse in some ways but might actually work: I’m trying to override the alias "consumables:scripts:buff_town" in my manifest to point to my own script that will trigger the event.

Alright, I’ve made a lot of progress. Now when my Ember view is initialized, I call a function “add_item_trackers” that I set up a lua call handler for. This call handler accesses the player’s inventory, checks if those item trackers exist, and if not it adds them. Then in the “.done(…)” deferred function, I get the item trackers into my Ember view. That all works!

Additionally, overriding the buff_town script with my own also works, and I’m triggering an event in that function. This gets rid of all the monkey patching.

However, my listening for the event never happens. Here’s what I’m doing to trigger it in my “scripts/ot_buff_town.lua” file:

local Obligations = radiant.mods.require('obligation_controller')
radiant.events.trigger(Obligations, 'stonehearth:consumable:used', consumable_data)

Then in my “services/obligation_controller.lua” script (which is listed as a controller in my manifest: "obligation_controller": "file(services/obligation_controller.lua)"), I try to listen for that event like this:

radiant.events.listen(self, 'stonehearth:consumable:used', self, self._on_consumable_used)

I’m guessing that I’m not using a good object for triggering the event? But the events I’ve seen always trigger on self, and I’m not sure how to listen to that random script’s events, so I tried to shift “where” the event was triggering from.

You can trigger events on anything. As long as you trigger and listen on the same object.

Thanks! Okay, after a little more testing it looks like that local Obligations object is just nil. So the issue isn’t the event triggering, it’s that I’m having trouble getting a reference to my controller. Do I have to account for the path to the file I want, or the path to the file I’m currently in?

Edit: Actually, at this point events are pointless because I could just straight up call the function I want. Can I listen for events “on” a nil object? Could I do radiant.events.listen(nil, ...)?

Not on nil, but you can always listen/trigger on radiant. Just make sure your event has a unique enough name.

1 Like

Hmm, I tried doing it on both radiant and stonehearth to no avail. Maybe it can’t call my handler because of the arguments I’m trying to pass? I assume I can pass arguments with an event?

You can pass up to 2 arguments with events, though I would suggest one table instead if you need to pass multiple values. Hard to tell why it might not be working in your particular case without seeing the full code. I suppose check that both are on the same side (client/server) and that the listen happens before the trigger?

Thanks! Okay, the problem is probably that my :initialize() function isn’t getting called. I thought that automatically got called when a controller was set up, and I also tried :__init(). Am I supposed to manually call such a function like this in any controller I make?

radiant.events.listen_once(radiant, 'radiant:required_loaded', ObligationController, ObligationController.initialize)

Are you creating the controller using radiant.create_controller()?

Well that’s silly, Max, why would I want to actually create my controller instead of just skipping that step and assuming that Stonehearth would magically do it for me? :man_facepalming: Thanks for the help, I’ll continue to post my progress. :stuck_out_tongue:

Okay, it turns out I’m still confused. I guess I was imagining my controller as a service. I’m having trouble finding an example of how to start my service/controller that doesn’t look overkill. I was imagining I could just do it in my “server_init_script”, but at this point I’m kind of trying random things and could use some more guidance.

A controller is something that you create many instances of and that gets saved/remoted automatically. A service is a singleton. The use cases of the two are entirely different.

Ahh, I definitely want a service. I want something that maintains a datastore object (which I’m assuming I can use similar to the clock_widget to send updates to my view) and listens to a couple events to update that datastore object.

Yes, sounds like you want to create a service, store/remote data on it, and listen/trigger events on it. I would look at other mods to see how they create services. It’s pretty straightforward. For example, I think Glassworks adds a new service.

1 Like

Great, thanks!