Skip to content
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
57 changes: 55 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,31 @@ jobs:
- name: Build portable ZIP
run: cpack --preset windows-portable

# The runner has Visual Studio, so a missing runtime DLL would never fail
# a test here. Every VC++ runtime DLL the package's programs and DLLs
# import must be in the package, and every other DLL there or in Windows.
- name: Check the package's DLL imports
shell: pwsh
run: |
$zip = Get-ChildItem out/build/windows-msvc -Filter 'OpenMM2-*-portable.zip' | Select-Object -First 1
$dir = Join-Path $env:RUNNER_TEMP 'portable-check'
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
Expand-Archive $zip.FullName -DestinationPath $dir
$bad = @()
foreach ($exe in Get-ChildItem $dir -Recurse -Include *.exe, *.dll) {
$here = $exe.DirectoryName
$deps = (dumpbin /nologo /dependents $exe.FullName) |
ForEach-Object { if ($_ -match '^\s+(\S+\.dll)\s*$') { $Matches[1] } }
foreach ($dll in $deps) {
$packaged = Test-Path (Join-Path $here $dll)
$runtime = $dll -match '^(vcruntime|msvcp|concrt|vccorlib)'
$system = (Test-Path (Join-Path "$env:WINDIR\System32" $dll)) -or $dll -match '^(api|ext)-ms-'
Write-Host "$($exe.Name): $dll packaged=$packaged"
if (-not $packaged -and ($runtime -or -not $system)) { $bad += "$($exe.Name) imports $dll, which the package does not ship" }
}
}
if ($bad.Count -gt 0) { $bad | ForEach-Object { Write-Error $_ }; exit 1 }

- name: Upload packages
uses: actions/upload-artifact@v7
with:
Expand All @@ -159,10 +184,11 @@ jobs:
steps:
- name: Install build dependencies
# Wine runs the determinism tests (the toolchain file makes it the
# tests' emulator when it is installed at configure time).
# tests' emulator when it is installed at configure time) and the
# installer tests (which also need diffutils).
run: |
pacman -Syu --noconfirm --needed \
base-devel git cmake ninja mingw-w64-gcc shaderc glslang wine
base-devel git cmake ninja mingw-w64-gcc shaderc glslang wine diffutils

- uses: actions/checkout@v7

Expand Down Expand Up @@ -194,6 +220,33 @@ jobs:
wineboot --init
ctest --preset mingw-cross-release -R '^Determinism[.]'

# NSIS is not packaged for Arch Linux: use the Windows makensis.exe
# through Wine (pinned by hash).
- name: Cache NSIS
uses: actions/cache@v6
with:
path: nsis-3.13
key: nsis-3.13-windows

# Installs, updates (also from the 0.4.0 installer), repair,
# downgrades, a running game, a failed game data copy, moving and
# uninstalling, and the wizard's pages, all under Wine with synthetic
# game data (packaging/windows/test-installer.sh).
- name: Test the installer under Wine
run: |
if [ ! -f nsis-3.13/makensis.exe ]; then
curl -fsSL --retry 3 -o nsis.zip \
https://downloads.sourceforge.net/project/nsis/NSIS%203/3.13/nsis-3.13.zip
echo "ba63dffc4410ee89193e1cb5a41989991bd77c61068da17e3156d136b7b0b3d8 nsis.zip" | sha256sum -c -
cmake -E tar xf nsis.zip
rm nsis.zip
fi
# The update tests start from the last release's installer scripts.
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git fetch --no-tags --depth=1 origin tag v0.4.0
MAKENSIS="$PWD/nsis-3.13/makensis.exe" MAKENSIS_WINE=1 \
packaging/windows/test-installer.sh out/build/mingw-cross-release

- name: Package
run: cpack --preset mingw-portable

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25...4.0)

project(OpenMM2
VERSION 0.4.0
VERSION 0.4.1
DESCRIPTION "Open source reimplementation of Midtown Madness 2"
HOMEPAGE_URL "https://github.com/lowlevelmetal/OpenMM2"
LANGUAGES C CXX)
Expand Down
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ Run `OpenMM2-<version>-windows-x86_64-setup.exe` (64-bit Windows 7 or later).
The installer asks where your Midtown Madness 2 files are and checks them
before continuing. You can also skip that step; OpenMM2 asks on first start.

To update, run the new version's installer. It finds the installed version
and updates it in its folder, keeping your settings, driver profiles,
replays and game data; the game data and shortcut choices are filled in from
last time, so an update is Next, Next, Install. Files that the new version no
longer ships are removed. Running the same version again repairs the
installation, and an older version asks before it replaces a newer one. Close
OpenMM2 first: setup waits (Retry/Cancel) until it is closed.

Silent installs accept:

| Option | Meaning |
Expand All @@ -53,9 +61,27 @@ Silent installs accept:
| `/D=C:\Games\OpenMM2` | install directory (must be last, unquoted) |
| `/SOURCE=<path>` | game data: disc image, drive root such as `D:\`, or install folder |
| `/COPYDATA` | copy the game archives to `<install dir>\gamedata` |
| `/ALLOWDOWNGRADE` | replace a newer installed version (refused otherwise) |

The exit code is 0 on success and 2 if the program was installed but the
`/SOURCE` was rejected.
| Exit code | Meaning |
| --- | --- |
| 0 | success |
| 1 | cancelled or failed (for example, another program held a file open) |
| 2 | installed, but the `/SOURCE` game data was rejected (OpenMM2 asks on first start) |
| 3 | nothing changed: a newer version is installed and `/ALLOWDOWNGRADE` was not given |
| 4 | nothing changed: OpenMM2 is running |

The uninstaller also stops while OpenMM2 is running (exit code 4 when run as
`uninstall.exe /S _?=<install dir>`; otherwise it restarts itself from a
temporary copy and returns at once).

A silent update keeps the last installation's choices: game data, shortcuts
and developer tools. `/D=` naming another folder moves the installation:
setup removes the old folder's program files and uninstaller but keeps its
`gamedata` folder and `openmm2-install.ini` (and keeps using that game data
unless `/SOURCE` is given), so the old folder stays only if it holds them.
The installer lists what it installed in `install-manifest.txt`, which the
next update and the uninstaller use to remove exactly those files.

The portable ZIP (`OpenMM2-<version>-windows-x86_64-portable.zip`) needs no
installation: unpack it anywhere. Because it contains `portable.txt`, settings
Expand Down Expand Up @@ -170,6 +196,18 @@ The installer is built from `packaging/windows/openmm2.nsi` and needs NSIS 3
`makensis` also works when cross-compiling. The icon artwork is in
`packaging/icons` (`render-icons.sh` regenerates the bitmaps from the SVG).

`packaging/windows/test-installer.sh <build dir>` tests the installer under
Wine: new installations, updates (also from the last release's installer),
repair, downgrades, a running game, a failed game data copy, moving with
`/D=`, uninstalling, and the wizard's pages. It uses synthetic game data, so
it needs no game files. It needs Wine, a MinGW-w64 C compiler and makensis
(`MAKENSIS=<path>`, or `MAKENSIS_WINE=1` with the Windows `makensis.exe`);
the script's header lists its options. CI runs it on the MinGW build.

```sh
packaging/windows/test-installer.sh out/build/mingw-cross-release
```

## mm2tool

`mm2tool` is a developer tool for looking inside the game data:
Expand Down
5 changes: 4 additions & 1 deletion cmake/Dependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ check_c_source_compiles("
#include <threads.h>
int main(void) { mtx_t m; if (mtx_init(&m, mtx_recursive) != thrd_success) return 1; mtx_destroy(&m); return 0; }"
OPENMM2_HAVE_C11_THREADS)
if(OPENMM2_HAVE_C11_THREADS)
# Not on MSVC: its C11 threads live in vcruntime140_threads.dll (VS 2022 17.8+),
# which older VC++ redistributables lack and the packages did not ship, so the
# game failed to start on such PCs. The library's Win32 threads, as on MinGW.
if(OPENMM2_HAVE_C11_THREADS AND NOT MSVC)
target_compile_definitions(openmm2_dmusic PRIVATE _DM_USE_NATIVE_THREAD=1)
else()
target_sources(openmm2_dmusic PRIVATE "${_dm}/src/thread/Thread.Posix.c" "${_dm}/src/thread/Thread.Win32.c")
Expand Down
5 changes: 5 additions & 0 deletions packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<display_length compare="ge">768</display_length>
</requires>
<releases>
<release version="0.4.1" date="2026-10-10" type="development">
<description>
<p>Multiplayer fixes: a late network message from one race can no longer stop a player's car responding in the next, a host's 25-character password can be typed by joiners, and changing the shared traffic settings clears the joiners' ready.</p>
</description>
</release>
<release version="0.4.0" date="2026-10-10" type="development">
<description>
<p>Multiplayer rebuilt around the host: the host decides every car, collision, traffic and police car, prop, damage and rule, while each player's own car and the cars near it are predicted so they stay responsive. Players see the same game on every screen, and Windows and Linux players simulate identically.</p>
Expand Down
8 changes: 8 additions & 0 deletions packaging/windows/GameDataPage.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
; Inputs/outputs (declared in openmm2.nsi):
; $GameSource chosen path, "" to skip
; $CopyData 1 to import the archives into $INSTDIR\gamedata
; Input (declared here):
; $GD_NextIsInstall 1 when installation starts right after this page (an
; update skips the directory page), so Next reads Install
; Requires the section index ${SecGameData} and the define STAGE_DIR.

!include nsDialogs.nsh
Expand Down Expand Up @@ -38,6 +41,7 @@ Var GD_Folder
Var GD_CopyState ; 0/1
Var GD_Initialized
Var GD_ExeReady
Var GD_NextIsInstall

; Fills the state from $GameSource (previous install or /SOURCE=) or from
; whatever can be found on this machine.
Expand Down Expand Up @@ -124,6 +128,10 @@ Function GameDataPageCreate
${If} $GD_Dialog == error
Abort
${EndIf}
${If} $GD_NextIsInstall == 1
GetDlgItem $0 $HWNDPARENT 1
SendMessage $0 ${WM_SETTEXT} 0 "STR:$(^InstallBtn)"
${EndIf}

${NSD_CreateLabel} 0 0 100% 18u "OpenMM2 contains no game content. It uses the files from your original Midtown Madness 2 disc. Where are they?"
Pop $0
Expand Down
31 changes: 22 additions & 9 deletions packaging/windows/GenerateFileList.cmake
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Writes an NSIS include with two macros for a staged install tree:
# !macro <MACRO>_INSTALL SetOutPath/File lines for every staged file
# !macro <MACRO>_UNINSTALL matching Delete lines, then RMDir for the
# subdirectories (deepest first, never recursive)
# so the uninstaller removes exactly what was installed.
# Writes an NSIS include with three macros for a staged install tree:
# !macro <MACRO>_INSTALL SetOutPath/File lines for every staged file
# !macro <MACRO>_UNINSTALL <dir> matching Delete lines under <dir>, then
# RMDir for the subdirectories (deepest
# first, never recursive)
# !macro <MACRO>_MANIFEST <handle> FileWrite lines naming every staged file,
# relative to the install directory, for
# install-manifest.txt (see openmm2.nsi)
# so setup and the uninstaller remove exactly what was installed.
#
# cmake -DSTAGE=<dir> -DOUTPUT=<file.nsh> -DMACRO=OPENMM2_APP
# [-DWINE_PATHS=ON] -P GenerateFileList.cmake
Expand Down Expand Up @@ -38,10 +42,17 @@ list(SORT files)

set(install "; Generated by packaging/windows/GenerateFileList.cmake. Do not edit.\n")
string(APPEND install "!macro ${MACRO}_INSTALL\n")
set(uninstall "!macro ${MACRO}_UNINSTALL\n")
set(uninstall "!macro ${MACRO}_UNINSTALL base\n")
set(manifest "!macro ${MACRO}_MANIFEST handle\n")
set(dirs)
set(current_dir "<none>")
foreach(rel IN LISTS files)
# install-manifest.txt is written with FileWrite, which converts to the
# ANSI code page: keep the names plain ASCII so the file reads the same
# everywhere. The names are ours (programs, licences, fonts).
if(NOT rel MATCHES "^[A-Za-z0-9 ._+/-]+$")
message(FATAL_ERROR "GenerateFileList.cmake: unsupported characters in staged file name '${rel}'")
endif()
cmake_path(GET rel PARENT_PATH dir)
if(NOT dir STREQUAL current_dir)
set(current_dir "${dir}")
Expand All @@ -63,7 +74,8 @@ foreach(rel IN LISTS files)
string(APPEND install " File \"${src}\"\n")
string(REPLACE "/" "\\" rel_win "${rel}")
_nsis_escape(rel_win "${rel_win}")
string(APPEND uninstall " Delete \"$INSTDIR\\${rel_win}\"\n")
string(APPEND uninstall " Delete \"\${base}\\${rel_win}\"\n")
string(APPEND manifest " FileWrite \${handle} \"${rel_win}$\\r$\\n\"\n")
endforeach()

# Deepest directories first.
Expand All @@ -80,9 +92,10 @@ foreach(entry IN LISTS keyed)
string(REGEX REPLACE "^[0-9]+\\|" "" d "${entry}")
string(REPLACE "/" "\\" d_win "${d}")
_nsis_escape(d_win "${d_win}")
string(APPEND uninstall " RMDir \"$INSTDIR\\${d_win}\"\n")
string(APPEND uninstall " RMDir \"\${base}\\${d_win}\"\n")
endforeach()

string(APPEND install "!macroend\n")
string(APPEND uninstall "!macroend\n")
file(WRITE "${OUTPUT}" "${install}\n${uninstall}")
string(APPEND manifest "!macroend\n")
file(WRITE "${OUTPUT}" "${install}\n${uninstall}\n${manifest}")
Loading
Loading