@crowdedkingdoms/ckx-kit 0.0.0-stage → 0.1.1-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.
- package/LICENSE-APACHE +176 -0
- package/LICENSE-MIT +21 -0
- package/README.md +76 -2
- package/checks/compute-toolchain/host-catalog.json +68 -0
- package/crates/ckx-sdk/Cargo.toml +17 -0
- package/crates/ckx-sdk/LICENSE-APACHE +176 -0
- package/crates/ckx-sdk/LICENSE-MIT +21 -0
- package/crates/ckx-sdk/README.md +47 -0
- package/crates/ckx-sdk/src/abi.rs +115 -0
- package/crates/ckx-sdk/src/cron.rs +717 -0
- package/crates/ckx-sdk/src/emit.rs +140 -0
- package/crates/ckx-sdk/src/lib.rs +588 -0
- package/crates/ckx-sdk/src/model.rs +482 -0
- package/crates/ckx-sdk/src/node.rs +942 -0
- package/crates/ckx-sdk/src/rt.rs +519 -0
- package/crates/crowdy-client-sdk/Cargo.toml +15 -0
- package/crates/crowdy-client-sdk/LICENSE-APACHE +176 -0
- package/crates/crowdy-client-sdk/LICENSE-MIT +21 -0
- package/crates/crowdy-client-sdk/README.md +25 -0
- package/crates/crowdy-client-sdk/src/lib.rs +698 -0
- package/dist/checks/src/build/client-capabilities.js +123 -0
- package/dist/checks/src/build/exec-source.js +343 -0
- package/dist/checks/src/build/rust-source-guard.js +165 -0
- package/dist/checks/src/build/wasm-inspect.js +249 -0
- package/dist/cli/src/build.js +99 -0
- package/dist/cli/src/crate.js +64 -0
- package/dist/cli/src/doctor.js +51 -0
- package/dist/cli/src/kit.js +66 -0
- package/dist/cli/src/main.js +109 -0
- package/dist/cli/src/new.js +74 -0
- package/dist/cli/src/tools.js +104 -0
- package/kit.pin +9 -0
- package/package.json +81 -4
- package/rust-toolchain.toml +6 -0
- package/starters/ckx.json +9 -0
- package/starters/grid-mod/Cargo.toml +13 -0
- package/starters/grid-mod/src/lib.rs +82 -0
- package/starters/matchmaker/Cargo.toml +13 -0
- package/starters/matchmaker/src/lib.rs +362 -0
- package/starters/npc-mobs/Cargo.toml +14 -0
- package/starters/npc-mobs/src/lib.rs +469 -0
- package/starters/session/Cargo.toml +13 -0
- package/starters/session/src/lib.rs +395 -0
- package/starters/world-tick/Cargo.toml +14 -0
- package/starters/world-tick/src/lib.rs +346 -0
- package/templates/client/Cargo.toml +16 -0
- package/templates/client/src/lib.rs +31 -0
- package/templates/hub/Cargo.toml +11 -0
- package/templates/hub/src/lib.rs +73 -0
- package/templates/spoke/Cargo.toml +11 -0
- package/templates/spoke/src/lib.rs +53 -0
- package/tools/instrument/Cargo.lock +140 -0
- package/tools/instrument/Cargo.toml +11 -0
- package/tools/instrument/src/main.rs +165 -0
- package/vendor.lock +167 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
//! Write ck-exec hubs and spokes.
|
|
2
|
+
//!
|
|
3
|
+
//! A **hub** owns state and runs one handler at a time; a **spoke** holds no authoritative
|
|
4
|
+
//! state and changes hubs only by calling their endpoints. Both are plain Rust types built for
|
|
5
|
+
//! `wasm32-unknown-unknown` and exported with [`export_hub!`] or [`export_spoke!`].
|
|
6
|
+
//!
|
|
7
|
+
//! ```ignore
|
|
8
|
+
//! use ckx_sdk::prelude::*;
|
|
9
|
+
//!
|
|
10
|
+
//! #[derive(Default, Serialize, Deserialize)]
|
|
11
|
+
//! struct Counter { total: i64 }
|
|
12
|
+
//!
|
|
13
|
+
//! impl Hub for Counter {
|
|
14
|
+
//! fn spawn(_ctx: &Ctx, _seed: &[u8]) -> Result<Self> { Ok(Self::default()) }
|
|
15
|
+
//! fn load(_ctx: &Ctx, snapshot: &[u8], _from_version: u64) -> Result<Self> { decode(snapshot) }
|
|
16
|
+
//! fn persist(&mut self, _ctx: &Ctx) -> Result<Vec<u8>> { encode(self) }
|
|
17
|
+
//! fn handle(&mut self, _ctx: &Ctx, call: Call<'_>) -> Result<Vec<u8>> {
|
|
18
|
+
//! match call.method {
|
|
19
|
+
//! "add" => { self.total += call.decode::<i64>()?; encode(&self.total) }
|
|
20
|
+
//! other => Err(Error::unknown_method(other)),
|
|
21
|
+
//! }
|
|
22
|
+
//! }
|
|
23
|
+
//! }
|
|
24
|
+
//!
|
|
25
|
+
//! ckx_sdk::export_hub!(Counter);
|
|
26
|
+
//! ```
|
|
27
|
+
//!
|
|
28
|
+
//! Payloads are bytes. [`encode`] and [`decode`] use MessagePack with named fields, which
|
|
29
|
+
//! game clients decode into plain objects.
|
|
30
|
+
//!
|
|
31
|
+
//! Realtime events players around see at once go through [`Ctx::emit_spatial`],
|
|
32
|
+
//! [`Ctx::emit_actor`] and [`Ctx::emit_channel`]; see [`emit`].
|
|
33
|
+
//!
|
|
34
|
+
//! Platform data (world, grids, players, grid permissions, players' access to the app) is behind
|
|
35
|
+
//! [`Ctx::world`], [`Ctx::grids`], [`Ctx::players`], [`Ctx::permissions`] and [`Ctx::access`],
|
|
36
|
+
//! each needing a scope the node type declares in the manifest; see [`node`].
|
|
37
|
+
//!
|
|
38
|
+
//! Hubs also get platform events, each a default no-op on [`Hub`]: timers they set
|
|
39
|
+
//! ([`Hub::on_timer`]), topics they watch ([`Hub::on_topic`]), players entering and leaving
|
|
40
|
+
//! the app ([`Hub::on_presence`], root hub only) and players' connections subscribing to them
|
|
41
|
+
//! ([`Hub::on_session`]). A timer can follow a cron schedule, the legacy automations' syntax:
|
|
42
|
+
//! see [`cron`] and [`Ctx::timer_cron`].
|
|
43
|
+
|
|
44
|
+
mod abi;
|
|
45
|
+
pub mod cron;
|
|
46
|
+
pub mod emit;
|
|
47
|
+
pub mod model;
|
|
48
|
+
pub mod node;
|
|
49
|
+
#[doc(hidden)]
|
|
50
|
+
pub mod rt;
|
|
51
|
+
|
|
52
|
+
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
|
53
|
+
|
|
54
|
+
pub mod prelude {
|
|
55
|
+
pub use crate::cron::Cron;
|
|
56
|
+
pub use crate::emit::{Spatial, actor_id};
|
|
57
|
+
pub use crate::model::{Model, PropKind, Schema, Viewer, Visibility};
|
|
58
|
+
pub use crate::node::{ChunkPos, GridPick, VoxelWrite};
|
|
59
|
+
pub use crate::{
|
|
60
|
+
Call, CallError, Caller, Ctx, Error, Hub, Kind, Level, Presence, PresencePlayer, Result, Session, Spoke, Status, TopicMsg,
|
|
61
|
+
WorldEvent, decode, encode,
|
|
62
|
+
};
|
|
63
|
+
pub use serde::{Deserialize, Serialize};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// An error a handler returns. The caller receives its message with status `AppError`.
|
|
67
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
68
|
+
pub struct Error(pub String);
|
|
69
|
+
|
|
70
|
+
impl Error {
|
|
71
|
+
pub fn new(msg: impl Into<String>) -> Self {
|
|
72
|
+
Self(msg.into())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn unknown_method(method: &str) -> Self {
|
|
76
|
+
Self(format!("unknown method `{method}`"))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl std::fmt::Display for Error {
|
|
81
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
82
|
+
f.write_str(&self.0)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
impl From<CallError> for Error {
|
|
87
|
+
fn from(e: CallError) -> Self {
|
|
88
|
+
Self(format!("{:?}: {}", e.status, e.message))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl From<String> for Error {
|
|
93
|
+
fn from(s: String) -> Self {
|
|
94
|
+
Self(s)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl From<&str> for Error {
|
|
99
|
+
fn from(s: &str) -> Self {
|
|
100
|
+
Self(s.to_owned())
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
105
|
+
|
|
106
|
+
/// Encodes a value as MessagePack with named fields.
|
|
107
|
+
pub fn encode<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>> {
|
|
108
|
+
rmp_serde::to_vec_named(value).map_err(|e| Error(format!("encode: {e}")))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Decodes MessagePack.
|
|
112
|
+
pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
|
|
113
|
+
rmp_serde::from_slice(bytes).map_err(|e| Error(format!("decode: {e}")))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
117
|
+
pub enum Kind {
|
|
118
|
+
Hub,
|
|
119
|
+
Spoke,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Who is calling. The platform sets this; a handler can trust it for authorization.
|
|
123
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
124
|
+
pub enum Caller {
|
|
125
|
+
Platform,
|
|
126
|
+
Instance(u64),
|
|
127
|
+
Player(u64),
|
|
128
|
+
/// A developer of the app (the org's `manage_compute`), by user id, connected with
|
|
129
|
+
/// `execConnectAsDeveloper`: studio tools, manual runs and admin endpoints. It may call any
|
|
130
|
+
/// type, not only `client` ones, so check for it in handlers only developers should reach.
|
|
131
|
+
Developer(u64),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// One incoming call.
|
|
135
|
+
#[derive(Debug, Clone, Copy)]
|
|
136
|
+
pub struct Call<'a> {
|
|
137
|
+
pub caller: Caller,
|
|
138
|
+
pub method: &'a str,
|
|
139
|
+
pub payload: &'a [u8],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl Call<'_> {
|
|
143
|
+
pub fn decode<T: DeserializeOwned>(&self) -> Result<T> {
|
|
144
|
+
decode(self.payload)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// The calling player, or an error when the caller is not a player.
|
|
148
|
+
pub fn player(&self) -> Result<u64> {
|
|
149
|
+
match self.caller {
|
|
150
|
+
Caller::Player(p) => Ok(p),
|
|
151
|
+
other => Err(Error(format!("`{}` is for players, not {other:?}", self.method))),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The calling developer, or an error when the caller is not one: the guard for an admin
|
|
156
|
+
/// endpoint.
|
|
157
|
+
pub fn developer(&self) -> Result<u64> {
|
|
158
|
+
match self.caller {
|
|
159
|
+
Caller::Developer(u) => Ok(u),
|
|
160
|
+
other => Err(Error(format!("`{}` is for the app's developers, not {other:?}", self.method))),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// A message published on a topic this hub watches ([`Ctx::subscribe`]).
|
|
166
|
+
#[derive(Debug, Clone, Copy)]
|
|
167
|
+
pub struct TopicMsg<'a> {
|
|
168
|
+
/// The publishing instance's type and key.
|
|
169
|
+
pub node_type: &'a str,
|
|
170
|
+
pub key: &'a str,
|
|
171
|
+
pub topic: &'a str,
|
|
172
|
+
pub payload: &'a [u8],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
impl TopicMsg<'_> {
|
|
176
|
+
pub fn decode<T: DeserializeOwned>(&self) -> Result<T> {
|
|
177
|
+
decode(self.payload)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Players who entered or left the app since the last event, or a new count, delivered to the
|
|
182
|
+
/// root hub. Events can be lost when the root hub moves; `count` is always current.
|
|
183
|
+
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
184
|
+
pub struct Presence {
|
|
185
|
+
/// Players in the app now.
|
|
186
|
+
pub count: u32,
|
|
187
|
+
#[serde(default)]
|
|
188
|
+
pub joined: Vec<PresencePlayer>,
|
|
189
|
+
#[serde(default)]
|
|
190
|
+
pub left: Vec<PresencePlayer>,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
194
|
+
pub struct PresencePlayer {
|
|
195
|
+
pub player: u64,
|
|
196
|
+
/// The player's avatar in the world, when they have one.
|
|
197
|
+
#[serde(default)]
|
|
198
|
+
pub actor: Option<String>,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Something that happened in a mod's grid, delivered to mods only. Events come in batches
|
|
202
|
+
/// and can be lost; each names the chunk it happened in.
|
|
203
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
204
|
+
pub struct WorldEvent {
|
|
205
|
+
pub grid: u64,
|
|
206
|
+
/// [`WorldEvent::ACTORS`] or [`WorldEvent::VOXELS`].
|
|
207
|
+
pub kind: String,
|
|
208
|
+
pub chunk: [i64; 3],
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl WorldEvent {
|
|
212
|
+
/// Actors moved into or out of the chunk.
|
|
213
|
+
pub const ACTORS: &'static str = "actors";
|
|
214
|
+
/// Voxels in the chunk changed.
|
|
215
|
+
pub const VOXELS: &'static str = "voxels";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// A player's connection subscribing to a hub's first topic, or leaving its last. A player
|
|
219
|
+
/// connected twice joins twice.
|
|
220
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
221
|
+
pub enum Session {
|
|
222
|
+
Joined,
|
|
223
|
+
Left,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// The platform's status for a call this instance made. Mirrors the wire status codes.
|
|
227
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
228
|
+
pub enum Status {
|
|
229
|
+
Ok,
|
|
230
|
+
AppError,
|
|
231
|
+
Busy,
|
|
232
|
+
Moved,
|
|
233
|
+
NotFound,
|
|
234
|
+
DeadlineExceeded,
|
|
235
|
+
Denied,
|
|
236
|
+
RateLimited,
|
|
237
|
+
Unavailable,
|
|
238
|
+
Internal,
|
|
239
|
+
Trapped,
|
|
240
|
+
BadRequest,
|
|
241
|
+
Unknown(u8),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
impl Status {
|
|
245
|
+
pub fn from_u8(v: u8) -> Self {
|
|
246
|
+
match v {
|
|
247
|
+
0 => Status::Ok,
|
|
248
|
+
1 => Status::AppError,
|
|
249
|
+
2 => Status::Busy,
|
|
250
|
+
3 => Status::Moved,
|
|
251
|
+
4 => Status::NotFound,
|
|
252
|
+
5 => Status::DeadlineExceeded,
|
|
253
|
+
6 => Status::Denied,
|
|
254
|
+
7 => Status::RateLimited,
|
|
255
|
+
8 => Status::Unavailable,
|
|
256
|
+
9 => Status::Internal,
|
|
257
|
+
10 => Status::Trapped,
|
|
258
|
+
11 => Status::BadRequest,
|
|
259
|
+
other => Status::Unknown(other),
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
pub fn is_retryable(self) -> bool {
|
|
264
|
+
matches!(self, Status::Busy | Status::Moved | Status::Unavailable | Status::RateLimited)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// A failed call made by this instance.
|
|
269
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
270
|
+
pub struct CallError {
|
|
271
|
+
pub status: Status,
|
|
272
|
+
pub message: String,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
impl std::fmt::Display for CallError {
|
|
276
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
277
|
+
write!(f, "{:?}: {}", self.status, self.message)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
282
|
+
pub enum Level {
|
|
283
|
+
Error = 0,
|
|
284
|
+
Warn = 1,
|
|
285
|
+
Info = 2,
|
|
286
|
+
Debug = 3,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// This instance's identity and its way out: calls, messages, publishing and the node API.
|
|
290
|
+
#[derive(Debug, Clone)]
|
|
291
|
+
pub struct Ctx {
|
|
292
|
+
pub app: u64,
|
|
293
|
+
pub instance: u64,
|
|
294
|
+
pub epoch: u64,
|
|
295
|
+
/// The deployed code version.
|
|
296
|
+
pub version: u64,
|
|
297
|
+
pub kind: Kind,
|
|
298
|
+
pub node_type: String,
|
|
299
|
+
pub key: String,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// Default deadline for calls made without one.
|
|
303
|
+
pub const DEFAULT_DEADLINE_MS: u32 = 2_000;
|
|
304
|
+
|
|
305
|
+
fn request(node_type: &str, key: &str, method: &str, payload: &[u8]) -> Vec<u8> {
|
|
306
|
+
let (t, k, m) = (
|
|
307
|
+
&node_type.as_bytes()[..node_type.len().min(255)],
|
|
308
|
+
&key.as_bytes()[..key.len().min(65535)],
|
|
309
|
+
&method.as_bytes()[..method.len().min(255)],
|
|
310
|
+
);
|
|
311
|
+
let mut req = Vec::with_capacity(4 + t.len() + k.len() + m.len() + payload.len());
|
|
312
|
+
req.push(t.len() as u8);
|
|
313
|
+
req.extend_from_slice(t);
|
|
314
|
+
req.extend_from_slice(&(k.len() as u16).to_le_bytes());
|
|
315
|
+
req.extend_from_slice(k);
|
|
316
|
+
req.push(m.len() as u8);
|
|
317
|
+
req.extend_from_slice(m);
|
|
318
|
+
req.extend_from_slice(payload);
|
|
319
|
+
req
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
impl Ctx {
|
|
323
|
+
/// Calls an endpoint on another instance of this app and waits for its reply. This
|
|
324
|
+
/// instance handles nothing else until the reply arrives or the deadline passes.
|
|
325
|
+
pub fn call(&self, node_type: &str, key: &str, method: &str, payload: &[u8]) -> Result<Vec<u8>, CallError> {
|
|
326
|
+
self.call_with_deadline(node_type, key, method, payload, DEFAULT_DEADLINE_MS)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
pub fn call_with_deadline(
|
|
330
|
+
&self,
|
|
331
|
+
node_type: &str,
|
|
332
|
+
key: &str,
|
|
333
|
+
method: &str,
|
|
334
|
+
payload: &[u8],
|
|
335
|
+
deadline_ms: u32,
|
|
336
|
+
) -> Result<Vec<u8>, CallError> {
|
|
337
|
+
rt::import_result(abi::call(&request(node_type, key, method, payload), deadline_ms))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// [`Ctx::call`] with MessagePack encoding on both sides.
|
|
341
|
+
pub fn call_typed<Req: Serialize + ?Sized, Resp: DeserializeOwned>(
|
|
342
|
+
&self,
|
|
343
|
+
node_type: &str,
|
|
344
|
+
key: &str,
|
|
345
|
+
method: &str,
|
|
346
|
+
req: &Req,
|
|
347
|
+
) -> Result<Resp> {
|
|
348
|
+
let bytes = self.call(node_type, key, method, &encode(req)?)?;
|
|
349
|
+
decode(&bytes)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Sends a one-way message. Delivery is attempted once; there is no reply.
|
|
353
|
+
pub fn send(&self, node_type: &str, key: &str, method: &str, payload: &[u8]) -> Result<(), CallError> {
|
|
354
|
+
match Status::from_u8(abi::send(&request(node_type, key, method, payload)) as u8) {
|
|
355
|
+
Status::Ok => Ok(()),
|
|
356
|
+
status => Err(CallError { status, message: "send refused".into() }),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// Pushes `payload` to every subscriber of `topic` on this instance.
|
|
361
|
+
pub fn publish(&self, topic: &str, payload: &[u8]) {
|
|
362
|
+
abi::publish(topic, payload);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// A raw node API request: `op` and a JSON body, answered with JSON. The typed operations
|
|
366
|
+
/// behind [`Ctx::world`], [`Ctx::grids`], [`Ctx::players`], [`Ctx::permissions`] and
|
|
367
|
+
/// [`Ctx::access`] use it.
|
|
368
|
+
pub fn node(&self, op: &str, payload: &[u8]) -> Result<Vec<u8>, CallError> {
|
|
369
|
+
rt::import_result(abi::node(op, payload, DEFAULT_DEADLINE_MS))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// Sends a spatial event to the players within `distance` chunks of `at` (at most 8), at
|
|
373
|
+
/// once, through the realtime tier. `actor` is the event's actor: the actor whose state it
|
|
374
|
+
/// is, or the one an [`Spatial::ActorMessage`] is for. See [`emit`].
|
|
375
|
+
pub fn emit_spatial(&self, at: node::ChunkPos, distance: u8, actor: &[u8; 32], event: emit::Spatial<'_>) -> Result<(), CallError> {
|
|
376
|
+
emit::spatial(at, distance, actor, event)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/// Shows a server-driven actor (an NPC, a mob) with `state` to the players near `at`.
|
|
380
|
+
pub fn emit_actor(&self, actor: &[u8; 32], at: node::ChunkPos, distance: u8, state: &[u8]) -> Result<(), CallError> {
|
|
381
|
+
emit::spatial(at, distance, actor, emit::Spatial::Actor { state })
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Sends `payload` to every member of one of the app's channels, as a channel message from
|
|
385
|
+
/// `sender`.
|
|
386
|
+
pub fn emit_channel(&self, channel: u64, sender: &[u8; 32], payload: &[u8]) -> Result<(), CallError> {
|
|
387
|
+
emit::channel(channel, sender, payload)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// Chunks, voxels and actors in this app's world (scopes `world.read`, `world.write`).
|
|
391
|
+
pub fn world(&self) -> node::World<'_> {
|
|
392
|
+
node::World(self)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// This app's grids and their permissions (scopes `grids.read`, `grids.write`).
|
|
396
|
+
pub fn grids(&self) -> node::Grids<'_> {
|
|
397
|
+
node::Grids(self)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// Players of this app: identity, tier features, group permissions and app state (scopes
|
|
401
|
+
/// `players.read`, `players.write`).
|
|
402
|
+
pub fn players(&self) -> node::Players<'_> {
|
|
403
|
+
node::Players(self)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Grants and revokes grid permissions (scope `permissions.write`).
|
|
407
|
+
pub fn permissions(&self) -> node::Permissions<'_> {
|
|
408
|
+
node::Permissions(self)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// Players' access to this app: read, grant, revoke, suspend (scopes `access.read`,
|
|
412
|
+
/// `access.write`).
|
|
413
|
+
pub fn access(&self) -> node::Access<'_> {
|
|
414
|
+
node::Access(self)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
pub fn log(&self, level: Level, msg: &str) {
|
|
418
|
+
abi::log(level as u32, msg);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
pub fn now_ms(&self) -> u64 {
|
|
422
|
+
abi::now_ms()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// Runs [`Hub::on_timer`] with `name` once, `delay_ms` from now, replacing any timer of
|
|
426
|
+
/// that name. Timers are kept in the snapshot, so they survive stops and moves; they run
|
|
427
|
+
/// only while the hub runs, and one that came due meanwhile runs once when it is back.
|
|
428
|
+
pub fn timer_after(&self, name: &str, delay_ms: u64) -> Result<()> {
|
|
429
|
+
rt::set_timer(self, name, delay_ms, 0)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Runs [`Hub::on_timer`] with `name` every `period_ms` (at least 10), the first time one
|
|
433
|
+
/// period from now. Replaces any timer of that name.
|
|
434
|
+
pub fn timer_every(&self, name: &str, period_ms: u64) -> Result<()> {
|
|
435
|
+
rt::set_timer(self, name, period_ms, period_ms)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// Cancels a timer. Returns whether it was set.
|
|
439
|
+
pub fn cancel_timer(&self, name: &str) -> bool {
|
|
440
|
+
rt::cancel_timer(name)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// Pending timers and when each is next due, in Unix milliseconds.
|
|
444
|
+
pub fn timers(&self) -> Vec<(String, u64)> {
|
|
445
|
+
rt::timers()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/// Subscribes this hub to `topic` on another instance of the app, if the manifest lets this
|
|
449
|
+
/// type call that one. Messages arrive at [`Hub::on_topic`]. The subscription is kept in
|
|
450
|
+
/// the snapshot and renewed whenever the hub starts again.
|
|
451
|
+
pub fn subscribe(&self, node_type: &str, key: &str, topic: &str) -> Result<(), CallError> {
|
|
452
|
+
rt::watch(self, node_type, key, topic, true)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
pub fn unsubscribe(&self, node_type: &str, key: &str, topic: &str) -> Result<(), CallError> {
|
|
456
|
+
rt::watch(self, node_type, key, topic, false)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Snapshots this hub soon after the current call returns (at most once a second), for a
|
|
460
|
+
/// change that should not wait for the snapshot interval.
|
|
461
|
+
pub fn persist_now(&self) {
|
|
462
|
+
abi::persist_now();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// A stateful node: one instance per key, one handler at a time.
|
|
467
|
+
pub trait Hub: Sized + 'static {
|
|
468
|
+
/// A new instance with no snapshot.
|
|
469
|
+
fn spawn(ctx: &Ctx, seed: &[u8]) -> Result<Self>;
|
|
470
|
+
/// An instance restored from its last snapshot, written by code version `from_version`.
|
|
471
|
+
fn load(ctx: &Ctx, snapshot: &[u8], from_version: u64) -> Result<Self>;
|
|
472
|
+
/// The state to snapshot. Called on the snapshot interval, before a move and before a stop.
|
|
473
|
+
fn persist(&mut self, ctx: &Ctx) -> Result<Vec<u8>>;
|
|
474
|
+
fn handle(&mut self, ctx: &Ctx, call: Call<'_>) -> Result<Vec<u8>>;
|
|
475
|
+
/// Called once before a graceful stop, after the last persist.
|
|
476
|
+
fn evict(&mut self, _ctx: &Ctx) {}
|
|
477
|
+
/// A timer set with [`Ctx::timer_after`] or [`Ctx::timer_every`] is due. An error is logged;
|
|
478
|
+
/// the timer stays as scheduled.
|
|
479
|
+
fn on_timer(&mut self, _ctx: &Ctx, _timer: &str) -> Result<()> {
|
|
480
|
+
Ok(())
|
|
481
|
+
}
|
|
482
|
+
/// A message on a topic this hub watches.
|
|
483
|
+
fn on_topic(&mut self, _ctx: &Ctx, _msg: TopicMsg<'_>) -> Result<()> {
|
|
484
|
+
Ok(())
|
|
485
|
+
}
|
|
486
|
+
/// Root hub only: players entered or left the app.
|
|
487
|
+
fn on_presence(&mut self, _ctx: &Ctx, _presence: &Presence) -> Result<()> {
|
|
488
|
+
Ok(())
|
|
489
|
+
}
|
|
490
|
+
/// A player's connection subscribed to this hub, or left it.
|
|
491
|
+
fn on_session(&mut self, _ctx: &Ctx, _player: u64, _session: Session) -> Result<()> {
|
|
492
|
+
Ok(())
|
|
493
|
+
}
|
|
494
|
+
/// Mods only: things happened in the mod's grid. An actor arriving starts a mod that is
|
|
495
|
+
/// not running, and this is its first call after `spawn` or `load`.
|
|
496
|
+
fn on_world(&mut self, _ctx: &Ctx, _events: &[WorldEvent]) -> Result<()> {
|
|
497
|
+
Ok(())
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// A stateless node. Replicas may run anywhere; keep nothing here you cannot lose.
|
|
502
|
+
pub trait Spoke: Sized + 'static {
|
|
503
|
+
fn start(ctx: &Ctx) -> Result<Self>;
|
|
504
|
+
fn handle(&mut self, ctx: &Ctx, call: Call<'_>) -> Result<Vec<u8>>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/// Exports a [`Hub`] type as this module's node.
|
|
508
|
+
#[macro_export]
|
|
509
|
+
macro_rules! export_hub {
|
|
510
|
+
($t:ty) => {
|
|
511
|
+
$crate::__export_node!($crate::rt::HubNode<$t>);
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// Exports a [`Spoke`] type as this module's node.
|
|
516
|
+
#[macro_export]
|
|
517
|
+
macro_rules! export_spoke {
|
|
518
|
+
($t:ty) => {
|
|
519
|
+
$crate::__export_node!($crate::rt::SpokeNode<$t>);
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
#[doc(hidden)]
|
|
524
|
+
#[macro_export]
|
|
525
|
+
macro_rules! __export_node {
|
|
526
|
+
($node:ty) => {
|
|
527
|
+
#[unsafe(no_mangle)]
|
|
528
|
+
pub extern "C" fn ckx_abi() -> u32 {
|
|
529
|
+
$crate::rt::ABI_VERSION
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[unsafe(no_mangle)]
|
|
533
|
+
pub extern "C" fn ckx_alloc(len: u32) -> u32 {
|
|
534
|
+
$crate::rt::alloc(len)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#[unsafe(no_mangle)]
|
|
538
|
+
pub extern "C" fn ckx_init(
|
|
539
|
+
ctx_ptr: u32,
|
|
540
|
+
ctx_len: u32,
|
|
541
|
+
seed_ptr: u32,
|
|
542
|
+
seed_len: u32,
|
|
543
|
+
snap_ptr: u32,
|
|
544
|
+
snap_len: u32,
|
|
545
|
+
snap_version: u64,
|
|
546
|
+
has_snap: u32,
|
|
547
|
+
) -> u64 {
|
|
548
|
+
$crate::rt::init::<$node>(ctx_ptr, ctx_len, seed_ptr, seed_len, snap_ptr, snap_len, snap_version, has_snap)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#[unsafe(no_mangle)]
|
|
552
|
+
pub extern "C" fn ckx_handle(caller_kind: u32, caller_id: u64, buf_ptr: u32, method_len: u32, payload_len: u32) -> u64 {
|
|
553
|
+
$crate::rt::handle(caller_kind, caller_id, buf_ptr, method_len, payload_len)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#[unsafe(no_mangle)]
|
|
557
|
+
pub extern "C" fn ckx_persist() -> u64 {
|
|
558
|
+
$crate::rt::persist()
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#[unsafe(no_mangle)]
|
|
562
|
+
pub extern "C" fn ckx_evict() {
|
|
563
|
+
$crate::rt::evict()
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#[cfg(test)]
|
|
569
|
+
mod tests {
|
|
570
|
+
use super::*;
|
|
571
|
+
|
|
572
|
+
#[test]
|
|
573
|
+
fn request_layout_matches_the_abi() {
|
|
574
|
+
let req = request("arena", "m1", "hit", b"\x01");
|
|
575
|
+
assert_eq!(req, b"\x05arena\x02\x00m1\x03hit\x01");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
#[test]
|
|
579
|
+
fn encode_uses_named_fields() {
|
|
580
|
+
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
|
|
581
|
+
struct Hit {
|
|
582
|
+
damage: i64,
|
|
583
|
+
}
|
|
584
|
+
let bytes = encode(&Hit { damage: 5 }).unwrap();
|
|
585
|
+
assert!(bytes.windows(6).any(|w| w == b"damage"));
|
|
586
|
+
assert_eq!(decode::<Hit>(&bytes).unwrap(), Hit { damage: 5 });
|
|
587
|
+
}
|
|
588
|
+
}
|