Przeglądaj źródła

noclip collision fix and fly

stan 3 tygodni temu
rodzic
commit
233c811e8c

+ 6 - 2
README.md

@@ -5,14 +5,18 @@ Destiny 2 Offline Exploration Mod
 > This mod installs onto an old build of the game and allows you to play it offline, loading into
 > This mod installs onto an old build of the game and allows you to play it offline, loading into
 > destinations and exploring them.
 > destinations and exploring them.
 >
 >
-> No other features are currently supported. (Missions, Enemies, NPCs, Quests, Inventory
-> Management, ...)
+> Most gameplay features are not currently supported. (Missions, Enemies, NPCs, Quests, Persistent Saves, ...)
 
 
 - [Install Instructions](https://github.com/stanuwu/Sunrise/wiki/Installing)
 - [Install Instructions](https://github.com/stanuwu/Sunrise/wiki/Installing)
 - [FAQ](https://github.com/stanuwu/Sunrise/wiki/FAQ)
 - [FAQ](https://github.com/stanuwu/Sunrise/wiki/FAQ)
 - [Common Issues](https://github.com/stanuwu/Sunrise/wiki/Common-Issues)
 - [Common Issues](https://github.com/stanuwu/Sunrise/wiki/Common-Issues)
 - [Discord](https://discord.gg/22JS6et5k9)
 - [Discord](https://discord.gg/22JS6et5k9)
 
 
+## Features
+- Load into any Destination (matchmade activities are currently broken)
+- Exploration Features (Fly, Noclip, Activity Override, ...)
+- Basic Inventory Management
+
 ## WIP
 ## WIP
 
 
 This mod is work in progress. Things might break or work in unexpected ways. There is also currently
 This mod is work in progress. Things might break or work in unexpected ways. There is also currently

+ 4 - 0
Sunrise/Sunrise.vcxproj

@@ -208,6 +208,8 @@
     <ClCompile Include="src\client\movement\movement_settings_store.cpp" />
     <ClCompile Include="src\client\movement\movement_settings_store.cpp" />
     <ClCompile Include="src\client\hooks\noclip\horizontal_noclip.cpp" />
     <ClCompile Include="src\client\hooks\noclip\horizontal_noclip.cpp" />
     <ClCompile Include="src\client\hooks\sword_skate\sword_skate.cpp" />
     <ClCompile Include="src\client\hooks\sword_skate\sword_skate.cpp" />
+    <ClCompile Include="src\client\hooks\fly\fly.cpp" />
+    <ClCompile Include="src\client\input\window_focus.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_move.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_move.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_action_key.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_action_key.cpp" />
@@ -761,6 +763,8 @@
     <ClInclude Include="src\client\movement\movement_settings_store.h" />
     <ClInclude Include="src\client\movement\movement_settings_store.h" />
     <ClInclude Include="src\client\hooks\noclip\runtime.h" />
     <ClInclude Include="src\client\hooks\noclip\runtime.h" />
     <ClInclude Include="src\client\hooks\sword_skate\sword_skate.h" />
     <ClInclude Include="src\client\hooks\sword_skate\sword_skate.h" />
+    <ClInclude Include="src\client\hooks\fly\fly.h" />
+    <ClInclude Include="src\client\input\window_focus.h" />
     <ClInclude Include="src\client\hooks\teleport\internal.h" />
     <ClInclude Include="src\client\hooks\teleport\internal.h" />
     <ClInclude Include="src\client\hooks\teleport\runtime.h" />
     <ClInclude Include="src\client\hooks\teleport\runtime.h" />
     <ClInclude Include="src\client\ui\movement\movement_panel.h" />
     <ClInclude Include="src\client\ui\movement\movement_panel.h" />

+ 331 - 0
Sunrise/src/client/hooks/fly/fly.cpp

@@ -0,0 +1,331 @@
+/**
+ * Velocity fly. The movement keys set the player's velocity every tick.
+ * Velocity is set, not added. Adding compounds each tick and leaves gravity in the vertical lane.
+ * Setting it means releasing every key stops the player, which is what holds a hover.
+ * The write goes in before the simulation step, and again before the sync that publishes it.
+ */
+
+#include "fly.h"
+
+#include <Windows.h>
+
+#include <array>
+#include <atomic>
+#include <cmath>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+
+#include "../../../core/logging/log.h"
+#include "../../../core/ui/runtime/ui_visibility_runtime.h"
+#include "../../../state/account/account_state.h"
+#include "../../../state/runtime/runtime.h"
+#include "../../input/window_focus.h"
+#include "../../movement/movement_settings_store.h"
+#include "../noclip/runtime.h"
+#include "../teleport/runtime.h"
+
+namespace sunrise::client::hooks::fly {
+namespace {
+
+namespace bindings = state::account::settings::bindings;
+
+/** The high bit of a polled key state marks it held. */
+constexpr SHORT kKeyHeldBit = static_cast<SHORT>(0x8000);
+/** Below this squared length a direction counts as none. */
+constexpr float kMinimumLengthSquared = 0.000001F;
+
+/** The horizontal lanes. The basis is X forward, Z up. */
+constexpr std::size_t kLaneX = 0;
+constexpr std::size_t kLaneY = 1;
+
+/** One movement direction in the player's own frame. */
+enum class Direction : std::size_t {
+    forward,
+    backward,
+    left,
+    right,
+    up,
+    down,
+    count,
+};
+
+/** Sizes the per-direction flag array. */
+constexpr std::size_t kDirectionCount = static_cast<std::size_t>(Direction::count);
+
+/** One polled action and the direction it moves. */
+struct ActionDirection {
+    bindings::Action action;
+    Direction direction;
+};
+
+/** Crouch has two actions and either one descends. Both are read as a hold, to pair with jump. */
+constexpr std::array<ActionDirection, 7> kActions{{
+    {bindings::Action::moveForward, Direction::forward},
+    {bindings::Action::moveBackward, Direction::backward},
+    {bindings::Action::moveLeft, Direction::left},
+    {bindings::Action::moveRight, Direction::right},
+    {bindings::Action::jump, Direction::up},
+    {bindings::Action::toggleCrouch, Direction::down},
+    {bindings::Action::holdCrouch, Direction::down},
+}};
+
+/** Both binding halves of each polled action, in the order of the table above. */
+std::array<bindings::Binding, kActions.size()> g_bindings{};
+/** Set once the bindings are read, so the costly account snapshot runs once. */
+bool g_bindingsRead{false};
+/** The toggle key on the previous frame, so the switch only flips on the press. */
+std::atomic_bool g_toggleDown{false};
+/** The height the body had before the step, which is the height to hold. */
+float g_heightBeforeStep{0.0F};
+bool g_heightValid{false};
+/** Set while a press owns the vertical lane. The hold stands aside. */
+bool g_steered{false};
+
+/**
+ * Takes the account's movement bindings once they are loaded.
+ * @return True when they have been read.
+ */
+[[nodiscard]] bool read_bindings() noexcept {
+    if (g_bindingsRead) {
+        return true;
+    }
+    // The snapshot copies the whole account, so it stops once the bindings arrive.
+    const state::AccountState account = state::account_snapshot();
+    if (!account.settings.keyBindings.configured) {
+        return false;
+    }
+    for (std::size_t index = 0; index < kActions.size(); ++index) {
+        const std::size_t action = static_cast<std::size_t>(kActions[index].action);
+        g_bindings[index] = account.settings.keyBindings.values[action];
+    }
+    g_bindingsRead = true;
+    return true;
+}
+
+/**
+ * A half is an input code, not a virtual key. A half bound to a mouse button gives no key.
+ * @param half One binding half, empty when unbound.
+ * @return True while its key is down.
+ */
+[[nodiscard]] bool half_down(const std::optional<std::uint16_t>& half) noexcept {
+    if (!half.has_value()) {
+        return false;
+    }
+    const std::uint32_t key = teleport::action_key(*half);
+    return key != 0 && (GetAsyncKeyState(static_cast<int>(key)) & kKeyHeldBit) != 0;
+}
+
+/** @return One flag per direction, true while any key bound to it is down. */
+[[nodiscard]] std::array<bool, kDirectionCount> pressed_directions() noexcept {
+    std::array<bool, kDirectionCount> pressed{};
+    for (std::size_t index = 0; index < kActions.size(); ++index) {
+        const bindings::Binding& binding = g_bindings[index];
+        if (half_down(binding.primary) || half_down(binding.secondary)) {
+            pressed[static_cast<std::size_t>(kActions[index].direction)] = true;
+        }
+    }
+    return pressed;
+}
+
+/**
+ * Forward turned about the up axis. Which turn is right is unverified: if strafing is mirrored,
+ * negate both lanes.
+ * @return The strafe axis, or zeroes when the camera looks straight up or down.
+ */
+[[nodiscard]] teleport::Vector right_of(const teleport::Vector& forward) noexcept {
+    teleport::Vector right{forward[kLaneY], -forward[kLaneX], 0.0F};
+    const float lengthSquared = right[kLaneX] * right[kLaneX] + right[kLaneY] * right[kLaneY];
+    if (lengthSquared <= kMinimumLengthSquared) {
+        return teleport::Vector{};
+    }
+    const float length = std::sqrt(lengthSquared);
+    right[kLaneX] /= length;
+    right[kLaneY] /= length;
+    return right;
+}
+
+/**
+ * Composes the pressed directions into one unit vector.
+ * @param pressed One flag per direction.
+ * @param forward Camera forward vector.
+ * @return The direction to fly, or all zeroes when nothing is pressed.
+ */
+[[nodiscard]] teleport::Vector travel(const std::array<bool, kDirectionCount>& pressed,
+                                      const teleport::Vector& forward) noexcept {
+    const teleport::Vector right = right_of(forward);
+    teleport::Vector move{};
+    const auto add = [&move](const teleport::Vector& axis, float scale) noexcept {
+        for (std::size_t lane = 0; lane < teleport::kVectorLanes; ++lane) {
+            move[lane] += axis[lane] * scale;
+        }
+    };
+    if (pressed[static_cast<std::size_t>(Direction::forward)]) {
+        add(forward, 1.0F);
+    }
+    if (pressed[static_cast<std::size_t>(Direction::backward)]) {
+        add(forward, -1.0F);
+    }
+    if (pressed[static_cast<std::size_t>(Direction::right)]) {
+        add(right, 1.0F);
+    }
+    if (pressed[static_cast<std::size_t>(Direction::left)]) {
+        add(right, -1.0F);
+    }
+    if (pressed[static_cast<std::size_t>(Direction::up)]) {
+        move[teleport::kVerticalLane] += 1.0F;
+    }
+    if (pressed[static_cast<std::size_t>(Direction::down)]) {
+        move[teleport::kVerticalLane] -= 1.0F;
+    }
+    float lengthSquared = 0.0F;
+    for (const float lane : move) {
+        lengthSquared += lane * lane;
+    }
+    if (lengthSquared <= kMinimumLengthSquared) {
+        return teleport::Vector{};
+    }
+    // Normalised, so a diagonal is not faster than a straight line.
+    const float length = std::sqrt(lengthSquared);
+    for (float& lane : move) {
+        lane /= length;
+    }
+    return move;
+}
+
+/**
+ * Shortens a velocity to a speed limit, keeping its direction.
+ * @param velocity Velocity to limit in place.
+ * @param limit Highest speed to leave.
+ */
+void cap_speed(teleport::Vector& velocity, float limit) noexcept {
+    float speedSquared = 0.0F;
+    for (const float lane : velocity) {
+        speedSquared += lane * lane;
+    }
+    if (speedSquared <= limit * limit) {
+        return;
+    }
+    const float scale = limit / std::sqrt(speedSquared);
+    for (float& lane : velocity) {
+        lane *= scale;
+    }
+}
+
+/**
+ * Works out the velocity the keys ask for. Also records whether a press owns the vertical lane.
+ * @param speed Configured fly speed.
+ * @return Velocity in world units per second.
+ */
+[[nodiscard]] teleport::Vector desired_velocity(float speed) noexcept {
+    // The interface or another application owns the keyboard. Their presses must not steer.
+    std::array<bool, kDirectionCount> pressed{};
+    if (!core::ui::runtime::snapshot().visible && input::game_focused()) {
+        pressed = pressed_directions();
+    }
+    teleport::Vector forward{};
+    const teleport::Vector move =
+        teleport::camera_forward(forward) ? travel(pressed, forward) : teleport::Vector{};
+    g_steered = move[teleport::kVerticalLane] != 0.0F;
+    teleport::Vector velocity{};
+    for (std::size_t lane = 0; lane < teleport::kVectorLanes; ++lane) {
+        velocity[lane] = move[lane] * speed;
+    }
+    return velocity;
+}
+
+} // namespace
+
+/** Reads the toggle key once a frame and flips the switch on the press. */
+void poll_toggle() noexcept {
+    const client::movement::Settings settings = client::movement::get();
+    if (settings.flyToggleKey == client::movement::kNoKey) {
+        g_toggleDown.store(false, std::memory_order_relaxed);
+        return;
+    }
+    const bool down =
+        input::game_focused()
+        && (GetAsyncKeyState(static_cast<int>(settings.flyToggleKey)) & kKeyHeldBit) != 0;
+    // The interface owns the keyboard, so the key tracks the press but never flips the switch.
+    if (core::ui::runtime::snapshot().visible) {
+        g_toggleDown.store(down, std::memory_order_relaxed);
+        return;
+    }
+    if (down && !g_toggleDown.exchange(true, std::memory_order_acq_rel)) {
+        client::movement::Settings updated = settings;
+        updated.flyEnabled = !settings.flyEnabled;
+        if (!client::movement::publish(updated)) {
+            return;
+        }
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::info,
+                         updated.flyEnabled ? "ev=fly stage=toggle enabled=1"
+                                            : "ev=fly stage=toggle enabled=0");
+        return;
+    }
+    if (!down) {
+        g_toggleDown.store(false, std::memory_order_release);
+    }
+}
+
+/** Writes the player's velocity on the physics sync, which publishes it. */
+void apply(void* component) noexcept {
+    const client::movement::Settings settings = client::movement::get();
+    if (!settings.flyEnabled) {
+        return;
+    }
+    if (component == nullptr || !teleport::owns_local_player(component)) {
+        return;
+    }
+    if (!read_bindings()) {
+        return;
+    }
+    // Capped, because this is the field the game reads to decide the player hit something too
+    // hard. The step has the real speed; this is only what the sync publishes.
+    teleport::Vector velocity = desired_velocity(settings.flySpeed);
+    cap_speed(velocity, kPublishedSpeedCap);
+    (void)teleport::write_velocity(component, velocity);
+}
+
+/** Reports whether fly is on. */
+bool enabled() noexcept {
+    return client::movement::get().flyEnabled;
+}
+
+/** Sets the velocity the coming simulation step integrates. */
+void before_step(void* body) noexcept {
+    g_heightValid = false;
+    if (body == nullptr || !read_bindings()) {
+        return;
+    }
+    noclip::write_body_velocity(body, desired_velocity(client::movement::get().flySpeed));
+    noclip::Vector position{};
+    noclip::read_body_position(body, position);
+    g_heightBeforeStep = position[teleport::kVerticalLane];
+    g_heightValid = true;
+}
+
+/** Puts back the height the step's gravity took. */
+void after_step(void* body, bool heldElsewhere) noexcept {
+    if (body == nullptr || heldElsewhere || g_steered || !g_heightValid) {
+        return;
+    }
+    // The height from before this step, so a respawn or any other placement is kept.
+    noclip::Vector position{};
+    noclip::read_body_position(body, position);
+    position[teleport::kVerticalLane] = g_heightBeforeStep;
+    noclip::write_body_position(body, position);
+    // The step also left its gravity in the velocity, where it would build up.
+    noclip::Vector velocity{};
+    noclip::read_body_velocity(body, velocity);
+    velocity[teleport::kVerticalLane] = 0.0F;
+    noclip::write_body_velocity(body, velocity);
+}
+
+/** Clears the key state and the held height. The switch is a stored setting and survives. */
+void reset() noexcept {
+    g_toggleDown.store(false, std::memory_order_release);
+    g_heightValid = false;
+}
+
+} // namespace sunrise::client::hooks::fly

+ 39 - 0
Sunrise/src/client/hooks/fly/fly.h

@@ -0,0 +1,39 @@
+#pragma once
+
+namespace sunrise::client::hooks::fly {
+
+/**
+ * Fastest speed the game is shown while this hook drives the position itself. Contact with
+ * geometry damages the player above roughly this, and the real speed is not needed for movement.
+ */
+inline constexpr float kPublishedSpeedCap = 8.0F;
+
+/** Reads the toggle key once a frame and flips the switch on the press. */
+void poll_toggle() noexcept;
+
+/**
+ * Writes the player's velocity on the physics sync, which publishes it.
+ * @param component Physics component being synced. Tested for player ownership here.
+ */
+void apply(void* component) noexcept;
+
+/** @return True while fly is on. */
+[[nodiscard]] bool enabled() noexcept;
+
+/**
+ * Sets the velocity the coming simulation step integrates. Noclip reads it too.
+ * @param body Character rigid body. Live only inside the step hook.
+ */
+void before_step(void* body) noexcept;
+
+/**
+ * Puts back the height the step's gravity took.
+ * @param body Character rigid body. Live only inside the step hook.
+ * @param heldElsewhere True when noclip carries the vertical lane.
+ */
+void after_step(void* body, bool heldElsewhere) noexcept;
+
+/** Clears the key state. The switch is a stored setting and survives. */
+void reset() noexcept;
+
+} // namespace sunrise::client::hooks::fly

+ 132 - 70
Sunrise/src/client/hooks/noclip/horizontal_noclip.cpp

@@ -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>
 #include <Windows.h>
@@ -9,7 +10,7 @@
 #include <algorithm>
 #include <algorithm>
 #include <array>
 #include <array>
 #include <atomic>
 #include <atomic>
-#include <bit>
+#include <cmath>
 #include <cstddef>
 #include <cstddef>
 #include <cstdint>
 #include <cstdint>
 #include <cstdio>
 #include <cstdio>
@@ -18,8 +19,10 @@
 #include "../../../core/logging/log.h"
 #include "../../../core/logging/log.h"
 #include "../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../hooking/detour.h"
 #include "../../hooking/detour.h"
+#include "../../input/window_focus.h"
 #include "../../movement/movement_settings_store.h"
 #include "../../movement/movement_settings_store.h"
 #include "../../patterns/image_scan.h"
 #include "../../patterns/image_scan.h"
+#include "../fly/fly.h"
 #include "runtime.h"
 #include "runtime.h"
 
 
 namespace sunrise::client::hooks::noclip {
 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 kBodyPosition = 0x1C0;
 constexpr std::size_t kBodyVelocity = 0x230;
 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 kHorizontalX = 0;
 constexpr std::size_t kHorizontalY = 1;
 constexpr std::size_t kHorizontalY = 1;
+constexpr std::size_t kVertical = 2;
 constexpr std::size_t kVectorLanes = 4;
 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_installed{false};
 std::atomic_bool g_toggleDown{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. */
 /** Module-owned vtable target; unlike Havok objects, its address is stable until DLL teardown. */
 std::uintptr_t g_characterMotionVtable{};
 std::uintptr_t g_characterMotionVtable{};
 hooking::detour::Handle g_stepHandle{};
 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);
     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. */
 /** @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 {
 [[nodiscard]] bool valid_array(const HavokArray& array, std::int32_t maximum) noexcept {
     const std::uint32_t capacity = array.capacityAndFlags & kArrayCapacityMask;
     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;
     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.
  * 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.
  * 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);
         g_toggleDown.store(false, std::memory_order_relaxed);
         return settings.noclipEnabled;
         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.
     // An open interface owns the keyboard, so the bound key only tracks the press, it never flips.
     if (core::ui::runtime::snapshot().visible) {
     if (core::ui::runtime::snapshot().visible) {
         g_toggleDown.store(down, std::memory_order_relaxed);
         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)) {
         if (!client::movement::publish(updated)) {
             return settings.noclipEnabled;
             return settings.noclipEnabled;
         }
         }
-        invalidate_target();
         core::log::write(core::log::Channel::client,
         core::log::write(core::log::Channel::client,
                          core::log::Level::info,
                          core::log::Level::info,
                          updated.noclipEnabled
                          updated.noclipEnabled
@@ -202,71 +226,88 @@ template <typename T> [[nodiscard]] T& field(std::byte* object, std::size_t offs
     return client::movement::get().noclipEnabled;
     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::int32_t __fastcall havok_step(std::byte* simulation, float deltaTime) noexcept {
     std::array<float, kVectorLanes> nativeVelocity{};
     std::array<float, kVectorLanes> nativeVelocity{};
+    std::array<float, kVectorLanes> nativePosition{};
     const bool enabledBeforeStep = poll_toggle();
     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);
         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 HavokStep next = reinterpret_cast<HavokStep>(g_stepHandle.original);
     const std::int32_t result = next != nullptr ? next(simulation, deltaTime) : 0;
     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.
     // 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;
         return result;
     }
     }
-
     const float step = std::clamp(deltaTime, 0.0F, kMaximumStepSeconds);
     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) {
     if (step <= 0.0F || velocitySquared <= kMinimumVelocitySquared) {
         return result;
         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;
     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;
     return result;
 }
 }
 
 
@@ -314,7 +355,7 @@ bool install() noexcept {
     return true;
     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 {
 void uninstall() noexcept {
     if (!g_installed.exchange(false, std::memory_order_acq_rel)) {
     if (!g_installed.exchange(false, std::memory_order_acq_rel)) {
         return;
         return;
@@ -322,13 +363,34 @@ void uninstall() noexcept {
     (void)hooking::detour::uninstall(g_stepHandle);
     (void)hooking::detour::uninstall(g_stepHandle);
     g_stepHandle = {};
     g_stepHandle = {};
     g_characterMotionVtable = 0;
     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);
     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
 } // namespace sunrise::client::hooks::noclip

+ 20 - 2
Sunrise/src/client/hooks/noclip/runtime.h

@@ -1,7 +1,12 @@
 #pragma once
 #pragma once
 
 
+#include <array>
+
 namespace sunrise::client::hooks::noclip {
 namespace sunrise::client::hooks::noclip {
 
 
+/** Three lanes of a Havok vector. A write leaves the stored fourth lane alone. */
+using Vector = std::array<float, 3>;
+
 /**
 /**
  * Resolves the Havok targets and attaches the independent simulation-step detour.
  * Resolves the Havok targets and attaches the independent simulation-step detour.
  * @return True when both signatures resolve and the detour is attached.
  * @return True when both signatures resolve and the detour is attached.
@@ -11,7 +16,20 @@ namespace sunrise::client::hooks::noclip {
 /** Detaches the simulation-step detour and clears runtime state. */
 /** Detaches the simulation-step detour and clears runtime state. */
 void uninstall() noexcept;
 void uninstall() noexcept;
 
 
-/** Invalidates the horizontal target after another feature changes rigid-body position. */
-void invalidate_target() noexcept;
+/**
+ * Reads a live rigid body's world position.
+ * @param body Body from the simulation hook. Valid only inside that call.
+ * @param position Receives the three lanes.
+ */
+void read_body_position(void* body, Vector& position) noexcept;
+
+/** Writes a live rigid body's world position. @see read_body_position */
+void write_body_position(void* body, const Vector& position) noexcept;
+
+/** Reads a live rigid body's linear velocity. @see read_body_position */
+void read_body_velocity(void* body, Vector& velocity) noexcept;
+
+/** Writes a live rigid body's linear velocity. @see read_body_position */
+void write_body_velocity(void* body, const Vector& velocity) noexcept;
 
 
 } // namespace sunrise::client::hooks::noclip
 } // namespace sunrise::client::hooks::noclip

+ 68 - 30
Sunrise/src/client/hooks/sword_skate/sword_skate.cpp

@@ -1,27 +1,24 @@
 /**
 /**
- * Sword skate. A sword's air attack throws the player forward, and a glide started while that
- * throw is still carrying them keeps the speed for about a second. The client refuses to start a
- * glide while the throw is active, so the speed decays instead and the chain cannot be continued.
- *
- * The refusal is one bit of a movement-state field on the player's physics component. It is set
- * when the air attack starts and cleared when it ends, and the glide reads it before anything
- * else. Clearing it on the tick the jump is pressed lets the client start its own glide, with its
- * own lift, drift and air control; nothing here moves the player.
- *
- * The bit was found by comparing two builds through the same injected inputs at the same timings:
- * one where the glide starts after a sword throw and one where it is refused. The throw itself is
- * identical on both, reaching the same speed, and a glide the client accepts on its own decays at
- * the same rate on both, so neither the throw nor the glide was changed. Only the refusal was.
+ * Sword skate. A sword's air attack throws the player forward, and a glide started during the
+ * throw keeps that speed. The client refuses the glide while the throw is active.
+ * The refusal is one bit of a movement-state field, set by the attack and read by the glide.
+ * Clearing it on the tick jump is pressed lets the client start its own glide.
+ * Nothing here moves the player.
  */
  */
 
 
 #include "sword_skate.h"
 #include "sword_skate.h"
 
 
 #include <Windows.h>
 #include <Windows.h>
 
 
+#include <array>
 #include <cstddef>
 #include <cstddef>
 #include <cstdint>
 #include <cstdint>
+#include <optional>
 
 
 #include "../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../../core/ui/runtime/ui_visibility_runtime.h"
+#include "../../../state/account/account_state.h"
+#include "../../../state/runtime/runtime.h"
+#include "../../input/window_focus.h"
 #include "../../movement/movement_settings_store.h"
 #include "../../movement/movement_settings_store.h"
 #include "../teleport/runtime.h"
 #include "../teleport/runtime.h"
 
 
@@ -31,16 +28,23 @@ namespace {
 /** Movement-state flags on the player's physics component. */
 /** Movement-state flags on the player's physics component. */
 constexpr std::size_t kMovementStateOffset = 15492;
 constexpr std::size_t kMovementStateOffset = 15492;
 /**
 /**
- * Set while a sword's air attack is carrying the player, and read by the glide before it starts.
- * The field carries other bits that the attack also sets; only this one refuses the glide, and
- * clearing only it leaves the rest of the attack's state alone.
+ * Set while a sword's air attack carries the player, and read by the glide before it starts.
+ * The attack sets other bits in the same field. Only this one refuses the glide, so only it is
+ * cleared.
  */
  */
 constexpr std::uint32_t kGlideRefusedBit = 0x00000800U;
 constexpr std::uint32_t kGlideRefusedBit = 0x00000800U;
 /** The high bit of a polled key state marks it held. */
 /** The high bit of a polled key state marks it held. */
 constexpr SHORT kKeyHeldBit = static_cast<SHORT>(0x8000);
 constexpr SHORT kKeyHeldBit = static_cast<SHORT>(0x8000);
+/** The action this watches. Its binding is the player's jump key. */
+constexpr std::uint16_t kJumpAction =
+    static_cast<std::uint16_t>(state::account::settings::bindings::Action::jump);
 
 
 /** Jump held on the previous tick, so the flag is only cleared on the press and not on the hold. */
 /** Jump held on the previous tick, so the flag is only cleared on the press and not on the hold. */
 bool g_jumpHeld{false};
 bool g_jumpHeld{false};
+/** Both halves of the jump binding. An unbound half stays empty. */
+std::array<std::optional<std::uint16_t>, 2> g_jumpBinding{};
+/** Set once the binding is read, so the costly account snapshot runs once. */
+bool g_bindingRead{false};
 
 
 /**
 /**
  * Reads one value out of game memory without faulting on a torn pointer.
  * Reads one value out of game memory without faulting on a torn pointer.
@@ -66,34 +70,68 @@ bool g_jumpHeld{false};
            && written == sizeof value;
            && written == sizeof value;
 }
 }
 
 
+/**
+ * Takes the account's jump binding once it is loaded.
+ * @return True when the binding has been read, whether or not either half is bound.
+ */
+[[nodiscard]] bool read_jump_binding() noexcept {
+    if (g_bindingRead) {
+        return true;
+    }
+    // The snapshot copies the whole account, so it stops once the bindings arrive.
+    const state::AccountState account = state::account_snapshot();
+    if (!account.settings.keyBindings.configured) {
+        return false;
+    }
+    const auto& binding = account.settings.keyBindings.values[kJumpAction];
+    g_jumpBinding = {binding.primary, binding.secondary};
+    g_bindingRead = true;
+    return true;
+}
+
+/**
+ * A half is an input code, not a virtual key. A half bound to a mouse button gives no key.
+ * @return True while either half of the jump binding is down.
+ */
+[[nodiscard]] bool jump_down() noexcept {
+    for (const std::optional<std::uint16_t>& half : g_jumpBinding) {
+        if (!half.has_value()) {
+            continue;
+        }
+        const std::uint32_t key = teleport::action_key(*half);
+        if (key != 0 && (GetAsyncKeyState(static_cast<int>(key)) & kKeyHeldBit) != 0) {
+            return true;
+        }
+    }
+    return false;
+}
+
 } // namespace
 } // namespace
 
 
 /** Clears the glide refusal for one physics tick of the local player. */
 /** Clears the glide refusal for one physics tick of the local player. */
 void apply(void* component) noexcept {
 void apply(void* component) noexcept {
     const client::movement::Settings settings = client::movement::get();
     const client::movement::Settings settings = client::movement::get();
-    // An open interface owns the keyboard, so a press meant for it must not reach this either.
-    const bool usable = settings.swordSkateEnabled
-                        && settings.swordSkateJumpKey != client::movement::kNoKey
-                        && !core::ui::runtime::snapshot().visible;
+    // The interface or another application owns the keyboard. Their presses must not reach this.
+    const bool usable = settings.swordSkateEnabled && !core::ui::runtime::snapshot().visible
+                        && input::game_focused();
     if (!usable) {
     if (!usable) {
-        // Cleared rather than left as it was, or the first press after the feature comes back is
-        // read as a hold and skipped.
+        // Cleared, or the first press after the feature comes back reads as a hold and is skipped.
         g_jumpHeld = false;
         g_jumpHeld = false;
         return;
         return;
     }
     }
-    // Ownership is tested before the key is read, because this runs for every component the sync
-    // touches and the held flag must only ever advance on the player's own tick. Advancing it on
-    // any other component lets that component consume the press, and the player's tick then sees
-    // no edge at all.
+    // Ownership is tested before the key is read. The held flag must only advance on the player's
+    // own tick, or another component consumes the press and the player's tick sees no edge.
     if (component == nullptr || !teleport::owns_local_player(component)) {
     if (component == nullptr || !teleport::owns_local_player(component)) {
         return;
         return;
     }
     }
-    const bool held =
-        (GetAsyncKeyState(static_cast<int>(settings.swordSkateJumpKey)) & kKeyHeldBit) != 0;
+    if (!read_jump_binding()) {
+        return;
+    }
+    const bool held = jump_down();
     const bool wasHeld = g_jumpHeld;
     const bool wasHeld = g_jumpHeld;
     g_jumpHeld = held;
     g_jumpHeld = held;
-    // Only the press matters. Clearing the flag for as long as the key is down would keep it clear
-    // through the whole attack, which is a different change to make and not this one.
+    // Only the press matters. Clearing it for as long as the key is down would hold it clear
+    // through the whole attack.
     if (!held || wasHeld) {
     if (!held || wasHeld) {
         return;
         return;
     }
     }

+ 0 - 5
Sunrise/src/client/hooks/teleport/internal.h

@@ -52,9 +52,4 @@ inline constexpr std::size_t kBodyPositionX = 448;
 /** Rigid-body velocity. The sync copies this into the physics component every tick. */
 /** Rigid-body velocity. The sync copies this into the physics component every tick. */
 inline constexpr std::size_t kBodyVelocityX = 560;
 inline constexpr std::size_t kBodyVelocityX = 560;
 
 
-/** Three floats make one position or velocity vector. */
-inline constexpr std::size_t kVectorLanes = 3;
-/** The vertical lane, given the camera basis above. */
-inline constexpr std::size_t kVerticalLane = 2;
-
 } // namespace sunrise::client::hooks::teleport
 } // namespace sunrise::client::hooks::teleport

+ 48 - 0
Sunrise/src/client/hooks/teleport/runtime.h

@@ -1,10 +1,19 @@
 #pragma once
 #pragma once
 
 
+#include <array>
 #include <cstddef>
 #include <cstddef>
 #include <cstdint>
 #include <cstdint>
 
 
 namespace sunrise::client::hooks::teleport {
 namespace sunrise::client::hooks::teleport {
 
 
+/** Three floats make one position or velocity vector. */
+inline constexpr std::size_t kVectorLanes = 3;
+/** The vertical lane. The camera basis is X forward, Z up, so the third lane is up. */
+inline constexpr std::size_t kVerticalLane = 2;
+
+/** One world-space vector as the game stores it. */
+using Vector = std::array<float, kVectorLanes>;
+
 /** Writes the local player's controlled-object handle, or the invalid sentinel. */
 /** Writes the local player's controlled-object handle, or the invalid sentinel. */
 using ControlledHandle = std::uint32_t* (*)(std::uint32_t*);
 using ControlledHandle = std::uint32_t* (*)(std::uint32_t*);
 /** Returns the camera pose block array. The pointer in its global is obfuscated, so we call it. */
 /** Returns the camera pose block array. The pointer in its global is obfuscated, so we call it. */
@@ -78,4 +87,43 @@ void apply_pending(void* component) noexcept;
  */
  */
 [[nodiscard]] bool owns_local_player(void* component) noexcept;
 [[nodiscard]] bool owns_local_player(void* component) noexcept;
 
 
+/**
+ * Reads the world position of the body a physics component drives.
+ * @param component Physics component.
+ * @param position Receives the three lanes.
+ * @return True when the body was found and read.
+ */
+[[nodiscard]] bool read_position(void* component, Vector& position) noexcept;
+
+/**
+ * Writes the world position of the body a physics component drives.
+ * @param component Physics component.
+ * @param position Three lanes to store.
+ * @return True when the body was found and written.
+ */
+[[nodiscard]] bool write_position(void* component, const Vector& position) noexcept;
+
+/**
+ * Reads the linear velocity of the body a physics component drives.
+ * @param component Physics component.
+ * @param velocity Receives the three lanes.
+ * @return True when the body was found and read.
+ */
+[[nodiscard]] bool read_velocity(void* component, Vector& velocity) noexcept;
+
+/**
+ * Writes the linear velocity of the body a physics component drives.
+ * @param component Physics component.
+ * @param velocity Three lanes to store.
+ * @return True when the body was found and written.
+ */
+[[nodiscard]] bool write_velocity(void* component, const Vector& velocity) noexcept;
+
+/**
+ * The camera hook is the only site that sees the pose block, so it publishes the vector here.
+ * @param forward Receives the camera forward vector published this frame.
+ * @return True once the camera hook has published one.
+ */
+[[nodiscard]] bool camera_forward(Vector& forward) noexcept;
+
 } // namespace sunrise::client::hooks::teleport
 } // namespace sunrise::client::hooks::teleport

+ 11 - 0
Sunrise/src/client/hooks/teleport/teleport_action_key.cpp

@@ -40,6 +40,13 @@ constexpr std::size_t kKeyTableCount = 105;
 constexpr std::uint16_t kKeyCodeMask = 0x00FF;
 constexpr std::uint16_t kKeyCodeMask = 0x00FF;
 /** The table byte meaning the index resolves through its scan code instead. */
 /** The table byte meaning the index resolves through its scan code instead. */
 constexpr std::uint8_t kAbsentVirtualKey = 0xFF;
 constexpr std::uint8_t kAbsentVirtualKey = 0xFF;
+/** First either-side modifier code. These sit above the key table and are not indexed by it. */
+constexpr std::uint16_t kFirstModifierCode = 105;
+/**
+ * Virtual keys for the four either-side codes: shift, control, key_windows, alt.
+ * Windows has no either-side windows key, so that one resolves to the left key alone.
+ */
+constexpr std::array<std::uint8_t, 4> kModifierKeys{VK_SHIFT, VK_CONTROL, VK_LWIN, VK_MENU};
 
 
 const std::uint8_t* g_virtualKeys{};
 const std::uint8_t* g_virtualKeys{};
 const std::uint8_t* g_scanCodes{};
 const std::uint8_t* g_scanCodes{};
@@ -92,6 +99,10 @@ void clear_action_keys() noexcept {
 std::uint32_t action_key(std::uint16_t binding) noexcept {
 std::uint32_t action_key(std::uint16_t binding) noexcept {
     // The key code is the low byte. The bits above it are modifiers, which the tables do not index.
     // The key code is the low byte. The bits above it are modifiers, which the tables do not index.
     const std::uint16_t index = binding & kKeyCodeMask;
     const std::uint16_t index = binding & kKeyCodeMask;
+    // A binding may name a modifier on its own. Crouch does by default.
+    if (index >= kFirstModifierCode && index < kFirstModifierCode + kModifierKeys.size()) {
+        return kModifierKeys[index - kFirstModifierCode];
+    }
     if (g_virtualKeys == nullptr || g_scanCodes == nullptr || index >= kKeyTableCount) {
     if (g_virtualKeys == nullptr || g_scanCodes == nullptr || index >= kKeyTableCount) {
         return 0;
         return 0;
     }
     }

+ 5 - 0
Sunrise/src/client/hooks/teleport/teleport_lifecycle.cpp

@@ -13,6 +13,7 @@
 
 
 #include "../../../core/logging/log.h"
 #include "../../../core/logging/log.h"
 #include "../../hooking/detour.h"
 #include "../../hooking/detour.h"
+#include "../fly/fly.h"
 #include "../polled_input/runtime.h"
 #include "../polled_input/runtime.h"
 #include "../sword_skate/sword_skate.h"
 #include "../sword_skate/sword_skate.h"
 #include "internal.h"
 #include "internal.h"
@@ -78,6 +79,8 @@ std::int64_t __fastcall camera_transform(std::uint32_t playerIndex) noexcept {
     capture_forward(playerIndex);
     capture_forward(playerIndex);
     poll_request();
     poll_request();
     force_pending();
     force_pending();
+    // Read here, not on the physics tick: that tick stops for a player who is standing still.
+    hooks::fly::poll_toggle();
     return result;
     return result;
 }
 }
 
 
@@ -93,6 +96,7 @@ std::int64_t __fastcall physics_sync(std::byte* component, std::byte* outFlags)
     // Shares this detour rather than adding a second one to the same function. The flag it clears
     // Shares this detour rather than adding a second one to the same function. The flag it clears
     // is written and read inside this tick, so it has to run here and not on a frame poll.
     // is written and read inside this tick, so it has to run here and not on a frame poll.
     hooks::sword_skate::apply(component);
     hooks::sword_skate::apply(component);
+    hooks::fly::apply(component);
     const PhysicsSync next = original<PhysicsSync>(kPhysicsSlot);
     const PhysicsSync next = original<PhysicsSync>(kPhysicsSlot);
     return next != nullptr ? next(component, outFlags) : 0;
     return next != nullptr ? next(component, outFlags) : 0;
 }
 }
@@ -190,6 +194,7 @@ void uninstall() noexcept {
     }
     }
     clear_targets();
     clear_targets();
     clear_action_keys();
     clear_action_keys();
+    hooks::fly::reset();
     polled_input::release_key();
     polled_input::release_key();
     (void)hooking::detour::uninstall(g_handles);
     (void)hooking::detour::uninstall(g_handles);
     g_handles = {};
     g_handles = {};

+ 49 - 6
Sunrise/src/client/hooks/teleport/teleport_move.cpp

@@ -16,8 +16,8 @@
 #include "../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../../state/account/account_state.h"
 #include "../../../state/account/account_state.h"
 #include "../../../state/runtime/runtime.h"
 #include "../../../state/runtime/runtime.h"
+#include "../../input/window_focus.h"
 #include "../../movement/movement_settings_store.h"
 #include "../../movement/movement_settings_store.h"
-#include "../noclip/runtime.h"
 #include "../polled_input/runtime.h"
 #include "../polled_input/runtime.h"
 #include "internal.h"
 #include "internal.h"
 #include "runtime.h"
 #include "runtime.h"
@@ -378,7 +378,8 @@ void poll_request() noexcept {
         g_keyDown.store(false, std::memory_order_relaxed);
         g_keyDown.store(false, std::memory_order_relaxed);
         return;
         return;
     }
     }
-    const bool down = (GetAsyncKeyState(static_cast<int>(settings.virtualKey)) & 0x8000) != 0;
+    const bool down = client::input::game_focused()
+                      && (GetAsyncKeyState(static_cast<int>(settings.virtualKey)) & 0x8000) != 0;
     if (down && !g_keyDown.exchange(down, std::memory_order_relaxed)) {
     if (down && !g_keyDown.exchange(down, std::memory_order_relaxed)) {
         g_requestAge.store(0, std::memory_order_relaxed);
         g_requestAge.store(0, std::memory_order_relaxed);
         g_requested.store(true, std::memory_order_release);
         g_requested.store(true, std::memory_order_release);
@@ -408,9 +409,7 @@ void apply_pending(void* component) noexcept {
         return;
         return;
     }
     }
     g_requested.store(false, std::memory_order_release);
     g_requested.store(false, std::memory_order_release);
-    if (perform_move(physics)) {
-        noclip::invalidate_target();
-    }
+    (void)perform_move(physics);
 }
 }
 
 
 /** Runs the move for a request no physics tick collected. */
 /** Runs the move for a request no physics tick collected. */
@@ -429,7 +428,6 @@ void force_pending() noexcept {
     if (!perform_move(physics)) {
     if (!perform_move(physics)) {
         return;
         return;
     }
     }
-    noclip::invalidate_target();
     invoke_sync(physics);
     invoke_sync(physics);
     core::log::write(
     core::log::write(
         core::log::Channel::client, core::log::Level::info, "ev=teleport stage=force result=ok");
         core::log::Channel::client, core::log::Level::info, "ev=teleport stage=force result=ok");
@@ -441,4 +439,49 @@ bool owns_local_player(void* component) noexcept {
            && owns_player(static_cast<std::byte*>(component));
            && owns_player(static_cast<std::byte*>(component));
 }
 }
 
 
+/** Reads the world position of the body a physics component drives. */
+bool read_position(void* component, Vector& position) noexcept {
+    if (component == nullptr) {
+        return false;
+    }
+    std::byte* const body = body_of(static_cast<std::byte*>(component));
+    return body != nullptr && read_at(body + kBodyPositionX, position);
+}
+
+/** Writes the world position of the body a physics component drives. */
+bool write_position(void* component, const Vector& position) noexcept {
+    if (component == nullptr) {
+        return false;
+    }
+    std::byte* const body = body_of(static_cast<std::byte*>(component));
+    return body != nullptr && write_vector(body + kBodyPositionX, position);
+}
+
+/** Reads the linear velocity of the body a physics component drives. */
+bool read_velocity(void* component, Vector& velocity) noexcept {
+    if (component == nullptr) {
+        return false;
+    }
+    std::byte* const body = body_of(static_cast<std::byte*>(component));
+    return body != nullptr && read_at(body + kBodyVelocityX, velocity);
+}
+
+/** Writes the linear velocity of the body a physics component drives. */
+bool write_velocity(void* component, const Vector& velocity) noexcept {
+    if (component == nullptr) {
+        return false;
+    }
+    std::byte* const body = body_of(static_cast<std::byte*>(component));
+    return body != nullptr && write_vector(body + kBodyVelocityX, velocity);
+}
+
+/** Reports the camera forward vector published this frame. */
+bool camera_forward(Vector& forward) noexcept {
+    if (!g_forwardValid.load(std::memory_order_acquire)) {
+        return false;
+    }
+    forward = g_forward;
+    return true;
+}
+
 } // namespace sunrise::client::hooks::teleport
 } // namespace sunrise::client::hooks::teleport

+ 23 - 0
Sunrise/src/client/input/window_focus.cpp

@@ -0,0 +1,23 @@
+/**
+ * Whether the game holds focus. The test is the process, not one window.
+ * The game has several windows, and which one is in front changes with the display mode.
+ */
+
+#include "window_focus.h"
+
+#include <Windows.h>
+
+namespace sunrise::client::input {
+
+/** Reports whether a window of this process is in front. */
+bool game_focused() noexcept {
+    const HWND foreground = GetForegroundWindow();
+    if (foreground == nullptr) {
+        return false;
+    }
+    DWORD processId = 0;
+    (void)GetWindowThreadProcessId(foreground, &processId);
+    return processId == GetCurrentProcessId();
+}
+
+} // namespace sunrise::client::input

+ 12 - 0
Sunrise/src/client/input/window_focus.h

@@ -0,0 +1,12 @@
+#pragma once
+
+namespace sunrise::client::input {
+
+/**
+ * The asynchronous key state reports a key held whichever application owns it. Every movement key
+ * poll is gated on this, because the game stops acting on input when it loses focus.
+ * @return True while a window of this process is in front.
+ */
+[[nodiscard]] bool game_focused() noexcept;
+
+} // namespace sunrise::client::input

+ 20 - 6
Sunrise/src/client/movement/movement_settings_store.cpp

@@ -8,6 +8,7 @@
 
 
 #include <Windows.h>
 #include <Windows.h>
 
 
+#include <algorithm>
 #include <array>
 #include <array>
 #include <cstddef>
 #include <cstddef>
 #include <cstdio>
 #include <cstdio>
@@ -39,7 +40,8 @@ bool g_pathResolved{};
     return settings.distance >= kMinimumDistance && settings.distance <= kMaximumDistance
     return settings.distance >= kMinimumDistance && settings.distance <= kMaximumDistance
            && settings.virtualKey <= kMaximumVirtualKey
            && settings.virtualKey <= kMaximumVirtualKey
            && settings.noclipToggleKey <= kMaximumVirtualKey
            && settings.noclipToggleKey <= kMaximumVirtualKey
-           && settings.swordSkateJumpKey <= kMaximumVirtualKey;
+           && settings.flyToggleKey <= kMaximumVirtualKey && settings.flySpeed >= kMinimumFlySpeed
+           && settings.flySpeed <= kMaximumFlySpeed;
 }
 }
 
 
 /** @param reason Key naming the step that failed. */
 /** @param reason Key naming the step that failed. */
@@ -130,9 +132,17 @@ void parse(std::string_view text, Settings& output) noexcept {
     if (scalar_for(text, "\"sword_skate_enabled\"", scalar)) {
     if (scalar_for(text, "\"sword_skate_enabled\"", scalar)) {
         output.swordSkateEnabled = scalar.starts_with("true");
         output.swordSkateEnabled = scalar.starts_with("true");
     }
     }
-    if (scalar_for(text, "\"sword_skate_jump_key\"", scalar) && terminated(scalar, buffer)) {
-        output.swordSkateJumpKey =
-            static_cast<std::uint32_t>(std::strtoul(buffer.data(), nullptr, 0));
+    if (scalar_for(text, "\"fly_enabled\"", scalar)) {
+        output.flyEnabled = scalar.starts_with("true");
+    }
+    if (scalar_for(text, "\"fly_toggle_key\"", scalar) && terminated(scalar, buffer)) {
+        output.flyToggleKey = static_cast<std::uint32_t>(std::strtoul(buffer.data(), nullptr, 0));
+    }
+    if (scalar_for(text, "\"fly_speed\"", scalar) && terminated(scalar, buffer)) {
+        // Clamped, not refused. A speed saved before the maximum came down would otherwise fail
+        // the range check and take every other movement setting with it.
+        output.flySpeed =
+            std::clamp(std::strtof(buffer.data(), nullptr), kMinimumFlySpeed, kMaximumFlySpeed);
     }
     }
 }
 }
 
 
@@ -154,14 +164,18 @@ void parse(std::string_view text, Settings& output) noexcept {
                                    "  \"noclip_enabled\": %s,\n"
                                    "  \"noclip_enabled\": %s,\n"
                                    "  \"noclip_toggle_key\": %u,\n"
                                    "  \"noclip_toggle_key\": %u,\n"
                                    "  \"sword_skate_enabled\": %s,\n"
                                    "  \"sword_skate_enabled\": %s,\n"
-                                   "  \"sword_skate_jump_key\": %u\n}\n",
+                                   "  \"fly_enabled\": %s,\n"
+                                   "  \"fly_toggle_key\": %u,\n"
+                                   "  \"fly_speed\": %.3f\n}\n",
                                    settings.enabled ? "true" : "false",
                                    settings.enabled ? "true" : "false",
                                    static_cast<double>(settings.distance),
                                    static_cast<double>(settings.distance),
                                    static_cast<unsigned>(settings.virtualKey),
                                    static_cast<unsigned>(settings.virtualKey),
                                    settings.noclipEnabled ? "true" : "false",
                                    settings.noclipEnabled ? "true" : "false",
                                    static_cast<unsigned>(settings.noclipToggleKey),
                                    static_cast<unsigned>(settings.noclipToggleKey),
                                    settings.swordSkateEnabled ? "true" : "false",
                                    settings.swordSkateEnabled ? "true" : "false",
-                                   static_cast<unsigned>(settings.swordSkateJumpKey));
+                                   settings.flyEnabled ? "true" : "false",
+                                   static_cast<unsigned>(settings.flyToggleKey),
+                                   static_cast<double>(settings.flySpeed));
     if (size <= 0) {
     if (size <= 0) {
         return false;
         return false;
     }
     }

+ 12 - 4
Sunrise/src/client/movement/movement_settings_store.h

@@ -12,8 +12,13 @@ inline constexpr float kMinimumDistance = 1.0F;
 inline constexpr float kMaximumDistance = 100.0F;
 inline constexpr float kMaximumDistance = 100.0F;
 /** No key is bound until one is picked, so a fresh install cannot fire a movement feature. */
 /** No key is bound until one is picked, so a fresh install cannot fire a movement feature. */
 inline constexpr std::uint32_t kNoKey = 0;
 inline constexpr std::uint32_t kNoKey = 0;
-/** Space is the stock jump binding, and the one the sword skate fix has to watch to do anything. */
-inline constexpr std::uint32_t kDefaultJumpKey = 0x20;
+
+/** Default fly speed, in world units per second. */
+inline constexpr float kDefaultFlySpeed = 15.0F;
+/** Slowest offered fly speed. Below this a press does not visibly move the player. */
+inline constexpr float kMinimumFlySpeed = 1.0F;
+/** Fastest offered fly speed. Past this the player outruns what the map streams in. */
+inline constexpr float kMaximumFlySpeed = 100.0F;
 
 
 /** Runtime movement configuration. This module owns it; Core settings do not carry it. */
 /** Runtime movement configuration. This module owns it; Core settings do not carry it. */
 struct Settings {
 struct Settings {
@@ -22,9 +27,12 @@ struct Settings {
     std::uint32_t virtualKey{kNoKey};
     std::uint32_t virtualKey{kNoKey};
     bool noclipEnabled{false};
     bool noclipEnabled{false};
     std::uint32_t noclipToggleKey{kNoKey};
     std::uint32_t noclipToggleKey{kNoKey};
+    /** The jump key comes from the account binding, so none is stored here. */
     bool swordSkateEnabled{false};
     bool swordSkateEnabled{false};
-    /** The player's jump key. The refusal is only cleared on the tick this key goes down. */
-    std::uint32_t swordSkateJumpKey{kDefaultJumpKey};
+    bool flyEnabled{false};
+    std::uint32_t flyToggleKey{kNoKey};
+    /** World units per second while a direction is pressed. */
+    float flySpeed{kDefaultFlySpeed};
 };
 };
 
 
 /**
 /**

+ 31 - 14
Sunrise/src/client/ui/movement/movement_panel.cpp

@@ -29,7 +29,7 @@ enum class CaptureTarget {
     none,
     none,
     teleport,
     teleport,
     noclip,
     noclip,
-    swordSkate,
+    fly,
 };
 };
 
 
 CaptureTarget g_capturing{CaptureTarget::none};
 CaptureTarget g_capturing{CaptureTarget::none};
@@ -168,8 +168,7 @@ void draw() noexcept {
     ImGui::Spacing();
     ImGui::Spacing();
     ImGui::TextUnformatted("Noclip");
     ImGui::TextUnformatted("Noclip");
     ImGui::Separator();
     ImGui::Separator();
-    ImGui::TextWrapped("Uses native horizontal rigid-body velocity while preserving the game's "
-                       "vertical movement. The bound key turns it on and off in game.");
+    ImGui::TextWrapped("Disable collision on the horizontal axis.");
     ImGui::Spacing();
     ImGui::Spacing();
 
 
     changed =
     changed =
@@ -185,27 +184,45 @@ void draw() noexcept {
 
 
     ImGui::Spacing();
     ImGui::Spacing();
     ImGui::Spacing();
     ImGui::Spacing();
-    ImGui::TextUnformatted("Sword Skate Fix");
+    ImGui::TextUnformatted("Fly");
     ImGui::Separator();
     ImGui::Separator();
-    ImGui::TextWrapped("A sword's air attack throws you forward, and a glide started while that "
-                       "throw is still carrying you keeps the speed. The client refuses to start a "
-                       "glide during the throw. This clears that refusal on the tick you press "
-                       "jump, and the client's own glide runs from there.");
+    ImGui::TextWrapped("Fly with your movement keys.");
     ImGui::Spacing();
     ImGui::Spacing();
 
 
+    changed = core::ui::components::toggle::control("Enabled##fly", settings.flyEnabled) || changed;
+
+    ImGui::Spacing();
+    ImGui::AlignTextToFramePadding();
+    ImGui::TextUnformatted("Toggle key");
+    ImGui::SameLine(labelWidth);
     changed =
     changed =
-        core::ui::components::toggle::control("Enabled##sword_skate", settings.swordSkateEnabled)
-        || changed;
+        key_picker("fly_key", CaptureTarget::fly, settings.flyToggleKey, controlWidth) || changed;
 
 
     ImGui::Spacing();
     ImGui::Spacing();
     ImGui::AlignTextToFramePadding();
     ImGui::AlignTextToFramePadding();
-    ImGui::TextUnformatted("Jump key");
+    ImGui::TextUnformatted("Speed");
     ImGui::SameLine(labelWidth);
     ImGui::SameLine(labelWidth);
+    ImGui::SetNextItemWidth(controlWidth);
+    float flySpeed = settings.flySpeed;
+    if (ImGui::SliderFloat("##fly_speed",
+                           &flySpeed,
+                           client::movement::kMinimumFlySpeed,
+                           client::movement::kMaximumFlySpeed,
+                           "%.0f units/s")) {
+        settings.flySpeed = flySpeed;
+        changed = true;
+    }
+
+    ImGui::Spacing();
+    ImGui::Spacing();
+    ImGui::TextUnformatted("Sword Skate Fix");
+    ImGui::Separator();
+    ImGui::TextWrapped("Disable sword swings blocking ability usage.");
+    ImGui::Spacing();
+
     changed =
     changed =
-        key_picker(
-            "sword_skate_key", CaptureTarget::swordSkate, settings.swordSkateJumpKey, controlWidth)
+        core::ui::components::toggle::control("Enabled##sword_skate", settings.swordSkateEnabled)
         || changed;
         || changed;
-    ImGui::TextWrapped("Must match the key the game jumps on. Nothing happens on any other key.");
 
 
     if (changed && !client::movement::publish(settings)) {
     if (changed && !client::movement::publish(settings)) {
         ImGui::Spacing();
         ImGui::Spacing();