mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 00:00:17 +02:00
Online First-person camera anchor fix
This commit is contained in:
1 parent
0353b76baa
commit
ee34fbe419
5 files changed
+320
-46
No files matched your search
@@ -89,10 +89,14 @@ four are live and are also exposed in the F10 settings bar.
|
||||
|
||||
By default the headset sits where Mario Kart's own chase camera sits, and `world_units_per_meter`
|
||||
of 500 presents the race as a small diorama on a table. Turning on `first_person` moves the camera
|
||||
to the Player 1 driver's head instead, and switches the world scale to
|
||||
to the local driver's head instead, and switches the world scale to
|
||||
`first_person_units_per_meter`, whose default of 30 is what makes the race read life-size from the
|
||||
seat. It is a matter of taste rather than a property of the game, so the F10 bar exposes it.
|
||||
|
||||
The kart is selected through the game's local-screen-to-racer mapping, including online races
|
||||
where your racer is not slot zero. First person requires a locally controlled racer; spectating
|
||||
another racer keeps the game's own camera.
|
||||
|
||||
The game's own transforms are never modified. Each guest frame the runtime reads the race camera's
|
||||
view matrix and the player kart's physics pose and derives one affine transform from the recorded
|
||||
view space into the space to render from. That transform is published with the sealed frame, and
|
||||
|
||||
@@ -345,6 +345,12 @@ target_include_directories(mkw_vr_first_person_tests PRIVATE "${CMAKE_CURRENT_LI
|
||||
target_compile_features(mkw_vr_first_person_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
|
||||
|
||||
# Resolve the real local-kart pointer walk against synthetic offline/online rosters.
|
||||
add_executable(mkw_vr_player_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_player_tests.cpp")
|
||||
target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_player_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_player_tests COMMAND mkw_vr_player_tests)
|
||||
|
||||
if(MKW_ENABLE_OPENXR AND MKW_PLATFORM_WINDOWS)
|
||||
add_executable(mkw_openxr_replay_tests
|
||||
tests/openxr_d3d12_replay_tests.cpp src/vr/openxr_d3d12.cpp)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mkw::vr::detail {
|
||||
|
||||
struct LocalPlayerKartRead {
|
||||
const char* failed_step = nullptr;
|
||||
uint32_t race_data = 0;
|
||||
uint32_t player_index = 0xFFu;
|
||||
uint32_t manager = 0;
|
||||
uint32_t players = 0;
|
||||
uint32_t proxy = 0;
|
||||
uint32_t accessor = 0;
|
||||
};
|
||||
|
||||
// PAL RMCP01. Use the same screen-to-racer mapping as RaceCameraMgr::__ct
|
||||
// (0x805A8468), not a kart-array slot or a search for a non-CPU racer.
|
||||
// The memory provider is injectable so the actual pointer walk can be tested
|
||||
// without a running guest. Production uses Memory's checked, big-endian reads.
|
||||
template <typename GuestMemory>
|
||||
LocalPlayerKartRead ReadLocalPlayerKart() noexcept {
|
||||
// Racedata::CreateInstance (0x8052FE58): 0x809C0000 - 10456.
|
||||
constexpr uint32_t kRaceDataInstanceAddress = 0x809BD728u;
|
||||
// Racedata::GetPlayerIdOfLocalPlayer (0x80531F70): byte +2948 + screen.
|
||||
constexpr uint32_t kHudPlayerIdsOffset = 0xB84u;
|
||||
// Active race scenario, not the menu scenario. GetRacePlayerCount
|
||||
// (0x8052DD30) reads +0x24; the local count is the byte at +0x26.
|
||||
constexpr uint32_t kPlayerCountOffset = 0x24u;
|
||||
constexpr uint32_t kLocalPlayerCountOffset = 0x26u;
|
||||
// Scenario::GetPlayer (0x8052DD20): scenario +8 + index*0xF0.
|
||||
// Race scenario starts at +0x20; Player::GetPlayerType (0x8052ED20)
|
||||
// reads +0x10. TYPE_REAL_LOCAL is 0; TYPE_REAL_ONLINE is 4.
|
||||
constexpr uint32_t kPlayerTypeOffset = 0x38u;
|
||||
constexpr uint32_t kPlayerStride = 0xF0u;
|
||||
constexpr uint32_t kMaxPlayers = 12;
|
||||
// Kart::Manager::CreateInstance (0x8058FAA8), GetKartPlayer (0x80590100),
|
||||
// and Kart::Link's shared proxy -> accessor link.
|
||||
constexpr uint32_t kKartManagerInstanceAddress = 0x809C18F8u;
|
||||
constexpr uint32_t kKartManagerPlayersOffset = 0x20u;
|
||||
|
||||
LocalPlayerKartRead read{};
|
||||
const auto pointer = [](uint32_t address, uint32_t& out) {
|
||||
return GuestMemory::TryRead32(address, out) && out != 0;
|
||||
};
|
||||
if (!pointer(kRaceDataInstanceAddress, read.race_data)) {
|
||||
read.failed_step = "Racedata instance";
|
||||
return read;
|
||||
}
|
||||
read.failed_step = "local racer mapping";
|
||||
if (!GuestMemory::Contains(read.race_data, kHudPlayerIdsOffset + 1u)) {
|
||||
return read;
|
||||
}
|
||||
try {
|
||||
const uint32_t player_count = GuestMemory::Read8(read.race_data + kPlayerCountOffset);
|
||||
if (player_count == 0 || player_count > kMaxPlayers ||
|
||||
GuestMemory::Read8(read.race_data + kLocalPlayerCountOffset) != 1) {
|
||||
return read;
|
||||
}
|
||||
// Immersive first person supports one local screen. Its racer may be
|
||||
// anywhere in the online roster. 0xFF means no racer is assigned.
|
||||
read.player_index = GuestMemory::Read8(read.race_data + kHudPlayerIdsOffset);
|
||||
if (read.player_index >= player_count ||
|
||||
GuestMemory::Read32(read.race_data + kPlayerTypeOffset +
|
||||
read.player_index * kPlayerStride) != 0) {
|
||||
return read;
|
||||
}
|
||||
} catch (const typename GuestMemory::AccessViolation&) {
|
||||
return read;
|
||||
}
|
||||
|
||||
if (!pointer(kKartManagerInstanceAddress, read.manager)) {
|
||||
read.failed_step = "Kart::Manager instance";
|
||||
} else if (!pointer(read.manager + kKartManagerPlayersOffset, read.players)) {
|
||||
read.failed_step = "Kart::Manager players array";
|
||||
} else if (!pointer(read.players + read.player_index * 4u, read.proxy)) {
|
||||
read.failed_step = "local player kart object";
|
||||
} else if (!pointer(read.proxy, read.accessor)) {
|
||||
read.failed_step = "local kart accessor";
|
||||
} else {
|
||||
read.failed_step = nullptr;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
} // namespace mkw::vr::detail
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/mkw_vr_player.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -50,19 +51,9 @@ constexpr uint32_t kG3DCameraMtxStateAddress = 0x802BBAB4u;
|
||||
constexpr uint32_t kG3DCameraMtxBankOffset = 0x2u;
|
||||
constexpr uint32_t kG3DCameraMtxOffset = 52u;
|
||||
|
||||
// Kart::Manager's instance pointer. Its CreateInstance (0x8058FAA8) resolves
|
||||
// the slot as 0x809C0000 + 6392 in the generated translation. Read directly
|
||||
// rather than observed from Kart::Manager::Update's r3, so enabling the camera
|
||||
// needs no change to the translated output: an entry observer only exists in a
|
||||
// build whose translation was regenerated for it, and its absence is silent.
|
||||
// This mirrors how the race scene's instance slot is reached in
|
||||
// mkw_vr_instrumentation.cpp.
|
||||
constexpr uint32_t kKartManagerInstanceAddress = 0x809C18F8u;
|
||||
// Kart::Manager::GetKartPlayer (0x80590100): `lwz r3,0x20(r3)` then indexes.
|
||||
constexpr uint32_t kKartManagerPlayersOffset = 0x20u;
|
||||
// Kart::Link::GetKartPosition (0x8059020C) walks proxy -> accessor -> body ->
|
||||
// physics -> dynamics; the first three links are shared by every kart accessor.
|
||||
constexpr uint32_t kKartProxyAccessorOffset = 0x00u;
|
||||
// physics -> dynamics. The local racer and its accessor are resolved by
|
||||
// ReadLocalPlayerKart, shared with driver visibility.
|
||||
constexpr uint32_t kKartAccessorBodyOffset = 0x08u;
|
||||
constexpr uint32_t kKartBodyPhysicsOffset = 0x90u;
|
||||
// KartPhysics::pose (Kart::Link::GetMtx 0x80590264). This is the physics-driven
|
||||
@@ -96,10 +87,6 @@ constexpr uint32_t kModelHolderArrayOffset = 0xD8u;
|
||||
constexpr uint32_t kModelHolderCountOffset = 0xF0u;
|
||||
constexpr uint32_t kMaxPlayerModels = 32;
|
||||
|
||||
// Offline Mario Kart Wii puts the local racer first, and immersive
|
||||
// presentation already requires exactly one on-screen player.
|
||||
constexpr uint32_t kLocalPlayerIndex = 0;
|
||||
|
||||
// Frames the last good anchor survives a failed read before the camera returns
|
||||
// to the game's own. Rides out a transient null during a respawn or transition
|
||||
// without letting a genuinely broken anchor persist.
|
||||
@@ -184,27 +171,17 @@ bool ReadRaceCameraViewMatrix(const CpuContext* context, uint32_t camera_address
|
||||
// The pointer walk, kept inspectable: on failure `failed_step` names the link
|
||||
// that broke and the resolved pointers before it are still filled in. One log
|
||||
// line then says exactly which offset needs revisiting.
|
||||
struct KartPoseRead {
|
||||
const char* failed_step = nullptr;
|
||||
uint32_t manager = 0;
|
||||
uint32_t players = 0;
|
||||
uint32_t proxy = 0;
|
||||
uint32_t accessor = 0;
|
||||
struct KartPoseRead : detail::LocalPlayerKartRead {
|
||||
uint32_t body = 0;
|
||||
uint32_t physics = 0;
|
||||
};
|
||||
|
||||
KartPoseRead ReadPlayerKartPose(Mtx34& out) noexcept {
|
||||
KartPoseRead read{};
|
||||
if (!ReadGuestPointer(kKartManagerInstanceAddress, read.manager)) {
|
||||
read.failed_step = "Kart::Manager instance";
|
||||
} else if (!ReadGuestPointer(read.manager + kKartManagerPlayersOffset, read.players)) {
|
||||
read.failed_step = "Kart::Manager players array";
|
||||
} else if (!ReadGuestPointer(read.players + kLocalPlayerIndex * 4u, read.proxy)) {
|
||||
read.failed_step = "player kart object";
|
||||
} else if (!ReadGuestPointer(read.proxy + kKartProxyAccessorOffset, read.accessor)) {
|
||||
read.failed_step = "kart accessor";
|
||||
} else if (!ReadGuestPointer(read.accessor + kKartAccessorBodyOffset, read.body)) {
|
||||
KartPoseRead ReadPlayerKartPose(const detail::LocalPlayerKartRead& player, Mtx34& out) noexcept {
|
||||
KartPoseRead read{player};
|
||||
if (read.failed_step != nullptr) {
|
||||
return read;
|
||||
}
|
||||
if (!ReadGuestPointer(read.accessor + kKartAccessorBodyOffset, read.body)) {
|
||||
read.failed_step = "kart body";
|
||||
} else if (!ReadGuestPointer(read.body + kKartBodyPhysicsOffset, read.physics)) {
|
||||
read.failed_step = "kart physics";
|
||||
@@ -240,6 +217,7 @@ struct FirstPersonState {
|
||||
FirstPersonRotation rotation = FirstPersonRotation::YawOnly;
|
||||
|
||||
uint32_t camera_address = 0;
|
||||
detail::LocalPlayerKartRead player_kart{};
|
||||
// Armed by the draw boundary, consumed by the frame seal.
|
||||
bool armed = false;
|
||||
uint64_t armed_frame = 0;
|
||||
@@ -260,16 +238,9 @@ FirstPersonState g_state;
|
||||
ModelVisibilityState g_visibility;
|
||||
|
||||
// Walks to the player's ModelsVisibility, or zero when the race is not up.
|
||||
uint32_t ResolveModelsVisibility() noexcept {
|
||||
uint32_t manager = 0;
|
||||
uint32_t players = 0;
|
||||
uint32_t proxy = 0;
|
||||
uint32_t accessor = 0;
|
||||
uint32_t ResolveModelsVisibility(uint32_t accessor) noexcept {
|
||||
uint32_t visibility = 0;
|
||||
if (!ReadGuestPointer(kKartManagerInstanceAddress, manager) ||
|
||||
!ReadGuestPointer(manager + kKartManagerPlayersOffset, players) ||
|
||||
!ReadGuestPointer(players + kLocalPlayerIndex * 4u, proxy) ||
|
||||
!ReadGuestPointer(proxy + kKartProxyAccessorOffset, accessor) ||
|
||||
if (accessor == 0 ||
|
||||
!ReadGuestPointer(accessor + kKartAccessorModelsVisibilityOffset, visibility)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -362,7 +333,7 @@ void ApplyModelVisibilityLocked() noexcept {
|
||||
RestoreModelVisibilityLocked();
|
||||
return;
|
||||
}
|
||||
const uint32_t visibility = ResolveModelsVisibility();
|
||||
const uint32_t visibility = ResolveModelsVisibility(g_state.player_kart.accessor);
|
||||
if (visibility == 0 ||
|
||||
!Memory::Contains(visibility + kModelsVisibilityDrawOffset, 1)) {
|
||||
return;
|
||||
@@ -442,6 +413,7 @@ void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from
|
||||
// little below the chase camera, and well in front of it.
|
||||
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] first-person anchor: frame=" << frame << ", camera=0x"
|
||||
<< std::hex << g_state.camera_address << std::dec
|
||||
<< ", local racer=" << kart.player_index
|
||||
<< ", head from camera (right, up, forward)=(" << -anchor[3] << ", "
|
||||
<< -anchor[7] << ", " << anchor[11] << ") units" << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] first-person view: rows=(" << view_from_world[0] << ", "
|
||||
@@ -541,6 +513,7 @@ void MkwVRFirstPersonReset() noexcept {
|
||||
g_state.armed = false;
|
||||
g_state.armed_view_valid = false;
|
||||
g_state.camera_address = 0;
|
||||
g_state.player_kart = {};
|
||||
g_state.anchor = {};
|
||||
g_state.hold_frames = 0;
|
||||
g_state.ever_valid_this_race = false;
|
||||
@@ -558,6 +531,15 @@ void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_add
|
||||
RestoreModelVisibilityLocked();
|
||||
return;
|
||||
}
|
||||
const auto player = detail::ReadLocalPlayerKart<Memory>();
|
||||
if (player.failed_step != nullptr || player.accessor != g_state.player_kart.accessor) {
|
||||
// Do not carry a held anchor or hidden models across an ownership
|
||||
// change, including entering spectator mode or an online roster reset.
|
||||
RestoreModelVisibilityLocked();
|
||||
g_state.anchor = {};
|
||||
g_state.hold_frames = 0;
|
||||
}
|
||||
g_state.player_kart = player;
|
||||
g_state.armed = true;
|
||||
g_state.armed_frame = guest_frame_index;
|
||||
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
|
||||
@@ -591,7 +573,8 @@ void MkwVRFirstPersonCommit() noexcept {
|
||||
!(g_state.camera_address != 0 &&
|
||||
ReadRaceCameraViewMatrix(TryGetCpuContext(), g_state.camera_address, view_from_world))) {
|
||||
failed_step = "scene view matrix";
|
||||
} else if (kart = ReadPlayerKartPose(kart_from_local); kart.failed_step != nullptr) {
|
||||
} else if (kart = ReadPlayerKartPose(g_state.player_kart, kart_from_local);
|
||||
kart.failed_step != nullptr) {
|
||||
failed_step = kart.failed_step;
|
||||
} else if (!ComputeFirstPersonAnchor(view_from_world, kart_from_local,
|
||||
g_state.offsets.right * g_state.units_per_meter,
|
||||
@@ -621,7 +604,9 @@ void MkwVRFirstPersonCommit() noexcept {
|
||||
RT_LOG(RT_TAG_RUNTIME)
|
||||
<< "[mkw-vr] first-person camera is enabled but could not resolve the "
|
||||
<< failed_step << "; staying on the game's own camera (camera=0x" << std::hex
|
||||
<< g_state.camera_address << ", manager=0x" << kart.manager << ", players=0x"
|
||||
<< g_state.camera_address << ", race data=0x" << kart.race_data
|
||||
<< ", local racer=" << std::dec << kart.player_index << std::hex
|
||||
<< ", manager=0x" << kart.manager << ", players=0x"
|
||||
<< kart.players << ", kart=0x" << kart.proxy << ", accessor=0x" << kart.accessor
|
||||
<< ", body=0x" << kart.body << ", physics=0x" << kart.physics << std::dec << ")"
|
||||
<< std::endl;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Exercise the production local-kart resolver against a synthetic PAL roster.
|
||||
|
||||
#include "vr/mkw_vr_player.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
++g_failures;
|
||||
std::cerr << "FAILED: " << what << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
struct GuestMemory {
|
||||
using AccessViolation = std::out_of_range;
|
||||
inline static std::unordered_map<uint32_t, uint8_t> bytes;
|
||||
inline static uint32_t fault_address = 0;
|
||||
|
||||
static bool Contains(uint32_t address, uint32_t size) {
|
||||
for (uint32_t i = 0; i < size; ++i) {
|
||||
if (bytes.count(address + i) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t Read8(uint32_t address) {
|
||||
if (address == fault_address) {
|
||||
throw AccessViolation("injected read fault");
|
||||
}
|
||||
return bytes.at(address);
|
||||
}
|
||||
|
||||
static uint32_t Read32(uint32_t address) {
|
||||
uint32_t value = 0;
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
value = (value << 8) | Read8(address + i);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool TryRead32(uint32_t address, uint32_t& out) {
|
||||
try {
|
||||
out = Read32(address);
|
||||
return true;
|
||||
} catch (const AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void Write32(uint32_t address, uint32_t value) {
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
bytes[address + i] = static_cast<uint8_t>(value >> (24 - 8 * i));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constexpr uint32_t kRaceData = 0x81000000u;
|
||||
constexpr uint32_t kManager = 0x81100000u;
|
||||
constexpr uint32_t kKarts = 0x81100100u;
|
||||
constexpr uint32_t kProxies = 0x81200000u;
|
||||
constexpr uint32_t kAccessors = 0x81300000u;
|
||||
|
||||
void SetLocalRacer(uint8_t player) {
|
||||
GuestMemory::bytes[kRaceData + 0xB84] = player;
|
||||
for (uint32_t i = 0; i < 12; ++i) {
|
||||
GuestMemory::Write32(kRaceData + 0x38 + i * 0xF0, i == player ? 0 : 4);
|
||||
}
|
||||
}
|
||||
|
||||
void MakeRace(uint8_t local_player) {
|
||||
GuestMemory::bytes.clear();
|
||||
GuestMemory::fault_address = 0;
|
||||
for (uint32_t i = 0; i < 0xB90; ++i) {
|
||||
GuestMemory::bytes[kRaceData + i] = 0;
|
||||
}
|
||||
GuestMemory::Write32(0x809BD728u, kRaceData);
|
||||
GuestMemory::Write32(0x809C18F8u, kManager);
|
||||
GuestMemory::Write32(kManager + 0x20, kKarts);
|
||||
GuestMemory::bytes[kRaceData + 0x24] = 12;
|
||||
GuestMemory::bytes[kRaceData + 0x26] = 1;
|
||||
for (uint32_t i = 0; i < 12; ++i) {
|
||||
GuestMemory::Write32(kKarts + i * 4, kProxies + i * 0x100);
|
||||
GuestMemory::Write32(kProxies + i * 0x100, kAccessors + i * 0x100);
|
||||
}
|
||||
SetLocalRacer(local_player);
|
||||
}
|
||||
|
||||
auto Resolve() {
|
||||
return mkw::vr::detail::ReadLocalPlayerKart<GuestMemory>();
|
||||
}
|
||||
|
||||
void TestSinglePlayer() {
|
||||
MakeRace(0);
|
||||
for (uint32_t i = 1; i < 12; ++i) {
|
||||
GuestMemory::Write32(kRaceData + 0x38 + i * 0xF0, 1); // CPU
|
||||
}
|
||||
const auto read = Resolve();
|
||||
Check(read.failed_step == nullptr && read.player_index == 0 &&
|
||||
read.accessor == kAccessors,
|
||||
"offline racing still selects the local kart in slot zero");
|
||||
}
|
||||
|
||||
void TestEveryOnlineSlot() {
|
||||
for (uint8_t player = 0; player < 12; ++player) {
|
||||
MakeRace(player);
|
||||
const auto read = Resolve();
|
||||
Check(read.failed_step == nullptr && read.player_index == player &&
|
||||
read.proxy == kProxies + player * 0x100u &&
|
||||
read.accessor == kAccessors + player * 0x100u,
|
||||
"an all-human online roster selects only the screen's local racer");
|
||||
}
|
||||
}
|
||||
|
||||
void TestRosterRemapping() {
|
||||
MakeRace(5);
|
||||
Check(Resolve().accessor == kAccessors + 0x500, "initial online assignment");
|
||||
SetLocalRacer(11);
|
||||
Check(Resolve().accessor == kAccessors + 0xB00, "a new roster assignment is read afresh");
|
||||
}
|
||||
|
||||
void TestNoOpponentFallback() {
|
||||
for (uint32_t type : {1u, 2u, 3u, 4u, 5u}) {
|
||||
MakeRace(5);
|
||||
GuestMemory::Write32(kRaceData + 0x38 + 5 * 0xF0, type);
|
||||
GuestMemory::Write32(kRaceData + 0x38, 0); // Another valid kart is not a fallback.
|
||||
const auto read = Resolve();
|
||||
Check(read.failed_step != nullptr && read.accessor == 0,
|
||||
"a mapped CPU, unused, ghost, remote or absent racer cannot be an anchor");
|
||||
}
|
||||
MakeRace(5);
|
||||
GuestMemory::Write32(kKarts + 5 * 4, 0);
|
||||
Check(Resolve().failed_step != nullptr && Resolve().accessor == 0,
|
||||
"a missing local kart never falls back to another valid kart");
|
||||
}
|
||||
|
||||
void TestInvalidMapping() {
|
||||
for (uint8_t player : {12, 127, 255}) {
|
||||
MakeRace(5);
|
||||
GuestMemory::bytes[kRaceData + 0xB84] = player;
|
||||
Check(Resolve().failed_step != nullptr, "out-of-range and unassigned HUD racers fail");
|
||||
}
|
||||
MakeRace(5);
|
||||
GuestMemory::bytes[kRaceData + 0x24] = 5;
|
||||
Check(Resolve().failed_step != nullptr, "racer must be inside the active roster");
|
||||
for (uint8_t count : {0, 13}) {
|
||||
MakeRace(5);
|
||||
GuestMemory::bytes[kRaceData + 0x24] = count;
|
||||
Check(Resolve().failed_step != nullptr, "empty and corrupt rosters fail");
|
||||
}
|
||||
for (uint8_t count : {0, 2}) {
|
||||
MakeRace(5);
|
||||
GuestMemory::bytes[kRaceData + 0x26] = count;
|
||||
Check(Resolve().failed_step != nullptr, "spectating and split-screen have no anchor");
|
||||
}
|
||||
}
|
||||
|
||||
void TestUnavailableGuestData() {
|
||||
MakeRace(5);
|
||||
GuestMemory::Write32(0x809BD728u, 0);
|
||||
Check(Resolve().failed_step != nullptr, "missing race data fails");
|
||||
MakeRace(5);
|
||||
GuestMemory::bytes.erase(kRaceData + 0xB84);
|
||||
Check(Resolve().failed_step != nullptr, "unmapped race data fails");
|
||||
MakeRace(5);
|
||||
GuestMemory::fault_address = kRaceData + 0x38 + 5 * 0xF0;
|
||||
Check(Resolve().failed_step != nullptr, "a fault after the bounds check is contained");
|
||||
MakeRace(5);
|
||||
GuestMemory::bytes.erase(kProxies + 5 * 0x100);
|
||||
Check(Resolve().failed_step != nullptr, "an unreadable local accessor fails");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestSinglePlayer();
|
||||
TestEveryOnlineSlot();
|
||||
TestRosterRemapping();
|
||||
TestNoOpponentFallback();
|
||||
TestInvalidMapping();
|
||||
TestUnavailableGuestData();
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user