Skip to content
This repository was archived by the owner on Jul 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/build-modern.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Build unofficial modern clients

on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/build-modern.yml"
- "scripts/**"
- "v1.0-pre-modern/**"
- "v2.5-beta-1-modern/**"
- "artifacts/inc/**"
- "artifacts/lib/i386/zlib.lib"

permissions:
contents: read

jobs:
build:
name: Build, package, and smoke test
runs-on: windows-2022
timeout-minutes: 30

steps:
- name: Check out source
uses: actions/checkout@v5

- name: Build both modernized clients
shell: cmd
run: |
for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%I"
if not defined VSINSTALL (
echo No Visual Studio C++ installation was found.
exit /b 1
)
call "%VSINSTALL%\VC\Auxiliary\Build\vcvars32.bat"
if errorlevel 1 exit /b 1
if not exist "%VCToolsInstallDir%ATLMFC\lib\spectre\x86" (
echo The x86 Spectre-mitigated MFC libraries are missing.
exit /b 1
)

if exist "v1.0-pre-modern\Release" rmdir /s /q "v1.0-pre-modern\Release"
pushd "v1.0-pre-modern"
nmake /f chat.mak CFG="chat - Win32 Release"
if errorlevel 1 exit /b 1
popd

if exist "v2.5-beta-1-modern\Release" rmdir /s /q "v2.5-beta-1-modern\Release"
pushd "v2.5-beta-1-modern"
nmake /f chat.mak CFG="chat - Win32 Release"
if errorlevel 1 exit /b 1
popd

- name: Create archival ZIP files
shell: pwsh
run: ./scripts/package-modern-builds.ps1 -SourceRepositoryUrl "${{ github.server_url }}/${{ github.repository }}"

- name: Smoke test from random folders
shell: pwsh
run: ./scripts/smoke-test-modern-builds.ps1

- name: Upload release-ready packages
uses: actions/upload-artifact@v4
with:
name: comic-chat-unofficial-modern-builds-${{ github.sha }}
path: |
out/modern-builds/*.zip
out/modern-builds/SHA256SUMS.txt
if-no-files-found: error
retention-days: 90
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ nmake /f chat.mak CFG="chat - Win32 Debug" REM asserts + TRACE for DebugVie

It carries the mouse-wheel and panels-per-row work across and runs **DPI-unaware** so Windows scales the whole window uniformly (rather than scaling a few surfaces and leaving the rest tiny). The chief 2.5-specific fixes were dropping the MFC-4.0 common-control struct-tag remap, adding a Common Controls v6 manifest so the rebar toolbar creates, and the runtime fixes needed to connect/join/chat on a present-day IRC network. See [`v2.5-beta-1-modern/README.md`](v2.5-beta-1-modern/README.md).

### Cloud builds

The manually triggered **Build unofficial modern clients** GitHub Actions
workflow builds both modernized clients on a pinned Windows runner, packages
each executable with its art and documentation, smoke-tests the extracted ZIPs
from random folders, and uploads release-ready artifacts with SHA-256 hashes.
Fork owners can enable Actions and run the workflow without any secrets. See
[`docs/UNOFFICIAL-RELEASE.md`](docs/UNOFFICIAL-RELEASE.md) for the packaging and
manual release process.

### A note on the modernized folders

This repository is published primarily as a **historical artifact** — the source is here for reference, study, and preservation, not as a maintained product. The `*-modern` folders are **not** a polished re-release; they're **worked examples** of the kinds of changes it takes to get a 1996–1998 MFC application building and running on a current machine, such as:
Expand Down
39 changes: 39 additions & 0 deletions docs/UNOFFICIAL-RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Creating an unofficial modern build release

The repository's modernized clients are historical worked examples, not official
product releases. GitHub Actions can produce two unsigned, portable ZIP files for
archivists, educators, and people experimenting with forks.

## Build the packages

1. Open the repository's **Actions** tab.
2. Select **Build unofficial modern clients**.
3. Choose **Run workflow** on the commit to package.
4. Download the `comic-chat-unofficial-modern-builds-*` artifact after the job
finishes.

The artifact contains:

- `ComicChat-1.0-pre-unofficial-modern.zip`
- `ComicChat-2.5-beta-1-unofficial-modern.zip`
- `SHA256SUMS.txt`

Each ZIP contains a statically linked x86 executable, its bundled `ComicArt`
directory, the original help file, documentation, the repository license, and a
provenance notice. The workflow extracts each ZIP into a random path containing
spaces, launches it with an unrelated working directory, and verifies that it
remains running.

## Publish the manual GitHub release

1. Draft a new release with a date-based tag such as
`unofficial-modern-builds-YYYY-MM`.
2. Use a title such as **Unofficial modern builds - Month YYYY**.
3. Mark the release as a **pre-release**.
4. Attach both ZIP files and `SHA256SUMS.txt`.
5. Identify the source commit and state that the executables are unsigned,
unsupported archival builds rather than an official Microsoft product
release.

No installer or code-signing material is required. Users should extract an
entire ZIP before running the executable so `ComicArt` remains beside it.
137 changes: 137 additions & 0 deletions scripts/package-modern-builds.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
[CmdletBinding()]
param(
[string]$OutputDirectory,
[string]$SourceRevision,
[string]$SourceRepositoryUrl
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if (-not $OutputDirectory) {
$OutputDirectory = Join-Path $repoRoot 'out\modern-builds'
}
elseif (-not [IO.Path]::IsPathRooted($OutputDirectory)) {
$OutputDirectory = Join-Path $repoRoot $OutputDirectory
}
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)

if (-not $SourceRevision) {
$SourceRevision = (& git -C $repoRoot rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0) {
throw 'Could not determine the source revision.'
}
}
if (-not $SourceRepositoryUrl) {
$SourceRepositoryUrl = (& git -C $repoRoot remote get-url origin).Trim()
if ($LASTEXITCODE -ne 0) {
throw 'Could not determine the source repository URL.'
}
if ($SourceRepositoryUrl -match '^git@github\.com:(.+?)(?:\.git)?$') {
$SourceRepositoryUrl = "https://github.com/$($Matches[1])"
}
elseif ($SourceRepositoryUrl.EndsWith('.git')) {
$SourceRepositoryUrl = $SourceRepositoryUrl.Substring(0, $SourceRepositoryUrl.Length - 4)
}
}
$SourceRepositoryUrl = $SourceRepositoryUrl.TrimEnd('/')

$stagingDirectory = Join-Path $OutputDirectory 'staging'
if (Test-Path -LiteralPath $stagingDirectory) {
Remove-Item -LiteralPath $stagingDirectory -Recurse -Force
}
New-Item -ItemType Directory -Path $stagingDirectory -Force | Out-Null

$packages = @(
[pscustomobject]@{
Name = 'ComicChat-1.0-pre-unofficial-modern'
Version = 'Comic Chat 1.0 prerelease (Beta 2), modernized'
Executable = 'v1.0-pre-modern\chat.exe'
Art = 'v1.0-pre-modern\comicart'
Help = 'v1.0-pre-modern\cchat.hlp'
Readme = 'docs\MODERNIZATION.md'
RuntimeFiles = @(
'v1.0-pre-modern\cchat.cnt'
'v1.0-pre-modern\log.ept'
'v1.0-pre-modern\profile.txt'
'v1.0-pre-modern\readme.gif'
'v1.0-pre-modern\readme.htm'
'v1.0-pre-modern\readme.txt'
'v1.0-pre-modern\titles.txt'
)
},
[pscustomobject]@{
Name = 'ComicChat-2.5-beta-1-unofficial-modern'
Version = 'Comic Chat 2.5 beta 1, modernized'
Executable = 'v2.5-beta-1-modern\Release\CChat.exe'
Art = 'v2.5-beta-1-modern\comicart'
Help = 'v2.5-beta-1-modern\cchat.hlp'
Readme = 'v2.5-beta-1-modern\README.md'
RuntimeFiles = @(
'v2.5-beta-1-modern\Release\CChat.exe.manifest'
)
}
)

foreach ($package in $packages) {
$packageDirectory = Join-Path $stagingDirectory $package.Name
$artDirectory = Join-Path $packageDirectory 'ComicArt'
New-Item -ItemType Directory -Path $artDirectory -Force | Out-Null

$executable = Join-Path $repoRoot $package.Executable
$artSource = Join-Path $repoRoot $package.Art
$helpFile = Join-Path $repoRoot $package.Help
$readmeFile = Join-Path $repoRoot $package.Readme
foreach ($requiredPath in @($executable, $artSource, $helpFile, $readmeFile)) {
if (-not (Test-Path -LiteralPath $requiredPath)) {
throw "Required package input is missing: $requiredPath"
}
}

Copy-Item -LiteralPath $executable -Destination $packageDirectory
Copy-Item -Path (Join-Path $artSource '*') -Destination $artDirectory -Recurse
Copy-Item -LiteralPath $helpFile -Destination $packageDirectory
Copy-Item -LiteralPath $readmeFile -Destination (Join-Path $packageDirectory 'README.md')
Copy-Item -LiteralPath (Join-Path $repoRoot 'LICENSE') -Destination (Join-Path $packageDirectory 'LICENSE.txt')
foreach ($runtimeFile in $package.RuntimeFiles) {
$runtimePath = Join-Path $repoRoot $runtimeFile
if (-not (Test-Path -LiteralPath $runtimePath)) {
throw "Required runtime file is missing: $runtimePath"
}
Copy-Item -LiteralPath $runtimePath -Destination $packageDirectory
}

@(
'UNOFFICIAL ARCHIVAL BUILD'
''
$package.Version
''
'This is an unsigned, unsupported build of the modernized historical source.'
'It is not an official Microsoft product release.'
''
"Source: $SourceRepositoryUrl/tree/$SourceRevision"
"Revision: $SourceRevision"
"Built (UTC): $([DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ'))"
''
'There is no installer. Extract the entire directory and run the executable'
'in place so that the bundled ComicArt directory remains beside it.'
) | Set-Content -LiteralPath (Join-Path $packageDirectory 'UNOFFICIAL-BUILD.txt') -Encoding ascii

$zipPath = Join-Path $OutputDirectory "$($package.Name).zip"
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
Compress-Archive -LiteralPath $packageDirectory -DestinationPath $zipPath -CompressionLevel Optimal
}

$hashLines = Get-ChildItem -LiteralPath $OutputDirectory -Filter '*.zip' |
Sort-Object Name |
ForEach-Object {
$hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $($_.Name)"
}
$hashLines | Set-Content -LiteralPath (Join-Path $OutputDirectory 'SHA256SUMS.txt') -Encoding ascii

Remove-Item -LiteralPath $stagingDirectory -Recurse -Force
Write-Host "Created archival packages in $OutputDirectory"
99 changes: 99 additions & 0 deletions scripts/smoke-test-modern-builds.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
[CmdletBinding()]
param(
[string]$PackageDirectory,
[ValidateRange(1, 60)]
[int]$RunSeconds = 8
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if (-not $PackageDirectory) {
$PackageDirectory = Join-Path $repoRoot 'out\modern-builds'
}
elseif (-not [IO.Path]::IsPathRooted($PackageDirectory)) {
$PackageDirectory = Join-Path $repoRoot $PackageDirectory
}
$PackageDirectory = [IO.Path]::GetFullPath($PackageDirectory)

$packages = @(
[pscustomobject]@{
Name = 'ComicChat-1.0-pre-unofficial-modern'
Executable = 'chat.exe'
ArtPatterns = @('ComicArt\Avatars\*.avb', 'ComicArt\Backdrop\*')
},
[pscustomobject]@{
Name = 'ComicChat-2.5-beta-1-unofficial-modern'
Executable = 'CChat.exe'
ArtPatterns = @('ComicArt\*.avb', 'ComicArt\*.bgb')
}
)

$testRoot = Join-Path ([IO.Path]::GetTempPath()) "Comic Chat smoke $([guid]::NewGuid())"
$workingDirectory = Join-Path ([IO.Path]::GetTempPath()) "Unrelated working directory $([guid]::NewGuid())"
New-Item -ItemType Directory -Path $testRoot, $workingDirectory -Force | Out-Null

try {
foreach ($package in $packages) {
$zipPath = Join-Path $PackageDirectory "$($package.Name).zip"
if (-not (Test-Path -LiteralPath $zipPath)) {
throw "Package is missing: $zipPath"
}

$extractDirectory = Join-Path $testRoot $package.Name
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDirectory
$packageRoot = Join-Path $extractDirectory $package.Name
$executable = Join-Path $packageRoot $package.Executable
if (-not (Test-Path -LiteralPath $executable)) {
throw "Packaged executable is missing: $executable"
}

foreach ($pattern in $package.ArtPatterns) {
if (-not (Get-ChildItem -Path (Join-Path $packageRoot $pattern) -File -ErrorAction SilentlyContinue)) {
throw "Package contains no files matching $pattern"
}
}

$bytes = [IO.File]::ReadAllBytes($executable)
$peOffset = [BitConverter]::ToInt32($bytes, 0x3c)
$machine = [BitConverter]::ToUInt16($bytes, $peOffset + 4)
if ($machine -ne 0x014c) {
throw ('Expected an x86 PE executable, but {0} has machine type 0x{1:x4}.' -f $executable, $machine)
}

$process = $null
try {
$process = Start-Process -FilePath $executable -WorkingDirectory $workingDirectory -PassThru
$deadline = [DateTime]::UtcNow.AddSeconds($RunSeconds)
$mainWindow = [IntPtr]::Zero
while ([DateTime]::UtcNow -lt $deadline) {
Start-Sleep -Milliseconds 500
$process.Refresh()
if ($process.HasExited) {
throw "$($package.Executable) exited during the random-folder smoke test with code $($process.ExitCode)."
}
if ($process.MainWindowHandle -ne [IntPtr]::Zero) {
$mainWindow = $process.MainWindowHandle
}
}
if ($mainWindow -eq [IntPtr]::Zero) {
throw "$($package.Executable) did not create a top-level window during the random-folder smoke test."
}
Write-Host "$($package.Name) displayed a window and remained running for $RunSeconds seconds from $packageRoot"
}
finally {
if ($null -ne $process) {
$process.Refresh()
if (-not $process.HasExited) {
Stop-Process -Id $process.Id
$process.WaitForExit(5000) | Out-Null
}
$process.Dispose()
}
}
}
}
finally {
Remove-Item -LiteralPath $testRoot, $workingDirectory -Recurse -Force -ErrorAction SilentlyContinue
}
7 changes: 3 additions & 4 deletions v1.0-pre-modern/avatar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ CPose *GetPoseFromID(unsigned short poseID, BOOL loadMask) {
// Nope, we need to construct a pose, and register it
CPose *pose = new CPose;

char buff[100];
CString path;
AVFileRec *arec = (AVFileRec *) avRec[poseID];

sprintf(buff, "%s\\%s.avb", theApp.GetAvatarDir(), arec->filename);
VERIFY(fp = fopen(buff, "rb"));
path.Format("%s\\%s.avb", theApp.GetAvatarDir(), arec->filename);
VERIFY(fp = fopen(path, "rb"));
pose->m_drawing = new CDIB;
fseek(fp, (long)arec->fgndOffset, SEEK_SET);
VERIFY(pose->m_drawing->Load(fp));
Expand Down Expand Up @@ -973,4 +973,3 @@ const char *CAvatarX::OriginalName() {
BOOL NullAvatar() {
return (MyAvatar() == NULL);
}

6 changes: 3 additions & 3 deletions v1.0-pre-modern/bodycam.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ CBodyCam::CBodyCam()
EnableToolTips(TRUE);
m_toolTip.Activate(TRUE);

char buff[80];
CString path;
for (int i = 0; i < NEMOTIONS; i++) {
sprintf(buff, "%s\\%s", theApp.GetAvatarDir(), lg_icons[i]);
VERIFY(m_icons[i].Load(buff));
path.Format("%s\\%s", theApp.GetAvatarDir(), lg_icons[i]);
VERIFY(m_icons[i].Load(path));
}

// Scale the (96-DPI) emotion-wheel pixel metrics to the display DPI once, so the
Expand Down
Loading
Loading