workshop_rs/gameplay/
data.rs1use sha2::{Digest, Sha256};
8use std::sync::OnceLock;
9
10use crate::gameplay::{GameplayCatalog, GameplayDataError, GameplayDatasetIdentity, Hero};
11
12pub const GAMEPLAY_DATA: &str = include_str!("gameplay.json");
14
15const SCHEMA_VERSION: u32 = 2;
16
17#[derive(serde::Deserialize)]
18#[serde(rename_all = "camelCase")]
19struct GameplaySchema {
20 schema_version: u32,
21}
22
23#[derive(serde::Deserialize)]
24#[serde(rename_all = "camelCase")]
25struct GameplayFile {
26 schema_version: u32,
27 identity: GameplayDatasetIdentity,
28 heroes: Vec<Hero>,
29}
30
31fn probe_schema_version(json: &str) -> Result<u32, GameplayDataError> {
32 let schema: GameplaySchema = serde_json::from_str(json)
33 .map_err(|error| GameplayDataError::Malformed(error.to_string()))?;
34 if schema.schema_version != SCHEMA_VERSION {
35 return Err(GameplayDataError::UnsupportedSchema(schema.schema_version));
36 }
37 Ok(schema.schema_version)
38}
39
40pub fn load(json: &str) -> Result<GameplayCatalog, GameplayDataError> {
42 let schema_version = probe_schema_version(json)?;
43
44 let file: GameplayFile = serde_json::from_str(json)
45 .map_err(|error| GameplayDataError::Malformed(error.to_string()))?;
46 if file.schema_version != schema_version {
47 return Err(GameplayDataError::UnsupportedSchema(file.schema_version));
48 }
49
50 let computed = content_digest(json)?;
51 if file.identity.digest != computed {
52 return Err(GameplayDataError::DigestMismatch {
53 declared: file.identity.digest,
54 computed,
55 });
56 }
57
58 GameplayCatalog::new(file.identity, file.heroes)
59}
60
61pub fn builtin() -> Result<GameplayCatalog, GameplayDataError> {
63 builtin_ref().cloned().map_err(Clone::clone)
64}
65
66pub fn builtin_ref() -> Result<&'static GameplayCatalog, &'static GameplayDataError> {
68 static DATA: OnceLock<Result<GameplayCatalog, GameplayDataError>> = OnceLock::new();
69 match DATA.get_or_init(|| load(GAMEPLAY_DATA)) {
70 Ok(catalog) => Ok(catalog),
71 Err(error) => Err(error),
72 }
73}
74
75pub fn content_digest(json: &str) -> Result<String, GameplayDataError> {
81 let mut value: serde_json::Value = serde_json::from_str(json)
82 .map_err(|error| GameplayDataError::Malformed(error.to_string()))?;
83 let identity = value
84 .as_object_mut()
85 .and_then(|root| root.get_mut("identity"))
86 .and_then(serde_json::Value::as_object_mut)
87 .ok_or_else(|| GameplayDataError::Malformed("missing identity object".to_string()))?;
88 identity
89 .remove("digest")
90 .ok_or_else(|| GameplayDataError::Malformed("missing identity.digest".to_string()))?;
91
92 let canonical = serde_json::to_string(&value)
93 .map_err(|error| GameplayDataError::Malformed(error.to_string()))?;
94 Ok(format!("sha256:{:x}", Sha256::digest(canonical.as_bytes())))
95}