Merge pull request 'feat: forward RunicNPC's profiles and placements; the egg installs RunicNPC (runicnpc stage 4)' (#21) from feat/runicnpc-stage4 into edge
Reviewed-on: #21
This commit is contained in:
@@ -138,6 +138,21 @@ rg_install() {
|
|||||||
fetch '.sidecar.assets["linux-x86_64"]' rust-link-sidecar
|
fetch '.sidecar.assets["linux-x86_64"]' rust-link-sidecar
|
||||||
fetch '.sidecar.launcher' with-sidecar.sh
|
fetch '.sidecar.launcher' with-sidecar.sh
|
||||||
fetch '.payload.asset' plugin.tar.gz
|
fetch '.payload.asset' plugin.tar.gz
|
||||||
|
# RunicNPC (docs/runicnpc/PLAN.md D224), when the bundle carries it: a third
|
||||||
|
# file beside the bridge, checked the same way before anything is placed.
|
||||||
|
local npc=""
|
||||||
|
if jq -e '.npc != null' "${work}/bundle.json" >/dev/null; then
|
||||||
|
npc="$(jq -r '.npc.tag' "${work}/bundle.json")"
|
||||||
|
fetch '.npc.asset' runicnpc.tar.gz
|
||||||
|
tar -xzf "${work}/runicnpc.tar.gz" -C "${work}"
|
||||||
|
local npc_manifest="${work}/runicnpc/manifest.json"
|
||||||
|
[ -f "${npc_manifest}" ] || { echo "Runic Gateway: the RunicNPC tarball has no manifest.json"; return 1; }
|
||||||
|
[ "$(jq -r '.api' "${npc_manifest}")" = "$(jq -r '.npc.api' "${work}/bundle.json")" ] \
|
||||||
|
|| { echo "Runic Gateway: RunicNPC answers API $(jq -r '.api' "${npc_manifest}"), the bundle says $(jq -r '.npc.api' "${work}/bundle.json") - refusing it"; return 1; }
|
||||||
|
[ -f "${work}/runicnpc/RunicNPC.cs" ] || { echo "Runic Gateway: the RunicNPC tarball has no RunicNPC.cs"; return 1; }
|
||||||
|
echo "$(jq -r '.files["RunicNPC.cs"]' "${npc_manifest}") ${work}/runicnpc/RunicNPC.cs" | sha256sum -c --quiet - \
|
||||||
|
|| { echo "Runic Gateway: RunicNPC.cs does not match its manifest's sha256 - refusing it"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
tar -xzf "${work}/plugin.tar.gz" -C "${work}"
|
tar -xzf "${work}/plugin.tar.gz" -C "${work}"
|
||||||
local manifest="${work}/runicgateway-rust-plugin/manifest.json"
|
local manifest="${work}/runicgateway-rust-plugin/manifest.json"
|
||||||
@@ -164,15 +179,22 @@ rg_install() {
|
|||||||
|| { echo "Runic Gateway: ${file} does not match the plugin manifest's sha256 - refusing it"; return 1; }
|
|| { echo "Runic Gateway: ${file} does not match the plugin manifest's sha256 - refusing it"; return 1; }
|
||||||
done
|
done
|
||||||
[ -f "${work}/runicgateway-rust-plugin/RunicGateway.cs" ] || { echo "Runic Gateway: the plugin tarball has no RunicGateway.cs"; return 1; }
|
[ -f "${work}/runicgateway-rust-plugin/RunicGateway.cs" ] || { echo "Runic Gateway: the plugin tarball has no RunicGateway.cs"; return 1; }
|
||||||
|
# RunicNPC before the bridge: the bridge reports it at hello. Its data directory
|
||||||
|
# is NOT made here - one made from outside the game is not writable by it
|
||||||
|
# (runicnpc PLAN.md §1.5); RunicNPC makes its own on first load.
|
||||||
|
if [ -n "${npc}" ]; then
|
||||||
|
install -m 644 "${work}/runicnpc/RunicNPC.cs" "${plugins}/RunicNPC.cs"
|
||||||
|
fi
|
||||||
for file in $(jq -r '.files | keys[]' "${manifest}"); do
|
for file in $(jq -r '.files | keys[]' "${manifest}"); do
|
||||||
install -m 644 "${work}/runicgateway-rust-plugin/${file}" "${plugins}/${file}"
|
install -m 644 "${work}/runicgateway-rust-plugin/${file}" "${plugins}/${file}"
|
||||||
done
|
done
|
||||||
# What is installed, readable from the panel's file manager.
|
# What is installed, readable from the panel's file manager.
|
||||||
jq --arg framework "${FRAMEWORK}" --arg installed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
jq --arg framework "${FRAMEWORK}" --arg installed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
'{ bundle, protocol, framework: $framework, installed: $installed,
|
'{ bundle, protocol, framework: $framework, installed: $installed,
|
||||||
sidecar: { tag: .sidecar.tag }, plugin: { tag: .payload.tag, commit: .payload.commit } }' \
|
sidecar: { tag: .sidecar.tag }, plugin: { tag: .payload.tag, commit: .payload.commit } }
|
||||||
|
+ (if .npc == null then {} else { runicnpc: { tag: .npc.tag, api: .npc.api } } end)' \
|
||||||
"${work}/bundle.json" > /mnt/server/rust-link/bundle.json
|
"${work}/bundle.json" > /mnt/server/rust-link/bundle.json
|
||||||
echo "Runic Gateway: installed sidecar $(jq -r '.sidecar.tag' "${work}/bundle.json") and plugin $(jq -r '.payload.tag' "${work}/bundle.json") (${FRAMEWORK})"
|
echo "Runic Gateway: installed sidecar $(jq -r '.sidecar.tag' "${work}/bundle.json") and plugin $(jq -r '.payload.tag' "${work}/bundle.json")${npc:+ and RunicNPC ${npc}} (${FRAMEWORK})"
|
||||||
echo "Runic Gateway: add this server under Admin -> Rust -> Servers; the console prints its URL and, on first boot, its token."
|
echo "Runic Gateway: add this server under Admin -> Rust -> Servers; the console prints its URL and, on first boot, its token."
|
||||||
}
|
}
|
||||||
# In a subshell so `set -e` inside cannot leak into the rest of this script, and
|
# In a subshell so `set -e` inside cannot leak into the rest of this script, and
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
|||||||
// held by the plugin in memory and read by BetterChat on the chat path. Nothing here knows
|
// held by the plugin in memory and read by BetterChat on the chat path. Nothing here knows
|
||||||
// what a title is.
|
// what a title is.
|
||||||
.route("/titles", post(titles))
|
.route("/titles", post(titles))
|
||||||
|
// Protocol 13, RunicNPC (runicnpc PLAN.md stage 4): the site's NPC profiles and the
|
||||||
|
// server's placements. RunicNPC is called by the plugin, never by this process, and nothing
|
||||||
|
// here knows a profile from a placement. Four more thin forwards.
|
||||||
|
.route("/npc/profiles", get(npc_profiles).post(npc_profiles_set))
|
||||||
|
.route("/npc/placements", get(npc_placements))
|
||||||
|
.route("/npc/placement", post(npc_placement))
|
||||||
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -585,6 +591,32 @@ async fn titles(State(st): State<AppState>, Json(body): Json<Value>) -> Response
|
|||||||
forward_object(&st, body, "titles.set", "a title set").await
|
forward_object(&st, body, "titles.set", "a title set").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The server's RunicNPC profiles as RunicNPC holds them: whether a site manages them, each profile,
|
||||||
|
/// and those refused. The website reads this before its first push, to adopt them (D244). Live.
|
||||||
|
async fn npc_profiles(State(st): State<AppState>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "npc.profiles", "reqId": req_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the whole profile set (a push). `npc.ok` lists the profiles RunicNPC refused.
|
||||||
|
async fn npc_profiles_set(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "npc.profiles.set", "a profile push").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every placement, the routes one may walk and the cost warning. Live: an admin's `/rnpc` in game
|
||||||
|
/// changes it at any moment.
|
||||||
|
async fn npc_placements(State(st): State<AppState>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "npc.placements", "reqId": req_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One change to one placement, by `op`: add, set, remove, rename or respawn.
|
||||||
|
async fn npc_placement(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "npc.placement", "a placement change").await
|
||||||
|
}
|
||||||
|
|
||||||
/// What this map is, where its picture comes from, and its monuments (protocol 11, stage one).
|
/// What this map is, where its picture comes from, and its monuments (protocol 11, stage one).
|
||||||
/// Live, and never cached here: a wipe changes the answer, and the module compares its key and
|
/// Live, and never cached here: a wipe changes the answer, and the module compares its key and
|
||||||
/// hash against what it holds to decide whether to fetch at all.
|
/// hash against what it holds to decide whether to fetch at all.
|
||||||
@@ -1092,6 +1124,26 @@ mod tests {
|
|||||||
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
|
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RunicNPC's writes are stamped like every other forward: a placement change cannot become a
|
||||||
|
/// profile push, or anything else, by naming one.
|
||||||
|
#[test]
|
||||||
|
fn a_runicnpc_command_cannot_choose_its_own_command() {
|
||||||
|
let body = json!({ "cmd": "npc.profiles.set", "reqId": "theirs", "op": "remove", "id": "bandit-1" });
|
||||||
|
let stamped = stamp(body, "npc.placement", "r-9").expect("an object is stamped");
|
||||||
|
|
||||||
|
assert_eq!(stamped["cmd"], "npc.placement");
|
||||||
|
assert_eq!(stamped["reqId"], "r-9");
|
||||||
|
assert_eq!(stamped["op"], "remove");
|
||||||
|
assert_eq!(stamped["id"], "bandit-1");
|
||||||
|
|
||||||
|
let body = json!({ "cmd": "npc.placement", "profiles": { "warden": { "health": 250 } } });
|
||||||
|
let stamped = stamp(body, "npc.profiles.set", "r-10").expect("an object is stamped");
|
||||||
|
|
||||||
|
assert_eq!(stamped["cmd"], "npc.profiles.set");
|
||||||
|
assert_eq!(stamped["profiles"]["warden"]["health"], 250);
|
||||||
|
assert!(stamp(json!("warden"), "npc.profiles.set", "r-11").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
/// Protocol 13: an inventory page request keeps its paging fields and cannot become a sync.
|
/// Protocol 13: an inventory page request keeps its paging fields and cannot become a sync.
|
||||||
#[test]
|
#[test]
|
||||||
fn an_inventory_request_cannot_choose_its_own_command() {
|
fn an_inventory_request_cannot_choose_its_own_command() {
|
||||||
|
|||||||
Reference in New Issue
Block a user