Nelzanger Posted September 9, 2021 Share Posted September 9, 2021 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 : Re-equipping the hat when unequipped. However, that doesn't work very well since effects are applied again when equipping, and is visually unpleasant ; Removing the equippable component when equiping the hat, and re-adding it after special conditions have been fulfilled ; 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) ? 1 Link to comment Share on other sites More sharing options...
Monti18 Posted September 9, 2021 Share Posted September 9, 2021 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. 2 Link to comment Share on other sites More sharing options...
Nelzanger Posted September 13, 2021 Author Share Posted September 13, 2021 (edited) 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 September 13, 2021 by Nelzanger Added precisions 1 Link to comment Share on other sites More sharing options...
Monti18 Posted September 13, 2021 Share Posted September 13, 2021 Oh that's right, I didn't think of this! 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. 2 Link to comment Share on other sites More sharing options...
Nelzanger Posted September 14, 2021 Author Share Posted September 14, 2021 (edited) 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 September 14, 2021 by Nelzanger 1 Link to comment Share on other sites More sharing options...
Monti18 Posted September 14, 2021 Share Posted September 14, 2021 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! 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. 1 Link to comment Share on other sites More sharing options...
Nelzanger Posted September 14, 2021 Author Share Posted September 14, 2021 (edited) 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 : 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 ; 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) ; 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 September 14, 2021 by Nelzanger Fixed some typos 1 Link to comment Share on other sites More sharing options...
Monti18 Posted September 15, 2021 Share Posted September 15, 2021 (edited) 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 Edited September 15, 2021 by Monti18 2 Link to comment Share on other sites More sharing options...
Nelzanger Posted September 15, 2021 Author Share Posted September 15, 2021 (edited) The main issue for solution 3 was never the tag, but the null components. 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. Edited September 15, 2021 by Nelzanger Added more precisions 1 Link to comment Share on other sites More sharing options...
Monti18 Posted September 15, 2021 Share Posted September 15, 2021 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 2 Link to comment Share on other sites More sharing options...
Nelzanger Posted September 15, 2021 Author Share Posted September 15, 2021 (edited) 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. Edited September 15, 2021 by Nelzanger 2 Link to comment Share on other sites More sharing options...
Monti18 Posted September 15, 2021 Share Posted September 15, 2021 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 Thanks for describing how you made it impossible to unequip an item 1 Link to comment Share on other sites More sharing options...
Nelzanger Posted September 22, 2021 Author Share Posted September 22, 2021 (edited) I will make an update on this post. I had the unpleasant surprise to see that my prevention failed for controller inputs. It's entirely my fault, I completely forgot the game can be played with a controller (I thanks my friend that helped me test this on multiplayer, since he only plays the game with a controller). Therefore, if you need to prevent unequipping with controller inputs, you will also need to override the following actions : GLOBAL.ACTIONS.UNEQUIP.fn GLOBAL.ACTIONS.DROP.fn Doing this will conflict with the OnControl override. While it works when letting both, it's not good to let unnatural behavior for the well being of the game's runtime. I tested removing the OnControl override, but if you do that, you will need to override the following to prevent inventory manipulation when the player clicks, or press A, on the equipped item : inventory:SwapEquipWithActiveItem inventory:TakeActiveItemFromEquipSlot BUT ! Doing so will re-introduce the UI artifacts, something you may not want since, while it's perfectly correct code wise, the UI will still receive the instruction to put the item into the active item, and will show something incorrect from the point of view of the player. Which is not great for feedback, and will let players think that the feature is bugged. So in the end, if you want to get rid of those artifacts, you will need to overwrite the OnControl method, and inject your conditions before the two inventory methods above are called (Basically, you copy paste the code from OnControl, you don't forget to add the scope for variables, otherwise it will crash, and you put your conditions). The final code looks like this : -- This file is in the "scripts" folder, 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 local function CanExecuteAction(act) if act.doer and act.doer.prefab == "yourCharacter" then local equippable = act.invobject.components.equippable if (equippable ~= nil) then local equipSlot = equippable.equipslot if (equipSlot == GLOBAL.EQUIPSLOTS.HEAD and IsCurseActive(act.doer)) then return false end end end return true end --- OVERRIDES ---------------------------------------------------------------- -- Prevent equipping another hat in the inventory by actions for both mouse and controller inputs local oldActionEquip = GLOBAL.ACTIONS.EQUIP.fn local oldActionUnequip = GLOBAL.ACTIONS.UNEQUIP.fn local oldActionDrop = GLOBAL.ACTIONS.DROP.fn GLOBAL.ACTIONS.EQUIP.fn = function(act) if not CanExecuteAction(act) then return false end return oldActionEquip(act) end GLOBAL.ACTIONS.UNEQUIP.fn = function(act) if not CanExecuteAction(act) then return false end return oldActionUnequip(act) end GLOBAL.ACTIONS.DROP.fn = function(act) if not CanExecuteAction(act) then return false end return oldActionDrop(act) end -- Overwrite of the equipslot:OnControl method to prevent unequipping with active item local function OnControlOverride(self) self.OnControl = function(self, control, down, ...) if self.tile ~= nil then self.tile:UpdateTooltip() end if down then local inventory = self.owner.replica.inventory if control == GLOBAL.CONTROL_ACCEPT then local active_item = inventory:GetActiveItem() if active_item ~= nil then if active_item.replica.equippable ~= nil and self.equipslot == active_item.replica.equippable:EquipSlot() and not active_item.replica.equippable:IsRestricted(self.owner) then if self.tile ~= nil and self.tile.item ~= nil then if ( IsHatCurseActive(self.owner, self.equipslot) ) then return false end inventory:SwapEquipWithActiveItem() else inventory:EquipActiveItem() end end elseif self.tile ~= nil and self.tile.item ~= nil and self.owner.replica.inventory:GetNumSlots() > 0 then if ( IsHatCurseActive(self.owner, self.equipslot) ) then return false end inventory:TakeActiveItemFromEquipSlot(self.equipslot) end return true elseif control == GLOBAL.CONTROL_SECONDARY and self.tile ~= nil and self.tile.item ~= nil then if GLOBAL.TheInput:IsControlPressed(GLOBAL.CONTROL_FORCE_TRADE) then inventory:DropItemFromInvTile(self.tile.item, GLOBAL.TheInput:IsControlPressed(GLOBAL.CONTROL_FORCE_STACK)) else inventory:UseItemFromInvTile(self.tile.item) end return true end end end end ------------------------------------------------------------------------------- AddClassPostConstruct("widgets/equipslot", OnControlOverride) Edited October 7, 2021 by Nelzanger I don't know why my indentations in code are wonky. Dunno how to fix this sorry. :( 1 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now