diff --git a/soh/include/functions.h b/soh/include/functions.h index 9bc1d0615..1d9895523 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -2447,6 +2447,8 @@ void Heaps_Free(void); CollisionHeader* BgCheck_GetCollisionHeader(CollisionContext* colCtx, s32 bgId); +void Interface_CreateQuadVertexGroup(Vtx* vtxList, s32 xStart, s32 yStart, s32 width, s32 height, u8 flippedH); + // Exposing these methods to leverage them from the file select screen to render messages void Message_OpenText(PlayState* play, u16 textId); void Message_Decode(PlayState* play); diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index b5f3ec8be..2d7695af7 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -259,6 +259,8 @@ static std::map cosmeticOptions = { COSMETIC_OPTION("Hud_MinimapEntrance", "Minimap Entrance", GROUP_HUD, ImVec4(200, 0, 0, 255), false, true, true), COSMETIC_OPTION("Hud_EnemyHealthBar", "Enemy Health Bar", GROUP_HUD, ImVec4(255, 0, 0, 255), true, true, false), COSMETIC_OPTION("Hud_EnemyHealthBorder", "Enemy Health Border", GROUP_HUD, ImVec4(255, 255, 255, 255), true, false, true), + COSMETIC_OPTION("Hud_NameTagActorText", "Nametag Text", GROUP_HUD, ImVec4(255, 255, 255, 255), true, true, false), + COSMETIC_OPTION("Hud_NameTagActorBackground", "Nametag Background", GROUP_HUD, ImVec4(0, 0, 0, 80), true, false, true), COSMETIC_OPTION("Title_FileChoose", "File Choose", GROUP_TITLE, ImVec4(100, 150, 255, 255), false, true, false), COSMETIC_OPTION("Title_NintendoLogo", "Nintendo Logo", GROUP_TITLE, ImVec4( 0, 0, 255, 255), false, true, true), diff --git a/soh/soh/Enhancements/debugger/actorViewer.cpp b/soh/soh/Enhancements/debugger/actorViewer.cpp index de35a6acd..943220ff8 100644 --- a/soh/soh/Enhancements/debugger/actorViewer.cpp +++ b/soh/soh/Enhancements/debugger/actorViewer.cpp @@ -2,6 +2,8 @@ #include "../../util.h" #include "../../UIWidgets.hpp" #include "soh/ActorDB.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/nametag.h" #include #include @@ -22,6 +24,8 @@ extern PlayState* gPlayState; #include "textures/icon_item_24_static/icon_item_24_static.h" } +#define DEBUG_ACTOR_NAMETAG_TAG "debug_actor_viewer" + typedef struct { u16 id; u16 params; @@ -51,6 +55,13 @@ std::array acMapping = { "Chest" }; +typedef enum { + ACTORVIEWER_NAMETAGS_NONE, + ACTORVIEWER_NAMETAGS_DESC, + ACTORVIEWER_NAMETAGS_NAME, + ACTORVIEWER_NAMETAGS_BOTH, +} ActorViewerNameTagsType; + const std::string GetActorDescription(u16 id) { return ActorDB::Instance->RetrieveEntry(id).entry.valid ? ActorDB::Instance->RetrieveEntry(id).entry.desc : "???"; } @@ -96,6 +107,42 @@ void PopulateActorDropdown(int i, std::vector& data) { } } +void ActorViewer_AddTagForActor(Actor* actor) { + int val = CVarGetInteger("gDebugActorViewerNameTags", ACTORVIEWER_NAMETAGS_NONE); + auto entry = ActorDB::Instance->RetrieveEntry(actor->id); + std::string tag; + + if (val > 0 && entry.entry.valid) { + switch (val) { + case ACTORVIEWER_NAMETAGS_DESC: + tag = entry.desc; + break; + case ACTORVIEWER_NAMETAGS_NAME: + tag = entry.name; + break; + case ACTORVIEWER_NAMETAGS_BOTH: + tag = entry.name + '\n' + entry.desc; + break; + } + + NameTag_RegisterForActorWithOptions(actor, tag.c_str(), { .tag = DEBUG_ACTOR_NAMETAG_TAG }); + } +} + +void ActorViewer_AddTagForAllActors() { + if (gPlayState == nullptr) { + return; + } + + for (size_t i = 0; i < ARRAY_COUNT(gPlayState->actorCtx.actorLists); i++) { + ActorListEntry currList = gPlayState->actorCtx.actorLists[i]; + Actor* currAct = currList.head; + while (currAct != nullptr) { + ActorViewer_AddTagForActor(currAct); + currAct = currAct->next; + } + } +} void ActorViewerWindow::DrawElement() { ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); @@ -319,7 +366,7 @@ void ActorViewerWindow::DrawElement() { newActor.pos.y, newActor.pos.z, newActor.rot.x, newActor.rot.y, newActor.rot.z, newActor.params); } else { - func_80078884(NA_SE_SY_ERROR); + func_80078884(NA_SE_SY_ERROR); } } } @@ -330,6 +377,22 @@ void ActorViewerWindow::DrawElement() { ImGui::TreePop(); } + + static const char* nameTagOptions[] = { + "None", + "Short Description", + "Actor ID", + "Both" + }; + + UIWidgets::Spacer(0); + + ImGui::Text("Actor Name Tags"); + if (UIWidgets::EnhancementCombobox("gDebugActorViewerNameTags", nameTagOptions, ACTORVIEWER_NAMETAGS_NONE)) { + NameTag_RemoveAllByTag(DEBUG_ACTOR_NAMETAG_TAG); + ActorViewer_AddTagForAllActors(); + } + UIWidgets::Tooltip("Adds \"name tags\" above actors for identification"); } else { ImGui::Text("Global Context needed for actor info!"); if (needs_reset) { @@ -340,7 +403,13 @@ void ActorViewerWindow::DrawElement() { needs_reset = false; } } - ImGui::End(); } + +void ActorViewerWindow::InitElement() { + GameInteractor::Instance->RegisterGameHook([](void* refActor) { + Actor* actor = static_cast(refActor); + ActorViewer_AddTagForActor(actor); + }); +} diff --git a/soh/soh/Enhancements/debugger/actorViewer.h b/soh/soh/Enhancements/debugger/actorViewer.h index 319e9a969..6eceaa92d 100644 --- a/soh/soh/Enhancements/debugger/actorViewer.h +++ b/soh/soh/Enhancements/debugger/actorViewer.h @@ -7,6 +7,6 @@ class ActorViewerWindow : public LUS::GuiWindow { using GuiWindow::GuiWindow; void DrawElement() override; - void InitElement() override {}; + void InitElement() override; void UpdateElement() override {}; -}; \ No newline at end of file +}; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 193adb6c4..79a2cb465 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -151,9 +151,11 @@ public: DEFINE_HOOK(OnSceneSpawnActors, void()); DEFINE_HOOK(OnPlayerUpdate, void()); DEFINE_HOOK(OnOcarinaSongAction, void()); - + DEFINE_HOOK(OnActorInit, void(void* actor)); DEFINE_HOOK(OnActorUpdate, void(void* actor)); DEFINE_HOOK(OnPlayerBonk, void()); + DEFINE_HOOK(OnPlayDestroy, void()); + DEFINE_HOOK(OnPlayDrawEnd, void()); DEFINE_HOOK(OnSaveFile, void(int32_t fileNum)); DEFINE_HOOK(OnLoadFile, void(int32_t fileNum)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 7277d765f..e96474996 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -42,6 +42,10 @@ void GameInteractor_ExecuteOnOcarinaSongAction() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnActorInit(void* actor) { + GameInteractor::Instance->ExecuteHooks(actor); +} + void GameInteractor_ExecuteOnActorUpdate(void* actor) { GameInteractor::Instance->ExecuteHooks(actor); } @@ -50,6 +54,14 @@ void GameInteractor_ExecuteOnPlayerBonk() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnPlayDestroy() { + GameInteractor::Instance->ExecuteHooks(); +} + +void GameInteractor_ExecuteOnPlayDrawEnd() { + GameInteractor::Instance->ExecuteHooks(); +} + // MARK: - Save Files void GameInteractor_ExecuteOnSaveFile(int32_t fileNum) { diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index edff2f84b..c69a8ec24 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -14,9 +14,12 @@ void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum); void GameInteractor_ExecuteOnSceneSpawnActors(); void GameInteractor_ExecuteOnPlayerUpdate(); void GameInteractor_ExecuteOnOcarinaSongAction(); +void GameInteractor_ExecuteOnActorInit(void* actor); void GameInteractor_ExecuteOnActorUpdate(void* actor); void GameInteractor_ExecuteOnPlayerBonk(); void GameInteractor_ExecuteOnOcarinaSongAction(); +void GameInteractor_ExecuteOnPlayDestroy(); +void GameInteractor_ExecuteOnPlayDrawEnd(); // MARK: - Save Files void GameInteractor_ExecuteOnSaveFile(int32_t fileNum); diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index 2048ead31..6fc4a87f5 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -6,6 +6,7 @@ #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/randomizer/3drando/random.hpp" #include "soh/Enhancements/cosmetics/authenticGfxPatches.h" +#include "soh/Enhancements/nametag.h" extern "C" { #include @@ -621,4 +622,5 @@ void InitMods() { RegisterBonkDamage(); RegisterMenuPathFix(); RegisterMirrorModeHandler(); + NameTag_RegisterHooks(); } diff --git a/soh/soh/Enhancements/nametag.cpp b/soh/soh/Enhancements/nametag.cpp new file mode 100644 index 000000000..038ba4e1f --- /dev/null +++ b/soh/soh/Enhancements/nametag.cpp @@ -0,0 +1,313 @@ +#include "nametag.h" +#include +#include +#include +#include "soh/frame_interpolation.h" +#include "soh/Enhancements/custom-message/CustomMessageInterfaceAddon.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +#include "z64.h" +#include "macros.h" +#include "functions.h" +#include "variables.h" +#include "textures/message_static/message_static.h" +extern PlayState* gPlayState; +} + +typedef struct { + Actor* actor; + std::string text; // Original text + std::string processedText; // Text filtered for supported font textures + const char* tag; // Tag identifier + Color_RGBA8 textColor; // Text color override. Global color is used if alpha is 0 + int16_t height; // Textbox height + int16_t width; // Textbox width + int16_t yOffset; // Addition Y offset + Mtx* mtx; // Allocated Mtx for rendering + Vtx* vtx; // Allocated Vtx for rendering +} NameTag; + +static std::vector nameTags; +static std::vector nameTagDl; + +void FreeNameTag(NameTag* nameTag) { + if (nameTag->vtx != nullptr) { + free(nameTag->vtx); + nameTag->vtx = nullptr; + } + if (nameTag->mtx != nullptr) { + free(nameTag->mtx); + nameTag->mtx = nullptr; + } +} + +void DrawNameTag(PlayState* play, const NameTag* nameTag) { + if (nameTag->actor == nullptr || nameTag->actor->draw == nullptr || !nameTag->actor->isDrawn || + nameTag->vtx == nullptr || nameTag->mtx == nullptr) { + return; + } + + // Name tag is too far away to meaningfully read, don't bother rendering it + if (nameTag->actor->xyzDistToPlayerSq > 200000.0f) { + return; + } + + // Fade out name tags that are far away + float alpha = 1.0f; + if (nameTag->actor->xyzDistToPlayerSq > 160000.0f) { + alpha = (200000.0f - nameTag->actor->xyzDistToPlayerSq) / 40000.0f; + } + + float scale = 75.0f / 100.f; + + size_t numChar = nameTag->processedText.length(); + // No text to render + if (numChar == 0) { + return; + } + + Color_RGBA8 textboxColor = { 0, 0, 0, 80}; + Color_RGBA8 textColor = { 255, 255, 255, 255 }; + + if (CVarGetInteger("gCosmetics.Hud_NameTagActorBackground.Changed", 0)) { + textboxColor = CVarGetColor("gCosmetics.Hud_NameTagActorBackground.Value", textboxColor); + } + if (CVarGetInteger("gCosmetics.Hud_NameTagActorText.Changed", 0)) { + textColor = CVarGetColor("gCosmetics.Hud_NameTagActorText.Value", textColor); + } + + FrameInterpolation_RecordOpenChild(nameTag->actor, 10); + + // Prefer the highest between world position and focus position if targetable + float posY = nameTag->actor->world.pos.y; + if (nameTag->actor->flags & ACTOR_FLAG_TARGETABLE) { + posY = std::max(posY, nameTag->actor->focus.pos.y); + } + + posY += nameTag->yOffset + 16; + + // Set position, billboard effect, scale (with mirror mode), then center nametag + Matrix_Translate(nameTag->actor->world.pos.x, posY, nameTag->actor->world.pos.z, MTXMODE_NEW); + Matrix_ReplaceRotation(&play->billboardMtxF); + Matrix_Scale(scale * (CVarGetInteger("gMirroredWorld", 0) ? -1 : 1), -scale, 1.0f, MTXMODE_APPLY); + Matrix_Translate(-(float)nameTag->width / 2, -nameTag->height, 0, MTXMODE_APPLY); + Matrix_ToMtx(nameTag->mtx, (char*)__FILE__, __LINE__); + + nameTagDl.push_back(gsSPMatrix(nameTag->mtx, G_MTX_PUSH | G_MTX_LOAD | G_MTX_MODELVIEW)); + + // textbox + nameTagDl.push_back(gsSPVertex(nameTag->vtx, 4, 0)); + nameTagDl.push_back(gsDPSetPrimColor(0, 0, textboxColor.r, textboxColor.g, textboxColor.b, textboxColor.a * alpha)); + + // Multi-instruction macro, need to insert all to the dl buffer + Gfx textboxTexture[] = { gsDPLoadTextureBlock_4b(gDefaultMessageBackgroundTex, G_IM_FMT_I, 128, 64, 0, G_TX_MIRROR, + G_TX_NOMIRROR, 7, 0, G_TX_NOLOD, G_TX_NOLOD) }; + nameTagDl.insert(nameTagDl.end(), std::begin(textboxTexture), std::end(textboxTexture)); + + nameTagDl.push_back(gsSP1Quadrangle(0, 2, 3, 1, 0)); + + // text + if (nameTag->textColor.a == 0) { + nameTagDl.push_back(gsDPSetPrimColor(0, 0, textColor.r, textColor.g, textColor.b, textColor.a * alpha)); + } else { + nameTagDl.push_back(gsDPSetPrimColor(0, 0, nameTag->textColor.r, nameTag->textColor.g, nameTag->textColor.b, + nameTag->textColor.a * alpha)); + } + + for (size_t i = 0, vtxGroup = 0; i < numChar; i++) { + uint16_t texIndex = nameTag->processedText[i] - 32; + + // A maximum of 64 Vtx can be loaded at once by gSPVertex, or basically 16 characters + // handle loading groups of 16 chars at a time until there are no more left to load + if (i % 16 == 0) { + size_t numVtxToLoad = std::min(numChar - i, 16) * 4; + nameTagDl.push_back(gsSPVertex(&(nameTag->vtx)[4 + (vtxGroup * 16 * 4)], numVtxToLoad, 0)); + vtxGroup++; + } + + if (texIndex != 0 && nameTag->processedText[i] != '\n') { + uintptr_t texture = (uintptr_t)Font_FetchCharTexture(texIndex); + int16_t vertexStart = 4 * (i % 16); + + // Multi-instruction macro, need to insert all to the dl buffer + Gfx charTexture[] = { gsDPLoadTextureBlock_4b( + texture, G_IM_FMT_I, FONT_CHAR_TEX_WIDTH, FONT_CHAR_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_CLAMP, + G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD) }; + nameTagDl.insert(nameTagDl.end(), std::begin(charTexture), std::end(charTexture)); + + nameTagDl.push_back(gsSP1Quadrangle(vertexStart, vertexStart + 2, vertexStart + 3, vertexStart + 1, 0)); + } + } + + nameTagDl.push_back(gsSPPopMatrix(G_MTX_MODELVIEW)); + + FrameInterpolation_RecordCloseChild(); +} + +// Draw all the name tags by leveraging a system heap buffer for majority of the graphics commands +void DrawNameTags() { + if (gPlayState == nullptr || nameTags.size() == 0) { + return; + } + + nameTagDl.clear(); + + OPEN_DISPS(gPlayState->state.gfxCtx); + + // Setup before rendering name tags + Gfx_SetupDL_38Xlu(gPlayState->state.gfxCtx); + nameTagDl.push_back(gsDPSetAlphaDither(G_AD_DISABLE)); + nameTagDl.push_back(gsSPClearGeometryMode(G_SHADE)); + + nameTagDl.push_back( + gsDPSetCombineLERP(0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0)); + + // Add all the name tags + for (const auto& nameTag : nameTags) { + DrawNameTag(gPlayState, &nameTag); + } + + // End the display list buffer + nameTagDl.push_back(gsSPEndDisplayList()); + + gSPDisplayList(POLY_XLU_DISP++, nameTagDl.data()); + + CLOSE_DISPS(gPlayState->state.gfxCtx); +} + +void UpdateNameTags() { + // Leveraging ZBuffer above allows the name tags to be obscured by OPA surfaces based on depth. + // However, XLU surfaces do not calculate depth with regards to other XLU surfaces. + // With multiple name tags, a tag can only obscure other tags based on draw order. + // Here we sort the tags so that actors further away from the camera are ordered first. + std::sort(nameTags.begin(), nameTags.end(), [](NameTag a, NameTag b) { + if (a.actor == nullptr || b.actor == nullptr) { + return true; + } + + f32 aDistToCamera = Actor_WorldDistXZToPoint(a.actor, &gPlayState->mainCamera.eye); + f32 bDistToCamera = Actor_WorldDistXZToPoint(b.actor, &gPlayState->mainCamera.eye); + + return aDistToCamera > bDistToCamera; + }); +} + +extern "C" void NameTag_RegisterForActorWithOptions(Actor* actor, const char* text, NameTagOptions options) { + std::string processedText = std::string(Interface_ReplaceSpecialCharacters((char*)text)); + + // Strip out unsupported characters + processedText.erase(std::remove_if(processedText.begin(), processedText.end(), [](const char& c) { + // 172 is max supported texture for the in-game font system, + // and filter anything less than a space but not the newline or nul characters + return c > 172 || (c < ' ' && c != '\n' && c != '\0'); + }), processedText.end()); + + int16_t numChar = processedText.length(); + int16_t numLines = 1; + int16_t offsetX = 0; + int16_t maxOffsetX = 0; + + // 4 vertex per character plus one extra group of 4 for the textbox + Vtx* vertices = (Vtx*)calloc(sizeof(Vtx[4]), numChar + 1); + + // Set all the char vtx first to get the total size for the textbox + for (size_t i = 0; i < numChar; i++) { + if (processedText[i] == '\n') { + offsetX = 0; + numLines++; + } + + int16_t charIndex = processedText[i] - 32; + int16_t charWidth = 0; + // Don't add width for newline chars + if (charIndex >= 0) { + charWidth = (int16_t)(Message_GetCharacterWidth(charIndex) * (100.0f / R_TEXT_CHAR_SCALE)); + } + + Interface_CreateQuadVertexGroup(&(vertices)[(i + 1) * 4], offsetX, (numLines - 1) * 16, charWidth, 16, 0); + offsetX += charWidth; + + if (offsetX > maxOffsetX) { + maxOffsetX = offsetX; + } + } + + // Vtx for textbox, add +/- 4 in all directions + int16_t height = (numLines * 16) + 8; + int16_t width = maxOffsetX + 8; + Interface_CreateQuadVertexGroup(vertices, -4, -4, width, height, 0); + + // Update the texture coordinates to consume the full textbox texture size (including mirror region) + vertices[1].v.tc[0] = 256 << 5; + vertices[2].v.tc[1] = 64 << 5; + vertices[3].v.tc[0] = 256 << 5; + vertices[3].v.tc[1] = 64 << 5; + + NameTag nameTag; + nameTag.actor = actor; + nameTag.text = std::string(text); + nameTag.processedText = processedText; + nameTag.tag = options.tag; + nameTag.textColor = options.textColor; + nameTag.height = height; + nameTag.width = width; + nameTag.yOffset = options.yOffset; + nameTag.mtx = new Mtx(); + nameTag.vtx = vertices; + + nameTags.push_back(nameTag); +} + +extern "C" void NameTag_RegisterForActor(Actor* actor, const char* text) { + NameTag_RegisterForActorWithOptions(actor, text, {}); +} + +extern "C" void NameTag_RemoveAllForActor(Actor* actor) { + for (auto it = nameTags.begin(); it != nameTags.end();) { + if (it->actor == actor) { + FreeNameTag(&(*it)); + it = nameTags.erase(it); + } else { + it++; + } + } +} + +extern "C" void NameTag_RemoveAllByTag(const char* tag) { + for (auto it = nameTags.begin(); it != nameTags.end();) { + if (it->tag != nullptr && strcmp(it->tag, tag) == 0) { + FreeNameTag(&(*it)); + it = nameTags.erase(it); + } else { + it++; + } + } +} + +void RemoveAllNameTags() { + for (auto& nameTag : nameTags) { + FreeNameTag(&nameTag); + } + + nameTags.clear(); +} + +static bool sRegisteredHooks = false; + +void NameTag_RegisterHooks() { + if (sRegisteredHooks) { + return; + } + + sRegisteredHooks = true; + + // Reorder tags every frame to mimic depth rendering + GameInteractor::Instance->RegisterGameHook([]() { UpdateNameTags(); }); + + // Render name tags at the end of player draw to avoid overflowing the display buffers + GameInteractor::Instance->RegisterGameHook([]() { DrawNameTags(); }); + + // Remove all name tags on play state destroy as all actors are removed anyways + GameInteractor::Instance->RegisterGameHook([]() { RemoveAllNameTags(); }); +} diff --git a/soh/soh/Enhancements/nametag.h b/soh/soh/Enhancements/nametag.h new file mode 100644 index 000000000..9fd26643e --- /dev/null +++ b/soh/soh/Enhancements/nametag.h @@ -0,0 +1,31 @@ +#ifndef _NAMETAG_H_ +#define _NAMETAG_H_ +#include + +typedef struct { + const char* tag; // Tag identifier to filter/remove multiple tags + int16_t yOffset; // Additional Y offset to apply for the name tag + Color_RGBA8 textColor; // Text color override. Global color is used if alpha is 0 +} NameTagOptions; + +// Register required hooks for nametags on startup +void NameTag_RegisterHooks(); + +#ifdef __cplusplus +extern "C" { +#endif + +// Registers a name tag to an actor with additional options applied +void NameTag_RegisterForActorWithOptions(Actor* actor, const char* text, NameTagOptions options); +// Registers a name tag to an actor. Multiple name tags can exist for the same actor +void NameTag_RegisterForActor(Actor* actor, const char* text); +// Remove all name tags registered to a specific actor +void NameTag_RemoveAllForActor(Actor* actor); +// Remove all name tags that share the same tag identifier +void NameTag_RemoveAllByTag(const char* tag); + +#ifdef __cplusplus +} +#endif + +#endif // _NAMETAG_H_ diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index fd44a3abd..796fc6d71 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -10,6 +10,7 @@ #include "soh/Enhancements/enemyrandomizer.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/Enhancements/nametag.h" #include "soh/ActorDB.h" @@ -1213,6 +1214,8 @@ void Actor_Init(Actor* actor, PlayState* play) { actor->init(actor, play); actor->init = NULL; + GameInteractor_ExecuteOnActorInit(actor); + // For enemy health bar we need to know the max health during init if (actor->category == ACTORCAT_ENEMY) { actor->maximumHealth = actor->colChkInfo.health; @@ -1228,6 +1231,8 @@ void Actor_Destroy(Actor* actor, PlayState* play) { // "No Actor class destruct [%s]" osSyncPrintf("Actorクラス デストラクトがありません [%s]\n" VT_RST, ActorDB_Retrieve(actor->id)->name); } + + NameTag_RemoveAllForActor(actor); } void func_8002D7EC(Actor* actor) { diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index dc5b890ec..beef71e9c 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -171,6 +171,7 @@ void Play_Destroy(GameState* thisx) { PlayState* play = (PlayState*)thisx; Player* player = GET_PLAYER(play); + GameInteractor_ExecuteOnPlayDestroy(); // Only initialize the frame counter when exiting the title screen if (gSaveContext.fileNum == 0xFF) { @@ -1713,6 +1714,8 @@ void Play_Draw(PlayState* play) { } } + GameInteractor_ExecuteOnPlayDrawEnd(); + // Reset the inverted culling if (CVarGetInteger("gMirroredWorld", 0)) { gSPClearExtraGeometryMode(POLY_OPA_DISP++, G_EX_INVERT_CULLING);