diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp index c0b206b4..09101ef8 100644 --- a/Extension/Plugins/Test/plugin_tests.cpp +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -1235,10 +1235,15 @@ void plugins_server_copies_are_the_authority() { player.session_now = {true, false, {{2, "Bob", true, false}, {1, "Ann", false, true}}}; const std::string main = R"( reskate.log("side " .. reskate.side) - if reskate.side == "server" then + local E = reskate.Enums + assert(E.Side.server == "server" and E.Event.tick == "tick" and E.Key.Enter == "Enter" and E.LogLevel.warn == "warn" + and E.Target.all == "all" and E.EntityStatus.ready == "ready" and E.Throwdown.spot_battle == "spot_battle") + assert(not pcall(function() E.Pop.huge = "huge" end), "an enum is read-only") + reskate.log("enum check", E.LogLevel.warn) + if reskate.side == E.Side.server then reskate.on("message", function(from, text) reskate.log("server heard " .. text .. " from " .. from.name) - assert(reskate.net.send("all", "state " .. text)) + assert(reskate.net.send(reskate.Enums.Target.all, "state " .. text)) assert(reskate.server.kick(from, "testing")) assert(reskate.server.say("round over")) assert(not pcall(reskate.net.send, "server", "x"), "it is the server") diff --git a/Extension/Plugins/plugin_api.cpp b/Extension/Plugins/plugin_api.cpp index 85646683..9eddf9be 100644 --- a/Extension/Plugins/plugin_api.cpp +++ b/Extension/Plugins/plugin_api.cpp @@ -230,7 +230,7 @@ struct ApiFunctions { auto& plugin = plugin_of(L); const auto message = text_of(L, 1); const std::string level = luaL_optstring(L, 2, "info"); - if (level != "info" && level != "warn" && level != "error") luaL_argerror(L, 2, "the level must be \"info\", \"warn\" or \"error\""); + if (!one_of(log_levels, level)) luaL_argerror(L, 2, "the level must be \"info\", \"warn\" or \"error\""); plugin.log(level == "error" ? LogLevel::error : level == "warn" ? LogLevel::warn : LogLevel::info, message); return 0; } @@ -675,21 +675,25 @@ void Api::install(Plugin& plugin, lua_State* L) { lua_setfield(L, -2, "side"); // Named constants, read-only and built from the C++ lists so they cannot drift: // reskate.Enums.Side.server is "server", reskate.Enums.Event.tick is "tick". Plain strings still work. - lua_createtable(L, 0, 2); - lua_createtable(L, 0, 3); - for (const auto side : {Side::client, Side::server, Side::both}) { - lua_pushstring(L, side_name(side)); - lua_setfield(L, -2, side_name(side)); - } - readonly(); - lua_setfield(L, -2, "Side"); - lua_createtable(L, 0, static_cast(events.size())); - for (const auto& event : events) { - lua_pushlstring(L, event.name.data(), event.name.size()); - lua_setfield(L, -2, std::string(event.name).c_str()); - } - readonly(); - lua_setfield(L, -2, "Event"); + const auto push_enum = [&](const char* name, const auto& values, auto word) { + lua_createtable(L, 0, static_cast(std::size(values))); + for (const auto& value : values) { + const std::string_view w = word(value); + lua_pushlstring(L, w.data(), w.size()); + lua_setfield(L, -2, std::string(w).c_str()); + } + readonly(); + lua_setfield(L, -2, name); + }; + const auto same = [](std::string_view w) { return w; }; + lua_createtable(L, 0, 13); + push_enum("Side", std::array{Side::client, Side::server, Side::both}, [](Side side) { return std::string_view(side_name(side)); }); + push_enum("Event", events, [](const Event& event) { return event.name; }); + push_enum("Key", key_names, [](const auto& key) { return key.first; }); + for (const auto& [name, words] : {std::pair{"LogLevel", log_levels}, {"Align", aligns}, {"GrindSide", grind_sides}, + {"Stance", stances}, {"Pop", pops}, {"Space", spaces}, {"Throwdown", throwdown_modes}, + {"Card", throwdown_cards}, {"EntityStatus", entity_statuses}, {"Target", send_targets}}) + push_enum(name, words, same); readonly(); lua_setfield(L, -2, "Enums"); using F = ApiFunctions; diff --git a/Extension/Plugins/plugin_api.h b/Extension/Plugins/plugin_api.h index 91d6002f..8de21f40 100644 --- a/Extension/Plugins/plugin_api.h +++ b/Extension/Plugins/plugin_api.h @@ -1,7 +1,11 @@ #pragma once #include "plugin_vm.h" #include "lua.h" +#include #include +#include +#include +#include // Inside the plugin host only: the reskate API a plugin's VM gets. namespace dingosdk::plugins { @@ -45,6 +49,27 @@ void add_functions(lua_State* L, std::initializer_list functions); // Pushes a new read-only table of the functions, kept outside reskate (the draw and ui handles). void push_functions(lua_State* L, std::initializer_list functions); +// ---- fixed words ------------------------------------------------------------------------------- +// The words an API function takes or gives from a fixed set. Each list is also reskate.Enums. +// (Api::install), so the check and the constant cannot drift apart. +using Words = std::span; +inline constexpr std::string_view log_levels[] = {"info", "warn", "error"}; // Enums.LogLevel +inline constexpr std::string_view aligns[] = {"left", "centre", "center", "right"}; // Enums.Align +inline constexpr std::string_view grind_sides[] = {"toe", "heel", "left", "right"}; // Enums.GrindSide +inline constexpr std::string_view stances[] = {"regular", "goofy"}; // Enums.Stance +inline constexpr std::string_view pops[] = {"low", "mid", "high"}; // Enums.Pop +inline constexpr std::string_view spaces[] = {"local", "world"}; // Enums.Space +inline constexpr std::string_view throwdown_modes[] = {"jam", "spot_battle", "skate"}; // Enums.Throwdown +inline constexpr std::string_view throwdown_cards[] = {"skate", "spot", "spot_battle", "jam"}; // Enums.Card +inline constexpr std::string_view entity_statuses[] = {"waiting", "loading", "missing", "ready", "hidden"}; // Enums.EntityStatus +inline constexpr std::string_view send_targets[] = {"all", "server"}; // Enums.Target +// Named keys for input.key and the like (letters and digits are their own character): Enums.Key. +inline constexpr std::pair key_names[] = { + {"Enter", 13}, {"Backspace", 8}, {"Escape", 27}, {"End", 35}, {"Home", 36}, {"PageUp", 33}, {"PageDown", 34}, {"Space", 32}, + {"Shift", 16}, {"Ctrl", 17}, {"Alt", 18}, {"Tab", 9}, {"Left", 37}, {"Up", 38}, {"Right", 39}, {"Down", 40}, + {"[", 0xDB}, {"]", 0xDD}, {"LeftMouse", 1}, {"RightMouse", 2}, {"MiddleMouse", 4}}; +inline bool one_of(Words words, std::string_view word) { return std::ranges::find(words, word) != words.end(); } + // ---- what most API functions start with -------------------------------------------------------- // The plugin calling, or a Lua error once it has been stopped. Plugin& plugin_of(lua_State* L); diff --git a/Extension/Plugins/plugin_api_services.cpp b/Extension/Plugins/plugin_api_services.cpp index b5224853..5db4f655 100644 --- a/Extension/Plugins/plugin_api_services.cpp +++ b/Extension/Plugins/plugin_api_services.cpp @@ -146,11 +146,7 @@ int key_code(lua_State* L, int index) { return vk; } const std::string name = luaL_checkstring(L, index); - static const std::pair names[] = { - {"Enter", 13}, {"Backspace", 8}, {"Escape", 27}, {"End", 35}, {"Home", 36}, {"PageUp", 33}, {"PageDown", 34}, {"Space", 32}, - {"Shift", 16}, {"Ctrl", 17}, {"Alt", 18}, {"Tab", 9}, {"Left", 37}, {"Up", 38}, {"Right", 39}, {"Down", 40}, - {"[", 0xDB}, {"]", 0xDD}, {"LeftMouse", 1}, {"RightMouse", 2}, {"MiddleMouse", 4}}; - for (const auto& [n, vk] : names) + for (const auto& [n, vk] : key_names) if (name == n) return vk; if (name.size() == 1 && ((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= '0' && name[0] <= '9'))) return name[0]; luaL_argerror(L, index, "a key name (A-Z, 0-9, Enter, Backspace, Escape, End, PageUp, PageDown, [, ], ...) or a virtual key"); diff --git a/Extension/Plugins/plugin_api_ui.cpp b/Extension/Plugins/plugin_api_ui.cpp index 88a59988..7b2acda3 100644 --- a/Extension/Plugins/plugin_api_ui.cpp +++ b/Extension/Plugins/plugin_api_ui.cpp @@ -281,7 +281,7 @@ struct UiApi { need(L, plugin, "overlay", "reskate.ui.toast"); const auto message = checked_text(L, 1, 200, "a toast"); const std::string level = luaL_optstring(L, 2, "info"); - if (level != "info" && level != "warn" && level != "error") luaL_argerror(L, 2, "the level is \"info\", \"warn\" or \"error\""); + if (!one_of(log_levels, level)) luaL_argerror(L, 2, "the level is \"info\", \"warn\" or \"error\""); const double now = plugin.services_.now_ms(); std::erase_if(plugin.toasts_, [&](double at) { return now - at >= 5000; }); if (static_cast(plugin.toasts_.size()) >= plugin.limits_.toasts) { diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 7672a790..2489d30a 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -117,7 +117,7 @@ struct WorldApi { lua_getfield(L, 1, "side"); wish.side = luaL_checkstring(L, -1); lua_pop(L, 1); - if (wish.side != "toe" && wish.side != "heel" && wish.side != "left" && wish.side != "right") + if (!one_of(grind_sides, wish.side)) luaL_error(L, "a grind exit's side is \"toe\", \"heel\", \"left\" or \"right\""); lua_getfield(L, 1, "stance"); if (!lua_isnil(L, -1)) { @@ -460,10 +460,9 @@ struct WorldApi { static int entities_status(lua_State* L) { auto& plugin = plugin_of(L); need(L, plugin, "world.entities", "reskate.entities.status"); - static constexpr const char* names[] = {nullptr, "waiting", "loading", "missing", "ready", "hidden"}; const auto status = game(plugin).entity_status(plugin.id(), entity_id(L, 1)); - if (status <= 0 || status > 5) lua_pushnil(L); - else lua_pushstring(L, names[status]); + if (status <= 0 || status > static_cast(std::size(entity_statuses))) lua_pushnil(L); + else lua_pushlstring(L, entity_statuses[status - 1].data(), entity_statuses[status - 1].size()); return 1; } static int entities_ready(lua_State* L) { @@ -743,7 +742,7 @@ struct WorldApi { lua_getfield(L, 3, "pop"); if (!lua_isnil(L, -1)) pop = luaL_checkstring(L, -1); lua_pop(L, 1); - if (pop != "" && pop != "low" && pop != "mid" && pop != "high") luaL_argerror(L, 3, "a pop is \"low\", \"mid\" or \"high\""); + if (!pop.empty() && !one_of(pops, pop)) luaL_argerror(L, 3, "a pop is \"low\", \"mid\" or \"high\""); } const float speed = number_field(L, 3, "speed", 1, 0.1f, 4); return result(L, game(plugin).replace_trick(plugin.id(), std::move(trick), std::move(pop), std::move(clip), speed)); @@ -867,7 +866,7 @@ struct WorldApi { lua_getfield(L, 2, "space"); const std::string space = lua_isnil(L, -1) ? "local" : luaL_checkstring(L, -1); lua_pop(L, 1); - if (space != "local" && space != "world") luaL_argerror(L, 2, "space is \"local\" or \"world\""); + if (!one_of(spaces, space)) luaL_argerror(L, 2, "space is \"local\" or \"world\""); return result(L, game(plugin).board_set(plugin.id(), bone, value, space == "world")); } static int board_hide(lua_State* L) { @@ -1284,7 +1283,7 @@ struct WorldApi { mode.cursor_height = static_cast(h); } lua_pop(L, 1); - const auto stock = [](const std::string& tile) { return tile == "skate" || tile == "spot" || tile == "spot_battle" || tile == "jam"; }; + const auto stock = [](const std::string& tile) { return one_of(throwdown_cards, tile); }; if (!mode.replaces.empty() && !stock(mode.replaces)) luaL_argerror(L, 1, "replaces is \"skate\", \"spot\" or \"jam\""); if (!stock(mode.card)) luaL_argerror(L, 1, "card is \"skate\", \"spot\" or \"jam\""); if (mode.flag.size() > 256 || mode.art.size() > 256) luaL_argerror(L, 1, "art and flag are at most 256 characters");