summaryrefslogtreecommitdiff
path: root/src/engine/actors/Text.cpp
diff options
context:
space:
mode:
authorMegaMech <MegaMech@users.noreply.github.com>2025-11-09 19:07:44 -0700
committerGitHub <noreply@github.com>2025-11-09 19:07:44 -0700
commitde9c5d510175bba11ab8704e67be3a3a94f9149e (patch)
tree34c42dbfe9f2a6eb5fbce5f6a31b532ff2a3f243 /src/engine/actors/Text.cpp
parent760fa3bf5226c2a6052a5d4d53fd0e27f2911e60 (diff)
Implement SpawnParams struct (#536)
* Impl SpawnParams * Added json submodule * Update json * Update * Works * Remove comment * Works refactor * Snowman and thwomp working * Impl hot air balloon * More progress * All OObjects are done * cleanup * Refactor 2Dpath to normal path * Update nlohmann json & fix compile * Rest of actors * MORE CHANGES * Finish actors * Done PR, some fix to collision viewer * Impl falling rocks * Add const * wip editor refactor * Property work * continue * Overridable editor properties * Actor saving/loading works now * Fix light alignment * Clarification * Impl penguin * params impl signs * properties impl falling rock * More property impls * impl air balloon * Add spawnParams to OObject Translate * Snowman translate better * impl hedgehog properly * properties impl trophy * thwomp progress * Finish impl properties * Fix compile * Fix cursor collisions * Move registered actors * Rename pathPoint XYZ to xyz * Fix editor pause bug * Clean up * Review comments * Remove SpawnParams struct from actor classes * Rename * Player Label First Iteration * Work now * Working 3d text * Fix boo bug * Finish AText actor * Fix spawnparams compile * Register AText * Finish Text Actor * Fix thwomp interpolation * Fix compile * Fix crab and hedgehog * Fix loading flagpole * Fix Hot Air Balloon * Turn zbuffer on for AText * Update --------- Co-authored-by: MegaMech <7255464+MegaMech@users.noreply.github.com>
Diffstat (limited to 'src/engine/actors/Text.cpp')
-rw-r--r--src/engine/actors/Text.cpp588
1 files changed, 588 insertions, 0 deletions
diff --git a/src/engine/actors/Text.cpp b/src/engine/actors/Text.cpp
new file mode 100644
index 000000000..2d2fab0b0
--- /dev/null
+++ b/src/engine/actors/Text.cpp
@@ -0,0 +1,588 @@
+#include <libultraship.h>
+#include <libultra/gbi.h>
+#include <vector>
+
+#include "Text.h"
+
+#include "port/interpolation/FrameInterpolation.h"
+#include "engine/Matrix.h"
+#include "engine/editor/EditorMath.h"
+
+extern "C" {
+#include "defines.h"
+#include "main.h"
+#include "menu_items.h"
+#include "assets/data_segment2.h"
+#include "render_player.h"
+#include "math_util.h"
+#include "assets/texture_data_2.h"
+#include "render_objects.h"
+#include "common_structs.h"
+#include "code_80005FD0.h"
+}
+
+AText::AText(const SpawnParams& params) : AActor(params) {
+ Name = "Text";
+ ResourceName = "hm:text";
+
+ SpawnPos = params.Location.value_or(FVector(0.0f, 100.0f, 0.0f));
+ Pos[0] = SpawnPos.x;
+ Pos[1] = SpawnPos.y;
+ Pos[2] = SpawnPos.z;
+
+ SpawnRot = params.Rotation.value_or(IRotator(0, 0, 0));
+ Rot[0] = SpawnRot.pitch;
+ Rot[1] = SpawnRot.yaw;
+ Rot[2] = SpawnRot.roll;
+
+ SpawnScale = params.Scale.value_or(FVector(1.0f, 1.0f, 1.0f));
+ Scale = SpawnScale;
+
+ ScaleX = params.FVec2.value_or(FVector{1.0f, 0.0f, 0.0f}).x;
+
+ Mode = static_cast<TextMode>(params.Type.value_or(0)); // STATIONARY
+
+ Animate = params.Bool.value_or(false);
+ FaceCamera = params.Bool2.value_or(true);
+
+ WidthOffset = params.Speed.value_or(0.0f);
+ HeightOffset = params.SpeedB.value_or(8.0f);
+
+ FVector options = params.Velocity.value_or(FVector{1.0f, 14000.0f, 0.0f});
+ LetterSpacing = options.x;
+ Far = options.y;
+ Close = options.z;
+
+ TextColour[0] = params.Colour.value_or(RGBA8{255, 255, 255, 255});
+ TextColour[1] = params.Colour2.value_or(RGBA8{255, 255, 255, 255});
+ TextColour[2] = params.Colour3.value_or(RGBA8{255, 255, 255, 255});
+ TextColour[3] = params.Colour4.value_or(RGBA8{255, 255, 255, 255});
+
+ PlayerIndex = static_cast<uint32_t>(params.Behaviour.value_or(0));
+ if (PlayerIndex < 0 || PlayerIndex >= NUM_PLAYERS) {
+ PlayerIndex = 0;
+ }
+
+ Text = ValidateString(params.Skin.value_or("Harbour Masters"));
+ AText::Print3D((char*)Text.c_str(), 0, CENTER_TEXT_MODE_2);
+}
+
+/**
+ * Filters out bad characters (allows a-z, A-Z, 0-9, space)
+ * Returns "Blank Text" for blank input
+ * Returns "Invalid" if no valid input found
+ * Limits str to 20 characters
+ *
+ * The font does support some symbols and other language characters
+ * But these need to be checked thoroughly before white-listing.
+ */
+std::string AText::ValidateString(const std::string_view& s) {
+ if (s.empty()) { return "Blank Text"; }
+
+ Text.clear();
+
+ for (char c : s) {
+ if (std::isalpha(static_cast<unsigned char>(c)) ||
+ std::isdigit(static_cast<unsigned char>(c)) ||
+ c == ' ')
+ {
+ Text += c;
+ if (Text.size() >= 20) {
+ break;
+ };
+ }
+ }
+
+ // No valid characters found
+ if (Text.empty()) {
+ return "Invalid";
+ }
+ return Text;
+}
+
+/*
+ * Most changes during runtime require a refresh because the text is generated statically
+ * with the intention of this code being somewhat performant
+ */
+void AText::Refresh() {
+ AText::TextureList.clear();
+ AText::Print3D((char*)Text.c_str(), 0, CENTER_TEXT_MODE_2);
+}
+
+bool AText::IsMod() { return true; }
+
+void AText::SetSpawnParams(SpawnParams& params) {
+ AActor::SetSpawnParams(params);
+ params.Name = ResourceName;
+ params.Type = static_cast<int16_t>(Mode);
+ params.Behaviour = PlayerIndex;
+ params.Skin = Text;
+
+ params.Colour = TextColour[0];
+ params.Colour2 = TextColour[1];
+ params.Colour3 = TextColour[2];
+ params.Colour4 = TextColour[3];
+
+ params.Speed = WidthOffset;
+ params.SpeedB = HeightOffset;
+
+ params.Velocity = {LetterSpacing, Far, Close};
+ params.FVec2 = {ScaleX, 0.0f, 0.0f};
+
+ params.Bool = Animate;
+ params.Bool2 = FaceCamera;
+}
+
+void AText::Tick() {
+ switch(Mode) {
+ case STATIONARY:
+ break; // Do nothing
+ case FOLLOW_PLAYER:
+ AText::FollowPlayer();
+ break;
+ }
+}
+
+void AText::FollowPlayer() {
+ Pos[0] = gPlayers[PlayerIndex].pos[0] + WidthOffset;
+ Pos[1] = gPlayers[PlayerIndex].pos[1] + HeightOffset;
+ Pos[2] = gPlayers[PlayerIndex].pos[2];
+
+ // Animate text if player is in first place
+ // if (gGPCurrentRaceRankByPlayerId[PlayerIndex] == 0) {
+ // Animate = true;
+ // } else {
+ // Animate = false;
+ // }
+}
+
+void AText::Draw(Camera* camera) {
+ switch(Mode) {
+ case STATIONARY:
+ break; // Do nothing
+ case FOLLOW_PLAYER:
+ if (PlayerIndex == camera->playerId) {
+ return; // Do not draw the local players own name
+ }
+ if ((gPlayers[PlayerIndex].effects & BOO_EFFECT) == BOO_EFFECT) {
+ FadeState = FADE_OUT;
+ AText::DrawText3D(camera);
+ return; // Skip expensive calculations below
+ }
+ break;
+ }
+
+ f32 distance = is_within_render_distance(camera->pos, (float*)&Pos[0], camera->rot[1], Close,
+ gCameraZoom[camera - camera1], Far);
+
+ if (distance == -1.0f) {
+ Dist = DistanceProps::TOO_FAR;
+ } else if (distance < Close) {
+ Dist = DistanceProps::TOO_CLOSE;
+ } else {
+ Dist = DistanceProps::ACTIVE;
+ }
+
+ if (Dist != PrevState) {
+ PrevState = Dist;
+ if ((distance == -1.0f) || (distance < Close)) {
+ FadeState = FADE_OUT;
+ } else {
+ FadeState = FADE_IN;
+ }
+ }
+
+ AText::DrawText3D(camera);
+}
+
+/**
+ * These have been refactored for efficiency purposes.
+ * The new method uses 1 matrix to display the whole string
+ * And then setting vertex data is done during the setup/constructor phase,
+ * instead of during rendering
+ * This requires a refresh if the data ever changes
+ *
+ * This method is more efficient because the original version does all this work
+ * during the render phase. Now it's done during the actor spawn phase with the exception
+ * if any changes are made at runtime.
+ */
+void AText::Print3D(char* text, s32 tracking, s32 mode) {
+ char* temp_string = text;
+ s32 stringWidth = 0;
+ s32 glyphIndex;
+ s32 sp60;
+
+ s32 column = 0;
+ s32 row = 0;
+
+ if (text == NULL) {
+ // @port if invalid text is loaded it will skip rendering it.
+ return;
+ }
+
+ while (*temp_string != '\0') {
+ glyphIndex = char_to_glyph_index(temp_string);
+ if (glyphIndex >= 0) {
+ stringWidth += ((gGlyphDisplayWidth[glyphIndex] + tracking) * ScaleX);
+ } else if ((glyphIndex != -2) && (glyphIndex == -1)) {
+ stringWidth += ((tracking + 7) * ScaleX);
+ } else {
+ return;
+ }
+ if (glyphIndex >= 0x30) {
+ temp_string += 2;
+ } else {
+ temp_string += 1;
+ }
+ }
+
+ switch (mode) {
+ case LEFT_TEXT:
+ //! FAKE:
+ do {
+ } while (0);
+ case RIGHT_TEXT:
+ column -= stringWidth;
+ break;
+ case CENTER_TEXT_MODE_1:
+ case CENTER_TEXT_MODE_2:
+ column -= stringWidth / 2;
+ break;
+ default:
+ break;
+ }
+
+ if (mode < 3) {
+ sp60 = 1;
+ } else {
+ sp60 = 2;
+ }
+
+ while (*text != '\0') {
+ glyphIndex = char_to_glyph_index(text);
+ if (glyphIndex >= 0) {
+ AText::PrintLetter3D(gGlyphTextureLUT[glyphIndex],
+ column, row, sp60);
+ column = column + (s32) ((gGlyphDisplayWidth[glyphIndex] + tracking) * ScaleX);
+ } else if ((glyphIndex != -2) && (glyphIndex == -1)) {
+ column = column + (s32) ((tracking + 7) * ScaleX);
+ } else {
+ return;
+ }
+ if (glyphIndex >= 0x30) {
+ text += 2;
+ } else {
+ text += 1;
+ }
+ }
+ SetupVtx(); // position each letter
+}
+
+void AText::PrintLetter3D(MenuTexture* glyphTexture, f32 column, f32 row, s32 mode) {
+ s32 var_v0;
+ u8* temp_v0_2;
+ f32 thing0;
+ f32 thing1;
+ MenuTexture* texture;
+
+ texture = glyphTexture;
+ while (texture->textureData != NULL) {
+ if (texture->textureData != 0) {
+ f32 col = texture->dX + column;
+
+ TextureList.emplace_back(CharacterList{
+ (const char*) texture->textureData,
+ col,
+ texture->dY + row,
+ texture->width,
+ texture->height,
+ mode,
+ });
+ }
+ texture++;
+ }
+}
+
+void AText::SetupVtx() {
+ for (CharacterList& character : TextureList) {
+
+ Vtx* vtxPtr;
+ switch (character.width) {
+ default:
+ vtxPtr = (Vtx*)&AText::myVtx[4];
+ break;
+ case 16:
+ vtxPtr = (Vtx*)&AText::myVtx[4];
+ break;
+ case 26:
+ vtxPtr = (Vtx*)&AText::myVtx[0];
+ break;
+ case 30:
+ vtxPtr = (Vtx*)&AText::myVtx[8];
+ break;
+ }
+ // printf("col %f width %d span %d\n", character.column, character.width, character.span);
+ // memcpy the vtx data into the unique vtx data for this letter
+ Vtx* vtxSrc = (Vtx*)vtxPtr;
+ memcpy(&character.vtx, vtxSrc, sizeof(Vtx) * 4);
+
+ for (size_t i = 0; i < 4; i++) {
+ // Set the location for this letter (beside the previous letter) center the text over the anchor point
+ float span = (character.column * LetterSpacing);
+ character.vtx[i].v.ob[0] += (s16)span;
+
+ // Set the colour for this letter
+ character.vtx[i].v.cn[0] = TextColour[i].r;
+ character.vtx[i].v.cn[1] = TextColour[i].g;
+ character.vtx[i].v.cn[2] = TextColour[i].b;
+ character.vtx[i].v.cn[3] = TextColour[i].a;
+ }
+ }
+}
+
+void AText::DrawText3D(Camera* camera) { // Based on func_80095BD0
+ Mat4 mtx;
+
+ if (FaceCamera) {
+ ApplySphericalBillBoard(mtx, *(FVector*)Pos, Scale, camera->cameraId);
+ } else {
+ ApplyMatrixTransformations(mtx, *(FVector*)Pos, *(IRotator*)Rot, Scale);
+ }
+
+ AddObjectMatrix(mtx, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW);
+
+ FrameInterpolation_RecordOpenChild("actor_text", TAG_LETTER(this));
+ gSPDisplayList(gDisplayListHead++, (Gfx*)D_020077A8);
+ switch (1) {
+ case 1:
+ gSPDisplayList(gDisplayListHead++, (Gfx*)D_020077F8);
+ break;
+ case 2:
+ gSPDisplayList(gDisplayListHead++, (Gfx*)D_02007818);
+ break;
+ }
+
+ for (CharacterList& tex : TextureList) {
+ //printf("tex texture %p width %d height %d mode %d col %f\n", tex.Texture, tex.width, tex.height, tex.mode, tex.column);
+ gDPLoadTextureTile_4b(gDisplayListHead++, (Gfx*)tex.Texture, G_IM_FMT_I, tex.width, 0, 0, 0, tex.width, tex.height + 2, 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);
+
+ // gSPClearGeometryMode(gDisplayListHead++, G_ZBUFFER);
+
+ if (Animate) {
+ AnimateColour(tex.vtx);
+ }
+
+ switch(FadeState) {
+ case FADE_IN:
+ AText::FadeIn(tex.vtx);
+ break;
+ case FADE_OUT:
+ AText::FadeOut(tex.vtx);
+ break;
+ }
+
+ gSPVertex(gDisplayListHead++, (uintptr_t)tex.vtx, 4, 0);
+ gSP2Triangles(gDisplayListHead++, 0, 2, 1, 0, 0, 3, 2, 0);
+
+ // gSPSetGeometryMode(gDisplayListHead++, G_ZBUFFER);
+ }
+
+ gSPDisplayList(gDisplayListHead++, (Gfx*)D_020077D8);
+ FrameInterpolation_RecordCloseChild();
+}
+
+void AText::AnimateColour(Vtx* vtx) {
+ u8 r = (sin(gGlobalTimer * 0.1f) * 0.5f + 0.5f) * 200;
+ u8 g = (sin(gGlobalTimer * 0.1f + 2.0f) * 0.5f + 0.5f) * 200;
+ u8 b = (sin(gGlobalTimer * 0.1f + 4.0f) * 0.5f + 0.5f) * 200;
+
+ for (size_t i = 0; i < 4; i++) {
+ vtx[i].v.cn[0] = r;
+ vtx[i].v.cn[1] = g;
+ vtx[i].v.cn[2] = b;
+ }
+}
+
+#define fadeSpeed 16
+void AText::FadeIn(Vtx* vtx) {
+ uint8_t alpha = vtx[0].v.cn[3];
+ if (alpha + fadeSpeed > 255) {
+ alpha = 255;
+ FadeState = NO_FADE;
+ } else {
+ alpha += fadeSpeed;
+ }
+
+ // Apply alpha to all 4 vertices
+ for (size_t i = 0; i < 4; i++) {
+ vtx[i].v.cn[3] = alpha;
+ }
+}
+
+void AText::FadeOut(Vtx* vtx) {
+ uint8_t alpha = vtx[0].v.cn[3];
+ if (alpha < fadeSpeed) {
+ alpha = 0;
+ FadeState = NO_FADE;
+ } else {
+ alpha -= fadeSpeed;
+ }
+
+ // Apply alpha to all 4 vertices
+ for (size_t i = 0; i < 4; i++) {
+ vtx[i].v.cn[3] = alpha;
+ }
+}
+#undef fadeSpeed
+
+void AText::DrawEditorProperties() {
+ bool updated = false;
+ ImGui::Text("Text");
+ ImGui::SameLine();
+
+ char text[21] = "";
+ strncpy(text, Text.c_str(), sizeof(text));
+ ImGui::InputText("Enter text", text, IM_ARRAYSIZE(text));
+
+ Text = std::string(text);
+
+ ImGui::Text("Mode");
+ ImGui::SameLine();
+
+ int32_t mode = static_cast<int32_t>(Mode);
+ const char* items[] = { "STATIONARY", "FOLLOW PLAYER" };
+
+ if (ImGui::Combo("##Type", &mode, items, IM_ARRAYSIZE(items))) {
+ Mode = static_cast<TextMode>(mode);
+ updated = true;
+ }
+
+ ImGui::DragFloat("Far Render Dist", &Far);
+ ImGui::DragFloat("Close Render Dist", &Close);
+
+ ImGui::Checkbox("Face Camera", &FaceCamera);
+ if (!FaceCamera) {
+ ImGui::Text("Rotation");
+ ImGui::SameLine();
+
+ IRotator objRot = GetRotation();
+
+ // Convert to temporary int values (to prevent writing 32bit values to 16bit variables)
+ int rot[3] = {
+ objRot.pitch,
+ objRot.yaw,
+ objRot.roll
+ };
+
+ if (ImGui::DragInt3("##Rotation", rot, 5.0f)) {
+ for (size_t i = 0; i < 3; i++) {
+ // Wrap around 0–65535
+ rot[i] = (rot[i] % 65536 + 65536) % 65536;
+ }
+ IRotator newRot;
+ newRot.Set(
+ static_cast<uint16_t>(rot[0]),
+ static_cast<uint16_t>(rot[1]),
+ static_cast<uint16_t>(rot[2])
+ );
+ Rotate(newRot);
+ }
+ }
+
+ switch(mode) {
+ case STATIONARY: {
+ ImGui::Text("Location");
+ ImGui::SameLine();
+ FVector location = GetLocation();
+ if (ImGui::DragFloat3("##Location", (float*)&location)) {
+ Translate(location);
+ gEditor.eObjectPicker.eGizmo.Pos = location;
+ }
+ break;
+ }
+ case FOLLOW_PLAYER:
+ // Allow setting PlayerIndex
+ int32_t playerIdx = PlayerIndex + 1;
+
+ // Draw the input box (ImGui input limited between 1 and 8)
+ ImGui::SetNextItemWidth(100);
+ if (ImGui::InputInt("Follow Player", &playerIdx)) {
+ // Clamp display value to [1, 8]
+ if (playerIdx < 1) playerIdx = 1;
+ if (playerIdx > 8) playerIdx = 8;
+
+ // Update the internal value (0–7)
+ PlayerIndex = playerIdx - 1;
+ }
+
+ ImGui::DragFloat("Width Offset", &WidthOffset);
+ ImGui::DragFloat("Height Offset", &HeightOffset);
+ break;
+ }
+
+ DrawColourEditor(&updated);
+
+ ImGui::Text("Transform Settings");
+ ImGui::Separator();
+
+ ImGui::SetNextItemWidth(100);
+ ImGui::DragFloat("Scale X", &ScaleX, 0.1f, -5.0f, 5.0f, "%.2f");
+
+ ImGui::SetNextItemWidth(100);
+ if (ImGui::DragFloat("Letter Spacing", &LetterSpacing, 0.1f, 0.0f, 5.0f, "%.2f")) {
+ updated = true;
+ }
+
+ if (updated) {
+ Refresh();
+ }
+}
+
+void AText::DrawColourEditor(bool* updated) {
+ ImGui::Checkbox("Use single colour", &SingleColour);
+
+ if (SingleColour)
+ {
+ // Convert 8bit colours to float
+ ImVec4 colour(
+ TextColour[0].r / 255.0f,
+ TextColour[0].g / 255.0f,
+ TextColour[0].b / 255.0f,
+ TextColour[0].a / 255.0f
+ );
+
+ // Single color input
+ ImGui::ColorEdit4("Colour", (float*)&colour);
+ // Apply same color to all vertices
+ for (int i = 0; i < 4; i++) {
+ TextColour[i].r = FloatToU8(colour.x);
+ TextColour[i].g = FloatToU8(colour.y);
+ TextColour[i].b = FloatToU8(colour.z);
+ TextColour[i].a = FloatToU8(colour.w);
+ }
+ *updated = true;
+
+ } else {
+ // Separate color pickers for each vertex
+ for (int i = 0; i < 4; i++)
+ {
+ ImVec4 colour2(
+ TextColour[i].r / 255.0f,
+ TextColour[i].g / 255.0f,
+ TextColour[i].b / 255.0f,
+ TextColour[i].a / 255.0f
+ );
+ char label[32];
+ snprintf(label, sizeof(label), "Vtx %d Colour", i);
+ if (ImGui::ColorEdit4(label, (float*)&colour2)) {
+ TextColour[i].r = FloatToU8(colour2.x);
+ TextColour[i].g = FloatToU8(colour2.y);
+ TextColour[i].b = FloatToU8(colour2.z);
+ TextColour[i].a = FloatToU8(colour2.w);
+ *updated = true;
+ }
+ }
+ }
+} \ No newline at end of file