mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-25 07:14:41 +01:00
Merge pull request #148 from Vebjorhk/feat/r3-first-person-camera-cycle
First person as a step in the R3 camera height cycle (opt-in)
This commit is contained in:
10 files changed
+210
-1
No files matched your search
@@ -136,6 +136,9 @@ if(DINGOSDK_BUILD_MULTIPLAYER_TESTS AND WIN32)
|
||||
add_executable(dingosdk_first_person_stabilize_tests Engine/Game/Skater/Test/first_person_stabilize_tests.cpp)
|
||||
target_include_directories(dingosdk_first_person_stabilize_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME first_person_stabilize COMMAND dingosdk_first_person_stabilize_tests)
|
||||
add_executable(dingosdk_first_person_cycle_tests Engine/Game/Skater/Test/first_person_cycle_tests.cpp)
|
||||
target_include_directories(dingosdk_first_person_cycle_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME first_person_cycle COMMAND dingosdk_first_person_cycle_tests)
|
||||
add_executable(dingosdk_playstation_report_tests Engine/Game/Input/Test/playstation_report_tests.cpp)
|
||||
target_include_directories(dingosdk_playstation_report_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME playstation_report COMMAND dingosdk_playstation_report_tests)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "Engine/Game/Skater/first_person_cycle.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string_view>
|
||||
|
||||
// First person in the camera cycle (first_person::cycle_height_write): the game's camera height
|
||||
// button flips UseHighCam; the option turns that into high -> low -> first person -> high.
|
||||
namespace {
|
||||
using namespace dingosdk::first_person;
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, std::string_view what) {
|
||||
if (condition) return;
|
||||
++failures;
|
||||
std::printf("FAIL: %.*s\n", static_cast<int>(what.size()), what.data());
|
||||
}
|
||||
bool same(const CycleWrite& write, bool high, CycleStep step) { return write.high == high && write.step == step; }
|
||||
|
||||
void off() {
|
||||
for (const bool first_person : {false, true})
|
||||
for (const bool high : {false, true})
|
||||
expect(same(cycle_height_write({false, first_person, true}, high), high, CycleStep::none),
|
||||
"with the option off the game's height passes through and first person is left alone");
|
||||
}
|
||||
|
||||
void steps() {
|
||||
expect(same(cycle_height_write({true, false, true}, false), false, CycleStep::none), "high -> low is the game's own step");
|
||||
expect(same(cycle_height_write({true, false, true}, true), false, CycleStep::enter),
|
||||
"low -> first person keeps the low camera behind it");
|
||||
expect(same(cycle_height_write({true, true, false}, true), true, CycleStep::leave),
|
||||
"first person (low behind it) -> the high camera");
|
||||
expect(same(cycle_height_write({true, true, false}, false), true, CycleStep::leave),
|
||||
"first person switched on in the menu over the high camera still lands on high");
|
||||
expect(same(cycle_height_write({true, false, false}, true), true, CycleStep::none),
|
||||
"where first person cannot start (on foot, Freecam) low -> high is the game's own step");
|
||||
}
|
||||
|
||||
// The game reads the height it keeps and writes the opposite on every press.
|
||||
void presses() {
|
||||
for (const bool start_high : {true, false}) {
|
||||
bool high = start_high, first_person = false;
|
||||
const auto press = [&] {
|
||||
const auto write = cycle_height_write({true, first_person, !first_person}, !high);
|
||||
high = write.high;
|
||||
if (write.step == CycleStep::enter) first_person = true;
|
||||
if (write.step == CycleStep::leave) first_person = false;
|
||||
};
|
||||
if (!start_high) press(); // low -> first person
|
||||
if (!start_high) press(); // first person -> high
|
||||
for (int round = 0; round < 3; ++round) {
|
||||
expect(high && !first_person, "the cycle starts each round on the high camera");
|
||||
press();
|
||||
expect(!high && !first_person, "first press: low camera");
|
||||
press();
|
||||
expect(!high && first_person, "second press: first person over the low camera");
|
||||
press();
|
||||
}
|
||||
expect(high && !first_person, "the third press of each round returns to the high camera");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
off();
|
||||
steps();
|
||||
presses();
|
||||
if (failures) {
|
||||
std::printf("%d first-person cycle test(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
std::printf("first-person cycle tests passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace dingosdk::first_person {
|
||||
// "First person in the camera cycle". With the game's Camera height control on, its camera
|
||||
// height button (R3) writes the profile option UseHighCam as the opposite of the height it reads
|
||||
// (true is the high camera). With the option on, ReSkate turns that into high -> low -> first
|
||||
// person -> high by changing the value written: the step into first person keeps the low camera
|
||||
// behind it, and the step out of first person always lands on the high camera.
|
||||
enum class CycleStep : std::uint8_t { none, enter, leave };
|
||||
struct CycleState {
|
||||
bool enabled{}; // the option is on and the local skater is in control
|
||||
bool first_person{}; // first person has the camera
|
||||
bool can_enter{}; // first person can take the camera now: on the board, no Freecam
|
||||
};
|
||||
struct CycleWrite {
|
||||
bool high{}; // the height the game keeps
|
||||
CycleStep step{}; // what first person does
|
||||
};
|
||||
constexpr CycleWrite cycle_height_write(const CycleState& state, bool high) noexcept {
|
||||
if (!state.enabled) return {high, CycleStep::none};
|
||||
if (state.first_person) return {true, CycleStep::leave};
|
||||
if (high && state.can_enter) return {false, CycleStep::enter};
|
||||
return {high, CycleStep::none};
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ struct Settings {
|
||||
bool stabilize = true, follow_flips = true;
|
||||
// Hand the view back to the game's camera while the skater walks (opt-in).
|
||||
bool board_only = false;
|
||||
// Put first person into the game's camera height cycle (first_person_cycle.h, opt-in).
|
||||
bool camera_cycle = false;
|
||||
float smoothing = 60, head_pitch = 86, head_roll = 0, bob = 25;
|
||||
};
|
||||
// Adaptive low-pass ("One Euro") state for one channel: heavy smoothing while the
|
||||
|
||||
@@ -100,6 +100,11 @@ void set_local_values(const std::vector<std::pair<std::string, Json>>& values) n
|
||||
// read it every frame without touching the database.
|
||||
float local_menu_scale() noexcept;
|
||||
bool set_local_menu_scale(float value) noexcept;
|
||||
// The game's camera height option (UseHighCam, true is the high camera) as the game writes it,
|
||||
// which its camera height button does: the listener returns the height to keep instead. Runs on
|
||||
// the thread that writes the option.
|
||||
using CameraHeightWrite = bool (*)(bool high) noexcept;
|
||||
void set_camera_height_write_listener(CameraHeightWrite listener) noexcept;
|
||||
}
|
||||
PlayerCardModel local_profile_player_card();
|
||||
// Renames only the local RIP Card. An empty name restores the Steam name.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Engine/Game/Build/20260929/named_settings.h"
|
||||
#include "local_user_settings.h"
|
||||
#include "Extension/Profile/local_profile_runtime.h"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
@@ -132,6 +133,30 @@ void apply_own_default(std::uintptr_t group, const char* key, const NativeSettin
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The game's camera height button writes UseHighCam (a cloud option, true is the high camera)
|
||||
// as the opposite of the height it reads. The listener may keep another height
|
||||
// (first person in the camera cycle, Extension/Skater/client_debug.cpp).
|
||||
std::atomic<CameraHeightWrite> height_write_listener{};
|
||||
// True when the game's write of `value` is to be replaced by `high`.
|
||||
bool replace_camera_height(const char* key, std::uintptr_t type, const NativeSettingValue* value, bool& high) noexcept {
|
||||
constexpr std::string_view camera_height_key = "UseHighCam";
|
||||
const auto listener = height_write_listener.load(std::memory_order_acquire);
|
||||
if (!listener || type != local_runtime().base + native_types::native_bool) return false;
|
||||
char text[camera_height_key.size() + 1];
|
||||
if (memory::peek_cstring(reinterpret_cast<std::uintptr_t>(key), text, sizeof(text)) !=
|
||||
static_cast<std::ptrdiff_t>(camera_height_key.size()) || camera_height_key != text) return false;
|
||||
NativeSettingValue native{};
|
||||
std::uint8_t byte{};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(value), native) ||
|
||||
!memory::peek(reinterpret_cast<std::uintptr_t>(native.data), byte) || byte > 1) return false;
|
||||
high = listener(byte != 0);
|
||||
return high != (byte != 0);
|
||||
}
|
||||
}
|
||||
|
||||
void set_camera_height_write_listener(CameraHeightWrite listener) noexcept {
|
||||
height_write_listener.store(listener, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool restore_native_setting(std::uintptr_t group, const char* key, std::uintptr_t type,
|
||||
@@ -181,6 +206,10 @@ const NativeSettingValue* native_setting_get(std::uintptr_t group, const char* k
|
||||
}
|
||||
|
||||
bool native_setting_set(std::uintptr_t group, const char* key, std::uintptr_t type, const NativeSettingValue* value) {
|
||||
bool high{};
|
||||
const NativeSettingValue kept{type, &high};
|
||||
// The original setter clones the boxed value, so `kept` only needs to outlive the call.
|
||||
if (replace_camera_height(key, type, value, high)) value = &kept;
|
||||
const bool changed = native_settings().set(group, key, type, value);
|
||||
PreserveError preserve;
|
||||
try {
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
#include "Engine/Game/Build/20260929/engine.h"
|
||||
#include "Engine/Game/Multiplayer/session_tools.h"
|
||||
#include "free_flight.h"
|
||||
#include "Engine/Game/Skater/first_person_cycle.h"
|
||||
#include "Engine/Game/Skater/velocity_boost.h"
|
||||
#include "Extension/Multiplayer/Remote/native_skater.h"
|
||||
#include "Extension/Objects/ParkEditor/park_editor_runtime.h"
|
||||
#include "Extension/Profile/local_profile_runtime.h"
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
@@ -255,6 +257,7 @@ constexpr std::array<const char*, 4> strength{"FirstPerson.Up", "FirstPerson.Dow
|
||||
constexpr const char* stabilize = "FirstPerson.Stabilize";
|
||||
constexpr const char* follow_flips = "FirstPerson.FollowFlips";
|
||||
constexpr const char* board_only = "FirstPerson.ThirdPersonOnFoot";
|
||||
constexpr const char* camera_cycle = "FirstPerson.CameraCycle";
|
||||
constexpr std::array<const char*, 4> steady{"FirstPerson.Smoothing", "FirstPerson.HeadPitch", "FirstPerson.HeadRoll", "FirstPerson.Bob"};
|
||||
constexpr ULONGLONG delay_ms = 750;
|
||||
|
||||
@@ -293,6 +296,7 @@ void save_debug(InteractiveDebug& debug) noexcept {
|
||||
values.emplace_back(saved::stabilize, arm.stabilize);
|
||||
values.emplace_back(saved::follow_flips, arm.follow_flips);
|
||||
values.emplace_back(saved::board_only, arm.board_only);
|
||||
values.emplace_back(saved::camera_cycle, arm.camera_cycle);
|
||||
const std::array<float, 4> steady{arm.smoothing, arm.head_pitch, arm.head_roll, arm.bob};
|
||||
for (std::size_t i = 0; i < 4; ++i) values.emplace_back(saved::steady[i], static_cast<double>(steady[i]));
|
||||
profile_runtime::set_local_values(values);
|
||||
@@ -336,6 +340,7 @@ void load_saved_debug(InteractiveDebug& debug) noexcept {
|
||||
if (const auto enabled = profile_runtime::local_preference(saved::stabilize)) arm.stabilize = *enabled;
|
||||
if (const auto enabled = profile_runtime::local_preference(saved::follow_flips)) arm.follow_flips = *enabled;
|
||||
if (const auto enabled = profile_runtime::local_preference(saved::board_only)) arm.board_only = *enabled;
|
||||
if (const auto enabled = profile_runtime::local_preference(saved::camera_cycle)) arm.camera_cycle = *enabled;
|
||||
const std::array<float*, 4> steady{&arm.smoothing, &arm.head_pitch, &arm.head_roll, &arm.bob};
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
if (const auto value = saved::number(saved::steady[i])) *steady[i] = *value;
|
||||
@@ -423,7 +428,9 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
|
||||
case Action::set_first_person_head_roll: settings.head_roll = request.value; break;
|
||||
case Action::set_first_person_bob: settings.bob = request.value; break;
|
||||
case Action::set_first_person_board_only: settings.board_only = request.enabled; break;
|
||||
case Action::reset_first_person_arm: settings = {}; break;
|
||||
case Action::set_first_person_camera_cycle: settings.camera_cycle = request.enabled; break;
|
||||
// Resets the view; which steps the camera height button goes through is kept.
|
||||
case Action::reset_first_person_arm: settings = {.camera_cycle = settings.camera_cycle}; break;
|
||||
default: break;
|
||||
}
|
||||
source_require(first_person::valid(settings), "First-person arm setting is outside its supported range.");
|
||||
@@ -438,6 +445,8 @@ void debug_action(SourceTrial& trial, std::uintptr_t client, bool can_control, b
|
||||
? (settings.stabilize ? "True first person on: level horizon, steady view." : "True first person off: the view follows the raw head.")
|
||||
: request.action == Action::set_first_person_board_only
|
||||
? (settings.board_only ? "Third person while walking; first person on the board." : "First person while walking too.")
|
||||
: request.action == Action::set_first_person_camera_cycle
|
||||
? (settings.camera_cycle ? "Camera height button: high, low, first person." : "Camera height button: high and low only.")
|
||||
: "First-person settings updated.";
|
||||
return;
|
||||
}
|
||||
@@ -776,6 +785,57 @@ void first_person_board_tick(SourceTrial& trial, std::uintptr_t client, bool can
|
||||
debug.first_person_retry_after = now + 500;
|
||||
}
|
||||
}
|
||||
// "First person in the camera cycle" (Engine/Game/Skater/first_person_cycle.h). The game writes
|
||||
// its camera height option on the thread that handles the button, so each tick publishes what
|
||||
// that write needs, and the next tick carries out the step it asked for.
|
||||
namespace {
|
||||
enum CycleFlag : std::uint8_t { cycle_enabled = 1, cycle_first_person = 2, cycle_can_enter = 4 };
|
||||
std::atomic<std::uint8_t> cycle_flags{}, cycle_request{};
|
||||
// When the flags were published: a tick that stopped running (loading, no local skater) leaves
|
||||
// the game's own height alone.
|
||||
std::atomic<ULONGLONG> cycle_published{};
|
||||
constexpr ULONGLONG cycle_flags_lifetime_ms = 250;
|
||||
}
|
||||
bool first_person_camera_height_write(bool high) noexcept {
|
||||
const auto published = cycle_published.load(std::memory_order_acquire);
|
||||
const auto flags = cycle_flags.load(std::memory_order_acquire);
|
||||
const auto now = GetTickCount64();
|
||||
if (now < published || now - published > cycle_flags_lifetime_ms) return high;
|
||||
const auto write = first_person::cycle_height_write(
|
||||
{(flags & cycle_enabled) != 0, (flags & cycle_first_person) != 0, (flags & cycle_can_enter) != 0}, high);
|
||||
if (write.step != first_person::CycleStep::none)
|
||||
cycle_request.store(static_cast<std::uint8_t>(write.step), std::memory_order_release);
|
||||
return write.high;
|
||||
}
|
||||
void first_person_cycle_tick(SourceTrial& trial, std::uintptr_t client, bool can_control, bool phase, DWORD error) {
|
||||
auto& debug = trial.debug;
|
||||
const auto now = GetTickCount64();
|
||||
const bool enabled = debug.first_person_settings.camera_cycle && can_control && phase &&
|
||||
!debug.park_editor && !debug.noclip && !debug.editor_transition;
|
||||
const auto step = static_cast<first_person::CycleStep>(cycle_request.exchange(0, std::memory_order_acq_rel));
|
||||
if (enabled && step != first_person::CycleStep::none) {
|
||||
// The player's own First person choice, so it replaces an on-foot hand-back.
|
||||
debug.first_person_paused = false;
|
||||
try {
|
||||
debug_action(trial, client, can_control, phase,
|
||||
{overlay::DebugAction::set_first_person, step == first_person::CycleStep::enter}, error);
|
||||
} catch (const SourceGuard& guard) {
|
||||
debug.status = guard.message;
|
||||
// The press kept the low camera; the next ones switch height as the game does.
|
||||
debug.first_person_cycle_retry_after = now + 2000;
|
||||
} catch (...) {
|
||||
debug.first_person_cycle_retry_after = now + 2000;
|
||||
}
|
||||
}
|
||||
bool can_enter = false;
|
||||
if (enabled && !debug.first_person && !debug.camera_owned && now >= debug.first_person_cycle_retry_after) {
|
||||
const auto on_foot = first_person_on_foot(trial.base, client);
|
||||
can_enter = on_foot && !*on_foot;
|
||||
}
|
||||
cycle_flags.store(static_cast<std::uint8_t>((enabled ? cycle_enabled : 0) | (debug.first_person ? cycle_first_person : 0) |
|
||||
(can_enter ? cycle_can_enter : 0)), std::memory_order_release);
|
||||
cycle_published.store(now, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +857,7 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
|
||||
if (!debug.saved_loaded) {
|
||||
debug.saved_loaded = true;
|
||||
load_saved_debug(debug);
|
||||
profile_runtime::set_camera_height_write_listener(&first_person_camera_height_write);
|
||||
}
|
||||
if (debug.save_pending && GetTickCount64() >= debug.save_due) save_debug(debug);
|
||||
if (debug.no_bail_restore && can_control && !debug.park_editor && GetTickCount64() >= debug.no_bail_restore_after) {
|
||||
@@ -832,6 +893,7 @@ overlay::DebugModel on_client_debug_tick(std::uintptr_t base, std::uintptr_t cli
|
||||
catch (...) { debug.status = "Debug action failed; inspect the current state before retrying."; }
|
||||
}
|
||||
first_person_board_tick(trial, client, can_control, camera_phase_observed, error.value);
|
||||
first_person_cycle_tick(trial, client, can_control, camera_phase_observed, error.value);
|
||||
// Camera ownership survives a busy tick or a failed presentation snapshot.
|
||||
// Input capture must follow the lease, not those transient model failures.
|
||||
state.free_camera_active.store(debug.camera_owned && !debug.first_person, std::memory_order_release);
|
||||
|
||||
@@ -107,6 +107,9 @@ struct InteractiveDebug {
|
||||
// skater walks and takes it again once they are back on the board.
|
||||
bool first_person_paused{}, first_person_last_on_foot{};
|
||||
ULONGLONG first_person_foot_since{}, first_person_retry_after{};
|
||||
// "First person in the camera cycle": after first person could not start, the camera height
|
||||
// button switches height as the game does until this time.
|
||||
ULONGLONG first_person_cycle_retry_after{};
|
||||
// FreeCamera vertical FOV (+0xac): chosen value (0 = unchanged) and the value
|
||||
// it held before first person took it, restored when first person ends.
|
||||
float first_person_fov{}, first_person_saved_fov{};
|
||||
|
||||
@@ -74,6 +74,7 @@ enum class DebugAction {
|
||||
set_first_person_head_roll,
|
||||
set_first_person_bob,
|
||||
set_first_person_board_only,
|
||||
set_first_person_camera_cycle,
|
||||
reset_first_person_arm,
|
||||
set_free_camera_fov, // 0 = the game's own FOV
|
||||
// Keep the last action in sync with the bound in request_scheduler.h.
|
||||
|
||||
@@ -97,6 +97,11 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
|
||||
"Use the game's camera while walking; first person comes back when you get on the board.",
|
||||
settings.board_only, can_edit))
|
||||
debug_request(menu, callbacks, {DebugAction::set_first_person_board_only, settings.board_only});
|
||||
if (toggle_row(menu, "First person in the camera cycle",
|
||||
"Clicking the right stick (R3) on the board goes high camera, low camera, first person, then high again. "
|
||||
"Needs Camera height control on in the game's settings (Gameplay > Advanced Camera).",
|
||||
settings.camera_cycle, can_edit))
|
||||
debug_request(menu, callbacks, {DebugAction::set_first_person_camera_cycle, settings.camera_cycle});
|
||||
note("Nod, tilt and bob are the share of the head's own movement kept. Use Spring arm > Pitch to look "
|
||||
"further down at the board.");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user