Skip to main content

edda_types/combat/
protocol.rs

1//! Messages exchanged with the combat service.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use super::{CombatCommand, CombatEvent, CombatState, CombatStatus};
8
9/// A command plus the metadata that makes it trackable. `command_id` is
10/// chosen by the client: the server tags the resulting events with it (so
11/// the client knows its prediction was confirmed) and ignores repeats (so
12/// resending after a dropped connection is safe).
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
15pub struct CommandEnvelope {
16    pub command_id: Uuid,
17    /// Free-text name of whoever issued it, for the log ("DM", "Travis").
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub actor: Option<String>,
20    pub command: CombatCommand,
21}
22
23impl CommandEnvelope {
24    /// An envelope for `command`, with a fresh id.
25    pub fn new(command: CombatCommand) -> Self {
26        Self {
27            command_id: Uuid::new_v4(),
28            actor: None,
29            command,
30        }
31    }
32
33    /// The same, saying who sends it (the server puts the device's name
34    /// there anyway).
35    #[must_use]
36    pub fn by(mut self, actor: impl Into<String>) -> Self {
37        self.actor = Some(actor.into());
38        self
39    }
40}
41
42/// One entry in a combat's permanent log.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
45pub struct LoggedEvent {
46    /// Position in the log, starting at 1 with no gaps.
47    pub seq: u64,
48    /// The command that caused it; `None` for the `Created` event.
49    pub command_id: Option<Uuid>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub actor: Option<String>,
52    pub at: DateTime<Utc>,
53    pub event: CombatEvent,
54}
55
56/// The current state of a combat as of log position `seq`.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
59pub struct CombatSnapshot {
60    pub id: Uuid,
61    pub seq: u64,
62    pub state: CombatState,
63}
64
65/// A combat in a list: `GET /combats`.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
68pub struct CombatSummary {
69    pub id: Uuid,
70    pub campaign: String,
71    pub name: String,
72    pub status: CombatStatus,
73    pub round: u32,
74    pub created_at: DateTime<Utc>,
75    /// When the last event happened.
76    pub updated_at: DateTime<Utc>,
77}
78
79/// Sent by the server over a combat's WebSocket.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
82#[serde(tag = "type", content = "data", rename_all = "snake_case")]
83pub enum ServerMessage {
84    /// Sent on connect, and again if this client fell too far behind.
85    Snapshot(CombatSnapshot),
86    /// Broadcast to everyone, in log order.
87    Event(LoggedEvent),
88    /// Sent only to the client whose command was refused.
89    Rejected { command_id: Uuid, error: String },
90    /// The command was already applied earlier (e.g. resent after a
91    /// reconnect); its events are already part of the state.
92    AlreadyApplied { command_id: Uuid },
93}
94
95/// Sent by a client over a combat's WebSocket.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
98#[serde(tag = "type", content = "data", rename_all = "snake_case")]
99pub enum ClientMessage {
100    Command(CommandEnvelope),
101}