diff --git a/Engine/Vfs/Test/mod_materials_tests.cpp b/Engine/Vfs/Test/mod_materials_tests.cpp index 14d645ea..6ebeac9b 100644 --- a/Engine/Vfs/Test/mod_materials_tests.cpp +++ b/Engine/Vfs/Test/mod_materials_tests.cpp @@ -58,7 +58,7 @@ int main() try { "parameters": [ {"guid": "1af346775c032d5b59ff5e88010069b6", "type": "colour", "default": [0.5, 0.5, 0.5]}, {"guid": "126f57aa15de0b5859ff5e8801003944", "type": "color", "default": [0, 0, 0]}, - {"guid": "d317d1d20f5c745159ff5e880100dcb0", "type": "number", "default": -2.5} + {"guid": "d317d1d20f5c74512d5cb62b0100dcb0", "type": "number", "default": -2.5} ] }] })"); @@ -109,6 +109,9 @@ int main() try { "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"); diff --git a/Engine/Vfs/mod_materials.cpp b/Engine/Vfs/mod_materials.cpp index 6cda8622..adb374c8 100644 --- a/Engine/Vfs/mod_materials.cpp +++ b/Engine/Vfs/mod_materials.cpp @@ -36,6 +36,12 @@ MaterialParameter parameter(const Json& entry) { 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(c>='A'&&c<='F'?c+32: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]"); @@ -72,7 +78,7 @@ bool material_parameter_key(const std::string& guid, std::uint64_t& key, std::ui std::vector 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() || root.at("schema").get() != 1 || + if (!root.is_object() || !root.contains("schema") || !root.at("schema").is_number_integer() || root.at("schema").get() != 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"); diff --git a/Extension/Customization/Test/board_material_tests.cpp b/Extension/Customization/Test/board_material_tests.cpp index f7d7269f..a3e044ba 100644 --- a/Extension/Customization/Test/board_material_tests.cpp +++ b/Extension/Customization/Test/board_material_tests.cpp @@ -294,28 +294,30 @@ std::vector declared_declarations() { 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); + 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); + 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).empty(), "another deck's do not"); - check(detail::declared_for(all, Part::grip, "Example_Deck", never_sold).empty(), "nor another part's"); - check(detail::declared_for(all, Part::deck, "", never_sold).empty(), "nor an empty slot's"); + 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).empty(), "An item the game sells (or one it cannot tell) takes none"); + 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}}}}); - const auto merged = detail::declared_for(both, Part::deck, "Example_Deck", never_sold); - check(std::none_of(merged.begin(), merged.end(), [](const detail::Declared& d) { return d.key == liquid[0].key; }) && + 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, - "A key two mods declare differently is left out; one they agree on is kept once"); + "Another mod cannot suppress the owner's declarations"); } void lists_and_refuses_declared() { @@ -461,6 +463,8 @@ void a_change_of_deck_lets_go() { 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. diff --git a/Extension/Customization/board_material.cpp b/Extension/Customization/board_material.cpp index 47803868..a0543f92 100644 --- a/Extension/Customization/board_material.cpp +++ b/Extension/Customization/board_material.cpp @@ -6,6 +6,7 @@ #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 "Extension/Multiplayer/Remote/native_pose_layout.h" #include "Extension/Multiplayer/Remote/native_skater.h" #include @@ -104,10 +105,13 @@ bool setter_matches(State& s) { // ---- 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. +struct DeclarationSet { + std::vector materials; + std::vector sources; +}; struct Declarations { std::mutex mutex; - std::shared_ptr> ready; - std::vector stamps; + std::shared_ptr ready; bool reading{}; std::uint64_t looked{}; }; @@ -115,18 +119,11 @@ Declarations& declarations() { static auto* value = new Declarations; return *value; } -std::vector declaration_stamps() { - std::vector stamps; - for (const auto& mod : mods::catalog().mods) { - std::error_code error; - const auto stamp = std::filesystem::last_write_time(mod.directory / L"reskate-materials.json", error); - stamps.push_back(error ? std::filesystem::file_time_type::min() : stamp); - } - return stamps; -} -std::vector read_declarations() { - std::vector result; - for (const auto& mod : mods::catalog().mods) { +DeclarationSet read_declarations() { + DeclarationSet result; + const auto active=mods::load_catalog(mods::catalog().data_root,{},false); + result.sources=mods::read_item_sources(active.root/L".reskate"/mods::item_sources_file); + for (const auto& mod : active.mods) { const auto path = mod.directory / L"reskate-materials.json"; std::error_code error; if (!std::filesystem::is_regular_file(path, error) || std::filesystem::file_size(path, error) > 64 * 1024) continue; @@ -137,7 +134,7 @@ std::vector read_declarations() { for (const auto& d : read) count += d.parameters.size(); logging::log(logging::Level::info, logging::Channel::customization, "Board parameters: {} declares {} shader input{} for its items.", mod.name, count, count == 1 ? "" : "s"); - std::move(read.begin(), read.end(), std::back_inserter(result)); + std::move(read.begin(), read.end(), std::back_inserter(result.materials)); } catch (const std::exception& failure) { logging::log(logging::Level::warning, logging::Channel::customization, "Board parameters: {}'s reskate-materials.json is left out: {}", mod.name, failure.what()); @@ -147,28 +144,20 @@ std::vector read_declarations() { } void read_declarations_now() noexcept { auto& d = declarations(); - std::shared_ptr> read; - std::vector stamps; + std::shared_ptr read; try { - stamps = declaration_stamps(); - bool same{}; - { - std::lock_guard lock(d.mutex); - same = d.ready && stamps == d.stamps; - } - if (!same) read = std::make_shared>(read_declarations()); + read = std::make_shared(read_declarations()); } catch (...) {} std::lock_guard lock(d.mutex); if (read) { d.ready = std::move(read); - d.stamps = std::move(stamps); } else if (!d.ready) { - d.ready = std::make_shared>(); + d.ready = std::make_shared(); } d.reading = false; } // What was last read, and a read started when the last began two seconds ago or more. -std::shared_ptr> current_declarations() { +std::shared_ptr current_declarations() { auto& d = declarations(); std::lock_guard lock(d.mutex); const auto now = GetTickCount64(); @@ -183,10 +172,12 @@ std::shared_ptr> current_declaratio } return d.ready; } -// Whether the game sells an item, or cannot tell yet (no content pack read): either way its -// declarations do not apply. Only a mod's own items take them. +// All stock/developer items are excluded, free or paid. Unknown catalogue +// state is refused too; ownership is additionally checked against item sources. bool sold(std::string_view item) { - return !content_cache::catalogs().available || profile_runtime::reserved_cosmetic(item); + std::string key(item); for(auto& c:key)if(c>='A'&&c<='Z')c=static_cast(c+32); + const auto& catalog=content_cache::catalogs(); + return !catalog.available || catalog.items.contains(key) || key.starts_with("dev_") || key.starts_with("own_dev_") || profile_runtime::reserved_cosmetic(item); } constexpr std::array worn_slots{"Deck", "Grip tape", "Trucks", "Wheels"}; @@ -194,7 +185,7 @@ constexpr std::array worn_slots{"Deck", "Gr // have settled (detail::settled). void declare(State& s, detail::Found& found) { const auto all = current_declarations(); - if (!all || all->empty()) return; + 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); @@ -207,9 +198,17 @@ void declare(State& s, detail::Found& found) { std::uintptr_t material{}; for (const auto& m : found.materials) if (static_cast(m.part) == part) { material = m.address; break; } - if (!detail::settled(s.gates[part], item, material, now) || found.pending) continue; + 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); }; - auto add = detail::declared_for(*all, static_cast(part), item, is_sold); + std::string key(item);for(auto& c:key)if(c>='A'&&c<='Z')c=static_cast(c+32); + std::string_view owner; + for(const auto& source:all->sources) { + if(std::find(source.keys.begin(),source.keys.end(),key)!=source.keys.end()){owner=source.mod;break;} + } + auto add = detail::declared_for(all->materials, static_cast(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); diff --git a/Extension/Customization/board_material.h b/Extension/Customization/board_material.h index 219232fd..12c2f316 100644 --- a/Extension/Customization/board_material.h +++ b/Extension/Customization/board_material.h @@ -76,6 +76,7 @@ struct Found { std::vector materials; bool pending{}; // a recipe on its way: nothing is written std::vector declared; // what the equipped items' mods declare (declarations below) + std::array declarations_pending{}; }; // Each part's named material on the board's slot controllers; the deck slot holds two. struct Target { @@ -331,13 +332,14 @@ void apply(std::vector& saved, const std::vector& holds, const Foun // ---- declarations --------------------------------------------------------------------------------- // The declarations of the item equipped on `part` (its key, as the game names it; any case), from -// every enabled mod's reskate-materials.json. A key two declarations give differently is left out: +// 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 -std::vector declared_for(const std::vector& declarations, Part part, std::string_view item, Sold& sold) { +std::vector declared_for(const std::vector& declarations, Part part, std::string_view item, Sold& sold, std::string_view owner) { std::vector out; - if (item.empty() || sold(item)) return out; + if (item.empty() || owner.empty() || sold(item)) return out; const auto folded = [](std::string_view text) { std::string s(text); for (auto& c : s) c = static_cast(c >= 'A' && c <= 'Z' ? c + 32 : c); @@ -346,7 +348,7 @@ std::vector declared_for(const std::vector& const auto wanted = folded(item); std::vector conflicting; for (const auto& d : declarations) { - if (d.part != part_names[static_cast(part)]) continue; + if (d.mod != owner || d.part != part_names[static_cast(part)]) continue; if (std::none_of(d.items.begin(), d.items.end(), [&](const std::string& i) { return folded(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}; @@ -380,7 +382,7 @@ inline bool settled(Gate& gate, std::string_view item, std::uintptr_t material, inline std::size_t drop_undeclared(std::vector& holds, const Found& live) { if (live.pending) return 0; return std::erase_if(holds, [&](const Hold& h) { - if (!h.declared) return false; + if (!h.declared || live.declarations_pending[static_cast(h.part)]) return false; const auto* d = declared(live, h.part, h.key); return !d || d->kind != h.kind; }); diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp index f829e588..84359238 100644 --- a/Extension/Plugins/Test/plugin_tests.cpp +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -62,8 +62,6 @@ struct FakeGame final : Game { return ending == "roadrash_leg1" ? "RoadRash_Leg1" : ""; } std::vector outfit_slots() override { return {{"tattoo_legl", "", {0.5f, 1}}, {"moles", "moles_empty", {}}}; } - int board_preparations{}; - void prepare_board() override { ++board_preparations; } std::optional> board_bones() override { std::array bones{}; bones[3].position = {0, 0.1f, 0}; @@ -99,7 +97,7 @@ struct FakeGame final : Game { for (const auto key : declared_keys) declaration.parameters.push_back({key, static_cast(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); + 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; } @@ -2478,15 +2476,6 @@ 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 board_observation_is_prepared_only_with_permission() { - TestEnvironment environment; - Host host(environment, Side::client); - host.apply({script("plain", "assert(reskate)")}); - check(environment.fake.board_preparations == 0, "No board preparation without permission"); - host.apply({script("boardreader", "assert(reskate.board.bones())", R"("board")")}); - running(host, "boardreader", environment); - check(environment.fake.board_preparations == 1, "Board observation is prepared before the script reads its pose"); -} void declared_board_colours_through_the_api() { TestEnvironment environment; @@ -4251,7 +4240,6 @@ 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(board_observation_is_prepared_only_with_permission); run(declared_board_colours_through_the_api); run(key_names_become_binds); run(level_events_reach_plugins); diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 728b1742..381d309e 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -2891,7 +2891,6 @@ struct WorldApi { void install_world_api(Plugin& plugin, lua_State* L) { WorldApi::install(plugin, L); } void WorldApi::install(Plugin& plugin, lua_State* L) { - if (plugin.has("board")) game(plugin).prepare_board(); (void)plugin; // The metatables of player tables (push_player) and of permissions; their own stay hidden. lua_createtable(L, 0, 2); diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index a888dbf8..6e99c7bf 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -806,8 +806,6 @@ public: std::array rotation{0, 0, 0, 1}; Vec3 scale{1, 1, 1}; }; - // Install native board observation before entering a timed Lua call. - virtual void prepare_board() {} virtual std::optional> board_bones() { return std::nullopt; } virtual std::string board_set(std::string_view /*plugin*/, int, const BoardBone&, bool /*world*/) { return unavailable; } // `everyone`: hidden from the other players too, not only on this screen. diff --git a/Extension/Plugins/plugin_game_runtime.cpp b/Extension/Plugins/plugin_game_runtime.cpp index 53240ff0..941d3160 100644 --- a/Extension/Plugins/plugin_game_runtime.cpp +++ b/Extension/Plugins/plugin_game_runtime.cpp @@ -19,7 +19,6 @@ #include "Extension/Skater/skater_events.h" #include "Extension/Skater/outfit_overlay.h" #include "Extension/Skater/board_pose.h" -#include "Extension/Multiplayer/Remote/native_skater.h" #include "Extension/Customization/board_material.h" #include "Engine/Vfs/mod_catalog.h" #include "Engine/Core/Log/logging.h" @@ -501,13 +500,7 @@ public: return thrower_ == plugin ? multiplayer::throwdown_relay_host_status() : std::string{}; } // ---- board ----------------------------------------------------------------------------------------- - void prepare_board() override { - if (board_hooks_ready_) return; - std::string detail; - board_hooks_ready_ = multiplayer::install_entity_hooks(base_, detail); - } std::optional> board_bones() override { - if (!board_hooks_ready_) return std::nullopt; const auto drawn = board_pose::read(); if (!drawn) return std::nullopt; std::array out{}; @@ -1816,7 +1809,6 @@ private: } std::uintptr_t base_; - bool board_hooks_ready_{}; std::string capture_owner_, capture_file_; // the plugin whose capture it is, and where it saved to std::string camera_holder_, follower_, camera_status_; std::array camera_pose_{}; diff --git a/contrib/declared-board-materials.md b/contrib/declared-board-materials.md deleted file mode 100644 index fe1c9c32..00000000 --- a/contrib/declared-board-materials.md +++ /dev/null @@ -1,19 +0,0 @@ -# Declared board material inputs - -Plugins already read and hold live colour/number values through `reskate.board.parameters`, `set_parameter` and `clear_parameter`. A custom shader can have inputs whose defaults are compiled into it, with no live node for that API to hold. A mod may declare these in its root `reskate-materials.json`: - -```json -{"schema":1,"materials":[{"part":"deck","items":["Own_Example_Deck"],"parameters":[{"guid":"1af346775c032d5b59ff5e88010069b6","type":"colour","default":[0.5,0.5,0.5]}]}]} -``` - -The declaration describes an existing compiled shader input; it does not install shaders, select materials, unlock items or add textures. The GUID is its 16 serialized bytes in hex. The shader and declaration must agree on identity, type and default. Only the declaring equipped mod-owned item receives the value. Store items are excluded. - -A permitted plugin sees an absent live value as `inherited = true`, with the declared default. It uses the existing `board` permission and existing set/clear API. Missing declared colours are inserted with the engine typed setter at native preset priority. A number must already have a live value; the declaration does not enable missing float insertion. Integers, switches and textures cannot be declared or changed. - -Held values are released on clear, unload and item changes. Existing nodes restore their prior values; inserted nodes restore declared defaults (the engine retains the node). Type/identity mismatches and values outside bounds are refused. Competing plugins cannot take over a held key. The native setter/descriptor are checked against the supported build before use. - -Bounds: 32 materials, 64 item keys per declaration, 32 parameters per declaration; text at most 255 bytes; colour channels 0–1 and numbers -100–100. Malformed files are skipped as a whole. - -Board observation is prepared before timed plugin execution, only for a plugin with the `board` permission. No effect-specific solver, artwork, controller or raw-memory plugin access is introduced. - -Tests: `mod_materials`, `board_material`, `plugins`.