Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"schemaVersion": 1,
"entryOverrides": [
{ "path": "gamemodes.general.heroLimit", "fixture": { "name": "Hero Limit", "kind": "enum", "domain": "heroLimit" }, "generated": { "name": "Hero Limit", "kind": "enum", "domain": "setting_gamemodes_general_heroLimit" } },
{ "path": "gamemodes.general.roleLimit", "fixture": { "name": "Limit Roles", "kind": "enum", "domain": "roleLimit" }, "generated": { "name": "Limit Roles", "kind": "enum", "domain": "setting_gamemodes_general_roleLimit" } },
{ "path": "gamemodes.general.spawnHealthPacks", "fixture": { "name": "Spawn Health Packs", "kind": "enum", "domain": "spawnHealthPacks" }, "generated": { "name": "Spawn Health Packs", "kind": "enum", "domain": "setting_gamemodes_general_spawnHealthPacks" } },
{ "path": "heroes.<team>.<hero>.combatUltGen%", "fixture": { "name": "Ultimate Generation - Combat Blizzard", "kind": "percent" }, "generated": { "name": "Ultimate Generation - Combat %1$s", "kind": "percent" } },
{ "path": "heroes.<team>.<hero>.enableAbility1", "fixture": { "name": "Ability 1", "kind": "bool" }, "generated": { "name": "%1$s", "kind": "bool" } },
{ "path": "heroes.<team>.<hero>.enableAbility2", "fixture": { "name": "Ability 2", "kind": "bool" }, "generated": { "name": "%1$s", "kind": "bool" } },
{ "path": "heroes.<team>.<hero>.enableSecondaryFire", "fixture": { "name": "Secondary Fire", "kind": "bool" }, "generated": { "name": "%1$s", "kind": "bool" } },
{ "path": "heroes.<team>.<hero>.passiveUltGen%", "fixture": { "name": "Ultimate Generation - Passive Blizzard", "kind": "percent" }, "generated": { "name": "Ultimate Generation - Passive %1$s", "kind": "percent" } },
{ "path": "lobby.mapRotation", "fixture": { "name": "Map Rotation", "kind": "enum", "domain": "mapRotation" }, "generated": { "name": "Map Rotation", "kind": "enum", "domain": "setting_lobby_mapRotation" } },
{ "path": "lobby.returnToLobby", "fixture": { "name": "Return To Lobby", "kind": "enum", "domain": "returnToLobby" }, "generated": { "name": "Return To Lobby", "kind": "enum", "domain": "setting_lobby_returnToLobby" } },
{ "path": "lobby.team1Slots", "fixture": { "name": "Max Team 1 Players", "kind": "number" }, "generated": { "name": "Max %1$s Players", "kind": "number" } },
{ "path": "lobby.team2Slots", "fixture": { "name": "Max Team 2 Players", "kind": "number" }, "generated": { "name": "Max %1$s Players", "kind": "number" } }
],
"enumMemberMappings": [
{ "sourceDomain": "setting_gamemodes_general_heroLimit", "sourceMember": "off", "targetDomain": "heroLimit", "targetMember": "off" },
{ "sourceDomain": "setting_gamemodes_general_heroLimit", "sourceMember": "1PerTeam", "targetDomain": "heroLimit", "targetMember": "1PerTeam" },
{ "sourceDomain": "setting_gamemodes_general_heroLimit", "sourceMember": "2PerTeam", "targetDomain": "heroLimit", "targetMember": "2PerTeam" },
{ "sourceDomain": "setting_gamemodes_general_heroLimit", "sourceMember": "1PerGame", "targetDomain": "heroLimit", "targetMember": "1PerGame" },
{ "sourceDomain": "setting_gamemodes_general_heroLimit", "sourceMember": "2PerGame", "targetDomain": "heroLimit", "targetMember": "2PerGame" },
{ "sourceDomain": "setting_gamemodes_general_roleLimit", "sourceMember": "off", "targetDomain": "roleLimit", "targetMember": "off" },
{ "sourceDomain": "setting_gamemodes_general_roleLimit", "sourceMember": "2OfEachRolePerTeam", "targetDomain": "roleLimit", "targetMember": "2OfEachRolePerTeam" },
{ "sourceDomain": "setting_gamemodes_general_roleLimit", "sourceMember": "1Tank2Offense2Support", "targetDomain": "roleLimit", "targetMember": "1Tank2Offense2Support" },
{ "sourceDomain": "setting_gamemodes_general_spawnHealthPacks", "sourceMember": "modeDependent", "targetDomain": "spawnHealthPacks", "targetMember": "modeDependent" },
{ "sourceDomain": "setting_gamemodes_general_spawnHealthPacks", "sourceMember": "enabled", "targetDomain": "spawnHealthPacks", "targetMember": "enabled" },
{ "sourceDomain": "setting_gamemodes_general_spawnHealthPacks", "sourceMember": "disabled", "targetDomain": "spawnHealthPacks", "targetMember": "disabled" },
{ "sourceDomain": "setting_lobby_mapRotation", "sourceMember": "afterMirrorMatch", "targetDomain": "mapRotation", "targetMember": "afterMirrorMatch" },
{ "sourceDomain": "setting_lobby_mapRotation", "sourceMember": "afterGame", "targetDomain": "mapRotation", "targetMember": "afterAGame" },
{ "sourceDomain": "setting_lobby_mapRotation", "sourceMember": "paused", "targetDomain": "mapRotation", "targetMember": "paused" },
{ "sourceDomain": "setting_lobby_returnToLobby", "sourceMember": "never", "targetDomain": "returnToLobby", "targetMember": "never" },
{ "sourceDomain": "setting_lobby_returnToLobby", "sourceMember": "afterGame", "targetDomain": "returnToLobby", "targetMember": "afterAGame" },
{ "sourceDomain": "setting_lobby_returnToLobby", "sourceMember": "afterMirrorMatch", "targetDomain": "returnToLobby", "targetMember": "afterMirrorMatch" }
]
}
1 change: 1 addition & 0 deletions crates/workshop-rs/src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! Extracted from the Wright-authored `wright-ir` crate; see
//! [`docs/provenance.md`](https://github.com/wrightkit/workshop-rs/blob/main/docs/provenance.md).

pub(crate) mod reconciliation;
pub mod schema;
pub mod table;

Expand Down
161 changes: 161 additions & 0 deletions crates/workshop-rs/src/settings/reconciliation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! Reviewed reconciliation rules for overlapping raw settings projections.

use std::sync::OnceLock;

use serde::Deserialize;

const DATA: &str = include_str!("data/projection_reconciliation.json");

#[derive(Debug, Deserialize)]
pub(crate) struct Reconciliation {
#[serde(rename = "schemaVersion")]
pub(crate) schema_version: u32,
#[serde(rename = "entryOverrides")]
pub(crate) entry_overrides: Vec<EntryOverride>,
#[serde(rename = "enumMemberMappings")]
pub(crate) enum_member_mappings: Vec<EnumMemberMapping>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct EntryOverride {
pub(crate) path: String,
pub(crate) fixture: EntryContract,
pub(crate) generated: EntryContract,
}

#[derive(Debug, Deserialize)]
pub(crate) struct EntryContract {
pub(crate) name: String,
pub(crate) kind: String,
pub(crate) domain: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct EnumMemberMapping {
#[serde(rename = "sourceDomain")]
pub(crate) source_domain: String,
#[serde(rename = "sourceMember")]
pub(crate) source_member: String,
#[serde(rename = "targetDomain")]
pub(crate) target_domain: String,
#[serde(rename = "targetMember")]
pub(crate) target_member: String,
}

pub(crate) fn data() -> &'static Reconciliation {
static RECONCILIATION: OnceLock<Reconciliation> = OnceLock::new();
RECONCILIATION.get_or_init(|| {
serde_json::from_str(DATA).expect("settings projection reconciliation data is valid JSON")
})
}

/// Reject ambiguous reconciliation rules before callers can use their
/// first-match lookup behavior.
pub(crate) fn validate() -> Vec<String> {
validate_manifest(data())
}

fn validate_manifest(reconciliation: &Reconciliation) -> Vec<String> {
use std::collections::HashSet;

let mut errors = Vec::new();
if reconciliation.schema_version != 1 {
errors.push(format!(
"unsupported settings reconciliation schema version: {}",
reconciliation.schema_version
));
}

let mut entry_paths = HashSet::new();
for override_ in &reconciliation.entry_overrides {
if !entry_paths.insert(override_.path.as_str()) {
errors.push(format!(
"duplicate settings reconciliation entry override: {}",
override_.path
));
}
}

let mut member_sources = HashSet::new();
for mapping in &reconciliation.enum_member_mappings {
let source = (
mapping.source_domain.as_str(),
mapping.source_member.as_str(),
);
if !member_sources.insert(source) {
errors.push(format!(
"duplicate settings enum reconciliation source: {}.{}",
mapping.source_domain, mapping.source_member
));
}
}
errors
}

#[cfg(test)]
mod tests {
use super::*;

fn entry_override(path: &str) -> EntryOverride {
EntryOverride {
path: path.to_string(),
fixture: EntryContract {
name: "Fixture".to_string(),
kind: "bool".to_string(),
domain: None,
},
generated: EntryContract {
name: "Generated".to_string(),
kind: "bool".to_string(),
domain: None,
},
}
}

fn mapping(source_member: &str) -> EnumMemberMapping {
EnumMemberMapping {
source_domain: "exportDomain".to_string(),
source_member: source_member.to_string(),
target_domain: "canonicalDomain".to_string(),
target_member: "canonicalMember".to_string(),
}
}

fn manifest() -> Reconciliation {
Reconciliation {
schema_version: 1,
entry_overrides: Vec::new(),
enum_member_mappings: Vec::new(),
}
}

#[test]
fn manifest_rejects_duplicate_entry_override_paths() {
let mut reconciliation = manifest();
reconciliation.entry_overrides = vec![
entry_override("lobby.mapRotation"),
entry_override("lobby.mapRotation"),
];
let errors = validate_manifest(&reconciliation);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("duplicate settings reconciliation entry override"));
}

#[test]
fn manifest_rejects_duplicate_enum_member_sources() {
let mut reconciliation = manifest();
reconciliation.enum_member_mappings = vec![mapping("afterGame"), mapping("afterGame")];
let errors = validate_manifest(&reconciliation);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("duplicate settings enum reconciliation source"));
}

#[test]
fn manifest_rejects_unsupported_schema_versions() {
let mut reconciliation = manifest();
reconciliation.schema_version = 2;
let errors = validate_manifest(&reconciliation);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("unsupported settings reconciliation schema version"));
}
}
Loading
Loading