Jump to content

[SOLVED] How to add a "use" option to an item?


Recommended Posts

Hello,
I would like to know how to allow an item in the player's inventory to be interactable. For example, 'using' this item would give the player 1 flint.
I searched through the code that is already in DST but they seem to apply to certain situations only, like healing. I can't find a universal 'use' component.
Any help would be appreciated!

Edited by BombardmentPigs
Link to comment
Share on other sites

If you can't find an universal component, then you can make your own or add actions to an existing component. The closest thing to an universal component for this is "useableitem", but by default it only works when the item in question is equipped. You could still add onto it with AddComponentAction, just make sure your custom behavior won't conflict with existing items (a custom tag is a good way to do it).

Link to comment
Share on other sites

22 hours ago, ariadnesGambit said:

If you can't find an universal component, then you can make your own or add actions to an existing component. The closest thing to an universal component for this is "useableitem", but by default it only works when the item in question is equipped. You could still add onto it with AddComponentAction, just make sure your custom behavior won't conflict with existing items (a custom tag is a good way to do it).

Thanks, although this item cannot be equipped, so I think I'll try making a custom action. I'm pretty sure there's a guide for this out there.

Link to comment
Share on other sites

Adding component is a standard way of adding action.

I've written a universal component that adds any action anyway.

Spoiler
--Usage: in prefab or PrefabPostInit, both server and client (so this component goes before TheWorld.ismastersim)
--to avoid namespace collision, please rename "anyaction" to your custom component name
--inst:AddComponent("anyaction")
--inst.components.anyaction:RegisterStaticAction("inventory",ACTIONS.EAT)
--inst.components.anyaction:RegisterStaticAction("inventory","EAT")
--inst.components.anyaction:RegisterDynamicAction("inventory",function(inst,doer,actions,right)
--  if true or false then
--    table.insert(actions,ACTIONS.EAT)
--  end
--end)
local AnyAction = Class(function(self, inst)
  self.inst = inst
  self.staticactions = {scene = {}, useitem = {}, inventory = {}, point = {}, equip = {}}
  self.dynamicactions = {scene = {}, useitem = {}, inventory = {}, point = {}, equip = {}}
end)
function AnyAction:CollectSceneActions(inst, doer, actions, right)
  for b, c in ipairs(self.staticactions.scene) do table.insert(actions, c) end
  for b, c in ipairs(self.dynamicactions.scene) do c(inst, doer, actions, right) end
end

function AnyAction:CollectUseItemActions(inst, doer, target, actions, right)
  for b, c in ipairs(self.staticactions.useitem) do table.insert(actions, c) end
  for b, c in ipairs(self.dynamicactions.useitem) do c(inst, doer, target, actions, right) end
end

function AnyAction:CollectInventoryActions(inst, doer, actions, right)
  for b, c in ipairs(self.staticactions.inventory) do table.insert(actions, c) end
  for b, c in ipairs(self.dynamicactions.inventory) do c(inst, doer, actions, right) end
end

function AnyAction:CollectPointActions(inst, doer, pos, actions, right, target)
  for b, c in ipairs(self.staticactions.point) do table.insert(actions, c) end
  for b, c in ipairs(self.dynamicactions.point) do c(inst, doer, pos, actions, right, target) end
end

function AnyAction:CollectEquipActions(inst, doer, target, actions, right)
  for b, c in ipairs(self.staticactions.equip) do table.insert(actions, c) end
  for b, c in ipairs(self.dynamicactions.equip) do c(inst, doer, target, actions, right) end
end

function AnyAction:RegisterStaticAction(actype, act)
  if type(act) == "string" then act = ACTIONS[act] end
  if not actype or not act then return end
  table.insert(self.staticactions[actype:lower()], act)
end

function AnyAction:RegisterDynamicAction(actype, fn)
  if not actype or not fn then return end
  table.insert(self.dynamicactions[actype:lower()], fn)
end
local inited = false
--please make sure you do in your modmain.lua, run the following code. You can just copy and paste the Init chunk there. If you are lazy then do
--local anyaction_init=require("components/anyaction").init
--GLOBAL.setfenv(anyaction_init,env)
--anyaction_init()
function AnyAction:Init()
  if inited then return end
  inited = true
  local function scene(inst, doer, actions, right)
    if inst and inst.components.anyaction then
      inst.components.anyaction:CollectSceneActions(inst, doer, actions, right)
    end
  end
  AddComponentAction('SCENE', "anyaction", scene)

  local function useitem(inst, doer, target, actions, right)
    if inst and inst.components.anyaction then
      inst.components.anyaction:CollectUseItemActions(inst, doer, target, actions, right)
    end
  end
  AddComponentAction('USEITEM', "anyaction", useitem)

  local function inventory(inst, doer, actions, right)
    if inst and inst.components.anyaction then
      inst.components.anyaction:CollectInventoryActions(inst, doer, actions, right)
    end
  end
  AddComponentAction('INVENTORY', "anyaction", inventory)

  local function point(inst, doer, pos, actions, right, target)
    if inst and inst.components.anyaction then
      inst.components.anyaction:CollectPointActions(inst, doer, pos, actions, right, target)
    end
  end
  AddComponentAction('POINT', "anyaction", point)

  local function equip(inst, doer, target, actions, right)
    if inst and inst.components.anyaction then
      inst.components.anyaction:CollectEquipActions(inst, doer, target, actions, right)
    end
  end
  AddComponentAction('EQUIPPED', "anyaction", equip)
end

return AnyAction

It is easy to write some component like

return Class(function(self, inst)
  self.inst = inst
end)

let's say it is a component_anyaction.lua, and you call AddComponentAction(see my universal component).

AddComponentAction("component_anyaction","INVENTORY",function((something),actions,(others))
    table.insert(actions,ACTIONS.ANYACTION)
    end)

It is easy to write some action like

local act = Action({mount_valid = true})
act.id = "ANYACTION"
act.name = act.id
act.priority = 3
act.distance = 10
act.instant=true
act.fn = function(act) return true end
AddAction(act)
STRINGS.ACTIONS[act.name:upper()] = "name of ANYACTION"

and if you play animation, act.instant=false

AddStategraphPostInit("wilson",function(sg)
  sg.actionhandlers[ACTIONS.ANYACTION] = ActionHandler(ACTIONS.ANYACTION, "doshortaction")
end)
AddStategraphPostInit("wilson_client",function(sg)
  sg.actionhandlers[ACTIONS.ANYACTION] = ActionHandler(ACTIONS.ANYACTION, "doshortaction")
end)

 

Edited by Rickzzs
Link to comment
Share on other sites

Ok, after a while I have tried to add a component action, along with an action as well.
I'm still not sure if I need an entirely new component (I hope I don't), but here is the code so far, which doesn't work.

GLOBAL.ACTIONS.ACQUIREFUEL.fn = function(act)
	repeat
		act.doer.components.inventory:GiveItem(SpawnPrefab("aligniumfuel"))
		act.invobject.components.fueled:DoDelta(-120)
	until act.invobject.components.fueled.currentfuel == 0
	return true
end

local function acquirefuel(inst, doer, actions, right)
    if right and doer:HasTag("player") and inst.components.fueled and inst.components.fueled.currentfuel > 0 and inst:HasTag("aligniumstorage") then
		table.insert(actions, GLOBAL.ACTIONS.ACQUIREFUEL)
    end
end

AddComponentAction("INVENTORY", "fueled", acquirefuel)

Basically, I'm trying to create an action where the player can "use" this item to give the player a certain number of another item (aligniumfuel), depending on the fuel of the "used" item. (Each 120 units of fuel = 1 item)

It gives the error of this inside the server log:
 

Quote

[00:00:00]: [string "../mods/Alignium mod test/modmain.lua"]:115: attempt to index field 'ACQUIREFUEL' (a nil value)
LUA ERROR stack traceback:
        ../mods/Alignium mod test/modmain.lua(115,1) in main chunk
        =[C] in function 'xpcall'
        scripts/util.lua(780,1) in function 'RunInEnvironment'
        scripts/mods.lua(574,1) in function 'InitializeModMain'
        scripts/mods.lua(548,1) in function 'LoadMods'
        scripts/main.lua(376,1) in function 'ModSafeStartup'
        scripts/main.lua(506,1) in function 'callback'
        scripts/modindex.lua(103,1) in function 'BeginStartupSequence'
        scripts/main.lua(505,1) in function 'callback'
        scripts/modindex.lua(735,1)
        =[C] in function 'GetPersistentString'
        scripts/modindex.lua(709,1) in function 'Load'
        scripts/main.lua(504,1) in main chunk



Any assistance would be appreciated!

Link to comment
Share on other sites

You need to create an action through AddAction, you can't reference an action that doesn't exist yet. Something like this:

AddAction("ACQUIREFUEL", "Acquire Fuel", function(act)
    -- Your code here
end)

The second argument, the one I set as "Acquire Fuel", is the string that shows for performing the action. Set it to whatever you'd like!

Link to comment
Share on other sites

Ok so this is the code I have currently inside my modmain, but what do I add to my prefab to make it work?

AddComponentAction("INVENTORY", "fueled", acquirefuel)

AddAction("ACQUIREFUEL", "Extract Fuel from Stabilizer", function(act)
	repeat
		act.doer.components.inventory:GiveItem(SpawnPrefab("aligniumfuel"))
		act.invobject.components.fueled:DoDelta(-120)
	until act.invobject.components.fueled.currentfuel == 0
	return true
end)

local function acquirefuel(inst, doer, actions, right)
    if right and doer:HasTag("player") and inst.components.fueled and inst.components.fueled.currentfuel > 0 and inst:HasTag("aligniumstorage") then
		table.insert(actions, GLOBAL.ACTIONS.ACQUIREFUEL)
    end
end

Thanks.

Link to comment
Share on other sites

34 minutes ago, BombardmentPigs said:

but what do I add to my prefab to make it work?

You're using AddComponentAction, so it just needs the component "fueled" and to pass the checks you have, which it looks like it needs the "aligniumstorage" tag. I assume your prefab has both of them, but the action still isn't working? Then it must be because the player doesn't know what state to enter to do the action.

You can either make the action instant, like this:

GLOBAL.ACTIONS.ACQUIREFUEL.instant = true

Or you need to use AddStategraphActionHandler too, which will basically tell the player what state to enter when doing the action. To start, I recommend using something like "dolongaction", it's used by a ton of other actions (like crafting), it's the one where the character does the "swirly hands" thing. But you can get fancier if you wish.

Add something like this to your modmain:

AddStategraphActionHandler("wilson", GLOBAL.ActionHandler(GLOBAL.ACTIONS.ACQUIREFUEL, "dolongaction"))
AddStategraphActionHandler("wilson_client", GLOBAL.ActionHandler(GLOBAL.ACTIONS.ACQUIREFUEL, "dolongaction"))

 

Edited by ClumsyPenny
Link to comment
Share on other sites

5 hours ago, BombardmentPigs said:

When I hover over the only action is right click to examine.
When I hold alt It's left click inspect, right click examine.

Then it means something about AddComponentAction isn't working. Could you send the whole mod so I can look into it?

To recap about actions:

- AddAction creates the action itself and its code runs server side once the action is called, typically through the stategraph with inst:PerformBufferedAction(). If its function returns nothing or false, the player will say "I can't do that."

- AddComponentAction runs client side too and it's for what actions are available to perform on an object in the world, your inventory, etc. This depends on what type of action you use between SCENE, USEITEM, POINT, EQUIPPED, INVENTORY. If your action prompt doesn't show up, then this one isn't working properly.

- AddStategraphActionHandler is to tell the player what state to enter to perform the action, then the state will call inst:PerformBufferedAction() at a certain frame. If your action prompt shows up and nothing happens when performing it, then this one isn't working properly.

The bare minimum you need for an action is just AddAction, but in most cases you'll need all 3.

Link to comment
Share on other sites

14 hours ago, ClumsyPenny said:

Could you send the whole mod so I can look into it?

Alright, here is the whole mod, though I assume you probably only need the modmain and the aligniumstorage prefab?
The mod is a bit of a mess as it is a WIP, sorry.
Thanks for helping me so far, it's been very useful.

(Mod zip file removed since problem solved)

 

Edited by BombardmentPigs
Link to comment
Share on other sites

1 hour ago, BombardmentPigs said:

Alright, here is the whole mod

Okay, after some testing, I have figured out that there actually were four issues:

  • Your "acquirefuel" function needs to be declared before you use it in AddComponentAction, otherwise the code won't know what it is. You technically could just put it inside AddComponentAction without a name like you did for the AddAction function, but that's up to how readable the code feels to you, it's a very subjective thing.
  • The "fueled" component doesn't exist on clients and it doesn't have a replica, so you need to detect its fuel amount in a different way. Thankfully Klei applies tags to items through components for this exact purpose, all you need to do is check the "fueldepleted" tag to know if it's out of fuel.
  • For some reason the component action INVENTORY never has the "right" parameter set to true, it's always nil. I checked through Klei's INVENTORY actions and they rarely if ever use "right". Turns out that INVENTORY actions default to being right click, so in this specific case you don't need it. You learn something new every day, I had no idea!
  • SpawnPrefab needs GLOBAL. before it, or it will crash.

All of that being said, here's the new reworked code:

local function acquirefuel(inst, doer, actions)
    if doer:HasTag("player") and not inst:HasTag("fueldepleted") and inst:HasTag("aligniumstorage") then
		table.insert(actions, GLOBAL.ACTIONS.ACQUIREFUEL)
    end
end

AddComponentAction("INVENTORY", "fueled", acquirefuel)

AddAction("ACQUIREFUEL", "Extract Fuel from Stabilizer", function(act)
	repeat
		act.doer.components.inventory:GiveItem(GLOBAL.SpawnPrefab("aligniumfuel"))
		act.invobject.components.fueled:DoDelta(-120)
	until act.invobject.components.fueled.currentfuel == 0
	return true
end)

AddStategraphActionHandler("wilson", GLOBAL.ActionHandler(GLOBAL.ACTIONS.ACQUIREFUEL, "dolongaction"))
AddStategraphActionHandler("wilson_client", GLOBAL.ActionHandler(GLOBAL.ACTIONS.ACQUIREFUEL, "dolongaction"))

Lastly, I found an issue that isn't on your side, just an unfortunate way Klei coded things. Your Alignium Fuel Stabilizer will reset to 0% on saving and loading if it was 100% previously, because Klei only saves the amount if the current fuel is different than the max fuel. Basically they assume that fueled items get initialized to be full. Kind of a weird assumption given mods exist... Anyway, it's not impossible to fix, just letting you know I noticed the issue while testing.

1 hour ago, BombardmentPigs said:

The mod is a bit of a mess as it is a WIP, sorry.

Haha, don't worry, I'm used to it. I can read through it just fine.

1 hour ago, BombardmentPigs said:

Thanks for helping me so far, it's been very useful.

You're welcome! I really enjoy helping people with modding problems, it's a nice break from dealing with my own modding problems, hehe!

Edited by ClumsyPenny
  • Thanks 1
Link to comment
Share on other sites

11 hours ago, ClumsyPenny said:

Okay, after some testing, I have figured out that there actually were four issues:

Yikes! I didn't know there were so many problems! :shock:
Thanks so much for solving all of them, it now works exactly as I planned!

12 hours ago, ClumsyPenny said:

Lastly, I found an issue that isn't on your side, just an unfortunate way Klei coded things. Your Alignium Fuel Stabilizer will reset to 0% on saving and loading if it was 100% previously, because Klei only saves the amount if the current fuel is different than the max fuel. Basically they assume that fueled items get initialized to be full. Kind of a weird assumption given mods exist... Anyway, it's not impossible to fix, just letting you know I noticed the issue while testing.

About that issue, I found a way to solve it.
Since each piece of fuel adds 120 units, and the former maximum storage was 120x5 or 600 units, I just made the new maximum storage 1 more unit (601 units), so that when it stores 5 pieces of fuel, it won't reach full capacity, and so the problem won't occur.
To prevent the player from adding the 6th piece of fuel to reach max capacity, I put this in the MainFunction:

    inst:ListenForEvent("percentusedchange", OnFuelChanged)

And then I created a new function that prevents it from accepting fuel once it reaches 600 units:

local function OnFuelChanged(inst, data)
    if inst.components.fueled.currentfuel < 120*5 then
		inst.components.fueled.accepting = true
    else
		inst.components.fueled.accepting = false
    end
end

Just putting it out here in case anyone needs it.

However, I noticed that sometimes I can add fuel even though it has 600 units if you exit and re-enter the world, so I just added the same function but triggered when the game is loaded in.
Here's the code:

--In MainFunction
inst.OnLoad = OnLoadFn

--The function that triggers when the game loads, put outside of MainFunction
local function OnLoadFn(inst)
    if inst.components.fueled.currentfuel < 120*5 then
		inst.components.fueled.accepting = true
    else
		inst.components.fueled.accepting = false
    end
end

Once again thank you so much @ClumsyPenny for solving this problem!

  • GL Happy 1
Link to comment
Share on other sites

16 minutes ago, BombardmentPigs said:

About that issue, I found a way to solve it.
Since each piece of fuel adds 120 units, and the former maximum storage was 120x5 or 600 units, I just made the new maximum storage 1 more unit (601 units), so that when it stores 5 pieces of fuel, it won't reach full capacity, and so the problem won't occur.

The fact that the item wouldn't reach 100% would drive me INSANE, but that's just me personally. I have an idea on how to fix it, but I'm about to go to sleep so I haven't tested it:

AddComponentPostInit("fueled", function(self)
	local _old_onsave = self.OnSave
	
	function self:OnSave(...)
		local data = _old_onsave(self, ...)
		
		if self.inst:HasTag("aligniumstorage") and self.currentfuel == self.maxfuel then
			if data == nil then
				data = {}
			end
			
			data.fuel = self.currentfuel
		end
		
		return data
	end
end)

Basically I'm hooking into fueled's OnSave function to account for your storage item and save the fuel even if it's full.

Link to comment
Share on other sites

13 minutes ago, ClumsyPenny said:

The fact that the item wouldn't reach 100% would drive me INSANE, but that's just me personally. I have an idea on how to fix it, but I'm about to go to sleep so I haven't tested it:

AddComponentPostInit("fueled", function(self)
	local _old_onsave = self.OnSave
	
	function self:OnSave(...)
		local data = _old_onsave(self, ...)
		
		if self.inst:HasTag("aligniumstorage") and self.currentfuel == self.maxfuel then
			if data == nil then
				data = {}
			end
			
			data.fuel = self.currentfuel
		end
		
		return data
	end
end)

An interesting idea. However, I don't think I need to use it since, funnily enough, in-game when it is at 600 units it says the storage is at 100%, probably due to rounding (600/601 is 99.8%). You wouldn't be able to know that it actually hasn't reached max capacity unless you looked at the code.

Edited by BombardmentPigs
  • Haha 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...