Files
RP412/RP_L4/RPL4.CPP
T
CydandClaude Opus 5 aa294071f9 The render target is the size that was asked for
A crosshair off-centre on the second race, and underneath it every race
after the first was a different race.

Windowed, BackBufferWidth/Height were left at zero, so D3D sized the back
buffer to the device window's client area at the moment the device was
created. Everything downstream is built from the size we ASKED for
instead - the projection matrix takes its aspect from it, the reticle is
centred on it - so a window that was not exactly that size rendered at
the wrong shape and got rescaled on the way to the viewscreen pane.

-fit decided which window that was, and it decided differently for the
first mission than for the rest. Its borderless full-monitor placement
lived only in SVGA16's cockpit build, which does not run until a mission
starts - just after that mission has built its device. So race one was
set up against a still-bordered client and every race after it against
the borderless monitor. On a 3440x1440 panel that is a 1.778 image drawn
across a 2.389 target, against 1.816 the first time.

That is not a cosmetic difference. The simulation advances on wall-clock
deltas, so frame cost is physics: two render targets that size and scale
differently are two different races from one lobby and one set of
settings. A racing sim does not get to do that.

So: the back buffer is the requested size windowed as well as
full-screen, and -fit takes its shape at startup rather than four
screens later. SVGA16 still applies the same rect when it builds the
cockpit - that call is now a no-op instead of a change, which is the
point. The first lobby also stops being the only one with a title bar.

The reticle keeps its own share of the blame and is fixed on its own
terms, so it cannot drift again if a target ever does move:

- It is measured against the viewport at draw time and rebuilt when that
  changes, rather than baked once in the constructor from the renderer's
  requested size. One GetViewport a frame, no rewrite until it moves.
- The arms are quads, not lines. D3D9 line rasterisation follows the
  diamond-exit rule and is free to differ between drivers on a segment
  running along a pixel boundary, which is how a crosshair loses one
  pair of arms and keeps the other - and full-screen, where both
  dimensions are usually even and both pairs sit on boundaries, how it
  can lose the lot.
- Arm thickness follows the target rather than being one pixel whatever
  the resolution. One pixel is a width the presentation can throw away
  in a downscale, and it was a hairline at 1440 next to the pod's line
  at 480.

The log names the viewport, the requested size and where the crosshair
landed, and says TARGET DISAGREES with both aspects when the first two
do not match - so the next report of this arrives with its own diagnosis.

Window creation cleaned up while in there: it computed a style and then
handed CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless, so the
full-screen path never got the WS_POPUP it thought it was asking for.
Borderless modes are now born borderless instead of being restyled a
moment after. The requested size also goes through AdjustWindowRect,
because -res is a render size and was being used as the OUTER rectangle
with the chrome taken out of the middle - which is how -res 640 480 came
to present into a 624x441 client and started all of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 22:19:39 -05:00

682 lines
23 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 <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)
{
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");
std::ofstream logfile;
logfile.open(filename);
std::cout.rdbuf(logfile.rdbuf());
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;
// 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;
#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);
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;
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";
SpoolFile *spool = spool_mgr->GetStoredSpoolFile(spoolFileName);
if (spool)
{
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool);
} else
{
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;
}