mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 17:55:28 +02:00
PRACTICE > TELEPORT now shows the waypoint the player placed on the game's pause map, with a "Go to map waypoint" button. The console gets `trainer waypoint [info]` and `trainer ground <x> <z>`. Reading the waypoint (trainer_waypoint.cpp) - It is a point of interest in the map's registry, the one ReSkate's party markers register into (addr::native_party::map_manager), with kind DingoMapPOIType_Waypoint (3) and its world matrix at @0. - The registry is walked only while the TELEPORT card is drawn (the page notes it, like the class list) or the command runs, every 250 ms at most. A player who never opens the card pays nothing. - A registry the map has filled without a waypoint means it was removed; an empty or unreadable one keeps the last waypoint seen. A new level forgets it. Landing on the surface (trainer_landing.h) - The waypoint's height is not the ground: teleporting to it dropped the skater through the map. local_ground_height() casts the park editor's downward ray (editor::cast_surface) through the client physics world and takes the topmost hit. - The first ray starts at 1500 or 500 above the hint. A custom map can sit higher than that (a waypoint at Y 12 over ground at Y 1939 was seen in game), so a miss is cast again from 20000. - If collision there is still streaming in, the teleport goes to the hint's height and a 12 s watch puts the skater on the surface when it arrives (at most 6 corrections; the teleport is re-sent up to 3 times if it never arrives). - Teleports still go through go_to(), so they follow the host's noclip / teleport permission in a session. The decisions (ray spans, the watch, remember/forget) have no game in them and are tested by trainer_landing (DINGOSDK_BUILD_TRAINER_TESTS). README and CHANGELOG updated.
91 lines
5.3 KiB
C++
91 lines
5.3 KiB
C++
// The decisions behind a ground-safe arrival (trainer_landing.h): where the downward ray
|
|
// starts, the watch that puts a skater who sinks back on the surface, and when a pause-map
|
|
// waypoint is remembered or forgotten.
|
|
#include "Extension/Trainer/trainer_landing.h"
|
|
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
namespace {
|
|
using namespace dingosdk::trainer::landing;
|
|
int failures = 0;
|
|
void check(bool condition, const std::string &message) {
|
|
if (!condition) { std::cerr << "FAIL: " << message << '\n'; ++failures; }
|
|
}
|
|
Sample at(std::uint64_t now, Vec3 position, std::optional<float> ground, float vertical = 0.0f, bool may_resend = true) {
|
|
return Sample{.now = now, .position = position, .vertical = vertical, .ground = ground, .may_resend = may_resend};
|
|
}
|
|
} // namespace
|
|
|
|
int main() {
|
|
// ---- the ray --------------------------------------------------------------------------
|
|
check(first_ray(12.0f).top == ray_ceiling && first_ray(12.0f).bottom == ray_floor, "a low hint starts at the usual ceiling");
|
|
check(first_ray(1800.0f).top == 2300.0f, "a high hint starts 500 above it");
|
|
check(std::isfinite(first_ray(std::nanf("")).top), "a broken hint still gives a usable ray");
|
|
// A waypoint at Y 12 over a custom map's ground at Y 1939 (seen in game): the first ray
|
|
// starts below that ground and misses, so the retry covers what lies above the first one.
|
|
const auto retry = retry_ray(first_ray(12.0f));
|
|
check(retry && retry->top >= 1939.0f && retry->bottom == ray_ceiling, "a miss is cast again from far above");
|
|
check(!retry_ray(Ray{ray_retry_ceiling, 0.0f}), "nothing to retry from the top of the world");
|
|
|
|
// ---- the landing watch ----------------------------------------------------------------
|
|
{
|
|
auto w = start(100.0f, 200.0f, 50.5f, 1000);
|
|
check(step(w, at(1000, {0, 0, 0}, 50.0f)) == Step::wait, "the teleport gets a moment to arrive");
|
|
check(step(w, at(2600, {100.0f, 50.6f, 200.0f}, 50.0f)) == Step::landed, "standing on the surface ends the watch");
|
|
check(!w.active, "a landed watch is over");
|
|
check(step(w, at(2800, {100.0f, -20.0f, 200.0f}, 50.0f)) == Step::wait, "an ended watch does nothing");
|
|
}
|
|
{
|
|
auto w = start(100.0f, 200.0f, 50.5f, 1000);
|
|
check(step(w, at(2600, {100.0f, 30.0f, 200.0f}, 50.0f)) == Step::put_back, "sinking under the surface puts the skater back");
|
|
check(w.fixes == 1, "a correction is counted");
|
|
check(step(w, at(2700, {100.0f, 30.0f, 200.0f}, 50.0f)) == Step::wait, "the next look waits for the correction to arrive");
|
|
check(step(w, at(4200, {100.0f, 50.4f, 200.0f}, 50.0f)) == Step::landed, "then it lands");
|
|
}
|
|
{
|
|
auto w = start(100.0f, 200.0f, 50.5f, 1000);
|
|
check(step(w, at(2600, {100.0f, 30.0f, 200.0f}, std::nullopt)) == Step::wait, "no ground yet: collision is streaming, wait");
|
|
check(w.active, "still watching");
|
|
check(step(w, at(2800, {100.0f, 49.0f, 200.0f}, 50.0f, -8.0f)) == Step::wait, "still falling onto it: not landed yet");
|
|
}
|
|
{
|
|
auto w = start(100.0f, 200.0f, 50.5f, 1000);
|
|
check(step(w, at(2600, {0, 0, 0}, 50.0f)) == Step::wait, "not arrived, too early to resend");
|
|
check(step(w, at(3600, {0, 0, 0}, 50.0f, 0.0f, false)) == Step::wait, "never resend while the game ignores teleports");
|
|
check(step(w, at(3800, {0, 0, 0}, 50.0f)) == Step::resend, "a teleport that never arrived is sent again");
|
|
check(w.resends == max_resends - 1, "the resend is counted");
|
|
check(step(w, at(4000, {0, 0, 0}, 50.0f)) == Step::wait, "resends are spaced out");
|
|
int sent = 1;
|
|
for (std::uint64_t now = 4000; now < 60000 && w.active; now += 200)
|
|
if (step(w, at(now, {0, 0, 0}, 50.0f)) == Step::resend) ++sent;
|
|
check(sent == max_resends, "at most three resends");
|
|
check(!w.active, "a teleport that never arrives ends the watch");
|
|
}
|
|
{
|
|
auto w = start(100.0f, 200.0f, 50.5f, 1000);
|
|
int fixes = 0;
|
|
Step last{};
|
|
for (std::uint64_t now = 2600; now < 60000 && w.active; now += 100)
|
|
if ((last = step(w, at(now, {100.0f, 0.0f, 200.0f}, 50.0f))) == Step::put_back) ++fixes;
|
|
check(fixes == max_fixes && last == Step::ended, "a skater who keeps sinking is corrected six times, then left alone");
|
|
}
|
|
{
|
|
auto w = start(100.0f, 200.0f, 50.5f, 1000);
|
|
check(step(w, at(1000 + watch_ms + 1, {100.0f, 70.0f, 200.0f}, 50.0f)) == Step::ended, "the watch times out");
|
|
}
|
|
|
|
// ---- the remembered waypoint ----------------------------------------------------------
|
|
const std::optional<Vec3> old = Vec3{1.0f, 2.0f, 3.0f};
|
|
const Vec3 fresh{4.0f, 5.0f, 6.0f};
|
|
check(remembered(old, Reading{.read = true, .typed = 58, .waypoint = fresh}) == fresh, "a placed waypoint replaces the old one");
|
|
check(!remembered(old, Reading{.read = true, .typed = 58, .waypoint = std::nullopt}), "a filled map without one: it was removed");
|
|
check(remembered(old, Reading{.read = true, .typed = 0, .waypoint = std::nullopt}) == old, "an empty map says nothing: keep it");
|
|
check(remembered(old, Reading{}) == old, "an unreadable registry says nothing: keep it");
|
|
check(!remembered(std::nullopt, Reading{}), "nothing seen, nothing remembered");
|
|
|
|
if (failures) std::cerr << failures << " failure(s)\n";
|
|
else std::cout << "landing: all checks passed\n";
|
|
return failures ? 1 : 0;
|
|
}
|