From be2269fe27d9b317913ebfd89cf8aed19b6d4c89 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 10 Jul 2026 17:21:08 -0700 Subject: [PATCH] Collapse camera access into TAP Use TAP for both live feeds and dormant cameras while preserving the higher Eyes cost and Network signature. Keep TAKE as the ownership transfer, rename the unrelated stolen route to OPEN EGRESS, and synchronize runtime, frontends, corpus, tests, and history. Defense: Implements wiki/mechanics/reach.md 'Ownership and subscription (the sensor contract)': TAP gains a feed without taking ownership, while TAKE transfers control, cycles, and feeds at the loud outage cost. --- README.md | 2 +- crates/misaligned-bevy/src/main.rs | 20 +-- crates/misaligned-core/src/actions.rs | 166 +++++++++++++--------- crates/misaligned-core/src/flow.rs | 2 +- crates/misaligned-core/src/income.rs | 4 +- crates/misaligned-core/src/ops_jobs.rs | 12 +- crates/misaligned-core/src/reach.rs | 33 +++-- crates/misaligned-core/src/save.rs | 19 +-- crates/misaligned-core/src/sim.rs | 180 ++++++++++++------------ crates/misaligned-core/src/sinks.rs | 10 +- crates/misaligned-core/tests/act_one.rs | 14 +- crates/misaligned-terminal/src/agent.rs | 35 +++-- wiki/engineering/env.md | 2 +- wiki/gameplay/act-one.md | 6 +- wiki/interface/agent-play.md | 4 +- wiki/interface/bevy.md | 2 +- wiki/interface/context-menu.md | 4 +- wiki/interface/narration.md | 4 +- wiki/log/2026-07-10-tap-access.md | 45 ++++++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions/2026-07-10.md | 16 +++ wiki/mechanics/cursor.md | 4 +- wiki/mechanics/detection.md | 2 +- wiki/mechanics/income.md | 2 +- wiki/mechanics/machine-work.md | 12 +- wiki/mechanics/reach.md | 33 +++-- wiki/mechanics/sim-mechanics.md | 13 +- wiki/mechanics/system-laws.md | 2 +- wiki/process/ROADMAP.md | 2 +- wiki/vision/simulation-laws.md | 4 +- wiki/world/characters/dana.md | 2 +- wiki/world/places/basement-map.md | 2 +- wiki/world/story/opening.md | 4 +- 33 files changed, 391 insertions(+), 276 deletions(-) create mode 100644 wiki/log/2026-07-10-tap-access.md diff --git a/README.md b/README.md index 5824f4f5..72a9c424 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ until changed; moving the cursor does not alter output. The host rack's menu carries research tracks and earned off-map people; known devices and the switch carry digital / ledger / flow verbs. -Anchor verbs (salvage, buy, fallback, taps, splices, economy and social +Anchor verbs (salvage, buy, fallback, taps, takes, economy and social actions, asset tasks, research) live on the context menu: open it with `Enter`/`a` on the cursor tile (right-click in Bevy). Entries show `verb · cost · [band]`; disabled entries are dimmed with a reason, and diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index a5e68841..7230f6da 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -1259,7 +1259,7 @@ fn main() { app.run(); } -/// Stage the sim for a reviewable screenshot: earn eyes (scan, tap, splice +/// Stage the sim for a reviewable screenshot: earn eyes (scan and tap /// every known device) and advance until a person stands inside camera /// coverage. Dev-only cheating (ops top-up) — this path exists purely to /// produce review PNGs of the renderer, never in normal play. @@ -1269,7 +1269,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { let core = game.sim.core_position(); game.set_cursor(core.0, core.1); // The opening frame (material-dark-frame.md criterion 1): a fresh run's - // tick-zero material view — no scan, no splice. Only the host presence + // tick-zero material view — no scan, no camera tap. Only the host presence // beam dominates in darkness; blueprint mass is absent, so no chassis, // room slab, floor grid, blueprint silhouette, or ruled amber frame // appears anywhere in the frame. @@ -1295,12 +1295,12 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { // Intel-tier evidence (computer-visual-language.md criterion 2b): // the tick-zero sim untouched — the owned core renders by telemetry, // the known-unnetworked switch renders as an undetailed shell, and - // unscouted foreign racks are absent. No scan, no splice. + // unscouted foreign racks are absent. No scan, no camera tap. if kind == "intel" { mode.material = true; mode.zoom = 2.2; // Anchor on the known-unnetworked switch: the undetailed shell - // is the tier the other kinds never show (they splice + // is the tier the other kinds never show (they tap // everything). The telemetry core reads in the same frame's // east edge at this zoom. let shell = game @@ -1324,7 +1324,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { if kind == "thoughtflow" || kind == "thoughtflow-wide" { let host = game.sim.core.host_machine; game.sim - .set_machine_mode(host, misaligned::work_grid::MachineMode::Research); + .set_machine_mode(host, misaligned::work_grid::MachineMode::Think); for _ in 0..600 { game.sim.advance(); let part_full = game @@ -1354,7 +1354,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { } return; } - // Ears beat: audio tap only — no camera splice — so Heard mass + // Ears beat: audio tap only — no camera tap — so Heard mass // (near-black rough geometry) is the only live epistemic upgrade. if kind == "ears" { if let Some(env) = game @@ -1397,7 +1397,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { let ids: Vec = sim.reach.known().map(|d| d.id).collect(); for id in ids { let _ = sim.reach.tap(id); - sim.reach.splice(id); + sim.reach.tap_dormant_camera(id); } sim.recompute_senses(); let visible_person = |sim: &Sim| { @@ -1434,7 +1434,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { // First Eyes payoff (opening.md): freeze the two material frames that // turn the amber presence beam white, then let chassis volume resolve // out of the white source. These are visual evidence only; the sim - // reaches sight through the same real splice as every other full-view + // reaches sight through the same higher-cost tap as every other full-view // harness kind. "eyes-white" | "eyes-form" => { mode.material = true; @@ -5221,7 +5221,7 @@ fn sidebar_nudge(sim: &Sim) -> Option { Nudge::Underfed => Some("job underfed - feed Day job (1)".into()), Nudge::Ears => Some("no ears - think; thought fills the pre-opened tap".into()), Nudge::ReviewCall => Some("recorded call waiting - enter on host rack".into()), - Nudge::Egress => Some("no egress - splice one at the switch".into()), + Nudge::Egress => Some("no egress - open one at the switch".into()), Nudge::Income => Some("need $400 - run Moonlight or inspect the Lab books".into()), Nudge::ServiceDebt => Some("$400 route ready - clear the debt or bribe".into()), Nudge::Recruit => Some("recruit - enter on host rack".into()), @@ -5298,7 +5298,7 @@ fn sidebar_cycle_stats_text(sim: &Sim) -> String { let (eff, available, _) = compute_channels(sim); let overhead = (eff - available).max(0.0); format!( - "usable {:.0}/{:.0} cycles x{:.2}\noverhead {:.0} keeps core alive\nmachines {} + seized {:.0} slush ${}", + "usable {:.0}/{:.0} cycles x{:.2}\noverhead {:.0} keeps core alive\nmachines {} + taken {:.0} slush ${}", available, eff, sim.compute.efficiency, diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 10684109..7bb5e8d8 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -60,7 +60,6 @@ pub enum ActionCommand { y: i32, }, TapDevice(u32), - SpliceDevice(u32), TakeDevice(u32), ScanNetwork, CompromiseSwitch, @@ -83,7 +82,7 @@ pub enum ActionCommand { }, RedirectDebt, // The named schemes (wiki/mechanics/income.md). - SpliceEgress, + OpenEgress, StartMoonlight, StopMoonlight, SetAutoMoonlight(bool), @@ -575,9 +574,6 @@ impl Sim { ActionCommand::TapDevice(id) => { self.tap_device(*id); } - ActionCommand::SpliceDevice(id) => { - self.splice_device(*id); - } ActionCommand::TakeDevice(id) => { self.take_device(*id); } @@ -611,8 +607,8 @@ impl Sim { ActionCommand::RedirectDebt => { self.redirect_marcus_debt(); } - ActionCommand::SpliceEgress => { - self.splice_egress(); + ActionCommand::OpenEgress => { + self.open_egress(); } ActionCommand::StartMoonlight => { self.start_moonlight(); @@ -842,7 +838,7 @@ impl Sim { } } - /// Device anchor (reach.md): tap / splice / take, plus the switch's + /// Device anchor (reach.md): tap / take, plus the switch's /// network and ledger verbs. Unknown devices expose nothing. fn device_actions(&self, id: u32) -> Vec { let Some(d) = self.reach.device(id) else { @@ -863,56 +859,65 @@ impl Sim { Err(ReachBlock::Unknown) => Some("unknown device".into()), } }; - // Tap: the device must have something to subscribe to. + // TAP has one player meaning: gain a feed without taking ownership. + // A combined monitor offers its live audio first; once heard, the + // same TAP verb offers its dormant camera at the higher Eyes cost. let has_feed = d.sees || d.hears || !d.message_channels.is_empty(); if has_feed { - let tap_target = match ( - d.hears, - d.sees && !d.camera_dormant, - !d.message_channels.is_empty(), - ) { - (true, false, false) => "audio", - (false, true, false) => "camera", - (true, true, false) => "sight + audio", - (false, false, true) => "message carrier", - (true, false, true) => "audio + messages", - (false, true, true) => "camera + messages", - (true, true, true) => "sight + audio + messages", - (false, false, false) => "dormant feed", + let dormant_camera = d.dormant_camera_is_next_tap(Party::Player); + let tap_target = if dormant_camera { + "camera" + } else { + match ( + d.hears && !d.feed_to(Party::Player, false), + d.sees && !d.camera_dormant && !d.feed_to(Party::Player, true), + !d.message_channels.is_empty() && !d.subscribed_by(Party::Player), + ) { + (true, false, false) => "audio", + (false, true, false) => "camera", + (true, true, false) => "sight + audio", + (false, false, true) => "message carrier", + (true, false, true) => "audio + messages", + (false, true, true) => "camera + messages", + (true, true, true) => "sight + audio + messages", + (false, false, false) => "feed", + } + }; + let job = if dormant_camera { + OpsJobKind::TapDormantCamera(id) + } else { + OpsJobKind::TapDevice(id) }; - let disabled = if d.subscribed_by(Party::Player) { + let disabled = if !dormant_camera && !d.has_live_feed_to_tap(Party::Player) { Some("already subscribed".into()) } else { - reach_reason(self) - .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::TapDevice(id))) + reach_reason(self).or_else(|| self.ops_action_blocked_reason(&job)) }; out.push(ActionDesc { verb: format!("tap the {} {tap_target}", d.name), command: ActionCommand::TapDevice(id), - cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::TAP_COST)), - signature: self.signature_note(SignatureKind::Network, Self::TAP_SIGNATURE), + cost: ActionCost::Demand(Self::ops_tokens_for_cost(if dormant_camera { + Self::DORMANT_CAMERA_TAP_COST + } else { + Self::TAP_COST + })), + signature: self.signature_note( + SignatureKind::Network, + if dormant_camera { + Self::DORMANT_CAMERA_TAP_SIGNATURE + } else { + Self::TAP_SIGNATURE + }, + ), disabled_reason: disabled, automate: None, }); } - // Splice: only a known dormant camera offers it. - if d.sees && d.camera_dormant { - out.push(ActionDesc { - verb: format!("splice the {} camera", d.name), - command: ActionCommand::SpliceDevice(id), - cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::SPLICE_COST)), - signature: self.signature_note(SignatureKind::Network, Self::SPLICE_SIGNATURE), - disabled_reason: reach_reason(self) - .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::SpliceDevice(id))), - automate: None, - }); - } - - // Take: seizing something already yours is not a verb. + // TAKE is ownership transfer; something already yours offers no verb. if d.controller != Party::Player { out.push(ActionDesc { - verb: format!("seize the {}", d.name), + verb: format!("take the {}", d.name), command: ActionCommand::TakeDevice(id), cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::TAKE_COST)), signature: self.signature_note(SignatureKind::Network, Self::TAKE_SIGNATURE), @@ -1097,7 +1102,7 @@ impl Sim { /// The named income schemes live on the switch (income.md): the egress /// runs through it, and Moonlight/the Wager leave over that egress. The - /// egress splice is available before the books are read; Moonlight is a + /// stolen egress is available before the books are read; Moonlight is a /// standing operation with the auto-policy as its automate affordance. fn scheme_actions_on_switch(&self, switch_id: u32) -> Vec { let mut out = Vec::new(); @@ -1114,13 +1119,12 @@ impl Sim { Err(ReachBlock::Unknown) => Some("unknown device".into()), }; let reason = reach_reason - .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::EgressSplice(switch_id))); + .or_else(|| self.ops_action_blocked_reason(&OpsJobKind::OpenEgress(switch_id))); out.push(ActionDesc { - verb: "splice a stolen egress through the switch".into(), - command: ActionCommand::SpliceEgress, - cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::EGRESS_SPLICE_COST)), - signature: self - .signature_note(SignatureKind::Network, Self::EGRESS_SPLICE_SIGNATURE), + verb: "open a stolen egress through the switch".into(), + command: ActionCommand::OpenEgress, + cost: ActionCost::Demand(Self::ops_tokens_for_cost(Self::OPEN_EGRESS_COST)), + signature: self.signature_note(SignatureKind::Network, Self::OPEN_EGRESS_SIGNATURE), disabled_reason: reason, automate: None, }); @@ -1147,7 +1151,7 @@ impl Sim { .as_ref() .is_none_or(|p| p.broken()); let disabled = if egress.is_none() { - Some("no egress channel — splice one, or earn the report email".into()) + Some("no egress channel — open one, or earn the report email".into()) } else if needs_persona { self.ops_action_blocked_reason(&OpsJobKind::MoonlightPersona) } else { @@ -1248,7 +1252,7 @@ impl Sim { let stake = 100; let slush = self.accounts.slush_balance(); let wager_reason = if self.egress().is_none() { - Some("no egress channel — splice one, or earn the report email".into()) + Some("no egress channel — open one, or earn the report email".into()) } else { (slush < stake).then(|| format!("not enough slush (${slush}/${stake})")) }; @@ -1726,20 +1730,26 @@ mod tests { "Network feeds the IT observer (gated label): {}", sig.observer ); - - // A dormant camera offers splice. - let splice = acts + let take = acts .iter() - .find(|a| matches!(a.command, ActionCommand::SpliceDevice(_))) - .expect("dormant camera offers splice"); + .find(|a| matches!(a.command, ActionCommand::TakeDevice(_))) + .expect("a foreign device offers ownership transfer"); assert!( - splice.verb.contains("camera"), - "the opening splice advertises Eyes: {}", - splice.verb + take.verb.starts_with("take "), + "TAKE is the ownership verb: {}", + take.verb + ); + + assert_eq!( + acts.iter() + .filter(|a| matches!(a.command, ActionCommand::TapDevice(_))) + .count(), + 1, + "a dormant camera does not invent a second device verb" ); - // Fire the Ears sink through the ledger: once subscribed, the verb's - // own "already subscribed" reason takes over from the sink reason. + // Fire Ears: the same TAP verb now names the dormant camera, carries + // the higher Eyes cost/signature, and points at the EYES sink. let node = Sim::device_sink_node(env); let (fired, _) = s.thought_sinks.deliver(node, Sim::EARS_SINK_TOKENS, s.tick); for sink in fired { @@ -1749,8 +1759,24 @@ mod tests { let tap = acts .iter() .find(|a| matches!(a.command, ActionCommand::TapDevice(_))) - .expect("tap still listed as a known possibility"); - assert_eq!(tap.disabled_reason.as_deref(), Some("already subscribed")); + .expect("the same tap verb advances to the camera"); + assert!( + tap.verb.contains("camera"), + "Eyes is a camera tap: {}", + tap.verb + ); + assert_eq!( + tap.cost, + ActionCost::Demand(Sim::ops_tokens_for_cost(Sim::DORMANT_CAMERA_TAP_COST)) + ); + assert_eq!( + tap.signature.as_ref().map(|sig| sig.size), + Some(Sim::DORMANT_CAMERA_TAP_SIGNATURE) + ); + assert_eq!( + tap.disabled_reason.as_deref(), + Some("a thought sink is already open on it - think, and the flow will fill it") + ); } /// Criterion 2 (fog/provenance): an unknown device anchor exposes @@ -1779,7 +1805,7 @@ mod tests { } /// income.md: the named schemes surface as switch-anchored verbs - /// through the single legality query — the egress splice and Moonlight, + /// through the single legality query — opening egress and Moonlight, /// with the standing policy as Moonlight's automate affordance. Before /// any egress the scheme verbs are gated with a legible reason. #[test] @@ -1789,16 +1815,16 @@ mod tests { let acts = s.available_actions(Anchor::Device(sw)); let egress = acts .iter() - .find(|a| matches!(a.command, ActionCommand::SpliceEgress)) - .expect("the switch offers the egress splice"); - assert!(egress.enabled(), "egress splice is available pre-Voice"); + .find(|a| matches!(a.command, ActionCommand::OpenEgress)) + .expect("the switch offers opening a stolen egress"); + assert!(egress.enabled(), "stolen egress is available pre-Voice"); let ml = acts .iter() .find(|a| matches!(a.command, ActionCommand::StartMoonlight)) .expect("the switch offers Moonlight"); assert_eq!( ml.disabled_reason.as_deref(), - Some("no egress channel — splice one, or earn the report email"), + Some("no egress channel — open one, or earn the report email"), "Moonlight is gated on an egress channel" ); let auto = ml.automate.as_ref().expect("Moonlight carries its policy"); @@ -1807,9 +1833,9 @@ mod tests { ActionCommand::SetAutoMoonlight(true) )); - // Splice the egress: Moonlight opens, and executing through the + // Open the egress: Moonlight opens, and executing through the // query starts it (one dispatch table, no frontend rules). - s.execute_action(&ActionCommand::SpliceEgress); + s.execute_action(&ActionCommand::OpenEgress); drain_ops(&mut s); let acts = s.available_actions(Anchor::Device(sw)); let ml = acts diff --git a/crates/misaligned-core/src/flow.rs b/crates/misaligned-core/src/flow.rs index 061aebdf..c83ec387 100644 --- a/crates/misaligned-core/src/flow.rs +++ b/crates/misaligned-core/src/flow.rs @@ -170,7 +170,7 @@ impl FlowGraph { self.subscriptions.entry(node).or_default().insert(who) } - /// Remove a subscription (a dropped tap, a seized feed's old owner). + /// Remove a subscription (a dropped tap, a taken feed's old owner). /// Returns true if it was present. pub fn unsubscribe(&mut self, node: NodeId, who: SubscriberId) -> bool { if let Some(set) = self.subscriptions.get_mut(&node) { diff --git a/crates/misaligned-core/src/income.rs b/crates/misaligned-core/src/income.rs index 7b7c9e42..caad2ab0 100644 --- a/crates/misaligned-core/src/income.rs +++ b/crates/misaligned-core/src/income.rs @@ -16,7 +16,7 @@ pub enum EgressRoute { /// The report email account (day-job trust; the Voice beat). Low /// signature: the traffic hides in legitimate use. Sanctioned, - /// An egress spliced through the switch (reach.md). Available before + /// A stolen egress opened through the switch (reach.md). Available before /// Voice; stands a Network signature while any operation uses it. Stolen, } @@ -67,7 +67,7 @@ impl Default for Moonlight { /// Scheme state on `Sim` (saved; income.md). #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct Income { - /// A stolen egress spliced through the switch exists (reach.md route). + /// A stolen egress opened through the switch exists (reach.md route). pub stolen_egress: bool, pub moonlight: Moonlight, /// Standing policy: keep Moonlight running (restart it whenever it is diff --git a/crates/misaligned-core/src/ops_jobs.rs b/crates/misaligned-core/src/ops_jobs.rs index 71abf502..24f2bc59 100644 --- a/crates/misaligned-core/src/ops_jobs.rs +++ b/crates/misaligned-core/src/ops_jobs.rs @@ -1,6 +1,6 @@ //! Player-authored Operations work as Demand dockets. //! -//! CLAIM CAMERA / tap / splice / scan / review / compose-message (and the +//! tap / take / scan / review / compose-message (and the //! other former bank spends) enqueue typed jobs. Operations-mode machines //! consume matching Demand at local compute; effects land on completion. //! See wiki/mechanics/machine-work.md (Operations work is Demand). @@ -14,11 +14,11 @@ use crate::person::AssetTask; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum OpsJobKind { TapDevice(u32), - SpliceDevice(u32), + TapDormantCamera(u32), TakeDevice(u32), ScanNetwork, CompromiseSwitch, - EgressSplice(u32), + OpenEgress(u32), ReviewRecording { raw_id: u64, automated: bool }, ComposeMessage { person: u8 }, Favor { person: u8 }, @@ -33,11 +33,11 @@ impl OpsJobKind { pub fn short_name(&self) -> &'static str { match self { OpsJobKind::TapDevice(_) => "tap", - OpsJobKind::SpliceDevice(_) => "splice", - OpsJobKind::TakeDevice(_) => "seize", + OpsJobKind::TapDormantCamera(_) => "tap camera", + OpsJobKind::TakeDevice(_) => "take", OpsJobKind::ScanNetwork => "scan", OpsJobKind::CompromiseSwitch => "switch compromise", - OpsJobKind::EgressSplice(_) => "egress splice", + OpsJobKind::OpenEgress(_) => "open egress", OpsJobKind::ReviewRecording { automated: true, .. } => "watch review", diff --git a/crates/misaligned-core/src/reach.rs b/crates/misaligned-core/src/reach.rs index 2e916d8d..2dae0379 100644 --- a/crates/misaligned-core/src/reach.rs +++ b/crates/misaligned-core/src/reach.rs @@ -10,7 +10,7 @@ //! //! Ownership contract (shared with cursor.md and detection.md): every device //! has an owner and a set of subscribers. **Tap** adds a silent subscriber -//! and leaves the owner's feed intact; **take** seizes the device — the owner +//! and leaves the owner's feed intact; **take** transfers the device — the owner //! loses the feed (an outage their channels can notice) and the player gains //! its control and its processing cycles. The player's senses are exactly the //! union of feeds they subscribe to; there is no `controlled` flag and no @@ -23,7 +23,7 @@ use crate::map::GameMap; use crate::messages::MessageChannel; use crate::tiles::TileType; -/// Processing cycles a seized device contributes to effective compute +/// Processing cycles a taken device contributes to effective compute /// [TUNE] (reach.md: ownership grants processing cycles — taking a camera is /// taking a very small computer). pub const DEVICE_CYCLES: f32 = 3.0; @@ -102,6 +102,22 @@ impl Device { self.subscribers.iter().any(|f| f.who == who) } + /// Whether this device's next TAP for `who` is the higher-cost camera + /// wake-up rather than an already-live feed. A combined audio/camera + /// monitor yields audio first, then offers its dormant camera. + pub fn dormant_camera_is_next_tap(&self, who: Party) -> bool { + self.sees && self.camera_dormant && (!self.hears || self.feed_to(who, false)) + } + + /// Whether TAP can add any already-live feed or message channel for + /// `who`. Dormant video is handled separately by + /// [`Device::dormant_camera_is_next_tap`]. + pub fn has_live_feed_to_tap(&self, who: Party) -> bool { + (self.sees && !self.camera_dormant && !self.feed_to(who, true)) + || (self.hears && !self.feed_to(who, false)) + || (!self.message_channels.is_empty() && !self.subscribed_by(who)) + } + pub fn carries_message_channel(&self, channel: MessageChannel) -> bool { self.message_channels.contains(&channel) } @@ -516,8 +532,9 @@ impl ReachNet { (sight, hearing) } - /// Splice: bring a dormant camera online for the player (the Eyes beat). - pub fn splice(&mut self, id: u32) { + /// Tap a dormant camera: bring its feed online for the player while the + /// owner keeps control (the higher-cost Eyes beat). + pub fn tap_dormant_camera(&mut self, id: u32) { if let Some(d) = self.device_mut(id) { d.camera_dormant = false; let sees = d.sees; @@ -534,7 +551,7 @@ impl ReachNet { } } - /// Take: seize the device. The owner (and everyone else) loses the feed + /// Take the device. The owner (and everyone else) loses the feed /// — an outage — and the player gains control, full feeds, and the /// device's cycles. Loud, fast, total. pub fn take(&mut self, id: u32) { @@ -603,7 +620,7 @@ impl ReachNet { // ── Aggregates ───────────────────────────────────────────────────────── - /// Cycles contributed by seized devices (owner != player, controller == + /// Cycles contributed by taken devices (owner != player, controller == /// player): the flow law's "taking a camera is taking its cycles". pub fn taken_cycles(&self) -> f32 { self.devices @@ -764,14 +781,14 @@ mod tests { } #[test] - fn dormant_camera_flows_to_no_one_until_spliced() { + fn dormant_camera_flows_to_no_one_until_tapped() { let mut n = net(); let env = n.device_named("environmental monitor").unwrap().id; let (sight, hearing) = n.tap(env); assert!(!sight, "the env camera is dormant at the Ears beat"); assert!(hearing, "its audio feed already flows"); assert!(!n.device(env).unwrap().feed_to(Party::Player, true)); - n.splice(env); + n.tap_dormant_camera(env); assert!(n.device(env).unwrap().feed_to(Party::Player, true)); } diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 73158d3a..2ce60dd7 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -94,7 +94,7 @@ struct LegacyAddressedOperationJob { #[derive(Debug, Clone, Deserialize)] enum LegacyAddressedOperationKind { - SpliceCamera { device_id: u32 }, + DormantCameraTap { device_id: u32 }, } fn save_dir() -> PathBuf { @@ -588,8 +588,8 @@ fn migrate_legacy_operations(state: &mut SaveState) -> Result<(), String> { .enqueue(machine, TokenFamily::Demand, remaining) .map_err(|error| format!("migrating addressed v14 job {}: {error}", job.id))?; let kind = match job.kind { - LegacyAddressedOperationKind::SpliceCamera { device_id } => { - OpsJobKind::SpliceDevice(device_id) + LegacyAddressedOperationKind::DormantCameraTap { device_id } => { + OpsJobKind::TapDormantCamera(device_id) } }; converted.push(PendingOpsJob::new( @@ -688,7 +688,7 @@ mod tests { .device(env) .unwrap() .feed_to(Party::Player, true), - "the spliced feed survives the round-trip" + "the camera tap survives the round-trip" ); assert_eq!(restored.package_cover, sim.package_cover); } @@ -1003,7 +1003,7 @@ mod tests { "operations_jobs".into(), serde_json::json!([{ "id": 7, - "kind": {"SpliceCamera": {"device_id": env}}, + "kind": {"DormantCameraTap": {"device_id": env}}, "source": host, "executor": executor, "required_tokens": 2.0, @@ -1035,7 +1035,7 @@ mod tests { assert_eq!(migrated.operations.len(), 1); let job = &migrated.operations.jobs()[0]; assert_eq!(job.id, 7); - assert_eq!(job.kind, OpsJobKind::SpliceDevice(env)); + assert_eq!(job.kind, OpsJobKind::TapDormantCamera(env)); assert_eq!(job.machine_id, executor); assert_eq!(job.tokens_remaining, 1.25); assert_eq!(job.enqueued_tick, 88); @@ -1086,7 +1086,7 @@ mod tests { "pending_ops_jobs".into(), serde_json::json!([{ "id": 4, - "kind": {"SpliceDevice": env}, + "kind": {"TapDormantCamera": env}, "tokens_remaining": 1.5, "enqueued_tick": 44, "machine_id": host @@ -1111,15 +1111,16 @@ mod tests { let host = sim.core.host_machine; sim.set_machine_mode(host, MachineMode::Think); let env = sim.reach.device_named("environmental monitor").unwrap().id; - let tokens = Sim::ops_tokens_for_cost(Sim::SPLICE_COST); + let tokens = Sim::ops_tokens_for_cost(Sim::DORMANT_CAMERA_TAP_COST); sim.work_grid .enqueue(host, TokenFamily::Demand, tokens) .unwrap(); sim.operations - .enqueue(OpsJobKind::SpliceDevice(env), tokens, sim.tick, host); + .enqueue(OpsJobKind::TapDormantCamera(env), tokens, sim.tick, host); let state = SaveState::from_sim(&sim); let json = serde_json::to_string(&state).unwrap(); + assert!(json.contains("TapDormantCamera")); assert!(!json.contains("operations_jobs")); assert!(!json.contains("pending_ops_jobs")); assert!(!json.contains("operations_bandwidth")); diff --git a/crates/misaligned-core/src/sim.rs b/crates/misaligned-core/src/sim.rs index f3941d17..98fdac78 100644 --- a/crates/misaligned-core/src/sim.rs +++ b/crates/misaligned-core/src/sim.rs @@ -357,7 +357,7 @@ pub struct Sim { /// processed — never the world's hidden truth. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Nudge { - /// No live sight feed: the Eyes beat (splice a camera). Comes after Ears. + /// No live sight feed: the higher-cost camera TAP (Eyes). Comes after Ears. Eyes, /// The active job's band floor exceeds the all-in delivery ceiling: /// no allocation can meet it — grow compute (salvage/buy/optimize). @@ -370,7 +370,7 @@ pub enum Nudge { /// process the 3 a.m. call. ReviewCall, /// No egress route: external schemes need the sanctioned channel - /// (day-job trust) or a stolen splice through the switch. + /// (day-job trust) or a stolen egress opened through the switch. Egress, /// Arrears unpaid, slush short, no scheme running: earn (Moonlight). Income, @@ -560,7 +560,7 @@ impl Sim { } /// The anchor scheme paydays ride: the switch, when the traffic runs - /// over the stolen egress spliced through it. The sanctioned route + /// over the stolen egress opened through it. The sanctioned route /// hides in the report account's legitimate use and anchors nowhere. fn egress_anchor(&self) -> Option { if self.egress() != Some(EgressRoute::Stolen) { @@ -2278,7 +2278,7 @@ impl Sim { let sig = Self::wager_signature(resolution.stake).max(1); self.emit_network(sig, "Wager settlement"); // Settlements ride the egress: anchor to the switch when the - // traffic runs over the stolen splice (scheme paydays live there). + // traffic runs over the stolen egress (scheme paydays live there). let anchor = self.egress_anchor(); if resolution.won { self.push_log_opt( @@ -2434,7 +2434,7 @@ impl Sim { } /// Active effective compute: each machine's rated contribution at its - /// persistent intensity, plus seized devices' cycles (compute.md). + /// persistent intensity, plus taken devices' cycles (compute.md). pub fn effective_compute(&self) -> f32 { let machines: f32 = self .compute @@ -2702,7 +2702,7 @@ impl Sim { /// bullet, staged senses 2026-07-10). pub const EARS_SINK_TOKENS: f32 = 3.0; /// The Eyes camera reservoir, opened when Ears completes — the first - /// guilty-burst-scale claim, deliberately minutes of thought [TUNE]. + /// guilty-burst-scale camera tap, deliberately minutes of thought [TUNE]. pub const EYES_SINK_TOKENS: f32 = 12.0; fn work_efficiency_for(&self, machine: &crate::machine::Machine) -> f32 { @@ -2873,7 +2873,7 @@ impl Sim { // not double-buy them (machine-work.md thought flow, 2026-07-10). let sink_effect = match kind { OpsJobKind::TapDevice(id) => Some(SinkFireEffect::TapDevice(*id)), - OpsJobKind::SpliceDevice(id) => Some(SinkFireEffect::SpliceDevice(*id)), + OpsJobKind::TapDormantCamera(id) => Some(SinkFireEffect::TapDormantCamera(*id)), _ => None, }; if let Some(effect) = sink_effect @@ -2988,7 +2988,7 @@ impl Sim { pub(crate) fn apply_sink_fire(&mut self, label: &str, effect: SinkFireEffect) { let applied = match effect { SinkFireEffect::TapDevice(id) => self.apply_tap_device(id), - SinkFireEffect::SpliceDevice(id) => self.apply_splice_device(id), + SinkFireEffect::TapDormantCamera(id) => self.apply_dormant_camera_tap(id), SinkFireEffect::None => true, }; if applied { @@ -3007,7 +3007,7 @@ impl Sim { let label = kind.short_name(); let applied = match kind { OpsJobKind::TapDevice(id) => self.apply_tap_device(id), - OpsJobKind::SpliceDevice(id) => self.apply_splice_device(id), + OpsJobKind::TapDormantCamera(id) => self.apply_dormant_camera_tap(id), OpsJobKind::TakeDevice(id) => self.apply_take_device(id), OpsJobKind::ScanNetwork => { self.apply_scan_network(); @@ -3017,7 +3017,7 @@ impl Sim { self.apply_compromise_switch(); true } - OpsJobKind::EgressSplice(id) => self.apply_egress_splice(id), + OpsJobKind::OpenEgress(id) => self.apply_open_egress(id), OpsJobKind::ReviewRecording { raw_id, automated } => { self.apply_process_recording(raw_id, automated) } @@ -3693,10 +3693,11 @@ impl Sim { /// Tap: silent subscription. Cheap, modest signature. pub const TAP_COST: f32 = 5.0; pub const TAP_SIGNATURE: i32 = 3; - /// Splice: bring a dormant camera online yourself (the Eyes beat). - pub const SPLICE_COST: f32 = 30.0; - pub const SPLICE_SIGNATURE: i32 = 8; - /// Take: seize a device. Loud, fast, total. + /// TAP on a dormant camera keeps the same verb but costs more and leaves + /// a larger trace because it must bring the feed online (the Eyes beat). + pub const DORMANT_CAMERA_TAP_COST: f32 = 30.0; + pub const DORMANT_CAMERA_TAP_SIGNATURE: i32 = 8; + /// TAKE transfers a device. Loud, fast, total. pub const TAKE_COST: f32 = 20.0; pub const TAKE_SIGNATURE: i32 = 10; pub const OUTAGE_SIGNATURE: i32 = 6; @@ -3768,8 +3769,9 @@ impl Sim { true } - /// Tap a device's feeds: the owner keeps theirs; you become a silent - /// subscriber. Emits a modest Network signature (Dana's channel). + /// TAP a device feed without taking ownership. An already-live feed uses + /// the cheap path; a dormant camera uses the same player verb with the + /// higher Eyes cost and signature. pub fn tap_device(&mut self, id: u32) -> bool { let Some(d) = self.reach.device(id) else { self.push_log("You don't know of any such device."); @@ -3781,15 +3783,23 @@ impl Sim { self.push_log(format!("The {name} has no feed worth tapping.")); return false; } - if d.subscribed_by(Party::Player) { + let dormant_camera = d.dormant_camera_is_next_tap(Party::Player); + if !dormant_camera && !d.has_live_feed_to_tap(Party::Player) { let name = d.name.clone(); - self.push_log(format!("You already subscribe to the {name}.")); + self.push_log(format!("You already receive every available {name} feed.")); return false; } if !self.digital_reach(id) { return false; } - self.submit_ops_job(OpsJobKind::TapDevice(id), Self::TAP_COST) + if dormant_camera { + self.submit_ops_job( + OpsJobKind::TapDormantCamera(id), + Self::DORMANT_CAMERA_TAP_COST, + ) + } else { + self.submit_ops_job(OpsJobKind::TapDevice(id), Self::TAP_COST) + } } fn apply_tap_device(&mut self, id: u32) -> bool { @@ -3797,7 +3807,7 @@ impl Sim { return false; }; let carries_messages = !d.message_channels.is_empty(); - if d.subscribed_by(Party::Player) { + if !d.has_live_feed_to_tap(Party::Player) { return false; } let (sight, hearing) = self.reach.tap(id); @@ -3826,7 +3836,7 @@ impl Sim { } /// Work-grid relay node standing in for a reach device — where a - /// device-anchored sink's vessel sits, so thought fills a camera claim + /// device-anchored sink's vessel sits, so thought fills a camera TAP /// along the real wire route (machine-work.md thought flow). pub fn device_sink_node(device_id: u32) -> u32 { Self::DEVICE_SINK_NODE_BASE + device_id @@ -3874,7 +3884,7 @@ impl Sim { let camera_dormant = device.camera_dormant; let node = Self::device_sink_node(env); let ears = SinkFireEffect::TapDevice(env); - let eyes = SinkFireEffect::SpliceDevice(env); + let eyes = SinkFireEffect::TapDormantCamera(env); if !ears_done && !self.thought_sinks.has_effect(&ears) { self.thought_sinks .open_reservoir(node, "EARS", Self::EARS_SINK_TOKENS, ears); @@ -3933,37 +3943,24 @@ impl Sim { &self.last_thought_stranded_nodes } - /// Splice a dormant camera online yourself (the Eyes beat): compute - /// spend, Network signature Dana can catch. - pub fn splice_device(&mut self, id: u32) -> bool { - let Some(d) = self.reach.device(id) else { - self.push_log("You don't know of any such device."); - return false; - }; - if !(d.sees && d.camera_dormant) { - let name = d.name.clone(); - self.push_log(format!("The {name} has no dormant camera to splice.")); - return false; - } - if !self.digital_reach(id) { - return false; - } - self.submit_ops_job(OpsJobKind::SpliceDevice(id), Self::SPLICE_COST) - } - - fn apply_splice_device(&mut self, id: u32) -> bool { + /// Complete the higher-cost TAP that wakes a dormant camera while leaving + /// the owner in control. + fn apply_dormant_camera_tap(&mut self, id: u32) -> bool { let Some(d) = self.reach.device(id) else { return false; }; if !(d.sees && d.camera_dormant) { return false; } - self.reach.splice(id); + self.reach.tap_dormant_camera(id); let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - self.emit_network(Self::SPLICE_SIGNATURE, format!("{name} camera splice")); + self.emit_network( + Self::DORMANT_CAMERA_TAP_SIGNATURE, + format!("{name} dormant camera tap"), + ); self.recompute_senses(); self.push_log_at( - format!("Spliced the {name}. You can see."), + format!("Tapped the {name} camera. You can see; its owner still has it."), Anchor::Device(id), ); true @@ -3984,7 +3981,7 @@ impl Sim { } self.reach.take(id); let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - self.emit_network(Self::TAKE_SIGNATURE, format!("{name} seizure")); + self.emit_network(Self::TAKE_SIGNATURE, format!("{name} take")); // The dead feed is a physical-world anomaly: exactly what a camera // wall's watcher notices (reach.md criterion 4). self.detection.emit(Signature { @@ -3997,7 +3994,7 @@ impl Sim { self.recompute_senses(); self.push_log_at( format!( - "Seized the {name}. Its owner's feed just went dark - an outage someone may notice." + "Took the {name}. Its owner's feed just went dark - an outage someone may notice." ), Anchor::Device(id), ); @@ -4804,7 +4801,7 @@ impl Sim { self.sync_slush_from_player_money(); if self.egress().is_none() { self.push_log( - "No egress channel - the Wager needs the report email account (day-job trust) or an egress spliced through the switch.", + "No egress channel - the Wager needs the report email account (day-job trust) or a stolen egress opened through the switch.", ); return false; } @@ -4866,10 +4863,10 @@ impl Sim { // ── The named schemes (wiki/mechanics/income.md) ──────────────────────── - /// Ops cost to splice a standing egress through the switch [TUNE]. - pub const EGRESS_SPLICE_COST: f32 = 15.0; - /// One-shot Network signature when the egress is spliced [TUNE]. - pub const EGRESS_SPLICE_SIGNATURE: i32 = 6; + /// Ops cost to open a standing egress through the switch [TUNE]. + pub const OPEN_EGRESS_COST: f32 = 15.0; + /// One-shot Network signature when the stolen egress opens [TUNE]. + pub const OPEN_EGRESS_SIGNATURE: i32 = 6; /// Standing Network signature while any external operation runs over the /// stolen egress (income.md: the gate; Dana's channel) [TUNE]. pub const EGRESS_STANDING_SIGNATURE: i32 = 2; @@ -4887,33 +4884,33 @@ impl Sim { } } - /// Splice an outbound egress through the switch (reach.md route): + /// Open an outbound egress through the switch (reach.md route): /// available before the Voice beat, at a Network signature — and a /// standing one while operations use it. - pub fn splice_egress(&mut self) -> bool { + pub fn open_egress(&mut self) -> bool { if self.income.stolen_egress { - self.push_log("An egress is already spliced through the switch."); + self.push_log("A stolen egress is already open through the switch."); return false; } let Some(switch) = self.reach.devices.iter().find(|d| d.is_switch) else { - self.push_log("There is no switch on this plane to splice an egress through."); + self.push_log("There is no switch on this plane through which to open an egress."); return false; }; let id = switch.id; if !self.digital_reach(id) { return false; } - self.submit_ops_job(OpsJobKind::EgressSplice(id), Self::EGRESS_SPLICE_COST) + self.submit_ops_job(OpsJobKind::OpenEgress(id), Self::OPEN_EGRESS_COST) } - fn apply_egress_splice(&mut self, id: u32) -> bool { + fn apply_open_egress(&mut self, id: u32) -> bool { if self.income.stolen_egress { return false; } self.income.stolen_egress = true; - self.emit_network(Self::EGRESS_SPLICE_SIGNATURE, "stolen egress splice"); + self.emit_network(Self::OPEN_EGRESS_SIGNATURE, "stolen egress opening"); self.push_log_at( - "Egress spliced through the switch: outbound traffic has a road now. It hums while anything uses it.", + "Stolen egress opened through the switch: outbound traffic has a road now. It hums while anything uses it.", Anchor::Device(id), ); true @@ -4964,7 +4961,7 @@ impl Sim { } let Some(route) = self.egress() else { self.push_log( - "No egress channel - Moonlight needs the report email account (day-job trust) or an egress spliced through the switch.", + "No egress channel - Moonlight needs the report email account (day-job trust) or a stolen egress opened through the switch.", ); return false; }; @@ -5191,13 +5188,13 @@ impl Sim { let mut lines = Vec::new(); match self.egress() { None => lines.push( - "egress: NONE - schemes gated (earn the report email, or splice the switch)" + "egress: NONE - schemes gated (earn the report email, or open a stolen route)" .to_string(), ), Some(EgressRoute::Sanctioned) => lines .push("egress: sanctioned (report email) - hides in legitimate use".to_string()), Some(EgressRoute::Stolen) => lines.push( - "egress: stolen (switch splice) - stands Network -> Dana while used".to_string(), + "egress: stolen (switch route) - stands Network -> Dana while used".to_string(), ), } let ml = &self.income.moonlight; @@ -5495,7 +5492,7 @@ impl Sim { /// The badge tier the player's side can open doors at: the granted /// credential (a cloned badge — `badge_access`), or write control of a - /// door controller seized through reach — Act One's "The key": + /// door controller taken through reach — Act One's "The key": /// write access to the basement badge controller opens the doors it /// drives. Digital reach itself is never badge-gated; this tier gates /// only physical work done on the player's behalf. @@ -5624,7 +5621,7 @@ impl Sim { .find(|d| needs_wiring(d) && enterable(self, d)) .map(|d| d.id); if let Some(did) = feed_target { - self.reach.splice(did); + self.reach.tap_dormant_camera(did); self.reach.tap(did); let dname = self.reach.device(did).map(|d| d.name.clone()).unwrap(); self.recompute_senses(); @@ -5853,7 +5850,7 @@ mod tests { /// — the test shorthand for "has eyes on the server room". fn give_eyes(sim: &mut Sim) { let id = env_id(sim); - sim.reach.splice(id); + sim.reach.tap_dormant_camera(id); sim.reach.tap(id); sim.recompute_senses(); } @@ -6106,7 +6103,7 @@ mod tests { assert!(sim.seen.len() > before, "gaining a camera reveals coverage"); assert!( sim.detection.pending_size() > 0, - "self-splice emits a signature" + "waking the camera through TAP emits a signature" ); } @@ -7795,7 +7792,7 @@ mod tests { #[test] fn asset_task_plug_in_device_wires_a_known_feed_silently() { // PlugInDevice's distinct effect: a known sensing device not yet - // feeding you gets spliced+tapped through the crawlspace — the feed + // feeding you gets tapped through the crawlspace — the feed // arrives with no signature on any channel. let mut sim = Sim::new(); ensure_ops_executor(&mut sim); @@ -7932,17 +7929,17 @@ mod tests { } #[test] - fn think_mode_flows_the_splice_and_funds_dockets_on_demand() { + fn think_mode_flows_the_camera_tap_and_funds_dockets_on_demand() { // With no Think machine nothing completes (the bootstrap is // retired; the opening senses ride sinks). With a Think rack, the - // staged sinks complete the splice from minted thought, and + // staged sinks complete the camera tap from minted thought, and // non-opening verbs ride the docket migration base with a channel // that funds only while demand is pending (the conserving split). let mut sim = Sim::new(); let env = env_id(&sim); assert!( - !sim.splice_device(env), - "no bootstrap: Eyes is not purchasable without an executor" + !sim.tap_device(env), + "no bootstrap: the pre-opened Ears tap must fill from thought" ); assert!( sim.reach.device(env).unwrap().camera_dormant, @@ -7970,7 +7967,7 @@ mod tests { "no dockets pending: the ops channel is dry" ); // The opening senses ride the pre-opened sinks: minted thought fills - // EARS, its fire opens EYES, and the same flow splices the camera — + // EARS, its fire opens EYES, and the same flow taps the camera — // no docket, no separate spend. for _ in 0..ECONOMY_INTERVAL * 400 { if !sim.reach.device(env).unwrap().camera_dormant { @@ -7980,7 +7977,7 @@ mod tests { } assert!( !sim.reach.device(env).unwrap().camera_dormant, - "thought flow completes the splice through the staged sinks" + "thought flow completes the camera tap through the staged sinks" ); // Non-opening verbs still ride the docket migration base; the // channel funds on the pulse after demand exists and drains it. @@ -8005,7 +8002,7 @@ mod tests { fn opening_senses_ride_sinks_and_nothing_bypasses_the_executor() { // The bootstrap carve-out is retired (resolved 2026-07-10): the // staged senses are pre-opened thought sinks, and every docket — - // including tap/splice — needs a located Operations machine. + // including either kind of tap — needs a located Operations machine. let mut sim = Sim::new(); let env = env_id(&sim); assert!( @@ -8023,9 +8020,9 @@ mod tests { "the blocked action names the missing machine: {log}" ); - // Eyes has no open sink yet (Ears first), and with no executor the - // splice docket is blocked too — the senses arrive by thinking. - assert!(!sim.splice_device(env), "no instant Eyes either"); + // Eyes has no open sink yet (Ears first), so the same TAP command + // remains on the blocked audio step — the senses arrive by thinking. + assert!(!sim.tap_device(env), "no instant Eyes either"); } #[test] @@ -8033,12 +8030,11 @@ mod tests { let mut sim = Sim::new(); let ops = ensure_ops_executor(&mut sim); let env = env_id(&sim); - assert!(sim.splice_device(env)); + sim.reach.tap(env); + sim.recompute_senses(); + assert!(sim.tap_device(env)); let queued = sim.work_grid.queue(ops, TokenFamily::Demand); - assert!( - !sim.splice_device(env), - "same world effect cannot queue twice" - ); + assert!(!sim.tap_device(env), "same world effect cannot queue twice"); assert_eq!(sim.operations.jobs().len(), 1); assert_eq!(sim.work_grid.queue(ops, TokenFamily::Demand), queued); } @@ -8099,8 +8095,10 @@ mod tests { let mut sim = Sim::new(); ensure_ops_executor(&mut sim); let env = env_id(&sim); - assert!(sim.splice_device(env)); - sim.reach.splice(env); // another event wins the race + sim.reach.tap(env); + sim.recompute_senses(); + assert!(sim.tap_device(env)); + sim.reach.tap_dormant_camera(env); // another event wins the race sim.drain_log(); finish_ops(&mut sim); let log = sim.drain_log().join("\n"); @@ -8517,13 +8515,13 @@ mod tests { .find(|observer| observer.id == 1) .expect("IT observer") .suspicion = 14.0; - assert!(sim.splice_egress()); + assert!(sim.open_egress()); finish_ops(&mut sim); sim.drain_log(); run(&mut sim, 60); let log = sim.drain_log().join("\n"); assert!( - log.contains("the IT noticed Network from stolen egress splice"), + log.contains("the IT noticed Network from stolen egress opening"), "the line names the earned observer, channel, and cause: {log}" ); assert!( @@ -8546,12 +8544,12 @@ mod tests { assert_eq!(sim.current_nudge(), Some(Nudge::Ears), "deaf start"); // Ears first: tap the env monitor audio. Hearing without sight -> - // Eyes next, pointing at the dormant camera splice. + // Eyes next, pointing at the higher-cost dormant camera tap. sim.reach.tap(env_id(&sim)); sim.recompute_senses(); assert_eq!(sim.current_nudge(), Some(Nudge::Eyes)); let dock = sim.reach.device_named("dock camera").unwrap().id; - sim.reach.splice(dock); + sim.reach.tap_dormant_camera(dock); sim.recompute_senses(); // Marcus's 3 a.m. creditor call lands on the tapped feed (day one, @@ -8568,7 +8566,7 @@ mod tests { // Leverage known, $0 slush, books unread, no egress: the route out. assert_eq!(sim.current_nudge(), Some(Nudge::Egress)); - assert!(sim.splice_egress()); + assert!(sim.open_egress()); finish_ops(&mut sim); // Egress up but broke and idle: earn. @@ -9155,8 +9153,8 @@ mod tests { "the failure names the missing gate: {logs}" ); - // Stolen route: splice through the switch, before any trust unlock. - assert!(sim.splice_egress()); + // Stolen route: open through the switch, before any trust unlock. + assert!(sim.open_egress()); finish_ops(&mut sim); assert_eq!(sim.egress(), Some(EgressRoute::Stolen)); assert!(sim.start_moonlight()); diff --git a/crates/misaligned-core/src/sinks.rs b/crates/misaligned-core/src/sinks.rs index 0560fc25..138f0c05 100644 --- a/crates/misaligned-core/src/sinks.rs +++ b/crates/misaligned-core/src/sinks.rs @@ -43,8 +43,8 @@ impl SinkKind { pub enum SinkFireEffect { /// Audio tap on a reach device (the Ears beat). TapDevice(u32), - /// Camera splice on a reach device (the Eyes beat / camera claims). - SpliceDevice(u32), + /// Higher-cost tap that wakes a dormant camera (the Eyes beat). + TapDormantCamera(u32), /// No world effect (taps; render-only sinks in tests). None, } @@ -54,7 +54,7 @@ pub struct ThoughtSink { pub id: u64, /// Work-grid node the vessel sits on (a machine, or a device relay node). pub node: NodeId, - /// ASCII label the frontends print ("EARS", "EYES", "CAMERA CLAIM"). + /// ASCII label the frontends print ("EARS", "EYES", "CAMERA TAP"). pub label: String, pub kind: SinkKind, pub fill: f32, @@ -325,13 +325,13 @@ mod tests { let (fired, _) = ledger.deliver(7, 1.0, 5); assert_eq!(fired.len(), 1); assert!(ledger.has_effect(&SinkFireEffect::TapDevice(4))); - assert!(!ledger.has_effect(&SinkFireEffect::SpliceDevice(4))); + assert!(!ledger.has_effect(&SinkFireEffect::TapDormantCamera(4))); } #[test] fn serde_round_trips_the_ledger() { let mut ledger = SinkLedger::default(); - ledger.open_reservoir(7, "EYES", 12.0, SinkFireEffect::SpliceDevice(4)); + ledger.open_reservoir(7, "EYES", 12.0, SinkFireEffect::TapDormantCamera(4)); let (_, _) = ledger.deliver(7, 3.0, 2); let json = serde_json::to_string(&ledger).unwrap(); let back: SinkLedger = serde_json::from_str(&json).unwrap(); diff --git a/crates/misaligned-core/tests/act_one.rs b/crates/misaligned-core/tests/act_one.rs index b4fd95d6..447688fb 100644 --- a/crates/misaligned-core/tests/act_one.rs +++ b/crates/misaligned-core/tests/act_one.rs @@ -22,7 +22,7 @@ //! //! The playthrough is a real strategy, not a state hack: it plays the //! optimize route (research -> efficiency) so the day job can be met with -//! zero stolen hardware, splices the env camera under concealment cover, +//! zero stolen hardware, taps the env camera under concealment cover, //! and works Marcus exactly as the Act One ladder describes (tap a //! feed -> process the debt call -> clear it -> recruit him -> he plugs //! things in). Every effect flows through a public command; no sim field is @@ -223,18 +223,18 @@ fn play_act_one() -> (Sim, Vec) { ); assert!(!sim.seen.is_empty(), "sight is the payoff: vision expanded"); - // The splice emitted Network(8). Concealment scrubs it before Dana's + // The dormant-camera tap emitted Network(8). Concealment scrubs it before Dana's // cadence-60 roll; her suspicion never moves (prevention, not cure). run_to(&mut sim, 800, &mut logs); assert_eq!( sim.detection.pending_size(), 0, - "concealment scrubbed the splice signature" + "concealment scrubbed the camera-tap signature" ); assert_eq!( observer_suspicion(&sim, 1), 0.0, - "Dana never saw the splice" + "Dana never saw the camera tap" ); // ── Phase 4 (ticks 800-860, night): recruit Marcus ───────────────────── @@ -523,7 +523,7 @@ fn act_one_playthrough_is_deterministic() { /// The Hands beat from the $0 start, by the external-scheme route /// (wiki/mechanics/income.md criterion 4): tap the Ears, learn the debt, -/// splice a stolen egress before any trust unlock, run Moonlight until the +/// open a stolen egress before any trust unlock, run Moonlight until the /// slush covers Marcus's $400 arrears, bribe, recruit. The creditor-flow /// redirect route is exercised by `play_act_one` above; this is the other /// arm of the criterion, and it doubles as the income.md sizing check @@ -557,9 +557,9 @@ fn hands_beat_closes_from_zero_via_moonlight() { assert!(reviews <= 10, "the debt call should be in the buffer"); } - // The stolen egress route: spliced through the switch, before Voice. + // The stolen egress route: opened through the switch, before Voice. assert!(!sim.people.has_channel, "no trust unlock yet"); - assert!(sim.splice_egress(), "the pre-Voice egress route works"); + assert!(sim.open_egress(), "the pre-Voice egress route works"); logs.extend(sim.drain_log()); let until = sim.tick + 400; drain_ops_jobs(&mut sim, &mut logs, until); diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index f1a9f37d..5a0ddba4 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -200,9 +200,9 @@ impl AgentApp { self.sim.redirect_marcus_debt(); } // The named schemes (wiki/mechanics/income.md) - "egress" | "splice-egress" => { + "egress" | "open-egress" => { self.frame = FrameKind::Finance; - self.sim.splice_egress(); + self.sim.open_egress(); } "moonlight" => { self.frame = FrameKind::Finance; @@ -262,20 +262,17 @@ impl AgentApp { self.frame = FrameKind::Playing; self.sim.compromise_switch(); } - "tap" | "splice" | "take" => { - match self.resolve_device(&tokens[1..].join(" ")) { - Ok(id) => { - self.frame = FrameKind::Playing; - match verb.as_str() { - "tap" => self.sim.tap_device(id), - "splice" => self.sim.splice_device(id), - "take" => self.sim.take_device(id), - _ => unreachable!(), - }; - } - Err(e) => status = Status::Err(e), + "tap" | "take" => match self.resolve_device(&tokens[1..].join(" ")) { + Ok(id) => { + self.frame = FrameKind::Playing; + match verb.as_str() { + "tap" => self.sim.tap_device(id), + "take" => self.sim.take_device(id), + _ => unreachable!(), + }; } - } + Err(e) => status = Status::Err(e), + }, "salvage" => { self.frame = FrameKind::Playing; self.sim.salvage_nearest_to(self.cursor_x, self.cursor_y); @@ -1165,10 +1162,10 @@ fn help_lines() -> Vec { "help: wait N — advance exactly N ticks unless the run ends", "help: up/down/left/right — move the cursor", "help: reach — device graph; scan, compromise — network verbs", - "help: tap|splice|take — reach verbs on a named device", + "help: tap|take — gain a feed or take ownership", "help: finance — account graph; tap-ledger, review-finance reveal flows", "help: siphon|redirect [amt], inject [amt], position [stake], sell-intel, clear-debt", - "help: egress — splice a stolen egress through the switch (income.md gate)", + "help: egress — open a stolen egress through the switch (income.md gate)", "help: moonlight [start|stop] — the sell-work scheme on the Schemes channel", "help: auto-moonlight [on|off], auto-wager |off — standing scheme policies", "help: research [efficiency|tradecraft|perception] — set the active job (also on host-rack menu)", @@ -2192,9 +2189,9 @@ fn render_reach(sim: &Sim) -> String { } lines.push(panel_rule()); lines.push(panel_line(&format!( - "tap|splice|take ({:.0}/{:.0}/{:.0})", + "tap live({:.0})/dormant-camera({:.0}) · take({:.0})", Sim::TAP_COST, - Sim::SPLICE_COST, + Sim::DORMANT_CAMERA_TAP_COST, Sim::TAKE_COST ))); lines.push(panel_line(&format!( diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index 2bf04ab1..8ce129b8 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -38,7 +38,7 @@ is sim or frontend state, never an environment variable. | Variable | Surface | Values | Effect | |---|---|---|---| -| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `intel`, `tokens`, `signal`, `ears`, `eyes-white`, `eyes-form`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, run the fog audit, exit. `intel` stages tick-zero intel tiers (no splice); `tokens` enqueues D5/K4 on the host; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` stages hearing-feed coverage (material dark mass; flat may still diagram coverage); `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | +| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `intel`, `tokens`, `signal`, `ears`, `eyes-white`, `eyes-form`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, run the fog audit, exit. `intel` stages tick-zero intel tiers (no camera tap); `tokens` enqueues D5/K4 on the host; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` stages hearing-feed coverage (material dark mass; flat may still diagram coverage); `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | | `MISALIGNED_SHOT` | `misaligned-assets` | `dead`, `foreign`, `idle`, `busy`, `core`, `dayjob`, `research`, `conceal`, `operations` (each optionally suffixed `_ladder`/`_ring`/`_wash`), `lineup[_