Merge pull request 'feat(rust): RunicNPC is required in the Rust bundle (runicnpc stage 9, D310)' (#38) from feat/runicnpc-required into edge
Reviewed-on: #38
This commit is contained in:
@@ -377,44 +377,39 @@ compose_rust() {
|
||||
[ "$link_protocol" = "$plugin_protocol" ] || fail \
|
||||
"PROTOCOL MISMATCH — Rust-Link ${link_tag} speaks ${link_protocol}, Rust-Plugins ${plugin_tag} declares ${plugin_protocol}. The game link has no 409: a mismatched plugin would mis-parse. Fix: land the matching half and let its release cut, or bump Rust-Plugins/overlay.toml."
|
||||
|
||||
# RunicNPC (docs/runicnpc/PLAN.md D224): a third file, optional until its stage 9.
|
||||
# The bridge's manifest says which RunicNPC API its npc.* commands need
|
||||
# (`runicnpc_api`, from overlay.toml); the latest RunicNPC release is carried
|
||||
# only when it answers at least that. A bridge that needs none carries none: a
|
||||
# RunicNPC it cannot talk to would be an NPC plugin nobody manages.
|
||||
local npc='null' need npc_tag npc_tarball npc_manifest npc_api npc_carried=''
|
||||
# RunicNPC (docs/runicnpc/PLAN.md D224): a third file, REQUIRED since its stage 9
|
||||
# (D310) — the bridge refuses every NPC an event places without it, and the
|
||||
# installer refuses a Rust bundle that does not carry it. The bridge's manifest
|
||||
# says which RunicNPC API it needs (`runicnpc_api`, from overlay.toml); the
|
||||
# latest RunicNPC release is carried only when it answers at least that. With
|
||||
# none that does, the Rust bundle is not composed: the published one stays.
|
||||
local npc need npc_tag npc_tarball npc_manifest npc_api
|
||||
need="$(jq -r '.runicnpc_api // 0' "$manifest")"
|
||||
if [ "$need" -gt 0 ]; then
|
||||
rc=0
|
||||
resolve_latest "$RUNICNPC_REPO" runicnpc || rc=$?
|
||||
if [ "$rc" -eq 2 ]; then
|
||||
note "- **Rust**: the bridge needs RunicNPC API ${need}, and RunicNPC has not released yet — the bundle carries none."
|
||||
else
|
||||
npc_tag="$(release_tag runicnpc)"
|
||||
verify_assets runicnpc
|
||||
npc_tarball="$(single_tarball runicnpc)"
|
||||
npc_manifest="$(payload_manifest runicnpc "$npc_tarball" "$RUNICNPC_PREFIX" "$npc_tag")"
|
||||
[ -f "$(dirname "$npc_manifest")/RunicNPC.cs" ] \
|
||||
|| fail "the RunicNPC tarball has no ${RUNICNPC_PREFIX}/RunicNPC.cs"
|
||||
npc_api="$(jq -r '.api' "$npc_manifest")"
|
||||
echo "==> rust: RunicNPC ${npc_tag} api=${npc_api} | the bridge needs ${need}"
|
||||
if [ "$npc_api" -lt "$need" ]; then
|
||||
note "- **Rust**: RunicNPC \`${npc_tag}\` answers API ${npc_api} and the bridge needs ${need} — the bundle carries none until RunicNPC releases one that does."
|
||||
else
|
||||
npc="$(jq -n --arg repo "$RUNICNPC_REPO" --arg tag "$npc_tag" --slurpfile m "$npc_manifest" \
|
||||
--argjson asset "$(asset_json runicnpc "$npc_tarball")" \
|
||||
'{ repo: $repo, tag: $tag, version: ($tag | ltrimstr("v")), commit: $m[0].commit, api: $m[0].api,
|
||||
compat: { frameworks: { oxide: { min_version: $m[0].min_oxide_version },
|
||||
carbon: { min_version: $m[0].min_carbon_version } },
|
||||
requires_plugins: $m[0].requires_plugins },
|
||||
asset: $asset }')"
|
||||
jq -e '([.commit, .compat.frameworks[].min_version] | all(type == "string"))
|
||||
and (.api | type == "number") and (.compat.requires_plugins | type == "array")' <<<"$npc" >/dev/null \
|
||||
|| fail "the RunicNPC manifest is missing commit, api, a framework floor, or requires_plugins"
|
||||
npc_carried="$npc_tag"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[ "$need" -gt 0 ] || fail \
|
||||
"Rust-Plugins ${plugin_tag} declares no runicnpc_api, and RunicNPC is required (D310) — a bridge older than RunicNPC's stage 4 cannot be bundled any more."
|
||||
rc=0
|
||||
resolve_latest "$RUNICNPC_REPO" runicnpc || rc=$?
|
||||
[ "$rc" -ne 2 ] || fail "the bridge needs RunicNPC API ${need}, and RunicNPC has not released — RunicNPC is required (D310)."
|
||||
npc_tag="$(release_tag runicnpc)"
|
||||
verify_assets runicnpc
|
||||
npc_tarball="$(single_tarball runicnpc)"
|
||||
npc_manifest="$(payload_manifest runicnpc "$npc_tarball" "$RUNICNPC_PREFIX" "$npc_tag")"
|
||||
[ -f "$(dirname "$npc_manifest")/RunicNPC.cs" ] \
|
||||
|| fail "the RunicNPC tarball has no ${RUNICNPC_PREFIX}/RunicNPC.cs"
|
||||
npc_api="$(jq -r '.api' "$npc_manifest")"
|
||||
echo "==> rust: RunicNPC ${npc_tag} api=${npc_api} | the bridge needs ${need}"
|
||||
[ "$npc_api" -ge "$need" ] || fail \
|
||||
"RunicNPC ${npc_tag} answers API ${npc_api} and Rust-Plugins ${plugin_tag} needs ${need} — RunicNPC is required (D310), so no Rust bundle is composed until it releases one that does."
|
||||
npc="$(jq -n --arg repo "$RUNICNPC_REPO" --arg tag "$npc_tag" --slurpfile m "$npc_manifest" \
|
||||
--argjson asset "$(asset_json runicnpc "$npc_tarball")" \
|
||||
'{ repo: $repo, tag: $tag, version: ($tag | ltrimstr("v")), commit: $m[0].commit, api: $m[0].api,
|
||||
compat: { frameworks: { oxide: { min_version: $m[0].min_oxide_version },
|
||||
carbon: { min_version: $m[0].min_carbon_version } },
|
||||
requires_plugins: $m[0].requires_plugins },
|
||||
asset: $asset }')"
|
||||
jq -e '([.commit, .compat.frameworks[].min_version] | all(type == "string"))
|
||||
and (.api | type == "number") and (.compat.requires_plugins | type == "array")' <<<"$npc" >/dev/null \
|
||||
|| fail "the RunicNPC manifest is missing commit, api, a framework floor, or requires_plugins"
|
||||
|
||||
jq -n \
|
||||
--arg link_repo "$RUST_LINK_REPO" --arg link_tag "$link_tag" \
|
||||
@@ -432,8 +427,8 @@ compose_rust() {
|
||||
compat: { frameworks: { oxide: { min_version: $m[0].min_oxide_version },
|
||||
carbon: { min_version: $m[0].min_carbon_version } },
|
||||
requires_plugins: $m[0].requires_plugins },
|
||||
asset: $p_asset } }
|
||||
+ (if $npc == null then {} else { npc: $npc } end)' > "$WORK/rust-s2.json"
|
||||
asset: $p_asset },
|
||||
npc: $npc }' > "$WORK/rust-s2.json"
|
||||
# A manifest missing a key would compose a `null` the installer then trusts.
|
||||
jq -e '([.payload.compat.frameworks[].min_version, .payload.commit] | all(type == "string"))
|
||||
and (.payload.compat.requires_plugins | type == "array")' "$WORK/rust-s2.json" >/dev/null \
|
||||
@@ -446,7 +441,7 @@ compose_rust() {
|
||||
local tag
|
||||
tag="$(fresh_tag "$PUBLISHED/v2/rust")"
|
||||
publish_doc "$PUBLISHED/v2/rust" "$tag" "$WORK/rust-s2.json"
|
||||
note "- **Rust**: published \`${tag}\` — Rust-Link \`${link_tag}\`, Rust-Plugins \`${plugin_tag}\`, protocol ${link_protocol}${npc_carried:+, RunicNPC \`${npc_tag}\`}."
|
||||
note "- **Rust**: published \`${tag}\` — Rust-Link \`${link_tag}\`, Rust-Plugins \`${plugin_tag}\`, protocol ${link_protocol}, RunicNPC \`${npc_tag}\`."
|
||||
echo "rust ${tag}" >> "$WORK/published-games.txt"
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
# current.json, bundle-<tag>.json schema 1, ServUO: link + overlay
|
||||
# v2/servuo/current.json, bundle-<tag>.json schema 2, game "servuo"
|
||||
# v2/rust/current.json, bundle-<tag>.json schema 2, game "rust": Rust-Link + Rust-Plugins,
|
||||
# and RunicNPC when the bridge needs it (D224)
|
||||
# and RunicNPC, required since its stage 9 (D224, D310)
|
||||
#
|
||||
# Schema 2 (D146) names ONE game, with a `game` discriminant and a `payload`
|
||||
# that is an overlay for ServUO and a plugin for Rust. Schema 1 (D147) is still
|
||||
|
||||
@@ -362,8 +362,9 @@ pub struct RustBundle {
|
||||
pub sidecar: RustSidecar,
|
||||
pub payload: PluginPayload,
|
||||
/// RunicNPC, Runic Gateway's NPC plugin (docs/runicnpc/PLAN.md D224): a third file placed in
|
||||
/// the plugins directory beside the bridge. Optional until RunicNPC's stage 9 makes it
|
||||
/// required, so a bundle without it — every one before stage 4 — still parses and installs.
|
||||
/// the plugins directory beside the bridge. Required since RunicNPC's stage 9 (D310): a bundle
|
||||
/// without it — every one before stage 4 — still PARSES, so `doctor` and an old pin can name
|
||||
/// it, but `install` and `update` refuse it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub npc: Option<NpcComponent>,
|
||||
}
|
||||
|
||||
@@ -178,9 +178,10 @@ fn instance_rows(
|
||||
}
|
||||
}
|
||||
|
||||
// ── RunicNPC: ours when the bundle carried it (D224) ──────────────────────
|
||||
// A warning, like a helper's: until RunicNPC's stage 9 the bridge runs without it, and what is
|
||||
// lost is the site's NPC profiles and placements and events' profile NPCs.
|
||||
// ── RunicNPC: ours, and required (D224, D310) ─────────────────────────────
|
||||
// A failure when it is missing or was never placed: since RunicNPC's stage 9 the bridge
|
||||
// refuses every NPC an event places without it. A file edited by hand still loads, so that
|
||||
// stays a warning, as the bridge's own helpers are.
|
||||
if let Some(file) = &instance.npc {
|
||||
let tag = record.npc.as_ref().map(|n| n.tag.as_str()).unwrap_or("?");
|
||||
match std::fs::read(&file.path) {
|
||||
@@ -192,11 +193,17 @@ fn instance_rows(
|
||||
.note("edited or replaced by hand; `update --game rust` puts the released one back"),
|
||||
),
|
||||
Err(_) => rows.push(
|
||||
Row::warn(&label("RunicNPC"), format!("{} is missing", file.path))
|
||||
.note("without it the site's NPC profiles and placements, and events' profile NPCs, are off")
|
||||
Row::fail(&label("RunicNPC"), format!("{} is missing", file.path))
|
||||
.note("RunicNPC is required: without it every NPC an event places is refused, Rust's own scientists included")
|
||||
.note("`update --game rust` puts it back"),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
rows.push(
|
||||
Row::fail(&label("RunicNPC"), "not installed")
|
||||
.note("RunicNPC is required: without it every NPC an event places is refused, Rust's own scientists included")
|
||||
.note("`update --game rust` installs it from the current bundle"),
|
||||
);
|
||||
}
|
||||
|
||||
// ── The plugin's config: the website's ───────────────────────────────────
|
||||
|
||||
@@ -183,7 +183,9 @@ pub fn deploy(cli: &Cli, mode: Mode) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
// RunicNPC (D224), fetched and checked beside the plugin, before anything is planned.
|
||||
// RunicNPC (D224), fetched and checked beside the plugin, before anything is planned. It is
|
||||
// required since RunicNPC's stage 9 (D310): the bridge refuses every NPC an event places
|
||||
// without it, so a bundle that cannot place it is refused before anything is written.
|
||||
let npc_released = match &bundle.npc {
|
||||
Some(component) => {
|
||||
let path = scratch.path().join(&component.asset.name);
|
||||
@@ -191,7 +193,8 @@ pub fn deploy(cli: &Cli, mode: Mode) -> Result<()> {
|
||||
let released = npc::read_tarball(&path)?;
|
||||
if released.manifest.api != component.api {
|
||||
bail!(
|
||||
"RunicNPC in bundle {} answers API {}, but the bundle says {} — refusing a component the bundle does not describe.",
|
||||
"RunicNPC in bundle {} answers API {}, but the bundle says {} — refusing a \
|
||||
component the bundle does not describe.",
|
||||
bundle.bundle,
|
||||
released.manifest.api,
|
||||
component.api
|
||||
@@ -205,7 +208,12 @@ pub fn deploy(cli: &Cli, mode: Mode) -> Result<()> {
|
||||
}
|
||||
Some(released)
|
||||
}
|
||||
None => None,
|
||||
None => bail!(
|
||||
"bundle {} carries no RunicNPC, and Runic Gateway's Rust events need it: without it \
|
||||
the bridge refuses every NPC an event places. Install the current bundle (leave \
|
||||
--bundle off), or pin one published with RunicNPC v1.0.0 or later.",
|
||||
bundle.bundle
|
||||
),
|
||||
};
|
||||
|
||||
// ── Plan every instance ──────────────────────────────────────────────────
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//! it. RunicNPC makes that directory itself on first load, because one made from outside the game
|
||||
//! is not writable by it (runicnpc PLAN.md §1.5).
|
||||
//!
|
||||
//! It is optional until RunicNPC's stage 9: a bundle without it installs as before, and a host
|
||||
//! without it runs the bridge with Rust's own scientists only (D243).
|
||||
//! It is required since RunicNPC's stage 9 (D310): the bridge refuses every NPC an event places
|
||||
//! without it, Rust's own scientists included, so `install` and `update` refuse a bundle that does
|
||||
//! not carry it and `doctor` fails a host that lacks it.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Read;
|
||||
|
||||
Reference in New Issue
Block a user