Skip to main content

flatland_protocol/
types.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4pub type EntityId = u64;
5pub type Tick = u64;
6pub type Seq = u32;
7pub type SessionId = u64;
8
9/// World position `(x, y, z, w, t)` — `t` reserved at 0.
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct WorldCoord {
12    pub x: f32,
13    pub y: f32,
14    pub z: f32,
15    pub w: u32,
16    pub t: u32,
17}
18
19impl WorldCoord {
20    pub fn surface(x: f32, y: f32) -> Self {
21        Self {
22            x,
23            y,
24            z: 0.0,
25            w: 0,
26            t: 0,
27        }
28    }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
32pub struct Velocity2D {
33    pub vx: f32,
34    pub vy: f32,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
38pub struct Transform {
39    pub position: WorldCoord,
40    pub yaw: f32,
41    pub velocity: Velocity2D,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum LifeState {
47    Alive,
48    Dead,
49}
50
51impl Default for LifeState {
52    fn default() -> Self {
53        Self::Alive
54    }
55}
56
57/// Player health / resources (players only; omitted on other entities).
58#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
59pub struct PlayerVitals {
60    pub health: f32,
61    pub health_max: f32,
62    pub mana: f32,
63    pub mana_max: f32,
64    pub stamina: f32,
65    pub stamina_max: f32,
66    #[serde(default)]
67    pub coins: u32,
68    #[serde(default)]
69    pub deaths: u32,
70    #[serde(default)]
71    pub life_state: LifeState,
72}
73
74impl Default for PlayerVitals {
75    fn default() -> Self {
76        Self {
77            health: 100.0,
78            health_max: 100.0,
79            mana: 50.0,
80            mana_max: 50.0,
81            stamina: 100.0,
82            stamina_max: 100.0,
83            coins: 0,
84            deaths: 0,
85            life_state: LifeState::Alive,
86        }
87    }
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct EntityState {
92    pub id: EntityId,
93    pub transform: Transform,
94    /// Character / display name (first letter used as map glyph for other players).
95    #[serde(default)]
96    pub label: String,
97    #[serde(default)]
98    pub vitals: Option<PlayerVitals>,
99    /// When inside a building interior (players only).
100    #[serde(default)]
101    pub inside_building: Option<String>,
102}
103
104/// Nearby voice chat vs magical long-range (requires whisper-stone item).
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum ChatChannel {
108    Nearby,
109    WhisperStone,
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct ChatMessage {
114    pub channel: ChatChannel,
115    pub from_entity: EntityId,
116    pub from_name: String,
117    pub text: String,
118    pub tick: Tick,
119}
120
121/// Client → server gameplay input (reliable, sequenced per entity).
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub enum Intent {
124    Move {
125        entity_id: EntityId,
126        forward: f32,
127        strafe: f32,
128        seq: Seq,
129    },
130    Stop {
131        entity_id: EntityId,
132        seq: Seq,
133    },
134    Harvest {
135        entity_id: EntityId,
136        node_id: String,
137        seq: Seq,
138    },
139    Use {
140        entity_id: EntityId,
141        item_instance_id: Uuid,
142        seq: Seq,
143    },
144    Say {
145        entity_id: EntityId,
146        channel: ChatChannel,
147        text: String,
148        seq: Seq,
149    },
150    /// Start a blueprint craft (timed, like harvest).
151    Craft {
152        entity_id: EntityId,
153        blueprint_id: String,
154        seq: Seq,
155    },
156    /// Door, NPC, enter/exit building.
157    Interact {
158        entity_id: EntityId,
159        target_id: String,
160        seq: Seq,
161    },
162    /// Dev / test: apply damage to self (co-op testing).
163    TestDamage {
164        entity_id: EntityId,
165        amount: f32,
166        seq: Seq,
167    },
168}
169
170/// Server → client AOI-filtered entity updates for one sim tick.
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub struct TickDelta {
173    pub tick: Tick,
174    pub entities: Vec<EntityState>,
175    #[serde(default)]
176    pub resource_nodes: Vec<ResourceNodeView>,
177    #[serde(default)]
178    pub buildings: Vec<BuildingView>,
179    #[serde(default)]
180    pub doors: Vec<DoorView>,
181    #[serde(default)]
182    pub npcs: Vec<NpcView>,
183    /// Observer inventory stacks (template → qty).
184    #[serde(default)]
185    pub inventory: Vec<ItemStack>,
186    #[serde(default)]
187    pub blueprints: Vec<BlueprintView>,
188}
189
190/// Full state on region enter or reconnect.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct Snapshot {
193    pub tick: Tick,
194    pub chunk_rev: u64,
195    pub entities: Vec<EntityState>,
196    #[serde(default)]
197    pub resource_nodes: Vec<ResourceNodeView>,
198    /// Segment play area width in meters (for HUD).
199    #[serde(default)]
200    pub world_width_m: f32,
201    #[serde(default)]
202    pub world_height_m: f32,
203    #[serde(default)]
204    pub buildings: Vec<BuildingView>,
205    #[serde(default)]
206    pub doors: Vec<DoorView>,
207    #[serde(default)]
208    pub npcs: Vec<NpcView>,
209    #[serde(default)]
210    pub inventory: Vec<ItemStack>,
211    #[serde(default)]
212    pub blueprints: Vec<BlueprintView>,
213}
214
215/// Harvestable node visible to clients.
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct ResourceNodeView {
218    pub id: String,
219    pub label: String,
220    pub x: f32,
221    pub y: f32,
222    pub z: f32,
223    pub item_template: String,
224    #[serde(default = "default_node_state")]
225    pub state: ResourceNodeState,
226    /// When true, players cannot walk through this node while available/harvesting.
227    #[serde(default)]
228    pub blocking: bool,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "snake_case")]
233pub enum ResourceNodeState {
234    Available,
235    Harvesting,
236    Cooldown,
237}
238
239fn default_node_state() -> ResourceNodeState {
240    ResourceNodeState::Available
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct ItemStack {
245    pub template_id: String,
246    pub quantity: u32,
247}
248
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct BlueprintIngredientView {
251    pub template_id: String,
252    pub quantity: u32,
253    /// Always serialize — `true` is not bool::default() so postcard keeps it; explicit for clarity.
254    pub consumed: bool,
255}
256
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub struct ToolRequirementView {
259    pub item: String,
260    /// Always serialize — postcard omits `false` by default, which breaks roundtrip without explicit value.
261    pub consumed: bool,
262}
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub struct SkillRequirementView {
266    pub skill: String,
267    pub level: u32,
268}
269
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct BlueprintView {
272    pub id: String,
273    pub label: String,
274    pub output: String,
275    pub output_qty: u32,
276    pub craft_ticks: u32,
277    pub inputs: Vec<BlueprintIngredientView>,
278    /// Postcard always serializes optional fields (no `skip_serializing_if`) so decode stays aligned.
279    #[serde(default)]
280    pub station: Option<String>,
281    #[serde(default)]
282    pub category: Option<String>,
283    #[serde(default)]
284    pub required_tools: Vec<ToolRequirementView>,
285    #[serde(default)]
286    pub skill: Option<SkillRequirementView>,
287    #[serde(default)]
288    pub failure_chance: f32,
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct BuildingView {
293    pub id: String,
294    pub label: String,
295    pub x: f32,
296    pub y: f32,
297    pub width_m: f32,
298    pub depth_m: f32,
299    /// Interior map origin (for wall rendering when player is inside).
300    #[serde(default)]
301    pub interior_origin_x: f32,
302    #[serde(default)]
303    pub interior_origin_y: f32,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct DoorView {
308    pub id: String,
309    pub building_id: String,
310    pub x: f32,
311    pub y: f32,
312    pub open: bool,
313    #[serde(default)]
314    pub is_exit: bool,
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318pub struct NpcView {
319    pub id: String,
320    pub label: String,
321    pub role: String,
322    pub x: f32,
323    pub y: f32,
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub building_id: Option<String>,
326}
327
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
329pub struct CraftResult {
330    pub blueprint_id: String,
331    pub outputs: Vec<ItemStack>,
332    pub consumed: Vec<ItemStack>,
333}
334
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct DeathNotice {
337    pub entity_id: EntityId,
338    pub respawn_x: f32,
339    pub respawn_y: f32,
340    pub message: String,
341}
342
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub struct InteractionNotice {
345    pub target_id: String,
346    pub message: String,
347    #[serde(default)]
348    pub coins_delta: i32,
349    #[serde(default)]
350    pub inventory_delta: Vec<ItemStack>,
351}
352
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
354pub struct HarvestResult {
355    pub node_id: String,
356    /// Stack quantity granted (not one-node-one-instance).
357    pub quantity: u32,
358    pub item_template: String,
359    /// Optional DB row id when persisted to control plane.
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub item_instance_id: Option<Uuid>,
362}
363
364/// Every on-wire payload is wrapped for versioning and codec uniformity.
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
366pub struct Envelope<T> {
367    pub protocol_version: u16,
368    pub payload: T,
369}
370
371impl<T> Envelope<T> {
372    pub fn new(payload: T) -> Self {
373        Self {
374            protocol_version: crate::PROTOCOL_VERSION,
375            payload,
376        }
377    }
378}
379
380/// Session handshake after transport connect.
381#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382pub struct Hello {
383    pub client_name: String,
384    pub protocol_version: u16,
385    #[serde(default)]
386    pub auth: AuthCredential,
387    /// Required for session auth; embedded in `ApiToken` variant otherwise.
388    #[serde(default)]
389    pub character_id: Option<Uuid>,
390}
391
392/// How the client authenticates to the game gateway.
393/// Uses default serde enum encoding (postcard-compatible; not internally tagged).
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395#[serde(rename_all = "snake_case")]
396pub enum AuthCredential {
397    DevLocal,
398    Session { token: String },
399    ApiToken { token: String, character_id: Uuid },
400}
401
402impl Default for AuthCredential {
403    fn default() -> Self {
404        Self::DevLocal
405    }
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct Welcome {
410    pub session_id: SessionId,
411    pub entity_id: EntityId,
412    pub snapshot: Snapshot,
413}
414
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416pub enum ServerMessage {
417    Welcome(Welcome),
418    Tick(TickDelta),
419    IntentAck {
420        entity_id: EntityId,
421        seq: Seq,
422        tick: Tick,
423    },
424    Chat(ChatMessage),
425    HarvestResult(HarvestResult),
426    CraftResult(CraftResult),
427    Death(DeathNotice),
428    Interaction(InteractionNotice),
429}
430
431#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432pub enum ClientMessage {
433    Hello(Hello),
434    Intent(Intent),
435    Disconnect,
436}