41 Commits
Author SHA1 Message Date
a0331e78d2 Compiled Build 5.1.0b_RC1 Files
Deployed game tree copied back from the Windows build machine, containing the
compiled 5.1.0b_RC1 binaries and every asset the deploy produces. 846 files:
21 added, 825 modified.

Freshly built binaries: MW4.exe, Launcher.exe, autoconfig.exe, ctcls.dll,
MissionLang.dll, ScriptStrings.dll, Language.dll and the rest of the runtime DLL
set, carrying everything merged this cycle -- the 16-pilots-plus-cameraship launch
fix, the MFD mode 4 split-display support and its stutter fix, -tmon, -tcoop, -fps,
-tbaud, the Load File console lobby feature, configurable Rookie Mission, and the
English Language.dll build.

New in the deployment:
- set-appcompat.bat / set-appcompat.ps1 -- the one-click AppCompat shim installer,
  now shipped by deploy-mw4.ps1 so any copy of an install can repair its own
  registration (the layer is keyed on the exe's full path).
- libmysql.dll -- required by mw4print 2.0's MySQL export, late-bound at runtime.
- Four hsh art files restored from 5.0.7D: MFD/assassin2.bmp, Mechs/battlemaster
  iic.bmp, Mechs/behemoth ii.bmp, Mechs/mad cat mkii.bmp.
- banner.txt, dbstruct.txt -- mw4print banner text and the exported DB structure
  reference.

dgVoodoo2 is included as MW4/dgvoodoo2_files/ -- deliberately in a subfolder and
NOT alongside the executable. Nothing loads from there, so the files are available
for a pod owner to install on Windows 10/11 without being active by default. This
keeps XP pods safe: they use native DirectDraw, and having dgVoodoo2 loose in the
game folder would break them. Do not move these files up a level in the repo.

Runtime leftovers from testing on the build machine are included as well
(DDrawCompat logs, DebugLog.txt, mw4-help.txt, two screenshots), consistent with
this repo's stated purpose as a full disaster-recovery mirror in which nothing is
excluded on purpose.

All binaries and art are stored via Git LFS per .gitattributes; verified that
MW4.exe, libmysql.dll, the dgVoodoo2 DLLs and the screenshots staged as LFS
pointers rather than raw blobs.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 19:52:43 -05:00
1b931d5fb8 Correct dgVoodoo2 guidance: never ship it, XP pods must not have it
Two corrections from the project owner.

1. dgVoodoo2 must NEVER be added to the repo or the deploy script.

The pod fleet is mixed. XP pods use native DirectDraw and need nothing extra --
installing dgVoodoo2 on them BREAKS them. Only Windows 10/11 pods need it, and
only for the MFD modes. It is therefore an installed-per-machine, OS-dependent
prerequisite that the pod owner sets up, not a build artifact.

This reverses the recommendation added in the previous commit, which suggested
versioning the working dgVoodoo.conf and having deploy-mw4.ps1 place it. That
advice was wrong: it would have pushed a Win10-only dependency onto every XP
machine. dgVoodoo.conf and dgVoodooCpl.exe were removed in 0ceba9c7 deliberately
and must stay out.

- CLAUDE.md STEP 10: the "TODO: commit it" note is replaced with the reasoning,
  flagged so nobody re-adds the files later thinking it was an oversight.
- CLAUDE.md Next steps: that TODO is removed. The borderless-windowed item now
  notes it is the only route that removes the dgVoodoo2 prerequisite WITHOUT
  breaking the XP pods, since it needs no external DLL on either OS.
- Release notes gain an explicit warning callout: dgVoodoo2 is not part of the
  build and must not be installed on XP pods, with the advice to configure it per
  machine rather than inside the game folder that gets copied around. Without this
  an owner with a mixed fleet could reasonably have copied a working Win10 install
  onto an XP pod and broken it.

2. Version numbering is intentional, not a mismatch.

V5.1.0b<n> denotes beta build n; these are handed out as release candidates for
testing. The "b" is dropped and it becomes 5.1.0 when the release is final. So the
console string V5.1.0b1 alongside a release named V5.1.0b_RC1 is expected, and the
earlier suggestion to reconcile them is withdrawn.

Both release-notes formats regenerated; the XP warning renders as a warning
callout in the HTML. Both remain plain ASCII with CRLF.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 19:47:26 -05:00
10dab8f2d5 Add V5.1.0b_RC1 end-user release notes (markdown + HTML)
Release notes covering the 58 commits since dbcf4052 (-tbaud), written for pod
owners rather than developers: what changed, what the defaults are if they touch
nothing, and how to switch on anything new.

Two formats. The HTML is generated from the markdown so the two cannot drift, and
carries the same inline styling as BTFrstrm/autoconfig-file-spec.html -- no external
files, opens in any browser, prints cleanly. Both are plain ASCII with CRLF so they
open correctly in Notepad on a pod.

Structure leads with the two things that will otherwise generate support traffic:
running set-appcompat.bat after install (the AppCompat layer is keyed on the install
path, and its absence produces the misleading "Another application is preventing use
of full screen mode"), and the dgVoodoo2 scaling mode, which fails silently when set
wrong. Every command line switch is listed with an explicit "if you leave it off"
column.

Facts checked against the source while writing, and corrected:

- [RookieMission] key names. The first draft documented RookieMission=, RookieGameType=
  and so on -- the internal variable names. The game actually reads MissionName,
  GameType, TimeLimit, Visibility, Weather, TimeOfDay, Radar, HeatOn, FriendlyFire,
  SplashDamage, UnlimitedAmmo, WeaponJam, AdvanceMode, ArmorMode. Anyone following the
  draft would have edited options.ini and seen no effect with nothing to explain why.
  The section now carries the full annotated block verbatim from options.ini, and all
  14 documented keys are machine-verified against MW4Shell.cpp.

- [automaticmode] needs TWO keys, not one. automaticmode=1 shows the Load File button;
  automaticfile=<path> tells it what to load. Without the second, CTCL_LoadAutoFile
  returns immediately -- the button appears and silently does nothing. Both are now
  documented in a table, with the path rules (bare filename resolves next to MW4.exe,
  or give a full path).

- dgVoodoo2 is required on Windows 10/11 for EVERY MFD mode, including the original
  spanned display (-tmfds 1 and 3), not only the new split mode (-tmfds 4). The first
  draft implied it was specific to the new feature, which would have led owners running
  the span to skip it.

- The mw4print MySQL export is flagged as included but not yet tested against a live
  server. Everything else in the notes is confirmed working by the project owner.
  libmysql.dll ships in the deployment (Gameleap/mw4, kept by the deploy's root *.dll
  rule and not on the skip list), so there is nothing for owners to download.

Also repaired 23 em dashes that had been written as literal '?' characters, including
in headings -- a side effect of the workspace saving new files as CP949, which cannot
represent them. Both files are now pure ASCII.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 19:22:54 -05:00
2f4ed244ea Fix -fps logger measuring its own overhead; buffer output, fix percentiles
First real run of -fps (222 seconds, 4-monitor bench, in-mission) showed a
rock-solid 60.0 fps average but a worst frame of ~24ms in 82% of seconds, with a
median of 24.4ms and a minimum of 23.2ms. That regularity was the tell: 60Hz vsync
intervals are 16.7 / 33.3 / 50.0 ms, so 24ms is not a multiple of anything and had
no business recurring once per second with that consistency.

Cause: the logger was measuring itself. The per-second line was written straight to
the file with WriteFile on the render thread, and the frame duration is recorded
BEFORE the line is emitted -- so the write's cost landed in the *next* frame's
measurement, which belongs to the next second's bucket. Result: exactly one
inflated frame per second, indefinitely. Holding the handle open (as the first
version did) was not enough; a single WriteFile is still several milliseconds when
something like antivirus is in the path.

Three fixes:

1. Buffered output. Lines accumulate in a 128 KB static buffer (~2000 rows, about
   33 minutes at one row per second) and are flushed only when the buffer fills or
   at exit via atexit(). A normal measurement session now performs no file writes
   at all while running. Trade-off, deliberately accepted: a hard crash loses the
   un-flushed tail. gos-displays.txt remains the crash-survivable log; gos-fps.txt
   is a measurement instrument and must not perturb what it measures.

2. Meaningful percentiles. A "1% low" over 60 samples per second computes
   nFrames/100 = 0, which was clamped to 1 -- so the column was literally
   1000/worst_frame, i.e. the worst-frame column restated in different units.
   Verified against the log: worst 24.1ms -> 41.5 fps, exactly the "1% low" printed.
   Per-second is now a 5% low (worst 3 of 60), which is a number that actually
   differs from the worst frame. True whole-session 1% and 0.1% lows are computed
   at exit from a 0.5ms-bucket frame time histogram (2000 buckets, 0-1000ms) and
   printed in a new session summary along with total frames, elapsed time, average,
   and total hitches. The histogram gives exact-enough percentiles over an entire
   session without retaining every frame time.

3. Carry-over. A single frame longer than one second (a level load: the log shows
   4338ms and 4799ms frames) left the accumulator above the 1000ms threshold, so
   the next few frames each emitted their own bogus one-frame row -- visible in the
   log as rows reporting "1 frame, 1339 fps". The excess is now discarded.

Also adds #include <stdlib.h> to WinMain.cpp for atexit(); it was not reachable
through pch.hpp. GOS_FpsAtExit is declared __cdecl: GameOS builds with /Gz, which
makes __stdcall the default calling convention, but atexit() takes a __cdecl
callback -- without it VC6 rejects the call with C2664.

What the run did establish, and still stands: 60 frames per second sustained for
~150 seconds with no rhythmic multi-hitch pattern, so the mode 4 split-MFD stutter
fix (0a657b59) is holding. Genuine dropped frames (>40ms) occurred in 18% of
seconds, including clusters of 80-90ms; those are real and unaffected by this fix.
Re-measure after this change to see the true baseline.

CLAUDE.md STEP 10 updated: the previous claim that holding the handle open kept the
logger from perturbing the measurement was wrong, and is replaced with what was
actually observed.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 18:43:42 -05:00
9c67dddd83 Display diagnostics + -fps/-tcoop; native multi-monitor MFD ruled out
Four-monitor MFD bring-up on the new bench (MR_new: AMD FirePro W4100, 4 outputs,
Win10). Adds permanent display diagnostics, a Release-capable frame pacing logger,
a self-locating AppCompat shim installer, and settles -- empirically -- whether
dgVoodoo2 can be dropped for the multi-monitor MFD modes. It cannot.

WHY THIS WAS HARD
-----------------
CHSH_Device::InitFirst/InitSecond discarded EVERY HRESULT (SetCooperativeLevel,
SetDisplayMode, CreateSurface, GetAttachedSurface, QueryInterface, CreateDevice)
and returned true unconditionally. A panel that failed to open produced no error,
no crash and no log entry -- the monitor just stayed on the desktop. SPEW is
compiled out of shipping builds, so none of it was visible. Restoring that
visibility is what unblocked everything else.

DIAGNOSTICS ADDED (keep these)
------------------------------
* VideoCard.cpp -- LogDisplayDevices() writes gos-displays.txt next to the exe:
  NumDevices/NumHWDevices/NumMonitors, every DirectDraw device with its
  hw_rasterization flag, the role assignment (FullScreenDevice / g_nNonDualHead /
  g_nDualHead / g_nDualHead2 / g_nMFD1 / g_nMFD2), per-slot -tmon APPLIED/REJECTED
  (previously silent), and whether mode 4's "BOTH mfd1 and mfd2" requirement is met.
* render.cpp -- HSH_LogInit()/HSH_CheckHR()/HSH_HRName() log every DirectDraw call
  in the panel init path with its HRESULT decoded by name (27 DDERR_* codes, all
  verified present and collision-free against build-env/dx7asdk/include/ddraw.h).
  Each line is opened/appended/closed individually so the log survives a crash.
* WinMain.cpp -- GOS_LogFrameRate() writes gos-fps.txt: per second, frame count,
  average fps, 1% low (mean of the worst 1% of frames), worst frame in ms, and a
  count of frames exceeding 2x average ("hitches"). Average fps alone cannot
  distinguish 60fps from 60fps-with-a-dropped-frame-every-second; the 1% low can.
  The engine's own FrameRate readout is #ifdef LAB_ONLY (MWMission.cpp) so it only
  exists in MW4pro.exe; this works in Release, which is what runs on the pods.

NEW SWITCHES (both documented in -help)
---------------------------------------
* -fps    Enable the frame pacing report. Off by default: the gate is the first
          statement in GOS_LogFrameRate, so an unflagged run does no arithmetic
          and does not even create the file. The file handle is held open for the
          process lifetime -- opening/closing it every second would put a syscall
          of unpredictable latency on the render thread, i.e. the measurement tool
          perturbing what it measures.
* -tcoop <0-5>  Selects the SetCooperativeLevel form used by the radar/MFD panels.
          0 = legacy (unchanged shipped behaviour, remains the default).
          Exists so every candidate fix could be compared on real hardware without
          a rebuild between attempts.

CRASH-SAFETY FIX
----------------
hsh_initialized was set unconditionally after panel init, so a failed panel left
null surfaces and a null IDirect3DDevice7 behind and the per-frame path called
straight through them. Now:
  - all four InitSecond overrides (CMR/CRadar/CMFD/CMFDRight) bail on base failure,
  - CMFD_Device::InitFirst reports its real result instead of always returning true,
  - hsh_initialized is only set when the panels genuinely came up.
A display failure now leaves the game running without MFDs instead of bombing to
desktop. Crash signature for the record: `call [ecx+0x44]` with ECX=0 is
IDirectDrawSurface7::GetDC on a never-created surface (vtable offset confirmed
against the DX7 header), reported as "Attempt to read from address 0x00000044".

APPCOMPAT SHIM INSTALLER (new)
------------------------------
build-env/set-appcompat.ps1 + set-appcompat.bat. Self-locating via $PSScriptRoot:
applies DWM8And16BitMitigation to the MW4 executables sitting next to it, wherever
that install lives. HKCU always, HKLM too when elevated (the HKLM value format
differs -- it carries a leading "$" marker -- so the two must not be interchanged).
Verifies by reading back; detects the HIGHDPIAWARE-only entry that SUPPRESSES the
automatic shim; supports -Remove and -WhatIfOnly. deploy-mw4.ps1 now ships both
files into every deployment.

This matters because the layer is keyed on the executable's FULL PATH -- any copy
of an install to another folder or machine silently loses it, and the resulting
error is actively misleading (see below).

WHAT WE LEARNED
---------------
* The AppCompat shim SYNTHESISES 16-bit display modes. Proved directly: the crash
  dump shows "16 bit modes :" EMPTY without it and fully populated with it. MW4
  renders at bitdepth=16 and modern GPUs expose no 16-bit modes at all.
* Without the shim, GameOS reports "Another application is preventing use of full
  screen mode" (GOS_DXRASTERIZER_NOFULLSCREEN, DXRasterizer.cpp ~1125). That is a
  catch-all fired after every SetDisplayMode attempt fails -- it even scans for
  NetMeeting -- and it sends you looking for a conflicting program that does not
  exist. The real cause is the missing shim.
* Exclusive fullscreen DOES work on Win10 with the system ddraw.dll and dgVoodoo2
  physically removed, once the shim is applied to that exe path.
* Windowed mode works natively with no shim at all: the windowed path sets
  Environment.bitDepth = DesktopBpp (32), so there is no mode switch. Verified for
  the console/shell; a full mission windowed is still untested.
* Native DirectDraw enumerates all four W4100 outputs, so dgVoodoo2 was never
  needed for device enumeration.
* The engine is 4:3 ONLY. ImageHlp.cpp ~464 asserts the complete supported set:
  640x480, 512x384, 800x600, 960x720, 1024x768, 1280x1024 (5:4), 1600x1200. No
  16:9 mode and no aspect correction anywhere in the codebase. On a 16:9 monitor
  the scaler must adapt: plain stretch distorts geometry, keep-aspect pillarboxes.
* -2dt is not a recognised switch anywhere in the codebase, despite appearing in
  production ctcl.ini launch lines. Completely inert.
* NumHWDevices (5) can exceed NumDevices (4): it counts D3D device-enumeration
  callbacks, and an adapter exposing both a HAL and a T&L HAL yields two. Benign.

WHAT WE TRIED AND WHY IT FAILED
-------------------------------
The panel cooperative-level call was genuinely wrong -- a latent 2002 bug. Every
panel asked to be BOTH the process focus window AND its own device window, on the
one shared hWindow, after the main device had already taken exclusive mode on it.
The main device (DXRasterizer.cpp ~1027) already uses the correct two-call idiom
(SETFOCUSWINDOW alone, then EXCLUSIVE|FULLSCREEN) -- tagged //sanghoon, the same
author. The panels never were.

Results on real hardware, no dgVoodoo2, shim applied, -tmfds 4:

  -tcoop 0  SETFOCUSWINDOW|CREATEDEVICEWINDOW|ALLOWREBOOT|EXCLUSIVE|FULLSCREEN
            -> DDERR_EXCLUSIVEMODEALREADYSET
  -tcoop 1  CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN (no focus claim)
            -> DDERR_INVALIDPARAMS (CREATEDEVICEWINDOW needs a focus window)
  -tcoop 2  SETFOCUSWINDOW, then CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN
            -> DDERR_INVALIDPARAMS
  -tcoop 3  SETFOCUSWINDOW, then EXCLUSIVE|FULLSCREEN
            -> first panel collides, but that collision STEALS exclusive mode from
               the main display, after which panels 2 and 3 fully initialise (the
               radar reached CreateDevice(HAL) = DD_OK -- a secondary panel running
               entirely on native DirectDraw). Side effect: the desktop was left at
               1920x1080 16bpp. Not viable.
  -tcoop 4  EXCLUSIVE|FULLSCREEN only          -> EXCLUSIVEMODEALREADYSET, all panels
  -tcoop 5  ALLOWREBOOT|EXCLUSIVE|FULLSCREEN   -> EXCLUSIVEMODEALREADYSET, all panels

CONCLUSION: on modern Windows only ONE DirectDraw object per process may hold
exclusive fullscreen. The main display takes it; every secondary panel is refused.
XP allowed multiple. dgVoodoo2 allows it because it is a full reimplementation of
ddraw and is not bound by that rule -- it is not papering over a bug we can fix.

=> dgVoodoo2 CANNOT be removed for -tmfds 1/3/4 by correcting these flags. The
default stays -tcoop 0. The switch is retained because it is how this was settled
and it will re-settle it on different hardware.

The only native path is a borderless windowed panel design (DDSCL_NORMAL + clipper
per monitor, no exclusive mode anywhere). Assessment and staged plan are recorded
in CLAUDE.md STEP 10; not started.

WORKING 4-MONITOR CONFIG (with dgVoodoo2)
-----------------------------------------
dgVoodoo2 Scaling mode MUST be "Stretched, Keep Aspect Ratio". Plain "Stretched"
fails silently: main and radar go fullscreen black, both MFD monitors keep showing
the desktop, and every DirectDraw call still returns DD_OK -- the devices are alive
but dgVoodoo2 never drives those outputs. Diagnosed with a temporary per-panel
colour-flash test (since removed), which also established that device index maps
1:1 to physical monitor on this bench, so -tmon 1,2,3,4 equals auto-detection.
Confirmed working end to end: all three secondary panels present, full mission
played.

KNOWN GAP: the working dgVoodoo.conf is still not versioned in the repo (removed
in 0ceba9c7), so a fresh deploy will reproduce the silent MFD failure.

Behaviour with no new switches supplied is unchanged from the previous build
except on failure paths, which now degrade gracefully instead of crashing.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 18:22:31 -05:00
f402e5becc Add OPTIONS-INI.md reference
options.ini had no documentation. This documents every section and key, compiled
by reading the actual read sites rather than by describing the shipped file, so
that settings which look active but are not are identified as such.

Covers [graphics options], [sound options], [special commands], [server],
[joystick], [Cameraship Params], [Battle Tech Misc], [RookieMission] and
[battle tech print], plus how the file is loaded, which parts the game writes
back, and the per-role options-game/cam/mr variants.

Notable findings, all verified against source:

- The entire [network options] section is DEAD. None of connectiontype,
  connectionspeed, packetsize, defaultconnection, playericon or teamicon is read
  by any code; the connection type actually used comes from the multiplayer
  connection wizard. These are stock MW4 leftovers.

- Several [Battle Tech Misc] keys have names that do not describe what they do,
  and the real meanings are now recorded: RuleBook sets g_nMechVariant, DawnWar
  sets g_nMechLabOp, BiggieSizeIt sets g_nMechPodNum (the console lobby's larger
  roster flag), and CanYouHearTheFootSteps sets g_nBlackMech.

- The shipped options.ini misspells two keys. It contains secmissionreplay and
  secmissionreport, but the code reads SecsMissionReplay and SecsMissionReport.
  The shipped values are therefore ignored and the compiled defaults apply.

- Three keys are read and then immediately overridden, so editing them does
  nothing: videodriverindex (device forced to 0), huddamagemode and
  hudtargetdamagemode (both forced false). Their GetEntry calls are commented out.

- [special commands] killgame is a self-clearing kill switch: if true at startup
  the game rewrites it to false, saves options.ini and exits immediately.

- maxplayers/maxbots are serialised to clients as 5-bit fields, so 31 is the
  maximum usable value; cross-referenced to RAISING-PLAYER-CAP.md.

Also records why a typo produces no diagnostic: GetEntry returns false and the
compiled default is kept silently. Page and key lookup were confirmed to be
case-insensitive (NotationFile::FindPage lowercases; Page::FindNote uses
_stricmp), so the capitalisation differences between the shipped file and the
code are harmless -- the misspellings above are not.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 08:54:28 -05:00
308fe041d4 Make -help a complete command line reference
MW4.exe accepts 76 command line switches. Only 27 of them appeared in the old
-help output, that output went to the debug log rather than to the user, and it
did not stop the game from launching afterwards -- so in practice the switches
were undocumented.

-help now writes a full, categorised reference to mw4-help.txt next to the
executable, opens it in Notepad, and returns from WinMain without starting the
game. If Notepad cannot be launched, a message box reports where the file was
written. The check runs immediately after the command line is lower-cased at the
top of WinMain, before any subsystem is initialised.

A file plus a viewer was chosen over a message box because MW4 is a GUI-subsystem
application with no console, and 76 switches with real descriptions do not fit
legibly in a dialog.

The reference documents every switch actually parsed, grouped as: display and
video, audio and plasma display, pod hardware and arcade (CTCL), zoom and field
of view, multiplayer and network, logging and diagnostics, development and test
builds, and other. Each entry records the accepted value range where the parser
enforces one -- for example -tbaud 9600-921600, -armorlevel 0-4, -tmfds 0-4,
-zmfovb 0.01-0.5, and the -zmtime special case where 0 means instant.

Two switches are listed under "recognised but inactive" so their behaviour is not
misrepresented: -join, whose consuming line is commented out, and -noabzug, which
is only parsed inside a disabled code path. The LAB-only switches are marked as
accepted and ignored in Release builds.

The old partial SPEWALWAYS list in GetGameOSEnvironment was removed so there is a
single maintained reference rather than two that can drift apart. A comment there
points at the new one.

Also adds an explicit #include <stdio.h>; this translation unit previously had no
direct stdio use and relied on transitive inclusion.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 08:54:10 -05:00
0009f868eb RAISING-PLAYER-CAP.md: sharpen the drop-zone wording
The previous correction overstated the case by implying the code reading was
wrong outright. It was not. The engine really does place surplus 'Mechs on
already-occupied spawn points -- two 'Mechs dropped on the same spot -- exactly
as reading the code suggests.

The only wrong part was the predicted consequence. The original draft said those
players "silently fail to spawn". They do spawn; the collision system then pushes
the stacked 'Mechs apart within a second or two, costing some minor contact
damage, and play continues normally.

Reworded Layer 7 and the known-traps entry to separate the two claims: the
spawn-point reuse is real and code-predictable, the failure-to-spawn conclusion
was not.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:50:12 -05:00
33c28cf816 RAISING-PLAYER-CAP.md: correct the drop-zone analysis
The original draft claimed that players beyond a map's available drop zones
"silently fail to spawn", and called per-map drop-zone authoring the true gating
task for raising the player cap. That was inferred from reading the code and is
wrong.

Corrected from real pod testing: when there are more 'Mechs than drop zones, the
surplus 'Mechs spawn on top of each other. They clip and collide briefly, take
some minor damage, then separate and play normally. It resolves itself within
seconds.

So a drop-zone shortfall is a quality-of-experience issue, not a functional
failure. It does not block raising the cap and should not gate the schedule.
Adding start points to busy maps is still worth doing eventually -- overlapping
spawns are untidy and hand out free chip damage -- but it can happen at any point
and never needs to be complete.

Updated accordingly: the TL;DR table, Layer 7, the implementation order (drop
zones moved from step 5 to last and marked optional), the verification checklist,
and the known-traps list.

The biggest remaining non-code task is now the lobby pod-grid and scoreboard
layout rework.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:49:01 -05:00
a41dfb4aa8 Add RAISING-PLAYER-CAP.md reference
Captures the full audit of every player-count limit in the codebase, done while
tracking down the "16 pilots + 1 cameraship" launch failure. Research only --
nothing in it is implemented.

Key finding: the practical ceiling without a wire-format change is 31, not 32.
m_maxPlayers and m_maxBots are serialized as 5-bit fields in
NetMissionParameters, so 32 truncates to 0. This supersedes the "32" figure in
the existing CLAUDE.md plan sections.

Documents, with file references:
- what is NOT a limit (Adept::Maximum_Players is 255, connectionID is a BYTE,
  DirectPlay imposes nothing, and there is no 32-bit player bitmask)
- the 5-bit serialization ceiling and what widening it would cost
- the compiled defaults in CTCL_DefaultHostSetup that actually gate connections
- the CTCL roster arrays, including that g_aPlayerInfos[20] has NO bounds check
  in CTCL_AddPlayer and that ctcl.h is duplicated across ~6 directories
- MAX_LANCEMATES 16 for bots
- the lobby script constants and the pod-grid UI work
- scoreboard/radar/review layout work
- per-map drop zones, which is the real gating task and produces silent spawn
  failures when short
- the O(n^2) replication cost, reframed as verify-don't-assume on modern hardware

Also records the failure signatures to expect, so a future attempt recognises
them quickly: silent launch hang from a count mismatch, silent non-spawn from
missing drop zones, and 5-bit truncation looking like "max players became zero".

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:40:14 -05:00
9ba2d594b1 Add -tmon switch to hard-assign display devices
Display roles are auto-detected in FindVideoCards(): the main view takes the
first hardware-rasterizing device that is not a 1280x480-capable span, the radar
takes the next, and mode 4's two MFD panels take the two after that. That works
until DirectDraw enumerates the adapters in an unexpected order, at which point
the wrong content appears on the wrong monitor with no way to correct it.

Adds an override that keeps auto-detection as the default:

    -tmon <main>,<radar>,<mfd1>,<mfd2>

Values are 1-based, so the normal case is "-tmon 1,2,3,4". A value of 0 leaves
that slot auto-detected, so "-tmon 2,1,0,0" swaps only main and radar. Separators
may be ',' '/' or ':'. mfd1/mfd2 are only meaningful with -tmfds 4. Omitting the
switch entirely preserves existing behaviour exactly.

Implementation:

- Parsed in MW4Application's WinMain alongside -tmfds. That runs before GameOS
  calls FindVideoCards(), so the values are in place for device selection.
- Stored in g_naMonitorOverride[4] (VideoCard.cpp), extern'd in MW4Application.cpp.
- ApplyMonitorOverride() validates the index against NumDevices, so a stale -tmon
  on a machine with fewer monitors falls back to auto-detection rather than
  breaking startup.
- Ordering matters and is deliberate: main and radar are applied BEFORE the mode 4
  MFD search so that search still skips whichever devices the operator picked;
  mfd1/mfd2 are applied after it. Partial overrides therefore compose correctly.

Role map: main = Environment.FullScreenDevice, radar = g_nNonDualHead,
mfd1 = g_nMFD1, mfd2 = g_nMFD2. The mode 1 span (g_nDualHead) is intentionally
not overridable -- it is detected by 1280x480 mode support, which only the span
card advertises, so that detection is reliable.

Also adds a SPEW line logging the final role -> device map and device count.
Note this is only visible in Profile/LAB builds; SPEW compiles out in Release
(gos2X/Gos.h), so in Release the order is determined by observation.

Verified: compiles clean, console launches. Multi-monitor testing pending.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:39:59 -05:00
0a657b5998 Fix constant rhythmic stutter in MFD mode 4 (split dual 640x480)
-tmfds 4 splits the 1280x480 MFD span into two independent 640x480 monitors so
the span hardware is no longer required. It rendered correctly but stuttered
rhythmically and constantly, making the game unplayable. -tmfds 1 on the same
binary was flawless.

Root cause: a cross-DirectDraw-object texture read, twice every 7-frame cycle.

CHSH_Device::InitFirst with no pOtherHSHD peer creates its OWN IDirectDraw7 (via
wDirectDrawCreateEx on that monitor's device GUID) and InitSecond creates its OWN
primary flip chain and IDirect3DDevice7. CMFDRight_Device::InitFirst passes no
peer, so the right MFD is an entirely separate DirectDraw object.

EndChannel then did, for channels 3-4:

    target->pD3DDevice->SetTexture(0, pDDSTarget);

where target->pD3DDevice belonged to the RIGHT device but pDDSTarget was the LEFT
device's render-target texture. The old code comment asserted "both devices are
on the same GPU so VRAM textures are mutually accessible" -- that premise is
wrong. In DirectDraw 7 a surface belongs to the IDirectDraw7 that created it, not
to the physical GPU, so it is not a valid texture on another object's D3D device.

The MFDs still displayed, which means the runtime was emulating the access with a
VRAM -> system-memory readback and re-upload of the 1024x512 16-bit render target.
That forces a full GPU pipeline stall, and it happened on channels 3 and 4 (that
is, sh_step 5 and 6) -- twice per 7-frame cycle, on the same GPU drawing the main
view. Hence a fixed-period hitch in the whole game, forever.

This also explains why the earlier stagger work (eaa5fd3, BeginSceneRight) did not
help: it only moved a Flip from sh_step 0 to 1, and the flips already used
DDFLIP_DONOTWAIT|DDFLIP_NOVSYNC and never blocked. The flips were never the
problem.

Fix: give the right device everything it draws with, and render channels 3-4
entirely on it.

- New HSH_CreateMFDTextures() builds the mech image atlas and the MFD sprite atlas
  on a caller-supplied IDirectDraw7. Both devices now call it, so each owns a
  complete independent texture set. CMFD_Device::InitSecond was refactored onto it.

- CMFDRight_Device gained its own pDDSMechTexture / pDDSDamageTexture /
  pDDSTargetTexture plus a Release() override, and its InitSecond now sets up
  tw/th and the material/render state exactly like the left device.

- New CMFD_Device::SwapRightState() exchanges this object's DATA members with the
  right device's. BeginChannel swaps in when channel >= 3 in mode 4; EndChannel
  swaps back. This routes all existing drawing to the correct monitor without
  touching the ~233 mfd_device.* call sites in hudchat/huddamage/hudweapon/
  GUIRadarManager. The vtable pointer is deliberately never swapped, so virtual
  dispatch is unaffected; CHSHFont has no virtual functions so its array is
  swapped bytewise to avoid ctor/dtor side effects on a temporary.

- EndChannel's composite is now a single path for all modes. Mode 4 composites
  full 640 width at x=0 (each device is a standalone panel); modes 1-3 keep the
  half-width (ch/3)*w packing into one backbuffer.

Side effects: startup builds the 65-bitmap mech atlas twice (once per device), and
VRAM use rises by a few MB. Modes 0-3 are behaviourally unchanged.

Known cosmetic leftover, deliberately not changed: huddamage.cpp lines ~1319 and
~1901 call LoadTargetTexture outside the channel-3 block, so those loads land on
the left device and go unused. The in-channel call at ~2209 runs every frame in
that branch and correctly populates the right device's copy, so behaviour is
correct -- it is just a redundant load on target change.

Requires rebuild: MW4.exe (GameOS changes recompile the engine library).
Verified: compiles clean, console launches. Two-monitor testing pending.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:39:42 -05:00
29aee4f71b Fix 16 pilots + 1 cameraship failing to launch
A full 16-'Mech roster plus a cameraship silently refused to launch: the console
sat in nLaunchState 3 ("loading") forever with no crash and no error. 15 mechs +
camera worked, and 16 mechs with no camera worked.

Two independent bugs, both counting the cameraship against the 16 'Mech slots.

1. Session capacity (broke all-human rosters)

   CTCL_DefaultHostSetup derived the camera reserve from
   CTCL_GetTeslaCountAll() - CTCL_GetTeslaCount(). Those counters read the CTCL
   tesla table, which is only populated when CTCL_IsConsoleOrCOOP() is true --
   i.e. only on the console. But the machine that creates the network session is
   the cameraship pod (CTCL_DoMission sets g_nServer = nCameraship, and that pod
   runs CTCL_DoCreateGame -> CTCL_DefaultHostSetup(0) -> Mech4CreateGame ->
   gos_CreateGame(..., Environment.NetworkMaxPlayers, ...)).

   On that pod both counters return 0, so the reserve collapsed to +0 and the
   session was created with dwMaxPlayers = 16. The 17th connection was refused,
   CTCL_CheckServerReady never saw nCount == g_nTeslas + 1, and the launch hung.

   Fixed by reserving with a constant, MW4_CAMERASHIP_RESERVE (4, matching
   MAX_CAMERAS), which is valid on every machine regardless of the tesla table.

2. Bot admission (broke any roster containing bots)

   MW4Shell::AddBot rejects when (player_count + bot_count) >= m_maxPlayers.
   player_count is the DirectPlay player count, which includes the cameraship
   connection, so with m_maxPlayers = 16 the last bot was silently refused.
   g_nBOTs then never matched the connected lancemates and the same readiness
   check spun forever.

   Fixed by subtracting cameraship participants from player_count under CTCL,
   via a new CTCL_CountCameraShipsInGame() helper (non-bot entries with
   m_nMechIndex == 0). Cameraships hold a network slot but pilot no 'Mech, so
   they must not consume a 'Mech slot.

Also applies the constant reserve in the PLAYER_LIMIT_PARAMETER path, guarded by
!CTCL_IsNone() so a standalone non-pod host keeps its exact configured limit.

This supersedes commit f76dc05f, which had the right formula but evaluated it on
the console rather than on the pod that actually creates the session.

Note for later: m_maxPlayers is serialized in only 5 bits (MWApplication.cpp),
so 31 is the hard ceiling for any future player-cap work. See
RAISING-PLAYER-CAP.md.

Requires rebuild: MW4.exe (Release + Profile). No script or resource changes.
Verified: compiles clean, console launches. Pod testing pending.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:39:16 -05:00
dicion 515adc2413 mw4print: persist banner text via banner.txt
- Add banner.txt load/save helpers in banner settings dialog

- Load banner from banner.txt first, with options.ini BannerText fallback

- Fix options.ini path join to include missing path separator
2026-07-24 21:56:24 -05:00
dicion ff650f7bec Load File: finalize first-click deterministic apply, add NoReturn, and align docs
- Stabilize Load File first-click behavior across mission/map/options/slots.

- Fix map sequencing by rebuilding game-type scenario list before mission lookup.

- Resolve mission-name miss to map index 0 fallback for selected game type.

- Fix decal handling: map INI decal IDs to dropdown indices, clamp invalid indices, display actual decal IDs in UI labels, and avoid redraw-time decal overwrite.

- Fix option apply timing and UI visibility issues, including Weapon Jam refresh.

- Add NoReturn support end-to-end: parse/store in MW4Shell auto globals, expose script variable, and apply to respawn/no-return mission params and UI checkbox.

- Update autoconfig spec to reflect actual parser/default/fallback behavior and add NoReturn examples.
2026-07-24 21:42:54 -05:00
dicion e5561181bb Fix MAIL_LOAD_AUTO_MISSION crash: defer initialize(this) to sender==this self-mail
'gui_objects have no parent' crash at line 907: initialize(this) cannot be
called from sender==@conlobby@ context (external mail). Fix:

1. Replace initialize(this)+mail(-9998,parent) in MAIL_LOAD_AUTO_MISSION
   with mail(MAIL_LOAD_AUTO_MISSION_DONE=-8888, this) -- a self-mail.

2. Add MAIL_LOAD_AUTO_MISSION_DONE handler in sender==this block, which
   safely calls initialize(this)+mail(-9998,parent) from the correct context.

Also repaired a corrupt duplicate MAIL_PREVIOUS_MISSION_PARAMS block
that was left orphaned by an earlier edit (the original body of
MAIL_LOAD_AUTO_MISSION had been inserted there when the handler was
moved from sender==this to sender==@conlobby@).

No rebuild required (script-only).
2026-07-24 11:09:50 -05:00
dicion 06de4534da BTFrstrm: add Load File test ini files (FFA Coliseum + 4-team KotH CentralPark) 2026-07-24 11:01:11 -05:00
dicion 23284b18b0 Fix MAIL_LOAD_AUTO_MISSION: move handler to sender==@conlobby@ block
Handler was inside 'if (sender == this)' but MAIL_LOAD_AUTO_MISSION is sent
from ConLobby (sender != this), so it never fired. All game params (FriendlyFire,
SplashDamage, UnlimitedAmmo, WeaponJam, AdvanceMode, ArmorMode, etc.) were
silently ignored on every Load File click.

Fix: move the ~70-line handler to the 'if (sender == @conlobby@)' block,
alongside MAIL_SET_ROOKIE_MISSION and MAIL_PREVIOUS_MISSION — where all
ConLobby-originated mails are handled. No rebuild required (script only).
2026-07-24 10:56:57 -05:00
dicion 62dc0952b1 BTFrstrm: add Load File test configs (FFA Coliseum, Team KoH CentralPark) 2026-07-24 10:46:24 -05:00
dicion 9a9f2d0df2 autoconfig-file-spec.html: add TeamAllowed/TeamCount to example; fix caveat #3 2026-07-24 10:37:37 -05:00
dicion cd2fe73c88 Fix Load File: separate Auto globals; fix team/FFA double-press
Issue 1 — double press to fix team/FFA display:
  cur_team_val is updated by ConLobbyMission's -9998 signal which was async.
  Fix: call SetNetworkMissionParamater(team_allowed, ...) directly in ConLobby
  before the slot loop, then re-read cur_team_val via CTCL_GetTeamParams.
  This is synchronous so the slot display is correct on the first click.
  New file keys: TeamAllowed=0/1, TeamCount=2 (default 2 teams).

Issue 2 — Default button broken after Load File click:
  CTCL_LoadAutoFile was overwriting g_nRookieGameType/g_szRookieMission etc.
  so the Default button loaded the last auto-file params instead of defaults.
  Fix: 14 new dedicated g_nAutoXxx/g_szAutoMission globals. CTCL_LoadAutoFile
  populates ONLY these. Rookie Mission globals are never touched.
  New MAIL_LOAD_AUTO_MISSION (-6666) sent to ConLobbyMission applies the Auto
  globals (mirrors MAIL_SET_ROOKIE_MISSION_PARAMS but uses g_nAutoXxx).
  ConLobby reads game type/mission from Auto globals directly into the
  ConLobbyMission dropdowns (@ConLobbyMission@o_game_options[N].nselected).

Files changed:
  MW4Shell.cpp: 16 new globals, StartUp/ShutDown registration, CTCL_LoadAutoFile
  ConLobby.script: MAIL_LOAD_AUTO_MISSION define, updated handler
  ConLobbyMission.script: MAIL_LOAD_AUTO_MISSION define + handler
  autoconfig-file-spec.html: document TeamAllowed + TeamCount fields
Rebuild required: MW4.exe (Release + Profile).
2026-07-24 10:35:11 -05:00
dicion 3f2d79a038 BTFrstrm: add weapon location spreadsheets (Special1/2 and rear-facing) 2026-07-24 10:18:06 -05:00
dicion c3d82d78c9 Load File: hide button when automaticmode!=1; fix team/skin double-click
1. Expose g_bAutomaticMode as gosScript variable so ConLobby can check
   it at init time. o_load_file.state is set to 3 (disabled/hidden) if
   automaticmode != 1 in options.ini. Button is fully visible and active
   only when the feature is intentionally enabled.
   Rebuild required: MW4.exe (Release + Profile).

2. Remove cur_team_val conditional for team/skin slot assignment.
   Previously, only one of o_team[k] or o_skins[k] was set depending on
   cur_team_val at click time, but MAIL_SET_ROOKIE_MISSION propagates the
   new game type asynchronously -- cur_team_val would not reflect the
   file's GameType until the next frame, requiring a second click.
   Fix: always set both o_team[k] and o_skins[k] unconditionally. The
   mission launch code uses whichever is relevant for the active mode;
   the other is harmlessly ignored. No rebuild (script-only).
2026-07-24 10:08:21 -05:00
dicion 17ca966473 Fix Load File mech lookup: use stock_array to get chassis name
mech[j] in the flat sorted array hits variant entries (e.g. 'Assassin2 A'
sorts before 'AssassinII' alphabetically, pushing all subsequent chassis
indices off by 1 or more). The script's stock_array[] maps each chassis
index -> its actual position in the flat mech[] array, bypassing variant
entries.

Fix: mech[allowed_mechs[j]] -> mech[stock_array[allowed_mechs[j]]]

Only stock (chassis) names are supported in Mech= field. Operators can
adjust variants manually after Load File is clicked. No rebuild required
(script-only change).
2026-07-24 10:02:13 -05:00
dicion 7852269dc7 Fix Load File crash: VALUEPARM for literal field arg in CTCL_GetAutoSlotInt
Script calls callback(CTCL_GetAutoSlotInt, k, 0/1/2/3) where 0-3 are
integer literals. The script engine passes literals as (void*)N directly
(not as pointers), so INTPARM(1) = *((int*)data[1]) dereferences NULL
when field=0, producing the 'Attempt to read from NULL' STOP.

Fix: VALUEPARM(1) = (int)data[1] reads the value without dereferencing.
k (data[0]) remains INTPARM because it is a script variable (passed as
a pointer to the variable's storage, not a literal).

Also add exists(@ConLobbyMission@) guard before MAIL_SET_ROOKIE_MISSION
for defensive safety if the sub-script is not running.

Rebuild required: MW4.exe (Release + Profile).
2026-07-24 09:31:51 -05:00
dicion 76121f1c68 BTFrstrm: add autoconfig-file-spec.html (Load File format reference) 2026-07-24 09:13:37 -05:00
dicion c33465611f Add [automaticmode] Load File button to console lobby
options.ini [automaticmode] section:
  automaticmode=1
  automaticfile=c:\path\to\config.ini

Right-click was considered then dropped in favor of a dedicated button
at 467,510 (below Pick Cond., left of Reprint).

C++ (MW4Shell.cpp):
- SAutoFileSlot struct + g_aAutoSlots[16], g_bAutomaticMode, g_szAutomaticFile globals
- [automaticmode] ini read at StartUp
- CTCL_LoadAutoFile: checks file exists, reads [mission] page into existing
  g_nRookieXxx globals + [slot0]..[slot15] pages into g_aAutoSlots[]; returns 1 if loaded
- CTCL_GetAutoSlotName(out_str, k): pilot name for slot k
- CTCL_GetAutoSlotMech(out_str, k): mech display name for slot k
- CTCL_GetAutoSlotInt(k, field): Type/Team/Skin/Decal for slot k (fields 0-3)
- Register/unregister all 4 callbacks in StartUp/ShutDown

Script (ConLobby.script):
- o_load_file button at 467, 510
- Handler: CTCL_LoadAutoFile -> if loaded, sends MAIL_SET_ROOKIE_MISSION to
  ConLobbyMission (game options), clears all slots, then applies per-slot data
  in a loop (pilot names, mech by display-name lookup in allowed_mechs[], team,
  skin, decal). USE_ALLOWED_MECHS/non-ALLOWED_MECHS both handled via #if.

File not consumed (stays on disk); external app overwrites for next load.
Rebuild required: MW4.exe (Release + Profile).
2026-07-24 08:44:57 -05:00
dicion fccdc2dee4 CLAUDE.md: document 5813aeb6 through 0344418a work (2026-07-23)
- hsh/ BMP canonical renames (MFD + Mechs, commit 5813aeb6)
- RookieMission configurable defaults via options.ini (MW4Shell.cpp + ConLobbyMission.script, 5813aeb6)
- Mechlab turn rate label correction (StringResource.rc, 5813aeb6)
- BTFrstrm design docs: MechDependencyTree.docx + Special_Zones.docx (840bc96c)
- mech_loadouts.md: MechEditor data sources, field conversions, hsh naming reference (0344418a)
2026-07-23 22:18:43 -05:00
dicion 0344418aae mech_loadouts.md: document MechEditor data sources, conversions, hsh naming (2026-07-23) 2026-07-23 21:56:14 -05:00
dicion 520a860414 Update testing checklist through 2026-07-23 (RookieMission, turnrate label, hsh renames) 2026-07-23 21:47:08 -05:00
dicion 5813aeb6e9 Fix missing asset files with ones from 5.0.7D 2026-07-23 21:36:27 -05:00
dicion 840bc96cc1 Documentation! 2026-07-23 16:16:45 -05:00
dicion 72e1e59d8e CLAUDE.md: document eaa5fd3 through 5.1.0b-in-progress work
- MFD mode 4 right-device stagger fix (eaa5fd3): cycle diagram, files touched
- Linux→Windows rsync workflow: sync-to-windows.sh (55b9bfc5)
- ddraw.dll removed from repo; build-resources.ps1 moves it aside (0ceba9c7, 24825ff3)
- ConLobby V5.1.0b1 / Super6 6-mech rotation from Highlight (c768f7c4)
- CRIOMAIN.CPP Korean translation + RIO poll timeout scaling; CRLF hazard note;
  min/max undeclared in VC6 in this TU (16fca6c4, a712002f)
- 16 pilots + 1 cameraship: NetworkMaxPlayers formula, fall-through break warning (f76dc05f)
2026-07-19 19:55:46 -05:00
dicion b591bae273 add checklist of changes for testing! 2026-07-19 19:18:09 -05:00
dicion a712002fec CRIOMAIN.CPP: fix min/max undeclared identifier (VC6)
min() and max() are not in scope in this translation unit under VC6.
Replace with explicit ternary clamping expression; no new headers needed.
2026-07-19 19:00:08 -05:00
dicion f76dc05f46 Support 16 pilots + 1 cameraship in multiplayer
MW4Shell.cpp:
- CTCL_DefaultHostSetup (non-coop): replaced hardcoded
  Environment.NetworkMaxPlayers=16 with
  params->m_maxPlayers + (CTCL_GetTeslaCountAll() - CTCL_GetTeslaCount())
  so DirectPlay reserves one extra slot per installed cameraship.
  CTCL_GetTeslaCountAll() - CTCL_GetTeslaCount() = camera-only seat count.
- SetNetworkMissionParamater / PLAYER_LIMIT_PARAMETER: applied the same
  camera-slot formula when the host changes the player limit at runtime.
  Also restored the gos_NetServerCommands(gos_Commend_UpdateMaxPlayers)
  call (was accidentally dropped) and the missing break that caused
  fall-through into JOIN_IN_PROGRESS_PARAMETER.
- COOP branch: no change (capped at 9+bots; camera seats not needed there).

ConLobby.script:
- Raised the launch-guard cap from nTempPlayerCount > 16 to > 17,
  allowing the 17th connection (the cameraship) to not trigger the
  'Too many player/bots' error.
2026-07-19 18:54:38 -05:00
dicion 16fca6c4f1 CRIOMAIN.CPP: translate Korean comments to English, clean UTF-8
Translated all EUC-KR/CP949 Korean developer comments (~35 lines) to
English. File re-saved as UTF-8 (was CP949/EUC-KR on disk).

Also included: g_dwRIOPollTimeout formula that scales WaitForMultipleObjects
timeout by baud rate: clamp(ceil(480000/baud), 5, 50) ms.

Key translations:
- Developer markers (sanghoon/hyun)
- Packet receive loop comments
- ACK/NAK handling comments
- Thread/COM init comments
- Button table comments
- Joystick pedal/throttle diagram comments

Decorative diamond markers (◆) stripped from case labels and section
dividers. Unicode arrows (← →) in diagram comments replaced with ASCII.
2026-07-19 18:52:30 -05:00
c768f7c46b Bump console version to V5.1.0b1; merge Highlight Super6 changes
Version bumped to V5.1.0b1 to align with the 5.1.0b-in-progress branch name and
the goal of producing a tested release candidate.

Incorporated manually-tested script changes from Buddy 'Highlight' Taylor of
MechCorps (MCHL), who expanded the default mech pool from Fab4 to Super6:
- ROOKIEMECH defines extended to 6 entries (added Archer ID=1, Warhammer ID=62)
- 16-slot default mech assignments updated to cycle through all 6 Super6 mechs
- Right-click mech randomizer expanded from random(0,3) to random(0,5) to
  include Archer and Warhammer in the pool

Changelog entries for Cyd (06/24/26), MCHL (06/27/26), and RT (07/19/26)
added to the script header.

Co-authored-by: Claude Sonnet 4.6 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-19 17:19:08 -05:00
dicion 24825ff396 more ddraw handling fixes for profiler. ugh. 2026-07-19 16:41:45 -05:00
dicion 0ceba9c778 ddraw.dll breaking mw4pro windowed mode 2026-07-19 16:28:51 -05:00
55b9bfc521 Add sync-to-windows.sh: rsync script for Linux→Windows build machine
Syncs all source, content, toolchain, and assets needed to build and test the game on the Windows machine at /vwe/firestorm. Excludes generated build outputs (bin dirs, *.mw4, *.dep), .git/LFS, _UNUSED, and the MW4 deploy dir.

Co-authored-by: Claude Sonnet 4.6 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-19 15:40:24 -05:00
1055 changed files with 7399 additions and 1037 deletions
Binary file not shown.
Binary file not shown.
+585
View File
@@ -0,0 +1,585 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Automatic Config File Specification ? Firestorm Console Load File</title>
<style>
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; margin: 60px 72px; color: #1a1a1a; max-width: 900px; }
h1 { font-size: 20pt; color: #1F3864; border-bottom: 3px solid #1F3864; padding-bottom: 8px; margin-top: 0; }
h2 { font-size: 14pt; color: #1F3864; border-bottom: 1px solid #BDD7EE; padding-bottom: 4px; margin-top: 28px; }
h3 { font-size: 12pt; color: #2E4D7B; margin-top: 20px; margin-bottom: 4px; }
h4 { font-size: 11pt; color: #2E4D7B; margin-top: 16px; margin-bottom: 2px; font-style: italic; }
.meta { color: #555; font-size: 10pt; margin-bottom: 28px; }
code, .code { font-family: Consolas, "Courier New", monospace; font-size: 10pt; background: #F4F7FB; padding: 1px 4px; border-radius: 2px; color: #1a1a1a; }
pre { font-family: Consolas, "Courier New", monospace; font-size: 10pt; background: #F4F7FB; border: 1px solid #BDD7EE; border-left: 4px solid #2E4D7B; padding: 12px 16px; border-radius: 0 4px 4px 0; overflow-x: auto; line-height: 1.5; }
pre .section { color: #7B2D8B; font-weight: bold; }
pre .key { color: #1F6A8F; }
pre .value { color: #2D6A2D; }
pre .comment { color: #888; font-style: italic; }
table { border-collapse: collapse; width: 100%; margin: 10px 0 18px 0; font-size: 10.5pt; }
th { background: #1F3864; color: white; padding: 7px 12px; text-align: left; font-weight: bold; font-size: 10pt; }
tr:nth-child(even) td { background: #EFF4FB; }
td { padding: 5px 12px; border-bottom: 1px solid #D0D9E8; vertical-align: top; }
td code { background: #E8EFF9; }
.warn { background: #FFF3CD; border: 1px solid #FFEAA0; border-left: 4px solid #F0B429; padding: 10px 14px; border-radius: 0 4px 4px 0; margin: 14px 0; font-size: 10.5pt; }
.warn strong { color: #7A4F00; }
.note { background: #E7F3FD; border: 1px solid #B8D9F5; border-left: 4px solid #1F3864; padding: 10px 14px; border-radius: 0 4px 4px 0; margin: 14px 0; font-size: 10.5pt; }
.optional { color: #666; font-size: 9.5pt; font-style: italic; }
ul, ol { margin: 6px 0 10px 0; padding-left: 24px; }
li { margin-bottom: 4px; }
hr { border: none; border-top: 1px solid #D0D9E8; margin: 30px 0; }
.key-block { background: #F0F4FA; border: 1px solid #C5D5E8; padding: 8px 12px; border-radius: 4px; margin: 8px 0 12px 0; }
.key-block .kname { font-family: Consolas, monospace; font-size: 10.5pt; font-weight: bold; color: #1F6A8F; }
.key-block .ktype { color: #888; font-size: 9.5pt; margin-left: 8px; }
.default-tag { background: #D4EDDA; color: #155724; font-size: 9pt; padding: 1px 6px; border-radius: 3px; margin-left: 6px; font-weight: bold; }
.ignored-tag { background: #F8D7DA; color: #721C24; font-size: 9pt; padding: 1px 6px; border-radius: 3px; margin-left: 6px; }
.mech-table td:first-child { font-family: Consolas, monospace; font-size: 10pt; white-space: nowrap; }
@media print { body { margin: 40px; } h2 { page-break-before: auto; } }
</style>
</head>
<body>
<h1>Automatic Config File Format Specification</h1>
<div class="meta">
Feature: Console Lobby &ldquo;Load File&rdquo; button &nbsp;|&nbsp;
Commit: <code>c3346561</code> (branch <code>5.1.0b-in-progress</code>) &nbsp;|&nbsp;
Date: 2026-07-24, updated 2026-07-24 (commit <code>cd2fe73c</code>)
</div>
<h2>1. Overview</h2>
<p>
The system operates with <strong>two separate files</strong>:
</p>
<ol>
<li>
<strong><code>options.ini</code></strong> &mdash; machine-level one-time configuration, read once at game startup.
Contains the path to the config file. Never changed by the game.
</li>
<li>
<strong>The config file</strong> (path specified in <code>options.ini</code>) &mdash;
written by the external application before each match.
Read by the game when the operator clicks <strong>Load File</strong> in the console lobby.
<strong>Not consumed</strong> &mdash; the game leaves it on disk. Overwrite it to set the next match.
</li>
</ol>
<p>
When <strong>Load File</strong> is clicked, the game:
</p>
<ol>
<li>Checks that <code>automaticmode=1</code> and the file path is configured.</li>
<li>Checks that the config file exists on disk (<code>GetFileAttributes</code>). If not, does nothing.</li>
<li>Reads the file, applies game options, clears all current lobby slots, then fills slots from the file.</li>
<li>Leaves the file on disk unchanged.</li>
</ol>
<hr>
<h2>2. <code>options.ini</code> Setup (One-Time, Per Machine)</h2>
<p>Add the following section to the game&rsquo;s <code>options.ini</code> in the game working directory.</p>
<pre><span class="section">[automaticmode]</span>
<span class="key">automaticmode</span>=<span class="value">1</span>
<span class="key">automaticfile</span>=<span class="value">c:\mw4files\nextmatch.ini</span></pre>
<table>
<tr><th>Key</th><th>Type</th><th>Description</th></tr>
<tr><td><code>automaticmode</code></td><td>integer</td><td><code>1</code> = enabled. <code>0</code> = disabled; Load File button does nothing.</td></tr>
<tr><td><code>automaticfile</code></td><td>string</td><td>Full absolute Windows path to the config file. Max 259 characters. Can be any local drive letter.</td></tr>
</table>
<div class="note">
<strong>Read at startup only.</strong> Changing <code>options.ini</code> requires a game restart.
If the <code>[automaticmode]</code> section is absent, or <code>automaticmode=0</code>, or
<code>automaticfile</code> is empty, Load File always does nothing regardless of what config file exists.
</div>
<hr>
<h2>3. Config File Format Rules</h2>
<p>The config file uses <strong>standard Windows INI format</strong>, parsed by <code>NotationFile::Standard</code>.</p>
<ul>
<li><strong>Encoding:</strong> ANSI / ASCII only. No Unicode, no UTF-8 BOM.</li>
<li><strong>Line endings:</strong> CRLF or LF &mdash; both accepted.</li>
<li><strong>Section headers:</strong> <code>[sectionname]</code> &mdash; <strong>case-sensitive, must be all lowercase exactly as shown.</strong></li>
<li><strong>Key names:</strong> Case-sensitive. Must match exactly as documented below.</li>
<li><strong>String values:</strong> Everything after <code>=</code> to end of line. Do <strong>not</strong> wrap in quotes. Example: <code>MissionName=ScarabStronghold - Attrition</code></li>
<li><strong>Integer values:</strong> Raw decimal integer only. No quotes, no sign unless negative. Example: <code>GameType=2</code></li>
<li><strong>Do not use comments</strong> (<code>//</code> or <code>;</code>). Parser behavior is not guaranteed. Keep the file clean.</li>
<li><strong>Missing key:</strong> If a key is absent, parser defaults are used. For <code>[mission]</code> this means built-in auto defaults (not current UI values). For <code>[slotX]</code> fields, omitted numeric values default to <code>0</code> and omitted strings default to empty.</li>
<li><strong>Missing section:</strong> If <code>[mission]</code> is absent, Load File still applies mission defaults. If <code>[slot0]</code> is absent, all slots are cleared and remain empty.</li>
</ul>
<hr>
<h2>4. Section: <code>[mission]</code></h2>
<p>Sets all game lobby parameters. This section is <span class="optional">optional</span>, but if omitted, Load File applies built-in mission defaults (it does not preserve current UI values).</p>
<pre><span class="section">[mission]</span>
<span class="key">MissionName</span>=<span class="value">ScarabStronghold - Attrition</span>
<span class="key">GameType</span>=<span class="value">2</span>
<span class="key">TeamAllowed</span>=<span class="value">0</span>
<span class="key">Visibility</span>=<span class="value">0</span>
<span class="key">Weather</span>=<span class="value">0</span>
<span class="key">TimeOfDay</span>=<span class="value">0</span>
<span class="key">TimeLimit</span>=<span class="value">7</span>
<span class="key">Radar</span>=<span class="value">0</span>
<span class="key">HeatOn</span>=<span class="value">0</span>
<span class="key">FriendlyFire</span>=<span class="value">0</span>
<span class="key">SplashDamage</span>=<span class="value">0</span>
<span class="key">UnlimitedAmmo</span>=<span class="value">1</span>
<span class="key">NoReturn</span>=<span class="value">0</span>
<span class="key">WeaponJam</span>=<span class="value">0</span>
<span class="key">AdvanceMode</span>=<span class="value">0</span>
<span class="key">ArmorMode</span>=<span class="value">0</span></pre>
<h3>4.1 &nbsp;<code>MissionName</code> &mdash; string, max 255 characters</h3>
<p>
The scenario name, <strong>exactly as it appears in the mission list dropdown in the console lobby.</strong>
Match is exact string equality, case-sensitive. Trailing/leading spaces are stripped by the parser.
</p>
<div class="warn">
<strong>If the name is not found</strong> in the list for the selected <code>GameType</code>,
map selection falls back to index <code>0</code> (first available map for that game type).
No error is logged. All other parameters (Visibility, Weather, etc.) are still applied.
The available mission names depend on what scenario files are installed on the machine;
there is no static guaranteed list.
</div>
<h3>4.2 &nbsp;<code>GameType</code> &mdash; integer (dropdown index, 0-based)</h3>
<div class="warn">
<strong>This is the position in the game type dropdown, NOT the internal game type ID number.</strong>
The dropdown is built dynamically at lobby startup &mdash; only game types that have at least
one installed scenario are shown. If a game type has no installed maps it is skipped and all
subsequent indices shift down by one.
</div>
<p>For a <strong>standard full installation</strong> with all game types present, the order is fixed:</p>
<table>
<tr><th>Index</th><th>Display Name</th><th>Mode</th></tr>
<tr><td><code>0</code></td><td>Destruction</td><td>FFA</td></tr>
<tr><td><code>1</code></td><td>Team Destruction</td><td>Team</td></tr>
<tr><td><code>2</code></td><td>Attrition</td><td>FFA &mdash; <strong>default</strong> <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>3</code></td><td>Team Attrition</td><td>Team</td></tr>
<tr><td><code>4</code></td><td>Capture the Flag</td><td>FFA</td></tr>
<tr><td><code>5</code></td><td>King of the Hill</td><td>FFA</td></tr>
<tr><td><code>6</code></td><td>Team King of the Hill</td><td>Team</td></tr>
<tr><td><code>7</code></td><td>Steal the Bacon</td><td>FFA</td></tr>
<tr><td><code>8</code></td><td>Master Trial</td><td>Special</td></tr>
<tr><td><code>9</code></td><td>Siege Assault</td><td>Special</td></tr>
<tr><td><code>10+</code></td><td>Custom map variants</td><td>Varies (if custom content installed)</td></tr>
</table>
<p><strong>Team vs. FFA is important:</strong> it determines whether the <code>Team</code> or <code>Skin</code> field is applied to each slot. See Section 5 for details.</p>
<h3>4.3 &nbsp;<code>Visibility</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Display Name</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Default</td><td>Same as Clear <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>Clear</td><td>Full visibility range</td></tr>
<tr><td><code>2</code></td><td>Light Fog</td><td>Reduced visibility range</td></tr>
<tr><td><code>3</code></td><td>Heavy Fog</td><td>Heavily reduced visibility range</td></tr>
<tr><td><code>4</code></td><td>Pea Soup Fog</td><td>Near-zero visibility range</td></tr>
</table>
<h3>4.4 &nbsp;<code>Weather</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Display</th></tr>
<tr><td><code>0</code></td><td>Off <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On (rain or snow depending on map)</td></tr>
</table>
<h3>4.5 &nbsp;<code>TimeOfDay</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Display</th></tr>
<tr><td><code>0</code></td><td>Day <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>Night</td></tr>
</table>
<h3>4.6 &nbsp;<code>TimeLimit</code> &mdash; integer (<strong>actual minutes</strong>, not a list index)</h3>
<p>
Specify the desired match length in <strong>real minutes</strong>.
Valid values that appear in the lobby dropdown:
</p>
<p>
<code>1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20, 25, 30</code>
</p>
<p>
Other integers are accepted and applied to the network parameter but will not match a dropdown item
(the visual display may show the nearest value).
</p>
<p>
Special value <code>-1</code>: use the server&rsquo;s current default time (7 minutes unless changed separately).
</p>
<p>Default if key absent: server current default (equivalent to <code>-1</code>).</p>
<h3>4.7 &nbsp;<code>Radar</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Display</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Novice</td><td>All mechs visible, no range limit <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>Normal</td><td>Standard radar with range cutoff</td></tr>
<tr><td><code>2</code></td><td>Team Only</td><td>Only your own team visible on radar</td></tr>
<tr><td><code>3</code></td><td>No Radar</td><td>Radar completely disabled for all players</td></tr>
</table>
<h3>4.8 &nbsp;<code>HeatOn</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off &mdash; heat never builds, weapons never overheat <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On &mdash; standard heat management enabled</td></tr>
</table>
<h3>4.9 &nbsp;<code>FriendlyFire</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off &mdash; friendly fire disabled <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On &mdash; 100% damage to teammates</td></tr>
</table>
<h3>4.10 &nbsp;<code>SplashDamage</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On &mdash; area/splash damage from missiles and explosions</td></tr>
</table>
<h3>4.11 &nbsp;<code>UnlimitedAmmo</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off &mdash; ammo is finite</td></tr>
<tr><td><code>1</code></td><td>On &mdash; unlimited ammo <span class="default-tag">DEFAULT (console/arcade)</span></td></tr>
</table>
<h3>4.12 &nbsp;<code>NoReturn</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off &mdash; pilots can respawn <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On &mdash; no respawn ("No Return")</td></tr>
</table>
<p><strong>Implementation note:</strong> this maps to <code>RESPAWN_LIMIT_PARAMETER</code> (on/off), with <code>RESPAWN_LIMIT_NUMBER_PARAMETER</code> forced to <code>0</code>, matching current lobby behavior.</p>
<h3>4.13 &nbsp;<code>WeaponJam</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On &mdash; weapons can randomly jam</td></tr>
</table>
<h3>4.14 &nbsp;<code>AdvanceMode</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On</td></tr>
</table>
<h3>4.15 &nbsp;<code>ArmorMode</code> &mdash; integer</h3>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Off <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>On</td></tr>
</table>
<h3>4.16 &nbsp;<code>TeamAllowed</code> &mdash; integer <span class="optional">(required for correct team/FFA slot display on first click)</span></h3>
<p>Must match the <code>GameType</code> selected. Controls whether <code>Team</code> or <code>Skin</code> is applied to slots, and whether team dropdowns are shown.</p>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>FFA mode ? slot <code>Skin</code> field is used <span class="default-tag">DEFAULT</span></td></tr>
<tr><td><code>1</code></td><td>Team game ? slot <code>Team</code> field is used</td></tr>
</table>
<h3>4.17 &nbsp;<code>TeamCount</code> &mdash; integer <span class="optional">(optional, only when TeamAllowed=1)</span></h3>
<p>Number of teams. Valid range: 2&ndash;8. Default: <code>2</code>.</p>
<hr>
<h2>5. Sections: <code>[slot0]</code> through <code>[slot15]</code></h2>
<p>One section per roster slot, numbered starting from <strong>zero</strong>.</p>
<div class="warn">
<strong>Slots must be contiguous starting from <code>[slot0]</code>.</strong>
The parser stops at the first missing slot number. If <code>[slot2]</code> is absent,
<code>[slot3]</code> through <code>[slot15]</code> are never read, even if present in the file.
Do not skip numbers.
</div>
<p>Maximum: <strong>16 slots</strong> (indices 0&ndash;15). All slots are cleared before applying file data, so slots not defined in the file will be empty in the lobby after Load File is clicked.</p>
<pre><span class="section">[slot0]</span>
<span class="key">Type</span>=<span class="value">1</span>
<span class="key">PilotName</span>=<span class="value">Alpha One</span>
<span class="key">Mech</span>=<span class="value">Atlas</span>
<span class="key">Team</span>=<span class="value">0</span>
<span class="key">Skin</span>=<span class="value">0</span>
<span class="key">Decal</span>=<span class="value">3</span></pre>
<h3>5.1 &nbsp;<code>Type</code> &mdash; integer, required</h3>
<p>Determines what occupies this slot. All other fields are ignored when <code>Type=0</code>.</p>
<table>
<tr><th>Value</th><th>Description</th></tr>
<tr><td><code>0</code></td><td>Empty &mdash; slot skipped, left empty in lobby</td></tr>
<tr><td><code>1</code></td><td>Player slot &mdash; a human pilot can connect to this seat</td></tr>
<tr><td><code>2</code></td><td>Bot, difficulty level 1 (easiest)</td></tr>
<tr><td><code>3</code></td><td>Bot, difficulty level 2</td></tr>
<tr><td><code>4</code></td><td>Bot, difficulty level 3</td></tr>
<tr><td><code>5</code></td><td>Bot, difficulty level 4</td></tr>
<tr><td><code>6</code></td><td>Bot, difficulty level 5</td></tr>
<tr><td><code>7</code></td><td>Bot, difficulty level 6</td></tr>
<tr><td><code>8</code></td><td>Bot, difficulty level 7</td></tr>
<tr><td><code>9</code></td><td>Bot, difficulty level 8 (hardest)</td></tr>
</table>
<p>If <code>Type</code> key is absent, it defaults to <code>0</code> (empty) due to zero-initialization.</p>
<h3>5.2 &nbsp;<code>PilotName</code> &mdash; string, max 63 characters</h3>
<p>Only meaningful when <code>Type=1</code> (player slot). Sets the callsign shown in the lobby pilot editbox. The connecting player can override this after they connect.</p>
<p>For bots (<code>Type=2&ndash;9</code>), this field is read from the file but ignored by the script.</p>
<p>Set to empty value or omit key to leave the name blank: <code>PilotName=</code></p>
<p>Strings longer than 63 characters are silently truncated.</p>
<h3>5.3 &nbsp;<code>Mech</code> &mdash; string (chassis name, exact and case-sensitive)</h3>
<p>Must match <strong>exactly</strong> one of the chassis names from <code>MechTable.tbl</code>. The lookup is case-sensitive exact string comparison. If the name is not found, the mech dropdown for that slot stays at its previous value. No error is raised.</p>
<div class="warn">
<strong>These are the table key strings, not display labels.</strong>
Pay close attention to capitalization, spacing, and special characters (hyphens, Roman numerals).
</div>
<table class="mech-table">
<tr><th>Exact <code>Mech=</code> value</th><th>Notes</th></tr>
<tr><td>Annihilator</td><td></td></tr>
<tr><td>Archer</td><td></td></tr>
<tr><td>Arctic Wolf</td><td>space between words</td></tr>
<tr><td>Ares</td><td></td></tr>
<tr><td>Argus</td><td></td></tr>
<tr><td>AssassinII</td><td>no spaces; capital I</td></tr>
<tr><td>Atlas</td><td></td></tr>
<tr><td>Avatar</td><td></td></tr>
<tr><td>Awesome</td><td></td></tr>
<tr><td>Battlemaster</td><td></td></tr>
<tr><td>BattlemasterIIC</td><td>no spaces</td></tr>
<tr><td>Behemoth</td><td></td></tr>
<tr><td>BehemothII</td><td>no space; capital I</td></tr>
<tr><td>Black Hawk</td><td>space between words</td></tr>
<tr><td>Black Knight</td><td>space between words</td></tr>
<tr><td>Black Lanner</td><td>space between words</td></tr>
<tr><td>Brigand</td><td></td></tr>
<tr><td>Bushwacker</td><td></td></tr>
<tr><td>Catapult</td><td></td></tr>
<tr><td>Cauldron-Born</td><td>hyphen between words</td></tr>
<tr><td>Chimera</td><td></td></tr>
<tr><td>Commando</td><td></td></tr>
<tr><td>Cougar</td><td></td></tr>
<tr><td>Cyclops</td><td></td></tr>
<tr><td>Daishi</td><td></td></tr>
<tr><td>Deimos</td><td></td></tr>
<tr><td>Dragon</td><td></td></tr>
<tr><td>Fafnir</td><td></td></tr>
<tr><td>Flea</td><td></td></tr>
<tr><td>Gladiator</td><td></td></tr>
<tr><td>Grizzly</td><td></td></tr>
<tr><td>Hauptmann</td><td></td></tr>
<tr><td>Hellhound</td><td></td></tr>
<tr><td>Hellspawn</td><td></td></tr>
<tr><td>Highlander</td><td></td></tr>
<tr><td>HollanderII</td><td>no space; capital I</td></tr>
<tr><td>Hunchback</td><td></td></tr>
<tr><td>Kodiak</td><td></td></tr>
<tr><td>Loki</td><td></td></tr>
<tr><td>Longbow</td><td></td></tr>
<tr><td>Mad Cat</td><td>space between words</td></tr>
<tr><td>Mad Cat MKII</td><td>space + "MKII" all caps, no period</td></tr>
<tr><td>Masakari</td><td></td></tr>
<tr><td>Mauler</td><td></td></tr>
<tr><td>Nova Cat</td><td>space between words</td></tr>
<tr><td>Osiris</td><td></td></tr>
<tr><td>Owens</td><td></td></tr>
<tr><td>Puma</td><td></td></tr>
<tr><td>Raven</td><td></td></tr>
<tr><td>Rifleman</td><td></td></tr>
<tr><td>Ryoken</td><td></td></tr>
<tr><td>Shadow Cat</td><td>space between words</td></tr>
<tr><td>Solitaire</td><td></td></tr>
<tr><td>Sunder</td><td></td></tr>
<tr><td>Templar</td><td></td></tr>
<tr><td>Thanatos</td><td></td></tr>
<tr><td>Thor</td><td></td></tr>
<tr><td>Uller</td><td></td></tr>
<tr><td>Urbanmech</td><td></td></tr>
<tr><td>Uziel</td><td></td></tr>
<tr><td>Victor</td><td></td></tr>
<tr><td>Vulture</td><td></td></tr>
<tr><td>Warhammer</td><td></td></tr>
<tr><td>Wolfhound</td><td></td></tr>
<tr><td>Zeus</td><td></td></tr>
</table>
<h3>5.4 &nbsp;<code>Team</code> &mdash; integer</h3>
<p>
<strong>Only applied in team game modes</strong> (game types marked &ldquo;Team&rdquo; in Section 4.2 table).
<strong class="ignored-tag">IGNORED in FFA modes.</strong>
</p>
<table>
<tr><th>Value</th><th>Team</th><th>Default Color</th></tr>
<tr><td><code>0</code></td><td>Team 1</td><td>Blue</td></tr>
<tr><td><code>1</code></td><td>Team 2</td><td>Red</td></tr>
<tr><td><code>2</code></td><td>Team 3</td><td>Yellow</td></tr>
<tr><td><code>3</code></td><td>Team 4</td><td>Green</td></tr>
<tr><td><code>4</code></td><td>Team 5</td><td>Purple</td></tr>
<tr><td><code>5</code></td><td>Team 6</td><td>Light Blue</td></tr>
<tr><td><code>6</code></td><td>Team 7</td><td>Orange</td></tr>
<tr><td><code>7</code></td><td>Team 8</td><td>Charcoal</td></tr>
</table>
<h3>5.5 &nbsp;<code>Skin</code> &mdash; integer (FFA camo, dropdown index 0-based)</h3>
<p>
<strong>Only applied in FFA game modes.</strong>
<strong class="ignored-tag">IGNORED in team modes.</strong>
</p>
<table>
<tr><th>Value</th><th>Camo Color/Pattern</th></tr>
<tr><td><code>0</code></td><td>Blue</td></tr>
<tr><td><code>1</code></td><td>Red</td></tr>
<tr><td><code>2</code></td><td>Yellow</td></tr>
<tr><td><code>3</code></td><td>Green</td></tr>
<tr><td><code>4</code></td><td>Purple</td></tr>
<tr><td><code>5</code></td><td>Light Blue</td></tr>
<tr><td><code>6</code></td><td>Orange</td></tr>
<tr><td><code>7</code></td><td>Charcoal</td></tr>
<tr><td><code>8</code>&ndash;<code>15</code></td><td>Additional camo patterns</td></tr>
</table>
<h3>5.6 &nbsp;<code>Decal</code> &mdash; integer (faction emblem, 0&ndash;63)</h3>
<p>
Applied in <strong>both team and FFA modes.</strong>
Valid range: <code>0</code>&ndash;<code>63</code>. All values in range are accepted. Common values:
</p>
<table>
<tr><th>Value</th><th>Faction</th></tr>
<tr><td><code>0</code></td><td>None</td></tr>
<tr><td><code>2</code></td><td>Davion</td></tr>
<tr><td><code>3</code></td><td>Steiner</td></tr>
<tr><td><code>4</code></td><td>Kurita</td></tr>
<tr><td><code>5</code></td><td>Liao</td></tr>
<tr><td><code>6</code></td><td>Marik</td></tr>
<tr><td><code>9</code></td><td>ComStar</td></tr>
<tr><td><code>10</code></td><td>Star League</td></tr>
<tr><td><code>11</code></td><td>Wolf Clan</td></tr>
<tr><td><code>12</code></td><td>Jade Falcon</td></tr>
<tr><td><code>16</code></td><td>Ghost Bear</td></tr>
<tr><td><code>20</code></td><td>Hell Horses</td></tr>
<tr><td><code>21</code></td><td>Coyote</td></tr>
<tr><td><code>22</code></td><td>Blood Spirit</td></tr>
<tr><td><code>24</code></td><td>Ice Hellion</td></tr>
<tr><td><code>27</code></td><td>Goliath Scorpion</td></tr>
<tr><td><code>46</code></td><td>BKG</td></tr>
<tr><td><code>47</code></td><td>FSA</td></tr>
<tr><td><code>48</code></td><td>331st</td></tr>
<tr><td><code>49</code></td><td>DDC</td></tr>
<tr><td><code>50</code></td><td>VGL</td></tr>
</table>
<hr>
<h2>6. Complete Example File</h2>
<pre><span class="section">[mission]</span>
<span class="key">MissionName</span>=<span class="value">ScarabStronghold - Attrition</span>
<span class="key">GameType</span>=<span class="value">2</span>
<span class="key">Visibility</span>=<span class="value">0</span>
<span class="key">Weather</span>=<span class="value">0</span>
<span class="key">TimeOfDay</span>=<span class="value">0</span>
<span class="key">TimeLimit</span>=<span class="value">7</span>
<span class="key">Radar</span>=<span class="value">0</span>
<span class="key">HeatOn</span>=<span class="value">0</span>
<span class="key">FriendlyFire</span>=<span class="value">0</span>
<span class="key">SplashDamage</span>=<span class="value">0</span>
<span class="key">UnlimitedAmmo</span>=<span class="value">1</span>
<span class="key">WeaponJam</span>=<span class="value">0</span>
<span class="key">AdvanceMode</span>=<span class="value">0</span>
<span class="key">ArmorMode</span>=<span class="value">0</span>
<span class="section">[slot0]</span>
<span class="key">Type</span>=<span class="value">1</span>
<span class="key">PilotName</span>=<span class="value">Blackjack</span>
<span class="key">Mech</span>=<span class="value">Atlas</span>
<span class="key">Team</span>=<span class="value">0</span>
<span class="key">Skin</span>=<span class="value">0</span>
<span class="key">Decal</span>=<span class="value">3</span>
<span class="section">[slot1]</span>
<span class="key">Type</span>=<span class="value">1</span>
<span class="key">PilotName</span>=<span class="value">Viper</span>
<span class="key">Mech</span>=<span class="value">Mad Cat</span>
<span class="key">Team</span>=<span class="value">0</span>
<span class="key">Skin</span>=<span class="value">1</span>
<span class="key">Decal</span>=<span class="value">4</span>
<span class="section">[slot2]</span>
<span class="key">Type</span>=<span class="value">2</span>
<span class="key">PilotName</span>=
<span class="key">Mech</span>=<span class="value">Thor</span>
<span class="key">Team</span>=<span class="value">1</span>
<span class="key">Skin</span>=<span class="value">1</span>
<span class="key">Decal</span>=<span class="value">4</span>
<span class="section">[slot3]</span>
<span class="key">Type</span>=<span class="value">2</span>
<span class="key">PilotName</span>=
<span class="key">Mech</span>=<span class="value">Warhammer</span>
<span class="key">Team</span>=<span class="value">1</span>
<span class="key">Skin</span>=<span class="value">1</span>
<span class="key">Decal</span>=<span class="value">4</span></pre>
<hr>
<h2>7. Known Constraints and Caveats</h2>
<ol>
<li>
<strong>Slots must be numbered contiguously from <code>[slot0]</code>.</strong>
The parser uses a loop that breaks on the first missing section. Defining <code>[slot0]</code>
and <code>[slot2]</code> without <code>[slot1]</code> means only slot 0 is processed.
</li>
<li>
<strong>All current slots are cleared before file slots are applied.</strong>
Any slot not defined in the file (or defined with <code>Type=0</code>) will be empty in the lobby.
</li>
<li>
<strong>Team vs. FFA display is correct on the first click</strong> provided <code>TeamAllowed=</code> is set correctly in the file.
The game updates <code>team_allowed</code> synchronously from this field before processing slots.
If <code>TeamAllowed</code> is absent, it defaults to <code>0</code> (FFA) regardless of <code>GameType</code>.
</li>
<li>
<strong><code>MissionName</code> and <code>Mech</code> matches are case-sensitive.</strong>
<code>atlas</code> will not match <code>Atlas</code>.
</li>
<li>
<strong>The file is never deleted by the game.</strong>
Write it before the match, overwrite it before the next match. No locking is performed.
</li>
<li>
<strong><code>options.ini</code> is read only at game startup.</strong>
Changing the <code>automaticfile</code> path or enabling/disabling the feature requires a restart.
The config file itself is re-read fresh on every Load File button click.
</li>
<li>
<strong>Buffer limits:</strong> <code>PilotName</code> &le; 63 chars (truncated silently);
<code>MissionName</code> &le; 255 chars;
<code>automaticfile</code> path &le; 259 chars (<code>MAX_PATH&nbsp;&minus;&nbsp;1</code>).
</li>
<li>
<strong>Mech names are sourced from <code>MechTable.tbl</code>.</strong>
If the game installation has custom mechs added or some mechs removed, the valid name list
changes accordingly. The table above reflects the standard Firestorm build.
</li>
</ol>
<hr>
<p style="font-size:9pt; color:#888; text-align:center; margin-top:30px;">
Generated from source: <code>MW4Shell.cpp</code> &mdash; <code>CTCL_LoadAutoFile</code>,
<code>CTCL_GetAutoSlotName/Mech/Int</code> &nbsp;|&nbsp;
<code>ConLobby.script</code> &mdash; <code>o_load_file</code> handler &nbsp;|&nbsp;
<code>ConLobbyMission.script</code> &mdash; <code>MAIL_LOAD_AUTO_MISSION</code> &nbsp;|&nbsp;
Commit cd2fe73c &nbsp;|&nbsp; 2026-07-24
</p>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Generate special_weapon_locations.csv and rear_facing_weapons.csv from stock mech subsystems."""
import csv
from pathlib import Path
BASE = Path("Gameleap/mw4/Content")
MECH_TABLE = BASE / "Tables" / "MechTable.tbl"
OUT_DIR = Path("BTFrstrm")
def parse_ini(path):
sections, cur_sec, cur_dict = [], None, {}
with open(path, 'r', encoding='latin-1', errors='replace') as f:
for raw in f:
line = raw.strip()
if line.startswith('[') and line.endswith(']'):
if cur_sec is not None:
sections.append((cur_sec, cur_dict))
cur_sec, cur_dict = line[1:-1], {}
elif '=' in line and cur_sec is not None:
k, _, v = line.partition('=')
cur_dict[k.strip()] = v.strip()
if cur_sec is not None:
sections.append((cur_sec, cur_dict))
return sections
def find_dir_ci(parent, name):
"""Case-insensitive directory lookup for Linux."""
exact = parent / name
if exact.exists():
return exact
name_lower = name.lower()
for child in parent.iterdir():
if child.is_dir() and child.name.lower() == name_lower:
return child
return None
def weapon_model(model_path):
return Path(model_path.replace('\\', '/')).stem
# Read chassis list from MechTable.tbl
chassis_list = []
with open(MECH_TABLE, 'r', encoding='latin-1') as f:
for raw in f:
line = raw.strip()
if not line or line.startswith('//') or line.startswith('[') or '=' not in line:
continue
name, _, rel = line.partition('=')
# rel is like "Mechs\Atlas\Atlas.instance"
parts = rel.strip().replace('\\', '/').split('/')
chassis_list.append((name.strip(), parts)) # parts = ['Mechs', 'Atlas', 'Atlas.instance']
print(f"Found {len(chassis_list)} chassis in MechTable.tbl")
HDR = ['Chassis', 'InternalLocation', 'Model', 'Site', 'AmmoCount', 'GroupIndex', 'WeaponFacing']
special_rows, rear_rows, missing = [], [], []
mechs_dir = BASE / "Mechs"
for chassis_name, path_parts in chassis_list:
# path_parts[1] is the mech directory name (e.g. 'Atlas', 'MadCat_MkII', 'Urbanmech')
if len(path_parts) < 2:
missing.append(chassis_name)
continue
mech_dir = find_dir_ci(mechs_dir, path_parts[1])
if not mech_dir:
missing.append(f"{chassis_name} (dir not found: {path_parts[1]})")
continue
subs = [f for f in mech_dir.glob('*.subsystems') if f.suffix == '.subsystems']
if not subs:
missing.append(f"{chassis_name} (no .subsystems in {mech_dir.name})")
continue
subsys_file = subs[0]
sections = parse_ini(subsys_file)
for sec_name, fields in sections:
if 'WeaponSubsystem' not in fields.get('Model', ''):
continue
loc = fields.get('InternalLocation', '')
model = weapon_model(fields.get('Model', ''))
site = fields.get('Site', '')
ammo = fields.get('AmmoCount', '')
group = fields.get('GroupIndex', '')
facing = fields.get('WeaponFacing', '')
if loc in ('Special1', 'Special2'):
special_rows.append([chassis_name, loc, model, site, ammo, group, facing])
if facing and facing != '0':
rear_rows.append([chassis_name, loc, model, site, ammo, group, facing])
# Write CSVs
out1 = OUT_DIR / 'special_weapon_locations.csv'
with open(out1, 'w', newline='') as f:
w = csv.writer(f)
w.writerow(HDR)
w.writerows(special_rows)
out2 = OUT_DIR / 'rear_facing_weapons.csv'
with open(out2, 'w', newline='') as f:
w = csv.writer(f)
w.writerow(HDR)
w.writerows(rear_rows)
print(f"Spreadsheet 1 ? Special1/2 weapon locations: {len(special_rows)} rows -> {out1}")
print(f"Spreadsheet 2 ? Non-forward WeaponFacing: {len(rear_rows)} rows -> {out2}")
if missing:
print(f"\nSkipped: {missing}")
+360
View File
@@ -292,3 +292,363 @@ At the time this doc was written:
- non-playable mechs are explicitly marked `No` - non-playable mechs are explicitly marked `No`
- Templar and other mechs with rear or side mounts have facing visible in the CSV - Templar and other mechs with rear or side mounts have facing visible in the CSV
- the file is intended to be a living audit artifact for future data cleanup and enablement work - the file is intended to be a living audit artifact for future data cleanup and enablement work
---
## MechEditor Web App — Data Sources, Fields, and Conversions (documented 2026-07-23)
The MechEditor is a single-file Python HTTP server at `/home/rich/Repositories/MechEditor/mech_editor.py`.
It runs at `localhost:8765`, parses the firestorm content tree, and presents a full mech configuration editor in the browser.
This section documents every data field it reads, every conversion it performs, and every naming rule it enforces.
---
### Data Files Parsed Per Mech
All files live under `Gameleap/mw4/Content/Mechs/<MechDir>/`.
#### `.data` file (`parse_data()`)
| Field in file | Editor key | Notes |
|---|---|---|
| `VehicleTonnage` | `tonnage` | chassis base tonnage (float) |
| `MaxVehicleTonnage` | `max_tonnage` | max loadout tonnage (float) |
| `TechType` | `tech` | `$(Tech_IS)` ? `"IS"`, `$(Tech_Clan)` ? `"Clan"` |
| `MaxHeat` | `max_heat` | heat capacity (int) |
| `VehicleTradeValue` | `trade_value` | C-bills |
| `DragoonValue` | `dragoon` | used in Power Rating bar scaling |
| `MinMaxSpeed` | `min_max_speed` | base speed ceiling in **m/s** |
| `MaxSpeed` | `max_speed` | absolute speed ceiling in **m/s** (engine upgrades may not exceed this) |
| `FullStopTurnRate` | `full_stop_turn` | turn rate at zero speed, in **degrees/sec** |
| `TopSpeedTurnRate` | `top_speed_turn` | turn rate at top speed, in **degrees/sec** |
| `Acceleration` | `acceleration` | forward acceleration in **m/s²** |
| `Decceleration` | `decceleration` | forward braking in **m/s²****double-c spelling is canonical in the engine source** |
| `ReverseAccelerationMultiplier` | `rev_accel_mult` | multiplier applied to Acceleration for reverse |
| `ReverseDeccelerationMultiplier` | `rev_decel_mult` | multiplier applied to Decceleration for reverse — double-c canonical |
| `MinStandTransitionSpeed` | not surfaced | animation threshold only — see note below |
| `CanLoadJumpJets` | `can_jj` | Yes/No |
| `CanLoadECM` | `can_ecm` | Yes/No |
| `CanLoadBeagle` | `can_bap` | Yes/No |
| `CanLoadLightAmp` | `can_lightamp` | Yes/No |
| `CanLoadAMS` | `can_ams` | Yes/No |
| `CanLoadLAMS` | `can_lams` | Yes/No |
| `CanLoadIFF_Jammer` | `can_iff` | Yes/No |
**MinStandTransitionSpeed** is the speed (m/s) below which the mech switches from its walking animation to its idle/standing animation.
It is NOT a hard movement limit. It is purely an animation state machine threshold in `Mech.cpp`.
Code path: `animStateEngine?RequestState(StandState)` when `currentSpeedMPS <= minStandTransitionSpeed`.
Must be > 0 (validated in `Vehicle_Tool.cpp`). Argus = 12.631 m/s = 45.5 kph.
#### `.instance` file (`parse_instance()`)
| Field | Editor key | Notes |
|---|---|---|
| `PowerRating` | `PowerRating` | mechlab bar value, 0100 |
| `ArmorRating` | `ArmorRating` | mechlab bar value, 0100 |
| `SpeedRating` | `SpeedRating` | mechlab bar value, 0100 |
| `HeatRating` | `HeatRating` | mechlab bar value, 0100 |
| `DoesHaveLightAmp` | `has_lightamp` | 0 or 1, default 1 — whether LightAmp is currently installed |
#### `.subsystems` file (`parse_subsystems()`)
Provides: armor type + per-zone multipliers, installed heatsinks, jump jets, engine upgrades, weapons, electronics.
**Armor block:**
- `ArmorType=` ? armor type string (`Standard`, `FerroFiberus`, `Reactive`, `Reflective`, `Solarian`)
- Per-zone entries: `LeftArm=1.0`, `RightTorso=2.5`, etc. — multiplier for that zone
**Engine:**
- `EngineUpgrade` blocks counted ? `engine_upgrades` (05)
**Weapons** — each weapon block contains:
- `Model=` ? weapon subsystem resource path (name extracted)
- `InternalLocation=` ? zone name
- `Site=` ? mount port name (from armature)
- `GroupIndex=` ? weapon group number
- `WeaponFacing=` ? 0=Front, 1=Rear, 2=Side (absent = Front)
- `AmmoCount=` ? rounds for ammo-using weapons
- `EjectSite=` ? optional ejection site for ammo
**Electronics:** ECM, Beagle (BAP), AMS, LAMS, IFF_Jammer detected by subsystem model path.
#### `.damage` file (`parse_damage()`)
Per zone section `[ZoneInternal]`:
- `BaseArmorValue` ? starting armor (float)
- `MaxArmorValue` ? hard cap on armor pts for that zone
- `InternalHPValue` ? internal structure HP
- `OmniSlots`, `BeamSlots`, `MissileSlots`, `ProjectileSlots` ? weapon slot counts
Special zones:
- `SpecialAttachedToZone=` ? which body section this special zone is attached to
- `DamagePropagationZone=` ? where overflow damage propagates
#### `.engine` file (`parse_engine()`)
| Field | Editor key | Notes |
|---|---|---|
| `NumHeatSinks` | `NumHeatSinks` | free heatsinks from engine (not in subsystems) |
| `TonsPerUpgrade` | `TonsPerUpgrade` | tonnage cost per engine upgrade tier |
| `MPSPerUpgrade` | `MPSPerUpgrade` | m/s speed gain per upgrade tier |
#### `.torso` file (`parse_torso()`)
| Field | Notes |
|---|---|
| `TwistSpeed` | torso horizontal rotation speed — may be a macro reference |
| `PitchSpeed` | torso vertical rotation speed — may be a macro reference |
| `TwistRadius` | max horizontal twist angle — may be a macro reference |
| `PitchRadius` | max vertical pitch angle — may be a macro reference |
| `ArmRatioAngle` | ratio of arm tracking vs torso rotation — may be a macro reference |
All torso fields may reference macros from `Content/Defines/MechTorso.defines`.
The editor resolves them using a preloaded `TORSO_DEFINES` dict. Notable values:
```
NORMAL_RATIO = 40
SNAIL_TSPEED = 40
NORMAL_TSPEED = 60
FAST_TSPEED = 80
WIDE_TRADIUS = 160
NORMAL_PRADIUS = 40
```
---
### Calculated Ratings and Conversions
#### Speed (kph)
```
top_speed_kph = min(MinMaxSpeed + MPSPerUpgrade × engine_upgrades, MaxSpeed) × 3.6
```
- `MinMaxSpeed` and `MaxSpeed` from `.data` (m/s)
- `MPSPerUpgrade` from `.engine` (m/s per tier)
- `engine_upgrades` from `.subsystems` (05)
- Multiply by 3.6 to convert m/s ? kph
- Argus example: (20.28 + 1.11 × 10) × 3.6 = 113.5 kph
#### Speed Rating (bar)
```
speed_rating = (top_speed_mps / MaxSpeed) × 100 [capped at 100]
```
#### Turn Rate — degrees to radians
The `.data` file stores turn rates in **degrees/sec**. The mechlab UI label was changed to show rad/sec:
- `StringResource.rc`: `IDS_ML_CH_TURNRATE` ? `"Turn Rate (Top Speed Rad/Sec):"`
- Conversion: `radians = degrees × ?/180` where `?/180 ? 0.017453`
- Argus: FullStopTurn = 75° = **1.309 rad/sec**, TopSpeedTurn = 45° = **0.785 rad/sec**
#### Acceleration / Deceleration (m/s²)
Stored directly in `.data`. Reverse values are derived:
```
reverse_accel = Acceleration × ReverseAccelerationMultiplier
reverse_decel = Decceleration × ReverseDeccelerationMultiplier
```
**The double-c spelling (`Decceleration`, `ReverseDeccelerationMultiplier`) is canonical — it matches the engine source. Do not "fix" the spelling.**
#### Heat Rating (bar)
```
total_hs = NumHeatSinks (engine) + installed_heatsinks (subsystems)
effective_hs = total_hs × (2 if Double else 1)
heat_rating = (effective_hs / MaxHeat) × 100 [capped at 100]
```
#### Power Rating (bar)
```
total_damage = ? (DamageAmount × NumFire) for each installed weapon
power_rating = (total_damage / 80) × 100 [capped at 100]
```
- `DamageAmount` and `NumFire` come from `WeaponSubsystems/<weapon>.data` following `!include` chains
- Parsed by `load_weapon_damages()` at server startup, cached in `Handler.weapon_damages`
- Argus example with default load: ~36.2 total damage ? 45 rating (stored = 42)
#### Armor Rating (bar)
```
for each zone:
pts = multiplier × ARMOR_PTS_PER_TON[armor_type]
effective = min(pts, MaxArmorValue[zone])
armor_rating = (? effective / ? MaxArmorValue) × 100
```
Armor pts per ton by type (from `Adept/ResourceImagePool.cpp` and game design):
| Type | Pts/ton |
|---|---|
| Standard | 32 |
| FerroFiberus | 38 |
| Reactive | 30 |
| Reflective | 30 |
| Solarian | 60 |
Note: `FerroFiberus` is the canonical internal token (not player-visible). The player sees `DNL_FERROFIB = "Ferro Fibrous"` via string lookup.
---
### Active-in-Game Detection
Source: `Gameleap/mw4/Content/Tables/MechChassisTable.tbl`
Format:
```
DisplayKey=Mechs\DirName\FileName.data
//CommentedKey=Mechs\DirName\FileName.data <- inactive
```
- Active = entry exists AND is not prefixed with `//`
- Currently the only inactive mech: **Dasher** (commented out)
- The editor shows a green **ACTIVE IN GAME** or red **NOT IN GAME** banner at top of Stats tab
---
### hsh/ Image Naming Conventions
The `hsh/` directory under `Gameleap/mw4/` holds loose BMP files loaded at runtime (not packed into `.mw4`).
There are four relevant subdirectories, each with a different naming authority.
#### `hsh/hud/` — in-game HUD damage silhouette (own mech)
#### `hsh/MFD/` — MFD target display silhouette (target mech)
#### `hsh/radar/hud/` — radar damage overlay
**All three use identical stems** sourced from `huddamage.cpp` `texturename[]` array.
Load path:
- hud/MFD: `hsh\<texturename>.bmp` where texturename = `hud\<stem>` ? file = `hsh/hud/<stem>.bmp`
- radar: `hsh\radar\<texturename>.bmp` ? file = `hsh/radar/hud/<stem>.bmp`
Code: `render.cpp` `CRadar_Device::LoadRadarDamageTexture()` and `huddamage.cpp` `HUDDamage`.
#### `hsh/Mechs/` — mw4print scorecard portrait
Load path: `recscore.cpp` ? `GetLocString(model->m_nameIndex)` ? `DNL_*` string from `StringResource.rc` ? lowercased filename.
Different naming authority from the other three.
---
### Complete Canonical Name Table
Key: mech directory name (case-insensitive) ? canonical stem for `hsh/hud/`, `hsh/MFD/`, `hsh/radar/hud/`.
Entries in **bold** differ from the directory name.
| Directory | hud/MFD/radar stem | hsh/Mechs/ portrait filename |
|---|---|---|
| Annihilator | annihilator | annihilator.bmp |
| Archer | archer | archer.bmp |
| ArcticWolf | arcticwolf | arctic wolf.bmp |
| Ares | ares | ares.bmp |
| Argus | argus | argus.bmp |
| Assassin2 | assassin2 | **assassin ii.bmp** |
| Atlas | atlas | atlas.bmp |
| Avatar | avatar | avatar.bmp |
| Awesome | awesome | awesome.bmp |
| Battlemaster | battlemaster | battlemaster.bmp |
| Battlemaster2c | **battlemasteriic** | **battlemaster iic.bmp** |
| Behemoth | behemoth | behemoth.bmp |
| Behemoth2 | **behemothii** | **behemoth ii.bmp** |
| Blackhawk | blackhawk | black hawk.bmp |
| Blacknight | **blackknight** | black knight.bmp |
| Blacklanner | blacklanner | black lanner.bmp |
| Brigand | brigand | brigand.bmp |
| Bushwacker | bushwacker | bushwacker.bmp |
| Catapult | catapult | catapult.bmp |
| CauldronBorn | cauldronborn | **cauldronborn.bmp** (table key has hyphen; DNL does not) |
| Chimera | chimera | chimera.bmp |
| Commando | commando | commando.bmp |
| Cougar | cougar | cougar.bmp |
| Cyclops | cyclops | cyclops.bmp |
| Daishi | daishi | daishi.bmp |
| Deimos | deimos | deimos.bmp |
| Dragon | dragon | dragon.bmp |
| Fafnir | fafnir | fafnir.bmp |
| Flea | flea | flea.bmp |
| Gladiator | gladiator | gladiator.bmp |
| Grizzly | grizzly | grizzly.bmp |
| Hauptmann | hauptmann | hauptmann.bmp |
| Hellhound | hellhound | hellhound.bmp |
| Hellspawn | hellspawn | hellspawn.bmp |
| Highlander | highlander | highlander.bmp |
| Hollander | **hollanderii** | **hollander ii.bmp** |
| Hunchback | hunchback | hunchback.bmp |
| Kodiak | kodiak | kodiak.bmp |
| Loki | loki | loki.bmp |
| Longbow | longbow | longbow.bmp |
| Madcat | madcat | mad cat.bmp |
| Madcat_MKII | **madcat2** | mad cat mkii.bmp |
| Masakari | masakari | masakari.bmp |
| Mauler | mauler | mauler.bmp |
| Novacat | novacat | nova cat.bmp |
| Osiris | osiris | osiris.bmp |
| Owens | owens | owens.bmp |
| Puma | puma | puma.bmp |
| Raven | raven | raven.bmp |
| Rifleman | rifleman | rifleman.bmp |
| Ryoken | ryoken | ryoken.bmp |
| Shadowcat | shadowcat | shadow cat.bmp |
| Solitaire | solitaire | solitaire.bmp |
| Sunder | sunder | sunder.bmp |
| Templar | templar | templar.bmp |
| Thanatos | thanatos | thanatos.bmp |
| Thor | thor | thor.bmp |
| Uller | uller | uller.bmp |
| Urbanmech | urbanmech | urbanmech.bmp |
| Uziel | uziel | uziel.bmp |
| Victor | victor | victor.bmp |
| Vulture | vulture | vulture.bmp |
| Warhammer | warhammer | warhammer.bmp |
| Wolfhound | wolfhound | wolfhound.bmp |
| Zeus | zeus | zeus.bmp |
**Critical mismatches where directory name ? hud/MFD stem (files must use the stem, not the dir name):**
| Directory | Wrong name (dir-based) | Correct name (stem) |
|---|---|---|
| Battlemaster2c | battlemaster2c.bmp | **battlemasteriic.bmp** |
| Behemoth2 | behemoth2.bmp | **behemothii.bmp** |
| Blacknight | blacknight.bmp | **blackknight.bmp** |
| Hollander | hollander.bmp | **hollanderii.bmp** |
| Madcat_MKII | madcat_mkii.bmp | **madcat2.bmp** |
**Portrait mismatches for hsh/Mechs/ (table key ? DNL string):**
The `MechChassisTable.tbl` display key and `GetLocString()` DNL string differ for these mechs.
mw4print uses the DNL string. The table key is NOT the correct portrait filename for these 5 mechs.
| Directory | Table key (wrong for mw4print) | DNL string (correct portrait stem) |
|---|---|---|
| Assassin2 | AssassinII ? assassinii | `DNL_ASSASSIN2 "Assassin II"` ? **assassin ii.bmp** |
| Battlemaster2c | BattlemasterIIC ? battlemasteriic | `DNL_BATTLEMASTERIIC "Battlemaster IIc"` ? **battlemaster iic.bmp** |
| Behemoth2 | BehemothII ? behemothii | `DNL_BEHEMOTHII "Behemoth II"` ? **behemoth ii.bmp** |
| CauldronBorn | Cauldron-Born ? cauldron-born | `DNL_CAULDRONBORN "Cauldronborn"` ? **cauldronborn.bmp** |
| Hollander | HollanderII ? hollanderii | `DNL_HOLLANDERII "Hollander II"` ? **hollander ii.bmp** |
---
### MechEditor Implementation Notes
The editor encodes all of the above knowledge in two Python dicts:
**`MECH_HSH_STEMS`** (in `mech_editor.py`)
Maps lowercase dir name ? canonical stem for `hsh/hud/`, `hsh/MFD/`, `hsh/radar/hud/`.
Source: `huddamage.cpp` `texturename[]` array.
**`MECH_PORTRAIT_OVERRIDES`** (in `mech_editor.py`)
Maps lowercase dir name ? portrait stem for `hsh/Mechs/` where the DNL string differs from the chassis table key.
Source: `DNL_*` entries in `Gameleap/code/mw4/Code/scriptstrings/StringResource.rc`.
For all other mechs, the portrait stem is derived dynamically from `MechChassisTable.tbl` (display key, lowercased).
The Assets tab in the editor shows all four image types. When an image is missing, it displays:
```
Wants: hsh/<subdir>/<expected_filename>.bmp
```
so the user knows exactly what to rename or create.
+64
View File
@@ -0,0 +1,64 @@
Chassis,InternalLocation,Model,Site,AmmoCount,GroupIndex,WeaponFacing
Archer,LeftTorso,LRM15,site_ldmissile,24,2,2
Archer,RightTorso,LRM15,site_rdmissile,24,2,2
Ares,Special2,ClanLRM10,site_lmissileport,24,2,2
Ares,Special2,ClanLRM10,site_missileport,24,2,2
Ares,Special2,ClanLRM10,site_rmissileport,24,2,2
AssassinII,Special1,LRM5,site_missileport,24,2,2
Atlas,LeftTorso,LRM20,site_lmissileport,12,2,2
Avatar,LeftTorso,LRM5,site_lmissileport,24,2,2
Avatar,RightTorso,LRM5,site_rmissileport,24,2,2
Avatar,LeftTorso,LRM10,site_lmissileport,24,2,2
Avatar,RightTorso,LRM10,site_rmissileport,24,2,2
Awesome,CenterTorso,LRM5,site_ctorsoport,24,2,2
Battlemaster,RightTorso,MediumLaser,site_rdtorsoport,,1,1
Battlemaster,LeftTorso,MediumLaser,site_ldtorsoport,,1,1
BattlemasterIIC,RightTorso,ClanERMediumLaser,site_rdtorsoport,,1,1
BattlemasterIIC,LeftTorso,ClanERMediumLaser,site_ldtorsoport,,1,1
Black Lanner,Special2,ClanSRM6,site_rmissileport,15,2,2
Black Lanner,Special1,ClanLRM10,site_lmissileport,24,2,2
Bushwacker,Special1,LRM5,site_missileport,24,2,2
Bushwacker,Special1,LRM5,site_missileport,24,2,2
Catapult,LeftArm,LRM20,site_lmissileport,12,2,2
Catapult,RightArm,LRM20,site_rmissileport,12,2,2
Cauldron-Born,Special1,ClanLRM10,site_rmissileport,24,2,2
Cauldron-Born,Special2,ClanLRM10,site_lmissileport,24,2,2
Chimera,RightTorso,LRM5,site_missileport,24,2,2
Chimera,RightTorso,LRM5,site_missileport,24,2,2
Chimera,RightTorso,LRM5,site_missileport,24,2,2
Chimera,RightTorso,LRM5,site_missileport,24,2,2
Cyclops,LeftTorso,LRM10,site_lmissile,24,2,2
Cougar,LeftTorso,ClanLRM10,site_lmissileport,12,2,2
Cougar,RightTorso,ClanLRM10,site_rmissileport,12,2,2
Deimos,Special1,ClanLRM15,site_lmissileport,16,2,2
Deimos,Special2,ClanLRM15,site_rmissileport,16,2,2
Dragon,CenterTorso,LRM10,site_missleport,24,2,2
Flea,LeftTorso,SmallLaser,site_ltorsoport,,1,1
Flea,RightTorso,SmallLaser,site_rtorsoport,,1,1
Grizzly,LeftTorso,CLANLRM10,site_missleport,24,2,2
Hellhound,LeftTorso,ClanLRM10,site_missileport,12,2,2
Mad Cat,Special1,ClanLRM10,site_lmissileport,24,2,2
Mad Cat,Special2,ClanLRM10,site_rmissileport,24,2,2
Mad Cat MKII,Special2,ClanLRM15,site_lmissileport,16,2,2
Mad Cat MKII,Special1,ClanLRM15,site_rmissileport,16,2,2
Masakari,LeftTorso,ClanLRM10,site_lmissleport,24,2,2
Masakari,LeftTorso,ClanLRM10,site_ltorsoport,24,2,2
Mauler,LeftTorso,LRM5,site_lmissileport,24,2,2
Mauler,LeftTorso,LRM5,site_lmissileport,24,2,2
Mauler,LeftTorso,LRM5,site_lmissileport,24,2,2
Mauler,RightTorso,LRM5,site_rmissileport,24,2,2
Mauler,RightTorso,LRM5,site_rmissileport,24,2,2
Mauler,RightTorso,LRM5,site_rmissileport,24,2,2
Raven,RightTorso,LRM5,site_rtorsoport,24,2,2
Sunder,LeftTorso,MediumLaser,site_ldtorsoport,,1,1
Sunder,RightTorso,MediumLaser,site_rdtorsoport,,1,1
Templar,LeftTorso,MediumPulseLaser,site_ldrtorsoport,,1,1
Templar,RightTorso,MediumPulseLaser,site_rdrtorsoport,,1,1
Thor,Special1,CLANLRM10,site_missileport,24,2,2
Uziel,Special1,LRM10,site_missileport,24,2,2
Vulture,LeftTorso,ClanLRM5,site_lmissileport,24,2,2
Vulture,RightTorso,ClanLRM5,site_rmissileport,24,2,2
Vulture,LeftTorso,ClanLRM10,site_lmissileport,24,2,2
Vulture,RightTorso,ClanLRM10,site_rmissileport,24,2,2
Wolfhound,CenterTorso,ClanERMediumLaser,site_reartorsoport,,1,1
Zeus,RightTorso,MediumPulseLaser,site_reartorsoport,,1,1
1 Chassis InternalLocation Model Site AmmoCount GroupIndex WeaponFacing
2 Archer LeftTorso LRM15 site_ldmissile 24 2 2
3 Archer RightTorso LRM15 site_rdmissile 24 2 2
4 Ares Special2 ClanLRM10 site_lmissileport 24 2 2
5 Ares Special2 ClanLRM10 site_missileport 24 2 2
6 Ares Special2 ClanLRM10 site_rmissileport 24 2 2
7 AssassinII Special1 LRM5 site_missileport 24 2 2
8 Atlas LeftTorso LRM20 site_lmissileport 12 2 2
9 Avatar LeftTorso LRM5 site_lmissileport 24 2 2
10 Avatar RightTorso LRM5 site_rmissileport 24 2 2
11 Avatar LeftTorso LRM10 site_lmissileport 24 2 2
12 Avatar RightTorso LRM10 site_rmissileport 24 2 2
13 Awesome CenterTorso LRM5 site_ctorsoport 24 2 2
14 Battlemaster RightTorso MediumLaser site_rdtorsoport 1 1
15 Battlemaster LeftTorso MediumLaser site_ldtorsoport 1 1
16 BattlemasterIIC RightTorso ClanERMediumLaser site_rdtorsoport 1 1
17 BattlemasterIIC LeftTorso ClanERMediumLaser site_ldtorsoport 1 1
18 Black Lanner Special2 ClanSRM6 site_rmissileport 15 2 2
19 Black Lanner Special1 ClanLRM10 site_lmissileport 24 2 2
20 Bushwacker Special1 LRM5 site_missileport 24 2 2
21 Bushwacker Special1 LRM5 site_missileport 24 2 2
22 Catapult LeftArm LRM20 site_lmissileport 12 2 2
23 Catapult RightArm LRM20 site_rmissileport 12 2 2
24 Cauldron-Born Special1 ClanLRM10 site_rmissileport 24 2 2
25 Cauldron-Born Special2 ClanLRM10 site_lmissileport 24 2 2
26 Chimera RightTorso LRM5 site_missileport 24 2 2
27 Chimera RightTorso LRM5 site_missileport 24 2 2
28 Chimera RightTorso LRM5 site_missileport 24 2 2
29 Chimera RightTorso LRM5 site_missileport 24 2 2
30 Cyclops LeftTorso LRM10 site_lmissile 24 2 2
31 Cougar LeftTorso ClanLRM10 site_lmissileport 12 2 2
32 Cougar RightTorso ClanLRM10 site_rmissileport 12 2 2
33 Deimos Special1 ClanLRM15 site_lmissileport 16 2 2
34 Deimos Special2 ClanLRM15 site_rmissileport 16 2 2
35 Dragon CenterTorso LRM10 site_missleport 24 2 2
36 Flea LeftTorso SmallLaser site_ltorsoport 1 1
37 Flea RightTorso SmallLaser site_rtorsoport 1 1
38 Grizzly LeftTorso CLANLRM10 site_missleport 24 2 2
39 Hellhound LeftTorso ClanLRM10 site_missileport 12 2 2
40 Mad Cat Special1 ClanLRM10 site_lmissileport 24 2 2
41 Mad Cat Special2 ClanLRM10 site_rmissileport 24 2 2
42 Mad Cat MKII Special2 ClanLRM15 site_lmissileport 16 2 2
43 Mad Cat MKII Special1 ClanLRM15 site_rmissileport 16 2 2
44 Masakari LeftTorso ClanLRM10 site_lmissleport 24 2 2
45 Masakari LeftTorso ClanLRM10 site_ltorsoport 24 2 2
46 Mauler LeftTorso LRM5 site_lmissileport 24 2 2
47 Mauler LeftTorso LRM5 site_lmissileport 24 2 2
48 Mauler LeftTorso LRM5 site_lmissileport 24 2 2
49 Mauler RightTorso LRM5 site_rmissileport 24 2 2
50 Mauler RightTorso LRM5 site_rmissileport 24 2 2
51 Mauler RightTorso LRM5 site_rmissileport 24 2 2
52 Raven RightTorso LRM5 site_rtorsoport 24 2 2
53 Sunder LeftTorso MediumLaser site_ldtorsoport 1 1
54 Sunder RightTorso MediumLaser site_rdtorsoport 1 1
55 Templar LeftTorso MediumPulseLaser site_ldrtorsoport 1 1
56 Templar RightTorso MediumPulseLaser site_rdrtorsoport 1 1
57 Thor Special1 CLANLRM10 site_missileport 24 2 2
58 Uziel Special1 LRM10 site_missileport 24 2 2
59 Vulture LeftTorso ClanLRM5 site_lmissileport 24 2 2
60 Vulture RightTorso ClanLRM5 site_rmissileport 24 2 2
61 Vulture LeftTorso ClanLRM10 site_lmissileport 24 2 2
62 Vulture RightTorso ClanLRM10 site_rmissileport 24 2 2
63 Wolfhound CenterTorso ClanERMediumLaser site_reartorsoport 1 1
64 Zeus RightTorso MediumPulseLaser site_reartorsoport 1 1
+45
View File
@@ -0,0 +1,45 @@
Chassis,InternalLocation,Model,Site,AmmoCount,GroupIndex,WeaponFacing
Ares,Special1,ClanERSmallLaser,site_lgunport,,1,
Ares,Special1,ClanERSmallLaser,site_rgunport,,1,
Ares,Special2,ClanLRM10,site_lmissileport,24,2,2
Ares,Special2,ClanLRM10,site_missileport,24,2,2
Ares,Special2,ClanLRM10,site_rmissileport,24,2,2
AssassinII,Special1,LRM5,site_missileport,24,2,2
Atlas,Special1,AC20,site_rtorsoport,10,1,
Atlas,Special2,SRM6,site_llmissile,15,2,
Avatar,Special1,MachineGun,site_ltorsoport,200,1,
Avatar,Special1,MachineGun,site_rtorsoport,200,1,
Battlemaster,Special2,PPC,site_rdgunport,,1,
Battlemaster,Special1,SRM6,site_missile,15,2,
BattlemasterIIC,Special1,ClanSSRM6,site_missile,15,2,
Behemoth,Special1,LargeLaser,site_ctorsoport,,1,
BehemothII,Special1,LargeLaser,site_ctorsoport,,1,
Black Lanner,Special2,ClanSRM6,site_rmissileport,15,2,2
Black Lanner,Special1,ClanLRM10,site_lmissileport,24,2,2
Brigand,Special2,MediumLaser,site_ltorsoport,,1,
Brigand,Special1,MediumLaser,site_rtorsoport,,1,
Bushwacker,Special1,LRM5,site_missileport,24,2,2
Bushwacker,Special1,LRM5,site_missileport,24,2,2
Cauldron-Born,Special1,ClanLRM10,site_rmissileport,24,2,2
Cauldron-Born,Special2,ClanLRM10,site_lmissileport,24,2,2
Cyclops,Special1,MediumPulseLaser,site_gunport,,1,
Deimos,Special1,ClanLRM15,site_lmissileport,16,2,2
Deimos,Special2,ClanLRM15,site_rmissileport,16,2,2
Hauptmann,Special1,ClanUltraAC20,site_rutorsoport,16,1,
Hellspawn,Special1,SSRM2,site_rmissileport,50,2,
HollanderII,Special2,SmallPulseLaser,site_cltorsoport,,1,
HollanderII,Special2,SmallPulseLaser,site_crtorsoport,,1,
HollanderII,Special1,GaussRifle,site_rtorsoport,16,1,
Hunchback,Special1,AC10,site_rutorsoport,20,1,
Loki,Special1,ClanSSRM6,site_missileport,15,2,
Mad Cat,Special1,ClanLRM10,site_lmissileport,24,2,2
Mad Cat,Special2,ClanLRM10,site_rmissileport,24,2,2
Mad Cat MKII,Special2,ClanLRM15,site_lmissileport,16,2,2
Mad Cat MKII,Special1,ClanLRM15,site_rmissileport,16,2,2
Rifleman,Special1,MediumLaser,site_ltorsoport,,1,
Rifleman,Special1,MediumLaser,site_rtorsoport,,1,
Solitaire,Special1,ClanERLargeLaser,site_rtorsoport,,1,
Thor,Special1,CLANLRM10,site_missileport,24,2,2
Uziel,Special1,LRM10,site_missileport,24,2,2
Vulture,Special1,ClanMachineGun,site_ltorsoport,200,1,
Vulture,Special1,ClanMachineGun,site_rtorsoport,200,1,
1 Chassis InternalLocation Model Site AmmoCount GroupIndex WeaponFacing
2 Ares Special1 ClanERSmallLaser site_lgunport 1
3 Ares Special1 ClanERSmallLaser site_rgunport 1
4 Ares Special2 ClanLRM10 site_lmissileport 24 2 2
5 Ares Special2 ClanLRM10 site_missileport 24 2 2
6 Ares Special2 ClanLRM10 site_rmissileport 24 2 2
7 AssassinII Special1 LRM5 site_missileport 24 2 2
8 Atlas Special1 AC20 site_rtorsoport 10 1
9 Atlas Special2 SRM6 site_llmissile 15 2
10 Avatar Special1 MachineGun site_ltorsoport 200 1
11 Avatar Special1 MachineGun site_rtorsoport 200 1
12 Battlemaster Special2 PPC site_rdgunport 1
13 Battlemaster Special1 SRM6 site_missile 15 2
14 BattlemasterIIC Special1 ClanSSRM6 site_missile 15 2
15 Behemoth Special1 LargeLaser site_ctorsoport 1
16 BehemothII Special1 LargeLaser site_ctorsoport 1
17 Black Lanner Special2 ClanSRM6 site_rmissileport 15 2 2
18 Black Lanner Special1 ClanLRM10 site_lmissileport 24 2 2
19 Brigand Special2 MediumLaser site_ltorsoport 1
20 Brigand Special1 MediumLaser site_rtorsoport 1
21 Bushwacker Special1 LRM5 site_missileport 24 2 2
22 Bushwacker Special1 LRM5 site_missileport 24 2 2
23 Cauldron-Born Special1 ClanLRM10 site_rmissileport 24 2 2
24 Cauldron-Born Special2 ClanLRM10 site_lmissileport 24 2 2
25 Cyclops Special1 MediumPulseLaser site_gunport 1
26 Deimos Special1 ClanLRM15 site_lmissileport 16 2 2
27 Deimos Special2 ClanLRM15 site_rmissileport 16 2 2
28 Hauptmann Special1 ClanUltraAC20 site_rutorsoport 16 1
29 Hellspawn Special1 SSRM2 site_rmissileport 50 2
30 HollanderII Special2 SmallPulseLaser site_cltorsoport 1
31 HollanderII Special2 SmallPulseLaser site_crtorsoport 1
32 HollanderII Special1 GaussRifle site_rtorsoport 16 1
33 Hunchback Special1 AC10 site_rutorsoport 20 1
34 Loki Special1 ClanSSRM6 site_missileport 15 2
35 Mad Cat Special1 ClanLRM10 site_lmissileport 24 2 2
36 Mad Cat Special2 ClanLRM10 site_rmissileport 24 2 2
37 Mad Cat MKII Special2 ClanLRM15 site_lmissileport 16 2 2
38 Mad Cat MKII Special1 ClanLRM15 site_rmissileport 16 2 2
39 Rifleman Special1 MediumLaser site_ltorsoport 1
40 Rifleman Special1 MediumLaser site_rtorsoport 1
41 Solitaire Special1 ClanERLargeLaser site_rtorsoport 1
42 Thor Special1 CLANLRM10 site_missileport 24 2 2
43 Uziel Special1 LRM10 site_missileport 24 2 2
44 Vulture Special1 ClanMachineGun site_ltorsoport 200 1
45 Vulture Special1 ClanMachineGun site_rtorsoport 200 1
+145
View File
@@ -0,0 +1,145 @@
[mission]
MissionName=Coliseum - Attrition
GameType=2
TeamAllowed=0
Visibility=1
Weather=0
TimeOfDay=0
TimeLimit=15
Radar=0
HeatOn=0
FriendlyFire=1
SplashDamage=1
UnlimitedAmmo=0
NoReturn=0
WeaponJam=1
AdvanceMode=1
ArmorMode=1
[slot0]
Type=1
PilotName=Viper
Mech=Atlas
Team=0
Skin=0
Decal=3
[slot1]
Type=1
PilotName=Blackjack
Mech=Mad Cat
Team=0
Skin=1
Decal=4
[slot2]
Type=1
PilotName=Ghost
Mech=Warhammer
Team=0
Skin=2
Decal=2
[slot3]
Type=1
PilotName=Maverick
Mech=Thor
Team=0
Skin=3
Decal=5
[slot4]
Type=1
PilotName=Reaper
Mech=Loki
Team=0
Skin=4
Decal=6
[slot5]
Type=1
PilotName=Phoenix
Mech=Daishi
Team=0
Skin=5
Decal=9
[slot6]
Type=1
PilotName=Hammer
Mech=Shadow Cat
Team=0
Skin=6
Decal=10
[slot7]
Type=1
PilotName=Blade
Mech=Battlemaster
Team=0
Skin=7
Decal=11
[slot8]
Type=1
PilotName=Ironside
Mech=Cauldron-Born
Team=0
Skin=8
Decal=12
[slot9]
Type=1
PilotName=Tempest
Mech=Vulture
Team=0
Skin=9
Decal=3
[slot10]
Type=1
PilotName=Saber
Mech=Black Hawk
Team=0
Skin=10
Decal=16
[slot11]
Type=1
PilotName=Ajax
Mech=Archer
Team=0
Skin=11
Decal=49
[slot12]
Type=1
PilotName=Thunder
Mech=Kodiak
Team=0
Skin=12
Decal=2
[slot13]
Type=1
PilotName=Raven
Mech=Uziel
Team=0
Skin=13
Decal=50
[slot14]
Type=1
PilotName=Wraith
Mech=Grizzly
Team=0
Skin=14
Decal=48
[slot15]
Type=1
PilotName=Goliath
Mech=Annihilator
Team=0
Skin=15
Decal=5
+146
View File
@@ -0,0 +1,146 @@
[mission]
MissionName=CentralPark - Team KotH
GameType=6
TeamAllowed=1
TeamCount=4
Visibility=1
Weather=0
TimeOfDay=0
TimeLimit=15
Radar=0
HeatOn=0
FriendlyFire=1
SplashDamage=1
UnlimitedAmmo=0
NoReturn=0
WeaponJam=1
AdvanceMode=1
ArmorMode=1
[slot0]
Type=1
PilotName=Viper
Mech=Atlas
Team=0
Skin=0
Decal=3
[slot1]
Type=1
PilotName=Blackjack
Mech=Mad Cat
Team=0
Skin=0
Decal=3
[slot2]
Type=1
PilotName=Ghost
Mech=Warhammer
Team=0
Skin=0
Decal=3
[slot3]
Type=1
PilotName=Maverick
Mech=Thor
Team=0
Skin=0
Decal=3
[slot4]
Type=1
PilotName=Reaper
Mech=Loki
Team=1
Skin=1
Decal=4
[slot5]
Type=1
PilotName=Phoenix
Mech=Daishi
Team=1
Skin=1
Decal=4
[slot6]
Type=1
PilotName=Hammer
Mech=Shadow Cat
Team=1
Skin=1
Decal=4
[slot7]
Type=1
PilotName=Blade
Mech=Battlemaster
Team=1
Skin=1
Decal=4
[slot8]
Type=1
PilotName=Ironside
Mech=Cauldron-Born
Team=2
Skin=2
Decal=2
[slot9]
Type=1
PilotName=Tempest
Mech=Vulture
Team=2
Skin=2
Decal=2
[slot10]
Type=1
PilotName=Saber
Mech=Black Hawk
Team=2
Skin=2
Decal=2
[slot11]
Type=1
PilotName=Ajax
Mech=Archer
Team=2
Skin=2
Decal=2
[slot12]
Type=1
PilotName=Thunder
Mech=Kodiak
Team=3
Skin=3
Decal=5
[slot13]
Type=1
PilotName=Raven
Mech=Uziel
Team=3
Skin=3
Decal=5
[slot14]
Type=1
PilotName=Wraith
Mech=Grizzly
Team=3
Skin=3
Decal=5
[slot15]
Type=1
PilotName=Goliath
Mech=Annihilator
Team=3
Skin=3
Decal=5
+316
View File
@@ -795,7 +795,323 @@ keeping old-RIO (type 0) protocol behavior byte-identical:
alongside `mw4print.exe`. alongside `mw4print.exe`.
- Version bumped to **2.0**, copyright year updated to **2026**. - Version bumped to **2.0**, copyright year updated to **2026**.
## 📋 MFD mode 4: right device stagger fix (2026-07-18, eaa5fd3)
Root cause: `CMFD_Device::BeginScene()` cleared BOTH MFD device back-buffers at `sh_step==0`.
But in mode 4 the right MFD flip also fires at `sh_step==1` (= old `sh_step==0` after the
stagger increment), so it presented a just-cleared buffer — only the grid, no channel data.
Fix: `BeginScene()` now only clears/grids the LEFT device at `sh_step==0`. New `BeginSceneRight()`
(added to `render.cpp` / `render.hpp`, called from `WinMain.cpp`) clears/grids the RIGHT device at
`sh_step==1` — one frame AFTER the right flip — so channels 34 render into a fresh buffer before
the next flip. Mode 1 is unchanged.
Mode 4 cycle (with stagger):
- `sh_step 0`: radar + left `BeginScene` (clear + grid); no flip
- `sh_step 1`: flip right MFD (shows channels 34 from previous cycle) + right `BeginScene` (clear + grid)
- `sh_step 24`: channels 02 → left device
- `sh_step 56`: channels 34 → right device
- `sh_step 0` (next): flip radar + left MFD (shows channels 02 from previous cycle)
Files: `CoreTech/Libraries/GameOS/WinMain.cpp`, `render.cpp`, `render.hpp`.
## 📋 Linux→Windows development workflow (2026-07-19, 55b9bfc5)
`build-env/sync-to-windows.sh` — rsync script that pushes all source, content, toolchain, and
assets to the Windows build machine at `/vwe/firestorm`. Excludes generated build outputs
(`rel.bin/`, `dbg.bin/`, `*.mw4`, `*.dep`), `.git`/LFS objects, `_UNUSED/`, and the game deploy
dir (`MW4/`). Run from Linux before triggering a Windows build.
## 📋 ddraw.dll removed from repo; build script moves it aside (2026-07-19, 0ceba9c7, 24825ff3)
`Gameleap/mw4/ddraw.dll` (DDrawCompat) removed from git (was LFS-tracked); now lives as
`ddraw.dll.old` in the same dir for reference. `deploy-editor.ps1` installs DDrawCompat as
`ddraw.dll` at deploy time (the editor needs it for its windowed D3D7 viewport).
`build-resources.ps1` moves `ddraw.dll` aside (`.buildaside`) before running `MW4pro.exe`
because **DDrawCompat is fatal to MW4pro.exe in BOTH windowed and fullscreen modes**, then
restores it in `finally`. The same block also defensively covers dgVoodoo2 interceptors
(`D3D8/D3D9/D3DImm.dll`) in case they reappear.
## 📋 Branch `5.1.0b-in-progress`: multiplayer + RIO + source cleanup (2026-07-19)
### ConLobby V5.1.0b1 — Super6 mech rotation (c768f7c4)
Changes from Buddy 'Highlight' Taylor (MCHL), merged manually:
- Console version string bumped to **V5.1.0b1**.
- `ROOKIEMECH` defines expanded from 4 → **6 mechs**: added Archer (ID=1) and Warhammer (ID=62).
- 16-slot default assignments cycle through all 6 Super6 mechs.
- Right-click randomizer expanded from `random(0,3)``random(0,5)`.
### CRIOMAIN.CPP — Korean translation + RIO poll timeout scaling (16fca6c4, a712002f)
- Translated all EUC-KR/CP949 Korean developer comments (~35 lines) to English. File re-saved
as clean UTF-8. CRLF line endings preserved (`* -text` in `.gitattributes`).
⚠️ **Encoding hazard:** always read/write CRIOMAIN.CPP as binary (or with an explicit encoding
codec). Python text-mode `readlines()` silently strips `\r`, causing a 3000+-line git diff when
every line's `\r\n` becomes `\n`. If that happens, restore CRLF with `re.sub(b'(?<!\r)\n', b'\r\n', data)` in binary mode, then amend the commit.
- Added **`g_dwRIOPollTimeout`** global (default 50 ms). Computed in `SetupConnection` before the
receive loop using: `clamp(⌈480000/baud⌉, 5, 50)`. Preserves 50 ms at 9600 baud; floors at
5 ms for 115200 baud. Implemented with explicit ternary — **`min`/`max` are undeclared in this
translation unit under VC6** (not pulled in by CRIOMAIN's includes); use ternary or `__min`/`__max`.
### 16 pilots + 1 cameraship in multiplayer (f76dc05f)
`MW4Shell.cpp`:
- `CTCL_DefaultHostSetup` (non-coop): `Environment.NetworkMaxPlayers` set to
`params->m_maxPlayers + (CTCL_GetTeslaCountAll() - CTCL_GetTeslaCount())`.
The delta = camera-only seats (Tesla seats not assigned to pilots), reserving one extra
DirectPlay slot per cameraship so the 17th connection isn't rejected.
- `SetNetworkMissionParamater / PLAYER_LIMIT_PARAMETER`: same formula applied at runtime when
host changes the player limit. `gos_NetServerCommands(gos_Commend_UpdateMaxPlayers)` still called;
`break` must be present in this case (was accidentally dropped once — fall-through to
`JOIN_IN_PROGRESS_PARAMETER` corrupts `m_joinInProgress`).
- COOP branch unchanged (capped at 9+bots; no camera seat needed there).
`ConLobby.script`: launch guard `nTempPlayerCount > 16` raised to `> 17` so the cameraship
connection doesn't trigger "Too many player/bots".
### hsh/ BMP canonical renames (5813aeb6, 2026-07-23)
All `hsh/MFD/*.bmp` and `hsh/Mechs/*.bmp` filenames reconciled against the canonical stems
expected by game code. The engine loads MFD images via `huddamage.cpp` `texturename[]` array
(lowercase, no spaces) and Mechs portraits via `GetLocString` DNL strings (mixed-case with
spaces). Any mismatch = silently missing image at runtime.
Key renames:
- `hsh/MFD/assassinii.bmp``assassin2.bmp` (matches `texturename[]` canonical)
- `hsh/Mechs/battlemasteriic.bmp``battlemaster iic.bmp`
- `hsh/Mechs/mad cat mk.ii.bmp``mad cat mkii.bmp`
- `hsh/Mechs/behemoth ii.bmp` added (was absent)
Most other files in both directories are LFS pointer updates only (content unchanged).
### RookieMission configurable defaults via options.ini (5813aeb6, 2026-07-23)
CTCL (console) arcade mode has a "Rookie Mission" quick-launch that previously hardcoded all
game params. Now all 13 params are overridable from an `[RookieMission]` section in
`options.ini` (read by `CTCL_SetCDSP` at startup):
- `MW4Shell.cpp`: added 14 `g_` globals (`g_szRookieMission`, `g_nRookieGameType`, and 12
numeric params); registered as `gosScript_RegisterVariable` in `StartUp`/`ShutDown`;
`CTCL_SetCDSP` reads the `[RookieMission]` page via `NotationFile` and populates them.
Defaults: `"ScarabStronghold - Attrition"`, GameType=2 (Attrition), UnlimitedAmmo=1, all
others zero. `g_nRookieTimeLimit=-1` means "use the server's current time setting".
- `ConLobbyMission.script`: all hardcoded values in `MAIL_SET_ROOKIE_MISSION` handler replaced
with `$$g_szRookieMission$$` / `$$g_nRookieXxx$$` references.
- Requires rebuild: `MW4.exe` (Release + Profile).
### Mechlab turn rate label (5813aeb6, 2026-07-23)
`StringResource.rc` `IDS_ML_CH_TURNRATE`: "Turn Rate (Degrees/Sec.):" →
"Turn Rate (Top Speed Rad/Sec):" to match the actual `.data` field semantics
(`TopSpeedTurnRate` is in rad/s at top speed, not deg/s).
Requires rebuild: `ScriptStrings.dll`.
### BTFrstrm design documentation (840bc96c, 2026-07-23)
Two Word documents added to `BTFrstrm/`:
- `MechDependencyTree.docx` — dependency relationships between mech chassis/variants.
- `Special_Zones.docx` — documentation of special zone types used in maps/missions.
### mech_loadouts.md: MechEditor data model (0344418a, 2026-07-23)
`BTFrstrm/mech_loadouts.md` extended with a full reference section documenting the
MechEditor web app (`/home/rich/Repositories/MechEditor/mech_editor.py`, localhost:8765):
every `.data`/`.instance`/`.subsystems` field parsed, conversions performed (m/s ↔ kph,
rad/s ↔ deg/s), and `hsh/` naming rules for MFD/Mechs/HUD/Radar images. Canonical
reference for future mech data work.
### Load File autoconfig stabilization + docs update (2026-07-24)
End-to-end Load File flow for the console lobby was stabilized and verified with a
full-delta regression INI (all mission options + all 16 slots changed away from rookie
defaults), then round-tripped back via the Default button.
Key fixes in `ConLobby.script` / `ConLobbyMission.script`:
- Eliminated first-click vs second-click drift by moving auto-load to a dedicated
deterministic mission path and preventing redraw-time decal mutation.
- Corrected decal handling: map INI decal IDs to lobby dropdown indices, clamp invalid
indices, and show actual decal IDs in labels.
- Corrected mission+map sequencing: game type now rebuilds map list first; mission name
then resolves against that list (with fallback to map index 0).
- Fixed option-state application so first click applies all options (visibility/weather/
time/radar/heat/friendly fire/splash/unlimited/jam/advance/armor) without needing a
second click.
- Fixed Weapon Jam checkbox visibility refresh when set via Load File (UI now re-inits
correctly when heat/advanced states are applied).
New supported autoconfig key:
- Added `NoReturn=0|1` under `[mission]`:
- parser + script variable in `MW4Shell.cpp` (`g_nAutoNoReturn`),
- application in `ConLobbyMission.script` (`RESPAWN_LIMIT_PARAMETER`),
- docs + sample INIs updated.
Docs updates:
- `BTFrstrm/autoconfig-file-spec.html` corrected for real behavior:
- missing mission keys/section apply defaults (not current UI values),
- missing `MissionName` now falls back to first map for that game type,
- added `NoReturn` semantics and examples.
## 📋 STEP 10: Four-monitor MFD bring-up, display diagnostics, and the native-DirectDraw
## exclusive-mode wall (2026-07-25)
A long, dense session on the new 4-monitor bench (`MR_new`: AMD FirePro W4100, 4 outputs,
Win10). Everything below is empirical — measured on real hardware, not inferred.
### The root problem that made all of this hard
`CHSH_Device::InitFirst` / `InitSecond` (`CoreTech\Libraries\GameOS\render.cpp`) **discarded
every single `HRESULT`** (`SetCooperativeLevel`, `SetDisplayMode`, `CreateSurface`,
`GetAttachedSurface`, `QueryInterface`, `CreateDevice`) and **returned `true`
unconditionally**. A panel that failed to open produced no error, no crash and no log entry —
the monitor simply stayed on the desktop. SPEW is compiled out of shipping builds, so there
was no way to see any of it. **Fixing that visibility was what unblocked the whole session.**
### ✅ Diagnostics added (KEEP THESE — they are why everything below was findable)
- **`gos-displays.txt`** (written next to the exe by `VideoCard.cpp` `LogDisplayDevices()`):
`NumDevices`/`NumHWDevices`/`NumMonitors`, every DirectDraw device + `hw_rasterization`,
the role assignment (`FullScreenDevice`/`g_nNonDualHead`/`g_nDualHead`/`g_nDualHead2`/
`g_nMFD1`/`g_nMFD2`), `-tmon` APPLIED/REJECTED per slot, and the decisive
"mode 4 requires BOTH mfd1 and mfd2" line.
- **Per-call HRESULT logging** in `InitFirst`/`InitSecond` via `HSH_LogInit()` / `HSH_CheckHR()`
/ `HSH_HRName()` (27 `DDERR_*` codes decoded by name). Appends to the same file; each line is
written and flushed individually so **the log survives a crash**.
- **`gos-fps.txt`** — new **`-fps`** switch (`WinMain.cpp` `GOS_LogFrameRate`, hooked onto the
existing `frameRate` global). Per-second: frames, avg fps, **5% low**, worst frame in ms,
and a count of frames over 2x average ("hitches"), plus a **session summary** at exit with
true whole-session 1% / 0.1% lows (computed from a 0.5 ms-bucket histogram). Works in
**Release** — the engine's own `AddDebugData("FrameRate")` is `#ifdef LAB_ONLY`
(`MWMission.cpp`) so it only exists in `MW4pro.exe`. Off unless `-fps` is given (a global
read + branch when absent, no file created).
- ⚠️ **Lesson from the first real run: the instrument was measuring itself.** The original
version wrote one line per second straight to the file. The frame time is recorded
*before* the line is emitted, so the `WriteFile` cost landed in the **next** frame —
producing exactly one inflated frame every second (median worst-frame 24.4 ms against a
16.7 ms vsync interval, in 82% of seconds). Output is now buffered in memory and flushed
only when full or at exit (`atexit`), so a normal session performs no writes while
running. Trade-off: a hard crash loses the un-flushed tail — `gos-displays.txt` is the
crash-survivable log, this one is a measurement instrument.
- Also fixed: a "1% low" over 60 samples/sec degenerates to `nFrames/100 = 0` → clamped to
1 → literally the worst frame restated, so the column was redundant. Per-second is now a
5% low (worst 3-of-60); true 1% / 0.1% lows are in the session summary. And a single
frame longer than a second (a level load) no longer spills into following buckets and
emits a run of bogus one-frame rows.
### ✅ Crash-safety fix (independent of everything else, worth keeping)
`hsh_initialized` was set **unconditionally** after panel init. A failed panel therefore left
null surfaces and a null `IDirect3DDevice7` behind, and the per-frame path called straight
through them. Now: all four `InitSecond` overrides (`CMR`/`CRadar`/`CMFD`/`CMFDRight`) bail on
base failure, `CMFD_Device::InitFirst` reports the real result instead of always `true`, and
`hsh_initialized` is only set when the panels genuinely came up → **the game runs without MFDs
instead of dying**.
- **Crash signature to recognise:** `call [ecx+0x44]` with `ECX=0` = `IDirectDrawSurface7::GetDC`
on a never-created surface (verified against `build-env\dx7asdk\include\ddraw.h` vtable order).
Reported as `EXCEPTION : Attempt to read from address 0x00000044`.
### ✅ AppCompat shim — the single most important operational fact
**`DWM8And16BitMitigation` is keyed on the executable's FULL PATH.** A copy of the game at a
new path silently loses it. MW4 renders at `bitdepth=16` and **modern GPUs expose ZERO 16-bit
display modes** — the shim *synthesises* them (proved directly: crash dump shows
`16 bit modes :` EMPTY without the shim, populated with it).
- **Without the shim the error message actively misleads.** GameOS raises
`GOS_DXRASTERIZER_NOFULLSCREEN` — *"Another application is preventing use of full screen
mode"* (`DXRasterizer.cpp:~1125`). That is a **catch-all** fired after every `SetDisplayMode`
attempt fails; it even scans for NetMeeting. It sends you hunting for a conflicting program
that does not exist. The real cause is the missing shim / absent 16-bit modes.
- **NEW: `build-env\set-appcompat.ps1` + `set-appcompat.bat`** — self-locating (`$PSScriptRoot`)
one-click installer. Applies the layer to `MW4.exe`/`MW4pro.exe`/`MW4Ed2.exe` sitting next to
it, wherever that install lives; HKCU always, HKLM too if elevated (note the HKLM value format
differs — it carries a leading `$` marker). Verifies by reading back; detects the
`HIGHDPIAWARE`-only entry that *suppresses* the auto-shim (the original STEP 5 bug).
`-Remove` / `-WhatIfOnly` supported. **`deploy-mw4.ps1` now ships both into every deployment.**
### ✅ Exclusive fullscreen DOES work on Win10 without dgVoodoo2
Confirmed on the W4100 with system `ddraw.dll` and dgVoodoo2 physically removed: `MW4.exe`
alt-enters to exclusive fullscreen correctly **once the shim is applied to that exe path**.
Windowed mode also works natively with no shim at all (windowed sets
`Environment.bitDepth = DesktopBpp` = 32, so there is no mode switch). Console/shell confirmed
windowed; a **full mission windowed is still untested**.
### ❌ Native multi-monitor MFD is BLOCKED — and no flag combination fixes it
The panels' cooperative-level call was genuinely wrong (a latent 2002 bug): every panel asked to
be **both** the process focus window **and** its own device window, on the one shared `hWindow`,
*after* the main device had already taken exclusive mode on it. The main device
(`DXRasterizer.cpp:~1027`) already uses the correct two-call idiom
(`SETFOCUSWINDOW` alone, then `EXCLUSIVE|FULLSCREEN`) — tagged `//sanghoon`, same author. The
panels never were.
A new **`-tcoop <0-5>`** switch was added to test every plausible form on real hardware without a
rebuild between attempts. Results (no dgVoodoo2, shim applied, `-tmfds 4`):
| `-tcoop` | Flags | Result |
|---|---|---|
| 0 | `SETFOCUSWINDOW\|CREATEDEVICEWINDOW\|ALLOWREBOOT\|EXCLUSIVE\|FULLSCREEN` (legacy) | `DDERR_EXCLUSIVEMODEALREADYSET` |
| 1 | `CREATEDEVICEWINDOW\|EXCLUSIVE\|FULLSCREEN` (no focus claim) | `DDERR_INVALIDPARAMS` |
| 2 | `SETFOCUSWINDOW`, then `CREATEDEVICEWINDOW\|…` | `DDERR_INVALIDPARAMS` |
| 3 | `SETFOCUSWINDOW`, then `EXCLUSIVE\|FULLSCREEN` | 1st panel collides; **that collision steals exclusive from the main display**, after which panels 2+3 fully init (radar reached `CreateDevice(HAL) = DD_OK`). Side effect: desktop left at 1920x1080 **16bpp**. Not viable. |
| 4 | `EXCLUSIVE\|FULLSCREEN` only | `DDERR_EXCLUSIVEMODEALREADYSET` on **all** panels |
| 5 | `ALLOWREBOOT\|EXCLUSIVE\|FULLSCREEN` | `DDERR_EXCLUSIVEMODEALREADYSET` on **all** panels |
**CONCLUSION: on modern Windows only ONE DirectDraw object per process may hold exclusive
fullscreen.** The main display takes it; every secondary panel is refused. XP allowed multiple;
**dgVoodoo2 allows it because it is a full reimplementation of ddraw, not bound by that rule.**
=> **dgVoodoo2 cannot be removed for `-tmfds 1/3/4` by fixing flags.** Default stays `-tcoop 0`.
(`-tcoop` is retained: it is how this was settled and will re-settle it on different hardware.)
### ✅ Working 4-monitor config (WITH dgVoodoo2)
- **dgVoodoo2 Scaling mode MUST be "Stretched, Keep Aspect Ratio".** Plain "Stretched" fails
*silently*: main + radar go fullscreen black, both MFD monitors keep showing the desktop, and
**every DirectDraw call still returns `DD_OK`** — the devices are alive but dgVoodoo2 never
drives those outputs. Diagnosed with a temporary per-panel colour-flash test (since removed).
- Device index → physical monitor is **1:1** on this bench (colour test confirmed): main=0,
radar=1, mfd-left=2, mfd-right=3, so `-tmon 1,2,3,4` is identical to auto-detection.
- **Confirmed working end-to-end: all three secondary panels present, full mission played.**
- ⚠️ **dgVoodoo2 is deliberately NOT in the repo and must NOT be — it breaks Windows XP
deployments.** The pod fleet is mixed: XP pods need native DirectDraw and nothing else, while
Win10/11 pods need dgVoodoo2 for any MFD mode. Shipping it in the build (or having
`deploy-mw4.ps1` place it) would push a Win10-only dependency onto XP machines and break them.
It stays an **installed-per-machine, OS-dependent prerequisite** set up by the pod owner on
Win10/11 only. `dgVoodoo.conf`/`dgVoodooCpl.exe` were removed in `0ceba9c7` for this reason —
do not "fix" that by re-adding them. The required setting (Scaling mode
`Stretched, Keep Aspect Ratio`, on the **General** tab) is documented in the pod-owner release
notes instead.
### 📐 Engine is 4:3 ONLY (relevant to every display decision)
`ImageHlp.cpp:~464` asserts the complete supported resolution set: **640x480, 512x384, 800x600,
960x720, 1024x768, 1280x1024 (5:4), 1600x1200**. There is no 16:9 mode and **no aspect
correction anywhere** in the codebase (the only `aspect` hits are texture-dimension caps and
CameraShip). On a 16:9 monitor a 4:3 image must be adapted by the scaler: plain stretch =
distorted (circles → ovals, reticle wrong); keep-aspect = correct geometry + pillarbox bars.
Native widescreen would require re-authoring every 2D/shell layout (all fixed 640x480 pixel
coordinates) — a content project, not a code tweak.
### 📐 Assessment: moving to borderless windowed (discussed, NOT started)
The only native path to multi-monitor, since it removes exclusive mode from the picture entirely.
Smaller than it sounds because the mechanisms already exist:
- The windowed present is already `wBlt(FrontBufferSurface, &Window, BackBufferSurface, …)`
a blit to an arbitrary screen rect. `Blt` stretches when the dest rect differs in size, so
aspect-preserving borderless output is **rect maths, not new machinery**.
- Clipper wrappers (`wCreateClipper`/`wSetClipper`/`wSetHWnd`) already exist in `DirectDraw.cpp`,
unused by the fullscreen path.
- **None of the drawing code changes** — panels already render into `pDDSTarget` and composite;
`DrawQuad`/`DrawTexture`/fonts/`SwapRightState` don't care whether the present is Flip or Blt.
- Work is confined to `render.cpp`, `DXRasterizer.cpp`, `WinMain.cpp` (+ a little `Windows.cpp`):
panel init → `DDSCL_NORMAL` + clipper + offscreen render surface (~150 lines); ~4 panel `Flip`
sites → `Blt`; break the `HSH_EnterFullScreen2()``EnterFullScreenMode()` coupling
(`DXRasterizer.cpp:~1132` is its ONLY call site, which is also why windowed mode currently
creates no panels at all); **new: explicit frame pacing** (windowed Blt has no vsync — this is
the same root cause as the known mechlab fast-spin bug).
- **Gains:** no dgVoodoo2, no shim, no 16-bit dependency, no exclusive-mode contention, aspect
under our control, and panels stay at their monitor's native mode so "this panel won't accept
640x480" becomes impossible.
- **Risk that decides it:** windowed D3D7 device creation is per-GPU (the *editor* hit
`DDERR_INVALIDOBJECT` on Win11 and needed DDrawCompat; the *game* succeeded natively on the
W4100). Version lockstep means one build must serve every pod.
- **Recommended staging:** (1) `-borderless` for the MAIN display only, default off; (2) convert
**one** panel (radar) — the small, make-or-break test of `DDSCL_NORMAL` + clipper +
D3D-on-offscreen on the target GPU; (3) all three panels; (4) retire dgVoodoo2 + shim. Keep
exclusive fullscreen switch-selectable indefinitely.
### Incidental findings
- **`-2dt` is not a recognised switch anywhere in the codebase**, despite appearing in production
`ctcl.ini` launch lines. Completely inert. (2D targets are already the default; `-3dt` is what
switches to the 3D model.)
- `NumHWDevices` (5) can exceed `NumDevices` (4) — it counts D3D device-enumeration callbacks, and
an adapter exposing both a HAL and a T&L HAL yields two. Benign; `InitSecond` hardcodes
`IID_IDirect3DHALDevice`.
- New switches are documented in `-help` (`-fps` under LOGGING AND DIAGNOSTICS, `-tcoop` under
DISPLAY AND VIDEO).
## Next steps (proposed) ## Next steps (proposed)
- [ ] (Decision pending) Borderless-windowed migration — see the staged assessment in STEP 10.
Step 2 (one panel) is the cheap, decisive experiment. Note this is the only route that would
remove the dgVoodoo2 prerequisite **without** breaking the XP pods, since it needs no external
DLL on either OS.
- [ ] Test a **full mission windowed** (only the console/shell has been verified windowed).
- [x] ~~Windowed 3D viewport on Win11~~ — DONE via DDrawCompat (see STEP 8 viewport section). - [x] ~~Windowed 3D viewport on Win11~~ — DONE via DDrawCompat (see STEP 8 viewport section).
- [ ] (Optional) Curate remaining WIP content as features are exercised (editor loads dev `Content\`). - [ ] (Optional) Curate remaining WIP content as features are exercised (editor loads dev `Content\`).
- [ ] (Optional) Build Debug/Armor configs (need `dbg.bin`/`arm.bin` output dirs). - [ ] (Optional) Build Debug/Armor configs (need `dbg.bin`/`arm.bin` output dirs).
@@ -48,6 +48,10 @@ int g_nDualHead2 = -1; // jcem
int g_nNonDualHead = -1; // jcem int g_nNonDualHead = -1; // jcem
int g_nMFD1 = -1; // mode 4: left 640x480 MFD monitor int g_nMFD1 = -1; // mode 4: left 640x480 MFD monitor
int g_nMFD2 = -1; // mode 4: right 640x480 MFD monitor int g_nMFD2 = -1; // mode 4: right 640x480 MFD monitor
// [tmon] Operator override for display-device selection, in Main/Radar/MFD1/MFD2 order.
// -1 = leave that slot to auto-detection. Populated by the -tmon command-line switch,
// which is parsed in MW4Application's WinMain - that runs before FindVideoCards().
int g_naMonitorOverride[4] = { -1, -1, -1, -1 };
// MSL 5.03 Mechview // MSL 5.03 Mechview
int g_nMechViewType; // jcem : 0 - no mechview, 1 - on radar screen, 2 - on main screen int g_nMechViewType; // jcem : 0 - no mechview, 1 - on radar screen, 2 - on main screen
@@ -277,6 +281,129 @@ HRESULT CALLBACK EnumModesCallback2( LPDDSURFACEDESC2 lpDDSurfaceDesc, LPVOID lp
// //
// //
// Called once at startup, enumerate and get caps of all 3D video cards // Called once at startup, enumerate and get caps of all 3D video cards
//
// [tmon] Apply a hard-assigned display device, but only if the operator supplied one
// and it names a device that actually exists. Anything else is ignored, so a stale
// -tmon left on the command line of a machine with fewer monitors simply falls back
// to auto-detection instead of breaking startup. Rejections are reported in
// gos-displays.txt - silently doing nothing is worse than useless when the operator
// is trying to work out why a monitor did not light up.
//
static void ApplyMonitorOverride(int slot, int* pTarget)
{
int dev = g_naMonitorOverride[slot];
if ((dev >= 0) && (dev < (int)NumDevices))
*pTarget = dev;
}
//
// [displaylog] Append one printf-style line to the display report.
//
static void EmitDisplayLine( HANDLE hFile, const char* fmt, ... )
{
char szLine[512];
DWORD dwWritten=0;
va_list ap;
va_start( ap, fmt );
vsprintf( szLine, fmt, ap );
va_end( ap );
WriteFile( hFile, szLine, (DWORD)strlen(szLine), &dwWritten, NULL );
}
//
// [displaylog] Write gos-displays.txt next to the executable, describing every
// DirectDraw device GameOS found and which display role each one was assigned.
//
// This exists because there is no other way to see it: the SPEW macros are compiled
// out of shipping builds, and a monitor that simply never lights up gives no clue
// whether the device was missing, lacked hardware caps, or was assigned elsewhere.
//
static void LogDisplayDevices()
{
char szDir[MAX_PATH];
char szPath[MAX_PATH];
szDir[0]='\0';
GetModuleFileNameA( NULL, szDir, sizeof(szDir) );
char* pSlash=strrchr( szDir, '\\' );
if( pSlash )
*(pSlash+1)='\0';
else
szDir[0]='\0';
sprintf( szPath, "%sgos-displays.txt", szDir );
HANDLE hFile=CreateFileA( szPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile==INVALID_HANDLE_VALUE )
return;
EmitDisplayLine( hFile, "GameOS display device enumeration\r\n" );
EmitDisplayLine( hFile, "=================================\r\n\r\n" );
EmitDisplayLine( hFile, "NumDevices = %d (DeviceArray holds at most 8)\r\n", (int)NumDevices );
EmitDisplayLine( hFile, "NumHWDevices = %d\r\n", (int)NumHWDevices );
EmitDisplayLine( hFile, "NumMonitors = %d\r\n\r\n", (int)NumMonitors );
for( DWORD t0=0; t0<NumDevices; t0++ )
{
EmitDisplayLine( hFile, " [%d] %-40s hw_rasterization=%s\r\n",
(int)t0,
DeviceArray[t0].DDid.szDescription,
(DeviceArray[t0].D3DCaps.dwDevCaps & D3DDEVCAPS_HWRASTERIZATION) ? "yes" : "NO" );
}
EmitDisplayLine( hFile, "\r\nAssigned roles (-1 means not found):\r\n" );
EmitDisplayLine( hFile, " main (FullScreenDevice) = %d\r\n", Environment.FullScreenDevice );
EmitDisplayLine( hFile, " radar (g_nNonDualHead) = %d\r\n", g_nNonDualHead );
EmitDisplayLine( hFile, " span (g_nDualHead) = %d\r\n", g_nDualHead );
EmitDisplayLine( hFile, " span2 (g_nDualHead2) = %d\r\n", g_nDualHead2 );
EmitDisplayLine( hFile, " mfd1 (g_nMFD1) = %d\r\n", g_nMFD1 );
EmitDisplayLine( hFile, " mfd2 (g_nMFD2) = %d\r\n", g_nMFD2 );
// Report what -tmon asked for and whether each slot could be honoured.
{
static const char* apszRole[4] = { "main", "radar", "mfd1", "mfd2" };
bool bAny=false;
for( int s=0; s<4; s++ )
if( g_naMonitorOverride[s] >= 0 )
bAny=true;
if( bAny )
{
EmitDisplayLine( hFile, "\r\n-tmon override:\r\n" );
for( int s2=0; s2<4; s2++ )
{
int dev=g_naMonitorOverride[s2];
if( dev < 0 )
EmitDisplayLine( hFile, " %-5s : auto\r\n", apszRole[s2] );
else if( dev < (int)NumDevices )
EmitDisplayLine( hFile, " %-5s : device %d APPLIED\r\n", apszRole[s2], dev );
else
EmitDisplayLine( hFile, " %-5s : device %d *** REJECTED - only %d device(s) exist ***\r\n",
apszRole[s2], dev, (int)NumDevices );
}
}
else
{
EmitDisplayLine( hFile, "\r\n-tmon override: not supplied (all roles auto-detected)\r\n" );
}
}
// The decisive line: whether the MFD subsystem will initialise at all.
EmitDisplayLine( hFile, "\r\nMFD mode (-tmfds) = %d\r\n", g_nTypeOfMFDs );
if( g_nTypeOfMFDs == 4 )
{
bool bOK = (g_nMFD1 != -1) && (g_nMFD2 != -1);
EmitDisplayLine( hFile, "mode 4 requires BOTH mfd1 and mfd2: %s\r\n", bOK ? "OK" : "*** FAILED ***" );
if( !bOK )
EmitDisplayLine( hFile,
" -> IsMultimonitorAvaliable() returns false, so HSH_EnterFullScreen2()\r\n"
" falls back to the single-secondary-monitor path and the two MFD\r\n"
" displays are never opened. Four hardware-rasterizing DirectDraw\r\n"
" devices are needed: main, radar, mfd1, mfd2.\r\n" );
}
CloseHandle( hFile );
}
// //
// //
void FindVideoCards() void FindVideoCards()
@@ -382,6 +509,10 @@ void FindVideoCards()
} }
} }
} }
// [tmon] Hard-assigned Main/Radar overrides. Applied BEFORE the mode 4 MFD search
// below so that search still skips whichever devices the operator picked.
ApplyMonitorOverride(0, &Environment.FullScreenDevice);
ApplyMonitorOverride(1, &g_nNonDualHead);
// Mode 4 (split dual 640x480): find two extra D3D devices beyond // Mode 4 (split dual 640x480): find two extra D3D devices beyond
// FullScreenDevice and g_nNonDualHead. These need no special resolution. // FullScreenDevice and g_nNonDualHead. These need no special resolution.
{ {
@@ -396,6 +527,13 @@ void FindVideoCards()
} }
} }
} }
// [tmon] Hard-assigned MFD overrides (only meaningful with -tmfds 4).
ApplyMonitorOverride(2, &g_nMFD1);
ApplyMonitorOverride(3, &g_nMFD2);
// [displaylog] Write gos-displays.txt with the full enumeration and the role
// assignment. SPEW is compiled out of shipping builds, so a file is the only way
// the operator can see why a display did or did not get picked up.
LogDisplayDevices();
// jcem // jcem
// //
// Check any known video cards have up to date drivers (Only on Windows9x) // Check any known video cards have up to date drivers (Only on Windows9x)
@@ -20,6 +20,7 @@
#include "MemoryManager.hpp" #include "MemoryManager.hpp"
#include "DirectX.hpp" #include "DirectX.hpp"
#include "FileIO.hpp" #include "FileIO.hpp"
#include <stdlib.h> // [fpslog] atexit() for the frame pacing summary
#include "ControlManager.hpp" #include "ControlManager.hpp"
#include "LocalizationManager.hpp" #include "LocalizationManager.hpp"
#include "Time.hpp" #include "Time.hpp"
@@ -841,6 +842,257 @@ HRESULT App_Render( LPDIRECT3DDEVICE7 pd3dDevice,LPDIRECTDRAWSURFACE7 pddsTextur
//sanghoon end //sanghoon end
//
// [fpslog] Frame pacing report, written to gos-fps.txt next to the executable.
//
// Off unless the -fps switch is given, so a normal pod run does no extra work at all.
// Set from MW4Application's command-line parser.
//
int g_nFpsLog = 0;
// The engine's own FrameRate readout is inside #ifdef LAB_ONLY, so it exists only in
// MW4pro.exe. This works in Release builds, which is what actually runs on the pods.
//
// Average FPS on its own hides stutter: 60 fps average is indistinguishable from 60 fps
// with a dropped frame every second. So this also reports the slowest frames, which is
// where rhythmic hitching shows up.
//
// IMPORTANT - why the output is buffered:
// The first version wrote one line per second straight to the file. That put a WriteFile
// on the render thread every second, and its cost landed in the NEXT frame's measurement
// (the frame time is recorded before the line is emitted). The result was exactly one
// inflated frame per second, forever - the tool was reporting its own overhead as the
// game's worst frame. Everything is now accumulated in memory and flushed only when the
// buffer fills or at exit, so a normal session performs no writes at all while running.
// Trade-off: a hard crash loses the un-flushed tail. gos-displays.txt is the
// crash-survivable log; this one is a measurement instrument.
//
static char s_szFpsBuf[131072]; // ~2000 lines, about 33 minutes at 1 line/sec
static int s_nFpsBufUsed = 0;
static HANDLE s_hFpsFile = INVALID_HANDLE_VALUE;
static int s_bFpsAtExit = 0;
// Whole-session frame time histogram in 0.5 ms buckets (0 - 1000 ms). Lets the true
// session-wide percentiles be computed at exit without retaining every frame time.
#define FPS_HIST_BUCKETS 2000
static DWORD s_adwFpsHist[FPS_HIST_BUCKETS];
static DWORD s_dwFpsFrames = 0;
static double s_dFpsTotalMS = 0.0;
static DWORD s_dwFpsHitches = 0;
static void GOS_FpsFlush( void )
{
DWORD dwWritten=0;
if( s_nFpsBufUsed<=0 )
return;
if( s_hFpsFile==INVALID_HANDLE_VALUE )
{
char szDir[MAX_PATH];
char szPath[MAX_PATH];
szDir[0]='\0';
GetModuleFileNameA( NULL, szDir, sizeof(szDir) );
char* pSlash=strrchr( szDir, '\\' );
if( pSlash )
*(pSlash+1)='\0';
else
szDir[0]='\0';
sprintf( szPath, "%sgos-fps.txt", szDir );
// FILE_SHARE_READ so the file can be read while the game is still running.
s_hFpsFile=CreateFileA( szPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( s_hFpsFile==INVALID_HANDLE_VALUE )
{
s_nFpsBufUsed=0; // drop it rather than retry every second
return;
}
}
WriteFile( s_hFpsFile, s_szFpsBuf, (DWORD)s_nFpsBufUsed, &dwWritten, NULL );
s_nFpsBufUsed=0;
}
// Mean frame time (ms) of the slowest dwCount frames of the whole session.
static double GOS_FpsSlowestMean( DWORD dwCount )
{
double dSum=0.0;
DWORD dwSeen=0;
int b;
if( dwCount<1 )
dwCount=1;
for( b=FPS_HIST_BUCKETS-1; b>=0 && dwSeen<dwCount; b-- )
{
DWORD dwTake=s_adwFpsHist[b];
if( !dwTake )
continue;
if( dwTake>dwCount-dwSeen )
dwTake=dwCount-dwSeen;
dSum += (((double)b+0.5)/2.0)*(double)dwTake;
dwSeen += dwTake;
}
return dwSeen ? (dSum/(double)dwSeen) : 0.0;
}
static void GOS_FpsAppend( const char* pszLine )
{
int nLen=(int)strlen(pszLine);
if( nLen<=0 )
return;
if( s_nFpsBufUsed+nLen > (int)sizeof(s_szFpsBuf) )
GOS_FpsFlush(); // buffer full: one write, rarely
if( s_nFpsBufUsed+nLen > (int)sizeof(s_szFpsBuf) )
return; // still no room: give up quietly
memcpy( s_szFpsBuf+s_nFpsBufUsed, pszLine, nLen );
s_nFpsBufUsed += nLen;
}
// NOTE: must be __cdecl. GameOS builds with /Gz (__stdcall by default), but atexit()
// takes a __cdecl callback, so without this the compiler rejects it with C2664.
static void __cdecl GOS_FpsAtExit( void )
{
char szLine[256];
if( s_dwFpsFrames )
{
// True session-wide percentiles, which a per-second bucket cannot give:
// 1% of 60 frames is 0, so a "1% low" computed per second degenerates into
// "the single worst frame". Over a whole session it is meaningful.
double dAvgMS = s_dFpsTotalMS/(double)s_dwFpsFrames;
double dOnePct = GOS_FpsSlowestMean( s_dwFpsFrames/100 );
double dTenthPct= GOS_FpsSlowestMean( s_dwFpsFrames/1000 );
GOS_FpsAppend( "\r\n" );
GOS_FpsAppend( "session summary\r\n" );
GOS_FpsAppend( "---------------\r\n" );
sprintf( szLine, " frames : %lu\r\n", (unsigned long)s_dwFpsFrames );
GOS_FpsAppend( szLine );
sprintf( szLine, " elapsed : %.1f s\r\n", s_dFpsTotalMS/1000.0 );
GOS_FpsAppend( szLine );
sprintf( szLine, " average : %.1f fps (%.2f ms)\r\n",
(dAvgMS>0.0)?1000.0/dAvgMS:0.0, dAvgMS );
GOS_FpsAppend( szLine );
sprintf( szLine, " 1%% low : %.1f fps (%.2f ms)\r\n",
(dOnePct>0.0)?1000.0/dOnePct:0.0, dOnePct );
GOS_FpsAppend( szLine );
sprintf( szLine, " 0.1%% low : %.1f fps (%.2f ms)\r\n",
(dTenthPct>0.0)?1000.0/dTenthPct:0.0, dTenthPct );
GOS_FpsAppend( szLine );
sprintf( szLine, " frames over 2x avg: %lu\r\n", (unsigned long)s_dwFpsHitches );
GOS_FpsAppend( szLine );
}
GOS_FpsFlush();
if( s_hFpsFile!=INVALID_HANDLE_VALUE )
{
CloseHandle( s_hFpsFile );
s_hFpsFile=INVALID_HANDLE_VALUE;
}
}
static void GOS_LogFrameRate( float fFrameRate )
{
enum { MAX_WORST = 64 };
static float s_afWorst[MAX_WORST]; // longest frames this second, descending ms
static int s_nWorst = 0;
static float s_fAccumMS= 0.0f;
static float s_fSumMS = 0.0f;
static float s_fMaxMS = 0.0f;
static int s_nFrames = 0;
static int s_nSecond = 0;
char szLine[256];
if( !g_nFpsLog ) // [fpslog] -fps not given: do nothing
return;
if( fFrameRate<=0.0f )
return;
if( !s_bFpsAtExit )
{
s_bFpsAtExit=1;
atexit( GOS_FpsAtExit ); // flush the buffer + write the summary
}
float fMS = 1000.0f/fFrameRate;
s_nFrames++;
s_fSumMS += fMS;
s_fAccumMS += fMS;
if( fMS>s_fMaxMS )
s_fMaxMS = fMS;
// Whole-session histogram (0.5 ms buckets, saturating at the top bucket).
int nBucket=(int)(fMS*2.0f);
if( nBucket<0 )
nBucket=0;
if( nBucket>=FPS_HIST_BUCKETS )
nBucket=FPS_HIST_BUCKETS-1;
s_adwFpsHist[nBucket]++;
s_dwFpsFrames++;
s_dFpsTotalMS += fMS;
// Maintain the longest frames of this second, sorted longest-first.
if( s_nWorst<MAX_WORST )
s_afWorst[s_nWorst++] = fMS;
else if( fMS>s_afWorst[MAX_WORST-1] )
s_afWorst[MAX_WORST-1] = fMS;
for( int i=s_nWorst-1; i>0 && s_afWorst[i]>s_afWorst[i-1]; i-- ) {
float fTmp=s_afWorst[i]; s_afWorst[i]=s_afWorst[i-1]; s_afWorst[i-1]=fTmp;
}
if( s_fAccumMS<1000.0f )
return;
float fAvgMS = s_fSumMS/(float)s_nFrames;
float fAvgFPS = 1000.0f/fAvgMS;
// 5% low, not 1%: at 60 samples a "1% low" is a single frame, which is just the
// worst-frame column restated. Worst 3-of-60 is a number that actually differs.
int nLow = s_nFrames/20;
if( nLow<3 )
nLow = 3;
if( nLow>s_nWorst )
nLow = s_nWorst;
float fLowSum=0.0f;
for( int j=0; j<nLow; j++ )
fLowSum += s_afWorst[j];
float fLowFPS = (fLowSum>0.0f) ? (1000.0f/(fLowSum/(float)nLow)) : 0.0f;
// Frames that took more than twice the average - i.e. visible hitches.
int nHitch=0;
for( int k=0; k<s_nWorst; k++ )
if( s_afWorst[k]>fAvgMS*2.0f )
nHitch++;
s_dwFpsHitches += nHitch;
if( s_nSecond==0 )
GOS_FpsAppend( " sec frames avg fps 5% low worst frame hitches\r\n"
" --- ------ ------- ------ ----------- -------\r\n" );
sprintf( szLine, "%5d %6d %8.1f %8.1f %8.1f ms %s%d\r\n",
s_nSecond, s_nFrames, fAvgFPS, fLowFPS, s_fMaxMS,
nHitch ? "<-- " : "", nHitch );
GOS_FpsAppend( szLine );
s_nSecond++;
s_nFrames = 0;
s_nWorst = 0;
s_fSumMS = 0.0f;
s_fMaxMS = 0.0f;
// A single frame longer than a second (a level load) must not spill into the next
// buckets and emit a run of bogus one-frame rows, which is what used to happen.
s_fAccumMS-= 1000.0f;
if( s_fAccumMS<0.0f || s_fAccumMS>=1000.0f )
s_fAccumMS = 0.0f;
}
// //
// Update the display (call all the 'Update Renderers' routines // Update the display (call all the 'Update Renderers' routines
// //
@@ -885,6 +1137,7 @@ void gos_UpdateDisplay( bool Everything )
frameRate=(float)frequency/(float)thisframe; // Calculate frame rate based on clock frequency frameRate=(float)frequency/(float)thisframe; // Calculate frame rate based on clock frequency
StartOfRenderTime=GetCycles(); StartOfRenderTime=GetCycles();
GOS_LogFrameRate( frameRate ); // [fpslog] per-second pacing report
// //
// Don't trigger watchdog timer // Don't trigger watchdog timer
// //
+425 -114
View File
@@ -4,6 +4,7 @@
#include <d3d.h> #include <d3d.h>
#include <malloc.h> #include <malloc.h>
#include <stdio.h> #include <stdio.h>
#include <stdarg.h> // [displaylog] va_list/va_start for HSH_LogInit
//#include "directx.hpp" //#include "directx.hpp"
#include "dxrasterizer.hpp" #include "dxrasterizer.hpp"
#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } } #define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }
@@ -522,19 +523,185 @@ void CHSH_Device::SetRenderTargetTexture()
const DWORD dwFlags = DDSCL_SETFOCUSWINDOW | DDSCL_CREATEDEVICEWINDOW | const DWORD dwFlags = DDSCL_SETFOCUSWINDOW | DDSCL_CREATEDEVICEWINDOW |
DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN; DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN;
// [panelcoop] Which SetCooperativeLevel form the secondary panels use; 0 = legacy.
// Selected with -tcoop <0-3>. See CHSH_Device::InitFirst for what each one does.
int g_nHshCoopMode = 0;
extern HRESULT wDirectDrawCreateEx( GUID* lpGUID, void** lplpDD, REFIID iid, IUnknown* pUnkOuter ); extern HRESULT wDirectDrawCreateEx( GUID* lpGUID, void** lplpDD, REFIID iid, IUnknown* pUnkOuter );
//
// [displaylog] Append a printf-style line to gos-displays.txt, the report VideoCard.cpp
// writes at device-enumeration time.
//
// This exists because the panel initialisation below discarded every HRESULT it produced
// and returned true unconditionally. A display that failed to open therefore generated no
// error, no crash and no log entry - the monitor simply stayed on the desktop, which is
// indistinguishable from the display never having been asked to open at all.
//
void HSH_LogInit( const char* fmt, ... )
{
char szDir[MAX_PATH];
char szPath[MAX_PATH];
char szLine[512];
DWORD dwWritten=0;
va_list ap;
szDir[0]='\0';
GetModuleFileNameA( NULL, szDir, sizeof(szDir) );
char* pSlash=strrchr( szDir, '\\' );
if( pSlash )
*(pSlash+1)='\0';
else
szDir[0]='\0';
sprintf( szPath, "%sgos-displays.txt", szDir );
HANDLE hFile=CreateFileA( szPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile==INVALID_HANDLE_VALUE )
return;
SetFilePointer( hFile, 0, NULL, FILE_END );
va_start( ap, fmt );
vsprintf( szLine, fmt, ap );
va_end( ap );
WriteFile( hFile, szLine, (DWORD)strlen(szLine), &dwWritten, NULL );
CloseHandle( hFile );
}
//
// [displaylog] Readable name for the result codes multi-device fullscreen init can return.
//
static const char* HSH_HRName( HRESULT hr )
{
switch( hr )
{
case DD_OK: return "DD_OK";
case DDERR_ALREADYINITIALIZED: return "DDERR_ALREADYINITIALIZED";
case DDERR_CANNOTATTACHSURFACE: return "DDERR_CANNOTATTACHSURFACE";
case DDERR_DIRECTDRAWALREADYCREATED: return "DDERR_DIRECTDRAWALREADYCREATED";
case DDERR_EXCLUSIVEMODEALREADYSET: return "DDERR_EXCLUSIVEMODEALREADYSET";
case DDERR_GENERIC: return "DDERR_GENERIC";
case DDERR_HWNDALREADYSET: return "DDERR_HWNDALREADYSET";
case DDERR_HWNDSUBCLASSED: return "DDERR_HWNDSUBCLASSED";
case DDERR_INCOMPATIBLEPRIMARY: return "DDERR_INCOMPATIBLEPRIMARY";
case DDERR_INVALIDCAPS: return "DDERR_INVALIDCAPS";
case DDERR_INVALIDMODE: return "DDERR_INVALIDMODE";
case DDERR_INVALIDOBJECT: return "DDERR_INVALIDOBJECT";
case DDERR_INVALIDPARAMS: return "DDERR_INVALIDPARAMS";
case DDERR_INVALIDPIXELFORMAT: return "DDERR_INVALIDPIXELFORMAT";
case DDERR_INVALIDSURFACETYPE: return "DDERR_INVALIDSURFACETYPE";
case DDERR_NOCOOPERATIVELEVELSET: return "DDERR_NOCOOPERATIVELEVELSET";
case DDERR_NODIRECTDRAWHW: return "DDERR_NODIRECTDRAWHW";
case DDERR_NOEXCLUSIVEMODE: return "DDERR_NOEXCLUSIVEMODE";
case DDERR_NO3D: return "DDERR_NO3D";
case DDERR_NOTFOUND: return "DDERR_NOTFOUND";
case DDERR_OUTOFMEMORY: return "DDERR_OUTOFMEMORY";
case DDERR_OUTOFVIDEOMEMORY: return "DDERR_OUTOFVIDEOMEMORY";
case DDERR_PRIMARYSURFACEALREADYEXISTS: return "DDERR_PRIMARYSURFACEALREADYEXISTS";
case DDERR_SURFACEBUSY: return "DDERR_SURFACEBUSY";
case DDERR_UNSUPPORTED: return "DDERR_UNSUPPORTED";
case DDERR_UNSUPPORTEDMODE: return "DDERR_UNSUPPORTEDMODE";
case DDERR_WRONGMODE: return "DDERR_WRONGMODE";
}
return "(unrecognised)";
}
// [displaylog] Log one DirectDraw call result, flagging failures.
static void HSH_CheckHR( const char* pszWhat, HRESULT hr )
{
HSH_LogInit( " %-24s = 0x%08lX %s%s\r\n",
pszWhat, (unsigned long)hr, HSH_HRName(hr),
FAILED(hr) ? " *** FAILED ***" : "" );
}
bool CHSH_Device::InitFirst(int devicenum,DWORD resx,DWORD resy, CHSH_Device* pOtherHSHD/* = NULL*/) bool CHSH_Device::InitFirst(int devicenum,DWORD resx,DWORD resy, CHSH_Device* pOtherHSHD/* = NULL*/)
{ {
m_pOtherHSHD = pOtherHSHD; m_pOtherHSHD = pOtherHSHD;
if (pOtherHSHD) { if (pOtherHSHD) {
pDD = pOtherHSHD->pDD; pDD = pOtherHSHD->pDD;
HSH_LogInit( " InitFirst : shares the DirectDraw object of another panel\r\n" );
} else { } else {
HRESULT hr; HRESULT hr;
HSH_LogInit( " InitFirst : device %d, %lux%lu 16bpp 60Hz\r\n",
devicenum, (unsigned long)resx, (unsigned long)resy );
if( devicenum<0 || devicenum>=(int)NumDevices ) {
HSH_LogInit( " *** device index out of range - this panel cannot open ***\r\n" );
pDD=0;
size_back.cx=resx;
size_back.cy=resy;
return false;
}
//1. Resolution change. //1. Resolution change.
wDirectDrawCreateEx( &DeviceArray[devicenum].DeviceGUID , (VOID**) &(pDD), IID_IDirectDraw7, NULL ); hr=wDirectDrawCreateEx( &DeviceArray[devicenum].DeviceGUID , (VOID**) &(pDD), IID_IDirectDraw7, NULL );
pDD->SetCooperativeLevel(hWindow, dwFlags ); HSH_CheckHR( "DirectDrawCreateEx", hr );
if( FAILED(hr) || !pDD ) {
pDD=0;
size_back.cx=resx;
size_back.cy=resy;
return false;
}
// [panelcoop] Establish the cooperative level for this panel.
//
// The legacy call asks every panel to be BOTH the process focus window and its own
// device window, using the one shared hWindow, after the main display device has
// already taken exclusive mode on it. XP's DirectDraw and dgVoodoo2 tolerate that;
// Windows 10's does not - it returns DDERR_EXCLUSIVEMODEALREADYSET on the first
// panel and DDERR_INVALIDPARAMS on the rest, after which the primary surface fails
// with DDERR_NOCOOPERATIVELEVELSET.
//
// The main device (DXRasterizer.cpp, EnterFullScreenMode) already uses the correct
// two-call form: SETFOCUSWINDOW on its own, then EXCLUSIVE|FULLSCREEN. The panels
// never were. -tcoop picks the form so the alternatives can be compared on real
// hardware without a rebuild between each one.
switch( g_nHshCoopMode )
{
case 1: // one call, but stop claiming the focus window
HSH_LogInit( " coop 1: CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN\r\n" );
hr=pDD->SetCooperativeLevel( hWindow,
DDSCL_CREATEDEVICEWINDOW|DDSCL_ALLOWREBOOT|DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN );
HSH_CheckHR( "SetCoopLevel", hr );
break;
case 2: // two calls, DirectDraw makes a device window for this monitor
HSH_LogInit( " coop 2: SETFOCUSWINDOW, then CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN\r\n" );
hr=pDD->SetCooperativeLevel( hWindow, DDSCL_SETFOCUSWINDOW );
HSH_CheckHR( "SetCoopLevel(focus)", hr );
hr=pDD->SetCooperativeLevel( hWindow,
DDSCL_CREATEDEVICEWINDOW|DDSCL_ALLOWREBOOT|DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN );
HSH_CheckHR( "SetCoopLevel(device)", hr );
break;
case 3: // two calls, exactly what the main display device does
HSH_LogInit( " coop 3: SETFOCUSWINDOW, then EXCLUSIVE|FULLSCREEN\r\n" );
hr=pDD->SetCooperativeLevel( hWindow, DDSCL_SETFOCUSWINDOW );
HSH_CheckHR( "SetCoopLevel(focus)", hr );
hr=pDD->SetCooperativeLevel( hWindow, DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN );
HSH_CheckHR( "SetCoopLevel(device)", hr );
break;
case 4: // exclusive only - the main device has ALREADY set the focus window,
// so re-claiming it is what makes the first panel collide with it.
HSH_LogInit( " coop 4: EXCLUSIVE|FULLSCREEN only (no focus claim)\r\n" );
hr=pDD->SetCooperativeLevel( hWindow, DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN );
HSH_CheckHR( "SetCoopLevel", hr );
break;
case 5: // as 4, plus ALLOWREBOOT (the legacy flag set minus the two window flags)
HSH_LogInit( " coop 5: ALLOWREBOOT|EXCLUSIVE|FULLSCREEN (no focus claim)\r\n" );
hr=pDD->SetCooperativeLevel( hWindow,
DDSCL_ALLOWREBOOT|DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN );
HSH_CheckHR( "SetCoopLevel", hr );
break;
default: // 0 = legacy, byte-for-byte the original behaviour
HSH_LogInit( " coop 0: legacy combined call\r\n" );
hr=pDD->SetCooperativeLevel( hWindow, dwFlags );
HSH_CheckHR( "SetCoopLevel", hr );
break;
}
hr=pDD->SetDisplayMode( resx, resy, 16, 60, 0 ); hr=pDD->SetDisplayMode( resx, resy, 16, 60, 0 );
HSH_CheckHR( "SetDisplayMode", hr );
} }
size_back.cx=resx; size_back.cx=resx;
size_back.cy=resy; size_back.cy=resy;
@@ -613,6 +780,46 @@ char *mechnames[]={
}; };
#define ARRAYSIZE(x) (sizeof(x)/sizeof(x[0])) #define ARRAYSIZE(x) (sizeof(x)/sizeof(x[0]))
// [mode 4 split-MFD fix]
// Build the MFD texture set (mech image atlas + sprite atlas) on a given DirectDraw object.
//
// Mode 4 drives two physically separate 640x480 monitors. DirectDraw binds one
// IDirectDraw7 to one monitor, so the right-hand panel necessarily has its own
// IDirectDraw7 *and* its own IDirect3DDevice7. A DirectDraw surface belongs to the
// IDirectDraw7 that created it - NOT to the physical GPU - so a texture made by the left
// device can never be sampled by the right device's D3D device, even when both monitors
// hang off the same card. Attempting it makes the runtime emulate the access with a
// VRAM->system-memory readback every time, which stalls the whole GPU. Hence: each
// device gets its own complete copy of these textures.
static void HSH_CreateMFDTextures(LPDIRECTDRAW7 pdd,
LPDIRECTDRAWSURFACE7* ppMechTexture,
LPDIRECTDRAWSURFACE7* ppSpriteTexture)
{
char temppath[MAX_PATH];
if(g_f3dtarget){
*ppMechTexture=CreatePixelFormatTexture(pdd,128,128,&DDPF_R5G6B5);//Texture for storing 3D mech image...
}else{
// MSL 5.02 Target MFD Image
*ppMechTexture=CreatePixelFormatTexture(pdd,1024,1024,&DDPF_R5G6B5);//Texture for storing 2D mech image...
//Store images for all mechs.
for(int mech=0;mech<ARRAYSIZE(mechnames);mech++){
int x=(mech%8)*128;
int y=(mech/8)*128;
sprintf(temppath,"%s\\hsh\\mfd\\%s.bmp",AssetsDirectory1,mechnames[mech]);
DrawBitmapToSurface(*ppMechTexture,x,y,temppath);
}
}
sprintf(temppath,"%s\\hsh\\%s",AssetsDirectory1,"mfd_texture.bmp");
*ppSpriteTexture=CreateATextureFromFile(pdd, temppath);
}
// [mode 4 split-MFD fix] exchange one data member between two devices.
#define HSH_SWAP_MEMBER(type, a, b) { type _hsh_swaptmp = (a); (a) = (b); (b) = _hsh_swaptmp; }
bool CHSH_Device::InitSecond(DWORD tresx,DWORD tresy) bool CHSH_Device::InitSecond(DWORD tresx,DWORD tresy)
{ {
if (m_pOtherHSHD) { if (m_pOtherHSHD) {
@@ -624,25 +831,50 @@ bool CHSH_Device::InitSecond(DWORD tresx,DWORD tresy)
//////Front Buffer //////Front Buffer
DDSURFACEDESC2 ddsd={0,}; DDSURFACEDESC2 ddsd={0,};
DDSCAPS2 ddsCaps; DDSCAPS2 ddsCaps;
HRESULT hr;
// [displaylog] InitFirst can now fail cleanly; without this guard every call below
// dereferences a null IDirectDraw7 and takes the process down.
if( !pDD ) {
HSH_LogInit( " InitSecond: skipped - InitFirst produced no DirectDraw object\r\n" );
return false;
}
ddsd.dwSize = sizeof(ddsd); ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_BACKBUFFERCOUNT | DDSD_CAPS; ddsd.dwFlags = DDSD_BACKBUFFERCOUNT | DDSD_CAPS;
ddsd.dwBackBufferCount = 1; ddsd.dwBackBufferCount = 1;
ddsd.ddsCaps.dwCaps = DDSCAPS_COMPLEX | DDSCAPS_FLIP | DDSCAPS_PRIMARYSURFACE| DDSCAPS_3DDEVICE; ddsd.ddsCaps.dwCaps = DDSCAPS_COMPLEX | DDSCAPS_FLIP | DDSCAPS_PRIMARYSURFACE| DDSCAPS_3DDEVICE;
pDD->CreateSurface( &ddsd, &pDDSFront, NULL ); hr=pDD->CreateSurface( &ddsd, &pDDSFront, NULL );
HSH_CheckHR( "CreateSurface(primary)", hr );
if( FAILED(hr) || !pDDSFront ) {
pDDSFront=0;
return false;
}
//////BackBuffer //////BackBuffer
ZeroMemory( &ddsCaps, sizeof(ddsCaps) ); ZeroMemory( &ddsCaps, sizeof(ddsCaps) );
ddsCaps.dwCaps = DDSCAPS_BACKBUFFER; ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;
pDDSFront->GetAttachedSurface( &ddsCaps, &pDDSBack ); hr=pDDSFront->GetAttachedSurface( &ddsCaps, &pDDSBack );
HSH_CheckHR( "GetAttachedSurface", hr );
if( FAILED(hr) || !pDDSBack ) {
pDDSBack=0;
return false;
}
//////3D Device //////3D Device
LPDIRECT3D7 pD3D; LPDIRECT3D7 pD3D=0;
pDD->QueryInterface( IID_IDirect3D7, (VOID**)&(pD3D) ); hr=pDD->QueryInterface( IID_IDirect3D7, (VOID**)&(pD3D) );
pD3D->CreateDevice(IID_IDirect3DHALDevice,pDDSBack,&pD3DDevice); HSH_CheckHR( "QueryInterface(D3D7)", hr );
if( FAILED(hr) || !pD3D )
return false;
hr=pD3D->CreateDevice(IID_IDirect3DHALDevice,pDDSBack,&pD3DDevice);
HSH_CheckHR( "CreateDevice(HAL)", hr );
pD3D->Release(); pD3D->Release();
if( FAILED(hr) || !pD3DDevice ) {
pD3DDevice=0;
return false;
}
D3DVIEWPORT7 vp = { 0, 0, size_back.cx, size_back.cy, 0.0f, 1.0f }; D3DVIEWPORT7 vp = { 0, 0, size_back.cx, size_back.cy, 0.0f, 1.0f };
pD3DDevice->SetViewport( &vp ); pD3DDevice->SetViewport( &vp );
@@ -978,7 +1210,10 @@ bool CMR_Device::InitFirst()
bool CMR_Device::InitSecond() bool CMR_Device::InitSecond()
{ {
map_loaded=false; map_loaded=false;
CHSH_Device::InitSecond(0,0); // no off screen - do not create Target Texture. // [panelcoop] see CRadar_Device::InitSecond - do not build surfaces on a device
// that never got a cooperative level.
if( !CHSH_Device::InitSecond(0,0) ) // no off screen - do not create Target Texture.
return false;
char temppath[MAX_PATH],temppatha[MAX_PATH]; char temppath[MAX_PATH],temppatha[MAX_PATH];
@@ -1068,9 +1303,9 @@ bool CMR_Device::LoadMRTargetTexture(const char *mechtexturename)
CMR_Device mr_device; CMR_Device mr_device;
// Mode 4: the right-hand 640x480 MFD device. Shares VRAM with mfd_device // Mode 4: the right-hand 640x480 MFD device. It owns a SEPARATE IDirectDraw7 and
// (same GPU), so mfd_device's pDDSTarget texture can be passed to this // IDirect3DDevice7 (one DirectDraw device is bound to one monitor), so it also owns a
// device's pD3DDevice->SetTexture without a copy. // complete, independent copy of the MFD texture set - see HSH_CreateMFDTextures.
CMFDRight_Device mfd_device_right; CMFDRight_Device mfd_device_right;
bool CMFDRight_Device::InitFirst() bool CMFDRight_Device::InitFirst()
@@ -1081,8 +1316,35 @@ bool CMFDRight_Device::InitFirst()
bool CMFDRight_Device::InitSecond() bool CMFDRight_Device::InitSecond()
{ {
// Create flip chain, D3D device and a render-target slot (1024x512). // Create flip chain, D3D device, render target (1024x512) and fonts.
return CHSH_Device::InitSecond(1024, 512); // [displaylog] If the device never came up, everything below dereferences a null
// IDirect3DDevice7; bail out and leave the reason in gos-displays.txt.
if( !CHSH_Device::InitSecond(1024, 512) )
return false;
// Independent copy of the MFD texture set owned by THIS DirectDraw object.
HSH_CreateMFDTextures(pDD, &pDDSMechTexture, &pDDSTexture);
pDDSDamageTexture = 0; // created on demand by LoadDamageTexture
pDDSTargetTexture = 0; // created on demand by LoadTargetTexture
tw = 512;
th = 512;
D3DMATERIAL7 mtrl;
ZeroMemory( &mtrl, sizeof(mtrl) );
mtrl.diffuse.r = mtrl.diffuse.g = mtrl.diffuse.b = 1.0f;
mtrl.ambient.r = mtrl.ambient.g = mtrl.ambient.b = 1.0f;
pD3DDevice->SetMaterial( &mtrl );
pD3DDevice->SetRenderState( D3DRENDERSTATE_AMBIENT, 0xffffffff );
return true;
}
bool CMFDRight_Device::Release()
{
SAFE_RELEASE(pDDSMechTexture);
SAFE_RELEASE(pDDSDamageTexture);
SAFE_RELEASE(pDDSTargetTexture);
return CHSH_Device::Release();
} }
/////////////////////////////////// CRadar_Device /////////////////////////////////// /////////////////////////////////// CRadar_Device ///////////////////////////////////
@@ -1128,7 +1390,11 @@ bool CRadar_Device::InitFirst()
bool CRadar_Device::InitSecond() bool CRadar_Device::InitSecond()
{ {
ZeroMemory(recent_state,sizeof(recent_state)); ZeroMemory(recent_state,sizeof(recent_state));
CHSH_Device::InitSecond(512,1024); // 480 x 640 // [panelcoop] If the device did not come up, stop here. Continuing would create
// surfaces on a DirectDraw object with no cooperative level - they come back NULL,
// and the first GetDC on one faults (IDirectDrawSurface7 vtable +0x44).
if( !CHSH_Device::InitSecond(512,1024) ) // 480 x 640
return false;
MapDrawn=false; MapDrawn=false;
char temppath[MAX_PATH],temppatha[MAX_PATH]; char temppath[MAX_PATH],temppatha[MAX_PATH];
@@ -1368,7 +1634,7 @@ static DWORD channel_color[6]={
CMFD_Device::CMFD_Device() CMFD_Device::CMFD_Device()
: ch((DWORD)-1), m_pRightDevice(NULL) : ch((DWORD)-1), m_pRightDevice(NULL), m_bSwappedToRight(false)
{ {
} }
@@ -1382,9 +1648,11 @@ bool CMFD_Device::InitFirst()
// device here so both displays are ready before InitSecond runs. // device here so both displays are ready before InitSecond runs.
if (g_nTypeOfMFDs == 4) { if (g_nTypeOfMFDs == 4) {
m_pRightDevice = &mfd_device_right; m_pRightDevice = &mfd_device_right;
CHSH_Device::InitFirst(g_nMFD1, 640, 480); // left device // [panelcoop] Report the REAL result. This used to return true unconditionally,
mfd_device_right.InitFirst(); // right device display mode // which hid a failed panel and let the per-frame code run on null surfaces.
return true; bool bLeft = CHSH_Device::InitFirst(g_nMFD1, 640, 480);
bool bRight = mfd_device_right.InitFirst();
return (bLeft && bRight);
} }
m_pRightDevice = NULL; m_pRightDevice = NULL;
return CHSH_Device::InitFirst(g_nDualHead,1280,480); return CHSH_Device::InitFirst(g_nDualHead,1280,480);
@@ -1392,39 +1660,17 @@ bool CMFD_Device::InitFirst()
bool CMFD_Device::InitSecond() bool CMFD_Device::InitSecond()
{ {
CHSH_Device::InitSecond(1024,512); // 640 x 480 // [displaylog] see CMFDRight_Device::InitSecond - a failed device must not fall
if(g_f3dtarget){ // through into the texture/material setup below.
pDDSMechTexture=CreatePixelFormatTexture(pDD,128,128,&DDPF_R5G6B5);//Texture for storing 3D mech image... if( !CHSH_Device::InitSecond(1024,512) ) // 640 x 480
}else{ return false;
// MSL 5.02 Target MFD Image
// pDDSMechTexture=CreatePixelFormatTexture(pDD,512,512,&DDPF_R5G6B5);//texture for storing 2D mech image...
pDDSMechTexture=CreatePixelFormatTexture(pDD,1024,1024,&DDPF_R5G6B5);//Texture for storing 2D mech image...
//Store images for all mechs.
for(int mech=0;mech<ARRAYSIZE(mechnames);mech++){
// MSL 5.02 Target MFD Image
// int x=(mech%4)*128;
// int y=(mech/4)*128;
int x=(mech%8)*128;
int y=(mech/8)*128;
char temppath[MAX_PATH]; HSH_CreateMFDTextures(pDD, &pDDSMechTexture, &pDDSTexture);
sprintf(temppath,"%s\\hsh\\mfd\\%s.bmp",AssetsDirectory1,mechnames[mech]);
DrawBitmapToSurface(pDDSMechTexture,x,y,temppath);
}
}
//if(pDDSMechTexture==NULL)MessageBeep(0);
//pDDSMechTexture=0;
//pDDSDamageTexture=CreatePixelFormatTexture(pDD,512,512,&DDPF_R4G4B4A4);//damage image..
pDDSDamageTexture=0;//On Reset: recreated... no need to create now. pDDSDamageTexture=0;//On Reset: recreated... no need to create now.
// MSL 5.03 Target Damage Display // MSL 5.03 Target Damage Display
pDDSTargetTexture=0; pDDSTargetTexture=0;
//////Texture
char temppath[MAX_PATH];
sprintf(temppath,"%s\\hsh\\%s",AssetsDirectory1,"mfd_texture.bmp");
pDDSTexture=CreateATextureFromFile(pDD, temppath);
//pDDSTexture=0;
tw=512; tw=512;
th=512; th=512;
@@ -1439,12 +1685,50 @@ bool CMFD_Device::InitSecond()
// Mode 4: create flip chain + D3D device for the right 640x480 monitor. // Mode 4: create flip chain + D3D device for the right 640x480 monitor.
if (g_nTypeOfMFDs == 4 && m_pRightDevice) { if (g_nTypeOfMFDs == 4 && m_pRightDevice) {
mfd_device_right.InitSecond(); if( !mfd_device_right.InitSecond() )
return false;
} }
return true; return true;
} }
// [mode 4 split-MFD fix]
// Exchange this object's DATA members with the right-hand device's, so that every
// existing "mfd_device.<member>" drawing call in hudchat/huddamage/hudweapon/radar
// transparently targets whichever physical 640x480 panel the current channel belongs to.
// Only data is exchanged - never the vtable pointer, so virtual dispatch is unaffected.
void CMFD_Device::SwapRightState()
{
if (!m_pRightDevice)
return;
CMFDRight_Device* r = m_pRightDevice;
HSH_SWAP_MEMBER(LPDIRECTDRAW7, pDD, r->pDD);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSFront, r->pDDSFront);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSBack, r->pDDSBack);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSTarget, r->pDDSTarget);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSTexture, r->pDDSTexture);
HSH_SWAP_MEMBER(LPDIRECT3DDEVICE7, pD3DDevice, r->pD3DDevice);
HSH_SWAP_MEMBER(SIZE, size_back, r->size_back);
HSH_SWAP_MEMBER(SIZE, size_target, r->size_target);
HSH_SWAP_MEMBER(float, tw, r->tw);
HSH_SWAP_MEMBER(float, th, r->th);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSMechTexture, r->pDDSMechTexture);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSDamageTexture, r->pDDSDamageTexture);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSTargetTexture, r->pDDSTargetTexture);
// CHSHFont has no virtual functions, so a raw byte swap is safe here and avoids
// running constructor/destructor side effects on a temporary copy.
for (int i = 0; i < 4; i++) {
char fonttmp[sizeof(CHSHFont)];
memcpy(fonttmp, &pFont[i], sizeof(CHSHFont));
memcpy(&pFont[i], &r->pFont[i], sizeof(CHSHFont));
memcpy(&r->pFont[i], fonttmp, sizeof(CHSHFont));
}
m_bSwappedToRight = !m_bSwappedToRight;
}
bool CMFD_Device::BeginChannel(DWORD channel) bool CMFD_Device::BeginChannel(DWORD channel)
{ {
if (!sh_game_started) if (!sh_game_started)
@@ -1491,6 +1775,13 @@ bool CMFD_Device::BeginChannel(DWORD channel)
} }
ch=channel; ch=channel;
// [mode 4 split-MFD fix] Channels 3-4 belong to the right-hand 640x480 monitor,
// which is a separate DirectDraw object with its own D3D device and its own copies
// of the MFD textures and fonts. Swap our members over to it for the duration of
// the channel so all existing drawing code targets it, and swap back in EndChannel.
if (g_nTypeOfMFDs == 4 && m_pRightDevice && channel >= 3)
SwapRightState();
//Set the Render Target. //Set the Render Target.
SetRenderTargetTexture(); SetRenderTargetTexture();
pD3DDevice->Clear(0,NULL,D3DCLEAR_TARGET ,0,0,0); pD3DDevice->Clear(0,NULL,D3DCLEAR_TARGET ,0,0,0);
@@ -1513,72 +1804,56 @@ bool CMFD_Device::EndChannel()
gCaptureScreen = 0; gCaptureScreen = 0;
} }
// Mode 4: route this channel's blit to the correct physical monitor. // [mode 4 split-MFD fix] One composite path for every mode. In mode 4 BeginChannel
// Handled here, before the modes 1-3 SetRenderTargetBackbuffer/BeginScene // has already swapped our members over to the device that owns this channel, so
// block, so each path has exactly one matched Begin/EndScene pair. // pDDSTarget and pD3DDevice always belong to the SAME DirectDraw object. The old
// Channels 0-2 go to the LEFT device (this, g_nMFD1). // code sampled the LEFT device's pDDSTarget from the RIGHT device's D3D device,
// Channels 3-4 go to the RIGHT device (m_pRightDevice, g_nMFD2). // which DirectDraw cannot do natively and emulated with a VRAM->system-memory
// pDDSTarget was created by this device's pDD; both devices are on the // readback on channels 3 and 4 - a full GPU stall twice every 7-frame cycle, which
// same GPU so VRAM textures are mutually accessible. // is what produced the constant rhythmic stutter in mode 4.
if (g_nTypeOfMFDs == 4 && m_pRightDevice) { SetRenderTargetBackbuffer();
CHSH_Device* target = (ch >= 3) ? m_pRightDevice : static_cast<CHSH_Device*>(this); pD3DDevice->BeginScene();
target->SetRenderTargetBackbuffer();
target->pD3DDevice->BeginScene();
target->pD3DDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE,TRUE); pD3DDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE,TRUE);
target->pD3DDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,D3DBLEND_ONE); pD3DDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,D3DBLEND_ONE);
target->pD3DDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND,D3DBLEND_ONE); pD3DDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND,D3DBLEND_ONE);
target->pD3DDevice->SetTexture(0, pDDSTarget); pD3DDevice->SetTexture( 0, pDDSTarget );
DWORD color4 = channel_color[ch]; DWORD color;
float w4 = (float)target->size_back.cx; // 640
float h4 = (float)target->size_back.cy; // 480
float w2_4 = (float)size_target.cx; // 1024
float h2_4 = (float)size_target.cy; // 512
D3DTLVERTEX V4[4];
V4[0]=D3DTLVERTEX(D3DVECTOR( -0.5f,h4-0.5f,0.9f),1.0f,color4,0, 0/w2_4,h4/h2_4);
V4[1]=D3DTLVERTEX(D3DVECTOR( -0.5f, -0.5f,0.9f),1.0f,color4,0, 0/w2_4, 0/h2_4);
V4[2]=D3DTLVERTEX(D3DVECTOR(w4-0.5f,h4-0.5f,0.9f),1.0f,color4,0,w4/w2_4,h4/h2_4);
V4[3]=D3DTLVERTEX(D3DVECTOR(w4-0.5f, -0.5f,0.9f),1.0f,color4,0,w4/w2_4, 0/h2_4);
target->pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,V4,4,NULL);
target->pD3DDevice->EndScene(); color = channel_color[ch];
ch = (DWORD)-1; D3DTLVERTEX Vertices[4]; // Vertices for the cube
// Modes 1-3 pack two 640-wide panels into a single backbuffer, so channels 0-2 go
// to the left half and 3-4 to the right half. In mode 4 each device IS a
// standalone 640x480 panel, so every channel composites full width at x=0.
float w, x1;
if (g_nTypeOfMFDs == 4) {
w = (float)size_back.cx;
x1 = 0.0f;
} else { } else {
// Modes 1-3: blit to half of the single 1280-wide (or 640-wide) backbuffer. w = (float)size_back.cx/2;//<==NOTE: this differs from radar.
SetRenderTargetBackbuffer(); x1 = (ch/3)*w;
pD3DDevice->BeginScene();
pD3DDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE,TRUE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,D3DBLEND_ONE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND,D3DBLEND_ONE);
pD3DDevice->SetTexture( 0, pDDSTarget );
DWORD color;
color = channel_color[ch];
//color = 0xFFFFFFFF;
D3DTLVERTEX Vertices[4]; // Vertices for the cube
float w=(float)size_back.cx/2;//<==NOTE: this differs from radar.
float x1=(ch/3)*w;
float x2=x1+w;
float h=(float)size_back.cy;
float w2=(float)size_target.cx;
float h2=(float)size_target.cy;
Vertices[0] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,h-0.5f,0.9f),1.0f,color,0, 0/w2, h/h2);
Vertices[1] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,0-0.5f,0.9f),1.0f,color,0, 0/w2, 0/h2);
Vertices[2] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,h-0.5f,0.9f),1.0f,color,0, w/w2, h/h2);
Vertices[3] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,0-0.5f,0.9f),1.0f,color,0, w/w2, 0/h2);
pD3DDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX , Vertices, 4, NULL );
pD3DDevice->EndScene();
ch = (DWORD)-1;
} }
float x2=x1+w;
float h=(float)size_back.cy;
float w2=(float)size_target.cx;
float h2=(float)size_target.cy;
Vertices[0] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,h-0.5f,0.9f),1.0f,color,0, 0/w2, h/h2);
Vertices[1] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,0-0.5f,0.9f),1.0f,color,0, 0/w2, 0/h2);
Vertices[2] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,h-0.5f,0.9f),1.0f,color,0, w/w2, h/h2);
Vertices[3] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,0-0.5f,0.9f),1.0f,color,0, w/w2, 0/h2);
pD3DDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX , Vertices, 4, NULL );
pD3DDevice->EndScene();
ch = (DWORD)-1;
// Hand our members back to the left device if this channel was routed right.
if (m_bSwappedToRight)
SwapRightState();
} }
return true; return true;
} }
@@ -2649,21 +2924,57 @@ bool IsSecondaryMonitorAvaliable()
void HSH_EnterFullScreen2() void HSH_EnterFullScreen2()
{ {
// [displaylog] Record which of the three possible paths was taken and how each
// individual panel fared. Mode 4 opens four fullscreen DirectDraw devices that all
// share one focus window, so a partial failure is both plausible and, until now,
// completely invisible.
HSH_LogInit( "\r\nHSH_EnterFullScreen2\r\n" );
HSH_LogInit( "--------------------\r\n" );
if(IsMultimonitorAvaliable()){ if(IsMultimonitorAvaliable()){
mfd_device.InitFirst(); HSH_LogInit( " path: multi-monitor (MFD + radar)\r\n" );
radar_device.InitFirst();
mfd_device.InitSecond(); HSH_LogInit( " [mfd]\r\n" );
radar_device.InitSecond(); bool bMFD = mfd_device.InitFirst();
HSH_LogInit( " [radar]\r\n" );
bool bRadar = radar_device.InitFirst();
hsh_initialized=true; if( bMFD ) {
if(g_f3dtarget){ HSH_LogInit( " [mfd]\r\n" );
CopyTargetImage(0,mfd_device.pDDSMechTexture); bMFD = mfd_device.InitSecond();
}
if( bRadar ) {
HSH_LogInit( " [radar]\r\n" );
bRadar = radar_device.InitSecond();
}
// [panelcoop] Only claim the panels are up if they actually are.
//
// This flag used to be set unconditionally. A panel that failed to initialise
// therefore left null surfaces and a null IDirect3DDevice7 behind, and the
// per-frame render path called straight through them - the process died on a
// null COM vtable call ("Attempt to read from address 0x00000044") rather than
// simply running without MFDs.
if( bMFD && bRadar ) {
hsh_initialized=true;
HSH_LogInit( " result: panels initialised OK\r\n" );
if(g_f3dtarget){
CopyTargetImage(0,mfd_device.pDDSMechTexture);
}
} else {
HSH_LogInit( " result: *** FAILED (mfd=%s radar=%s) - continuing WITHOUT panels ***\r\n",
bMFD ? "ok" : "failed", bRadar ? "ok" : "failed" );
mfd_device.Release();
radar_device.Release();
hsh_initialized=false;
} }
}else if(IsSecondaryMonitorAvaliable()){ }else if(IsSecondaryMonitorAvaliable()){
HSH_LogInit( " path: single secondary monitor (mr_device)\r\n" );
mr_device.InitFirst(); mr_device.InitFirst();
mr_device.InitSecond(); mr_device.InitSecond();
hsh_mrdev_initialized=true; hsh_mrdev_initialized=true;
}else{
HSH_LogInit( " path: none - no secondary displays opened\r\n" );
} }
} }
@@ -159,6 +159,10 @@ public:
bool MapDrawn; bool MapDrawn;
}; };
// Mode 4 (split dual 640x480): the right-hand MFD lives on its own DirectDraw object,
// so CMFD_Device keeps a typed pointer to it.
class CMFDRight_Device;
class CMFD_Device:public CHSH_Device class CMFD_Device:public CHSH_Device
{ {
public: public:
@@ -169,7 +173,8 @@ public:
LPDIRECTDRAWSURFACE7 pDDSTargetTexture;//Target damage texture.. LPDIRECTDRAWSURFACE7 pDDSTargetTexture;//Target damage texture..
DWORD ch; //current channel.... DWORD ch; //current channel....
CHSH_Device* m_pRightDevice; // mode 4: pointer to right 640x480 device (NULL in modes 1-3) CMFDRight_Device* m_pRightDevice; // mode 4: right 640x480 device (NULL in modes 1-3)
bool m_bSwappedToRight; // mode 4: true while our members point at the right device
public: public:
CMFD_Device(); CMFD_Device();
~CMFD_Device(); ~CMFD_Device();
@@ -177,6 +182,7 @@ public:
bool InitSecond(); bool InitSecond();
bool BeginChannel(DWORD channel);// Select channel 0-4. bool BeginChannel(DWORD channel);// Select channel 0-4.
bool EndChannel(); bool EndChannel();
void SwapRightState(); // mode 4: exchange data members with the right device
virtual bool BeginScene(); //Performs normal BeginScene/EndScene. virtual bool BeginScene(); //Performs normal BeginScene/EndScene.
bool BeginSceneRight(); // mode 4: clear+grid on right device at sh_step==1 bool BeginSceneRight(); // mode 4: clear+grid on right device at sh_step==1
virtual bool EndScene(); //Performs the Flip. virtual bool EndScene(); //Performs the Flip.
@@ -192,16 +198,24 @@ public:
}; };
// Mode 4 (split dual 640x480): right-side MFD device on its own 640x480 monitor. // Mode 4 (split dual 640x480): right-side MFD device on its own 640x480 monitor.
// Same GPU as the left device (mfd_device), so VRAM textures are shared. // It owns a SEPARATE IDirectDraw7 and IDirect3DDevice7 (a DirectDraw device is bound to
// one monitor), therefore it must also own a complete, independent copy of the MFD
// texture set: a surface belongs to the IDirectDraw7 that created it, not to the GPU,
// so surfaces can NOT be shared between the two devices even on the same card.
class CMFDRight_Device : public CHSH_Device class CMFDRight_Device : public CHSH_Device
{ {
public: public:
CMFDRight_Device() {} LPDIRECTDRAWSURFACE7 pDDSMechTexture;
LPDIRECTDRAWSURFACE7 pDDSDamageTexture;
LPDIRECTDRAWSURFACE7 pDDSTargetTexture;
public:
CMFDRight_Device()
: pDDSMechTexture(0), pDDSDamageTexture(0), pDDSTargetTexture(0) {}
bool InitFirst(); bool InitFirst();
bool InitSecond(); bool InitSecond();
virtual bool BeginScene() { return true; } virtual bool BeginScene() { return true; }
virtual bool EndScene() { return true; } virtual bool EndScene() { return true; }
virtual bool Release() { return CHSH_Device::Release(); } virtual bool Release();
}; };
class CMR_Device:public CHSH_Device class CMR_Device:public CHSH_Device
+2 -2
View File
@@ -9,7 +9,7 @@
#include "recscore.h" #include "recscore.h"
#endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER) #endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER)
#define MAX_TESLAS 31 #define MAX_TESLAS 16
#define MAX_CAMERAS 4 #define MAX_CAMERAS 4
#define PORT_Launcher 1000 #define PORT_Launcher 1000
@@ -260,7 +260,7 @@ CInviteCOOP g_InviteCOOP;
// Mech4 only // Mech4 only
int g_nMech4Comm = 0; int g_nMech4Comm = 0;
BOOL g_bIsServer = FALSE; BOOL g_bIsServer = FALSE;
SPlayerInfo g_aPlayerInfos[35]; // 31 pilots + 1 CS + 3 spare SPlayerInfo g_aPlayerInfos[20];
int g_nPlayerInfos; int g_nPlayerInfos;
int g_nServer; int g_nServer;
int g_nTeslas; int g_nTeslas;
+55 -42
View File
@@ -43,7 +43,7 @@ LONG g_LeftPedalLast = 0;
LONG g_RightPedalLast = 0; LONG g_RightPedalLast = 0;
BOOL g_StartGame = FALSE; BOOL g_StartGame = FALSE;
static int g_nOpenComState = 0; // static int g_nOpenComState = 0; // hyun
DWORD g_dwLastAnalogUpdate = 0; DWORD g_dwLastAnalogUpdate = 0;
@@ -54,7 +54,7 @@ DWORD g_dwLastAnalogUpdate = 0;
#define RESTART_CHAR 0xFE #define RESTART_CHAR 0xFE
#define IDLE_CHAR 0xFF #define IDLE_CHAR 0xFF
//member 변수처럼 활용할것. //Use it like a member variable.
static HANDLE g_hCom = INVALID_HANDLE_VALUE; static HANDLE g_hCom = INVALID_HANDLE_VALUE;
static OVERLAPPED wos; static OVERLAPPED wos;
static OVERLAPPED ros; static OVERLAPPED ros;
@@ -108,6 +108,7 @@ int g_nRIOPacketCountB = (sizeof(g_baRIOLengthsB) / sizeof(g_baRIOLengthsB[0]));
int g_nRIOType = 0; // 0: old(original) type, 1: new type int g_nRIOType = 0; // 0: old(original) type, 1: new type
DWORD g_dwRIOBaud = 0; // [tbaud] 0: default by RIO type; else COM1 baud forced by -tbaud (high-speed replica of the original RIO board, protocol unchanged) DWORD g_dwRIOBaud = 0; // [tbaud] 0: default by RIO type; else COM1 baud forced by -tbaud (high-speed replica of the original RIO board, protocol unchanged)
DWORD g_dwRIOPollTimeout = 50; // [tbaud] WaitForMultipleObjects timeout (ms); computed from baud rate before the receive loop starts
int g_nEjectButton = 61; // 61: original, 31: new type int g_nEjectButton = 61; // 61: original, 31: new type
BYTE* g_pbRIOLengths = NULL; BYTE* g_pbRIOLengths = NULL;
int g_nRIOPacketCount = 0; int g_nRIOPacketCount = 0;
@@ -318,7 +319,7 @@ void RequestVersion();
void RequestAnalogUpdate(BYTE bFreq = 3); void RequestAnalogUpdate(BYTE bFreq = 3);
// start - for new RIO Board only... // start - for new RIO Board only...
//received packet은 각각 다른 스레드에서 처리하므로.. ciritical section이 필요하다.. //Received packets are handled in different threads.. a critical section is needed..
BYTE received_buffer[128][16]; BYTE received_buffer[128][16];
int received_packet=0;//packets count in the received_buffer. int received_packet=0;//packets count in the received_buffer.
DWORD received_serial=0; DWORD received_serial=0;
@@ -569,7 +570,7 @@ BOOL SetupConnection(HANDLE hCom)
return TRUE; return TRUE;
/* /*
원래 처리 루틴.. Original processing routine..
NPTTYINFO npTTYInfo=0 ; NPTTYINFO npTTYInfo=0 ;
BYTE bset1 = (BYTE) ((FLOWCTRL( npTTYInfo ) & FC_DTRDSR) != 0) ; BYTE bset1 = (BYTE) ((FLOWCTRL( npTTYInfo ) & FC_DTRDSR) != 0) ;
BYTE bset2 = (BYTE) ((FLOWCTRL( npTTYInfo ) & FC_RTSCTS) != 0) ; BYTE bset2 = (BYTE) ((FLOWCTRL( npTTYInfo ) & FC_RTSCTS) != 0) ;
@@ -603,7 +604,7 @@ BOOL SetupConnection(HANDLE hCom)
//처리되지 않는 항목들.. GetCommState의 기본값으로 처리한다. //Unhandled items.. handled using GetCommState default values.
DWORD fDsrSensitivity:1; // DSR sensitivity DWORD fDsrSensitivity:1; // DSR sensitivity
DWORD fTXContinueOnXoff:1; // XOFF continues Tx DWORD fTXContinueOnXoff:1; // XOFF continues Tx
DWORD fErrorChar: 1; // enable error replacement DWORD fErrorChar: 1; // enable error replacement
@@ -647,7 +648,7 @@ HANDLE OpenConnection(int port)
g_hLampEvent = CreateEvent(0,FALSE,0,0); g_hLampEvent = CreateEvent(0,FALSE,0,0);
g_hCommWatchThread = CreateThread((LPSECURITY_ATTRIBUTES)NULL, 0, (LPTHREAD_START_ROUTINE)CommWatchProc, NULL, CREATE_SUSPENDED, &g_dwThreadID); g_hCommWatchThread = CreateThread((LPSECURITY_ATTRIBUTES)NULL, 0, (LPTHREAD_START_ROUTINE)CommWatchProc, NULL, CREATE_SUSPENDED, &g_dwThreadID);
if (g_hCommWatchThread) { if (g_hCommWatchThread) {
//Thread까지 정상적으로 생성되었다. //Thread was created successfully.
////////////////////////////////////////////// //////////////////////////////////////////////
//All OK //All OK
//Exit Point <====== //Exit Point <======
@@ -744,11 +745,11 @@ retry:
for(int i=0;i<(int)dwLength;i++){ for(int i=0;i<(int)dwLength;i++){
int ch=(BYTE)lpszBlock[i]; int ch=(BYTE)lpszBlock[i];
//받은 문자만큼 루프를 돈다. //Loop for the number of received characters.
//현재.. 받고 있는 packet이 없고.. 어떤 control문자라도 올 수 있는 상태이다. //Currently.. no packet is being received.. can receive any control character.
if(packetbyteremain!=0){ if(packetbyteremain!=0){
//패킷에 해당하는 문자가 도착했다.. 나머지 문자들을 받는다. //Character corresponding to a packet has arrived.. receive the remaining characters.
if (ch & 0x80) { if (ch & 0x80) {
packetbyteremain=0; packetbyteremain=0;
chinpacket=0; chinpacket=0;
@@ -758,7 +759,7 @@ retry:
packetbytes[chinpacket] = ch; packetbytes[chinpacket] = ch;
chinpacket++; chinpacket++;
if (packetbyteremain == 0) { if (packetbyteremain == 0) {
//하나의 패킷이 완성되었다. 도착했다.. 즉시 반응한다. //A packet has been completed and arrived.. respond immediately.
chinpacket--; // exclude check byte chinpacket--; // exclude check byte
int packettype=packetbytes[0]; int packettype=packetbytes[0];
BYTE bCheckByte = 0; BYTE bCheckByte = 0;
@@ -837,21 +838,21 @@ retry:
packettype++; packettype++;
packettype--; packettype--;
#endif // _DEBUG #endif // _DEBUG
case rio_Ack2://◆◆◆◆◆◆◆◆◆◆ case rio_Ack2:
if (g_nRIOType != 0) { if (g_nRIOType != 0) {
BYTE id=packetbytes[1]; BYTE id=packetbytes[1];
int popped_index=PopPacket(id); int popped_index=PopPacket(id);
ack_timeout=GetTickCount()+30; ack_timeout=GetTickCount()+30;
if(popped_index==0){ if(popped_index==0){
//맨처음것... 정상적인 경우이다. //This is the very first one... Normal case.
}else if(popped_index>0){ }else if(popped_index>0){
//맨처음것이 아닌것. //Not the very first one.
//이전것들이 정상적으로 보내지지 않았으므로 다시 보낸다... //Previous ones were not sent successfully, resend...
ReSendPackets(popped_index);//잘못된 개수 만큼.. ReSendPackets(popped_index);//By the number of incorrect ones..
}else{ }else{
//실패 했다는것은.. 없는 id가 왔다는 것인데.. //Failure means an id that does not exist arrived..
//명백한 에러.. 그러나 특별히 대처할 방법은 없다. //An obvious error.. but there is no particular way to handle it.
//그냥 무시한다. //Simply ignore it.
} }
} }
break; break;
@@ -870,9 +871,9 @@ retry:
// WriteTTYBlock(hWnd,"|",1); // WriteTTYBlock(hWnd,"|",1);
chinpacket=0; chinpacket=0;
//packetbytes버퍼를 지운다. //Clear the packetbytes buffer.
} else { } else {
//아직 패킷 문자들이 다 도착하지 않았다.. 아무것도 하지 않는다. //Packet characters have not all arrived yet.. do nothing.
; ;
} }
}else{ }else{
@@ -889,7 +890,7 @@ retry:
}//for(int i=0;i<(int)dwLength;i++) }//for(int i=0;i<(int)dwLength;i++)
//상훈 뒤 //sanghoon end
if (!fReadStat){ if (!fReadStat){
if (GetLastError() == ERROR_IO_PENDING){ if (GetLastError() == ERROR_IO_PENDING){
while(!GetOverlappedResult( g_hCom,&ros, &dwLength, TRUE )){ while(!GetOverlappedResult( g_hCom,&ros, &dwLength, TRUE )){
@@ -1000,12 +1001,24 @@ DWORD FAR PASCAL CommWatchProc( LPSTR lpData )
} }
g_dwLastAnalogUpdate = GetTickCount() + 2800; g_dwLastAnalogUpdate = GetTickCount() + 2800;
// [tbaud] Scale the poll timeout proportionally to baud rate so faster links
// poll more frequently. Formula: clamp(ceil(480000/baud), 5, 50)
// preserves the existing 50ms at 9600 baud, floors at 5ms for high speeds.
// effectiveBaud: use -tbaud override if set, else the RIO type's default.
{
DWORD effectiveBaud = (g_dwRIOBaud != 0) ? g_dwRIOBaud
: ((g_nRIOType == 0) ? 9600UL : 115200UL);
{
DWORD t = (480000UL + effectiveBaud - 1) / effectiveBaud;
g_dwRIOPollTimeout = (t < 5UL) ? 5UL : (t > 50UL ? 50UL : t);
}
}
while(1) { while(1) {
DWORD dwEvtMask = 0; DWORD dwEvtMask = 0;
WaitCommEvent( g_hCom, &dwEvtMask,&wcos ); WaitCommEvent( g_hCom, &dwEvtMask,&wcos );
DWORD ret=WaitForMultipleObjects(3,events,FALSE,50/*INFINITE*/); DWORD ret=WaitForMultipleObjects(3,events,FALSE,g_dwRIOPollTimeout);
if(ret==WAIT_OBJECT_0){ if(ret==WAIT_OBJECT_0){
//An Comm Event has arrived. //An Comm Event has arrived.
DWORD bytesread; DWORD bytesread;
@@ -1022,10 +1035,10 @@ DWORD FAR PASCAL CommWatchProc( LPSTR lpData )
} }
} else {*/ } else {*/
if (g_nRIOType != 0) { if (g_nRIOType != 0) {
///◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆위의 ReadCommBlock에서 ack를 받았을 경우 duetime을 update시킨다. ///When ack is received from ReadCommBlock above, update duetime.
///◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆CheckSentBuffer... ///CheckSentBuffer...
if(GetTickCount()>ack_timeout) if(GetTickCount()>ack_timeout)
ReSendPackets(sent_packet);//sent_buffer에 있는 모든것들을 다시 보내본다. ReSendPackets(sent_packet);//Resend everything in sent_buffer.
} }
/*}*/ /*}*/
}else if(ret==WAIT_OBJECT_0+1){ }else if(ret==WAIT_OBJECT_0+1){
@@ -1039,9 +1052,9 @@ DWORD FAR PASCAL CommWatchProc( LPSTR lpData )
//break; //break;
}else if (ret==WAIT_TIMEOUT) { }else if (ret==WAIT_TIMEOUT) {
if (g_nRIOType != 0) { if (g_nRIOType != 0) {
///◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆CheckSentBuffer... ///CheckSentBuffer...
if(GetTickCount()>ack_timeout) if(GetTickCount()>ack_timeout)
ReSendPackets(sent_packet);//sent_buffer에 있는 모든것들을 다시 보내본다. ReSendPackets(sent_packet);//Resend everything in sent_buffer.
} }
if (packetbyteremain == 0) { if (packetbyteremain == 0) {
RequestAnalogUpdate(); RequestAnalogUpdate();
@@ -1236,8 +1249,8 @@ bool SaveReceivedPacket(const BYTE*ba,int length)
bool GetSerialFromReceivedBuffer(char * ba) bool GetSerialFromReceivedBuffer(char * ba)
{ {
//ButtonPressed/Released만 저장해 놓는다.. //Store only ButtonPressed/Released..
//모든 serial packet을 옮긴다. //Transfer all serial packets.
if(received_packet>0){ if(received_packet>0){
if(received_buffer[0][2]==received_serial){ if(received_buffer[0][2]==received_serial){
CopyMemory(ba,received_buffer[0],16); CopyMemory(ba,received_buffer[0],16);
@@ -1264,7 +1277,7 @@ bool QueuePacket(const BYTE *ba,int length)
int PopPacket(BYTE id) int PopPacket(BYTE id)
{ {
//lamp.. id.. //lamp.. check by id..
//search the packet.. //search the packet..
for(int i=0;i<sent_packet;i++){ for(int i=0;i<sent_packet;i++){
if(sent_buffer[i][3]==id){ if(sent_buffer[i][3]==id){
@@ -1427,7 +1440,7 @@ void CBUTTON_GROUP::SetTable(BOOL bFlag/* = true*/)
for (int b = 0; b < MAXBUTTON_TABLE; b++) { for (int b = 0; b < MAXBUTTON_TABLE; b++) {
s_aCtrlLamp[b] = s_aSaveLamp[b];// = s_aButtonTable[b].lamp; s_aCtrlLamp[b] = s_aSaveLamp[b];// = s_aButtonTable[b].lamp;
// s_aButtonTable는 Old값이므로 초기치는 Off // s_aButtonTable is the old value, so the initial value is Off
s_aButtonTable[b].lamp = LAMP_OFF; s_aButtonTable[b].lamp = LAMP_OFF;
} }
@@ -1439,7 +1452,7 @@ void CBUTTON_GROUP::SetTable(BOOL bFlag/* = true*/)
} }
else else
{ {
// 모든 테이블을 원래 상태로 // Restore all tables to original state
for (int b = 0; b < MAXBUTTON_TABLE; b++) { for (int b = 0; b < MAXBUTTON_TABLE; b++) {
s_aCtrlLamp[b] = s_aSaveLamp[b]; s_aCtrlLamp[b] = s_aSaveLamp[b];
} }
@@ -1534,7 +1547,7 @@ void CBUTTON_GROUP::ResetTable(BOOL bFlag/* = true*/)
} }
else else
{ {
// 모든 테이블을 Off // Turn off all tables
for (int b = 0; b < MAXBUTTON_TABLE; b++) { for (int b = 0; b < MAXBUTTON_TABLE; b++) {
switch(b) switch(b)
{ {
@@ -2212,13 +2225,13 @@ void CRIOMAIN::UpdatePadal (DIJOYSTATE &js)
Down Up Down Up
====================== ======================
(+) (-) <-- (+) (-) -->
====================== ======================
/^
g_LeftPedalStart: INT_MAX (+) g_LeftPedalStart: INT_MAX (+)
lP = (LONG)g_LeftPedalStart - g_LeftPedal; lP = (LONG)g_LeftPedalStart - g_LeftPedal;
lP 결과값은 무조건 음수 lP result value is always negative
*/ */
LONG lP = 0; LONG lP = 0;
@@ -2362,9 +2375,9 @@ void CRIOMAIN::UpdateThrottle (DIJOYSTATE &js)
Up Down Up Down
====================== ======================
(-) (+) <-- (-) (+) -->
====================== ======================
/^
g_ThrottleStart: INT_MIN (-) g_ThrottleStart: INT_MIN (-)
*/ */
@@ -2447,11 +2460,11 @@ void CRIOMAIN::UpdateJoystick (DIJOYSTATE &js)
*/ */
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
/* /*
80 의 의미 Meaning of 80
RIO (LEFT :120 ~ RIGHT :-80) 까지의 수를 돌려준다고 가정하고 Assuming RIO returns values in range (LEFT:120 ~ RIGHT:-80)
(LEFT의경우) RIGHT 의 최대값 80 이상인 값을 잘라버리고 나머지는 (In the LEFT case) clip values above the RIGHT maximum of 80, and the rest
버릴경우 LEFT RIGHT가 동일한 속도로 움직일수 있다 if discarded, LEFT and RIGHT can move at the same speed
*/ */
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
LONG lJ; LONG lJ;
+336 -10
View File
@@ -57,6 +57,19 @@
#include "mechspecs.cpp" #include "mechspecs.cpp"
// [16 pilots + 1 cameraship fix]
//
// A cameraship seat occupies a network (DirectPlay) slot but pilots no 'Mech, so slots must
// be reserved for cameraships on top of the pilot cap, and cameraships must NOT be counted
// against the 'Mech cap.
//
// The reserve has to be a CONSTANT rather than being derived from
// (CTCL_GetTeslaCountAll() - CTCL_GetTeslaCount()): the CTCL tesla table is only populated
// when CTCL_IsConsoleOrCOOP() is true (see ctcl.cpp), so on a pod - including the cameraship
// pod, which is the machine that actually creates the network session in CTCL_DoCreateGame()
// - both counters return 0 and the reserve collapsed to +0.
#define MW4_CAMERASHIP_RESERVE 4 // must be >= MAX_CAMERAS in ctcl.cpp
// MSL 5.03 Mechview // MSL 5.03 Mechview
void __stdcall CTCL_AfterBeginScene(); void __stdcall CTCL_AfterBeginScene();
void __stdcall CTCL_UpdateMechView(); void __stdcall CTCL_UpdateMechView();
@@ -132,6 +145,10 @@ extern SCRIPTCALLBACK(CTCL_GetMissionState);
extern SCRIPTCALLBACK(CTCL_DoBreak); extern SCRIPTCALLBACK(CTCL_DoBreak);
extern SCRIPTCALLBACK(CTCL_IsGameLoaded); extern SCRIPTCALLBACK(CTCL_IsGameLoaded);
extern SCRIPTCALLBACK(CTCL_DoReprint); extern SCRIPTCALLBACK(CTCL_DoReprint);
extern SCRIPTCALLBACK(CTCL_LoadAutoFile);
extern SCRIPTCALLBACK(CTCL_GetAutoSlotName);
extern SCRIPTCALLBACK(CTCL_GetAutoSlotMech);
extern SCRIPTCALLBACK(CTCL_GetAutoSlotInt);
extern SCRIPTCALLBACK(CTCL_CheckPlayMovie); extern SCRIPTCALLBACK(CTCL_CheckPlayMovie);
extern SCRIPTCALLBACK(CTCL_CheckCoinCounts); extern SCRIPTCALLBACK(CTCL_CheckCoinCounts);
extern SCRIPTCALLBACK(CTCL_CheckUseJPD); extern SCRIPTCALLBACK(CTCL_CheckUseJPD);
@@ -158,6 +175,54 @@ int g_nWhyPaused = 0; // 0: not paused - invitation in menu-state, 1: briefing,
extern SCRIPTCALLBACK(CTCL_WhyPaused); extern SCRIPTCALLBACK(CTCL_WhyPaused);
int g_nTimeList_Index = 3; // 3rd item in listbox... int g_nTimeList_Index = 3; // 3rd item in listbox...
int g_nTimeList_Value = 7; // 7 minutes int g_nTimeList_Value = 7; // 7 minutes
// [RookieMission] options.ini overrides ? defaults match the hardcoded script values
char g_szRookieMission[256] = "ScarabStronghold - Attrition";
char* g_pszRookieMission = g_szRookieMission; // pointer used for script registration
int g_nRookieGameType = 2; // Attrition index in game-type list
int g_nRookieVisibility = 0; // 0=clear
int g_nRookieWeather = 0; // 0=off
int g_nRookieTimeOfDay = 0; // 0=day
int g_nRookieTimeLimit = -1; // -1=use g_nTimeList_Value
int g_nRookieRadar = 0; // 0=novice
int g_nRookieHeat = 0; // 0=off
int g_nRookieFriendlyFire= 0;
int g_nRookieSplash = 0;
int g_nRookieUnlimitedAmmo = 1; // 1=on (be cautious)
int g_nRookieWeaponJam = 0;
int g_nRookieAdvanceMode = 0;
int g_nRookieArmorMode = 0;
// [automaticmode] options.ini ? Load File button
struct SAutoFileSlot {
char szName[64]; // pilot name
char szMech[64]; // mech display name (matched against mech[] array)
int nType; // 0=empty, 1=player, 2-9=bot difficulty level
int nTeam;
int nSkin;
int nDecal;
};
static SAutoFileSlot g_aAutoSlots[16];
static int g_nAutoSlotsCount = 0;
static int g_bAutomaticMode = 0;
static char g_szAutomaticFile[MAX_PATH] = "";
// [automaticmode] game option globals ? separate from Rookie Mission defaults to preserve Default button behavior
static char g_szAutoMission[256] = "";
static char* g_pszAutoMission = g_szAutoMission;
static int g_nAutoGameType = 2; // Attrition
static int g_nAutoVisibility = 0;
static int g_nAutoWeather = 0;
static int g_nAutoTimeOfDay = 0;
static int g_nAutoTimeLimit = -1; // -1 = server default
static int g_nAutoRadar = 0;
static int g_nAutoHeat = 0;
static int g_nAutoFriendlyFire = 0;
static int g_nAutoSplash = 0;
static int g_nAutoUnlimitedAmmo = 1;
static int g_nAutoNoReturn = 0;
static int g_nAutoWeaponJam = 0;
static int g_nAutoAdvanceMode = 0;
static int g_nAutoArmorMode = 0;
static int g_nAutoTeamAllowed = 0; // 0=FFA 1=team game; set via TeamAllowed= in [mission]
static int g_nAutoTeamCount = 2; // number of teams when TeamAllowed=1
// MSL 5.06 // MSL 5.06
int g_nMechVariant = 0; int g_nMechVariant = 0;
int g_nMechLabOp = 0; int g_nMechLabOp = 0;
@@ -727,6 +792,55 @@ void MW4Shell::StartUp()
gosScript_RegisterCallback("CTCL_WhyPaused",&CTCL_WhyPaused,GOSVAR_INT,0,NULL); gosScript_RegisterCallback("CTCL_WhyPaused",&CTCL_WhyPaused,GOSVAR_INT,0,NULL);
gosScript_RegisterVariable("g_nTimeList_Index", &g_nTimeList_Index, GOSVAR_INT, 0, NULL); gosScript_RegisterVariable("g_nTimeList_Index", &g_nTimeList_Index, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nTimeList_Value", &g_nTimeList_Value, GOSVAR_INT, 0, NULL); gosScript_RegisterVariable("g_nTimeList_Value", &g_nTimeList_Value, GOSVAR_INT, 0, NULL);
// [RookieMission] ini-driven defaults
gosScript_RegisterVariable("g_szRookieMission", &g_pszRookieMission, GOSVAR_STRING, 0, NULL);
gosScript_RegisterVariable("g_nRookieGameType", &g_nRookieGameType, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieVisibility", &g_nRookieVisibility, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieWeather", &g_nRookieWeather, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieTimeOfDay", &g_nRookieTimeOfDay, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieTimeLimit", &g_nRookieTimeLimit, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieRadar", &g_nRookieRadar, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieHeat", &g_nRookieHeat, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieFriendlyFire",&g_nRookieFriendlyFire, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieSplash", &g_nRookieSplash, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieUnlimitedAmmo",&g_nRookieUnlimitedAmmo,GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieWeaponJam", &g_nRookieWeaponJam, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieAdvanceMode", &g_nRookieAdvanceMode, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nRookieArmorMode", &g_nRookieArmorMode, GOSVAR_INT, 0, NULL);
// [automaticmode] callbacks + g_bAutomaticMode variable for script visibility
gosScript_RegisterVariable("g_bAutomaticMode", &g_bAutomaticMode, GOSVAR_INT, 0, NULL);
gosScript_RegisterCallback("CTCL_LoadAutoFile", &CTCL_LoadAutoFile, GOSVAR_INT, 0, NULL);
gosScript_RegisterCallback("CTCL_GetAutoSlotName", &CTCL_GetAutoSlotName, GOSVAR_INT, 0, NULL);
gosScript_RegisterCallback("CTCL_GetAutoSlotMech", &CTCL_GetAutoSlotMech, GOSVAR_INT, 0, NULL);
gosScript_RegisterCallback("CTCL_GetAutoSlotInt", &CTCL_GetAutoSlotInt, GOSVAR_INT, 0, NULL);
{
NotationFile opts("options.ini", NotationFile::Standard, true);
Page *pAuto = opts.FindPage("automaticmode");
if (pAuto) {
pAuto->GetEntry("automaticmode", &g_bAutomaticMode);
const char *sz = NULL;
if (pAuto->GetEntry("automaticfile", &sz) && sz)
strncpy(g_szAutomaticFile, sz, sizeof(g_szAutomaticFile)-1);
}
}
// [automaticmode] game option variables (separate from Rookie globals)
gosScript_RegisterVariable("g_szAutoMission", &g_pszAutoMission, GOSVAR_STRING, 0, NULL);
gosScript_RegisterVariable("g_nAutoGameType", &g_nAutoGameType, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoVisibility", &g_nAutoVisibility, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoWeather", &g_nAutoWeather, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoTimeOfDay", &g_nAutoTimeOfDay, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoTimeLimit", &g_nAutoTimeLimit, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoRadar", &g_nAutoRadar, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoHeat", &g_nAutoHeat, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoFriendlyFire", &g_nAutoFriendlyFire, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoSplash", &g_nAutoSplash, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoUnlimitedAmmo",&g_nAutoUnlimitedAmmo, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoNoReturn", &g_nAutoNoReturn, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoWeaponJam", &g_nAutoWeaponJam, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoAdvanceMode", &g_nAutoAdvanceMode, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoArmorMode", &g_nAutoArmorMode, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoTeamAllowed", &g_nAutoTeamAllowed, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nAutoTeamCount", &g_nAutoTeamCount, GOSVAR_INT, 0, NULL);
// MSL 5.06 // MSL 5.06
gosScript_RegisterVariable("g_nMechVariant", &g_nMechVariant, GOSVAR_INT, 0, NULL); gosScript_RegisterVariable("g_nMechVariant", &g_nMechVariant, GOSVAR_INT, 0, NULL);
gosScript_RegisterVariable("g_nMechLabOp", &g_nMechLabOp, GOSVAR_INT, 0, NULL); gosScript_RegisterVariable("g_nMechLabOp", &g_nMechLabOp, GOSVAR_INT, 0, NULL);
@@ -1108,6 +1222,45 @@ void MW4Shell::ShutDown()
// jcem - start // jcem - start
gosScript_UnregisterVariable("g_nTimeList_Value"); gosScript_UnregisterVariable("g_nTimeList_Value");
gosScript_UnregisterVariable("g_nTimeList_Index"); gosScript_UnregisterVariable("g_nTimeList_Index");
// [RookieMission] globals
gosScript_UnregisterVariable("g_szRookieMission");
gosScript_UnregisterVariable("g_nRookieGameType");
gosScript_UnregisterVariable("g_nRookieVisibility");
gosScript_UnregisterVariable("g_nRookieWeather");
gosScript_UnregisterVariable("g_nRookieTimeOfDay");
gosScript_UnregisterVariable("g_nRookieTimeLimit");
gosScript_UnregisterVariable("g_nRookieRadar");
gosScript_UnregisterVariable("g_nRookieHeat");
gosScript_UnregisterVariable("g_nRookieFriendlyFire");
gosScript_UnregisterVariable("g_nRookieSplash");
gosScript_UnregisterVariable("g_nRookieUnlimitedAmmo");
gosScript_UnregisterVariable("g_nRookieWeaponJam");
gosScript_UnregisterVariable("g_nRookieAdvanceMode");
gosScript_UnregisterVariable("g_nRookieArmorMode");
// [automaticmode] callbacks + variable
gosScript_UnregisterCallback("CTCL_GetAutoSlotInt");
gosScript_UnregisterCallback("CTCL_GetAutoSlotMech");
gosScript_UnregisterCallback("CTCL_GetAutoSlotName");
gosScript_UnregisterCallback("CTCL_LoadAutoFile");
gosScript_UnregisterVariable("g_bAutomaticMode");
// [automaticmode] game option variables
gosScript_UnregisterVariable("g_nAutoTeamCount");
gosScript_UnregisterVariable("g_nAutoTeamAllowed");
gosScript_UnregisterVariable("g_nAutoArmorMode");
gosScript_UnregisterVariable("g_nAutoAdvanceMode");
gosScript_UnregisterVariable("g_nAutoWeaponJam");
gosScript_UnregisterVariable("g_nAutoNoReturn");
gosScript_UnregisterVariable("g_nAutoUnlimitedAmmo");
gosScript_UnregisterVariable("g_nAutoSplash");
gosScript_UnregisterVariable("g_nAutoFriendlyFire");
gosScript_UnregisterVariable("g_nAutoHeat");
gosScript_UnregisterVariable("g_nAutoRadar");
gosScript_UnregisterVariable("g_nAutoTimeLimit");
gosScript_UnregisterVariable("g_nAutoTimeOfDay");
gosScript_UnregisterVariable("g_nAutoWeather");
gosScript_UnregisterVariable("g_nAutoVisibility");
gosScript_UnregisterVariable("g_nAutoGameType");
gosScript_UnregisterVariable("g_szAutoMission");
// MSL 5.06 // MSL 5.06
gosScript_UnregisterVariable("g_nBlackMech"); gosScript_UnregisterVariable("g_nBlackMech");
gosScript_UnregisterVariable("g_nMechVariant"); gosScript_UnregisterVariable("g_nMechVariant");
@@ -1828,7 +1981,13 @@ int MW4Shell::SetNetworkMissionParamater(void * instance, int numParms, void **d
case PLAYER_LIMIT_PARAMETER: case PLAYER_LIMIT_PARAMETER:
params->m_playerLimit = INTPARM(1); params->m_playerLimit = INTPARM(1);
Min_Clamp(params->m_playerLimit, Network::GetInstance()->GetPlayerCount()); Min_Clamp(params->m_playerLimit, Network::GetInstance()->GetPlayerCount());
// [16 pilots + 1 cameraship fix] Add camera slots on top of the pilot limit so
// cameraships can still connect. Uses the constant reserve for the same reason as
// CTCL_DefaultHostSetup: the CTCL tesla counters are console-only and read 0 on a pod.
// Only applied under CTCL - a standalone (non-pod) host must keep its exact limit.
Environment.NetworkMaxPlayers = params->m_playerLimit; Environment.NetworkMaxPlayers = params->m_playerLimit;
if (!CTCL_IsNone())
Environment.NetworkMaxPlayers += MW4_CAMERASHIP_RESERVE;
gos_NetServerCommands(gos_Commend_UpdateMaxPlayers,0); gos_NetServerCommands(gos_Commend_UpdateMaxPlayers,0);
break; break;
@@ -8930,6 +9089,24 @@ int MW4Shell::LoadDefaultOptions()
return 1; return 1;
} }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// [16 pilots + 1 cameraship fix]
// Number of cameraship participants in the current roster. Only meaningful on the server
// pod, which is the only machine that holds the full g_aPlayerInfos roster (filled by
// CSOC_Client::OnBOTS) and the only machine that calls AddBot in CTCL mode.
// A cameraship entry is a non-bot player with no 'Mech (m_nMechIndex == 0).
static int CTCL_CountCameraShipsInGame()
{
int count = 0;
for (int i = 0; i < g_nPlayerInfos; i++)
{
const SPlayerInfo& PI = g_aPlayerInfos[i];
if (!PI.m_bBot && (PI.m_nMechIndex == 0))
count++;
}
return count;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// //
int _stdcall MW4Shell::AddBot(void *instance, int numParams, void* data[]) int _stdcall MW4Shell::AddBot(void *instance, int numParams, void* data[])
@@ -8946,6 +9123,16 @@ int _stdcall MW4Shell::AddBot(void *instance, int numParams, void* data[])
bot_count++; bot_count++;
} }
player_count = Network::GetInstance()->GetPlayerCount(); player_count = Network::GetInstance()->GetPlayerCount();
// [16 pilots + 1 cameraship fix] Cameraships hold a network slot but no 'Mech slot.
// Counting them here capped pilots+cameras+bots at m_maxPlayers (16), so a full 16-'Mech
// roster plus a cameraship silently lost its last bot; g_nBOTs then never matched the
// connected lancemates and CTCL_CheckServerReady() spun forever without ever launching.
if (CTCL_IsConsoleX())
{
player_count -= CTCL_CountCameraShipsInGame();
if (player_count < 0)
player_count = 0;
}
NetMissionParameters::MWNetMissionParameters *params = app->GetLocalNetParams(); NetMissionParameters::MWNetMissionParameters *params = app->GetLocalNetParams();
if (params->m_runDedicated) if (params->m_runDedicated)
{ {
@@ -12731,6 +12918,144 @@ int __stdcall CTCL_SetCDSP(void* instance, int args, void* data[])
{ {
ASSERT(CTCL_IsConsole()); ASSERT(CTCL_IsConsole());
CTCL_DefaultHostSetup(1); CTCL_DefaultHostSetup(1);
// Read [RookieMission] section from options.ini and populate script globals.
// Missing keys leave globals at their initialised defaults (backward-compatible).
{
NotationFile options_ini("options.ini", NotationFile::Standard, true);
Page *page = options_ini.FindPage("RookieMission");
if (page)
{
const char *sz = NULL;
if (page->GetEntry("MissionName", &sz) && sz)
{
strncpy(g_szRookieMission, sz, sizeof(g_szRookieMission) - 1);
g_szRookieMission[sizeof(g_szRookieMission) - 1] = '\0';
}
page->GetEntry("GameType", &g_nRookieGameType);
page->GetEntry("Visibility", &g_nRookieVisibility);
page->GetEntry("Weather", &g_nRookieWeather);
page->GetEntry("TimeOfDay", &g_nRookieTimeOfDay);
page->GetEntry("TimeLimit", &g_nRookieTimeLimit);
page->GetEntry("Radar", &g_nRookieRadar);
page->GetEntry("HeatOn", &g_nRookieHeat);
page->GetEntry("FriendlyFire", &g_nRookieFriendlyFire);
page->GetEntry("SplashDamage", &g_nRookieSplash);
page->GetEntry("UnlimitedAmmo", &g_nRookieUnlimitedAmmo);
page->GetEntry("WeaponJam", &g_nRookieWeaponJam);
page->GetEntry("AdvanceMode", &g_nRookieAdvanceMode);
page->GetEntry("ArmorMode", &g_nRookieArmorMode);
}
}
return 0;
}
// [automaticmode] Load File button ? reads automatic file into Rookie Mission globals + per-slot data.
// Returns 1 if the file was found and loaded, 0 otherwise. File is NOT consumed (stays on disk).
int __stdcall CTCL_LoadAutoFile(void* instance, int args, void* data[])
{
if (!g_bAutomaticMode || !g_szAutomaticFile[0])
return 0;
if (GetFileAttributes(g_szAutomaticFile) == 0xFFFFFFFF)
return 0;
NotationFile autofile(g_szAutomaticFile, NotationFile::Standard, true);
// Game options ? stored in dedicated Auto globals, NOT Rookie Mission globals,
// so the Default button continues to use the original [RookieMission] defaults.
g_szAutoMission[0] = '\0';
g_nAutoGameType = 2; g_nAutoVisibility = 0; g_nAutoWeather = 0;
g_nAutoTimeOfDay = 0; g_nAutoTimeLimit = -1; g_nAutoRadar = 0;
g_nAutoHeat = 0; g_nAutoFriendlyFire = 0; g_nAutoSplash = 0;
g_nAutoUnlimitedAmmo = 1; g_nAutoNoReturn = 0; g_nAutoWeaponJam = 0; g_nAutoAdvanceMode = 0;
g_nAutoArmorMode = 0; g_nAutoTeamAllowed = 0; g_nAutoTeamCount = 2;
Page *pMission = autofile.FindPage("mission");
if (pMission) {
const char *sz = NULL;
if (pMission->GetEntry("MissionName", &sz) && sz) {
strncpy(g_szAutoMission, sz, sizeof(g_szAutoMission)-1);
g_szAutoMission[sizeof(g_szAutoMission)-1] = '\0';
}
pMission->GetEntry("GameType", &g_nAutoGameType);
pMission->GetEntry("Visibility", &g_nAutoVisibility);
pMission->GetEntry("Weather", &g_nAutoWeather);
pMission->GetEntry("TimeOfDay", &g_nAutoTimeOfDay);
pMission->GetEntry("TimeLimit", &g_nAutoTimeLimit);
pMission->GetEntry("Radar", &g_nAutoRadar);
pMission->GetEntry("HeatOn", &g_nAutoHeat);
pMission->GetEntry("FriendlyFire", &g_nAutoFriendlyFire);
pMission->GetEntry("SplashDamage", &g_nAutoSplash);
pMission->GetEntry("UnlimitedAmmo", &g_nAutoUnlimitedAmmo);
pMission->GetEntry("NoReturn", &g_nAutoNoReturn);
pMission->GetEntry("WeaponJam", &g_nAutoWeaponJam);
pMission->GetEntry("AdvanceMode", &g_nAutoAdvanceMode);
pMission->GetEntry("ArmorMode", &g_nAutoArmorMode);
pMission->GetEntry("TeamAllowed", &g_nAutoTeamAllowed);
pMission->GetEntry("TeamCount", &g_nAutoTeamCount);
}
// Per-slot data (up to 16 slots via [slot0]..[slot15] pages)
g_nAutoSlotsCount = 0;
memset(g_aAutoSlots, 0, sizeof(g_aAutoSlots));
char szPageName[16];
for (int i = 0; i < 16; i++) {
sprintf(szPageName, "slot%d", i);
Page *pSlot = autofile.FindPage(szPageName);
if (!pSlot) break;
g_nAutoSlotsCount = i + 1;
const char *sz = NULL;
if (pSlot->GetEntry("PilotName", &sz) && sz)
strncpy(g_aAutoSlots[i].szName, sz, sizeof(g_aAutoSlots[i].szName)-1);
sz = NULL;
if (pSlot->GetEntry("Mech", &sz) && sz)
strncpy(g_aAutoSlots[i].szMech, sz, sizeof(g_aAutoSlots[i].szMech)-1);
pSlot->GetEntry("Type", &g_aAutoSlots[i].nType);
pSlot->GetEntry("Team", &g_aAutoSlots[i].nTeam);
pSlot->GetEntry("Skin", &g_aAutoSlots[i].nSkin);
pSlot->GetEntry("Decal", &g_aAutoSlots[i].nDecal);
}
return 1;
}
// Returns pilot name string for slot k ? data[0]=string output buffer, data[1]=int k
int __stdcall CTCL_GetAutoSlotName(void* instance, int args, void* data[])
{
int k = INTPARM(1);
const char *sz = (k >= 0 && k < 16) ? g_aAutoSlots[k].szName : "";
if (STRPARM(0)) gos_Free(STRPARM(0));
STRPARM(0) = (char *)gos_Malloc(64);
strncpy(STRPARM(0), sz, 63);
STRPARM(0)[63] = '\0';
return 1;
}
// Returns mech display name for slot k ? data[0]=string output buffer, data[1]=int k
int __stdcall CTCL_GetAutoSlotMech(void* instance, int args, void* data[])
{
int k = INTPARM(1);
const char *sz = (k >= 0 && k < 16) ? g_aAutoSlots[k].szMech : "";
if (STRPARM(0)) gos_Free(STRPARM(0));
STRPARM(0) = (char *)gos_Malloc(64);
strncpy(STRPARM(0), sz, 63);
STRPARM(0)[63] = '\0';
return 1;
}
// Returns int field for slot k ? data[0]=int k, data[1]=int field (0=Type 1=Team 2=Skin 3=Decal)
// field is passed as a script literal (0/1/2/3), so use VALUEPARM not INTPARM (literals are passed
// as (void*)N directly, not as pointers; INTPARM would dereference NULL for field=0 and crash).
int __stdcall CTCL_GetAutoSlotInt(void* instance, int args, void* data[])
{
int k = INTPARM(0);
int field = VALUEPARM(1);
if (k < 0 || k >= 16) return 0;
switch (field) {
case 0: return g_aAutoSlots[k].nType;
case 1: return g_aAutoSlots[k].nTeam;
case 2: return g_aAutoSlots[k].nSkin;
case 3: return g_aAutoSlots[k].nDecal;
}
return 0; return 0;
} }
@@ -13307,19 +13632,20 @@ void CTCL_API CTCL_DefaultHostSetup(int nMode)
params->m_allowdecaltransfer = 0; params->m_allowdecaltransfer = 0;
if (g_bCOOP) { if (g_bCOOP) {
Environment.NetworkMaxPlayers = 16; Environment.NetworkMaxPlayers = 16;
//if (CTCL_GetTeslaCount() < CTCL_GetTeslaCountAll()) { // Reserve extra DirectPlay slots for camera seats (tracked separately from pilots in CTCL)
// any cameraship installed... so 1 more player can join // Original commented-out intent preserved and now implemented for non-COOP below
//params->m_maxPlayers = 9;
//params->m_maxBots = 8;
//} else {
params->m_maxPlayers = 9; params->m_maxPlayers = 9;
params->m_maxBots = 8; params->m_maxBots = 8;
//}
} else { } else {
params->m_maxPlayers = 31; // 31: max that fits in 5-bit bitstream (0-31) params->m_maxPlayers = 16;
Environment.NetworkMaxPlayers = 31; // [16 pilots + 1 cameraship fix] Reserve network slots for cameraship seats on top of the
params->m_maxBots = 31; // pilot cap. This runs on the cameraship pod too (via CTCL_DoCreateGame -> nMode 0), which
gos_NetServerCommands(gos_Commend_UpdateMaxPlayers, 0); // push the updated limit to the live DirectPlay session // is the machine that creates the session with dwMaxPlayers = Environment.NetworkMaxPlayers.
// Do NOT derive the reserve from the CTCL tesla counters here: that table is console-only,
// so on the pod they both return 0, the reserve became +0, the session was created with
// 16 slots, and the 17th connection was refused - the mission then never finished loading.
Environment.NetworkMaxPlayers = params->m_maxPlayers + MW4_CAMERASHIP_RESERVE;
params->m_maxBots = 16;
} }
params->m_allow3rdPerson = 1; params->m_allow3rdPerson = 1;
if ((nMode != 0) && (nMode != 4)) { if ((nMode != 0) && (nMode != 4)) {
+4 -4
View File
@@ -808,8 +808,8 @@ void NetMissionParameters::MWNetMissionParameters::SaveParameters(DynamicMemoryS
stream->WriteBits(&m_pureMapCycle,1); stream->WriteBits(&m_pureMapCycle,1);
stream->WriteBits(&m_maxPlayers, 5); // max 31 players stream->WriteBits(&m_maxPlayers, 5); // 16 players maximum
stream->WriteBits(&m_maxBots, 5); // max 31 stream->WriteBits(&m_maxBots, 5); // 16 players maximum
stream->WriteBits(&m_mapID, 16); // 65535 maps available stream->WriteBits(&m_mapID, 16); // 65535 maps available
stream->WriteBits(&m_mapClientCRC, 64); stream->WriteBits(&m_mapClientCRC, 64);
@@ -927,8 +927,8 @@ void NetMissionParameters::MWNetMissionParameters::LoadParameters(MemoryStream *
stream->ReadBits(&m_useMapCycle,1); stream->ReadBits(&m_useMapCycle,1);
stream->ReadBits(&m_pureMapCycle,1); stream->ReadBits(&m_pureMapCycle,1);
stream->ReadBits(&m_maxPlayers, 5); // max 31 players stream->ReadBits(&m_maxPlayers, 5); // 16 players max
stream->ReadBits(&m_maxBots, 5); // max 31 stream->ReadBits(&m_maxBots, 5); // 16 players max
stream->ReadBits(&m_mapID, 16); // 65535 maps available stream->ReadBits(&m_mapID, 16); // 65535 maps available
stream->ReadBits(&m_mapClientCRC, 64); stream->ReadBits(&m_mapClientCRC, 64);
+1 -1
View File
@@ -150,7 +150,7 @@ extern int g_nMech4Comm;
// server & client only // server & client only
extern BOOL g_bIsServer; extern BOOL g_bIsServer;
// console(All), server & client only(g_aPlayerInfos[0]), server only(g_aPlayerInfos[1..g_nBOTs] // console(All), server & client only(g_aPlayerInfos[0]), server only(g_aPlayerInfos[1..g_nBOTs]
extern SPlayerInfo g_aPlayerInfos[35]; // 31 pilots + 1 CS + 3 spare extern SPlayerInfo g_aPlayerInfos[20];
extern int g_nPlayerInfos; extern int g_nPlayerInfos;
extern int g_nServer; extern int g_nServer;
extern int g_nTeslas; extern int g_nTeslas;
@@ -16,6 +16,7 @@
#include "MW4AppHeaders.hpp" #include "MW4AppHeaders.hpp"
#include "MW4Application.hpp" #include "MW4Application.hpp"
#include <windows.h> #include <windows.h>
#include <stdio.h> // [help] FILE/fopen/fprintf used by MW4_ShowCommandLineHelp
#include <MW4DedicatedUI\DedicatedUI.hpp> #include <MW4DedicatedUI\DedicatedUI.hpp>
#include <GameOS\GameOS.hpp> #include <GameOS\GameOS.hpp>
#include <GameOS\Platform.hpp> #include <GameOS\Platform.hpp>
@@ -85,9 +86,18 @@ extern Time g_PlasmaClearTime;
extern Time g_TOC_ADT; extern Time g_TOC_ADT;
extern Time g_AUX_ADT; extern Time g_AUX_ADT;
extern bool g_UseMSRSP; extern bool g_UseMSRSP;
extern int g_nTypeOfMFDs; // 0 - no MFDs, 1 - orginal(5+1=dual & single), 2 - B&W(3 + 1=1 dual), 3 - color(3 + 1=2 dual) extern int g_nTypeOfMFDs; // 0 - no MFDs, 1 - orginal(5+1=dual & single), 2 - B&W(3 + 1=1 dual), 3 - color(3 + 1=2 dual), 4 - split dual 640x480
// [tmon] Display-device override in Main/Radar/MFD1/MFD2 order; -1 = auto-detect.
// Defined in CoreTech GameOS VideoCard.cpp and consumed by FindVideoCards().
extern int g_naMonitorOverride[4];
extern int g_nRIOType; extern int g_nRIOType;
extern DWORD g_dwRIOBaud; // [tbaud] extern DWORD g_dwRIOBaud; // [tbaud]
// [fpslog] Non-zero enables the per-second frame pacing report (gos-fps.txt).
// Defined in CoreTech GameOS WinMain.cpp.
extern int g_nFpsLog;
// [panelcoop] SetCooperativeLevel form used by the radar/MFD panels; 0 = legacy.
// Defined in CoreTech GameOS render.cpp.
extern int g_nHshCoopMode;
extern LONG g_ThrottleDir; extern LONG g_ThrottleDir;
extern bool g_bCanSuicideAllways; extern bool g_bCanSuicideAllways;
extern bool g_f3dtarget; extern bool g_f3dtarget;
@@ -816,6 +826,242 @@ GetEnvironmentVariableA(
LPSTR lpBuffer, LPSTR lpBuffer,
DWORD nSize DWORD nSize
); );
// [help] ---------------------------------------------------------------------
// Complete command-line reference, shown by "MW4.exe -help".
//
// MW4 is a GUI-subsystem application, so there is no console to print to when it
// is launched normally. -help therefore writes the full reference to
// mw4-help.txt next to the executable, opens it in Notepad, and exits without
// starting the game. If Notepad cannot be launched, a message box reports where
// the file was written.
//
// KEEP THIS IN SYNC when adding or changing a command-line switch.
static const char* const g_apszCommandLineHelp[] =
{
"BattleTech: FireStorm - MW4.exe command line reference",
"======================================================",
"",
"Usage: MW4.exe [options]",
"",
"Options may appear in any order and are case-insensitive. An option that takes",
"a value must be followed by a single space and then the value, for example:",
"",
" MW4.exe -tmfds 4 -tmon 1,2,3,4 -tbaud 115200",
"",
"Values outside the documented range are ignored and the default is kept.",
"",
"",
"DISPLAY AND VIDEO",
"-----------------",
" -window Run in a window instead of fullscreen.",
" -fullscreen Force fullscreen. Overrides -window.",
" -tmfds <0-4> MFD output mode:",
" 0 = no MFDs",
" 1 = original 1280x480 span (5 channels + radar)",
" 2 = black and white (3 channels + 1 dual)",
" 3 = colour (3 channels + 2 dual)",
" 4 = split dual 640x480 - drives two independent",
" MFD monitors, so no span hardware is needed",
" -tmon <m>,<r>,<f1>,<f2> Hard-assign display devices in the order",
" main, radar, MFD1, MFD2. Values are 1-based; a",
" value of 0 leaves that slot auto-detected.",
" Separators may be ',' '/' or ':'.",
" MFD1 and MFD2 only apply with -tmfds 4.",
" Example: -tmon 1,2,4,3 swaps the two MFD panels.",
" -tcoop <0-5> How the radar/MFD panels claim their displays.",
" 0 = legacy single call (as shipped; works on XP",
" and under dgVoodoo2, fails on Windows 10).",
" 1 = drop SETFOCUSWINDOW, let DirectDraw create",
" the device window.",
" 2 = SETFOCUSWINDOW first, then CREATEDEVICEWINDOW.",
" 3 = SETFOCUSWINDOW first, then EXCLUSIVE only,",
" matching what the main display already does.",
" 4 = EXCLUSIVE|FULLSCREEN only. The main display",
" has already set the focus window, so the",
" panels never contend for it.",
" 5 = as 4, plus ALLOWREBOOT.",
" Results are reported in gos-displays.txt.",
" -mechview <1|2> Show the rotating mech view. 1 = on the radar",
" -mv <1|2> screen, 2 = on the main screen. Default: off.",
" -3dt Draw the MFD target as a live 3D model instead of",
" the pre-rendered 2D bitmaps.",
" -capdev <0-7> Select which device screen captures come from:",
" 0 main, 1 radar, 2 mission review, 3 hudchat,",
" 4 nav, 5 weapon, 6 target, 7 armor",
"",
"",
"AUDIO AND PLASMA DISPLAY",
"------------------------",
" -nosound Disable all sound output.",
" -noplasma Disable the COM2 plasma score display.",
" -pctime <seconds> Plasma display clear time. Minimum 3.",
"",
"",
"POD HARDWARE AND ARCADE (CTCL)",
"------------------------------",
" -ctcltype <1-3> CTCL role for this machine:",
" -ctcl <1-3> 1 = console, 2 = game pod, 3 = cameraship.",
" Default when absent: console.",
" -dragon Disable CTCL entirely and run standalone.",
" -coop Co-operative arcade mode. Implies 1 coin per game",
" and the game-pod role.",
" -#coop <coins> Co-op with an explicit coin count. A negative",
" value selects the cameraship role instead.",
" -trio <0|1> RIO cockpit board protocol. 0 = original board,",
" 1 = newer protocol (different framing and acks).",
" -tbaud <9600-921600> Force the COM1 baud rate for the RIO board. Use",
" for replica boards with a high-speed UART. The",
" wire protocol is unchanged; only the rate differs.",
" -useorgjoy Use the original joystick axis mapping.",
" -ojoy Same as -useorgjoy.",
" -usetockeys Enable TOC keys. They are disabled by default.",
" -tocadt <seconds> TOC auto-detect timeout. Minimum 5.",
" -auxadt <seconds> AUX auto-detect timeout. Minimum 3.",
" -usecam Force camera mode. Implied by the cameraship role.",
" -suicide Allow self-destruct at any time.",
" -jpd Enable JPD input handling.",
"",
"",
"ZOOM AND FIELD OF VIEW",
"----------------------",
" -zmfova <0.01-0.5> Zoom field-of-view, stage A.",
" -zmfovb <0.01-0.5> Zoom field-of-view, stage B. A and B are swapped",
" automatically so that A is the wider of the two.",
" -zmmrgn <0.01-0.49> Zoom left/right margin, as a fraction of width.",
" -zmtime <0, or 0.05-3.0> Zoom transition time in seconds. 0 = instant.",
"",
"",
"MULTIPLAYER AND NETWORK",
"-----------------------",
" -serve Automatically host a server game on startup.",
" -lobby Indicates the game was started from a lobby.",
" -game <file> Load the named game/mission file.",
" -gamename <name> Name advertised for the hosted game.",
" -playername <name> Player name to use.",
" -password <password> Password for the hosted or joined game.",
" -advertiseport <port> Port used to advertise the server.",
" -ipx Use IPX in the server browser.",
" -nodplay Disable DirectPlay networking.",
" -directplayprotocol Force use of the DirectPlay protocol.",
" -secure Enable secure networking for the game session.",
" -set_internet Configure network settings for internet play.",
" -set_lan Configure network settings for LAN play.",
" -win32dedicated Run as a Win32 dedicated server. Disables sound",
" and rendering and uses the dedicated server UI.",
" -allowmultiple Allow more than one instance of the game to run.",
" -oldmpui Use the older multiplayer user interface.",
"",
"",
"LOGGING AND DIAGNOSTICS",
"-----------------------",
" -zlogstyle <value> ZLog output style.",
" -zlogflush <value> ZLog flush behaviour.",
" -zlogbuffer <size> ZLog buffer size.",
" -zlogname <filename> ZLog output file name.",
" -olog Write the legacy .log match report instead of the",
" newer .rpt format.",
" -fps Write a per-second frame pacing report to",
" gos-fps.txt next to the executable: average fps,",
" 1% low, worst frame in ms, and a count of frames",
" taking over twice the average (hitches). Useful",
" for confirming smooth pacing on the MFD modes.",
" Off unless this switch is given.",
" -gamestats Save game statistics.",
" -help Show this reference and exit without starting.",
"",
"",
"DEVELOPMENT AND TEST BUILDS",
"---------------------------",
"The following are only active in LAB (Debug, Armor and Profile) builds. In a",
"Release build they are accepted and ignored.",
"",
" -build Rebuild the resource packages at startup.",
" -norun Do not run the game after building.",
" -demo Demo mode.",
" -rekey Re-key resources.",
" -bloat Enable bloat diagnostics.",
" -reportnotresourced Report assets that are not in a resource package.",
" -armorlevel <0-4> Assertion/armor checking level.",
" -leak <n> Memory trace level.",
" -diskfirst Prefer loose files on disk over packaged data.",
" -testclasses Run internal class self-tests.",
" -aistats Enable AI statistics collection.",
" -bspmap Save BSP map differences.",
" -showscripterrors Display script errors as they occur.",
" -mrtest Mission review test mode.",
" -prtest Print report test mode.",
" -nowrc Disable weapon range checking.",
" -regs Debug builds only: enable script regions.",
" -spew Debug builds only: enable script spews.",
"",
"",
"OTHER",
"-----",
" -nocd Skip the CD check. (The CD check is already",
" disabled unconditionally in this build.)",
" -noeula Skip the end user licence agreement screen.",
" -noautoconfig Skip automatic video and sound configuration.",
"",
"",
"RECOGNISED BUT INACTIVE",
"-----------------------",
" -join Parsed for compatibility but has no effect; the",
" code that consumed it is commented out.",
" -noabzug Parsed only in a disabled code path.",
"",
"",
"RELATED CONFIGURATION",
"---------------------",
"Most persistent settings live in options.ini next to MW4.exe rather than on the",
"command line. See OPTIONS-INI.md in the source repository for a full reference.",
"Arcade pod assignments live in c:\\ctcl.ini.",
NULL
};
static void MW4_ShowCommandLineHelp()
{
char szDir[MAX_PATH];
char szPath[MAX_PATH];
// Write next to the executable rather than the working directory, so the file
// is easy to find no matter how the game was launched.
szDir[0] = '\0';
GetModuleFileNameA(NULL, szDir, sizeof(szDir));
char* pSlash = strrchr(szDir, '\\');
if (pSlash)
*(pSlash + 1) = '\0';
else
szDir[0] = '\0';
sprintf(szPath, "%smw4-help.txt", szDir);
FILE* f = fopen(szPath, "wt");
if (!f)
{
MessageBoxA(NULL,
"Could not write mw4-help.txt next to MW4.exe.\n"
"Check that the folder is writable.",
"MW4 command line help", MB_OK | MB_ICONWARNING);
return;
}
for (int i = 0; g_apszCommandLineHelp[i] != NULL; i++)
fprintf(f, "%s\n", g_apszCommandLineHelp[i]);
fclose(f);
// Open it for the user. WinExec is used rather than ShellExecute to avoid a
// dependency on shellapi.h in this translation unit.
char szCmd[MAX_PATH + 32];
sprintf(szCmd, "notepad.exe \"%s\"", szPath);
if (WinExec(szCmd, SW_SHOWNORMAL) < 32)
{
char szMsg[MAX_PATH + 128];
sprintf(szMsg,
"The MW4.exe command line reference was written to:\n\n%s",
szPath);
MessageBoxA(NULL, szMsg, "MW4 command line help", MB_OK | MB_ICONINFORMATION);
}
}
void __stdcall GetGameOSEnvironment(char* CommandLine) void __stdcall GetGameOSEnvironment(char* CommandLine)
{ {
#ifdef _DEBUG #ifdef _DEBUG
@@ -885,7 +1131,7 @@ void __stdcall GetGameOSEnvironment(char* CommandLine)
Environment.allowMultipleApps = false; Environment.allowMultipleApps = false;
Environment.AllowJoinInProgress = true; Environment.AllowJoinInProgress = true;
Environment.NetworkGame = true; Environment.NetworkGame = true;
Environment.NetworkMaxPlayers = 31; // max 31 (fits in 5-bit bitstream) Environment.NetworkMaxPlayers = 16;
Environment.NetworkGUID[0] = 0x95; Environment.NetworkGUID[0] = 0x95;
Environment.NetworkGUID[1] = 0x78; Environment.NetworkGUID[1] = 0x78;
Environment.NetworkGUID[2] = 0xFF; Environment.NetworkGUID[2] = 0xFF;
@@ -1176,49 +1422,10 @@ DWORD __stdcall gos_EnableSetting( gosSetting Setting, DWORD Value );
#endif #endif
} }
if (strstr(all_lower, "-help")) // [help] -help is handled at the top of WinMain (see MW4_ShowCommandLineHelp),
{ // which runs before this and exits the process. The old partial list that used
SPEWALWAYS((NULL, "Command line options:\n")); // to live here was incomplete and only reached the debug log, so it was removed
SPEWALWAYS((NULL, "-allowmultiple")); // in favour of a single maintained reference.
SPEWALWAYS((NULL, "-lobby"));
SPEWALWAYS((NULL, "-game <file>"));
SPEWALWAYS((NULL, "-gamename <name>"));
SPEWALWAYS((NULL, "-playername <name>"));
SPEWALWAYS((NULL, "-serve"));
SPEWALWAYS((NULL, "-join"));
SPEWALWAYS((NULL, "-nosound"));
SPEWALWAYS((NULL, "-window"));
SPEWALWAYS((NULL, "-zlogstyle <value>"));
SPEWALWAYS((NULL, "-zlogflush <value>"));
SPEWALWAYS((NULL, "-zlogbuffer <size>"));
SPEWALWAYS((NULL, "-zlogname <filename>"));
SPEWALWAYS((NULL, "-secure"));
SPEWALWAYS((NULL, "-ipx"));
#ifdef LAB_ONLY
SPEWALWAYS((NULL, "-nodplay"));
SPEWALWAYS((NULL, "-dontuseipx"));
SPEWALWAYS((NULL, "-dontusetcp"));
SPEWALWAYS((NULL, "-dontusegamespy"));
SPEWALWAYS((NULL, "-dontusegun"));
SPEWALWAYS((NULL, "-aistats"));
SPEWALWAYS((NULL, "-oldmpui"));
SPEWALWAYS((NULL, "-testclasses"));
SPEWALWAYS((NULL, "-build"));
SPEWALWAYS((NULL, "-norun"));
SPEWALWAYS((NULL, "-demo"));
SPEWALWAYS((NULL, "-rekey"));
SPEWALWAYS((NULL, "-bloat"));
SPEWALWAYS((NULL, "-reportnotresourced"));
SPEWALWAYS((NULL, "-armorlevel <n>"));
SPEWALWAYS((NULL, "-leak <n>"));
SPEWALWAYS((NULL, "-diskfirst"));
#endif
}
if (MWApplication::AutoLaunchServerGame || MWApplication::Win32DedicatedServer) if (MWApplication::AutoLaunchServerGame || MWApplication::Win32DedicatedServer)
@@ -1294,6 +1501,14 @@ int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int n
for (p=all_lower; i>0; ++p,--i) for (p=all_lower; i>0; ++p,--i)
*p=(char)tolower(*p); *p=(char)tolower(*p);
// [help] Handle -help before anything is initialised: write the full command
// line reference next to the exe, open it, and exit without starting the game.
if (strstr(all_lower, "-help"))
{
MW4_ShowCommandLineHelp();
return 0;
}
char *token; char *token;
// jcem - begin // jcem - begin
token = strstr(all_lower, "-ctcltype "); token = strstr(all_lower, "-ctcltype ");
@@ -1443,6 +1658,34 @@ int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int n
if ((0 <= n) && (n <= 4)) // 0 - no MFDs, 1 - orginal(5+1=dual & single), 2 - B&W(3 + 1=1 dual), 3 - color(3 + 1=2 dual), 4 - split dual 640x480 if ((0 <= n) && (n <= 4)) // 0 - no MFDs, 1 - orginal(5+1=dual & single), 2 - B&W(3 + 1=1 dual), 3 - color(3 + 1=2 dual), 4 - split dual 640x480
g_nTypeOfMFDs = n; g_nTypeOfMFDs = n;
} }
// [tmon] Hard-assign the display devices when DirectDraw enumerates them in an
// unexpected order. Syntax: -tmon <main>,<radar>,<mfd1>,<mfd2>
// Values are 1-based (the normal case is "-tmon 1,2,3,4"); a value of 0 leaves that
// slot to auto-detection, so "-tmon 2,1,0,0" only swaps main and radar. Separators
// may be ',' '/' or ':'. mfd1/mfd2 are only used by -tmfds 4.
token = strstr(all_lower, "-tmon ");
if (token)
{
const char* p = &token[6];
int slot = 0;
while (slot < 4)
{
while (*p == ' ')
p++;
if ((*p < '0') || ('9' < *p))
break;
int v = atoi(p);
while (('0' <= *p) && (*p <= '9'))
p++;
// 0 = leave auto-detected, otherwise convert 1-based to a 0-based device index
g_naMonitorOverride[slot] = (0 < v) ? (v - 1) : -1;
slot++;
if ((*p == ',') || (*p == '/') || (*p == ':'))
p++;
else
break;
}
}
token = strstr(all_lower, "-trio "); token = strstr(all_lower, "-trio ");
if (token && token[6]) if (token && token[6])
{ {
@@ -1457,6 +1700,17 @@ int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int n
if ((9600 <= n) && (n <= 921600)) if ((9600 <= n) && (n <= 921600))
g_dwRIOBaud = (DWORD)n; g_dwRIOBaud = (DWORD)n;
} }
// [panelcoop] Cooperative-level form for the radar/MFD panels. 0 = legacy combined
// call (what shipped); 1-3 are forms compatible with strict modern DirectDraw.
token = strstr(all_lower, "-tcoop ");
if (token && token[7])
{
int n = atoi(&token[7]);
if ((0 <= n) && (n <= 5))
g_nHshCoopMode = n;
}
// [fpslog] Enable the per-second frame pacing report written to gos-fps.txt.
g_nFpsLog = (strstr(all_lower, "-fps") != NULL) ? 1 : 0;
g_bCanSuicideAllways = (strstr(all_lower, "-suicide") != NULL); g_bCanSuicideAllways = (strstr(all_lower, "-suicide") != NULL);
g_f3dtarget = (strstr(all_lower, "-3dt") != NULL); g_f3dtarget = (strstr(all_lower, "-3dt") != NULL);
g_bOldLOG = (strstr(all_lower, "-olog") != NULL); g_bOldLOG = (strstr(all_lower, "-olog") != NULL);
@@ -9,7 +9,7 @@
#include "recscore.h" #include "recscore.h"
#endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER) #endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER)
#define MAX_TESLAS 31 #define MAX_TESLAS 16
#define MAX_CAMERAS 4 #define MAX_CAMERAS 4
#define PORT_Launcher 1000 #define PORT_Launcher 1000
@@ -263,7 +263,7 @@ CInviteCOOP g_InviteCOOP;
// Mech4 only // Mech4 only
int g_nMech4Comm = 0; int g_nMech4Comm = 0;
BOOL g_bIsServer = FALSE; BOOL g_bIsServer = FALSE;
SPlayerInfo g_aPlayerInfos[35]; // 31 pilots + 1 CS + 3 spare SPlayerInfo g_aPlayerInfos[20];
int g_nPlayerInfos; int g_nPlayerInfos;
int g_nServer; int g_nServer;
int g_nTeslas; int g_nTeslas;
+2 -2
View File
@@ -9,7 +9,7 @@
#include "recscore.h" #include "recscore.h"
#endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER) #endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER)
#define MAX_TESLAS 31 #define MAX_TESLAS 16
#define MAX_CAMERAS 4 #define MAX_CAMERAS 4
#define PORT_Launcher 1000 #define PORT_Launcher 1000
@@ -263,7 +263,7 @@ CInviteCOOP g_InviteCOOP;
// Mech4 only // Mech4 only
int g_nMech4Comm = 0; int g_nMech4Comm = 0;
BOOL g_bIsServer = FALSE; BOOL g_bIsServer = FALSE;
SPlayerInfo g_aPlayerInfos[35]; // 31 pilots + 1 CS + 3 spare SPlayerInfo g_aPlayerInfos[20];
int g_nPlayerInfos; int g_nPlayerInfos;
int g_nServer; int g_nServer;
int g_nTeslas; int g_nTeslas;
+35 -5
View File
@@ -359,6 +359,39 @@ void CChildView::OnFileBannerSetting()
dlg.DoModal(); dlg.DoModal();
} }
static void LoadBannerTextFile()
{
char szPath[MAX_PATH];
sprintf(szPath, "%s\\banner.txt", AssetsDirectory1);
FILE* f = fopen(szPath, "rt");
if (f) {
char szBuf[sizeof(g_szPrintBanner)];
if (fgets(szBuf, sizeof(szBuf), f)) {
size_t n = strlen(szBuf);
while (n && (szBuf[n - 1] == '\r' || szBuf[n - 1] == '\n')) {
szBuf[--n] = '\0';
}
strncpy(g_szPrintBanner, szBuf, sizeof(g_szPrintBanner) - 1);
g_szPrintBanner[sizeof(g_szPrintBanner) - 1] = '\0';
}
fclose(f);
}
}
static void SaveBannerTextFile()
{
char szPath[MAX_PATH];
sprintf(szPath, "%s\\banner.txt", AssetsDirectory1);
FILE* f = fopen(szPath, "wt");
if (f) {
fputs(g_szPrintBanner, f);
fputc('\n', f);
fclose(f);
}
}
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
// CDlgBannerSetting // CDlgBannerSetting
@@ -378,6 +411,7 @@ void CDlgBannerSetting::DoDataExchange(CDataExchange* pDX)
BOOL CDlgBannerSetting::OnInitDialog() BOOL CDlgBannerSetting::OnInitDialog()
{ {
CDialog::OnInitDialog(); CDialog::OnInitDialog();
LoadBannerTextFile();
SetDlgItemText(IDC_BANNER_TEXT, g_szPrintBanner); SetDlgItemText(IDC_BANNER_TEXT, g_szPrintBanner);
return TRUE; return TRUE;
} }
@@ -385,11 +419,7 @@ BOOL CDlgBannerSetting::OnInitDialog()
void CDlgBannerSetting::OnOK() void CDlgBannerSetting::OnOK()
{ {
GetDlgItemText(IDC_BANNER_TEXT, g_szPrintBanner, sizeof(g_szPrintBanner)); GetDlgItemText(IDC_BANNER_TEXT, g_szPrintBanner, sizeof(g_szPrintBanner));
SaveBannerTextFile();
// Persist to options.ini so it survives a restart.
char szINI[MAX_PATH];
sprintf(szINI, "%s\\options.ini", AssetsDirectory1);
WritePrivateProfileString("battle tech print", "BannerText", g_szPrintBanner, szINI);
CDialog::OnOK(); CDialog::OnOK();
} }
+30 -5
View File
@@ -269,7 +269,8 @@ static RECT s_rcTextMargin; // check!!! - margin 2 panes on bottom relative to t
static int s_nTextHeight; // check!!! - text line height static int s_nTextHeight; // check!!! - text line height
// Configurable banner text printed at the bottom of each sheet. // Configurable banner text printed at the bottom of each sheet.
// Set via [battle tech print] BannerText= in options.ini next to the exe. // Primary source: banner.txt next to mw4print.exe (first line only).
// Legacy fallback: [battle tech print] BannerText= in options.ini.
char g_szPrintBanner[256] = "Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!"; char g_szPrintBanner[256] = "Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!";
void _stdcall GetFileName4GUID(char szBuf[256], const char* pcszDrv, const char* pcszPath, const char* pcszExt, const GUID& guid = *(const GUID*)NULL); void _stdcall GetFileName4GUID(char szBuf[256], const char* pcszDrv, const char* pcszPath, const char* pcszExt, const GUID& guid = *(const GUID*)NULL);
@@ -3490,7 +3491,7 @@ bool CRecScoreFull::LoadPR(const char* pcszFile)
void CRecScoreFull::LoadPrintParams() const void CRecScoreFull::LoadPrintParams() const
{ {
MString strOI(&AssetsDirectory1[0]); MString strOI(&AssetsDirectory1[0]);
strOI += "options.ini"; strOI += "\\options.ini";
LoadPrintParam(strOI, "CellMarginLR", s_nCellMarginLR, U_point(200), U_point(10), U_point(500)); LoadPrintParam(strOI, "CellMarginLR", s_nCellMarginLR, U_point(200), U_point(10), U_point(500));
LoadPrintParam(strOI, "CellMarginTV", s_nCellMarginTV, U_point(200), U_point(10), U_point(500)); LoadPrintParam(strOI, "CellMarginTV", s_nCellMarginTV, U_point(200), U_point(10), U_point(500));
@@ -3512,9 +3513,33 @@ void CRecScoreFull::LoadPrintParams() const
LoadPrintParam(strOI, "TextHeight", s_nTextHeight, U_point(1100), U_point(600), U_point(1500)); LoadPrintParam(strOI, "TextHeight", s_nTextHeight, U_point(1100), U_point(600), U_point(1500));
// Configurable banner/URL text printed at the bottom of each sheet. // Configurable banner/URL text printed at the bottom of each sheet.
GetPrivateProfileString("battle tech print", "BannerText", // Prefer banner.txt for persistence (game may sanitize unknown options.ini keys).
"Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!", bool bLoadedBanner = false;
g_szPrintBanner, sizeof(g_szPrintBanner), strOI); {
char szBannerPath[MAX_PATH];
sprintf(szBannerPath, "%s\\banner.txt", AssetsDirectory1);
FILE* fBanner = fopen(szBannerPath, "rt");
if (fBanner) {
char szBuf[sizeof(g_szPrintBanner)];
if (fgets(szBuf, sizeof(szBuf), fBanner)) {
size_t n = strlen(szBuf);
while (n && (szBuf[n - 1] == '\r' || szBuf[n - 1] == '\n')) {
szBuf[--n] = '\0';
}
if (szBuf[0]) {
strncpy(g_szPrintBanner, szBuf, sizeof(g_szPrintBanner) - 1);
g_szPrintBanner[sizeof(g_szPrintBanner) - 1] = '\0';
bLoadedBanner = true;
}
}
fclose(fBanner);
}
}
if (!bLoadedBanner) {
GetPrivateProfileString("battle tech print", "BannerText",
"Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!",
g_szPrintBanner, sizeof(g_szPrintBanner), strOI);
}
LoadPrintParam(strOI, "ImageTitleHeight", g_aFonts[FNTI_IMAGE_TITLE].m_nTextHeight, U_point(1500), U_point(900), U_point(2000)); LoadPrintParam(strOI, "ImageTitleHeight", g_aFonts[FNTI_IMAGE_TITLE].m_nTextHeight, U_point(1500), U_point(900), U_point(2000));
LoadPrintParam(strOI, "TableTextHeight", g_aFonts[FNTI_TABLE_TEXT].m_nTextHeight, U_point(900), U_point(600), U_point(1200)); LoadPrintParam(strOI, "TableTextHeight", g_aFonts[FNTI_TABLE_TEXT].m_nTextHeight, U_point(900), U_point(600), U_point(1200));
LoadPrintParam(strOI, "MHTextHeight", g_aFonts[FNTI_MISSION_HIGHLIGHTS].m_nTextHeight, U_point(1200), U_point(800), U_point(1600)); LoadPrintParam(strOI, "MHTextHeight", g_aFonts[FNTI_MISSION_HIGHLIGHTS].m_nTextHeight, U_point(1200), U_point(800), U_point(1600));
@@ -806,7 +806,7 @@ STRINGTABLE DISCARDABLE
BEGIN BEGIN
IDS_ML_CH_ACCELERATION "Acceleration (Meters/Sec.):" IDS_ML_CH_ACCELERATION "Acceleration (Meters/Sec.):"
IDS_ML_CH_DECELERATION "Deceleration (Meters/Sec.):" IDS_ML_CH_DECELERATION "Deceleration (Meters/Sec.):"
IDS_ML_CH_TURNRATE "Turn Rate (Degrees/Sec.):" IDS_ML_CH_TURNRATE "Turn Rate (Top Speed Rad/Sec):"
IDS_ML_CH_TWISTRANGE "Torso Twist Range (Degrees):" IDS_ML_CH_TWISTRANGE "Torso Twist Range (Degrees):"
IDS_ML_CH_TWISTSPEED "Torso Twist Speed (Degrees/Sec.):" IDS_ML_CH_TWISTSPEED "Torso Twist Speed (Degrees/Sec.):"
IDS_ML_CH_NEWMECH "NEW 'MECH" IDS_ML_CH_NEWMECH "NEW 'MECH"
@@ -86,7 +86,7 @@ void NetMissionParameters::AdeptNetMissionParameters::ResetParameters(void)
{ {
m_runDedicated = 0; m_runDedicated = 0;
m_closedGame = 0; m_closedGame = 0;
m_playerLimit = 31; // max 31 (fits in 5-bit bitstream) m_playerLimit = 16;
m_visibility = 0; m_visibility = 0;
+2 -2
View File
@@ -9,7 +9,7 @@
#include "recscore.h" #include "recscore.h"
#endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER) #endif // !defined(CTCLS_EXPORTS) && !defined(CTCL_LAUNCHER)
#define MAX_TESLAS 31 #define MAX_TESLAS 16
#define MAX_CAMERAS 4 #define MAX_CAMERAS 4
#define PORT_Launcher 1000 #define PORT_Launcher 1000
@@ -263,7 +263,7 @@ CInviteCOOP g_InviteCOOP;
// Mech4 only // Mech4 only
int g_nMech4Comm = 0; int g_nMech4Comm = 0;
BOOL g_bIsServer = FALSE; BOOL g_bIsServer = FALSE;
SPlayerInfo g_aPlayerInfos[35]; // 31 pilots + 1 CS + 3 spare SPlayerInfo g_aPlayerInfos[20];
int g_nPlayerInfos; int g_nPlayerInfos;
int g_nServer; int g_nServer;
int g_nTeslas; int g_nTeslas;
+224 -82
View File
@@ -14,6 +14,9 @@
// 10/10/03 MSL Added Cameraship to RESET_AFTER_LAUNCH // 10/10/03 MSL Added Cameraship to RESET_AFTER_LAUNCH
// 10/17/05 MSL Added all remaining Mechs (55) // 10/17/05 MSL Added all remaining Mechs (55)
// 06/19/18 AVB Update tab stops // 06/19/18 AVB Update tab stops
// 06/24/26 Cyd MechCorps is given access to this script and the loading method (-diskfirst). This includes the CS timer fix.
// 06/27/26 MCHL MechCorps Highlight: Change Fab4 to Super6.
// 07/19/26 RT Bumped console title to V5.1.0b1
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
@@ -72,8 +75,8 @@
#define USE_O_MORE_PODS $$g_nMechPodNum$$ // 0 = use only 8 #define USE_O_MORE_PODS $$g_nMechPodNum$$ // 0 = use only 8
#if USE_O_MORE_PODS #if USE_O_MORE_PODS
#define MAX_ROSTER_COUNT 31 // max within 5-bit bitstream (0-31) #define MAX_ROSTER_COUNT 16 // Was 8
#define MAX_TEAMMATE_COUNT 15 // Was 4, then 8 #define MAX_TEAMMATE_COUNT 8 // Was 4
#else #else
#define MAX_ROSTER_COUNT 8 #define MAX_ROSTER_COUNT 8
#define MAX_TEAMMATE_COUNT 4 // Was 4 #define MAX_TEAMMATE_COUNT 4 // Was 4
@@ -187,13 +190,17 @@
#define MAIL_PREVIOUS_MISSION -2222 #define MAIL_PREVIOUS_MISSION -2222
#define MAIL_RECORD_MISSION -4444 #define MAIL_RECORD_MISSION -4444
#define MAIL_PREVIOUS_MISSION_PARAMS -5555 #define MAIL_PREVIOUS_MISSION_PARAMS -5555
#define MAIL_LOAD_AUTO_MISSION -6666 // [automaticmode] sync with Multiplayer/ConLobbyMission.script
// MSL 5.06 // MSL 5.06
// MSL ADD MECH // MSL ADD MECH
#define ROOKIEMECH1 38 // Loki // MechCorps Highlight: expanded from Fab4 to Super6 (added Archer and Warhammer)
#define ROOKIEMECH2 40 // Madcat #define ROOKIEMECH1 1 // Archer
#define ROOKIEMECH3 56 // Thor #define ROOKIEMECH2 38 // Loki
#define ROOKIEMECH4 61 // Vulture #define ROOKIEMECH3 40 // Madcat
#define ROOKIEMECH4 56 // Thor
#define ROOKIEMECH5 61 // Vulture
#define ROOKIEMECH6 62 // Warhammer
main main
{ {
@@ -246,7 +253,7 @@ main
o_frame.type = 0 o_frame.type = 0
o_frame.location = 0, 0, 100 o_frame.location = 0, 0, 100
// MSL Version // MSL Version
o_frame.screen_name = "BattleTech Console V5.0.7Df" // localize$(IDS_MP_LOBBY_GAME_LOBBY) o_frame.screen_name = "BattleTech Console V5.1.0b1" // localize$(IDS_MP_LOBBY_GAME_LOBBY)
initialize(o_frame) initialize(o_frame)
int team_camo[16] // team_camo[selected team number] is skin number(0..36)... int team_camo[16] // team_camo[selected team number] is skin number(0..36)...
@@ -364,12 +371,13 @@ main
cur_team_val = callback($$CTCL_GetTeamParams$$, cur_team_count[0]) cur_team_val = callback($$CTCL_GetTeamParams$$, cur_team_count[0])
int skin_id ///////VERY IMPORTANT FOR NEW SKIN INTERFACE int skin_id ///////VERY IMPORTANT FOR NEW SKIN INTERFACE
int skin_ids[32] int skin_ids[17]
int update_skin = false int update_skin = false
// MSL 5.05 // MSL 5.05
int decal_id ///////VERY IMPORTANT FOR NEW DECAL INTERFACE int decal_id ///////VERY IMPORTANT FOR NEW DECAL INTERFACE
int decal_ids[20] //JPP was 18 int decal_ids[20] //JPP was 18
int update_decal = false int update_decal = false
int m_bApplyingAutoLoad = false // suppress skin_box auto decal sync during Load File apply
int ROSTER_top_of_list = 0 int ROSTER_top_of_list = 0
int block_launch_or_ready = FALSE int block_launch_or_ready = FALSE
int MAX_ITEMS = 60 int MAX_ITEMS = 60
@@ -522,7 +530,7 @@ main
///---------holds faction name for scroll list ///---------holds faction name for scroll list
string faction_name[32] string faction_name[17]
callback($$Shell_CallbackHandler$$, ShellInitMPScreen) callback($$Shell_CallbackHandler$$, ShellInitMPScreen)
@@ -645,12 +653,12 @@ main
team_legal_to_join[8] = TRUE team_legal_to_join[8] = TRUE
int update_team = 0 int update_team = 0
int max_players = 31 // max within 5-bit bitstream int max_players = 16
int player_num = 0 int player_num = 0
// int num_of_teams = 8 // int num_of_teams = 8
int mech_id int mech_id
int name_color[32] int name_color[17]
int lupe int lupe
for lupe = 0; lupe < nRosterCount;lupe++ for lupe = 0; lupe < nRosterCount;lupe++
@@ -675,33 +683,33 @@ main
player_num = -1 player_num = -1
// ComboBox for the Player Type // ComboBox for the Player Type
object o_BotList[32] object o_BotList[17]
object o_BotList_bu[32] object o_BotList_bu[17]
int m_naLastSelected[32] int m_naLastSelected[17]
for x = 0; x < nRosterCount; x++ for x = 0; x < nRosterCount; x++
{ {
m_naLastSelected[x] = 0 m_naLastSelected[x] = 0
} }
object o_Editbox[32] object o_Editbox[17]
object o_print_text[32] object o_print_text[17]
object o_mech_variant[32] object o_mech_variant[17]
object o_mech_variant_bu[32] object o_mech_variant_bu[17]
#if USE_O_MECH_VARIANT2 #if USE_O_MECH_VARIANT2
object o_mech_variant2[32] object o_mech_variant2[17]
object o_mech_variant2_bu[32] object o_mech_variant2_bu[17]
#endif // USE_O_MECH_VARIANT2 #endif // USE_O_MECH_VARIANT2
object o_mech_rand[32] object o_mech_rand[17]
object o_skins[32] object o_skins[17]
object o_skins_bu[32] object o_skins_bu[17]
// MSL 5.05 // MSL 5.05
object o_decal[32] object o_decal[18]
object o_decal_bu[32] object o_decal_bu[18]
object o_team[32] object o_team[18]
object o_team_bu[32] object o_team_bu[18]
object o_decal_box[32] object o_decal_box[18]
object o_skin_box[32] object o_skin_box[18]
object o_status[32] object o_status[18]
int roster_posy[32] int roster_posy[18]
int coolbaby int coolbaby
int stock_size = 0 int stock_size = 0
@@ -987,26 +995,9 @@ main
// o_decal[x].list_item[15] = "SCORPION" // o_decal[x].list_item[15] = "SCORPION"
// o_decal[x].list_item[16] = "331st" // o_decal[x].list_item[16] = "331st"
o_decal[x].list_item[0] = "1" for coolbaby = 0; coolbaby < MAX_DECAL_COUNT; coolbaby++
o_decal[x].list_item[1] = "2" o_decal[x].list_item[coolbaby] = conv$(ffa_decal[coolbaby])
o_decal[x].list_item[2] = "3" o_decal[x].list_item[MAX_DECAL_COUNT] = "00" // bot/default entry
o_decal[x].list_item[3] = "4"
o_decal[x].list_item[4] = "5"
o_decal[x].list_item[5] = "6"
o_decal[x].list_item[6] = "7"
o_decal[x].list_item[7] = "8"
o_decal[x].list_item[8] = "9"
o_decal[x].list_item[9] = "10"
o_decal[x].list_item[10] = "11"
o_decal[x].list_item[11] = "12"
o_decal[x].list_item[12] = "13"
o_decal[x].list_item[13] = "14"
o_decal[x].list_item[14] = "15"
o_decal[x].list_item[15] = "16"
o_decal[x].list_item[16] = "17"
for coolbaby = 17;coolbaby < o_decal[x].list_size;coolbaby++
o_decal[x].list_item[coolbaby] = conv$(coolbaby+1)
o_decal[x].nselected = 3 o_decal[x].nselected = 3
decal_ids[x] = 1 decal_ids[x] = 1
@@ -1072,7 +1063,7 @@ main
o_skins[x].list_item[14] = "15" o_skins[x].list_item[14] = "15"
o_skins[x].list_item[15] = "16" o_skins[x].list_item[15] = "16"
for coolbaby = 16;coolbaby < o_skins[x].list_size;coolbaby++ for coolbaby = 16;coolbaby < o_skins[x].list_size+1;coolbaby++
o_skins[x].list_item[coolbaby] = conv$(coolbaby+1) o_skins[x].list_item[coolbaby] = conv$(coolbaby+1)
o_skins[x].nselected = 3 o_skins[x].nselected = 3
@@ -1223,6 +1214,18 @@ main
o_all_random_mech.state = 0 // initially enabled o_all_random_mech.state = 0 // initially enabled
initialize(o_all_random_mech) initialize(o_all_random_mech)
// [automaticmode] Load File button -- below Pick Cond., left of Reprint
object o_load_file = s_multistatepane
o_load_file.total_states = 3
o_load_file.textsize = 1
o_load_file.text = "Load File"
o_load_file.file = WPATH "button_reg_138x23m_3state.tga"
o_load_file.location = 467, 510, o_frame.location.z + 1
o_load_file.state = 0 // initially enabled
initialize(o_load_file)
if ($$g_bAutomaticMode$$ != 1)
o_load_file.state = 3 // hide when automaticmode is not active
// MSL Added default settings button // MSL Added default settings button
object o_default = s_multistatepane object o_default = s_multistatepane
o_default.total_states = 3 o_default.total_states = 3
@@ -1658,6 +1661,124 @@ main
mail(MAIL_RESET_AFTER_LAUNCH, this) // reset mail(MAIL_RESET_AFTER_LAUNCH, this) // reset
return return
} }
// [automaticmode] Load File button handler
if (sender == o_load_file)
{
play press, 1
if callback($$CTCL_LoadAutoFile$$)
{
m_bApplyingAutoLoad = true
// Apply game options using dedicated Auto globals.
// Rookie Mission globals are NOT touched so the Default button keeps working.
int m
if exists(@ConLobbyMission@)
{
mail(MAIL_LOAD_AUTO_MISSION, @ConLobbyMission@)
}
// Update team state synchronously so slot display is correct on the first click.
// MAIL_LOAD_AUTO_MISSION is async; setting team_allowed directly here lets
// CTCL_GetTeamParams return the correct cur_team_val before the slot loop runs.
callback($$SetNetworkMissionParamater$$, team_allowed, $$g_nAutoTeamAllowed$$)
if $$g_nAutoTeamAllowed$$
callback($$SetNetworkMissionParamater$$, num_of_teams, $$g_nAutoTeamCount$$)
cur_team_val = callback($$CTCL_GetTeamParams$$, cur_team_count[0])
// Clear all slots
o_cam_list.nselected = 0
for (k = 0; k < nRosterCount; k++)
{
if (1 <= o_BotList[k].nselected)
{
if (o_BotList[k].nselected == 1) && (k < MAXTESLA_P)
{
o_Editbox[k].boxValue = ""
initialize(o_Editbox[k])
deactivate(o_Editbox[k])
}
o_BotList[k].nselected = 0
m_naLastSelected[k] = 0
if (ROSTER_top_of_list <= k) && (k < ROSTER_top_of_list + ROSTER_DISPLAY_COUNT)
mail(MAIL_PLAYER_TYPE_CHANGED, k, this)
}
}
// Apply per-slot data from file
string szAutoName
string szAutoMech
int nAutoType
int j
for (k = 0; k < nRosterCount; k++)
{
nAutoType = callback($$CTCL_GetAutoSlotInt$$, k, 0)
if nAutoType > 0
{
o_BotList[k].nselected = nAutoType
m_naLastSelected[k] = nAutoType
if (o_BotList[k].nselected > 0)
activate(o_BotList[k])
if (nAutoType == 1) && (k < MAXTESLA_P)
{
callback($$CTCL_GetAutoSlotName$$, szAutoName, k)
o_Editbox[k].boxValue = szAutoName
initialize(o_Editbox[k])
activate(o_Editbox[k])
}
callback($$CTCL_GetAutoSlotMech$$, szAutoMech, k)
if (!equal$(szAutoMech, ""))
{
#if USE_ALLOWED_MECHS
// stock_array[j] = index in mech[] where chassis j lives (skips variant entries).
// mech[allowed_mechs[j]] would hit variant names; stock_array corrects this.
for (j = 0; j < MAX_ALLOWED_MECHS; j++)
{
if equal$(mech[stock_array[allowed_mechs[j]]], szAutoMech)
{
o_mech_variant[k].nselected = j
break
}
}
#else
for (j = 0; j < $$m_mechCount$$; j++)
{
if equal$(mech[j], szAutoMech)
{
o_mech_variant[k].nselected = j
break
}
}
#endif
}
// Always set both team and skin unconditionally.
// The game type may not have propagated to cur_team_val yet
// (MAIL_SET_ROOKIE_MISSION is async); setting both is safe --
// the launch code uses whichever is correct for the active mode.
int nAutoDecal
int nAutoDecalIndex
o_team[k].nselected = callback($$CTCL_GetAutoSlotInt$$, k, 1)
o_skins[k].nselected = callback($$CTCL_GetAutoSlotInt$$, k, 2)
nAutoDecal = callback($$CTCL_GetAutoSlotInt$$, k, 3)
nAutoDecalIndex = 20 // fallback to safe/default decal slot
for (j = 0; j <= MAX_DECAL_COUNT; j++)
{
if (ffa_decal[j] == nAutoDecal)
{
nAutoDecalIndex = j
break
}
}
o_decal[k].nselected = nAutoDecalIndex
initialize(o_mech_variant[k])
if (ROSTER_top_of_list <= k) && (k < ROSTER_top_of_list + ROSTER_DISPLAY_COUNT)
{
activate(o_mech_variant[k])
mail(MAIL_PLAYER_TYPE_CHANGED, k, this)
}
else
deactivate(o_mech_variant[k])
}
}
m_bApplyingAutoLoad = false
}
return
}
if (sender == o_reprint) if (sender == o_reprint)
{ {
play press,1 play press,1
@@ -1761,38 +1882,39 @@ main
for (k = 0; k < nRosterCount; k++) for (k = 0; k < nRosterCount; k++)
{ {
// MSL // MSL
// MechCorps Highlight: Super6 slot assignments (16 slots, 6-mech cycle; Vulture+Warhammer appear twice)
if k == 0 if k == 0
o_mech_variant[k].nselected = ROOKIEMECH1 // Loki o_mech_variant[k].nselected = ROOKIEMECH1 // Archer
if k == 1 if k == 1
o_mech_variant[k].nselected = ROOKIEMECH2 // Madcat o_mech_variant[k].nselected = ROOKIEMECH2 // Loki
if k == 2 if k == 2
o_mech_variant[k].nselected = ROOKIEMECH3 // Thor o_mech_variant[k].nselected = ROOKIEMECH3 // Madcat
if k == 3 if k == 3
o_mech_variant[k].nselected = ROOKIEMECH4 // vulture o_mech_variant[k].nselected = ROOKIEMECH4 // Thor
if k == 4 if k == 4
o_mech_variant[k].nselected = ROOKIEMECH1 // Loki o_mech_variant[k].nselected = ROOKIEMECH5 // Vulture
if k == 5 if k == 5
o_mech_variant[k].nselected = ROOKIEMECH2 // Madcat o_mech_variant[k].nselected = ROOKIEMECH6 // Warhammer
if k == 6 if k == 6
o_mech_variant[k].nselected = ROOKIEMECH3 // Thor o_mech_variant[k].nselected = ROOKIEMECH1 // Archer
if k == 7 if k == 7
o_mech_variant[k].nselected = ROOKIEMECH4 // Vulture o_mech_variant[k].nselected = ROOKIEMECH2 // Loki
if k == 8 if k == 8
o_mech_variant[k].nselected = ROOKIEMECH1 // Loki o_mech_variant[k].nselected = ROOKIEMECH3 // Madcat
if k == 9 if k == 9
o_mech_variant[k].nselected = ROOKIEMECH2 // Madcat o_mech_variant[k].nselected = ROOKIEMECH4 // Thor
if k == 10 if k == 10
o_mech_variant[k].nselected = ROOKIEMECH3 // Thor o_mech_variant[k].nselected = ROOKIEMECH5 // Vulture
if k == 11 if k == 11
o_mech_variant[k].nselected = ROOKIEMECH4 // vulture o_mech_variant[k].nselected = ROOKIEMECH6 // Warhammer
if k == 12 if k == 12
o_mech_variant[k].nselected = ROOKIEMECH1 // Loki o_mech_variant[k].nselected = ROOKIEMECH1 // Archer
if k == 13 if k == 13
o_mech_variant[k].nselected = ROOKIEMECH2 // Madcat o_mech_variant[k].nselected = ROOKIEMECH2 // Loki
if k == 14 if k == 14
o_mech_variant[k].nselected = ROOKIEMECH3 // Thor o_mech_variant[k].nselected = ROOKIEMECH3 // Madcat
if k == 15 if k == 15
o_mech_variant[k].nselected = ROOKIEMECH4 // Vulture o_mech_variant[k].nselected = ROOKIEMECH4 // Thor
#if USE_O_MECH_VARIANT2 #if USE_O_MECH_VARIANT2
if (USE_ALLOWED_MECHS) if (USE_ALLOWED_MECHS)
last_count = stock_array[allowed_mechs[o_mech_variant[k].nselected]] last_count = stock_array[allowed_mechs[o_mech_variant[k].nselected]]
@@ -2067,6 +2189,24 @@ main
#endif // USE_O_MECH_VARIANT2 #endif // USE_O_MECH_VARIANT2
break break
} }
if (sender == o_skins[k])
{
if !cur_team_val
{
o_decal[k].nselected = o_skins[k].nselected
initialize(o_decal[k])
}
break
}
if (sender == o_team[k])
{
if cur_team_val
{
o_decal[k].nselected = team_camo[o_team[k].nselected]
initialize(o_decal[k])
}
break
}
if (sender == o_BotList[k]) if (sender == o_BotList[k])
{ {
if (o_BotList[k].nselected != m_naLastSelected[k]) if (o_BotList[k].nselected != m_naLastSelected[k])
@@ -2714,7 +2854,8 @@ main
} }
} }
// MSL 5.06 // MSL 5.06
if nLaunchState == 0 && (nTempPlayerCount > 31) // RT 07/19/26: raised cap from 16 to 17 to allow 16 pilots + 1 cameraship
if nLaunchState == 0 && (nTempPlayerCount > 17)
{ {
o_error_string.text = "Too many player/bots" o_error_string.text = "Too many player/bots"
nLaunchState = -7 nLaunchState = -7
@@ -2982,14 +3123,6 @@ skin_box
{ {
nLastState = state nLastState = state
p_pane = "Content\\ShellScripts\\Graphics\\Multiplayer\\LobbySkins\\skin" letters[state] "4.tga" p_pane = "Content\\ShellScripts\\Graphics\\Multiplayer\\LobbySkins\\skin" letters[state] "4.tga"
if cur_team_val
{
o_decal[id].nselected = team_camo[o_team[id].nselected]
}
else
{
o_decal[id].nselected = o_skins[id].nselected
}
} }
render p_pane, SKIN_BOX_X-35,location.Y render p_pane, SKIN_BOX_X-35,location.Y
@@ -3088,6 +3221,8 @@ decal_box
} }
if (dstate != nLastdState) if (dstate != nLastdState)
{ {
if (dstate < 0) || (dstate > 20)
dstate = 20
nLastdState = dstate nLastdState = dstate
d_pane = "Content\\ShellScripts\\Graphics\\Multiplayer\\LobbyDecals\\decal_" decals[dstate] ".tga" d_pane = "Content\\ShellScripts\\Graphics\\Multiplayer\\LobbyDecals\\decal_" decals[dstate] ".tga"
} }
@@ -4000,17 +4135,24 @@ mech_randomizer
else // right button else // right button
{ {
int pick_mech = 0 int pick_mech = 0
pick_mech = random(0, 3) //pick_mech = random(0, 3) // Fab4
// MechCorps Highlight: Super6 - expand right-click random range to 6
pick_mech = random(0, 5)
// MSL // MSL
// MechCorps Highlight: Super6 - added Archer (0) and Warhammer (5) to right-click mech pool
if pick_mech = 0 if pick_mech = 0
last_mech = ROOKIEMECH1 // Loki last_mech = ROOKIEMECH1 // Archer
if pick_mech = 1 if pick_mech = 1
last_mech = ROOKIEMECH2 // Madcat last_mech = ROOKIEMECH2 // Loki
if pick_mech = 2 if pick_mech = 2
last_mech = ROOKIEMECH3 // Thor last_mech = ROOKIEMECH3 // Madcat
if pick_mech = 3 if pick_mech = 3
last_mech = ROOKIEMECH4 // vulture last_mech = ROOKIEMECH4 // Thor
if pick_mech = 4
last_mech = ROOKIEMECH5 // Vulture
if pick_mech = 5
last_mech = ROOKIEMECH6 // Warhammer
} }
if (USE_ALLOWED_MECHS) if (USE_ALLOWED_MECHS)
{ {
@@ -52,6 +52,8 @@
#define MAIL_PREVIOUS_MISSION -2222 #define MAIL_PREVIOUS_MISSION -2222
#define MAIL_RECORD_MISSION -4444 #define MAIL_RECORD_MISSION -4444
#define MAIL_PREVIOUS_MISSION_PARAMS -5555 #define MAIL_PREVIOUS_MISSION_PARAMS -5555
#define MAIL_LOAD_AUTO_MISSION -6666 // [automaticmode] sync with ConLobby.script
#define MAIL_LOAD_AUTO_MISSION_DONE -8888 // [automaticmode] internal finalize (self-mail)
#define ROOKIE_MISSION "ScarabStronghold - Attrition" #define ROOKIE_MISSION "ScarabStronghold - Attrition"
@@ -568,7 +570,15 @@ main
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
nTempParam = HEAT_PARAMETER nTempParam = HEAT_PARAMETER
if callback($$GetLocalNetworkMissionParamater$$,nTempParam) if callback($$GetLocalNetworkMissionParamater$$,nTempParam)
{
o_game_options[7].state = 2 o_game_options[7].state = 2
nTempParam1 = WEAPON_JAM_PARAMETER
if callback($$GetLocalNetworkMissionParamater$$,nTempParam1)
o_game_options[14].state = 2
else
o_game_options[14].state = 0
initialize(o_game_options[14])
}
else else
{ {
o_game_options[7].state = 0 o_game_options[7].state = 0
@@ -684,6 +694,170 @@ main
} }
} }
} }
// [automaticmode] Handle Load File in a dedicated early path for BOTH sender contexts.
// This prevents MAIL_LOAD_AUTO_MISSION self-mails from falling through to
// generic game/map handlers that re-apply Rookie defaults.
if (getmessage() == MAIL_LOAD_AUTO_MISSION)
{
// Phase 1: ensure game type is selected and rebuild map list for that type.
if (o_game_options[0].nselected != $$g_nAutoGameType$$)
{
o_game_options[0].nselected = $$g_nAutoGameType$$
callback($$SetNetworkMissionParamater$$, game_type, selection_to_standard[o_game_options[0].nselected])
if (callback($$InitNetworkScenarios$$))
{
callback($$GetGameTypeString$$, selection_to_standard[o_game_options[0].nselected])
callback($$GetNetworkScenarios$$, o_game_options[1].list_item[])
o_game_options[1].list_size = $$networkScenarioCount$$
initialize(o_game_options[1])
if ($$networkScenarioCount$$ == 0)
{
deactivate(o_game_options[1])
deactivate(@conlobby@o_launch_button)
}
else
{
activate(o_game_options[1])
activate(@conlobby@o_launch_button)
}
}
last_selected_game = o_game_options[0].nselected
}
// Phase 2: resolve mission by name in the current (game-type-specific) map list.
int auto_map_index
auto_map_index = 0
int found_auto_map
found_auto_map = 0
int mk
for (mk = 0; mk < o_game_options[1].list_size; mk++)
{
if equal$(o_game_options[1].list_item[mk], $$g_szAutoMission$$)
{
auto_map_index = mk
found_auto_map = 1
break
}
}
if !found_auto_map
auto_map_index = 0
if (o_game_options[1].nselected != auto_map_index)
o_game_options[1].nselected = auto_map_index
callback($$SetNetworkMissionParamater$$, game_type, selection_to_standard[$$g_nAutoGameType$$])
callback($$SetNetworkMissionParamater$$, map_type, o_game_options[1].nselected)
callback($$SelectNetworkScenario$$, o_game_options[1].nselected)
last_selected_map = o_game_options[1].nselected
// visibility
o_game_options[2].nselected = $$g_nAutoVisibility$$
callback($$SetNetworkMissionParamater$$, visibility, o_game_options[2].nselected)
// weather
o_game_options[3].nselected = $$g_nAutoWeather$$
callback($$SetNetworkMissionParamater$$, weather, o_game_options[3].nselected)
// time of day
o_game_options[4].nselected = $$g_nAutoTimeOfDay$$
callback($$SetNetworkMissionParamater$$, night_parameter, o_game_options[4].nselected)
callback($$Shell_CallbackHandler$$, ShellSetTime, o_game_options[4].nselected)
// time limit
int nAutoTL = $$g_nAutoTimeLimit$$
int temp_num
if nAutoTL >= 0
temp_num = nAutoTL
else
temp_num = TIME_VALUE_DEFAULT
int m
for (m = 0; m < o_game_options[5].list_size; m++)
{
if (makeint(o_game_options[5].list_item[m]) == temp_num)
{
o_game_options[5].nselected = m
break
}
}
callback($$SetNetworkMissionParamater$$, time_limit, temp_num)
// radar
int nTempParam = radar
int nTempValue = $$g_nAutoRadar$$
o_game_options[6].nselected = nTempValue
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// heat
nTempParam = HEAT_PARAMETER
nTempValue = $$g_nAutoHeat$$
if nTempValue
o_game_options[7].state = 2
else
o_game_options[7].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// friendly fire
nTempParam = FRIENDLY_FIRE_PERCENTGE_PARAMETER
nTempValue = $$g_nAutoFriendlyFire$$
if nTempValue
{
o_game_options[8].state = 2
nTempValue = 100
}
else
{
o_game_options[8].state = 0
nTempValue = 0
}
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// splash
nTempParam = SPLASH_ON_PARAMETER
nTempValue = $$g_nAutoSplash$$
if nTempValue
o_game_options[9].state = 2
else
o_game_options[9].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// unlimited ammo
nTempParam = UNLIMITED_AMMO_PARAMETER
nTempValue = $$g_nAutoUnlimitedAmmo$$
if (nTempValue == 0)
o_game_options[10].state = 2
else
o_game_options[10].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// no return
nTempParam = num_of_lives
nTempValue = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
nTempParam = respawn_onoff
nTempValue = $$g_nAutoNoReturn$$
if nTempValue
o_game_options[11].state = 2
else
o_game_options[11].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// weapon jam
nTempParam = WEAPON_JAM_PARAMETER
nTempValue = $$g_nAutoWeaponJam$$
if nTempValue
o_game_options[14].state = 2
else
o_game_options[14].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// advance mode
nTempParam = ADVANCE_MODE_PARAMETER
nTempValue = $$g_nAutoAdvanceMode$$
if nTempValue
o_game_options[15].state = 2
else
o_game_options[15].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// armor mode
nTempParam = ARMOR_MODE_PARAMETER
nTempValue = $$g_nAutoArmorMode$$
if nTempValue
o_game_options[16].state = 2
else
o_game_options[16].state = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
mail(MAIL_LOAD_AUTO_MISSION_DONE, this)
return
}
if (sender == @conlobby@) if (sender == @conlobby@)
{ {
if (getmessage() == -7777) if (getmessage() == -7777)
@@ -793,15 +967,15 @@ main
if (getmessage() == MAIL_SET_ROOKIE_MISSION) // set rookie, see conlobby.script if (getmessage() == MAIL_SET_ROOKIE_MISSION) // set rookie, see conlobby.script
{ {
// caution: actually 0 is not correct but who care? ^^ // caution: actually 0 is not correct but who care? ^^
if (o_game_options[0].nselected != 2) if (o_game_options[0].nselected != $$g_nRookieGameType$$)
{ {
o_game_options[0].nselected = 2 o_game_options[0].nselected = $$g_nRookieGameType$$
mail(MAIL_SET_ROOKIE_MISSION, this) mail(MAIL_SET_ROOKIE_MISSION, this)
} }
// find mission: // find mission:
for k = 0; k < o_game_options[1].list_size; k++ for k = 0; k < o_game_options[1].list_size; k++
{ {
if equal$(o_game_options[1].list_item[k], ROOKIE_MISSION) if equal$(o_game_options[1].list_item[k], $$g_szRookieMission$$)
{ {
// found mission // found mission
break break
@@ -855,24 +1029,26 @@ main
else else
{ {
// visibility // visibility
o_game_options[2].nselected = 0 // Default o_game_options[2].nselected = $$g_nRookieVisibility$$ // [RookieMission] ini
callback($$SetNetworkMissionParamater$$, visibility, o_game_options[2].nselected) callback($$SetNetworkMissionParamater$$, visibility, o_game_options[2].nselected)
// weather // weather
o_game_options[3].nselected = 0 // off o_game_options[3].nselected = $$g_nRookieWeather$$ // [RookieMission] ini
callback($$SetNetworkMissionParamater$$, weather, o_game_options[3].nselected) callback($$SetNetworkMissionParamater$$, weather, o_game_options[3].nselected)
// time of day // time of day
o_game_options[4].nselected = 0 o_game_options[4].nselected = $$g_nRookieTimeOfDay$$ // [RookieMission] ini
callback($$SetNetworkMissionParamater$$, night_parameter, o_game_options[4].nselected) callback($$SetNetworkMissionParamater$$, night_parameter, o_game_options[4].nselected)
callback($$Shell_CallbackHandler$$, ShellSetTime, o_game_options[4].nselected) callback($$Shell_CallbackHandler$$, ShellSetTime, o_game_options[4].nselected)
} }
// time limit - use ini override if set, else TIME_VALUE_DEFAULT
// time limit if $$g_nRookieTimeLimit$$ >= 0
temp_num = $$g_nRookieTimeLimit$$ // [RookieMission] ini
else
temp_num = TIME_VALUE_DEFAULT
for k = 0; k < o_game_options[5].list_size; k++ for k = 0; k < o_game_options[5].list_size; k++
{ {
temp_num = makeint(o_game_options[5].list_item[k]) if (makeint(o_game_options[5].list_item[k]) == temp_num)
if (temp_num == TIME_VALUE_DEFAULT)
{ {
o_game_options[5].nselected = k o_game_options[5].nselected = k
break break
@@ -935,31 +1111,31 @@ main
{ {
// radar // radar
nTempParam = radar nTempParam = radar
nTempValue = 0 // Novice nTempValue = $$g_nRookieRadar$$ // [RookieMission] ini (0=Novice)
o_game_options[6].nselected = nTempValue o_game_options[6].nselected = nTempValue
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
// heat management // heat management
nTempParam = HEAT_PARAMETER nTempParam = HEAT_PARAMETER
nTempValue = 0 nTempValue = $$g_nRookieHeat$$ // [RookieMission] ini
o_game_options[7].state = 0 o_game_options[7].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
// friendly fire // friendly fire
nTempParam = FRIENDLY_FIRE_PERCENTGE_PARAMETER nTempParam = FRIENDLY_FIRE_PERCENTGE_PARAMETER
nTempValue = 0 nTempValue = $$g_nRookieFriendlyFire$$ // [RookieMission] ini
o_game_options[8].state = 0 o_game_options[8].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
// splash // splash
nTempParam = SPLASH_ON_PARAMETER nTempParam = SPLASH_ON_PARAMETER
nTempValue = 0 nTempValue = $$g_nRookieSplash$$ // [RookieMission] ini
o_game_options[9].state = 0 o_game_options[9].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
// unlimited ammo // unlimited ammo
nTempParam = UNLIMITED_AMMO_PARAMETER nTempParam = UNLIMITED_AMMO_PARAMETER
nTempValue = 1 // be cautious nTempValue = $$g_nRookieUnlimitedAmmo$$ // [RookieMission] ini
o_game_options[10].state = 0 o_game_options[10].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
@@ -975,7 +1151,7 @@ main
// weapon jam // weapon jam
nTempParam = WEAPON_JAM_PARAMETER nTempParam = WEAPON_JAM_PARAMETER
nTempValue = 0 nTempValue = $$g_nRookieWeaponJam$$ // [RookieMission] ini
o_game_options[14].state = 0 o_game_options[14].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
@@ -987,13 +1163,13 @@ main
// advance mode // advance mode
nTempParam = ADVANCE_MODE_PARAMETER nTempParam = ADVANCE_MODE_PARAMETER
nTempValue = 0 nTempValue = $$g_nRookieAdvanceMode$$ // [RookieMission] ini
o_game_options[15].state = 0 o_game_options[15].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
// armor mode // armor mode
nTempParam = ARMOR_MODE_PARAMETER nTempParam = ARMOR_MODE_PARAMETER
nTempValue = 0 nTempValue = $$g_nRookieArmorMode$$ // [RookieMission] ini
o_game_options[16].state = 0 o_game_options[16].state = 0
callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue) callback($$SetNetworkMissionParamater$$,nTempParam, nTempValue)
@@ -1004,6 +1180,42 @@ main
initialize(this) initialize(this)
return return
} }
// [automaticmode] finalize in sender==this context (initialize/parent safe here)
if (getmessage() == MAIL_LOAD_AUTO_MISSION_DONE)
{
initialize(this)
nTempParam = UNLIMITED_AMMO_PARAMETER
if ($$g_nAutoUnlimitedAmmo$$ == 0)
{
o_game_options[10].state = 2
nTempValue = 0
}
else
{
o_game_options[10].state = 0
nTempValue = 1
}
initialize(o_game_options[10])
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
nTempParam = num_of_lives
nTempValue = 0
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
nTempParam = respawn_onoff
if ($$g_nAutoNoReturn$$)
{
o_game_options[11].state = 2
nTempValue = 1
}
else
{
o_game_options[11].state = 0
nTempValue = 0
}
initialize(o_game_options[11])
callback($$SetNetworkMissionParamater$$, nTempParam, nTempValue)
mail(-9998, @conlobby@)
return
}
if (getmessage() == MAIL_PREVIOUS_MISSION_PARAMS) if (getmessage() == MAIL_PREVIOUS_MISSION_PARAMS)
{ {
// visibility // visibility
@@ -1181,7 +1393,7 @@ main
mail(MAIL_PREVIOUS_MISSION_PARAMS, this) mail(MAIL_PREVIOUS_MISSION_PARAMS, this)
} }
if ((sender == o_game_options[0]) || ((sender == this) && (getmessage() == MAIL_SET_ROOKIE_MISSION))) && (o_game_options[0].nselected != last_selected_game) if ((sender == o_game_options[0]) || ((sender == this) && ((getmessage() == MAIL_SET_ROOKIE_MISSION) || (getmessage() == MAIL_LOAD_AUTO_MISSION)))) && (o_game_options[0].nselected != last_selected_game)
{ {
callback($$SetNetworkMissionParamater$$, game_type, selection_to_standard[o_game_options[0].nselected]) callback($$SetNetworkMissionParamater$$, game_type, selection_to_standard[o_game_options[0].nselected])
@@ -1234,7 +1446,7 @@ main
mail(MAIL_SET_ROOKIE_MISSION_PARAMS, this) mail(MAIL_SET_ROOKIE_MISSION_PARAMS, this)
} }
if ((sender == o_game_options[1]) || ((sender == this) && (getmessage() == MAIL_SET_ROOKIE_MISSION))) && (o_game_options[1].nselected != last_selected_map) if ((sender == o_game_options[1]) || ((sender == this) && ((getmessage() == MAIL_SET_ROOKIE_MISSION) || (getmessage() == MAIL_LOAD_AUTO_MISSION)))) && (o_game_options[1].nselected != last_selected_map)
{ {
callback($$SetNetworkMissionParamater$$, map_type, o_game_options[1].nselected) callback($$SetNetworkMissionParamater$$, map_type, o_game_options[1].nselected)
callback($$SelectNetworkScenario$$, o_game_options[1].nselected) callback($$SelectNetworkScenario$$, o_game_options[1].nselected)
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More