Version 2.1 maintains helmet state through loading screens without loading screen hook shenanigans. This means that the following options are no longer needed in the config:
bReHideOnLoad iHideOnLoadDelay
If you have any crashes, please be specific with the steps to replicate them.
Controller Users Controller inputs can now be mapped in the configuration. You can find the valid inputs here
Hide More Armor Pieces I've made another mod that allows you to hide more armor pieces here
Can't get this mod to work for the life of me, using the auto hide helmet file btw. Do I need to edit anything in the lua file? I've installed manually.
So I am having a issue with this mod. It keeps deleting my helmet from my inventory and forces me to go find another one. Lost 3 helmets at this point. Any suggestions?
Firstly, Thank you so much for sharing your work on this. This inspired me to learn UE4SS and for now am happy code below
I wanted - hidden on game start - hidden when closing inventory after equipping - toggling was unecessary for me - I'm not a milk-drinker
I hope you don't mind looking over code I adapted from your work I would love any feedback - especially tips with performance, efficiencies, issues with hooks or more appropriate hooks to use This is my first week with UE4SS and lua, but it just works so far
- loops body slots - forms cached in an array - works without having to reference TESforms explicitly - unhides in UI mode - I would prefer to only run when inventory and stats page... I'll continue digging into the API more - hides quiver in game
Thanks again
--The variables below get initialized in the LoopAsync at the end of the script local playerCharacter = nil local CBPC = nil
--init an empty array to hold hidden objects local EquipmentHidden = {}
local function HideArmor() for bodySlot, SlotIndexValue in pairs(slotsToHide) do if CBPC:GetBodyPartForm(SlotIndexValue):IsValid() then -- print("[AlwaysHideHelmetAndQuiver] HideArmor() " .. bodySlot .. " is equipped and will be set invisible\n") EquipmentHidden[SlotIndexValue] = CBPC:GetBodyPartForm(SlotIndexValue)
ExecuteInGameThread(function () CBPC:SetNakedOnSlot(SlotIndexValue) playerCharacter:RefreshAppearance(4) end) end end end local function ShowArmor() for SlotIndexValue, hiddenObject in pairs(EquipmentHidden) do -- print("hidden is valid " .. SlotIndexValue .. "\n") ExecuteInGameThread(function () CBPC:EquipFormOnSlot(SlotIndexValue, hiddenObject) playerCharacter:RefreshAppearance(4) end) end --emptify/nullify the table values to allow re-populatioin by HideArmor() using same indexes for i = 0, 9, 1 do EquipmentHidden[i]=nil end end
local function HideQuiver() local WPC = playerCharacter.WeaponsPairingComponent if WPC.QuiverActor:IsValid() then WPC.QuiverActor:SetActorHiddenInGame(true) playerCharacter:RefreshAppearance(4) end end
local function CreateHook() -- switching between game and menus RegisterHook("/Game/Dev/PlayerBlueprints/BP_OblivionPlayerCharacter.BP_OblivionPlayerCharacter_C:OnSwitchToUIMappings", function() ShowArmor() end ) RegisterHook("/Game/Dev/PlayerBlueprints/BP_OblivionPlayerCharacter.BP_OblivionPlayerCharacter_C:OnSwitchToGameMappings", function() HideArmor() end )
-- on when quiver equiped changed RegisterHook("/Game/Dev/PlayerBlueprints/BP_InventoryPlayer.BP_InventoryPlayer_C:OnQuiverFormChanged", function () HideQuiver() end )
--on load of level map begin event RegisterHook("/Script/Altar.VLevelChangeData:OnFadeToGameBeginEventReceived", function () HideArmor() HideQuiver() end) end
LoopAsync(1, function () playerCharacter = FindFirstOf("VOblivionPlayerCharacter") -- wait for the player character to be created if not playerCharacter or not playerCharacter:IsValid() then return false end CBPC = playerCharacter.CharacterBodyPairingComponent
--not used, but want to wait so that the OnQuiverFormChanged function will hook local inventoryPlayer = FindFirstOf("VInventoryPlayerCharacter") if not inventoryPlayer or not inventoryPlayer:IsValid() then return false end
To be clear, your goal is to hide the armor in gameplay but not in the menus?
Without the TESObjectRefComponents, the game will garbage collect the hidden armor pieces if they are hidden for too long. If this happens, the armor will be unable to unhide.
I would recommend against setting the first argument of LoopAsync to 1, as this causes the loop to only sleep for a millisecond, leaving little breathing room for other mods to begin their loading process until this one gets what it needs. I would leave it at 1000 so that it will check roughly every second.
One small thing that is more than likely inconsequential is the fact that you are refreshing the character's appearance inside the loops. I would refresh the appearance once after the loop is done.
This mod has been working flawlessly for me, thank you. However, it doesn't seem to play nice with NBO, and somehow the two cause a game freeze at the end of questline dialogue sequences (may just be MQ, not tested that thoroughly). Removing one or the other mod resolves the issue, but I wondered if mod order might be significant so that using both was still possible?
---@diagnostic disable: need-check-nil local cfg = require("HideArmorConfig") local objectRefClass = StaticFindObject("/Script/Altar.VTESObjectRefComponent") local bHide = false local keyPressed = false local objectRefComponent = {} local playerCharacter = nil local CBPC = nil -- ✅ Final working slots (based on your testing) -- 0 = Helmet, 2 = Cuirass, 3 = Greaves, 4 = Gauntlets, 5 = Boots local armorSlots = {0, 2, 3, 4, 5} local function safeIsValid(obj) return obj and obj.IsValid and obj:IsValid() end local function ToggleArmor(hide) if not safeIsValid(playerCharacter) then print("[HideArmor] Invalid playerCharacter") return end if not safeIsValid(CBPC) then print("[HideArmor] Invalid CBPC") return end ExecuteInGameThread(function () for _, slot in ipairs(armorSlots) do local success, err = pcall(function() local equippedForm = CBPC:GetBodyPartForm(slot) if hide then if safeIsValid(equippedForm) then if not objectRefComponent[slot] or not safeIsValid(objectRefComponent[slot]) then objectRefComponent[slot] = StaticConstructObject(objectRefClass, playerCharacter) end if objectRefComponent[slot] then objectRefComponent[slot].TESForm = equippedForm CBPC:SetNakedOnSlot(slot) print("[HideArmor] Hid slot " .. slot) end end else local storedRef = objectRefComponent[slot] if storedRef and safeIsValid(storedRef.TESForm) then CBPC:EquipFormOnSlot(slot, storedRef.TESForm) storedRef.TESForm = CreateInvalidObject() print("[HideArmor] Restored slot " .. slot) end end end) if not success then print("[HideArmor] Error in slot " .. slot .. ": " .. tostring(err)) end end playerCharacter:RefreshAppearance(4) end) end local function CreateHook() RegisterHook("/Game/Dev/Controllers/BP_AltarPlayerController.BP_AltarPlayerController_C:InpActEvt_AnyKey_K2Node_InputKeyEvent_1", function(Context, Key) if Key:get().KeyName:ToString() ~= cfg.toggleKey then return end if not keyPressed then bHide = not bHide print("[HideArmor] Toggle key pressed. New state: " .. tostring(bHide)) ToggleArmor(bHide) end keyPressed = not keyPressed end) end LoopAsync(1000, function () playerCharacter = FindFirstOf("VOblivionPlayerCharacter") if not playerCharacter or not safeIsValid(playerCharacter) then return false end print("[HideArmor] Found player: " .. playerCharacter:GetFullName()) CBPC = playerCharacter.CharacterBodyPairingComponent if not CBPC or not safeIsValid(CBPC) then print("[HideArmor] Invalid CBPC") return false end CreateHook() return true end)
Just had chat GPT add all armor to this mod - cheers
It does work, tested it myself, have to make sure to rename the files to HideArmor not HideHelmet. I should also mention you can have both at the same time too if you change the hotkeys to different ones it seems like.
might be helpful to add that the delete button is the default toggle key in your description also im still having an issue with the helmet state reverting after loading screens
171 comments
bReHideOnLoad
iHideOnLoadDelay
If you have any crashes, please be specific with the steps to replicate them.
Controller Users
Controller inputs can now be mapped in the configuration. You can find the valid inputs here
Hide More Armor Pieces
I've made another mod that allows you to hide more armor pieces here
playerCharacter:RefreshAppearance(4)
to
ExecuteWithDelay(500, function()
playerCharacter:RefreshAppearanceAsync(4)
end)
Firstly, Thank you so much for sharing your work on this. This inspired me to learn UE4SS and for now am happy code below
I wanted
- hidden on game start
- hidden when closing inventory after equipping
- toggling was unecessary for me - I'm not a milk-drinker
I hope you don't mind looking over code I adapted from your work
I would love any feedback - especially tips with performance, efficiencies, issues with hooks or more appropriate hooks to use
This is my first week with UE4SS and lua, but it just works so far
- loops body slots
- forms cached in an array
- works without having to reference TESforms explicitly
- unhides in UI mode - I would prefer to only run when inventory and stats page... I'll continue digging into the API more
- hides quiver in game
Thanks again
--The variables below get initialized in the LoopAsync at the end of the script
local playerCharacter = nil
local CBPC = nil
local slotsToHide = {
Helmet = 0
-- , Head = 1
, UpperBody = 2
-- , LowerBody = 3
, Hands = 4
-- , Feet = 5
-- , Tail = 6
-- , RightRing = 7
-- , LeftRing = 8
-- , Amulet = 9
}
--init an empty array to hold hidden objects
local EquipmentHidden = {}
local function HideArmor()
for bodySlot, SlotIndexValue in pairs(slotsToHide) do
if CBPC:GetBodyPartForm(SlotIndexValue):IsValid() then
-- print("[AlwaysHideHelmetAndQuiver] HideArmor() " .. bodySlot .. " is equipped and will be set invisible\n")
EquipmentHidden[SlotIndexValue] = CBPC:GetBodyPartForm(SlotIndexValue)
ExecuteInGameThread(function ()
CBPC:SetNakedOnSlot(SlotIndexValue)
playerCharacter:RefreshAppearance(4)
end)
end
end
end
local function ShowArmor()
for SlotIndexValue, hiddenObject in pairs(EquipmentHidden) do
-- print("hidden is valid " .. SlotIndexValue .. "\n")
ExecuteInGameThread(function ()
CBPC:EquipFormOnSlot(SlotIndexValue, hiddenObject)
playerCharacter:RefreshAppearance(4)
end)
end
--emptify/nullify the table values to allow re-populatioin by HideArmor() using same indexes
for i = 0, 9, 1 do
EquipmentHidden[i]=nil
end
end
local function HideQuiver()
local WPC = playerCharacter.WeaponsPairingComponent
if WPC.QuiverActor:IsValid() then
WPC.QuiverActor:SetActorHiddenInGame(true)
playerCharacter:RefreshAppearance(4)
end
end
local function CreateHook()
-- switching between game and menus
RegisterHook("/Game/Dev/PlayerBlueprints/BP_OblivionPlayerCharacter.BP_OblivionPlayerCharacter_C:OnSwitchToUIMappings", function()
ShowArmor()
end )
RegisterHook("/Game/Dev/PlayerBlueprints/BP_OblivionPlayerCharacter.BP_OblivionPlayerCharacter_C:OnSwitchToGameMappings", function()
HideArmor()
end )
-- on when quiver equiped changed
RegisterHook("/Game/Dev/PlayerBlueprints/BP_InventoryPlayer.BP_InventoryPlayer_C:OnQuiverFormChanged", function ()
HideQuiver()
end )
--on load of level map begin event
RegisterHook("/Script/Altar.VLevelChangeData:OnFadeToGameBeginEventReceived", function ()
HideArmor()
HideQuiver()
end)
end
LoopAsync(1, function ()
playerCharacter = FindFirstOf("VOblivionPlayerCharacter")
-- wait for the player character to be created
if not playerCharacter or not playerCharacter:IsValid() then
return false
end
CBPC = playerCharacter.CharacterBodyPairingComponent
--not used, but want to wait so that the OnQuiverFormChanged function will hook
local inventoryPlayer = FindFirstOf("VInventoryPlayerCharacter")
if not inventoryPlayer or not inventoryPlayer:IsValid() then
return false
end
CreateHook()
return true
end
Without the TESObjectRefComponents, the game will garbage collect the hidden armor pieces if they are hidden for too long. If this happens, the armor will be unable to unhide.
I would recommend against setting the first argument of LoopAsync to 1, as this causes the loop to only sleep for a millisecond, leaving little breathing room for other mods to begin their loading process until this one gets what it needs. I would leave it at 1000 so that it will check roughly every second.
One small thing that is more than likely inconsequential is the fact that you are refreshing the character's appearance inside the loops. I would refresh the appearance once after the loop is done.
Other than that, it looks fine to me 👍
I'll check out your modular version soon
Have a great weekend!
thanks again
---@diagnostic disable: need-check-nil
Just had chat GPT add all armor to this mod - cheerslocal cfg = require("HideArmorConfig")
local objectRefClass = StaticFindObject("/Script/Altar.VTESObjectRefComponent")
local bHide = false
local keyPressed = false
local objectRefComponent = {}
local playerCharacter = nil
local CBPC = nil
-- ✅ Final working slots (based on your testing)
-- 0 = Helmet, 2 = Cuirass, 3 = Greaves, 4 = Gauntlets, 5 = Boots
local armorSlots = {0, 2, 3, 4, 5}
local function safeIsValid(obj)
return obj and obj.IsValid and obj:IsValid()
end
local function ToggleArmor(hide)
if not safeIsValid(playerCharacter) then
print("[HideArmor] Invalid playerCharacter")
return
end
if not safeIsValid(CBPC) then
print("[HideArmor] Invalid CBPC")
return
end
ExecuteInGameThread(function ()
for _, slot in ipairs(armorSlots) do
local success, err = pcall(function()
local equippedForm = CBPC:GetBodyPartForm(slot)
if hide then
if safeIsValid(equippedForm) then
if not objectRefComponent[slot] or not safeIsValid(objectRefComponent[slot]) then
objectRefComponent[slot] = StaticConstructObject(objectRefClass, playerCharacter)
end
if objectRefComponent[slot] then
objectRefComponent[slot].TESForm = equippedForm
CBPC:SetNakedOnSlot(slot)
print("[HideArmor] Hid slot " .. slot)
end
end
else
local storedRef = objectRefComponent[slot]
if storedRef and safeIsValid(storedRef.TESForm) then
CBPC:EquipFormOnSlot(slot, storedRef.TESForm)
storedRef.TESForm = CreateInvalidObject()
print("[HideArmor] Restored slot " .. slot)
end
end
end)
if not success then
print("[HideArmor] Error in slot " .. slot .. ": " .. tostring(err))
end
end
playerCharacter:RefreshAppearance(4)
end)
end
local function CreateHook()
RegisterHook("/Game/Dev/Controllers/BP_AltarPlayerController.BP_AltarPlayerController_C:InpActEvt_AnyKey_K2Node_InputKeyEvent_1", function(Context, Key)
if Key:get().KeyName:ToString() ~= cfg.toggleKey then return end
if not keyPressed then
bHide = not bHide
print("[HideArmor] Toggle key pressed. New state: " .. tostring(bHide))
ToggleArmor(bHide)
end
keyPressed = not keyPressed
end)
end
LoopAsync(1000, function ()
playerCharacter = FindFirstOf("VOblivionPlayerCharacter")
if not playerCharacter or not safeIsValid(playerCharacter) then return false end
print("[HideArmor] Found player: " .. playerCharacter:GetFullName())
CBPC = playerCharacter.CharacterBodyPairingComponent
if not CBPC or not safeIsValid(CBPC) then
print("[HideArmor] Invalid CBPC")
return false
end
CreateHook()
return true
end)
local config = {}
-- Choose a toggle key (e.g., "H" or "F1")
config.toggleKey = "H"
return config
(and rename it HideArmorConfig)
I should also mention you can have both at the same time too if you change the hotkeys to different ones it seems like.
also im still having an issue with the helmet state reverting after loading screens