mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 18:37:07 +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.
92 lines
4.1 KiB
C++
92 lines
4.1 KiB
C++
#include "trainer_waypoint.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Abi/native_data.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/native_party.h"
|
|
#include <cmath>
|
|
#include <format>
|
|
#include <map>
|
|
|
|
// The pause map keeps every point of interest in one registry (the map POI manager, the same
|
|
// one ReSkate's party markers register into: native_party_hooks.cpp update_map_markers). A
|
|
// waypoint the player places with the map cursor is a POI whose kind (MapObjectData @404) is
|
|
// DingoMapPOIType_Waypoint (3); its world transform is the matrix at @0.
|
|
namespace dingosdk::trainer {
|
|
namespace {
|
|
namespace party = addr::native_party;
|
|
using Address = std::uintptr_t;
|
|
using Handle = std::uint64_t;
|
|
constexpr std::uint32_t kind_waypoint = 3;
|
|
constexpr std::uint32_t kind_offset = 404;
|
|
constexpr std::uint32_t max_buckets = 65536, max_entries = 20000;
|
|
|
|
template<class T> bool get(Address address, T &value) { return address && memory::peek(address, value); }
|
|
|
|
Address model_type(Address base, Address manager, Handle handle) {
|
|
const auto record = reinterpret_cast<Address (*)(Address, Handle, std::uint8_t)>(base + party::model_record)(manager, handle, 0);
|
|
if (!record) return 0;
|
|
return reinterpret_cast<Address (*)(Address, Handle, Address, std::uint8_t)>(base + party::model_type)(manager, handle, record, 0);
|
|
}
|
|
|
|
bool sane(const std::array<float, 16> &world) {
|
|
for (const auto i : {12, 13, 14})
|
|
if (!std::isfinite(world[i]) || std::abs(world[i]) > 1e6f) return false;
|
|
return true;
|
|
}
|
|
|
|
MapWaypointRead walk(Address base) {
|
|
MapWaypointRead out;
|
|
Address map{}, vtable{}, manager{};
|
|
if (!get(base + party::map_manager, map) || !map) { out.detail = "no map POI manager"; return out; }
|
|
if (!get(map, vtable) || vtable != base + party::map_manager_vtable) { out.detail = "map POI manager type differs"; return out; }
|
|
if (!get(map + 0x18, manager) || !manager) { out.detail = "no map model manager"; return out; }
|
|
auto &n = game::native_data().models;
|
|
if (!n.value || !n.lock || !n.unlock) { out.detail = "native model API unavailable"; return out; }
|
|
game::ModelWriteLock lock(manager);
|
|
Address table{};
|
|
std::uint32_t buckets{}, count{};
|
|
if (!get(map + 0x48, table) || !get(map + 0x50, buckets) || !get(map + 0x54, count) || !table ||
|
|
buckets > max_buckets || count > max_entries) { out.detail = "map POI registry unreadable"; return out; }
|
|
const auto poi_type = base + party::map_poi_type;
|
|
std::map<std::uint32_t, unsigned> kinds;
|
|
unsigned visited{};
|
|
for (std::uint32_t i = 0; i < buckets && visited < count; ++i) {
|
|
Address node{};
|
|
if (!get(table + i * 8, node)) break;
|
|
while (node && visited++ < count) {
|
|
Handle handle{};
|
|
if (!get(node, handle)) break;
|
|
if (handle && model_type(base, manager, handle) == poi_type) {
|
|
if (const auto value = n.value(manager, handle, 0, 0)) {
|
|
std::uint32_t kind{};
|
|
if (get(value + kind_offset, kind)) {
|
|
++kinds[kind];
|
|
++out.reading.typed;
|
|
std::array<float, 16> world{};
|
|
if (kind == kind_waypoint && !out.reading.waypoint && get(value, world) && sane(world))
|
|
out.reading.waypoint = landing::Vec3{world[12], world[13], world[14]};
|
|
}
|
|
}
|
|
}
|
|
if (!get(node + 8, node)) break;
|
|
}
|
|
}
|
|
out.reading.read = true;
|
|
out.detail = std::format("{} map POIs", visited);
|
|
for (const auto &[kind, number] : kinds) out.detail += std::format(", kind {} x{}", kind, number);
|
|
return out;
|
|
}
|
|
} // namespace
|
|
|
|
MapWaypointRead read_map_waypoint(std::uintptr_t base) noexcept {
|
|
if (!base) return {};
|
|
try {
|
|
return walk(base);
|
|
} catch (const std::exception &e) {
|
|
return MapWaypointRead{.reading = {}, .detail = e.what()};
|
|
} catch (...) {
|
|
return MapWaypointRead{.reading = {}, .detail = "map POI walk failed"};
|
|
}
|
|
}
|
|
} // namespace dingosdk::trainer
|