diff --git a/Extension/Boot/ea_service_block.cpp b/Extension/Boot/ea_service_block.cpp index 8947c55..4807875 100644 --- a/Extension/Boot/ea_service_block.cpp +++ b/Extension/Boot/ea_service_block.cpp @@ -116,17 +116,31 @@ hostent* WSAAPI get_host_by_name(const char* name) { return original_get_host_by_name.load()(name); } +// `required`: the entry point the game itself resolves names through; without it nothing is +// blocked, and that is an error. The others close the same door for anything else loaded into +// the process, and are left alone where they cannot be hooked: Wine's ws2_32 (Proton, on Linux +// and the Steam Deck) does not have all of them, and what it does not have nothing can call. template -void hook(const char* name, void* replacement, std::atomic& original) { +void hook(const char* name, void* replacement, std::atomic& original, bool required) { + const auto fail = [&](const std::string& why) { + if (required) throw std::runtime_error(why); + logging::log(logging::Level::info, logging::Channel::runtime, "EA online services block: {} left as it is ({}).", name, why); + }; const auto module = LoadLibraryExW(L"ws2_32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); const auto target = module ? reinterpret_cast(GetProcAddress(module, name)) : nullptr; - if (!target) throw std::runtime_error(std::string("Missing ") + name); + if (!target) return fail(std::string("Missing ") + name); void* relay{}; const auto create = hook_prepare(target, replacement, &relay); - if (create != HookOk) throw std::runtime_error(std::string("Cannot hook ") + name + ": " + hook_status_string(create)); + if (create != HookOk || !relay) { + if (create == HookOk) (void)hook_remove(target); + return fail(std::string("Cannot hook ") + name + ": " + hook_status_string(create == HookOk ? HookUnsupportedFunction : create)); + } original.store(reinterpret_cast(relay)); const auto enable = hook_enable(target); - if (enable != HookOk) throw std::runtime_error(std::string("Cannot enable ") + name + ": " + hook_status_string(enable)); + if (enable != HookOk) { + (void)hook_remove(target); + return fail(std::string("Cannot enable ") + name + ": " + hook_status_string(enable)); + } } } // namespace @@ -137,11 +151,11 @@ bool start_ea_service_block(std::string& error) noexcept { // same door for anything else loaded into the process. #pragma warning(push) #pragma warning(disable: 4191) - hook("getaddrinfo", reinterpret_cast(&get_addr_info_a), original_get_addr_info_a); - hook("GetAddrInfoW", reinterpret_cast(&get_addr_info_w), original_get_addr_info_w); - hook("GetAddrInfoExA", reinterpret_cast(&get_addr_info_ex_a), original_get_addr_info_ex_a); - hook("GetAddrInfoExW", reinterpret_cast(&get_addr_info_ex_w), original_get_addr_info_ex_w); - hook("gethostbyname", reinterpret_cast(&get_host_by_name), original_get_host_by_name); + hook("getaddrinfo", reinterpret_cast(&get_addr_info_a), original_get_addr_info_a, true); + hook("GetAddrInfoW", reinterpret_cast(&get_addr_info_w), original_get_addr_info_w, false); + hook("GetAddrInfoExA", reinterpret_cast(&get_addr_info_ex_a), original_get_addr_info_ex_a, false); + hook("GetAddrInfoExW", reinterpret_cast(&get_addr_info_ex_w), original_get_addr_info_ex_w, false); + hook("gethostbyname", reinterpret_cast(&get_host_by_name), original_get_host_by_name, false); #pragma warning(pop) error.clear(); return true; diff --git a/Extension/UI/Overlay/chat_overlay.cpp b/Extension/UI/Overlay/chat_overlay.cpp index 6f27652..c2cdf59 100644 --- a/Extension/UI/Overlay/chat_overlay.cpp +++ b/Extension/UI/Overlay/chat_overlay.cpp @@ -449,77 +449,96 @@ void draw_command_list(ChatState& c, ImFont* heading, ImFont* body, ImVec2 botto "; Tab for the next, Shift+Tab back").c_str()); } -// The vote a dedicated server is running, or has just finished: a card above the chat with what -// is voted on, the tally, the time left, and Yes and No. The player answers with their binds -// (shown on the buttons; F1 and F2 unless changed), or by clicking while the cursor is free. -// `bottom_right` is the corner the card sits on. Returns the room it took, 0 when there is none. -float draw_vote(const MultiplayerVote& vote, ImFont* heading, ImFont* body, ImVec2 bottom_right, float width, float scale, - bool clickable) { - if (!vote.id) return 0.0f; - constexpr ImU32 green = IM_COL32(88, 214, 141, 255), red = IM_COL32(236, 96, 96, 255); +// The vote a dedicated server is running, or has just finished: a card on the middle of the +// screen's right edge, in the look of the overlay's notices (a dark tile with a strip of colour +// down its left). It says what is voted on, the tally and the time left, and has Yes and No. The +// player answers with their binds (the keycaps on the buttons; F1 and F2 unless changed), or by +// clicking while the cursor is free. +void draw_vote(const MultiplayerVote& vote, ImFont* heading, ImFont* body, ImVec2 display, float scale, bool clickable) { + if (!vote.id) return; + constexpr ImU32 green = skate_theme::good, red = skate_theme::danger; const bool running = vote.outcome == 0; const ImU32 accent = running ? theme::blue : vote.outcome == 1 ? green : vote.outcome == 2 ? red : theme::muted; const char* title = running ? "VOTE" : vote.outcome == 1 ? "VOTE PASSED" : vote.outcome == 2 ? "VOTE FAILED" : "VOTE CANCELLED"; std::string question = vote.label; if (!question.empty() && question.front() >= 'a' && question.front() <= 'z') question.front() = static_cast(question.front() - 32); if (running) question += '?'; - const float pad = 12.0f * scale, gap = 6.0f * scale, title_size = 14.0f * scale, size = 16.0f * scale, fine = 14.0f * scale; - const float button_height = 30.0f * scale, bar = 3.0f * scale, inner = width - pad * 2.0f; - const auto question_extent = body->CalcTextSizeA(size, FLT_MAX, inner, question.c_str()); + const float width = 360.0f * scale, margin = 24.0f * scale, strip = 5.0f * scale; + const float pad = 14.0f * scale, gap = 8.0f * scale, title_size = 13.0f * scale, size = 18.0f * scale, fine = 13.0f * scale; + const float button_height = 34.0f * scale, bar = 4.0f * scale, inner = width - strip - pad * 2.0f; + const auto question_extent = heading->CalcTextSizeA(size, FLT_MAX, inner, question.c_str()); const bool answers = running && vote.may_vote; - const float height = pad + title_size + gap + question_extent.y + gap + fine + gap + bar + + const float height = pad + title_size + gap + question_extent.y + gap + bar + gap + fine + (running ? gap + (answers ? button_height : fine) : 0.0f) + pad; - const ImVec2 min(bottom_right.x - width, bottom_right.y - height), max(bottom_right); + const ImVec2 min(display.x - margin - width, std::floor((display.y - height) * 0.5f)), max(min.x + width, min.y + height); auto* draw = ImGui::GetForegroundDrawList(); - draw->AddRectFilled(min, max, with_alpha(theme::ink, 0.92f), 4.0f * scale); - draw->AddRect(min, max, with_alpha(accent, 0.9f), 4.0f * scale); + draw->AddRectFilled(min, max, with_alpha(theme::ink, 0.92f)); + draw->AddRectFilled(min, ImVec2(min.x + strip, max.y), accent); + const float left = min.x + strip + pad, right = max.x - pad; float y = min.y + pad; - draw->AddText(heading, title_size, ImVec2(min.x + pad, y), accent, title); + draw->AddText(heading, title_size, ImVec2(left, y), accent, title); if (running) { - const auto left = std::to_string(vote.seconds) + " s"; - const auto extent = heading->CalcTextSizeA(title_size, FLT_MAX, 0.0f, left.c_str()); - draw->AddText(heading, title_size, ImVec2(max.x - pad - extent.x, y), theme::muted, left.c_str()); + const auto time = std::to_string(vote.seconds) + " s"; + const auto extent = heading->CalcTextSizeA(title_size, FLT_MAX, 0.0f, time.c_str()); + draw->AddText(heading, title_size, ImVec2(right - extent.x, y), theme::paper, time.c_str()); } y += title_size + gap; - draw->AddText(body, size, ImVec2(min.x + pad, y), theme::paper, question.c_str(), nullptr, inner); + draw->AddText(heading, size, ImVec2(left, y), theme::paper, question.c_str(), nullptr, inner); y += question_extent.y + gap; - // The tally, and a bar of how far Yes is towards what it needs. - float x = min.x + pad; + // How far Yes is towards what it needs, then the tally under it. + const float share = vote.needed ? std::clamp(static_cast(vote.yes) / static_cast(vote.needed), 0.0f, 1.0f) : 0.0f; + draw->AddRectFilled(ImVec2(left, y), ImVec2(right, y + bar), skate_theme::tile_light); + if (share > 0.0f) draw->AddRectFilled(ImVec2(left, y), ImVec2(left + inner * share, y + bar), green); + y += bar + gap; + float x = left; const auto word = [&](const std::string& text, ImU32 colour) { - draw->AddText(heading, fine, ImVec2(x, y), colour, text.c_str()); - x += heading->CalcTextSizeA(fine, FLT_MAX, 0.0f, text.c_str()).x + 14.0f * scale; + draw->AddText(body, fine, ImVec2(x, y), colour, text.c_str()); + x += body->CalcTextSizeA(fine, FLT_MAX, 0.0f, text.c_str()).x + 14.0f * scale; }; word("Yes " + std::to_string(vote.yes), green); word("No " + std::to_string(vote.no), red); - word(std::to_string(vote.needed) + " needed", theme::muted); - y += fine + gap; - const float share = vote.needed ? std::clamp(static_cast(vote.yes) / static_cast(vote.needed), 0.0f, 1.0f) : 0.0f; - draw->AddRectFilled(ImVec2(min.x + pad, y), ImVec2(max.x - pad, y + bar), with_alpha(theme::muted, 0.35f), bar * 0.5f); - if (share > 0.0f) draw->AddRectFilled(ImVec2(min.x + pad, y), ImVec2(min.x + pad + inner * share, y + bar), green, bar * 0.5f); - y += bar; - if (!running) return height + gap; + { + const auto needed = std::to_string(vote.needed) + " needed"; + const auto extent = body->CalcTextSizeA(fine, FLT_MAX, 0.0f, needed.c_str()); + draw->AddText(body, fine, ImVec2(right - extent.x, y), theme::muted, needed.c_str()); + } + y += fine; + if (!running) return; y += gap; if (!answers) { - draw->AddText(body, fine, ImVec2(min.x + pad, y), theme::muted, "This vote is about you, so you have no vote in it."); - return height + gap; + draw->AddText(body, fine, ImVec2(left, y), theme::muted, "This vote is about you, so you have no vote in it."); + return; } + // Two tiles as the menu's: grey, the answer's colour once it is this player's, each with its + // bind on a white keycap. const float button_width = (inner - gap) * 0.5f; - const auto button = [&](float left, bool yes, std::uint32_t bind) { - const ImVec2 a(left, y), b(left + button_width, y + button_height); + const auto button = [&](float at, bool yes, std::uint32_t bind) { + const ImVec2 a(at, y), b(at + button_width, y + button_height); const ImU32 colour = yes ? green : red; const bool chosen = vote.mine == (yes ? 1 : 2); const bool hovered = clickable && ImGui::IsMouseHoveringRect(a, b, false); - draw->AddRectFilled(a, b, with_alpha(colour, chosen ? 0.85f : hovered ? 0.35f : 0.14f), 4.0f * scale); - draw->AddRect(a, b, with_alpha(colour, 0.9f), 4.0f * scale); - const std::string label = (bind ? dingosdk::controller_combo_label(bind) + " " : std::string{}) + (yes ? "Yes" : "No"); - const auto extent = heading->CalcTextSizeA(size, FLT_MAX, 0.0f, label.c_str()); - draw->AddText(heading, size, ImVec2(a.x + (button_width - extent.x) * 0.5f, a.y + (button_height - extent.y) * 0.5f), - chosen ? theme::ink : theme::paper, label.c_str()); + draw->AddRectFilled(a, b, chosen ? colour : hovered ? skate_theme::tile_light : skate_theme::tile_grey); + if (!chosen) draw->AddRectFilled(ImVec2(a.x, b.y - 3.0f * scale), b, colour); + const std::string key = bind ? dingosdk::controller_combo_label(bind) : std::string{}; + const char* label = yes ? "YES" : "NO"; + const float label_size = 15.0f * scale, key_size = 12.0f * scale, cap_height = 20.0f * scale; + const auto label_extent = heading->CalcTextSizeA(label_size, FLT_MAX, 0.0f, label); + const auto key_extent = key.empty() ? ImVec2{} : heading->CalcTextSizeA(key_size, FLT_MAX, 0.0f, key.c_str()); + const float cap_width = key.empty() ? 0.0f : key_extent.x + 12.0f * scale, between = key.empty() ? 0.0f : 8.0f * scale; + float cursor = a.x + (button_width - cap_width - between - label_extent.x) * 0.5f; + if (!key.empty()) { + const ImVec2 cap(cursor, a.y + (button_height - cap_height) * 0.5f); + draw->AddRectFilled(cap, ImVec2(cap.x + cap_width, cap.y + cap_height), theme::paper, 3.0f * scale); + draw->AddText(heading, key_size, ImVec2(cap.x + 6.0f * scale, cap.y + (cap_height - key_extent.y) * 0.5f), skate_theme::black, + key.c_str()); + cursor += cap_width + between; + } + draw->AddText(heading, label_size, ImVec2(cursor, a.y + (button_height - label_extent.y) * 0.5f), + chosen ? skate_theme::black : theme::paper, label); if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) queue_multiplayer_action("vote", yes ? "yes" : "no"); }; - button(min.x + pad, true, vote.yes_bind); - button(min.x + pad + button_width + gap, false, vote.no_bind); - return height + gap; + button(left, true, vote.yes_bind); + button(left + button_width + gap, false, vote.no_bind); } void draw_chat() { @@ -637,11 +656,10 @@ void draw_chat() { ImGui::End(); ImGui::PopStyleVar(2); ImGui::PopStyleColor(4); - // The vote card sits on the panel; the command list and the emote picker go above it. - const float above = panel_top - 6.0f * scale - - draw_vote(c.feed.vote, heading, body, ImVec2(display.x - margin, panel_top - 6.0f * scale), width, scale, true); + const float above = panel_top - 6.0f * scale; draw_command_list(c, heading, body, ImVec2(display.x - margin - width, above), width, scale); emote_picker(c, heading, ImVec2(display.x - margin - width, above), width, scale); + draw_vote(c.feed.vote, heading, body, display, scale, true); // the cursor is free while typing return; } @@ -692,9 +710,7 @@ void draw_chat() { else draw->AddText(body, size, text_at, text, drawn.text.c_str(), nullptr, wrap); bottom = top_left.y - gap; } - // Above the newest lines, or in the corner when there are none. Clickable while the menu or - // the console has freed the cursor. - draw_vote(c.feed.vote, heading, body, ImVec2(display.x - margin, bottom), width, scale, - s.visible.load() || s.console_visible.load()); + // Clickable while the menu or the console has freed the cursor. + draw_vote(c.feed.vote, heading, body, display, scale, s.visible.load() || s.console_visible.load()); } } // namespace dingosdk::overlay::detail diff --git a/Extension/UI/Overlay/skate_menu_skater.cpp b/Extension/UI/Overlay/skate_menu_skater.cpp index d7a4d47..5609442 100644 --- a/Extension/UI/Overlay/skate_menu_skater.cpp +++ b/Extension/UI/Overlay/skate_menu_skater.cpp @@ -139,7 +139,7 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) { const auto& debug = model.debug; - begin_card(menu, "movement", "MOVEMENT"); + begin_card(menu, "movement", "FLIGHT & BAILS"); bool noclip = debug.noclip; if (toggle_row(menu, "Noclip", "Fly with the normal player camera. Includes No Bail; uses the Freecam flight speed.", noclip, (debug.noclip_available || debug.noclip) && callbacks.queue_debug)) @@ -155,9 +155,14 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c (debug.no_bail_available || debug.no_bail) && callbacks.queue_debug)) debug_request(menu, callbacks, {DebugAction::set_no_bail, no_bail}); bool hall_of_meat = model.hall_of_meat.enabled; - if (toggle_row(menu, "Hall of Meat", - "Your bails show the bones they hurt and score a Meat card, as in skate. 3. A break slows the game in single player.", - hall_of_meat, model.hall_of_meat.available && callbacks.queue_console_command)) + // No Bail (and noclip, which includes it) stops the bails Hall of Meat scores. + const bool bails_off = debug.no_bail || debug.no_bail_active; + const char* meat_help = hall_of_meat && bails_off + ? "On, but No Bail is stopping your bails, so nothing shows. Turn No Bail (and noclip) off to use it." + : bails_off + ? "Your bails show the bones they hurt and score a Meat card. Needs No Bail (and noclip) off." + : "Your bails show the bones they hurt and score a Meat card. A break slows the game in single player."; + if (toggle_row(menu, "Hall of Meat", meat_help, hall_of_meat, model.hall_of_meat.available && callbacks.queue_console_command)) send_console(menu, callbacks, hall_of_meat ? "hallofmeat 1" : "hallofmeat 0"); end_card(); @@ -182,7 +187,7 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c } void skater_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) { - category_tabs(menu, menu.skater_tab, {"CAMERA", "MOVEMENT"}, "skater-tabs"); + category_tabs(menu, menu.skater_tab, {"CAMERA", "GAMEPLAY"}, "skater-tabs"); ImGui::PushID(menu.skater_tab); ImGui::BeginChild("skater-tab", ImVec2(0, page_body_height(menu))); if (menu.skater_tab == 0) camera_controls(menu, model, callbacks); diff --git a/Server/server_votes.cpp b/Server/server_votes.cpp index b18c611..75bc200 100644 --- a/Server/server_votes.cpp +++ b/Server/server_votes.cpp @@ -164,7 +164,7 @@ void Host::start_vote(Guest &guest, VoteKind kind, std::string_view argument) { const auto label = vote.label; vote_ = std::move(vote); send_chat(guest_name(guest) + " started a vote to " + label + " (" + std::to_string(setting.percent) + "% needed, " + - std::to_string(config_.votes.seconds) + " s). Vote on the card above chat, or type /yes or /no."); + std::to_string(config_.votes.seconds) + " s). Vote on the card at the right of your screen, or type /yes or /no."); log_("[vote] " + guest_name(guest) + " started a vote to " + label + "."); check_vote(false); } @@ -228,7 +228,7 @@ void Host::check_vote(bool expired) { const bool lost = !passed && (expired || yes + (eligible - std::min(eligible, yes + no)) < needed); const auto tally = std::to_string(yes) + " yes, " + std::to_string(no) + " no, " + std::to_string(needed) + " needed"; if (!passed && !lost) { - // Games show the tally as it changes (the vote card above chat); chat is not filled with it. + // Games show the tally as it changes (the vote card); chat is not filled with it. vote.shown_yes = yes; vote.shown_no = no; show_vote(vote, multiplayer::vote_running, yes, no, needed);