mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-05 17:47:54 -04:00
Mods: Support Luau mods (#2377)
* Mods: Support Luau mods * Enable `integer` type & use it * Link to headers in modding.md * Update link to webgpu.h
This commit is contained in:
@@ -565,6 +565,7 @@ include(cmake/ModSDK.cmake)
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
add_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
|
||||
add_subdirectory(mods/luau_runtime)
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
|
||||
+324
-108
@@ -1,35 +1,137 @@
|
||||
# Dusklight Mod API
|
||||
|
||||
Mods are `.dusk` bundles: zip archives that can contain code (in the form of native libraries), resources, DVD overlay
|
||||
Mods are `.dusk` bundles: zip archives that can contain code (native libraries or scripts), resources, DVD overlay
|
||||
files, and texture replacements. Mods may be enabled, disabled and reloaded at runtime.
|
||||
|
||||
When code mods are loaded, they get dynamically linked by the operating system to the running game process. The mod
|
||||
There are three types of mods:
|
||||
|
||||
- **Asset-only mods**: Mods that contain no code, and may replace files on the game disc ("overlays") and provide
|
||||
replacement textures (texture packs).
|
||||
- **Native mods (C++)**: Fully-featured, with the ability to interop with game code and hook functions. Must be compiled
|
||||
for every supported platform. (See [mod-template](https://github.com/TwilitRealm/mod-template))
|
||||
- **Script mods (Luau)**: Simple, widely-compatible, but can only access provided services.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [mod.json](#modjson)
|
||||
2. [Asset-only Mods](#asset-only-mods)
|
||||
3. [Native Mods (C++)](#native-mods)
|
||||
4. [Script Mods (Luau)](#script-mods)
|
||||
5. [Services](#services)
|
||||
6. [Built-in Services](#built-in-services)
|
||||
7. [Hooking Game Functions](#hooking-game-functions)
|
||||
8. [Asset Overlays](#asset-overlays)
|
||||
9. [Runtime Lifecycle](#runtime-lifecycle)
|
||||
10. [Error Handling](#error-handling)
|
||||
11. [Advanced](#advanced)
|
||||
|
||||
---
|
||||
|
||||
## mod.json
|
||||
|
||||
Every mod starts with a single file, a `mod.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.example.my_mod",
|
||||
"name": "My Mod",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "A short description shown in the mod manager.",
|
||||
"icon": "res/icon.png",
|
||||
"banner": "res/banner.png"
|
||||
}
|
||||
```
|
||||
|
||||
`id` is required: a unique, stable identifier (reverse-DNS style; periods, underscores, and lowercase alphanumerics).
|
||||
Everything else is optional but recommended.
|
||||
|
||||
`icon` and `banner` are bundle paths to PNG images that display in the in-game mod manager and mod website. A square
|
||||
icon (1:1), and a banner (~3.5:1, minimum 800px width). If omitted, `res/icon.png` and `res/banner.png` are used
|
||||
automatically when present.
|
||||
|
||||
Simply create a zip file with a `mod.json`, and rename it to `.dusk`. That's it!
|
||||
|
||||
---
|
||||
|
||||
## Asset-only Mods
|
||||
|
||||
```
|
||||
my_mod.dusk
|
||||
├── mod.json
|
||||
├── res/ (optional bundled resources)
|
||||
├── overlay/ (optional game file overrides)
|
||||
└── textures/ (optional texture replacements)
|
||||
```
|
||||
|
||||
Place files in `overlay/` to replace the disc version of that file. Examples:
|
||||
|
||||
- `overlay/Movie/demo_movie98_00.thp`: Replaces `Movie/demo_movie98_00.thp` on disc.
|
||||
- `overlay/res/Object/Kmdl/archive/bmwr/al.bmd`: Replaces `archive/bmwr/al.bmd` _within_ `res/Object/Kmdl.arc` without
|
||||
overwriting the entire `.arc`.
|
||||
|
||||
Place textures in `textures/` to automatically register them as texture replacements when active. These follow the same
|
||||
[Dolphin-compatible naming scheme](#textureservice-modssvctextureh) as the user `<data>/texture_replacements/`
|
||||
directory. Directories are scanned recursively. Examples:
|
||||
|
||||
- `textures/tex1_256x128_e6a4c7be9bf48305_14.dds`: Replaces the Hero's Clothes texture.
|
||||
|
||||
Simply zip the `mod.json` and adjacent folders, then rename to `.dusk`. Then, copy the `.dusk` into the user mods
|
||||
folder:
|
||||
|
||||
- Windows: `%APPDATA%\TwilitRealm\Dusklight\mods`
|
||||
- Linux: `~/.local/share/TwilitRealm/Dusklight/mods`
|
||||
- macOS: `~/Library/Application Support/TwilitRealm/Dusklight/mods`
|
||||
|
||||
---
|
||||
|
||||
## Native Mods
|
||||
|
||||
Mods built with [mod-template](https://github.com/TwilitRealm/mod-template) are native C++ mods. Native mods are very
|
||||
powerful and can interact with and [hook](#hooking-game-functions) game code directly. All features are available to
|
||||
native mods.
|
||||
|
||||
Example C++ mod:
|
||||
|
||||
```cpp
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/log.h"
|
||||
|
||||
DEFINE_MOD(); // once, in exactly one translation unit
|
||||
IMPORT_SERVICE(LogService, svc_log); // resolved by the loader before mod_initialize
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
svc_log->info(mod_ctx, "hello from my_mod");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError* error) { // called every frame
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError* error) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
When native mods are loaded, they get dynamically linked by the operating system to the running game process. The mod
|
||||
exports lifecycle functions that Dusklight calls into (`mod_initialize`, `mod_update`, `mod_shutdown`), and the mod
|
||||
communicates with the host via **services**: plain C APIs, individually versioned. Dusklight exports several built-in
|
||||
services, and mods may export services of their own, permitting framework mods and cross-mod integration.
|
||||
|
||||
Beyond services, mods have full access to the original game's code: include game headers, call directly into any public
|
||||
function, read and write data fields, and hook the vast majority of game functions.
|
||||
Beyond services, native mods have full access to the original game's code: include game headers, call directly into any
|
||||
public function, read and write data fields, and hook the vast majority of game functions.
|
||||
|
||||
## Table of Contents
|
||||
### Quick Start (Native Mods)
|
||||
|
||||
1. [Getting Started](#getting-started)
|
||||
2. [mod.json](#modjson)
|
||||
3. [Anatomy of a Code Mod](#anatomy-of-a-code-mod)
|
||||
4. [Services](#services)
|
||||
5. [Built-in Services](#built-in-services)
|
||||
6. [Hooking Game Functions](#hooking-game-functions)
|
||||
7. [Asset Overlays](#asset-overlays)
|
||||
8. [Runtime Lifecycle](#runtime-lifecycle)
|
||||
9. [Error Handling](#error-handling)
|
||||
10. [Advanced](#advanced)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
Fork the [mod template](https://github.com/TwilitRealm/mod-template), a self-contained CMake project that uses the
|
||||
Dusklight mod SDK.
|
||||
Create a repository from
|
||||
the [mod-template](https://github.com/new?template_name=mod-template&template_owner=TwilitRealm),
|
||||
a self-contained CMake project that uses the Dusklight mod SDK. It includes a GitHub Actions CI workflow that builds the
|
||||
mod for every supported platform.
|
||||
|
||||
```
|
||||
my_mod/
|
||||
@@ -65,10 +167,12 @@ add_mod(my_mod
|
||||
|
||||
Available features:
|
||||
|
||||
- `fmt`: Provides the header-only `{fmt}` library and the formatted logging helpers in `mods/svc/log.hpp`.
|
||||
- `fmt`: Provides the header-only `{fmt}` library and the formatted logging helpers in
|
||||
[`mods/svc/log.hpp`](../sdk/include/mods/svc/log.hpp).
|
||||
- `game`: Allows calling into and hooking game code. Mods that **only** use services may omit it, providing a wider
|
||||
range of compatibility with Dusklight versions and a slightly faster build process.
|
||||
- `webgpu`: Allows importing the WebGPU API (`webgpu/webgpu.h`). Must be enabled when using
|
||||
- `webgpu`: Allows importing the WebGPU API
|
||||
([`webgpu/webgpu.h`](https://github.com/webgpu-native/webgpu-headers/blob/main/webgpu.h)). Must be enabled when using
|
||||
[GfxService](#gfxservice-modssvcgfxh).
|
||||
|
||||
Building produces `my_mod.dusk` in `build/mods/`. Copy the `.dusk` into the user mods folder:
|
||||
@@ -81,63 +185,101 @@ Passing `--mods <dir>` on the command line replaces the user directory with one
|
||||
|
||||
---
|
||||
|
||||
## mod.json
|
||||
## Script Mods
|
||||
|
||||
For simpler use cases where direct game code access and hooks aren't necessary, Luau mods are also supported.
|
||||
Luau mods do not need to be compiled, and are always supported on all platforms. However, they can only use services
|
||||
that are provided as Luau modules.
|
||||
|
||||
Mods can utilize Luau to add UI elements to their mod panel, use configuration variables, and dynamically swap out
|
||||
models/textures at runtime (e.g. to switch model variants or disable certain replacements) while remaining widely
|
||||
compatible.
|
||||
|
||||
Example mod structure:
|
||||
|
||||
```
|
||||
my_luau_mod.dusk
|
||||
├── mod.json
|
||||
└── res/
|
||||
├── main.luau
|
||||
└── lib/util.luau
|
||||
```
|
||||
|
||||
Set the `runtime` to `dev.twilitrealm.luau@1.0` in `mod.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.example.my_mod",
|
||||
"name": "My Mod",
|
||||
"id": "com.example.my_luau_mod",
|
||||
"name": "My Script Mod",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "A short description shown in the mod manager.",
|
||||
"icon": "res/my_icon.png",
|
||||
"banner": "res/my_banner.png"
|
||||
"runtime": "dev.twilitrealm.luau@1.0"
|
||||
}
|
||||
```
|
||||
|
||||
`id` is required: a unique, stable identifier (reverse-DNS style; periods, underscores, and alphanumerics). Everything
|
||||
else is optional but recommended.
|
||||
`res/main.luau` runs once when the mod activates. Register optional update and shutdown callbacks through
|
||||
`dusklight.host`:
|
||||
|
||||
`icon` and `banner` are bundle-relative paths to PNG images for the in-game mod manager: the square icon (e.g.
|
||||
512x512), the banner (~3.5:1). If omitted, `res/icon.png` and `res/banner.png` are used automatically when present.
|
||||
|
||||
---
|
||||
|
||||
## Anatomy of a Code Mod
|
||||
|
||||
```cpp
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/log.h"
|
||||
|
||||
DEFINE_MOD(); // once, in exactly one translation unit
|
||||
IMPORT_SERVICE(LogService, svc_log); // resolved by the loader before mod_initialize
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
svc_log->info(mod_ctx, "hello from my_mod");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError* error) { // called every frame
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError* error) {
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
```lua
|
||||
local host = require("dusklight.host")
|
||||
host.on_update(function()
|
||||
-- Runs every frame
|
||||
end)
|
||||
host.on_shutdown(function()
|
||||
-- Runs on mod deactivation
|
||||
end)
|
||||
```
|
||||
|
||||
All three lifecycle exports are required. `mod_ctx` is your mod's identity token, set by the loader before
|
||||
`mod_initialize` runs. Pass it as the first argument to every service call.
|
||||
Require script modules with `./` or `../` paths relative to the requiring file. The runtime appends `.luau` and rejects
|
||||
paths that escape `res/`.
|
||||
|
||||
The runtime provides these modules:
|
||||
|
||||
- `dusklight.log`: `write`, `trace`, `debug`, `info`, `warn`, and `error`.
|
||||
- `dusklight.host`: host `version`, mod metadata and directories, lifecycle callbacks, and `fail`.
|
||||
- `dusklight.config`: bool, integer, float, and string variables with `get`, `set`, and subscriptions.
|
||||
- `dusklight.resource`: binary-safe reads from the mod's `res/` tree.
|
||||
- `dusklight.overlay`: file and copied-buffer overlays with removable handles.
|
||||
- `dusklight.texture`: encoded-file and raw-data replacements with unregisterable handles.
|
||||
- `dusklight.ui`: Mods panels, controls, lists, windows, dialogs, styles, menu tabs, toasts, and clipboard access.
|
||||
|
||||
For completion and type checking, add `sdk/luau/dusklight.d.luau` to the `luau-lsp.types.definitionFiles` setting.
|
||||
|
||||
Configurable overlay example:
|
||||
|
||||
```lua
|
||||
local config = require("dusklight.config")
|
||||
local overlay = require("dusklight.overlay")
|
||||
|
||||
local hood = config.register({ name = "hood", type = "bool", default = true })
|
||||
local current
|
||||
|
||||
local function apply(enabled)
|
||||
if current then current:remove() end
|
||||
current = overlay.add_file("/res/Object/Alink.arc",
|
||||
enabled and "res/alink_hood.arc" or "res/alink_nohood.arc")
|
||||
end
|
||||
|
||||
hood:subscribe(apply)
|
||||
apply(hood:get())
|
||||
```
|
||||
|
||||
To create a script mod, copy `sdk/luau/template`, edit its manifest and source, then zip the `mod.json` and adjacent
|
||||
folders and rename to `.dusk`. Copy the `.dusk` into the user mods folder:
|
||||
|
||||
- Windows: `%APPDATA%\TwilitRealm\Dusklight\mods`
|
||||
- Linux: `~/.local/share/TwilitRealm/Dusklight/mods`
|
||||
- macOS: `~/Library/Application Support/TwilitRealm/Dusklight/mods`
|
||||
|
||||
**Restrictions:** The Luau VM has no raw filesystem, network, or game-code access. Each script mod has a 64 MiB memory
|
||||
limit. Calls are interrupted after 250 ms for updates and UI callbacks or 5 seconds for lifecycle calls. An uncaught
|
||||
error, timeout, or memory exhaustion fails and disables the mod.
|
||||
|
||||
---
|
||||
|
||||
## Services
|
||||
|
||||
A service is a struct of C function pointers with a version header. You declare what you use at file scope, and the
|
||||
loader resolves it before your mod initializes:
|
||||
A service is a struct of C function pointers with a version header. Import services at file scope, and the loader
|
||||
resolves them before initializing the mod:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(LogService, svc_log); // required, latest minor version
|
||||
@@ -145,7 +287,7 @@ IMPORT_SERVICE_VERSION(LogService, svc_log, 0); // required, minimum minor ver
|
||||
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||
```
|
||||
|
||||
A service must be imported in only **one** file (usually your `mod.cpp`). Other files may simply use `svc_log` or
|
||||
A service should be imported in only **one** file (usually your `mod.cpp`). Other files may simply use `svc_log` or
|
||||
`mods::log::` after including the appropriate header.
|
||||
|
||||
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||
@@ -153,26 +295,19 @@ allowing backwards compatibility with older mods while still changing services f
|
||||
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||
functions to the end of the struct without breaking existing callers and simply bumps the minor version.
|
||||
|
||||
`IMPORT_SERVICE` and `IMPORT_OPTIONAL_SERVICE` require the latest minor version compiled against, making every field in
|
||||
the service safe to call. A mod can use `IMPORT_SERVICE_VERSION` (or its optional counterpart) with an older minor
|
||||
version to remain compatible with older Dusklight versions, then use `SERVICE_HAS` to check at runtime for fields added
|
||||
after that explicitly requested version.
|
||||
|
||||
The contract (see `sdk/include/mods/api.h` for the full version):
|
||||
|
||||
- **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear
|
||||
error. No need to null check at call sites.
|
||||
- **Anything at or below the minor version you imported can be called unconditionally.** The default macros import
|
||||
the service type's current minor version; the versioned macros explicitly override that minimum.
|
||||
- Optional imports may be null; check once in `mod_initialize`.
|
||||
- Fields newer than your imported minor version must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a
|
||||
null check.
|
||||
`IMPORT_SERVICE` and `IMPORT_OPTIONAL_SERVICE` require the latest minor version compiled against, guaranteeing that
|
||||
every function is present. If a mod doesn't use (or may operate without) functions added in later minor versions, and
|
||||
wants to remain compatible with older Dusklight versions, it may use `IMPORT_SERVICE_VERSION` or
|
||||
`IMPORT_OPTIONAL_SERVICE_VERSION` to require an older minor version. It can then check `SERVICE_HAS` at runtime to see
|
||||
if a newer function is present (i.e. running on a new enough Dusklight version).
|
||||
|
||||
---
|
||||
|
||||
## Built-in Services
|
||||
|
||||
### LogService (`mods/svc/log.h`)
|
||||
### LogService ([`mods/svc/log.h`](../sdk/include/mods/svc/log.h))
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
@@ -183,9 +318,16 @@ svc_log->error(mod_ctx, "very bad");
|
||||
svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local log = require("dusklight.log")
|
||||
log.info("spawned the thing")
|
||||
```
|
||||
|
||||
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
|
||||
call returns. C++ mods can enable `add_mod(... FEATURES fmt)` and use the formatted logging helpers in
|
||||
`mods/svc/log.hpp`:
|
||||
[`mods/svc/log.hpp`](../sdk/include/mods/svc/log.hpp):
|
||||
|
||||
```cpp
|
||||
#include <mods/svc/log.hpp>
|
||||
@@ -194,11 +336,13 @@ mods::log::info("spawned actor {} at ({}, {})", actorName, x, y);
|
||||
mods::log::warn("health is down to {:.1f}%", healthPercent);
|
||||
```
|
||||
|
||||
### ResourceService (`mods/svc/resource.h`)
|
||||
### ResourceService ([`mods/svc/resource.h`](../sdk/include/mods/svc/resource.h))
|
||||
|
||||
Loads files from the `res/` tree of your `.dusk` archive. Paths are relative to `res/` (pass `"config.txt"`, not
|
||||
`"res/config.txt"`); absolute paths and `..` are rejected.
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(ResourceService, svc_resource);
|
||||
|
||||
@@ -209,10 +353,17 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
|
||||
}
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local resource = require("dusklight.resource")
|
||||
local contents = resource.load("config.txt")
|
||||
```
|
||||
|
||||
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. The bundle is read-only; use
|
||||
`HostService::data_dir` for persistent storage.
|
||||
|
||||
### FileService (`mods/svc/file.h`)
|
||||
### FileService ([`mods/svc/file.h`](../sdk/include/mods/svc/file.h))
|
||||
|
||||
Provides file and folder pickers, file I/O, exports and folder enumeration.
|
||||
|
||||
@@ -257,9 +408,10 @@ mods::file::export_file(location, "report.txt", [](mods::file::PickResult result
|
||||
`export_file` copies an existing file to a user-selected destination and returns the destination location in its
|
||||
callback. Mod-owned persistent files belong in `HostService::data_dir`.
|
||||
|
||||
### HttpService (`mods/svc/http.h`)
|
||||
### HttpService ([`mods/svc/http.h`](../sdk/include/mods/svc/http.h))
|
||||
|
||||
Asynchronous HTTPS requests supporting HTTP/2 and TLS 1.2+. C++ mods should use the helpers in `mods/svc/http.hpp`:
|
||||
Asynchronous HTTPS requests supporting HTTP/2 and TLS 1.2+. C++ mods should use the helpers in
|
||||
[`mods/svc/http.hpp`](../sdk/include/mods/svc/http.hpp):
|
||||
|
||||
```cpp
|
||||
#include "mods/svc/http.hpp"
|
||||
@@ -299,9 +451,11 @@ 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.
|
||||
|
||||
### HostService (`mods/svc/host.h`)
|
||||
### HostService ([`mods/svc/host.h`](../sdk/include/mods/svc/host.h))
|
||||
|
||||
Mod metadata and runtime interaction with the loader:
|
||||
Mod metadata and runtime interaction with the loader.
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(HostService, svc_host);
|
||||
@@ -319,6 +473,20 @@ if (svc_host->data_dir(mod_ctx, &dataDir) == MOD_OK) {
|
||||
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened");
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local host = require("dusklight.host")
|
||||
local dataDir = host.data_dir()
|
||||
host.on_update(function()
|
||||
-- Runs every frame
|
||||
end)
|
||||
host.on_shutdown(function()
|
||||
-- Runs on mod deactivation
|
||||
end)
|
||||
host.fail("something unrecoverable happened")
|
||||
```
|
||||
|
||||
`get_service`/`publish_service` provide dynamic service lookup; see [Exporting Services](#exporting-services).
|
||||
|
||||
**Lifecycle watches.** If your mod provides a service that hands out per-caller state (registrations, callbacks,
|
||||
@@ -341,17 +509,19 @@ svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch);
|
||||
`MOD_LIFECYCLE_DETACHED` fires on the game thread at a lifecycle safe point, after the subject's `mod_shutdown` ran and
|
||||
every service dropped its state. For your own mod's teardown, use `mod_shutdown` instead.
|
||||
|
||||
### HookService (`mods/svc/hook.h`)
|
||||
### HookService ([`mods/svc/hook.h`](../sdk/include/mods/svc/hook.h))
|
||||
|
||||
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
|
||||
`mods/svc/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
[`mods/svc/hook.hpp`](../sdk/include/mods/svc/hook.hpp) described in
|
||||
[Hooking Game Functions](#hooking-game-functions).
|
||||
|
||||
### OverlayService (`mods/svc/overlay.h`)
|
||||
### OverlayService ([`mods/svc/overlay.h`](../sdk/include/mods/svc/overlay.h))
|
||||
|
||||
Registers DVD file overlays at runtime: the dynamic counterpart to the static `overlay/` directory (see
|
||||
[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, a file within an archive,
|
||||
or with a caller-owned buffer
|
||||
(copied on registration):
|
||||
or with a caller-owned buffer (copied on registration).
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(OverlayService, svc_overlay);
|
||||
@@ -363,6 +533,14 @@ svc_overlay->add_buffer(mod_ctx, "/generated.txt", data, size, nullptr);
|
||||
svc_overlay->remove(mod_ctx, handle);
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local overlay = require("dusklight.overlay")
|
||||
local replacement = overlay.add_file("/Movie/demo_movie98_00.thp", "res/replacement.thp")
|
||||
replacement:remove()
|
||||
```
|
||||
|
||||
`disc_path` must be absolute (leading `/`) and is matched against the disc case-insensitively. Paths that don't exist
|
||||
on the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read
|
||||
stays in memory until the file is re-read: sometimes a scene reload, and in the worst case, a full restart.
|
||||
@@ -372,11 +550,13 @@ data get refreshed without a full restart.
|
||||
|
||||
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
|
||||
|
||||
### TextureService (`mods/svc/texture.h`)
|
||||
### TextureService ([`mods/svc/texture.h`](../sdk/include/mods/svc/texture.h))
|
||||
|
||||
Registers texture replacements at runtime: the dynamic counterpart to the static `textures/` directory (see
|
||||
[Asset Overlays](#asset-overlays)). Two forms: raw texel data with an explicit key, or an encoded `.dds`/`.png` from
|
||||
your bundle whose filename encodes the key:
|
||||
your bundle whose filename encodes the key.
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(TextureService, svc_texture);
|
||||
@@ -397,6 +577,14 @@ svc_texture->register_data(mod_ctx, &key, &data, nullptr);
|
||||
svc_texture->unregister(mod_ctx, handle);
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local texture = require("dusklight.texture")
|
||||
local replacement = texture.register_file("res/tex1_32x32_$_6.png")
|
||||
replacement:unregister()
|
||||
```
|
||||
|
||||
Filenames use the same Dolphin-style convention as the user's `texture_replacements` directory:
|
||||
`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, where hashes may be `$` (wildcard). `_mipN` sidecar files next to
|
||||
a registered file are picked up automatically. Files are decoded lazily on first use by the renderer; raw data is copied
|
||||
@@ -404,11 +592,13 @@ at registration. Registrations follow your mod's lifecycle.
|
||||
|
||||
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
|
||||
|
||||
### ConfigService (`mods/svc/config.h`)
|
||||
### ConfigService ([`mods/svc/config.h`](../sdk/include/mods/svc/config.h))
|
||||
|
||||
Persistent, mod-scoped configuration variables. Each var is stored in the user's `config.json` under
|
||||
`mod.<escaped mod id>.<name>` (escaping: `.` → `_`, `_` → `__`, so `com.example.my_mod` becomes `com_example_my__mod`),
|
||||
next to the host's own settings:
|
||||
next to the host's own settings.
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(ConfigService, svc_config);
|
||||
@@ -432,6 +622,17 @@ void on_speed_changed(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue
|
||||
svc_config->subscribe(mod_ctx, var, on_speed_changed, nullptr, nullptr);
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local config = require("dusklight.config")
|
||||
local speed = config.register({ name = "speedMultiplier", type = "float", default = 1.0 })
|
||||
speed:subscribe(function(value, previous)
|
||||
-- React to the new value.
|
||||
end)
|
||||
speed:set(2.0)
|
||||
```
|
||||
|
||||
Types: `CONFIG_VAR_BOOL` (`bool`), `CONFIG_VAR_INT` (`int64_t`), `CONFIG_VAR_FLOAT` (`double`), `CONFIG_VAR_STRING`
|
||||
(UTF-8; `get_string` copies into a caller buffer, pass a `NULL` buffer with size 0 to query the length). Accessors are
|
||||
typed and must match the registration.
|
||||
@@ -440,7 +641,7 @@ Change callbacks fire on the game thread whenever the value changes at runtime (
|
||||
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
|
||||
**not** fire callbacks; read the value after `register_var` for the starting state.
|
||||
|
||||
### SaveService (`mods/svc/save.h`)
|
||||
### SaveService ([`mods/svc/save.h`](../sdk/include/mods/svc/save.h))
|
||||
|
||||
Stores named binary blobs for each save slot. Blob names are scoped to the calling mod, and each mod may store up to
|
||||
`SAVE_BLOB_BUDGET_BYTES` per slot. The service copies data passed to `set_blob`.
|
||||
@@ -473,7 +674,7 @@ buffer contract as `get_blob`. Pass a `NULL` buffer to either read function to q
|
||||
are cleared. Observers are removed automatically when the mod is detached, so the output handle is only needed for
|
||||
manual unregistration. Save callbacks run on the game thread.
|
||||
|
||||
### StageService (`mods/svc/stage.h`)
|
||||
### StageService ([`mods/svc/stage.h`](../sdk/include/mods/svc/stage.h))
|
||||
|
||||
Allows making changes to a stage's "stage info" (contents of .dzs/.dzr files).
|
||||
(Currently only supports editing actor nodes.)
|
||||
@@ -522,7 +723,7 @@ Stage names may contain up to 8 characters. For patches and deletions, room `0xf
|
||||
layer; additions require a specific room. Edits are removed when the mod is detached. If multiple mods edit the same
|
||||
record, the later-loaded mod wins.
|
||||
|
||||
### UiService (`mods/svc/ui.h`)
|
||||
### UiService ([`mods/svc/ui.h`](../sdk/include/mods/svc/ui.h))
|
||||
|
||||
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
|
||||
create custom windows and modal dialogs, apply custom RCSS stylesheets (anywhere!), and add menu bar tabs.
|
||||
@@ -532,6 +733,8 @@ content is rebuilt, and `update` runs every frame while that mod is selected. Wh
|
||||
pane carries your mod's id as a `mod-id` attribute (like custom window roots), so scoped RCSS can target it (e.g.
|
||||
`[mod-id="com.example.mod"]`).
|
||||
|
||||
**C++**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
|
||||
@@ -555,6 +758,18 @@ panel.update = update;
|
||||
svc_ui->register_mods_panel(mod_ctx, &panel);
|
||||
```
|
||||
|
||||
**Luau**
|
||||
|
||||
```lua
|
||||
local ui = require("dusklight.ui")
|
||||
ui.register_mods_panel({
|
||||
build = function(panel)
|
||||
panel:add_section("Status")
|
||||
panel:add_text("running")
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
Element setters must match the element kind: `elem_set_text`/`elem_set_rml` on text rows, and `elem_set_progress` on
|
||||
progress bars. `elem_set_class` sets or clears an RCSS class on any element handle, for styling via scoped or
|
||||
per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as do exceptions thrown from any UI
|
||||
@@ -704,7 +919,7 @@ existing documents restyle immediately, and future ones pick it up when created.
|
||||
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||
unless changing host UI is intentional.
|
||||
|
||||
### WindowService (`mods/svc/window.h`)
|
||||
### WindowService ([`mods/svc/window.h`](../sdk/include/mods/svc/window.h))
|
||||
|
||||
Allows creating new windows that can be rendered to via `GfxService`.
|
||||
|
||||
@@ -724,12 +939,13 @@ one present target may be attached to a WindowService window at a time.
|
||||
|
||||
New windows are hidden by default so a mod can finish attaching graphics before calling `show_window`.
|
||||
|
||||
### GfxService (`mods/svc/gfx.h`)
|
||||
### GfxService ([`mods/svc/gfx.h`](../sdk/include/mods/svc/gfx.h))
|
||||
|
||||
**Requires `add_mod(... FEATURES webgpu)`**
|
||||
|
||||
Direct WebGPU access at various stages of the rendering pipeline. Mods use the `wgpu*` C API (via `webgpu/webgpu.h`) for
|
||||
custom draws and compute dispatches. Mods must manage their own WebGPU state, including pipelines and bind groups.
|
||||
Direct WebGPU access at various stages of the rendering pipeline. Mods use the `wgpu*` C API (via
|
||||
[`webgpu/webgpu.h`](https://github.com/webgpu-native/webgpu-headers/blob/main/webgpu.h)) for custom draws and compute
|
||||
dispatches. Mods must manage their own WebGPU state, including pipelines and bind groups.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
@@ -779,7 +995,7 @@ To create a `WGPUSurface` manually, `GfxDeviceInfo` holds the `WGPUInstance` and
|
||||
`push_present` must be called every frame from a GfxService stage callback. If surface was lost, `push_present` returns
|
||||
`MOD_ERROR`. Unregister and re-register the target before trying again.
|
||||
|
||||
### CameraService (`mods/svc/camera.h`)
|
||||
### CameraService ([`mods/svc/camera.h`](../sdk/include/mods/svc/camera.h))
|
||||
|
||||
Converts a game view provided by a render callback into WebGPU-convention camera data. Matrix fields are column-major
|
||||
`float[16]` values using the matrix * column-vector convention (transpose of the game's row-major `Mtx`/`Mtx44` layout),
|
||||
@@ -801,7 +1017,7 @@ first in-game frame. Projection matrices match the renderer's WebGPU clip conven
|
||||
Camera operators allow overriding the main camera. When an operator callback returns true, its values replace the camera
|
||||
state for the current frame. Register and unregister using `register_camera_operator` / `unregister_camera_operator`.
|
||||
|
||||
### GameModeService (`mods/svc/game_mode.h`)
|
||||
### GameModeService ([`mods/svc/game_mode.h`](../sdk/include/mods/svc/game_mode.h))
|
||||
|
||||
Allows a mod to register a game mode with callbacks for key gameplay and save lifecycle events. Registered game modes
|
||||
appear in the prelaunch menu. Game modes may use a unique set of saves by configuring `save_name`; leave it empty to use
|
||||
@@ -927,7 +1143,7 @@ svc_game_mode->register_game_mode(mod_ctx, &gameModeDesc);
|
||||
**Requires `add_mod(... FEATURES game)`**
|
||||
|
||||
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
|
||||
`mods/svc/hook.hpp` provides typed helpers over the hook service:
|
||||
[`mods/svc/hook.hpp`](../sdk/include/mods/svc/hook.hpp) provides typed helpers over the hook service:
|
||||
|
||||
```cpp
|
||||
#include "mods/svc/hook.hpp"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(luau_runtime CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
set(LUAU_BUILD_CLI OFF CACHE BOOL "" FORCE)
|
||||
set(LUAU_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(LUAU_BUILD_WEB OFF CACHE BOOL "" FORCE)
|
||||
set(LUAU_WERROR OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(luau
|
||||
URL https://github.com/luau-lang/luau/archive/refs/tags/0.734.tar.gz
|
||||
URL_HASH SHA256=cb55a891226d8c70284e22eb9281cc2b4496c709a4050f52aaa18a355fe7b1a3
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(luau)
|
||||
|
||||
add_mod(luau_runtime
|
||||
SOURCES
|
||||
src/bindings.cpp
|
||||
src/config.cpp
|
||||
src/runtime.cpp
|
||||
src/ui.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
|
||||
target_link_libraries(luau_runtime PRIVATE Luau.VM Luau.Compiler)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.luau",
|
||||
"name": "Luau Support",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Luau script runtime for mods"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2025 Roblox Corporation
|
||||
Copyright (c) 1994-2019 Lua.org, PUC-Rio.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace luau_runtime {
|
||||
namespace {
|
||||
|
||||
constexpr char kOverlayMetatable[] = "dusklight.overlay_handle";
|
||||
constexpr char kTextureMetatable[] = "dusklight.texture_handle";
|
||||
|
||||
uint64_t get_hash_field(lua_State* state, int table, const char* field, bool required) {
|
||||
lua_getfield(state, table, field);
|
||||
if (lua_isnil(state, -1)) {
|
||||
lua_pop(state, 1);
|
||||
if (required) {
|
||||
luaL_error(state, "field '%s' is required", field);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (!lua_isinteger64(state, -1)) {
|
||||
luaL_error(state, "field '%s' must be an integer", field);
|
||||
}
|
||||
const uint64_t value = static_cast<uint64_t>(lua_tointeger64(state, -1, nullptr));
|
||||
lua_pop(state, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
int log_write(lua_State* state, LogLevel level, int messageIndex) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_log == nullptr) {
|
||||
service_unavailable(state, "LogService");
|
||||
}
|
||||
size_t length = 0;
|
||||
const char* message = luaL_checklstring(state, messageIndex, &length);
|
||||
const std::string copy{message, length};
|
||||
svc_log->write(vm.subject, level, copy.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int log_trace(lua_State* state) {
|
||||
return log_write(state, LOG_LEVEL_TRACE, 1);
|
||||
}
|
||||
|
||||
int log_debug(lua_State* state) {
|
||||
return log_write(state, LOG_LEVEL_DEBUG, 1);
|
||||
}
|
||||
|
||||
int log_info(lua_State* state) {
|
||||
return log_write(state, LOG_LEVEL_INFO, 1);
|
||||
}
|
||||
|
||||
int log_warn(lua_State* state) {
|
||||
return log_write(state, LOG_LEVEL_WARN, 1);
|
||||
}
|
||||
|
||||
int log_error(lua_State* state) {
|
||||
return log_write(state, LOG_LEVEL_ERROR, 1);
|
||||
}
|
||||
|
||||
int log_write_level(lua_State* state) {
|
||||
static constexpr const char* kLevels[] = {"trace", "debug", "info", "warn", "error", nullptr};
|
||||
const int level = luaL_checkoption(state, 1, nullptr, kLevels);
|
||||
return log_write(state, static_cast<LogLevel>(level), 2);
|
||||
}
|
||||
|
||||
int host_mod_id(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr) {
|
||||
service_unavailable(state, "HostService");
|
||||
}
|
||||
lua_pushstring(state, svc_host->mod_id(vm.subject));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int host_mod_name(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr) {
|
||||
service_unavailable(state, "HostService");
|
||||
}
|
||||
lua_pushstring(state, svc_host->mod_name(vm.subject));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int host_mod_version(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr) {
|
||||
service_unavailable(state, "HostService");
|
||||
}
|
||||
lua_pushstring(state, svc_host->mod_version(vm.subject));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int host_mod_dir(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr) {
|
||||
service_unavailable(state, "HostService");
|
||||
}
|
||||
lua_pushstring(state, svc_host->mod_dir(vm.subject));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int host_data_dir(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr || !SERVICE_HAS(svc_host, HostService, data_dir) ||
|
||||
svc_host->data_dir == nullptr)
|
||||
{
|
||||
service_unavailable(state, "HostService::data_dir");
|
||||
}
|
||||
const char* path = nullptr;
|
||||
check_result(state, svc_host->data_dir(vm.subject, &path), "host.data_dir");
|
||||
lua_pushstring(state, path != nullptr ? path : "");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int host_fail(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr) {
|
||||
service_unavailable(state, "HostService");
|
||||
}
|
||||
const char* message = luaL_checkstring(state, 1);
|
||||
svc_host->fail(vm.subject, MOD_ERROR, message);
|
||||
luaL_error(state, "%s", message);
|
||||
}
|
||||
|
||||
int register_host_callback(lua_State* state, std::vector<int>& callbacks) {
|
||||
luaL_checktype(state, 1, LUA_TFUNCTION);
|
||||
lua_pushvalue(state, 1);
|
||||
callbacks.push_back(lua_ref(state, -1));
|
||||
lua_pop(state, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int host_on_update(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
return register_host_callback(state, vm.updateRefs);
|
||||
}
|
||||
|
||||
int host_on_shutdown(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
return register_host_callback(state, vm.shutdownRefs);
|
||||
}
|
||||
|
||||
int resource_load(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_resource == nullptr) {
|
||||
service_unavailable(state, "ResourceService");
|
||||
}
|
||||
const char* path = luaL_checkstring(state, 1);
|
||||
ResourceBuffer buffer = RESOURCE_BUFFER_INIT;
|
||||
check_result(state, svc_resource->load(vm.subject, path, &buffer), "resource.load");
|
||||
const auto* data = buffer.data != nullptr ? static_cast<const char*>(buffer.data) : "";
|
||||
const std::string copy{data, buffer.size};
|
||||
svc_resource->free(vm.subject, &buffer);
|
||||
lua_pushlstring(state, copy.data(), copy.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int overlay_remove(lua_State* state) {
|
||||
auto& handle = check_handle(state, 1, kOverlayMetatable, HandleKind::Overlay);
|
||||
if (svc_overlay == nullptr) {
|
||||
service_unavailable(state, "OverlayService");
|
||||
}
|
||||
check_result(state, svc_overlay->remove(handle.vm->subject, handle.value), "overlay.remove");
|
||||
handle.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int overlay_add_file(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_overlay == nullptr) {
|
||||
service_unavailable(state, "OverlayService");
|
||||
}
|
||||
const char* discPath = luaL_checkstring(state, 1);
|
||||
const char* bundlePath = luaL_checkstring(state, 2);
|
||||
OverlayHandle handle = 0;
|
||||
check_result(state, svc_overlay->add_file(vm.subject, discPath, bundlePath, &handle),
|
||||
"overlay.add_file");
|
||||
push_handle(state, vm, handle, HandleKind::Overlay, kOverlayMetatable);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int overlay_add_buffer(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_overlay == nullptr) {
|
||||
service_unavailable(state, "OverlayService");
|
||||
}
|
||||
const char* discPath = luaL_checkstring(state, 1);
|
||||
size_t size = 0;
|
||||
const char* data = luaL_checklstring(state, 2, &size);
|
||||
OverlayHandle handle = 0;
|
||||
check_result(state, svc_overlay->add_buffer(vm.subject, discPath, data, size, &handle),
|
||||
"overlay.add_buffer");
|
||||
push_handle(state, vm, handle, HandleKind::Overlay, kOverlayMetatable);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int texture_unregister(lua_State* state) {
|
||||
auto& handle = check_handle(state, 1, kTextureMetatable, HandleKind::Texture);
|
||||
if (svc_texture == nullptr) {
|
||||
service_unavailable(state, "TextureService");
|
||||
}
|
||||
check_result(
|
||||
state, svc_texture->unregister(handle.vm->subject, handle.value), "texture.unregister");
|
||||
handle.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int texture_register_file(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_texture == nullptr) {
|
||||
service_unavailable(state, "TextureService");
|
||||
}
|
||||
const char* path = luaL_checkstring(state, 1);
|
||||
TextureReplacementHandle handle = 0;
|
||||
check_result(
|
||||
state, svc_texture->register_file(vm.subject, path, &handle), "texture.register_file");
|
||||
push_handle(state, vm, handle, HandleKind::Texture, kTextureMetatable);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int texture_register_data(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_texture == nullptr) {
|
||||
service_unavailable(state, "TextureService");
|
||||
}
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
luaL_checktype(state, 2, LUA_TTABLE);
|
||||
|
||||
TextureKey key = TEXTURE_KEY_INIT;
|
||||
key.kind = TEXTURE_KEY_SOURCE;
|
||||
key.texture_hash = get_hash_field(state, 1, "texture_hash", true);
|
||||
key.tlut_hash = get_hash_field(state, 1, "tlut_hash", false);
|
||||
key.width = static_cast<uint32_t>(get_optional_int(state, 1, "width", 0));
|
||||
key.height = static_cast<uint32_t>(get_optional_int(state, 1, "height", 0));
|
||||
key.gx_format = static_cast<uint32_t>(get_optional_int(state, 1, "gx_format", 0));
|
||||
key.has_tlut = get_optional_bool(state, 1, "has_tlut", false);
|
||||
|
||||
lua_getfield(state, 2, "data");
|
||||
size_t size = 0;
|
||||
const char* bytes = luaL_checklstring(state, -1, &size);
|
||||
TextureData data = TEXTURE_DATA_INIT;
|
||||
data.data = bytes;
|
||||
data.size = size;
|
||||
data.width = static_cast<uint32_t>(get_optional_int(state, 2, "width", key.width));
|
||||
data.height = static_cast<uint32_t>(get_optional_int(state, 2, "height", key.height));
|
||||
data.mip_count = static_cast<uint32_t>(get_optional_int(state, 2, "mip_count", 1));
|
||||
data.gx_format = static_cast<uint32_t>(get_optional_int(state, 2, "gx_format", key.gx_format));
|
||||
|
||||
TextureReplacementHandle handle = 0;
|
||||
const ModResult result = svc_texture->register_data(vm.subject, &key, &data, &handle);
|
||||
lua_pop(state, 1);
|
||||
check_result(state, result, "texture.register_data");
|
||||
push_handle(state, vm, handle, HandleKind::Texture, kTextureMetatable);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int open_log(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_log == nullptr) {
|
||||
service_unavailable(state, "LogService");
|
||||
}
|
||||
lua_newtable(state);
|
||||
set_function(state, vm, "write", log_write_level);
|
||||
set_function(state, vm, "trace", log_trace);
|
||||
set_function(state, vm, "debug", log_debug);
|
||||
set_function(state, vm, "info", log_info);
|
||||
set_function(state, vm, "warn", log_warn);
|
||||
set_function(state, vm, "error", log_error);
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int open_host(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_host == nullptr) {
|
||||
service_unavailable(state, "HostService");
|
||||
}
|
||||
lua_newtable(state);
|
||||
lua_pushstring(state, svc_host->version != nullptr ? svc_host->version : "");
|
||||
lua_setfield(state, -2, "version");
|
||||
set_function(state, vm, "mod_id", host_mod_id);
|
||||
set_function(state, vm, "mod_name", host_mod_name);
|
||||
set_function(state, vm, "mod_version", host_mod_version);
|
||||
set_function(state, vm, "mod_dir", host_mod_dir);
|
||||
set_function(state, vm, "data_dir", host_data_dir);
|
||||
set_function(state, vm, "on_update", host_on_update);
|
||||
set_function(state, vm, "on_shutdown", host_on_shutdown);
|
||||
set_function(state, vm, "fail", host_fail);
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int open_resource(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_resource == nullptr) {
|
||||
service_unavailable(state, "ResourceService");
|
||||
}
|
||||
lua_newtable(state);
|
||||
set_function(state, vm, "load", resource_load);
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int open_overlay(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_overlay == nullptr) {
|
||||
service_unavailable(state, "OverlayService");
|
||||
}
|
||||
static const luaL_Reg kMethods[] = {{"remove", overlay_remove}, {nullptr, nullptr}};
|
||||
create_handle_metatable(state, kOverlayMetatable, kMethods, "OverlayHandle");
|
||||
lua_newtable(state);
|
||||
set_function(state, vm, "add_file", overlay_add_file);
|
||||
set_function(state, vm, "add_buffer", overlay_add_buffer);
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int open_texture(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_texture == nullptr) {
|
||||
service_unavailable(state, "TextureService");
|
||||
}
|
||||
static const luaL_Reg kMethods[] = {{"unregister", texture_unregister}, {nullptr, nullptr}};
|
||||
create_handle_metatable(state, kTextureMetatable, kMethods, "TextureHandle");
|
||||
lua_newtable(state);
|
||||
set_function(state, vm, "register_file", texture_register_file);
|
||||
set_function(state, vm, "register_data", texture_register_data);
|
||||
lua_pushinteger64(state, static_cast<int64_t>(TEXTURE_HASH_WILDCARD));
|
||||
lua_setfield(state, -2, "hash_wildcard");
|
||||
lua_pushinteger64(state, static_cast<int64_t>(TEXTURE_TLUT_WILDCARD));
|
||||
lua_setfield(state, -2, "tlut_wildcard");
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace luau_runtime
|
||||
@@ -0,0 +1,243 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace luau_runtime {
|
||||
namespace {
|
||||
|
||||
constexpr char kConfigVarMetatable[] = "dusklight.config_var";
|
||||
constexpr char kConfigSubscriptionMetatable[] = "dusklight.config_subscription";
|
||||
|
||||
void config_changed(ModContext*, ConfigVarHandle, const ConfigVarValue* value,
|
||||
const ConfigVarValue* previous, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
if (value == nullptr || previous == nullptr) {
|
||||
return;
|
||||
}
|
||||
push_config_value(callback.vm->state, *value);
|
||||
push_config_value(callback.vm->state, *previous);
|
||||
std::string error;
|
||||
if (!call_ref(*callback.vm, callback.refs[0], 2, 0, kCallbackBudget, error)) {
|
||||
fail_callback(*callback.vm, "config change callback", error);
|
||||
}
|
||||
}
|
||||
|
||||
int config_get(lua_State* state) {
|
||||
auto& handle = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
switch (handle.configType) {
|
||||
case CONFIG_VAR_BOOL: {
|
||||
bool value = false;
|
||||
check_result(
|
||||
state, svc_config->get_bool(handle.vm->subject, handle.value, &value), "config get");
|
||||
lua_pushboolean(state, value);
|
||||
break;
|
||||
}
|
||||
case CONFIG_VAR_INT: {
|
||||
int64_t value = 0;
|
||||
check_result(
|
||||
state, svc_config->get_int(handle.vm->subject, handle.value, &value), "config get");
|
||||
lua_pushinteger64(state, value);
|
||||
break;
|
||||
}
|
||||
case CONFIG_VAR_FLOAT: {
|
||||
double value = 0;
|
||||
check_result(
|
||||
state, svc_config->get_float(handle.vm->subject, handle.value, &value), "config get");
|
||||
lua_pushnumber(state, value);
|
||||
break;
|
||||
}
|
||||
case CONFIG_VAR_STRING: {
|
||||
size_t size = 0;
|
||||
check_result(state,
|
||||
svc_config->get_string(handle.vm->subject, handle.value, nullptr, 0, &size),
|
||||
"config get");
|
||||
std::vector<char> value(size + 1);
|
||||
check_result(state,
|
||||
svc_config->get_string(
|
||||
handle.vm->subject, handle.value, value.data(), value.size(), nullptr),
|
||||
"config get");
|
||||
lua_pushlstring(state, value.data(), size);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
luaL_error(state, "unknown config value type");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int config_set(lua_State* state) {
|
||||
auto& handle = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
ModResult result = MOD_INVALID_ARGUMENT;
|
||||
switch (handle.configType) {
|
||||
case CONFIG_VAR_BOOL:
|
||||
result = svc_config->set_bool(
|
||||
handle.vm->subject, handle.value, luaL_checkboolean(state, 2) != 0);
|
||||
break;
|
||||
case CONFIG_VAR_INT:
|
||||
result = svc_config->set_int(handle.vm->subject, handle.value, check_int64(state, 2));
|
||||
break;
|
||||
case CONFIG_VAR_FLOAT:
|
||||
result =
|
||||
svc_config->set_float(handle.vm->subject, handle.value, luaL_checknumber(state, 2));
|
||||
break;
|
||||
case CONFIG_VAR_STRING:
|
||||
result =
|
||||
svc_config->set_string(handle.vm->subject, handle.value, luaL_checkstring(state, 2));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
check_result(state, result, "config set");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_unregister(lua_State* state) {
|
||||
auto& handle = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
check_result(
|
||||
state, svc_config->unregister_var(handle.vm->subject, handle.value), "config unregister");
|
||||
handle.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_unsubscribe(lua_State* state) {
|
||||
auto& handle =
|
||||
check_handle(state, 1, kConfigSubscriptionMetatable, HandleKind::ConfigSubscription);
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
check_result(
|
||||
state, svc_config->unsubscribe(handle.vm->subject, handle.value), "config unsubscribe");
|
||||
handle.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int subscribe(lua_State* state, ScriptHandle& variable, int functionIndex) {
|
||||
luaL_checktype(state, functionIndex, LUA_TFUNCTION);
|
||||
Callback& callback = retain_callback(*variable.vm);
|
||||
callback.refs[0] = lua_ref(state, functionIndex);
|
||||
|
||||
ConfigSubscriptionHandle subscription = 0;
|
||||
check_result(state,
|
||||
svc_config->subscribe(
|
||||
variable.vm->subject, variable.value, config_changed, &callback, &subscription),
|
||||
"config.subscribe");
|
||||
push_handle(state, *variable.vm, subscription, HandleKind::ConfigSubscription,
|
||||
kConfigSubscriptionMetatable);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int config_subscribe(lua_State* state) {
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
auto& variable = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
|
||||
return subscribe(state, variable, 2);
|
||||
}
|
||||
|
||||
int config_var_subscribe(lua_State* state) {
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
auto& variable = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
|
||||
return subscribe(state, variable, 2);
|
||||
}
|
||||
|
||||
int config_register(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
|
||||
const std::string name = get_optional_string(state, 1, "name");
|
||||
const std::string type = get_optional_string(state, 1, "type");
|
||||
ConfigVarDesc desc = CONFIG_VAR_DESC_INIT;
|
||||
desc.name = name.c_str();
|
||||
if (type == "bool") {
|
||||
desc.type = CONFIG_VAR_BOOL;
|
||||
desc.default_bool = get_optional_bool(state, 1, "default", false);
|
||||
} else if (type == "int") {
|
||||
desc.type = CONFIG_VAR_INT;
|
||||
desc.default_int = get_optional_int(state, 1, "default", 0);
|
||||
} else if (type == "float") {
|
||||
desc.type = CONFIG_VAR_FLOAT;
|
||||
desc.default_float = get_optional_number(state, 1, "default", 0);
|
||||
} else if (type == "string") {
|
||||
desc.type = CONFIG_VAR_STRING;
|
||||
} else {
|
||||
luaL_error(state, "config type must be 'bool', 'int', 'float', or 'string'");
|
||||
}
|
||||
|
||||
std::string defaultString;
|
||||
if (desc.type == CONFIG_VAR_STRING) {
|
||||
defaultString = get_optional_string(state, 1, "default");
|
||||
desc.default_string = defaultString.c_str();
|
||||
}
|
||||
|
||||
ConfigVarHandle handle = 0;
|
||||
check_result(state, svc_config->register_var(vm.subject, &desc, &handle), "config.register");
|
||||
push_handle(state, vm, handle, HandleKind::ConfigVar, kConfigVarMetatable, desc.type);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void push_config_value(lua_State* state, const ConfigVarValue& value) {
|
||||
switch (value.type) {
|
||||
case CONFIG_VAR_BOOL:
|
||||
lua_pushboolean(state, value.bool_value);
|
||||
break;
|
||||
case CONFIG_VAR_INT:
|
||||
lua_pushinteger64(state, value.int_value);
|
||||
break;
|
||||
case CONFIG_VAR_FLOAT:
|
||||
lua_pushnumber(state, value.float_value);
|
||||
break;
|
||||
case CONFIG_VAR_STRING:
|
||||
lua_pushlstring(state, value.string_value != nullptr ? value.string_value : "",
|
||||
value.string_value != nullptr ? value.string_length : 0);
|
||||
break;
|
||||
default:
|
||||
lua_pushnil(state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int open_config(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_config == nullptr) {
|
||||
service_unavailable(state, "ConfigService");
|
||||
}
|
||||
static const luaL_Reg kVarMethods[] = {
|
||||
{"get", config_get},
|
||||
{"set", config_set},
|
||||
{"subscribe", config_var_subscribe},
|
||||
{"unregister", config_unregister},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
static const luaL_Reg kSubscriptionMethods[] = {
|
||||
{"unsubscribe", config_unsubscribe},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
create_handle_metatable(state, kConfigVarMetatable, kVarMethods, "ConfigVar");
|
||||
create_handle_metatable(
|
||||
state, kConfigSubscriptionMetatable, kSubscriptionMethods, "ConfigSubscription");
|
||||
|
||||
lua_newtable(state);
|
||||
set_function(state, vm, "register", config_register);
|
||||
set_function(state, vm, "subscribe", config_subscribe);
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace luau_runtime
|
||||
@@ -0,0 +1,611 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include "Luau/Common.h"
|
||||
#include "luacode.h"
|
||||
#include "mods/runtime.h"
|
||||
#include "mods/service.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
LUAU_FASTFLAG(LuauIntegerLibrary)
|
||||
LUAU_FASTFLAG(LuauIntegerType2)
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_OPTIONAL_SERVICE(LogService, svc_log);
|
||||
IMPORT_OPTIONAL_SERVICE(HostService, svc_host);
|
||||
IMPORT_OPTIONAL_SERVICE(ConfigService, svc_config);
|
||||
IMPORT_OPTIONAL_SERVICE(ResourceService, svc_resource);
|
||||
IMPORT_OPTIONAL_SERVICE(OverlayService, svc_overlay);
|
||||
IMPORT_OPTIONAL_SERVICE(TextureService, svc_texture);
|
||||
IMPORT_OPTIONAL_SERVICE(UiService, svc_ui);
|
||||
|
||||
namespace luau_runtime {
|
||||
namespace {
|
||||
|
||||
std::unordered_map<ModContext*, std::unique_ptr<Vm>> s_vms;
|
||||
|
||||
void* limited_realloc(void* userData, void* pointer, size_t oldSize, size_t newSize) {
|
||||
auto& budget = *static_cast<MemoryBudget*>(userData);
|
||||
if (newSize == 0) {
|
||||
std::free(pointer);
|
||||
budget.used -= std::min(budget.used, oldSize);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const size_t growth = newSize > oldSize ? newSize - oldSize : 0;
|
||||
if (growth > budget.limit - std::min(budget.used, budget.limit)) {
|
||||
return nullptr;
|
||||
}
|
||||
void* result = std::realloc(pointer, newSize);
|
||||
if (result != nullptr) {
|
||||
budget.used -= std::min(budget.used, oldSize);
|
||||
budget.used += newSize;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int traceback_handler(lua_State* state) {
|
||||
const char* message = lua_tostring(state, 1);
|
||||
luaL_traceback(state, state, message != nullptr ? message : "Luau error", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
class DeadlineScope {
|
||||
public:
|
||||
DeadlineScope(Vm& vm, std::chrono::steady_clock::duration budget)
|
||||
: m_vm{vm}, m_previousDeadline{vm.deadline}, m_previousActive{vm.deadlineActive} {
|
||||
const auto requested = std::chrono::steady_clock::now() + budget;
|
||||
if (!vm.deadlineActive || requested < vm.deadline) {
|
||||
vm.deadline = requested;
|
||||
}
|
||||
vm.deadlineActive = true;
|
||||
++vm.callDepth;
|
||||
}
|
||||
|
||||
~DeadlineScope() {
|
||||
--m_vm.callDepth;
|
||||
m_vm.deadline = m_previousDeadline;
|
||||
m_vm.deadlineActive = m_previousActive;
|
||||
}
|
||||
|
||||
private:
|
||||
Vm& m_vm;
|
||||
std::chrono::steady_clock::time_point m_previousDeadline;
|
||||
bool m_previousActive;
|
||||
};
|
||||
|
||||
bool protected_call(lua_State* state, Vm& vm, int argumentCount, int resultCount,
|
||||
std::chrono::steady_clock::duration budget, std::string& outError) {
|
||||
const int functionIndex = lua_gettop(state) - argumentCount;
|
||||
lua_pushcfunction(state, traceback_handler, "dusklight traceback");
|
||||
lua_insert(state, functionIndex);
|
||||
|
||||
DeadlineScope deadline{vm, budget};
|
||||
const int status = lua_pcall(state, argumentCount, resultCount, functionIndex);
|
||||
if (status != LUA_OK) {
|
||||
const char* message = lua_tostring(state, -1);
|
||||
outError = message != nullptr ? message : "unknown Luau error";
|
||||
lua_pop(state, 1);
|
||||
lua_remove(state, functionIndex);
|
||||
return false;
|
||||
}
|
||||
lua_remove(state, functionIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> normalize_module_path(
|
||||
std::string_view currentPath, std::string_view requested) {
|
||||
if ((!requested.starts_with("./") && !requested.starts_with("../")) ||
|
||||
requested.find('\\') != std::string_view::npos)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> parts;
|
||||
const auto parentEnd = currentPath.rfind('/');
|
||||
std::string combined;
|
||||
if (parentEnd != std::string_view::npos) {
|
||||
combined.assign(currentPath.substr(0, parentEnd + 1));
|
||||
}
|
||||
combined.append(requested);
|
||||
|
||||
size_t begin = 0;
|
||||
while (begin <= combined.size()) {
|
||||
const size_t end = combined.find('/', begin);
|
||||
const auto part = std::string_view{combined}.substr(
|
||||
begin, end == std::string::npos ? combined.size() - begin : end - begin);
|
||||
if (part.empty() || part == ".") {
|
||||
} else if (part == "..") {
|
||||
if (parts.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
parts.pop_back();
|
||||
} else {
|
||||
parts.push_back(part);
|
||||
}
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
begin = end + 1;
|
||||
}
|
||||
if (parts.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string normalized;
|
||||
for (const auto part : parts) {
|
||||
if (!normalized.empty()) {
|
||||
normalized.push_back('/');
|
||||
}
|
||||
normalized.append(part);
|
||||
}
|
||||
if (!normalized.ends_with(".luau")) {
|
||||
normalized += ".luau";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
ModuleOpenFn module_factory(std::string_view name) {
|
||||
if (name == "dusklight.log") {
|
||||
return open_log;
|
||||
}
|
||||
if (name == "dusklight.host") {
|
||||
return open_host;
|
||||
}
|
||||
if (name == "dusklight.resource") {
|
||||
return open_resource;
|
||||
}
|
||||
if (name == "dusklight.overlay") {
|
||||
return open_overlay;
|
||||
}
|
||||
if (name == "dusklight.texture") {
|
||||
return open_texture;
|
||||
}
|
||||
if (name == "dusklight.config") {
|
||||
return open_config;
|
||||
}
|
||||
if (name == "dusklight.ui") {
|
||||
return open_ui;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int module_require(lua_State* state);
|
||||
|
||||
void install_module_require(lua_State* state, Vm& vm, std::string_view currentPath) {
|
||||
lua_pushlightuserdata(state, &vm);
|
||||
lua_pushlstring(state, currentPath.data(), currentPath.size());
|
||||
lua_pushcclosure(state, module_require, "require", 2);
|
||||
lua_setglobal(state, "require");
|
||||
}
|
||||
|
||||
bool load_source_module(
|
||||
lua_State* caller, Vm& vm, const std::string& path, bool keepResult, std::string& outError) {
|
||||
if (svc_resource == nullptr) {
|
||||
outError = "ResourceService is not available in this Dusklight build";
|
||||
return false;
|
||||
}
|
||||
|
||||
ResourceBuffer buffer = RESOURCE_BUFFER_INIT;
|
||||
const ModResult loadResult = svc_resource->load(vm.subject, path.c_str(), &buffer);
|
||||
if (loadResult != MOD_OK) {
|
||||
outError = loadResult == MOD_UNAVAILABLE ? "module not found: res/" + path :
|
||||
"failed to load module: res/" + path;
|
||||
return false;
|
||||
}
|
||||
std::string source;
|
||||
if (buffer.data != nullptr) {
|
||||
source.assign(static_cast<const char*>(buffer.data), buffer.size);
|
||||
}
|
||||
svc_resource->free(vm.subject, &buffer);
|
||||
|
||||
size_t bytecodeSize = 0;
|
||||
lua_CompileOptions options{};
|
||||
options.optimizationLevel = 1;
|
||||
options.debugLevel = 1;
|
||||
char* bytecode = luau_compile(source.data(), source.size(), &options, &bytecodeSize);
|
||||
if (bytecode == nullptr) {
|
||||
outError = "Luau compiler ran out of memory";
|
||||
return false;
|
||||
}
|
||||
|
||||
lua_State* moduleState = lua_newthread(caller);
|
||||
if (moduleState == nullptr) {
|
||||
std::free(bytecode);
|
||||
outError = "Luau VM ran out of memory";
|
||||
return false;
|
||||
}
|
||||
luaL_sandboxthread(moduleState);
|
||||
install_module_require(moduleState, vm, path);
|
||||
|
||||
const std::string chunkName = "@res/" + path;
|
||||
const int loadStatus = luau_load(moduleState, chunkName.c_str(), bytecode, bytecodeSize, 0);
|
||||
std::free(bytecode);
|
||||
if (loadStatus != LUA_OK) {
|
||||
const char* message = lua_tostring(moduleState, -1);
|
||||
outError = message != nullptr ? message : "failed to load Luau bytecode";
|
||||
lua_pop(caller, 1);
|
||||
return false;
|
||||
}
|
||||
if (!protected_call(moduleState, vm, 0, keepResult ? 1 : 0, kLifecycleBudget, outError)) {
|
||||
lua_pop(caller, 1);
|
||||
return false;
|
||||
}
|
||||
if (!keepResult) {
|
||||
lua_pop(caller, 1);
|
||||
return true;
|
||||
}
|
||||
if (lua_isnil(moduleState, -1)) {
|
||||
outError = "module res/" + path + " must return a value";
|
||||
lua_pop(caller, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
lua_xmove(moduleState, caller, 1);
|
||||
lua_remove(caller, -2);
|
||||
return true;
|
||||
}
|
||||
|
||||
int module_require(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
const std::string requested = luaL_checkstring(state, 1);
|
||||
|
||||
std::string moduleName;
|
||||
ModuleOpenFn factory = nullptr;
|
||||
if (requested.starts_with("dusklight.")) {
|
||||
moduleName = requested;
|
||||
factory = module_factory(moduleName);
|
||||
if (factory == nullptr) {
|
||||
luaL_error(state, "unknown module '%s'", moduleName.c_str());
|
||||
}
|
||||
} else {
|
||||
const char* currentPath = lua_tostring(state, lua_upvalueindex(2));
|
||||
const auto normalized =
|
||||
normalize_module_path(currentPath != nullptr ? currentPath : "main.luau", requested);
|
||||
if (!normalized.has_value()) {
|
||||
luaL_error(
|
||||
state, "module paths must be relative and remain inside the mod's res directory");
|
||||
}
|
||||
moduleName = *normalized;
|
||||
}
|
||||
|
||||
if (const auto found = vm.moduleRefs.find(moduleName); found != vm.moduleRefs.end()) {
|
||||
lua_getref(state, found->second);
|
||||
return 1;
|
||||
}
|
||||
if (!vm.loadingModules.insert(moduleName).second) {
|
||||
luaL_error(state, "cyclic require of '%s'", moduleName.c_str());
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (factory != nullptr) {
|
||||
factory(state);
|
||||
} else if (!load_source_module(state, vm, moduleName, true, error)) {
|
||||
vm.loadingModules.erase(moduleName);
|
||||
luaL_error(state, "%s", error.c_str());
|
||||
}
|
||||
vm.loadingModules.erase(moduleName);
|
||||
|
||||
const int ref = lua_ref(state, -1);
|
||||
vm.moduleRefs.emplace(moduleName, ref);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) {
|
||||
if (subject == nullptr) {
|
||||
return set_error(outError, MOD_INVALID_ARGUMENT, "Delegated mod context is null");
|
||||
}
|
||||
if (s_vms.contains(subject)) {
|
||||
return set_error(outError, MOD_CONFLICT, "Delegated mod is already active");
|
||||
}
|
||||
|
||||
auto vm = std::make_unique<Vm>();
|
||||
vm->subject = subject;
|
||||
vm->state = lua_newstate(limited_realloc, &vm->memory);
|
||||
if (vm->state == nullptr) {
|
||||
return set_error(outError, MOD_ERROR, "Failed to create Luau VM");
|
||||
}
|
||||
lua_callbacks(vm->state)->userdata = vm.get();
|
||||
lua_callbacks(vm->state)->interrupt = [](lua_State* state, int gc) {
|
||||
auto* current = static_cast<Vm*>(lua_callbacks(state)->userdata);
|
||||
if (gc < 0 && current != nullptr && current->deadlineActive &&
|
||||
std::chrono::steady_clock::now() > current->deadline)
|
||||
{
|
||||
luaL_error(state, "script execution exceeded its time budget");
|
||||
}
|
||||
};
|
||||
luaL_openlibs(vm->state);
|
||||
luaL_sandbox(vm->state);
|
||||
|
||||
std::string error;
|
||||
vm->loadingModules.insert("main.luau");
|
||||
const bool loadedMain = load_source_module(vm->state, *vm, "main.luau", false, error);
|
||||
vm->loadingModules.erase("main.luau");
|
||||
if (!loadedMain) {
|
||||
return set_error(outError, MOD_ERROR, error);
|
||||
}
|
||||
|
||||
s_vms.emplace(subject, std::move(vm));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult runtime_update(ModContext*, ModContext* subject, ModError* outError) {
|
||||
const auto found = s_vms.find(subject);
|
||||
if (found == s_vms.end()) {
|
||||
return set_error(outError, MOD_INVALID_ARGUMENT, "Delegated mod is not active");
|
||||
}
|
||||
Vm& vm = *found->second;
|
||||
const size_t callbackCount = vm.updateRefs.size();
|
||||
if (callbackCount == 0) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
DeadlineScope deadline{vm, kUpdateBudget};
|
||||
std::string error;
|
||||
for (size_t i = 0; i < callbackCount; ++i) {
|
||||
const int ref = vm.updateRefs[i];
|
||||
if (!call_ref(vm, ref, 0, 0, kUpdateBudget, error)) {
|
||||
return set_error(outError, MOD_ERROR, error);
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult runtime_deactivate(ModContext*, ModContext* subject, ModError*) {
|
||||
const auto found = s_vms.find(subject);
|
||||
if (found == s_vms.end()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
Vm& vm = *found->second;
|
||||
DeadlineScope deadline{vm, kLifecycleBudget};
|
||||
const size_t callbackCount = vm.shutdownRefs.size();
|
||||
for (size_t i = callbackCount; i > 0; --i) {
|
||||
std::string error;
|
||||
const int ref = vm.shutdownRefs[i - 1];
|
||||
if (!call_ref(vm, ref, 0, 0, kLifecycleBudget, error) && svc_log != nullptr) {
|
||||
svc_log->write(subject, LOG_LEVEL_ERROR, error.c_str());
|
||||
}
|
||||
}
|
||||
s_vms.erase(found);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
constexpr ModRuntimeService s_runtimeService{
|
||||
.header = SERVICE_HEADER(ModRuntimeService, 1, 0),
|
||||
.activate = runtime_activate,
|
||||
.update = runtime_update,
|
||||
.deactivate = runtime_deactivate,
|
||||
};
|
||||
EXPORT_SERVICE_AS(s_runtimeService, "dev.twilitrealm.luau");
|
||||
|
||||
} // namespace
|
||||
|
||||
Vm::~Vm() {
|
||||
if (state != nullptr) {
|
||||
lua_close(state);
|
||||
}
|
||||
}
|
||||
|
||||
Vm& vm_from_upvalue(lua_State* state) {
|
||||
auto* vm = static_cast<Vm*>(lua_tolightuserdata(state, lua_upvalueindex(1)));
|
||||
if (vm == nullptr) {
|
||||
luaL_error(state, "missing Luau runtime context");
|
||||
}
|
||||
return *vm;
|
||||
}
|
||||
|
||||
void push_vm_closure(lua_State* state, Vm& vm, lua_CFunction function, const char* name) {
|
||||
lua_pushlightuserdata(state, &vm);
|
||||
lua_pushcclosure(state, function, name, 1);
|
||||
}
|
||||
|
||||
void set_function(lua_State* state, Vm& vm, const char* name, lua_CFunction function) {
|
||||
push_vm_closure(state, vm, function, name);
|
||||
lua_setfield(state, -2, name);
|
||||
}
|
||||
|
||||
[[noreturn]] void service_unavailable(lua_State* state, const char* name) {
|
||||
luaL_error(state, "%s is not available in this Dusklight build", name);
|
||||
}
|
||||
|
||||
void check_result(lua_State* state, ModResult result, const char* operation) {
|
||||
if (result == MOD_OK) {
|
||||
return;
|
||||
}
|
||||
const char* resultName = "error";
|
||||
switch (result) {
|
||||
case MOD_UNAVAILABLE:
|
||||
resultName = "unavailable";
|
||||
break;
|
||||
case MOD_UNSUPPORTED:
|
||||
resultName = "unsupported";
|
||||
break;
|
||||
case MOD_CONFLICT:
|
||||
resultName = "conflict";
|
||||
break;
|
||||
case MOD_INVALID_ARGUMENT:
|
||||
resultName = "invalid argument";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
luaL_error(state, "%s failed: %s", operation, resultName);
|
||||
}
|
||||
|
||||
bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback) {
|
||||
lua_getfield(state, table, field);
|
||||
const bool value = lua_isnil(state, -1) ? fallback : luaL_checkboolean(state, -1) != 0;
|
||||
lua_pop(state, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool to_int64(lua_State* state, int index, int64_t& outValue) {
|
||||
if (lua_isinteger64(state, index)) {
|
||||
outValue = lua_tointeger64(state, index, nullptr);
|
||||
return true;
|
||||
}
|
||||
if (!lua_isnumber(state, index)) {
|
||||
return false;
|
||||
}
|
||||
const double value = lua_tonumber(state, index);
|
||||
constexpr double kMaxSafeInteger = 9007199254740991.0;
|
||||
if (!std::isfinite(value) || value < -kMaxSafeInteger || value > kMaxSafeInteger ||
|
||||
std::trunc(value) != value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
outValue = static_cast<int64_t>(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
int64_t check_int64(lua_State* state, int index) {
|
||||
int64_t value = 0;
|
||||
if (!to_int64(state, index, value)) {
|
||||
luaL_argerror(state, index, "integer value expected");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback) {
|
||||
lua_getfield(state, table, field);
|
||||
const int64_t value = lua_isnil(state, -1) ? fallback : check_int64(state, -1);
|
||||
lua_pop(state, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
double get_optional_number(lua_State* state, int table, const char* field, double fallback) {
|
||||
lua_getfield(state, table, field);
|
||||
const double value = lua_isnil(state, -1) ? fallback : luaL_checknumber(state, -1);
|
||||
lua_pop(state, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string get_optional_string(
|
||||
lua_State* state, int table, const char* field, std::string fallback) {
|
||||
lua_getfield(state, table, field);
|
||||
if (!lua_isnil(state, -1)) {
|
||||
size_t length = 0;
|
||||
const char* value = luaL_checklstring(state, -1, &length);
|
||||
fallback.assign(value, length);
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
int ref_optional_function(lua_State* state, int table, const char* field) {
|
||||
lua_getfield(state, table, field);
|
||||
if (lua_isnil(state, -1)) {
|
||||
lua_pop(state, 1);
|
||||
return LUA_NOREF;
|
||||
}
|
||||
luaL_argexpected(state, lua_isfunction(state, -1), table, "function field");
|
||||
const int ref = lua_ref(state, -1);
|
||||
lua_pop(state, 1);
|
||||
return ref;
|
||||
}
|
||||
|
||||
int ref_required_function(lua_State* state, int table, const char* field) {
|
||||
const int ref = ref_optional_function(state, table, field);
|
||||
if (ref == LUA_NOREF) {
|
||||
luaL_error(state, "field '%s' is required", field);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
Callback& retain_callback(Vm& vm) {
|
||||
auto callback = std::make_unique<Callback>();
|
||||
callback->vm = &vm;
|
||||
callback->refs.fill(LUA_NOREF);
|
||||
vm.callbacks.push_back(std::move(callback));
|
||||
return *vm.callbacks.back();
|
||||
}
|
||||
|
||||
bool call_ref(Vm& vm, int ref, int argumentCount, int resultCount,
|
||||
std::chrono::steady_clock::duration budget, std::string& outError) {
|
||||
lua_State* state = vm.state;
|
||||
lua_getref(state, ref);
|
||||
if (!lua_isfunction(state, -1)) {
|
||||
lua_pop(state, 1);
|
||||
outError = "callback is no longer a function";
|
||||
return false;
|
||||
}
|
||||
if (argumentCount != 0) {
|
||||
lua_insert(state, -argumentCount - 1);
|
||||
}
|
||||
return protected_call(state, vm, argumentCount, resultCount, budget, outError);
|
||||
}
|
||||
|
||||
void fail_callback(Vm& vm, std::string_view callbackName, std::string_view error) {
|
||||
const std::string message = std::string{callbackName} + ": " + std::string{error};
|
||||
if (svc_host != nullptr) {
|
||||
svc_host->fail(vm.subject, MOD_ERROR, message.c_str());
|
||||
} else if (svc_log != nullptr) {
|
||||
svc_log->write(vm.subject, LOG_LEVEL_ERROR, message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ModResult set_error(ModError* outError, ModResult result, std::string_view message) {
|
||||
if (outError != nullptr && outError->struct_size >= sizeof(ModError)) {
|
||||
outError->code = result;
|
||||
const size_t size = std::min(message.size(), sizeof(outError->message) - 1);
|
||||
std::memcpy(outError->message, message.data(), size);
|
||||
outError->message[size] = '\0';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void create_handle_metatable(
|
||||
lua_State* state, const char* name, const luaL_Reg* methods, const char* typeName) {
|
||||
luaL_newmetatable(state, name);
|
||||
luaL_register(state, nullptr, methods);
|
||||
lua_pushvalue(state, -1);
|
||||
lua_setfield(state, -2, "__index");
|
||||
lua_pushstring(state, typeName);
|
||||
lua_setfield(state, -2, "__type");
|
||||
lua_setreadonly(state, -1, true);
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
|
||||
ScriptHandle& check_handle(lua_State* state, int index, const char* metatable, HandleKind kind) {
|
||||
auto* handle = static_cast<ScriptHandle*>(luaL_checkudata(state, index, metatable));
|
||||
if (handle == nullptr || handle->kind != kind || handle->vm == nullptr || handle->value == 0) {
|
||||
luaL_argerror(state, index, "stale handle");
|
||||
}
|
||||
return *handle;
|
||||
}
|
||||
|
||||
void push_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind, const char* metatable,
|
||||
ConfigVarType configType) {
|
||||
auto* handle = static_cast<ScriptHandle*>(lua_newuserdata(state, sizeof(ScriptHandle)));
|
||||
*handle = ScriptHandle{.vm = &vm, .value = value, .kind = kind, .configType = configType};
|
||||
luaL_getmetatable(state, metatable);
|
||||
lua_setmetatable(state, -2);
|
||||
}
|
||||
|
||||
} // namespace luau_runtime
|
||||
|
||||
extern "C" {
|
||||
MOD_EXPORT ModResult mod_initialize(ModError*) {
|
||||
FFlag::LuauIntegerType2.value = true;
|
||||
FFlag::LuauIntegerLibrary.value = true;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
luau_runtime::s_vms.clear();
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include "lua.h"
|
||||
#include "lualib.h"
|
||||
|
||||
#include "mods/api.h"
|
||||
#include "mods/svc/config.h"
|
||||
#include "mods/svc/host.h"
|
||||
#include "mods/svc/log.h"
|
||||
#include "mods/svc/overlay.h"
|
||||
#include "mods/svc/resource.h"
|
||||
#include "mods/svc/texture.h"
|
||||
#include "mods/svc/ui.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace luau_runtime {
|
||||
|
||||
constexpr size_t kMemoryLimit = 64u * 1024u * 1024u;
|
||||
constexpr auto kUpdateBudget = std::chrono::milliseconds{250};
|
||||
constexpr auto kLifecycleBudget = std::chrono::seconds{5};
|
||||
constexpr auto kCallbackBudget = std::chrono::milliseconds{250};
|
||||
|
||||
struct MemoryBudget {
|
||||
size_t used = 0;
|
||||
size_t limit = kMemoryLimit;
|
||||
};
|
||||
|
||||
struct Vm;
|
||||
|
||||
struct Callback {
|
||||
Vm* vm = nullptr;
|
||||
std::array<int, 8> refs{};
|
||||
std::string returnedString;
|
||||
int tag = 0;
|
||||
};
|
||||
|
||||
struct Vm {
|
||||
MemoryBudget memory;
|
||||
lua_State* state = nullptr;
|
||||
ModContext* subject = nullptr;
|
||||
std::vector<int> updateRefs;
|
||||
std::vector<int> shutdownRefs;
|
||||
std::unordered_map<std::string, int> moduleRefs;
|
||||
std::unordered_set<std::string> loadingModules;
|
||||
std::vector<std::unique_ptr<Callback>> callbacks;
|
||||
std::chrono::steady_clock::time_point deadline{};
|
||||
unsigned callDepth = 0;
|
||||
bool deadlineActive = false;
|
||||
|
||||
~Vm();
|
||||
};
|
||||
|
||||
enum class HandleKind : uint8_t {
|
||||
ConfigVar,
|
||||
ConfigSubscription,
|
||||
Overlay,
|
||||
Texture,
|
||||
UiWindow,
|
||||
UiDialog,
|
||||
UiElement,
|
||||
UiStyle,
|
||||
UiMenuTab,
|
||||
UiList,
|
||||
};
|
||||
|
||||
struct ScriptHandle {
|
||||
Vm* vm = nullptr;
|
||||
uint64_t value = 0;
|
||||
HandleKind kind = HandleKind::ConfigVar;
|
||||
ConfigVarType configType = CONFIG_VAR_BOOL;
|
||||
};
|
||||
|
||||
using ModuleOpenFn = int (*)(lua_State* state);
|
||||
|
||||
Vm& vm_from_upvalue(lua_State* state);
|
||||
void push_vm_closure(lua_State* state, Vm& vm, lua_CFunction function, const char* name);
|
||||
void set_function(lua_State* state, Vm& vm, const char* name, lua_CFunction function);
|
||||
|
||||
[[noreturn]] void service_unavailable(lua_State* state, const char* name);
|
||||
void check_result(lua_State* state, ModResult result, const char* operation);
|
||||
|
||||
bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback);
|
||||
bool to_int64(lua_State* state, int index, int64_t& outValue);
|
||||
int64_t check_int64(lua_State* state, int index);
|
||||
int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback);
|
||||
double get_optional_number(lua_State* state, int table, const char* field, double fallback);
|
||||
std::string get_optional_string(
|
||||
lua_State* state, int table, const char* field, std::string fallback = {});
|
||||
int ref_optional_function(lua_State* state, int table, const char* field);
|
||||
int ref_required_function(lua_State* state, int table, const char* field);
|
||||
|
||||
Callback& retain_callback(Vm& vm);
|
||||
bool call_ref(Vm& vm, int ref, int argumentCount, int resultCount,
|
||||
std::chrono::steady_clock::duration budget, std::string& outError);
|
||||
void fail_callback(Vm& vm, std::string_view callbackName, std::string_view error);
|
||||
ModResult set_error(ModError* outError, ModResult result, std::string_view message);
|
||||
|
||||
void create_handle_metatable(
|
||||
lua_State* state, const char* name, const luaL_Reg* methods, const char* typeName);
|
||||
ScriptHandle& check_handle(lua_State* state, int index, const char* metatable, HandleKind kind);
|
||||
void push_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind, const char* metatable,
|
||||
ConfigVarType configType = CONFIG_VAR_BOOL);
|
||||
|
||||
void push_config_value(lua_State* state, const ConfigVarValue& value);
|
||||
void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind);
|
||||
|
||||
int open_log(lua_State* state);
|
||||
int open_host(lua_State* state);
|
||||
int open_resource(lua_State* state);
|
||||
int open_overlay(lua_State* state);
|
||||
int open_texture(lua_State* state);
|
||||
int open_config(lua_State* state);
|
||||
int open_ui(lua_State* state);
|
||||
|
||||
} // namespace luau_runtime
|
||||
@@ -0,0 +1,856 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace luau_runtime {
|
||||
namespace {
|
||||
|
||||
constexpr char kUiWindowMetatable[] = "dusklight.ui_window";
|
||||
constexpr char kUiDialogMetatable[] = "dusklight.ui_dialog";
|
||||
constexpr char kUiElementMetatable[] = "dusklight.ui_element";
|
||||
constexpr char kUiStyleMetatable[] = "dusklight.ui_style";
|
||||
constexpr char kUiMenuTabMetatable[] = "dusklight.ui_menu_tab";
|
||||
constexpr char kUiListMetatable[] = "dusklight.ui_list";
|
||||
|
||||
const char* ui_metatable(HandleKind kind) {
|
||||
switch (kind) {
|
||||
case HandleKind::UiWindow:
|
||||
return kUiWindowMetatable;
|
||||
case HandleKind::UiDialog:
|
||||
return kUiDialogMetatable;
|
||||
case HandleKind::UiStyle:
|
||||
return kUiStyleMetatable;
|
||||
case HandleKind::UiMenuTab:
|
||||
return kUiMenuTabMetatable;
|
||||
case HandleKind::UiList:
|
||||
return kUiListMetatable;
|
||||
default:
|
||||
return kUiElementMetatable;
|
||||
}
|
||||
}
|
||||
|
||||
ScriptHandle& check_ui_handle(lua_State* state, int index, HandleKind kind) {
|
||||
return check_handle(state, index, ui_metatable(kind), kind);
|
||||
}
|
||||
|
||||
ScriptHandle& check_config_var(lua_State* state, int index) {
|
||||
luaL_checktype(state, index, LUA_TUSERDATA);
|
||||
auto* handle = static_cast<ScriptHandle*>(lua_touserdata(state, index));
|
||||
if (handle == nullptr || handle->kind != HandleKind::ConfigVar || handle->value == 0 ||
|
||||
handle->vm == nullptr)
|
||||
{
|
||||
luaL_argerror(state, index, "expected a live ConfigVar");
|
||||
}
|
||||
return *handle;
|
||||
}
|
||||
|
||||
bool call_callback(Callback& callback, int ref, int argumentCount, int resultCount,
|
||||
const char* name, std::string* outError = nullptr) {
|
||||
std::string error;
|
||||
if (call_ref(*callback.vm, ref, argumentCount, resultCount, kCallbackBudget, error)) {
|
||||
return true;
|
||||
}
|
||||
if (outError != nullptr) {
|
||||
*outError = std::move(error);
|
||||
} else {
|
||||
fail_callback(*callback.vm, name, error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ModResult call_build(
|
||||
Callback& callback, int ref, int argumentCount, const char* name, ModError* outError) {
|
||||
std::string error;
|
||||
if (call_callback(callback, ref, argumentCount, 0, name, &error)) {
|
||||
return MOD_OK;
|
||||
}
|
||||
return set_error(outError, MOD_ERROR, error);
|
||||
}
|
||||
|
||||
bool call_predicate(Callback& callback, int ref, int argumentCount, const char* name) {
|
||||
if (!call_callback(callback, ref, argumentCount, 1, name)) {
|
||||
return false;
|
||||
}
|
||||
lua_State* state = callback.vm->state;
|
||||
const bool result = lua_toboolean(state, -1) != 0;
|
||||
lua_pop(state, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
void control_get(ModContext*, void* userData, UiControlValue* outValue) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
if (outValue == nullptr || !call_callback(callback, callback.refs[0], 0, 1, "control get")) {
|
||||
return;
|
||||
}
|
||||
lua_State* state = callback.vm->state;
|
||||
switch (static_cast<UiControlKind>(callback.tag)) {
|
||||
case UI_CONTROL_TOGGLE:
|
||||
outValue->bool_value = lua_toboolean(state, -1) != 0;
|
||||
break;
|
||||
case UI_CONTROL_NUMBER:
|
||||
case UI_CONTROL_SELECT: {
|
||||
int64_t value = 0;
|
||||
if (!to_int64(state, -1, value)) {
|
||||
fail_callback(*callback.vm, "control get", "callback must return an integer");
|
||||
} else {
|
||||
outValue->int_value = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UI_CONTROL_STRING:
|
||||
case UI_CONTROL_COLOR:
|
||||
case UI_CONTROL_FILE_PICKER: {
|
||||
size_t length = 0;
|
||||
const char* value = lua_tolstring(state, -1, &length);
|
||||
if (value == nullptr) {
|
||||
fail_callback(*callback.vm, "control get", "callback must return a string");
|
||||
} else {
|
||||
callback.returnedString.assign(value, length);
|
||||
outValue->string_value = callback.returnedString.c_str();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
|
||||
void control_set(ModContext*, void* userData, const UiControlValue* value) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
}
|
||||
lua_State* state = callback.vm->state;
|
||||
switch (static_cast<UiControlKind>(callback.tag)) {
|
||||
case UI_CONTROL_TOGGLE:
|
||||
lua_pushboolean(state, value->bool_value);
|
||||
break;
|
||||
case UI_CONTROL_NUMBER:
|
||||
case UI_CONTROL_SELECT:
|
||||
lua_pushinteger64(state, value->int_value);
|
||||
break;
|
||||
case UI_CONTROL_STRING:
|
||||
case UI_CONTROL_COLOR:
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
lua_pushstring(state, value->string_value != nullptr ? value->string_value : "");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
call_callback(callback, callback.refs[1], 1, 0, "control set");
|
||||
}
|
||||
|
||||
bool control_disabled(ModContext*, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
return call_predicate(callback, callback.refs[3], 0, "control is_disabled");
|
||||
}
|
||||
|
||||
bool control_modified(ModContext*, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
return call_predicate(callback, callback.refs[4], 0, "control is_modified");
|
||||
}
|
||||
|
||||
bool control_selected(ModContext*, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
return call_predicate(callback, callback.refs[5], 0, "control is_selected");
|
||||
}
|
||||
|
||||
void control_pressed(ModContext*, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
call_callback(callback, callback.refs[2], 0, 0, "control on_pressed");
|
||||
}
|
||||
|
||||
ModResult panel_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
|
||||
return call_build(callback, callback.refs[0], 1, "panel build", outError);
|
||||
}
|
||||
|
||||
ModResult panel_update(ModContext*, void* userData, ModError* outError) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
return call_build(callback, callback.refs[1], 0, "panel update", outError);
|
||||
}
|
||||
|
||||
ModResult group_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
|
||||
return call_build(callback, callback.refs[0], 1, "group build", outError);
|
||||
}
|
||||
|
||||
ModResult tab_build(ModContext*, UiWindowHandle window, UiElementHandle left, UiElementHandle right,
|
||||
void* userData, ModError* outError) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, window, HandleKind::UiWindow);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, left, HandleKind::UiElement);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, right, HandleKind::UiElement);
|
||||
return call_build(callback, callback.refs[0], 3, "window tab build", outError);
|
||||
}
|
||||
|
||||
void window_closed(ModContext*, UiWindowHandle window, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, window, HandleKind::UiWindow);
|
||||
call_callback(callback, callback.refs[0], 1, 0, "window on_closed");
|
||||
}
|
||||
|
||||
void dialog_action(ModContext*, UiDialogHandle dialog, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, dialog, HandleKind::UiDialog);
|
||||
call_callback(callback, callback.refs[0], 1, 0, "dialog action");
|
||||
}
|
||||
|
||||
bool dialog_action_disabled(ModContext*, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
return call_predicate(callback, callback.refs[1], 0, "dialog action is_disabled");
|
||||
}
|
||||
|
||||
void dialog_dismissed(ModContext*, UiDialogHandle dialog, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, dialog, HandleKind::UiDialog);
|
||||
call_callback(callback, callback.refs[0], 1, 0, "dialog on_dismiss");
|
||||
}
|
||||
|
||||
ModResult dialog_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
|
||||
return call_build(callback, callback.refs[1], 1, "dialog build", outError);
|
||||
}
|
||||
|
||||
void menu_selected(ModContext*, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
call_callback(callback, callback.refs[0], 0, 0, "menu tab on_selected");
|
||||
}
|
||||
|
||||
void list_pressed(ModContext*, UiListHandle list, uint64_t key, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
|
||||
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
|
||||
call_callback(callback, callback.refs[0], 2, 0, "list on_pressed");
|
||||
}
|
||||
|
||||
bool list_selected(ModContext*, UiListHandle list, uint64_t key, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
|
||||
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
|
||||
return call_predicate(callback, callback.refs[1], 2, "list is_selected");
|
||||
}
|
||||
|
||||
bool list_disabled(ModContext*, UiListHandle list, uint64_t key, void* userData) {
|
||||
auto& callback = *static_cast<Callback*>(userData);
|
||||
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
|
||||
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
|
||||
return call_predicate(callback, callback.refs[2], 2, "list is_disabled");
|
||||
}
|
||||
|
||||
std::vector<std::string> string_array(lua_State* state, int table, const char* field) {
|
||||
std::vector<std::string> result;
|
||||
lua_getfield(state, table, field);
|
||||
if (lua_isnil(state, -1)) {
|
||||
lua_pop(state, 1);
|
||||
return result;
|
||||
}
|
||||
luaL_checktype(state, -1, LUA_TTABLE);
|
||||
const int count = lua_objlen(state, -1);
|
||||
result.reserve(count);
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
lua_rawgeti(state, -1, i);
|
||||
result.emplace_back(luaL_checkstring(state, -1));
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<UiListItem> list_items(lua_State* state, int table, std::vector<std::string>& labels) {
|
||||
luaL_checktype(state, table, LUA_TTABLE);
|
||||
const int count = lua_objlen(state, table);
|
||||
labels.reserve(count);
|
||||
std::vector<UiListItem> items;
|
||||
items.reserve(count);
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
lua_rawgeti(state, table, i);
|
||||
luaL_checktype(state, -1, LUA_TTABLE);
|
||||
labels.push_back(get_optional_string(state, -1, "label"));
|
||||
UiListItem item = UI_LIST_ITEM_INIT;
|
||||
item.key = static_cast<uint64_t>(get_optional_int(state, -1, "key", 0));
|
||||
items.push_back(item);
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
items[i].label = labels[i].c_str();
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
UiControlKind control_kind(lua_State* state, const std::string& kind) {
|
||||
if (kind == "button")
|
||||
return UI_CONTROL_BUTTON;
|
||||
if (kind == "toggle")
|
||||
return UI_CONTROL_TOGGLE;
|
||||
if (kind == "number")
|
||||
return UI_CONTROL_NUMBER;
|
||||
if (kind == "string")
|
||||
return UI_CONTROL_STRING;
|
||||
if (kind == "select")
|
||||
return UI_CONTROL_SELECT;
|
||||
if (kind == "color")
|
||||
return UI_CONTROL_COLOR;
|
||||
if (kind == "group")
|
||||
return UI_CONTROL_GROUP;
|
||||
if (kind == "file_picker")
|
||||
return UI_CONTROL_FILE_PICKER;
|
||||
luaL_error(state, "unknown UI control kind '%s'", kind.c_str());
|
||||
}
|
||||
|
||||
UiStyleScope style_scope(lua_State* state, const std::string& scope) {
|
||||
if (scope == "prelaunch")
|
||||
return UI_SCOPE_PRELAUNCH;
|
||||
if (scope == "window")
|
||||
return UI_SCOPE_WINDOW;
|
||||
if (scope == "menu_bar")
|
||||
return UI_SCOPE_MENU_BAR;
|
||||
if (scope == "overlay")
|
||||
return UI_SCOPE_OVERLAY;
|
||||
if (scope == "touch_controls")
|
||||
return UI_SCOPE_TOUCH_CONTROLS;
|
||||
if (scope == "graphics_tuner")
|
||||
return UI_SCOPE_GRAPHICS_TUNER;
|
||||
luaL_error(state, "unknown UI style scope '%s'", scope.c_str());
|
||||
}
|
||||
|
||||
int pane_add_section(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
check_result(state,
|
||||
svc_ui->pane_add_section(pane.vm->subject, pane.value, luaL_checkstring(state, 2)),
|
||||
"ui pane_add_section");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pane_add_text(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
UiElementHandle element = 0;
|
||||
check_result(state,
|
||||
svc_ui->pane_add_text(pane.vm->subject, pane.value, luaL_checkstring(state, 2), &element),
|
||||
"ui pane_add_text");
|
||||
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pane_add_rml(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
UiElementHandle element = 0;
|
||||
check_result(state,
|
||||
svc_ui->pane_add_rml(pane.vm->subject, pane.value, luaL_checkstring(state, 2), &element),
|
||||
"ui pane_add_rml");
|
||||
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pane_add_progress(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
UiElementHandle element = 0;
|
||||
check_result(state,
|
||||
svc_ui->pane_add_progress(
|
||||
pane.vm->subject, pane.value, static_cast<float>(luaL_checknumber(state, 2)), &element),
|
||||
"ui pane_add_progress");
|
||||
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pane_add_control(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
luaL_checktype(state, 2, LUA_TTABLE);
|
||||
|
||||
UiControlDesc desc = UI_CONTROL_DESC_INIT;
|
||||
const std::string kind = get_optional_string(state, 2, "kind");
|
||||
const std::string label = get_optional_string(state, 2, "label");
|
||||
const std::string help = get_optional_string(state, 2, "help_rml");
|
||||
const std::string prefix = get_optional_string(state, 2, "prefix");
|
||||
const std::string suffix = get_optional_string(state, 2, "suffix");
|
||||
desc.kind = control_kind(state, kind);
|
||||
desc.label = label.c_str();
|
||||
desc.help_rml = help.empty() ? nullptr : help.c_str();
|
||||
desc.min = get_optional_int(state, 2, "min", 0);
|
||||
desc.max = get_optional_int(state, 2, "max", 0);
|
||||
desc.step = get_optional_int(state, 2, "step", 1);
|
||||
desc.prefix = prefix.empty() ? nullptr : prefix.c_str();
|
||||
desc.suffix = suffix.empty() ? nullptr : suffix.c_str();
|
||||
desc.max_length = static_cast<int32_t>(get_optional_int(state, 2, "max_length", 0));
|
||||
desc.color_alpha = get_optional_bool(state, 2, "color_alpha", false);
|
||||
desc.directory_mode = get_optional_bool(state, 2, "directory_mode", false);
|
||||
desc.string_set_mode = get_optional_string(state, 2, "string_set_mode") == "change" ?
|
||||
UI_STRING_SET_ON_CHANGE :
|
||||
UI_STRING_SET_ON_COMMIT;
|
||||
|
||||
std::vector<std::string> options = string_array(state, 2, "options");
|
||||
std::vector<const char*> optionPointers;
|
||||
optionPointers.reserve(options.size());
|
||||
for (const auto& option : options)
|
||||
optionPointers.push_back(option.c_str());
|
||||
desc.options = optionPointers.data();
|
||||
desc.option_count = optionPointers.size();
|
||||
|
||||
std::vector<std::string> presets = string_array(state, 2, "color_presets");
|
||||
std::vector<const char*> presetPointers;
|
||||
presetPointers.reserve(presets.size());
|
||||
for (const auto& preset : presets)
|
||||
presetPointers.push_back(preset.c_str());
|
||||
desc.color_presets = presetPointers.data();
|
||||
desc.color_preset_count = presetPointers.size();
|
||||
|
||||
std::vector<std::string> filterNames;
|
||||
std::vector<std::string> filterPatterns;
|
||||
std::vector<FileFilter> filters;
|
||||
lua_getfield(state, 2, "file_filters");
|
||||
if (!lua_isnil(state, -1)) {
|
||||
luaL_checktype(state, -1, LUA_TTABLE);
|
||||
const int count = lua_objlen(state, -1);
|
||||
filterNames.reserve(count);
|
||||
filterPatterns.reserve(count);
|
||||
filters.resize(count);
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
lua_rawgeti(state, -1, i);
|
||||
filterNames.push_back(get_optional_string(state, -1, "name"));
|
||||
filterPatterns.push_back(get_optional_string(state, -1, "pattern"));
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
filters[i] = {filterNames[i].c_str(), filterPatterns[i].c_str()};
|
||||
}
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
desc.file_filters = filters.data();
|
||||
desc.file_filter_count = filters.size();
|
||||
|
||||
Callback& callback = retain_callback(*pane.vm);
|
||||
callback.tag = desc.kind;
|
||||
callback.refs[2] = ref_optional_function(state, 2, "on_pressed");
|
||||
callback.refs[3] = ref_optional_function(state, 2, "is_disabled");
|
||||
callback.refs[4] = ref_optional_function(state, 2, "is_modified");
|
||||
callback.refs[5] = ref_optional_function(state, 2, "is_selected");
|
||||
desc.user_data = &callback;
|
||||
desc.on_pressed = callback.refs[2] != LUA_NOREF ? control_pressed : nullptr;
|
||||
desc.is_disabled = callback.refs[3] != LUA_NOREF ? control_disabled : nullptr;
|
||||
desc.is_modified = callback.refs[4] != LUA_NOREF ? control_modified : nullptr;
|
||||
desc.is_selected = callback.refs[5] != LUA_NOREF ? control_selected : nullptr;
|
||||
|
||||
lua_getfield(state, 2, "config_var");
|
||||
if (!lua_isnil(state, -1)) {
|
||||
auto& variable = check_config_var(state, -1);
|
||||
desc.binding = UI_BINDING_CONFIG_VAR;
|
||||
desc.config_var = variable.value;
|
||||
} else if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP) {
|
||||
desc.binding = UI_BINDING_CALLBACKS;
|
||||
callback.refs[0] = ref_required_function(state, 2, "get");
|
||||
callback.refs[1] = ref_required_function(state, 2, "set");
|
||||
desc.get = control_get;
|
||||
desc.set = control_set;
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
|
||||
UiElementHandle element = 0;
|
||||
check_result(state, svc_ui->pane_add_control(pane.vm->subject, pane.value, &desc, &element),
|
||||
"ui pane_add_control");
|
||||
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pane_add_group(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
auto& target = check_ui_handle(state, 2, HandleKind::UiElement);
|
||||
luaL_checktype(state, 3, LUA_TTABLE);
|
||||
const std::string label = get_optional_string(state, 3, "label");
|
||||
Callback& callback = retain_callback(*pane.vm);
|
||||
callback.refs[0] = ref_required_function(state, 3, "build");
|
||||
UiGroupDesc desc = UI_GROUP_DESC_INIT;
|
||||
desc.label = label.c_str();
|
||||
desc.build = group_build;
|
||||
desc.user_data = &callback;
|
||||
UiElementHandle element = 0;
|
||||
check_result(state,
|
||||
svc_ui->pane_add_group(pane.vm->subject, pane.value, target.value, &desc, &element),
|
||||
"ui pane_add_group");
|
||||
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pane_add_list(lua_State* state) {
|
||||
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
luaL_checktype(state, 2, LUA_TTABLE);
|
||||
Callback& callback = retain_callback(*pane.vm);
|
||||
callback.refs[0] = ref_required_function(state, 2, "on_pressed");
|
||||
callback.refs[1] = ref_optional_function(state, 2, "is_selected");
|
||||
callback.refs[2] = ref_optional_function(state, 2, "is_disabled");
|
||||
|
||||
std::vector<std::string> labels;
|
||||
std::vector<UiListItem> items;
|
||||
lua_getfield(state, 2, "items");
|
||||
if (!lua_isnil(state, -1))
|
||||
items = list_items(state, -1, labels);
|
||||
lua_pop(state, 1);
|
||||
|
||||
UiListDesc desc = UI_LIST_DESC_INIT;
|
||||
desc.items = items.data();
|
||||
desc.item_count = items.size();
|
||||
desc.on_pressed = list_pressed;
|
||||
desc.is_selected = callback.refs[1] != LUA_NOREF ? list_selected : nullptr;
|
||||
desc.is_disabled = callback.refs[2] != LUA_NOREF ? list_disabled : nullptr;
|
||||
desc.user_data = &callback;
|
||||
UiListHandle list = 0;
|
||||
check_result(state, svc_ui->pane_add_list(pane.vm->subject, pane.value, &desc, &list),
|
||||
"ui pane_add_list");
|
||||
push_ui_handle(state, *pane.vm, list, HandleKind::UiList);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int element_set_text(lua_State* state) {
|
||||
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
check_result(state,
|
||||
svc_ui->elem_set_text(element.vm->subject, element.value, luaL_checkstring(state, 2)),
|
||||
"ui elem_set_text");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int element_set_rml(lua_State* state) {
|
||||
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
check_result(state,
|
||||
svc_ui->elem_set_rml(element.vm->subject, element.value, luaL_checkstring(state, 2)),
|
||||
"ui elem_set_rml");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int element_set_progress(lua_State* state) {
|
||||
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
check_result(state,
|
||||
svc_ui->elem_set_progress(
|
||||
element.vm->subject, element.value, static_cast<float>(luaL_checknumber(state, 2))),
|
||||
"ui elem_set_progress");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int element_set_class(lua_State* state) {
|
||||
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
|
||||
check_result(state,
|
||||
svc_ui->elem_set_class(element.vm->subject, element.value, luaL_checkstring(state, 2),
|
||||
luaL_checkboolean(state, 3) != 0),
|
||||
"ui elem_set_class");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int list_set_items(lua_State* state) {
|
||||
auto& list = check_ui_handle(state, 1, HandleKind::UiList);
|
||||
std::vector<std::string> labels;
|
||||
auto items = list_items(state, 2, labels);
|
||||
check_result(state,
|
||||
svc_ui->list_set_items(list.vm->subject, list.value, items.data(), items.size()),
|
||||
"ui list_set_items");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int window_close(lua_State* state) {
|
||||
auto& window = check_ui_handle(state, 1, HandleKind::UiWindow);
|
||||
check_result(state, svc_ui->window_close(window.vm->subject, window.value), "ui window_close");
|
||||
window.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dialog_close(lua_State* state) {
|
||||
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
|
||||
check_result(state, svc_ui->dialog_close(dialog.vm->subject, dialog.value), "ui dialog_close");
|
||||
dialog.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dialog_set_body(lua_State* state) {
|
||||
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
|
||||
check_result(state,
|
||||
svc_ui->dialog_set_body(dialog.vm->subject, dialog.value, luaL_checkstring(state, 2)),
|
||||
"ui dialog_set_body");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dialog_set_icon(lua_State* state) {
|
||||
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
|
||||
check_result(state,
|
||||
svc_ui->dialog_set_icon(dialog.vm->subject, dialog.value, luaL_checkstring(state, 2)),
|
||||
"ui dialog_set_icon");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int style_unregister(lua_State* state) {
|
||||
auto& style = check_ui_handle(state, 1, HandleKind::UiStyle);
|
||||
check_result(
|
||||
state, svc_ui->unregister_styles(style.vm->subject, style.value), "ui unregister_styles");
|
||||
style.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int menu_tab_unregister(lua_State* state) {
|
||||
auto& tab = check_ui_handle(state, 1, HandleKind::UiMenuTab);
|
||||
check_result(
|
||||
state, svc_ui->unregister_menu_tab(tab.vm->subject, tab.value), "ui unregister_menu_tab");
|
||||
tab.value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int register_mods_panel(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
Callback& callback = retain_callback(vm);
|
||||
callback.refs[0] = ref_required_function(state, 1, "build");
|
||||
callback.refs[1] = ref_optional_function(state, 1, "update");
|
||||
UiModsPanelDesc desc = UI_MODS_PANEL_DESC_INIT;
|
||||
desc.build = panel_build;
|
||||
desc.update = callback.refs[1] != LUA_NOREF ? panel_update : nullptr;
|
||||
desc.user_data = &callback;
|
||||
check_result(state, svc_ui->register_mods_panel(vm.subject, &desc), "ui register_mods_panel");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int window_push(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
std::vector<UiTabDesc> tabs;
|
||||
std::vector<std::string> titles;
|
||||
lua_getfield(state, 1, "tabs");
|
||||
luaL_checktype(state, -1, LUA_TTABLE);
|
||||
const int tabCount = lua_objlen(state, -1);
|
||||
tabs.reserve(tabCount);
|
||||
titles.reserve(tabCount);
|
||||
for (int i = 1; i <= tabCount; ++i) {
|
||||
lua_rawgeti(state, -1, i);
|
||||
titles.push_back(get_optional_string(state, -1, "title"));
|
||||
Callback& callback = retain_callback(vm);
|
||||
callback.refs[0] = ref_required_function(state, -1, "build");
|
||||
callback.refs[1] = ref_optional_function(state, -1, "update");
|
||||
UiTabDesc tab = UI_TAB_DESC_INIT;
|
||||
tab.build = tab_build;
|
||||
tab.update = callback.refs[1] != LUA_NOREF ? panel_update : nullptr;
|
||||
tab.user_data = &callback;
|
||||
tabs.push_back(tab);
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
for (size_t i = 0; i < tabs.size(); ++i)
|
||||
tabs[i].title = titles[i].c_str();
|
||||
|
||||
const std::string rcss = get_optional_string(state, 1, "rcss");
|
||||
Callback& closed = retain_callback(vm);
|
||||
closed.refs[0] = ref_optional_function(state, 1, "on_closed");
|
||||
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
|
||||
desc.tabs = tabs.data();
|
||||
desc.tab_count = tabs.size();
|
||||
desc.rcss = rcss.empty() ? nullptr : rcss.c_str();
|
||||
desc.on_closed = closed.refs[0] != LUA_NOREF ? window_closed : nullptr;
|
||||
desc.user_data = &closed;
|
||||
UiWindowHandle window = 0;
|
||||
check_result(state, svc_ui->window_push(vm.subject, &desc, &window), "ui window_push");
|
||||
push_ui_handle(state, vm, window, HandleKind::UiWindow);
|
||||
return 1;
|
||||
}
|
||||
|
||||
UiDialogVariant dialog_variant(lua_State* state, const std::string& variant) {
|
||||
if (variant.empty() || variant == "normal")
|
||||
return UI_DIALOG_NORMAL;
|
||||
if (variant == "warning")
|
||||
return UI_DIALOG_WARNING;
|
||||
if (variant == "danger")
|
||||
return UI_DIALOG_DANGER;
|
||||
luaL_error(state, "unknown dialog variant '%s'", variant.c_str());
|
||||
}
|
||||
|
||||
int dialog_push(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
const std::string title = get_optional_string(state, 1, "title");
|
||||
const std::string body = get_optional_string(state, 1, "body_rml");
|
||||
const std::string icon = get_optional_string(state, 1, "icon");
|
||||
const std::string variant = get_optional_string(state, 1, "variant");
|
||||
|
||||
std::vector<UiDialogAction> actions;
|
||||
std::vector<std::string> labels;
|
||||
lua_getfield(state, 1, "actions");
|
||||
luaL_checktype(state, -1, LUA_TTABLE);
|
||||
const int actionCount = lua_objlen(state, -1);
|
||||
actions.reserve(actionCount);
|
||||
labels.reserve(actionCount);
|
||||
for (int i = 1; i <= actionCount; ++i) {
|
||||
lua_rawgeti(state, -1, i);
|
||||
labels.push_back(get_optional_string(state, -1, "label"));
|
||||
Callback& callback = retain_callback(vm);
|
||||
callback.refs[0] = ref_optional_function(state, -1, "on_pressed");
|
||||
callback.refs[1] = ref_optional_function(state, -1, "is_disabled");
|
||||
UiDialogAction action = UI_DIALOG_ACTION_INIT;
|
||||
action.on_pressed = callback.refs[0] != LUA_NOREF ? dialog_action : nullptr;
|
||||
action.is_disabled = callback.refs[1] != LUA_NOREF ? dialog_action_disabled : nullptr;
|
||||
action.user_data = &callback;
|
||||
action.keep_open = get_optional_bool(state, -1, "keep_open", false);
|
||||
actions.push_back(action);
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
for (size_t i = 0; i < actions.size(); ++i)
|
||||
actions[i].label = labels[i].c_str();
|
||||
|
||||
Callback& callback = retain_callback(vm);
|
||||
callback.refs[0] = ref_optional_function(state, 1, "on_dismiss");
|
||||
callback.refs[1] = ref_optional_function(state, 1, "build");
|
||||
UiDialogDesc desc = UI_DIALOG_DESC_INIT;
|
||||
desc.title = title.c_str();
|
||||
desc.body_rml = body.c_str();
|
||||
desc.icon = icon.empty() ? nullptr : icon.c_str();
|
||||
desc.variant = dialog_variant(state, variant);
|
||||
desc.actions = actions.data();
|
||||
desc.action_count = actions.size();
|
||||
desc.on_dismiss = callback.refs[0] != LUA_NOREF ? dialog_dismissed : nullptr;
|
||||
desc.build = callback.refs[1] != LUA_NOREF ? dialog_build : nullptr;
|
||||
desc.user_data = &callback;
|
||||
UiDialogHandle dialog = 0;
|
||||
check_result(state, svc_ui->dialog_push(vm.subject, &desc, &dialog), "ui dialog_push");
|
||||
push_ui_handle(state, vm, dialog, HandleKind::UiDialog);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int is_any_document_visible(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
bool visible = false;
|
||||
check_result(
|
||||
state, svc_ui->is_any_document_visible(vm.subject, &visible), "ui is_any_document_visible");
|
||||
lua_pushboolean(state, visible);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int register_styles(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
const auto scope = style_scope(state, luaL_checkstring(state, 1));
|
||||
UiStyleHandle style = 0;
|
||||
check_result(state,
|
||||
svc_ui->register_styles(vm.subject, scope, luaL_checkstring(state, 2), &style),
|
||||
"ui register_styles");
|
||||
push_ui_handle(state, vm, style, HandleKind::UiStyle);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int register_styles_file(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
const auto scope = style_scope(state, luaL_checkstring(state, 1));
|
||||
UiStyleHandle style = 0;
|
||||
check_result(state,
|
||||
svc_ui->register_styles_file(vm.subject, scope, luaL_checkstring(state, 2), &style),
|
||||
"ui register_styles_file");
|
||||
push_ui_handle(state, vm, style, HandleKind::UiStyle);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int register_menu_tab(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
const std::string label = get_optional_string(state, 1, "label");
|
||||
Callback& callback = retain_callback(vm);
|
||||
callback.refs[0] = ref_required_function(state, 1, "on_selected");
|
||||
UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT;
|
||||
desc.label = label.c_str();
|
||||
desc.on_selected = menu_selected;
|
||||
desc.user_data = &callback;
|
||||
UiMenuTabHandle tab = 0;
|
||||
check_result(state, svc_ui->register_menu_tab(vm.subject, &desc, &tab), "ui register_menu_tab");
|
||||
push_ui_handle(state, vm, tab, HandleKind::UiMenuTab);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int push_toast(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
luaL_checktype(state, 1, LUA_TTABLE);
|
||||
const std::string type = get_optional_string(state, 1, "type");
|
||||
const std::string title = get_optional_string(state, 1, "title_rml");
|
||||
const std::string body = get_optional_string(state, 1, "body_rml");
|
||||
UiToastDesc desc = UI_TOAST_DESC_INIT;
|
||||
desc.type = type.empty() ? nullptr : type.c_str();
|
||||
desc.title_rml = title.empty() ? nullptr : title.c_str();
|
||||
desc.body_rml = body.empty() ? nullptr : body.c_str();
|
||||
desc.duration_ms = static_cast<uint32_t>(get_optional_int(state, 1, "duration_ms", 0));
|
||||
check_result(state, svc_ui->push_toast(vm.subject, &desc), "ui push_toast");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_clipboard_text(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
size_t size = 0;
|
||||
check_result(
|
||||
state, svc_ui->get_clipboard_text(vm.subject, nullptr, 0, &size), "ui get_clipboard_text");
|
||||
std::vector<char> text(size + 1);
|
||||
check_result(state, svc_ui->get_clipboard_text(vm.subject, text.data(), text.size(), nullptr),
|
||||
"ui get_clipboard_text");
|
||||
lua_pushlstring(state, text.data(), size);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int set_clipboard_text(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
check_result(state, svc_ui->set_clipboard_text(vm.subject, luaL_checkstring(state, 1)),
|
||||
"ui set_clipboard_text");
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind) {
|
||||
push_handle(state, vm, value, kind, ui_metatable(kind));
|
||||
}
|
||||
|
||||
int open_ui(lua_State* state) {
|
||||
Vm& vm = vm_from_upvalue(state);
|
||||
if (svc_ui == nullptr) {
|
||||
service_unavailable(state, "UiService");
|
||||
}
|
||||
static const luaL_Reg kElementMethods[] = {
|
||||
{"add_section", pane_add_section},
|
||||
{"add_text", pane_add_text},
|
||||
{"add_rml", pane_add_rml},
|
||||
{"add_progress", pane_add_progress},
|
||||
{"add_control", pane_add_control},
|
||||
{"add_group", pane_add_group},
|
||||
{"add_list", pane_add_list},
|
||||
{"set_text", element_set_text},
|
||||
{"set_rml", element_set_rml},
|
||||
{"set_progress", element_set_progress},
|
||||
{"set_class", element_set_class},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
static const luaL_Reg kWindowMethods[] = {{"close", window_close}, {nullptr, nullptr}};
|
||||
static const luaL_Reg kDialogMethods[] = {
|
||||
{"close", dialog_close},
|
||||
{"set_body", dialog_set_body},
|
||||
{"set_icon", dialog_set_icon},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
static const luaL_Reg kStyleMethods[] = {{"unregister", style_unregister}, {nullptr, nullptr}};
|
||||
static const luaL_Reg kMenuMethods[] = {
|
||||
{"unregister", menu_tab_unregister}, {nullptr, nullptr}};
|
||||
static const luaL_Reg kListMethods[] = {{"set_items", list_set_items}, {nullptr, nullptr}};
|
||||
create_handle_metatable(state, kUiElementMetatable, kElementMethods, "UiElement");
|
||||
create_handle_metatable(state, kUiWindowMetatable, kWindowMethods, "UiWindow");
|
||||
create_handle_metatable(state, kUiDialogMetatable, kDialogMethods, "UiDialog");
|
||||
create_handle_metatable(state, kUiStyleMetatable, kStyleMethods, "UiStyle");
|
||||
create_handle_metatable(state, kUiMenuTabMetatable, kMenuMethods, "UiMenuTab");
|
||||
create_handle_metatable(state, kUiListMetatable, kListMethods, "UiList");
|
||||
|
||||
lua_newtable(state);
|
||||
set_function(state, vm, "register_mods_panel", register_mods_panel);
|
||||
set_function(state, vm, "window_push", window_push);
|
||||
set_function(state, vm, "dialog_push", dialog_push);
|
||||
set_function(state, vm, "is_any_document_visible", is_any_document_visible);
|
||||
set_function(state, vm, "register_styles", register_styles);
|
||||
set_function(state, vm, "register_styles_file", register_styles_file);
|
||||
set_function(state, vm, "register_menu_tab", register_menu_tab);
|
||||
set_function(state, vm, "push_toast", push_toast);
|
||||
set_function(state, vm, "get_clipboard_text", get_clipboard_text);
|
||||
set_function(state, vm, "set_clipboard_text", set_clipboard_text);
|
||||
lua_setreadonly(state, -1, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace luau_runtime
|
||||
@@ -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;
|
||||
@@ -19,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. */
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
--!strict
|
||||
|
||||
export type IntegerInput = integer | number
|
||||
export type ConfigValue = boolean | integer | number | string
|
||||
export type ConfigVar<T> = {
|
||||
get: (self: ConfigVar<T>) -> T,
|
||||
set: (self: ConfigVar<T>, value: T) -> (),
|
||||
subscribe: (self: ConfigVar<T>, callback: (value: T, previous: T) -> ()) -> ConfigSubscription,
|
||||
unregister: (self: ConfigVar<T>) -> (),
|
||||
}
|
||||
export type ConfigSubscription = {
|
||||
unsubscribe: (self: ConfigSubscription) -> (),
|
||||
}
|
||||
export type ConfigDesc = {
|
||||
name: string,
|
||||
type: "bool" | "int" | "float" | "string",
|
||||
default: ConfigValue?,
|
||||
}
|
||||
export type ConfigModule = {
|
||||
register: (desc: ConfigDesc) -> ConfigVar<any>,
|
||||
subscribe: <T>(variable: ConfigVar<T>, callback: (value: T, previous: T) -> ()) -> ConfigSubscription,
|
||||
}
|
||||
|
||||
export type OverlayHandle = { remove: (self: OverlayHandle) -> () }
|
||||
export type OverlayModule = {
|
||||
add_file: (discPath: string, bundlePath: string) -> OverlayHandle,
|
||||
add_buffer: (discPath: string, data: string) -> OverlayHandle,
|
||||
}
|
||||
export type TextureHandle = { unregister: (self: TextureHandle) -> () }
|
||||
export type TextureKey = {
|
||||
texture_hash: integer,
|
||||
tlut_hash: integer?,
|
||||
width: IntegerInput,
|
||||
height: IntegerInput,
|
||||
gx_format: IntegerInput,
|
||||
has_tlut: boolean?,
|
||||
}
|
||||
export type TextureData = {
|
||||
data: string,
|
||||
width: IntegerInput?,
|
||||
height: IntegerInput?,
|
||||
mip_count: IntegerInput?,
|
||||
gx_format: IntegerInput?,
|
||||
}
|
||||
export type TextureModule = {
|
||||
hash_wildcard: integer,
|
||||
tlut_wildcard: integer,
|
||||
register_file: (bundlePath: string) -> TextureHandle,
|
||||
register_data: (key: TextureKey, data: TextureData) -> TextureHandle,
|
||||
}
|
||||
|
||||
export type UiElement = {
|
||||
add_section: (self: UiElement, title: string) -> (),
|
||||
add_text: (self: UiElement, text: string) -> UiElement,
|
||||
add_rml: (self: UiElement, rml: string) -> UiElement,
|
||||
add_progress: (self: UiElement, value: number) -> UiElement,
|
||||
add_control: (self: UiElement, desc: UiControlDesc) -> UiElement,
|
||||
add_group: (self: UiElement, target: UiElement, desc: UiGroupDesc) -> UiElement,
|
||||
add_list: (self: UiElement, desc: UiListDesc) -> UiList,
|
||||
set_text: (self: UiElement, text: string) -> (),
|
||||
set_rml: (self: UiElement, rml: string) -> (),
|
||||
set_progress: (self: UiElement, value: number) -> (),
|
||||
set_class: (self: UiElement, name: string, active: boolean) -> (),
|
||||
}
|
||||
export type UiControlDesc = {
|
||||
kind: "button" | "toggle" | "number" | "string" | "select" | "color" | "group" | "file_picker",
|
||||
label: string,
|
||||
help_rml: string?,
|
||||
config_var: ConfigVar<any>?,
|
||||
get: (() -> ConfigValue)?,
|
||||
set: ((value: ConfigValue) -> ())?,
|
||||
on_pressed: (() -> ())?,
|
||||
is_disabled: (() -> boolean)?,
|
||||
is_modified: (() -> boolean)?,
|
||||
is_selected: (() -> boolean)?,
|
||||
min: IntegerInput?,
|
||||
max: IntegerInput?,
|
||||
step: IntegerInput?,
|
||||
prefix: string?,
|
||||
suffix: string?,
|
||||
options: {string}?,
|
||||
max_length: IntegerInput?,
|
||||
color_presets: {string}?,
|
||||
color_alpha: boolean?,
|
||||
string_set_mode: ("commit" | "change")?,
|
||||
file_filters: {{name: string, pattern: string}}?,
|
||||
directory_mode: boolean?,
|
||||
}
|
||||
export type UiGroupDesc = { label: string, build: (pane: UiElement) -> () }
|
||||
export type UiListItem = { key: IntegerInput, label: string }
|
||||
export type UiListDesc = {
|
||||
items: {UiListItem}?,
|
||||
on_pressed: (list: UiList, key: integer) -> (),
|
||||
is_selected: ((list: UiList, key: integer) -> boolean)?,
|
||||
is_disabled: ((list: UiList, key: integer) -> boolean)?,
|
||||
}
|
||||
export type UiList = { set_items: (self: UiList, items: {UiListItem}) -> () }
|
||||
export type UiWindow = { close: (self: UiWindow) -> () }
|
||||
export type UiDialog = {
|
||||
close: (self: UiDialog) -> (),
|
||||
set_body: (self: UiDialog, rml: string) -> (),
|
||||
set_icon: (self: UiDialog, icon: string) -> (),
|
||||
}
|
||||
export type UiStyle = { unregister: (self: UiStyle) -> () }
|
||||
export type UiMenuTab = { unregister: (self: UiMenuTab) -> () }
|
||||
export type UiTabDesc = {
|
||||
title: string,
|
||||
build: (window: UiWindow, left: UiElement, right: UiElement) -> (),
|
||||
update: (() -> ())?,
|
||||
}
|
||||
export type UiModule = {
|
||||
register_mods_panel: (desc: {build: (panel: UiElement) -> (), update: (() -> ())?}) -> (),
|
||||
window_push: (desc: {tabs: {UiTabDesc}, rcss: string?, on_closed: ((window: UiWindow) -> ())?}) -> UiWindow,
|
||||
dialog_push: (desc: {
|
||||
title: string,
|
||||
body_rml: string,
|
||||
variant: ("normal" | "warning" | "danger")?,
|
||||
icon: string?,
|
||||
actions: {{label: string, on_pressed: ((dialog: UiDialog) -> ())?, keep_open: boolean?, is_disabled: (() -> boolean)?}},
|
||||
on_dismiss: ((dialog: UiDialog) -> ())?,
|
||||
build: ((pane: UiElement) -> ())?,
|
||||
}) -> UiDialog,
|
||||
is_any_document_visible: () -> boolean,
|
||||
register_styles: (scope: string, rcss: string) -> UiStyle,
|
||||
register_styles_file: (scope: string, path: string) -> UiStyle,
|
||||
register_menu_tab: (desc: {label: string, on_selected: () -> ()}) -> UiMenuTab,
|
||||
push_toast: (desc: {type: string?, title_rml: string?, body_rml: string?, duration_ms: IntegerInput?}) -> (),
|
||||
get_clipboard_text: () -> string,
|
||||
set_clipboard_text: (text: string) -> (),
|
||||
}
|
||||
|
||||
export type LogModule = {
|
||||
write: (level: "trace" | "debug" | "info" | "warn" | "error", message: string) -> (),
|
||||
trace: (message: string) -> (),
|
||||
debug: (message: string) -> (),
|
||||
info: (message: string) -> (),
|
||||
warn: (message: string) -> (),
|
||||
error: (message: string) -> (),
|
||||
}
|
||||
export type HostModule = {
|
||||
version: string,
|
||||
mod_id: () -> string,
|
||||
mod_name: () -> string,
|
||||
mod_version: () -> string,
|
||||
mod_dir: () -> string,
|
||||
data_dir: () -> string,
|
||||
on_update: (callback: () -> ()) -> (),
|
||||
on_shutdown: (callback: () -> ()) -> (),
|
||||
fail: (message: string) -> never,
|
||||
}
|
||||
export type ResourceModule = { load: (relativePath: string) -> string }
|
||||
|
||||
declare function require(path: "dusklight.log"): LogModule
|
||||
declare function require(path: "dusklight.host"): HostModule
|
||||
declare function require(path: "dusklight.config"): ConfigModule
|
||||
declare function require(path: "dusklight.resource"): ResourceModule
|
||||
declare function require(path: "dusklight.overlay"): OverlayModule
|
||||
declare function require(path: "dusklight.texture"): TextureModule
|
||||
declare function require(path: "dusklight.ui"): UiModule
|
||||
declare function require(path: string): any
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "com.example.my_script_mod",
|
||||
"name": "My Script Mod",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "A Luau script mod",
|
||||
"runtime": "dev.twilitrealm.luau@1.0"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
local log = require("dusklight.log")
|
||||
local host = require("dusklight.host")
|
||||
|
||||
log.info("My Script Mod initialized")
|
||||
|
||||
host.on_shutdown(function()
|
||||
log.info("My Script Mod shutting down")
|
||||
end)
|
||||
+18
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/config_var.hpp"
|
||||
#include "mods/api.h"
|
||||
#include "mods/runtime.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
@@ -35,6 +37,7 @@ struct ModManifestInfo {
|
||||
struct Import {
|
||||
std::string id;
|
||||
uint16_t major = 0;
|
||||
uint16_t minMinor = 0;
|
||||
bool required = false;
|
||||
bool operator==(const Import&) const = default;
|
||||
};
|
||||
@@ -48,6 +51,19 @@ struct ModManifestInfo {
|
||||
bool operator==(const ModManifestInfo&) const = default;
|
||||
};
|
||||
|
||||
struct DelegatedModRuntime {
|
||||
std::string id;
|
||||
uint16_t major = 0;
|
||||
uint16_t minMinor = 0;
|
||||
|
||||
const ModRuntimeService* service = nullptr;
|
||||
ModContext* providerContext = nullptr;
|
||||
|
||||
bool operator==(const DelegatedModRuntime& other) const {
|
||||
return id == other.id && major == other.major && minMinor == other.minMinor;
|
||||
}
|
||||
};
|
||||
|
||||
struct ModMetadata {
|
||||
std::string id;
|
||||
std::string name;
|
||||
@@ -180,7 +196,7 @@ struct LoadedMod {
|
||||
bool loadFailed = false;
|
||||
std::string failureReason;
|
||||
|
||||
// mod_initialize succeeded; a mod_shutdown is owed on deactivation.
|
||||
// Initialization succeeded; shutdown is owed on deactivation.
|
||||
bool initialized = false;
|
||||
// Static service exports are currently present in the registry.
|
||||
bool servicesRegistered = false;
|
||||
@@ -202,6 +218,7 @@ struct LoadedMod {
|
||||
|
||||
NativeModStatus nativeStatus = NativeModStatus::None;
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::optional<DelegatedModRuntime> runtime;
|
||||
std::unique_ptr<ModContext> context;
|
||||
|
||||
// Shared with overlay file registrations so in-flight DVD reads survive disable/reload.
|
||||
|
||||
+176
-44
@@ -5,6 +5,7 @@
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
@@ -269,7 +270,57 @@ static std::string resolve_image_path(ModBundle& bundle, const std::string& modI
|
||||
return {};
|
||||
}
|
||||
|
||||
static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle& bundle) {
|
||||
struct LoadedManifest {
|
||||
ModMetadata metadata;
|
||||
std::optional<DelegatedModRuntime> runtime;
|
||||
};
|
||||
|
||||
static uint16_t parse_runtime_version_component(std::string_view text, std::string_view fieldName) {
|
||||
uint32_t value = 0;
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (text.empty() || error != std::errc{} || end != text.data() + text.size() ||
|
||||
value > UINT16_MAX)
|
||||
{
|
||||
throw InvalidModDataException(fmt::format("Invalid {} in runtime version pin", fieldName));
|
||||
}
|
||||
return static_cast<uint16_t>(value);
|
||||
}
|
||||
|
||||
static std::optional<DelegatedModRuntime> parse_runtime(const nlohmann::json& manifest) {
|
||||
const auto field = manifest.find("runtime");
|
||||
if (field == manifest.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!field->is_string()) {
|
||||
throw InvalidModDataException("runtime must be a string");
|
||||
}
|
||||
|
||||
const std::string pin = field->get<std::string>();
|
||||
const auto at = pin.rfind('@');
|
||||
if (at == std::string::npos || at == 0 || at + 1 == pin.size() || pin.find('@') != at ||
|
||||
at >= MOD_META_SERVICE_ID_SIZE)
|
||||
{
|
||||
throw InvalidModDataException(
|
||||
"runtime must be a service id followed by @major or @major.minor");
|
||||
}
|
||||
|
||||
const std::string_view version{pin.data() + at + 1, pin.size() - at - 1};
|
||||
const auto dot = version.find('.');
|
||||
if (dot != std::string_view::npos && version.find('.', dot + 1) != std::string_view::npos) {
|
||||
throw InvalidModDataException("runtime version pin has too many components");
|
||||
}
|
||||
|
||||
DelegatedModRuntime result;
|
||||
result.id = pin.substr(0, at);
|
||||
result.major = parse_runtime_version_component(
|
||||
dot == std::string_view::npos ? version : version.substr(0, dot), "major version");
|
||||
if (dot != std::string_view::npos) {
|
||||
result.minMinor = parse_runtime_version_component(version.substr(dot + 1), "minor version");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle) {
|
||||
const auto metaJson = bundle.readFile("mod.json");
|
||||
auto j = nlohmann::json::parse(metaJson);
|
||||
|
||||
@@ -297,14 +348,18 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle
|
||||
std::string bannerPath =
|
||||
resolve_image_path(bundle, metaId, "banner", metaBanner, "res/banner.png"s);
|
||||
|
||||
return ModMetadata{
|
||||
std::move(metaId),
|
||||
std::move(metaName),
|
||||
std::move(metaVersion),
|
||||
std::move(metaAuthor),
|
||||
std::move(metaDescription),
|
||||
std::move(iconPath),
|
||||
std::move(bannerPath),
|
||||
return LoadedManifest{
|
||||
.metadata =
|
||||
{
|
||||
std::move(metaId),
|
||||
std::move(metaName),
|
||||
std::move(metaVersion),
|
||||
std::move(metaAuthor),
|
||||
std::move(metaDescription),
|
||||
std::move(iconPath),
|
||||
std::move(bannerPath),
|
||||
},
|
||||
.runtime = parse_runtime(j),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -517,8 +572,8 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod)
|
||||
if (libDir.empty()) {
|
||||
return {};
|
||||
}
|
||||
fs::path path = libDir / fs::path(mod.metadata.id +
|
||||
borealis::io::fs_path_to_string(fs::path(k_nativeLibName).extension()));
|
||||
fs::path path = libDir / fs::path(mod.metadata.id + borealis::io::fs_path_to_string(
|
||||
fs::path(k_nativeLibName).extension()));
|
||||
std::error_code ec;
|
||||
if (!fs::is_regular_file(path, ec)) {
|
||||
return {};
|
||||
@@ -684,6 +739,13 @@ bool ModLoader::load_native_if_present(LoadedMod& mod) {
|
||||
}
|
||||
|
||||
const auto& native = std::get<NativeRuntimeLocation>(result);
|
||||
if (mod.runtime.has_value() &&
|
||||
(native.anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())))
|
||||
{
|
||||
mod.nativeStatus = NativeModStatus::InvalidBundle;
|
||||
fail_mod(mod, MOD_CONFLICT, "A mod cannot declare both runtime and native code");
|
||||
return false;
|
||||
}
|
||||
if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) {
|
||||
mod.nativeStatus = NativeModStatus::None;
|
||||
return true;
|
||||
@@ -728,7 +790,7 @@ static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) {
|
||||
continue;
|
||||
}
|
||||
info.imports.push_back({record->service_id.chars, record->major_version,
|
||||
(record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0});
|
||||
record->min_minor_version, (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0});
|
||||
}
|
||||
info.exports.reserve(parsed.exports.size());
|
||||
for (const auto* record : parsed.exports) {
|
||||
@@ -801,21 +863,22 @@ void ModLoader::try_load_mod(
|
||||
return;
|
||||
}
|
||||
|
||||
ModMetadata metadata;
|
||||
LoadedManifest manifest;
|
||||
try {
|
||||
metadata = load_metadata(modPath, *bundle);
|
||||
manifest = load_manifest(modPath, *bundle);
|
||||
} catch (const std::exception& e) {
|
||||
Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto* existing = find_mod(metadata.id)) {
|
||||
if (const auto* existing = find_mod(manifest.metadata.id)) {
|
||||
if (existing->searchDirIndex < searchDirIndex) {
|
||||
log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority duplicate {}",
|
||||
log::write(manifest.metadata.id, LOG_LEVEL_INFO,
|
||||
"{} shadowed by higher-priority duplicate {}",
|
||||
data::abbreviated_path_string(modPath),
|
||||
data::abbreviated_path_string(existing->modPath));
|
||||
} else {
|
||||
log::write(metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}",
|
||||
log::write(manifest.metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}",
|
||||
data::abbreviated_path_string(modPath));
|
||||
}
|
||||
return;
|
||||
@@ -827,12 +890,29 @@ void ModLoader::try_load_mod(
|
||||
mod.modPath = fs::absolute(modPath);
|
||||
mod.searchDirIndex = searchDirIndex;
|
||||
mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir;
|
||||
mod.metadata = std::move(metadata);
|
||||
mod.metadata = std::move(manifest.metadata);
|
||||
mod.runtime = std::move(manifest.runtime);
|
||||
mod.bundle = std::move(bundle);
|
||||
mod.context = std::make_unique<ModContext>();
|
||||
mod.context->mod = &mod;
|
||||
mod.cvarIsEnabled =
|
||||
std::make_unique<ConfigVar<bool>>(mod_enabled_cvar_name(mod.metadata.id), true);
|
||||
if (mod.runtime.has_value()) {
|
||||
const auto& runtime = *mod.runtime;
|
||||
mod.manifestInfo.imports.push_back({runtime.id, runtime.major, runtime.minMinor, true});
|
||||
|
||||
std::error_code ec;
|
||||
mod.dir = fs::absolute(m_cacheDir / mod.metadata.id / "data", ec);
|
||||
if (!ec) {
|
||||
fs::create_directories(mod.dir, ec);
|
||||
}
|
||||
if (ec) {
|
||||
fail_mod(mod, MOD_ERROR,
|
||||
fmt::format("Failed to create script scratch directory: {}", ec.message()));
|
||||
} else {
|
||||
mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir);
|
||||
}
|
||||
}
|
||||
if (load_native_if_present(mod) && mod.native) {
|
||||
mod.manifestInfo = build_manifest_info(mod.native->parsed);
|
||||
}
|
||||
@@ -846,12 +926,12 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
|
||||
mod.active = true;
|
||||
|
||||
// Asset-only mods have no lifecycle beyond their overlay files.
|
||||
if (!mod.native) {
|
||||
if (!mod.native && !mod.runtime.has_value()) {
|
||||
mod.enabledApplied = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mod.servicesRegistered) {
|
||||
if (mod.native && !mod.servicesRegistered) {
|
||||
if (!register_static_service_exports(mod)) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to register service exports");
|
||||
deactivate_mod(mod);
|
||||
@@ -860,30 +940,60 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
|
||||
mod.servicesRegistered = true;
|
||||
}
|
||||
|
||||
if (!resolve_service_imports(mod)) {
|
||||
if (mod.native && !resolve_service_imports(mod)) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to resolve service imports");
|
||||
deactivate_mod(mod);
|
||||
return false;
|
||||
}
|
||||
|
||||
svc::hook_resolve_mod_records(mod);
|
||||
if (mod.native) {
|
||||
svc::hook_resolve_mod_records(mod);
|
||||
*mod.native->contextSymbol = mod.context.get();
|
||||
} else {
|
||||
auto& runtime = *mod.runtime;
|
||||
const auto* record = svc::find_service(runtime.id.c_str(), runtime.major, runtime.minMinor);
|
||||
if (record == nullptr) {
|
||||
fail_mod(mod, MOD_UNAVAILABLE,
|
||||
describe_missing_import(runtime.id.c_str(), runtime.major, runtime.minMinor));
|
||||
deactivate_mod(mod);
|
||||
return false;
|
||||
}
|
||||
|
||||
*mod.native->contextSymbol = mod.context.get();
|
||||
const auto* service = static_cast<const ModRuntimeService*>(record->service);
|
||||
constexpr size_t kRequiredSize = offsetof(ModRuntimeService, deactivate) +
|
||||
sizeof(((ModRuntimeService*)nullptr)->deactivate);
|
||||
if (record->provider == nullptr || service == nullptr ||
|
||||
service->header.struct_size < kRequiredSize || service->activate == nullptr ||
|
||||
service->update == nullptr || service->deactivate == nullptr)
|
||||
{
|
||||
fail_mod(mod, MOD_UNAVAILABLE,
|
||||
fmt::format("Runtime service {}@{} has an invalid lifecycle contract", runtime.id,
|
||||
runtime.major));
|
||||
deactivate_mod(mod);
|
||||
return false;
|
||||
}
|
||||
runtime.service = service;
|
||||
runtime.providerContext = record->provider->context.get();
|
||||
}
|
||||
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize");
|
||||
const char* initializeName = mod.native ? "mod_initialize" : "runtime activate";
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling {}", initializeName);
|
||||
try {
|
||||
ModError error = MOD_ERROR_INIT;
|
||||
const auto result = mod.native->fn_initialize(&error);
|
||||
const auto result = mod.native ?
|
||||
mod.native->fn_initialize(&error) :
|
||||
mod.runtime->service->activate(
|
||||
mod.runtime->providerContext, mod.context.get(), &error);
|
||||
if (result == MOD_OK && !mod.loadFailed) {
|
||||
mod.initialized = true;
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "mod_initialize succeeded");
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "{} succeeded", initializeName);
|
||||
} else if (result != MOD_OK && !mod.loadFailed) {
|
||||
fail_mod(mod, result, lifecycle_error_message("mod_initialize", result, error));
|
||||
fail_mod(mod, result, lifecycle_error_message(initializeName, result, error));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod_initialize: {}", e.what()));
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("Exception in {}: {}", initializeName, e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(mod, MOD_ERROR, "Unknown exception in mod_initialize");
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("Unknown exception in {}", initializeName));
|
||||
}
|
||||
|
||||
warn_unpublished_deferred_exports(mod);
|
||||
@@ -900,21 +1010,31 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
|
||||
|
||||
void ModLoader::deactivate_mod(LoadedMod& mod) {
|
||||
svc::modules_mod_deactivating(mod);
|
||||
if (mod.initialized && mod.native && mod.native->fn_shutdown) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_shutdown");
|
||||
if (mod.initialized && ((mod.native && mod.native->fn_shutdown) ||
|
||||
(mod.runtime.has_value() && mod.runtime->service != nullptr)))
|
||||
{
|
||||
const char* shutdownName = mod.native ? "mod_shutdown" : "runtime deactivate";
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling {}", shutdownName);
|
||||
try {
|
||||
ModError error = MOD_ERROR_INIT;
|
||||
const auto result = mod.native->fn_shutdown(&error);
|
||||
const auto result = mod.native ?
|
||||
mod.native->fn_shutdown(&error) :
|
||||
mod.runtime->service->deactivate(
|
||||
mod.runtime->providerContext, mod.context.get(), &error);
|
||||
if (result == MOD_OK) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "mod_shutdown succeeded");
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "{} succeeded", shutdownName);
|
||||
} else {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_shutdown failed: {}",
|
||||
lifecycle_error_message("mod_shutdown", result, error));
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{} failed: {}", shutdownName,
|
||||
lifecycle_error_message(shutdownName, result, error));
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
mod.initialized = false;
|
||||
if (mod.runtime.has_value()) {
|
||||
mod.runtime->service = nullptr;
|
||||
mod.runtime->providerContext = nullptr;
|
||||
}
|
||||
|
||||
if (mod.servicesRegistered) {
|
||||
svc::remove_services_for_provider(mod);
|
||||
@@ -1168,29 +1288,35 @@ bool ModLoader::reload_bundle(LoadedMod& mod) {
|
||||
data::abbreviated_path_string(mod.modPath));
|
||||
|
||||
std::shared_ptr<ModBundle> newBundle;
|
||||
ModMetadata newMetadata;
|
||||
LoadedManifest newManifest;
|
||||
try {
|
||||
std::error_code ec;
|
||||
newBundle = load_bundle(mod.modPath, fs::is_directory(mod.modPath, ec));
|
||||
newMetadata = load_metadata(mod.modPath, *newBundle);
|
||||
newManifest = load_manifest(mod.modPath, *newBundle);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("Reload failed: {}", e.what()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newMetadata.id != mod.metadata.id) {
|
||||
if (newManifest.metadata.id != mod.metadata.id) {
|
||||
fail_mod(mod, MOD_CONFLICT,
|
||||
fmt::format("Mod ID changed on reload ('{}'); restart required", newMetadata.id));
|
||||
fmt::format(
|
||||
"Mod ID changed on reload ('{}'); restart required", newManifest.metadata.id));
|
||||
return false;
|
||||
}
|
||||
|
||||
mod.metadata = std::move(newMetadata);
|
||||
mod.metadata = std::move(newManifest.metadata);
|
||||
mod.runtime = std::move(newManifest.runtime);
|
||||
// In-flight readers of the old bundle keep it alive through their shared_ptr.
|
||||
mod.bundle = std::move(newBundle);
|
||||
mod.loadFailed = false;
|
||||
mod.failureReason.clear();
|
||||
|
||||
ModManifestInfo newInfo;
|
||||
if (mod.runtime.has_value()) {
|
||||
const auto& runtime = *mod.runtime;
|
||||
newInfo.imports.push_back({runtime.id, runtime.major, runtime.minMinor, true});
|
||||
}
|
||||
if (!load_native_if_present(mod)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1364,17 +1490,23 @@ void ModLoader::tick() {
|
||||
apply_pending_requests();
|
||||
|
||||
for (auto& mod : mods()) {
|
||||
if (!mod.active || !mod.native) {
|
||||
if (!mod.active || (!mod.native && !mod.runtime.has_value())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ModError error = MOD_ERROR_INIT;
|
||||
const auto result = mod.native->fn_update(&error);
|
||||
const bool delegated = !mod.native;
|
||||
const auto result = delegated ?
|
||||
mod.runtime->service->update(
|
||||
mod.runtime->providerContext, mod.context.get(), &error) :
|
||||
mod.native->fn_update(&error);
|
||||
if (result != MOD_OK) {
|
||||
fail_mod(mod, result, lifecycle_error_message("mod_update", result, error));
|
||||
fail_mod(mod, result,
|
||||
lifecycle_error_message(
|
||||
delegated ? "runtime update" : "mod_update", result, error));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod_update: {}", e.what()));
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod update: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(mod, MOD_ERROR, "Unknown exception in mod_update");
|
||||
}
|
||||
|
||||
@@ -915,7 +915,7 @@ ModResult ui_window_close(LoadedMod& mod, uint64_t handle) {
|
||||
if (slot == nullptr || slot->document == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
slot->document->hide(true);
|
||||
slot->document->pop();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -1164,6 +1164,7 @@ void ui_remove_mod(LoadedMod& mod) {
|
||||
if (s_modMenuTabs.erase(&mod) != 0) {
|
||||
s_menuTabsDirty = true;
|
||||
}
|
||||
bool restoreCoveredDocument = false;
|
||||
auto entries = s_slots.take_all(mod);
|
||||
for (auto& entry : entries) {
|
||||
auto& slot = entry.value;
|
||||
@@ -1171,6 +1172,7 @@ void ui_remove_mod(LoadedMod& mod) {
|
||||
case UiSlotKind::Window: {
|
||||
auto* window = static_cast<ui::ModWindow*>(slot.document);
|
||||
if (window != nullptr) {
|
||||
restoreCoveredDocument |= ui::top_document() == window;
|
||||
window->force_hide(true);
|
||||
}
|
||||
break;
|
||||
@@ -1178,6 +1180,7 @@ void ui_remove_mod(LoadedMod& mod) {
|
||||
case UiSlotKind::Dialog: {
|
||||
auto* dialog = static_cast<ModDialog*>(slot.document);
|
||||
if (dialog != nullptr) {
|
||||
restoreCoveredDocument |= ui::top_document() == dialog;
|
||||
dialog->force_hide(true);
|
||||
}
|
||||
break;
|
||||
@@ -1189,6 +1192,9 @@ void ui_remove_mod(LoadedMod& mod) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (restoreCoveredDocument) {
|
||||
ui::uncover_top_document();
|
||||
}
|
||||
}
|
||||
|
||||
ModResult ui_get_clipboard_text(
|
||||
|
||||
Reference in New Issue
Block a user