Files
ReSkate/Launcher/gui_home.cpp
T
AyeZeeBB b763631b77 Trim per-frame multiplayer client work; size mesh pool to the GPU
Mesh streaming pool (fixes loading crashes on older GPUs)
- 1.0.0 forced MeshStreaming.PoolSize to 3.5 GiB on every card. An RX 580
  crashed while loading with DXGI_ERROR_DEVICE_RESET, and the oversized pool
  is the likely cause. The pool is now a quarter of the largest hardware
  adapter's dedicated video memory, capped at 3.5 GiB; cards of 6 GB or less
  keep the game's own 520 MB. The card, its memory and the chosen size are
  logged once at startup.

Remote players (busy servers)
- Transform interpolate is inline plain arithmetic: 22 -> 5.8 ns per joint.
  It was 4% of a busy server's client frame, since every player within
  200 m has the whole skeleton interpolated every frame.
- Players out of the camera's view and more than 8 m away are sampled at
  20 Hz, like far players. Their position still updates every frame for
  collision, sound and nametags, and a player coming into view is sampled
  that frame.

Expression hooks (run for every expression the game executes)
- No profiler zones in the expression hooks: taking a sample switched them
  on, inflating the very samples that measured the hooks.
- The throwdown lab pump does one sorted lookup of the running graph and
  skips everything else for graphs it doesn't watch (graph_throttle gets
  throttled_graph for this).

Settings and memory reads
- Script gameplay-setting and native-setting gets are served from small
  per-thread caches, keyed on the asset/key address, its first 8 name
  bytes and a new profile::Store::changes() counter, instead of a store
  lookup each call.
- Per-frame reads in the client tick, native settings and population
  controls use memory::peek instead of ReadProcessMemory.
- Script error logging probes each argument pointer once.
2026-10-02 21:07:43 -05:00

431 lines
24 KiB
C++

#include "gui_internal.h"
#include "mod_manager.h"
#include <cmath>
#include <format>
// The main screen: background, name plate, tiles and the STATUS tile.
namespace dingosdk::launcher_gui::detail {
namespace {
void draw_background(ImDrawList* draw, ImVec2 size, float time) {
// Vertical gradient plus a warm glow bottom-right and a cool glow top-left,
// evaluated on a grid of bilinear quads. The quads are opaque: stacked
// circles band, and translucent layers under the modal dim left holes on
// an RX 9070 XT.
struct Glow { ImVec2 centre; float radius; ImVec4 colour; };
const Glow glows[]{
{ImVec2(size.x * 0.95f, size.y * 1.05f), S(700), ImVec4(1.0f, 0.5f, 0.08f, 0.42f)},
{ImVec2(size.x * 0.02f, size.y * -0.08f), S(560), ImVec4(0.23f, 0.38f, 0.9f, 0.22f)},
};
const auto top = ImGui::ColorConvertU32ToFloat4(color::background_top);
const auto bottom = ImGui::ColorConvertU32ToFloat4(color::background_bottom);
const auto field = [&](float x, float y) {
const float v = std::clamp(y / size.y, 0.0f, 1.0f);
ImVec4 out(top.x + (bottom.x - top.x) * v, top.y + (bottom.y - top.y) * v, top.z + (bottom.z - top.z) * v, 1);
for (const auto& glow : glows) {
const float t = std::min(std::hypot(x - glow.centre.x, y - glow.centre.y) / glow.radius, 1.0f);
const float a = glow.colour.w * (1 - t) * (1 - t);
out.x += (glow.colour.x - out.x) * a;
out.y += (glow.colour.y - out.y) * a;
out.z += (glow.colour.z - out.z) * a;
}
return ImGui::ColorConvertFloat4ToU32(out);
};
constexpr int columns = 32, rows = 18;
const float cell_x = size.x / columns, cell_y = size.y / rows;
for (int row = 0; row < rows; ++row)
for (int column = 0; column < columns; ++column) {
const float x0 = cell_x * static_cast<float>(column), y0 = cell_y * static_cast<float>(row);
const float x1 = x0 + cell_x, y1 = y0 + cell_y;
draw->AddRectFilledMultiColor(ImVec2(x0, y0), ImVec2(x1, y1),
field(x0, y0), field(x1, y0), field(x1, y1), field(x0, y1));
}
// Skatepark floor: a perspective grid converging on the wordmark.
const ImVec2 vanish(size.x * 0.5f, size.y * 0.46f);
const float floor = size.y * 0.62f;
for (int line = -14; line <= 14; ++line) {
const float x = size.x * 0.5f + static_cast<float>(line) * S(120);
draw->AddLine(ImVec2(vanish.x + (x - vanish.x) * 0.18f, floor), ImVec2(x, size.y), rgba(255, 255, 255, 0.035f), 1);
}
const float scroll = std::fmod(time * 0.06f, 1.0f);
for (int row = 0; row < 9; ++row) {
const float t = (static_cast<float>(row) + scroll) / 9.0f;
const float y = floor + (size.y - floor) * t * t;
draw->AddLine(ImVec2(0, y), ImVec2(size.x, y), rgba(255, 255, 255, 0.03f * t + 0.01f), 1);
}
// Drifting outline shapes, like chalk marks on a ramp.
for (int index = 0; index < 28; ++index) {
const float seed = static_cast<float>(index) * 12.9898f;
const float fx = std::fmod(std::abs(std::sin(seed) * 43758.5453f), 1.0f);
const float fy = std::fmod(std::abs(std::sin(seed * 1.7f) * 24634.6345f), 1.0f);
const float drift = time * (0.004f + 0.006f * fx);
const ImVec2 centre(size.x * std::fmod(fx + drift, 1.0f), size.y * fy + std::sin(time * 0.3f + seed) * S(6));
const float radius = S(3.0f + 16.0f * fy * fx);
const float alpha = 0.05f + 0.07f * fy;
if (index % 3 == 0) {
const float angle = time * 0.2f + seed;
ImVec2 corners[4];
for (int corner = 0; corner < 4; ++corner) {
const float a = angle + static_cast<float>(corner) * 1.5707963f;
corners[corner] = ImVec2(centre.x + std::cos(a) * radius, centre.y + std::sin(a) * radius);
}
draw->AddQuad(corners[0], corners[1], corners[2], corners[3], rgba(255, 255, 255, alpha), 1.2f);
} else {
draw->AddCircle(centre, radius, rgba(255, 255, 255, alpha), 0, 1.2f);
}
}
// Vignette edges.
draw->AddRectFilledMultiColor(ImVec2(0, size.y * 0.75f), size, 0, 0, rgba(0, 0, 0, 0.45f), rgba(0, 0, 0, 0.45f));
}
// A tile's icon centred at `centre`; false when it isn't loaded (use the drawn one).
bool tile_icon(ImDrawList* draw, const Background& icon, ImVec2 centre, float size, bool hovered) {
if (!icon.id) return false;
const float half = size * 0.5f;
draw->AddImage(icon.id, ImVec2(centre.x - half, centre.y - half), ImVec2(centre.x + half, centre.y + half),
ImVec2(0, 0), ImVec2(1, 1), rgba(255, 255, 255, hovered ? 0.6f : 0.32f));
return true;
}
void draw_photo(ImDrawList* draw, ImVec2 size, float time) {
// Cover-fit with a slow drift so the still image feels alive.
const float zoom = 1.04f + 0.02f * std::sin(time * 0.05f);
const float scale = std::max(size.x / g_background.width, size.y / g_background.height) * zoom;
const ImVec2 image(g_background.width * scale, g_background.height * scale);
const ImVec2 origin((size.x - image.x) * 0.5f + std::sin(time * 0.037f) * S(10),
(size.y - image.y) * 0.5f + std::cos(time * 0.029f) * S(6));
draw->AddImage(g_background.id, origin, ImVec2(origin.x + image.x, origin.y + image.y));
// Keep the wordmark and bottom row readable over a busy photo.
draw->AddRectFilled(ImVec2(0, 0), size, rgba(6, 8, 12, 0.38f));
draw->AddRectFilledMultiColor(ImVec2(0, size.y * 0.45f), size, rgba(6, 8, 12, 0), rgba(6, 8, 12, 0),
rgba(6, 8, 12, 0.92f), rgba(6, 8, 12, 0.92f));
draw->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(size.x, S(90)), rgba(6, 8, 12, 0.6f), rgba(6, 8, 12, 0.6f),
rgba(6, 8, 12, 0), rgba(6, 8, 12, 0));
}
// Drawing shared with the in-game menu (UI/skate_theme.h), at the launcher's scale.
using skate_theme::Icon;
using skate_theme::rotate_since;
void rough_rect(ImDrawList* draw, ImVec2 a, ImVec2 b, ImU32 colour, unsigned seed) {
skate_theme::rough_rect(draw, a, b, colour, seed, g_scale);
}
void scribble(ImDrawList* draw, ImVec2 a, ImVec2 b, unsigned seed) { skate_theme::scribble(draw, a, b, seed, g_scale); }
void progress_bar(ImDrawList* draw, ImVec2 a, ImVec2 b, float fill, float time) {
skate_theme::striped_bar(draw, a, b, fill, time, g_scale);
}
// Brushed page title, tilted like the HUB's.
void draw_title(ImDrawList* draw, const Fonts& fonts, ImVec2 position, const char* text) {
const int start = draw->VtxBuffer.Size;
const float size = fonts.title->FontSize;
const auto extent = fonts.title->CalcTextSizeA(size, FLT_MAX, 0, text);
draw->AddText(fonts.title, size, ImVec2(position.x + S(3), position.y + S(4)), rgba(0, 0, 0, 0.5f), text);
draw->AddText(fonts.title, size, position, color::text, text);
rotate_since(draw, start, -4.0f, ImVec2(position.x + extent.x * 0.5f, position.y + extent.y * 0.5f));
}
// Name plate in the top-left corner, like the HUB's player card.
void draw_plate(ImDrawList* draw, const Fonts& fonts, ImVec2 position, const std::string& title, const std::string& version) {
const float h = S(46);
const float text = std::max(fonts.bold->CalcTextSizeA(fonts.bold->FontSize, FLT_MAX, 0, title.c_str()).x,
fonts.body->CalcTextSizeA(fonts.body->FontSize, FLT_MAX, 0, version.c_str()).x);
const float width = std::max(S(300), h + S(28) + text);
draw->AddRectFilled(ImVec2(position.x + h, position.y + S(3)), ImVec2(position.x + width, position.y + h - S(3)),
rgba(44, 47, 49, 0.94f));
draw->AddRectFilled(position, ImVec2(position.x + h, position.y + h), color::avatar);
// A small deck on the avatar tile.
const ImVec2 c(position.x + h * 0.5f, position.y + h * 0.5f);
const int start = draw->VtxBuffer.Size;
draw->AddRectFilled(ImVec2(c.x - S(15), c.y - S(5.5f)), ImVec2(c.x + S(15), c.y + S(5.5f)), color::text, S(5.5f));
for (const float x : {-9.0f, 9.0f}) draw->AddCircleFilled(ImVec2(c.x + S(x), c.y), S(1.8f), color::avatar);
rotate_since(draw, start, -40.0f, c);
draw->AddCircleFilled(ImVec2(position.x + S(3), position.y + S(3)), S(5), color::good);
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(position.x + h + S(12), position.y + S(5)), color::text, title.c_str());
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(position.x + h + S(12), position.y + S(24)), color::muted, version.c_str());
}
bool hit(const char* id, ImVec2 position, ImVec2 size, bool enabled, bool& hovered) {
ImGui::SetCursorScreenPos(position);
ImGui::BeginDisabled(!enabled);
const bool pressed = ImGui::InvisibleButton(id, size);
hovered = enabled && ImGui::IsItemHovered();
ImGui::EndDisabled();
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
return pressed;
}
// The big action tile: blue like skate.'s selected tile, grey and scribbled when locked.
bool action_tile(ImDrawList* draw, const Fonts& fonts, ImVec2 position, ImVec2 size, const char* label,
const std::string& detail, bool enabled, bool secondary) {
bool hovered{};
const bool pressed = hit("##primary", position, size, enabled, hovered);
const ImVec2 end(position.x + size.x, position.y + size.y);
const bool blue = enabled && !secondary;
rough_rect(draw, position, end, blue ? color::blue : enabled ? color::tile : color::tile_grey, 7);
if (!enabled) scribble(draw, position, end, 7);
if (hovered) draw->AddRect(ImVec2(position.x - S(3), position.y - S(3)), ImVec2(end.x + S(3), end.y + S(3)), color::text, 0, 0, S(3));
const ImU32 ink = blue ? color::ink : enabled ? color::text : rgba(160, 162, 168);
draw->AddText(fonts.action, fonts.action->FontSize, ImVec2(position.x + S(18), position.y + S(12)), ink, label);
if (!detail.empty())
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(position.x + S(20), end.y - S(20) - fonts.bold->FontSize),
blue ? rgba(0, 0, 0, 0.72f) : color::muted, detail.c_str(), nullptr, size.x - S(40));
return pressed;
}
bool settings_tile(ImDrawList* draw, const Fonts& fonts, ImVec2 position, ImVec2 size, bool enabled) {
bool hovered{};
const bool pressed = hit("##settings", position, size, enabled, hovered);
const ImVec2 end(position.x + size.x, position.y + size.y);
rough_rect(draw, position, end, color::tile, 3);
if (hovered) draw->AddRect(ImVec2(position.x - S(3), position.y - S(3)), ImVec2(end.x + S(3), end.y + S(3)), color::text, 0, 0, S(3));
draw->AddText(fonts.tile, fonts.tile->FontSize, ImVec2(position.x + S(18), position.y + (size.y - fonts.tile->FontSize) * 0.5f),
color::text, "SETTINGS");
// Large faded wheel in the corner, like the icons on the HUB's tiles.
const ImVec2 centre(end.x - S(44), position.y + size.y * 0.5f);
if (tile_icon(draw, g_icon_settings, centre, S(58), hovered)) return pressed;
const ImU32 ink = rgba(255, 255, 255, hovered ? 0.55f : 0.3f);
for (int tooth = 0; tooth < 8; ++tooth) {
const float angle = static_cast<float>(tooth) * 0.7853982f;
draw->AddLine(ImVec2(centre.x + std::cos(angle) * S(14), centre.y + std::sin(angle) * S(14)),
ImVec2(centre.x + std::cos(angle) * S(22), centre.y + std::sin(angle) * S(22)), ink, S(6));
}
draw->AddCircle(centre, S(15), ink, 0, S(4));
draw->AddCircle(centre, S(6), ink, 0, S(3));
return pressed;
}
void window_buttons(ImDrawList* draw, HWND window, ImVec2 size) {
const ImVec2 button(S(46), S(34));
bool hovered{};
ImVec2 position(size.x - button.x * 2, 0);
if (hit("##minimise", position, button, true, hovered)) ShowWindow(window, SW_MINIMIZE);
if (hovered) draw->AddRectFilled(position, ImVec2(position.x + button.x, button.y), rgba(255, 255, 255, 0.08f));
const ImVec2 middle(position.x + button.x * 0.5f, button.y * 0.5f);
draw->AddLine(ImVec2(middle.x - S(5), middle.y), ImVec2(middle.x + S(5), middle.y), color::text, S(1));
position.x += button.x;
if (hit("##close", position, button, true, hovered)) PostMessageW(window, WM_CLOSE, 0, 0);
if (hovered) draw->AddRectFilled(position, ImVec2(position.x + button.x, button.y), rgba(232, 17, 35));
const ImVec2 cross(position.x + button.x * 0.5f, button.y * 0.5f);
draw->AddLine(ImVec2(cross.x - S(5), cross.y - S(5)), ImVec2(cross.x + S(5), cross.y + S(5)), color::text, S(1));
draw->AddLine(ImVec2(cross.x - S(5), cross.y + S(5)), ImVec2(cross.x + S(5), cross.y - S(5)), color::text, S(1));
}
bool mods_tile(ImDrawList* draw, const Fonts& fonts, ImVec2 position, ImVec2 size, bool enabled, const std::string& detail) {
bool hovered{};
const bool pressed = hit("##mods", position, size, enabled, hovered);
const ImVec2 end(position.x + size.x, position.y + size.y);
rough_rect(draw, position, end, color::tile, 4);
if (hovered) draw->AddRect(ImVec2(position.x - S(3), position.y - S(3)), ImVec2(end.x + S(3), end.y + S(3)), color::text, 0, 0, S(3));
const float text_y = position.y + (size.y - fonts.tile->FontSize) * 0.5f - (detail.empty() ? 0 : S(9));
draw->AddText(fonts.tile, fonts.tile->FontSize, ImVec2(position.x + S(18), text_y), color::text, "MODS");
if (!detail.empty())
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(position.x + S(20), text_y + fonts.tile->FontSize + S(2)),
color::muted, detail.c_str());
// The skate tool, faded like the wheel on SETTINGS.
const ImVec2 centre(end.x - S(44), position.y + size.y * 0.5f);
if (tile_icon(draw, g_icon_mods, centre, S(58), hovered)) return pressed;
const ImU32 ink = rgba(255, 255, 255, hovered ? 0.55f : 0.3f);
for (int layer = 0; layer < 3; ++layer) {
const float y = centre.y - S(12) + static_cast<float>(layer) * S(12);
const ImVec2 points[]{ImVec2(centre.x, y - S(7)), ImVec2(centre.x + S(20), y), ImVec2(centre.x, y + S(7)),
ImVec2(centre.x - S(20), y)};
draw->AddPolyline(points, 4, ink, ImDrawFlags_Closed, S(3));
}
return pressed;
}
// The STATUS tile, laid out like the HUB's BOUNTIES tile: a header and icon rows.
void status_tile(ImDrawList* draw, const Fonts& fonts, const State& state, ImVec2 position, float width, float time) {
struct Row { Icon icon; std::string text; std::string detail; };
std::vector<Row> rows;
Icon now = Icon::busy;
switch (state.phase) {
case Phase::ready: now = Icon::check; break;
case Phase::failed: now = Icon::fail; break;
case Phase::update_available:
case Phase::game_missing:
case Phase::game_outdated: now = Icon::warning; break;
default: break;
}
rows.push_back({now, state.status, state.detail});
if (state.config) {
const auto build = state.config->game.build_id.empty() ? state.config->game.manifest_id : state.config->game.build_id;
const bool game_ok = state.phase != Phase::game_missing && state.phase != Phase::game_outdated &&
!(state.phase == Phase::downloading);
rows.push_back({game_ok ? Icon::check : Icon::warning, "skate. build " + build, {}});
if (update::binary_updates_enabled())
rows.push_back({state.phase == Phase::update_available ? Icon::warning : Icon::check,
"ReSkate " + state.config->runtime.version, {}});
}
const float text_x = position.x + S(52), text_width = width - S(72);
float height = S(74);
for (const auto& row : rows) {
height += fonts.bold->CalcTextSizeA(fonts.bold->FontSize, FLT_MAX, text_width, row.text.c_str()).y + S(12);
if (!row.detail.empty())
height += fonts.body->CalcTextSizeA(fonts.body->FontSize, FLT_MAX, text_width, row.detail.c_str()).y + S(4);
}
if (state.progress >= 0) height += S(28);
const ImVec2 end(position.x + width, position.y + height);
rough_rect(draw, position, end, color::tile, 5);
draw->AddText(fonts.tile, fonts.tile->FontSize, ImVec2(position.x + S(18), position.y + S(16)), color::text, "STATUS");
float y = position.y + S(66);
for (const auto& row : rows) {
skate_theme::status_icon(draw, ImVec2(position.x + S(30), y + fonts.bold->FontSize * 0.5f), row.icon, time, g_scale);
const auto text = fonts.bold->CalcTextSizeA(fonts.bold->FontSize, FLT_MAX, text_width, row.text.c_str());
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(text_x, y),
row.icon == Icon::fail ? color::danger : color::text, row.text.c_str(), nullptr, text_width);
y += text.y + S(4);
if (!row.detail.empty()) {
const auto detail = fonts.body->CalcTextSizeA(fonts.body->FontSize, FLT_MAX, text_width, row.detail.c_str());
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, y), color::muted, row.detail.c_str(), nullptr, text_width);
y += detail.y + S(4);
}
y += S(8);
}
if (state.progress >= 0)
progress_bar(draw, ImVec2(position.x + S(20), y + S(4)), ImVec2(end.x - S(20), y + S(16)), state.progress, time);
}
} // namespace
void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPanel& mods_panel) {
const auto& io = ImGui::GetIO();
const ImVec2 size = io.DisplaySize;
const float time = static_cast<float>(ImGui::GetTime());
const auto state = launcher.snapshot();
const bool qr_open = !state.qr.empty();
// The Thunderstore listing loads in the background from the start, so the
// MODS tile can say when installed mods have updates.
collect_listing(mods_panel);
refresh_listing(mods_panel, time);
// A mod dropped on the window opens the Mods panel and installs it.
{
std::lock_guard lock(g_dropped_mutex);
if (!g_dropped.empty() && !ui.settings && !ui.sign_in && !qr_open && !state.prompt && !mods_panel.installing) {
ui.mods = true;
mods_panel.tab = 0;
if (!mods_panel.scanned) scan(mods_panel, launcher.session());
start_install(mods_panel, g_dropped.front(), false);
}
g_dropped.clear();
}
// The Steam name for the plate, and the one-time offline notice, both come
// from the first Steam check; steam_offline_seen keeps it to one decision.
if (time - ui.steam_checked > 5) {
ui.steam_name = launcher_app::steam_persona_name();
ui.steam_checked = time;
if (!ui.steam_offline_seen) {
ui.steam_offline_seen = true;
ui.steam_offline = !launcher.settings().offline && !launcher_app::steam_signed_in();
}
}
const bool modal = ui.settings || ui.mods || ui.sign_in || qr_open || state.prompt.has_value() ||
ui.steam_offline;
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(size);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::Begin("##root", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBackground |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(2);
auto* draw = ImGui::GetWindowDrawList();
if (g_background.id) draw_photo(draw, size, time);
else draw_background(draw, size, time);
std::string version = "Development build";
if (update::binary_updates_enabled() && state.config && !state.config->launcher.version.empty())
version = "Launcher " + state.config->launcher.version;
const bool greet = !launcher.settings().offline && !ui.steam_name.empty();
draw_plate(draw, fonts, ImVec2(S(50), S(40)), greet ? "Welcome, " + ui.steam_name : std::string("ReSkate"), version);
draw_title(draw, fonts, ImVec2(S(52), S(88)), "RESKATE");
window_buttons(draw, window, size);
ImGui::BeginDisabled(modal);
const float column = S(340);
const ImVec2 primary_size(column, S(150));
const ImVec2 primary(size.x - S(60) - column, size.y - S(50) - primary_size.y);
const ImVec2 settings(primary.x, primary.y - S(14) - S(84));
const ImVec2 mods_position(primary.x, settings.y - S(14) - S(84));
const char* label = "PLAY";
std::string detail;
bool enabled = !launcher.busy();
bool secondary = false;
const auto build = state.config && !state.config->game.build_id.empty() ? state.config->game.build_id : std::string();
switch (state.phase) {
case Phase::checking: label = "CHECKING"; enabled = false; break;
case Phase::update_available: label = "UPDATE"; detail = "New ReSkate files are ready"; break;
case Phase::updating: label = "UPDATING"; enabled = false; break;
case Phase::game_missing: label = "INSTALL"; detail = "Download skate. from Steam"; break;
case Phase::game_outdated: label = "DOWNLOAD"; detail = "Get skate. build " + build; break;
case Phase::downloading:
label = "CANCEL"; enabled = true; secondary = true;
detail = state.progress >= 0 ? std::format("Downloading {:.0f}%", state.progress * 100) : "Downloading";
break;
case Phase::ready:
label = "PLAY";
// Release builds install the configured runtime before PLAY appears.
detail = !state.config ? std::string("Offline")
: !update::binary_updates_enabled() ? std::string("ReSkate development build")
: state.config->runtime.version.empty() ? std::string("ReSkate")
: "ReSkate " + state.config->runtime.version;
break;
case Phase::launching: label = "LAUNCHING"; enabled = false; break;
case Phase::failed: label = "RETRY"; break;
}
const auto act = [&] {
switch (state.phase) {
case Phase::update_available: launcher.apply_updates(); break;
case Phase::game_missing:
case Phase::game_outdated: open_sign_in(launcher, ui, false); break;
case Phase::downloading: launcher.cancel(); break;
case Phase::ready: launcher.play(); break;
case Phase::failed: launcher.check(); break;
default: break;
}
};
if (action_tile(draw, fonts, primary, primary_size, label, detail, enabled && !modal, secondary)) act();
if (settings_tile(draw, fonts, settings, ImVec2(column, S(84)), !modal)) ui.settings = true;
if (time - ui.mods_checked > 5 && !ui.mods) {
std::size_t enabled_mods = 0, installed = 0;
const auto list = mods::scan_mods(launcher_mods::mods_root(launcher.session().paths.directory).parent_path());
for (const auto& entry : list.entries) {
++installed;
if (entry.enabled) ++enabled_mods;
}
const auto pending = updates(mods_panel.store, installed_versions(list)).size();
ui.mods_detail = pending ? (pending == 1 ? std::string("1 update available") : std::format("{} updates available", pending))
: installed ? std::format("{} of {} enabled", enabled_mods, installed)
: std::string("Install and browse mods");
ui.mods_checked = time;
}
if (mods_tile(draw, fonts, mods_position, ImVec2(column, S(84)), !modal, ui.mods_detail)) {
ui.mods = true;
mods_panel.scanned = false;
}
if (enabled && !secondary && !modal &&
(ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false))) act();
ImGui::EndDisabled();
status_tile(draw, fonts, state, ImVec2(S(60), S(200)), S(360), time);
if (modal) draw->AddRectFilled(ImVec2(0, 0), size, rgba(4, 6, 9, 0.72f));
g_drag_allowed = !modal && !ImGui::IsAnyItemHovered();
ImGui::End();
if (state.prompt) prompt_window(launcher, fonts, size, *state.prompt, ui);
else if (qr_open) qr_window(launcher, fonts, size, state.qr);
else if (ui.sign_in) sign_in_window(launcher, fonts, size, ui);
else if (ui.settings) settings_window(launcher, fonts, size, ui);
else if (ui.mods) mods_window(launcher, fonts, size, ui, mods_panel, window);
else if (ui.steam_offline) steam_offline_window(fonts, size, ui);
}
} // namespace dingosdk::launcher_gui::detail