|
|
@@ -1,7 +1,8 @@
|
|
|
/**
|
|
|
- * Horizontal noclip at the Havok simulation boundary. The hook reads native horizontal velocity
|
|
|
- * before simulation, lets Havok run normally, then replaces the character body's resolved X/Y
|
|
|
- * position before Destiny publishes it.
|
|
|
+ * Noclip at the Havok simulation boundary. The hook reads the body's position and velocity before
|
|
|
+ * simulation, lets Havok run, then writes the position on from where the body stood.
|
|
|
+ * Collision resolution is discarded for the lanes it carries. The game keeps the position, so a
|
|
|
+ * respawn or a teleport needs nothing reset here.
|
|
|
*/
|
|
|
|
|
|
#include <Windows.h>
|
|
|
@@ -9,7 +10,7 @@
|
|
|
#include <algorithm>
|
|
|
#include <array>
|
|
|
#include <atomic>
|
|
|
-#include <bit>
|
|
|
+#include <cmath>
|
|
|
#include <cstddef>
|
|
|
#include <cstdint>
|
|
|
#include <cstdio>
|
|
|
@@ -18,8 +19,10 @@
|
|
|
#include "../../../core/logging/log.h"
|
|
|
#include "../../../core/ui/runtime/ui_visibility_runtime.h"
|
|
|
#include "../../hooking/detour.h"
|
|
|
+#include "../../input/window_focus.h"
|
|
|
#include "../../movement/movement_settings_store.h"
|
|
|
#include "../../patterns/image_scan.h"
|
|
|
+#include "../fly/fly.h"
|
|
|
#include "runtime.h"
|
|
|
|
|
|
namespace sunrise::client::hooks::noclip {
|
|
|
@@ -55,9 +58,10 @@ constexpr std::size_t kBodyMotion = 0x150;
|
|
|
constexpr std::size_t kBodyPosition = 0x1C0;
|
|
|
constexpr std::size_t kBodyVelocity = 0x230;
|
|
|
|
|
|
-/** X and Y are Destiny's horizontal world-space lanes. */
|
|
|
+/** X and Y are Destiny's horizontal world-space lanes, and Z the vertical one. */
|
|
|
constexpr std::size_t kHorizontalX = 0;
|
|
|
constexpr std::size_t kHorizontalY = 1;
|
|
|
+constexpr std::size_t kVertical = 2;
|
|
|
constexpr std::size_t kVectorLanes = 4;
|
|
|
|
|
|
/**
|
|
|
@@ -87,9 +91,6 @@ static_assert(sizeof(HavokArray) == kHavokArrayBytes);
|
|
|
|
|
|
std::atomic_bool g_installed{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. */
|
|
|
-std::atomic<std::uint64_t> g_horizontalTarget{};
|
|
|
/** Module-owned vtable target; unlike Havok objects, its address is stable until DLL teardown. */
|
|
|
std::uintptr_t g_characterMotionVtable{};
|
|
|
hooking::detour::Handle g_stepHandle{};
|
|
|
@@ -99,6 +100,38 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
|
|
|
return *reinterpret_cast<T*>(object + offset);
|
|
|
}
|
|
|
|
|
|
+/** Copies the lanes the shorter vector carries, and leaves any beyond it alone. */
|
|
|
+template <typename Source, typename Destination>
|
|
|
+void copy_lanes(const Source& source, Destination& destination) noexcept {
|
|
|
+ // The shorter vector's lane count, so a 3-lane caller never touches the stored fourth.
|
|
|
+ constexpr std::size_t lanes =
|
|
|
+ (std::min)(std::tuple_size_v<Source>, std::tuple_size_v<Destination>);
|
|
|
+ for (std::size_t lane = 0; lane < lanes; ++lane) {
|
|
|
+ destination[lane] = source[lane];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Shortens a velocity to a speed limit, keeping its direction.
|
|
|
+ * @param velocity Velocity to limit.
|
|
|
+ * @param limit Highest speed to return.
|
|
|
+ */
|
|
|
+[[nodiscard]] std::array<float, kVectorLanes>
|
|
|
+capped_speed(const std::array<float, kVectorLanes>& velocity, float limit) noexcept {
|
|
|
+ const float speedSquared = velocity[kHorizontalX] * velocity[kHorizontalX]
|
|
|
+ + velocity[kHorizontalY] * velocity[kHorizontalY]
|
|
|
+ + velocity[kVertical] * velocity[kVertical];
|
|
|
+ if (speedSquared <= limit * limit) {
|
|
|
+ return velocity;
|
|
|
+ }
|
|
|
+ std::array<float, kVectorLanes> capped = velocity;
|
|
|
+ const float scale = limit / std::sqrt(speedSquared);
|
|
|
+ capped[kHorizontalX] *= scale;
|
|
|
+ capped[kHorizontalY] *= scale;
|
|
|
+ capped[kVertical] *= scale;
|
|
|
+ return capped;
|
|
|
+}
|
|
|
+
|
|
|
/** @return True when the array header is internally consistent and within the supplied bound. */
|
|
|
[[nodiscard]] bool valid_array(const HavokArray& array, std::int32_t maximum) noexcept {
|
|
|
const std::uint32_t capacity = array.capacityAndFlags & kArrayCapacityMask;
|
|
|
@@ -150,16 +183,6 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
|
|
|
return nullptr;
|
|
|
}
|
|
|
|
|
|
-/** Packs the horizontal target into one atomic value. */
|
|
|
-[[nodiscard]] std::uint64_t pack_target(float x, float y) noexcept {
|
|
|
- return std::bit_cast<std::uint64_t>(std::array<float, 2>{x, y});
|
|
|
-}
|
|
|
-
|
|
|
-/** Unpacks one atomically published horizontal target. */
|
|
|
-[[nodiscard]] std::array<float, 2> unpack_target(std::uint64_t value) noexcept {
|
|
|
- return std::bit_cast<std::array<float, 2>>(value);
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* 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.
|
|
|
@@ -171,7 +194,9 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
|
|
|
g_toggleDown.store(false, std::memory_order_relaxed);
|
|
|
return settings.noclipEnabled;
|
|
|
}
|
|
|
- const bool down = (GetAsyncKeyState(static_cast<int>(settings.noclipToggleKey)) & 0x8000) != 0;
|
|
|
+ const bool down =
|
|
|
+ client::input::game_focused()
|
|
|
+ && (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);
|
|
|
@@ -183,7 +208,6 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
|
|
|
if (!client::movement::publish(updated)) {
|
|
|
return settings.noclipEnabled;
|
|
|
}
|
|
|
- invalidate_target();
|
|
|
core::log::write(core::log::Channel::client,
|
|
|
core::log::Level::info,
|
|
|
updated.noclipEnabled
|
|
|
@@ -202,71 +226,88 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
|
|
|
return client::movement::get().noclipEnabled;
|
|
|
}
|
|
|
|
|
|
-/** Runs Havok normally, then replaces collision-resolved horizontal position for the character. */
|
|
|
+/** Runs Havok normally, then moves the character on from where it stood before the step. */
|
|
|
std::int32_t __fastcall havok_step(std::byte* simulation, float deltaTime) noexcept {
|
|
|
std::array<float, kVectorLanes> nativeVelocity{};
|
|
|
+ std::array<float, kVectorLanes> nativePosition{};
|
|
|
const bool enabledBeforeStep = poll_toggle();
|
|
|
- std::byte* const before = enabledBeforeStep ? character_body(simulation) : nullptr;
|
|
|
- const bool hasVelocity = before != nullptr;
|
|
|
- if (hasVelocity) {
|
|
|
+ const bool flying = fly::enabled();
|
|
|
+ std::byte* const before = (enabledBeforeStep || flying) ? character_body(simulation) : nullptr;
|
|
|
+ // Fly writes first, so the velocity read below is the one it asked for.
|
|
|
+ if (flying) {
|
|
|
+ fly::before_step(before);
|
|
|
+ }
|
|
|
+ const bool hasBody = before != nullptr;
|
|
|
+ if (hasBody) {
|
|
|
nativeVelocity = field<std::array<float, kVectorLanes>>(before, kBodyVelocity);
|
|
|
+ nativePosition = field<std::array<float, kVectorLanes>>(before, kBodyPosition);
|
|
|
+ }
|
|
|
+ // Flying through geometry, the body does not have to carry the speed through the step: this
|
|
|
+ // hook writes the position itself. It is put to rest instead, because damage taken inside
|
|
|
+ // geometry scales with contact speed and a resting body makes no fast contacts. The speed is
|
|
|
+ // held above and put back after the step.
|
|
|
+ const bool rested = enabledBeforeStep && flying && hasBody;
|
|
|
+ if (rested) {
|
|
|
+ field<std::array<float, kVectorLanes>>(before, kBodyVelocity) = {};
|
|
|
}
|
|
|
|
|
|
const HavokStep next = reinterpret_cast<HavokStep>(g_stepHandle.original);
|
|
|
const std::int32_t result = next != nullptr ? next(simulation, deltaTime) : 0;
|
|
|
|
|
|
+ // The body is resolved once here for both features.
|
|
|
+ std::byte* const body = (enabledBeforeStep || flying) ? character_body(simulation) : nullptr;
|
|
|
+ // A character created or replaced during this step has no matching before-state.
|
|
|
+ const bool sameBody = hasBody && body == before;
|
|
|
// 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);
|
|
|
- if (body == nullptr) {
|
|
|
- // Other Havok worlds do not contain the player and must not disturb the shared target.
|
|
|
- // Only a world that contained the character before this step can prove it was removed.
|
|
|
- if (before != nullptr) {
|
|
|
- invalidate_target();
|
|
|
- }
|
|
|
- return result;
|
|
|
+ const bool noclipping = enabledBeforeStep && enabled();
|
|
|
+ // With both on this hook drives all three lanes. Fly holds the height, so carrying the
|
|
|
+ // vertical one is safe.
|
|
|
+ const bool verticalToo = noclipping && flying;
|
|
|
+ if (flying) {
|
|
|
+ fly::after_step(body, verticalToo);
|
|
|
}
|
|
|
- std::array<float, kVectorLanes> position =
|
|
|
- field<std::array<float, kVectorLanes>>(body, kBodyPosition);
|
|
|
- // A character created or replaced during this step has no compatible velocity or target.
|
|
|
- if (before == nullptr || body != before) {
|
|
|
- invalidate_target();
|
|
|
- }
|
|
|
- if (!g_targetValid.load(std::memory_order_acquire)) {
|
|
|
- g_horizontalTarget.store(pack_target(position[kHorizontalX], position[kHorizontalY]),
|
|
|
- std::memory_order_relaxed);
|
|
|
- g_targetValid.store(true, std::memory_order_release);
|
|
|
- return result;
|
|
|
+ // The game reads this field after the step and damages the player for carrying speed into
|
|
|
+ // geometry. It is shown a capped speed instead. Nothing is lost: the step it belonged to has
|
|
|
+ // already run, and fly writes the real speed again before the next one.
|
|
|
+ if (flying && sameBody) {
|
|
|
+ const std::array<float, kVectorLanes> moved =
|
|
|
+ rested ? nativeVelocity : field<std::array<float, kVectorLanes>>(body, kBodyVelocity);
|
|
|
+ field<std::array<float, kVectorLanes>>(body, kBodyVelocity) =
|
|
|
+ capped_speed(moved, fly::kPublishedSpeedCap);
|
|
|
}
|
|
|
- if (!hasVelocity) {
|
|
|
+ if (!noclipping || !sameBody) {
|
|
|
return result;
|
|
|
}
|
|
|
-
|
|
|
const float step = std::clamp(deltaTime, 0.0F, kMaximumStepSeconds);
|
|
|
- const float velocitySquared = nativeVelocity[kHorizontalX] * nativeVelocity[kHorizontalX]
|
|
|
- + nativeVelocity[kHorizontalY] * nativeVelocity[kHorizontalY];
|
|
|
+ float velocitySquared = nativeVelocity[kHorizontalX] * nativeVelocity[kHorizontalX]
|
|
|
+ + nativeVelocity[kHorizontalY] * nativeVelocity[kHorizontalY];
|
|
|
+ if (verticalToo) {
|
|
|
+ // Straight up has no horizontal velocity, and without this the move is dropped.
|
|
|
+ velocitySquared += nativeVelocity[kVertical] * nativeVelocity[kVertical];
|
|
|
+ }
|
|
|
if (step <= 0.0F || velocitySquared <= kMinimumVelocitySquared) {
|
|
|
return result;
|
|
|
}
|
|
|
- const std::array<float, 2> target =
|
|
|
- unpack_target(g_horizontalTarget.load(std::memory_order_acquire));
|
|
|
- const float targetX = target[kHorizontalX] + nativeVelocity[kHorizontalX] * step;
|
|
|
- const float targetY = target[kHorizontalY] + nativeVelocity[kHorizontalY] * step;
|
|
|
- position[kHorizontalX] = targetX;
|
|
|
- position[kHorizontalY] = targetY;
|
|
|
+ // Moved on from where the body stood before the step, not from a position of our own. The game
|
|
|
+ // owns the position, so a respawn or any other placement is picked up with nothing to reset.
|
|
|
+ std::array<float, kVectorLanes> position =
|
|
|
+ field<std::array<float, kVectorLanes>>(body, kBodyPosition);
|
|
|
+ position[kHorizontalX] = nativePosition[kHorizontalX] + nativeVelocity[kHorizontalX] * step;
|
|
|
+ position[kHorizontalY] = nativePosition[kHorizontalY] + nativeVelocity[kHorizontalY] * step;
|
|
|
+ if (verticalToo) {
|
|
|
+ position[kVertical] = nativePosition[kVertical] + nativeVelocity[kVertical] * step;
|
|
|
+ }
|
|
|
field<std::array<float, kVectorLanes>>(body, kBodyPosition) = position;
|
|
|
|
|
|
- // Collision may consume horizontal velocity before publication. Restore the pre-simulation
|
|
|
- // velocity so the next step keeps advancing the target, while retaining resolved vertical
|
|
|
- // velocity for ordinary ground movement and gravity.
|
|
|
- std::array<float, kVectorLanes> wakeVelocity =
|
|
|
- field<std::array<float, kVectorLanes>>(body, kBodyVelocity);
|
|
|
- wakeVelocity[kHorizontalX] = nativeVelocity[kHorizontalX];
|
|
|
- wakeVelocity[kHorizontalY] = nativeVelocity[kHorizontalY];
|
|
|
- field<std::array<float, kVectorLanes>>(body, kBodyVelocity) = wakeVelocity;
|
|
|
- g_horizontalTarget.store(pack_target(targetX, targetY), std::memory_order_release);
|
|
|
+ // Collision may consume velocity before publication. Restore it so the next step still moves.
|
|
|
+ // The vertical lane stays resolved. A rested body already had every lane put back above.
|
|
|
+ if (!rested) {
|
|
|
+ std::array<float, kVectorLanes> wakeVelocity =
|
|
|
+ field<std::array<float, kVectorLanes>>(body, kBodyVelocity);
|
|
|
+ wakeVelocity[kHorizontalX] = nativeVelocity[kHorizontalX];
|
|
|
+ wakeVelocity[kHorizontalY] = nativeVelocity[kHorizontalY];
|
|
|
+ field<std::array<float, kVectorLanes>>(body, kBodyVelocity) = wakeVelocity;
|
|
|
+ }
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
@@ -314,7 +355,7 @@ bool install() noexcept {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
-/** Detaches the simulation-step detour, then clears the toggle and the horizontal target. */
|
|
|
+/** Detaches the simulation-step detour, then clears the key state. */
|
|
|
void uninstall() noexcept {
|
|
|
if (!g_installed.exchange(false, std::memory_order_acq_rel)) {
|
|
|
return;
|
|
|
@@ -322,13 +363,34 @@ void uninstall() noexcept {
|
|
|
(void)hooking::detour::uninstall(g_stepHandle);
|
|
|
g_stepHandle = {};
|
|
|
g_characterMotionVtable = 0;
|
|
|
- // The switch is a stored setting, so detaching clears only the key state and the target.
|
|
|
+ // The switch is a stored setting, so detaching clears only the key state.
|
|
|
g_toggleDown.store(false, std::memory_order_release);
|
|
|
- invalidate_target();
|
|
|
}
|
|
|
|
|
|
-void invalidate_target() noexcept {
|
|
|
- g_targetValid.store(false, std::memory_order_release);
|
|
|
+/** Reads a live rigid body's world position. */
|
|
|
+void read_body_position(void* body, Vector& position) noexcept {
|
|
|
+ copy_lanes(field<std::array<float, kVectorLanes>>(static_cast<std::byte*>(body), kBodyPosition),
|
|
|
+ position);
|
|
|
+}
|
|
|
+
|
|
|
+/** Writes a live rigid body's world position. */
|
|
|
+void write_body_position(void* body, const Vector& position) noexcept {
|
|
|
+ auto& stored =
|
|
|
+ field<std::array<float, kVectorLanes>>(static_cast<std::byte*>(body), kBodyPosition);
|
|
|
+ copy_lanes(position, stored);
|
|
|
+}
|
|
|
+
|
|
|
+/** Reads a live rigid body's linear velocity. */
|
|
|
+void read_body_velocity(void* body, Vector& velocity) noexcept {
|
|
|
+ copy_lanes(field<std::array<float, kVectorLanes>>(static_cast<std::byte*>(body), kBodyVelocity),
|
|
|
+ velocity);
|
|
|
+}
|
|
|
+
|
|
|
+/** Writes a live rigid body's linear velocity. */
|
|
|
+void write_body_velocity(void* body, const Vector& velocity) noexcept {
|
|
|
+ auto& stored =
|
|
|
+ field<std::array<float, kVectorLanes>>(static_cast<std::byte*>(body), kBodyVelocity);
|
|
|
+ copy_lanes(velocity, stored);
|
|
|
}
|
|
|
|
|
|
} // namespace sunrise::client::hooks::noclip
|