mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-11 19:21:36 -04:00
Merge main
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
#define ITEM_CHECK_FREESTANDING_PREFIX "freestanding:" /* <stage>:<bit> */
|
||||
#define ITEM_CHECK_GOLDEN_WOLF_PREFIX "golden_wolf:" /* <event_flag> */
|
||||
#define ITEM_CHECK_POE_PREFIX "poe:" /* <stage>:<switch> */
|
||||
#define ITEM_CHECK_SHOP_PREFIX "shop:" /* <stage>:<item> */
|
||||
#define ITEM_CHECK_SHOP_PREFIX "shop:" /* <stage>:<room>:<item> */
|
||||
#define ITEM_CHECK_SKY_PREFIX "sky:" /* <stage>:<room> */
|
||||
#define ITEM_CHECK_DUNGEON_REWARD_PREFIX "dungeon_reward:" /* <stage> */
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#define ITEM_CHECK_BULBLIN_KEY "bulblin_key:D_MN09"
|
||||
#define ITEM_CHECK_CORAL_EARRING "coral_earring"
|
||||
#define ITEM_CHECK_CORO_BOTTLE "coro_bottle"
|
||||
#define ITEM_CHECK_CORO_GATE_KEY "coro_gate_key"
|
||||
#define ITEM_CHECK_CORO_LANTERN "coro_lantern"
|
||||
#define ITEM_CHECK_DUNGEON_MAP_SNOWPEAK "dungeon_map:D_MN11"
|
||||
#define ITEM_CHECK_FAIRY_REWARD "fairy_reward:D_SB01"
|
||||
#define ITEM_CHECK_FISHING_BOTTLE "fishing_bottle"
|
||||
|
||||
@@ -13,21 +13,27 @@
|
||||
* modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one
|
||||
* constant-initialized record object in the metadata section.
|
||||
*/
|
||||
#if defined(__has_attribute) && __has_attribute(no_sanitize)
|
||||
#define MOD_META_NO_ASAN __attribute__((no_sanitize("address")))
|
||||
#else
|
||||
#define MOD_META_NO_ASAN
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#pragma section("modmeta$a", read, write)
|
||||
#pragma section("modmeta$d", read, write)
|
||||
#pragma section("modmeta$z", read, write)
|
||||
#if defined(__clang__)
|
||||
#define MOD_META_RECORD __declspec(allocate("modmeta$d")) __attribute__((used))
|
||||
#define MOD_META_RECORD __declspec(allocate("modmeta$d")) __attribute__((used)) MOD_META_NO_ASAN
|
||||
#else
|
||||
#define MOD_META_RECORD __declspec(allocate("modmeta$d"))
|
||||
#endif
|
||||
#elif defined(__APPLE__)
|
||||
#define MOD_META_RECORD __attribute__((section("__DATA,__modmeta"), used))
|
||||
#define MOD_META_RECORD __attribute__((section("__DATA,__modmeta"), used)) MOD_META_NO_ASAN
|
||||
#elif defined(__has_attribute) && __has_attribute(retain)
|
||||
#define MOD_META_RECORD __attribute__((section("modmeta"), used, retain))
|
||||
#define MOD_META_RECORD __attribute__((section("modmeta"), used, retain)) MOD_META_NO_ASAN
|
||||
#else
|
||||
#define MOD_META_RECORD __attribute__((section("modmeta"), used))
|
||||
#define MOD_META_RECORD __attribute__((section("modmeta"), used)) MOD_META_NO_ASAN
|
||||
#endif
|
||||
|
||||
/* Section bounds for the mod_meta descriptor */
|
||||
@@ -46,8 +52,8 @@ extern "C" const unsigned char mod_meta_bounds_end[] __asm("section$end$__DATA$_
|
||||
#define MOD_META_BOUNDS_BEGIN (mod_meta_bounds_begin)
|
||||
#define MOD_META_BOUNDS_END (mod_meta_bounds_end)
|
||||
#else
|
||||
extern "C" const unsigned char __start_modmeta[];
|
||||
extern "C" const unsigned char __stop_modmeta[];
|
||||
extern "C" __attribute__((visibility("hidden"))) const unsigned char __start_modmeta[];
|
||||
extern "C" __attribute__((visibility("hidden"))) const unsigned char __stop_modmeta[];
|
||||
#define MOD_META_BOUNDS_DEFN
|
||||
#define MOD_META_BOUNDS_BEGIN (__start_modmeta)
|
||||
#define MOD_META_BOUNDS_END (__stop_modmeta)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
/*
|
||||
* Lifecycle service for mods delegated to a runtime named by mod.json. The host passes the
|
||||
* runtime's context as ctx and the delegated mod's context as subject.
|
||||
*/
|
||||
typedef struct ModRuntimeService {
|
||||
ServiceHeader header;
|
||||
|
||||
ModResult (*activate)(ModContext* ctx, ModContext* subject, ModError* out_error);
|
||||
ModResult (*update)(ModContext* ctx, ModContext* subject, ModError* out_error);
|
||||
ModResult (*deactivate)(ModContext* ctx, ModContext* subject, ModError* out_error);
|
||||
} ModRuntimeService;
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/config.h>
|
||||
|
||||
#define ACTOR_SERVICE_ID "dev.twilitrealm.dusklight.actor"
|
||||
#define ACTOR_SERVICE_MAJOR 1u
|
||||
#define ACTOR_SERVICE_MINOR 0u
|
||||
|
||||
typedef int16_t ProfileName;
|
||||
typedef uint32_t ActorId;
|
||||
typedef uint64_t ActorHandle;
|
||||
|
||||
typedef struct {
|
||||
const char name[8]; // Canonical stage name. Must be unique among active mod actors. Matching a
|
||||
// game actor name intentionally overrides stage lookup for that actor.
|
||||
|
||||
uint16_t priority_group; /* priorityGroup is the priority for when execute will be called on the
|
||||
actor. Here are the main groups:
|
||||
0: The room manager actor
|
||||
1: Game scenes
|
||||
2: Various room change actors
|
||||
3: Most objects to be executed before Link
|
||||
4: Some bosses, canoe, epona, spinner, chests
|
||||
5: Link's actor
|
||||
6: Boomerang, midna
|
||||
7: Most objects, actors, bosses, triggers to be executed after link (most actors go here)
|
||||
8: Various objects and enemies
|
||||
9: Various actors
|
||||
10: Timer, Scene Exit actor
|
||||
11: Grass, Suspend Actors
|
||||
*/
|
||||
|
||||
size_t process_size; // Size of the actor class (use sizeof(my_actor_class))
|
||||
int16_t draw_priority; // an enum value that is prefixed with fpcDwPi. Select an existing value
|
||||
// from the fpcDwPi to pick a draw priority matching the actor you wish
|
||||
// to match priorities with.
|
||||
uint32_t status; // Flags from fopAc_Status_e enum (all have fopAcStts_UNK_0x40000_e, a lot
|
||||
// have fopAcStts_UNK_0x4000_e, add fopAcStts_CULL_e to enable culling)
|
||||
uint8_t group; // The actor type. An enum value from fopAc_Group_e (fopAc_ACTOR_e,
|
||||
// fopAc_ENEMY_e, fopAc_NPC_e)
|
||||
uint8_t cull_type; // Enum value from fopAc_Cull_e
|
||||
int (*create_function)(
|
||||
void*); // Called after the actor is spawned, return type is a enum value of cPhs_Step
|
||||
int (*delete_function)(void*); // Releases resources; returns 1 when deletion is complete
|
||||
int (*execute_function)(void*); // Called once per game tick, the actor's priorityGroup
|
||||
// determines when it will run relative to other actors
|
||||
int (*is_delete_function)(void*); // Returns 1 when normal deletion may begin
|
||||
int (*draw_function)(void*); // Called to draw the actor
|
||||
} ActorProfileDesc;
|
||||
|
||||
typedef struct {
|
||||
uint32_t parameters; // The parameters to be passed to the actor
|
||||
int8_t argument; // The argument to be passed to the actor (acts as an extra byte for a parameter)
|
||||
int8_t room_num; // The room to spawn the actor in
|
||||
struct {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
} position;
|
||||
struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t z;
|
||||
} angle;
|
||||
struct {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
} scale;
|
||||
int (*create_function)(void*); // Optional: A custom function to run when the actor is created.
|
||||
} ActorSpawnParams;
|
||||
|
||||
typedef struct ActorService {
|
||||
ServiceHeader header;
|
||||
ModResult (*register_actor)(ModContext* ctx, const ActorProfileDesc* desc,
|
||||
ProfileName* outProfileName, ActorHandle* outActorHandle);
|
||||
ModResult (*unregister_actor)(ModContext* ctx, ActorHandle handle);
|
||||
ModResult (*create_actor_from_name)(
|
||||
ModContext* ctx, const char* name, const ActorSpawnParams* params, ActorId* outId);
|
||||
ModResult (*create_actor)(
|
||||
ModContext* ctx, ProfileName name, const ActorSpawnParams* params, ActorId* outId);
|
||||
ModResult (*create_child_actor_from_name)(ModContext* ctx, const char* name, ActorId parentID,
|
||||
const ActorSpawnParams* params, ActorId* outId);
|
||||
ModResult (*create_child_actor)(ModContext* ctx, ProfileName name, ActorId parentID,
|
||||
const ActorSpawnParams* params, ActorId* outId);
|
||||
ModResult (*get_actor_id)(ModContext* ctx, ProfileName name, ActorId* outId);
|
||||
ModResult (*get_actor_room_num)(ModContext* ctx, ActorId actorId, int8_t* outRoomNum);
|
||||
/* Returns MOD_UNAVAILABLE if the actor cannot be queued for deletion immediately. */
|
||||
ModResult (*delete_actor)(ModContext* ctx, ActorId actorId);
|
||||
} ActorService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<ActorService> {
|
||||
static constexpr const char* id = ACTOR_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = ACTOR_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = ACTOR_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
@@ -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
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http"
|
||||
#define HTTP_SERVICE_MAJOR 1u
|
||||
#define HTTP_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for an in-flight request. 0 is never a valid handle. */
|
||||
typedef uint64_t HttpRequestHandle;
|
||||
|
||||
typedef enum HttpMethod {
|
||||
HTTP_METHOD_GET = 0,
|
||||
HTTP_METHOD_POST = 1,
|
||||
HTTP_METHOD_HEAD = 2,
|
||||
} HttpMethod;
|
||||
|
||||
/* Transport-level outcome. HTTP status errors are reported through status_code. */
|
||||
typedef enum HttpError {
|
||||
HTTP_ERROR_NONE = 0,
|
||||
HTTP_ERROR_INVALID_URL = 1,
|
||||
HTTP_ERROR_UNSUPPORTED_SCHEME = 2,
|
||||
HTTP_ERROR_TIMEOUT = 3,
|
||||
HTTP_ERROR_TOO_LARGE = 4,
|
||||
HTTP_ERROR_CANCELED = 5,
|
||||
HTTP_ERROR_IO = 6,
|
||||
HTTP_ERROR_NETWORK = 7,
|
||||
} HttpError;
|
||||
|
||||
typedef struct HttpHeader {
|
||||
const char* name;
|
||||
const char* value;
|
||||
} HttpHeader;
|
||||
|
||||
typedef struct HttpRequestDesc {
|
||||
uint32_t struct_size;
|
||||
HttpMethod method;
|
||||
const char* url;
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
/* Request body; POST only. */
|
||||
const void* body;
|
||||
size_t body_size;
|
||||
/* Absolute destination under this mod's data_dir or mod_dir, or NULL for an in-memory body.
|
||||
* GET and POST only. */
|
||||
const char* download_path;
|
||||
uint32_t connect_timeout_ms; /* 0 = 10 seconds */
|
||||
uint32_t idle_timeout_ms; /* 0 = 10 seconds without network progress */
|
||||
uint32_t total_timeout_ms; /* 0 = no total timeout */
|
||||
size_t max_body_bytes; /* 0 = 1 MiB; ignored for downloads */
|
||||
} HttpRequestDesc;
|
||||
|
||||
#define HTTP_REQUEST_DESC_INIT \
|
||||
{sizeof(HttpRequestDesc), HTTP_METHOD_GET, NULL, NULL, 0u, NULL, 0u, NULL, 0u, 0u, 0u, 0u}
|
||||
|
||||
/* Snapshot valid only for the duration of the completion callback. */
|
||||
typedef struct HttpResult {
|
||||
uint32_t struct_size;
|
||||
HttpError error;
|
||||
const char* error_message;
|
||||
int32_t status_code;
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
const void* body;
|
||||
size_t body_size;
|
||||
/* Published absolute destination, or NULL unless a download succeeded. */
|
||||
const char* download_path;
|
||||
} HttpResult;
|
||||
|
||||
/* Runs on the game thread exactly once, unless the calling mod begins deactivation first. */
|
||||
typedef void (*HttpCompleteFn)(
|
||||
ModContext* ctx, HttpRequestHandle request, const HttpResult* result, void* user_data);
|
||||
|
||||
typedef struct HttpProgress {
|
||||
uint32_t struct_size;
|
||||
uint64_t completed_bytes;
|
||||
uint64_t total_bytes;
|
||||
bool total_known;
|
||||
} HttpProgress;
|
||||
|
||||
#define HTTP_PROGRESS_INIT {sizeof(HttpProgress), 0u, 0u, false}
|
||||
|
||||
typedef struct HttpService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Starts an asynchronous HTTPS request. */
|
||||
ModResult (*request)(ModContext* ctx, const HttpRequestDesc* desc, HttpCompleteFn fn,
|
||||
void* user_data, HttpRequestHandle* out_handle);
|
||||
ModResult (*progress)(ModContext* ctx, HttpRequestHandle request, HttpProgress* out_progress);
|
||||
/* Requests cancellation. The completion callback still runs if the mod remains active. */
|
||||
ModResult (*cancel)(ModContext* ctx, HttpRequestHandle request);
|
||||
} HttpService;
|
||||
|
||||
MOD_DECLARE_SERVICE(HttpService, svc_http, HTTP_SERVICE_ID, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR);
|
||||
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/svc/http.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mods::http {
|
||||
|
||||
struct Header {
|
||||
std::string name;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct Request {
|
||||
HttpMethod method = HTTP_METHOD_GET;
|
||||
std::string url;
|
||||
std::vector<Header> headers;
|
||||
std::string body;
|
||||
std::string downloadPath;
|
||||
uint32_t connectTimeoutMs = 0;
|
||||
uint32_t idleTimeoutMs = 0;
|
||||
uint32_t totalTimeoutMs = 0;
|
||||
size_t maxBodyBytes = 0;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
HttpError error = HTTP_ERROR_NETWORK;
|
||||
std::string errorMessage;
|
||||
int statusCode = 0;
|
||||
std::vector<Header> headers;
|
||||
std::vector<uint8_t> body;
|
||||
std::string downloadPath;
|
||||
|
||||
bool ok() const { return error == HTTP_ERROR_NONE && statusCode >= 200 && statusCode < 300; }
|
||||
|
||||
const std::string* header(std::string_view name) const {
|
||||
const auto equal = [](std::string_view left, std::string_view right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
};
|
||||
const auto iter = std::find_if(headers.begin(), headers.end(),
|
||||
[&](const Header& value) { return equal(value.name, name); });
|
||||
return iter != headers.end() ? &iter->value : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct Completion {
|
||||
HttpRequestHandle handle = 0;
|
||||
std::function<void(Response)> callback;
|
||||
};
|
||||
|
||||
inline std::unordered_map<HttpRequestHandle, std::unique_ptr<Completion>> completions;
|
||||
|
||||
inline void complete(ModContext*, HttpRequestHandle handle, const HttpResult* raw, void* userData) {
|
||||
const auto iter = completions.find(handle);
|
||||
if (iter == completions.end() || iter->second.get() != userData) {
|
||||
return;
|
||||
}
|
||||
auto completion = std::move(iter->second);
|
||||
completions.erase(iter);
|
||||
|
||||
Response response;
|
||||
if (raw != nullptr) {
|
||||
response.error = raw->error;
|
||||
response.errorMessage = raw->error_message != nullptr ? raw->error_message : "";
|
||||
response.statusCode = raw->status_code;
|
||||
response.headers.reserve(raw->header_count);
|
||||
for (uint32_t i = 0; i < raw->header_count; ++i) {
|
||||
response.headers.push_back({
|
||||
.name = raw->headers[i].name != nullptr ? raw->headers[i].name : "",
|
||||
.value = raw->headers[i].value != nullptr ? raw->headers[i].value : "",
|
||||
});
|
||||
}
|
||||
if (raw->body != nullptr && raw->body_size != 0) {
|
||||
const auto* begin = static_cast<const uint8_t*>(raw->body);
|
||||
response.body.assign(begin, begin + raw->body_size);
|
||||
}
|
||||
response.downloadPath = raw->download_path != nullptr ? raw->download_path : "";
|
||||
}
|
||||
if (completion->callback) {
|
||||
completion->callback(std::move(response));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class Pending {
|
||||
public:
|
||||
Pending() = default;
|
||||
Pending(HttpRequestHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
|
||||
~Pending() { reset(); }
|
||||
Pending(const Pending&) = delete;
|
||||
Pending& operator=(const Pending&) = delete;
|
||||
Pending(Pending&& other) noexcept { *this = std::move(other); }
|
||||
Pending& operator=(Pending&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
mHandle = std::exchange(other.mHandle, 0);
|
||||
mResult = other.mResult;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
if (mResult != MOD_OK || mHandle == 0 || svc_http == nullptr ||
|
||||
!detail::completions.contains(mHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
HttpProgress value = HTTP_PROGRESS_INIT;
|
||||
return svc_http->progress(mod_ctx, mHandle, &value) == MOD_OK;
|
||||
}
|
||||
ModResult result() const { return mResult; }
|
||||
HttpRequestHandle handle() const { return mHandle; }
|
||||
|
||||
HttpProgress progress() const {
|
||||
HttpProgress value = HTTP_PROGRESS_INIT;
|
||||
if (mHandle != 0 && svc_http != nullptr) {
|
||||
svc_http->progress(mod_ctx, mHandle, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (mHandle != 0 && svc_http != nullptr) {
|
||||
if (svc_http->cancel(mod_ctx, mHandle) != MOD_OK) {
|
||||
detail::completions.erase(mHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void detach() { mHandle = 0; }
|
||||
|
||||
private:
|
||||
void reset() {
|
||||
cancel();
|
||||
mHandle = 0;
|
||||
}
|
||||
|
||||
HttpRequestHandle mHandle = 0;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
inline Pending request(const Request& request, std::function<void(Response)> callback) {
|
||||
if (svc_http == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
if (!callback) {
|
||||
return {0, MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
|
||||
if (request.headers.size() > std::numeric_limits<uint32_t>::max()) {
|
||||
return {0, MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
std::vector<HttpHeader> headers;
|
||||
headers.reserve(request.headers.size());
|
||||
for (const auto& header : request.headers) {
|
||||
headers.push_back({
|
||||
.name = header.name.c_str(),
|
||||
.value = header.value.c_str(),
|
||||
});
|
||||
}
|
||||
HttpRequestDesc desc = HTTP_REQUEST_DESC_INIT;
|
||||
desc.method = request.method;
|
||||
desc.url = request.url.c_str();
|
||||
desc.headers = headers.empty() ? nullptr : headers.data();
|
||||
desc.header_count = static_cast<uint32_t>(headers.size());
|
||||
desc.body = request.body.empty() ? nullptr : request.body.data();
|
||||
desc.body_size = request.body.size();
|
||||
desc.download_path = request.downloadPath.empty() ? nullptr : request.downloadPath.c_str();
|
||||
desc.connect_timeout_ms = request.connectTimeoutMs;
|
||||
desc.idle_timeout_ms = request.idleTimeoutMs;
|
||||
desc.total_timeout_ms = request.totalTimeoutMs;
|
||||
desc.max_body_bytes = request.maxBodyBytes;
|
||||
|
||||
auto completion = std::make_unique<detail::Completion>();
|
||||
auto* userData = completion.get();
|
||||
completion->callback = std::move(callback);
|
||||
HttpRequestHandle handle = 0;
|
||||
const auto result = svc_http->request(mod_ctx, &desc, detail::complete, userData, &handle);
|
||||
if (result != MOD_OK) {
|
||||
return {0, result};
|
||||
}
|
||||
completion->handle = handle;
|
||||
detail::completions.emplace(handle, std::move(completion));
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
} // namespace mods::http
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#define ITEM_SERVICE_ID "dev.twilitrealm.dusklight.item"
|
||||
#define ITEM_SERVICE_MAJOR 2u
|
||||
#define ITEM_SERVICE_MINOR 1u
|
||||
#define ITEM_SERVICE_MINOR 3u
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t ItemCheckHandle;
|
||||
@@ -32,11 +32,13 @@ typedef struct ItemCheckInfo {
|
||||
uint8_t vanilla_item;
|
||||
uint8_t current_item;
|
||||
uint8_t current_display_item;
|
||||
bool was_resolved;
|
||||
} ItemCheckInfo;
|
||||
|
||||
typedef struct ItemCheckResolution {
|
||||
uint8_t item;
|
||||
uint8_t display_item; /* leave unset (0xFF/NONE) to use the item */
|
||||
bool was_resolved;
|
||||
} ItemCheckResolution;
|
||||
|
||||
/* Return true and write out_result to replace the result, or false to leave it unchanged. */
|
||||
@@ -97,6 +99,13 @@ typedef struct ItemService {
|
||||
ModContext* ctx, ItemGiveObserveFn fn, void* user_data, ItemGiveHandle* out_handle);
|
||||
|
||||
ModResult (*unobserve_gives)(ModContext* ctx, ItemGiveHandle handle);
|
||||
|
||||
/* Minor version 2 */
|
||||
|
||||
/* Resolve a live preview (item + display item) without granting an item or notifying give
|
||||
* observers. */
|
||||
ModResult (*resolve_check_full)(ModContext* ctx, const char* name, uint8_t vanilla_item,
|
||||
ItemCheckResolution* out_resolution);
|
||||
} ItemService;
|
||||
|
||||
MOD_DECLARE_SERVICE(ItemService, svc_item, ITEM_SERVICE_ID, ITEM_SERVICE_MAJOR, ITEM_SERVICE_MINOR);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define NET_SERVICE_ID "dev.twilitrealm.dusklight.net"
|
||||
#define NET_SERVICE_MAJOR 1u
|
||||
#define NET_SERVICE_MINOR 0u
|
||||
|
||||
/** 0 is never a valid handle. */
|
||||
typedef uint64_t NetHandle;
|
||||
|
||||
typedef enum NetError {
|
||||
NET_ERROR_NONE = 0,
|
||||
NET_ERROR_INVALID_ENDPOINT = 1,
|
||||
NET_ERROR_RESOLVE = 2,
|
||||
NET_ERROR_TIMEOUT = 3,
|
||||
NET_ERROR_REFUSED = 4,
|
||||
NET_ERROR_UNREACHABLE = 5,
|
||||
NET_ERROR_RESET = 6,
|
||||
NET_ERROR_ADDRESS_IN_USE = 7,
|
||||
NET_ERROR_PERMISSION = 8,
|
||||
NET_ERROR_TOO_LARGE = 9,
|
||||
NET_ERROR_CANCELED = 10,
|
||||
NET_ERROR_NETWORK = 11,
|
||||
} NetError;
|
||||
|
||||
#define NET_ENDPOINT_MAX 80
|
||||
/** NUL-terminated tcp://host:port or udp://host:port endpoint. */
|
||||
typedef struct NetEndpoint {
|
||||
char text[NET_ENDPOINT_MAX];
|
||||
} NetEndpoint;
|
||||
|
||||
typedef struct NetConnectDesc {
|
||||
uint32_t struct_size;
|
||||
/** TCP endpoint. Hostnames and IP literals are accepted. */
|
||||
const char* endpoint;
|
||||
/** 0 defaults to 10 seconds. Resolution is included. */
|
||||
uint32_t connect_timeout_ms;
|
||||
/** 0 defaults to 5 seconds for flush and peer EOF. */
|
||||
uint32_t close_timeout_ms;
|
||||
/** 0 defaults to 1 MiB. Maximum of 8 MiB. */
|
||||
size_t max_send_queue_bytes;
|
||||
bool no_delay;
|
||||
/** Sampled when each event is polled. */
|
||||
void* user_data;
|
||||
} NetConnectDesc;
|
||||
|
||||
#define NET_CONNECT_DESC_INIT {sizeof(NetConnectDesc), NULL, 0u, 0u, 0u, true, NULL}
|
||||
|
||||
typedef struct NetListenDesc {
|
||||
uint32_t struct_size;
|
||||
/** TCP endpoint with an IP literal. Port 0 requests an ephemeral port. */
|
||||
const char* bind;
|
||||
uint32_t close_timeout_ms;
|
||||
size_t max_send_queue_bytes;
|
||||
bool no_delay;
|
||||
void* user_data;
|
||||
} NetListenDesc;
|
||||
|
||||
#define NET_LISTEN_DESC_INIT {sizeof(NetListenDesc), NULL, 0u, 0u, true, NULL}
|
||||
|
||||
typedef struct NetDatagramDesc {
|
||||
uint32_t struct_size;
|
||||
/** UDP endpoint with an IP literal. Port 0 requests an ephemeral port. */
|
||||
const char* bind;
|
||||
size_t max_send_queue_bytes;
|
||||
void* user_data;
|
||||
} NetDatagramDesc;
|
||||
|
||||
#define NET_DATAGRAM_DESC_INIT {sizeof(NetDatagramDesc), NULL, 0u, NULL}
|
||||
|
||||
typedef enum NetEventType {
|
||||
NET_EVENT_NONE = 0,
|
||||
NET_EVENT_CONNECTED = 1,
|
||||
NET_EVENT_ACCEPTED = 2,
|
||||
NET_EVENT_STREAM_DATA = 3,
|
||||
NET_EVENT_DATAGRAM = 4,
|
||||
NET_EVENT_DROPPED = 5,
|
||||
NET_EVENT_RESOLVED = 6,
|
||||
NET_EVENT_CLOSED = 7,
|
||||
} NetEventType;
|
||||
|
||||
typedef struct NetEvent {
|
||||
uint32_t struct_size;
|
||||
NetEventType type;
|
||||
/** Source handle. A CLOSED or RESOLVED handle is invalid after poll_event returns it. */
|
||||
NetHandle handle;
|
||||
void* user_data;
|
||||
NetHandle accepted;
|
||||
/** Peer, datagram source, or resolved endpoint as applicable. */
|
||||
NetEndpoint endpoint;
|
||||
/** Valid until this mod's next poll_event call or deactivation. */
|
||||
const void* data;
|
||||
size_t size;
|
||||
uint32_t dropped;
|
||||
NetError error;
|
||||
/** Never NULL. Valid until this mod's next poll_event call or deactivation. */
|
||||
const char* error_message;
|
||||
} NetEvent;
|
||||
|
||||
#define NET_EVENT_INIT \
|
||||
{sizeof(NetEvent), NET_EVENT_NONE, 0u, NULL, 0u, {{0}}, NULL, 0u, 0u, NET_ERROR_NONE, ""}
|
||||
|
||||
typedef struct NetStats {
|
||||
uint32_t struct_size;
|
||||
size_t queued_send_bytes;
|
||||
uint64_t inbound_dropped;
|
||||
uint64_t send_failures;
|
||||
uint64_t bytes_sent;
|
||||
uint64_t bytes_received;
|
||||
} NetStats;
|
||||
|
||||
#define NET_STATS_INIT {sizeof(NetStats), 0u, 0u, 0u, 0u, 0u}
|
||||
|
||||
typedef struct NetService {
|
||||
ServiceHeader header;
|
||||
|
||||
/** Starts an asynchronous TCP connection. */
|
||||
ModResult (*connect)(ModContext* ctx, const NetConnectDesc* desc, NetHandle* out_handle);
|
||||
/** Opens a TCP listener and returns its local endpoint. */
|
||||
ModResult (*listen)(ModContext* ctx, const NetListenDesc* desc, NetHandle* out_handle,
|
||||
NetEndpoint* out_local, NetError* out_error);
|
||||
/** Opens a UDP socket and returns its local endpoint. */
|
||||
ModResult (*open_datagram)(ModContext* ctx, const NetDatagramDesc* desc, NetHandle* out_handle,
|
||||
NetEndpoint* out_local, NetError* out_error);
|
||||
/** Resolves a TCP or UDP endpoint asynchronously. */
|
||||
ModResult (*resolve)(
|
||||
ModContext* ctx, const char* endpoint, void* user_data, NetHandle* out_handle);
|
||||
/** Returns MOD_OK and NET_EVENT_NONE when the queue is empty. */
|
||||
ModResult (*poll_event)(ModContext* ctx, NetEvent* out_event);
|
||||
/** Copies bytes to a connected stream's outbound queue. */
|
||||
ModResult (*send)(ModContext* ctx, NetHandle stream, const void* data, size_t size);
|
||||
/** Copies one datagram for a literal UDP destination. The maximum size is 65,507 bytes. */
|
||||
ModResult (*send_to)(
|
||||
ModContext* ctx, NetHandle socket, const char* endpoint, const void* data, size_t size);
|
||||
ModResult (*set_user_data)(ModContext* ctx, NetHandle handle, void* user_data);
|
||||
ModResult (*stats)(ModContext* ctx, NetHandle handle, NetStats* out_stats);
|
||||
ModResult (*close)(ModContext* ctx, NetHandle handle);
|
||||
} NetService;
|
||||
|
||||
MOD_DECLARE_SERVICE(NetService, svc_net, NET_SERVICE_ID, NET_SERVICE_MAJOR, NET_SERVICE_MINOR);
|
||||
@@ -0,0 +1,189 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/svc/net.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace mods::net {
|
||||
|
||||
class Socket {
|
||||
public:
|
||||
Socket() = default;
|
||||
Socket(NetHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
|
||||
~Socket() { reset(); }
|
||||
|
||||
Socket(const Socket&) = delete;
|
||||
Socket& operator=(const Socket&) = delete;
|
||||
Socket(Socket&& other) noexcept { *this = std::move(other); }
|
||||
Socket& operator=(Socket&& 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; }
|
||||
NetHandle handle() const { return mHandle; }
|
||||
|
||||
ModResult send(std::span<const std::byte> bytes) const {
|
||||
return svc_net != nullptr && mHandle != 0 ?
|
||||
svc_net->send(mod_ctx, mHandle, bytes.data(), bytes.size()) :
|
||||
MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
ModResult send_to(std::string_view endpoint, std::span<const std::byte> bytes) const {
|
||||
if (svc_net == nullptr || mHandle == 0) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const std::string endpointText{endpoint};
|
||||
return svc_net->send_to(mod_ctx, mHandle, endpointText.c_str(), bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
std::optional<NetStats> stats() const {
|
||||
if (svc_net == nullptr || mHandle == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
NetStats value = NET_STATS_INIT;
|
||||
if (svc_net->stats(mod_ctx, mHandle, &value) != MOD_OK) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
ModResult set_user_data(void* userData) const {
|
||||
return svc_net != nullptr && mHandle != 0 ?
|
||||
svc_net->set_user_data(mod_ctx, mHandle, userData) :
|
||||
MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
ModResult close() {
|
||||
if (mHandle == 0) {
|
||||
return mResult == MOD_OK ? MOD_OK : MOD_UNAVAILABLE;
|
||||
}
|
||||
mResult = svc_net != nullptr ? svc_net->close(mod_ctx, mHandle) : MOD_UNAVAILABLE;
|
||||
mHandle = 0;
|
||||
return mResult;
|
||||
}
|
||||
|
||||
void detach() { mHandle = 0; }
|
||||
|
||||
private:
|
||||
void reset() { (void)close(); }
|
||||
|
||||
NetHandle mHandle = 0;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
struct BindOutcome {
|
||||
std::string local;
|
||||
NetError error = NET_ERROR_NONE;
|
||||
};
|
||||
|
||||
inline Socket connect(std::string_view endpoint, NetConnectDesc options = NET_CONNECT_DESC_INIT) {
|
||||
if (svc_net == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
const std::string endpointText{endpoint};
|
||||
options.struct_size = sizeof(options);
|
||||
options.endpoint = endpointText.c_str();
|
||||
NetHandle handle = 0;
|
||||
const ModResult result = svc_net->connect(mod_ctx, &options, &handle);
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
inline Socket listen(std::string_view bind, BindOutcome* out = nullptr,
|
||||
NetListenDesc options = NET_LISTEN_DESC_INIT) {
|
||||
if (svc_net == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
const std::string bindText{bind};
|
||||
options.struct_size = sizeof(options);
|
||||
options.bind = bindText.c_str();
|
||||
NetHandle handle = 0;
|
||||
NetEndpoint local{};
|
||||
NetError error = NET_ERROR_NONE;
|
||||
const ModResult result = svc_net->listen(mod_ctx, &options, &handle, &local, &error);
|
||||
if (out != nullptr) {
|
||||
*out = {.local = local.text, .error = error};
|
||||
}
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
inline Socket open_datagram(std::string_view bind, BindOutcome* out = nullptr,
|
||||
NetDatagramDesc options = NET_DATAGRAM_DESC_INIT) {
|
||||
if (svc_net == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
const std::string bindText{bind};
|
||||
options.struct_size = sizeof(options);
|
||||
options.bind = bindText.c_str();
|
||||
NetHandle handle = 0;
|
||||
NetEndpoint local{};
|
||||
NetError error = NET_ERROR_NONE;
|
||||
const ModResult result = svc_net->open_datagram(mod_ctx, &options, &handle, &local, &error);
|
||||
if (out != nullptr) {
|
||||
*out = {.local = local.text, .error = error};
|
||||
}
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
inline Socket resolve(std::string_view endpoint, void* userData = nullptr) {
|
||||
if (svc_net == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
const std::string endpointText{endpoint};
|
||||
NetHandle handle = 0;
|
||||
const ModResult result = svc_net->resolve(mod_ctx, endpointText.c_str(), userData, &handle);
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
inline Socket adopt(NetHandle accepted) {
|
||||
return {accepted, accepted != 0 ? MOD_OK : MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
|
||||
struct Event {
|
||||
NetEventType type = NET_EVENT_NONE;
|
||||
NetHandle handle = 0;
|
||||
void* userData = nullptr;
|
||||
NetHandle accepted = 0;
|
||||
std::string_view endpoint;
|
||||
std::span<const std::byte> data;
|
||||
uint32_t dropped = 0;
|
||||
NetError error = NET_ERROR_NONE;
|
||||
std::string_view message;
|
||||
};
|
||||
|
||||
inline bool poll(Event& out) {
|
||||
out = {};
|
||||
if (svc_net == nullptr) {
|
||||
return false;
|
||||
}
|
||||
NetEvent raw = NET_EVENT_INIT;
|
||||
if (svc_net->poll_event(mod_ctx, &raw) != MOD_OK || raw.type == NET_EVENT_NONE) {
|
||||
return false;
|
||||
}
|
||||
out.type = raw.type;
|
||||
out.handle = raw.handle;
|
||||
out.userData = raw.user_data;
|
||||
out.accepted = raw.accepted;
|
||||
out.endpoint = raw.endpoint.text;
|
||||
if (raw.data != nullptr && raw.size != 0) {
|
||||
out.data = {static_cast<const std::byte*>(raw.data), raw.size};
|
||||
}
|
||||
out.dropped = raw.dropped;
|
||||
out.error = raw.error;
|
||||
out.message = raw.error_message != nullptr ? raw.error_message : "";
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mods::net
|
||||
+17
-10
@@ -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,
|
||||
@@ -18,9 +19,9 @@
|
||||
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
|
||||
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
|
||||
* MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild
|
||||
* destroys the previous build's elements, so re-acquire handles inside the build callback rather
|
||||
* than caching them. Strings are UTF-8 and, in both directions, only valid for the duration of the
|
||||
* call.
|
||||
* destroys the previous build's elements, so re-acquire handles in each build callback and use them
|
||||
* only until the next rebuild. Strings are UTF-8 and, in both directions, only valid for the
|
||||
* duration of the call.
|
||||
*/
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
@@ -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,107 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/http.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define WEBSOCKET_SERVICE_ID "dev.twilitrealm.dusklight.websocket"
|
||||
#define WEBSOCKET_SERVICE_MAJOR 1u
|
||||
#define WEBSOCKET_SERVICE_MINOR 0u
|
||||
|
||||
/** Generational connection handle. Zero is never valid. */
|
||||
typedef uint64_t WebSocketHandle;
|
||||
|
||||
/** Connection outcome. Callers must tolerate values added by later service minors. */
|
||||
typedef enum WebSocketError {
|
||||
WEBSOCKET_ERROR_NONE = 0,
|
||||
WEBSOCKET_ERROR_INVALID_URL = 1,
|
||||
WEBSOCKET_ERROR_UNSUPPORTED_SCHEME = 2,
|
||||
WEBSOCKET_ERROR_TIMEOUT = 3,
|
||||
WEBSOCKET_ERROR_TOO_LARGE = 4,
|
||||
WEBSOCKET_ERROR_CANCELED = 5,
|
||||
WEBSOCKET_ERROR_NETWORK = 6,
|
||||
WEBSOCKET_ERROR_PROTOCOL = 7,
|
||||
WEBSOCKET_ERROR_HANDSHAKE = 8,
|
||||
} WebSocketError;
|
||||
|
||||
typedef enum WebSocketMessageKind {
|
||||
WEBSOCKET_MESSAGE_TEXT = 0,
|
||||
WEBSOCKET_MESSAGE_BINARY = 1,
|
||||
} WebSocketMessageKind;
|
||||
|
||||
typedef struct WebSocketConnectDesc {
|
||||
uint32_t struct_size;
|
||||
/** wss:// URL, or ws:// for localhost, 127.0.0.1, or [::1]. */
|
||||
const char* url;
|
||||
/** Request headers. WebSocket handshake headers and User-Agent are reserved. */
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
const char* const* protocols;
|
||||
uint32_t protocol_count;
|
||||
uint32_t connect_timeout_ms;
|
||||
uint32_t close_timeout_ms;
|
||||
uint32_t keepalive_interval_ms;
|
||||
/** 0 defaults to 1 MiB. Maximum of 16 MiB. */
|
||||
size_t max_message_bytes;
|
||||
/** Passed in every event. */
|
||||
void* user_data;
|
||||
} WebSocketConnectDesc;
|
||||
|
||||
#define WEBSOCKET_CONNECT_DESC_INIT \
|
||||
{sizeof(WebSocketConnectDesc), NULL, NULL, 0u, NULL, 0u, 0u, 0u, 0u, 0u, NULL}
|
||||
|
||||
typedef enum WebSocketEventType {
|
||||
WEBSOCKET_EVENT_NONE = 0,
|
||||
WEBSOCKET_EVENT_OPEN = 1,
|
||||
WEBSOCKET_EVENT_MESSAGE = 2,
|
||||
WEBSOCKET_EVENT_CLOSED = 3,
|
||||
} WebSocketEventType;
|
||||
|
||||
typedef struct WebSocketEvent {
|
||||
uint32_t struct_size;
|
||||
WebSocketEventType type;
|
||||
WebSocketHandle ws;
|
||||
void* user_data;
|
||||
|
||||
const char* protocol;
|
||||
/** Handshake response headers for OPEN or a handshake-rejected CLOSED event. */
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
|
||||
WebSocketMessageKind message_kind;
|
||||
/** Valid until this mod's next poll_event call or deactivation. */
|
||||
const void* data;
|
||||
size_t size;
|
||||
|
||||
WebSocketError error;
|
||||
/** Never NULL. Valid until this mod's next poll_event call or deactivation. */
|
||||
const char* error_message;
|
||||
int32_t handshake_status;
|
||||
uint16_t close_code;
|
||||
const char* close_reason;
|
||||
} WebSocketEvent;
|
||||
|
||||
#define WEBSOCKET_EVENT_INIT \
|
||||
{sizeof(WebSocketEvent), WEBSOCKET_EVENT_NONE, 0u, NULL, "", NULL, 0u, WEBSOCKET_MESSAGE_TEXT, \
|
||||
NULL, 0u, WEBSOCKET_ERROR_NONE, "", 0, 0u, ""}
|
||||
|
||||
typedef struct WebSocketService {
|
||||
ServiceHeader header;
|
||||
|
||||
/** Starts a connection. */
|
||||
ModResult (*connect)(
|
||||
ModContext* ctx, const WebSocketConnectDesc* desc, WebSocketHandle* out_handle);
|
||||
/** Returns MOD_OK and WEBSOCKET_EVENT_NONE when the queue is empty. */
|
||||
ModResult (*poll_event)(ModContext* ctx, WebSocketEvent* out_event);
|
||||
/** Copies a message into the outbound queue. */
|
||||
ModResult (*send)(ModContext* ctx, WebSocketHandle ws, WebSocketMessageKind kind,
|
||||
const void* data, size_t size);
|
||||
/** Code 0 defaults to 1000; accepted: 1000, 1001, and 3000-4999. */
|
||||
ModResult (*close)(ModContext* ctx, WebSocketHandle ws, uint16_t code, const char* reason);
|
||||
} WebSocketService;
|
||||
|
||||
MOD_DECLARE_SERVICE(WebSocketService, svc_websocket, WEBSOCKET_SERVICE_ID, WEBSOCKET_SERVICE_MAJOR,
|
||||
WEBSOCKET_SERVICE_MINOR);
|
||||
@@ -0,0 +1,172 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/svc/http.hpp>
|
||||
#include <mods/svc/websocket.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mods::ws {
|
||||
|
||||
struct Options {
|
||||
std::string url;
|
||||
std::vector<http::Header> headers;
|
||||
std::vector<std::string> protocols;
|
||||
uint32_t connectTimeoutMs = 0;
|
||||
uint32_t closeTimeoutMs = 0;
|
||||
uint32_t keepaliveIntervalMs = 0;
|
||||
size_t maxMessageBytes = 0;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
class Connection {
|
||||
public:
|
||||
Connection() = default;
|
||||
Connection(WebSocketHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
|
||||
~Connection() { reset(); }
|
||||
|
||||
Connection(const Connection&) = delete;
|
||||
Connection& operator=(const Connection&) = delete;
|
||||
Connection(Connection&& other) noexcept { *this = std::move(other); }
|
||||
Connection& operator=(Connection&& 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; }
|
||||
WebSocketHandle handle() const { return mHandle; }
|
||||
|
||||
ModResult send(WebSocketMessageKind kind, std::span<const std::byte> bytes) const {
|
||||
return svc_websocket != nullptr && mHandle != 0 ?
|
||||
svc_websocket->send(mod_ctx, mHandle, kind, bytes.data(), bytes.size()) :
|
||||
MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
ModResult send_text(std::string_view text) const {
|
||||
return svc_websocket != nullptr && mHandle != 0 ?
|
||||
svc_websocket->send(
|
||||
mod_ctx, mHandle, WEBSOCKET_MESSAGE_TEXT, text.data(), text.size()) :
|
||||
MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
ModResult send_binary(std::span<const std::byte> bytes) const {
|
||||
return send(WEBSOCKET_MESSAGE_BINARY, bytes);
|
||||
}
|
||||
|
||||
ModResult close(uint16_t code = 1000, std::string_view reason = {}) {
|
||||
if (svc_websocket == nullptr || mHandle == 0) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const std::string reasonText{reason};
|
||||
mResult = svc_websocket->close(mod_ctx, mHandle, code, reasonText.c_str());
|
||||
if (mResult == MOD_OK) {
|
||||
mHandle = 0;
|
||||
}
|
||||
return mResult;
|
||||
}
|
||||
|
||||
void detach() { mHandle = 0; }
|
||||
|
||||
private:
|
||||
void reset() {
|
||||
if (mHandle != 0) {
|
||||
(void)close(1001, "Connection owner released");
|
||||
mHandle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
WebSocketHandle mHandle = 0;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
inline Connection connect(const Options& options) {
|
||||
if (svc_websocket == nullptr || options.headers.size() > std::numeric_limits<uint32_t>::max() ||
|
||||
options.protocols.size() > std::numeric_limits<uint32_t>::max())
|
||||
{
|
||||
return {0, svc_websocket == nullptr ? MOD_UNAVAILABLE : MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
|
||||
std::vector<HttpHeader> headers;
|
||||
headers.reserve(options.headers.size());
|
||||
for (const auto& header : options.headers) {
|
||||
headers.push_back({.name = header.name.c_str(), .value = header.value.c_str()});
|
||||
}
|
||||
std::vector<const char*> protocols;
|
||||
protocols.reserve(options.protocols.size());
|
||||
for (const auto& protocol : options.protocols) {
|
||||
protocols.push_back(protocol.c_str());
|
||||
}
|
||||
|
||||
WebSocketConnectDesc desc = WEBSOCKET_CONNECT_DESC_INIT;
|
||||
desc.url = options.url.c_str();
|
||||
desc.headers = headers.empty() ? nullptr : headers.data();
|
||||
desc.header_count = static_cast<uint32_t>(headers.size());
|
||||
desc.protocols = protocols.empty() ? nullptr : protocols.data();
|
||||
desc.protocol_count = static_cast<uint32_t>(protocols.size());
|
||||
desc.connect_timeout_ms = options.connectTimeoutMs;
|
||||
desc.close_timeout_ms = options.closeTimeoutMs;
|
||||
desc.keepalive_interval_ms = options.keepaliveIntervalMs;
|
||||
desc.max_message_bytes = options.maxMessageBytes;
|
||||
desc.user_data = options.userData;
|
||||
|
||||
WebSocketHandle handle = 0;
|
||||
const ModResult result = svc_websocket->connect(mod_ctx, &desc, &handle);
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
struct Event {
|
||||
WebSocketEventType type = WEBSOCKET_EVENT_NONE;
|
||||
WebSocketHandle handle = 0;
|
||||
void* userData = nullptr;
|
||||
std::string_view protocol;
|
||||
std::span<const HttpHeader> headers;
|
||||
WebSocketMessageKind messageKind = WEBSOCKET_MESSAGE_TEXT;
|
||||
std::span<const std::byte> data;
|
||||
WebSocketError error = WEBSOCKET_ERROR_NONE;
|
||||
std::string_view message;
|
||||
int handshakeStatus = 0;
|
||||
uint16_t closeCode = 0;
|
||||
std::string_view closeReason;
|
||||
};
|
||||
|
||||
inline bool poll(Event& out) {
|
||||
out = {};
|
||||
if (svc_websocket == nullptr) {
|
||||
return false;
|
||||
}
|
||||
WebSocketEvent raw = WEBSOCKET_EVENT_INIT;
|
||||
if (svc_websocket->poll_event(mod_ctx, &raw) != MOD_OK || raw.type == WEBSOCKET_EVENT_NONE) {
|
||||
return false;
|
||||
}
|
||||
out.type = raw.type;
|
||||
out.handle = raw.ws;
|
||||
out.userData = raw.user_data;
|
||||
out.protocol = raw.protocol != nullptr ? raw.protocol : "";
|
||||
if (raw.headers != nullptr && raw.header_count != 0) {
|
||||
out.headers = {raw.headers, raw.header_count};
|
||||
}
|
||||
out.messageKind = raw.message_kind;
|
||||
if (raw.data != nullptr && raw.size != 0) {
|
||||
out.data = {static_cast<const std::byte*>(raw.data), raw.size};
|
||||
}
|
||||
out.error = raw.error;
|
||||
out.message = raw.error_message != nullptr ? raw.error_message : "";
|
||||
out.handshakeStatus = raw.handshake_status;
|
||||
out.closeCode = raw.close_code;
|
||||
out.closeReason = raw.close_reason != nullptr ? raw.close_reason : "";
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mods::ws
|
||||
Reference in New Issue
Block a user