@crowdedkingdoms/ckx-kit 0.2.0 → 0.3.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ //! Shard keys: one keyed hub per square of the world, so no one hub holds everyone.
2
+ //!
3
+ //! A shard is `size` by `size` chunks on the x and z axes, and its key is its centre chunk,
4
+ //! `"<x>,<z>"`. Players' clients compute the same keys (the kit clients carry this function),
5
+ //! so a client finds the hub for where it stands without asking anyone. A hub that sees its
6
+ //! neighbours' edges subscribes to [`around`]'s keys.
7
+
8
+ /// The shard's centre chunk for chunk `(x, z)`. `size` is at least 1.
9
+ pub fn centre(x: i64, z: i64, size: i64) -> (i64, i64) {
10
+ let size = size.max(1);
11
+ (x.div_euclid(size) * size + size / 2, z.div_euclid(size) * size + size / 2)
12
+ }
13
+
14
+ /// The key of the shard that holds chunk `(x, z)`.
15
+ pub fn key(x: i64, z: i64, size: i64) -> String {
16
+ let (cx, cz) = centre(x, z, size);
17
+ format!("{cx},{cz}")
18
+ }
19
+
20
+ /// The centre chunk a key names, or `None` when it is not `"<x>,<z>"`.
21
+ pub fn parse(key: &str) -> Option<(i64, i64)> {
22
+ let (x, z) = key.split_once(',')?;
23
+ Some((x.trim().parse().ok()?, z.trim().parse().ok()?))
24
+ }
25
+
26
+ /// The keys of the shards within `radius` shards of the one holding `(x, z)`, its own first,
27
+ /// then row by row.
28
+ pub fn around(x: i64, z: i64, size: i64, radius: i64) -> Vec<String> {
29
+ let size = size.max(1);
30
+ let (cx, cz) = centre(x, z, size);
31
+ let mut keys = vec![format!("{cx},{cz}")];
32
+ for dz in -radius..=radius {
33
+ for dx in -radius..=radius {
34
+ if (dx, dz) != (0, 0) {
35
+ keys.push(format!("{},{}", cx + dx * size, cz + dz * size));
36
+ }
37
+ }
38
+ }
39
+ keys
40
+ }
41
+
42
+ #[cfg(test)]
43
+ mod tests {
44
+ use super::*;
45
+
46
+ #[test]
47
+ fn a_chunk_falls_in_one_shard_named_by_its_centre() {
48
+ assert_eq!(key(10, -4, 8), "12,-4");
49
+ assert_eq!(key(8, -8, 8), "12,-4");
50
+ assert_eq!(key(7, -9, 8), "4,-12");
51
+ assert_eq!(key(-1, 0, 16), "-8,8");
52
+ assert_eq!(key(3, 5, 1), "3,5", "size 1 is the chunk itself");
53
+ assert_eq!(parse("12,-4"), Some((12, -4)));
54
+ assert_eq!(parse("x"), None);
55
+ }
56
+
57
+ #[test]
58
+ fn around_starts_with_its_own_shard() {
59
+ let keys = around(0, 0, 8, 1);
60
+ assert_eq!(keys.len(), 9);
61
+ assert_eq!(keys[0], "4,4");
62
+ assert!(keys.contains(&"-4,-4".to_string()) && keys.contains(&"12,12".to_string()));
63
+ assert_eq!(around(0, 0, 8, 0), vec!["4,4".to_string()]);
64
+ }
65
+ }
@@ -0,0 +1,266 @@
1
+ //! What kit hubs show players near them through the realtime tier ([`crate::emit`]): server
2
+ //! events, numbered by the registry below, and the state of server-driven actors.
3
+ //!
4
+ //! A server event's payload is `[event type, u16 little endian][JSON]`. The bodies here use
5
+ //! camelCase keys, the shape the kit clients' event parsers read. An app's own events take
6
+ //! numbers from [`APP_EVENTS`] up.
7
+ //!
8
+ //! A server-driven actor's state is [`ActorState`] as MessagePack: the kit clients decode it
9
+ //! into their actor stores like a player's.
10
+
11
+ use serde::{Deserialize, Serialize};
12
+
13
+ use crate::emit::{MAX_DISTANCE, MAX_PAYLOAD, Spatial};
14
+ use crate::node::ChunkPos;
15
+ use crate::{Ctx, Error, Result, encode};
16
+
17
+ /// A mob or a trap hit a player.
18
+ pub const CONTACT_DAMAGE: u16 = 77;
19
+ /// The weather or the time of day turned.
20
+ pub const WEATHER: u16 = 90;
21
+ /// A turn passed to the next player.
22
+ pub const TURN: u16 = 91;
23
+ /// Scores or a result.
24
+ pub const SCORE: u16 = 92;
25
+ /// A match was proposed to the players in it.
26
+ pub const PROPOSAL: u16 = 93;
27
+ /// An ability was cast, or landed.
28
+ pub const ABILITY: u16 = 94;
29
+ /// A movement the warden flagged.
30
+ pub const MOVEMENT_VIOLATION: u16 = 95;
31
+ /// A control point changed hands, or its capture moved.
32
+ pub const CONTROL_POINT: u16 = 96;
33
+ /// A racer started, passed a checkpoint, finished a lap or the race.
34
+ pub const RACE_TIMING: u16 = 97;
35
+ /// A play zone moved or shrank.
36
+ pub const ZONE_CHANGE: u16 = 98;
37
+ /// A wave began or ended, or a boss changed phase.
38
+ pub const ENCOUNTER: u16 = 99;
39
+ /// The contested object (a ball) changed hands, or a goal was scored.
40
+ pub const POSSESSION: u16 = 100;
41
+ /// An emote: a client event players send each other (the kit clients' convention); a hub may
42
+ /// send one too, for a server-driven actor.
43
+ pub const EMOTE: u16 = 101;
44
+ /// The first number an app's own events may use.
45
+ pub const APP_EVENTS: u16 = 1000;
46
+
47
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
48
+ #[serde(rename_all = "camelCase")]
49
+ pub struct ContactDamage {
50
+ pub target_uuid: String,
51
+ pub damage: i64,
52
+ pub mob_id: String,
53
+ pub mob_name: String,
54
+ }
55
+
56
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
57
+ #[serde(rename_all = "camelCase")]
58
+ pub struct WeatherChange {
59
+ pub weather: String,
60
+ pub since_ms: u64,
61
+ pub until_ms: u64,
62
+ pub night: bool,
63
+ pub time_of_day: f64,
64
+ }
65
+
66
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
67
+ #[serde(rename_all = "camelCase")]
68
+ pub struct TurnChange {
69
+ /// Whose turn it is now: a player id, or a unit's id.
70
+ pub actor_id: String,
71
+ pub round: u32,
72
+ pub turn_in_round: u32,
73
+ }
74
+
75
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
76
+ #[serde(rename_all = "camelCase")]
77
+ pub struct Standing {
78
+ pub actor_id: String,
79
+ pub score: i64,
80
+ pub rank: u32,
81
+ }
82
+
83
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
84
+ #[serde(rename_all = "camelCase")]
85
+ pub struct ScoreUpdate {
86
+ pub standings: Vec<Standing>,
87
+ pub winner_id: Option<String>,
88
+ }
89
+
90
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
91
+ #[serde(rename_all = "camelCase")]
92
+ pub struct ProposalMade {
93
+ pub proposal_id: String,
94
+ pub mode: String,
95
+ pub players: Vec<String>,
96
+ }
97
+
98
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
99
+ #[serde(rename_all = "camelCase")]
100
+ pub struct AbilityUse {
101
+ /// `cast` or `impact`.
102
+ pub kind: String,
103
+ pub ability_id: String,
104
+ pub caster_id: String,
105
+ pub victim_id: Option<String>,
106
+ pub damage: i64,
107
+ }
108
+
109
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
110
+ #[serde(rename_all = "camelCase")]
111
+ pub struct MovementViolation {
112
+ /// `speed`, `teleport` or `bounds`.
113
+ pub kind: String,
114
+ pub user_id: String,
115
+ pub detail: String,
116
+ }
117
+
118
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
119
+ #[serde(rename_all = "camelCase")]
120
+ pub struct ControlPointChange {
121
+ pub point_id: String,
122
+ pub owner: String,
123
+ pub previous_owner: String,
124
+ /// Capture progress toward the attacker, 0 to 1.
125
+ pub progress: f64,
126
+ }
127
+
128
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
129
+ #[serde(rename_all = "camelCase")]
130
+ pub struct RaceTiming {
131
+ /// `started`, `checkpoint`, `lap` or `finished`.
132
+ pub kind: String,
133
+ pub course_id: String,
134
+ pub user_id: String,
135
+ pub checkpoint: u32,
136
+ pub lap: u32,
137
+ pub elapsed_ms: u64,
138
+ }
139
+
140
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
141
+ #[serde(rename_all = "camelCase")]
142
+ pub struct ZoneChange {
143
+ /// `hold`, `shrink` or `closed`.
144
+ pub kind: String,
145
+ pub phase: Option<u32>,
146
+ pub radius_now: f64,
147
+ pub center_x: f64,
148
+ pub center_z: f64,
149
+ }
150
+
151
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
152
+ #[serde(rename_all = "camelCase")]
153
+ pub struct EncounterChange {
154
+ /// `wave_started`, `wave_cleared`, `boss_phase` or `finished`.
155
+ pub kind: String,
156
+ pub encounter_id: String,
157
+ pub wave: u32,
158
+ pub phase: u32,
159
+ }
160
+
161
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
162
+ #[serde(rename_all = "camelCase")]
163
+ pub struct PossessionChange {
164
+ /// `claimed`, `passed`, `loose` or `goal`.
165
+ pub kind: String,
166
+ pub holder_id: Option<String>,
167
+ pub team: Option<String>,
168
+ }
169
+
170
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
171
+ #[serde(rename_all = "camelCase")]
172
+ pub struct Emote {
173
+ /// The emote's name, as the game knows it: `wave`, `cheer`, ...
174
+ pub emote: String,
175
+ /// The actor it is aimed at, if any.
176
+ pub target_uuid: Option<String>,
177
+ }
178
+
179
+ /// A server event's payload: its type, then its body as JSON.
180
+ pub fn event_bytes<T: Serialize + ?Sized>(event_type: u16, body: &T) -> Result<Vec<u8>> {
181
+ let json = serde_json::to_vec(body).map_err(|e| Error::new(format!("an event body encodes as JSON: {e}")))?;
182
+ let mut out = Vec::with_capacity(2 + json.len());
183
+ out.extend_from_slice(&event_type.to_le_bytes());
184
+ out.extend_from_slice(&json);
185
+ if out.len() > MAX_PAYLOAD {
186
+ return Err(Error::new(format!("a realtime payload is at most {MAX_PAYLOAD} bytes")));
187
+ }
188
+ Ok(out)
189
+ }
190
+
191
+ /// Sends a server event to the players within `distance` chunks of `at` (at most
192
+ /// [`MAX_DISTANCE`]). `actor` is the event's actor: the one it is about, or the hub's own.
193
+ pub fn emit_event<T: Serialize + ?Sized>(ctx: &Ctx, at: ChunkPos, distance: u8, actor: &[u8; 32], event_type: u16, body: &T) -> Result<()> {
194
+ let bytes = event_bytes(event_type, body)?;
195
+ ctx.emit_spatial(at, distance.min(MAX_DISTANCE), actor, Spatial::Event { event_type, state: &bytes[2..] })?;
196
+ Ok(())
197
+ }
198
+
199
+ /// A server-driven actor as players near it see it.
200
+ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
201
+ pub struct ActorState {
202
+ /// What it is: `mob`, `npc`, `pet`, `ball`, `ghost`, a game's own word.
203
+ pub kind: String,
204
+ /// Its id within the hub that shows it.
205
+ pub id: u64,
206
+ /// Where it is, in chunks.
207
+ pub x: f64,
208
+ #[serde(default)]
209
+ pub y: f64,
210
+ pub z: f64,
211
+ /// Its health, when it has any.
212
+ pub hp: Option<i64>,
213
+ #[serde(default, skip_serializing_if = "Option::is_none")]
214
+ pub max_hp: Option<i64>,
215
+ /// What it is doing: `idle`, `chase`, `follow`, ...
216
+ #[serde(default, skip_serializing_if = "Option::is_none")]
217
+ pub mode: Option<String>,
218
+ /// The player it belongs to (a pet, a summon).
219
+ #[serde(default, skip_serializing_if = "Option::is_none")]
220
+ pub owner: Option<u64>,
221
+ /// Facing, in radians.
222
+ #[serde(default, skip_serializing_if = "Option::is_none")]
223
+ pub yaw: Option<f64>,
224
+ #[serde(default, skip_serializing_if = "Option::is_none")]
225
+ pub name: Option<String>,
226
+ }
227
+
228
+ impl ActorState {
229
+ pub fn new(kind: &str, id: u64, x: f64, z: f64) -> Self {
230
+ Self { kind: kind.to_owned(), id, x, y: 0.0, z, hp: None, max_hp: None, mode: None, owner: None, yaw: None, name: None }
231
+ }
232
+
233
+ /// The chunk it stands in.
234
+ pub fn chunk(&self) -> ChunkPos {
235
+ ChunkPos::new(self.x.floor() as i64, self.y.floor() as i64, self.z.floor() as i64)
236
+ }
237
+ }
238
+
239
+ /// Shows a server-driven actor to the players within `distance` chunks of where it stands.
240
+ pub fn emit_actor_state(ctx: &Ctx, actor: &[u8; 32], distance: u8, state: &ActorState) -> Result<()> {
241
+ ctx.emit_actor(actor, state.chunk(), distance.min(MAX_DISTANCE), &encode(state)?)?;
242
+ Ok(())
243
+ }
244
+
245
+ #[cfg(test)]
246
+ mod tests {
247
+ use super::*;
248
+
249
+ #[test]
250
+ fn an_event_is_its_type_then_json() {
251
+ let bytes = event_bytes(TURN, &TurnChange { actor_id: "7".into(), round: 2, turn_in_round: 1 }).unwrap();
252
+ assert_eq!(&bytes[..2], &[91, 0]);
253
+ let body: serde_json::Value = serde_json::from_slice(&bytes[2..]).unwrap();
254
+ assert_eq!(body, serde_json::json!({ "actorId": "7", "round": 2, "turnInRound": 1 }));
255
+ assert!(event_bytes(APP_EVENTS, &"x".repeat(MAX_PAYLOAD)).is_err());
256
+ }
257
+
258
+ #[test]
259
+ fn an_actor_state_leaves_out_what_it_does_not_have() {
260
+ let mut s = ActorState::new("mob", 3, 1.5, -2.25);
261
+ s.hp = Some(40);
262
+ let back: serde_json::Value = crate::decode(&encode(&s).unwrap()).unwrap();
263
+ assert_eq!(back, serde_json::json!({ "kind": "mob", "id": 3, "x": 1.5, "y": 0.0, "z": -2.25, "hp": 40 }));
264
+ assert_eq!(s.chunk(), ChunkPos::new(1, 0, -3));
265
+ }
266
+ }
@@ -44,10 +44,15 @@
44
44
  //! Nodes are unit tested natively with `cargo test`: `testing` runs one against a fake of the
45
45
  //! platform (calls, timers, events, snapshots, the node API, a mod's grid). [`limits`] has the
46
46
  //! platform's bounds.
47
+ //!
48
+ //! [`kit`] is the game kit's half here: the protocol kit hubs speak to the kit clients
49
+ //! (snapshots and deltas with sequence numbers, retry-safe actions, refusal codes), shard keys,
50
+ //! a seeded random generator, and the realtime event registry.
47
51
 
48
52
  mod abi;
49
53
  pub mod cron;
50
54
  pub mod emit;
55
+ pub mod kit;
51
56
  pub mod limits;
52
57
  pub mod model;
53
58
  pub mod node;
@@ -546,20 +551,25 @@ macro_rules! export_spoke {
546
551
  };
547
552
  }
548
553
 
554
+ // Only the module the sandbox loads needs the exports: a native build (a unit test, a crate
555
+ // that includes several nodes' sources) defines none, so nodes link side by side.
549
556
  #[doc(hidden)]
550
557
  #[macro_export]
551
558
  macro_rules! __export_node {
552
559
  ($node:ty) => {
560
+ #[cfg(target_arch = "wasm32")]
553
561
  #[unsafe(no_mangle)]
554
562
  pub extern "C" fn ckx_abi() -> u32 {
555
563
  $crate::rt::ABI_VERSION
556
564
  }
557
565
 
566
+ #[cfg(target_arch = "wasm32")]
558
567
  #[unsafe(no_mangle)]
559
568
  pub extern "C" fn ckx_alloc(len: u32) -> u32 {
560
569
  $crate::rt::alloc(len)
561
570
  }
562
571
 
572
+ #[cfg(target_arch = "wasm32")]
563
573
  #[unsafe(no_mangle)]
564
574
  pub extern "C" fn ckx_init(
565
575
  ctx_ptr: u32,
@@ -574,16 +584,19 @@ macro_rules! __export_node {
574
584
  $crate::rt::init::<$node>(ctx_ptr, ctx_len, seed_ptr, seed_len, snap_ptr, snap_len, snap_version, has_snap)
575
585
  }
576
586
 
587
+ #[cfg(target_arch = "wasm32")]
577
588
  #[unsafe(no_mangle)]
578
589
  pub extern "C" fn ckx_handle(caller_kind: u32, caller_id: u64, buf_ptr: u32, method_len: u32, payload_len: u32) -> u64 {
579
590
  $crate::rt::handle(caller_kind, caller_id, buf_ptr, method_len, payload_len)
580
591
  }
581
592
 
593
+ #[cfg(target_arch = "wasm32")]
582
594
  #[unsafe(no_mangle)]
583
595
  pub extern "C" fn ckx_persist() -> u64 {
584
596
  $crate::rt::persist()
585
597
  }
586
598
 
599
+ #[cfg(target_arch = "wasm32")]
587
600
  #[unsafe(no_mangle)]
588
601
  pub extern "C" fn ckx_evict() {
589
602
  $crate::rt::evict()
@@ -209,6 +209,11 @@ impl Model {
209
209
  self.containers.get(&id)
210
210
  }
211
211
 
212
+ /// Every container's id, oldest first.
213
+ pub fn ids(&self) -> impl Iterator<Item = u64> + '_ {
214
+ self.containers.keys().copied()
215
+ }
216
+
212
217
  /// Every container of `type_name`, oldest first.
213
218
  pub fn of_type<'a>(&'a self, type_name: &'a str) -> impl Iterator<Item = u64> + 'a {
214
219
  self.containers.iter().filter(move |(_, c)| c.type_name == type_name).map(|(id, _)| *id)
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "crowdy-client-sdk"
3
- version = "0.1.0"
3
+ version = "0.1.0-dev.1"
4
4
  edition = "2021"
5
5
  description = "Guest SDK for the CLIENT half of a ck-exec mod: browser WASM (ck ABI v0 + the client host calls)."
6
6
  license = "MIT OR Apache-2.0"
package/kit.pin CHANGED
@@ -1,9 +1,9 @@
1
1
  # The platform toolchain release this kit was copied from (sync-kit.sh in the platform's
2
2
  # repository). The paths in `copied` are byte-identical to it: change them there, never here.
3
- toolchain_version=v1.10.1
4
- toolchain_commit=277975dcb57b2fc770e67933cd47b0f911bf6b7d
5
- sdk_commit=4aa2af5aa86c3b1d90ed7eb10f65a0fcd14a1387
6
- ckx_sdk_version=0.9.0
3
+ toolchain_version=v1.12.0
4
+ toolchain_commit=41921e53e1bbd4901fb8c4946e4277aa1e93bfec
5
+ sdk_commit=60e91d4892727b847531e657cc9c4ba1edd2e2c1
6
+ ckx_sdk_version=0.10.0
7
7
  crowdy_client_sdk_version=0.1.0
8
8
  rust_version=1.97.1
9
9
  copied=crates/ckx-sdk/src crates/crowdy-client-sdk/src tools/instrument/Cargo.toml tools/instrument/Cargo.lock tools/instrument/src checks/src/build/exec-source.ts checks/src/build/exec-source.spec.ts checks/src/build/rust-source-guard.ts checks/src/build/rust-source-guard.spec.ts checks/src/build/wasm-inspect.ts checks/src/build/wasm-inspect.spec.ts checks/src/build/client-capabilities.ts checks/src/build/client-capabilities.spec.ts checks/src/test-support/wasm-module.ts checks/compute-toolchain/host-catalog.json starters rust-toolchain.toml vendor.lock
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/ckx-kit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-dev.1",
4
4
  "description": "Build, check and test Crowded Kingdoms ck-exec mods, hubs and spokes on your machine, as the platform builds them",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "homepage": "https://docs.crowdedkingdoms.com/exec/intro",
@@ -9,5 +9,5 @@ publish = false
9
9
  crate-type = ["cdylib"]
10
10
 
11
11
  [dependencies]
12
- ckx-sdk = "0.9.0"
12
+ ckx-sdk = "0.10.0"
13
13
  serde = { version = "1", features = ["derive"] }
@@ -9,5 +9,5 @@ publish = false
9
9
  crate-type = ["cdylib"]
10
10
 
11
11
  [dependencies]
12
- ckx-sdk = "0.9.0"
12
+ ckx-sdk = "0.10.0"
13
13
  serde = { version = "1", features = ["derive"] }