Add Excel roundtrip for cards data #93
7
cards_to_excel.bat
Normal file
7
cards_to_excel.bat
Normal file
@@ -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
|
||||||
BIN
data/cards.xlsx
Normal file
BIN
data/cards.xlsx
Normal file
Binary file not shown.
7
excel_to_cards.bat
Normal file
7
excel_to_cards.bat
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
chcp 65001 >nul
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\cards\cards_excel.ps1" import
|
||||||
|
echo.
|
||||||
|
echo Press any key to close this window.
|
||||||
|
pause >nul
|
||||||
629
tools/cards/cards_excel.ps1
Normal file
629
tools/cards/cards_excel.ps1
Normal file
@@ -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 "<c r=""$Ref"" s=""1"" t=""inlineStr""><is><t xml:space=""preserve"">$escaped</t></is></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-TextCellXml([string]$Ref, [string]$Text) {
|
||||||
|
$escaped = Escape-Xml $Text
|
||||||
|
return "<c r=""$Ref"" t=""inlineStr""><is><t xml:space=""preserve"">$escaped</t></is></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-NumberCellXml([string]$Ref, $Value) {
|
||||||
|
if ($null -eq $Value) { return $null }
|
||||||
|
if ($Value -is [string] -and $Value -eq '') { return $null }
|
||||||
|
return "<c r=""$Ref""><v>$(To-InvariantNumber $Value)</v></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "<c r=""$Ref"" t=""b""><v>$n</v></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
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("<col min=""$($i + 1)"" max=""$($i + 1)"" width=""$width"" customWidth=""1"" />")
|
||||||
|
}
|
||||||
|
|
||||||
|
$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("<row r=""1"" spans=""1:$maxCol"" ht=""20"" customHeight=""1"">$($headerCells -join '')</row>")
|
||||||
|
|
||||||
|
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("<row r=""$($r + 2)"" spans=""1:$maxCol"">$($cells -join '')</row>")
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetView = '<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/><selection pane="bottomLeft" activeCell="A2" sqref="A2"/></sheetView></sheetViews>'
|
||||||
|
$cols = '<cols>' + ($colsXml -join '') + '</cols>'
|
||||||
|
$sheetData = '<sheetData>' + ($rowsXml -join '') + '</sheetData>'
|
||||||
|
$autoFilter = "<autoFilter ref=""A1:$lastCol$rowCount""/>"
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
|
$sheetView
|
||||||
|
<sheetFormatPr defaultRowHeight="18"/>
|
||||||
|
$cols
|
||||||
|
$sheetData
|
||||||
|
$autoFilter
|
||||||
|
<pageMargins left="0.25" right="0.25" top="0.5" bottom="0.5" header="0.3" footer="0.3"/>
|
||||||
|
</worksheet>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-StylesXml {
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||||
|
<fonts count="2">
|
||||||
|
<font>
|
||||||
|
<sz val="11"/>
|
||||||
|
<color rgb="FF000000"/>
|
||||||
|
<name val="Calibri"/>
|
||||||
|
<family val="2"/>
|
||||||
|
<scheme val="minor"/>
|
||||||
|
</font>
|
||||||
|
<font>
|
||||||
|
<b/>
|
||||||
|
<sz val="11"/>
|
||||||
|
<color rgb="FFFFFFFF"/>
|
||||||
|
<name val="Calibri"/>
|
||||||
|
<family val="2"/>
|
||||||
|
<scheme val="minor"/>
|
||||||
|
</font>
|
||||||
|
</fonts>
|
||||||
|
<fills count="2">
|
||||||
|
<fill><patternFill patternType="none"/></fill>
|
||||||
|
<fill><patternFill patternType="solid"><fgColor rgb="FF2D3748"/><bgColor indexed="64"/></patternFill></fill>
|
||||||
|
</fills>
|
||||||
|
<borders count="1">
|
||||||
|
<border>
|
||||||
|
<left/><right/><top/><bottom/><diagonal/>
|
||||||
|
</border>
|
||||||
|
</borders>
|
||||||
|
<cellStyleXfs count="1">
|
||||||
|
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
|
||||||
|
</cellStyleXfs>
|
||||||
|
<cellXfs count="2">
|
||||||
|
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
|
||||||
|
<xf numFmtId="0" fontId="1" fillId="1" borderId="0" xfId="0" applyFont="1" applyFill="1"/>
|
||||||
|
</cellXfs>
|
||||||
|
<cellStyles count="1">
|
||||||
|
<cellStyle name="Normal" xfId="0" builtinId="0"/>
|
||||||
|
</cellStyles>
|
||||||
|
</styleSheet>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WorkbookXml([string[]]$SheetNames) {
|
||||||
|
$sheetsXml = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 0; $i -lt $SheetNames.Count; $i++) {
|
||||||
|
$sheetsXml.Add("<sheet name=""$(Escape-Xml $SheetNames[$i])"" sheetId=""$($i + 1)"" r:id=""rId$($i + 1)""/>")
|
||||||
|
}
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
|
<sheets>
|
||||||
|
$($sheetsXml -join '')
|
||||||
|
</sheets>
|
||||||
|
</workbook>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WorkbookRelsXml([int]$SheetCount) {
|
||||||
|
$rels = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 1; $i -le $SheetCount; $i++) {
|
||||||
|
$rels.Add("<Relationship Id=""rId$i"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"" Target=""worksheets/sheet$i.xml""/>")
|
||||||
|
}
|
||||||
|
$rels.Add("<Relationship Id=""rId$($SheetCount + 1)"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"" Target=""styles.xml""/>")
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
$($rels -join '')
|
||||||
|
</Relationships>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RootRelsXml {
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||||
|
</Relationships>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ContentTypesXml([int]$SheetCount) {
|
||||||
|
$overrides = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 1; $i -le $SheetCount; $i++) {
|
||||||
|
$overrides.Add("<Override PartName=""/xl/worksheets/sheet$i.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml""/>")
|
||||||
|
}
|
||||||
|
$overrides.Add('<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>')
|
||||||
|
$overrides.Add('<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>')
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="xml" ContentType="application/xml"/>
|
||||||
|
$($overrides -join '')
|
||||||
|
</Types>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user