From 1be770d62db625684def3bd28f834cbb86b503d7 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Mon, 21 Apr 2025 23:45:04 -0400 Subject: [PATCH] GUECS: Minimal components from zedcaster that will let me make a GUI for a game. --- Makefile | 9 +- color.hpp | 15 ++ config.cpp | 35 +++ config.hpp | 19 ++ constants.hpp | 28 +++ dbc.cpp | 35 +-- dbc.hpp | 35 ++- dinkyecs.hpp | 214 +++++++++++++++++ events.hpp | 7 + guecs.cpp | 310 +++++++++++++++++++++++++ guecs.hpp | 235 +++++++++++++++++++ lel.cpp | 117 ++++++++++ lel.hpp | 57 +++++ lel_parser.cpp | 263 +++++++++++++++++++++ lel_parser.rl | 68 ++++++ matrix.cpp | 31 +++ matrix.hpp | 49 ++++ meson.build | 17 +- point.hpp | 20 ++ rand.cpp | 6 + rand.hpp | 28 +++ shaders.cpp | 77 +++++++ shaders.hpp | 28 +++ shiterator.hpp | 607 +++++++++++++++++++++++++++++++++++++++++++++++++ sound.cpp | 82 +++++++ sound.hpp | 26 +++ textures.cpp | 99 ++++++++ textures.hpp | 39 ++++ 28 files changed, 2527 insertions(+), 29 deletions(-) create mode 100644 color.hpp create mode 100644 config.cpp create mode 100644 config.hpp create mode 100644 constants.hpp create mode 100644 dinkyecs.hpp create mode 100644 events.hpp create mode 100644 guecs.cpp create mode 100644 guecs.hpp create mode 100644 lel.cpp create mode 100644 lel.hpp create mode 100644 lel_parser.cpp create mode 100644 lel_parser.rl create mode 100644 matrix.cpp create mode 100644 matrix.hpp create mode 100644 point.hpp create mode 100644 rand.cpp create mode 100644 rand.hpp create mode 100644 shaders.cpp create mode 100644 shaders.hpp create mode 100644 shiterator.hpp create mode 100644 sound.cpp create mode 100644 sound.hpp create mode 100644 textures.cpp create mode 100644 textures.hpp diff --git a/Makefile b/Makefile index 0f0c968..32cbe3c 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,10 @@ reset: patch: powershell "cp ./patches/process.h ./subprojects/libgit2-1.9.0/src/util/process.h" -build: +%.cpp : %.rl + ragel -o $@ $< + +build: lel_parser.cpp meson compile -C builddir config: @@ -19,10 +22,10 @@ test: build install: build test powershell "cp ./builddir/subprojects/libgit2-1.9.0/liblibgit2package.dll ." powershell "cp ./builddir/subprojects/efsw/libefsw.dll ." - powershell "cp builddir/escape_turings_tarpit.exe ." + powershell "cp builddir/ttpit.exe ." run: install - ./escape_turings_tarpit.exe + ./ttpit.exe clean: meson compile --clean -C builddir diff --git a/color.hpp b/color.hpp new file mode 100644 index 0000000..4026ba5 --- /dev/null +++ b/color.hpp @@ -0,0 +1,15 @@ +#pragma once +#include + +namespace ColorValue { + const sf::Color BLACK{0, 0, 0}; + const sf::Color DARK_DARK{10, 10, 10}; + const sf::Color DARK_MID{30, 30, 30}; + const sf::Color DARK_LIGHT{60, 60, 60}; + const sf::Color MID{100, 100, 100}; + const sf::Color LIGHT_DARK{150, 150, 150}; + const sf::Color LIGHT_MID{200, 200, 200}; + const sf::Color LIGHT_LIGHT{230, 230, 230}; + const sf::Color WHITE{255, 255, 255}; + const sf::Color TRANSPARENT = sf::Color::Transparent; +} diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..ea3ef62 --- /dev/null +++ b/config.cpp @@ -0,0 +1,35 @@ +#include "config.hpp" +#include "dbc.hpp" +#include + +using nlohmann::json; +using fmt::format; + +Config::Config(const std::string src_path) : $src_path(src_path) { + std::ifstream infile($src_path); + $config = json::parse(infile); +} + +json &Config::operator[](const std::string &key) { + dbc::check($config.contains(key), fmt::format("ERROR in config, key {} doesn't exist.", key)); + return $config[key]; +} + +std::wstring Config::wstring(const std::string main_key, const std::string sub_key) { + dbc::check($config.contains(main_key), fmt::format("ERROR wstring main/key in config, main_key {} doesn't exist.", main_key)); + dbc::check($config[main_key].contains(sub_key), fmt::format("ERROR wstring in config, main_key/key {}/{} doesn't exist.", main_key, sub_key)); + + const std::string& str_val = $config[main_key][sub_key]; + std::wstring_convert> $converter; + return $converter.from_bytes(str_val); +} + +std::vector Config::keys() { + std::vector the_fucking_keys; + + for(auto& [key, value] : $config.items()) { + the_fucking_keys.push_back(key); + } + + return the_fucking_keys; +} diff --git a/config.hpp b/config.hpp new file mode 100644 index 0000000..52bf664 --- /dev/null +++ b/config.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include + +struct Config { + nlohmann::json $config; + std::string $src_path; + + Config(const std::string src_path); + + Config(nlohmann::json config, std::string src_path) + : $config(config), $src_path(src_path) {} + + nlohmann::json &operator[](const std::string &key); + nlohmann::json &json() { return $config; }; + std::wstring wstring(const std::string main_key, const std::string sub_key); + std::vector keys(); +}; diff --git a/constants.hpp b/constants.hpp new file mode 100644 index 0000000..8c653dd --- /dev/null +++ b/constants.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "color.hpp" +#include + +constexpr const int TEXTURE_WIDTH=256; +constexpr const int TEXTURE_HEIGHT=256; +constexpr const int SCREEN_WIDTH=1280; +constexpr const int SCREEN_HEIGHT=720; + +constexpr const bool VSYNC=false; +constexpr const int FRAME_LIMIT=60; + +constexpr const int GUECS_PADDING = 3; +constexpr const int GUECS_BORDER_PX = 1; +constexpr const int GUECS_FONT_SIZE = 30; +const sf::Color GUECS_FILL_COLOR = ColorValue::DARK_MID; +const sf::Color GUECS_TEXT_COLOR = ColorValue::LIGHT_LIGHT; +const sf::Color GUECS_BG_COLOR = ColorValue::MID; +const sf::Color GUECS_BORDER_COLOR = ColorValue::MID; +constexpr const char *FONT_FILE_NAME="assets/text.otf"; + +#ifdef NDEBUG +constexpr const bool DEBUG_BUILD=false; +#else +constexpr const bool DEBUG_BUILD=true; +#endif diff --git a/dbc.cpp b/dbc.cpp index c25d32a..6b17faf 100644 --- a/dbc.cpp +++ b/dbc.cpp @@ -1,40 +1,47 @@ #include "dbc.hpp" +#include -void dbc::log(const string &message) { - fmt::print("{}\n", message); +void dbc::log(const string &message, const std::source_location location) { + std::cout << '[' << location.file_name() << ':' + << location.line() << "|" + << location.function_name() << "] " + << message << std::endl; } -void dbc::sentinel(const string &message) { - string err = fmt::format("[SENTINEL!] {}\n", message); +void dbc::sentinel(const string &message, const std::source_location location) { + string err = fmt::format("[SENTINEL!] {}", message); + dbc::log(err, location); throw dbc::SentinelError{err}; } -void dbc::pre(const string &message, bool test) { +void dbc::pre(const string &message, bool test, const std::source_location location) { if(!test) { - string err = fmt::format("[PRE!] {}\n", message); + string err = fmt::format("[PRE!] {}", message); + dbc::log(err, location); throw dbc::PreCondError{err}; } } -void dbc::pre(const string &message, std::function tester) { - dbc::pre(message, tester()); +void dbc::pre(const string &message, std::function tester, const std::source_location location) { + dbc::pre(message, tester(), location); } -void dbc::post(const string &message, bool test) { +void dbc::post(const string &message, bool test, const std::source_location location) { if(!test) { - string err = fmt::format("[POST!] {}\n", message); + string err = fmt::format("[POST!] {}", message); + dbc::log(err, location); throw dbc::PostCondError{err}; } } -void dbc::post(const string &message, std::function tester) { - dbc::post(message, tester()); +void dbc::post(const string &message, std::function tester, const std::source_location location) { + dbc::post(message, tester(), location); } -void dbc::check(bool test, const string &message) { +void dbc::check(bool test, const string &message, const std::source_location location) { if(!test) { string err = fmt::format("[CHECK!] {}\n", message); - fmt::println("{}", err); + dbc::log(err, location); throw dbc::CheckError{err}; } } diff --git a/dbc.hpp b/dbc.hpp index 919d729..87e4fb0 100644 --- a/dbc.hpp +++ b/dbc.hpp @@ -3,6 +3,7 @@ #include #include #include +#include using std::string; @@ -19,11 +20,31 @@ namespace dbc { class PreCondError : public Error {}; class PostCondError : public Error {}; - void log(const string &message); - void sentinel(const string &message); - void pre(const string &message, bool test); - void pre(const string &message, std::function tester); - void post(const string &message, bool test); - void post(const string &message, std::function tester); - void check(bool test, const string &message); + void log(const string &message, + const std::source_location location = + std::source_location::current()); + + [[noreturn]] void sentinel(const string &message, + const std::source_location location = + std::source_location::current()); + + void pre(const string &message, bool test, + const std::source_location location = + std::source_location::current()); + + void pre(const string &message, std::function tester, + const std::source_location location = + std::source_location::current()); + + void post(const string &message, bool test, + const std::source_location location = + std::source_location::current()); + + void post(const string &message, std::function tester, + const std::source_location location = + std::source_location::current()); + + void check(bool test, const string &message, + const std::source_location location = + std::source_location::current()); } diff --git a/dinkyecs.hpp b/dinkyecs.hpp new file mode 100644 index 0000000..1434558 --- /dev/null +++ b/dinkyecs.hpp @@ -0,0 +1,214 @@ +#pragma once + +#include "dbc.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DinkyECS +{ + typedef unsigned long Entity; + + using EntityMap = std::unordered_map; + + template + struct ComponentStorage { + std::vector data; + std::queue free_indices; + }; + + struct Event { + int event = 0; + Entity entity = 0; + std::any data; + }; + + typedef std::queue EventQueue; + + struct World { + unsigned long entity_count = 0; + std::unordered_map $components; + std::unordered_map $facts; + std::unordered_map $events; + std::unordered_map $component_storages; + std::vector $constants; + + Entity entity() { return ++entity_count; } + + void clone_into(DinkyECS::World &to_world) { + to_world.$constants = $constants; + to_world.$facts = $facts; + to_world.entity_count = entity_count; + to_world.$component_storages = $component_storages; + + for(auto eid : $constants) { + for(const auto &[tid, eid_map] : $components) { + auto &their_map = to_world.$components[tid]; + if(eid_map.contains(eid)) { + their_map.insert_or_assign(eid, eid_map.at(eid)); + } + } + } + } + + void make_constant(DinkyECS::Entity entity) { + $constants.push_back(entity); + } + + template + size_t make_component() { + auto &storage = component_storage_for(); + size_t index; + + if(!storage.free_indices.empty()) { + index = storage.free_indices.front(); + storage.free_indices.pop(); + } else { + storage.data.emplace_back(); + index = storage.data.size() - 1; + } + + return index; + } + + template + ComponentStorage &component_storage_for() { + auto type_index = std::type_index(typeid(Comp)); + $component_storages.try_emplace(type_index, ComponentStorage{}); + return std::any_cast &>( + $component_storages.at(type_index)); + } + + template + EntityMap &entity_map_for() { + return $components[std::type_index(typeid(Comp))]; + } + + template + EventQueue &queue_map_for() { + return $events[std::type_index(typeid(Comp))]; + } + + template + void remove(Entity ent) { + EntityMap &map = entity_map_for(); + + if(map.contains(ent)) { + size_t index = map.at(ent); + component_storage_for().free_indices.push(index); + } + + map.erase(ent); + } + + template + void set_the(Comp val) { + $facts.insert_or_assign(std::type_index(typeid(Comp)), val); + } + + template + Comp &get_the() { + auto comp_id = std::type_index(typeid(Comp)); + dbc::check($facts.contains(comp_id), + fmt::format("!!!! ATTEMPT to access world fact that hasn't " + "been set yet: {}", + typeid(Comp).name())); + + // use .at to get std::out_of_range if fact not set + std::any &res = $facts.at(comp_id); + return std::any_cast(res); + } + + template + bool has_the() { + auto comp_id = std::type_index(typeid(Comp)); + return $facts.contains(comp_id); + } + + template + void set(Entity ent, Comp val) { + EntityMap &map = entity_map_for(); + + if(has(ent)) { + get(ent) = val; + return; + } + + map.insert_or_assign(ent, make_component()); + get(ent) = val; + } + + template + Comp &get(Entity ent) { + EntityMap &map = entity_map_for(); + auto &storage = component_storage_for(); + auto index = map.at(ent); + return storage.data[index]; + } + + template + bool has(Entity ent) { + EntityMap &map = entity_map_for(); + return map.contains(ent); + } + + template + void query(std::function cb) { + EntityMap &map = entity_map_for(); + + for(auto &[entity, index] : map) { + cb(entity, get(entity)); + } + } + + template + void query(std::function cb) { + EntityMap &map_a = entity_map_for(); + EntityMap &map_b = entity_map_for(); + + for(auto &[entity, index_a] : map_a) { + if(map_b.contains(entity)) { + cb(entity, get(entity), get(entity)); + } + } + } + + template + void send(Comp event, Entity entity, std::any data) { + EventQueue &queue = queue_map_for(); + queue.push({event, entity, data}); + } + + template + Event recv() { + EventQueue &queue = queue_map_for(); + Event evt = queue.front(); + queue.pop(); + return evt; + } + + template + bool has_event() { + EventQueue &queue = queue_map_for(); + return !queue.empty(); + } + + /* std::optional can't do references. Don't try it! + * Actually, this sucks, either delete it or have it + * return pointers (assuming optional can handle pointers) + */ + template + std::optional get_if(DinkyECS::Entity entity) { + if(has(entity)) { + return std::make_optional(get(entity)); + } else { + return std::nullopt; + } + } + }; +} // namespace DinkyECS diff --git a/events.hpp b/events.hpp new file mode 100644 index 0000000..984b732 --- /dev/null +++ b/events.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace Events { + enum GUI { + START, NOOP + }; +} diff --git a/guecs.cpp b/guecs.cpp new file mode 100644 index 0000000..360fe6b --- /dev/null +++ b/guecs.cpp @@ -0,0 +1,310 @@ +#include "guecs.hpp" +#include "shaders.hpp" +#include "sound.hpp" + +namespace guecs { + + void Textual::init(lel::Cell &cell, shared_ptr font_ptr) { + dbc::check(font_ptr != nullptr, "you failed to initialize this WideText"); + if(font == nullptr) font = font_ptr; + if(text == nullptr) text = make_shared(*font, content, size); + text->setFillColor(color); + + if(centered) { + auto bounds = text->getLocalBounds(); + auto text_cell = lel::center(bounds.size.x, bounds.size.y, cell); + // this stupid / 2 is because SFML renders from baseline rather than from the claimed bounding box + text->setPosition({float(text_cell.x), float(text_cell.y) - text_cell.h / 2}); + } else { + text->setPosition({float(cell.x + padding * 2), float(cell.y + padding * 2)}); + } + + text->setCharacterSize(size); + } + + void Textual::update(std::wstring& new_content) { + content = new_content; + text->setString(content); + } + + void Sprite::init(lel::Cell &cell) { + auto sprite_texture = textures::get(name); + + sprite = make_shared( + *sprite_texture.texture, + sprite_texture.sprite->getTextureRect()); + + sprite->setPosition({ + float(cell.x + padding), + float(cell.y + padding)}); + + auto bounds = sprite->getLocalBounds(); + + sprite->setScale({ + float(cell.w - padding * 2) / bounds.size.x, + float(cell.h - padding * 2) / bounds.size.y}); + } + + void Rectangle::init(lel::Cell& cell) { + sf::Vector2f size{float(cell.w) - padding * 2, float(cell.h) - padding * 2}; + if(shape == nullptr) shape = make_shared(size); + shape->setPosition({float(cell.x + padding), float(cell.y + padding)}); + shape->setFillColor(color); + shape->setOutlineColor(border_color); + shape->setOutlineThickness(border_px); + } + + + void Meter::init(lel::Cell& cell) { + bar.init(cell); + } + + void Sound::play(bool hover) { + if(!hover) { + sound::play(on_click); + } + } + + void Background::init() { + sf::Vector2f size{float(w), float(h)}; + if(shape == nullptr) shape = make_shared(size); + shape->setPosition({float(x), float(y)}); + shape->setFillColor(color); + } + + void Effect::init(lel::Cell &cell) { + $shader_version = shaders::version(); + $shader = shaders::get(name); + $shader->setUniform("u_resolution", sf::Vector2f({float(cell.w), float(cell.h)})); + $clock = std::make_shared(); + } + + void Effect::step() { + sf::Time cur_time = $clock->getElapsedTime(); + float u_time = cur_time.asSeconds(); + + if(u_time < $u_time_end) { + $shader->setUniform("u_duration", duration); + $shader->setUniform("u_time_end", $u_time_end); + $shader->setUniform("u_time", u_time); + } else { + $active = false; + } + } + + void Effect::run() { + $active = true; + sf::Time u_time = $clock->getElapsedTime(); + $u_time_end = u_time.asSeconds() + duration; + } + + shared_ptr Effect::checkout_ptr() { + if(shaders::updated($shader_version)) { + $shader = shaders::get(name); + $shader_version = shaders::version(); + } + + return $shader; + } + + UI::UI() { + $font = make_shared(FONT_FILE_NAME); + } + + void UI::position(int x, int y, int width, int height) { + $parser.position(x, y, width, height); + } + + void UI::layout(std::string grid) { + $grid = grid; + bool good = $parser.parse($grid); + dbc::check(good, "LEL parsing failed."); + + for(auto& [name, cell] : $parser.cells) { + auto ent = init_entity(name); + $world.set(ent, cell); + } + } + + DinkyECS::Entity UI::init_entity(std::string name) { + auto entity = $world.entity(); + // this lets you look up an entity by name + $name_ents.insert_or_assign(name, entity); + // this makes it easier to get the name during querying + $world.set(entity, {name}); + return entity; + } + + DinkyECS::Entity UI::entity(std::string name) { + dbc::check($name_ents.contains(name), + fmt::format("GUECS entity {} does not exist. Forgot to init_entity?", name)); + return $name_ents.at(name); + } + + void UI::init() { + if($world.has_the()) { + auto& bg = $world.get_the(); + bg.init(); + } + + $world.query([](auto, auto& bg) { + bg.init(); + }); + + $world.query([](auto, auto& cell, auto& rect) { + rect.init(cell); + }); + + $world.query([](auto, auto& cell, auto& shader) { + shader.init(cell); + }); + + $world.query([](auto, auto& bg, auto &) { + bg.shape->setFillColor(ColorValue::BLACK); + }); + + $world.query([](auto, auto &cell, auto& meter) { + meter.init(cell); + }); + + $world.query([this](auto, auto& cell, auto& text) { + text.init(cell, $font); + }); + + $world.query([this](auto, auto& cell, auto& text) { + text.init(cell, $font); + }); + + $world.query([&](auto, auto &cell, auto &sprite) { + sprite.init(cell); + }); + } + + void UI::debug_layout(sf::RenderWindow& window) { + $world.query([&](const auto, auto &cell) { + sf::RectangleShape rect{{float(cell.w), float(cell.h)}}; + rect.setPosition({float(cell.x), float(cell.y)}); + rect.setFillColor(sf::Color::Transparent); + rect.setOutlineColor(sf::Color::Red); + rect.setOutlineThickness(2.0f); + window.draw(rect); + }); + } + + void UI::render(sf::RenderWindow& window) { + if($world.has_the()) { + auto& bg = $world.get_the(); + window.draw(*bg.shape); + } + + $world.query([&](auto, auto& shader) { + if(shader.$active) shader.step(); + }); + + $world.query([&](auto ent, auto& rect) { + render_helper(window, ent, true, rect.shape); + }); + + $world.query([&](auto ent, auto& cell, const auto &meter) { + float level = std::clamp(meter.percent, 0.0f, 1.0f) * float(cell.w); + // ZED: this 6 is a border width, make it a thing + meter.bar.shape->setSize({std::max(level, 0.0f), float(cell.h - 6)}); + render_helper(window, ent, true, meter.bar.shape); + }); + + $world.query([&](auto ent, auto& sprite) { + render_helper(window, ent, false, sprite.sprite); + }); + + $world.query