|
|
@@ -18,8 +18,8 @@
|
|
|
#include "../../../core/logging/log.h"
|
|
|
#include "../../../core/ui/runtime/ui_visibility_runtime.h"
|
|
|
#include "../../hooking/detour.h"
|
|
|
+#include "../../movement/movement_settings_store.h"
|
|
|
#include "../../patterns/image_scan.h"
|
|
|
-#include "../../teleport/teleport_settings_store.h"
|
|
|
#include "runtime.h"
|
|
|
|
|
|
namespace sunrise::client::hooks::noclip {
|
|
|
@@ -29,6 +29,7 @@ namespace {
|
|
|
constexpr std::string_view kHavokStepText =
|
|
|
"40 53 48 83 EC 20 83 79 38 01 48 8B D9 77 06 48 8B 01 FF 50 20 F7 43 38 FD FF FF FF "
|
|
|
"75 09 48 8B 03 48 8B CB FF 50 28";
|
|
|
+/** Masked form of the step text. This is the form the image scan takes. */
|
|
|
constexpr auto kHavokStep =
|
|
|
patterns::signature<patterns::signature_length(kHavokStepText)>(kHavokStepText);
|
|
|
|
|
|
@@ -80,10 +81,11 @@ struct HavokArray {
|
|
|
std::uint32_t capacityAndFlags{};
|
|
|
};
|
|
|
|
|
|
-static_assert(sizeof(HavokArray) == 16);
|
|
|
+/** hkArray packs its pointer, size and capacity into 16 bytes with no tail padding. */
|
|
|
+constexpr std::size_t kHavokArrayBytes = 16;
|
|
|
+static_assert(sizeof(HavokArray) == kHavokArrayBytes);
|
|
|
|
|
|
std::atomic_bool g_installed{false};
|
|
|
-std::atomic_bool g_active{false};
|
|
|
std::atomic_bool g_toggleDown{false};
|
|
|
std::atomic_bool g_targetValid{false};
|
|
|
/** The two target floats are one atomic publication, so readers never observe mixed coordinates. */
|
|
|
@@ -158,45 +160,52 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
|
|
|
return std::bit_cast<std::array<float, 2>>(value);
|
|
|
}
|
|
|
|
|
|
-/** Polls the configured edge-triggered toggle on the physics thread. */
|
|
|
-void poll_toggle() noexcept {
|
|
|
- const client::teleport::Settings settings = client::teleport::get();
|
|
|
- const bool usable =
|
|
|
- settings.noclipEnabled && settings.noclipToggleKey != client::teleport::kNoKey;
|
|
|
- const bool down =
|
|
|
- usable && (GetAsyncKeyState(static_cast<int>(settings.noclipToggleKey)) & 0x8000) != 0;
|
|
|
- if (!usable) {
|
|
|
+/**
|
|
|
+ * Polls the bound key on the physics thread and flips the stored switch when it goes down.
|
|
|
+ * The key and the interface toggle write the same stored value, so there is one on/off state.
|
|
|
+ * @return True while noclip is on.
|
|
|
+ */
|
|
|
+[[nodiscard]] bool poll_toggle() noexcept {
|
|
|
+ const client::movement::Settings settings = client::movement::get();
|
|
|
+ if (settings.noclipToggleKey == client::movement::kNoKey) {
|
|
|
g_toggleDown.store(false, std::memory_order_relaxed);
|
|
|
- if (g_active.exchange(false, std::memory_order_acq_rel)) {
|
|
|
- invalidate_target();
|
|
|
- }
|
|
|
- return;
|
|
|
+ return settings.noclipEnabled;
|
|
|
}
|
|
|
+ const bool down = (GetAsyncKeyState(static_cast<int>(settings.noclipToggleKey)) & 0x8000) != 0;
|
|
|
+ // An open interface owns the keyboard, so the bound key only tracks the press, it never flips.
|
|
|
if (core::ui::runtime::snapshot().visible) {
|
|
|
g_toggleDown.store(down, std::memory_order_relaxed);
|
|
|
- return;
|
|
|
+ return settings.noclipEnabled;
|
|
|
}
|
|
|
if (down && !g_toggleDown.exchange(true, std::memory_order_acq_rel)) {
|
|
|
- const bool enabled = !g_active.load(std::memory_order_acquire);
|
|
|
- g_active.store(enabled, std::memory_order_release);
|
|
|
+ client::movement::Settings updated = settings;
|
|
|
+ updated.noclipEnabled = !settings.noclipEnabled;
|
|
|
+ if (!client::movement::publish(updated)) {
|
|
|
+ return settings.noclipEnabled;
|
|
|
+ }
|
|
|
invalidate_target();
|
|
|
core::log::write(core::log::Channel::client,
|
|
|
core::log::Level::info,
|
|
|
- enabled ? "ev=noclip stage=toggle active=1 mode=rigid_body_position"
|
|
|
- : "ev=noclip stage=toggle active=0 mode=rigid_body_position");
|
|
|
- return;
|
|
|
+ updated.noclipEnabled
|
|
|
+ ? "ev=noclip stage=toggle enabled=1 mode=rigid_body_position"
|
|
|
+ : "ev=noclip stage=toggle enabled=0 mode=rigid_body_position");
|
|
|
+ return updated.noclipEnabled;
|
|
|
}
|
|
|
if (!down) {
|
|
|
g_toggleDown.store(false, std::memory_order_release);
|
|
|
}
|
|
|
+ return settings.noclipEnabled;
|
|
|
+}
|
|
|
+
|
|
|
+/** @return True while the stored switch has noclip on. */
|
|
|
+[[nodiscard]] bool enabled() noexcept {
|
|
|
+ return client::movement::get().noclipEnabled;
|
|
|
}
|
|
|
|
|
|
/** Runs Havok normally, then replaces collision-resolved horizontal position for the character. */
|
|
|
std::int32_t __fastcall havok_step(std::byte* simulation, float deltaTime) noexcept {
|
|
|
- poll_toggle();
|
|
|
-
|
|
|
std::array<float, kVectorLanes> nativeVelocity{};
|
|
|
- const bool enabledBeforeStep = active();
|
|
|
+ const bool enabledBeforeStep = poll_toggle();
|
|
|
std::byte* const before = enabledBeforeStep ? character_body(simulation) : nullptr;
|
|
|
const bool hasVelocity = before != nullptr;
|
|
|
if (hasVelocity) {
|
|
|
@@ -206,7 +215,8 @@ std::int32_t __fastcall havok_step(std::byte* simulation, float deltaTime) noexc
|
|
|
const HavokStep next = reinterpret_cast<HavokStep>(g_stepHandle.original);
|
|
|
const std::int32_t result = next != nullptr ? next(simulation, deltaTime) : 0;
|
|
|
|
|
|
- if (!enabledBeforeStep || !active()) {
|
|
|
+ // Re-read after the step, so a toggle from the interface thread lands before a position write.
|
|
|
+ if (!enabledBeforeStep || !enabled()) {
|
|
|
return result;
|
|
|
}
|
|
|
std::byte* const body = character_body(simulation);
|
|
|
@@ -274,6 +284,7 @@ void report_install_failure(const char* reason) noexcept {
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
+/** Resolves the Havok targets and attaches the simulation-step detour. */
|
|
|
bool install() noexcept {
|
|
|
if (g_installed.load(std::memory_order_acquire)) {
|
|
|
return true;
|
|
|
@@ -303,6 +314,7 @@ bool install() noexcept {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+/** Detaches the simulation-step detour, then clears the toggle and the horizontal target. */
|
|
|
void uninstall() noexcept {
|
|
|
if (!g_installed.exchange(false, std::memory_order_acq_rel)) {
|
|
|
return;
|
|
|
@@ -310,19 +322,11 @@ void uninstall() noexcept {
|
|
|
(void)hooking::detour::uninstall(g_stepHandle);
|
|
|
g_stepHandle = {};
|
|
|
g_characterMotionVtable = 0;
|
|
|
- reset();
|
|
|
-}
|
|
|
-
|
|
|
-void reset() noexcept {
|
|
|
- g_active.store(false, std::memory_order_release);
|
|
|
+ // The switch is a stored setting, so detaching clears only the key state and the target.
|
|
|
g_toggleDown.store(false, std::memory_order_release);
|
|
|
invalidate_target();
|
|
|
}
|
|
|
|
|
|
-bool active() noexcept {
|
|
|
- return g_active.load(std::memory_order_acquire);
|
|
|
-}
|
|
|
-
|
|
|
void invalidate_target() noexcept {
|
|
|
g_targetValid.store(false, std::memory_order_release);
|
|
|
}
|