mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
Compare commits
3 Commits
54d5f85538
...
5b75ff28e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b75ff28e0 | |||
| e9e371739b | |||
| 28411535bb |
@@ -2,4 +2,4 @@ idf_component_register()
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE cbsdk)
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE cardboy_sdk cardboy_sdk)
|
||||
@@ -1,10 +1,9 @@
|
||||
idf_component_register(SRCS
|
||||
src/app_main.cpp
|
||||
src/app_system.cpp
|
||||
src/apps/menu_app.cpp
|
||||
src/apps/clock_app.cpp
|
||||
src/apps/tetris_app.cpp
|
||||
src/apps/gameboy_app.cpp
|
||||
../sdk/apps/menu_app.cpp
|
||||
../sdk/apps/clock_app.cpp
|
||||
../sdk/apps/tetris_app.cpp
|
||||
../sdk/apps/gameboy_app.cpp
|
||||
src/display.cpp
|
||||
src/bat_mon.cpp
|
||||
src/spi_global.cpp
|
||||
@@ -16,8 +15,9 @@ idf_component_register(SRCS
|
||||
src/power_helper.cpp
|
||||
src/buzzer.cpp
|
||||
src/fs_helper.cpp
|
||||
../sdk/src/app_system.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
|
||||
INCLUDE_DIRS "include"
|
||||
INCLUDE_DIRS "include" "../sdk/include"
|
||||
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
|
||||
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
@@ -1,118 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_platform.hpp"
|
||||
#include "input_state.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
class AppSystem;
|
||||
|
||||
using AppTimerHandle = std::uint32_t;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = 0;
|
||||
|
||||
enum class AppEventType {
|
||||
Button,
|
||||
Timer,
|
||||
};
|
||||
|
||||
struct AppButtonEvent {
|
||||
InputState current{};
|
||||
InputState previous{};
|
||||
};
|
||||
|
||||
struct AppTimerEvent {
|
||||
AppTimerHandle handle = kInvalidAppTimer;
|
||||
};
|
||||
|
||||
struct AppEvent {
|
||||
AppEventType type;
|
||||
std::uint32_t timestamp_ms = 0;
|
||||
AppButtonEvent button{};
|
||||
AppTimerEvent timer{};
|
||||
};
|
||||
using AppEventType = cardboy::sdk::AppEventType;
|
||||
using AppButtonEvent = cardboy::sdk::AppButtonEvent;
|
||||
using AppTimerEvent = cardboy::sdk::AppTimerEvent;
|
||||
using AppEvent = cardboy::sdk::AppEvent;
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
struct BasicAppContext {
|
||||
using Framebuffer = FramebufferT;
|
||||
using Input = InputT;
|
||||
using Clock = ClockT;
|
||||
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
|
||||
|
||||
BasicAppContext() = delete;
|
||||
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
using AppContext = cardboy::sdk::AppContext;
|
||||
|
||||
FramebufferT& framebuffer;
|
||||
InputT& input;
|
||||
ClockT& clock;
|
||||
AppSystem* system = nullptr;
|
||||
|
||||
void requestAppSwitchByIndex(std::size_t index) {
|
||||
pendingAppIndex = index;
|
||||
pendingAppName.clear();
|
||||
pendingSwitchByName = false;
|
||||
pendingSwitch = true;
|
||||
}
|
||||
|
||||
void requestAppSwitchByName(std::string_view name) {
|
||||
pendingAppName.assign(name.begin(), name.end());
|
||||
pendingSwitchByName = true;
|
||||
pendingSwitch = true;
|
||||
}
|
||||
|
||||
bool hasPendingAppSwitch() const { return pendingSwitch; }
|
||||
|
||||
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat = false) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(delay_ms, repeat);
|
||||
}
|
||||
|
||||
AppTimerHandle scheduleRepeatingTimer(uint32_t interval_ms) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(interval_ms, true);
|
||||
}
|
||||
|
||||
void cancelTimer(AppTimerHandle handle) {
|
||||
if (!system)
|
||||
return;
|
||||
cancelTimerInternal(handle);
|
||||
}
|
||||
|
||||
void cancelAllTimers() {
|
||||
if (!system)
|
||||
return;
|
||||
cancelAllTimersInternal();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class AppSystem;
|
||||
bool pendingSwitch = false;
|
||||
bool pendingSwitchByName = false;
|
||||
std::size_t pendingAppIndex = 0;
|
||||
std::string pendingAppName;
|
||||
|
||||
AppTimerHandle scheduleTimerInternal(uint32_t delay_ms, bool repeat);
|
||||
void cancelTimerInternal(AppTimerHandle handle);
|
||||
void cancelAllTimersInternal();
|
||||
};
|
||||
|
||||
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
|
||||
|
||||
class IApp {
|
||||
public:
|
||||
virtual ~IApp() = default;
|
||||
virtual void onStart() {}
|
||||
virtual void onStop() {}
|
||||
virtual void handleEvent(const AppEvent& event) = 0;
|
||||
};
|
||||
|
||||
class IAppFactory {
|
||||
public:
|
||||
virtual ~IAppFactory() = default;
|
||||
virtual const char* name() const = 0;
|
||||
virtual std::unique_ptr<IApp> create(AppContext& context) = 0;
|
||||
};
|
||||
using IApp = cardboy::sdk::IApp;
|
||||
using IAppFactory = cardboy::sdk::IAppFactory;
|
||||
using Services = cardboy::sdk::Services;
|
||||
using IBuzzer = cardboy::sdk::IBuzzer;
|
||||
using IBatteryMonitor = cardboy::sdk::IBatteryMonitor;
|
||||
using IStorage = cardboy::sdk::IStorage;
|
||||
using IRandom = cardboy::sdk::IRandom;
|
||||
using IHighResClock = cardboy::sdk::IHighResClock;
|
||||
using IPowerManager = cardboy::sdk::IPowerManager;
|
||||
using IFilesystem = cardboy::sdk::IFilesystem;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "config.hpp"
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <buttons.hpp>
|
||||
#include <disp_tools.hpp>
|
||||
@@ -10,28 +11,32 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class PlatformFramebuffer {
|
||||
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
int width() const { return DISP_WIDTH; }
|
||||
int height() const { return DISP_HEIGHT; }
|
||||
int width() const override { return cardboy::sdk::kDisplayWidth; }
|
||||
int height() const override { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void drawPixel(int x, int y, bool on) {
|
||||
void drawPixel(int x, int y, bool on) override {
|
||||
if (x < 0 || y < 0 || x >= width() || y >= height())
|
||||
return;
|
||||
DispTools::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void clear(bool on) {
|
||||
void clear(bool on) override {
|
||||
for (int y = 0; y < height(); ++y)
|
||||
for (int x = 0; x < width(); ++x)
|
||||
DispTools::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void beginFrame() override { DispTools::draw_to_display_async_wait(); }
|
||||
void endFrame() override { DispTools::draw_to_display_async_start(); }
|
||||
bool isFrameInFlight() const override { return DispTools::draw_to_display_async_busy(); }
|
||||
};
|
||||
|
||||
class PlatformInput {
|
||||
class PlatformInput final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
InputState readState() {
|
||||
InputState state{};
|
||||
cardboy::sdk::InputState readState() override {
|
||||
cardboy::sdk::InputState state{};
|
||||
const uint8_t pressed = Buttons::get().get_pressed();
|
||||
if (pressed & BTN_UP)
|
||||
state.up = true;
|
||||
@@ -53,12 +58,16 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class PlatformClock {
|
||||
class PlatformClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
uint32_t millis() {
|
||||
std::uint32_t millis() override {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<uint32_t>((static_cast<uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void sleep_ms(uint32_t ms) { PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); }
|
||||
void sleep_ms(std::uint32_t ms) override {
|
||||
if (ms == 0)
|
||||
return;
|
||||
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,79 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class AppSystem {
|
||||
public:
|
||||
explicit AppSystem(AppContext context);
|
||||
|
||||
void registerApp(std::unique_ptr<IAppFactory> factory);
|
||||
bool startApp(const std::string& name);
|
||||
bool startAppByIndex(std::size_t index);
|
||||
|
||||
void run();
|
||||
|
||||
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
|
||||
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
|
||||
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
|
||||
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
|
||||
|
||||
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
|
||||
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
||||
|
||||
private:
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
friend struct BasicAppContext;
|
||||
|
||||
struct TimerRecord {
|
||||
AppTimerHandle id = kInvalidAppTimer;
|
||||
std::uint32_t generation = 0;
|
||||
std::uint32_t due_ms = 0;
|
||||
std::uint32_t interval_ms = 0;
|
||||
bool repeat = false;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat);
|
||||
void cancelTimer(AppTimerHandle handle);
|
||||
void cancelAllTimers();
|
||||
|
||||
void dispatchEvent(const AppEvent& event);
|
||||
|
||||
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
|
||||
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
|
||||
void clearTimersForCurrentApp();
|
||||
TimerRecord* findTimer(AppTimerHandle handle);
|
||||
bool handlePendingSwitchRequest();
|
||||
|
||||
AppContext context;
|
||||
std::vector<std::unique_ptr<IAppFactory>> factories;
|
||||
std::unique_ptr<IApp> current;
|
||||
IAppFactory* activeFactory = nullptr;
|
||||
std::size_t activeIndex = static_cast<std::size_t>(-1);
|
||||
std::vector<TimerRecord> timers;
|
||||
AppTimerHandle nextTimerId = 1;
|
||||
std::uint32_t currentGeneration = 0;
|
||||
InputState lastInputState{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(uint32_t delay_ms, bool repeat) {
|
||||
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
|
||||
if (system)
|
||||
system->cancelTimer(handle);
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
|
||||
if (system)
|
||||
system->cancelAllTimers();
|
||||
}
|
||||
using AppSystem = cardboy::sdk::AppSystem;
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<IAppFactory> createClockAppFactory();
|
||||
|
||||
}
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kGameboyAppName[] = "Game Boy";
|
||||
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
|
||||
|
||||
std::unique_ptr<IAppFactory> createGameboyAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kMenuAppName[] = "Menu";
|
||||
inline constexpr std::string_view kMenuAppNameView = kMenuAppName;
|
||||
|
||||
std::unique_ptr<IAppFactory> createMenuAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<IAppFactory> createTetrisAppFactory();
|
||||
|
||||
}
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
|
||||
#define SPI_BUS SPI2_HOST
|
||||
|
||||
#define DISP_WIDTH 400
|
||||
#define DISP_HEIGHT 240
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
|
||||
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
|
||||
|
||||
#define BUZZER_PIN GPIO_NUM_25
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
|
||||
#include "Surface.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
namespace SMD {
|
||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
struct InputState {
|
||||
bool up = false;
|
||||
bool left = false;
|
||||
bool right = false;
|
||||
bool down = false;
|
||||
bool a = false;
|
||||
bool b = false;
|
||||
bool select = false;
|
||||
bool start = false;
|
||||
};
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
using InputState = cardboy::sdk::InputState;
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include "app_system.hpp"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_platform.hpp"
|
||||
#include "apps/clock_app.hpp"
|
||||
#include "apps/gameboy_app.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "apps/tetris_app.hpp"
|
||||
#include "config.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <bat_mon.hpp>
|
||||
#include <buttons.hpp>
|
||||
@@ -16,6 +18,8 @@
|
||||
#include <display.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
#include <i2c_global.hpp>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <power_helper.hpp>
|
||||
#include <shutdowner.hpp>
|
||||
#include <spi_global.hpp>
|
||||
@@ -26,6 +30,8 @@
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "sdkconfig.h"
|
||||
@@ -36,8 +42,94 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
class EspBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) override {
|
||||
Buzzer::get().tone(freq, duration_ms, gap_ms);
|
||||
}
|
||||
|
||||
void beepRotate() override { Buzzer::get().beepRotate(); }
|
||||
void beepMove() override { Buzzer::get().beepMove(); }
|
||||
void beepLock() override { Buzzer::get().beepLock(); }
|
||||
void beepLines(int lines) override { Buzzer::get().beepLines(lines); }
|
||||
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
|
||||
void beepGameOver() override { Buzzer::get().beepGameOver(); }
|
||||
|
||||
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
|
||||
void toggleMuted() override { Buzzer::get().toggleMuted(); }
|
||||
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
|
||||
};
|
||||
|
||||
class EspBatteryMonitor final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return true; }
|
||||
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
||||
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
||||
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
||||
};
|
||||
|
||||
class EspStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
std::uint32_t value = 0;
|
||||
esp_err_t err = nvs_get_u32(handle, keyStr.c_str(), &value);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK)
|
||||
return false;
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READWRITE, &handle) != ESP_OK)
|
||||
return;
|
||||
nvs_set_u32(handle, keyStr.c_str(), value);
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
};
|
||||
|
||||
class EspRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
||||
};
|
||||
|
||||
class EspHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
||||
};
|
||||
|
||||
class EspPowerManager final : public cardboy::sdk::IPowerManager {
|
||||
public:
|
||||
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
|
||||
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
|
||||
};
|
||||
|
||||
class EspFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
|
||||
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
|
||||
[[nodiscard]] std::string basePath() const override {
|
||||
const char* path = FsHelper::get().basePath();
|
||||
return path ? std::string(path) : std::string{};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||
namespace {
|
||||
|
||||
@@ -217,7 +309,25 @@ extern "C" void app_main() {
|
||||
static PlatformInput input;
|
||||
static PlatformClock clock;
|
||||
|
||||
static EspBuzzer buzzerService;
|
||||
static EspBatteryMonitor batteryService;
|
||||
static EspStorage storageService;
|
||||
static EspRandom randomService;
|
||||
static EspHighResClock highResClockService;
|
||||
static EspPowerManager powerService;
|
||||
static EspFilesystem filesystemService;
|
||||
|
||||
static cardboy::sdk::Services services{};
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResClockService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
|
||||
AppContext context(framebuffer, input, clock);
|
||||
context.services = &services;
|
||||
AppSystem system(context);
|
||||
context.system = &system;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
|
||||
#include "Fonts.hpp"
|
||||
#include "cardboy/gfx/Fonts.hpp"
|
||||
|
||||
void FbTty::draw_char(int col, int row) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
|
||||
@@ -1,11 +1,70 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(sdk-top)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(cardboy_sdk LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
||||
set(CMAKE_CXX_EXTENSIONS NO)
|
||||
|
||||
add_subdirectory(library)
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
add_subdirectory(sfml-port)
|
||||
add_subdirectory(examples)
|
||||
add_library(cardboy_sdk STATIC
|
||||
src/app_system.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_sdk
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||
|
||||
add_library(cardboy_apps STATIC
|
||||
apps/menu_app.cpp
|
||||
apps/clock_app.cpp
|
||||
apps/tetris_app.cpp
|
||||
apps/gameboy_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_apps
|
||||
PUBLIC
|
||||
cardboy_sdk
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
||||
|
||||
option(CARDBOY_BUILD_SFML "Build SFML harness" OFF)
|
||||
|
||||
if (CARDBOY_BUILD_SFML)
|
||||
include(FetchContent)
|
||||
|
||||
set(SFML_BUILD_AUDIO OFF CACHE BOOL "Disable SFML audio module" FORCE)
|
||||
set(SFML_BUILD_NETWORK OFF CACHE BOOL "Disable SFML network module" FORCE)
|
||||
set(SFML_BUILD_EXAMPLES OFF CACHE BOOL "Disable SFML examples" FORCE)
|
||||
set(SFML_BUILD_TESTS OFF CACHE BOOL "Disable SFML tests" FORCE)
|
||||
set(SFML_USE_SYSTEM_DEPS OFF CACHE BOOL "Use bundled SFML dependencies" FORCE)
|
||||
|
||||
FetchContent_Declare(
|
||||
SFML
|
||||
GIT_REPOSITORY https://github.com/SFML/SFML.git
|
||||
GIT_TAG 3.0.2
|
||||
GIT_SHALLOW ON
|
||||
)
|
||||
FetchContent_MakeAvailable(SFML)
|
||||
|
||||
add_executable(cardboy_desktop
|
||||
hosts/sfml_main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_desktop
|
||||
PRIVATE
|
||||
cardboy_apps
|
||||
SFML::Graphics
|
||||
SFML::Window
|
||||
SFML::System
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
|
||||
endif ()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "apps/clock_app.hpp"
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
#include "app_system.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "font16x8.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -17,6 +17,8 @@ namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
|
||||
constexpr const char* kClockAppName = "Clock";
|
||||
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
@@ -31,10 +33,10 @@ struct TimeSnapshot {
|
||||
int month = 0;
|
||||
int day = 0;
|
||||
int weekday = 0;
|
||||
uint64_t uptimeSeconds = 0;
|
||||
std::uint64_t uptimeSeconds = 0;
|
||||
};
|
||||
|
||||
class ClockApp final : public IApp {
|
||||
class ClockApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
||||
|
||||
@@ -50,12 +52,12 @@ public:
|
||||
|
||||
void onStop() override { cancelRefreshTimer(); }
|
||||
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
||||
switch (event.type) {
|
||||
case AppEventType::Button:
|
||||
case cardboy::sdk::AppEventType::Button:
|
||||
handleButtonEvent(event.button);
|
||||
break;
|
||||
case AppEventType::Timer:
|
||||
case cardboy::sdk::AppEventType::Timer:
|
||||
if (event.timer.handle == refreshTimer)
|
||||
updateDisplay();
|
||||
break;
|
||||
@@ -69,18 +71,18 @@ private:
|
||||
|
||||
bool use24Hour = true;
|
||||
bool dirty = false;
|
||||
AppTimerHandle refreshTimer = kInvalidAppTimer;
|
||||
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
TimeSnapshot lastSnapshot{};
|
||||
|
||||
void cancelRefreshTimer() {
|
||||
if (refreshTimer != kInvalidAppTimer) {
|
||||
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(refreshTimer);
|
||||
refreshTimer = kInvalidAppTimer;
|
||||
refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void handleButtonEvent(const AppButtonEvent& button) {
|
||||
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {
|
||||
const auto& current = button.current;
|
||||
const auto& previous = button.previous;
|
||||
|
||||
@@ -113,8 +115,8 @@ private:
|
||||
TimeSnapshot snap{};
|
||||
snap.uptimeSeconds = clock.millis() / 1000ULL;
|
||||
|
||||
time_t raw = 0;
|
||||
if (time(&raw) != static_cast<time_t>(-1) && raw > 0) {
|
||||
std::time_t raw = 0;
|
||||
if (std::time(&raw) != static_cast<std::time_t>(-1) && raw > 0) {
|
||||
std::tm tm{};
|
||||
if (localtime_r(&raw, &tm) != nullptr) {
|
||||
snap.hasWallTime = true;
|
||||
@@ -158,7 +160,7 @@ private:
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
DispTools::draw_to_display_async_wait();
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
const int scaleLarge = 3;
|
||||
@@ -199,10 +201,10 @@ private:
|
||||
|
||||
if (!snap.hasWallTime) {
|
||||
char uptimeLine[32];
|
||||
const uint64_t days = snap.uptimeSeconds / 86400ULL;
|
||||
const uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
||||
const uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
||||
const uint64_t secs = snap.uptimeSeconds % 60ULL;
|
||||
const std::uint64_t days = snap.uptimeSeconds / 86400ULL;
|
||||
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
||||
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
||||
const std::uint64_t secs = snap.uptimeSeconds % 60ULL;
|
||||
if (days > 0) {
|
||||
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
|
||||
static_cast<unsigned long long>(days), static_cast<unsigned long long>(hrs),
|
||||
@@ -218,18 +220,20 @@ private:
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
|
||||
|
||||
DispTools::draw_to_display_async_start();
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
};
|
||||
|
||||
class ClockAppFactory final : public IAppFactory {
|
||||
class ClockAppFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kClockAppName; }
|
||||
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<ClockApp>(context); }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||
return std::make_unique<ClockApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
@@ -1,16 +1,11 @@
|
||||
#pragma GCC optimize("Ofast")
|
||||
#include "apps/gameboy_app.hpp"
|
||||
#include "apps/peanut_gb.h"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/peanut_gb.h"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_system.hpp"
|
||||
#include "font16x8.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
|
||||
|
||||
#include "esp_timer.h"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
@@ -26,6 +21,7 @@
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#define GAMEBOY_PERF_METRICS 0
|
||||
|
||||
@@ -45,16 +41,16 @@ namespace {
|
||||
constexpr int kMenuStartY = 48;
|
||||
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
using cardboy::sdk::AppEvent;
|
||||
using cardboy::sdk::AppEventType;
|
||||
using cardboy::sdk::AppTimerHandle;
|
||||
using cardboy::sdk::InputState;
|
||||
using cardboy::sdk::kInvalidAppTimer;
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
|
||||
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
|
||||
|
||||
extern "C" {
|
||||
extern const uint8_t _binary_builtin_demo1_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo1_gb_end[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_end[];
|
||||
}
|
||||
struct EmbeddedRomDescriptor {
|
||||
std::string_view name;
|
||||
std::string_view saveSlug;
|
||||
@@ -62,6 +58,14 @@ struct EmbeddedRomDescriptor {
|
||||
const uint8_t* end;
|
||||
};
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
extern "C" {
|
||||
extern const uint8_t _binary_builtin_demo1_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo1_gb_end[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_end[];
|
||||
}
|
||||
|
||||
static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
||||
"Builtin Demo 1",
|
||||
"builtin_demo1",
|
||||
@@ -74,6 +78,9 @@ static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
||||
_binary_builtin_demo2_gb_start,
|
||||
_binary_builtin_demo2_gb_end,
|
||||
}}};
|
||||
#else
|
||||
static const std::array<EmbeddedRomDescriptor, 0> kEmbeddedRomDescriptors{};
|
||||
#endif
|
||||
|
||||
struct RomEntry {
|
||||
std::string name; // short display name
|
||||
@@ -167,9 +174,10 @@ void drawTextRotated(Framebuffer& fb, int x, int y, std::string_view text, bool
|
||||
}
|
||||
}
|
||||
|
||||
class GameboyApp final : public IApp {
|
||||
class GameboyApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
||||
explicit GameboyApp(AppContext& ctx) :
|
||||
context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {}
|
||||
|
||||
void onStart() override {
|
||||
cancelTick();
|
||||
@@ -197,9 +205,9 @@ public:
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
||||
tickTimer = kInvalidAppTimer;
|
||||
const uint64_t frameStartUs = esp_timer_get_time();
|
||||
const uint64_t frameStartUs = nowMicros();
|
||||
performStep();
|
||||
const uint64_t frameEndUs = esp_timer_get_time();
|
||||
const uint64_t frameEndUs = nowMicros();
|
||||
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
|
||||
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
|
||||
scheduleAfterFrame(elapsedUs);
|
||||
@@ -214,27 +222,27 @@ public:
|
||||
void performStep() {
|
||||
GB_PERF_ONLY(perf.resetForStep();)
|
||||
|
||||
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
|
||||
GB_PERF_ONLY(const uint64_t inputStartUs = nowMicros();)
|
||||
const InputState input = context.input.readState();
|
||||
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
|
||||
GB_PERF_ONLY(perf.inputUs = nowMicros() - inputStartUs;)
|
||||
|
||||
const Mode stepMode = mode;
|
||||
|
||||
switch (stepMode) {
|
||||
case Mode::Browse: {
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||
handleBrowserInput(input);
|
||||
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
||||
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
|
||||
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
|
||||
renderBrowser();
|
||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
||||
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||
break;
|
||||
}
|
||||
case Mode::Running: {
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||
handleGameInput(input);
|
||||
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
||||
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
|
||||
|
||||
if (!gbReady) {
|
||||
mode = Mode::Browse;
|
||||
@@ -242,17 +250,21 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
GB_PERF_ONLY(const uint64_t geometryStartUs = esp_timer_get_time();)
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();)
|
||||
ensureRenderGeometry();
|
||||
GB_PERF_ONLY(perf.geometryUs = esp_timer_get_time() - geometryStartUs;)
|
||||
GB_PERF_ONLY(perf.geometryUs = nowMicros() - geometryStartUs;)
|
||||
|
||||
GB_PERF_ONLY(const uint64_t runStartUs = esp_timer_get_time();)
|
||||
GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();)
|
||||
gb_run_frame(&gb);
|
||||
GB_PERF_ONLY(perf.runUs = esp_timer_get_time() - runStartUs;)
|
||||
GB_PERF_ONLY(perf.runUs = nowMicros() - runStartUs;)
|
||||
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
|
||||
renderGameFrame();
|
||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
||||
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||
framebuffer.endFrame();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -329,14 +341,14 @@ private:
|
||||
}
|
||||
|
||||
void resetForStep() {
|
||||
stepStartUs = esp_timer_get_time();
|
||||
stepStartUs = clockMicros();
|
||||
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
|
||||
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
|
||||
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
|
||||
}
|
||||
|
||||
void finishStep() {
|
||||
const uint64_t now = esp_timer_get_time();
|
||||
const uint64_t now = clockMicros();
|
||||
totalUs = now - stepStartUs;
|
||||
lastStepEndUs = now;
|
||||
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
|
||||
@@ -418,7 +430,7 @@ private:
|
||||
return;
|
||||
if (!aggStartUs)
|
||||
aggStartUs = stepStartUs;
|
||||
const uint64_t now = lastStepEndUs ? lastStepEndUs : esp_timer_get_time();
|
||||
const uint64_t now = lastStepEndUs ? lastStepEndUs : clockMicros();
|
||||
const uint64_t span = now - aggStartUs;
|
||||
if (!force && span < 1000000ULL)
|
||||
return;
|
||||
@@ -469,6 +481,12 @@ private:
|
||||
aggCbLcdCalls = 0;
|
||||
aggCbErrorCalls = 0;
|
||||
}
|
||||
|
||||
static uint64_t clockMicros() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count());
|
||||
}
|
||||
};
|
||||
|
||||
class ScopedCallbackTimer {
|
||||
@@ -478,7 +496,7 @@ private:
|
||||
if (instance) {
|
||||
app = instance;
|
||||
cbKind = kind;
|
||||
startUs = esp_timer_get_time();
|
||||
startUs = instance->nowMicros();
|
||||
}
|
||||
#else
|
||||
(void) instance;
|
||||
@@ -490,7 +508,7 @@ private:
|
||||
#if GAMEBOY_PERF_METRICS
|
||||
if (!app)
|
||||
return;
|
||||
const uint64_t end = esp_timer_get_time();
|
||||
const uint64_t end = app->nowMicros();
|
||||
app->perf.addCallback(cbKind, end - startUs);
|
||||
#endif
|
||||
}
|
||||
@@ -520,6 +538,8 @@ private:
|
||||
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
cardboy::sdk::IFilesystem* filesystem = nullptr;
|
||||
cardboy::sdk::IHighResClock* highResClock = nullptr;
|
||||
PerfTracker perf{};
|
||||
AppTimerHandle tickTimer = kInvalidAppTimer;
|
||||
int64_t frameDelayCarryUs = 0;
|
||||
@@ -584,9 +604,13 @@ private:
|
||||
}
|
||||
|
||||
bool ensureFilesystemReady() {
|
||||
esp_err_t err = FsHelper::get().mount();
|
||||
if (err != ESP_OK) {
|
||||
setStatus("LittleFS mount failed");
|
||||
if (!filesystem) {
|
||||
setStatus("Storage unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filesystem->isMounted() && !filesystem->mount()) {
|
||||
setStatus("Storage mount failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -610,7 +634,9 @@ private:
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string romDirectory() const {
|
||||
std::string result(FsHelper::get().basePath());
|
||||
std::string result;
|
||||
if (filesystem)
|
||||
result = filesystem->basePath();
|
||||
if (!result.empty() && result.back() != '/')
|
||||
result.push_back('/');
|
||||
result.append("roms");
|
||||
@@ -631,7 +657,7 @@ private:
|
||||
void refreshRomList() {
|
||||
roms.clear();
|
||||
|
||||
bool fsMounted = FsHelper::get().isMounted();
|
||||
bool fsMounted = filesystem ? filesystem->isMounted() : false;
|
||||
std::string statusHint;
|
||||
const auto updateStatusHintIfEmpty = [&](std::string value) {
|
||||
if (statusHint.empty())
|
||||
@@ -641,7 +667,7 @@ private:
|
||||
if (!fsMounted) {
|
||||
fsMounted = ensureFilesystemReady();
|
||||
if (!fsMounted)
|
||||
updateStatusHintIfEmpty("Built-in ROMs only (LittleFS unavailable)");
|
||||
updateStatusHintIfEmpty("Built-in ROMs only (filesystem unavailable)");
|
||||
}
|
||||
|
||||
if (fsMounted) {
|
||||
@@ -676,9 +702,9 @@ private:
|
||||
}
|
||||
closedir(dir);
|
||||
if (roms.empty())
|
||||
updateStatusHintIfEmpty("Copy .gb/.gbc to /lfs/roms");
|
||||
updateStatusHintIfEmpty("Copy .gb/.gbc into ROMS/");
|
||||
} else {
|
||||
updateStatusHintIfEmpty("No /lfs/roms directory");
|
||||
updateStatusHintIfEmpty("ROM directory missing");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,7 +849,8 @@ private:
|
||||
return;
|
||||
browserDirty = false;
|
||||
|
||||
DispTools::draw_to_display_async_wait();
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
const std::string_view title = "GAME BOY";
|
||||
const int titleWidth = font16x8::measureText(title, 2, 1);
|
||||
@@ -832,7 +859,7 @@ private:
|
||||
|
||||
if (roms.empty()) {
|
||||
font16x8::drawText(framebuffer, 24, kMenuStartY + 12, "NO ROMS FOUND", 1, true, 1);
|
||||
font16x8::drawText(framebuffer, 24, kMenuStartY + kMenuSpacing + 12, "/LFS/ROMS", 1, true, 1);
|
||||
font16x8::drawText(framebuffer, 24, kMenuStartY + kMenuSpacing + 12, "ADD FILES TO ROMS/", 1, true, 1);
|
||||
} else {
|
||||
const std::size_t visibleCount =
|
||||
static_cast<std::size_t>(std::max(1, (framebuffer.height() - kMenuStartY - 64) / kMenuSpacing));
|
||||
@@ -866,7 +893,7 @@ private:
|
||||
font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1);
|
||||
}
|
||||
|
||||
DispTools::draw_to_display_async_start();
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
|
||||
bool loadRom(std::size_t index) {
|
||||
@@ -948,7 +975,7 @@ private:
|
||||
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
||||
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
||||
std::string savePath;
|
||||
const bool fsReady = FsHelper::get().isMounted() || ensureFilesystemReady();
|
||||
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
|
||||
if (fsReady)
|
||||
savePath = buildSavePath(rom, romDirectory());
|
||||
activeRomSavePath = savePath;
|
||||
@@ -1158,7 +1185,6 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
DispTools::draw_to_display_async_start();
|
||||
}
|
||||
|
||||
void maybeSaveRam() {
|
||||
@@ -1198,6 +1224,14 @@ private:
|
||||
browserDirty = true;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint64_t nowMicros() const {
|
||||
if (highResClock)
|
||||
return highResClock->micros();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count());
|
||||
}
|
||||
|
||||
void resetFpsStats() {
|
||||
fpsLastSampleMs = 0;
|
||||
fpsFrameCounter = 0;
|
||||
@@ -1302,10 +1336,6 @@ private:
|
||||
|
||||
Framebuffer& fb = self->framebuffer;
|
||||
|
||||
GB_PERF_ONLY(const uint64_t waitStartUs = esp_timer_get_time();)
|
||||
DispTools::draw_to_display_async_wait();
|
||||
GB_PERF_ONLY(self->perf.waitUs = esp_timer_get_time() - waitStartUs;)
|
||||
|
||||
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
|
||||
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
|
||||
|
||||
@@ -1366,14 +1396,18 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
class GameboyAppFactory final : public IAppFactory {
|
||||
class GameboyAppFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kGameboyAppName; }
|
||||
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<GameboyApp>(context); }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||
return std::make_unique<GameboyApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAppFactory> createGameboyAppFactory() { return std::make_unique<GameboyAppFactory>(); }
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() {
|
||||
return std::make_unique<GameboyAppFactory>();
|
||||
}
|
||||
|
||||
} // namespace apps
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
|
||||
#include "app_system.hpp"
|
||||
#include "font16x8.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
@@ -15,6 +15,8 @@ namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
|
||||
struct MenuEntry {
|
||||
@@ -22,7 +24,7 @@ struct MenuEntry {
|
||||
std::size_t index = 0;
|
||||
};
|
||||
|
||||
class MenuApp final : public IApp {
|
||||
class MenuApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
|
||||
|
||||
@@ -32,8 +34,8 @@ public:
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
if (event.type != AppEventType::Button)
|
||||
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
||||
if (event.type != cardboy::sdk::AppEventType::Button)
|
||||
return;
|
||||
|
||||
const auto& current = event.button.current;
|
||||
@@ -85,7 +87,7 @@ private:
|
||||
return;
|
||||
const std::size_t total = context.system->appCount();
|
||||
for (std::size_t i = 0; i < total; ++i) {
|
||||
const IAppFactory* factory = context.system->factoryAt(i);
|
||||
const cardboy::sdk::IAppFactory* factory = context.system->factoryAt(i);
|
||||
if (!factory)
|
||||
continue;
|
||||
const char* name = factory->name();
|
||||
@@ -133,7 +135,7 @@ private:
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
DispTools::draw_to_display_async_wait();
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
|
||||
@@ -156,18 +158,20 @@ private:
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
|
||||
}
|
||||
|
||||
DispTools::draw_to_display_async_start();
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
};
|
||||
|
||||
class MenuAppFactory final : public IAppFactory {
|
||||
class MenuAppFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kMenuAppName; }
|
||||
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<MenuApp>(context); }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||
return std::make_unique<MenuApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
624
Firmware/sdk/apps/tetris_app.cpp
Normal file
624
Firmware/sdk/apps/tetris_app.cpp
Normal file
@@ -0,0 +1,624 @@
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace apps {
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppButtonEvent;
|
||||
using cardboy::sdk::AppContext;
|
||||
using cardboy::sdk::AppEvent;
|
||||
using cardboy::sdk::AppEventType;
|
||||
using cardboy::sdk::AppTimerHandle;
|
||||
using cardboy::sdk::InputState;
|
||||
|
||||
constexpr char kTetrisAppName[] = "Tetris";
|
||||
|
||||
constexpr int kBoardWidth = 10;
|
||||
constexpr int kBoardHeight = 20;
|
||||
constexpr int kCellSize = 10;
|
||||
|
||||
constexpr std::array<int, 5> kLineScores = {0, 40, 100, 300, 1200};
|
||||
|
||||
struct BlockOffset {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
|
||||
struct Tetromino {
|
||||
std::array<std::array<BlockOffset, 4>, 4> rotations{};
|
||||
};
|
||||
|
||||
constexpr std::array<BlockOffset, 4> makeOffsets(std::initializer_list<BlockOffset> blocks) {
|
||||
std::array<BlockOffset, 4> out{};
|
||||
std::size_t idx = 0;
|
||||
for (const auto& b: blocks) {
|
||||
out[idx++] = b;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
constexpr std::array<BlockOffset, 4> rotate(const std::array<BlockOffset, 4>& src) {
|
||||
std::array<BlockOffset, 4> out{};
|
||||
for (std::size_t i = 0; i < src.size(); ++i) {
|
||||
out[i].x = -src[i].y;
|
||||
out[i].y = src[i].x;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
constexpr Tetromino makeTetromino(std::initializer_list<BlockOffset> baseBlocks) {
|
||||
Tetromino tet{};
|
||||
tet.rotations[0] = makeOffsets(baseBlocks);
|
||||
for (int r = 1; r < 4; ++r)
|
||||
tet.rotations[r] = rotate(tet.rotations[r - 1]);
|
||||
return tet;
|
||||
}
|
||||
|
||||
constexpr std::array<Tetromino, 7> kPieces = {{
|
||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
|
||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
|
||||
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
|
||||
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
|
||||
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
|
||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
|
||||
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
|
||||
}};
|
||||
|
||||
class RandomBag {
|
||||
public:
|
||||
RandomBag() { refill(); }
|
||||
|
||||
void seed(std::uint32_t value) { rng.seed(value); }
|
||||
|
||||
int next() {
|
||||
if (bag.empty())
|
||||
refill();
|
||||
int val = bag.back();
|
||||
bag.pop_back();
|
||||
return val;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<int> bag;
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
|
||||
void refill() {
|
||||
bag.clear();
|
||||
bag.reserve(7);
|
||||
for (int i = 0; i < 7; ++i)
|
||||
bag.push_back(i);
|
||||
std::shuffle(bag.begin(), bag.end(), rng);
|
||||
}
|
||||
};
|
||||
|
||||
struct ActivePiece {
|
||||
int type = 0;
|
||||
int rotation = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
|
||||
struct GameState {
|
||||
std::array<int, kBoardWidth * kBoardHeight> board{};
|
||||
ActivePiece current{};
|
||||
int nextPiece = 0;
|
||||
int level = 1;
|
||||
int linesCleared = 0;
|
||||
int score = 0;
|
||||
int highScore = 0;
|
||||
bool paused = false;
|
||||
bool gameOver = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
|
||||
if (auto* rnd = ctx.random())
|
||||
return rnd->nextUint32();
|
||||
static std::random_device rd;
|
||||
return rd();
|
||||
}
|
||||
|
||||
class TetrisGame {
|
||||
public:
|
||||
explicit TetrisGame(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {
|
||||
bag.seed(randomSeed(context));
|
||||
loadHighScore();
|
||||
reset();
|
||||
}
|
||||
|
||||
void onStart() {
|
||||
scheduleDropTimer();
|
||||
dirty = true;
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
void onStop() { cancelTimers(); }
|
||||
|
||||
void handleEvent(const AppEvent& event) {
|
||||
switch (event.type) {
|
||||
case AppEventType::Button:
|
||||
handleButtons(event.button);
|
||||
break;
|
||||
case AppEventType::Timer:
|
||||
handleTimer(event.timer.handle);
|
||||
break;
|
||||
}
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
private:
|
||||
AppContext& context;
|
||||
typename AppContext::Framebuffer& framebuffer;
|
||||
|
||||
GameState state;
|
||||
RandomBag bag;
|
||||
InputState lastInput{};
|
||||
bool dirty = false;
|
||||
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
void reset() {
|
||||
cancelTimers();
|
||||
int oldHigh = state.highScore;
|
||||
state = {};
|
||||
state.highScore = oldHigh;
|
||||
state.current.type = bag.next();
|
||||
state.nextPiece = bag.next();
|
||||
state.current.x = kBoardWidth / 2;
|
||||
state.current.y = 0;
|
||||
state.level = 1;
|
||||
state.gameOver = false;
|
||||
state.paused = false;
|
||||
dirty = true;
|
||||
scheduleDropTimer();
|
||||
if (auto* power = context.powerManager())
|
||||
power->setSlowMode(false);
|
||||
}
|
||||
|
||||
void handleButtons(const AppButtonEvent& evt) {
|
||||
const auto& cur = evt.current;
|
||||
const auto& prev = evt.previous;
|
||||
lastInput = cur;
|
||||
|
||||
if (cur.b && !prev.b) {
|
||||
context.requestAppSwitchByName(kMenuAppName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cur.start && !prev.start) {
|
||||
if (state.gameOver) {
|
||||
reset();
|
||||
} else {
|
||||
state.paused = !state.paused;
|
||||
if (auto* power = context.powerManager())
|
||||
power->setSlowMode(state.paused);
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (state.paused || state.gameOver)
|
||||
return;
|
||||
|
||||
if (cur.left && !prev.left)
|
||||
tryMove(-1, 0);
|
||||
if (cur.right && !prev.right)
|
||||
tryMove(1, 0);
|
||||
if (cur.a && !prev.a)
|
||||
rotate(1);
|
||||
if (cur.select && !prev.select)
|
||||
hardDrop();
|
||||
|
||||
if (cur.down && !prev.down) {
|
||||
softDropStep();
|
||||
scheduleSoftDropTimer();
|
||||
} else if (!cur.down && prev.down) {
|
||||
cancelSoftDropTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void handleTimer(AppTimerHandle handle) {
|
||||
if (handle == dropTimer) {
|
||||
if (!state.paused && !state.gameOver)
|
||||
gravityStep();
|
||||
} else if (handle == softTimer) {
|
||||
if (lastInput.down && !state.paused && !state.gameOver)
|
||||
softDropStep();
|
||||
else
|
||||
cancelSoftDropTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelTimers() {
|
||||
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(dropTimer);
|
||||
dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
cancelSoftDropTimer();
|
||||
}
|
||||
|
||||
void cancelSoftDropTimer() {
|
||||
if (softTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(softTimer);
|
||||
softTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void scheduleDropTimer() {
|
||||
cancelDropTimer();
|
||||
const std::uint32_t interval = dropIntervalMs();
|
||||
dropTimer = context.scheduleRepeatingTimer(interval);
|
||||
}
|
||||
|
||||
void cancelDropTimer() {
|
||||
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(dropTimer);
|
||||
dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void scheduleSoftDropTimer() {
|
||||
cancelSoftDropTimer();
|
||||
softTimer = context.scheduleRepeatingTimer(60);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t dropIntervalMs() const {
|
||||
const int base = 700;
|
||||
const int step = 50;
|
||||
int interval = base - (state.level - 1) * step;
|
||||
if (interval < 120)
|
||||
interval = 120;
|
||||
return static_cast<std::uint32_t>(interval);
|
||||
}
|
||||
|
||||
[[nodiscard]] const Tetromino& currentPiece() const { return kPieces[state.current.type]; }
|
||||
|
||||
bool canPlace(int nx, int ny, int rot) const {
|
||||
const auto& piece = kPieces[state.current.type];
|
||||
rot = ((rot % 4) + 4) % 4;
|
||||
for (const auto& block: piece.rotations[rot]) {
|
||||
int gx = nx + block.x;
|
||||
int gy = ny + block.y;
|
||||
if (gx < 0 || gx >= kBoardWidth)
|
||||
return false;
|
||||
if (gy >= kBoardHeight)
|
||||
return false;
|
||||
if (gy >= 0 && cellAt(gx, gy) != 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] int cellAt(int x, int y) const { return state.board[y * kBoardWidth + x]; }
|
||||
|
||||
void setCell(int x, int y, int value) { state.board[y * kBoardWidth + x] = value; }
|
||||
|
||||
void tryMove(int dx, int dy) {
|
||||
int nx = state.current.x + dx;
|
||||
int ny = state.current.y + dy;
|
||||
if (canPlace(nx, ny, state.current.rotation)) {
|
||||
state.current.x = nx;
|
||||
state.current.y = ny;
|
||||
dirty = true;
|
||||
if (dx != 0) {
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rotate(int direction) {
|
||||
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
|
||||
nextRot = ((nextRot % 4) + 4) % 4;
|
||||
if (canPlace(state.current.x, state.current.y, nextRot)) {
|
||||
state.current.rotation = nextRot;
|
||||
dirty = true;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepRotate();
|
||||
}
|
||||
}
|
||||
|
||||
void gravityStep() {
|
||||
if (!canPlace(state.current.x, state.current.y + 1, state.current.rotation)) {
|
||||
lockPiece();
|
||||
} else {
|
||||
state.current.y++;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void softDropStep() {
|
||||
if (canPlace(state.current.x, state.current.y + 1, state.current.rotation)) {
|
||||
state.current.y++;
|
||||
state.score += 1;
|
||||
updateHighScore();
|
||||
dirty = true;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
} else {
|
||||
lockPiece();
|
||||
}
|
||||
}
|
||||
|
||||
void hardDrop() {
|
||||
int distance = 0;
|
||||
while (canPlace(state.current.x, state.current.y + distance + 1, state.current.rotation))
|
||||
++distance;
|
||||
if (distance > 0) {
|
||||
state.current.y += distance;
|
||||
state.score += distance * 2;
|
||||
updateHighScore();
|
||||
dirty = true;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
}
|
||||
lockPiece();
|
||||
}
|
||||
|
||||
void lockPiece() {
|
||||
for (const auto& block: currentPiece().rotations[state.current.rotation]) {
|
||||
int gx = state.current.x + block.x;
|
||||
int gy = state.current.y + block.y;
|
||||
if (gy >= 0 && gy < kBoardHeight && gx >= 0 && gx < kBoardWidth)
|
||||
setCell(gx, gy, state.current.type + 1);
|
||||
if (gy < 0)
|
||||
state.gameOver = true;
|
||||
}
|
||||
|
||||
handleLineClear();
|
||||
spawnNext();
|
||||
dirty = true;
|
||||
|
||||
if (state.gameOver) {
|
||||
cancelSoftDropTimer();
|
||||
cancelDropTimer();
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepGameOver();
|
||||
if (auto* power = context.powerManager())
|
||||
power->setSlowMode(true);
|
||||
} else {
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepLock();
|
||||
}
|
||||
}
|
||||
|
||||
void handleLineClear() {
|
||||
int cleared = 0;
|
||||
for (int y = kBoardHeight - 1; y >= 0; --y) {
|
||||
bool full = true;
|
||||
for (int x = 0; x < kBoardWidth; ++x) {
|
||||
if (cellAt(x, y) == 0) {
|
||||
full = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (full) {
|
||||
++cleared;
|
||||
for (int pull = y; pull > 0; --pull)
|
||||
for (int x = 0; x < kBoardWidth; ++x)
|
||||
setCell(x, pull, cellAt(x, pull - 1));
|
||||
for (int x = 0; x < kBoardWidth; ++x)
|
||||
setCell(x, 0, 0);
|
||||
++y; // re-check same row after collapse
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared > 0) {
|
||||
state.linesCleared += cleared;
|
||||
if (cleared < static_cast<int>(kLineScores.size()))
|
||||
state.score += kLineScores[cleared] * state.level;
|
||||
else
|
||||
state.score += kLineScores.back() * state.level;
|
||||
|
||||
int newLevel = 1 + state.linesCleared / 10;
|
||||
if (newLevel != state.level) {
|
||||
state.level = newLevel;
|
||||
scheduleDropTimer();
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepLevelUp(state.level);
|
||||
}
|
||||
updateHighScore();
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepLines(cleared);
|
||||
}
|
||||
}
|
||||
|
||||
void spawnNext() {
|
||||
state.current.type = state.nextPiece;
|
||||
state.current.rotation = 0;
|
||||
state.current.x = kBoardWidth / 2;
|
||||
state.current.y = 0;
|
||||
state.nextPiece = bag.next();
|
||||
if (!canPlace(state.current.x, state.current.y, state.current.rotation))
|
||||
state.gameOver = true;
|
||||
}
|
||||
|
||||
void updateHighScore() {
|
||||
if (state.score > state.highScore) {
|
||||
state.highScore = state.score;
|
||||
if (auto* storage = context.storage())
|
||||
storage->writeUint32("tetris", "best", static_cast<std::uint32_t>(state.highScore));
|
||||
}
|
||||
}
|
||||
|
||||
void loadHighScore() {
|
||||
if (auto* storage = context.storage()) {
|
||||
std::uint32_t stored = 0;
|
||||
if (storage->readUint32("tetris", "best", stored))
|
||||
state.highScore = static_cast<int>(stored);
|
||||
}
|
||||
}
|
||||
|
||||
void renderIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
drawBoard();
|
||||
drawActivePiece();
|
||||
drawNextPreview();
|
||||
drawHUD();
|
||||
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
|
||||
void drawBoard() {
|
||||
const int originX = (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
|
||||
const int originY = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
|
||||
|
||||
for (int y = 0; y < kBoardHeight; ++y) {
|
||||
for (int x = 0; x < kBoardWidth; ++x) {
|
||||
if (int value = cellAt(x, y); value != 0)
|
||||
drawCell(originX, originY, x, y, value, true);
|
||||
}
|
||||
}
|
||||
|
||||
drawGuides(originX, originY);
|
||||
}
|
||||
|
||||
void drawActivePiece() {
|
||||
if (state.gameOver)
|
||||
return;
|
||||
const int originX = (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
|
||||
const int originY = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
|
||||
|
||||
for (const auto& block: currentPiece().rotations[state.current.rotation]) {
|
||||
int gx = state.current.x + block.x;
|
||||
int gy = state.current.y + block.y;
|
||||
if (gy < 0)
|
||||
continue;
|
||||
drawCell(originX, originY, gx, gy, state.current.type + 1, false);
|
||||
}
|
||||
}
|
||||
|
||||
void drawCell(int originX, int originY, int cx, int cy, int value, bool solid) {
|
||||
const int x0 = originX + cx * kCellSize;
|
||||
const int y0 = originY + cy * kCellSize;
|
||||
for (int dy = 0; dy < kCellSize; ++dy) {
|
||||
for (int dx = 0; dx < kCellSize; ++dx) {
|
||||
bool on = solid ? true : (dx == 0 || dx == kCellSize - 1 || dy == 0 || dy == kCellSize - 1);
|
||||
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
|
||||
}
|
||||
}
|
||||
(void) value; // value currently unused (monochrome display)
|
||||
}
|
||||
|
||||
void drawGuides(int originX, int originY) {
|
||||
for (int y = 0; y <= kBoardHeight; ++y) {
|
||||
const int py = originY + y * kCellSize;
|
||||
for (int x = 0; x < kBoardWidth * kCellSize; ++x)
|
||||
framebuffer.drawPixel(originX + x, py, (y % 5) == 0);
|
||||
}
|
||||
for (int x = 0; x <= kBoardWidth; ++x) {
|
||||
const int px = originX + x * kCellSize;
|
||||
for (int y = 0; y < kBoardHeight * kCellSize; ++y)
|
||||
framebuffer.drawPixel(px, originY + y, (x % 5) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void drawNextPreview() {
|
||||
const int blockSize = kCellSize;
|
||||
const int boxSize = blockSize * 4;
|
||||
const int originX = (cardboy::sdk::kDisplayWidth + kBoardWidth * kCellSize) / 2 + 24;
|
||||
const int originY = (cardboy::sdk::kDisplayHeight - boxSize) / 2;
|
||||
|
||||
for (int dy = 0; dy < boxSize; ++dy)
|
||||
for (int dx = 0; dx < boxSize; ++dx)
|
||||
framebuffer.drawPixel(originX + dx, originY + dy, (dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
|
||||
|
||||
const auto& piece = kPieces[state.nextPiece];
|
||||
for (const auto& block: piece.rotations[0]) {
|
||||
const int px = originX + (block.x + 1) * blockSize;
|
||||
const int py = originY + (block.y + 1) * blockSize;
|
||||
for (int dy = 1; dy < blockSize - 1; ++dy)
|
||||
for (int dx = 1; dx < blockSize - 1; ++dx)
|
||||
framebuffer.drawPixel(px + dx, py + dy, true);
|
||||
}
|
||||
}
|
||||
|
||||
void drawLabel(int x, int y, std::string_view text, int scale = 1) {
|
||||
font16x8::drawText(framebuffer, x, y, text, scale, true, 1);
|
||||
}
|
||||
|
||||
void drawHUD() {
|
||||
const int margin = 16;
|
||||
drawLabel(margin, margin, "SCORE", 1);
|
||||
drawLabel(margin, margin + 16, std::to_string(state.score), 1);
|
||||
|
||||
drawLabel(margin, margin + 40, "BEST", 1);
|
||||
drawLabel(margin, margin + 56, std::to_string(state.highScore), 1);
|
||||
|
||||
drawLabel(margin, margin + 80, "LEVEL", 1);
|
||||
drawLabel(margin, margin + 96, std::to_string(state.level), 1);
|
||||
|
||||
if (auto* battery = context.battery(); battery && battery->hasData()) {
|
||||
char line[32];
|
||||
std::snprintf(line, sizeof(line), "BAT %.2fV", battery->voltage());
|
||||
drawLabel(margin, margin + 120, line, 1);
|
||||
}
|
||||
|
||||
drawLabel(margin, cardboy::sdk::kDisplayHeight - 48, "A ROTATE", 1);
|
||||
drawLabel(margin, cardboy::sdk::kDisplayHeight - 32, "DOWN DROP", 1);
|
||||
drawLabel(margin, cardboy::sdk::kDisplayHeight - 16, "B MENU", 1);
|
||||
|
||||
if (state.paused)
|
||||
drawCenteredBanner("PAUSED");
|
||||
else if (state.gameOver)
|
||||
drawCenteredBanner("GAME OVER");
|
||||
}
|
||||
|
||||
void drawCenteredBanner(std::string_view text) {
|
||||
const int w = font16x8::measureText(text, 2, 1);
|
||||
const int h = font16x8::kGlyphHeight * 2;
|
||||
const int x = (cardboy::sdk::kDisplayWidth - w) / 2;
|
||||
const int y = (cardboy::sdk::kDisplayHeight - h) / 2;
|
||||
for (int yy = -4; yy < h + 4; ++yy)
|
||||
for (int xx = -6; xx < w + 6; ++xx)
|
||||
framebuffer.drawPixel(x + xx, y + yy, yy == -4 || yy == h + 3 || xx == -6 || xx == w + 5);
|
||||
font16x8::drawText(framebuffer, x, y, text, 2, true, 1);
|
||||
}
|
||||
};
|
||||
|
||||
class TetrisApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
|
||||
|
||||
void onStart() override { game.onStart(); }
|
||||
void onStop() override { game.onStop(); }
|
||||
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
|
||||
|
||||
private:
|
||||
TetrisGame game;
|
||||
};
|
||||
|
||||
class TetrisFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kTetrisAppName; }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
|
||||
return std::make_unique<TetrisApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() {
|
||||
return std::make_unique<TetrisFactory>();
|
||||
}
|
||||
|
||||
} // namespace apps
|
||||
397
Firmware/sdk/hosts/sfml_main.cpp
Normal file
397
Firmware/sdk/hosts/sfml_main.cpp
Normal file
@@ -0,0 +1,397 @@
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kPixelScale = 2;
|
||||
|
||||
class DesktopBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {}
|
||||
void beepRotate() override {}
|
||||
void beepMove() override {}
|
||||
void beepLock() override {}
|
||||
void beepLines(int) override {}
|
||||
void beepLevelUp(int) override {}
|
||||
void beepGameOver() override {}
|
||||
};
|
||||
|
||||
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return false; }
|
||||
};
|
||||
|
||||
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
auto it = data.find(composeKey(ns, key));
|
||||
if (it == data.end())
|
||||
return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
data[composeKey(ns, key)] = value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::uint32_t> data;
|
||||
|
||||
static std::string composeKey(std::string_view ns, std::string_view key) {
|
||||
std::string result;
|
||||
result.reserve(ns.size() + key.size() + 1);
|
||||
result.append(ns.begin(), ns.end());
|
||||
result.push_back(':');
|
||||
result.append(key.begin(), key.end());
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits<std::uint32_t>::max()) {}
|
||||
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return dist(rng); }
|
||||
|
||||
private:
|
||||
std::mt19937 rng;
|
||||
std::uniform_int_distribution<std::uint32_t> dist;
|
||||
};
|
||||
|
||||
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
DesktopHighResClock() : start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
[[nodiscard]] std::uint64_t micros() override {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now - start).count());
|
||||
}
|
||||
|
||||
private:
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopPowerManager final : public cardboy::sdk::IPowerManager {
|
||||
public:
|
||||
void setSlowMode(bool enable) override { slowMode = enable; }
|
||||
[[nodiscard]] bool isSlowMode() const override { return slowMode; }
|
||||
|
||||
private:
|
||||
bool slowMode = false;
|
||||
};
|
||||
|
||||
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
DesktopFilesystem() {
|
||||
if (const char* env = std::getenv("CARDBOY_ROM_DIR"); env && *env) {
|
||||
basePathPath = std::filesystem::path(env);
|
||||
} else {
|
||||
basePathPath = std::filesystem::current_path() / "roms";
|
||||
}
|
||||
}
|
||||
|
||||
bool mount() override {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(basePathPath, ec)) {
|
||||
mounted = std::filesystem::is_directory(basePathPath, ec);
|
||||
} else {
|
||||
mounted = std::filesystem::create_directories(basePathPath, ec);
|
||||
}
|
||||
return mounted;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isMounted() const override { return mounted; }
|
||||
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
|
||||
|
||||
private:
|
||||
std::filesystem::path basePathPath;
|
||||
bool mounted = false;
|
||||
};
|
||||
|
||||
class DesktopRuntime;
|
||||
|
||||
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
explicit DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
int width() const override;
|
||||
int height() const override;
|
||||
void drawPixel(int x, int y, bool on) override;
|
||||
void clear(bool on) override;
|
||||
void beginFrame() override {}
|
||||
void endFrame() override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
};
|
||||
|
||||
class DesktopInput final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
explicit DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
cardboy::sdk::InputState readState() override { return state; }
|
||||
|
||||
void handleKey(sf::Keyboard::Key key, bool pressed);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
cardboy::sdk::InputState state{};
|
||||
};
|
||||
|
||||
class DesktopClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
explicit DesktopClock(DesktopRuntime& runtime)
|
||||
: runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint32_t millis() override {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
||||
}
|
||||
|
||||
void sleep_ms(std::uint32_t ms) override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopRuntime {
|
||||
private:
|
||||
friend class DesktopFramebuffer;
|
||||
friend class DesktopInput;
|
||||
friend class DesktopClock;
|
||||
|
||||
sf::RenderWindow window;
|
||||
sf::Texture texture;
|
||||
sf::Sprite sprite;
|
||||
std::vector<std::uint8_t> pixels; // RGBA buffer
|
||||
bool dirty = true;
|
||||
bool running = true;
|
||||
|
||||
DesktopBuzzer buzzerService;
|
||||
DesktopBattery batteryService;
|
||||
DesktopStorage storageService;
|
||||
DesktopRandom randomService;
|
||||
DesktopHighResClock highResService;
|
||||
DesktopPowerManager powerService;
|
||||
DesktopFilesystem filesystemService;
|
||||
cardboy::sdk::Services services{};
|
||||
|
||||
public:
|
||||
DesktopRuntime();
|
||||
|
||||
DesktopFramebuffer framebuffer;
|
||||
DesktopInput input;
|
||||
DesktopClock clock;
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry() { return services; }
|
||||
|
||||
void processEvents();
|
||||
void presentIfNeeded();
|
||||
void sleepFor(std::uint32_t ms);
|
||||
|
||||
[[nodiscard]] bool isRunning() const { return running; }
|
||||
|
||||
private:
|
||||
void setPixel(int x, int y, bool on);
|
||||
void clearPixels(bool on);
|
||||
};
|
||||
|
||||
DesktopRuntime::DesktopRuntime()
|
||||
: window(sf::VideoMode(sf::Vector2u{cardboy::sdk::kDisplayWidth * kPixelScale,
|
||||
cardboy::sdk::kDisplayHeight * kPixelScale}),
|
||||
"Cardboy Desktop"),
|
||||
texture(),
|
||||
sprite(texture),
|
||||
pixels(static_cast<std::size_t>(cardboy::sdk::kDisplayWidth * cardboy::sdk::kDisplayHeight) * 4, 0),
|
||||
framebuffer(*this),
|
||||
input(*this),
|
||||
clock(*this) {
|
||||
window.setFramerateLimit(60);
|
||||
if (!texture.resize(sf::Vector2u{cardboy::sdk::kDisplayWidth, cardboy::sdk::kDisplayHeight}))
|
||||
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
|
||||
sprite.setTexture(texture, true);
|
||||
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
|
||||
clearPixels(false);
|
||||
presentIfNeeded();
|
||||
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
}
|
||||
|
||||
void DesktopRuntime::setPixel(int x, int y, bool on) {
|
||||
if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight)
|
||||
return;
|
||||
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
|
||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||
pixels[idx + 0] = value;
|
||||
pixels[idx + 1] = value;
|
||||
pixels[idx + 2] = value;
|
||||
pixels[idx + 3] = 255;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::clearPixels(bool on) {
|
||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
||||
pixels[i + 0] = value;
|
||||
pixels[i + 1] = value;
|
||||
pixels[i + 2] = value;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::processEvents() {
|
||||
while (auto eventOpt = window.pollEvent()) {
|
||||
const sf::Event& event = *eventOpt;
|
||||
|
||||
if (event.is<sf::Event::Closed>()) {
|
||||
running = false;
|
||||
window.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyPressed = event.getIf<sf::Event::KeyPressed>()) {
|
||||
input.handleKey(keyPressed->code, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyReleased = event.getIf<sf::Event::KeyReleased>()) {
|
||||
input.handleKey(keyReleased->code, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopRuntime::presentIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
texture.update(pixels.data());
|
||||
window.clear(sf::Color::Black);
|
||||
window.draw(sprite);
|
||||
window.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void DesktopRuntime::sleepFor(std::uint32_t ms) {
|
||||
const auto target = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
|
||||
do {
|
||||
processEvents();
|
||||
presentIfNeeded();
|
||||
if (!running)
|
||||
std::exit(0);
|
||||
if (ms == 0)
|
||||
return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= target)
|
||||
return;
|
||||
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(target - now);
|
||||
if (remaining.count() > 2)
|
||||
std::this_thread::sleep_for(std::min<std::chrono::milliseconds>(remaining, std::chrono::milliseconds(8)));
|
||||
else
|
||||
std::this_thread::yield();
|
||||
} while (true);
|
||||
}
|
||||
|
||||
int DesktopFramebuffer::width() const { return cardboy::sdk::kDisplayWidth; }
|
||||
|
||||
int DesktopFramebuffer::height() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void DesktopFramebuffer::drawPixel(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
||||
|
||||
void DesktopFramebuffer::clear(bool on) { runtime.clearPixels(on); }
|
||||
|
||||
void DesktopFramebuffer::endFrame() { runtime.dirty = true; }
|
||||
|
||||
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
||||
switch (key) {
|
||||
case sf::Keyboard::Key::Up:
|
||||
state.up = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Down:
|
||||
state.down = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Left:
|
||||
state.left = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Right:
|
||||
state.right = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Z:
|
||||
case sf::Keyboard::Key::A:
|
||||
state.a = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::X:
|
||||
case sf::Keyboard::Key::S:
|
||||
state.b = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Space:
|
||||
state.select = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Enter:
|
||||
state.start = pressed;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopClock::sleep_ms(std::uint32_t ms) { runtime.sleepFor(ms); }
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DesktopRuntime runtime;
|
||||
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createClockAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
|
||||
system.run();
|
||||
} catch (const std::exception& ex) {
|
||||
std::fprintf(stderr, "Cardboy desktop runtime failed: %s\n", ex.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
12
Firmware/sdk/include/cardboy/apps/clock_app.hpp
Normal file
12
Firmware/sdk/include/cardboy/apps/clock_app.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kGameboyAppName[] = "Game Boy";
|
||||
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
16
Firmware/sdk/include/cardboy/apps/menu_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/menu_app.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kMenuAppName[] = "Menu";
|
||||
inline constexpr std::string_view kMenuAppNameView = kMenuAppName;
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createMenuAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
@@ -92,12 +92,12 @@
|
||||
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
|
||||
* pixel data. */
|
||||
#ifndef PEANUT_GB_12_COLOUR
|
||||
# define PEANUT_GB_12_COLOUR 1
|
||||
# define PEANUT_GB_12_COLOUR 0
|
||||
#endif
|
||||
|
||||
/* Adds more code to improve LCD rendering accuracy. */
|
||||
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
|
||||
# define PEANUT_GB_HIGH_LCD_ACCURACY 1
|
||||
# define PEANUT_GB_HIGH_LCD_ACCURACY 0
|
||||
#endif
|
||||
|
||||
/* Use intrinsic functions. This may produce smaller and faster code. */
|
||||
12
Firmware/sdk/include/cardboy/apps/tetris_app.hpp
Normal file
12
Firmware/sdk/include/cardboy/apps/tetris_app.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Fonts.hpp"
|
||||
#include "app_framework.hpp"
|
||||
#include "cardboy/gfx/Fonts.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
133
Firmware/sdk/include/cardboy/sdk/app_framework.hpp
Normal file
133
Firmware/sdk/include/cardboy/sdk/app_framework.hpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include "platform.hpp"
|
||||
#include "services.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class AppSystem;
|
||||
|
||||
using AppTimerHandle = std::uint32_t;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = 0;
|
||||
|
||||
enum class AppEventType {
|
||||
Button,
|
||||
Timer,
|
||||
};
|
||||
|
||||
struct AppButtonEvent {
|
||||
InputState current{};
|
||||
InputState previous{};
|
||||
};
|
||||
|
||||
struct AppTimerEvent {
|
||||
AppTimerHandle handle = kInvalidAppTimer;
|
||||
};
|
||||
|
||||
struct AppEvent {
|
||||
AppEventType type;
|
||||
std::uint32_t timestamp_ms = 0;
|
||||
AppButtonEvent button{};
|
||||
AppTimerEvent timer{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
struct BasicAppContext {
|
||||
using Framebuffer = FramebufferT;
|
||||
using Input = InputT;
|
||||
using Clock = ClockT;
|
||||
|
||||
BasicAppContext() = delete;
|
||||
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
|
||||
FramebufferT& framebuffer;
|
||||
InputT& input;
|
||||
ClockT& clock;
|
||||
AppSystem* system = nullptr;
|
||||
Services* services = nullptr;
|
||||
|
||||
[[nodiscard]] Services* getServices() const { return services; }
|
||||
|
||||
[[nodiscard]] IBuzzer* buzzer() const { return services ? services->buzzer : nullptr; }
|
||||
[[nodiscard]] IBatteryMonitor* battery() const { return services ? services->battery : nullptr; }
|
||||
[[nodiscard]] IStorage* storage() const { return services ? services->storage : nullptr; }
|
||||
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
|
||||
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
|
||||
[[nodiscard]] IPowerManager* powerManager() const { return services ? services->powerManager : nullptr; }
|
||||
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
|
||||
|
||||
void requestAppSwitchByIndex(std::size_t index) {
|
||||
pendingAppIndex = index;
|
||||
pendingAppName.clear();
|
||||
pendingSwitchByName = false;
|
||||
pendingSwitch = true;
|
||||
}
|
||||
|
||||
void requestAppSwitchByName(std::string_view name) {
|
||||
pendingAppName.assign(name.begin(), name.end());
|
||||
pendingSwitchByName = true;
|
||||
pendingSwitch = true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasPendingAppSwitch() const { return pendingSwitch; }
|
||||
|
||||
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat = false) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(delay_ms, repeat);
|
||||
}
|
||||
|
||||
AppTimerHandle scheduleRepeatingTimer(std::uint32_t interval_ms) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(interval_ms, true);
|
||||
}
|
||||
|
||||
void cancelTimer(AppTimerHandle handle) {
|
||||
if (!system)
|
||||
return;
|
||||
cancelTimerInternal(handle);
|
||||
}
|
||||
|
||||
void cancelAllTimers() {
|
||||
if (!system)
|
||||
return;
|
||||
cancelAllTimersInternal();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class AppSystem;
|
||||
bool pendingSwitch = false;
|
||||
bool pendingSwitchByName = false;
|
||||
std::size_t pendingAppIndex = 0;
|
||||
std::string pendingAppName;
|
||||
|
||||
AppTimerHandle scheduleTimerInternal(std::uint32_t delay_ms, bool repeat);
|
||||
void cancelTimerInternal(AppTimerHandle handle);
|
||||
void cancelAllTimersInternal();
|
||||
};
|
||||
|
||||
using AppContext = BasicAppContext<IFramebuffer, IInput, IClock>;
|
||||
|
||||
class IApp {
|
||||
public:
|
||||
virtual ~IApp() = default;
|
||||
virtual void onStart() {}
|
||||
virtual void onStop() {}
|
||||
virtual void handleEvent(const AppEvent& event) = 0;
|
||||
};
|
||||
|
||||
class IAppFactory {
|
||||
public:
|
||||
virtual ~IAppFactory() = default;
|
||||
virtual const char* name() const = 0;
|
||||
virtual std::unique_ptr<IApp> create(AppContext& context) = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
85
Firmware/sdk/include/cardboy/sdk/app_system.hpp
Normal file
85
Firmware/sdk/include/cardboy/sdk/app_system.hpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class AppSystem {
|
||||
public:
|
||||
explicit AppSystem(AppContext context);
|
||||
|
||||
void registerApp(std::unique_ptr<IAppFactory> factory);
|
||||
bool startApp(const std::string& name);
|
||||
bool startAppByIndex(std::size_t index);
|
||||
|
||||
void run();
|
||||
|
||||
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
|
||||
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
|
||||
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
|
||||
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
|
||||
|
||||
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
|
||||
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
||||
|
||||
private:
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
friend struct BasicAppContext;
|
||||
|
||||
struct TimerRecord {
|
||||
AppTimerHandle id = kInvalidAppTimer;
|
||||
std::uint32_t generation = 0;
|
||||
std::uint32_t due_ms = 0;
|
||||
std::uint32_t interval_ms = 0;
|
||||
bool repeat = false;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat);
|
||||
void cancelTimer(AppTimerHandle handle);
|
||||
void cancelAllTimers();
|
||||
|
||||
void dispatchEvent(const AppEvent& event);
|
||||
|
||||
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
|
||||
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
|
||||
void clearTimersForCurrentApp();
|
||||
TimerRecord* findTimer(AppTimerHandle handle);
|
||||
bool handlePendingSwitchRequest();
|
||||
|
||||
AppContext context;
|
||||
std::vector<std::unique_ptr<IAppFactory>> factories;
|
||||
std::unique_ptr<IApp> current;
|
||||
IAppFactory* activeFactory = nullptr;
|
||||
std::size_t activeIndex = static_cast<std::size_t>(-1);
|
||||
std::vector<TimerRecord> timers;
|
||||
AppTimerHandle nextTimerId = 1;
|
||||
std::uint32_t currentGeneration = 0;
|
||||
InputState lastInputState{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(std::uint32_t delay_ms,
|
||||
bool repeat) {
|
||||
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
|
||||
if (system)
|
||||
system->cancelTimer(handle);
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
|
||||
if (system)
|
||||
system->cancelAllTimers();
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
9
Firmware/sdk/include/cardboy/sdk/display_spec.hpp
Normal file
9
Firmware/sdk/include/cardboy/sdk/display_spec.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
inline constexpr int kDisplayWidth = 400;
|
||||
inline constexpr int kDisplayHeight = 240;
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
17
Firmware/sdk/include/cardboy/sdk/input_state.hpp
Normal file
17
Firmware/sdk/include/cardboy/sdk/input_state.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
struct InputState {
|
||||
bool up = false;
|
||||
bool left = false;
|
||||
bool right = false;
|
||||
bool down = false;
|
||||
bool a = false;
|
||||
bool b = false;
|
||||
bool select = false;
|
||||
bool start = false;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
40
Firmware/sdk/include/cardboy/sdk/platform.hpp
Normal file
40
Firmware/sdk/include/cardboy/sdk/platform.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class IFramebuffer {
|
||||
public:
|
||||
virtual ~IFramebuffer() = default;
|
||||
|
||||
virtual int width() const = 0;
|
||||
virtual int height() const = 0;
|
||||
virtual void drawPixel(int x, int y, bool on) = 0;
|
||||
virtual void clear(bool on) = 0;
|
||||
|
||||
// Optional hooks for async display pipelines; default to no-ops.
|
||||
virtual void beginFrame() {}
|
||||
virtual void endFrame() {}
|
||||
virtual bool isFrameInFlight() const { return false; }
|
||||
};
|
||||
|
||||
class IInput {
|
||||
public:
|
||||
virtual ~IInput() = default;
|
||||
|
||||
virtual InputState readState() = 0;
|
||||
};
|
||||
|
||||
class IClock {
|
||||
public:
|
||||
virtual ~IClock() = default;
|
||||
|
||||
virtual std::uint32_t millis() = 0;
|
||||
virtual void sleep_ms(std::uint32_t ms) = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
87
Firmware/sdk/include/cardboy/sdk/services.hpp
Normal file
87
Firmware/sdk/include/cardboy/sdk/services.hpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class IBuzzer {
|
||||
public:
|
||||
virtual ~IBuzzer() = default;
|
||||
|
||||
virtual void init() {}
|
||||
virtual void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) = 0;
|
||||
|
||||
virtual void beepRotate() {}
|
||||
virtual void beepMove() {}
|
||||
virtual void beepLock() {}
|
||||
virtual void beepLines(int /*lines*/) {}
|
||||
virtual void beepLevelUp(int /*level*/) {}
|
||||
virtual void beepGameOver() {}
|
||||
|
||||
virtual void setMuted(bool /*muted*/) {}
|
||||
virtual void toggleMuted() {}
|
||||
[[nodiscard]] virtual bool isMuted() const { return false; }
|
||||
};
|
||||
|
||||
class IBatteryMonitor {
|
||||
public:
|
||||
virtual ~IBatteryMonitor() = default;
|
||||
|
||||
[[nodiscard]] virtual bool hasData() const { return false; }
|
||||
[[nodiscard]] virtual float voltage() const { return 0.0f; }
|
||||
[[nodiscard]] virtual float charge() const { return 0.0f; }
|
||||
[[nodiscard]] virtual float current() const { return 0.0f; }
|
||||
};
|
||||
|
||||
class IStorage {
|
||||
public:
|
||||
virtual ~IStorage() = default;
|
||||
|
||||
[[nodiscard]] virtual bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) = 0;
|
||||
virtual void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) = 0;
|
||||
};
|
||||
|
||||
class IRandom {
|
||||
public:
|
||||
virtual ~IRandom() = default;
|
||||
|
||||
[[nodiscard]] virtual std::uint32_t nextUint32() = 0;
|
||||
};
|
||||
|
||||
class IHighResClock {
|
||||
public:
|
||||
virtual ~IHighResClock() = default;
|
||||
|
||||
[[nodiscard]] virtual std::uint64_t micros() = 0;
|
||||
};
|
||||
|
||||
class IPowerManager {
|
||||
public:
|
||||
virtual ~IPowerManager() = default;
|
||||
|
||||
virtual void setSlowMode(bool enable) = 0;
|
||||
[[nodiscard]] virtual bool isSlowMode() const = 0;
|
||||
};
|
||||
|
||||
class IFilesystem {
|
||||
public:
|
||||
virtual ~IFilesystem() = default;
|
||||
|
||||
virtual bool mount() = 0;
|
||||
[[nodiscard]] virtual bool isMounted() const = 0;
|
||||
[[nodiscard]] virtual std::string basePath() const = 0;
|
||||
};
|
||||
|
||||
struct Services {
|
||||
IBuzzer* buzzer = nullptr;
|
||||
IBatteryMonitor* battery = nullptr;
|
||||
IStorage* storage = nullptr;
|
||||
IRandom* random = nullptr;
|
||||
IHighResClock* highResClock = nullptr;
|
||||
IPowerManager* powerManager = nullptr;
|
||||
IFilesystem* filesystem = nullptr;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -1,22 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
add_library(cbsdk
|
||||
src/Window.cpp
|
||||
include_public/Window.hpp
|
||||
include_public/Pixel.hpp
|
||||
src/Event.cpp
|
||||
include_public/Event.hpp
|
||||
include_public/StandardEvents.hpp
|
||||
include_public/Surface.hpp
|
||||
include_public/Fonts.hpp
|
||||
src/TextWindow.cpp
|
||||
include_public/TextWindow.hpp
|
||||
include_public/utils.hpp
|
||||
include_public/SubSurface.hpp)
|
||||
|
||||
target_include_directories(cbsdk PUBLIC include_public)
|
||||
target_include_directories(cbsdk PRIVATE include)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
add_subdirectory(test)
|
||||
endif ()
|
||||
@@ -1,139 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef EVENT_HPP
|
||||
#define EVENT_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <concepts>
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <functional>
|
||||
|
||||
enum class EventHandlingResult { DONE, IGNORE, CONTINUE };
|
||||
|
||||
class Event {};
|
||||
|
||||
struct LoopQuitEvent : public Event {};
|
||||
|
||||
template<typename T>
|
||||
concept IsEvent = std::is_base_of_v<Event, T>;
|
||||
|
||||
template<typename H, typename E>
|
||||
concept HasHandleFor = requires(H h, E e) {
|
||||
{ h.handle(e) } -> std::same_as<EventHandlingResult>;
|
||||
};
|
||||
|
||||
template<typename H, typename... Ts>
|
||||
concept HandlesAll = (HasHandleFor<H, Ts> && ...);
|
||||
|
||||
template<typename Derived, typename... T>
|
||||
requires(IsEvent<T> && ...)
|
||||
class EventHandler {
|
||||
public:
|
||||
EventHandler() { static_assert(HandlesAll<Derived, T...>); }
|
||||
};
|
||||
|
||||
class EventLoop;
|
||||
|
||||
class EventQueueBase {
|
||||
public:
|
||||
virtual void process_events() = 0;
|
||||
|
||||
virtual ~EventQueueBase() = default;
|
||||
};
|
||||
|
||||
template<typename HandlerType, typename... Ts>
|
||||
class EventQueue : public EventQueueBase {
|
||||
public:
|
||||
EventQueue(EventLoop* loop, HandlerType* handler) : _loop(loop), _handler(handler) {};
|
||||
|
||||
std::optional<std::variant<Ts...>> poll();
|
||||
|
||||
void process_events() override {
|
||||
while (auto event = poll()) {
|
||||
std::visit([this](auto&& e) { _handler->handle(e); }, *event);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires std::disjunction_v<std::is_same<T, Ts>...>
|
||||
void push(T&& event);
|
||||
|
||||
private:
|
||||
EventLoop* _loop;
|
||||
HandlerType* _handler;
|
||||
std::list<std::variant<Ts...>> _events;
|
||||
};
|
||||
|
||||
class EventLoop : EventHandler<EventLoop, LoopQuitEvent>, public EventQueue<EventLoop, LoopQuitEvent> {
|
||||
public:
|
||||
EventLoop() : EventQueue<EventLoop, LoopQuitEvent>(this, this) {}
|
||||
|
||||
template<typename... Ts>
|
||||
void notify_pending(EventQueue<Ts...>* queue) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
// TODO:
|
||||
if (std::find(_events.begin(), _events.end(), queue) != _events.end()) {
|
||||
return; // Already registered
|
||||
}
|
||||
_events.push_back(queue);
|
||||
_condition.notify_all();
|
||||
}
|
||||
|
||||
void run(std::function<void()> after_callback) {
|
||||
while (_running) {
|
||||
std::list<EventQueueBase*> new_events;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
_condition.wait(lock, [this] { return !_events.empty() || !_running; });
|
||||
std::swap(new_events, _events);
|
||||
}
|
||||
|
||||
for (auto queue: new_events) {
|
||||
queue->process_events();
|
||||
}
|
||||
|
||||
after_callback();
|
||||
}
|
||||
}
|
||||
|
||||
EventHandlingResult handle(LoopQuitEvent event) {
|
||||
_running = false;
|
||||
_condition.notify_all();
|
||||
return EventHandlingResult::DONE;
|
||||
}
|
||||
|
||||
private:
|
||||
std::list<EventQueueBase*> _events;
|
||||
std::mutex _mutex;
|
||||
std::condition_variable _condition;
|
||||
bool _running = true;
|
||||
};
|
||||
|
||||
template<typename HandlerType, typename... Ts>
|
||||
std::optional<std::variant<Ts...>> EventQueue<HandlerType, Ts...>::poll() {
|
||||
if (_events.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto event = std::move(_events.front());
|
||||
_events.pop_front();
|
||||
return event;
|
||||
}
|
||||
|
||||
template<typename HandlerType, typename... Ts>
|
||||
template<typename T>
|
||||
requires std::disjunction_v<std::is_same<T, Ts>...>
|
||||
void EventQueue<HandlerType, Ts...>::push(T&& event) {
|
||||
_events.emplace_back(std::forward<T>(event));
|
||||
_loop->notify_pending(static_cast<HandlerType*>(this));
|
||||
}
|
||||
|
||||
#endif // EVENT_HPP
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef GRIDWINDOW_HPP
|
||||
#define GRIDWINDOW_HPP
|
||||
#include <string>
|
||||
|
||||
#include "Fonts.hpp"
|
||||
#include "SubSurface.hpp"
|
||||
#include "Window.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
template<typename SurfaceType, unsigned nWidth, unsigned nHeight>
|
||||
class GridWindow : public Window<SurfaceType> {
|
||||
public:
|
||||
using PixelType = typename SurfaceType::PixelType;
|
||||
|
||||
explicit GridWindow(SurfaceType* owner) : Window<SurfaceType>(owner) {
|
||||
for (int i = 0; i < nWidth; ++i) {
|
||||
for (int j = 0; j < nHeight; ++j) {
|
||||
_grid[i][j].emplace(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventHandlingResult handle_v(KeyboardEvent keyboardEvent) override {
|
||||
if (keyboardEvent.key_code == Key::Escape) {
|
||||
if (!_has_focus) {
|
||||
return EventHandlingResult::CONTINUE;
|
||||
} else {
|
||||
auto res = _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
|
||||
if (res == EventHandlingResult::DONE) {
|
||||
return EventHandlingResult::DONE;
|
||||
} else {
|
||||
_has_focus = false;
|
||||
}
|
||||
}
|
||||
} else if (keyboardEvent.key_code == Key::Enter) {
|
||||
if (!_has_focus) {
|
||||
_has_focus = true;
|
||||
} else {
|
||||
return _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
|
||||
}
|
||||
} else {
|
||||
if (_has_focus) {
|
||||
return _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
|
||||
}
|
||||
|
||||
if (keyboardEvent.key_code == Key::Left) {
|
||||
if (_current_focus_x > 0) {
|
||||
_current_focus_x--;
|
||||
}
|
||||
} else if (keyboardEvent.key_code == Key::Right) {
|
||||
if (_current_focus_x < nWidth - 1) {
|
||||
_current_focus_x++;
|
||||
}
|
||||
} else if (keyboardEvent.key_code == Key::Up) {
|
||||
if (_current_focus_y > 0) {
|
||||
_current_focus_y--;
|
||||
}
|
||||
} else if (keyboardEvent.key_code == Key::Down) {
|
||||
if (_current_focus_y < nHeight - 1) {
|
||||
_current_focus_y++;
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
return EventHandlingResult::DONE;
|
||||
}
|
||||
|
||||
EventHandlingResult handle_v(SurfaceResizeEvent resize) override {
|
||||
_cell_width = this->_owner->get_width() / nWidth;
|
||||
_cell_height = this->_owner->get_height() / nHeight;
|
||||
for (int i = 0; i < nWidth; ++i) {
|
||||
for (int j = 0; j < nHeight; ++j) {
|
||||
if constexpr (is_specialization_of<SubSurface, SurfaceType>::value) {
|
||||
_grid[i][j]->set_pos(this->_owner->get_x_offset() + i * _cell_width + 1,
|
||||
this->_owner->get_y_offset() + j * _cell_height + 1, _cell_width - 2,
|
||||
_cell_height - 2);
|
||||
} else {
|
||||
_grid[i][j]->set_pos(i * _cell_width + 1, j * _cell_height + 1, _cell_width - 2, _cell_height - 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
return EventHandlingResult::DONE;
|
||||
}
|
||||
|
||||
template<typename WindowType, typename... Args>
|
||||
void set_window(unsigned x, unsigned y, Args&&... args) {
|
||||
_grid[x][y]->template set_window<WindowType>(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
SubSurface<SurfaceType>& get_subsurface(unsigned x, unsigned y) {
|
||||
// assert(x >= nWidth && y >= nHeight);
|
||||
return *_grid[x][y];
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
for (int i = 0; i < nWidth; ++i) {
|
||||
for (int j = 0; j < nHeight; ++j) {
|
||||
if (i == _current_focus_x && j == _current_focus_y) {
|
||||
this->_owner->draw_rect(i * _cell_width, j * _cell_height, _cell_width, _cell_height,
|
||||
PixelType(true));
|
||||
} else {
|
||||
this->_owner->draw_rect(i * _cell_width, j * _cell_height, _cell_width, _cell_height,
|
||||
PixelType(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
using SubType = std::conditional_t<is_specialization_of<SubSurface, SurfaceType>::value, SurfaceType,
|
||||
SubSurface<SurfaceType>>;
|
||||
|
||||
std::array<std::array<std::optional<SubType>, nWidth>, nHeight> _grid;
|
||||
|
||||
unsigned _cell_width = 0;
|
||||
unsigned _cell_height = 0;
|
||||
|
||||
unsigned _current_focus_x = 0;
|
||||
unsigned _current_focus_y = 0;
|
||||
bool _has_focus = false;
|
||||
};
|
||||
|
||||
#endif // TEXTWINDOW_HPP
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef PIXEL_HPP
|
||||
#define PIXEL_HPP
|
||||
|
||||
class Pixel {
|
||||
};
|
||||
|
||||
struct BwPixel : public Pixel {
|
||||
bool on = false;
|
||||
|
||||
BwPixel() = default;
|
||||
BwPixel(bool on) : on(on) {}
|
||||
|
||||
bool operator==(const BwPixel& other) const { return on == other.on; }
|
||||
bool operator!=(const BwPixel& other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
#endif //PIXEL_HPP
|
||||
@@ -1,163 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef STANDARDEVENTS_HPP
|
||||
#define STANDARDEVENTS_HPP
|
||||
|
||||
#include "Event.hpp"
|
||||
|
||||
// TODO: rewrite this
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SFML - Simple and Fast Multimedia Library
|
||||
// Copyright (C) 2007-2025 Laurent Gomila (laurent@sfml-dev.org)
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied warranty.
|
||||
// In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it freely,
|
||||
// subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented;
|
||||
// you must not claim that you wrote the original software.
|
||||
// If you use this software in a product, an acknowledgment
|
||||
// in the product documentation would be appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such,
|
||||
// and must not be misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
enum class Key {
|
||||
Unknown = -1, //!< Unhandled key
|
||||
A = 0, //!< The A key
|
||||
B, //!< The B key
|
||||
C, //!< The C key
|
||||
D, //!< The D key
|
||||
E, //!< The E key
|
||||
F, //!< The F key
|
||||
G, //!< The G key
|
||||
H, //!< The H key
|
||||
I, //!< The I key
|
||||
J, //!< The J key
|
||||
K, //!< The K key
|
||||
L, //!< The L key
|
||||
M, //!< The M key
|
||||
N, //!< The N key
|
||||
O, //!< The O key
|
||||
P, //!< The P key
|
||||
Q, //!< The Q key
|
||||
R, //!< The R key
|
||||
S, //!< The S key
|
||||
T, //!< The T key
|
||||
U, //!< The U key
|
||||
V, //!< The V key
|
||||
W, //!< The W key
|
||||
X, //!< The X key
|
||||
Y, //!< The Y key
|
||||
Z, //!< The Z key
|
||||
Num0, //!< The 0 key
|
||||
Num1, //!< The 1 key
|
||||
Num2, //!< The 2 key
|
||||
Num3, //!< The 3 key
|
||||
Num4, //!< The 4 key
|
||||
Num5, //!< The 5 key
|
||||
Num6, //!< The 6 key
|
||||
Num7, //!< The 7 key
|
||||
Num8, //!< The 8 key
|
||||
Num9, //!< The 9 key
|
||||
Escape, //!< The Escape key
|
||||
LControl, //!< The left Control key
|
||||
LShift, //!< The left Shift key
|
||||
LAlt, //!< The left Alt key
|
||||
LSystem, //!< The left OS specific key: window (Windows and Linux), apple (macOS), ...
|
||||
RControl, //!< The right Control key
|
||||
RShift, //!< The right Shift key
|
||||
RAlt, //!< The right Alt key
|
||||
RSystem, //!< The right OS specific key: window (Windows and Linux), apple (macOS), ...
|
||||
Menu, //!< The Menu key
|
||||
LBracket, //!< The [ key
|
||||
RBracket, //!< The ] key
|
||||
Semicolon, //!< The ; key
|
||||
Comma, //!< The , key
|
||||
Period, //!< The . key
|
||||
Apostrophe, //!< The ' key
|
||||
Slash, //!< The / key
|
||||
Backslash, //!< The \ key
|
||||
Grave, //!< The ` key
|
||||
Equal, //!< The = key
|
||||
Hyphen, //!< The - key (hyphen)
|
||||
Space, //!< The Space key
|
||||
Enter, //!< The Enter/Return keys
|
||||
Backspace, //!< The Backspace key
|
||||
Tab, //!< The Tabulation key
|
||||
PageUp, //!< The Page up key
|
||||
PageDown, //!< The Page down key
|
||||
End, //!< The End key
|
||||
Home, //!< The Home key
|
||||
Insert, //!< The Insert key
|
||||
Delete, //!< The Delete key
|
||||
Add, //!< The + key
|
||||
Subtract, //!< The - key (minus, usually from numpad)
|
||||
Multiply, //!< The * key
|
||||
Divide, //!< The / key
|
||||
Left, //!< Left arrow
|
||||
Right, //!< Right arrow
|
||||
Up, //!< Up arrow
|
||||
Down, //!< Down arrow
|
||||
Numpad0, //!< The numpad 0 key
|
||||
Numpad1, //!< The numpad 1 key
|
||||
Numpad2, //!< The numpad 2 key
|
||||
Numpad3, //!< The numpad 3 key
|
||||
Numpad4, //!< The numpad 4 key
|
||||
Numpad5, //!< The numpad 5 key
|
||||
Numpad6, //!< The numpad 6 key
|
||||
Numpad7, //!< The numpad 7 key
|
||||
Numpad8, //!< The numpad 8 key
|
||||
Numpad9, //!< The numpad 9 key
|
||||
F1, //!< The F1 key
|
||||
F2, //!< The F2 key
|
||||
F3, //!< The F3 key
|
||||
F4, //!< The F4 key
|
||||
F5, //!< The F5 key
|
||||
F6, //!< The F6 key
|
||||
F7, //!< The F7 key
|
||||
F8, //!< The F8 key
|
||||
F9, //!< The F9 key
|
||||
F10, //!< The F10 key
|
||||
F11, //!< The F11 key
|
||||
F12, //!< The F12 key
|
||||
F13, //!< The F13 key
|
||||
F14, //!< The F14 key
|
||||
F15, //!< The F15 key
|
||||
Pause, //!< The Pause key
|
||||
};
|
||||
|
||||
|
||||
struct KeyboardEvent : public Event {
|
||||
KeyboardEvent(Key key_code) : key_code(key_code) {}
|
||||
Key key_code;
|
||||
};
|
||||
|
||||
struct SurfaceEvent : public Event {
|
||||
enum class EventType { CLOSED, OPENED };
|
||||
|
||||
EventType type;
|
||||
};
|
||||
|
||||
struct SurfaceResizeEvent : public Event {
|
||||
SurfaceResizeEvent(unsigned int width, unsigned int height) : width(width), height(height) {}
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
};
|
||||
|
||||
template<typename Derived>
|
||||
using StandardEventHandler = EventHandler<Derived, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>;
|
||||
|
||||
template<typename Derived>
|
||||
using StandardEventQueue = EventQueue<Derived, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>;
|
||||
|
||||
#endif // STANDARDEVENTS_HPP
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 27.07.2025.
|
||||
//
|
||||
|
||||
#ifndef SUBSURFACE_HPP
|
||||
#define SUBSURFACE_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Pixel.hpp"
|
||||
#include "StandardEvents.hpp"
|
||||
#include "Surface.hpp"
|
||||
#include "Window.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
template<typename SurfaceParent>
|
||||
class SubSurface : public Surface<SubSurface<SurfaceParent>, typename SurfaceParent::PixelType> {
|
||||
public:
|
||||
using PixelType = typename SurfaceParent::PixelType;
|
||||
|
||||
SubSurface(SurfaceParent* parent) : _parent(parent) {}
|
||||
SubSurface(SubSurface<SurfaceParent>* parent) : _parent(parent->_parent) {}
|
||||
|
||||
void draw_pixel_impl(unsigned x, unsigned y, const PixelType& pixel) {
|
||||
if (x >= _x_size || y >= _y_size) {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
_parent->draw_pixel(x + _x_offset, y + _y_offset, pixel);
|
||||
}
|
||||
|
||||
unsigned get_x_offset() const { return _x_offset; }
|
||||
|
||||
unsigned get_y_offset() const { return _y_offset; }
|
||||
|
||||
unsigned get_width_impl() const { return _x_size; }
|
||||
|
||||
unsigned get_height_impl() const { return _y_size; }
|
||||
|
||||
void set_pos(unsigned x_offset, unsigned y_offset, unsigned x_size, unsigned y_size) {
|
||||
_x_offset = x_offset;
|
||||
_y_offset = y_offset;
|
||||
_x_size = x_size;
|
||||
_y_size = y_size;
|
||||
this->handle(SurfaceResizeEvent(x_size, y_size));
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned _x_offset = 0;
|
||||
unsigned _y_offset = 0;
|
||||
unsigned _x_size = 0;
|
||||
unsigned _y_size = 0;
|
||||
|
||||
SurfaceParent* _parent;
|
||||
};
|
||||
|
||||
#endif // SUBSURFACE_HPP
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef SURFACE_HPP
|
||||
#define SURFACE_HPP
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Pixel.hpp"
|
||||
#include "StandardEvents.hpp"
|
||||
#include "Window.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
template<typename Derived, typename PixelType>
|
||||
requires std::is_base_of_v<Pixel, PixelType>
|
||||
class Surface : public StandardEventHandler<Derived> {
|
||||
public:
|
||||
Surface() { static_assert(std::is_same_v<PixelType, typename Derived::PixelType>); }
|
||||
|
||||
void draw_pixel(unsigned x, unsigned y, const BwPixel& pixel) {
|
||||
static_cast<Derived*>(this)->draw_pixel_impl(x, y, pixel);
|
||||
}
|
||||
|
||||
void draw_rect(unsigned x, unsigned y, unsigned width, unsigned height, const BwPixel& pixel) {
|
||||
for (unsigned i = 0; i < width; ++i) {
|
||||
draw_pixel(x + i, y, pixel);
|
||||
draw_pixel(x + i, y + height - 1, pixel);
|
||||
}
|
||||
for (unsigned i = 0; i < height; ++i) {
|
||||
draw_pixel(x, y + i, pixel);
|
||||
draw_pixel(x + width - 1, y + i, pixel);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
for (unsigned x = 0; x < get_width(); x++) {
|
||||
for (unsigned y = 0; y < get_height(); y++) {
|
||||
draw_pixel(x, y, PixelType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get_width() const { return static_cast<const Derived*>(this)->get_width_impl(); }
|
||||
|
||||
int get_height() const { return static_cast<const Derived*>(this)->get_height_impl(); }
|
||||
|
||||
template<typename T>
|
||||
EventHandlingResult handle(const T& event) {
|
||||
if (_window.get())
|
||||
return _window->handle(event);
|
||||
return EventHandlingResult::CONTINUE;
|
||||
}
|
||||
|
||||
template<typename WindowType, typename... Args>
|
||||
void set_window(Args&&... args) {
|
||||
_window = std::make_unique<WindowType>(static_cast<Derived*>(this), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
Surface(const Surface& other) = delete;
|
||||
|
||||
Surface(Surface&& other) noexcept = delete;
|
||||
|
||||
Surface& operator=(const Surface& other) = delete;
|
||||
|
||||
Surface& operator=(Surface&& other) noexcept = delete;
|
||||
|
||||
bool has_window() const { return _window != nullptr; }
|
||||
|
||||
Window<Derived>* get_window() {
|
||||
assert(has_window());
|
||||
return _window.get();
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Window<Derived>> _window = nullptr;
|
||||
};
|
||||
|
||||
#endif // SURFACE_HPP
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef TEXTWINDOW_HPP
|
||||
#define TEXTWINDOW_HPP
|
||||
#include <string>
|
||||
|
||||
#include "Fonts.hpp"
|
||||
#include "Window.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
template<typename StringType>
|
||||
struct TextUpdateEvent : public Event {
|
||||
TextUpdateEvent(StringType text) : new_text(std::move(text)) {}
|
||||
StringType new_text;
|
||||
};
|
||||
|
||||
template<typename SurfaceType, typename StringType>
|
||||
class TextWindow : public Window<SurfaceType>,
|
||||
public EventHandler<TextUpdateEvent<StringType>>,
|
||||
public EventQueue<TextWindow<SurfaceType, StringType>, TextUpdateEvent<StringType>> {
|
||||
public:
|
||||
using PixelType = typename SurfaceType::PixelType;
|
||||
|
||||
explicit TextWindow(SurfaceType* owner, EventLoop* loop, StringType text = "") :
|
||||
Window<SurfaceType>(owner), EventQueue<TextWindow, TextUpdateEvent<StringType>>(loop, this),
|
||||
_text(std::move(text)) {}
|
||||
|
||||
EventHandlingResult handle_v(SurfaceResizeEvent resize) override {
|
||||
refresh();
|
||||
return EventHandlingResult::DONE;
|
||||
}
|
||||
|
||||
EventHandlingResult handle(TextUpdateEvent<StringType> event) {
|
||||
_text = std::move(event.new_text);
|
||||
refresh();
|
||||
return EventHandlingResult::DONE;
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
this->_owner->clear();
|
||||
size_t _max_col = this->_owner->get_width() / 8;
|
||||
size_t _max_row = this->_owner->get_height() / 16;
|
||||
int col = 0, row = 0;
|
||||
for (char c: _text) {
|
||||
if (c == '\n' || col >= _max_col) {
|
||||
row++;
|
||||
col = 0;
|
||||
if (c == '\n')
|
||||
continue;
|
||||
}
|
||||
|
||||
if (row >= _max_row) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (int x = 0; x < 8; x++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
bool color = fonts_Terminess_Powerline[c][y] & (1 << (8 - x));
|
||||
this->_owner->draw_pixel(col * 8 + x, row * 16 + y, PixelType(color));
|
||||
}
|
||||
}
|
||||
col++;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
StringType _text;
|
||||
};
|
||||
|
||||
#endif // TEXTWINDOW_HPP
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef WINDOW_HPP
|
||||
#define WINDOW_HPP
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "Event.hpp"
|
||||
#include "Pixel.hpp"
|
||||
#include "StandardEvents.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
template<typename Derived, typename PixelType>
|
||||
requires std::is_base_of_v<Pixel, PixelType>
|
||||
class Surface;
|
||||
|
||||
template<typename SurfaceType>
|
||||
class Window : StandardEventHandler<Window<SurfaceType>> {
|
||||
public:
|
||||
using PixelType = typename SurfaceType::PixelType;
|
||||
|
||||
explicit Window(SurfaceType* owner) : _owner(owner) {
|
||||
// static_assert(is_specialization_of<Surface, SurfaceType>::value);
|
||||
}
|
||||
|
||||
virtual ~Window() = default;
|
||||
|
||||
EventHandlingResult handle(auto Event) { return handle_v(Event); }
|
||||
|
||||
virtual EventHandlingResult handle_v(KeyboardEvent) { return EventHandlingResult::CONTINUE; }
|
||||
virtual EventHandlingResult handle_v(SurfaceEvent) { return EventHandlingResult::CONTINUE; }
|
||||
virtual EventHandlingResult handle_v(SurfaceResizeEvent) { return EventHandlingResult::CONTINUE; }
|
||||
|
||||
protected:
|
||||
SurfaceType* _owner = nullptr;
|
||||
};
|
||||
|
||||
#endif // SURFACE_HPP
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 27.07.2025.
|
||||
//
|
||||
|
||||
#ifndef UTILS_HPP
|
||||
#define UTILS_HPP
|
||||
|
||||
template <template <typename...> class T, typename U>
|
||||
struct is_specialization_of: std::false_type {};
|
||||
|
||||
template <template <typename...> class T, typename... Us>
|
||||
struct is_specialization_of<T, T<Us...>>: std::true_type {};
|
||||
|
||||
#endif //UTILS_HPP
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#include "Event.hpp"
|
||||
@@ -1,8 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#include "TextWindow.hpp"
|
||||
#include "Fonts.hpp"
|
||||
#include "Surface.hpp"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#include "Window.hpp"
|
||||
@@ -1,23 +0,0 @@
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
|
||||
)
|
||||
# For Windows: Prevent overriding the parent project's compiler/linker settings
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
enable_testing()
|
||||
include(GoogleTest)
|
||||
|
||||
add_executable(
|
||||
EventTests
|
||||
src/EventTests.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
EventTests PRIVATE
|
||||
GTest::gtest_main cbsdk
|
||||
)
|
||||
|
||||
gtest_discover_tests(EventTests DISCOVERY_TIMEOUT 600)
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Event.hpp"
|
||||
|
||||
struct EventOne : public Event {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct EventTwo : public Event {
|
||||
int value;
|
||||
};
|
||||
|
||||
template<typename Derived>
|
||||
using TestEventHandler = EventHandler<Derived, EventOne, EventTwo>;
|
||||
|
||||
class EventHandlerTest : public TestEventHandler<EventHandlerTest> {
|
||||
public:
|
||||
template<typename T>
|
||||
void handle(const T& event) {
|
||||
seen_unknown = true;
|
||||
}
|
||||
|
||||
void handle(const EventOne& event) {
|
||||
seen_event_one = true;
|
||||
}
|
||||
|
||||
bool seen_event_one = false;
|
||||
bool seen_unknown = false;
|
||||
};
|
||||
|
||||
TEST(Event, Basic) {
|
||||
EventHandlerTest handler;
|
||||
EventOne event_one;
|
||||
EventTwo event_two;
|
||||
handler.handle(event_one);
|
||||
ASSERT_TRUE(handler.seen_event_one);
|
||||
handler.handle(event_two);
|
||||
ASSERT_TRUE(handler.seen_unknown);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(SFML
|
||||
GIT_REPOSITORY https://github.com/SFML/SFML.git
|
||||
GIT_TAG 3.0.1
|
||||
GIT_SHALLOW ON
|
||||
EXCLUDE_FROM_ALL
|
||||
SYSTEM)
|
||||
FetchContent_MakeAvailable(SFML)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
# if (NOT DEFINED SANITIZE)
|
||||
# set(SANITIZE YES)
|
||||
# endif ()
|
||||
endif ()
|
||||
|
||||
if (SANITIZE STREQUAL "YES")
|
||||
message(STATUS "Enabling sanitizers!")
|
||||
add_compile_options(-Werror -O0 -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-unused-variable
|
||||
-Wno-error=unused-function
|
||||
-Wshadow -Wformat=2 -Wfloat-equal -D_GLIBCXX_DEBUG -Wconversion)
|
||||
add_compile_options(-fsanitize=address -fno-sanitize-recover)
|
||||
add_link_options(-fsanitize=address -fno-sanitize-recover)
|
||||
endif ()
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
|
||||
endif ()
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
add_compile_options(-O3)
|
||||
add_link_options(-O3)
|
||||
endif ()
|
||||
|
||||
add_executable(main src/main.cpp
|
||||
src/SfmlWindow.cpp
|
||||
include_public/SfmlWindow.hpp)
|
||||
|
||||
target_include_directories(main PRIVATE include)
|
||||
target_include_directories(main PUBLIC include_public)
|
||||
target_link_libraries(main PRIVATE SFML::Graphics)
|
||||
target_link_libraries(main PUBLIC cbsdk)
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef SFMLWINDOW_HPP
|
||||
#define SFMLWINDOW_HPP
|
||||
|
||||
#include "Surface.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
class SfmlSurface : public Surface<SfmlSurface, BwPixel>, public StandardEventQueue<SfmlSurface> {
|
||||
public:
|
||||
using PixelType = BwPixel;
|
||||
|
||||
SfmlSurface(EventLoop* loop);
|
||||
|
||||
~SfmlSurface(); // override;
|
||||
|
||||
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
|
||||
|
||||
unsigned get_width_impl() const;
|
||||
|
||||
unsigned get_height_impl() const;
|
||||
|
||||
template<typename T>
|
||||
EventHandlingResult handle(const T& event) {
|
||||
return _window->handle(event);
|
||||
}
|
||||
|
||||
EventHandlingResult handle(SurfaceResizeEvent event);
|
||||
|
||||
sf::RenderWindow _sf_window;
|
||||
|
||||
sf::Image _image;
|
||||
sf::Texture _texture;
|
||||
sf::Sprite _sprite;
|
||||
};
|
||||
|
||||
#endif // SFMLWINDOW_HPP
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#include "SfmlWindow.hpp"
|
||||
|
||||
void SfmlSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
|
||||
_image.setPixel({x, y}, pixel.on ? sf::Color::Black : sf::Color::White);
|
||||
}
|
||||
|
||||
unsigned SfmlSurface::get_width_impl() const { return _image.getSize().x; }
|
||||
|
||||
unsigned SfmlSurface::get_height_impl() const { return _image.getSize().y; }
|
||||
|
||||
EventHandlingResult SfmlSurface::handle(SurfaceResizeEvent event) {
|
||||
_sf_window.clear();
|
||||
_image.resize({event.width, event.height});
|
||||
_texture.resize({event.width, event.height});
|
||||
_texture.update(_image);
|
||||
_sprite = sf::Sprite(_texture);
|
||||
sf::FloatRect view({0, 0}, {static_cast<float>(event.width), static_cast<float>(event.height)});
|
||||
_sf_window.setView(sf::View(view));
|
||||
return _window->handle(event);
|
||||
}
|
||||
|
||||
SfmlSurface::SfmlSurface(EventLoop* loop) :
|
||||
Surface<SfmlSurface, BwPixel>(),
|
||||
EventQueue<SfmlSurface, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>(loop, this),
|
||||
_sf_window(sf::VideoMode({640, 480}), "Test"), _image({640, 480}, sf::Color::White), _texture(_image),
|
||||
_sprite(_texture) {
|
||||
_sf_window.setFramerateLimit(60);
|
||||
_sf_window.clear();
|
||||
_sf_window.draw(_sprite);
|
||||
_sf_window.display();
|
||||
}
|
||||
|
||||
SfmlSurface::~SfmlSurface() {}
|
||||
@@ -1,75 +0,0 @@
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <barrier>
|
||||
#include <latch>
|
||||
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "GridWindow.hpp"
|
||||
#include "SfmlWindow.hpp"
|
||||
#include "TextWindow.hpp"
|
||||
|
||||
int main() {
|
||||
EventLoop loop;
|
||||
std::latch barrier{1};
|
||||
|
||||
SfmlSurface* surface_ptr;
|
||||
int i = 0;
|
||||
std::thread loop_thread{[&] {
|
||||
SfmlSurface surface(&loop);
|
||||
surface_ptr = &surface;
|
||||
|
||||
barrier.count_down();
|
||||
|
||||
surface.set_window<GridWindow<SfmlSurface, 2, 2>>();
|
||||
|
||||
GridWindow<SfmlSurface, 2, 2>* window =
|
||||
static_cast<GridWindow<SfmlSurface, 2, 2>*>(surface.get_window());
|
||||
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(0, 0, &loop, "hello");
|
||||
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(0, 1, &loop, "hello1");
|
||||
window->set_window<GridWindow<SubSurface<SfmlSurface>, 2, 2>>(1, 0);
|
||||
GridWindow<SubSurface<SfmlSurface>, 2, 2>* window2 =
|
||||
static_cast<GridWindow<SubSurface<SfmlSurface>, 2, 2>*>(
|
||||
window->get_subsurface(1, 0).get_window());
|
||||
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(1, 1, &loop, "hello3");
|
||||
|
||||
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
|
||||
0, 0, &loop, "hello2");
|
||||
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
|
||||
0, 1, &loop, "hello4");
|
||||
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
|
||||
1, 0, &loop, "hello5");
|
||||
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
|
||||
1, 1, &loop, "hello6");
|
||||
loop.run([&] {
|
||||
surface._sf_window.clear();
|
||||
surface._texture.update(surface._image);
|
||||
surface._sf_window.draw(surface._sprite);
|
||||
surface._sf_window.display();
|
||||
static_cast<TextWindow<SubSurface<SfmlSurface>, std::string>*>(
|
||||
window->get_subsurface(0, 0).get_window())
|
||||
->push(TextUpdateEvent<std::string>{std::string("Hello, SFML!") + std::to_string(i++)});
|
||||
});
|
||||
}};
|
||||
|
||||
barrier.wait();
|
||||
|
||||
while (surface_ptr->_sf_window.isOpen()) {
|
||||
while (const std::optional event = surface_ptr->_sf_window.pollEvent()) {
|
||||
if (event->is<sf::Event::Closed>()) {
|
||||
surface_ptr->_sf_window.close();
|
||||
loop.push(LoopQuitEvent{});
|
||||
}
|
||||
if (event->is<sf::Event::Resized>()) {
|
||||
auto newSize = event->getIf<sf::Event::Resized>()->size;
|
||||
surface_ptr->push(SurfaceResizeEvent{newSize.x, newSize.y});
|
||||
}
|
||||
if (event->is<sf::Event::KeyPressed>()) {
|
||||
auto key = event->getIf<sf::Event::KeyPressed>();
|
||||
surface_ptr->push(KeyboardEvent{static_cast<Key>(key->code)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop_thread.join();
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
#include "app_system.hpp"
|
||||
|
||||
#include <buttons.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
namespace {
|
||||
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
|
||||
return a.up != b.up || a.down != b.down || a.left != b.left || a.right != b.right || a.a != b.a || a.b != b.b ||
|
||||
a.select != b.select || a.start != b.start;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t kIdlePollMs = 16;
|
||||
} // namespace
|
||||
|
||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
|
||||
@@ -26,10 +24,9 @@ void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
||||
|
||||
bool AppSystem::startApp(const std::string& name) {
|
||||
for (std::size_t i = 0; i < factories.size(); ++i) {
|
||||
if (factories[i]->name() == name) {
|
||||
if (factories[i]->name() == name)
|
||||
return startAppByIndex(i);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -66,7 +63,6 @@ void AppSystem::run() {
|
||||
return;
|
||||
}
|
||||
|
||||
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
|
||||
std::vector<AppEvent> events;
|
||||
events.reserve(4);
|
||||
|
||||
@@ -88,24 +84,23 @@ void AppSystem::run() {
|
||||
|
||||
for (const auto& evt: events) {
|
||||
dispatchEvent(evt);
|
||||
if (handlePendingSwitchRequest()) {
|
||||
if (handlePendingSwitchRequest())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const std::uint32_t waitBase = context.clock.millis();
|
||||
const std::uint32_t waitMs = nextTimerDueMs(waitBase);
|
||||
TickType_t waitTicks;
|
||||
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
|
||||
waitTicks = portMAX_DELAY;
|
||||
} else {
|
||||
waitTicks = pdMS_TO_TICKS(waitMs);
|
||||
if (waitTicks == 0)
|
||||
waitTicks = 1;
|
||||
}
|
||||
std::uint32_t waitMs = nextTimerDueMs(waitBase);
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, waitTicks);
|
||||
if (waitMs == 0)
|
||||
continue;
|
||||
|
||||
if (waitMs == std::numeric_limits<std::uint32_t>::max())
|
||||
waitMs = kIdlePollMs;
|
||||
else
|
||||
waitMs = std::min(waitMs, kIdlePollMs);
|
||||
|
||||
if (waitMs > 0)
|
||||
context.clock.sleep_ms(waitMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +120,7 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
|
||||
return static_cast<std::size_t>(-1);
|
||||
}
|
||||
|
||||
AppTimerHandle AppSystem::scheduleTimer(uint32_t delay_ms, bool repeat) {
|
||||
AppTimerHandle AppSystem::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
|
||||
if (!current)
|
||||
return kInvalidAppTimer;
|
||||
TimerRecord record;
|
||||
@@ -225,10 +220,12 @@ bool AppSystem::handlePendingSwitchRequest() {
|
||||
context.pendingSwitchByName = false;
|
||||
context.pendingAppName.clear();
|
||||
bool switched = false;
|
||||
if (byName) {
|
||||
if (byName)
|
||||
switched = startApp(reqName);
|
||||
} else {
|
||||
else
|
||||
switched = startAppByIndex(reqIndex);
|
||||
}
|
||||
return switched;
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
Reference in New Issue
Block a user