@crowdedkingdoms/ckx-kit 0.0.0-stage → 0.1.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.
- 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,482 @@
|
|
|
1
|
+
//! Typed state for a hub (HS-27): what the legacy game models kept in the database, kept in the
|
|
2
|
+
//! hub instead. Containers of declared types hold typed properties with defaults, edges relate
|
|
3
|
+
//! them, and each property's visibility decides what a player may read.
|
|
4
|
+
//!
|
|
5
|
+
//! ```ignore
|
|
6
|
+
//! let schema = Schema::new()
|
|
7
|
+
//! .container("Character", |c| {
|
|
8
|
+
//! c.prop("hp", PropKind::Int, 100, Visibility::Public)
|
|
9
|
+
//! .prop("gold", PropKind::Int, 0, Visibility::Owner)
|
|
10
|
+
//! .prop("weakness", PropKind::Str, "", Visibility::Hidden)
|
|
11
|
+
//! })
|
|
12
|
+
//! .container("Item", |c| c.prop("name", PropKind::Str, "", Visibility::Public));
|
|
13
|
+
//! let hero = self.model.ensure(&schema, "Character", &player.to_string(), Some(player))?;
|
|
14
|
+
//! self.model.add(&schema, hero, "hp", -12)?;
|
|
15
|
+
//! self.model.publish_changes(ctx, &schema, "world");
|
|
16
|
+
//! ```
|
|
17
|
+
//!
|
|
18
|
+
//! The model is plain data: keep it in the hub's state and it is snapshotted with the rest.
|
|
19
|
+
//! The schema is code, rebuilt with the module, so a property added in a new version reads as
|
|
20
|
+
//! its default in containers created before it.
|
|
21
|
+
|
|
22
|
+
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
|
23
|
+
|
|
24
|
+
use serde::{Deserialize, Serialize};
|
|
25
|
+
use serde_json::Value;
|
|
26
|
+
|
|
27
|
+
use crate::{Ctx, Error, Result};
|
|
28
|
+
|
|
29
|
+
/// What a property holds. `Json` holds any JSON value.
|
|
30
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
31
|
+
pub enum PropKind {
|
|
32
|
+
Bool,
|
|
33
|
+
Int,
|
|
34
|
+
Float,
|
|
35
|
+
Str,
|
|
36
|
+
Json,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl PropKind {
|
|
40
|
+
fn accepts(self, v: &Value) -> bool {
|
|
41
|
+
match self {
|
|
42
|
+
PropKind::Bool => v.is_boolean(),
|
|
43
|
+
PropKind::Int => v.is_i64() || v.is_u64(),
|
|
44
|
+
PropKind::Float => v.is_number(),
|
|
45
|
+
PropKind::Str => v.is_string(),
|
|
46
|
+
PropKind::Json => true,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Who may read a property: anyone who may read the container, only its owner, or nobody but
|
|
52
|
+
/// the hub itself (secret stats, seeds).
|
|
53
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
54
|
+
pub enum Visibility {
|
|
55
|
+
Public,
|
|
56
|
+
Owner,
|
|
57
|
+
Hidden,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Who a [`Model::view`] is for.
|
|
61
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
62
|
+
pub enum Viewer {
|
|
63
|
+
/// The hub, another instance or a developer: every property.
|
|
64
|
+
Server,
|
|
65
|
+
/// A player: public properties, and owner ones on containers the player owns.
|
|
66
|
+
Player(u64),
|
|
67
|
+
/// Anyone: public properties only, as [`Model::publish_changes`] sends them.
|
|
68
|
+
Public,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
72
|
+
pub struct Prop {
|
|
73
|
+
pub kind: PropKind,
|
|
74
|
+
pub default: Value,
|
|
75
|
+
pub visibility: Visibility,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// The declared container types and their properties.
|
|
79
|
+
#[derive(Clone, Debug, Default)]
|
|
80
|
+
pub struct Schema {
|
|
81
|
+
types: BTreeMap<String, BTreeMap<String, Prop>>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Declares one container type's properties; see [`Schema::container`].
|
|
85
|
+
#[derive(Default)]
|
|
86
|
+
pub struct ContainerDef {
|
|
87
|
+
props: BTreeMap<String, Prop>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl ContainerDef {
|
|
91
|
+
/// A property with its kind, its default, and who may read it. A default of the wrong kind
|
|
92
|
+
/// panics when the schema is built, which is where a typo belongs.
|
|
93
|
+
pub fn prop(mut self, key: &str, kind: PropKind, default: impl Into<Value>, visibility: Visibility) -> Self {
|
|
94
|
+
let default = default.into();
|
|
95
|
+
assert!(kind.accepts(&default), "default of `{key}` is not a {kind:?}");
|
|
96
|
+
self.props.insert(key.to_owned(), Prop { kind, default, visibility });
|
|
97
|
+
self
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Schema {
|
|
102
|
+
pub fn new() -> Self {
|
|
103
|
+
Self::default()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn container(mut self, name: &str, define: impl FnOnce(ContainerDef) -> ContainerDef) -> Self {
|
|
107
|
+
self.types.insert(name.to_owned(), define(ContainerDef::default()).props);
|
|
108
|
+
self
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn props(&self, type_name: &str) -> Result<&BTreeMap<String, Prop>> {
|
|
112
|
+
self.types.get(type_name).ok_or_else(|| Error(format!("no container type `{type_name}`")))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn prop(&self, type_name: &str, key: &str) -> Result<&Prop> {
|
|
116
|
+
self.props(type_name)?.get(key).ok_or_else(|| Error(format!("`{type_name}` has no property `{key}`")))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
121
|
+
pub struct Container {
|
|
122
|
+
pub type_name: String,
|
|
123
|
+
/// The player who owns it, if one does: they read its `Owner` properties.
|
|
124
|
+
pub owner: Option<u64>,
|
|
125
|
+
/// The key it was [`Model::ensure`]d under, if it was.
|
|
126
|
+
pub key: Option<String>,
|
|
127
|
+
/// Properties set so far; the rest read as their defaults.
|
|
128
|
+
props: BTreeMap<String, Value>,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
132
|
+
pub struct Edge {
|
|
133
|
+
pub from: u64,
|
|
134
|
+
pub relation: String,
|
|
135
|
+
pub to: u64,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// What changed since the last [`Model::take_changes`].
|
|
139
|
+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
140
|
+
pub struct Changes {
|
|
141
|
+
pub changed: Vec<u64>,
|
|
142
|
+
pub removed: Vec<u64>,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// One published change: a container's public view, or its removal.
|
|
146
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
147
|
+
pub struct Published {
|
|
148
|
+
pub id: u64,
|
|
149
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
150
|
+
pub type_name: Option<String>,
|
|
151
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
152
|
+
pub props: Option<BTreeMap<String, Value>>,
|
|
153
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
154
|
+
pub removed: bool,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// The deepest [`Model::traverse`] walks, as the legacy graph traversal.
|
|
158
|
+
pub const MAX_DEPTH: usize = 5;
|
|
159
|
+
|
|
160
|
+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
161
|
+
pub struct Model {
|
|
162
|
+
next_id: u64,
|
|
163
|
+
containers: BTreeMap<u64, Container>,
|
|
164
|
+
/// `"<type>\0<key>"` to the container ensured under it.
|
|
165
|
+
keys: BTreeMap<String, u64>,
|
|
166
|
+
edges: BTreeSet<Edge>,
|
|
167
|
+
#[serde(skip)]
|
|
168
|
+
changed: BTreeSet<u64>,
|
|
169
|
+
#[serde(skip)]
|
|
170
|
+
removed: BTreeSet<u64>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn key_of(type_name: &str, key: &str) -> String {
|
|
174
|
+
format!("{type_name}\0{key}")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
impl Model {
|
|
178
|
+
pub fn new() -> Self {
|
|
179
|
+
Self::default()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// A new container of `type_name`, with every property at its default.
|
|
183
|
+
pub fn create(&mut self, schema: &Schema, type_name: &str, owner: Option<u64>) -> Result<u64> {
|
|
184
|
+
schema.props(type_name)?;
|
|
185
|
+
self.next_id += 1;
|
|
186
|
+
let id = self.next_id;
|
|
187
|
+
self.containers.insert(id, Container { type_name: type_name.to_owned(), owner, key: None, props: BTreeMap::new() });
|
|
188
|
+
self.changed.insert(id);
|
|
189
|
+
Ok(id)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The container of `type_name` under `key`, created the first time: a player's character,
|
|
193
|
+
/// a match's scoreboard.
|
|
194
|
+
pub fn ensure(&mut self, schema: &Schema, type_name: &str, key: &str, owner: Option<u64>) -> Result<u64> {
|
|
195
|
+
if let Some(id) = self.find(type_name, key) {
|
|
196
|
+
return Ok(id);
|
|
197
|
+
}
|
|
198
|
+
let id = self.create(schema, type_name, owner)?;
|
|
199
|
+
self.containers.get_mut(&id).expect("just created").key = Some(key.to_owned());
|
|
200
|
+
self.keys.insert(key_of(type_name, key), id);
|
|
201
|
+
Ok(id)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
pub fn find(&self, type_name: &str, key: &str) -> Option<u64> {
|
|
205
|
+
self.keys.get(&key_of(type_name, key)).copied()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pub fn container(&self, id: u64) -> Option<&Container> {
|
|
209
|
+
self.containers.get(&id)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Every container of `type_name`, oldest first.
|
|
213
|
+
pub fn of_type<'a>(&'a self, type_name: &'a str) -> impl Iterator<Item = u64> + 'a {
|
|
214
|
+
self.containers.iter().filter(move |(_, c)| c.type_name == type_name).map(|(id, _)| *id)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn existing(&self, id: u64) -> Result<&Container> {
|
|
218
|
+
self.containers.get(&id).ok_or_else(|| Error(format!("no container {id}")))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// A property's value, or its default when it was never set.
|
|
222
|
+
pub fn get(&self, schema: &Schema, id: u64, key: &str) -> Result<Value> {
|
|
223
|
+
let c = self.existing(id)?;
|
|
224
|
+
let prop = schema.prop(&c.type_name, key)?;
|
|
225
|
+
Ok(c.props.get(key).cloned().unwrap_or_else(|| prop.default.clone()))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
pub fn get_i64(&self, schema: &Schema, id: u64, key: &str) -> Result<i64> {
|
|
229
|
+
self.get(schema, id, key)?.as_i64().ok_or_else(|| Error(format!("`{key}` is not an integer")))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
pub fn get_f64(&self, schema: &Schema, id: u64, key: &str) -> Result<f64> {
|
|
233
|
+
self.get(schema, id, key)?.as_f64().ok_or_else(|| Error(format!("`{key}` is not a number")))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
pub fn get_bool(&self, schema: &Schema, id: u64, key: &str) -> Result<bool> {
|
|
237
|
+
self.get(schema, id, key)?.as_bool().ok_or_else(|| Error(format!("`{key}` is not a bool")))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pub fn get_str(&self, schema: &Schema, id: u64, key: &str) -> Result<String> {
|
|
241
|
+
match self.get(schema, id, key)? {
|
|
242
|
+
Value::String(s) => Ok(s),
|
|
243
|
+
_ => Err(Error(format!("`{key}` is not a string"))),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Sets a property, refusing a value of the wrong kind.
|
|
248
|
+
pub fn set(&mut self, schema: &Schema, id: u64, key: &str, value: impl Into<Value>) -> Result<()> {
|
|
249
|
+
let value = value.into();
|
|
250
|
+
let type_name = self.existing(id)?.type_name.clone();
|
|
251
|
+
let prop = schema.prop(&type_name, key)?;
|
|
252
|
+
if !prop.kind.accepts(&value) {
|
|
253
|
+
return Err(Error(format!("`{type_name}.{key}` is a {:?}, not {value}", prop.kind)));
|
|
254
|
+
}
|
|
255
|
+
self.containers.get_mut(&id).expect("checked").props.insert(key.to_owned(), value);
|
|
256
|
+
self.changed.insert(id);
|
|
257
|
+
Ok(())
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Adds `delta` to an integer property and returns the new value.
|
|
261
|
+
pub fn add(&mut self, schema: &Schema, id: u64, key: &str, delta: i64) -> Result<i64> {
|
|
262
|
+
let now = self.get_i64(schema, id, key)?.saturating_add(delta);
|
|
263
|
+
self.set(schema, id, key, now)?;
|
|
264
|
+
Ok(now)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Removes a container, its key and every edge touching it.
|
|
268
|
+
pub fn delete(&mut self, id: u64) -> bool {
|
|
269
|
+
let Some(c) = self.containers.remove(&id) else { return false };
|
|
270
|
+
if let Some(key) = &c.key {
|
|
271
|
+
self.keys.remove(&key_of(&c.type_name, key));
|
|
272
|
+
}
|
|
273
|
+
self.edges.retain(|e| e.from != id && e.to != id);
|
|
274
|
+
self.changed.remove(&id);
|
|
275
|
+
self.removed.insert(id);
|
|
276
|
+
true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// `from --relation--> to`, e.g. an inventory holding an item.
|
|
280
|
+
pub fn link(&mut self, from: u64, relation: &str, to: u64) -> Result<()> {
|
|
281
|
+
self.existing(from)?;
|
|
282
|
+
self.existing(to)?;
|
|
283
|
+
self.edges.insert(Edge { from, relation: relation.to_owned(), to });
|
|
284
|
+
self.changed.insert(from);
|
|
285
|
+
Ok(())
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
pub fn unlink(&mut self, from: u64, relation: &str, to: u64) -> bool {
|
|
289
|
+
let gone = self.edges.remove(&Edge { from, relation: relation.to_owned(), to });
|
|
290
|
+
if gone {
|
|
291
|
+
self.changed.insert(from);
|
|
292
|
+
}
|
|
293
|
+
gone
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// The containers `from` links to by `relation`.
|
|
297
|
+
pub fn linked(&self, from: u64, relation: &str) -> Vec<u64> {
|
|
298
|
+
self.edges.iter().filter(|e| e.from == from && e.relation == relation).map(|e| e.to).collect()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Every container reachable from `root` by `relation`, breadth first, at most
|
|
302
|
+
/// [`MAX_DEPTH`] steps away; `root` itself is not included.
|
|
303
|
+
pub fn traverse(&self, root: u64, relation: &str, depth: usize) -> Vec<u64> {
|
|
304
|
+
let depth = depth.min(MAX_DEPTH);
|
|
305
|
+
let mut seen = BTreeSet::from([root]);
|
|
306
|
+
let mut out = Vec::new();
|
|
307
|
+
let mut queue = VecDeque::from([(root, 0usize)]);
|
|
308
|
+
while let Some((at, d)) = queue.pop_front() {
|
|
309
|
+
if d == depth {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for next in self.linked(at, relation) {
|
|
313
|
+
if seen.insert(next) {
|
|
314
|
+
out.push(next);
|
|
315
|
+
queue.push_back((next, d + 1));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
out
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// The properties `viewer` may read, with defaults filled in: every one for the server,
|
|
323
|
+
/// public ones for a player, and owner ones too on containers the player owns.
|
|
324
|
+
pub fn view(&self, schema: &Schema, id: u64, viewer: Viewer) -> Result<BTreeMap<String, Value>> {
|
|
325
|
+
let c = self.existing(id)?;
|
|
326
|
+
let owns = matches!(viewer, Viewer::Player(p) if c.owner == Some(p));
|
|
327
|
+
let mut out = BTreeMap::new();
|
|
328
|
+
for (key, prop) in schema.props(&c.type_name)? {
|
|
329
|
+
let visible = match (viewer, prop.visibility) {
|
|
330
|
+
(Viewer::Server, _) | (_, Visibility::Public) => true,
|
|
331
|
+
(Viewer::Player(_), Visibility::Owner) => owns,
|
|
332
|
+
(Viewer::Player(_), Visibility::Hidden) | (Viewer::Public, _) => false,
|
|
333
|
+
};
|
|
334
|
+
if visible {
|
|
335
|
+
out.insert(key.clone(), c.props.get(key).cloned().unwrap_or_else(|| prop.default.clone()));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
Ok(out)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// The containers changed and removed since the last call.
|
|
342
|
+
pub fn take_changes(&mut self) -> Changes {
|
|
343
|
+
Changes {
|
|
344
|
+
changed: std::mem::take(&mut self.changed).into_iter().collect(),
|
|
345
|
+
removed: std::mem::take(&mut self.removed).into_iter().collect(),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// The changes since the last call as what anyone may see: each changed container's public
|
|
350
|
+
/// view, and each removal.
|
|
351
|
+
pub fn public_changes(&mut self, schema: &Schema) -> Vec<Published> {
|
|
352
|
+
let Changes { changed, removed } = self.take_changes();
|
|
353
|
+
let mut out = Vec::with_capacity(changed.len() + removed.len());
|
|
354
|
+
for id in changed {
|
|
355
|
+
if let (Some(c), Ok(props)) = (self.containers.get(&id), self.view(schema, id, Viewer::Public)) {
|
|
356
|
+
out.push(Published { id, type_name: Some(c.type_name.clone()), props: Some(props), removed: false });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
out.extend(removed.into_iter().map(|id| Published { id, type_name: None, props: None, removed: true }));
|
|
360
|
+
out
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Publishes [`Model::public_changes`] on this hub's `topic`, one message per container,
|
|
364
|
+
/// for subscribed clients. Owner and hidden properties are never published.
|
|
365
|
+
pub fn publish_changes(&mut self, ctx: &Ctx, schema: &Schema, topic: &str) {
|
|
366
|
+
for p in self.public_changes(schema) {
|
|
367
|
+
if let Ok(bytes) = crate::encode(&p) {
|
|
368
|
+
ctx.publish(topic, &bytes);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[cfg(test)]
|
|
375
|
+
mod tests {
|
|
376
|
+
use super::*;
|
|
377
|
+
use serde_json::json;
|
|
378
|
+
|
|
379
|
+
fn schema() -> Schema {
|
|
380
|
+
Schema::new()
|
|
381
|
+
.container("Character", |c| {
|
|
382
|
+
c.prop("hp", PropKind::Int, 100, Visibility::Public)
|
|
383
|
+
.prop("gold", PropKind::Int, 0, Visibility::Owner)
|
|
384
|
+
.prop("weakness", PropKind::Str, "", Visibility::Hidden)
|
|
385
|
+
.prop("speed", PropKind::Float, 1.5, Visibility::Public)
|
|
386
|
+
})
|
|
387
|
+
.container("Item", |c| c.prop("name", PropKind::Str, "", Visibility::Public))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#[test]
|
|
391
|
+
fn properties_default_and_are_type_checked() {
|
|
392
|
+
let s = schema();
|
|
393
|
+
let mut m = Model::new();
|
|
394
|
+
let hero = m.create(&s, "Character", Some(7)).unwrap();
|
|
395
|
+
assert_eq!(m.get_i64(&s, hero, "hp").unwrap(), 100);
|
|
396
|
+
assert_eq!(m.add(&s, hero, "hp", -12).unwrap(), 88);
|
|
397
|
+
assert!(m.set(&s, hero, "hp", "lots").is_err(), "a string is not an Int");
|
|
398
|
+
assert!(m.set(&s, hero, "speed", 2).is_ok(), "an integer is a Float");
|
|
399
|
+
assert!(m.set(&s, hero, "armor", 3).is_err(), "undeclared property");
|
|
400
|
+
assert!(m.create(&s, "Dragon", None).is_err(), "undeclared type");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#[test]
|
|
404
|
+
fn ensure_is_get_or_create_by_key() {
|
|
405
|
+
let s = schema();
|
|
406
|
+
let mut m = Model::new();
|
|
407
|
+
let a = m.ensure(&s, "Character", "42", Some(42)).unwrap();
|
|
408
|
+
assert_eq!(m.ensure(&s, "Character", "42", Some(42)).unwrap(), a);
|
|
409
|
+
assert_ne!(m.ensure(&s, "Character", "43", Some(43)).unwrap(), a);
|
|
410
|
+
assert_eq!(m.find("Character", "42"), Some(a));
|
|
411
|
+
assert!(m.delete(a));
|
|
412
|
+
assert_eq!(m.find("Character", "42"), None);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[test]
|
|
416
|
+
fn a_player_sees_public_properties_and_owner_ones_on_their_own() {
|
|
417
|
+
let s = schema();
|
|
418
|
+
let mut m = Model::new();
|
|
419
|
+
let mine = m.create(&s, "Character", Some(7)).unwrap();
|
|
420
|
+
m.set(&s, mine, "gold", 30).unwrap();
|
|
421
|
+
m.set(&s, mine, "weakness", "fire").unwrap();
|
|
422
|
+
let theirs = m.view(&s, mine, Viewer::Player(8)).unwrap();
|
|
423
|
+
assert_eq!(theirs.keys().collect::<Vec<_>>(), ["hp", "speed"]);
|
|
424
|
+
let own = m.view(&s, mine, Viewer::Player(7)).unwrap();
|
|
425
|
+
assert_eq!(own.get("gold"), Some(&json!(30)));
|
|
426
|
+
assert!(!own.contains_key("weakness"), "hidden from its owner too");
|
|
427
|
+
assert_eq!(m.view(&s, mine, Viewer::Server).unwrap().get("weakness"), Some(&json!("fire")));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
fn edges_link_traverse_and_go_with_their_containers() {
|
|
432
|
+
let s = schema();
|
|
433
|
+
let mut m = Model::new();
|
|
434
|
+
let hero = m.create(&s, "Character", None).unwrap();
|
|
435
|
+
let bag = m.create(&s, "Item", None).unwrap();
|
|
436
|
+
let gem = m.create(&s, "Item", None).unwrap();
|
|
437
|
+
m.link(hero, "holds", bag).unwrap();
|
|
438
|
+
m.link(bag, "holds", gem).unwrap();
|
|
439
|
+
m.link(gem, "holds", hero).unwrap();
|
|
440
|
+
assert_eq!(m.linked(hero, "holds"), vec![bag]);
|
|
441
|
+
assert_eq!(m.traverse(hero, "holds", 10), vec![bag, gem], "cycles end, root excluded");
|
|
442
|
+
assert_eq!(m.traverse(hero, "holds", 1), vec![bag]);
|
|
443
|
+
m.delete(bag);
|
|
444
|
+
assert!(m.linked(hero, "holds").is_empty());
|
|
445
|
+
assert!(m.link(hero, "holds", 999).is_err());
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#[test]
|
|
449
|
+
fn changes_publish_public_views_and_removals_once() {
|
|
450
|
+
let s = schema();
|
|
451
|
+
let mut m = Model::new();
|
|
452
|
+
let hero = m.create(&s, "Character", Some(7)).unwrap();
|
|
453
|
+
let item = m.create(&s, "Item", None).unwrap();
|
|
454
|
+
m.set(&s, hero, "gold", 5).unwrap();
|
|
455
|
+
m.take_changes();
|
|
456
|
+
m.set(&s, hero, "hp", 90).unwrap();
|
|
457
|
+
m.delete(item);
|
|
458
|
+
let out = m.public_changes(&s);
|
|
459
|
+
assert_eq!(out.len(), 2);
|
|
460
|
+
let props = out[0].props.as_ref().unwrap();
|
|
461
|
+
assert_eq!(props.get("hp"), Some(&json!(90)));
|
|
462
|
+
assert!(!props.contains_key("gold") && !props.contains_key("weakness"));
|
|
463
|
+
assert!(out[1].removed && out[1].id == item);
|
|
464
|
+
assert!(m.public_changes(&s).is_empty());
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[test]
|
|
468
|
+
fn a_model_survives_a_snapshot() {
|
|
469
|
+
let s = schema();
|
|
470
|
+
let mut m = Model::new();
|
|
471
|
+
let hero = m.ensure(&s, "Character", "7", Some(7)).unwrap();
|
|
472
|
+
let item = m.create(&s, "Item", None).unwrap();
|
|
473
|
+
m.set(&s, hero, "hp", 3).unwrap();
|
|
474
|
+
m.link(hero, "holds", item).unwrap();
|
|
475
|
+
let back: Model = crate::decode(&crate::encode(&m).unwrap()).unwrap();
|
|
476
|
+
assert_eq!(back.get_i64(&s, hero, "hp").unwrap(), 3);
|
|
477
|
+
assert_eq!(back.find("Character", "7"), Some(hero));
|
|
478
|
+
assert_eq!(back.linked(hero, "holds"), vec![item]);
|
|
479
|
+
let mut back = back;
|
|
480
|
+
assert_eq!(back.create(&s, "Item", None).unwrap(), item + 1, "ids keep counting");
|
|
481
|
+
}
|
|
482
|
+
}
|