Merge pull request 'feat(sidecar): protocol 9 — five world forwards (phase 13a)' (#9) from feat/phase-13a-world into edge
Reviewed-on: #9
This commit is contained in:
@@ -138,10 +138,17 @@ use tracing_subscriber::EnvFilter;
|
|||||||
/// website. This process moves lines between them and learns nothing about either, which is why
|
/// website. This process moves lines between them and learns nothing about either, which is why
|
||||||
/// the whole protocol is three thin forwards here.
|
/// the whole protocol is three thin forwards here.
|
||||||
///
|
///
|
||||||
|
/// # Protocol 9 — the world verbs
|
||||||
|
///
|
||||||
|
/// `GET /world/monuments`, `GET /world/owned`, `POST /world/zone`, `POST /world/place` and
|
||||||
|
/// `POST /world/revert`: what an event makes in the world — a zone, crates, NPCs — and gives back
|
||||||
|
/// (the module's PLAN.md §28). Five more thin forwards. The allowlist, the bounds, the monument
|
||||||
|
/// vocabulary and the registry of what each run owns all live in the plugin.
|
||||||
|
///
|
||||||
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
|
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
|
||||||
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases; this constant is one
|
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs;
|
||||||
/// of its four declaration sites.
|
/// this constant is one of its four declaration sites.
|
||||||
pub const PROTOCOL_VERSION: u32 = 8;
|
pub const PROTOCOL_VERSION: u32 = 9;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let args = match cli::parse(std::env::args().skip(1)) {
|
let args = match cli::parse(std::env::args().skip(1)) {
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
|||||||
// allowlist, the ceiling and the timer; the website holds the ledger. This moves lines.
|
// allowlist, the ceiling and the timer; the website holds the ledger. This moves lines.
|
||||||
.route("/lease", get(lease_list).post(lease_apply))
|
.route("/lease", get(lease_list).post(lease_apply))
|
||||||
.route("/lease/release", post(lease_release))
|
.route("/lease/release", post(lease_release))
|
||||||
|
// Protocol 9 (§28): what an event makes in the world and gives back. Five more thin
|
||||||
|
// forwards. The allowlist, the bounds, the monument vocabulary and the registry of what
|
||||||
|
// each run owns are all the plugin's; nothing here knows a crate from a zone.
|
||||||
|
.route("/world/monuments", get(world_monuments))
|
||||||
|
.route("/world/owned", get(world_owned))
|
||||||
|
.route("/world/zone", post(world_zone))
|
||||||
|
.route("/world/place", post(world_place))
|
||||||
|
.route("/world/revert", post(world_revert))
|
||||||
.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()
|
||||||
@@ -453,6 +461,53 @@ async fn lease_release(State(st): State<AppState>, Json(body): Json<Value>) -> R
|
|||||||
forward_object(&st, body, "lease.release", "a lease release").await
|
forward_object(&st, body, "lease.release", "a lease release").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This wipe's monuments and the plugin's placeable allowlist, for the authoring form. Live, like
|
||||||
|
/// every catalogue here: a map changes at every wipe, and a cached list would offer monuments that
|
||||||
|
/// no longer exist.
|
||||||
|
async fn world_monuments(State(st): State<AppState>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "world.monuments", "reqId": req_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `GET /world/owned` may narrow to: one run, or every run when absent.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct WorldOwnedQuery {
|
||||||
|
#[serde(rename = "runId")]
|
||||||
|
run_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the world still holds of what events made. The plugin LOOKS for each thing — a restart
|
||||||
|
/// is not proof a crate is gone — so this is the website's reconcile answer, and it is never
|
||||||
|
/// cached.
|
||||||
|
async fn world_owned(State(st): State<AppState>, Query(q): Query<WorldOwnedQuery>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let mut command = json!({ "cmd": "world.owned", "reqId": req_id });
|
||||||
|
|
||||||
|
if let Some(run_id) = q.run_id {
|
||||||
|
command["runId"] = Value::String(run_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a zone a run owns. `world.ok` or `world.error` with a reason.
|
||||||
|
async fn world_zone(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "world.zone", "a zone").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place crates or NPCs a run owns. A repeated idempotency key is answered with the first call's
|
||||||
|
/// ids by the plugin; this process does not know one call from another.
|
||||||
|
async fn world_place(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "world.place", "a placement").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give back what a run owns. **Something already gone is a `200`**, listed as `gone`, because
|
||||||
|
/// reverting a thing a player looted is a success, not a failure.
|
||||||
|
async fn world_revert(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "world.revert", "a revert").await
|
||||||
|
}
|
||||||
|
|
||||||
/// Put `cmd` and `reqId` on a caller's object, **after** it is taken, so they overwrite anything
|
/// Put `cmd` and `reqId` on a caller's object, **after** it is taken, so they overwrite anything
|
||||||
/// the caller put there. `None` when the body is not an object.
|
/// the caller put there. `None` when the body is not an object.
|
||||||
fn stamp(body: Value, cmd: &str, req_id: &str) -> Option<Value> {
|
fn stamp(body: Value, cmd: &str, req_id: &str) -> Option<Value> {
|
||||||
@@ -906,6 +961,17 @@ mod tests {
|
|||||||
assert!(stamp(json!("decay.scale"), "lease.release", "r-1").is_none());
|
assert!(stamp(json!("decay.scale"), "lease.release", "r-1").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_world_command_cannot_choose_its_own_command() {
|
||||||
|
let body = json!({ "cmd": "world.revert", "reqId": "theirs", "runId": "7", "prefab": "crate.elite" });
|
||||||
|
let stamped = stamp(body, "world.place", "r-1").expect("an object is stamped");
|
||||||
|
|
||||||
|
assert_eq!(stamped["cmd"], "world.place");
|
||||||
|
assert_eq!(stamped["reqId"], "r-1");
|
||||||
|
assert_eq!(stamped["runId"], "7");
|
||||||
|
assert_eq!(stamped["prefab"], "crate.elite");
|
||||||
|
}
|
||||||
|
|
||||||
/// The same envelope rule as `perm_sync`, on the route that writes to a filesystem.
|
/// The same envelope rule as `perm_sync`, on the route that writes to a filesystem.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_configuration_write_cannot_choose_its_own_command() {
|
fn a_configuration_write_cannot_choose_its_own_command() {
|
||||||
|
|||||||
Reference in New Issue
Block a user