mirror of
https://github.com/TwilitRealm/dusklight.git
synced 2026-09-12 21:29:43 -07:00
Mods: NetService and WebSocketService (#2385)
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::io 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::net borealis::presentation borealis::sentry borealis::update borealis::ws 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)
|
||||
|
||||
@@ -451,6 +451,82 @@ For large responses, set `downloadPath` to an absolute path in the calling mod's
|
||||
an empty `body` and the final path in `downloadPath`. Check `Response::ok()` before using the file.
|
||||
`Pending::progress()` reports download progress when the server provides a total size.
|
||||
|
||||
### WebSocketService ([`mods/svc/websocket.h`](../sdk/include/mods/svc/websocket.h))
|
||||
|
||||
WebSocket client connections with text or binary messages. Secure `wss://` URLs are supported everywhere. Insecure
|
||||
`ws://` is limited to `localhost`, `127.0.0.1`, and `[::1]`.
|
||||
|
||||
```cpp
|
||||
#include "mods/svc/websocket.hpp"
|
||||
|
||||
IMPORT_SERVICE(WebSocketService, svc_websocket);
|
||||
|
||||
mods::ws::Connection connection;
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError*) {
|
||||
connection = mods::ws::connect({.url = "wss://example.com/events"});
|
||||
return connection ? MOD_OK : connection.result();
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
mods::ws::Event event;
|
||||
while (mods::ws::poll(event)) {
|
||||
if (event.type == WEBSOCKET_EVENT_MESSAGE) {
|
||||
consume(event.data);
|
||||
} else if (event.type == WEBSOCKET_EVENT_CLOSED) {
|
||||
schedule_reconnect(event.error);
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
```
|
||||
|
||||
Keep the `Connection` alive and drain `mods::ws::poll()` regularly, usually on every `mod_update` tick. A connection
|
||||
emits an (optional) `OPEN` event, zero to many `MESSAGE` events, and exactly one `CLOSED` event.
|
||||
|
||||
**Restrictions:** A mod may only open four connections at once. Messages have a 1 MiB limit by default, and may request
|
||||
up to 16 MiB. Unread data is limited to 16 MiB and the outbound queue to 4 MiB. `send` returns `MOD_CONFLICT` when the
|
||||
outbound queue is full. Dropping the `Connection` or deactivating the mod attempts to gracefully close with code 1001,
|
||||
until the close deadline expires.
|
||||
|
||||
### NetService ([`mods/svc/net.h`](../sdk/include/mods/svc/net.h))
|
||||
|
||||
Asynchronous raw TCP and UDP networking. Endpoints can be `tcp://host:port` or `udp://host:port`. TCP connections and
|
||||
`resolve` accept hostnames. Listeners and UDP endpoints require IP literals.
|
||||
|
||||
```cpp
|
||||
#include "mods/svc/net.hpp"
|
||||
|
||||
IMPORT_SERVICE(NetService, svc_net);
|
||||
|
||||
mods::net::BindOutcome bound;
|
||||
mods::net::Socket listener;
|
||||
mods::net::Socket client;
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError*) {
|
||||
listener = mods::net::listen("tcp://127.0.0.1:0", &bound);
|
||||
client = mods::net::connect(bound.local);
|
||||
return listener && client ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
mods::net::Event event;
|
||||
while (mods::net::poll(event)) {
|
||||
if (event.type == NET_EVENT_ACCEPTED) {
|
||||
remember_client(mods::net::adopt(event.accepted));
|
||||
} else if (event.type == NET_EVENT_STREAM_DATA) {
|
||||
consume(event.data);
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
```
|
||||
|
||||
`send` and `send_to` copy the payload and return `MOD_CONFLICT` when the socket's outbound queue is full. `stats`
|
||||
reports queued bytes, traffic, dropped inbound datagrams, and asynchronous UDP send failures.
|
||||
|
||||
**Restrictions:** A mod may only have 32 streams, 4 listeners, 4 UDP sockets, and 8 DNS resolutions active at once.
|
||||
|
||||
### HostService ([`mods/svc/host.h`](../sdk/include/mods/svc/host.h))
|
||||
|
||||
Mod metadata and runtime interaction with the loader.
|
||||
|
||||
Vendored
+1
-1
Submodule extern/borealis updated: 08de28e885...c3b8014495
@@ -1501,6 +1501,9 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/hook.cpp
|
||||
src/dusk/mods/svc/host.cpp
|
||||
src/dusk/mods/svc/http.cpp
|
||||
src/dusk/mods/svc/net.cpp
|
||||
src/dusk/mods/svc/net.hpp
|
||||
src/dusk/mods/svc/websocket.cpp
|
||||
src/dusk/mods/svc/item.cpp
|
||||
src/dusk/mods/svc/item.hpp
|
||||
src/dusk/mods/svc/log.cpp
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
android:appCategory="game"
|
||||
android:icon="@mipmap/icon"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@android:style/Theme.NoTitleBar"
|
||||
android:enableOnBackInvokedCallback="false">
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
<domain includeSubdomains="false">::1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -85,5 +85,12 @@
|
||||
<true/>
|
||||
<key>LSSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Dusklight allows network-enabled mods to connect to devices on your local network.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -32,5 +32,12 @@
|
||||
<string>public.app-category.adventure-games</string>
|
||||
<key>LSSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Dusklight allows network-enabled mods to connect to devices on your local network.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -47,5 +47,12 @@
|
||||
<string>Automatic</string>
|
||||
<key>LSSupportsGameMode</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Dusklight allows network-enabled mods to connect to devices on your local network.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+91
-217
@@ -1,107 +1,80 @@
|
||||
#if _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
using socket_t = SOCKET;
|
||||
static void closeSocket(socket_t s) {
|
||||
LINGER li{1, 0};
|
||||
setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char*>(&li), sizeof(li));
|
||||
closesocket(s);
|
||||
}
|
||||
static int socketError(socket_t s) {
|
||||
int err = 0;
|
||||
int len = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &len);
|
||||
return err;
|
||||
}
|
||||
static constexpr int kSendFlags = 0;
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
using socket_t = int;
|
||||
static void closeSocket(socket_t s) {
|
||||
struct linger li{1, 0};
|
||||
setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li));
|
||||
close(s);
|
||||
}
|
||||
static int socketError(socket_t s) {
|
||||
int err = 0;
|
||||
socklen_t len = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len);
|
||||
return err;
|
||||
}
|
||||
#ifndef INVALID_SOCKET
|
||||
#define INVALID_SOCKET -1
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
static constexpr int kSendFlags = 0;
|
||||
#else
|
||||
static constexpr int kSendFlags = MSG_NOSIGNAL;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include "dusk/livesplit.h"
|
||||
|
||||
#include "borealis/net.hpp"
|
||||
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
namespace dusk::speedrun {
|
||||
namespace {
|
||||
|
||||
static bool running = false;
|
||||
static bool startPending = false;
|
||||
static uint64_t frameCount = 0;
|
||||
static socket_t sock = INVALID_SOCKET;
|
||||
static bool wasLoading = false;
|
||||
static bool connected = false;
|
||||
static bool connectPending = false;
|
||||
static bool disconnectPending = false;
|
||||
static uint32_t idleProbeCounter = 0;
|
||||
static uint32_t reconnectCounter = 0;
|
||||
static char storedHost[64] = "127.0.0.1";
|
||||
static int storedPort = 16834;
|
||||
bool running = false;
|
||||
bool startPending = false;
|
||||
uint64_t frameCount = 0;
|
||||
bool wasLoading = false;
|
||||
bool connected = false;
|
||||
bool connectPending = false;
|
||||
bool disconnectPending = false;
|
||||
uint32_t reconnectCounter = 0;
|
||||
std::string storedEndpoint = "tcp://127.0.0.1:16834";
|
||||
std::unique_ptr<borealis::net::Context> netContext;
|
||||
borealis::net::SocketId socketId = 0;
|
||||
|
||||
static void sendCmd(const char* cmd) {
|
||||
if (sock == INVALID_SOCKET) {
|
||||
void send_cmd(const char* command) {
|
||||
if (!netContext || !connected || socketId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char msg[64];
|
||||
const int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd);
|
||||
if (len <= 0 || len >= static_cast<int>(sizeof(msg))) {
|
||||
char message[64];
|
||||
const int length = snprintf(message, sizeof(message), "%s\r\n", command);
|
||||
if (length <= 0 || length >= static_cast<int>(sizeof(message))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (send(sock, msg, len, kSendFlags) >= 0) {
|
||||
if (!connected) {
|
||||
connected = connectPending = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const int err = WSAGetLastError();
|
||||
if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOTCONN) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = connectPending = false;
|
||||
reconnectCounter = 0;
|
||||
const auto chars = std::span<const char>{message, static_cast<size_t>(length)};
|
||||
netContext->send(socketId, std::as_bytes(chars));
|
||||
}
|
||||
|
||||
void reconnect() {
|
||||
netContext.reset();
|
||||
netContext = std::make_unique<borealis::net::Context>();
|
||||
connected = false;
|
||||
connectPending = false;
|
||||
socketId = netContext->connect(storedEndpoint);
|
||||
}
|
||||
|
||||
void poll_network() {
|
||||
if (!netContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
borealis::net::Event event;
|
||||
while (netContext->poll(event)) {
|
||||
if (event.id != socketId) {
|
||||
continue;
|
||||
}
|
||||
if (event.kind == borealis::net::Event::Kind::Connected) {
|
||||
connected = true;
|
||||
connectPending = true;
|
||||
send_cmd("initgametime");
|
||||
} else if (event.kind == borealis::net::Event::Kind::Closed) {
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
connected = false;
|
||||
connectPending = false;
|
||||
socketId = 0;
|
||||
reconnectCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t getFrameCount() {
|
||||
return frameCount;
|
||||
}
|
||||
@@ -111,10 +84,9 @@ void onGameFrame() {
|
||||
return;
|
||||
}
|
||||
|
||||
bool loading = fopOvlpM_IsDoingReq() != 0;
|
||||
|
||||
const bool loading = fopOvlpM_IsDoingReq() != 0;
|
||||
if (loading != wasLoading) {
|
||||
sendCmd(loading ? "pausegametime" : "unpausegametime");
|
||||
send_cmd(loading ? "pausegametime" : "unpausegametime");
|
||||
wasLoading = loading;
|
||||
}
|
||||
|
||||
@@ -141,172 +113,74 @@ void reset() {
|
||||
startPending = false;
|
||||
frameCount = 0;
|
||||
wasLoading = false;
|
||||
sendCmd("reset");
|
||||
}
|
||||
|
||||
static void reconnect() {
|
||||
if (sock != INVALID_SOCKET) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
connected = connectPending = false;
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
u_long nb = 1;
|
||||
if (ioctlsocket(sock, FIONBIO, &nb) != 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
const int fl = fcntl(sock, F_GETFL, 0);
|
||||
if (fl < 0 || fcntl(sock, F_SETFL, fl | O_NONBLOCK) < 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
{
|
||||
int opt = 1;
|
||||
setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt));
|
||||
}
|
||||
#endif
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(storedPort));
|
||||
if (inet_pton(AF_INET, storedHost, &addr.sin_addr) != 1) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
|
||||
const int cr = connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
|
||||
#if _WIN32
|
||||
const bool connectPending_ = cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK;
|
||||
#else
|
||||
const bool connectPending_ = cr < 0 && errno == EINPROGRESS;
|
||||
#endif
|
||||
if (cr != 0 && !connectPending_) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
send_cmd("reset");
|
||||
}
|
||||
|
||||
void connectLiveSplit(const char* host, int port) {
|
||||
#if _WIN32
|
||||
WSADATA wd{};
|
||||
WSAStartup(MAKEWORD(2, 2), &wd);
|
||||
#endif
|
||||
snprintf(storedHost, sizeof(storedHost), "%s", host);
|
||||
storedPort = port;
|
||||
std::string endpointHost = host;
|
||||
if (endpointHost.find(':') != std::string::npos &&
|
||||
!(endpointHost.starts_with('[') && endpointHost.ends_with(']')))
|
||||
{
|
||||
endpointHost = '[' + endpointHost + ']';
|
||||
}
|
||||
storedEndpoint = "tcp://" + endpointHost + ':' + std::to_string(port);
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void disconnectLiveSplit() {
|
||||
if (sock != INVALID_SOCKET) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
connected = connectPending = disconnectPending = false;
|
||||
netContext.reset();
|
||||
socketId = 0;
|
||||
connected = false;
|
||||
connectPending = false;
|
||||
disconnectPending = false;
|
||||
}
|
||||
|
||||
bool consumeConnectedEvent() {
|
||||
bool v = connectPending;
|
||||
const bool value = connectPending;
|
||||
connectPending = false;
|
||||
return v;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool consumeDisconnectedEvent() {
|
||||
bool v = disconnectPending;
|
||||
const bool value = disconnectPending;
|
||||
disconnectPending = false;
|
||||
return v;
|
||||
return value;
|
||||
}
|
||||
|
||||
void updateLiveSplit() {
|
||||
if (sock == INVALID_SOCKET) {
|
||||
poll_network();
|
||||
if (socketId == 0) {
|
||||
if ((reconnectCounter++ % 30) == 0) {
|
||||
reconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
fd_set writefds, errorfds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_ZERO(&errorfds);
|
||||
FD_SET(sock, &writefds);
|
||||
FD_SET(sock, &errorfds);
|
||||
timeval tv{0, 0};
|
||||
#if _WIN32
|
||||
const int r = select(0, nullptr, &writefds, &errorfds, &tv);
|
||||
#else
|
||||
const int r = select(sock + 1, nullptr, &writefds, &errorfds, &tv);
|
||||
#endif
|
||||
if (r < 0 || FD_ISSET(sock, &errorfds) || socketError(sock) != 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
reconnectCounter = 0;
|
||||
return;
|
||||
}
|
||||
if (!FD_ISSET(sock, &writefds)) {
|
||||
return;
|
||||
}
|
||||
sendCmd("initgametime");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startPending) {
|
||||
startPending = false;
|
||||
sendCmd("initgametime");
|
||||
sendCmd("reset");
|
||||
sendCmd("starttimer");
|
||||
send_cmd("initgametime");
|
||||
send_cmd("reset");
|
||||
send_cmd("starttimer");
|
||||
}
|
||||
|
||||
if (!running) {
|
||||
if ((idleProbeCounter++ % 60) == 0) {
|
||||
char buf;
|
||||
const int r = recv(sock, &buf, 1, 0);
|
||||
if (r == 0
|
||||
#if _WIN32
|
||||
|| (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK)
|
||||
#else
|
||||
|| (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = connectPending = false;
|
||||
reconnectCounter = 0;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t totalMs = frameCount * 1000 / 30;
|
||||
const uint64_t totalSec = totalMs / 1000;
|
||||
char cmd[32];
|
||||
snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u",
|
||||
char command[32];
|
||||
snprintf(command, sizeof(command), "setgametime %u:%02u:%02u.%03u",
|
||||
static_cast<uint32_t>(totalSec / 3600), static_cast<uint32_t>((totalSec / 60) % 60),
|
||||
static_cast<uint32_t>(totalSec % 60), static_cast<uint32_t>(totalMs % 1000));
|
||||
sendCmd(cmd);
|
||||
send_cmd(command);
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
disconnectLiveSplit();
|
||||
#if _WIN32
|
||||
WSACleanup();
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::speedrun
|
||||
|
||||
@@ -1027,7 +1027,12 @@ void ModLoader::deactivate_mod(LoadedMod& mod) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{} failed: {}", shutdownName,
|
||||
lifecycle_error_message(shutdownName, result, error));
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
log::write(
|
||||
mod.metadata.id, LOG_LEVEL_ERROR, "{} threw: {}", shutdownName, exception.what());
|
||||
} catch (...) {
|
||||
log::write(
|
||||
mod.metadata.id, LOG_LEVEL_ERROR, "{} threw an unknown exception", shutdownName);
|
||||
}
|
||||
}
|
||||
mod.initialized = false;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#include "dusk/mods/svc/actor.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/camera_operators.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "config.hpp"
|
||||
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/config.hpp"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
#include "internal.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#include "dusk/game_mode.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user