Nelzanger

(SOLVED) Is disabling equip actions in specific conditions doable ?

Recommended Posts

Nelzanger    14

Hi,

I want to make some of my custom hat like a "cursed items", something that you cannot unequip until some conditions are fulfilled. I played around with the components and actions and so far, it "kinda" works by doing the following :

  1. Re-equipping the hat when unequipped. However, that doesn't work very well since effects are applied again when equipping, and is visually unpleasant ;
  2. Removing the equippable component when equiping the hat, and re-adding it after special conditions have been fulfilled ;
  3. Overriding the UNEQUIP.fn action defined in actions.lua, and restore the old behavior once special conditions have been fulfilled.

However, this is obviously prone to errors and crashes. If I cannot unequip the hat with a right-clic when doing this, I can still unequip it by clicking on it, or swap it with another hat by equipping that other one, which leads to nasty issues.

Worst, doing it when overriding the unequip action makes me overriding something that's inside GLOBAL, which means I'm overriding all unequip actions, even for body/hand slots while I just want to do it for hats, and I also strongly suspect that I'm altering the behavior for other players as well, which is not wanted.

Hence my question : is it doable to disable the unequip action only for the hat slot, only for the player wearing the cursed hat, and disable other actions that potentially manipulate the hat (clic/mouse slot, swap) ?

Share this post


Link to post
Share on other sites
Monti18    341

I think the easiest way is to use The UNEQUIP.fn action.

You overwrite the function, but when you save the old function  and return it if the conditions are not met, it doesn't alter the behaviour for other players and objects.

local old_equip = ACTIONS.EQUIP.fn
ACTIONS.EQUIP.fn = function(act,...)
	if act.doer and act.doer.prefab == "yourcharacterprefab" then
		if act.doer.components.inventory then
			local hat = act.doer.components.inventory:GetEquippedItem(EQUIPSLOTS.HEAD)
			if hat and hat.prefab == "yourhatprefab" then
				--your conditions that it isn't unequippable then
				return false
			end
		end
	end
	return unpack{old_equip(act,...)}
end

local old_unequip = ACTIONS.UNEQUIP.fn
ACTIONS.UNEQUIP.fn = function(act,...)
	if act.doer and act.doer.prefab == "yourcharacterprefab" then
		local hat = act.invobject
		if hat and hat.prefab == "yourhatprefab" then
			--if your conditions that it isn't unequippable then
			return false
		end
	end
	return unpack{old_unequip(act,...)}
end

If you overwrite the equip and unequip functions, it shouldn't be possible to unequip your hat by equipping another one.

It should look approximately like this.

Another way would perhaps be to tinker in the inventory component in the equip function, but I think actions is the easiest one.

  • Like 1

Share this post


Link to post
Share on other sites
Nelzanger    14
On 9/9/2021 at 8:13 PM, Monti18 said:

I think the easiest way is to use The UNEQUIP.fn action.

You overwrite the function, but when you save the old function  and return it if the conditions are not met, it doesn't alter the behaviour for other players and objects.

If you overwrite the equip and unequip functions, it shouldn't be possible to unequip your hat by equipping another one.

Thanks a lot for the insight. While overriding the unequip worked, I didn't know I could check for my character prefab or even the hat itself, since I didn't know the object I was manipulating. I didn't thought about overriding the equip action. Implementing this solution works and prevent from unequipping the hat, or swapping it, when the condition is not met.

However, I can still unequip the hat by clicking on it in the inventory (the object is kept in a special slot dedicated to the mouse) even if the conditions are not met. So I assume I would need to prevent inventory manipulation for the headslot as well during the effect. It seems that the very item in inventory that "follows the mouse" is the "active item" (As we can see in Inventory:GiveActiveItem). But this function is not global and I don't think I can override it like the actions. Can the functions in the inventory component be tampered with ? If so, what would be a good and safe way to do it ?

Edited by Nelzanger
Added precisions

Share this post


Link to post
Share on other sites
Monti18    341

Oh that's right, I didn't think of this! :D

Seems like activeitem is the one that follows your mouse. But I think it's better to try to change the function CanAccessItem, as it's called before the other one.

As for changing components, you can use:

AddComponentPostInit("inventory",fn)
--or
AddClassPostConstruct("components/inventory",fn)

Both work, AddClassPostConstruct can be used for more things like widgets or screens or also other lua files that are classes.

So in this case, a safe way would be:

local function InventoryPostInit(self)
	local old_CanAccessItem = self.CanAccessItem --save the old CanAccessItem function
  	self.CanAccessItem = function(self,item,...)
    	if item and item.prefab == "yourhat" then
      		--your conditions
      		return false
      	end
    	return unpack{old_CanAccessItem(self,item,...)} --return it as an unpacked table so that all arguments are returned properly
  	end
end

AddComponentPostInit("inventory",InventoryPostInit)

If it's working this way, you won't probably even need the overwritting of the actions.

  • Like 1

Share this post


Link to post
Share on other sites
Nelzanger    14

Thanks for the answer. I didn't know that you could alter components after they have been added. Letting the "unpack" makes the server crash, because of the following error :

attempt to call global 'unpack' (a nil value)

I put the function in modmain.lua, but I'm not sure if it's where it should be placed. But since CanAccessItem returns one bool, is it necessary to unpack it ? Unpack seems to be used for multiple returns.

Otherwise, it works partially : I can prevent unequipping with right click, but not the swapping, or unequipping with the left click. But now that I know I can pre-hook functions in components, I can work again on it and I'll play more with the "InvSlot click action handlers" in inventory.lua.

I'll let you know if I could reach something that works fine. ;)

Edited by Nelzanger

Share this post


Link to post
Share on other sites
Monti18    341

Oh sorry, you need to add a GLOBAL before unpack as unpack is in the global environment.

Sure, it will work without the unpack, this is just to prevent it from breaking if Klei decides to return two values with this function.

Yeah this seems like quite a complicated feat! I hope you will be able to find a solution for that, please let us know if you find it as I'm sure somebody would also love to know how it's done! :D

I wish you luck with that, if you need more help just ask!

P.S: As for functions that can be used to alter the existing files, have a look at modutil.lua, they are defined there.

Share this post


Link to post
Share on other sites
Nelzanger    14

I've toyed more with actions and function hook and found a way to make it possible. However, I still have some artifacts that I would get rid off (like the active item that shows the hat while it's still on the character). So I went a bit further and tried to override the controls themselves for equip slots by hooking on EquipSlot:OnControl (widgets/equipslot.lua).

In the end, by implementing several ways to do it, I noticed the following :

  1. If I override each crucial inventory methods (SwapEquipWithActiveItem, etc.) individually and check the hat item for a "cursed" tag, preventing from unequipping the hat works, but I got visual artifacts that are not really good for feedback (It feels like a hack). Using the talker component will fully work, with message display and sound ;
  2. If I override OnControl and check self.owner tags for a "cursed" tag, preventing from unequipping the hat works perfectly. Odd fact : if I use the talker component, the message will be displayed, but my character will not speak with sound (no talk animation, no sound speech) ;
  3. If I override OnControl and check the hat item for a "cursed" tag, preventing from unequipping will crash, because I "attempt to index nil values".

I could go for solution 2, but it's not really convenient when implementing hat effects (needs more controls and checks when applying them). Solution 3 looks more appealing to me, because you would add a simple tag in the hat, and then BAM, job's done, and only the override function would check what happens. But solution 3 doesn't work, and I would really like to know why.

I basically copied/paste the EquipSlot:OnControl to override it completely, and put lots of prints to understand how and when methods are called. However, I can't go further at some point, due to nil value.

Here is the edited code below :

local function InventoryPostInit5(self)
  local old_OnControl = self.OnControl
  self.OnControl = function(self, control, down, ...)

    if self.tile ~= nil then
      self.tile:UpdateTooltip()
    end

    print("=== ON CONTROL START ==========")

    if down then        
      local inventory = self.owner.replica.inventory
      local currentHat = inventory:GetEquippedItem(GLOBAL.EQUIPSLOTS.HEAD)
		
      -- currentHat exists, but I noticed that currentHat has a different GUID when retreived here, than the one I get when creating it
      if (currentHat) then
        print("CURRENT HAT : " .. tostring(currentHat))
      else
        print("CURRENT HAT IS NULL !")
      end

      local currentHatComponents = currentHat.components

      print("COMPONENTS COUNT : " .. #currentHatComponents)

      -- Trying to show the list of components
      for i = 1, #currentHatComponents do
        print("COMPONENTS CONTENT : " ..  currentHatComponents[i])
      end

      print ("COMPONENTS : " .. tostring(currentHatComponents))

      local invitem = currentHatComponents.inventoryitem

      -- invitem is always null
      if (invitem == nil) then
        print("INVENTORY ITEM IS NULL FOR SOME REASONS!")
        return
      end            

      -- Rest of Equipslot:OnControl cut to lighten the code section, since it crashes before

    end

    print("=== ON CONTROL END ==========")

    return GLOBAL.unpack{old_OnControl(self, control, down, ...)}
  end
end

 

I noticed that currentHat.components has... 0 components. While it should has all the components expected from a hat (equippable, inventoryitem, etc.) And since there's no component, I cannot access, in my case, to my special hat component.

So I would like to know why there's no components in there while it should have. I strongly suspect this is tied to how replicas works, with my components data being server-side, and the controls, client-side. Since self.owner does not seem to be a replica, it seems logical to work perfectly when doing it with solution 2. But I still have a hard time to understand how replicas work, and most of all, why components would suddenly be empty.

What I am missing here ? Do I need to create a specialHatComponent_replica and specialHatComponent_classified to make it work ? I'll gladly take any enlightenment on the subject.

Edited by Nelzanger
Fixed some typos

Share this post


Link to post
Share on other sites
Monti18    341

3 looks like the best possibility :)

As to your questions, if you run a server with caves, it means that the client doesn't have the components that the server has. This is where the replicas take action. These are synchronized with their counterpart components on the server, which means you need to call replicas if you are running code on the client as the components are nil.

But no need to get this complicated with creating your own replica or classified.

I just tested this code snipped and it works as it should:

local function InventoryPostInit5(self)
	local old_OnControl = self.OnControl
	self.OnControl = function(self, control, down, ...)
		if down then
			print("OnControl equipslot")
			local inventory = self.owner.replica.inventory
			local active_item = inventory:GetActiveItem()
			if active_item == nil then 
				print(self.tile)
				if self.tile ~= nil and self.tile.item ~= nil and self.tile.item:HasTag("wood") then
					print("tile item is there and cannot be unequipped",self.tile,self.tile.item)
					return false
				end
			elseif active_item ~= nil then
				if self.tile ~= nil and self.tile.item ~= nil and self.tile.item:HasTag("wood") then
					print("tile item is there and cannot be unequipped",self.tile,self.tile.item)
					return false
				end
			end
		end
		return GLOBAL.unpack({old_OnControl(self, control, down,...)})
	end
end

AddClassPostConstruct("widgets/equipslot",InventoryPostInit5)

This makes it impossible to unequip an item if it has the tag wood, for example a wood armor if you want to test it.

The item that is in the slot is located in tile.item, so no need to go over self.owner.

You will still need to implement the changes to actions equip and unequip, but I think then it should work as you want it to :)

As for your number 2, if I remember correctly, there is the funciton Chatter which makes the character talk even if you call it only on the client, so with this it should also work :D

Edited by Monti18
  • Like 1

Share this post


Link to post
Share on other sites
Nelzanger    14

The main issue for solution 3 was never the tag, but the null components. :D

7 hours ago, Monti18 said:

if you run a server with caves, it means that the client doesn't have the components that the server has. This is where the replicas take action. These are synchronized with their counterpart components on the server, which means you need to call replicas if you are running code on the client as the components are nil.

Aaaaand that's the final nail in the coffin, as I suspected.

I must apologize, I didn't give the whole context. My character uses a fourth custom gauge, which is added to my character prefab (it made more sense to have my character have it, instead of individual hats). However, it will not behave like wetness for example : it can be shown, UI wise, even at 0, because you can build up meter, or on the contrary, you will have to wait for it to reach 0 to remove a hat for example. In that purpose, hats have a reference to my character component, since I know which is the user when wearing a hat : I think this is a "code smell", but it allows me to have tight control on activation or deactivation and effects tied to hats.

But as you said, components are nils when run on the client, and obviously, equipslot.lua seems to be client side, so I guess that if I want solution 3 to work, I'll have to make replica... which I want to avoid because I'm still struggling with the replica system, and only a player playing my character can get hat effects.

7 hours ago, Monti18 said:

As for your number 2, if I remember correctly, there is the function Chatter which makes the character talk even if you call it only on the client, so with this it should also work

I've looked at this method but it requires an id. Since I made custom strings, I don't know what to give as id to it. Talker:Say() works fine, and sometimes, you will have some lines without sound/speech animation (when a torch ran out for example) so it will be fine.

I think I'll go with the solution 2, since it works best without any artifacts. I'll post it once I've cleaned all my dirty code. :D

Edited by Nelzanger
Added more precisions

Share this post


Link to post
Share on other sites
Monti18    341

Ah I understand now!
But this is no problem, it makes it even easier ;)

The tag was just an example, you have the item and can call whatever you want, for example the replicas of inventory or inventoryitem that are present.

But as you have a working meter, you probably also added a netvar to the character. As it is a netvar, it can also be accessed from the client.

So you can use:

self.owner.name_of_netvar:value() 

to get the value of your meter on the client and then use this to determine if the hat can be unequipped or not.

This post helped me get started with replicas, you can have a look if you want to get some insight as to how they should look:

As for the Chatter, I also have no idea, I will have a look and see how it's used :)

  • Like 1

Share this post


Link to post
Share on other sites
Nelzanger    14

After implementing and testing several way to do it, here are my results.

First of all, you need to know about the "active item". The active item is the one your mouse "hold", for example when you click on an item on the floor to pick it up but your inventory is full, or clicking on an object from your inventory to give it to another player.  

Then, look at equipslot.lua and the OnControl method, because this is what manage your input. In this method, you can find the following iventory methods that will manage item manipulation.

-- SwapEquipWithActiveItem is called when swapping your equipped item with the active item. 
-- If you want to prevent unequipping, you must manage this case.
inventory:SwapEquipWithActiveItem()

-- EquipActiveItem is called directly to equip the active item where you have no equipment on self for the concerned slot. 
-- You don't need to manage this case.
inventory:EquipActiveItem()

-- TakeActiveItemFromEquipSlot is called when clicking on your equipped item with the left click. It will put the equipped item into the active item.
-- If you want to prevent unequipping, you must manage this case.
inventory:TakeActiveItemFromEquipSlot(self.equipslot)

-- DropItemFromInvTile is called when using the drop action when your item is equipped.
-- If you want to prevent unequipping, you must manage this case.
inventory:DropItemFromInvTile(self.tile.item, GLOBAL.TheInput:IsControlPressed(GLOBAL.CONTROL_FORCE_STACK))

-- UseItemFromInvTile is called when unequipping with the unequip action when your item is equipped.
-- If you want to prevent unequipping, you must manage this case.
inventory:UseItemFromInvTile(self.tile.item)

 

You can override them manually but you will have UI artifacts (for example, the hat will still be displayed in the active item while it's still on your character). An example of such override can be seen as below :

local function SwapOverride(self)
    local old_SwapEquipWithActiveItem = self.SwapEquipWithActiveItem
  	self.SwapEquipWithActiveItem = function(self, ...)
           
        if ( "[Your condition]") then
            self:ReturnActiveItem() -- This line can fix some UI artifacts since you tell the item to return to its previous inventory slot, or on the ground
            return
        end

        return GLOBAL.unpack{old_SwapEquipWithActiveItem(self, ...)}
    end
end

AddComponentPostInit("inventory", SwapOverride)

If you do it that way, know that by overriding the inventory:CanAccessItem method, it will work for DropItemFromInvTile and UseItemFromInvTile since both use CanAccessItem to process further.

However, the Equip action on another equippable in your inventory will still work, whenever you override OnControl, or the inventory methods, so you will need to override GLOBAL.ACTIONS.EQUIP.fn as well as seen up above in this topic.

 

I ended up overriding OnControl, because I didn't want the UI artifacts, and it seemed better to prevent actions rather than letting them executing parts of it. I used the solution 2 I talked about earlier, by using a cursed tag on my character which is added when the hat is equipped, and removed when the hat effect ends. The following code is in the folder scripts, and is imported in modmain.lua with the modimport method :

--- UTILS ---------------------------------------------------------------------

local function IsHatCurseActive(owner, equipSlotType)

    if (owner:HasTag("cursed") and equipSlotType == GLOBAL.EQUIPSLOTS.HEAD) then
        owner.components.talker:Say("The effects are still active, I cannot unequip it!")
        return true
    end
    
    return false
end

-- Obliged to name the method differently because lua doesn't support polymorphism. Well not in a way like C++ or C# do.
local function IsCurseActive(owner)
    return IsHatCurseActive(owner, GLOBAL.EQUIPSLOTS.HEAD)
end


--- OVERRIDES ----------------------------------------------------------------

-- Prevent equipping another hat in the inventory by right clicking it
local oldActionEquip = GLOBAL.ACTIONS.EQUIP.fn

GLOBAL.ACTIONS.EQUIP.fn = function(act)

    if act.doer and act.doer.prefab == "yourCharacter" then
    
        local equipSlot = act.invobject.components.equippable.equipslot
        
        if (equipSlot == GLOBAL.EQUIPSLOTS.HEAD and IsCurseActive(act.doer)) then
            return false
		end
	end
	
    return oldActionEquip(act)
end


local function OnControlOverride(self)

    local old_OnControl = self.OnControl
  	self.OnControl = function(self, control, down, ...)
    
        if ( IsHatCurseActive(self.owner, self.equipslot) ) then
            return
        end
    
    	return GLOBAL.unpack{old_OnControl(self, control, down, ...)}
  	end
end

-------------------------------------------------------------------------------

 

16 minutes ago, Monti18 said:

But as you have a working meter, you probably also added a netvar to the character. As it is a netvar, it can also be accessed from the client.

So you can use:


self.owner.name_of_netvar:value() 

to get the value of your meter on the client and then use this to determine if the hat can be unequipped or not.

While I have indeed a working meter, not all of its variables are listened to, and the less I send in network, the better it is in general. I didn't know I could access directly to the netvars like this so this is definitely a excellent information. However, doing that way feels like a hack, since the idea I got behind all the _replica and _classified files when I looked at them was to provide some kind of interface.

I think I'll go with solution 2 for now, and if I finish my mod in an acceptable state, I'll then think about refactoring with netvars instead if it's better code quality wise.

Thanks for the link about replicas. I'll definitely check it to get a better grasp of them. :D

Edited by Nelzanger
  • Like 1

Share this post


Link to post
Share on other sites
Monti18    341
35 minutes ago, Nelzanger said:

While I have indeed a working meter, not all of its variables are listened to, and the less I send in network, the better it is in general. I didn't know I could access directly to the netvars like this so this is definitely a excellent information. However, doing that way feels like a hack, since the idea I got behind all the _replica and _classified files when I looked at them was to provide some kind of interface.

This won't impact the network as far as I know, as these values are synchronized as soon as they change. So value x is there on the server and on the client, so calling value() of the netvar will not impact the network.

The replica and classified files are mostly needed to have a better overview about what you are doing and using it for multiple prefabs. You can achieve exactly the same without those, it's just more clean if you have a lot of netvars.

But you shouldn't be worried about the network if you are only sending these few things, this is in no way a problem for the game. For example, each time you add a tag, it also synchronize with the clients which sends something over the network. Which means that doing it with tags is "worse" than just getting the value of the netvar. "Worse" because it's not really worse, you won't feel a difference in performance.

Do it like you said, this way even if it doesn't work you will still have a functioning mod :D

Thanks for describing how you made it impossible to unequip an item :)

Share this post


Link to post
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