mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 08:06:43 +01:00
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:
1 parent
f763792732
commit
0706730e79
8 files changed
+246
-54
No files matched your search
@@ -1,5 +1,6 @@
|
||||
// The plugin host apart from the game: manifests, the reskate API, storage, modules, hot reload,
|
||||
// and the sandbox against plugins written to break it (loops, memory, error floods, escapes).
|
||||
#include "Extension/Plugins/plugin_content.h"
|
||||
#include "Extension/Plugins/plugin_discovery.h"
|
||||
#include "Extension/Plugins/plugin_host.h"
|
||||
#include "Extension/Plugins/plugin_manifest.h"
|
||||
@@ -2893,6 +2894,54 @@ void a_plugin_reads_the_content_folder_it_names() {
|
||||
check(environment.printed("content ok") && game.called("content list parks"), "The plugin reads its content folder\n" + environment.all());
|
||||
}
|
||||
|
||||
// What the game and a dedicated server read for mods.content (plugin_content.h), from real folders:
|
||||
// every mod folder in a Mods folder by name, each one's files under the folder asked for, and the
|
||||
// limits a plugin cannot get past.
|
||||
void content_is_read_from_the_mod_folders() {
|
||||
namespace content = dingosdk::plugins::content;
|
||||
const auto mods = scratch / "content mods";
|
||||
const auto write = [](const std::filesystem::path& file, const std::string& text) {
|
||||
std::filesystem::create_directories(file.parent_path());
|
||||
std::ofstream(file, std::ios::binary) << text;
|
||||
};
|
||||
write(mods / "b_track" / "parks" / "start.json", "{}");
|
||||
write(mods / "b_track" / "parks" / "deep" / "a" / "b" / "c" / "level4.json", "{}");
|
||||
write(mods / "b_track" / "parks" / "deep" / "a" / "b" / "c" / "d" / "level5.json", "{}");
|
||||
write(mods / "b_track" / "parks" / "half.tmp", "");
|
||||
write(mods / "b_track" / "parks" / "big.json", std::string((1u << 20) + 1, ' '));
|
||||
write(mods / "b_track" / "manifest.json", R"({"name": "B Track", "version_number": "1.0.0"})");
|
||||
write(mods / "A_Park" / "parks" / "z.json", "zz");
|
||||
write(mods / "A_Park" / "reskate-levels.json", "{}");
|
||||
write(mods / "no parks" / "manifest.json", "not JSON");
|
||||
write(mods / ".reskate_plugin_data" / "parks" / "secret.json", "{}");
|
||||
write(mods / "loose.json", "{}");
|
||||
|
||||
const auto found = content::mod_folders(mods);
|
||||
check(found.size() == 3 && found[0].name == "A_Park" && found[1].name == "b_track" && found[2].name == "no parks",
|
||||
"Every mod folder, by name, none hidden");
|
||||
check(found[0].title == "A_Park" && found[1].title == "B Track" && found[2].title == "no parks", "titled as their manifest names them");
|
||||
check(content::mod_folders(scratch / "no such folder").empty(), "No Mods folder, no mods");
|
||||
|
||||
std::vector<Game::ContentFile> files;
|
||||
content::list(found, "parks", files);
|
||||
std::string listed;
|
||||
for (const auto& file : files) listed += std::format("{}:{}:{} ", file.mod, file.path, file.size);
|
||||
check(listed == std::format("A_Park:z.json:2 b_track:big.json:{} b_track:deep/a/b/c/level4.json:2 b_track:start.json:2 ", (1u << 20) + 1),
|
||||
"Mod by mod, sorted, four folders deep at most, no .tmp: " + listed);
|
||||
|
||||
std::string text;
|
||||
check(content::read(found, "B_TRACK", "parks", "start.json", text).empty() && text == "{}", "A file is read, the mod named in any case");
|
||||
check(content::read(found, "b_track", "parks", "missing.json", text) == "not found", "A missing file");
|
||||
check(content::read(found, "b_track", "parks", "big.json", text) == "the file is larger than 1 MB", "A file over 1 MB");
|
||||
check(content::read(found, "nobody", "parks", "start.json", text) == "no such mod is on", "A mod that is not there");
|
||||
check(content::read(found, ".reskate_plugin_data", "parks", "secret.json", text) == "no such mod is on", "A hidden folder is no mod");
|
||||
|
||||
for (int i = 0; i < 1005; ++i) write(mods / "c_many" / "parks" / std::format("{:04}.json", i), "");
|
||||
files.clear();
|
||||
content::list(content::mod_folders(mods), "parks", files);
|
||||
check(files.size() == content::most_files, std::format("At most {} files, {} listed", content::most_files, files.size()));
|
||||
}
|
||||
|
||||
void board_break_snaps_the_board_on_a_slam() {
|
||||
TestEnvironment environment;
|
||||
std::filesystem::remove_all(scratch / "storage" / "board-break");
|
||||
@@ -3896,6 +3945,7 @@ int main() {
|
||||
run(dual_stick_edits_the_companions_settings);
|
||||
run(rock_to_fakie_relabels_and_records_the_stall);
|
||||
run(a_plugin_reads_the_content_folder_it_names);
|
||||
run(content_is_read_from_the_mod_folders);
|
||||
run(board_break_snaps_the_board_on_a_slam);
|
||||
run(fpv_drone_flies_and_follows);
|
||||
run(hall_of_meat_scores_a_bail);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "plugin_content.h"
|
||||
#include "Engine/Core/Json/json.h"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace dingosdk::plugins::content {
|
||||
namespace {
|
||||
// The names mod_list.h's scan_mods takes for a mod folder, as long as read_content takes.
|
||||
bool mod_name(std::string_view name) {
|
||||
if (name.empty() || name.size() > 64 || name.front() == '.') return false;
|
||||
return std::all_of(name.begin(), name.end(), [](char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' || c == ' ';
|
||||
});
|
||||
}
|
||||
bool same_name(std::string_view a, std::string_view b) {
|
||||
return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char x, char y) {
|
||||
const auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? static_cast<char>(c + 32) : c; };
|
||||
return lower(x) == lower(y);
|
||||
});
|
||||
}
|
||||
// manifest.json's "name", 1 to 64 characters, or empty when there is none to read.
|
||||
std::string manifest_name(const std::filesystem::path& directory) {
|
||||
const auto file = directory / "manifest.json";
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_regular_file(file, error) || std::filesystem::file_size(file, error) > 32 * 1024 || error) return {};
|
||||
try {
|
||||
std::ifstream in(file, std::ios::binary);
|
||||
std::stringstream text;
|
||||
text << in.rdbuf();
|
||||
const auto root = Json::parse(text.str());
|
||||
if (!root.is_object() || !root.contains("name") || !root.at("name").is_string()) return {};
|
||||
const auto& name = root.at("name").string();
|
||||
return name.size() <= 64 ? name : std::string{};
|
||||
} catch (const std::exception&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<Mod> mod_folders(const std::filesystem::path& mods_root) {
|
||||
std::vector<Mod> mods;
|
||||
std::error_code error;
|
||||
for (std::filesystem::directory_iterator it(mods_root, error), end; !error && it != end; it.increment(error)) {
|
||||
std::error_code entry;
|
||||
if (!it->is_directory(entry)) continue;
|
||||
const auto utf8 = it->path().filename().u8string();
|
||||
std::string name(utf8.begin(), utf8.end());
|
||||
if (!mod_name(name)) continue;
|
||||
auto title = manifest_name(it->path());
|
||||
mods.push_back({name, title.empty() ? name : std::move(title), it->path()});
|
||||
}
|
||||
std::sort(mods.begin(), mods.end(), [](const Mod& a, const Mod& b) { return a.name < b.name; });
|
||||
return mods;
|
||||
}
|
||||
|
||||
void list(const std::vector<Mod>& mods, std::string_view folder, std::vector<Game::ContentFile>& files) {
|
||||
for (const auto& mod : 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<Game::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_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_files) break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string read(const std::vector<Mod>& mods, std::string_view mod, std::string_view folder, std::string_view path, std::string& text) {
|
||||
const auto found = std::find_if(mods.begin(), mods.end(), [&](const Mod& each) { return same_name(each.name, mod); });
|
||||
if (found == mods.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_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 {};
|
||||
}
|
||||
} // namespace dingosdk::plugins::content
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include "plugin_game.h"
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// mods.content and read_content, from the mods' folders on disk: the game reads its enabled mods
|
||||
// (plugin_game_runtime.cpp), a dedicated server every mod folder in its Mods (server_plugins.cpp).
|
||||
// Both keep the same limits, so a plugin reads the same files on either.
|
||||
namespace dingosdk::plugins::content {
|
||||
struct Mod {
|
||||
std::string name, title; // the folder's name and the mod's own name
|
||||
std::filesystem::path directory;
|
||||
};
|
||||
inline constexpr std::size_t most_files = 1000, most_bytes = 1u << 20;
|
||||
|
||||
// Every folder in `mods_root` a mod can be, by name: not hidden (".reskate_plugin_data"), and a
|
||||
// name a plugin can ask for again. Titled by its manifest.json's "name", else its folder's name.
|
||||
std::vector<Mod> mod_folders(const std::filesystem::path& mods_root);
|
||||
// The files under <mod>/<folder>/ of each of `mods` in turn, sorted within a mod, four folders deep
|
||||
// at most and most_files in all; `folder` is checked against the manifest already.
|
||||
void list(const std::vector<Mod>& mods, std::string_view folder, std::vector<Game::ContentFile>& files);
|
||||
// One of them, at most most_bytes; `path` is checked already (safe_relative_path). Empty when read,
|
||||
// else why not.
|
||||
std::string read(const std::vector<Mod>& mods, std::string_view mod, std::string_view folder, std::string_view path, std::string& text);
|
||||
} // namespace dingosdk::plugins::content
|
||||
@@ -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)
|
||||
|
||||
@@ -211,6 +211,10 @@ int run(const std::filesystem::path &fixtures) {
|
||||
local ok, why = pcall(reskate.server.parks)
|
||||
reskate.console.print(ok and "blind listed parks" or "blind parks refused: " .. tostring(why))
|
||||
end)
|
||||
reskate.console.command("blind content", "mod content without mods.read", function()
|
||||
local ok, why = pcall(reskate.mods.content, "parks")
|
||||
reskate.console.print(ok and "blind listed content" or "blind content refused: " .. tostring(why))
|
||||
end)
|
||||
)");
|
||||
|
||||
// One that says what the level is, and when it changes.
|
||||
@@ -251,6 +255,22 @@ int run(const std::filesystem::path &fixtures) {
|
||||
end)
|
||||
)");
|
||||
|
||||
// One that reads the parks folder of the mods the server has, as a game's plugin reads its
|
||||
// enabled mods'.
|
||||
write(root / "Plugins" / "reader" / "reskate-plugin.json",
|
||||
R"({"api": 1, "entry": "plugin/main.lua", "side": "server", "permissions": ["console", "mods.read"], "content": ["parks"]})");
|
||||
write(root / "Plugins" / "reader" / "plugin" / "main.lua", R"(
|
||||
reskate.console.command("reader", "mod content, for the test", function()
|
||||
for _, f in ipairs(assert(reskate.mods.content("parks"))) do
|
||||
reskate.console.print(string.format("content %s (%s) %s %d", f.mod, f.title, f.path, f.size))
|
||||
end
|
||||
local text = assert(reskate.mods.read_content("mixed park", "parks", "grom.park.json"))
|
||||
reskate.console.print("read " .. #text .. " " .. tostring(text:find('"reskate-park"', 1, true) ~= nil))
|
||||
local missing, why = reskate.mods.read_content("mixed park", "parks", "nothing.json")
|
||||
reskate.console.print("missing " .. tostring(missing) .. " " .. why)
|
||||
end)
|
||||
)");
|
||||
|
||||
std::vector<std::string> log;
|
||||
SteamTransport transport;
|
||||
Host host(config, transport, [&](const std::string &line) { log.push_back(line); });
|
||||
@@ -350,6 +370,20 @@ int run(const std::filesystem::path &fixtures) {
|
||||
plugin_command("blind parks");
|
||||
check(has(log, "blind parks refused: ") && has(log, "server.parks"), "A plugin without server.parks listed the parks");
|
||||
|
||||
// The mods the server has, read through mods.content as a game reads its enabled mods.
|
||||
const auto size_of = [&](const char *mod, const char *file) {
|
||||
return std::to_string(std::filesystem::file_size(root / "Mods" / mod / "parks" / file));
|
||||
};
|
||||
plugin_command("reader");
|
||||
check(has(log, "content mixed park (mixed park) grom.park.json " + size_of("mixed park", "grom.park.json")) &&
|
||||
has(log, "content popular skate 2 street park (popular skate 2 street park) bam.park.json " +
|
||||
size_of("popular skate 2 street park", "bam.park.json")),
|
||||
"The server's mods' parks folders were not listed");
|
||||
check(has(log, "read " + size_of("mixed park", "grom.park.json") + " true"), "A park mod's file was not read");
|
||||
check(has(log, "missing nil not found"), "A missing file was not \"not found\"");
|
||||
plugin_command("blind content");
|
||||
check(has(log, "blind content refused: ") && has(log, "mods.read"), "A plugin without mods.read listed the mods' content");
|
||||
|
||||
// The level, as players' games name it, and its change heard when the server changes it.
|
||||
const auto *first = find_level(config.map);
|
||||
const auto other = std::find_if(levels().begin(), levels().end(), [&](const auto &l) { return l.asset != first->asset; });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "server_plugins.h"
|
||||
#include "server_host.h"
|
||||
#include "Extension/Plugins/plugin_content.h"
|
||||
#include "Extension/Plugins/plugin_host.h"
|
||||
#include "Extension/Plugins/plugin_manifest.h"
|
||||
#include "Extension/Plugins/plugin_wire.h"
|
||||
@@ -101,6 +102,15 @@ class ServerGame final : public Game {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// mods.content: every mod folder in the server's Mods, as a game reads its enabled mods. The
|
||||
// server has no load order or mods.json of its own; the folders come by name, as for its maps.
|
||||
std::string content_list(std::string_view folder, std::vector<ContentFile> &files) override {
|
||||
content::list(mod_folders(), folder, files);
|
||||
return {};
|
||||
}
|
||||
std::string content_read(std::string_view mod, std::string_view folder, std::string_view path, std::string &text) override {
|
||||
return content::read(mod_folders(), mod, folder, path, text);
|
||||
}
|
||||
// Through the console's own ban and unban, so they keep its checks and save the bans file.
|
||||
std::string ban(std::uint64_t player) override {
|
||||
const auto answer = host_.command("ban " + std::to_string(player));
|
||||
@@ -116,6 +126,15 @@ class ServerGame final : public Game {
|
||||
}
|
||||
|
||||
private:
|
||||
// Read again at most every 2 s: a plugin may list them every tick.
|
||||
const std::vector<content::Mod> &mod_folders() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!mods_read_ || now - *mods_read_ >= std::chrono::seconds(2)) {
|
||||
mods_read_ = now;
|
||||
mods_ = content::mod_folders(host_.config().mods);
|
||||
}
|
||||
return mods_;
|
||||
}
|
||||
bool present(std::uint64_t player) const {
|
||||
for (const auto &p : host_.player_list())
|
||||
if (p.id == player) return true;
|
||||
@@ -123,6 +142,8 @@ class ServerGame final : public Game {
|
||||
}
|
||||
Host &host_;
|
||||
ServerPlugins::Log log_;
|
||||
std::vector<content::Mod> mods_;
|
||||
std::optional<std::chrono::steady_clock::time_point> mods_read_;
|
||||
};
|
||||
|
||||
class ServerEnvironment final : public Environment {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
// a game mode's rules run. The operator installs them in Plugins beside the server, a folder a
|
||||
// plugin (with its reskate-plugin.json, "side" "server" or "both"); they load, reload and unload as
|
||||
// the folder changes, checked every 2 s. The server has no game: what a plugin may do here is the
|
||||
// session's messages, its players, the server API (kick, say, tell, move) and its storage.
|
||||
// session's messages, its players, the server API (kick, say, tell, move), its storage and reading
|
||||
// the content folders of the mods in Mods (mods.content).
|
||||
// Everything here runs on the server's tick thread.
|
||||
namespace dingosdk::server {
|
||||
class Host;
|
||||
|
||||
@@ -147,6 +147,7 @@ add_library(dingosdk_plugins STATIC
|
||||
Extension/Plugins/plugin_api_services.cpp
|
||||
Extension/Plugins/plugin_api_throwdowns.cpp
|
||||
Extension/Plugins/plugin_api_capture.cpp
|
||||
Extension/Plugins/plugin_content.cpp
|
||||
# The throwdown kit's store and the plugins' modes, which the plugin host and the game's
|
||||
# native UI share (kit::flag is the game's: Extension/Throwdowns/native_flag_session.cpp).
|
||||
Extension/Throwdowns/throwdown_kit.cpp
|
||||
|
||||
Reference in new issue
Block a user