From 39356e50388f6e6d76f2bd702d1565887aa0e986 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 24 Jun 2026 21:52:24 +0900 Subject: [PATCH] Add cards Excel roundtrip tools --- cards_to_excel.bat | 7 + data/cards.xlsx | Bin 0 -> 24565 bytes excel_to_cards.bat | 7 + tools/cards/cards_excel.ps1 | 629 ++++++++++++++++++++++++++++++++++++ 4 files changed, 643 insertions(+) create mode 100644 cards_to_excel.bat create mode 100644 data/cards.xlsx create mode 100644 excel_to_cards.bat create mode 100644 tools/cards/cards_excel.ps1 diff --git a/cards_to_excel.bat b/cards_to_excel.bat new file mode 100644 index 0000000..7e92401 --- /dev/null +++ b/cards_to_excel.bat @@ -0,0 +1,7 @@ +@echo off +setlocal +chcp 65001 >nul +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\cards\cards_excel.ps1" export +echo. +echo Press any key to close this window. +pause >nul diff --git a/data/cards.xlsx b/data/cards.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..57de5bbff978d26148c1e622bdb26994329e1824 GIT binary patch literal 24565 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 +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\cards\cards_excel.ps1" import +echo. +echo Press any key to close this window. +pause >nul diff --git a/tools/cards/cards_excel.ps1 b/tools/cards/cards_excel.ps1 new file mode 100644 index 0000000..e70d47e --- /dev/null +++ b/tools/cards/cards_excel.ps1 @@ -0,0 +1,629 @@ +param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet('export', 'import')] + [string]$Action, + [string]$JsonPath, + [string]$XlsxPath, + [string]$OutJsonPath +) + +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.IO.Compression.FileSystem +Add-Type -AssemblyName System.IO.Compression + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +if ([string]::IsNullOrWhiteSpace($JsonPath)) { $JsonPath = Join-Path $repoRoot 'data\cards.json' } +if ([string]::IsNullOrWhiteSpace($XlsxPath)) { $XlsxPath = Join-Path $repoRoot 'data\cards.xlsx' } +if ([string]::IsNullOrWhiteSpace($OutJsonPath)) { $OutJsonPath = $JsonPath } + +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Escape-Xml([string]$Text) { + if ($null -eq $Text) { return '' } + return [System.Security.SecurityElement]::Escape($Text) +} + +function Get-ColumnName([int]$Index) { + $n = $Index + $name = '' + while ($n -gt 0) { + $n-- + $name = [char][int](65 + ($n % 26)) + $name + $n = [math]::Floor($n / 26) + } + return $name +} + +function Get-ColumnIndex([string]$Name) { + $n = 0 + foreach ($ch in $Name.ToCharArray()) { + if ($ch -match '[A-Z]') { + $n = $n * 26 + ([int][char]$ch - 64) + } + } + return $n +} + +function Get-CellRef([int]$Col, [int]$Row) { + return (Get-ColumnName $Col) + $Row +} + +function Has-MapKey($Map, $Key) { + if ($null -eq $Map) { return $false } + if ($null -eq $Key) { return $false } + if ($Key -is [string] -and [string]::IsNullOrWhiteSpace($Key)) { return $false } + foreach ($existingKey in $Map.Keys) { + if ($existingKey -eq $Key) { return $true } + } + return $false +} + +function Get-ScalarType($Value) { + if ($null -eq $Value) { return 'null' } + if ($Value -is [bool]) { return 'boolean' } + if ($Value -is [byte] -or $Value -is [sbyte] -or + $Value -is [int16] -or $Value -is [uint16] -or + $Value -is [int32] -or $Value -is [uint32] -or + $Value -is [int64] -or $Value -is [uint64] -or + $Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) { return 'number' } + if ($Value -is [string]) { return 'string' } + return 'string' +} + +function Get-CardSchema($Cards) { + $schema = [ordered]@{} + foreach ($cardEntry in $Cards.PSObject.Properties) { + $card = $cardEntry.Value + foreach ($prop in $card.PSObject.Properties) { + $kind = Get-ScalarType $prop.Value + if (-not (Has-MapKey $schema $prop.Name)) { + $schema[$prop.Name] = $kind + } elseif ($schema[$prop.Name] -ne $kind -and $kind -ne 'null') { + $schema[$prop.Name] = 'string' + } + } + } + return $schema +} + +function Get-ColumnWidth([string]$Header, [string]$Type) { + switch ($Header) { + 'id' { return 18 } + 'name' { return 24 } + 'desc' { return 48 } + 'image' { return 36 } + 'fx' { return 36 } + 'kind' { return 12 } + 'class' { return 12 } + 'rarity' { return 12 } + default { + if ($Type -eq 'boolean') { return 10 } + if ($Type -eq 'number') { return 12 } + return 16 + } + } +} + +function To-InvariantNumber($Value) { + return [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, '{0}', $Value) +} + +function New-HeaderCellXml([string]$Ref, [string]$Text) { + $escaped = Escape-Xml $Text + return "$escaped" +} + +function New-TextCellXml([string]$Ref, [string]$Text) { + $escaped = Escape-Xml $Text + return "$escaped" +} + +function New-NumberCellXml([string]$Ref, $Value) { + if ($null -eq $Value) { return $null } + if ($Value -is [string] -and $Value -eq '') { return $null } + return "$(To-InvariantNumber $Value)" +} + +function New-BoolCellXml([string]$Ref, $Value) { + if ($null -eq $Value) { return $null } + if ($Value -is [string] -and $Value -eq '') { return $null } + $bool = $false + if ($Value -is [bool]) { + $bool = $Value + } else { + $text = [string]$Value + if ($text -match '^(?i:true|1|yes|y)$') { $bool = $true } + elseif ($text -match '^(?i:false|0|no|n)$') { $bool = $false } + else { return $null } + } + $n = if ($bool) { 1 } else { 0 } + return "$n" +} + +function New-CellXml([string]$Ref, $Value, [string]$Type) { + switch ($Type) { + 'number' { return New-NumberCellXml $Ref $Value } + 'boolean' { return New-BoolCellXml $Ref $Value } + default { + if ($null -eq $Value) { + return New-TextCellXml $Ref '' + } + return New-TextCellXml $Ref ([string]$Value) + } + } +} + +function Get-WorksheetXml([string]$SheetName, [string[]]$Headers, [object[]]$Rows, [hashtable]$TypeMap) { + $maxCol = $Headers.Count + $lastCol = Get-ColumnName $maxCol + $rowCount = $Rows.Count + 1 + $colsXml = New-Object System.Collections.Generic.List[string] + for ($i = 0; $i -lt $Headers.Count; $i++) { + $header = $Headers[$i] + $type = if (Has-MapKey $TypeMap $header) { [string]$TypeMap[$header] } else { 'string' } + $width = Get-ColumnWidth $header $type + $colsXml.Add("") + } + + $rowsXml = New-Object System.Collections.Generic.List[string] + $headerCells = New-Object System.Collections.Generic.List[string] + for ($i = 0; $i -lt $Headers.Count; $i++) { + $headerCells.Add((New-HeaderCellXml (Get-CellRef ($i + 1) 1) $Headers[$i])) + } + $rowsXml.Add("$($headerCells -join '')") + + for ($r = 0; $r -lt $Rows.Count; $r++) { + $row = $Rows[$r] + $cells = New-Object System.Collections.Generic.List[string] + for ($c = 0; $c -lt $Headers.Count; $c++) { + $header = $Headers[$c] + $type = if (Has-MapKey $TypeMap $header) { [string]$TypeMap[$header] } else { 'string' } + $value = $null + if (Has-MapKey $row $header) { $value = $row[$header] } + $cellXml = New-CellXml (Get-CellRef ($c + 1) ($r + 2)) $value $type + if ($null -ne $cellXml) { $cells.Add($cellXml) } + } + $rowsXml.Add("$($cells -join '')") + } + + $sheetView = '' + $cols = '' + ($colsXml -join '') + '' + $sheetData = '' + ($rowsXml -join '') + '' + $autoFilter = "" + return @" + + + $sheetView + + $cols + $sheetData + $autoFilter + + +"@ +} + +function Get-StylesXml { + return @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ +} + +function Get-WorkbookXml([string[]]$SheetNames) { + $sheetsXml = New-Object System.Collections.Generic.List[string] + for ($i = 0; $i -lt $SheetNames.Count; $i++) { + $sheetsXml.Add("") + } + return @" + + + + $($sheetsXml -join '') + + +"@ +} + +function Get-WorkbookRelsXml([int]$SheetCount) { + $rels = New-Object System.Collections.Generic.List[string] + for ($i = 1; $i -le $SheetCount; $i++) { + $rels.Add("") + } + $rels.Add("") + return @" + + + $($rels -join '') + +"@ +} + +function Get-RootRelsXml { + return @" + + + + +"@ +} + +function Get-ContentTypesXml([int]$SheetCount) { + $overrides = New-Object System.Collections.Generic.List[string] + for ($i = 1; $i -le $SheetCount; $i++) { + $overrides.Add("") + } + $overrides.Add('') + $overrides.Add('') + return @" + + + + + $($overrides -join '') + +"@ +} + +function Write-Xlsx([string]$Path, [hashtable]$Parts) { + $dir = Split-Path -Parent $Path + if (-not [string]::IsNullOrWhiteSpace($dir) -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + if (Test-Path $Path) { + Remove-Item -LiteralPath $Path -Force + } + $file = [System.IO.File]::Open($Path, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite) + try { + $zip = New-Object System.IO.Compression.ZipArchive($file, [System.IO.Compression.ZipArchiveMode]::Create, $false) + try { + foreach ($entryName in $Parts.Keys) { + $entry = $zip.CreateEntry($entryName) + $stream = $entry.Open() + $writer = New-Object System.IO.StreamWriter($stream, $utf8NoBom) + try { + $writer.Write([string]$Parts[$entryName]) + } finally { + $writer.Dispose() + $stream.Dispose() + } + } + } finally { + $zip.Dispose() + } + } finally { + $file.Dispose() + } +} + +function Read-XlsxXml([string]$Path, [string]$EntryName) { + $zip = [System.IO.Compression.ZipFile]::OpenRead($Path) + try { + $entry = $zip.GetEntry($EntryName) + if ($null -eq $entry) { throw "Missing XLSX entry: $EntryName" } + $stream = $entry.Open() + try { + $reader = New-Object System.IO.StreamReader($stream, $utf8NoBom) + try { return $reader.ReadToEnd() } finally { $reader.Dispose() } + } finally { + $stream.Dispose() + } + } finally { + $zip.Dispose() + } +} + +function Read-SharedStrings([string]$Path) { + try { + $xmlText = Read-XlsxXml $Path 'xl/sharedStrings.xml' + } catch { + return @() + } + [xml]$xml = $xmlText + $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $ns.AddNamespace('x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main') + $items = $xml.SelectNodes('/x:sst/x:si', $ns) + $values = New-Object System.Collections.Generic.List[string] + foreach ($item in $items) { + $values.Add([string]$item.InnerText) + } + return $values.ToArray() +} + +function Read-WorksheetRows([string]$XmlText, [string[]]$SharedStrings) { + [xml]$xml = $XmlText + $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $ns.AddNamespace('x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main') + $rows = $xml.SelectNodes('/x:worksheet/x:sheetData/x:row', $ns) + $parsed = @() + foreach ($row in $rows) { + $cells = @{} + foreach ($cell in @($row.ChildNodes)) { + if ($cell.Name -ne 'c') { continue } + $ref = [string]$cell.Attributes['r'].Value + $col = Get-ColumnIndex (($ref -replace '\d+$', '')) + $type = [string]$cell.Attributes['t'].Value + $text = [string]$cell.InnerText + if ($type -eq 's' -and $text -match '^\d+$') { + $index = [int]$text + if ($index -ge 0 -and $index -lt $SharedStrings.Count) { + $text = [string]$SharedStrings[$index] + } + } + $cells[$col] = $text + } + $parsed += ,$cells + } + return $parsed +} + +function Convert-CellValue([string]$Text, [string]$Type) { + if ($null -eq $Text -or $Text -eq '') { return $null } + switch ($Type) { + 'number' { + $num = 0 + if ([double]::TryParse($Text, [System.Globalization.NumberStyles]::Any, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$num)) { + if ([math]::Abs($num - [math]::Round($num)) -lt 0.0000001) { return [int64][math]::Round($num) } + return $num + } + return $null + } + 'boolean' { + if ($Text -match '^(?i:true|1|yes|y)$') { return $true } + if ($Text -match '^(?i:false|0|no|n)$') { return $false } + return $null + } + default { + return $Text + } + } +} + +function Export-Cards { + $source = Get-Content -LiteralPath $JsonPath -Raw -Encoding utf8 | ConvertFrom-Json + $schema = Get-CardSchema $source.cards + $cardCore = @('id', 'name', 'cost', 'kind', 'rarity', 'class', 'desc', 'image', 'fx') + $cardExtras = @($schema.Keys | Where-Object { $_ -notin $cardCore } | Sort-Object) + $cardHeaders = @($cardCore + $cardExtras) + + $maxDeckSize = 0 + foreach ($deckEntry in $source.starterDecks.PSObject.Properties) { + $deckSize = @($deckEntry.Value).Count + if ($deckSize -gt $maxDeckSize) { + $maxDeckSize = $deckSize + } + } + if ($maxDeckSize -lt 1) { $maxDeckSize = 1 } + + $starterDeckHeaders = New-Object System.Collections.Generic.List[string] + $starterDeckHeaders.Add('class') + for ($i = 1; $i -le $maxDeckSize; $i++) { + $starterDeckHeaders.Add("slot$i") + } + + $cardRows = New-Object System.Collections.Generic.List[object] + foreach ($cardEntry in $source.cards.PSObject.Properties) { + $cardId = $cardEntry.Name + $card = $cardEntry.Value + $row = [ordered]@{ id = $cardId } + foreach ($header in $cardHeaders) { + if ($header -eq 'id') { continue } + if ($card.PSObject.Properties.Name -contains $header) { + $row[$header] = $card.$header + } else { + $row[$header] = $null + } + } + $cardRows.Add($row) + } + + $deckRows = New-Object System.Collections.Generic.List[object] + foreach ($deckEntry in $source.starterDecks.PSObject.Properties) { + $cls = $deckEntry.Name + $deck = @($deckEntry.Value) + $row = [ordered]@{ class = $cls } + for ($i = 1; $i -le $maxDeckSize; $i++) { + $key = "slot$i" + $row[$key] = if ($i -le $deck.Count) { $deck[$i - 1] } else { $null } + } + $deckRows.Add($row) + } + + $cardSheet = Get-WorksheetXml 'Cards' $cardHeaders $cardRows $schema + $deckTypeMap = [ordered]@{ class = 'string' } + for ($i = 1; $i -le $maxDeckSize; $i++) { $deckTypeMap["slot$i"] = 'string' } + $deckSheet = Get-WorksheetXml 'StarterDecks' $starterDeckHeaders $deckRows $deckTypeMap + + $parts = [ordered]@{ + '[Content_Types].xml' = (Get-ContentTypesXml 2) + '_rels/.rels' = (Get-RootRelsXml) + 'xl/workbook.xml' = (Get-WorkbookXml @('Cards', 'StarterDecks')) + 'xl/_rels/workbook.xml.rels' = (Get-WorkbookRelsXml 2) + 'xl/styles.xml' = (Get-StylesXml) + 'xl/worksheets/sheet1.xml' = $cardSheet + 'xl/worksheets/sheet2.xml' = $deckSheet + } + + Write-Host "Source JSON: $JsonPath" + Write-Host "Target XLSX: $XlsxPath" + Write-Xlsx $XlsxPath $parts + Write-Host "Excel export complete: $XlsxPath" +} + +function Import-Cards { + $source = Get-Content -LiteralPath $JsonPath -Raw -Encoding utf8 | ConvertFrom-Json + $schema = Get-CardSchema $source.cards + $origCardOrders = @{} + foreach ($cardEntry in $source.cards.PSObject.Properties) { + $origCardOrders[$cardEntry.Name] = @($cardEntry.Value.PSObject.Properties.Name) + } + $origDeckOrder = @($source.starterDecks.PSObject.Properties.Name) + + $sharedStrings = Read-SharedStrings $XlsxPath + $cardsXml = Read-XlsxXml $XlsxPath 'xl/worksheets/sheet1.xml' + $deckXml = Read-XlsxXml $XlsxPath 'xl/worksheets/sheet2.xml' + $cardRowsRaw = Read-WorksheetRows $cardsXml $sharedStrings + $deckRowsRaw = Read-WorksheetRows $deckXml $sharedStrings + + if ($cardRowsRaw.Count -lt 2) { throw 'Cards sheet has no data rows.' } + if ($deckRowsRaw.Count -lt 2) { throw 'StarterDecks sheet has no data rows.' } + + $cardHeaderMap = $cardRowsRaw[0] + $cardHeaders = @($cardHeaderMap.Keys | Sort-Object) + $orderedCardHeaders = New-Object System.Collections.Generic.List[string] + foreach ($col in $cardHeaders) { + $header = $cardHeaderMap[$col] + if ([string]::IsNullOrWhiteSpace($header)) { continue } + $orderedCardHeaders.Add($header) + } + + $newCards = [ordered]@{} + for ($r = 1; $r -lt $cardRowsRaw.Count; $r++) { + $row = $cardRowsRaw[$r] + $cardId = $null + $rowValues = @{} + for ($c = 0; $c -lt $orderedCardHeaders.Count; $c++) { + $header = $orderedCardHeaders[$c] + $text = $null + if (Has-MapKey $row ($c + 1)) { $text = $row[$c + 1] } + if ($header -eq 'id') { + $cardId = [string]$text + continue + } + $rowValues[$header] = $text + } + if (-not [string]::IsNullOrWhiteSpace($cardId)) { + $cardObj = [ordered]@{} + $fieldOrder = New-Object System.Collections.Generic.List[string] + if ($origCardOrders.ContainsKey($cardId)) { + foreach ($name in @($origCardOrders[$cardId])) { + if ($name -ne 'id' -and -not $fieldOrder.Contains($name)) { + $fieldOrder.Add($name) + } + } + } + foreach ($name in $orderedCardHeaders) { + if ($name -ne 'id' -and -not $fieldOrder.Contains($name)) { + $fieldOrder.Add($name) + } + } + foreach ($header in $fieldOrder) { + $text = $null + if (Has-MapKey $rowValues $header) { $text = $rowValues[$header] } + $type = if (Has-MapKey $schema $header) { [string]$schema[$header] } else { 'string' } + $value = Convert-CellValue $text $type + if ($null -eq $value) { continue } + $cardObj[$header] = $value + } + $newCards[$cardId] = $cardObj + } + } + + $deckHeaderMap = $deckRowsRaw[0] + $deckHeaderCols = @($deckHeaderMap.Keys | Sort-Object) + $orderedDeckHeaders = New-Object System.Collections.Generic.List[string] + foreach ($col in $deckHeaderCols) { + $header = $deckHeaderMap[$col] + if ([string]::IsNullOrWhiteSpace($header)) { continue } + $orderedDeckHeaders.Add($header) + } + + $newDecks = [ordered]@{} + for ($r = 1; $r -lt $deckRowsRaw.Count; $r++) { + $row = $deckRowsRaw[$r] + $cls = $null + $deckValues = @{} + for ($c = 0; $c -lt $orderedDeckHeaders.Count; $c++) { + $header = $orderedDeckHeaders[$c] + $text = $null + if (Has-MapKey $row ($c + 1)) { $text = $row[$c + 1] } + if ($header -eq 'class') { + $cls = [string]$text + continue + } + $deckValues[$header] = $text + } + if (-not [string]::IsNullOrWhiteSpace($cls)) { + $deck = New-Object System.Collections.Generic.List[string] + foreach ($header in $orderedDeckHeaders) { + if ($header -eq 'class') { continue } + $text = $null + if (Has-MapKey $deckValues $header) { $text = $deckValues[$header] } + if (-not [string]::IsNullOrWhiteSpace([string]$text)) { + $deck.Add([string]$text) + } + } + $newDecks[$cls] = $deck.ToArray() + } + } + + if ($origDeckOrder.Count -gt 0) { + $orderedDecks = [ordered]@{} + foreach ($cls in $origDeckOrder) { + if (Has-MapKey $newDecks $cls) { + $orderedDecks[$cls] = $newDecks[$cls] + } + } + foreach ($entry in $newDecks.GetEnumerator()) { + if (-not (Has-MapKey $orderedDecks $entry.Key)) { + $orderedDecks[$entry.Key] = $entry.Value + } + } + $newDecks = $orderedDecks + } + + $out = [ordered]@{ + cards = $newCards + starterDecks = $newDecks + } + + $json = $out | ConvertTo-Json -Depth 64 + Write-Host "Source XLSX: $XlsxPath" + Write-Host "Target JSON: $OutJsonPath" + [System.IO.File]::WriteAllText($OutJsonPath, $json, $utf8NoBom) + Write-Host "JSON import complete: $OutJsonPath" +} + +switch ($Action) { + 'export' { Export-Cards } + 'import' { Import-Cards } +}