#include "NameTag.h" #include #include #include #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/ShipUtils.h" extern "C" { #include "z64actor.h" #include "macros.h" #include "functions.h" #include "variables.h" #include "interface/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 uint8_t noZBuffer; // Allow rendering over geometry Mtx* mtx; // Allocated Mtx for rendering Vtx* vtx; // Allocated Vtx for rendering } NameTag; static std::vector nameTags; static std::vector nameTagDl; static bool sMirrorWorldActive = false; void NameTag_RegisterHooks(); 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 > 440000.0f) { return; } // Fade out name tags that are far away float alpha = 1.0f; if (nameTag->actor->xyzDistToPlayerSq > 360000.0f) { alpha = (440000.0f - nameTag->actor->xyzDistToPlayerSq) / 80000.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 }; // BENTODO: Cosmetic editor support // if (CVarGetInteger(CVAR_COSMETIC("HUD.NameTagActorBackground.Changed"), 0)) { // textboxColor = CVarGetColor(CVAR_COSMETIC("HUD.NameTagActorBackground.Value"), textboxColor); // } // if (CVarGetInteger(CVAR_COSMETIC("HUD.NameTagActorText.Changed"), 0)) { // textColor = CVarGetColor(CVAR_COSMETIC("HUD.NameTagActorText.Value"), textColor); // } FrameInterpolation_RecordOpenChild(nameTag->actor, 10000); // Prefer the highest between world position and focus position if targetable float posY = nameTag->actor->world.pos.y; if (nameTag->actor->flags & ACTOR_FLAG_ATTENTION_ENABLED) { 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 * (sMirrorWorldActive ? -1.0f : 1.0f), -scale, 1.0f, MTXMODE_APPLY); Matrix_Translate(-(float)nameTag->width / 2, -nameTag->height, 0, MTXMODE_APPLY); Matrix_ToMtx(nameTag->mtx); 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(gMessageDefaultBackgroundTex, 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++) { // 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++; } TexturePtr texture = Ship_GetCharFontTextureNES(nameTag->processedText[i]); 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 POLY_XLU_DISP = Gfx_SetupDL39(POLY_XLU_DISP); nameTagDl.push_back(gsDPSetAlphaCompare(G_AC_NONE)); nameTagDl.push_back( gsDPSetCombineLERP(0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0)); bool zbufferEnabled = false; // Add all the name tags for (const auto& nameTag : nameTags) { // Toggle ZBuffer mode based on last state and next tag if (zbufferEnabled == nameTag.noZBuffer) { if (nameTag.noZBuffer) { nameTagDl.push_back(gsSPClearGeometryMode(G_ZBUFFER)); zbufferEnabled = false; } else { nameTagDl.push_back(gsSPSetGeometryMode(G_ZBUFFER)); zbufferEnabled = true; } } 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; }); sMirrorWorldActive = CVarGetInteger("gModes.MirroredWorld.State", 0); } extern "C" void NameTag_RegisterForActorWithOptions(Actor* actor, const char* text, NameTagOptions options) { // BENTODO: Handle mapping UTF-8 special chars to font equivalent std::string processedText = std::string(text); // Strip out unsupported characters // 188 is max supported texture for the in-game font system, // and filter anything less than a space but not the newline or nul characters processedText.erase( std::remove_if(processedText.begin(), processedText.end(), [](const char& c) { return (uint8_t)c > 188 || (c < ' ' && c != '\n' && c != '\0'); }), processedText.end()); size_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 charWidth = Ship_GetCharFontWidthNES(processedText[i]); Ship_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; Ship_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.noZBuffer = options.noZBuffer; nameTag.mtx = new Mtx(); nameTag.vtx = vertices; nameTags.push_back(nameTag); NameTag_RegisterHooks(); } 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++; } } NameTag_RegisterHooks(); } 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++; } } NameTag_RegisterHooks(); } void RemoveAllNameTags() { for (auto& nameTag : nameTags) { FreeNameTag(&nameTag); } nameTags.clear(); NameTag_RegisterHooks(); } void NameTag_RegisterHooks() { static HOOK_ID gameStatUpdateHookID = 0; static HOOK_ID drawHookID = 0; static HOOK_ID playDestroyHookID = 0; static HOOK_ID actorDestroyHookID = 0; static bool sRegisteredHooks = false; // Hooks already (un)registered based on nametags if ((nameTags.size() > 0) == sRegisteredHooks) { return; } GameInteractor::Instance->UnregisterGameHook(gameStatUpdateHookID); GameInteractor::Instance->UnregisterGameHook(drawHookID); GameInteractor::Instance->UnregisterGameHook(playDestroyHookID); GameInteractor::Instance->UnregisterGameHook(actorDestroyHookID); gameStatUpdateHookID = 0; drawHookID = 0; playDestroyHookID = 0; actorDestroyHookID = 0; sRegisteredHooks = false; if (nameTags.size() == 0) { return; } sRegisteredHooks = true; // Reorder tags every frame to mimic depth rendering gameStatUpdateHookID = GameInteractor::Instance->RegisterGameHook(UpdateNameTags); // Render name tags at the end of the Play World drawing drawHookID = GameInteractor::Instance->RegisterGameHook(DrawNameTags); // Remove all name tags on play state destroy as all actors are removed anyways playDestroyHookID = GameInteractor::Instance->RegisterGameHook(RemoveAllNameTags); // Remove all name tags for actor on destroy actorDestroyHookID = GameInteractor::Instance->RegisterGameHook(NameTag_RemoveAllForActor); }