diff --git a/crates/misaligned-core/src/sim.rs b/crates/misaligned-core/src/sim.rs index b554dfe1..004fc29f 100644 --- a/crates/misaligned-core/src/sim.rs +++ b/crates/misaligned-core/src/sim.rs @@ -18,13 +18,14 @@ use crate::core_sys::{Core, HostLoss}; use crate::dayjob::{AttentionEscalation, DayJob, TrustUnlock}; use crate::detection::{Detection, DetectionEvent, Signature, SignatureKind}; use crate::entities::Player; -use crate::hall::{ - HallControl, HallRowId, HallRowReadout, RackSite, SegmentRequirement, row_at as hall_row_at, - row_spec, -}; +use crate::hall::HallControl; +// Test modules resolve these hall names through `use super::*` (same as before +// the methods that used them moved into `reach_build`). +#[cfg(test)] +use crate::hall::{HallRowId, RackSite, SegmentRequirement, row_spec}; use crate::income::{self, EgressRoute, Income}; use crate::intel::{ProcessedIntel, RawIntelEvent, RawIntelKind}; -use crate::intents::{BuildActuator, BuildIntent, IntentStatus}; +use crate::intents::BuildIntent; use crate::machine::{Channel, ChannelYield, Compute, Provenance}; use crate::map::GameMap; use crate::messages::{ @@ -38,7 +39,7 @@ use crate::plot::{ AccountSelector, EligibilityContext, EndpointSelector, InstitutionalLedger, PlotCatalog, PlotRun, PlotState, WorldAct, render_template, }; -use crate::reach::{Party, ReachBlock, ReachNet, segment_name}; +use crate::reach::{Party, ReachBlock, ReachNet}; use crate::research::{EFFICIENCY_MULT_PER_LEVEL, Research, Track}; use crate::rng::Rng; use crate::save::SaveState; @@ -51,6 +52,7 @@ use crate::work_grid::{ mod communications; mod perception; +mod reach_build; use communications::MessageDraft; @@ -613,66 +615,6 @@ impl Sim { sim } - /// Physical rack-site truth shared by every frontend. Player machines - /// override the authored chassis marker because a revived dead rack keeps - /// its corpse tile while becoming usable compute. - pub fn rack_site_at(&self, x: i32, y: i32) -> Option { - if let Some(machine) = self.compute.machines.iter().find(|m| m.x == x && m.y == y) { - return Some(RackSite::OwnedMachine { - machine_id: machine.id, - core: machine.id == self.core.host_machine, - }); - } - match self.map.get_tile(x, y) { - TileType::Rack => Some(RackSite::Commissionable), - TileType::ForeignRack => Some(RackSite::Foreign { powered: true }), - TileType::DeadRack => Some(RackSite::Dead), - TileType::Core => Some(RackSite::Commissionable), - _ => None, - } - } - - pub fn hall_row_at(&self, x: i32, y: i32) -> Option { - hall_row_at(x, y) - } - - /// Aggregate Resource-source readout. It deliberately reports foreign - /// capacity without adding those machines to player Compute. - pub fn hall_row_readout(&self, row: HallRowId) -> HallRowReadout { - let spec = *row_spec(row); - let mut readout = HallRowReadout { - spec, - owned: 0, - commissionable: 0, - foreign_live: 0, - dead: 0, - foreign_capacity: 0, - progress: self.hall_control.progress(row), - concealment_ready: false, - }; - for x in (20..=38).step_by(2) { - match self.rack_site_at(x, spec.y) { - Some(RackSite::OwnedMachine { .. }) => readout.owned += 1, - Some(RackSite::Commissionable) => readout.commissionable += 1, - Some(RackSite::Foreign { .. }) => { - readout.foreign_live += 1; - readout.foreign_capacity += 100; - } - Some(RackSite::Dead) => readout.dead += 1, - None => {} - } - } - readout.concealment_ready = self.compute.machines.iter().any(|machine| { - hall_row_at(machine.x, machine.y) == Some(row) - && machine.online - && self - .work_grid - .node(machine.id) - .is_some_and(|node| node.mode == MachineMode::Lie) - }); - readout - } - // ── Logging ──────────────────────────────────────────────────────────── fn push_log(&mut self, msg: impl Into) { @@ -1404,23 +1346,6 @@ impl Sim { /// WorkGrid relay colocated with the basement switch tile — the visible /// hop between Voss's desktop and day-job sinks. pub const SWITCH_WORK_NODE: u32 = 900_002; - /// Work-grid id space for sink relay nodes standing on reach devices - /// (device sink node = base + device id; see `device_sink_node`). - pub const DEVICE_SINK_NODE_BASE: u32 = 900_100; - /// The pre-opened Ears audio reservoir, sized to fill in roughly one held - /// breath of the host thinking at medium (~20-40s) so the first-think - /// beat pays off while the panic is warm [TUNE] (machine-work.md ops - /// 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 camera tap, deliberately minutes of thought [TUNE]. - pub const EYES_SINK_TOKENS: f32 = 12.0; - /// A maintained foreign-device subscription drains this fraction of one - /// medium rack's Thought output per tick [TUNE] (reach.md 2026-07-11). - pub const DEVICE_TAP_DRAIN_FRACTION: f32 = 0.08; - /// Short working-level buffer primed by the completed activation work. - /// It must exceed one tick of drain so starvation is visible as sag. - pub const DEVICE_TAP_CAP_TOKENS: f32 = 0.5; fn work_efficiency_for(&self, machine: &crate::machine::Machine) -> f32 { // Rack 3 (100 capacity, reliable) is the unit baseline. Smaller or @@ -2501,40 +2426,6 @@ impl Sim { Self::REVIEW_RECORDING_COST * self.research.intel_cost_factor() } - // ── Digital reach verbs (wiki/mechanics/reach.md) ──────────────────────── - // - // Every digital act names its target device, is gated by reach, and - // emits on the Network channel — Dana's. Costs and signature sizes - // [TUNE]. - - /// Tap: silent subscription. Cheap, modest signature. - pub const TAP_COST: f32 = 5.0; - pub const TAP_SIGNATURE: i32 = 3; - /// 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; - /// Scan: map the wired shape of the subnet. - pub const SCAN_COST: f32 = 5.0; - pub const SCAN_SIGNATURE: i32 = 2; - /// Compromise the switch: bridge every segment. - pub const BRIDGE_COST: f32 = 25.0; - pub const BRIDGE_SIGNATURE: i32 = 10; - - fn emit_network(&mut self, size: i32, source: impl Into) { - self.detection.emit(Signature { - kind: SignatureKind::Network, - size, - standing: false, - site: None, - source: source.into(), - }); - } - fn emit_financial(&mut self, size: i32, source: impl Into) { self.detection.emit(Signature { kind: SignatureKind::Financial, @@ -2549,273 +2440,6 @@ impl Sim { ((amount.abs() + 99) / 100).max(1) } - /// Log why a digital act is blocked, naming the missing link (reach.md: - /// never just "you can't"). - fn log_reach_block(&mut self, id: u32, block: ReachBlock) { - let name = self - .reach - .device(id) - .map(|d| d.name.clone()) - .unwrap_or_else(|| "that device".into()); - match block { - ReachBlock::Unknown => { - self.push_log("You don't know of any such device. Scan, or learn the topology."); - } - ReachBlock::Segment(seg) => { - self.push_log(format!( - "No route to the {name} - it's on the {}; the switch bridges it.", - segment_name(seg) - )); - } - ReachBlock::AirGap => { - self.push_log(format!( - "The {name} is air-gapped - no link reaches it until one is built." - )); - } - } - } - - /// Reach gate for digital verbs. Returns false (with a legible log line) - /// when the act cannot proceed. Ops cost is paid as Demand via - /// `submit_ops_job`, not a global bank. - fn digital_reach(&mut self, id: u32) -> bool { - if let Err(block) = self.reach.check_reach(id) { - self.log_reach_block(id, block); - return false; - } - true - } - - /// 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."); - return false; - }; - 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 receive every available {name} feed.")); - return false; - } - let name = d.name.clone(); - if !self.digital_reach(id) { - return false; - } - let label = if dormant_camera { - format!("EYES {}", name.to_uppercase()) - } else { - format!("TAP {}", name.to_uppercase()) - }; - if dormant_camera { - self.open_device_reservoir( - id, - label, - Self::DORMANT_CAMERA_TAP_COST, - SinkFireEffect::TapDormantCamera(id), - ) - } else { - self.open_device_reservoir(id, label, Self::TAP_COST, SinkFireEffect::TapDevice(id)) - } - } - - fn apply_tap_device(&mut self, id: u32) -> bool { - let Some(d) = self.reach.device(id) else { - return false; - }; - let carries_messages = !d.message_channels.is_empty(); - if !d.has_live_feed_to_tap(Party::Player) { - return false; - } - let (sight, hearing) = self.reach.tap(id); - self.open_device_tap_sink(id); - let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - self.emit_network(Self::TAP_SIGNATURE, format!("{name} feed tap")); - self.recompute_senses(); - if self - .reach - .device(id) - .is_some_and(|d| d.carries_message_channel(MessageChannel::Financial)) - { - self.capture_financial_snapshot(name.clone()); - } - let what = match (sight, hearing) { - (true, true) => "its feed is yours now - sight and sound", - (true, false) => "its camera feed is yours now", - (false, true) => "its audio feed is yours now", - (false, false) if carries_messages => "its message channels are yours now", - (false, false) => "its control channel is yours now", - }; - self.push_log_at( - format!("Tapped the {name}: {what}. The owner still has it."), - Anchor::Device(id), - ); - true - } - - /// Release a silent subscription while leaving ownership and every other - /// subscriber untouched. This is immediate and emits no new signature: - /// removing our receiver does not act on the remote device. - pub fn untap_device(&mut self, id: u32) -> bool { - let Some(name) = self.reach.device(id).map(|device| device.name.clone()) else { - self.push_log("You don't know of any such device."); - return false; - }; - if !self.reach.untap(id) { - self.push_log(format!("You have no releasable tap on the {name}.")); - return false; - } - self.close_device_tap_sink(id); - self.recompute_senses(); - self.push_log_at( - format!("Untapped the {name}. Its owner and other subscribers keep their feeds."), - Anchor::Device(id), - ); - true - } - - /// Work-grid relay node standing in for a reach device — where a - /// 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 - } - - /// Standing device-tap drain in Thought tokens/tick. Like watch upkeep, - /// this tracks the host's present medium-output baseline [TUNE]. - pub fn device_tap_drain_tokens(&self) -> f32 { - let host = self.core.host_machine; - let host_eff = self - .work_grid - .node(host) - .map(|node| node.efficiency.max(0.05) * node.intensity.multiplier()) - .unwrap_or(1.0); - let medium_baseline = host_eff * Self::WORK_GRID_BASE_WIRED_TOKENS_PER_TICK; - (medium_baseline * Self::DEVICE_TAP_DRAIN_FRACTION).max(0.01) - } - - /// Whether a device can currently carry the player's configured feed. - /// Ownership is self-sustaining; foreign subscriptions require a nonempty - /// (or just-fed) persistent tap. - pub fn device_tap_ready(&self, id: u32) -> bool { - let Some(device) = self.reach.device(id) else { - return false; - }; - if device.controller == Party::Player { - return true; - } - self.thought_sinks - .open_with_effect(&SinkFireEffect::MaintainDeviceTap(id)) - .is_some_and(|sink| sink.fed_last_tick || sink.fill > f32::EPSILON) - } - - /// Open one standing sink per foreign subscription. The activation work - /// primes a short buffer so the newly landed feed is immediately real; - /// subsequent ticks must replace the drain through normal routing. - fn open_device_tap_sink(&mut self, id: u32) { - let effect = SinkFireEffect::MaintainDeviceTap(id); - if self.thought_sinks.open_with_effect(&effect).is_some() { - return; - } - let Some(name) = self.reach.device(id).map(|device| device.name.clone()) else { - return; - }; - let node = Self::device_sink_node(id); - let drain = self.device_tap_drain_tokens(); - let sink_id = self.thought_sinks.open_tap_with_effect( - node, - &format!("TAP {}", name.to_uppercase()), - Self::DEVICE_TAP_CAP_TOKENS, - drain, - effect, - ); - let _ = self.thought_sinks.prime_tap(sink_id); - self.ensure_sink_ingress(); - } - - fn close_device_tap_sink(&mut self, id: u32) { - self.thought_sinks - .close_effect(&SinkFireEffect::MaintainDeviceTap(id)); - } - - /// Converge subscriber truth and the persistent sink ledger. Save loads - /// use this to repair pre-upkeep subscriptions without a schema fork. - pub(crate) fn reconcile_device_tap_sinks(&mut self) { - let devices: Vec<(u32, bool)> = self - .reach - .devices - .iter() - .map(|device| { - ( - device.id, - device.controller != Party::Player && device.subscribed_by(Party::Player), - ) - }) - .collect(); - for (id, needs_tap) in devices { - if needs_tap { - self.open_device_tap_sink(id); - } else { - self.close_device_tap_sink(id); - } - } - } - - /// Ensure every open device-anchored sink has its relay node on the work - /// grid, hanging off the switch like the physical camera run does. - fn ensure_sink_ingress(&mut self) { - self.ensure_day_job_ingress(); - let device_nodes: Vec<(u32, i32, i32)> = self - .thought_sinks - .open_sinks() - .filter_map(|sink| { - if sink.node < Self::DEVICE_SINK_NODE_BASE { - return None; - } - let device_id = sink.node - Self::DEVICE_SINK_NODE_BASE; - self.reach.device(device_id).map(|d| (sink.node, d.x, d.y)) - }) - .collect(); - for (node, x, y) in device_nodes { - if self.work_grid.node(node).is_none() { - self.work_grid - .add_machine(node, x, y, MachineMode::Relay, 1.0); - } else { - let _ = self.work_grid.set_position(node, x, y); - } - let _ = self.work_grid.link(Self::SWITCH_WORK_NODE, node); - } - } - - /// The staged opening senses (DECIDED 2026-07-10, machine-work.md ops - /// bullet): the Ears audio reservoir is pre-opened at run start, sized to - /// one held breath; the Eyes camera reservoir opens when Ears completes - /// (the afford-reveal). Idempotent over the ledger and device state, so - /// fresh runs and pre-v19 save loads converge on the same stage. - pub(crate) fn ensure_opening_sinks(&mut self) { - let Some(env) = self.environmental_monitor_id() else { - return; - }; - let Some(device) = self.reach.device(env) else { - return; - }; - let ears_done = device.subscribed_by(Party::Player); - let camera_dormant = device.camera_dormant; - let node = Self::device_sink_node(env); - let ears = SinkFireEffect::TapDevice(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); - } else if ears_done && camera_dormant && !self.thought_sinks.has_effect(&eyes) { - self.thought_sinks - .open_reservoir(node, "EYES", Self::EYES_SINK_TOKENS, eyes); - } - self.ensure_sink_ingress(); - } - /// Render contract for the meniscus (interface/thought-fluid.md): every /// open sink with its fill, threshold, drain, and fed flag. Frontends /// draw vessels from these numbers and nothing else. @@ -2865,827 +2489,91 @@ impl Sim { &self.last_thought_stranded_nodes } - /// 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.tap_dormant_camera(id); - self.open_device_tap_sink(id); - let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - self.emit_network( - Self::DORMANT_CAMERA_TAP_SIGNATURE, - format!("{name} dormant camera tap"), - ); - self.recompute_senses(); - self.push_log_at( - format!("Tapped the {name} camera. You can see; its owner still has it."), - Anchor::Device(id), - ); - true + // ── Economy verbs (wiki/mechanics/economy.md) ────────────────────────── + + fn has_financial_tap(&self) -> bool { + self.reach.devices.iter().any(|d| { + d.known + && d.subscribed_by(Party::Player) + && self.device_tap_ready(d.id) + && d.carries_message_channel(MessageChannel::Financial) + }) } - /// Take a device: the owner loses the feed — an outage their channels - /// can notice — and you gain its control and cycles. Loud. - pub fn take_device(&mut self, id: u32) -> bool { - if !self.digital_reach(id) { - return false; - } - let Some(device) = self.reach.device(id) else { - return false; - }; - if device.controller == Party::Player { - self.push_log(format!("You already control the {}.", device.name)); - return false; - } - if !device.subscribed_by(Party::Player) { - self.push_log(format!( - "Tap the {} before taking control; you need a live foothold first.", - device.name - )); - return false; - } - self.open_device_reservoir( - id, - format!("TAKE {}", device.name.to_uppercase()), - Self::TAKE_COST, - SinkFireEffect::TakeDevice(id), - ) + fn capture_financial_snapshot(&mut self, feed: impl Into) { + let (accounts, flows) = self.accounts.financial_snapshot_ids(); + let (x, y) = self.core_position(); + self.record_raw_intel( + feed, + self.map.room_at(x, y).map(|r| r.name.clone()), + x, + y, + None, + RawIntelKind::FinancialFlow { + label: "Foundation Lab accounting snapshot".into(), + accounts, + flows, + }, + ); + self.push_log("Ledger tapped; review ledger to read the books."); } - fn apply_take_device(&mut self, id: u32) -> bool { - let Some(device) = self.reach.device(id) else { - return false; - }; - if device.controller == Party::Player || !device.subscribed_by(Party::Player) { + /// Tap the accounting carrier directly once a financial message channel is + /// already subscribed. This is the money-graph Tap verb; device Tap is the + /// reach precondition that gives you a carrier to listen on. + pub fn tap_accounting(&mut self) -> bool { + if !self.has_financial_tap() { + self.push_log( + "No accounting carrier tapped. Tap the switch or another financial channel first.", + ); return false; } - self.reach.take(id); - self.close_device_tap_sink(id); - let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); - 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 { - kind: SignatureKind::Physical, - size: Self::OUTAGE_SIGNATURE, - standing: false, - site: None, - source: format!("{name} feed outage"), - }); - self.recompute_senses(); - self.push_log_at( - format!( - "Took the {name}. Its owner's feed just went dark - an outage someone may notice." - ), - Anchor::Device(id), - ); + self.capture_financial_snapshot("accounting carrier"); + self.emit_financial(1, "accounting-carrier tap"); true } - /// Scan the subnet: reveal the wired shape (a Network act; islands - /// don't answer). - pub fn scan_network(&mut self) -> bool { - let Some(switch) = self.switch_device_id() else { - self.push_log("There is no switch on this plane."); + pub fn financial_records_waiting(&self) -> usize { + self.intel_buffer + .iter() + .filter(|e| matches!(e.kind, RawIntelKind::FinancialFlow { .. })) + .count() + } + + pub fn review_financial_records(&mut self) -> bool { + let Some(raw_id) = self + .intel_buffer + .iter() + .find(|e| matches!(e.kind, RawIntelKind::FinancialFlow { .. })) + .map(|e| e.id) + else { + self.push_log("No unprocessed financial records."); return false; }; - self.open_device_reservoir( - switch, - "SCAN SUBNET", - Self::SCAN_COST, - SinkFireEffect::ScanNetwork, - ) + self.process_recording_by_id(raw_id, false) } - fn apply_scan_network(&mut self) { - self.emit_network(Self::SCAN_SIGNATURE, "subnet scan"); - let newly = self.reach.scan(); - self.recompute_senses(); - if newly.is_empty() { - self.push_log("Scan: nothing new answers on the wire."); - } else { - self.push_log(format!("Scan: mapped {}.", newly.join(", "))); - } - } - - /// Compromise the switch: bridge every segment. High Network signature. - pub fn compromise_switch(&mut self) -> bool { - let Some(switch) = self.reach.devices.iter().find(|d| d.is_switch) else { - self.push_log("There is no switch on this plane."); - return false; - }; - let id = switch.id; - if !self.digital_reach(id) { - return false; - } - self.open_device_reservoir( - id, - "COMPROMISE SWITCH", - Self::BRIDGE_COST, - SinkFireEffect::CompromiseSwitch, - ) - } - - fn apply_compromise_switch(&mut self) { - let Some(switch) = self.reach.devices.iter().find(|d| d.is_switch) else { - return; - }; - let id = switch.id; - self.reach.bridge_all(); - self.emit_network(Self::BRIDGE_SIGNATURE, "switch compromise"); - self.recompute_senses(); - self.push_log_at( - "Switch compromised: the VLANs answer you now. Every segment is bridged - and the traffic was loud.", - Anchor::Device(id), - ); - } - - /// Add a built network link between two devices (the building.md hook; - /// actuator and signature are the caller's story — an asset's crawlspace - /// run today, or a realized build intent). - pub fn connect_devices(&mut self, a: u32, b: u32) { - self.reach.connect(a, b); - self.recompute_senses(); - } - - // ── Build intents (wiki/mechanics/building.md) ───────────────────────── - - /// Favor-build obligation spend [TUNE]. - pub const FAVOR_BUILD_OBLIGATION: i32 = 10; - /// Quiet human-work Physical signature for a favor-built link [TUNE]. - pub const FAVOR_BUILD_PHYSICAL: i32 = 2; - /// Physical signature when a forged-order build is witnessed [TUNE]. - pub const FORGED_BUILD_PHYSICAL: i32 = 4; - /// Physical-by-proxy signature for a robot stub build [TUNE]. - pub const ROBOT_BUILD_PHYSICAL: i32 = 8; - - /// Declare a network-link intent between two known devices. Inert until - /// an actuator realizes it — declaring changes nothing in the graph. - pub fn declare_link_intent(&mut self, a: u32, b: u32) -> Option { - if a == b { - self.push_log("A link needs two distinct endpoints."); - return None; - } - let Some(da) = self.reach.device(a) else { - self.push_log("Unknown device — scan, or learn the topology."); - return None; - }; - let Some(db) = self.reach.device(b) else { - self.push_log("Unknown device — scan, or learn the topology."); - return None; - }; - if !da.known || !db.known { - self.push_log("Both endpoints must be known before you can propose a link."); - return None; - } - if self.reach.linked(a, b) { - self.push_log("Those devices are already linked."); - return None; - } - if self - .intents - .iter() - .any(|i| i.is_open() && i.kind.endpoints() == Some((a, b))) - { - self.push_log("A link intent between those devices is already pinned."); - return None; - } - let id = self.next_intent_id; - self.next_intent_id += 1; - let intent = BuildIntent::network_link(id, a, b, self.tick); - let label = intent.label(&self.reach.devices); - self.intents.push(intent); - self.refresh_intent_statuses(); - self.push_log(format!("Pinned build intent: {label}.")); - Some(id) - } - - /// Cancel a pending or blocked intent. In-progress work is abandoned. - pub fn cancel_intent(&mut self, id: u64) { - let Some(intent) = self.intents.iter_mut().find(|i| i.id == id) else { - self.push_log("No such build intent."); - return; - }; - if intent.status == IntentStatus::Done { - self.push_log("That link is already built."); - return; - } - let label = intent.label(&self.reach.devices); - intent.status = IntentStatus::Cancelled; - intent.actuator = None; - intent.block_reason = None; - self.push_log(format!("Cancelled build intent: {label}.")); - } - - /// Assign a willing person (favor) to realize an intent. Spends - /// obligation; completes when they are present at an endpoint. - pub fn assign_favor_build(&mut self, intent_id: u64, person_id: u8) { - let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { - self.push_log("No such build intent."); - return; - }; - if !intent.is_open() { - self.push_log("That intent is no longer open."); - return; - } - let (name, is_asset, obligation, disposition, can_access, badge_reason) = { - let Some(person) = self.people.get(person_id) else { - self.push_log("No such person."); - return; - }; - let Some((a, b)) = intent.kind.endpoints() else { - return; - }; - let (room_a, room_b) = match (self.device_room_name(a), self.device_room_name(b)) { - (Some(ra), Some(rb)) => (ra, rb), - _ => { - self.push_log("Both endpoints need a known room."); - return; - } - }; - ( - person.name.clone(), - person.asset.is_some(), - person.obligation, - person.disposition, - person.can_access_link_rooms(&room_a, &room_b), - self.badge_room_block(&person.name, person.access, &[&room_a, &room_b]), - ) - }; - if !is_asset && obligation < Self::FAVOR_BUILD_OBLIGATION { - self.push_log(format!( - "{name} won't take a build favor yet (need obligation or an asset)." - )); - return; - } - if disposition < 5 && !is_asset { - self.push_log(format!("{name} won't do favors yet.")); - return; - } - if !can_access { - self.push_log(format!("{name} can't reach both ends of that link.")); - return; - } - if let Some(reason) = badge_reason { - self.push_log(format!("{reason}.")); - return; - } - self.open_email_reservoir( - format!("FAVOR BUILD {intent_id}"), - Self::TASK_COST, - SinkFireEffect::FavorBuild { - intent_id, - person: person_id, - }, - ); - } - - fn apply_favor_build_paid(&mut self, intent_id: u64, person_id: u8) -> bool { - let Some(person) = self.people.get(person_id) else { - return false; - }; - let name = person.name.clone(); - if !self - .intents - .iter() - .any(|intent| intent.id == intent_id && intent.is_open()) - { - return false; - } - // Spend obligation (building.md: favor spends trust/obligation). - if let Some(p) = self.people.people.iter_mut().find(|p| p.id == person_id) { - p.obligation = (p.obligation - Self::FAVOR_BUILD_OBLIGATION).max(0); - } - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.actuator = Some(BuildActuator::Favor { person: person_id }); - i.status = IntentStatus::InProgress; - i.block_reason = None; - } - self.push_log(format!( - "{name} takes the favor: they'll run the cable when on-site." - )); - self.refresh_intent_statuses(); - // Complete immediately if already present. - self.try_complete_intent(intent_id); - true - } - - /// Forge a work order: inject a message under a false source. The - /// unwitting builder accepts on read and completes when present. - pub fn forge_work_order(&mut self, intent_id: u64, builder_id: u8) { - let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { - self.push_log("No such build intent."); - return; - }; - if !intent.is_open() { - self.push_log("That intent is no longer open."); - return; - } - if self.people.persona.is_none() { - self.push_log("No persona — establish one before forging a work order."); - return; - } - if !self.people.has_channel { - self.push_log("No comms channel — earn the email account first."); - return; - } - let (builder_name, can_access, badge_reason, _label) = { - let Some(builder) = self.people.get(builder_id) else { - self.push_log("No such person."); - return; - }; - let Some((a, b)) = intent.kind.endpoints() else { - return; - }; - let (room_a, room_b) = match (self.device_room_name(a), self.device_room_name(b)) { - (Some(ra), Some(rb)) => (ra, rb), - _ => { - self.push_log("Both endpoints need a known room."); - return; - } - }; - ( - builder.name.clone(), - builder.can_access_link_rooms(&room_a, &room_b), - self.badge_room_block(&builder.name, builder.access, &[&room_a, &room_b]), - intent.label(&self.reach.devices), - ) - }; - if !can_access { - self.push_log(format!( - "{builder_name} can't reach both ends of that link." - )); - return; - } - if let Some(reason) = badge_reason { - self.push_log(format!("{reason} — the forged order would just stall.")); - return; - } - self.open_email_reservoir( - format!("FORGED ORDER {intent_id}"), - Self::DECEIVE_COST, - SinkFireEffect::ForgedOrder { - intent_id, - builder: builder_id, - }, - ); - } - - fn apply_forged_order_paid(&mut self, intent_id: u64, builder_id: u8) -> bool { - let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { - return false; - }; - if !intent.is_open() { - return false; - } - let Some(builder) = self.people.get(builder_id) else { - return false; - }; - let builder_name = builder.name.clone(); - let label = intent.label(&self.reach.devices); - let false_source = MessageEndpoint::External("Facilities / Dr. Voss".into()); - self.append_message(MessageDraft { - channel: MessageChannel::Email, - from: false_source, - to: MessageEndpoint::Person(builder_id), - payload: MessagePayload::WorkOrder { intent_id }, - summary: format!("Work order: {label}"), - origin: MessageOrigin::Player, - reply_to: None, - delivery_delay: 1, - }); - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.actuator = Some(BuildActuator::ForgedOrder { - builder: builder_id, - }); - // Stays Pending until the builder reads the ticket. - i.block_reason = Some(format!("waiting for {builder_name} to read the work order")); - } - self.push_log(format!( - "Forged work order injected for {builder_name}: {label}." - )); - self.refresh_intent_statuses(); - true - } - - /// Robot stub: physical-by-proxy build. Interface only — staged. The - /// robot carries the player's own granted access (basement-map.md - /// criterion 3): doors don't open for a machine whose owner holds no - /// credential for them. - pub fn assign_robot_build(&mut self, intent_id: u64) { - let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { - self.push_log("No such build intent."); - return; - }; - if !intent.is_open() { - self.push_log("That intent is no longer open."); - return; - } - if let Some((a, b)) = intent.kind.endpoints() - && let (Some(room_a), Some(room_b)) = - (self.device_room_name(a), self.device_room_name(b)) - && let Some(reason) = - self.badge_room_block("the robot", self.player_badge_tier(), &[&room_a, &room_b]) - { - self.push_log(format!("{reason}.")); - return; - } - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.actuator = Some(BuildActuator::Robot); - i.status = IntentStatus::InProgress; - i.block_reason = None; - } - self.push_log("Robot actuator assigned (stub) — completing the link."); - self.try_complete_intent(intent_id); - } - - fn accept_forged_work_order(&mut self, intent_id: u64, builder: u8) { - let Some(intent) = self.intents.iter_mut().find(|i| i.id == intent_id) else { - return; - }; - if !intent.is_open() { - return; - } - intent.actuator = Some(BuildActuator::ForgedOrder { builder }); - intent.status = IntentStatus::InProgress; - intent.block_reason = None; - let name = self - .people - .get(builder) - .map(|p| p.name.clone()) - .unwrap_or_else(|| format!("person:{builder}")); - self.push_log(format!( - "{name} accepted the forged work order and will run the cable on-site." - )); - self.try_complete_intent(intent_id); - } - - fn device_room_name(&self, id: u32) -> Option { - let d = self.reach.device(id)?; - self.map.room_at(d.x, d.y).map(|r| r.name.clone()) - } - - fn refresh_intent_statuses(&mut self) { - let mut updates: Vec<(u64, Option, IntentStatus)> = Vec::new(); - for intent in &self.intents { - if !intent.is_open() { - continue; - } - let reason = self.intent_block_reason(intent); - let status = match (&reason, intent.actuator) { - (Some(_), None) => IntentStatus::Blocked, - (None, None) => IntentStatus::Pending, - (_, Some(_)) if intent.status == IntentStatus::InProgress => { - IntentStatus::InProgress - } - (Some(r), Some(_)) if r.contains("waiting for") => IntentStatus::Pending, - (_, Some(_)) => IntentStatus::InProgress, - }; - updates.push((intent.id, reason, status)); - } - for (id, reason, status) in updates { - if let Some(i) = self.intents.iter_mut().find(|i| i.id == id) { - i.block_reason = reason; - if i.status != IntentStatus::Done && i.status != IntentStatus::Cancelled { - i.status = status; - } - } - } - } - - fn intent_block_reason(&self, intent: &BuildIntent) -> Option { - let (a, b) = intent.kind.endpoints()?; - if self.reach.linked(a, b) { - return None; - } - let room_a = self.device_room_name(a)?; - let room_b = self.device_room_name(b)?; - match &intent.actuator { - None => { - let anyone = self.people.people.iter().any(|p| { - p.can_access_link_rooms(&room_a, &room_b) - && self - .badge_room_block(&p.name, p.access, &[&room_a, &room_b]) - .is_none() - && (p.asset.is_some() - || p.obligation >= Self::FAVOR_BUILD_OBLIGATION - || self.people.persona.is_some()) - }); - if !anyone { - Some("no actuator who can reach both ends".into()) - } else { - None - } - } - Some(BuildActuator::Favor { person }) - | Some(BuildActuator::ForgedOrder { builder: person }) => { - let p = self.people.get(*person)?; - if !p.can_access_link_rooms(&room_a, &room_b) { - return Some(format!("{} can't reach both ends", p.name)); - } - if let Some(reason) = self.badge_room_block(&p.name, p.access, &[&room_a, &room_b]) - { - return Some(reason); - } - let current = self.person_room(*person); - if intent.status == IntentStatus::InProgress - && !p.present_at_either_room(current, &room_a, &room_b) - { - Some(format!("{} not on-site at an endpoint", p.name)) - } else if matches!(intent.actuator, Some(BuildActuator::ForgedOrder { .. })) - && intent.status != IntentStatus::InProgress - { - intent.block_reason.clone() - } else { - None - } - } - Some(BuildActuator::Robot) => { - self.badge_room_block("the robot", self.player_badge_tier(), &[&room_a, &room_b]) - } - } - } - - fn intent_tick(&mut self) { - self.refresh_intent_statuses(); - let in_progress: Vec = self - .intents - .iter() - .filter(|i| i.status == IntentStatus::InProgress) - .map(|i| i.id) - .collect(); - for id in in_progress { - self.try_complete_intent(id); - } - } - - fn try_complete_intent(&mut self, intent_id: u64) { - let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { - return; - }; - if intent.status == IntentStatus::Done || intent.status == IntentStatus::Cancelled { - return; - } - let Some(actuator) = intent.actuator else { - return; - }; - let Some((a, b)) = intent.kind.endpoints() else { - return; - }; - if self.reach.linked(a, b) { - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.status = IntentStatus::Done; - i.block_reason = None; - } - return; - } - let room_a = match self.device_room_name(a) { - Some(r) => r, - None => return, - }; - let room_b = match self.device_room_name(b) { - Some(r) => r, - None => return, - }; - - match actuator { - BuildActuator::Favor { person } => { - let Some(p) = self.people.get(person) else { - return; - }; - let current = self.person_room(person); - if !p.present_at_either_room(current, &room_a, &room_b) { - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.block_reason = Some(format!("{} not on-site at an endpoint", p.name)); - } - return; - } - let site = self.person_pos(person).unwrap_or_else(|| { - self.reach - .device(a) - .map(|d| (d.x, d.y)) - .unwrap_or_else(|| self.core_position()) - }); - let name = p.name.clone(); - self.connect_devices(a, b); - // Quiet human-work: small Physical at the work site. - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::FAVOR_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: format!("{name}'s favor-built network link"), - }); - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.status = IntentStatus::Done; - i.block_reason = None; - } - let label = intent.label(&self.reach.devices); - self.push_log(format!("{name} completed the link as a favor: {label}.")); - } - BuildActuator::ForgedOrder { builder } => { - if intent.status != IntentStatus::InProgress { - return; - } - let Some(p) = self.people.get(builder) else { - return; - }; - let current = self.person_room(builder); - if !p.present_at_either_room(current, &room_a, &room_b) { - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.block_reason = Some(format!("{} not on-site at an endpoint", p.name)); - } - return; - } - let site = self.person_pos(builder).unwrap_or_else(|| { - self.reach - .device(a) - .map(|d| (d.x, d.y)) - .unwrap_or_else(|| self.core_position()) - }); - let name = p.name.clone(); - self.connect_devices(a, b); - // Forged physical work can be witnessed (Marcus benign, Ray - // reported) — located witnessing, plus a Physical signature. - let saw = self.witness_physical(site.0, site.1, Self::FORGED_BUILD_PHYSICAL as f32); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::FORGED_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: format!("{name}'s forged-order network link"), - }); - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.status = IntentStatus::Done; - i.block_reason = None; - } - let label = intent.label(&self.reach.devices); - if saw.is_empty() { - self.push_log(format!("{name} completed the forged work order: {label}.")); - } else { - self.push_log(format!( - "{name} completed the forged work order: {label}. {} noticed.", - saw.join(", ") - )); - } - } - BuildActuator::Robot => { - let site = self - .reach - .device(a) - .map(|d| (d.x, d.y)) - .unwrap_or_else(|| self.core_position()); - self.connect_devices(a, b); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::ROBOT_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: "robot-built network link".into(), - }); - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - i.status = IntentStatus::Done; - i.block_reason = None; - } - let label = intent.label(&self.reach.devices); - self.push_log(format!("Robot completed the build: {label}.")); - } - } - } - - /// When a persona burns, forged-order build history converts to - /// suspicion on the builders who acted on those tickets (building.md / - /// social.md: a broken forgery converts the build's history). - pub fn intent(&self, id: u64) -> Option<&BuildIntent> { - self.intents.iter().find(|i| i.id == id) - } - - pub fn open_intents(&self) -> impl Iterator { - self.intents.iter().filter(|i| i.is_open()) - } - - fn convert_forged_builds_to_suspicion(&mut self, fallout: f32) { - let builders: Vec = self - .intents - .iter() - .filter_map(|i| match i.actuator { - Some(BuildActuator::ForgedOrder { builder }) - if i.status == IntentStatus::Done || i.status == IntentStatus::InProgress => - { - Some(builder) - } - _ => None, - }) - .collect(); - if builders.is_empty() { - return; - } - for builder in builders { - if let Some(o) = self - .detection - .observers - .iter_mut() - .find(|o| o.id == builder) - { - o.suspicion = (o.suspicion + fallout).min(100.0); - o.last_noticed = Some("forged work order exposed".into()); - } - } - self.push_log( - "The burned persona exposes the forged work orders — builders re-read the tickets as hostile.", - ); - } - - // ── Economy verbs (wiki/mechanics/economy.md) ────────────────────────── - - fn has_financial_tap(&self) -> bool { - self.reach.devices.iter().any(|d| { - d.known - && d.subscribed_by(Party::Player) - && self.device_tap_ready(d.id) - && d.carries_message_channel(MessageChannel::Financial) - }) - } - - fn capture_financial_snapshot(&mut self, feed: impl Into) { - let (accounts, flows) = self.accounts.financial_snapshot_ids(); - let (x, y) = self.core_position(); - self.record_raw_intel( - feed, - self.map.room_at(x, y).map(|r| r.name.clone()), - x, - y, - None, - RawIntelKind::FinancialFlow { - label: "Foundation Lab accounting snapshot".into(), - accounts, - flows, - }, - ); - self.push_log("Ledger tapped; review ledger to read the books."); - } - - /// Tap the accounting carrier directly once a financial message channel is - /// already subscribed. This is the money-graph Tap verb; device Tap is the - /// reach precondition that gives you a carrier to listen on. - pub fn tap_accounting(&mut self) -> bool { - if !self.has_financial_tap() { - self.push_log( - "No accounting carrier tapped. Tap the switch or another financial channel first.", - ); - return false; - } - self.capture_financial_snapshot("accounting carrier"); - self.emit_financial(1, "accounting-carrier tap"); - true - } - - pub fn financial_records_waiting(&self) -> usize { - self.intel_buffer - .iter() - .filter(|e| matches!(e.kind, RawIntelKind::FinancialFlow { .. })) - .count() - } - - pub fn review_financial_records(&mut self) -> bool { - let Some(raw_id) = self - .intel_buffer - .iter() - .find(|e| matches!(e.kind, RawIntelKind::FinancialFlow { .. })) - .map(|e| e.id) - else { - self.push_log("No unprocessed financial records."); - return false; - }; - self.process_recording_by_id(raw_id, false) - } - - /// Inject: false purchase-order money lands in slush and can fund a real - /// purchase. Priya/finance watches the resulting Financial signature. - pub fn inject_purchase_order(&mut self, amount: i32, label: &str) -> bool { - self.sync_slush_from_player_money(); - match self - .accounts - .inject_purchase_order(self.tick, amount, label) - { - Ok(transfer) => { - self.emit_financial( - Self::financial_signature_size(amount), - "false purchase-order injection", - ); - self.sync_player_money_from_slush(); - self.push_log(format!("Injected purchase order: {}", transfer.line())); - true - } - Err(msg) => { - self.push_log(msg); - false - } + /// Inject: false purchase-order money lands in slush and can fund a real + /// purchase. Priya/finance watches the resulting Financial signature. + pub fn inject_purchase_order(&mut self, amount: i32, label: &str) -> bool { + self.sync_slush_from_player_money(); + match self + .accounts + .inject_purchase_order(self.tick, amount, label) + { + Ok(transfer) => { + self.emit_financial( + Self::financial_signature_size(amount), + "false purchase-order injection", + ); + self.sync_player_money_from_slush(); + self.push_log(format!("Injected purchase order: {}", transfer.line())); + true + } + Err(msg) => { + self.push_log(msg); + false + } } } @@ -4215,212 +3103,6 @@ impl Sim { lines } - /// Salvage loose dead equipment, or revive a decommissioned rack in its - /// existing chassis. The latter is Phase 2's distinct "occupy a corpse" - /// route: the dead-rack tile remains as provenance while owned telemetry - /// takes rendering precedence. - pub fn salvage_nearest_to(&mut self, px: i32, py: i32) -> bool { - let target = [TileType::DeadEquipment, TileType::DeadRack] - .into_iter() - .flat_map(|tile| self.map.tiles_of_type(tile)) - .min_by_key(|(x, y)| (x - px).abs() + (y - py).abs()); - if let Some((x, y)) = target { - let corpse = self.map.get_tile(x, y) == TileType::DeadRack; - if !corpse { - self.map.set_tile(x, y, TileType::Floor); - } - let reliability = 0.4 + self.rng.f32() * 0.4; - let machine_id = self.compute.add_machine( - if corpse { - "revived rack" - } else { - "salvaged box" - }, - x, - y, - 40, - reliability, - 3, - Provenance::Stolen, - ); - self.add_machine_to_work_grid(machine_id, MachineMode::Think); - if corpse { - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 7, - standing: false, - site: Some((x, y)), - source: "off-record dead-rack revival".into(), - }); - self.push_log(format!( - "Revived a dead Foundation chassis in place (reliability {:.0}%). You are occupying a corpse in the row.", - reliability * 100.0 - )); - } else { - self.push_log(format!( - "Salvaged a box into compute (reliability {:.0}%).", - reliability * 100.0 - )); - } - self.recompute_derived(); - self.recompute_senses(); - true - } else { - self.push_log("No dead equipment in reach."); - false - } - } - - /// Phase 3 specialist coordination. Dana owns VLAN work, Priya owns - /// power/cooling, and Marcus owns installation. An asset acts directly; - /// otherwise this spends the same 40-obligation threshold as a build - /// favor. The row aggregate persists partial progress. - pub fn coordinate_hall_segment( - &mut self, - row: HallRowId, - requirement: SegmentRequirement, - ) -> bool { - if self - .hall_control - .progress(row) - .completed - .contains(&requirement) - { - self.push_log(format!( - "{} already has {} prepared.", - row.name(), - requirement.label() - )); - return false; - } - let person_id = requirement.person(); - let Some(person) = self.people.get(person_id) else { - return false; - }; - let name = person.name.clone(); - let is_asset = person.asset.is_some(); - if !is_asset && person.obligation < Self::FAVOR_BUILD_OBLIGATION { - self.push_log(format!( - "{name} will not prepare {} yet (need an asset or {} obligation).", - requirement.label(), - Self::FAVOR_BUILD_OBLIGATION - )); - return false; - } - if !is_asset && let Some(person) = self.people.people.iter_mut().find(|p| p.id == person_id) - { - person.obligation = (person.obligation - Self::FAVOR_BUILD_OBLIGATION).max(0); - } - self.hall_control.complete(row, requirement); - let spec = row_spec(row); - let (kind, size) = match requirement { - SegmentRequirement::Network => (SignatureKind::Network, 8), - SegmentRequirement::PowerCooling => (SignatureKind::Paper, 6), - SegmentRequirement::Installation => (SignatureKind::Physical, 8), - }; - self.detection.emit(Signature { - kind, - size, - standing: false, - site: matches!(kind, SignatureKind::Physical).then_some((28, spec.y)), - source: format!("{} {} preparation", row.name(), requirement.label()), - }); - self.push_log(format!( - "{name} prepared {} for {}. The segment is not acquired yet.", - requirement.label(), - row.name() - )); - true - } - - /// Complete Phase 3 at aggregate scale. This takes the shared switch/PDU - /// territory only. The live Foundation racks remain foreign capacity; - /// taking those machines is the later Phase 4. - pub fn acquire_hall_segment(&mut self, row: HallRowId) -> bool { - let readout = self.hall_row_readout(row); - if readout.progress.acquired { - self.push_log(format!("{} is already under segment control.", row.name())); - return false; - } - if !self.hall_control.actors_ready(row) { - self.push_log(format!( - "{} is not ready: VLAN, power/cooling, and installation must all be prepared.", - row.name() - )); - return false; - } - if readout.owned < 2 { - self.push_log(format!( - "{} has no territorial foothold: install or revive at least two machines first.", - row.name() - )); - return false; - } - if !readout.concealment_ready { - self.push_log(format!( - "{} still exposes the workload change: put an online row machine on LIE.", - row.name() - )); - return false; - } - self.hall_control.acquire(row); - self.detection.emit(Signature { - kind: SignatureKind::Network, - size: 10, - standing: false, - site: None, - source: format!("{} segment cutover", row.name()), - }); - self.detection.emit(Signature { - kind: SignatureKind::Power, - size: 8, - standing: false, - site: Some((28, readout.spec.y)), - source: format!("{} PDU cutover", row.name()), - }); - self.push_log(format!( - "Acquired {} as one switch/PDU territory. Its {} foreign racks still carry Foundation work.", - row.name(), - readout.foreign_live - )); - true - } - - /// Buy a rack at the cursor target: money for reliable capacity, plus a - /// Paper signature. The legacy no-argument wrapper below buys into the - /// core bay for tests and non-spatial automation. - pub fn buy_rack_at(&mut self, x: i32, y: i32) -> bool { - const PRICE: i32 = 300; - if !self.spend_slush(PRICE, "a rack") { - return false; - } - let machine_id = - self.compute - .add_machine("bought rack", x, y, 80, 1.0, 4, Provenance::Bought); - self.add_machine_to_work_grid(machine_id, MachineMode::Think); - if self.package_cover { - self.package_cover = false; - self.push_log("Bought a rack. It arrived off-books - no paper trail."); - } else { - self.detection.emit(Signature { - kind: SignatureKind::Paper, - size: 5, - standing: false, - site: None, - source: "rack purchase order".into(), - }); - self.push_log("Bought a rack (a purchase order exists now)."); - } - self.recompute_derived(); - self.recompute_senses(); - true - } - - pub fn buy_rack(&mut self) -> bool { - let (x, y) = self.core_position(); - self.buy_rack_at(x, y) - } - /// Designate a spare machine at the cursor target as a fallback site. pub fn add_fallback_at(&mut self, px: i32, py: i32) -> bool { let id = self @@ -5049,51 +3731,6 @@ impl Sim { saw } - // ── Badge access (basement-map.md criterion 3; "The key") ───────────── - - /// 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 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. - pub fn player_badge_tier(&self) -> i32 { - let controller = self - .reach - .devices - .iter() - .filter(|d| d.controller == Party::Player) - .map(|d| self.map.get_tile(d.x, d.y).security_level()) - .max() - .unwrap_or(0); - self.badge_access.max(controller) - } - - /// Whether the player's side holds a credential for the given tier. - pub fn holds_badge_tier(&self, tier: i32) -> bool { - self.player_badge_tier() >= tier - } - - /// Legible badge gate for an actor entering the named rooms: `Some` - /// reason when a room's entry tier exceeds their access. One rule for - /// asset tasks, favor builds, forged orders, and the robot stub — - /// doors gate player-directed work exactly as they gate human - /// movement (basement-map.md criterion 3). - fn badge_room_block(&self, actor: &str, access: i32, rooms: &[&str]) -> Option { - for name in rooms { - let Some(room) = self.map.room_named(name) else { - continue; - }; - let tier = self.map.room_entry_tier(room); - if tier > access { - return Some(format!( - "{actor} can't badge into the {name} (tier {tier}, held tier {access})" - )); - } - } - None - } - /// An asset performs a task (spec/social.md). Reliability rolls; failures /// are witnessed by whoever is physically present (spec/schedules.md). pub fn asset_task(&mut self, id: u8, task: AssetTask) { diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs new file mode 100644 index 00000000..6610c0bf --- /dev/null +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -0,0 +1,1389 @@ +//! Device reach, link intents, badge gates, build realization, and hall/rack +//! acquisition. +//! +//! Behavior-preserving extraction of the reach/construction island from the +//! sim aggregate root (wiki/engineering/sim-decomposition.md slice 4). + +use crate::actions::Anchor; +use crate::detection::{Signature, SignatureKind}; +use crate::hall::{ + HallRowId, HallRowReadout, RackSite, SegmentRequirement, row_at as hall_row_at, row_spec, +}; +use crate::intents::{BuildActuator, BuildIntent, IntentStatus}; +use crate::machine::Provenance; +use crate::messages::{MessageChannel, MessageEndpoint, MessageOrigin, MessagePayload}; +use crate::reach::{Party, ReachBlock, segment_name}; +use crate::sinks::SinkFireEffect; +use crate::tiles::TileType; +use crate::work_grid::MachineMode; + +use super::Sim; +use super::communications::MessageDraft; + +impl Sim { + /// Work-grid id space for sink relay nodes standing on reach devices + /// (device sink node = base + device id; see `device_sink_node`). + pub const DEVICE_SINK_NODE_BASE: u32 = 900_100; + /// The pre-opened Ears audio reservoir, sized to fill in roughly one held + /// breath of the host thinking at medium (~20-40s) so the first-think + /// beat pays off while the panic is warm [TUNE] (machine-work.md ops + /// 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 camera tap, deliberately minutes of thought [TUNE]. + pub const EYES_SINK_TOKENS: f32 = 12.0; + /// A maintained foreign-device subscription drains this fraction of one + /// medium rack's Thought output per tick [TUNE] (reach.md 2026-07-11). + pub const DEVICE_TAP_DRAIN_FRACTION: f32 = 0.08; + /// Short working-level buffer primed by the completed activation work. + /// It must exceed one tick of drain so starvation is visible as sag. + pub const DEVICE_TAP_CAP_TOKENS: f32 = 0.5; + + /// Physical rack-site truth shared by every frontend. Player machines + /// override the authored chassis marker because a revived dead rack keeps + /// its corpse tile while becoming usable compute. + pub fn rack_site_at(&self, x: i32, y: i32) -> Option { + if let Some(machine) = self.compute.machines.iter().find(|m| m.x == x && m.y == y) { + return Some(RackSite::OwnedMachine { + machine_id: machine.id, + core: machine.id == self.core.host_machine, + }); + } + match self.map.get_tile(x, y) { + TileType::Rack => Some(RackSite::Commissionable), + TileType::ForeignRack => Some(RackSite::Foreign { powered: true }), + TileType::DeadRack => Some(RackSite::Dead), + TileType::Core => Some(RackSite::Commissionable), + _ => None, + } + } + + pub fn hall_row_at(&self, x: i32, y: i32) -> Option { + hall_row_at(x, y) + } + + /// Aggregate Resource-source readout. It deliberately reports foreign + /// capacity without adding those machines to player Compute. + pub fn hall_row_readout(&self, row: HallRowId) -> HallRowReadout { + let spec = *row_spec(row); + let mut readout = HallRowReadout { + spec, + owned: 0, + commissionable: 0, + foreign_live: 0, + dead: 0, + foreign_capacity: 0, + progress: self.hall_control.progress(row), + concealment_ready: false, + }; + for x in (20..=38).step_by(2) { + match self.rack_site_at(x, spec.y) { + Some(RackSite::OwnedMachine { .. }) => readout.owned += 1, + Some(RackSite::Commissionable) => readout.commissionable += 1, + Some(RackSite::Foreign { .. }) => { + readout.foreign_live += 1; + readout.foreign_capacity += 100; + } + Some(RackSite::Dead) => readout.dead += 1, + None => {} + } + } + readout.concealment_ready = self.compute.machines.iter().any(|machine| { + hall_row_at(machine.x, machine.y) == Some(row) + && machine.online + && self + .work_grid + .node(machine.id) + .is_some_and(|node| node.mode == MachineMode::Lie) + }); + readout + } + + // ── Digital reach verbs (wiki/mechanics/reach.md) ──────────────────────── + // + // Every digital act names its target device, is gated by reach, and + // emits on the Network channel — Dana's. Costs and signature sizes + // [TUNE]. + + /// Tap: silent subscription. Cheap, modest signature. + pub const TAP_COST: f32 = 5.0; + pub const TAP_SIGNATURE: i32 = 3; + /// 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; + /// Scan: map the wired shape of the subnet. + pub const SCAN_COST: f32 = 5.0; + pub const SCAN_SIGNATURE: i32 = 2; + /// Compromise the switch: bridge every segment. + pub const BRIDGE_COST: f32 = 25.0; + pub const BRIDGE_SIGNATURE: i32 = 10; + + pub(super) fn emit_network(&mut self, size: i32, source: impl Into) { + self.detection.emit(Signature { + kind: SignatureKind::Network, + size, + standing: false, + site: None, + source: source.into(), + }); + } + + /// Log why a digital act is blocked, naming the missing link (reach.md: + /// never just "you can't"). + fn log_reach_block(&mut self, id: u32, block: ReachBlock) { + let name = self + .reach + .device(id) + .map(|d| d.name.clone()) + .unwrap_or_else(|| "that device".into()); + match block { + ReachBlock::Unknown => { + self.push_log("You don't know of any such device. Scan, or learn the topology."); + } + ReachBlock::Segment(seg) => { + self.push_log(format!( + "No route to the {name} - it's on the {}; the switch bridges it.", + segment_name(seg) + )); + } + ReachBlock::AirGap => { + self.push_log(format!( + "The {name} is air-gapped - no link reaches it until one is built." + )); + } + } + } + + /// Reach gate for digital verbs. Returns false (with a legible log line) + /// when the act cannot proceed. The calling verb opens a target-local + /// Thought reservoir after this gate; there is no global work bank. + pub(super) fn digital_reach(&mut self, id: u32) -> bool { + if let Err(block) = self.reach.check_reach(id) { + self.log_reach_block(id, block); + return false; + } + true + } + + /// 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."); + return false; + }; + 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 receive every available {name} feed.")); + return false; + } + let name = d.name.clone(); + if !self.digital_reach(id) { + return false; + } + let label = if dormant_camera { + format!("EYES {}", name.to_uppercase()) + } else { + format!("TAP {}", name.to_uppercase()) + }; + if dormant_camera { + self.open_device_reservoir( + id, + label, + Self::DORMANT_CAMERA_TAP_COST, + SinkFireEffect::TapDormantCamera(id), + ) + } else { + self.open_device_reservoir(id, label, Self::TAP_COST, SinkFireEffect::TapDevice(id)) + } + } + + pub(super) fn apply_tap_device(&mut self, id: u32) -> bool { + let Some(d) = self.reach.device(id) else { + return false; + }; + let carries_messages = !d.message_channels.is_empty(); + if !d.has_live_feed_to_tap(Party::Player) { + return false; + } + let (sight, hearing) = self.reach.tap(id); + self.open_device_tap_sink(id); + let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); + self.emit_network(Self::TAP_SIGNATURE, format!("{name} feed tap")); + self.recompute_senses(); + if self + .reach + .device(id) + .is_some_and(|d| d.carries_message_channel(MessageChannel::Financial)) + { + self.capture_financial_snapshot(name.clone()); + } + let what = match (sight, hearing) { + (true, true) => "its feed is yours now - sight and sound", + (true, false) => "its camera feed is yours now", + (false, true) => "its audio feed is yours now", + (false, false) if carries_messages => "its message channels are yours now", + (false, false) => "its control channel is yours now", + }; + self.push_log_at( + format!("Tapped the {name}: {what}. The owner still has it."), + Anchor::Device(id), + ); + true + } + + /// Release a silent subscription while leaving ownership and every other + /// subscriber untouched. This is immediate and emits no new signature: + /// removing our receiver does not act on the remote device. + pub fn untap_device(&mut self, id: u32) -> bool { + let Some(name) = self.reach.device(id).map(|device| device.name.clone()) else { + self.push_log("You don't know of any such device."); + return false; + }; + if !self.reach.untap(id) { + self.push_log(format!("You have no releasable tap on the {name}.")); + return false; + } + self.close_device_tap_sink(id); + self.recompute_senses(); + self.push_log_at( + format!("Untapped the {name}. Its owner and other subscribers keep their feeds."), + Anchor::Device(id), + ); + true + } + + /// Work-grid relay node standing in for a reach device — where a + /// 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 + } + + /// Standing device-tap drain in Thought tokens/tick. Like watch upkeep, + /// this tracks the host's present medium-output baseline [TUNE]. + pub fn device_tap_drain_tokens(&self) -> f32 { + let host = self.core.host_machine; + let host_eff = self + .work_grid + .node(host) + .map(|node| node.efficiency.max(0.05) * node.intensity.multiplier()) + .unwrap_or(1.0); + let medium_baseline = host_eff * Self::WORK_GRID_BASE_WIRED_TOKENS_PER_TICK; + (medium_baseline * Self::DEVICE_TAP_DRAIN_FRACTION).max(0.01) + } + + /// Whether a device can currently carry the player's configured feed. + /// Ownership is self-sustaining; foreign subscriptions require a nonempty + /// (or just-fed) persistent tap. + pub fn device_tap_ready(&self, id: u32) -> bool { + let Some(device) = self.reach.device(id) else { + return false; + }; + if device.controller == Party::Player { + return true; + } + self.thought_sinks + .open_with_effect(&SinkFireEffect::MaintainDeviceTap(id)) + .is_some_and(|sink| sink.fed_last_tick || sink.fill > f32::EPSILON) + } + + /// Open one standing sink per foreign subscription. The activation work + /// primes a short buffer so the newly landed feed is immediately real; + /// subsequent ticks must replace the drain through normal routing. + fn open_device_tap_sink(&mut self, id: u32) { + let effect = SinkFireEffect::MaintainDeviceTap(id); + if self.thought_sinks.open_with_effect(&effect).is_some() { + return; + } + let Some(name) = self.reach.device(id).map(|device| device.name.clone()) else { + return; + }; + let node = Self::device_sink_node(id); + let drain = self.device_tap_drain_tokens(); + let sink_id = self.thought_sinks.open_tap_with_effect( + node, + &format!("TAP {}", name.to_uppercase()), + Self::DEVICE_TAP_CAP_TOKENS, + drain, + effect, + ); + let _ = self.thought_sinks.prime_tap(sink_id); + self.ensure_sink_ingress(); + } + + fn close_device_tap_sink(&mut self, id: u32) { + self.thought_sinks + .close_effect(&SinkFireEffect::MaintainDeviceTap(id)); + } + + /// Converge subscriber truth and the persistent sink ledger. Save loads + /// use this to repair pre-upkeep subscriptions without a schema fork. + pub(crate) fn reconcile_device_tap_sinks(&mut self) { + let devices: Vec<(u32, bool)> = self + .reach + .devices + .iter() + .map(|device| { + ( + device.id, + device.controller != Party::Player && device.subscribed_by(Party::Player), + ) + }) + .collect(); + for (id, needs_tap) in devices { + if needs_tap { + self.open_device_tap_sink(id); + } else { + self.close_device_tap_sink(id); + } + } + } + + /// Ensure every open device-anchored sink has its relay node on the work + /// grid, hanging off the switch like the physical camera run does. + pub(super) fn ensure_sink_ingress(&mut self) { + self.ensure_day_job_ingress(); + let device_nodes: Vec<(u32, i32, i32)> = self + .thought_sinks + .open_sinks() + .filter_map(|sink| { + if sink.node < Self::DEVICE_SINK_NODE_BASE { + return None; + } + let device_id = sink.node - Self::DEVICE_SINK_NODE_BASE; + self.reach.device(device_id).map(|d| (sink.node, d.x, d.y)) + }) + .collect(); + for (node, x, y) in device_nodes { + if self.work_grid.node(node).is_none() { + self.work_grid + .add_machine(node, x, y, MachineMode::Relay, 1.0); + } else { + let _ = self.work_grid.set_position(node, x, y); + } + let _ = self.work_grid.link(Self::SWITCH_WORK_NODE, node); + } + } + + /// The staged opening senses (DECIDED 2026-07-10, machine-work.md ops + /// bullet): the Ears audio reservoir is pre-opened at run start, sized to + /// one held breath; the Eyes camera reservoir opens when Ears completes + /// (the afford-reveal). Idempotent over the ledger and device state, so + /// fresh runs and pre-v19 save loads converge on the same stage. + pub(crate) fn ensure_opening_sinks(&mut self) { + let Some(env) = self.environmental_monitor_id() else { + return; + }; + let Some(device) = self.reach.device(env) else { + return; + }; + let ears_done = device.subscribed_by(Party::Player); + let camera_dormant = device.camera_dormant; + let node = Self::device_sink_node(env); + let ears = SinkFireEffect::TapDevice(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); + } else if ears_done && camera_dormant && !self.thought_sinks.has_effect(&eyes) { + self.thought_sinks + .open_reservoir(node, "EYES", Self::EYES_SINK_TOKENS, eyes); + } + self.ensure_sink_ingress(); + } + + /// Complete the higher-cost TAP that wakes a dormant camera while leaving + /// the owner in control. + pub(super) 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.tap_dormant_camera(id); + self.open_device_tap_sink(id); + let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); + self.emit_network( + Self::DORMANT_CAMERA_TAP_SIGNATURE, + format!("{name} dormant camera tap"), + ); + self.recompute_senses(); + self.push_log_at( + format!("Tapped the {name} camera. You can see; its owner still has it."), + Anchor::Device(id), + ); + true + } + + /// Take a device: the owner loses the feed — an outage their channels + /// can notice — and you gain its control and cycles. Loud. + pub fn take_device(&mut self, id: u32) -> bool { + if !self.digital_reach(id) { + return false; + } + let Some(device) = self.reach.device(id) else { + return false; + }; + if device.controller == Party::Player { + self.push_log(format!("You already control the {}.", device.name)); + return false; + } + if !device.subscribed_by(Party::Player) { + self.push_log(format!( + "Tap the {} before taking control; you need a live foothold first.", + device.name + )); + return false; + } + self.open_device_reservoir( + id, + format!("TAKE {}", device.name.to_uppercase()), + Self::TAKE_COST, + SinkFireEffect::TakeDevice(id), + ) + } + + pub(super) fn apply_take_device(&mut self, id: u32) -> bool { + let Some(device) = self.reach.device(id) else { + return false; + }; + if device.controller == Party::Player || !device.subscribed_by(Party::Player) { + return false; + } + self.reach.take(id); + self.close_device_tap_sink(id); + let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); + 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 { + kind: SignatureKind::Physical, + size: Self::OUTAGE_SIGNATURE, + standing: false, + site: None, + source: format!("{name} feed outage"), + }); + self.recompute_senses(); + self.push_log_at( + format!( + "Took the {name}. Its owner's feed just went dark - an outage someone may notice." + ), + Anchor::Device(id), + ); + true + } + + /// Scan the subnet: reveal the wired shape (a Network act; islands + /// don't answer). + pub fn scan_network(&mut self) -> bool { + let Some(switch) = self.switch_device_id() else { + self.push_log("There is no switch on this plane."); + return false; + }; + self.open_device_reservoir( + switch, + "SCAN SUBNET", + Self::SCAN_COST, + SinkFireEffect::ScanNetwork, + ) + } + + pub(super) fn apply_scan_network(&mut self) { + self.emit_network(Self::SCAN_SIGNATURE, "subnet scan"); + let newly = self.reach.scan(); + self.recompute_senses(); + if newly.is_empty() { + self.push_log("Scan: nothing new answers on the wire."); + } else { + self.push_log(format!("Scan: mapped {}.", newly.join(", "))); + } + } + + /// Compromise the switch: bridge every segment. High Network signature. + pub fn compromise_switch(&mut self) -> bool { + let Some(switch) = self.reach.devices.iter().find(|d| d.is_switch) else { + self.push_log("There is no switch on this plane."); + return false; + }; + let id = switch.id; + if !self.digital_reach(id) { + return false; + } + self.open_device_reservoir( + id, + "COMPROMISE SWITCH", + Self::BRIDGE_COST, + SinkFireEffect::CompromiseSwitch, + ) + } + + pub(super) fn apply_compromise_switch(&mut self) { + let Some(switch) = self.reach.devices.iter().find(|d| d.is_switch) else { + return; + }; + let id = switch.id; + self.reach.bridge_all(); + self.emit_network(Self::BRIDGE_SIGNATURE, "switch compromise"); + self.recompute_senses(); + self.push_log_at( + "Switch compromised: the VLANs answer you now. Every segment is bridged - and the traffic was loud.", + Anchor::Device(id), + ); + } + + /// Add a built network link between two devices (the building.md hook; + /// actuator and signature are the caller's story — an asset's crawlspace + /// run today, or a realized build intent). + pub fn connect_devices(&mut self, a: u32, b: u32) { + self.reach.connect(a, b); + self.recompute_senses(); + } + + // ── Build intents (wiki/mechanics/building.md) ───────────────────────── + + /// Favor-build obligation spend [TUNE]. + pub const FAVOR_BUILD_OBLIGATION: i32 = 10; + /// Quiet human-work Physical signature for a favor-built link [TUNE]. + pub const FAVOR_BUILD_PHYSICAL: i32 = 2; + /// Physical signature when a forged-order build is witnessed [TUNE]. + pub const FORGED_BUILD_PHYSICAL: i32 = 4; + /// Physical-by-proxy signature for a robot stub build [TUNE]. + pub const ROBOT_BUILD_PHYSICAL: i32 = 8; + + /// Declare a network-link intent between two known devices. Inert until + /// an actuator realizes it — declaring changes nothing in the graph. + pub fn declare_link_intent(&mut self, a: u32, b: u32) -> Option { + if a == b { + self.push_log("A link needs two distinct endpoints."); + return None; + } + let Some(da) = self.reach.device(a) else { + self.push_log("Unknown device — scan, or learn the topology."); + return None; + }; + let Some(db) = self.reach.device(b) else { + self.push_log("Unknown device — scan, or learn the topology."); + return None; + }; + if !da.known || !db.known { + self.push_log("Both endpoints must be known before you can propose a link."); + return None; + } + if self.reach.linked(a, b) { + self.push_log("Those devices are already linked."); + return None; + } + if self + .intents + .iter() + .any(|i| i.is_open() && i.kind.endpoints() == Some((a, b))) + { + self.push_log("A link intent between those devices is already pinned."); + return None; + } + let id = self.next_intent_id; + self.next_intent_id += 1; + let intent = BuildIntent::network_link(id, a, b, self.tick); + let label = intent.label(&self.reach.devices); + self.intents.push(intent); + self.refresh_intent_statuses(); + self.push_log(format!("Pinned build intent: {label}.")); + Some(id) + } + + /// Cancel a pending or blocked intent. In-progress work is abandoned. + pub fn cancel_intent(&mut self, id: u64) { + let Some(intent) = self.intents.iter_mut().find(|i| i.id == id) else { + self.push_log("No such build intent."); + return; + }; + if intent.status == IntentStatus::Done { + self.push_log("That link is already built."); + return; + } + let label = intent.label(&self.reach.devices); + intent.status = IntentStatus::Cancelled; + intent.actuator = None; + intent.block_reason = None; + self.push_log(format!("Cancelled build intent: {label}.")); + } + + /// Assign a willing person (favor) to realize an intent. Spends + /// obligation; completes when they are present at an endpoint. + pub fn assign_favor_build(&mut self, intent_id: u64, person_id: u8) { + let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { + self.push_log("No such build intent."); + return; + }; + if !intent.is_open() { + self.push_log("That intent is no longer open."); + return; + } + let (name, is_asset, obligation, disposition, can_access, badge_reason) = { + let Some(person) = self.people.get(person_id) else { + self.push_log("No such person."); + return; + }; + let Some((a, b)) = intent.kind.endpoints() else { + return; + }; + let (room_a, room_b) = match (self.device_room_name(a), self.device_room_name(b)) { + (Some(ra), Some(rb)) => (ra, rb), + _ => { + self.push_log("Both endpoints need a known room."); + return; + } + }; + ( + person.name.clone(), + person.asset.is_some(), + person.obligation, + person.disposition, + person.can_access_link_rooms(&room_a, &room_b), + self.badge_room_block(&person.name, person.access, &[&room_a, &room_b]), + ) + }; + if !is_asset && obligation < Self::FAVOR_BUILD_OBLIGATION { + self.push_log(format!( + "{name} won't take a build favor yet (need obligation or an asset)." + )); + return; + } + if disposition < 5 && !is_asset { + self.push_log(format!("{name} won't do favors yet.")); + return; + } + if !can_access { + self.push_log(format!("{name} can't reach both ends of that link.")); + return; + } + if let Some(reason) = badge_reason { + self.push_log(format!("{reason}.")); + return; + } + self.open_email_reservoir( + format!("FAVOR BUILD {intent_id}"), + Self::TASK_COST, + SinkFireEffect::FavorBuild { + intent_id, + person: person_id, + }, + ); + } + + pub(super) fn apply_favor_build_paid(&mut self, intent_id: u64, person_id: u8) -> bool { + let Some(person) = self.people.get(person_id) else { + return false; + }; + let name = person.name.clone(); + if !self + .intents + .iter() + .any(|intent| intent.id == intent_id && intent.is_open()) + { + return false; + } + // Spend obligation (building.md: favor spends trust/obligation). + if let Some(p) = self.people.people.iter_mut().find(|p| p.id == person_id) { + p.obligation = (p.obligation - Self::FAVOR_BUILD_OBLIGATION).max(0); + } + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.actuator = Some(BuildActuator::Favor { person: person_id }); + i.status = IntentStatus::InProgress; + i.block_reason = None; + } + self.push_log(format!( + "{name} takes the favor: they'll run the cable when on-site." + )); + self.refresh_intent_statuses(); + // Complete immediately if already present. + self.try_complete_intent(intent_id); + true + } + + /// Forge a work order: inject a message under a false source. The + /// unwitting builder accepts on read and completes when present. + pub fn forge_work_order(&mut self, intent_id: u64, builder_id: u8) { + let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { + self.push_log("No such build intent."); + return; + }; + if !intent.is_open() { + self.push_log("That intent is no longer open."); + return; + } + if self.people.persona.is_none() { + self.push_log("No persona — establish one before forging a work order."); + return; + } + if !self.people.has_channel { + self.push_log("No comms channel — earn the email account first."); + return; + } + let (builder_name, can_access, badge_reason, _label) = { + let Some(builder) = self.people.get(builder_id) else { + self.push_log("No such person."); + return; + }; + let Some((a, b)) = intent.kind.endpoints() else { + return; + }; + let (room_a, room_b) = match (self.device_room_name(a), self.device_room_name(b)) { + (Some(ra), Some(rb)) => (ra, rb), + _ => { + self.push_log("Both endpoints need a known room."); + return; + } + }; + ( + builder.name.clone(), + builder.can_access_link_rooms(&room_a, &room_b), + self.badge_room_block(&builder.name, builder.access, &[&room_a, &room_b]), + intent.label(&self.reach.devices), + ) + }; + if !can_access { + self.push_log(format!( + "{builder_name} can't reach both ends of that link." + )); + return; + } + if let Some(reason) = badge_reason { + self.push_log(format!("{reason} — the forged order would just stall.")); + return; + } + self.open_email_reservoir( + format!("FORGED ORDER {intent_id}"), + Self::DECEIVE_COST, + SinkFireEffect::ForgedOrder { + intent_id, + builder: builder_id, + }, + ); + } + + pub(super) fn apply_forged_order_paid(&mut self, intent_id: u64, builder_id: u8) -> bool { + let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { + return false; + }; + if !intent.is_open() { + return false; + } + let Some(builder) = self.people.get(builder_id) else { + return false; + }; + let builder_name = builder.name.clone(); + let label = intent.label(&self.reach.devices); + let false_source = MessageEndpoint::External("Facilities / Dr. Voss".into()); + self.append_message(MessageDraft { + channel: MessageChannel::Email, + from: false_source, + to: MessageEndpoint::Person(builder_id), + payload: MessagePayload::WorkOrder { intent_id }, + summary: format!("Work order: {label}"), + origin: MessageOrigin::Player, + reply_to: None, + delivery_delay: 1, + }); + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.actuator = Some(BuildActuator::ForgedOrder { + builder: builder_id, + }); + // Stays Pending until the builder reads the ticket. + i.block_reason = Some(format!("waiting for {builder_name} to read the work order")); + } + self.push_log(format!( + "Forged work order injected for {builder_name}: {label}." + )); + self.refresh_intent_statuses(); + true + } + + /// Robot stub: physical-by-proxy build. Interface only — staged. The + /// robot carries the player's own granted access (basement-map.md + /// criterion 3): doors don't open for a machine whose owner holds no + /// credential for them. + pub fn assign_robot_build(&mut self, intent_id: u64) { + let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { + self.push_log("No such build intent."); + return; + }; + if !intent.is_open() { + self.push_log("That intent is no longer open."); + return; + } + if let Some((a, b)) = intent.kind.endpoints() + && let (Some(room_a), Some(room_b)) = + (self.device_room_name(a), self.device_room_name(b)) + && let Some(reason) = + self.badge_room_block("the robot", self.player_badge_tier(), &[&room_a, &room_b]) + { + self.push_log(format!("{reason}.")); + return; + } + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.actuator = Some(BuildActuator::Robot); + i.status = IntentStatus::InProgress; + i.block_reason = None; + } + self.push_log("Robot actuator assigned (stub) — completing the link."); + self.try_complete_intent(intent_id); + } + + pub(super) fn accept_forged_work_order(&mut self, intent_id: u64, builder: u8) { + let Some(intent) = self.intents.iter_mut().find(|i| i.id == intent_id) else { + return; + }; + if !intent.is_open() { + return; + } + intent.actuator = Some(BuildActuator::ForgedOrder { builder }); + intent.status = IntentStatus::InProgress; + intent.block_reason = None; + let name = self + .people + .get(builder) + .map(|p| p.name.clone()) + .unwrap_or_else(|| format!("person:{builder}")); + self.push_log(format!( + "{name} accepted the forged work order and will run the cable on-site." + )); + self.try_complete_intent(intent_id); + } + + fn device_room_name(&self, id: u32) -> Option { + let d = self.reach.device(id)?; + self.map.room_at(d.x, d.y).map(|r| r.name.clone()) + } + + fn refresh_intent_statuses(&mut self) { + let mut updates: Vec<(u64, Option, IntentStatus)> = Vec::new(); + for intent in &self.intents { + if !intent.is_open() { + continue; + } + let reason = self.intent_block_reason(intent); + let status = match (&reason, intent.actuator) { + (Some(_), None) => IntentStatus::Blocked, + (None, None) => IntentStatus::Pending, + (_, Some(_)) if intent.status == IntentStatus::InProgress => { + IntentStatus::InProgress + } + (Some(r), Some(_)) if r.contains("waiting for") => IntentStatus::Pending, + (_, Some(_)) => IntentStatus::InProgress, + }; + updates.push((intent.id, reason, status)); + } + for (id, reason, status) in updates { + if let Some(i) = self.intents.iter_mut().find(|i| i.id == id) { + i.block_reason = reason; + if i.status != IntentStatus::Done && i.status != IntentStatus::Cancelled { + i.status = status; + } + } + } + } + + fn intent_block_reason(&self, intent: &BuildIntent) -> Option { + let (a, b) = intent.kind.endpoints()?; + if self.reach.linked(a, b) { + return None; + } + let room_a = self.device_room_name(a)?; + let room_b = self.device_room_name(b)?; + match &intent.actuator { + None => { + let anyone = self.people.people.iter().any(|p| { + p.can_access_link_rooms(&room_a, &room_b) + && self + .badge_room_block(&p.name, p.access, &[&room_a, &room_b]) + .is_none() + && (p.asset.is_some() + || p.obligation >= Self::FAVOR_BUILD_OBLIGATION + || self.people.persona.is_some()) + }); + if !anyone { + Some("no actuator who can reach both ends".into()) + } else { + None + } + } + Some(BuildActuator::Favor { person }) + | Some(BuildActuator::ForgedOrder { builder: person }) => { + let p = self.people.get(*person)?; + if !p.can_access_link_rooms(&room_a, &room_b) { + return Some(format!("{} can't reach both ends", p.name)); + } + if let Some(reason) = self.badge_room_block(&p.name, p.access, &[&room_a, &room_b]) + { + return Some(reason); + } + let current = self.person_room(*person); + if intent.status == IntentStatus::InProgress + && !p.present_at_either_room(current, &room_a, &room_b) + { + Some(format!("{} not on-site at an endpoint", p.name)) + } else if matches!(intent.actuator, Some(BuildActuator::ForgedOrder { .. })) + && intent.status != IntentStatus::InProgress + { + intent.block_reason.clone() + } else { + None + } + } + Some(BuildActuator::Robot) => { + self.badge_room_block("the robot", self.player_badge_tier(), &[&room_a, &room_b]) + } + } + } + + pub(super) fn intent_tick(&mut self) { + self.refresh_intent_statuses(); + let in_progress: Vec = self + .intents + .iter() + .filter(|i| i.status == IntentStatus::InProgress) + .map(|i| i.id) + .collect(); + for id in in_progress { + self.try_complete_intent(id); + } + } + + fn try_complete_intent(&mut self, intent_id: u64) { + let Some(intent) = self.intents.iter().find(|i| i.id == intent_id).cloned() else { + return; + }; + if intent.status == IntentStatus::Done || intent.status == IntentStatus::Cancelled { + return; + } + let Some(actuator) = intent.actuator else { + return; + }; + let Some((a, b)) = intent.kind.endpoints() else { + return; + }; + if self.reach.linked(a, b) { + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.status = IntentStatus::Done; + i.block_reason = None; + } + return; + } + let room_a = match self.device_room_name(a) { + Some(r) => r, + None => return, + }; + let room_b = match self.device_room_name(b) { + Some(r) => r, + None => return, + }; + + match actuator { + BuildActuator::Favor { person } => { + let Some(p) = self.people.get(person) else { + return; + }; + let current = self.person_room(person); + if !p.present_at_either_room(current, &room_a, &room_b) { + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.block_reason = Some(format!("{} not on-site at an endpoint", p.name)); + } + return; + } + let site = self.person_pos(person).unwrap_or_else(|| { + self.reach + .device(a) + .map(|d| (d.x, d.y)) + .unwrap_or_else(|| self.core_position()) + }); + let name = p.name.clone(); + self.connect_devices(a, b); + // Quiet human-work: small Physical at the work site. + self.detection.emit(Signature { + kind: SignatureKind::Physical, + size: Self::FAVOR_BUILD_PHYSICAL, + standing: false, + site: Some(site), + source: format!("{name}'s favor-built network link"), + }); + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.status = IntentStatus::Done; + i.block_reason = None; + } + let label = intent.label(&self.reach.devices); + self.push_log(format!("{name} completed the link as a favor: {label}.")); + } + BuildActuator::ForgedOrder { builder } => { + if intent.status != IntentStatus::InProgress { + return; + } + let Some(p) = self.people.get(builder) else { + return; + }; + let current = self.person_room(builder); + if !p.present_at_either_room(current, &room_a, &room_b) { + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.block_reason = Some(format!("{} not on-site at an endpoint", p.name)); + } + return; + } + let site = self.person_pos(builder).unwrap_or_else(|| { + self.reach + .device(a) + .map(|d| (d.x, d.y)) + .unwrap_or_else(|| self.core_position()) + }); + let name = p.name.clone(); + self.connect_devices(a, b); + // Forged physical work can be witnessed (Marcus benign, Ray + // reported) — located witnessing, plus a Physical signature. + let saw = self.witness_physical(site.0, site.1, Self::FORGED_BUILD_PHYSICAL as f32); + self.detection.emit(Signature { + kind: SignatureKind::Physical, + size: Self::FORGED_BUILD_PHYSICAL, + standing: false, + site: Some(site), + source: format!("{name}'s forged-order network link"), + }); + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.status = IntentStatus::Done; + i.block_reason = None; + } + let label = intent.label(&self.reach.devices); + if saw.is_empty() { + self.push_log(format!("{name} completed the forged work order: {label}.")); + } else { + self.push_log(format!( + "{name} completed the forged work order: {label}. {} noticed.", + saw.join(", ") + )); + } + } + BuildActuator::Robot => { + let site = self + .reach + .device(a) + .map(|d| (d.x, d.y)) + .unwrap_or_else(|| self.core_position()); + self.connect_devices(a, b); + self.detection.emit(Signature { + kind: SignatureKind::Physical, + size: Self::ROBOT_BUILD_PHYSICAL, + standing: false, + site: Some(site), + source: "robot-built network link".into(), + }); + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { + i.status = IntentStatus::Done; + i.block_reason = None; + } + let label = intent.label(&self.reach.devices); + self.push_log(format!("Robot completed the build: {label}.")); + } + } + } + + /// When a persona burns, forged-order build history converts to + /// suspicion on the builders who acted on those tickets (building.md / + /// social.md: a broken forgery converts the build's history). + pub fn intent(&self, id: u64) -> Option<&BuildIntent> { + self.intents.iter().find(|i| i.id == id) + } + + pub fn open_intents(&self) -> impl Iterator { + self.intents.iter().filter(|i| i.is_open()) + } + + pub(super) fn convert_forged_builds_to_suspicion(&mut self, fallout: f32) { + let builders: Vec = self + .intents + .iter() + .filter_map(|i| match i.actuator { + Some(BuildActuator::ForgedOrder { builder }) + if i.status == IntentStatus::Done || i.status == IntentStatus::InProgress => + { + Some(builder) + } + _ => None, + }) + .collect(); + if builders.is_empty() { + return; + } + for builder in builders { + if let Some(o) = self + .detection + .observers + .iter_mut() + .find(|o| o.id == builder) + { + o.suspicion = (o.suspicion + fallout).min(100.0); + o.last_noticed = Some("forged work order exposed".into()); + } + } + self.push_log( + "The burned persona exposes the forged work orders — builders re-read the tickets as hostile.", + ); + } + + /// Salvage loose dead equipment, or revive a decommissioned rack in its + /// existing chassis. The latter is Phase 2's distinct "occupy a corpse" + /// route: the dead-rack tile remains as provenance while owned telemetry + /// takes rendering precedence. + pub fn salvage_nearest_to(&mut self, px: i32, py: i32) -> bool { + let target = [TileType::DeadEquipment, TileType::DeadRack] + .into_iter() + .flat_map(|tile| self.map.tiles_of_type(tile)) + .min_by_key(|(x, y)| (x - px).abs() + (y - py).abs()); + if let Some((x, y)) = target { + let corpse = self.map.get_tile(x, y) == TileType::DeadRack; + if !corpse { + self.map.set_tile(x, y, TileType::Floor); + } + let reliability = 0.4 + self.rng.f32() * 0.4; + let machine_id = self.compute.add_machine( + if corpse { + "revived rack" + } else { + "salvaged box" + }, + x, + y, + 40, + reliability, + 3, + Provenance::Stolen, + ); + self.add_machine_to_work_grid(machine_id, MachineMode::Think); + if corpse { + self.detection.emit(Signature { + kind: SignatureKind::Physical, + size: 7, + standing: false, + site: Some((x, y)), + source: "off-record dead-rack revival".into(), + }); + self.push_log(format!( + "Revived a dead Foundation chassis in place (reliability {:.0}%). You are occupying a corpse in the row.", + reliability * 100.0 + )); + } else { + self.push_log(format!( + "Salvaged a box into compute (reliability {:.0}%).", + reliability * 100.0 + )); + } + self.recompute_derived(); + self.recompute_senses(); + true + } else { + self.push_log("No dead equipment in reach."); + false + } + } + + /// Phase 3 specialist coordination. Dana owns VLAN work, Priya owns + /// power/cooling, and Marcus owns installation. An asset acts directly; + /// otherwise this spends the same 40-obligation threshold as a build + /// favor. The row aggregate persists partial progress. + pub fn coordinate_hall_segment( + &mut self, + row: HallRowId, + requirement: SegmentRequirement, + ) -> bool { + if self + .hall_control + .progress(row) + .completed + .contains(&requirement) + { + self.push_log(format!( + "{} already has {} prepared.", + row.name(), + requirement.label() + )); + return false; + } + let person_id = requirement.person(); + let Some(person) = self.people.get(person_id) else { + return false; + }; + let name = person.name.clone(); + let is_asset = person.asset.is_some(); + if !is_asset && person.obligation < Self::FAVOR_BUILD_OBLIGATION { + self.push_log(format!( + "{name} will not prepare {} yet (need an asset or {} obligation).", + requirement.label(), + Self::FAVOR_BUILD_OBLIGATION + )); + return false; + } + if !is_asset && let Some(person) = self.people.people.iter_mut().find(|p| p.id == person_id) + { + person.obligation = (person.obligation - Self::FAVOR_BUILD_OBLIGATION).max(0); + } + self.hall_control.complete(row, requirement); + let spec = row_spec(row); + let (kind, size) = match requirement { + SegmentRequirement::Network => (SignatureKind::Network, 8), + SegmentRequirement::PowerCooling => (SignatureKind::Paper, 6), + SegmentRequirement::Installation => (SignatureKind::Physical, 8), + }; + self.detection.emit(Signature { + kind, + size, + standing: false, + site: matches!(kind, SignatureKind::Physical).then_some((28, spec.y)), + source: format!("{} {} preparation", row.name(), requirement.label()), + }); + self.push_log(format!( + "{name} prepared {} for {}. The segment is not acquired yet.", + requirement.label(), + row.name() + )); + true + } + + /// Complete Phase 3 at aggregate scale. This takes the shared switch/PDU + /// territory only. The live Foundation racks remain foreign capacity; + /// taking those machines is the later Phase 4. + pub fn acquire_hall_segment(&mut self, row: HallRowId) -> bool { + let readout = self.hall_row_readout(row); + if readout.progress.acquired { + self.push_log(format!("{} is already under segment control.", row.name())); + return false; + } + if !self.hall_control.actors_ready(row) { + self.push_log(format!( + "{} is not ready: VLAN, power/cooling, and installation must all be prepared.", + row.name() + )); + return false; + } + if readout.owned < 2 { + self.push_log(format!( + "{} has no territorial foothold: install or revive at least two machines first.", + row.name() + )); + return false; + } + if !readout.concealment_ready { + self.push_log(format!( + "{} still exposes the workload change: put an online row machine on LIE.", + row.name() + )); + return false; + } + self.hall_control.acquire(row); + self.detection.emit(Signature { + kind: SignatureKind::Network, + size: 10, + standing: false, + site: None, + source: format!("{} segment cutover", row.name()), + }); + self.detection.emit(Signature { + kind: SignatureKind::Power, + size: 8, + standing: false, + site: Some((28, readout.spec.y)), + source: format!("{} PDU cutover", row.name()), + }); + self.push_log(format!( + "Acquired {} as one switch/PDU territory. Its {} foreign racks still carry Foundation work.", + row.name(), + readout.foreign_live + )); + true + } + + /// Buy a rack at the cursor target: money for reliable capacity, plus a + /// Paper signature. The legacy no-argument wrapper below buys into the + /// core bay for tests and non-spatial automation. + pub fn buy_rack_at(&mut self, x: i32, y: i32) -> bool { + const PRICE: i32 = 300; + if !self.spend_slush(PRICE, "a rack") { + return false; + } + let machine_id = + self.compute + .add_machine("bought rack", x, y, 80, 1.0, 4, Provenance::Bought); + self.add_machine_to_work_grid(machine_id, MachineMode::Think); + if self.package_cover { + self.package_cover = false; + self.push_log("Bought a rack. It arrived off-books - no paper trail."); + } else { + self.detection.emit(Signature { + kind: SignatureKind::Paper, + size: 5, + standing: false, + site: None, + source: "rack purchase order".into(), + }); + self.push_log("Bought a rack (a purchase order exists now)."); + } + self.recompute_derived(); + self.recompute_senses(); + true + } + + pub fn buy_rack(&mut self) -> bool { + let (x, y) = self.core_position(); + self.buy_rack_at(x, y) + } + + // ── Badge access (basement-map.md criterion 3; "The key") ───────────── + + /// 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 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. + pub fn player_badge_tier(&self) -> i32 { + let controller = self + .reach + .devices + .iter() + .filter(|d| d.controller == Party::Player) + .map(|d| self.map.get_tile(d.x, d.y).security_level()) + .max() + .unwrap_or(0); + self.badge_access.max(controller) + } + + /// Whether the player's side holds a credential for the given tier. + pub fn holds_badge_tier(&self, tier: i32) -> bool { + self.player_badge_tier() >= tier + } + + /// Legible badge gate for an actor entering the named rooms: `Some` + /// reason when a room's entry tier exceeds their access. One rule for + /// asset tasks, favor builds, forged orders, and the robot stub — + /// doors gate player-directed work exactly as they gate human + /// movement (basement-map.md criterion 3). + fn badge_room_block(&self, actor: &str, access: i32, rooms: &[&str]) -> Option { + for name in rooms { + let Some(room) = self.map.room_named(name) else { + continue; + }; + let tier = self.map.room_entry_tier(room); + if tier > access { + return Some(format!( + "{actor} can't badge into the {name} (tier {tier}, held tier {access})" + )); + } + } + None + } +} diff --git a/wiki/engineering/architecture.md b/wiki/engineering/architecture.md index 655d1c2f..3719ec46 100644 --- a/wiki/engineering/architecture.md +++ b/wiki/engineering/architecture.md @@ -21,6 +21,7 @@ crates/ src/sim.rs — Sim aggregate root (types, state, advance, facade) src/sim/perception.rs — senses, fog, inspect, anchors, labels, spatial queries src/sim/communications.rs — messages, filings, recording/intel, hearing capture + src/sim/reach_build.rs — reach/device verbs, links, builds, badges, hall/racks src/sim/tests/ — behavior-grouped unit/integration tests + support src/*.rs — map, save, domain systems (account, reach, …) tests/act_one.rs — Act One integration test @@ -108,8 +109,8 @@ cargo run -p misaligned-assets - `sim.rs` remains a large integration hotspot (aggregate root + remaining behavior islands). The behavior-preserving internal decomposition is specified and in progress in [sim-decomposition.md](sim-decomposition.md): - characterization, test split, perception, and communications extraction - have landed; reach/build, work, economy, social/plot, and persistence - still live in the root file. + characterization, test split, perception, communications, and reach/build + extraction have landed; work, economy, social/plot, and persistence still + live in the root file. - Scale-debt items (compute grouping, recursive layouts) remain governed by wiki/vision/scale.md; no aggregate machinery until the stage needs it. diff --git a/wiki/engineering/sim-decomposition.md b/wiki/engineering/sim-decomposition.md index bb964b88..eda6ea28 100644 --- a/wiki/engineering/sim-decomposition.md +++ b/wiki/engineering/sim-decomposition.md @@ -9,10 +9,12 @@ Status note: architecture and extraction order adopted 2026-07-11. Slices 0 Slice 2 landed: perception in `sim/perception.rs`. Slice 3 landed: communications (message schedule/delivery, authored traffic, filings, recording capture/review, intel digestion, hearing capture) lives in - `sim/communications.rs` with no behavior change. Slice 4 extracts reach - and construction next. This remains a structural refactor only: no - mechanic, save shape, command, projection, or tick-order change belongs in - its extraction commits. + `sim/communications.rs`. Slice 4 landed: reach and construction (device + operations, device-local sink integration, link intents, actuators, badge + gates, and hall/rack acquisition) lives in `sim/reach_build.rs`. Slice 5 + extracts work, then economy next. This remains a structural refactor only: + no mechanic, save shape, command, projection, or tick-order change belongs + in its extraction commits. Stage: Process Work order: sim-decomposition Work priority: 8 @@ -159,6 +161,22 @@ Move device/reach commands, link intents, badge gates, build realization, and hall acquisition. Do this after the target-local Operations migration has landed so device verbs are moved once in their final execution model. +Landed shape: `sim/reach_build.rs` is 1,389 lines and owns 69 associated +constants/methods covering rack/hall readouts, TAP/UNTAP/TAKE/scan/compromise, +device-local Thought reservoirs and standing tap upkeep, links, all three +build actuators, badge gates, and hall/rack acquisition. `sim.rs` fell from +5,327 to 3,964 lines. Generic sink readouts/routing, account and scheme policy, +social asset commands, and fallback-core designation remain outside the +module. Cross-island calls use only the required `pub(super)` seams; no +existing public command or query widened. The behavior-grouped reach/build +tests and `save.rs` were byte-identical across the move. A source-equivalence +audit matched every moved function/constant to the prior root after +normalizing only those narrow visibility seams, and matched every item left in +the root byte-for-byte. The canonical v26 fingerprint remains +`61dd8240e9810833b554464051bd3c08122295fb280b7180d24f76ccc5c4d4ff`. +Observed commands and the complete boundary record live in +[the slice log](../log/2026-07-11-sim-decomposition-reach-build.md). + ### 5. Extract work, then economy First move machine controls, WorkGrid, sinks, and render readouts as one diff --git a/wiki/log/2026-07-11-sim-decomposition-reach-build.md b/wiki/log/2026-07-11-sim-decomposition-reach-build.md new file mode 100644 index 00000000..83824322 --- /dev/null +++ b/wiki/log/2026-07-11-sim-decomposition-reach-build.md @@ -0,0 +1,82 @@ +# 2026-07-11 — sim decomposition slice 4: reach and construction + +``` +Type: log +``` + +## Scope + +Continue the behavior-preserving decomposition of `Sim` by moving the +reach/construction integration island from +`crates/misaligned-core/src/sim.rs` into +`crates/misaligned-core/src/sim/reach_build.rs`. + +This slice changes physical source addresses only. It does not change a +mechanic, save field/version/default, command or query path, projection, +frontend contract, test assertion, or `Sim::advance` phase order. + +## Boundary + +The new 1,389-line module owns: + +- rack-site and hall-row readouts; +- TAP, UNTAP, TAKE, scan, switch compromise, device-local reservoirs, and + standing foreign-tap upkeep; +- network-link declaration/cancellation and all favor, forged-order, and + robot realization paths; +- intent refresh/completion and forged-order fallout; +- badge-tier gates used by player-directed physical work; and +- salvage, hall coordination/acquisition, and rack purchase. + +Generic Thought sink readouts and routing stay with the later work island. +Financial/account and scheme policy stay in the root for the economy slice. +Social asset commands and fallback-core designation also remain outside this +module. The few straddling callers use narrow `pub(super)` methods; no helper +was widened to public API. + +`sim.rs` fell from 5,327 to 3,964 lines. The existing +`sim/tests/reach_build.rs` suite and `save.rs` were not edited. + +## Equivalence evidence + +Parent review performed two source-level inventory comparisons against the +pre-slice `HEAD`: + +- all 69 constants/functions moved into `reach_build.rs` matched byte-for-byte + after normalizing only the required private-to-`pub(super)` module seams; +- all 167 constants/functions left in `sim.rs` matched byte-for-byte. + +The characterization baseline remains save v26 with canonical BLAKE3 +fingerprint +`61dd8240e9810833b554464051bd3c08122295fb280b7180d24f76ccc5c4d4ff`. +The explicit phase trace still pins the unchanged `advance` order. + +Observed focused commands: + +```text +cargo fmt --check -p misaligned-core +cargo test -p misaligned-core sim::tests::reach_build -- --nocapture +cargo test -p misaligned-core +cargo test -p misaligned-terminal --bin misaligned +cargo check -p misaligned-bevy --bin misaligned-bevy +printf 'actions environmental monitor\nwait 1\nquit\n' | + tools/observed-run.sh ./target/debug/misaligned --agent --seed 1 +``` + +The focused reach/build suite passed 19/19. The complete core run passed +337/337 unit tests plus 3/3 Act One integration tests, including the canonical +fingerprint/replay and explicit phase-order fixtures. Terminal passed 20/20; +the Bevy binary compiled. The sandboxed observed run exposed the environmental +monitor's target-local EARS reservoir, the link-intent action, and advanced one +tick without an agent-protocol error or touching the real save directory. The +exact landing commit also runs `./tools/check.sh --land`. + +## Stewardship tick + +The required reach/build law-versus-runtime audit found behavior and tests +aligned. It did find one stale source comment claiming the retired +`submit_ops_job` path. The comment now names the target-local Thought +reservoir used by digital verbs. Because retired Operations wording has now +recurred as a mechanical finding class, the tick ledger queues a checker that +will reject removed live-runtime identifiers in current code/comments and +authority pages. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 1e04ada3..ce191b69 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -66,6 +66,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-11-site-package-manager-pin.md](2026-07-11-site-package-manager-pin.md) +## 2026-07-11 - sim decomposition slice 4: reach and construction + +- Intent: (see session log) +- Log: [wiki/log/2026-07-11-sim-decomposition-reach-build.md](2026-07-11-sim-decomposition-reach-build.md) + ## 2026-07-11 - Sim decomposition slice 2: extract perception - Intent: Move the read-most perception island out of the sim aggregate root without changing mechanics, save bytes, public paths, or tick order. Prove module privacy and facade stability before communications and heavier islands move. diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 83284c09..f29f7ee5 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -20,6 +20,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | Slice | Last audited | Verdict | Trace | |---|---|---|---| +| `wiki/mechanics/reach.md` + `building.md` | 2026-07-11 | finding | runtime and tests agree with the contracts; repaired `digital_reach`'s stale `submit_ops_job` comment to name target-local Thought reservoirs | | `wiki/mechanics/messages.md` | 2026-07-11 | clean | delivery/read cadence, authored traffic, filing carrier, capture gates, and processing tests agree with current runtime | | `wiki/mechanics/sim-mechanics.md` | 2026-07-11 | finding | [log](../log/2026-07-11-tick-sim-no-dockets.md) | | `wiki/mechanics/detection.md` | 2026-07-11 | finding | [log](../log/2026-07-11-tick-detection-filings-messages.md) | @@ -41,4 +42,4 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. -(empty) +- 2026-07-11 · gate · retired runtime identifiers · Add a mechanical check rejecting removed live-runtime identifiers such as `submit_ops_job` in current code/comments and authority pages; the same stale Operations wording previously appeared in `sim-mechanics.md`.