diff --git a/sidecar/src/main.rs b/sidecar/src/main.rs index 9059e01..39107cf 100644 --- a/sidecar/src/main.rs +++ b/sidecar/src/main.rs @@ -183,6 +183,11 @@ use tracing_subscriber::EnvFilter; /// earned a kit" messages (D333), with `noticesTold` in its report; both pass through untouched /// like the rest of that body. The first-class optional plugins (§11) join this protocol too. /// +/// Economics (§20.3, the module's PLAN_REDESIGNS.md §11.2) adds three forwards: +/// `POST /economics/pay` (coins from an event step, kept once paid), `POST /economics/balances` +/// and `POST /economics/adjust` (staff). The plugin remembers a payment's key; this process keeps +/// nothing. The tally's `balance` and the `economics.wiped` event ride the feed like any frame. +/// /// `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, /// §16 the rewards, §17 the map, §18 the optional mods, §19 the walk's fixes, §20 the step editor; diff --git a/sidecar/src/web.rs b/sidecar/src/web.rs index 3338d73..51a76ec 100644 --- a/sidecar/src/web.rs +++ b/sidecar/src/web.rs @@ -134,6 +134,11 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> { // used a kit up or are on its cooldown, for the warning beside a kit weekend. The plugin // reads Kits and answers counts; the list of players is the website's and is not kept. .route("/kits/uses", post(kits_uses)) + // Protocol 14, Economics (§20.3): thin forwards. The plugin remembers a payment's key so a + // retry never pays twice; nothing about a payment or a balance is kept here. + .route("/economics/pay", post(economics_pay)) + .route("/economics/balances", post(economics_balances)) + .route("/economics/adjust", post(economics_adjust)) .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 @@ -590,6 +595,24 @@ async fn kits_uses(State(st): State, Json(body): Json) -> Respo forward_object(&st, body, "kits.uses", "a kit-use question").await } +/// Coins to a list of players from an event step (protocol 14). The body is `{key, amount, +/// steamIds, message?, delivery?}`; the answer is `economics.paid` with each player's balance +/// before and after, or `economics.error` with a reason. +async fn economics_pay(State(st): State, Json(body): Json) -> Response { + forward_object(&st, body, "economics.pay", "a payment").await +} + +/// The balances of a list of players, with the server's starting balance (protocol 14). +async fn economics_balances(State(st): State, Json(body): Json) -> Response { + forward_object(&st, body, "economics.balances", "a balance question").await +} + +/// Staff deposit, withdraw or set one player's balance (protocol 14). The body is `{key, steamId, +/// op, amount}`; the answer is `economics.adjusted` with the balance before and after. +async fn economics_adjust(State(st): State, Json(body): Json) -> Response { + forward_object(&st, body, "economics.adjust", "a balance adjustment").await +} + /// Say one line in the server's chat. A repeated key is answered `ok` by the plugin and not said /// again, so a retried step never repeats itself. async fn chat(State(st): State, Json(body): Json) -> Response { @@ -1195,6 +1218,36 @@ mod tests { assert_eq!(stamped["notices"][0]["delivery"], "popup"); } + /// Protocol 14, Economics: each body keeps its fields and is stamped with its own command, so + /// a payment cannot be sent as an adjustment by naming one in its body. + #[test] + fn protocol_14_economics_bodies_pass_through() { + let body = json!({ "cmd": "economics.adjust", "key": "7:4", "amount": 500.0, "steamIds": ["76561190000000001"], "message": "You earned {amount} coins", "delivery": "popup" }); + let stamped = stamp(body, "economics.pay", "r-31").expect("an object is stamped"); + + assert_eq!(stamped["cmd"], "economics.pay"); + assert_eq!(stamped["reqId"], "r-31"); + assert_eq!(stamped["key"], "7:4"); + assert_eq!(stamped["amount"], 500.0); + assert_eq!(stamped["message"], "You earned {amount} coins"); + + let stamped = stamp( + json!({ "steamIds": ["76561190000000001"] }), + "economics.balances", + "r-32", + ) + .expect("an object is stamped"); + assert_eq!(stamped["cmd"], "economics.balances"); + assert_eq!(stamped["steamIds"][0], "76561190000000001"); + + let body = json!({ "key": "adj-1", "steamId": "76561190000000001", "op": "withdraw", "amount": 340.5 }); + let stamped = stamp(body, "economics.adjust", "r-33").expect("an object is stamped"); + assert_eq!(stamped["cmd"], "economics.adjust"); + assert_eq!(stamped["op"], "withdraw"); + assert_eq!(stamped["amount"], 340.5); + assert!(stamp(json!([1]), "economics.pay", "r-34").is_none()); + } + /// Protocol 12: a title set is forwarded whole. The markup inside each `text` is the module's /// and the plugin's business, so it must arrive byte for byte as the website composed it. #[test]