Files
healer-man/scripts/runewaker-pipeline/refresh-population.ps1
T
2026-08-14 15:56:39 -04:00

263 lines
11 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$ServerInstance,
[Parameter(Mandatory = $true)][string]$GlobalBackup,
[Parameter(Mandatory = $true)][string]$ObjectBackup,
[Parameter(Mandatory = $true)][int]$ZoneId,
[int64]$ZoneObjectTemplateId = 0,
[Parameter(Mandatory = $true)][string]$DatabaseSuffix,
[Parameter(Mandatory = $true)][string]$OutputFile
)
$ErrorActionPreference = "Stop"
$safeSuffix = ($DatabaseSuffix -replace '[^A-Za-z0-9_]', '_')
if ($safeSuffix.Length -gt 32) { $safeSuffix = $safeSuffix.Substring(0, 32) }
$globalDatabase = "HealerMan_RW_Global_" + $safeSuffix
$objectDatabase = "HealerMan_RW_Objects_" + $safeSuffix
function Quote-SqlLiteral([string]$Value) {
return "N'" + $Value.Replace("'", "''") + "'"
}
function Quote-SqlIdentifier([string]$Value) {
return "[" + $Value.Replace("]", "]]" ) + "]"
}
function Invoke-SqlTable(
[System.Data.SqlClient.SqlConnection]$Connection,
[string]$CommandText
) {
$command = $Connection.CreateCommand()
$command.CommandTimeout = 300
$command.CommandText = $CommandText
$table = [System.Data.DataTable]::new()
$reader = $command.ExecuteReader()
try { $table.Load($reader) }
finally {
$reader.Dispose()
$command.Dispose()
}
return ,$table
}
function Invoke-SqlNonQuery(
[System.Data.SqlClient.SqlConnection]$Connection,
[string]$CommandText
) {
$command = $Connection.CreateCommand()
$command.CommandTimeout = 600
$command.CommandText = $CommandText
try { [void]$command.ExecuteNonQuery() }
finally { $command.Dispose() }
}
function Convert-DataRow([System.Data.DataRow]$Row) {
$result = [ordered]@{}
foreach ($column in $Row.Table.Columns) {
$value = $Row[$column.ColumnName]
$result[$column.ColumnName] = if ($value -is [DBNull]) { $null } else { $value }
}
return $result
}
function Remove-ForensicDatabase(
[System.Data.SqlClient.SqlConnection]$Connection,
[string]$DatabaseName
) {
$identifier = Quote-SqlIdentifier $DatabaseName
Invoke-SqlNonQuery $Connection @"
IF DB_ID($(Quote-SqlLiteral $DatabaseName)) IS NOT NULL
BEGIN
ALTER DATABASE $identifier SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE $identifier;
END
"@
}
function Restore-ForensicDatabase(
[System.Data.SqlClient.SqlConnection]$Connection,
[string]$DatabaseName,
[string]$BackupFile,
[string]$DataDirectory,
[string]$LogDirectory
) {
if (-not (Test-Path -LiteralPath $BackupFile -PathType Leaf)) {
throw "Backup not found: $BackupFile"
}
$files = Invoke-SqlTable $Connection (
"RESTORE FILELISTONLY FROM DISK = " + (Quote-SqlLiteral $BackupFile)
)
if ($files.Rows.Count -eq 0) { throw "Backup contains no logical files: $BackupFile" }
$moves = [System.Collections.Generic.List[string]]::new()
$dataIndex = 0
$logIndex = 0
foreach ($file in $files.Rows) {
$isLog = [string]$file.Type -eq "L"
if ($isLog) {
$physical = Join-Path $LogDirectory ($DatabaseName + "_" + $logIndex + ".ldf")
$logIndex += 1
}
else {
$extension = if ($dataIndex -eq 0) { ".mdf" } else { ".ndf" }
$physical = Join-Path $DataDirectory ($DatabaseName + "_" + $dataIndex + $extension)
$dataIndex += 1
}
$moves.Add("MOVE " + (Quote-SqlLiteral ([string]$file.LogicalName)) + " TO " + (Quote-SqlLiteral $physical))
}
Remove-ForensicDatabase $Connection $DatabaseName
$newline = [Environment]::NewLine
$restoreSql = @(
"RESTORE DATABASE " + (Quote-SqlIdentifier $DatabaseName),
"FROM DISK = " + (Quote-SqlLiteral $BackupFile),
"WITH REPLACE, RECOVERY, STATS = 10,",
($moves -join (',' + $newline))
) -join $newline
Invoke-SqlNonQuery $Connection $restoreSql
Invoke-SqlNonQuery $Connection (
"ALTER DATABASE " + (Quote-SqlIdentifier $DatabaseName) +
" SET READ_ONLY WITH ROLLBACK IMMEDIATE"
)
}
$connectionString = "Server=" + $ServerInstance + ";Database=master;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True;Connect Timeout=15;Application Name=HealerMan RuneWaker Forensics"
$connection = [System.Data.SqlClient.SqlConnection]::new($connectionString)
try {
$connection.Open()
$paths = Invoke-SqlTable $connection @"
SELECT
CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(4000)) AS DataPath,
CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(4000)) AS LogPath,
(SELECT TOP (1) physical_name FROM sys.master_files WHERE database_id = 1 AND file_id = 1) AS MasterFile;
"@
$dataDirectory = [string]$paths.Rows[0].DataPath
$logDirectory = [string]$paths.Rows[0].LogPath
if ([string]::IsNullOrWhiteSpace($dataDirectory)) {
$dataDirectory = Split-Path -Parent ([string]$paths.Rows[0].MasterFile)
}
if ([string]::IsNullOrWhiteSpace($logDirectory)) { $logDirectory = $dataDirectory }
Restore-ForensicDatabase $connection $globalDatabase $GlobalBackup $dataDirectory $logDirectory
Restore-ForensicDatabase $connection $objectDatabase $ObjectBackup $dataDirectory $logDirectory
$globalId = Quote-SqlIdentifier $globalDatabase
$objectId = Quote-SqlIdentifier $objectDatabase
$zoneObjectColumns = @()
$zoneObjects = @()
if ($ZoneObjectTemplateId -gt 0) {
$columnSql = "SELECT COLUMN_NAME, DATA_TYPE, ORDINAL_POSITION FROM $objectId.INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'ZoneObjectDB' ORDER BY ORDINAL_POSITION;"
$columnTable = Invoke-SqlTable $connection $columnSql
$zoneObjectColumns = @($columnTable.Rows | ForEach-Object { Convert-DataRow $_ })
$zoneTable = Invoke-SqlTable $connection ("SELECT * FROM $objectId.dbo.ZoneObjectDB WHERE guid = " + $ZoneObjectTemplateId)
$zoneObjects = @($zoneTable.Rows | ForEach-Object { Convert-DataRow $_ })
}
$table = Invoke-SqlTable $connection @"
SELECT
d.DBID AS spawn_id,
d.OrgObjID AS template_id,
d.ZoneID AS zone_id,
d.RoomID AS room_id,
CONVERT(float, d.X) AS source_x,
CONVERT(float, d.Y) AS source_y,
CONVERT(float, d.Z) AS source_z,
CONVERT(float, d.Dir) AS direction,
ISNULL(d.RoleName, '') AS role_name,
ISNULL(d.AutoPlot, '') AS auto_plot,
ISNULL(d.PlotClassName, '') AS plot_class_name,
ISNULL(n.sex, 0) AS sex,
ISNULL(n.level, 0) AS native_level,
ISNULL(n.imageid, 0) AS image_id,
ISNULL(CONVERT(nvarchar(4000), image_row.actworld), '') AS model_path,
ISNULL(CONVERT(nvarchar(4000), n.szluainitscript), '') AS lua_init_script,
ISNULL(CONVERT(nvarchar(4000), n.szluadisplayscript), '') AS lua_display_script,
ISNULL(CONVERT(nvarchar(4000), n.luaevent_beginattack), '') AS lua_begin_attack,
ISNULL(CONVERT(nvarchar(4000), n.luaevent_endattack), '') AS lua_end_attack,
ISNULL(CONVERT(nvarchar(4000), n.luaevent_onassistmagic), '') AS lua_assist_magic,
ISNULL(CONVERT(nvarchar(4000), n.luaevent_onattackmagic), '') AS lua_attack_magic,
ISNULL(CONVERT(nvarchar(4000), n.luaevent_ondead), '') AS lua_on_dead,
ISNULL(CONVERT(nvarchar(4000), n.luaevent_onkill), '') AS lua_on_kill,
ISNULL(n.spellmagic1, 0) AS spell_1, ISNULL(n.spellmagiclv1, 0) AS spell_level_1,
ISNULL(n.spellmagic2, 0) AS spell_2, ISNULL(n.spellmagiclv2, 0) AS spell_level_2,
ISNULL(n.spellmagic3, 0) AS spell_3, ISNULL(n.spellmagiclv3, 0) AS spell_level_3,
ISNULL(n.spellmagic4, 0) AS spell_4, ISNULL(n.spellmagiclv4, 0) AS spell_level_4,
ISNULL(n.spellmagic5, 0) AS spell_5, ISNULL(n.spellmagiclv5, 0) AS spell_level_5,
ISNULL(n.spellmagic6, 0) AS spell_6, ISNULL(n.spellmagiclv6, 0) AS spell_level_6,
ISNULL(n.spellmagic7, 0) AS spell_7, ISNULL(n.spellmagiclv7, 0) AS spell_level_7,
ISNULL(n.spellmagic8, 0) AS spell_8, ISNULL(n.spellmagiclv8, 0) AS spell_level_8,
ISNULL(s.Content, '') AS localized_name
FROM $globalId.dbo.NPCData d
LEFT JOIN $objectId.dbo.NPCObjectDB n ON n.guid = d.OrgObjID
OUTER APPLY (
SELECT TOP (1) i.actworld
FROM $objectId.dbo.ImageObjectDB i
WHERE i.guid = n.imageid OR i.imageid = n.imageid
ORDER BY CASE WHEN i.guid = n.imageid THEN 0 ELSE 1 END, i.guid
) image_row
LEFT JOIN $objectId.dbo.StringDB s
ON s.KeyStr = 'Sys' + CAST(d.OrgObjID AS varchar) + '_name'
WHERE d.ZoneID = $ZoneId
AND d.IsDelflag = 0
ORDER BY d.DBID;
"@
$rows = foreach ($row in $table.Rows) {
$spells = for ($index = 1; $index -le 8; $index += 1) {
$spellId = [int]$row.("spell_" + $index)
if ($spellId -gt 0) {
[ordered]@{ id = $spellId; level = [int]$row.("spell_level_" + $index) }
}
}
[ordered]@{
spawnId = [int64]$row.spawn_id
templateId = [int]$row.template_id
zoneId = [int]$row.zone_id
roomId = [int]$row.room_id
sourcePosition = @(
[Convert]::ToDouble($row.source_x),
[Convert]::ToDouble($row.source_y),
[Convert]::ToDouble($row.source_z)
)
direction = [Convert]::ToDouble($row.direction)
roleName = [string]$row.role_name
autoPlot = [string]$row.auto_plot
plotClassName = [string]$row.plot_class_name
sex = [int]$row.sex
nativeLevel = [int]$row.native_level
imageId = [int]$row.image_id
modelPath = [string]$row.model_path
localizedName = [string]$row.localized_name
templateScripts = [ordered]@{
init = [string]$row.lua_init_script
display = [string]$row.lua_display_script
beginAttack = [string]$row.lua_begin_attack
endAttack = [string]$row.lua_end_attack
assistMagic = [string]$row.lua_assist_magic
attackMagic = [string]$row.lua_attack_magic
onDead = [string]$row.lua_on_dead
onKill = [string]$row.lua_on_kill
}
spells = @($spells)
}
}
$payload = [ordered]@{
schemaVersion = 2
queryVersion = 2
zoneId = $ZoneId
rowCount = @($rows).Count
zoneObjectTemplateId = $ZoneObjectTemplateId
zoneObjectColumns = $zoneObjectColumns
zoneObjects = $zoneObjects
rows = @($rows)
}
$outputDirectory = Split-Path -Parent $OutputFile
if ($outputDirectory) { [void](New-Item -ItemType Directory -Path $outputDirectory -Force) }
$payload | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputFile -Encoding UTF8
Write-Host ("Exported {0} active Zone {1} rows." -f @($rows).Count, $ZoneId)
}
finally {
if ($connection.State -eq [System.Data.ConnectionState]::Open) {
Remove-ForensicDatabase $connection $objectDatabase
Remove-ForensicDatabase $connection $globalDatabase
$connection.Close()
}
$connection.Dispose()
}