chore(release): cut edge over to main — protocols 3–12, the egg, and the first release (rust phase 18, D145) #14
@@ -153,10 +153,18 @@ use tracing_subscriber::EnvFilter;
|
|||||||
/// passes through untouched like the rest of that body. The tally, the kit catalogue and the chat
|
/// passes through untouched like the rest of that body. The tally, the kit catalogue and the chat
|
||||||
/// memory all live in the plugin.
|
/// memory all live in the plugin.
|
||||||
///
|
///
|
||||||
|
/// # Protocol 11 — the map
|
||||||
|
///
|
||||||
|
/// `GET /map`, `GET /map/chunk`, `POST /map/render` and `GET /map/live`: the picture of this
|
||||||
|
/// wipe's map, in slices, and everything that moves on it (the module's PLAN.md §30). Four more
|
||||||
|
/// thin forwards. **Nothing here is stored**: the picture passes through to the website, which
|
||||||
|
/// keeps it, and positions are asked for while somebody is looking and never touch the database
|
||||||
|
/// (D111). Which layer a viewer may see is decided on the website, never here.
|
||||||
|
///
|
||||||
/// `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, §15 the world verbs,
|
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs,
|
||||||
/// §16 the rewards; this constant is one of its four declaration sites.
|
/// §16 the rewards, §17 the map; this constant is one of its four declaration sites.
|
||||||
pub const PROTOCOL_VERSION: u32 = 10;
|
pub const PROTOCOL_VERSION: u32 = 11;
|
||||||
|
|
||||||
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)) {
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
|||||||
.route("/tally/close", post(tally_close))
|
.route("/tally/close", post(tally_close))
|
||||||
.route("/kits", get(kits))
|
.route("/kits", get(kits))
|
||||||
.route("/chat", post(chat))
|
.route("/chat", post(chat))
|
||||||
|
// Protocol 11 (§30): the map. The picture passes through in slices and is never kept
|
||||||
|
// here, and positions are asked for while somebody is looking and never filed (D111). The
|
||||||
|
// plugin decides what exists and what is stale; this moves lines, like everything above.
|
||||||
|
.route("/map", get(map_info))
|
||||||
|
.route("/map/chunk", get(map_chunk))
|
||||||
|
.route("/map/render", post(map_render))
|
||||||
|
.route("/map/live", get(map_live))
|
||||||
.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()
|
||||||
@@ -558,6 +565,55 @@ async fn chat(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
|||||||
forward_object(&st, body, "chat.say", "a chat line").await
|
forward_object(&st, body, "chat.say", "a chat line").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// hash against what it holds to decide whether to fetch at all.
|
||||||
|
async fn map_info(State(st): State<AppState>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "map.info", "reqId": req_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `GET /map/chunk` reads: which picture, and which slice of it.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct MapChunkQuery {
|
||||||
|
#[serde(rename = "mapKey")]
|
||||||
|
map_key: String,
|
||||||
|
sha256: String,
|
||||||
|
n: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One slice of the picture, base64 (stage two). The plugin refuses `stale` when the key or hash
|
||||||
|
/// has moved since the caller asked `/map`, so a fetch that straddles a wipe cannot splice two
|
||||||
|
/// maps — which is also why the two are forwarded as the caller sent them and never filled in.
|
||||||
|
async fn map_chunk(State(st): State<AppState>, Query(q): Query<MapChunkQuery>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({
|
||||||
|
"cmd": "map.fetch",
|
||||||
|
"reqId": req_id,
|
||||||
|
"mapKey": q.map_key,
|
||||||
|
"sha256": q.sha256,
|
||||||
|
"chunk": q.n,
|
||||||
|
});
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the game to draw its own map when there is no cached picture (D109). **Answered at once
|
||||||
|
/// and done on a later frame**: the render stalls the game for seconds, longer than this
|
||||||
|
/// sidecar's reply budget on any large map, so the module polls `/map` for the result instead of
|
||||||
|
/// waiting here.
|
||||||
|
async fn map_render(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||||
|
forward_object(&st, body, "map.render", "a render").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything that moves on the map, every layer at once. Which layers a viewer may see is the
|
||||||
|
/// website's decision (§8.5), so nothing here filters, and nothing here keeps the answer.
|
||||||
|
async fn map_live(State(st): State<AppState>) -> Response {
|
||||||
|
let req_id = st.rpc.next_req_id();
|
||||||
|
let command = json!({ "cmd": "map.live", "reqId": req_id });
|
||||||
|
respond(st.rpc.call(&st.game, command, &req_id).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> {
|
||||||
@@ -1041,6 +1097,31 @@ mod tests {
|
|||||||
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
|
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Protocol 11's one opaque forward is stamped like the rest: a render cannot become a world
|
||||||
|
/// placement by naming one.
|
||||||
|
#[test]
|
||||||
|
fn a_render_cannot_choose_its_own_command() {
|
||||||
|
let body = json!({ "cmd": "world.place", "reqId": "theirs", "actor": "admin" });
|
||||||
|
let stamped = stamp(body, "map.render", "r-5").expect("an object is stamped");
|
||||||
|
|
||||||
|
assert_eq!(stamped["cmd"], "map.render");
|
||||||
|
assert_eq!(stamped["reqId"], "r-5");
|
||||||
|
assert_eq!(stamped["actor"], "admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A map slice must fit the game link's line cap on the way BACK, or the plugin's reply is
|
||||||
|
/// discarded by this process's reader and the caller sees a `504` for a picture that exists.
|
||||||
|
/// The plugin slices at 512 KiB; base64 grows that by a third, and the frame around it is
|
||||||
|
/// small.
|
||||||
|
#[test]
|
||||||
|
fn a_map_slice_fits_the_game_links_line_cap() {
|
||||||
|
let slice: usize = 512 * 1024;
|
||||||
|
let base64 = slice.div_ceil(3) * 4;
|
||||||
|
let envelope = 1024;
|
||||||
|
|
||||||
|
assert!(base64 + envelope <= crate::game::MAX_INBOUND_LINE_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
/// 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