mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 22:06:42 +01:00
Drone: settings move into the ReSkate menu (DRONE page)
The drone's own floating window becomes a DRONE page in the ReSkate menu (Insert), after TRAINER, built from the menu's cards, ON/OFF rows, fields and option strips like every other page. Its tabs are the window's: FLY, HANDLING, RATES, DISPLAY, CONTROLLER and LOBBY. `dronemenu` now opens the menu on that page. The FLY tab ends with a small credit line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MArBL6UbYZPsp9rMnq2Ks5
This commit is contained in:
12 files changed
+491
-490
No files matched your search
@@ -62,7 +62,7 @@ void register_movement_commands(Commands ®istry) {
|
||||
registry.add(std::move(entry));
|
||||
}
|
||||
auto drone_mode = variable("dronemode",
|
||||
"Fly the free camera as an FPV drone. Easiest from the drone window (dronemenu): Start flying",
|
||||
"Fly the free camera as an FPV drone. Easiest from the DRONE page of the ReSkate menu (Insert): Start flying",
|
||||
Group::movement, argument("0|1", Type::boolean));
|
||||
drone_mode.inspect = [](const Model &m) {
|
||||
return boolean_state(m.debug.available, drone::is_enabled(), "Waiting for local player controls.",
|
||||
@@ -126,12 +126,11 @@ void register_movement_commands(Commands ®istry) {
|
||||
: std::format("{} = {:g} (the closest allowed value)", name, value));
|
||||
};
|
||||
registry.add(std::move(tune));
|
||||
auto menu_command = action("dronemenu", "Show or hide the drone window (it shows while this menu is open)", Group::movement);
|
||||
auto menu_command = action("dronemenu", "Open the ReSkate menu on its DRONE page", Group::movement);
|
||||
menu_command.inspect = [](const Model &) { return State{true, {}, {}, {}, false}; };
|
||||
menu_command.run = [](const Model &, const Values &, const Output &out) {
|
||||
const bool now_open = !drone::menu_flag().load();
|
||||
drone::menu_flag().store(now_open);
|
||||
out(now_open ? "Drone window shown." : "Drone window hidden.");
|
||||
drone::menu_flag().store(true);
|
||||
out("Opening the DRONE page.");
|
||||
};
|
||||
registry.add(std::move(menu_command));
|
||||
auto arm_command = action("dronearm", "Arm the drone: motors on, ready to fly", Group::movement);
|
||||
@@ -162,13 +161,13 @@ void register_movement_commands(Commands ®istry) {
|
||||
Group::movement, argument("0|1", Type::boolean));
|
||||
radio_mode.inspect = [](const Model &m) {
|
||||
return boolean_state(m.debug.available, drone::radio::config().enabled.load(), "Waiting for local player controls.",
|
||||
"Set it up in the drone window (dronemenu, Controller tab)");
|
||||
"Set it up on the DRONE page (dronemenu), CONTROLLER tab");
|
||||
};
|
||||
radio_mode.run = [](const Model &, const Values &args, const Output &out) {
|
||||
const bool on = std::get<bool>(args[0]);
|
||||
drone::radio::config().enabled = on;
|
||||
profile_runtime::set_local_values({{"Drone.Radio", on ? 1.0 : 0.0}});
|
||||
out(on ? "Controller on. If a stick does the wrong thing, run Set up controller (dronemenu, Controller tab)."
|
||||
out(on ? "Controller on. If a stick does the wrong thing, run Set up controller (dronemenu, CONTROLLER tab)."
|
||||
: "Controller off. The drone uses the keyboard and mouse.");
|
||||
};
|
||||
radio_mode.reset = [](const Model &, const Output &) {
|
||||
@@ -196,7 +195,7 @@ void register_movement_commands(Commands ®istry) {
|
||||
out(std::format("Mapping (roll pitch throttle yaw): {} {} {} {}", map[0], map[1], map[2], map[3]));
|
||||
out(std::format("Drone reads: roll {:+.2f} (right +) pitch {:+.2f} (forward +) throttle {:.2f} yaw {:+.2f} (right +)",
|
||||
cmd.roll, cmd.pitch, cmd.throttle, cmd.yaw));
|
||||
out("A stick doing the wrong thing? Easiest fix: dronemenu, Controller tab, Set up controller.");
|
||||
out("A stick doing the wrong thing? Easiest fix: dronemenu, CONTROLLER tab, Set up controller.");
|
||||
};
|
||||
registry.add(std::move(radio_check));
|
||||
const auto axis_argument = [](const char *name) {
|
||||
|
||||
@@ -93,10 +93,10 @@ inline std::atomic<bool>& armed_flag() {
|
||||
}
|
||||
inline bool armed() { return armed_flag().load(std::memory_order_acquire); }
|
||||
inline void set_armed(bool on) { armed_flag().store(on, std::memory_order_release); }
|
||||
// The drone's settings window in the overlay (UI/Overlay/drone_menu_overlay.cpp): shown while the
|
||||
// overlay is open; the X closes it and `dronemenu` brings it back.
|
||||
// `dronemenu` asks for the ReSkate menu to open on its DRONE page (UI/Overlay/drone_page.cpp);
|
||||
// the menu takes the request when it next draws.
|
||||
inline std::atomic<bool>& menu_flag() {
|
||||
static std::atomic<bool> value{true};
|
||||
static std::atomic<bool> value{false};
|
||||
return value;
|
||||
}
|
||||
// Asks the flight tick to put the drone back at its starting point and restart the timer and battery.
|
||||
@@ -162,7 +162,7 @@ inline constexpr KnobInfo knob_info[] = {
|
||||
{"motorpitch", "Motor sound pitch. Higher sounds like a smaller drone", 0.4f, 2.5f, 1.0f},
|
||||
{"throttlecentre", "Your throttle stick springs back to the middle (gamepads): the middle hovers, up climbs, down descends", 0.0f, 1.0f, 0.0f},
|
||||
{"deadzone", "How far a stick must move before the drone reacts (0-0.3). Raise it if the drone drifts on its own", 0.0f, 0.3f, 0.03f},
|
||||
{"goggles", "What your skater wears over their eyes while you fly: 0 = nothing, 1-8 = one of the eyewear choices in the drone window", 0.0f, 8.0f, 1.0f},
|
||||
{"goggles", "What your skater wears over their eyes while you fly: 0 = nothing, 1-8 = one of the eyewear choices on the DRONE page", 0.0f, 8.0f, 1.0f},
|
||||
{"hideui", "Hide the game's own interface while you fly (Start flying hides it, Stop flying brings it back)", 0.0f, 1.0f, 1.0f},
|
||||
};
|
||||
inline constexpr std::size_t knob_count = std::size(knob_info);
|
||||
@@ -217,7 +217,7 @@ inline bool failsafe_due(float distance, float range, float seconds_lost) {
|
||||
}
|
||||
|
||||
// Presets: whole drones in one choice. Each sets the handling, rates, size and motor sound, and
|
||||
// the top speed (the `flyspeed` setting). The drone window applies them through the same commands
|
||||
// the top speed (the `flyspeed` setting). The DRONE page applies them through the same commands
|
||||
// a player could type, so they are saved like any other change, and shows "Custom" once one is changed.
|
||||
struct PresetValue { Knob knob; float value; };
|
||||
struct Preset {
|
||||
|
||||
@@ -94,7 +94,7 @@ struct Sample {
|
||||
// axis that moved furthest from rest, once it has been held well over for a moment, and takes the
|
||||
// direction it moved as "plus" (throttle up, yaw right, pitch forward, roll right). The stick must
|
||||
// come back to rest before the next one. Switches are any other axis that swings across, or a
|
||||
// button that was not already down at rest. Plain maths: driven by the drone window's frames.
|
||||
// button that was not already down at rest. Plain maths: driven by the DRONE page's frames.
|
||||
class Calibration {
|
||||
public:
|
||||
enum class Step { idle, rest, throttle, yaw, pitch, roll, arm, reset, done };
|
||||
@@ -237,10 +237,10 @@ private:
|
||||
}
|
||||
}
|
||||
};
|
||||
// The one setup the drone window runs. Overlay thread only; the game thread reads calibrating().
|
||||
// The one setup the DRONE page runs. Overlay thread only; the game thread reads calibrating().
|
||||
inline Calibration& calibration() { static Calibration value; return value; }
|
||||
// While the setup is on screen the game holds the drone still instead of flying it with the sticks.
|
||||
// The drone window marks each frame it shows the setup; leaving the tab or closing the menu lets
|
||||
// The DRONE page marks each frame it shows the setup; leaving the tab or closing the menu lets
|
||||
// the drone fly again within half a second, even with the setup unfinished.
|
||||
inline std::atomic<std::int64_t>& calibration_seen() { static std::atomic<std::int64_t> value{0}; return value; }
|
||||
inline std::int64_t calibration_clock() {
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
#include "overlay_internal.h"
|
||||
#include "skate_style.h"
|
||||
#include "Extension/Skater/drone_flight.h"
|
||||
#include "Extension/Skater/drone_beacon.h"
|
||||
#include "Extension/Skater/drone_outfit.h"
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
|
||||
// The FPV drone's settings window: fly / arm / reset buttons, handling, Betaflight rates, the
|
||||
// display, the radio and multiplayer. It shows while the menu or console holds the pointer
|
||||
// (like the profiler window). Every change takes effect at once; it is also saved by running the
|
||||
// same `dronetune` command you could type, so the console and this window always agree.
|
||||
|
||||
namespace dingosdk::overlay::detail {
|
||||
namespace {
|
||||
namespace skate = dingosdk::skate_theme;
|
||||
namespace drone = dingosdk::drone;
|
||||
using drone::Knob;
|
||||
|
||||
float scale() {
|
||||
return std::clamp(state().model.menu_scale, dingosdk::min_menu_scale, dingosdk::max_menu_scale);
|
||||
}
|
||||
|
||||
// Runs a console command on the game's thread (what the console itself does).
|
||||
bool run(const std::string& command) {
|
||||
auto& s = state();
|
||||
if (!s.callbacks.queue_console_command) return false;
|
||||
std::array<char, 256> result{};
|
||||
return s.callbacks.queue_console_command(s.callbacks.user, command.c_str(), result.data(), result.size());
|
||||
}
|
||||
|
||||
float get(Knob k) { return drone::knobs().value[k].load(std::memory_order_relaxed); }
|
||||
void set(Knob k, float v) { drone::knobs().value[k].store(v, std::memory_order_relaxed); }
|
||||
// Saves a setting by running `dronetune`, which also clamps it and keeps it for next time.
|
||||
void save(Knob k) { run(std::format("dronetune {} {}", drone::knob_info[k].name, get(k))); }
|
||||
|
||||
void tip(Knob k) {
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s.\n\nConsole: dronetune %s (default %g)", drone::knob_info[k].help, drone::knob_info[k].name,
|
||||
static_cast<double>(drone::knob_info[k].def));
|
||||
}
|
||||
|
||||
void slider(Knob k, const char* label, const char* format = "%.2f") {
|
||||
float v = get(k);
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::SliderFloat(label, &v, drone::knob_info[k].lo, drone::knob_info[k].hi, format)) set(k, v);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) save(k);
|
||||
tip(k);
|
||||
}
|
||||
void integer(Knob k, const char* label) {
|
||||
int v = static_cast<int>(std::lround(get(k)));
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::SliderInt(label, &v, static_cast<int>(drone::knob_info[k].lo), static_cast<int>(drone::knob_info[k].hi)))
|
||||
set(k, static_cast<float>(v));
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) save(k);
|
||||
tip(k);
|
||||
}
|
||||
void toggle(Knob k, const char* label) {
|
||||
bool on = get(k) >= 0.5f;
|
||||
if (ImGui::Checkbox(label, &on)) { set(k, on ? 1.0f : 0.0f); save(k); }
|
||||
tip(k);
|
||||
}
|
||||
void choice(Knob k, const char* label, const char* const* items, int count) {
|
||||
int v = std::clamp(static_cast<int>(std::lround(get(k))), 0, count - 1);
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::Combo(label, &v, items, count)) { set(k, static_cast<float>(v)); save(k); }
|
||||
tip(k);
|
||||
}
|
||||
|
||||
// Presets (drone_flight.h): a whole drone in one choice. "Custom" once anything is changed.
|
||||
void preset_picker() {
|
||||
auto& s = state();
|
||||
const int current = drone::matching_preset(s.model.debug.camera_speed);
|
||||
const char* shown = current >= 0 ? drone::presets[current].name : "Custom";
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::BeginCombo("Drone", shown)) {
|
||||
for (std::size_t p = 0; p < drone::preset_count; ++p) {
|
||||
const auto& preset = drone::presets[p];
|
||||
if (ImGui::Selectable(preset.name, static_cast<int>(p) == current)) {
|
||||
for (const auto& v : preset.values) { set(v.knob, v.value); save(v.knob); }
|
||||
s.model.debug.camera_speed = preset.top_speed;
|
||||
run(std::format("flyspeed {}", preset.top_speed));
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", preset.description);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (current >= 0) ImGui::TextDisabled("%s", drone::presets[current].description);
|
||||
else ImGui::TextDisabled("Your own settings. Pick a drone above to start from one of the presets.");
|
||||
}
|
||||
|
||||
void tab_fly() {
|
||||
auto& s = state();
|
||||
const bool enabled = drone::is_enabled();
|
||||
const bool radio_on = drone::radio::config().enabled.load();
|
||||
const bool freecam = s.model.debug.free_camera;
|
||||
if (enabled && freecam) ImGui::TextDisabled("Flying with %s.", radio_on ? "the controller" : "the keyboard and mouse");
|
||||
else if (enabled) ImGui::TextDisabled("Drone mode is on, but freecam is off: press Start flying.");
|
||||
ImGui::BeginDisabled(!s.model.debug.camera_available);
|
||||
if (ImGui::Button(enabled && freecam ? "Stop flying" : "Start flying", ImVec2(-FLT_MIN, 0))) {
|
||||
// The game's interface is hidden for the flight (the `hideui` drone setting) and only
|
||||
// brought back if the drone was what hid it.
|
||||
static bool hid_ui = false;
|
||||
if (enabled && freecam) {
|
||||
run("dronemode 0");
|
||||
run("freecam 0"); // back to the skater
|
||||
if (hid_ui) run("hideui 0");
|
||||
hid_ui = false;
|
||||
} else {
|
||||
if (!freecam) run("freecam 1");
|
||||
if (!enabled) run("dronemode 1");
|
||||
if (get(Knob::kHideUi) >= 0.5f && !s.model.debug.game_ui_hidden) { run("hideui 1"); hid_ui = true; }
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (!s.model.debug.camera_available) ImGui::TextDisabled("Load into a level to fly.");
|
||||
bool radio = radio_on;
|
||||
if (ImGui::Checkbox("Fly with a controller (RC radio or gamepad)", &radio)) run(radio ? "droneradio 1" : "droneradio 0");
|
||||
if (!radio_on && drone::radio::poll().valid)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "A controller is connected. Tick the box above to fly with it, or the game "
|
||||
"keeps using it for your skater.");
|
||||
ImGui::Separator();
|
||||
preset_picker();
|
||||
{ // What the pilot's skater wears over their eyes while flying.
|
||||
const int choice = std::clamp(static_cast<int>(std::lround(get(Knob::kGoggles))), 0, static_cast<int>(drone::outfit::eyewear_count));
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::BeginCombo("Goggles", choice ? drone::outfit::eyewear[choice - 1].label : "None")) {
|
||||
if (ImGui::Selectable("None", choice == 0)) { set(Knob::kGoggles, 0.0f); save(Knob::kGoggles); }
|
||||
for (std::size_t i = 0; i < drone::outfit::eyewear_count; ++i)
|
||||
if (ImGui::Selectable(drone::outfit::eyewear[i].label, choice == static_cast<int>(i) + 1)) {
|
||||
set(Knob::kGoggles, static_cast<float>(i + 1));
|
||||
save(Knob::kGoggles);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Your skater wears these while you fly, and other players see them. Your own eyewear comes back when you land.");
|
||||
}
|
||||
toggle(Knob::kHideUi, "Hide the game's interface while flying");
|
||||
ImGui::Separator();
|
||||
const bool armed = drone::armed();
|
||||
ImGui::Text("Motors: %s", armed ? "ARMED" : "DISARMED");
|
||||
const float half = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2.0f) / 3.0f;
|
||||
if (ImGui::Button(armed ? "Disarm" : "Arm", ImVec2(half, 0))) drone::set_armed(!armed);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Back to takeoff", ImVec2(half, 0))) drone::request_reset();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Puts the drone back where it took off, with a fresh battery and timer.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset settings", ImVec2(half, 0))) run("dronetune reset");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Puts every drone setting back to its default.");
|
||||
ImGui::Separator();
|
||||
if (drone::flying()) {
|
||||
const auto& t = drone::telemetry();
|
||||
ImGui::Text("Throttle %3.0f%% Speed %.0f km/h Altitude %.1f m Time %.0f s",
|
||||
static_cast<double>(t.throttle.load() * 100.0f), static_cast<double>(t.speed.load() * 3.6f),
|
||||
static_cast<double>(t.altitude.load()), static_cast<double>(t.seconds.load()));
|
||||
} else {
|
||||
ImGui::TextDisabled("Not flying.");
|
||||
}
|
||||
// The game's value comes back every frame; hold the one being set until the game has it, or
|
||||
// the slider would snap back while it is dragged.
|
||||
static float pending = -1.0f;
|
||||
static double sent_at = 0.0;
|
||||
if (pending >= 0.0f && (std::abs(s.model.debug.camera_speed - pending) < 0.01f ||
|
||||
(sent_at > 0.0 && ImGui::GetTime() - sent_at > 1.0))) { pending = -1.0f; sent_at = 0.0; }
|
||||
float top = pending >= 0.0f ? pending : s.model.debug.camera_speed;
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::SliderFloat("Top speed", &top, 0.6f, 300.0f, "%.1f", ImGuiSliderFlags_Logarithmic)) pending = top;
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) { run(std::format("flyspeed {}", top)); sent_at = ImGui::GetTime(); }
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("The same setting as freecam's fly speed.\n\nConsole: flyspeed");
|
||||
// The flight model caps speed at four times the fly speed (m/s), twice that with boost.
|
||||
ImGui::TextDisabled("Up to about %.0f km/h (twice that with boost).", static_cast<double>(top * 4.0f * 3.6f));
|
||||
}
|
||||
|
||||
void tab_handling() {
|
||||
slider(Knob::kGravity, "Gravity", "%.1f");
|
||||
slider(Knob::kHover, "Hover throttle (lower = more power)");
|
||||
slider(Knob::kDrag, "Air drag");
|
||||
slider(Knob::kUptilt, "Camera tilt (degrees)", "%.0f");
|
||||
ImGui::Separator();
|
||||
toggle(Knob::kCollide, "Collide with the world");
|
||||
slider(Knob::kRadius, "Collision size");
|
||||
slider(Knob::kFloor, "Invisible floor (with collisions off)", "%.1f");
|
||||
toggle(Knob::kDisarmFreeze, "Hang in the air when disarmed");
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Range");
|
||||
bool limited = get(Knob::kRange) > 0.0f;
|
||||
if (ImGui::Checkbox("Limited range", &limited)) { set(Knob::kRange, limited ? 300.0f : 0.0f); save(Knob::kRange); }
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Like a real radio and video link: the picture breaks up as you near the limit, and\n"
|
||||
"further out the motors cut (failsafe) until you reset. Off = fly anywhere on the map.");
|
||||
if (limited) {
|
||||
float metres = get(Knob::kRange);
|
||||
ImGui::SetNextItemWidth(-ImGui::GetFontSize() * 9.0f);
|
||||
if (ImGui::SliderFloat("Range (metres)", &metres, 50.0f, 5000.0f, "%.0f", ImGuiSliderFlags_Logarithmic)) set(Knob::kRange, metres);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) save(Knob::kRange);
|
||||
ImGui::TextWrapped("Measured from where you took off. The picture starts to break up at %.0f m and is lost at %.0f m. "
|
||||
"Stay without a picture for %.0f seconds, or go past %.0f m, and the motors cut.",
|
||||
static_cast<double>(get(Knob::kRange) * drone::range_clean), static_cast<double>(get(Knob::kRange)),
|
||||
static_cast<double>(drone::failsafe_after_seconds),
|
||||
static_cast<double>(get(Knob::kRange) * drone::range_failsafe));
|
||||
}
|
||||
}
|
||||
|
||||
void rate_row(const char* axis, Knob centre, Knob max, Knob expo) {
|
||||
ImGui::PushID(axis);
|
||||
ImGui::TextUnformatted(axis);
|
||||
slider(centre, "Centre sensitivity", "%.0f");
|
||||
slider(max, "Max rate", "%.0f");
|
||||
slider(expo, "Expo");
|
||||
ImGui::PopID();
|
||||
}
|
||||
void tab_rates() {
|
||||
ImGui::TextWrapped("How fast the drone turns, set like Betaflight's \"Actual\" rates. Centre: the speed near the middle of "
|
||||
"the stick. Max: the speed at full stick. Expo: how much softer the middle is.");
|
||||
rate_row("Roll", Knob::kRollCenter, Knob::kRollMax, Knob::kRollExpo);
|
||||
ImGui::Separator();
|
||||
rate_row("Pitch", Knob::kPitchCenter, Knob::kPitchMax, Knob::kPitchExpo);
|
||||
ImGui::Separator();
|
||||
rate_row("Yaw", Knob::kYawCenter, Knob::kYawMax, Knob::kYawExpo);
|
||||
}
|
||||
|
||||
void tab_display() {
|
||||
toggle(Knob::kOsd, "Show the goggle display (OSD)");
|
||||
static const char* const colours[] = {"White", "Green", "Yellow", "Cyan", "Orange", "Pink"};
|
||||
static const char* const fonts[] = {"Pixel (goggles)", "Smooth"};
|
||||
slider(Knob::kOsdScale, "Text size");
|
||||
choice(Knob::kOsdColor, "Colour", colours, 6);
|
||||
choice(Knob::kOsdFont, "Font", fonts, 2);
|
||||
slider(Knob::kOsdMargin, "Distance from the edge");
|
||||
toggle(Knob::kCrosshair, "Crosshair");
|
||||
ImGui::TextDisabled("Show:");
|
||||
toggle(Knob::kShowArmed, "Armed / disarmed"); ImGui::SameLine();
|
||||
toggle(Knob::kShowBattery, "Battery");
|
||||
toggle(Knob::kShowTimer, "Timer"); ImGui::SameLine();
|
||||
toggle(Knob::kShowThrottle, "Throttle"); ImGui::SameLine();
|
||||
toggle(Knob::kShowAlt, "Altitude"); ImGui::SameLine();
|
||||
toggle(Knob::kShowSpeed, "Speed");
|
||||
ImGui::Separator();
|
||||
toggle(Knob::kSticks, "Show my sticks on screen");
|
||||
slider(Knob::kStickSize, "On-screen sticks size");
|
||||
ImGui::Separator();
|
||||
toggle(Knob::kAnalog, "Analogue video look");
|
||||
slider(Knob::kAnalogNoise, "Static");
|
||||
slider(Knob::kAnalogScan, "Scanlines");
|
||||
slider(Knob::kAnalogVignette, "Dark corners");
|
||||
slider(Knob::kAnalogGlitch, "Rolling glitches");
|
||||
ImGui::Separator();
|
||||
slider(Knob::kVolume, "Motor sound volume");
|
||||
slider(Knob::kMotorPitch, "Motor sound pitch");
|
||||
}
|
||||
|
||||
const char* axis_text(int signed_axis, char* buffer, std::size_t size) {
|
||||
if (signed_axis == 0) std::snprintf(buffer, size, "none");
|
||||
else std::snprintf(buffer, size, "axis %d%s", std::abs(signed_axis), signed_axis < 0 ? " (reversed)" : "");
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Live bars for the six axes; `highlight` (0-5) is drawn as the one being picked.
|
||||
void axis_bars(const drone::radio::Sample& sample, int highlight) {
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const auto v = sample.axes[static_cast<std::size_t>(i)];
|
||||
char value[16];
|
||||
std::snprintf(value, sizeof value, "%+.2f", static_cast<double>(v));
|
||||
const bool lit = i == highlight;
|
||||
if (lit) ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(120, 220, 120, 255));
|
||||
ImGui::ProgressBar((v + 1.0f) * 0.5f, ImVec2(ImGui::GetContentRegionAvail().x * 0.7f, 0), value);
|
||||
if (lit) ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Axis %d%s", i + 1, lit ? " <" : "");
|
||||
}
|
||||
}
|
||||
|
||||
// The guided setup (drone_radio.h: Calibration). Saves through the same commands as the manual controls.
|
||||
void setup_wizard(const drone::radio::Sample& sample) {
|
||||
using Step = drone::radio::Calibration::Step;
|
||||
auto& c = drone::radio::calibration();
|
||||
if (sample.valid) c.update(sample.axes, sample.buttons, ImGui::GetIO().DeltaTime);
|
||||
const auto step = c.step();
|
||||
if (c.active()) drone::radio::mark_calibrating();
|
||||
const char* title = "";
|
||||
const char* body = "";
|
||||
switch (step) {
|
||||
case Step::rest:
|
||||
title = "Step 1 of 7: sticks at rest";
|
||||
body = "Let go of both sticks so they centre, and put the throttle all the way down (a gamepad's sticks just "
|
||||
"centre). Then press Ready.";
|
||||
break;
|
||||
case Step::throttle:
|
||||
title = "Step 2 of 7: throttle";
|
||||
body = c.waiting_for_return() ? "Got it. Now let the throttle back down."
|
||||
: "Push the throttle all the way up and hold it there. On a gamepad, that's the left stick, up.";
|
||||
break;
|
||||
case Step::yaw:
|
||||
title = "Step 3 of 7: yaw";
|
||||
body = c.waiting_for_return() ? "Got it. Let the stick go." : "Push the yaw stick (left stick, sideways) fully right and hold it.";
|
||||
break;
|
||||
case Step::pitch:
|
||||
title = "Step 4 of 7: pitch";
|
||||
body = c.waiting_for_return() ? "Got it. Let the stick go." : "Push the right stick fully forward (away from you) and hold it.";
|
||||
break;
|
||||
case Step::roll:
|
||||
title = "Step 5 of 7: roll";
|
||||
body = c.waiting_for_return() ? "Got it. Let the stick go." : "Push the right stick fully right and hold it.";
|
||||
break;
|
||||
case Step::arm:
|
||||
title = "Step 6 of 7: arm switch (optional)";
|
||||
body = "Flip the switch you want to arm the motors with to its ARMED position. No arm switch? Press Skip.";
|
||||
break;
|
||||
case Step::reset:
|
||||
title = "Step 7 of 7: reset switch (optional)";
|
||||
body = c.waiting_for_return() ? "Got it. Let the switch go back."
|
||||
: "Flick the switch or press the button you want to put the drone back at its start. No reset switch? Press Skip.";
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (step == Step::done) {
|
||||
// Save once, then show what was found.
|
||||
static bool saved = false;
|
||||
if (!saved) {
|
||||
const auto m = c.mapping();
|
||||
run(std::format("droneaxes {} {} {} {}", m[0], m[1], m[2], m[3]));
|
||||
set(Knob::kArmAxis, static_cast<float>(c.arm_axis())); save(Knob::kArmAxis);
|
||||
set(Knob::kArmButton, static_cast<float>(c.arm_button())); save(Knob::kArmButton);
|
||||
set(Knob::kResetAxis, static_cast<float>(c.reset_axis())); save(Knob::kResetAxis);
|
||||
set(Knob::kResetButton, static_cast<float>(c.reset_button())); save(Knob::kResetButton);
|
||||
set(Knob::kThrottleCentre, c.throttle_centred() ? 1.0f : 0.0f); save(Knob::kThrottleCentre);
|
||||
// Gamepad sticks drift more than a radio's gimbals.
|
||||
if (c.throttle_centred() && get(Knob::kDeadzone) < 0.12f) { set(Knob::kDeadzone, 0.12f); save(Knob::kDeadzone); }
|
||||
// Setting a controller up means flying with it.
|
||||
if (!drone::radio::config().enabled.load()) run("droneradio 1");
|
||||
saved = true;
|
||||
}
|
||||
ImGui::TextUnformatted("Setup saved. The drone now flies with this controller.");
|
||||
const auto m = c.mapping();
|
||||
static const char* const names[4] = {"Roll", "Pitch", "Throttle", "Yaw"};
|
||||
char buffer[32];
|
||||
for (int i = 0; i < 4; ++i) ImGui::BulletText("%s: %s", names[i], axis_text(m[static_cast<std::size_t>(i)], buffer, sizeof buffer));
|
||||
const auto switch_text = [&buffer](int axis, int button) -> std::string {
|
||||
if (button > 0) return std::format("button {}", button);
|
||||
return axis_text(axis, buffer, sizeof buffer);
|
||||
};
|
||||
ImGui::BulletText("Arm switch: %s", switch_text(c.arm_axis(), c.arm_button()).c_str());
|
||||
ImGui::BulletText("Reset switch: %s", switch_text(c.reset_axis(), c.reset_button()).c_str());
|
||||
if (c.throttle_centred())
|
||||
ImGui::TextWrapped("Your throttle springs back to the middle, so the middle now holds the drone up: push up to "
|
||||
"climb, down to descend.");
|
||||
if (ImGui::Button("Done", ImVec2(-FLT_MIN, 0))) { c.cancel(); saved = false; }
|
||||
return;
|
||||
}
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::TextWrapped("%s", body);
|
||||
if (!sample.valid) ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f), "The controller is not connected.");
|
||||
else axis_bars(sample, c.waiting_for_return() ? -1 : c.candidate());
|
||||
const float third = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2.0f) / 3.0f;
|
||||
if (step == Step::rest) {
|
||||
ImGui::BeginDisabled(!sample.valid);
|
||||
if (ImGui::Button("Ready", ImVec2(third, 0))) c.capture_rest(sample.axes, sample.buttons);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
} else if ((step == Step::arm || step == Step::reset) && !c.waiting_for_return()) {
|
||||
if (ImGui::Button("Skip", ImVec2(third, 0))) c.skip();
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (ImGui::Button("Cancel", ImVec2(third, 0))) c.cancel();
|
||||
}
|
||||
|
||||
void tab_radio() {
|
||||
const auto sample = drone::radio::poll();
|
||||
auto& c = drone::radio::calibration();
|
||||
if (c.step() != drone::radio::Calibration::Step::idle) {
|
||||
setup_wizard(sample);
|
||||
return;
|
||||
}
|
||||
if (!sample.valid) {
|
||||
ImGui::TextWrapped("No controller found. Plug in a gamepad, or an RC radio with its USB mode set to Joystick (on "
|
||||
"EdgeTX radios, pick \"USB Joystick (HID)\" when you plug it in).");
|
||||
} else {
|
||||
ImGui::Text("Controller: %s", sample.name.c_str());
|
||||
ImGui::BeginDisabled(!sample.valid);
|
||||
if (ImGui::Button("Set up controller", ImVec2(-FLT_MIN, 0))) c.start();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::TextDisabled("Moves you through each stick and switch and saves which is which. The drone holds still meanwhile.");
|
||||
}
|
||||
if (!ImGui::CollapsingHeader("Manual setup")) return;
|
||||
if (sample.valid) {
|
||||
axis_bars(sample, -1);
|
||||
std::string pressed;
|
||||
for (int n = 1; n <= 32; ++n)
|
||||
if (sample.buttons & (1u << (n - 1))) pressed += (pressed.empty() ? "" : " ") + std::to_string(n);
|
||||
ImGui::Text("Buttons pressed: %s", pressed.empty() ? "none" : pressed.c_str());
|
||||
ImGui::TextDisabled("Move a stick or flip a switch to see which axis or button it is.");
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Which axis each stick uses");
|
||||
static const char* const names[4] = {"Roll", "Pitch", "Throttle", "Yaw"};
|
||||
static const char* const axes[] = {"Axis 1", "Axis 2", "Axis 3", "Axis 4", "Axis 5", "Axis 6"};
|
||||
auto map = drone::radio::mapping_snapshot();
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ImGui::PushID(i);
|
||||
int axis = std::clamp(std::abs(map[static_cast<std::size_t>(i)]), 1, 6) - 1;
|
||||
bool flip = map[static_cast<std::size_t>(i)] < 0;
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 7.0f);
|
||||
if (ImGui::Combo(names[i], &axis, axes, 6)) changed = true;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Reverse", &flip)) changed = true;
|
||||
map[static_cast<std::size_t>(i)] = (axis + 1) * (flip ? -1 : 1);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (changed) run(std::format("droneaxes {} {} {} {}", map[0], map[1], map[2], map[3]));
|
||||
toggle(Knob::kThrottleCentre, "Throttle springs back to the middle (gamepads)");
|
||||
slider(Knob::kDeadzone, "Stick dead zone");
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Switches (use an axis or button no stick uses; 0 = none)");
|
||||
integer(Knob::kArmAxis, "Arm switch axis");
|
||||
integer(Knob::kArmButton, "Arm button");
|
||||
integer(Knob::kResetAxis, "Reset switch axis");
|
||||
integer(Knob::kResetButton, "Reset button");
|
||||
}
|
||||
|
||||
void tab_lobby() {
|
||||
if (!drone::beacon::server_allows().load()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "This server has drones turned off.");
|
||||
ImGui::TextWrapped("You can still fly, but nobody here sees your drone and you don't see theirs.");
|
||||
ImGui::Separator();
|
||||
}
|
||||
ImGui::TextWrapped("Other players only see drones if they are running this version of ReSkate.");
|
||||
toggle(Knob::kBeacon, "Let other players see my drone");
|
||||
toggle(Knob::kShowBeacons, "Show other players' drones");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void draw_drone_window() {
|
||||
if (!drone::menu_flag().load()) return;
|
||||
auto& s = state();
|
||||
const float k = scale();
|
||||
const auto P = [k](float value) { return value * k; };
|
||||
auto& io = ImGui::GetIO();
|
||||
const auto restore_font_scale = io.FontGlobalScale;
|
||||
io.FontGlobalScale = k;
|
||||
const auto* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x + 16, viewport->WorkPos.y + 16), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(P(520), std::min(P(640), viewport->WorkSize.y - 32)), ImGuiCond_FirstUseEver);
|
||||
ImGui::PushFont(s.menu.body);
|
||||
const int colours = skate::push_widget_colours();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
|
||||
bool open = true;
|
||||
if (ImGui::Begin("FPV drone###ReSkateDrone", &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (ImGui::BeginTabBar("##drone_tabs")) {
|
||||
if (ImGui::BeginTabItem("Fly")) { tab_fly(); ImGui::EndTabItem(); }
|
||||
if (ImGui::BeginTabItem("Handling")) { tab_handling(); ImGui::EndTabItem(); }
|
||||
if (ImGui::BeginTabItem("Rates")) { tab_rates(); ImGui::EndTabItem(); }
|
||||
if (ImGui::BeginTabItem("Display")) { tab_display(); ImGui::EndTabItem(); }
|
||||
if (ImGui::BeginTabItem("Controller")) { tab_radio(); ImGui::EndTabItem(); }
|
||||
if (ImGui::BeginTabItem("Lobby")) { tab_lobby(); ImGui::EndTabItem(); }
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(colours);
|
||||
ImGui::PopFont();
|
||||
io.FontGlobalScale = restore_font_scale;
|
||||
if (!open) drone::menu_flag().store(false);
|
||||
}
|
||||
} // namespace dingosdk::overlay::detail
|
||||
@@ -0,0 +1,463 @@
|
||||
#include "skate_menu_internal.h"
|
||||
#include "overlay_internal.h"
|
||||
#include "Extension/Skater/drone_flight.h"
|
||||
#include "Extension/Skater/drone_beacon.h"
|
||||
#include "Extension/Skater/drone_outfit.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
// The DRONE page of the ReSkate menu: flying, handling, rates, the display, the controller and
|
||||
// multiplayer. Every change takes effect at once and is saved by running the same `dronetune`
|
||||
// command a player could type, so the console and this page always agree.
|
||||
|
||||
namespace dingosdk::overlay::menu {
|
||||
namespace {
|
||||
namespace drone = dingosdk::drone;
|
||||
using drone::Knob;
|
||||
|
||||
// Runs a console command on the game's thread, quietly: a slider's save is not news.
|
||||
void run(const CallbacksV3& callbacks, const std::string& command) {
|
||||
if (!callbacks.queue_console_command) return;
|
||||
std::array<char, 256> result{};
|
||||
callbacks.queue_console_command(callbacks.user, command.c_str(), result.data(), result.size());
|
||||
}
|
||||
|
||||
float get(Knob k) { return drone::knobs().value[k].load(std::memory_order_relaxed); }
|
||||
void set(Knob k, float v) { drone::knobs().value[k].store(v, std::memory_order_relaxed); }
|
||||
// Saves a setting by running `dronetune`, which also clamps it and keeps it for next time.
|
||||
void save(const CallbacksV3& callbacks, Knob k) { run(callbacks, std::format("dronetune {} {}", drone::knob_info[k].name, get(k))); }
|
||||
std::string tip(Knob k) {
|
||||
return std::format("{}.\n\nConsole: dronetune {} (default {:g})", drone::knob_info[k].help, drone::knob_info[k].name,
|
||||
drone::knob_info[k].def);
|
||||
}
|
||||
|
||||
void slider(SkateMenu& menu, const CallbacksV3& callbacks, Knob k, const char* label, const char* format = "%.2f") {
|
||||
const auto help = tip(k);
|
||||
field(menu, label, help.c_str());
|
||||
float v = get(k);
|
||||
ImGui::PushID(static_cast<int>(k));
|
||||
if (ImGui::SliderFloat("##value", &v, drone::knob_info[k].lo, drone::knob_info[k].hi, format)) set(k, v);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) save(callbacks, k);
|
||||
ImGui::PopID();
|
||||
}
|
||||
void integer(SkateMenu& menu, const CallbacksV3& callbacks, Knob k, const char* label) {
|
||||
const auto help = tip(k);
|
||||
field(menu, label, help.c_str());
|
||||
int v = static_cast<int>(std::lround(get(k)));
|
||||
ImGui::PushID(static_cast<int>(k));
|
||||
if (ImGui::SliderInt("##value", &v, static_cast<int>(drone::knob_info[k].lo), static_cast<int>(drone::knob_info[k].hi)))
|
||||
set(k, static_cast<float>(v));
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) save(callbacks, k);
|
||||
ImGui::PopID();
|
||||
}
|
||||
void toggle(SkateMenu& menu, const CallbacksV3& callbacks, Knob k, const char* label) {
|
||||
const auto help = tip(k);
|
||||
bool on = get(k) >= 0.5f;
|
||||
if (toggle_row(menu, label, help.c_str(), on)) { set(k, on ? 1.0f : 0.0f); save(callbacks, k); }
|
||||
}
|
||||
void options(SkateMenu& menu, const CallbacksV3& callbacks, Knob k, const char* label, std::initializer_list<const char*> names) {
|
||||
const auto help = tip(k);
|
||||
field(menu, label, help.c_str());
|
||||
int v = std::clamp(static_cast<int>(std::lround(get(k))), 0, static_cast<int>(names.size()) - 1);
|
||||
if (choice(menu, drone::knob_info[k].name, v, names)) { set(k, static_cast<float>(v)); save(callbacks, k); }
|
||||
}
|
||||
|
||||
// ---- FLY
|
||||
void fly_tab(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
|
||||
const bool enabled = drone::is_enabled();
|
||||
const bool radio_on = drone::radio::config().enabled.load();
|
||||
const bool freecam = model.debug.free_camera;
|
||||
const bool flying = enabled && freecam;
|
||||
|
||||
begin_card(menu, "drone-fly", "FLY");
|
||||
// The game's interface is hidden for the flight (the `hideui` drone setting) and only
|
||||
// brought back if the drone was what hid it.
|
||||
static bool hid_ui = false;
|
||||
if (primary_button(menu, flying ? "STOP FLYING" : "START FLYING", model.debug.camera_available)) {
|
||||
if (flying) {
|
||||
run(callbacks, "dronemode 0");
|
||||
run(callbacks, "freecam 0"); // back to the skater
|
||||
if (hid_ui) run(callbacks, "hideui 0");
|
||||
hid_ui = false;
|
||||
} else {
|
||||
if (!freecam) run(callbacks, "freecam 1");
|
||||
if (!enabled) run(callbacks, "dronemode 1");
|
||||
if (get(Knob::kHideUi) >= 0.5f && !model.debug.game_ui_hidden) { run(callbacks, "hideui 1"); hid_ui = true; }
|
||||
}
|
||||
}
|
||||
if (!model.debug.camera_available) warn("Load into a level to fly.");
|
||||
else if (flying) note(radio_on ? "Flying with the controller. Close the menu to fly." : "Flying with the keyboard and mouse. Close the menu to fly.");
|
||||
else if (enabled) note("Drone mode is on, but freecam is off: press Start flying.");
|
||||
else note("Keyboard: W/S tilt forward/back, A/D roll, E/Q throttle up/down, mouse turns, Shift boosts.");
|
||||
bool radio = radio_on;
|
||||
if (toggle_row(menu, "Fly with a controller", "An RC radio in USB Joystick mode, or a gamepad. Set it up on the CONTROLLER tab.", radio))
|
||||
run(callbacks, radio ? "droneradio 1" : "droneradio 0");
|
||||
if (!radio_on && drone::radio::poll().valid)
|
||||
warn("A controller is connected. Turn on Fly with a controller to use it, or the game keeps using it for your skater.");
|
||||
toggle(menu, callbacks, Knob::kHideUi, "Hide the game's interface while flying");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-quad", "DRONE");
|
||||
{ // Presets (drone_flight.h): a whole drone in one choice. "Custom" once anything is changed.
|
||||
const int current = drone::matching_preset(model.debug.camera_speed);
|
||||
field(menu, "Preset", "A whole drone in one choice: handling, rates, size, camera tilt, motor sound and top speed.");
|
||||
if (ImGui::BeginCombo("##preset", current >= 0 ? drone::presets[current].name : "Custom")) {
|
||||
for (std::size_t p = 0; p < drone::preset_count; ++p) {
|
||||
const auto& preset = drone::presets[p];
|
||||
if (ImGui::Selectable(preset.name, static_cast<int>(p) == current)) {
|
||||
for (const auto& v : preset.values) { set(v.knob, v.value); save(callbacks, v.knob); }
|
||||
run(callbacks, std::format("flyspeed {}", preset.top_speed));
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", preset.description);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
note(current >= 0 ? drone::presets[current].description : "Your own settings. Pick a preset to start from one of them.");
|
||||
}
|
||||
{ // What the pilot's skater wears over their eyes while flying.
|
||||
const int worn = std::clamp(static_cast<int>(std::lround(get(Knob::kGoggles))), 0, static_cast<int>(drone::outfit::eyewear_count));
|
||||
field(menu, "Goggles", "Your skater wears these while you fly. Your own eyewear comes back when you land.");
|
||||
if (ImGui::BeginCombo("##goggles", worn ? drone::outfit::eyewear[worn - 1].label : "None")) {
|
||||
if (ImGui::Selectable("None", worn == 0)) { set(Knob::kGoggles, 0.0f); save(callbacks, Knob::kGoggles); }
|
||||
for (std::size_t i = 0; i < drone::outfit::eyewear_count; ++i)
|
||||
if (ImGui::Selectable(drone::outfit::eyewear[i].label, worn == static_cast<int>(i) + 1)) {
|
||||
set(Knob::kGoggles, static_cast<float>(i + 1));
|
||||
save(callbacks, Knob::kGoggles);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
{ // The game's value comes back every frame; hold the one being set until the game has it,
|
||||
// or the slider would snap back while it is dragged.
|
||||
static float pending = -1.0f;
|
||||
static double sent_at = 0.0;
|
||||
if (pending >= 0.0f && (std::abs(model.debug.camera_speed - pending) < 0.01f ||
|
||||
(sent_at > 0.0 && ImGui::GetTime() - sent_at > 1.0))) { pending = -1.0f; sent_at = 0.0; }
|
||||
float top = pending >= 0.0f ? pending : model.debug.camera_speed;
|
||||
field(menu, "Top speed", "The same setting as freecam's flight speed.\n\nConsole: flyspeed");
|
||||
if (ImGui::SliderFloat("##top-speed", &top, 0.6f, 300.0f, "%.1f", ImGuiSliderFlags_Logarithmic)) pending = top;
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) { run(callbacks, std::format("flyspeed {}", top)); sent_at = ImGui::GetTime(); }
|
||||
// The flight model caps speed at four times the fly speed (m/s), twice that with boost.
|
||||
const auto speed = std::format("Up to about {:.0f} km/h (twice that with boost).", top * 4.0f * 3.6f);
|
||||
note(speed.c_str());
|
||||
}
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-motors", "MOTORS");
|
||||
const bool armed = drone::armed();
|
||||
info(menu, "Motors", armed ? "ARMED" : "DISARMED");
|
||||
if (drone::flying()) {
|
||||
const auto& t = drone::telemetry();
|
||||
info(menu, "Flight", std::format("{:.0f}% throttle, {:.0f} km/h, {:.1f} m up, {:.0f} s", t.throttle.load() * 100.0f,
|
||||
t.speed.load() * 3.6f, t.altitude.load(), t.seconds.load()));
|
||||
}
|
||||
const float third = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2.0f) / 3.0f;
|
||||
if (ImGui::Button(armed ? "Disarm" : "Arm", ImVec2(third, 0))) drone::set_armed(!armed);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Back to takeoff", ImVec2(third, 0))) drone::request_reset();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Puts the drone back where it took off, with a fresh battery and timer.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset settings", ImVec2(third, 0))) run(callbacks, "dronetune reset");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Puts every drone setting back to its default.");
|
||||
end_card();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 170, 170, 255));
|
||||
ImGui::TextUnformatted("Created by neekgate for ReSkate");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// ---- HANDLING
|
||||
void handling_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
|
||||
begin_card(menu, "drone-flight", "FLIGHT");
|
||||
slider(menu, callbacks, Knob::kGravity, "Gravity", "%.1f");
|
||||
slider(menu, callbacks, Knob::kHover, "Hover throttle");
|
||||
slider(menu, callbacks, Knob::kDrag, "Air drag");
|
||||
slider(menu, callbacks, Knob::kUptilt, "Camera tilt", "%.0f degrees");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-collisions", "COLLISIONS");
|
||||
toggle(menu, callbacks, Knob::kCollide, "Collide with the world");
|
||||
slider(menu, callbacks, Knob::kRadius, "Collision size", "%.2f m");
|
||||
slider(menu, callbacks, Knob::kFloor, "Invisible floor", "%.1f m");
|
||||
toggle(menu, callbacks, Knob::kDisarmFreeze, "Hang in the air when disarmed");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-range", "RANGE");
|
||||
bool limited = get(Knob::kRange) > 0.0f;
|
||||
if (toggle_row(menu, "Limited range",
|
||||
"Like a real radio and video link: the picture breaks up as you near the limit, and further out the motors cut "
|
||||
"(failsafe) until you reset. Off: fly anywhere on the map.", limited)) {
|
||||
set(Knob::kRange, limited ? 300.0f : 0.0f);
|
||||
save(callbacks, Knob::kRange);
|
||||
}
|
||||
if (limited) {
|
||||
field(menu, "Range");
|
||||
float metres = get(Knob::kRange);
|
||||
if (ImGui::SliderFloat("##range", &metres, 50.0f, 5000.0f, "%.0f m", ImGuiSliderFlags_Logarithmic)) set(Knob::kRange, metres);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) save(callbacks, Knob::kRange);
|
||||
const auto explained = std::format(
|
||||
"Measured from where you took off. The picture starts to break up at {:.0f} m and is lost at {:.0f} m. Stay without a "
|
||||
"picture for {:.0f} seconds, or go past {:.0f} m, and the motors cut.",
|
||||
get(Knob::kRange) * drone::range_clean, get(Knob::kRange), drone::failsafe_after_seconds,
|
||||
get(Knob::kRange) * drone::range_failsafe);
|
||||
note(explained.c_str());
|
||||
} else {
|
||||
note("Unlimited: fly anywhere on the map.");
|
||||
}
|
||||
end_card();
|
||||
}
|
||||
|
||||
// ---- RATES
|
||||
void rates_card(SkateMenu& menu, const CallbacksV3& callbacks, const char* id, const char* title, Knob centre, Knob max, Knob expo) {
|
||||
begin_card(menu, id, title);
|
||||
slider(menu, callbacks, centre, "Centre sensitivity", "%.0f deg/s");
|
||||
slider(menu, callbacks, max, "Max rate", "%.0f deg/s");
|
||||
slider(menu, callbacks, expo, "Expo");
|
||||
end_card();
|
||||
}
|
||||
void rates_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
|
||||
note("How fast the drone turns, set like Betaflight's \"Actual\" rates. Centre: the speed near the middle of the stick. "
|
||||
"Max: the speed at full stick. Expo: how much softer the middle is.");
|
||||
rates_card(menu, callbacks, "drone-roll", "ROLL", Knob::kRollCenter, Knob::kRollMax, Knob::kRollExpo);
|
||||
rates_card(menu, callbacks, "drone-pitch", "PITCH", Knob::kPitchCenter, Knob::kPitchMax, Knob::kPitchExpo);
|
||||
rates_card(menu, callbacks, "drone-yaw", "YAW", Knob::kYawCenter, Knob::kYawMax, Knob::kYawExpo);
|
||||
}
|
||||
|
||||
// ---- DISPLAY
|
||||
void display_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
|
||||
begin_card(menu, "drone-osd", "GOGGLE DISPLAY", "the OSD");
|
||||
toggle(menu, callbacks, Knob::kOsd, "Show the goggle display");
|
||||
slider(menu, callbacks, Knob::kOsdScale, "Text size");
|
||||
options(menu, callbacks, Knob::kOsdColor, "Colour", {"White", "Green", "Yellow", "Cyan", "Orange", "Pink"});
|
||||
options(menu, callbacks, Knob::kOsdFont, "Font", {"Pixel", "Smooth"});
|
||||
slider(menu, callbacks, Knob::kOsdMargin, "Distance from the edge");
|
||||
toggle(menu, callbacks, Knob::kCrosshair, "Crosshair");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-osd-show", "SHOW");
|
||||
toggle(menu, callbacks, Knob::kShowArmed, "Armed / disarmed");
|
||||
toggle(menu, callbacks, Knob::kShowBattery, "Battery");
|
||||
toggle(menu, callbacks, Knob::kShowTimer, "Timer");
|
||||
toggle(menu, callbacks, Knob::kShowThrottle, "Throttle");
|
||||
toggle(menu, callbacks, Knob::kShowAlt, "Altitude");
|
||||
toggle(menu, callbacks, Knob::kShowSpeed, "Speed");
|
||||
toggle(menu, callbacks, Knob::kSticks, "My sticks on screen");
|
||||
slider(menu, callbacks, Knob::kStickSize, "Sticks size");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-analog", "ANALOGUE VIDEO");
|
||||
toggle(menu, callbacks, Knob::kAnalog, "Analogue video look");
|
||||
slider(menu, callbacks, Knob::kAnalogNoise, "Static");
|
||||
slider(menu, callbacks, Knob::kAnalogScan, "Scanlines");
|
||||
slider(menu, callbacks, Knob::kAnalogVignette, "Dark corners");
|
||||
slider(menu, callbacks, Knob::kAnalogGlitch, "Rolling glitches");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-sound", "SOUND");
|
||||
slider(menu, callbacks, Knob::kVolume, "Motor volume");
|
||||
slider(menu, callbacks, Knob::kMotorPitch, "Motor pitch");
|
||||
end_card();
|
||||
}
|
||||
|
||||
// ---- CONTROLLER
|
||||
const char* axis_text(int signed_axis, char* buffer, std::size_t size) {
|
||||
if (signed_axis == 0) std::snprintf(buffer, size, "none");
|
||||
else std::snprintf(buffer, size, "axis %d%s", std::abs(signed_axis), signed_axis < 0 ? " (reversed)" : "");
|
||||
return buffer;
|
||||
}
|
||||
// Live bars for the six axes; `highlight` (0-5) is drawn as the one being picked.
|
||||
void axis_bars(const drone::radio::Sample& sample, int highlight) {
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const auto v = sample.axes[static_cast<std::size_t>(i)];
|
||||
char value[16];
|
||||
std::snprintf(value, sizeof value, "%+.2f", static_cast<double>(v));
|
||||
const bool lit = i == highlight;
|
||||
if (lit) ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(120, 220, 120, 255));
|
||||
ImGui::ProgressBar((v + 1.0f) * 0.5f, ImVec2(ImGui::GetContentRegionAvail().x * 0.75f, 0), value);
|
||||
if (lit) ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Axis %d%s", i + 1, lit ? " <" : "");
|
||||
}
|
||||
}
|
||||
// The guided setup (drone_radio.h: Calibration). Saves through the same commands as the manual controls.
|
||||
void setup_wizard(SkateMenu& menu, const CallbacksV3& callbacks, const drone::radio::Sample& sample) {
|
||||
using Step = drone::radio::Calibration::Step;
|
||||
auto& c = drone::radio::calibration();
|
||||
if (sample.valid) c.update(sample.axes, sample.buttons, ImGui::GetIO().DeltaTime);
|
||||
const auto step = c.step();
|
||||
if (c.active()) drone::radio::mark_calibrating();
|
||||
const char* title = "";
|
||||
const char* body = "";
|
||||
switch (step) {
|
||||
case Step::rest:
|
||||
title = "STEP 1 OF 7: STICKS AT REST";
|
||||
body = "Let go of both sticks so they centre, and put the throttle all the way down (a gamepad's sticks just centre). "
|
||||
"Then press Ready.";
|
||||
break;
|
||||
case Step::throttle:
|
||||
title = "STEP 2 OF 7: THROTTLE";
|
||||
body = c.waiting_for_return() ? "Got it. Now let the throttle back down."
|
||||
: "Push the throttle all the way up and hold it there. On a gamepad, that's the left stick, up.";
|
||||
break;
|
||||
case Step::yaw:
|
||||
title = "STEP 3 OF 7: YAW";
|
||||
body = c.waiting_for_return() ? "Got it. Let the stick go." : "Push the yaw stick (left stick, sideways) fully right and hold it.";
|
||||
break;
|
||||
case Step::pitch:
|
||||
title = "STEP 4 OF 7: PITCH";
|
||||
body = c.waiting_for_return() ? "Got it. Let the stick go." : "Push the right stick fully forward (away from you) and hold it.";
|
||||
break;
|
||||
case Step::roll:
|
||||
title = "STEP 5 OF 7: ROLL";
|
||||
body = c.waiting_for_return() ? "Got it. Let the stick go." : "Push the right stick fully right and hold it.";
|
||||
break;
|
||||
case Step::arm:
|
||||
title = "STEP 6 OF 7: ARM SWITCH (OPTIONAL)";
|
||||
body = "Flip the switch you want to arm the motors with to its ARMED position. No arm switch? Press Skip.";
|
||||
break;
|
||||
case Step::reset:
|
||||
title = "STEP 7 OF 7: RESET SWITCH (OPTIONAL)";
|
||||
body = c.waiting_for_return() ? "Got it. Let the switch go back."
|
||||
: "Flick the switch or press the button you want to put the drone back at its start. No reset switch? Press Skip.";
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (step == Step::done) {
|
||||
// Save once, then show what was found.
|
||||
static bool saved = false;
|
||||
if (!saved) {
|
||||
const auto m = c.mapping();
|
||||
run(callbacks, std::format("droneaxes {} {} {} {}", m[0], m[1], m[2], m[3]));
|
||||
set(Knob::kArmAxis, static_cast<float>(c.arm_axis())); save(callbacks, Knob::kArmAxis);
|
||||
set(Knob::kArmButton, static_cast<float>(c.arm_button())); save(callbacks, Knob::kArmButton);
|
||||
set(Knob::kResetAxis, static_cast<float>(c.reset_axis())); save(callbacks, Knob::kResetAxis);
|
||||
set(Knob::kResetButton, static_cast<float>(c.reset_button())); save(callbacks, Knob::kResetButton);
|
||||
set(Knob::kThrottleCentre, c.throttle_centred() ? 1.0f : 0.0f); save(callbacks, Knob::kThrottleCentre);
|
||||
// Gamepad sticks drift more than a radio's gimbals.
|
||||
if (c.throttle_centred() && get(Knob::kDeadzone) < 0.12f) { set(Knob::kDeadzone, 0.12f); save(callbacks, Knob::kDeadzone); }
|
||||
// Setting a controller up means flying with it.
|
||||
if (!drone::radio::config().enabled.load()) run(callbacks, "droneradio 1");
|
||||
saved = true;
|
||||
}
|
||||
begin_card(menu, "drone-setup-done", "SETUP SAVED", "the drone now flies with this controller");
|
||||
const auto m = c.mapping();
|
||||
static const char* const names[4] = {"Roll", "Pitch", "Throttle", "Yaw"};
|
||||
char buffer[32];
|
||||
for (int i = 0; i < 4; ++i) info(menu, names[i], axis_text(m[static_cast<std::size_t>(i)], buffer, sizeof buffer));
|
||||
const auto switch_text = [&buffer](int axis, int button) -> std::string {
|
||||
if (button > 0) return std::format("button {}", button);
|
||||
return axis_text(axis, buffer, sizeof buffer);
|
||||
};
|
||||
info(menu, "Arm switch", switch_text(c.arm_axis(), c.arm_button()));
|
||||
info(menu, "Reset switch", switch_text(c.reset_axis(), c.reset_button()));
|
||||
if (c.throttle_centred())
|
||||
note("Your throttle springs back to the middle, so the middle now holds the drone up: push up to climb, down to descend.");
|
||||
if (primary_button(menu, "DONE")) { c.cancel(); saved = false; }
|
||||
end_card();
|
||||
return;
|
||||
}
|
||||
begin_card(menu, "drone-setup", title);
|
||||
note(body);
|
||||
if (!sample.valid) warn("The controller is not connected.");
|
||||
else axis_bars(sample, c.waiting_for_return() ? -1 : c.candidate());
|
||||
const float third = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2.0f) / 3.0f;
|
||||
if (step == Step::rest) {
|
||||
ImGui::BeginDisabled(!sample.valid);
|
||||
if (ImGui::Button("Ready", ImVec2(third, 0))) c.capture_rest(sample.axes, sample.buttons);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
} else if ((step == Step::arm || step == Step::reset) && !c.waiting_for_return()) {
|
||||
if (ImGui::Button("Skip", ImVec2(third, 0))) c.skip();
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (ImGui::Button("Cancel", ImVec2(third, 0))) c.cancel();
|
||||
end_card();
|
||||
}
|
||||
void controller_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
|
||||
const auto sample = drone::radio::poll();
|
||||
auto& c = drone::radio::calibration();
|
||||
if (c.step() != drone::radio::Calibration::Step::idle) {
|
||||
setup_wizard(menu, callbacks, sample);
|
||||
return;
|
||||
}
|
||||
begin_card(menu, "drone-controller", "CONTROLLER");
|
||||
if (!sample.valid) {
|
||||
warn("No controller found. Plug in a gamepad, or an RC radio with its USB mode set to Joystick (on EdgeTX radios, pick "
|
||||
"\"USB Joystick (HID)\" when you plug it in).");
|
||||
} else {
|
||||
info(menu, "Connected", sample.name);
|
||||
if (primary_button(menu, "SET UP CONTROLLER")) c.start();
|
||||
note("Goes through each stick and switch and saves which is which. The drone holds still meanwhile.");
|
||||
}
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-manual", "MANUAL SETUP");
|
||||
if (sample.valid) {
|
||||
axis_bars(sample, -1);
|
||||
std::string pressed;
|
||||
for (int n = 1; n <= 32; ++n)
|
||||
if (sample.buttons & (1u << (n - 1))) pressed += (pressed.empty() ? "" : " ") + std::to_string(n);
|
||||
info(menu, "Buttons pressed", pressed.empty() ? "none" : pressed);
|
||||
note("Move a stick or flip a switch to see which axis or button it is.");
|
||||
}
|
||||
static const char* const names[4] = {"Roll", "Pitch", "Throttle", "Yaw"};
|
||||
static const char* const axes[] = {"Axis 1", "Axis 2", "Axis 3", "Axis 4", "Axis 5", "Axis 6"};
|
||||
auto map = drone::radio::mapping_snapshot();
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ImGui::PushID(i);
|
||||
int axis = std::clamp(std::abs(map[static_cast<std::size_t>(i)]), 1, 6) - 1;
|
||||
bool flip = map[static_cast<std::size_t>(i)] < 0;
|
||||
field(menu, names[i]);
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.6f);
|
||||
if (ImGui::Combo("##axis", &axis, axes, 6)) changed = true;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Reverse", &flip)) changed = true;
|
||||
map[static_cast<std::size_t>(i)] = (axis + 1) * (flip ? -1 : 1);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (changed) run(callbacks, std::format("droneaxes {} {} {} {}", map[0], map[1], map[2], map[3]));
|
||||
toggle(menu, callbacks, Knob::kThrottleCentre, "Throttle springs back to the middle");
|
||||
slider(menu, callbacks, Knob::kDeadzone, "Stick dead zone");
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "drone-switches", "SWITCHES", "an axis or button no stick uses; 0 = none");
|
||||
integer(menu, callbacks, Knob::kArmAxis, "Arm switch axis");
|
||||
integer(menu, callbacks, Knob::kArmButton, "Arm button");
|
||||
integer(menu, callbacks, Knob::kResetAxis, "Reset switch axis");
|
||||
integer(menu, callbacks, Knob::kResetButton, "Reset button");
|
||||
end_card();
|
||||
}
|
||||
|
||||
// ---- LOBBY
|
||||
void lobby_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
|
||||
begin_card(menu, "drone-lobby", "OTHER PLAYERS");
|
||||
if (!drone::beacon::server_allows().load()) {
|
||||
warn("This server has drones turned off. You can still fly, but nobody here sees your drone and you don't see theirs.");
|
||||
}
|
||||
toggle(menu, callbacks, Knob::kBeacon, "Let other players see my drone");
|
||||
toggle(menu, callbacks, Knob::kShowBeacons, "Show other players' drones");
|
||||
note("Other players only see drones if they are on a version of ReSkate with the drone.");
|
||||
end_card();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void drone_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
|
||||
category_tabs(menu, menu.drone_tab, {"FLY", "HANDLING", "RATES", "DISPLAY", "CONTROLLER", "LOBBY"}, "drone-tabs");
|
||||
ImGui::PushID(menu.drone_tab);
|
||||
ImGui::BeginChild("drone-tab", ImVec2(0, page_body_height(menu)));
|
||||
switch (menu.drone_tab) {
|
||||
case 0: fly_tab(menu, model, callbacks); break;
|
||||
case 1: handling_tab(menu, callbacks); break;
|
||||
case 2: rates_tab(menu, callbacks); break;
|
||||
case 3: display_tab(menu, callbacks); break;
|
||||
case 4: controller_tab(menu, callbacks); break;
|
||||
default: lobby_tab(menu, callbacks); break;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopID();
|
||||
}
|
||||
} // namespace dingosdk::overlay::menu
|
||||
@@ -315,7 +315,7 @@ void draw_nametags() {
|
||||
const auto dim = with_alpha(IM_COL32(230, 230, 230, 255), 0.85f * alpha);
|
||||
draw->AddText(font, detail, ImVec2(distance_at.x + offset, distance_at.y + offset), shadow, distance.c_str());
|
||||
draw->AddText(font, detail, distance_at, dim, distance.c_str());
|
||||
// Flying the FPV drone: said above the name, unless drones are hidden (drone window, Lobby).
|
||||
// Flying the FPV drone: said above the name, unless drones are hidden (DRONE page, LOBBY tab).
|
||||
if (tag.flying_drone &&
|
||||
dingosdk::drone::knobs().value[dingosdk::drone::kShowBeacons].load(std::memory_order_relaxed) >= 0.5f) {
|
||||
const char *label = "FLYING DRONE";
|
||||
|
||||
@@ -321,8 +321,6 @@ void draw_nametags();
|
||||
bool perf_hud_pending();
|
||||
void draw_perf_hud();
|
||||
void draw_perf_window();
|
||||
// The FPV drone settings window (drone_menu_overlay.cpp): drawn while the menu or console is open.
|
||||
void draw_drone_window();
|
||||
// The skater item grids' search bar and prompts (item_browser_overlay.cpp): polled every presented frame.
|
||||
enum class ItemBrowserKey { favorite, filter };
|
||||
bool item_browser_pending();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Engine/Core/Profiling/profiler.h"
|
||||
#include "overlay_internal.h"
|
||||
#include "Extension/Trainer/trainer_page.h"
|
||||
#include "Extension/Skater/drone_flight.h"
|
||||
#include "Extension/HallOfMeat/hall_of_meat_overlay.h"
|
||||
#include "park_previews.h"
|
||||
#include "chat_emotes.h"
|
||||
@@ -640,6 +641,7 @@ void render(IDXGISwapChain* presented, UINT flags) {
|
||||
const bool perf_frame = perf_hud_pending() || trainer_hud_pending();
|
||||
const bool hub_frame = hub_page_pending();
|
||||
if (trainer_open_requested()) s.visible.store(true);
|
||||
if (dingosdk::drone::menu_flag().load()) s.visible.store(true); // `dronemenu`: the menu opens on its DRONE page
|
||||
const bool menu_frame = interactive_visible(s);
|
||||
if (!menu_frame) {
|
||||
if (s.ui_was_interactive) {
|
||||
@@ -704,7 +706,6 @@ void render(IDXGISwapChain* presented, UINT flags) {
|
||||
draw_menu();
|
||||
draw_console();
|
||||
draw_perf_window();
|
||||
draw_drone_window();
|
||||
}
|
||||
draw_hub_page();
|
||||
draw_hall_of_meat();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Engine/Core/Platform/launcher_support.h"
|
||||
#include "skate_menu_internal.h"
|
||||
#include "Extension/Skater/drone_flight.h"
|
||||
#include "skate_menu.h"
|
||||
#include "modding_menu.h"
|
||||
#include "skate_style.h"
|
||||
@@ -278,11 +279,11 @@ ImFont* embedded_font(const wchar_t* name, float size, const ImWchar* ranges) {
|
||||
&config, ranges);
|
||||
}
|
||||
|
||||
enum Page { map, world, build, skater, training, multiplayer, progress, mods, music, settings, special, developer, page_count };
|
||||
enum Page { map, world, build, skater, training, drone, multiplayer, progress, mods, music, settings, special, developer, page_count };
|
||||
constexpr std::array<const char*, page_count> page_names{
|
||||
"MAP", "WORLD", "BUILD", "SKATER", "TRAINER", "MULTIPLAYER", "PROGRESS", "MODS", "MUSIC", "SETTINGS", "SPECIAL", "DEVELOPER"};
|
||||
"MAP", "WORLD", "BUILD", "SKATER", "TRAINER", "DRONE", "MULTIPLAYER", "PROGRESS", "MODS", "MUSIC", "SETTINGS", "SPECIAL", "DEVELOPER"};
|
||||
constexpr std::array<const char*, page_count> page_subtitles{
|
||||
"Pick your spot.", "Set the vibe.", "Make the park yours.", "Ride it your way.", "Tune it. Drill it. Measure it.",
|
||||
"Pick your spot.", "Set the vibe.", "Make the park yours.", "Ride it your way.", "Tune it. Drill it. Measure it.", "Take it to the sky.",
|
||||
"Bring your crew.", "Pick up where you want.", "Bring your own.", "Your sound, in the game.", "Your controls, your screen.", "Not everyone gets this page.",
|
||||
"Under the hood."};
|
||||
}
|
||||
@@ -370,6 +371,7 @@ void draw_skate_menu(SkateMenu& menu, const Model& model, const CallbacksV3& cal
|
||||
}
|
||||
draw->AddText(menu.body, px(12), ImVec2(origin.x + px(18), origin.y + px(66)), muted, "YOUR SESSION. YOUR RULES.");
|
||||
if (trainer_page_wanted()) menu.page = training;
|
||||
if (dingosdk::drone::menu_flag().exchange(false)) menu.page = drone; // `dronemenu`
|
||||
menu.page = std::clamp(menu.page, 0, static_cast<int>(page_count) - 1);
|
||||
// Developer (the network view) is hidden for now; offline there is no Multiplayer either.
|
||||
// Special is only there for a player the backend lists.
|
||||
@@ -429,6 +431,7 @@ void draw_skate_menu(SkateMenu& menu, const Model& model, const CallbacksV3& cal
|
||||
case build: build_page(menu, model, callbacks); break;
|
||||
case skater: skater_page(menu, model, callbacks); break;
|
||||
case training: trainer_page(menu, model, callbacks); break;
|
||||
case drone: drone_page(menu, model, callbacks); break;
|
||||
case multiplayer: multiplayer_page(menu, model, callbacks); break;
|
||||
case progress: progression_page(menu, model, callbacks); break;
|
||||
case mods:
|
||||
|
||||
@@ -83,6 +83,7 @@ struct SkateMenu {
|
||||
int world_layer_category = 0;
|
||||
std::array<char, 128> world_layer_search{};
|
||||
int progression_tab = 0;
|
||||
int drone_tab = 0; // the DRONE page (drone_page.cpp)
|
||||
int bus_stop_map = 0;
|
||||
int challenge_type = 0;
|
||||
std::array<char, 128> challenge_search{};
|
||||
|
||||
@@ -62,6 +62,8 @@ void graphics_page(SkateMenu&, const Model&, const CallbacksV3&);
|
||||
void developer_page(SkateMenu&, const Model&, const CallbacksV3&);
|
||||
// Extension/Music/music_page.cpp
|
||||
void music_page(SkateMenu&, const Model&, const CallbacksV3&);
|
||||
// The FPV drone (drone_page.cpp).
|
||||
void drone_page(SkateMenu&, const Model&, const CallbacksV3&);
|
||||
// Extension/Trainer/trainer_page.cpp
|
||||
void trainer_page(SkateMenu&, const Model&, const CallbacksV3&);
|
||||
bool trainer_page_wanted();
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ add_library(dingosdk_overlay STATIC
|
||||
Extension/UI/Overlay/hub_page.cpp
|
||||
Extension/UI/Overlay/game_text_overlay.cpp
|
||||
Extension/UI/Overlay/drone_hud_overlay.cpp
|
||||
Extension/UI/Overlay/drone_menu_overlay.cpp
|
||||
Extension/UI/Overlay/drone_page.cpp
|
||||
Extension/UI/Overlay/perf_overlay.cpp
|
||||
Extension/UI/Overlay/skate_hud_overlay.cpp
|
||||
Extension/UI/Overlay/nametag_overlay.cpp
|
||||
|
||||
Reference in new issue
Block a user