diff --git a/.gitignore b/.gitignore index ec2481e..ca37570 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,13 @@ tags # Persistent undo [._]*.un~ +subprojects +builddir +ttassets +backup +*.exe +*.dll +*.world +coverage +coverage/* +.venv diff --git a/.vimrc_proj b/.vimrc_proj new file mode 100644 index 0000000..2b745b4 --- /dev/null +++ b/.vimrc_proj @@ -0,0 +1 @@ +set makeprg=meson\ compile\ -C\ . diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fa86918 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +all: build test + +reset: +ifeq '$(OS)' 'Windows_NT' + powershell -executionpolicy bypass .\scripts\reset_build.ps1 +else + sh -x ./scripts/reset_build.sh +endif + +%.cpp : %.rl + ragel -o $@ $< + +build: lel_parser.cpp + meson compile -j 10 -C builddir + +release_build: + meson --wipe builddir -Db_ndebug=true --buildtype release + meson compile -j 10 -C builddir + +debug_build: + meson setup --wipe builddir -Db_ndebug=true --buildtype debugoptimized + meson compile -j 10 -C builddir + +tracy_build: + meson setup --wipe builddir --buildtype debugoptimized -Dtracy_enable=true -Dtracy:on_demand=true + meson compile -j 10 -C builddir + +test: build + ./builddir/runtests + +run: build test +ifeq '$(OS)' 'Windows_NT' + powershell "cp ./builddir/calculator.exe ." + ./calculator +else + ./builddir/calculator +endif + +debug: build + gdb --nx -x .gdbinit --ex run --args builddir/calculator + +debug_run: build + gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/calculator + +debug_walk: build test + gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/calculator t + +clean: + meson compile --clean -C builddir + +debug_test: build + gdb --nx -x .gdbinit --ex run --args builddir/runtests -e + +win_installer: + powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp' + +coverage_report: + powershell 'scripts/coverage_report.ps1' diff --git a/dbc.cpp b/dbc.cpp new file mode 100644 index 0000000..6b17faf --- /dev/null +++ b/dbc.cpp @@ -0,0 +1,47 @@ +#include "dbc.hpp" +#include + +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, 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, const std::source_location location) { + if(!test) { + string err = fmt::format("[PRE!] {}", message); + dbc::log(err, location); + throw dbc::PreCondError{err}; + } +} + +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, const std::source_location location) { + if(!test) { + string err = fmt::format("[POST!] {}", message); + dbc::log(err, location); + throw dbc::PostCondError{err}; + } +} + +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, const std::source_location location) { + if(!test) { + string err = fmt::format("[CHECK!] {}\n", message); + dbc::log(err, location); + throw dbc::CheckError{err}; + } +} diff --git a/dbc.hpp b/dbc.hpp new file mode 100644 index 0000000..87e4fb0 --- /dev/null +++ b/dbc.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +using std::string; + +namespace dbc { + class Error { + public: + const string message; + Error(string m) : message{m} {} + Error(const char *m) : message{m} {} + }; + + class CheckError : public Error {}; + class SentinelError : public Error {}; + class PreCondError : public Error {}; + class PostCondError : public Error {}; + + 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/guecs.cpp b/guecs.cpp new file mode 100644 index 0000000..22dba4b --- /dev/null +++ b/guecs.cpp @@ -0,0 +1,354 @@ +#include "guecs.hpp" +#include "shaders.hpp" +#include "sound.hpp" +#include "textures.hpp" +#include + +namespace guecs { + using std::make_shared; + + 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(const wstring& new_content) { + content = new_content; + text->setString(content); + } + + void Sprite::update(const string& new_name) { + if(new_name != name) { + name = new_name; + auto sprite_texture = textures::get(name); + sprite->setTexture(*sprite_texture.texture); + sprite->setTextureRect(sprite_texture.sprite->getTextureRect()); + } + } + + 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 Meter::render(lel::Cell& cell) { + float level = std::clamp(percent, 0.0f, 1.0f) * float(cell.w); + // ZED: this 6 is a border width, make it a thing + bar.shape->setSize({std::max(level, 0.0f), float(cell.h - 6)}); + } + + void Sound::play(bool hover) { + if(!hover) { + sound::play(on_click); + } + } + + void Sound::stop(bool hover) { + if(!hover) { + sound::stop(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; + } + + void Effect::stop() { + $active = false; + } + + 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); + } + + sf::Vector2f UI::get_position() { + return {float($parser.grid_x), float($parser.grid_y)}; + } + + sf::Vector2f UI::get_size() { + return {float($parser.grid_w), float($parser.grid_h)}; + } + + void UI::layout(const 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); + set(ent, cell); + } + } + + Entity UI::init_entity(const string& name) { + auto ent = entity(); + // this lets you look up an entity by name + $name_ents.insert_or_assign(name, ent); + // this makes it easier to get the name during querying + set(ent, {name}); + return ent; + } + + Entity UI::entity(const string& name) { + dbc::check($name_ents.contains(name), + fmt::format("GUECS entity {} does not exist. Mispelled cell name?", name)); + return $name_ents.at(name); + } + + Entity UI::entity(const string& name, int id) { + return entity(fmt::format("{}{}", name, id)); + } + + void UI::init() { + query([](auto, auto& bg) { + bg.init(); + }); + + query([](auto, auto& cell, auto& rect) { + rect.init(cell); + }); + + query([](auto, auto& cell, auto& shader) { + shader.init(cell); + }); + + query([](auto, auto& bg, auto &meter) { + bg.shape->setFillColor(meter.color); + }); + + query([](auto, auto &cell, auto& meter) { + meter.init(cell); + }); + + query([this](auto, auto& cell, auto& text) { + text.init(cell, $font); + }); + + query([this](auto, auto& cell, auto& text) { + text.init(cell, $font); + }); + + query([&](auto, auto &cell, auto &sprite) { + sprite.init(cell); + }); + } + + void UI::debug_layout(sf::RenderWindow& window) { + 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(auto bg = get_if(MAIN)) { + window.draw(*(bg->shape)); + } + + query([&](auto, auto& shader) { + if(shader.$active) shader.step(); + }); + + query([&](auto ent, auto& rect) { + render_helper(window, ent, true, rect.shape); + }); + + query([&](auto ent, auto& cell, auto &meter) { + meter.render(cell); + render_helper(window, ent, true, meter.bar.shape); + }); + + query([&](auto ent, auto& sprite) { + render_helper(window, ent, false, sprite.sprite); + }); + + query