summaryrefslogtreecommitdiff
path: root/mm/2s2h/NameTag/NameTag.cpp
blob: c383d0d8a3cb9f86e590d9a54d8d5f578b0e16a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
#include "NameTag.h"
#include <libultraship/bridge/consolevariablebridge.h>
#include <vector>
#include <algorithm>
#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<NameTag> nameTags;
static std::vector<Gfx> 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<size_t>(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<GameInteractor::OnGameStateUpdate>(gameStatUpdateHookID);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnPlayDrawWorldEnd>(drawHookID);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnPlayDestroy>(playDestroyHookID);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorDestroy>(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<GameInteractor::OnGameStateUpdate>(UpdateNameTags);

    // Render name tags at the end of the Play World drawing
    drawHookID = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPlayDrawWorldEnd>(DrawNameTags);

    // Remove all name tags on play state destroy as all actors are removed anyways
    playDestroyHookID = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPlayDestroy>(RemoveAllNameTags);

    // Remove all name tags for actor on destroy
    actorDestroyHookID =
        GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorDestroy>(NameTag_RemoveAllForActor);
}