docs(modules): module-rust supports Carbon too, ships a Pterodactyl egg, and the rigs move to the panel #253
@@ -1,10 +1,10 @@
|
||||
# `module-rust` — the plan
|
||||
|
||||
**Status:** phases 0 and 1 done, 2026-09-15. **Twenty-one decisions of record; one outstanding
|
||||
request, no open questions.** Audited against the whole contract, not just the game-facing chapters
|
||||
**Status:** phases 0 and 1 done, 2026-09-15. **Twenty-two decisions of record, no open questions.** Audited against the whole contract, not just the game-facing chapters
|
||||
(§7); the event and engagement catalogues are §9 and §10; §11 is a second pass over `MODULE_API.md`
|
||||
itself. **R19–R21 (2026-09-15) added a second modding framework, a Pterodactyl egg, and moved the
|
||||
rigs off the workstation** — see [`CARBON.md`](CARBON.md) for the framework reference.
|
||||
itself. **R19–R22 (2026-09-15) added a second modding framework, a Pterodactyl egg, moved the rigs off
|
||||
the workstation, and put the sidecar's configuration in the egg** — see [`CARBON.md`](CARBON.md) for
|
||||
the framework reference and §14 for the rig as built.
|
||||
|
||||
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it.
|
||||
This is the document that builds it. Where the two disagree, this one is later and wins — but the dry
|
||||
@@ -810,39 +810,83 @@ one. So the deployment loop is three tiers, matched to what each is for:
|
||||
| **Working-tree iteration** — an uncommitted `.cs` under test | A **client** API key (`ptlc_…`): `files/write`, then `command` to reload | The Pterodactyl analogue of `servuo-plugins/deploy.ps1`, and it carries the same caveat: **if something only works when the push script copies it, it does not ship** |
|
||||
| **Bulk or binary** — sidecar builds, world files | SFTP on the node, port 2022 | Where the client API's per-file write is the wrong shape |
|
||||
|
||||
**The client key now exists beside the application one** — the token file holds both, as
|
||||
`application:` and `user:` lines — but at the time of writing it authenticates and then sees **no
|
||||
servers**, so tier 2 is not yet usable. §3 has the diagnosis. Until it is, iteration is
|
||||
reinstall-only: correct, and slow enough that nobody would choose it twice. The push script itself
|
||||
**Both keys exist and both were exercised on 2026-09-15** — the token file holds them as
|
||||
`application:` and `user:` lines, and §14 records the rig they built together. The push script itself
|
||||
lives in **`Rust-Plugins`**, mirroring where `deploy.ps1` lives for ServUO.
|
||||
|
||||
### R22 — the sidecar is configured from the egg's variables, not from a file the operator edits
|
||||
|
||||
**Decided 2026-09-15 (org lead).** What normally lives in `sidecar.toml` moves into the Rust egg's
|
||||
variables, so an operator on Pterodactyl configures the sidecar in the panel alongside the game's own
|
||||
settings rather than opening a file manager to edit TOML. One configuration surface, in the place
|
||||
they are already looking.
|
||||
|
||||
**This is nearly free, because the sidecar already does it.** `rust-link`'s `config.rs` documents its
|
||||
precedence as *environment overrides file overrides defaults* and already reads all five keys from
|
||||
the environment: `RUSTLINK_GAME_BIND`, `RUSTLINK_SERVER_ID`, `RUSTLINK_WEB_BIND`,
|
||||
`RUSTLINK_WEB_TOKEN`, `RUSTLINK_DB_PATH` (plus `RUSTLINK_CONFIG` for the file's own path).
|
||||
Pterodactyl exposes every egg variable to the container as an environment variable, so the mapping is
|
||||
one-to-one and **no second configuration mechanism is introduced** — the file stays canonical, the
|
||||
environment overrides it, the egg sets the environment, and the installer (R4) keeps writing the file
|
||||
exactly as it does now.
|
||||
|
||||
Which gives the two halves of the shard side two different config surfaces, deliberately:
|
||||
|
||||
| | Configured from | Mechanism |
|
||||
|---|---|---|
|
||||
| The **plugin** | the website, Admin → the R18 config editor | D3: it reads `oxide/config/RunicGateway.json`, so it is inside R18 for free |
|
||||
| The **sidecar** | the panel, as egg variables | R22: `RUSTLINK_*` in the container environment |
|
||||
|
||||
That split is right rather than merely convenient. The plugin is configured by the thing it talks to;
|
||||
the sidecar is configured by the thing that starts it, and on a panel the operator has no shell.
|
||||
|
||||
**Three things the variable set has to get right**, each of which is a way to hand somebody a footgun:
|
||||
|
||||
- **`RUSTLINK_GAME_BIND` is not operator-editable.** D2 makes loopback the authentication on the game
|
||||
link; a panel field that accepts `0.0.0.0:7799` is a web form that puts an unauthenticated command
|
||||
channel on the network. It is set by the egg and marked neither viewable nor editable — the same
|
||||
posture R18 takes toward the plugin's own `Host`/`Port`, for the same reason.
|
||||
- **`RUSTLINK_WEB_BIND` is derived from an allocation, not typed.** It has to match the port the panel
|
||||
actually handed out, exactly as the egg already derives `QUERY_PORT` and `RCON_PORT`. A free-text
|
||||
bind is a bind that silently does not match the allocation, and the failure is the website never
|
||||
connecting with nothing in any log to say why.
|
||||
- **`RUSTLINK_DB_PATH` must point somewhere `REMOVE_FILES` never sweeps.** Already named in R20 and
|
||||
restated here because this is the decision that makes the path an operator-visible field: the wipe
|
||||
list and the database path become two settings on the same screen, and they must not be able to
|
||||
agree.
|
||||
|
||||
**The token is the one place the ergonomics are not automatic.** Today the sidecar generates a token
|
||||
when it finds none and persists it to its config file, which is what makes it secure out of the box;
|
||||
`--print-config` is how an operator reads it back. A panel variable cannot be filled in by the
|
||||
program that generates it, so the choices are: ship an empty default and let the sidecar generate and
|
||||
persist as it does now, with the operator reading it out of the panel's file manager once; or make
|
||||
the operator paste one in. The existing precedence already supports both — a set variable wins, an
|
||||
empty one falls through to generation — so this is a default to choose when the egg is built, not a
|
||||
mechanism to design. **Whichever is chosen, note that a Pterodactyl variable is visible to anyone
|
||||
with panel access to that server and appears in the container environment**, which is a different
|
||||
exposure from a `0600` file and should be stated in the operator guide rather than discovered.
|
||||
|
||||
**Lands in phase 18 with the rest of R20's egg.**
|
||||
|
||||
## 3. Open questions
|
||||
|
||||
**None — but one thing is unresolved on the panel.** R21's iteration loop needs a Pterodactyl
|
||||
**client** API key, and one now exists (`ptlc_…`, alongside the application key, both in
|
||||
`RunicGateway/pterodactyl_claude_api_token`). It authenticates correctly —
|
||||
`/api/client/account` returns user 1 `whitlocktech`, `root_admin: true`, the owner of every server on
|
||||
the node — **and then lists zero servers.** `/api/client` is `total: 0` for the default listing and
|
||||
for `type=admin` and `type=admin-all` alike, and every `/api/client/servers/{id}/…` route 404s for
|
||||
both the short identifier and the full UUID.
|
||||
**None.** Every question this section carried was closed on 2026-09-15, and so was the one open
|
||||
*request*: the token file now holds both keys, and **both were exercised end to end on 2026-09-15**
|
||||
(§14).
|
||||
|
||||
That is a panel-side condition, not a token-shape problem, and it is **not blocking the plan** — it
|
||||
blocks tier 2 of R21's deployment loop, leaving reinstall and SFTP working. Diagnosis so far, so that
|
||||
whoever picks it up does not repeat it:
|
||||
One correction belongs here rather than being quietly dropped, because the shape of the mistake is
|
||||
the reusable part. This section briefly recorded that the client key "authenticates and then lists
|
||||
zero servers", and built a diagnosis on top of it — including a claim that *includes are broken on
|
||||
this panel*, because `/api/application/servers?include=user` returned an empty list where the same
|
||||
route without the include had returned six.
|
||||
|
||||
- The key is **not** mis-scoped: `/api/client/account` and `/api/client/permissions` both answer 200.
|
||||
- The account is **not** missing ownership: `/api/application/servers` lists six servers, and server 7
|
||||
carries `"user": 1`.
|
||||
- **Includes are broken on this panel independently**, which is worth knowing before trusting a
|
||||
diagnosis built on them: `/api/application/servers?include=user` returns an *empty* list where the
|
||||
same route without the include returns six, and `/api/application/users/1?include=servers` reports
|
||||
zero servers owned. Neither is evidence of anything about ownership.
|
||||
**Both claims were wrong, and they were wrong the same way.** The servers were being deleted while
|
||||
the probing was happening, so two reads minutes apart were reads of two different worlds. The empty
|
||||
client list was correct. The empty include was correct. Nothing was broken.
|
||||
|
||||
The remaining candidates are a stale panel cache and the key's allowed-IP field. Worth noting that
|
||||
`gitea.whitlocktech.com` was returning 502 and Cloudflare 530 during the same window, so "something
|
||||
is wrong with that host right now" is a live hypothesis rather than a coincidence to dismiss.
|
||||
|
||||
Everything else in this section was closed on 2026-09-15.
|
||||
The lesson is not "check twice"; it is that **a differential diagnosis across two API calls silently
|
||||
assumes the state did not move between them**, and on a live panel somebody else is also holding the
|
||||
controls. Once a server existed, every one of those calls answered correctly on the first try.
|
||||
|
||||
*Clans in the base set while the Team provider reads first-party* was confirmed as the intended
|
||||
reading: complementary, not in conflict — the plugin is installed for alliances and clan chat, the
|
||||
@@ -952,7 +996,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
|
||||
| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral | Module-Rust + docs | A refusal does not go public in the channel |
|
||||
| 17 | **Optional mod integrations** (R15). **BetterChat** first — leaderboard titles through `API_RegisterThirdPartyTitle`, a pull with no drift — then the uMod **Clans** adapter (alliances and clan chat, beside the provider rather than under it, R5), then others as they prove useful | Rust-Plugins + Module-Rust + docs | A server missing every optional mod still runs the module, Teams included |
|
||||
| 18 | **The installer** (R4) **and the Pterodactyl egg** (R20) — the two halves of "how the shard side reaches an operator", built together so one story is not told twice. `--game servuo\|rust`, the bundle payload as a variant, a **framework** prerequisite check in `doctor` (which one, not whether Oxide — R19), the protocol pairing refusal carried over; the egg derived from "Rust Autowipe" with the sidecar inside the game container, a second allocation for `[web].bind`, the sidecar store held out of `REMOVE_FILES`, and its install script fetching the same pinned pair the installer resolves | installer + Rust-Link + docs | An operator sets a Rust server up with the released binary and nothing hand-copied; **and** a second operator imports the egg, deploys, and reaches the same place — on either framework |
|
||||
| 18 | **The installer** (R4) **and the Pterodactyl egg** (R20) — the two halves of "how the shard side reaches an operator", built together so one story is not told twice. `--game servuo\|rust`, the bundle payload as a variant, a **framework** prerequisite check in `doctor` (which one, not whether Oxide — R19), the protocol pairing refusal carried over; the egg derived from "Rust Autowipe" with the sidecar inside the game container, a second allocation for `[web].bind`, **the sidecar configured from egg variables** (R22), the sidecar store held out of `REMOVE_FILES`, and its install script fetching the same pinned pair the installer resolves | installer + Rust-Link + docs | An operator sets a Rust server up with the released binary and nothing hand-copied; **and** a second operator imports the egg, deploys, and reaches the same place — on either framework |
|
||||
| 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change, and Pterodactyl is a hosting claim the site can now make); the Integration-kit question R2 raised; and whether the kit owes a reader anything about **supporting two mod frameworks at once** (R19) — a shape it has no chapter for either | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way |
|
||||
|
||||
### Why the lease comes before the reward action
|
||||
@@ -1708,6 +1752,199 @@ permission gate, because every plugin's check short-circuits without a `BasePlay
|
||||
account bypasses most of them non-uniformly. A second, non-admin Steam account has to be arranged
|
||||
before phase 7 — it is the one prerequisite this rig cannot satisfy on its own.
|
||||
|
||||
## 14. The Pterodactyl rig as built, 2026-09-15
|
||||
|
||||
R21's first server exists, made with the application key and driven with the client key. **Both
|
||||
credentials work; neither can do the other's job.** What follows is what building it actually taught,
|
||||
including one finding that changes R20's shape.
|
||||
|
||||
### 14.0 The rig
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Panel | `http://192.168.0.12` (no TLS — `https://` fails outright), node 1 `Main` |
|
||||
| Server | `rust-oxide`, id **17**, identifier **`e6758c06`** |
|
||||
| Egg | 18 `Rust Autowipe`, `ghcr.io/pterodactyl/games:rust` |
|
||||
| `FRAMEWORK` | `oxide` |
|
||||
| Limits | 8192 MB memory, 25600 MB disk — deliberately under half the node, so the Carbon rig fits beside it |
|
||||
| Allocations | 21000 game (default), 21001 query, 21002 RCON, 21003 Rust+, **21004 held for the sidecar's `[web].bind`** |
|
||||
| World | procedural, size 3000, seed 1234 |
|
||||
| SFTP | `192.168.0.12:2022` |
|
||||
|
||||
The RCON password is a generated 24-byte token rather than the old rig's `letmein`, kept out of this
|
||||
document and out of the repo. §6 named that shape as the thing nothing published should copy; this is
|
||||
the first rig where it was not copied.
|
||||
|
||||
**A Rust server install is about 6 GB, not the ~20 GB this plan assumed** when it worried about node
|
||||
capacity — measured at 5,894 MB with the game installed and the world generating. Two rigs are
|
||||
comfortable on a 128 GB node, and the 25600 MB limit is generous rather than tight.
|
||||
|
||||
### 14.1 The two keys, and what each one is actually for
|
||||
|
||||
Confirmed by use rather than by reading:
|
||||
|
||||
| | Application (`ptla_`) | Client (`ptlc_`) |
|
||||
|---|---|---|
|
||||
| Create / configure a server, assign allocations | **yes** | no |
|
||||
| List, read, power, console, **files** | no (`403`) | **yes** |
|
||||
| Write or import an egg | **no** — `/api/application/eggs` 404s; eggs are an admin-UI or `php artisan` operation | no |
|
||||
|
||||
So the full loop needs both, and **a published egg is a JSON file a human imports** — which is also
|
||||
exactly how an operator will consume ours, so it is a constraint worth designing into rather than
|
||||
around.
|
||||
|
||||
**File operations are refused during install** with `409 ServerStateConflictException` —
|
||||
*"this server has not yet completed its installation process"*. Anything that pushes files has to
|
||||
wait for `is_installing: false`, not merely for the server to exist.
|
||||
|
||||
### 14.2 The correction: there was never a panel bug
|
||||
|
||||
An earlier pass through this section recorded that the client key "authenticates and then lists zero
|
||||
servers", and reasoned from there to a second claim — that *includes are broken on this panel*,
|
||||
because `/api/application/servers?include=user` returned an empty list where the same route without
|
||||
the include had returned six.
|
||||
|
||||
**Both were wrong, and wrong the same way.** The servers were being deleted while the probing
|
||||
happened, so two calls minutes apart read two different worlds. Once a server existed, every one of
|
||||
those calls answered correctly on the first attempt — the client list, the single-server route, and
|
||||
`include=user`.
|
||||
|
||||
The reusable part is not "check twice". It is that **a differential diagnosis across two API calls
|
||||
silently assumes the state did not move between them**, and on a live panel somebody else is also
|
||||
holding the controls.
|
||||
|
||||
### 14.2b The upload loop, proven with the real plugin
|
||||
|
||||
Not a hello-world: phase 1's actual `RunicGateway.cs` (27,642 bytes, 709 lines) was pushed straight
|
||||
from the working tree with the client key, and it came back byte-identical on read.
|
||||
|
||||
Three things that worked and were not certain to:
|
||||
|
||||
- **`files/write` creates missing parents.** `/oxide/plugins/` did not exist — the framework is laid
|
||||
down at boot (§14.3), and the server had never been started — and the write created the whole path.
|
||||
- **A plugin placed before Oxide exists survives Oxide arriving.** The entrypoint's `unzip -o` over
|
||||
`oxide/` left the file untouched, so the push does not have to wait for a first boot.
|
||||
- **It compiled and loaded on Linux**, which no previous phase had ever established. Every prior
|
||||
finding came from Windows and Mono:
|
||||
|
||||
```
|
||||
02:19 [Info] RunicGateway was compiled successfully in 0ms
|
||||
02:19 [Info] [Runic Gateway] protocol 1, serverId 'main', sidecar 127.0.0.1:7799
|
||||
02:19 [Info] Loaded plugin Runic Gateway v0.1.0 by RunicGateway
|
||||
02:19 [Info] [Runic Gateway] cannot reach the sidecar: Connection refused - retrying quietly
|
||||
```
|
||||
|
||||
That last line is phase 1's no-stall contract holding on a second platform: no sidecar exists on
|
||||
this host yet, the plugin says so once and keeps the game running.
|
||||
|
||||
**Read the console without a websocket.** Pterodactyl streams console over a websocket, which is
|
||||
awkward to drive from a script — but `wrapper.js` also writes `latest.log`, and Oxide writes
|
||||
`oxide/logs/oxide_<date>.txt`. Both are plain reads through `files/contents`, which is how every log
|
||||
line quoted in this section was obtained. Worth knowing before anyone writes a websocket client.
|
||||
|
||||
### 14.2c The tier-2 loop, end to end
|
||||
|
||||
R21's middle tier is the one that has to be pleasant to use, so it was run rather than described.
|
||||
One pass: patch the working-tree source so the change is visible in the game console, push, reload
|
||||
through the client API's `command` endpoint, read Oxide's log back, then restore.
|
||||
|
||||
```
|
||||
patched source: True
|
||||
push -> HTTP 204
|
||||
oxide.reload -> HTTP 204
|
||||
02:27 [Info] RunicGateway was compiled successfully in 3392ms
|
||||
02:27 [Info] Unloaded plugin Runic Gateway v0.1.0 by RunicGateway
|
||||
02:27 [Info] [Runic Gateway] protocol 1 [PTERODACTYL-PUSH-PROOF], serverId 'main', sidecar 127.0.0.1:7799
|
||||
02:27 [Info] Loaded plugin Runic Gateway v0.1.0 by RunicGateway
|
||||
restored source and re-pushed -> 204
|
||||
```
|
||||
|
||||
**Roughly ten seconds from a saved edit to a reloaded plugin**, against a running server with a
|
||||
generated world, without touching the panel UI. That is the loop `deploy.ps1` gives us for ServUO,
|
||||
and it is the thing that makes the panel a workable rig rather than only a deployment target.
|
||||
|
||||
Four details worth carrying into the push script:
|
||||
|
||||
- **Reload is `POST /command`, not a file operation**, and it answers `204` whether or not the plugin
|
||||
actually came back. The proof has to be read out of `oxide/logs/` afterwards — the same shape R18's
|
||||
auto-rollback needs, and an early rehearsal of it.
|
||||
- **The unload/load pair straddles the plugin's own `Init` log line.** `Unloaded` is printed, then the
|
||||
new instance's startup line, then `Loaded`. A script that waits for `Loaded` before reading has
|
||||
already passed the line it wanted.
|
||||
- **Oxide's compiler idles out and restarts.** The boot compile was `0ms`; the reload compile was
|
||||
`3392ms` because `Shutting down compiler because idle shutdown` had happened in between. A timeout
|
||||
tuned against a warm compiler will be wrong on the first reload after a quiet period.
|
||||
- **Restore the working tree and re-push it.** A test that leaves a marker in the source is a test
|
||||
that ships a marker. Both were put back and verified byte-identical against the server copy.
|
||||
|
||||
### 14.3 The image installs the framework on **every boot**, and neither version is pinnable
|
||||
|
||||
`ghcr.io/pterodactyl/games:rust`'s entrypoint is where `FRAMEWORK` is consumed — **not** the egg's
|
||||
install script, which knows nothing about it. On every single start, before the game runs, it:
|
||||
|
||||
- runs `steamcmd +app_update 258550` unless `AUTO_UPDATE=0`;
|
||||
- for `carbon`, downloads
|
||||
`CarbonCommunity/Carbon.Core/releases/download/**production_build**/Carbon.Linux.Release.tar.gz`;
|
||||
- for `oxide`, downloads `OxideMod/Oxide.Rust/releases/**latest**/Oxide.Rust-linux.zip`.
|
||||
|
||||
**Both are moving targets, fetched fresh at every restart.** CARBON.md §8 predicted this for Carbon
|
||||
from its rolling release tags; the egg makes it true of *Oxide as well*, because `latest` is the same
|
||||
kind of promise. The consequence is sharper than "the rig may drift":
|
||||
|
||||
> **A restart is a framework upgrade.** Two runs of the same test on the same server, minutes apart,
|
||||
> are not guaranteed to be running the same framework build — and nothing in the panel says so.
|
||||
|
||||
That reaches three places. **R4's `doctor`**: the weaker "current enough" claim is not Carbon-specific
|
||||
after all; under the egg neither framework has a pinned version to check. **§6's wipe-cadence risk**:
|
||||
the re-verify step is per *restart*, not per wipe. And **R20 itself**: if the egg is our deliverable,
|
||||
whether it should pin the framework at all is a decision, not an oversight — the upstream egg's
|
||||
answer is "always newest", which is right for an operator on wipe day and wrong for a test rig
|
||||
trying to reproduce a finding.
|
||||
|
||||
### 14.4 **The trap that changes R20: the startup string is not a safe place to launch the sidecar**
|
||||
|
||||
R20 says the startup command becomes "a small wrapper that launches `rust-link-sidecar` and then
|
||||
`RustDedicated`". The mechanism allows it and the ordering makes it wrong.
|
||||
|
||||
`wrapper.js` runs the startup string through `child_process.exec`, which is `/bin/sh -c` — so
|
||||
`./rust-link-sidecar & ./RustDedicated …` is syntactically fine. **But for Carbon the entrypoint
|
||||
prepends to the whole string:**
|
||||
|
||||
```bash
|
||||
MODIFIED_STARTUP="LD_PRELOAD=$(pwd)/libdoorstop.so ${MODIFIED_STARTUP}"
|
||||
```
|
||||
|
||||
So a startup beginning with our sidecar becomes:
|
||||
|
||||
```bash
|
||||
LD_PRELOAD=…/libdoorstop.so ./rust-link-sidecar & ./RustDedicated …
|
||||
```
|
||||
|
||||
**The preload lands on the sidecar and not on the game.** Carbon loads through Doorstop rather than
|
||||
through a patched `Assembly-CSharp.dll`, so the result is a server that starts cleanly, reports no
|
||||
error, and **is not modded** — no plugins, no hooks, and a bridge that connects to a game it can
|
||||
never hear from. It is the exact silent-success failure §6 keeps cataloguing, and it would only ever
|
||||
appear on the Carbon half.
|
||||
|
||||
Two further consequences of the same handoff:
|
||||
|
||||
- **`quit` SIGTERMs the shell, not the sidecar.** `wrapper.js` kills `gameProcess`, which is the `sh`
|
||||
running the startup string; a backgrounded sidecar is not its child in the way that reaches. R20
|
||||
already required "stop means stop the game" — this is the mechanism by which it would fail, and it
|
||||
leaves an orphan holding port 21004 against the next start.
|
||||
- **Doorstop also confirms R21's clean-install rule from a second direction.** Switching `FRAMEWORK`
|
||||
on an existing install does not undo the other framework: Oxide's patched DLL stays on disk while
|
||||
Carbon preloads over it. The migration argument was the soft reason for a fresh Carbon rig; this is
|
||||
the hard one.
|
||||
|
||||
**So R20 needs a decision it did not know it needed:** the sidecar is launched by something other
|
||||
than the startup string — our own image or entrypoint layered on the upstream one — or the startup
|
||||
string is composed so that whatever the entrypoint prepends still lands on `RustDedicated`. The first
|
||||
is more work and survives upstream changing its entrypoint; the second is free and depends on a line
|
||||
in somebody else's repository. Raised rather than settled.
|
||||
|
||||
---
|
||||
|
||||
[rl]: https://gitea.whitlocktech.com/RunicGateway/Rust-Link
|
||||
[rp]: https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins
|
||||
[mr]: https://gitea.whitlocktech.com/RunicGateway/Module-Rust
|
||||
|
||||
Reference in New Issue
Block a user