From a9ad2c511c1551367af1e5442a3f2a64116c6532 Mon Sep 17 00:00:00 2001 From: Andrew Pogue Date: Fri, 4 Sep 2026 17:59:37 -0700 Subject: [PATCH 1/4] wip(lua): graph-based representation of Lua state --- CMakeLists.txt | 1 + content/terrain.lua | 12 +-- core/include/core/error.hh | 11 -- core/include/core/result.hh | 20 ++++ game/CMakeLists.txt | 6 +- game/include/game/lua.hh | 136 ----------------------- game/include/game/sprite.hh | 9 +- game/src/CMakeLists.txt | 2 +- game/src/sprite.cc | 78 +++++++------- play.cc | 9 -- sdk/CMakeLists.txt | 17 +++ sdk/include/sdk/detail.hh | 46 ++++++++ sdk/include/sdk/node.hh | 64 +++++++++++ sdk/include/sdk/path.hh | 73 +++++++++++++ sdk/include/sdk/schema.hh | 96 +++++++++++++++++ sdk/include/sdk/table.hh | 128 ++++++++++++++++++++++ sdk/include/sdk/types.hh | 31 ++++++ sdk/src/CMakeLists.txt | 1 + sdk/src/detail.cc | 6 ++ sdk/src/frame.cc | 66 ++++++++++++ sdk/src/lua-builder.cc | 59 ++++++++++ {game => sdk}/src/lua.cc | 35 +++++- sdk/src/node.cc | 210 ++++++++++++++++++++++++++++++++++++ sdk/src/object.cc | 67 ++++++++++++ sdk/src/path.cc | 28 +++++ sdk/src/query.cc | 76 +++++++++++++ sdk/src/table.cc | 40 +++++++ sdk/src/types.cc | 31 ++++++ 28 files changed, 1147 insertions(+), 211 deletions(-) delete mode 100644 core/include/core/error.hh create mode 100644 core/include/core/result.hh delete mode 100644 game/include/game/lua.hh create mode 100644 sdk/CMakeLists.txt create mode 100644 sdk/include/sdk/detail.hh create mode 100644 sdk/include/sdk/node.hh create mode 100644 sdk/include/sdk/path.hh create mode 100644 sdk/include/sdk/schema.hh create mode 100644 sdk/include/sdk/table.hh create mode 100644 sdk/include/sdk/types.hh create mode 100644 sdk/src/CMakeLists.txt create mode 100644 sdk/src/detail.cc create mode 100644 sdk/src/frame.cc create mode 100644 sdk/src/lua-builder.cc rename {game => sdk}/src/lua.cc (80%) create mode 100644 sdk/src/node.cc create mode 100644 sdk/src/object.cc create mode 100644 sdk/src/path.cc create mode 100644 sdk/src/query.cc create mode 100644 sdk/src/table.cc create mode 100644 sdk/src/types.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b174cf..b570113 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ endif () add_subdirectory (core) add_subdirectory (sys) add_subdirectory (gfx) +add_subdirectory (sdk) add_subdirectory (app) add_subdirectory (game) diff --git a/content/terrain.lua b/content/terrain.lua index 0ad9619..f175c4e 100644 --- a/content/terrain.lua +++ b/content/terrain.lua @@ -7,27 +7,27 @@ local function make_sprite(x, y, color) } end -terrain 'grass-1' { +content['terrain']['grass-1'] = { sprite = make_sprite(5, 0, 0x3BD872FF) } -terrain 'grass-2' { +content['terrain']['grass-2'] = { sprite = make_sprite(6, 0, 0x43FF64FF) } -terrain 'grass-3' { +content['terrain']['grass-3'] = { sprite = make_sprite(7, 0, 0x32DC96FF) } -terrain 'grass-tall' { +content['terrain']['grass-tall'] = { sprite = make_sprite(0, 2, 0x0C9B64FF) } -terrain 'dirt' { +content['terrain']['dirt'] = { sprite = make_sprite(2, 0, 0x79464bff) } -terrain 'rocks' { +content['terrain']['rocks'] = { sprite = make_sprite(2, 0, 0xcec5b7ff) } diff --git a/core/include/core/error.hh b/core/include/core/error.hh deleted file mode 100644 index ffda27b..0000000 --- a/core/include/core/error.hh +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include -#include - -struct Error -{ - std::string msg; -}; - -template -using Result = std::expected; diff --git a/core/include/core/result.hh b/core/include/core/result.hh new file mode 100644 index 0000000..f4ad6d4 --- /dev/null +++ b/core/include/core/result.hh @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +template +using Result = std::expected; +using Error = std::unexpected; +using Status = std::expected; + +template +Error err(std::string_view msg) +{ + return Error(std::in_place, msg); +} + +template +Error err(Result& result) +{ + return std::move(result).error(); +} diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 34fb0c6..9b25996 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -1,17 +1,13 @@ -find_package(Lua 5.4 REQUIRED) - add_library (game) target_include_directories ( game - PRIVATE ${LUA_INCLUDE_DIR} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) target_link_libraries ( game - PRIVATE ${LUA_LIBRARIES} - PUBLIC core gfx + PUBLIC core gfx sdk ) add_subdirectory (src) diff --git a/game/include/game/lua.hh b/game/include/game/lua.hh deleted file mode 100644 index a422356..0000000 --- a/game/include/game/lua.hh +++ /dev/null @@ -1,136 +0,0 @@ -#pragma once -#include "core/error.hh" -#include "core/types.hh" -#include -#include -#include -#include -#include -#include -#include - -// helper functions for Lua's C API -namespace lua { - [[nodiscard]] Error make_content_error(lua_State* L, int found, int expected); - void add_global_function(lua_State* L, char const* name, lua_CFunction fn); - void add_global_userdata(lua_State* L, char const* name, void* ptr); - void push_string(lua_State* L, std::string_view value); - - template - void push_fstring(lua_State* L, std::format_string fmt, Args&&... args) - { - auto str = std::format(fmt, std::forward(args)...); - lua_pushstring(L, str.data()); - } - - [[nodiscard]] std::string pop_string(lua_State* L); - [[nodiscard]] lua_Integer pop_integer(lua_State* L); - [[nodiscard]] lua_Number pop_number(lua_State* L); - [[nodiscard]] bool pop_boolean(lua_State* L); - - // Only pushes onto the stack if the expected type is found. - // Returns: absolute index of the new item, or a content error if the expected - // type wasn't found. Assumes: [L] is not null, [idx] is non-zero, value at - // [idx] is a table, [field] is non-zero. - [[nodiscard]] Result try_push_field(lua_State* L, int type, int idx, unsigned field); - - // Only pushes onto the stack if the expected type is found. - // Returns: absolute index of the new item, or a content error if the expected - // type wasn't found. Assumes: [L] is not null, [idx] is non-zero, value at - // [idx] is a table - [[nodiscard]] Result try_push_field(lua_State* L, int type, int idx, std::string_view field); - - [[nodiscard]] Result try_get_boolean(lua_State* L, int idx, unsigned field); - [[nodiscard]] Result try_get_boolean(lua_State* L, int idx, std::string_view field); - [[nodiscard]] Result try_get_string(lua_State* L, int idx, unsigned field); - [[nodiscard]] Result try_get_string(lua_State* L, int idx, std::string_view field); - [[nodiscard]] Result try_get_integer(lua_State* L, int idx, unsigned field); - [[nodiscard]] Result try_get_integer(lua_State* L, int idx, std::string_view field); - [[nodiscard]] Result try_get_number(lua_State* L, int idx, unsigned field); - [[nodiscard]] Result try_get_number(lua_State* L, int idx, std::string_view field); - - template - Result try_get(lua_State* L, int idx, std::string_view field) - { - if constexpr (std::same_as) return try_get_boolean(L, idx, field); - else if constexpr (std::integral) - return try_get_integer(L, idx, field).transform([](auto value) { - return static_cast(value); - }); - else if constexpr (std::floating_point) - return try_get_number(L, idx, field).transform([](auto value) { - return static_cast(value); - }); - else if constexpr (std::same_as) return try_get_string(L, idx, field); - else static_assert(false, "unsupported field type"); - } - - template - Result try_get(lua_State* L, int idx, int field) - { - if constexpr (std::same_as) return try_get_boolean(L, idx, field); - else if constexpr (std::integral) - return try_get_integer(L, idx, field).transform([](auto value) { - return static_cast(value); - }); - else if constexpr (std::floating_point) - return try_get_number(L, idx, field).transform([](auto value) { - return static_cast(value); - }); - else if constexpr (std::same_as) return try_get_string(L, idx, field); - else static_assert(false, "unsupported field type"); - } - - template - struct Field - { - std::string_view name; - std::variant< - f32 Type::*, - f64 Type::*, - u8 Type::*, - u16 Type::*, - u32 Type::*, - u64 Type::*, - i8 Type::*, - i16 Type::*, - i32 Type::*, - i64 Type::*, - bool Type::*, - std::string Type::*> - member; - }; - - // TODO: subtable fields? - /* This is supposed to reduce the amount of lua parsing code needed for - definitions constexpr Field fields[] = { { "x", &Rectangle::x }, { - "y", &Rectangle::y }, { "w", &Rectangle::w }, { "h", &Rectangle::h }, - }; - - auto result = try_get_fields(L, idx, fields); - */ - template - using Schema = std::span const>; - - template - Result try_get_fields(lua_State* L, int idx, Schema schema) - { - Type out; - for (auto const& field : schema) { - auto result = std::visit( - [&](auto member) -> Result { - using Member = std::remove_cvref_t; - auto value = try_get(L, idx, field.name); - if (!value) return std::unexpected(value.error()); - out.*member = std::move(*value); - return {}; - }, - field.member); - if (!result) return std::unexpected(result.error()); - } - return out; - } - - [[nodiscard]] Result - do_file(lua_State* L, std::string_view path, unsigned argc = 0u, unsigned resultc = 0u); -} // namespace lua diff --git a/game/include/game/sprite.hh b/game/include/game/sprite.hh index b7696ef..d060d08 100644 --- a/game/include/game/sprite.hh +++ b/game/include/game/sprite.hh @@ -1,12 +1,12 @@ #pragma once -#include "core/error.hh" +#include "core/result.hh" #include "game/types.hh" #include "gfx/color.hh" #include "gfx/rectangle.hh" #include struct TextureDef; -struct lua_State; +struct LuaTable; struct Sprite { @@ -15,7 +15,4 @@ struct Sprite Color tint; }; -namespace lua { - Result - try_get_sprite(CatalogWriter catalog, lua_State* L, int idx, std::string_view field); -} +Result parse_sprite(LuaTable const& table); diff --git a/game/src/CMakeLists.txt b/game/src/CMakeLists.txt index c05f6fd..2d5c64e 100644 --- a/game/src/CMakeLists.txt +++ b/game/src/CMakeLists.txt @@ -1,2 +1,2 @@ add_subdirectory (biome) -target_sources (game PRIVATE load.cc command.cc command-buffer.cc catalog.cc registry.cc entity.cc context.cc scene.cc lua.cc terrain.cc sprite.cc) +target_sources (game PRIVATE load.cc command.cc command-buffer.cc catalog.cc registry.cc entity.cc context.cc scene.cc terrain.cc sprite.cc) diff --git a/game/src/sprite.cc b/game/src/sprite.cc index d6e6b81..131ea05 100644 --- a/game/src/sprite.cc +++ b/game/src/sprite.cc @@ -1,44 +1,50 @@ -#include "game/sprite.hh" -#include "core/defer.hh" #include "game/catalog.hh" -#include "game/lua.hh" -#include "game/types.hh" +#include "game/context.hh" +#include "game/sprite.hh" +#include "sdk/node.hh" -static Result try_get_rectangle(lua_State* L, int idx, std::string_view field) +Result parse_rectangle(LuaNode const& node) { - auto rectangle = lua::try_push_field(L, LUA_TTABLE, idx, field); - if (!rectangle) return std::unexpected(rectangle.error()); - DEFER(lua_pop(L, 1)); - auto x = lua::try_get(L, *rectangle, 1); - if (!x) return std::unexpected(x.error()); - auto y = lua::try_get(L, *rectangle, 2); - if (!y) return std::unexpected(y.error()); - auto w = lua::try_get(L, *rectangle, 3); - if (!w) return std::unexpected(y.error()); - auto h = lua::try_get(L, *rectangle, 4); - if (!h) return std::unexpected(y.error()); - return Rectangle({ *x, *y }, { *w, *h }); + Rectangle rectangle; + + if (auto x = node.expect_number(1)) { + rectangle.origin.x = f32(*x); + } else return err(x); + + if (auto y = node.expect_number(2)) { + rectangle.origin.y = f32(*y); + } else return err(y); + + if (auto w = node.expect_number(3)) { + rectangle.extent.x = f32(*w); + } else return err(w); + + if (auto h = node.expect_number(4)) { + rectangle.extent.y = f32(*h); + } else return err(h); + + return rectangle; } -// sprite = { atlas = "path", x = N, y = N, color = N } -Result -lua::try_get_sprite(CatalogWriter catalog, lua_State* L, int idx, std::string_view field) +Result parse_sprite(LuaNode const& node) { - auto sprite = try_push_field(L, LUA_TTABLE, idx, field); - if (!sprite) return std::unexpected(sprite.error()); - DEFER(lua_pop(L, 1)); - auto atlas = try_get_string(L, *sprite, "atlas"); - if (!atlas) return std::unexpected(atlas.error()); - auto source = try_get_rectangle(L, *sprite, "source"); - if (!source) return std::unexpected(source.error()); - auto color = try_get(L, *sprite, "color"); - if (!color) return std::unexpected(color.error()); - auto texture = catalog.find(*atlas); - if (!texture) texture = catalog.emplace(*atlas, TextureDef{ *atlas }); - return Sprite{ - .atlas = texture, - .source = *source, - .tint = make_color_hex(*color), - }; + auto catalog = access_catalog(node.context()); + Sprite sprite; + + if (auto atlas = node.expect_string("atlas")) { + auto texture = catalog.find(*atlas); + if (!texture) texture = catalog.emplace(*atlas, TextureDef{ *atlas }); + sprite.atlas = texture; + } else return err(atlas); + + if (auto source = node.find("source").and_then(parse_rectangle)) { + sprite.source = *source; + } else return err(source); + + if (auto color = node.expect_integer("color")) { + sprite.tint = make_color_hex(*color); + } else return err(color); + + return sprite; } diff --git a/play.cc b/play.cc index c98f3c6..c7c69ac 100644 --- a/play.cc +++ b/play.cc @@ -1,22 +1,13 @@ #include "app/application.hh" #include "core/clock.hh" -#include "core/panic.hh" #include "core/random.hh" #include "game/biome/grassland.hh" #include "game/command-buffer.hh" -#include "game/component/pose.hh" -#include "game/context.hh" #include "game/entity.hh" #include "game/load.hh" #include "game/simulation.hh" -#include "game/types.hh" #include "sys/main.hh" -#include -#include -#include -#include #include -#include struct Runtime { diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt new file mode 100644 index 0000000..a80e48f --- /dev/null +++ b/sdk/CMakeLists.txt @@ -0,0 +1,17 @@ +find_package(Lua 5.4 REQUIRED) + +add_library (sdk) + +target_include_directories ( + sdk + PRIVATE ${LUA_INCLUDE_DIR} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_link_libraries ( + sdk + PRIVATE ${LUA_LIBRARIES} + PUBLIC core +) + +add_subdirectory (src) diff --git a/sdk/include/sdk/detail.hh b/sdk/include/sdk/detail.hh new file mode 100644 index 0000000..9ed57b6 --- /dev/null +++ b/sdk/include/sdk/detail.hh @@ -0,0 +1,46 @@ +#pragma once +#include "core/result.hh" +#include "core/types.hh" +#include "sdk/path.hh" +#include "sdk/types.hh" +#include +#include +#include + +struct lua_State; + +struct LuaValue; +struct LuaPath; +struct LuaNode; + +// TODO: LuaTable and LuaValue::expect_table() +// TODO: LuaFunction and LuaValue::expect_function() +// TODO: LuaValue::expect_userdata() + +namespace detail { + constexpr LuaType type(int t); + + Error make_type_error(lua_State*, LuaType expected, LuaType found); + + [[nodiscard]] Result try_resolve_path(lua_State*, i32 start, LuaPath const& path); + + LuaType push_key(lua_State*, LuaKey const& key); + + [[nodiscard]] LuaValue pop_value(lua_State*); + [[nodiscard]] bool pop_boolean(lua_State*); + [[nodiscard]] f64 pop_number(lua_State*); + [[nodiscard]] std::string pop_string(lua_State*); + + [[nodiscard]] Result try_pop_boolean(lua_State*); + [[nodiscard]] Result try_pop_number(lua_State*); + [[nodiscard]] Result try_pop_string(lua_State*); + + [[nodiscard]] Result find(LuaNode start, LuaPath); + + // void add_global_function(lua_State* L, char const* name, lua_CFunction fn); + // void add_global_userdata(lua_State* L, char const* name, void* ptr); + // void push_string(lua_State* L, std::string_view value); + + [[nodiscard]] Result + do_file(lua_State* L, std::string_view path, unsigned argc = 0u, unsigned resultc = 0u); +} diff --git a/sdk/include/sdk/node.hh b/sdk/include/sdk/node.hh new file mode 100644 index 0000000..a770c66 --- /dev/null +++ b/sdk/include/sdk/node.hh @@ -0,0 +1,64 @@ +#pragma once +#include "core/result.hh" +#include "core/types.hh" +#include "sdk/path.hh" +#include "sdk/types.hh" + +struct lua_State; +struct LuaNode; + +std::string describe(LuaNode const& value); + +struct LuaNode +{ + LuaNode() = delete; + LuaNode(LuaNode const&) = delete; + LuaNode& operator=(LuaNode const&) = delete; + + LuaNode(lua_State* L, i32 reference) noexcept; + LuaNode(LuaNode&&) noexcept; + LuaNode& operator=(LuaNode&&) noexcept; + ~LuaNode() noexcept; + + [[nodiscard]] Result find(LuaKey const&) const; + [[nodiscard]] Result find(LuaPath const& path) const; + + [[nodiscard]] bool as_boolean() const; + [[nodiscard]] f64 as_number() const; + [[nodiscard]] std::string as_string() const; + + [[nodiscard]] Result expect_boolean() const; + [[nodiscard]] Result expect_number() const; + [[nodiscard]] Result expect_string() const; + + [[nodiscard]] Result expect_boolean(LuaKey const&) const; + [[nodiscard]] Result expect_number(LuaKey const&) const; + [[nodiscard]] Result expect_string(LuaKey const&) const; + + [[nodiscard]] Result expect_boolean(LuaPath const&) const; + [[nodiscard]] Result expect_number(LuaPath const&) const; + [[nodiscard]] Result expect_string(LuaPath const&) const; + + template + [[nodiscard]] Result expect(Args&&... args) const + { + if constexpr (std::same_as) return expect_boolean(std::forward(args)...); + else if constexpr (std::integral || std::floating_point) + return expect_number(std::forward(args)...).transform([](auto value) { + return static_cast(value); + }); + else if constexpr (std::same_as) + return expect_string(std::forward(args)...); + else static_assert(false, "unsupported type"); + } + + [[nodiscard]] LuaType type() const noexcept { return type_; } + + [[nodiscard]] lua_State* handle() const noexcept { return handle_; } + +private: + lua_State* handle_; + i32 reference_; + LuaType type_; +}; + diff --git a/sdk/include/sdk/path.hh b/sdk/include/sdk/path.hh new file mode 100644 index 0000000..0153474 --- /dev/null +++ b/sdk/include/sdk/path.hh @@ -0,0 +1,73 @@ +#pragma once +#include "core/panic.hh" +#include "sdk/types.hh" +#include +#include +#include +#include + +struct LuaPath; + +std::string describe(LuaKey const& key); +std::string describe(LuaPath const& path); +std::string describe(std::span path); + +struct LuaPath +{ + LuaPath() = default; + LuaPath(LuaPath const&) = default; + LuaPath(LuaPath&&) noexcept = default; + LuaPath& operator=(LuaPath const&) = default; + LuaPath& operator=(LuaPath&&) noexcept = default; + ~LuaPath() noexcept = default; + + constexpr LuaPath(std::initializer_list keys) : keys_(keys) {} + + constexpr LuaPath(std::span keys) : keys_(keys.begin(), keys.end()) {} + + constexpr LuaKey const& operator[](size_t i) const { return keys_[i]; } + + LuaPath& operator+=(LuaPath const& rhs) + { + keys_.insert(keys_.end(), rhs.keys_.begin(), rhs.keys_.end()); + return *this; + } + + friend LuaPath operator+(LuaPath lhs, LuaPath const& rhs) + { + lhs += rhs; + return lhs; + } + + LuaPath& operator+=(LuaKey const& key) + { + keys_.push_back(key); + return *this; + } + + friend LuaPath operator+(LuaPath lhs, LuaKey const& rhs) + { + lhs += rhs; + return lhs; + } + + constexpr size_t size() const { return keys_.size(); } + + constexpr bool empty() const { return keys_.empty(); } + + constexpr auto begin() const { return keys_.begin(); } + + constexpr auto end() const { return keys_.end(); } + + constexpr std::span span() const { return keys_; } + + constexpr std::span span(size_t length) const + { + PRECONDITION(length < keys_.size()); + return { keys_.begin(), keys_.begin() + long(length) }; + } + +private: + std::vector keys_; +}; + diff --git a/sdk/include/sdk/schema.hh b/sdk/include/sdk/schema.hh new file mode 100644 index 0000000..ad80465 --- /dev/null +++ b/sdk/include/sdk/schema.hh @@ -0,0 +1,96 @@ +#pragma once +#include "core/types.hh" +#include "sdk/node.hh" +#include "sdk/path.hh" +#include +#include +#include +#include +#include + +template +using LuaParser = std::function; + +template +struct LuaField +{ + LuaKey key; + std::variant< + f32 Type::*, + f64 Type::*, + u8 Type::*, + u16 Type::*, + u32 Type::*, + u64 Type::*, + i8 Type::*, + i16 Type::*, + i32 Type::*, + i64 Type::*, + bool Type::*, + std::string Type::*, + LuaParser> + member; +}; + +template +struct LuaTraits +{}; + +struct Rectangle +{ + f32 x, y, w, h; +}; + +template +using LuaSchema = std::span const>; + +template <> +struct LuaTraits +{ + static constexpr LuaField schema[] = { + { "x", &Rectangle::x }, + { "y", &Rectangle::y }, + { "w", &Rectangle::w }, + { "h", &Rectangle::h }, + }; +}; + +template +concept LuaTable = requires { + { LuaTraits::schema } -> std::convertible_to>; +}; + +template +concept LuaValue = requires { + std::integral || std::floating_point || std::same_as; +}; + +template +LuaField make_nested_field(LuaKey key, Member Class::* member) +{ + return { + std::move(key), + [member](LuaNode const& node, Class& out) -> Status { + auto ok = parse(node, out.*member); + if (!ok) return ok; + return {}; + }, + }; +} + +template +Status expect_table(LuaNode const& node, Type& out) +{ + for (auto const& field : (LuaSchema)LuaTraits::schema) { + auto ok = std::visit( + [&](auto member) -> Status { + using Member = std::remove_cvref_t; + + return parse(node, field.key, out.*member); + }, + field.member); + if (!ok) return ok; + } + return {}; +} + diff --git a/sdk/include/sdk/table.hh b/sdk/include/sdk/table.hh new file mode 100644 index 0000000..9ff19d7 --- /dev/null +++ b/sdk/include/sdk/table.hh @@ -0,0 +1,128 @@ +#pragma once +#include "core/result.hh" +#include "core/types.hh" +#include "sdk/node.hh" +#include "sdk/types.hh" +#include +#include +#include + +template +struct LuaTraits +{}; + +// Not actually a "parser"? What about "compiler"? "Translator"? "Mapper"? +template +using LuaParser = std::function; + +template +struct LuaField +{ + LuaKey key; + std::variant< + f32 Type::*, + f64 Type::*, + u8 Type::*, + u16 Type::*, + u32 Type::*, + u64 Type::*, + i8 Type::*, + i16 Type::*, + i32 Type::*, + i64 Type::*, + bool Type::*, + std::string Type::*, + LuaParser> + member; +}; + +template +using LuaSchema = std::span const>; + +template +concept LuaTable = requires { + { LuaTraits::schema } -> std::convertible_to>; +}; + +template +concept LuaValue = requires { + std::integral || std::floating_point || std::same_as; +}; + +template +Status translate(LuaNode const& node, Type& out); + +template +Status translate_table(LuaNode const& node, LuaKey key, Type& out) +{ + for (auto const& field : (LuaSchema)LuaTraits::schema) { + auto ok = std::visit( + [&](auto member) -> Status { + // using Member = std::remove_cvref_t; + return translate(out.*member, field.key); + }, + field.member); + if (!ok) return ok; + } + return {}; +} + +template +Status translate_value(LuaNode const& node, LuaKey key, Type& out) +{ + if (auto value = node.expect(key)) out = std::move(*value); + else return Error(std::move(value).error()); + return {}; +} + +template +Status translate(LuaNode const& node, Type& out) +{ + if constexpr (LuaValue) return translate_value(node, out); + else if constexpr (LuaTable) return translate_table(node, out); + else static_assert(false, "unsupported type"); +} + +template +Status nested_table(Member Class::* member) +{ + return [member](LuaNode const& node, Class& out) -> Status { + auto ok = translate(node, out.*member); + if (!ok) return ok; + return {}; + }; +} + +struct Rectangle +{ + f32 x, y, w, h; +}; + +template <> +struct LuaTraits +{ + static constexpr LuaField schema[] = { + { "x", &Rectangle::x }, + { "y", &Rectangle::y }, + { "w", &Rectangle::w }, + { "h", &Rectangle::h }, + }; +}; + +struct Sprite +{ + std::string atlas; + Rectangle source; + u32 tint; +}; + +template <> +struct LuaTraits +{ + static constexpr LuaField schema[] = { + { "atlas", &Sprite::atlas }, + { "source", nested_table(&Sprite::source) }, + { "tint", &Sprite::tint }, + }; +}; + diff --git a/sdk/include/sdk/types.hh b/sdk/include/sdk/types.hh new file mode 100644 index 0000000..7eb52bd --- /dev/null +++ b/sdk/include/sdk/types.hh @@ -0,0 +1,31 @@ +#pragma once +#include "core/types.hh" +#include +#include +#include +#include + +using LuaKey = std::variant; + +using LuaQuery = std::variant>; + +enum class LuaType : int { + None = -1, + Nil = 0, + Boolean = 1, + LightUserData = 2, + Number = 3, + String = 4, + Table = 5, + Function = 6, + UserData = 7, + Thread = 8 +}; + +void describe(LuaType type); + +// template +// concept LuaValue = requires { +// std::integral || std::floating_point || std::same_as; +// }; + diff --git a/sdk/src/CMakeLists.txt b/sdk/src/CMakeLists.txt new file mode 100644 index 0000000..e4fc08b --- /dev/null +++ b/sdk/src/CMakeLists.txt @@ -0,0 +1 @@ +target_sources (sdk PRIVATE frame.cc value.cc table.cc query.cc object.cc) diff --git a/sdk/src/detail.cc b/sdk/src/detail.cc new file mode 100644 index 0000000..9b4a049 --- /dev/null +++ b/sdk/src/detail.cc @@ -0,0 +1,6 @@ +#include "sdk/detail.hh" +#include +#include + +// Context* LuaValue::context() const { return static_cast(lua_getextraspace(state_)); } + diff --git a/sdk/src/frame.cc b/sdk/src/frame.cc new file mode 100644 index 0000000..00523d8 --- /dev/null +++ b/sdk/src/frame.cc @@ -0,0 +1,66 @@ +#include "game/lua/table.hh" +#include "game/lua/util.hh" +#include +#include +#include + +std::string lua::path_string(LuaPath const& path, size_t len) +{ + std::stringstream out; + for (size_t i = 0; i < len; ++i) { + if (i > 0) out << '.'; + std::visit([&out](auto const& key) { out << key; }, path[i]); + } + return std::move(out).str(); +} + +std::string lua::path_string(LuaPath const& path) { return path_string(path, path.size()); } + +void lua::push_key(lua_State* L, LuaKey const& key) +{ + std::visit( + [L](auto const& value) { + using Type = std::remove_cvref_t; + if constexpr (std::same_as) + lua_pushlstring(L, value.data(), value.size()); + else if constexpr (std::integral) lua_pushinteger(L, value); + else static_assert(false, "unsupported lua key type"); + }, + key); +} + +Result lua::try_pop_boolean(lua_State* L) +{ + auto type = lua_type(L, -1); + if (type != LUA_TBOOLEAN) return lua::type_error(L, LUA_TBOOLEAN, type); + auto out = lua_toboolean(L, -1); + lua_pop(L, -1); + return out; +} + +Result lua::try_pop_number(lua_State* L) +{ + auto type = lua_type(L, -1); + if (type != LUA_TNUMBER) return lua::type_error(L, LUA_TNUMBER, type); + auto out = lua_tonumber(L, -1); + lua_pop(L, -1); + return out; +} + +Result lua::try_pop_string(lua_State* L) {} + +Result lua::try_pop_table(lua_State* L) {} + +void push_key(lua_State* L, LuaKey const& key) +{ + std::visit( + [L](auto const& value) { + using Type = std::remove_cvref_t; + if constexpr (std::same_as) + lua_pushlstring(L, value.data(), value.size()); + else if constexpr (std::integral) lua_pushinteger(L, value); + else static_assert(false, "unsupported lua key type"); + }, + key); +} + diff --git a/sdk/src/lua-builder.cc b/sdk/src/lua-builder.cc new file mode 100644 index 0000000..78f4fdd --- /dev/null +++ b/sdk/src/lua-builder.cc @@ -0,0 +1,59 @@ +#include "core/defer.hh" +#include "game/catalog.hh" +#include "game/lua.hh" +#include "game/sprite.hh" +#include "game/terrain.hh" +#include "game/types.hh" +#include + +// terrain "name" { sprite = {} } +static int parse_terrain_table(lua_State* L) +{ + INVARIANT(lua_islightuserdata(L, lua_upvalueindex(1))); + INVARIANT(lua_isstring(L, lua_upvalueindex(2))); + auto catalog = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto name = lua_tostring(L, lua_upvalueindex(2)); + do { + // arg 1: definition table + if (!lua_istable(L, 1)) { + lua::push_fstring( + L, "terrain '{}': expected table, found {}", name, lua_typename(L, lua_type(L, 1))); + break; + } + + int terrain = lua_gettop(L); + auto sprite = lua::try_get_sprite(*catalog, L, terrain, "sprite"); + if (!sprite) { + lua::push_string(L, sprite.error().msg); + break; + } + + DEFER(lua_pop(L, 1)); + catalog->emplace(name, *sprite); + return 0; + } while (false); + // this will unwind the stack without calling C++ destructors + return lua_error(L); +} + +static int build_terrain(lua_State* L) +{ + INVARIANT(lua_islightuserdata(L, lua_upvalueindex(1))); + // arg 1: name string + if (!lua_isstring(L, 1)) { + lua_pushstring( + L, std::format("terrain: expected string, found {}", lua_typename(L, lua_type(L, 1))).data()); + return lua_error(L); + } + lua_pushvalue(L, lua_upvalueindex(1)); + lua_pushvalue(L, 1); // name + lua_pushcclosure(L, parse_terrain_table, 2); + return 1; +} + +void lua::add_terrain_builder(lua_State* L, CatalogWriter& catalog) +{ + lua_pushlightuserdata(L, &catalog); + lua_pushcclosure(L, build_terrain, 1); + lua_setglobal(L, "terrain"); +} diff --git a/game/src/lua.cc b/sdk/src/lua.cc similarity index 80% rename from game/src/lua.cc rename to sdk/src/lua.cc index 6cc3fd5..fd5ddd2 100644 --- a/game/src/lua.cc +++ b/sdk/src/lua.cc @@ -1,12 +1,45 @@ -#include "game/lua.hh" #include "core/defer.hh" #include "core/panic.hh" +#include "game/lua.hh" #include #include +#include #include using namespace lua; +LuaReader::LuaReader(lua_State* L, i64 idx) noexcept : handle_{ L }, idx_{ idx } +{ + PRECONDITION(L != nullptr); + PRECONDITION(idx != 0); +} + +LuaReader::~LuaReader() noexcept +{ + DEBUG_ASSERT(lua_gettop(handle_) == idx_); + lua_settop(handle_, idx_); +} + +[[nodiscard]] Result LuaReader::expect_boolean() const; +[[nodiscard]] Result LuaReader::expect_integer() const; +[[nodiscard]] Result LuaReader::expect_number() const; +[[nodiscard]] Result LuaReader::expect_string() const; +[[nodiscard]] Result LuaReader::expect_table() const; + +// { +// lua_pushvalue(handle_, idx_); +// i64 out = lua_tointeger(handle_, idx_); +// lua_pop(handle_, 1); +// return out; +// } + +Result lua::try_get_boolean(lua_State* L, int table, std::string_view field) +{ + auto idx = try_push_field(L, LUA_TBOOLEAN, table, field); + if (!idx) return std::unexpected(idx.error()); + return pop_boolean(L); +} + static int handle_error(lua_State* L) { luaL_traceback(L, L, lua_tostring(L, -1), 1); diff --git a/sdk/src/node.cc b/sdk/src/node.cc new file mode 100644 index 0000000..182c321 --- /dev/null +++ b/sdk/src/node.cc @@ -0,0 +1,210 @@ +#include "core/panic.hh" +#include "sdk/detail.hh" +#include "sdk/node.hh" +#include "sdk/types.hh" +#include +#include +#include +#include + +static std::string type_error(lua_State* L, int expected, int found) +{ + return std::format("expected {} but found {}", lua_typename(L, expected), lua_typename(L, found)); +} + +static void push_key(lua_State* L, LuaKey const& key) +{ + std::visit( + [L](auto const& value) -> void { + using Type = std::remove_cvref_t; + if constexpr (std::same_as) lua_pushlstring(L, value.data(), value.size()); + else if constexpr (std::integral) lua_pushinteger(L, value); + else static_assert(false, "unsupported lua key type"); + }, + key); +} + +static Result try_resolve_path(lua_State* L, int start, LuaPath const& path) +{ + lua_pushvalue(L, start); + + for (size_t key = 0; key < path.size(); ++key) { + if (lua_istable(L, -1)) { + auto msg = std::format( + "{}: {}", describe(path.span(key + 1)), type_error(L, LUA_TTABLE, lua_type(L, -1))); + lua_pop(L, 1); + return Error(std::move(msg)); + } + push_key(L, path[key]); + lua_rawget(L, -2); + lua_replace(L, -2); + } + + if (lua_isnone(L, -1)) { + auto msg = std::format("{}: no value found", describe(path)); + lua_pop(L, 1); + return Error(std::move(msg)); + } + + return LuaType{ lua_type(L, -1) }; +} + +static Result try_get_field(lua_State* L, int idx, LuaKey const& key) +{ + if (lua_istable(L, idx)) return Error(type_error(L, LUA_TTABLE, lua_type(L, idx))); + push_key(L, key); + auto type = lua_rawget(L, idx); + return LuaType{ type }; +} + +static Result try_pop_boolean(lua_State* L) +{ + if (lua_isboolean(L, -1)) return Error(type_error(L, LUA_TBOOLEAN, lua_type(L, -1))); + auto boolean = lua_toboolean(L, -1); + lua_pop(L, 1); + return boolean; +} + +static Result try_pop_number(lua_State* L) +{ + if (lua_isnumber(L, -1)) return Error(type_error(L, LUA_TNUMBER, lua_type(L, -1))); + auto number = lua_tonumber(L, -1); + lua_pop(L, 1); + return number; +} + +static Result try_pop_string(lua_State* L) +{ + if (lua_isstring(L, -1)) return Error(type_error(L, LUA_TSTRING, lua_type(L, -1))); + auto string = lua_tostring(L, -1); + lua_pop(L, 1); + return string; +} + +LuaNode::LuaNode(lua_State* L, i32 reference) noexcept + : handle_{ L }, reference_{ reference }, type_{} +{ + PRECONDITION(L != nullptr); + type_ = LuaType{ lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_) }; + lua_pop(handle_, 1); +} + +LuaNode::LuaNode(LuaNode&& v) noexcept + : handle_{ v.handle_ }, reference_{ v.reference_ }, type_{ v.type_ } +{ + v.reference_ = LUA_REFNIL; + v.type_ = LuaType::None; +} + +LuaNode& LuaNode::operator=(LuaNode&& v) noexcept +{ + if (&v == this) return *this; + reference_ = v.reference_; + handle_ = v.handle_; + v.reference_ = LUA_REFNIL; + v.type_ = LuaType::None; + return *this; +} + +LuaNode::~LuaNode() noexcept +{ + luaL_unref(handle_, LUA_REGISTRYINDEX, reference_); + reference_ = LUA_REFNIL; + type_ = LuaType::None; +} + +Result LuaNode::find(LuaKey const& key) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_get_field(handle_, -1, key); + if (!ok) return err(ok); + auto ref = luaL_ref(handle_, LUA_REGISTRYINDEX); + return LuaNode(handle_, ref); +} + +Result LuaNode::find(LuaPath const& path) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_resolve_path(handle_, -1, path); + if (!ok) return err(ok); + auto ref = luaL_ref(handle_, LUA_REGISTRYINDEX); + return LuaNode(handle_, ref); +} + +Result LuaNode::expect_boolean() const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + return try_pop_boolean(handle_); +} + +Result LuaNode::expect_number() const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + return try_pop_number(handle_); +} + +Result LuaNode::expect_string() const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + return try_pop_string(handle_); +} + +Result LuaNode::expect_boolean(LuaKey const& key) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_get_field(handle_, -1, key); + if (!ok) return err(ok); + return try_pop_boolean(handle_); +} + +Result LuaNode::expect_number(LuaKey const& key) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_get_field(handle_, -1, key); + if (!ok) return err(ok); + return try_pop_number(handle_); +} + +Result LuaNode::expect_string(LuaKey const& key) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_get_field(handle_, -1, key); + if (!ok) return err(ok); + return try_pop_string(handle_); +} + +Result LuaNode::expect_boolean(LuaPath const& path) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_resolve_path(handle_, -1, path); + if (!ok) return err(ok); + return try_pop_boolean(handle_); +} + +Result LuaNode::expect_number(LuaPath const& path) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_resolve_path(handle_, -1, path); + if (!ok) return err(ok); + return try_pop_number(handle_); +} + +Result LuaNode::expect_string(LuaPath const& path) const +{ + PRECONDITION(reference_ != LUA_REFNIL); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); + auto ok = try_resolve_path(handle_, -1, path); + if (!ok) return err(ok); + return try_pop_string(handle_); +} + diff --git a/sdk/src/object.cc b/sdk/src/object.cc new file mode 100644 index 0000000..2c53e6e --- /dev/null +++ b/sdk/src/object.cc @@ -0,0 +1,67 @@ +#include "core/panic.hh" +#include "game/lua.hh" +#include "game/lua/object.hh" +#include "game/lua/util.hh" +#include +#include + +LuaObject::LuaObject(lua_State* L, i32 ref) noexcept : handle_{ L }, ref_{ ref } +{ + PRECONDITION(L != nullptr); +} + +LuaObject::LuaObject(LuaObject&& other) noexcept : handle_{ other.handle_ }, ref_{ other.ref_ } +{ + other.handle_ = nullptr; + other.ref_ = 0; +} + +LuaObject::~LuaObject() noexcept { luaL_unref(handle_, LUA_REGISTRYINDEX, ref_); } + +LuaObject& LuaObject::operator=(LuaObject&& other) noexcept +{ + if (&other == this) return *this; + handle_ = other.handle_; + ref_ = other.ref_; + other.handle_ = nullptr; + other.ref_ = 0; + return *this; +} + +Result LuaObject::expect_boolean() const +{ + lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); + return lua::try_pop_boolean(handle_); +} + +Result LuaObject::expect_number() const +{ + lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); + return lua::try_pop_number(handle_); +} + +Result LuaObject::expect_string() const +{ + lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); + return lua::try_pop_string(handle_); +} + +Result LuaObject::expect_table() const +{ + auto type = lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); + if (type != LUA_TTABLE) return lua::type_error(handle_, LUA_TTABLE, type); + return LuaTable(handle_, luaL_ref(handle_, LUA_REGISTRYINDEX)); +} + +i32 LuaObject::type() const noexcept +{ + auto type = lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); + lua_pop(handle_, -1); + return lua_; +} + +bool LuaObject::is_nil() const noexcept { return type() == LUA_TNIL; } + +bool LuaObject::is_none() const noexcept { return type() == LUA_TNONE; } + +LuaType push() const { return luaL_unref(handle(), LUA_REGISTRYINDEX, reference_); } diff --git a/sdk/src/path.cc b/sdk/src/path.cc new file mode 100644 index 0000000..624a866 --- /dev/null +++ b/sdk/src/path.cc @@ -0,0 +1,28 @@ +#include "sdk/path.hh" +#include +#include + +std::string describe(LuaKey const& key) +{ + return std::visit( + [](auto const& value) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::same_as) return value; + else if constexpr (std::integral) return std::to_string(value); + else static_assert(false, "unsupported lua key type"); + }, + key); +} + +std::string describe(LuaPath const& path) { return describe(path.span()); } + +std::string describe(std::span path) +{ + std::stringstream out; + for (size_t i = 0; i < path.size(); ++i) { + if (i > 0) out << '.'; + std::visit([&out](auto const& key) { out << key; }, path[i]); + } + return std::move(out).str(); +} + diff --git a/sdk/src/query.cc b/sdk/src/query.cc new file mode 100644 index 0000000..19e5938 --- /dev/null +++ b/sdk/src/query.cc @@ -0,0 +1,76 @@ +#include "core/panic.hh" +#include "sdk/query.hh" +#include + +LuaQuery::LuaQuery(lua_State* handle, i32 idx) noexcept : handle_{ handle }, root_{ idx }, path_() +{ + PRECONDITION(handle != nullptr); +} + +LuaQuery::LuaQuery(lua_State* handle, i32 idx, LuaKey key) noexcept + : handle_{ handle }, root_{ idx }, path_() +{} + +Result LuaQuery::resolve() const +{ + push(); + auto ref = luaL_ref(handle(), LUA_REGISTRYINDEX); + return LuaObject(handle(), ref); +} + +void LuaQuery::append(LuaKey const& key) { path_.push_back(key); } + +void LuaQuery::append(LuaPath const& path) +{ + for (auto const& key : path) append(key); +} + +LuaQuery LuaQuery::operator[](LuaKey key) const { return LuaQuery(*this, key); } + +LuaType LuaQuery::push() const +{ + auto ok = try_push(); + if (!ok) return LuaType::None; + return *ok; +} + +std::string describe_path(LuaPath const& path, size_t len) +{ + std::stringstream out; + for (size_t i = 0; i < len; ++i) { + if (i > 0) out << '.'; + std::visit([&out](auto const& key) { out << key; }, path[i]); + } + return std::move(out).str(); +} + +std::string describe_path(LuaPath const& path) { return path_string(path, path.size()); } + +// Attempts to push onto the stack the value at the end of the path. +Result try_resolve_path() const +{ + lua_pushvalue(L, start); + int idx = lua_gettop(L); + + for (size_t key = 0; key < path.size(); ++key) { + if (auto type = lua_type(L, idx); type != LUA_TTABLE) { + auto msg = std::format( + "{}: expected {} but found {}", describe_path(path, idx + 1), lua_typename(L, LUA_TTABLE), + lua_typename(L, type)); + lua_settop(L, idx - 1); + return Error(std::move(msg)); + } + lua::push_key(L, path[key]); + lua_rawget(L, idx); + lua_replace(L, idx); + } + + if (lua_type(L, idx) == LUA_TNONE) { + auto msg = std::format("{}: no value found", describe_path(path)); + lua_settop(L, idx - 1); + return Error(std::move(msg)); + } + + return lua_type(L, -1); +} + diff --git a/sdk/src/table.cc b/sdk/src/table.cc new file mode 100644 index 0000000..6226a7b --- /dev/null +++ b/sdk/src/table.cc @@ -0,0 +1,40 @@ +#include "core/panic.hh" +#include "game/lua/table.hh" +#include +#include + +static bool is_table_reference(lua_State* L, i64 ref) +{ + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + bool out = lua_istable(L, -1); + lua_pop(L, -1); + return out; +} + +LuaTable::LuaTable(lua_State* handle, i32 ref) noexcept : handle_{ handle } +{ + PRECONDITION(handle != nullptr); + PRECONDITION(is_table_reference(handle_, ref)); +} + +LuaTable::LuaTable(LuaTable&&) noexcept {} + +LuaTable& LuaTable::operator=(LuaTable&&) noexcept +{ + if (&other == this) return *this; + return *this; +} + +LuaTable::~LuaTable() noexcept +{ + // destroy owned reference +} + +LuaCursor LuaTable::operator[](LuaKey key) const noexcept { return { handle_, ref_ }; } + +size_t LuaTable::size() const noexcept { return 0u; } + +// auto type = lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); +// auto +// DEBUG_ASSERT(type == LUA_TTABLE); +// lua_rawgeti(handle_, -1, ) diff --git a/sdk/src/types.cc b/sdk/src/types.cc new file mode 100644 index 0000000..9dd60ee --- /dev/null +++ b/sdk/src/types.cc @@ -0,0 +1,31 @@ +#include "core/panic.hh" +#include "sdk/types.hh" +#include + +constexpr LuaType enumerate_lua_type(int t) +{ + switch (t) { + case LUA_TNONE: + return LuaType::None; + case LUA_TNIL: + return LuaType::Nil; + case LUA_TBOOLEAN: + return LuaType::Boolean; + case LUA_TLIGHTUSERDATA: + return LuaType::LightUserData; + case LUA_TNUMBER: + return LuaType::Number; + case LUA_TSTRING: + return LuaType::String; + case LUA_TTABLE: + return LuaType::Table; + case LUA_TFUNCTION: + return LuaType::Function; + case LUA_TUSERDATA: + return LuaType::UserData; + case LUA_TTHREAD: + return LuaType::Thread; + } + INVARIANT(false, "lua type not represented"); +} + From 2d46dbe4e820b39847c4cefaa2ab0a20fd9435f3 Mon Sep 17 00:00:00 2001 From: atpogue-bot <292650069+atpogue-bot@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:00:15 -0700 Subject: [PATCH 2/4] refactor(lua): finish table translation API Consolidate the WIP SDK around an owned Lua state, registry-backed nodes, path-aware type errors, deterministic table traversal, and erased schema field translators. Compile the existing terrain content through nested Sprite and Rectangle schemas, remove abandoned API experiments, and add focused Catch2 coverage. Co-authored-by: Andrew Pogue Assisted-by: ChatGPT --- app/include/app/application.hh | 3 +- core/include/core/result.hh | 14 +- game/include/game/simulation.hh | 2 +- game/include/game/sprite.hh | 5 +- game/include/game/terrain.hh | 5 +- game/src/load.cc | 68 +++++-- game/src/sprite.cc | 92 +++++---- game/src/terrain.cc | 61 +----- gfx/include/gfx/renderer.hh | 3 +- gfx/include/gfx/surface.hh | 2 +- gfx/src/renderer.cc | 8 +- gfx/src/surface.cc | 5 +- play.cc | 3 +- sdk/CMakeLists.txt | 4 + sdk/include/sdk/detail.hh | 46 ----- sdk/include/sdk/node.hh | 64 +++++-- sdk/include/sdk/path.hh | 16 +- sdk/include/sdk/schema.hh | 96 ---------- sdk/include/sdk/state.hh | 33 ++++ sdk/include/sdk/table.hh | 142 ++++++-------- sdk/include/sdk/types.hh | 40 ++-- sdk/src/CMakeLists.txt | 2 +- sdk/src/detail.cc | 6 - sdk/src/frame.cc | 66 ------- sdk/src/lua-builder.cc | 59 ------ sdk/src/lua.cc | 179 ------------------ sdk/src/node.cc | 326 ++++++++++++++++++++------------ sdk/src/object.cc | 67 ------- sdk/src/path.cc | 16 +- sdk/src/query.cc | 76 -------- sdk/src/state.cc | 188 ++++++++++++++++++ sdk/src/table.cc | 40 ---- sdk/src/types.cc | 31 --- sdk/tests/CMakeLists.txt | 15 ++ sdk/tests/lua.cc | 150 +++++++++++++++ sys/include/sys/window.hh | 3 +- sys/src/window.cc | 3 +- 37 files changed, 891 insertions(+), 1048 deletions(-) delete mode 100644 sdk/include/sdk/detail.hh delete mode 100644 sdk/include/sdk/schema.hh create mode 100644 sdk/include/sdk/state.hh delete mode 100644 sdk/src/detail.cc delete mode 100644 sdk/src/frame.cc delete mode 100644 sdk/src/lua-builder.cc delete mode 100644 sdk/src/lua.cc delete mode 100644 sdk/src/object.cc delete mode 100644 sdk/src/query.cc create mode 100644 sdk/src/state.cc delete mode 100644 sdk/src/table.cc delete mode 100644 sdk/src/types.cc create mode 100644 sdk/tests/CMakeLists.txt create mode 100644 sdk/tests/lua.cc diff --git a/app/include/app/application.hh b/app/include/app/application.hh index 8a50262..1449625 100644 --- a/app/include/app/application.hh +++ b/app/include/app/application.hh @@ -1,5 +1,5 @@ #pragma once -#include "core/error.hh" +#include "core/result.hh" #include "game/pilot.hh" #include "gfx/camera2D.hh" #include "gfx/renderer.hh" @@ -40,4 +40,3 @@ private: Renderer renderer_; std::vector textures_; }; - diff --git a/core/include/core/result.hh b/core/include/core/result.hh index f4ad6d4..39830b5 100644 --- a/core/include/core/result.hh +++ b/core/include/core/result.hh @@ -5,16 +5,4 @@ template using Result = std::expected; using Error = std::unexpected; -using Status = std::expected; - -template -Error err(std::string_view msg) -{ - return Error(std::in_place, msg); -} - -template -Error err(Result& result) -{ - return std::move(result).error(); -} +using Status = Result; diff --git a/game/include/game/simulation.hh b/game/include/game/simulation.hh index 81354dd..536c486 100644 --- a/game/include/game/simulation.hh +++ b/game/include/game/simulation.hh @@ -1,5 +1,5 @@ #pragma once -#include "core/error.hh" +#include "core/result.hh" #include "core/panic.hh" #include "game/catalog.hh" #include "game/chunk.hh" diff --git a/game/include/game/sprite.hh b/game/include/game/sprite.hh index d060d08..ee85d08 100644 --- a/game/include/game/sprite.hh +++ b/game/include/game/sprite.hh @@ -3,10 +3,9 @@ #include "game/types.hh" #include "gfx/color.hh" #include "gfx/rectangle.hh" -#include struct TextureDef; -struct LuaTable; +struct LuaNode; struct Sprite { @@ -15,4 +14,4 @@ struct Sprite Color tint; }; -Result parse_sprite(LuaTable const& table); +[[nodiscard]] Result parse_sprite(LuaNode const& node, CatalogWriter catalog); diff --git a/game/include/game/terrain.hh b/game/include/game/terrain.hh index fa1ea12..91b814c 100644 --- a/game/include/game/terrain.hh +++ b/game/include/game/terrain.hh @@ -1,12 +1,13 @@ #pragma once +#include "core/result.hh" #include "game/sprite.hh" #include "game/types.hh" -struct lua_State; +struct LuaNode; struct Terrain { Sprite sprite; }; -namespace lua { void add_terrain_builder(lua_State* L, CatalogWriter& catalog); } +[[nodiscard]] Result parse_terrain(LuaNode const& node, CatalogWriter catalog); diff --git a/game/src/load.cc b/game/src/load.cc index e59971f..32ed295 100644 --- a/game/src/load.cc +++ b/game/src/load.cc @@ -1,25 +1,69 @@ #include "game/load.hh" +#include "core/result.hh" #include "game/catalog.hh" #include "game/context.hh" -#include "game/lua.hh" #include "game/terrain.hh" +#include "sdk/node.hh" +#include "sdk/state.hh" #include -#include +#include +#include +#include +#include -bool load_content(LoadContext ctx, std::string_view path) +namespace { + +Status compile_terrain(LuaNode const& table, CatalogWriter catalog) +{ + Result> keys = table.keys(); + if (!keys) return Error(std::move(keys).error()); + + for (LuaKey const& key : *keys) { + std::string const* label = std::get_if(&key); + if (label == nullptr) { + return Error(describe(table) + ": terrain definition labels must be strings"); + } + + Result definition = table.find(key); + if (!definition) return Error(std::move(definition).error()); + + Result terrain = parse_terrain(*definition, catalog); + if (!terrain) return Error(std::move(terrain).error()); + catalog.emplace(*label, std::move(*terrain)); + } + return {}; +} + +Status compile_content(LuaState const& state, LoadContext context) { - auto catalog = access_catalog(ctx); - lua_State* L = luaL_newstate(); - ASSERT(L != nullptr, "failed to create lua state"); - lua::add_terrain_builder(L, catalog); - auto result = lua::do_file(L, path); + Result content = state.global("content"); + if (!content) return Error(std::move(content).error()); + + Result terrain = content->find("terrain"); + if (!terrain) return Error(std::move(terrain).error()); + return compile_terrain(*terrain, access_catalog(context)); +} + +} // namespace + +bool load_content(LoadContext context, std::string_view path) +{ + Result state = LuaState::create(); + if (!state) { + SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not initialize Lua: %s", state.error().c_str()); + return false; + } + + Status result = state->create_table({ "content", "terrain" }); + if (result) result = state->do_file(path); + // TODO: Normalize metatable-backed content after all content packages have executed. + if (result) result = compile_content(*state, context); if (!result) { + std::string const terminated_path(path); SDL_LogError( - SDL_LOG_CATEGORY_ERROR, "Error in %s:\n\t %s", path.data(), // NOLINT - result.error().msg.data()); + SDL_LOG_CATEGORY_ERROR, "Error in %s:\n\t%s", terminated_path.c_str(), + result.error().c_str()); return false; } - INVARIANT(lua_gettop(L) == 0 && "unprocessed items still on the lua stack"); - lua_close(L); return true; } diff --git a/game/src/sprite.cc b/game/src/sprite.cc index 131ea05..8224c9f 100644 --- a/game/src/sprite.cc +++ b/game/src/sprite.cc @@ -1,50 +1,66 @@ -#include "game/catalog.hh" -#include "game/context.hh" #include "game/sprite.hh" -#include "sdk/node.hh" - -Result parse_rectangle(LuaNode const& node) -{ - Rectangle rectangle; - - if (auto x = node.expect_number(1)) { - rectangle.origin.x = f32(*x); - } else return err(x); +#include "game/catalog.hh" +#include "game/texture.hh" +#include "sdk/table.hh" +#include +#include - if (auto y = node.expect_number(2)) { - rectangle.origin.y = f32(*y); - } else return err(y); +namespace { - if (auto w = node.expect_number(3)) { - rectangle.extent.x = f32(*w); - } else return err(w); +struct RectangleData +{ + f32 x; + f32 y; + f32 width; + f32 height; +}; - if (auto h = node.expect_number(4)) { - rectangle.extent.y = f32(*h); - } else return err(h); +struct SpriteData +{ + std::string atlas; + RectangleData source; + u32 color; +}; - return rectangle; -} +} // namespace -Result parse_sprite(LuaNode const& node) +template <> +struct LuaTraits { - auto catalog = access_catalog(node.context()); - Sprite sprite; + static constexpr LuaField schema[] = { + lua_field<&RectangleData::x>(1), + lua_field<&RectangleData::y>(2), + lua_field<&RectangleData::width>(3), + lua_field<&RectangleData::height>(4), + }; +}; - if (auto atlas = node.expect_string("atlas")) { - auto texture = catalog.find(*atlas); - if (!texture) texture = catalog.emplace(*atlas, TextureDef{ *atlas }); - sprite.atlas = texture; - } else return err(atlas); +template <> +struct LuaTraits +{ + static constexpr LuaField schema[] = { + lua_field<&SpriteData::atlas>("atlas"), + lua_field<&SpriteData::source>("source"), + lua_field<&SpriteData::color>("color"), + }; +}; - if (auto source = node.find("source").and_then(parse_rectangle)) { - sprite.source = *source; - } else return err(source); +Result parse_sprite(LuaNode const& node, CatalogWriter catalog) +{ + Result data = translate(node); + if (!data) return Error(std::move(data).error()); - if (auto color = node.expect_integer("color")) { - sprite.tint = make_color_hex(*color); - } else return err(color); + Token texture = catalog.find(data->atlas); + if (!texture) { + texture = catalog.emplace(data->atlas, TextureDef{ data->atlas }); + } - return sprite; + return Sprite{ + .atlas = texture, + .source = Rectangle{ + .origin = { data->source.x, data->source.y }, + .extent = { data->source.width, data->source.height }, + }, + .tint = make_color_hex(data->color), + }; } - diff --git a/game/src/terrain.cc b/game/src/terrain.cc index c80f781..79444a2 100644 --- a/game/src/terrain.cc +++ b/game/src/terrain.cc @@ -1,59 +1,14 @@ #include "game/terrain.hh" -#include "core/defer.hh" #include "game/catalog.hh" -#include "game/lua.hh" -#include "game/sprite.hh" -#include "game/types.hh" -#include +#include "sdk/node.hh" +#include -// terrain "name" { sprite = {} } -static int parse_terrain_table(lua_State* L) +Result parse_terrain(LuaNode const& node, CatalogWriter catalog) { - INVARIANT(lua_islightuserdata(L, lua_upvalueindex(1))); - INVARIANT(lua_isstring(L, lua_upvalueindex(2))); - auto catalog = static_cast(lua_touserdata(L, lua_upvalueindex(1))); - auto name = lua_tostring(L, lua_upvalueindex(2)); - do { - // arg 1: definition table - if (!lua_istable(L, 1)) { - lua::push_fstring( - L, "terrain '{}': expected table, found {}", name, lua_typename(L, lua_type(L, 1))); - break; - } + Result sprite_node = node.find("sprite"); + if (!sprite_node) return Error(std::move(sprite_node).error()); - int terrain = lua_gettop(L); - auto sprite = lua::try_get_sprite(*catalog, L, terrain, "sprite"); - if (!sprite) { - lua::push_string(L, sprite.error().msg); - break; - } - - DEFER(lua_pop(L, 1)); - catalog->emplace(name, *sprite); - return 0; - } while (false); - // this will unwind the stack without calling C++ destructors - return lua_error(L); -} - -static int build_terrain(lua_State* L) -{ - INVARIANT(lua_islightuserdata(L, lua_upvalueindex(1))); - // arg 1: name string - if (!lua_isstring(L, 1)) { - lua_pushstring( - L, std::format("terrain: expected string, found {}", lua_typename(L, lua_type(L, 1))).data()); - return lua_error(L); - } - lua_pushvalue(L, lua_upvalueindex(1)); - lua_pushvalue(L, 1); // name - lua_pushcclosure(L, parse_terrain_table, 2); - return 1; -} - -void lua::add_terrain_builder(lua_State* L, CatalogWriter& catalog) -{ - lua_pushlightuserdata(L, &catalog); - lua_pushcclosure(L, build_terrain, 1); - lua_setglobal(L, "terrain"); + Result sprite = parse_sprite(*sprite_node, catalog); + if (!sprite) return Error(std::move(sprite).error()); + return Terrain{ .sprite = std::move(*sprite) }; } diff --git a/gfx/include/gfx/renderer.hh b/gfx/include/gfx/renderer.hh index 07ebbbd..df3b072 100644 --- a/gfx/include/gfx/renderer.hh +++ b/gfx/include/gfx/renderer.hh @@ -3,7 +3,7 @@ // TODO: migration to SDL3 GPU for shader support // TODO: headless renderer -#include "core/error.hh" +#include "core/result.hh" #include "gfx/color.hh" #include "gfx/rectangle.hh" @@ -49,4 +49,3 @@ struct Renderer private: SDL_Renderer* handle_ = nullptr; }; - diff --git a/gfx/include/gfx/surface.hh b/gfx/include/gfx/surface.hh index 3924286..e6c712d 100644 --- a/gfx/include/gfx/surface.hh +++ b/gfx/include/gfx/surface.hh @@ -1,5 +1,5 @@ #pragma once -#include "core/error.hh" +#include "core/result.hh" #include struct Surface; diff --git a/gfx/src/renderer.cc b/gfx/src/renderer.cc index 521f741..7d864b6 100644 --- a/gfx/src/renderer.cc +++ b/gfx/src/renderer.cc @@ -42,7 +42,7 @@ Renderer::~Renderer() noexcept { SDL_DestroyRenderer(handle_); } Result create_renderer(Window& window) { SDL_Renderer* handle = SDL_CreateRenderer(window.handle(), nullptr); - if (!handle) return std::unexpected(SDL_GetError()); + if (!handle) return Error(SDL_GetError()); if (!SDL_SetRenderVSync(handle, 1)) std::println("[Warning] Could not enable vsync: %s", SDL_GetError()); return Renderer(handle); @@ -79,7 +79,7 @@ Result Renderer::create_texture(int w, int h) { SDL_Texture* texture = SDL_CreateTexture(handle_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC, w, h); - if (!texture) return std::unexpected(SDL_GetError()); + if (!texture) return Error(SDL_GetError()); if (!SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST)) std::println("[Warning] Could not set scale mode: %s", SDL_GetError()); return Texture(handle_, texture); @@ -88,7 +88,7 @@ Result Renderer::create_texture(int w, int h) Result Renderer::create_texture(Surface& surface) { SDL_Texture* texture = SDL_CreateTextureFromSurface(handle_, surface.handle()); - if (!texture) return std::unexpected(SDL_GetError()); + if (!texture) return Error(SDL_GetError()); if (!SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST)) std::println("[Warning] Could not set scale mode: %s", SDL_GetError()); return Texture(handle_, texture); @@ -98,7 +98,7 @@ Result Renderer::create_target(int width, int height) { auto target = SDL_CreateTexture(handle_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height); - if (!target) return std::unexpected(SDL_GetError()); + if (!target) return Error(SDL_GetError()); if (!SDL_SetTextureScaleMode(target, SDL_SCALEMODE_NEAREST)) std::println("[Warning] Could not set scale mode: %s", SDL_GetError()); return Texture(handle_, target); diff --git a/gfx/src/surface.cc b/gfx/src/surface.cc index b18ec59..8f4b17e 100644 --- a/gfx/src/surface.cc +++ b/gfx/src/surface.cc @@ -27,10 +27,9 @@ Result load_image(std::string_view path) if (path.ends_with(".png")) surface = SDL_LoadPNG(std::string(path).c_str()); else if (path.ends_with(".bmp")) surface = SDL_LoadBMP(std::string(path).c_str()); else { - return std::unexpected( + return Error( std::format("Unsupported file type (expected BMP or PNG): {}", path)); } - if (!surface) { return std::unexpected(SDL_GetError()); } + if (!surface) { return Error(SDL_GetError()); } return Surface(surface); } - diff --git a/play.cc b/play.cc index c7c69ac..e62d556 100644 --- a/play.cc +++ b/play.cc @@ -46,7 +46,7 @@ Runtime* start(int /*argc*/, char* /*argv*/[]) } load_chunk(*ctx, state->sim.scene()); if (auto result = state->app.load(*ctx, load_player(*ctx)); !result) { - std::println("Failed to open application: {}", result.error().msg); + std::println("Failed to open application: {}", result.error()); return nullptr; } return state.release(); @@ -85,4 +85,3 @@ void iterate(Runtime& state) void handle_event(Runtime& state, SDL_Event const& event) { state.app.handle_event(event); } void quit(Runtime* state) { delete state; } - diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index a80e48f..32a079d 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -15,3 +15,7 @@ target_link_libraries ( ) add_subdirectory (src) + +if (BUILD_TESTS) + add_subdirectory (tests) +endif () diff --git a/sdk/include/sdk/detail.hh b/sdk/include/sdk/detail.hh deleted file mode 100644 index 9ed57b6..0000000 --- a/sdk/include/sdk/detail.hh +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "core/types.hh" -#include "sdk/path.hh" -#include "sdk/types.hh" -#include -#include -#include - -struct lua_State; - -struct LuaValue; -struct LuaPath; -struct LuaNode; - -// TODO: LuaTable and LuaValue::expect_table() -// TODO: LuaFunction and LuaValue::expect_function() -// TODO: LuaValue::expect_userdata() - -namespace detail { - constexpr LuaType type(int t); - - Error make_type_error(lua_State*, LuaType expected, LuaType found); - - [[nodiscard]] Result try_resolve_path(lua_State*, i32 start, LuaPath const& path); - - LuaType push_key(lua_State*, LuaKey const& key); - - [[nodiscard]] LuaValue pop_value(lua_State*); - [[nodiscard]] bool pop_boolean(lua_State*); - [[nodiscard]] f64 pop_number(lua_State*); - [[nodiscard]] std::string pop_string(lua_State*); - - [[nodiscard]] Result try_pop_boolean(lua_State*); - [[nodiscard]] Result try_pop_number(lua_State*); - [[nodiscard]] Result try_pop_string(lua_State*); - - [[nodiscard]] Result find(LuaNode start, LuaPath); - - // void add_global_function(lua_State* L, char const* name, lua_CFunction fn); - // void add_global_userdata(lua_State* L, char const* name, void* ptr); - // void push_string(lua_State* L, std::string_view value); - - [[nodiscard]] Result - do_file(lua_State* L, std::string_view path, unsigned argc = 0u, unsigned resultc = 0u); -} diff --git a/sdk/include/sdk/node.hh b/sdk/include/sdk/node.hh index a770c66..7ac3d6b 100644 --- a/sdk/include/sdk/node.hh +++ b/sdk/include/sdk/node.hh @@ -3,6 +3,11 @@ #include "core/types.hh" #include "sdk/path.hh" #include "sdk/types.hh" +#include +#include +#include +#include +#include struct lua_State; struct LuaNode; @@ -15,7 +20,6 @@ struct LuaNode LuaNode(LuaNode const&) = delete; LuaNode& operator=(LuaNode const&) = delete; - LuaNode(lua_State* L, i32 reference) noexcept; LuaNode(LuaNode&&) noexcept; LuaNode& operator=(LuaNode&&) noexcept; ~LuaNode() noexcept; @@ -23,42 +27,74 @@ struct LuaNode [[nodiscard]] Result find(LuaKey const&) const; [[nodiscard]] Result find(LuaPath const& path) const; - [[nodiscard]] bool as_boolean() const; - [[nodiscard]] f64 as_number() const; - [[nodiscard]] std::string as_string() const; - [[nodiscard]] Result expect_boolean() const; + [[nodiscard]] Result expect_integer() const; [[nodiscard]] Result expect_number() const; [[nodiscard]] Result expect_string() const; [[nodiscard]] Result expect_boolean(LuaKey const&) const; + [[nodiscard]] Result expect_integer(LuaKey const&) const; [[nodiscard]] Result expect_number(LuaKey const&) const; [[nodiscard]] Result expect_string(LuaKey const&) const; [[nodiscard]] Result expect_boolean(LuaPath const&) const; + [[nodiscard]] Result expect_integer(LuaPath const&) const; [[nodiscard]] Result expect_number(LuaPath const&) const; [[nodiscard]] Result expect_string(LuaPath const&) const; - template - [[nodiscard]] Result expect(Args&&... args) const + template + [[nodiscard]] Result expect() const { - if constexpr (std::same_as) return expect_boolean(std::forward(args)...); - else if constexpr (std::integral || std::floating_point) - return expect_number(std::forward(args)...).transform([](auto value) { + if constexpr (std::same_as) return expect_boolean(); + else if constexpr (std::integral) { + Result value = expect_integer(); + if (!value) return Error(std::move(value).error()); + if (!std::in_range(*value)) { + return Error(describe(*this) + ": integer is outside the destination type's range"); + } + return static_cast(*value); + } else if constexpr (std::floating_point) + return expect_number().transform([](auto value) { return static_cast(value); }); else if constexpr (std::same_as) - return expect_string(std::forward(args)...); - else static_assert(false, "unsupported type"); + return expect_string(); + else static_assert(always_false, "unsupported type"); + } + + template + [[nodiscard]] Result expect(LuaKey const& key) const + { + Result value = find(key); + if (!value) return Error(std::move(value).error()); + return value->expect(); + } + + template + [[nodiscard]] Result expect(LuaPath const& path) const + { + Result value = find(path); + if (!value) return Error(std::move(value).error()); + return value->expect(); } + [[nodiscard]] Result> keys() const; + [[nodiscard]] LuaType type() const noexcept { return type_; } - [[nodiscard]] lua_State* handle() const noexcept { return handle_; } + [[nodiscard]] LuaPath const& path() const noexcept { return path_; } private: + friend struct LuaState; + + template + static constexpr bool always_false = false; + + LuaNode(std::shared_ptr owner, i32 reference, LuaPath path) noexcept; + + std::shared_ptr owner_; lua_State* handle_; i32 reference_; LuaType type_; + LuaPath path_; }; - diff --git a/sdk/include/sdk/path.hh b/sdk/include/sdk/path.hh index 0153474..1cd116d 100644 --- a/sdk/include/sdk/path.hh +++ b/sdk/include/sdk/path.hh @@ -1,5 +1,6 @@ #pragma once #include "core/panic.hh" +#include "core/types.hh" #include "sdk/types.hh" #include #include @@ -25,7 +26,7 @@ struct LuaPath constexpr LuaPath(std::span keys) : keys_(keys.begin(), keys.end()) {} - constexpr LuaKey const& operator[](size_t i) const { return keys_[i]; } + constexpr LuaKey const& operator[](u32 i) const { return keys_[i]; } LuaPath& operator+=(LuaPath const& rhs) { @@ -51,23 +52,22 @@ struct LuaPath return lhs; } - constexpr size_t size() const { return keys_.size(); } + constexpr u32 size() const { return static_cast(keys_.size()); } constexpr bool empty() const { return keys_.empty(); } - constexpr auto begin() const { return keys_.begin(); } + constexpr std::vector::const_iterator begin() const { return keys_.begin(); } - constexpr auto end() const { return keys_.end(); } + constexpr std::vector::const_iterator end() const { return keys_.end(); } constexpr std::span span() const { return keys_; } - constexpr std::span span(size_t length) const + constexpr std::span span(u32 length) const { - PRECONDITION(length < keys_.size()); - return { keys_.begin(), keys_.begin() + long(length) }; + PRECONDITION(length <= keys_.size()); + return { keys_.begin(), length }; } private: std::vector keys_; }; - diff --git a/sdk/include/sdk/schema.hh b/sdk/include/sdk/schema.hh deleted file mode 100644 index ad80465..0000000 --- a/sdk/include/sdk/schema.hh +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once -#include "core/types.hh" -#include "sdk/node.hh" -#include "sdk/path.hh" -#include -#include -#include -#include -#include - -template -using LuaParser = std::function; - -template -struct LuaField -{ - LuaKey key; - std::variant< - f32 Type::*, - f64 Type::*, - u8 Type::*, - u16 Type::*, - u32 Type::*, - u64 Type::*, - i8 Type::*, - i16 Type::*, - i32 Type::*, - i64 Type::*, - bool Type::*, - std::string Type::*, - LuaParser> - member; -}; - -template -struct LuaTraits -{}; - -struct Rectangle -{ - f32 x, y, w, h; -}; - -template -using LuaSchema = std::span const>; - -template <> -struct LuaTraits -{ - static constexpr LuaField schema[] = { - { "x", &Rectangle::x }, - { "y", &Rectangle::y }, - { "w", &Rectangle::w }, - { "h", &Rectangle::h }, - }; -}; - -template -concept LuaTable = requires { - { LuaTraits::schema } -> std::convertible_to>; -}; - -template -concept LuaValue = requires { - std::integral || std::floating_point || std::same_as; -}; - -template -LuaField make_nested_field(LuaKey key, Member Class::* member) -{ - return { - std::move(key), - [member](LuaNode const& node, Class& out) -> Status { - auto ok = parse(node, out.*member); - if (!ok) return ok; - return {}; - }, - }; -} - -template -Status expect_table(LuaNode const& node, Type& out) -{ - for (auto const& field : (LuaSchema)LuaTraits::schema) { - auto ok = std::visit( - [&](auto member) -> Status { - using Member = std::remove_cvref_t; - - return parse(node, field.key, out.*member); - }, - field.member); - if (!ok) return ok; - } - return {}; -} - diff --git a/sdk/include/sdk/state.hh b/sdk/include/sdk/state.hh new file mode 100644 index 0000000..e9cefd7 --- /dev/null +++ b/sdk/include/sdk/state.hh @@ -0,0 +1,33 @@ +#pragma once +#include "core/result.hh" +#include "sdk/node.hh" +#include "sdk/path.hh" +#include +#include + +struct lua_State; + +struct LuaState +{ + LuaState() = delete; + LuaState(LuaState const&) = delete; + LuaState& operator=(LuaState const&) = delete; + LuaState(LuaState&&) noexcept; + LuaState& operator=(LuaState&&) noexcept; + ~LuaState() noexcept; + + [[nodiscard]] static Result create(); + + [[nodiscard]] Status create_table(LuaPath const& path); + [[nodiscard]] Status do_file(std::string_view path); + [[nodiscard]] Status do_string(std::string_view source, std::string_view name = "content"); + [[nodiscard]] Result global(std::string_view name) const; + +private: + explicit LuaState(std::shared_ptr owner) noexcept; + + [[nodiscard]] Status execute(int load_status); + + std::shared_ptr owner_; + lua_State* handle_; +}; diff --git a/sdk/include/sdk/table.hh b/sdk/include/sdk/table.hh index 9ff19d7..de7c6f0 100644 --- a/sdk/include/sdk/table.hh +++ b/sdk/include/sdk/table.hh @@ -1,39 +1,38 @@ #pragma once #include "core/result.hh" -#include "core/types.hh" #include "sdk/node.hh" #include "sdk/types.hh" #include -#include #include +#include +#include +#include template struct LuaTraits {}; -// Not actually a "parser"? What about "compiler"? "Translator"? "Mapper"? +template +struct MemberPointerTraits; + +template +struct MemberPointerTraits +{ + using class_type = Class; + using member_type = Member; +}; + template -using LuaParser = std::function; +using LuaTranslator = Status (*)(LuaNode const&, LuaKey const&, Type&); +// A field erases its member type behind one callback. The callback generated by lua_field still +// knows the exact pointer-to-member type and can therefore dispatch to primitive or table +// translation without adding that member type to a variant. template struct LuaField { - LuaKey key; - std::variant< - f32 Type::*, - f64 Type::*, - u8 Type::*, - u16 Type::*, - u32 Type::*, - u64 Type::*, - i8 Type::*, - i16 Type::*, - i32 Type::*, - i64 Type::*, - bool Type::*, - std::string Type::*, - LuaParser> - member; + LuaKey key; + LuaTranslator translate; }; template @@ -45,84 +44,61 @@ concept LuaTable = requires { }; template -concept LuaValue = requires { - std::integral || std::floating_point || std::same_as; -}; +concept LuaValue = std::same_as || std::integral || std::floating_point + || std::same_as; -template +template Status translate(LuaNode const& node, Type& out); template -Status translate_table(LuaNode const& node, LuaKey key, Type& out) +Status translate(LuaNode const& node, Type& out); + +template +constexpr auto lua_field(LuaKey key) { - for (auto const& field : (LuaSchema)LuaTraits::schema) { - auto ok = std::visit( - [&](auto member) -> Status { - // using Member = std::remove_cvref_t; - return translate(out.*member, field.key); - }, - field.member); - if (!ok) return ok; - } - return {}; + using MemberPointer = decltype(Member); + using Class = typename MemberPointerTraits::class_type; + + return LuaField{ + .key = std::move(key), + .translate = [](LuaNode const& node, LuaKey const& field, Class& out) { + Result value = node.find(field); + if (!value) return Status(Error(std::move(value).error())); + return translate(*value, out.*Member); + }, + }; } template -Status translate_value(LuaNode const& node, LuaKey key, Type& out) +Status translate(LuaNode const& node, Type& out) { - if (auto value = node.expect(key)) out = std::move(*value); - else return Error(std::move(value).error()); + Result value = node.expect(); + if (!value) return Error(std::move(value).error()); + out = std::move(*value); return {}; } -template +template Status translate(LuaNode const& node, Type& out) { - if constexpr (LuaValue) return translate_value(node, out); - else if constexpr (LuaTable) return translate_table(node, out); - else static_assert(false, "unsupported type"); -} + if (node.type() != LuaType::Table) { + return Error( + describe(node) + ": expected table but found " + std::string(describe(node.type()))); + } -template -Status nested_table(Member Class::* member) -{ - return [member](LuaNode const& node, Class& out) -> Status { - auto ok = translate(node, out.*member); - if (!ok) return ok; - return {}; - }; + for (LuaField const& field : LuaTraits::schema) { + Status result = field.translate(node, field.key, out); + if (!result) return result; + } + return {}; } -struct Rectangle -{ - f32 x, y, w, h; -}; - -template <> -struct LuaTraits -{ - static constexpr LuaField schema[] = { - { "x", &Rectangle::x }, - { "y", &Rectangle::y }, - { "w", &Rectangle::w }, - { "h", &Rectangle::h }, - }; -}; - -struct Sprite -{ - std::string atlas; - Rectangle source; - u32 tint; -}; - -template <> -struct LuaTraits +template +requires std::default_initializable && (LuaValue || LuaTable) +[[nodiscard]] Result translate(LuaNode const& node) { - static constexpr LuaField schema[] = { - { "atlas", &Sprite::atlas }, - { "source", nested_table(&Sprite::source) }, - { "tint", &Sprite::tint }, - }; -}; - + Type out{}; + Status result = translate(node, out); + if (!result) return Error(std::move(result).error()); + return out; +} diff --git a/sdk/include/sdk/types.hh b/sdk/include/sdk/types.hh index 7eb52bd..9d14099 100644 --- a/sdk/include/sdk/types.hh +++ b/sdk/include/sdk/types.hh @@ -1,13 +1,10 @@ #pragma once #include "core/types.hh" -#include -#include #include +#include #include -using LuaKey = std::variant; - -using LuaQuery = std::variant>; +using LuaKey = std::variant; enum class LuaType : int { None = -1, @@ -22,10 +19,29 @@ enum class LuaType : int { Thread = 8 }; -void describe(LuaType type); - -// template -// concept LuaValue = requires { -// std::integral || std::floating_point || std::same_as; -// }; - +[[nodiscard]] constexpr std::string_view describe(LuaType type) noexcept +{ + switch (type) { + case LuaType::None: + return "none"; + case LuaType::Nil: + return "nil"; + case LuaType::Boolean: + return "boolean"; + case LuaType::LightUserData: + return "light userdata"; + case LuaType::Number: + return "number"; + case LuaType::String: + return "string"; + case LuaType::Table: + return "table"; + case LuaType::Function: + return "function"; + case LuaType::UserData: + return "userdata"; + case LuaType::Thread: + return "thread"; + } + return "unknown"; +} diff --git a/sdk/src/CMakeLists.txt b/sdk/src/CMakeLists.txt index e4fc08b..98d7d1e 100644 --- a/sdk/src/CMakeLists.txt +++ b/sdk/src/CMakeLists.txt @@ -1 +1 @@ -target_sources (sdk PRIVATE frame.cc value.cc table.cc query.cc object.cc) +target_sources (sdk PRIVATE node.cc path.cc state.cc) diff --git a/sdk/src/detail.cc b/sdk/src/detail.cc deleted file mode 100644 index 9b4a049..0000000 --- a/sdk/src/detail.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "sdk/detail.hh" -#include -#include - -// Context* LuaValue::context() const { return static_cast(lua_getextraspace(state_)); } - diff --git a/sdk/src/frame.cc b/sdk/src/frame.cc deleted file mode 100644 index 00523d8..0000000 --- a/sdk/src/frame.cc +++ /dev/null @@ -1,66 +0,0 @@ -#include "game/lua/table.hh" -#include "game/lua/util.hh" -#include -#include -#include - -std::string lua::path_string(LuaPath const& path, size_t len) -{ - std::stringstream out; - for (size_t i = 0; i < len; ++i) { - if (i > 0) out << '.'; - std::visit([&out](auto const& key) { out << key; }, path[i]); - } - return std::move(out).str(); -} - -std::string lua::path_string(LuaPath const& path) { return path_string(path, path.size()); } - -void lua::push_key(lua_State* L, LuaKey const& key) -{ - std::visit( - [L](auto const& value) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) - lua_pushlstring(L, value.data(), value.size()); - else if constexpr (std::integral) lua_pushinteger(L, value); - else static_assert(false, "unsupported lua key type"); - }, - key); -} - -Result lua::try_pop_boolean(lua_State* L) -{ - auto type = lua_type(L, -1); - if (type != LUA_TBOOLEAN) return lua::type_error(L, LUA_TBOOLEAN, type); - auto out = lua_toboolean(L, -1); - lua_pop(L, -1); - return out; -} - -Result lua::try_pop_number(lua_State* L) -{ - auto type = lua_type(L, -1); - if (type != LUA_TNUMBER) return lua::type_error(L, LUA_TNUMBER, type); - auto out = lua_tonumber(L, -1); - lua_pop(L, -1); - return out; -} - -Result lua::try_pop_string(lua_State* L) {} - -Result lua::try_pop_table(lua_State* L) {} - -void push_key(lua_State* L, LuaKey const& key) -{ - std::visit( - [L](auto const& value) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) - lua_pushlstring(L, value.data(), value.size()); - else if constexpr (std::integral) lua_pushinteger(L, value); - else static_assert(false, "unsupported lua key type"); - }, - key); -} - diff --git a/sdk/src/lua-builder.cc b/sdk/src/lua-builder.cc deleted file mode 100644 index 78f4fdd..0000000 --- a/sdk/src/lua-builder.cc +++ /dev/null @@ -1,59 +0,0 @@ -#include "core/defer.hh" -#include "game/catalog.hh" -#include "game/lua.hh" -#include "game/sprite.hh" -#include "game/terrain.hh" -#include "game/types.hh" -#include - -// terrain "name" { sprite = {} } -static int parse_terrain_table(lua_State* L) -{ - INVARIANT(lua_islightuserdata(L, lua_upvalueindex(1))); - INVARIANT(lua_isstring(L, lua_upvalueindex(2))); - auto catalog = static_cast(lua_touserdata(L, lua_upvalueindex(1))); - auto name = lua_tostring(L, lua_upvalueindex(2)); - do { - // arg 1: definition table - if (!lua_istable(L, 1)) { - lua::push_fstring( - L, "terrain '{}': expected table, found {}", name, lua_typename(L, lua_type(L, 1))); - break; - } - - int terrain = lua_gettop(L); - auto sprite = lua::try_get_sprite(*catalog, L, terrain, "sprite"); - if (!sprite) { - lua::push_string(L, sprite.error().msg); - break; - } - - DEFER(lua_pop(L, 1)); - catalog->emplace(name, *sprite); - return 0; - } while (false); - // this will unwind the stack without calling C++ destructors - return lua_error(L); -} - -static int build_terrain(lua_State* L) -{ - INVARIANT(lua_islightuserdata(L, lua_upvalueindex(1))); - // arg 1: name string - if (!lua_isstring(L, 1)) { - lua_pushstring( - L, std::format("terrain: expected string, found {}", lua_typename(L, lua_type(L, 1))).data()); - return lua_error(L); - } - lua_pushvalue(L, lua_upvalueindex(1)); - lua_pushvalue(L, 1); // name - lua_pushcclosure(L, parse_terrain_table, 2); - return 1; -} - -void lua::add_terrain_builder(lua_State* L, CatalogWriter& catalog) -{ - lua_pushlightuserdata(L, &catalog); - lua_pushcclosure(L, build_terrain, 1); - lua_setglobal(L, "terrain"); -} diff --git a/sdk/src/lua.cc b/sdk/src/lua.cc deleted file mode 100644 index fd5ddd2..0000000 --- a/sdk/src/lua.cc +++ /dev/null @@ -1,179 +0,0 @@ -#include "core/defer.hh" -#include "core/panic.hh" -#include "game/lua.hh" -#include -#include -#include -#include - -using namespace lua; - -LuaReader::LuaReader(lua_State* L, i64 idx) noexcept : handle_{ L }, idx_{ idx } -{ - PRECONDITION(L != nullptr); - PRECONDITION(idx != 0); -} - -LuaReader::~LuaReader() noexcept -{ - DEBUG_ASSERT(lua_gettop(handle_) == idx_); - lua_settop(handle_, idx_); -} - -[[nodiscard]] Result LuaReader::expect_boolean() const; -[[nodiscard]] Result LuaReader::expect_integer() const; -[[nodiscard]] Result LuaReader::expect_number() const; -[[nodiscard]] Result LuaReader::expect_string() const; -[[nodiscard]] Result LuaReader::expect_table() const; - -// { -// lua_pushvalue(handle_, idx_); -// i64 out = lua_tointeger(handle_, idx_); -// lua_pop(handle_, 1); -// return out; -// } - -Result lua::try_get_boolean(lua_State* L, int table, std::string_view field) -{ - auto idx = try_push_field(L, LUA_TBOOLEAN, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_boolean(L); -} - -static int handle_error(lua_State* L) -{ - luaL_traceback(L, L, lua_tostring(L, -1), 1); - return 1; -} - -Error lua::make_content_error(lua_State* L, int found, int expected) -{ - return { std::format( - "expected {}, found {}", lua_typename(L, expected), lua_typename(L, found)) }; -} - -void lua::push_string(lua_State* L, std::string_view value) -{ - lua_pushlstring(L, value.data(), value.size()); // string may not be null-terminated -} - -bool lua::pop_boolean(lua_State* L) -{ - bool out = lua_toboolean(L, -1); - lua_pop(L, 1); - return out; -} - -std::string lua::pop_string(lua_State* L) -{ - std::string out = lua_tostring(L, -1); - lua_pop(L, 1); - return out; -} - -lua_Integer lua::pop_integer(lua_State* L) -{ - auto out = lua_tointeger(L, -1); - lua_pop(L, 1); - return out; -} - -lua_Number lua::pop_number(lua_State* L) -{ - auto out = lua_tonumber(L, -1); - lua_pop(L, 1); - return out; -} - -Result lua::try_push_field(lua_State* L, int type, int table, unsigned field) -{ - PRECONDITION(L); - PRECONDITION(table != 0); - PRECONDITION(lua_istable(L, table)); - PRECONDITION(field > 0u); - int found = lua_geti(L, table, field); - if (found == type) return lua_gettop(L); - lua_pop(L, -1); - return std::unexpected(lua::make_content_error(L, found, type)); -} - -Result lua::try_push_field(lua_State* L, int type, int table, std::string_view field) -{ - PRECONDITION(L); - PRECONDITION(table != 0); - PRECONDITION(lua_istable(L, table)); - table = lua_absindex(L, table); - push_string(L, field); - int found = lua_gettable(L, table); - if (found == type) return lua_gettop(L); - lua_pop(L, 1); - return std::unexpected(lua::make_content_error(L, found, type)); -} - -Result lua::try_get_string(lua_State* L, int table, std::string_view field) -{ - auto idx = try_push_field(L, LUA_TSTRING, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_string(L); -} - -Result lua::try_get_string(lua_State* L, int table, unsigned field) -{ - auto idx = try_push_field(L, LUA_TSTRING, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_string(L); -} - -Result lua::try_get_integer(lua_State* L, int table, std::string_view field) -{ - auto idx = try_push_field(L, LUA_TNUMBER, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_integer(L); -} - -Result lua::try_get_integer(lua_State* L, int table, unsigned field) -{ - auto idx = try_push_field(L, LUA_TNUMBER, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_integer(L); -} - -Result lua::try_get_number(lua_State* L, int table, std::string_view field) -{ - auto idx = try_push_field(L, LUA_TNUMBER, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_number(L); -} - -Result lua::try_get_number(lua_State* L, int table, unsigned field) -{ - auto idx = try_push_field(L, LUA_TNUMBER, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_number(L); -} - -Result lua::try_get_boolean(lua_State* L, int table, std::string_view field) -{ - auto idx = try_push_field(L, LUA_TBOOLEAN, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_boolean(L); -} - -Result lua::try_get_boolean(lua_State* L, int table, unsigned field) -{ - auto idx = try_push_field(L, LUA_TBOOLEAN, table, field); - if (!idx) return std::unexpected(idx.error()); - return pop_boolean(L); -} - -Result lua::do_file(lua_State* L, std::string_view path, unsigned argc, unsigned resultc) -{ - lua_pushcfunction(L, handle_error); - int handler = lua_gettop(L); - DEFER(lua_remove(L, handler)); - int error = luaL_loadfile(L, std::string(path).c_str()); - if (error != LUA_OK) [[unlikely]] { return std::unexpected(pop_string(L)); } - error = lua_pcall(L, argc, resultc, handler); - if (error != LUA_OK) [[unlikely]] { return std::unexpected(pop_string(L)); } - return {}; -} diff --git a/sdk/src/node.cc b/sdk/src/node.cc index 182c321..c43f78f 100644 --- a/sdk/src/node.cc +++ b/sdk/src/node.cc @@ -1,210 +1,296 @@ -#include "core/panic.hh" -#include "sdk/detail.hh" #include "sdk/node.hh" -#include "sdk/types.hh" +#include "core/panic.hh" +#include #include -#include #include +#include #include -static std::string type_error(lua_State* L, int expected, int found) -{ - return std::format("expected {} but found {}", lua_typename(L, expected), lua_typename(L, found)); -} +namespace { -static void push_key(lua_State* L, LuaKey const& key) +void push_key(lua_State* handle, LuaKey const& key) { std::visit( - [L](auto const& value) -> void { + [handle](auto const& value) { using Type = std::remove_cvref_t; - if constexpr (std::same_as) lua_pushlstring(L, value.data(), value.size()); - else if constexpr (std::integral) lua_pushinteger(L, value); - else static_assert(false, "unsupported lua key type"); + if constexpr (std::same_as) { + lua_pushlstring(handle, value.data(), value.size()); + } else { + lua_pushinteger(handle, value); + } }, key); } -static Result try_resolve_path(lua_State* L, int start, LuaPath const& path) +std::string type_error(LuaPath const& path, LuaType expected, LuaType found) { - lua_pushvalue(L, start); + std::string const location = path.empty() ? "value" : describe(path); + return std::format( + "{}: expected {} but found {}", location, describe(expected), describe(found)); +} - for (size_t key = 0; key < path.size(); ++key) { - if (lua_istable(L, -1)) { - auto msg = std::format( - "{}: {}", describe(path.span(key + 1)), type_error(L, LUA_TTABLE, lua_type(L, -1))); - lua_pop(L, 1); - return Error(std::move(msg)); - } - push_key(L, path[key]); - lua_rawget(L, -2); - lua_replace(L, -2); +Result reference_child( + lua_State* handle, i32 reference, LuaKey const& key, LuaPath const& parent_path, + LuaPath const& child_path) +{ + LuaType const parent_type + = static_cast(lua_rawgeti(handle, LUA_REGISTRYINDEX, reference)); + if (parent_type != LuaType::Table) { + lua_pop(handle, 1); + return Error(type_error(parent_path, LuaType::Table, parent_type)); } - if (lua_isnone(L, -1)) { - auto msg = std::format("{}: no value found", describe(path)); - lua_pop(L, 1); - return Error(std::move(msg)); + push_key(handle, key); + LuaType const child_type = static_cast(lua_rawget(handle, -2)); + lua_remove(handle, -2); + + if (child_type == LuaType::Nil) { + lua_pop(handle, 1); + return Error(std::format("{}: no value found", describe(child_path))); } - return LuaType{ lua_type(L, -1) }; + return luaL_ref(handle, LUA_REGISTRYINDEX); } -static Result try_get_field(lua_State* L, int idx, LuaKey const& key) +template +Result read_value( + lua_State* handle, i32 reference, LuaPath const& path, LuaType expected, Reader read) { - if (lua_istable(L, idx)) return Error(type_error(L, LUA_TTABLE, lua_type(L, idx))); - push_key(L, key); - auto type = lua_rawget(L, idx); - return LuaType{ type }; -} + LuaType const found = static_cast(lua_rawgeti(handle, LUA_REGISTRYINDEX, reference)); + if (found != expected) { + lua_pop(handle, 1); + return Error(type_error(path, expected, found)); + } -static Result try_pop_boolean(lua_State* L) -{ - if (lua_isboolean(L, -1)) return Error(type_error(L, LUA_TBOOLEAN, lua_type(L, -1))); - auto boolean = lua_toboolean(L, -1); - lua_pop(L, 1); - return boolean; + Type value = read(handle); + lua_pop(handle, 1); + return value; } -static Result try_pop_number(lua_State* L) +Result read_key(lua_State* handle) { - if (lua_isnumber(L, -1)) return Error(type_error(L, LUA_TNUMBER, lua_type(L, -1))); - auto number = lua_tonumber(L, -1); - lua_pop(L, 1); - return number; + switch (lua_type(handle, -2)) { + case LUA_TNUMBER: + if (!lua_isinteger(handle, -2)) { + return Error("table contains a non-integer numeric key"); + } + return LuaKey{ static_cast(lua_tointeger(handle, -2)) }; + case LUA_TSTRING: { + size_t length = 0; + char const* data = lua_tolstring(handle, -2, &length); + return LuaKey{ std::string(data, length) }; + } + default: + return Error(std::format( + "table contains unsupported {} key", lua_typename(handle, lua_type(handle, -2)))); + } } -static Result try_pop_string(lua_State* L) +} // namespace + +std::string describe(LuaNode const& value) { - if (lua_isstring(L, -1)) return Error(type_error(L, LUA_TSTRING, lua_type(L, -1))); - auto string = lua_tostring(L, -1); - lua_pop(L, 1); - return string; + return value.path().empty() ? "value" : describe(value.path()); } -LuaNode::LuaNode(lua_State* L, i32 reference) noexcept - : handle_{ L }, reference_{ reference }, type_{} +LuaNode::LuaNode(std::shared_ptr owner, i32 reference, LuaPath path) noexcept + : owner_{ std::move(owner) }, handle_{ owner_.get() }, reference_{ reference }, + type_{ LuaType::None }, path_(std::move(path)) { - PRECONDITION(L != nullptr); - type_ = LuaType{ lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_) }; + PRECONDITION(handle_ != nullptr); + PRECONDITION(reference != LUA_NOREF && reference != LUA_REFNIL); + type_ = static_cast(lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_)); lua_pop(handle_, 1); } -LuaNode::LuaNode(LuaNode&& v) noexcept - : handle_{ v.handle_ }, reference_{ v.reference_ }, type_{ v.type_ } +LuaNode::LuaNode(LuaNode&& other) noexcept + : owner_{ std::move(other.owner_) }, handle_{ other.handle_ }, reference_{ other.reference_ }, + type_{ other.type_ }, + path_{ std::move(other.path_) } { - v.reference_ = LUA_REFNIL; - v.type_ = LuaType::None; + other.handle_ = nullptr; + other.reference_ = LUA_NOREF; + other.type_ = LuaType::None; } -LuaNode& LuaNode::operator=(LuaNode&& v) noexcept +LuaNode& LuaNode::operator=(LuaNode&& other) noexcept { - if (&v == this) return *this; - reference_ = v.reference_; - handle_ = v.handle_; - v.reference_ = LUA_REFNIL; - v.type_ = LuaType::None; + if (&other == this) return *this; + + if (handle_ != nullptr && reference_ != LUA_NOREF && reference_ != LUA_REFNIL) { + luaL_unref(handle_, LUA_REGISTRYINDEX, reference_); + } + + owner_ = std::move(other.owner_); + handle_ = other.handle_; + reference_ = other.reference_; + type_ = other.type_; + path_ = std::move(other.path_); + other.handle_ = nullptr; + other.reference_ = LUA_NOREF; + other.type_ = LuaType::None; return *this; } LuaNode::~LuaNode() noexcept { - luaL_unref(handle_, LUA_REGISTRYINDEX, reference_); - reference_ = LUA_REFNIL; - type_ = LuaType::None; + if (handle_ != nullptr && reference_ != LUA_NOREF && reference_ != LUA_REFNIL) { + luaL_unref(handle_, LUA_REGISTRYINDEX, reference_); + } } Result LuaNode::find(LuaKey const& key) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_get_field(handle_, -1, key); - if (!ok) return err(ok); - auto ref = luaL_ref(handle_, LUA_REGISTRYINDEX); - return LuaNode(handle_, ref); + PRECONDITION(handle_ != nullptr); + LuaPath const child_path = path_ + key; + Result reference = reference_child(handle_, reference_, key, path_, child_path); + if (!reference) return Error(std::move(reference).error()); + return LuaNode(owner_, *reference, child_path); } Result LuaNode::find(LuaPath const& path) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_resolve_path(handle_, -1, path); - if (!ok) return err(ok); - auto ref = luaL_ref(handle_, LUA_REGISTRYINDEX); - return LuaNode(handle_, ref); + PRECONDITION(handle_ != nullptr); + if (path.empty()) return Error("cannot resolve an empty Lua path"); + + Result current = find(path[0]); + for (u32 i = 1; current && i < path.size(); ++i) { + current = current->find(path[i]); + } + return current; } Result LuaNode::expect_boolean() const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - return try_pop_boolean(handle_); + PRECONDITION(handle_ != nullptr); + return read_value( + handle_, reference_, path_, LuaType::Boolean, + [](lua_State* handle) { return lua_toboolean(handle, -1) != 0; }); +} + +Result LuaNode::expect_integer() const +{ + PRECONDITION(handle_ != nullptr); + LuaType const found = static_cast(lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_)); + if (found != LuaType::Number || !lua_isinteger(handle_, -1)) { + lua_pop(handle_, 1); + std::string const location = path_.empty() ? "value" : describe(path_); + return Error(std::format("{}: expected integer but found {}", location, describe(found))); + } + + i64 const value = static_cast(lua_tointeger(handle_, -1)); + lua_pop(handle_, 1); + return value; } Result LuaNode::expect_number() const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - return try_pop_number(handle_); + PRECONDITION(handle_ != nullptr); + return read_value( + handle_, reference_, path_, LuaType::Number, + [](lua_State* handle) { return static_cast(lua_tonumber(handle, -1)); }); } Result LuaNode::expect_string() const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - return try_pop_string(handle_); + PRECONDITION(handle_ != nullptr); + return read_value( + handle_, reference_, path_, LuaType::String, + [](lua_State* handle) { + size_t length = 0; + char const* data = lua_tolstring(handle, -1, &length); + return std::string(data, length); + }); } Result LuaNode::expect_boolean(LuaKey const& key) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_get_field(handle_, -1, key); - if (!ok) return err(ok); - return try_pop_boolean(handle_); + Result value = find(key); + if (!value) return Error(std::move(value).error()); + return value->expect_boolean(); +} + +Result LuaNode::expect_integer(LuaKey const& key) const +{ + Result value = find(key); + if (!value) return Error(std::move(value).error()); + return value->expect_integer(); } Result LuaNode::expect_number(LuaKey const& key) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_get_field(handle_, -1, key); - if (!ok) return err(ok); - return try_pop_number(handle_); + Result value = find(key); + if (!value) return Error(std::move(value).error()); + return value->expect_number(); } Result LuaNode::expect_string(LuaKey const& key) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_get_field(handle_, -1, key); - if (!ok) return err(ok); - return try_pop_string(handle_); + Result value = find(key); + if (!value) return Error(std::move(value).error()); + return value->expect_string(); } Result LuaNode::expect_boolean(LuaPath const& path) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_resolve_path(handle_, -1, path); - if (!ok) return err(ok); - return try_pop_boolean(handle_); + Result value = find(path); + if (!value) return Error(std::move(value).error()); + return value->expect_boolean(); +} + +Result LuaNode::expect_integer(LuaPath const& path) const +{ + Result value = find(path); + if (!value) return Error(std::move(value).error()); + return value->expect_integer(); } Result LuaNode::expect_number(LuaPath const& path) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_resolve_path(handle_, -1, path); - if (!ok) return err(ok); - return try_pop_number(handle_); + Result value = find(path); + if (!value) return Error(std::move(value).error()); + return value->expect_number(); } Result LuaNode::expect_string(LuaPath const& path) const { - PRECONDITION(reference_ != LUA_REFNIL); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_); - auto ok = try_resolve_path(handle_, -1, path); - if (!ok) return err(ok); - return try_pop_string(handle_); + Result value = find(path); + if (!value) return Error(std::move(value).error()); + return value->expect_string(); } +Result> LuaNode::keys() const +{ + PRECONDITION(handle_ != nullptr); + LuaType const found = static_cast(lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_)); + if (found != LuaType::Table) { + lua_pop(handle_, 1); + return Error(type_error(path_, LuaType::Table, found)); + } + + std::vector keys; + lua_pushnil(handle_); + while (lua_next(handle_, -2) != 0) { + Result key = read_key(handle_); + lua_pop(handle_, 1); + if (!key) { + lua_pop(handle_, 2); + return Error(std::format("{}: {}", describe(*this), std::move(key).error())); + } + keys.push_back(std::move(*key)); + } + lua_pop(handle_, 1); + + std::ranges::sort(keys, [](LuaKey const& lhs, LuaKey const& rhs) { + if (lhs.index() != rhs.index()) return lhs.index() < rhs.index(); + return std::visit( + [](auto const& left, auto const& right) { + using Left = std::remove_cvref_t; + using Right = std::remove_cvref_t; + if constexpr (std::same_as) return left < right; + else return false; + }, + lhs, rhs); + }); + return keys; +} diff --git a/sdk/src/object.cc b/sdk/src/object.cc deleted file mode 100644 index 2c53e6e..0000000 --- a/sdk/src/object.cc +++ /dev/null @@ -1,67 +0,0 @@ -#include "core/panic.hh" -#include "game/lua.hh" -#include "game/lua/object.hh" -#include "game/lua/util.hh" -#include -#include - -LuaObject::LuaObject(lua_State* L, i32 ref) noexcept : handle_{ L }, ref_{ ref } -{ - PRECONDITION(L != nullptr); -} - -LuaObject::LuaObject(LuaObject&& other) noexcept : handle_{ other.handle_ }, ref_{ other.ref_ } -{ - other.handle_ = nullptr; - other.ref_ = 0; -} - -LuaObject::~LuaObject() noexcept { luaL_unref(handle_, LUA_REGISTRYINDEX, ref_); } - -LuaObject& LuaObject::operator=(LuaObject&& other) noexcept -{ - if (&other == this) return *this; - handle_ = other.handle_; - ref_ = other.ref_; - other.handle_ = nullptr; - other.ref_ = 0; - return *this; -} - -Result LuaObject::expect_boolean() const -{ - lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); - return lua::try_pop_boolean(handle_); -} - -Result LuaObject::expect_number() const -{ - lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); - return lua::try_pop_number(handle_); -} - -Result LuaObject::expect_string() const -{ - lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); - return lua::try_pop_string(handle_); -} - -Result LuaObject::expect_table() const -{ - auto type = lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); - if (type != LUA_TTABLE) return lua::type_error(handle_, LUA_TTABLE, type); - return LuaTable(handle_, luaL_ref(handle_, LUA_REGISTRYINDEX)); -} - -i32 LuaObject::type() const noexcept -{ - auto type = lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); - lua_pop(handle_, -1); - return lua_; -} - -bool LuaObject::is_nil() const noexcept { return type() == LUA_TNIL; } - -bool LuaObject::is_none() const noexcept { return type() == LUA_TNONE; } - -LuaType push() const { return luaL_unref(handle(), LUA_REGISTRYINDEX, reference_); } diff --git a/sdk/src/path.cc b/sdk/src/path.cc index 624a866..26ced92 100644 --- a/sdk/src/path.cc +++ b/sdk/src/path.cc @@ -1,6 +1,8 @@ #include "sdk/path.hh" +#include #include #include +#include std::string describe(LuaKey const& key) { @@ -19,10 +21,16 @@ std::string describe(LuaPath const& path) { return describe(path.span()); } std::string describe(std::span path) { std::stringstream out; - for (size_t i = 0; i < path.size(); ++i) { - if (i > 0) out << '.'; - std::visit([&out](auto const& key) { out << key; }, path[i]); + for (u32 i = 0; i < path.size(); ++i) { + std::visit( + [&out, i](auto const& key) { + using Type = std::remove_cvref_t; + if constexpr (std::same_as) { + if (i > 0) out << '.'; + out << key; + } else out << '[' << key << ']'; + }, + path[i]); } return std::move(out).str(); } - diff --git a/sdk/src/query.cc b/sdk/src/query.cc deleted file mode 100644 index 19e5938..0000000 --- a/sdk/src/query.cc +++ /dev/null @@ -1,76 +0,0 @@ -#include "core/panic.hh" -#include "sdk/query.hh" -#include - -LuaQuery::LuaQuery(lua_State* handle, i32 idx) noexcept : handle_{ handle }, root_{ idx }, path_() -{ - PRECONDITION(handle != nullptr); -} - -LuaQuery::LuaQuery(lua_State* handle, i32 idx, LuaKey key) noexcept - : handle_{ handle }, root_{ idx }, path_() -{} - -Result LuaQuery::resolve() const -{ - push(); - auto ref = luaL_ref(handle(), LUA_REGISTRYINDEX); - return LuaObject(handle(), ref); -} - -void LuaQuery::append(LuaKey const& key) { path_.push_back(key); } - -void LuaQuery::append(LuaPath const& path) -{ - for (auto const& key : path) append(key); -} - -LuaQuery LuaQuery::operator[](LuaKey key) const { return LuaQuery(*this, key); } - -LuaType LuaQuery::push() const -{ - auto ok = try_push(); - if (!ok) return LuaType::None; - return *ok; -} - -std::string describe_path(LuaPath const& path, size_t len) -{ - std::stringstream out; - for (size_t i = 0; i < len; ++i) { - if (i > 0) out << '.'; - std::visit([&out](auto const& key) { out << key; }, path[i]); - } - return std::move(out).str(); -} - -std::string describe_path(LuaPath const& path) { return path_string(path, path.size()); } - -// Attempts to push onto the stack the value at the end of the path. -Result try_resolve_path() const -{ - lua_pushvalue(L, start); - int idx = lua_gettop(L); - - for (size_t key = 0; key < path.size(); ++key) { - if (auto type = lua_type(L, idx); type != LUA_TTABLE) { - auto msg = std::format( - "{}: expected {} but found {}", describe_path(path, idx + 1), lua_typename(L, LUA_TTABLE), - lua_typename(L, type)); - lua_settop(L, idx - 1); - return Error(std::move(msg)); - } - lua::push_key(L, path[key]); - lua_rawget(L, idx); - lua_replace(L, idx); - } - - if (lua_type(L, idx) == LUA_TNONE) { - auto msg = std::format("{}: no value found", describe_path(path)); - lua_settop(L, idx - 1); - return Error(std::move(msg)); - } - - return lua_type(L, -1); -} - diff --git a/sdk/src/state.cc b/sdk/src/state.cc new file mode 100644 index 0000000..668f25e --- /dev/null +++ b/sdk/src/state.cc @@ -0,0 +1,188 @@ +#include "sdk/state.hh" +#include "core/panic.hh" +#include +#include +#include +#include +#include + +namespace { + +void push_key(lua_State* handle, LuaKey const& key) +{ + std::visit( + [handle](auto const& value) { + using Type = std::remove_cvref_t; + if constexpr (std::same_as) { + lua_pushlstring(handle, value.data(), value.size()); + } else { + lua_pushinteger(handle, value); + } + }, + key); +} + +std::string pop_string(lua_State* handle) +{ + size_t length = 0; + char const* data = lua_tolstring(handle, -1, &length); + std::string result = data == nullptr ? "unknown Lua error" : std::string(data, length); + lua_pop(handle, 1); + return result; +} + +int add_traceback(lua_State* handle) +{ + char const* message = lua_tostring(handle, 1); + if (message == nullptr) return 1; + luaL_traceback(handle, handle, message, 1); + return 1; +} + +void open_content_libraries(lua_State* handle) +{ + struct Library + { + char const* name; + lua_CFunction open; + }; + + constexpr Library libraries[] = { + { LUA_GNAME, luaopen_base }, + { LUA_TABLIBNAME, luaopen_table }, + { LUA_STRLIBNAME, luaopen_string }, + { LUA_MATHLIBNAME, luaopen_math }, + { LUA_UTF8LIBNAME, luaopen_utf8 }, + }; + + for (Library const& library : libraries) { + luaL_requiref(handle, library.name, library.open, 1); + lua_pop(handle, 1); + } + + constexpr char const* unavailable[] = { "collectgarbage", "dofile", "load", "loadfile" }; + for (char const* name : unavailable) { + lua_pushnil(handle); + lua_setglobal(handle, name); + } +} + +} // namespace + +LuaState::LuaState(std::shared_ptr owner) noexcept + : owner_{ std::move(owner) }, handle_{ owner_.get() } +{ + PRECONDITION(handle_ != nullptr); +} + +LuaState::LuaState(LuaState&& other) noexcept + : owner_{ std::move(other.owner_) }, handle_{ owner_.get() } +{ + other.handle_ = nullptr; +} + +LuaState& LuaState::operator=(LuaState&& other) noexcept +{ + if (&other == this) return *this; + owner_ = std::move(other.owner_); + handle_ = owner_.get(); + other.handle_ = nullptr; + return *this; +} + +LuaState::~LuaState() noexcept = default; + +Result LuaState::create() +{ + lua_State* handle = luaL_newstate(); + if (handle == nullptr) return Error("failed to create Lua state"); + + std::shared_ptr owner(handle, lua_close); + open_content_libraries(handle); + return LuaState(std::move(owner)); +} + +Status LuaState::create_table(LuaPath const& path) +{ + PRECONDITION(handle_ != nullptr); + if (path.empty()) return Error("cannot create a table at an empty Lua path"); + + int const base = lua_gettop(handle_); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); + + LuaPath current; + for (LuaKey const& key : path) { + current += key; + push_key(handle_, key); + int const type = lua_rawget(handle_, -2); + + if (type == LUA_TNIL) { + lua_pop(handle_, 1); + lua_newtable(handle_); + push_key(handle_, key); + lua_pushvalue(handle_, -2); + lua_rawset(handle_, -4); + } else if (type != LUA_TTABLE) { + std::string message = std::format( + "{}: expected table but found {}", describe(current), lua_typename(handle_, type)); + lua_settop(handle_, base); + return Error(std::move(message)); + } + + lua_remove(handle_, -2); + } + + lua_settop(handle_, base); + return {}; +} + +Status LuaState::do_file(std::string_view path) +{ + PRECONDITION(handle_ != nullptr); + std::string const terminated_path(path); + return execute(luaL_loadfile(handle_, terminated_path.c_str())); +} + +Status LuaState::do_string(std::string_view source, std::string_view name) +{ + PRECONDITION(handle_ != nullptr); + std::string const terminated_name(name); + return execute(luaL_loadbuffer(handle_, source.data(), source.size(), terminated_name.c_str())); +} + +Status LuaState::execute(int load_status) +{ + PRECONDITION(handle_ != nullptr); + if (load_status != LUA_OK) return Error(pop_string(handle_)); + + int const function = lua_gettop(handle_); + lua_pushcfunction(handle_, add_traceback); + lua_insert(handle_, function); + + int const call_status = lua_pcall(handle_, 0, 0, function); + if (call_status != LUA_OK) { + std::string message = pop_string(handle_); + lua_remove(handle_, function); + return Error(std::move(message)); + } + + lua_remove(handle_, function); + return {}; +} + +Result LuaState::global(std::string_view name) const +{ + PRECONDITION(handle_ != nullptr); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); + lua_pushlstring(handle_, name.data(), name.size()); + int const type = lua_rawget(handle_, -2); + lua_remove(handle_, -2); + + if (type == LUA_TNIL) { + lua_pop(handle_, 1); + return Error(std::format("{}: no value found", name)); + } + + i32 const reference = luaL_ref(handle_, LUA_REGISTRYINDEX); + return LuaNode(owner_, reference, LuaPath{ LuaKey{ std::string(name) } }); +} diff --git a/sdk/src/table.cc b/sdk/src/table.cc deleted file mode 100644 index 6226a7b..0000000 --- a/sdk/src/table.cc +++ /dev/null @@ -1,40 +0,0 @@ -#include "core/panic.hh" -#include "game/lua/table.hh" -#include -#include - -static bool is_table_reference(lua_State* L, i64 ref) -{ - lua_rawgeti(L, LUA_REGISTRYINDEX, ref); - bool out = lua_istable(L, -1); - lua_pop(L, -1); - return out; -} - -LuaTable::LuaTable(lua_State* handle, i32 ref) noexcept : handle_{ handle } -{ - PRECONDITION(handle != nullptr); - PRECONDITION(is_table_reference(handle_, ref)); -} - -LuaTable::LuaTable(LuaTable&&) noexcept {} - -LuaTable& LuaTable::operator=(LuaTable&&) noexcept -{ - if (&other == this) return *this; - return *this; -} - -LuaTable::~LuaTable() noexcept -{ - // destroy owned reference -} - -LuaCursor LuaTable::operator[](LuaKey key) const noexcept { return { handle_, ref_ }; } - -size_t LuaTable::size() const noexcept { return 0u; } - -// auto type = lua_rawgeti(handle_, LUA_REGISTRYINDEX, ref_); -// auto -// DEBUG_ASSERT(type == LUA_TTABLE); -// lua_rawgeti(handle_, -1, ) diff --git a/sdk/src/types.cc b/sdk/src/types.cc deleted file mode 100644 index 9dd60ee..0000000 --- a/sdk/src/types.cc +++ /dev/null @@ -1,31 +0,0 @@ -#include "core/panic.hh" -#include "sdk/types.hh" -#include - -constexpr LuaType enumerate_lua_type(int t) -{ - switch (t) { - case LUA_TNONE: - return LuaType::None; - case LUA_TNIL: - return LuaType::Nil; - case LUA_TBOOLEAN: - return LuaType::Boolean; - case LUA_TLIGHTUSERDATA: - return LuaType::LightUserData; - case LUA_TNUMBER: - return LuaType::Number; - case LUA_TSTRING: - return LuaType::String; - case LUA_TTABLE: - return LuaType::Table; - case LUA_TFUNCTION: - return LuaType::Function; - case LUA_TUSERDATA: - return LuaType::UserData; - case LUA_TTHREAD: - return LuaType::Thread; - } - INVARIANT(false, "lua type not represented"); -} - diff --git a/sdk/tests/CMakeLists.txt b/sdk/tests/CMakeLists.txt new file mode 100644 index 0000000..3f09164 --- /dev/null +++ b/sdk/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +list (APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) + +add_executable (test-sdk) + +target_sources (test-sdk PRIVATE lua.cc) + +target_link_libraries ( + test-sdk + PRIVATE sdk + PRIVATE Catch2::Catch2WithMain +) + +include (CTest) +include (Catch) +catch_discover_tests (test-sdk) diff --git a/sdk/tests/lua.cc b/sdk/tests/lua.cc new file mode 100644 index 0000000..5f67278 --- /dev/null +++ b/sdk/tests/lua.cc @@ -0,0 +1,150 @@ +#include "sdk/state.hh" +#include "sdk/table.hh" +#include +#include +#include +#include + +namespace { + +struct Rectangle +{ + f32 x; + f32 y; + f32 width; + f32 height; +}; + +struct Sprite +{ + std::string atlas; + Rectangle source; + u32 tint; +}; + +} // namespace + +template <> +struct LuaTraits +{ + static constexpr LuaField schema[] = { + lua_field<&Rectangle::x>(1), + lua_field<&Rectangle::y>(2), + lua_field<&Rectangle::width>(3), + lua_field<&Rectangle::height>(4), + }; +}; + +template <> +struct LuaTraits +{ + static constexpr LuaField schema[] = { + lua_field<&Sprite::atlas>("atlas"), + lua_field<&Sprite::source>("source"), + lua_field<&Sprite::tint>("tint"), + }; +}; + +TEST_CASE("Lua nodes resolve typed values through paths") +{ + Result state = LuaState::create(); + REQUIRE(state.has_value()); + REQUIRE(state->do_string("content = { enabled = true, count = 42, name = 'grass' }") + .has_value()); + + Result content = state->global("content"); + REQUIRE(content.has_value()); + CHECK(content->expect("enabled") == true); + CHECK(content->expect("count") == 42); + CHECK(content->expect("name") == "grass"); +} + +TEST_CASE("Lua nodes keep their state alive") +{ + Result content = []() -> Result { + Result state = LuaState::create(); + if (!state) return Error(std::move(state).error()); + Status result = state->do_string("content = { value = 42 }"); + if (!result) return Error(std::move(result).error()); + return state->global("content"); + }(); + + REQUIRE(content.has_value()); + CHECK(content->expect("value") == 42); +} + +TEST_CASE("Lua schemas recursively translate nested tables") +{ + Result state = LuaState::create(); + REQUIRE(state.has_value()); + REQUIRE(state + ->do_string( + "content = { sprite = { atlas = 'tiles.png', source = { 16, 32, 8, 12 }, " + "tint = 4294967295 } }") + .has_value()); + + Result content = state->global("content"); + REQUIRE(content.has_value()); + Result node = content->find("sprite"); + REQUIRE(node.has_value()); + + Result sprite = translate(*node); + REQUIRE(sprite.has_value()); + CHECK(sprite->atlas == "tiles.png"); + CHECK(sprite->source.x == 16.0f); + CHECK(sprite->source.y == 32.0f); + CHECK(sprite->source.width == 8.0f); + CHECK(sprite->source.height == 12.0f); + CHECK(sprite->tint == 0xffffffffu); +} + +TEST_CASE("Lua translation errors identify the complete nested path") +{ + Result state = LuaState::create(); + REQUIRE(state.has_value()); + REQUIRE(state + ->do_string( + "content = { sprite = { atlas = 'tiles.png', source = { 16, 32, 'wide', 12 }, " + "tint = 1 } }") + .has_value()); + + Result content = state->global("content"); + REQUIRE(content.has_value()); + Result node = content->find("sprite"); + REQUIRE(node.has_value()); + + Result sprite = translate(*node); + REQUIRE_FALSE(sprite.has_value()); + CHECK(sprite.error() == "content.sprite.source[3]: expected number but found string"); +} + +TEST_CASE("Lua table keys have deterministic traversal order") +{ + Result state = LuaState::create(); + REQUIRE(state.has_value()); + REQUIRE(state->do_string("content = { z = true, a = true, [2] = true, [1] = true }") + .has_value()); + + Result content = state->global("content"); + REQUIRE(content.has_value()); + Result> keys = content->keys(); + REQUIRE(keys.has_value()); + REQUIRE(keys->size() == 4); + CHECK(std::get((*keys)[0]) == 1); + CHECK(std::get((*keys)[1]) == 2); + CHECK(std::get((*keys)[2]) == "a"); + CHECK(std::get((*keys)[3]) == "z"); +} + +TEST_CASE("Lua integer translation rejects narrowing") +{ + Result state = LuaState::create(); + REQUIRE(state.has_value()); + REQUIRE(state->do_string("content = { value = 256 }").has_value()); + + Result content = state->global("content"); + REQUIRE(content.has_value()); + Result value = content->expect("value"); + REQUIRE_FALSE(value.has_value()); + CHECK(value.error().contains("outside the destination type's range")); +} diff --git a/sys/include/sys/window.hh b/sys/include/sys/window.hh index 8c631e8..be37ce7 100644 --- a/sys/include/sys/window.hh +++ b/sys/include/sys/window.hh @@ -1,5 +1,5 @@ #pragma once -#include "core/error.hh" +#include "core/result.hh" #include "core/types.hh" #include @@ -35,4 +35,3 @@ struct Window private: SDL_Window* handle_ = nullptr; }; - diff --git a/sys/src/window.cc b/sys/src/window.cc index d76d5c0..d45702f 100644 --- a/sys/src/window.cc +++ b/sys/src/window.cc @@ -22,7 +22,7 @@ Result create_window(std::string_view title, i32 width, i32 height) { ASSERT(width != 0 && height != 0); auto window = SDL_CreateWindow(std::string(title).c_str(), width, height, 0); - if (!window) return std::unexpected(SDL_GetError()); + if (!window) return Error(SDL_GetError()); return Window(window); } @@ -41,4 +41,3 @@ glm::ivec2 Window::size() const noexcept } void Window::resize(i32 width, i32 height) noexcept { SDL_SetWindowSize(handle_, width, height); } - From 95df1f5f962cd2d321a3f8da61dded02beae61d4 Mon Sep 17 00:00:00 2001 From: Andrew Pogue Date: Tue, 29 Sep 2026 16:46:03 -0700 Subject: [PATCH 3/4] wip(lua): moving Lua-based content authorship to App --- .clang-format | 9 +- .clang-tidy | 4 +- .gitignore | 1 + CMakeLists.txt | 15 +- README.md | 2 +- app/CMakeLists.txt | 15 +- app/include/app/application.hh | 42 --- app/include/app/content.hh | 19 ++ .../app/{keyboard.hh => keyboard-pilot.hh} | 9 +- app/include/app/lua/key.hh | 57 ++++ .../game => app/include/app/lua}/load.hh | 0 app/include/app/lua/reference.hh | 40 +++ app/include/app/lua/state.hh | 40 +++ app/include/app/lua/table.hh | 57 ++++ app/include/app/lua/types.hh | 39 +++ app/include/app/lua/value.hh | 117 +++++++ app/include/app/pilot.hh | 14 + app/include/app/sprite.hh | 15 + app/include/app/state.hh | 48 +++ app/src/application.cc | 10 +- app/src/content.cc | 36 +++ app/src/load.cc | 59 ++++ app/src/lua/CMakeLists.txt | 1 + app/src/lua/reference.cc | 94 ++++++ app/src/lua/state.cc | 117 +++++++ app/src/lua/table.cc | 98 ++++++ app/src/lua/types.cc | 38 +++ app/src/lua/value.cc | 165 ++++++++++ app/src/sprite.cc | 17 + architecture.md | 19 ++ content/terrain.lua | 12 +- core/include/core/basic-catalog.hh | 62 ++-- core/include/core/bit-set.hh | 9 +- core/include/core/defer.hh | 8 +- core/include/core/grid2.hh | 3 +- core/include/core/result.hh | 7 + core/include/core/slot-map.hh | 6 +- core/include/core/types.hh | 20 +- core/src/random.cc | 3 +- core/tests/registry.cc | 9 +- game/CMakeLists.txt | 12 +- game/include/game/biome/caves.hh | 24 +- game/include/game/biome/grassland.hh | 24 +- game/include/game/catalog.hh | 85 ----- game/include/game/chunk.hh | 55 ++-- game/include/game/command-buffer.hh | 170 +++++----- game/include/game/command.hh | 145 ++++----- game/include/game/component/pose.hh | 10 +- game/include/game/content/terrain.hh | 19 ++ game/include/game/context.hh | 95 ------ game/include/game/entity.hh | 103 +----- game/include/game/pilot.hh | 14 - game/include/game/registry.hh | 89 ------ game/include/game/scene.hh | 7 - game/include/game/simulation.hh | 69 ---- game/include/game/sprite.hh | 17 - game/include/game/terrain.hh | 13 - game/include/game/texture.hh | 8 - game/include/game/tile.hh | 17 +- game/include/game/types.hh | 32 -- game/include/game/world.hh | 76 ++--- game/src/CMakeLists.txt | 3 +- game/src/biome/caves.cc | 116 +++---- game/src/biome/grassland.cc | 8 +- game/src/catalog.cc | 12 - game/src/command-buffer.cc | 55 ++-- game/src/command.cc | 43 ++- game/src/content/CMakeLists.txt | 1 + game/src/context.cc | 18 -- game/src/entity.cc | 28 -- game/src/load.cc | 69 ---- game/src/registry.cc | 11 - game/src/scene.cc | 6 - game/src/sprite.cc | 66 ---- game/src/terrain.cc | 14 - game/src/world.cc | 63 +--- gfx/CMakeLists.txt | 13 +- gfx/include/gfx/rectangle.hh | 1 - gfx/src/surface.cc | 7 +- play.cc | 3 +- sdk/CMakeLists.txt | 21 -- sdk/include/sdk/node.hh | 100 ------ sdk/include/sdk/path.hh | 73 ----- sdk/include/sdk/state.hh | 33 -- sdk/include/sdk/table.hh | 104 ------ sdk/include/sdk/types.hh | 47 --- sdk/src/CMakeLists.txt | 1 - sdk/src/node.cc | 296 ------------------ sdk/src/path.cc | 36 --- sdk/src/state.cc | 188 ----------- sdk/tests/CMakeLists.txt | 15 - sdk/tests/lua.cc | 150 --------- sys/CMakeLists.txt | 15 +- sys/src/main.cc | 10 +- 94 files changed, 1631 insertions(+), 2415 deletions(-) delete mode 100644 app/include/app/application.hh create mode 100644 app/include/app/content.hh rename app/include/app/{keyboard.hh => keyboard-pilot.hh} (53%) create mode 100644 app/include/app/lua/key.hh rename {game/include/game => app/include/app/lua}/load.hh (100%) create mode 100644 app/include/app/lua/reference.hh create mode 100644 app/include/app/lua/state.hh create mode 100644 app/include/app/lua/table.hh create mode 100644 app/include/app/lua/types.hh create mode 100644 app/include/app/lua/value.hh create mode 100644 app/include/app/pilot.hh create mode 100644 app/include/app/sprite.hh create mode 100644 app/include/app/state.hh create mode 100644 app/src/content.cc create mode 100644 app/src/load.cc create mode 100644 app/src/lua/CMakeLists.txt create mode 100644 app/src/lua/reference.cc create mode 100644 app/src/lua/state.cc create mode 100644 app/src/lua/table.cc create mode 100644 app/src/lua/types.cc create mode 100644 app/src/lua/value.cc create mode 100644 app/src/sprite.cc create mode 100644 architecture.md delete mode 100644 game/include/game/catalog.hh create mode 100644 game/include/game/content/terrain.hh delete mode 100644 game/include/game/context.hh delete mode 100644 game/include/game/pilot.hh delete mode 100644 game/include/game/registry.hh delete mode 100644 game/include/game/scene.hh delete mode 100644 game/include/game/simulation.hh delete mode 100644 game/include/game/sprite.hh delete mode 100644 game/include/game/terrain.hh delete mode 100644 game/include/game/texture.hh delete mode 100644 game/include/game/types.hh delete mode 100644 game/src/catalog.cc create mode 100644 game/src/content/CMakeLists.txt delete mode 100644 game/src/context.cc delete mode 100644 game/src/entity.cc delete mode 100644 game/src/load.cc delete mode 100644 game/src/registry.cc delete mode 100644 game/src/scene.cc delete mode 100644 game/src/sprite.cc delete mode 100644 game/src/terrain.cc delete mode 100644 sdk/CMakeLists.txt delete mode 100644 sdk/include/sdk/node.hh delete mode 100644 sdk/include/sdk/path.hh delete mode 100644 sdk/include/sdk/state.hh delete mode 100644 sdk/include/sdk/table.hh delete mode 100644 sdk/include/sdk/types.hh delete mode 100644 sdk/src/CMakeLists.txt delete mode 100644 sdk/src/node.cc delete mode 100644 sdk/src/path.cc delete mode 100644 sdk/src/state.cc delete mode 100644 sdk/tests/CMakeLists.txt delete mode 100644 sdk/tests/lua.cc diff --git a/.clang-format b/.clang-format index 53ea2f0..48ac222 100644 --- a/.clang-format +++ b/.clang-format @@ -26,14 +26,14 @@ AlignConsecutiveShortCaseStatements: AllowAllArgumentsOnNextLine: true AllowBreakBeforeNoexceptSpecifier: OnlyWithParen AlignEscapedNewlines: Left -AllowShortBlocksOnASingleLine: Always +AllowShortBlocksOnASingleLine: Never AllowShortCaseExpressionOnASingleLine: true AllowShortCompoundRequirementOnASingleLine: true AllowShortEnumsOnASingleLine: true AllowShortNamespacesOnASingleLine: true -AllowShortCaseLabelsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true AllowShortIfStatementsOnASingleLine: AllIfsAndElse -AllowShortLoopsOnASingleLine: true +AllowShortLoopsOnASingleLine: false # AllowShortRecordOnASingleLine = Always # v23 AlwaysBreakTemplateDeclarations: true BreakAdjacentStringLiterals: false @@ -85,11 +85,12 @@ IndentExportBlock: true IndentExternBlock: true IndentPPDirectives: BeforeHash IndentRequiresClause: false +InsertBraces: false +InsertNewlineAtEOF: false IntegerLiteralSeparator: Decimal: 3 IncludeBlocks: Merge IncludeIsMainRegex: '$' -InsertNewlineAtEOF: true LambdaBodyIndentation: Signature ReflowComments: Always RequiresClausePosition: WithPreceding diff --git a/.clang-tidy b/.clang-tidy index 4b23d76..35d31a2 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,7 +6,9 @@ Checks: > cert-*, performance-*, -bugprone-switch-missing-default-case, - -bugprone-easily-swappable-parameters + -bugprone-easily-swappable-parameters, + -clang-diagnostic-gnu-statement-expression, + -clang-diagnostic-gnu-statement-expression-from-macro-expansion # readability-* # modernize-*, # cppcoreguidelines-*, diff --git a/.gitignore b/.gitignore index d2fbdfc..d85adb7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ out/ *.bak *.tmp +*.old # Prerequisites *.d diff --git a/CMakeLists.txt b/CMakeLists.txt index b570113..b45b57b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,27 +42,20 @@ if (BUILD_TESTS) endif () add_subdirectory (core) +add_subdirectory (game) add_subdirectory (sys) add_subdirectory (gfx) -add_subdirectory (sdk) add_subdirectory (app) -add_subdirectory (game) add_library(project_warnings INTERFACE) -if (MSVC) - target_compile_options(project_warnings INTERFACE /W4) -else () - target_compile_options(project_warnings INTERFACE -Wall -Wextra -Wpedantic) -endif() +target_compile_options(project_warnings INTERFACE -Wall -Wextra -Wpedantic -fno-exceptions) if (ENABLE_DEEP_ANALYSIS) - if (MSVC) - target_compile_options (project_warnings INTERFACE /analyze) - elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options (project_warnings INTERFACE -fanalyzer) else () - message (FATAL_ERROR "ENABLE_DEEP_ANALYSIS is ON, but ${CMAKE_CXX_COMPILER_ID} doesn't have an analyzer.") + message (FATAL_ERROR "ENABLE_DEEP_ANALYSIS requires the GCC compiler.") endif () endif () diff --git a/README.md b/README.md index 092c27f..e8448d4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is the early development of a game and engine built from scratch in C++23. ### Toolchain -- A C++23 compiler: GCC 14+, Clang 17+, or MSVC 19.38+ +- A C++23 compiler that supports GNU statement expressions: GCC 14+, Clang 17+ - [CMake](https://cmake.org/) 3.25+ - [Lua](https://www.lua.org/) 5.4+ diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 8e53feb..d3b5a92 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,14 +1,17 @@ -add_library (app) +find_package(Lua 5.4 REQUIRED) -target_include_directories ( +add_library(app) + +target_include_directories( app + PRIVATE ${LUA_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) -target_link_libraries ( +target_link_libraries( app - PRIVATE sys gfx - PUBLIC core game + PRIVATE ${LUA_LIBRARIES} gfx + PUBLIC game ) -add_subdirectory (src) +add_subdirectory(src) diff --git a/app/include/app/application.hh b/app/include/app/application.hh deleted file mode 100644 index 1449625..0000000 --- a/app/include/app/application.hh +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "game/pilot.hh" -#include "gfx/camera2D.hh" -#include "gfx/renderer.hh" -#include "gfx/texture.hh" -#include "sys/window.hh" -#include -#include - -union SDL_Event; -struct SDL_Texture; -struct CommandBuffer; -struct TextureDef; - -struct Application -{ - Application() = default; - Application(Application&&) noexcept = default; - Application(Application const&) = delete; - Application& operator=(Application&&) noexcept = default; - Application& operator=(Application const&) = delete; - - Result load(LoadContext ctx, Entity player); - void handle_event(SDL_Event const& event); - void step(CommandBuffer& cmds, ConstContext ctx); - void update(ConstContext ctx, f32 dt); - void render(ConstContext ctx, f32 a); - -private: - struct Player - { - Entity entity; - std::unique_ptr pilot; - Camera2D camera; - }; - - Player player_; - Window window_; - Renderer renderer_; - std::vector textures_; -}; diff --git a/app/include/app/content.hh b/app/include/app/content.hh new file mode 100644 index 0000000..f8ea887 --- /dev/null +++ b/app/include/app/content.hh @@ -0,0 +1,19 @@ +#pragma once +#include "app/sprite.hh" +#include "game/content/terrain.hh" + +struct Texture; + +namespace Lua { + struct Table; + + bool import(Color&, Table const&); + + bool import(Game::Content&, Table const&); + + bool import(App::Assets&, App::Sprite&, Table const&); + + bool import(Game::Content&, Game::Terrain&, Table const&); + + bool read(Content&, Lua::Table const&, Sprite&); +} diff --git a/app/include/app/keyboard.hh b/app/include/app/keyboard-pilot.hh similarity index 53% rename from app/include/app/keyboard.hh rename to app/include/app/keyboard-pilot.hh index aa33338..30ed4a5 100644 --- a/app/include/app/keyboard.hh +++ b/app/include/app/keyboard-pilot.hh @@ -1,17 +1,12 @@ #pragma once - -// TODO: polling of action results -// TODO: HerdPilot - -#include "game/pilot.hh" -#include "game/types.hh" +#include "app/pilot.hh" #include "sys/keyboard.hh" #include "sys/mouse.hh" struct KeyboardPilot : Pilot { void handle_event(SDL_Event const& event) override; - void steer(CommandBuffer& out, ConstContext ctx, Entity e) override; + void steer(World const&, CommandBuffer& out, Entity e) override; private: diff --git a/app/include/app/lua/key.hh b/app/include/app/lua/key.hh new file mode 100644 index 0000000..5269ff7 --- /dev/null +++ b/app/include/app/lua/key.hh @@ -0,0 +1,57 @@ +#pragma once +#include "core/panic.hh" +#include "core/types.hh" +#include + +namespace Lua { + + // Note: Key does not own or manage a copy of the string value. It holds a string view. + struct Key + { + enum class Kind : u8 { Index, Name }; + + constexpr Key(char const* name) : _kind{ Kind::Name }, _name{ name } {} + + constexpr Key(std::string_view name) : _kind{ Kind::Name }, _name{ name } {} + + constexpr Key(int index) noexcept : _kind{ Kind::Index }, _index{ index } {} + + constexpr Key& operator=(std::string_view name) + { + _kind = Kind::Name; + _name = name; + return *this; + } + + constexpr Key& operator=(int index) noexcept + { + _kind = Kind::Index; + _index = index; + return *this; + } + + [[nodiscard]] std::string_view as_name() const + { + DEBUG_ASSERT(_kind == Kind::Name); + return std::string_view(_name.begin(), _name.end()); + } + + [[nodiscard]] int as_index() const noexcept + { + DEBUG_ASSERT(_kind == Kind::Index); + return _index; + } + + [[nodiscard]] int is_index() const noexcept { return _kind == Kind::Index; } + + [[nodiscard]] int is_name() const noexcept { return _kind == Kind::Name; } + + [[nodiscard]] Kind kind() const noexcept { return _kind; } + + private: + Kind _kind = Kind::Index; + int _index = 0; + std::string_view _name = {}; + }; + +} // namespace Lua diff --git a/game/include/game/load.hh b/app/include/app/lua/load.hh similarity index 100% rename from game/include/game/load.hh rename to app/include/app/lua/load.hh diff --git a/app/include/app/lua/reference.hh b/app/include/app/lua/reference.hh new file mode 100644 index 0000000..c80e836 --- /dev/null +++ b/app/include/app/lua/reference.hh @@ -0,0 +1,40 @@ +#pragma once +#include "sdk/types.hh" + +namespace Lua { + + struct State; + + // Must not outlive the state that created it. + struct Reference + { + Reference() noexcept; + Reference(State* state, i32 ridx) noexcept; + Reference(Reference const&) noexcept; + Reference(Reference&&) noexcept; + Reference& operator=(Reference const&) noexcept; + Reference& operator=(Reference&&) noexcept; + ~Reference() noexcept; + + // Returns true if both refer to the same Lua value. + friend bool operator==(Reference const& l, Reference const& r) noexcept; + + friend bool operator!=(Reference const& l, Reference const& r) noexcept + { + return !(operator==(l, r)); + } + + [[nodiscard]] Type type() const noexcept { return _type; } + + [[nodiscard]] State& state() const noexcept { return *_state; } + + // Pushes the referenced value onto the top of the stack. + int push() const noexcept; + + private: + State* _state; // non-owning pointer + int _ridx; + Type _type; + }; + +} // namespace Lua diff --git a/app/include/app/lua/state.hh b/app/include/app/lua/state.hh new file mode 100644 index 0000000..eb0a948 --- /dev/null +++ b/app/include/app/lua/state.hh @@ -0,0 +1,40 @@ +#pragma once + +#include "core/result.hh" +#include "sdk/table.hh" +#include "sdk/value.hh" +#include + +struct lua_State; + +namespace Lua { + + struct State + { + [[nodiscard]] static Result create(); + + State() = default; + State(State const&) = delete; + State& operator=(State const&) = delete; + + State(State&&) noexcept; + State& operator=(State&&) noexcept; + ~State() noexcept; + + [[nodiscard]] Status load(std::string_view file); + + [[nodiscard]] Status execute(std::string_view name, std::string_view source); + + [[nodiscard]] Table globals() noexcept; + + [[nodiscard]] Table create_table() noexcept; + + [[nodiscard]] lua_State* get() const noexcept { return _handle; } + + private: + explicit State(lua_State*) noexcept; + + lua_State* _handle; + }; + +} // namespace Lua diff --git a/app/include/app/lua/table.hh b/app/include/app/lua/table.hh new file mode 100644 index 0000000..602573d --- /dev/null +++ b/app/include/app/lua/table.hh @@ -0,0 +1,57 @@ +#pragma once + +// TODO: ability to iterate over all non-nil fields in the table + +#include "sdk/key.hh" +#include "sdk/reference.hh" +#include "sdk/value.hh" +#include + +namespace Lua { + + struct Table; + + template + concept TableReadable = requires (T& dst, Table const& src) { + { read(dst, src) } -> std::same_as; + }; + + // Must not outlive the state that created it. + // Ignores metamethods... + struct Table + { + Table(Reference, std::string path) noexcept; + + template + [[nodiscard]] bool read(T& dst) const + { + return read(dst, *this); + } + + // Returns a nil-value object on missing field + [[nodiscard]] Value operator[](Key key) const noexcept; + + void set(Key key, bool value) const noexcept; + void set(Key key, f64 value) const noexcept; + void set(Key key, i64 value) const noexcept; + void set(Key key, std::string_view value) const noexcept; + + void set(Key key, Table const& table) const noexcept { set(key, table._reference); } + + void set(Key key, Value const& value) const noexcept { set(key, value._reference); } + + // Assumes that the handle was produced by the same state. + void set(Key key, Reference const& handle) const noexcept; + + void erase(Key key) const noexcept; + + [[nodiscard]] std::vector keys() const; + + [[nodiscard]] std::string_view path() const { return _path; } + + private: + Reference _reference; + std::string _path; + }; + +} // namespace Lua diff --git a/app/include/app/lua/types.hh b/app/include/app/lua/types.hh new file mode 100644 index 0000000..5df8f25 --- /dev/null +++ b/app/include/app/lua/types.hh @@ -0,0 +1,39 @@ +#pragma once +#include "core/types.hh" +#include + +namespace Lua { + + enum class Type : i8 { + Nil = 0, + Boolean = 1, + LightUserData = 2, + Number = 3, + String = 4, + Table = 5, + Function = 6, + UserData = 7, + Thread = 8, + Integer = 9 + }; + + [[nodiscard]] constexpr std::string_view type_name(Type type) noexcept + { + switch (type) { + case Type::Nil: return "nil"; + case Type::Boolean: return "boolean"; + case Type::LightUserData: return "light userdata"; + case Type::Number: return "number"; + case Type::String: return "string"; + case Type::Table: return "table"; + case Type::Function: return "function"; + case Type::UserData: return "userdata"; + case Type::Thread: return "thread"; + case Type::Integer: return "integer"; + } + return "none"; + } + + using Number = f64; + using Integer = i64; +} // namespace Lua diff --git a/app/include/app/lua/value.hh b/app/include/app/lua/value.hh new file mode 100644 index 0000000..020d0c9 --- /dev/null +++ b/app/include/app/lua/value.hh @@ -0,0 +1,117 @@ +#pragma once +#include "core/types.hh" +#include "sdk/reference.hh" +#include "sdk/types.hh" +#include +#include +#include +#include + +struct Rectangle; +struct Color; + +namespace Lua { + struct Table; + struct Value; + + template + concept ValueReadable = requires (T& dst, Value const& src) { + { read(dst, src) } -> std::same_as; + }; + + bool read(bool& dst, Value const& srct); + + bool read(std::string& dst, Value const& srct); + + template + bool read(T& dst, Value const& srct); + + template + bool read(T& dst, Value const& src); + + // Must not outlive the state that created it. + struct Value + { + Value(Reference, std::string path) noexcept; + + // Problem: accumulate multiple errors and provide full diagnostic + // Problem: diagnostic should include path + // Problem: diagnostic can include warnings on happy path + template + [[nodiscard]] bool read(T& dst) const + { + return read(dst, *this); + } + + // Intended to be called once the type is known (i.e. in a switch statement over type). + // In debug: asserts on type mismatch. + // In release: same behavior as equivalent lua C API. + [[nodiscard]] bool to_boolean() const noexcept; + [[nodiscard]] f64 to_number() const noexcept; + [[nodiscard]] i64 to_integer() const noexcept; + [[nodiscard]] std::string_view to_string() const noexcept; + [[nodiscard]] Table to_table() const noexcept; + + [[nodiscard]] std::optional expect_boolean() const; + [[nodiscard]] std::optional expect_integer() const; + [[nodiscard]] std::optional expect_integer_range(Integer min, Integer max) const; + [[nodiscard]] std::optional expect_number() const; + [[nodiscard]] std::optional expect_number_range(Number min, Number max) const; + [[nodiscard]] std::optional expect_string() const; + [[nodiscard]] std::optional expect_table() const; + + [[nodiscard]] Type type() const noexcept { return _reference.type(); } + + [[nodiscard]] bool is_nil() const noexcept { return type() == Type::Nil; } + + [[nodiscard]] bool is_boolean() const noexcept { return type() == Type::Boolean; } + + [[nodiscard]] bool is_integer() const noexcept { return type() == Type::Integer; } + + [[nodiscard]] bool is_number() const noexcept { return type() == Type::Number; } + + [[nodiscard]] bool is_string() const noexcept { return type() == Type::String; } + + [[nodiscard]] bool is_table() const noexcept { return type() == Type::Table; } + + [[nodiscard]] std::string describe() const; + + [[nodiscard]] std::string_view path() const { return _path; } + + private: + friend Table; + + Reference _reference; + std::string _path; + }; + + bool read(bool& dst, Value const& value); + bool read(Integer& dst, Value const& value); + bool read(Number& dst, Value const& value); + bool read(std::string& dst, Value const& value); + + template + bool read(T& dst, Value const& src) + { + auto integer + = src.expect_integer_range(std::numeric_limits::lowest(), std::numeric_limits::max()); + if (!integer) return false; + dst = static_cast(*integer); + return true; + } + + template + bool read(T& dst, Value const& src) + { + auto number + = src.expect_number_range(std::numeric_limits::lowest(), std::numeric_limits::max()); + if (!number) return false; + dst = static_cast(*number); + return true; + } + + bool read(Rectangle& rect, Value const& src); + bool read(Color& color, Value const& src); + +} // namespace Lua + diff --git a/app/include/app/pilot.hh b/app/include/app/pilot.hh new file mode 100644 index 0000000..e173df8 --- /dev/null +++ b/app/include/app/pilot.hh @@ -0,0 +1,14 @@ +#pragma once +#include "game/entity.hh" + +union SDL_Event; +struct CommandBuffer; + +// Generates commands to pass to Actors +struct Pilot +{ + virtual void handle_event(SDL_Event const& event) = 0; + virtual void steer(World const&, CommandBuffer&, Entity e) = 0; + virtual ~Pilot() = default; +}; + diff --git a/app/include/app/sprite.hh b/app/include/app/sprite.hh new file mode 100644 index 0000000..16e24e4 --- /dev/null +++ b/app/include/app/sprite.hh @@ -0,0 +1,15 @@ +#pragma once +#include "gfx/color.hh" +#include "gfx/rectangle.hh" + +struct Texture; + +// TODO: make this a user-interface implementation detail +constexpr f32 pixels_per_unit = 16.f; + +struct Sprite +{ + Handle atlas; + Rectangle source; + Color tint; +}; diff --git a/app/include/app/state.hh b/app/include/app/state.hh new file mode 100644 index 0000000..9c22e10 --- /dev/null +++ b/app/include/app/state.hh @@ -0,0 +1,48 @@ +#pragma once +#include "app/pilot.hh" +#include "core/basic-catalog.hh" +#include "core/result.hh" +#include "game/world.hh" +#include "gfx/camera2D.hh" +#include "gfx/renderer.hh" +#include "gfx/texture.hh" +#include "sys/window.hh" +#include +#include + +union SDL_Event; +struct SDL_Texture; +struct CommandBuffer; + +namespace App { + using Assets = TypeList; + using Catalog = BasicCatalog; + + struct Player + { + Entity entity; + std::unique_ptr pilot; + Camera2D camera; + }; + + struct State + { + State() = default; + State(State&&) noexcept = default; + State(State const&) = delete; + State& operator=(State&&) noexcept = default; + State& operator=(State const&) = delete; + + Result load(World&, Entity player); + void handle_event(SDL_Event const&); + void step(World const&, CommandBuffer&); + void update(World const&, f32 delta); + void render(World const&, f32 alpha); + + private: + Player _player; + Window _window; + Renderer _renderer; + Catalog _textures; + }; +} diff --git a/app/src/application.cc b/app/src/application.cc index cf2a95a..ac1b6cd 100644 --- a/app/src/application.cc +++ b/app/src/application.cc @@ -79,18 +79,14 @@ void Application::handle_event(SDL_Event const& event) case SDL_EVENT_KEY_DOWN: switch (event.key.scancode) { case SDL_SCANCODE_ESCAPE: - case SDL_SCANCODE_Q: - push_event(make_quit_event()); - return; - default: - break; + case SDL_SCANCODE_Q: push_event(make_quit_event()); return; + default: break; } break; case SDL_EVENT_MOUSE_WHEEL: player_.camera.zoom = glm::clamp(player_.camera.zoom + 0.5f * event.wheel.y, 0.7f, 1.9f); return; - default: - break; + default: break; } if (player_.pilot) player_.pilot->handle_event(event); diff --git a/app/src/content.cc b/app/src/content.cc new file mode 100644 index 0000000..98a4af9 --- /dev/null +++ b/app/src/content.cc @@ -0,0 +1,36 @@ +#include "game/terrain.hh" +#include "sdk/content.hh" +#include "sdk/table.hh" + +namespace sdk { + + static bool import_terrain(Content& content, Lua::Table const& root) + { + bool ok = true; + Terrain terrain; + for (auto key : root.keys()) { + if (key.is_index()) { + ok &= false; + std::println( + "{}[{}]: terrain entries must be keyed by string", root.path(), key.as_index()); + continue; + } + + auto record = root[key].expect_table(); + if (!record) { + ok &= false; + continue; + } + + ok &= import(content, terrain, *record); + define(content, key.as_name(), terrain); + } + return ok; + } + + bool import(Content& content, Lua::Table const& root) + { + if (auto table = root["terrain"].expect_table()) import_terrain(content, *table); + } + +} diff --git a/app/src/load.cc b/app/src/load.cc new file mode 100644 index 0000000..9154f97 --- /dev/null +++ b/app/src/load.cc @@ -0,0 +1,59 @@ +#include "game/content.hh" +#include "game/context.hh" +#include "game/sprite.hh" +#include "game/terrain.hh" +#include "game/types.hh" +#include "sdk/node.hh" +#include "sdk/state.hh" +#include "sdk/table.hh" +#include "sdk/value.hh" +#include + +static bool compile_terrain(Lua::Table& table, CatalogWriter catalog) +{ + bool ok = true; + Terrain terrain; + for (auto key : table.keys()) { + terrain = {}; + if (key.is_index()) continue; + ok = table[key].read(terrain); + catalog.emplace(key.as_name(), terrain); + } + return ok; +} + +static void init_lua(Lua::State& lua) +{ + Lua::Table const G = lua.globals(); + Lua::Table const content = lua.create_table(); + + G.set("content", content); + content.set("terrain", lua.create_table()); +}; + +bool load_content(Content&, LuaTable) +{ + auto lua = Lua::State::create(); + if (!lua) return false; + init_lua(*lua); + + if (!lua->load("content/terrain.lua")) return false; + + Lua::Table const G = lua->globals(); + + auto content_table = G["content"].expect_table(); + if (!content_table) return false; + + Content content; + content_table-> + + std::println("Failed to compile content!"); + return read() + + return compile_content(*content, access_catalog(context)); +} + +// upvalues? requires parsing to be on call: +// type label {...} +// DTOs? +// extra space? diff --git a/app/src/lua/CMakeLists.txt b/app/src/lua/CMakeLists.txt new file mode 100644 index 0000000..60eb3cf --- /dev/null +++ b/app/src/lua/CMakeLists.txt @@ -0,0 +1 @@ +target_sources (sdk PRIVATE table.cc value.cc reference.cc) diff --git a/app/src/lua/reference.cc b/app/src/lua/reference.cc new file mode 100644 index 0000000..ee08167 --- /dev/null +++ b/app/src/lua/reference.cc @@ -0,0 +1,94 @@ +#include "core/panic.hh" +#include "sdk/reference.hh" +#include "sdk/state.hh" +#include + +namespace Lua { + + static Type get_type(lua_State* L, int idx) noexcept + { + if (lua_isinteger(L, idx)) return Type::Integer; + return static_cast(idx); + } + + Reference::Reference(State* state, i32 ridx) noexcept + : _state{ state }, _ridx{ ridx }, _type{ Type::None } + { + PRECONDITION(_state != nullptr); + lua_State* const L = _state->handle(); + lua_rawgeti(L, LUA_REGISTRYINDEX, _ridx); + _type = get_type(L, -1); + lua_pop(L, 1); + } + + Reference::Reference(Reference const& other) noexcept + : _state{ other._state }, _ridx{ LUA_NOREF }, _type{ Type::None } + { + lua_State* const L = _state->handle(); + lua_rawgeti(L, LUA_REGISTRYINDEX, other._ridx); + _type = get_type(L, -1); + _ridx = luaL_ref(L, LUA_REGISTRYINDEX); + } + + Reference::Reference(Reference&& other) noexcept + : _state{ other._state }, _ridx{ other._ridx }, _type{ other._type } + { + other._ridx = LUA_NOREF; + other._type = Type::None; + } + + Reference& Reference::operator=(Reference const& other) noexcept + { + if (&other == this) return *this; + if (_ridx != LUA_NOREF) luaL_unref(_state->handle(), LUA_REGISTRYINDEX, _ridx); + _state = other._state; + lua_State* const L = _state->handle(); + lua_rawgeti(L, LUA_REGISTRYINDEX, other._ridx); + _type = get_type(L, -1); + _ridx = luaL_ref(L, LUA_REGISTRYINDEX); + return *this; + } + + Reference& Reference::operator=(Reference&& other) noexcept + { + if (&other == this) return *this; + if (_ridx != LUA_NOREF) luaL_unref(_state->handle(), LUA_REGISTRYINDEX, _ridx); + _state = other._state; + _ridx = other._ridx; + _type = other._type; + other._ridx = LUA_NOREF; + other._type = Type::None; + return *this; + } + + Reference::~Reference() noexcept + { + lua_State* const L = _state->handle(); + if (_ridx != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, _ridx); + } + } + + bool operator==(Reference const& l, Reference const& r) noexcept + { + if (l._state != r._state) return false; + lua_State* const L = l._state->handle(); + if (L == nullptr) return true; + if (l._ridx == r._ridx) return true; + lua_rawgeti(L, LUA_REGISTRYINDEX, l._ridx); + lua_rawgeti(L, LUA_REGISTRYINDEX, r._ridx); + bool out = lua_rawequal(L, -1, -2); + lua_pop(L, 2); + return out; + } + + int Reference::push() const noexcept + { + lua_State* const L = _state->handle(); + DEBUG_ASSERT(_ridx != LUA_NOREF); + lua_rawgeti(L, LUA_REGISTRYINDEX, _ridx); + DEBUG_ASSERT(_type == get_type(L, -1)); + return lua_gettop(L); + } + +} // namespace Lua diff --git a/app/src/lua/state.cc b/app/src/lua/state.cc new file mode 100644 index 0000000..3e331b6 --- /dev/null +++ b/app/src/lua/state.cc @@ -0,0 +1,117 @@ +#include "core/defer.hh" +#include "core/panic.hh" +#include "sdk/reference.hh" +#include "sdk/state.hh" +#include +#include +#include +#include +#include +#include + +namespace Lua { + static int traceback(lua_State* L) + { + char const* message = lua_tostring(L, 1); + if (message == nullptr) return 1; + luaL_traceback(L, L, message, 1); + return 1; + } + + static void open_libraries(lua_State* L) + { + struct Library + { + char const* name; + lua_CFunction open; + }; + + constexpr Library libraries[] = { + { LUA_GNAME, luaopen_base }, { LUA_TABLIBNAME, luaopen_table }, + { LUA_STRLIBNAME, luaopen_string }, { LUA_MATHLIBNAME, luaopen_math }, + { LUA_UTF8LIBNAME, luaopen_utf8 }, + }; + + for (Library const& library : libraries) { + luaL_requiref(L, library.name, library.open, 1); + lua_pop(L, 1); + } + + constexpr char const* unavailable[] = { "collectgarbage", "dofile", "load", "loadfile" }; + for (char const* name : unavailable) { + lua_pushnil(L); + lua_setglobal(L, name); + } + } + + // Ignores meta-methods in the global table. + static void push_global(lua_State* L, std::string_view name) noexcept + { + lua_pushglobaltable(L); + lua_pushlstring(L, name.data(), name.size()); + lua_rawget(L, -2); + lua_replace(L, -2); + } + + static std::string pop_string(lua_State* L) + { + size_t length = 0; + char const* data = lua_tolstring(L, -1, &length); + std::string string(data, length); + lua_pop(L, 1); + return string; + } + + static Status call(lua_State* L) + { + lua_pushcfunction(L, traceback); + auto errh = lua_gettop(L); + DEFER(lua_remove(L, errh)); + int const status = lua_pcall(L, 0, 0, errh); + if (status != LUA_OK) return Error(pop_string(L)); + return {}; + } + + Result State::create() + { + lua_State* L = luaL_newstate(); + if (L == nullptr) return Error("failed to create Lua state"); + open_libraries(L); + return State(L); + } + + State::State(State&& other) noexcept : _handle{ other._handle } { other._handle = nullptr; } + + State& State::operator=(State&& other) noexcept + { + if (&other == this) return *this; + _handle = other._handle; + other._handle = nullptr; + return *this; + } + + State::~State() noexcept { lua_close(_handle); } + + Table State::globals() noexcept { return Table(_handle, LUA_RIDX_GLOBALS); } + + Table State::create_table(LuaPath const& path) noexcept + { + lua_newtable(_handle); + return Table(_handle, luaL_ref(_handle, LUA_REGISTRYINDEX)); + } + + Status State::load(std::string_view path) + { + auto status = luaL_loadfile(_handle, std::string(path).c_str()); + if (status != LUA_OK) return Error(pop_string(_handle)); + return call(_handle); + } + + Status State::execute(std::string_view name, std::string_view source) + { + auto status = luaL_loadbuffer(_handle, source.data(), source.size(), std::string(name).c_str()); + if (status != LUA_OK) return Error(pop_string(_handle)); + return call(_handle); + } + +} // namespace Lua diff --git a/app/src/lua/table.cc b/app/src/lua/table.cc new file mode 100644 index 0000000..1def4c2 --- /dev/null +++ b/app/src/lua/table.cc @@ -0,0 +1,98 @@ +#include "core/panic.hh" +#include "sdk/state.hh" +#include "sdk/table.hh" +#include +#include +#include + +namespace Lua { + static void push_key(lua_State* L, Key key) noexcept + { + if (key.is_name()) { + auto str = key.as_name(); + lua_pushlstring(L, str.data(), str.size()); + } else lua_pushinteger(L, key.as_index()); + } + + Table::Table(Reference reference) noexcept : Reference(std::move(reference)) + { + DEBUG_ASSERT(reference.type() == Type::Table); + } + + void Table::set(Key key, bool value) const noexcept + { + lua_State* const L = state(); + int const idx = push(); + push_key(L, key); + lua_pushboolean(L, value); + lua_rawset(L, idx); + lua_pop(L, 1); + } + + void Table::set(Key key, f64 value) const noexcept + { + lua_State* const L = state(); + int const idx = push(); + push_key(L, key); + lua_pushnumber(L, value); + lua_rawset(L, idx); + lua_pop(L, 1); + } + + void Table::set(Key key, i64 value) const noexcept + { + lua_State* const L = state(); + int const idx = push(); + push_key(L, key); + lua_pushinteger(L, value); + lua_rawset(L, idx); + lua_pop(L, 1); + } + + void Table::set(Key key, std::string_view value) const noexcept + { + lua_State* const L = state(); + int const idx = push(); + push_key(L, key); + lua_pushlstring(L, value.data(), value.size()); + lua_rawset(L, idx); + lua_pop(L, 1); + } + + void Table::set(Key key, Reference const& value) const noexcept + { + PRECONDITION(value.state() == state()); + lua_State* const L = state(); + int const idx = push(); + push_key(L, key); + value.push(); + lua_rawset(L, idx); + lua_pop(L, 1); + } + + void Table::erase(Key key) const noexcept + { + lua_State* const L = state().get(); + int const idx = push(); + push_key(L, key); + lua_pushnil(L); + lua_rawset(L, idx); + lua_pop(L, 1); + } + + Value Table::operator[](Key key) const noexcept + { + lua_State* const L = state().get(); + int const idx = push(); + push_key(L, key); + lua_rawget(L, idx); + return Value({ &state(), luaL_ref(L, LUA_REGISTRYINDEX) }); + } + + Status Table::expect_table(Key key) const + { + if (type() != Type::Table) return type_error(Type::Table, type()); + return {}; + } + +} // namespace Lua diff --git a/app/src/lua/types.cc b/app/src/lua/types.cc new file mode 100644 index 0000000..a5c34a3 --- /dev/null +++ b/app/src/lua/types.cc @@ -0,0 +1,38 @@ +#include "sdk/types.hh" +#include +#include + +namespace Lua { + + static std::string list(std::initializer_list types) + { + auto it = types.begin(); + std::string out(type_name(*it)); + while (++it != types.end()) { + out += ", "; + out += type_name(*it); + } + return out; + } + + Error type_error(Type expected, Type found) + { + return Error{ std::format("expected {}, found {}", type_name(expected), type_name(found)) }; + } + + Error type_error(std::initializer_list expected, Type found) + { + return Error{ std::format("expected one of ({}), found {}", list(expected), type_name(found)) }; + } + + Error range_error(i64 min, i64 max, i64 found) + { + return Error{ std::format("expected integer in range [{}, {}], found {}", min, max, found) }; + } + + Error range_error(f64 min, f64 max, f64 found) + { + return Error{ std::format("expected number in range [{}, {}], found {}", min, max, found) }; + } + +} diff --git a/app/src/lua/value.cc b/app/src/lua/value.cc new file mode 100644 index 0000000..73a26c6 --- /dev/null +++ b/app/src/lua/value.cc @@ -0,0 +1,165 @@ +#include "core/color.hh" +#include "core/rectangle.hh" +#include "sdk/state.hh" +#include "sdk/table.hh" +#include "sdk/value.hh" +#include +#include + +namespace Lua { + + Value::Value(Handle handle) noexcept : Handle(std::move(handle)) {} + + bool Value::to_boolean() const noexcept + { + lua_State* const L = state(); + push(); + bool boolean = lua_toboolean(L, -1); + lua_pop(L, 1); + return boolean; + } + + f64 Value::to_number() const noexcept + { + lua_State* const L = state(); + push(); + f64 number = lua_tonumber(L, -1); + lua_pop(L, 1); + return number; + } + + i64 Value::to_integer() const noexcept + { + lua_State* const L = state(); + push(); + i64 integer = lua_tointeger(L, -1); + lua_pop(L, 1); + return integer; + } + + std::string Value::to_string() const + { + lua_State* const L = state(); + push(); + std::string string = lua_tostring(L, -1); + lua_pop(L, 1); + return string; + } + + Status Value::expect(Type expected) const + { + if (type() == expected) return {}; + Error e{ "expected " }; + e.error() += type_name(Type::Table); + e.error() += ", found "; + e.error() += describe(); + return e; + } + + std::string Value::describe() const + { + std::string description{ type_name(type()) }; + switch (type()) { + case Type::Boolean: description += (to_boolean() ? "true" : "false"); break; + case Type::Integer: description += std::to_string(to_integer()); break; + case Type::Number: description += std::to_string(to_number()); break; + case Type::String: + description += '"'; + description += to_string(); + description += '"'; + break; + default: break; + } + return description; + } + + bool read(Value const& src, bool& dst) + { + auto boolean = src.expect_boolean(); + if (!boolean) return false; + dst = *boolean; + return true; + } + + bool read(Value const& src, Integer& dst) + { + auto integer = src.expect_integer(); + if (!integer) return false; + dst = *integer; + return true; + } + + bool read(Value const& src, Number& dst) + { + auto number = src.expect_number(); + if (!number) return false; + dst = *number; + return true; + } + + bool read(Value const& src, std::string& dst) + { + auto string = src.expect_string(); + if (!string) return false; + dst = std::string(*string); + return true; + } + + bool read(Value const& value, Rectangle& rect) + { + Table table = ({ + auto table = value.expect_table(); + if (!table) return false; + std::move(*table); + }); + + bool ok = true; + ok &= table["x"].read(rect.origin.x); + ok &= table["y"].read(rect.origin.y); + ok &= table["w"].read(rect.extent.x); + ok &= table["h"].read(rect.extent.y); + return ok; + } + + bool read(Value const& value, Color& color) + { + switch (value.type()) { + case Lua::Type::Integer: + { + u32 rgba; + if (!value.read(rgba)) return false; + color = make_color_hex(rgba); + break; + } + case Lua::Type::Table: + { + Lua::Table table = value.to_table(); + + bool ok = true; + ok &= table["r"].read(color.r); + ok &= table["g"].read(color.g); + ok &= table["b"].read(color.b); + ok &= table["a"].read(color.a); + if (!ok) return false; + break; + } + default: + std::println("{}: expected integer or table, found {}", value.path(), value.describe()); + return false; + } + return true; + } + +} // namespace Lua + +// Result Value::expect_bounded_integer(Integer min, Integer max) const {} + +// Result Value::expect_bounded_number(Number min, Number max) const +// { +// f64 number = TRY(expect_number()); +// if (number > max || number < min) { +// // return range_error_message( +// // src.type(), std::numeric_limits::lowest(), std::numeric_limits::max(), number); +// } +// } + diff --git a/app/src/sprite.cc b/app/src/sprite.cc new file mode 100644 index 0000000..3c04f3f --- /dev/null +++ b/app/src/sprite.cc @@ -0,0 +1,17 @@ +#include "game/world.hh" +#include "sdk/lua/value.hh" + +bool read(World& world, Value const& value, Sprite& sprite) +{ + Table table = ({ + auto table = value.expect_table(); + if (!table) return false; + std::move(*table); + }); + + bool ok = true; + ok &= table["atlas"].read(sprite.atlas); + ok &= table["source"].read(sprite.source); + ok &= table["tint"].read(sprite.tint); + return ok; +} diff --git a/architecture.md b/architecture.md new file mode 100644 index 0000000..1056748 --- /dev/null +++ b/architecture.md @@ -0,0 +1,19 @@ +```mermaid +--- +config: + look: handDrawn + theme: neutral + layout: elk +--- +flowchart RL + Core(Core) + Platform(Platform) + Game(Game) + Graphics(Graphics Engine) + App(Application) + Platform --> Core + Graphics --> Platform + Game --> Core + App --> Graphics + App --> Game +``` diff --git a/content/terrain.lua b/content/terrain.lua index f175c4e..51d9775 100644 --- a/content/terrain.lua +++ b/content/terrain.lua @@ -7,27 +7,27 @@ local function make_sprite(x, y, color) } end -content['terrain']['grass-1'] = { +content.terrain['grass-1'] = { sprite = make_sprite(5, 0, 0x3BD872FF) } -content['terrain']['grass-2'] = { +content.terrain['grass-2'] = { sprite = make_sprite(6, 0, 0x43FF64FF) } -content['terrain']['grass-3'] = { +content.terrain['grass-3'] = { sprite = make_sprite(7, 0, 0x32DC96FF) } -content['terrain']['grass-tall'] = { +content.terrain['grass-tall'] = { sprite = make_sprite(0, 2, 0x0C9B64FF) } -content['terrain']['dirt'] = { +content.terrain.dirt = { sprite = make_sprite(2, 0, 0x79464bff) } -content['terrain']['rocks'] = { +content.terrain.rocks = { sprite = make_sprite(2, 0, 0xcec5b7ff) } diff --git a/core/include/core/basic-catalog.hh b/core/include/core/basic-catalog.hh index 6f2a0e2..7beb8a4 100644 --- a/core/include/core/basic-catalog.hh +++ b/core/include/core/basic-catalog.hh @@ -6,6 +6,7 @@ #include #include +// Goal: append-only, patchable, stable index, serializable template struct BasicCatalog; @@ -18,60 +19,74 @@ struct BasicCatalog> BasicCatalog& operator=(BasicCatalog const&) = delete; template - [[nodiscard]] bool valid(Token token) const + [[nodiscard]] bool valid(Handle handle) const { - return token.value < store_of().size(); + return handle.generation == _generation && handle.index < store_of().size(); } template - [[nodiscard]] Token find(std::string_view label) const + [[nodiscard]] Handle find(std::string_view label) const { - return { store_of().find(label) }; + return { store_of().find(label), _generation }; } template - [[nodiscard]] std::string_view label(Token token) const + [[nodiscard]] std::string_view label(Handle handle) const { - DEBUG_ASSERT(valid(token)); - return store_of().key(token.value); + DEBUG_ASSERT(valid(handle)); + return store_of().key(handle.index); } template - [[nodiscard]] T& operator[](Token token) + [[nodiscard]] T& operator[](Handle handle) { - DEBUG_ASSERT(valid(token)); - return store_of()[token.value]; + DEBUG_ASSERT(valid(handle)); + return store_of()[handle.index]; } template - [[nodiscard]] T const& operator[](Token token) const + [[nodiscard]] T const& operator[](Handle handle) const { - DEBUG_ASSERT(valid(token)); - return store_of()[token.value]; + DEBUG_ASSERT(valid(handle)); + return store_of()[handle.index]; + } + + template + Handle create(std::string_view label, T const& value) + { + return { store_of().emplace(label, value), _generation }; + } + + template + Handle create(std::string_view label, T&& value) + { + return { store_of().emplace(label, std::forward(value)), _generation }; } template requires std::constructible_from - Token emplace(std::string_view label, Args&&... args) + Handle emplace(std::string_view label, Args&&... args) { - return { store_of().emplace(label, std::forward(args)...) }; + return { store_of().emplace(label, std::forward(args)...), _generation }; } template - [[nodiscard]] T const* try_get(std::string_view label) const + [[nodiscard]] T const* try_get(Handle handle) const { - return store_of().try_get(label); + if (!valid(handle)) return nullptr; + return store_of()[handle.index]; } template - [[nodiscard]] T* try_get(std::string_view label) + [[nodiscard]] T* try_get(Handle handle) { - return store_of().try_get(label); + if (!valid(handle)) return nullptr; + return store_of()[handle.index]; } void clear() { - std::apply([](auto&... stores) { (stores.clear(), ...); }, stores_); + std::apply([](auto&... stores) { (stores.clear(), ...); }, _stores); } template @@ -101,17 +116,18 @@ private: template constexpr IndexedMap& store_of() { - return std::get>(stores_); + return std::get>(_stores); } template constexpr IndexedMap const& store_of() const { - return std::get>(stores_); + return std::get>(_stores); } BasicCatalog(BasicCatalog const&) = default; - std::tuple...> stores_; + u32 _generation; + std::tuple...> _stores; }; diff --git a/core/include/core/bit-set.hh b/core/include/core/bit-set.hh index b96d91b..36f5493 100644 --- a/core/include/core/bit-set.hh +++ b/core/include/core/bit-set.hh @@ -49,7 +49,8 @@ struct BitSet { PRECONDITION(this != &other); PRECONDITION(size_ == other.size_); - for (u32 i = 0u; i < size_; ++i) words_[i] |= other.words_[i]; + for (u32 i = 0u; i < size_; ++i) + words_[i] |= other.words_[i]; return *this; } @@ -63,7 +64,8 @@ struct BitSet { PRECONDITION(this != &other); PRECONDITION(size_ == other.size_); - for (u32 i = 0u; i < size_; ++i) words_[i] &= other.words_[i]; + for (u32 i = 0u; i < size_; ++i) + words_[i] &= other.words_[i]; return *this; } @@ -77,7 +79,8 @@ struct BitSet { PRECONDITION(this != &other); PRECONDITION(size_ == other.size_); - for (u32 i = 0u; i < size_; ++i) words_[i] ^= other.words_[i]; + for (u32 i = 0u; i < size_; ++i) + words_[i] ^= other.words_[i]; return *this; } diff --git a/core/include/core/defer.hh b/core/include/core/defer.hh index 012576a..ecdf238 100644 --- a/core/include/core/defer.hh +++ b/core/include/core/defer.hh @@ -13,8 +13,8 @@ struct Defer template Defer(F) -> Defer; -#define DEFER(expr) \ - Defer _defer_##__LINE__ \ - { \ - [&] { expr; } \ +#define DEFER(expr) \ + Defer _defer##__COUNTER__ \ + { \ + [&] { expr; } \ } diff --git a/core/include/core/grid2.hh b/core/include/core/grid2.hh index 2fc1b16..83e5a70 100644 --- a/core/include/core/grid2.hh +++ b/core/include/core/grid2.hh @@ -85,7 +85,8 @@ struct Grid2 { if (x + src.width() > width_ || y + src.height() > height_) return false; for (u32 j = 0u; j < src.height(); ++j) - for (u32 i = 0u; i < src.width(); ++i) (*this)[x + i, y + j] = src[i, j]; + for (u32 i = 0u; i < src.width(); ++i) + (*this)[x + i, y + j] = src[i, j]; return true; } diff --git a/core/include/core/result.hh b/core/include/core/result.hh index 39830b5..975645c 100644 --- a/core/include/core/result.hh +++ b/core/include/core/result.hh @@ -6,3 +6,10 @@ template using Result = std::expected; using Error = std::unexpected; using Status = Result; + +#define TRY(expr) \ + ({ \ + auto _result = (expr); \ + if (!_result) return _result; \ + std::move(*_result); \ + }) diff --git a/core/include/core/slot-map.hh b/core/include/core/slot-map.hh index d42517a..c29f3c2 100644 --- a/core/include/core/slot-map.hh +++ b/core/include/core/slot-map.hh @@ -217,7 +217,8 @@ public: [[nodiscard]] const_iterator cbegin() const noexcept { u32 i = 0u; - while (i < slots_.size() && !slots_[i].live) i++; + while (i < slots_.size() && !slots_[i].live) + i++; return const_iterator(this, i); } @@ -230,7 +231,8 @@ public: [[nodiscard]] iterator begin() noexcept { u32 i = 0u; - while (i < slots_.size() && !slots_[i].live) i++; + while (i < slots_.size() && !slots_[i].live) + i++; return iterator(this, i); } diff --git a/core/include/core/types.hh b/core/include/core/types.hh index 1b56cd9..3b702df 100644 --- a/core/include/core/types.hh +++ b/core/include/core/types.hh @@ -19,29 +19,13 @@ constexpr u32 nil = UINT32_MAX; enum AccessFlag : bool { Read = false, Write = true }; -// A stable, type-tagged index into an append-only container (i.e. `Catalog`). -// The tag prevents IDs representing different resources being silently interchanged. -template -struct Token -{ - // I would call this a Key but that's already used as a template parameter for containers. - - u32 value = nil; - - [[nodiscard]] static consteval Token null() noexcept { return {}; } - - [[nodiscard]] constexpr std::strong_ordering operator<=>(Token const&) const noexcept = default; - - [[nodiscard]] constexpr explicit operator bool() const noexcept { return value != nil; } -}; - // A type-tagged identifier to a recyclable index that may become stale (i.e. refer to a deleted // resource) similar to the role of a weak pointer. template struct Handle { - u32 index = nil; - u32 generation = nil; + uint32_t index = nil; + uint32_t generation = nil; [[nodiscard]] static consteval Handle null() noexcept { return { nil, nil }; } diff --git a/core/src/random.cc b/core/src/random.cc index b16519d..72cde9e 100644 --- a/core/src/random.cc +++ b/core/src/random.cc @@ -15,7 +15,8 @@ u64 random_seed() Xoshiro256ss::Xoshiro256ss(u64 seed) { SplitMix64 rng(seed); - for (int i = 0; i < 4; ++i) s[i] = rng(); + for (int i = 0; i < 4; ++i) + s[i] = rng(); } u64 Xoshiro256ss::operator()() diff --git a/core/tests/registry.cc b/core/tests/registry.cc index 6f66f1e..dc393ed 100644 --- a/core/tests/registry.cc +++ b/core/tests/registry.cc @@ -61,7 +61,8 @@ TEST_CASE("Registry – entity creation and destruction", "[registry][entity]") SECTION("destroying entities") { std::array, 10> entities; - for (auto& h : entities) h = r.create(); + for (auto& h : entities) + h = r.create(); for (u32 i = 0u; i < 10u; ++i) { REQUIRE(r.size() == 10u - i); auto const h = entities[i]; @@ -183,7 +184,8 @@ TEST_CASE("Registry – entity creation and destruction", "[registry][entity]") { // iterating empty registry int count = 0; - for (auto [h, _] : r) ++count; + for (auto [h, _] : r) + ++count; REQUIRE(count == 0); Handle a = r.create(), b = r.create(), c = r.create(), d = r.create(); @@ -236,7 +238,8 @@ TEST_CASE("Registry – entity creation and destruction", "[registry][entity]") SECTION("entity meta-data") { Handle h = r.create("foo"); - for (auto [_, e] : r) REQUIRE(e.name == "foo"); + for (auto [_, e] : r) + REQUIRE(e.name == "foo"); // meta-data is independent, per-entity (void)r.create("bar"); Entity const& e = r[h]; diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 9b25996..5703971 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -1,13 +1,11 @@ -add_library (game) +add_library(game) -target_include_directories ( +target_include_directories( game + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) -target_link_libraries ( - game - PUBLIC core gfx sdk -) +target_link_libraries(game PUBLIC core) -add_subdirectory (src) +add_subdirectory(src) diff --git a/game/include/game/biome/caves.hh b/game/include/game/biome/caves.hh index 6626d62..86bcc11 100644 --- a/game/include/game/biome/caves.hh +++ b/game/include/game/biome/caves.hh @@ -3,17 +3,19 @@ #include "core/types.hh" #include "game/chunk.hh" -// cellular automata rule used for cave generation -void generate_cave( - Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations); +namespace Game { + // cellular automata rule used for cave generation + void generate_cave( + Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations); -struct CaveGenerator : ChunkGenerator -{ - CaveGenerator(Context const ctx, u64 seed); - void generate(u32 x, u32 y, Chunk& chunk) override; + struct CaveGenerator : ChunkGenerator + { + CaveGenerator(World& world, u64 seed); + void generate(u32 x, u32 y, Chunk& chunk) override; -private: + private: - u64 seed_; - u32 wall_, floor_; -}; + u64 seed_; + u32 wall_, floor_; + }; +} diff --git a/game/include/game/biome/grassland.hh b/game/include/game/biome/grassland.hh index 52074ee..d2c6f95 100644 --- a/game/include/game/biome/grassland.hh +++ b/game/include/game/biome/grassland.hh @@ -1,15 +1,15 @@ #pragma once #include "game/chunk.hh" -struct Terrain; - -struct GrasslandGenerator : ChunkGenerator -{ - GrasslandGenerator(Context const ctx, u64 seed); - void generate(u32 x, u32 y, Chunk& chunk) override; - -private: - - u64 const seed_; - Token const terrain_[6]; -}; +namespace Game { + struct GrasslandGenerator : ChunkGenerator + { + GrasslandGenerator(World&, u64 seed); + void generate(u32 x, u32 y, Chunk& chunk) override; + + private: + + u64 const seed_; + Handle const terrain_[6]; + }; +} diff --git a/game/include/game/catalog.hh b/game/include/game/catalog.hh deleted file mode 100644 index 076bdc1..0000000 --- a/game/include/game/catalog.hh +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once -#include "core/basic-catalog.hh" -#include "game/terrain.hh" -#include "game/texture.hh" -#include "game/types.hh" - -using Definitions = TypeList; - -using Catalog = BasicCatalog; - -[[nodiscard]] CatalogReader access_catalog(ConstContext ctx); -[[nodiscard]] CatalogWriter access_catalog(LoadContext ctx); - -// A subset of the definitions catalog API that simulation systems can use. -template -struct CatalogView -{ - using Source = std::conditional_t; - - CatalogView(Source& src) noexcept : src_(src) {} - - CatalogView(CatalogView&&) noexcept = default; - CatalogView(CatalogView const&) noexcept = default; - ~CatalogView() noexcept = default; - - template - [[nodiscard]] bool valid(Token token) const - { - return src_.valid(token); - } - - template - [[nodiscard]] Token find(std::string_view label) const - { - return src_.template find(label); - } - - template - [[nodiscard]] std::string_view label(Token token) const - { - return src_.label(token); - } - - template - [[nodiscard]] auto& operator[](Token token) const - { - return src_[token]; - } - - template - [[nodiscard]] auto* try_get(std::string_view label) const - { - return src_.template try_get(label); - } - - template - requires (Access == Write) && std::constructible_from - Token emplace(std::string_view label, Args&&... args) const - { - return src_.template emplace(label, std::forward(args)...); - } - - template - [[nodiscard]] u32 count() const - { - return src_.template count(); - } - - template - requires (Access == Write) - void erase(Token id) const - { - src_.template erase(id); - } - - template - constexpr auto each() const - { - return src_.template each(); - } - -private: - - Source& src_; -}; diff --git a/game/include/game/chunk.hh b/game/include/game/chunk.hh index aca7840..9fc3b30 100644 --- a/game/include/game/chunk.hh +++ b/game/include/game/chunk.hh @@ -1,37 +1,38 @@ #pragma once #include "core/grid2.hh" #include "game/tile.hh" -#include "game/types.hh" -struct Camera; -struct Context; +namespace Game { + struct Camera; + struct World; -constexpr u32 chunk_size = 64; + constexpr u32 chunk_size = 64; -struct Chunk : Grid2 -{ - // other relevant data... - void render(ConstContext ctx, Camera const& camera, float tile_size) const; -}; + struct Chunk : Grid2 + { + // other relevant data... + void render(World&, Camera const& camera, float tile_size) const; + }; -struct ChunkGenerator -{ - virtual ~ChunkGenerator() = default; - virtual void generate(u32 x, u32 y, Chunk& chunk) = 0; -}; + struct ChunkGenerator + { + virtual ~ChunkGenerator() = default; + virtual void generate(u32 x, u32 y, Chunk& chunk) = 0; + }; -struct NullChunkGenerator final : ChunkGenerator -{ - void generate(u32, u32, Chunk&) override {}; -}; + struct NullChunkGenerator final : ChunkGenerator + { + void generate(u32, u32, Chunk&) override {}; + }; -struct ChunkLoader -{ - virtual ~ChunkLoader() = default; - virtual void load(u32 x, u32 y, Chunk& chunk) = 0; -}; + struct ChunkLoader + { + virtual ~ChunkLoader() = default; + virtual void load(u32 x, u32 y, Chunk& chunk) = 0; + }; -struct NullChunkLoader final : ChunkLoader -{ - void load(u32, u32, Chunk&) override {}; -}; + struct NullChunkLoader final : ChunkLoader + { + void load(u32, u32, Chunk&) override {}; + }; +} diff --git a/game/include/game/command-buffer.hh b/game/include/game/command-buffer.hh index 661fb3e..49bf551 100644 --- a/game/include/game/command-buffer.hh +++ b/game/include/game/command-buffer.hh @@ -6,118 +6,120 @@ #include #include -struct CommandRequest -{ - inline void put(Command const& cmd) +namespace Game { + struct CommandRequest { - kind_ = Kind::Command; - detail_.command = cmd; - } + void put(Command const& cmd) + { + kind_ = Kind::Command; + detail_.command = cmd; + } - inline void put(CommandReply const& reply) - { - kind_ = Kind::Reply; - detail_.reply = reply; - } + void put(CommandReply const& reply) + { + kind_ = Kind::Reply; + detail_.reply = reply; + } - [[nodiscard]] inline Command const& as_command() const - { - DEBUG_ASSERT(kind_ == Kind::Command); - return detail_.command; - } + [[nodiscard]] Command const& as_command() const + { + DEBUG_ASSERT(kind_ == Kind::Command); + return detail_.command; + } - [[nodiscard]] inline CommandReply const& as_reply() const - { - DEBUG_ASSERT(kind_ == Kind::Reply); - return detail_.reply; - } + [[nodiscard]] CommandReply const& as_reply() const + { + DEBUG_ASSERT(kind_ == Kind::Reply); + return detail_.reply; + } - [[nodiscard]] inline bool is_resolved() const { return kind_ == Kind::Reply; } + [[nodiscard]] bool is_resolved() const { return kind_ == Kind::Reply; } - [[nodiscard]] inline explicit operator bool() const { return is_resolved(); } + [[nodiscard]] explicit operator bool() const { return is_resolved(); } -private: - union Detail - { - Command command; - CommandReply reply; - }; + private: + union Detail + { + Command command; + CommandReply reply; + }; - enum class Kind : bool { Command, Reply }; + enum class Kind : bool { Command, Reply }; - Kind kind_ = Kind::Command; - Detail detail_ = { .command = {} }; -}; + Kind kind_ = Kind::Command; + Detail detail_ = { .command = {} }; + }; -struct CommandBuffer -{ - static constexpr u32 max_batch_count = 2; - static constexpr u32 max_command_count = 256; + struct CommandBuffer + { + static constexpr u32 max_batch_count = 2; + static constexpr u32 max_command_count = 256; - CommandBuffer(); + CommandBuffer(); - CommandBuffer(CommandBuffer&&) noexcept = default; - CommandBuffer& operator=(CommandBuffer&&) noexcept = default; - CommandBuffer& operator=(CommandBuffer const&) = delete; - ~CommandBuffer() noexcept = default; + CommandBuffer(CommandBuffer&&) noexcept = default; + CommandBuffer& operator=(CommandBuffer&&) noexcept = default; + CommandBuffer& operator=(CommandBuffer const&) = delete; + ~CommandBuffer() noexcept = default; - Handle post(Command cmd); + Handle post(Command cmd); - [[nodiscard]] std::optional poll(Handle handle) const; + [[nodiscard]] std::optional poll(Handle handle) const; - void dispatch(Context ctx); + void dispatch(World&); - [[nodiscard]] u32 count() const { return get_batch(generation_).size(); } + [[nodiscard]] u32 count() const { return get_batch(generation_).size(); } - [[nodiscard]] u32 generation() const { return generation_; } + [[nodiscard]] u32 generation() const { return generation_; } - [[nodiscard]] std::span view() const - { - return get_batch(generation_).view(); - } + [[nodiscard]] std::span view() const + { + return get_batch(generation_).view(); + } - [[nodiscard]] CommandBuffer copy() const { return *this; } + [[nodiscard]] CommandBuffer copy() const { return *this; } -private: - CommandBuffer(CommandBuffer const&) = default; + private: + CommandBuffer(CommandBuffer const&) = default; - // placeholder name, find accurate name - struct Batch - { - [[nodiscard]] CommandRequest const& operator[](u32 idx) const + // placeholder name, find accurate name + struct Batch { - DEBUG_ASSERT(idx < size_); - return requests_[idx]; - } + [[nodiscard]] CommandRequest const& operator[](u32 idx) const + { + DEBUG_ASSERT(idx < size_); + return requests_[idx]; + } - // Assumes: the batch is not full - u32 append(Command const& cmd) - { - DEBUG_ASSERT(size_ < max_command_count); - requests_[size_].put(cmd); - return size_++; - } + // Assumes: the batch is not full + u32 append(Command const& cmd) + { + DEBUG_ASSERT(size_ < max_command_count); + requests_[size_].put(cmd); + return size_++; + } - [[nodiscard]] std::span view() { return { requests_.data(), size_ }; } + [[nodiscard]] std::span view() { return { requests_.data(), size_ }; } - [[nodiscard]] std::span view() const - { - return { requests_.data(), size_ }; - } + [[nodiscard]] std::span view() const + { + return { requests_.data(), size_ }; + } - [[nodiscard]] u32 size() const { return size_; } + [[nodiscard]] u32 size() const { return size_; } - void reset() { size_ = 0u; } + void reset() { size_ = 0u; } - private: - std::array requests_; - u32 size_ = 0u; - }; + private: + std::array requests_; + u32 size_ = 0u; + }; - [[nodiscard]] Batch& get_batch(u32 gen) { return batches_[gen % max_batch_count]; } + [[nodiscard]] Batch& get_batch(u32 gen) { return batches_[gen % max_batch_count]; } - [[nodiscard]] Batch const& get_batch(u32 gen) const { return batches_[gen % max_batch_count]; } + [[nodiscard]] Batch const& get_batch(u32 gen) const { return batches_[gen % max_batch_count]; } - std::array batches_; - u32 generation_; -}; + std::array batches_; + u32 generation_; + }; +} diff --git a/game/include/game/command.hh b/game/include/game/command.hh index ff2466f..a4a26bb 100644 --- a/game/include/game/command.hh +++ b/game/include/game/command.hh @@ -1,93 +1,96 @@ #pragma once #include "core/panic.hh" -#include "game/types.hh" +#include "game/entity.hh" #include -enum class CommandError : u8 { - None, - BadCommand, - DeadEntity, - MissingComponent, - Obstructed, -}; - -enum class CommandStatus : u8 { - Pending, - Accepted, - Rejected, -}; - -// Disposition of a posted command: was it accepted into the simulation? -// This is *not* the action's outcome. -struct CommandReply -{ - CommandStatus status; - CommandError error = CommandError::None; -}; - -struct Command -{ - enum class Kind : u8 { - Nil, - Custom, // modder-defined command, interpret body (speculative) - Move, +namespace Game { + struct World; + + enum class CommandError : u8 { + None, + BadCommand, + DeadEntity, + MissingComponent, + Obstructed, + }; + + enum class CommandStatus : u8 { + Pending, + Accepted, + Rejected, }; - union Detail + // Disposition of a posted command: was it accepted into the simulation? + // This is *not* the action's outcome. + struct CommandReply { - struct Move - { - f32 x, y; - } move; + CommandStatus status; + CommandError error = CommandError::None; }; - Command() noexcept : actor_{ Entity::Nil }, kind_{ Kind::Nil }, body_{} {} + struct Command + { + enum class Kind : u8 { + Nil, + Custom, // modder-defined command, interpret body (speculative) + Move, + }; - Command(Entity e, Detail::Move detail) : Command(e, Kind::Move, Detail{ detail }) {} + union Detail + { + struct Move + { + f32 x, y; + } move; + }; - Command(Command const&) noexcept = default; - Command(Command&&) noexcept = default; - Command& operator=(Command&&) noexcept = default; - Command& operator=(Command const&) noexcept = default; - ~Command() noexcept = default; + Command() noexcept : actor_{ Entity::Nil }, kind_{ Kind::Nil }, body_{} {} - [[nodiscard]] inline Detail::Move const& as_move() const - { - DEBUG_ASSERT(kind_ == Kind::Move); - return detail_.move; - } + Command(Entity e, Detail::Move detail) : Command(e, Kind::Move, Detail{ detail }) {} - [[nodiscard]] inline Entity actor() const { return actor_; } + Command(Command const&) noexcept = default; + Command(Command&&) noexcept = default; + Command& operator=(Command&&) noexcept = default; + Command& operator=(Command const&) noexcept = default; + ~Command() noexcept = default; - [[nodiscard]] inline Kind kind() const { return kind_; } + [[nodiscard]] inline Detail::Move const& as_move() const + { + DEBUG_ASSERT(kind_ == Kind::Move); + return detail_.move; + } - [[nodiscard]] inline bool is_empty() const { return kind_ != Kind::Nil; } + [[nodiscard]] inline Entity actor() const { return actor_; } - [[nodiscard]] inline explicit operator bool() const { return !is_empty(); } + [[nodiscard]] inline Kind kind() const { return kind_; } -private: - Command(Entity e, Kind kind, Detail detail) : actor_{ e }, kind_{ kind }, detail_{ detail } - { - DEBUG_ASSERT(actor_ != Entity::Nil); - DEBUG_ASSERT(kind_ != Kind::Nil); - } + [[nodiscard]] inline bool is_empty() const { return kind_ != Kind::Nil; } - Entity actor_ = Entity::Nil; - Kind kind_ = Kind::Nil; + [[nodiscard]] inline explicit operator bool() const { return !is_empty(); } - union - { - std::byte body_[sizeof(Detail)] = {}; // may hold anything or nothing - Detail detail_; - }; -}; + private: + Command(Entity e, Kind kind, Detail detail) : actor_{ e }, kind_{ kind }, detail_{ detail } + { + DEBUG_ASSERT(actor_ != Entity::Nil); + DEBUG_ASSERT(kind_ != Kind::Nil); + } -[[nodiscard]] inline Command make_command_move(Entity e, f32 x, f32 y) -{ - return Command(e, Command::Detail::Move{ x, y }); -} + Entity actor_ = Entity::Nil; + Kind kind_ = Kind::Nil; + + union + { + std::byte body_[sizeof(Detail)] = {}; // may hold anything or nothing + Detail detail_; + }; + }; -// Validate and translate a command into an action on its actor. -// Can be rejected if the entity doesn't have the components to execute the command. -CommandReply submit_command(Context ctx, Command const& cmd); + [[nodiscard]] inline Command make_command_move(Entity e, f32 x, f32 y) + { + return Command(e, Command::Detail::Move{ x, y }); + } + // Validate and translate a command into an action on its actor. + // Can be rejected if the entity doesn't have the components to execute the command. + CommandReply submit_command(World&, Command const&); +} diff --git a/game/include/game/component/pose.hh b/game/include/game/component/pose.hh index 8e4919a..dd645a2 100644 --- a/game/include/game/component/pose.hh +++ b/game/include/game/component/pose.hh @@ -4,7 +4,9 @@ #include -struct Pose -{ - glm::vec2 position; -}; +namespace Game { + struct Pose + { + glm::vec2 position; + }; +} diff --git a/game/include/game/content/terrain.hh b/game/include/game/content/terrain.hh new file mode 100644 index 0000000..bc62166 --- /dev/null +++ b/game/include/game/content/terrain.hh @@ -0,0 +1,19 @@ +#pragma once + +// #include "core/types.hh" +// #include + +namespace Game { + struct World; + + struct Terrain + {}; + + // [[nodiscard]] bool is_valid(World const&, Handle); + + // [[nodiscard]] Terrain const* try_get(World const&, Handle); + + // [[nodiscard]] Terrain const& get(World const&, Handle); + + // [[nodiscard]] Handle find_terrain(World const&, std::string_view); +} diff --git a/game/include/game/context.hh b/game/include/game/context.hh deleted file mode 100644 index db68d23..0000000 --- a/game/include/game/context.hh +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once - -// Contexts are an attempt to constrain the simulations API to only the methods the caller needs to -// fulfill their function. For example, simulation systems should be able to read but not write to -// the catalog (which is supposed to be immutable after load), and the UI should be able to read but -// not write to simulation state. - -#include "game/types.hh" - -struct Tile; -struct Simulation; - -[[nodiscard]] Entity who(ConstContext ctx, Handle handle); - -[[nodiscard]] Handle find_entity(ConstContext ctx, Entity id); - -[[nodiscard]] bool is_valid(ConstContext ctx, Handle handle); - -struct ConstContext -{ - ConstContext(ConstContext const&) noexcept = default; - ConstContext(ConstContext&&) noexcept = default; - ~ConstContext() noexcept = default; - -private: - - friend struct Simulation; - friend struct Context; - friend struct LoadContext; - friend RegistryReader access_registry(ConstContext); - friend CatalogReader access_catalog(ConstContext); - friend EntityReader access_entity(ConstContext, Handle); - friend Handle find_entity(ConstContext, Entity); - friend Entity who(ConstContext, Handle); - friend bool is_valid(ConstContext, Handle); - friend Tile const* tile_at(ConstContext ctx, u32 x, u32 y); - - ConstContext(Simulation const& src) noexcept : src_{ src } {} - - Simulation const* operator->() const { return &src_; } - - Simulation const& operator*() const { return src_; } - - Simulation const& src_; -}; - -struct Context -{ - Context(Context const&) noexcept = default; - Context(Context&&) noexcept = default; - ~Context() noexcept = default; - - operator ConstContext() const noexcept { return ConstContext(src_); } - -private: - - friend struct Simulation; - friend struct LoadContext; - friend RegistryWriter access_registry(Context); - friend EntityWriter access_entity(Context, Handle); - friend EntityWriter create_entity(Context ctx); - - Context(Simulation& src) noexcept : src_{ src } {} - - Simulation* operator->() const { return &src_; } - - Simulation& operator*() const { return src_; } - - Simulation& src_; -}; - -struct LoadContext -{ - LoadContext(LoadContext const&) noexcept = default; - LoadContext(LoadContext&&) noexcept = default; - ~LoadContext() noexcept = default; - - operator ConstContext() const noexcept { return ConstContext(src_); } - - operator Context() const noexcept { return Context(src_); } - -private: - - friend struct Simulation; - friend CatalogWriter access_catalog(LoadContext); - - LoadContext(Simulation& src) noexcept : src_{ src } {} - - Simulation* operator->() const { return &src_; } - - Simulation& operator*() const { return src_; } - - Simulation& src_; -}; - diff --git a/game/include/game/entity.hh b/game/include/game/entity.hh index 4301812..e8bbf6c 100644 --- a/game/include/game/entity.hh +++ b/game/include/game/entity.hh @@ -1,100 +1,19 @@ #pragma once +#include "core/types.hh" -// TODO: `EntityBuilder` that does not register its ID as active until all its components are -// constructed and the user calls `builder.commit()`. If the builder goes out of scope, the -// ID is released but not retired (may be used again). If multi-threading is ever implemented, this -// would mean the entity could not be polled by other systems mid-construction. +namespace Game { + struct World; -// TODO: Lua defined entity prefabs. + // Goal: stable, persistent, cross-session, unique identifier for a simulation object. + enum class Entity : u64 { Nil = UINT64_MAX }; -// TODO: Entity destruction and a way to retire an entity ID. Is abscence of ID in persistent -// storage enough or are tombstones useful? + // [[nodiscard]] bool is_valid(World&, Handle); -// TODO: Persistence and lazy loading; Entities can be loaded in session memory (the -// registry) or stored on disk, just because an entity does not exist in the registry doesn't mean -// it is dead/retired, it may not be loaded. Use SQLite? LevelDB? + // [[nodiscard]] Entity get(World&, Handle); -#include "game/context.hh" -#include "game/registry.hh" + // [[nodiscard]] Entity try_get(World&, Handle); -[[nodiscard]] EntityWriter create_entity(Context ctx); - -[[nodiscard]] EntityReader access_entity(ConstContext ctx, Handle e); -[[nodiscard]] EntityWriter access_entity(Context ctx, Handle e); - -template -struct EntityView -{ - // Not sure if this class is necessary. The same function can be achieved with a handle and a - // registry view. - - using Source = std::conditional_t; - - EntityView(Source& src, Handle e) noexcept : src_(src), handle_(e) - { - INVARIANT(handle_); - INVARIANT(src_.valid(handle_)); - } - - EntityView(EntityView&&) noexcept = default; - EntityView(EntityView const&) noexcept = default; - ~EntityView() noexcept = default; - - template - [[nodiscard]] bool has() const - { - return src_.template has(handle_); - } - - template - [[nodiscard]] bool all() const - { - return src_.template all(handle_); - } - - template - [[nodiscard]] bool any() const - { - return src_.template any(handle_); - } - - template - [[nodiscard]] auto& get() const - { - return src_.template get(handle_); - } - - template - [[nodiscard]] auto* try_get() const - { - return src_.template try_get(handle_); - } - - template - requires (Access == Write) && std::constructible_from - T& emplace(Args&&... args) const - { - return src_.template emplace(handle_, std::forward(args)...); - } - - template - requires (Access == Write) - void erase() const - { - src_.template erase(handle_); - } - - [[nodiscard]] Handle handle() const { return handle_; } - - [[nodiscard]] operator Handle() const { return handle(); } - - [[nodiscard]] Entity id() const { return src_[handle_]; } - - [[nodiscard]] operator Entity() const { return id(); } - -private: - - Source& src_; - Handle handle_; // should never be null, assumed to be alive -}; + // [[nodiscard]] Handle find_entity(World&, Entity); + // [[nodiscard]] Handle spawn(World&); +} diff --git a/game/include/game/pilot.hh b/game/include/game/pilot.hh deleted file mode 100644 index db2730c..0000000 --- a/game/include/game/pilot.hh +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include "game/types.hh" - -union SDL_Event; -struct CommandBuffer; - -// Generates commands to pass to Actors -struct Pilot -{ - virtual void handle_event(SDL_Event const& event) = 0; - virtual void steer(CommandBuffer& out, ConstContext ctx, Entity e) = 0; - virtual ~Pilot() = default; -}; - diff --git a/game/include/game/registry.hh b/game/include/game/registry.hh deleted file mode 100644 index 0a10f5d..0000000 --- a/game/include/game/registry.hh +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once -#include "core/basic-registry.hh" -#include "game/component/pose.hh" -#include "game/types.hh" - -using Components = TypeList; - -using Registry = BasicRegistry; - -[[nodiscard]] RegistryReader access_registry(ConstContext ctx); -[[nodiscard]] RegistryWriter access_registry(Context ctx); - -// A subset of the component registry API that simulation systems can use. -template -struct RegistryView -{ - using Source = std::conditional_t; - - RegistryView(Source& src) noexcept : src_(src) {} - - RegistryView(RegistryView&&) noexcept = default; - RegistryView(RegistryView const&) noexcept = default; - ~RegistryView() noexcept = default; - - [[nodiscard]] bool valid(Handle handle) const { return src_.valid(handle); } - - [[nodiscard]] Entity id(Handle handle) const { return src_[handle]; } - - template - [[nodiscard]] bool has(Handle handle) const - { - return src_.template has(handle); - } - - template - [[nodiscard]] bool all(Handle handle) const - { - return src_.template all(handle); - } - - template - [[nodiscard]] bool any(Handle handle) const - { - return src_.template any(handle); - } - - template - [[nodiscard]] auto& get(Handle handle) const - { - return src_.template get(handle); - } - - template - [[nodiscard]] auto* try_get(Handle handle) const - { - return src_.template try_get(handle); - } - - template - requires (Access == Write) && std::constructible_from - T& emplace(Handle handle, Args&&... args) const - { - return src_.template emplace(handle, std::forward(args)...); - } - - template - requires (Access == Write) - void erase(Handle handle) const - { - src_.template erase(handle); - } - - template - requires (Access == Write) - bool try_erase(Handle handle) const - { - return src_.template try_erase(handle); - } - - template - [[nodiscard]] constexpr auto each() const - { - return src_.template each(); - } - -private: - - Source& src_; -}; diff --git a/game/include/game/scene.hh b/game/include/game/scene.hh deleted file mode 100644 index c740b91..0000000 --- a/game/include/game/scene.hh +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include "game/types.hh" - -struct Tile; - -// Placeholder function just to get things working -[[nodiscard]] Tile const* tile_at(ConstContext ctx, u32 x, u32 y); diff --git a/game/include/game/simulation.hh b/game/include/game/simulation.hh deleted file mode 100644 index 536c486..0000000 --- a/game/include/game/simulation.hh +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "core/panic.hh" -#include "game/catalog.hh" -#include "game/chunk.hh" -#include "game/context.hh" -#include "game/registry.hh" -#include "game/types.hh" -#include - -struct Simulation -{ - Simulation() = default; - Simulation(Simulation&&) noexcept = default; - Simulation& operator=(Simulation&&) noexcept = default; - Simulation& operator=(Simulation const&) = delete; - - inline Catalog& catalog() { return codex_; } - - inline Catalog const& catalog() const { return codex_; } - - inline Registry& registry() { return registry_; } - - inline Registry const& registry() const { return registry_; } - - inline Chunk& scene() { return scene_; } - - inline Chunk const& scene() const { return scene_; } - - [[nodiscard]] Handle find(Entity e) const - { - auto it = lookup_.find(e); - return it != lookup_.end() ? it->second : Handle::null(); - } - - Handle create() - { - auto id = Entity{ entity_count_++ }; - auto handle = registry_.create(id); - auto [_, success] = lookup_.emplace(id, handle); - INVARIANT(success); - return handle; - } - - Context step(i64) { return *this; } - - Result load() { return LoadContext(*this); } - - void quit() {} - - [[nodiscard]] ConstContext context() const { return *this; } - - // Explciit copy to prevent unintended and expensive implicit copies. - inline Simulation clone() const { return *this; } - -private: - - Simulation(Simulation const&) : codex_(), registry_(), scene_() {} - - Catalog codex_; - Registry registry_; // entity data - Chunk scene_; - - // Hash map used because entity ID is supposed to be sparse not dense. - std::unordered_map> lookup_; - - // Total number of entities including those not loaded (stored on disk). - u64 entity_count_; -}; diff --git a/game/include/game/sprite.hh b/game/include/game/sprite.hh deleted file mode 100644 index ee85d08..0000000 --- a/game/include/game/sprite.hh +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "game/types.hh" -#include "gfx/color.hh" -#include "gfx/rectangle.hh" - -struct TextureDef; -struct LuaNode; - -struct Sprite -{ - Token atlas; - Rectangle source; - Color tint; -}; - -[[nodiscard]] Result parse_sprite(LuaNode const& node, CatalogWriter catalog); diff --git a/game/include/game/terrain.hh b/game/include/game/terrain.hh deleted file mode 100644 index 91b814c..0000000 --- a/game/include/game/terrain.hh +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "game/sprite.hh" -#include "game/types.hh" - -struct LuaNode; - -struct Terrain -{ - Sprite sprite; -}; - -[[nodiscard]] Result parse_terrain(LuaNode const& node, CatalogWriter catalog); diff --git a/game/include/game/texture.hh b/game/include/game/texture.hh deleted file mode 100644 index 3488d57..0000000 --- a/game/include/game/texture.hh +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include - -struct TextureDef -{ - std::string path; -}; - diff --git a/game/include/game/tile.hh b/game/include/game/tile.hh index 12ae302..530b973 100644 --- a/game/include/game/tile.hh +++ b/game/include/game/tile.hh @@ -1,11 +1,12 @@ #pragma once +#include "content/terrain.hh" #include "core/types.hh" -struct Terrain; - -struct Tile -{ - Token terrain; - // u32 elevation; - // u32 structure; -}; +namespace Game { + struct Tile + { + Handle terrain; + // u32 elevation; + // u32 structure; + }; +} diff --git a/game/include/game/types.hh b/game/include/game/types.hh deleted file mode 100644 index 9727031..0000000 --- a/game/include/game/types.hh +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include "core/types.hh" -#include - -// Stable, persistent, cross-session, unique identifier for a simulation object. -enum class Entity : u64 { Nil = UINT64_MAX }; - -struct Context; -struct ConstContext; -struct LoadContext; - -template -struct RegistryView; - -using RegistryReader = RegistryView; -using RegistryWriter = RegistryView; - -template -struct EntityView; - -using EntityReader = EntityView; -using EntityWriter = EntityView; - -template -struct CatalogView; - -using CatalogReader = CatalogView; -using CatalogWriter = CatalogView; - -// TODO: make this a user-interface implementation detail -constexpr f32 pixels_per_unit = 16.f; - diff --git a/game/include/game/world.hh b/game/include/game/world.hh index 35de653..846b7d2 100644 --- a/game/include/game/world.hh +++ b/game/include/game/world.hh @@ -1,66 +1,34 @@ #pragma once +#include "component/pose.hh" +#include "content/terrain.hh" +#include "core/basic-catalog.hh" +#include "core/basic-registry.hh" #include "game/chunk.hh" -#include "game/tile.hh" -#include +#include "game/entity.hh" #include -struct Context; -struct Camera; +namespace Game { + using Definitions = TypeList; + using Catalog = BasicCatalog; -struct World -{ - World( - u32 width, - u32 height, - std::unique_ptr generator, - std::unique_ptr loader); - World(World const& other) = delete; - World& operator=(World const& other) = delete; - World(World&& other) = default; - World& operator=(World&& other) = default; + using Components = TypeList; + using Registry = BasicRegistry; - constexpr u64 size() const { return u64{ chunk_size } * width_ * height_; } - - constexpr bool has(u32 x, u32 y) const { return x < width_ && y < height_; } - - void render(ConstContext ctx, Camera const& camera, float tile_size) const; - Tile& operator[](u32 x, u32 y); - Tile* get(u32 x, u32 y); - Tile const* find(u32 x, u32 y) const; - - constexpr void wrap_around(u32& x, u32& y) const + struct World { - x %= (chunk_size * width_); - y %= (chunk_size * height_); - } - - constexpr auto begin() const { return chunks_.begin(); } - - constexpr auto begin() { return chunks_.begin(); } + Catalog content; + Registry entities; + Chunk environment; - constexpr auto end() const { return chunks_.end(); } + [[nodiscard]] Handle find(Entity e) const; - constexpr auto end() { return chunks_.end(); } + [[nodiscard]] Handle create(); - constexpr u32 width() const { return width_; } + void advance(); - constexpr u32 height() const { return height_; } - -private: - - u32 width_, height_; - std::unique_ptr generator_; - std::unique_ptr loader_; - std::unordered_map chunks_; - - constexpr u64 key_at(u32 x, u32 y) const { return x / chunk_size + (y / chunk_size) * width_; } - - constexpr void coord_of(u64 key, u32& x, u32& y) const - { - x = key % width_ * chunk_size; - y = key / width_ * chunk_size; - } + private: - Chunk const* find_chunk(u64 key) const; - Chunk& get_chunk(u64 key); -}; + u64 _entity_counter; + std::unordered_map> _lookup; + }; +} diff --git a/game/src/CMakeLists.txt b/game/src/CMakeLists.txt index 2d5c64e..4a72444 100644 --- a/game/src/CMakeLists.txt +++ b/game/src/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory (content) add_subdirectory (biome) -target_sources (game PRIVATE load.cc command.cc command-buffer.cc catalog.cc registry.cc entity.cc context.cc scene.cc terrain.cc sprite.cc) +target_sources (game PRIVATE command.cc command-buffer.cc) diff --git a/game/src/biome/caves.cc b/game/src/biome/caves.cc index ecc87d8..cc78b77 100644 --- a/game/src/biome/caves.cc +++ b/game/src/biome/caves.cc @@ -1,73 +1,79 @@ -#include "game/biome/caves.hh" #include "core/grid2.hh" #include "core/random.hh" -#include "game/context.hh" +#include "game/biome/caves.hh" +#include "game/world.hh" #include #include #include -void generate_cave( - Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations) -{ - DEBUG_ASSERT(out.size() > 0u); - DEBUG_ASSERT(iterations > 0u); - DEBUG_ASSERT(range > 0u); - Grid2 buffer(out.width(), out.height()); - auto const count_walls = [range, wall, &out](i64 x, i64 y) { - u32 n = 0; - for (i64 ny = y - range; ny <= y + range; ++ny) { - for (i64 nx = x - range; nx <= x + range; ++nx) { - if (nx < 0 || ny < 0 || !out.has(nx, ny)) { - // out of bounds - ++n; - continue; +namespace Game { + void generate_cave( + Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations) + { + DEBUG_ASSERT(out.size() > 0u); + DEBUG_ASSERT(iterations > 0u); + DEBUG_ASSERT(range > 0u); + Grid2 buffer(out.width(), out.height()); + auto const count_walls = [range, wall, &out](i64 x, i64 y) { + u32 n = 0; + for (i64 ny = y - range; ny <= y + range; ++ny) { + for (i64 nx = x - range; nx <= x + range; ++nx) { + if (nx < 0 || ny < 0 || !out.has(nx, ny)) { + // out of bounds + ++n; + continue; + } + n += (out[nx, ny] == wall); } - n += (out[nx, ny] == wall); } - } - return n; - }; - while (iterations > 0u) { - for (u32 y = 0u; y < out.height(); ++y) { - for (u32 x = 0u; x < out.width(); ++x) { - u32 count = count_walls(x, y); - u32& tile = buffer[x, y]; - bool is_wall = (tile == wall); - if (is_wall) { - is_wall = (count >= survival); - } else { - is_wall = (count >= birth); + return n; + }; + while (iterations > 0u) { + for (u32 y = 0u; y < out.height(); ++y) { + for (u32 x = 0u; x < out.width(); ++x) { + u32 count = count_walls(x, y); + u32& tile = buffer[x, y]; + bool is_wall = (tile == wall); + if (is_wall) { + is_wall = (count >= survival); + } else { + is_wall = (count >= birth); + } + tile = is_wall ? wall : floor; } - tile = is_wall ? wall : floor; } + std::swap(out, buffer); + iterations--; } - std::swap(out, buffer); - iterations--; } -} -CaveGenerator::CaveGenerator(Context const ctx, u64 seed) - : seed_{ seed } - , wall_{ ctx.codex.terrain.find("stone") } - , floor_{ ctx.codex.terrain.find("dirt") } -{ - PRECONDITION(wall_ != nil); - PRECONDITION(floor_ != nil); -} + CaveGenerator::CaveGenerator(World& world, u64 seed) + : _seed{ seed } + , _wall{ world.content.find("stone") } + , _floor{ world.content.find("dirt") } + { + PRECONDITION(wall_ != nil); + PRECONDITION(floor_ != nil); + } -void CaveGenerator::generate(u32 x, u32 y, Chunk& chunk) -{ - auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); - Grid2 cave(chunk_size, chunk_size); - // uniformly random fill - Xoshiro256ss rng{ hash }; - std::bernoulli_distribution coin{ 0.45f }; - for (auto& tile : cave) tile = coin(rng) ? wall_ : floor_; - generate_cave(cave, wall_, floor_, 5, 6, 1, 1); - generate_cave(cave, wall_, floor_, 3, 4, 2, 2); - generate_cave(cave, wall_, floor_, 9, 10, 2, 1); - for (auto [tile, terrain] : std::views::zip(chunk, cave)) { tile.terrain = terrain; } + void CaveGenerator::generate(u32 x, u32 y, Chunk& chunk) + { + auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); + Grid2 cave(chunk_size, chunk_size); + // uniformly random fill + Xoshiro256ss rng{ hash }; + std::bernoulli_distribution coin{ 0.45f }; + for (auto& tile : cave) + tile = coin(rng) ? wall_ : floor_; + generate_cave(cave, wall_, floor_, 5, 6, 1, 1); + generate_cave(cave, wall_, floor_, 3, 4, 2, 2); + generate_cave(cave, wall_, floor_, 9, 10, 2, 1); + for (auto [tile, terrain] : std::views::zip(chunk, cave)) { + tile.terrain = terrain; + } + } } // TODO: perlin noise // procedural generation as a series of transformations + diff --git a/game/src/biome/grassland.cc b/game/src/biome/grassland.cc index 2f6f5c0..0c65893 100644 --- a/game/src/biome/grassland.cc +++ b/game/src/biome/grassland.cc @@ -16,7 +16,9 @@ GrasslandGenerator::GrasslandGenerator(Context const ctx, u64 seed) find_terrain(ctx, "grass-tall"), find_terrain(ctx, "dirt"), find_terrain(ctx, "rocks"), } { - for (auto token : terrain_) { PRECONDITION(token, "terrain undefined"); } + for (auto token : terrain_) { + PRECONDITION(token, "terrain undefined"); + } } void GrasslandGenerator::generate(u32 x, u32 y, Chunk& chunk) @@ -25,5 +27,7 @@ void GrasslandGenerator::generate(u32 x, u32 y, Chunk& chunk) Xoshiro256ss rng{ hash }; std::uniform_int_distribution distribution{ 0u, (u32)std::size(terrain_) - 1u }; - for (auto& tile : chunk) { tile.terrain = terrain_[distribution(rng)]; } + for (auto& tile : chunk) { + tile.terrain = terrain_[distribution(rng)]; + } } diff --git a/game/src/catalog.cc b/game/src/catalog.cc deleted file mode 100644 index 91bfab6..0000000 --- a/game/src/catalog.cc +++ /dev/null @@ -1,12 +0,0 @@ -#include "game/catalog.hh" -#include "game/context.hh" -#include "game/simulation.hh" -#include "game/types.hh" - -static_assert(std::is_trivially_copy_constructible_v); -static_assert(std::is_trivially_copy_constructible_v); - -CatalogReader access_catalog(ConstContext ctx) { return ctx->catalog(); } - -CatalogWriter access_catalog(LoadContext ctx) { return ctx->catalog(); } - diff --git a/game/src/command-buffer.cc b/game/src/command-buffer.cc index ef1ca87..6523409 100644 --- a/game/src/command-buffer.cc +++ b/game/src/command-buffer.cc @@ -1,35 +1,36 @@ #include "game/command-buffer.hh" -#include "game/context.hh" -static_assert(std::is_trivially_copyable_v); -static_assert(std::is_trivially_copyable_v); +namespace Game { + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); -CommandBuffer::CommandBuffer() : batches_(), generation_{ 0u } {} + CommandBuffer::CommandBuffer() : batches_(), generation_{ 0u } {} -Handle CommandBuffer::post(Command cmd) -{ - Batch& batch = CommandBuffer::get_batch(generation_); - if (batch.size() >= max_command_count) return Handle::null(); - return { batch.append(cmd), generation_ }; -} + Handle CommandBuffer::post(Command cmd) + { + Batch& batch = CommandBuffer::get_batch(generation_); + if (batch.size() >= max_command_count) return Handle::null(); + return { batch.append(cmd), generation_ }; + } -std::optional CommandBuffer::poll(Handle handle) const -{ - PRECONDITION(handle, "polled a null handle"); - PRECONDITION(handle.generation <= generation_, "handle from a future generation"); - if (generation_ - handle.generation >= max_batch_count) return std::nullopt; - Batch const& batch = get_batch(handle.generation); - PRECONDITION(handle.index < batch.size(), "handle was never issued in its generation"); - CommandRequest const& request = batch[handle.index]; - if (request.is_resolved()) return request.as_reply(); - return CommandReply{ CommandStatus::Pending }; -} + std::optional CommandBuffer::poll(Handle handle) const + { + PRECONDITION(handle, "polled a null handle"); + PRECONDITION(handle.generation <= generation_, "handle from a future generation"); + if (generation_ - handle.generation >= max_batch_count) return std::nullopt; + Batch const& batch = get_batch(handle.generation); + PRECONDITION(handle.index < batch.size(), "handle was never issued in its generation"); + CommandRequest const& request = batch[handle.index]; + if (request.is_resolved()) return request.as_reply(); + return CommandReply{ CommandStatus::Pending }; + } -void CommandBuffer::dispatch(Context ctx) -{ - for (CommandRequest& request : get_batch(generation_).view()) { - DEBUG_ASSERT(!request.is_resolved(), "drained a batch twice"); - request.put(submit_command(ctx, request.as_command())); + void CommandBuffer::dispatch(World& world) + { + for (CommandRequest& request : get_batch(generation_).view()) { + DEBUG_ASSERT(!request.is_resolved(), "drained a batch twice"); + request.put(submit_command(world, request.as_command())); + } + get_batch(++generation_).reset(); } - get_batch(++generation_).reset(); } diff --git a/game/src/command.cc b/game/src/command.cc index 147fce6..a533493 100644 --- a/game/src/command.cc +++ b/game/src/command.cc @@ -1,30 +1,29 @@ #include "game/command.hh" -#include "game/context.hh" -#include "game/registry.hh" +#include "game/world.hh" #include #include -static_assert(std::is_trivially_copy_constructible_v); -static_assert(std::is_trivially_copy_assignable_v); +namespace Game { + static_assert(std::is_trivially_copy_constructible_v); + static_assert(std::is_trivially_copy_assignable_v); -static CommandReply resolve(Context ctx, Entity actor, Command::Detail::Move const& move) -{ - auto handle = find_entity(ctx, actor); - if (!handle) return { CommandStatus::Rejected, CommandError::DeadEntity }; - auto pose = access_registry(ctx).try_get(handle); - if (!pose) return { CommandStatus::Rejected, CommandError::MissingComponent }; - pose->position += glm::normalize(glm::vec2(move.x, move.y)); - return { CommandStatus::Accepted }; -} + static CommandReply resolve(World& world, Entity actor, Command::Detail::Move const& move) + { + auto handle = world.find_entity(actor); + if (!handle) return { CommandStatus::Rejected, CommandError::DeadEntity }; + auto pose = world.entities.try_get(handle); + if (!pose) return { CommandStatus::Rejected, CommandError::MissingComponent }; + pose->position += glm::normalize(glm::vec2(move.x, move.y)); + return { CommandStatus::Accepted }; + } -CommandReply submit_command(Context ctx, Command const& cmd) -{ - switch (cmd.kind()) { - case Command::Kind::Nil: - case Command::Kind::Custom: - return { CommandStatus::Rejected, CommandError::BadCommand }; - case Command::Kind::Move: - return resolve(ctx, cmd.actor(), cmd.as_move()); + CommandReply submit_command(World& world, Command const& cmd) + { + switch (cmd.kind()) { + case Command::Kind::Nil: + case Command::Kind::Custom: return { CommandStatus::Rejected, CommandError::BadCommand }; + case Command::Kind::Move: return resolve(world, cmd.actor(), cmd.as_move()); + } + std::unreachable(); } - std::unreachable(); } diff --git a/game/src/content/CMakeLists.txt b/game/src/content/CMakeLists.txt new file mode 100644 index 0000000..4dac06c --- /dev/null +++ b/game/src/content/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(game PRIVATE) diff --git a/game/src/context.cc b/game/src/context.cc deleted file mode 100644 index 34c1f95..0000000 --- a/game/src/context.cc +++ /dev/null @@ -1,18 +0,0 @@ -#include "game/context.hh" -#include "game/simulation.hh" -#include - -static_assert(std::is_trivially_copy_constructible_v); -static_assert(std::is_trivially_copy_constructible_v); -static_assert(std::is_trivially_copy_constructible_v); - -Entity who(ConstContext ctx, Handle handle) -{ - auto& reg = ctx->registry(); - return reg.valid(handle) ? reg[handle] : Entity::Nil; -} - -Handle find_entity(ConstContext ctx, Entity id) { return ctx->find(id); } - -bool is_valid(ConstContext ctx, Handle handle) { return ctx->registry().valid(handle); } - diff --git a/game/src/entity.cc b/game/src/entity.cc deleted file mode 100644 index 470c4ca..0000000 --- a/game/src/entity.cc +++ /dev/null @@ -1,28 +0,0 @@ -#include "game/entity.hh" -#include "core/panic.hh" -#include "game/context.hh" -#include "game/simulation.hh" -#include "game/types.hh" - -static_assert(std::is_trivially_copy_constructible_v); -static_assert(std::is_trivially_copy_constructible_v); - -EntityWriter create_entity(Context ctx) -{ - auto handle = ctx->create(); - INVARIANT(who(ctx, handle) != Entity::Nil); - return EntityWriter(ctx->registry(), handle); -} - -EntityReader access_entity(ConstContext ctx, Handle e) -{ - PRECONDITION(ctx->registry().valid(e)); - return EntityReader(ctx->registry(), e); -} - -EntityWriter access_entity(Context ctx, Handle e) -{ - PRECONDITION(ctx->registry().valid(e)); - return EntityWriter(ctx->registry(), e); -} - diff --git a/game/src/load.cc b/game/src/load.cc deleted file mode 100644 index 32ed295..0000000 --- a/game/src/load.cc +++ /dev/null @@ -1,69 +0,0 @@ -#include "game/load.hh" -#include "core/result.hh" -#include "game/catalog.hh" -#include "game/context.hh" -#include "game/terrain.hh" -#include "sdk/node.hh" -#include "sdk/state.hh" -#include -#include -#include -#include -#include - -namespace { - -Status compile_terrain(LuaNode const& table, CatalogWriter catalog) -{ - Result> keys = table.keys(); - if (!keys) return Error(std::move(keys).error()); - - for (LuaKey const& key : *keys) { - std::string const* label = std::get_if(&key); - if (label == nullptr) { - return Error(describe(table) + ": terrain definition labels must be strings"); - } - - Result definition = table.find(key); - if (!definition) return Error(std::move(definition).error()); - - Result terrain = parse_terrain(*definition, catalog); - if (!terrain) return Error(std::move(terrain).error()); - catalog.emplace(*label, std::move(*terrain)); - } - return {}; -} - -Status compile_content(LuaState const& state, LoadContext context) -{ - Result content = state.global("content"); - if (!content) return Error(std::move(content).error()); - - Result terrain = content->find("terrain"); - if (!terrain) return Error(std::move(terrain).error()); - return compile_terrain(*terrain, access_catalog(context)); -} - -} // namespace - -bool load_content(LoadContext context, std::string_view path) -{ - Result state = LuaState::create(); - if (!state) { - SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not initialize Lua: %s", state.error().c_str()); - return false; - } - - Status result = state->create_table({ "content", "terrain" }); - if (result) result = state->do_file(path); - // TODO: Normalize metatable-backed content after all content packages have executed. - if (result) result = compile_content(*state, context); - if (!result) { - std::string const terminated_path(path); - SDL_LogError( - SDL_LOG_CATEGORY_ERROR, "Error in %s:\n\t%s", terminated_path.c_str(), - result.error().c_str()); - return false; - } - return true; -} diff --git a/game/src/registry.cc b/game/src/registry.cc deleted file mode 100644 index 96e1541..0000000 --- a/game/src/registry.cc +++ /dev/null @@ -1,11 +0,0 @@ -#include "game/registry.hh" -#include "game/context.hh" -#include "game/simulation.hh" -#include "game/types.hh" - -static_assert(std::is_trivially_copy_constructible_v); -static_assert(std::is_trivially_copy_constructible_v); - -RegistryReader access_registry(ConstContext ctx) { return ctx->registry(); } - -RegistryWriter access_registry(Context ctx) { return ctx->registry(); } diff --git a/game/src/scene.cc b/game/src/scene.cc deleted file mode 100644 index 80455fc..0000000 --- a/game/src/scene.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "game/scene.hh" -#include "game/chunk.hh" -#include "game/context.hh" -#include "game/simulation.hh" - -Tile const* tile_at(ConstContext ctx, u32 x, u32 y) { return ctx->scene().get(x, y); } diff --git a/game/src/sprite.cc b/game/src/sprite.cc deleted file mode 100644 index 8224c9f..0000000 --- a/game/src/sprite.cc +++ /dev/null @@ -1,66 +0,0 @@ -#include "game/sprite.hh" -#include "game/catalog.hh" -#include "game/texture.hh" -#include "sdk/table.hh" -#include -#include - -namespace { - -struct RectangleData -{ - f32 x; - f32 y; - f32 width; - f32 height; -}; - -struct SpriteData -{ - std::string atlas; - RectangleData source; - u32 color; -}; - -} // namespace - -template <> -struct LuaTraits -{ - static constexpr LuaField schema[] = { - lua_field<&RectangleData::x>(1), - lua_field<&RectangleData::y>(2), - lua_field<&RectangleData::width>(3), - lua_field<&RectangleData::height>(4), - }; -}; - -template <> -struct LuaTraits -{ - static constexpr LuaField schema[] = { - lua_field<&SpriteData::atlas>("atlas"), - lua_field<&SpriteData::source>("source"), - lua_field<&SpriteData::color>("color"), - }; -}; - -Result parse_sprite(LuaNode const& node, CatalogWriter catalog) -{ - Result data = translate(node); - if (!data) return Error(std::move(data).error()); - - Token texture = catalog.find(data->atlas); - if (!texture) { - texture = catalog.emplace(data->atlas, TextureDef{ data->atlas }); - } - - return Sprite{ - .atlas = texture, - .source = Rectangle{ - .origin = { data->source.x, data->source.y }, - .extent = { data->source.width, data->source.height }, - }, - .tint = make_color_hex(data->color), - }; -} diff --git a/game/src/terrain.cc b/game/src/terrain.cc deleted file mode 100644 index 79444a2..0000000 --- a/game/src/terrain.cc +++ /dev/null @@ -1,14 +0,0 @@ -#include "game/terrain.hh" -#include "game/catalog.hh" -#include "sdk/node.hh" -#include - -Result parse_terrain(LuaNode const& node, CatalogWriter catalog) -{ - Result sprite_node = node.find("sprite"); - if (!sprite_node) return Error(std::move(sprite_node).error()); - - Result sprite = parse_sprite(*sprite_node, catalog); - if (!sprite) return Error(std::move(sprite).error()); - return Terrain{ .sprite = std::move(*sprite) }; -} diff --git a/game/src/world.cc b/game/src/world.cc index 598f54f..a55cb12 100644 --- a/game/src/world.cc +++ b/game/src/world.cc @@ -1,63 +1,20 @@ -/* #include "core/panic.hh" -#include "engine/render/camera.hh" -#include "game/catalog.hh" -#include "game/context.hh" -#include "game/terrain.hh" #include "game/world.hh" -World::World( - u32 width, - u32 height, - std::unique_ptr generator, - std::unique_ptr loader) - : width_{ width } - , height_{ height } - , generator_(generator.release()) - , loader_(loader.release()) - , chunks_() +Handle World::create() { - DEBUG_ASSERT(width_ != 0u && height_ != 0u); - PRECONDITION(generator_); - PRECONDITION(loader_); + auto const id = Entity{ _entity_counter++ }; + auto const handle = _entities.create(id); + auto [_, success] = _lookup.emplace(id, handle); + INVARIANT(success); + return handle; } -Tile& World::operator[](u32 x, u32 y) +Handle World::find_entity(Entity e) const { - DEBUG_ASSERT(has(x, y)); - return get_chunk(key_at(x, y))[x % chunk_size, y % chunk_size]; + auto it = _lookup.find(e); + return it != _lookup.end() ? it->second : Handle::null(); } -Tile* World::get(u32 x, u32 y) -{ - if (!has(x, y)) return nullptr; - return &get_chunk(key_at(x, y))[x % chunk_size, y % chunk_size]; -} - -Tile const* World::find(u32 x, u32 y) const -{ - if (!has(x, y)) return nullptr; - auto chunk = find_chunk(key_at(x, y)); - return chunk ? chunk->get(x % chunk_size, y % chunk_size) : nullptr; -} +void World::advance() {} -Chunk const* World::find_chunk(u64 key) const -{ - if (auto it = chunks_.find(key); it != chunks_.end()) return &it->second; - return nullptr; -} - -Chunk& World::get_chunk(u64 key) -{ - auto [it, is_new] = chunks_.try_emplace(key); - auto& chunk = it->second; - if (is_new) { - u32 x, y; - coord_of(key, x, y); - generator_->generate(x, y, chunk); - loader_->load(x, y, chunk); - return chunk; - } - return chunk; -} -*/ diff --git a/gfx/CMakeLists.txt b/gfx/CMakeLists.txt index 370d531..b2bfeb1 100644 --- a/gfx/CMakeLists.txt +++ b/gfx/CMakeLists.txt @@ -1,15 +1,14 @@ -add_library (gfx) +add_library(gfx) -target_include_directories ( +target_include_directories( gfx - PRIVATE ${SDL3_INCLUDE_DIRS} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) -target_link_libraries ( +target_link_libraries( gfx - PRIVATE SDL3::SDL3 - PUBLIC glm::glm core sys + PUBLIC sys glm::glm ) -add_subdirectory (src) +add_subdirectory(src) diff --git a/gfx/include/gfx/rectangle.hh b/gfx/include/gfx/rectangle.hh index 8bb1b23..a1e7d41 100644 --- a/gfx/include/gfx/rectangle.hh +++ b/gfx/include/gfx/rectangle.hh @@ -34,4 +34,3 @@ struct Rectangle return Rectangle(lo, hi - lo); } }; - diff --git a/gfx/src/surface.cc b/gfx/src/surface.cc index 8f4b17e..fa958e7 100644 --- a/gfx/src/surface.cc +++ b/gfx/src/surface.cc @@ -27,9 +27,10 @@ Result load_image(std::string_view path) if (path.ends_with(".png")) surface = SDL_LoadPNG(std::string(path).c_str()); else if (path.ends_with(".bmp")) surface = SDL_LoadBMP(std::string(path).c_str()); else { - return Error( - std::format("Unsupported file type (expected BMP or PNG): {}", path)); + return Error(std::format("Unsupported file type (expected BMP or PNG): {}", path)); + } + if (!surface) { + return Error(SDL_GetError()); } - if (!surface) { return Error(SDL_GetError()); } return Surface(surface); } diff --git a/play.cc b/play.cc index e62d556..9e3383f 100644 --- a/play.cc +++ b/play.cc @@ -74,7 +74,8 @@ void render(Runtime& state, f32 alpha) void iterate(Runtime& state) { auto tick = state.clock.tick(); - for (auto steps = state.clock.advance(); steps > 0; steps--) step(state, tick++); + for (auto steps = state.clock.advance(); steps > 0; steps--) + step(state, tick++); DEBUG_ASSERT(tick == state.clock.tick()); update(state, state.clock.delta()); diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt deleted file mode 100644 index 32a079d..0000000 --- a/sdk/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -find_package(Lua 5.4 REQUIRED) - -add_library (sdk) - -target_include_directories ( - sdk - PRIVATE ${LUA_INCLUDE_DIR} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include -) - -target_link_libraries ( - sdk - PRIVATE ${LUA_LIBRARIES} - PUBLIC core -) - -add_subdirectory (src) - -if (BUILD_TESTS) - add_subdirectory (tests) -endif () diff --git a/sdk/include/sdk/node.hh b/sdk/include/sdk/node.hh deleted file mode 100644 index 7ac3d6b..0000000 --- a/sdk/include/sdk/node.hh +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "core/types.hh" -#include "sdk/path.hh" -#include "sdk/types.hh" -#include -#include -#include -#include -#include - -struct lua_State; -struct LuaNode; - -std::string describe(LuaNode const& value); - -struct LuaNode -{ - LuaNode() = delete; - LuaNode(LuaNode const&) = delete; - LuaNode& operator=(LuaNode const&) = delete; - - LuaNode(LuaNode&&) noexcept; - LuaNode& operator=(LuaNode&&) noexcept; - ~LuaNode() noexcept; - - [[nodiscard]] Result find(LuaKey const&) const; - [[nodiscard]] Result find(LuaPath const& path) const; - - [[nodiscard]] Result expect_boolean() const; - [[nodiscard]] Result expect_integer() const; - [[nodiscard]] Result expect_number() const; - [[nodiscard]] Result expect_string() const; - - [[nodiscard]] Result expect_boolean(LuaKey const&) const; - [[nodiscard]] Result expect_integer(LuaKey const&) const; - [[nodiscard]] Result expect_number(LuaKey const&) const; - [[nodiscard]] Result expect_string(LuaKey const&) const; - - [[nodiscard]] Result expect_boolean(LuaPath const&) const; - [[nodiscard]] Result expect_integer(LuaPath const&) const; - [[nodiscard]] Result expect_number(LuaPath const&) const; - [[nodiscard]] Result expect_string(LuaPath const&) const; - - template - [[nodiscard]] Result expect() const - { - if constexpr (std::same_as) return expect_boolean(); - else if constexpr (std::integral) { - Result value = expect_integer(); - if (!value) return Error(std::move(value).error()); - if (!std::in_range(*value)) { - return Error(describe(*this) + ": integer is outside the destination type's range"); - } - return static_cast(*value); - } else if constexpr (std::floating_point) - return expect_number().transform([](auto value) { - return static_cast(value); - }); - else if constexpr (std::same_as) - return expect_string(); - else static_assert(always_false, "unsupported type"); - } - - template - [[nodiscard]] Result expect(LuaKey const& key) const - { - Result value = find(key); - if (!value) return Error(std::move(value).error()); - return value->expect(); - } - - template - [[nodiscard]] Result expect(LuaPath const& path) const - { - Result value = find(path); - if (!value) return Error(std::move(value).error()); - return value->expect(); - } - - [[nodiscard]] Result> keys() const; - - [[nodiscard]] LuaType type() const noexcept { return type_; } - - [[nodiscard]] LuaPath const& path() const noexcept { return path_; } - -private: - friend struct LuaState; - - template - static constexpr bool always_false = false; - - LuaNode(std::shared_ptr owner, i32 reference, LuaPath path) noexcept; - - std::shared_ptr owner_; - lua_State* handle_; - i32 reference_; - LuaType type_; - LuaPath path_; -}; diff --git a/sdk/include/sdk/path.hh b/sdk/include/sdk/path.hh deleted file mode 100644 index 1cd116d..0000000 --- a/sdk/include/sdk/path.hh +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once -#include "core/panic.hh" -#include "core/types.hh" -#include "sdk/types.hh" -#include -#include -#include -#include - -struct LuaPath; - -std::string describe(LuaKey const& key); -std::string describe(LuaPath const& path); -std::string describe(std::span path); - -struct LuaPath -{ - LuaPath() = default; - LuaPath(LuaPath const&) = default; - LuaPath(LuaPath&&) noexcept = default; - LuaPath& operator=(LuaPath const&) = default; - LuaPath& operator=(LuaPath&&) noexcept = default; - ~LuaPath() noexcept = default; - - constexpr LuaPath(std::initializer_list keys) : keys_(keys) {} - - constexpr LuaPath(std::span keys) : keys_(keys.begin(), keys.end()) {} - - constexpr LuaKey const& operator[](u32 i) const { return keys_[i]; } - - LuaPath& operator+=(LuaPath const& rhs) - { - keys_.insert(keys_.end(), rhs.keys_.begin(), rhs.keys_.end()); - return *this; - } - - friend LuaPath operator+(LuaPath lhs, LuaPath const& rhs) - { - lhs += rhs; - return lhs; - } - - LuaPath& operator+=(LuaKey const& key) - { - keys_.push_back(key); - return *this; - } - - friend LuaPath operator+(LuaPath lhs, LuaKey const& rhs) - { - lhs += rhs; - return lhs; - } - - constexpr u32 size() const { return static_cast(keys_.size()); } - - constexpr bool empty() const { return keys_.empty(); } - - constexpr std::vector::const_iterator begin() const { return keys_.begin(); } - - constexpr std::vector::const_iterator end() const { return keys_.end(); } - - constexpr std::span span() const { return keys_; } - - constexpr std::span span(u32 length) const - { - PRECONDITION(length <= keys_.size()); - return { keys_.begin(), length }; - } - -private: - std::vector keys_; -}; diff --git a/sdk/include/sdk/state.hh b/sdk/include/sdk/state.hh deleted file mode 100644 index e9cefd7..0000000 --- a/sdk/include/sdk/state.hh +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "sdk/node.hh" -#include "sdk/path.hh" -#include -#include - -struct lua_State; - -struct LuaState -{ - LuaState() = delete; - LuaState(LuaState const&) = delete; - LuaState& operator=(LuaState const&) = delete; - LuaState(LuaState&&) noexcept; - LuaState& operator=(LuaState&&) noexcept; - ~LuaState() noexcept; - - [[nodiscard]] static Result create(); - - [[nodiscard]] Status create_table(LuaPath const& path); - [[nodiscard]] Status do_file(std::string_view path); - [[nodiscard]] Status do_string(std::string_view source, std::string_view name = "content"); - [[nodiscard]] Result global(std::string_view name) const; - -private: - explicit LuaState(std::shared_ptr owner) noexcept; - - [[nodiscard]] Status execute(int load_status); - - std::shared_ptr owner_; - lua_State* handle_; -}; diff --git a/sdk/include/sdk/table.hh b/sdk/include/sdk/table.hh deleted file mode 100644 index de7c6f0..0000000 --- a/sdk/include/sdk/table.hh +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once -#include "core/result.hh" -#include "sdk/node.hh" -#include "sdk/types.hh" -#include -#include -#include -#include -#include - -template -struct LuaTraits -{}; - -template -struct MemberPointerTraits; - -template -struct MemberPointerTraits -{ - using class_type = Class; - using member_type = Member; -}; - -template -using LuaTranslator = Status (*)(LuaNode const&, LuaKey const&, Type&); - -// A field erases its member type behind one callback. The callback generated by lua_field still -// knows the exact pointer-to-member type and can therefore dispatch to primitive or table -// translation without adding that member type to a variant. -template -struct LuaField -{ - LuaKey key; - LuaTranslator translate; -}; - -template -using LuaSchema = std::span const>; - -template -concept LuaTable = requires { - { LuaTraits::schema } -> std::convertible_to>; -}; - -template -concept LuaValue = std::same_as || std::integral || std::floating_point - || std::same_as; - -template -Status translate(LuaNode const& node, Type& out); - -template -Status translate(LuaNode const& node, Type& out); - -template -constexpr auto lua_field(LuaKey key) -{ - using MemberPointer = decltype(Member); - using Class = typename MemberPointerTraits::class_type; - - return LuaField{ - .key = std::move(key), - .translate = [](LuaNode const& node, LuaKey const& field, Class& out) { - Result value = node.find(field); - if (!value) return Status(Error(std::move(value).error())); - return translate(*value, out.*Member); - }, - }; -} - -template -Status translate(LuaNode const& node, Type& out) -{ - Result value = node.expect(); - if (!value) return Error(std::move(value).error()); - out = std::move(*value); - return {}; -} - -template -Status translate(LuaNode const& node, Type& out) -{ - if (node.type() != LuaType::Table) { - return Error( - describe(node) + ": expected table but found " + std::string(describe(node.type()))); - } - - for (LuaField const& field : LuaTraits::schema) { - Status result = field.translate(node, field.key, out); - if (!result) return result; - } - return {}; -} - -template -requires std::default_initializable && (LuaValue || LuaTable) -[[nodiscard]] Result translate(LuaNode const& node) -{ - Type out{}; - Status result = translate(node, out); - if (!result) return Error(std::move(result).error()); - return out; -} diff --git a/sdk/include/sdk/types.hh b/sdk/include/sdk/types.hh deleted file mode 100644 index 9d14099..0000000 --- a/sdk/include/sdk/types.hh +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once -#include "core/types.hh" -#include -#include -#include - -using LuaKey = std::variant; - -enum class LuaType : int { - None = -1, - Nil = 0, - Boolean = 1, - LightUserData = 2, - Number = 3, - String = 4, - Table = 5, - Function = 6, - UserData = 7, - Thread = 8 -}; - -[[nodiscard]] constexpr std::string_view describe(LuaType type) noexcept -{ - switch (type) { - case LuaType::None: - return "none"; - case LuaType::Nil: - return "nil"; - case LuaType::Boolean: - return "boolean"; - case LuaType::LightUserData: - return "light userdata"; - case LuaType::Number: - return "number"; - case LuaType::String: - return "string"; - case LuaType::Table: - return "table"; - case LuaType::Function: - return "function"; - case LuaType::UserData: - return "userdata"; - case LuaType::Thread: - return "thread"; - } - return "unknown"; -} diff --git a/sdk/src/CMakeLists.txt b/sdk/src/CMakeLists.txt deleted file mode 100644 index 98d7d1e..0000000 --- a/sdk/src/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -target_sources (sdk PRIVATE node.cc path.cc state.cc) diff --git a/sdk/src/node.cc b/sdk/src/node.cc deleted file mode 100644 index c43f78f..0000000 --- a/sdk/src/node.cc +++ /dev/null @@ -1,296 +0,0 @@ -#include "sdk/node.hh" -#include "core/panic.hh" -#include -#include -#include -#include -#include - -namespace { - -void push_key(lua_State* handle, LuaKey const& key) -{ - std::visit( - [handle](auto const& value) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) { - lua_pushlstring(handle, value.data(), value.size()); - } else { - lua_pushinteger(handle, value); - } - }, - key); -} - -std::string type_error(LuaPath const& path, LuaType expected, LuaType found) -{ - std::string const location = path.empty() ? "value" : describe(path); - return std::format( - "{}: expected {} but found {}", location, describe(expected), describe(found)); -} - -Result reference_child( - lua_State* handle, i32 reference, LuaKey const& key, LuaPath const& parent_path, - LuaPath const& child_path) -{ - LuaType const parent_type - = static_cast(lua_rawgeti(handle, LUA_REGISTRYINDEX, reference)); - if (parent_type != LuaType::Table) { - lua_pop(handle, 1); - return Error(type_error(parent_path, LuaType::Table, parent_type)); - } - - push_key(handle, key); - LuaType const child_type = static_cast(lua_rawget(handle, -2)); - lua_remove(handle, -2); - - if (child_type == LuaType::Nil) { - lua_pop(handle, 1); - return Error(std::format("{}: no value found", describe(child_path))); - } - - return luaL_ref(handle, LUA_REGISTRYINDEX); -} - -template -Result read_value( - lua_State* handle, i32 reference, LuaPath const& path, LuaType expected, Reader read) -{ - LuaType const found = static_cast(lua_rawgeti(handle, LUA_REGISTRYINDEX, reference)); - if (found != expected) { - lua_pop(handle, 1); - return Error(type_error(path, expected, found)); - } - - Type value = read(handle); - lua_pop(handle, 1); - return value; -} - -Result read_key(lua_State* handle) -{ - switch (lua_type(handle, -2)) { - case LUA_TNUMBER: - if (!lua_isinteger(handle, -2)) { - return Error("table contains a non-integer numeric key"); - } - return LuaKey{ static_cast(lua_tointeger(handle, -2)) }; - case LUA_TSTRING: { - size_t length = 0; - char const* data = lua_tolstring(handle, -2, &length); - return LuaKey{ std::string(data, length) }; - } - default: - return Error(std::format( - "table contains unsupported {} key", lua_typename(handle, lua_type(handle, -2)))); - } -} - -} // namespace - -std::string describe(LuaNode const& value) -{ - return value.path().empty() ? "value" : describe(value.path()); -} - -LuaNode::LuaNode(std::shared_ptr owner, i32 reference, LuaPath path) noexcept - : owner_{ std::move(owner) }, handle_{ owner_.get() }, reference_{ reference }, - type_{ LuaType::None }, path_(std::move(path)) -{ - PRECONDITION(handle_ != nullptr); - PRECONDITION(reference != LUA_NOREF && reference != LUA_REFNIL); - type_ = static_cast(lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_)); - lua_pop(handle_, 1); -} - -LuaNode::LuaNode(LuaNode&& other) noexcept - : owner_{ std::move(other.owner_) }, handle_{ other.handle_ }, reference_{ other.reference_ }, - type_{ other.type_ }, - path_{ std::move(other.path_) } -{ - other.handle_ = nullptr; - other.reference_ = LUA_NOREF; - other.type_ = LuaType::None; -} - -LuaNode& LuaNode::operator=(LuaNode&& other) noexcept -{ - if (&other == this) return *this; - - if (handle_ != nullptr && reference_ != LUA_NOREF && reference_ != LUA_REFNIL) { - luaL_unref(handle_, LUA_REGISTRYINDEX, reference_); - } - - owner_ = std::move(other.owner_); - handle_ = other.handle_; - reference_ = other.reference_; - type_ = other.type_; - path_ = std::move(other.path_); - other.handle_ = nullptr; - other.reference_ = LUA_NOREF; - other.type_ = LuaType::None; - return *this; -} - -LuaNode::~LuaNode() noexcept -{ - if (handle_ != nullptr && reference_ != LUA_NOREF && reference_ != LUA_REFNIL) { - luaL_unref(handle_, LUA_REGISTRYINDEX, reference_); - } -} - -Result LuaNode::find(LuaKey const& key) const -{ - PRECONDITION(handle_ != nullptr); - LuaPath const child_path = path_ + key; - Result reference = reference_child(handle_, reference_, key, path_, child_path); - if (!reference) return Error(std::move(reference).error()); - return LuaNode(owner_, *reference, child_path); -} - -Result LuaNode::find(LuaPath const& path) const -{ - PRECONDITION(handle_ != nullptr); - if (path.empty()) return Error("cannot resolve an empty Lua path"); - - Result current = find(path[0]); - for (u32 i = 1; current && i < path.size(); ++i) { - current = current->find(path[i]); - } - return current; -} - -Result LuaNode::expect_boolean() const -{ - PRECONDITION(handle_ != nullptr); - return read_value( - handle_, reference_, path_, LuaType::Boolean, - [](lua_State* handle) { return lua_toboolean(handle, -1) != 0; }); -} - -Result LuaNode::expect_integer() const -{ - PRECONDITION(handle_ != nullptr); - LuaType const found = static_cast(lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_)); - if (found != LuaType::Number || !lua_isinteger(handle_, -1)) { - lua_pop(handle_, 1); - std::string const location = path_.empty() ? "value" : describe(path_); - return Error(std::format("{}: expected integer but found {}", location, describe(found))); - } - - i64 const value = static_cast(lua_tointeger(handle_, -1)); - lua_pop(handle_, 1); - return value; -} - -Result LuaNode::expect_number() const -{ - PRECONDITION(handle_ != nullptr); - return read_value( - handle_, reference_, path_, LuaType::Number, - [](lua_State* handle) { return static_cast(lua_tonumber(handle, -1)); }); -} - -Result LuaNode::expect_string() const -{ - PRECONDITION(handle_ != nullptr); - return read_value( - handle_, reference_, path_, LuaType::String, - [](lua_State* handle) { - size_t length = 0; - char const* data = lua_tolstring(handle, -1, &length); - return std::string(data, length); - }); -} - -Result LuaNode::expect_boolean(LuaKey const& key) const -{ - Result value = find(key); - if (!value) return Error(std::move(value).error()); - return value->expect_boolean(); -} - -Result LuaNode::expect_integer(LuaKey const& key) const -{ - Result value = find(key); - if (!value) return Error(std::move(value).error()); - return value->expect_integer(); -} - -Result LuaNode::expect_number(LuaKey const& key) const -{ - Result value = find(key); - if (!value) return Error(std::move(value).error()); - return value->expect_number(); -} - -Result LuaNode::expect_string(LuaKey const& key) const -{ - Result value = find(key); - if (!value) return Error(std::move(value).error()); - return value->expect_string(); -} - -Result LuaNode::expect_boolean(LuaPath const& path) const -{ - Result value = find(path); - if (!value) return Error(std::move(value).error()); - return value->expect_boolean(); -} - -Result LuaNode::expect_integer(LuaPath const& path) const -{ - Result value = find(path); - if (!value) return Error(std::move(value).error()); - return value->expect_integer(); -} - -Result LuaNode::expect_number(LuaPath const& path) const -{ - Result value = find(path); - if (!value) return Error(std::move(value).error()); - return value->expect_number(); -} - -Result LuaNode::expect_string(LuaPath const& path) const -{ - Result value = find(path); - if (!value) return Error(std::move(value).error()); - return value->expect_string(); -} - -Result> LuaNode::keys() const -{ - PRECONDITION(handle_ != nullptr); - LuaType const found = static_cast(lua_rawgeti(handle_, LUA_REGISTRYINDEX, reference_)); - if (found != LuaType::Table) { - lua_pop(handle_, 1); - return Error(type_error(path_, LuaType::Table, found)); - } - - std::vector keys; - lua_pushnil(handle_); - while (lua_next(handle_, -2) != 0) { - Result key = read_key(handle_); - lua_pop(handle_, 1); - if (!key) { - lua_pop(handle_, 2); - return Error(std::format("{}: {}", describe(*this), std::move(key).error())); - } - keys.push_back(std::move(*key)); - } - lua_pop(handle_, 1); - - std::ranges::sort(keys, [](LuaKey const& lhs, LuaKey const& rhs) { - if (lhs.index() != rhs.index()) return lhs.index() < rhs.index(); - return std::visit( - [](auto const& left, auto const& right) { - using Left = std::remove_cvref_t; - using Right = std::remove_cvref_t; - if constexpr (std::same_as) return left < right; - else return false; - }, - lhs, rhs); - }); - return keys; -} diff --git a/sdk/src/path.cc b/sdk/src/path.cc deleted file mode 100644 index 26ced92..0000000 --- a/sdk/src/path.cc +++ /dev/null @@ -1,36 +0,0 @@ -#include "sdk/path.hh" -#include -#include -#include -#include - -std::string describe(LuaKey const& key) -{ - return std::visit( - [](auto const& value) -> std::string { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) return value; - else if constexpr (std::integral) return std::to_string(value); - else static_assert(false, "unsupported lua key type"); - }, - key); -} - -std::string describe(LuaPath const& path) { return describe(path.span()); } - -std::string describe(std::span path) -{ - std::stringstream out; - for (u32 i = 0; i < path.size(); ++i) { - std::visit( - [&out, i](auto const& key) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) { - if (i > 0) out << '.'; - out << key; - } else out << '[' << key << ']'; - }, - path[i]); - } - return std::move(out).str(); -} diff --git a/sdk/src/state.cc b/sdk/src/state.cc deleted file mode 100644 index 668f25e..0000000 --- a/sdk/src/state.cc +++ /dev/null @@ -1,188 +0,0 @@ -#include "sdk/state.hh" -#include "core/panic.hh" -#include -#include -#include -#include -#include - -namespace { - -void push_key(lua_State* handle, LuaKey const& key) -{ - std::visit( - [handle](auto const& value) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) { - lua_pushlstring(handle, value.data(), value.size()); - } else { - lua_pushinteger(handle, value); - } - }, - key); -} - -std::string pop_string(lua_State* handle) -{ - size_t length = 0; - char const* data = lua_tolstring(handle, -1, &length); - std::string result = data == nullptr ? "unknown Lua error" : std::string(data, length); - lua_pop(handle, 1); - return result; -} - -int add_traceback(lua_State* handle) -{ - char const* message = lua_tostring(handle, 1); - if (message == nullptr) return 1; - luaL_traceback(handle, handle, message, 1); - return 1; -} - -void open_content_libraries(lua_State* handle) -{ - struct Library - { - char const* name; - lua_CFunction open; - }; - - constexpr Library libraries[] = { - { LUA_GNAME, luaopen_base }, - { LUA_TABLIBNAME, luaopen_table }, - { LUA_STRLIBNAME, luaopen_string }, - { LUA_MATHLIBNAME, luaopen_math }, - { LUA_UTF8LIBNAME, luaopen_utf8 }, - }; - - for (Library const& library : libraries) { - luaL_requiref(handle, library.name, library.open, 1); - lua_pop(handle, 1); - } - - constexpr char const* unavailable[] = { "collectgarbage", "dofile", "load", "loadfile" }; - for (char const* name : unavailable) { - lua_pushnil(handle); - lua_setglobal(handle, name); - } -} - -} // namespace - -LuaState::LuaState(std::shared_ptr owner) noexcept - : owner_{ std::move(owner) }, handle_{ owner_.get() } -{ - PRECONDITION(handle_ != nullptr); -} - -LuaState::LuaState(LuaState&& other) noexcept - : owner_{ std::move(other.owner_) }, handle_{ owner_.get() } -{ - other.handle_ = nullptr; -} - -LuaState& LuaState::operator=(LuaState&& other) noexcept -{ - if (&other == this) return *this; - owner_ = std::move(other.owner_); - handle_ = owner_.get(); - other.handle_ = nullptr; - return *this; -} - -LuaState::~LuaState() noexcept = default; - -Result LuaState::create() -{ - lua_State* handle = luaL_newstate(); - if (handle == nullptr) return Error("failed to create Lua state"); - - std::shared_ptr owner(handle, lua_close); - open_content_libraries(handle); - return LuaState(std::move(owner)); -} - -Status LuaState::create_table(LuaPath const& path) -{ - PRECONDITION(handle_ != nullptr); - if (path.empty()) return Error("cannot create a table at an empty Lua path"); - - int const base = lua_gettop(handle_); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); - - LuaPath current; - for (LuaKey const& key : path) { - current += key; - push_key(handle_, key); - int const type = lua_rawget(handle_, -2); - - if (type == LUA_TNIL) { - lua_pop(handle_, 1); - lua_newtable(handle_); - push_key(handle_, key); - lua_pushvalue(handle_, -2); - lua_rawset(handle_, -4); - } else if (type != LUA_TTABLE) { - std::string message = std::format( - "{}: expected table but found {}", describe(current), lua_typename(handle_, type)); - lua_settop(handle_, base); - return Error(std::move(message)); - } - - lua_remove(handle_, -2); - } - - lua_settop(handle_, base); - return {}; -} - -Status LuaState::do_file(std::string_view path) -{ - PRECONDITION(handle_ != nullptr); - std::string const terminated_path(path); - return execute(luaL_loadfile(handle_, terminated_path.c_str())); -} - -Status LuaState::do_string(std::string_view source, std::string_view name) -{ - PRECONDITION(handle_ != nullptr); - std::string const terminated_name(name); - return execute(luaL_loadbuffer(handle_, source.data(), source.size(), terminated_name.c_str())); -} - -Status LuaState::execute(int load_status) -{ - PRECONDITION(handle_ != nullptr); - if (load_status != LUA_OK) return Error(pop_string(handle_)); - - int const function = lua_gettop(handle_); - lua_pushcfunction(handle_, add_traceback); - lua_insert(handle_, function); - - int const call_status = lua_pcall(handle_, 0, 0, function); - if (call_status != LUA_OK) { - std::string message = pop_string(handle_); - lua_remove(handle_, function); - return Error(std::move(message)); - } - - lua_remove(handle_, function); - return {}; -} - -Result LuaState::global(std::string_view name) const -{ - PRECONDITION(handle_ != nullptr); - lua_rawgeti(handle_, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); - lua_pushlstring(handle_, name.data(), name.size()); - int const type = lua_rawget(handle_, -2); - lua_remove(handle_, -2); - - if (type == LUA_TNIL) { - lua_pop(handle_, 1); - return Error(std::format("{}: no value found", name)); - } - - i32 const reference = luaL_ref(handle_, LUA_REGISTRYINDEX); - return LuaNode(owner_, reference, LuaPath{ LuaKey{ std::string(name) } }); -} diff --git a/sdk/tests/CMakeLists.txt b/sdk/tests/CMakeLists.txt deleted file mode 100644 index 3f09164..0000000 --- a/sdk/tests/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -list (APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) - -add_executable (test-sdk) - -target_sources (test-sdk PRIVATE lua.cc) - -target_link_libraries ( - test-sdk - PRIVATE sdk - PRIVATE Catch2::Catch2WithMain -) - -include (CTest) -include (Catch) -catch_discover_tests (test-sdk) diff --git a/sdk/tests/lua.cc b/sdk/tests/lua.cc deleted file mode 100644 index 5f67278..0000000 --- a/sdk/tests/lua.cc +++ /dev/null @@ -1,150 +0,0 @@ -#include "sdk/state.hh" -#include "sdk/table.hh" -#include -#include -#include -#include - -namespace { - -struct Rectangle -{ - f32 x; - f32 y; - f32 width; - f32 height; -}; - -struct Sprite -{ - std::string atlas; - Rectangle source; - u32 tint; -}; - -} // namespace - -template <> -struct LuaTraits -{ - static constexpr LuaField schema[] = { - lua_field<&Rectangle::x>(1), - lua_field<&Rectangle::y>(2), - lua_field<&Rectangle::width>(3), - lua_field<&Rectangle::height>(4), - }; -}; - -template <> -struct LuaTraits -{ - static constexpr LuaField schema[] = { - lua_field<&Sprite::atlas>("atlas"), - lua_field<&Sprite::source>("source"), - lua_field<&Sprite::tint>("tint"), - }; -}; - -TEST_CASE("Lua nodes resolve typed values through paths") -{ - Result state = LuaState::create(); - REQUIRE(state.has_value()); - REQUIRE(state->do_string("content = { enabled = true, count = 42, name = 'grass' }") - .has_value()); - - Result content = state->global("content"); - REQUIRE(content.has_value()); - CHECK(content->expect("enabled") == true); - CHECK(content->expect("count") == 42); - CHECK(content->expect("name") == "grass"); -} - -TEST_CASE("Lua nodes keep their state alive") -{ - Result content = []() -> Result { - Result state = LuaState::create(); - if (!state) return Error(std::move(state).error()); - Status result = state->do_string("content = { value = 42 }"); - if (!result) return Error(std::move(result).error()); - return state->global("content"); - }(); - - REQUIRE(content.has_value()); - CHECK(content->expect("value") == 42); -} - -TEST_CASE("Lua schemas recursively translate nested tables") -{ - Result state = LuaState::create(); - REQUIRE(state.has_value()); - REQUIRE(state - ->do_string( - "content = { sprite = { atlas = 'tiles.png', source = { 16, 32, 8, 12 }, " - "tint = 4294967295 } }") - .has_value()); - - Result content = state->global("content"); - REQUIRE(content.has_value()); - Result node = content->find("sprite"); - REQUIRE(node.has_value()); - - Result sprite = translate(*node); - REQUIRE(sprite.has_value()); - CHECK(sprite->atlas == "tiles.png"); - CHECK(sprite->source.x == 16.0f); - CHECK(sprite->source.y == 32.0f); - CHECK(sprite->source.width == 8.0f); - CHECK(sprite->source.height == 12.0f); - CHECK(sprite->tint == 0xffffffffu); -} - -TEST_CASE("Lua translation errors identify the complete nested path") -{ - Result state = LuaState::create(); - REQUIRE(state.has_value()); - REQUIRE(state - ->do_string( - "content = { sprite = { atlas = 'tiles.png', source = { 16, 32, 'wide', 12 }, " - "tint = 1 } }") - .has_value()); - - Result content = state->global("content"); - REQUIRE(content.has_value()); - Result node = content->find("sprite"); - REQUIRE(node.has_value()); - - Result sprite = translate(*node); - REQUIRE_FALSE(sprite.has_value()); - CHECK(sprite.error() == "content.sprite.source[3]: expected number but found string"); -} - -TEST_CASE("Lua table keys have deterministic traversal order") -{ - Result state = LuaState::create(); - REQUIRE(state.has_value()); - REQUIRE(state->do_string("content = { z = true, a = true, [2] = true, [1] = true }") - .has_value()); - - Result content = state->global("content"); - REQUIRE(content.has_value()); - Result> keys = content->keys(); - REQUIRE(keys.has_value()); - REQUIRE(keys->size() == 4); - CHECK(std::get((*keys)[0]) == 1); - CHECK(std::get((*keys)[1]) == 2); - CHECK(std::get((*keys)[2]) == "a"); - CHECK(std::get((*keys)[3]) == "z"); -} - -TEST_CASE("Lua integer translation rejects narrowing") -{ - Result state = LuaState::create(); - REQUIRE(state.has_value()); - REQUIRE(state->do_string("content = { value = 256 }").has_value()); - - Result content = state->global("content"); - REQUIRE(content.has_value()); - Result value = content->expect("value"); - REQUIRE_FALSE(value.has_value()); - CHECK(value.error().contains("outside the destination type's range")); -} diff --git a/sys/CMakeLists.txt b/sys/CMakeLists.txt index 1abee9d..f2ff24d 100644 --- a/sys/CMakeLists.txt +++ b/sys/CMakeLists.txt @@ -1,17 +1,14 @@ -add_library (sys) +add_library(sys) -target_include_directories ( +target_include_directories( sys - PUBLIC ${SDL3_INCLUDE_DIRS} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${SDL3_INCLUDE_DIRS} ) -target_link_libraries ( +target_link_libraries( sys - PUBLIC SDL3::SDL3 - PUBLIC glm::glm - PUBLIC core + PUBLIC core SDL3::SDL3 glm::glm ) -add_subdirectory (src) +add_subdirectory(src) diff --git a/sys/src/main.cc b/sys/src/main.cc index 1d22b3c..4130865 100644 --- a/sys/src/main.cc +++ b/sys/src/main.cc @@ -37,13 +37,9 @@ SDL_AppResult SDL_AppEvent(void* state, SDL_Event* event) auto runtime = static_cast(state); handle_event(*runtime, *event); switch (event->type) { - case SDL_EVENT_QUIT: - return SDL_APP_SUCCESS; - case SDL_EVENT_WINDOW_FOCUS_LOST: - SDL_ResetKeyboard(); - break; - default: - break; + case SDL_EVENT_QUIT: return SDL_APP_SUCCESS; + case SDL_EVENT_WINDOW_FOCUS_LOST: SDL_ResetKeyboard(); break; + default: break; } return SDL_APP_CONTINUE; } From e3dd9dd9050709f6e5b93abfcbc77ad3b0925422 Mon Sep 17 00:00:00 2001 From: Imp Date: Wed, 30 Sep 2026 00:21:14 +0000 Subject: [PATCH 4/4] refactor(lua): finish moving content authorship to app Complete the WIP move of Lua-based content loading from the game layer into app, and bring the whole branch back to a compiling state. Lua library (app/lua): - Fallible operations return std::optional (or bool) and print their diagnostic to stdout, prefixed with the value's path. - Values and tables carry their path as a std::string, formatted the way it would be written in Lua (i.e. content.terrain["grass-1"].sprite). - Reference holds the lua_State* directly so it survives the owning State being moved. - Table::keys() returns integer keys ascending, then names sorted, so traversal order is deterministic. - Integer reads clamp to the destination type's range; number reads accept Lua integers. App: - load_content() runs the script, defines Game::Terrain in the world's catalog, and builds sprites parallel to the terrain definitions. Errors accumulate across definitions before failing. - App::State replaces Application and works on Game::World directly. - Port the deleted sdk Lua tests to app/tests. Game: - Finish the Game::World API and fix the grassland and cave generators. - Fix Command::is_empty(), which returned the inverse. Core: - Qualify detail::panic in PANIC so it works inside namespaces that have their own detail namespace. - Fix BasicCatalog::try_get returning a value instead of a pointer, and initialize its generation. - Give DEFER a unique, non-reserved variable name. Build: - Link game against GLM, which it uses for Pose and command movement. - README: require Clang 19+, the first version whose concepts support enables std::expected in libstdc++. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EukP9918Z3pX9d2utk9WWE --- README.md | 2 +- app/CMakeLists.txt | 8 +- app/include/app/content.hh | 31 ++-- app/include/app/keyboard-pilot.hh | 18 ++- app/include/app/lua/key.hh | 48 +++--- app/include/app/lua/load.hh | 5 - app/include/app/lua/reference.hh | 39 +++-- app/include/app/lua/state.hh | 29 ++-- app/include/app/lua/table.hh | 60 +++++--- app/include/app/lua/types.hh | 3 + app/include/app/lua/value.hh | 121 ++++++++------- app/include/app/pilot.hh | 21 ++- app/include/app/sprite.hh | 19 ++- app/include/app/state.hh | 40 ++--- app/src/CMakeLists.txt | 3 +- app/src/application.cc | 142 ----------------- app/src/content.cc | 163 +++++++++++++++++--- app/src/keyboard-pilot.cc | 24 +++ app/src/keyboard.cc | 23 --- app/src/load.cc | 59 ------- app/src/lua/CMakeLists.txt | 2 +- app/src/lua/reference.cc | 100 ++++++------ app/src/lua/state.cc | 102 ++++++------ app/src/lua/table.cc | 138 ++++++++++------- app/src/lua/types.cc | 38 ----- app/src/lua/value.cc | 222 ++++++++++++++------------- app/src/sprite.cc | 17 -- app/src/state.cc | 105 +++++++++++++ app/tests/CMakeLists.txt | 15 ++ app/tests/lua.cc | 200 ++++++++++++++++++++++++ content/terrain.lua | 2 +- core/include/core/basic-catalog.hh | 32 ++-- core/include/core/defer.hh | 11 +- core/include/core/panic.hh | 8 +- game/CMakeLists.txt | 2 +- game/include/game/biome/caves.hh | 8 +- game/include/game/biome/grassland.hh | 4 +- game/include/game/chunk.hh | 4 - game/include/game/command.hh | 2 +- game/include/game/content/terrain.hh | 14 +- game/include/game/entity.hh | 12 -- game/include/game/tile.hh | 2 +- game/include/game/world.hh | 17 +- game/src/CMakeLists.txt | 2 +- game/src/biome/CMakeLists.txt | 2 +- game/src/biome/caves.cc | 32 ++-- game/src/biome/grassland.cc | 45 +++--- game/src/command.cc | 2 +- game/src/world.cc | 33 ++-- play.cc | 65 ++++---- 50 files changed, 1179 insertions(+), 917 deletions(-) delete mode 100644 app/include/app/lua/load.hh delete mode 100644 app/src/application.cc create mode 100644 app/src/keyboard-pilot.cc delete mode 100644 app/src/keyboard.cc delete mode 100644 app/src/load.cc delete mode 100644 app/src/lua/types.cc delete mode 100644 app/src/sprite.cc create mode 100644 app/src/state.cc create mode 100644 app/tests/CMakeLists.txt create mode 100644 app/tests/lua.cc diff --git a/README.md b/README.md index e8448d4..bd1f29b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is the early development of a game and engine built from scratch in C++23. ### Toolchain -- A C++23 compiler that supports GNU statement expressions: GCC 14+, Clang 17+ +- A C++23 compiler that supports GNU statement expressions: GCC 14+, Clang 19+ - [CMake](https://cmake.org/) 3.25+ - [Lua](https://www.lua.org/) 5.4+ diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d3b5a92..4f91906 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -10,8 +10,12 @@ target_include_directories( target_link_libraries( app - PRIVATE ${LUA_LIBRARIES} gfx - PUBLIC game + PRIVATE ${LUA_LIBRARIES} + PUBLIC game gfx ) add_subdirectory(src) + +if (BUILD_TESTS) + add_subdirectory(tests) +endif () diff --git a/app/include/app/content.hh b/app/include/app/content.hh index f8ea887..7803361 100644 --- a/app/include/app/content.hh +++ b/app/include/app/content.hh @@ -1,19 +1,28 @@ #pragma once #include "app/sprite.hh" -#include "game/content/terrain.hh" +#include "core/basic-catalog.hh" +#include "gfx/texture.hh" +#include +#include -struct Texture; +struct Renderer; -namespace Lua { - struct Table; +namespace Game { struct World; } - bool import(Color&, Table const&); +namespace App { + using Assets = TypeList; + using Catalog = BasicCatalog; // labeled by file path - bool import(Game::Content&, Table const&); + // The presentation of content definitions. + struct Visuals + { + Catalog assets; + std::vector terrain; // parallel to the world's terrain definitions + }; - bool import(App::Assets&, App::Sprite&, Table const&); - - bool import(Game::Content&, Game::Terrain&, Table const&); - - bool read(Content&, Lua::Table const&, Sprite&); + // Runs the Lua content script at the path, then compiles the definitions it authored into the + // world's catalog and their presentation into the visuals. Diagnostics are printed to stdout. + // Returns false if the script failed or any definition could not be compiled. + [[nodiscard]] bool + load_content(std::string_view path, Game::World& world, Renderer& renderer, Visuals& visuals); } diff --git a/app/include/app/keyboard-pilot.hh b/app/include/app/keyboard-pilot.hh index 30ed4a5..5daae8a 100644 --- a/app/include/app/keyboard-pilot.hh +++ b/app/include/app/keyboard-pilot.hh @@ -3,13 +3,15 @@ #include "sys/keyboard.hh" #include "sys/mouse.hh" -struct KeyboardPilot : Pilot -{ - void handle_event(SDL_Event const& event) override; - void steer(World const&, CommandBuffer& out, Entity e) override; +namespace App { + struct KeyboardPilot : Pilot + { + void handle_event(SDL_Event const& event) override; + void steer(Game::World const&, Game::CommandBuffer& out, Game::Entity entity) override; -private: + private: - Keyboard keyboard; - Mouse mouse; -}; + Keyboard keyboard_; + Mouse mouse_; + }; +} diff --git a/app/include/app/lua/key.hh b/app/include/app/lua/key.hh index 5269ff7..fef9161 100644 --- a/app/include/app/lua/key.hh +++ b/app/include/app/lua/key.hh @@ -1,57 +1,47 @@ #pragma once +#include "app/lua/types.hh" #include "core/panic.hh" -#include "core/types.hh" #include namespace Lua { + // Identifies a field of a table by name or by integer index. // Note: Key does not own or manage a copy of the string value. It holds a string view. struct Key { enum class Kind : u8 { Index, Name }; - constexpr Key(char const* name) : _kind{ Kind::Name }, _name{ name } {} + constexpr Key(char const* name) noexcept : kind_{ Kind::Name }, name_{ name } {} - constexpr Key(std::string_view name) : _kind{ Kind::Name }, _name{ name } {} + constexpr Key(std::string_view name) noexcept : kind_{ Kind::Name }, name_{ name } {} - constexpr Key(int index) noexcept : _kind{ Kind::Index }, _index{ index } {} + constexpr Key(Integer index) noexcept : kind_{ Kind::Index }, index_{ index } {} - constexpr Key& operator=(std::string_view name) - { - _kind = Kind::Name; - _name = name; - return *this; - } - - constexpr Key& operator=(int index) noexcept - { - _kind = Kind::Index; - _index = index; - return *this; - } + constexpr Key(int index) noexcept : Key(Integer{ index }) {} - [[nodiscard]] std::string_view as_name() const + [[nodiscard]] constexpr std::string_view as_name() const noexcept { - DEBUG_ASSERT(_kind == Kind::Name); - return std::string_view(_name.begin(), _name.end()); + DEBUG_ASSERT(kind_ == Kind::Name); + return name_; } - [[nodiscard]] int as_index() const noexcept + [[nodiscard]] constexpr Integer as_index() const noexcept { - DEBUG_ASSERT(_kind == Kind::Index); - return _index; + DEBUG_ASSERT(kind_ == Kind::Index); + return index_; } - [[nodiscard]] int is_index() const noexcept { return _kind == Kind::Index; } + [[nodiscard]] constexpr bool is_index() const noexcept { return kind_ == Kind::Index; } - [[nodiscard]] int is_name() const noexcept { return _kind == Kind::Name; } + [[nodiscard]] constexpr bool is_name() const noexcept { return kind_ == Kind::Name; } - [[nodiscard]] Kind kind() const noexcept { return _kind; } + [[nodiscard]] constexpr Kind kind() const noexcept { return kind_; } private: - Kind _kind = Kind::Index; - int _index = 0; - std::string_view _name = {}; + + Kind kind_ = Kind::Index; + Integer index_ = 0; + std::string_view name_ = {}; }; } // namespace Lua diff --git a/app/include/app/lua/load.hh b/app/include/app/lua/load.hh deleted file mode 100644 index d1a0271..0000000 --- a/app/include/app/lua/load.hh +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once -#include "game/types.hh" -#include - -bool load_content(LoadContext ctx, std::string_view path); diff --git a/app/include/app/lua/reference.hh b/app/include/app/lua/reference.hh index c80e836..1a25042 100644 --- a/app/include/app/lua/reference.hh +++ b/app/include/app/lua/reference.hh @@ -1,40 +1,45 @@ #pragma once -#include "sdk/types.hh" +#include "app/lua/types.hh" -namespace Lua { +struct lua_State; - struct State; +namespace Lua { + // Owns a slot in the Lua registry that keeps a value alive. // Must not outlive the state that created it. struct Reference { - Reference() noexcept; - Reference(State* state, i32 ridx) noexcept; + // Pops the value on the top of the stack into a new reference. + [[nodiscard]] static Reference pop(lua_State* L) noexcept; + + Reference() noexcept = default; Reference(Reference const&) noexcept; Reference(Reference&&) noexcept; Reference& operator=(Reference const&) noexcept; Reference& operator=(Reference&&) noexcept; ~Reference() noexcept; - // Returns true if both refer to the same Lua value. + // Returns true if both refer to the same Lua value (raw equality). friend bool operator==(Reference const& l, Reference const& r) noexcept; - friend bool operator!=(Reference const& l, Reference const& r) noexcept - { - return !(operator==(l, r)); - } - - [[nodiscard]] Type type() const noexcept { return _type; } + [[nodiscard]] Type type() const noexcept { return type_; } - [[nodiscard]] State& state() const noexcept { return *_state; } + [[nodiscard]] lua_State* state() const noexcept { return state_; } - // Pushes the referenced value onto the top of the stack. + // Pushes the referenced value onto the top of the stack and returns its stack index. int push() const noexcept; private: - State* _state; // non-owning pointer - int _ridx; - Type _type; + + Reference(lua_State* L, int ridx, Type type) noexcept; + + void release() noexcept; + + static constexpr int no_ref = -2; // LUA_NOREF + + lua_State* state_ = nullptr; // non-owning pointer + int ridx_ = no_ref; + Type type_ = Type::None; }; } // namespace Lua diff --git a/app/include/app/lua/state.hh b/app/include/app/lua/state.hh index eb0a948..e253eb5 100644 --- a/app/include/app/lua/state.hh +++ b/app/include/app/lua/state.hh @@ -1,19 +1,20 @@ #pragma once - -#include "core/result.hh" -#include "sdk/table.hh" -#include "sdk/value.hh" +#include "app/lua/table.hh" +#include +#include #include struct lua_State; namespace Lua { + // Owns a sandboxed Lua state: only the base, table, string, math, and utf8 libraries are opened. + // Failures print a diagnostic. struct State { - [[nodiscard]] static Result create(); + [[nodiscard]] static std::optional create(); - State() = default; + State() noexcept = default; State(State const&) = delete; State& operator=(State const&) = delete; @@ -21,20 +22,24 @@ namespace Lua { State& operator=(State&&) noexcept; ~State() noexcept; - [[nodiscard]] Status load(std::string_view file); + // Runs the file at the given path. + [[nodiscard]] bool load(std::string_view path); - [[nodiscard]] Status execute(std::string_view name, std::string_view source); + // Runs the source code, using the name to identify the chunk in diagnostics. + [[nodiscard]] bool execute(std::string_view name, std::string_view source); - [[nodiscard]] Table globals() noexcept; + [[nodiscard]] Table globals() const; - [[nodiscard]] Table create_table() noexcept; + // The path is used to identify the table and its fields in diagnostics. + [[nodiscard]] Table create_table(std::string path = {}) const; - [[nodiscard]] lua_State* get() const noexcept { return _handle; } + [[nodiscard]] lua_State* get() const noexcept { return handle_; } private: + explicit State(lua_State*) noexcept; - lua_State* _handle; + lua_State* handle_ = nullptr; }; } // namespace Lua diff --git a/app/include/app/lua/table.hh b/app/include/app/lua/table.hh index 602573d..c128766 100644 --- a/app/include/app/lua/table.hh +++ b/app/include/app/lua/table.hh @@ -1,57 +1,73 @@ #pragma once -// TODO: ability to iterate over all non-nil fields in the table +// TODO: iterate over key/value pairs without collecting the keys first -#include "sdk/key.hh" -#include "sdk/reference.hh" -#include "sdk/value.hh" +#include "app/lua/key.hh" +#include "app/lua/reference.hh" +#include "app/lua/value.hh" +#include +#include #include namespace Lua { struct Table; - template - concept TableReadable = requires (T& dst, Table const& src) { + template + concept TableReadable = requires (Type& dst, Table const& src) { { read(dst, src) } -> std::same_as; }; + // A table held by a Lua state, annotated with the path it was reached from. // Must not outlive the state that created it. - // Ignores metamethods... + // Access is raw: metamethods are ignored. struct Table { - Table(Reference, std::string path) noexcept; + // Assumes: the reference holds a table. + Table(Reference reference, std::string path) noexcept; - template - [[nodiscard]] bool read(T& dst) const + template + [[nodiscard]] bool read(Type& dst) const { - return read(dst, *this); + return detail::dispatch_read(dst, *this); } - // Returns a nil-value object on missing field - [[nodiscard]] Value operator[](Key key) const noexcept; + // Returns a nil value on a missing field. + [[nodiscard]] Value operator[](Key key) const; void set(Key key, bool value) const noexcept; - void set(Key key, f64 value) const noexcept; - void set(Key key, i64 value) const noexcept; + void set(Key key, Number value) const noexcept; + void set(Key key, Integer value) const noexcept; void set(Key key, std::string_view value) const noexcept; - void set(Key key, Table const& table) const noexcept { set(key, table._reference); } + void set(Key key, int value) const noexcept { set(key, Integer{ value }); } - void set(Key key, Value const& value) const noexcept { set(key, value._reference); } + // Prevents string literals from decaying into booleans. + void set(Key key, char const* value) const noexcept { set(key, std::string_view(value)); } - // Assumes that the handle was produced by the same state. - void set(Key key, Reference const& handle) const noexcept; + void set(Key key, Table const& value) const noexcept { set(key, value.reference_); } + + void set(Key key, Value const& value) const noexcept { set(key, value.reference()); } + + // Assumes: the reference was produced by the same state. + void set(Key key, Reference const& value) const noexcept; void erase(Key key) const noexcept; + // Returns the integer and string keys of the table in a deterministic order: integers in + // ascending order followed by names in lexicographic order. Keys of other types are skipped + // with a diagnostic. + // Note: names view strings owned by the Lua state and remain valid while the table holds them. [[nodiscard]] std::vector keys() const; - [[nodiscard]] std::string_view path() const { return _path; } + [[nodiscard]] std::string_view path() const noexcept { return path_; } + + [[nodiscard]] Reference const& reference() const noexcept { return reference_; } private: - Reference _reference; - std::string _path; + + Reference reference_; + std::string path_; }; } // namespace Lua diff --git a/app/include/app/lua/types.hh b/app/include/app/lua/types.hh index 5df8f25..84b9838 100644 --- a/app/include/app/lua/types.hh +++ b/app/include/app/lua/types.hh @@ -4,7 +4,9 @@ namespace Lua { + // Mirrors the Lua C API type tags, with integers distinguished from other numbers. enum class Type : i8 { + None = -1, Nil = 0, Boolean = 1, LightUserData = 2, @@ -20,6 +22,7 @@ namespace Lua { [[nodiscard]] constexpr std::string_view type_name(Type type) noexcept { switch (type) { + case Type::None: return "none"; case Type::Nil: return "nil"; case Type::Boolean: return "boolean"; case Type::LightUserData: return "light userdata"; diff --git a/app/include/app/lua/value.hh b/app/include/app/lua/value.hh index 020d0c9..6eb3bf4 100644 --- a/app/include/app/lua/value.hh +++ b/app/include/app/lua/value.hh @@ -1,57 +1,69 @@ #pragma once -#include "core/types.hh" -#include "sdk/reference.hh" -#include "sdk/types.hh" +#include "app/lua/reference.hh" +#include "app/lua/types.hh" #include #include #include #include - -struct Rectangle; -struct Color; +#include +#include namespace Lua { struct Table; struct Value; - template - concept ValueReadable = requires (T& dst, Value const& src) { - { read(dst, src) } -> std::same_as; - }; + // Readers translate a Lua value into a C++ object. On failure, they print a diagnostic that + // includes the value's path and return false. Overloads for other types can be declared in + // namespace Lua and will be found by `Value::read`. - bool read(bool& dst, Value const& srct); + bool read(bool& dst, Value const& src); + bool read(Integer& dst, Value const& src); + bool read(Number& dst, Value const& src); + bool read(std::string& dst, Value const& src); - bool read(std::string& dst, Value const& srct); + template + bool read(Type& dst, Value const& src); - template - bool read(T& dst, Value const& srct); + template + bool read(Type& dst, Value const& src); - template - bool read(T& dst, Value const& src); + template + concept ValueReadable = requires (Type& dst, Value const& src) { + { read(dst, src) } -> std::same_as; + }; + + namespace detail { + // Calls `read` from outside the scope of the member functions of the same name, which would + // otherwise hide the free functions. + template + bool dispatch_read(Type& dst, Source const& src) + { + return read(dst, src); + } + } + // A value held by a Lua state, annotated with the path it was reached from. // Must not outlive the state that created it. struct Value { - Value(Reference, std::string path) noexcept; + Value(Reference reference, std::string path) noexcept; - // Problem: accumulate multiple errors and provide full diagnostic - // Problem: diagnostic should include path - // Problem: diagnostic can include warnings on happy path - template - [[nodiscard]] bool read(T& dst) const + template + [[nodiscard]] bool read(Type& dst) const { - return read(dst, *this); + return detail::dispatch_read(dst, *this); } // Intended to be called once the type is known (i.e. in a switch statement over type). // In debug: asserts on type mismatch. - // In release: same behavior as equivalent lua C API. + // In release: same behavior as the equivalent Lua C API. [[nodiscard]] bool to_boolean() const noexcept; - [[nodiscard]] f64 to_number() const noexcept; - [[nodiscard]] i64 to_integer() const noexcept; - [[nodiscard]] std::string_view to_string() const noexcept; - [[nodiscard]] Table to_table() const noexcept; + [[nodiscard]] Number to_number() const noexcept; + [[nodiscard]] Integer to_integer() const noexcept; + [[nodiscard]] std::string_view to_string() const noexcept; // valid while this value is alive + [[nodiscard]] Table to_table() const; + // Print a diagnostic and return nothing if the value is not of the expected type. [[nodiscard]] std::optional expect_boolean() const; [[nodiscard]] std::optional expect_integer() const; [[nodiscard]] std::optional expect_integer_range(Integer min, Integer max) const; @@ -60,7 +72,7 @@ namespace Lua { [[nodiscard]] std::optional expect_string() const; [[nodiscard]] std::optional
expect_table() const; - [[nodiscard]] Type type() const noexcept { return _reference.type(); } + [[nodiscard]] Type type() const noexcept { return reference_.type(); } [[nodiscard]] bool is_nil() const noexcept { return type() == Type::Nil; } @@ -68,50 +80,55 @@ namespace Lua { [[nodiscard]] bool is_integer() const noexcept { return type() == Type::Integer; } + // Note: integers are distinguished from other numbers; `is_number` is false for integers. [[nodiscard]] bool is_number() const noexcept { return type() == Type::Number; } [[nodiscard]] bool is_string() const noexcept { return type() == Type::String; } [[nodiscard]] bool is_table() const noexcept { return type() == Type::Table; } + // Describes the type and, for scalar types, the value (i.e. `integer 42`). [[nodiscard]] std::string describe() const; - [[nodiscard]] std::string_view path() const { return _path; } + [[nodiscard]] std::string_view path() const noexcept { return path_; } + + [[nodiscard]] Reference const& reference() const noexcept { return reference_; } private: - friend Table; - Reference _reference; - std::string _path; - }; + // Prints a type mismatch diagnostic. + void report(std::string_view expected) const; - bool read(bool& dst, Value const& value); - bool read(Integer& dst, Value const& value); - bool read(Number& dst, Value const& value); - bool read(std::string& dst, Value const& value); + Reference reference_; + std::string path_; + }; - template - bool read(T& dst, Value const& src) + template + bool read(Type& dst, Value const& src) { - auto integer - = src.expect_integer_range(std::numeric_limits::lowest(), std::numeric_limits::max()); + // Clamp the destination type's range to the range representable by a Lua integer. + constexpr Integer min + = std::cmp_less(std::numeric_limits::lowest(), std::numeric_limits::lowest()) + ? std::numeric_limits::lowest() + : static_cast(std::numeric_limits::lowest()); + constexpr Integer max + = std::cmp_greater(std::numeric_limits::max(), std::numeric_limits::max()) + ? std::numeric_limits::max() + : static_cast(std::numeric_limits::max()); + std::optional integer = src.expect_integer_range(min, max); if (!integer) return false; - dst = static_cast(*integer); + dst = static_cast(*integer); return true; } - template - bool read(T& dst, Value const& src) + template + bool read(Type& dst, Value const& src) { - auto number - = src.expect_number_range(std::numeric_limits::lowest(), std::numeric_limits::max()); + std::optional number = src.expect_number_range( + std::numeric_limits::lowest(), std::numeric_limits::max()); if (!number) return false; - dst = static_cast(*number); + dst = static_cast(*number); return true; } - bool read(Rectangle& rect, Value const& src); - bool read(Color& color, Value const& src); - } // namespace Lua - diff --git a/app/include/app/pilot.hh b/app/include/app/pilot.hh index e173df8..8aff9f0 100644 --- a/app/include/app/pilot.hh +++ b/app/include/app/pilot.hh @@ -2,13 +2,18 @@ #include "game/entity.hh" union SDL_Event; -struct CommandBuffer; -// Generates commands to pass to Actors -struct Pilot -{ - virtual void handle_event(SDL_Event const& event) = 0; - virtual void steer(World const&, CommandBuffer&, Entity e) = 0; - virtual ~Pilot() = default; -}; +namespace Game { + struct CommandBuffer; + struct World; +} +namespace App { + // Generates commands on behalf of an entity. + struct Pilot + { + virtual void handle_event(SDL_Event const& event) = 0; + virtual void steer(Game::World const&, Game::CommandBuffer&, Game::Entity entity) = 0; + virtual ~Pilot() = default; + }; +} diff --git a/app/include/app/sprite.hh b/app/include/app/sprite.hh index 16e24e4..e5d9420 100644 --- a/app/include/app/sprite.hh +++ b/app/include/app/sprite.hh @@ -1,15 +1,18 @@ #pragma once +#include "core/types.hh" #include "gfx/color.hh" #include "gfx/rectangle.hh" struct Texture; -// TODO: make this a user-interface implementation detail -constexpr f32 pixels_per_unit = 16.f; +namespace App { + // TODO: make this a user-interface implementation detail + constexpr f32 pixels_per_unit = 16.f; -struct Sprite -{ - Handle atlas; - Rectangle source; - Color tint; -}; + struct Sprite + { + Handle atlas; + Rectangle source; + Color tint; + }; +} diff --git a/app/include/app/state.hh b/app/include/app/state.hh index 9c22e10..0b6922a 100644 --- a/app/include/app/state.hh +++ b/app/include/app/state.hh @@ -1,26 +1,24 @@ #pragma once +#include "app/content.hh" #include "app/pilot.hh" -#include "core/basic-catalog.hh" #include "core/result.hh" -#include "game/world.hh" +#include "game/entity.hh" #include "gfx/camera2D.hh" #include "gfx/renderer.hh" -#include "gfx/texture.hh" #include "sys/window.hh" #include -#include union SDL_Event; -struct SDL_Texture; -struct CommandBuffer; -namespace App { - using Assets = TypeList; - using Catalog = BasicCatalog; +namespace Game { + struct CommandBuffer; + struct World; +} +namespace App { struct Player { - Entity entity; + Game::Entity entity = Game::Entity::Nil; std::unique_ptr pilot; Camera2D camera; }; @@ -33,16 +31,20 @@ namespace App { State& operator=(State&&) noexcept = default; State& operator=(State const&) = delete; - Result load(World&, Entity player); - void handle_event(SDL_Event const&); - void step(World const&, CommandBuffer&); - void update(World const&, f32 delta); - void render(World const&, f32 alpha); + // Opens the window and loads content into the world. + [[nodiscard]] Result load(Game::World&, Game::Entity player); + void handle_event(SDL_Event const&); + void step(Game::World const&, Game::CommandBuffer&); + void update(Game::World const&, f32 delta); + void render(Game::World const&, f32 alpha); private: - Player _player; - Window _window; - Renderer _renderer; - Catalog _textures; + + // Declaration order matters: visuals must be destroyed before the renderer, and the renderer + // before the window. + Player player_; + Window window_; + Renderer renderer_; + Visuals visuals_; }; } diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 55ebe0e..9822ff7 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -1 +1,2 @@ -target_sources (app PRIVATE application.cc cue.cc keyboard.cc) +add_subdirectory (lua) +target_sources (app PRIVATE content.cc cue.cc keyboard-pilot.cc state.cc) diff --git a/app/src/application.cc b/app/src/application.cc deleted file mode 100644 index ac1b6cd..0000000 --- a/app/src/application.cc +++ /dev/null @@ -1,142 +0,0 @@ -#include "app/application.hh" -#include "app/keyboard.hh" -#include "core/panic.hh" -#include "game/catalog.hh" -#include "game/chunk.hh" -#include "game/command-buffer.hh" -#include "game/component/pose.hh" -#include "game/context.hh" -#include "game/entity.hh" -#include "game/scene.hh" -#include "game/texture.hh" -#include "game/types.hh" -#include "gfx/projection2D.hh" -#include "gfx/rectangle.hh" -#include "gfx/renderer.hh" -#include "gfx/surface.hh" -#include "sys/event.hh" -#include -#include -#include - -// void scene_present(float zoom) -// { -// SDL_SetRenderTarget(renderer, NULL); -// SDL_SetRenderDrawColor(renderer, background.r, background.g, background.b, background.a); -// SDL_RenderClear(renderer); - -// // Sample a centred sub-rectangle of size `window / zoom` and stretch it to -// // fill the window: that stretch is the zoom. -// SDL_FRect src = { -// (scene_px.x - window_px.x / zoom) * 0.5f, -// (scene_px.y - window_px.y / zoom) * 0.5f, -// window_px.x / zoom, -// window_px.y / zoom, -// }; -// SDL_FRect dst = {0.f, 0.f, window_px.x, window_px.y}; -// SDL_RenderTexture(renderer, scene_target, &src, &dst); -// } -// scene_px = glm::ceil(window_px / config.min_zoom); -// constexpr SDL_Color background = {73, 49, 62, SDL_ALPHA_OPAQUE}; - -Result Application::load(LoadContext ctx, Entity player) -{ - PRECONDITION(player != Entity::Nil); - player_ = { - .entity = player, - .pilot = std::make_unique(), - .camera = { - .position = { 0.f, 0.f }, - .zoom = 1.3f, - }, - }; - { - auto result = create_window("Game", 800, 600); - if (!result) return std::unexpected(result.error()); - window_ = std::move(*result); - } - { - auto result = create_renderer(window_); - if (!result) return std::unexpected(result.error()); - renderer_ = std::move(*result); - } - auto catalog = access_catalog(ctx); - textures_.reserve(catalog.count()); - for (auto& asset : catalog.each()) { - // TODO: reuse the same surface, don't recreate a surface every time - auto surface = load_image(asset.path); - if (!surface) return std::unexpected(surface.error()); - auto texture = renderer_.create_texture(*surface); - if (!texture) return std::unexpected(texture.error()); - textures_.push_back(std::move(*texture)); - } - return {}; -} - -void Application::handle_event(SDL_Event const& event) -{ - switch (event.type) { - case SDL_EVENT_KEY_DOWN: - switch (event.key.scancode) { - case SDL_SCANCODE_ESCAPE: - case SDL_SCANCODE_Q: push_event(make_quit_event()); return; - default: break; - } - break; - case SDL_EVENT_MOUSE_WHEEL: - player_.camera.zoom = glm::clamp(player_.camera.zoom + 0.5f * event.wheel.y, 0.7f, 1.9f); - return; - default: break; - } - - if (player_.pilot) player_.pilot->handle_event(event); -} - -void Application::step(CommandBuffer& cmds, ConstContext ctx) -{ - if (player_.pilot) player_.pilot->steer(cmds, ctx, player_.entity); - auto h = find_entity(ctx, player_.entity); - if (!h) return; - auto e = access_entity(ctx, h); - auto pose = e.try_get(); - if (pose) player_.camera.position = pose->position; -} - -void Application::update(ConstContext, f32) {} - -void Application::render(ConstContext ctx, f32) -{ - constexpr Color background = { 73, 49, 62, SDL_ALPHA_OPAQUE }; - Projection2D const project{ - .extent = window_.size(), - .pixels_per_unit = pixels_per_unit, - }; - Rectangle const bounds = project.clip(player_.camera); - - renderer_.clear(background); - - auto catalog = access_catalog(ctx); - - glm::ivec2 lo = glm::ivec2(glm::floor(bounds.min())); - glm::ivec2 hi = glm::ivec2(glm::ceil(bounds.max())); - - lo = glm::max(lo, glm::ivec2{ 0 }); - hi = glm::min(hi, glm::ivec2{ chunk_size }); - - for (i32 x = lo.x; x < hi.x; ++x) { - for (i32 y = lo.y; y < hi.y; ++y) { - Tile const* tile = tile_at(ctx, u32(x), u32(y)); - if (!tile) continue; - auto pixel = project.to_screen_space(player_.camera, { x, y }); - auto& sprite = catalog[tile->terrain].sprite; - DEBUG_ASSERT(sprite.atlas.value < textures_.size()); - auto& texture = textures_[sprite.atlas.value]; - if (!texture) continue; - auto scale = player_.camera.zoom * pixels_per_unit; - Rectangle dst{ pixel, { scale, scale } }; - renderer_.draw_texture(texture, &sprite.source, &dst, &sprite.tint); - } - } - renderer_.present(); -} - diff --git a/app/src/content.cc b/app/src/content.cc index 98a4af9..64e7eb1 100644 --- a/app/src/content.cc +++ b/app/src/content.cc @@ -1,36 +1,159 @@ -#include "game/terrain.hh" -#include "sdk/content.hh" -#include "sdk/table.hh" +#include "app/content.hh" +#include "app/lua/state.hh" +#include "app/lua/table.hh" +#include "app/lua/value.hh" +#include "core/panic.hh" +#include "game/world.hh" +#include "gfx/renderer.hh" +#include "gfx/surface.hh" +#include +#include -namespace sdk { +namespace Lua { + // Reads an array of the form `{ x, y, width, height }`. + static bool read(Rectangle& dst, Value const& src) + { + std::optional
table = src.expect_table(); + if (!table) return false; + + bool ok = true; + ok &= (*table)[1].read(dst.origin.x); + ok &= (*table)[2].read(dst.origin.y); + ok &= (*table)[3].read(dst.extent.x); + ok &= (*table)[4].read(dst.extent.y); + return ok; + } + + // Reads either a hex integer of the form `0xRRGGBBAA` or a table of the form `{ r, g, b, a }`. + static bool read(Color& dst, Value const& src) + { + switch (src.type()) { + case Type::Integer: + { + u32 rgba = 0u; + if (!src.read(rgba)) return false; + dst = make_color_hex(rgba); + return true; + } + case Type::Table: + { + Table const table = src.to_table(); + + bool ok = true; + ok &= table["r"].read(dst.r); + ok &= table["g"].read(dst.g); + ok &= table["b"].read(dst.b); + ok &= table["a"].read(dst.a); + return ok; + } + default: + std::println("{}: expected integer or table, found {}", src.path(), src.describe()); + return false; + } + } +} + +namespace App { + namespace { + struct Loader + { + Game::World& world; + Renderer& renderer; + Visuals& visuals; + }; + } - static bool import_terrain(Content& content, Lua::Table const& root) + // Finds the texture loaded from the path, loading it if it hasn't been already. + static std::optional> import_texture(Loader& loader, Lua::Value const& value) { - bool ok = true; - Terrain terrain; - for (auto key : root.keys()) { - if (key.is_index()) { - ok &= false; - std::println( - "{}[{}]: terrain entries must be keyed by string", root.path(), key.as_index()); + std::optional path = value.expect_string(); + if (!path) return std::nullopt; + + Handle const handle = loader.visuals.assets.find(*path); + if (handle) return handle; + + Result surface = load_image(*path); + if (!surface) { + std::println("{}: {}", value.path(), surface.error()); + return std::nullopt; + } + Result texture = loader.renderer.create_texture(*surface); + if (!texture) { + std::println("{}: {}", value.path(), texture.error()); + return std::nullopt; + } + return loader.visuals.assets.emplace(*path, std::move(*texture)); + } + + static bool import_sprite(Loader& loader, Lua::Value const& value, Sprite& sprite) + { + std::optional table = value.expect_table(); + if (!table) return false; + + bool ok = true; + if (std::optional> atlas = import_texture(loader, (*table)["atlas"])) { + sprite.atlas = *atlas; + } else ok = false; + ok &= (*table)["source"].read(sprite.source); + ok &= (*table)["tint"].read(sprite.tint); + return ok; + } + + static bool import_terrain(Loader& loader, Lua::Table const& definitions) + { + bool ok = true; + for (Lua::Key const key : definitions.keys()) { + Lua::Value const value = definitions[key]; + if (!key.is_name()) { + std::println("{}: terrain must be labeled by a string", value.path()); + ok = false; continue; } - auto record = root[key].expect_table(); - if (!record) { - ok &= false; + std::optional definition = value.expect_table(); + if (!definition) { + ok = false; continue; } - ok &= import(content, terrain, *record); - define(content, key.as_name(), terrain); + Game::Terrain terrain; + Sprite sprite; + if (!import_sprite(loader, (*definition)["sprite"], sprite)) { + ok = false; + continue; + } + + Handle const handle + = loader.world.content.emplace(key.as_name(), terrain); + INVARIANT( + handle.index == loader.visuals.terrain.size(), + "terrain visuals must be parallel to terrain definitions"); + loader.visuals.terrain.push_back(sprite); } return ok; } - bool import(Content& content, Lua::Table const& root) + bool load_content(std::string_view path, Game::World& world, Renderer& renderer, Visuals& visuals) { - if (auto table = root["terrain"].expect_table()) import_terrain(content, *table); - } + std::optional lua = Lua::State::create(); + if (!lua) return false; + + Lua::Table const globals = lua->globals(); + { + Lua::Table const content = lua->create_table(); + content.set("terrain", lua->create_table()); + globals.set("content", content); + } + + if (!lua->load(path)) return false; + // Read back through the globals in case the script replaced the tables. + std::optional content = globals["content"].expect_table(); + if (!content) return false; + std::optional terrain = (*content)["terrain"].expect_table(); + if (!terrain) return false; + + Loader loader{ .world = world, .renderer = renderer, .visuals = visuals }; + return import_terrain(loader, *terrain); + } } diff --git a/app/src/keyboard-pilot.cc b/app/src/keyboard-pilot.cc new file mode 100644 index 0000000..d6a207e --- /dev/null +++ b/app/src/keyboard-pilot.cc @@ -0,0 +1,24 @@ +#include "app/keyboard-pilot.hh" +#include "game/command-buffer.hh" +#include "game/command.hh" +#include + +namespace App { + void KeyboardPilot::handle_event(SDL_Event const& event) + { + mouse_.event(event); + keyboard_.event(event); + } + + void KeyboardPilot::steer(Game::World const&, Game::CommandBuffer& out, Game::Entity entity) + { + f32 x = 0.f, y = 0.f; + if (keyboard_[SDL_SCANCODE_W]) y -= 0.1f; + if (keyboard_[SDL_SCANCODE_S]) y += 0.1f; + if (keyboard_[SDL_SCANCODE_A]) x -= 0.1f; + if (keyboard_[SDL_SCANCODE_D]) x += 0.1f; + if (x != 0.f || y != 0.f) out.post(Game::make_command_move(entity, x, y)); + mouse_.flush(); + keyboard_.flush(); + } +} diff --git a/app/src/keyboard.cc b/app/src/keyboard.cc deleted file mode 100644 index ecec756..0000000 --- a/app/src/keyboard.cc +++ /dev/null @@ -1,23 +0,0 @@ -#include "app/keyboard.hh" -#include "game/command-buffer.hh" -#include "game/command.hh" -#include "game/context.hh" -#include - -void KeyboardPilot::handle_event(SDL_Event const& event) -{ - mouse.event(event); - keyboard.event(event); -} - -void KeyboardPilot::steer(CommandBuffer& out, ConstContext, Entity e) -{ - f32 x = 0.f, y = 0.f; - if (keyboard[SDL_SCANCODE_W]) y -= 0.1f; - if (keyboard[SDL_SCANCODE_S]) y += 0.1f; - if (keyboard[SDL_SCANCODE_A]) x -= 0.1f; - if (keyboard[SDL_SCANCODE_D]) x += 0.1f; - if (x != 0.f || y != 0.f) out.post(make_command_move(e, x, y)); - mouse.flush(); - keyboard.flush(); -} diff --git a/app/src/load.cc b/app/src/load.cc deleted file mode 100644 index 9154f97..0000000 --- a/app/src/load.cc +++ /dev/null @@ -1,59 +0,0 @@ -#include "game/content.hh" -#include "game/context.hh" -#include "game/sprite.hh" -#include "game/terrain.hh" -#include "game/types.hh" -#include "sdk/node.hh" -#include "sdk/state.hh" -#include "sdk/table.hh" -#include "sdk/value.hh" -#include - -static bool compile_terrain(Lua::Table& table, CatalogWriter catalog) -{ - bool ok = true; - Terrain terrain; - for (auto key : table.keys()) { - terrain = {}; - if (key.is_index()) continue; - ok = table[key].read(terrain); - catalog.emplace(key.as_name(), terrain); - } - return ok; -} - -static void init_lua(Lua::State& lua) -{ - Lua::Table const G = lua.globals(); - Lua::Table const content = lua.create_table(); - - G.set("content", content); - content.set("terrain", lua.create_table()); -}; - -bool load_content(Content&, LuaTable) -{ - auto lua = Lua::State::create(); - if (!lua) return false; - init_lua(*lua); - - if (!lua->load("content/terrain.lua")) return false; - - Lua::Table const G = lua->globals(); - - auto content_table = G["content"].expect_table(); - if (!content_table) return false; - - Content content; - content_table-> - - std::println("Failed to compile content!"); - return read() - - return compile_content(*content, access_catalog(context)); -} - -// upvalues? requires parsing to be on call: -// type label {...} -// DTOs? -// extra space? diff --git a/app/src/lua/CMakeLists.txt b/app/src/lua/CMakeLists.txt index 60eb3cf..a880c84 100644 --- a/app/src/lua/CMakeLists.txt +++ b/app/src/lua/CMakeLists.txt @@ -1 +1 @@ -target_sources (sdk PRIVATE table.cc value.cc reference.cc) +target_sources (app PRIVATE reference.cc state.cc table.cc value.cc) diff --git a/app/src/lua/reference.cc b/app/src/lua/reference.cc index ee08167..ca6e728 100644 --- a/app/src/lua/reference.cc +++ b/app/src/lua/reference.cc @@ -1,94 +1,92 @@ +#include "app/lua/reference.hh" #include "core/panic.hh" -#include "sdk/reference.hh" -#include "sdk/state.hh" #include namespace Lua { - static Type get_type(lua_State* L, int idx) noexcept + static Type type_at(lua_State* L, int idx) noexcept { if (lua_isinteger(L, idx)) return Type::Integer; - return static_cast(idx); + return static_cast(lua_type(L, idx)); } - Reference::Reference(State* state, i32 ridx) noexcept - : _state{ state }, _ridx{ ridx }, _type{ Type::None } + Reference Reference::pop(lua_State* L) noexcept { - PRECONDITION(_state != nullptr); - lua_State* const L = _state->handle(); - lua_rawgeti(L, LUA_REGISTRYINDEX, _ridx); - _type = get_type(L, -1); - lua_pop(L, 1); + static_assert(no_ref == LUA_NOREF); + PRECONDITION(L != nullptr); + Type const type = type_at(L, -1); + int const ridx = luaL_ref(L, LUA_REGISTRYINDEX); + return Reference(L, ridx, type); } - Reference::Reference(Reference const& other) noexcept - : _state{ other._state }, _ridx{ LUA_NOREF }, _type{ Type::None } + Reference::Reference(lua_State* L, int ridx, Type type) noexcept + : state_{ L }, ridx_{ ridx }, type_{ type } + {} + + Reference::Reference(Reference const& other) noexcept : type_{ other.type_ } { - lua_State* const L = _state->handle(); - lua_rawgeti(L, LUA_REGISTRYINDEX, other._ridx); - _type = get_type(L, -1); - _ridx = luaL_ref(L, LUA_REGISTRYINDEX); + if (other.state_ == nullptr) return; + state_ = other.state_; + lua_rawgeti(state_, LUA_REGISTRYINDEX, other.ridx_); + ridx_ = luaL_ref(state_, LUA_REGISTRYINDEX); } Reference::Reference(Reference&& other) noexcept - : _state{ other._state }, _ridx{ other._ridx }, _type{ other._type } + : state_{ other.state_ }, ridx_{ other.ridx_ }, type_{ other.type_ } { - other._ridx = LUA_NOREF; - other._type = Type::None; + other.state_ = nullptr; + other.ridx_ = no_ref; + other.type_ = Type::None; } Reference& Reference::operator=(Reference const& other) noexcept { if (&other == this) return *this; - if (_ridx != LUA_NOREF) luaL_unref(_state->handle(), LUA_REGISTRYINDEX, _ridx); - _state = other._state; - lua_State* const L = _state->handle(); - lua_rawgeti(L, LUA_REGISTRYINDEX, other._ridx); - _type = get_type(L, -1); - _ridx = luaL_ref(L, LUA_REGISTRYINDEX); - return *this; + Reference copy(other); + return *this = std::move(copy); } Reference& Reference::operator=(Reference&& other) noexcept { if (&other == this) return *this; - if (_ridx != LUA_NOREF) luaL_unref(_state->handle(), LUA_REGISTRYINDEX, _ridx); - _state = other._state; - _ridx = other._ridx; - _type = other._type; - other._ridx = LUA_NOREF; - other._type = Type::None; + release(); + state_ = other.state_; + ridx_ = other.ridx_; + type_ = other.type_; + other.state_ = nullptr; + other.ridx_ = no_ref; + other.type_ = Type::None; return *this; } - Reference::~Reference() noexcept + Reference::~Reference() noexcept { release(); } + + void Reference::release() noexcept { - lua_State* const L = _state->handle(); - if (_ridx != LUA_NOREF) { - luaL_unref(L, LUA_REGISTRYINDEX, _ridx); - } + if (state_ != nullptr && ridx_ != LUA_NOREF) luaL_unref(state_, LUA_REGISTRYINDEX, ridx_); + state_ = nullptr; + ridx_ = no_ref; + type_ = Type::None; } bool operator==(Reference const& l, Reference const& r) noexcept { - if (l._state != r._state) return false; - lua_State* const L = l._state->handle(); - if (L == nullptr) return true; - if (l._ridx == r._ridx) return true; - lua_rawgeti(L, LUA_REGISTRYINDEX, l._ridx); - lua_rawgeti(L, LUA_REGISTRYINDEX, r._ridx); - bool out = lua_rawequal(L, -1, -2); + if (l.state_ != r.state_) return false; + if (l.state_ == nullptr || l.ridx_ == r.ridx_) return true; + lua_State* const L = l.state_; + lua_rawgeti(L, LUA_REGISTRYINDEX, l.ridx_); + lua_rawgeti(L, LUA_REGISTRYINDEX, r.ridx_); + bool const equal = lua_rawequal(L, -1, -2); lua_pop(L, 2); - return out; + return equal; } int Reference::push() const noexcept { - lua_State* const L = _state->handle(); - DEBUG_ASSERT(_ridx != LUA_NOREF); - lua_rawgeti(L, LUA_REGISTRYINDEX, _ridx); - DEBUG_ASSERT(_type == get_type(L, -1)); - return lua_gettop(L); + PRECONDITION(state_ != nullptr, "pushed an empty reference"); + lua_rawgeti(state_, LUA_REGISTRYINDEX, ridx_); + DEBUG_ASSERT(type_ == type_at(state_, -1)); + return lua_gettop(state_); } } // namespace Lua diff --git a/app/src/lua/state.cc b/app/src/lua/state.cc index 3e331b6..450d8a0 100644 --- a/app/src/lua/state.cc +++ b/app/src/lua/state.cc @@ -1,15 +1,12 @@ +#include "app/lua/state.hh" #include "core/defer.hh" #include "core/panic.hh" -#include "sdk/reference.hh" -#include "sdk/state.hh" -#include -#include -#include #include +#include #include -#include namespace Lua { + static int traceback(lua_State* L) { char const* message = lua_tostring(L, 1); @@ -44,74 +41,91 @@ namespace Lua { } } - // Ignores meta-methods in the global table. - static void push_global(lua_State* L, std::string_view name) noexcept - { - lua_pushglobaltable(L); - lua_pushlstring(L, name.data(), name.size()); - lua_rawget(L, -2); - lua_replace(L, -2); - } - - static std::string pop_string(lua_State* L) + // Prints and pops the error message on the top of the stack. + static void report_error(lua_State* L) { - size_t length = 0; - char const* data = lua_tolstring(L, -1, &length); - std::string string(data, length); + size_t length = 0; + char const* data = lua_tolstring(L, -1, &length); + std::string_view message = data != nullptr ? std::string_view(data, length) + : std::string_view("(error object is not a string)"); + std::println("{}", message); lua_pop(L, 1); - return string; } - static Status call(lua_State* L) + // Calls the function on the top of the stack. + static bool call(lua_State* L) { + int const function = lua_gettop(L); lua_pushcfunction(L, traceback); - auto errh = lua_gettop(L); - DEFER(lua_remove(L, errh)); - int const status = lua_pcall(L, 0, 0, errh); - if (status != LUA_OK) return Error(pop_string(L)); - return {}; + lua_insert(L, function); // place the message handler beneath the function + DEFER(lua_remove(L, function)); + if (lua_pcall(L, 0, 0, function) == LUA_OK) return true; + report_error(L); + return false; } - Result State::create() + std::optional State::create() { lua_State* L = luaL_newstate(); - if (L == nullptr) return Error("failed to create Lua state"); + if (L == nullptr) { + std::println("failed to create Lua state"); + return std::nullopt; + } open_libraries(L); return State(L); } - State::State(State&& other) noexcept : _handle{ other._handle } { other._handle = nullptr; } + State::State(lua_State* L) noexcept : handle_{ L } {} + + State::State(State&& other) noexcept : handle_{ other.handle_ } { other.handle_ = nullptr; } State& State::operator=(State&& other) noexcept { if (&other == this) return *this; - _handle = other._handle; - other._handle = nullptr; + if (handle_ != nullptr) lua_close(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; return *this; } - State::~State() noexcept { lua_close(_handle); } + State::~State() noexcept + { + if (handle_ != nullptr) lua_close(handle_); + } - Table State::globals() noexcept { return Table(_handle, LUA_RIDX_GLOBALS); } + Table State::globals() const + { + PRECONDITION(handle_ != nullptr); + lua_rawgeti(handle_, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); + return Table(Reference::pop(handle_), {}); + } - Table State::create_table(LuaPath const& path) noexcept + Table State::create_table(std::string path) const { - lua_newtable(_handle); - return Table(_handle, luaL_ref(_handle, LUA_REGISTRYINDEX)); + PRECONDITION(handle_ != nullptr); + lua_newtable(handle_); + return Table(Reference::pop(handle_), std::move(path)); } - Status State::load(std::string_view path) + bool State::load(std::string_view path) { - auto status = luaL_loadfile(_handle, std::string(path).c_str()); - if (status != LUA_OK) return Error(pop_string(_handle)); - return call(_handle); + PRECONDITION(handle_ != nullptr); + if (luaL_loadfile(handle_, std::string(path).c_str()) != LUA_OK) { + report_error(handle_); + return false; + } + return call(handle_); } - Status State::execute(std::string_view name, std::string_view source) + bool State::execute(std::string_view name, std::string_view source) { - auto status = luaL_loadbuffer(_handle, source.data(), source.size(), std::string(name).c_str()); - if (status != LUA_OK) return Error(pop_string(_handle)); - return call(_handle); + PRECONDITION(handle_ != nullptr); + std::string const chunk_name = "=" + std::string(name); + if (luaL_loadbuffer(handle_, source.data(), source.size(), chunk_name.c_str()) != LUA_OK) { + report_error(handle_); + return false; + } + return call(handle_); } } // namespace Lua diff --git a/app/src/lua/table.cc b/app/src/lua/table.cc index 1def4c2..0464b54 100644 --- a/app/src/lua/table.cc +++ b/app/src/lua/table.cc @@ -1,98 +1,132 @@ +#include "app/lua/table.hh" #include "core/panic.hh" -#include "sdk/state.hh" -#include "sdk/table.hh" -#include +#include +#include #include -#include +#include namespace Lua { + static void push_key(lua_State* L, Key key) noexcept { if (key.is_name()) { - auto str = key.as_name(); - lua_pushlstring(L, str.data(), str.size()); + std::string_view const name = key.as_name(); + lua_pushlstring(L, name.data(), name.size()); } else lua_pushinteger(L, key.as_index()); } - Table::Table(Reference reference) noexcept : Reference(std::move(reference)) + static bool is_identifier(std::string_view name) noexcept { - DEBUG_ASSERT(reference.type() == Type::Table); + auto const is_alpha + = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; }; + auto const is_alnum = [&](char c) { return is_alpha(c) || (c >= '0' && c <= '9'); }; + return !name.empty() && is_alpha(name.front()) && std::ranges::all_of(name, is_alnum); } - void Table::set(Key key, bool value) const noexcept + // Formats the path of a field the way it would be written in Lua (i.e. `a.b["c-d"][1]`). + static std::string field_path(std::string_view parent, Key key) { - lua_State* const L = state(); - int const idx = push(); - push_key(L, key); - lua_pushboolean(L, value); - lua_rawset(L, idx); - lua_pop(L, 1); + if (key.is_index()) return std::format("{}[{}]", parent, key.as_index()); + std::string_view const name = key.as_name(); + if (!is_identifier(name)) return std::format("{}[\"{}\"]", parent, name); + if (parent.empty()) return std::string(name); + return std::format("{}.{}", parent, name); } - void Table::set(Key key, f64 value) const noexcept + // Assigns the value pushed by the callable to the key, bypassing metamethods. + template + static void raw_set(Reference const& table, Key key, PushValue push_value) noexcept { - lua_State* const L = state(); - int const idx = push(); + lua_State* const L = table.state(); + int const idx = table.push(); push_key(L, key); - lua_pushnumber(L, value); + push_value(L); lua_rawset(L, idx); lua_pop(L, 1); } - void Table::set(Key key, i64 value) const noexcept + Table::Table(Reference reference, std::string path) noexcept + : reference_(std::move(reference)), path_(std::move(path)) { - lua_State* const L = state(); - int const idx = push(); - push_key(L, key); - lua_pushinteger(L, value); - lua_rawset(L, idx); - lua_pop(L, 1); + PRECONDITION(reference_.type() == Type::Table); + } + + void Table::set(Key key, bool value) const noexcept + { + raw_set(reference_, key, [value](lua_State* L) { lua_pushboolean(L, value); }); + } + + void Table::set(Key key, Number value) const noexcept + { + raw_set(reference_, key, [value](lua_State* L) { lua_pushnumber(L, value); }); + } + + void Table::set(Key key, Integer value) const noexcept + { + raw_set(reference_, key, [value](lua_State* L) { lua_pushinteger(L, value); }); } void Table::set(Key key, std::string_view value) const noexcept { - lua_State* const L = state(); - int const idx = push(); - push_key(L, key); - lua_pushlstring(L, value.data(), value.size()); - lua_rawset(L, idx); - lua_pop(L, 1); + raw_set( + reference_, key, [value](lua_State* L) { lua_pushlstring(L, value.data(), value.size()); }); } void Table::set(Key key, Reference const& value) const noexcept { - PRECONDITION(value.state() == state()); - lua_State* const L = state(); - int const idx = push(); - push_key(L, key); - value.push(); - lua_rawset(L, idx); - lua_pop(L, 1); + PRECONDITION(value.state() == reference_.state(), "value belongs to a different state"); + raw_set(reference_, key, [&value](lua_State*) { value.push(); }); } void Table::erase(Key key) const noexcept { - lua_State* const L = state().get(); - int const idx = push(); - push_key(L, key); - lua_pushnil(L); - lua_rawset(L, idx); - lua_pop(L, 1); + raw_set(reference_, key, [](lua_State* L) { lua_pushnil(L); }); } - Value Table::operator[](Key key) const noexcept + Value Table::operator[](Key key) const { - lua_State* const L = state().get(); - int const idx = push(); + lua_State* const L = reference_.state(); + int const idx = reference_.push(); push_key(L, key); lua_rawget(L, idx); - return Value({ &state(), luaL_ref(L, LUA_REGISTRYINDEX) }); + Reference field = Reference::pop(L); + lua_pop(L, 1); + return Value(std::move(field), field_path(path_, key)); } - Status Table::expect_table(Key key) const + std::vector Table::keys() const { - if (type() != Type::Table) return type_error(Type::Table, type()); - return {}; + std::vector indices; + std::vector names; + + lua_State* const L = reference_.state(); + int const idx = reference_.push(); + lua_pushnil(L); + while (lua_next(L, idx) != 0) { + // key at -2, value at -1 + if (lua_type(L, -2) == LUA_TSTRING) { + size_t length = 0; + char const* data = lua_tolstring(L, -2, &length); // does not convert; safe for lua_next + names.emplace_back(data, length); + } else if (lua_isinteger(L, -2)) { + indices.push_back(lua_tointeger(L, -2)); + } else { + std::println("{}: ignoring key of type {}", path_, luaL_typename(L, -2)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + std::ranges::sort(indices); + std::ranges::sort(names); + + std::vector keys; + keys.reserve(indices.size() + names.size()); + for (Integer index : indices) + keys.emplace_back(index); + for (std::string_view name : names) + keys.emplace_back(name); + return keys; } } // namespace Lua diff --git a/app/src/lua/types.cc b/app/src/lua/types.cc deleted file mode 100644 index a5c34a3..0000000 --- a/app/src/lua/types.cc +++ /dev/null @@ -1,38 +0,0 @@ -#include "sdk/types.hh" -#include -#include - -namespace Lua { - - static std::string list(std::initializer_list types) - { - auto it = types.begin(); - std::string out(type_name(*it)); - while (++it != types.end()) { - out += ", "; - out += type_name(*it); - } - return out; - } - - Error type_error(Type expected, Type found) - { - return Error{ std::format("expected {}, found {}", type_name(expected), type_name(found)) }; - } - - Error type_error(std::initializer_list expected, Type found) - { - return Error{ std::format("expected one of ({}), found {}", list(expected), type_name(found)) }; - } - - Error range_error(i64 min, i64 max, i64 found) - { - return Error{ std::format("expected integer in range [{}, {}], found {}", min, max, found) }; - } - - Error range_error(f64 min, f64 max, f64 found) - { - return Error{ std::format("expected number in range [{}, {}], found {}", min, max, found) }; - } - -} diff --git a/app/src/lua/value.cc b/app/src/lua/value.cc index 73a26c6..95ca0d2 100644 --- a/app/src/lua/value.cc +++ b/app/src/lua/value.cc @@ -1,165 +1,177 @@ -#include "core/color.hh" -#include "core/rectangle.hh" -#include "sdk/state.hh" -#include "sdk/table.hh" -#include "sdk/value.hh" +#include "app/lua/value.hh" +#include "app/lua/table.hh" +#include "core/panic.hh" +#include #include #include namespace Lua { - Value::Value(Handle handle) noexcept : Handle(std::move(handle)) {} + Value::Value(Reference reference, std::string path) noexcept + : reference_(std::move(reference)), path_(std::move(path)) + {} bool Value::to_boolean() const noexcept { - lua_State* const L = state(); - push(); - bool boolean = lua_toboolean(L, -1); + DEBUG_ASSERT(is_boolean()); + lua_State* const L = reference_.state(); + reference_.push(); + bool const boolean = lua_toboolean(L, -1); lua_pop(L, 1); return boolean; } - f64 Value::to_number() const noexcept + Number Value::to_number() const noexcept { - lua_State* const L = state(); - push(); - f64 number = lua_tonumber(L, -1); + DEBUG_ASSERT(is_number() || is_integer()); + lua_State* const L = reference_.state(); + reference_.push(); + Number const number = lua_tonumber(L, -1); lua_pop(L, 1); return number; } - i64 Value::to_integer() const noexcept + Integer Value::to_integer() const noexcept { - lua_State* const L = state(); - push(); - i64 integer = lua_tointeger(L, -1); + DEBUG_ASSERT(is_integer()); + lua_State* const L = reference_.state(); + reference_.push(); + Integer const integer = lua_tointeger(L, -1); lua_pop(L, 1); return integer; } - std::string Value::to_string() const + std::string_view Value::to_string() const noexcept { - lua_State* const L = state(); - push(); - std::string string = lua_tostring(L, -1); + DEBUG_ASSERT(is_string()); + lua_State* const L = reference_.state(); + reference_.push(); + size_t length = 0; + char const* data = lua_tolstring(L, -1, &length); lua_pop(L, 1); - return string; + // The registry reference keeps the string alive. + return std::string_view(data, length); } - Status Value::expect(Type expected) const + Table Value::to_table() const { - if (type() == expected) return {}; - Error e{ "expected " }; - e.error() += type_name(Type::Table); - e.error() += ", found "; - e.error() += describe(); - return e; + DEBUG_ASSERT(is_table()); + return Table(reference_, path_); + } + + void Value::report(std::string_view expected) const + { + std::println("{}: expected {}, found {}", path_, expected, describe()); + } + + std::optional Value::expect_boolean() const + { + if (is_boolean()) return to_boolean(); + report(type_name(Type::Boolean)); + return std::nullopt; + } + + std::optional Value::expect_integer() const + { + if (is_integer()) return to_integer(); + if (is_number()) { + // Accept floats with an exact integer representation (i.e. `2.0`). + lua_State* const L = reference_.state(); + reference_.push(); + int exact = 0; + Integer const integer = lua_tointegerx(L, -1, &exact); + lua_pop(L, 1); + if (exact) return integer; + } + report(type_name(Type::Integer)); + return std::nullopt; + } + + std::optional Value::expect_integer_range(Integer min, Integer max) const + { + std::optional integer = expect_integer(); + if (!integer) return std::nullopt; + if (*integer < min || *integer > max) { + std::println("{}: expected integer in range [{}, {}], found {}", path_, min, max, *integer); + return std::nullopt; + } + return integer; + } + + std::optional Value::expect_number() const + { + if (is_number() || is_integer()) return to_number(); + report(type_name(Type::Number)); + return std::nullopt; + } + + std::optional Value::expect_number_range(Number min, Number max) const + { + std::optional number = expect_number(); + if (!number) return std::nullopt; + if (!(*number >= min && *number <= max)) { + std::println("{}: expected number in range [{}, {}], found {}", path_, min, max, *number); + return std::nullopt; + } + return number; + } + + std::optional Value::expect_string() const + { + if (is_string()) return to_string(); + report(type_name(Type::String)); + return std::nullopt; + } + + std::optional
Value::expect_table() const + { + if (is_table()) return to_table(); + report(type_name(Type::Table)); + return std::nullopt; } std::string Value::describe() const { - std::string description{ type_name(type()) }; + std::string_view const name = type_name(type()); switch (type()) { - case Type::Boolean: description += (to_boolean() ? "true" : "false"); break; - case Type::Integer: description += std::to_string(to_integer()); break; - case Type::Number: description += std::to_string(to_number()); break; - case Type::String: - description += '"'; - description += to_string(); - description += '"'; - break; - default: break; + case Type::Boolean: return std::format("{} {}", name, to_boolean()); + case Type::Integer: return std::format("{} {}", name, to_integer()); + case Type::Number: return std::format("{} {}", name, to_number()); + case Type::String: return std::format("{} \"{}\"", name, to_string()); + default: return std::string(name); } - return description; } - bool read(Value const& src, bool& dst) + bool read(bool& dst, Value const& src) { - auto boolean = src.expect_boolean(); + std::optional boolean = src.expect_boolean(); if (!boolean) return false; dst = *boolean; return true; } - bool read(Value const& src, Integer& dst) + bool read(Integer& dst, Value const& src) { - auto integer = src.expect_integer(); + std::optional integer = src.expect_integer(); if (!integer) return false; dst = *integer; return true; } - bool read(Value const& src, Number& dst) + bool read(Number& dst, Value const& src) { - auto number = src.expect_number(); + std::optional number = src.expect_number(); if (!number) return false; dst = *number; return true; } - bool read(Value const& src, std::string& dst) + bool read(std::string& dst, Value const& src) { - auto string = src.expect_string(); + std::optional string = src.expect_string(); if (!string) return false; dst = std::string(*string); return true; } - bool read(Value const& value, Rectangle& rect) - { - Table table = ({ - auto table = value.expect_table(); - if (!table) return false; - std::move(*table); - }); - - bool ok = true; - ok &= table["x"].read(rect.origin.x); - ok &= table["y"].read(rect.origin.y); - ok &= table["w"].read(rect.extent.x); - ok &= table["h"].read(rect.extent.y); - return ok; - } - - bool read(Value const& value, Color& color) - { - switch (value.type()) { - case Lua::Type::Integer: - { - u32 rgba; - if (!value.read(rgba)) return false; - color = make_color_hex(rgba); - break; - } - case Lua::Type::Table: - { - Lua::Table table = value.to_table(); - - bool ok = true; - ok &= table["r"].read(color.r); - ok &= table["g"].read(color.g); - ok &= table["b"].read(color.b); - ok &= table["a"].read(color.a); - if (!ok) return false; - break; - } - default: - std::println("{}: expected integer or table, found {}", value.path(), value.describe()); - return false; - } - return true; - } - } // namespace Lua - -// Result Value::expect_bounded_integer(Integer min, Integer max) const {} - -// Result Value::expect_bounded_number(Number min, Number max) const -// { -// f64 number = TRY(expect_number()); -// if (number > max || number < min) { -// // return range_error_message( -// // src.type(), std::numeric_limits::lowest(), std::numeric_limits::max(), number); -// } -// } - diff --git a/app/src/sprite.cc b/app/src/sprite.cc deleted file mode 100644 index 3c04f3f..0000000 --- a/app/src/sprite.cc +++ /dev/null @@ -1,17 +0,0 @@ -#include "game/world.hh" -#include "sdk/lua/value.hh" - -bool read(World& world, Value const& value, Sprite& sprite) -{ - Table table = ({ - auto table = value.expect_table(); - if (!table) return false; - std::move(*table); - }); - - bool ok = true; - ok &= table["atlas"].read(sprite.atlas); - ok &= table["source"].read(sprite.source); - ok &= table["tint"].read(sprite.tint); - return ok; -} diff --git a/app/src/state.cc b/app/src/state.cc new file mode 100644 index 0000000..078147f --- /dev/null +++ b/app/src/state.cc @@ -0,0 +1,105 @@ +#include "app/state.hh" +#include "app/keyboard-pilot.hh" +#include "core/panic.hh" +#include "game/command-buffer.hh" +#include "game/world.hh" +#include "gfx/projection2D.hh" +#include "gfx/rectangle.hh" +#include "gfx/texture.hh" +#include "sys/event.hh" +#include + +namespace App { + Result State::load(Game::World& world, Game::Entity player) + { + PRECONDITION(player != Game::Entity::Nil); + player_ = { + .entity = player, + .pilot = std::make_unique(), + .camera = { + .position = { 0.f, 0.f }, + .zoom = 1.3f, + }, + }; + { + auto result = create_window("Game", 800, 600); + if (!result) return Error(std::move(result).error()); + window_ = std::move(*result); + } + { + auto result = create_renderer(window_); + if (!result) return Error(std::move(result).error()); + renderer_ = std::move(*result); + } + if (!load_content("content/terrain.lua", world, renderer_, visuals_)) { + return Error("failed to load content"); + } + return {}; + } + + void State::handle_event(SDL_Event const& event) + { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + switch (event.key.scancode) { + case SDL_SCANCODE_ESCAPE: + case SDL_SCANCODE_Q: push_event(make_quit_event()); return; + default: break; + } + break; + case SDL_EVENT_MOUSE_WHEEL: + player_.camera.zoom = glm::clamp(player_.camera.zoom + 0.5f * event.wheel.y, 0.7f, 1.9f); + return; + default: break; + } + + if (player_.pilot) player_.pilot->handle_event(event); + } + + void State::step(Game::World const& world, Game::CommandBuffer& cmds) + { + if (player_.pilot) player_.pilot->steer(world, cmds, player_.entity); + Handle const handle = world.find(player_.entity); + if (!handle) return; + if (auto pose = world.entities.try_get(handle)) { + player_.camera.position = pose->position; + } + } + + void State::update(Game::World const&, f32) {} + + void State::render(Game::World const& world, f32) + { + constexpr Color background = { 73, 49, 62, SDL_ALPHA_OPAQUE }; + Projection2D const project{ + .extent = window_.size(), + .pixels_per_unit = pixels_per_unit, + }; + Rectangle const bounds = project.clip(player_.camera); + + renderer_.clear(background); + + glm::ivec2 lo = glm::ivec2(glm::floor(bounds.min())); + glm::ivec2 hi = glm::ivec2(glm::ceil(bounds.max())); + + lo = glm::max(lo, glm::ivec2{ 0 }); + hi = glm::min(hi, glm::ivec2{ Game::chunk_size }); + + for (i32 x = lo.x; x < hi.x; ++x) { + for (i32 y = lo.y; y < hi.y; ++y) { + Game::Tile const* tile = world.environment.get(u32(x), u32(y)); + if (!tile) continue; + DEBUG_ASSERT(world.content.valid(tile->terrain)); + DEBUG_ASSERT(tile->terrain.index < visuals_.terrain.size()); + Sprite const& sprite = visuals_.terrain[tile->terrain.index]; + Texture& texture = visuals_.assets[sprite.atlas]; + if (!texture) continue; + auto const pixel = project.to_screen_space(player_.camera, { x, y }); + auto const scale = player_.camera.zoom * pixels_per_unit; + Rectangle const dst{ pixel, { scale, scale } }; + renderer_.draw_texture(texture, &sprite.source, &dst, &sprite.tint); + } + } + renderer_.present(); + } +} diff --git a/app/tests/CMakeLists.txt b/app/tests/CMakeLists.txt new file mode 100644 index 0000000..3856067 --- /dev/null +++ b/app/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +list (APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) + +add_executable (test-app) + +target_sources (test-app PRIVATE lua.cc) + +target_link_libraries ( + test-app + PRIVATE Catch2::Catch2WithMain + PRIVATE app +) + +include (CTest) +include (Catch) +catch_discover_tests (test-app) diff --git a/app/tests/lua.cc b/app/tests/lua.cc new file mode 100644 index 0000000..088c34e --- /dev/null +++ b/app/tests/lua.cc @@ -0,0 +1,200 @@ +#include "app/lua/state.hh" +#include "app/lua/table.hh" +#include "app/lua/value.hh" +#include +#include +#include +#include + +namespace { + struct Point + { + f32 x; + f32 y; + }; +} + +namespace Lua { + static bool read(Point& dst, Table const& src) + { + bool ok = true; + ok &= src["x"].read(dst.x); + ok &= src["y"].read(dst.y); + return ok; + } +} + +static Lua::State make_state(std::string_view source) +{ + std::optional state = Lua::State::create(); + REQUIRE(state.has_value()); + REQUIRE(state->execute("test", source)); + return std::move(*state); +} + +TEST_CASE("Lua values read scalar fields") +{ + Lua::State const state = make_state("content = { enabled = true, count = 42, name = 'grass' }"); + + std::optional content = state.globals()["content"].expect_table(); + REQUIRE(content.has_value()); + + bool enabled = false; + u16 count = 0; + std::string name; + CHECK((*content)["enabled"].read(enabled)); + CHECK((*content)["count"].read(count)); + CHECK((*content)["name"].read(name)); + CHECK(enabled == true); + CHECK(count == 42); + CHECK(name == "grass"); +} + +TEST_CASE("Lua values track their path") +{ + Lua::State const state = make_state("content = { sprite = { source = { 16, 32, 'wide', 12 } } }"); + + std::optional content = state.globals()["content"].expect_table(); + REQUIRE(content.has_value()); + std::optional sprite = (*content)["sprite"].expect_table(); + REQUIRE(sprite.has_value()); + std::optional source = (*sprite)["source"].expect_table(); + REQUIRE(source.has_value()); + CHECK(source->path() == "content.sprite.source"); + + Lua::Value const width = (*source)[3]; + CHECK(width.path() == "content.sprite.source[3]"); + CHECK(width.is_string()); + + f32 number = 0.f; + CHECK_FALSE(width.read(number)); + + Lua::Table const terrain = state.create_table("content.terrain"); + CHECK(terrain["grass-1"].path() == R"(content.terrain["grass-1"])"); +} + +TEST_CASE("Lua numbers accept integers") +{ + Lua::State const state = make_state("content = { integer = 16, float = 2.0, fraction = 2.5 }"); + + std::optional content = state.globals()["content"].expect_table(); + REQUIRE(content.has_value()); + + f32 number = 0.f; + CHECK((*content)["integer"].read(number)); + CHECK(number == 16.f); + + i32 integer = 0; + CHECK((*content)["float"].read(integer)); + CHECK(integer == 2); + CHECK_FALSE((*content)["fraction"].read(integer)); +} + +TEST_CASE("Lua integer reads reject narrowing") +{ + Lua::State const state = make_state("content = { value = 256, negative = -1 }"); + + std::optional content = state.globals()["content"].expect_table(); + REQUIRE(content.has_value()); + + u8 small = 0; + CHECK_FALSE((*content)["value"].read(small)); + CHECK_FALSE((*content)["negative"].read(small)); + + u64 large = 0; + CHECK_FALSE((*content)["negative"].read(large)); + CHECK((*content)["value"].read(large)); + CHECK(large == 256u); +} + +TEST_CASE("Lua tables read user-defined types") +{ + Lua::State const state = make_state("point = { x = 1.5, y = -2 }"); + + std::optional table = state.globals()["point"].expect_table(); + REQUIRE(table.has_value()); + + Point point{}; + CHECK(table->read(point)); + CHECK(point.x == 1.5f); + CHECK(point.y == -2.f); +} + +TEST_CASE("Lua table keys have deterministic traversal order") +{ + Lua::State const state = make_state("content = { z = true, a = true, [2] = true, [1] = true }"); + + std::optional content = state.globals()["content"].expect_table(); + REQUIRE(content.has_value()); + + std::vector const keys = content->keys(); + REQUIRE(keys.size() == 4); + CHECK(keys[0].as_index() == 1); + CHECK(keys[1].as_index() == 2); + CHECK(keys[2].as_name() == "a"); + CHECK(keys[3].as_name() == "z"); +} + +TEST_CASE("Lua tables can be modified from C++") +{ + std::optional state = Lua::State::create(); + REQUIRE(state.has_value()); + + Lua::Table const globals = state->globals(); + Lua::Table const content = state->create_table(); + content.set("flag", true); + content.set("count", 3); + content.set("ratio", 0.5); + content.set("name", "grass"); + content.set(1, content); + globals.set("content", content); + + constexpr std::string_view script = R"( + assert(content.flag == true and content.count == 3 and content.ratio == 0.5) + assert(content.name == 'grass' and content[1] == content) + content.flag = nil + )"; + REQUIRE(state->execute("test", script)); + + CHECK(content["flag"].is_nil()); + CHECK(content["count"].is_integer()); + CHECK(content["ratio"].is_number()); + CHECK(content[1].reference() == content.reference()); + + content.erase("name"); + CHECK(content["name"].is_nil()); +} + +TEST_CASE("Lua references survive their parent and a moved state") +{ + std::optional state = Lua::State::create(); + REQUIRE(state.has_value()); + REQUIRE(state->execute("test", "content = { name = 'grass' }")); + + std::optional name; + { + std::optional content = state->globals()["content"].expect_table(); + REQUIRE(content.has_value()); + name = (*content)["name"]; + } + REQUIRE(state->execute("test", "content = nil")); + + Lua::State const moved = std::move(*state); + REQUIRE(name->is_string()); + CHECK(name->to_string() == "grass"); + + Lua::Value const copy = *name; + CHECK(copy.reference() == name->reference()); + name.reset(); + CHECK(copy.to_string() == "grass"); +} + +TEST_CASE("Lua scripts report errors") +{ + std::optional state = Lua::State::create(); + REQUIRE(state.has_value()); + CHECK_FALSE(state->execute("syntax", "content = {")); + CHECK_FALSE(state->execute("runtime", "error('failure')")); + CHECK_FALSE(state->execute("sandbox", "dofile('content/terrain.lua')")); + CHECK_FALSE(state->load("missing.lua")); +} diff --git a/content/terrain.lua b/content/terrain.lua index 51d9775..7181dc3 100644 --- a/content/terrain.lua +++ b/content/terrain.lua @@ -3,7 +3,7 @@ local function make_sprite(x, y, color) return { atlas = 'content/kenney-1bitpack.png', source = { x * tilesize, y * tilesize, tilesize, tilesize}, - color = color + tint = color } end diff --git a/core/include/core/basic-catalog.hh b/core/include/core/basic-catalog.hh index 7beb8a4..080dd36 100644 --- a/core/include/core/basic-catalog.hh +++ b/core/include/core/basic-catalog.hh @@ -21,13 +21,13 @@ struct BasicCatalog> template [[nodiscard]] bool valid(Handle handle) const { - return handle.generation == _generation && handle.index < store_of().size(); + return handle.generation == generation_ && handle.index < store_of().size(); } template [[nodiscard]] Handle find(std::string_view label) const { - return { store_of().find(label), _generation }; + return { store_of().find(label), generation_ }; } template @@ -51,42 +51,30 @@ struct BasicCatalog> return store_of()[handle.index]; } - template - Handle create(std::string_view label, T const& value) - { - return { store_of().emplace(label, value), _generation }; - } - - template - Handle create(std::string_view label, T&& value) - { - return { store_of().emplace(label, std::forward(value)), _generation }; - } - template requires std::constructible_from Handle emplace(std::string_view label, Args&&... args) { - return { store_of().emplace(label, std::forward(args)...), _generation }; + return { store_of().emplace(label, std::forward(args)...), generation_ }; } template [[nodiscard]] T const* try_get(Handle handle) const { if (!valid(handle)) return nullptr; - return store_of()[handle.index]; + return &store_of()[handle.index]; } template [[nodiscard]] T* try_get(Handle handle) { if (!valid(handle)) return nullptr; - return store_of()[handle.index]; + return &store_of()[handle.index]; } void clear() { - std::apply([](auto&... stores) { (stores.clear(), ...); }, _stores); + std::apply([](auto&... stores) { (stores.clear(), ...); }, stores_); } template @@ -116,18 +104,18 @@ private: template constexpr IndexedMap& store_of() { - return std::get>(_stores); + return std::get>(stores_); } template constexpr IndexedMap const& store_of() const { - return std::get>(_stores); + return std::get>(stores_); } BasicCatalog(BasicCatalog const&) = default; - u32 _generation; - std::tuple...> _stores; + u32 generation_ = 0u; + std::tuple...> stores_; }; diff --git a/core/include/core/defer.hh b/core/include/core/defer.hh index ecdf238..ea4ec7c 100644 --- a/core/include/core/defer.hh +++ b/core/include/core/defer.hh @@ -13,8 +13,11 @@ struct Defer template Defer(F) -> Defer; -#define DEFER(expr) \ - Defer _defer##__COUNTER__ \ - { \ - [&] { expr; } \ +#define DEFER_CONCAT_IMPL(a, b) a##b +#define DEFER_CONCAT(a, b) DEFER_CONCAT_IMPL(a, b) + +#define DEFER(expr) \ + Defer DEFER_CONCAT(defer_, __COUNTER__) \ + { \ + [&] { expr; } \ } diff --git a/core/include/core/panic.hh b/core/include/core/panic.hh index 5bde381..3ba0e91 100644 --- a/core/include/core/panic.hh +++ b/core/include/core/panic.hh @@ -9,10 +9,10 @@ namespace detail { std::source_location location = std::source_location::current()); } // namespace detail -#define PANIC(category, cond, ...) \ - do { \ - if (!(cond)) [[unlikely]] \ - detail::panic(category, #cond, "" __VA_ARGS__); \ +#define PANIC(category, cond, ...) \ + do { \ + if (!(cond)) [[unlikely]] \ + ::detail::panic(category, #cond, "" __VA_ARGS__); \ } while (0) // Always-on: aborts in both debug and release. diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 5703971..17786dc 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -6,6 +6,6 @@ target_include_directories( PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) -target_link_libraries(game PUBLIC core) +target_link_libraries(game PUBLIC core glm::glm) add_subdirectory(src) diff --git a/game/include/game/biome/caves.hh b/game/include/game/biome/caves.hh index 86bcc11..1949a54 100644 --- a/game/include/game/biome/caves.hh +++ b/game/include/game/biome/caves.hh @@ -4,18 +4,20 @@ #include "game/chunk.hh" namespace Game { + struct World; + // cellular automata rule used for cave generation void generate_cave( Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations); struct CaveGenerator : ChunkGenerator { - CaveGenerator(World& world, u64 seed); + CaveGenerator(World const& world, u64 seed); void generate(u32 x, u32 y, Chunk& chunk) override; private: - u64 seed_; - u32 wall_, floor_; + u64 seed_; + Handle wall_, floor_; }; } diff --git a/game/include/game/biome/grassland.hh b/game/include/game/biome/grassland.hh index d2c6f95..86a470b 100644 --- a/game/include/game/biome/grassland.hh +++ b/game/include/game/biome/grassland.hh @@ -2,9 +2,11 @@ #include "game/chunk.hh" namespace Game { + struct World; + struct GrasslandGenerator : ChunkGenerator { - GrasslandGenerator(World&, u64 seed); + GrasslandGenerator(World const&, u64 seed); void generate(u32 x, u32 y, Chunk& chunk) override; private: diff --git a/game/include/game/chunk.hh b/game/include/game/chunk.hh index 9fc3b30..2e53b0a 100644 --- a/game/include/game/chunk.hh +++ b/game/include/game/chunk.hh @@ -3,15 +3,11 @@ #include "game/tile.hh" namespace Game { - struct Camera; - struct World; - constexpr u32 chunk_size = 64; struct Chunk : Grid2 { // other relevant data... - void render(World&, Camera const& camera, float tile_size) const; }; struct ChunkGenerator diff --git a/game/include/game/command.hh b/game/include/game/command.hh index a4a26bb..5830c70 100644 --- a/game/include/game/command.hh +++ b/game/include/game/command.hh @@ -64,7 +64,7 @@ namespace Game { [[nodiscard]] inline Kind kind() const { return kind_; } - [[nodiscard]] inline bool is_empty() const { return kind_ != Kind::Nil; } + [[nodiscard]] inline bool is_empty() const { return kind_ == Kind::Nil; } [[nodiscard]] inline explicit operator bool() const { return !is_empty(); } diff --git a/game/include/game/content/terrain.hh b/game/include/game/content/terrain.hh index bc62166..ac8868d 100644 --- a/game/include/game/content/terrain.hh +++ b/game/include/game/content/terrain.hh @@ -1,19 +1,7 @@ #pragma once -// #include "core/types.hh" -// #include - namespace Game { - struct World; - + // TODO: simulation properties of terrain (i.e. walkable, movement cost) struct Terrain {}; - - // [[nodiscard]] bool is_valid(World const&, Handle); - - // [[nodiscard]] Terrain const* try_get(World const&, Handle); - - // [[nodiscard]] Terrain const& get(World const&, Handle); - - // [[nodiscard]] Handle find_terrain(World const&, std::string_view); } diff --git a/game/include/game/entity.hh b/game/include/game/entity.hh index e8bbf6c..d7eaecc 100644 --- a/game/include/game/entity.hh +++ b/game/include/game/entity.hh @@ -2,18 +2,6 @@ #include "core/types.hh" namespace Game { - struct World; - // Goal: stable, persistent, cross-session, unique identifier for a simulation object. enum class Entity : u64 { Nil = UINT64_MAX }; - - // [[nodiscard]] bool is_valid(World&, Handle); - - // [[nodiscard]] Entity get(World&, Handle); - - // [[nodiscard]] Entity try_get(World&, Handle); - - // [[nodiscard]] Handle find_entity(World&, Entity); - - // [[nodiscard]] Handle spawn(World&); } diff --git a/game/include/game/tile.hh b/game/include/game/tile.hh index 530b973..851ebbc 100644 --- a/game/include/game/tile.hh +++ b/game/include/game/tile.hh @@ -1,6 +1,6 @@ #pragma once -#include "content/terrain.hh" #include "core/types.hh" +#include "game/content/terrain.hh" namespace Game { struct Tile diff --git a/game/include/game/world.hh b/game/include/game/world.hh index 846b7d2..bfb458e 100644 --- a/game/include/game/world.hh +++ b/game/include/game/world.hh @@ -1,9 +1,9 @@ #pragma once -#include "component/pose.hh" -#include "content/terrain.hh" #include "core/basic-catalog.hh" #include "core/basic-registry.hh" #include "game/chunk.hh" +#include "game/component/pose.hh" +#include "game/content/terrain.hh" #include "game/entity.hh" #include @@ -16,9 +16,9 @@ namespace Game { struct World { - Catalog content; - Registry entities; - Chunk environment; + Catalog content; // immutable after loading + Registry entities; // entity data + Chunk environment; // placeholder for a chunked world [[nodiscard]] Handle find(Entity e) const; @@ -28,7 +28,10 @@ namespace Game { private: - u64 _entity_counter; - std::unordered_map> _lookup; + // Total number of entities including those not loaded. + u64 entity_counter_ = 0u; + + // Hash map used because entity IDs are sparse, not dense. + std::unordered_map> lookup_; }; } diff --git a/game/src/CMakeLists.txt b/game/src/CMakeLists.txt index 4a72444..3ed1f38 100644 --- a/game/src/CMakeLists.txt +++ b/game/src/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory (content) add_subdirectory (biome) -target_sources (game PRIVATE command.cc command-buffer.cc) +target_sources (game PRIVATE command.cc command-buffer.cc world.cc) diff --git a/game/src/biome/CMakeLists.txt b/game/src/biome/CMakeLists.txt index 83b1768..0220195 100644 --- a/game/src/biome/CMakeLists.txt +++ b/game/src/biome/CMakeLists.txt @@ -1 +1 @@ -target_sources (game PRIVATE grassland.cc) +target_sources (game PRIVATE caves.cc grassland.cc) diff --git a/game/src/biome/caves.cc b/game/src/biome/caves.cc index cc78b77..5566383 100644 --- a/game/src/biome/caves.cc +++ b/game/src/biome/caves.cc @@ -1,6 +1,6 @@ +#include "game/biome/caves.hh" #include "core/grid2.hh" #include "core/random.hh" -#include "game/biome/caves.hh" #include "game/world.hh" #include #include @@ -47,29 +47,31 @@ namespace Game { } } - CaveGenerator::CaveGenerator(World& world, u64 seed) - : _seed{ seed } - , _wall{ world.content.find("stone") } - , _floor{ world.content.find("dirt") } + CaveGenerator::CaveGenerator(World const& world, u64 seed) + : seed_{ seed } + , wall_{ world.content.find("stone") } + , floor_{ world.content.find("dirt") } { - PRECONDITION(wall_ != nil); - PRECONDITION(floor_ != nil); + PRECONDITION(wall_, "terrain undefined"); + PRECONDITION(floor_, "terrain undefined"); } void CaveGenerator::generate(u32 x, u32 y, Chunk& chunk) { - auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); - Grid2 cave(chunk_size, chunk_size); + auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); + constexpr u32 wall = 1u; + constexpr u32 floor = 0u; + Grid2 cave(chunk_size, chunk_size); // uniformly random fill Xoshiro256ss rng{ hash }; std::bernoulli_distribution coin{ 0.45f }; for (auto& tile : cave) - tile = coin(rng) ? wall_ : floor_; - generate_cave(cave, wall_, floor_, 5, 6, 1, 1); - generate_cave(cave, wall_, floor_, 3, 4, 2, 2); - generate_cave(cave, wall_, floor_, 9, 10, 2, 1); - for (auto [tile, terrain] : std::views::zip(chunk, cave)) { - tile.terrain = terrain; + tile = coin(rng) ? wall : floor; + generate_cave(cave, wall, floor, 5, 6, 1, 1); + generate_cave(cave, wall, floor, 3, 4, 2, 2); + generate_cave(cave, wall, floor, 9, 10, 2, 1); + for (auto [tile, cell] : std::views::zip(chunk, cave)) { + tile.terrain = cell == wall ? wall_ : floor_; } } } diff --git a/game/src/biome/grassland.cc b/game/src/biome/grassland.cc index 0c65893..78867b4 100644 --- a/game/src/biome/grassland.cc +++ b/game/src/biome/grassland.cc @@ -1,33 +1,30 @@ #include "game/biome/grassland.hh" #include "core/random.hh" -#include "game/catalog.hh" -#include "game/context.hh" +#include "game/world.hh" #include -Token find_terrain(Context ctx, std::string_view label) -{ - return access_catalog(ctx).find(label); -} - -GrasslandGenerator::GrasslandGenerator(Context const ctx, u64 seed) - : seed_{ seed } - , terrain_{ - find_terrain(ctx, "grass-1"), find_terrain(ctx, "grass-2"), find_terrain(ctx, "grass-3"), - find_terrain(ctx, "grass-tall"), find_terrain(ctx, "dirt"), find_terrain(ctx, "rocks"), +namespace Game { + GrasslandGenerator::GrasslandGenerator(World const& world, u64 seed) + : seed_{ seed } + , terrain_{ + world.content.find("grass-1"), world.content.find("grass-2"), + world.content.find("grass-3"), world.content.find("grass-tall"), + world.content.find("dirt"), world.content.find("rocks"), + } + { + for (Handle terrain : terrain_) { + PRECONDITION(terrain, "terrain undefined"); + } } -{ - for (auto token : terrain_) { - PRECONDITION(token, "terrain undefined"); - } -} -void GrasslandGenerator::generate(u32 x, u32 y, Chunk& chunk) -{ - auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); - Xoshiro256ss rng{ hash }; - std::uniform_int_distribution distribution{ 0u, (u32)std::size(terrain_) - 1u }; + void GrasslandGenerator::generate(u32 x, u32 y, Chunk& chunk) + { + auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); + Xoshiro256ss rng{ hash }; + std::uniform_int_distribution distribution{ 0u, (u32)std::size(terrain_) - 1u }; - for (auto& tile : chunk) { - tile.terrain = terrain_[distribution(rng)]; + for (Tile& tile : chunk) { + tile.terrain = terrain_[distribution(rng)]; + } } } diff --git a/game/src/command.cc b/game/src/command.cc index a533493..469371c 100644 --- a/game/src/command.cc +++ b/game/src/command.cc @@ -9,7 +9,7 @@ namespace Game { static CommandReply resolve(World& world, Entity actor, Command::Detail::Move const& move) { - auto handle = world.find_entity(actor); + auto handle = world.find(actor); if (!handle) return { CommandStatus::Rejected, CommandError::DeadEntity }; auto pose = world.entities.try_get(handle); if (!pose) return { CommandStatus::Rejected, CommandError::MissingComponent }; diff --git a/game/src/world.cc b/game/src/world.cc index a55cb12..0ac71d1 100644 --- a/game/src/world.cc +++ b/game/src/world.cc @@ -1,20 +1,21 @@ -#include "core/panic.hh" #include "game/world.hh" +#include "core/panic.hh" -Handle World::create() -{ - auto const id = Entity{ _entity_counter++ }; - auto const handle = _entities.create(id); - auto [_, success] = _lookup.emplace(id, handle); - INVARIANT(success); - return handle; -} - -Handle World::find_entity(Entity e) const -{ - auto it = _lookup.find(e); - return it != _lookup.end() ? it->second : Handle::null(); -} +namespace Game { + Handle World::create() + { + auto const id = Entity{ entity_counter_++ }; + Handle const handle = entities.create(id); + auto [_, success] = lookup_.emplace(id, handle); + INVARIANT(success); + return handle; + } -void World::advance() {} + Handle World::find(Entity e) const + { + auto it = lookup_.find(e); + return it != lookup_.end() ? it->second : Handle::null(); + } + void World::advance() {} +} diff --git a/play.cc b/play.cc index 9e3383f..590fbe5 100644 --- a/play.cc +++ b/play.cc @@ -1,36 +1,38 @@ -#include "app/application.hh" +#include "app/state.hh" #include "core/clock.hh" +#include "core/panic.hh" #include "core/random.hh" #include "game/biome/grassland.hh" #include "game/command-buffer.hh" -#include "game/entity.hh" -#include "game/load.hh" -#include "game/simulation.hh" +#include "game/world.hh" #include "sys/main.hh" +#include #include +#include struct Runtime { - Simulation sim; - Application app; - CommandBuffer cmds; - Clock clock = { 0, 32 }; + Game::World world; + App::State app; + Game::CommandBuffer cmds; + Clock clock = { 0, 32 }; }; -void load_chunk(LoadContext ctx, Chunk& chunk) +static void generate_environment(Game::World& world) { static u64 const seed = random_seed(); static SplitMix64 rng(seed); - std::uniform_int_distribution dist{ 0u, chunk_size - 1u }; - GrasslandGenerator(ctx, seed).generate(dist(rng), dist(rng), chunk); + std::uniform_int_distribution dist{ 0u, Game::chunk_size - 1u }; + Game::GrasslandGenerator(world, seed).generate(dist(rng), dist(rng), world.environment); } -Entity load_player(Context ctx) +static Game::Entity spawn_player(Game::World& world) { - auto player = create_entity(ctx); - player.emplace(glm::vec2{ chunk_size * 0.5f, chunk_size * 0.5f }); - INVARIANT(find_entity(ctx, player.id()) != Handle::null()); - return player.id(); + Handle const handle = world.create(); + world.entities.emplace( + handle, glm::vec2{ Game::chunk_size * 0.5f, Game::chunk_size * 0.5f }); + INVARIANT(world.find(world.entities[handle]) == handle); + return world.entities[handle]; } Runtime* start(int /*argc*/, char* /*argv*/[]) @@ -38,38 +40,25 @@ Runtime* start(int /*argc*/, char* /*argv*/[]) auto state = std::make_unique(); state->clock.set_rate(32); - auto ctx = state->sim.load(); - if (!ctx) return nullptr; - if (!load_content(*ctx, "content/terrain.lua")) { - std::println("Failed to load content."); - return nullptr; - } - load_chunk(*ctx, state->sim.scene()); - if (auto result = state->app.load(*ctx, load_player(*ctx)); !result) { + Game::Entity const player = spawn_player(state->world); + if (auto result = state->app.load(state->world, player); !result) { std::println("Failed to open application: {}", result.error()); return nullptr; } + generate_environment(state->world); return state.release(); } -void step(Runtime& state, i64 tick) +static void step(Runtime& state, i64 /*tick*/) { - auto ctx = state.sim.step(tick); - state.app.step(state.cmds, ctx); - state.cmds.dispatch(ctx); + state.app.step(state.world, state.cmds); + state.cmds.dispatch(state.world); + state.world.advance(); } -void update(Runtime& state, f32 delta) -{ - auto ctx = state.sim.context(); - state.app.update(ctx, delta); -} +static void update(Runtime& state, f32 delta) { state.app.update(state.world, delta); } -void render(Runtime& state, f32 alpha) -{ - auto ctx = state.sim.context(); - state.app.render(ctx, alpha); -} +static void render(Runtime& state, f32 alpha) { state.app.render(state.world, alpha); } void iterate(Runtime& state) {