-- Ephemeral third-person visuals only. Main loads this once before runtime. -- No ItemStacks, inventories, metadata, static data, attacks or file paths from input. local C, H = core, hearth local meshes = { ["hearth:knife"] = "hearth_knife.obj", ["hearth:axe"] = "hearth_axe.obj", ["hearth:pick"] = "hearth_pick.obj", ["hearth:spear"] = "hearth_spear.obj", } local records, pending = {}, nil local entity_name = "hearth:held_visual" local function live(record) local p, name = record.player, record.name return C.get_player_by_name(name) == p and H.sessions[name] == record.session and H.players[name] ~= nil and not record.session.waiting and p:get_pos() ~= nil and p:get_hp() > 0 and p:get_wielded_item():get_name() == record.item end local function remove(record) -- Clear ownership BEFORE remove/on_deactivate; an old callback cannot clear -- a replacement belonging to a same-name reconnect or a different weapon. if records[record.name] == record then records[record.name] = nil end local object = record.object record.object = nil if object and object:get_pos() then object:remove() end end local function attached(record) local object = record.object if not object or not object:get_pos() then return false end local parent, bone, pos, rotation, forced = object:get_attach() return parent == record.player and bone == "Hand_Right" and pos and pos.x == 0 and pos.y == 0 and pos.z == 0 and rotation and rotation.x == 90 and rotation.y == 0 and rotation.z == 0 and forced == false end C.register_entity(entity_name, { initial_properties = { visual = "mesh", mesh = "hearth_knife.obj", textures = {"hearth_person.png"}, -- The generated B3D and held OBJ both use model units (10 per node). visual_size = {x = 1, y = 1, z = 1}, is_visible = false, physical = false, collide_with_objects = false, pointable = false, collisionbox = {0, 0, 0, 0, 0, 0}, selectionbox = {0, 0, 0, 0, 0, 0}, static_save = false, hp_max = 1, makes_footstep_sound = false, }, on_activate = function(self, staticdata) -- Only synchronous creation by presentation_step may bind an entity. -- Restored/foreign/unowned entities disappear rather than becoming drops. local record = pending if staticdata ~= "" or not record or record.object or not live(record) then self.object:remove(); return end record.object, self._held_record = self.object, record self.object:set_armor_groups({immortal = 1}) end, get_staticdata = function() return "" end, on_step = function(self) local record = self._held_record if not record then self.object:remove(); return end if records[record.name] ~= record or not live(record) or not attached(record) then remove(record) end end, on_deactivate = function(self) local record = self._held_record self._held_record = nil if record then if records[record.name] == record then records[record.name] = nil end record.object = nil end end, -- Cosmetic, non-pointable, immortal; even a direct punch has no gameplay effect. on_punch = function() return true end, on_rightclick = function() end, }) function H.presentation_step(player) local name = player:get_player_name() local record, session = records[name], H.sessions[name] local item = player:get_wielded_item():get_name() local mesh = meshes[item] if record then if record.player == player and live(record) and attached(record) then return end -- A deferred call on an old PlayerRef must not destroy the live replacement. if record.player ~= player and C.get_player_by_name(name) ~= player then return end remove(record) end if not mesh or not session then return end record = {name = name, player = player, session = session, item = item} if not live(record) then return end records[name], pending = record, record local ok, object = pcall(C.add_entity, player:get_pos(), entity_name) pending = nil if not ok or not object or object ~= record.object or not object:get_pos() then remove(record); return end object:set_properties({mesh = mesh}) -- forced_visible is the FIFTH argument: false hides this from its owner in -- first person, but permits third-person rendering. Never toggle is_visible -- from a guessed server-side camera mode (it would hide it for observers). object:set_attach(player, "Hand_Right", {x = 0, y = 0, z = 0}, {x = 90, y = 0, z = 0}, false) if not attached(record) then remove(record); return end object:set_properties({is_visible = true}) end function H.presentation_cleanup(player) local record = records[player:get_player_name()] if record and record.player == player then remove(record) end end C.register_on_leaveplayer(H.presentation_cleanup) C.register_on_dieplayer(H.presentation_cleanup) C.register_on_shutdown(function() local snapshot = {}; for _, record in pairs(records) do snapshot[#snapshot + 1] = record end for _, record in ipairs(snapshot) do remove(record) end end) return H.presentation_step