module; #include export module app; import coral; import coral.files; import coral.image; import coral.io; import coral.math; import oar; namespace app { struct directory : public coral::fs { struct rules { bool can_read; bool can_write; }; virtual rules access_rules() const = 0; virtual coral::slice native_path() const = 0; }; struct file : public coral::file_reader, public coral::file_writer { enum class open_mode { read_only, overwrite, }; enum class [[nodiscard]] close_result { ok, io_unavailable, }; enum class [[nodiscard]] open_result { ok, io_unavailable, access_denied, not_found, }; file(directory * file_directory) : rw_ops{nullptr} { this->file_directory = file_directory; } close_result close() { if (::SDL_RWclose(this->rw_ops) != 0) return close_result::io_unavailable; this->rw_ops = nullptr; return close_result::ok; } bool is_open() const { return this->rw_ops != nullptr; } open_result open(coral::path const & file_path, open_mode mode) { if (this->is_open()) switch (this->close()) { case close_result::ok: break; case close_result::io_unavailable: return open_result::io_unavailable; default: coral::unreachable(); } // Windows system path max is something like 512 bytes (depending on the version) and // on Linux it's 4096. coral::fixed_buffer<4096> path_buffer{0}; if (!path_buffer.write(this->file_directory->native_path().as_bytes()).is_ok()) // What kind of directory path is being used that would be longer than 4096 bytes? return open_result::not_found; if (!path_buffer.write(file_path.as_slice().as_bytes()).is_ok()) // This happening is still implausible but slightly more reasonable? return open_result::not_found; // No room for zero terminator. if (path_buffer.is_full()) return open_result::not_found; switch (mode) { case open_mode::read_only: { if (!this->file_directory->access_rules().can_read) return open_result::access_denied; this->rw_ops = ::SDL_RWFromFile( reinterpret_cast(path_buffer.begin()), "rb"); break; } case open_mode::overwrite: { if (!this->file_directory->access_rules().can_write) return open_result::access_denied; this->rw_ops = ::SDL_RWFromFile( reinterpret_cast(path_buffer.begin()), "wb"); break; } default: coral::unreachable(); } if (this->rw_ops == nullptr) return open_result::not_found; return open_result::ok; } coral::expected read(coral::slice const & data) override { if (!this->is_open()) return coral::io_error::unavailable; coral::usize const data_read{::SDL_RWread(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)}; if ((data_read == 0) && (::SDL_GetError() != nullptr)) return coral::io_error::unavailable; return data_read; } coral::expected seek(coral::u64 offset) override { if (!this->is_open()) return coral::io_error::unavailable; // TODO: Fix cast. coral::i64 const byte_position{ ::SDL_RWseek(this->rw_ops, static_cast(offset), RW_SEEK_SET)}; if (byte_position == -1) return coral::io_error::unavailable; return static_cast(byte_position); } coral::expected tell() override { if (!this->is_open()) return coral::io_error::unavailable; coral::i64 const byte_position{::SDL_RWseek(this->rw_ops, 0, RW_SEEK_SET)}; if (byte_position == -1) return coral::io_error::unavailable; return static_cast(byte_position); } coral::expected write(coral::slice const & data) override { if (!this->is_open()) return coral::io_error::unavailable; coral::usize const data_written{::SDL_RWwrite(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)}; if ((data_written == 0) && (::SDL_GetError() != nullptr)) return coral::io_error::unavailable; return data_written; } private: directory * file_directory; ::SDL_RWops * rw_ops; }; } struct base_directory : public app::directory { base_directory() : directory_path{} { char * const path{::SDL_GetBasePath()}; if (path == nullptr) return; coral::usize path_length{0}; while (path[path_length] != 0) path_length += 1; if (path_length == 0) { ::SDL_free(path); return; } this->directory_path = {path, path_length}; } ~base_directory() override { ::SDL_free(this->directory_path.begin()); } rules access_rules() const override { return { .can_read = true, .can_write = false, }; } coral::slice native_path() const override { return this->directory_path; } void read_file(coral::path const & file_path, coral::callable const & then) override { if (this->directory_path.length == 0) return; app::file file{this}; if (file.open(file_path, app::file::open_mode::read_only) != app::file::open_result::ok) return; then(file); if (file.close() != app::file::close_result::ok) return; } void write_file(coral::path const & file_path, coral::callable const & then) override { // Directory is read-only. } protected: coral::slice directory_path; }; struct user_directory : public app::directory { user_directory(coral::path const & title) : directory_path{} { char * const path{::SDL_GetPrefPath("ona", title.begin())}; if (path == nullptr) return; coral::usize path_length{0}; while (path[path_length] != 0) path_length += 1; if (path_length == 0) { ::SDL_free(path); return; } this->directory_path = {path, path_length}; } ~user_directory() override { ::SDL_free(this->directory_path.begin()); } rules access_rules() const override { return { .can_read = true, .can_write = false, }; } coral::slice native_path() const override { return this->directory_path; } void read_file(coral::path const & file_path, coral::callable const & then) override { if (this->directory_path.length == 0) return; app::file file{this}; if (file.open(file_path, app::file::open_mode::read_only) != app::file::open_result::ok) return; then(file); if (file.close() != app::file::close_result::ok) return; } void write_file(coral::path const & file_path, coral::callable const & then) override { if (this->directory_path.length == 0) return; app::file file{this}; if (file.open(file_path, app::file::open_mode::overwrite) != app::file::open_result::ok) return; then(file); if (file.close() != app::file::close_result::ok) return; } protected: coral::slice directory_path; }; export namespace app { enum class log_level { notice, warning, error, }; struct system { system(coral::path const & title) : user_directory{title}, res_archive{&base_directory, res_archive_path} {} app::directory & base_dir() { return this->base_directory; } bool poll() { while (::SDL_PollEvent(&this->sdl_event) != 0) { switch (this->sdl_event.type) { case SDL_QUIT: return false; } } return true; } coral::fs & res_fs() { return this->res_archive; } void log(app::log_level level, coral::slice const & message) { coral::i32 const length{static_cast( coral::min(message.length, static_cast(coral::i32_max)))}; ::SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer); } coral::allocator & thread_safe_allocator() { return this->allocator; } app::directory & user_dir() { return this->user_directory; } private: static constexpr coral::path res_archive_path{"base.oar"}; ::SDL_Event sdl_event; struct : public coral::allocator { coral::u8 * reallocate(coral::u8 * maybe_allocation, coral::usize requested_size) override { return reinterpret_cast(::SDL_malloc(requested_size)); } void deallocate(void * allocation) override { ::SDL_free(allocation); } } allocator; struct base_directory base_directory; struct user_directory user_directory; oar::archive res_archive; }; struct graphics { enum class [[nodiscard]] show_result { ok, out_of_memory, }; struct canvas { coral::color background_color; }; graphics(coral::path const & title) { this->retitle(title); } void present() { if (this->sdl_renderer != nullptr) { ::SDL_RenderPresent(this->sdl_renderer); } } void render(canvas & source_canvas) { if (this->sdl_renderer != nullptr) { SDL_SetRenderDrawColor(this->sdl_renderer, source_canvas.background_color.to_r8(), source_canvas.background_color.to_g8(), source_canvas.background_color.to_b8(), source_canvas.background_color.to_a8()); SDL_RenderClear(this->sdl_renderer); } } void retitle(coral::path const & title) { this->title = title; if (this->sdl_window != nullptr) ::SDL_SetWindowTitle(this->sdl_window, this->title.begin()); } show_result show(coral::u16 physical_width, coral::u16 physical_height) { if (this->sdl_window == nullptr) { constexpr int sdl_windowpos = SDL_WINDOWPOS_UNDEFINED; constexpr coral::u32 sdl_windowflags = 0; this->sdl_window = ::SDL_CreateWindow(this->title.begin(), sdl_windowpos, sdl_windowpos, static_cast(physical_width), static_cast(physical_height), sdl_windowflags); if (this->sdl_window == nullptr) return show_result::out_of_memory; } else { ::SDL_ShowWindow(this->sdl_window); } if (this->sdl_renderer == nullptr) { constexpr coral::u32 sdl_rendererflags = 0; this->sdl_renderer = ::SDL_CreateRenderer(this->sdl_window, -1, sdl_rendererflags); if (this->sdl_renderer == nullptr) return show_result::out_of_memory; } return show_result::ok; } private: coral::path title; ::SDL_Window * sdl_window = nullptr; ::SDL_Renderer * sdl_renderer = nullptr; }; using graphical_runnable = coral::callable; int display(coral::path const & title, graphical_runnable const & run) { system app_system{title}; graphics app_graphics{title}; return run(app_system, app_graphics); } }