1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::detailplan::DetailPlanContent;
7use crate::diagnostic::{Diagnostic, codes};
8use crate::ids::CampaignId;
9use crate::layout::{GeometryBriefContent, LayoutGraphContent};
10use crate::siteplan::SitePlanContent;
11use crate::stages::{
12 ClassesContent, DialogueContent, NpcsContent, QuestPlanContent, QuestsContent, WorldContent,
13 WorldEditsContent,
14};
15
16pub const DSL_VERSION: &str = "0.20.0";
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
29#[serde(rename_all = "kebab-case")]
30pub enum Stage {
31 World,
33 Npcs,
35 Classes,
37 QuestPlan,
39 Quests,
41 Dialogue,
43 WorldEdits,
45 GeometryBrief,
49 LayoutGraph,
52 SitePlan,
55 DetailPlan,
59}
60
61impl Stage {
62 pub fn name(self) -> &'static str {
65 match self {
66 Stage::World => "world",
67 Stage::Npcs => "npcs",
68 Stage::Classes => "classes",
69 Stage::QuestPlan => "quest-plan",
70 Stage::Quests => "quests",
71 Stage::Dialogue => "dialogue",
72 Stage::WorldEdits => "world-edits",
73 Stage::GeometryBrief => "geometry-brief",
74 Stage::LayoutGraph => "layout-graph",
75 Stage::SitePlan => "site-plan",
76 Stage::DetailPlan => "detail-plan",
77 }
78 }
79
80 pub const ALL: [Stage; 11] = [
88 Stage::World,
89 Stage::Npcs,
90 Stage::Classes,
91 Stage::QuestPlan,
92 Stage::Quests,
93 Stage::Dialogue,
94 Stage::WorldEdits,
95 Stage::GeometryBrief,
96 Stage::LayoutGraph,
97 Stage::SitePlan,
98 Stage::DetailPlan,
99 ];
100}
101
102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
104#[serde(deny_unknown_fields)]
105pub struct Envelope<T> {
106 pub dsl_version: String,
108 pub campaign_id: CampaignId,
110 pub stage: Stage,
112 pub content: T,
114}
115
116#[derive(Clone, Debug, PartialEq)]
119pub struct Campaign {
120 pub world: Envelope<WorldContent>,
122 pub npcs: Envelope<NpcsContent>,
124 pub classes: Envelope<ClassesContent>,
126 pub quest_plan: Envelope<QuestPlanContent>,
128 pub quests: Envelope<QuestsContent>,
130 pub dialogue: Envelope<DialogueContent>,
132 pub world_edits: Option<Envelope<WorldEditsContent>>,
136 pub geometry_brief: Option<Envelope<GeometryBriefContent>>,
138 pub layout_graph: Option<Envelope<LayoutGraphContent>>,
140 pub site_plan: Option<Envelope<SitePlanContent>>,
142 pub detail_plan: Option<Envelope<DetailPlanContent>>,
145}
146
147#[derive(Clone, Debug, PartialEq)]
150pub struct RawCampaign {
151 pub world: String,
153 pub npcs: String,
155 pub classes: String,
157 pub quest_plan: String,
159 pub quests: String,
161 pub dialogue: String,
163 pub world_edits: Option<String>,
166 pub geometry_brief: Option<String>,
168 pub layout_graph: Option<String>,
170 pub site_plan: Option<String>,
172 pub detail_plan: Option<String>,
174}
175
176fn parse_stage<T: for<'de> Deserialize<'de>>(
177 src: &str,
178 stage: Stage,
179 out: &mut Result<Envelope<T>, ()>,
180 diags: &mut Vec<Diagnostic>,
181) {
182 match serde_json::from_str::<Envelope<T>>(src) {
183 Ok(env) => *out = Ok(env),
184 Err(e) => {
185 *out = Err(());
186 diags.push(Diagnostic::error(
187 codes::SCHEMA,
188 stage.name(),
189 "",
190 format!(
191 "`{name}` stage document does not conform to its schema: {e}. Fix the \
192 offending field (unknown field, wrong type, or missing required one) in the \
193 campaign JSON to match the schema — run `delvec schema --stage {name}` to \
194 see the exact shape of THIS document. The schema is the authority on the \
195 form; a spec that disagrees with it is the stale one.",
196 name = stage.name()
197 ),
198 ));
199 }
200 }
201}
202
203pub fn parse_campaign(raw: &RawCampaign) -> Result<Campaign, Vec<Diagnostic>> {
208 let mut diags = Vec::new();
209 let mut world = Err(());
210 let mut npcs = Err(());
211 let mut classes = Err(());
212 let mut quest_plan = Err(());
213 let mut quests = Err(());
214 let mut dialogue = Err(());
215
216 parse_stage(&raw.world, Stage::World, &mut world, &mut diags);
217 parse_stage(&raw.npcs, Stage::Npcs, &mut npcs, &mut diags);
218 parse_stage(&raw.classes, Stage::Classes, &mut classes, &mut diags);
219 parse_stage(
220 &raw.quest_plan,
221 Stage::QuestPlan,
222 &mut quest_plan,
223 &mut diags,
224 );
225 parse_stage(&raw.quests, Stage::Quests, &mut quests, &mut diags);
226 parse_stage(&raw.dialogue, Stage::Dialogue, &mut dialogue, &mut diags);
227 let mut world_edits: Result<Option<Envelope<WorldEditsContent>>, ()> = Ok(None);
230 if let Some(src) = &raw.world_edits {
231 let mut parsed = Err(());
232 parse_stage(src, Stage::WorldEdits, &mut parsed, &mut diags);
233 world_edits = parsed.map(Some);
234 }
235 let mut geometry_brief: Result<Option<Envelope<GeometryBriefContent>>, ()> = Ok(None);
239 if let Some(src) = &raw.geometry_brief {
240 let mut parsed = Err(());
241 parse_stage(src, Stage::GeometryBrief, &mut parsed, &mut diags);
242 geometry_brief = parsed.map(Some);
243 }
244 let mut layout_graph: Result<Option<Envelope<LayoutGraphContent>>, ()> = Ok(None);
245 if let Some(src) = &raw.layout_graph {
246 let mut parsed = Err(());
247 parse_stage(src, Stage::LayoutGraph, &mut parsed, &mut diags);
248 layout_graph = parsed.map(Some);
249 }
250 let mut site_plan: Result<Option<Envelope<SitePlanContent>>, ()> = Ok(None);
251 if let Some(src) = &raw.site_plan {
252 let mut parsed = Err(());
253 parse_stage(src, Stage::SitePlan, &mut parsed, &mut diags);
254 site_plan = parsed.map(Some);
255 }
256 let mut detail_plan: Result<Option<Envelope<DetailPlanContent>>, ()> = Ok(None);
257 if let Some(src) = &raw.detail_plan {
258 let mut parsed = Err(());
259 parse_stage(src, Stage::DetailPlan, &mut parsed, &mut diags);
260 detail_plan = parsed.map(Some);
261 }
262
263 match (
264 world,
265 npcs,
266 classes,
267 quest_plan,
268 quests,
269 dialogue,
270 world_edits,
271 geometry_brief,
272 layout_graph,
273 site_plan,
274 detail_plan,
275 ) {
276 (
277 Ok(world),
278 Ok(npcs),
279 Ok(classes),
280 Ok(quest_plan),
281 Ok(quests),
282 Ok(dialogue),
283 Ok(world_edits),
284 Ok(geometry_brief),
285 Ok(layout_graph),
286 Ok(site_plan),
287 Ok(detail_plan),
288 ) => {
289 let mut campaign = Campaign {
290 world,
291 npcs,
292 classes,
293 quest_plan,
294 quests,
295 dialogue,
296 world_edits,
297 geometry_brief,
298 layout_graph,
299 site_plan,
300 detail_plan,
301 };
302 campaign.quests.content.expand_ambushes();
309 Ok(campaign)
310 }
311 _ => Err(diags),
312 }
313}
314
315pub fn check_campaign(raw: &RawCampaign) -> Vec<Diagnostic> {
321 match parse_campaign(raw) {
322 Ok(campaign) => crate::validate::validate_campaign(&campaign),
323 Err(diags) => diags,
324 }
325}
326
327#[cfg(test)]
328mod version_tests {
329 use super::*;
330
331 #[test]
335 fn the_accepted_dsl_version_is_the_crate_version() {
336 assert_eq!(DSL_VERSION, env!("CARGO_PKG_VERSION"));
337 }
338}