Plugins: mods.content and read_content read the server's Mods on a dedicated server

A server plugin with mods.read could call reskate.mods.content, but the
server's game had no content of its own, so it got nil and "not
available". A mode that keeps its data in mods (a race reading start
and finish from a park mod) could only read it in the players' games.

The server now answers from every mod folder in its Mods, by name, as it
finds its maps there; it has no load order or mods.json. A hidden folder
(.reskate_plugin_data) and names a plugin could not ask for are left out,
and a mod's title is its manifest.json's "name".

The listing and reading moved from plugin_game_runtime.cpp into
plugin_content.cpp, which the game and the server share, so both keep
the same limits: the folders the manifest's "content" names, 1000 files,
four folders deep, 1 MB a file, nothing outside the mod's folder. The
game's side is unchanged apart from calling it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYabE7FCPgj3SaWeF92EML
This commit is contained in:
Robin MoellerandClaude Opus 5.5 committed 2026-10-23 15:50:55 +02:00
1 parent f763792732
commit 0706730e79
8 files changed
+246 -54

No files matched your search

+6 -53
View File
@@ -1,5 +1,6 @@
#include "plugin_game_runtime.h"
#include "plugin_session_feed.h"
#include "plugin_content.h"
#include "Runtime/runtime_internal.h"
#include "plugin_ui.h"
#include "Engine/Game/World/park_rotation.h"
@@ -1442,13 +1443,9 @@ public:
tricks::prepare_trick_gestures();
return tricks::trick_gesture(trick);
}
// ---- content: <mod>/<folder>/ of every enabled mod ---------------------------------------------
// ---- content: <mod>/<folder>/ of every enabled mod (plugin_content.h) --------------------------
// The enabled mods, read again at most every 2 s: a page may list them every frame.
struct EnabledMod {
std::string name, title;
std::filesystem::path directory;
};
std::vector<EnabledMod> enabled_mods() {
std::vector<content::Mod> enabled_mods() {
const auto now = GetTickCount64();
if (enabled_read_at_ && now - enabled_read_at_ < 2000) return enabled_mods_;
enabled_read_at_ = now;
@@ -1459,56 +1456,12 @@ public:
if (entry.enabled) enabled_mods_.push_back({entry.mod.name, entry.mod.title.empty() ? entry.mod.name : entry.mod.title, entry.mod.directory});
return enabled_mods_;
}
static constexpr std::size_t most_content_files = 1000, most_content_bytes = 1u << 20;
std::string content_list(std::string_view folder, std::vector<ContentFile>& files) override {
for (const auto& mod : enabled_mods()) {
const auto root = mod.directory / std::filesystem::path(std::string(folder));
std::error_code error;
if (!std::filesystem::is_directory(root, error)) continue;
std::vector<ContentFile> found;
for (std::filesystem::recursive_directory_iterator it(root, std::filesystem::directory_options::none, error), end;
!error && it != end; it.increment(error)) {
if (it.depth() >= 4) it.disable_recursion_pending();
if (!it->is_regular_file(error)) continue;
const auto relative = it->path().lexically_relative(root).generic_wstring();
std::string path;
bool plain = true;
for (const auto c : relative) {
if (c < 0x20 || c > 0x7e) plain = false;
path.push_back(static_cast<char>(c));
}
if (!plain || path.ends_with(".tmp")) continue; // a name a plugin could not ask for again
found.push_back({mod.name, mod.title, std::move(path), static_cast<std::uint64_t>(it->file_size(error))});
if (files.size() + found.size() >= most_content_files) break;
}
std::sort(found.begin(), found.end(), [](const auto& a, const auto& b) { return a.path < b.path; });
for (auto& file : found) files.push_back(std::move(file));
if (files.size() >= most_content_files) break;
}
content::list(enabled_mods(), folder, files);
return {};
}
std::string content_read(std::string_view mod, std::string_view folder, std::string_view path, std::string& text) override {
const auto mods_now = enabled_mods();
const auto found = std::find_if(mods_now.begin(), mods_now.end(), [&](const EnabledMod& each) {
return each.name.size() == mod.size() && _strnicmp(each.name.c_str(), mod.data(), mod.size()) == 0;
});
if (found == mods_now.end()) return "no such mod is on";
const auto root = found->directory / std::filesystem::path(std::string(folder));
const auto file = root / std::filesystem::path(std::string(path));
std::error_code error;
const auto real_root = std::filesystem::weakly_canonical(root, error);
const auto real_file = error ? std::filesystem::path{} : std::filesystem::weakly_canonical(file, error);
if (error) return "not found";
const auto relative = real_file.lexically_relative(real_root);
if (relative.empty() || *relative.begin() == "..") return "outside the mod's folder";
if (!std::filesystem::is_regular_file(file, error)) return "not found";
const auto size = std::filesystem::file_size(file, error);
if (error) return "the file cannot be read";
if (size > most_content_bytes) return "the file is larger than 1 MB";
std::ifstream in(file, std::ios::binary);
text.resize(static_cast<std::size_t>(size));
if (!in || !in.read(text.data(), static_cast<std::streamsize>(size))) return "the file cannot be read";
return {};
return content::read(enabled_mods(), mod, folder, path, text);
}
std::string trick_label(std::string_view plugin, std::string_view from, std::string_view to) override {
return trick_rewrite::label(plugin, from, to);
@@ -1741,7 +1694,7 @@ private:
std::vector<std::string> installed;
};
std::map<std::string, VariantRead, std::less<>> variant_reads_;
std::vector<EnabledMod> enabled_mods_;
std::vector<content::Mod> enabled_mods_;
std::string reacher_; // the plugin holding the skater's arm (skater_reach)
std::string poser_; // the plugin turning the skater's joints (skater_pose)
std::string carrier_; // the plugin having the skater carry something (skater_carry)