From ea9c7e68e71318814f7214c6355cd58ef7b97231 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 06:33:44 -0700 Subject: [PATCH] rendering,ui: sync selection circle to renderer instance position The selection circle was positioned using the entity's game-logic interpolator (entity->getX/Y/Z), while the actual M2 model is positioned by CharacterRenderer's independent interpolator (moveInstanceTo). These two systems can drift apart during movement, causing the circle to appear under the wrong position relative to the visible model. Fix: add CharacterRenderer::getInstancePosition / Application::getRenderPositionForGuid and use the renderer's inst.position for XY (with footZ override for Z) so the circle always tracks the rendered model exactly. Falls back to the entity game-logic position when no CharacterRenderer instance exists. --- include/core/application.hpp | 1 + include/rendering/character_renderer.hpp | 1 + src/core/application.cpp | 20 ++++++++++++++++++++ src/rendering/character_renderer.cpp | 7 +++++++ src/ui/game_screen.cpp | 24 +++++++++++++++++++----- 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 4d10acc7..7587fb7b 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -78,6 +78,7 @@ public: // Render bounds lookup (for click targeting / selection) bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const; bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const; + bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const; // Character skin composite state (saved at spawn for re-compositing on equipment change) const std::string& getBodySkinPath() const { return bodySkinPath_; } diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 7a01c0d7..f516b3a4 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -91,6 +91,7 @@ public: bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const; bool getInstanceBounds(uint32_t instanceId, glm::vec3& outCenter, float& outRadius) const; bool getInstanceFootZ(uint32_t instanceId, float& outFootZ) const; + bool getInstancePosition(uint32_t instanceId, glm::vec3& outPos) const; /** Debug: Log all available animations for an instance */ void dumpAnimations(uint32_t instanceId) const; diff --git a/src/core/application.cpp b/src/core/application.cpp index ca528bb2..9f32c66b 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -4879,6 +4879,26 @@ bool Application::getRenderFootZForGuid(uint64_t guid, float& outFootZ) const { return renderer->getCharacterRenderer()->getInstanceFootZ(instanceId, outFootZ); } +bool Application::getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const { + if (!renderer || !renderer->getCharacterRenderer()) return false; + uint32_t instanceId = 0; + + if (gameHandler && guid == gameHandler->getPlayerGuid()) { + instanceId = renderer->getCharacterInstanceId(); + } + if (instanceId == 0) { + auto pit = playerInstances_.find(guid); + if (pit != playerInstances_.end()) instanceId = pit->second; + } + if (instanceId == 0) { + auto it = creatureInstances_.find(guid); + if (it != creatureInstances_.end()) instanceId = it->second; + } + if (instanceId == 0) return false; + + return renderer->getCharacterRenderer()->getInstancePosition(instanceId, outPos); +} + pipeline::M2Model Application::loadCreatureM2Sync(const std::string& m2Path) { auto m2Data = assetManager->readFile(m2Path); if (m2Data.empty()) { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index f69ae75c..59965ec8 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -3175,6 +3175,13 @@ bool CharacterRenderer::getInstanceFootZ(uint32_t instanceId, float& outFootZ) c return true; } +bool CharacterRenderer::getInstancePosition(uint32_t instanceId, glm::vec3& outPos) const { + auto it = instances.find(instanceId); + if (it == instances.end()) return false; + outPos = it->second.position; + return true; +} + void CharacterRenderer::detachWeapon(uint32_t charInstanceId, uint32_t attachmentId) { auto charIt = instances.find(charInstanceId); if (charIt == instances.end()) return; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index bded7481..0eef9b83 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -522,11 +522,25 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget(); if (target) { - targetGLPos = core::coords::canonicalToRender( - glm::vec3(target->getX(), target->getY(), target->getZ())); - float footZ = 0.0f; - if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) { - targetGLPos.z = footZ; + // Prefer the renderer's actual instance position so the selection + // circle tracks the rendered model (not a parallel entity-space + // interpolator that can drift from the visual position). + glm::vec3 instPos; + if (core::Application::getInstance().getRenderPositionForGuid(target->getGuid(), instPos)) { + targetGLPos = instPos; + // Override Z with foot position to sit the circle on the ground. + float footZ = 0.0f; + if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) { + targetGLPos.z = footZ; + } + } else { + // Fallback: entity game-logic position (no CharacterRenderer instance yet) + targetGLPos = core::coords::canonicalToRender( + glm::vec3(target->getX(), target->getY(), target->getZ())); + float footZ = 0.0f; + if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) { + targetGLPos.z = footZ; + } } renderer->setTargetPosition(&targetGLPos);