use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::diagnostic::{Diagnostic, codes};
use crate::ids::CampaignId;
use crate::stages::{
ClassesContent, DialogueContent, NpcsContent, QuestPlanContent, QuestsContent, WorldContent,
WorldEditsContent,
};
pub const SUPPORTED_DSL_VERSION: &str = "0.9.0";
pub const SUPPORTED_DSL_VERSIONS: &[&str] = &[
"0.2.0", "0.3.0", "0.4.0", "0.5.0", "0.6.0", "0.7.0", "0.8.0", "0.9.0",
];
pub fn is_supported_version(version: &str) -> bool {
SUPPORTED_DSL_VERSIONS.contains(&version)
}
fn ordinal(version: &str) -> u32 {
match version {
"0.2.0" => 2,
"0.3.0" => 3,
"0.4.0" => 4,
"0.5.0" => 5,
"0.6.0" => 6,
"0.7.0" => 7,
"0.8.0" => 8,
"0.9.0" => 9,
_ => 0,
}
}
pub fn is_v03(version: &str) -> bool {
ordinal(version) >= 3
}
pub fn is_v04(version: &str) -> bool {
ordinal(version) >= 4
}
pub fn is_v05(version: &str) -> bool {
ordinal(version) >= 5
}
pub fn is_v06(version: &str) -> bool {
ordinal(version) >= 6
}
pub fn is_v07(version: &str) -> bool {
ordinal(version) >= 7
}
pub fn is_v08(version: &str) -> bool {
ordinal(version) >= 8
}
pub fn is_v09(version: &str) -> bool {
ordinal(version) >= 9
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Stage {
World,
Npcs,
Classes,
QuestPlan,
Quests,
Dialogue,
WorldEdits,
}
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",
}
}
}
#[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>>,
}
#[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>,
}
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!(
"`{}` 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 <1..7>` to see the \
exact shape.",
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);
}
match (
world,
npcs,
classes,
quest_plan,
quests,
dialogue,
world_edits,
) {
(
Ok(world),
Ok(npcs),
Ok(classes),
Ok(quest_plan),
Ok(quests),
Ok(dialogue),
Ok(world_edits),
) => {
let mut campaign = Campaign {
world,
npcs,
classes,
quest_plan,
quests,
dialogue,
world_edits,
};
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,
}
}