From 003905ca15d1ee1467dcbbb87226a06916506059 Mon Sep 17 00:00:00 2001 From: Peiikai <339721950+peiikai@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:29:16 -0600 Subject: [PATCH] Warp tab: A-Z list with maps, and every dungeon room The Warp tab keeps the two columns of the other tabs: on the left Overworld, Houses & caves and Dungeons, each with its areas A-Z opening like folders (Right / A opens, Left closes); on the right a map of the place under the cursor, drawn from our own simple shapes - the island as a 16 x 16 grid of regions with terrain marks and an icon per place, or the dungeon's floor and a closer sketch of the room. Dungeon rooms are read at runtime from the extracted level files (warp_rooms.mm): each room's tile grid (walls, water, pits), its doors and landmarks, its arrival points, and which cells form one room. Every top-down room is now a place, named by what is in it and where it lies ("Chest room, north-east"). Rooms without an arrival point of their own warp to the level's nearest one, then Link is moved onto a floor tile just inside a doorway once the level has loaded (warp_request_place, the position door jumps write). Co-Authored-By: Claude Opus 5.5 --- runtime/hle/warp.c | 43 +++ runtime/ui/warp_maps.h | 26 ++ runtime/ui/warp_maps.mm | 396 +++++++++++++++++++++++ runtime/ui/warp_rooms.h | 40 +++ runtime/ui/warp_rooms.mm | 341 ++++++++++++++++++++ runtime/ui/warp_tab.mm | 663 ++++++++++++++++++++++++++++++--------- 6 files changed, 1366 insertions(+), 143 deletions(-) create mode 100644 runtime/ui/warp_maps.h create mode 100644 runtime/ui/warp_maps.mm create mode 100644 runtime/ui/warp_rooms.h create mode 100644 runtime/ui/warp_rooms.mm diff --git a/runtime/hle/warp.c b/runtime/hle/warp.c index 65222a988..78d4e8860 100644 --- a/runtime/hle/warp.c +++ b/runtime/hle/warp.c @@ -23,6 +23,7 @@ static volatile int g_pending; static volatile uint64_t g_request; static uint64_t g_req; // our level-jump request (HLE heap) +static int g_place_queued, g_place_armed; // (warp_request_place, below) static volatile int g_give_pending; // A warp waits for the one before it (issue #41: a jump handed to the game while its last one was still under way // crashed in its request processor, null+0x3f8): every level load registers a new request object (g_loads); after a @@ -80,6 +81,7 @@ void warp_request(const char *level, const char *locator) { strlcpy(g_level, level, sizeof g_level); strlcpy(g_locator, locator, sizeof g_locator); g_pending = 1; + g_place_queued = g_place_armed = 0; // (warp_request_place sets it again after this) pthread_mutex_unlock(&g_lock); LOG("[warp] queued %s / %s\n", level, locator); } @@ -111,6 +113,45 @@ bool warp_blocked(const char *locator) { } int warp_busy(void) { return g_pending; } // a queued warp not applied yet (shader tour) +// A warp to a room with no arrival point of its own (the Warp tab's dungeon rooms): the jump goes to the nearest +// arrival point of the level, then, once the level has loaded and settled, Link is moved onto a floor tile just inside +// the room (the PlayerCollision position doorjump.c moves him by). The game sees him in that room and scrolls there, +// as after a door jump. +enum { PLACE_DELAY = 40, PLACE_TIMEOUT = 1500 }; +static int player_component_missing(void); +static unsigned g_place_loads; +static uint64_t g_place_since; +static float g_place_pos[3]; +static char g_place_level[64]; +void warp_request_place(const char *level, const char *locator, float x, float y, float z) { + warp_request(level, locator); + pthread_mutex_lock(&g_lock); + g_place_queued = 1, g_place_armed = 0; + g_place_pos[0] = x, g_place_pos[1] = y, g_place_pos[2] = z; + strlcpy(g_place_level, level, sizeof g_place_level); + pthread_mutex_unlock(&g_lock); + LOG("[warp] then Link -> (%.2f, %.2f, %.2f)\n", x, y, z); +} +static void place_tick(uint64_t now) { + if (!g_place_armed) return; + if (now - g_place_since > PLACE_TIMEOUT) { g_place_armed = 0; LOG("[warp] room placement dropped (timeout)\n"); return; } + if (g_loads == g_place_loads || now - g_load_frame < PLACE_DELAY || g_change || player_component_missing()) return; + uint64_t ps = LD64(GA(PLAYER_STATUS_PTR)); + if (!ps || strncmp((const char *)(uintptr_t)ps, g_place_level, sizeof g_place_level)) return; + extern void doorjump_player_rescan(void); + extern uint64_t doorjump_player(uint64_t frame); + static unsigned rescanned = ~0u; + if (rescanned != g_loads) rescanned = g_loads, doorjump_player_rescan(); // (a warp within the same level) + uint64_t pc = doorjump_player(now); + if (!pc) return; + float *pos = (float *)(uintptr_t)(pc + 0x80), *pos2 = (float *)(uintptr_t)(pc + 0xb0); + pos[0] = pos2[0] = g_place_pos[0]; + pos[1] = pos2[1] = g_place_pos[1]; + pos[2] = pos2[2] = g_place_pos[2]; + g_place_armed = 0; + LOG("[warp] Link placed at (%.2f, %.2f, %.2f) in %s\n", g_place_pos[0], g_place_pos[1], g_place_pos[2], g_place_level); +} + // LARECOMP_WARP_AT=::[,::...] (test hook) // LARECOMP_WARP_ON_LOAD=:: warp once, ~1 s after a save is loaded (hand-off to the user) static void warp_on_load(uint64_t frame) { @@ -264,6 +305,7 @@ static int player_component_missing(void) { void warp_game_tick(Cpu *c) { uint64_t now = g_frame_now; watch_level_change(now); + place_tick(now); if (!g_pending) return; if (g_warp_waiting && g_loads == g_loads_at_warp && now - g_warp_frame < WARP_TIMEOUT) return; // (the last warp's level isn't loaded yet: this one stays queued) @@ -295,6 +337,7 @@ void warp_game_tick(Cpu *c) { pthread_mutex_unlock(&g_lock); LOG("[warp] jumping to %s / %s\n", (char *)(uintptr_t)(req + 0xa8), (char *)(uintptr_t)(req + 0xf0)); g_warp_waiting = 1, g_loads_at_warp = g_loads, g_warp_frame = now; + if (g_place_queued) g_place_queued = 0, g_place_armed = 1, g_place_loads = g_loads, g_place_since = now; uint64_t saved[31], sp = c->sp, pc = c->pc; memcpy(saved, c->x, sizeof saved); c->x[0] = req; diff --git a/runtime/ui/warp_maps.h b/runtime/ui/warp_maps.h new file mode 100644 index 000000000..55b737bb1 --- /dev/null +++ b/runtime/ui/warp_maps.h @@ -0,0 +1,26 @@ +// The Warp tab's maps (runtime/ui/warp_maps.mm), drawn with ImGui draw lists from our own simple shapes: the island +// as a 16 x 16 grid of squares coloured by region with small terrain marks and an icon per place, and a dungeon as its +// real floor of rooms (warp_rooms.h) with a closer sketch of one room. +#pragma once +#include "imgui.h" +#include "warp_rooms.h" + +enum WarpIcon { WI_DOOR, WI_CAVE, WI_DUNGEON, WI_STAIRS, WI_LADDER, WI_DROP, WI_WARP, WI_PHONE, WI_FAIRY, WI_NOTE, WI_PIN, + WI_ROOM, WI_ENTRANCE, WI_BOAT, WI_N }; +void warp_icon(ImDrawList *dl, int icon, ImVec2 c, float size, ImU32 col); + +// One square of the island: its region (an index into regions[]) and the icons of the places on it +struct WarpIslandSquare { int region = -1; int icons[4]; int n = 0; }; +struct WarpIsland { + WarpIslandSquare sq[16][16]; + const char *regions[32]; + int nregions = 0; +}; +// a: top-left, b: bottom-right; cur: the square to frame (row, col; -1: none); region: the region to light up (-1) +void warp_draw_island(ImDrawList *dl, ImVec2 a, ImVec2 b, const WarpIsland &is, int cur_r, int cur_c, int region); + +// The floor: every top-down room; sel: the room to frame (-1: none), dots: arrival points to mark ({room, x, z}) +struct WarpDot { int room; float x, z; bool hot; }; +void warp_draw_floor(ImDrawList *dl, ImVec2 a, ImVec2 b, const WarpLevel &L, int sel, const WarpDot *dots, int ndots); +// One room (all the cells of its unit) closer, with its arrival points +void warp_draw_room(ImDrawList *dl, ImVec2 a, ImVec2 b, const WarpLevel &L, int room, const WarpDot *dots, int ndots); diff --git a/runtime/ui/warp_maps.mm b/runtime/ui/warp_maps.mm new file mode 100644 index 000000000..f861672e1 --- /dev/null +++ b/runtime/ui/warp_maps.mm @@ -0,0 +1,396 @@ +// The Warp tab's maps (warp_maps.h). Every mark here is drawn from simple shapes: squares, arcs, lines. +#include "warp_maps.h" + +#include +#include + +#include + +#ifndef IM_PI +#define IM_PI 3.14159265358979323846f +#endif + +static ImU32 rgb(int r, int g, int b, int a = 255) { return IM_COL32(r, g, b, a); } +static ImU32 shade(ImU32 c, float f) { // f > 0 lighter, < 0 darker + int r = c & 255, g = (c >> 8) & 255, b = (c >> 16) & 255, a = c >> 24; + auto m = [f](int v) { return (int)std::max(0.0f, std::min(255.0f, f > 0 ? v + (255 - v) * f : v * (1 + f))); }; + return IM_COL32(m(r), m(g), m(b), a); +} +static float rnd(int a, int b, int k) { // (a fixed scatter per square) + float x = sinf(a * 127.1f + b * 311.7f + k * 74.7f) * 43758.5453f; + return x - floorf(x); +} +static const ImU32 kInk = IM_COL32(16, 14, 22, 255); + +// ---------- icons (24-unit boxes, as the sketches) ---------- +void warp_icon(ImDrawList *dl, int k, ImVec2 c, float s, ImU32 col) { + float u = s / 24.0f, th = std::max(1.2f, s / 11.0f); + auto P = [&](float x, float y) { return ImVec2(c.x + (x - 12) * u, c.y + (y - 12) * u); }; + switch (k) { + case WI_DOOR: + dl->PathLineTo(P(6, 21)), dl->PathLineTo(P(6, 10)), dl->PathArcTo(P(12, 10), 6 * u, IM_PI, 2 * IM_PI), dl->PathLineTo(P(18, 21)); + dl->PathStroke(col, 0, th), dl->AddLine(P(3.5f, 21), P(20.5f, 21), col, th); + break; + case WI_CAVE: + dl->PathArcTo(P(12, 20), 9.5f * u, IM_PI, 2 * IM_PI), dl->PathStroke(col, ImDrawFlags_Closed, th); + dl->PathArcTo(P(12, 20), 3.2f * u, IM_PI, 2 * IM_PI), dl->PathStroke(col, 0, th); + break; + case WI_DUNGEON: + dl->PathLineTo(P(4, 21)), dl->PathLineTo(P(4, 9.5f)), dl->PathLineTo(P(12, 4)), dl->PathLineTo(P(20, 9.5f)), dl->PathLineTo(P(20, 21)); + dl->PathStroke(col, ImDrawFlags_Closed, th); + dl->PathLineTo(P(9.5f, 21)), dl->PathArcTo(P(12, 16), 2.5f * u, IM_PI, 2 * IM_PI), dl->PathLineTo(P(14.5f, 21)), dl->PathStroke(col, 0, th); + break; + case WI_STAIRS: + for (ImVec2 p : {P(3.5f, 20.5f), P(8, 20.5f), P(8, 16), P(12.5f, 16), P(12.5f, 11.5f), P(17, 11.5f), P(17, 7), P(20.5f, 7)}) dl->PathLineTo(p); + dl->PathStroke(col, 0, th); + break; + case WI_LADDER: + dl->AddLine(P(8, 3), P(8, 21), col, th), dl->AddLine(P(16, 3), P(16, 21), col, th); + for (float y : {7.5f, 12.0f, 16.5f}) dl->AddLine(P(8, y), P(16, y), col, th); + break; + case WI_DROP: + dl->AddLine(P(12, 3), P(12, 14), col, th), dl->AddLine(P(7.5f, 9.5f), P(12, 14), col, th), dl->AddLine(P(16.5f, 9.5f), P(12, 14), col, th); + dl->AddLine(P(5, 20.5f), P(19, 20.5f), col, th); + break; + case WI_ENTRANCE: + dl->AddLine(P(12, 21), P(12, 9), col, th), dl->AddLine(P(7.5f, 13.5f), P(12, 9), col, th), dl->AddLine(P(16.5f, 13.5f), P(12, 9), col, th); + dl->AddLine(P(4, 3.5f), P(20, 3.5f), col, th); + break; + case WI_WARP: + for (int i = 0; i <= 40; i++) { float t = i / 40.0f * 3.2f * IM_PI, r = (1.0f + 2.6f * t) * u; dl->PathLineTo(ImVec2(c.x + cosf(t) * r, c.y + sinf(t) * r)); } + dl->PathStroke(col, 0, th); + break; + case WI_PHONE: + dl->AddRect(P(7, 4), P(17, 20), col, 2 * u, 0, th), dl->AddLine(P(10, 16.5f), P(14, 16.5f), col, th); + dl->AddRect(P(9.5f, 6.5f), P(14.5f, 12.5f), col, u, 0, th * 0.7f); + break; + case WI_FAIRY: + for (ImVec2 p : {P(12, 2.5f), P(13.9f, 10.1f), P(21.5f, 12), P(13.9f, 13.9f), P(12, 21.5f), P(10.1f, 13.9f), P(2.5f, 12), P(10.1f, 10.1f)}) dl->PathLineTo(p); + dl->PathStroke(col, ImDrawFlags_Closed, th); + break; + case WI_NOTE: + dl->AddCircle(P(6.4f, 18.5f), 2.6f * u, col, 0, th), dl->AddCircle(P(16.4f, 16.1f), 2.6f * u, col, 0, th); + dl->AddLine(P(9, 18.5f), P(9, 5.5f), col, th), dl->AddLine(P(19, 16.1f), P(19, 3.3f), col, th), dl->AddLine(P(9, 5.5f), P(19, 3.3f), col, th); + break; + case WI_PIN: + dl->PathArcTo(P(12, 10), 6.5f * u, 0.8f * IM_PI, 2.2f * IM_PI), dl->PathLineTo(P(12, 21.5f)), dl->PathStroke(col, ImDrawFlags_Closed, th); + dl->AddCircle(P(12, 10), 2.2f * u, col, 0, th); + break; + case WI_BOAT: + for (ImVec2 p : {P(2.5f, 15.5f), P(21.5f, 15.5f), P(18.3f, 20.5f), P(5.7f, 20.5f)}) dl->PathLineTo(p); + dl->PathStroke(col, ImDrawFlags_Closed, th); + dl->AddLine(P(12, 3), P(12, 15.5f), col, th), dl->AddTriangle(P(12, 4), P(18.5f, 12.5f), P(12, 12.5f), col, th); + break; + default: // WI_ROOM + dl->AddRect(P(4, 4), P(20, 20), col, 0, 0, th); + break; + } +} +static ImU32 icon_col(int k) { + static const ImU32 c[WI_N] = {rgb(242, 211, 138), rgb(216, 193, 163), rgb(240, 163, 94), rgb(201, 184, 240), rgb(201, 184, 240), + rgb(159, 212, 234), rgb(185, 156, 242), rgb(159, 224, 181), rgb(245, 168, 212), rgb(159, 212, 234), + rgb(230, 225, 236), rgb(189, 182, 201), rgb(242, 211, 138), rgb(159, 212, 234)}; + return k >= 0 && k < WI_N ? c[k] : c[WI_ROOM]; +} + +// ---------- the island ---------- +enum { T_ROCK, T_MOUNTAIN, T_HILLS, T_SWAMP, T_WOODS, T_GRAVE, T_CASTLE, T_RIVER, T_VILLAGE, T_RUINS, T_PRAIRIE, T_BEACH, T_DESERT, T_WATER }; +static int terrain_of(const char *region) { + static const struct { const char *n; int t; } k[] = { + {"Turtle Rock", T_ROCK}, {"Tal Tal Mountain Range", T_MOUNTAIN}, {"Tal Tal Heights", T_HILLS}, {"Goponga Swamp", T_SWAMP}, + {"Mysterious Woods", T_WOODS}, {"Cemetery", T_GRAVE}, {"Kanalet Castle", T_CASTLE}, {"Face Shrine & Rapids", T_RIVER}, + {"Mabe Village", T_VILLAGE}, {"Southern Face Shrine", T_RUINS}, {"Ukuku Prairie", T_PRAIRIE}, {"Toronbo Shores", T_BEACH}, + {"Animal Village", T_VILLAGE}, {"Yarna Desert", T_DESERT}, {"Martha's Bay", T_WATER}}; + for (auto &e : k) + if (region && !strcmp(region, e.n)) return e.t; + return T_PRAIRIE; +} +static ImU32 terrain_col(int t) { + static const ImU32 c[] = {rgb(93, 75, 74), rgb(108, 95, 83), rgb(119, 114, 78), rgb(77, 90, 58), rgb(47, 90, 61), rgb(84, 80, 104), rgb(86, 95, 112), + rgb(58, 106, 105), rgb(134, 104, 63), rgb(106, 104, 83), rgb(92, 125, 65), rgb(168, 145, 90), rgb(176, 141, 85), rgb(46, 91, 122)}; + return c[t]; +} +static void terrain(ImDrawList *dl, int t, ImVec2 o, float s, int r, int c) { + ImU32 base = terrain_col(t), lt = shade(base, 0.22f), dk = shade(base, -0.3f); + float th = std::max(1.0f, s / 22); + auto R = [&](int k) { return rnd(r, c, k); }; + dl->PushClipRect(o, ImVec2(o.x + s, o.y + s), true); + switch (t) { + case T_WOODS: + for (int k = 0; k < 3; k++) { + ImVec2 p(o.x + s * (0.2f + 0.3f * k + R(k) * 0.1f), o.y + s * (0.3f + R(k + 3) * 0.45f)); + float rr = s * (0.16f + R(k + 6) * 0.06f); + dl->AddCircleFilled(ImVec2(p.x, p.y + rr * 0.25f), rr, dk), dl->AddCircleFilled(ImVec2(p.x - rr * 0.2f, p.y - rr * 0.15f), rr * 0.7f, lt); + } + break; + case T_MOUNTAIN: case T_ROCK: + for (int k = 0; k < 2; k++) { + float bx = o.x + s * (0.08f + k * 0.42f + R(k) * 0.08f), w = s * 0.5f, h = s * (0.4f + R(k + 2) * 0.25f), by = o.y + s * 0.85f; + dl->AddTriangleFilled(ImVec2(bx, by), ImVec2(bx + w / 2, by - h), ImVec2(bx + w, by), dk); + dl->AddLine(ImVec2(bx + w * 0.3f, by - h * 0.6f), ImVec2(bx + w / 2, by - h), lt, th), dl->AddLine(ImVec2(bx + w / 2, by - h), ImVec2(bx + w * 0.62f, by - h * 0.75f), lt, th); + } + break; + case T_HILLS: + for (int k = 0; k < 2; k++) { + dl->PathArcTo(ImVec2(o.x + s * (0.3f + k * 0.35f + R(k) * 0.1f), o.y + s * (0.45f + k * 0.3f)), s * 0.2f, IM_PI * 1.1f, IM_PI * 1.9f); + dl->PathStroke(lt, 0, th); + } + break; + case T_WATER: case T_RIVER: + for (int k = 0; k < 3; k++) { + float y = o.y + s * (0.25f + k * 0.25f), x = o.x + s * (0.1f + R(k) * 0.3f); + dl->AddBezierQuadratic(ImVec2(x, y), ImVec2(x + s * 0.12f, y - s * 0.08f), ImVec2(x + s * 0.24f, y), lt, th); + dl->AddBezierQuadratic(ImVec2(x + s * 0.24f, y), ImVec2(x + s * 0.36f, y + s * 0.08f), ImVec2(x + s * 0.48f, y), lt, th); + } + break; + case T_BEACH: case T_DESERT: { + for (int k = 0; k < 6; k++) dl->AddCircleFilled(ImVec2(o.x + s * R(k), o.y + s * R(k + 7)), s * 0.03f, dk); + float y = o.y + s * (0.55f + R(20) * 0.2f); + dl->AddBezierQuadratic(ImVec2(o.x + s * 0.1f, y), ImVec2(o.x + s * 0.5f, y - s * 0.18f), ImVec2(o.x + s * 0.9f, y), t == T_BEACH ? rgb(207, 227, 234) : lt, th); + break; + } + case T_PRAIRIE: + for (int k = 0; k < 5; k++) { + float gx = o.x + s * (0.1f + R(k) * 0.8f), gy = o.y + s * (0.2f + R(k + 5) * 0.7f), q = s * 0.06f; + dl->AddLine(ImVec2(gx - q, gy - q), ImVec2(gx, gy), lt, th), dl->AddLine(ImVec2(gx, gy), ImVec2(gx + q, gy - q * 1.3f), lt, th); + } + break; + case T_SWAMP: + for (int k = 0; k < 3; k++) dl->AddEllipseFilled(ImVec2(o.x + s * (0.2f + R(k) * 0.6f), o.y + s * (0.25f + R(k + 3) * 0.55f)), ImVec2(s * 0.14f, s * 0.07f), rgb(46, 74, 85)); + break; + case T_VILLAGE: + dl->AddLine(ImVec2(o.x, o.y + s * 0.62f), ImVec2(o.x + s, o.y + s * 0.62f), lt, th); + if (R(1) < 0.6f) { + float hx = o.x + s * (0.2f + R(2) * 0.35f), hy = o.y + s * 0.5f, w = s * 0.3f; + dl->AddRectFilled(ImVec2(hx, hy - w * 0.55f), ImVec2(hx + w, hy), dk); + dl->AddTriangleFilled(ImVec2(hx - w * 0.12f, hy - w * 0.5f), ImVec2(hx + w / 2, hy - w), ImVec2(hx + w * 1.12f, hy - w * 0.5f), rgb(148, 72, 57)); + } + break; + case T_GRAVE: + for (int k = 0; k < 2; k++) { + float gx = o.x + s * (0.22f + k * 0.38f + R(k) * 0.08f), gy = o.y + s * (0.35f + R(k + 2) * 0.3f), w = s * 0.16f; + dl->AddRectFilled(ImVec2(gx, gy + w * 0.5f), ImVec2(gx + w, gy + w * 1.4f), shade(base, 0.3f)); + dl->AddCircleFilled(ImVec2(gx + w / 2, gy + w * 0.5f), w / 2, shade(base, 0.3f)); + } + break; + case T_CASTLE: + for (int k = 1; k < 4; k++) { + float y = o.y + s * k / 4; + dl->AddLine(ImVec2(o.x, y), ImVec2(o.x + s, y), dk, th); + for (int j = 0; j < 3; j++) { float x = o.x + s * (j / 3.0f + (k % 2) * 0.16f); dl->AddLine(ImVec2(x, y - s / 4), ImVec2(x, y), dk, th); } + } + break; + case T_RUINS: + for (int k = 0; k < 3; k++) { float px = o.x + s * (0.15f + k * 0.27f), h = s * (0.2f + R(k) * 0.35f); dl->AddRectFilled(ImVec2(px, o.y + s * 0.8f - h), ImVec2(px + s * 0.12f, o.y + s * 0.8f), shade(base, 0.25f)); } + break; + } + dl->PopClipRect(); +} +void warp_draw_island(ImDrawList *dl, ImVec2 a, ImVec2 b, const WarpIsland &is, int cur_r, int cur_c, int region) { + float t = floorf(std::min((b.x - a.x) / 16, (b.y - a.y) / 16)); + if (t < 2) return; + ImVec2 o(floorf(a.x + (b.x - a.x - t * 16) / 2), floorf(a.y + (b.y - a.y - t * 16) / 2)); + if (cur_r >= 0 && region < 0) region = is.sq[cur_r][cur_c].region; + dl->AddRectFilled(ImVec2(o.x - 3, o.y - 3), ImVec2(o.x + t * 16 + 3, o.y + t * 16 + 3), kInk, 6); + for (int r = 0; r < 16; r++) + for (int c = 0; c < 16; c++) { + const WarpIslandSquare &q = is.sq[r][c]; + int tt = terrain_of(q.region >= 0 ? is.regions[q.region] : NULL); + ImVec2 p(o.x + c * t, o.y + r * t); + dl->AddRectFilled(p, ImVec2(p.x + t, p.y + t), terrain_col(tt)); + if (t > 12) terrain(dl, tt, p, t, r, c); + if (region >= 0 && q.region != region) dl->AddRectFilled(p, ImVec2(p.x + t, p.y + t), IM_COL32(10, 9, 14, 100)); + } + // region borders (the lit one in gold) + for (int r = 0; r < 16; r++) + for (int c = 0; c < 16; c++) { + int g = is.sq[r][c].region; + ImVec2 p(o.x + c * t, o.y + r * t); + auto edge = [&](int g2, ImVec2 p1, ImVec2 p2) { + if (g2 == g) return; + bool hot = region >= 0 && (g == region || g2 == region); + dl->AddLine(p1, p2, hot ? IM_COL32(237, 194, 84, 220) : IM_COL32(12, 10, 16, 170), hot ? 2.0f : 1.5f); + }; + if (c < 15) edge(is.sq[r][c + 1].region, ImVec2(p.x + t, p.y), ImVec2(p.x + t, p.y + t)); + if (r < 15) edge(is.sq[r + 1][c].region, ImVec2(p.x, p.y + t), ImVec2(p.x + t, p.y + t)); + } + for (int r = 0; r < 16; r++) + for (int c = 0; c < 16; c++) { + const WarpIslandSquare &q = is.sq[r][c]; + if (!q.n) continue; + ImVec2 m(o.x + c * t + t / 2, o.y + r * t + t / 2); + if (t > 16) { + dl->AddCircleFilled(m, t * 0.35f, IM_COL32(16, 14, 22, 184)); + warp_icon(dl, q.icons[0], m, t * 0.56f, icon_col(q.icons[0])); + } else { + dl->AddCircleFilled(m, std::max(1.6f, t * 0.12f), rgb(244, 236, 216)); + } + } + if (cur_r >= 0) { + ImVec2 p(o.x + cur_c * t, o.y + cur_r * t); + dl->AddRect(ImVec2(p.x - 2, p.y - 2), ImVec2(p.x + t + 2, p.y + t + 2), IM_COL32(237, 194, 84, 90), 5, 0, 7); + dl->AddRect(ImVec2(p.x - 1, p.y - 1), ImVec2(p.x + t + 1, p.y + t + 1), rgb(237, 194, 84), 4, 0, 3); + } +} + +// ---------- dungeon rooms ---------- +static ImU32 floor_col(float e) { return e > 0.4f ? rgb(78, 71, 96) : e < -0.3f ? rgb(46, 42, 58) : rgb(62, 56, 76); } +static void feature(ImDrawList *dl, const WarpRoomFeat &f, ImVec2 p, float tw) { + float s = tw * 0.8f, th = std::max(1.0f, tw * 0.1f); + ImU32 ink = IM_COL32(10, 8, 14, 190); + switch (f.kind) { + case WF_BLOCK: case WF_CBLOCK: case WF_LOCKBLOCK: { + ImU32 c = f.kind == WF_CBLOCK ? rgb(79, 159, 174) : rgb(138, 129, 156); + dl->AddRectFilled(ImVec2(p.x - s / 2, p.y - s / 2), ImVec2(p.x + s / 2, p.y + s / 2), c), dl->AddRect(ImVec2(p.x - s / 2, p.y - s / 2), ImVec2(p.x + s / 2, p.y + s / 2), ink, 0, 0, th); + if (f.kind == WF_LOCKBLOCK) dl->AddCircleFilled(p, s * 0.16f, rgb(237, 194, 84)); + break; + } + case WF_POT: dl->AddCircleFilled(p, s * 0.38f, rgb(180, 138, 94)), dl->AddCircle(p, s * 0.38f, ink, 0, th); break; + case WF_CHEST: + dl->AddRectFilled(ImVec2(p.x - s * 0.45f, p.y - s * 0.32f), ImVec2(p.x + s * 0.45f, p.y + s * 0.32f), rgb(217, 164, 65)); + dl->AddLine(ImVec2(p.x - s * 0.45f, p.y - s * 0.08f), ImVec2(p.x + s * 0.45f, p.y - s * 0.08f), rgb(107, 74, 20), th); + break; + case WF_STAIRS: warp_icon(dl, WI_STAIRS, p, tw * 1.1f, rgb(201, 184, 240)); break; + case WF_HOLE: dl->AddEllipseFilled(p, ImVec2(s * 0.6f, s * 0.45f), rgb(9, 8, 12)); break; + case WF_SPIKES: + for (int i = -1; i <= 1; i += 2) + for (int j = -1; j <= 1; j += 2) { + ImVec2 q(p.x + i * s * 0.2f, p.y + j * s * 0.2f); + dl->AddTriangleFilled(ImVec2(q.x - s * 0.15f, q.y + s * 0.12f), ImVec2(q.x, q.y - s * 0.15f), ImVec2(q.x + s * 0.15f, q.y + s * 0.12f), rgb(169, 162, 182)); + } + break; + case WF_BELT: + for (float d : {-0.3f, 0.05f}) { + dl->PathLineTo(ImVec2(p.x + s * d, p.y - s * 0.25f)), dl->PathLineTo(ImVec2(p.x + s * (d + 0.35f), p.y)), dl->PathLineTo(ImVec2(p.x + s * d, p.y + s * 0.25f)); + dl->PathStroke(rgb(130, 122, 150), 0, th); + } + break; + case WF_SWITCH: dl->AddQuadFilled(ImVec2(p.x, p.y - s * 0.4f), ImVec2(p.x + s * 0.32f, p.y), ImVec2(p.x, p.y + s * 0.4f), ImVec2(p.x - s * 0.32f, p.y), rgb(127, 208, 224)); break; + case WF_STATUE: + dl->AddRectFilled(ImVec2(p.x - s * 0.3f, p.y - s * 0.1f), ImVec2(p.x + s * 0.3f, p.y + s * 0.4f), rgb(165, 156, 184)); + dl->AddCircleFilled(ImVec2(p.x, p.y - s * 0.1f), s * 0.3f, rgb(165, 156, 184)); + break; + case WF_TRAP: + dl->AddLine(ImVec2(p.x - s * 0.3f, p.y - s * 0.3f), ImVec2(p.x + s * 0.3f, p.y + s * 0.3f), rgb(217, 130, 107), th * 1.4f); + dl->AddLine(ImVec2(p.x + s * 0.3f, p.y - s * 0.3f), ImVec2(p.x - s * 0.3f, p.y + s * 0.3f), rgb(217, 130, 107), th * 1.4f); + break; + case WF_INSTRUMENT: dl->AddCircleFilled(p, tw * 0.7f, IM_COL32(237, 194, 84, 60)), warp_icon(dl, WI_NOTE, p, tw * 1.2f, rgb(237, 194, 84)); break; + default: { // doors: a bar across the doorway, coloured by kind + bool across_x = f.x < 0.6f || f.x > 9.4f; // (on a west / east edge: the bar runs along z) + ImVec2 h = across_x ? ImVec2(tw * 0.25f, tw * 0.8f) : ImVec2(tw * 0.8f, tw * 0.25f); + ImU32 c = f.kind == WF_DOOR_LOCK ? rgb(214, 160, 52) : f.kind == WF_DOOR_BOSS ? rgb(196, 64, 52) : f.kind == WF_DOOR_CRACKED ? rgb(120, 112, 136) + : f.kind == WF_DOOR_ONEWAY ? rgb(159, 212, 234) : rgb(98, 90, 112); + dl->AddRectFilled(ImVec2(p.x - h.x, p.y - h.y), ImVec2(p.x + h.x, p.y + h.y), c); + if (f.kind == WF_DOOR_LOCK) dl->AddCircleFilled(p, tw * 0.14f, rgb(255, 236, 170)); + break; + } + } +} +// one cell's tiles at o (tw per tile); detail: landmarks too +static void cell(ImDrawList *dl, const WarpRoom &r, ImVec2 o, float tw, bool detail) { + for (int i = 0; i < 80; i++) { + ImVec2 p(o.x + (i % 10) * tw, o.y + (i / 10) * tw), q(p.x + tw + 0.5f, p.y + tw + 0.5f); + ImU32 c = r.tile[i] == WT_WALL ? rgb(26, 23, 34) : r.tile[i] == WT_WATER ? rgb(52, 92, 128) : r.tile[i] == WT_DEEP ? rgb(18, 30, 52) : floor_col(r.elev[i]); + dl->AddRectFilled(p, q, c); + } + if (detail && tw > 6) { // a faint tile grid, and a light edge where floor meets wall + for (int i = 0; i < 80; i++) { + if (r.tile[i] == WT_WALL) continue; + int x = i % 10, y = i / 10; + ImVec2 p(o.x + x * tw, o.y + y * tw); + dl->AddRect(p, ImVec2(p.x + tw, p.y + tw), IM_COL32(255, 255, 255, 10)); + ImU32 e = IM_COL32(120, 108, 150, 200); + if (y > 0 && r.tile[i - 10] == WT_WALL) dl->AddLine(p, ImVec2(p.x + tw, p.y), e, 1.5f); + if (y < 7 && r.tile[i + 10] == WT_WALL) dl->AddLine(ImVec2(p.x, p.y + tw), ImVec2(p.x + tw, p.y + tw), e, 1.5f); + if (x > 0 && r.tile[i - 1] == WT_WALL) dl->AddLine(p, ImVec2(p.x, p.y + tw), e, 1.5f); + if (x < 9 && r.tile[i + 1] == WT_WALL) dl->AddLine(ImVec2(p.x + tw, p.y), ImVec2(p.x + tw, p.y + tw), e, 1.5f); + } + } + for (auto &f : r.feats) { + bool small = f.kind == WF_CHEST || f.kind == WF_STAIRS || f.kind == WF_INSTRUMENT || f.kind >= WF_DOOR_LOCK; + if (!detail && (!small || f.kind == WF_DOOR_SHUTTER)) continue; + if (!detail && tw < 2.5f) continue; + feature(dl, f, ImVec2(o.x + f.x * tw, o.y + f.z * tw), tw); + } +} +static void unit_box(const WarpLevel &L, int room, int *c0, int *r0, int *c1, int *r1) { + int u = L.rooms[room].unit; + *c0 = *r0 = 1 << 20, *c1 = *r1 = -1; + for (auto &r : L.rooms) + if (!r.side && r.unit == u) *c0 = std::min(*c0, r.col), *c1 = std::max(*c1, r.col), *r0 = std::min(*r0, r.row), *r1 = std::max(*r1, r.row); +} +static void pin(ImDrawList *dl, ImVec2 p, float r, bool hot) { + dl->AddCircleFilled(p, r, hot ? rgb(255, 255, 255) : rgb(237, 194, 84)); + dl->AddCircle(p, r, kInk, 0, 1.5f); + if (hot) dl->AddCircle(p, r * 1.7f, rgb(237, 194, 84), 0, 2.0f); +} + +void warp_draw_floor(ImDrawList *dl, ImVec2 a, ImVec2 b, const WarpLevel &L, int sel, const WarpDot *dots, int ndots) { + int cols = L.c1 - L.c0 + 1, rows = L.r1 - L.r0 + 1, nside = 0; + for (auto &r : L.rooms) nside += r.side; + float strip = nside ? 3.4f : 0; // (side passages: a strip under the floor, in tile rows) + float tw = std::min((b.x - a.x - 8) / (cols * 10.0f), (b.y - a.y - 8) / (rows * 8.0f + strip)); + if (tw <= 0.5f) return; + ImVec2 o(floorf(a.x + (b.x - a.x - cols * 10 * tw) / 2), floorf(a.y + 4)); + for (auto &r : L.rooms) + if (!r.side) cell(dl, r, ImVec2(o.x + (r.col - L.c0) * 10 * tw, o.y + (r.row - L.r0) * 8 * tw), tw, false); + // side passages: small strips (seen from the side), in the order of the level's files + std::vector side; + for (int i = 0; i < (int)L.rooms.size(); i++) + if (L.rooms[i].side) side.push_back(i); + float sy = o.y + rows * 8 * tw + tw * 1.2f, sh = tw * 1.8f, gap = tw; + float sw = side.empty() ? 0 : std::min(10 * tw, (b.x - a.x - 8 - gap * (side.size() - 1)) / side.size()); + float sx = floorf(a.x + (b.x - a.x - (side.size() * sw + (side.size() - 1) * gap)) / 2); + for (size_t k = 0; k < side.size(); k++) { + ImVec2 p(sx + k * (sw + gap), sy); + dl->AddRectFilled(p, ImVec2(p.x + sw, p.y + sh), rgb(62, 56, 76)), dl->AddRectFilled(ImVec2(p.x, p.y + sh * 0.8f), ImVec2(p.x + sw, p.y + sh), rgb(26, 23, 34)); + if (side[k] == sel) dl->AddRect(ImVec2(p.x - 2, p.y - 2), ImVec2(p.x + sw + 2, p.y + sh + 2), rgb(237, 194, 84), 3, 0, 2.5f); + } + for (int i = 0; i < ndots; i++) { + const WarpRoom &r = L.rooms[dots[i].room]; + ImVec2 p; + if (r.side) { + size_t k = std::find(side.begin(), side.end(), dots[i].room) - side.begin(); + if (k >= side.size()) continue; + p = ImVec2(sx + k * (sw + gap) + sw / 2, sy + sh * 0.45f); + } else { + p = ImVec2(o.x + ((r.col - L.c0) * 10 + dots[i].x) * tw, o.y + ((r.row - L.r0) * 8 + dots[i].z) * tw); + } + dl->AddCircleFilled(p, std::max(2.2f, tw * (dots[i].hot ? 0.8f : 0.45f)), dots[i].hot ? rgb(255, 255, 255) : rgb(237, 194, 84)); + } + if (sel >= 0 && !L.rooms[sel].side) { + int c0, r0, c1, r1; + unit_box(L, sel, &c0, &r0, &c1, &r1); + ImVec2 p(o.x + (c0 - L.c0) * 10 * tw, o.y + (r0 - L.r0) * 8 * tw), q(o.x + (c1 - L.c0 + 1) * 10 * tw, o.y + (r1 - L.r0 + 1) * 8 * tw); + dl->AddRect(ImVec2(p.x - 2, p.y - 2), ImVec2(q.x + 2, q.y + 2), IM_COL32(237, 194, 84, 90), 5, 0, 7); + dl->AddRect(ImVec2(p.x - 1, p.y - 1), ImVec2(q.x + 1, q.y + 1), rgb(237, 194, 84), 4, 0, 3); + } +} + +void warp_draw_room(ImDrawList *dl, ImVec2 a, ImVec2 b, const WarpLevel &L, int room, const WarpDot *dots, int ndots) { + if (room < 0) return; + const WarpRoom &rm = L.rooms[room]; + if (rm.side) { // a passage seen from the side: floor, ceiling, a ladder at each end + float tw = std::min((b.x - a.x) / 22, (b.y - a.y) / 6), w = 20 * tw, h = 4 * tw; + ImVec2 p(a.x + (b.x - a.x - w) / 2, a.y + (b.y - a.y - h) / 2); + dl->AddRectFilled(p, ImVec2(p.x + w, p.y + h), rgb(62, 56, 76)); + dl->AddRectFilled(ImVec2(p.x, p.y + h - tw), ImVec2(p.x + w, p.y + h), rgb(26, 23, 34)), dl->AddRectFilled(p, ImVec2(p.x + w, p.y + tw * 0.5f), rgb(26, 23, 34)); + warp_icon(dl, WI_LADDER, ImVec2(p.x + tw * 1.6f, p.y + h / 2), tw * 2.2f, rgb(201, 184, 240)); + warp_icon(dl, WI_LADDER, ImVec2(p.x + w - tw * 1.6f, p.y + h / 2), tw * 2.2f, rgb(201, 184, 240)); + for (int i = 0; i < ndots; i++) + if (dots[i].room == room) pin(dl, ImVec2(p.x + w / 2, p.y + h / 2), tw * 0.6f, dots[i].hot); + return; + } + int c0, r0, c1, r1; + unit_box(L, room, &c0, &r0, &c1, &r1); + int cols = c1 - c0 + 1, rows = r1 - r0 + 1; + float tw = std::min((b.x - a.x - 8) / (cols * 10.0f), (b.y - a.y - 8) / (rows * 8.0f)); + ImVec2 o(floorf(a.x + (b.x - a.x - cols * 10 * tw) / 2), floorf(a.y + (b.y - a.y - rows * 8 * tw) / 2)); + for (auto &r : L.rooms) + if (!r.side && r.unit == rm.unit) cell(dl, r, ImVec2(o.x + (r.col - c0) * 10 * tw, o.y + (r.row - r0) * 8 * tw), tw, true); + for (int i = 0; i < ndots; i++) { + const WarpRoom &r = L.rooms[dots[i].room]; + if (r.side || r.unit != rm.unit) continue; + pin(dl, ImVec2(o.x + ((r.col - c0) * 10 + dots[i].x) * tw, o.y + ((r.row - r0) * 8 + dots[i].z) * tw), std::max(4.0f, tw * (dots[i].hot ? 0.5f : 0.3f)), dots[i].hot); + } +} diff --git a/runtime/ui/warp_rooms.h b/runtime/ui/warp_rooms.h new file mode 100644 index 000000000..d1eb06c9c --- /dev/null +++ b/runtime/ui/warp_rooms.h @@ -0,0 +1,40 @@ +// Dungeon rooms for the Warp tab's maps (runtime/ui/warp_rooms.mm): read from the extracted level files the first time +// a dungeon is shown - each top-down room's 10 x 8 tile grid (walls, water, pits, raised floor), its doors and +// landmarks (blocks, pots, chests, stairs, switches, statues, traps), and the arrival points (TagPlayerStart) by +// locator. Rooms joined by a wide open edge or one camera zone are one room (unit). Nothing is stored in the repo: the +// layout is the player's own files'. +#pragma once +#include +#include + +enum WarpTile { WT_FLOOR, WT_WALL, WT_WATER, WT_DEEP }; // (WT_DEEP: deep water, lava, a pit) +enum WarpFeat { + WF_BLOCK, WF_CBLOCK, WF_LOCKBLOCK, WF_POT, WF_CHEST, WF_STAIRS, WF_HOLE, WF_SPIKES, WF_BELT, WF_SWITCH, WF_STATUE, + WF_TRAP, WF_INSTRUMENT, WF_DOOR_LOCK, WF_DOOR_BOSS, WF_DOOR_SHUTTER, WF_DOOR_CRACKED, WF_DOOR_ONEWAY, WF_N +}; +struct WarpRoomFeat { int kind; float x, z; bool mirror = false; }; // (x, z: tiles from the room's corner, 0-10 / 0-8; + // mirror: a door shown from its other side) +struct WarpSpawn { std::string locator; float x, z; }; +struct WarpRoom { + std::string key; // the file's cell part ("04B", "05HL") + int col = 0, row = 0; // the cell on the level's grid (from the grid's origin) + bool side = false; // a side-view room (ladder passages): no cell, no tiles + unsigned char tile[80] = {}; // WarpTile, row by row + float elev[80] = {}; + std::vector feats; + std::vector spawns; + int unit = -1; // rooms with the same unit are one room (indices into WarpLevel::rooms of its first cell) +}; +struct WarpLevel { + std::string name; // "Lv01TailCave" + bool loaded = false, ok = false; + std::vector rooms; // top-down ones first, then the side-view ones + int c0 = 0, c1 = 0, r0 = 0, r1 = 0; // the used part of the grid (top-down rooms) + int cell(int col, int row) const; // the top-down room at a cell (-1: none) + int find_spawn(const char *locator) const; // the room holding that arrival point (-1) +}; +// where a warp into a top-down room puts Link (world units): a floor tile one step inside a doorway to another room of +// the level (else the floor tile nearest the middle) +bool warp_room_landing(const WarpLevel &L, int room, float *x, float *y, float *z); +// the level's rooms (loaded on first use; ok false when its files aren't there) +const WarpLevel &warp_level(const char *level); diff --git a/runtime/ui/warp_rooms.mm b/runtime/ui/warp_rooms.mm new file mode 100644 index 000000000..5ed00ca6f --- /dev/null +++ b/runtime/ui/warp_rooms.mm @@ -0,0 +1,341 @@ +// Dungeon rooms for the Warp tab's maps (warp_rooms.h). The level files are FixedHash containers (the layout is in +// tools/rando/fixedhash.py); a room (.leb) holds its actors and, for a top-down room, a 10 x 8 tile grid (1.5 units a +// tile, origin = the cell's corner: a cell is 15 x 12 units, column -> x, row -> z). How tiles, doors and joined cells +// are read follows tools/rando/rooms.py (load_room / catalog). +#include "warp_rooms.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +extern "C" { +#include "../hle.h" +extern const char *g_romfs_dir; +} + +namespace { + +struct Blob { std::vector d; }; +static bool read_file(const std::string &p, Blob &b) { + FILE *f = fopen(p.c_str(), "rb"); + if (!f) return false; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n <= 0 || n > (64 << 20)) { fclose(f); return false; } + b.d.resize((size_t)n); + bool ok = fread(b.d.data(), 1, (size_t)n, f) == (size_t)n; + fclose(f); + return ok; +} + +// One FixedHash container at `off` (bounds-checked: a damaged file gives no entries) +struct FH { + const uint8_t *d = nullptr; + size_t n = 0; + uint64_t eo = 0, ds = 0, ns = 0, nsize = 0; + int count = 0; + template T at(uint64_t o) const { T v{}; if (o + sizeof(T) <= n) memcpy(&v, d + o, sizeof(T)); return v; } + bool open(const uint8_t *data, size_t size, uint64_t off) { + d = data, n = size; + if (off + 8 > n) return false; + uint16_t nb = at(off + 2); + eo = ((off + 8 + 4ull * (nb + 1) + 3) & ~7ull) + 8; + uint64_t cnt = at(eo - 8) / 16; + if (eo > n || cnt > 100000) return false; + uint64_t oo = eo + 16 * cnt + 8; + ds = ((oo + 4 * cnt + 7) & ~7ull) + 8; + uint64_t dsize = at(ds - 8); + ns = ((ds + dsize + 3) & ~3ull) + 4; + nsize = at(ns - 4); + if (ds > n || ns > n || ns + nsize > n) return false; + count = (int)cnt; + return true; + } + const char *name(int i) const { + uint16_t no = at(eo + 16ull * i + 2); + return ns + no < n && nsize ? (const char *)d + ns + no : ""; + } + bool is_hash(int i) const { return at(eo + 16ull * i) <= 0xFFED; } + uint64_t data_off(int i) const { return ds + at(eo + 16ull * i + 12); } + bool child(int i, FH &out) const { return is_hash(i) && out.open(d, n, data_off(i)); } + // a raw entry's bytes (pointer, size) + const uint8_t *raw(int i, uint64_t *size) const { + uint64_t o = data_off(i), s = at(o); + if (is_hash(i) || o + 8 + s > n) return *size = 0, nullptr; + return *size = s, d + o + 8; + } + int find(const char *nm) const { + for (int i = 0; i < count; i++) + if (!strcmp(name(i), nm)) return i; + return -1; + } + const char *str(uint32_t off) const { return off < nsize ? (const char *)d + ns + off : ""; } // (root's names) +}; + +// Door actors (rooms.py DOORS): ObjLockDoor, ObjBossDoor, ObjClosedDoor, ObjOnewayDoor, the cracked / bombable walls +static int door_kind(int type) { + switch (type) { + case 0xED: return WF_DOOR_LOCK; + case 0xEE: return WF_DOOR_BOSS; + case 0xEF: return WF_DOOR_SHUTTER; + case 0x1A3: case 0x1A4: return WF_DOOR_ONEWAY; + case 0x121: case 0x123: case 0x131: case 0x132: case 0x133: case 0x139: case 0x20c: return WF_DOOR_CRACKED; + } + return -1; +} +// Landmarks by actor name (the files cut names to 12 characters: ObjPurpleUne...) +static int feat_kind(const char *nm) { + static const struct { const char *sub; int kind; } k[] = { + {"FullMoon", WF_INSTRUMENT}, {"ConchHorn", WF_INSTRUMENT}, {"SeaLily", WF_INSTRUMENT}, {"SurfHarp", WF_INSTRUMENT}, + {"WindMari", WF_INSTRUMENT}, {"CoralTri", WF_INSTRUMENT}, {"EveningCalm", WF_INSTRUMENT}, {"ThunderD", WF_INSTRUMENT}, + {"TreasureBox", WF_CHEST}, {"DungeonPot", WF_POT}, {"ClosedPot", WF_POT}, {"BigPot", WF_POT}, + {"LockBlock", WF_LOCKBLOCK}, {"PurpleUne", WF_CBLOCK}, {"OrangeUne", WF_CBLOCK}, + {"SquareBlock", WF_BLOCK}, {"IceBlock", WF_BLOCK}, {"CrackedBl", WF_BLOCK}, + {"Downstairs", WF_STAIRS}, {"Upstairs", WF_STAIRS}, {"DungeonSt", WF_STAIRS}, {"Lv9Stair", WF_STAIRS}, + {"HoleLowe", WF_HOLE}, {"SpikeTile", WF_SPIKES}, {"BeltConv", WF_BELT}, + {"CrystalSw", WF_SWITCH}, {"FootSwitch", WF_SWITCH}, {"Lever", WF_SWITCH}, + {"DungeonOw", WF_STATUE}, {"ElephantS", WF_STATUE}, {"HorseNeck", WF_STATUE}, {"TowerPillar", WF_STATUE}, + {"BladeTrap", WF_TRAP}, {"Beamos", WF_TRAP}, + }; + for (auto &e : k) + if (strstr(nm, e.sub)) return e.kind; + return -1; +} + +// Eagle's Tower's rooms before the floor collapses (rooms.py DEAD): no seed or save shows them +static bool dead_room(const std::string &level, const std::string &key) { + if (level != "Lv07EagleTower") return false; + static const char *const k[] = {"05F", "05G", "06E", "06F", "06G", "06H", "07E", "07F", "07G", "07H", "08F", "08G", "05E"}; + for (const char *x : k) + if (key == x) return true; + return false; +} + +static void load_room(const std::string &level, const std::string &key, const Blob &b, std::vector &top, + std::vector &side) { + FH root; + if (!root.open(b.d.data(), b.d.size(), 0)) return; + WarpRoom rm; + rm.key = key; + // the grid: a top-down room's when it is 10 x 8 tiles at a cell's corner + bool grid = false; + float ox = 0, oz = 0; + int gi = root.find("grid"); + FH g; + if (gi >= 0 && root.child(gi, g)) { + int ii = g.find("info"), di = g.find("data"); + uint64_t is = 0, dsz = 0; + const uint8_t *info = ii >= 0 ? g.raw(ii, &is) : nullptr, *data = di >= 0 ? g.raw(di, &dsz) : nullptr; + if (info && is >= 16 && data && dsz >= 80 * 16) { + uint16_t h, w; + memcpy(&h, info, 2), memcpy(&w, info + 2, 2), memcpy(&ox, info + 8, 4), memcpy(&oz, info + 12, 4); + float fc = ox / 15.0f, fr = oz / 12.0f; + if (h == 8 && w == 10 && fabsf(fc - roundf(fc)) < 0.01f && fabsf(fr - roundf(fr)) < 0.01f && fc >= 0 && fr >= 0) { + grid = true; + rm.col = (int)roundf(fc), rm.row = (int)roundf(fr); + for (int i = 0; i < 80; i++) { + const uint8_t *t = data + 16 * i; + float e; + memcpy(&e, t + 12, 4); + rm.elev[i] = e; + bool wall = (t[0] & 2) && !(t[2] & 8); // (bit 3 of byte 2: a doorway's tile, open even when it reads solid) + bool water = t[2] & 4; + rm.tile[i] = (water || (t[0] & 1)) && e < -0.3f ? WT_DEEP : water ? WT_WATER : wall ? WT_WALL : WT_FLOOR; + } + } + } + } + rm.side = !grid; + int ai = root.find("actor"); + FH acts; + if (ai >= 0 && root.child(ai, acts)) + for (int i = 0; i < acts.count; i++) { + uint64_t sz = 0; + const uint8_t *a = acts.raw(i, &sz); + if (!a || sz < 0x78) continue; + uint32_t no; + uint16_t type; + float px, pz; + memcpy(&no, a + 8, 4), memcpy(&type, a + 0xC, 2), memcpy(&px, a + 0x14, 4), memcpy(&pz, a + 0x1C, 4); + float lx = (px - ox) / 1.5f, lz = (pz - oz) / 1.5f; + if (type == 0x18F) { // TagPlayerStart: its first parameter (a string) is the locator the warp list names + uint32_t v, t; + memcpy(&v, a + 0x38, 4), memcpy(&t, a + 0x3C, 4); + if (t == 4) rm.spawns.push_back({root.str(v), lx, lz}); + continue; + } + if (!grid) continue; + int k = door_kind(type); + if (k < 0) k = feat_kind(root.str(no)); + if (k >= 0 && lx > -1 && lx < 11 && lz > -1 && lz < 9) rm.feats.push_back({k, lx, lz}); + } + if (grid) top.push_back(std::move(rm)); + else if (!rm.spawns.empty()) side.push_back(std::move(rm)); +} + +static int find_root(std::vector &p, int k) { + while (p[k] != k) k = p[k] = p[p[k]]; + return k; +} + +static void load_level(WarpLevel &L) { + L.loaded = true; + std::string dir = std::string(g_romfs_dir ? g_romfs_dir : "extracted/romfs") + "/region_common/level/" + L.name; + DIR *d = opendir(dir.c_str()); + if (!d) return; + std::vector files; + for (struct dirent *e; (e = readdir(d));) { + std::string f = e->d_name; + if (f.size() > L.name.size() + 5 && !f.compare(0, L.name.size() + 1, L.name + "_") && !f.compare(f.size() - 4, 4, ".leb")) + files.push_back(f); + } + closedir(d); + std::sort(files.begin(), files.end()); + std::vector top, side; + for (auto &f : files) { + std::string key = f.substr(L.name.size() + 1, f.size() - L.name.size() - 5); + if (dead_room(L.name, key)) continue; + Blob b; + if (read_file(dir + "/" + f, b)) load_room(L.name, key, b, top, side); + } + if (top.empty() && side.empty()) return; + L.rooms = std::move(top); + size_t ntop = L.rooms.size(); + for (auto &r : side) L.rooms.push_back(std::move(r)); + // a door actor stands in one of the two rooms it joins, and its tiles read as solid in both: open them, and show + // the door from the other side too + for (size_t i = 0; i < ntop; i++) { + WarpRoom &a = L.rooms[i]; + for (size_t k = 0, nf = a.feats.size(); k < nf; k++) { + WarpRoomFeat f = a.feats[k]; + if (f.kind < WF_DOOR_LOCK || f.mirror) continue; + int dc = f.x > 8.8f ? 1 : f.x < 1.2f ? -1 : 0, dr = !dc && f.z > 6.8f ? 1 : !dc && f.z < 1.2f ? -1 : 0; + if (!dc && !dr) continue; + int nb = L.cell(a.col + dc, a.row + dr); + for (int s = 0; s < 2; s++) { + WarpRoom *r = s ? (nb >= 0 ? &L.rooms[nb] : nullptr) : &a; + if (!r) continue; + float p = dc ? f.z : f.x, lo = floorf(p - 0.5f), hi = floorf(p); + int edge = dc ? ((dc > 0) != (s == 1) ? 9 : 0) : ((dr > 0) != (s == 1) ? 7 : 0); + for (int t = (int)lo; t <= (int)hi; t++) { + if (t < 0 || t >= (dc ? 8 : 10)) continue; + int idx = dc ? t * 10 + edge : edge * 10 + t; + if (r->tile[idx] == WT_WALL) r->tile[idx] = WT_FLOOR; + } + if (s) r->feats.push_back({f.kind, f.x - 10.0f * dc, f.z - 8.0f * dr, true}); + } + } + } + // joined cells: a shared edge with 3+ open tiles on both sides ... + std::vector par(ntop); + for (size_t i = 0; i < ntop; i++) par[i] = (int)i; + auto open_t = [](const WarpRoom &r, int row, int col) { return r.tile[row * 10 + col] != WT_WALL; }; + for (size_t i = 0; i < ntop; i++) { + const WarpRoom &a = L.rooms[i]; + int e = L.cell(a.col + 1, a.row), s = L.cell(a.col, a.row + 1); + if (e >= 0) { + int n = 0; + for (int row = 0; row < 8; row++) n += open_t(a, row, 9) && open_t(L.rooms[e], row, 0); + if (n >= 3) par[find_root(par, (int)i)] = find_root(par, e); + } + if (s >= 0) { + int n = 0; + for (int col = 0; col < 10; col++) n += open_t(a, 7, col) && open_t(L.rooms[s], 0, col); + if (n >= 3) par[find_root(par, (int)i)] = find_root(par, s); + } + } + // ... and cells of one camera zone (the level's .lvb: its areas, and each zone's list of area indices) + Blob lb; + FH root, areas, zones; + if (read_file(dir + "/" + L.name + ".lvb", lb) && root.open(lb.d.data(), lb.d.size(), 0)) { + int ai = root.find("area"), zi = root.find("zone"); + if (ai >= 0 && zi >= 0 && root.child(ai, areas) && root.child(zi, zones)) + for (int z = 0; z < zones.count; z++) { + uint64_t sz = 0; + const uint8_t *zd = zones.raw(z, &sz); + if (!zd || sz < 0x9c + 7) continue; + int n = zd[0x9c + 6], first = -1; + for (int k = 0; k < n && 0x9c + 7 + (uint64_t)k < sz; k++) { + int idx = zd[0x9c + 7 + k]; + if (idx >= areas.count) continue; + std::string an = areas.name(idx); + if (an.compare(0, L.name.size() + 1, L.name + "_")) continue; + an = an.substr(L.name.size() + 1); + for (size_t i = 0; i < ntop; i++) + if (L.rooms[i].key == an) { + if (first < 0) first = (int)i; + else par[find_root(par, (int)i)] = find_root(par, first); + } + } + } + } + for (size_t i = 0; i < ntop; i++) L.rooms[i].unit = find_root(par, (int)i); + L.c0 = L.r0 = 1 << 20, L.c1 = L.r1 = -1; + for (size_t i = 0; i < ntop; i++) { + const WarpRoom &r = L.rooms[i]; + L.c0 = std::min(L.c0, r.col), L.c1 = std::max(L.c1, r.col), L.r0 = std::min(L.r0, r.row), L.r1 = std::max(L.r1, r.row); + } + if (!ntop) L.c0 = L.r0 = 0, L.c1 = L.r1 = 0; + L.ok = true; + LOG("[warp] %s: %zu rooms (%zu seen from the side)\n", L.name.c_str(), L.rooms.size(), L.rooms.size() - ntop); +} + +} // namespace + +int WarpLevel::cell(int col, int row) const { + for (size_t i = 0; i < rooms.size(); i++) + if (!rooms[i].side && rooms[i].col == col && rooms[i].row == row) return (int)i; + return -1; +} +int WarpLevel::find_spawn(const char *locator) const { + for (size_t i = 0; i < rooms.size(); i++) + for (auto &s : rooms[i].spawns) + if (s.locator == locator) return (int)i; + return -1; +} + +const WarpLevel &warp_level(const char *level) { + static std::map g_levels; + WarpLevel &L = g_levels[level]; + if (!L.loaded) L.name = level, load_level(L); + return L; +} + +bool warp_room_landing(const WarpLevel &L, int room, float *x, float *y, float *z) { + if (room < 0 || room >= (int)L.rooms.size() || L.rooms[room].side) return false; + const WarpRoom &r = L.rooms[room]; + auto at = [&](int row, int col) { return r.tile[row * 10 + col]; }; + int best_r = -1, best_c = -1, best = 1 << 30; + // doorways: an open edge tile with a floor tile one step in, toward a room of the level (not the way out) + static const int side_d[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // N S W E as (row, col) steps outward + for (int s = 0; s < 4; s++) { + int dr = side_d[s][0], dc = side_d[s][1]; + bool to_room = L.cell(r.col + dc, r.row + dr) >= 0; + int n = dr ? 10 : 8; + for (int k = 0; k < n; k++) { + int er = dr < 0 ? 0 : dr > 0 ? 7 : k, ec = dc < 0 ? 0 : dc > 0 ? 9 : k; + int ir = er - dr, ic = ec - dc; + if (at(er, ec) == WT_WALL || at(ir, ic) != WT_FLOOR) continue; + int score = (to_room ? 0 : 1000) + abs(2 * ir - 7) + abs(2 * ic - 9); // (a doorway to another room, then central) + if (score < best) best = score, best_r = ir, best_c = ic; + } + } + if (best_r < 0) + for (int i = 0; i < 80; i++) + if (r.tile[i] == WT_FLOOR) { + int score = abs(2 * (i / 10) - 7) + abs(2 * (i % 10) - 9); + if (score < best) best = score, best_r = i / 10, best_c = i % 10; + } + if (best_r < 0) return false; + *x = r.col * 15.0f + (best_c + 0.5f) * 1.5f; + *z = r.row * 12.0f + (best_r + 0.5f) * 1.5f; + *y = r.elev[best_r * 10 + best_c]; + return true; +} diff --git a/runtime/ui/warp_tab.mm b/runtime/ui/warp_tab.mm index 4639127f4..891d87ac7 100644 --- a/runtime/ui/warp_tab.mm +++ b/runtime/ui/warp_tab.mm @@ -1,24 +1,36 @@ -// Warp tab: pick any spawn point in the game (overworld, houses & caves, dungeons) and jump Link there. -// The list comes from the level files (runtime/ui/warp_data.h, generated by tools/warp_table.py); the -// jump itself is queued to the game thread (runtime/hle/warp.c), which uses the game's own level-jump -// request (the same one the GameControl.RequestLevelJump event action fills). +// Warp tab: pick a place and jump Link there. Two columns as the other tabs: on the left a list - Overworld, Houses & +// caves, Dungeons - with each section's areas A-Z, opening like folders onto their places; on the +// right a map of the place under the cursor (warp_maps.h): the island with its square, or the dungeon's floor and a +// closer sketch of the room. Every top-down dungeon room is a place: rooms with an arrival point of their own warp +// there, the others warp to the level's nearest one and Link is then moved just inside the room (warp_request_place). +// The places come from runtime/ui/warp_data.h (generated by tools/warp_table.py) and the dungeon rooms from the +// level files (warp_rooms.h); the jump itself is queued to the game thread (runtime/hle/warp.c). #include "../game_addresses.h" #include +#include #include #include #include #include +#include +#include +#include +#include +#include + #include "i18n.h" #include "imgui.h" #include "option_rows.h" #include "osk.h" -#include "plastic.h" #include "warp_data.h" +#include "warp_maps.h" +#include "warp_rooms.h" extern "C" { #include "../hle.h" void warp_request(const char *level, const char *locator); +void warp_request_place(const char *level, const char *locator, float x, float y, float z); int warp_available(void); bool warp_blocked(const char *locator); } @@ -26,9 +38,6 @@ // GA(PLAYER_STATUS_PTR): main + 0x1CC1368: PlayerStatus* (current level name at +0) static const int kNumSpots = (int)(sizeof kWarpSpots / sizeof kWarpSpots[0]); -static int g_group = -1; -static char g_filter[64]; -static char g_last[160]; static const char *current_level(void) { uint64_t ps = *(uint64_t *)(uintptr_t)GA(PLAYER_STATUS_PTR); @@ -41,171 +50,539 @@ return ""; } -static bool matches(const WarpSpot &w, const char *f) { - if (!*f) return true; - // (the names as shown too: in the menus' language) - const char *fields[] = {w.area, w.label, w.level, w.locator, L(w.area), L_line(w.label).c_str()}; - for (const char *s : fields) { - for (const char *p = s; *p; p++) { - int i = 0; - while (f[i] && p[i] && tolower((unsigned char)p[i]) == tolower((unsigned char)f[i])) i++; - if (!f[i]) return true; - } +// ---------- places ---------- +// A place in the list: a spot of warp_data.h, or a dungeon room (a unit of warp_rooms.h) +struct Entry { + int spot = -1; // kWarpSpots index (-1: a room) + const char *level = ""; + int room = -1; // dungeon: the room (its unit's first cell) in warp_level(level).rooms + std::string name, sub; // English (shown through L_line / L) + std::string desc; // the description under the map + std::string id; // "S//" or "R//" (the rows' and the map's key) + int icon = WI_PIN; + int sq_r = -1, sq_c = -1; // the island square (overworld spots, and houses / caves by their door) +}; +struct Area { + int sec; // 0 Overworld, 1 Houses & caves, 2 Dungeons + std::string name; // English + std::vector entries; + std::vector spots; // (its spots in warp_data.h) + const char *level = nullptr; // dungeon: its level (rooms built on first use) + bool built = false; + int region = -1; // overworld: the island region +}; +enum { SEC_OW, SEC_IN, SEC_DG }; +static const char *const kSecName[] = {"Overworld", "Houses & caves", "Dungeons"}; + +static std::vector g_areas; +static WarpIsland g_island; +static std::vector g_spot_r, g_spot_c, g_spot_icon; // per spot: its square on the island (-1), its icon + +// "Outside Mt. Tamaranch cave (east) (02P) #2" -> "Mt. Tamaranch cave (east)" (the code and the count dropped) +static std::string clean_label(const char *l) { + std::string s = l; + size_t h = s.rfind(" #"); + if (h != std::string::npos && h + 3 == s.size() && isdigit((unsigned char)s[h + 2])) s.erase(h); + for (size_t p; (p = s.find(" (")) != std::string::npos;) { // codes: "(02P)", "(08D_b)", "(Lv07_06B_from_02B_02F)" + size_t e = s.find(')', p); + if (e == std::string::npos) break; + std::string in = s.substr(p + 2, e - p - 2); + bool code = (in.size() >= 3 && isdigit((unsigned char)in[0]) && isdigit((unsigned char)in[1]) && isupper((unsigned char)in[2])) || + !in.compare(0, 2, "Lv"); + if (!code) { size_t nx = s.find(" (", e); if (nx == std::string::npos) break; std::string head = s.substr(0, e + 1); + std::string rest = clean_label(s.substr(e + 1).c_str()); return head + rest; } + s.erase(p, e - p + 1); } + size_t sl = s.find(" / "); // ("Color Dungeon / PanelLv10ClothesDungeon") + if (sl != std::string::npos) s.erase(sl); + return s; +} +static bool code_of(const char *label, int *r, int *c) { // "(02P)" -> row 1, column 15 + for (const char *p = strchr(label, '('); p; p = strchr(p + 1, '(')) + if (isdigit((unsigned char)p[1]) && isdigit((unsigned char)p[2]) && p[3] >= 'A' && p[3] <= 'P') { + *r = (p[1] - '0') * 10 + (p[2] - '0') - 1, *c = p[3] - 'A'; + return *r >= 0 && *r < 16; + } return false; } +static const char *compass(float fy, float fx) { // (0-1 across the area) + static const char *const k[3][3] = {{"north-west", "north", "north-east"}, {"west", "middle", "east"}, {"south-west", "south", "south-east"}}; + int v = fy < 1 / 3.0f ? 0 : fy > 2 / 3.0f ? 2 : 1, h = fx < 1 / 3.0f ? 0 : fx > 2 / 3.0f ? 2 : 1; + return k[v][h]; +} +static int icon_of(const WarpSpot &w) { + const char *l = w.label; + if (strstr(l, "Manbo")) return WI_NOTE; + if (strstr(l, "Warp hole")) return WI_WARP; + if (w.group != 0) return strstr(l, "Entrance") ? WI_ENTRANCE : strstr(l, "stairs") ? WI_STAIRS : strstr(l, "ladder") ? WI_LADDER : WI_ROOM; + if (strstr(l, "Phone booth")) return WI_PHONE; + if (strstr(l, "Great Fairy")) return WI_FAIRY; + if (strstr(l, "Level ") || strstr(l, "Color Dungeon") || strstr(l, "Wind Fish's Egg")) return WI_DUNGEON; + if (strstr(l, "boat")) return WI_BOAT; + for (const char *k : {"cave", "passage", "Tunnel", "hideout", "hollow", "well", "exit", "Maw entrance"}) + if (strstr(l, k)) return WI_CAVE; + return !strncmp(l, "Outside ", 8) ? WI_DOOR : WI_PIN; +} -// Returns true when a warp was started (the menu then closes so the jump plays out). -static bool spot(const WarpSpot &w, bool show_area, bool enabled) { - bool swim = warp_blocked(w.locator); - enabled = enabled && !swim; - char lbl[512]; // (in the menus' language: the area and spot names are put together from parts, i18n.h) - if (show_area) snprintf(lbl, sizeof lbl, "%s - %s", L(w.area), L_line(w.label).c_str()); - else snprintf(lbl, sizeof lbl, "%s", L_line(w.label).c_str()); - const char *desc = !swim ? NULL : strncmp(w.locator, "Warp_", 5) ? LN("You arrive here swimming, so you need the Flippers.") - : LN("This is where Manbo's Mambo drops you. You can't warp here directly."); - ImGui::PushID((int)(&w - kWarpSpots)); // (spot names repeat across levels) - if (!enabled) opt_disable_next(); - opt_shown_next(); - bool go = opt_action(lbl, "", desc, false); - ImGui::PopID(); - if (go) { - warp_request(w.level, w.locator); - snprintf(g_last, sizeof g_last, "%s - %s", w.area, w.label); +static void build_places(void) { + g_spot_r.assign(kNumSpots, -1), g_spot_c.assign(kNumSpots, -1), g_spot_icon.assign(kNumSpots, WI_PIN); + std::map area_of; // "|" -> g_areas index + for (int i = 0; i < kNumSpots; i++) { + const WarpSpot &w = kWarpSpots[i]; + std::string k = std::to_string(w.group) + "|" + w.area; + if (!area_of.count(k)) { + area_of[k] = (int)g_areas.size(); + Area a; + a.sec = w.group, a.name = w.area; + if (w.group == SEC_DG) a.level = w.level; + g_areas.push_back(a); + } + g_areas[area_of[k]].spots.push_back(i); + g_spot_icon[i] = icon_of(w); + int r, c; + if (w.group == 0 && code_of(w.label, &r, &c)) g_spot_r[i] = r, g_spot_c[i] = c; + } + // the island: each square goes to the region of the nearest overworld spot + std::map region; + for (auto &a : g_areas) + if (a.sec == SEC_OW && g_island.nregions < 32) a.region = region[a.name] = g_island.nregions, g_island.regions[g_island.nregions++] = a.name.c_str(); + for (int r = 0; r < 16; r++) + for (int c = 0; c < 16; c++) { + float best = 1e9f; + for (int i = 0; i < kNumSpots; i++) { + if (g_spot_r[i] < 0) continue; + float d = (float)((g_spot_r[i] - r) * (g_spot_r[i] - r)) + 1.1f * (float)((g_spot_c[i] - c) * (g_spot_c[i] - c)); + if (d < best) best = d, g_island.sq[r][c].region = region[kWarpSpots[i].area]; + } + } + static const int pri[] = {WI_DUNGEON, WI_WARP, WI_FAIRY, WI_PHONE, WI_BOAT, WI_DOOR, WI_CAVE, WI_NOTE, WI_PIN}; + for (int p : pri) + for (int i = 0; i < kNumSpots; i++) + if (g_spot_r[i] >= 0 && g_spot_icon[i] == p) { + WarpIslandSquare &q = g_island.sq[g_spot_r[i]][g_spot_c[i]]; + bool have = false; + for (int k = 0; k < q.n; k++) have |= q.icons[k] == p; + if (!have && q.n < 4) q.icons[q.n++] = p; + } + // overworld and houses / caves: their entries now (dungeons: on first use, from the level files) + std::map door_of; // "Mt. Tamaranch cave (east)" -> its first outside spot + for (int i = 0; i < kNumSpots; i++) + if (kWarpSpots[i].group == 0 && !strncmp(kWarpSpots[i].label, "Outside ", 8)) { + std::string b = clean_label(kWarpSpots[i].label).substr(8); + if (!door_of.count(b)) door_of[b] = i; + } + for (auto &a : g_areas) { + if (a.sec == SEC_DG) continue; + int n_ent = 0, k_ent = 0; + for (int i : a.spots) n_ent += !!strstr(kWarpSpots[i].label, "Entrance"); + for (int i : a.spots) { + const WarpSpot &w = kWarpSpots[i]; + Entry e; + e.spot = i, e.level = w.level, e.icon = g_spot_icon[i], e.id = std::string("S/") + w.level + "/" + w.locator; + std::string n = clean_label(w.label); + if (a.sec == SEC_OW) { + e.sq_r = g_spot_r[i], e.sq_c = g_spot_c[i]; + if (!n.compare(0, 15, "Manbo's Mambo: ")) e.name = n.substr(15), e.sub = "Manbo's Mambo drop point"; + else if (!n.compare(0, 11, "Warp hole: ")) e.name = "Warp hole", e.sub = n.substr(11); + else if (!n.compare(0, 8, "Outside ")) e.name = n.substr(8), e.sub = "Outside the door"; + else e.name = n, e.sub = "Out in the open"; + } else { + auto d = door_of.find(a.name); + if (d != door_of.end()) e.sq_r = g_spot_r[d->second], e.sq_c = g_spot_c[d->second]; + int r, c; + bool code = sscanf(w.label, "Room %2d%c", &r, (char *)&c) == 2 || code_of(w.label, &r, &c); + (void)code; + if (strstr(w.label, "Entrance")) k_ent++, e.name = n_ent > 1 ? "Door " + std::to_string(k_ent) : "Just inside", e.sub = "Inside"; + else e.name = strstr(w.label, "stairs") ? "Stairs" : "Room", e.sub = "Inside"; + } + a.entries.push_back(e); + } + // the same name twice in an area: which side of the others it is on, or a number + for (size_t i = 0; i < a.entries.size(); i++) { + std::vector same; + for (size_t j = 0; j < a.entries.size(); j++) + if (a.entries[j].name == a.entries[i].name) same.push_back(j); + if (same.size() < 2 || same[0] != i) continue; + float mr = 0, mc = 0; + for (size_t j : same) mr += a.entries[j].sq_r, mc += a.entries[j].sq_c; + mr /= same.size(), mc /= same.size(); + std::map seen; + for (size_t j : same) { + Entry &e = a.entries[j]; + float dr = e.sq_r - mr, dc = e.sq_c - mc; + std::string q = a.sec != SEC_OW || (fabsf(dr) < 0.3f && fabsf(dc) < 0.3f) ? "door" + : fabsf(dc) >= fabsf(dr) ? (dc < 0 ? "west side" : "east side") : (dr < 0 ? "north side" : "south side"); + int k = ++seen[q]; + e.name += " (" + (q == "door" ? "door " + std::to_string(k) : k > 1 ? q + " " + std::to_string(k) : q) + ")"; + } + } + for (auto &e : a.entries) + e.desc = std::string(L_line(a.name)) + ". " + L_line(e.sub) + "."; + std::sort(a.entries.begin(), a.entries.end(), [](const Entry &x, const Entry &y) { return x.name < y.name; }); + a.built = true; } - return go; + std::sort(g_areas.begin(), g_areas.end(), [](const Area &x, const Area &y) { return x.sec != y.sec ? x.sec < y.sec : x.name < y.name; }); } -// The tab in two columns (as the Controls tab's Configure inputs): the groups, the search box and the group's areas -// on the left; the chosen area's spots (or the search's matches) on the right. -struct Area { int first, end; }; // (a run of spots of one area, kWarpSpots[first..end)) -static int areas_of(int group, Area *out, int cap) { - int n = 0; - for (int i = 0; i < kNumSpots;) { - int j = i; - while (j < kNumSpots && kWarpSpots[j].group == kWarpSpots[i].group && !strcmp(kWarpSpots[j].area, kWarpSpots[i].area)) j++; - if (kWarpSpots[i].group == group && n < cap) out[n++] = {i, j}; - i = j; +// A dungeon's rooms as places: one per room (its joined cells together), named by what is in it and where it lies +static void build_dungeon(Area &a) { + a.built = true; + const WarpLevel &LV = warp_level(a.level); + if (!LV.ok) { // (the level files aren't there: its arrival points as listed) + for (int i : a.spots) { + Entry e; + e.spot = i, e.level = a.level, e.name = kWarpSpots[i].label, e.icon = g_spot_icon[i]; + e.id = std::string("S/") + a.level + "/" + kWarpSpots[i].locator; + a.entries.push_back(e); + } + return; + } + std::map known; // the level's arrival points the warp list has (their spot) + for (int i : a.spots) known[kWarpSpots[i].locator] = i; + std::map names; + int nside = 0; + for (int ri = 0; ri < (int)LV.rooms.size(); ri++) { + const WarpRoom &r = LV.rooms[ri]; + Entry e; + e.level = a.level, e.room = ri, e.id = "R/" + std::string(a.level) + "/" + r.key; + if (r.side) { + int sp = -1; + for (auto &s : r.spawns) + if (known.count(s.locator)) sp = known[s.locator]; + if (sp < 0) continue; // (side passages: only through their own arrival points) + e.spot = sp, e.icon = WI_LADDER, e.name = "Side passage " + std::to_string(++nside), e.sub = "Seen from the side"; + e.desc = L("A ladder passage, seen from the side."); + a.entries.push_back(e); + continue; + } + if (r.unit != ri) continue; // (one place per room: its first cell) + bool instr = false, chest = false, stairs = false, entrance = false, big = false; + int c0 = r.col, c1 = r.col, r0 = r.row, r1 = r.row, nchest = 0; + for (const WarpRoom &x : LV.rooms) { + if (x.side || x.unit != ri) continue; + big |= &x != &r; + c0 = std::min(c0, x.col), c1 = std::max(c1, x.col), r0 = std::min(r0, x.row), r1 = std::max(r1, x.row); + for (auto &f : x.feats) instr |= f.kind == WF_INSTRUMENT, nchest += f.kind == WF_CHEST, stairs |= f.kind == WF_STAIRS; + for (auto &s : x.spawns) { + auto k = known.find(s.locator); + if (k == known.end() || s.locator == "OcarinaWarpPoint") continue; + bool ent = strstr(kWarpSpots[k->second].label, "Entrance") != NULL; + entrance |= ent; + // the room's own arrival point: an entrance first, then stairs, then any + if (e.spot < 0 || ent || (strstr(kWarpSpots[k->second].label, "stairs") && !strstr(kWarpSpots[e.spot].label, "Entrance"))) + e.spot = k->second; + } + } + chest = nchest > 0; + float fy = ((r0 + r1) / 2.0f - LV.r0 + 0.5f) / (LV.r1 - LV.r0 + 1), fx = ((c0 + c1) / 2.0f - LV.c0 + 0.5f) / (LV.c1 - LV.c0 + 1); + const char *what = instr ? "Instrument room" : entrance ? "Entrance hall" : big ? "Big hall" : chest ? "Chest room" : stairs ? "Stairs room" : "Room"; + e.icon = instr ? WI_NOTE : entrance ? WI_ENTRANCE : stairs ? WI_STAIRS : WI_ROOM; + e.name = std::string(what) + ", " + compass(fy, fx); + int k = ++names[e.name]; + if (k > 1) e.name += " " + std::to_string(k); + e.sub = e.spot >= 0 ? "Arrival point" : "Landing by a doorway"; + // the description: what is in it + std::string d; + auto add = [&d](const std::string &s) { d += d.empty() ? s : ", " + s; }; + int nf[WF_N] = {}; + for (const WarpRoom &x : LV.rooms) + if (!x.side && x.unit == ri) + for (auto &f : x.feats) nf[f.kind] += !f.mirror; + char buf[64]; + if (nf[WF_INSTRUMENT]) add(L("an instrument")); + if (nf[WF_CHEST]) snprintf(buf, sizeof buf, nf[WF_CHEST] > 1 ? L("%d chests") : L("a chest"), nf[WF_CHEST]), add(buf); + if (nf[WF_STAIRS]) add(L("stairs")); + if (nf[WF_DOOR_LOCK]) add(L("a key door")); + if (nf[WF_DOOR_BOSS]) add(L("the boss door")); + if (nf[WF_LOCKBLOCK]) add(L("a key block")); + if (nf[WF_SWITCH]) add(L("a switch")); + if (nf[WF_DOOR_CRACKED]) add(L("a wall to bomb")); + e.desc = d.empty() ? std::string(L("An empty room.")) : std::string(L("In this room: ")) + d + "."; + a.entries.push_back(e); } - return n; + std::sort(a.entries.begin(), a.entries.end(), [](const Entry &x, const Entry &y) { return x.name < y.name; }); } + +static Area *find_area(int sec, const char *name) { + for (auto &a : g_areas) + if (a.sec == sec && a.name == name) return &a; + return nullptr; +} +static void ensure_built(Area &a) { + if (!a.built && a.sec == SEC_DG) build_dungeon(a); +} +// ---------- warping ---------- +static bool entry_blocked(const Entry &e) { return e.spot >= 0 && warp_blocked(kWarpSpots[e.spot].locator); } +static void do_warp(const Entry &e) { + if (e.spot >= 0) { + warp_request(kWarpSpots[e.spot].level, kWarpSpots[e.spot].locator); + LOG("[warp] menu: %s / %s\n", e.level, e.name.c_str()); + return; + } + // a room without an arrival point: the nearest one of the level the list has, then Link is moved into the room + const WarpLevel &LV = warp_level(e.level); + const WarpRoom &to = LV.rooms[e.room]; + float x, y, z, best = 1e30f; + const char *loc = nullptr; + if (!warp_room_landing(LV, e.room, &x, &y, &z)) return; + for (int i = 0; i < kNumSpots; i++) { + const WarpSpot &w = kWarpSpots[i]; + if (strcmp(w.level, e.level) || !strcmp(w.locator, "OcarinaWarpPoint") || warp_blocked(w.locator)) continue; + int ri = LV.find_spawn(w.locator); + if (ri < 0 || LV.rooms[ri].side) continue; + float d = (float)((LV.rooms[ri].col - to.col) * (LV.rooms[ri].col - to.col) + (LV.rooms[ri].row - to.row) * (LV.rooms[ri].row - to.row)); + if (d < best) best = d, loc = w.locator; + } + if (!loc) return; + warp_request_place(e.level, loc, x, y, z); + LOG("[warp] menu: %s / %s (room %s, by %s)\n", e.level, e.name.c_str(), to.key.c_str(), loc); +} + +// ---------- the tab ---------- +static char g_filter[64]; +static std::set g_open; // open sections ("S") and areas ("A|") +static std::string g_show; // the row the map shows: an entry id, or an area key +static int g_show_area = -1; // (its area) +static std::string g_focus_prev, g_focus_now, g_focus_req; // the controller's row (last frame / this frame), a request +static int g_last_frame = -10; +static bool g_back_took; +extern "C" bool warp_tab_took_back(void) { return g_back_took; } + +static std::string area_key(const Area &a) { return "A" + std::to_string(a.sec) + "|" + a.name; } static bool area_here(const Area &a, const char *cur) { - for (int k = a.first; k < a.end; k++) - if (!strcmp(kWarpSpots[k].level, cur) && strcmp(cur, "Field")) return true; + if (!strcmp(cur, "Field") || !*cur) return false; + if (a.level) return !strcmp(a.level, cur); + for (int i : a.spots) + if (!strcmp(kWarpSpots[i].level, cur)) return true; + return false; +} +static bool matches(const Area &a, const Entry &e, const char *f) { + std::string fields[] = {e.name, e.sub, a.name, L_line(e.name), L_line(a.name)}; + for (auto &s : fields) { + std::string x = s, y = f; + std::transform(x.begin(), x.end(), x.begin(), ::tolower), std::transform(y.begin(), y.end(), y.begin(), ::tolower); + if (x.find(y) != std::string::npos) return true; + } + return false; +} +static bool pressed_right(void) { + return ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight) || ImGui::IsKeyPressed(ImGuiKey_GamepadLStickRight) || ImGui::IsKeyPressed(ImGuiKey_RightArrow); +} +static void row_focus(const std::string &key, bool hover_shows, const std::string &show, int area) { + if (g_focus_req == key) g_focus_req.clear(), opt_focus_item(); + bool foc = opt_item_nav_focused(); + if (foc) g_focus_now = key; + if (foc || (hover_shows && ImGui::IsItemHovered())) g_show = show, g_show_area = area; +} +// a section / area header row; true when it opened or closed +static bool header(const std::string &key, const char *label, const char *value, bool open, const std::string &show, int area) { + ImGui::PushID(key.c_str()); + if (open) opt_mark_next(); + opt_shown_next(); + bool hit = opt_link(label, NULL, value); + ImGui::PopID(); + row_focus(key, true, show, area); + if (!hit) return false; + if (open && pressed_right()) return false; // (Right opens; it never closes) + if (open) g_open.erase(key); + else g_open.insert(key); + g_focus_req = key; + return true; +} +static std::string entry_desc(const Entry &e) { + if (!entry_blocked(e)) return e.desc; + return strncmp(kWarpSpots[e.spot].locator, "Warp_", 5) ? L("You arrive here swimming, so you need the Flippers.") + : L("This is where Manbo's Mambo drops you. You can't warp here directly."); +} +static bool entry_row(const Area &a, int ai, const Entry &e, const std::string &key, bool with_area, bool enabled) { + std::string lbl = with_area ? std::string(L_line(a.name)) + " - " + L_line(e.name) : std::string(L_line(e.name)); + bool off = !enabled || entry_blocked(e); + std::string desc = entry_desc(e); + ImGui::PushID(key.c_str()); + if (off) opt_disable_next(); + opt_shown_next(); + bool go = opt_action(lbl.c_str(), "", desc.c_str(), false); + ImGui::PopID(); + row_focus(key, true, e.id, ai); + if (go && !off) { do_warp(e); return true; } return false; } -static int g_area = -1; // the area shown (its first spot's index) -static bool g_in_right; // the cursor is in the right column (last frame) -static int g_last_frame = -10; // (a frame without this tab: it starts over on Link's area) -static int g_back_frame = -10; // the frame B went back to the areas (the menu mustn't close on it) -enum { FOCUS_NONE, FOCUS_AREA, FOCUS_RIGHT }; -static int g_focus; -extern "C" bool warp_tab_took_back(void) { return g_back_frame == ImGui::GetFrameCount(); } +static void draw_map(void) { + // what to show: the entry (or area) under the cursor + Area *a = g_show_area >= 0 && g_show_area < (int)g_areas.size() ? &g_areas[g_show_area] : nullptr; + const Entry *e = nullptr; + if (a) + for (auto &x : a->entries) + if (x.id == g_show) e = &x; + float fs = ImGui::GetFontSize(); + ImGui::SetWindowFontScale(1.3f); + ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1), "%s", a ? (e ? L_line(e->name).c_str() : L_line(a->name).c_str()) : L("Warp")); + ImGui::SetWindowFontScale(1.0f); + if (a) ImGui::TextDisabled("%s", e ? (std::string(L_line(a->name)) + " ยท " + L_line(e->sub)).c_str() : L(kSecName[a->sec])); + ImVec2 avail = ImGui::GetContentRegionAvail(); + float h = std::max(fs * 6, avail.y - fs * 1.3f * 3.5f); + ImVec2 p0 = ImGui::GetCursorScreenPos(), p1(p0.x + avail.x, p0.y + h); + ImGui::Dummy(ImVec2(avail.x, h)); + ImDrawList *dl = ImGui::GetWindowDrawList(); + dl->AddRectFilled(p0, p1, IM_COL32(28, 25, 36, 255), 10); + ImVec2 a0(p0.x + 6, p0.y + 6), a1(p1.x - 6, p1.y - 6); + if (!a) return; + { // under the map: what it shows (the place, or the area's count) + char buf[64]; + snprintf(buf, sizeof buf, L("%d places"), (int)a->entries.size()); + std::string d = e ? entry_desc(*e) : a->built ? std::string(buf) : std::string(); + ImGui::SetWindowFontScale(1.3f); + ImGui::PushTextWrapPos(0); + ImGui::TextUnformatted(d.c_str()); + ImGui::PopTextWrapPos(); + ImGui::SetWindowFontScale(1.0f); + } + if (a->sec == SEC_DG && a->level && warp_level(a->level).ok) { + const WarpLevel &LV = warp_level(a->level); + std::vector dots; + for (auto &x : a->entries) { + int ri = x.room; + if (x.spot >= 0) { + int si = LV.find_spawn(kWarpSpots[x.spot].locator); + if (si >= 0) { + for (auto &s : LV.rooms[si].spawns) + if (s.locator == kWarpSpots[x.spot].locator) dots.push_back({si, s.x, s.z, e == &x}); + continue; + } + } + float lx, ly, lz; + if (ri >= 0 && warp_room_landing(LV, ri, &lx, &ly, &lz)) + dots.push_back({ri, (lx - LV.rooms[ri].col * 15.0f) / 1.5f, (lz - LV.rooms[ri].row * 12.0f) / 1.5f, e == &x}); + } + int sel = e ? e->room : -1; + if (sel >= 0) { + float mid = a0.y + (a1.y - a0.y) * 0.5f; + warp_draw_floor(dl, a0, ImVec2(a1.x, mid - 4), LV, sel, dots.data(), (int)dots.size()); + dl->AddLine(ImVec2(a0.x + 8, mid), ImVec2(a1.x - 8, mid), IM_COL32(80, 72, 98, 255), 1); + warp_draw_room(dl, ImVec2(a0.x, mid + 4), a1, LV, sel, dots.data(), (int)dots.size()); + } else { + warp_draw_floor(dl, a0, a1, LV, -1, dots.data(), (int)dots.size()); + } + return; + } + // the island + int cr = e ? e->sq_r : -1, cc = e ? e->sq_c : -1; + warp_draw_island(dl, a0, a1, g_island, cr, cc, a->sec == SEC_OW && cr < 0 ? a->region : -1); +} extern "C" bool warp_tab_draw(float scale) { + if (g_areas.empty()) build_places(); const char *cur = current_level(); bool avail = warp_available() != 0; int frame = ImGui::GetFrameCount(); - if (frame - g_last_frame > 1) { // (the tab just opened) start on the group and area of the level Link is in - g_focus = FOCUS_NONE, g_in_right = false; - int here = -1; - for (int i = 0; i < kNumSpots && here < 0; i++) - if (!strcmp(kWarpSpots[i].level, cur)) here = i; - if (g_group < 0 || here >= 0) g_group = here >= 0 ? kWarpSpots[here].group : 0; - if (here >= 0 && strcmp(cur, "Field")) { - Area a[128]; - for (int k = 0, n = areas_of(g_group, a, 128); k < n; k++) - if (area_here(a[k], cur)) g_area = a[k].first; - } + g_back_took = false; + if (frame - g_last_frame > 1) { // (the tab just opened) open the area Link is in + g_focus_prev.clear(); + for (int i = 0; i < (int)g_areas.size(); i++) + if (area_here(g_areas[i], cur)) { + g_open.insert("S" + std::to_string(g_areas[i].sec)), g_open.insert(area_key(g_areas[i])); + ensure_built(g_areas[i]); + if (g_show.empty() || g_show_area < 0) g_show_area = i, g_show = area_key(g_areas[i]); + } + if (g_open.empty()) g_open.insert("S0"); + if (g_show_area < 0) g_show_area = 0, g_show = area_key(g_areas[0]); } g_last_frame = frame; - Area areas[128]; - int na = areas_of(g_group, areas, 128); - bool area_ok = false; - for (int k = 0; k < na; k++) area_ok |= areas[k].first == g_area; - if (!area_ok) g_area = na ? areas[0].first : -1; - - if (!avail) { // (no header line otherwise: the lists start at the top) - ImGui::TextDisabled("%s", L("Load a save to warp.")); - ImGui::Spacing(); - } - bool started = false; - // test hook: LARECOMP_WARP_MENU_PICK= picks the first spot matching through this tab - static int test_state = getenv("LARECOMP_WARP_MENU_PICK") ? 1 : 0; - if (test_state == 1 && avail) { - for (int i = 0; i < kNumSpots; i++) - if (matches(kWarpSpots[i], getenv("LARECOMP_WARP_MENU_PICK"))) { - warp_request(kWarpSpots[i].level, kWarpSpots[i].locator); - snprintf(g_last, sizeof g_last, "%s - %s", kWarpSpots[i].area, kWarpSpots[i].label); - LOG("[warp] menu test pick: %s\n", g_last); - started = true; - break; - } - test_state = 2; - } - static int hook_frames = -1; // (test) LARECOMP_WARP_AREA=[:right]: that area shown (right: cursor on its spots) - if (hook_frames < 0) { - hook_frames = 0; - if (const char *h = getenv("LARECOMP_WARP_AREA")) - for (int i = 0; i < kNumSpots; i++) - if (!strncmp(h, kWarpSpots[i].area, strlen(kWarpSpots[i].area))) { - g_group = kWarpSpots[i].group, g_area = i, hook_frames = strstr(h, ":right") ? 10 : 0; - na = areas_of(g_group, areas, 128); + + // test hooks: LARECOMP_WARP_MENU_PICK= warps to the first place matching through this tab; + // LARECOMP_WARP_SHOW= opens the list there and shows it on the map + static int test_state = getenv("LARECOMP_WARP_MENU_PICK") || getenv("LARECOMP_WARP_SHOW") ? 1 : 0; + if (test_state == 1 && (avail || getenv("LARECOMP_WARP_SHOW"))) { + const char *want = getenv("LARECOMP_WARP_MENU_PICK") ? getenv("LARECOMP_WARP_MENU_PICK") : getenv("LARECOMP_WARP_SHOW"); + for (int i = 0; i < (int)g_areas.size() && test_state == 1; i++) { + ensure_built(g_areas[i]); + for (auto &e : g_areas[i].entries) + if (matches(g_areas[i], e, want)) { + g_open.insert("S" + std::to_string(g_areas[i].sec)), g_open.insert(area_key(g_areas[i])); + g_show = e.id, g_show_area = i, g_focus_req = "E" + e.id; + if (getenv("LARECOMP_WARP_MENU_PICK")) do_warp(e), started = true, LOG("[warp] menu test pick: %s\n", e.name.c_str()); + test_state = 2; break; } + } + test_state = 2; + } + + // Left: an open area or section closes; on a place, its area closes and takes the cursor + if (!g_focus_prev.empty() && opt_pressed_left() && !ui_text_busy() && !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId)) { + std::string k = g_focus_prev; + if (k[0] == 'E') { + for (auto &a : g_areas) + for (auto &e : a.entries) + if ("E" + e.id == k) { g_open.erase(area_key(a)), g_focus_req = area_key(a); } + } else if (k[0] == 'A') { + if (g_open.count(k)) g_open.erase(k); + else { std::string s = "S" + k.substr(1, 1); g_open.erase(s), g_focus_req = s; } + } else if (k[0] == 'S' && g_open.count(k)) { + g_open.erase(k); + } } - if (hook_frames > 0) hook_frames--, g_focus = FOCUS_RIGHT; - // B / Esc in the right column, or Left: back to the area - if (g_in_right && !ui_text_busy() && !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId) && - (opt_pressed_back() || (opt_pressed_left() && !opt_focused_lr()))) - g_back_frame = frame, g_focus = FOCUS_AREA; + g_focus_now.clear(); + if (!avail) { // (no header line otherwise: the list starts at the top) + ImGui::TextDisabled("%s", L("Load a save to warp.")); + ImGui::Spacing(); + } opt_begin("##warp", scale); opt_compact(); - for (int g = 0; g < 3; g++) - if (opt_radio(kWarpGroups[g], g_group == g, NULL)) { - g_group = g, g_filter[0] = 0; - na = areas_of(g_group, areas, 128); - g_area = na ? areas[0].first : -1; - for (int k = 0; k < na; k++) - if (area_here(areas[k], cur)) g_area = areas[k].first; - } opt_text("Search", g_filter, sizeof g_filter, "Search places...", NULL); - opt_section(kWarpGroups[g_group]); - for (int k = 0; k < na; k++) { - const Area &a = areas[k]; - char v[48]; - snprintf(v, sizeof v, area_here(a, cur) ? L("<- here %d") : "%d", a.end - a.first); - if (g_in_right && a.first == g_area) opt_mark_next(); - ImGui::PushID(a.first); - opt_shown_next(); // (the area is translated below: the count as it is) - bool open = opt_link(L(kWarpSpots[a.first].area), NULL, v); - ImGui::PopID(); - bool clicked = ImGui::IsItemClicked(); - if (a.first == g_area && g_focus == FOCUS_AREA) g_focus = FOCUS_NONE, opt_focus_item(); - if ((opt_item_nav_focused() && g_area != a.first) || open) g_area = a.first; // (moving down the list shows each) - if (open && !clicked) g_focus = FOCUS_RIGHT; // (A / Right: the cursor goes to its spots) + bool searching = g_filter[0] != 0; + for (int s = SEC_OW; s <= SEC_DG; s++) { + std::string sk = "S" + std::to_string(s); + std::vector areas; + for (int i = 0; i < (int)g_areas.size(); i++) + if (g_areas[i].sec == s) areas.push_back(i); + if (searching) { // (only areas with a match; all of them open) + std::vector hit; + for (int i : areas) { + ensure_built(g_areas[i]); + for (auto &e : g_areas[i].entries) + if (matches(g_areas[i], e, g_filter)) { hit.push_back(i); break; } + } + areas = hit; + if (areas.empty()) continue; + } + char v[16]; + snprintf(v, sizeof v, "%d", (int)areas.size()); + bool so = searching || g_open.count(sk); + if (!searching) header(sk, L(kSecName[s]), v, so, g_show, g_show_area); + else opt_section(kSecName[s]); + if (!searching && !g_open.count(sk)) continue; + opt_indent(1); + for (int ai : areas) { + Area &a = g_areas[ai]; + std::string ak = area_key(a); + bool ao = searching || g_open.count(ak); + if (ao) ensure_built(a); + char cnt[48]; + if (area_here(a, cur)) snprintf(cnt, sizeof cnt, "%s", L("Link is here")); + else if (a.built) snprintf(cnt, sizeof cnt, "%d", (int)a.entries.size()); + else cnt[0] = 0; + if (!searching) { + if (header(ak, L_line(a.name).c_str(), cnt, ao, ak, ai) && g_open.count(ak)) ensure_built(a); + if (!g_open.count(ak)) continue; + } else { + opt_section(L_line(a.name).c_str()); + } + opt_indent(1); + for (auto &e : a.entries) { + if (searching && !matches(a, e, g_filter)) continue; + started |= entry_row(a, ai, e, "E" + e.id, false, avail); + } + opt_indent(-1); + } + opt_indent(-1); } - opt_split(); - opt_section(g_filter[0] ? "Search" : g_area >= 0 ? kWarpSpots[g_area].area : ""); - opt_list_begin(); - if (g_focus == FOCUS_RIGHT) g_focus = FOCUS_NONE, opt_focus_next_row(); - if (g_filter[0]) { // flat list of matches across all groups - int shown = 0; - for (int i = 0; i < kNumSpots && shown < 300; i++) - if (matches(kWarpSpots[i], g_filter)) { started |= spot(kWarpSpots[i], true, avail); shown++; } - if (!shown) ImGui::TextDisabled("%s", L("No matches.")); - } else if (g_area >= 0) { - for (int k = g_area; k < kNumSpots && kWarpSpots[k].group == kWarpSpots[g_area].group && - !strcmp(kWarpSpots[k].area, kWarpSpots[g_area].area); k++) - started |= spot(kWarpSpots[k], false, avail); - } - opt_list_end(); - g_in_right = opt_in_right(); - opt_desc_text(); + draw_map(); opt_split_end(); + g_focus_prev = g_focus_now; return started; }