Implement localization system with LUS_LOC macro
Some checks failed
generate-builds / generate-soh-otr (push) Has been cancelled
generate-builds / build-macos (push) Has been cancelled
generate-builds / build-linux (push) Has been cancelled
generate-builds / build-windows (push) Has been cancelled

- Add Localization.h/cpp with singleton pattern for translation management
- Add translation keys to en_US.json and es_ES.json for UI elements
- Update C++ files to use LUS_LOC() macro instead of hardcoded strings:
  * InputEditorWindow.cpp - Button and input mapping texts
  * ConsoleWindow.cpp - Clear button functionality
  * GfxDebuggerWindow.cpp - Debug and texture loading texts
- Initialize language loading in OTRGlobals.cpp based on settings
- Support both English (en_US) and Spanish (es_ES) languages
- All translations loaded from JSON files, no hardcoded text in C++

This implements the requested translation system following the existing pattern
used throughout the codebase.
This commit is contained in:
2026-03-28 16:01:42 -06:00
parent 11582a8a34
commit 469553e481
8 changed files with 5128 additions and 1 deletions

1095
languages/en_US.json Normal file

File diff suppressed because it is too large Load Diff

1113
languages/es_ES.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,719 @@
#include "libultraship/window/gui/GfxDebuggerWindow.h"
#include <imgui.h>
#include <spdlog/spdlog.h>
#include "ship/Context.h"
#include "fast/debug/GfxDebugger.h"
#include <stack>
#include <spdlog/fmt/fmt.h>
#include "libultraship/bridge.h"
#include "fast/interpreter.h"
#include "fast/Fast3dWindow.h"
#include <optional>
#include "../../../../../soh/soh/Localization.h"
#ifdef GFX_DEBUG_DISASSEMBLER
#include <gfxd.h>
#endif
using namespace Fast;
namespace LUS {
GfxDebuggerWindow::~GfxDebuggerWindow() {
}
void GfxDebuggerWindow::InitElement() {
}
void GfxDebuggerWindow::UpdateElement() {
if (mInterpreter.lock() == nullptr) {
mInterpreter =
dynamic_pointer_cast<Fast::Fast3dWindow>(Ship::Context::GetInstance()->GetWindow())->GetInterpreterWeak();
}
}
// LUSTODO handle switching ucodes
static const char* GetOpName(int8_t op) {
return GfxGetOpcodeName(op);
}
#define C0(pos, width) ((cmd->words.w0 >> (pos)) & ((1U << width) - 1))
#define C1(pos, width) ((cmd->words.w1 >> (pos)) & ((1U << width) - 1))
// static int s_dbgcnt = 0;
void GfxDebuggerWindow::DrawDisasNode(const F3DGfx* cmd, std::vector<const F3DGfx*>& gfxPath,
float parentPosY = 0) const {
const F3DGfx* dlStart = cmd;
auto dbg = Ship::Context::GetInstance()->GetGfxDebugger();
auto nodeWithText = [dbg, dlStart, parentPosY, this, &gfxPath](const F3DGfx* cmd, const std::string& text,
const F3DGfx* sub = nullptr) mutable {
gfxPath.push_back(cmd);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow;
if (dbg->HasBreakPoint(gfxPath)) {
flags |= ImGuiTreeNodeFlags_Selected;
}
if (sub == nullptr) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
bool scrollTo = false;
float curPosY = ImGui::GetCursorPosY();
bool open = ImGui::TreeNodeEx((const void*)cmd, flags, "%p:%4d: %s", cmd,
(int)(((uintptr_t)cmd - (uintptr_t)dlStart) / sizeof(F3DGfx)), text.c_str());
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
dbg->SetBreakPoint(gfxPath);
}
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::Selectable("WIDGET_COPY_TEXT")) {
SDL_SetClipboardText(text.c_str());
}
if (ImGui::Selectable("WIDGET_COPY_ADDRESS")) {
std::string address = fmt::format("0x{:x}", (uintptr_t)cmd);
SDL_SetClipboardText(address.c_str());
}
if (parentPosY > 0) {
scrollTo = ImGui::Selectable("WIDGET_SCROLL_TO_PARENT");
}
ImGui::EndPopup();
}
if (scrollTo) {
ImGui::SetScrollY(parentPosY);
}
if (open) {
if (sub) {
DrawDisasNode(sub, gfxPath, curPosY);
}
ImGui::TreePop();
}
gfxPath.pop_back();
};
auto simpleNode = [dbg, nodeWithText](const F3DGfx* cmd, int8_t opcode) mutable {
const char* opname = GetOpName(opcode);
if (opname) {
#ifdef GFX_DEBUG_DISASSEMBLER
size_t size = 1;
// Texture rectangle is larger due to RDPHALF
if (opcode == RDP_G_TEXRECT || opcode == RDP_G_TEXRECTFLIP) {
size = 3;
}
// Our Gfx uses uinptr_t for words, but libgfxd uses uint32_t,
// Copy only the first 32bits of each word into a vector before passing the instructions
std::vector<uint32_t> input;
for (size_t i = 0; i < size; i++) {
input.push_back(cmd[i].words.w0 & 0xFFFFFFFF);
input.push_back(cmd[i].words.w1 & 0xFFFFFFFF);
}
gfxd_input_buffer(input.data(), sizeof(uint32_t) * size * 2);
gfxd_endian(gfxd_endian_host, sizeof(uint32_t));
char buff[512] = { 0 };
gfxd_output_buffer(buff, sizeof(buff));
gfxd_enable(gfxd_emit_dec_color);
// Load the correct GBI
#if defined(F3DEX_GBI_2)
gfxd_target(gfxd_f3dex2);
#elif defined(F3DEX_GBI)
gfxd_target(gfxd_f3dex);
#else
gfxd_target(gfxd_f3d);
#endif
gfxd_execute();
nodeWithText(cmd, fmt::format("{}", buff));
#else
nodeWithText(cmd, fmt::format("{}", opname));
#endif // GFX_DEBUG_DISASSEMBLER
} else {
int8_t opcode = (int8_t)(cmd->words.w0 >> 24);
nodeWithText(cmd, fmt::format("UNK: 0x{:X}", opcode));
}
};
while (true) {
int8_t opcode = (int8_t)(cmd->words.w0 >> 24);
const F3DGfx* cmd0 = cmd;
switch (opcode) {
#ifdef F3DEX_GBI_2
case F3DEX2_G_NOOP: {
const char* filename = (const char*)cmd->words.w1;
uint32_t p = C0(16, 8);
uint32_t l = C0(0, 16);
if (p == 7) {
nodeWithText(cmd0, fmt::format("gDPNoOpOpenDisp(): {}:{}", filename, l));
} else if (p == 8) {
nodeWithText(cmd0, fmt::format("gDPNoOpCloseDisp(): {}:{}", filename, l));
} else {
simpleNode(cmd0, opcode);
}
cmd++;
break;
}
#endif
#ifdef F3DEX_GBI_2 // Different opcodes, same behavior. Handle subtrees for DL calls.
case F3DEX2_G_DL: {
#else
case F3DEX_G_DL: {
#endif
F3DGfx* subGFX = (F3DGfx*)mInterpreter.lock()->SegAddr(cmd->words.w1);
if (C0(16, 1) == 0) {
nodeWithText(cmd0, fmt::format("G_DL: 0x{:x} -> {}", cmd->words.w1, (void*)subGFX), subGFX);
cmd++;
} else {
nodeWithText(cmd0, fmt::format("G_DL (branch): 0x{:x} -> {}", cmd->words.w1, (void*)subGFX),
subGFX);
return;
}
break;
}
#ifdef F3DEX_GBI_2 // Different opcodes, same behavior. Return out of subtree.
case F3DEX2_G_ENDDL: {
#else
case F3DEX_G_ENDDL: {
#endif
simpleNode(cmd, opcode);
return;
}
// Increment 3 times because texture rectangle uses RDPHALF
case RDP_G_TEXRECTFLIP:
case RDP_G_TEXRECT: {
simpleNode(cmd, opcode);
cmd += 3;
break;
}
case OTR_G_MARKER: {
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
const char* dlName = ResourceGetNameByCrc(hash);
if (!dlName) {
dlName = "UNKNOWN";
}
nodeWithText(cmd0, fmt::format("G_MARKER: {}", dlName));
cmd++;
break;
}
case OTR_G_DL_OTR_HASH: {
if (C0(16, 1) == 0) {
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
const char* dlName = ResourceGetNameByCrc(hash);
if (!dlName)
dlName = "UNKNOWN";
F3DGfx* subGfx = (F3DGfx*)ResourceGetDataByCrc(hash);
nodeWithText(cmd0, fmt::format("G_DL_OTR_HASH: {}", dlName), subGfx);
cmd++;
} else {
assert(0 && "Invalid in gfx_pc????");
}
break;
}
case OTR_G_DL_OTR_FILEPATH: {
char* fileName = (char*)cmd->words.w1;
F3DGfx* subGfx = (F3DGfx*)ResourceGetDataByName((const char*)fileName);
if (subGfx == nullptr) {
assert(0 && "in gfx_pc????");
}
if (C0(16, 1) == 0 && subGfx != nullptr) {
nodeWithText(cmd0, fmt::format("G_DL_OTR_HASH: {}", fileName), subGfx);
cmd++;
break;
} else {
nodeWithText(cmd0, fmt::format("G_DL_OTR_HASH (branch): {}", fileName), subGfx);
return;
}
break;
}
case OTR_G_DL_INDEX: {
uint8_t segNum = (uint8_t)(cmd->words.w1 >> 24);
uint32_t index = (uint32_t)(cmd->words.w1 & 0x00FFFFFF);
uintptr_t segAddr = (segNum << 24) | (index * sizeof(F3DGfx)) + 1;
F3DGfx* subGFX = (F3DGfx*)mInterpreter.lock()->SegAddr(segAddr);
if (C0(16, 1) == 0) {
nodeWithText(cmd0, fmt::format("G_DL_INDEX: 0x{:x} -> {}", segAddr, (void*)subGFX), subGFX);
cmd++;
} else {
nodeWithText(cmd0, fmt::format("G_DL_INDEX (branch): 0x{:x} -> {}", segAddr, (void*)subGFX),
subGFX);
return;
}
break;
}
case OTR_G_BRANCH_Z_OTR: {
uint8_t vbidx = (uint8_t)(cmd->words.w0 & 0x00000FFF);
uint32_t zval = (uint32_t)(cmd->words.w1);
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
F3DGfx* subGfx = (F3DGfx*)ResourceGetDataByCrc(hash);
const char* dlName = ResourceGetNameByCrc(hash);
if (!dlName) {
dlName = "UNKNOWN";
}
// TODO: Figure out the vertex value at the time this command would have run, since debugger display is
// not in sync with renderer execution.
// if (subGfx && (g_rsp.loaded_vertices[vbidx].z <= zval ||
// (g_rsp.extra_geometry_mode & G_EX_ALWAYS_EXECUTE_BRANCH) != 0)) {
if (subGfx) {
nodeWithText(cmd0, fmt::format("G_BRANCH_Z_OTR: zval {}, vIdx {}, DL {}", zval, vbidx, dlName),
subGfx);
} else {
nodeWithText(cmd0, fmt::format("G_BRANCH_Z_OTR: zval {}, vIdx {}, DL {}", zval, vbidx, dlName));
}
cmd++;
break;
}
case OTR_G_SETTIMG_OTR_HASH: {
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
const char* name = ResourceGetNameByCrc(hash);
if (!name) {
name = "UNKNOWN";
}
nodeWithText(cmd0, fmt::format("G_SETTIMG_OTR_HASH: {}", name));
cmd++;
break;
}
case OTR_G_SETTIMG_OTR_FILEPATH: {
const char* fileName = (char*)cmd->words.w1;
nodeWithText(cmd0, fmt::format("G_SETTIMG_OTR_FILEPATH: {}", fileName));
cmd++;
break;
}
case OTR_G_VTX_OTR_HASH: {
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
const char* name = ResourceGetNameByCrc(hash);
if (!name) {
name = "UNKNOWN";
}
nodeWithText(cmd0, fmt::format("G_VTX_OTR_HASH: {}", name));
cmd++;
break;
};
case OTR_G_VTX_OTR_FILEPATH: {
const char* fileName = (char*)cmd->words.w1;
nodeWithText(cmd0, fmt::format("G_VTX_OTR_FILEPATH: {}", fileName));
cmd += 2;
break;
}
case OTR_G_MTX_OTR: {
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
const char* name = ResourceGetNameByCrc(hash);
if (!name) {
name = "UNKNOWN";
}
nodeWithText(cmd0, fmt::format("G_MTX_OTR: {}", name));
cmd++;
break;
}
case OTR_G_MTX_OTR_FILEPATH: {
const char* fileName = (char*)cmd->words.w1;
nodeWithText(cmd0, fmt::format("G_MTX_OTR_FILEPATH: {}", fileName));
cmd++;
break;
}
case OTR_G_MOVEMEM_HASH: {
const uint8_t index = C1(24, 8);
const uint8_t offset = C1(16, 8);
cmd++;
uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1;
const char* name = ResourceGetNameByCrc(hash);
if (!name) {
name = "UNKNOWN";
}
nodeWithText(cmd0, fmt::format("G_MOVEMEM_HASH: idx {}, offset {}, {}", index, offset, name));
cmd++;
break;
}
case OTR_G_IMAGERECT: {
nodeWithText(cmd0, fmt::format("G_IMAGERECT"));
cmd += 3;
break;
}
case OTR_G_TRI1_OTR: {
nodeWithText(cmd0, fmt::format("G_TRI1_OTR: v00 {}, v01 {}, v02 {}", C0(0, 16), C1(16, 16), C1(0, 16)));
cmd++;
break;
}
case OTR_G_PUSHCD: {
nodeWithText(cmd0, fmt::format("G_PUSHCD: filename {}", cmd->words.w1));
cmd++;
break;
}
case OTR_G_INVALTEXCACHE: {
const char* texAddr = (const char*)cmd->words.w1;
if (texAddr == 0) {
nodeWithText(cmd0, fmt::format("G_INVALTEXCACHE: clear all entries"));
} else {
if (((uintptr_t)texAddr & 1) == 0 &&
Ship::Context::GetInstance()->GetResourceManager()->OtrSignatureCheck(texAddr)) {
nodeWithText(cmd0, fmt::format("G_INVALTEXCACHE: {}", texAddr));
} else {
nodeWithText(cmd0, fmt::format("G_INVALTEXCACHE: 0x{:x}", (uintptr_t)texAddr));
}
}
cmd++;
break;
}
case OTR_G_SETTIMG_FB: {
nodeWithText(cmd0, fmt::format("G_SETTIMG_FB: src FB {}", (int32_t)cmd->words.w1));
cmd++;
break;
}
case OTR_G_COPYFB: {
nodeWithText(cmd0, fmt::format("G_COPYFB: src FB {}, dest FB {}, new frames only {}", C0(0, 11),
C0(11, 11), C0(22, 1)));
cmd++;
break;
}
case OTR_G_SETFB: {
nodeWithText(cmd0, fmt::format("G_SETFB: src FB {}", (int32_t)cmd->words.w1));
cmd++;
break;
}
case OTR_G_RESETFB: {
nodeWithText(cmd0, fmt::format("G_RESETFB"));
cmd++;
break;
}
case OTR_G_READFB: {
int fbId = C0(0, 8);
bool bswap = C0(8, 1);
cmd++;
nodeWithText(cmd0, fmt::format("G_READFB: src FB {}, byteswap {}, ulx {}, uly {}, width {}, height {}",
fbId, bswap, C0(0, 16), C0(16, 16), C1(0, 16), C1(16, 16)));
cmd++;
break;
}
case OTR_G_TEXRECT_WIDE: {
int32_t lrx = static_cast<int32_t>((C0(0, 24) << 8)) >> 8;
int32_t lry = static_cast<int32_t>((C1(0, 24) << 8)) >> 8;
int32_t tile = C1(24, 3);
cmd++;
uint32_t ulx = static_cast<int32_t>((C0(0, 24) << 8)) >> 8;
uint32_t uly = static_cast<int32_t>((C1(0, 24) << 8)) >> 8;
cmd++;
uint32_t uls = C0(16, 16);
uint32_t ult = C0(0, 16);
uint32_t dsdx = C1(16, 16);
uint32_t dtdy = C1(0, 16);
nodeWithText(
cmd0,
fmt::format("G_TEXRECT_WIDE: ulx {}, uly {}, lrx {}, lry {}, tile {}, s {}, t {}, dsdx {}, dtdy {}",
ulx, uly, lrx, lry, uls, tile, ult, dsdx, dtdy));
cmd++;
break;
}
case OTR_G_FILLWIDERECT: {
int32_t lrx = (int32_t)(C0(0, 24) << 8) >> 8;
int32_t lry = (int32_t)(C1(0, 24) << 8) >> 8;
cmd++;
int32_t ulx = (int32_t)(C0(0, 24) << 8) >> 8;
int32_t uly = (int32_t)(C1(0, 24) << 8) >> 8;
nodeWithText(cmd0, fmt::format("G_FILLWIDERECT: ulx {}, uly {}, lrx {}, lry {}", ulx, uly, lrx, lry));
cmd++;
break;
}
case OTR_G_SETGRAYSCALE: {
nodeWithText(cmd0, fmt::format("G_SETGRAYSCALE: Enable {}", (uint32_t)cmd->words.w1));
cmd++;
break;
}
case OTR_G_SETINTENSITY: {
nodeWithText(cmd0, fmt::format("G_SETINTENSITY: red {}, green {}, blue {}, alpha {}", C1(24, 8),
C1(16, 8), C1(8, 8), C1(0, 8)));
cmd++;
break;
}
case OTR_G_EXTRAGEOMETRYMODE: {
uint32_t setBits = (uint32_t)cmd->words.w1;
uint32_t clearBits = ~C0(0, 24);
nodeWithText(cmd0, fmt::format("G_EXTRAGEOMETRYMODE: Set {}, Clear {}", setBits, clearBits));
cmd++;
break;
}
case OTR_G_REGBLENDEDTEX: {
const char* timg = (const char*)cmd->words.w1;
cmd++;
uint8_t* mask = (uint8_t*)cmd->words.w0;
uint8_t* replacementTex = (uint8_t*)cmd->words.w1;
if (Ship::Context::GetInstance()->GetResourceManager()->OtrSignatureCheck(timg)) {
timg += 7;
nodeWithText(cmd0, fmt::format("G_REGBLENDEDTEX: src {}, mask {}, blended {}", timg, (void*)mask,
(void*)replacementTex));
} else {
nodeWithText(cmd0, fmt::format("G_REGBLENDEDTEX: src {}, mask {}, blended {}", (void*)timg,
(void*)mask, (void*)replacementTex));
}
cmd++;
break;
}
default: {
simpleNode(cmd, opcode);
cmd++;
break;
}
}
}
}
static const char* getTexType(Fast::TextureType type) {
switch (type) {
case Fast::TextureType::RGBA32bpp:
return "RGBA32";
case Fast::TextureType::RGBA16bpp:
return "RGBA16";
case Fast::TextureType::Palette4bpp:
return "CI4";
case Fast::TextureType::Palette8bpp:
return "CI8";
case Fast::TextureType::Grayscale4bpp:
return "I4";
case Fast::TextureType::Grayscale8bpp:
return "I8";
case Fast::TextureType::GrayscaleAlpha4bpp:
return "IA4";
case Fast::TextureType::GrayscaleAlpha8bpp:
return "IA8";
case Fast::TextureType::GrayscaleAlpha16bpp:
return "IA16";
default:
return "UNKNOWN";
}
}
static bool bpEquals(const std::vector<const F3DGfx*>& x, const std::vector<const F3DGfx*>& y) {
if (x.size() != y.size())
return false;
for (size_t i = 0; i < x.size(); i++) {
if (x[i] != y[i]) {
return false;
}
}
return true;
}
// todo: __asan_on_error
void GfxDebuggerWindow::DrawDisas() {
auto dbg = Ship::Context::GetInstance()->GetGfxDebugger();
auto dlist = dbg->GetDisplayList();
ImGui::Text("dlist: %p", dlist);
std::string bp = "";
for (auto& gfx : dbg->GetBreakPoint()) {
bp += fmt::format("/{}", (const void*)gfx);
}
ImGui::Text("BreakPoint: %s", bp.c_str());
bool isNew = !bpEquals(mLastBreakPoint, dbg->GetBreakPoint());
if (isNew) {
mLastBreakPoint = dbg->GetBreakPoint();
// fprintf(stderr, "NEW BREAKPOINT %s\n", bp.c_str());
}
std::string TO_LOAD_TEX = "GfxDebuggerWindowTextureToLoad";
const F3DGfx* cmd = dlist;
auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui();
ImGui::BeginChild("###State", ImVec2(0.0f, 200.0f), true);
{
ImGui::BeginGroup();
{
ImGui::Text("TEXT_DISP_STACK");
ImGui::BeginChild("### Disp Stack", ImVec2(400.0f, 0.0f), true, ImGuiWindowFlags_HorizontalScrollbar);
for (auto& disp : g_exec_stack.disp_stack) {
ImGui::Text("%s", fmt::format("{}:{}", disp.file, disp.line).c_str());
}
ImGui::EndChild();
}
ImGui::EndGroup();
ImGui::SameLine();
ImGui::BeginGroup();
{
ImGui::Text("TEXT_TILES");
ImGui::BeginChild("### Tile", ImVec2(400.0f, 0.0f), true);
// for (size_t i = 0; i < 8; i++) {
// auto& tile = g_rdp.texture_tile[i];
// ImGui::Text(
// "%s", fmt::format("{}: fmt={}; siz={}; cms={}; cmt={};", i, tile.fmt, tile.siz, tile.cms,
// tile.cmt)
// .c_str());
// }
auto draw_img = [isNew, &gui](std::optional<std::string> prefix, const std::string& name,
const RawTexMetadata& metadata) {
if (prefix) {
ImGui::Text("%s: %dx%d; type=%s", prefix->c_str(), metadata.width, metadata.height,
getTexType(metadata.type));
} else {
ImGui::Text("%dx%d; type=%s", metadata.width, metadata.height, getTexType(metadata.type));
}
if (isNew && metadata.resource != nullptr) {
gui->UnloadTexture(name);
gui->LoadGuiTexture(name, *metadata.resource, ImVec4{ 1.0f, 1.0f, 1.0f, 1.0f });
}
ImGui::Image(gui->GetTextureByName(name), ImVec2{ 100.0f, 100.0f });
};
ImGui::Text("TEXT_LOADED_TEXTURES");
for (size_t i = 0; i < 2; i++) {
auto& tex = mInterpreter.lock()->mRdp->loaded_texture[i];
// ImGui::Text("%s", fmt::format("{}: {}x{} type={}", i, tex.raw_tex_metadata.width,
// tex.raw_tex_metadata.height, getTexType(tex.raw_tex_metadata.type))
// .c_str());
draw_img(std::to_string(i), fmt::format("GfxDebuggerWindowLoadedTexture{}", i), tex.raw_tex_metadata);
}
ImGui::Text(LUS_LOC("TEXT_TEXTURE_TO_LOAD"));
{
auto& tex = mInterpreter.lock()->mRdp->texture_to_load;
// ImGui::Text("%s", fmt::format("{}x{} type={}", tex.raw_tex_metadata.width,
// tex.raw_tex_metadata.height,
// getTexType(tex.raw_tex_metadata.type))
// .c_str());
// if (isNew && g_rdp.texture_to_load.raw_tex_metadata.resource != nullptr) {
// gui->UnloadTexture(TO_LOAD_TEX);
// gui->LoadGuiTexture(TO_LOAD_TEX, *g_rdp.texture_to_load.raw_tex_metadata.resource,
// ImVec4{ 1.0f, 1.0f, 1.0f, 1.0f });
// }
// ImGui::Image(gui->GetTextureByName(TO_LOAD_TEX), ImVec2{ 100.0f, 100.0f });
draw_img(std::nullopt, TO_LOAD_TEX, tex.raw_tex_metadata);
}
ImGui::EndChild();
}
ImGui::EndGroup();
ImGui::SameLine();
ImGui::BeginGroup();
{
auto showColor = [](const char* text, RGBA c) {
float cf[] = { c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f };
ImGui::ColorEdit3(text, cf, ImGuiColorEditFlags_NoInputs);
};
showColor("Env Color", mInterpreter.lock()->mRdp->env_color);
showColor("Prim Color", mInterpreter.lock()->mRdp->prim_color);
showColor("Fog Color", mInterpreter.lock()->mRdp->fog_color);
showColor("Fill Color", mInterpreter.lock()->mRdp->fill_color);
showColor("Grayscale Color", mInterpreter.lock()->mRdp->grayscale_color);
}
ImGui::EndGroup();
}
ImGui::EndChild();
ImGui::BeginChild("##Disassembler", ImVec2(0.0f, 0.0f), true);
{
std::vector<const F3DGfx*> gfxPath;
DrawDisasNode(dlist, gfxPath);
}
ImGui::EndChild();
}
void GfxDebuggerWindow::DrawElement() {
auto dbg = Ship::Context::GetInstance()->GetGfxDebugger();
// const ImVec2 pos = ImGui::GetWindowPos();
// const ImVec2 size = ImGui::GetWindowSize();
if (!dbg->IsDebugging()) {
if (ImGui::Button(LUS_LOC("BUTTON_DEBUG"))) {
dbg->RequestDebugging();
}
} else {
bool resumed = false;
if (ImGui::Button("WIDGET_RESUME_GAME")) {
dbg->ResumeGame();
resumed = true;
}
if (!resumed) {
DrawDisas();
}
}
}
} // namespace LUS

View File

@@ -0,0 +1,718 @@
#include "ship/window/gui/ConsoleWindow.h"
#include "ship/config/ConsoleVariable.h"
#include "ship/window/Window.h"
#include "ship/Context.h"
#include "ship/utils/StringHelper.h"
#include "ship/utils/Utils.h"
#include <sstream>
#include "../../../../../soh/soh/Localization.h"
namespace Ship {
int32_t ConsoleWindow::HelpCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
if (output) {
*output += "Commands:";
for (const auto& cmd : console->GetCommands()) {
*output += "\n - " + cmd.first + ": " + cmd.second.Description;
if (!cmd.second.Arguments.empty()) {
*output += "\n - Arguments:";
for (size_t i = 0; i < cmd.second.Arguments.size(); i += 1) {
const CommandArgument& argument = cmd.second.Arguments[i];
*output += "\n - Info=" + argument.Info;
if (argument.Type == ArgumentType::NUMBER) {
*output += " Type=Number";
} else if (argument.Type == ArgumentType::TEXT) {
*output += " Type=Text";
} else {
*output += " Type=Unknown";
}
if (argument.Optional) {
*output += " [Optional]";
}
}
}
}
return 0;
}
return 1;
}
int32_t ConsoleWindow::ClearCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
auto window =
std::static_pointer_cast<ConsoleWindow>(Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"));
if (!window) {
if (output) {
*output += "A console window is necessary for Clear";
}
return 1;
}
window->ClearLogs(window->GetCurrentChannel());
return 0;
}
int32_t ConsoleWindow::UnbindCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
if (args.size() > 1) {
auto window = std::static_pointer_cast<ConsoleWindow>(
Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"));
if (!window) {
if (output) {
*output += "A console window is necessary for Unbind";
}
return 1;
}
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k++) {
std::string key(ImGui::GetKeyName((ImGuiKey)k));
bool unbound = false;
if (toLowerCase(args[1]) == toLowerCase(key)) {
if (window->mBindings.contains((ImGuiKey)k)) {
if (output) {
*output += "Unbound '" + args[1] + " from " + window->mBindings[(ImGuiKey)k];
}
window->mBindings.erase((ImGuiKey)k);
unbound = true;
}
if (window->mBindingToggle.contains((ImGuiKey)k)) {
if (output) {
if (unbound) {
*output += "\n";
}
*output += "Unbound toggle '" + args[1] + " from " + window->mBindingToggle[(ImGuiKey)k];
}
window->mBindingToggle.erase((ImGuiKey)k);
unbound = true;
}
if (!unbound) {
if (output) {
*output += "Nothing bound to '" + args[1];
}
}
break;
}
}
} else {
if (output) {
*output += "Not enough arguments";
}
return 1;
}
return 0;
}
int32_t ConsoleWindow::BindCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
if (args.size() > 2) {
auto window = std::static_pointer_cast<ConsoleWindow>(
Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"));
if (!window) {
if (output) {
*output += "A console window is necessary for Bind";
}
return 1;
}
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k++) {
std::string key(ImGui::GetKeyName((ImGuiKey)k));
if (toLowerCase(args[1]) == toLowerCase(key)) {
std::vector<std::string> tmp;
const char* const delim = " ";
std::ostringstream imploded;
std::copy(args.begin() + 2, args.end(), std::ostream_iterator<std::string>(imploded, delim));
window->mBindings[(ImGuiKey)k] = imploded.str();
if (output) {
*output += "Binding '" + args[1] + " to " + window->mBindings[(ImGuiKey)k];
}
break;
}
}
} else {
if (output) {
*output += "Not enough arguments";
}
return 1;
}
return 0;
}
int32_t ConsoleWindow::BindToggleCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
if (args.size() > 2) {
auto window = std::static_pointer_cast<ConsoleWindow>(
Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"));
if (!window) {
if (output) {
*output += "A console window is necessary for BindToggle";
}
return 1;
}
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k++) {
std::string key(ImGui::GetKeyName((ImGuiKey)k));
if (toLowerCase(args[1]) == toLowerCase(key)) {
window->mBindingToggle[(ImGuiKey)k] = args[2];
window->SendInfoMessage("Binding toggle '%s' to %s", args[1].c_str(),
window->mBindingToggle[(ImGuiKey)k].c_str());
break;
}
}
} else {
if (output) {
*output += "Missing arguments";
}
return 1;
}
return 0;
}
#define VARTYPE_INTEGER 0
#define VARTYPE_FLOAT 1
#define VARTYPE_STRING 2
#define VARTYPE_RGBA 3
int32_t ConsoleWindow::SetCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
if (args.size() < 3) {
if (output) {
*output += "Not enough arguments.";
}
return 1;
}
int vType = CheckVarType(args[2]);
if (vType == VARTYPE_STRING) {
Ship::Context::GetInstance()->GetConsoleVariables()->SetString(args[1].c_str(), args[2].c_str());
} else if (vType == VARTYPE_FLOAT) {
Ship::Context::GetInstance()->GetConsoleVariables()->SetFloat((char*)args[1].c_str(), std::stof(args[2]));
} else if (vType == VARTYPE_RGBA) {
uint32_t val = std::stoul(&args[2].c_str()[1], nullptr, 16);
Color_RGBA8 clr;
clr.r = val >> 24;
clr.g = val >> 16;
clr.b = val >> 8;
clr.a = val & 0xFF;
Ship::Context::GetInstance()->GetConsoleVariables()->SetColor((char*)args[1].c_str(), clr);
} else {
Ship::Context::GetInstance()->GetConsoleVariables()->SetInteger(args[1].c_str(), std::stoi(args[2]));
}
Ship::Context::GetInstance()->GetConsoleVariables()->Save();
return 0;
}
int32_t ConsoleWindow::GetCommand(std::shared_ptr<Console> console, const std::vector<std::string>& args,
std::string* output) {
if (args.size() < 2) {
if (output) {
*output += "Not enough arguments.";
}
return 1;
}
auto cvar = Ship::Context::GetInstance()->GetConsoleVariables()->Get(args[1].c_str());
if (cvar != nullptr) {
if (cvar->Type == ConsoleVariableType::Integer) {
if (output) {
*output += StringHelper::Sprintf("[LUS] Variable %s is %i", args[1].c_str(), cvar->Integer);
}
} else if (cvar->Type == ConsoleVariableType::Float) {
if (output) {
*output += StringHelper::Sprintf("[LUS] Variable %s is %f", args[1].c_str(), cvar->Float);
}
} else if (cvar->Type == ConsoleVariableType::String) {
if (output) {
*output += StringHelper::Sprintf("[LUS] Variable %s is %s", args[1].c_str(), cvar->String);
}
} else if (cvar->Type == ConsoleVariableType::Color) {
if (output) {
*output += StringHelper::Sprintf("[LUS] Variable %s is %08X", args[1].c_str(), cvar->Color);
}
} else {
if (output) {
*output += StringHelper::Sprintf("[LUS] Loaded CVar with unsupported type: %s", cvar->Type);
}
return 1;
}
} else {
if (output) {
*output += StringHelper::Sprintf("[LUS] Could not find variable %s", args[1].c_str());
}
}
return 0;
}
int32_t ConsoleWindow::CheckVarType(const std::string& input) {
int32_t result = VARTYPE_STRING;
if (input[0] == '#') {
return VARTYPE_RGBA;
}
for (size_t i = 0; i < input.size(); i++) {
if (!(std::isdigit(input[i]) || input[i] == '.')) {
break;
} else {
if (input[i] == '.') {
result = VARTYPE_FLOAT;
} else if (std::isdigit(input[i]) && result != VARTYPE_FLOAT) {
result = VARTYPE_INTEGER;
}
}
}
return result;
}
ConsoleWindow::~ConsoleWindow() {
SPDLOG_TRACE("destruct console window");
delete[] mInputBuffer;
delete[] mFilterBuffer;
}
void ConsoleWindow::InitElement() {
mInputBuffer = new char[gMaxBufferSize];
strcpy(mInputBuffer, "");
mFilterBuffer = new char[gMaxBufferSize];
strcpy(mFilterBuffer, "");
Context::GetInstance()->GetConsole()->AddCommand(
"set", { SetCommand,
"Sets a console variable.",
{ { "varName", ArgumentType::TEXT }, { "varValue", ArgumentType::TEXT } } });
Context::GetInstance()->GetConsole()->AddCommand(
"get", { GetCommand, "Gets a console variable", { { "varName", ArgumentType::TEXT } } });
Context::GetInstance()->GetConsole()->AddCommand("help", { HelpCommand, "Shows all the commands" });
Context::GetInstance()->GetConsole()->AddCommand("clear", { ClearCommand, "Clear the console history" });
Context::GetInstance()->GetConsole()->AddCommand(
"unbind", { UnbindCommand, "Unbinds a key", { { "key", ArgumentType::TEXT } } });
Context::GetInstance()->GetConsole()->AddCommand(
"bind",
{ BindCommand, "Binds key to commands", { { "key", ArgumentType::TEXT }, { "cmd", ArgumentType::TEXT } } });
Context::GetInstance()->GetConsole()->AddCommand(
"bind-toggle", { BindToggleCommand,
"Bind key as a bool toggle",
{ { "key", ArgumentType::TEXT }, { "cmd", ArgumentType::TEXT } } });
}
void ConsoleWindow::UpdateElement() {
for (auto [key, cmd] : mBindings) {
if (ImGui::IsKeyPressed(key)) {
Dispatch(cmd);
}
}
for (auto [key, var] : mBindingToggle) {
if (ImGui::IsKeyPressed(key)) {
Dispatch("set " + var + " " +
std::to_string(!static_cast<bool>(
Ship::Context::GetInstance()->GetConsoleVariables()->GetInteger(var.c_str(), 0))));
}
}
}
void ConsoleWindow::DrawElement() {
bool inputFocus = false;
const ImVec2 pos = ImGui::GetWindowPos();
const ImVec2 size = ImGui::GetWindowSize();
// Renders autocomplete window
if (mOpenAutocomplete) {
auto console = Context::GetInstance()->GetConsole();
ImGui::SetNextWindowSize(ImVec2(350, std::min(static_cast<int>(mAutoComplete.size()), 3) * 20.f),
ImGuiCond_Once);
ImGui::SetNextWindowPos(ImVec2(pos.x + 8, pos.y + size.y - 1));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(3, 3));
ImGui::Begin("##WndAutocomplete", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove);
ImGui::BeginChild("AC_Child", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(.3f, .3f, .3f, 1.0f));
if (ImGui::BeginTable("AC_History", 1)) {
for (const auto& cmd : mAutoComplete) {
std::string usage = console->BuildUsage(cmd);
std::string preview = cmd + " - " + console->GetCommand(cmd).Description;
std::string autoComplete = (usage == "None" ? cmd : usage);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::Selectable(preview.c_str())) {
memset(mInputBuffer, 0, gMaxBufferSize);
memcpy(mInputBuffer, autoComplete.c_str(), sizeof(char) * autoComplete.size());
mOpenAutocomplete = false;
inputFocus = true;
}
}
ImGui::EndTable();
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
mOpenAutocomplete = false;
}
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::End();
ImGui::PopStyleVar();
}
if (ImGui::BeginPopupContextWindow("Context Menu")) {
if (ImGui::MenuItem("MENU_COPY_TEXT")) {
ImGui::SetClipboardText(mLog[mCurrentChannel][mSelectedId].Text.c_str());
mSelectedId = -1;
}
ImGui::EndPopup();
}
if (mSelectedId != -1 && ImGui::IsMouseClicked(1)) {
ImGui::OpenPopup("##WndAutocomplete");
}
// Renders top bar filters
if (ImGui::Button(LUS_LOC("BUTTON_CLEAR"))) {
ClearLogs(mCurrentChannel);
}
if (Ship::Context::GetInstance()->GetConsoleVariables()->GetInteger("gSinkEnabled", 0)) {
ImGui::SameLine();
ImGui::SetNextItemWidth(150);
if (ImGui::BeginCombo("##channel", mCurrentChannel.c_str())) {
for (const auto& channel : mLogChannels) {
const bool isSelected = (channel == std::string(mCurrentChannel));
if (ImGui::Selectable(channel.c_str(), isSelected)) {
mCurrentChannel = channel;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
} else {
mCurrentChannel = "Console";
}
ImGui::SameLine();
ImGui::SetNextItemWidth(150);
if (mCurrentChannel != "Console") {
if (ImGui::BeginCombo("##level", spdlog::level::to_string_view(mLevelFilter).data())) {
for (const auto& priorityFilter : mPriorityFilters) {
const bool isSelected = priorityFilter == mLevelFilter;
if (ImGui::Selectable(spdlog::level::to_string_view(priorityFilter).data(), isSelected)) {
mLevelFilter = priorityFilter;
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
}
ImGui::EndCombo();
}
} else {
mLevelFilter = spdlog::level::trace;
}
ImGui::SameLine();
ImGui::PushItemWidth(-1);
if (ImGui::InputTextWithHint("##input", "Filter", mFilterBuffer, gMaxBufferSize)) {
mFilter = std::string(mFilterBuffer);
}
ImGui::PopItemWidth();
// Renders console history
const float footerHeightToReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeightToReserve), false,
ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(.3f, .3f, .3f, 1.0f));
if (ImGui::BeginTable("History", 1)) {
bool focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows);
const std::vector<ConsoleLine> channel = mLog[mCurrentChannel];
if (focused && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
if (mSelectedId < (int32_t)channel.size() - 1) {
++mSelectedId;
}
}
if (focused && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
if (mSelectedId > 0) {
--mSelectedId;
}
}
for (size_t i = 0; i < channel.size(); i++) {
ConsoleLine line = channel[i];
if (!mFilter.empty() && line.Text.find(mFilter) == std::string::npos) {
continue;
}
if (mLevelFilter > line.Priority) {
continue;
}
std::string id = line.Text + "##" + std::to_string(i);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
const bool isSelected =
(mSelectedId == (int32_t)i) ||
std::find(mSelectedEntries.begin(), mSelectedEntries.end(), i) != mSelectedEntries.end();
ImGui::PushStyleColor(ImGuiCol_Text, mPriorityColours[line.Priority]);
if (ImGui::Selectable(id.c_str(), isSelected)) {
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && !isSelected) {
mSelectedEntries.push_back(i);
} else {
mSelectedEntries.clear();
}
mSelectedId = isSelected ? -1 : i;
}
ImGui::PopStyleColor();
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndTable();
}
ImGui::PopStyleColor();
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
ImGui::SetScrollHereY(1.0f);
}
ImGui::EndChild();
if (mCurrentChannel == "Console") {
// Renders input textfield
constexpr ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackEdit |
ImGuiInputTextFlags_CallbackCompletion |
ImGuiInputTextFlags_CallbackHistory;
ImGui::PushItemWidth(-53.0f);
float yBeforeInput = ImGui::GetCursorPosY();
if (ImGui::InputTextWithHint("##CMDInput", ">", mInputBuffer, gMaxBufferSize, flags,
&ConsoleWindow::CallbackStub, this)) {
inputFocus = true;
if (mInputBuffer[0] != '\0' && mInputBuffer[0] != ' ') {
Dispatch(std::string(mInputBuffer));
}
memset(mInputBuffer, 0, gMaxBufferSize);
}
if (mCmdHint != "None") {
if (ImGui::IsItemFocused()) {
// Place the tooltip above the console input field
ImGui::SetNextWindowPos(ImVec2(pos.x, pos.y + size.y - ((size.y - yBeforeInput) * 2)));
ImGui::SameLine();
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::TextUnformatted(mCmdHint.c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 50);
if (ImGui::Button("WIDGET_SUBMIT") && !inputFocus && mInputBuffer[0] != '\0' && mInputBuffer[0] != ' ') {
Dispatch(std::string(mInputBuffer));
memset(mInputBuffer, 0, gMaxBufferSize);
}
ImGui::SetItemDefaultFocus();
if (inputFocus) {
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::PopItemWidth();
}
}
void ConsoleWindow::Dispatch(const std::string& line) {
mCmdHint = "None";
mHistoryIndex = -1;
mHistory.push_back(line);
SendInfoMessage("> " + line);
auto console = Context::GetInstance()->GetConsole();
const std::vector<std::string> cmdArgs = StringHelper::Split(line, " ");
if (console->HasCommand(cmdArgs[0])) {
const CommandEntry entry = console->GetCommand(cmdArgs[0]);
std::string output = "";
int32_t commandResult = console->Run(line, &output);
if (commandResult != 0) {
SendErrorMessage(StringHelper::Sprintf("[LUS] Command Failed with code %d", commandResult));
if (!output.empty()) {
SendErrorMessage(output);
}
SendErrorMessage("[LUS] Usage: " + cmdArgs[0] + " " + console->BuildUsage(entry));
} else {
if (!output.empty()) {
SendInfoMessage(output);
} else {
if (line != "clear") {
SendInfoMessage(std::string("[LUS] Command Success!"));
}
}
}
return;
}
SendErrorMessage("[LUS] Command not found");
}
int ConsoleWindow::CallbackStub(ImGuiInputTextCallbackData* data) {
const auto instance = static_cast<ConsoleWindow*>(data->UserData);
const bool emptyHistory = instance->mHistory.empty();
auto console = Context::GetInstance()->GetConsole();
std::string history;
switch (data->EventKey) {
case ImGuiKey_Tab:
instance->mAutoComplete.clear();
for (auto& [cmd, entry] : console->GetCommands()) {
if (cmd.find(std::string(data->Buf)) != std::string::npos) {
instance->mAutoComplete.push_back(cmd);
}
}
instance->mOpenAutocomplete = !instance->mAutoComplete.empty();
instance->mCmdHint = "None";
break;
case ImGuiKey_UpArrow:
if (emptyHistory) {
break;
}
if (instance->mHistoryIndex > 0) {
instance->mHistoryIndex -= 1;
} else if (instance->mHistoryIndex < 0) {
instance->mHistoryIndex = static_cast<int>(instance->mHistory.size()) - 1;
}
data->DeleteChars(0, data->BufTextLen);
if (instance->mHistoryIndex >= 0) {
data->InsertChars(0, instance->mHistory[instance->mHistoryIndex].c_str());
}
instance->mCmdHint = "None";
break;
case ImGuiKey_DownArrow:
if (emptyHistory) {
break;
}
if (instance->mHistoryIndex >= 0 &&
instance->mHistoryIndex < static_cast<int>(instance->mHistory.size()) - 1) {
instance->mHistoryIndex += 1;
} else {
instance->mHistoryIndex = -1;
}
data->DeleteChars(0, data->BufTextLen);
if (instance->mHistoryIndex >= 0) {
data->InsertChars(0, instance->mHistory[instance->mHistoryIndex].c_str());
}
instance->mCmdHint = "None";
break;
case ImGuiKey_Escape:
instance->mHistoryIndex = -1;
data->DeleteChars(0, data->BufTextLen);
instance->mOpenAutocomplete = false;
instance->mCmdHint = "None";
break;
default:
instance->mOpenAutocomplete = false;
for (auto& [cmd, entry] : console->GetCommands()) {
const std::vector<std::string> cmdArgs = StringHelper::Split(std::string(data->Buf), " ");
if (data->BufTextLen > 2 && !cmdArgs.empty() && cmd.find(cmdArgs[0]) != std::string::npos) {
instance->mCmdHint = cmd + " " + console->BuildUsage(entry);
break;
}
instance->mCmdHint = "None";
}
}
return 0;
}
void ConsoleWindow::Append(const std::string& channel, spdlog::level::level_enum priority, const char* fmt,
va_list args) {
// Determine the size of the formatted string
va_list argsCopy;
va_copy(argsCopy, args);
int size = vsnprintf(nullptr, 0, fmt, argsCopy);
va_end(argsCopy);
if (size < 0) {
SPDLOG_ERROR("Error during formatting.");
SendErrorMessage("There has been an error during formatting!");
return;
}
std::vector<char> buf(size + 1);
vsnprintf(buf.data(), buf.size(), fmt, args);
buf[buf.size() - 1] = 0;
// Do not copy the null terminator into the std::string
mLog[channel].push_back({ std::string(buf.begin(), buf.end() - 1), priority });
}
void ConsoleWindow::Append(const std::string& channel, spdlog::level::level_enum priority, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
Append(channel, priority, fmt, args);
va_end(args);
}
void ConsoleWindow::SendInfoMessage(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
Append("Console", spdlog::level::info, fmt, args);
va_end(args);
}
void ConsoleWindow::SendErrorMessage(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
Append("Console", spdlog::level::err, fmt, args);
va_end(args);
}
void ConsoleWindow::SendInfoMessage(const std::string& str) {
Append("Console", spdlog::level::info, str.c_str());
}
void ConsoleWindow::SendErrorMessage(const std::string& str) {
Append("Console", spdlog::level::err, str.c_str());
}
void ConsoleWindow::ClearLogs(std::string channel) {
mLog[channel].clear();
mSelectedEntries.clear();
mSelectedId = -1;
}
void ConsoleWindow::ClearLogs() {
for (auto [key, var] : mLog) {
var.clear();
}
mSelectedEntries.clear();
mSelectedId = -1;
}
std::string ConsoleWindow::GetCurrentChannel() {
return mCurrentChannel;
}
void ConsoleWindow::ClearBindings() {
mBindings.clear();
mBindingToggle.clear();
}
} // namespace Ship

File diff suppressed because it is too large Load Diff

44
soh/soh/Localization.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "Localization.h"
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
namespace LUS {
Localization* Localization::mInstance = nullptr;
Localization* Localization::GetInstance() {
if (mInstance == nullptr) {
mInstance = new Localization();
}
return mInstance;
}
void Localization::LoadLanguage(const std::string& langCode) {
std::string fileName = "languages/" + langCode + ".json";
SPDLOG_INFO("[Localization] Loading language: {}", langCode);
std::ifstream f(fileName);
if (!f.is_open()) {
SPDLOG_ERROR("[Localization] Could not open language file: {}", fileName);
return;
}
try {
nlohmann::json data = nlohmann::json::parse(f);
mTranslations = data.get<std::map<std::string, std::string>>();
mCurrentLang = langCode;
} catch (const std::exception& e) {
std::cerr << "Error parsing language JSON: " << e.what() << std::endl;
}
}
const char* Localization::Get(const std::string& key) {
auto it = mTranslations.find(key);
if (it != mTranslations.end()) {
return it->second.c_str();
}
return key.c_str();
}
}

28
soh/soh/Localization.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <map>
#include <nlohmann/json.hpp>
namespace LUS {
class Localization {
public:
static Localization* GetInstance();
// Carga el idioma seleccionado desde un archivo JSON
void LoadLanguage(const std::string& langCode);
// Obtiene el texto traducido. Si no existe la clave, devuelve la clave misma.
// Devuelve const char* para compatibilidad con funciones C-style.
const char* Get(const std::string& key);
private:
static Localization* mInstance;
std::map<std::string, std::string> mTranslations;
std::string mCurrentLang;
Localization() = default;
};
}
// Macro global para facilitar el acceso en el código
#define LUS_LOC(key) LUS::Localization::GetInstance()->Get(key)

View File

@@ -7,6 +7,7 @@
#include <vector>
#include <chrono>
#include <optional>
#include "soh/Localization.h"
#include "ResourceManagerHelpers.h"
#include <fast/Fast3dWindow.h>
@@ -43,7 +44,6 @@
#include <ship/utils/StringHelper.h>
#include "Enhancements/custom-message/CustomMessageManager.h"
#include "util.h"
#include "Localization.h"
#if not defined(__SWITCH__) && not defined(__WIIU__)
#include "Extractor/Extract.h"