A tester ran 4.12.233 under Proton and it died on first mission load. The cause turned out to name itself, and to be a Windows bug we have been shipping the whole time. Wine implements d3dx9_43 itself, and 63 of its 329 exports are stubs. The game imports twenty of them and exactly one is stubbed: D3DXConcatenateMeshes, called once per session from ConsolidateStaticObjects on the first-mission-load latch - so every player hits it on their first race. Wine raised EXCEPTION_WINE_STUB naming the function in plain text, and our own crash filter caught it and wrote a minidump, which is how a self-describing failure arrived as a mystery. The fix is one file, and it was overdue on Windows. d3dx9_43.dll is a HARD import of the exe - only steam_api.dll is delay-loaded - and it is not part of Windows. It ships with the June 2010 DirectX End-User Runtime and nothing else. Any customer on a clean Windows 10 or 11 who never installed that redistributable cannot start the game at all: the loader fails at 0xC0000135 before WinMain, no window, no log line. Every machine this has been built or tested on had the SDK or some older game installed, which is precisely why nobody has ever seen it. pack-dist now extracts the DLL from the SDK's redist cab into dist. Wine prefers an application-directory DLL over its builtin, so the same file closes the Windows failure and the Linux blocker together, and it is verified working on a stock prefix with nothing installed. The SDK terms nominate DXSETUP as the delivery method for this redistributable; the loose DLL is what is verified and is near-universal practice, but that wants confirming before a public release. /LARGEADDRESSAWARE, because the same session reported VmSize 3193MB and an older Wine that died before video init with the address space exhausted. The game is not using that memory: measured here, a standalone mission peaks at 166MB committed against 795MB of address space. The gap is reservations, mapped files, and mostly the graphics driver mapping VRAM apertures into our process - under wined3d's OpenGL path that grows several times over. For a 32-bit process the 2GB address ceiling is a hard limit with nothing to do with RAM, so it is entirely possible to run out of addresses while using 170MB. The flag raises it to 4GB, frees nothing, and changes no behaviour. Worth having on Windows too: 795MB of 2GB is already 40% before 1080p, eight pods, and the 100MB buffer RP412RECORDSIZE takes when recording is armed. And two diagnostics so the next one costs a log line instead of an investigation. The crash filter now decodes 0x80000100 and writes the offending module.function before the minidump; startup probes wine_get_version in ntdll and logs the platform beside the version banner. Neither can appear on Windows - verified: the smoke run's log has the version line and no platform line. Not fixed here, deliberately: the durable version of this is to write the mesh merge by hand, since D3DXSimplifyMesh and D3DXSplitMesh are stubs too and any future mesh work hits the same wall. And the field test ran wined3d, not DXVK, so the child-window Present with GDI panes clipped over it - the risk the assessment led with - is still untested. The doc records both, along with the prediction it got wrong: the windowsapp.lib import was called a load-time failure risk under Wine, and it simply is not one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
915 lines
32 KiB
C++
915 lines
32 KiB
C++
////===========================================================================//
|
|
//// File: rp4lbe4.cc //
|
|
//// Project: MUNGA Brick: Red Planet LBE Application //
|
|
//// Contents: //
|
|
////---------------------------------------------------------------------------//
|
|
//// Date Who Modification //
|
|
//// -------- --- ---------------------------------------------------------- //
|
|
//// //
|
|
////---------------------------------------------------------------------------//
|
|
//// Copyright (C) 1994, Virtual World Entertainment, Inc. //
|
|
//// All Rights reserved worldwide //
|
|
//// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
|
////===========================================================================//
|
|
//
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define _WIN32_WINNT 0x0500
|
|
#define WINVER 0x0500
|
|
|
|
#include <tchar.h>
|
|
#include "rpl4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "rpl4pb.h"
|
|
#include "rpl4fe.h"
|
|
#include "rpl4environ.h"
|
|
#include "rpl4console.h"
|
|
#include "rpl4lobby.h"
|
|
#include "..\munga_l4\l4steamtransport.h"
|
|
#include "..\munga_l4\l4splr.h"
|
|
#include "..\munga_l4\l4mfdview.h" // RPWindowLayout_*
|
|
#include "..\munga_l4\l4joy.h" // RPJoyConfigWizard
|
|
#include "rpl4ver.h"
|
|
#include "rpl4build.h" // generated: RP412_VERSION / RP412_VERSION_LONG
|
|
#include "..\munga\resver.h"
|
|
#include "..\munga\resource.h"
|
|
// added for game status drawing support
|
|
#include "..\munga\player.h"
|
|
#include "..\munga\mission.h"
|
|
#include "..\munga_l4\l4ctrl.h"
|
|
// end add
|
|
#include <DbgHelp.h>
|
|
#include <strsafe.h>
|
|
#include <fstream> // the log is rolled before it is opened
|
|
#include <ctime> // and stamped with the wall clock
|
|
#include <direct.h>
|
|
#include <shellapi.h>
|
|
#include <time.h> // the test-build expiry check
|
|
|
|
#define SPOOL_SIZE 0x600000
|
|
|
|
//#define SCREEN_WIDTH 1024
|
|
//#define SCREEN_HEIGHT 768
|
|
|
|
Byte version[3] = {MAJOR_DATA_VERSION, RELEASE_VERSION, MINOR_DATA_VERSION};
|
|
|
|
char* names[] = {"InitializingState",
|
|
"WaitingForEgg",
|
|
"LoadingMission",
|
|
"WaitingForLaunch",
|
|
"LaunchingMission",
|
|
"RunningMission",
|
|
"EndingMission",
|
|
"StoppingMission",
|
|
"SuspendingMission",
|
|
"ResumingMission",
|
|
"AbortingMission",
|
|
"CreatingMission",
|
|
"ApplicationStateCount"};
|
|
|
|
//##########################################################################
|
|
//################## RP4L4Application Globals ########################
|
|
//##########################################################################
|
|
|
|
extern const char* const ProgName;
|
|
const char* const ProgName = "rpl4";
|
|
Application *rpl4App;
|
|
HWND hWnd;
|
|
|
|
//
|
|
// Unhandled-exception minidump: rpl4crash.dmp lands beside the exe so
|
|
// a crash on a test machine hands back a stack. dbghelp loads lazily -
|
|
// no link-time dependency.
|
|
//
|
|
static LONG WINAPI RPL4CrashDumpFilter(EXCEPTION_POINTERS *exception)
|
|
{
|
|
//
|
|
// Wine raises 0x80000100 (EXCEPTION_WINE_STUB) when the game calls an
|
|
// API Wine declares but has never implemented, and puts the module and
|
|
// function names in the first two exception parameters as plain ANSI
|
|
// strings. Wine prints that to its own stderr - which a player running
|
|
// through Steam never sees - and then this filter caught the exception
|
|
// and wrote a minidump, so a perfectly self-describing failure arrived
|
|
// as a mystery crash. That is exactly how D3DXConcatenateMeshes cost a
|
|
// full investigation on 4.12.233: one stubbed function out of the
|
|
// twenty d3dx9 imports, and nothing in our log named it.
|
|
//
|
|
// Unlike an access violation this is an orderly raise from Wine with
|
|
// the heap intact, so the log stream is safe to use here. Written
|
|
// before the dump, in case the dump is what fails.
|
|
//
|
|
if (exception != NULL &&
|
|
exception->ExceptionRecord != NULL &&
|
|
exception->ExceptionRecord->ExceptionCode == 0x80000100 &&
|
|
exception->ExceptionRecord->NumberParameters >= 2)
|
|
{
|
|
const char *stub_module =
|
|
(const char *) exception->ExceptionRecord->ExceptionInformation[0];
|
|
const char *stub_function =
|
|
(const char *) exception->ExceptionRecord->ExceptionInformation[1];
|
|
|
|
if (stub_module != NULL && stub_function != NULL &&
|
|
!IsBadStringPtrA(stub_module, 128) &&
|
|
!IsBadStringPtrA(stub_function, 128))
|
|
{
|
|
DEBUG_STREAM << "\n*** WINE STUB: " << stub_module << "."
|
|
<< stub_function << " is not implemented by this Wine.\n"
|
|
<< "*** The game called an API Wine only declares. Shipping the"
|
|
<< " real DLL beside the exe overrides Wine's builtin and is"
|
|
<< " usually the whole fix; see docs/RP412-LINUX.md.\n"
|
|
<< std::flush;
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "\n*** WINE STUB: an unimplemented API was called"
|
|
<< " (names unreadable)\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
HMODULE dbghelp = LoadLibraryA("dbghelp.dll");
|
|
if (dbghelp != NULL)
|
|
{
|
|
typedef BOOL (WINAPI *MiniDumpWriteDumpFn)(
|
|
HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
|
|
PMINIDUMP_EXCEPTION_INFORMATION,
|
|
PMINIDUMP_USER_STREAM_INFORMATION,
|
|
PMINIDUMP_CALLBACK_INFORMATION);
|
|
MiniDumpWriteDumpFn write_dump =
|
|
(MiniDumpWriteDumpFn) GetProcAddress(dbghelp, "MiniDumpWriteDump");
|
|
if (write_dump != NULL)
|
|
{
|
|
HANDLE file = CreateFileA("rpl4crash.dmp", GENERIC_WRITE, 0,
|
|
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (file != INVALID_HANDLE_VALUE)
|
|
{
|
|
MINIDUMP_EXCEPTION_INFORMATION dump_info;
|
|
dump_info.ThreadId = GetCurrentThreadId();
|
|
dump_info.ExceptionPointers = exception;
|
|
dump_info.ClientPointers = FALSE;
|
|
write_dump(GetCurrentProcess(), GetCurrentProcessId(), file,
|
|
MiniDumpNormal, &dump_info, NULL, NULL);
|
|
CloseHandle(file);
|
|
}
|
|
}
|
|
}
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
|
|
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (uMsg)
|
|
{
|
|
case WM_CLOSE:
|
|
DestroyWindow(hWnd);
|
|
return 0;
|
|
case WM_DESTROY:
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
case WM_SYSCOMMAND:
|
|
// Alt is a flight control (reverse thrust) - releasing it must
|
|
// not pop the window menu and steal keyboard focus
|
|
if ((wParam & 0xFFF0) == SC_KEYMENU)
|
|
{
|
|
return 0;
|
|
}
|
|
break;
|
|
case WM_EXITSIZEMOVE:
|
|
//
|
|
// A drag or resize just finished. Here rather than in the
|
|
// cockpit's subclass so it also covers the console screen, which
|
|
// is where the window gets moved before there is any cockpit to
|
|
// subclass. No-op unless RP412MFDLAYOUT is save.
|
|
//
|
|
RPWindowLayout_Save();
|
|
return 0;
|
|
}
|
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
|
{
|
|
char filename[50];
|
|
strcpy_s(filename, 50, "rpl4.log");
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// The log keeps the whole night, not just the last race.
|
|
//
|
|
// Every launch used to truncate it, so a tester who played six races
|
|
// and hit trouble in the second had nothing to send: the evidence was
|
|
// overwritten by the races that followed. Collecting it meant copying
|
|
// the file aside between games, which nobody remembers to do while
|
|
// everyone is waiting to start.
|
|
//
|
|
// Appending costs a separator, below, so the runs can still be told
|
|
// apart. RP412LOGAPPEND=0 restores the old behaviour.
|
|
//
|
|
// Rolled once when it gets big rather than left to grow: a session
|
|
// with the per-frame diagnostics turned on writes fast, and a log
|
|
// nobody can open is no more use than one that was overwritten. The
|
|
// previous generation is kept beside it.
|
|
//---------------------------------------------------------------------
|
|
//
|
|
const char *append_setting = getenv("RP412LOGAPPEND");
|
|
bool append_log = !(append_setting != NULL && append_setting[0] == '0');
|
|
|
|
if (append_log)
|
|
{
|
|
const long kMaximumLogBytes = 64L * 1024L * 1024L;
|
|
|
|
std::ifstream probe(filename, std::ios::binary | std::ios::ate);
|
|
if (probe.is_open())
|
|
{
|
|
std::streamoff grown = probe.tellg();
|
|
probe.close();
|
|
if (grown > kMaximumLogBytes)
|
|
{
|
|
remove("rpl4.prev.log");
|
|
rename(filename, "rpl4.prev.log");
|
|
}
|
|
}
|
|
}
|
|
|
|
std::ofstream logfile;
|
|
logfile.open(filename, append_log ? (std::ios::out | std::ios::app)
|
|
: std::ios::out);
|
|
std::cout.rdbuf(logfile.rdbuf());
|
|
|
|
//
|
|
// Where one run stops and the next starts. Wall-clock time, because
|
|
// that is what a tester reports trouble against ("the third race,
|
|
// about nine") and nothing else in the log carries it.
|
|
//
|
|
if (append_log)
|
|
{
|
|
time_t now = time(NULL);
|
|
struct tm local;
|
|
char stamp[32];
|
|
|
|
if (localtime_s(&local, &now) == 0 &&
|
|
strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", &local) > 0)
|
|
{
|
|
DEBUG_STREAM << "\n"
|
|
<< "========================================================\n"
|
|
<< " run started " << stamp << "\n"
|
|
<< "========================================================\n"
|
|
<< std::flush;
|
|
}
|
|
}
|
|
|
|
SetUnhandledExceptionFilter(RPL4CrashDumpFilter);
|
|
|
|
//
|
|
// Which build this is, before anything else can fail. The patch number
|
|
// is this repository's commit count and the hash beside it names the
|
|
// commit, so a log from a test machine says exactly where it came from.
|
|
// A trailing '+' means the tree had uncommitted changes when it was
|
|
// built. See stamp-version.ps1.
|
|
//
|
|
DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush;
|
|
|
|
//
|
|
// And whether this is Windows at all.
|
|
//
|
|
// Wine exports wine_get_version from ntdll and Windows does not, so
|
|
// one GetProcAddress separates the two. It matters because the
|
|
// failure modes differ: Wine implements Direct3D and the D3DX helpers
|
|
// itself, and where a helper is only a stub the game dies at the call
|
|
// with no hint in its own log (see the filter above). A tester's log
|
|
// saying which platform it came from turns that from an investigation
|
|
// into a lookup. Never true on Windows, where the lookup fails and
|
|
// nothing is printed.
|
|
//
|
|
{
|
|
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
|
|
if (ntdll != NULL)
|
|
{
|
|
typedef const char * (__cdecl *WineGetVersionFn)(void);
|
|
WineGetVersionFn wine_get_version =
|
|
(WineGetVersionFn) GetProcAddress(ntdll, "wine_get_version");
|
|
if (wine_get_version != NULL)
|
|
{
|
|
DEBUG_STREAM << "Platform: Wine " << wine_get_version()
|
|
<< " (not Windows) - D3DX comes from Wine's own"
|
|
<< " implementation\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
// load up our environment variables
|
|
//controls
|
|
if(getenv("L4CONTROLS") == NULL)
|
|
putenv("L4CONTROLS=KEYBOARD");
|
|
if(getenv("DPLARG") == NULL)
|
|
putenv("DPLARG=1");
|
|
if(getenv("L4DPLCFG") == NULL)
|
|
putenv("L4DPLCFG=RPDPL.INI");
|
|
if(getenv("MULTISAMPLE") == NULL)
|
|
putenv("MULTISAMPLE=0");
|
|
if(getenv("TARGETFPS") == NULL)
|
|
putenv("TARGETFPS=60");
|
|
if(getenv("MAXPARTICLES") == NULL)
|
|
putenv("MAXPARTICLES=8192");
|
|
//
|
|
// environ.ini: written on first run and read here. The exe owns the
|
|
// template rather than the packaging script laying one down on every
|
|
// unzip, so a tester can drop a new build over an old folder and keep
|
|
// their settings. See rpl4environ.h.
|
|
//
|
|
RPL4Environ_Load();
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Test builds have a shelf life.
|
|
//
|
|
// A tester still racing a fortnight-old binary reports things that were
|
|
// fixed a week ago, and the afternoon spent chasing them is gone. So the
|
|
// build says plainly that it is out of date and stops, rather than
|
|
// running on and being quietly wrong about what it is.
|
|
//
|
|
// This is a nudge, not a lock: the date comes from the machine's own
|
|
// clock, so anyone determined can wind it back, and RP412NOEXPIRY=1 is
|
|
// there for us when an old build has to be run on purpose. It is
|
|
// deliberately not listed in environ.ini - a bypass every tester can see
|
|
// is a bypass every tester will use, and then the build never goes stale
|
|
// for the one person it was meant to stop.
|
|
//
|
|
// $expireDays in stamp-version.ps1 is what sets this, and 0 turns it off
|
|
// for a real release.
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
#if RP412_EXPIRES
|
|
{
|
|
const char *no_expiry = getenv("RP412NOEXPIRY");
|
|
Logical overridden = (no_expiry != NULL && atoi(no_expiry) != 0);
|
|
|
|
__time64_t raw_now = _time64(NULL);
|
|
struct tm today;
|
|
if (!overridden && _localtime64_s(&today, &raw_now) == 0)
|
|
{
|
|
int now_stamp =
|
|
(today.tm_year + 1900) * 10000 + (today.tm_mon + 1) * 100 + today.tm_mday;
|
|
int expiry_stamp =
|
|
RP412_EXPIRY_YEAR * 10000 + RP412_EXPIRY_MONTH * 100 + RP412_EXPIRY_DAY;
|
|
|
|
if (now_stamp > expiry_stamp)
|
|
{
|
|
DEBUG_STREAM << "Build expired on " << RP412_EXPIRY_TEXT
|
|
<< " - refusing to run\n" << std::flush;
|
|
|
|
char notice[512];
|
|
sprintf(notice,
|
|
"This Red Planet test build has expired.\n\n"
|
|
" Build %s\n"
|
|
" Expired %s\n\n"
|
|
"Test builds are good for a fortnight so that nobody spends an "
|
|
"afternoon chasing something that was fixed a week ago.\n\n"
|
|
"Ask for the current one.",
|
|
RP412_VERSION_LONG, RP412_EXPIRY_TEXT);
|
|
MessageBoxA(NULL, notice, "Red Planet - test build expired",
|
|
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
|
return 1;
|
|
}
|
|
|
|
//
|
|
// The last few days get a line in the log, so somebody reading a
|
|
// report can see the build was nearly out rather than wondering.
|
|
//
|
|
struct tm expiry_day;
|
|
memset(&expiry_day, 0, sizeof(expiry_day));
|
|
expiry_day.tm_year = RP412_EXPIRY_YEAR - 1900;
|
|
expiry_day.tm_mon = RP412_EXPIRY_MONTH - 1;
|
|
expiry_day.tm_mday = RP412_EXPIRY_DAY;
|
|
//
|
|
// The END of the expiry day, because that is the rule the check
|
|
// above enforces - the build is good for all of that date and
|
|
// refuses the morning after. Anchoring at midday instead would
|
|
// report one day fewer than the build actually has left.
|
|
//
|
|
expiry_day.tm_hour = 23;
|
|
expiry_day.tm_min = 59;
|
|
expiry_day.tm_sec = 59;
|
|
expiry_day.tm_isdst = -1;
|
|
__time64_t expiry_time = _mktime64(&expiry_day);
|
|
if (expiry_time != (__time64_t) -1)
|
|
{
|
|
int days_left = (int)((expiry_time - raw_now) / (24 * 60 * 60));
|
|
if (days_left <= 3)
|
|
{
|
|
DEBUG_STREAM << "Build expires " << RP412_EXPIRY_TEXT << " ("
|
|
<< days_left << " day(s) left)\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
else if (overridden)
|
|
{
|
|
DEBUG_STREAM << "Build expiry (" << RP412_EXPIRY_TEXT
|
|
<< ") waived by RP412NOEXPIRY\n" << std::flush;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
|
|
|
|
//
|
|
// The frame target and whether drawing interpolates, stated outright.
|
|
// Both were previously invisible: TARGETFPS appeared nowhere in the log,
|
|
// so a run could not be checked afterwards against what it was actually
|
|
// asked for, and interpolation only announced itself when switched OFF.
|
|
// Between them that cost a wasted test run and an ambiguous one.
|
|
//
|
|
// The mismatch note matters because a frame target that is not the
|
|
// physics rate used to mean visibly stepped motion - it no longer does
|
|
// while interpolation is on, which is exactly why the log should say
|
|
// which of the two states it is in.
|
|
//
|
|
{
|
|
const char *interp = getenv("RP412INTERP");
|
|
Logical blending = !(interp != NULL && atoi(interp) == 0);
|
|
const char *physics = getenv("RP412PHYSICSHZ");
|
|
int fps = atoi(getenv("TARGETFPS"));
|
|
int hz = (physics != NULL) ? atoi(physics) : 0;
|
|
|
|
DEBUG_STREAM << "Video: frame target " << fps << " fps, drawing "
|
|
<< (blending ? "interpolated across the physics step" : "on exact physics steps")
|
|
<< std::endl << std::flush;
|
|
|
|
if (!blending && hz > 0 && fps != hz)
|
|
{
|
|
DEBUG_STREAM << "Video: " << fps << " fps against a " << hz
|
|
<< " Hz physics step with interpolation off - motion will step"
|
|
<< std::endl << std::flush;
|
|
}
|
|
}
|
|
|
|
#ifdef RP412_STEAM
|
|
//
|
|
// RP412STEAM=1 (environ.ini or a Steam launch) swaps the wire to
|
|
// ISteamNetworkingSockets. Failure logs the reason and the game
|
|
// carries on over TCP - dev boxes without Steam are unaffected.
|
|
//
|
|
{
|
|
const char *steam_switch = getenv("RP412STEAM");
|
|
if (steam_switch != NULL && atoi(steam_switch) != 0)
|
|
{
|
|
SteamNetTransport_Install();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// GetProcessAffinityMask(GetCurrentProcess(),
|
|
DWORD_PTR res = SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1);
|
|
if (res == 0)
|
|
{
|
|
DWORD err = GetLastError();
|
|
DEBUG_STREAM << "Could not set thread affinity. Err Code = " << err << std::endl << std::flush;
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// Parse command line for an egg notation file name
|
|
//----------------------------------------------------------------------
|
|
//
|
|
int argc;
|
|
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// -spoolstats <file.spl>: headless spool analysis (RPL4SPOOLSTATS.cpp).
|
|
// Runs before any window, renderer, or engine object exists and exits
|
|
// with the analyzer's code. Output lands on the parent console when
|
|
// launched from one, or a fresh console otherwise.
|
|
//----------------------------------------------------------------------
|
|
//
|
|
for (int spool_arg = 1; spool_arg < argc - 1; ++spool_arg)
|
|
{
|
|
if (_wcsicmp(argv[spool_arg], L"-spoolstats") == 0)
|
|
{
|
|
char spool_path[512];
|
|
size_t converted = 0;
|
|
wcstombs_s(&converted, spool_path, sizeof(spool_path),
|
|
argv[spool_arg + 1], _TRUNCATE);
|
|
if (!AttachConsole(ATTACH_PARENT_PROCESS))
|
|
{
|
|
AllocConsole();
|
|
}
|
|
FILE *spool_out = NULL;
|
|
freopen_s(&spool_out, "CONOUT$", "w", stdout);
|
|
extern int RPL4SpoolStats_Run(const char *path);
|
|
return RPL4SpoolStats_Run(spool_path);
|
|
}
|
|
}
|
|
|
|
Logical run_application = RPL4Application::ParseCommandLine(argc, argv);
|
|
if (!run_application)
|
|
{
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
Start_Registering();
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Create our window. This is only temporary because this will move after
|
|
// the graphics are moved over.
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
WNDCLASS wc;
|
|
|
|
if (!hPrevInstance)
|
|
{
|
|
wc.style = 0;
|
|
wc.lpfnWndProc = (WNDPROC) WndProc;
|
|
wc.cbClsExtra = 0;
|
|
wc.cbWndExtra = 0;
|
|
wc.hInstance = hInstance;
|
|
wc.hIcon = LoadIcon((HINSTANCE) NULL, IDI_APPLICATION);
|
|
wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
|
wc.lpszMenuName = NULL;
|
|
wc.lpszClassName = L"MainWndClass";
|
|
|
|
if (!RegisterClass(&wc))
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
//
|
|
// The style the window is BORN with, rather than one worked out and
|
|
// then thrown away - the old code computed a style and handed
|
|
// CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless.
|
|
//
|
|
// Windowed keeps the full overlapped set on purpose: the sizing
|
|
// border and the maximise box are how the player arranges the
|
|
// cockpit, and mfd_layout.cfg remembers where they left it. The
|
|
// borderless modes are born borderless instead of being restyled a
|
|
// moment later, so there is no framed window on screen first.
|
|
//
|
|
DWORD wsStyle = WS_OVERLAPPEDWINDOW;
|
|
if (L4Application::GetFullscreen() || L4Application::GetFitDisplay())
|
|
{
|
|
wsStyle = WS_POPUP;
|
|
}
|
|
|
|
//
|
|
// -res is a RENDER size, so it is the client area that has to be it.
|
|
// Passed straight to CreateWindowEx it sets the OUTER rectangle and
|
|
// the chrome comes out of the middle - which is how -res 640 480
|
|
// came to present into a 624x441 client.
|
|
//
|
|
RECT wanted;
|
|
wanted.left = 0;
|
|
wanted.top = 0;
|
|
wanted.right = (LONG) L4Application::GetScreenWidth();
|
|
wanted.bottom = (LONG) L4Application::GetScreenHeight();
|
|
AdjustWindowRect(&wanted, wsStyle, FALSE);
|
|
|
|
hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", wsStyle, 0, 0,
|
|
wanted.right - wanted.left, wanted.bottom - wanted.top,
|
|
(HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
|
if (!hWnd)
|
|
return FALSE;
|
|
|
|
ShowWindow(hWnd, nShowCmd);
|
|
|
|
//
|
|
// Sticky placement, from the first frame the player sees. SVGA16 also
|
|
// registers and reloads when it builds the cockpit, but that is not
|
|
// until a mission starts - without this the console screen would come
|
|
// up at the default rect with its title bar still on, and only jump
|
|
// to the saved placement once a race began. -fit takes the whole
|
|
// monitor and has no placement of the player's to restore.
|
|
//
|
|
if (!L4Application::GetFitDisplay() && !L4Application::GetFullscreen())
|
|
{
|
|
RPWindowLayout_Register(hWnd, "RPL4", True);
|
|
RPWindowLayout_Load();
|
|
}
|
|
else if (L4Application::GetFitDisplay())
|
|
{
|
|
//
|
|
// -fit has no saved placement to restore, but it does have a
|
|
// shape to take, and it has to take it NOW. SVGA16 applies the
|
|
// same borderless full-monitor rect when it assembles the
|
|
// cockpit, which is after the first mission has already built
|
|
// its D3D device against the window as it stands - so the first
|
|
// race of a session used to render to a bordered client and
|
|
// every race after it to the borderless monitor. Same lobby,
|
|
// same settings, different target and different frame cost.
|
|
//
|
|
// Settle the window here and every mission of the session,
|
|
// first included, is set up against the identical client area.
|
|
//
|
|
L4Application::FitWindowToMonitor(hWnd);
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// RP412JOYCONFIG=1 (joyconfig.bat): the joystick setup wizard, before
|
|
// anything else claims the screen. It asks the player to move each
|
|
// control on their stick, HOTAS or pedals and writes the joy* rows of
|
|
// bindings.txt, then falls through into the game so they can try them
|
|
// straight away.
|
|
//
|
|
// One shot: the variable is cleared from this process so a rebuilt
|
|
// PadRIO later in the session cannot run the wizard a second time.
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
{
|
|
const char *joyconfig = getenv("RP412JOYCONFIG");
|
|
if (joyconfig != NULL && *joyconfig != '\0' && atoi(joyconfig) != 0)
|
|
{
|
|
RPJoyConfigWizard();
|
|
SetEnvironmentVariableA("RP412JOYCONFIG", NULL);
|
|
_putenv("RP412JOYCONFIG=");
|
|
}
|
|
}
|
|
|
|
#if !_DEBUG
|
|
// Arcade pods have no mouse - but desktop/windowed play needs the
|
|
// cursor for the on-screen cockpit buttons, so hide it only when
|
|
// running fullscreen.
|
|
if (L4Application::GetFullscreen())
|
|
{
|
|
ShowCursor(FALSE);
|
|
}
|
|
#endif
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Front-end mode: started without an egg, a network, or mission review.
|
|
// The race loop below keeps everything in ONE process - setup screen,
|
|
// mission, teardown, and back to the setup screen - with the local
|
|
// console marshaling each race. (-egg / -net / -mr run a single pass,
|
|
// exactly as before.)
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
Logical front_end_mode =
|
|
L4Application::GetMissionReviewMode() == 0 &&
|
|
L4Application::GetNetworkCommonFlatAddress() == 0 &&
|
|
!L4Application::GetEggNotationFileName(); // no egg on the command line
|
|
|
|
int last_launch_mode = FELaunchSingle;
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// A hand-fed egg run stays unmarshaled - no console, so the race never
|
|
// ends - UNLESS it is given a length. RP412MISSIONSECONDS supplies one,
|
|
// and with it '-egg' can play a whole race through to the buzzer, the
|
|
// podium and the results.
|
|
//
|
|
// That is the difference between a shortcut that can only be watched and
|
|
// one that can be TESTED: everything after the chequered flag - the fade,
|
|
// the winners' circle, the teardown - was unreachable from the command
|
|
// line, so it could only ever be exercised by hand through the menu.
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
if (!front_end_mode && L4Application::GetEggNotationFileName())
|
|
{
|
|
const char *egg_seconds = getenv("RP412MISSIONSECONDS");
|
|
if (egg_seconds != NULL && atoi(egg_seconds) > 0)
|
|
{
|
|
DEBUG_STREAM << "LocalConsole: marshalling the hand-fed egg run ("
|
|
<< atoi(egg_seconds) << "s, RP412MISSIONSECONDS)\n" << std::flush;
|
|
L4Application::SetNetworkCommonFlatAddress(0);
|
|
RPL4LocalConsole_Install(atoi(egg_seconds));
|
|
}
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
if (front_end_mode)
|
|
{
|
|
if (!IsWindow(hWnd))
|
|
{
|
|
break; // the cockpit window is gone (closed mid-race)
|
|
}
|
|
|
|
// drain any WM_QUIT left over from the previous mission's
|
|
// teardown so it cannot instantly close the setup screen
|
|
MSG leftover;
|
|
while (PeekMessage(&leftover, NULL, WM_QUIT, WM_QUIT, PM_REMOVE))
|
|
{
|
|
}
|
|
|
|
// lobby races: the owner publishes the score sheet, members
|
|
// pull it - the same results screen shows on every machine
|
|
RPL4Lobby_PushRaceResults();
|
|
RPL4Lobby_PullRaceResults();
|
|
|
|
// last race's final scores first (no-op on first boot)
|
|
if (!RPL4FrontEnd_ShowResults(hInstance, hWnd))
|
|
{
|
|
break;
|
|
}
|
|
|
|
static char frontend_egg[MAX_PATH];
|
|
if (!RPL4FrontEnd_Run(hInstance, hWnd, frontend_egg, sizeof(frontend_egg)))
|
|
{
|
|
break; // player closed the menu without launching
|
|
}
|
|
last_launch_mode = RPL4FrontEnd_LastLaunchMode();
|
|
Exit_Code = 0;
|
|
|
|
if (last_launch_mode == FELaunchMember)
|
|
{
|
|
//
|
|
// Lobby member: enter the race as a network pod. The
|
|
// owner's console connects over the wire, feeds the egg,
|
|
// and marshals us - no local egg, no local console. If
|
|
// the owner leaves mid-race the mission ends (a race
|
|
// with no console never stops on its own).
|
|
//
|
|
L4Application::SetNetworkCommonFlatAddress(1501);
|
|
gConsoleLossEndsMission = True;
|
|
}
|
|
else
|
|
{
|
|
gConsoleLossEndsMission = False;
|
|
L4Application::SetEggNotationFileName(frontend_egg);
|
|
|
|
// Front-end games are marshaled by the in-process console:
|
|
// it ends the race when the selected time expires.
|
|
// (Hand-fed -egg runs stay unmarshaled - the developer
|
|
// shortcut.)
|
|
//
|
|
// RP412HOSTPODS turns the launch into a hosted network
|
|
// race: this pod switches to network mode (it meshes like
|
|
// any pod) and the console also marshals the listed member
|
|
// pods over the wire. The Steam lobby primes the same path.
|
|
const char *host_pods = getenv("RP412HOSTPODS");
|
|
if (host_pods != NULL && host_pods[0] != '\0')
|
|
{
|
|
int host_port = 1501;
|
|
const char *port_override = getenv("RP412HOSTPORT");
|
|
if (port_override != NULL && atoi(port_override) > 0)
|
|
{
|
|
host_port = atoi(port_override);
|
|
}
|
|
L4Application::SetNetworkCommonFlatAddress(host_port);
|
|
if (!RPL4LocalConsole_InstallNetworkRace(
|
|
RPL4FrontEnd_LastMissionSeconds(), frontend_egg,
|
|
host_pods, RPL4FrontEnd_LastPilotNames()))
|
|
{
|
|
DEBUG_STREAM << "Network race setup failed - back to the menu\n" << std::flush;
|
|
L4Application::SetNetworkCommonFlatAddress(0);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
L4Application::SetNetworkCommonFlatAddress(0);
|
|
RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds());
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Open the resource file. The brace will help correct the scoping problem
|
|
// of destruction the resource file
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
{
|
|
StreamableResourceFile resources("rpl4.res", version);
|
|
Check(&resources);
|
|
|
|
//
|
|
//----------------------------------------------
|
|
// Create and initialize the application manager
|
|
//----------------------------------------------
|
|
//
|
|
ApplicationManager* app_manager;
|
|
Application *new_app;
|
|
int review_mode = RPL4Application::GetMissionReviewMode();
|
|
if (review_mode > 0)
|
|
{
|
|
MissionReviewApplicationManager *spool_mgr = new MissionReviewApplicationManager(hInstance, hWnd, atoi(getenv("TARGETFPS")), (review_mode == 2) ? 1 : 2, SPOOL_SIZE);
|
|
app_manager = spool_mgr;
|
|
Register_Object(app_manager);
|
|
|
|
//
|
|
//-------------------------------------------------------------------
|
|
// Create and initialize the playback application if there is a spool
|
|
// file
|
|
//-------------------------------------------------------------------
|
|
//
|
|
#if 1
|
|
CString spoolFileName = L4Application::GetSpoolFileName();
|
|
if (!spoolFileName)
|
|
spoolFileName = "last.spl";
|
|
|
|
//
|
|
// Say which of the two this is. The choice was made in silence,
|
|
// and the two look identical from outside for the first few
|
|
// seconds - one plays a race back, the other sits there - so a
|
|
// spool that failed to load was indistinguishable from a spool
|
|
// that loaded and had not started yet.
|
|
//
|
|
SpoolFile *spool = spool_mgr->GetStoredSpoolFile(spoolFileName);
|
|
if (spool)
|
|
{
|
|
DEBUG_STREAM << "Playback: " << spoolFileName
|
|
<< " loaded, " << (int) spool->GetBytesRemaining()
|
|
<< " bytes to play\n" << std::flush;
|
|
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool);
|
|
} else
|
|
{
|
|
DEBUG_STREAM << "Playback: could not load '" << spoolFileName
|
|
<< "' - starting idle with nothing to show. It must exist, be"
|
|
<< " no larger than " << (int) SPOOL_SIZE
|
|
<< " bytes, and have been written by this build.\n" << std::flush;
|
|
new_app = new RPL4IdleApplication(hInstance, hWnd, &resources);
|
|
}
|
|
|
|
Register_Object(new_app);
|
|
app_manager->StartApplication(new_app);
|
|
#else
|
|
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, NULL);
|
|
Register_Object(new_app);
|
|
app_manager->StartApplication(new_app);
|
|
#endif
|
|
|
|
//
|
|
//------------------------------------
|
|
// Initialize the spooling application
|
|
//------------------------------------
|
|
//
|
|
if (review_mode == 1)
|
|
{
|
|
new_app = new L4SpoolingApplication(hInstance, hWnd, &resources, RPL4);
|
|
Register_Object(new_app);
|
|
app_manager->StartApplication(new_app);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
app_manager = new ApplicationManager(hInstance, hWnd, atoi(getenv("TARGETFPS")));
|
|
Register_Object(app_manager);
|
|
|
|
//
|
|
//--------------------------------------
|
|
// Create and initialize the application
|
|
//--------------------------------------
|
|
//
|
|
Application *new_app = new RPL4Application(hInstance, hWnd, &resources);
|
|
rpl4App = new_app;
|
|
Register_Object(new_app);
|
|
SetCursorPos(1680 / 2, 1050 / 2);
|
|
app_manager->StartApplication(new_app);
|
|
}
|
|
|
|
//
|
|
//--------------------------
|
|
// Run missions till we done
|
|
//--------------------------
|
|
//
|
|
app_manager->RunMissions();
|
|
rpl4App = NULL;
|
|
Unregister_Object(app_manager);
|
|
delete app_manager;
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Single-binary race loop: if the local console ended this mission
|
|
// (or we raced as a lobby member under the owner's console), go
|
|
// back to the setup screen / lobby room for the next one; anything
|
|
// else (user quit, -egg / -net / -mr single pass) falls out.
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
if (!front_end_mode ||
|
|
(last_launch_mode != FELaunchMember && !RPL4LocalConsole_MissionCompleted()))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Last word on the window's placement. Every finished drag has already
|
|
// written it and SVGA16 writes again as the cockpit comes down, but a
|
|
// session that never started a race has neither - and quitting from
|
|
// the console screen is exactly how somebody would leave after moving
|
|
// the window to where they want it. No-op unless the mode is save.
|
|
//
|
|
if (IsWindow(hWnd))
|
|
{
|
|
RPWindowLayout_Save();
|
|
}
|
|
RPWindowLayout_Forget(hWnd);
|
|
|
|
#if !_DEBUG
|
|
// symmetric with the fullscreen-only hide at startup
|
|
if (L4Application::GetFullscreen())
|
|
{
|
|
ShowCursor(TRUE);
|
|
}
|
|
#endif
|
|
|
|
Stop_Registering();
|
|
|
|
return Exit_Code;
|
|
}
|