mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 03:34:33 +02:00
A dedicated server that rotates its maps can now ask players where to go next instead of taking the pool's next map. Server - maps.rotation_vote: when the timer runs out, a vote between three maps drawn at random from the pool, "Random map" and "Stay here". The most votes wins, a tie is drawn, and with no votes it is the pool's next map. It ends early once everyone has voted. - maps.rotation_vote_seconds (10-120, default 30) and maps.rotation_extend_minutes (0-240, default 15; 0 leaves "Stay here" out). - maps.rotation_announce: a card when the map has 10, 5 and 1 minutes left. - The vote's maps are drawn a minute ahead and sent to games (PacketKind::next_maps), so their pictures are ready when it opens. - /vote extend: a built-in vote that gives the map in play rotation_extend_minutes more. On by default (votes.extend_map), and only runs while the maps are on a timer. "extend" is now a reserved vote name. - Console: rotation vote on|off, rotation announce on|off, rotation stay <minutes>|off, rotation extend [minutes], votes extend ..., vote extend. Game - The vote opens as a popup in the middle of the screen: a tile per answer with its picture, name, share of the votes and count. It holds input as the map download card does, and works with a controller (D-pad, A, B), the keyboard and the mouse. - Pictures: a mod map's icon from its folder, or from Thunderstore when it is not installed; the game's own maps show their loading screen picture, cut to its middle square. "Random map", "Stay here" and a map without a picture are drawn as shapes. - Older servers and polls are unchanged: the vote is a poll, so it can still be answered with /1, /2... in chat. Protocol 50: games and servers both need this build.
571 lines
34 KiB
C++
571 lines
34 KiB
C++
#include "overlay_internal.h"
|
|
#include "Engine/Game/Input/controller_bindings.h"
|
|
#include <format>
|
|
#include <mutex>
|
|
#include <wincodec.h>
|
|
|
|
// The card for a map a multiplayer session needs and this PC does not have
|
|
// (Extension/Assets/map_download.h), laid out as the mod's page is: its icon, name, author,
|
|
// version and description, then what is being asked or done. It asks whether to download it,
|
|
// in the middle of the screen with the controls under it as the game's own menus have them.
|
|
// Once the player has said yes it becomes a small card at the right edge, with the download,
|
|
// the install and the apply, so they can skate on while they wait; it goes when the map is in
|
|
// the game and starts loading. Both in the look of ReSkate's pages in the pause menu (hub_page.cpp).
|
|
//
|
|
// Answered by a controller (D-pad to pick, A to take the pick, B for no), the keyboard (arrow
|
|
// keys, Enter, Esc), the vote binds, or a click: the overlay has the pointer while
|
|
// the card asks (State::prompt_pointer). The runtime reads those and holds the game's input meanwhile.
|
|
//
|
|
// The icon is the package's own, a PNG the downloader hands over (set_map_download_icon). It
|
|
// is decoded here and goes to the GPU from the render thread (overlay_render.cpp), into the
|
|
// overlay's second texture slot; the first is ImGui's atlas, which is filled once at startup.
|
|
|
|
namespace dingosdk::overlay {
|
|
namespace {
|
|
using Microsoft::WRL::ComPtr;
|
|
std::atomic<MapDownloadFeed> feed{};
|
|
std::atomic<MapDownloadAnswer> answer{};
|
|
|
|
std::mutex image_mutex;
|
|
detail::CardPixels current; // the icon in use, kept to upload again after a device reset
|
|
bool waiting{}; // `current` has not gone to the GPU yet
|
|
unsigned image_serial{};
|
|
std::atomic<ImTextureID> image{}; // on the GPU and drawable; 0 for none
|
|
// The texture is the next-map vote's pictures (set_map_vote_icons), not the card's icon: the
|
|
// one slot is whichever was set last. Which square is whose, under image_mutex.
|
|
std::atomic<bool> image_is_atlas{};
|
|
std::vector<std::string> vote_icon_names;
|
|
std::vector<bool> vote_icon_has;
|
|
ImTextureID card_icon() { return image_is_atlas.load() ? ImTextureID{} : image.load(); }
|
|
|
|
// Straight-alpha RGBA, at most 512 pixels a side (an icon is 256). With `side`, scaled to a
|
|
// square of that many pixels (the vote's pictures share one texture).
|
|
bool decode(const std::string& bytes, detail::CardPixels& out, UINT side = 0) {
|
|
const HRESULT com = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
bool ok{};
|
|
{
|
|
ComPtr<IWICImagingFactory> factory;
|
|
ComPtr<IWICStream> stream;
|
|
ComPtr<IWICBitmapDecoder> decoder;
|
|
ComPtr<IWICBitmapFrameDecode> frame;
|
|
ComPtr<IWICFormatConverter> converter;
|
|
ComPtr<IWICBitmapScaler> scaler;
|
|
UINT width{}, height{};
|
|
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory))) &&
|
|
SUCCEEDED(factory->CreateStream(&stream)) &&
|
|
SUCCEEDED(stream->InitializeFromMemory(reinterpret_cast<BYTE*>(const_cast<char*>(bytes.data())), static_cast<DWORD>(bytes.size()))) &&
|
|
SUCCEEDED(factory->CreateDecoderFromStream(stream.Get(), nullptr, WICDecodeMetadataCacheOnDemand, &decoder)) &&
|
|
SUCCEEDED(decoder->GetFrame(0, &frame)) && SUCCEEDED(frame->GetSize(&width, &height)) && width && height &&
|
|
width <= (side ? 2048U : 512U) && height <= (side ? 2048U : 512U) && SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
|
|
(!side || (SUCCEEDED(factory->CreateBitmapScaler(&scaler)) &&
|
|
SUCCEEDED(scaler->Initialize(frame.Get(), side, side, WICBitmapInterpolationModeFant)))) &&
|
|
SUCCEEDED(converter->Initialize(side ? static_cast<IWICBitmapSource*>(scaler.Get()) : static_cast<IWICBitmapSource*>(frame.Get()),
|
|
GUID_WICPixelFormat32bppRGBA, WICBitmapDitherTypeNone, nullptr, 0,
|
|
WICBitmapPaletteTypeCustom))) {
|
|
if (side) width = height = side;
|
|
out.rgba.resize(static_cast<std::size_t>(width) * height * 4);
|
|
out.width = width;
|
|
out.height = height;
|
|
ok = SUCCEEDED(converter->CopyPixels(nullptr, width * 4, static_cast<UINT>(out.rgba.size()), out.rgba.data()));
|
|
}
|
|
}
|
|
if (SUCCEEDED(com)) CoUninitialize();
|
|
return ok;
|
|
}
|
|
}
|
|
void set_map_download_feed(MapDownloadFeed value) noexcept { feed.store(value); }
|
|
void set_map_download_answer(MapDownloadAnswer value) noexcept { answer.store(value); }
|
|
|
|
void set_map_download_icon(const std::string& png) noexcept {
|
|
try {
|
|
detail::CardPixels pixels;
|
|
const bool decoded = !png.empty() && png.size() <= 4 * 1024 * 1024 && decode(png, pixels);
|
|
std::lock_guard lock(image_mutex);
|
|
image.store(0);
|
|
image_is_atlas.store(false);
|
|
pixels.serial = ++image_serial;
|
|
current = decoded ? std::move(pixels) : detail::CardPixels{};
|
|
current.serial = image_serial;
|
|
waiting = decoded;
|
|
} catch (...) {}
|
|
}
|
|
} // namespace dingosdk::overlay
|
|
|
|
namespace dingosdk::overlay::detail {
|
|
bool take_card_pixels(CardPixels& out) {
|
|
std::lock_guard lock(image_mutex);
|
|
if (!waiting) return false;
|
|
waiting = false;
|
|
out = current; // (kept: a device reset uploads it again)
|
|
return true;
|
|
}
|
|
void card_image_uploaded(ImTextureID texture, unsigned serial) {
|
|
std::lock_guard lock(image_mutex);
|
|
if (serial == image_serial) image.store(texture);
|
|
}
|
|
void card_image_lost() {
|
|
std::lock_guard lock(image_mutex);
|
|
image.store(0);
|
|
waiting = !current.rgba.empty();
|
|
}
|
|
|
|
namespace {
|
|
MapDownloadCard card; // this frame's, read by map_download_pending
|
|
|
|
ImU32 faded(ImU32 colour, float alpha) {
|
|
const auto base = static_cast<float>((colour >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f;
|
|
return (colour & ~IM_COL32_A_MASK) | (static_cast<ImU32>(base * std::clamp(alpha, 0.0f, 1.0f) * 255.0f + 0.5f) << IM_COL32_A_SHIFT);
|
|
}
|
|
std::string megabytes(std::uint64_t bytes) {
|
|
const auto mb = static_cast<double>(bytes) / (1024.0 * 1024.0);
|
|
return mb >= 1024.0 ? std::format("{:.2f} GB", mb / 1024.0) : std::format("{:.0f} MB", mb);
|
|
}
|
|
}
|
|
|
|
bool map_download_pending() {
|
|
const auto read = feed.load();
|
|
card = read ? read() : MapDownloadCard{};
|
|
// (Nothing is shown once the map is in the game: its loading has the game's own screen.)
|
|
if (card.stage == 5) card.stage = 0;
|
|
state().prompt_pointer.store(card.stage == 1 || map_vote_asking());
|
|
return card.stage != 0;
|
|
}
|
|
|
|
namespace {
|
|
// After the player said yes: a small card at the right edge, out of the way of skating, with
|
|
// what is being fetched and how far it is.
|
|
void draw_progress(State& s) {
|
|
auto* bold = s.menu.bold ? s.menu.bold : ImGui::GetFont();
|
|
auto* body = s.menu.body ? s.menu.body : ImGui::GetFont();
|
|
const auto display = ImGui::GetIO().DisplaySize;
|
|
const float u = std::min(display.x / 3840.0f, display.y / 2160.0f);
|
|
const bool downloading = card.stage == 2, applying = card.stage == 4;
|
|
const char* title = downloading ? "DOWNLOADING MAP" : card.stage == 3 ? "INSTALLING MAP" : "APPLYING MAP";
|
|
const std::string status = downloading ? (card.total ? megabytes(card.received) + " of " + megabytes(card.total) : megabytes(card.received))
|
|
: card.stage == 3 ? std::string("Putting it in your Mods folder...")
|
|
: card.step_name.empty() ? std::string("Adding it to the game...") : card.step_name + "...";
|
|
const float width = 820 * u, inset = 30 * u, icon = 132 * u, between = 26 * u, bar = 12 * u;
|
|
const float label_size = 24 * u, name_size = 38 * u, text_size = 26 * u;
|
|
const float column = width - inset * 2 - icon - between;
|
|
const float height = inset + std::max(icon, label_size + 12 * u + name_size + 18 * u + bar + 14 * u + text_size) + inset;
|
|
// Beside where a vote's card goes (the middle of the right edge): above it.
|
|
const ImVec2 min(std::floor(display.x - 48 * u - width), std::floor(display.y * 0.2f)), max(min.x + width, min.y + height);
|
|
auto* draw = ImGui::GetForegroundDrawList();
|
|
skate_theme::rough_rect(draw, min, max, IM_COL32(26, 26, 26, 235), 0x4d2, u * 2);
|
|
const float left = min.x + inset, top = min.y + inset;
|
|
const ImVec2 icon_min(left, top), icon_max(left + icon, top + icon);
|
|
if (const auto texture = card_icon()) draw->AddImage(texture, icon_min, icon_max);
|
|
else skate_theme::rough_rect(draw, icon_min, icon_max, skate_theme::tile_light, 0x1c1, u * 2);
|
|
const float x = left + icon + between, right = max.x - inset;
|
|
float y = top;
|
|
draw->AddText(bold, label_size, ImVec2(x, y), theme::blue, title);
|
|
y += label_size + 12 * u;
|
|
draw->PushClipRect(ImVec2(x, y - name_size), ImVec2(right, y + name_size * 2), true);
|
|
draw->AddText(bold, name_size, ImVec2(x, y), theme::paper, card.map.c_str());
|
|
draw->PopClipRect();
|
|
y += name_size + 18 * u;
|
|
const float share = downloading ? (card.total ? static_cast<float>(card.received) / static_cast<float>(card.total) : -1.0f)
|
|
: (applying && card.steps ? static_cast<float>(card.step) / static_cast<float>(card.steps) : -1.0f);
|
|
draw->AddRectFilled(ImVec2(x, y), ImVec2(right, y + bar), skate_theme::tile_light, 4 * u);
|
|
if (share >= 0.0f) draw->AddRectFilled(ImVec2(x, y), ImVec2(x + column * std::clamp(share, 0.0f, 1.0f), y + bar), theme::blue, 4 * u);
|
|
else {
|
|
// How far is not known: a block going back and forth.
|
|
const float at = static_cast<float>(std::fmod(ImGui::GetTime(), 2.0));
|
|
const float where = (at < 1.0f ? at : 2.0f - at) * (column * 0.75f);
|
|
draw->AddRectFilled(ImVec2(x + where, y), ImVec2(x + where + column * 0.25f, y + bar), theme::blue, 4 * u);
|
|
}
|
|
y += bar + 14 * u;
|
|
draw->PushClipRect(ImVec2(x, y - text_size), ImVec2(right, y + text_size * 2), true);
|
|
draw->AddText(body, text_size, ImVec2(x, y), theme::muted, status.c_str());
|
|
draw->PopClipRect();
|
|
}
|
|
}
|
|
|
|
void draw_map_download() {
|
|
if (!card.stage) return;
|
|
auto& s = state();
|
|
if (card.stage != 1) return draw_progress(s);
|
|
// Drawn as the pages of the game's pause menu are (hub_page.cpp): the same unit (one of
|
|
// the game's 3840 by 2160), brushed heading, hand-cut tiles, the yellow note for what to
|
|
// press and the keycaps under it.
|
|
auto* bold = s.menu.bold ? s.menu.bold : ImGui::GetFont();
|
|
auto* body = s.menu.body ? s.menu.body : ImGui::GetFont();
|
|
auto* heading = s.menu.heading ? s.menu.heading : bold;
|
|
const auto display = ImGui::GetIO().DisplaySize;
|
|
const float u = std::min(display.x / 3840.0f, display.y / 2160.0f);
|
|
constexpr ImU32 sticky = IM_COL32(236, 215, 108, 255), sticky_dim = IM_COL32(150, 140, 86, 255);
|
|
const bool asking = card.stage == 1, downloading = card.stage == 2, applying = card.stage == 4;
|
|
const bool clickable = asking || s.visible.load() || s.console_visible.load();
|
|
const char* host = card.server ? "server" : "host";
|
|
const auto width_of = [](ImFont* font, float size, const std::string& text) { return font->CalcTextSizeA(size, FLT_MAX, 0, text.c_str()).x; };
|
|
|
|
const char* title = asking ? (!card.have.empty() ? "MAP VERSION" : card.moved ? "MAP CHANGED" : "MAP NEEDED") : downloading ? "DOWNLOADING MAP" : card.stage == 3 ? "INSTALLING MAP"
|
|
: applying ? "APPLYING MAP" : "JOINING";
|
|
// Why it is asked: the session moved here with the player in it, or the player is joining.
|
|
const auto why = card.moved ? std::format("The {} changed to this map, and you need it to stay in the session.", host)
|
|
: std::format("The {} is on this map, and you need it to join.", host);
|
|
const std::string status = asking ? (!card.have.empty() ? std::format("The {} runs version {} of this map and you have {}. Update to its version?", host, card.version, card.have)
|
|
: why + (card.installed ? " You have it, switched off. Switch it on?" : " Download it?"))
|
|
: downloading ? (card.total ? megabytes(card.received) + " of " + megabytes(card.total) : megabytes(card.received))
|
|
: card.stage == 3 ? std::string("Putting it in your Mods folder...")
|
|
: applying ? (card.step_name.empty() ? std::string("Adding it to the game...") : card.step_name + "...")
|
|
: std::string("Loading the map...");
|
|
struct Fact { const char* name; std::string value; };
|
|
std::vector<Fact> facts{{"Author", card.author}};
|
|
if (!card.version.empty()) facts.push_back({"Version", card.version});
|
|
facts.push_back({"From", card.installed ? std::string("Your Mods folder, switched off") : std::string("Thunderstore")});
|
|
|
|
// The controls under the card: the pad's own names for its buttons when one is connected.
|
|
dingosdk::ControllerInput pad;
|
|
DingoSDKOverlayReadControllerInput(&pad, true);
|
|
struct Hint { std::string key; const char* what; };
|
|
std::vector<Hint> hints;
|
|
if (asking) {
|
|
hints.push_back({pad.available ? std::string("D-pad") : std::string("< >"), "Pick"});
|
|
hints.push_back({pad.available ? dingosdk::controller_combo_label(0x1000, pad.style) : std::string("Enter"), "Select"});
|
|
hints.push_back({pad.available ? dingosdk::controller_combo_label(0x2000, pad.style) : std::string("Esc"), "Cancel"});
|
|
}
|
|
const float key_scale = u * 2, hint_gap = 36 * u;
|
|
float hints_width{};
|
|
for (const auto& hint : hints)
|
|
hints_width += width_of(bold, 12 * key_scale, hint.key) + 20 * key_scale + width_of(bold, 14 * key_scale, hint.what);
|
|
if (!hints.empty()) hints_width += hint_gap * static_cast<float>(hints.size() - 1);
|
|
|
|
const float width = 1500 * u, inset = 44 * u, icon = 300 * u, between = 44 * u, tall = 110 * u, bar = 14 * u;
|
|
const float inner = width - inset * 2, column = inner - icon - between;
|
|
const float label_size = 28 * u, name_size = 64 * u, fact_size = 34 * u, text_size = 32 * u;
|
|
const auto name_extent = heading->CalcTextSizeA(name_size, FLT_MAX, column, card.map.c_str());
|
|
const auto described = card.description.empty() ? ImVec2{} : body->CalcTextSizeA(text_size, FLT_MAX, inner, card.description.c_str());
|
|
const float beside = name_extent.y + 22 * u + static_cast<float>(facts.size()) * 54 * u;
|
|
const float top_block = std::max(icon, beside);
|
|
const auto status_extent = body->CalcTextSizeA(text_size, FLT_MAX, inner, status.c_str());
|
|
const bool progress = downloading || applying;
|
|
float height = inset + label_size + 26 * u + top_block + inset + status_extent.y + inset;
|
|
if (!card.description.empty()) height += described.y + 36 * u;
|
|
if (progress) height += bar + 30 * u;
|
|
if (asking) height += 30 * u + tall;
|
|
if (downloading) height += 16 * u + label_size;
|
|
const float under = hints.empty() ? 0.0f : 34 * u + 20 * key_scale;
|
|
// In the middle while it asks; near the top, out of the way, while the player skates on.
|
|
const float top = asking ? (display.y - height - under) * 0.5f : 150 * u;
|
|
const ImVec2 min(std::floor((display.x - width) * 0.5f), std::floor(top)), max(min.x + width, min.y + height);
|
|
auto* draw = ImGui::GetForegroundDrawList();
|
|
// While it asks, what is behind is dimmed: the pause menu takes no input then, nor the game.
|
|
if (asking) draw->AddRectFilled(ImVec2(0, 0), display, IM_COL32(0, 0, 0, s.hub_pointer ? 170 : 110));
|
|
skate_theme::rough_rect(draw, min, max, IM_COL32(26, 26, 26, 250), 0x4d1, u * 2);
|
|
const float left = min.x + inset, right = max.x - inset;
|
|
float y = min.y + inset;
|
|
draw->AddText(bold, label_size, ImVec2(left, y), theme::blue, title);
|
|
y += label_size + 26 * u;
|
|
|
|
// The icon, or a plain tile until it is here (and for a mod that has none).
|
|
const ImVec2 icon_min(left, y), icon_max(left + icon, y + icon);
|
|
if (const auto texture = card_icon()) draw->AddImage(texture, icon_min, icon_max);
|
|
else {
|
|
skate_theme::rough_rect(draw, icon_min, icon_max, skate_theme::tile_light, 0x1c0, u * 2);
|
|
const std::string mark = "MAP";
|
|
draw->AddText(heading, 56 * u, ImVec2(left + (icon - width_of(heading, 56 * u, mark)) * 0.5f, y + (icon - 56 * u) * 0.5f), theme::muted, mark.c_str());
|
|
}
|
|
// Beside it, the map's name and what is known of the mod, as a session's facts are listed.
|
|
const float text_left = left + icon + between;
|
|
float text_y = y;
|
|
draw->AddText(heading, name_size, ImVec2(text_left, text_y), theme::paper, card.map.c_str(), nullptr, column);
|
|
text_y += name_extent.y + 22 * u;
|
|
for (const auto& fact : facts) {
|
|
draw->AddText(body, fact_size, ImVec2(text_left, text_y), theme::muted, fact.name);
|
|
draw->PushClipRect(ImVec2(text_left + 220 * u, text_y - fact_size), ImVec2(right, text_y + fact_size * 2), true);
|
|
draw->AddText(bold, fact_size, ImVec2(std::max(text_left + 220 * u, right - width_of(bold, fact_size, fact.value)), text_y), theme::paper, fact.value.c_str());
|
|
draw->PopClipRect();
|
|
text_y += 54 * u;
|
|
}
|
|
y += top_block + inset;
|
|
if (!card.description.empty()) {
|
|
draw->AddText(body, text_size, ImVec2(left, y), theme::muted, card.description.c_str(), nullptr, inner);
|
|
y += described.y + 36 * u;
|
|
}
|
|
if (progress) {
|
|
const float share = downloading ? (card.total ? static_cast<float>(card.received) / static_cast<float>(card.total) : -1.0f)
|
|
: (card.steps ? static_cast<float>(card.step) / static_cast<float>(card.steps) : -1.0f);
|
|
draw->AddRectFilled(ImVec2(left, y), ImVec2(right, y + bar), skate_theme::tile_light, 4 * u);
|
|
if (share >= 0.0f) draw->AddRectFilled(ImVec2(left, y), ImVec2(left + inner * std::clamp(share, 0.0f, 1.0f), y + bar), theme::blue, 4 * u);
|
|
else {
|
|
// How far is not known: a block going back and forth.
|
|
const float at = static_cast<float>(std::fmod(ImGui::GetTime(), 2.0));
|
|
const float where = (at < 1.0f ? at : 2.0f - at) * (inner * 0.75f);
|
|
draw->AddRectFilled(ImVec2(left + where, y), ImVec2(left + where + inner * 0.25f, y + bar), theme::blue, 4 * u);
|
|
}
|
|
y += bar + 30 * u;
|
|
}
|
|
draw->AddText(body, text_size, ImVec2(left, y), asking ? theme::paper : theme::muted, status.c_str(), nullptr, inner);
|
|
y += status_extent.y;
|
|
if (downloading) {
|
|
y += 16 * u;
|
|
draw->AddText(body, label_size, ImVec2(left, y), theme::muted, "You can keep skating while it downloads.");
|
|
return;
|
|
}
|
|
if (!asking) return;
|
|
y += 30 * u;
|
|
// The game's two looks for a button: the yellow note for the thing to do, a tile for the
|
|
// way out. The picked one is the lit one: the note full yellow in a blue edge, the tile blue.
|
|
const float button_width = (inner - 24 * u) * 0.5f;
|
|
const auto button = [&](float at, bool yes, unsigned seed) {
|
|
const ImVec2 a(at, y), b(at + button_width, y + tall);
|
|
const bool picked = card.choice == yes;
|
|
const bool hovered = clickable && ImGui::IsMouseHoveringRect(a, b, false);
|
|
const bool lit = picked || hovered;
|
|
const ImU32 fill = yes ? (lit ? sticky : sticky_dim) : (lit ? theme::blue : skate_theme::tile_light);
|
|
skate_theme::rough_rect(draw, a, b, fill, seed, u * 2);
|
|
if (yes && picked) draw->AddRect(a, b, theme::blue, 0, 0, 6 * u);
|
|
const std::string label = yes ? (!card.have.empty() ? "UPDATE" : card.installed ? "SWITCH ON" : "DOWNLOAD") : "CANCEL";
|
|
const float size = 40 * u;
|
|
draw->AddText(bold, size, ImVec2(a.x + (button_width - width_of(bold, size, label)) * 0.5f, a.y + (tall - size) * 0.5f),
|
|
yes || lit ? skate_theme::black : theme::paper, label.c_str());
|
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
|
|
if (const auto reply = answer.load()) reply(yes);
|
|
};
|
|
button(left, true, 0x51);
|
|
button(left + button_width + 24 * u, false, 0x52);
|
|
// The controls, under the card and in its middle.
|
|
float x = min.x + (width - hints_width) * 0.5f;
|
|
const float hint_y = max.y + 34 * u;
|
|
for (const auto& hint : hints) x += theme::keycap(draw, bold, key_scale, ImVec2(x, hint_y), hint.key.c_str(), hint.what) + hint_gap;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- the vote for the next map
|
|
// A dedicated server's vote for the next map (Server/server_votes.cpp: the rotation's vote) is
|
|
// a poll whose answers are maps. It comes up as the maps to pick from: each with its picture,
|
|
// its name and its votes, in the middle of the screen as the map download's question is, and
|
|
// answered the same ways (D-pad or arrow keys to pick, A or Enter to vote, B or Esc to close
|
|
// it, or a click). Voting closes it; the vote then shows as the poll's small card, where the
|
|
// answer can still be changed.
|
|
namespace {
|
|
std::atomic<std::uint32_t> vote_shown{}; // the vote the popup is up for (its id)
|
|
std::atomic<std::uint32_t> vote_closed{}; // the vote it was answered or closed for
|
|
std::atomic<ULONGLONG> vote_drawn_at{}; // when it was last drawn
|
|
std::atomic<int> vote_pick{}, vote_options{}; // the picked answer, of how many
|
|
std::atomic<bool> vote_mine_seen{};
|
|
}
|
|
} // namespace dingosdk::overlay::detail
|
|
|
|
namespace dingosdk::overlay {
|
|
bool map_vote_asking() noexcept {
|
|
const auto shown = detail::vote_shown.load();
|
|
return shown && shown != detail::vote_closed.load() && GetTickCount64() - detail::vote_drawn_at.load() < 400;
|
|
}
|
|
void map_vote_move(int by) noexcept {
|
|
const int options = detail::vote_options.load();
|
|
if (options > 0) detail::vote_pick.store(((detail::vote_pick.load() + by) % options + options) % options);
|
|
}
|
|
void map_vote_dismiss() noexcept { detail::vote_closed.store(detail::vote_shown.load()); }
|
|
void map_vote_take() noexcept {
|
|
if (!map_vote_asking()) return;
|
|
try {
|
|
detail::queue_multiplayer_action("vote", std::to_string(detail::vote_pick.load() + 1));
|
|
} catch (...) {}
|
|
map_vote_dismiss();
|
|
}
|
|
|
|
void set_map_vote_icons(const std::vector<MapVoteIcon>& icons) noexcept {
|
|
try {
|
|
// One texture for them all: a row of squares, a map to each.
|
|
constexpr UINT side = 256;
|
|
detail::CardPixels atlas;
|
|
std::vector<std::string> names;
|
|
std::vector<bool> has;
|
|
if (!icons.empty()) {
|
|
atlas.width = side * static_cast<UINT>(icons.size());
|
|
atlas.height = side;
|
|
atlas.rgba.assign(static_cast<std::size_t>(atlas.width) * side * 4, 0);
|
|
}
|
|
bool any{};
|
|
for (std::size_t i = 0; i < icons.size(); ++i) {
|
|
names.push_back(icons[i].name);
|
|
detail::CardPixels one;
|
|
bool ok{};
|
|
if (icons[i].side == side && icons[i].rgba.size() == static_cast<std::size_t>(side) * side * 4) {
|
|
one.rgba = icons[i].rgba; // pixels already, at the atlas' size
|
|
one.width = one.height = side;
|
|
ok = true;
|
|
} else {
|
|
ok = !icons[i].png.empty() && icons[i].png.size() <= 4 * 1024 * 1024 && decode(icons[i].png, one, side) &&
|
|
one.width == side && one.height == side;
|
|
}
|
|
has.push_back(ok);
|
|
any |= ok;
|
|
if (!ok) continue;
|
|
for (UINT row = 0; row < side; ++row)
|
|
std::memcpy(atlas.rgba.data() + (static_cast<std::size_t>(row) * atlas.width + i * side) * 4,
|
|
one.rgba.data() + static_cast<std::size_t>(row) * side * 4, side * 4);
|
|
}
|
|
std::lock_guard lock(image_mutex);
|
|
vote_icon_names = std::move(names);
|
|
vote_icon_has = std::move(has);
|
|
// (Nothing to show: the card's icon, if one is up, is left alone.)
|
|
if (!any) {
|
|
if (image_is_atlas.load()) {
|
|
image.store(0);
|
|
current = detail::CardPixels{};
|
|
current.serial = ++image_serial;
|
|
waiting = false;
|
|
image_is_atlas.store(false);
|
|
}
|
|
return;
|
|
}
|
|
image.store(0);
|
|
atlas.serial = ++image_serial;
|
|
current = std::move(atlas);
|
|
waiting = true;
|
|
image_is_atlas.store(true);
|
|
} catch (...) {}
|
|
}
|
|
} // namespace dingosdk::overlay
|
|
|
|
namespace dingosdk::overlay::detail {
|
|
bool draw_map_vote(const MultiplayerVote& vote) {
|
|
const bool open = vote.id && vote.next_map && vote.outcome == 0 && vote.may_vote && vote.answers.size() >= 2;
|
|
if (!open) return false;
|
|
// A new vote comes up with nothing picked over from the last; one already answered (from
|
|
// the keys or chat) is not put in the way again.
|
|
if (vote_shown.exchange(vote.id) != vote.id) {
|
|
vote_pick.store(0);
|
|
vote_mine_seen.store(vote.mine != 0);
|
|
if (vote.mine) vote_closed.store(vote.id);
|
|
} else if (vote.mine && !vote_mine_seen.exchange(true)) {
|
|
vote_closed.store(vote.id);
|
|
}
|
|
if (vote_closed.load() == vote.id) return false;
|
|
vote_drawn_at.store(GetTickCount64());
|
|
const int options = static_cast<int>(vote.answers.size());
|
|
vote_options.store(options);
|
|
const int pick = std::clamp(vote_pick.load(), 0, options - 1);
|
|
|
|
auto& s = state();
|
|
auto* bold = s.menu.bold ? s.menu.bold : ImGui::GetFont();
|
|
auto* body = s.menu.body ? s.menu.body : ImGui::GetFont();
|
|
auto* heading = s.menu.heading ? s.menu.heading : bold;
|
|
const auto display = ImGui::GetIO().DisplaySize;
|
|
const float u = std::min(display.x / 3840.0f, display.y / 2160.0f);
|
|
constexpr ImU32 sticky = IM_COL32(236, 215, 108, 255);
|
|
const auto width_of = [](ImFont* font, float size, const std::string& text) { return font->CalcTextSizeA(size, FLT_MAX, 0, text.c_str()).x; };
|
|
|
|
dingosdk::ControllerInput pad;
|
|
DingoSDKOverlayReadControllerInput(&pad, true);
|
|
struct Hint { std::string key; const char* what; };
|
|
const std::vector<Hint> hints{
|
|
{pad.available ? std::string("D-pad") : std::string("< >"), "Pick"},
|
|
{pad.available ? dingosdk::controller_combo_label(0x1000, pad.style) : std::string("Enter"), "Vote"},
|
|
{pad.available ? dingosdk::controller_combo_label(0x2000, pad.style) : std::string("Esc"), "Close"}};
|
|
const float key_scale = u * 2, hint_gap = 36 * u;
|
|
float hints_width{};
|
|
for (const auto& hint : hints)
|
|
hints_width += width_of(bold, 12 * key_scale, hint.key) + 20 * key_scale + width_of(bold, 14 * key_scale, hint.what);
|
|
hints_width += hint_gap * static_cast<float>(hints.size() - 1);
|
|
|
|
// The tiles in a row, as many as fit the screen's width at their size, smaller when there are more.
|
|
const float inset = 44 * u, gap = 24 * u, label_size = 28 * u, title_size = 64 * u;
|
|
const float most = display.x - 200 * u - inset * 2;
|
|
const float tile = std::min(520 * u, (most - gap * static_cast<float>(options - 1)) / static_cast<float>(options));
|
|
const float pad_in = 20 * u, icon = tile - pad_in * 2, name_size = std::min(36 * u, tile * 0.075f), count_size = 30 * u, bar = 12 * u;
|
|
const float tile_height = pad_in + icon + 18 * u + name_size + 14 * u + bar + 12 * u + count_size + pad_in;
|
|
const float width = inset * 2 + tile * static_cast<float>(options) + gap * static_cast<float>(options - 1);
|
|
const float height = inset + label_size + 20 * u + title_size + 30 * u + tile_height + inset;
|
|
const float under = 34 * u + 20 * key_scale;
|
|
const ImVec2 min(std::floor((display.x - width) * 0.5f), std::floor((display.y - height - under) * 0.5f)), max(min.x + width, min.y + height);
|
|
auto* draw = ImGui::GetForegroundDrawList();
|
|
draw->AddRectFilled(ImVec2(0, 0), display, IM_COL32(0, 0, 0, s.hub_pointer ? 170 : 110));
|
|
skate_theme::rough_rect(draw, min, max, IM_COL32(26, 26, 26, 250), 0x4d3, u * 2);
|
|
const float left = min.x + inset, right = max.x - inset;
|
|
float y = min.y + inset;
|
|
draw->AddText(bold, label_size, ImVec2(left, y), theme::blue, "TIME IS UP ON THIS MAP");
|
|
{
|
|
const auto time = std::to_string(vote.seconds) + " s";
|
|
draw->AddText(bold, label_size, ImVec2(right - width_of(bold, label_size, time), y), theme::paper, time.c_str());
|
|
}
|
|
y += label_size + 20 * u;
|
|
draw->AddText(heading, title_size, ImVec2(left, y), theme::paper, "VOTE FOR THE NEXT MAP");
|
|
y += title_size + 30 * u;
|
|
|
|
// Which square of the atlas is each answer's, if it has one.
|
|
std::vector<int> cell(static_cast<std::size_t>(options), -1);
|
|
int cells{};
|
|
ImTextureID texture{};
|
|
{
|
|
std::lock_guard lock(image_mutex);
|
|
cells = static_cast<int>(vote_icon_names.size());
|
|
if (image_is_atlas.load()) texture = image.load();
|
|
for (int i = 0; i < options; ++i)
|
|
for (int k = 0; k < cells; ++k)
|
|
if (vote_icon_has[static_cast<std::size_t>(k)] && vote_icon_names[static_cast<std::size_t>(k)] == vote.answers[static_cast<std::size_t>(i)]) cell[static_cast<std::size_t>(i)] = k;
|
|
}
|
|
unsigned total{};
|
|
for (const auto count : vote.counts) total += count;
|
|
for (int i = 0; i < options; ++i) {
|
|
const auto& name = vote.answers[static_cast<std::size_t>(i)];
|
|
const unsigned count = static_cast<std::size_t>(i) < vote.counts.size() ? vote.counts[static_cast<std::size_t>(i)] : 0;
|
|
const ImVec2 a(left + (tile + gap) * static_cast<float>(i), y), b(a.x + tile, a.y + tile_height);
|
|
const bool hovered = ImGui::IsMouseHoveringRect(a, b, false);
|
|
if (hovered && (ImGui::GetIO().MouseDelta.x != 0 || ImGui::GetIO().MouseDelta.y != 0)) vote_pick.store(i);
|
|
const bool picked = pick == i, mine = vote.mine == static_cast<std::uint8_t>(i + 1);
|
|
skate_theme::rough_rect(draw, a, b, picked ? theme::blue : skate_theme::tile, 0x600 + static_cast<unsigned>(i), u * 2);
|
|
const ImVec2 icon_min(a.x + pad_in, a.y + pad_in), icon_max(icon_min.x + icon, icon_min.y + icon);
|
|
if (texture && cell[static_cast<std::size_t>(i)] >= 0 && cells > 0) {
|
|
const float from = static_cast<float>(cell[static_cast<std::size_t>(i)]) / static_cast<float>(cells);
|
|
draw->AddImage(texture, icon_min, icon_max, ImVec2(from, 0), ImVec2(from + 1.0f / static_cast<float>(cells), 1));
|
|
} else {
|
|
// No picture: the game's own maps, "Random map" and "Stay here", and one still on its way.
|
|
// Each is drawn as shapes on its own colour, sharp at any size (text this large is not).
|
|
const bool random = name == "Random map", stay = name == "Stay here";
|
|
constexpr ImU32 ink = IM_COL32(26, 26, 26, 255), green = IM_COL32(96, 190, 128, 255);
|
|
skate_theme::rough_rect(draw, icon_min, icon_max, random ? sticky : stay ? green : IM_COL32(52, 56, 62, 255), 0x620 + static_cast<unsigned>(i), u * 2);
|
|
const ImVec2 mid((icon_min.x + icon_max.x) * 0.5f, (icon_min.y + icon_max.y) * 0.5f);
|
|
const float r = icon * 0.26f, line = std::max(2.0f, icon * 0.035f);
|
|
if (random) {
|
|
// A die showing five.
|
|
draw->AddRectFilled(ImVec2(mid.x - r, mid.y - r), ImVec2(mid.x + r, mid.y + r), ink, r * 0.22f);
|
|
const float pip = r * 0.15f, out = r * 0.5f;
|
|
for (const auto& at : {ImVec2(0, 0), ImVec2(-1, -1), ImVec2(1, -1), ImVec2(-1, 1), ImVec2(1, 1)})
|
|
draw->AddCircleFilled(ImVec2(mid.x + at.x * out, mid.y + at.y * out), pip, sticky, 24);
|
|
} else if (stay) {
|
|
// A clock, and a plus for the time it adds.
|
|
draw->AddCircle(mid, r, ink, 64, line);
|
|
draw->AddLine(mid, ImVec2(mid.x, mid.y - r * 0.62f), ink, line);
|
|
draw->AddLine(mid, ImVec2(mid.x + r * 0.45f, mid.y + r * 0.2f), ink, line);
|
|
draw->AddCircleFilled(mid, line * 0.8f, ink, 16);
|
|
const ImVec2 plus(mid.x + r * 0.95f, mid.y - r * 0.95f);
|
|
const float arm = r * 0.26f;
|
|
draw->AddCircleFilled(plus, arm * 1.7f, ink, 32);
|
|
draw->AddLine(ImVec2(plus.x - arm, plus.y), ImVec2(plus.x + arm, plus.y), green, line);
|
|
draw->AddLine(ImVec2(plus.x, plus.y - arm), ImVec2(plus.x, plus.y + arm), green, line);
|
|
} else {
|
|
// A map without a picture: hills under a sun.
|
|
constexpr ImU32 far_hill = IM_COL32(104, 112, 122, 255), near_hill = IM_COL32(138, 146, 156, 255);
|
|
const float base = mid.y + r * 0.75f;
|
|
draw->AddCircleFilled(ImVec2(mid.x + r * 0.7f, mid.y - r * 0.7f), r * 0.24f, sticky, 32);
|
|
draw->AddTriangleFilled(ImVec2(mid.x - r * 0.2f, base), ImVec2(mid.x + r * 0.55f, mid.y - r * 0.2f), ImVec2(mid.x + r * 1.3f, base), far_hill);
|
|
draw->AddTriangleFilled(ImVec2(mid.x - r * 1.3f, base), ImVec2(mid.x - r * 0.35f, mid.y - r * 0.65f), ImVec2(mid.x + r * 0.6f, base), near_hill);
|
|
}
|
|
}
|
|
float ty = icon_max.y + 18 * u;
|
|
draw->PushClipRect(ImVec2(a.x + pad_in, ty - name_size), ImVec2(b.x - pad_in, ty + name_size * 2), true);
|
|
draw->AddText(bold, name_size, ImVec2(a.x + pad_in, ty), theme::paper, name.c_str());
|
|
draw->PopClipRect();
|
|
ty += name_size + 14 * u;
|
|
draw->AddRectFilled(ImVec2(a.x + pad_in, ty), ImVec2(b.x - pad_in, ty + bar), picked ? IM_COL32(0, 0, 0, 90) : skate_theme::tile_light, 4 * u);
|
|
if (total && count)
|
|
draw->AddRectFilled(ImVec2(a.x + pad_in, ty), ImVec2(a.x + pad_in + icon * static_cast<float>(count) / static_cast<float>(total), ty + bar), sticky, 4 * u);
|
|
ty += bar + 12 * u;
|
|
const auto votes = std::to_string(count) + (count == 1 ? " vote" : " votes") + (mine ? " - yours" : "");
|
|
draw->AddText(body, count_size, ImVec2(a.x + pad_in, ty), mine ? sticky : picked ? theme::paper : theme::muted, votes.c_str());
|
|
if (picked) draw->AddRect(a, b, sticky, 0, 0, 6 * u);
|
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
vote_pick.store(i);
|
|
map_vote_take();
|
|
}
|
|
}
|
|
float x = min.x + (width - hints_width) * 0.5f;
|
|
const float hint_y = max.y + 34 * u;
|
|
for (const auto& hint : hints) x += theme::keycap(draw, bold, key_scale, ImVec2(x, hint_y), hint.key.c_str(), hint.what) + hint_gap;
|
|
return true;
|
|
}
|
|
} // namespace dingosdk::overlay::detail
|