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);
}
|