@crowdedkingdoms/ckx-kit 0.5.0 → 0.6.0-test.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/crates/ckx-sdk/Cargo.toml +1 -1
- package/crates/ckx-sdk/src/lib.rs +38 -5
- package/crates/ckx-sdk/src/limits.rs +3 -0
- package/crates/ckx-sdk/src/node.rs +225 -4
- package/crates/ckx-sdk/src/rt.rs +17 -0
- package/crates/ckx-sdk/src/testing.rs +112 -27
- package/crates/crowdy-client-sdk/Cargo.toml +1 -1
- package/dist/checks/src/build/exec-source.js +34 -0
- package/kit.pin +4 -4
- package/package.json +1 -1
- package/starters/arena/Cargo.toml +1 -1
- package/starters/deck/Cargo.toml +1 -1
- package/starters/director/Cargo.toml +1 -1
- package/starters/grid-mod/Cargo.toml +1 -1
- package/starters/instance/Cargo.toml +1 -1
- package/starters/leaderboard/Cargo.toml +1 -1
- package/starters/market/Cargo.toml +1 -1
- package/starters/matchmaker/Cargo.toml +1 -1
- package/starters/minigame/Cargo.toml +1 -1
- package/starters/npc-mobs/Cargo.toml +1 -1
- package/starters/player/Cargo.toml +1 -1
- package/starters/racing/Cargo.toml +1 -1
- package/starters/session/Cargo.toml +1 -1
- package/starters/tactics/Cargo.toml +1 -1
- package/starters/territory/Cargo.toml +1 -1
- package/starters/world-tick/Cargo.toml +1 -1
|
@@ -31,15 +31,18 @@
|
|
|
31
31
|
//! Realtime events players around see at once go through [`Ctx::emit_spatial`],
|
|
32
32
|
//! [`Ctx::emit_actor`] and [`Ctx::emit_channel`]; see [`emit`].
|
|
33
33
|
//!
|
|
34
|
-
//! Platform data (world, grids, players, grid permissions, players' access to the app
|
|
35
|
-
//! [`Ctx::world`], [`Ctx::grids`], [`Ctx::players`], [`Ctx::permissions`]
|
|
36
|
-
//! each needing a scope the node type declares in the
|
|
34
|
+
//! Platform data (world, grids, players, grid permissions, players' access to the app, safety
|
|
35
|
+
//! reports) is behind [`Ctx::world`], [`Ctx::grids`], [`Ctx::players`], [`Ctx::permissions`],
|
|
36
|
+
//! [`Ctx::access`] and [`Ctx::safety`], each needing a scope the node type declares in the
|
|
37
|
+
//! manifest; see [`node`].
|
|
37
38
|
//!
|
|
38
39
|
//! Hubs also get platform events, each a default no-op on [`Hub`]: timers they set
|
|
39
40
|
//! ([`Hub::on_timer`]), topics they watch ([`Hub::on_topic`]), players entering and leaving
|
|
40
41
|
//! the app ([`Hub::on_presence`], root hub only) and players' connections subscribing to them
|
|
41
42
|
//! ([`Hub::on_session`]). A timer can follow a cron schedule, the legacy automations' syntax:
|
|
42
|
-
//! see [`cron`] and [`Ctx::timer_cron`].
|
|
43
|
+
//! see [`cron`] and [`Ctx::timer_cron`]. Two more concern what a hub holds about a player:
|
|
44
|
+
//! erasing it when their account is erased ([`Hub::on_player_erased`]) and exporting it
|
|
45
|
+
//! ([`Hub::on_player_export`]); a hub that does not implement them answers "not implemented".
|
|
43
46
|
//!
|
|
44
47
|
//! Nodes are unit tested natively with `cargo test`: `testing` runs one against a fake of the
|
|
45
48
|
//! platform (calls, timers, events, snapshots, the node API, a mod's grid). [`limits`] has the
|
|
@@ -63,12 +66,14 @@ pub mod rt;
|
|
|
63
66
|
pub mod testing;
|
|
64
67
|
|
|
65
68
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
|
69
|
+
/// What [`Hub::on_player_export`] answers in, for a crate that does not depend on it itself.
|
|
70
|
+
pub use serde_json;
|
|
66
71
|
|
|
67
72
|
pub mod prelude {
|
|
68
73
|
pub use crate::cron::Cron;
|
|
69
74
|
pub use crate::emit::{Spatial, actor_id};
|
|
70
75
|
pub use crate::model::{Model, PropKind, Schema, Viewer, Visibility};
|
|
71
|
-
pub use crate::node::{ChunkPos, GridPick, VoxelWrite};
|
|
76
|
+
pub use crate::node::{AgeBracket, ChunkPos, GridPick, ReportReason, SafetyReport, VoxelWrite};
|
|
72
77
|
pub use crate::{
|
|
73
78
|
Call, CallError, Caller, Ctx, Error, Hub, Kind, Level, Presence, PresencePlayer, Result, Session, Spoke, Status, TopicMsg,
|
|
74
79
|
WorldEvent, decode, encode,
|
|
@@ -88,6 +93,13 @@ impl Error {
|
|
|
88
93
|
pub fn unknown_method(method: &str) -> Self {
|
|
89
94
|
Self(format!("unknown method `{method}`"))
|
|
90
95
|
}
|
|
96
|
+
|
|
97
|
+
/// What a hook the hub does not implement answers ([`Hub::on_player_erased`],
|
|
98
|
+
/// [`Hub::on_player_export`]). The platform tells it from a failure: it records it and goes
|
|
99
|
+
/// on.
|
|
100
|
+
pub fn not_implemented(hook: &str) -> Self {
|
|
101
|
+
Self(format!("not implemented: `{hook}`"))
|
|
102
|
+
}
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
impl std::fmt::Display for Error {
|
|
@@ -446,6 +458,11 @@ impl Ctx {
|
|
|
446
458
|
node::Access(self)
|
|
447
459
|
}
|
|
448
460
|
|
|
461
|
+
/// Safety reports a player files through this hub (scope `safety.write`).
|
|
462
|
+
pub fn safety(&self) -> node::Safety<'_> {
|
|
463
|
+
node::Safety(self)
|
|
464
|
+
}
|
|
465
|
+
|
|
449
466
|
pub fn log(&self, level: Level, msg: &str) {
|
|
450
467
|
abi::log(level as u32, msg);
|
|
451
468
|
}
|
|
@@ -528,6 +545,22 @@ pub trait Hub: Sized + 'static {
|
|
|
528
545
|
fn on_world(&mut self, _ctx: &Ctx, _events: &[WorldEvent]) -> Result<()> {
|
|
529
546
|
Ok(())
|
|
530
547
|
}
|
|
548
|
+
/// `player`'s account is being erased: delete or anonymize everything this hub holds about
|
|
549
|
+
/// them. Every hub of the app that holds state hears it, a stopped one started for it,
|
|
550
|
+
/// the player's own included (one keyed by their id, which the platform then deletes with
|
|
551
|
+
/// its snapshot). The platform counts the hub done once a snapshot taken after this returns
|
|
552
|
+
/// is stored; an error is recorded, and the erasure asks again later. Left out, the hub
|
|
553
|
+
/// answers "not implemented", which is recorded and does not hold the erasure up.
|
|
554
|
+
fn on_player_erased(&mut self, _ctx: &Ctx, _player: u64) -> Result<()> {
|
|
555
|
+
Err(Error::not_implemented("on_player_erased"))
|
|
556
|
+
}
|
|
557
|
+
/// What this hub holds about `player`, for their data export: JSON, at most
|
|
558
|
+
/// [`limits::MAX_EXPORT_BYTES`] of it, or `Value::Null` when it holds nothing. Every hub of
|
|
559
|
+
/// the app that holds state is asked, a stopped one started for it. Left out, the hub
|
|
560
|
+
/// answers "not implemented", which the export records.
|
|
561
|
+
fn on_player_export(&mut self, _ctx: &Ctx, _player: u64) -> Result<serde_json::Value> {
|
|
562
|
+
Err(Error::not_implemented("on_player_export"))
|
|
563
|
+
}
|
|
531
564
|
}
|
|
532
565
|
|
|
533
566
|
/// A stateless node. Replicas may run anywhere; keep nothing here you cannot lose.
|
|
@@ -19,6 +19,9 @@ pub const MAX_LOG_BYTES: usize = 4 << 10;
|
|
|
19
19
|
pub const MAX_EMIT_BYTES: usize = 70 + crate::emit::MAX_PAYLOAD;
|
|
20
20
|
/// The largest snapshot a hub may persist. A larger one fails to save, and the last one stands.
|
|
21
21
|
pub const MAX_SNAPSHOT_BYTES: usize = 8 << 20;
|
|
22
|
+
/// The most JSON a hub's [`crate::Hub::on_player_export`] may answer. A larger answer is an
|
|
23
|
+
/// error, and the export records the hub as failed.
|
|
24
|
+
pub const MAX_EXPORT_BYTES: usize = 1 << 20;
|
|
22
25
|
/// The largest module an app's node type may deploy.
|
|
23
26
|
pub const ARTIFACT_MAX_BYTES: usize = 16 << 20;
|
|
24
27
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
//! Typed node API operations: the platform's world, grid, player, permission and access data,
|
|
2
|
-
//! always within this instance's app. Each needs a scope its node type
|
|
3
|
-
//! (`"scopes": ["world.read", ...]`); without it the call fails with
|
|
2
|
+
//! and safety reports, always within this instance's app. Each needs a scope its node type
|
|
3
|
+
//! declares in the manifest (`"scopes": ["world.read", ...]`); without it the call fails with
|
|
4
|
+
//! [`Status::Denied`].
|
|
4
5
|
//!
|
|
5
6
|
//! | Scope | Operations |
|
|
6
7
|
//! |---|---|
|
|
7
|
-
//! | `players.read` | [`Players::get`], [`Players::features`], [`Players::check_permission`], [`Players::state`], [`Players::avatar_state`] |
|
|
8
|
+
//! | `players.read` | [`Players::get`], [`Players::features`], [`Players::check_permission`], [`Players::state`], [`Players::avatar_state`], [`Players::blocked`] |
|
|
8
9
|
//! | `players.write` | [`Players::set_state`] |
|
|
9
10
|
//! | `world.read` | [`World::chunk`], [`World::voxels`], [`World::actors`], [`World::actors_near`] |
|
|
10
11
|
//! | `world.write` | [`World::set_voxels`], [`World::set_chunk_state`] |
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
//! | `permissions.write` | [`Permissions::grant`], [`Permissions::revoke`] |
|
|
14
15
|
//! | `access.read` | [`Access::get`] |
|
|
15
16
|
//! | `access.write` | [`Access::grant`], [`Access::revoke`], [`Access::suspend`], [`Access::unsuspend`] |
|
|
17
|
+
//! | `safety.write` | [`Safety::report`] |
|
|
16
18
|
//!
|
|
17
19
|
//! The game API rate limits each app (reads and writes separately); a refusal is
|
|
18
20
|
//! [`Status::RateLimited`].
|
|
@@ -31,6 +33,8 @@ pub const MAX_VOXEL_WRITES: usize = 16;
|
|
|
31
33
|
pub const MAX_VOXEL_STATE: usize = 1024;
|
|
32
34
|
/// A chunk's state bytes ([`World::set_chunk_state`]).
|
|
33
35
|
pub const MAX_CHUNK_STATE: usize = 65_536;
|
|
36
|
+
/// A safety report's details, in characters as the game API counts them (UTF-16 units).
|
|
37
|
+
pub const MAX_REPORT_DETAILS: usize = 1_000;
|
|
34
38
|
|
|
35
39
|
/// A chunk's coordinates.
|
|
36
40
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
@@ -164,6 +168,85 @@ pub struct Player {
|
|
|
164
168
|
/// What tells players with the same gamertag apart; most players have none.
|
|
165
169
|
#[serde(default)]
|
|
166
170
|
pub disambiguation: Option<String>,
|
|
171
|
+
/// The player's age bracket; `None` from a platform that does not say.
|
|
172
|
+
#[serde(default, alias = "ageBracket")]
|
|
173
|
+
pub age_bracket: Option<AgeBracket>,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// How old a player is, as a bracket: never their age itself.
|
|
177
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
178
|
+
pub enum AgeBracket {
|
|
179
|
+
/// No age is known: the platform refuses such a player a token, so a hub rarely sees one.
|
|
180
|
+
Unknown,
|
|
181
|
+
Under13,
|
|
182
|
+
From13To15,
|
|
183
|
+
From16To17,
|
|
184
|
+
Adult,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
impl AgeBracket {
|
|
188
|
+
/// As the platform writes it: `unknown`, `under_13`, `13_15`, `16_17` or `18_plus`.
|
|
189
|
+
pub fn as_str(self) -> &'static str {
|
|
190
|
+
match self {
|
|
191
|
+
AgeBracket::Unknown => "unknown",
|
|
192
|
+
AgeBracket::Under13 => "under_13",
|
|
193
|
+
AgeBracket::From13To15 => "13_15",
|
|
194
|
+
AgeBracket::From16To17 => "16_17",
|
|
195
|
+
AgeBracket::Adult => "18_plus",
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// 13 to 17: the platform holds the player to minors' defaults.
|
|
200
|
+
pub fn is_minor(self) -> bool {
|
|
201
|
+
matches!(self, AgeBracket::From13To15 | AgeBracket::From16To17)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// A bracket this SDK does not know reads as [`AgeBracket::Unknown`], the most guarded.
|
|
206
|
+
impl<'de> Deserialize<'de> for AgeBracket {
|
|
207
|
+
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
|
208
|
+
let s = String::deserialize(d)?;
|
|
209
|
+
let known = [AgeBracket::Under13, AgeBracket::From13To15, AgeBracket::From16To17, AgeBracket::Adult];
|
|
210
|
+
Ok(known.into_iter().find(|b| b.as_str() == s).unwrap_or(AgeBracket::Unknown))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Why a player is reported ([`SafetyReport::reason`]).
|
|
215
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
216
|
+
#[serde(rename_all = "snake_case")]
|
|
217
|
+
pub enum ReportReason {
|
|
218
|
+
ChildSafety,
|
|
219
|
+
Harassment,
|
|
220
|
+
SexualContent,
|
|
221
|
+
Hate,
|
|
222
|
+
SelfHarm,
|
|
223
|
+
Cheating,
|
|
224
|
+
Other,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// A safety report a hub files for one player about another ([`Safety::report`]).
|
|
228
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
229
|
+
pub struct SafetyReport {
|
|
230
|
+
/// The player reporting.
|
|
231
|
+
pub reporter: u64,
|
|
232
|
+
/// The player reported.
|
|
233
|
+
pub reported: u64,
|
|
234
|
+
/// The reported player's actor (its 32-character id), when the report is about what it
|
|
235
|
+
/// did in the world.
|
|
236
|
+
pub reported_actor: Option<String>,
|
|
237
|
+
pub reason: ReportReason,
|
|
238
|
+
/// What happened, in the reporter's words: at most [`MAX_REPORT_DETAILS`] characters.
|
|
239
|
+
pub details: String,
|
|
240
|
+
/// When it happened, in Unix milliseconds.
|
|
241
|
+
pub occurred_at_ms: u64,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// A report the platform recorded.
|
|
245
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
|
246
|
+
pub struct FiledReport {
|
|
247
|
+
/// What moderators know it by.
|
|
248
|
+
#[serde(deserialize_with = "de_u64", alias = "reportId")]
|
|
249
|
+
pub report_id: u64,
|
|
167
250
|
}
|
|
168
251
|
|
|
169
252
|
/// A player's access to the app, from [`Access::get`] and [`Access::grant`].
|
|
@@ -201,6 +284,21 @@ fn b64(bytes: &[u8]) -> String {
|
|
|
201
284
|
B64.encode(bytes)
|
|
202
285
|
}
|
|
203
286
|
|
|
287
|
+
/// Unix milliseconds as the game API writes a time: `2026-01-01T00:00:00.000Z`.
|
|
288
|
+
pub(crate) fn rfc3339(ms: u64) -> String {
|
|
289
|
+
let (secs, millis) = (ms / 1000, ms % 1000);
|
|
290
|
+
let (days, rem) = ((secs / 86_400) as i64, secs % 86_400);
|
|
291
|
+
let z = days + 719_468;
|
|
292
|
+
let (era, doe) = (z.div_euclid(146_097), z.rem_euclid(146_097));
|
|
293
|
+
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
|
294
|
+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
295
|
+
let mp = (5 * doy + 2) / 153;
|
|
296
|
+
let day = doy - (153 * mp + 2) / 5 + 1;
|
|
297
|
+
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
298
|
+
let year = yoe + era * 400 + i64::from(month <= 2);
|
|
299
|
+
format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{millis:03}Z", rem / 3600, rem % 3600 / 60, rem % 60)
|
|
300
|
+
}
|
|
301
|
+
|
|
204
302
|
// ---- wire shapes ----
|
|
205
303
|
|
|
206
304
|
#[derive(Deserialize)]
|
|
@@ -585,6 +683,19 @@ impl Players<'_> {
|
|
|
585
683
|
self.state_op("players.avatar_state", json!({ "avatar": avatar.to_string() }))
|
|
586
684
|
}
|
|
587
685
|
|
|
686
|
+
/// Whether either player has blocked the other in this app: check it before mail, friend
|
|
687
|
+
/// requests, trades, duels or invites pass between them, and before the hub's own events
|
|
688
|
+
/// carry one's words to the other (the realtime servers pass what a hub emits, blocks or
|
|
689
|
+
/// not). Scope `players.read`.
|
|
690
|
+
pub fn blocked(&self, player: u64, other: u64) -> Result<bool, CallError> {
|
|
691
|
+
#[derive(Deserialize)]
|
|
692
|
+
struct R {
|
|
693
|
+
blocked: bool,
|
|
694
|
+
}
|
|
695
|
+
let r: R = op(self.0, "players.blocked", json!({ "player": player.to_string(), "other": other.to_string() }))?;
|
|
696
|
+
Ok(r.blocked)
|
|
697
|
+
}
|
|
698
|
+
|
|
588
699
|
fn state_op(&self, name: &str, body: Value) -> Result<Option<Vec<u8>>, CallError> {
|
|
589
700
|
#[derive(Deserialize)]
|
|
590
701
|
struct R {
|
|
@@ -678,6 +789,31 @@ impl Access<'_> {
|
|
|
678
789
|
}
|
|
679
790
|
}
|
|
680
791
|
|
|
792
|
+
/// Safety reports: [`Ctx::safety`].
|
|
793
|
+
pub struct Safety<'a>(pub(crate) &'a Ctx);
|
|
794
|
+
|
|
795
|
+
impl Safety<'_> {
|
|
796
|
+
/// Files `report` as its reporter would from the game, both players being the app's:
|
|
797
|
+
/// moderators review it with what the app's safety log recorded of both around when it
|
|
798
|
+
/// happened. Scope `safety.write`.
|
|
799
|
+
pub fn report(&self, report: &SafetyReport) -> Result<FiledReport, CallError> {
|
|
800
|
+
if report.details.encode_utf16().count() > MAX_REPORT_DETAILS {
|
|
801
|
+
return Err(bad_request(format!("a report's details are at most {MAX_REPORT_DETAILS} characters")));
|
|
802
|
+
}
|
|
803
|
+
let mut body = json!({
|
|
804
|
+
"reporter": report.reporter.to_string(),
|
|
805
|
+
"reported": report.reported.to_string(),
|
|
806
|
+
"reason": report.reason,
|
|
807
|
+
"details": report.details,
|
|
808
|
+
"occurred_at": rfc3339(report.occurred_at_ms),
|
|
809
|
+
});
|
|
810
|
+
if let Some(actor) = &report.reported_actor {
|
|
811
|
+
body["reported_actor"] = json!(actor);
|
|
812
|
+
}
|
|
813
|
+
op(self.0, "safety.report", body)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
681
817
|
#[cfg(test)]
|
|
682
818
|
mod tests {
|
|
683
819
|
use std::cell::RefCell;
|
|
@@ -769,13 +905,98 @@ mod tests {
|
|
|
769
905
|
let reply = json!({ "player": "42", "gamertag": "Ada", "disambiguation": null });
|
|
770
906
|
let (got, name, body) = exchange(reply, |c| c.players().get(42));
|
|
771
907
|
assert_eq!((name.as_str(), body), ("players.get", json!({ "player": "42" })));
|
|
772
|
-
assert_eq!(got.unwrap(), Player { player: 42, gamertag: "Ada".into(), disambiguation: None });
|
|
908
|
+
assert_eq!(got.unwrap(), Player { player: 42, gamertag: "Ada".into(), disambiguation: None, age_bracket: None });
|
|
773
909
|
let (got, ..) = exchange(json!({ "player": 42, "gamertag": "Ada" }), |c| c.players().get(42));
|
|
774
910
|
assert_eq!(got.unwrap().disambiguation, None, "a platform that leaves it out");
|
|
775
911
|
let (got, ..) = exchange(json!({ "player": "42", "gamertag": "Ada", "disambiguation": "0001" }), |c| c.players().get(42));
|
|
776
912
|
assert_eq!(got.unwrap().disambiguation.as_deref(), Some("0001"));
|
|
777
913
|
}
|
|
778
914
|
|
|
915
|
+
#[test]
|
|
916
|
+
fn a_player_names_their_age_bracket_and_an_older_platform_none() {
|
|
917
|
+
let get = |reply: Value| exchange(reply, |c| c.players().get(42)).0.unwrap().age_bracket;
|
|
918
|
+
assert_eq!(get(json!({ "player": "42", "gamertag": "Ada", "disambiguation": null })), None, "an older ck-api");
|
|
919
|
+
for (wire, bracket) in [
|
|
920
|
+
("unknown", AgeBracket::Unknown),
|
|
921
|
+
("under_13", AgeBracket::Under13),
|
|
922
|
+
("13_15", AgeBracket::From13To15),
|
|
923
|
+
("16_17", AgeBracket::From16To17),
|
|
924
|
+
("18_plus", AgeBracket::Adult),
|
|
925
|
+
] {
|
|
926
|
+
assert_eq!(get(json!({ "player": "42", "gamertag": "Ada", "age_bracket": wire })), Some(bracket), "{wire}");
|
|
927
|
+
assert_eq!(bracket.as_str(), wire);
|
|
928
|
+
}
|
|
929
|
+
assert_eq!(get(json!({ "player": "42", "gamertag": "Ada", "ageBracket": "16_17" })), Some(AgeBracket::From16To17));
|
|
930
|
+
assert_eq!(get(json!({ "player": "42", "gamertag": "Ada", "age_bracket": null })), None);
|
|
931
|
+
assert_eq!(
|
|
932
|
+
get(json!({ "player": "42", "gamertag": "Ada", "age_bracket": "21_plus" })),
|
|
933
|
+
Some(AgeBracket::Unknown),
|
|
934
|
+
"one it does not know"
|
|
935
|
+
);
|
|
936
|
+
assert!(AgeBracket::From13To15.is_minor() && AgeBracket::From16To17.is_minor());
|
|
937
|
+
assert!(!AgeBracket::Adult.is_minor() && !AgeBracket::Unknown.is_minor() && !AgeBracket::Under13.is_minor());
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
#[test]
|
|
941
|
+
fn whether_either_player_blocked_the_other() {
|
|
942
|
+
let (got, name, body) = exchange(json!({ "blocked": true }), |c| c.players().blocked(42, u64::MAX));
|
|
943
|
+
assert!(got.unwrap());
|
|
944
|
+
assert_eq!((name.as_str(), body), ("players.blocked", json!({ "player": "42", "other": "18446744073709551615" })));
|
|
945
|
+
let (got, ..) = exchange(json!({ "blocked": false }), |c| c.players().blocked(42, 43));
|
|
946
|
+
assert!(!got.unwrap());
|
|
947
|
+
let (got, ..) = exchange(json!({}), |c| c.players().blocked(42, 43));
|
|
948
|
+
assert_eq!(got.unwrap_err().status, Status::Internal, "a reply without the answer");
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
#[test]
|
|
952
|
+
fn a_safety_report_sends_what_the_game_api_reads_and_its_id_comes_back() {
|
|
953
|
+
let report = SafetyReport {
|
|
954
|
+
reporter: 42,
|
|
955
|
+
reported: u64::MAX,
|
|
956
|
+
reported_actor: Some("0123456789abcdef0123456789abcdef".into()),
|
|
957
|
+
reason: ReportReason::SexualContent,
|
|
958
|
+
details: "asked for pictures".into(),
|
|
959
|
+
occurred_at_ms: 951_782_400_123,
|
|
960
|
+
};
|
|
961
|
+
let (got, name, body) = exchange(json!({ "report_id": "77" }), |c| c.safety().report(&report));
|
|
962
|
+
assert_eq!(got.unwrap(), FiledReport { report_id: 77 });
|
|
963
|
+
assert_eq!(name, "safety.report");
|
|
964
|
+
assert_eq!(
|
|
965
|
+
body,
|
|
966
|
+
json!({ "reporter": "42", "reported": "18446744073709551615", "reported_actor": "0123456789abcdef0123456789abcdef",
|
|
967
|
+
"reason": "sexual_content", "details": "asked for pictures", "occurred_at": "2000-02-29T00:00:00.123Z" })
|
|
968
|
+
);
|
|
969
|
+
let no_actor = SafetyReport { reported_actor: None, reason: ReportReason::ChildSafety, ..report.clone() };
|
|
970
|
+
let (got, _, body) = exchange(json!({ "report_id": 78 }), |c| c.safety().report(&no_actor));
|
|
971
|
+
assert_eq!(got.unwrap().report_id, 78);
|
|
972
|
+
assert!(body.get("reported_actor").is_none(), "left out when there is none");
|
|
973
|
+
assert_eq!(body["reason"], "child_safety");
|
|
974
|
+
let (got, ..) = exchange(json!({ "reportId": "79" }), |c| c.safety().report(&no_actor));
|
|
975
|
+
assert_eq!(got.unwrap().report_id, 79);
|
|
976
|
+
for (reason, wire) in [
|
|
977
|
+
(ReportReason::Harassment, "harassment"),
|
|
978
|
+
(ReportReason::Hate, "hate"),
|
|
979
|
+
(ReportReason::SelfHarm, "self_harm"),
|
|
980
|
+
(ReportReason::Cheating, "cheating"),
|
|
981
|
+
(ReportReason::Other, "other"),
|
|
982
|
+
] {
|
|
983
|
+
assert_eq!(serde_json::to_value(reason).unwrap(), wire);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
let at_cap = SafetyReport { details: "\u{1F600}".repeat(MAX_REPORT_DETAILS / 2), ..no_actor.clone() };
|
|
987
|
+
let (got, ..) = exchange(json!({ "report_id": "80" }), |c| c.safety().report(&at_cap));
|
|
988
|
+
assert!(got.is_ok(), "the cap itself is allowed");
|
|
989
|
+
let over = SafetyReport { details: format!("{}a", "\u{1F600}".repeat(MAX_REPORT_DETAILS / 2)), ..no_actor };
|
|
990
|
+
assert_eq!(refused_here(|c| c.safety().report(&over)).status, Status::BadRequest, "counted as the game API counts");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
#[test]
|
|
994
|
+
fn writes_times_as_the_game_api_does() {
|
|
995
|
+
assert_eq!(rfc3339(1_767_225_600_000), "2026-01-01T00:00:00.000Z");
|
|
996
|
+
assert_eq!(rfc3339(951_782_400_123), "2000-02-29T00:00:00.123Z");
|
|
997
|
+
assert_eq!(rfc3339(0), "1970-01-01T00:00:00.000Z");
|
|
998
|
+
}
|
|
999
|
+
|
|
779
1000
|
#[test]
|
|
780
1001
|
fn voxels_are_signed_and_name_their_author_and_time() {
|
|
781
1002
|
let reply = json!({ "voxels": [
|
package/crates/ckx-sdk/src/rt.rs
CHANGED
|
@@ -298,12 +298,27 @@ impl<T: Hub> HubNode<T> {
|
|
|
298
298
|
};
|
|
299
299
|
self.0.on_session(ctx, p, session)?
|
|
300
300
|
}
|
|
301
|
+
(ERASE, Caller::Platform) => self.0.on_player_erased(ctx, decode::<PlayerEvent>(call.payload)?.player)?,
|
|
302
|
+
(EXPORT, Caller::Platform) => {
|
|
303
|
+
let held = self.0.on_player_export(ctx, decode::<PlayerEvent>(call.payload)?.player)?;
|
|
304
|
+
let json = serde_json::to_vec(&held).map_err(|e| Error(format!("export: {e}")))?;
|
|
305
|
+
if json.len() > crate::limits::MAX_EXPORT_BYTES {
|
|
306
|
+
return Err(Error(format!("the export of {} bytes exceeds {}", json.len(), crate::limits::MAX_EXPORT_BYTES)));
|
|
307
|
+
}
|
|
308
|
+
return Ok(json);
|
|
309
|
+
}
|
|
301
310
|
(m, c) => return Err(Error(format!("no platform event `{m}` from {c:?}"))),
|
|
302
311
|
}
|
|
303
312
|
Ok(Vec::new())
|
|
304
313
|
}
|
|
305
314
|
}
|
|
306
315
|
|
|
316
|
+
/// The payload of `$erase` and `$export`.
|
|
317
|
+
#[derive(Serialize, Deserialize)]
|
|
318
|
+
pub(crate) struct PlayerEvent {
|
|
319
|
+
pub player: u64,
|
|
320
|
+
}
|
|
321
|
+
|
|
307
322
|
/// The platform's reserved method names: its events.
|
|
308
323
|
pub(crate) mod ckx_platform {
|
|
309
324
|
pub const TIMER: &str = "$timer";
|
|
@@ -313,6 +328,8 @@ pub(crate) mod ckx_platform {
|
|
|
313
328
|
pub const SESSION: &str = "$session";
|
|
314
329
|
pub const SESSION_JOINED: u8 = 1;
|
|
315
330
|
pub const SESSION_LEFT: u8 = 0;
|
|
331
|
+
pub const ERASE: &str = "$erase";
|
|
332
|
+
pub const EXPORT: &str = "$export";
|
|
316
333
|
}
|
|
317
334
|
|
|
318
335
|
impl<T: Hub> Node for HubNode<T> {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! [`TestHub`] and [`TestSpoke`] run a node on the test's thread the way a sandbox runs it:
|
|
4
4
|
//! calls from players, developers and other instances; a hub's platform events (timers on a
|
|
5
|
-
//! clock the test moves, topics it watches, presence, sessions,
|
|
6
|
-
//! a snapshot and a start from it. The platform's side is a [`FakePlatform`]. It keeps what the
|
|
5
|
+
//! clock the test moves, topics it watches, presence, sessions, a mod's world events, and a
|
|
6
|
+
//! player's erasure and export); and a snapshot and a start from it. The platform's side is a [`FakePlatform`]. It keeps what the
|
|
7
7
|
//! node logged, published, sent and emitted; holds each call to the sandbox's bounds
|
|
8
8
|
//! ([`crate::limits`]); answers calls to other types and node API operations with what the test
|
|
9
9
|
//! scripts; and serves a mod its [`FakeGrid`] under the node API's rules for mods.
|
|
@@ -70,7 +70,7 @@ use serde::de::DeserializeOwned;
|
|
|
70
70
|
use serde_json::{Value, json};
|
|
71
71
|
|
|
72
72
|
use crate::abi::native::{self, Host};
|
|
73
|
-
use crate::node::{Actor, Chunk, ChunkPos, Voxel};
|
|
73
|
+
use crate::node::{Actor, Chunk, ChunkPos, Voxel, rfc3339};
|
|
74
74
|
use crate::rt::{HubNode, Node, SpokeNode, ckx_platform as platform};
|
|
75
75
|
use crate::{Call, CallError, Caller, Ctx, Error, Hub, Kind, Level, Presence, Session, Spoke, Status, WorldEvent, decode, encode, limits};
|
|
76
76
|
|
|
@@ -431,7 +431,7 @@ impl FakePlatform {
|
|
|
431
431
|
}
|
|
432
432
|
match caller {
|
|
433
433
|
Caller::Platform => {
|
|
434
|
-
Err(bad_request("the platform calls a node only with its events: advance, topic, presence, session, world"))
|
|
434
|
+
Err(bad_request("the platform calls a node only with its events: advance, topic, presence, session, world, erase, export"))
|
|
435
435
|
}
|
|
436
436
|
Caller::Instance(_) if self.state.borrow().is_mod() => Err(denied("only players call mods")),
|
|
437
437
|
_ => Ok(()),
|
|
@@ -696,6 +696,26 @@ impl<T: Hub> TestHub<T> {
|
|
|
696
696
|
self.event(Caller::Platform, platform::PRESENCE, &payload)
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
+
/// `player`'s account being erased, as the platform tells every hub: `$erase`, then a
|
|
700
|
+
/// snapshot, which the platform stores before it counts the hub done. A hub without the hook
|
|
701
|
+
/// answers `AppError` "not implemented: …".
|
|
702
|
+
pub fn erase(&mut self, player: u64) -> Result<(), CallError> {
|
|
703
|
+
let payload = player_payload(player).map_err(|Error(m)| bad_request(m))?;
|
|
704
|
+
self.event(Caller::Platform, platform::ERASE, &payload)?;
|
|
705
|
+
self.persist().map(|_| ())
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/// What the hub holds about `player`, as the platform asks it for their data export:
|
|
709
|
+
/// `Value::Null` when it holds nothing. A hub without the hook answers `AppError` "not
|
|
710
|
+
/// implemented: …".
|
|
711
|
+
pub fn export(&mut self, player: u64) -> Result<Value, CallError> {
|
|
712
|
+
let payload = player_payload(player).map_err(|Error(m)| bad_request(m))?;
|
|
713
|
+
let node = self.node.as_mut().expect("the hub runs");
|
|
714
|
+
let json = self.platform.handle(node, Caller::Platform, platform::EXPORT, &payload)?;
|
|
715
|
+
serde_json::from_slice(&json)
|
|
716
|
+
.map_err(|e| CallError { status: Status::Internal, message: format!("an export that is not JSON: {e}") })
|
|
717
|
+
}
|
|
718
|
+
|
|
699
719
|
/// `node_type`/`key` publishing `payload` on `topic`. A hub hears only the topics it
|
|
700
720
|
/// watches.
|
|
701
721
|
pub fn topic(&mut self, node_type: &str, key: &str, topic: &str, payload: &[u8]) -> Result<(), CallError> {
|
|
@@ -818,6 +838,12 @@ pub fn world_payload(events: &[WorldEvent]) -> crate::Result<Vec<u8>> {
|
|
|
818
838
|
encode(&Batch { events })
|
|
819
839
|
}
|
|
820
840
|
|
|
841
|
+
/// The payload of the `$erase` and `$export` calls the platform makes about `player`: what
|
|
842
|
+
/// [`TestHub::erase`] and [`TestHub::export`] hand a hub.
|
|
843
|
+
pub fn player_payload(player: u64) -> crate::Result<Vec<u8>> {
|
|
844
|
+
encode(&crate::rt::PlayerEvent { player })
|
|
845
|
+
}
|
|
846
|
+
|
|
821
847
|
// ---- the node API, as it serves a mod ----
|
|
822
848
|
|
|
823
849
|
/// The node API operations a mod may call.
|
|
@@ -825,12 +851,13 @@ const MOD_OPS: [&str; 7] =
|
|
|
825
851
|
["world.chunk", "world.voxels", "world.actors", "world.actors_radius", "world.set_voxels", "grids.get", "grids.check_permission"];
|
|
826
852
|
|
|
827
853
|
/// Each node API operation and the scope it needs.
|
|
828
|
-
const OP_SCOPES: [(&str, &str);
|
|
854
|
+
const OP_SCOPES: [(&str, &str); 29] = [
|
|
829
855
|
("players.get", "players.read"),
|
|
830
856
|
("players.features", "players.read"),
|
|
831
857
|
("players.check_permission", "players.read"),
|
|
832
858
|
("players.state", "players.read"),
|
|
833
859
|
("players.avatar_state", "players.read"),
|
|
860
|
+
("players.blocked", "players.read"),
|
|
834
861
|
("players.set_state", "players.write"),
|
|
835
862
|
("world.chunk", "world.read"),
|
|
836
863
|
("world.voxels", "world.read"),
|
|
@@ -853,6 +880,7 @@ const OP_SCOPES: [(&str, &str); 27] = [
|
|
|
853
880
|
("access.revoke", "access.write"),
|
|
854
881
|
("access.suspend", "access.write"),
|
|
855
882
|
("access.unsuspend", "access.write"),
|
|
883
|
+
("safety.report", "safety.write"),
|
|
856
884
|
];
|
|
857
885
|
|
|
858
886
|
/// Rows the node API returns at most: actors in one chunk, in a radius, voxels in a chunk.
|
|
@@ -954,21 +982,6 @@ fn b64(bytes: &[u8]) -> String {
|
|
|
954
982
|
B64.encode(bytes)
|
|
955
983
|
}
|
|
956
984
|
|
|
957
|
-
/// Unix milliseconds as the game API writes a time: `2026-01-01T00:00:00.000Z`.
|
|
958
|
-
fn rfc3339(ms: u64) -> String {
|
|
959
|
-
let (secs, millis) = (ms / 1000, ms % 1000);
|
|
960
|
-
let (days, rem) = ((secs / 86_400) as i64, secs % 86_400);
|
|
961
|
-
let z = days + 719_468;
|
|
962
|
-
let (era, doe) = (z.div_euclid(146_097), z.rem_euclid(146_097));
|
|
963
|
-
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
|
964
|
-
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
965
|
-
let mp = (5 * doy + 2) / 153;
|
|
966
|
-
let day = doy - (153 * mp + 2) / 5 + 1;
|
|
967
|
-
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
968
|
-
let year = yoe + era * 400 + i64::from(month <= 2);
|
|
969
|
-
format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{millis:03}Z", rem / 3600, rem % 3600 / 60, rem % 60)
|
|
970
|
-
}
|
|
971
|
-
|
|
972
985
|
type Pos = (i64, i64, i64);
|
|
973
986
|
|
|
974
987
|
fn key(p: ChunkPos) -> Pos {
|
|
@@ -1424,6 +1437,85 @@ mod tests {
|
|
|
1424
1437
|
assert_eq!(hub.call(Caller::Player(1), "nope", &[]).unwrap_err().status, Status::AppError);
|
|
1425
1438
|
}
|
|
1426
1439
|
|
|
1440
|
+
/// Scores by player: what the platform's erasure and export reach.
|
|
1441
|
+
#[derive(Default, Serialize, Deserialize)]
|
|
1442
|
+
struct Keeper {
|
|
1443
|
+
scores: std::collections::BTreeMap<u64, i64>,
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
impl Hub for Keeper {
|
|
1447
|
+
fn spawn(_ctx: &Ctx, _seed: &[u8]) -> Result<Self> {
|
|
1448
|
+
Ok(Self::default())
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
fn load(_ctx: &Ctx, snapshot: &[u8], _from_version: u64) -> Result<Self> {
|
|
1452
|
+
decode(snapshot)
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
fn persist(&mut self, _ctx: &Ctx) -> Result<Vec<u8>> {
|
|
1456
|
+
encode(self)
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
fn handle(&mut self, _ctx: &Ctx, call: Call<'_>) -> Result<Vec<u8>> {
|
|
1460
|
+
match call.method {
|
|
1461
|
+
"score" => {
|
|
1462
|
+
*self.scores.entry(call.player()?).or_default() += call.decode::<i64>()?;
|
|
1463
|
+
Ok(Vec::new())
|
|
1464
|
+
}
|
|
1465
|
+
"flood" => {
|
|
1466
|
+
self.scores.extend((0..60_000).map(|p| (p, i64::MAX)));
|
|
1467
|
+
Ok(Vec::new())
|
|
1468
|
+
}
|
|
1469
|
+
other => Err(Error::unknown_method(other)),
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
fn on_player_erased(&mut self, _ctx: &Ctx, player: u64) -> Result<()> {
|
|
1474
|
+
self.scores.remove(&player);
|
|
1475
|
+
Ok(())
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
fn on_player_export(&mut self, _ctx: &Ctx, player: u64) -> Result<Value> {
|
|
1479
|
+
if player == 7 {
|
|
1480
|
+
return Ok(json!({ "everyone": self.scores }));
|
|
1481
|
+
}
|
|
1482
|
+
Ok(self.scores.get(&player).map_or(Value::Null, |s| json!({ "score": s })))
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
#[test]
|
|
1487
|
+
fn a_hub_erases_and_exports_a_player_as_the_platform_asks() {
|
|
1488
|
+
let mut hub = TestHub::<Keeper>::spawn("board", "a").unwrap();
|
|
1489
|
+
hub.call(Caller::Player(42), "score", &encode(&5).unwrap()).unwrap();
|
|
1490
|
+
hub.call(Caller::Player(43), "score", &encode(&9).unwrap()).unwrap();
|
|
1491
|
+
assert_eq!(hub.export(42).unwrap(), json!({ "score": 5 }));
|
|
1492
|
+
assert_eq!(hub.export(44).unwrap(), Value::Null, "nothing held");
|
|
1493
|
+
hub.erase(42).unwrap();
|
|
1494
|
+
assert_eq!(hub.export(42).unwrap(), Value::Null);
|
|
1495
|
+
hub.restart().unwrap();
|
|
1496
|
+
assert_eq!(hub.hub().scores.keys().copied().collect::<Vec<_>>(), vec![43], "the erasure is in the snapshot");
|
|
1497
|
+
for method in ["$erase", "$export"] {
|
|
1498
|
+
for caller in [Caller::Player(42), Caller::Developer(1), Caller::Instance(9)] {
|
|
1499
|
+
let e = hub.call(caller, method, &player_payload(43).unwrap()).unwrap_err();
|
|
1500
|
+
assert_eq!(e.status, Status::Denied, "{method} from {caller:?}");
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
assert_eq!(hub.hub().scores.len(), 1);
|
|
1504
|
+
|
|
1505
|
+
hub.call(Caller::Player(1), "flood", &[]).unwrap();
|
|
1506
|
+
let e = hub.export(7).unwrap_err();
|
|
1507
|
+
assert!(e.status == Status::AppError && e.message.contains("exceeds"), "{e}");
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
#[test]
|
|
1511
|
+
fn a_hub_without_the_hooks_answers_not_implemented() {
|
|
1512
|
+
let mut hub = TestHub::<Ticker>::spawn("ticker", "a").unwrap();
|
|
1513
|
+
for e in [hub.erase(42).unwrap_err(), hub.export(42).unwrap_err()] {
|
|
1514
|
+
assert_eq!(e.status, Status::AppError);
|
|
1515
|
+
assert!(e.message.starts_with("not implemented: `on_player_e"), "{e}");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1427
1519
|
#[test]
|
|
1428
1520
|
fn one_node_runs_on_a_thread_at_a_time() {
|
|
1429
1521
|
let hub = TestHub::<Ticker>::spawn("ticker", "a").unwrap();
|
|
@@ -1432,11 +1524,4 @@ mod tests {
|
|
|
1432
1524
|
drop(hub);
|
|
1433
1525
|
TestHub::<Ticker>::spawn("ticker", "b").unwrap();
|
|
1434
1526
|
}
|
|
1435
|
-
|
|
1436
|
-
#[test]
|
|
1437
|
-
fn writes_times_as_the_game_api_does() {
|
|
1438
|
-
assert_eq!(rfc3339(START_MS), "2026-01-01T00:00:00.000Z");
|
|
1439
|
-
assert_eq!(rfc3339(951_782_400_123), "2000-02-29T00:00:00.123Z");
|
|
1440
|
-
assert_eq!(rfc3339(0), "1970-01-01T00:00:00.000Z");
|
|
1441
|
-
}
|
|
1442
1527
|
}
|
|
@@ -13,6 +13,7 @@ exports.EXEC_ARTIFACT_MAX_BYTES = exports.EXEC_ABI_EXPORTS = exports.EXEC_ABI_IM
|
|
|
13
13
|
exports.validateExecPath = validateExecPath;
|
|
14
14
|
exports.validateExecManifest = validateExecManifest;
|
|
15
15
|
exports.validateExecCrates = validateExecCrates;
|
|
16
|
+
exports.execArtifactName = execArtifactName;
|
|
16
17
|
exports.rewriteExecManifest = rewriteExecManifest;
|
|
17
18
|
exports.clientTickIntervalMs = clientTickIntervalMs;
|
|
18
19
|
exports.validateExecArtifact = validateExecArtifact;
|
|
@@ -259,6 +260,39 @@ function validateExecCrates(crates, kind = 'exec') {
|
|
|
259
260
|
if (total > exports.EXEC_MAX_SOURCE_BYTES)
|
|
260
261
|
fail(`a build's sources are at most ${exports.EXEC_MAX_SOURCE_BYTES} bytes`);
|
|
261
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The file name cargo gives a validated crate's module: its `[lib] name`, else its package
|
|
265
|
+
* name with `-` as `_`, then `.wasm`. A build's crates share one target directory, so the
|
|
266
|
+
* module has to be found by name rather than as the only `.wasm` there.
|
|
267
|
+
*/
|
|
268
|
+
function execArtifactName(manifest) {
|
|
269
|
+
let section = '';
|
|
270
|
+
let pkg = null;
|
|
271
|
+
let lib = null;
|
|
272
|
+
for (const raw of manifest.split(/\r?\n/)) {
|
|
273
|
+
const line = stripComment(raw).trim();
|
|
274
|
+
const header = line.match(/^\[([^\]]+)\]$/);
|
|
275
|
+
if (header) {
|
|
276
|
+
section = header[1].trim();
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const kv = line.match(/^name\s*=\s*"([^"]+)"$/);
|
|
280
|
+
if (!kv)
|
|
281
|
+
continue;
|
|
282
|
+
if (section === 'package')
|
|
283
|
+
pkg = kv[1];
|
|
284
|
+
if (section === 'lib')
|
|
285
|
+
lib = kv[1];
|
|
286
|
+
}
|
|
287
|
+
const name = lib ?? pkg;
|
|
288
|
+
if (!name)
|
|
289
|
+
fail('Cargo.toml has no [package] name');
|
|
290
|
+
// The name becomes a file name under the build's target directory: a cargo identifier and
|
|
291
|
+
// nothing a path could be made of, before cargo itself would refuse it.
|
|
292
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name))
|
|
293
|
+
fail(`Cargo.toml name '${name}' is not a crate name`);
|
|
294
|
+
return `${name.replace(/-/g, '_')}.wasm`;
|
|
295
|
+
}
|
|
262
296
|
/**
|
|
263
297
|
* Points the kind's SDK at the toolchain's pinned copy and appends the platform release
|
|
264
298
|
* profile (a crate may not declare its own; the validator refuses [profile]).
|
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.
|
|
4
|
-
toolchain_commit=
|
|
5
|
-
sdk_commit=
|
|
6
|
-
ckx_sdk_version=0.
|
|
3
|
+
toolchain_version=v1.16.0
|
|
4
|
+
toolchain_commit=9423aa5f66b5480f2aa19110b6888d3e04c4534d
|
|
5
|
+
sdk_commit=27d5774d7b9d3b3c28928ad1a4d27902ac5130e1
|
|
6
|
+
ckx_sdk_version=0.13.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.
|
|
3
|
+
"version": "0.6.0-test.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",
|
package/starters/deck/Cargo.toml
CHANGED