mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-27 15:07:09 +01:00
Merge pull request #358 from hypnomalice/runtime/declared-board-materials
Plugins: support declared colour inputs on mod-owned board materials Conflicts with #363 resolved by keeping both: reconcile_variant_additions beside active_mod_sources in mod_catalog.h, and both test targets in cmake/Apps.cmake.
This commit is contained in:
commit
7170938799
21 files changed
+1364
-50
No files matched your search
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace dingosdk {
|
||||
// Bounded reads share delete access so a writer can rename the old document.
|
||||
class SharedTextFile {
|
||||
public:
|
||||
explicit SharedTextFile(const std::filesystem::path& path)
|
||||
#ifdef _WIN32
|
||||
: handle_(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) {
|
||||
if (handle_ == INVALID_HANDLE_VALUE) throw std::runtime_error("Cannot open content document");
|
||||
#else
|
||||
: file_(path, std::ios::binary | std::ios::ate) {
|
||||
if (!file_) throw std::runtime_error("Cannot open content document");
|
||||
#endif
|
||||
}
|
||||
SharedTextFile(const SharedTextFile&) = delete;
|
||||
SharedTextFile& operator=(const SharedTextFile&) = delete;
|
||||
~SharedTextFile() {
|
||||
#ifdef _WIN32
|
||||
CloseHandle(handle_);
|
||||
#endif
|
||||
}
|
||||
std::string read(std::size_t limit) {
|
||||
#ifdef _WIN32
|
||||
LARGE_INTEGER length{};
|
||||
if (!GetFileSizeEx(handle_, &length) || length.QuadPart < 0 ||
|
||||
static_cast<unsigned long long>(length.QuadPart) > limit || limit > MAXDWORD)
|
||||
throw std::runtime_error("Content document exceeds its read bound");
|
||||
std::string text(static_cast<std::size_t>(length.QuadPart), '\0');
|
||||
DWORD got{};
|
||||
if (!ReadFile(handle_, text.data(), static_cast<DWORD>(text.size()), &got, nullptr) || got != text.size())
|
||||
throw std::runtime_error("Cannot finish content document read");
|
||||
#else
|
||||
const auto length = file_.tellg();
|
||||
if (length < 0 || static_cast<std::uintmax_t>(length) > limit) throw std::runtime_error("Content document exceeds its read bound");
|
||||
std::string text(static_cast<std::size_t>(length), '\0');
|
||||
file_.seekg(0);
|
||||
if (!file_.read(text.data(), static_cast<std::streamsize>(text.size()))) throw std::runtime_error("Cannot finish content document read");
|
||||
#endif
|
||||
return text;
|
||||
}
|
||||
private:
|
||||
#ifdef _WIN32
|
||||
HANDLE handle_;
|
||||
#else
|
||||
std::ifstream file_;
|
||||
#endif
|
||||
};
|
||||
inline std::string read_shared_text(const std::filesystem::path& path, std::size_t limit) {
|
||||
return SharedTextFile(path).read(limit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "Engine/Vfs/material_declaration_refresh.h"
|
||||
#include "Engine/Vfs/mod_catalog.h"
|
||||
#include "Engine/Core/Platform/shared_file.h"
|
||||
#include <Windows.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace dingosdk;
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
void check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); }
|
||||
void write(const fs::path& path, std::string_view text) {
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
file << text;
|
||||
if (!file) throw std::runtime_error("fixture write failed");
|
||||
}
|
||||
}
|
||||
int main() try {
|
||||
const auto root = fs::temp_directory_path() / ("reskate-declaration-refresh-" + std::to_string(GetCurrentProcessId()));
|
||||
fs::create_directories(root);
|
||||
const auto file = root / "reskate-items.json";
|
||||
write(file, R"({"mods":[{"mod":"Owner","items":["own_test"]},{"mod":"LowerPriority","items":["own_test"]}]})");
|
||||
auto sources = mods::read_item_sources_checked(file);
|
||||
check(sources && mods::item_source_owner(*sources, "OWN_TEST") == "Owner", "actual ownership reader resolves a case-insensitive item to the highest-priority source");
|
||||
check(mods::item_source_owner(*sources, "own_missing").empty(), "unknown ownership grants nothing");
|
||||
|
||||
mods::MaterialDeclarationRefresh refresh;
|
||||
std::size_t loads{};
|
||||
const auto load = [&] {
|
||||
++loads;
|
||||
auto parsed = mods::read_item_sources_checked(file);
|
||||
if (!parsed) throw std::runtime_error("partial ownership write");
|
||||
mods::MaterialDeclarations value;
|
||||
value.sources = std::move(*parsed);
|
||||
return value;
|
||||
};
|
||||
auto signature = std::vector{mods::declaration_file_stamp(file)};
|
||||
check(refresh.update(signature, load), "initial declarations are loaded");
|
||||
const auto good = refresh.value();
|
||||
for (int poll = 0; poll < 100; ++poll) check(!refresh.update(signature, load), "unchanged polls do not reload");
|
||||
check(loads == 1, "unchanged polling never invokes the JSON loader");
|
||||
signature[0].revision = 1;
|
||||
check(refresh.update(signature, load) && loads == 2, "a live apply invalidates ownership even when file metadata is unchanged");
|
||||
const auto after_apply = refresh.value();
|
||||
write(file, "{partial");
|
||||
signature = {mods::declaration_file_stamp(file)};
|
||||
bool failed{};
|
||||
try { refresh.update(signature, load); } catch (const std::exception&) { failed = true; }
|
||||
check(failed && refresh.value() == after_apply && good != after_apply, "partial reads retain the exact last good ownership snapshot");
|
||||
write(file, R"({"mods":[]})");
|
||||
signature = {mods::declaration_file_stamp(file)};
|
||||
check(refresh.update(signature, load) && refresh.value()->sources.empty(), "valid empty ownership differs from failed reads");
|
||||
|
||||
const auto replacement = root / "replacement.json";
|
||||
write(replacement, R"({"mods":[{"mod":"NewOwner","items":["own_test"]}]})");
|
||||
{
|
||||
SharedTextFile held(file);
|
||||
// Windows may refuse replacing an open target even with delete sharing,
|
||||
// but the read must at least allow the old document to be renamed away.
|
||||
const auto retired = root / "retired.json";
|
||||
fs::rename(file, retired);
|
||||
fs::rename(replacement, file);
|
||||
check(held.read(1024) == R"({"mods":[]})", "an open ownership read permits renaming the old document and retains its old file");
|
||||
}
|
||||
sources = mods::read_item_sources_checked(file);
|
||||
check(sources && mods::item_source_owner(*sources, "own_test") == "NewOwner", "the next read sees the replaced ownership document");
|
||||
failed = false;
|
||||
try { (void)read_shared_text(file, 1); } catch (const std::exception&) { failed = true; }
|
||||
check(failed, "shared reads retain their size bound");
|
||||
|
||||
mods::Catalog active;
|
||||
mods::Mod running; running.name = "Running"; running.directory = root / "running";
|
||||
active.mods.push_back(running);
|
||||
mods::variants_applied(active);
|
||||
const auto before = mods::active_mod_sources();
|
||||
write(root / "mods.json", R"({"mods":[{"name":"Unapplied","enabled":true}]})");
|
||||
check(mods::active_mod_sources() == before && before.size() == 1 && before[0].name == "Running",
|
||||
"editing saved settings does not activate unapplied mod sources");
|
||||
active.mods[0].name = "Applied";
|
||||
active.mods[0].directory = root / "applied";
|
||||
mods::variants_applied(active);
|
||||
check(mods::active_mod_sources()[0].directory == root / "applied", "successful live apply publishes the actual source directory");
|
||||
fs::remove(file); fs::remove(root / "retired.json"); fs::remove(root / "mods.json"); fs::remove(root);
|
||||
std::cout << "material declaration refresh tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; }
|
||||
@@ -0,0 +1,141 @@
|
||||
// Checks how a mod's reskate-materials.json is read: a GUID's ParamDbKey hash and identity, what a
|
||||
// declaration may hold (colours and numbers in bounds, never integers, switches or textures), the
|
||||
// limits, and that a document of the wrong shape is refused whole.
|
||||
#include "Engine/Vfs/mod_materials.h"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
int failures = 0;
|
||||
void check(bool condition, const std::string& message) {
|
||||
if (!condition) { std::cerr << "FAIL: " << message << '\n'; ++failures; }
|
||||
}
|
||||
|
||||
using dingosdk::mods::parse_material_declarations;
|
||||
|
||||
bool refused(const std::string& json) {
|
||||
try { parse_material_declarations("mod", json); } catch (const std::exception&) { return true; }
|
||||
return false;
|
||||
}
|
||||
std::string with_parameter(const std::string& parameter, const std::string& part = "deck") {
|
||||
return R"({"schema":1,"materials":[{"part":")" + part + R"(","items":["Example_Deck"],"parameters":[)" + parameter + "]}]}";
|
||||
}
|
||||
constexpr const char* guid = "1af346775c032d5b59ff5e88010069b6";
|
||||
}
|
||||
|
||||
int main() try {
|
||||
// The eight inputs of the Example declared-input deck: their GUIDs and the hash and identity the game
|
||||
// keys them by (parameter-contract.json).
|
||||
struct Known { const char* guid; std::uint64_t key; std::uint16_t identity; };
|
||||
const Known known[] = {
|
||||
{"1af346775c032d5b59ff5e88010069b6", 6569911127076434714ULL, 46697},
|
||||
{"d317d1d20f5c745159ff5e880100dcb0", 5869417437401782227ULL, 45276},
|
||||
{"ed58e12fb758105c59ff5e8801005aea", 6633899794922297581ULL, 59994},
|
||||
{"9f361c20ac084e5a59ff5e8801006ffe", 6507148046963062431ULL, 65135},
|
||||
{"c7c08365cf15fc5759ff5e8801006651", 6339966355936362695ULL, 20838},
|
||||
{"83f212bddd522f5a59ff5e880100e557", 6498503899632300675ULL, 22501},
|
||||
{"97901b52e1a1fd5559ff5e88010012b9", 6196286651448922263ULL, 47378},
|
||||
{"126f57aa15de0b5859ff5e8801003944", 6344408684715011858ULL, 17465},
|
||||
};
|
||||
for (const auto& k : known) {
|
||||
std::uint64_t key{};
|
||||
std::uint16_t identity{};
|
||||
check(dingosdk::mods::material_parameter_key(k.guid, key, identity) && key == k.key && identity == k.identity,
|
||||
std::string("GUID ") + k.guid + " keys as the game keys it");
|
||||
}
|
||||
std::uint64_t key{};
|
||||
std::uint16_t identity{};
|
||||
check(!dingosdk::mods::material_parameter_key("1af34677", key, identity), "A GUID is 32 hex digits");
|
||||
check(!dingosdk::mods::material_parameter_key("zzf346775c032d5b59ff5e88010069b6", key, identity), "of hex");
|
||||
|
||||
{
|
||||
const auto read = parse_material_declarations("Example", R"({
|
||||
"schema": 1,
|
||||
"materials": [{
|
||||
"part": "deck",
|
||||
"items": ["Example_Deck", "Example_Deck_Wide"],
|
||||
"parameters": [
|
||||
{"guid": "1af346775c032d5b59ff5e88010069b6", "type": "colour", "default": [0.5, 0.5, 0.5]},
|
||||
{"guid": "126f57aa15de0b5859ff5e8801003944", "type": "color", "default": [0, 0, 0]},
|
||||
{"guid": "d317d1d20f5c74512d5cb62b0100dcb0", "type": "number", "default": -2.5}
|
||||
]
|
||||
}]
|
||||
})");
|
||||
check(read.size() == 1 && read[0].mod == "Example" && read[0].part == "deck" && read[0].items.size() == 2, "A material is read");
|
||||
const auto& p = read[0].parameters;
|
||||
check(p.size() == 3 && p[0].colour && p[0].key == 6569911127076434714ULL && p[0].identity == 46697 &&
|
||||
p[0].fallback == std::array<float, 3>{.5f, .5f, .5f},
|
||||
"with its colours keyed from their GUIDs and their defaults");
|
||||
check(p[1].colour, "either spelling of colour");
|
||||
check(!p[2].colour && p[2].fallback[0] == -2.5f, "and a number");
|
||||
}
|
||||
|
||||
{
|
||||
std::string collection = R"({"schema":1,"materials":[)";
|
||||
for (int deck = 0; deck < 14; ++deck) {
|
||||
if (deck) collection += ',';
|
||||
collection += R"({"part":"deck","items":["Own_Test_)" + std::to_string(deck) + R"("],"parameters":[)";
|
||||
for (int channel = 0; channel < 8; ++channel) {
|
||||
if (channel) collection += ',';
|
||||
collection += R"({"guid":")" + std::string(known[channel].guid) +
|
||||
R"(","type":"colour","default":[0,0,)" + std::to_string(deck / 16.0) + "]}";
|
||||
}
|
||||
collection += "]}";
|
||||
}
|
||||
const auto declarations = parse_material_declarations("Collection", collection + "]}");
|
||||
check(declarations.size() == 14, "A fourteen-deck collection is accepted as a whole");
|
||||
for (const auto& material : declarations)
|
||||
check(material.items.size() == 1 && material.parameters.size() == 8,
|
||||
"Each deck retains its eight defaults and item scope");
|
||||
check(declarations.front().parameters.back().fallback != declarations.back().parameters.back().fallback,
|
||||
"Different deck defaults survive parsing");
|
||||
}
|
||||
|
||||
// Only what a plugin may set, in bounds.
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"integer","default":13})")),
|
||||
"An integer picks how the item is drawn: not declarable");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"switch","default":true})")), "nor a switch");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"texture","default":0})")), "nor a texture");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"colour","default":[1.5,0,0]})")), "A colour is 0 to 1");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"colour","default":[1,0]})")), "with three channels");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"number","default":1000})")), "A number has bounds");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"number","default":"1"})")), "and is a number");
|
||||
check(refused(with_parameter(R"({"guid":"1af34677","type":"colour","default":[0,0,0]})")), "A GUID is 32 hex digits");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"colour","default":[0,0,0]},{"guid":")" +
|
||||
std::string(guid) + R"(","type":"colour","default":[1,1,1]})")),
|
||||
"A parameter is declared once");
|
||||
check(refused(with_parameter(R"({"guid":")" + std::string(guid) + R"(","type":"colour","default":[0,0,0]})", "skater")),
|
||||
"A part is one of the board's");
|
||||
|
||||
// The wrong shape is refused whole.
|
||||
check(refused(R"({"schema":1.5,"materials":[]})"),"Fractional schema rejected");
|
||||
check(refused(with_parameter(R"({"guid":"1af346775c032d5b2d5cb62b010069b6","type":"colour","default":[0,0,0]})")),"GUID type must match declared colour");
|
||||
check(refused(with_parameter(R"({"guid":"1af346775c032d5b59ff5e88020069b6","type":"colour","default":[0,0,0]})")),"GUID count must be one");
|
||||
check(refused("{}"), "A document without a schema");
|
||||
check(refused(R"({"schema":2,"materials":[]})"), "or of another schema");
|
||||
check(refused(R"({"schema":1,"materials":[{"part":"deck","items":[],"parameters":[]}]})"), "A material with no items");
|
||||
check(refused(R"({"schema":1,"materials":[{"part":"deck","items":["a"],"parameters":[]}]})"), "or no parameters");
|
||||
check(refused(R"({"schema":1,"materials":[{"part":"deck","items":["bad
|
||||
name"],"parameters":[{"guid":"1af346775c032d5b59ff5e88010069b6","type":"colour","default":[0,0,0]}]}]})"),
|
||||
"An item's key is text without control characters");
|
||||
{
|
||||
std::string many = R"({"schema":1,"materials":[)";
|
||||
for (int i = 0; i < 33; ++i) many += std::string(i ? "," : "") + R"({"part":"deck","items":["a"],"parameters":[{"guid":")" + guid + R"(","type":"colour","default":[0,0,0]}]})";
|
||||
check(refused(many + "]}"), "At most 32 material groups");
|
||||
std::string parameters;
|
||||
for (int i = 0; i < 33; ++i) {
|
||||
char g[33];
|
||||
std::snprintf(g, sizeof(g), "%02x%030x", i + 1, i + 1);
|
||||
parameters += std::string(i ? "," : "") + R"({"guid":")" + g + R"(","type":"colour","default":[0,0,0]})";
|
||||
}
|
||||
check(refused(with_parameter(parameters)), "and 32 parameters to a material");
|
||||
}
|
||||
|
||||
if (failures) return 1;
|
||||
std::cout << "mod materials tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "FAIL: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "mod_materials.h"
|
||||
#include "mod_item_sources.h"
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace dingosdk::mods {
|
||||
struct MaterialDeclarations {
|
||||
std::uint64_t revision{};
|
||||
std::vector<MaterialDeclaration> materials;
|
||||
std::vector<ItemSource> sources;
|
||||
};
|
||||
struct DeclarationFileStamp {
|
||||
std::filesystem::path path;
|
||||
std::string owner;
|
||||
bool present{};
|
||||
std::uintmax_t size{};
|
||||
std::filesystem::file_time_type written{};
|
||||
std::uint64_t revision{};
|
||||
bool operator==(const DeclarationFileStamp&) const = default;
|
||||
};
|
||||
inline DeclarationFileStamp declaration_file_stamp(const std::filesystem::path& path, std::string owner = {}) {
|
||||
DeclarationFileStamp stamp{path, std::move(owner)};
|
||||
stamp.present = std::filesystem::exists(path);
|
||||
if (stamp.present) {
|
||||
stamp.size = std::filesystem::file_size(path);
|
||||
stamp.written = std::filesystem::last_write_time(path);
|
||||
}
|
||||
return stamp;
|
||||
}
|
||||
// Owned by the single refresh worker. No successful signature or value changes
|
||||
// until a complete load succeeds, so failed/transient reads can be retried.
|
||||
class MaterialDeclarationRefresh {
|
||||
public:
|
||||
template<class Load> bool update(const std::vector<DeclarationFileStamp>& signature, Load&& load) {
|
||||
if (value_ && signature == signature_) return false;
|
||||
auto next = std::make_shared<const MaterialDeclarations>(load());
|
||||
auto next_signature = signature;
|
||||
signature_ = std::move(next_signature);
|
||||
value_ = std::move(next);
|
||||
return true;
|
||||
}
|
||||
std::shared_ptr<const MaterialDeclarations> value() const { return value_; }
|
||||
private:
|
||||
std::vector<DeclarationFileStamp> signature_;
|
||||
std::shared_ptr<const MaterialDeclarations> value_;
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
@@ -143,10 +144,12 @@ using VariantMap = std::map<std::string, std::string, std::less<>>;
|
||||
struct VariantsApplied {
|
||||
VariantMap in_play;
|
||||
std::vector<std::string> set_aside;
|
||||
std::vector<ModSource> sources;
|
||||
};
|
||||
std::mutex variants_mutex;
|
||||
std::optional<VariantMap> chosen_variants;
|
||||
std::optional<VariantsApplied> applied_variants;
|
||||
std::atomic<std::uint64_t> applied_revision{};
|
||||
VariantMap read_choices() {
|
||||
VariantMap out;
|
||||
const auto path = variants_file();
|
||||
@@ -253,10 +256,24 @@ std::vector<std::string> set_aside_for_variants() {
|
||||
void variants_applied(const Catalog& catalog) noexcept {
|
||||
try {
|
||||
std::lock_guard lock(variants_mutex);
|
||||
applied_variants = VariantsApplied{catalog.variants_in_play, catalog.set_aside};
|
||||
std::vector<ModSource> sources;
|
||||
for (const auto& mod : catalog.mods) sources.push_back({mod.name, mod.directory});
|
||||
applied_variants = VariantsApplied{catalog.variants_in_play, catalog.set_aside, std::move(sources)};
|
||||
applied_revision.fetch_add(1, std::memory_order_release);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
std::vector<ModSource> active_mod_sources() {
|
||||
{
|
||||
std::lock_guard lock(variants_mutex);
|
||||
if (applied_variants) return applied_variants->sources;
|
||||
}
|
||||
std::vector<ModSource> result;
|
||||
for (const auto& mod : catalog().mods) result.push_back({mod.name, mod.directory});
|
||||
return result;
|
||||
}
|
||||
std::uint64_t active_sources_revision() noexcept { return applied_revision.load(std::memory_order_acquire); }
|
||||
|
||||
std::filesystem::path engine_data_root() noexcept {
|
||||
try {
|
||||
std::array<wchar_t, 32768> module_path{};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -52,6 +53,16 @@ struct Catalog {
|
||||
// selected variant. Withdraw orphan donors and rebuild authority from survivors.
|
||||
void reconcile_variant_additions(Catalog& catalog);
|
||||
|
||||
struct ModSource {
|
||||
std::string name;
|
||||
std::filesystem::path directory;
|
||||
bool operator==(const ModSource&) const = default;
|
||||
};
|
||||
// The launch catalogue, or the last successfully applied catalogue. Reading
|
||||
// saved mods.json alone must not activate content that is not running yet.
|
||||
std::vector<ModSource> active_mod_sources();
|
||||
std::uint64_t active_sources_revision() noexcept;
|
||||
|
||||
// Variants (mod_list.h). For the run each variant is a mod of its own, listed after the mod that
|
||||
// holds it as <mod's folder>#<variant>, with that mod's build stamp; mods.json and the mod list only
|
||||
// know the mod. One variant of each such mod loads, the one the player chose (else the first of its
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Engine/Core/Json/json.h"
|
||||
#include "Engine/Core/Platform/path_text.h"
|
||||
#include "Engine/Core/Platform/shared_file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
@@ -11,12 +12,10 @@
|
||||
namespace dingosdk::mods {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::vector<ItemSource> read_item_sources(const fs::path& file) noexcept {
|
||||
std::optional<std::vector<ItemSource>> read_item_sources_checked(const fs::path& file) noexcept {
|
||||
std::vector<ItemSource> sources;
|
||||
try {
|
||||
std::ifstream input(file, std::ios::binary);
|
||||
if (!input) return {};
|
||||
const std::string text{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
const auto text = read_shared_text(file, 64u << 20);
|
||||
const auto root = Json::parse(text, {64u << 20, 32, 1u << 24});
|
||||
const auto& mods = root.at("mods");
|
||||
for (std::size_t i = 0; i < mods.size(); ++i) {
|
||||
@@ -28,11 +27,15 @@ std::vector<ItemSource> read_item_sources(const fs::path& file) noexcept {
|
||||
sources.push_back(std::move(source));
|
||||
}
|
||||
} catch (...) {
|
||||
return {}; // an unreadable file only means no card says where its item is from
|
||||
return std::nullopt;
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
std::vector<ItemSource> read_item_sources(const fs::path& file) noexcept {
|
||||
return read_item_sources_checked(file).value_or(std::vector<ItemSource>{});
|
||||
}
|
||||
|
||||
void write_item_sources(const fs::path& file, const std::vector<ItemSource>& sources) {
|
||||
auto mods = Json::array();
|
||||
for (const auto& source : sources) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// Which mod each cosmetic item came from, so its card in the customization menu can say so:
|
||||
@@ -18,9 +20,24 @@ struct ItemSource {
|
||||
std::vector<std::string> keys; // the items it adds or changes, lower case
|
||||
};
|
||||
|
||||
inline std::string item_key(std::string_view item) {
|
||||
std::string key(item);
|
||||
for (auto& c : key) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + ('a' - 'A'));
|
||||
return key;
|
||||
}
|
||||
inline std::string_view item_source_owner(const std::vector<ItemSource>& sources, std::string_view item) {
|
||||
const auto key = item_key(item);
|
||||
for (const auto& source : sources)
|
||||
for (const auto& candidate : source.keys) if (candidate == key) return source.mod;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Highest priority first, as the merge orders the mods: where two list one item, the game uses
|
||||
// the first one's, so that is where it is from. Empty when there is no file or it cannot be read.
|
||||
[[nodiscard]] std::vector<ItemSource> read_item_sources(const std::filesystem::path& file) noexcept;
|
||||
// Unlike the card's best-effort reader, ownership-sensitive callers must retain
|
||||
// their last good snapshot when a document cannot be read or parsed.
|
||||
[[nodiscard]] std::optional<std::vector<ItemSource>> read_item_sources_checked(const std::filesystem::path& file) noexcept;
|
||||
// Throws when the file cannot be written.
|
||||
void write_item_sources(const std::filesystem::path& file, const std::vector<ItemSource>& sources);
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "Engine/Vfs/mod_materials.h"
|
||||
#include "Engine/Core/Json/json.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace dingosdk::mods {
|
||||
namespace {
|
||||
constexpr std::size_t most_materials = 32, most_items = 64, most_parameters = 32;
|
||||
constexpr float most_number = 100.0f; // board_material::most_number
|
||||
constexpr std::array<const char*, 4> parts{"deck", "grip", "trucks", "wheels"};
|
||||
|
||||
bool text_ok(const std::string& value) {
|
||||
return !value.empty() && value.size() <= 255 &&
|
||||
std::none_of(value.begin(), value.end(), [](unsigned char c) { return c < 32 || c == 127; });
|
||||
}
|
||||
int hex_digit(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
float finite(const Json& value, const char* what) {
|
||||
if (!value.is_number()) throw std::runtime_error(std::string(what) + " is not a number");
|
||||
const auto number = value.get<double>();
|
||||
if (!std::isfinite(number)) throw std::runtime_error(std::string(what) + " is not finite");
|
||||
return static_cast<float>(number);
|
||||
}
|
||||
MaterialParameter parameter(const Json& entry) {
|
||||
if (!entry.is_object() || !entry.contains("guid") || !entry.at("guid").is_string() || !entry.contains("type") ||
|
||||
!entry.at("type").is_string() || !entry.contains("default"))
|
||||
throw std::runtime_error("a parameter needs a guid, a type and a default");
|
||||
MaterialParameter out;
|
||||
if (!material_parameter_key(entry.at("guid").string(), out.key, out.identity))
|
||||
throw std::runtime_error("a parameter's guid is 32 hex digits");
|
||||
const auto type = entry.at("type").string();
|
||||
const auto& fallback = entry.at("default");
|
||||
auto guid = entry.at("guid").string();
|
||||
std::transform(guid.begin(), guid.end(), guid.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(c >= 'A' && c <= 'F' ? c + ('a' - 'A') : c);
|
||||
});
|
||||
const auto expected = (type == "colour" || type == "color") ? "59ff5e88" : "2d5cb62b";
|
||||
if (guid.substr(16, 8) != expected || guid.substr(24, 4) != "0100")
|
||||
throw std::runtime_error("parameter GUID must match its declared type and have count 1");
|
||||
|
||||
if (type == "colour" || type == "color") {
|
||||
out.colour = true;
|
||||
if (!fallback.is_array() || fallback.size() != 3) throw std::runtime_error("a colour's default is [r, g, b]");
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
out.fallback[c] = finite(fallback.at(c), "a colour channel");
|
||||
if (out.fallback[c] < 0 || out.fallback[c] > 1) throw std::runtime_error("a colour's channels are 0 to 1");
|
||||
}
|
||||
} else if (type == "number") {
|
||||
out.fallback[0] = finite(fallback, "a number's default");
|
||||
if (std::abs(out.fallback[0]) > most_number) throw std::runtime_error("a number is -100 to 100");
|
||||
} else {
|
||||
// An integer, a switch or a texture picks how the item is drawn: not declarable.
|
||||
throw std::runtime_error("a parameter's type is \"colour\" or \"number\"");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool material_parameter_key(const std::string& guid, std::uint64_t& key, std::uint16_t& identity) {
|
||||
if (guid.size() != 32) return false;
|
||||
std::array<std::uint8_t, 16> bytes{};
|
||||
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
||||
const int high = hex_digit(guid[i * 2]), low = hex_digit(guid[i * 2 + 1]);
|
||||
if (high < 0 || low < 0) return false;
|
||||
bytes[i] = static_cast<std::uint8_t>(high * 16 + low);
|
||||
}
|
||||
// As the game serializes a ParamDbKey: the hash is the GUID's first eight bytes, little-endian,
|
||||
// and the identity its last two (developer_hoodie_detail::ParameterKey).
|
||||
key = 0;
|
||||
for (std::size_t i = 0; i < 8; ++i) key |= std::uint64_t{bytes[i]} << (8 * i);
|
||||
identity = static_cast<std::uint16_t>(bytes[14] | (bytes[15] << 8));
|
||||
return key != 0;
|
||||
}
|
||||
|
||||
std::vector<MaterialDeclaration> parse_material_declarations(const std::string& mod, const std::string& json) {
|
||||
const auto root = Json::parse(json);
|
||||
if (!root.is_object() || !root.contains("schema") || !root.at("schema").is_number_integer() || root.at("schema").get<int>() != 1 ||
|
||||
!root.contains("materials") || !root.at("materials").is_array())
|
||||
throw std::runtime_error("expected schema 1 with a materials array");
|
||||
const auto& list = root.at("materials");
|
||||
if (list.size() > most_materials) throw std::runtime_error("at most 32 materials");
|
||||
std::vector<MaterialDeclaration> result;
|
||||
for (std::size_t i = 0; i < list.size(); ++i) {
|
||||
const auto& entry = list.at(i);
|
||||
if (!entry.is_object() || !entry.contains("part") || !entry.at("part").is_string() || !entry.contains("items") ||
|
||||
!entry.at("items").is_array() || !entry.contains("parameters") || !entry.at("parameters").is_array())
|
||||
throw std::runtime_error("a material needs a part, items and parameters");
|
||||
MaterialDeclaration declaration{mod, entry.at("part").string(), {}, {}};
|
||||
if (std::find(parts.begin(), parts.end(), declaration.part) == parts.end())
|
||||
throw std::runtime_error("a material's part is \"deck\", \"grip\", \"trucks\" or \"wheels\"");
|
||||
const auto& items = entry.at("items");
|
||||
const auto& parameters = entry.at("parameters");
|
||||
if (items.empty() || items.size() > most_items) throw std::runtime_error("a material names 1 to 64 items");
|
||||
if (parameters.empty() || parameters.size() > most_parameters) throw std::runtime_error("a material declares 1 to 32 parameters");
|
||||
for (std::size_t k = 0; k < items.size(); ++k) {
|
||||
if (!items.at(k).is_string() || !text_ok(items.at(k).string())) throw std::runtime_error("an item is its key, as text");
|
||||
declaration.items.push_back(items.at(k).string());
|
||||
}
|
||||
std::set<std::uint64_t> keys;
|
||||
for (std::size_t k = 0; k < parameters.size(); ++k) {
|
||||
auto p = parameter(parameters.at(k));
|
||||
if (!keys.insert(p.key).second) throw std::runtime_error("a parameter is declared twice");
|
||||
declaration.parameters.push_back(p);
|
||||
}
|
||||
result.push_back(std::move(declaration));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace dingosdk::mods
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dingosdk::mods {
|
||||
|
||||
// A shader input a mod's own board item declares for plugins (reskate.board.set_parameter): a
|
||||
// colour or a number its compiled shader reads, whose default lives in that shader and so is in
|
||||
// no material's live parameters until something sets it. Integers, switches and textures pick how
|
||||
// an item is drawn and cannot be declared.
|
||||
struct MaterialParameter {
|
||||
std::uint64_t key{}; // the ParamDbKey hash: the GUID's first eight bytes
|
||||
std::uint16_t identity{}; // the ParamDbKey identity: the GUID's last two bytes
|
||||
bool colour{}; // a ColorRgb, else a Float32
|
||||
std::array<float, 3> fallback{}; // the shader's own default (a number in [0])
|
||||
};
|
||||
struct MaterialDeclaration {
|
||||
std::string mod;
|
||||
std::string part; // "deck", "grip", "trucks" or "wheels"
|
||||
std::vector<std::string> items; // the item keys whose material these belong to
|
||||
std::vector<MaterialParameter> parameters;
|
||||
};
|
||||
|
||||
// Reads a mod's reskate-materials.json:
|
||||
// {"schema": 1, "materials": [{"part": "deck", "items": ["<item key>", ...],
|
||||
// "parameters": [{"guid": "<32 hex digits>", "type": "colour", "default": [r, g, b]},
|
||||
// {"guid": "...", "type": "number", "default": 0.5}]}]}
|
||||
// A GUID is the parameter's 16 serialized bytes as hex, as ReSkate Studio shows them. A colour's
|
||||
// channels are 0 to 1; a number is -100 to 100 (board_material::most_number). Text is non-empty, at
|
||||
// most 255 bytes and free of control characters. At most 32 materials, 64 items and 32 parameters
|
||||
// each. Throws std::exception on a document that is not this shape (the whole file is skipped:
|
||||
// half a declaration could hand a shader a value it does not expect).
|
||||
std::vector<MaterialDeclaration> parse_material_declarations(const std::string& mod, const std::string& json);
|
||||
|
||||
// The ParamDbKey hash and identity of a GUID given as 32 hex digits; false if it is not one.
|
||||
bool material_parameter_key(const std::string& guid, std::uint64_t& key, std::uint16_t& identity);
|
||||
|
||||
} // namespace dingosdk::mods
|
||||
@@ -37,6 +37,7 @@ constexpr std::uint64_t number_key = 0x841c7d74837f1d39ULL, integer_key = 0xb00a
|
||||
|
||||
struct Board {
|
||||
Memory memory;
|
||||
std::uintptr_t colour_type{};
|
||||
std::uintptr_t entity = 0x10000000;
|
||||
std::uintptr_t next = 0x20000000; // where the next allocation goes
|
||||
std::uintptr_t grip{}, deck{}, trucks{};
|
||||
@@ -64,6 +65,26 @@ struct Board {
|
||||
std::array<std::uint32_t, 4> raw{};
|
||||
std::uint32_t flags = 3;
|
||||
};
|
||||
// What the engine's typed setter does with a key a material does not hold: a node of its own,
|
||||
// linked into the material's map (developer_board.cpp, the native set_shader_parameter).
|
||||
std::uintptr_t add(std::uintptr_t material, std::uint64_t key, std::uint16_t identity, const Colour& value) {
|
||||
std::uintptr_t buckets{}, node{};
|
||||
std::uint32_t count{};
|
||||
memory(material + n::material_parameters, &buckets, sizeof(buckets));
|
||||
memory(material + n::material_parameter_count, &count, sizeof(count));
|
||||
const auto added = take(0x48);
|
||||
memory.put(added, developer_hoodie_detail::ParameterKey{identity, 12, 1, 1, key});
|
||||
std::array<std::uint32_t, 4> raw{};
|
||||
std::memcpy(raw.data(), value.data(), sizeof(value));
|
||||
memory.put(added + n::parameter_value, raw);
|
||||
memory.put(added + n::parameter_type, colour_type);
|
||||
memory.put<std::uint32_t>(added + n::parameter_flags, 3);
|
||||
memory(buckets, &node, sizeof(node));
|
||||
memory.put(added + n::parameter_next, node); // first in its bucket
|
||||
memory.put(buckets, added);
|
||||
memory.put<std::uint32_t>(material + n::material_parameter_count, count + 1);
|
||||
return added;
|
||||
}
|
||||
std::uintptr_t material(const std::vector<Value>& values, std::map<std::uint64_t, std::uintptr_t>* nodes = nullptr) {
|
||||
const auto at = take(0x300), buckets = take(16);
|
||||
memory.put(at + n::material_parameters, buckets);
|
||||
@@ -103,9 +124,11 @@ struct Board {
|
||||
}
|
||||
return at;
|
||||
}
|
||||
Board() {
|
||||
// `declared`: the deck as the Example declared-input deck's material holds it live: integers and switches only.
|
||||
explicit Board(bool declared = false) {
|
||||
const auto colour = type(0x7780c50, n::color_rgb_type_hash, 12), number = type(0x765db28, n::float_type_hash, 4),
|
||||
integer = type(0x765da68, n::integer_type_hash, 4), toggle = type(0x7780f50, n::boolean_type_hash, 4);
|
||||
colour_type = colour;
|
||||
const auto bits = [](float a, float b = 0, float c = 0) {
|
||||
std::array<std::uint32_t, 4> raw{};
|
||||
const std::array<float, 3> f{a, b, c};
|
||||
@@ -118,7 +141,13 @@ struct Board {
|
||||
{switch_key, toggle, 4, {1}},
|
||||
{0x1111, colour, 12, bits(.5f, .5f, .5f), 0}}, // not live: bit 0 of its flags clear
|
||||
&grip_nodes);
|
||||
deck = material({{base_colour, colour, 12, bits(.5f, .5f, .5f)}});
|
||||
if (declared) {
|
||||
std::vector<Value> selectors;
|
||||
for (std::uint64_t k = 1; k <= 7; ++k) selectors.push_back({0x5000 + k, k % 2 ? integer : toggle, 4, {static_cast<std::uint32_t>(k)}});
|
||||
deck = material(selectors);
|
||||
} else {
|
||||
deck = material({{base_colour, colour, 12, bits(.5f, .5f, .5f)}});
|
||||
}
|
||||
trucks = material({{base_colour, colour, 12, bits(.1f, .1f, .1f)}});
|
||||
const auto collection = take(0x80), cosmetic = take(0x200), appearance = take(0x100), list = take(24);
|
||||
memory.put(entity, base + addr::engine::board_entity_vtable);
|
||||
@@ -246,11 +275,231 @@ void holds_and_puts_back() {
|
||||
detail::apply(saved, holds, found, write, publish);
|
||||
check(writes == before, "Nothing is written while the board is being dressed");
|
||||
}
|
||||
|
||||
// ---- shader inputs a mod declares for its own deck (reskate-materials.json) ---------------------------------
|
||||
// The Example declared-input deck's eight inputs (parameter-contract.json): colours its compiled shader reads,
|
||||
// in no live map until something sets them.
|
||||
struct Liquid { std::uint64_t key; std::uint16_t identity; };
|
||||
constexpr Liquid liquid[] = {
|
||||
{6569911127076434714ULL, 46697}, {5869417437401782227ULL, 45276}, {6633899794922297581ULL, 59994}, {6507148046963062431ULL, 65135},
|
||||
{6339966355936362695ULL, 20838}, {6498503899632300675ULL, 22501}, {6196286651448922263ULL, 47378}, {6344408684715011858ULL, 17465}};
|
||||
constexpr std::uint64_t declared_number = 0x7777777777777777ULL;
|
||||
|
||||
std::vector<mods::MaterialDeclaration> declared_declarations() {
|
||||
mods::MaterialDeclaration d{"Example", "deck", {"Example_Deck"}, {}};
|
||||
for (const auto& l : liquid) d.parameters.push_back({l.key, l.identity, true, {.5f, .5f, .5f}});
|
||||
d.parameters.push_back({declared_number, 0x4242, false, {.25f, 0, 0}});
|
||||
return {d};
|
||||
}
|
||||
auto never_sold = [](std::string_view) { return false; };
|
||||
detail::Found declared_found(Board& board) {
|
||||
auto found = board.walk();
|
||||
found.declared = detail::declared_for(declared_declarations(), Part::deck, "example_deck", never_sold, "Example");
|
||||
return found;
|
||||
}
|
||||
|
||||
void declarations_follow_the_equipped_item() {
|
||||
const auto all = declared_declarations();
|
||||
const auto deck = detail::declared_for(all, Part::deck, "EXAMPLE_DECK", never_sold, "Example");
|
||||
check(deck.size() == 9 && deck[0].key == liquid[0].key && deck[0].identity == liquid[0].identity && deck[0].kind == Kind::colour &&
|
||||
deck[0].fallback == Colour{.5f, .5f, .5f} && deck[8].kind == Kind::number,
|
||||
"The equipped item's declarations apply (its key in any case)");
|
||||
check(detail::declared_for(all, Part::deck, "Some_Other_Deck", never_sold, "Example").empty(), "another deck's do not");
|
||||
check(detail::declared_for(all, Part::grip, "Example_Deck", never_sold, "Example").empty(), "nor another part's");
|
||||
check(detail::declared_for(all, Part::deck, "", never_sold, "Example").empty(), "nor an empty slot's");
|
||||
auto sold = [](std::string_view) { return true; };
|
||||
check(detail::declared_for(all, Part::deck, "Example_Deck", sold, "Example").empty(), "An item the game sells (or one it cannot tell) takes none");
|
||||
// Two mods giving one key differently: which shader is right cannot be told, so neither.
|
||||
auto both = all;
|
||||
both.push_back({"Other", "deck", {"Example_Deck"}, {{liquid[0].key, 1, true, {0, 0, 0}}, {liquid[1].key, liquid[1].identity, true, {.5f, .5f, .5f}}}});
|
||||
check(detail::declared_for(all,Part::deck,"Example_Deck",never_sold,"Other").empty(),"Other mod cannot declare this item");
|
||||
check(detail::declared_for(all,Part::deck,"Example_Deck",never_sold,"").empty(),"Unknown ownership is refused");
|
||||
const auto merged = detail::declared_for(both, Part::deck, "Example_Deck", never_sold, "Example");
|
||||
check(std::any_of(merged.begin(), merged.end(), [](const detail::Declared& d) { return d.key == liquid[0].key; }) &&
|
||||
std::count_if(merged.begin(), merged.end(), [](const detail::Declared& d) { return d.key == liquid[1].key; }) == 1,
|
||||
"Another mod cannot suppress the owner's declarations");
|
||||
}
|
||||
|
||||
void lists_and_refuses_declared() {
|
||||
Board board(true);
|
||||
auto found = declared_found(board);
|
||||
const auto listed = detail::listed(found, Part::deck);
|
||||
check(listed.size() == 7 + 9, "The deck lists its seven live selectors and its nine declared inputs");
|
||||
check(std::all_of(listed.begin(), listed.begin() + 7, [](const Parameter& p) { return !p.inherited && (p.kind == Kind::integer || p.kind == Kind::toggle); }),
|
||||
"the selectors as the material holds them");
|
||||
check(std::all_of(listed.begin() + 7, listed.end(), [](const Parameter& p) { return p.inherited; }) && listed[7].value == Colour{.5f, .5f, .5f},
|
||||
"the declared ones inherited, at their default");
|
||||
check(detail::listed(board.walk(), Part::deck).size() == 7, "Without the declaration only the live ones show");
|
||||
|
||||
Kind kind{};
|
||||
check(detail::refusal(found, Part::deck, liquid[0].key, {.1f, .2f, .3f}, true, kind).empty() && kind == Kind::colour,
|
||||
"A declared colour the material does not hold may be held");
|
||||
check(detail::refusal(board.walk(), Part::deck, liquid[0].key, {.1f, .2f, .3f}, true, kind).find("no parameter") != std::string::npos,
|
||||
"but not on a deck that does not declare it");
|
||||
check(!detail::refusal(found, Part::deck, liquid[0].key, {.1f, .2f, .3f}, false, kind).empty(), "A declared colour is not a number");
|
||||
check(!detail::refusal(found, Part::deck, liquid[0].key, {1.5f, 0, 0}, true, kind).empty(), "and its channels are 0 to 1");
|
||||
check(detail::refusal(found, Part::deck, declared_number, {.5f, 0, 0}, false, kind).find("only a declared colour") != std::string::npos,
|
||||
"A declared number with no live value cannot be added (its type is not known to this build)");
|
||||
check(detail::refusal(found, Part::deck, 0x5001, {1, 0, 0}, false, kind).find("only read") != std::string::npos,
|
||||
"A live integer stays read-only");
|
||||
// A declaration naming a key the material holds as an integer or a switch changes nothing.
|
||||
auto clash = found;
|
||||
clash.declared.push_back({Part::deck, 0x5002, 0x1234, Kind::colour, {0, 0, 0}});
|
||||
check(detail::refusal(clash, Part::deck, 0x5002, {1, 0, 0}, true, kind).find("not as its mod declares") != std::string::npos,
|
||||
"A switch a mod declares as a colour is refused");
|
||||
// A live colour whose identity is not the declared one is not the declared parameter.
|
||||
Board ordinary;
|
||||
auto other = ordinary.walk();
|
||||
other.declared.push_back({Part::grip, graphic_colour, 0x9999, Kind::colour, {.5f, .5f, .5f}});
|
||||
check(detail::refusal(other, Part::grip, graphic_colour, {1, 0, 0}, true, kind).find("not as its mod declares") != std::string::npos,
|
||||
"and so is a live colour of another identity");
|
||||
}
|
||||
|
||||
void owners_and_limits() {
|
||||
Board board(true);
|
||||
const auto found = declared_found(board);
|
||||
std::vector<detail::Hold> holds;
|
||||
check(detail::take(holds, found, "liquid", Part::deck, liquid[0].key, {.1f, .2f, .3f}, true).empty() && holds.size() == 1 &&
|
||||
holds[0].declared && holds[0].kind == Kind::colour,
|
||||
"A plugin takes a declared colour");
|
||||
check(detail::take(holds, found, "other", Part::deck, liquid[0].key, {1, 1, 1}, true).find("liquid holds") != std::string::npos,
|
||||
"another plugin cannot take it from it");
|
||||
check(detail::take(holds, found, "liquid", Part::deck, liquid[0].key, {.4f, .5f, .6f}, true).empty() && holds.size() == 1 &&
|
||||
holds[0].value == Colour{.4f, .5f, .6f},
|
||||
"its owner sets it again");
|
||||
check(!detail::take(holds, found, "liquid", Part::deck, 0x5001, {1, 0, 0}, false).empty() && holds.size() == 1, "A refusal holds nothing");
|
||||
for (std::size_t i = 1; i < most_held; ++i) holds.push_back({"liquid", Part::grip, 0x9000 + i, Kind::colour, {}, false});
|
||||
check(detail::take(holds, found, "liquid", Part::deck, liquid[1].key, {0, 0, 0}, true).find("at most") != std::string::npos,
|
||||
"A plugin holds at most 32, declared ones included");
|
||||
check(detail::take(holds, found, "other", Part::deck, liquid[1].key, {0, 0, 0}, true).empty(), "another has its own 32");
|
||||
detail::release(holds, "liquid", Part::grip, std::nullopt);
|
||||
check(holds.size() == 2, "Letting go of a part lets go of the plugin's holds there only");
|
||||
detail::release(holds, "liquid", std::nullopt, std::nullopt);
|
||||
check(holds.size() == 1 && holds[0].owner == "other", "and letting go of all (an unload) leaves the others' alone");
|
||||
}
|
||||
|
||||
void adds_and_puts_back_declared() {
|
||||
Board board(true);
|
||||
std::vector<detail::Saved> saved;
|
||||
std::vector<detail::Hold> holds;
|
||||
int writes{}, publishes{}, inserts{};
|
||||
std::map<std::uint64_t, std::uintptr_t> added;
|
||||
auto write = [&](std::uintptr_t material, std::uintptr_t node, Kind kind, const Colour& value) {
|
||||
check(material == board.deck, "Only the deck is written");
|
||||
++writes;
|
||||
board.memory.put_bytes(node + n::parameter_value, value.data(), kind == Kind::colour ? sizeof(Colour) : sizeof(float));
|
||||
return true;
|
||||
};
|
||||
auto publish = [&](std::uintptr_t item) {
|
||||
check(item == board.deck_item, "The deck's item publishes");
|
||||
++publishes;
|
||||
};
|
||||
auto insert = [&](std::uintptr_t material, const detail::Declared& d, const Colour& value) {
|
||||
check(material == board.deck && d.part == Part::deck && d.kind == Kind::colour, "A declared colour is added to the deck's material");
|
||||
++inserts;
|
||||
added[d.key] = board.add(material, d.key, d.identity, value);
|
||||
return true;
|
||||
};
|
||||
const auto found = [&] { return declared_found(board); };
|
||||
const auto tick = [&] { detail::apply(saved, holds, found(), write, publish, insert); };
|
||||
// The liquid controller's eight colours, all held at once.
|
||||
for (std::size_t i = 0; i < 8; ++i)
|
||||
check(detail::take(holds, found(), "liquid", Part::deck, liquid[i].key, {.1f * i, .2f, .3f}, true).empty(), "each liquid colour is taken");
|
||||
tick();
|
||||
check(inserts == 8 && writes == 0 && publishes == 1, "The eight are added by the engine's setter, and the deck published once");
|
||||
for (std::size_t i = 0; i < 8; ++i) check(board.colour_at(added[liquid[i].key]) == Colour{.1f * i, .2f, .3f}, "at the held value");
|
||||
const auto walked = board.walk();
|
||||
const auto* deck = part_of(walked, Part::deck);
|
||||
check(deck && deck->nodes.size() == 15 && std::count_if(deck->nodes.begin(), deck->nodes.end(), [&](const detail::Node& node) {
|
||||
return node.parameter.kind == Kind::colour && node.identity == liquid[0].identity;
|
||||
}) == 1,
|
||||
"and then the material holds them live, with their identities");
|
||||
for (int i = 0; i < 6; ++i) tick();
|
||||
check(inserts == 8 && writes == 0, "Once added they are not added again");
|
||||
// About 30 times a second, the controller's new values: written in place.
|
||||
holds[0].value = {.9f, .8f, .7f};
|
||||
tick();
|
||||
check(inserts == 8 && writes == 1 && board.colour_at(added[liquid[0].key]) == Colour{.9f, .8f, .7f}, "A new value is written in place");
|
||||
// Let go (clear_parameter, or the plugin unloading): each goes back to its declared default.
|
||||
detail::release(holds, "liquid", std::nullopt, std::nullopt);
|
||||
for (int i = 0; i < 8; ++i) tick();
|
||||
for (std::size_t i = 0; i < 8; ++i)
|
||||
check(board.colour_at(added[liquid[i].key]) == Colour{.5f, .5f, .5f}, "Let go, a declared colour goes back to its shader's default");
|
||||
check(saved.empty(), "and nothing is kept once that shows");
|
||||
const auto after = board.walk();
|
||||
check(part_of(after, Part::deck)->nodes.size() == 15, "What was added stays (the renderer copies it), at its default");
|
||||
check(std::all_of(part_of(after, Part::deck)->nodes.begin(), part_of(after, Part::deck)->nodes.begin() + 15, [](const detail::Node& node) {
|
||||
return node.parameter.kind != Kind::integer || node.parameter.integer != 0;
|
||||
}),
|
||||
"and the selectors were never touched");
|
||||
}
|
||||
|
||||
void a_change_of_deck_lets_go() {
|
||||
Board board(true);
|
||||
std::vector<detail::Saved> saved;
|
||||
std::vector<detail::Hold> holds;
|
||||
std::uintptr_t node{};
|
||||
int inserts{};
|
||||
auto write = [&](std::uintptr_t, std::uintptr_t at, Kind, const Colour& value) {
|
||||
board.memory.put_bytes(at + n::parameter_value, value.data(), sizeof(Colour));
|
||||
return true;
|
||||
};
|
||||
auto publish = [](std::uintptr_t) {};
|
||||
auto insert = [&](std::uintptr_t material, const detail::Declared& d, const Colour& value) {
|
||||
++inserts;
|
||||
node = board.add(material, d.key, d.identity, value);
|
||||
return true;
|
||||
};
|
||||
check(detail::take(holds, declared_found(board), "liquid", Part::deck, liquid[0].key, {1, 0, 0}, true).empty(), "taken");
|
||||
detail::apply(saved, holds, declared_found(board), write, publish, insert);
|
||||
check(inserts == 1 && board.colour_at(node) == Colour{1, 0, 0}, "added on the declared-input deck");
|
||||
// The deck is changed for one that declares nothing: the walk finds no declaration.
|
||||
auto changed = board.walk();
|
||||
check(detail::drop_undeclared(holds, changed) == 1 && holds.empty(), "The hold is let go when its deck no longer declares it");
|
||||
for (int i = 0; i < 8; ++i) detail::apply(saved, holds, changed, write, publish, insert);
|
||||
check(inserts == 1 && board.colour_at(node) == Colour{.5f, .5f, .5f} && saved.empty(), "what it added goes back, and nothing more is added");
|
||||
// A hold kept (taken while declared) is never written to a deck that does not declare it.
|
||||
holds = {{"liquid", Part::deck, liquid[1].key, Kind::colour, {0, 1, 0}, true}};
|
||||
detail::apply(saved, holds, changed, write, publish, insert);
|
||||
check(inserts == 1 && saved.empty(), "A declared hold is not carried onto an incompatible deck");
|
||||
// A board being dressed: nothing is let go or written while the recipe is on its way.
|
||||
changed.declarations_pending[0]=true;
|
||||
check(detail::drop_undeclared(holds,changed)==0 && holds.size()==1,"Rebuilding materials retain declared holds while settling");
|
||||
changed.pending = true;
|
||||
check(detail::drop_undeclared(holds, changed) == 0 && holds.size() == 1, "nor let go while the board is being dressed");
|
||||
// Back on the declared-input deck it applies again.
|
||||
detail::apply(saved, holds, declared_found(board), write, publish, insert);
|
||||
check(inserts == 2, "and is added again once the declaring deck is back");
|
||||
}
|
||||
|
||||
void declarations_wait_for_the_board_to_settle() {
|
||||
detail::Gate gate;
|
||||
check(!detail::settled(gate, "Example_Deck", 0x1000, 0), "A newly equipped item does not apply at once");
|
||||
check(!detail::settled(gate, "Example_Deck", 0x1000, detail::settle_ms - 1), "nor before it has settled");
|
||||
check(detail::settled(gate, "Example_Deck", 0x1000, detail::settle_ms), "then it does");
|
||||
check(!detail::settled(gate, "Example_Deck", 0x2000, detail::settle_ms + 5), "A new material under it starts again");
|
||||
check(!detail::settled(gate, "Other_Deck", 0x2000, 2 * detail::settle_ms + 5), "as does another item");
|
||||
check(!detail::settled(gate, "Other_Deck", 0, 9 * detail::settle_ms), "and no material never settles");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::map<std::string, int> stock{{"free_stock", 1}, {"paid_stock", 1}};
|
||||
const auto not_reserved = [](std::string_view) { return false; };
|
||||
check(detail::declared_item_restricted("FREE_STOCK", true, stock, not_reserved), "free stock cannot grant declarations");
|
||||
check(detail::declared_item_restricted("paid_stock", true, stock, not_reserved), "paid stock cannot grant declarations");
|
||||
check(detail::declared_item_restricted("OWN_DEV_TEST", true, stock, not_reserved), "developer items cannot grant declarations");
|
||||
check(detail::declared_item_restricted("own_private", false, stock, not_reserved), "unknown stock catalogue state fails closed");
|
||||
check(!detail::declared_item_restricted("own_private", true, stock, not_reserved), "a non-stock private item can proceed to ownership checks");
|
||||
check(detail::declared_item_restricted("own_reserved", true, stock, [](std::string_view) { return true; }), "reservation policy is still applied");
|
||||
walks_the_board();
|
||||
refuses_what_picks_the_look();
|
||||
holds_and_puts_back();
|
||||
declarations_follow_the_equipped_item();
|
||||
lists_and_refuses_declared();
|
||||
owners_and_limits();
|
||||
adds_and_puts_back_declared();
|
||||
a_change_of_deck_lets_go();
|
||||
declarations_wait_for_the_board_to_settle();
|
||||
std::cout << "board material tests passed\n";
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
#include "board_material.h"
|
||||
#include "worn_items.h"
|
||||
#include "Engine/Game/Build/supported_build.h"
|
||||
#include "local_customization_runtime.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Vfs/content_catalogs.h"
|
||||
#include "Engine/Vfs/mod_catalog.h"
|
||||
#include "Engine/Vfs/mod_item_sources.h"
|
||||
#include "Engine/Vfs/material_declaration_refresh.h"
|
||||
#include "Engine/Core/Platform/shared_file.h"
|
||||
#include "Extension/Multiplayer/Remote/native_pose_layout.h"
|
||||
#include "Extension/Multiplayer/Remote/native_skater.h"
|
||||
#include <Windows.h>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace dingosdk::board_material {
|
||||
namespace {
|
||||
@@ -13,7 +26,18 @@ struct State {
|
||||
std::vector<detail::Hold> holds;
|
||||
std::vector<detail::Saved> saved;
|
||||
std::optional<bool> functions; // the material publisher's bytes, once checked
|
||||
std::optional<bool> setter; // the typed setter's bytes and the ColorRgb descriptor, once checked
|
||||
std::uint16_t colour_type{}; // the ColorRgb descriptor's type index
|
||||
std::uint64_t reported{};
|
||||
std::uint64_t apply_reported{};
|
||||
// The board's items as last read (worn_items::worn_board), and when.
|
||||
std::vector<worn_items::Worn> worn;
|
||||
std::uint64_t worn_at{};
|
||||
std::array<detail::Gate, part_names.size()> gates;
|
||||
// The item each part was last reported as wearing: once per change, so a mod's maker can read
|
||||
// the key their reskate-materials.json has to name from the log.
|
||||
std::array<std::string, part_names.size()> reported_items;
|
||||
std::array<std::size_t, part_names.size()> reported_counts{};
|
||||
};
|
||||
State& state() {
|
||||
static auto* value = new State; // what is held may still be out at exit
|
||||
@@ -34,6 +58,19 @@ bool publish_materials(std::uintptr_t base, std::uintptr_t item) noexcept {
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
}
|
||||
// A declared colour the material does not hold yet, added by the engine's own typed setter
|
||||
// (MaterialInstance:addEsVector's, as developer_board.cpp adds the board's colours): it allocates,
|
||||
// hashes and links the node itself. Priority zero, a native preset's, so a later preset replaces it.
|
||||
bool insert_colour(std::uintptr_t base, std::uint16_t type_index, std::uintptr_t material, const detail::Declared& declared,
|
||||
const Colour& value) noexcept {
|
||||
const developer_hoodie_detail::ParameterKey key{declared.identity, sizeof(Colour), type_index, 1, declared.key};
|
||||
const developer_board_detail::NativeColor native{value, 0, base + addr::native_cosmetics::color_rgb_type};
|
||||
__try {
|
||||
return reinterpret_cast<std::uint64_t (*)(std::uintptr_t, const developer_hoodie_detail::ParameterKey*,
|
||||
const developer_board_detail::NativeColor*, std::uint8_t)>(
|
||||
base + addr::native_cosmetics::set_shader_parameter)(material, &key, &native, 0) != 0;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
}
|
||||
bool functions_match(State& s) {
|
||||
if (!s.functions) {
|
||||
std::array<unsigned char, addr::native_cosmetics::publish_materials_prefix.size()> bytes{};
|
||||
@@ -45,6 +82,168 @@ bool functions_match(State& s) {
|
||||
}
|
||||
return *s.functions;
|
||||
}
|
||||
// The typed setter, and the ColorRgb type it is given, as the supported build has them (checked as
|
||||
// developer_board_detail::materials checks the descriptor). Without them nothing declared is added.
|
||||
bool setter_matches(State& s) {
|
||||
if (!s.setter) {
|
||||
namespace n = addr::native_cosmetics;
|
||||
std::array<unsigned char, n::set_shader_parameter_prefix.size()> bytes{};
|
||||
std::uintptr_t info{};
|
||||
std::uint32_t hash{};
|
||||
std::uint16_t size{}, index{};
|
||||
const auto type = s.base + n::color_rgb_type;
|
||||
s.setter = peek(s.base + n::set_shader_parameter, bytes.data(), bytes.size()) && bytes == n::set_shader_parameter_prefix &&
|
||||
peek(type, &info, sizeof(info)) && info >= s.base && info < s.base + supported_build::game_image_size && peek(info, &hash, sizeof(hash)) &&
|
||||
hash == n::color_rgb_type_hash && peek(info + 6, &size, sizeof(size)) && size == sizeof(Colour) &&
|
||||
peek(type + 8, &index, sizeof(index));
|
||||
s.colour_type = index;
|
||||
if (!*s.setter)
|
||||
logging::log(logging::Level::warning, logging::Channel::customization,
|
||||
"Board parameters: the material setter differs from the supported build; declared colours cannot be added.");
|
||||
}
|
||||
return *s.setter;
|
||||
}
|
||||
|
||||
// ---- declarations: every enabled mod's reskate-materials.json -------------------------------------------
|
||||
// Read on a thread of their own and again only when a file changes, as the mods' music is
|
||||
// (local_music_assets.cpp): the game's frame never waits on the disk.
|
||||
using DeclarationSet = mods::MaterialDeclarations;
|
||||
struct Declarations {
|
||||
std::mutex mutex;
|
||||
std::shared_ptr<const DeclarationSet> ready;
|
||||
mods::MaterialDeclarationRefresh refresh;
|
||||
std::string error;
|
||||
bool reading{};
|
||||
std::uint64_t looked{};
|
||||
};
|
||||
Declarations& declarations() {
|
||||
static auto* value = new Declarations;
|
||||
return *value;
|
||||
}
|
||||
std::vector<mods::DeclarationFileStamp> declaration_signature(const std::vector<mods::ModSource>& sources,
|
||||
const std::filesystem::path& root) {
|
||||
std::vector<mods::DeclarationFileStamp> signature;
|
||||
signature.push_back(mods::declaration_file_stamp(root / mods::order_file));
|
||||
signature.back().revision = mods::active_sources_revision();
|
||||
signature.push_back(mods::declaration_file_stamp(root / mods::generated_folder / mods::item_sources_file));
|
||||
for (const auto& source : sources)
|
||||
signature.push_back(mods::declaration_file_stamp(source.directory / L"reskate-materials.json", source.name));
|
||||
return signature;
|
||||
}
|
||||
DeclarationSet read_declarations(const std::vector<mods::DeclarationFileStamp>& signature) {
|
||||
DeclarationSet result;
|
||||
result.revision = signature[0].revision;
|
||||
const auto has_declarations = std::any_of(signature.begin() + 2, signature.end(), [](const auto& file) { return file.present; });
|
||||
if (!has_declarations) return result;
|
||||
auto sources = mods::read_item_sources_checked(signature[1].path);
|
||||
if (!sources) throw std::runtime_error("Item ownership could not be read; retaining previous declarations");
|
||||
result.sources = std::move(*sources);
|
||||
for (std::size_t at = 2; at < signature.size(); ++at) {
|
||||
const auto& file = signature[at];
|
||||
if (!file.present) continue;
|
||||
auto read = mods::parse_material_declarations(file.owner, read_shared_text(file.path, 64 * 1024));
|
||||
std::move(read.begin(), read.end(), std::back_inserter(result.materials));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void read_declarations_now() noexcept {
|
||||
auto& d = declarations();
|
||||
std::shared_ptr<const DeclarationSet> read;
|
||||
std::string failure;
|
||||
try {
|
||||
const auto sources = mods::active_mod_sources();
|
||||
const auto root = mods::catalog().root;
|
||||
const auto signature = declaration_signature(sources, root);
|
||||
const bool changed = d.refresh.update(signature, [&] {
|
||||
auto next = read_declarations(signature);
|
||||
if (sources != mods::active_mod_sources() || signature != declaration_signature(sources, root))
|
||||
throw std::runtime_error("Content changed during declaration refresh; retaining previous declarations");
|
||||
return next;
|
||||
});
|
||||
read = d.refresh.value();
|
||||
if (changed)
|
||||
logging::log(logging::Level::info, logging::Channel::customization,
|
||||
"Board parameters: refreshed {} material declaration(s).", read->materials.size());
|
||||
} catch (const std::exception& error) {
|
||||
failure = error.what();
|
||||
} catch (...) {
|
||||
failure = "Declaration refresh failed; retaining previous declarations";
|
||||
}
|
||||
std::lock_guard lock(d.mutex);
|
||||
if (read) d.ready = std::move(read);
|
||||
else if (!d.ready) d.ready = std::make_shared<const DeclarationSet>();
|
||||
if (!failure.empty() && failure != d.error)
|
||||
logging::log(logging::Level::warning, logging::Channel::customization, "Board parameters: {}", failure);
|
||||
d.error = std::move(failure);
|
||||
d.reading = false;
|
||||
}
|
||||
// Only file metadata and the applied source snapshot are polled. Unchanged
|
||||
// polls never parse declarations, reload the catalogue, or unpack built-ins.
|
||||
std::shared_ptr<const DeclarationSet> current_declarations() {
|
||||
auto& d = declarations();
|
||||
std::lock_guard lock(d.mutex);
|
||||
const auto now = GetTickCount64();
|
||||
if (!d.reading && (!d.looked || now - d.looked >= 2000)) {
|
||||
d.reading = true;
|
||||
d.looked = now;
|
||||
try {
|
||||
std::thread(read_declarations_now).detach();
|
||||
} catch (...) {
|
||||
d.reading = false;
|
||||
}
|
||||
}
|
||||
// Keep old values for recovery, but never grant declarations from a source
|
||||
// snapshot belonging to mods that are no longer the running catalogue.
|
||||
if (d.ready && d.ready->revision != mods::active_sources_revision()) return {};
|
||||
return d.ready;
|
||||
}
|
||||
// Stock/developer entries and unknown catalogue state cannot grant inputs.
|
||||
bool sold(std::string_view item) {
|
||||
const auto& catalog = content_cache::catalogs();
|
||||
return detail::declared_item_restricted(item, catalog.available, catalog.items,
|
||||
[](std::string_view key) { return profile_runtime::reserved_cosmetic(key); });
|
||||
}
|
||||
constexpr std::array<std::string_view, part_names.size()> worn_slots{"Deck", "Grip tape", "Trucks", "Wheels"};
|
||||
|
||||
// The equipped items' declarations, on the board found: a part's apply once its item and material
|
||||
// have settled (detail::settled).
|
||||
void declare(State& s, detail::Found& found) {
|
||||
const auto all = current_declarations();
|
||||
if (!all) {
|
||||
found.pending = true;
|
||||
return;
|
||||
}
|
||||
const auto now = GetTickCount64();
|
||||
if (now - s.worn_at >= 250 || s.worn.empty()) {
|
||||
s.worn = worn_items::worn_board(false);
|
||||
s.worn_at = now;
|
||||
}
|
||||
for (std::size_t part = 0; part < part_names.size(); ++part) {
|
||||
std::string_view item;
|
||||
for (const auto& w : s.worn)
|
||||
if (w.slot == worn_slots[part]) item = w.item;
|
||||
std::uintptr_t material{};
|
||||
for (const auto& m : found.materials)
|
||||
if (static_cast<std::size_t>(m.part) == part) { material = m.address; break; }
|
||||
if (!detail::settled(s.gates[part], item, material, now) || found.pending) {
|
||||
found.declarations_pending[part] = true;
|
||||
continue;
|
||||
}
|
||||
auto is_sold = [](std::string_view i) { return sold(i); };
|
||||
const auto owner = mods::item_source_owner(all->sources, item);
|
||||
|
||||
auto add = detail::declared_for(all->materials, static_cast<Part>(part), item, is_sold, owner);
|
||||
// Report a hot-reloaded declaration even when the worn item did not change.
|
||||
if (s.reported_items[part] != item || s.reported_counts[part] != add.size()) {
|
||||
s.reported_items[part] = std::string(item);
|
||||
s.reported_counts[part] = add.size();
|
||||
logging::log(logging::Level::info, logging::Channel::customization,
|
||||
"Board parameters: the {} is {}; {} declared shader input{} apply.", part_names[part], item,
|
||||
add.size(), add.size() == 1 ? "" : "s");
|
||||
}
|
||||
found.declared.insert(found.declared.end(), add.begin(), add.end());
|
||||
}
|
||||
}
|
||||
// The local player's board as it is now (a change of cosmetics can replace it); none out of play.
|
||||
std::optional<detail::Found> board(State& s) {
|
||||
if (!s.base || !s.client) return std::nullopt;
|
||||
@@ -53,7 +252,9 @@ std::optional<detail::Found> board(State& s) {
|
||||
if (!local.ready) return std::nullopt;
|
||||
const auto entity = multiplayer::read_native_board(peek, s.base, local.entity).entity;
|
||||
if (!entity) return std::nullopt;
|
||||
return detail::walk(peek, s.base, entity);
|
||||
auto found = detail::walk(peek, s.base, entity);
|
||||
declare(s, found);
|
||||
return found;
|
||||
} catch (const std::exception& error) {
|
||||
// (Once in a while: a board being built reads as unfinished for a few ticks.)
|
||||
if (const auto now = GetTickCount64(); now - s.reported > 10000) {
|
||||
@@ -69,38 +270,23 @@ std::optional<std::vector<Parameter>> read(Part part) {
|
||||
auto& s = state();
|
||||
const auto found = board(s);
|
||||
if (!found) return std::nullopt;
|
||||
std::vector<Parameter> out;
|
||||
// The first of the part's materials: another (a variant) holds the same keys.
|
||||
for (const auto& material : found->materials)
|
||||
if (material.part == part) {
|
||||
for (const auto& node : material.nodes) out.push_back(node.parameter);
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
return detail::listed(*found, part);
|
||||
}
|
||||
|
||||
std::string set(std::string_view owner, Part part, std::uint64_t key, const Colour& value, bool colour) {
|
||||
auto& s = state();
|
||||
const auto name = part_names[static_cast<std::size_t>(part)];
|
||||
const auto found = board(s);
|
||||
if (!found) return "there is no board in play";
|
||||
Kind kind{};
|
||||
if (auto why = detail::refusal(*found, part, key, value, colour, kind); !why.empty()) return why;
|
||||
if (!functions_match(s)) return "board parameters are unavailable with this game build";
|
||||
const auto held = std::find_if(s.holds.begin(), s.holds.end(), [&](const detail::Hold& h) { return h.part == part && h.key == key; });
|
||||
if (held != s.holds.end() && held->owner != owner) return std::format("{} holds the {}'s {:016x}", held->owner, name, key);
|
||||
if (held == s.holds.end() &&
|
||||
static_cast<std::size_t>(std::count_if(s.holds.begin(), s.holds.end(), [&](const detail::Hold& h) { return h.owner == owner; })) >= most_held)
|
||||
return std::format("a plugin holds at most {} board parameters", most_held);
|
||||
const Colour kept = colour ? value : Colour{value[0], 0, 0};
|
||||
if (held != s.holds.end()) held->value = kept;
|
||||
else s.holds.push_back({std::string(owner), part, key, kind, kept});
|
||||
return {};
|
||||
if (detail::declared(*found, part, key) && !detail::live(*found, part, key) && !setter_matches(s))
|
||||
return "declared colours cannot be added with this game build";
|
||||
return detail::take(s.holds, *found, owner, part, key, value, colour);
|
||||
}
|
||||
|
||||
void clear(std::string_view owner, std::optional<Part> part, std::optional<std::uint64_t> key) {
|
||||
auto& holds = state().holds;
|
||||
std::erase_if(holds, [&](const detail::Hold& h) { return h.owner == owner && (!part || h.part == *part) && (!key || h.key == *key); });
|
||||
detail::release(state().holds, owner, part, key);
|
||||
}
|
||||
|
||||
void tick(std::uintptr_t base, std::uintptr_t client) noexcept {
|
||||
@@ -111,11 +297,33 @@ void tick(std::uintptr_t base, std::uintptr_t client) noexcept {
|
||||
if (!functions_match(s)) return;
|
||||
const auto found = board(s);
|
||||
if (!found) return;
|
||||
if (const auto dropped = detail::drop_undeclared(s.holds, *found))
|
||||
logging::log(logging::Level::info, logging::Channel::customization,
|
||||
"Board parameters: {} declared value{} let go: the item that declared {} is no longer on the board.", dropped,
|
||||
dropped == 1 ? "" : "s", dropped == 1 ? "it" : "them");
|
||||
auto write = [](std::uintptr_t material, std::uintptr_t node, Kind kind, const Colour& value) {
|
||||
return write_value(material, node, value.data(), kind == Kind::colour ? sizeof(Colour) : sizeof(float));
|
||||
};
|
||||
auto publish = [&](std::uintptr_t item) { (void)publish_materials(s.base, item); };
|
||||
detail::apply(s.saved, s.holds, *found, write, publish);
|
||||
std::size_t inserted{}, refused{}, published{}, publish_failed{};
|
||||
auto publish = [&](std::uintptr_t item) {
|
||||
const bool ok = publish_materials(s.base, item);
|
||||
if (ok) ++published;
|
||||
else ++publish_failed;
|
||||
};
|
||||
auto insert = [&](std::uintptr_t material, const detail::Declared& declared, const Colour& value) {
|
||||
const bool added = setter_matches(s) && insert_colour(s.base, s.colour_type, material, declared, value);
|
||||
if (added) ++inserted;
|
||||
else ++refused;
|
||||
return added;
|
||||
};
|
||||
detail::apply(s.saved, s.holds, *found, write, publish, insert);
|
||||
// Lua accepts a hold before this tick applies it; report native failures separately.
|
||||
if ((inserted || refused || publish_failed) && GetTickCount64() - s.apply_reported >= 5000) {
|
||||
s.apply_reported = GetTickCount64();
|
||||
logging::log(logging::Level::info, logging::Channel::customization,
|
||||
"Board parameter apply: {} inserted, {} refused, {} published, {} publication failures.",
|
||||
inserted, refused, published, publish_failed);
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
} // namespace dingosdk::board_material
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
#include "developer_board_material.h"
|
||||
#include "Engine/Vfs/mod_materials.h"
|
||||
#include "Engine/Vfs/mod_item_sources.h"
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
@@ -14,6 +16,13 @@
|
||||
// integer or a switch picks how the board is drawn (the foil looks the store sells among them), so
|
||||
// those are only read. Nothing of the board's items changes and nobody else sees it: other players
|
||||
// draw this board from its cosmetics, as they are.
|
||||
//
|
||||
// A mod's own board item may also declare, in its reskate-materials.json (Engine/Vfs/mod_materials.h),
|
||||
// colours and numbers its compiled shader reads. Their defaults live in that shader, so the material
|
||||
// has no live value of them until one is set: while that item is equipped (and is not one the game
|
||||
// sells), a plugin may hold a declared colour too. The engine's typed setter adds it to the material
|
||||
// at native preset priority, as the developer board adds its colours, and when it is let go the
|
||||
// declared default is written back (a value once added is never taken out: the renderer copies it).
|
||||
namespace dingosdk::board_material {
|
||||
enum class Part : std::uint8_t { deck, grip, trucks, wheels };
|
||||
inline constexpr std::array<std::string_view, 4> part_names{"deck", "grip", "trucks", "wheels"};
|
||||
@@ -24,6 +33,7 @@ struct Parameter {
|
||||
Kind kind{Kind::other};
|
||||
Colour value{}; // a colour, or a number in [0]
|
||||
std::int32_t integer{}; // an integer, or a switch's 0 or 1
|
||||
bool inherited{}; // declared by the item's mod, with no live value yet: `value` is its default
|
||||
};
|
||||
// The two colours known by what they colour (the developer board animates them): the graphic's and
|
||||
// the base's.
|
||||
@@ -47,6 +57,16 @@ namespace detail {
|
||||
struct Node {
|
||||
std::uintptr_t address{};
|
||||
Parameter parameter;
|
||||
std::uint16_t identity{}; // its ParamDbKey identity
|
||||
};
|
||||
// A colour or a number the equipped item's mod declares for the part's material
|
||||
// (mods::MaterialDeclaration), as the board's walk is given it: only while that item is equipped.
|
||||
struct Declared {
|
||||
Part part{};
|
||||
std::uint64_t key{};
|
||||
std::uint16_t identity{};
|
||||
Kind kind{}; // colour or number
|
||||
Colour fallback{}; // the shader's default, as the mod declares it
|
||||
};
|
||||
struct Material {
|
||||
std::uintptr_t item{}, address{};
|
||||
@@ -56,6 +76,8 @@ struct Material {
|
||||
struct Found {
|
||||
std::vector<Material> materials;
|
||||
bool pending{}; // a recipe on its way: nothing is written
|
||||
std::vector<Declared> declared; // what the equipped items' mods declare (declarations below)
|
||||
std::array<bool,4> declarations_pending{};
|
||||
};
|
||||
// Each part's named material on the board's slot controllers; the deck slot holds two.
|
||||
struct Target {
|
||||
@@ -98,6 +120,7 @@ template <class Memory> std::vector<Node> parameters(Memory& m, std::uintptr_t m
|
||||
const auto type = m.ptr(node, n::parameter_type);
|
||||
Parameter p;
|
||||
p.key = key.hash;
|
||||
const auto identity = key.identity;
|
||||
if (type) {
|
||||
const auto info = m.ptr(type);
|
||||
// A type's information is in the game's image, as the board's colour descriptor is.
|
||||
@@ -113,7 +136,7 @@ template <class Memory> std::vector<Node> parameters(Memory& m, std::uintptr_t m
|
||||
} else if (p.kind == Kind::toggle) {
|
||||
p.integer = raw[0] != 0;
|
||||
}
|
||||
out.push_back({node, p});
|
||||
out.push_back({node, p, identity});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -176,14 +199,32 @@ inline const char* kind_name(Kind kind) noexcept {
|
||||
default: return "of a kind plugins do not set";
|
||||
}
|
||||
}
|
||||
inline const Declared* declared(const Found& found, Part part, std::uint64_t key) noexcept {
|
||||
for (const auto& d : found.declared)
|
||||
if (d.part == part && d.key == key) return &d;
|
||||
return nullptr;
|
||||
}
|
||||
// Whether `value` may be held on that key of the part: empty, and its kind in `kind`, else why not.
|
||||
// A key the part's material holds live is checked as it is held; one it does not hold may be a colour
|
||||
// the equipped item's mod declares. Integers and switches are never set, whatever a mod declares.
|
||||
inline std::string refusal(const Found& found, Part part, std::uint64_t key, const Colour& value, bool colour, Kind& kind) {
|
||||
const auto name = part_names[static_cast<std::size_t>(part)];
|
||||
std::optional<Kind> has;
|
||||
std::optional<std::uint16_t> identity;
|
||||
for (const auto& material : found.materials)
|
||||
if (material.part == part)
|
||||
for (const auto& node : material.nodes)
|
||||
if (node.parameter.key == key) has = node.parameter.kind;
|
||||
if (node.parameter.key == key) has = node.parameter.kind, identity = node.identity;
|
||||
const auto* mod = declared(found, part, key);
|
||||
if (has && mod && (*has != mod->kind || *identity != mod->identity))
|
||||
return std::format("the {}'s material holds {:016x} as {}, not as its mod declares it", name, key, kind_name(*has));
|
||||
if (!has && mod) {
|
||||
// Added through the engine's typed setter, which takes a colour's type (a number's is not
|
||||
// known to this build's addresses): a number has to have a live value first.
|
||||
if (mod->kind != Kind::colour)
|
||||
return std::format("{:016x} is a number the {}'s shader declares; only a declared colour can be added", key, name);
|
||||
has = mod->kind;
|
||||
}
|
||||
if (!has) return std::format("the {} has no parameter {:016x}", name, key);
|
||||
if (*has != Kind::colour && *has != Kind::number)
|
||||
return std::format("{:016x} is {}, which picks how the board is drawn; plugins only read it", key, kind_name(*has));
|
||||
@@ -201,22 +242,32 @@ struct Hold {
|
||||
std::uint64_t key{};
|
||||
Kind kind{};
|
||||
Colour value{};
|
||||
bool declared{}; // held through the equipped item's declaration: only while that declares it
|
||||
};
|
||||
struct Saved {
|
||||
std::uintptr_t material{}, node{};
|
||||
std::uintptr_t material{}, node{}; // node 0: added this tick, not yet walked
|
||||
Kind kind{};
|
||||
Colour original{}, last{};
|
||||
std::uint8_t settling{}; // publishes still owed for `last`
|
||||
std::uint64_t key{};
|
||||
};
|
||||
inline bool same(Kind kind, const Colour& a, const Colour& b) noexcept {
|
||||
return kind == Kind::colour ? a == b : a[0] == b[0];
|
||||
}
|
||||
// A held key still holds: a declared one only while the equipped item declares it, as it was.
|
||||
inline bool holds_now(const Hold& hold, const Found& live, const Material& material, const Node* node) noexcept {
|
||||
if (!hold.declared) return true;
|
||||
const auto* d = declared(live, material.part, hold.key);
|
||||
return d && d->kind == hold.kind && (!node || node->identity == d->identity);
|
||||
}
|
||||
// One tick, as the developer board's colours go (developer_hoodie_detail::animate): what is held
|
||||
// written where it is not yet, and published until it shows (the renderer shows a value one publish
|
||||
// late); what is no longer held put back while it is still ours, a value changed by the game since
|
||||
// left as the game has it. write(material, node, kind, value) -> written; publish(item).
|
||||
template <class Write, class Publish>
|
||||
void apply(std::vector<Saved>& saved, const std::vector<Hold>& holds, const Found& live, Write& write, Publish& publish) {
|
||||
// left as the game has it. A declared colour the material does not hold yet is added
|
||||
// (insert(material, declared, value) -> added), and is put back to its declared default when let go.
|
||||
// write(material, node, kind, value) -> written; publish(item).
|
||||
template <class Write, class Publish, class Insert>
|
||||
void apply(std::vector<Saved>& saved, const std::vector<Hold>& holds, const Found& live, Write& write, Publish& publish, Insert& insert) {
|
||||
if (live.pending) return;
|
||||
using developer_hoodie_detail::settle_publishes;
|
||||
std::vector<Saved> next;
|
||||
@@ -227,10 +278,10 @@ void apply(std::vector<Saved>& saved, const std::vector<Hold>& holds, const Foun
|
||||
const auto& p = node.parameter;
|
||||
if (p.kind != Kind::colour && p.kind != Kind::number) continue;
|
||||
const auto hold = std::find_if(holds.begin(), holds.end(), [&](const Hold& h) {
|
||||
return h.part == material.part && h.key == p.key && h.kind == p.kind;
|
||||
return h.part == material.part && h.key == p.key && h.kind == p.kind && holds_now(h, live, material, &node);
|
||||
});
|
||||
const auto at = std::find_if(saved.begin(), saved.end(), [&](const Saved& s) {
|
||||
return s.material == material.address && s.node == node.address && s.kind == p.kind;
|
||||
return s.material == material.address && s.key == p.key && s.kind == p.kind;
|
||||
});
|
||||
const Saved* was = at == saved.end() ? nullptr : &*at;
|
||||
const bool ours = was && same(p.kind, p.value, was->last);
|
||||
@@ -238,28 +289,149 @@ void apply(std::vector<Saved>& saved, const std::vector<Hold>& holds, const Foun
|
||||
if (hold != holds.end()) {
|
||||
if (same(p.kind, p.value, hold->value)) {
|
||||
const std::uint8_t left = ours ? was->settling : 0;
|
||||
next.push_back({material.address, node.address, p.kind, original, hold->value, static_cast<std::uint8_t>(left ? left - 1 : 0)});
|
||||
next.push_back({material.address, node.address, p.kind, original, hold->value, static_cast<std::uint8_t>(left ? left - 1 : 0), p.key});
|
||||
show = show || left != 0;
|
||||
} else if (write(material.address, node.address, p.kind, hold->value)) {
|
||||
next.push_back({material.address, node.address, p.kind, original, hold->value, settle_publishes});
|
||||
next.push_back({material.address, node.address, p.kind, original, hold->value, settle_publishes, p.key});
|
||||
show = true;
|
||||
} else if (was) {
|
||||
next.push_back(*was);
|
||||
}
|
||||
} else if (was && (ours || same(p.kind, p.value, was->original))) {
|
||||
// Back to the board's own value, and published until it shows.
|
||||
// Back to the board's own value (a declared colour's default, for one added), and
|
||||
// published until it shows.
|
||||
const bool first = !same(p.kind, was->last, was->original);
|
||||
const bool back = same(p.kind, p.value, was->original) || write(material.address, node.address, p.kind, was->original);
|
||||
const std::uint8_t left = first ? settle_publishes : was->settling ? was->settling - 1 : 0;
|
||||
if (!back) next.push_back(*was);
|
||||
else if (left) next.push_back({material.address, node.address, p.kind, was->original, was->original, left});
|
||||
else if (left) next.push_back({material.address, node.address, p.kind, was->original, was->original, left, p.key});
|
||||
show = show || back;
|
||||
}
|
||||
}
|
||||
// A declared colour this material does not hold yet: added, with its declared default as
|
||||
// what it goes back to.
|
||||
for (const auto& hold : holds) {
|
||||
if (!hold.declared || hold.part != material.part || hold.kind != Kind::colour) continue;
|
||||
if (std::any_of(material.nodes.begin(), material.nodes.end(), [&](const Node& n) { return n.parameter.key == hold.key; })) continue;
|
||||
const auto* d = declared(live, material.part, hold.key);
|
||||
if (!d || d->kind != Kind::colour || !holds_now(hold, live, material, nullptr)) continue;
|
||||
if (insert(material.address, *d, hold.value)) {
|
||||
next.push_back({material.address, 0, Kind::colour, d->fallback, hold.value, settle_publishes, hold.key});
|
||||
show = true;
|
||||
}
|
||||
}
|
||||
if (show && std::find(shown.begin(), shown.end(), material.item) == shown.end()) shown.push_back(material.item);
|
||||
}
|
||||
saved = std::move(next);
|
||||
for (const auto item : shown) publish(item);
|
||||
}
|
||||
template <class Write, class Publish>
|
||||
void apply(std::vector<Saved>& saved, const std::vector<Hold>& holds, const Found& live, Write& write, Publish& publish) {
|
||||
auto none = [](std::uintptr_t, const Declared&, const Colour&) { return false; };
|
||||
apply(saved, holds, live, write, publish, none);
|
||||
}
|
||||
|
||||
// ---- declarations ---------------------------------------------------------------------------------
|
||||
template<class Items, class Reserved>
|
||||
bool declared_item_restricted(std::string_view item, bool available, const Items& items, Reserved&& reserved) {
|
||||
const auto key = mods::item_key(item);
|
||||
return !available || items.contains(key) || key.starts_with("dev_") || key.starts_with("own_dev_") || reserved(item);
|
||||
}
|
||||
// The declarations of the item equipped on `part` (its key, as the game names it; any case), from
|
||||
// the owning enabled mod's reskate-materials.json. Other mods cannot grant or suppress keys.
|
||||
// A key two declarations from the owner give differently is left out:
|
||||
// which shader is right cannot be told. `sold` says whether the game sells an item (or cannot tell
|
||||
// yet): its declarations never apply.
|
||||
template <class Sold>
|
||||
std::vector<Declared> declared_for(const std::vector<mods::MaterialDeclaration>& declarations, Part part, std::string_view item, Sold& sold, std::string_view owner) {
|
||||
std::vector<Declared> out;
|
||||
if (item.empty() || owner.empty() || sold(item)) return out;
|
||||
const auto wanted = mods::item_key(item);
|
||||
std::vector<std::uint64_t> conflicting;
|
||||
for (const auto& d : declarations) {
|
||||
if (d.mod != owner || d.part != part_names[static_cast<std::size_t>(part)]) continue;
|
||||
if (std::none_of(d.items.begin(), d.items.end(), [&](const std::string& i) { return mods::item_key(i) == wanted; })) continue;
|
||||
for (const auto& p : d.parameters) {
|
||||
const Declared next{part, p.key, p.identity, p.colour ? Kind::colour : Kind::number, p.fallback};
|
||||
const auto at = std::find_if(out.begin(), out.end(), [&](const Declared& o) { return o.key == p.key; });
|
||||
if (at == out.end()) out.push_back(next);
|
||||
else if (at->identity != next.identity || at->kind != next.kind || at->fallback != next.fallback) conflicting.push_back(p.key);
|
||||
}
|
||||
}
|
||||
std::erase_if(out, [&](const Declared& d) { return std::find(conflicting.begin(), conflicting.end(), d.key) != conflicting.end(); });
|
||||
return out;
|
||||
}
|
||||
// When an item's declarations apply: the same item has been equipped, on the same material, for
|
||||
// `settle_ms`. A change of cosmetics replaces the board over a few ticks, during which the item read
|
||||
// and the board walked can disagree; nothing declared is added to a board until they have settled.
|
||||
inline constexpr std::uint64_t settle_ms = 1000;
|
||||
struct Gate {
|
||||
std::string item;
|
||||
std::uintptr_t material{};
|
||||
std::uint64_t since{};
|
||||
};
|
||||
inline bool settled(Gate& gate, std::string_view item, std::uintptr_t material, std::uint64_t now) {
|
||||
if (gate.item != item || gate.material != material || !material) {
|
||||
gate = {std::string(item), material, now};
|
||||
return false;
|
||||
}
|
||||
return now - gate.since >= settle_ms;
|
||||
}
|
||||
// Holds through a declaration the equipped item no longer makes (another deck, or one being put
|
||||
// on) are let go: what they added goes back to its default, and the plugin has to set it again.
|
||||
// Returns how many were let go.
|
||||
inline std::size_t drop_undeclared(std::vector<Hold>& holds, const Found& live) {
|
||||
if (live.pending) return 0;
|
||||
return std::erase_if(holds, [&](const Hold& h) {
|
||||
if (!h.declared || live.declarations_pending[static_cast<std::size_t>(h.part)]) return false;
|
||||
const auto* d = declared(live, h.part, h.key);
|
||||
return !d || d->kind != h.kind;
|
||||
});
|
||||
}
|
||||
|
||||
// ---- what plugins see and hold, apart from the game (read, set and clear) ------------------------------
|
||||
inline bool live(const Found& found, Part part, std::uint64_t key) noexcept {
|
||||
for (const auto& material : found.materials)
|
||||
if (material.part == part)
|
||||
for (const auto& node : material.nodes)
|
||||
if (node.parameter.key == key) return true;
|
||||
return false;
|
||||
}
|
||||
// The part's parameters: its first material's (another, a variant, holds the same keys), then what
|
||||
// the equipped item's mod declares that the material does not hold yet, at its default.
|
||||
inline std::vector<Parameter> listed(const Found& found, Part part) {
|
||||
std::vector<Parameter> out;
|
||||
for (const auto& material : found.materials)
|
||||
if (material.part == part) {
|
||||
for (const auto& node : material.nodes) out.push_back(node.parameter);
|
||||
break;
|
||||
}
|
||||
for (const auto& d : found.declared)
|
||||
if (d.part == part && std::none_of(out.begin(), out.end(), [&](const Parameter& p) { return p.key == d.key; }))
|
||||
out.push_back({d.key, d.kind, d.fallback, 0, true});
|
||||
return out;
|
||||
}
|
||||
// `owner` holds `value` on that key from now on: empty, else why not. A key is one plugin's at a
|
||||
// time, and a plugin holds at most `most_held`.
|
||||
inline std::string take(std::vector<Hold>& holds, const Found& found, std::string_view owner, Part part, std::uint64_t key,
|
||||
const Colour& value, bool colour) {
|
||||
const auto name = part_names[static_cast<std::size_t>(part)];
|
||||
Kind kind{};
|
||||
if (auto why = refusal(found, part, key, value, colour, kind); !why.empty()) return why;
|
||||
const bool by_declaration = declared(found, part, key) != nullptr;
|
||||
const auto held = std::find_if(holds.begin(), holds.end(), [&](const Hold& h) { return h.part == part && h.key == key; });
|
||||
if (held != holds.end() && held->owner != owner) return std::format("{} holds the {}'s {:016x}", held->owner, name, key);
|
||||
if (held == holds.end() &&
|
||||
static_cast<std::size_t>(std::count_if(holds.begin(), holds.end(), [&](const Hold& h) { return h.owner == owner; })) >= most_held)
|
||||
return std::format("a plugin holds at most {} board parameters", most_held);
|
||||
const Colour kept = colour ? value : Colour{value[0], 0, 0};
|
||||
if (held != holds.end()) held->value = kept, held->declared = by_declaration;
|
||||
else holds.push_back({std::string(owner), part, key, kind, kept, by_declaration});
|
||||
return {};
|
||||
}
|
||||
// What `owner` holds of that part and key (all its parts, all its keys) let go: apply() puts it back.
|
||||
inline void release(std::vector<Hold>& holds, std::string_view owner, std::optional<Part> part, std::optional<std::uint64_t> key) {
|
||||
std::erase_if(holds, [&](const Hold& h) { return h.owner == owner && (!part || h.part == *part) && (!key || h.key == *key); });
|
||||
}
|
||||
} // namespace detail
|
||||
} // namespace dingosdk::board_material
|
||||
@@ -47,7 +47,7 @@ std::int32_t selected_preset(std::uintptr_t base, std::uintptr_t manager) {
|
||||
// The board the skater rides now: the board recipe of the selected preset (the first when none is
|
||||
// selected), its parts in the card's order, empty slots left out. Game thread.
|
||||
} // namespace
|
||||
std::vector<Worn> worn_board() {
|
||||
std::vector<Worn> worn_board(bool titles) {
|
||||
std::vector<Worn> parts;
|
||||
const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
|
||||
std::uintptr_t manager{}, begin{}, end{};
|
||||
@@ -66,7 +66,7 @@ std::vector<Worn> worn_board() {
|
||||
for (const auto& wanted : board_slots)
|
||||
for (const auto& item : recipe.items) {
|
||||
if (item.slot != wanted.slot || item.asset.empty()) continue;
|
||||
parts.push_back({wanted.label, item.asset, item_title(item.asset)});
|
||||
parts.push_back({wanted.label, item.asset, titles ? item_title(item.asset) : std::string{}});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ struct Worn {
|
||||
std::string title; // as the game shows it
|
||||
};
|
||||
std::vector<Worn> worn_outfit();
|
||||
std::vector<Worn> worn_board();
|
||||
// `titles` off leaves each title empty: what the board's material declarations need is only the
|
||||
// items' keys, read every quarter second (board_material.cpp).
|
||||
std::vector<Worn> worn_board(bool titles = true);
|
||||
// Its catalog title, the title an item grid showed this session (a mod's item has no catalog
|
||||
// entry), or its key made readable the way the game does for a mod's item.
|
||||
std::string item_title(const std::string& asset);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Extension/Throwdowns/custom_modes.h"
|
||||
#include "Extension/Throwdowns/throwdown_kit.h"
|
||||
#include "Engine/Vfs/mod_list.h"
|
||||
#include "Extension/Customization/board_material.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <format>
|
||||
@@ -22,6 +23,8 @@
|
||||
|
||||
using namespace dingosdk::plugins;
|
||||
namespace plugin_ui = dingosdk::plugin_ui;
|
||||
namespace board_material = dingosdk::board_material;
|
||||
namespace mods = dingosdk::mods;
|
||||
namespace {
|
||||
void check(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); }
|
||||
|
||||
@@ -73,7 +76,41 @@ struct FakeGame final : Game {
|
||||
return {};
|
||||
}
|
||||
void board_park(std::string_view plugin, std::vector<int> bones) override { calls.push_back(std::format("board park {} {}", plugin, bones.size())); }
|
||||
// The Example declared-input deck as the game holds it: seven live selectors, and the eight colours (and a
|
||||
// number) its mod declares for its shader. What plugins hold of it is kept by the same rules as
|
||||
// the game's (board_material.h: take, listed, release). Off: no deck declares anything.
|
||||
bool declared_rules{}; // the deck answers as the Example deck (else as before: everything is taken)
|
||||
bool declared_deck{}; // with declared_rules: the Example deck is equipped (else a deck that declares nothing)
|
||||
std::vector<board_material::detail::Hold> declared_holds;
|
||||
static constexpr std::array<std::uint64_t, 8> declared_keys{
|
||||
0x5b2d035c7746f31aULL, 0x51745c0fd2d117d3ULL, 0x5c1058b72fe158edULL, 0x5a4e08ac201c369fULL,
|
||||
0x57fc15cf6583c0c7ULL, 0x5a2f52ddbd12f283ULL, 0x55fda1e1521b9097ULL, 0x580bde15aa576f12ULL};
|
||||
static constexpr std::uint64_t declared_number = 0x7777777777777777ULL;
|
||||
board_material::detail::Found declared_board() const {
|
||||
using namespace board_material;
|
||||
detail::Found found;
|
||||
detail::Material deck{0x100, 0x200, Part::deck, {}};
|
||||
for (std::uint64_t k = 1; k <= 7; ++k)
|
||||
deck.nodes.push_back({0x300 + k, {0x5000 + k, k % 2 ? Kind::integer : Kind::toggle, {}, static_cast<std::int32_t>(k)}, 0x1234});
|
||||
found.materials.push_back(deck);
|
||||
mods::MaterialDeclaration declaration{"Example", "deck", {"Example_Deck"}, {}};
|
||||
for (const auto key : declared_keys) declaration.parameters.push_back({key, static_cast<std::uint16_t>(key), true, {.5f, .5f, .5f}});
|
||||
declaration.parameters.push_back({declared_number, 0x4242, false, {.25f, 0, 0}});
|
||||
auto never_sold = [](std::string_view) { return false; };
|
||||
found.declared = detail::declared_for({declaration}, Part::deck, declared_deck ? "Example_Deck" : "Plain_Deck", never_sold, "Example");
|
||||
return found;
|
||||
}
|
||||
bool declared_holds_deck(int part) const { return declared_rules && part == 0; }
|
||||
std::optional<std::vector<BoardParameter>> board_parameters(int part) override {
|
||||
if (declared_holds_deck(part)) {
|
||||
std::vector<BoardParameter> out;
|
||||
for (const auto& p : board_material::detail::listed(declared_board(), board_material::Part::deck)) {
|
||||
using board_material::Kind;
|
||||
out.push_back({p.key, p.kind == Kind::colour ? "colour" : p.kind == Kind::number ? "number" : p.kind == Kind::integer ? "integer" : "switch",
|
||||
p.value, p.integer, p.inherited});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (part != 1) return std::vector<BoardParameter>{};
|
||||
return std::vector<BoardParameter>{{0x873f48af36e50d59ULL, "colour", {.2f, .3f, .4f}, 0},
|
||||
{0x841c7d74837f1d39ULL, "number", {1, 0, 0}, 0},
|
||||
@@ -81,11 +118,20 @@ struct FakeGame final : Game {
|
||||
{0xa5f9a6b8098279adULL, "switch", {}, 1}};
|
||||
}
|
||||
std::string board_set_parameter(std::string_view plugin, int part, std::uint64_t key, std::array<float, 3> value, bool colour) override {
|
||||
if (declared_holds_deck(part)) {
|
||||
auto why = board_material::detail::take(declared_holds, declared_board(), plugin, board_material::Part::deck, key, value, colour);
|
||||
if (why.empty()) calls.push_back(std::format("board parameter {} {} {:016x} {} {:.1f}", plugin, part, key, colour ? "colour" : "number", value[0]));
|
||||
return why;
|
||||
}
|
||||
calls.push_back(std::format("board parameter {} {} {:016x} {} {:.1f}", plugin, part, key, colour ? "colour" : "number", value[0]));
|
||||
return {};
|
||||
}
|
||||
void board_clear_parameter(std::string_view plugin, int part, std::optional<std::uint64_t> key) override {
|
||||
calls.push_back(std::format("board parameter clear {} {} {}", plugin, part, key ? std::format("{:016x}", *key) : "all"));
|
||||
board_material::detail::release(declared_holds, plugin, part < 0 ? std::nullopt : std::optional(static_cast<board_material::Part>(part)), key);
|
||||
}
|
||||
std::size_t declared_held(std::string_view plugin) const {
|
||||
return static_cast<std::size_t>(std::count_if(declared_holds.begin(), declared_holds.end(), [&](const auto& h) { return h.owner == plugin; }));
|
||||
}
|
||||
// The general services a game layer in Lua reads and drives (plugin_api_services.cpp).
|
||||
std::optional<SkaterTelemetry> telemetry;
|
||||
@@ -640,7 +686,11 @@ struct FakeGame final : Game {
|
||||
return {};
|
||||
}
|
||||
std::vector<std::pair<std::string, std::string>> take_presses() override { return std::exchange(presses, {}); }
|
||||
void release(std::string_view plugin) override { released.emplace_back(plugin); entity_clear(plugin); }
|
||||
void release(std::string_view plugin) override {
|
||||
released.emplace_back(plugin);
|
||||
entity_clear(plugin);
|
||||
board_material::detail::release(declared_holds, plugin, std::nullopt, std::nullopt);
|
||||
}
|
||||
// The game menu, its item grid, what is worn, and a screen capture of something turning once
|
||||
// every `turning` frames (reskate.capture, room.turn, menu.game/grid, outfit.board).
|
||||
GameMenu menu_now;
|
||||
@@ -2469,6 +2519,76 @@ void web_requests_go_only_to_listed_domains() {
|
||||
check(environment.logged("many more than 4 web requests waiting"), "4 waiting at most\n" + environment.all());
|
||||
}
|
||||
|
||||
|
||||
void declared_board_colours_through_the_api() {
|
||||
TestEnvironment environment;
|
||||
environment.fake.declared_rules = true;
|
||||
environment.fake.declared_deck = true;
|
||||
Host host(environment, Side::client);
|
||||
host.apply({script("materialapi", R"(
|
||||
reskate.console.command("materialapi", "x", function(args)
|
||||
local deck = reskate.board.parameters("deck")
|
||||
assert(#deck == 7 + 9, "seven selectors and nine declared inputs, not " .. #deck)
|
||||
local inherited = 0
|
||||
for i, p in deck do
|
||||
if i <= 7 then
|
||||
assert(not p.inherited and (p.type == "integer" or p.type == "switch"), "a live selector")
|
||||
else
|
||||
assert(p.inherited == true, "a declared input is inherited")
|
||||
if p.type == "colour" then
|
||||
inherited += 1
|
||||
assert(p.value.r == 0.5 and p.value.g == 0.5 and p.value.b == 0.5, "at its declared default")
|
||||
end
|
||||
end
|
||||
end
|
||||
assert(inherited == 8, "eight declared colours")
|
||||
for _, key in {"5b2d035c7746f31a", "51745c0fd2d117d3", "5c1058b72fe158ed", "5a4e08ac201c369f",
|
||||
"57fc15cf6583c0c7", "5a2f52ddbd12f283", "55fda1e1521b9097", "580bde15aa576f12"} do
|
||||
assert(reskate.board.set_parameter("deck", key, {r = 0.25, g = 0.75, b = 0.375}), key)
|
||||
end
|
||||
local ok, why = reskate.board.set_parameter("deck", "0000000000005001", 3)
|
||||
assert(not ok and why:find("only read"), "a selector stays read-only: " .. tostring(why))
|
||||
ok, why = reskate.board.set_parameter("deck", "5b2d035c7746f31a", 0.5)
|
||||
assert(not ok and why:find("colour"), "a declared colour is not a number: " .. tostring(why))
|
||||
ok, why = reskate.board.set_parameter("deck", "5b2d035c7746f31a", {r = 2, g = 0, b = 0})
|
||||
assert(not ok and why:find("0 to 1"), "its channels are 0 to 1")
|
||||
ok, why = reskate.board.set_parameter("deck", "7777777777777777", 0.5)
|
||||
assert(not ok and why:find("only a declared colour"), "a declared number with no live value is not added: " .. tostring(why))
|
||||
ok, why = reskate.board.set_parameter("deck", "1234567812345678", {r = 0, g = 0, b = 0})
|
||||
assert(not ok and why:find("no parameter"), "nor anything undeclared")
|
||||
reskate.log("materialapi fine")
|
||||
end)
|
||||
reskate.console.command("materialapi_one", "x", function() reskate.board.clear_parameter("deck", "5b2d035c7746f31a") end)
|
||||
)", R"("board", "console")"),
|
||||
script("thief", R"(
|
||||
reskate.console.command("thief", "x", function()
|
||||
local ok, why = reskate.board.set_parameter("deck", "5b2d035c7746f31a", {r = 1, g = 1, b = 1})
|
||||
assert(not ok and why:find("materialapi holds"), "another plugin's colour is not taken: " .. tostring(why))
|
||||
reskate.log("thief refused")
|
||||
end)
|
||||
)", R"("board", "console")"),
|
||||
script("nopermission", R"(
|
||||
local ok, err = pcall(reskate.board.parameters, "deck")
|
||||
assert(not ok and err:find("needs the \"board\" permission"), err)
|
||||
ok, err = pcall(reskate.board.set_parameter, "deck", "5b2d035c7746f31a", {r = 1, g = 1, b = 1})
|
||||
assert(not ok and err:find("needs the \"board\" permission"), err)
|
||||
ok, err = pcall(reskate.board.clear_parameter, "deck")
|
||||
assert(not ok and err:find("needs the \"board\" permission"), err)
|
||||
reskate.log("nopermission refused")
|
||||
)")});
|
||||
running(host, "materialapi", environment);
|
||||
running(host, "nopermission", environment);
|
||||
check(environment.logged("nopermission refused"), "A plugin without the board permission reaches no board parameter\n" + environment.all());
|
||||
check(environment.fake.declared_holds.empty(), "Nothing is held before a plugin asks");
|
||||
check(host.run_console("materialapi") && environment.logged("materialapi fine"), "Declared colours list and set\n" + environment.all());
|
||||
check(environment.fake.declared_held("materialapi") == 8, "The plugin holds the eight declared colours");
|
||||
check(host.run_console("thief") && environment.logged("thief refused"), "Another plugin cannot take them\n" + environment.all());
|
||||
check(environment.fake.declared_held("thief") == 0, "and holds none");
|
||||
check(host.run_console("materialapi_one") && environment.fake.declared_held("materialapi") == 7, "Clearing one key lets go of that one");
|
||||
host.apply({});
|
||||
check(environment.fake.declared_holds.empty(), "Unloading lets go of every one (the game puts them back to their defaults)");
|
||||
}
|
||||
|
||||
void the_wider_set_reaches_the_game() {
|
||||
TestEnvironment environment;
|
||||
auto& game = environment.fake;
|
||||
@@ -4163,6 +4283,7 @@ int main() {
|
||||
run(plugins_call_and_hear_each_other);
|
||||
run(web_requests_go_only_to_listed_domains);
|
||||
run(the_wider_set_reaches_the_game);
|
||||
run(declared_board_colours_through_the_api);
|
||||
run(key_names_become_binds);
|
||||
run(level_events_reach_plugins);
|
||||
run(timers_run_when_due);
|
||||
|
||||
@@ -1830,7 +1830,9 @@ struct WorldApi {
|
||||
}
|
||||
// parameters(part): {{key = "873f48af36e50d59", name = "graphic", type = "colour", value = {r, g, b}}, ...}
|
||||
// as the part's material holds them now, or nil without a board. A number's value is a number, an
|
||||
// integer's an integer, a switch's true or false.
|
||||
// integer's an integer, a switch's true or false. A colour or a number the equipped item's mod
|
||||
// declares (reskate-materials.json) that the material does not hold yet comes with inherited =
|
||||
// true and its shader's default as its value; set_parameter adds a declared colour.
|
||||
static int board_parameters(lua_State* L) {
|
||||
auto& plugin = plugin_of(L);
|
||||
need(L, plugin, "board", "reskate.board.parameters");
|
||||
@@ -1871,12 +1873,18 @@ struct WorldApi {
|
||||
lua_pushboolean(L, p.integer != 0);
|
||||
lua_setfield(L, -2, "value");
|
||||
}
|
||||
if (p.inherited) {
|
||||
lua_pushboolean(L, 1);
|
||||
lua_setfield(L, -2, "inherited");
|
||||
}
|
||||
lua_rawseti(L, -2, ++at);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
// set_parameter(part, key, value): a colour {r, g, b} (0 to 1) or a number held on the part until
|
||||
// cleared, on this screen; an integer or a switch is only read.
|
||||
// cleared, on this screen; an integer or a switch is only read. A colour the equipped item's mod
|
||||
// declares is added to the material and goes back to its declared default when let go, or when
|
||||
// that item is no longer on the board.
|
||||
static int board_set_parameter(lua_State* L) {
|
||||
auto& plugin = plugin_of(L);
|
||||
need(L, plugin, "board", "reskate.board.set_parameter");
|
||||
|
||||
@@ -832,6 +832,9 @@ public:
|
||||
std::string type;
|
||||
std::array<float, 3> value{};
|
||||
std::int32_t integer{};
|
||||
// Declared by the equipped item's mod (reskate-materials.json) and not held by the material
|
||||
// yet: `value` is the default its shader has; set_parameter adds it.
|
||||
bool inherited{};
|
||||
};
|
||||
virtual std::optional<std::vector<BoardParameter>> board_parameters(int /*part*/) { return std::nullopt; }
|
||||
// A colour (`colour`, all three) or a number (value[0]) held there until cleared; empty when held.
|
||||
|
||||
@@ -528,7 +528,7 @@ public:
|
||||
using board_material::Kind;
|
||||
const char* type = p.kind == Kind::colour ? "colour" : p.kind == Kind::number ? "number" : p.kind == Kind::integer ? "integer"
|
||||
: p.kind == Kind::toggle ? "switch" : "other";
|
||||
out.push_back({p.key, type, p.value, p.integer});
|
||||
out.push_back({p.key, type, p.value, p.integer, p.inherited});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32)
|
||||
add_executable(dingosdk_variant_additions_plan_tests Engine/Vfs/Test/variant_additions_plan_tests.cpp)
|
||||
target_include_directories(dingosdk_variant_additions_plan_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME variant_additions_plan COMMAND dingosdk_variant_additions_plan_tests)
|
||||
add_executable(dingosdk_material_declaration_refresh_tests Engine/Vfs/Test/material_declaration_refresh_tests.cpp)
|
||||
target_link_libraries(dingosdk_material_declaration_refresh_tests PRIVATE dingosdk_mods)
|
||||
target_include_directories(dingosdk_material_declaration_refresh_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME material_declaration_refresh COMMAND dingosdk_material_declaration_refresh_tests)
|
||||
add_executable(dingosdk_bundle_ref_table_tests Engine/Vfs/Test/bundle_ref_table_tests.cpp)
|
||||
target_link_libraries(dingosdk_bundle_ref_table_tests PRIVATE dingosdk_mods)
|
||||
add_test(NAME bundle_ref_table COMMAND dingosdk_bundle_ref_table_tests)
|
||||
@@ -111,6 +115,11 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32)
|
||||
target_link_libraries(dingosdk_mod_music_tests PRIVATE dingosdk_mods)
|
||||
target_include_directories(dingosdk_mod_music_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME mod_music COMMAND dingosdk_mod_music_tests)
|
||||
# The board shader inputs a mod declares for its own items (Engine/Vfs/mod_materials.h).
|
||||
add_executable(dingosdk_mod_materials_tests Engine/Vfs/Test/mod_materials_tests.cpp)
|
||||
target_link_libraries(dingosdk_mod_materials_tests PRIVATE dingosdk_mods)
|
||||
target_include_directories(dingosdk_mod_materials_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME mod_materials COMMAND dingosdk_mod_materials_tests)
|
||||
add_executable(dingosdk_music_safety_tests Extension/Music/Test/local_music_safety_tests.cpp)
|
||||
target_include_directories(dingosdk_music_safety_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
dingosdk_configure_target(dingosdk_music_safety_tests)
|
||||
|
||||
@@ -109,6 +109,7 @@ if(WIN32)
|
||||
Engine/Vfs/mod_merge_grid.cpp
|
||||
Engine/Vfs/mod_merge_load_screens.cpp
|
||||
Engine/Vfs/mod_music.cpp
|
||||
Engine/Vfs/mod_materials.cpp
|
||||
Engine/Vfs/mod_scoring.cpp
|
||||
Engine/Vfs/mod_store_copies.cpp
|
||||
Engine/Vfs/mod_item_sources.cpp
|
||||
|
||||
Reference in new issue
Block a user