mtrevival 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mtrevival/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Tools for reviving Monopoly Tycoon (2001)."""
2
+
3
+ __version__ = "0.1.0"
mtrevival/__main__.py ADDED
@@ -0,0 +1,95 @@
1
+ """Command line entry point: ``python -m mtrevival``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from . import fixpack
11
+
12
+ _RESOLUTION = re.compile(r"^(\d{3,4})x(\d{3,4})$")
13
+
14
+
15
+ def resolution(text: str) -> tuple[int, int]:
16
+ """argparse type for WIDTHxHEIGHT."""
17
+ m = _RESOLUTION.match(text.strip().lower())
18
+ if not m:
19
+ raise argparse.ArgumentTypeError(
20
+ "expected WIDTHxHEIGHT such as 1920x1080, got %r" % text)
21
+ return int(m.group(1)), int(m.group(2))
22
+
23
+
24
+ def main(argv: list[str] | None = None) -> int:
25
+ parser = argparse.ArgumentParser(
26
+ prog="mtrevival",
27
+ description="Make Monopoly Tycoon (2001) run on modern Windows.")
28
+ sub = parser.add_subparsers(dest="command", required=True)
29
+
30
+ def common(p: argparse.ArgumentParser) -> None:
31
+ p.add_argument("--game-dir", type=Path, default=None,
32
+ help="install directory containing mc.exe")
33
+ p.add_argument("--resolution", type=resolution,
34
+ default=(fixpack.DEFAULT_WIDTH, fixpack.DEFAULT_HEIGHT),
35
+ metavar="WxH",
36
+ help="display mode, default %dx%d; the adapter must list it"
37
+ % (fixpack.DEFAULT_WIDTH, fixpack.DEFAULT_HEIGHT))
38
+
39
+ for name, help_text in (("check", "report what would be done, change nothing"),
40
+ ("fix", "write config.cfg with a working adapter")):
41
+ p = sub.add_parser(name, help=help_text)
42
+ common(p)
43
+ p.add_argument("--windowed", action="store_true",
44
+ help="run in a window instead of fullscreen (needs patch 1.2)")
45
+ p.add_argument("--music", action="store_true",
46
+ help="restore the soundtrack: install the wmsource-shim "
47
+ "for this user and write music 0")
48
+ common(sub.add_parser("adapters", help="list adapters and whether they fit"))
49
+
50
+ args = parser.parse_args(argv)
51
+ width, height = args.resolution
52
+ windowed = getattr(args, "windowed", False)
53
+ with_music = getattr(args, "music", False)
54
+
55
+ try:
56
+ game_dir = fixpack.find_install(args.game_dir)
57
+ except fixpack.FixError as error:
58
+ print("error: %s" % error, file=sys.stderr)
59
+ return 2
60
+
61
+ if args.command == "adapters":
62
+ adapters, source = fixpack.read_adapters(game_dir)
63
+ print("source: %s" % source)
64
+ for adapter in adapters:
65
+ fits = adapter.supports(width, height)
66
+ print(" adapter %d %-34s modes=%-4d %s%s"
67
+ % (adapter.index, adapter.description or "(unnamed)",
68
+ len(adapter.modes),
69
+ ("OK %dx%d" if fits else "no %dx%d") % (width, height),
70
+ " [portrait]" if adapter.is_portrait else ""))
71
+ return 0
72
+
73
+ plan = fixpack.build_plan(game_dir, width, height, windowed, with_music)
74
+ print(fixpack.describe(plan))
75
+
76
+ if args.command == "check":
77
+ return 0 if plan.ok else 1
78
+
79
+ try:
80
+ backup = fixpack.apply(plan)
81
+ except fixpack.FixError as error:
82
+ print("\nerror: %s" % error, file=sys.stderr)
83
+ return 1
84
+
85
+ if plan.music:
86
+ print("\nInstalled %s and registered it for this user." % plan.shim_target)
87
+ print("\nWrote %s" % plan.config_path)
88
+ if backup:
89
+ print("Backed up previous config to %s" % backup)
90
+ print("Launch the game with mc.exe.")
91
+ return 0
92
+
93
+
94
+ if __name__ == "__main__":
95
+ sys.exit(main())
Binary file
mtrevival/d3denum.py ADDED
@@ -0,0 +1,114 @@
1
+ """Parser for Monopoly Tycoon's own ``D3DEnum.txt``.
2
+
3
+ The game writes this file when it enumerates Direct3D 8 adapters. It is the
4
+ authoritative record of what *the game* sees, which is what matters when
5
+ choosing an adapter index for ``config.cfg`` — it can differ from what Windows
6
+ reports, and a rotated display shows portrait modes here.
7
+
8
+ Example of the format::
9
+
10
+ Number of display Adapters :- 2
11
+
12
+ Adapter #0
13
+ nvldumd.dll
14
+ NVIDIA GeForce RTX 4080
15
+ Device Validated
16
+ Available Video Modes = 522
17
+ Vid Mode 480 X 640 X 32
18
+ Valid Video Modes = 46
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from dataclasses import dataclass, field
25
+
26
+ _ADAPTER = re.compile(r"^Adapter #(\d+)\s*$")
27
+ _MODE = re.compile(r"^Vid Mode\s+(\d+)\s+X\s+(\d+)\s+X\s+(\d+)\s*$")
28
+ _COUNT = re.compile(r"^Number of display Adapters\s*:-\s*(\d+)\s*$")
29
+
30
+
31
+ @dataclass
32
+ class Adapter:
33
+ """One Direct3D 8 adapter as the game enumerated it."""
34
+
35
+ index: int
36
+ description: str = ""
37
+ validated: bool = False
38
+ modes: list[tuple[int, int, int]] = field(default_factory=list)
39
+
40
+ def supports(self, width: int, height: int, bpp: int | None = None) -> bool:
41
+ """True if this adapter offers the given mode."""
42
+ for w, h, b in self.modes:
43
+ if w == width and h == height and (bpp is None or b == bpp):
44
+ return True
45
+ return False
46
+
47
+ @property
48
+ def is_portrait(self) -> bool:
49
+ """True if every mode is taller than it is wide (a rotated display)."""
50
+ return bool(self.modes) and all(h > w for w, h, _ in self.modes)
51
+
52
+
53
+ def parse(text: str) -> list[Adapter]:
54
+ """Parse the contents of ``D3DEnum.txt`` into a list of adapters."""
55
+ adapters: list[Adapter] = []
56
+ current: Adapter | None = None
57
+ # A description is the first non-empty line after the DLL name line.
58
+ lines_since_header = 0
59
+
60
+ for raw in text.splitlines():
61
+ line = raw.strip()
62
+
63
+ m = _ADAPTER.match(line)
64
+ if m:
65
+ current = Adapter(index=int(m.group(1)))
66
+ adapters.append(current)
67
+ lines_since_header = 0
68
+ continue
69
+
70
+ if current is None:
71
+ continue
72
+
73
+ m = _MODE.match(line)
74
+ if m:
75
+ current.modes.append((int(m.group(1)), int(m.group(2)), int(m.group(3))))
76
+ continue
77
+
78
+ if line == "Device Validated":
79
+ current.validated = True
80
+ continue
81
+ if line == "Device is Not Valid":
82
+ current.validated = False
83
+ continue
84
+
85
+ if line:
86
+ lines_since_header += 1
87
+ # line 1 is the driver DLL, line 2 is the human-readable description
88
+ if lines_since_header == 2 and not current.description:
89
+ current.description = line
90
+
91
+ return adapters
92
+
93
+
94
+ def declared_adapter_count(text: str) -> int | None:
95
+ """The adapter count the game printed, or None if the line is absent."""
96
+ for raw in text.splitlines():
97
+ m = _COUNT.match(raw.strip())
98
+ if m:
99
+ return int(m.group(1))
100
+ return None
101
+
102
+
103
+ def choose_adapter(adapters: list[Adapter], width: int, height: int,
104
+ bpp: int | None = None) -> int | None:
105
+ """Pick the lowest-numbered validated adapter that supports the mode.
106
+
107
+ Returns the adapter index for ``SysSetup device``, or None when no adapter
108
+ offers the mode. Adapter 0 is preferred when it qualifies, because that is
109
+ what the game uses by default and it keeps the config closest to stock.
110
+ """
111
+ for adapter in sorted(adapters, key=lambda a: a.index):
112
+ if adapter.validated and adapter.supports(width, height, bpp):
113
+ return adapter.index
114
+ return None
mtrevival/displays.py ADDED
@@ -0,0 +1,115 @@
1
+ """Windows display enumeration, used only when ``D3DEnum.txt`` is absent.
2
+
3
+ ``D3DEnum.txt`` is preferred because it records what the *game* sees. This
4
+ module is the fallback for a first run, before the game has ever started.
5
+
6
+ Caveat worth knowing: Direct3D 8 adapter ordering is not guaranteed to match
7
+ ``EnumDisplayDevices`` ordering. In practice both follow the desktop device
8
+ order, with the primary display first, but a config produced from this fallback
9
+ should be re-derived from ``D3DEnum.txt`` once the game has run once.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ctypes
15
+ from ctypes import wintypes
16
+
17
+ from .d3denum import Adapter
18
+
19
+ ENUM_CURRENT_SETTINGS = -1
20
+ DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001
21
+ DISPLAY_DEVICE_PRIMARY_DEVICE = 0x00000004
22
+
23
+
24
+ class DISPLAY_DEVICEW(ctypes.Structure):
25
+ _fields_ = [
26
+ ("cb", wintypes.DWORD),
27
+ ("DeviceName", wintypes.WCHAR * 32),
28
+ ("DeviceString", wintypes.WCHAR * 128),
29
+ ("StateFlags", wintypes.DWORD),
30
+ ("DeviceID", wintypes.WCHAR * 128),
31
+ ("DeviceKey", wintypes.WCHAR * 128),
32
+ ]
33
+
34
+
35
+ class _POINTL(ctypes.Structure):
36
+ _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
37
+
38
+
39
+ class DEVMODEW(ctypes.Structure):
40
+ _fields_ = [
41
+ ("dmDeviceName", wintypes.WCHAR * 32),
42
+ ("dmSpecVersion", wintypes.WORD),
43
+ ("dmDriverVersion", wintypes.WORD),
44
+ ("dmSize", wintypes.WORD),
45
+ ("dmDriverExtra", wintypes.WORD),
46
+ ("dmFields", wintypes.DWORD),
47
+ ("dmPosition", _POINTL),
48
+ ("dmDisplayOrientation", wintypes.DWORD),
49
+ ("dmDisplayFixedOutput", wintypes.DWORD),
50
+ ("dmColor", ctypes.c_short),
51
+ ("dmDuplex", ctypes.c_short),
52
+ ("dmYResolution", ctypes.c_short),
53
+ ("dmTTOption", ctypes.c_short),
54
+ ("dmCollate", ctypes.c_short),
55
+ ("dmFormName", wintypes.WCHAR * 32),
56
+ ("dmLogPixels", wintypes.WORD),
57
+ ("dmBitsPerPel", wintypes.DWORD),
58
+ ("dmPelsWidth", wintypes.DWORD),
59
+ ("dmPelsHeight", wintypes.DWORD),
60
+ ("dmDisplayFlags", wintypes.DWORD),
61
+ ("dmDisplayFrequency", wintypes.DWORD),
62
+ ("dmICMMethod", wintypes.DWORD),
63
+ ("dmICMIntent", wintypes.DWORD),
64
+ ("dmMediaType", wintypes.DWORD),
65
+ ("dmDitherType", wintypes.DWORD),
66
+ ("dmReserved1", wintypes.DWORD),
67
+ ("dmReserved2", wintypes.DWORD),
68
+ ("dmPanningWidth", wintypes.DWORD),
69
+ ("dmPanningHeight", wintypes.DWORD),
70
+ ]
71
+
72
+
73
+ def enumerate_adapters() -> list[Adapter]:
74
+ """Enumerate attached displays and their modes, in desktop device order."""
75
+ user32 = ctypes.windll.user32
76
+ adapters: list[Adapter] = []
77
+ index = 0
78
+ device_number = 0
79
+
80
+ while True:
81
+ dev = DISPLAY_DEVICEW()
82
+ dev.cb = ctypes.sizeof(DISPLAY_DEVICEW)
83
+ if not user32.EnumDisplayDevicesW(None, device_number, ctypes.byref(dev), 0):
84
+ break
85
+ device_number += 1
86
+ if not dev.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP:
87
+ continue
88
+
89
+ adapter = Adapter(index=index, description=dev.DeviceString, validated=True)
90
+ index += 1
91
+
92
+ mode_number = 0
93
+ seen: set[tuple[int, int, int]] = set()
94
+ while True:
95
+ dm = DEVMODEW()
96
+ dm.dmSize = ctypes.sizeof(DEVMODEW)
97
+ if not user32.EnumDisplaySettingsW(dev.DeviceName, mode_number,
98
+ ctypes.byref(dm)):
99
+ break
100
+ mode_number += 1
101
+ entry = (int(dm.dmPelsWidth), int(dm.dmPelsHeight), int(dm.dmBitsPerPel))
102
+ if entry not in seen:
103
+ seen.add(entry)
104
+ adapter.modes.append(entry)
105
+
106
+ # Primary display sorts first, matching how D3D8 numbers adapters.
107
+ adapters.append((0 if dev.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE else 1,
108
+ adapter))
109
+
110
+ adapters.sort(key=lambda pair: pair[0])
111
+ result = []
112
+ for i, (_, adapter) in enumerate(adapters):
113
+ adapter.index = i
114
+ result.append(adapter)
115
+ return result
mtrevival/fixpack.py ADDED
@@ -0,0 +1,316 @@
1
+ """Apply the Windows 11 fixes to an installed copy of Monopoly Tycoon.
2
+
3
+ Two crashes stop the 2001 game on modern Windows, both the same defect: the
4
+ game creates an object, never checks whether it succeeded, then dereferences
5
+ null.
6
+
7
+ 1. ``IDirect3D8::CreateDevice`` fails when the game asks for 640x480 exclusive
8
+ fullscreen on an adapter with no such mode — for example a rotated portrait
9
+ primary display. It then calls ``SetViewport`` on the null device.
10
+ Fixed by pointing ``SysSetup device`` at an adapter that has the mode.
11
+
12
+ 2. Opening ``gamedata\\sound\\music\\music_intro.wma`` fails because the Windows
13
+ Media DirectShow source filter no longer ships with Windows.
14
+ Avoided by ``SysSetup music 1``, which suppresses that path; fixed by
15
+ ``--music``, which installs a stand-in for the missing filter and writes
16
+ ``music 0`` (see ``music.py``).
17
+
18
+ A third problem must be solved before either fix can persist: the game cannot
19
+ write ``config.cfg`` inside Program Files under a standard user account.
20
+
21
+ The same mechanism sets the resolution. Any mode the chosen adapter enumerates
22
+ works, fullscreen or (on patch 1.2) windowed; 1920x1080 and 1280x720 windowed
23
+ were verified on one machine.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import shutil
30
+ from dataclasses import dataclass
31
+ from datetime import datetime
32
+ from pathlib import Path
33
+
34
+ from . import d3denum, gameconfig, music
35
+
36
+ DEFAULT_INSTALL = Path(r"C:\Program Files (x86)\Infogrames\Monopoly Tycoon")
37
+ DEFAULT_WIDTH = gameconfig.DEFAULT_WIDTH
38
+ DEFAULT_HEIGHT = gameconfig.DEFAULT_HEIGHT
39
+ TARGET_BPP = 32
40
+
41
+ # MD5 of mc.exe for the builds this tool has been run against. Patch 1.2
42
+ # renamed the windowed-mode key, so the version decides which keys are honoured.
43
+ KNOWN_BUILDS = {
44
+ "fd34022887dc347b664b689fedeb9a37": "1.0",
45
+ "5965dbb1a4be4f58fa4452df78786e63": "1.2",
46
+ }
47
+ WINDOWED_NEEDS = "1.2"
48
+
49
+
50
+ class FixError(Exception):
51
+ """A problem that stops the fix being applied."""
52
+
53
+
54
+ @dataclass
55
+ class Plan:
56
+ """What the fix would do, before it does it."""
57
+
58
+ game_dir: Path
59
+ game_version: str
60
+ writable: bool
61
+ adapter: int | None
62
+ adapter_source: str
63
+ adapter_description: str
64
+ width: int
65
+ height: int
66
+ windowed: bool
67
+ config_path: Path
68
+ config_exists: bool
69
+ rendered: str
70
+ music: bool = False
71
+ shim_source: Path | None = None
72
+ shim_target: Path | None = None
73
+ reader_available: bool = True
74
+ run_as_admin: bool = False
75
+ tool_elevated: bool = False
76
+
77
+ @property
78
+ def ok(self) -> bool:
79
+ return (self.writable and self.adapter is not None
80
+ and not self.music_problems)
81
+
82
+ @property
83
+ def music_problems(self) -> list[str]:
84
+ """What stops ``--music`` from being applied. Empty when music is off."""
85
+ if not self.music:
86
+ return []
87
+ out = []
88
+ if self.shim_source is None:
89
+ out.append("%s is not bundled with this copy of mtrevival. Build it "
90
+ "with tools\\wmsource-shim\\build.ps1 (needs the MSVC "
91
+ "Build Tools) or install a release that includes it."
92
+ % music.SHIM_NAME)
93
+ if not self.reader_available:
94
+ out.append("The WM ASF Reader (Windows Media) is not registered for "
95
+ "32-bit programs. On Windows N editions install the "
96
+ "Media Feature Pack, then retry.")
97
+ return out
98
+
99
+ @property
100
+ def warnings(self) -> list[str]:
101
+ """Things the user should know that do not stop the fix.
102
+
103
+ Only 1.0 was observed ignoring the Window key. An unrecognised build
104
+ gets told exactly that, not a prediction.
105
+ """
106
+ out = []
107
+ if self.windowed and self.game_version == "unknown":
108
+ out.append("Windowed mode is only verified on patch %s, and this "
109
+ "mc.exe is not a build this tool recognises. The Window "
110
+ "key is written; whether the game honours it is unproven."
111
+ % WINDOWED_NEEDS)
112
+ elif self.windowed and self.game_version != WINDOWED_NEEDS:
113
+ out.append("Windowed mode needs patch %s; game version here is %s, "
114
+ "which ignores the Window key and runs fullscreen."
115
+ % (WINDOWED_NEEDS, self.game_version))
116
+ if self.music and self.run_as_admin:
117
+ out.append("mc.exe is set to run as administrator. An elevated "
118
+ "process ignores per-user COM registrations, so it will "
119
+ "not see the music shim and will crash with music 0. "
120
+ "Clear that compatibility setting before playing.")
121
+ if self.music and self.tool_elevated:
122
+ out.append("This shell is elevated. The shim is registered for the "
123
+ "account running this command; the game must run as the "
124
+ "same account, not elevated. If you elevated as a "
125
+ "different user, rerun this from a normal shell.")
126
+ return out
127
+
128
+
129
+ def find_install(explicit: Path | None = None) -> Path:
130
+ """Locate the game directory, or raise FixError.
131
+
132
+ An explicitly supplied directory is never overridden by a search: writing
133
+ into a different install than the one the caller named would be worse than
134
+ failing.
135
+ """
136
+ if explicit is not None:
137
+ if (explicit / "mc.exe").is_file():
138
+ # Absolute: the path is written into the registry and must not
139
+ # depend on the shell's working directory.
140
+ return explicit.resolve()
141
+ raise FixError("Could not find mc.exe in %s" % explicit)
142
+
143
+ for path in (DEFAULT_INSTALL,
144
+ Path(r"C:\Program Files\Infogrames\Monopoly Tycoon"),
145
+ Path(r"C:\Program Files (x86)\Infogrames Interactive\Monopoly Tycoon")):
146
+ if (path / "mc.exe").is_file():
147
+ return path
148
+ raise FixError(
149
+ "Could not find mc.exe. Pass the install directory with --game-dir.")
150
+
151
+
152
+ def game_version(game_dir: Path) -> str:
153
+ """'1.0', '1.2', or 'unknown', from the hash of mc.exe."""
154
+ digest = hashlib.md5((game_dir / "mc.exe").read_bytes()).hexdigest()
155
+ return KNOWN_BUILDS.get(digest, "unknown")
156
+
157
+
158
+ def is_writable(directory: Path) -> bool:
159
+ """True if this account can create files in the game directory."""
160
+ probe = directory / ".mtrevival-write-probe"
161
+ try:
162
+ probe.touch()
163
+ probe.unlink()
164
+ return True
165
+ except OSError:
166
+ return False
167
+
168
+
169
+ def grant_command(directory: Path) -> str:
170
+ """The elevated command that makes the game directory writable."""
171
+ return ('icacls "%s" /grant "$($env:USERNAME):(OI)(CI)M" /T' % directory)
172
+
173
+
174
+ def read_adapters(game_dir: Path) -> tuple[list[d3denum.Adapter], str]:
175
+ """Adapters as the game sees them, falling back to Windows enumeration."""
176
+ enum_file = game_dir / "D3DEnum.txt"
177
+ if enum_file.is_file():
178
+ adapters = d3denum.parse(enum_file.read_text(errors="replace"))
179
+ if adapters:
180
+ return adapters, "D3DEnum.txt"
181
+ from . import displays # imported lazily; Windows-only
182
+ return displays.enumerate_adapters(), "Windows display enumeration"
183
+
184
+
185
+ def build_plan(game_dir: Path, width: int = DEFAULT_WIDTH,
186
+ height: int = DEFAULT_HEIGHT, windowed: bool = False,
187
+ with_music: bool = False) -> Plan:
188
+ """Work out what to do without changing anything."""
189
+ adapters, source = read_adapters(game_dir)
190
+ index = d3denum.choose_adapter(adapters, width, height, TARGET_BPP)
191
+ if index is None:
192
+ # Retry ignoring colour depth: some sources record modes without bpp.
193
+ index = d3denum.choose_adapter(adapters, width, height)
194
+
195
+ description = ""
196
+ for adapter in adapters:
197
+ if adapter.index == index:
198
+ description = adapter.description
199
+ break
200
+
201
+ config_path = game_dir / "config.cfg"
202
+ config = gameconfig.default_config(index if index is not None else 0,
203
+ width, height, windowed, with_music)
204
+ return Plan(
205
+ game_dir=game_dir,
206
+ game_version=game_version(game_dir),
207
+ writable=is_writable(game_dir),
208
+ adapter=index,
209
+ adapter_source=source,
210
+ adapter_description=description,
211
+ width=width,
212
+ height=height,
213
+ windowed=windowed,
214
+ config_path=config_path,
215
+ config_exists=config_path.is_file(),
216
+ rendered=config.render(),
217
+ music=with_music,
218
+ shim_source=music.bundled_shim() if with_music else None,
219
+ shim_target=game_dir / music.SHIM_NAME,
220
+ reader_available=music.reader_available() if with_music else True,
221
+ run_as_admin=music.run_as_admin_flagged(game_dir / "mc.exe") if with_music else False,
222
+ tool_elevated=music.is_elevated() if with_music else False,
223
+ )
224
+
225
+
226
+ def install_music(plan: Plan) -> None:
227
+ """Copy the shim into the game directory and register it for this user.
228
+
229
+ The copy lives next to mc.exe rather than inside the Python package so
230
+ the registration survives the package being upgraded, moved, or run from
231
+ a temporary environment.
232
+ """
233
+ if plan.music_problems:
234
+ raise FixError("\n".join(plan.music_problems))
235
+ assert plan.shim_source is not None and plan.shim_target is not None
236
+ try:
237
+ if not (plan.shim_target.is_file()
238
+ and plan.shim_target.read_bytes() == plan.shim_source.read_bytes()):
239
+ shutil.copy2(plan.shim_source, plan.shim_target)
240
+ except OSError as error:
241
+ raise FixError("Could not copy %s into the game directory (%s). "
242
+ "Close the game if it is running, then retry."
243
+ % (music.SHIM_NAME, error))
244
+ try:
245
+ music.register(plan.shim_target)
246
+ except OSError as error:
247
+ raise FixError("Could not register %s for this user (%s)."
248
+ % (music.SHIM_NAME, error))
249
+
250
+
251
+ def apply(plan: Plan) -> Path | None:
252
+ """Write config.cfg, backing up any existing file. Returns the backup path.
253
+
254
+ With music requested, the shim is installed and registered first, so a
255
+ failure there leaves the previous config (and its ``music 1``) in place.
256
+ """
257
+ if not plan.writable:
258
+ raise FixError(
259
+ "The game directory is not writable by this account.\n"
260
+ "Run this once in an elevated PowerShell, then try again:\n\n "
261
+ + grant_command(plan.game_dir))
262
+ if plan.adapter is None:
263
+ raise FixError(
264
+ "No display adapter offers %dx%d. Every adapter reported only other "
265
+ "modes — a rotated display can do this. Pick a resolution the "
266
+ "adapter lists (see `adapters`), rotate a display to landscape, or "
267
+ "attach one that supports the mode."
268
+ % (plan.width, plan.height))
269
+ if plan.music:
270
+ install_music(plan)
271
+
272
+ backup = None
273
+ if plan.config_exists:
274
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
275
+ backup = plan.config_path.with_suffix(".cfg.%s.bak" % stamp)
276
+ shutil.copy2(plan.config_path, backup)
277
+
278
+ plan.config_path.write_bytes(plan.rendered.encode("ascii"))
279
+ return backup
280
+
281
+
282
+ def describe(plan: Plan) -> str:
283
+ """A human-readable summary of the plan."""
284
+ lines = [
285
+ "Game directory : %s" % plan.game_dir,
286
+ "Game version : %s" % plan.game_version,
287
+ "Writable : %s" % ("yes" if plan.writable else "NO"),
288
+ "Resolution : %dx%d %s" % (plan.width, plan.height,
289
+ "windowed" if plan.windowed else "fullscreen"),
290
+ "Adapter source : %s" % plan.adapter_source,
291
+ ]
292
+ if plan.adapter is None:
293
+ lines.append("Adapter : none supports %dx%d" % (plan.width, plan.height))
294
+ else:
295
+ lines.append("Adapter : %d%s"
296
+ % (plan.adapter,
297
+ (" (%s)" % plan.adapter_description)
298
+ if plan.adapter_description else ""))
299
+ lines.append("config.cfg : %s"
300
+ % ("exists, will be backed up" if plan.config_exists else "new"))
301
+ if plan.music:
302
+ lines.append("Music : restore (music 0; %s -> %s, registered "
303
+ "per user)" % (music.SHIM_NAME, plan.shim_target))
304
+ else:
305
+ lines.append("Music : off (music 1); add --music to restore it")
306
+ for problem in plan.music_problems:
307
+ lines.append("PROBLEM : %s" % problem)
308
+ for warning in plan.warnings:
309
+ lines.append("WARNING : %s" % warning)
310
+ lines.append("")
311
+ lines.append("config.cfg to write:")
312
+ lines += [" " + line for line in plan.rendered.replace("\r\n", "\n").rstrip().split("\n")]
313
+ if not plan.writable:
314
+ lines += ["", "Run this once in an elevated PowerShell first:",
315
+ " " + grant_command(plan.game_dir)]
316
+ return "\n".join(lines)
@@ -0,0 +1,97 @@
1
+ """Read and write Monopoly Tycoon's ``config.cfg``.
2
+
3
+ The file is a list of console commands, one per line, CRLF-terminated::
4
+
5
+ SysSetup api D3D
6
+ SysSetup device 1
7
+ SysSetup width 640
8
+
9
+ Verified by reading the game's process memory after it parsed a file written by
10
+ hand: the exact bytes appeared on the heap, and ``device`` changed which adapter
11
+ ``CreateDevice`` targeted.
12
+
13
+ Not every key is honoured on every code path. ``device``, ``bitdepth 32``,
14
+ ``music``, ``width``, ``height`` and (on patch 1.2) ``Window`` were observed
15
+ taking effect; the 1.0 key ``windowed`` and ``bitdepth 16`` were observed being
16
+ ignored. Treat any other key as unproven until you watch it work.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+
23
+ # Key spellings exactly as the game's own format strings write them. The 1.0
24
+ # keys come first in the order 1.0 writes them; the patch 1.2 keys follow, in
25
+ # the position verified running (``Window`` after ``music``). 1.0 ignores keys
26
+ # it does not know.
27
+ KEY_ORDER = ["api", "device", "width", "height", "bitdepth", "texbitdepth",
28
+ "windowed", "Texdetail", "sound", "music", "shware", "avail",
29
+ "Window", "Fog", "Halos", "Multitexture", "No3d", "NoMovie"]
30
+
31
+ LINE_ENDING = "\r\n"
32
+ COMMAND = "SysSetup"
33
+
34
+ DEFAULT_WIDTH = 640
35
+ DEFAULT_HEIGHT = 480
36
+
37
+
38
+ @dataclass
39
+ class Config:
40
+ """The SysSetup settings, preserving the order they should be written in."""
41
+
42
+ values: dict[str, str]
43
+
44
+ def __getitem__(self, key: str) -> str:
45
+ return self.values[key]
46
+
47
+ def get(self, key: str, default: str | None = None) -> str | None:
48
+ return self.values.get(key, default)
49
+
50
+ def set(self, key: str, value) -> None:
51
+ self.values[key] = str(value)
52
+
53
+ def render(self) -> str:
54
+ """Serialise back to the game's format, CRLF-terminated."""
55
+ known = [k for k in KEY_ORDER if k in self.values]
56
+ extra = [k for k in self.values if k not in KEY_ORDER]
57
+ lines = ["%s %s %s" % (COMMAND, k, self.values[k]) for k in known + extra]
58
+ return LINE_ENDING.join(lines) + LINE_ENDING
59
+
60
+
61
+ def parse(text: str) -> Config:
62
+ """Parse ``config.cfg`` text. Unrecognised lines are ignored."""
63
+ values: dict[str, str] = {}
64
+ for raw in text.splitlines():
65
+ parts = raw.strip().split(None, 2)
66
+ if len(parts) == 3 and parts[0].lower() == COMMAND.lower():
67
+ values[parts[1]] = parts[2]
68
+ return Config(values)
69
+
70
+
71
+ def default_config(device: int, width: int = DEFAULT_WIDTH,
72
+ height: int = DEFAULT_HEIGHT, windowed: bool = False,
73
+ music: bool = False) -> Config:
74
+ """The configuration verified to run the game on Windows 11.
75
+
76
+ ``music 1`` suppresses the WMA playback path, which crashes because the
77
+ Windows Media DirectShow source filter no longer ships with Windows.
78
+ ``music 0`` enables it, and is only safe once the wmsource-shim is
79
+ registered (see ``music.py``); 1280x720 windowed with ``music 0`` was
80
+ verified playing on patch 1.2.
81
+ ``bitdepth 32`` matches the format the game actually selects; the 16-bit
82
+ path asks for D3DFMT_R5G6B5, which modern drivers do not offer fullscreen.
83
+ ``Window 1`` is only written when asked for: it is a patch 1.2 key, and a
84
+ stock config should stay as close to what the game itself writes as it can.
85
+ """
86
+ values = {
87
+ "api": "D3D",
88
+ "device": str(device),
89
+ "width": str(width),
90
+ "height": str(height),
91
+ "bitdepth": "32",
92
+ "texbitdepth": "16",
93
+ "music": "0" if music else "1",
94
+ }
95
+ if windowed:
96
+ values["Window"] = "1"
97
+ return Config(values)
mtrevival/music.py ADDED
@@ -0,0 +1,142 @@
1
+ """Restore the WMA soundtrack.
2
+
3
+ The game plays music through a DirectShow graph whose source filter is the
4
+ 2001 "Windows Media Source Filter", CLSID ``{6B6D0800-...}`` from
5
+ ``dxmasf.dll``. Modern Windows ships neither that class nor a registration
6
+ for it, so the game's ``CoCreateInstance`` fails and, with music enabled, the
7
+ game dereferences the null result. ``SysSetup music 1`` only avoids the path.
8
+
9
+ ``wmsource-shim.dll`` (source in ``tools/wmsource-shim``) answers that CLSID
10
+ by aggregating the filter that replaced it, the WM ASF Reader, and adapting
11
+ the two things the game needs: a fresh reader per ``Load`` (the reader refuses
12
+ a second one) and ``FindPin(L"Stream 1")`` mapped onto the reader's output
13
+ pin. See ``docs/music.md``.
14
+
15
+ Registration is per user, in the 32-bit view of ``HKCU\\Software\\Classes``
16
+ (the game is a 32-bit process), so no elevation is needed and nothing outside
17
+ the user's profile changes. An elevated process ignores per-user COM classes,
18
+ so the game must not be run as administrator.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+
25
+ LEGACY_CLSID = "{6B6D0800-9ADA-11D0-A520-00A0D10129C0}"
26
+ ASF_READER_CLSID = "{187463A0-5BB7-11D3-ACBE-0080C75E246E}"
27
+ SHIM_NAME = "wmsource-shim.dll"
28
+ CLASSES = r"Software\Classes"
29
+ DESCRIPTION = "Windows Media Source Filter (mtrevival wmsource-shim)"
30
+ LAYERS = r"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"
31
+
32
+
33
+ def bundled_shim() -> Path | None:
34
+ """The shim DLL shipped inside this package, or None if it was not built."""
35
+ path = Path(__file__).with_name("bin") / SHIM_NAME
36
+ return path if path.is_file() else None
37
+
38
+
39
+ def _winreg():
40
+ try:
41
+ import winreg
42
+ except ImportError: # not Windows
43
+ return None
44
+ return winreg
45
+
46
+
47
+ def _read_value(hive_name: str, subkey: str, name: str,
48
+ view_32bit: bool = True) -> str | None:
49
+ winreg = _winreg()
50
+ if winreg is None:
51
+ return None
52
+ access = winreg.KEY_READ | (winreg.KEY_WOW64_32KEY if view_32bit
53
+ else winreg.KEY_WOW64_64KEY)
54
+ try:
55
+ with winreg.OpenKey(getattr(winreg, hive_name), subkey, 0, access) as key:
56
+ value, _ = winreg.QueryValueEx(key, name)
57
+ except OSError:
58
+ return None
59
+ return str(value)
60
+
61
+
62
+ def reader_available() -> bool:
63
+ """True if the WM ASF Reader is registered for 32-bit processes.
64
+
65
+ It is part of Windows Media, which N editions lack until the Media
66
+ Feature Pack is installed. Without it the shim cannot play anything.
67
+ """
68
+ subkey = r"%s\CLSID\%s\InprocServer32" % (CLASSES, ASF_READER_CLSID)
69
+ return _read_value("HKEY_LOCAL_MACHINE", subkey, "") is not None
70
+
71
+
72
+ def registered_server(classes: str = CLASSES) -> str | None:
73
+ """Path of the DLL currently registered for the legacy CLSID, per user."""
74
+ subkey = r"%s\CLSID\%s\InprocServer32" % (classes, LEGACY_CLSID)
75
+ return _read_value("HKEY_CURRENT_USER", subkey, "")
76
+
77
+
78
+ def register(dll: Path, classes: str = CLASSES) -> None:
79
+ """Register ``dll`` as the in-process server for the legacy CLSID, per user."""
80
+ winreg = _winreg()
81
+ if winreg is None:
82
+ raise OSError("COM registration needs Windows")
83
+ access = winreg.KEY_WRITE | winreg.KEY_WOW64_32KEY
84
+ clsid = r"%s\CLSID\%s" % (classes, LEGACY_CLSID)
85
+ with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, clsid, 0, access) as key:
86
+ winreg.SetValueEx(key, "", 0, winreg.REG_SZ, DESCRIPTION)
87
+ with winreg.CreateKeyEx(key, "InprocServer32", 0, access) as server:
88
+ winreg.SetValueEx(server, "", 0, winreg.REG_SZ, str(dll))
89
+ winreg.SetValueEx(server, "ThreadingModel", 0, winreg.REG_SZ, "Both")
90
+
91
+
92
+ def unregister(classes: str = CLASSES) -> bool:
93
+ """Remove the per-user registration. Returns True if there was one.
94
+
95
+ Only the two keys ``register`` creates are deleted. If something else
96
+ added subkeys under the CLSID, that key is left in place rather than
97
+ torn down blindly, and the InprocServer32 removal alone already unhooks
98
+ the class.
99
+ """
100
+ winreg = _winreg()
101
+ if winreg is None:
102
+ return False
103
+ access = winreg.KEY_WRITE | winreg.KEY_WOW64_32KEY
104
+ clsid = r"%s\CLSID\%s" % (classes, LEGACY_CLSID)
105
+ removed = False
106
+ for subkey in (clsid + r"\InprocServer32", clsid):
107
+ try:
108
+ winreg.DeleteKeyEx(winreg.HKEY_CURRENT_USER, subkey, access, 0)
109
+ removed = True
110
+ except FileNotFoundError:
111
+ pass
112
+ except PermissionError: # key has other subkeys; leave it
113
+ break
114
+ return removed
115
+
116
+
117
+ def run_as_admin_flagged(exe: Path) -> bool:
118
+ """True if Windows is set to run ``exe`` elevated (compatibility tab).
119
+
120
+ Elevated processes do not see per-user COM registrations, so the shim
121
+ would be invisible to the game and music would crash it again. The
122
+ Layers key is read in the 64-bit view so a 32-bit Python sees the same
123
+ machine-wide entry the shell wrote.
124
+ """
125
+ for hive in ("HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE"):
126
+ value = _read_value(hive, LAYERS, str(exe), view_32bit=False)
127
+ if value and "RUNASADMIN" in value.upper():
128
+ return True
129
+ return False
130
+
131
+
132
+ def is_elevated() -> bool:
133
+ """True if this process runs with an elevated token.
134
+
135
+ Registration goes into the running account's HKCU. From a shell elevated
136
+ as a different account it would land in the wrong hive.
137
+ """
138
+ try:
139
+ import ctypes
140
+ return bool(ctypes.windll.shell32.IsUserAnAdmin())
141
+ except (AttributeError, OSError):
142
+ return False
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: mtrevival
3
+ Version: 0.3.1
4
+ Summary: Make Monopoly Tycoon (2001) run on modern Windows
5
+ Author: Nasser Albusaidi
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/NasserAlbusaidi/monopoly-tycoon-revival
8
+ Project-URL: Issues, https://github.com/NasserAlbusaidi/monopoly-tycoon-revival/issues
9
+ Project-URL: Changelog, https://github.com/NasserAlbusaidi/monopoly-tycoon-revival/blob/main/CHANGELOG.md
10
+ Keywords: monopoly-tycoon,game-preservation,windows,directshow
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Games/Entertainment
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # Monopoly Tycoon Revival
24
+
25
+ Make **Monopoly Tycoon** (2001, Deep Red Games / Infogrames) install and run on
26
+ Windows 10 and 11.
27
+
28
+ The game installs fine on modern Windows and then crashes on startup — usually
29
+ right after the studio logo, sometimes before anything appears. There are two
30
+ separate causes, both fixable in about a minute. No patched executable, no
31
+ compatibility mode. The soundtrack needs one small COM shim registered for
32
+ your user account; everything else is a text file.
33
+
34
+ **This repository contains no game content.** You need your own copy of the game.
35
+
36
+ ---
37
+
38
+ ## Quick fix, by hand
39
+
40
+ ### 1. Let the game write to its own folder
41
+
42
+ The game stores settings, profiles and savegames next to `mc.exe`. Under
43
+ `C:\Program Files (x86)` a standard user account cannot write there, so the game
44
+ silently fails to save anything — including the settings that fix the crash.
45
+
46
+ Run once in an **elevated** PowerShell:
47
+
48
+ ```powershell
49
+ icacls "C:\Program Files (x86)\Infogrames\Monopoly Tycoon" /grant "$($env:USERNAME):(OI)(CI)M" /T
50
+ ```
51
+
52
+ ### 2. Create `config.cfg`
53
+
54
+ In the game folder, create a file named `config.cfg`:
55
+
56
+ ```
57
+ SysSetup api D3D
58
+ SysSetup device 1
59
+ SysSetup width 640
60
+ SysSetup height 480
61
+ SysSetup bitdepth 32
62
+ SysSetup texbitdepth 16
63
+ SysSetup music 1
64
+ ```
65
+
66
+ **`device` is the display adapter index and is specific to your machine.** Use
67
+ `0` if your primary display is a normal landscape monitor. Use `1` (or higher)
68
+ if your primary display is rotated to portrait, or does not offer a 640×480
69
+ mode. The tool below works this out for you.
70
+
71
+ `music 1` means music **off**. That is the safe setting without the shim
72
+ described under [Music](#music); the game runs, silently.
73
+
74
+ That is the whole fix. Launch `mc.exe`.
75
+
76
+ ---
77
+
78
+ ## Working it out automatically
79
+
80
+ Requires Python 3.12 or newer. Standard library only.
81
+
82
+ ```
83
+ py -m pip install mtrevival # or: pipx install mtrevival
84
+ mtrevival adapters # list adapters, show which support 640x480
85
+ mtrevival check # show what would be written, change nothing
86
+ mtrevival fix # write config.cfg, backing up any existing one
87
+
88
+ mtrevival fix --resolution 1920x1080 # any mode the adapter lists
89
+ mtrevival fix --resolution 1280x720 --windowed # windowed needs patch 1.2
90
+ mtrevival fix --music # restore the soundtrack
91
+ ```
92
+
93
+ `py -m mtrevival …` works too. From a git checkout use `py -m pip install .`;
94
+ the music shim then needs building (see `CONTRIBUTING.md`) or the DLL from a
95
+ [release](https://github.com/NasserAlbusaidi/monopoly-tycoon-revival/releases).
96
+
97
+ Example on a machine with a portrait primary display:
98
+
99
+ ```
100
+ source: D3DEnum.txt
101
+ adapter 0 NVIDIA GeForce RTX 4080 modes=46 no 640x480 [portrait]
102
+ adapter 1 NVIDIA GeForce RTX 4080 modes=54 OK 640x480
103
+ ```
104
+
105
+ `fix` never overwrites an existing `config.cfg` without copying it to a
106
+ timestamped `.bak` first.
107
+
108
+ ### Music
109
+
110
+ The game plays its WMA soundtrack through a DirectShow filter that Windows
111
+ dropped years ago. `fix --music` copies `wmsource-shim.dll` (built from
112
+ [`tools/wmsource-shim`](tools/wmsource-shim), source included) next to
113
+ `mc.exe`, registers it **for your user only** — no elevation, nothing outside
114
+ your profile — and writes `music 0`. The shim hands the game Windows' own
115
+ WM ASF Reader with the two adaptations the game needs. Details, evidence and
116
+ removal in [`docs/music.md`](docs/music.md).
117
+
118
+ Do not run the game "as administrator" with music on: an elevated process
119
+ cannot see per-user COM registrations. `fix --music` warns if that flag is set.
120
+
121
+ ---
122
+
123
+ ## What actually goes wrong
124
+
125
+ Both crashes are the same defect: the game creates an object, never checks
126
+ whether creation succeeded, and then dereferences the null pointer.
127
+
128
+ ### Crash 1 — access violation at `mc.exe+0x000E792E`
129
+
130
+ Windows logs exception `0xC0000005`, fault offset `0x000E792E`, faulting module
131
+ `mc.exe`. The instruction is:
132
+
133
+ ```asm
134
+ 004E78FE call dword ptr [edx+0x3C] ; IDirect3D8::CreateDevice (vtable slot 15)
135
+ 004E7901 mov eax, [ebp+0x44] ; ...continues without checking the HRESULT
136
+ 004E792E mov ecx, [eax] ; <-- faults, EAX = 0
137
+ 004E7932 call dword ptr [ecx+0xA0] ; IDirect3DDevice8::SetViewport (slot 40)
138
+ ```
139
+
140
+ The game requests **640×480 exclusive fullscreen** on adapter 0. Recovered
141
+ `D3DPRESENT_PARAMETERS` from a crash dump:
142
+
143
+ | Field | Value |
144
+ |---|---|
145
+ | BackBufferWidth / Height | 640 × 480 |
146
+ | BackBufferFormat | 22 (`D3DFMT_X8R8G8B8`) |
147
+ | Windowed | 0 — exclusive fullscreen |
148
+ | returned device | **NULL** |
149
+
150
+ For exclusive fullscreen, Direct3D 8 requires the width, height and format to
151
+ match an enumerated display mode. If your primary display is rotated to
152
+ portrait, its mode list contains `480 X 640` and **no** `640 X 480`.
153
+ `CreateDevice` fails, and the unchecked null device is dereferenced by
154
+ `SetViewport`.
155
+
156
+ The game writes its own evidence to `D3DEnum.txt` in the install folder. Look for
157
+ an adapter whose modes are all taller than they are wide — that is the culprit.
158
+
159
+ **Fix:** point `SysSetup device` at an adapter that offers 640×480.
160
+
161
+ ### Crash 2 — access violation at `mc.exe+0x000A801B`
162
+
163
+ ```asm
164
+ 004A8007 call dword ptr [0x96E4E8] ; MultiByteToWideChar -> filename
165
+ 004A800D mov eax, [ebx+0x2CE8] ; media interface -> NULL
166
+ 004A801B mov edx, [eax] ; <-- faults, EAX = 0
167
+ 004A801E call dword ptr [edx+0xC]
168
+ ```
169
+
170
+ The filename recovered from the dump is
171
+ `gamedata\sound\music\music_intro.wma`. The game builds a DirectShow graph to
172
+ play its WMA soundtrack and asks for the 2001 Windows Media DirectShow source
173
+ filter by CLSID. Windows 11 keeps only a 5 KB stub of `dxmasf.dll` and no
174
+ longer registers the class, so `CoCreateInstance` fails. `quartz.dll` and
175
+ `devenum.dll` load; the Windows Media source does not. The interface comes
176
+ back null and is used anyway.
177
+
178
+ **Fix:** `SysSetup music 1` suppresses that path, and the game runs without
179
+ its music. `fix --music` restores it: the filter the game asks for,
180
+ `{6B6D0800-…}`, is answered by a shim that wraps Windows' WM ASF Reader,
181
+ creating a fresh reader per track (the reader refuses a second `Load`) and
182
+ mapping the game's `FindPin(L"Stream 1")` onto the reader's `Raw Audio 0`.
183
+ The 2001 Windows Media redistributable on the CD is not touched. See
184
+ [`docs/music.md`](docs/music.md).
185
+
186
+ ---
187
+
188
+ ## Compatibility notes
189
+
190
+ - Tested on Windows 11 Pro (build 26200), NVIDIA RTX 4080, English CD.
191
+ Verified with game version 1.0 and again after applying the official patch
192
+ 1.2 — the same `config.cfg` works for both. Verified on one machine only.
193
+ - Patch 1.2 applies cleanly on Windows 11 when run **elevated**. It changes
194
+ `mc.exe`, three Lua scripts and the string tables, and leaves `config.cfg`
195
+ alone. Both crashes above are still present in the 1.2 executable. Details
196
+ in [`docs/patch-1.2.md`](docs/patch-1.2.md).
197
+ - Resolution is free. `SysSetup width` / `height` accept any mode the chosen
198
+ adapter enumerates; 1920×1080 exclusive fullscreen and 1280×720 windowed
199
+ (`SysSetup Window 1`, patch 1.2 only) were verified running with a sane UI.
200
+ The 1.0 key `windowed` is ignored; 1.2 renamed it.
201
+ - Music was verified on patch 1.2 (intro track by ear, all ten tracks and
202
+ repeated track changes in a DirectShow harness). It needs Windows Media,
203
+ which N editions get from the Media Feature Pack.
204
+ - Patch 1.2 drops an empty `MTS.txt` in the game folder while it runs and
205
+ removes it on exit. If the game is killed, delete that file, or the next
206
+ launch offers Safe Mode and overwrites `config.cfg`.
207
+ - GameSpy multiplayer is dead and out of scope.
208
+
209
+ ## Modding
210
+
211
+ The game is unusually open for its age, and this is the reason the project
212
+ exists. All verified:
213
+
214
+ - **200 plain-text Lua scripts** in `scripts\`, across 56 namespaces —
215
+ `DEFAULT`, `MAPS`, `SCENARIO1`–`SCENARIO20`, `TUTORIAL1`–`TUTORIAL12`,
216
+ `NET0`–`NET8`. Economy tuning lives in `businesssettings.lua`,
217
+ `blocksettings.lua` and `commoditysettings.lua`.
218
+ - **Saves are Lua too.** `profiles\<name>\settings.lua` contains
219
+ `g_ProfileOptions.BoardName = "gb"`, which selects among **19 national
220
+ boards** shipped on the disc (`usa`, `f`, `ger`, `aus`, `swe`, and more).
221
+ - `parameters\*.prm` holds per-decade tuning, plus `aiparameter.prm` and
222
+ `consumer.prm`.
223
+ - The shipped executable still contains debug flags (`AIDEBUG`, `DEBUGTEXT`,
224
+ `SHOWALLCARS`, `CHEATINGGIT`) and remnants of the developers' block and
225
+ building editors.
226
+
227
+ See [`docs/phase-0-findings.md`](docs/phase-0-findings.md) for the full
228
+ investigation, and
229
+ [`docs/superpowers/specs/`](docs/superpowers/specs/) for the project design.
230
+
231
+ ## Licence
232
+
233
+ MIT — see [LICENSE](LICENSE). This covers the tools and documentation in this
234
+ repository only. Monopoly Tycoon itself is the property of its rights holders
235
+ and is not distributed here.
@@ -0,0 +1,14 @@
1
+ mtrevival/__init__.py,sha256=FxsriInuBdOk4p7tfnhG6mw1eqf4rMV2A53KNKjnwZY,75
2
+ mtrevival/__main__.py,sha256=TzvOohn3m7sljHnZ7Ky9sKu2IQ7iZG81YQ7JdgLqxQQ,3611
3
+ mtrevival/d3denum.py,sha256=9DTb-TgyhDpVa1boAiZLMsN1MVKtBRP6j3wN0Tg35aY,3791
4
+ mtrevival/displays.py,sha256=3583xYnyRNsgdv0gE0lSa9vfoA2ODLLYIQxIUugetlc,4153
5
+ mtrevival/fixpack.py,sha256=27au2V1WIEoBe2qwXE7CZU7GELuYgu0H65VfsvMmKi8,13226
6
+ mtrevival/gameconfig.py,sha256=59g6gHwJaC-vcVwMOyKBh_bMZ65YfPrLNrxKbvO9sP4,3738
7
+ mtrevival/music.py,sha256=kJT85-ZORQ_dzteazIkuP6JqUNq4ZJDEfU_jf4aH7so,5694
8
+ mtrevival/bin/wmsource-shim.dll,sha256=kIFOZR7JrDwQ-8W0XeOSJMxMoI7uOyhLgzPIZyxtX2c,137728
9
+ mtrevival-0.3.1.dist-info/licenses/LICENSE,sha256=ijmA-7hcYuMe431-THkzD_Yyv90qJdNAY-1OtRw6T3M,1094
10
+ mtrevival-0.3.1.dist-info/METADATA,sha256=3ILFlQRWeo3QqX6EID8i-QRrTCox33faSWgpgNy6ZQ4,9966
11
+ mtrevival-0.3.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ mtrevival-0.3.1.dist-info/entry_points.txt,sha256=iGJ7lmZzfq-eZI8anMRdIvsQTCHyUHKdVPSC8mPjSDE,54
13
+ mtrevival-0.3.1.dist-info/top_level.txt,sha256=tkWpmmgpaXFN3k38a0F7K8Eu14QquEgd51xAAP7NNcA,10
14
+ mtrevival-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mtrevival = mtrevival.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nasser Albusaidi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mtrevival