Compare commits
3 commits
eb98beac43
...
b4635cdb99
| Author | SHA1 | Date | |
|---|---|---|---|
| b4635cdb99 | |||
| 718679c13b | |||
| 3addd01f54 |
18 changed files with 434 additions and 232 deletions
26
assets/tilesets/terrain.json
Normal file
26
assets/tilesets/terrain.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"texture": "terrain.png",
|
||||||
|
"tile_size": 32,
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ defaults:
|
||||||
-Wall
|
-Wall
|
||||||
-Wextra
|
-Wextra
|
||||||
-pedantic
|
-pedantic
|
||||||
|
-Ivendor/nlohmann_json/include
|
||||||
-Ivendor/RmlUi/Include
|
-Ivendor/RmlUi/Include
|
||||||
linker_args: >-
|
linker_args: >-
|
||||||
-lSDL3
|
-lSDL3
|
||||||
|
|
|
||||||
12
src/rutile/core/common.cppm
Normal file
12
src/rutile/core/common.cppm
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
module;
|
||||||
|
|
||||||
|
export module rutile.core.common;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace rutile
|
||||||
|
{
|
||||||
|
// Alias for shared_ptr to be used for shared resource pointers (see ResourceServer)
|
||||||
|
template <typename T>
|
||||||
|
using ResourcePtr = std::shared_ptr<T>;
|
||||||
|
}
|
||||||
|
|
@ -5,15 +5,17 @@ module;
|
||||||
export module rutile.core.drawing.sprite;
|
export module rutile.core.drawing.sprite;
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
|
import rutile.core.common;
|
||||||
import rutile.core.render_server;
|
import rutile.core.render_server;
|
||||||
|
import rutile.core.resources.texture;
|
||||||
import wrappers.sdl;
|
import wrappers.sdl;
|
||||||
|
|
||||||
export namespace rutile
|
export namespace rutile
|
||||||
{
|
{
|
||||||
class Sprite
|
class Sprite
|
||||||
{
|
{
|
||||||
// Texture ID (managed by TextureManager in RenderServer)
|
// Shared pointer to a texture (managed by ResourceServer)
|
||||||
TextureID texture_id_;
|
ResourcePtr<Texture> texture_ptr_;
|
||||||
|
|
||||||
// Texture source boundaries: which part of the texture to render
|
// Texture source boundaries: which part of the texture to render
|
||||||
sdl::FRect texture_boundaries_;
|
sdl::FRect texture_boundaries_;
|
||||||
|
|
@ -30,13 +32,13 @@ export namespace rutile
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Sprite(
|
explicit Sprite(
|
||||||
const TextureID texture_id,
|
const ResourcePtr<Texture>& texture_ptr,
|
||||||
const sdl::FRect texture_boundaries,
|
const sdl::FRect texture_boundaries,
|
||||||
const sdl::FPoint position,
|
const sdl::FPoint position,
|
||||||
const sdl::FPoint texture_offset = {0, 0},
|
const sdl::FPoint texture_offset = {0, 0},
|
||||||
const sdl::FPoint texture_scale = {1, 1}
|
const sdl::FPoint texture_scale = {1, 1}
|
||||||
)
|
)
|
||||||
: texture_id_{texture_id},
|
: texture_ptr_{texture_ptr},
|
||||||
texture_boundaries_{texture_boundaries},
|
texture_boundaries_{texture_boundaries},
|
||||||
texture_offset_{texture_offset},
|
texture_offset_{texture_offset},
|
||||||
texture_scale_{texture_scale},
|
texture_scale_{texture_scale},
|
||||||
|
|
@ -46,20 +48,16 @@ export namespace rutile
|
||||||
}
|
}
|
||||||
|
|
||||||
static Sprite create_from_texture(
|
static Sprite create_from_texture(
|
||||||
RenderServer& render_server,
|
const ResourcePtr<Texture>& texture_ptr,
|
||||||
const std::string& filename,
|
|
||||||
std::optional<sdl::FRect> texture_boundaries,
|
std::optional<sdl::FRect> texture_boundaries,
|
||||||
auto... args
|
auto... args
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
const TextureID texture_id = render_server.load_texture(filename);
|
|
||||||
|
|
||||||
if (!texture_boundaries.has_value()) {
|
if (!texture_boundaries.has_value()) {
|
||||||
const sdl::Texture& texture = render_server.get_texture(texture_id);
|
texture_boundaries = texture_ptr->get_boundaries();
|
||||||
texture_boundaries = texture.get_boundaries();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Sprite(texture_id, texture_boundaries.value(), args...);
|
return Sprite(texture_ptr, texture_boundaries.value(), args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_position(const sdl::FPoint new_position)
|
void set_position(const sdl::FPoint new_position)
|
||||||
|
|
@ -76,7 +74,7 @@ export namespace rutile
|
||||||
texture_scale_.y * texture_boundaries_.h,
|
texture_scale_.y * texture_boundaries_.h,
|
||||||
};
|
};
|
||||||
|
|
||||||
render_server.render_texture(texture_id_, &texture_boundaries_, &dest_rect);
|
render_server.render_texture(*texture_ptr_, &texture_boundaries_, &dest_rect);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ module;
|
||||||
export module rutile.core.drawing.tile_map;
|
export module rutile.core.drawing.tile_map;
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
import rutile.core.drawing.tile_set;
|
import rutile.core.common;
|
||||||
import rutile.core.render_server;
|
import rutile.core.render_server;
|
||||||
|
import rutile.core.resources.tile_set;
|
||||||
import wrappers.sdl;
|
import wrappers.sdl;
|
||||||
|
|
||||||
export namespace rutile
|
export namespace rutile
|
||||||
|
|
@ -16,10 +17,8 @@ export namespace rutile
|
||||||
|
|
||||||
class TileMap
|
class TileMap
|
||||||
{
|
{
|
||||||
// Tile set ID
|
// Shared pointer to a tile set (managed by ResourceServer)
|
||||||
// TODO: Needed later when we centralize tile set management using a ResourceManager
|
ResourcePtr<TileSet> tile_set_ptr_;
|
||||||
[[maybe_unused]]
|
|
||||||
TileSetID tile_set_id_;
|
|
||||||
|
|
||||||
// Tile size
|
// Tile size
|
||||||
int tile_size_;
|
int tile_size_;
|
||||||
|
|
@ -36,16 +35,16 @@ export namespace rutile
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TileMap(
|
explicit TileMap(
|
||||||
const TileSetID tile_set_id,
|
const ResourcePtr<TileSet>& tile_set_ptr,
|
||||||
const int tile_size,
|
const int tile_size,
|
||||||
const MapCoord width,
|
const MapCoord width,
|
||||||
const MapCoord height,
|
const MapCoord height,
|
||||||
const sdl::FPoint position = {0, 0}
|
const sdl::FPoint position = {0, 0}
|
||||||
)
|
)
|
||||||
: tile_set_id_(tile_set_id),
|
: tile_set_ptr_{tile_set_ptr},
|
||||||
tile_size_(tile_size),
|
tile_size_{tile_size},
|
||||||
width_(width),
|
width_{width},
|
||||||
height_(height),
|
height_{height},
|
||||||
position_{position}
|
position_{position}
|
||||||
{
|
{
|
||||||
// TODO: This maybe shouldn't be an assert...
|
// TODO: This maybe shouldn't be an assert...
|
||||||
|
|
@ -72,11 +71,9 @@ export namespace rutile
|
||||||
tiles_[map_coords_to_index(x, y)] = tile_id;
|
tiles_[map_coords_to_index(x, y)] = tile_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Should we get the TileSet from the RenderServer, similar to how we get textures?
|
void draw(const RenderServer& render_server) const
|
||||||
void draw(const RenderServer& render_server, const TileSet& tile_set) const
|
|
||||||
{
|
{
|
||||||
// Get tile set texture
|
const TileSet& tile_set = *tile_set_ptr_;
|
||||||
const sdl::Texture& tile_set_texture = render_server.get_texture(tile_set.get_texture_id());
|
|
||||||
|
|
||||||
// TODO: Implement more efficiently (we could just iterate over tiles_ directly)
|
// TODO: Implement more efficiently (we could just iterate over tiles_ directly)
|
||||||
for (MapCoord y = 0; y < height_; y++) {
|
for (MapCoord y = 0; y < height_; y++) {
|
||||||
|
|
@ -97,7 +94,7 @@ export namespace rutile
|
||||||
};
|
};
|
||||||
|
|
||||||
render_server.render_texture(
|
render_server.render_texture(
|
||||||
tile_set_texture,
|
tile_set.get_texture(),
|
||||||
&tile_atlas_src_rect,
|
&tile_atlas_src_rect,
|
||||||
&tile_dest_rect
|
&tile_dest_rect
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export module rutile.core.engine;
|
||||||
import std;
|
import std;
|
||||||
import rutile.config;
|
import rutile.config;
|
||||||
import rutile.core.render_server;
|
import rutile.core.render_server;
|
||||||
|
import rutile.core.resource_server;
|
||||||
import rutile.core.ui.ui_server;
|
import rutile.core.ui.ui_server;
|
||||||
import wrappers.sdl;
|
import wrappers.sdl;
|
||||||
|
|
||||||
|
|
@ -22,12 +23,14 @@ export namespace rutile
|
||||||
|
|
||||||
sdl::Window window_;
|
sdl::Window window_;
|
||||||
RenderServer render_server_;
|
RenderServer render_server_;
|
||||||
|
ResourceServer resource_server_;
|
||||||
UIServer ui_server_;
|
UIServer ui_server_;
|
||||||
|
|
||||||
// Private constructor
|
// Private constructor
|
||||||
Engine(sdl::Window&& window, sdl::Renderer&& renderer)
|
Engine(sdl::Window&& window, sdl::Renderer&& renderer)
|
||||||
: window_{std::move(window)},
|
: window_{std::move(window)},
|
||||||
render_server_{std::move(renderer)},
|
render_server_{std::move(renderer)},
|
||||||
|
resource_server_{render_server_.get_renderer()},
|
||||||
ui_server_{render_server_.get_renderer(), window_}
|
ui_server_{render_server_.get_renderer(), window_}
|
||||||
{
|
{
|
||||||
instantiated_ = true;
|
instantiated_ = true;
|
||||||
|
|
@ -84,6 +87,11 @@ export namespace rutile
|
||||||
return render_server_;
|
return render_server_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResourceServer& get_resource_server()
|
||||||
|
{
|
||||||
|
return resource_server_;
|
||||||
|
}
|
||||||
|
|
||||||
UIServer& get_ui_server()
|
UIServer& get_ui_server()
|
||||||
{
|
{
|
||||||
return ui_server_;
|
return ui_server_;
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,21 @@ module;
|
||||||
export module rutile.core.render_server;
|
export module rutile.core.render_server;
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
import rutile.core.resource_manager;
|
import rutile.core.resource_server;
|
||||||
import rutile.core.texture_loader;
|
import rutile.core.resources.texture;
|
||||||
import wrappers.sdl;
|
import wrappers.sdl;
|
||||||
|
|
||||||
export namespace rutile
|
export namespace rutile
|
||||||
{
|
{
|
||||||
using TextureID = unsigned int;
|
|
||||||
using TextureManager = ResourceManager<TextureID, sdl::Texture, TextureLoader>;
|
|
||||||
|
|
||||||
class RenderServer
|
class RenderServer
|
||||||
{
|
{
|
||||||
sdl::Renderer renderer_;
|
sdl::Renderer renderer_;
|
||||||
TextureLoader texture_loader_;
|
|
||||||
TextureManager texture_manager_;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RenderServer() = delete;
|
RenderServer() = delete;
|
||||||
|
|
||||||
explicit RenderServer(sdl::Renderer&& renderer)
|
explicit RenderServer(sdl::Renderer&& renderer)
|
||||||
: renderer_{std::move(renderer)},
|
: renderer_{std::move(renderer)}
|
||||||
texture_loader_{renderer_},
|
|
||||||
texture_manager_{texture_loader_}
|
|
||||||
{
|
{
|
||||||
renderer_.set_vsync(1);
|
renderer_.set_vsync(1);
|
||||||
}
|
}
|
||||||
|
|
@ -42,22 +35,6 @@ export namespace rutile
|
||||||
return renderer_;
|
return renderer_;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a texture from a file (if not loaded yet) and return its texture ID.
|
|
||||||
*/
|
|
||||||
constexpr TextureID load_texture(const std::string& filename)
|
|
||||||
{
|
|
||||||
return texture_manager_.load_resource_by_name(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a reference to a texture by its texture ID.
|
|
||||||
*/
|
|
||||||
constexpr const sdl::Texture& get_texture(const TextureID texture_id) const
|
|
||||||
{
|
|
||||||
return texture_manager_.get_resource(texture_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void start_frame() const
|
void start_frame() const
|
||||||
{
|
{
|
||||||
renderer_.clear();
|
renderer_.clear();
|
||||||
|
|
@ -68,17 +45,8 @@ export namespace rutile
|
||||||
renderer_.present();
|
renderer_.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_texture(
|
|
||||||
const TextureID texture_id,
|
|
||||||
const sdl::FRect* src_rect,
|
|
||||||
const sdl::FRect* dest_rect
|
|
||||||
) const
|
|
||||||
{
|
|
||||||
render_texture(get_texture(texture_id), src_rect, dest_rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr void render_texture(
|
constexpr void render_texture(
|
||||||
const sdl::Texture& texture,
|
const Texture& texture,
|
||||||
const sdl::FRect* src_rect,
|
const sdl::FRect* src_rect,
|
||||||
const sdl::FRect* dest_rect
|
const sdl::FRect* dest_rect
|
||||||
) const
|
) const
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
module;
|
|
||||||
|
|
||||||
#include <cassert>
|
|
||||||
|
|
||||||
export module rutile.core.resource_manager;
|
|
||||||
|
|
||||||
import std;
|
|
||||||
|
|
||||||
export namespace rutile
|
|
||||||
{
|
|
||||||
template <typename ResourceLoaderType, typename ResourceType>
|
|
||||||
concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name)
|
|
||||||
{
|
|
||||||
{ loader.load_resource(name) } -> std::same_as<ResourceType>;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <
|
|
||||||
typename ResourceIDType,
|
|
||||||
typename ResourceType,
|
|
||||||
IsResourceLoader<ResourceType> ResourceLoaderType
|
|
||||||
>
|
|
||||||
class ResourceManager
|
|
||||||
{
|
|
||||||
// Resource loader
|
|
||||||
ResourceLoaderType& resource_loader_;
|
|
||||||
|
|
||||||
// Registry of loaded resources, array index is resource ID
|
|
||||||
std::vector<ResourceType> resource_registry_;
|
|
||||||
|
|
||||||
// Mapping of all loaded resources from (file) name to resource ID
|
|
||||||
std::map<std::string, ResourceIDType> resource_name_map_;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ResourceManager() = delete;
|
|
||||||
|
|
||||||
explicit ResourceManager(ResourceLoaderType& resource_loader)
|
|
||||||
: resource_loader_{resource_loader}
|
|
||||||
{}
|
|
||||||
|
|
||||||
// No copy or move operations
|
|
||||||
ResourceManager(const ResourceManager&) = delete;
|
|
||||||
ResourceManager& operator=(const ResourceManager&) = delete;
|
|
||||||
ResourceManager(ResourceManager&&) = delete;
|
|
||||||
ResourceManager& operator=(ResourceManager&&) = delete;
|
|
||||||
|
|
||||||
~ResourceManager() = default;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new resource to the registry and returns its ID.
|
|
||||||
*/
|
|
||||||
ResourceIDType add_resource(ResourceType&& resource)
|
|
||||||
{
|
|
||||||
// Add resource to end of vector
|
|
||||||
resource_registry_.push_back(std::move(resource));
|
|
||||||
|
|
||||||
// Return the index of the newly added resource
|
|
||||||
return static_cast<ResourceIDType>(resource_registry_.size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new resource to the registry, associates the name with it and returns its ID.
|
|
||||||
*/
|
|
||||||
ResourceIDType add_resource(ResourceType&& resource, const std::string& name)
|
|
||||||
{
|
|
||||||
// Add resource
|
|
||||||
ResourceIDType resource_id = add_resource(std::move(resource));
|
|
||||||
|
|
||||||
// Associate name with resource ID for future access
|
|
||||||
resource_name_map_.emplace(name, resource_id);
|
|
||||||
|
|
||||||
return resource_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the resource ID for a given resource name, or `std::nullopt` if the resource was not found.
|
|
||||||
*/
|
|
||||||
std::optional<ResourceIDType> get_resource_id_by_name(const std::string& name) const
|
|
||||||
{
|
|
||||||
// Check if resource is already loaded
|
|
||||||
if (
|
|
||||||
const auto search = resource_name_map_.find(name);
|
|
||||||
search != resource_name_map_.end()
|
|
||||||
) {
|
|
||||||
// Return resource ID
|
|
||||||
return search->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a resource (e.g. from a file) and adds it to the registry if it isn't loaded yet.
|
|
||||||
* Returns the resource ID.
|
|
||||||
*/
|
|
||||||
ResourceIDType load_resource_by_name(const std::string& name)
|
|
||||||
{
|
|
||||||
// Check if resource is already loaded
|
|
||||||
if (
|
|
||||||
auto resource_id = get_resource_id_by_name(name);
|
|
||||||
resource_id.has_value()
|
|
||||||
) {
|
|
||||||
return resource_id.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load resource and add it to the registry
|
|
||||||
return add_resource(resource_loader_.load_resource(name), name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a reference to the resource with the given ID.
|
|
||||||
* The reference is not guaranteed to be valid after adding new resources to the registry.
|
|
||||||
* Assumes that the resource ID is valid.
|
|
||||||
*/
|
|
||||||
const ResourceType& get_resource(const ResourceIDType resource_id) const
|
|
||||||
{
|
|
||||||
assert(resource_id < resource_registry_.size());
|
|
||||||
return resource_registry_[resource_id];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
61
src/rutile/core/resource_server.cppm
Normal file
61
src/rutile/core/resource_server.cppm
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
module;
|
||||||
|
|
||||||
|
export module rutile.core.resource_server;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import rutile.core.common;
|
||||||
|
import rutile.core.resources.resource_registry;
|
||||||
|
import rutile.core.resources.texture;
|
||||||
|
import rutile.core.resources.tile_set;
|
||||||
|
import rutile.core.resources.loaders.texture_loader;
|
||||||
|
import rutile.core.resources.loaders.tile_set_loader;
|
||||||
|
import wrappers.sdl;
|
||||||
|
|
||||||
|
export namespace rutile
|
||||||
|
{
|
||||||
|
using TextureRegistry = ResourceRegistry<TextureID, Texture>;
|
||||||
|
using TileSetRegistry = ResourceRegistry<TileSetID, TileSet>;
|
||||||
|
|
||||||
|
class ResourceServer
|
||||||
|
{
|
||||||
|
// Resource registries
|
||||||
|
TextureRegistry texture_registry_;
|
||||||
|
TileSetRegistry tile_set_registry_;
|
||||||
|
|
||||||
|
// Resource loaders
|
||||||
|
TextureLoader texture_loader_;
|
||||||
|
TileSetLoader tile_set_loader_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ResourceServer() = delete;
|
||||||
|
|
||||||
|
explicit ResourceServer(sdl::Renderer& renderer)
|
||||||
|
: texture_loader_{renderer},
|
||||||
|
tile_set_loader_{std::bind(&ResourceServer::load_texture, this, std::placeholders::_1)}
|
||||||
|
{}
|
||||||
|
|
||||||
|
// No copy or move operations
|
||||||
|
ResourceServer(const ResourceServer&) = delete;
|
||||||
|
ResourceServer& operator=(const ResourceServer&) = delete;
|
||||||
|
ResourceServer(ResourceServer&&) = delete;
|
||||||
|
ResourceServer& operator=(ResourceServer&&) = delete;
|
||||||
|
|
||||||
|
~ResourceServer() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a texture from a file (if not loaded yet) and return a reference to a shared Texture pointer.
|
||||||
|
*/
|
||||||
|
const ResourcePtr<Texture>& load_texture(const std::string& filename)
|
||||||
|
{
|
||||||
|
return texture_registry_.load_and_get_resource(filename, texture_loader_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a tile set from a JSON file (if not loaded yet) and return a reference to a shared TileSet pointer.
|
||||||
|
*/
|
||||||
|
const ResourcePtr<TileSet>& load_tile_set(const std::string& filename)
|
||||||
|
{
|
||||||
|
return tile_set_registry_.load_and_get_resource(filename, tile_set_loader_);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
module;
|
module;
|
||||||
|
|
||||||
export module rutile.core.texture_loader;
|
export module rutile.core.resources.loaders.texture_loader;
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
import rutile.core.resource_manager;
|
import rutile.core.resources.texture;
|
||||||
import wrappers.sdl;
|
import wrappers.sdl;
|
||||||
import wrappers.sdl_image;
|
import wrappers.sdl_image;
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ export namespace rutile
|
||||||
|
|
||||||
~TextureLoader() = default;
|
~TextureLoader() = default;
|
||||||
|
|
||||||
sdl::Texture load_resource(const std::string& filename) const
|
Texture load_resource(const std::string& filename) const
|
||||||
{
|
{
|
||||||
return sdl_image::load_texture(renderer_, filename);
|
return sdl_image::load_texture(renderer_, filename);
|
||||||
}
|
}
|
||||||
68
src/rutile/core/resources/loaders/tile_set_loader.cppm
Normal file
68
src/rutile/core/resources/loaders/tile_set_loader.cppm
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
module;
|
||||||
|
|
||||||
|
export module rutile.core.resources.loaders.tile_set_loader;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import rutile.core.common;
|
||||||
|
import rutile.core.resources.texture;
|
||||||
|
import rutile.core.resources.tile_set;
|
||||||
|
import wrappers.sdl;
|
||||||
|
import vendor.nlohmann.json;
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace rutile
|
||||||
|
{
|
||||||
|
using TextureLoadCallbackFunc = std::function<
|
||||||
|
const ResourcePtr<Texture>&(const std::string&)
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace rutile
|
||||||
|
{
|
||||||
|
class TileSetLoader
|
||||||
|
{
|
||||||
|
TextureLoadCallbackFunc texture_load_callback_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TileSetLoader(const TextureLoadCallbackFunc& texture_load_callback)
|
||||||
|
: texture_load_callback_{texture_load_callback}
|
||||||
|
{}
|
||||||
|
|
||||||
|
// No copy or move operations (reference)
|
||||||
|
TileSetLoader(const TileSetLoader&) = delete;
|
||||||
|
TileSetLoader& operator=(const TileSetLoader&) = delete;
|
||||||
|
TileSetLoader(TileSetLoader&&) = delete;
|
||||||
|
TileSetLoader& operator=(TileSetLoader&&) = delete;
|
||||||
|
|
||||||
|
~TileSetLoader() = default;
|
||||||
|
|
||||||
|
TileSet load_resource(const std::string& name) const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
std::ifstream json_file(name);
|
||||||
|
json data = json::parse(json_file);
|
||||||
|
|
||||||
|
// Texture path is relative to JSON file
|
||||||
|
fs::path texture_path = fs::path(name).replace_filename(data.at("texture"));
|
||||||
|
|
||||||
|
std::vector<sdl::Point> tile_atlas_coords;
|
||||||
|
for (auto tile : data.at("tiles")) {
|
||||||
|
tile_atlas_coords.push_back({tile.at("x"), tile.at("y")});
|
||||||
|
}
|
||||||
|
|
||||||
|
return TileSet::create_from_texture(
|
||||||
|
texture_load_callback_(texture_path),
|
||||||
|
data.at("tile_size"),
|
||||||
|
tile_atlas_coords
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (const json::exception& e) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::format("Error loading tile set from \"{}\": {}", name, e.what())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
128
src/rutile/core/resources/resource_registry.cppm
Normal file
128
src/rutile/core/resources/resource_registry.cppm
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
module;
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
export module rutile.core.resources.resource_registry;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import rutile.core.common;
|
||||||
|
|
||||||
|
export namespace rutile
|
||||||
|
{
|
||||||
|
template <typename ResourceLoaderType, typename ResourceType>
|
||||||
|
concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name)
|
||||||
|
{
|
||||||
|
{ loader.load_resource(name) } -> std::same_as<ResourceType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename ResourceIDType, typename ResourceType>
|
||||||
|
class ResourceRegistry
|
||||||
|
{
|
||||||
|
// Vector containing all currently loaded resources as shared pointers; array index is resource ID
|
||||||
|
std::vector<ResourcePtr<ResourceType>> resources_;
|
||||||
|
|
||||||
|
// Mapping of all loaded resources from name (e.g. file name) to resource ID
|
||||||
|
std::map<std::string, ResourceIDType> resource_name_map_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ResourceRegistry() = default;
|
||||||
|
|
||||||
|
// No copy or move operations
|
||||||
|
ResourceRegistry(const ResourceRegistry&) = delete;
|
||||||
|
ResourceRegistry& operator=(const ResourceRegistry&) = delete;
|
||||||
|
ResourceRegistry(ResourceRegistry&&) = delete;
|
||||||
|
ResourceRegistry& operator=(ResourceRegistry&&) = delete;
|
||||||
|
|
||||||
|
~ResourceRegistry() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a reference to a shared pointer of the resource with the given ID.
|
||||||
|
*
|
||||||
|
* The reference is not guaranteed to be valid after adding new resources to the registry. Copy the
|
||||||
|
* reference into a new shared_ptr if you want to keep using it.
|
||||||
|
*
|
||||||
|
* Assumes that the resource ID is valid (debug assertions only).
|
||||||
|
*/
|
||||||
|
const ResourcePtr<ResourceType>& get_resource_by_id(const ResourceIDType resource_id) const
|
||||||
|
{
|
||||||
|
assert(resource_id < resources_.size());
|
||||||
|
assert(resources_[resource_id].use_count() > 0);
|
||||||
|
return resources_[resource_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the resource ID for a given resource name, or return `std::nullopt` if the resource was not found.
|
||||||
|
*/
|
||||||
|
std::optional<ResourceIDType> find_resource_id_by_name(const std::string& name) const
|
||||||
|
{
|
||||||
|
// Check if resource is already loaded
|
||||||
|
const auto search = resource_name_map_.find(name);
|
||||||
|
if (search != resource_name_map_.end()) {
|
||||||
|
// Return resource ID
|
||||||
|
return search->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new resource (without a name) to the registry and return its ID.
|
||||||
|
*/
|
||||||
|
ResourceIDType add_resource(ResourceType&& resource)
|
||||||
|
{
|
||||||
|
// Add resource to end of vector, moving the resource object into a shared_ptr
|
||||||
|
resources_.push_back(
|
||||||
|
std::make_shared<ResourceType>(std::move(resource))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the index of the newly added resource
|
||||||
|
return static_cast<ResourceIDType>(resources_.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new named resource to the registry, associate the name with it and return its ID.
|
||||||
|
*/
|
||||||
|
ResourceIDType add_resource(ResourceType&& resource, const std::string& name)
|
||||||
|
{
|
||||||
|
// Add resource
|
||||||
|
ResourceIDType resource_id = add_resource(std::move(resource));
|
||||||
|
|
||||||
|
// Associate name with resource ID for future access
|
||||||
|
resource_name_map_.emplace(name, resource_id);
|
||||||
|
|
||||||
|
return resource_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a resource by (file) name using the given resource loader and add it to the registry, unless the
|
||||||
|
* resource is already loaded.
|
||||||
|
* Return the resource ID (in both cases).
|
||||||
|
*/
|
||||||
|
template <IsResourceLoader<ResourceType> ResourceLoaderType>
|
||||||
|
ResourceIDType load_resource(const std::string& name, ResourceLoaderType& resource_loader)
|
||||||
|
{
|
||||||
|
// Check if resource is already loaded
|
||||||
|
auto resource_id = find_resource_id_by_name(name);
|
||||||
|
if (resource_id.has_value()) {
|
||||||
|
return resource_id.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load resource and add it to the registry
|
||||||
|
return add_resource(resource_loader.load_resource(name), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a resource if it's not loaded yet (like `load_resource()`), then return a reference to a shared
|
||||||
|
* pointer to the resource (like `get_resource_by_id()`).
|
||||||
|
*/
|
||||||
|
template <IsResourceLoader<ResourceType> ResourceLoaderType>
|
||||||
|
const ResourcePtr<ResourceType>& load_and_get_resource(
|
||||||
|
const std::string& name,
|
||||||
|
ResourceLoaderType& resource_loader
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ResourceIDType resource_id = load_resource(name, resource_loader);
|
||||||
|
return get_resource_by_id(resource_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
12
src/rutile/core/resources/texture.cppm
Normal file
12
src/rutile/core/resources/texture.cppm
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
module;
|
||||||
|
|
||||||
|
export module rutile.core.resources.texture;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import wrappers.sdl;
|
||||||
|
|
||||||
|
export namespace rutile
|
||||||
|
{
|
||||||
|
using TextureID = unsigned int;
|
||||||
|
using Texture = sdl::Texture;
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,11 @@ module;
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
export module rutile.core.drawing.tile_set;
|
export module rutile.core.resources.tile_set;
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
import rutile.core.render_server;
|
import rutile.core.common;
|
||||||
|
import rutile.core.resources.texture;
|
||||||
import wrappers.sdl;
|
import wrappers.sdl;
|
||||||
|
|
||||||
export namespace rutile
|
export namespace rutile
|
||||||
|
|
@ -20,8 +21,8 @@ export namespace rutile
|
||||||
|
|
||||||
class TileSet
|
class TileSet
|
||||||
{
|
{
|
||||||
// Texture ID of tileset atlas texture
|
// Shared pointer to the tileset atlas texture (managed by ResourceServer)
|
||||||
TextureID texture_id_;
|
ResourcePtr<Texture> texture_ptr_;
|
||||||
|
|
||||||
// Size of each tile in pixels
|
// Size of each tile in pixels
|
||||||
// TODO: Do we need this?
|
// TODO: Do we need this?
|
||||||
|
|
@ -35,27 +36,23 @@ export namespace rutile
|
||||||
TileSet() = delete;
|
TileSet() = delete;
|
||||||
|
|
||||||
explicit TileSet(
|
explicit TileSet(
|
||||||
const TextureID texture_id,
|
const ResourcePtr<Texture>& texture_ptr,
|
||||||
std::vector<TileDefinition> tile_definitions
|
std::vector<TileDefinition> tile_definitions
|
||||||
)
|
)
|
||||||
: texture_id_{texture_id},
|
: texture_ptr_{texture_ptr},
|
||||||
tile_definitions_{std::move(tile_definitions)}
|
tile_definitions_{std::move(tile_definitions)}
|
||||||
{}
|
{}
|
||||||
|
|
||||||
static TileSet create_from_texture(
|
static TileSet create_from_texture(
|
||||||
RenderServer& render_server,
|
const ResourcePtr<Texture>& texture_ptr,
|
||||||
const std::string& filename,
|
|
||||||
const int tile_size,
|
const int tile_size,
|
||||||
const std::vector<sdl::Point>& tile_atlas_coords
|
const std::vector<sdl::Point>& tile_atlas_coords
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
const TextureID texture_id = render_server.load_texture(filename);
|
|
||||||
const sdl::Texture& texture = render_server.get_texture(texture_id);
|
|
||||||
|
|
||||||
assert(tile_size > 0);
|
assert(tile_size > 0);
|
||||||
// TODO: These probably shouldn't be asserts...
|
// TODO: These probably shouldn't be asserts...
|
||||||
assert(texture.get_width() % tile_size == 0);
|
assert(texture_ptr->get_width() % tile_size == 0);
|
||||||
assert(texture.get_height() % tile_size == 0);
|
assert(texture_ptr->get_height() % tile_size == 0);
|
||||||
|
|
||||||
std::vector<TileDefinition> tile_definitions;
|
std::vector<TileDefinition> tile_definitions;
|
||||||
|
|
||||||
|
|
@ -71,17 +68,17 @@ export namespace rutile
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: These probably shouldn't be asserts...
|
// TODO: These probably shouldn't be asserts...
|
||||||
assert(tile.atlas_src_rect.x + tile.atlas_src_rect.w <= texture.get_width());
|
assert(tile.atlas_src_rect.x + tile.atlas_src_rect.w <= texture_ptr->get_width());
|
||||||
assert(tile.atlas_src_rect.y + tile.atlas_src_rect.h <= texture.get_height());
|
assert(tile.atlas_src_rect.y + tile.atlas_src_rect.h <= texture_ptr->get_height());
|
||||||
tile_definitions.push_back(tile);
|
tile_definitions.push_back(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TileSet(texture_id, tile_definitions);
|
return TileSet(texture_ptr, tile_definitions);
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr TextureID get_texture_id() const
|
constexpr const Texture& get_texture() const
|
||||||
{
|
{
|
||||||
return texture_id_;
|
return *texture_ptr_;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr const TileDefinition& get_tile_definition(const TileID tile_id) const
|
constexpr const TileDefinition& get_tile_definition(const TileID tile_id) const
|
||||||
|
|
@ -7,7 +7,6 @@ export module rutile.game.game;
|
||||||
import std;
|
import std;
|
||||||
import rutile.core.drawing.sprite;
|
import rutile.core.drawing.sprite;
|
||||||
import rutile.core.drawing.tile_map;
|
import rutile.core.drawing.tile_map;
|
||||||
import rutile.core.drawing.tile_set;
|
|
||||||
import rutile.core.engine;
|
import rutile.core.engine;
|
||||||
import rutile.core.render_server;
|
import rutile.core.render_server;
|
||||||
import wrappers.rmlui.core;
|
import wrappers.rmlui.core;
|
||||||
|
|
@ -24,12 +23,9 @@ export namespace rutile
|
||||||
// Reference to the engine
|
// Reference to the engine
|
||||||
Engine& engine_;
|
Engine& engine_;
|
||||||
|
|
||||||
// Sprites for testing
|
// Sprites and stuff for testing
|
||||||
Sprite player_sprite_;
|
Sprite player_sprite_;
|
||||||
std::vector<Sprite> sprites_;
|
std::vector<Sprite> sprites_;
|
||||||
|
|
||||||
// Tile set and tile map (TODO: tile set should be moved to resource manager in engine)
|
|
||||||
TileSet tile_set_;
|
|
||||||
TileMap tile_map_;
|
TileMap tile_map_;
|
||||||
|
|
||||||
// Private constructor
|
// Private constructor
|
||||||
|
|
@ -37,30 +33,16 @@ export namespace rutile
|
||||||
: engine_(engine),
|
: engine_(engine),
|
||||||
player_sprite_{
|
player_sprite_{
|
||||||
Sprite::create_from_texture(
|
Sprite::create_from_texture(
|
||||||
engine_.get_render_server(),
|
engine_.get_resource_server().load_texture("assets/sprites/neocat_64.png"),
|
||||||
"assets/sprites/neocat_64.png",
|
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
sdl::FPoint{0, 0},
|
sdl::FPoint{0, 0},
|
||||||
sdl::FPoint{-32, -32}
|
sdl::FPoint{-32, -32}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
tile_set_{
|
tile_map_{
|
||||||
TileSet::create_from_texture(
|
engine_.get_resource_server().load_tile_set("assets/tilesets/terrain.json"),
|
||||||
engine_.get_render_server(),
|
32, 16, 12, {50, 50}
|
||||||
"assets/tilesets/terrain.png",
|
|
||||||
32,
|
|
||||||
{
|
|
||||||
// Tile 0: No tile
|
|
||||||
{0, 0},
|
|
||||||
// Tile 1: Actual first tile in the texture
|
|
||||||
{0, 0},
|
|
||||||
{1, 0},
|
|
||||||
{2, 0},
|
|
||||||
{3, 0},
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
},
|
|
||||||
tile_map_(0, 32, 16, 12, {50, 50})
|
|
||||||
{
|
{
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
const auto& ui_server = engine_.get_ui_server();
|
const auto& ui_server = engine_.get_ui_server();
|
||||||
|
|
@ -113,8 +95,7 @@ export namespace rutile
|
||||||
if (event.type == sdl::EventTypes::MouseButtonUp) {
|
if (event.type == sdl::EventTypes::MouseButtonUp) {
|
||||||
sprites_.push_back(
|
sprites_.push_back(
|
||||||
Sprite::create_from_texture(
|
Sprite::create_from_texture(
|
||||||
engine_.get_render_server(),
|
engine_.get_resource_server().load_texture("assets/sprites/neofox_64.png"),
|
||||||
"assets/sprites/neofox_64.png",
|
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
sdl::FPoint{event.motion.x, event.motion.y},
|
sdl::FPoint{event.motion.x, event.motion.y},
|
||||||
sdl::FPoint{-32, -32}
|
sdl::FPoint{-32, -32}
|
||||||
|
|
@ -138,7 +119,7 @@ export namespace rutile
|
||||||
render_server.start_frame();
|
render_server.start_frame();
|
||||||
|
|
||||||
// Render tilemap
|
// Render tilemap
|
||||||
tile_map_.draw(render_server, tile_set_);
|
tile_map_.draw(render_server);
|
||||||
|
|
||||||
// Render sprites
|
// Render sprites
|
||||||
for (const Sprite& sprite : sprites_) {
|
for (const Sprite& sprite : sprites_) {
|
||||||
|
|
|
||||||
51
src/vendor/nlohmann/json.cppm
vendored
Normal file
51
src/vendor/nlohmann/json.cppm
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | JSON for Modern C++
|
||||||
|
// | | |__ | | | | | | version 3.12.0 (develop branch)
|
||||||
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
module;
|
||||||
|
|
||||||
|
// GCC workaround for C++ modules support.
|
||||||
|
// When using C++20 modules, some compilers (particularly GCC) may have issues
|
||||||
|
// with template instantiations in the module preamble. If you encounter
|
||||||
|
// "redefinition" errors when including nlohmann/json.hpp, try one of:
|
||||||
|
// 1. Include nlohmann/json.hpp in your module preamble BEFORE other #includes
|
||||||
|
// 2. Or use: import nlohmann.json; instead of #include <nlohmann/json.hpp>
|
||||||
|
// 3. Or upgrade to a newer GCC version with better modules support.
|
||||||
|
// See: https://github.com/nlohmann/json/issues/5103
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
// NOTE: Module renamed from nlohmann.json to match directory structure
|
||||||
|
export module vendor.nlohmann.json;
|
||||||
|
|
||||||
|
export NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::adl_serializer;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::basic_json;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::json;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::json_pointer;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::ordered_json;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::ordered_map;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::to_string;
|
||||||
|
|
||||||
|
inline namespace literals
|
||||||
|
{
|
||||||
|
inline namespace json_literals
|
||||||
|
{
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::literals::json_literals::operator""_json;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::literals::json_literals::operator""_json_pointer;
|
||||||
|
} // namespace json_literals
|
||||||
|
} // namespace literals
|
||||||
|
|
||||||
|
// Note: the following nlohmann::detail symbols must be exported due to
|
||||||
|
// an MSVC bug failing to compile without these symbols visible (ticket #3970)
|
||||||
|
namespace detail
|
||||||
|
{
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_callback_parser;
|
||||||
|
using NLOHMANN_JSON_NAMESPACE::detail::unknown_size;
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
NLOHMANN_JSON_NAMESPACE_END
|
||||||
|
|
@ -56,7 +56,8 @@ export namespace sdl
|
||||||
*/
|
*/
|
||||||
std::pair<int, int> get_size() const
|
std::pair<int, int> get_size() const
|
||||||
{
|
{
|
||||||
int width, height;
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
if (!SDL_GetWindowSize(get_raw(), &width, &height)) {
|
if (!SDL_GetWindowSize(get_raw(), &width, &height)) {
|
||||||
throw SDLException("SDL_GetWindowSize");
|
throw SDLException("SDL_GetWindowSize");
|
||||||
|
|
|
||||||
13
vendor/README.md
vendored
13
vendor/README.md
vendored
|
|
@ -10,6 +10,19 @@ For now, SDL3 and SDL3_image need to be installed on the system. They should be
|
||||||
Same for freetype, which is required by RmlUi.
|
Same for freetype, which is required by RmlUi.
|
||||||
|
|
||||||
|
|
||||||
|
## nlohmann::json
|
||||||
|
|
||||||
|
[JSON for Modern C++](https://github.com/nlohmann/json).
|
||||||
|
|
||||||
|
Within the `vendor` directory, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# We're using the default branch (develop) here because the latest release does not support C++20 modules yet.
|
||||||
|
# (Last commit known working: 484483aca)
|
||||||
|
git clone git@github.com:nlohmann/json.git nlohmann_json
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## RmlUi
|
## RmlUi
|
||||||
|
|
||||||
[RmlUi](https://github.com/mikke89/RmlUi) is a UI library.
|
[RmlUi](https://github.com/mikke89/RmlUi) is a UI library.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue