Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 340f1dc8f5 | |||
| 9424dca6d1 | |||
| bf2940b819 | |||
| e2c0db2a5d | |||
| fe20ca41af | |||
| 9c366ab5b5 | |||
| 0a57d783ca | |||
| 15120ef80d | |||
| f8084955e8 | |||
| 914047f1b0 | |||
| 18c90961ba |
@@ -38,9 +38,12 @@
|
|||||||
# sidecar (PLAN.md §4): the bridge calls it in-process. What a consumer pairs
|
# sidecar (PLAN.md §4): the bridge calls it in-process. What a consumer pairs
|
||||||
# on is the API version other plugins call, from plugin.toml.
|
# on is the API version other plugins call, from plugin.toml.
|
||||||
#
|
#
|
||||||
# 4. NO BUNDLE DISPATCH, YET. The installer does not know RunicNPC until stage 4
|
# 4. THE BUNDLE IS RECOMPOSED. Since stage 4 RunicNPC is the Rust bundle's third
|
||||||
# makes it a third artefact of the Rust bundle (D224). That stage adds the
|
# artefact (D224): the installer's compose job carries the latest RunicNPC
|
||||||
# step Rust-Plugins ends with, which asks RunicGateway/installer to recompose.
|
# release that answers the API the bridge declares (Rust-Plugins' overlay.toml
|
||||||
|
# `runicnpc_api`). The last step here asks RunicGateway/installer to
|
||||||
|
# recompose, as Rust-Plugins' release does; the installer's nightly cron covers
|
||||||
|
# a dispatch that did not arrive.
|
||||||
#
|
#
|
||||||
# The version is stamped into the shipped copy's `[Info(…)]` attribute, so
|
# The version is stamped into the shipped copy's `[Info(…)]` attribute, so
|
||||||
# `oxide.plugins` / `c.plugins` on a server names the release it runs. The
|
# `oxide.plugins` / `c.plugins` on a server names the release it runs. The
|
||||||
@@ -55,7 +58,9 @@
|
|||||||
# Prerequisites (Settings → Actions → Secrets on RunicGateway/runicnpc-rust, or
|
# Prerequisites (Settings → Actions → Secrets on RunicGateway/runicnpc-rust, or
|
||||||
# the organisation's):
|
# the organisation's):
|
||||||
# REGISTRY_TOKEN — Gitea access token with `write:repository`, to push the
|
# REGISTRY_TOKEN — Gitea access token with `write:repository`, to push the
|
||||||
# tag and create the release.
|
# tag and create the release. The final step also dispatches
|
||||||
|
# RunicGateway/installer's bundle workflow, so the token
|
||||||
|
# needs write on that repo too; without it that step warns.
|
||||||
# REGISTRY_USER — the Gitea username that token belongs to.
|
# REGISTRY_USER — the Gitea username that token belongs to.
|
||||||
|
|
||||||
name: Release plugin
|
name: Release plugin
|
||||||
@@ -71,6 +76,7 @@ concurrency:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_HOST: gitea.whitlocktech.com
|
GITEA_HOST: gitea.whitlocktech.com
|
||||||
|
INSTALLER_REPO: RunicGateway/installer
|
||||||
REPO: RunicGateway/runicnpc-rust
|
REPO: RunicGateway/runicnpc-rust
|
||||||
ARTIFACT: runicnpc
|
ARTIFACT: runicnpc
|
||||||
PLUGIN: plugin/RunicNPC.cs
|
PLUGIN: plugin/RunicNPC.cs
|
||||||
@@ -474,3 +480,27 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo " uploaded ${f}"
|
echo " uploaded ${f}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ── Recompose the installer's bundle manifest (D224) ─────────────────
|
||||||
|
# DISPATCH, DON'T WAIT, as Rust-Plugins' release does. A dropped dispatch
|
||||||
|
# costs latency, not correctness: the installer's nightly cron recomputes
|
||||||
|
# the bundle from whatever the latest releases actually are.
|
||||||
|
- name: Ask the installer repo to recompose its bundle
|
||||||
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
||||||
|
HTTP="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${CI_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ref":"main"}' \
|
||||||
|
"https://${GITEA_HOST}/api/v1/repos/${INSTALLER_REPO}/actions/workflows/bundle.yml/dispatches" || echo 000)"
|
||||||
|
case "$HTTP" in
|
||||||
|
20*) echo "Dispatched ${INSTALLER_REPO} bundle.yml (HTTP ${HTTP}) — not waiting for it." ;;
|
||||||
|
403|404)
|
||||||
|
echo "::warning::Could not dispatch ${INSTALLER_REPO} bundle.yml (HTTP ${HTTP}). REGISTRY_TOKEN likely lacks write:repository on that repo. Release ${{ steps.plan.outputs.tag }} is published and fine; its bundle will be composed by the installer's nightly cron instead." ;;
|
||||||
|
*)
|
||||||
|
echo "::warning::Dispatching ${INSTALLER_REPO} bundle.yml returned HTTP ${HTTP}. Release ${{ steps.plan.outputs.tag }} is published and fine; the nightly cron will recompose the bundle." ;;
|
||||||
|
esac
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -7,8 +7,9 @@ works on its own, and on a [Runic Gateway](https://gitea.whitlocktech.com/RunicG
|
|||||||
website authors its NPC profiles and events use its NPCs. The plan of record, stage by stage, is
|
website authors its NPC profiles and events use its NPCs. The plan of record, stage by stage, is
|
||||||
[`docs/runicnpc/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/PLAN.md).
|
[`docs/runicnpc/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/PLAN.md).
|
||||||
|
|
||||||
> **Status: stage 0.** The plugin loads, answers its API version, and reports on itself. It spawns
|
> **Status: stage 4.** The NPC, its profiles, placements and routes, the API other plugins call, and
|
||||||
> nothing yet. Stage 1 is a measuring spike; the NPC and its API arrive in stage 2.
|
> the `/rnpc` commands admins use in game. On a Runic Gateway server, the website takes over the
|
||||||
|
> profiles and lists, edits and creates placements through the bridge (API 3).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -43,6 +44,26 @@ Answers at the server console and over RCON: the version, the API version, and w
|
|||||||
plugin's hooks have fired. A hook that never fires is the first sign a Rust or framework update has
|
plugin's hooks have fired. A hook that never fires is the first sign a Rust or framework update has
|
||||||
renamed it, because neither framework reports a hook that matches nothing.
|
renamed it, because neither framework reports a hook that matches nothing.
|
||||||
|
|
||||||
|
## In game
|
||||||
|
|
||||||
|
`/rnpc` in chat, and the same verbs as `rnpc.<verb>` in a console (F1, the server console or RCON).
|
||||||
|
`/rnpc` alone lists the ones you may use. Grant `runicnpc.place` to place and manage NPCs, and
|
||||||
|
`runicnpc.admin` for everything (it includes `runicnpc.place`); the server console has both.
|
||||||
|
|
||||||
|
```
|
||||||
|
/rnpc place bandit count=3 respawn=300 mode=group move=route:gate
|
||||||
|
> Placed bandit-1: 3 × 'bandit' (roamer, route:gate), respawn 300 s, group.
|
||||||
|
/rnpc path record gate then walk, and at each point: /rnpc path point
|
||||||
|
/rnpc path save loop (or back, to walk it back and forth)
|
||||||
|
/rnpc near · /rnpc info · /rnpc rename bandit-1 gateguards · /rnpc remove
|
||||||
|
```
|
||||||
|
|
||||||
|
`place` uses the spot you look at, `here` the spot you stand on; the server console gives
|
||||||
|
`at=x,y,z`. A roamer must stand on Rust's navmesh; only a sentry may stand off it. Every placement
|
||||||
|
answers with what the server's NPCs cost. On a standalone server, profiles are made from a console
|
||||||
|
with `rnpc.profile create|set|delete`. The full list is in
|
||||||
|
[PLAN.md §5](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/PLAN.md).
|
||||||
|
|
||||||
## For other plugins
|
## For other plugins
|
||||||
|
|
||||||
Every call is prefixed `RunicNpc_` and reached through `Call`:
|
Every call is prefixed `RunicNpc_` and reached through `Call`:
|
||||||
@@ -53,9 +74,11 @@ Every call is prefixed `RunicNpc_` and reached through `Call`:
|
|||||||
int api = RunicNPC?.Call<int>("RunicNpc_ApiVersion") ?? 0;
|
int api = RunicNPC?.Call<int>("RunicNpc_ApiVersion") ?? 0;
|
||||||
```
|
```
|
||||||
|
|
||||||
Stage 0 has only `RunicNpc_ApiVersion()`. The full API is planned in PLAN.md §4 and will be
|
The API is version 3, documented in
|
||||||
documented as `docs/runicnpc/API.md` in stage 2. The API version moves when a call or a raised hook
|
[`docs/runicnpc/API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/API.md):
|
||||||
changes shape, not on every release.
|
spawning and removing NPCs by owner, profiles, placements (created and named as in game, from a map point),
|
||||||
|
routes, the cost warning, and the hooks it raises.
|
||||||
|
The API version moves when a call or a raised hook changes shape, not on every release.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
@@ -64,7 +87,22 @@ changes shape, not on every release.
|
|||||||
| `plugin/RunicNPC.cs` | The plugin. The only file a server gets. |
|
| `plugin/RunicNPC.cs` | The plugin. The only file a server gets. |
|
||||||
| `plugin.toml` | Its declarations: API version, framework floors, required plugins. The release copies them into the manifest. |
|
| `plugin.toml` | Its declarations: API version, framework floors, required plugins. The release copies them into the manifest. |
|
||||||
| `scripts/checkPlugin.js` | The static checks run on every pull request and again before a release (see its header). |
|
| `scripts/checkPlugin.js` | The static checks run on every pull request and again before a release (see its header). |
|
||||||
| `tools/` | Developer scaffolding for the test rigs, never shipped (see `tools/rigs.example.json`). |
|
| `tools/` | Developer scaffolding for the test rigs, never shipped: the panel scripts (see `tools/rigs.example.json`); `RunicNpcHarness.cs`, the stage 1 measurement plugin; `RunicNpcTest.cs`, the stage 2, 3 and 4 test harness (`rnt.run all`, then `rnt.after` after a reload or restart); and `fieldlist/` with `managed.js`, which regenerate the swap's field list (below). |
|
||||||
|
|
||||||
|
## After a Rust update: the swap's field list
|
||||||
|
|
||||||
|
Our NPC is Rust's scientist with two components swapped, and the swap copies a fixed list of fields (D232),
|
||||||
|
generated from Rust's unmodified assembly. `rnpc.status` reports it as `swap fields: npc=64/64 brain=32/32
|
||||||
|
missing=- added=-`. On Carbon, where the assembly is unmodified, `added` names any field Rust has added since
|
||||||
|
the list was made. To regenerate it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/managed.js carbon <dir> # the Carbon rig's RustDedicated_Data/Managed
|
||||||
|
dotnet run --project tools/fieldlist -- <dir> # rewrites the block in plugin/RunicNPC.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the Carbon rig: Oxide's patcher makes Rust's private fields public, so its assembly no longer says which
|
||||||
|
fields Rust itself serialises.
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,11 @@
|
|||||||
# a RunicNPC too old for the bridge it is pairing with. Declaring it here is what
|
# a RunicNPC too old for the bridge it is pairing with. Declaring it here is what
|
||||||
# lets a bundle check the pair BEFORE an operator installs it.
|
# lets a bundle check the pair BEFORE an operator installs it.
|
||||||
#
|
#
|
||||||
# Current: 1 — `RunicNpc_ApiVersion()` and nothing else (stage 0).
|
# 1 was stage 0's: `RunicNpc_ApiVersion()` and nothing else.
|
||||||
api = 1
|
# 2 was stage 2's whole API, PLAN.md §4.
|
||||||
|
# Current: 3 — stage 4 (D249): RunicNpc_AddPlacement, _RenamePlacement, _RespawnPlacement and the
|
||||||
|
# OnRunicNpcPlacementChanged hook, documented in docs/runicnpc/API.md.
|
||||||
|
api = 3
|
||||||
|
|
||||||
# ── Framework floors ─────────────────────────────────────────────────────────
|
# ── Framework floors ─────────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
|
|||||||
3184
plugin/RunicNPC.cs
3184
plugin/RunicNPC.cs
File diff suppressed because it is too large
Load Diff
@@ -179,7 +179,11 @@ function check(source, toml) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const methods = readMethods(source)
|
const methods = readMethods(source)
|
||||||
const hooks = methods.filter((x) => HOOK_NAME.test(x.name))
|
// An `override` is never a hook: a hook is a plugin method the framework finds by name, and the
|
||||||
|
// plugin class overrides nothing shaped like one. What does override `On*` methods is the NPC's
|
||||||
|
// own classes (RunicNpcPlayer.OnDied overrides Rust's ScientistNPC), and Rust, not the
|
||||||
|
// framework, calls those.
|
||||||
|
const hooks = methods.filter((x) => HOOK_NAME.test(x.name) && !/\boverride\b/.test(x.returns))
|
||||||
const hookNames = new Set(hooks.map((x) => x.name))
|
const hookNames = new Set(hooks.map((x) => x.name))
|
||||||
|
|
||||||
// 1. Every hook the plugin implements is one `rnpc.status` can report on.
|
// 1. Every hook the plugin implements is one `rnpc.status` can report on.
|
||||||
|
|||||||
@@ -126,6 +126,25 @@ test('a method that is not shaped like a hook is left alone', () => {
|
|||||||
assert.ok(HOOK_NAME.test('CanBeTargeted'))
|
assert.ok(HOOK_NAME.test('CanBeTargeted'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('an override shaped like a hook belongs to the NPC classes, not the framework, and is left alone', () => {
|
||||||
|
const methods = ` private void OnServerInitialized()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RunicNpcPlayer : ScientistNPC
|
||||||
|
{
|
||||||
|
public override void OnDied(HitInfo info)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStateChanged()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
assert.deepEqual(check(source({ methods }), toml()), [])
|
||||||
|
})
|
||||||
|
|
||||||
// ── The API ────────────────────────────────────────────────────────────────
|
// ── The API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
test('a public API call without [HookMethod] is caught, because Call cannot reach it', () => {
|
test('a public API call without [HookMethod] is caught, because Call cannot reach it', () => {
|
||||||
@@ -287,12 +306,13 @@ test('the parser actually reads the real plugin, rather than quietly matching no
|
|||||||
|
|
||||||
// Raise these floors as the plugin grows; they are what stops a regex that
|
// Raise these floors as the plugin grows; they are what stops a regex that
|
||||||
// matches nothing from passing every case above.
|
// matches nothing from passing every case above.
|
||||||
assert.ok(methods.length >= 5, `only found ${methods.length} methods in the real plugin`)
|
assert.ok(methods.length >= 40, `only found ${methods.length} methods in the real plugin`)
|
||||||
assert.ok(names.has('OnServerInitialized'), 'OnServerInitialized was not found by the method parser')
|
assert.ok(names.has('OnServerInitialized'), 'OnServerInitialized was not found by the method parser')
|
||||||
assert.ok(names.has('CmdStatus'), 'CmdStatus (under an attribute) was not found by the method parser')
|
assert.ok(names.has('CmdStatus'), 'CmdStatus (under an attribute) was not found by the method parser')
|
||||||
|
|
||||||
const api = methods.filter((m) => API_NAME.test(m.name)).map((m) => m.name)
|
const api = methods.filter((m) => API_NAME.test(m.name)).map((m) => m.name)
|
||||||
assert.ok(api.includes('RunicNpc_ApiVersion'), 'RunicNpc_ApiVersion was not found as an API call')
|
assert.ok(api.includes('RunicNpc_ApiVersion'), 'RunicNpc_ApiVersion was not found as an API call')
|
||||||
|
assert.ok(api.includes('RunicNpc_Spawn'), 'RunicNpc_Spawn was not found as an API call')
|
||||||
|
|
||||||
assert.ok(readExpectedHooks(real).length >= 1)
|
assert.ok(readExpectedHooks(real).length >= 1)
|
||||||
assert.deepEqual(readRequires(real), ['Kits'])
|
assert.deepEqual(readRequires(real), ['Kits'])
|
||||||
|
|||||||
2063
tools/RunicNpcHarness.cs
Normal file
2063
tools/RunicNpcHarness.cs
Normal file
File diff suppressed because it is too large
Load Diff
1204
tools/RunicNpcTest.cs
Normal file
1204
tools/RunicNpcTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
18
tools/fieldlist/FieldList.csproj
Normal file
18
tools/fieldlist/FieldList.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Generates the swap's field list (docs/runicnpc/PLAN.md D232) from Rust's unmodified
|
||||||
|
Assembly-CSharp.dll. A developer tool: never shipped, not built by CI. See Program.cs.
|
||||||
|
-->
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
98
tools/fieldlist/Program.cs
Normal file
98
tools/fieldlist/Program.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Writes the list of fields RunicNPC's swap copies from Rust's scientist to ours (D232).
|
||||||
|
//
|
||||||
|
// node tools/managed.js carbon <dir> (once per Rust update)
|
||||||
|
// dotnet run --project tools/fieldlist -- <dir> [plugin/RunicNPC.cs]
|
||||||
|
//
|
||||||
|
// <dir> must hold the CARBON rig's managed assemblies: Oxide's patcher makes Rust's private fields
|
||||||
|
// public, so its Assembly-CSharp.dll no longer says what Rust itself serialises.
|
||||||
|
//
|
||||||
|
// The rule is stage 1's, which is Unity's own for a component's authored data: every instance
|
||||||
|
// field from the component's type down to (not including) MonoBehaviour that is public and not
|
||||||
|
// [NonSerialized], or carries [SerializeField]; never readonly, const or a delegate. Applied here to
|
||||||
|
// the unmodified assembly, it picks the same fields on both frameworks. The result replaces the
|
||||||
|
// block between the two marker lines in the plugin, and the plugin resolves each name at load.
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
if (args.Length < 1)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("usage: dotnet run --project tools/fieldlist -- <managed dir> [plugin/RunicNPC.cs]");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
string managed = args[0];
|
||||||
|
string plugin = args.Length > 1 ? args[1] : Path.Combine("plugin", "RunicNPC.cs");
|
||||||
|
|
||||||
|
string[] dlls = Directory.GetFiles(managed, "*.dll");
|
||||||
|
var resolver = new PathAssemblyResolver(dlls);
|
||||||
|
using var context = new MetadataLoadContext(resolver, "mscorlib");
|
||||||
|
Assembly rust = context.LoadFromAssemblyPath(Path.Combine(managed, "Assembly-CSharp.dll"));
|
||||||
|
|
||||||
|
List<string> Fields(string typeName)
|
||||||
|
{
|
||||||
|
Type type = rust.GetType(typeName, throwOnError: true)!;
|
||||||
|
var names = new List<string>();
|
||||||
|
for (Type? t = type; t != null && t.FullName != "UnityEngine.MonoBehaviour"; t = t.BaseType)
|
||||||
|
{
|
||||||
|
foreach (FieldInfo f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
|
||||||
|
{
|
||||||
|
if (f.IsInitOnly || f.IsLiteral)
|
||||||
|
continue;
|
||||||
|
bool serialised = (f.IsPublic && (f.Attributes & FieldAttributes.NotSerialized) == 0) ||
|
||||||
|
f.GetCustomAttributesData().Any(a => a.AttributeType.FullName == "UnityEngine.SerializeField");
|
||||||
|
if (!serialised || IsDelegate(f.FieldType))
|
||||||
|
continue;
|
||||||
|
names.Add($"{t.Name}.{f.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsDelegate(Type t)
|
||||||
|
{
|
||||||
|
for (Type? b = t; b != null; b = b.BaseType)
|
||||||
|
if (b.FullName == "System.Delegate")
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> npc = Fields("ScientistNPC");
|
||||||
|
List<string> brain = Fields("ScientistBrain");
|
||||||
|
|
||||||
|
Guid mvid = rust.ManifestModule.ModuleVersionId;
|
||||||
|
|
||||||
|
const string Open = " // <fieldlist>";
|
||||||
|
const string Close = " // </fieldlist>";
|
||||||
|
|
||||||
|
var block = new StringBuilder();
|
||||||
|
block.AppendLine(Open);
|
||||||
|
block.AppendLine($" // Generated by tools/fieldlist from Rust's Assembly-CSharp.dll, module {mvid}.");
|
||||||
|
block.AppendLine(" // Do not edit by hand: regenerate after a Rust update (D232).");
|
||||||
|
Append(block, "SwapNpcFields", npc);
|
||||||
|
Append(block, "SwapBrainFields", brain);
|
||||||
|
block.Append(Close);
|
||||||
|
|
||||||
|
static void Append(StringBuilder sb, string name, List<string> fields)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" private static readonly string[] {name} =");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
foreach (string f in fields)
|
||||||
|
sb.AppendLine($" \"{f}\",");
|
||||||
|
sb.AppendLine(" };");
|
||||||
|
}
|
||||||
|
|
||||||
|
string text = File.ReadAllText(plugin);
|
||||||
|
string nl = text.Contains("\r\n") ? "\r\n" : "\n";
|
||||||
|
int start = text.IndexOf(Open, StringComparison.Ordinal);
|
||||||
|
int end = text.IndexOf(Close, StringComparison.Ordinal);
|
||||||
|
if (start < 0 || end < start)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"{plugin}: the marker lines '{Open.Trim()}' and '{Close.Trim()}' were not found.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string replacement = block.ToString().Replace("\r\n", "\n").Replace("\n", nl);
|
||||||
|
File.WriteAllText(plugin, text[..start] + replacement + text[(end + Close.Length)..]);
|
||||||
|
Console.WriteLine($"{plugin}: {npc.Count} NPC fields, {brain.Count} brain fields (Assembly-CSharp module {mvid}).");
|
||||||
|
return 0;
|
||||||
49
tools/managed.js
Normal file
49
tools/managed.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Downloads a rig's managed assemblies (RustDedicated_Data/Managed/*.dll) into a local directory,
|
||||||
|
// for tools/fieldlist, which reads Rust's own field declarations from them (D232).
|
||||||
|
//
|
||||||
|
// node tools/managed.js <rig> <out dir>
|
||||||
|
//
|
||||||
|
// Use the CARBON rig. Oxide's patcher rewrites Assembly-CSharp.dll and makes private fields public,
|
||||||
|
// so the Oxide rig's copy no longer says which fields Rust itself serialises. Carbon leaves the
|
||||||
|
// assembly as Facepunch shipped it.
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { load, serverId, unmsys } = require('./panel')
|
||||||
|
|
||||||
|
const config = load()
|
||||||
|
const [rig, out] = process.argv.slice(2).map(unmsys)
|
||||||
|
if (!rig || !out) {
|
||||||
|
console.error('usage: node tools/managed.js <rig> <out dir>')
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = `${config.panel}/api/client/servers/${serverId(config, rig)}`
|
||||||
|
const headers = { Authorization: 'Bearer ' + config.key, Accept: 'application/json' }
|
||||||
|
const dir = '/RustDedicated_Data/Managed'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const list = await fetch(`${base}/files/list?directory=${encodeURIComponent(dir)}`, { headers })
|
||||||
|
if (!list.ok) throw new Error(`list ${list.status}: ${await list.text()}`)
|
||||||
|
const files = (await list.json()).data
|
||||||
|
.map((f) => f.attributes)
|
||||||
|
.filter((f) => f.is_file && f.name.endsWith('.dll'))
|
||||||
|
|
||||||
|
fs.mkdirSync(out, { recursive: true })
|
||||||
|
let bytes = 0
|
||||||
|
for (const f of files) {
|
||||||
|
const target = path.join(out, f.name)
|
||||||
|
if (fs.existsSync(target) && fs.statSync(target).size === f.size) continue
|
||||||
|
const link = await fetch(`${base}/files/download?file=${encodeURIComponent(`${dir}/${f.name}`)}`, { headers })
|
||||||
|
if (!link.ok) throw new Error(`${f.name}: ${link.status} ${await link.text()}`)
|
||||||
|
const body = await fetch((await link.json()).attributes.url)
|
||||||
|
fs.writeFileSync(target, Buffer.from(await body.arrayBuffer()))
|
||||||
|
bytes += f.size
|
||||||
|
}
|
||||||
|
console.log(`${files.length} assemblies in ${out} (${(bytes / 1e6).toFixed(1)} MB downloaded)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e.message || e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user