//! Messages: authored social traffic, player threads, and institutional //! filings (wiki/mechanics/messages.md). //! //! A message is the unit of the social graph's flow law: it has a carrier //! channel, typed payload, provenance endpoints, and delivery/read state. //! The sim owns timing and side effects; this module is pure data plus small //! formatting helpers so the same state can round-trip through saves and be //! rendered by every frontend. use crate::person::Leverage; /// Carrier channels and their read conditions (enforced in `Sim`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum MessageChannel { /// Email / ticket traffic; read when the recipient reaches their next /// scheduled work block (the desk-block abstraction for B1). Email, /// Phone traffic; read when the recipient is awake/on shift, any room. Phone, /// Face-to-face traffic; read when sender and recipient are co-located. InPerson, /// Institutional reports; read on the receiving observer's sampling /// cadence. Filing, /// Accounting/payroll/procurement traffic carried by finance systems. Financial, } impl MessageChannel { pub fn label(self) -> &'static str { match self { MessageChannel::Email => "email", MessageChannel::Phone => "phone", MessageChannel::InPerson => "in-person", MessageChannel::Filing => "filing", MessageChannel::Financial => "financial", } } /// Whether this channel can ride a device tap. In-person can still be /// overheard by room audio, but there is no carrying device to subscribe /// to directly. pub fn device_carried(self) -> bool { !matches!(self, MessageChannel::InPerson) } } /// A node on the message graph. #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum MessageEndpoint { Player, Person(u8), Observer(u8), External(String), } impl MessageEndpoint { pub fn person(&self) -> Option { match self { MessageEndpoint::Person(id) => Some(*id), _ => None, } } pub fn observer(&self) -> Option { match self { MessageEndpoint::Observer(id) => Some(*id), _ => None, } } pub fn label(&self) -> String { match self { MessageEndpoint::Player => "you".into(), MessageEndpoint::Person(id) => format!("person:{id}"), MessageEndpoint::Observer(id) => format!("observer:{id}"), MessageEndpoint::External(name) => name.clone(), } } } /// Typed payloads — intercepted traffic becomes intel by inspecting this /// enum, not by parsing prose. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum MessagePayload { /// A low-stakes relationship ping from the player persona. SocialPing { disposition_delta: i32 }, /// A return message generated by a recipient's response distribution. SocialReply { disposition_delta: i32 }, /// A schedule fact about a person. ScheduleFact { person: u8 }, /// Leverage payload: the traffic itself exposes a person's want. LeverageFact { person: u8, leverage: Leverage }, /// Credentials, access material, ticket context, or similar account data. AccountMaterial { label: String }, /// Account graph snapshot: IDs are interpreted by the economy module. FinancialFlow { label: String, accounts: Vec, flows: Vec, }, /// A filed report from an observer to an aggregate observer. SuspicionReport { observer: u8, suspicion: f32 }, /// A forged work order realizing a build intent (building.md): the /// false-source inject that an unwitting builder acts on when read. WorkOrder { intent_id: u64 }, /// Authored non-mechanical color that still rides a channel. Note { label: String }, } impl MessagePayload { /// The person this payload teaches about, if any. Used by the intel /// pipeline to attach processed knowledge to the right people card. pub fn subject_person(&self) -> Option { match self { MessagePayload::ScheduleFact { person } | MessagePayload::LeverageFact { person, .. } => Some(*person), MessagePayload::SuspicionReport { observer, .. } => Some(*observer), MessagePayload::SocialPing { .. } | MessagePayload::SocialReply { .. } | MessagePayload::AccountMaterial { .. } | MessagePayload::FinancialFlow { .. } | MessagePayload::WorkOrder { .. } | MessagePayload::Note { .. } => None, } } pub fn label(&self) -> String { match self { MessagePayload::SocialPing { .. } => "social ping".into(), MessagePayload::SocialReply { .. } => "reply".into(), MessagePayload::ScheduleFact { person } => { format!("schedule fact about person:{person}") } MessagePayload::LeverageFact { leverage, .. } => { format!("leverage: {}", leverage.label()) } MessagePayload::AccountMaterial { label } => format!("account material: {label}"), MessagePayload::FinancialFlow { label, .. } => format!("financial flow: {label}"), MessagePayload::SuspicionReport { observer, suspicion, } => format!("filing from observer:{observer} ({suspicion:.0})"), MessagePayload::WorkOrder { intent_id } => { format!("work order (intent {intent_id})") } MessagePayload::Note { label } => label.clone(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum MessageStatus { /// Sent by the author; not yet delivered to the recipient's channel. Sent, /// Arrived on the channel; waiting for the recipient's read condition. Delivered, /// The recipient read it and the payload's effects have landed. Read, } impl MessageStatus { pub fn label(self) -> &'static str { match self { MessageStatus::Sent => "sent", MessageStatus::Delivered => "delivered", MessageStatus::Read => "read", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum MessageOrigin { Player, AuthoredTraffic, Filing, Reply, } /// One persisted message. In-flight messages are those whose status is not /// `Read`; read messages remain as thread/history/provenance. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Message { pub id: u64, pub channel: MessageChannel, pub from: MessageEndpoint, pub to: MessageEndpoint, pub payload: MessagePayload, pub summary: String, pub sent_tick: u64, pub delivered_tick: Option, pub read_tick: Option, pub status: MessageStatus, pub origin: MessageOrigin, /// Whether the player captured this traffic into the raw intel buffer. pub captured: bool, /// Thread parent for replies. pub reply_to: Option, } impl Message { pub fn in_thread_with_person(&self, id: u8) -> bool { self.from.person() == Some(id) || self.to.person() == Some(id) } pub fn state_line(&self) -> String { let timing = match (self.delivered_tick, self.read_tick) { (_, Some(t)) => format!("read t{t}"), (Some(t), None) => format!("delivered t{t}"), (None, None) => format!("sent t{}", self.sent_tick), }; format!( "{} · {} · {}", self.channel.label(), self.status.label(), timing ) } } /// Scheduled message event. The schedule carries *when*; the message record /// carries *what*. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum MessageEvent { Deliver(u64), Read(u64), } /// Authored recurring traffic on a person. These are per-instance data — the /// system does not special-case Marcus, Dana, or any future hire. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TrafficPattern { pub id: u32, pub channel: MessageChannel, pub to: MessageEndpoint, pub hour: u32, pub payload: MessagePayload, pub summary: String, /// Once processed from a capture, the people card may show this pattern. #[serde(default)] pub learned: bool, /// Optional fixed reply delay for authored two-way traffic. #[serde(default)] pub response_delay: Option, } impl TrafficPattern { pub fn new( id: u32, channel: MessageChannel, to: MessageEndpoint, hour: u32, payload: MessagePayload, summary: impl Into, ) -> Self { Self { id, channel, to, hour, payload, summary: summary.into(), learned: false, response_delay: None, } } pub fn learned_line(&self) -> String { format!( "{} at {:02}:00 ({})", self.summary, self.hour, self.channel.label() ) } }