#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 #include #include #include #include // 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 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(k)); if (::dingosdk::overlay::menu::slider_float("##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(std::lround(get(k))); ImGui::PushID(static_cast(k)); if (::dingosdk::overlay::menu::slider_int("##value", &v, static_cast(drone::knob_info[k].lo), static_cast(drone::knob_info[k].hi))) set(k, static_cast(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 names) { const auto help = tip(k); field(menu, label, help.c_str()); int v = std::clamp(static_cast(std::lround(get(k))), 0, static_cast(names.size()) - 1); if (choice(menu, drone::knob_info[k].name, v, names)) { set(k, static_cast(v)); save(callbacks, k); } } // ---- FLY // The game's interface is hidden for a flight or a follow (the `hideui` drone setting) and only // brought back if the drone was what hid it. bool hid_ui = false; void hide_ui_for_drone(const Model& model, const CallbacksV3& callbacks) { if (get(Knob::kHideUi) >= 0.5f && !model.debug.game_ui_hidden) { run(callbacks, "hideui 1"); hid_ui = true; } } void bring_back_ui(const CallbacksV3& callbacks) { if (hid_ui) run(callbacks, "hideui 0"); hid_ui = false; } 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"); 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 bring_back_ui(callbacks); } else { if (!freecam) run(callbacks, "freecam 1"); if (!enabled) run(callbacks, "dronemode 1"); hide_ui_for_drone(model, callbacks); } } const bool gamepad = drone::gamepad_mode(); if (!model.debug.camera_available) warn("Load into a level to fly."); else if (flying) note(!radio_on ? "Flying with the keyboard and mouse. Close the menu to fly." : gamepad ? (overlay::read_gamepad().valid ? "Flying with the gamepad. Close the menu to fly." : "No gamepad found: flying with the keyboard and mouse. Close the menu to fly.") : "Flying with the controller. 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) { options(menu, callbacks, Knob::kGamepad, "Controller", {"RC radio", "Gamepad"}); if (gamepad) note("Left stick: throttle (the middle hovers) and turning. Right stick: tilt forward, back and sideways. " "A (cross) arms and disarms, Menu (Options) puts the drone back."); } if (!radio_on && (drone::radio::poll().valid || overlay::read_gamepad().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 or following"); end_card(); // Drone follow (Skater/drone_follow.h): the drone flies itself behind the skater. begin_card(menu, "drone-follow", "FOLLOW ME", "skate while it films you"); const bool following = model.debug.drone_follow; if (primary_button(menu, following ? "STOP FOLLOWING" : "FOLLOW ME", model.debug.camera_available)) { run(callbacks, following ? "dronefollow 0" : "dronefollow 1"); if (following) bring_back_ui(callbacks); else hide_ui_for_drone(model, callbacks); } if (!model.debug.camera_available) warn("Load into a level to use it."); else note(following ? "Skate as normal: the drone flies itself behind you, as a pilot would." : "The drone takes off behind you and flies itself, keeping you in shot while you skate. " "It uses your drone's settings, flown self-levelling."); // (Read every tick: a change shows while it follows.) slider(menu, callbacks, Knob::kFollowDistance, "Follow distance", "%.1f m"); slider(menu, callbacks, Knob::kFollowAggression, "Aggressiveness"); 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(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(std::lround(get(Knob::kGoggles))), 0, static_cast(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(i) + 1)) { set(Knob::kGoggles, static_cast(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 (::dingosdk::overlay::menu::slider_float("##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's drag is set so full throttle flat out settles near four times the fly speed (m/s). const auto speed = std::format("Flat out at about {:.0f} km/h (a little faster 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-mode", "FLIGHT MODE"); options(menu, callbacks, Knob::kFlightMode, "Mode", {"Acro", "Angle (self-levelling)"}); if (get(Knob::kFlightMode) >= 0.5f) { slider(menu, callbacks, Knob::kMaxAngle, "Most lean", "%.0f degrees"); note("The sticks set how far the drone leans; let go and it levels out. Easier to learn on, but no flips."); } else { note("The sticks set how fast the drone turns, and it stays at whatever angle you leave it: how real FPV pilots fly."); } toggle(menu, callbacks, Knob::kAltHold, "Hold height with a springing throttle"); slider(menu, callbacks, Knob::kClimbRate, "Climb speed", "%.1f m/s"); note("For gamepads and the keyboard, whose throttle springs back. Off: the middle of the stick is hover power, and the " "throttle never changes by itself. On: the middle holds your height, adding throttle as you lean to keep it. " "An RC radio's throttle stays where you put it and is not affected."); end_card(); 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-crashes", "CRASHES"); toggle(menu, callbacks, Knob::kCrash, "Crashes"); if (get(Knob::kCrash) >= 0.5f) { slider(menu, callbacks, Knob::kCrashSpeed, "Hit that cuts the motors", "%.1f m/s"); const auto cut = get(Knob::kCrashSpeed) > 0.0f ? std::format("Bumps bounce the drone off and knock it about. Hitting something at {:.0f} km/h or more cuts the motors: " "it tumbles down, and you arm again to fly on.", get(Knob::kCrashSpeed) * 3.6f) : std::string("Bumps bounce the drone off and knock it about, but never cut the motors."); note(cut.c_str()); } else { note("Off: the drone just stops against whatever it meets."); } slider(menu, callbacks, Knob::kPropwash, "Prop wash shake"); 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 (::dingosdk::overlay::menu::slider_float("##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(i)]; char value[16]; std::snprintf(value, sizeof value, "%+.2f", static_cast(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(c.arm_axis())); save(callbacks, Knob::kArmAxis); set(Knob::kArmButton, static_cast(c.arm_button())); save(callbacks, Knob::kArmButton); set(Knob::kResetAxis, static_cast(c.reset_axis())); save(callbacks, Knob::kResetAxis); set(Knob::kResetButton, static_cast(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(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(); } // A gamepad (drone_flight.h: gamepad_sticks): a fixed Mode 2 layout, so nothing to set up but feel. void gamepad_tab(SkateMenu& menu, const CallbacksV3& callbacks) { const auto pad = overlay::read_gamepad(); begin_card(menu, "drone-gamepad", "GAMEPAD"); if (!pad.valid) { warn("No gamepad found. Plug in an Xbox or PlayStation controller."); } else { info(menu, "Connected", pad.playstation ? "PlayStation controller" : "Xbox controller"); const auto sticks = drone::read_gamepad(pad); const auto bar = [](const char* label, float v) { char value[16]; std::snprintf(value, sizeof value, "%+.2f", static_cast(v)); ImGui::ProgressBar((v + 1.0f) * 0.5f, ImVec2(ImGui::GetContentRegionAvail().x * 0.75f, 0), value); ImGui::SameLine(); ImGui::TextUnformatted(label); }; bar("Throttle", sticks.climb); bar("Yaw", sticks.yaw); bar("Pitch", sticks.pitch); bar("Roll", sticks.roll); note("What the drone gets from each stick, after the dead zone and sensitivity below."); } note("Left stick: throttle up and down, turning left and right. Right stick: tilt (pitch, see Invert below) and roll. " "A (cross) arms and disarms, Menu (Options) puts the drone back where it took off."); end_card(); begin_card(menu, "drone-gamepad-feel", "STICKS"); slider(menu, callbacks, Knob::kPadRate, "Sensitivity"); slider(menu, callbacks, Knob::kPadExpo, "Softer middle"); slider(menu, callbacks, Knob::kDeadzone, "Stick dead zone"); toggle(menu, callbacks, Knob::kPadInvert, "Invert the right stick (up tips the nose up)"); note("Sensitivity scales how fast the drone can turn at full stick (your RATES still shape it). A softer middle " "makes small corrections gentler without slowing full-stick flips. Gamepads use a dead zone of at least 0.08."); end_card(); } void controller_tab(SkateMenu& menu, const CallbacksV3& callbacks) { if (drone::gamepad_mode()) { options(menu, callbacks, Knob::kGamepad, "Controller", {"RC radio", "Gamepad"}); gamepad_tab(menu, callbacks); return; } 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; } options(menu, callbacks, Knob::kGamepad, "Controller", {"RC radio", "Gamepad"}); 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(i)]), 1, 6) - 1; bool flip = map[static_cast(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(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."); note("Drone model: FPV Drone by PinaPipe (CC BY 4.0), reduced and rescaled for the game."); end_card(); } } // namespace void drone_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) { category_tabs(menu, menu.drone_tab, {"FLY", "FLIGHT", "DISPLAY", "CONTROLLER"}, "drone-tabs"); ImGui::PushID(menu.drone_tab); ImGui::BeginChild("drone-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened); switch (menu.drone_tab) { case 0: fly_tab(menu, model, callbacks); break; case 1: handling_tab(menu, callbacks); rates_tab(menu, callbacks); break; case 2: display_tab(menu, callbacks); lobby_tab(menu, callbacks); break; default: controller_tab(menu, callbacks); break; } ImGui::EndChild(); ImGui::PopID(); } } // namespace dingosdk::overlay::menu