$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") $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") $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" } } 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)" } if ([int](Get-Prop $effect "amount" 0) -le 0) { Fail "Item $itemId effect $($effect.type) must have positive amount." } } } } 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." } } } } } 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($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" } $item = Find-Item-Def $itemId if ([int](Get-Prop $item "price" 0) -le 0) { Fail "Scenario $ScenarioId shop item $itemId has no positive price." } } $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" } $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." } } } } 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" } } 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"