Merge pull request #122 from Swagi-REDACTED/main

Added lots of new binds, allowed keys to be bound for controller back button support.
This commit is contained in:
zeex64 authored and GitHub committed 2026-10-08 20:49:59 -05:00
commit f18b50913b
28 files changed
+998 -78

No files matched your search

+4 -2
View File
@@ -40,8 +40,10 @@ inline std::ptrdiff_t peek_cstring(std::uintptr_t address, char* destination, st
if (!destination || !capacity || address < 0x10000 || address > highest_user_address - capacity) return -1;
__try {
const auto* source = reinterpret_cast<const char*>(address);
for (std::size_t i = 0; i < capacity; ++i)
if (!(destination[i] = source[i])) return static_cast<std::ptrdiff_t>(i);
for (std::size_t i = 0; i < capacity; ++i) {
destination[i] = source[i];
if (destination[i] == '\0') return static_cast<std::ptrdiff_t>(i);
}
return -1;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return -1;
@@ -35,4 +35,16 @@ inline constexpr std::array<OffboardFlightState, 3> offboard_flight_states{{
0xe3,0x71,0x21,0x4f,0x18,0x20,0xc5,0xe8,0x58,0xc1,0xc5,0xf8,0x11,0x06,0x4c,0x8b}}},
}};
inline constexpr std::uintptr_t offboard_flight_vtable = 0x65e8f40;
// FollowRagdoll / FollowAnimatedRagdoll / FollowSimulatedRagdoll also keep
// world velocity at +10. Their shared update derives it from displacement
// (RVA 48380DE), including Y at +14. Boosts must update this cached velocity
// as well as the skeleton bodies when diving or gliding.
inline constexpr std::array<std::array<std::uintptr_t, 3>, 3> offboard_ragdoll_states{{
{0x720, 0x65e8df8, 0x4832c80},
{0x8e0, 0x65e8e60, 0x4830a70},
{0xaa0, 0x65e8ed0, 0x48332f0},
}};
inline constexpr Fingerprint ragdoll_velocity_write{0x48380de, {
0xc4,0xe2,0x79,0x18,0x80,0xec,0x17,0x00,0x00,0xc5,0xe8,0x5e,0xc8,0xc5,0xf8,0x11,
0x4f,0x10,0xc5,0xf8,0x10,0x43,0x30,0xc5,0xf8,0x11,0x87,0x60,0x01,0x00,0x00,0x4c}};
}
+134 -10
View File
@@ -1,5 +1,7 @@
#pragma once
#include <algorithm>
#include <bit>
#include <array>
#include <cstdint>
#include <optional>
#include <string>
@@ -7,10 +9,69 @@
namespace dingosdk {
// XInput button bits, plus two synthetic trigger bits (above the dead zone).
inline constexpr std::uint32_t controller_button_mask = 0x3f3ff;
// Single keys retain their saved format. Chords pack up to five sorted key IDs
// into the remaining 30 bits of a distinct tag in the same profile field.
inline constexpr std::uint32_t keyboard_binding_tag = 0x80000000u;
inline constexpr std::uint32_t keyboard_chord_tag = 0x40000000u;
inline constexpr std::uint8_t keyboard_key_id(std::uint32_t key) noexcept {
if (key >= '0' && key <= '9') return static_cast<std::uint8_t>(key - '0' + 1);
if (key >= 'A' && key <= 'Z') return static_cast<std::uint8_t>(key - 'A' + 11);
if (key == 0x20) return 37; // Space
if (key >= 0x70 && key <= 0x7b) return static_cast<std::uint8_t>(key - 0x70 + 38); // F1-F12
if (key == 0x11) return 50; // Ctrl
if (key == 0x10) return 51; // Shift
if (key == 0x12) return 52; // Alt
return 0;
}
inline constexpr std::uint8_t keyboard_key_from_id(std::uint32_t id) noexcept {
if (id >= 1 && id <= 10) return static_cast<std::uint8_t>('0' + id - 1);
if (id >= 11 && id <= 36) return static_cast<std::uint8_t>('A' + id - 11);
if (id == 37) return 0x20;
if (id >= 38 && id <= 49) return static_cast<std::uint8_t>(0x70 + id - 38);
if (id == 50) return 0x11;
if (id == 51) return 0x10;
if (id == 52) return 0x12;
return 0;
}
inline constexpr bool bindable_keyboard_key(std::uint32_t key) noexcept {
return keyboard_key_id(key) != 0;
}
inline constexpr bool keyboard_binding(std::uint32_t value) noexcept {
if ((value & ~0xffu) == keyboard_binding_tag) return bindable_keyboard_key(value & 0xffu);
if ((value & 0xc0000000u) != keyboard_chord_tag) return false;
unsigned count = 0, previous = 0;
bool ended = false;
for (unsigned slot = 0; slot < 5; ++slot) {
const unsigned id = (value >> (slot * 6)) & 0x3fu;
if (!id) { ended = true; continue; }
if (ended || !keyboard_key_from_id(id) || id <= previous) return false;
previous = id;
++count;
}
return count >= 2;
}
inline std::uint32_t keyboard_binding_from_keys(const std::array<std::uint64_t, 4>& keys) noexcept {
std::uint32_t value = keyboard_chord_tag;
unsigned count = 0;
for (unsigned id = 1; id <= 52; ++id) {
const auto key = keyboard_key_from_id(id);
if (!(keys[key / 64] & (std::uint64_t{1} << (key % 64)))) continue;
if (count == 5) return 0;
value |= id << (count++ * 6);
}
if (count == 1) {
const auto id = value & 0x3fu;
return keyboard_binding_tag | keyboard_key_from_id(id);
}
return count >= 2 ? value : 0;
}
inline constexpr bool valid_controller_combo(std::uint32_t value) noexcept {
return (value & ~controller_button_mask) == 0;
}
// Button names shown to the player. Bindings are always stored as XInput bits.
inline constexpr bool valid_action_binding(std::uint32_t value) noexcept {
return valid_controller_combo(value) || keyboard_binding(value);
}
// Button names shown to the player. Controller chords keep their XInput bits.
enum class ControllerStyle : std::uint8_t { xbox, dualshock4, dualsense };
struct ControllerInput {
bool available{};
@@ -19,16 +80,46 @@ struct ControllerInput {
// (a pad connecting, disconnecting or switching source) resets latches.
unsigned device{};
ControllerStyle style{};
std::array<std::uint64_t, 4> keys{};
};
struct ControllerBindingsModel {
bool available{};
bool freecam_controller{};
std::uint32_t freecam_controller_combo{};
std::uint32_t freecam_combo{};
std::uint32_t tp_to_freecam_combo{};
std::uint32_t noclip_combo{};
std::uint32_t forward_velocity_combo{};
std::uint32_t up_velocity_combo{};
std::uint32_t offboard_up_velocity_combo{};
std::string status;
};
inline std::string controller_combo_label(std::uint32_t value, ControllerStyle style = ControllerStyle::xbox) {
if (!value) return "Not bound";
if (keyboard_binding(value)) {
const auto key_name = [](std::uint32_t key) {
if (key == 0x20) return std::string("Space");
if (key == 0x11) return std::string("Ctrl");
if (key == 0x10) return std::string("Shift");
if (key == 0x12) return std::string("Alt");
if (key >= 0x70) return "F" + std::to_string(key - 0x6fu);
return std::string(1, static_cast<char>(key));
};
if ((value & 0xc0000000u) == keyboard_binding_tag) return key_name(value & 0xffu);
std::string label;
const auto append = [&](unsigned wanted) {
for (unsigned slot = 0; slot < 5; ++slot) {
const auto id = (value >> (slot * 6)) & 0x3fu;
if (!id) break;
if (id != wanted) continue;
if (!label.empty()) label += " + ";
label += key_name(keyboard_key_from_id(id));
}
};
for (unsigned id = 50; id <= 52; ++id) append(id);
for (unsigned id = 1; id < 50; ++id) append(id);
return label;
}
const bool playstation = style != ControllerStyle::xbox;
struct Button { std::uint32_t bit; const char* xbox; const char* playstation; };
constexpr Button buttons[]{
@@ -46,18 +137,34 @@ inline std::string controller_combo_label(std::uint32_t value, ControllerStyle s
return label;
}
// A held combo never repeats. Returning from menus, reconnecting, changing a
// binding, and changing level all require releasing every bound button first.
// binding, and changing level all require releasing the bound inputs first.
struct ControllerComboLatch {
std::uint32_t combo{};
unsigned device{};
bool armed{};
bool update(std::uint32_t binding, ControllerInput input, bool inhibited) noexcept {
if (binding != combo || input.device != device) {
combo = binding; device = input.device; armed = false;
const bool keyboard = keyboard_binding(binding);
const unsigned source = keyboard ? 0 : input.device;
if (binding != combo || source != device) {
combo = binding; device = source; armed = false;
}
if (inhibited || !input.available || !combo || !valid_controller_combo(combo)) {
if (inhibited || (!keyboard && !input.available) || !combo || !valid_action_binding(combo)) {
armed = false; return false;
}
if (keyboard) {
bool any = false, all = true;
for (unsigned slot = 0; slot < 5; ++slot) {
const auto key = (combo & 0xc0000000u) == keyboard_binding_tag ?
(slot ? 0u : combo & 0xffu) : keyboard_key_from_id((combo >> (slot * 6)) & 0x3fu);
if (!key) break;
const bool held = (input.keys[key / 64] & (std::uint64_t{1} << (key % 64))) != 0;
any |= held; all &= held;
}
if (!any) armed = true;
if (!armed || !all) return false;
armed = false;
return true;
}
const auto held = input.buttons & combo;
if (!held) armed = true;
if (!armed || held != combo) return false;
@@ -71,13 +178,30 @@ struct ControllerComboCapture {
std::uint32_t chord{};
unsigned device{};
bool ready{};
std::optional<std::uint32_t> update(ControllerInput input) noexcept {
if (!input.available || input.device != device) {
chord = 0; ready = false; device = input.device;
std::array<std::uint64_t, 4> keyboard_chord{};
std::optional<std::uint32_t> update(ControllerInput input, bool allow_keyboard = false) noexcept {
if (input.device != device || (!input.available && !allow_keyboard)) {
chord = 0; ready = false; keyboard_chord = {}; device = input.device;
}
if (!input.available && !allow_keyboard) return {};
const auto buttons = input.buttons & controller_button_mask;
const bool keys_held = std::any_of(input.keys.begin(), input.keys.end(), [](auto word) { return word != 0; });
if (!ready) { ready = buttons == 0 && (!allow_keyboard || !keys_held); return {}; }
if (allow_keyboard && keys_held && !chord) {
if (std::popcount(input.keys[0]) + std::popcount(input.keys[1]) +
std::popcount(input.keys[2]) + std::popcount(input.keys[3]) >
std::popcount(keyboard_chord[0]) + std::popcount(keyboard_chord[1]) +
std::popcount(keyboard_chord[2]) + std::popcount(keyboard_chord[3])) keyboard_chord = input.keys;
}
if (std::any_of(keyboard_chord.begin(), keyboard_chord.end(), [](auto word) { return word != 0; })) {
if (!keys_held) {
const auto result = keyboard_binding_from_keys(keyboard_chord);
keyboard_chord = {}; ready = false;
if (result) return result;
}
return {};
}
if (!input.available) return {};
const auto buttons = input.buttons & controller_button_mask;
if (!ready) { ready = buttons == 0; return {}; }
if (std::popcount(buttons) > std::popcount(chord)) chord = buttons;
if (!buttons && chord) {
const auto result = chord; chord = 0; ready = false;
+4 -2
View File
@@ -981,8 +981,10 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
for (const auto& [index, guids] : left) {
// Name the mod whose asset took the item's name, when that is why.
const Asset* taken{};
for (const auto& guid : guids)
if ((taken = held.replacement(guid))) break;
for (const auto& guid : guids) {
taken = held.replacement(guid);
if (taken) break;
}
report.notes.push_back(mod_of(index) + ": " + entry.asset.name +
": " + std::to_string(guids.size()) + (itemList ? " item(s)" : " entry(ies)") +
" left out of the list because the merged "
+15
View File
@@ -112,9 +112,14 @@ ParkChoices park_choices(const Snapshot&);
WorldLayerChoices world_layer_choices(const Snapshot&);
WorldControls world_controls(const Snapshot&);
GraphicsControls graphics_controls(const Snapshot&);
std::uint32_t freecam_controller_binding(const Snapshot&);
bool freecam_controller(const Snapshot&);
std::uint32_t freecam_binding(const Snapshot&);
std::uint32_t tp_to_freecam_binding(const Snapshot&);
std::uint32_t noclip_binding(const Snapshot&);
std::uint32_t forward_velocity_binding(const Snapshot&);
std::uint32_t up_velocity_binding(const Snapshot&);
std::uint32_t offboard_up_velocity_binding(const Snapshot&);
std::vector<std::string> challenge_completed_criteria(const Snapshot&, std::string_view);
std::string encode(const Snapshot&);
Snapshot decode(std::string_view);
@@ -177,12 +182,22 @@ public:
void save_park_choice(unsigned lot, std::string_view id);
void save_world_controls(const WorldControls&);
void save_graphics_controls(const GraphicsControls&);
bool freecam_controller() const;
void save_freecam_controller(bool);
std::uint32_t freecam_controller_binding() const;
void save_freecam_controller_binding(std::uint32_t);
std::uint32_t freecam_binding() const;
void save_freecam_binding(std::uint32_t);
std::uint32_t tp_to_freecam_binding() const;
void save_tp_to_freecam_binding(std::uint32_t);
std::uint32_t noclip_binding() const;
void save_noclip_binding(std::uint32_t);
std::uint32_t forward_velocity_binding() const;
void save_forward_velocity_binding(std::uint32_t);
std::uint32_t up_velocity_binding() const;
void save_up_velocity_binding(std::uint32_t);
std::uint32_t offboard_up_velocity_binding() const;
void save_offboard_up_velocity_binding(std::uint32_t);
void save_world_layer_choice(unsigned layer, std::string_view mode);
void restore_world_layers(WorldMap map);
std::uint64_t begin_challenge(std::string_view id);
@@ -9,15 +9,20 @@ ControllerBindingsModel local_profile_controller_bindings() {
ControllerBindingsModel result;
result.status = binding_feedback;
if (!s.active || !s.store) return result;
result.freecam_controller = s.store->freecam_controller();
result.freecam_controller_combo = s.store->freecam_controller_binding();
result.freecam_combo = s.store->freecam_binding();
result.tp_to_freecam_combo = s.store->tp_to_freecam_binding();
result.noclip_combo = s.store->noclip_binding();
result.forward_velocity_combo = s.store->forward_velocity_binding();
result.up_velocity_combo = s.store->up_velocity_binding();
result.offboard_up_velocity_combo = s.store->offboard_up_velocity_binding();
result.available = true;
return result;
}
bool set_local_noclip_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_controller_combo(combo)) return false;
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_noclip_binding(combo);
binding_feedback = combo ? "Noclip binding saved." : "Noclip binding cleared.";
@@ -26,9 +31,55 @@ bool set_local_noclip_binding(std::uint32_t combo) {
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool local_freecam_controller() {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
return s.active && s.store && s.store->freecam_controller();
}
bool set_local_freecam_controller(bool value) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store) return false;
try {
s.store->save_freecam_controller(value);
return true;
} catch (...) { return false; }
}
bool set_local_freecam_controller_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_freecam_controller_binding(combo);
binding_feedback = combo ? "Freecam Controller binding saved." : "Freecam Controller binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","freecam_controller"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_freecam_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_freecam_binding(combo);
binding_feedback = combo ? "Freecam binding saved." : "Freecam binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","freecam"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_tp_to_freecam_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_tp_to_freecam_binding(combo);
binding_feedback = combo ? "TP to Freecam binding saved." : "TP to Freecam binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","tp_to_freecam"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_forward_velocity_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_controller_combo(combo)) return false;
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_forward_velocity_binding(combo);
binding_feedback = combo ? "Forward Boost binding saved." : "Forward Boost binding cleared.";
@@ -37,9 +88,20 @@ bool set_local_forward_velocity_binding(std::uint32_t combo) {
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_offboard_up_velocity_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_offboard_up_velocity_binding(combo);
binding_feedback = combo ? "Off-board Up Boost binding saved." : "Off-board Up Boost binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","offboard_up_velocity"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_up_velocity_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_controller_combo(combo)) return false;
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_up_velocity_binding(combo);
binding_feedback = combo ? "Up Boost binding saved." : "Up Boost binding cleared.";
@@ -60,9 +60,15 @@ bool queue_local_park_selection(const EditorSelectionRequest &);
bool queue_local_park_paste(const EditorPasteRequest &);
void tick_local_park_editor() noexcept;
ControllerBindingsModel local_profile_controller_bindings();
bool set_local_freecam_controller(bool);
bool local_freecam_controller();
bool set_local_freecam_controller_binding(std::uint32_t);
bool set_local_freecam_binding(std::uint32_t);
bool set_local_tp_to_freecam_binding(std::uint32_t);
bool set_local_noclip_binding(std::uint32_t);
bool set_local_forward_velocity_binding(std::uint32_t);
bool set_local_up_velocity_binding(std::uint32_t);
bool set_local_offboard_up_velocity_binding(std::uint32_t);
bool set_local_object_persistence(bool enabled);
bool clear_local_persisted_objects(std::string_view map);
bool delete_local_placed_object(std::string_view map, std::uint64_t token);
+61 -3
View File
@@ -146,12 +146,58 @@ void Store::save_district_rank(std::string_view id, std::uint32_t rank) {
}
bool Store::freecam_controller() const {
std::lock_guard lock(mutex_);
return profile::freecam_controller(value_);
}
void Store::save_freecam_controller(bool value) {
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"options", "freecam_controller"}) = value;
update.commit();
}
std::uint32_t Store::freecam_controller_binding() const {
std::lock_guard lock(mutex_);
return profile::freecam_controller_binding(value_);
}
void Store::save_freecam_controller_binding(std::uint32_t combo) {
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "freecam_controller"}) = combo;
update.commit();
}
std::uint32_t Store::freecam_binding() const {
std::lock_guard lock(mutex_);
return profile::freecam_binding(value_);
}
void Store::save_freecam_binding(std::uint32_t combo) {
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "freecam"}) = combo;
update.commit();
}
std::uint32_t Store::tp_to_freecam_binding() const {
std::lock_guard lock(mutex_);
return profile::tp_to_freecam_binding(value_);
}
void Store::save_tp_to_freecam_binding(std::uint32_t combo) {
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "tp_to_freecam"}) = combo;
update.commit();
}
std::uint32_t Store::noclip_binding() const {
std::lock_guard lock(mutex_);
return profile::noclip_binding(value_);
}
void Store::save_noclip_binding(std::uint32_t combo) {
require(valid_controller_combo(combo), "Unsupported controller combo");
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "noclip"}) = combo;
@@ -164,19 +210,31 @@ std::uint32_t Store::forward_velocity_binding() const {
return profile::forward_velocity_binding(value_);
}
void Store::save_forward_velocity_binding(std::uint32_t combo) {
require(valid_controller_combo(combo), "Unsupported controller combo");
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "forward_velocity"}) = combo;
update.commit();
}
std::uint32_t Store::offboard_up_velocity_binding() const {
std::lock_guard lock(mutex_);
return profile::offboard_up_velocity_binding(value_);
}
void Store::save_offboard_up_velocity_binding(std::uint32_t combo) {
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "offboard_up_velocity"}) = combo;
update.commit();
}
std::uint32_t Store::up_velocity_binding() const {
std::lock_guard lock(mutex_);
return profile::up_velocity_binding(value_);
}
void Store::save_up_velocity_binding(std::uint32_t combo) {
require(valid_controller_combo(combo), "Unsupported controller combo");
require(valid_action_binding(combo), "Unsupported controller combo or keyboard key");
std::lock_guard lock(mutex_);
Update update(*this);
update.json(value_.settings, {"bindings", "up_velocity"}) = combo;
+2 -1
View File
@@ -67,9 +67,10 @@ void Store::Update::encode(Json& patch) {
if (settings) {
detail::validate_settings(value);
(void)park_choices(value); (void)world_layer_choices(value);
(void)world_controls(value); (void)graphics_controls(value); (void)profile::noclip_binding(value);
(void)world_controls(value); (void)graphics_controls(value); (void)profile::freecam_controller(value); (void)profile::freecam_controller_binding(value); (void)profile::freecam_binding(value); (void)profile::tp_to_freecam_binding(value); (void)profile::noclip_binding(value);
(void)profile::forward_velocity_binding(value);
(void)profile::up_velocity_binding(value);
(void)profile::offboard_up_velocity_binding(value);
remove("player_settings", [](const Json& row) { return row[0] != "options"; });
auto residual = value.settings;
const auto flatten = [&](auto&& self, Json& node, const std::string& scope) -> void {
+67 -6
View File
@@ -5,6 +5,54 @@
namespace dingosdk::profile {
using namespace detail;
std::uint32_t freecam_controller_binding(const Snapshot& s) {
const auto bindings = s.settings.find("bindings");
if (bindings == s.settings.end()) return 0;
require(bindings->is_object(), "Bindings must be an object");
const auto value = bindings->find("freecam_controller");
if (value == bindings->end()) return 0;
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"Freecam Controller binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in Freecam Controller binding");
return static_cast<std::uint32_t>(mask);
}
bool freecam_controller(const Snapshot& s) {
const auto options = s.settings.find("options");
if (options == s.settings.end()) return false;
require(options->is_object(), "Options must be an object");
const auto value = options->find("freecam_controller");
if (value == options->end()) return false;
require(value->is_boolean(), "freecam_controller must be a boolean");
return value->get<bool>();
}
std::uint32_t freecam_binding(const Snapshot& s) {
const auto bindings = s.settings.find("bindings");
if (bindings == s.settings.end()) return 0;
require(bindings->is_object(), "Bindings must be an object");
const auto value = bindings->find("freecam");
if (value == bindings->end()) return 0;
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"Freecam binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in Freecam binding");
return static_cast<std::uint32_t>(mask);
}
std::uint32_t tp_to_freecam_binding(const Snapshot& s) {
const auto bindings = s.settings.find("bindings");
if (bindings == s.settings.end()) return 0;
require(bindings->is_object(), "Bindings must be an object");
const auto value = bindings->find("tp_to_freecam");
if (value == bindings->end()) return 0;
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"TP to Freecam binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in TP to Freecam binding");
return static_cast<std::uint32_t>(mask);
}
std::uint32_t noclip_binding(const Snapshot& s) {
const auto bindings = s.settings.find("bindings");
if (bindings == s.settings.end()) return 0;
@@ -14,8 +62,8 @@ std::uint32_t noclip_binding(const Snapshot& s) {
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"Noclip binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= controller_button_mask && valid_controller_combo(static_cast<std::uint32_t>(mask)),
"Unsupported controller buttons in Noclip binding");
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in Noclip binding");
return static_cast<std::uint32_t>(mask);
}
@@ -28,11 +76,24 @@ std::uint32_t forward_velocity_binding(const Snapshot& s) {
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"Forward velocity binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= controller_button_mask && valid_controller_combo(static_cast<std::uint32_t>(mask)),
"Unsupported controller buttons in Forward Boost binding");
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in Forward Boost binding");
return static_cast<std::uint32_t>(mask);
}
std::uint32_t offboard_up_velocity_binding(const Snapshot& s) {
const auto bindings = s.settings.find("bindings");
if (bindings == s.settings.end()) return 0;
require(bindings->is_object(), "Bindings must be an object");
const auto value = bindings->find("offboard_up_velocity");
if (value == bindings->end()) return 0;
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"Off-board Up Boost binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in Off-board Up Boost binding");
return static_cast<std::uint32_t>(mask);
}
std::uint32_t up_velocity_binding(const Snapshot& s) {
const auto bindings = s.settings.find("bindings");
if (bindings == s.settings.end()) return 0;
@@ -42,8 +103,8 @@ std::uint32_t up_velocity_binding(const Snapshot& s) {
require(value->is_number_unsigned() || (value->is_number_integer() && value->get<std::int64_t>() >= 0),
"Up velocity binding must be a nonnegative integer");
const auto mask = value->get<std::uint64_t>();
require(mask <= controller_button_mask && valid_controller_combo(static_cast<std::uint32_t>(mask)),
"Unsupported controller buttons in Up Boost binding");
require(mask <= UINT32_MAX && valid_action_binding(static_cast<std::uint32_t>(mask)),
"Unsupported controller or keyboard key in Up Boost binding");
return static_cast<std::uint32_t>(mask);
}
+102 -15
View File
@@ -1,6 +1,7 @@
#include "client_source_spawn.h"
#include "client_source_spawn_internal.h"
#include "no_bail.h"
#include "offboard_flight.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Platform/launcher_support.h"
#include "Engine/Game/Build/20260929/engine.h"
@@ -242,6 +243,7 @@ constexpr const char* no_bail = "NoBail";
constexpr const char* flight_speed = "FlightSpeed";
constexpr const char* forward_velocity = "ForwardVelocity";
constexpr const char* up_velocity = "UpVelocity";
constexpr const char* offboard_up_velocity = "OffboardUpVelocity";
constexpr const char* first_person_fov = "FirstPerson.Fov";
constexpr const char* free_camera_fov = "FreeCamera.Fov";
constexpr const char* spring = "FirstPerson.Spring";
@@ -272,6 +274,7 @@ void save_debug(InteractiveDebug& debug) noexcept {
{saved::flight_speed, static_cast<double>(debug.flight_speed)},
{saved::forward_velocity, static_cast<double>(debug.forward_velocity_speed)},
{saved::up_velocity, static_cast<double>(debug.up_velocity_speed)},
{saved::offboard_up_velocity, static_cast<double>(debug.offboard_up_velocity_speed)},
{saved::first_person_fov, static_cast<double>(debug.first_person_fov)},
{saved::free_camera_fov, static_cast<double>(debug.free_camera_fov)},
{saved::spring, arm.enabled}};
@@ -300,6 +303,8 @@ void load_saved_debug(InteractiveDebug& debug) noexcept {
debug.forward_velocity_speed = *speed;
if (const auto speed = saved::number(saved::up_velocity); speed && *speed >= 1.0f && *speed <= 25.0f)
debug.up_velocity_speed = *speed;
if (const auto speed = saved::number(saved::offboard_up_velocity); speed && *speed >= 1.0f && *speed <= 25.0f)
debug.offboard_up_velocity_speed = *speed;
if (const auto fov = saved::number(saved::first_person_fov);
fov && *fov >= first_person_fov_min && *fov <= first_person_fov_max) {
debug.first_person_fov = *fov;
@@ -372,6 +377,7 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
source_require(!debug.park_editor || debug.editor_transition || request.action == overlay::DebugAction::restore_debug ||
request.action == overlay::DebugAction::set_camera_speed || request.action == overlay::DebugAction::set_forward_velocity_speed ||
request.action == overlay::DebugAction::set_up_velocity_speed ||
request.action == overlay::DebugAction::set_offboard_up_velocity_speed ||
request.action == overlay::DebugAction::set_first_person_fov ||
request.action == overlay::DebugAction::set_free_camera_fov ||
(request.action >= overlay::DebugAction::set_first_person_spring && request.action <= overlay::DebugAction::reset_first_person_arm) ||
@@ -424,13 +430,21 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
return;
}
if (request.action == overlay::DebugAction::set_forward_velocity_speed ||
request.action == overlay::DebugAction::set_up_velocity_speed) {
const bool up = request.action == overlay::DebugAction::set_up_velocity_speed;
source_require(std::isfinite(request.value) && request.value >= 1.0f && request.value <= (up ? 25.0f : 300.0f),
up ? "Up Boost speed must be between 1 and 25." : "Forward Boost speed must be between 1 and 300.");
(up ? debug.up_velocity_speed : debug.forward_velocity_speed) = request.value;
request.action == overlay::DebugAction::set_up_velocity_speed ||
request.action == overlay::DebugAction::set_offboard_up_velocity_speed) {
if (request.action == overlay::DebugAction::set_offboard_up_velocity_speed) {
source_require(std::isfinite(request.value) && request.value >= 1.0f && request.value <= 25.0f,
"Off-board Up Boost speed must be between 1 and 25.");
debug.offboard_up_velocity_speed = request.value;
debug.status = "Off-board Up Boost speed updated.";
} else {
const bool up = request.action == overlay::DebugAction::set_up_velocity_speed;
source_require(std::isfinite(request.value) && request.value >= 1.0f && request.value <= (up ? 25.0f : 300.0f),
up ? "Up Boost speed must be between 1 and 25." : "Forward Boost speed must be between 1 and 300.");
(up ? debug.up_velocity_speed : debug.forward_velocity_speed) = request.value;
debug.status = up ? "Up Boost speed updated." : "Forward Boost speed updated.";
}
mark_debug_changed(debug);
debug.status = up ? "Up Boost speed updated." : "Forward Boost speed updated.";
return;
}
if (request.action == overlay::DebugAction::restore_debug) {
@@ -443,7 +457,9 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
debug.forward_velocity_speed = 20.0f;
debug.forward_velocity.valid = false;
debug.up_velocity_speed = 20.0f;
debug.offboard_up_velocity_speed = 20.0f;
debug.up_velocity.valid = false;
debug.offboard_up_velocity.valid = false;
debug.no_bail = false;
debug_stop_noclip(debug);
clear_no_bail();
@@ -475,29 +491,63 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
return;
}
source_require(can_control, "Wait for an active local level before changing debug settings.");
if (request.action == overlay::DebugAction::add_forward_velocity || request.action == overlay::DebugAction::add_up_velocity) {
if (request.action == overlay::DebugAction::add_forward_velocity || request.action == overlay::DebugAction::add_up_velocity || request.action == overlay::DebugAction::add_offboard_up_velocity) {
source_require(session_boosts_allowed(), "The host has turned off boosts in this session.");
const bool up = request.action == overlay::DebugAction::add_up_velocity;
const bool offboard_boost = request.action == overlay::DebugAction::add_offboard_up_velocity;
const bool up = request.action == overlay::DebugAction::add_up_velocity || offboard_boost;
source_require(!debug.noclip, "Disable Noclip before using velocity boosts.");
source_require(source_state().velocity_guard_active.load(std::memory_order_acquire),
"Velocity boosts are unavailable for this game build.");
overlay::DebugModel skater;
const auto transform = debug_skater(trial.base, client, skater);
const auto bodies = debug_noclip_bodies(trial.base, client, skater.skater_identity);
source_require(!bodies.offboard, "Velocity boosts require the skater to be on the board.");
if (offboard_boost) {
source_require(bodies.offboard || bodies.wipeout, "Off-board Up Boost requires walking, falling or gliding.");
} else {
source_require(!bodies.offboard, "Velocity boosts require the skater to be on the board.");
}
SourceReader reader;
source_require(reader.value<std::uint8_t>(skater.skater_identity, 0x7e0) == 0,
"Wait for the current teleport before using velocity boosts.");
const auto board_velocity = up ? std::array<float, 3>{} :
reader.value<std::array<float, 3>>(bodies.parts[0], 0x70);
reader.verify();
const auto velocity = velocity_boost_delta(transform, up ? debug.up_velocity_speed : debug.forward_velocity_speed,
const float speed = offboard_boost ? debug.offboard_up_velocity_speed : (up ? debug.up_velocity_speed : debug.forward_velocity_speed);
const auto velocity = velocity_boost_delta(transform, speed,
up ? VelocityBoostDirection::up : VelocityBoostDirection::forward, board_velocity);
source_require(velocity.has_value(), "Velocity boost direction or speed is invalid.");
auto& boost = up ? debug.up_velocity : debug.forward_velocity;
source_require(!boost.valid, "Velocity boost is already queued.");
auto& boost = offboard_boost ? debug.offboard_up_velocity : (up ? debug.up_velocity : debug.forward_velocity);
source_require(offboard_boost || !boost.valid, "Velocity boost is already queued.");
boost = {client, skater.skater_identity, bodies.core, GetTickCount64() + 1000, *velocity, true};
debug.status = up ? "Up Boost queued." : "Forward Boost queued.";
if (offboard_boost) source_state().offboard_boost_core.store(bodies.core, std::memory_order_release);
if (offboard_boost) {
// Give the player the impulse on this client tick. The motion hook
// keeps its upward speed briefly if native off-board drive rewrites it.
const float current_up = reader.value<float>(bodies.parts[9], 0x74);
const float target_up = std::max(speed, current_up + speed);
source_require(std::isfinite(target_up) && target_up <= 100000,
"Off-board boost produced an invalid upward speed.");
std::array<std::array<float, 3>, 32> velocities{};
std::array<std::uint32_t, 32> flags{};
for (std::size_t i = 9; i < bodies.parts.size(); ++i) {
velocities[i] = reader.value<std::array<float, 3>>(bodies.parts[i], 0x70);
flags[i] = reader.value<std::uint32_t>(bodies.parts[i], 0x60);
velocities[i][1] = std::max(velocities[i][1], target_up);
}
reader.verify();
if (bodies.offboard)
source_require(ensure_offboard_up_velocity(trial.base, bodies.core, bodies.context,
bodies.rig_wrapper, target_up), "Off-board boost motion ownership changed.");
for (std::size_t i = 9; i < bodies.parts.size(); ++i) {
debug_write(bodies.parts[i] + 0x70, velocities[i]);
debug_write(bodies.parts[i] + 0x60, flags[i] | 8u);
}
boost.velocity[1] = target_up;
boost.applied = true;
boost.expires = GetTickCount64() + 180;
++debug.offboard_up_velocity_updates;
debug.status = "Off-board up velocity added.";
} else debug.status = up ? "Up Boost queued." : "Forward Boost queued.";
return;
}
if (request.action == overlay::DebugAction::set_no_bail) {
@@ -532,6 +582,7 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
source_require(session_noclip_allowed(), "The host has turned off noclip in this session.");
debug.forward_velocity.valid = false;
debug.up_velocity.valid = false;
debug.offboard_up_velocity.valid = false;
source_require(no_bail_available(), "Noclip requires the native No Bail hooks.");
source_require(source_state().velocity_guard_active.load(std::memory_order_acquire),
"Velocity flight is unavailable for this game build.");
@@ -671,6 +722,7 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
SourceLastError error;
overlay::DebugModel model;
auto& state = source_state();
model.free_camera = state.free_camera_active.load(std::memory_order_acquire);
if (!state.initialized.load(std::memory_order_acquire) || state.busy.test_and_set(std::memory_order_acquire)) return model;
SourceBusyScope scope{state.busy};
auto& trial = state.trial;
@@ -689,6 +741,7 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
if (!can_control) {
debug.forward_velocity.valid = false;
debug.up_velocity.valid = false;
debug.offboard_up_velocity.valid = false;
}
if (debug.park_editor && !lobby_object_placement_allowed() && can_control && camera_phase_observed) {
try {
@@ -710,6 +763,10 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
catch (const SourceGuard& guard) { debug.status = guard.message; }
catch (...) { debug.status = "Debug action failed; inspect the current state before retrying."; }
}
// Camera ownership survives a busy tick or a failed presentation snapshot.
// Input capture must follow the lease, not those transient model failures.
state.free_camera_active.store(debug.camera_owned && !debug.first_person, std::memory_order_release);
model.free_camera = debug.camera_owned && !debug.first_person;
std::optional<SourceCameraSnapshot> flight_camera;
try { debug_flight_tick(trial, client, can_control, camera_phase_observed, flight_input, error.value, flight_camera); }
catch (const SourceGuard& guard) { debug_stop_noclip(debug); debug.status = guard.message; }
@@ -769,6 +826,11 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
debug.noclip ? "Disable Noclip before using Up Boost." :
!state.velocity_guard_active.load(std::memory_order_acquire) ? "Up Boost is unavailable for this game build." :
"Local skater physics could not be read.";
model.offboard_up_velocity_unavailable = !can_control ? "Waiting for active local controls." :
debug.park_editor ? "Close Park Editor before using Off-board Up Boost." :
debug.noclip ? "Disable Noclip before using Off-board Up Boost." :
!state.velocity_guard_active.load(std::memory_order_acquire) ? "Off-board Up Boost is unavailable for this game build." :
"Local skater physics could not be read.";
try {
(void)debug_skater(base, client, model);
const auto bodies = debug_noclip_bodies(base, client, model.skater_identity);
@@ -800,6 +862,22 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
} else if (debug.up_velocity.valid) {
model.up_velocity_unavailable = "Up Boost is being applied.";
}
if (debug.offboard_up_velocity.valid && (GetTickCount64() >= debug.offboard_up_velocity.expires ||
debug.offboard_up_velocity.entity != model.skater_identity || debug.offboard_up_velocity.core != bodies.core ||
(!debug.offboard_up_velocity.applied && !bodies.offboard && !bodies.wipeout))) {
const bool applied = debug.offboard_up_velocity.applied;
debug.offboard_up_velocity.valid = false;
if (!applied) debug.status = "Off-board Up Boost cancelled because skater physics changed.";
}
if (can_control && !debug.park_editor && !debug.noclip &&
state.velocity_guard_active.load(std::memory_order_acquire) && (bodies.offboard || bodies.wipeout)) {
model.offboard_up_velocity_available = true;
model.offboard_up_velocity_unavailable.clear();
} else if (!bodies.offboard && !bodies.wipeout) {
model.offboard_up_velocity_unavailable = "Off-board Up Boost requires walking, falling or gliding.";
} else if (debug.offboard_up_velocity.valid) {
model.offboard_up_velocity_unavailable = "Off-board Up Boost is being applied.";
}
model.no_bail_available = can_control && update_no_bail(client, model.skater_identity, debug.no_bail && no_bail_allowed,
debug.noclip && debug.noclip_velocity.valid, debug.noclip_velocity.expires);
if (model.camera_available) {
@@ -816,14 +894,16 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
if (model.camera_available) model.noclip_unavailable = guard.message;
model.forward_velocity_unavailable = guard.message;
model.up_velocity_unavailable = guard.message;
model.offboard_up_velocity_unavailable = guard.message;
} catch (...) {
if (model.camera_available) model.noclip_unavailable = "Local skater physics could not be read.";
model.forward_velocity_unavailable = "Local skater physics could not be read.";
model.up_velocity_unavailable = "Local skater physics could not be read.";
model.offboard_up_velocity_unavailable = model.up_velocity_unavailable;
}
if (!session_boosts_allowed()) {
model.forward_velocity_available = model.up_velocity_available = false;
model.forward_velocity_unavailable = model.up_velocity_unavailable = "The host has turned off boosts in this session.";
model.forward_velocity_available = model.up_velocity_available = model.offboard_up_velocity_available = false;
model.forward_velocity_unavailable = model.up_velocity_unavailable = model.offboard_up_velocity_unavailable = "The host has turned off boosts in this session.";
}
if (!model.no_bail_available) {
clear_no_bail();
@@ -850,8 +930,15 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
model.forward_velocity_updates = debug.forward_velocity_updates;
model.up_velocity_speed = debug.up_velocity_speed;
model.up_velocity_updates = debug.up_velocity_updates;
model.offboard_up_velocity_speed = debug.offboard_up_velocity_speed;
model.offboard_up_velocity_updates = debug.offboard_up_velocity_updates;
state.offboard_boost_core.store(debug.offboard_up_velocity.valid ? debug.offboard_up_velocity.core : 0,
std::memory_order_release);
return model;
}
bool client_free_camera_active() noexcept {
return source_state().free_camera_active.load(std::memory_order_acquire);
}
bool restore_client_debug(std::uintptr_t base, std::uintptr_t client, bool camera_phase_observed) {
const overlay::DebugRequest request{overlay::DebugAction::restore_debug};
const auto model = on_client_debug_tick(base, client, false, camera_phase_observed, &request);
+87 -10
View File
@@ -3,6 +3,7 @@
#include "no_bail.h"
#include "offboard_flight.h"
#include "Engine/Core/Hooks/hooks.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/no_bail.h"
#include "Engine/Game/Build/20260929/offboard_flight.h"
@@ -77,6 +78,11 @@ NoclipBodies debug_noclip_bodies(std::uintptr_t base, std::uintptr_t client, std
result.seconds = reader.value<float>(result.context, 0x17ec);
source_require(std::isfinite(result.seconds) && result.seconds >= 0 && result.seconds <= .1f, "Invalid physics timestep.");
result.offboard = reader.pointer(reader.pointer(result.core, 0x3b0)) == base + addr::offboard_flight::offboard_flight_vtable;
// Read the committed native state, not the selector's proposed next state
// (which is remapped by its caller and may be stale between selections).
const auto physics = reader.value<std::uint32_t>(result.context, 0x1414);
result.wipeout = physics == addr::no_bail::wipeout_physics_state ||
physics == addr::no_bail::offboard_physics_state;
source_require(reader.pointer(board) == base + spawn::board_physics_vtable && reader.pointer(rig) == base + spawn::rig_physics_vtable,
"Unsupported board or skeleton physics.");
const auto board_parts = reader.pointer(board, 0x20), rig_parts = reader.pointer(rig, 0x20);
@@ -104,41 +110,76 @@ void noclip_apply_velocity(std::uintptr_t core) noexcept {
state.busy.test_and_set(std::memory_order_acquire)) return;
SourceBusyScope scope{state.busy};
auto& debug = state.trial.debug;
const auto apply_boost = [&](InteractiveDebug::VelocityRequest& boost, std::uint64_t& updates, bool up) {
const auto apply_boost = [&](InteractiveDebug::VelocityRequest& boost, std::uint64_t& updates, bool up, bool offboard = false) {
if (!boost.valid || boost.core != core) return;
try {
if (offboard && boost.applied && GetTickCount64() >= boost.expires) {
boost.valid = false;
state.offboard_boost_core.store(0, std::memory_order_release);
return;
}
source_require(GetTickCount64() < boost.expires, "Velocity boost timed out.");
const auto bodies = debug_noclip_bodies(state.trial.base, boost.client, boost.entity);
source_require(bodies.core == core, "Skater physics was replaced before the velocity boost could apply.");
source_require(!bodies.offboard, "Velocity boosts require the skater to be on the board.");
source_require(offboard ? (boost.applied || bodies.offboard || bodies.wipeout) : !bodies.offboard,
offboard ? "Off-board Up Boost requires walking, falling or gliding." :
"Velocity boosts require the skater to be on the board.");
source_require(!debug.noclip && !debug.park_editor, "Velocity boost cancelled while editing or flying.");
SourceReader reader;
source_require(reader.value<std::uint8_t>(boost.entity, 0x7e0) == 0,
"Wait for the current teleport before using velocity boosts.");
std::array<std::array<float, 3>, 32> velocities{};
std::array<std::uint32_t, 32> flags{};
for (std::size_t i = 0; i < bodies.parts.size(); ++i) {
const std::size_t first = offboard ? 9 : 0; // Leave the carried/detached board alone.
if (offboard && !boost.applied) {
const auto current_up = reader.value<float>(bodies.parts[9], 0x74);
boost.velocity[1] = std::max(boost.velocity[1], current_up + boost.velocity[1]);
}
for (std::size_t i = first; i < bodies.parts.size(); ++i) {
velocities[i] = reader.value<std::array<float, 3>>(bodies.parts[i], 0x70);
flags[i] = reader.value<std::uint32_t>(bodies.parts[i], 0x60);
for (std::size_t axis = 0; axis < 3; ++axis) {
velocities[i][axis] += boost.velocity[axis];
if (offboard && axis == 1) velocities[i][axis] = std::max(velocities[i][axis], boost.velocity[1]);
else velocities[i][axis] += boost.velocity[axis];
source_require(std::isfinite(velocities[i][axis]) && std::abs(velocities[i][axis]) <= 100000,
"Velocity boost produced an invalid physics velocity.");
}
}
reader.verify();
boost.valid = false;
for (std::size_t i = 0; i < bodies.parts.size(); ++i) {
if (offboard && bodies.offboard)
source_require(ensure_offboard_up_velocity(state.trial.base, core, bodies.context,
bodies.rig_wrapper, boost.velocity[1]), "Off-board boost motion ownership changed.");
for (std::size_t i = first; i < bodies.parts.size(); ++i) {
body_write(bodies.parts[i] + 0x70, velocities[i]);
body_write(bodies.parts[i] + 0x60, flags[i] | 8u);
}
++updates;
debug.status = up ? "Up velocity added." : "Forward velocity added.";
} catch (const SourceGuard& issue) { boost.valid = false; debug.status = issue.message; }
catch (...) { boost.valid = false; debug.status = "Velocity boost failed during the physics update."; }
if (offboard) {
if (!boost.applied) {
++updates;
boost.applied = true;
boost.expires = GetTickCount64() + 180;
debug.status = "Off-board up velocity added.";
}
} else {
boost.valid = false;
++updates;
debug.status = up ? "Up velocity added." : "Forward velocity added.";
}
} catch (const SourceGuard& issue) {
boost.valid = false;
if (offboard) state.offboard_boost_core.store(0, std::memory_order_release);
debug.status = issue.message;
if (offboard) logging::write(logging::Level::warning, logging::Channel::skater, issue.message);
} catch (...) {
boost.valid = false;
if (offboard) state.offboard_boost_core.store(0, std::memory_order_release);
debug.status = "Velocity boost failed during the physics update.";
if (offboard) logging::write(logging::Level::warning, logging::Channel::skater, debug.status);
}
};
apply_boost(debug.forward_velocity, debug.forward_velocity_updates, false);
apply_boost(debug.up_velocity, debug.up_velocity_updates, true);
apply_boost(debug.offboard_up_velocity, debug.offboard_up_velocity_updates, true, true);
const auto& request = debug.noclip_velocity;
if (!debug.noclip || !request.valid || request.core != core) return;
try {
@@ -318,6 +359,36 @@ bool noclip_motion_target(std::uintptr_t rig, std::uintptr_t context,
state.busy.test_and_set(std::memory_order_acquire)) return false;
SourceBusyScope scope{state.busy};
auto& debug = state.trial.debug;
auto& boost = debug.offboard_up_velocity;
if (!debug.noclip && boost.valid && boost.applied) {
try {
SourceReader reader;
if (reader.pointer(boost.core, 0x438) != rig || reader.pointer(boost.core, 0x3c0) != context)
return false;
if (GetTickCount64() >= boost.expires) return false;
const auto bodies = debug_noclip_bodies(state.trial.base, boost.client, boost.entity);
source_require(bodies.core == boost.core && bodies.rig_wrapper == rig && bodies.context == context,
"Off-board boost skater changed.");
source_require(!debug.park_editor && reader.value<std::uint8_t>(boost.entity, 0x7e0) == 0,
"Off-board boost interrupted by editing or teleporting.");
source_require(reader.raw(reinterpret_cast<std::uintptr_t>(supplied), target.data(), sizeof(target)) &&
valid_flight_transform(target), "Invalid off-board motion target.");
// Native off-board movement drives the skeleton toward this target.
// Body velocity alone is undone by that drive on the same step.
// Lead only Y for the launch; keep native X/Z, rotation and collision flags.
target[13] = std::max(target[13], bodies.root[1] + boost.velocity[1] * bodies.seconds);
source_require(valid_flight_transform(target), "Off-board boost target is out of bounds.");
reader.verify();
if (bodies.offboard)
source_require(ensure_offboard_up_velocity(state.trial.base, bodies.core, context, rig, boost.velocity[1]),
"Off-board boost motion ownership changed.");
return bodies.seconds > 0;
} catch (const SourceGuard& issue) { debug.status = issue.message; }
catch (...) { debug.status = "Off-board boost motion failed."; }
boost.valid = false;
state.offboard_boost_core.store(0, std::memory_order_release);
return false;
}
const auto& request = debug.noclip_velocity;
if (!debug.noclip || !request.valid) return false;
try {
@@ -364,6 +435,12 @@ void noclip_skater_motion(std::uintptr_t rig, std::uintptr_t context,
alignas(16) std::array<float,16> target{};
const auto* motion = noclip_motion_target(rig, context, supplied, target) ? &target : supplied;
if (original) original(rig, context, motion, flags);
if (source_state().offboard_boost_core.load(std::memory_order_acquire)) {
std::uintptr_t core{};
if (memory::peek(rig + 0x4630, core) &&
core == source_state().offboard_boost_core.load(std::memory_order_acquire))
noclip_apply_velocity(core);
}
}
}
}
+1
View File
@@ -30,6 +30,7 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
bool can_control, bool camera_phase_observed, const overlay::DebugRequest* request = nullptr,
const overlay::FlightInput* flight_input = nullptr);
bool restore_client_debug(std::uintptr_t base, std::uintptr_t client, bool camera_phase_observed);
bool client_free_camera_active() noexcept;
// Native camera lease for the party's Spectate action. Null restores only the
// camera acquired by this function; it never changes another debug camera. A positive
// `fov` is applied to the spectate camera while it is held; its own comes back after.
@@ -123,14 +123,18 @@ struct InteractiveDebug {
ULONGLONG expires{};
std::array<float, 3> velocity{};
bool valid{};
bool applied{};
} noclip_velocity;
VelocityRequest forward_velocity;
VelocityRequest up_velocity;
VelocityRequest offboard_up_velocity;
std::uint64_t noclip_velocity_updates{}, noclip_motion_updates{};
std::uint64_t forward_velocity_updates{};
float forward_velocity_speed = 20.0f;
std::uint64_t up_velocity_updates{};
std::uint64_t offboard_up_velocity_updates{};
float up_velocity_speed = 20.0f;
float offboard_up_velocity_speed = 20.0f;
float noclip_altitude{};
bool noclip_altitude_valid{}, noclip_altitude_offboard{};
// Player choices kept in the local profile (see load_saved_debug):
@@ -158,6 +162,8 @@ struct SourceState {
std::atomic_flag busy = ATOMIC_FLAG_INIT;
bool velocity_guard_attempted{}; // Protected by initialization_mutex.
std::atomic<bool> velocity_guard_active{};
std::atomic<bool> free_camera_active{};
std::atomic<std::uintptr_t> offboard_boost_core{};
std::atomic<SourcePhysicsUpdate> velocity_update_original{};
std::atomic<SourceSkaterMotion> motion_original{};
SourceTrial trial;
@@ -172,6 +178,7 @@ struct NoclipBodies {
std::uintptr_t core{}, context{}, rig_wrapper{};
float seconds{}, board_height{};
bool offboard{};
bool wipeout{}; // Spread-eagle/torpedo use the native wipeout physics path.
std::array<float, 3> root{}; // Entity root: the camera target and the idle motion target.
std::array<std::uintptr_t, 32> parts{}; // Board 0..8, native skeleton velocity parts 1..23.
};
+70 -4
View File
@@ -135,11 +135,67 @@ void register_movement_commands(Commands &registry) {
request_debug(Debug::add_forward_velocity);
};
registry.add(std::move(boost));
auto mask = argument("controller_mask", Type::unsigned_integer);
auto mask = argument("button_or_key", Type::unsigned_integer);
mask.minimum = 0;
mask.maximum = UINT32_MAX;
auto freecamcontroller =
action("freecam_controller", "Block input and use controller for Freecam", Group::movement, {argument("enabled", Type::boolean)});
freecamcontroller.inspect = [](const Model &m) {
return boolean_state(m.bindings.available, m.bindings.freecam_controller, "Controller bindings are unavailable.");
};
freecamcontroller.run = [](const Model &, const Values &args, const Output &out) {
const bool saved = set_local_freecam_controller(std::get<bool>(args[0]));
out(saved ? "" : "error: Invalid setting or save failed.");
};
registry.add(std::move(freecamcontroller));
auto freecamcontroller_bind =
action("bind freecamcontroller", "Bind a controller combo or keyboard key to freecam controller toggle; 0 clears the binding", Group::movement, {mask});
freecamcontroller_bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
freecamcontroller_bind.run = [](const Model &, const Values &args, const Output &out) {
const bool saved = set_local_freecam_controller_binding(static_cast<std::uint32_t>(std::get<std::uint64_t>(args[0])));
out(saved ? local_profile_controller_bindings().status : "error: Invalid binding or save failed.");
};
registry.add(std::move(freecamcontroller_bind));
auto tp_to_freecam =
action("tp_to_freecam", "Teleport to freecam position and disable freecam", Group::movement);
tp_to_freecam.inspect = [](const Model &m) {
return boolean_state(m.debug.camera_available && m.debug.camera_position_valid, m.debug.free_camera,
!m.debug.camera_position_valid ? "Camera position unavailable" : "Camera controls unavailable");
};
tp_to_freecam.run = [](const Model &m, const Values &, const Output &) {
if (!m.debug.camera_position_valid) return;
teleport_local_skater(m.debug.camera_position);
if (m.debug.free_camera)
request_debug(overlay::DebugAction::set_free_camera, false);
};
registry.add(std::move(tp_to_freecam));
auto tp_to_freecam_bind =
action("bind tptofreecam", "Bind a controller combo or keyboard key to TP to Freecam; 0 clears the binding", Group::movement, {mask});
tp_to_freecam_bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
tp_to_freecam_bind.run = [](const Model &, const Values &args, const Output &out) {
const bool saved = set_local_tp_to_freecam_binding(static_cast<std::uint32_t>(std::get<std::uint64_t>(args[0])));
out(saved ? local_profile_controller_bindings().status : "error: Invalid binding or save failed.");
};
registry.add(std::move(tp_to_freecam_bind));
auto freecam_bind =
action("bind freecam", "Bind a controller combo or keyboard key to freecam; 0 clears the binding", Group::movement, {mask});
freecam_bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
freecam_bind.run = [](const Model &, const Values &args, const Output &out) {
const bool saved = set_local_freecam_binding(static_cast<std::uint32_t>(std::get<std::uint64_t>(args[0])));
out(saved ? local_profile_controller_bindings().status : "error: Invalid binding or save failed.");
};
registry.add(std::move(freecam_bind));
auto bind =
action("bind noclip", "Bind controller buttons to noclip; 0 clears the binding", Group::movement, {mask});
action("bind noclip", "Bind a controller combo or keyboard key to noclip; 0 clears the binding", Group::movement, {mask});
bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
@@ -149,7 +205,7 @@ void register_movement_commands(Commands &registry) {
};
registry.add(std::move(bind));
auto boost_bind =
action("bind forwardvelocity", "Bind controller buttons to Forward Boost; 0 clears the binding", Group::movement, {mask});
action("bind forwardvelocity", "Bind a controller combo or keyboard key to Forward Boost; 0 clears the binding", Group::movement, {mask});
boost_bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
@@ -159,7 +215,7 @@ void register_movement_commands(Commands &registry) {
};
registry.add(std::move(boost_bind));
auto up_bind =
action("bind upvelocity", "Bind controller buttons to Up Boost; 0 clears the binding", Group::movement, {mask});
action("bind upvelocity", "Bind a controller combo or keyboard key to Up Boost; 0 clears the binding", Group::movement, {mask});
up_bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
@@ -168,6 +224,16 @@ void register_movement_commands(Commands &registry) {
out(saved ? local_profile_controller_bindings().status : "error: Invalid binding or save failed.");
};
registry.add(std::move(up_bind));
auto offboard_up_bind =
action("bind offboardupvelocity", "Bind a controller combo or keyboard key to Off-board Up Boost; 0 clears the binding", Group::movement, {mask});
offboard_up_bind.inspect = [](const Model &m) {
return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false};
};
offboard_up_bind.run = [](const Model &, const Values &args, const Output &out) {
const bool saved = set_local_offboard_up_velocity_binding(static_cast<std::uint32_t>(std::get<std::uint64_t>(args[0])));
out(saved ? local_profile_controller_bindings().status : "error: Invalid binding or save failed.");
};
registry.add(std::move(offboard_up_bind));
}
void register_ai_commands(Commands &registry) {
const auto ready = [](const Model &m) {
+48
View File
@@ -4,6 +4,7 @@
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/no_bail.h"
#include "Engine/Game/Build/20260929/offboard_flight.h"
#include <algorithm>
#include <cmath>
namespace dingosdk {
@@ -33,6 +34,11 @@ bool offboard_flight_compatible(std::uintptr_t base) noexcept {
if (link(base + state.vtable_rva, 0x18) != base + state.update_rva ||
!memory::read(base + state.velocity_write.rva, bytes) || bytes != state.velocity_write.bytes) return false;
}
for (const auto& state : offboard_ragdoll_states)
if (link(base + state[1], 0x18) != base + state[2]) return false;
std::array<unsigned char, 32> ragdoll_bytes{};
if (!memory::read(base + ragdoll_velocity_write.rva, ragdoll_bytes) ||
ragdoll_bytes != ragdoll_velocity_write.bytes) return false;
return true;
}
bool scale_offboard_up_velocity(std::uintptr_t base, std::uintptr_t core, std::uintptr_t context,
@@ -55,6 +61,48 @@ bool scale_offboard_up_velocity(std::uintptr_t base, std::uintptr_t core, std::u
}
return false;
}
bool ensure_offboard_up_velocity(std::uintptr_t base, std::uintptr_t core, std::uintptr_t context,
std::uintptr_t rig, float minimum_up_speed) noexcept {
FlightLastError error;
if (!object(base) || !object(context) || !object(rig) || !std::isfinite(minimum_up_speed) ||
minimum_up_speed <= 0 || minimum_up_speed > 100000 ||
link(core) != base + addr::no_bail::bail_core_vtable || link(core, 0x3c0) != context || link(core, 0x438) != rig)
return false;
const auto parent = link(core, 0x3b0);
if (link(parent) != base + offboard_flight_vtable || link(parent, 8) != context ||
link(parent, 0x10) != link(core, 0x430) || link(parent, 0x18) != rig ||
link(rig) != context || link(rig, 0x4630) != core) return false;
const auto active = link(parent, 0x48);
for (const auto& state : offboard_flight_states) {
if (active != parent + state.offset) continue;
std::array<float, 3> velocity{};
if (link(active) != base + state.vtable_rva || !memory::read(active + 0x10, velocity)) return false;
velocity[1] = std::max(velocity[1], minimum_up_speed);
return sync_offboard_flight_velocity(base, core, context, rig, velocity);
}
for (const auto& state : offboard_ragdoll_states) {
if (active != parent + state[0]) continue;
if (link(active) != base + state[1]) return false;
float vertical{};
const auto address = active + 0x14;
if (!memory::read(address, vertical) || !std::isfinite(vertical) || std::abs(vertical) > 100000)
return false;
// Replace a fall with the positive launch speed, rather than adding
// an impulse to a cached negative velocity. Preserve horizontal motion.
const float launched = std::max(vertical, minimum_up_speed);
MEMORY_BASIC_INFORMATION page{};
if (!VirtualQuery(reinterpret_cast<const void*>(address), &page, sizeof(page)) ||
page.State != MEM_COMMIT || (page.Protect & (PAGE_GUARD | PAGE_NOACCESS)) ||
!(page.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_WRITECOPY)))
return false;
SIZE_T written{};
return WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(address), &launched,
sizeof(launched), &written) && written == sizeof(launched) &&
memory::read(address, vertical) && vertical == launched;
}
// Scripted animation/trajectory states have different motion rules.
return true;
}
bool sync_offboard_flight_velocity(std::uintptr_t base, std::uintptr_t core,
std::uintptr_t context, std::uintptr_t rig, const std::array<float, 3>& velocity) noexcept {
FlightLastError error;
+5 -2
View File
@@ -3,9 +3,12 @@
#include <cstdint>
namespace dingosdk {
// Keep a short boost's minimum rise speed through native off-board motion updates.
bool ensure_offboard_up_velocity(std::uintptr_t base, std::uintptr_t core, std::uintptr_t context,
std::uintptr_t rig, float minimum_up_speed) noexcept;
bool offboard_flight_compatible(std::uintptr_t base) noexcept;
// Only call during a freshly validated local noclip motion update, with live
// input. Synchronize the active falling/ground state, preserving its fourth lane.
// Only call during a freshly validated local motion/physics update.
// Synchronize the active falling/ground state, preserving its fourth lane.
bool sync_offboard_flight_velocity(std::uintptr_t base, std::uintptr_t core,
std::uintptr_t context, std::uintptr_t rig, const std::array<float, 3>& velocity) noexcept;
// Multiplies the upward part of the active off-board state's velocity, once (a jump that has
+167 -2
View File
@@ -2,6 +2,8 @@
#include "overlay_internal.h"
#include "input_capture.h"
#include "held_input.h"
#include "Engine/Game/Input/playstation_report.h"
#include <hidsdi.h>
#include <atomic>
#include <format>
#include <array>
@@ -19,7 +21,7 @@ thread_local unsigned overlay_input_access = 0;
thread_local HRAWINPUT noted_raw_input = nullptr;
bool block_polled_input() {
const auto error = GetLastError();
const bool capture = !overlay_input_access && owns_menu_cursor(state());
const bool capture = !overlay_input_access && (owns_menu_cursor(state()) || state().freecam_controller_active.load(std::memory_order_relaxed));
SetLastError(error);
return capture;
}
@@ -28,6 +30,11 @@ void note_input_record(const RAWINPUT& record) noexcept;
HeldInput& held_input() { static auto* value = new HeldInput; return *value; }
struct InputCaptureHooks {
Hook hid_attributes, read_file, overlapped, overlapped_ex, completion, completion_ex, close_handle;
struct HidRead { HANDLE file; void* buffer; DWORD capacity; PlayStationPad kind; };
std::mutex hid_mutex;
std::map<HANDLE, PlayStationPad> hid_devices;
std::map<OVERLAPPED*, HidRead> hid_reads;
Hook register_raw;
Hook async_key, key, keyboard, raw_data, raw_buffer, capture, release_capture, cursor, show_cursor, physical_cursor, cursor_position;
std::array<Hook, 5> xinput_state, xinput_extended, xinput_keystroke;
@@ -43,6 +50,139 @@ struct InputCaptureHooks {
};
InputCaptureHooks& input_hooks() { static auto* hooks = new InputCaptureHooks; return *hooks; }
BOOLEAN __stdcall captured_hid_attributes(HANDLE file, PHIDD_ATTRIBUTES attributes) {
const auto result = original<decltype(&HidD_GetAttributes)>(input_hooks().hid_attributes)(file, attributes);
const auto error = GetLastError();
if (result && attributes) {
const auto kind = playstation_pad(attributes->VendorID, attributes->ProductID);
if (kind != PlayStationPad::none) {
std::lock_guard lock(input_hooks().hid_mutex);
input_hooks().hid_devices[file] = kind;
}
}
SetLastError(error);
return result;
}
void neutral_hid(const InputCaptureHooks::HidRead& read, DWORD size) {
if (!block_polled_input() || !read.buffer || size > read.capacity) return;
auto* bytes = static_cast<std::uint8_t*>(read.buffer);
if (!parse_playstation_report(read.kind, playstation_bluetooth(read.capacity), bytes, size)) return;
const bool sense_full = read.kind == PlayStationPad::dualsense &&
(bytes[0] == 0x31 || (bytes[0] == 1 && !playstation_bluetooth(read.capacity)));
const unsigned offset = bytes[0] == 0x31 ? 2 : bytes[0] == 0x11 ? 3 : 1;
auto* data = bytes + offset;
std::fill_n(data, 4, std::uint8_t{128});
if (sense_full) {
data[4] = data[5] = 0; // triggers; retain report sequence at +6
data[7] = 8; data[8] = data[9] = 0;
} else {
data[4] = 8; data[5] = 0; data[6] &= 0xfc; // retain DS4 sequence bits
data[7] = data[8] = 0;
}
// Full Bluetooth reports have a CRC over the 0xA1 input prefix and payload.
// A stale checksum makes the native reader discard the neutral report.
if (size == 78 && (bytes[0] == 0x31 || bytes[0] == 0x11)) {
std::uint32_t crc = 0xffffffffu;
const auto feed = [&](std::uint8_t byte) {
crc ^= byte;
for (unsigned bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u)));
};
feed(0xa1);
for (DWORD i = 0; i < size - 4; ++i) feed(bytes[i]);
crc = ~crc;
std::memcpy(bytes + size - 4, &crc, sizeof(crc));
}
static std::atomic<bool> reported{};
if (!reported.exchange(true)) logging::write(logging::Level::info, logging::Channel::input,
"Native PlayStation HID report neutralized; overlay retains its independent input stream.");
}
void complete_hid(OVERLAPPED* request, DWORD bytes, bool success, bool finished) {
auto& h = input_hooks();
InputCaptureHooks::HidRead read{};
{
std::lock_guard lock(h.hid_mutex);
const auto found = h.hid_reads.find(request);
if (found == h.hid_reads.end()) return;
read = found->second;
if (finished) h.hid_reads.erase(found);
}
if (success) neutral_hid(read, bytes);
}
BOOL WINAPI captured_read_file(HANDLE file, LPVOID buffer, DWORD size, LPDWORD bytes, LPOVERLAPPED request) {
auto& h = input_hooks();
InputCaptureHooks::HidRead read{file, buffer, size, PlayStationPad::none};
if (!overlay_input_access) {
// Some readers inspect a temporary enumeration handle, then open a
// second handle for reports. Identify that handle as well.
bool known{};
{
std::lock_guard lock(h.hid_mutex);
known = h.hid_devices.contains(file);
}
if (!known && size >= 10 && size <= 512) {
const auto error = GetLastError();
HIDD_ATTRIBUTES attributes{sizeof(attributes)};
const auto okay = original<decltype(&HidD_GetAttributes)>(h.hid_attributes)(file, &attributes);
const auto kind = okay ? playstation_pad(attributes.VendorID, attributes.ProductID) : PlayStationPad::none;
{
std::lock_guard lock(h.hid_mutex);
h.hid_devices[file] = kind;
}
SetLastError(error);
}
std::lock_guard lock(h.hid_mutex);
const auto found = h.hid_devices.find(file);
if (found != h.hid_devices.end() && found->second != PlayStationPad::none) {
read.kind = found->second;
if (request) h.hid_reads[request] = read;
}
}
const auto result = original<decltype(&ReadFile)>(h.read_file)(file, buffer, size, bytes, request);
const auto error = GetLastError();
if (read.kind != PlayStationPad::none) {
if (result) neutral_hid(read, bytes ? *bytes : request ? static_cast<DWORD>(request->InternalHigh) : 0);
if (!result && error != ERROR_IO_PENDING && request) complete_hid(request, 0, false, true);
}
SetLastError(error);
return result;
}
BOOL WINAPI captured_overlapped(HANDLE file, LPOVERLAPPED request, LPDWORD bytes, BOOL wait) {
const auto result = original<decltype(&GetOverlappedResult)>(input_hooks().overlapped)(file, request, bytes, wait);
const auto error = GetLastError();
complete_hid(request, result && bytes ? *bytes : 0, result != FALSE, result || error != ERROR_IO_INCOMPLETE);
SetLastError(error); return result;
}
BOOL WINAPI captured_overlapped_ex(HANDLE file, LPOVERLAPPED request, LPDWORD bytes, DWORD timeout, BOOL alertable) {
const auto result = original<decltype(&GetOverlappedResultEx)>(input_hooks().overlapped_ex)(file, request, bytes, timeout, alertable);
const auto error = GetLastError();
complete_hid(request, result && bytes ? *bytes : 0, result != FALSE,
result || (error != ERROR_IO_INCOMPLETE && error != WAIT_TIMEOUT && error != WAIT_IO_COMPLETION));
SetLastError(error); return result;
}
BOOL WINAPI captured_completion(HANDLE port, LPDWORD bytes, PULONG_PTR key, LPOVERLAPPED* request, DWORD timeout) {
const auto result = original<decltype(&GetQueuedCompletionStatus)>(input_hooks().completion)(port, bytes, key, request, timeout);
const auto error = GetLastError();
if (request && *request) complete_hid(*request, result && bytes ? *bytes : 0, result != FALSE, true);
SetLastError(error); return result;
}
BOOL WINAPI captured_completion_ex(HANDLE port, LPOVERLAPPED_ENTRY entries, ULONG count, PULONG removed, DWORD timeout, BOOL alertable) {
const auto result = original<decltype(&GetQueuedCompletionStatusEx)>(input_hooks().completion_ex)(port, entries, count, removed, timeout, alertable);
const auto error = GetLastError();
if (result && entries && removed) for (ULONG i = 0; i < *removed; ++i)
complete_hid(entries[i].lpOverlapped, entries[i].dwNumberOfBytesTransferred,
static_cast<LONG>(entries[i].Internal) >= 0, true);
SetLastError(error); return result;
}
BOOL WINAPI captured_close_handle(HANDLE file) {
auto& h = input_hooks();
{
std::lock_guard lock(h.hid_mutex);
if (h.hid_devices.erase(file))
std::erase_if(h.hid_reads, [&](const auto& entry) { return entry.second.file == file; });
}
return original<decltype(&CloseHandle)>(h.close_handle)(file);
}
SHORT WINAPI captured_async_key(int key) {
const auto value = original<SHORT(WINAPI*)(int)>(input_hooks().async_key)(key);
return block_polled_input() ? 0 : value;
@@ -354,6 +494,7 @@ HRESULT STDMETHODCALLTYPE captured_device_state(void* device, DWORD size, LPVOID
DWORD neutral = 0;
if (object.dwType & DIDFT_POV) neutral = 0xffffffffu;
else if (object.dwType & DIDFT_ABSAXIS) {
neutral = 32767; // Standard DirectInput axis midpoint if range lookup fails.
DIPROPRANGE range{};
range.diph = {sizeof(range), sizeof(range.diph), object.dwOfs, DIPH_BYOFFSET};
auto** vtable = *reinterpret_cast<void***>(device);
@@ -363,6 +504,17 @@ HRESULT STDMETHODCALLTYPE captured_device_state(void* device, DWORD size, LPVOID
} else continue;
std::memcpy(static_cast<std::byte*>(data) + object.dwOfs, &neutral, 4);
}
} else if (size == sizeof(DIJOYSTATE) || size == sizeof(DIJOYSTATE2)) {
// A zeroed DirectInput axis is full travel, not the center. Some
// controllers use the standard joystick layout without a format
// record we can inspect; keep those pads neutral while captured.
auto* bytes = static_cast<std::byte*>(data);
const DWORD center = 32767;
const DWORD released_pov = 0xffffffffu;
for (std::size_t offset = 0; offset < 8 * sizeof(DWORD); offset += sizeof(DWORD))
std::memcpy(bytes + offset, &center, sizeof(center));
for (std::size_t offset = 8 * sizeof(DWORD); offset < 12 * sizeof(DWORD); offset += sizeof(DWORD))
std::memcpy(bytes + offset, &released_pov, sizeof(released_pov));
}
}
return result;
@@ -523,6 +675,19 @@ void sync_raw_mouse_registration(bool overlay_owns_pointer) {
bool install_input_capture() {
auto& h = input_hooks();
const auto hid = LoadLibraryExW(L"hid.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
const auto kernel = GetModuleHandleW(L"kernel32.dll");
if (!hid || !install(h.hid_attributes, reinterpret_cast<void*>(GetProcAddress(hid, "HidD_GetAttributes")),
reinterpret_cast<void*>(captured_hid_attributes))) return false;
const auto kernel_hook = [&](Hook& hook, const char* name, void* detour) {
return install(hook, reinterpret_cast<void*>(GetProcAddress(kernel, name)), detour);
};
if (!kernel_hook(h.read_file, "ReadFile", reinterpret_cast<void*>(captured_read_file)) ||
!kernel_hook(h.overlapped, "GetOverlappedResult", reinterpret_cast<void*>(captured_overlapped)) ||
!kernel_hook(h.overlapped_ex, "GetOverlappedResultEx", reinterpret_cast<void*>(captured_overlapped_ex)) ||
!kernel_hook(h.completion, "GetQueuedCompletionStatus", reinterpret_cast<void*>(captured_completion)) ||
!kernel_hook(h.completion_ex, "GetQueuedCompletionStatusEx", reinterpret_cast<void*>(captured_completion_ex)) ||
!kernel_hook(h.close_handle, "CloseHandle", reinterpret_cast<void*>(captured_close_handle))) return false;
const auto user32 = GetModuleHandleW(L"user32.dll");
const auto add = [&](Hook& hook, const char* name, void* detour) {
const bool okay = install(hook, reinterpret_cast<void*>(GetProcAddress(user32, name)), detour);
@@ -575,7 +740,7 @@ bool install_input_capture() {
if (!install(h.direct_create, reinterpret_cast<void*>(GetProcAddress(direct, "DirectInput8Create")),
reinterpret_cast<void*>(captured_direct_create))) return false;
}
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::input, "Menu input capture installed: keyboard, raw input, XInput, DirectInput and cursor ownership.");
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::input, "Menu input capture installed: keyboard, raw input, XInput, DirectInput, PlayStation HID and cursor ownership.");
return true;
}
+7
View File
@@ -52,6 +52,8 @@ enum class DebugAction {
set_park_editor,
add_up_velocity,
set_up_velocity_speed,
add_offboard_up_velocity,
set_offboard_up_velocity_speed,
set_first_person,
set_first_person_fov,
set_first_person_spring,
@@ -100,20 +102,24 @@ struct DebugModel {
bool noclip = false, noclip_available = false;
bool forward_velocity_available = false;
bool up_velocity_available = false;
bool offboard_up_velocity_available = false;
std::string camera_unavailable = "Waiting for local controls.";
std::string noclip_unavailable = "Waiting for local controls.";
std::string forward_velocity_unavailable = "Waiting for local controls.";
std::string up_velocity_unavailable = "Waiting for local controls.";
std::string offboard_up_velocity_unavailable = "Waiting for local controls.";
bool no_bail = false, no_bail_available = false, no_bail_active = false;
std::uint64_t noclip_velocity_updates = 0, noclip_motion_updates = 0;
std::uint64_t forward_velocity_updates = 0;
std::uint64_t up_velocity_updates = 0;
std::uint64_t offboard_up_velocity_updates = 0;
bool game_ui_hidden = false;
bool settings_owned = false;
// ReSkate free-flight speed in world units per second.
float camera_speed = 15.0f;
float forward_velocity_speed = 20.0f;
float up_velocity_speed = 20.0f;
float offboard_up_velocity_speed = 20.0f;
bool camera_position_valid = false;
bool skater_position_valid = false;
std::array<float, 3> camera_position{};
@@ -391,6 +397,7 @@ extern "C" __declspec(dllexport) void DingoSDKOverlayGetStatus(dingosdk::overlay
// Poll only on the client update thread. Returns no input while either overlay
// surface is open, the game is unfocused, or the overlay has stopped.
extern "C" void DingoSDKOverlayReadFlightInput(dingosdk::overlay::FlightInput* input, bool flight_active, bool player_flight = false);
extern "C" void DingoSDKOverlaySetFreecamInputCapture(bool active);
// allow_menu is used only by the Binds page while recording. Focus and lifetime
// gates still apply. Gameplay callers leave it false.
extern "C" void DingoSDKOverlayReadControllerInput(dingosdk::ControllerInput* input, bool allow_menu = false);
+31 -2
View File
@@ -241,7 +241,9 @@ LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
}
}
const bool capture = owns_pointer();
if (capture && is_input(message)) {
const bool freecam_capture = s.freecam_controller_active.load(std::memory_order_relaxed);
if (freecam_capture) release_game_buttons(window, previous);
if ((capture || freecam_capture) && is_input(message)) {
// Foreground raw-input packets still need DefWindowProc cleanup.
return message == WM_INPUT ? DefWindowProcW(window, message, wp, lp) : 0;
}
@@ -390,11 +392,27 @@ dingosdk::overlay::FlightInput read_player_flight_controller() {
const auto sample = read_controller_sample();
if (!sample.device) return {};
const auto& pad = sample.pad;
return dingosdk::controller_flight_input(pad.sThumbLX, pad.sThumbLY, pad.bLeftTrigger, pad.bRightTrigger,
auto input = dingosdk::controller_flight_input(pad.sThumbLX, pad.sThumbLY, pad.bLeftTrigger, pad.bRightTrigger,
(pad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0);
const float rx = static_cast<float>(pad.sThumbRX), ry = static_cast<float>(pad.sThumbRY);
const float magnitude = std::sqrt(rx * rx + ry * ry);
constexpr float deadzone = 8689.0f;
if (magnitude > deadzone) {
const float scale = std::clamp((magnitude - deadzone) / (32767.0f - deadzone), 0.0f, 1.0f) / magnitude;
input.look_x = rx * scale * 25.0f;
input.look_y = ry * scale * -25.0f;
}
return input;
}
}
extern "C" void DingoSDKOverlaySetFreecamInputCapture(bool active) {
const bool previous = state().freecam_controller_active.exchange(active, std::memory_order_relaxed);
if (previous != active)
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::input,
"Freecam controller input capture %s.", active ? "enabled" : "disabled");
}
extern "C" void DingoSDKOverlayReadControllerInput(dingosdk::ControllerInput* output, bool allow_menu) {
if (!output) return;
struct PreserveError { DWORD value = GetLastError(); ~PreserveError() { SetLastError(value); } } preserve_error;
@@ -403,6 +421,15 @@ extern "C" void DingoSDKOverlayReadControllerInput(dingosdk::ControllerInput* ou
const HWND window = s.window.load();
if (!window || s.stop.load() || s.failed.load() || (!allow_menu && interactive_visible(s)) ||
!game_window_foreground(window)) return;
// Read through the overlay's bypass: freecam capture hides keys from the game.
{
OverlayInputAccess access;
for (unsigned key = 0; key < 256; ++key) {
if (dingosdk::bindable_keyboard_key(key) && (GetAsyncKeyState(key) & 0x8000)) {
output->keys[key / 64] |= std::uint64_t{1} << (key % 64);
}
}
}
const auto sample = read_controller_sample();
output->style = sample.style;
if (!sample.device) return;
@@ -455,6 +482,8 @@ extern "C" void DingoSDKOverlayReadFlightInput(dingosdk::overlay::FlightInput* o
output->forward = std::clamp(output->forward + controller.forward, -1.0f, 1.0f);
output->up = std::clamp(output->up + controller.up, -1.0f, 1.0f);
output->boost = output->boost || controller.boost;
output->look_x = controller.look_x;
output->look_y = controller.look_y;
looking = false;
return; // Native camera owns look input; do not sample or recenter the cursor.
}
+1
View File
@@ -138,6 +138,7 @@ struct State {
std::atomic<bool> failed{false};
bool win32_ready = false;
bool dx12_ready = false;
std::atomic<bool> freecam_controller_active{false};
bool show_on_ready = false;
bool force_windowed = false;
HANDLE stop_event = nullptr;
-1
View File
@@ -380,7 +380,6 @@ void draw_menu() {
// than copying every retained log string a second time each poll.
ingest_console_log(std::move(next.console_log));
s.model = std::move(next);
}
}
// Nothing but the chat: the held model may be old, so it must not reopen the editor.
@@ -1,4 +1,5 @@
#include "playstation_input.h"
#include "input_capture.h"
#include "Engine/Core/Log/logging.h"
#include <Windows.h>
#include <cfgmgr32.h>
@@ -96,6 +97,7 @@ std::unique_ptr<Pad> open_pad(const std::wstring& path) {
// Drains every queued report so the newest one wins. Returns false once the
// device is gone.
bool drain(Pad& pad) {
OverlayInputAccess access; // This independent HID stream belongs to freecam/menu.
for (int reports = 0; reports < 256; ++reports) {
if (!pad.pending) {
ResetEvent(pad.event);
+15 -12
View File
@@ -67,12 +67,12 @@ void ui_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks)
}
void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "controller-binds", "CONTROLLER BINDS");
begin_card(menu, "controller-binds", "ACTION BINDS");
const bool available = model.bindings.available && callbacks.queue_console_command;
const auto save = [&](int action, std::uint32_t combo) {
std::array<char, 512> result{};
const auto command = std::string("bind ") +
(action == 1 ? "noclip " : action == 2 ? "forwardvelocity " : "upvelocity ") + std::to_string(combo);
(action == 1 ? "freecamcontroller " : action == 2 ? "freecam " : action == 3 ? "noclip " : action == 4 ? "forwardvelocity " : action == 5 ? "upvelocity " : action == 6 ? "tptofreecam " : "offboardupvelocity ") + std::to_string(combo);
const bool queued = callbacks.queue_console_command(callbacks.user, command.c_str(), result.data(), result.size());
result.back() = '\0';
feedback(menu, result[0] ? result.data() : queued ? "Saving binding..." : "Could not queue binding.");
@@ -83,7 +83,7 @@ void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callback
if (!available || ImGui::GetTime() >= menu.bind_capture_until) {
menu.recording_bind = 0;
feedback(menu, "Recording cancelled. Your binding is unchanged.");
} else if (const auto combo = menu.bind_capture.update(controller)) {
} else if (const auto combo = menu.bind_capture.update(controller, true)) {
const auto action = menu.recording_bind;
menu.recording_bind = 0;
save(action, *combo);
@@ -92,7 +92,7 @@ void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callback
ImGui::BeginDisabled(!available);
if (ImGui::BeginTable("controller-binds", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, px(130));
ImGui::TableSetupColumn("Controller combo", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Controller combo / key", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##controls", ImGuiTableColumnFlags_WidthFixed, px(170));
ImGui::TableHeadersRow();
const auto row = [&](int action, const char* name, std::uint32_t combo) {
@@ -120,19 +120,22 @@ void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callback
}
ImGui::PopID();
};
row(1, "Noclip", model.bindings.noclip_combo);
row(2, "Forward Boost", model.bindings.forward_velocity_combo);
row(3, "Up Boost", model.bindings.up_velocity_combo);
row(1, "Freecam Controller", model.bindings.freecam_controller_combo);
row(2, "Freecam", model.bindings.freecam_combo);
row(3, "Noclip", model.bindings.noclip_combo);
row(4, "Forward Boost", model.bindings.forward_velocity_combo);
row(5, "Up Boost", model.bindings.up_velocity_combo);
row(6, "TP to Freecam", model.bindings.tp_to_freecam_combo);
row(7, "Off-board Up Boost", model.bindings.offboard_up_velocity_combo);
ImGui::EndTable();
}
ImGui::EndDisabled();
if (menu.recording_bind) {
warn(!controller.available ? "Connect a controller and keep the game focused to record. PlayStation pads work directly or through DS4Windows / Steam Input." :
!menu.bind_capture.ready ? "Release all controller buttons first." :
"Hold the buttons you want together, then release them to save.");
warn(!menu.bind_capture.ready ? "Release controller buttons and keyboard keys first." :
"Hold up to five keys together, then release them. A-Z, 0-9, Space, F1-F12, Ctrl, Shift and Alt work; controller combos work too.");
} else {
note(("Record a button or combo, such as " + controller_combo_label(0x300, controller.style) +
". Noclip toggles; Forward Boost and Up Boost add their velocity once per press. Use a different combo for each.").c_str());
note(("Record a key, keyboard chord or controller combo, such as Ctrl + F5 or " + controller_combo_label(0x300, controller.style) +
". Freecam, Freecam Controller, and Noclip toggle; boosts add velocity once per press. Off-board Up Boost also works while falling or gliding; release and press again to repeat. On-board and off-board boosts can share a combo. Give toggles different combos.").c_str());
note("Saved to your profile.");
if (!model.bindings.status.empty()) note(model.bindings.status.c_str());
if (!model.bindings.available) warn("Waiting for the local profile.");
@@ -18,6 +18,19 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
if (toggle_row(menu, "Freecam", "Detach the camera and explore.", flight,
debug.available && debug.camera_available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_free_camera, flight});
bool freecam_controller = model.bindings.freecam_controller;
if (toggle_row(menu, "Block Input(Enable Controller Support)", "Block player input and use the controller for the Freecam.", freecam_controller,
model.bindings.available && callbacks.queue_debug)) {
std::array<char, 512> result{};
callbacks.queue_console_command(callbacks.user, freecam_controller ? "freecam_controller true" : "freecam_controller false", result.data(), result.size());
}
field(menu, "Teleport to Freecam");
ImGui::BeginDisabled(!debug.free_camera || !debug.camera_position_valid || !callbacks.queue_console_command);
if (ImGui::Button("Teleport", ImVec2(-1, 0))) {
std::array<char, 512> result{};
callbacks.queue_console_command(callbacks.user, "tp_to_freecam", result.data(), result.size());
}
ImGui::EndDisabled();
{
field(menu, "Field of view");
const bool custom = debug.free_camera_fov > 0;
@@ -126,6 +139,10 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
float up_velocity = debug.up_velocity_speed;
if (ImGui::SliderFloat("##up-velocity", &up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_up_velocity_speed, false, up_velocity});
field(menu, "Off-board up boost");
float offboard_up_velocity = debug.offboard_up_velocity_speed;
if (ImGui::SliderFloat("##offboard-up-velocity", &offboard_up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_offboard_up_velocity_speed, false, offboard_up_velocity});
ImGui::EndDisabled();
note("Controller: left stick moves, right stick looks, RT / LT rise and fall, click the left stick to boost.");
note("Keyboard: WASD / Q E, Shift to boost. Close the menu to fly.");
+64 -3
View File
@@ -375,18 +375,57 @@ void update_model(std::uintptr_t client, TickState& frame) {
const auto camera_phase_ready = [&] { return camera_phase_issue() == nullptr; };
std::optional<dingosdk::overlay::DebugRequest> debug_request;
bool debug_busy{}, controller_busy{};
std::uint32_t noclip_combo{}, forward_velocity_combo{}, up_velocity_combo{};
std::uint32_t freecam_controller_combo{}, freecam_combo{}, tp_to_freecam_combo{}, noclip_combo{}, forward_velocity_combo{}, up_velocity_combo{}, offboard_up_velocity_combo{};
bool freecam_controller = dingosdk::local_freecam_controller();
{
std::lock_guard lock(r.mutex);
debug_busy = r.requests.loading();
controller_busy = !r.requests.idle();
freecam_controller_combo = r.model.bindings.available ? r.model.bindings.freecam_controller_combo : 0;
freecam_combo = r.model.bindings.available ? r.model.bindings.freecam_combo : 0;
tp_to_freecam_combo = r.model.bindings.available ? r.model.bindings.tp_to_freecam_combo : 0;
noclip_combo = r.model.bindings.available ? r.model.bindings.noclip_combo : 0;
forward_velocity_combo = r.model.bindings.available ? r.model.bindings.forward_velocity_combo : 0;
up_velocity_combo = r.model.bindings.available ? r.model.bindings.up_velocity_combo : 0;
offboard_up_velocity_combo = r.model.bindings.available ? r.model.bindings.offboard_up_velocity_combo : 0;
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
// Capture belongs to the freecam state itself. Keep it active across brief
// request/loading phases instead of opening a path back to player input.
DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && freecam_controller);
dingosdk::ControllerInput controller;
if (noclip_combo || forward_velocity_combo || up_velocity_combo) DingoSDKOverlayReadControllerInput(&controller);
if (freecam_controller_combo || freecam_combo || tp_to_freecam_combo || noclip_combo || forward_velocity_combo || up_velocity_combo || offboard_up_velocity_combo) DingoSDKOverlayReadControllerInput(&controller);
if (r.freecam_controller_bind_latch.update(freecam_controller_combo, controller, r.observer_failed || controller_busy || !r.debug_model.free_camera)) {
if (dingosdk::set_local_freecam_controller(!freecam_controller))
freecam_controller = !freecam_controller;
DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && freecam_controller);
record("{\"event\":\"controller_binding_triggered\",\"action\":\"freecam_controller\"}");
}
if (r.tp_to_freecam_bind_latch.update(tp_to_freecam_combo, controller, r.observer_failed || controller_busy || !r.debug_model.free_camera || !r.debug_model.camera_position_valid || debug_request.has_value())) {
if (dingosdk::teleport_local_skater(r.debug_model.camera_position)) {
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::set_free_camera, false};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId()))
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
record("{\"event\":\"controller_binding_triggered\",\"action\":\"tp_to_freecam\"}");
}
const bool freecam_bind_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(state != 13 && state != 21) || !r.debug_model.camera_available ||
(freecam_combo && !camera_phase_ready());
if (r.freecam_bind_latch.update(freecam_combo, controller, freecam_bind_blocked)) {
{
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::set_free_camera, !r.debug_model.free_camera};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId()))
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
record("{\"event\":\"controller_binding_triggered\",\"action\":\"freecam\"}");
}
const bool bind_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(state != 13 && state != 21) || !r.debug_model.noclip_available ||
(noclip_combo && !camera_phase_ready());
@@ -401,6 +440,8 @@ void update_model(std::uintptr_t client, TickState& frame) {
record("{\"event\":\"controller_binding_triggered\",\"action\":\"noclip\"}");
}
const bool forward_velocity_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(r.debug_model.offboard_up_velocity_available && forward_velocity_combo == offboard_up_velocity_combo) ||
(r.debug_model.free_camera && freecam_controller) ||
(state != 13 && state != 21) || !r.debug_model.forward_velocity_available ||
(forward_velocity_combo && forward_velocity_combo == noclip_combo);
if (r.forward_velocity_bind_latch.update(forward_velocity_combo, controller, forward_velocity_blocked)) {
@@ -413,6 +454,8 @@ void update_model(std::uintptr_t client, TickState& frame) {
record("{\"event\":\"controller_binding_triggered\",\"action\":\"forward_velocity\"}");
}
const bool up_velocity_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(r.debug_model.offboard_up_velocity_available && up_velocity_combo == offboard_up_velocity_combo) ||
(r.debug_model.free_camera && freecam_controller) ||
(state != 13 && state != 21) || !r.debug_model.up_velocity_available ||
(up_velocity_combo && (up_velocity_combo == noclip_combo || up_velocity_combo == forward_velocity_combo));
if (r.up_velocity_bind_latch.update(up_velocity_combo, controller, up_velocity_blocked)) {
@@ -423,6 +466,19 @@ void update_model(std::uintptr_t client, TickState& frame) {
record("{\"event\":\"controller_binding_triggered\",\"action\":\"up_velocity\"}");
}
}
const bool offboard_up_velocity_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(r.debug_model.free_camera && freecam_controller) ||
(state != 13 && state != 21) || !r.debug_model.offboard_up_velocity_available ||
(offboard_up_velocity_combo && offboard_up_velocity_combo == noclip_combo);
if (r.offboard_up_velocity_bind_latch.update(offboard_up_velocity_combo, controller, offboard_up_velocity_blocked)) {
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::add_offboard_up_velocity};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId())) {
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
record("{\"event\":\"controller_binding_triggered\",\"action\":\"offboard_up_velocity\"}");
}
}
if (debug_request || r.debug_model.free_camera || r.debug_model.first_person || r.debug_model.noclip ||
now >= r.next_debug) {
ScheduledWorkScope work{r, debug_request.has_value()};
@@ -430,11 +486,13 @@ void update_model(std::uintptr_t client, TickState& frame) {
const bool debug_ready = !r.observer_failed && !debug_busy && (state == 13 || state == 21) && native_context_ready();
dingosdk::overlay::FlightInput flight_input;
DingoSDKOverlayReadFlightInput(&flight_input, (r.debug_model.free_camera || r.debug_model.noclip) && debug_ready,
r.debug_model.noclip);
r.debug_model.noclip || (r.debug_model.free_camera && freecam_controller));
const auto phase_issue = camera_phase_issue();
auto debug = dingosdk::on_client_debug_tick(r.base, client, debug_ready,
phase_issue == nullptr,
debug_request ? &*debug_request : nullptr, &flight_input);
// Input ownership must follow the live camera even with every overlay closed.
DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && freecam_controller);
// A debug action, or a mode the tick ended by itself (host policy, lost
// camera), can have replaced the native camera: validate it again.
if (debug_request || debug.free_camera != r.debug_model.free_camera ||
@@ -470,6 +528,8 @@ void update_model(std::uintptr_t client, TickState& frame) {
record("{\"event\":\"forward_velocity_applied\",\"count\":" + std::to_string(debug.forward_velocity_updates) + "}");
if (debug.up_velocity_updates != r.debug_model.up_velocity_updates)
record("{\"event\":\"up_velocity_applied\",\"count\":" + std::to_string(debug.up_velocity_updates) + "}");
if (debug.offboard_up_velocity_updates != r.debug_model.offboard_up_velocity_updates)
record("{\"event\":\"offboard_up_velocity_applied\",\"count\":" + std::to_string(debug.offboard_up_velocity_updates) + "}");
if (r.debug_model.noclip && !debug.noclip && !debug_request) {
std::ostringstream stopped;
stopped << "{\"event\":\"noclip_stopped\",\"reason\":" << std::quoted(debug.status) << '}';
@@ -911,6 +971,7 @@ void update_model(std::uintptr_t client, TickState& frame) {
// Validated afresh for the restore, which may itself change the camera.
frame.camera_issue.reset();
const bool restored = dingosdk::restore_client_debug(r.base, client, camera_phase_ready());
DingoSDKOverlaySetFreecamInputCapture(false);
frame.camera_issue.reset();
if (!restored) {
std::lock_guard lock(r.mutex);
+4
View File
@@ -69,9 +69,13 @@ struct Runtime {
std::vector<std::filesystem::path> patch_level_manifests;
ULONGLONG next_debug{}, flight_unavailable_since{};
std::string flight_unavailable_issue, flight_unavailable_logged;
dingosdk::ControllerComboLatch freecam_controller_bind_latch;
dingosdk::ControllerComboLatch freecam_bind_latch;
dingosdk::ControllerComboLatch tp_to_freecam_bind_latch;
dingosdk::ControllerComboLatch noclip_bind_latch;
dingosdk::ControllerComboLatch forward_velocity_bind_latch;
dingosdk::ControllerComboLatch up_velocity_bind_latch;
dingosdk::ControllerComboLatch offboard_up_velocity_bind_latch;
ULONGLONG next_offline{};
bool transition_seen{}, initialized{}, catalog_logged{},
native_tick_ready{}, native_loading_logging{}, fixed_stop_entitlement_route_ready{}, menu_load_queued{}, startup_load_queued{};
View File
File mode changed.