docs(modules): the Pterodactyl rig as built, R22, and a correction
Section 14 records R21's first rig, made with the application key and driven with the client key. Both work; neither can do the other's job, and there is no egg-write endpoint at all, so a published egg is a JSON file a human imports. The tier-2 loop was run rather than described: phase 1's real RunicGateway.cs pushed from the working tree, oxide.reload through the client API, the edit observed in Oxide's own log, source restored. About ten seconds end to end. Three things worked that were not certain to - files/write creates missing parents, a plugin placed before Oxide exists survives Oxide arriving, and the plugin compiles and loads on Linux, which no previous phase had established. Two findings that change decisions. The image installs the framework on EVERY boot, Carbon from the rolling production_build tag and Oxide from releases/latest, so a restart is a framework upgrade and neither is pinnable through the egg. And R20's "wrapper launches the sidecar then RustDedicated" does not survive Carbon: the entrypoint prepends LD_PRELOAD to the whole startup string, so the preload would land on the sidecar and the server would start cleanly, report nothing, and be silently unmodded. R22 (org lead): the sidecar's configuration moves into the egg's variables. Nearly free - rust-link already reads all five keys from the environment with env-over-file-over-defaults precedence - but the game bind must not be operator-editable, the web bind must derive from an allocation, and the db path must not be able to agree with REMOVE_FILES. Also corrects an earlier claim in this branch. The client key listing zero servers and includes returning empty were both CORRECT; the servers were being deleted between reads. A differential diagnosis across two API calls assumes the state did not move between them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -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