Files
heros/tools/validate_data.ps1

1573 lines
66 KiB
PowerShell

$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
function Read-Json($RelativePath) {
$path = Join-Path $Root $RelativePath
if (-not (Test-Path $path)) {
throw "Missing file: $RelativePath"
}
return Get-Content $path -Raw | ConvertFrom-Json
}
function Has-Prop($Object, [string]$Name) {
if ($null -eq $Object) {
return $false
}
return $null -ne ($Object.PSObject.Properties | Where-Object { $_.Name -eq $Name })
}
function Get-Prop($Object, [string]$Name, $Default = $null) {
if (Has-Prop $Object $Name) {
return $Object.$Name
}
return $Default
}
function Get-Names($Object) {
return @($Object.PSObject.Properties.Name)
}
function Fail($Message) {
throw $Message
}
function Check-Raw-Skills-Arrays() {
$relativePaths = @(
"data\defs\classes.json",
"data\defs\officers.json"
)
$scenarioDir = Join-Path $Root "data\scenarios"
foreach ($file in Get-ChildItem -Path $scenarioDir -Filter "*.json") {
$relativePaths += "data\scenarios\$($file.Name)"
}
foreach ($relativePath in $relativePaths) {
$path = Join-Path $Root $relativePath
$text = Get-Content $path -Raw
if ($text -match '"skills"\s*:\s*"') {
Fail "$relativePath has skills as a string; use an array."
}
}
}
function Get-Flag-Value-Kind($Value) {
if ($Value -is [bool]) {
return "bool"
}
if ($Value -is [string]) {
return "string"
}
if ($Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
return "number"
}
return "other"
}
function Terrain-At($Rows, [int]$X, [int]$Y) {
return [string]$Rows[$Y].Substring($X, 1)
}
$campaign = Read-Json "data\campaign\campaign.json"
$classes = Read-Json "data\defs\classes.json"
$items = Read-Json "data\defs\items.json"
$officers = Read-Json "data\defs\officers.json"
$skills = Read-Json "data\defs\skills.json"
$terrain = Read-Json "data\defs\terrain.json"
$classIds = Get-Names $classes
$itemIds = Get-Names $items
$officerIds = Get-Names $officers
$skillIds = Get-Names $skills
$terrainKeys = Get-Names $terrain
$validConditionTypes = @(
"all_units_defeated",
"all_enemies_defeated",
"all_players_defeated",
"any_unit_defeated",
"any_officer_defeated",
"unit_reaches_tile",
"turn_limit",
"turn_reached",
"all",
"any"
)
$validTriggers = @("battle_start", "battle_begin", "turn_start", "unit_reaches_tile")
$validActions = @("log", "dialogue", "set_objective", "spawn_deployment", "spawn_deployments")
$validEffects = @("heal_hp", "heal_mp", "cure_status")
$validItemKinds = @("weapon", "armor", "accessory", "consumable")
$validBonusStats = @("hp", "mp", "atk", "def", "int", "agi")
$validSkillKinds = @("damage", "heal", "support")
$validSkillTargets = @("enemy", "ally", "self", "any")
$validSkillStats = @("atk", "def", "int", "agi")
$validSkillEffectTypes = @("stat_bonus", "damage_over_time", "action_lock")
$validDialogueSides = @("left", "right")
$validPortraitPathPattern = '^res://.+\.(png|jpg|jpeg|webp)$'
$knownFlagIds = New-Object System.Collections.Generic.HashSet[string]
$knownFlagValueKinds = @{}
$joinedOfficerIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($officerIdValue in (Get-Prop $campaign "initial_joined_officers" @())) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Campaign initial_joined_officers has an empty officer id."
}
if (-not $officerIds.Contains($officerId)) {
Fail "Campaign initial_joined_officers references unknown officer: $officerId"
}
if (-not $joinedOfficerIds.Add($officerId)) {
Fail "Campaign initial_joined_officers has duplicate officer: $officerId"
}
}
$campaignScenarioIds = @()
$campaignScenarioIdSet = New-Object System.Collections.Generic.HashSet[string]
foreach ($scenario in @($campaign.scenarios)) {
if ($scenario -is [System.Array] -or $null -eq $scenario) {
Fail "Campaign has a malformed scenario entry."
}
$scenarioId = [string](Get-Prop $scenario "id" "")
$scenarioTitle = [string](Get-Prop $scenario "title" "")
$scenarioPath = [string](Get-Prop $scenario "path" "")
if ([string]::IsNullOrWhiteSpace($scenarioId)) {
Fail "Campaign has a scenario with an empty id."
}
if (-not ($scenarioId -match '^[a-z0-9_]+$')) {
Fail "Campaign scenario has unstable id: $scenarioId"
}
if ([string]::IsNullOrWhiteSpace($scenarioTitle)) {
Fail "Campaign scenario $scenarioId has an empty title."
}
if ([string]::IsNullOrWhiteSpace($scenarioPath)) {
Fail "Campaign scenario $scenarioId has an empty path."
}
if (-not $campaignScenarioIdSet.Add($scenarioId)) {
Fail "Campaign has duplicate scenario id: $scenarioId"
}
$campaignScenarioIds += $scenarioId
}
if ($campaignScenarioIds.Count -le 0) {
Fail "Campaign has no scenarios."
}
if (-not $campaignScenarioIdSet.Contains([string]$campaign.start_scenario)) {
Fail "Campaign start_scenario references unknown scenario: $($campaign.start_scenario)"
}
if (Has-Prop $campaign "chapters") {
$chaptersValue = Get-Prop $campaign "chapters" $null
if ($chaptersValue -isnot [System.Array]) {
Fail "Campaign chapters must be an array."
}
if (@($chaptersValue).Count -le 0) {
Fail "Campaign chapters must not be empty."
}
$chapterIds = New-Object System.Collections.Generic.HashSet[string]
$coveredScenarioIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($chapter in @($chaptersValue)) {
if ($chapter -is [System.Array] -or $null -eq $chapter) {
Fail "Campaign has a malformed chapter entry."
}
$chapterId = [string](Get-Prop $chapter "id" "")
$chapterTitle = [string](Get-Prop $chapter "title" "")
$startScenario = [string](Get-Prop $chapter "start_scenario" "")
$endScenario = [string](Get-Prop $chapter "end_scenario" "")
if ([string]::IsNullOrWhiteSpace($chapterId)) {
Fail "Campaign chapter has an empty id."
}
if (-not ($chapterId -match '^[a-z0-9_]+$')) {
Fail "Campaign chapter has unstable id: $chapterId"
}
if (-not $chapterIds.Add($chapterId)) {
Fail "Campaign has duplicate chapter id: $chapterId"
}
if ([string]::IsNullOrWhiteSpace($chapterTitle)) {
Fail "Campaign chapter $chapterId has an empty title."
}
if (-not $campaignScenarioIdSet.Contains($startScenario)) {
Fail "Campaign chapter $chapterId references unknown start_scenario: $startScenario"
}
if (-not $campaignScenarioIdSet.Contains($endScenario)) {
Fail "Campaign chapter $chapterId references unknown end_scenario: $endScenario"
}
$startIndex = [Array]::IndexOf($campaignScenarioIds, $startScenario)
$endIndex = [Array]::IndexOf($campaignScenarioIds, $endScenario)
if ($startIndex -lt 0 -or $endIndex -lt 0 -or $endIndex -lt $startIndex) {
Fail "Campaign chapter $chapterId has an invalid scenario range."
}
for ($index = $startIndex; $index -le $endIndex; $index++) {
$coveredScenarioId = [string]$campaignScenarioIds[$index]
if (-not $coveredScenarioIds.Add($coveredScenarioId)) {
Fail "Campaign chapter $chapterId overlaps scenario: $coveredScenarioId"
}
}
}
foreach ($scenarioId in $campaignScenarioIds) {
if (-not $coveredScenarioIds.Contains($scenarioId)) {
Fail "Campaign chapters do not cover scenario: $scenarioId"
}
}
}
function Check-Condition($Condition, [string]$ScenarioId, $EventIds, [int]$Width = -1, [int]$Height = -1, $Rows = $null, $KnownUnitIds = $null) {
if ($Condition -is [System.Array]) {
foreach ($entry in $Condition) {
Check-Condition $entry $ScenarioId $EventIds $Width $Height $Rows $KnownUnitIds
}
return
}
if (-not (Has-Prop $Condition "type")) {
Fail "Scenario $ScenarioId has a malformed condition."
}
$type = [string]$Condition.type
if (-not $validConditionTypes.Contains($type)) {
Fail "Scenario $ScenarioId has unknown condition type: $type"
}
if ((Has-Prop $Condition "after_event") -and (-not $EventIds.Contains([string]$Condition.after_event))) {
Fail "Scenario $ScenarioId condition references missing event: $($Condition.after_event)"
}
if (($type -eq "all" -or $type -eq "any") -and (Has-Prop $Condition "conditions")) {
Check-Condition $Condition.conditions $ScenarioId $EventIds $Width $Height $Rows $KnownUnitIds
}
if ($type -eq "any_unit_defeated") {
$unitIdsValue = @()
if (Has-Prop $Condition "unit_ids") {
$unitIdsValue = @($Condition.unit_ids)
}
if ($unitIdsValue.Count -le 0) {
Fail "Scenario $ScenarioId any_unit_defeated needs unit_ids."
}
foreach ($unitIdValue in $unitIdsValue) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId any_unit_defeated has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId any_unit_defeated references unknown unit: $unitId"
}
}
}
if ($type -eq "unit_reaches_tile") {
if (-not (Has-Prop $Condition "pos")) {
Fail "Scenario $ScenarioId unit_reaches_tile condition is missing pos."
}
$cell = Resolve-Cell $Condition.pos "Scenario $ScenarioId unit_reaches_tile condition"
$x = [int]$cell.X
$y = [int]$cell.Y
if ($Width -ge 0 -and $Height -ge 0 -and ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height)) {
Fail "Scenario $ScenarioId unit_reaches_tile condition is outside map at [$x,$y]."
}
if ($null -ne $Rows) {
$terrainKey = Terrain-At $Rows $x $y
if ([int]$terrain.$terrainKey.move_cost.foot -ge 99 -and [int]$terrain.$terrainKey.move_cost.mounted -ge 99 -and [int]$terrain.$terrainKey.move_cost.archer -ge 99) {
Fail "Scenario $ScenarioId unit_reaches_tile condition targets impassable terrain at [$x,$y]."
}
}
$unitIdsValue = $null
if (Has-Prop $Condition "unit_ids") {
$unitIdsValue = $Condition.unit_ids
}
if ($null -ne $unitIdsValue -and -not ($unitIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile unit_ids must be an array."
}
if ($null -ne $unitIdsValue) {
foreach ($unitIdValue in @($unitIdsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId unit_reaches_tile has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId unit_reaches_tile references unknown unit: $unitId"
}
}
}
$officerIdsValue = $null
if (Has-Prop $Condition "officer_ids") {
$officerIdsValue = $Condition.officer_ids
if ($null -ne $officerIdsValue -and -not ($officerIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile officer_ids must be an array."
}
foreach ($officerIdValue in @($officerIdsValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId) -or (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId unit_reaches_tile references unknown officer: $officerId"
}
}
}
if (($null -eq $unitIdsValue -or @($unitIdsValue).Count -le 0) -and ((-not (Has-Prop $Condition "officer_ids")) -or @($officerIdsValue).Count -le 0)) {
Fail "Scenario $ScenarioId unit_reaches_tile needs unit_ids or officer_ids."
}
$team = [string](Get-Prop $Condition "team" "")
if (-not [string]::IsNullOrWhiteSpace($team) -and $team -ne "player" -and $team -ne "enemy") {
Fail "Scenario $ScenarioId unit_reaches_tile has unknown team: $team"
}
}
}
function Check-Unit-Reaches-Event-When($When, [string]$ScenarioId, [int]$Width, [int]$Height, $Rows, $KnownUnitIds) {
if (-not (Has-Prop $When "pos")) {
Fail "Scenario $ScenarioId unit_reaches_tile event is missing pos."
}
$cell = Resolve-Cell $When.pos "Scenario $ScenarioId unit_reaches_tile event"
$x = [int]$cell.X
$y = [int]$cell.Y
if ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height) {
Fail "Scenario $ScenarioId unit_reaches_tile event is outside map at [$x,$y]."
}
$terrainKey = Terrain-At $Rows $x $y
if ([int]$terrain.$terrainKey.move_cost.foot -ge 99 -and [int]$terrain.$terrainKey.move_cost.mounted -ge 99 -and [int]$terrain.$terrainKey.move_cost.archer -ge 99) {
Fail "Scenario $ScenarioId unit_reaches_tile event targets impassable terrain at [$x,$y]."
}
$unitIdsValue = $null
if (Has-Prop $When "unit_ids") {
$unitIdsValue = $When.unit_ids
if ($null -ne $unitIdsValue -and -not ($unitIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile event unit_ids must be an array."
}
foreach ($unitIdValue in @($unitIdsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId unit_reaches_tile event has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId unit_reaches_tile event references unknown unit: $unitId"
}
}
}
$officerIdsValue = $null
if (Has-Prop $When "officer_ids") {
$officerIdsValue = $When.officer_ids
if ($null -ne $officerIdsValue -and -not ($officerIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile event officer_ids must be an array."
}
foreach ($officerIdValue in @($officerIdsValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId) -or (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId unit_reaches_tile event references unknown officer: $officerId"
}
}
}
$team = [string](Get-Prop $When "team" "")
if (-not [string]::IsNullOrWhiteSpace($team) -and $team -ne "player" -and $team -ne "enemy") {
Fail "Scenario $ScenarioId unit_reaches_tile event has unknown team: $team"
}
$unitIdCount = 0
if ($null -ne $unitIdsValue) {
$unitIdCount = @($unitIdsValue).Count
}
$officerIdCount = 0
if ($null -ne $officerIdsValue) {
$officerIdCount = @($officerIdsValue).Count
}
if ([string]::IsNullOrWhiteSpace($team) -and $unitIdCount -le 0 -and $officerIdCount -le 0) {
Fail "Scenario $ScenarioId unit_reaches_tile event needs team, unit_ids, or officer_ids."
}
}
function Resolve-Class-Id($Deployment, [string]$ScenarioId) {
$classId = [string](Get-Prop $Deployment "class_id" "")
if (-not [string]::IsNullOrWhiteSpace($classId)) {
return $classId
}
$officerId = [string](Get-Prop $Deployment "officer_id" "")
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId deployment $($Deployment.unit_id) has no class_id or officer_id."
}
if (-not $officerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId deployment $($Deployment.unit_id) references unknown officer: $officerId"
}
return [string]$officers.$officerId.class_id
}
function Check-Deployment($Deployment, [string]$ScenarioId, [int]$Width, [int]$Height, $Rows, $SeenIds) {
if (-not (Has-Prop $Deployment "unit_id")) {
Fail "Scenario $ScenarioId has a deployment without unit_id."
}
$unitId = [string]$Deployment.unit_id
if ($SeenIds.Contains($unitId)) {
Fail "Scenario $ScenarioId has duplicate deployment id: $unitId"
}
$SeenIds.Add($unitId) | Out-Null
$team = [string](Get-Prop $Deployment "team" "enemy")
$officerId = [string](Get-Prop $Deployment "officer_id" "")
if (-not [string]::IsNullOrWhiteSpace($officerId) -and (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId deployment $unitId references unknown officer: $officerId"
}
if (Has-Prop $Deployment "requires_joined") {
if (-not ((Get-Prop $Deployment "requires_joined" $false) -is [bool])) {
Fail "Scenario $ScenarioId deployment $unitId requires_joined must be boolean."
}
if ([bool](Get-Prop $Deployment "requires_joined" $false)) {
if ($team -ne "player") {
Fail "Scenario $ScenarioId deployment $unitId requires_joined is only valid for player deployments."
}
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId deployment $unitId requires_joined needs officer_id."
}
}
}
if ((Has-Prop $Deployment "controllable") -and (-not ((Get-Prop $Deployment "controllable" $true) -is [bool]))) {
Fail "Scenario $ScenarioId deployment $unitId controllable must be boolean."
}
if ((Has-Prop $Deployment "persist_progression") -and (-not ((Get-Prop $Deployment "persist_progression" $true) -is [bool]))) {
Fail "Scenario $ScenarioId deployment $unitId persist_progression must be boolean."
}
if (Has-Prop $Deployment "ai_target_priority") {
$priorityValue = Get-Prop $Deployment "ai_target_priority" 0
if (-not ($priorityValue -is [int] -or $priorityValue -is [long])) {
Fail "Scenario $ScenarioId deployment $unitId ai_target_priority must be an integer."
}
$priority = [int]$priorityValue
if ($priority -lt 0 -or $priority -gt 20) {
Fail "Scenario $ScenarioId deployment $unitId ai_target_priority must be between 0 and 20."
}
}
$classId = Resolve-Class-Id $Deployment $ScenarioId
if (-not $classIds.Contains($classId)) {
Fail "Scenario $ScenarioId deployment $unitId references unknown class: $classId"
}
Check-Skill-Refs (Get-Prop $Deployment "skills" @()) "Scenario $ScenarioId deployment $unitId"
$pos = Get-Prop $Deployment "pos" @()
if ($pos.Count -lt 2) {
Fail "Scenario $ScenarioId deployment $unitId has malformed pos."
}
$x = [int]$pos[0]
$y = [int]$pos[1]
if ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height) {
Fail "Scenario $ScenarioId deployment $unitId is outside map at [$x,$y]."
}
$terrainKey = Terrain-At $Rows $x $y
$classDef = $classes.$classId
$moveType = [string](Get-Prop $Deployment "move_type" (Get-Prop $classDef "move_type" "foot"))
$moveCost = $terrain.$terrainKey.move_cost.$moveType
if ($null -eq $moveCost) {
$moveCost = $terrain.$terrainKey.move_cost.foot
}
if ([int]$moveCost -ge 99) {
Fail "Scenario $ScenarioId deployment $unitId starts on impassable terrain."
}
}
function Add-Deployment-Unit-Id($Deployment, $Result) {
$unitId = [string](Get-Prop $Deployment "unit_id" (Get-Prop $Deployment "id" ""))
if (-not [string]::IsNullOrWhiteSpace($unitId)) {
$Result.Add($unitId) | Out-Null
}
}
function Collect-Scenario-Unit-Ids($ScenarioData) {
$result = New-Object System.Collections.Generic.HashSet[string]
foreach ($deployment in (Get-Prop $ScenarioData "deployments" @())) {
Add-Deployment-Unit-Id $deployment $result
}
foreach ($event in (Get-Prop $ScenarioData "events" @())) {
foreach ($action in (Get-Prop $event "actions" @())) {
$actionType = [string](Get-Prop $action "type" "")
if ($actionType -eq "spawn_deployment") {
Add-Deployment-Unit-Id $action.deployment $result
}
if ($actionType -eq "spawn_deployments") {
foreach ($deployment in (Get-Prop $action "deployments" @())) {
Add-Deployment-Unit-Id $deployment $result
}
}
}
}
return $result
}
function Find-Item-Def([string]$ItemId) {
$prop = $items.PSObject.Properties | Where-Object { $_.Name -eq $ItemId } | Select-Object -First 1
if ($null -eq $prop) {
return $null
}
return $prop.Value
}
function Check-Class-Equipment-Slots() {
$weaponTypes = @()
$armorTypes = @()
$accessoryTypes = @()
foreach ($itemProp in $items.PSObject.Properties) {
$item = $itemProp.Value
$kind = [string](Get-Prop $item "kind" "")
if ($kind -eq "weapon") {
$weaponTypes += [string](Get-Prop $item "weapon_type" "")
} elseif ($kind -eq "armor") {
$armorTypes += [string](Get-Prop $item "armor_type" "")
} elseif ($kind -eq "accessory") {
$accessoryTypes += [string](Get-Prop $item "accessory_type" "accessory")
}
}
foreach ($classProp in $classes.PSObject.Properties) {
$classId = $classProp.Name
$slots = Get-Prop $classProp.Value "equipment_slots" $null
if ($null -eq $slots) {
continue
}
foreach ($weaponType in (Get-Prop $slots "weapon" @())) {
if (-not $weaponTypes.Contains([string]$weaponType)) {
Fail "Class $classId allows unknown weapon type: $weaponType"
}
}
foreach ($armorType in (Get-Prop $slots "armor" @())) {
if (-not $armorTypes.Contains([string]$armorType)) {
Fail "Class $classId allows unknown armor type: $armorType"
}
}
foreach ($accessoryType in (Get-Prop $slots "accessory" @())) {
if (-not $accessoryTypes.Contains([string]$accessoryType)) {
Fail "Class $classId allows unknown accessory type: $accessoryType"
}
}
}
}
function Check-Class-Promotions() {
foreach ($classProp in $classes.PSObject.Properties) {
$classId = [string]$classProp.Name
$promotion = Get-Prop $classProp.Value "promotion" $null
if ($null -eq $promotion) {
continue
}
if ($promotion -is [string] -or $promotion -is [System.Array]) {
Fail "Class $classId promotion must be an object."
}
$level = [int](Get-Prop $promotion "level" 0)
if ($level -le 1) {
Fail "Class $classId promotion has invalid level."
}
$targetClassId = [string](Get-Prop $promotion "to" "")
if ([string]::IsNullOrWhiteSpace($targetClassId) -or (-not $classIds.Contains($targetClassId))) {
Fail "Class $classId promotes to unknown class: $targetClassId"
}
if ($targetClassId -eq $classId) {
Fail "Class $classId cannot promote to itself."
}
$targetClass = Get-Prop $classes $targetClassId $null
$currentTier = [int](Get-Prop $classProp.Value "tier" 1)
$targetTier = [int](Get-Prop $targetClass "tier" $currentTier)
if ($targetTier -le $currentTier) {
Fail "Class $classId promotion target must have higher tier: $targetClassId"
}
$sourceSlots = Get-Prop $classProp.Value "equipment_slots" $null
$targetSlots = Get-Prop $targetClass "equipment_slots" $null
foreach ($slot in @("weapon", "armor", "accessory")) {
$sourceAllowed = @()
if ($null -ne $sourceSlots) {
$sourceAllowed = @(Get-Prop $sourceSlots $slot @())
}
$targetAllowed = @()
if ($null -ne $targetSlots) {
$targetAllowed = @(Get-Prop $targetSlots $slot @())
}
foreach ($allowed in $sourceAllowed) {
if (-not $targetAllowed.Contains([string]$allowed)) {
Fail "Class $classId promotion target $targetClassId must keep $slot type: $allowed"
}
}
}
}
}
function Check-Officer-Equipment([string]$OfficerId, $Officer) {
$classId = [string](Get-Prop $Officer "class_id" "")
if ([string]::IsNullOrWhiteSpace($classId) -or (-not $classIds.Contains($classId))) {
Fail "Officer $OfficerId references unknown class: $classId"
}
$classDef = $classes.$classId
$slots = Get-Prop $classDef "equipment_slots" $null
$equipment = Get-Prop $Officer "equipment" $null
if ($null -eq $equipment) {
return
}
foreach ($slot in $equipment.PSObject.Properties) {
$slotName = [string]$slot.Name
$itemId = [string]$slot.Value
if ([string]::IsNullOrWhiteSpace($itemId)) {
continue
}
$item = Find-Item-Def $itemId
if ($null -eq $item) {
Fail "Officer $OfficerId references unknown equipment item: $itemId"
}
$kind = [string](Get-Prop $item "kind" "")
if ($slotName -eq "weapon") {
if ($kind -ne "weapon") {
Fail "Officer $OfficerId has non-weapon item $itemId in weapon slot."
}
$weaponType = [string](Get-Prop $item "weapon_type" "")
if ($null -ne $slots -and (-not @((Get-Prop $slots "weapon" @())).Contains($weaponType))) {
Fail "Officer $OfficerId cannot equip weapon type $weaponType."
}
} elseif ($slotName -eq "armor") {
if ($kind -ne "armor") {
Fail "Officer $OfficerId has non-armor item $itemId in armor slot."
}
$armorType = [string](Get-Prop $item "armor_type" "")
if ($null -ne $slots -and (-not @((Get-Prop $slots "armor" @())).Contains($armorType))) {
Fail "Officer $OfficerId cannot equip armor type $armorType."
}
} elseif ($slotName -eq "accessory") {
if ($kind -ne "accessory") {
Fail "Officer $OfficerId has non-accessory item $itemId in accessory slot."
}
$accessoryType = [string](Get-Prop $item "accessory_type" "accessory")
if ($null -ne $slots -and (-not @((Get-Prop $slots "accessory" @())).Contains($accessoryType))) {
Fail "Officer $OfficerId cannot equip accessory type $accessoryType."
}
}
}
}
function Check-Item-Effects() {
foreach ($itemProp in $items.PSObject.Properties) {
$itemId = $itemProp.Name
$item = $itemProp.Value
$kind = [string](Get-Prop $item "kind" "")
if (-not $validItemKinds.Contains($kind)) {
Fail "Item $itemId has unknown kind: $kind"
}
if ([int](Get-Prop $item "price" 0) -lt 0) {
Fail "Item $itemId has negative price."
}
if ($kind -eq "weapon") {
if (-not (Has-Prop $item "weapon_type")) {
Fail "Weapon item $itemId is missing weapon_type."
}
Check-Skill-Range (Get-Prop $item "range" 1) "Item $itemId"
}
if ($kind -eq "armor" -and (-not (Has-Prop $item "armor_type"))) {
Fail "Armor item $itemId is missing armor_type."
}
if ($kind -eq "accessory") {
$accessoryType = [string](Get-Prop $item "accessory_type" "")
if ([string]::IsNullOrWhiteSpace($accessoryType)) {
Fail "Accessory item $itemId is missing accessory_type."
}
}
if ($kind -eq "consumable") {
if ([int](Get-Prop $item "uses" 0) -le 0) {
Fail "Consumable item $itemId must have positive uses."
}
$target = [string](Get-Prop $item "target" "ally")
if (-not $validSkillTargets.Contains($target)) {
Fail "Consumable item $itemId has unknown target: $target"
}
Check-Skill-Range (Get-Prop $item "range" @(0, 1)) "Item $itemId"
}
$bonuses = Get-Prop $item "bonuses" $null
if ($null -ne $bonuses) {
foreach ($bonus in $bonuses.PSObject.Properties) {
if (-not $validBonusStats.Contains([string]$bonus.Name)) {
Fail "Item $itemId has unsupported bonus stat: $($bonus.Name)"
}
}
}
foreach ($effect in (Get-Prop $item "effects" @())) {
if (-not (Has-Prop $effect "type")) {
Fail "Item $itemId has malformed effect."
}
if (-not $validEffects.Contains([string]$effect.type)) {
Fail "Item $itemId has unknown effect: $($effect.type)"
}
$effectType = [string]$effect.type
if ($effectType -eq "heal_hp" -or $effectType -eq "heal_mp") {
if ([int](Get-Prop $effect "amount" 0) -le 0) {
Fail "Item $itemId effect $effectType must have positive amount."
}
} elseif ($effectType -eq "cure_status") {
$status = [string](Get-Prop $effect "status" "")
if ([string]::IsNullOrWhiteSpace($status) -or -not ($status -match '^[a-z0-9_]+$')) {
Fail "Item $itemId cure_status effect must have a stable status."
}
}
}
}
}
function Check-Terrain-Definitions() {
foreach ($terrainProp in $terrain.PSObject.Properties) {
$terrainKey = [string]$terrainProp.Name
$terrainDef = $terrainProp.Value
$defense = [int](Get-Prop $terrainDef "defense" 0)
if ($defense -lt 0) {
Fail "Terrain $terrainKey has negative defense."
}
$avoid = [int](Get-Prop $terrainDef "avoid" 0)
if ($avoid -lt 0 -or $avoid -gt 100) {
Fail "Terrain $terrainKey avoid must be between 0 and 100."
}
}
}
function Check-Skill-Range($Value, [string]$Context) {
if ($Value -is [System.Array]) {
if ($Value.Count -lt 1 -or $Value.Count -gt 2) {
Fail "$Context has malformed range."
}
$minRange = [int]$Value[0]
$maxRange = [int]$Value[$Value.Count - 1]
if ($minRange -lt 0 -or $maxRange -lt $minRange) {
Fail "$Context has invalid range."
}
return
}
$range = [int]$Value
if ($range -lt 0) {
Fail "$Context has invalid range."
}
}
function Check-Skill-Definitions() {
foreach ($skillProp in $skills.PSObject.Properties) {
$skillId = $skillProp.Name
$skill = $skillProp.Value
$kind = [string](Get-Prop $skill "kind" "")
if (-not $validSkillKinds.Contains($kind)) {
Fail "Skill $skillId has unknown kind: $kind"
}
$target = [string](Get-Prop $skill "target" "")
if (-not $validSkillTargets.Contains($target)) {
Fail "Skill $skillId has unknown target: $target"
}
Check-Skill-Range (Get-Prop $skill "range" 1) "Skill $skillId"
if ([int](Get-Prop $skill "mp_cost" 0) -lt 0) {
Fail "Skill $skillId has negative mp_cost."
}
if ([int](Get-Prop $skill "power" 0) -lt 0) {
Fail "Skill $skillId has negative power."
}
$stat = [string](Get-Prop $skill "stat" "int")
if (-not $validSkillStats.Contains($stat)) {
Fail "Skill $skillId references unsupported stat: $stat"
}
$effects = Get-Prop $skill "effects" @()
if ($kind -eq "support" -and @($effects).Count -eq 0) {
Fail "Support skill $skillId must define at least one effect."
}
foreach ($effect in @($effects)) {
if ($effect -isnot [pscustomobject]) {
Fail "Skill $skillId has malformed effect."
continue
}
$effectType = [string](Get-Prop $effect "type" "")
if (-not $validSkillEffectTypes.Contains($effectType)) {
Fail "Skill $skillId has unknown effect: $effectType"
}
if ($effectType -eq "stat_bonus") {
$effectStat = [string](Get-Prop $effect "stat" "")
if (-not $validSkillStats.Contains($effectStat)) {
Fail "Skill $skillId effect references unsupported stat: $effectStat"
}
if ([int](Get-Prop $effect "amount" 0) -eq 0) {
Fail "Skill $skillId stat_bonus effect must have non-zero amount."
}
if ([int](Get-Prop $effect "duration_turns" 1) -le 0) {
Fail "Skill $skillId stat_bonus effect must have positive duration_turns."
}
} elseif ($effectType -eq "damage_over_time") {
$status = [string](Get-Prop $effect "status" "")
if ([string]::IsNullOrWhiteSpace($status) -or -not ($status -match '^[a-z0-9_]+$')) {
Fail "Skill $skillId damage_over_time effect must have a stable status."
}
if ([int](Get-Prop $effect "amount" 0) -le 0) {
Fail "Skill $skillId damage_over_time effect must have positive amount."
}
if ([int](Get-Prop $effect "duration_turns" 1) -le 0) {
Fail "Skill $skillId damage_over_time effect must have positive duration_turns."
}
} elseif ($effectType -eq "action_lock") {
$status = [string](Get-Prop $effect "status" "")
if ([string]::IsNullOrWhiteSpace($status) -or -not ($status -match '^[a-z0-9_]+$')) {
Fail "Skill $skillId action_lock effect must have a stable status."
}
$action = [string](Get-Prop $effect "action" "")
if ($action -ne "skill") {
Fail "Skill $skillId action_lock effect currently supports only action=skill."
}
if ([int](Get-Prop $effect "duration_turns" 1) -le 0) {
Fail "Skill $skillId action_lock effect must have positive duration_turns."
}
}
}
}
}
function Check-Skill-Refs($Value, [string]$Context) {
if ($null -eq $Value) {
return
}
if ($Value -is [pscustomobject]) {
Fail "$Context skills must be an array."
}
foreach ($skillId in @($Value)) {
if (-not $skillIds.Contains([string]$skillId)) {
Fail "$Context references unknown skill: $skillId"
}
}
}
function Resolve-Shop-Item-Id($Entry, [string]$Context) {
if ($Entry -is [string]) {
return [string]$Entry
}
if (Has-Prop $Entry "id") {
return [string]$Entry.id
}
Fail "$Context has a malformed shop item entry."
}
function Check-Shop-Stock-Value($Value, [string]$Context) {
$isInteger = (
$Value -is [byte] -or
$Value -is [int16] -or
$Value -is [int] -or
$Value -is [long]
)
if (-not $isInteger -or [long]$Value -lt 0) {
Fail "$Context stock must be a non-negative integer."
}
}
function Check-Shop-Entry-Stock($Entry, [string]$ItemId, [string]$Context) {
if (-not (Has-Prop $Entry "stock")) {
return
}
Check-Shop-Stock-Value (Get-Prop $Entry "stock" $null) "$Context item $ItemId"
}
function Check-Shop-Stock-Block($Stock, $AllowedItemIds, [string]$Context) {
if ($null -eq $Stock -or $Stock -is [System.Array] -or $Stock -is [string] -or $Stock -is [ValueType]) {
Fail "$Context stock must be an object."
}
foreach ($prop in @($Stock.PSObject.Properties)) {
$itemId = [string]$prop.Name
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "$Context stock has an empty item id."
}
if (-not $AllowedItemIds.Contains($itemId)) {
Fail "$Context stock references item outside that shop branch: $itemId"
}
Check-Shop-Stock-Value $prop.Value "$Context stock $itemId"
}
}
function Check-Shop($Shop, [string]$ScenarioId) {
if ($null -eq $Shop) {
return
}
if (($Shop -is [System.Array]) -or ($Shop -is [string])) {
Fail "Scenario $ScenarioId shop must be an object."
}
if (-not (Has-Prop $Shop "items")) {
Fail "Scenario $ScenarioId shop is missing items."
}
$shopItemsValue = $Shop.items
if (-not ($shopItemsValue -is [System.Array])) {
Fail "Scenario $ScenarioId shop items must be an array."
}
$shopItems = @($shopItemsValue)
$seenShopItems = New-Object System.Collections.Generic.HashSet[string]
foreach ($entry in $shopItems) {
$itemId = Resolve-Shop-Item-Id $entry "Scenario $ScenarioId shop"
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "Scenario $ScenarioId shop has an empty item id."
}
if (-not $seenShopItems.Add($itemId)) {
Fail "Scenario $ScenarioId shop has duplicate item: $itemId"
}
if (-not $itemIds.Contains($itemId)) {
Fail "Scenario $ScenarioId shop references unknown item: $itemId"
}
Check-Shop-Entry-Stock $entry $itemId "Scenario $ScenarioId shop"
$item = Find-Item-Def $itemId
if ([int](Get-Prop $item "price" 0) -le 0) {
Fail "Scenario $ScenarioId shop item $itemId has no positive price."
}
}
if (Has-Prop $Shop "stock") {
Check-Shop-Stock-Block $Shop.stock $seenShopItems "Scenario $ScenarioId shop"
}
$conditionalItems = Get-Prop $Shop "conditional_items" @()
foreach ($block in @($conditionalItems)) {
if ($block -is [string] -or $block -is [System.Array]) {
Fail "Scenario $ScenarioId shop has malformed conditional_items entry."
}
$blockFlags = Get-Prop $block "campaign_flags" $null
if ($null -eq $blockFlags -or @($blockFlags.PSObject.Properties).Count -le 0) {
Fail "Scenario $ScenarioId shop conditional_items entry must have campaign_flags."
}
Check-Required-Flags $blockFlags $ScenarioId "shop conditional_items"
$blockItems = Get-Prop $block "items" $null
if ($null -eq $blockItems) {
Fail "Scenario $ScenarioId shop conditional_items entry must have item array."
}
$seenConditionalItems = New-Object System.Collections.Generic.HashSet[string]
foreach ($baseItem in $shopItems) {
$seenConditionalItems.Add((Resolve-Shop-Item-Id $baseItem "Scenario $ScenarioId shop")) | Out-Null
}
foreach ($entry in @($blockItems)) {
$itemId = Resolve-Shop-Item-Id $entry "Scenario $ScenarioId shop conditional_items"
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "Scenario $ScenarioId shop conditional_items has an empty item id."
}
if (-not $seenConditionalItems.Add($itemId)) {
Fail "Scenario $ScenarioId shop conditional_items duplicates item already in that branch: $itemId"
}
if (-not $itemIds.Contains($itemId)) {
Fail "Scenario $ScenarioId shop conditional_items references unknown item: $itemId"
}
Check-Shop-Entry-Stock $entry $itemId "Scenario $ScenarioId shop conditional_items"
$item = Find-Item-Def $itemId
if ([int](Get-Prop $item "price" 0) -le 0) {
Fail "Scenario $ScenarioId shop conditional item $itemId has no positive price."
}
}
if (Has-Prop $block "stock") {
Check-Shop-Stock-Block $block.stock $seenConditionalItems "Scenario $ScenarioId shop conditional_items"
}
}
}
function Resolve-Cell($Cell, [string]$Context) {
if (-not ($Cell -is [System.Array]) -or $Cell.Count -lt 2) {
Fail "$Context has a malformed cell."
}
return @{ X = [int]$Cell[0]; Y = [int]$Cell[1] }
}
function Cell-Key([int]$X, [int]$Y) {
return "$X,$Y"
}
function Check-Formation($Formation, [string]$ScenarioId, [int]$Width, [int]$Height, $Rows, $Deployments) {
if ($null -eq $Formation) {
return
}
if (($Formation -is [System.Array]) -or ($Formation -is [string])) {
Fail "Scenario $ScenarioId formation must be an object."
}
if (-not (Has-Prop $Formation "cells")) {
Fail "Scenario $ScenarioId formation is missing cells."
}
$cellsValue = $Formation.cells
if (-not ($cellsValue -is [System.Array])) {
Fail "Scenario $ScenarioId formation cells must be an array."
}
$formationKeys = New-Object System.Collections.Generic.HashSet[string]
foreach ($cellEntry in @($cellsValue)) {
$cell = Resolve-Cell $cellEntry "Scenario $ScenarioId formation"
$x = [int]$cell.X
$y = [int]$cell.Y
if ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height) {
Fail "Scenario $ScenarioId formation cell is outside map at [$x,$y]."
}
if (-not $formationKeys.Add((Cell-Key $x $y))) {
Fail "Scenario $ScenarioId formation has duplicate cell: [$x,$y]"
}
$terrainKey = Terrain-At $Rows $x $y
if ([int]$terrain.$terrainKey.move_cost.foot -ge 99 -and [int]$terrain.$terrainKey.move_cost.mounted -ge 99 -and [int]$terrain.$terrainKey.move_cost.archer -ge 99) {
Fail "Scenario $ScenarioId formation cell [$x,$y] is impassable for normal units."
}
}
$playerCount = 0
foreach ($deployment in $Deployments) {
$team = [string](Get-Prop $deployment "team" "enemy")
$pos = Get-Prop $deployment "pos" @()
if ($pos.Count -lt 2) {
continue
}
$x = [int]$pos[0]
$y = [int]$pos[1]
$key = Cell-Key $x $y
if ($team -eq "player") {
$playerCount += 1
if (-not $formationKeys.Contains($key)) {
Fail "Scenario $ScenarioId player deployment $($deployment.unit_id) starts outside formation cells."
}
} elseif ($formationKeys.Contains($key)) {
Fail "Scenario $ScenarioId formation overlaps enemy deployment at [$x,$y]."
}
}
if ($formationKeys.Count -lt $playerCount) {
Fail "Scenario $ScenarioId formation has fewer cells than player deployments."
}
}
function Check-Roster($Roster, [string]$ScenarioId, $Deployments) {
if ($null -eq $Roster) {
return
}
if (($Roster -is [System.Array]) -or ($Roster -is [string])) {
Fail "Scenario $ScenarioId roster must be an object."
}
$playerOfficerIds = New-Object System.Collections.Generic.HashSet[string]
$playerUnitIds = New-Object System.Collections.Generic.HashSet[string]
$officerToUnitIds = @{}
$playerCount = 0
foreach ($deployment in $Deployments) {
if ([string](Get-Prop $deployment "team" "enemy") -ne "player") {
continue
}
$playerCount += 1
$unitId = [string](Get-Prop $deployment "unit_id" (Get-Prop $deployment "id" ""))
if (-not [string]::IsNullOrWhiteSpace($unitId)) {
$playerUnitIds.Add($unitId) | Out-Null
}
$officerId = [string](Get-Prop $deployment "officer_id" "")
if (-not [string]::IsNullOrWhiteSpace($officerId)) {
$playerOfficerIds.Add($officerId) | Out-Null
if (-not $officerToUnitIds.ContainsKey($officerId)) {
$officerToUnitIds[$officerId] = @()
}
$officerToUnitIds[$officerId] += $unitId
}
}
if ($playerCount -le 0) {
Fail "Scenario $ScenarioId roster has no player deployments."
}
$maxUnits = [int](Get-Prop $Roster "max_units" $playerCount)
if ($maxUnits -le 0 -or $maxUnits -gt $playerCount) {
Fail "Scenario $ScenarioId roster max_units must be between 1 and player deployment count."
}
$requiredValue = @()
if (Has-Prop $Roster "required_officers") {
$requiredValue = $Roster.required_officers
}
if (-not ($requiredValue -is [System.Array])) {
Fail "Scenario $ScenarioId roster required_officers must be an array."
}
$required = New-Object System.Collections.Generic.HashSet[string]
$requiredDeploymentUnitIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($officerIdValue in @($requiredValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId roster has an empty required officer."
}
if (-not $required.Add($officerId)) {
Fail "Scenario $ScenarioId roster has duplicate required officer: $officerId"
}
if (-not $playerOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId roster requires officer not in player deployments: $officerId"
}
foreach ($unitId in @($officerToUnitIds[$officerId])) {
if (-not [string]::IsNullOrWhiteSpace([string]$unitId)) {
$requiredDeploymentUnitIds.Add([string]$unitId) | Out-Null
}
}
}
$requiredUnitsValue = @()
if (Has-Prop $Roster "required_units") {
$requiredUnitsValue = $Roster.required_units
}
if (-not ($requiredUnitsValue -is [System.Array])) {
Fail "Scenario $ScenarioId roster required_units must be an array."
}
$requiredUnits = New-Object System.Collections.Generic.HashSet[string]
foreach ($unitIdValue in @($requiredUnitsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId roster has an empty required unit."
}
if (-not $requiredUnits.Add($unitId)) {
Fail "Scenario $ScenarioId roster has duplicate required unit: $unitId"
}
if (-not $playerUnitIds.Contains($unitId)) {
Fail "Scenario $ScenarioId roster requires unit not in player deployments: $unitId"
}
$requiredDeploymentUnitIds.Add($unitId) | Out-Null
}
if ($requiredDeploymentUnitIds.Count -gt $maxUnits) {
Fail "Scenario $ScenarioId roster requires more units than max_units."
}
}
function Check-Joined-Deployment-Requirements($Deployments, [string]$ScenarioId) {
foreach ($deployment in $Deployments) {
if ([string](Get-Prop $deployment "team" "enemy") -ne "player") {
continue
}
if (-not [bool](Get-Prop $deployment "requires_joined" $false)) {
continue
}
$officerId = [string](Get-Prop $deployment "officer_id" "")
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId deployment $($deployment.unit_id) requires joined officer but has no officer_id."
}
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId deployment $($deployment.unit_id) requires officer before they join: $officerId"
}
}
}
function Check-Required-Officers-Joined($Roster, $Conditions, [string]$ScenarioId) {
if ($null -ne $Roster -and (Has-Prop $Roster "required_officers")) {
foreach ($officerIdValue in @($Roster.required_officers)) {
$officerId = [string]$officerIdValue
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId roster requires officer before they join: $officerId"
}
}
}
$conditionRequired = New-Object System.Collections.Generic.HashSet[string]
if ($null -ne $Conditions -and (Has-Prop $Conditions "defeat")) {
Collect-Required-Player-Officers $Conditions.defeat $conditionRequired
}
foreach ($officerId in $conditionRequired) {
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId defeat condition references officer before they join: $officerId"
}
}
}
function Collect-Required-Player-Officers($Condition, $Result) {
if ($Condition -is [System.Array]) {
foreach ($entry in $Condition) {
Collect-Required-Player-Officers $entry $Result
}
return
}
if (-not (Has-Prop $Condition "type")) {
return
}
$type = [string]$Condition.type
if ($type -eq "any_officer_defeated" -and [string](Get-Prop $Condition "team" "") -eq "player") {
foreach ($officerId in (Get-Prop $Condition "officer_ids" @())) {
$Result.Add([string]$officerId) | Out-Null
}
}
if (($type -eq "all" -or $type -eq "any") -and (Has-Prop $Condition "conditions")) {
Collect-Required-Player-Officers $Condition.conditions $Result
}
}
function Check-Roster-Condition-Consistency($Roster, $Conditions, [string]$ScenarioId) {
$conditionRequired = New-Object System.Collections.Generic.HashSet[string]
if ($null -ne $Conditions -and (Has-Prop $Conditions "defeat")) {
Collect-Required-Player-Officers $Conditions.defeat $conditionRequired
}
if ($conditionRequired.Count -le 0) {
return
}
$rosterRequired = New-Object System.Collections.Generic.HashSet[string]
if ($null -ne $Roster -and (Has-Prop $Roster "required_officers")) {
foreach ($officerIdValue in @($Roster.required_officers)) {
$rosterRequired.Add([string]$officerIdValue) | Out-Null
}
}
foreach ($officerId in $conditionRequired) {
if (-not $rosterRequired.Contains($officerId)) {
Fail "Scenario $ScenarioId defeat condition officer must be required in roster: $officerId"
}
}
}
function Check-Officer-Transition-List($Value, [string]$ScenarioId, [string]$Context) {
$result = New-Object System.Collections.Generic.HashSet[string]
if ($null -eq $Value) {
return ,$result
}
foreach ($officerIdValue in @($Value)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId $Context has empty officer id."
}
if (-not $officerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId $Context references unknown officer: $officerId"
}
if (-not $result.Add($officerId)) {
Fail "Scenario $ScenarioId $Context has duplicate officer: $officerId"
}
}
return ,$result
}
function Check-Post-Battle-Choices($Choices, [string]$ScenarioId) {
if ($null -eq $Choices) {
return
}
if (-not ($Choices -is [System.Array])) {
Fail "Scenario $ScenarioId post_battle_choices must be an array."
}
$seenChoices = New-Object System.Collections.Generic.HashSet[string]
foreach ($choice in @($Choices)) {
if ($choice -is [string] -or $choice -is [System.Array]) {
Fail "Scenario $ScenarioId has malformed post-battle choice."
}
$choiceId = [string](Get-Prop $choice "id" "")
if ([string]::IsNullOrWhiteSpace($choiceId)) {
Fail "Scenario $ScenarioId post-battle choice has empty id."
}
if (-not ($choiceId -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId post-battle choice has unstable id: $choiceId"
}
if (-not $seenChoices.Add($choiceId)) {
Fail "Scenario $ScenarioId has duplicate post-battle choice: $choiceId"
}
if ([string]::IsNullOrWhiteSpace([string](Get-Prop $choice "label" ""))) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has empty label."
}
if ([int](Get-Prop $choice "gold" 0) -lt 0) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has negative gold."
}
$choiceItems = @()
if (Has-Prop $choice "items") {
$choiceItems = @($choice.items)
}
foreach ($itemId in @($choiceItems)) {
if (-not $itemIds.Contains([string]$itemId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId grants unknown item: $itemId"
}
}
$choiceJoins = Check-Officer-Transition-List (Get-Prop $choice "join_officers" $null) $ScenarioId "post-battle choice $choiceId join_officers"
$choiceLeaves = Check-Officer-Transition-List (Get-Prop $choice "leave_officers" $null) $ScenarioId "post-battle choice $choiceId leave_officers"
foreach ($officerId in $choiceJoins) {
if ($choiceLeaves.Contains($officerId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId both joins and leaves officer: $officerId"
}
if ($joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId joins already joined officer: $officerId"
}
}
foreach ($officerId in $choiceLeaves) {
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId leaves officer before they join: $officerId"
}
}
$setFlags = Get-Prop $choice "set_flags" $null
if ($null -eq $setFlags) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has no set_flags."
}
if ($setFlags -is [string] -or $setFlags -is [System.Array]) {
Fail "Scenario $ScenarioId post-battle choice $choiceId set_flags must be an object."
}
foreach ($flag in $setFlags.PSObject.Properties) {
if ([string]::IsNullOrWhiteSpace([string]$flag.Name)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has empty flag name."
}
if (-not ([string]$flag.Name -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has unstable flag name: $($flag.Name)"
}
if ($null -eq $flag.Value -or $flag.Value -is [System.Array] -or $flag.Value -is [pscustomobject]) {
Fail "Scenario $ScenarioId post-battle choice $choiceId flag $($flag.Name) must be scalar."
}
$flagName = [string]$flag.Name
$flagKind = Get-Flag-Value-Kind $flag.Value
if ($knownFlagValueKinds.ContainsKey($flagName) -and $knownFlagValueKinds[$flagName] -ne $flagKind) {
Fail "Scenario $ScenarioId post-battle choice $choiceId changes flag type: $flagName"
}
$knownFlagIds.Add($flagName) | Out-Null
$knownFlagValueKinds[$flagName] = $flagKind
}
}
}
function Check-Required-Flags($RequiredFlags, [string]$ScenarioId, [string]$Context) {
if ($null -eq $RequiredFlags) {
return
}
if ($RequiredFlags -is [string] -or $RequiredFlags -is [System.Array]) {
Fail "Scenario $ScenarioId $Context campaign_flags must be an object."
}
foreach ($flag in $RequiredFlags.PSObject.Properties) {
$flagName = [string]$flag.Name
if ([string]::IsNullOrWhiteSpace($flagName)) {
Fail "Scenario $ScenarioId $Context has empty required flag name."
}
if (-not ($flagName -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId $Context has unstable required flag name: $flagName"
}
if (-not $knownFlagIds.Contains($flagName)) {
Fail "Scenario $ScenarioId $Context requires unknown campaign flag: $flagName"
}
if ($null -eq $flag.Value -or $flag.Value -is [System.Array] -or $flag.Value -is [pscustomobject]) {
Fail "Scenario $ScenarioId $Context required flag $flagName must be scalar."
}
$flagKind = Get-Flag-Value-Kind $flag.Value
if ($knownFlagValueKinds.ContainsKey($flagName) -and $knownFlagValueKinds[$flagName] -ne $flagKind) {
Fail "Scenario $ScenarioId $Context required flag $flagName has type $flagKind but was defined as $($knownFlagValueKinds[$flagName])."
}
}
}
function Check-Briefing($Briefing, [string]$ScenarioId) {
if ($null -eq $Briefing) {
return
}
if ($Briefing -is [string] -or $Briefing -is [System.Array]) {
Fail "Scenario $ScenarioId briefing must be an object."
}
$lines = Get-Prop $Briefing "lines" @()
foreach ($line in @($lines)) {
if ([string]::IsNullOrWhiteSpace([string]$line)) {
Fail "Scenario $ScenarioId briefing has an empty line."
}
}
$conditionalLines = Get-Prop $Briefing "conditional_lines" @()
foreach ($block in @($conditionalLines)) {
if ($block -is [string] -or $block -is [System.Array]) {
Fail "Scenario $ScenarioId briefing has malformed conditional_lines entry."
}
$blockFlags = Get-Prop $block "campaign_flags" $null
if ($null -eq $blockFlags -or @($blockFlags.PSObject.Properties).Count -le 0) {
Fail "Scenario $ScenarioId briefing conditional_lines entry must have campaign_flags."
}
Check-Required-Flags $blockFlags $ScenarioId "briefing conditional_lines"
$blockLines = Get-Prop $block "lines" $null
if ($null -eq $blockLines) {
Fail "Scenario $ScenarioId briefing conditional_lines entry must have line array."
}
foreach ($line in @($blockLines)) {
if ([string]::IsNullOrWhiteSpace([string]$line)) {
Fail "Scenario $ScenarioId briefing conditional_lines has an empty line."
}
}
}
}
function Check-Dialogue-Line($Line, [string]$Context) {
if ($Line -is [string]) {
if ([string]::IsNullOrWhiteSpace([string]$Line)) {
Fail "$Context has an empty dialogue line."
}
return
}
if ($Line -is [System.Array] -or -not (Has-Prop $Line "text")) {
Fail "$Context has malformed dialogue line."
}
if ([string]::IsNullOrWhiteSpace([string](Get-Prop $Line "text" ""))) {
Fail "$Context has an empty dialogue text."
}
if (Has-Prop $Line "portrait") {
Check-Portrait-Path (Get-Prop $Line "portrait" "") "$Context portrait"
}
if (Has-Prop $Line "side") {
$side = ([string](Get-Prop $Line "side" "")).Trim().ToLowerInvariant()
if (-not $validDialogueSides.Contains($side)) {
Fail "$Context has unknown dialogue side: $side"
}
}
}
function Check-Portrait-Path($Portrait, [string]$Context) {
if ($Portrait -is [System.Array] -or $Portrait -is [System.Management.Automation.PSCustomObject]) {
Fail "$Context must be a string resource path."
}
if ([string]::IsNullOrWhiteSpace([string]$Portrait)) {
Fail "$Context must not be empty."
}
if ([string]$Portrait -notmatch $validPortraitPathPattern) {
Fail "$Context must be a res:// image path."
}
}
function Check-Post-Battle-Dialogue($Dialogue, [string]$ScenarioId) {
if ($null -eq $Dialogue) {
return
}
$lines = @($Dialogue)
if ($lines.Count -le 0) {
Fail "Scenario $ScenarioId post_battle_dialogue must not be empty."
}
foreach ($line in $lines) {
Check-Dialogue-Line $line "Scenario $ScenarioId post_battle_dialogue"
}
}
function Check-Join-Officer-Rewards($Rewards, [string]$ScenarioId) {
if ($null -eq $Rewards) {
return
}
$joinOfficers = Check-Officer-Transition-List (Get-Prop $Rewards "join_officers" $null) $ScenarioId "rewards join_officers"
$leaveOfficers = Check-Officer-Transition-List (Get-Prop $Rewards "leave_officers" $null) $ScenarioId "rewards leave_officers"
foreach ($officerId in $joinOfficers) {
if ($leaveOfficers.Contains($officerId)) {
Fail "Scenario $ScenarioId rewards both joins and leaves officer: $officerId"
}
if ($joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId rewards joins already joined officer: $officerId"
}
}
foreach ($officerId in $leaveOfficers) {
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId rewards leaves officer before they join: $officerId"
}
}
foreach ($officerId in $joinOfficers) {
$joinedOfficerIds.Add($officerId) | Out-Null
}
foreach ($officerId in $leaveOfficers) {
$joinedOfficerIds.Remove($officerId) | Out-Null
}
}
Check-Raw-Skills-Arrays
Check-Item-Effects
Check-Terrain-Definitions
Check-Skill-Definitions
Check-Class-Equipment-Slots
Check-Class-Promotions
foreach ($classProp in $classes.PSObject.Properties) {
Check-Skill-Refs (Get-Prop $classProp.Value "skills" @()) "Class $($classProp.Name)"
}
$officerNames = New-Object System.Collections.Generic.HashSet[string]
foreach ($officerProp in $officers.PSObject.Properties) {
$officerName = [string](Get-Prop $officerProp.Value "name" "")
if ([string]::IsNullOrWhiteSpace($officerName)) {
Fail "Officer $($officerProp.Name) has empty name."
}
$normalizedOfficerName = $officerName.Trim().ToLowerInvariant()
if (-not $officerNames.Add($normalizedOfficerName)) {
Fail "Officer $($officerProp.Name) has duplicate name: $officerName"
}
Check-Skill-Refs (Get-Prop $officerProp.Value "skills" @()) "Officer $($officerProp.Name)"
Check-Officer-Equipment $officerProp.Name $officerProp.Value
if (Has-Prop $officerProp.Value "portrait") {
Check-Portrait-Path (Get-Prop $officerProp.Value "portrait" "") "Officer $($officerProp.Name) portrait"
}
}
foreach ($scenario in $campaign.scenarios) {
$scenarioPath = ([string]$scenario.path).Replace("res://", "")
$scenarioData = Read-Json $scenarioPath
$scenarioId = [string]$scenarioData.id
if ($scenarioId -ne [string]$scenario.id) {
Fail "Campaign id $($scenario.id) does not match scenario file id $scenarioId."
}
$map = $scenarioData.map
$width = [int]$map.width
$height = [int]$map.height
$rows = @($map.terrain)
if ($rows.Count -ne $height) {
Fail "Scenario $scenarioId map height mismatch."
}
foreach ($row in $rows) {
if ([string]$row.Length -ne [string]$width) {
Fail "Scenario $scenarioId map row width mismatch: $row"
}
foreach ($char in $row.ToCharArray()) {
if (-not $terrainKeys.Contains([string]$char)) {
Fail "Scenario $scenarioId uses unknown terrain key: $char"
}
}
}
$eventIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($event in (Get-Prop $scenarioData "events" @())) {
if (Has-Prop $event "id") {
$eventIds.Add([string]$event.id) | Out-Null
}
}
$knownUnitIds = Collect-Scenario-Unit-Ids $scenarioData
if (Has-Prop $scenarioData "conditions") {
if (Has-Prop $scenarioData.conditions "victory") {
Check-Condition $scenarioData.conditions.victory $scenarioId $eventIds $width $height $rows $knownUnitIds
}
if (Has-Prop $scenarioData.conditions "defeat") {
Check-Condition $scenarioData.conditions.defeat $scenarioId $eventIds $width $height $rows $knownUnitIds
}
}
$seenIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($deployment in $scenarioData.deployments) {
Check-Deployment $deployment $scenarioId $width $height $rows $seenIds
}
Check-Briefing (Get-Prop $scenarioData "briefing" $null) $scenarioId
Check-Shop (Get-Prop $scenarioData "shop" $null) $scenarioId
Check-Joined-Deployment-Requirements $scenarioData.deployments $scenarioId
Check-Roster (Get-Prop $scenarioData "roster" $null) $scenarioId $scenarioData.deployments
Check-Required-Officers-Joined (Get-Prop $scenarioData "roster" $null) (Get-Prop $scenarioData "conditions" $null) $scenarioId
Check-Roster-Condition-Consistency (Get-Prop $scenarioData "roster" $null) (Get-Prop $scenarioData "conditions" $null) $scenarioId
Check-Formation (Get-Prop $scenarioData "formation" $null) $scenarioId $width $height $rows $scenarioData.deployments
foreach ($event in (Get-Prop $scenarioData "events" @())) {
$eventId = [string](Get-Prop $event "id" "event")
$when = Get-Prop $event "when" $null
if ($null -eq $when -or $when -is [string] -or $when -is [System.Array]) {
Fail "Scenario $scenarioId event $eventId has malformed when."
}
Check-Required-Flags (Get-Prop $when "campaign_flags" $null) $scenarioId "event $eventId"
$trigger = [string]$event.when.type
if (-not $validTriggers.Contains($trigger)) {
Fail "Scenario $scenarioId has unknown trigger: $trigger"
}
if ($trigger -eq "unit_reaches_tile") {
Check-Unit-Reaches-Event-When $when $scenarioId $width $height $rows $knownUnitIds
}
foreach ($action in (Get-Prop $event "actions" @())) {
$actionType = [string]$action.type
if (-not $validActions.Contains($actionType)) {
Fail "Scenario $scenarioId has unknown event action: $actionType"
}
if ($actionType -eq "spawn_deployment") {
Check-Deployment $action.deployment $scenarioId $width $height $rows $seenIds
}
if ($actionType -eq "spawn_deployments") {
foreach ($deployment in $action.deployments) {
Check-Deployment $deployment $scenarioId $width $height $rows $seenIds
}
}
if ($actionType -eq "dialogue") {
if (Has-Prop $action "lines") {
$rawLines = Get-Prop $action "lines" $null
$lines = @($rawLines)
if ($null -eq $rawLines -or $lines.Count -le 0) {
Fail "Scenario $scenarioId event $eventId dialogue action has malformed lines."
}
foreach ($line in $lines) {
Check-Dialogue-Line $line "Scenario $scenarioId event $eventId dialogue"
}
} else {
Check-Dialogue-Line $action "Scenario $scenarioId event $eventId dialogue"
}
}
}
}
foreach ($itemId in (Get-Prop (Get-Prop $scenarioData "rewards" @{}) "items" @())) {
if (-not $itemIds.Contains([string]$itemId)) {
Fail "Scenario $scenarioId rewards unknown item: $itemId"
}
}
Check-Join-Officer-Rewards (Get-Prop $scenarioData "rewards" $null) $scenarioId
Check-Post-Battle-Dialogue (Get-Prop $scenarioData "post_battle_dialogue" $null) $scenarioId
Check-Post-Battle-Choices (Get-Prop $scenarioData "post_battle_choices" $null) $scenarioId
}
"data validation ok"