Files
andrewnakasandClaude Opus 5.5 da0aeff752 Trainer 0.2.0: Realistic and Fun modes, the game's other tuning classes, push speed on every push
The game keeps much of its tuning in data-defined classes (push speeds,
everything on foot, dive and glide, bail speeds...). The trainer finds 24 of
them in memory by the defaults and field order the game's data ships and lists
their 314 values by name. Push speed now drives the push class's own speeds;
auto push, off-board jump height and wider trick height ranges come with it.
The Tune tab opens on a Realistic or a Fun short list. Built on ReSkate 1.0.7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018szL7EmApPZxh9GK4Phw6x
2026-10-04 01:04:24 -06:00

230 lines
13 KiB
C++

// Lists the data assets of the game's level root bundle, or the values of one of them.
// dingosdk_trainer_asset_list <Skate folder> <word> [<word> ...] names containing any word
// dingosdk_trainer_asset_list <Skate folder> --dump <asset name> every plain value, by name
#include "Engine/Resource/ebx_document.h"
#include "Engine/Vfs/game_bundles.h"
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <memory>
#include <string>
#include <variant>
#include <vector>
namespace ebx = dingosdk::frostbite::ebx;
namespace {
std::string lower(std::string_view text) {
std::string result(text);
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
std::size_t value_size(ebx::FieldType type) {
switch (type) {
case ebx::FieldType::boolean: case ebx::FieldType::int8: case ebx::FieldType::uint8: return 1;
case ebx::FieldType::int16: case ebx::FieldType::uint16: return 2;
case ebx::FieldType::int32: case ebx::FieldType::uint32: case ebx::FieldType::enumeration:
case ebx::FieldType::float32: return 4;
case ebx::FieldType::int64: case ebx::FieldType::uint64: case ebx::FieldType::float64: return 8;
default: return 0;
}
}
void walk(const ebx::Document &document, const ebx::TypeDescriptor &type, std::span<const std::byte> image, std::size_t start, int depth,
const std::string &path) {
if (depth > 16) return;
for (std::size_t i = 0; i < type.fieldCount; ++i) {
const auto index = static_cast<std::size_t>(type.fieldIndex) + i;
if (index >= document.fields.size()) return;
const auto &field = document.fields[index];
const auto kind = field.type();
const auto name = path.empty() ? field.name : field.name.empty() ? path : path + "." + field.name;
if (kind == ebx::FieldType::inherited || (kind == ebx::FieldType::structure && field.category() != ebx::FieldCategory::array)) {
if (field.classRef >= document.types.size()) continue;
const bool inherited = kind == ebx::FieldType::inherited;
walk(document, document.types[field.classRef], image, inherited ? start : start + field.dataOffset, depth + 1,
inherited ? path : name);
continue;
}
const auto offset = start + field.dataOffset;
if (field.category() == ebx::FieldCategory::array) {
std::cout << name << "\t0x" << std::hex << offset << std::dec << "\tarray\n";
continue;
}
if (kind == ebx::FieldType::pointer) {
std::cout << name << "\t0x" << std::hex << offset << std::dec << "\tpointer\n";
continue;
}
const auto size = value_size(kind);
if (!size || offset + size > image.size()) continue;
std::cout << name << "\t0x" << std::hex << offset << std::dec << '\t';
if (kind == ebx::FieldType::float32) {
float value;
std::memcpy(&value, image.data() + offset, 4);
std::cout << "real\t" << value;
} else if (kind == ebx::FieldType::boolean) {
std::cout << "flag\t" << static_cast<int>(image[offset]);
} else {
std::int64_t value{};
std::memcpy(&value, image.data() + offset, size);
std::cout << "int" << size * 8 << '\t' << value;
}
std::cout << '\n';
}
}
void tree(const ebx::Value &value, int depth, const std::string &indent);
void tree(const ebx::Object &object, int depth, const std::string &indent) {
for (const auto &field : object.fields) {
std::cout << indent << (field.name.empty() ? "?" : field.name) << " = ";
tree(field.value, depth, indent);
}
}
void tree(const ebx::Value &value, int depth, const std::string &indent) {
if (depth > 12) { std::cout << "...\n"; return; }
if (const auto *b = std::get_if<bool>(&value.data)) std::cout << (*b ? "true" : "false") << '\n';
else if (const auto *i = std::get_if<std::int64_t>(&value.data)) std::cout << *i << '\n';
else if (const auto *u = std::get_if<std::uint64_t>(&value.data)) std::cout << *u << '\n';
else if (const auto *d = std::get_if<double>(&value.data)) std::cout << *d << '\n';
else if (const auto *s = std::get_if<std::string>(&value.data)) std::cout << '"' << *s << "\"\n";
else if (const auto *r = std::get_if<ebx::PointerReference>(&value.data))
std::cout << (r->kind == ebx::PointerKind::null ? "null" : r->kind == ebx::PointerKind::internal ? "-> instance " : "-> import ") << r->index << '\n';
else if (const auto *x = std::get_if<ebx::BoxedReference>(&value.data)) std::cout << "boxed type 0x" << std::hex << x->encodedType << std::dec << " at " << x->dataOffset << '\n';
else if (const auto *y = std::get_if<ebx::TypeReference>(&value.data)) std::cout << "type " << y->descriptor << " 0x" << std::hex << y->encoded << std::dec << '\n';
else if (const auto *o = std::get_if<std::shared_ptr<ebx::Object>>(&value.data)) {
std::cout << "{\n";
if (*o) tree(**o, depth + 1, indent + " ");
std::cout << indent << "}\n";
} else if (const auto *a = std::get_if<ebx::Value::Array>(&value.data)) {
std::cout << "[" << a->size() << "]\n";
std::size_t shown{};
for (const auto &element : *a) {
if (++shown > 40) { std::cout << indent << " ...\n"; break; }
std::cout << indent << " - ";
tree(element, depth + 1, indent + " ");
}
} else std::cout << "(other)\n";
}
} // namespace
int main(int argc, char **argv) {
if (argc < 3) {
std::cerr << "usage: dingosdk_trainer_asset_list <Skate folder> <word>... | --dump <asset name> | --types <word>...\n";
return 2;
}
try {
const std::filesystem::path game_root(argv[1]);
const dingosdk::vfs::GameData data(game_root);
const auto found = data.read_bundle(data.read_toc("Win32/levels/game/bam_levelroot/bam_levelroot.toc"),
"win32/levels/game/bam_levelroot/bam_levelroot");
if (!found) throw std::runtime_error("the level root bundle was not found");
const auto &bundle = *found;
const std::string mode = argv[2];
if (mode == "--floats" && argc >= 4) {
// Every aligned four bytes of the asset's data that read as a plausible number.
const auto wanted = lower(argv[3]);
for (std::size_t i = 0; i < bundle.manifest.ebx.size(); ++i) {
if (lower(bundle.manifest.ebx[i].name) != wanted) continue;
const auto *payload = bundle.payload(dingosdk::frostbite::AssetKind::ebx, i);
if (!payload) break;
const auto bytes = data.read(*payload);
const auto document = ebx::read_document(bytes);
std::cerr << document.rootType << ": data " << document.dataStart << ".." << document.dataEnd << " of " << bytes.size() << "\n";
for (std::size_t at = document.dataStart & ~std::size_t{3}; at + 4 <= bytes.size(); at += 4) {
float value;
std::memcpy(&value, bytes.data() + at, 4);
const float size = value < 0 ? -value : value;
if (size > 0.0009f && size < 100000.0f) std::cout << "0x" << std::hex << at << std::dec << "\t" << value << "\n";
}
return 0;
}
return 1;
}
if (mode == "--tree" && argc >= 4) {
const auto wanted = lower(argv[3]);
for (std::size_t i = 0; i < bundle.manifest.ebx.size(); ++i) {
if (lower(bundle.manifest.ebx[i].name) != wanted) continue;
const auto *payload = bundle.payload(dingosdk::frostbite::AssetKind::ebx, i);
if (!payload) break;
const auto bytes = data.read(*payload);
const auto document = ebx::read_document(bytes);
std::size_t number{};
for (const auto &instance : document.instances) {
const auto type = instance.descriptor >= 0 && static_cast<std::size_t>(instance.descriptor) < document.types.size()
? document.types[static_cast<std::size_t>(instance.descriptor)].name : std::string("?");
std::cout << "# instance " << number++ << " " << type << "\n";
if (instance.object) tree(*instance.object, 0, " ");
}
number = 0;
for (const auto &boxed : document.boxedValues) {
std::cout << "# boxed " << number++ << ": type 0x" << std::hex << boxed.encodedType << std::dec << " class " << boxed.classRef
<< " count " << boxed.count << " offset " << boxed.offset << " bytes " << boxed.rawBytes.size() << " :";
for (std::size_t at = 0; at + 4 <= boxed.rawBytes.size() && at < 256; at += 4) {
float real; std::int32_t whole;
std::memcpy(&real, boxed.rawBytes.data() + at, 4);
std::memcpy(&whole, boxed.rawBytes.data() + at, 4);
const float size = real < 0 ? -real : real;
if (size > 0.0001f && size < 1.0e6f) std::cout << ' ' << real; else std::cout << " #" << whole;
}
std::cout << "\n";
}
for (const auto &import : document.imports) (void)import;
std::cout << "# imports " << document.imports.size() << ", arrays " << document.arrays.size() << ", types";
for (const auto &type : document.types) std::cout << ' ' << type.name;
std::cout << "\n";
return 0;
}
return 1;
}
if (mode == "--dump" && argc >= 4) {
const auto wanted = lower(argv[3]);
for (std::size_t i = 0; i < bundle.manifest.ebx.size(); ++i) {
if (lower(bundle.manifest.ebx[i].name) != wanted) continue;
const auto *payload = bundle.payload(dingosdk::frostbite::AssetKind::ebx, i);
if (!payload) break;
const auto bytes = data.read(*payload);
const auto document = ebx::read_document(bytes);
std::cerr << bundle.manifest.ebx[i].name << ": root type " << document.rootType << ", " << document.instances.size()
<< " instances\n";
// Every instance: an asset keeps its lists and sub-objects as separate ones.
std::size_t number{};
for (const auto &instance : document.instances) {
if (instance.descriptor < 0 || static_cast<std::size_t>(instance.descriptor) >= document.types.size()) continue;
const auto &type = document.types[static_cast<std::size_t>(instance.descriptor)];
std::cout << "# instance " << number++ << " " << type.name << " (" << instance.rawImage.size() << " bytes)\n";
walk(document, type, instance.rawImage, 0, 0, {});
}
return 0;
}
std::cerr << "no such asset\n";
return 1;
}
std::vector<std::string> words;
for (int i = (mode == "--types" ? 3 : 2); i < argc; ++i) words.push_back(lower(argv[i]));
std::size_t shown{};
for (std::size_t i = 0; i < bundle.manifest.ebx.size(); ++i) {
const auto name = lower(bundle.manifest.ebx[i].name);
std::string type;
bool match = mode != "--types" && std::ranges::any_of(words, [&](const std::string &w) { return name.find(w) != std::string::npos; });
if (mode == "--types") {
// Slower: opens every asset to learn its root type.
try {
if (const auto *payload = bundle.payload(dingosdk::frostbite::AssetKind::ebx, i)) {
type = ebx::read_document(data.read(*payload)).rootType;
const auto low = lower(type);
match = std::ranges::any_of(words, [&](const std::string &w) { return low.find(w) != std::string::npos; });
}
} catch (const std::exception &) {}
}
if (!match) continue;
std::cout << bundle.manifest.ebx[i].name << (type.empty() ? "" : "\t" + type) << '\n';
++shown;
}
std::cerr << shown << " of " << bundle.manifest.ebx.size() << " assets\n";
return 0;
} catch (const std::exception &error) {
std::cerr << "error: " << error.what() << '\n';
return 1;
}
}