mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-02 08:43:42 -04:00
Mods: FileService (#2367)
This commit is contained in:
+1
-1
@@ -304,7 +304,7 @@ include(cmake/GameABIConfig.cmake)
|
||||
find_package(Threads REQUIRED)
|
||||
set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1)
|
||||
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
|
||||
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||
if (DUSK_HAS_FUNCHOOK)
|
||||
list(APPEND GAME_LIBS funchook-static)
|
||||
|
||||
+47
-2
@@ -48,7 +48,7 @@ cmake_minimum_required(VERSION 3.26)
|
||||
project(my_mod CXX)
|
||||
|
||||
if (NOT DUSKLIGHT_VERSION)
|
||||
set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f")
|
||||
set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f")
|
||||
endif ()
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake")
|
||||
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
|
||||
@@ -212,6 +212,51 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
|
||||
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. The bundle is read-only; use
|
||||
`HostService::data_dir` for persistent storage.
|
||||
|
||||
### FileService (`mods/svc/file.h`)
|
||||
|
||||
Provides file and folder pickers, file I/O, exports and folder enumeration.
|
||||
|
||||
A location is an opaque UTF-8 string returned by `pick_*`, `export_file`, `join`, or `create_child`.
|
||||
Save it and pass it back to the service. Never parse it or manually append path segments.
|
||||
|
||||
```cpp
|
||||
#include "mods/svc/file.hpp"
|
||||
|
||||
IMPORT_SERVICE(FileService, svc_file);
|
||||
|
||||
mods::file::PickOptions options;
|
||||
options.filters.push_back({"Audio", "wav;ogg"});
|
||||
mods::file::pick_file(options, [](mods::file::PickResult result) {
|
||||
if (result.status == MOD_OK && !result.locations.empty()) {
|
||||
save_location(result.locations.front());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Use `check` before reopening a saved location because removable storage or an access grant may no longer be available.
|
||||
`open` provides seekable streaming I/O. `read_all` allocates the entire file and should only be used when the file is
|
||||
small. Folder locations support `list` and child resolution through `join`. Only one picker can be open at a time.
|
||||
|
||||
`create_child` never replaces an existing file and returns `MOD_CONFLICT` if one exists. Use the returned location;
|
||||
document providers may adjust the requested name. `write_all` is a convenience function over
|
||||
`open`/`write`/`flush`/`close`.
|
||||
|
||||
```cpp
|
||||
std::string location;
|
||||
if (mods::file::create_child(folder, "report.txt", location) == MOD_OK) {
|
||||
mods::file::write_all(location, report);
|
||||
}
|
||||
|
||||
mods::file::export_file(location, "report.txt", [](mods::file::PickResult result) {
|
||||
if (result.status == MOD_OK) {
|
||||
remember_export_destination(result.locations.front());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`export_file` copies an existing file to a user-selected destination and returns the destination location in its
|
||||
callback. Mod-owned persistent files belong in `HostService::data_dir`.
|
||||
|
||||
### HostService (`mods/svc/host.h`)
|
||||
|
||||
Mod metadata and runtime interaction with the loader:
|
||||
@@ -587,7 +632,7 @@ svc_ui->dialog_push(mod_ctx, &dialog, nullptr);
|
||||
```
|
||||
|
||||
After an action's `on_pressed`, the dialog closes unless the action sets `keep_open`. It can then be closed later
|
||||
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
|
||||
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
|
||||
`dialog_set_icon` mutate a live dialog.
|
||||
|
||||
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
|
||||
|
||||
Vendored
+1
-1
Submodule extern/borealis updated: 6fd955e7e6...32153a5480
@@ -1492,6 +1492,7 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/camera.cpp
|
||||
src/dusk/mods/svc/config.cpp
|
||||
src/dusk/mods/svc/config.hpp
|
||||
src/dusk/mods/svc/file.cpp
|
||||
src/dusk/mods/svc/game.cpp
|
||||
src/dusk/mods/svc/gfx.cpp
|
||||
src/dusk/mods/svc/flow.cpp
|
||||
@@ -1549,6 +1550,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/graphics_tuner.hpp
|
||||
src/dusk/ui/group_button.cpp
|
||||
src/dusk/ui/group_button.hpp
|
||||
src/dusk/ui/file_button.cpp
|
||||
src/dusk/ui/file_button.hpp
|
||||
src/dusk/ui/icon_provider.cpp
|
||||
src/dusk/ui/icon_provider.hpp
|
||||
src/dusk/ui/input.cpp
|
||||
|
||||
@@ -17,5 +17,4 @@ add_mod(ao_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define FILE_SERVICE_ID "dev.twilitrealm.dusklight.file"
|
||||
#define FILE_SERVICE_MAJOR 1u
|
||||
#define FILE_SERVICE_MINOR 0u
|
||||
|
||||
typedef uint64_t FileStreamHandle;
|
||||
|
||||
typedef enum FileOpenMode {
|
||||
FILE_OPEN_READ = 0,
|
||||
FILE_OPEN_TRUNCATE = 1,
|
||||
FILE_OPEN_APPEND = 2,
|
||||
} FileOpenMode;
|
||||
|
||||
typedef struct FileFilter {
|
||||
const char* name;
|
||||
/* Semicolon-separated extensions or "*". */
|
||||
const char* pattern;
|
||||
} FileFilter;
|
||||
|
||||
typedef struct FilePickOptions {
|
||||
uint32_t struct_size;
|
||||
const FileFilter* filters;
|
||||
uint32_t filter_count;
|
||||
/* Optional previously returned location. */
|
||||
const char* default_location;
|
||||
} FilePickOptions;
|
||||
|
||||
#define FILE_PICK_OPTIONS_INIT {sizeof(FilePickOptions), NULL, 0u, NULL}
|
||||
|
||||
/* Locations and error are valid only for the duration of the callback. Canceled picks report
|
||||
* MOD_UNAVAILABLE. The callback runs on the game thread. */
|
||||
typedef void (*FilePickFn)(ModContext* ctx, ModResult status, const char* const* locations,
|
||||
uint32_t location_count, const char* error, void* user_data);
|
||||
|
||||
typedef struct FileBuffer {
|
||||
uint32_t struct_size;
|
||||
void* data;
|
||||
size_t size;
|
||||
} FileBuffer;
|
||||
|
||||
#define FILE_BUFFER_INIT {sizeof(FileBuffer), NULL, 0u}
|
||||
|
||||
typedef struct FileEntry {
|
||||
const char* name;
|
||||
const char* location;
|
||||
bool is_directory;
|
||||
} FileEntry;
|
||||
|
||||
/* Called once per entry, then once with entry == NULL. Entries are valid only for the duration of
|
||||
* the callback. */
|
||||
typedef void (*FileListFn)(ModContext* ctx, const FileEntry* entry, void* user_data);
|
||||
|
||||
/*
|
||||
* Access to user-selected files and folders.
|
||||
*
|
||||
* A location is an opaque UTF-8 string returned by `pick_*`, `export_file`, `join`, `create_child`.
|
||||
* Save it and pass it back to the service. Never parse it or manually append path segments.
|
||||
*
|
||||
* Android: Security grants are restored across launches. Only 512 (or 128 before API 30) grants are
|
||||
* allowed at a time. If a grant cannot be retained or was revoked, `check`/`open` returns
|
||||
* MOD_UNAVAILABLE so the user can select the location again.
|
||||
*
|
||||
* Calls other than picker completion are synchronous and must run on the game thread. Avoid file
|
||||
* reads and folder traversal in per-frame callbacks.
|
||||
*/
|
||||
typedef struct FileService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* One native dialog is allowed at a time. Returns MOD_CONFLICT while one is outstanding. */
|
||||
ModResult (*pick_file)(
|
||||
ModContext* ctx, const FilePickOptions* options, FilePickFn fn, void* user_data);
|
||||
ModResult (*pick_folder)(
|
||||
ModContext* ctx, const FilePickOptions* options, FilePickFn fn, void* user_data);
|
||||
/* Exports an existing file to a user-chosen destination. fn receives its final location. */
|
||||
ModResult (*export_file)(ModContext* ctx, const char* source_location,
|
||||
const char* suggested_name, FilePickFn fn, void* user_data);
|
||||
|
||||
ModResult (*display_name)(
|
||||
ModContext* ctx, const char* location, char* buffer, uint32_t buffer_size);
|
||||
/* MOD_OK, MOD_UNAVAILABLE when gone or inaccessible, or MOD_UNSUPPORTED. */
|
||||
ModResult (*check)(ModContext* ctx, const char* location);
|
||||
|
||||
/* Write modes require an existing writable location and return MOD_UNSUPPORTED otherwise. */
|
||||
ModResult (*open)(
|
||||
ModContext* ctx, const char* location, FileOpenMode mode, FileStreamHandle* out_handle);
|
||||
ModResult (*size)(ModContext* ctx, FileStreamHandle handle, uint64_t* out_size);
|
||||
ModResult (*read)(ModContext* ctx, FileStreamHandle handle, void* buffer, uint64_t length,
|
||||
uint64_t* out_read);
|
||||
ModResult (*write)(
|
||||
ModContext* ctx, FileStreamHandle handle, const void* buffer, uint64_t length);
|
||||
ModResult (*seek)(ModContext* ctx, FileStreamHandle handle, uint64_t offset);
|
||||
ModResult (*flush)(ModContext* ctx, FileStreamHandle handle);
|
||||
/* Reports flush and close failures; writers must check the result. */
|
||||
ModResult (*close)(ModContext* ctx, FileStreamHandle handle);
|
||||
|
||||
ModResult (*read_all)(ModContext* ctx, const char* location, FileBuffer* out_buffer);
|
||||
/* Truncates and writes an existing location. This operation is not atomic. */
|
||||
ModResult (*write_all)(ModContext* ctx, const char* location, const void* data, size_t size);
|
||||
void (*free)(ModContext* ctx, FileBuffer* buffer);
|
||||
|
||||
ModResult (*list)(ModContext* ctx, const char* folder_location, FileListFn fn, void* user_data);
|
||||
/* out_location remains valid until this mod's next `join` or `create_child` call. */
|
||||
ModResult (*join)(ModContext* ctx, const char* folder_location, const char* relative_path,
|
||||
const char** out_location);
|
||||
|
||||
/* Creates one file without replacing an existing child. */
|
||||
ModResult (*create_child)(
|
||||
ModContext* ctx, const char* folder_location, const char* name, const char** out_location);
|
||||
} FileService;
|
||||
|
||||
MOD_DECLARE_SERVICE(FileService, svc_file, FILE_SERVICE_ID, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR);
|
||||
@@ -0,0 +1,321 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/svc/file.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mods::file {
|
||||
|
||||
class File {
|
||||
public:
|
||||
File() = default;
|
||||
File(FileStreamHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
|
||||
~File() { reset(); }
|
||||
File(const File&) = delete;
|
||||
File& operator=(const File&) = delete;
|
||||
File(File&& other) noexcept { *this = std::move(other); }
|
||||
File& operator=(File&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
mHandle = std::exchange(other.mHandle, 0);
|
||||
mResult = other.mResult;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
explicit operator bool() const { return mResult == MOD_OK && mHandle != 0; }
|
||||
ModResult result() const { return mResult; }
|
||||
FileStreamHandle handle() const { return mHandle; }
|
||||
|
||||
uint64_t size() const {
|
||||
uint64_t value = 0;
|
||||
if (mHandle != 0 && svc_file != nullptr) {
|
||||
svc_file->size(mod_ctx, mHandle, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
uint64_t read(void* buffer, uint64_t length) {
|
||||
uint64_t value = 0;
|
||||
if (mHandle == 0 || svc_file == nullptr) {
|
||||
mResult = MOD_UNAVAILABLE;
|
||||
} else {
|
||||
mResult = svc_file->read(mod_ctx, mHandle, buffer, length, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool seek(uint64_t offset) {
|
||||
mResult = mHandle != 0 && svc_file != nullptr ? svc_file->seek(mod_ctx, mHandle, offset) :
|
||||
MOD_UNAVAILABLE;
|
||||
return mResult == MOD_OK;
|
||||
}
|
||||
|
||||
bool write(const void* buffer, uint64_t length) {
|
||||
if (mHandle == 0 || svc_file == nullptr) {
|
||||
mResult = MOD_UNAVAILABLE;
|
||||
} else {
|
||||
mResult = svc_file->write(mod_ctx, mHandle, buffer, length);
|
||||
}
|
||||
return mResult == MOD_OK;
|
||||
}
|
||||
|
||||
bool write(std::span<const uint8_t> bytes) { return write(bytes.data(), bytes.size()); }
|
||||
|
||||
bool flush() {
|
||||
if (mHandle == 0 || svc_file == nullptr) {
|
||||
mResult = MOD_UNAVAILABLE;
|
||||
} else {
|
||||
mResult = svc_file->flush(mod_ctx, mHandle);
|
||||
}
|
||||
return mResult == MOD_OK;
|
||||
}
|
||||
|
||||
bool close() {
|
||||
if (mHandle == 0) {
|
||||
return mResult == MOD_OK;
|
||||
}
|
||||
mResult = svc_file != nullptr ? svc_file->close(mod_ctx, mHandle) : MOD_UNAVAILABLE;
|
||||
mHandle = 0;
|
||||
return mResult == MOD_OK;
|
||||
}
|
||||
|
||||
void reset() { (void)close(); }
|
||||
|
||||
private:
|
||||
FileStreamHandle mHandle = 0;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
inline File open(const std::string& location, FileOpenMode mode = FILE_OPEN_READ) {
|
||||
if (svc_file == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
FileStreamHandle handle = 0;
|
||||
const auto result = svc_file->open(mod_ctx, location.c_str(), mode, &handle);
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
Buffer() = default;
|
||||
Buffer(FileBuffer buffer, ModResult result) : mBuffer{buffer}, mResult{result} {}
|
||||
~Buffer() { reset(); }
|
||||
Buffer(const Buffer&) = delete;
|
||||
Buffer& operator=(const Buffer&) = delete;
|
||||
Buffer(Buffer&& other) noexcept { *this = std::move(other); }
|
||||
Buffer& operator=(Buffer&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
mBuffer = other.mBuffer;
|
||||
mResult = other.mResult;
|
||||
other.mBuffer = FILE_BUFFER_INIT;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
explicit operator bool() const { return mResult == MOD_OK; }
|
||||
ModResult result() const { return mResult; }
|
||||
std::span<const uint8_t> bytes() const {
|
||||
return {static_cast<const uint8_t*>(mBuffer.data), mBuffer.size};
|
||||
}
|
||||
void reset() {
|
||||
if (mBuffer.data != nullptr && svc_file != nullptr) {
|
||||
svc_file->free(mod_ctx, &mBuffer);
|
||||
}
|
||||
mBuffer.data = nullptr;
|
||||
mBuffer.size = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
FileBuffer mBuffer = FILE_BUFFER_INIT;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
inline Buffer read_all(const std::string& location) {
|
||||
FileBuffer buffer = FILE_BUFFER_INIT;
|
||||
const auto result = svc_file != nullptr ?
|
||||
svc_file->read_all(mod_ctx, location.c_str(), &buffer) :
|
||||
MOD_UNAVAILABLE;
|
||||
return {buffer, result};
|
||||
}
|
||||
|
||||
inline ModResult check(const std::string& location) {
|
||||
return svc_file != nullptr ? svc_file->check(mod_ctx, location.c_str()) : MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
inline std::string display_name(const std::string& location) {
|
||||
if (svc_file == nullptr) {
|
||||
return {};
|
||||
}
|
||||
std::array<char, 1024> buffer{};
|
||||
return svc_file->display_name(mod_ctx, location.c_str(), buffer.data(),
|
||||
static_cast<uint32_t>(buffer.size())) == MOD_OK ?
|
||||
std::string{buffer.data()} :
|
||||
std::string{};
|
||||
}
|
||||
|
||||
inline ModResult join(
|
||||
const std::string& folder, const std::string& relativePath, std::string& outLocation) {
|
||||
outLocation.clear();
|
||||
if (svc_file == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const char* location = nullptr;
|
||||
const auto result = svc_file->join(mod_ctx, folder.c_str(), relativePath.c_str(), &location);
|
||||
if (result == MOD_OK && location != nullptr) {
|
||||
outLocation = location;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline ModResult create_child(
|
||||
const std::string& folder, const std::string& name, std::string& outLocation) {
|
||||
outLocation.clear();
|
||||
if (svc_file == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const char* location = nullptr;
|
||||
const auto result = svc_file->create_child(mod_ctx, folder.c_str(), name.c_str(), &location);
|
||||
if (result == MOD_OK && location != nullptr) {
|
||||
outLocation = location;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline ModResult write_all(const std::string& location, std::span<const uint8_t> bytes) {
|
||||
if (svc_file == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return svc_file->write_all(mod_ctx, location.c_str(), bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
std::string name;
|
||||
std::string location;
|
||||
bool isDirectory = false;
|
||||
};
|
||||
|
||||
inline ModResult list(const std::string& folder, std::vector<Entry>& outEntries) {
|
||||
outEntries.clear();
|
||||
if (svc_file == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return svc_file->list(
|
||||
mod_ctx, folder.c_str(),
|
||||
[](ModContext*, const FileEntry* entry, void* userData) {
|
||||
if (entry == nullptr) {
|
||||
return;
|
||||
}
|
||||
static_cast<std::vector<Entry>*>(userData)->push_back({
|
||||
.name = entry->name != nullptr ? entry->name : "",
|
||||
.location = entry->location != nullptr ? entry->location : "",
|
||||
.isDirectory = entry->is_directory,
|
||||
});
|
||||
},
|
||||
&outEntries);
|
||||
}
|
||||
|
||||
struct Filter {
|
||||
std::string name;
|
||||
std::string pattern;
|
||||
};
|
||||
|
||||
struct PickOptions {
|
||||
std::vector<Filter> filters;
|
||||
std::string defaultLocation;
|
||||
};
|
||||
|
||||
struct PickResult {
|
||||
ModResult status = MOD_ERROR;
|
||||
std::vector<std::string> locations;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline std::function<void(PickResult)> pickCallback;
|
||||
|
||||
inline void pick_trampoline(ModContext*, ModResult status, const char* const* locations,
|
||||
uint32_t locationCount, const char* error, void*) {
|
||||
PickResult result{.status = status, .error = error != nullptr ? error : ""};
|
||||
result.locations.reserve(locationCount);
|
||||
for (uint32_t i = 0; i < locationCount; ++i) {
|
||||
if (locations[i] != nullptr) {
|
||||
result.locations.emplace_back(locations[i]);
|
||||
}
|
||||
}
|
||||
auto callback = std::move(pickCallback);
|
||||
pickCallback = {};
|
||||
if (callback) {
|
||||
callback(std::move(result));
|
||||
}
|
||||
}
|
||||
|
||||
inline ModResult pick(
|
||||
const PickOptions& options, std::function<void(PickResult)> callback, bool folder) {
|
||||
if (svc_file == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (!callback) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (pickCallback) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
std::vector<FileFilter> filters;
|
||||
filters.reserve(options.filters.size());
|
||||
for (const auto& filter : options.filters) {
|
||||
filters.push_back({filter.name.c_str(), filter.pattern.c_str()});
|
||||
}
|
||||
FilePickOptions raw = FILE_PICK_OPTIONS_INIT;
|
||||
raw.filters = filters.empty() ? nullptr : filters.data();
|
||||
raw.filter_count = static_cast<uint32_t>(filters.size());
|
||||
raw.default_location =
|
||||
options.defaultLocation.empty() ? nullptr : options.defaultLocation.c_str();
|
||||
pickCallback = std::move(callback);
|
||||
const auto result = folder ? svc_file->pick_folder(mod_ctx, &raw, pick_trampoline, nullptr) :
|
||||
svc_file->pick_file(mod_ctx, &raw, pick_trampoline, nullptr);
|
||||
if (result != MOD_OK) {
|
||||
pickCallback = {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline ModResult pick_file(const PickOptions& options, std::function<void(PickResult)> callback) {
|
||||
return detail::pick(options, std::move(callback), false);
|
||||
}
|
||||
|
||||
inline ModResult pick_folder(const PickOptions& options, std::function<void(PickResult)> callback) {
|
||||
return detail::pick(options, std::move(callback), true);
|
||||
}
|
||||
|
||||
inline ModResult export_file(const std::string& sourceLocation, const std::string& suggestedName,
|
||||
std::function<void(PickResult)> callback) {
|
||||
if (svc_file == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (!callback) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (detail::pickCallback) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
detail::pickCallback = std::move(callback);
|
||||
const auto result = svc_file->export_file(
|
||||
mod_ctx, sourceLocation.c_str(), suggestedName.c_str(), detail::pick_trampoline, nullptr);
|
||||
if (result != MOD_OK) {
|
||||
detail::pickCallback = {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mods::file
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/config.h>
|
||||
#include <mods/svc/file.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||
#define UI_SERVICE_MAJOR 2u
|
||||
#define UI_SERVICE_MINOR 1u
|
||||
#define UI_SERVICE_MINOR 2u
|
||||
|
||||
/*
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
|
||||
@@ -53,6 +54,7 @@ typedef enum UiControlKind {
|
||||
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
|
||||
UI_CONTROL_COLOR = 5, /* RGB/RGBA color string with a picker */
|
||||
UI_CONTROL_GROUP = 6, /* navigation row (on_pressed) */
|
||||
UI_CONTROL_FILE_PICKER = 7, /* file/folder picker with an opaque string location */
|
||||
} UiControlKind;
|
||||
|
||||
typedef enum UiControlBinding {
|
||||
@@ -62,7 +64,8 @@ typedef enum UiControlBinding {
|
||||
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
|
||||
* directly: persistence, change notifications and the modified indicator (value != default) are
|
||||
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
|
||||
* SELECT = int, STRING and COLOR = string. Float vars are not bindable; use callbacks. */
|
||||
* SELECT = int, STRING, COLOR and FILE_PICKER = string. Float vars are not bindable; use
|
||||
* callbacks. */
|
||||
UI_BINDING_CONFIG_VAR = 1,
|
||||
} UiControlBinding;
|
||||
|
||||
@@ -71,10 +74,10 @@ typedef enum UiStringSetMode {
|
||||
UI_STRING_SET_ON_CHANGE = 1, /* invokes `set` on every text change (e.g. while typing) */
|
||||
} UiStringSetMode;
|
||||
|
||||
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
|
||||
* and COLOR read string_value. string_value passed to a setter is only valid during the call; a
|
||||
* getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid until
|
||||
* the next call into the mod — the host copies it right after the getter returns. */
|
||||
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING,
|
||||
* COLOR and FILE_PICKER read string_value. string_value passed to a setter is only valid during the
|
||||
* call; a getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid
|
||||
* until the next call into the mod. The host copies it right after the getter returns. */
|
||||
typedef struct UiControlValue {
|
||||
uint32_t struct_size;
|
||||
bool bool_value;
|
||||
@@ -128,12 +131,16 @@ typedef struct UiControlDesc {
|
||||
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
|
||||
UiPredicateFn is_selected; /* BUTTON/GROUP: optional selected state */
|
||||
UiStringSetMode string_set_mode; /* STRING: when to invoke the setter */
|
||||
/* FILE_PICKER: optional file filters and folder selection mode. */
|
||||
const FileFilter* file_filters;
|
||||
size_t file_filter_count;
|
||||
bool directory_mode;
|
||||
} UiControlDesc;
|
||||
|
||||
#define UI_CONTROL_DESC_INIT \
|
||||
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
||||
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL, \
|
||||
UI_STRING_SET_ON_COMMIT}
|
||||
UI_STRING_SET_ON_COMMIT, NULL, 0u, false}
|
||||
|
||||
typedef uint64_t UiListHandle;
|
||||
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr borealis::Log Log{"dusk::mods::file"};
|
||||
|
||||
static_assert(static_cast<int>(FILE_OPEN_READ) == static_cast<int>(borealis::io::File::Mode::Read));
|
||||
static_assert(
|
||||
static_cast<int>(FILE_OPEN_TRUNCATE) == static_cast<int>(borealis::io::File::Mode::Truncate));
|
||||
static_assert(
|
||||
static_cast<int>(FILE_OPEN_APPEND) == static_cast<int>(borealis::io::File::Mode::Append));
|
||||
|
||||
SlotMap<borealis::io::File> s_streams;
|
||||
std::unordered_map<void*, LoadedMod*> s_buffers;
|
||||
std::unordered_map<LoadedMod*, std::string> s_joinResults;
|
||||
ConfigVar<std::string> s_pickerOverride{"file.pickerOverride", ""};
|
||||
|
||||
struct PendingPick {
|
||||
LoadedMod* owner = nullptr;
|
||||
FilePickFn callback = nullptr;
|
||||
void* userData = nullptr;
|
||||
std::optional<std::string> overrideLocation;
|
||||
std::optional<std::string> exportSource;
|
||||
};
|
||||
|
||||
std::shared_ptr<PendingPick> s_pendingPick;
|
||||
|
||||
ModResult map_status(borealis::io::Status status) {
|
||||
switch (status) {
|
||||
case borealis::io::Status::Ok:
|
||||
return MOD_OK;
|
||||
case borealis::io::Status::NotFound:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::io::Status::Unsupported:
|
||||
return MOD_UNSUPPORTED;
|
||||
case borealis::io::Status::AlreadyExists:
|
||||
return MOD_CONFLICT;
|
||||
case borealis::io::Status::Failed:
|
||||
default:
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult map_picker_status(borealis::file_select::Status status) {
|
||||
switch (status) {
|
||||
case borealis::file_select::Status::Selected:
|
||||
return MOD_OK;
|
||||
case borealis::file_select::Status::Canceled:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::file_select::Status::Unsupported:
|
||||
return MOD_UNSUPPORTED;
|
||||
case borealis::file_select::Status::Busy:
|
||||
return MOD_CONFLICT;
|
||||
case borealis::file_select::Status::Failed:
|
||||
default:
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void invoke_pick(const std::shared_ptr<PendingPick>& pending, ModResult status,
|
||||
const std::vector<std::string>& locations, const std::string& error) {
|
||||
if (s_pendingPick == pending) {
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
auto* owner = pending->owner;
|
||||
const auto callback = pending->callback;
|
||||
if (owner == nullptr || callback == nullptr || !owner->active) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<const char*> rawLocations;
|
||||
rawLocations.reserve(locations.size());
|
||||
for (const auto& location : locations) {
|
||||
rawLocations.push_back(location.c_str());
|
||||
}
|
||||
try {
|
||||
callback(owner->context.get(), status, rawLocations.empty() ? nullptr : rawLocations.data(),
|
||||
static_cast<uint32_t>(rawLocations.size()), error.c_str(), pending->userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(*owner, MOD_ERROR,
|
||||
std::string{"exception in file picker callback: "} + exception.what());
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "unknown exception in file picker callback");
|
||||
}
|
||||
}
|
||||
|
||||
bool valid_pick_options(const FilePickOptions* options) {
|
||||
if (options == nullptr || options->struct_size < sizeof(FilePickOptions) ||
|
||||
(options->filter_count != 0 && options->filters == nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < options->filter_count; ++i) {
|
||||
if (options->filters[i].name == nullptr || options->filters[i].pattern == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult begin_pick(ModContext* context, const FilePickOptions* options, FilePickFn callback,
|
||||
void* userData, bool folder) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || callback == nullptr || !valid_pick_options(options)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
auto pending = std::make_shared<PendingPick>(PendingPick{
|
||||
.owner = mod,
|
||||
.callback = callback,
|
||||
.userData = userData,
|
||||
});
|
||||
const auto& overrideLocation = s_pickerOverride.getValue();
|
||||
if (!overrideLocation.empty()) {
|
||||
pending->overrideLocation = overrideLocation;
|
||||
s_pendingPick = std::move(pending);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
s_pendingPick = pending;
|
||||
const std::string defaultLocation =
|
||||
options->default_location != nullptr ? options->default_location : "";
|
||||
if (folder) {
|
||||
borealis::file_select::open_folder(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
[pending](borealis::file_select::Result result) {
|
||||
invoke_pick(
|
||||
pending, map_picker_status(result.status), result.locations, result.message);
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
std::vector<borealis::file_select::Filter> filters;
|
||||
filters.reserve(options->filter_count);
|
||||
for (uint32_t i = 0; i < options->filter_count; ++i) {
|
||||
filters.push_back({options->filters[i].name, options->filters[i].pattern});
|
||||
}
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = std::move(filters),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
[pending](borealis::file_select::Result result) {
|
||||
invoke_pick(
|
||||
pending, map_picker_status(result.status), result.locations, result.message);
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult begin_export(ModContext* context, const char* sourceLocation, const char* suggestedName,
|
||||
FilePickFn callback, void* userData) {
|
||||
auto* mod = mod_from_context(context);
|
||||
const std::string_view name = suggestedName != nullptr ? suggestedName : "";
|
||||
if (mod == nullptr || sourceLocation == nullptr || callback == nullptr || name.empty() ||
|
||||
name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
const auto available = borealis::io::check(sourceLocation);
|
||||
if (available != borealis::io::Status::Ok) {
|
||||
return map_status(available);
|
||||
}
|
||||
|
||||
auto pending = std::make_shared<PendingPick>(PendingPick{
|
||||
.owner = mod,
|
||||
.callback = callback,
|
||||
.userData = userData,
|
||||
});
|
||||
const auto& overrideLocation = s_pickerOverride.getValue();
|
||||
if (!overrideLocation.empty()) {
|
||||
pending->overrideLocation = overrideLocation;
|
||||
pending->exportSource = sourceLocation;
|
||||
s_pendingPick = std::move(pending);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
borealis::file_select::ExportOptions options{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.sourceLocation = sourceLocation,
|
||||
.suggestedName = suggestedName,
|
||||
};
|
||||
s_pendingPick = pending;
|
||||
try {
|
||||
borealis::file_select::export_file(
|
||||
std::move(options), [pending](borealis::file_select::Result result) {
|
||||
invoke_pick(
|
||||
pending, map_picker_status(result.status), result.locations, result.message);
|
||||
});
|
||||
} catch (...) {
|
||||
if (s_pendingPick == pending) {
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_pick_file(
|
||||
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
|
||||
return begin_pick(context, options, callback, userData, false);
|
||||
}
|
||||
|
||||
ModResult file_pick_folder(
|
||||
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
|
||||
return begin_pick(context, options, callback, userData, true);
|
||||
}
|
||||
|
||||
ModResult file_export_file(ModContext* context, const char* sourceLocation,
|
||||
const char* suggestedName, FilePickFn callback, void* userData) {
|
||||
try {
|
||||
return begin_export(context, sourceLocation, suggestedName, callback, userData);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult file_display_name(
|
||||
ModContext* context, const char* location, char* buffer, uint32_t bufferSize) {
|
||||
if (mod_from_context(context) == nullptr || location == nullptr || buffer == nullptr ||
|
||||
bufferSize == 0)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string name = borealis::io::display_name(location);
|
||||
if (name.size() + 1 > bufferSize) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buffer, name.c_str(), name.size() + 1);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_check(ModContext* context, const char* location) {
|
||||
if (mod_from_context(context) == nullptr || location == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return map_status(borealis::io::check(location));
|
||||
}
|
||||
|
||||
ModResult file_open(
|
||||
ModContext* context, const char* location, FileOpenMode mode, FileStreamHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || location == nullptr || outHandle == nullptr || mode < FILE_OPEN_READ ||
|
||||
mode > FILE_OPEN_APPEND)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
const auto ioMode = static_cast<borealis::io::File::Mode>(mode);
|
||||
if (mode != FILE_OPEN_READ) {
|
||||
const auto available = borealis::io::check(location);
|
||||
if (available != borealis::io::Status::Ok) {
|
||||
return map_status(available);
|
||||
}
|
||||
}
|
||||
auto result = borealis::io::open(location, ioMode);
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
Log.warn("[{}] open '{}' failed: {}", mod->metadata.id,
|
||||
borealis::io::display_name(location), result.message);
|
||||
return map_status(result.status);
|
||||
}
|
||||
*outHandle = s_streams.emplace(*mod, std::move(result.file));
|
||||
return MOD_OK;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
borealis::io::File* find_stream(ModContext* context, FileStreamHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* entry = s_streams.find_owned(handle, *mod);
|
||||
return entry != nullptr ? &entry->value : nullptr;
|
||||
}
|
||||
|
||||
ModResult file_size(ModContext* context, FileStreamHandle handle, uint64_t* outSize) {
|
||||
if (outSize == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outSize = 0;
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outSize = file->size();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_read(ModContext* context, FileStreamHandle handle, void* buffer, uint64_t length,
|
||||
uint64_t* outRead) {
|
||||
if (outRead == nullptr || (buffer == nullptr && length != 0)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outRead = 0;
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr || file->writable()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outRead = file->read(buffer, length);
|
||||
return file->error().empty() ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_seek(ModContext* context, FileStreamHandle handle, uint64_t offset) {
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return file->seek(offset) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_close(ModContext* context, FileStreamHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto entry = s_streams.take_owned(handle, *mod);
|
||||
if (!entry.has_value()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return entry->value.close() ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_write(
|
||||
ModContext* context, FileStreamHandle handle, const void* buffer, uint64_t length) {
|
||||
if ((buffer == nullptr && length != 0) || length > std::numeric_limits<size_t>::max()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr || !file->writable()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto bytes =
|
||||
std::span{static_cast<const std::byte*>(buffer), static_cast<size_t>(length)};
|
||||
return file->write(bytes) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_flush(ModContext* context, FileStreamHandle handle) {
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr || !file->writable()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return file->flush() ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_write_all(ModContext* context, const char* location, const void* data, size_t size) {
|
||||
if (mod_from_context(context) == nullptr || location == nullptr ||
|
||||
(data == nullptr && size != 0))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
const auto available = borealis::io::check(location);
|
||||
if (available != borealis::io::Status::Ok) {
|
||||
return map_status(available);
|
||||
}
|
||||
auto opened = borealis::io::open(location, borealis::io::File::Mode::Truncate);
|
||||
if (opened.status != borealis::io::Status::Ok) {
|
||||
return map_status(opened.status);
|
||||
}
|
||||
if (!opened.file.write(std::span{static_cast<const std::byte*>(data), size})) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return opened.file.close() ? MOD_OK : MOD_ERROR;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult file_read_all(ModContext* context, const char* location, FileBuffer* outBuffer) {
|
||||
if (outBuffer == nullptr || outBuffer->struct_size < sizeof(FileBuffer)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
outBuffer->data = nullptr;
|
||||
outBuffer->size = 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || location == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
auto opened = borealis::io::open(location);
|
||||
if (opened.status != borealis::io::Status::Ok) {
|
||||
return map_status(opened.status);
|
||||
}
|
||||
const uint64_t expectedSize = opened.file.size();
|
||||
if (expectedSize > std::numeric_limits<size_t>::max()) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::vector<unsigned char> bytes;
|
||||
bytes.resize(static_cast<size_t>(expectedSize));
|
||||
uint64_t total = 0;
|
||||
while (total < expectedSize) {
|
||||
const uint64_t count = opened.file.read(bytes.data() + total, expectedSize - total);
|
||||
if (count == 0) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
total += count;
|
||||
}
|
||||
if (expectedSize == 0) {
|
||||
unsigned char chunk[64 * 1024];
|
||||
while (true) {
|
||||
const uint64_t count = opened.file.read(chunk, sizeof(chunk));
|
||||
if (count == 0) {
|
||||
if (!opened.file.error().empty()) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
bytes.insert(bytes.end(), chunk, chunk + count);
|
||||
}
|
||||
}
|
||||
if (bytes.empty()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
std::unique_ptr<void, decltype(&std::free)> data{std::malloc(bytes.size()), &std::free};
|
||||
if (!data) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::memcpy(data.get(), bytes.data(), bytes.size());
|
||||
s_buffers.emplace(data.get(), mod);
|
||||
outBuffer->data = data.release();
|
||||
outBuffer->size = bytes.size();
|
||||
return MOD_OK;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void file_free(ModContext* context, FileBuffer* buffer) {
|
||||
if (buffer == nullptr || buffer->struct_size < sizeof(FileBuffer) || buffer->data == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
const auto found = s_buffers.find(buffer->data);
|
||||
if (mod == nullptr || found == s_buffers.end() || found->second != mod) {
|
||||
Log.error("[{}] file free: buffer is not owned by this mod", mod_id_from_context(context));
|
||||
return;
|
||||
}
|
||||
s_buffers.erase(found);
|
||||
std::free(buffer->data);
|
||||
buffer->data = nullptr;
|
||||
buffer->size = 0;
|
||||
}
|
||||
|
||||
ModResult file_list(
|
||||
ModContext* context, const char* folderLocation, FileListFn callback, void* userData) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || folderLocation == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
borealis::io::ListResult result;
|
||||
try {
|
||||
result = borealis::io::list(folderLocation);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
return map_status(result.status);
|
||||
}
|
||||
try {
|
||||
for (const auto& entry : result.entries) {
|
||||
const FileEntry raw{
|
||||
.name = entry.name.c_str(),
|
||||
.location = entry.location.c_str(),
|
||||
.is_directory = entry.isDirectory,
|
||||
};
|
||||
callback(mod->context.get(), &raw, userData);
|
||||
if (!mod->active) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
callback(mod->context.get(), nullptr, userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(
|
||||
*mod, MOD_ERROR, std::string{"exception in file list callback: "} + exception.what());
|
||||
return MOD_ERROR;
|
||||
} catch (...) {
|
||||
fail_mod(*mod, MOD_ERROR, "unknown exception in file list callback");
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_join(ModContext* context, const char* folderLocation, const char* relativePath,
|
||||
const char** outLocation) {
|
||||
if (outLocation != nullptr) {
|
||||
*outLocation = nullptr;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || folderLocation == nullptr || relativePath == nullptr ||
|
||||
outLocation == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto result = borealis::io::join(folderLocation, relativePath);
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
return map_status(result.status);
|
||||
}
|
||||
auto& saved = s_joinResults[mod];
|
||||
saved = std::move(result.location);
|
||||
*outLocation = saved.c_str();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_create_child(
|
||||
ModContext* context, const char* folderLocation, const char* name, const char** outLocation) {
|
||||
if (outLocation != nullptr) {
|
||||
*outLocation = nullptr;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || folderLocation == nullptr || name == nullptr || outLocation == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
auto result = borealis::io::create_child(folderLocation, name);
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
return map_status(result.status);
|
||||
}
|
||||
auto& saved = s_joinResults[mod];
|
||||
saved = std::move(result.location);
|
||||
*outLocation = saved.c_str();
|
||||
return MOD_OK;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void file_initialize() {
|
||||
config::Register(s_pickerOverride);
|
||||
}
|
||||
|
||||
ModResult copy_picker_override(
|
||||
std::string_view source, std::string_view destination, std::string& error) {
|
||||
try {
|
||||
if (source == destination) {
|
||||
return MOD_OK;
|
||||
}
|
||||
auto input = borealis::io::open(source);
|
||||
if (input.status != borealis::io::Status::Ok) {
|
||||
error = std::move(input.message);
|
||||
return map_status(input.status);
|
||||
}
|
||||
auto output = borealis::io::open(destination, borealis::io::File::Mode::Truncate);
|
||||
if (output.status != borealis::io::Status::Ok) {
|
||||
error = std::move(output.message);
|
||||
return map_status(output.status);
|
||||
}
|
||||
std::array<std::byte, 64 * 1024> buffer{};
|
||||
while (true) {
|
||||
const uint64_t read = input.file.read(buffer.data(), buffer.size());
|
||||
if (read == 0) {
|
||||
if (!input.file.error().empty()) {
|
||||
error = input.file.error();
|
||||
return MOD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!output.file.write(std::span{buffer.data(), static_cast<size_t>(read)})) {
|
||||
error = output.file.error();
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
if (!output.file.close()) {
|
||||
error = output.file.error();
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return MOD_OK;
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
return MOD_ERROR;
|
||||
} catch (...) {
|
||||
error = "Unable to copy export source";
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void file_frame_begin() {
|
||||
auto pending = s_pendingPick;
|
||||
if (pending == nullptr || !pending->overrideLocation.has_value()) {
|
||||
return;
|
||||
}
|
||||
if (!pending->exportSource.has_value()) {
|
||||
invoke_pick(pending, MOD_OK, {*pending->overrideLocation}, "");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
const ModResult result =
|
||||
copy_picker_override(*pending->exportSource, *pending->overrideLocation, error);
|
||||
invoke_pick(pending, result,
|
||||
result == MOD_OK ? std::vector<std::string>{*pending->overrideLocation} :
|
||||
std::vector<std::string>{},
|
||||
error);
|
||||
}
|
||||
|
||||
void file_remove_mod(LoadedMod& mod) {
|
||||
const size_t streams = s_streams.erase_all(mod);
|
||||
size_t buffers = 0;
|
||||
std::erase_if(s_buffers, [&](const auto& entry) {
|
||||
if (entry.second != &mod) {
|
||||
return false;
|
||||
}
|
||||
std::free(entry.first);
|
||||
++buffers;
|
||||
return true;
|
||||
});
|
||||
s_joinResults.erase(&mod);
|
||||
if (s_pendingPick != nullptr && s_pendingPick->owner == &mod) {
|
||||
s_pendingPick->owner = nullptr;
|
||||
s_pendingPick->callback = nullptr;
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
if (streams != 0 || buffers != 0) {
|
||||
Log.warn("[{}] reclaimed {} open stream(s) and {} file buffer(s)", mod.metadata.id, streams,
|
||||
buffers);
|
||||
}
|
||||
}
|
||||
|
||||
void file_shutdown() {
|
||||
config::unregister(s_pickerOverride);
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
|
||||
constexpr FileService s_fileService{
|
||||
.header = SERVICE_HEADER(FileService, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR),
|
||||
.pick_file = file_pick_file,
|
||||
.pick_folder = file_pick_folder,
|
||||
.export_file = file_export_file,
|
||||
.display_name = file_display_name,
|
||||
.check = file_check,
|
||||
.open = file_open,
|
||||
.size = file_size,
|
||||
.read = file_read,
|
||||
.write = file_write,
|
||||
.seek = file_seek,
|
||||
.flush = file_flush,
|
||||
.close = file_close,
|
||||
.read_all = file_read_all,
|
||||
.write_all = file_write_all,
|
||||
.free = file_free,
|
||||
.list = file_list,
|
||||
.join = file_join,
|
||||
.create_child = file_create_child,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_fileModule{
|
||||
.id = FILE_SERVICE_ID,
|
||||
.majorVersion = FILE_SERVICE_MAJOR,
|
||||
.minorVersion = FILE_SERVICE_MINOR,
|
||||
.service = &s_fileService,
|
||||
.initialize = file_initialize,
|
||||
.modDetached = file_remove_mod,
|
||||
.frameBegin = file_frame_begin,
|
||||
.shutdown = file_shutdown,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -9,6 +9,10 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
@@ -210,6 +214,9 @@ void ModLoader::init_services() {
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
&svc::g_resourceModule,
|
||||
#if !defined(__APPLE__) || !TARGET_OS_TV
|
||||
&svc::g_fileModule,
|
||||
#endif
|
||||
&svc::g_hookModule,
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
|
||||
@@ -68,6 +68,7 @@ void modules_shutdown();
|
||||
extern const ServiceModule g_hostModule;
|
||||
extern const ServiceModule g_logModule;
|
||||
extern const ServiceModule g_resourceModule;
|
||||
extern const ServiceModule g_fileModule;
|
||||
extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
|
||||
@@ -45,6 +45,8 @@ constexpr size_t kUiControlSelectedSize =
|
||||
offsetof(UiControlDesc, is_selected) + sizeof(UiPredicateFn);
|
||||
constexpr size_t kUiControlStringSetModeSize =
|
||||
offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode);
|
||||
constexpr size_t kUiControlFilePickerSize =
|
||||
offsetof(UiControlDesc, directory_mode) + sizeof(bool);
|
||||
constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*);
|
||||
constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*);
|
||||
|
||||
@@ -303,6 +305,7 @@ void wire_callback_binding(
|
||||
break;
|
||||
case UI_CONTROL_STRING:
|
||||
case UI_CONTROL_COLOR:
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
spec.getString = [getValue]() -> Rml::String {
|
||||
const UiControlValue value = getValue();
|
||||
return value.string_value != nullptr ? value.string_value : "";
|
||||
@@ -382,7 +385,8 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC
|
||||
return true;
|
||||
}
|
||||
case UI_CONTROL_STRING:
|
||||
case UI_CONTROL_COLOR: {
|
||||
case UI_CONTROL_COLOR:
|
||||
case UI_CONTROL_FILE_PICKER: {
|
||||
const auto find = [modPtr, varHandle] {
|
||||
return static_cast<ConfigVar<std::string>*>(
|
||||
config_find_var(*modPtr, varHandle, CONFIG_VAR_STRING));
|
||||
@@ -642,6 +646,14 @@ ModResult ui_pane_add_control(
|
||||
spec.colorPresets.emplace_back(desc.color_presets[i]);
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
spec.kind = ui::ModControlSpec::Kind::FilePicker;
|
||||
spec.directoryMode = desc.directory_mode;
|
||||
for (size_t i = 0; i < desc.file_filter_count; ++i) {
|
||||
spec.fileFilters.push_back(
|
||||
{desc.file_filters[i].name, desc.file_filters[i].pattern});
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_SELECT:
|
||||
spec.kind = ui::ModControlSpec::Kind::Select;
|
||||
if (slot->helpPane == nullptr) {
|
||||
@@ -1257,6 +1269,18 @@ bool valid_control_desc(const UiControlDesc& desc) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
if (desc.struct_size < kUiControlFilePickerSize ||
|
||||
(desc.file_filter_count != 0 && desc.file_filters == nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < desc.file_filter_count; ++i) {
|
||||
if (desc.file_filters[i].name == nullptr || desc.file_filters[i].pattern == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "file_button.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
FileButton::FileButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton{parent, {.key = std::move(props.key)}},
|
||||
mGetValue{std::move(props.getValue)}, mSetValue{std::move(props.setValue)},
|
||||
mIsDisabled{std::move(props.isDisabled)}, mIsModified{std::move(props.isModified)},
|
||||
mFilters{std::move(props.filters)}, mDirectoryMode{props.directoryMode} {}
|
||||
|
||||
bool FileButton::modified() const {
|
||||
return mIsModified ? mIsModified() : BaseControlledSelectButton::modified();
|
||||
}
|
||||
|
||||
bool FileButton::disabled() const {
|
||||
return borealis::file_select::busy() || (mIsDisabled && mIsDisabled());
|
||||
}
|
||||
|
||||
Rml::String FileButton::format_value() {
|
||||
const Rml::String location = mGetValue ? mGetValue() : "";
|
||||
if (location.empty()) {
|
||||
return "(none)";
|
||||
}
|
||||
const auto name = borealis::io::display_name(location);
|
||||
return name.empty() ? location : name;
|
||||
}
|
||||
|
||||
bool FileButton::handle_nav_command(NavCommand command) {
|
||||
if (command != NavCommand::Confirm) {
|
||||
return false;
|
||||
}
|
||||
open_picker();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileButton::open_picker() {
|
||||
if (disabled()) {
|
||||
return;
|
||||
}
|
||||
const std::string defaultLocation = mGetValue ? mGetValue() : "";
|
||||
auto complete = [setValue = mSetValue](borealis::file_select::Result result) {
|
||||
if (result.status == borealis::file_select::Status::Selected && !result.locations.empty() &&
|
||||
setValue)
|
||||
{
|
||||
setValue(std::move(result.locations.front()));
|
||||
}
|
||||
};
|
||||
if (mDirectoryMode) {
|
||||
borealis::file_select::open_folder(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
std::move(complete));
|
||||
return;
|
||||
}
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = mFilters,
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
std::move(complete));
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include <borealis/file_select.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class FileButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
std::vector<borealis::file_select::Filter> filters;
|
||||
bool directoryMode = false;
|
||||
};
|
||||
|
||||
FileButton(Rml::Element* parent, Props props);
|
||||
bool modified() const override;
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
bool handle_nav_command(NavCommand command) override;
|
||||
|
||||
private:
|
||||
void open_picker();
|
||||
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<void(Rml::String)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
std::function<bool()> mIsModified;
|
||||
std::vector<borealis::file_select::Filter> mFilters;
|
||||
bool mDirectoryMode = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "bool_button.hpp"
|
||||
#include "color_input.hpp"
|
||||
#include "file_button.hpp"
|
||||
#include "number_button.hpp"
|
||||
#include "string_button.hpp"
|
||||
|
||||
@@ -83,6 +84,17 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
.alpha = s.colorAlpha,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::FilePicker:
|
||||
control = &pane.add_child<FileButton>(FileButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getString,
|
||||
.setValue = s.setString,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.filters = s.fileFilters,
|
||||
.directoryMode = s.directoryMode,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Select:
|
||||
if (helpPane == nullptr || s.options.empty()) {
|
||||
return nullptr;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "pane.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <borealis/file_select.hpp>
|
||||
|
||||
#include <climits>
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -16,6 +18,7 @@ struct ModControlSpec {
|
||||
String,
|
||||
Select,
|
||||
Color,
|
||||
FilePicker,
|
||||
};
|
||||
|
||||
Kind kind = Kind::Button;
|
||||
@@ -42,6 +45,8 @@ struct ModControlSpec {
|
||||
bool stringSetOnChange = false;
|
||||
std::vector<Rml::String> colorPresets;
|
||||
bool colorAlpha = false;
|
||||
std::vector<borealis::file_select::Filter> fileFilters;
|
||||
bool directoryMode = false;
|
||||
};
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include <borealis/update.hpp>
|
||||
#include <borealis/version.h>
|
||||
@@ -130,42 +131,7 @@ struct DiscVerificationTask {
|
||||
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
|
||||
bool sDiscVerificationModalPushed = false;
|
||||
|
||||
struct UpdateCheckTask {
|
||||
UpdateCheckTask() {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
result = borealis::update::check_latest_github_release(AppInfo);
|
||||
} catch (const std::exception& e) {
|
||||
result = {
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = fmt::format("Update check failed with exception: {}", e.what()),
|
||||
};
|
||||
} catch (...) {
|
||||
result = {
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = "Update check failed with an unknown exception",
|
||||
};
|
||||
}
|
||||
done.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
~UpdateCheckTask() { join(); }
|
||||
|
||||
void join() {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
|
||||
|
||||
borealis::update::Result result;
|
||||
std::atomic_bool done = false;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
|
||||
borealis::Task<borealis::update::Result> sUpdateCheck;
|
||||
std::optional<borealis::update::Result> sUpdateCheckResult;
|
||||
|
||||
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
|
||||
@@ -243,20 +209,19 @@ void begin_update_check() {
|
||||
if (!getSettings().backend.checkForUpdates.getValue()) {
|
||||
return;
|
||||
}
|
||||
if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) {
|
||||
if (sUpdateCheck || sUpdateCheckResult.has_value()) {
|
||||
return;
|
||||
}
|
||||
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
|
||||
sUpdateCheck = borealis::update::check_latest_github_release(AppInfo);
|
||||
}
|
||||
|
||||
std::optional<borealis::update::Result> take_finished_update_check() {
|
||||
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
|
||||
if (!sUpdateCheck || !sUpdateCheck.ready()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
sUpdateCheckTask->join();
|
||||
auto result = std::move(sUpdateCheckTask->result);
|
||||
sUpdateCheckTask.reset();
|
||||
auto result = sUpdateCheck.try_take();
|
||||
sUpdateCheck = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -479,7 +444,7 @@ private:
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName = borealis::file_select::display_name(sDiscVerificationTask->path);
|
||||
std::string fileName = borealis::io::display_name(sDiscVerificationTask->path);
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
@@ -1167,7 +1132,7 @@ void Prelaunch::update() {
|
||||
sUpdateCheckResult = std::move(*result);
|
||||
}
|
||||
|
||||
if (sUpdateCheckTask != nullptr) {
|
||||
if (sUpdateCheck) {
|
||||
mUpdateStatus->SetAttribute("state", "checking");
|
||||
mUpdateMessage->SetInnerRML("Checking for updates...");
|
||||
} else if (!sUpdateCheckResult.has_value() ||
|
||||
|
||||
@@ -231,7 +231,7 @@ Rml::String configured_data_path_display_name() {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
auto display = borealis::file_select::display_name(path);
|
||||
auto display = borealis::io::display_name(path);
|
||||
if (display.empty()) {
|
||||
return path;
|
||||
}
|
||||
@@ -493,7 +493,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (path.empty()) {
|
||||
display = "(none)";
|
||||
} else {
|
||||
display = borealis::file_select::display_name(path);
|
||||
display = borealis::io::display_name(path);
|
||||
if (display.empty()) {
|
||||
display = path;
|
||||
}
|
||||
@@ -539,6 +539,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
.requireRealPath = true,
|
||||
},
|
||||
&data_folder_dialog_callback);
|
||||
});
|
||||
|
||||
+35
-21
@@ -794,12 +794,19 @@ int game_main(int argc, char* argv[]) {
|
||||
bool forcePreLaunchUI = false;
|
||||
bool saveConfigBeforePrelaunch = false;
|
||||
|
||||
const std::string p = dusk::getSettings().backend.isoPath;
|
||||
borealis::io::PathAccess dvdPathAccess;
|
||||
const auto resolveDvdLocation = [&dvdPathAccess](const std::string& location) {
|
||||
dvdPathAccess = borealis::io::access_path(location);
|
||||
return dvdPathAccess ? borealis::io::fs_path_to_string(dvdPathAccess.path()) : location;
|
||||
};
|
||||
|
||||
const std::string savedLocation = dusk::getSettings().backend.isoPath;
|
||||
dusk::iso::DiscInfo discInfo{};
|
||||
if (!p.empty() &&
|
||||
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
if (!savedLocation.empty() &&
|
||||
dusk::iso::inspect(savedLocation.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p);
|
||||
DuskLog.warn("Saved DVD image location failed validation, clearing it: {}",
|
||||
borealis::io::display_name(savedLocation));
|
||||
dusk::getSettings().backend.isoPath.setValue("");
|
||||
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
|
||||
forcePreLaunchUI = true;
|
||||
@@ -808,18 +815,23 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
bool skipPreLaunchUI = dusk::getSettings().backend.skipPreLaunchUI.getValue();
|
||||
|
||||
std::string dvd_path = dusk::getSettings().backend.isoPath;
|
||||
std::string dvdLocation = dusk::getSettings().backend.isoPath;
|
||||
std::string dvdPath = resolveDvdLocation(dvdLocation);
|
||||
bool dvd_opened = false;
|
||||
if (parsed_arg_options.count("dvd")) {
|
||||
dvd_path = parsed_arg_options["dvd"].as<std::string>();
|
||||
if (dusk::iso::inspect(dvd_path.c_str(), discInfo) == dusk::iso::ValidationError::Success) {
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
|
||||
dvd_opened = aurora_dvd_open(dvd_path.c_str());
|
||||
dvdLocation = parsed_arg_options["dvd"].as<std::string>();
|
||||
dvdPath = resolveDvdLocation(dvdLocation);
|
||||
if (dusk::iso::inspect(dvdLocation.c_str(), discInfo) ==
|
||||
dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvdPath);
|
||||
dvd_opened = aurora_dvd_open(dvdPath.c_str());
|
||||
if (!dvd_opened) {
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI",
|
||||
dvdPath);
|
||||
forcePreLaunchUI = true;
|
||||
} else {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::getSettings().backend.isoPath.setValue(dvdLocation);
|
||||
dusk::getSettings().backend.isoVerification.setValue(
|
||||
dusk::DiscVerificationState::Unknown);
|
||||
dusk::config::save();
|
||||
@@ -827,13 +839,14 @@ int game_main(int argc, char* argv[]) {
|
||||
skipPreLaunchUI = true;
|
||||
}
|
||||
} else {
|
||||
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
|
||||
DuskLog.warn(
|
||||
"DVD image from command line failed validation: {}, opening prelaunch UI", dvdPath);
|
||||
forcePreLaunchUI = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't load right into the game, stop requesting to load a stage or save
|
||||
if (forcePreLaunchUI || dvd_path.empty()) {
|
||||
if (forcePreLaunchUI || dvdPath.empty()) {
|
||||
if (dusk::StageRequested.set) {
|
||||
DuskLog.warn("Cannot load stage {} because no iso path is set, opening prelaunch UI",dusk::StageRequested.stage);
|
||||
dusk::StageRequested = {};
|
||||
@@ -893,18 +906,19 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
dvd_path = dusk::getSettings().backend.isoPath;
|
||||
if (dvd_path.empty()) {
|
||||
dvdLocation = dusk::getSettings().backend.isoPath;
|
||||
dvdPath = resolveDvdLocation(dvdLocation);
|
||||
if (dvdPath.empty()) {
|
||||
DuskLog.fatal("No DVD image specified, unable to boot!");
|
||||
}
|
||||
if (!dusk::IsGameLaunched &&
|
||||
dusk::iso::inspect(dvd_path.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
if (!dusk::IsGameLaunched && dusk::iso::inspect(dvdLocation.c_str(), discInfo) !=
|
||||
dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.fatal("DVD image failed validation: {}", dvd_path);
|
||||
DuskLog.fatal("DVD image failed validation: {}", dvdPath);
|
||||
}
|
||||
DuskLog.info("Loading DVD image: {}", dvd_path);
|
||||
if (!aurora_dvd_open(dvd_path.c_str())) {
|
||||
DuskLog.fatal("Failed to open DVD image: {}", dvd_path);
|
||||
DuskLog.info("Loading DVD image: {}", dvdPath);
|
||||
if (!aurora_dvd_open(dvdPath.c_str())) {
|
||||
DuskLog.fatal("Failed to open DVD image: {}", dvdPath);
|
||||
}
|
||||
|
||||
dusk::IsGameLaunched = true;
|
||||
|
||||
Reference in New Issue
Block a user