From 6a67ec20568f93628ad854e668426674bf7be0d4 Mon Sep 17 00:00:00 2001 From: erik Date: Mon, 9 Feb 2026 00:12:37 +0100 Subject: [PATCH] Add COM registration verification script Test-ComRegistration.ps1 registers all 10 COM server DLLs via regasm and verifies all 50 CLSIDs appear in the registry. Supports -Build to compile first and -Unregister to clean up. Checks both native and WOW6432Node registry paths for 32-bit COM on 64-bit Windows. Result: 50/50 CLSIDs pass, 0 GUID mismatches vs original Decal. Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-02-08-com-registration-test.md | 111 ++++++++ tools/Test-ComRegistration.ps1 | 245 ++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 docs/plans/2026-02-08-com-registration-test.md create mode 100644 tools/Test-ComRegistration.ps1 diff --git a/docs/plans/2026-02-08-com-registration-test.md b/docs/plans/2026-02-08-com-registration-test.md new file mode 100644 index 0000000..7aefa10 --- /dev/null +++ b/docs/plans/2026-02-08-com-registration-test.md @@ -0,0 +1,111 @@ +# COM Registration Test - Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Verify all 10 COM server DLLs register correctly with regasm and their CLSIDs match the original Decal v2.9.8.3. + +**Architecture:** Build a PowerShell verification script that: (1) registers each DLL with regasm, (2) queries the registry for expected CLSIDs, (3) reports pass/fail per GUID. We also cross-reference our [Guid] attributes against the original .rgs files from source_checkout. + +**Tech Stack:** regasm.exe (.NET Framework 4.x), PowerShell, Windows Registry + +--- + +### Task 1: Cross-Reference GUIDs Against Original .rgs Files + +**Files:** +- Read: `source_checkout/Decal/*.rgs` (original ATL registry scripts) +- Read: `source_checkout/DecalControls/*.rgs` +- Read: `source_checkout/DecalFilters/*.rgs` +- Read: `source_checkout/DecalInput/*.rgs` +- Read: `source_checkout/DecalNet/*.rgs` +- Read: `source_checkout/DecalDat/*.rgs` +- Read: All `*Impl.cs` files in `reconstructed/Managed/Decal.*/` + +**Step 1: Extract GUIDs from .rgs files** +Parse each .rgs file for CLSID patterns like `{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}`. + +**Step 2: Extract GUIDs from our [Guid("...")] attributes** +Grep all Impl.cs files for Guid attributes. + +**Step 3: Compare and report mismatches** +Any GUID in our code that doesn't match the original .rgs is a critical bug. + +--- + +### Task 2: Write the COM Registration Test Script + +**Files:** +- Create: `reconstructed/tools/Test-ComRegistration.ps1` + +**Step 1: Write PowerShell script** + +The script should: +1. Find regasm.exe (32-bit .NET Framework 4.x) +2. Build the solution if needed +3. Register each of the 10 DLLs with `regasm /codebase` +4. Query registry for each expected CLSID at `HKCR\CLSID\{GUID}\InprocServer32` +5. Verify the registered DLL path points to our built assembly +6. Report pass/fail per DLL and per GUID +7. Optionally unregister with `regasm /unregister` + +**Step 2: Run the script** + +```powershell +powershell -ExecutionPolicy Bypass -File reconstructed/tools/Test-ComRegistration.ps1 +``` + +Expected: All 55 CLSIDs register and appear in registry. + +**Step 3: Commit** + +```bash +git add reconstructed/tools/Test-ComRegistration.ps1 +git commit -m "feat: add COM registration verification script" +``` + +--- + +### Task 3: Run Registration and Fix Any Issues + +**Step 1: Execute registration** +Run regasm on each DLL and capture output. + +**Step 2: Investigate any failures** +Common issues: +- Missing dependencies (Interop DLLs not in same directory) +- x86 vs x64 regasm mismatch +- Missing ComVisible or Guid attributes + +**Step 3: Fix and re-test until all pass** + +**Step 4: Commit fixes** + +--- + +### Task 4: Unregister and Document Results + +**Step 1: Run unregister to clean up** +```powershell +regasm /unregister Decal.DecalDat.dll +``` + +**Step 2: Verify CLSIDs are removed from registry** + +**Step 3: Document results in a test report** + +--- + +## DLL-to-GUID Reference (55 CLSIDs across 10 DLLs) + +| DLL | Classes | CLSIDs | +|-----|---------|--------| +| Decal.DecalDat.dll | 3 | DatService, DatStream, DatLibrary | +| Decal.DHS.dll | 2 | HotkeySystem, Hotkey | +| Decal.SpellFilter.dll | 3 | Spells, Spell, Component | +| Decal.DecalInput.dll | 6 | InputService, Hotkey, WndMsg, WinMsgHook, Timer, InputBuffer | +| Decal.DecalNet.dll | 2 | NetService, WebRequest | +| Decal.DecalFilters.dll | 7 | World, WorldObject, WorldIterator, CharacterStats, EchoFilter, EchoFilter2, Prefilter | +| Decal.Core.dll | 4 | DecalCore, ACHooks, PluginSite2, DecalEnum | +| Decal.DecalControls.dll | 17 | Checkbox, List, StaticText, Choice, Progress, FixedLayout, BorderLayout, PageLayout, TextColumn, IconColumn, CheckColumn, DerethMap, Edit, Scroller, PushButton, Notebook, Slider | +| Decal.DecalRender.dll | 4 | RenderService, HUDBackground, HUDView, RenderTargetWrapper | +| Decal.D3DService.dll | 2 | D3DService, D3DObj | diff --git a/tools/Test-ComRegistration.ps1 b/tools/Test-ComRegistration.ps1 new file mode 100644 index 0000000..0fbba80 --- /dev/null +++ b/tools/Test-ComRegistration.ps1 @@ -0,0 +1,245 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Tests COM registration for the OpenDecal project. + +.DESCRIPTION + Builds the Decal solution (optionally), registers 10 COM server DLLs using + regasm /codebase, and verifies that every expected CLSID appears in the + registry under HKCR:\CLSID\{GUID}\InprocServer32. + +.PARAMETER Build + If specified, builds Decal.sln (Release) before registration. + +.PARAMETER Unregister + If specified, unregisters all DLLs instead of registering them. + +.EXAMPLE + .\Test-ComRegistration.ps1 -Build + Builds the solution, registers all DLLs, and verifies CLSIDs. + +.EXAMPLE + .\Test-ComRegistration.ps1 -Unregister + Unregisters all DLLs. +#> +[CmdletBinding()] +param( + [switch]$Build, + [switch]$Unregister +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Continue' + +# ── Paths ──────────────────────────────────────────────────────────────────── +$RegAsm = 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe' +$SolutionRoot = Split-Path -Parent $PSScriptRoot # reconstructed\ +$ManagedRoot = Join-Path $SolutionRoot 'Managed' +$SolutionFile = Join-Path $ManagedRoot 'Decal.sln' + +# ── Verify regasm exists ───────────────────────────────────────────────────── +if (-not (Test-Path $RegAsm)) { + Write-Error "RegAsm.exe not found at $RegAsm" + exit 1 +} + +# ── DLL → CLSID map ───────────────────────────────────────────────────────── +# Each key is the relative path from $ManagedRoot to the built DLL. +# Each value is an ordered list of [CLSID, FriendlyName] pairs. +$DllMap = [ordered]@{ + + 'Decal.Core\bin\Release\Decal.Core.dll' = @( + ,@('CB8875CD-ABC2-42AD-8175-8908706EED37', 'ACHooks') + ,@('4557D5A1-00DB-48F6-ACB3-4FEF30E2F358', 'DecalCore') + ,@('6DE65A82-C451-46E6-A82D-92137BE851AD', 'DecalEnum') + ,@('E2284FC7-17E5-4846-ADAB-07953273C5FB', 'PluginSite2') + ) + + 'Decal.DecalControls\bin\Release\Decal.DecalControls.dll' = @( + ,@('5AE37451-F79C-478A-834E-EDCF95F01B0E', 'Checkbox') + ,@('E099DC60-0F19-4690-AB7C-8B5834DA286E', 'Choice') + ,@('3035299A-C5FB-4CC7-A63C-66400B80DCA4', 'DerethMap') + ,@('8E8F88D2-AA47-474E-9DB7-912D49C8BE9D', 'Edit') + ,@('CD6556CD-F8D9-4CB0-AB7C-7C33058294E4', 'FixedLayout') + ,@('CA121762-31BB-4073-8597-33BAB4BDCAA3', 'BorderLayout') + ,@('5AD04D45-D4BF-4729-8A2E-5D37CF726CAA', 'PageLayout') + ,@('3839008F-AF5B-43FC-9F6A-3376B99E3DAF', 'List') + ,@('864DEABF-D079-4B61-A8CF-081418179239', 'TextColumn') + ,@('F12A2C4C-3B78-46EB-8722-68B27A75AE55', 'IconColumn') + ,@('48E444F1-8E30-4E4C-B203-4C87FC901586', 'CheckColumn') + ,@('ED14E7C5-11BE-4DFD-9829-873AB6BE3CFC', 'Notebook') + ,@('FE361225-6BB7-4AE0-A10C-8A6420621680', 'Progress') + ,@('AE4525BE-81D1-40FB-9170-77172077EB49', 'PushButton') + ,@('8FC80D21-1731-4816-9AD3-B0364D5F2C27', 'Scroller') + ,@('5D14557A-1268-43C6-A283-3B5B271359AA', 'Slider') + ,@('4887101C-A9F9-495B-921B-EF22822CFB97', 'StaticText') + ) + + 'Decal.DecalDat\bin\Release\Decal.DecalDat.dll' = @( + ,@('37B083F0-276E-43AD-8D26-3F7449B519DC', 'DatService') + ,@('9F7F6CD9-D164-418D-8CB5-3B9ACD70BEAF', 'DatStream') + ,@('6FA05FDA-B4B5-4386-AB45-92D7E6A5D698', 'DatLibrary') + ) + + 'Decal.DecalFilters\bin\Release\Decal.DecalFilters.dll' = @( + ,@('50A7E9EC-AB12-4484-9C28-C2A39274A636', 'WorldObject') + ,@('2681B113-294E-4ABF-B543-624194846BE1', 'WorldIterator') + ,@('443D4A68-5422-4E0C-9460-973F8FBDB190', 'Prefilter') + ,@('53092D1B-F0B0-46FF-BF11-8F031EC9B137', 'World') + ,@('8C2FA400-315D-41DE-B063-D6EF04F12E1F', 'EchoFilter') + ,@('34239EAD-6317-4C40-A405-193BA5232DD8', 'EchoFilter2') + ,@('4540C969-08D1-46BF-97AD-6B19D3C10BEE', 'CharacterStats') + ) + + 'Decal.DecalInput\bin\Release\Decal.DecalInput.dll' = @( + ,@('F183506A-3664-49D6-8CA4-CFD295F2811D', 'Hotkey') + ,@('85AB0296-124E-4E68-A6A8-FCF5721AC09B', 'WndMsg') + ,@('B33307BA-706D-474A-80B9-70BB8D13EF3E', 'InputService') + ,@('F3170E85-517E-43A4-B7B4-6F006A7B1B85', 'WinMsgHook') + ,@('79497C87-92E1-416B-AE5C-9D6C4C59133C', 'Timer') + ,@('F0A17A04-7F8F-4A17-A41D-8C297A1E929B', 'InputBuffer') + ) + + 'Decal.DecalNet\bin\Release\Decal.DecalNet.dll' = @( + ,@('15631E36-55CB-4D16-ADE7-74D9F8A5F4B6', 'WebRequest') + ,@('C8C406F8-BA2E-4964-8B04-FF38394A8E0E', 'NetService') + ) + + 'Decal.DecalRender\bin\Release\Decal.DecalRender.dll' = @( + ,@('FB3C8286-88ED-4B4D-B413-94B40F346239', 'RenderService') + ,@('218432A3-8960-4D91-BBC6-8CDC105C191E', 'HUDBackground') + ,@('EF810EF1-C843-4563-A3DE-19CE8BEEEEC0', 'HUDView') + ,@('175D6713-E942-41C0-8BC9-90A565024F76', 'RenderTargetWrapper') + ) + + 'Decal.D3DService\bin\Release\Decal.D3DService.dll' = @( + ,@('F0CC07A0-2C89-4FA4-9356-714665BC2F8B', 'D3DService') + ,@('81E79859-2783-4B9A-ADC4-308073F5BB3F', 'D3DObj') + ) + + 'Decal.DHS\bin\Release\Decal.DHS.dll' = @( + ,@('6B6B9FA8-37DE-4FA3-8C60-52BD6A2F9855', 'HotkeySystem') + ,@('F5AA853D-CCF3-4562-A654-3A68AB583BCC', 'Hotkey') + ) + + 'Decal.SpellFilter\bin\Release\Decal.SpellFilter.dll' = @( + ,@('C2D43735-BE7E-4829-AF73-F2E7E820EB16', 'Spells') + ,@('3040368A-B77A-4FE1-B8CC-D77FA3F36A6D', 'Spell') + ,@('20425F2F-54FF-4EAE-9538-36699E3DFB56', 'Component') + ) +} + +# ── Helper: coloured write ─────────────────────────────────────────────────── +function Write-Status { + param( + [string]$Label, + [string]$Message, + [ConsoleColor]$Color = 'White' + ) + Write-Host " [$Label] " -ForegroundColor $Color -NoNewline + Write-Host $Message +} + +# ── Administrator check ────────────────────────────────────────────────────── +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = New-Object Security.Principal.WindowsPrincipal $identity +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Error 'This script must be run as Administrator (regasm requires elevated privileges).' + exit 1 +} + +# ── Optional build step ────────────────────────────────────────────────────── +if ($Build) { + Write-Host "`n=== Building Decal.sln (Release) ===" -ForegroundColor Cyan + $buildArgs = @('build', $SolutionFile, '-c', 'Release') + & dotnet @buildArgs + if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed with exit code $LASTEXITCODE" + exit 1 + } + Write-Host '' +} + +# ── Counters ───────────────────────────────────────────────────────────────── +$totalCLSIDs = 0 +$passedCLSIDs = 0 +$failedCLSIDs = 0 + +# ── Process each DLL ───────────────────────────────────────────────────────── +foreach ($relPath in $DllMap.Keys) { + $dllPath = Join-Path $ManagedRoot $relPath + $dllName = Split-Path -Leaf $dllPath + + Write-Host "`n=== $dllName ===" -ForegroundColor Cyan + + # Check the DLL exists + if (-not (Test-Path $dllPath)) { + Write-Status 'SKIP' "$dllPath not found. Build the solution first (-Build)." Yellow + $clsids = $DllMap[$relPath] + $totalCLSIDs += $clsids.Count + $failedCLSIDs += $clsids.Count + continue + } + + # ── Register or unregister ─────────────────────────────────────────── + if ($Unregister) { + Write-Host " Unregistering..." -ForegroundColor Yellow + $output = & $RegAsm /unregister /nologo $dllPath 2>&1 + $regExitCode = $LASTEXITCODE + } + else { + Write-Host " Registering..." -ForegroundColor Yellow + $output = & $RegAsm /codebase /nologo $dllPath 2>&1 + $regExitCode = $LASTEXITCODE + } + + # Show regasm output + if ($output) { + foreach ($line in $output) { + Write-Host " $line" -ForegroundColor Gray + } + } + if ($regExitCode -ne 0) { + Write-Status 'WARN' "regasm exited with code $regExitCode" Yellow + } + + # ── Verify CLSIDs ──────────────────────────────────────────────────── + if (-not $Unregister) { + $clsids = $DllMap[$relPath] + foreach ($entry in $clsids) { + $guid = $entry[0] + $name = $entry[1] + $totalCLSIDs++ + + # 32-bit regasm registers under WOW6432Node on 64-bit Windows + $regKey64 = "Registry::HKEY_CLASSES_ROOT\CLSID\{$guid}\InprocServer32" + $regKey32 = "Registry::HKEY_CLASSES_ROOT\WOW6432Node\CLSID\{$guid}\InprocServer32" + if ((Test-Path $regKey64) -or (Test-Path $regKey32)) { + Write-Status 'PASS' "{$guid} $name" Green + $passedCLSIDs++ + } + else { + Write-Status 'FAIL' "{$guid} $name" Red + $failedCLSIDs++ + } + } + } +} + +# ── Summary ────────────────────────────────────────────────────────────────── +Write-Host "`n============================================" -ForegroundColor Cyan +if ($Unregister) { + Write-Host " Unregistration complete." -ForegroundColor Cyan +} +else { + $summaryColor = if ($failedCLSIDs -eq 0) { 'Green' } else { 'Red' } + Write-Host " Total CLSIDs checked : $totalCLSIDs" -ForegroundColor $summaryColor + Write-Host " Passed : $passedCLSIDs" -ForegroundColor Green + Write-Host " Failed : $failedCLSIDs" -ForegroundColor $(if ($failedCLSIDs -eq 0) { 'Green' } else { 'Red' }) +} +Write-Host "============================================`n" -ForegroundColor Cyan + +if ($failedCLSIDs -gt 0) { + exit 1 +} +exit 0