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!("data/gameplay.json");
14
15const SCHEMA_VERSION: u32 = 1;
16
17#[derive(serde::Deserialize)]
18#[serde(rename_all = "camelCase")]
19struct GameplayFile {
20 schema_version: u32,
21 identity: GameplayDatasetIdentity,
22 heroes: Vec<Hero>,
23}
24
25pub fn load(json: &str) -> Result<GameplayCatalog, GameplayDataError> {
27 let file: GameplayFile = serde_json::from_str(json)
28 .map_err(|error| GameplayDataError::Malformed(error.to_string()))?;
29 if file.schema_version != SCHEMA_VERSION {
30 return Err(GameplayDataError::UnsupportedSchema(file.schema_version));
31 }
32
33 let computed = content_digest(json)?;
34 if file.identity.digest != computed {
35 return Err(GameplayDataError::DigestMismatch {
36 declared: file.identity.digest,
37 computed,
38 });
39 }
40
41 GameplayCatalog::new(file.identity, file.heroes)
42}
43
44pub fn builtin() -> Result<GameplayCatalog, GameplayDataError> {
46 builtin_ref().cloned().map_err(Clone::clone)
47}
48
49pub(crate) fn builtin_ref() -> Result<&'static GameplayCatalog, &'static GameplayDataError> {
51 static DATA: OnceLock<Result<GameplayCatalog, GameplayDataError>> = OnceLock::new();
52 match DATA.get_or_init(|| load(GAMEPLAY_DATA)) {
53 Ok(catalog) => Ok(catalog),
54 Err(error) => Err(error),
55 }
56}
57
58pub fn content_digest(json: &str) -> Result<String, GameplayDataError> {
64 let mut value: serde_json::Value = serde_json::from_str(json)
65 .map_err(|error| GameplayDataError::Malformed(error.to_string()))?;
66 let identity = value
67 .as_object_mut()
68 .and_then(|root| root.get_mut("identity"))
69 .and_then(serde_json::Value::as_object_mut)
70 .ok_or_else(|| GameplayDataError::Malformed("missing identity object".to_string()))?;
71 identity
72 .remove("digest")
73 .ok_or_else(|| GameplayDataError::Malformed("missing identity.digest".to_string()))?;
74
75 let mut canonical = String::new();
76 write_canonical_json(&value, &mut canonical).map_err(GameplayDataError::Malformed)?;
77 Ok(format!("sha256:{:x}", Sha256::digest(canonical.as_bytes())))
78}
79
80fn write_canonical_json(value: &serde_json::Value, output: &mut String) -> Result<(), String> {
81 match value {
82 serde_json::Value::Null => output.push_str("null"),
83 serde_json::Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }),
84 serde_json::Value::Number(value) => output.push_str(&value.to_string()),
85 serde_json::Value::String(value) => {
86 output.push_str(&serde_json::to_string(value).map_err(|error| error.to_string())?)
87 }
88 serde_json::Value::Array(values) => {
89 output.push('[');
90 for (index, value) in values.iter().enumerate() {
91 if index > 0 {
92 output.push(',');
93 }
94 write_canonical_json(value, output)?;
95 }
96 output.push(']');
97 }
98 serde_json::Value::Object(values) => {
99 output.push('{');
100 let mut keys: Vec<_> = values.keys().collect();
101 keys.sort_unstable();
102 for (index, key) in keys.into_iter().enumerate() {
103 if index > 0 {
104 output.push(',');
105 }
106 output.push_str(&serde_json::to_string(key).map_err(|error| error.to_string())?);
107 output.push(':');
108 write_canonical_json(&values[key], output)?;
109 }
110 output.push('}');
111 }
112 }
113 Ok(())
114}