mirror of
https://gitlab.com/the-no-frauds-club/cosmonarchy-bw-prerelease.git
synced 2026-09-17 01:01:12 +00:00
25 August 2026 -- Running in Wine - tooling
Ported from cosmonarchy-bw-release 46d191e, minus plugins/gptp.qdp:
this repo's gptp.qdp is its own newer build (last rebuilt in c7cee7f7)
and the release binary would regress it.
This commit is contained in:
parent
36cc26b464
commit
f3f61dc35d
55
tools/linux/README.md
Normal file
55
tools/linux/README.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Linux / Wine setup
|
||||
|
||||
Cosmonarchy runs under Wine, but a stock prefix gets four things wrong. Each one
|
||||
fails in a way that does not name its own cause, so they are collected here.
|
||||
|
||||
```sh
|
||||
tools/linux/setup-bottle.sh --check # report, change nothing
|
||||
tools/linux/setup-bottle.sh # apply
|
||||
```
|
||||
|
||||
Defaults to the Bottles bottle named `Blizzard`. Use `--bottle NAME` for a
|
||||
different bottle, `--prefix PATH` for a plain `WINEPREFIX`, and `--game-dir PATH`
|
||||
if the StarCraft directory cannot be read from the prefix's registry.
|
||||
|
||||
Every change is backed up next to the file it modifies. `--check` exits non-zero
|
||||
when something is missing, so it works in a pre-flight.
|
||||
|
||||
## What it fixes
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| Fraud Launcher 2 exits instantly | .NET 8 loads `icu.dll`, which Wine forwards to a nonexistent `icuuc68.dll` | `DOTNET_SYSTEM_GLOBALIZATION_USENLS=1` |
|
||||
| `Fonts could not be loaded` | `aize_debug.qdp` opens `lucon.ttf` / `arialuni.ttf` with FreeType; Wine ships neither | drop DejaVu in under those filenames |
|
||||
| Entirely black screen | Wine loads its own `ddraw.dll`, ignoring cnc-ddraw, so StarCraft does a real 640×480 fullscreen mode switch | `ddraw=native,builtin` |
|
||||
| Black window, correct size | cnc-ddraw's `renderer=auto` draws black here | `renderer=gdi` |
|
||||
|
||||
### Notes
|
||||
|
||||
- The launcher failure shows up in Wine's output as
|
||||
`err:module:find_forwarded_export module not found for forward
|
||||
'icuuc68.u_charsToUChars_68'`. `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` also
|
||||
works but discards culture support, so NLS is preferred.
|
||||
- FreeType opens fonts by path, not by name, so any monospace / wide-coverage
|
||||
TTF under those two filenames satisfies it. Needs `fonts-dejavu` (or
|
||||
Liberation / Noto, which the script also accepts).
|
||||
- `renderer=gdi` and clearing `shader=` were changed together, so which one
|
||||
mattered is not isolated. The bundled GLSL shader only applies to the OpenGL
|
||||
renderer, so it is inert under GDI either way — if you want it back, try
|
||||
`renderer=opengl` first.
|
||||
- Only the global `[ddraw]` section of `ddraw.ini` is touched; the per-game
|
||||
preset sections below it are left alone, and CRLF endings are preserved.
|
||||
- The `ddraw` override is written to `user.reg` directly, so close the game
|
||||
first — Wine rewrites that file from memory when the last process exits.
|
||||
|
||||
## Verified on
|
||||
|
||||
Bottles `Blizzard` bottle, runner **GE-Proton10-25**, X11, dual 2560×1440.
|
||||
Plain system wine 10.0 could not boot even vanilla StarCraft here (null call
|
||||
through `storm.dll` ordinal 313 during 640×480 init), so GE-Proton is the
|
||||
supported runner.
|
||||
|
||||
Fraud Launcher 2 installs to `C:\Cosmonarchy\`: `Starcraft\` is a stock 1.16.1
|
||||
install, `Release\` is a git clone of this repo. The game runs from
|
||||
`Release\Cosmonarchy BW.exe`, and `_qdp-hotloader.qdp` packs `Release\mpq\` into
|
||||
`Release\temp.mpq` and loads it alongside the plugins.
|
||||
42
tools/linux/add-dll-override.py
Executable file
42
tools/linux/add-dll-override.py
Executable file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add a DllOverrides entry to a Wine prefix's user.reg.
|
||||
|
||||
Usage: add-dll-override.py USER_REG DLL VALUE
|
||||
e.g. add-dll-override.py ~/.../Blizzard/user.reg ddraw native,builtin
|
||||
|
||||
Only safe while no wineserver is live for that prefix - Wine rewrites user.reg
|
||||
from memory when the last process exits, which would drop the edit.
|
||||
"""
|
||||
import sys
|
||||
|
||||
SECTION = r'[Software\\Wine\\DllOverrides]'
|
||||
|
||||
|
||||
def main(path, dll, value):
|
||||
with open(path, encoding='utf-8', errors='surrogateescape') as fh:
|
||||
text = fh.read()
|
||||
|
||||
entry = '"%s"="%s"\n' % (dll, value)
|
||||
if entry in text:
|
||||
print('already set: %s = %s' % (dll, value))
|
||||
return 0
|
||||
|
||||
i = text.find(SECTION)
|
||||
if i == -1:
|
||||
text = text.rstrip('\n') + '\n\n' + SECTION + ' 0\n' + entry
|
||||
else:
|
||||
j = text.index('\n', i) + 1 # past the section header
|
||||
while j < len(text) and text[j] == '#': # past #time= and friends
|
||||
j = text.index('\n', j) + 1
|
||||
text = text[:j] + entry + text[j:]
|
||||
|
||||
with open(path, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
||||
fh.write(text)
|
||||
print('set: %s = %s' % (dll, value))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 4:
|
||||
sys.exit(__doc__)
|
||||
sys.exit(main(*sys.argv[1:]))
|
||||
81
tools/linux/patch-ddraw-ini.py
Executable file
81
tools/linux/patch-ddraw-ini.py
Executable file
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Set keys in the [ddraw] section of a cnc-ddraw ddraw.ini.
|
||||
|
||||
Usage: patch-ddraw-ini.py [--check] DDRAW_INI KEY=VALUE [KEY=VALUE ...]
|
||||
|
||||
Only the global [ddraw] section is touched; the hundreds of per-game preset
|
||||
sections further down the file are left alone. CRLF line endings are preserved,
|
||||
because cnc-ddraw writes the file back out as a Windows INI.
|
||||
|
||||
Exit status: 0 if the file already matches (or was updated), 1 under --check if
|
||||
it does not match.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def main(argv):
|
||||
check = False
|
||||
if argv and argv[0] == '--check':
|
||||
check, argv = True, argv[1:]
|
||||
if len(argv) < 2:
|
||||
sys.exit(__doc__)
|
||||
|
||||
path, assignments = argv[0], argv[1:]
|
||||
wanted = {}
|
||||
for a in assignments:
|
||||
if '=' not in a:
|
||||
sys.exit('not a KEY=VALUE pair: %s' % a)
|
||||
k, v = a.split('=', 1)
|
||||
wanted[k.strip().lower()] = v
|
||||
|
||||
with open(path, 'rb') as fh:
|
||||
raw = fh.read().decode('utf-8', 'surrogateescape')
|
||||
|
||||
newline = '\r\n' if '\r\n' in raw else '\n'
|
||||
lines = raw.split(newline)
|
||||
|
||||
out, in_ddraw, pending, changes = [], False, dict(wanted), []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('['):
|
||||
# leaving [ddraw]: append any keys the section never had
|
||||
if in_ddraw and pending:
|
||||
for k, v in pending.items():
|
||||
out.append('%s=%s' % (k, v))
|
||||
changes.append('added %s=%s' % (k, v))
|
||||
pending = {}
|
||||
in_ddraw = stripped.lower() == '[ddraw]'
|
||||
elif in_ddraw:
|
||||
m = re.match(r'^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*)=(.*)$', line)
|
||||
if m and m.group(2).lower() in pending:
|
||||
key = m.group(2).lower()
|
||||
want = pending.pop(key)
|
||||
if m.group(4) != want:
|
||||
changes.append('%s=%s -> %s=%s' % (m.group(2), m.group(4), m.group(2), want))
|
||||
line = '%s=%s' % (m.group(2), want)
|
||||
out.append(line)
|
||||
|
||||
if in_ddraw and pending:
|
||||
for k, v in pending.items():
|
||||
out.append('%s=%s' % (k, v))
|
||||
changes.append('added %s=%s' % (k, v))
|
||||
|
||||
if not changes:
|
||||
print('already set: %s' % ', '.join('%s=%s' % kv for kv in wanted.items()))
|
||||
return 0
|
||||
|
||||
if check:
|
||||
for c in changes:
|
||||
print('would change: %s' % c)
|
||||
return 1
|
||||
|
||||
with open(path, 'wb') as fh:
|
||||
fh.write(newline.join(out).encode('utf-8', 'surrogateescape'))
|
||||
for c in changes:
|
||||
print(c)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
229
tools/linux/setup-bottle.sh
Executable file
229
tools/linux/setup-bottle.sh
Executable file
@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare a Bottles/Wine prefix so Fraud Launcher 2 and Cosmonarchy BW will run.
|
||||
#
|
||||
# Fixes two things that a stock Wine prefix gets wrong:
|
||||
#
|
||||
# 1. Fraud Launcher 2 (.NET 8 WPF) aborts at startup because Wine's icu.dll
|
||||
# forwards to icuuc68.dll, which does not exist:
|
||||
# err:module:find_forwarded_export module not found for forward
|
||||
# 'icuuc68.u_charsToUChars_68' used by L"C:\\windows\\system32\\icu.dll"
|
||||
# Setting DOTNET_SYSTEM_GLOBALIZATION_USENLS=1 makes .NET use Windows NLS
|
||||
# instead of ICU. (DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 also works but
|
||||
# throws away culture support, so prefer NLS.)
|
||||
#
|
||||
# 2. aize_debug.qdp initialises its console with FreeType against two Windows
|
||||
# system fonts that Wine does not ship, and dies with "Fonts could not be
|
||||
# loaded":
|
||||
# %systemroot%\fonts\lucon.ttf (Lucida Console)
|
||||
# %systemroot%\fonts\arialuni.ttf (Arial Unicode MS)
|
||||
# FreeType opens these by path, not by name, so any monospace / wide-coverage
|
||||
# TTF dropped in under those filenames satisfies it.
|
||||
#
|
||||
# 3. Cosmonarchy ships cnc-ddraw (ddraw.dll beside StarCraft.exe), but Wine
|
||||
# loads its own builtin ddraw.dll unless told otherwise, so cnc-ddraw is
|
||||
# silently ignored. StarCraft then performs a real exclusive-fullscreen
|
||||
# 640x480 DirectDraw mode switch, which on a multi-monitor X11 desktop
|
||||
# shows up as an entirely black screen. Overriding ddraw to native,builtin
|
||||
# makes Wine load cnc-ddraw so the game renders in a normal window.
|
||||
#
|
||||
# 4. With cnc-ddraw actually loaded, its default renderer=auto still draws a
|
||||
# black window here; renderer=gdi renders correctly. The bundled GLSL
|
||||
# shader only applies to the OpenGL renderer, so it is cleared alongside.
|
||||
# (Both were changed together, so which one mattered is not isolated - if
|
||||
# you want the shader back, try renderer=opengl first.)
|
||||
#
|
||||
# Usage:
|
||||
# tools/linux/setup-bottle.sh [--check] [--bottle NAME | --prefix PATH]
|
||||
#
|
||||
# --check report what is missing, change nothing
|
||||
# --bottle NAME Bottles bottle name (default: Blizzard)
|
||||
# --prefix PATH a plain WINEPREFIX, instead of a Bottles bottle
|
||||
# --game-dir PATH the StarCraft directory holding ddraw.ini
|
||||
# (default: read from the prefix's registry InstallPath)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
BOTTLE=Blizzard
|
||||
PREFIX=
|
||||
GAMEDIR=
|
||||
CHECK=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--check) CHECK=1; shift ;;
|
||||
--bottle) BOTTLE="${2:?--bottle needs a name}"; shift 2 ;;
|
||||
--prefix) PREFIX="${2:?--prefix needs a path}"; shift 2 ;;
|
||||
--game-dir) GAMEDIR="${2:?--game-dir needs a path}"; shift 2 ;;
|
||||
-h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$PREFIX" ]; then
|
||||
for base in \
|
||||
"$HOME/.var/app/com.usebottles.bottles/data/bottles/bottles" \
|
||||
"$HOME/.local/share/bottles/bottles"
|
||||
do
|
||||
if [ -d "$base/$BOTTLE" ]; then PREFIX="$base/$BOTTLE"; break; fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$PREFIX" ] || [ ! -d "$PREFIX/drive_c" ]; then
|
||||
echo "error: no Wine prefix found (bottle '$BOTTLE')." >&2
|
||||
echo " pass --bottle NAME or --prefix /path/to/prefix" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "prefix: $PREFIX"
|
||||
rc=0
|
||||
|
||||
note() { if [ "$CHECK" = 1 ]; then echo " MISSING $1"; rc=1; else echo " fixed $1"; fi; }
|
||||
ok() { echo " ok $1"; }
|
||||
|
||||
# --- 1. fonts -----------------------------------------------------------------
|
||||
# Each entry is "target-filename<TAB>candidate source paths, first match wins".
|
||||
install_font() {
|
||||
local target="$1"; shift
|
||||
local fontdir dest src
|
||||
fontdir="$PREFIX/drive_c/windows/Fonts"
|
||||
[ -d "$fontdir" ] || fontdir="$PREFIX/drive_c/windows/fonts"
|
||||
mkdir -p "$fontdir"
|
||||
dest="$fontdir/$target"
|
||||
|
||||
if [ -s "$dest" ]; then ok "$target"; return 0; fi
|
||||
|
||||
if [ "$CHECK" = 1 ]; then note "$target"; return 0; fi
|
||||
|
||||
for src in "$@"; do
|
||||
if [ -s "$src" ]; then
|
||||
cp -- "$src" "$dest"
|
||||
note "$target <- $(basename "$src")"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# last resort: ask fontconfig for anything matching
|
||||
if command -v fc-match >/dev/null 2>&1; then
|
||||
src=$(fc-match -f '%{file}' "${FC_QUERY:-sans}" 2>/dev/null || true)
|
||||
if [ -n "$src" ] && [ -s "$src" ]; then
|
||||
cp -- "$src" "$dest"
|
||||
note "$target <- $(basename "$src") (fontconfig)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " FAILED $target - no source font found; install fonts-dejavu" >&2
|
||||
rc=1
|
||||
}
|
||||
|
||||
echo "fonts (for aize_debug.qdp's FreeType console):"
|
||||
FC_QUERY=monospace install_font lucon.ttf \
|
||||
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf \
|
||||
/usr/share/fonts/dejavu/DejaVuSansMono.ttf \
|
||||
/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf \
|
||||
/usr/share/fonts/liberation/LiberationMono-Regular.ttf
|
||||
FC_QUERY=sans install_font arialuni.ttf \
|
||||
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \
|
||||
/usr/share/fonts/dejavu/DejaVuSans.ttf \
|
||||
/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf \
|
||||
/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf
|
||||
|
||||
# --- 2. .NET globalization (Fraud Launcher 2) ---------------------------------
|
||||
echo ".NET globalization (for Fraud Launcher 2):"
|
||||
VAR=DOTNET_SYSTEM_GLOBALIZATION_USENLS
|
||||
YML="$PREFIX/bottle.yml"
|
||||
|
||||
if [ -f "$YML" ]; then
|
||||
if grep -q "^ *$VAR:" "$YML"; then
|
||||
ok "$VAR set in bottle.yml"
|
||||
elif [ "$CHECK" = 1 ]; then
|
||||
note "$VAR in bottle.yml"
|
||||
else
|
||||
cp -- "$YML" "$YML.bak-$(date +%Y%m%d%H%M%S)"
|
||||
python3 - "$YML" "$VAR" <<'PY'
|
||||
import re, sys
|
||||
path, var = sys.argv[1], sys.argv[2]
|
||||
s = open(path, encoding='utf-8').read()
|
||||
line = " %s: '1'\n" % var
|
||||
if re.search(r'^Environment_Variables:\s*\{\}\s*$', s, re.M):
|
||||
s = re.sub(r'^Environment_Variables:\s*\{\}\s*$',
|
||||
"Environment_Variables:\n" + line.rstrip('\n'), s, count=1, flags=re.M)
|
||||
elif re.search(r'^Environment_Variables:\s*$', s, re.M):
|
||||
s = re.sub(r'^(Environment_Variables:\s*\n)', r'\1' + line, s, count=1, flags=re.M)
|
||||
else:
|
||||
raise SystemExit("could not find Environment_Variables: in %s" % path)
|
||||
open(path, 'w', encoding='utf-8').write(s)
|
||||
PY
|
||||
note "$VAR in bottle.yml (backup written alongside)"
|
||||
fi
|
||||
else
|
||||
# plain WINEPREFIX: no bottle.yml to edit, so tell the caller how to export it
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
note "$VAR (no bottle.yml; export it yourself)"
|
||||
else
|
||||
echo " note no bottle.yml here - run the launcher with:"
|
||||
echo " $VAR=1 wine 'Fraud Launcher 2.exe'"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 3. ddraw override, so Wine loads cnc-ddraw rather than its own builtin ----
|
||||
echo "ddraw override (cnc-ddraw):"
|
||||
REG="$PREFIX/user.reg"
|
||||
HELPER="$(dirname "$0")/add-dll-override.py"
|
||||
if [ ! -f "$REG" ]; then
|
||||
echo " FAILED no user.reg in $PREFIX" >&2
|
||||
rc=1
|
||||
elif grep -q '^"ddraw"=' "$REG"; then
|
||||
ok "ddraw = $(grep -m1 '^\"ddraw\"=' "$REG" | cut -d= -f2- | tr -d '\"')"
|
||||
elif [ "$CHECK" = 1 ]; then
|
||||
note "ddraw=native,builtin in user.reg"
|
||||
elif pgrep -f "wineserver" >/dev/null 2>&1 && pgrep -af wineserver | grep -qF "$PREFIX"; then
|
||||
echo " SKIPPED a wineserver is live for this prefix - close the game and re-run" >&2
|
||||
rc=1
|
||||
elif [ ! -f "$HELPER" ]; then
|
||||
echo " FAILED helper not found: $HELPER" >&2
|
||||
rc=1
|
||||
else
|
||||
cp -- "$REG" "$REG.bak-$(date +%Y%m%d%H%M%S)"
|
||||
python3 "$HELPER" "$REG" ddraw native,builtin >/dev/null
|
||||
note "ddraw=native,builtin in user.reg (backup written alongside)"
|
||||
fi
|
||||
|
||||
|
||||
# --- 4. cnc-ddraw renderer ----------------------------------------------------
|
||||
echo "cnc-ddraw renderer:"
|
||||
if [ -z "$GAMEDIR" ]; then
|
||||
# HKLM\Software\Wow6432Node\Blizzard Entertainment\Starcraft -> InstallPath
|
||||
winpath=$(grep -a -m1 '^"InstallPath"=' "$PREFIX/system.reg" 2>/dev/null \
|
||||
| sed 's/^"InstallPath"="//; s/"$//')
|
||||
if [ -n "$winpath" ]; then
|
||||
rel=$(printf '%s' "$winpath" | sed 's/\\\\/\//g; s|^[A-Za-z]:||; s|/*$||')
|
||||
GAMEDIR="$PREFIX/drive_c$rel"
|
||||
fi
|
||||
fi
|
||||
|
||||
INI="$GAMEDIR/ddraw.ini"
|
||||
PATCHER="$(dirname "$0")/patch-ddraw-ini.py"
|
||||
if [ -z "$GAMEDIR" ] || [ ! -f "$INI" ]; then
|
||||
echo " SKIPPED no ddraw.ini found${GAMEDIR:+ at $GAMEDIR}; pass --game-dir" >&2
|
||||
elif [ ! -f "$PATCHER" ]; then
|
||||
echo " FAILED helper not found: $PATCHER" >&2
|
||||
rc=1
|
||||
elif python3 "$PATCHER" --check "$INI" renderer=gdi shader= >/dev/null 2>&1; then
|
||||
ok "renderer=gdi in $(basename "$GAMEDIR")/ddraw.ini"
|
||||
elif [ "$CHECK" = 1 ]; then
|
||||
note "renderer=gdi in $(basename "$GAMEDIR")/ddraw.ini"
|
||||
else
|
||||
cp -- "$INI" "$INI.bak-$(date +%Y%m%d%H%M%S)"
|
||||
python3 "$PATCHER" "$INI" renderer=gdi shader= >/dev/null
|
||||
note "renderer=gdi, shader= in $(basename "$GAMEDIR")/ddraw.ini (backup written alongside)"
|
||||
fi
|
||||
|
||||
|
||||
echo
|
||||
if [ "$CHECK" = 1 ] && [ "$rc" != 0 ]; then
|
||||
echo "check failed: run without --check to apply."
|
||||
elif [ "$rc" = 0 ]; then
|
||||
echo "bottle ready."
|
||||
fi
|
||||
exit $rc
|
||||
Loading…
Reference in New Issue
Block a user