Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c696c9952 | ||
|
|
ea9491d2d5 | ||
|
|
8dc6605a07 | ||
|
|
6b43971d2d | ||
|
|
b6045f3c94 | ||
|
|
0bec0f9640 | ||
|
|
8fb4b72f7a | ||
|
|
00cb87907c | ||
|
|
53228686b4 | ||
|
|
1bd6dd83e2 | ||
|
|
1ba3bde36a | ||
|
|
4b25f89e6f | ||
|
|
3194d3f973 | ||
|
|
0b84b32486 |
@@ -67,3 +67,6 @@ assets/RP411/startrp-800x600.ps1
|
||||
# ...but never the spool runtime output or Windows thumbnail caches.
|
||||
assets/**/*.spl
|
||||
assets/**/last.spl
|
||||
|
||||
# packaged releases (attached to Gitea releases, not tracked)
|
||||
RedPlanet-*.zip
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+127
-31
@@ -10,6 +10,33 @@ namespace
|
||||
|
||||
const int buttonGap = 4;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Button banks run UNDER the glass: a button reaches this far in
|
||||
// from the display edge, but only the indicator strip clears that
|
||||
// edge - the rest sits behind the picture, which is what the
|
||||
// player actually clicks. Applies to both the MFD strips (depth
|
||||
// measured vertically) and the map's side columns (horizontally).
|
||||
//---------------------------------------------------------------
|
||||
// 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.
|
||||
@@ -135,6 +162,10 @@ MFDSplitView::MFDSplitView(
|
||||
header->biCompression = BI_RGB;
|
||||
blitHeader = header;
|
||||
|
||||
buttonStyle = button_style;
|
||||
buttonAnchorA = anchorA;
|
||||
buttonAnchorB = anchorB;
|
||||
|
||||
int client_width = display_width;
|
||||
int client_height = display_height;
|
||||
LayoutButtons(button_style, anchorA, anchorB,
|
||||
@@ -255,10 +286,20 @@ void
|
||||
// edges (no border against the viewscreen behind); RIO
|
||||
// addresses descend from the anchor, row-major (vRIO
|
||||
// CockpitLayout::Mfd).
|
||||
//
|
||||
// The banks reach BEHIND the display: each button is
|
||||
// mfdButtonHeight tall but only mfdIndicatorStrip of it
|
||||
// clears the glass edge, so the lamp reads as a thin strip
|
||||
// and the picture above/below it is the click target. Paint
|
||||
// draws the buttons before the MFD image for that reason.
|
||||
//-----------------------------------------------------------
|
||||
int strip_h = display_height / 8;
|
||||
if (strip_h < 18) strip_h = 18;
|
||||
if (strip_h > 40) strip_h = 40;
|
||||
int strip_h = indicatorStrip;
|
||||
int button_h = buttonDepth;
|
||||
|
||||
// on a scaled-down cockpit, keep the two banks from meeting
|
||||
int limit = (display_height + 2 * strip_h) / 2;
|
||||
if (button_h > limit) button_h = limit;
|
||||
if (button_h < strip_h) button_h = strip_h;
|
||||
|
||||
int button_w = (display_width - 3 * buttonGap) / 4;
|
||||
|
||||
@@ -267,56 +308,73 @@ void
|
||||
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
int row = i / 4; // 0 = top strip, 1 = bottom strip
|
||||
int row = i / 4; // 0 = top bank, 1 = bottom bank
|
||||
int col = i % 4;
|
||||
|
||||
ScreenButton *button = &buttons[buttonCount++];
|
||||
button->unit = anchorA - i;
|
||||
button->amber = 0;
|
||||
button->x = col * (button_w + buttonGap);
|
||||
button->y = row ? (displayY + displayH) : 0;
|
||||
button->y = row
|
||||
? (displayY + displayH + strip_h - button_h)
|
||||
: 0;
|
||||
button->w = (col == 3)
|
||||
? (display_width - 3 * (button_w + buttonGap))
|
||||
: button_w;
|
||||
button->h = strip_h;
|
||||
button->h = button_h;
|
||||
}
|
||||
}
|
||||
else if (button_style == SideColumns)
|
||||
{
|
||||
//-----------------------------------------------------------
|
||||
// 6 buttons down each side of the glass, stacked contiguous
|
||||
// and flush to the pane edges, like the pod's strips (the
|
||||
// pod wires only 6 of each column's 8 addresses to buttons;
|
||||
// the rest are Tesla relays). Left = anchorA ascending,
|
||||
// right = anchorB.
|
||||
// 6 buttons down each side of the glass (the pod wires only 6
|
||||
// of each column's 8 addresses to buttons; the rest are Tesla
|
||||
// relays). Left = anchorA ascending, right = anchorB.
|
||||
//
|
||||
// Same trick as the MFD banks, turned on its side: each
|
||||
// button reaches buttonDepth in behind the map and leaves an
|
||||
// indicatorStrip clearing the edge, so the lamp reads as a
|
||||
// slim column and the map itself is the click target.
|
||||
//-----------------------------------------------------------
|
||||
int col_w = display_width / 8;
|
||||
if (col_w < 20) col_w = 20;
|
||||
if (col_w > 40) col_w = 40;
|
||||
int strip_w = indicatorStrip;
|
||||
int button_w = buttonDepth;
|
||||
|
||||
int button_h = display_height / 6;
|
||||
// keep the two columns from meeting behind a narrow map
|
||||
int limit = (display_width + 2 * strip_w) / 2;
|
||||
if (button_w > limit) button_w = limit;
|
||||
if (button_w < strip_w) button_w = strip_w;
|
||||
|
||||
displayX = col_w;
|
||||
*client_width = display_width + 2 * col_w;
|
||||
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->w = col_w;
|
||||
left->y = top;
|
||||
left->w = button_w;
|
||||
left->h = h;
|
||||
|
||||
ScreenButton *right = &buttons[buttonCount++];
|
||||
right->unit = anchorB + i;
|
||||
right->amber = 1;
|
||||
right->x = displayX + displayW;
|
||||
right->x = displayX + displayW + strip_w - button_w;
|
||||
right->y = left->y;
|
||||
right->w = col_w;
|
||||
right->w = button_w;
|
||||
right->h = h;
|
||||
}
|
||||
}
|
||||
@@ -342,15 +400,11 @@ void
|
||||
|
||||
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
||||
|
||||
SetStretchBltMode(mem, COLORONCOLOR);
|
||||
StretchDIBits(
|
||||
mem,
|
||||
displayX, displayY, displayW, displayH,
|
||||
0, 0, sourceWidth, sourceHeight,
|
||||
pixels, (const BITMAPINFO *) blitHeader,
|
||||
DIB_RGB_COLORS, SRCCOPY
|
||||
);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Buttons first, glass second: the MFD banks extend under the
|
||||
// display so the picture is the click target, and the image
|
||||
// painted over them leaves only their indicator strips showing.
|
||||
//---------------------------------------------------------------
|
||||
for (int i = 0; i < buttonCount; ++i)
|
||||
{
|
||||
const ScreenButton *button = &buttons[i];
|
||||
@@ -375,6 +429,20 @@ void
|
||||
DeleteObject(frame);
|
||||
}
|
||||
|
||||
// HALFTONE area-averages the downscale (the compact glass shows the
|
||||
// full 640x480 canvas at ~half size); COLORONCOLOR dropped every other
|
||||
// row/column, which shredded the 1-bit vector strokes and small text.
|
||||
// HALFTONE requires the brush origin be set (MSDN).
|
||||
SetStretchBltMode(mem, HALFTONE);
|
||||
SetBrushOrgEx(mem, 0, 0, NULL);
|
||||
StretchDIBits(
|
||||
mem,
|
||||
displayX, displayY, displayW, displayH,
|
||||
0, 0, sourceWidth, sourceHeight,
|
||||
pixels, (const BITMAPINFO *) blitHeader,
|
||||
DIB_RGB_COLORS, SRCCOPY
|
||||
);
|
||||
|
||||
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
||||
|
||||
SelectObject(mem, old_surface);
|
||||
@@ -433,6 +501,34 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Re-scale to a new glass size: the pixel buffer keeps its native
|
||||
// source resolution, so only the destination rectangle, the button
|
||||
// geometry and the window size change.
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
void
|
||||
MFDSplitView::Resize(int display_width, int display_height)
|
||||
{
|
||||
Check_Pointer(this);
|
||||
|
||||
buttonCount = 0;
|
||||
pressedIndex = -1;
|
||||
|
||||
int client_width = display_width;
|
||||
int client_height = display_height;
|
||||
LayoutButtons(buttonStyle, buttonAnchorA, buttonAnchorB,
|
||||
display_width, display_height, &client_width, &client_height);
|
||||
clientWidth = client_width;
|
||||
clientHeight = client_height;
|
||||
|
||||
if (window != NULL)
|
||||
{
|
||||
SetWindowPos((HWND) window, NULL, 0, 0,
|
||||
client_width, client_height,
|
||||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MFDSplitView::Repaint()
|
||||
{
|
||||
|
||||
@@ -65,6 +65,12 @@ public:
|
||||
void
|
||||
SetPosition(int x, int y);
|
||||
|
||||
// Re-scale the glass (cockpit resized): the source buffer is
|
||||
// unchanged, only the destination size and the button geometry
|
||||
// derived from it.
|
||||
void
|
||||
Resize(int display_width, int display_height);
|
||||
|
||||
~MFDSplitView();
|
||||
|
||||
Logical
|
||||
@@ -128,4 +134,11 @@ protected:
|
||||
buttonCount;
|
||||
int
|
||||
pressedIndex;
|
||||
|
||||
// kept so the bank can be rebuilt when the cockpit re-scales
|
||||
ButtonStyle
|
||||
buttonStyle;
|
||||
int
|
||||
buttonAnchorA,
|
||||
buttonAnchorB;
|
||||
};
|
||||
|
||||
@@ -376,8 +376,11 @@ namespace
|
||||
"pad DPadRight button 0x43 # Hat Right\n"
|
||||
"pad LeftShoulder button 0x3D # Panic\n"
|
||||
"pad RightShoulder button 0x3F # Throttle button (reverse)\n"
|
||||
"pad Start button 0x37 # config 1\n"
|
||||
"pad Back button 0x36 # config 2\n"
|
||||
"\n"
|
||||
"# Start and Back are left free - map them to whatever you want, e.g.\n"
|
||||
"# the config buttons on the Upper Right MFD:\n"
|
||||
"#pad Start button 0x37\n"
|
||||
"#pad Back button 0x36\n"
|
||||
"\n"
|
||||
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
|
||||
"# ---- QWERTY row is the row under it, left to right across the\n"
|
||||
|
||||
+682
-78
@@ -3821,6 +3821,496 @@ static LRESULT CALLBACK
|
||||
return CallWindowProcA(gViewscreenBaseProc, hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// The cockpit shell: re-fit the canvas whenever the window changes size
|
||||
// (maximise, restore, drag). The game window's own proc handles the
|
||||
// rest, so chain to it.
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
static WNDPROC gCockpitBaseProc = NULL;
|
||||
|
||||
static LRESULT CALLBACK
|
||||
CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (message == WM_SIZE && wParam != SIZE_MINIMIZED)
|
||||
{
|
||||
SVGA16 *cockpit = SVGA16::GetCockpit();
|
||||
if (cockpit != NULL)
|
||||
{
|
||||
cockpit->LayoutCockpit(LOWORD(lParam), HIWORD(lParam));
|
||||
}
|
||||
}
|
||||
return CallWindowProcA(gCockpitBaseProc, hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
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
|
||||
// letterboxes), centred, with every pane re-scaled and re-placed.
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
void
|
||||
SVGA16::LayoutCockpit(int client_w, int client_h)
|
||||
{
|
||||
Check_Pointer(this);
|
||||
|
||||
if (!splitViews || client_w <= 0 || client_h <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int canvas_w = 1920;
|
||||
const int canvas_h = 1080;
|
||||
|
||||
int fit_w = (client_w * 100) / canvas_w;
|
||||
int fit_h = (client_h * 100) / canvas_h;
|
||||
int scale = (fit_w < fit_h) ? fit_w : fit_h;
|
||||
if (scale < 25) scale = 25;
|
||||
|
||||
int view_w = (canvas_w * scale) / 100;
|
||||
int view_h = (canvas_h * scale) / 100;
|
||||
|
||||
// centre the canvas: the leftover is black on the long axis
|
||||
int origin_x = (client_w - view_w) / 2;
|
||||
int origin_y = (client_h - view_h) / 2;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The viewscreen fills the canvas; the 3D presents into it and
|
||||
// D3D stretches the back buffer to fit, so -res only decides how
|
||||
// sharp the scene is, not how big the cockpit is.
|
||||
//---------------------------------------------------------------
|
||||
if (cockpitViewscreen != NULL)
|
||||
{
|
||||
SetWindowPos(cockpitViewscreen, NULL,
|
||||
origin_x, origin_y, view_w, view_h,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
GlassSize glass[SplitViewCount];
|
||||
CockpitGlassSizes(scale, glass);
|
||||
|
||||
for (int view = 0; view < SplitViewCount; ++view)
|
||||
{
|
||||
if (splitView[view] == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
splitView[view]->Resize(glass[view].w, glass[view].h);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Top row edge to edge across the canvas, bottom cluster on the
|
||||
// bottom edge - all relative to the centred canvas origin.
|
||||
//---------------------------------------------------------------
|
||||
if (splitView[SplitMFDUpperLeft] != NULL)
|
||||
{
|
||||
splitView[SplitMFDUpperLeft]->SetPosition(origin_x, origin_y);
|
||||
}
|
||||
if (splitView[SplitMFDUpperCenter] != NULL)
|
||||
{
|
||||
splitView[SplitMFDUpperCenter]->SetPosition(
|
||||
origin_x + (view_w - splitView[SplitMFDUpperCenter]->ClientWidth()) / 2,
|
||||
origin_y);
|
||||
}
|
||||
if (splitView[SplitMFDUpperRight] != NULL)
|
||||
{
|
||||
splitView[SplitMFDUpperRight]->SetPosition(
|
||||
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 + slide_left,
|
||||
origin_y + view_h - splitView[SplitMFDLowerLeft]->ClientHeight());
|
||||
}
|
||||
if (splitView[SplitMFDLowerRight] != NULL)
|
||||
{
|
||||
splitView[SplitMFDLowerRight]->SetPosition(
|
||||
origin_x + view_w - splitView[SplitMFDLowerRight]->ClientWidth()
|
||||
- slide_right,
|
||||
origin_y + view_h - splitView[SplitMFDLowerRight]->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
|
||||
// present clips around every pane.
|
||||
if (cockpitViewscreen != NULL)
|
||||
{
|
||||
SetWindowPos(cockpitViewscreen, HWND_BOTTOM, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "SVGA16: cockpit fitted " << view_w << "x" << view_h
|
||||
<< " at " << scale << "% in a " << client_w << "x" << client_h
|
||||
<< " client\n" << std::flush;
|
||||
}
|
||||
|
||||
SVGA16::SVGA16(
|
||||
int mode,
|
||||
int init_width,
|
||||
@@ -3842,11 +4332,19 @@ SVGA16::SVGA16(
|
||||
//------------------------------------------------------------------
|
||||
splitViews = False;
|
||||
cockpitViewscreen = NULL;
|
||||
Logical explodedViews = False;
|
||||
{
|
||||
const char *split_string = getenv("L4MFDSPLIT");
|
||||
if (split_string != NULL && atoi(split_string) != 0)
|
||||
{
|
||||
splitViews = True;
|
||||
// L4MFDSPLIT=2+ : exploded diagnostic view - each display
|
||||
// in its own native-resolution desktop window instead of
|
||||
// the composited glass cockpit.
|
||||
if (atoi(split_string) >= 2)
|
||||
{
|
||||
explodedViews = True;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int view = 0; view < SplitViewCount; ++view)
|
||||
@@ -3856,7 +4354,88 @@ SVGA16::SVGA16(
|
||||
|
||||
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
|
||||
|
||||
if (splitViews)
|
||||
if (splitViews && explodedViews)
|
||||
{
|
||||
//---------------------------------------------------------------
|
||||
// Exploded diagnostic view: every display in its own full-size
|
||||
// desktop window (MFDs native 640x480, map 480x640 rotated),
|
||||
// decoded exactly as the pod's VDB split them from the single
|
||||
// gauge canvas - no cockpit compositing, no downscale. Each MFD
|
||||
// can be read and screenshotted at full resolution, matching the
|
||||
// emulator's per-channel reference windows. Laid out in the pod
|
||||
// arrangement (top row / bottom row) across the work area; the
|
||||
// windows are draggable (dragging a title bar briefly pauses the
|
||||
// game's single-per-frame message pump, as expected).
|
||||
//---------------------------------------------------------------
|
||||
RECT work;
|
||||
work.left = 0; work.top = 0; work.right = 1920; work.bottom = 1080;
|
||||
SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0);
|
||||
int work_w = work.right - work.left;
|
||||
int work_h = work.bottom - work.top;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// 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::MFDStrips, 0x2F, 0, 0);
|
||||
splitView[SplitMFDUpperCenter] = new MFDSplitView(
|
||||
"MFD upper center", init_width, init_height,
|
||||
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, 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,
|
||||
MFDSplitView::MFDStrips, 0x0F, 0, 0);
|
||||
splitView[SplitMFDLowerRight] = new MFDSplitView(
|
||||
"MFD lower right", init_width, init_height,
|
||||
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, 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)
|
||||
{
|
||||
//---------------------------------------------------------------
|
||||
// The 1920x1080 internal cockpit canvas.
|
||||
@@ -3876,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);
|
||||
@@ -3888,22 +4479,46 @@ 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;
|
||||
|
||||
int scale_num = 100;
|
||||
if (avail_w < canvas_w || avail_h < canvas_h)
|
||||
{
|
||||
int fit_w = (avail_w * 100) / canvas_w;
|
||||
int fit_h = (avail_h * 100) / canvas_h;
|
||||
scale_num = (fit_w < fit_h) ? fit_w : fit_h;
|
||||
if (scale_num < 25) scale_num = 25;
|
||||
//-----------------------------------------------------------
|
||||
// One uniform scale, up or down: the canvas keeps its 16:9
|
||||
// shape whatever the monitor is, so a wider-than-16:9 desktop
|
||||
// letterboxes instead of stretching. Scaling UP is free
|
||||
// quality on the MFDs - their glass is a downscale of a
|
||||
// native 640x480 channel until about 200%.
|
||||
//-----------------------------------------------------------
|
||||
int fit_w = (avail_w * 100) / canvas_w;
|
||||
int fit_h = (avail_h * 100) / canvas_h;
|
||||
int scale_num = (fit_w < fit_h) ? fit_w : fit_h;
|
||||
if (scale_num < 25) scale_num = 25;
|
||||
|
||||
DEBUG_STREAM << "SVGA16: cockpit canvas scaled to " << scale_num
|
||||
<< "% for the " << (work.right - work.left) << "x"
|
||||
<< (work.bottom - work.top) << " work area\n" << std::flush;
|
||||
}
|
||||
DEBUG_STREAM << "SVGA16: cockpit canvas at " << scale_num
|
||||
<< "% for the " << (work.right - work.left) << "x"
|
||||
<< (work.bottom - work.top) << " work area\n" << std::flush;
|
||||
|
||||
#define COCKPIT_CANVAS(v) (((v) * scale_num) / 100)
|
||||
|
||||
@@ -3915,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);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
@@ -3961,95 +4599,61 @@ 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);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Place the panes from their measured sizes: top row edge to
|
||||
// edge across the canvas, bottom cluster on the bottom edge.
|
||||
// Everything above built the panes at a starting size; the
|
||||
// layout itself lives in LayoutCockpit so a resized window can
|
||||
// re-run it. Track the instance for the WM_SIZE hook below.
|
||||
//---------------------------------------------------------------
|
||||
if (splitView[SplitMFDUpperLeft] != NULL)
|
||||
{
|
||||
splitView[SplitMFDUpperLeft]->SetPosition(0, 0);
|
||||
}
|
||||
if (splitView[SplitMFDUpperCenter] != NULL)
|
||||
{
|
||||
splitView[SplitMFDUpperCenter]->SetPosition(
|
||||
(client_w - splitView[SplitMFDUpperCenter]->ClientWidth()) / 2, 0);
|
||||
}
|
||||
if (splitView[SplitMFDUpperRight] != NULL)
|
||||
{
|
||||
splitView[SplitMFDUpperRight]->SetPosition(
|
||||
client_w - splitView[SplitMFDUpperRight]->ClientWidth(), 0);
|
||||
}
|
||||
if (splitView[SplitMFDLowerLeft] != NULL)
|
||||
{
|
||||
splitView[SplitMFDLowerLeft]->SetPosition(
|
||||
0, client_h - splitView[SplitMFDLowerLeft]->ClientHeight());
|
||||
}
|
||||
if (splitView[SplitMFDLowerRight] != NULL)
|
||||
{
|
||||
splitView[SplitMFDLowerRight]->SetPosition(
|
||||
client_w - splitView[SplitMFDLowerRight]->ClientWidth(),
|
||||
client_h - splitView[SplitMFDLowerRight]->ClientHeight());
|
||||
}
|
||||
if (splitView[SplitMap] != NULL)
|
||||
{
|
||||
splitView[SplitMap]->SetPosition(
|
||||
(client_w - splitView[SplitMap]->ClientWidth()) / 2,
|
||||
client_h - splitView[SplitMap]->ClientHeight());
|
||||
}
|
||||
activeCockpit = this;
|
||||
|
||||
// The panes go OVER the main display (pod bezel occlusion): pin
|
||||
// the viewscreen to the bottom of the sibling z-order so the 3D
|
||||
// present clips around every pane.
|
||||
if (cockpitViewscreen != NULL)
|
||||
if (cockpit != NULL)
|
||||
{
|
||||
SetWindowPos(cockpitViewscreen, HWND_BOTTOM, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
RECT inner;
|
||||
GetClientRect(cockpit, &inner);
|
||||
LayoutCockpit(inner.right, inner.bottom);
|
||||
|
||||
// catch maximise / restore / drag-resize and re-fit
|
||||
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
||||
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
||||
}
|
||||
else
|
||||
{
|
||||
LayoutCockpit(client_w, client_h);
|
||||
}
|
||||
|
||||
// the plasma glass sits out for now
|
||||
PlasmaScreen::Hide();
|
||||
|
||||
DEBUG_STREAM << "SVGA16: single-window cockpit assembled ("
|
||||
<< client_w << "x" << client_h << " canvas at " << scale_num
|
||||
<< "%, viewscreen " << view_w << "x" << view_h
|
||||
<< " centered)\n" << std::flush;
|
||||
|
||||
#undef COCKPIT_CANVAS
|
||||
}
|
||||
//STUBBED: VIDEO RB 1/15/07
|
||||
|
||||
@@ -308,6 +308,26 @@ private:
|
||||
SplitViewCount
|
||||
};
|
||||
|
||||
public:
|
||||
//------------------------------------------------------------------
|
||||
// Glass size of one secondary display, in whatever units the caller
|
||||
// asked for (canvas units while clamping, device pixels on return).
|
||||
//------------------------------------------------------------------
|
||||
struct GlassSize
|
||||
{
|
||||
int w, h;
|
||||
};
|
||||
|
||||
protected:
|
||||
//------------------------------------------------------------------
|
||||
// The six displays sized for a given canvas scale, honouring the
|
||||
// player's per-display scaling and clamped to keep the pod
|
||||
// arrangement legal. Every layout goes through here, first and
|
||||
// resized alike, so the two can never disagree.
|
||||
//------------------------------------------------------------------
|
||||
static void
|
||||
CockpitGlassSizes(int scale, GlassSize sizes[SplitViewCount]);
|
||||
|
||||
void
|
||||
FillSplitMFD(
|
||||
SplitViewID view,
|
||||
@@ -315,12 +335,30 @@ private:
|
||||
int shift
|
||||
);
|
||||
|
||||
public:
|
||||
//------------------------------------------------------------------
|
||||
// Fit the cockpit into a client area: one uniform scale keeps the
|
||||
// 1920x1080 canvas at its own aspect (so an ultrawide letterboxes
|
||||
// rather than stretching), centred in whatever space it is given.
|
||||
// Re-run whenever the cockpit window is resized.
|
||||
//------------------------------------------------------------------
|
||||
void
|
||||
LayoutCockpit(int client_w, int client_h);
|
||||
|
||||
static SVGA16 *
|
||||
GetCockpit()
|
||||
{ return activeCockpit; }
|
||||
|
||||
protected:
|
||||
Logical
|
||||
splitViews;
|
||||
MFDSplitView
|
||||
*splitView[SplitViewCount];
|
||||
HWND
|
||||
cockpitViewscreen; // child pane the 3D scene presents into
|
||||
|
||||
static SVGA16
|
||||
*activeCockpit; // the instance owning the cockpit window
|
||||
};
|
||||
|
||||
//########################################################################
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Red Planet 4.12 — the Steamification
|
||||
|
||||
**Red Planet** is VWE's pod-racing game: eight-player VTV races on Mars,
|
||||
originally played from inside full-motion cockpit pods. This repo is the
|
||||
third life of that code:
|
||||
**Red Planet** is VWE's pod-racing game: eight-player VTV contests on Mars
|
||||
— **Martian Death Race** and **Martian Football** — originally played from
|
||||
inside VWE Tesla cockpit pods. This repo is the third life of that code:
|
||||
|
||||
| Generation | What it was |
|
||||
|------------|-------------|
|
||||
| **Red Planet 4.10** | The original game, running on the **Tesla 1** pod platform (DOS-era MUNGA engine, serial RIO cockpit hardware, operator console, batch-file relaunch between missions) |
|
||||
| **Red Planet 4.11** | The **Win32 port** of 4.10 ([RP411](https://gitea.mysticmachines.com/VWE/RP411.git)) — DirectX 9, WinSock TCP, TeslaConsole/TeslaLauncher session control; still arcade pods, still a LAN |
|
||||
| **Red Planet 4.12** | **This repo: the Steamification of 4.11** — the same engine, the same wire protocol, the same missions, made sellable on Steam and playable over the internet with no cockpit hardware |
|
||||
| **Red Planet 4.11** | The **Win32 port** of 4.10 ([RP411](https://gitea.mysticmachines.com/VWE/RP411.git)) — DirectX 9, WinSock TCP, TeslaConsole/TeslaLauncher session control; still runs in pods, still a LAN |
|
||||
| **Red Planet 4.12** | **This repo: the Steamification of 4.11** — the same engine, the same wire protocol, the same missions, made distributable on Steam and playable over the internet with no cockpit hardware |
|
||||
|
||||
The architecture is deliberately conservative: the arcade's design survives
|
||||
The architecture is deliberately conservative: the VWE's design survives
|
||||
intact, with each pod-era dependency replaced by a consumer equivalent
|
||||
behind the engine's existing seams.
|
||||
|
||||
@@ -19,23 +19,34 @@ behind the engine's existing seams.
|
||||
| RIO cockpit board (serial) | **PadRIO** — virtual RIO from XInput pad + keyboard, fully rebindable (`bindings.txt`, vRIO profile format) |
|
||||
| Seven physical displays | **Single-window cockpit** — all displays composed on a locked 1920×1080 canvas around the viewscreen, with the real button banks lamp-lit and clickable |
|
||||
| Pod button lamps | On-screen lamps, plus an **RGB keyboard mirror** (Windows Dynamic Lighting) |
|
||||
| TeslaConsole operator | **In-game front end** — race setup menu builds the mission egg locally; an in-process console marshals every race (missions still only end on a console stop, exactly as designed in 1995) |
|
||||
| TeslaConsole operator | **In-game front end** — race setup menu builds the mission egg locally; an in-process console marshals every race (missions still only end on a console stop, exactly as designed in 1994) |
|
||||
| TeslaLauncher relaunch-per-mission | **Single binary** — menu → race → results → menu in one process |
|
||||
| WinSock TCP LAN mesh | **NetTransport seam** — the deterministic pod mesh unchanged, running over plain TCP (LAN/dev) or **Steam Networking Sockets** (FakeIP + Steam Datagram Relay) |
|
||||
| Site network / fixed IPs | **Steam lobbies** — lobby owner is the console; members exchange FakeIPs and loadouts as lobby data |
|
||||
|
||||
**Status: it works.** Release
|
||||
[v4.12.1](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.1)
|
||||
carries the first verified end-to-end internet build: three machines, three
|
||||
**Status: it works.**
|
||||
[v4.12.2](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.2)
|
||||
carried the first verified end-to-end internet build: three machines, three
|
||||
Steam accounts, lobby → mesh → marshaled five-minute race → deaths and
|
||||
respawns → timed stop → results on every machine → rematch from the same
|
||||
lobby.
|
||||
|
||||
[v4.12.3](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.3)
|
||||
is about the screen. The cockpit now scales up as well as down and re-fits
|
||||
whenever the window changes, keeping 16:9 so a wide panel letterboxes
|
||||
instead of stretching; `-fit` runs it borderless over the whole monitor and
|
||||
picks the render size to land on the viewscreen 1:1. The displays are the
|
||||
player's to arrange — each of the six can be scaled on its own and the
|
||||
radar moved out of the middle of the road (`environ.ini`). Each display's
|
||||
button bank now reaches under its glass, so the picture itself is the press
|
||||
target rather than a sliver at the edge.
|
||||
|
||||
## Playing
|
||||
|
||||
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
|
||||
run `start-windowed.bat` — the game boots into the race setup menu. Steam
|
||||
multiplayer: see
|
||||
run `start-windowed.bat` — the game boots into the setup menu, where the
|
||||
SCENARIO group picks Death Race or Football (Football swaps in the
|
||||
team/position columns and its own track list). Steam multiplayer: see
|
||||
[docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) (until RP412
|
||||
has its own AppID it runs under Spacewar, 480).
|
||||
|
||||
@@ -45,7 +56,8 @@ button, and axis; written with the full default layout on first run).
|
||||
Default controls: numpad flies (8/2/4/6 stick, 7/9 pedals, 0 trigger),
|
||||
Shift/Ctrl throttle, Alt reverse, arrows look, Space fires, letter rows
|
||||
are the MFD button banks as printed on the panel. **Alt+Q** aborts a
|
||||
mission.
|
||||
mission. Full map with pad and keyboard diagrams:
|
||||
[docs/CONTROLS.md](docs/CONTROLS.md).
|
||||
|
||||
## Building
|
||||
|
||||
@@ -58,6 +70,7 @@ mission.
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| [docs/CONTROLS.md](docs/CONTROLS.md) | Controls map — pad and keyboard diagrams, the panel button banks, rebinding |
|
||||
| [docs/RP412-ROADMAP.md](docs/RP412-ROADMAP.md) | The original plan and workstreams |
|
||||
| [docs/RP412-FRONTEND-DESIGN.md](docs/RP412-FRONTEND-DESIGN.md) | TeslaConsole analysis, the egg format, the console protocol, and the Steam mapping — with status notes as each layer landed |
|
||||
| [docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) | Multiplayer test procedure, Steam Input notes, the abort key |
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "Red Planet 4.12.2" << std::endl << std::flush;
|
||||
DEBUG_STREAM << "Red Planet 4.12.3" << std::endl << std::flush;
|
||||
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
|
||||
|
||||
#ifdef RP412_STEAM
|
||||
|
||||
+484
-69
@@ -35,6 +35,52 @@ namespace
|
||||
{ "headoff", "Freezemoon's Freeway" },
|
||||
};
|
||||
|
||||
// Football excludes pain/headoff and adds the football build of
|
||||
// Freezemoon's Freeway (RPConfig.xml per-scenario invalid lists).
|
||||
const CatalogEntry kFootballMaps[] =
|
||||
{
|
||||
{ "wise", "Wiseguy's Wake" },
|
||||
{ "lyzlane", "Lyz's Lane" },
|
||||
{ "yip", "Yip's Yahoorama" },
|
||||
{ "blade", "Blade's Edge" },
|
||||
{ "otto", "Berserker's Bahnzai" },
|
||||
{ "frstrm", "Firestorm's Fury" },
|
||||
{ "burnt", "Burnt Cookies" },
|
||||
{ "brewers", "Brewer's Bane" },
|
||||
{ "headmf", "Freezemoon's Freeway" },
|
||||
};
|
||||
|
||||
const CatalogEntry kScenarios[] =
|
||||
{
|
||||
{ "race", "Death Race" },
|
||||
{ "football", "Football" },
|
||||
};
|
||||
|
||||
// Two-color teams: crushers/blockers wear the team color, the
|
||||
// runner wears the runner color (that is how you spot the ball).
|
||||
struct TeamEntry
|
||||
{
|
||||
const char *key;
|
||||
const char *name;
|
||||
const char *teamColor;
|
||||
const char *runnerColor;
|
||||
};
|
||||
|
||||
const TeamEntry kTeams[] =
|
||||
{
|
||||
{ "Red/Pink", "Red / Pink", "Red", "Pink" },
|
||||
{ "Blue/Aqua", "Blue / Aqua", "Blue", "Aqua" },
|
||||
{ "Green/Yellow", "Green / Yellow", "Green", "Yellow" },
|
||||
{ "Black/Purple", "Black / Purple", "Black", "Purple" },
|
||||
};
|
||||
|
||||
const CatalogEntry kPositions[] =
|
||||
{
|
||||
{ "runner", "Runner" },
|
||||
{ "crusher", "Crusher" },
|
||||
{ "blocker", "Blocker" },
|
||||
};
|
||||
|
||||
const CatalogEntry kVehicles[] =
|
||||
{
|
||||
{ "quark", "Quark" },
|
||||
@@ -125,12 +171,31 @@ namespace
|
||||
GroupTime,
|
||||
GroupWeather,
|
||||
GroupLength,
|
||||
GroupScenario,
|
||||
GroupTeam, // football only
|
||||
GroupPosition, // football only
|
||||
GroupLaunch,
|
||||
GroupSteamHost, // buttons, not selections (Steam builds only)
|
||||
GroupSteamJoin,
|
||||
GroupCount
|
||||
};
|
||||
|
||||
Logical IsFootball(const int *selection)
|
||||
{
|
||||
return selection[GroupScenario] == 1;
|
||||
}
|
||||
|
||||
const CatalogEntry *ActiveMaps(const int *selection, int *count)
|
||||
{
|
||||
if (IsFootball(selection))
|
||||
{
|
||||
*count = (int)(sizeof(kFootballMaps) / sizeof(kFootballMaps[0]));
|
||||
return kFootballMaps;
|
||||
}
|
||||
*count = (int)(sizeof(kMaps) / sizeof(kMaps[0]));
|
||||
return kMaps;
|
||||
}
|
||||
|
||||
struct FEItem
|
||||
{
|
||||
int group;
|
||||
@@ -185,6 +250,8 @@ namespace
|
||||
char vehicle[24];
|
||||
char color[16];
|
||||
char badge[24];
|
||||
char team[32]; // football pick, "" = assign for them
|
||||
char position[16];
|
||||
};
|
||||
|
||||
// lobby-fed override (real personas + loadouts); empty = env parsing
|
||||
@@ -208,6 +275,8 @@ namespace
|
||||
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
|
||||
strcpy(extras[i].badge,
|
||||
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
|
||||
strcpy(extras[i].team, gHostedPilots[i].team);
|
||||
strcpy(extras[i].position, gHostedPilots[i].position);
|
||||
}
|
||||
return gHostedPilotCount;
|
||||
}
|
||||
@@ -259,6 +328,7 @@ namespace
|
||||
}
|
||||
|
||||
ExtraPilot *pilot = &extras[extra_count];
|
||||
memset(pilot, 0, sizeof(*pilot));
|
||||
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
|
||||
sprintf(pilot->name, "PILOT %d", extra_count + 2);
|
||||
strcpy(pilot->vehicle, kVehicles[
|
||||
@@ -311,8 +381,10 @@ namespace
|
||||
int top = client_h / 7;
|
||||
|
||||
int y = top;
|
||||
AddGroupItems(fe, GroupMap, FE_COUNT(kMaps), col1, &y, row_h, col_w);
|
||||
int y_after_maps = y;
|
||||
AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
|
||||
int map_count;
|
||||
ActiveMaps(fe->selection, &map_count);
|
||||
AddGroupItems(fe, GroupMap, map_count, col1, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), col1, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);
|
||||
@@ -321,8 +393,16 @@ namespace
|
||||
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
|
||||
|
||||
y = top + 2 * row_h; // leave room for the name edit + header
|
||||
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
|
||||
if (IsFootball(fe->selection))
|
||||
{
|
||||
AddGroupItems(fe, GroupTeam, FE_COUNT(kTeams), col3, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupPosition, FE_COUNT(kPositions), col3, &y, row_h, col_w);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
|
||||
}
|
||||
|
||||
// launch button
|
||||
FEItem *launch = &fe->items[fe->itemCount++];
|
||||
@@ -365,31 +445,45 @@ namespace
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case GroupScenario: return "SCENARIO";
|
||||
case GroupMap: return "TRACK";
|
||||
case GroupVehicle: return "VEHICLE";
|
||||
case GroupColor: return "COLOR";
|
||||
case GroupBadge: return "BADGE";
|
||||
case GroupTeam: return "TEAM";
|
||||
case GroupPosition: return "POSITION";
|
||||
case GroupTime: return "TIME OF DAY";
|
||||
case GroupWeather: return "WEATHER";
|
||||
case GroupLength: return "RACE LENGTH";
|
||||
case GroupLength: return "GAME LENGTH";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// the map rows need the active scenario's list
|
||||
const int *gItemNameSelection = NULL;
|
||||
|
||||
const char *ItemName(int group, int index)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case GroupMap: return kMaps[index].name;
|
||||
case GroupScenario: return kScenarios[index].name;
|
||||
case GroupMap:
|
||||
if (gItemNameSelection != NULL && IsFootball(gItemNameSelection))
|
||||
{
|
||||
return kFootballMaps[index].name;
|
||||
}
|
||||
return kMaps[index].name;
|
||||
case GroupVehicle: return kVehicles[index].name;
|
||||
case GroupColor: return kColors[index].name;
|
||||
case GroupBadge: return kBadges[index].name;
|
||||
case GroupTeam: return kTeams[index].name;
|
||||
case GroupPosition: return kPositions[index].name;
|
||||
case GroupTime: return kTimes[index].name;
|
||||
case GroupWeather: return kWeather[index].name;
|
||||
case GroupLength: return kLengths[index].name;
|
||||
case GroupLaunch: return "L A U N C H R A C E";
|
||||
case GroupSteamHost: return "HOST STEAM RACE";
|
||||
case GroupSteamJoin: return "JOIN STEAM RACE";
|
||||
case GroupLaunch: return "L A U N C H G A M E";
|
||||
case GroupSteamHost: return "HOST STEAM GAME";
|
||||
case GroupSteamJoin: return "JOIN STEAM GAME";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -413,13 +507,17 @@ namespace
|
||||
SetBkMode(mem, TRANSPARENT);
|
||||
|
||||
// title
|
||||
gItemNameSelection = fe->selection;
|
||||
SelectObject(mem, fe->titleFont);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
RECT title = client;
|
||||
title.top = client.bottom / 28;
|
||||
title.bottom = title.top + client.bottom / 10;
|
||||
DrawTextA(mem, "RED PLANET - MARTIAN DEATH RACE", -1, &title,
|
||||
DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||
DrawTextA(mem,
|
||||
IsFootball(fe->selection)
|
||||
? "RED PLANET - MARTIAN FOOTBALL"
|
||||
: "RED PLANET - MARTIAN DEATH RACE",
|
||||
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||
|
||||
SelectObject(mem, fe->textFont);
|
||||
|
||||
@@ -521,6 +619,20 @@ namespace
|
||||
else
|
||||
{
|
||||
fe->selection[item->group] = item->index;
|
||||
if (item->group == GroupScenario)
|
||||
{
|
||||
// the scenario swaps the map list and the
|
||||
// color/badge vs team/position columns
|
||||
int map_count;
|
||||
ActiveMaps(fe->selection, &map_count);
|
||||
if (fe->selection[GroupMap] >= map_count)
|
||||
{
|
||||
fe->selection[GroupMap] = 0;
|
||||
}
|
||||
RECT client;
|
||||
GetClientRect(fe->menuWindow, &client);
|
||||
LayoutMenu(fe, client.right, client.bottom);
|
||||
}
|
||||
InvalidateRect(fe->menuWindow, NULL, FALSE);
|
||||
}
|
||||
return;
|
||||
@@ -655,13 +767,19 @@ namespace
|
||||
extern const char *kOrdinalsBlock;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Egg assembly (console RPMission.ToEggString, race scenario).
|
||||
// Single player uses the standalone address; a hosted network
|
||||
// race lists the owner first plus one pilot per member pod.
|
||||
// Egg assembly (console RPMission.ToEggString / RPFootballMission).
|
||||
// Single player uses the standalone address; a hosted network game
|
||||
// lists the owner first plus one pilot per member pod. Football
|
||||
// adds team=/position= per pilot and the [teams] blocks; the
|
||||
// owner's team/position picks seed a deterministic assignment
|
||||
// across two teams (each team's first member without a runner
|
||||
// becomes one; the rest alternate crusher/blocker; the runner
|
||||
// wears the team's runner color, everyone else the team color).
|
||||
//---------------------------------------------------------------
|
||||
void BuildEggText(const FEState *fe, std::string &egg)
|
||||
{
|
||||
char line[256];
|
||||
Logical football = IsFootball(fe->selection);
|
||||
|
||||
ExtraPilot extras[maxExtraPilots];
|
||||
char owner_address[64];
|
||||
@@ -672,11 +790,176 @@ namespace
|
||||
extra_count = 0; // single player
|
||||
}
|
||||
|
||||
//
|
||||
// One flat pilot table: the owner then the extras
|
||||
//
|
||||
struct PilotEntry
|
||||
{
|
||||
const char *address;
|
||||
const char *name;
|
||||
const char *vehicle;
|
||||
const char *color;
|
||||
const char *badge;
|
||||
int team; // kTeams index, -1 outside football
|
||||
const char *position;
|
||||
Logical chosePosition; // picked it themselves
|
||||
};
|
||||
PilotEntry pilots[maxExtraPilots + 1];
|
||||
int pilot_count = 0;
|
||||
|
||||
PilotEntry *owner = &pilots[pilot_count++];
|
||||
owner->address = owner_address;
|
||||
owner->name = fe->pilotName;
|
||||
owner->vehicle = kVehicles[fe->selection[GroupVehicle]].key;
|
||||
owner->color = kColors[fe->selection[GroupColor]].key;
|
||||
owner->badge = kBadges[fe->selection[GroupBadge]].key;
|
||||
owner->team = -1;
|
||||
owner->position = NULL;
|
||||
owner->chosePosition = False;
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
{
|
||||
PilotEntry *pilot = &pilots[pilot_count++];
|
||||
pilot->address = extras[p].address;
|
||||
pilot->name = extras[p].name;
|
||||
pilot->vehicle = extras[p].vehicle;
|
||||
pilot->color = extras[p].color;
|
||||
pilot->badge = extras[p].badge;
|
||||
pilot->team = -1;
|
||||
pilot->position = NULL;
|
||||
pilot->chosePosition = False;
|
||||
}
|
||||
|
||||
//
|
||||
// Football: every pilot's own team/position pick is honored;
|
||||
// unset picks get filled in, then each team is normalized to
|
||||
// exactly one runner (the console's rule).
|
||||
//
|
||||
if (football)
|
||||
{
|
||||
//
|
||||
// 1. Teams: the host's pick, then each member's, then
|
||||
// alternate whoever did not choose across the two
|
||||
// most-used teams so the sides stay balanced.
|
||||
//
|
||||
owner->team = fe->selection[GroupTeam];
|
||||
for (int p = 1; p < pilot_count; ++p)
|
||||
{
|
||||
for (int t = 0; t < FE_COUNT(kTeams); ++t)
|
||||
{
|
||||
if (strcmp(extras[p - 1].team, kTeams[t].key) == 0)
|
||||
{
|
||||
pilots[p].team = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int fallback_team = (owner->team + 1) % FE_COUNT(kTeams);
|
||||
int unassigned = 0;
|
||||
for (int p = 1; p < pilot_count; ++p)
|
||||
{
|
||||
if (pilots[p].team < 0)
|
||||
{
|
||||
pilots[p].team = (unassigned++ % 2) ? owner->team : fallback_team;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 2. Positions: honor each pick, default the rest to the
|
||||
// line, then give every team exactly one runner.
|
||||
//
|
||||
for (int p = 1; p < pilot_count; ++p)
|
||||
{
|
||||
const char *pick = extras[p - 1].position;
|
||||
for (int i = 0; i < FE_COUNT(kPositions); ++i)
|
||||
{
|
||||
if (strcmp(pick, kPositions[i].key) == 0)
|
||||
{
|
||||
pilots[p].position = kPositions[i].key;
|
||||
pilots[p].chosePosition = True;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
owner->position = kPositions[fe->selection[GroupPosition]].key;
|
||||
owner->chosePosition = True;
|
||||
|
||||
for (int t = 0; t < FE_COUNT(kTeams); ++t)
|
||||
{
|
||||
int members = 0, runner = -1, spare = -1, line = 0;
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
if (pilots[p].team != t)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
++members;
|
||||
if (spare < 0 && !pilots[p].chosePosition)
|
||||
{
|
||||
spare = p; // nobody's plans to disturb
|
||||
}
|
||||
if (pilots[p].position != NULL &&
|
||||
strcmp(pilots[p].position, "runner") == 0)
|
||||
{
|
||||
if (runner < 0)
|
||||
{
|
||||
runner = p; // first runner keeps the ball
|
||||
}
|
||||
else
|
||||
{
|
||||
// two pilots claimed it - the later one lines up
|
||||
pilots[p].position = NULL;
|
||||
pilots[p].chosePosition = False;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (members == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (runner < 0 && spare >= 0)
|
||||
{
|
||||
// no volunteer: the first pilot without a pick of
|
||||
// their own runs (explicit picks are never overridden)
|
||||
pilots[spare].position = "runner";
|
||||
runner = spare;
|
||||
}
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
if (pilots[p].team == t && p != runner &&
|
||||
pilots[p].position == NULL)
|
||||
{
|
||||
pilots[p].position = (line++ % 2) ? "crusher" : "blocker";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 3. Colors follow team and position (runner wears the
|
||||
// team's runner color - that is how you spot the ball)
|
||||
//
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
const TeamEntry *team = &kTeams[pilots[p].team];
|
||||
pilots[p].color =
|
||||
(strcmp(pilots[p].position, "runner") == 0)
|
||||
? team->runnerColor : team->teamColor;
|
||||
pilots[p].badge = "None";
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// [mission]
|
||||
//
|
||||
int map_count;
|
||||
const CatalogEntry *maps = ActiveMaps(fe->selection, &map_count);
|
||||
|
||||
egg += "[mission]\n";
|
||||
egg += "adventure=Red Planet\n";
|
||||
sprintf(line, "map=%s\n", kMaps[fe->selection[GroupMap]].key);
|
||||
sprintf(line, "map=%s\n", maps[fe->selection[GroupMap]].key);
|
||||
egg += line;
|
||||
sprintf(line, "scenario=%s\n", kScenarios[fe->selection[GroupScenario]].key);
|
||||
egg += line;
|
||||
egg += "scenario=race\n";
|
||||
sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
|
||||
egg += line;
|
||||
sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
|
||||
@@ -690,84 +973,120 @@ namespace
|
||||
egg += line;
|
||||
}
|
||||
|
||||
//
|
||||
// [pilots] + per-pilot sections
|
||||
//
|
||||
egg += "[pilots]\n";
|
||||
sprintf(line, "pilot=%s\n", owner_address);
|
||||
egg += line;
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
sprintf(line, "pilot=%s\n", extras[p].address);
|
||||
sprintf(line, "pilot=%s\n", pilots[p].address);
|
||||
egg += line;
|
||||
}
|
||||
|
||||
sprintf(line, "[%s]\n", owner_address);
|
||||
egg += line;
|
||||
egg += "hostType=0\n";
|
||||
egg += "dropzone=one\n";
|
||||
sprintf(line, "name=%s\n", fe->pilotName);
|
||||
egg += line;
|
||||
egg += "bitmapindex=1\n";
|
||||
egg += "loadzones=1\n";
|
||||
sprintf(line, "vehicle=%s\n", kVehicles[fe->selection[GroupVehicle]].key);
|
||||
egg += line;
|
||||
sprintf(line, "color=%s\n", kColors[fe->selection[GroupColor]].key);
|
||||
egg += line;
|
||||
sprintf(line, "badge=%s\n", kBadges[fe->selection[GroupBadge]].key);
|
||||
egg += line;
|
||||
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
sprintf(line, "[%s]\n", extras[p].address);
|
||||
sprintf(line, "[%s]\n", pilots[p].address);
|
||||
egg += line;
|
||||
egg += "hostType=0\n";
|
||||
egg += "dropzone=one\n";
|
||||
sprintf(line, "name=%s\n", extras[p].name);
|
||||
sprintf(line, "name=%s\n", pilots[p].name);
|
||||
egg += line;
|
||||
sprintf(line, "bitmapindex=%d\n", p + 2);
|
||||
sprintf(line, "bitmapindex=%d\n", p + 1);
|
||||
egg += line;
|
||||
egg += "loadzones=1\n";
|
||||
sprintf(line, "vehicle=%s\n", extras[p].vehicle);
|
||||
sprintf(line, "vehicle=%s\n", pilots[p].vehicle);
|
||||
egg += line;
|
||||
sprintf(line, "color=%s\n", extras[p].color);
|
||||
sprintf(line, "color=%s\n", pilots[p].color);
|
||||
egg += line;
|
||||
sprintf(line, "badge=%s\n", extras[p].badge);
|
||||
sprintf(line, "badge=%s\n", pilots[p].badge);
|
||||
egg += line;
|
||||
if (football)
|
||||
{
|
||||
sprintf(line, "team=team::%s\n", kTeams[pilots[p].team].key);
|
||||
egg += line;
|
||||
sprintf(line, "position=%s\n", pilots[p].position);
|
||||
egg += line;
|
||||
}
|
||||
}
|
||||
|
||||
egg += "[largebitmap]\n";
|
||||
sprintf(line, "bitmap=BitMap::Large::%s\n", fe->pilotName);
|
||||
egg += line;
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
//
|
||||
// Football team blocks (console RPFootballMission layout)
|
||||
//
|
||||
if (football)
|
||||
{
|
||||
sprintf(line, "bitmap=BitMap::Large::%s\n", extras[p].name);
|
||||
std::string teams_list = "[teams]\n";
|
||||
std::string team_sections, pilots_sections;
|
||||
std::string teambitmap_list = "[teambitmap]\n";
|
||||
std::string teambitmap_sections;
|
||||
int bitmap_index = 1;
|
||||
for (int side = 0; side < FE_COUNT(kTeams); ++side)
|
||||
{
|
||||
Logical team_present = False;
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
if (pilots[p].team == side)
|
||||
{
|
||||
team_present = True;
|
||||
}
|
||||
}
|
||||
if (!team_present)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const char *team_key = kTeams[side].key;
|
||||
sprintf(line, "team=team::%s\n", team_key);
|
||||
teams_list += line;
|
||||
sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
|
||||
team_key, bitmap_index++, team_key);
|
||||
team_sections += line;
|
||||
sprintf(line, "[pilots::%s]\n", team_key);
|
||||
pilots_sections += line;
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
if (pilots[p].team == side)
|
||||
{
|
||||
sprintf(line, "pilot=%s\n", pilots[p].address);
|
||||
pilots_sections += line;
|
||||
}
|
||||
}
|
||||
sprintf(line, "bitmap=BitMap::Small::team::%s\n", team_key);
|
||||
teambitmap_list += line;
|
||||
sprintf(line, "[BitMap::Small::team::%s]\n", team_key);
|
||||
teambitmap_sections += line;
|
||||
AppendNameBitmap(teambitmap_sections, 64, 16, team_key);
|
||||
teambitmap_sections += "width=4\n";
|
||||
}
|
||||
egg += teams_list;
|
||||
egg += team_sections;
|
||||
egg += pilots_sections;
|
||||
egg += teambitmap_list;
|
||||
egg += teambitmap_sections;
|
||||
}
|
||||
|
||||
//
|
||||
// Name bitmaps + ordinals
|
||||
//
|
||||
egg += "[largebitmap]\n";
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
sprintf(line, "bitmap=BitMap::Large::%s\n", pilots[p].name);
|
||||
egg += line;
|
||||
}
|
||||
egg += "[smallbitmap]\n";
|
||||
sprintf(line, "bitmap=BitMap::Small::%s\n", fe->pilotName);
|
||||
egg += line;
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
sprintf(line, "bitmap=BitMap::Small::%s\n", extras[p].name);
|
||||
sprintf(line, "bitmap=BitMap::Small::%s\n", pilots[p].name);
|
||||
egg += line;
|
||||
}
|
||||
|
||||
sprintf(line, "[BitMap::Large::%s]\n", fe->pilotName);
|
||||
egg += line;
|
||||
AppendNameBitmap(egg, 128, 32, fe->pilotName);
|
||||
egg += "width=8\n";
|
||||
sprintf(line, "[BitMap::Small::%s]\n", fe->pilotName);
|
||||
egg += line;
|
||||
AppendNameBitmap(egg, 64, 16, fe->pilotName);
|
||||
egg += "width=4\n";
|
||||
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
for (int p = 0; p < pilot_count; ++p)
|
||||
{
|
||||
sprintf(line, "[BitMap::Large::%s]\n", extras[p].name);
|
||||
sprintf(line, "[BitMap::Large::%s]\n", pilots[p].name);
|
||||
egg += line;
|
||||
AppendNameBitmap(egg, 128, 32, extras[p].name);
|
||||
AppendNameBitmap(egg, 128, 32, pilots[p].name);
|
||||
egg += "width=8\n";
|
||||
sprintf(line, "[BitMap::Small::%s]\n", extras[p].name);
|
||||
sprintf(line, "[BitMap::Small::%s]\n", pilots[p].name);
|
||||
egg += line;
|
||||
AppendNameBitmap(egg, 64, 16, extras[p].name);
|
||||
AppendNameBitmap(egg, 64, 16, pilots[p].name);
|
||||
egg += "width=4\n";
|
||||
}
|
||||
|
||||
@@ -775,13 +1094,13 @@ namespace
|
||||
|
||||
// [pilots]-order names for the network console's results
|
||||
strcpy(gLastPilotNamesCsv, fe->pilotName);
|
||||
for (int p = 0; p < extra_count; ++p)
|
||||
for (int p = 1; p < pilot_count; ++p)
|
||||
{
|
||||
if (strlen(gLastPilotNamesCsv) + strlen(extras[p].name) + 2 <
|
||||
if (strlen(gLastPilotNamesCsv) + strlen(pilots[p].name) + 2 <
|
||||
sizeof(gLastPilotNamesCsv))
|
||||
{
|
||||
strcat(gLastPilotNamesCsv, ",");
|
||||
strcat(gLastPilotNamesCsv, extras[p].name);
|
||||
strcat(gLastPilotNamesCsv, pilots[p].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1061,6 +1380,102 @@ void
|
||||
strcpy(badge, kBadges[b].key);
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
// Football team / position picks (the lobby publishes and cycles these)
|
||||
//########################################################################
|
||||
|
||||
int
|
||||
RPL4FrontEnd_TeamCount()
|
||||
{
|
||||
return FE_COUNT(kTeams);
|
||||
}
|
||||
|
||||
const char *
|
||||
RPL4FrontEnd_TeamName(int index)
|
||||
{
|
||||
if (index < 0 || index >= FE_COUNT(kTeams))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return kTeams[index].name;
|
||||
}
|
||||
|
||||
const char *
|
||||
RPL4FrontEnd_TeamKey(int index)
|
||||
{
|
||||
if (index < 0 || index >= FE_COUNT(kTeams))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return kTeams[index].key;
|
||||
}
|
||||
|
||||
int
|
||||
RPL4FrontEnd_PositionCount()
|
||||
{
|
||||
return FE_COUNT(kPositions);
|
||||
}
|
||||
|
||||
const char *
|
||||
RPL4FrontEnd_PositionName(int index)
|
||||
{
|
||||
if (index < 0 || index >= FE_COUNT(kPositions))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return kPositions[index].name;
|
||||
}
|
||||
|
||||
const char *
|
||||
RPL4FrontEnd_PositionKey(int index)
|
||||
{
|
||||
if (index < 0 || index >= FE_COUNT(kPositions))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return kPositions[index].key;
|
||||
}
|
||||
|
||||
int
|
||||
RPL4FrontEnd_GetTeamIndex()
|
||||
{
|
||||
return gHavePersist ? gPersistSelection[GroupTeam] : 0;
|
||||
}
|
||||
|
||||
void
|
||||
RPL4FrontEnd_SetTeamIndex(int index)
|
||||
{
|
||||
if (index < 0 || index >= FE_COUNT(kTeams))
|
||||
{
|
||||
return;
|
||||
}
|
||||
gPersistSelection[GroupTeam] = index;
|
||||
gHavePersist = True;
|
||||
}
|
||||
|
||||
int
|
||||
RPL4FrontEnd_GetPositionIndex()
|
||||
{
|
||||
return gHavePersist ? gPersistSelection[GroupPosition] : 0;
|
||||
}
|
||||
|
||||
void
|
||||
RPL4FrontEnd_SetPositionIndex(int index)
|
||||
{
|
||||
if (index < 0 || index >= FE_COUNT(kPositions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
gPersistSelection[GroupPosition] = index;
|
||||
gHavePersist = True;
|
||||
}
|
||||
|
||||
Logical
|
||||
RPL4FrontEnd_IsFootballSelected()
|
||||
{
|
||||
return gHavePersist && IsFootball(gPersistSelection);
|
||||
}
|
||||
|
||||
void
|
||||
RPL4FrontEnd_SetHostedPilots(
|
||||
const char *owner_address,
|
||||
|
||||
@@ -50,6 +50,39 @@ int
|
||||
void
|
||||
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Football team / position: the catalogs, and this player's picks.
|
||||
// The lobby publishes the picks as member data and lets members
|
||||
// cycle them in the room; the host's egg builder honors them.
|
||||
//---------------------------------------------------------------
|
||||
int
|
||||
RPL4FrontEnd_TeamCount();
|
||||
const char *
|
||||
RPL4FrontEnd_TeamName(int index);
|
||||
const char *
|
||||
RPL4FrontEnd_TeamKey(int index);
|
||||
|
||||
int
|
||||
RPL4FrontEnd_PositionCount();
|
||||
const char *
|
||||
RPL4FrontEnd_PositionName(int index);
|
||||
const char *
|
||||
RPL4FrontEnd_PositionKey(int index);
|
||||
|
||||
int
|
||||
RPL4FrontEnd_GetTeamIndex();
|
||||
void
|
||||
RPL4FrontEnd_SetTeamIndex(int index);
|
||||
int
|
||||
RPL4FrontEnd_GetPositionIndex();
|
||||
void
|
||||
RPL4FrontEnd_SetPositionIndex(int index);
|
||||
|
||||
// True when this player's setup menu has Football selected (the host's
|
||||
// pick decides the mission; members use it to know what to show).
|
||||
Logical
|
||||
RPL4FrontEnd_IsFootballSelected();
|
||||
|
||||
// Hosted-race pilots fed by the Steam lobby: overrides the
|
||||
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
||||
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
||||
@@ -60,6 +93,9 @@ struct FEHostedPilot
|
||||
char vehicle[24];
|
||||
char color[16];
|
||||
char badge[24];
|
||||
// football: the member's own picks, empty = assign one for them
|
||||
char team[32]; // team key ("Red/Pink", ...)
|
||||
char position[16]; // "runner" / "crusher" / "blocker"
|
||||
};
|
||||
void
|
||||
RPL4FrontEnd_SetHostedPilots(
|
||||
|
||||
+117
-7
@@ -45,6 +45,7 @@ namespace
|
||||
int gLastGoNonce = 0; // launches we already answered
|
||||
int gShownResultsNonce = 0; // score sheets we already displayed
|
||||
const char kResultsKey[] = "res";
|
||||
const char kScenarioKey[] = "sc";
|
||||
|
||||
// async call-result plumbing
|
||||
Logical gCallDone = False;
|
||||
@@ -120,6 +121,12 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
Logical IsOwner()
|
||||
{
|
||||
return gInLobby &&
|
||||
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
|
||||
}
|
||||
|
||||
void PublishMemberData()
|
||||
{
|
||||
char value[64];
|
||||
@@ -140,12 +147,30 @@ namespace
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle);
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color);
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge);
|
||||
|
||||
// football: this member's own team and position pick
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "tm",
|
||||
RPL4FrontEnd_TeamKey(RPL4FrontEnd_GetTeamIndex()));
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
|
||||
RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));
|
||||
|
||||
// the owner also publishes the scenario, so members know
|
||||
// whether their team/position pick matters
|
||||
if (IsOwner())
|
||||
{
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
||||
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
||||
}
|
||||
}
|
||||
|
||||
Logical IsOwner()
|
||||
Logical FootballLobby()
|
||||
{
|
||||
return gInLobby &&
|
||||
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
|
||||
if (!gInLobby)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
const char *scenario = SteamMatchmaking()->GetLobbyData(gLobby, kScenarioKey);
|
||||
return (scenario != NULL && strcmp(scenario, "football") == 0);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
@@ -162,6 +187,8 @@ namespace
|
||||
char vehicle[24];
|
||||
char color[16];
|
||||
char badge[24];
|
||||
char team[32]; // football pick
|
||||
char position[16];
|
||||
Logical published;
|
||||
};
|
||||
|
||||
@@ -196,6 +223,12 @@ namespace
|
||||
strncpy(member->badge,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"),
|
||||
sizeof(member->badge) - 1);
|
||||
strncpy(member->team,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "tm"),
|
||||
sizeof(member->team) - 1);
|
||||
strncpy(member->position,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
|
||||
sizeof(member->position) - 1);
|
||||
member->published =
|
||||
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
||||
}
|
||||
@@ -241,6 +274,8 @@ namespace
|
||||
strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1);
|
||||
strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1);
|
||||
strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 1);
|
||||
strncpy(pilot->team, members[i].team, sizeof(pilot->team) - 1);
|
||||
strncpy(pilot->position, members[i].position, sizeof(pilot->position) - 1);
|
||||
|
||||
if (pods[0] != '\0')
|
||||
{
|
||||
@@ -274,8 +309,11 @@ namespace
|
||||
HFONT titleFont;
|
||||
RECT launchRect; // owner only
|
||||
RECT leaveRect;
|
||||
RECT teamRect; // football: cycle my team
|
||||
RECT positionRect; // football: cycle my position
|
||||
Logical launchClicked;
|
||||
Logical leaveClicked;
|
||||
Logical pickChanged;
|
||||
Logical closed;
|
||||
MemberInfo members[kMaxLobbyMembers];
|
||||
int memberCount;
|
||||
@@ -303,8 +341,9 @@ namespace
|
||||
RECT title = client;
|
||||
title.top = client.bottom / 28;
|
||||
title.bottom = title.top + client.bottom / 10;
|
||||
DrawTextA(mem, "STEAM RACE LOBBY", -1, &title,
|
||||
DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||
DrawTextA(mem,
|
||||
FootballLobby() ? "STEAM FOOTBALL LOBBY" : "STEAM RACE LOBBY",
|
||||
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||
|
||||
SelectObject(mem, room->textFont);
|
||||
int row_h = client.bottom / 16;
|
||||
@@ -328,7 +367,25 @@ namespace
|
||||
is_owner_row ? " [HOST]" : "");
|
||||
DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
if (member->vehicle[0] != '\0')
|
||||
if (FootballLobby())
|
||||
{
|
||||
// football: the roster is a team sheet
|
||||
if (member->team[0] != '\0')
|
||||
{
|
||||
const char *position_name = "?";
|
||||
for (int i = 0; i < RPL4FrontEnd_PositionCount(); ++i)
|
||||
{
|
||||
if (strcmp(member->position, RPL4FrontEnd_PositionKey(i)) == 0)
|
||||
{
|
||||
position_name = RPL4FrontEnd_PositionName(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
sprintf(text, "%s - %s", member->team, position_name);
|
||||
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
}
|
||||
else if (member->vehicle[0] != '\0')
|
||||
{
|
||||
sprintf(text, "%s / %s", member->vehicle, member->color);
|
||||
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||
@@ -347,11 +404,30 @@ namespace
|
||||
-1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
HBRUSH frame = CreateSolidBrush(kGreenBright);
|
||||
|
||||
//
|
||||
// Football: my own team and position, click to cycle
|
||||
//
|
||||
if (FootballLobby())
|
||||
{
|
||||
FrameRect(mem, &room->teamRect, frame);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
sprintf(text, "MY TEAM: %s",
|
||||
RPL4FrontEnd_TeamName(RPL4FrontEnd_GetTeamIndex()));
|
||||
DrawTextA(mem, text, -1, &room->teamRect,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
FrameRect(mem, &room->positionRect, frame);
|
||||
sprintf(text, "MY POSITION: %s",
|
||||
RPL4FrontEnd_PositionName(RPL4FrontEnd_GetPositionIndex()));
|
||||
DrawTextA(mem, text, -1, &room->positionRect,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
if (IsOwner())
|
||||
{
|
||||
FrameRect(mem, &room->launchRect, frame);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, "L A U N C H R A C E", -1, &room->launchRect,
|
||||
DrawTextA(mem, "L A U N C H G A M E", -1, &room->launchRect,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
FrameRect(mem, &room->leaveRect, frame);
|
||||
@@ -395,6 +471,18 @@ namespace
|
||||
{
|
||||
room->leaveClicked = True;
|
||||
}
|
||||
else if (FootballLobby() && PtInRect(&room->teamRect, point))
|
||||
{
|
||||
RPL4FrontEnd_SetTeamIndex(
|
||||
(RPL4FrontEnd_GetTeamIndex() + 1) % RPL4FrontEnd_TeamCount());
|
||||
room->pickChanged = True;
|
||||
}
|
||||
else if (FootballLobby() && PtInRect(&room->positionRect, point))
|
||||
{
|
||||
RPL4FrontEnd_SetPositionIndex(
|
||||
(RPL4FrontEnd_GetPositionIndex() + 1) % RPL4FrontEnd_PositionCount());
|
||||
room->pickChanged = True;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
@@ -465,6 +553,16 @@ namespace
|
||||
room.leaveRect.top = client.bottom - 3 * row_h;
|
||||
room.leaveRect.bottom = client.bottom - row_h;
|
||||
|
||||
// football pick buttons, above the launch button
|
||||
room.teamRect.left = (client.right - col_w) / 2;
|
||||
room.teamRect.right = room.teamRect.left + col_w;
|
||||
room.teamRect.top = client.bottom - 12 * row_h;
|
||||
room.teamRect.bottom = room.teamRect.top + 2 * row_h;
|
||||
room.positionRect.left = room.teamRect.left;
|
||||
room.positionRect.right = room.teamRect.right;
|
||||
room.positionRect.top = client.bottom - 9 * row_h;
|
||||
room.positionRect.bottom = room.positionRect.top + 2 * row_h;
|
||||
|
||||
gRoom = &room;
|
||||
PublishMemberData();
|
||||
|
||||
@@ -489,6 +587,18 @@ namespace
|
||||
outcome = LobbyRoomClosed;
|
||||
break;
|
||||
}
|
||||
//
|
||||
// A pick changed: republish so everyone's roster updates
|
||||
//
|
||||
if (room.pickChanged)
|
||||
{
|
||||
room.pickChanged = False;
|
||||
PublishMemberData();
|
||||
room.memberCount = CollectMembers(room.members);
|
||||
InvalidateRect(room.window, NULL, FALSE);
|
||||
RedrawWindow(room.window, NULL, NULL, RDW_UPDATENOW);
|
||||
}
|
||||
|
||||
if (room.leaveClicked)
|
||||
{
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# Red Planet 4.12 — controls map
|
||||
|
||||
Generated from the default `bindings.txt` (the file the game writes
|
||||
beside the exe on first run). Everything here is rebindable: edit that
|
||||
file and restart, or delete it to get these defaults back.
|
||||
|
||||
Grammar reminder, from the file's own header:
|
||||
|
||||
```
|
||||
key <name> button <addr> [toggle]
|
||||
key <name> axis <axis> deflect <n> # held, springs back
|
||||
key <name> axis <axis> rate <n> # walks per second, sticks
|
||||
pad <button> button <addr> [toggle]
|
||||
padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Xbox controller
|
||||
|
||||
```
|
||||
( LB ) Panic Throttle / reverse ( RB )
|
||||
( LT ) Left pedal Right pedal ( RT )
|
||||
______________________________________________________
|
||||
/ \
|
||||
/ [Back] [Start] \
|
||||
| unbound unbound |
|
||||
| |
|
||||
| ,--. (Y) |
|
||||
| ( L ) stick X / Y Upper |
|
||||
| `--' (X) (B) |
|
||||
| +---+ Pinky Middle |
|
||||
| D-pad --> |hat| (A) |
|
||||
| +---+ Main |
|
||||
| |
|
||||
| ,--. |
|
||||
| ( R ) throttle (Y axis) |
|
||||
\ `--' /
|
||||
\____________________________________________________ _/
|
||||
```
|
||||
|
||||
| Control | Does | RIO address |
|
||||
|---|---|---|
|
||||
| Left stick | Joystick X / Y (inverted, deadzone 0.24 — pod convention) | axes |
|
||||
| Right stick Y | Throttle, as a **rate**: hold to wind it up or down, position sticks | axis |
|
||||
| Left / right trigger | Left / right pedal (deadzone 0.12) | axes |
|
||||
| **A** | Main trigger | `0x40` |
|
||||
| **B** | Middle | `0x46` |
|
||||
| **X** | Pinky | `0x45` |
|
||||
| **Y** | Upper | `0x47` |
|
||||
| **LB** | Panic | `0x3D` |
|
||||
| **RB** | Throttle button — reverse thrust | `0x3F` |
|
||||
| D-pad | Hat: up / back / left / right (look) | `0x42` `0x41` `0x44` `0x43` |
|
||||
| **Start**, **Back** | unbound — yours to map | — |
|
||||
|
||||
The config buttons (`0x37`/`0x36`) are the first two buttons of the
|
||||
Upper Right MFD's top row, reachable from the keyboard on `9` and `0`.
|
||||
Start and Back ship unbound so you can put anything there; the
|
||||
commented lines in `bindings.txt` show how.
|
||||
|
||||
---
|
||||
|
||||
## Keyboard
|
||||
|
||||
### Flight cluster — the number pad
|
||||
|
||||
```
|
||||
+-----+-----+-----+
|
||||
| 7 | 8 | 9 | 7 / 9 left / right pedal
|
||||
| Lped| fwd |Rped | 8 / 2 stick forward / back
|
||||
+-----+-----+-----+ 4 / 6 stick left / right
|
||||
| 4 | 5 | 6 | 0 main trigger
|
||||
| left| |right|
|
||||
+-----+-----+-----+
|
||||
| 1 | 2 | 3 |
|
||||
| | back| |
|
||||
+-----+-----+-----+
|
||||
| 0 | . |
|
||||
| trigger | |
|
||||
+-----------+-----+
|
||||
|
||||
Shift throttle up Ctrl throttle down Alt reverse thrust
|
||||
(both throttle keys walk the axis and it holds where you leave it)
|
||||
```
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| NumPad 8 / 2 | Stick forward / back |
|
||||
| NumPad 4 / 6 | Stick left / right |
|
||||
| NumPad 7 / 9 | Left / right pedal |
|
||||
| NumPad 0, Space | Main trigger (`0x40`) |
|
||||
| Shift / Ctrl | Throttle up / down (position holds) |
|
||||
| Alt | Reverse thrust (`0x3F`) |
|
||||
| Arrow keys | Hat — look up / back / left / right |
|
||||
| **Alt + Q** | Abort the mission (score banked, back to menu or lobby) |
|
||||
|
||||
### Panel buttons — the letter and number rows
|
||||
|
||||
The rest of the keyboard is the pod's button board, laid out as it is
|
||||
printed on the panel. Each MFD has 8 buttons (a 4-wide top row and a
|
||||
4-wide bottom row); the keyboard rows run left to right across the
|
||||
three upper MFDs, then the two lower ones.
|
||||
|
||||
```
|
||||
UPPER MFDs Left MFD Middle MFD Right MFD
|
||||
top row 1 2 3 4 | 5 6 7 8 | 9 0 - =
|
||||
bottom row Q W E R | T Y U I | O P [ ]
|
||||
0x2F..0x2C 0x27..0x24 0x37..0x34
|
||||
0x2B..0x28 0x23..0x20 0x33..0x30
|
||||
|
||||
LOWER MFDs Left MFD Right MFD
|
||||
top row A S D F (G unbound) H J K L
|
||||
bottom row Z X C V (B unbound) N M , .
|
||||
0x0F..0x0C 0x07..0x04
|
||||
0x0B..0x08 0x03..0x00
|
||||
|
||||
COLUMNS beside the map
|
||||
Secondary F1 F2 F3 F4 F5 F6 0x10..0x15
|
||||
Screen F7 F8 F9 F10 F11 F12 0x18..0x1D
|
||||
```
|
||||
|
||||
G and B are deliberately unbound: they are the physical gap between the
|
||||
Lower Left and Lower Right MFD clusters on the real board.
|
||||
|
||||
Every one of these buttons is also **clickable on screen** — the red
|
||||
strips around each MFD and the amber strips beside the map are the same
|
||||
addresses, and they light up when the game commands their lamps.
|
||||
|
||||
### Not bound by default
|
||||
|
||||
The pilot keypad (`0x50`–`0x5F`) and external operator keypad
|
||||
(`0x60`–`0x6F`) have no keys assigned — the game never reads them. They
|
||||
remain bindable if you want them (they arrive as arcade RIO key events).
|
||||
|
||||
---
|
||||
|
||||
## RGB keyboard lighting
|
||||
|
||||
If the machine has a Dynamic Lighting keyboard, bound panel keys mirror
|
||||
the pod's lamps: red for the MFD banks, amber/yellow for the Secondary
|
||||
and Screen columns, blinking in step with the on-screen buttons.
|
||||
Unbound keys go dark. Set `RP412KEYLIGHT=0` in `environ.ini` to switch
|
||||
it off.
|
||||
|
||||
---
|
||||
|
||||
## Rebinding
|
||||
|
||||
`bindings.txt` sits beside the exe, written with the full default layout
|
||||
and its own documentation on first run. Change a line, restart the game.
|
||||
Delete the file to restore these defaults — worth doing after an update,
|
||||
since an existing file is never overwritten.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
key J button 0x40 # main trigger on J as well
|
||||
pad X button 0x40 # move the trigger to the X button
|
||||
key NumPadAdd axis Throttle rate 0.75 # throttle on numpad +/-
|
||||
key NumPadSubtract axis Throttle rate -0.75
|
||||
padaxis LeftStickX axis JoystickX deadzone 0.15 # drop 'invert' to flip X
|
||||
```
|
||||
|
||||
Addresses: `0x00`–`0x47` are the lamp buttons above, `0x50`–`0x6F` the
|
||||
keypads. Axis names: `Throttle`, `LeftPedal`, `RightPedal`, `JoystickX`,
|
||||
`JoystickY`. Key names follow the .NET `Keys` naming (`A`–`Z`, `D0`–`D9`
|
||||
for the digit row, `F1`–`F12`, `NumPad0`–`NumPad9`, `Up`, `Down`,
|
||||
`Left`, `Right`, `Space`, `Return`, `Shift`, `Ctrl`, `Alt`,
|
||||
`OemMinus`, `Oemplus`, `Oemcomma`, `OemPeriod`, `OemOpenBrackets`,
|
||||
`OemCloseBrackets`, …).
|
||||
@@ -155,12 +155,30 @@ launch flow, no in-game rejoin). Useful as a dev tool, not the product.
|
||||
**C. Hybrid:** in-engine UI, but mission/egg logic in a small shared C++
|
||||
library so a dev console tool and the game share one egg builder.
|
||||
|
||||
### Open decisions
|
||||
### Open decisions — all resolved
|
||||
|
||||
1. Front end location — Option A (in-engine, cockpit-rendered) confirmed?
|
||||
2. v1 scenario scope — Death Race only, or Football (needs teams UI) too?
|
||||
3. Results screen — post-race on the cockpit displays (replaces printout)?
|
||||
4. Steam model — lobby-owner-as-console confirmed? (Implies owner migration
|
||||
handling later; the egg re-issue path makes host migration plausible.)
|
||||
5. Pilot identity — Steam persona as pilot name; vehicle/color/badge
|
||||
persisted per player (Steam Cloud later)?
|
||||
1. Front end location — Option A (in-engine) **confirmed and shipped**.
|
||||
2. v1 scenario scope — **both scenarios shipped** (2026-07-24). The setup
|
||||
menu has a SCENARIO group; picking Football swaps the map list to the
|
||||
football-legal set (no `pain`/`headoff`, adds `headmf`), replaces the
|
||||
COLOR/BADGE columns with TEAM/POSITION, and the egg builder emits
|
||||
`scenario=football`, per-pilot `team=`/`position=`, and the
|
||||
`[teams]` / `[team::X]` / `[pilots::X]` / `[teambitmap]` blocks in
|
||||
RPFootballMission's layout. Colors are derived, not chosen: the
|
||||
runner wears the team's runner color, everyone else the team color.
|
||||
**Every player picks their own team and position** (2026-07-24):
|
||||
members publish their picks as lobby member data (`tm`/`ps`), the
|
||||
room shows the roster as a team sheet and gives each member MY TEAM
|
||||
/ MY POSITION buttons that cycle and republish live, and the owner
|
||||
publishes the scenario (`sc`) so members know when those picks
|
||||
matter. The egg builder honors every explicit pick and only fills
|
||||
gaps: unpicked pilots are spread across the sides, a team with no
|
||||
volunteer runner promotes its first *unpicked* member (explicit
|
||||
picks are never overridden), and a second pilot claiming runner on
|
||||
one team is moved to the line.
|
||||
3. Results screen — **shipped**, on the cockpit displays, with the owner
|
||||
publishing the score sheet to lobby members.
|
||||
4. Steam model — lobby-owner-as-console **confirmed and verified** on
|
||||
three machines. (Owner migration still unhandled.)
|
||||
5. Pilot identity — Steam persona as pilot name **shipped**; loadout
|
||||
persists in-session (Steam Cloud later).
|
||||
|
||||
@@ -9,13 +9,13 @@ the first live test.
|
||||
1. Steam client installed, logged in (different account per machine),
|
||||
and RUNNING.
|
||||
2. Copy the `dist\` folder onto the machine.
|
||||
3. In the dist folder, create `steam_appid.txt` containing exactly:
|
||||
`480`
|
||||
(Spacewar, Valve's public dev AppID — replaced by the real AppID
|
||||
once the store page exists.)
|
||||
4. Add to `environ.ini`:
|
||||
`RP412STEAM=1`
|
||||
5. Start with `start-windowed.bat` as usual.
|
||||
3. Start with `start-windowed.bat` as usual.
|
||||
|
||||
The packed dist already contains `steam_appid.txt` (480 — Spacewar,
|
||||
Valve's public dev AppID, replaced by the real one once the store page
|
||||
exists) and ships `RP412STEAM=1` in `environ.ini`. If a machine has an
|
||||
older dist, check both before blaming the network: a missing appid
|
||||
file makes SteamAPI_Init fail and the game quietly falls back to TCP.
|
||||
|
||||
Sanity check per machine: `rpl4.log` should show
|
||||
`SteamNetTransport: up as 169.254.x.y (fake ports N console, M game)`.
|
||||
|
||||
+184
-31
@@ -47,6 +47,13 @@ if (Test-Path $pdb) { Copy-Item $pdb $dist }
|
||||
# only activates with RP412STEAM=1; plain desktop runs never touch it.
|
||||
Copy-Item (Join-Path $root 'extern\steamworks_sdk_164\sdk\redistributable_bin\steam_api.dll') $dist
|
||||
|
||||
# steam_appid.txt: until RP412 has its own AppID, Steam testing runs
|
||||
# under Spacewar (480). Without this file SteamAPI_Init fails and the
|
||||
# game falls back to plain TCP - which is exactly the confusing symptom
|
||||
# testers hit when the file goes missing. Delete it once we ship under
|
||||
# our own AppID (the Steam client provides it then).
|
||||
Set-Content -Path "$dist\steam_appid.txt" -Encoding ascii -Value '480'
|
||||
|
||||
# --- game data -------------------------------------------------------------
|
||||
foreach ($dir in 'AUDIO', 'GAUGE', 'VIDEO') {
|
||||
Write-Host " copying $dir..."
|
||||
@@ -57,6 +64,18 @@ foreach ($file in 'RPDPL.INI', 'JOYSTICK.INI', 'RPL4.RES', 'TEST.EGG',
|
||||
Copy-Item (Join-Path $assets $file) $dist
|
||||
}
|
||||
|
||||
# The controls map travels with the game (players get the diagrams
|
||||
# without needing the repo). Flattened to ASCII so it reads correctly
|
||||
# in Notepad - the markdown source keeps its typography.
|
||||
$controls = Get-Content (Join-Path $root 'docs\CONTROLS.md') -Raw -Encoding UTF8
|
||||
foreach ($pair in @(
|
||||
@([char]0x2014, '-'), @([char]0x2013, '-'), @([char]0x2018, "'"),
|
||||
@([char]0x2019, "'"), @([char]0x201C, '"'), @([char]0x201D, '"'),
|
||||
@([char]0x00D7, 'x'), @([char]0x2192, '->'), @([char]0x2026, '...'))) {
|
||||
$controls = $controls.Replace([string]$pair[0], [string]$pair[1])
|
||||
}
|
||||
Set-Content -Path "$dist\CONTROLS.txt" -Encoding ascii -Value $controls
|
||||
|
||||
# --- OpenAL runtime --------------------------------------------------------
|
||||
# The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed
|
||||
# runtime beside the exe; oalinst.exe covers machines where that misses.
|
||||
@@ -85,27 +104,98 @@ Set-Content -Path "$dist\environ.ini" -Encoding ascii -Value @"
|
||||
|
||||
# ---- Core (the shipped configuration) --------------------------------------
|
||||
|
||||
# Control stack. PAD;KEYBOARD = the virtual RIO (XInput controller +
|
||||
# keyboard, rebindable via bindings.txt). RIO;KEYBOARD = real serial
|
||||
# cockpit hardware. KEYBOARD = engine keyboard only (no virtual RIO).
|
||||
# Control stack: tokens separated by ; or , processed left to right.
|
||||
# PAD the virtual RIO (XInput controller + keyboard,
|
||||
# rebindable via bindings.txt)
|
||||
# RIO real serial cockpit hardware on COM1
|
||||
# RIO:COMn same, on another port (RIO:COM3, ...)
|
||||
# KEYBOARD the engine keyboard handler
|
||||
# MOUSE, JOYSTICK, FLIGHTSTICKPRO, THRUSTMASTER, DIJOYSTICK
|
||||
# legacy pointer/joystick drivers (untested here)
|
||||
# Unset falls back to KEYBOARD alone.
|
||||
L4CONTROLS=PAD;KEYBOARD
|
||||
|
||||
# Renderer selection argument and its config file. Leave as shipped.
|
||||
# Renderer bring-up argument. Only its presence is checked (the DPL
|
||||
# resolution parsing it once fed is gone) and the game refuses to start
|
||||
# without it - any non-empty value works. Leave as shipped.
|
||||
DPLARG=1
|
||||
|
||||
# DPL (renderer/scene) configuration file, searched beside the exe.
|
||||
# Any notation file name; RPDPL.INI is the one that ships.
|
||||
L4DPLCFG=RPDPL.INI
|
||||
|
||||
# Gauge (MFD/instrument) canvas: WIDTHxHEIGHTxDEPTH.
|
||||
# Gauge (MFD/instrument) canvas. Must name a page of GAUGE\L4GAUGE.INI:
|
||||
# 640x480x8 | 640x480x16 | 800x600x16
|
||||
# Unset disables the gauge renderer (and with it all MFDs).
|
||||
L4GAUGE=640x480x16
|
||||
|
||||
# Plasma display: SCREEN renders the pod's plasma glass in-window
|
||||
# (currently parked off-layout); hardware values drive real glass.
|
||||
# Plasma display.
|
||||
# SCREEN render the pod's plasma glass in-window (currently
|
||||
# parked off-layout)
|
||||
# COM1, COM2... drive real plasma glass on that serial port
|
||||
# (9600 baud, N81)
|
||||
# Unset = no plasma display.
|
||||
L4PLASMA=SCREEN
|
||||
|
||||
# 1 = the single-window cockpit: all seven displays composed on a
|
||||
# locked 1920x1080 canvas around the viewscreen.
|
||||
# 0 = classic separate gauge windows; 1 = the single-window glass
|
||||
# cockpit (all seven displays composed on a locked 1920x1080 canvas
|
||||
# around the viewscreen); 2 = exploded diagnostic view (each display
|
||||
# in its own native-resolution desktop window - MFDs 640x480, map
|
||||
# 480x640 - decoded exactly as the pod's VDB split them, no downscale).
|
||||
L4MFDSPLIT=1
|
||||
|
||||
# Simulation/render frame rate.
|
||||
# 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
|
||||
|
||||
# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client
|
||||
@@ -116,48 +206,70 @@ RP412STEAM=1
|
||||
# ---- Optional ---------------------------------------------------------------
|
||||
|
||||
# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to
|
||||
# lamp buttons glow with the panel, flash modes and all. Default on;
|
||||
# set 0 to disable.
|
||||
# lamp buttons glow with the panel, flash modes and all.
|
||||
# Unset or nonzero = on (the default); 0 = off.
|
||||
#RP412KEYLIGHT=0
|
||||
|
||||
# Invert the stick on top of whatever bindings.txt produces: X, Y, XY.
|
||||
# Invert the stick on top of whatever bindings.txt produces:
|
||||
# X = invert X only, Y = invert Y only, XY = both (case-insensitive).
|
||||
#L4PADFLIP=XY
|
||||
|
||||
# Anti-aliasing samples (0 = off).
|
||||
# Anti-aliasing sample count, passed straight to Direct3D 9:
|
||||
# 0 = off, else 2..16 as the GPU supports (1 selects the driver's
|
||||
# "nonmaskable" mode; unsupported counts fail device creation).
|
||||
#MULTISAMPLE=0
|
||||
|
||||
# Particle budget.
|
||||
# Particle budget, integer. Default 8192.
|
||||
#MAXPARTICLES=8192
|
||||
|
||||
# On-screen plasma glass pixel size and position (when re-homed).
|
||||
# On-screen plasma glass (L4PLASMA=SCREEN only). SCALE = integer pixel
|
||||
# size 1..16, default 4 (out-of-range values are ignored). POS = window
|
||||
# top-left as X,Y screen coordinates; unset = auto, parked below the
|
||||
# main window.
|
||||
#L4PLASMASCALE=4
|
||||
#L4PLASMAPOS=0,0
|
||||
|
||||
# Fixed random seed (repeatable runs).
|
||||
# Fixed random seed (repeatable runs): any unsigned integer.
|
||||
# Unset seeds from the clock.
|
||||
#RANDOM=12345
|
||||
|
||||
# ---- LAN play without Steam -------------------------------------------------
|
||||
# Host a race over plain TCP: list the member pods' console channels
|
||||
# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501).
|
||||
# HOSTADDR is this machine's LAN IP as members can reach it.
|
||||
# RP412HOSTPODS comma-separated IP[:port] list, one entry per member
|
||||
# pod; port defaults to 1501 per entry
|
||||
# RP412HOSTPORT this machine's console port, integer > 0
|
||||
# (default 1501)
|
||||
# RP412HOSTADDR this machine's LAN IP as members can reach it
|
||||
# (default 127.0.0.1)
|
||||
#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501
|
||||
#RP412HOSTPORT=1501
|
||||
#RP412HOSTADDR=192.168.1.10
|
||||
|
||||
# ---- Developer / testing ----------------------------------------------------
|
||||
|
||||
# 1 arms the debug keys: Alt+W wireframe, Alt+V predator vision,
|
||||
# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision,
|
||||
# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump.
|
||||
# (Alt+Q, the mission abort, is always live.)
|
||||
# 0 or unset = off. (Alt+Q, the mission abort, is always live.)
|
||||
#RP412DEVKEYS=1
|
||||
|
||||
# Console race-length override in seconds (short test races).
|
||||
# Console race-length override, integer seconds (short test races).
|
||||
# Values <= 0 are ignored.
|
||||
#L4CONSOLELEN=30
|
||||
|
||||
# Steam transport loopback self-test at boot (logs PASS/FAIL).
|
||||
# Nonzero = Steam transport loopback self-test at boot (logs PASS/FAIL).
|
||||
#RP412STEAMSELFTEST=1
|
||||
|
||||
# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ----------
|
||||
# PRIMGAUGE / SECGAUGE / MFDGAUGE / MFDGAUGE2 pin a display to a monitor
|
||||
# by adapter index (0, 1, 2...). SPANDISABLE: 0 = let the MFDs span one
|
||||
# wide surface, nonzero = separate windows (setting MFDGAUGE2 alone also
|
||||
# forces spanning off). L4EYES = "x y z xrot yrot zrot [type]" floats
|
||||
# for a detached camera; a type starting with r offsets it relative to
|
||||
# the pod. L4INTERCOM enables the crew intercom - only its presence
|
||||
# matters (traditionally COM2). NOMODES skips the mode/lamp programming;
|
||||
# presence alone triggers it, even NOMODES=0. LOGSIZE > 0 sizes the
|
||||
# trace log in dev builds compiled with tracing.
|
||||
#PRIMGAUGE=1
|
||||
#SECGAUGE=2
|
||||
#MFDGAUGE=3
|
||||
@@ -171,19 +283,34 @@ RP412STEAM=1
|
||||
|
||||
Set-Content -Path "$dist\start-windowed.bat" -Encoding ascii -Value @"
|
||||
@echo off
|
||||
rem Red Planet 4.12 - desktop prototype. Boots into the race-setup
|
||||
rem front end; the cockpit canvas is 1920x1080 and -res matches it.
|
||||
rem Red Planet 4.12 - desktop prototype. Boots into the game-setup
|
||||
rem front end. The cockpit fits itself to the window and keeps 16:9,
|
||||
rem so a wider screen letterboxes rather than stretching. -res sets the
|
||||
rem 3D render size - raise it to match a big screen for sharper pixels,
|
||||
rem e.g. -res 2560 1440.
|
||||
rem (Add -egg TEST.EGG to skip the menu and run the canned mission.)
|
||||
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
|
||||
Red Planet 4.12.3
|
||||
=================
|
||||
|
||||
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
|
||||
@@ -196,11 +323,27 @@ documented default layout on first run; delete it to restore).
|
||||
A / Space / NumPad 0 joystick trigger
|
||||
RB / Alt reverse thrust
|
||||
DPad / arrow keys joystick hat (look)
|
||||
Start,Back / 9,0 config buttons
|
||||
9 / 0 keys config buttons (pad Start/Back left free)
|
||||
Number+letter rows MFD bank buttons (as printed on the panel)
|
||||
F1-F12 secondary / screen columns
|
||||
Alt+Q abort the mission (score banked)
|
||||
|
||||
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 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
|
||||
at its left, and the lower MFDs flanking the portrait map. The red
|
||||
@@ -209,7 +352,8 @@ pod's real button banks: click them with the mouse, and they light up
|
||||
as the game commands their lamps.
|
||||
environ.ini is self-documenting: every option ships in the file with
|
||||
a comment (Steam networking, keyboard lighting, stick inversion, LAN
|
||||
hosting, developer keys, and more).
|
||||
hosting, developer keys, and more). CONTROLS.txt has the full controls
|
||||
map with pad and keyboard diagrams.
|
||||
|
||||
Known prototype notes: pods race untextured (the player1-8 skins come
|
||||
from the presets system, not shipped data), and text drawn on the plasma
|
||||
@@ -223,7 +367,16 @@ $size = (Get-ChildItem $dist -Recurse | Measure-Object Length -Sum).Sum
|
||||
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
|
||||
|
||||
if ($Zip) {
|
||||
$zipPath = Join-Path $root 'RedPlanet-4.12.2.zip'
|
||||
$zipPath = Join-Path $root 'RedPlanet-4.12.3.zip'
|
||||
Write-Host "zipping to $zipPath..."
|
||||
Compress-Archive -Path "$dist\*" -DestinationPath $zipPath -Force
|
||||
|
||||
# Everything lives under a single RP412\ folder inside the zip, so
|
||||
# unpacking anywhere gives one self-contained game directory instead
|
||||
# of scattering files into the extraction folder.
|
||||
$stage = Join-Path ([System.IO.Path]::GetTempPath()) 'rp412-zipstage'
|
||||
if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
|
||||
New-Item -ItemType Directory -Force "$stage\RP412" | Out-Null
|
||||
Copy-Item "$dist\*" "$stage\RP412" -Recurse -Force
|
||||
Compress-Archive -Path "$stage\RP412" -DestinationPath $zipPath -Force
|
||||
Remove-Item -Recurse -Force $stage
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user