Cockpit: buttons under the glass, -fit, and player display layout

Button banks
  The exploded diagnostic view was still display-only - it predates the
  button work - so it now builds the same banks as the cockpit, with
  the pod arrangement laid out from the panes measured sizes rather
  than a hardcoded 640/480 grid (the banks make each window bigger than
  its glass, and the bottom row hung off the work area otherwise).

  The map side columns were spread height/6 from the top, but the maps
  own legend grid is not sixths: measured off the bitmap it starts 13
  rows down with six 102-tall cells on a 105 pitch. Every button sat
  high of its label, worst at the bottom. Each buttons top and bottom
  now come off that grid separately and are subtracted - scaling a
  height directly would let rounding drift them back out of step on a
  resized cockpit.

  Depth 100 to 240: against the 480 glass the two banks meet in the
  middle bar the strips, so practically the whole display is a press
  target. This mattered most in the cockpit, where the panes are small
  enough that the halfway clamp governs - at 100 the MFDs had a 110px
  dead band straight through the middle of the glass.

-fit (also spelled -windowed-fullscreen)
  Borderless over the whole monitor, with the render size chosen to
  match. The cockpit presents the 3D into a viewscreen that fills its
  canvas, so the right -res is that canvas at the scale the cockpit
  will settle on; computing it with identical arithmetic makes the
  stretch a copy. On the 3440x1440 panel that is 133% and -res 2553
  1436, against 125% for the windowed path that pays for the taskbar.

  The pick runs after the whole command line, so an explicit -res wins
  from either side of -fit. Capped at 3840x2160. Cockpit mode only -
  mode 0 has to stay playable on real pod hardware and mode 2 is a dev
  view - so those get the resolution and keep their windows.

Display layout, in environ.ini
  L4MFDSCALE sizes all five MFDs, L4MFDSCALE_UL and friends override
  any one of them, L4RADARSCALE the radar, and L4RADARPOS puts the
  radar bottom centre, in either bottom corner, or halfway up either
  side. Scaling is applied in canvas units before the canvas is fitted
  to the window, so a number means the same thing on every monitor.

  Sizing each display separately let the clamps become exact rather
  than one conservative rule for all five: what limits a display is its
  actual neighbour. Which neighbour that is depends on the radar, so
  the clamps follow it - on the bottom edge it clears the one MFD above
  its column, but centred on a side it has one above AND below and
  grows from the middle both ways, so it must clear the taller twice
  over. Clamping shrinks uniformly; these are photographs of real
  instruments and a one-axis clamp would squash them.

Verified on the ultrawide: all five radar positions, per-display and
group scaling with the clamps biting, -fit with and without an explicit
-res, and the button geometry measured back off the screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-25 14:05:02 -05:00
co-authored by Claude Fable 5
parent 6b43971d2d
commit 8dc6605a07
5 changed files with 687 additions and 68 deletions
+10
View File
@@ -79,6 +79,9 @@ public:
static unsigned int GetScreenWidth() { return mScreenWidth; }
static unsigned int GetScreenHeight() { return mScreenHeight; }
static bool GetFullscreen() { return mFullscreen; }
// -fit: borderless window filling the monitor, with the render size
// chosen to match the cockpit canvas it will be presented into.
static bool GetFitDisplay() { return mFitDisplay; }
static Logical GetSeeSolids() { return seeSolids; }
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
// The front end's multiplayer path turns network mode on at launch
@@ -93,10 +96,17 @@ public:
static CString GetRIORecordingFileName() { return rioRecordingFileName; }
protected:
// Pick mScreenWidth/mScreenHeight from the monitor. Runs after the
// whole command line is parsed, so an explicit -res always wins no
// matter which side of -fit it appears on.
static void ChooseFitResolution();
static HINSTANCE mhInstance;
static unsigned int mScreenWidth;
static unsigned int mScreenHeight;
static bool mFullscreen;
static bool mFitDisplay;
static bool mResExplicit;
static Logical seeSolids;
static CString eggNotationFileName;
static CString spoolFileName;
+80 -1
View File
@@ -94,6 +94,8 @@ HINSTANCE L4Application::mhInstance = NULL;
unsigned int L4Application::mScreenWidth = 800;
unsigned int L4Application::mScreenHeight = 600;
bool L4Application::mFullscreen = true;
bool L4Application::mFitDisplay = false;
bool L4Application::mResExplicit = false;
Logical L4Application::seeSolids = False;
CString L4Application::eggNotationFileName;
CString L4Application::spoolFileName;
@@ -205,6 +207,7 @@ Logical
{
mScreenWidth = atoi(W2A(argv[++(*arguement)]));
mScreenHeight = atoi(W2A(argv[++(*arguement)]));
mResExplicit = true;
}
else
{
@@ -217,18 +220,88 @@ Logical
mFullscreen = false;
}
//-------------------------------------------------------------------
// -fit: borderless window over the whole monitor, and a render size
// picked to match. Spelled out as -windowed-fullscreen too, since
// that is what the rest of the world calls it.
//-------------------------------------------------------------------
else if (
!stricmp(W2A(argv[*arguement]), "-fit") ||
!stricmp(W2A(argv[*arguement]), "-windowed-fullscreen")
)
{
mFullscreen = false;
mFitDisplay = true;
}
else if (
!stricmp(W2A(argv[*arguement]), "-h") || !stricmp(W2A(argv[*arguement]), "-help")
)
{
DEBUG_STREAM << "\n" << argv[0] <<
" -egg <filename> -net <memory_address> -solids -h -help\n";
" -egg <filename> -net <memory_address> -solids -h -help\n"
" -windowed windowed, title bar and all\n"
" -fit borderless over the whole monitor,\n"
" render size chosen to match\n"
" (long form: -windowed-fullscreen)\n"
" -res <width> <height> explicit render size; overrides the\n"
" size -fit would have picked\n";
return False;
}
return True;
}
//
//#############################################################################
// ChooseFitResolution
//#############################################################################
//
// The cockpit presents the 3D into a viewscreen that fills its 1920x1080
// canvas, and that canvas is fitted to the window at one uniform scale.
// So the render size that lands 1:1 on screen is the canvas at the same
// scale the cockpit will choose - work it out with identical arithmetic
// here and the stretch becomes a copy.
//
void
L4Application::ChooseFitResolution()
{
int monitor_w = GetSystemMetrics(SM_CXSCREEN);
int monitor_h = GetSystemMetrics(SM_CYSCREEN);
if (monitor_w <= 0 || monitor_h <= 0)
{
return;
}
const int canvas_w = 1920;
const int canvas_h = 1080;
int fit_w = (monitor_w * 100) / canvas_w;
int fit_h = (monitor_h * 100) / canvas_h;
int scale = (fit_w < fit_h) ? fit_w : fit_h;
if (scale < 25) scale = 25;
unsigned int width = (canvas_w * scale) / 100;
unsigned int height = (canvas_h * scale) / 100;
//-------------------------------------------------------------------
// A 1995 engine on a 5K panel would be asked for a back buffer well
// past anything it was built for; cap and let D3D scale the last bit.
//-------------------------------------------------------------------
if (width > 3840)
{
width = 3840;
height = 2160;
}
mScreenWidth = width;
mScreenHeight = height;
DEBUG_STREAM << "L4Application: -fit chose -res " << width << " "
<< height << " (" << scale << "% canvas) for the "
<< monitor_w << "x" << monitor_h << " monitor\n" << std::flush;
}
//
//#############################################################################
// ParseCommandLine
@@ -270,6 +343,12 @@ Logical
}
}
}
// after the whole line, so -res wins from either side of -fit
if (mFitDisplay && !mResExplicit)
{
ChooseFitResolution();
}
return True;
}
+30 -5
View File
@@ -17,9 +17,26 @@ namespace
// player actually clicks. Applies to both the MFD strips (depth
// measured vertically) and the map's side columns (horizontally).
//---------------------------------------------------------------
const int buttonDepth = 100;
// 240 against the 480 glass: the two banks reach in far enough to
// meet in the middle bar the strips' own width, so practically the
// whole display is a press target and neither bank overlaps the
// other. The clamps below keep that true on a scaled-down cockpit.
const int buttonDepth = 240;
const int indicatorStrip = 10;
//---------------------------------------------------------------
// The map paints its own legend beside each side button, and that
// grid is not the naive height/6: measured off the 640-tall map,
// the first cell starts 13 rows down and the six cells are 102
// tall on a 105 pitch (13 + 6*102 + 5*3 = 640). Spacing the
// buttons evenly from the top instead left every one of them
// riding high of its label. Scaled to the live glass height.
//---------------------------------------------------------------
const int mapLegendSpan = 640;
const int mapLegendTop = 13;
const int mapLegendCell = 102;
const int mapLegendPitch = 105;
//---------------------------------------------------------------
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
// bright), red family for MFD strips, amber for the side columns.
@@ -327,20 +344,28 @@ void
if (button_w > limit) button_w = limit;
if (button_w < strip_w) button_w = strip_w;
int button_h = display_height / 6;
displayX = strip_w;
*client_width = display_width + 2 * strip_w;
for (int i = 0; i < 6; ++i)
{
int h = (i == 5) ? (display_height - 5 * button_h) : button_h;
//-------------------------------------------------------
// Take top and bottom off the legend grid separately and
// subtract: scaling a height directly would let rounding
// drift the buttons out of step with the labels.
//-------------------------------------------------------
int cell_top = mapLegendTop + i * mapLegendPitch;
int top = (cell_top * display_height) / mapLegendSpan;
int bottom =
((cell_top + mapLegendCell) * display_height) / mapLegendSpan;
int h = bottom - top;
if (h < 1) h = 1;
ScreenButton *left = &buttons[buttonCount++];
left->unit = anchorA + i;
left->amber = 1;
left->x = 0;
left->y = i * button_h;
left->y = top;
left->w = button_w;
left->h = h;
+490 -58
View File
@@ -3844,6 +3844,323 @@ static LRESULT CALLBACK
SVGA16 *SVGA16::activeCockpit = NULL;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// The six secondary displays at their pod sizes, in canvas units.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static const int cockpitMfdBaseW = 320;
static const int cockpitMfdBaseH = 240;
// the radar reads best big: 1.35x the compact glass
// (1.5x proved a touch too large in playtest)
static const int cockpitRadarBaseW = 324;
static const int cockpitRadarBaseH = 432;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// A percentage from the environment, falling back to the given default
// when the variable is unset, unreadable or nonsense. That fallback is
// how a per-display setting inherits the group one.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static int
DisplayScalePercent(const char *variable, int fallback)
{
const char *text = getenv(variable);
if (text == NULL)
{
return fallback;
}
int percent = atoi(text);
if (percent <= 0)
{
return fallback;
}
if (percent < 25) percent = 25;
if (percent > 200) percent = 200;
return percent;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shrink a display to fit a limit, uniformly. Clamping one axis alone
// would stretch the glass out of shape, and these are photographs of
// real instruments - they have to keep their proportions.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static void
ClampGlass(SVGA16::GlassSize *glass, int max_w, int max_h)
{
if (max_w < 1) max_w = 1;
if (max_h < 1) max_h = 1;
if (glass->w > max_w)
{
glass->h = (glass->h * max_w) / glass->w;
glass->w = max_w;
}
if (glass->h > max_h)
{
glass->w = (glass->w * max_h) / glass->h;
glass->h = max_h;
}
if (glass->w < 1) glass->w = 1;
if (glass->h < 1) glass->h = 1;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Where the radar sits. The pod had it dead centre under the
// viewscreen, which on a wide panel is exactly where the road is, so
// L4RADARPOS moves it out of the way: either bottom corner, or halfway
// up either side.
//
// On the bottom row it is one of three panes and the lower MFD whose
// corner it takes slides inboard beside it. Halfway up a side it leaves
// the bottom row altogether and sits between that side's two MFDs.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enum RadarPlacement
{
RadarBottomCenter = 0,
RadarBottomLeft,
RadarBottomRight,
RadarMidLeft,
RadarMidRight
};
static Logical
RadarOnASide(RadarPlacement placement)
{
return (placement == RadarMidLeft || placement == RadarMidRight)
? True : False;
}
static Logical
RadarOnTheLeft(RadarPlacement placement)
{
return (placement == RadarBottomLeft || placement == RadarMidLeft)
? True : False;
}
static RadarPlacement
RadarPosition()
{
static int cached = -1;
if (cached < 0)
{
//---------------------------------------------------------------
// Two spellings each: the short ones were here first, the long
// ones say which edge out loud now that there are five.
//---------------------------------------------------------------
static const struct
{
const char *name;
int placement;
}
names[] =
{
{ "CENTER", RadarBottomCenter },
{ "CENTRE", RadarBottomCenter },
{ "BOTTOM", RadarBottomCenter },
{ "LEFT", RadarBottomLeft },
{ "BOTTOMLEFT", RadarBottomLeft },
{ "RIGHT", RadarBottomRight },
{ "BOTTOMRIGHT", RadarBottomRight },
{ "MIDLEFT", RadarMidLeft },
{ "LEFTCENTER", RadarMidLeft },
{ "LEFTCENTRE", RadarMidLeft },
{ "MIDRIGHT", RadarMidRight },
{ "RIGHTCENTER", RadarMidRight },
{ "RIGHTCENTRE", RadarMidRight }
};
cached = RadarBottomCenter;
const char *text = getenv("L4RADARPOS");
if (text != NULL)
{
for (int i = 0; i < (int) (sizeof(names) / sizeof(names[0])); ++i)
{
if (!stricmp(text, names[i].name))
{
cached = names[i].placement;
break;
}
}
}
static const char *described[] =
{
"bottom centre",
"bottom left",
"bottom right",
"left side, centred",
"right side, centred"
};
DEBUG_STREAM << "SVGA16: radar on the " << described[cached]
<< "\n" << std::flush;
}
return (RadarPlacement) cached;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// The six secondary displays sized for a given canvas scale.
//
// The pod bolted these down at fixed sizes; a desktop panel has room to
// trade viewscreen for instrument, so the player scales them. Each has
// its own setting, falling back to the group one - L4MFDSCALE sets all
// five MFDs, L4MFDSCALE_UL and friends override individually, and
// L4RADARSCALE the radar.
//
// Scaling happens in canvas units, before the canvas is fitted to the
// window, so a given number means the same thing on every monitor.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
SVGA16::CockpitGlassSizes(int scale, GlassSize sizes[SplitViewCount])
{
static Logical settings_read = False;
static int percent[SplitViewCount];
if (!settings_read)
{
settings_read = True;
int group = DisplayScalePercent("L4MFDSCALE", 100);
percent[SplitMFDUpperLeft] =
DisplayScalePercent("L4MFDSCALE_UL", group);
percent[SplitMFDUpperCenter] =
DisplayScalePercent("L4MFDSCALE_UC", group);
percent[SplitMFDUpperRight] =
DisplayScalePercent("L4MFDSCALE_UR", group);
percent[SplitMFDLowerLeft] =
DisplayScalePercent("L4MFDSCALE_LL", group);
percent[SplitMFDLowerRight] =
DisplayScalePercent("L4MFDSCALE_LR", group);
percent[SplitMap] = DisplayScalePercent("L4RADARSCALE", 100);
DEBUG_STREAM << "SVGA16: secondary displays at UL "
<< percent[SplitMFDUpperLeft] << "% UC "
<< percent[SplitMFDUpperCenter] << "% UR "
<< percent[SplitMFDUpperRight] << "% LL "
<< percent[SplitMFDLowerLeft] << "% LR "
<< percent[SplitMFDLowerRight] << "% radar "
<< percent[SplitMap] << "%\n" << std::flush;
}
GlassSize glass[SplitViewCount];
for (int view = 0; view < SplitViewCount; ++view)
{
int base_w = (view == SplitMap) ? cockpitRadarBaseW : cockpitMfdBaseW;
int base_h = (view == SplitMap) ? cockpitRadarBaseH : cockpitMfdBaseH;
glass[view].w = (base_w * percent[view]) / 100;
glass[view].h = (base_h * percent[view]) / 100;
}
//-------------------------------------------------------------------
// Keep the pod arrangement legal in canvas units. Sizing each
// display on its own means these limits can be exact rather than one
// conservative rule covering all five: what constrains a display is
// its actual neighbour, not the largest a neighbour might have been.
//
// The panes overlap the viewscreen by design, as the pod's bezels
// did - each other, never.
//-------------------------------------------------------------------
const int canvas_w = 1920;
const int canvas_h = 1080;
const int half_w = canvas_w / 2;
// nothing may exceed the canvas on its own
for (int view = 0; view < SplitViewCount; ++view)
{
ClampGlass(&glass[view], canvas_w, canvas_h);
}
//-------------------------------------------------------------------
// The radar shares its column with the MFDs above and below it, so
// their heights share the canvas. Which MFDs those are, and how much
// room they leave, depends on where the radar was put. It yields,
// being the one that grows into the view.
//-------------------------------------------------------------------
RadarPlacement placement = RadarPosition();
if (RadarOnASide(placement))
{
//---------------------------------------------------------------
// Halfway up a side it has an MFD above AND below, and it grows
// from the middle in both directions - so it must clear the
// taller of the two twice over. That is a real constraint: two
// big MFDs on one side leave a centred radar very little.
//---------------------------------------------------------------
Logical left = RadarOnTheLeft(placement);
int upper = left
? glass[SplitMFDUpperLeft].h : glass[SplitMFDUpperRight].h;
int lower = left
? glass[SplitMFDLowerLeft].h : glass[SplitMFDLowerRight].h;
int tallest = (upper > lower) ? upper : lower;
ClampGlass(&glass[SplitMap], canvas_w, canvas_h - 2 * tallest);
}
else
{
//---------------------------------------------------------------
// On the bottom edge it only has to clear the MFD hanging from
// the top of its column.
//---------------------------------------------------------------
int above =
(placement == RadarBottomLeft) ? glass[SplitMFDUpperLeft].h :
(placement == RadarBottomRight) ? glass[SplitMFDUpperRight].h :
glass[SplitMFDUpperCenter].h;
ClampGlass(&glass[SplitMap], canvas_w, canvas_h - above);
}
//-------------------------------------------------------------------
// The top row is a centred display with one anchored to either edge,
// so a side display gets whatever the centre one leaves of its half.
//-------------------------------------------------------------------
int top_side = half_w - glass[SplitMFDUpperCenter].w / 2;
ClampGlass(&glass[SplitMFDUpperLeft], top_side, canvas_h);
ClampGlass(&glass[SplitMFDUpperRight], top_side, canvas_h);
//-------------------------------------------------------------------
// What the bottom row looks like depends on the radar as well:
// centred it is the middle of that row, in a corner it is a third
// pane along it, and on a side it has left the row entirely and the
// two MFDs have the whole bottom edge to share.
//-------------------------------------------------------------------
if (RadarOnASide(placement))
{
ClampGlass(&glass[SplitMFDLowerLeft], half_w, canvas_h);
ClampGlass(&glass[SplitMFDLowerRight], half_w, canvas_h);
}
else if (placement == RadarBottomCenter)
{
int bottom_side = half_w - glass[SplitMap].w / 2;
ClampGlass(&glass[SplitMFDLowerLeft], bottom_side, canvas_h);
ClampGlass(&glass[SplitMFDLowerRight], bottom_side, canvas_h);
}
else
{
int bottom_share = (canvas_w - glass[SplitMap].w) / 2;
ClampGlass(&glass[SplitMFDLowerLeft], bottom_share, canvas_h);
ClampGlass(&glass[SplitMFDLowerRight], bottom_share, canvas_h);
}
//-------------------------------------------------------------------
// An upper and a lower MFD share a column, anchored to opposite
// edges, so their heights must not sum past the canvas. The lower
// one yields - it is the one the radar sits beside.
//-------------------------------------------------------------------
ClampGlass(&glass[SplitMFDLowerLeft], canvas_w,
canvas_h - glass[SplitMFDUpperLeft].h);
ClampGlass(&glass[SplitMFDLowerRight], canvas_w,
canvas_h - glass[SplitMFDUpperRight].h);
//-------------------------------------------------------------------
// Only now to device pixels: clamping in canvas units keeps every
// limit above independent of the monitor.
//-------------------------------------------------------------------
for (int view = 0; view < SplitViewCount; ++view)
{
sizes[view].w = (glass[view].w * scale) / 100;
sizes[view].h = (glass[view].h * scale) / 100;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Fit the 1920x1080 cockpit canvas into the given client area: one
// uniform scale (so the canvas never stretches - a wider desktop just
@@ -3886,11 +4203,8 @@ void
SWP_NOZORDER | SWP_NOACTIVATE);
}
int top_glass_w = (320 * scale) / 100;
int top_glass_h = (240 * scale) / 100;
// the radar reads best big: 1.35x the compact glass
int map_glass_w = (324 * scale) / 100;
int map_glass_h = (432 * scale) / 100;
GlassSize glass[SplitViewCount];
CockpitGlassSizes(scale, glass);
for (int view = 0; view < SplitViewCount; ++view)
{
@@ -3898,14 +4212,7 @@ void
{
continue;
}
if (view == SplitMap)
{
splitView[view]->Resize(map_glass_w, map_glass_h);
}
else
{
splitView[view]->Resize(top_glass_w, top_glass_h);
}
splitView[view]->Resize(glass[view].w, glass[view].h);
}
//---------------------------------------------------------------
@@ -3928,24 +4235,67 @@ void
origin_x + view_w - splitView[SplitMFDUpperRight]->ClientWidth(),
origin_y);
}
//---------------------------------------------------------------
// The radar: bottom centre, either bottom corner, or halfway up
// either side. Only when it takes a bottom corner does a lower MFD
// have to move - on a side it is out of that row's way already.
//---------------------------------------------------------------
RadarPlacement placement = RadarPosition();
int radar_w = (splitView[SplitMap] != NULL)
? splitView[SplitMap]->ClientWidth() : 0;
if (splitView[SplitMap] != NULL)
{
int radar_h = splitView[SplitMap]->ClientHeight();
int radar_x, radar_y;
switch (placement)
{
case RadarBottomLeft:
radar_x = origin_x;
radar_y = origin_y + view_h - radar_h;
break;
case RadarBottomRight:
radar_x = origin_x + view_w - radar_w;
radar_y = origin_y + view_h - radar_h;
break;
case RadarMidLeft:
radar_x = origin_x;
radar_y = origin_y + (view_h - radar_h) / 2;
break;
case RadarMidRight:
radar_x = origin_x + view_w - radar_w;
radar_y = origin_y + (view_h - radar_h) / 2;
break;
default:
radar_x = origin_x + (view_w - radar_w) / 2;
radar_y = origin_y + view_h - radar_h;
break;
}
splitView[SplitMap]->SetPosition(radar_x, radar_y);
}
int slide_left = (placement == RadarBottomLeft) ? radar_w : 0;
int slide_right = (placement == RadarBottomRight) ? radar_w : 0;
if (splitView[SplitMFDLowerLeft] != NULL)
{
splitView[SplitMFDLowerLeft]->SetPosition(
origin_x,
origin_x + slide_left,
origin_y + view_h - splitView[SplitMFDLowerLeft]->ClientHeight());
}
if (splitView[SplitMFDLowerRight] != NULL)
{
splitView[SplitMFDLowerRight]->SetPosition(
origin_x + view_w - splitView[SplitMFDLowerRight]->ClientWidth(),
origin_x + view_w - splitView[SplitMFDLowerRight]->ClientWidth()
- slide_right,
origin_y + view_h - splitView[SplitMFDLowerRight]->ClientHeight());
}
if (splitView[SplitMap] != NULL)
{
splitView[SplitMap]->SetPosition(
origin_x + (view_w - splitView[SplitMap]->ClientWidth()) / 2,
origin_y + view_h - splitView[SplitMap]->ClientHeight());
}
// The panes go OVER the main display (pod bezel occlusion): pin
// the viewscreen to the bottom of the sibling z-order so the 3D
@@ -4023,35 +4373,67 @@ SVGA16::SVGA16(
int work_w = work.right - work.left;
int work_h = work.bottom - work.top;
int mid_x = (work_w - init_width) / 2; if (mid_x < 0) mid_x = 0;
int right_x = work_w - init_width; if (right_x < 0) right_x = 0;
int bottom_y = work_h - init_height; if (bottom_y < 0) bottom_y = 0;
//---------------------------------------------------------------
// Each display carries its own button bank here too, at native
// size: the banks reach under the glass with their indicator
// strips clearing the edge, exactly as in the composited
// cockpit - so the buttons can be read and pressed full size.
//---------------------------------------------------------------
splitView[SplitMFDUpperLeft] = new MFDSplitView(
"MFD upper left", init_width, init_height,
init_width, init_height, work.left, work.top,
MFDSplitView::NoButtons, 0, 0, 0);
MFDSplitView::MFDStrips, 0x2F, 0, 0);
splitView[SplitMFDUpperCenter] = new MFDSplitView(
"MFD upper center", init_width, init_height,
init_width, init_height, work.left + mid_x, work.top,
MFDSplitView::NoButtons, 0, 0, 0);
init_width, init_height, work.left, work.top,
MFDSplitView::MFDStrips, 0x27, 0, 0);
splitView[SplitMFDUpperRight] = new MFDSplitView(
"MFD upper right", init_width, init_height,
init_width, init_height, work.left + right_x, work.top,
MFDSplitView::NoButtons, 0, 0, 0);
init_width, init_height, work.left, work.top,
MFDSplitView::MFDStrips, 0x37, 0, 0);
splitView[SplitMFDLowerLeft] = new MFDSplitView(
"MFD lower left", init_width, init_height,
init_width, init_height, work.left, work.top + bottom_y,
MFDSplitView::NoButtons, 0, 0, 0);
init_width, init_height, work.left, work.top,
MFDSplitView::MFDStrips, 0x0F, 0, 0);
splitView[SplitMFDLowerRight] = new MFDSplitView(
"MFD lower right", init_width, init_height,
init_width, init_height, work.left + right_x, work.top + bottom_y,
MFDSplitView::NoButtons, 0, 0, 0);
init_width, init_height, work.left, work.top,
MFDSplitView::MFDStrips, 0x07, 0, 0);
// map is portrait-mounted: source rotated to 480x640
splitView[SplitMap] = new MFDSplitView(
"Map", init_height, init_width,
init_height, init_width, work.left + mid_x, work.top + bottom_y,
MFDSplitView::NoButtons, 0, 0, 0);
init_height, init_width, work.left, work.top,
MFDSplitView::SideColumns, 0x10, 0x18, 0);
//---------------------------------------------------------------
// Lay them out in the pod arrangement from their measured sizes
// (the banks make each window bigger than its glass).
//---------------------------------------------------------------
RECT probe;
probe.left = 0; probe.top = 0; probe.right = 100; probe.bottom = 100;
AdjustWindowRect(&probe,
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE);
int chrome_w = (probe.right - probe.left) - 100;
int chrome_h = (probe.bottom - probe.top) - 100;
int mfd_w = splitView[SplitMFDUpperLeft]->ClientWidth() + chrome_w;
int mfd_h = splitView[SplitMFDUpperLeft]->ClientHeight() + chrome_h;
int map_w = splitView[SplitMap]->ClientWidth() + chrome_w;
int map_h = splitView[SplitMap]->ClientHeight() + chrome_h;
int mid_x = (work_w - mfd_w) / 2; if (mid_x < 0) mid_x = 0;
int right_x = work_w - mfd_w; if (right_x < 0) right_x = 0;
int bottom_y = work_h - mfd_h; if (bottom_y < 0) bottom_y = 0;
int map_x = (work_w - map_w) / 2; if (map_x < 0) map_x = 0;
int map_y = work_h - map_h; if (map_y < 0) map_y = 0;
splitView[SplitMFDUpperLeft]->SetPosition(work.left, work.top);
splitView[SplitMFDUpperCenter]->SetPosition(work.left + mid_x, work.top);
splitView[SplitMFDUpperRight]->SetPosition(work.left + right_x, work.top);
splitView[SplitMFDLowerLeft]->SetPosition(work.left, work.top + bottom_y);
splitView[SplitMFDLowerRight]->SetPosition(
work.left + right_x, work.top + bottom_y);
splitView[SplitMap]->SetPosition(work.left + map_x, work.top + map_y);
}
else if (splitViews)
{
@@ -4073,8 +4455,20 @@ SVGA16::SVGA16(
HWND cockpit = ApplicationManager::GetCurrentManager()->GetHWnd();
//---------------------------------------------------------------
// -fit takes the whole monitor rather than the work area, and
// pays no chrome: the shell goes borderless below. Otherwise
// stay inside the work area so the taskbar is still reachable.
//---------------------------------------------------------------
bool fit_display = L4Application::GetFitDisplay();
int chrome_w = 16, chrome_h = 40;
if (cockpit != NULL)
if (fit_display)
{
chrome_w = 0;
chrome_h = 0;
}
else if (cockpit != NULL)
{
RECT outer, inner;
GetWindowRect(cockpit, &outer);
@@ -4085,7 +4479,28 @@ SVGA16::SVGA16(
RECT work;
work.left = 0; work.top = 0; work.right = 1920; work.bottom = 1080;
SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0);
if (fit_display)
{
MONITORINFO monitor;
memset(&monitor, 0, sizeof(monitor));
monitor.cbSize = sizeof(monitor);
HMONITOR handle = MonitorFromWindow(
(cockpit != NULL) ? cockpit : GetDesktopWindow(),
MONITOR_DEFAULTTOPRIMARY);
if (GetMonitorInfoA(handle, &monitor))
{
work = monitor.rcMonitor;
}
else
{
work.right = GetSystemMetrics(SM_CXSCREEN);
work.bottom = GetSystemMetrics(SM_CYSCREEN);
}
}
else
{
SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0);
}
int avail_w = (work.right - work.left) - chrome_w;
int avail_h = (work.bottom - work.top) - chrome_h;
@@ -4115,12 +4530,35 @@ SVGA16::SVGA16(
//---------------------------------------------------------------
if (cockpit != NULL)
{
SetWindowLongPtrA(cockpit, GWL_STYLE,
GetWindowLongPtrA(cockpit, GWL_STYLE) | WS_CLIPCHILDREN);
LONG_PTR style = GetWindowLongPtrA(cockpit, GWL_STYLE);
SetWindowPos(cockpit, NULL, work.left, work.top,
client_w + chrome_w, client_h + chrome_h,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
if (fit_display)
{
//-------------------------------------------------------
// Borderless over the whole monitor. The window is the
// full panel, wider than 16:9 or not - LayoutCockpit
// centres the canvas inside it and the leftover stays
// black, so this letterboxes exactly like a resize.
//-------------------------------------------------------
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_BORDER |
WS_DLGFRAME);
style |= WS_POPUP | WS_CLIPCHILDREN;
SetWindowLongPtrA(cockpit, GWL_STYLE, style);
SetWindowPos(cockpit, NULL, work.left, work.top,
work.right - work.left, work.bottom - work.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
else
{
SetWindowLongPtrA(cockpit, GWL_STYLE,
style | WS_CLIPCHILDREN);
SetWindowPos(cockpit, NULL, work.left, work.top,
client_w + chrome_w, client_h + chrome_h,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
}
//---------------------------------------------------------------
@@ -4161,40 +4599,34 @@ SVGA16::SVGA16(
// 0x37.. / lower left 0x0F.. / lower right 0x07.. and the map
// flanked by Secondary 0x10-0x15 / Screen 0x18-0x1D.
//---------------------------------------------------------------
// all five MFDs at the compact size, corners + top center
int top_glass_w = COCKPIT_CANVAS(320);
int top_glass_h = COCKPIT_CANVAS(240);
int bot_glass_w = COCKPIT_CANVAS(320);
int bot_glass_h = COCKPIT_CANVAS(240);
// the radar reads best big: 1.35x the compact glass
// (1.5x proved a touch too large in playtest)
int map_glass_w = COCKPIT_CANVAS(324);
int map_glass_h = COCKPIT_CANVAS(432);
// each display at whatever the player asked for it
GlassSize glass[SplitViewCount];
CockpitGlassSizes(scale_num, glass);
splitView[SplitMFDUpperLeft] = new MFDSplitView(
"MFD upper left", init_width, init_height,
top_glass_w, top_glass_h, 0, 0,
glass[SplitMFDUpperLeft].w, glass[SplitMFDUpperLeft].h, 0, 0,
MFDSplitView::MFDStrips, 0x2F, 0, cockpit);
splitView[SplitMFDUpperCenter] = new MFDSplitView(
"MFD upper center", init_width, init_height,
top_glass_w, top_glass_h, 0, 0,
glass[SplitMFDUpperCenter].w, glass[SplitMFDUpperCenter].h, 0, 0,
MFDSplitView::MFDStrips, 0x27, 0, cockpit);
splitView[SplitMFDUpperRight] = new MFDSplitView(
"MFD upper right", init_width, init_height,
top_glass_w, top_glass_h, 0, 0,
glass[SplitMFDUpperRight].w, glass[SplitMFDUpperRight].h, 0, 0,
MFDSplitView::MFDStrips, 0x37, 0, cockpit);
splitView[SplitMFDLowerLeft] = new MFDSplitView(
"MFD lower left", init_width, init_height,
bot_glass_w, bot_glass_h, 0, 0,
glass[SplitMFDLowerLeft].w, glass[SplitMFDLowerLeft].h, 0, 0,
MFDSplitView::MFDStrips, 0x0F, 0, cockpit);
splitView[SplitMFDLowerRight] = new MFDSplitView(
"MFD lower right", init_width, init_height,
bot_glass_w, bot_glass_h, 0, 0,
glass[SplitMFDLowerRight].w, glass[SplitMFDLowerRight].h, 0, 0,
MFDSplitView::MFDStrips, 0x07, 0, cockpit);
// map is portrait: source rotated 90 degrees clockwise
splitView[SplitMap] = new MFDSplitView(
"Map", init_height, init_width,
map_glass_w, map_glass_h, 0, 0,
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
MFDSplitView::SideColumns, 0x10, 0x18, cockpit);
//---------------------------------------------------------------
+77 -4
View File
@@ -144,6 +144,56 @@ L4PLASMA=SCREEN
# 480x640 - decoded exactly as the pod's VDB split them, no downscale).
L4MFDSPLIT=1
# Size of the six secondary displays in the glass cockpit, as a
# percentage of their pod size. The pod bolted them down at one size;
# on a big panel there is room to trade viewscreen for instrument, so
# turn these up if you want to actually read the other displays while
# you fly. 100 = as the pod had them. Range 25-200 (out-of-range and
# unreadable values fall back to the group setting, then to 100).
#
# The scaling is applied in canvas units, before the cockpit is fitted
# to your window, so a given number looks the same on every monitor.
# The layout stays legal whatever you ask for - the panes are clamped
# against their actual neighbours, shrinking uniformly so a display
# never comes out stretched. They do overlap the viewscreen, exactly
# as the pod's bezels did, but never each other.
#
# L4MFDSCALE sets all five green MFDs at once.
L4MFDSCALE=100
# ...and any single display can override it. Uncomment one to size it
# on its own - useful if you only care about, say, the damage readout.
# UL upper left UC upper center UR upper right
# LL lower left LR lower right
#L4MFDSCALE_UL=100
#L4MFDSCALE_UC=100
#L4MFDSCALE_UR=100
#L4MFDSCALE_LL=100
#L4MFDSCALE_LR=100
# The portrait radar/map, sized on its own (it already sits at 1.35x
# the MFDs by default). It shares the canvas with whichever MFD is
# above it, so at extreme settings one of the two gives way.
L4RADARSCALE=100
# Where the radar sits:
# CENTER bottom centre, under the viewscreen, as the pod had it
# (default; BOTTOM and CENTRE mean the same)
# LEFT bottom left corner (or BOTTOMLEFT)
# RIGHT bottom right corner (or BOTTOMRIGHT)
# MIDLEFT left edge, halfway up (or LEFTCENTER / LEFTCENTRE)
# MIDRIGHT right edge, halfway up (or RIGHTCENTER / RIGHTCENTRE)
# Anywhere but CENTER stops it blocking the middle of the road, which
# is worth having on a wide screen.
#
# In a bottom corner it is one of three panes along the bottom, and the
# lower MFD whose corner it takes slides inboard beside it. Halfway up
# a side it leaves the bottom row entirely and sits between that side's
# two MFDs - roomy on a tall radar, but if the MFDs on that side are
# also scaled up, the radar is the one that gives way (it has to clear
# both of them, and it grows from the middle in both directions).
L4RADARPOS=CENTER
# Simulation/render frame rate, integer frames/second. The desktop
# default is 60; the arcade pods shipped at 25.
TARGETFPS=60
@@ -243,12 +293,24 @@ cd /d "%~dp0"
start rpl4opt.exe -windowed -res 1920 1080
"@
Set-Content -Path "$dist\start-fullscreen.bat" -Encoding ascii -Value @"
@echo off
rem Red Planet 4.12 - borderless over the whole monitor. -fit sizes the
rem window to the panel AND picks the render size to match the cockpit
rem canvas it lands in, so no -res guessing: the 3D arrives 1:1 instead
rem of being stretched. Pass -res yourself to override it.
rem (Add -egg TEST.EGG to skip the menu and run the canned mission.)
cd /d "%~dp0"
start rpl4opt.exe -fit
"@
Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @"
Red Planet 4.12.2
=================
Run start-windowed.bat (or: rpl4opt.exe -windowed -res 800 600 -egg TEST.EGG).
No cockpit hardware needed. If there is no sound, run oalinst.exe once.
Run start-fullscreen.bat for borderless over the whole monitor, or
start-windowed.bat to keep a title bar. No cockpit hardware needed.
If there is no sound, run oalinst.exe once.
Controls (XInput controller and/or keyboard) - EVERY input is
rebindable: edit bindings.txt beside the exe (written with the full
@@ -268,8 +330,19 @@ documented default layout on first run; delete it to restore).
The cockpit scales to whatever window you give it - maximise it and it
grows, keeping its 16:9 shape (an ultrawide gets black bars rather than
a stretched cockpit). The 3D itself renders at whatever -res says, so
on a big screen raise it to match: rpl4opt.exe -windowed -res 2560 1440.
a stretched cockpit).
The 3D itself renders at whatever -res says and is then stretched onto
the cockpit's viewscreen, so a mismatched -res costs sharpness. -fit
takes care of that for you: it measures the monitor, works out the
canvas the cockpit will settle on, and asks for exactly that render
size, so the picture arrives 1:1.
rpl4opt.exe -fit borderless, res chosen for you
rpl4opt.exe -fit -res 1280 720 same window, lighter render
rpl4opt.exe -windowed -res 2560 1440 pick both yourself
(-windowed-fullscreen is accepted as a long spelling of -fit.)
The full pod cockpit comes up in a single window: three green MFDs
across the top, the 3D viewscreen centered with the orange plasma glass