use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::design::DesignContent;
use crate::detailplan::DetailPlanContent;
use crate::diagnostic::{Diagnostic, codes};
use crate::ids::CampaignId;
use crate::layout::{GeometryBriefContent, LayoutGraphContent};
use crate::siteplan::SitePlanContent;
use crate::stages::{
ClassesContent, DialogueContent, NpcsContent, QuestPlanContent, QuestsContent, WorldContent,
WorldEditsContent,
};
pub const DSL_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Stage {
World,
Npcs,
Classes,
QuestPlan,
Quests,
Dialogue,
WorldEdits,
GeometryBrief,
LayoutGraph,
SitePlan,
DetailPlan,
Design,
}
impl Stage {
pub fn name(self) -> &'static str {
match self {
Stage::World => "world",
Stage::Npcs => "npcs",
Stage::Classes => "classes",
Stage::QuestPlan => "quest-plan",
Stage::Quests => "quests",
Stage::Dialogue => "dialogue",
Stage::WorldEdits => "world-edits",
Stage::GeometryBrief => "geometry-brief",
Stage::LayoutGraph => "layout-graph",
Stage::SitePlan => "site-plan",
Stage::DetailPlan => "detail-plan",
Stage::Design => "design",
}
}
pub const ALL: [Stage; 12] = [
Stage::World,
Stage::Npcs,
Stage::Classes,
Stage::QuestPlan,
Stage::Quests,
Stage::Dialogue,
Stage::WorldEdits,
Stage::GeometryBrief,
Stage::LayoutGraph,
Stage::SitePlan,
Stage::DetailPlan,
Stage::Design,
];
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Envelope<T> {
pub dsl_version: String,
pub campaign_id: CampaignId,
pub stage: Stage,
pub content: T,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Campaign {
pub world: Envelope<WorldContent>,
pub npcs: Envelope<NpcsContent>,
pub classes: Envelope<ClassesContent>,
pub quest_plan: Envelope<QuestPlanContent>,
pub quests: Envelope<QuestsContent>,
pub dialogue: Envelope<DialogueContent>,
pub world_edits: Option<Envelope<WorldEditsContent>>,
pub geometry_brief: Option<Envelope<GeometryBriefContent>>,
pub layout_graph: Option<Envelope<LayoutGraphContent>>,
pub site_plan: Option<Envelope<SitePlanContent>>,
pub detail_plan: Option<Envelope<DetailPlanContent>>,
pub design: Option<Envelope<DesignContent>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RawCampaign {
pub world: String,
pub npcs: String,
pub classes: String,
pub quest_plan: String,
pub quests: String,
pub dialogue: String,
pub world_edits: Option<String>,
pub geometry_brief: Option<String>,
pub layout_graph: Option<String>,
pub site_plan: Option<String>,
pub detail_plan: Option<String>,
pub design: Option<String>,
}
fn parse_stage<T: for<'de> Deserialize<'de>>(
src: &str,
stage: Stage,
out: &mut Result<Envelope<T>, ()>,
diags: &mut Vec<Diagnostic>,
) {
match serde_json::from_str::<Envelope<T>>(src) {
Ok(env) => *out = Ok(env),
Err(e) => {
*out = Err(());
diags.push(Diagnostic::error(
codes::SCHEMA,
stage.name(),
"",
format!(
"`{name}` stage document does not conform to its schema: {e}. Fix the \
offending field (unknown field, wrong type, or missing required one) in the \
campaign JSON to match the schema — run `delvec schema --stage {name}` to \
see the exact shape of THIS document. The schema is the authority on the \
form; a spec that disagrees with it is the stale one.",
name = stage.name()
),
));
}
}
}
pub fn parse_campaign(raw: &RawCampaign) -> Result<Campaign, Vec<Diagnostic>> {
let mut diags = Vec::new();
let mut world = Err(());
let mut npcs = Err(());
let mut classes = Err(());
let mut quest_plan = Err(());
let mut quests = Err(());
let mut dialogue = Err(());
parse_stage(&raw.world, Stage::World, &mut world, &mut diags);
parse_stage(&raw.npcs, Stage::Npcs, &mut npcs, &mut diags);
parse_stage(&raw.classes, Stage::Classes, &mut classes, &mut diags);
parse_stage(
&raw.quest_plan,
Stage::QuestPlan,
&mut quest_plan,
&mut diags,
);
parse_stage(&raw.quests, Stage::Quests, &mut quests, &mut diags);
parse_stage(&raw.dialogue, Stage::Dialogue, &mut dialogue, &mut diags);
let mut world_edits: Result<Option<Envelope<WorldEditsContent>>, ()> = Ok(None);
if let Some(src) = &raw.world_edits {
let mut parsed = Err(());
parse_stage(src, Stage::WorldEdits, &mut parsed, &mut diags);
world_edits = parsed.map(Some);
}
let mut geometry_brief: Result<Option<Envelope<GeometryBriefContent>>, ()> = Ok(None);
if let Some(src) = &raw.geometry_brief {
let mut parsed = Err(());
parse_stage(src, Stage::GeometryBrief, &mut parsed, &mut diags);
geometry_brief = parsed.map(Some);
}
let mut layout_graph: Result<Option<Envelope<LayoutGraphContent>>, ()> = Ok(None);
if let Some(src) = &raw.layout_graph {
let mut parsed = Err(());
parse_stage(src, Stage::LayoutGraph, &mut parsed, &mut diags);
layout_graph = parsed.map(Some);
}
let mut site_plan: Result<Option<Envelope<SitePlanContent>>, ()> = Ok(None);
if let Some(src) = &raw.site_plan {
let mut parsed = Err(());
parse_stage(src, Stage::SitePlan, &mut parsed, &mut diags);
site_plan = parsed.map(Some);
}
let mut detail_plan: Result<Option<Envelope<DetailPlanContent>>, ()> = Ok(None);
if let Some(src) = &raw.detail_plan {
let mut parsed = Err(());
parse_stage(src, Stage::DetailPlan, &mut parsed, &mut diags);
detail_plan = parsed.map(Some);
}
let mut design: Result<Option<Envelope<DesignContent>>, ()> = Ok(None);
if let Some(src) = &raw.design {
let mut parsed = Err(());
parse_stage(src, Stage::Design, &mut parsed, &mut diags);
design = parsed.map(Some);
}
match (
world,
npcs,
classes,
quest_plan,
quests,
dialogue,
world_edits,
geometry_brief,
layout_graph,
site_plan,
detail_plan,
design,
) {
(
Ok(world),
Ok(npcs),
Ok(classes),
Ok(quest_plan),
Ok(quests),
Ok(dialogue),
Ok(world_edits),
Ok(geometry_brief),
Ok(layout_graph),
Ok(site_plan),
Ok(detail_plan),
Ok(design),
) => {
let mut campaign = Campaign {
world,
npcs,
classes,
quest_plan,
quests,
dialogue,
world_edits,
geometry_brief,
layout_graph,
site_plan,
detail_plan,
design,
};
campaign.quests.content.expand_ambushes();
Ok(campaign)
}
_ => Err(diags),
}
}
pub fn check_campaign(raw: &RawCampaign) -> Vec<Diagnostic> {
match parse_campaign(raw) {
Ok(campaign) => crate::validate::validate_campaign(&campaign),
Err(diags) => diags,
}
}
#[cfg(test)]
mod version_tests {
use super::*;
#[test]
fn the_accepted_dsl_version_is_an_exact_three_part_number() {
let parts: Vec<&str> = DSL_VERSION.split('.').collect();
assert_eq!(parts.len(), 3, "DSL_VERSION is `{DSL_VERSION}`");
assert!(
parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit())),
"DSL_VERSION is `{DSL_VERSION}`"
);
}
}