Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52da65bc0d | ||
|
|
91420b5cb2 | ||
|
|
fd61316a40 | ||
|
|
dce8818273 | ||
|
|
a1f9c0e3c0 | ||
|
|
bfd5fa163e | ||
|
|
a52fec80a9 | ||
|
|
40b00ddde1 | ||
|
|
16ce4dfbea | ||
|
|
2a23ec0923 | ||
|
|
1efd5137d6 | ||
|
|
b97dcce3a2 | ||
|
|
f31c8401c7 | ||
|
|
e1a3ef7cf1 | ||
|
|
858fb7fb42 | ||
|
|
28df53aa31 | ||
|
|
461dcfb6b9 | ||
|
|
9389ec2003 | ||
|
|
3a24de03e6 | ||
|
|
e4025c2aec | ||
|
|
43cf086ea4 | ||
|
|
abd720dd6e | ||
|
|
0c696c9952 | ||
|
|
ea9491d2d5 | ||
|
|
8dc6605a07 | ||
|
|
6b43971d2d | ||
|
|
b6045f3c94 | ||
|
|
0bec0f9640 | ||
|
|
8fb4b72f7a | ||
|
|
00cb87907c | ||
|
|
53228686b4 | ||
|
|
1bd6dd83e2 | ||
|
|
1ba3bde36a | ||
|
|
4b25f89e6f | ||
|
|
3194d3f973 | ||
|
|
0b84b32486 |
@@ -40,6 +40,11 @@ ipch/
|
|||||||
*.log
|
*.log
|
||||||
rpl4.log
|
rpl4.log
|
||||||
|
|
||||||
|
# Where the game window and the exploded view's panes were last dragged
|
||||||
|
# to (RP412MFDLAYOUT). Per-machine by nature - it is screen coordinates
|
||||||
|
# on somebody's desk.
|
||||||
|
mfd_layout.cfg
|
||||||
|
|
||||||
# Build-output static libs that land in lib/ (the two committed dependency
|
# Build-output static libs that land in lib/ (the two committed dependency
|
||||||
# libs, OpenAL32.lib and libsndfile-1.lib, stay tracked).
|
# libs, OpenAL32.lib and libsndfile-1.lib, stay tracked).
|
||||||
/lib/Munga_L4.lib
|
/lib/Munga_L4.lib
|
||||||
@@ -67,3 +72,6 @@ assets/RP411/startrp-800x600.ps1
|
|||||||
# ...but never the spool runtime output or Windows thumbnail caches.
|
# ...but never the spool runtime output or Windows thumbnail caches.
|
||||||
assets/**/*.spl
|
assets/**/*.spl
|
||||||
assets/**/last.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 GetScreenWidth() { return mScreenWidth; }
|
||||||
static unsigned int GetScreenHeight() { return mScreenHeight; }
|
static unsigned int GetScreenHeight() { return mScreenHeight; }
|
||||||
static bool GetFullscreen() { return mFullscreen; }
|
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 Logical GetSeeSolids() { return seeSolids; }
|
||||||
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
|
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
|
||||||
// The front end's multiplayer path turns network mode on at launch
|
// The front end's multiplayer path turns network mode on at launch
|
||||||
@@ -93,10 +96,17 @@ public:
|
|||||||
static CString GetRIORecordingFileName() { return rioRecordingFileName; }
|
static CString GetRIORecordingFileName() { return rioRecordingFileName; }
|
||||||
|
|
||||||
protected:
|
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 HINSTANCE mhInstance;
|
||||||
static unsigned int mScreenWidth;
|
static unsigned int mScreenWidth;
|
||||||
static unsigned int mScreenHeight;
|
static unsigned int mScreenHeight;
|
||||||
static bool mFullscreen;
|
static bool mFullscreen;
|
||||||
|
static bool mFitDisplay;
|
||||||
|
static bool mResExplicit;
|
||||||
static Logical seeSolids;
|
static Logical seeSolids;
|
||||||
static CString eggNotationFileName;
|
static CString eggNotationFileName;
|
||||||
static CString spoolFileName;
|
static CString spoolFileName;
|
||||||
|
|||||||
+80
-1
@@ -94,6 +94,8 @@ HINSTANCE L4Application::mhInstance = NULL;
|
|||||||
unsigned int L4Application::mScreenWidth = 800;
|
unsigned int L4Application::mScreenWidth = 800;
|
||||||
unsigned int L4Application::mScreenHeight = 600;
|
unsigned int L4Application::mScreenHeight = 600;
|
||||||
bool L4Application::mFullscreen = true;
|
bool L4Application::mFullscreen = true;
|
||||||
|
bool L4Application::mFitDisplay = false;
|
||||||
|
bool L4Application::mResExplicit = false;
|
||||||
Logical L4Application::seeSolids = False;
|
Logical L4Application::seeSolids = False;
|
||||||
CString L4Application::eggNotationFileName;
|
CString L4Application::eggNotationFileName;
|
||||||
CString L4Application::spoolFileName;
|
CString L4Application::spoolFileName;
|
||||||
@@ -205,6 +207,7 @@ Logical
|
|||||||
{
|
{
|
||||||
mScreenWidth = atoi(W2A(argv[++(*arguement)]));
|
mScreenWidth = atoi(W2A(argv[++(*arguement)]));
|
||||||
mScreenHeight = atoi(W2A(argv[++(*arguement)]));
|
mScreenHeight = atoi(W2A(argv[++(*arguement)]));
|
||||||
|
mResExplicit = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -217,18 +220,88 @@ Logical
|
|||||||
mFullscreen = false;
|
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 (
|
else if (
|
||||||
!stricmp(W2A(argv[*arguement]), "-h") || !stricmp(W2A(argv[*arguement]), "-help")
|
!stricmp(W2A(argv[*arguement]), "-h") || !stricmp(W2A(argv[*arguement]), "-help")
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "\n" << argv[0] <<
|
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 False;
|
||||||
}
|
}
|
||||||
|
|
||||||
return True;
|
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
|
// ParseCommandLine
|
||||||
@@ -270,6 +343,12 @@ Logical
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// after the whole line, so -res wins from either side of -fit
|
||||||
|
if (mFitDisplay && !mResExplicit)
|
||||||
|
{
|
||||||
|
ChooseFitResolution();
|
||||||
|
}
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+78
-1
@@ -13,6 +13,7 @@ L4TEXOP::WrapType d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
|
|||||||
bool d3d_OBJECT::mLastTexturingState = true;
|
bool d3d_OBJECT::mLastTexturingState = true;
|
||||||
long d3d_OBJECT::mNextID = 1;
|
long d3d_OBJECT::mNextID = 1;
|
||||||
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;
|
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;
|
||||||
|
LPDIRECT3DTEXTURE9 d3d_OBJECT::mNamePlateMarkers[d3d_OBJECT::kNamePlateCount + 1] = { NULL };
|
||||||
|
|
||||||
void chgext(char *filePath, const char *newExtension)
|
void chgext(char *filePath, const char *newExtension)
|
||||||
{
|
{
|
||||||
@@ -156,7 +157,36 @@ d3d_OBJECT *d3d_OBJECT::LoadObject(LPDIRECT3DDEVICE9 device, char *fileName)
|
|||||||
{
|
{
|
||||||
sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
|
sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
|
||||||
}
|
}
|
||||||
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
|
//
|
||||||
|
// playerN is not a file - it is a pilot's callsign, drawn into
|
||||||
|
// a texture at run time, so loading it always fails. Give the
|
||||||
|
// plate its marker texture instead: it keeps the eight sign
|
||||||
|
// faces as eight separate draw ops through consolidation, and
|
||||||
|
// the Winners Circle swaps the real callsign in later.
|
||||||
|
//
|
||||||
|
const char *base = strrchr(textureFilename, '\\');
|
||||||
|
base = (base != NULL) ? base + 1 : textureFilename;
|
||||||
|
int plate = 0;
|
||||||
|
if (_strnicmp(base, "player", 6) == 0 &&
|
||||||
|
base[6] >= '1' && base[6] <= '8')
|
||||||
|
{
|
||||||
|
plate = base[6] - '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plate != 0)
|
||||||
|
{
|
||||||
|
memset(&object->mDrawOps[i].texture, 0, sizeof(L4TEXOP));
|
||||||
|
object->mDrawOps[i].texture.texture =
|
||||||
|
NamePlateMarker(device, plate);
|
||||||
|
if (object->mDrawOps[i].texture.texture != NULL)
|
||||||
|
{
|
||||||
|
object->mDrawOps[i].texture.texture->AddRef();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
|
if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
|
||||||
@@ -286,6 +316,53 @@ void d3d_OBJECT::FlushTextureCache()
|
|||||||
(*iter).second.texture->Release();
|
(*iter).second.texture->Release();
|
||||||
}
|
}
|
||||||
mTextureCache.clear();
|
mTextureCache.clear();
|
||||||
|
|
||||||
|
for (int plate = 0; plate <= kNamePlateCount; ++plate)
|
||||||
|
{
|
||||||
|
if (mNamePlateMarkers[plate] != NULL)
|
||||||
|
{
|
||||||
|
mNamePlateMarkers[plate]->Release();
|
||||||
|
mNamePlateMarkers[plate] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// A distinct 1x1 texture per plate, so the eight sign faces stay eight
|
||||||
|
// separate draw ops through mesh consolidation instead of merging into one.
|
||||||
|
// Its content never shows - the callsign replaces it before the podium.
|
||||||
|
//
|
||||||
|
LPDIRECT3DTEXTURE9 d3d_OBJECT::NamePlateMarker(LPDIRECT3DDEVICE9 device, int plate)
|
||||||
|
{
|
||||||
|
if (plate < 1 || plate > kNamePlateCount || device == NULL)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (mNamePlateMarkers[plate] == NULL)
|
||||||
|
{
|
||||||
|
device->CreateTexture(1, 1, 1, 0, D3DFMT_A4R4G4B4, D3DPOOL_MANAGED,
|
||||||
|
&mNamePlateMarkers[plate], NULL);
|
||||||
|
}
|
||||||
|
return mNamePlateMarkers[plate];
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Which plate a texture is the marker for, or 0 if it is not one.
|
||||||
|
//
|
||||||
|
int d3d_OBJECT::NamePlateFor(LPDIRECT3DTEXTURE9 texture)
|
||||||
|
{
|
||||||
|
if (texture == NULL)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (int plate = 1; plate <= kNamePlateCount; ++plate)
|
||||||
|
{
|
||||||
|
if (mNamePlateMarkers[plate] == texture)
|
||||||
|
{
|
||||||
|
return plate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
|
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
|
||||||
|
|||||||
@@ -111,6 +111,27 @@ public:
|
|||||||
// race loop tears the renderer down between missions).
|
// race loop tears the renderer down between missions).
|
||||||
static void FlushTextureCache();
|
static void FlushTextureCache();
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Pilot name plates.
|
||||||
|
//
|
||||||
|
// The Winners Circle signs ask for textures called player1..player8,
|
||||||
|
// which are not files - the renderer draws each pilot's callsign into
|
||||||
|
// a texture at run time. Loading them fails and the plates come out
|
||||||
|
// blank, so the draw ops that wanted them are noted here as the
|
||||||
|
// geometry loads, and bound to the real textures once the finishing
|
||||||
|
// order is known.
|
||||||
|
//
|
||||||
|
// Cleared with the texture cache: the entries point at objects that
|
||||||
|
// belong to the mission being torn down.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Each plate is given a distinct marker texture as it loads. Without
|
||||||
|
// one they are eight identical untextured draw ops, and consolidation
|
||||||
|
// merges every static mesh by material - all eight plates would
|
||||||
|
// collapse into a single shared op that can only ever show one name.
|
||||||
|
enum { kNamePlateCount = 8 };
|
||||||
|
static LPDIRECT3DTEXTURE9 NamePlateMarker(LPDIRECT3DDEVICE9 device, int plate);
|
||||||
|
static int NamePlateFor(LPDIRECT3DTEXTURE9 texture);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
|
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
|
||||||
|
|
||||||
@@ -150,6 +171,9 @@ private:
|
|||||||
|
|
||||||
static long mNextID;
|
static long mNextID;
|
||||||
static stdext::hash_map< std::string , L4TEXOP > mTextureCache;
|
static stdext::hash_map< std::string , L4TEXOP > mTextureCache;
|
||||||
|
|
||||||
|
// index 1..8; [0] unused so the index is the plate number
|
||||||
|
static LPDIRECT3DTEXTURE9 mNamePlateMarkers[kNamePlateCount + 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
extern int gNumBatches;
|
extern int gNumBatches;
|
||||||
|
|||||||
+1052
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
|||||||
|
//===========================================================================//
|
||||||
|
// File: l4joy.h //
|
||||||
|
// Project: MUNGA Brick: generic joystick reader //
|
||||||
|
// Contents: DirectInput 8 sticks, HOTAS throttles and pedals //
|
||||||
|
//---------------------------------------------------------------------------//
|
||||||
|
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||||
|
// PROPRIETARY AND CONFIDENTIAL //
|
||||||
|
//===========================================================================//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
//
|
||||||
|
// L4JOY - the generic-joystick reader.
|
||||||
|
//
|
||||||
|
// PadRIO reads XInput, which covers Xbox-class pads and nothing else.
|
||||||
|
// This layer adds every OTHER game device Windows knows - flight sticks,
|
||||||
|
// HOTAS throttles, twist grips, rudder pedals, wheels - through
|
||||||
|
// DirectInput 8, the standard generic-HID game API. It exposes up to
|
||||||
|
// joyMaxDevices attached devices as normalized state blocks; the PadRIO
|
||||||
|
// poll maps them onto the pod's control channels through the joydev /
|
||||||
|
// joyaxis / joybutton / joyhat rows of bindings.txt (L4PADBINDINGS.h),
|
||||||
|
// the same binding machinery the pad and keyboard already use.
|
||||||
|
//
|
||||||
|
// XInput-class devices are EXCLUDED here, or they would double-feed
|
||||||
|
// through both APIs and every input would count twice. A DirectInput
|
||||||
|
// device whose VID/PID also appears in a RawInput device path containing
|
||||||
|
// the "IG_" marker is an XInput device - the documented detection that
|
||||||
|
// does not drag in WMI.
|
||||||
|
//
|
||||||
|
// This is distinct from the legacy L4DINPUT.cpp DIJoystick, the 1995-era
|
||||||
|
// single-device `Joystick` engine interface reachable only through the
|
||||||
|
// old L4CONTROLS=DIJOYSTICK profile. That path is untouched.
|
||||||
|
//
|
||||||
|
// RP412JOYCONFIG=1 runs the interactive setup wizard at boot: it asks
|
||||||
|
// the player to move each control, works out which device and axis moved
|
||||||
|
// and which way, and writes the joystick section of bindings.txt.
|
||||||
|
// RP412JOYLOG=1 logs device attach/detach.
|
||||||
|
//
|
||||||
|
// Ported from BT411, whose glass cockpit needed the same thing.
|
||||||
|
//
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
joyMaxDevices = 4,
|
||||||
|
joyAxisCount = 8, // X Y Z RX RY RZ SL0 SL1 (DIJOYSTATE2 order)
|
||||||
|
joyButtonCount = 32, // buttons exposed to bindings (DI carries 128)
|
||||||
|
joyHatCount = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RPJoyDeviceState
|
||||||
|
{
|
||||||
|
int attached;
|
||||||
|
float axis[joyAxisCount]; // normalized -1..1, raw: deadzones
|
||||||
|
// are the binding layer's business
|
||||||
|
unsigned buttons; // bit n = button n held
|
||||||
|
int hat[joyHatCount]; // POV in centidegrees, -1 = centered
|
||||||
|
char name[64]; // product name ("T.16000M", ...)
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle. Init is lazy-safe (Poll calls it) and returns the attached
|
||||||
|
// non-XInput device count. Re-enumeration for hot-plug happens inside
|
||||||
|
// Poll on a ~3 s cadence whenever nothing is attached.
|
||||||
|
//
|
||||||
|
int RPJoyInit(void);
|
||||||
|
void RPJoyShutdown(void);
|
||||||
|
void RPJoyPoll(void);
|
||||||
|
|
||||||
|
int RPJoyDeviceCount(void);
|
||||||
|
const RPJoyDeviceState *RPJoyDevice(int index); // NULL out of range/detached
|
||||||
|
|
||||||
|
//
|
||||||
|
// Case-insensitive product-name substring match to a device index, -1 for
|
||||||
|
// no match. This is what a named joydev slot resolves through.
|
||||||
|
//
|
||||||
|
int RPJoyFindDevice(const char *name_substring);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The RP412JOYCONFIG capture wizard (console UI; called from RPL4.CPP
|
||||||
|
// before the front end). Returns 0 if it wrote a config, non-zero on
|
||||||
|
// abort or no device.
|
||||||
|
//
|
||||||
|
int RPJoyConfigWizard(void);
|
||||||
+497
-31
@@ -10,6 +10,101 @@ namespace
|
|||||||
|
|
||||||
const int buttonGap = 4;
|
const int buttonGap = 4;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Sticky placement for the exploded view. See L4MFDVIEW.h; ported
|
||||||
|
// from BT411's BT_GLASS_LAYOUT.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
enum { LayoutOff = 0, LayoutLoad, LayoutSave };
|
||||||
|
|
||||||
|
const char layoutFileName[] = "mfd_layout.cfg";
|
||||||
|
|
||||||
|
// every window taking part, so Save can walk them
|
||||||
|
enum { maxLayoutWindows = 12 };
|
||||||
|
struct LayoutWindow
|
||||||
|
{
|
||||||
|
void *window;
|
||||||
|
const char *title;
|
||||||
|
Logical withSize;
|
||||||
|
Logical bare; // frame stripped, per the file
|
||||||
|
};
|
||||||
|
LayoutWindow layoutWindows[maxLayoutWindows];
|
||||||
|
int layoutWindowCount = 0;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Take a window's frame off: no title bar, no border, nothing to
|
||||||
|
// drag it by. WS_SYSMENU stays - it draws nothing once the caption
|
||||||
|
// is gone, but without one DefWindowProc will not honour Alt+F4,
|
||||||
|
// and a bare window with no way to close it is a trap.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
void StripWindowFrame(HWND window)
|
||||||
|
{
|
||||||
|
LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE);
|
||||||
|
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX |
|
||||||
|
WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME);
|
||||||
|
style |= WS_POPUP;
|
||||||
|
SetWindowLongPtrA(window, GWL_STYLE, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayoutMode()
|
||||||
|
{
|
||||||
|
static int mode = -1;
|
||||||
|
if (mode < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412MFDLAYOUT");
|
||||||
|
if (setting == NULL || setting[0] == '\0')
|
||||||
|
{
|
||||||
|
mode = LayoutOff;
|
||||||
|
}
|
||||||
|
else if (!stricmp(setting, "off") || !stricmp(setting, "0"))
|
||||||
|
{
|
||||||
|
mode = LayoutOff;
|
||||||
|
}
|
||||||
|
else if (!stricmp(setting, "load") || !stricmp(setting, "restore"))
|
||||||
|
{
|
||||||
|
mode = LayoutLoad;
|
||||||
|
}
|
||||||
|
else // save / adjust / on / 1
|
||||||
|
{
|
||||||
|
mode = LayoutSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode != LayoutOff)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: RP412MFDLAYOUT="
|
||||||
|
<< ((mode == LayoutSave) ? "save" : "load")
|
||||||
|
<< " (" << layoutFileName << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// 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
|
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
|
||||||
// bright), red family for MFD strips, amber for the side columns.
|
// bright), red family for MFD strips, amber for the side columns.
|
||||||
@@ -81,6 +176,15 @@ namespace
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
//
|
||||||
|
// A drag just finished. Write the whole arrangement now rather
|
||||||
|
// than trusting teardown, so a hard kill still leaves the
|
||||||
|
// layout that was on screen. No-op unless the mode is save.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
return 0;
|
||||||
|
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
// Part of the cockpit; just hide it.
|
// Part of the cockpit; just hide it.
|
||||||
ShowWindow(hwnd, SW_HIDE);
|
ShowWindow(hwnd, SW_HIDE);
|
||||||
@@ -121,6 +225,8 @@ MFDSplitView::MFDSplitView(
|
|||||||
sourceHeight = source_height;
|
sourceHeight = source_height;
|
||||||
buttonCount = 0;
|
buttonCount = 0;
|
||||||
pressedIndex = -1;
|
pressedIndex = -1;
|
||||||
|
paneTitle = title;
|
||||||
|
ownWindow = (parent == NULL) ? True : False;
|
||||||
|
|
||||||
pixels = new unsigned long[source_width * source_height];
|
pixels = new unsigned long[source_width * source_height];
|
||||||
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
|
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
|
||||||
@@ -135,6 +241,10 @@ MFDSplitView::MFDSplitView(
|
|||||||
header->biCompression = BI_RGB;
|
header->biCompression = BI_RGB;
|
||||||
blitHeader = header;
|
blitHeader = header;
|
||||||
|
|
||||||
|
buttonStyle = button_style;
|
||||||
|
buttonAnchorA = anchorA;
|
||||||
|
buttonAnchorB = anchorB;
|
||||||
|
|
||||||
int client_width = display_width;
|
int client_width = display_width;
|
||||||
int client_height = display_height;
|
int client_height = display_height;
|
||||||
LayoutButtons(button_style, anchorA, anchorB,
|
LayoutButtons(button_style, anchorA, anchorB,
|
||||||
@@ -199,6 +309,14 @@ MFDSplitView::MFDSplitView(
|
|||||||
{
|
{
|
||||||
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
|
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
|
||||||
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
||||||
|
|
||||||
|
// only the exploded view's own windows can be dragged, so only
|
||||||
|
// those have a placement worth remembering. Position only - see
|
||||||
|
// RPWindowLayout_Register.
|
||||||
|
if (ownWindow)
|
||||||
|
{
|
||||||
|
RPWindowLayout_Register(window, paneTitle, False);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -210,6 +328,9 @@ MFDSplitView::~MFDSplitView()
|
|||||||
{
|
{
|
||||||
Check_Pointer(this);
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
// out of the layout registry before the window goes
|
||||||
|
RPWindowLayout_Forget(window);
|
||||||
|
|
||||||
if (window != NULL)
|
if (window != NULL)
|
||||||
{
|
{
|
||||||
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
|
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
|
||||||
@@ -228,6 +349,272 @@ Logical
|
|||||||
return pixels != NULL;
|
return pixels != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Join the saved layout. Registering twice just refreshes the entry, so
|
||||||
|
// a window re-registering after being rebuilt is harmless.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
RPWindowLayout_Register(void *window, const char *title, Logical with_size)
|
||||||
|
{
|
||||||
|
if (window == NULL || title == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
if (layoutWindows[i].window == window)
|
||||||
|
{
|
||||||
|
layoutWindows[i].title = title;
|
||||||
|
layoutWindows[i].withSize = with_size;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (layoutWindowCount >= maxLayoutWindows)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layoutWindows[layoutWindowCount].window = window;
|
||||||
|
layoutWindows[layoutWindowCount].title = title;
|
||||||
|
layoutWindows[layoutWindowCount].withSize = with_size;
|
||||||
|
layoutWindows[layoutWindowCount].bare = False;
|
||||||
|
++layoutWindowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RPWindowLayout_Forget(void *window)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
if (layoutWindows[i].window == window)
|
||||||
|
{
|
||||||
|
layoutWindows[i] = layoutWindows[--layoutWindowCount];
|
||||||
|
layoutWindows[layoutWindowCount].window = NULL;
|
||||||
|
layoutWindows[layoutWindowCount].title = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Sticky placement: restore the saved placement over the computed one.
|
||||||
|
//
|
||||||
|
// Called once everything has been built and placed, so a window the file
|
||||||
|
// does not mention simply keeps where it was put. Size comes back only
|
||||||
|
// for windows registered with it - see the header.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
RPWindowLayout_Load()
|
||||||
|
{
|
||||||
|
if (LayoutMode() == LayoutOff || layoutWindowCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *file = fopen(layoutFileName, "rt");
|
||||||
|
if (file == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: no " << layoutFileName
|
||||||
|
<< " yet (using the computed arrangement)\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int restored = 0;
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), file) != NULL)
|
||||||
|
{
|
||||||
|
char *cursor = line;
|
||||||
|
while (*cursor == ' ' || *cursor == '\t')
|
||||||
|
{
|
||||||
|
++cursor;
|
||||||
|
}
|
||||||
|
if (*cursor == '#' || *cursor == '\r' || *cursor == '\n' || *cursor == '\0')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// titles carry no '=', so the first one splits the line
|
||||||
|
char *equals = strchr(cursor, '=');
|
||||||
|
if (equals == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*equals = '\0';
|
||||||
|
char *end = equals;
|
||||||
|
while (end > cursor && (end[-1] == ' ' || end[-1] == '\t'))
|
||||||
|
{
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
*end = '\0';
|
||||||
|
|
||||||
|
int x = 0, y = 0, w = 0, h = 0;
|
||||||
|
if (sscanf(equals + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Optional trailing keyword, "x,y,w,h,noframe". Searched for
|
||||||
|
// rather than parsed in place so a hand-edited line survives a
|
||||||
|
// stray space or a missing comma.
|
||||||
|
//
|
||||||
|
Logical bare = (strstr(equals + 1, "noframe") != NULL) ? True : False;
|
||||||
|
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
LayoutWindow &entry = layoutWindows[i];
|
||||||
|
if (entry.title == NULL || strcmp(entry.title, cursor) != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HWND target = (HWND) entry.window;
|
||||||
|
if (target == NULL || !IsWindow(target))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a saved size of nothing is a corrupt line, not an instruction
|
||||||
|
Logical size_it = (entry.withSize && w > 0 && h > 0) ? True : False;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Saved coordinates belong to whatever monitors were plugged
|
||||||
|
// in that day. If the placement lands on none of today's, drop
|
||||||
|
// it and keep the computed one - restoring the game window
|
||||||
|
// somewhere invisible would leave nothing to drag back.
|
||||||
|
//
|
||||||
|
RECT landing;
|
||||||
|
landing.left = x;
|
||||||
|
landing.top = y;
|
||||||
|
if (size_it)
|
||||||
|
{
|
||||||
|
landing.right = x + w;
|
||||||
|
landing.bottom = y + h;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RECT current;
|
||||||
|
if (!GetWindowRect(target, ¤t))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
landing.right = x + (current.right - current.left);
|
||||||
|
landing.bottom = y + (current.bottom - current.top);
|
||||||
|
}
|
||||||
|
if (MonitorFromRect(&landing, MONITOR_DEFAULTTONULL) == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: " << cursor << " was saved at "
|
||||||
|
<< x << "," << y << " - off every monitor now, ignoring\n"
|
||||||
|
<< std::flush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Frame off, if the line asked. A bare window's rect IS its
|
||||||
|
// client rect, so whatever client area it has now is the size
|
||||||
|
// to hand back - which also makes the round trip stable: once
|
||||||
|
// bare, what Save records is already the client.
|
||||||
|
//
|
||||||
|
entry.bare = bare;
|
||||||
|
if (bare)
|
||||||
|
{
|
||||||
|
if (!size_it)
|
||||||
|
{
|
||||||
|
RECT client;
|
||||||
|
if (GetClientRect(target, &client))
|
||||||
|
{
|
||||||
|
w = client.right;
|
||||||
|
h = client.bottom;
|
||||||
|
size_it = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StripWindowFrame(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetWindowPos(target, NULL, x, y, w, h,
|
||||||
|
SWP_NOZORDER | SWP_NOACTIVATE |
|
||||||
|
(size_it ? 0 : SWP_NOSIZE) | (bare ? SWP_FRAMECHANGED : 0));
|
||||||
|
++restored;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "MFDLayout: restored " << restored << " window placement(s) from "
|
||||||
|
<< layoutFileName << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Write where every registered window currently is. The whole file each
|
||||||
|
// time - it is a handful of lines, and a rewrite leaves nothing
|
||||||
|
// half-written for a hard kill to catch.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
RPWindowLayout_Save()
|
||||||
|
{
|
||||||
|
if (LayoutMode() != LayoutSave || layoutWindowCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *file = fopen(layoutFileName, "wt");
|
||||||
|
if (file == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: could not write " << layoutFileName
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fputs("# RP412 window placement. RP412MFDLAYOUT=save writes this on each\n"
|
||||||
|
"# finished drag and on exit; =load restores it.\n"
|
||||||
|
"# <title>=<x>,<y>,<w>,<h>. The game window gets its size back too;\n"
|
||||||
|
"# for the display panes the size is reference only, since theirs\n"
|
||||||
|
"# follows their content.\n"
|
||||||
|
"# Append ,noframe to take that window's title bar and border off.\n"
|
||||||
|
"# Put it where you want it first - a bare window has nothing to\n"
|
||||||
|
"# drag by, so its line stops changing once the frame is gone.\n", file);
|
||||||
|
|
||||||
|
int wrote = 0;
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
LayoutWindow &entry = layoutWindows[i];
|
||||||
|
if (entry.title == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HWND entry_window = (HWND) entry.window;
|
||||||
|
if (entry_window == NULL || !IsWindow(entry_window))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// rcNormalPosition rather than GetWindowRect: a minimised window
|
||||||
|
// reports a nonsense rect and a maximised one reports the screen,
|
||||||
|
// and neither is what to come back to. This is the restored
|
||||||
|
// placement whatever state the window is in, so quitting from
|
||||||
|
// maximised still records where the window will reappear.
|
||||||
|
//
|
||||||
|
WINDOWPLACEMENT placement;
|
||||||
|
memset(&placement, 0, sizeof(placement));
|
||||||
|
placement.length = sizeof(placement);
|
||||||
|
if (!GetWindowPlacement(entry_window, &placement))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RECT bounds = placement.rcNormalPosition;
|
||||||
|
// the flag is the player's instruction, not something measured off
|
||||||
|
// the window, so a rewrite has to carry it back out
|
||||||
|
fprintf(file, "%s=%ld,%ld,%ld,%ld%s\n", entry.title,
|
||||||
|
(long) bounds.left, (long) bounds.top,
|
||||||
|
(long) (bounds.right - bounds.left),
|
||||||
|
(long) (bounds.bottom - bounds.top),
|
||||||
|
entry.bare ? ",noframe" : "");
|
||||||
|
++wrote;
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "MFDLayout: saved " << wrote << " window placement(s) to "
|
||||||
|
<< layoutFileName << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Compute the display rectangle and the button rectangles, growing the
|
// Compute the display rectangle and the button rectangles, growing the
|
||||||
// client area to make room for the strips.
|
// client area to make room for the strips.
|
||||||
@@ -255,10 +642,20 @@ void
|
|||||||
// edges (no border against the viewscreen behind); RIO
|
// edges (no border against the viewscreen behind); RIO
|
||||||
// addresses descend from the anchor, row-major (vRIO
|
// addresses descend from the anchor, row-major (vRIO
|
||||||
// CockpitLayout::Mfd).
|
// 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;
|
int strip_h = indicatorStrip;
|
||||||
if (strip_h < 18) strip_h = 18;
|
int button_h = buttonDepth;
|
||||||
if (strip_h > 40) strip_h = 40;
|
|
||||||
|
// 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;
|
int button_w = (display_width - 3 * buttonGap) / 4;
|
||||||
|
|
||||||
@@ -267,56 +664,73 @@ void
|
|||||||
|
|
||||||
for (int i = 0; i < 8; ++i)
|
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;
|
int col = i % 4;
|
||||||
|
|
||||||
ScreenButton *button = &buttons[buttonCount++];
|
ScreenButton *button = &buttons[buttonCount++];
|
||||||
button->unit = anchorA - i;
|
button->unit = anchorA - i;
|
||||||
button->amber = 0;
|
button->amber = 0;
|
||||||
button->x = col * (button_w + buttonGap);
|
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)
|
button->w = (col == 3)
|
||||||
? (display_width - 3 * (button_w + buttonGap))
|
? (display_width - 3 * (button_w + buttonGap))
|
||||||
: button_w;
|
: button_w;
|
||||||
button->h = strip_h;
|
button->h = button_h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (button_style == SideColumns)
|
else if (button_style == SideColumns)
|
||||||
{
|
{
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
// 6 buttons down each side of the glass, stacked contiguous
|
// 6 buttons down each side of the glass (the pod wires only 6
|
||||||
// and flush to the pane edges, like the pod's strips (the
|
// of each column's 8 addresses to buttons; the rest are Tesla
|
||||||
// pod wires only 6 of each column's 8 addresses to buttons;
|
// relays). Left = anchorA ascending, right = anchorB.
|
||||||
// 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;
|
int strip_w = indicatorStrip;
|
||||||
if (col_w < 20) col_w = 20;
|
int button_w = buttonDepth;
|
||||||
if (col_w > 40) col_w = 40;
|
|
||||||
|
|
||||||
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;
|
displayX = strip_w;
|
||||||
*client_width = display_width + 2 * col_w;
|
*client_width = display_width + 2 * strip_w;
|
||||||
|
|
||||||
for (int i = 0; i < 6; ++i)
|
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++];
|
ScreenButton *left = &buttons[buttonCount++];
|
||||||
left->unit = anchorA + i;
|
left->unit = anchorA + i;
|
||||||
left->amber = 1;
|
left->amber = 1;
|
||||||
left->x = 0;
|
left->x = 0;
|
||||||
left->y = i * button_h;
|
left->y = top;
|
||||||
left->w = col_w;
|
left->w = button_w;
|
||||||
left->h = h;
|
left->h = h;
|
||||||
|
|
||||||
ScreenButton *right = &buttons[buttonCount++];
|
ScreenButton *right = &buttons[buttonCount++];
|
||||||
right->unit = anchorB + i;
|
right->unit = anchorB + i;
|
||||||
right->amber = 1;
|
right->amber = 1;
|
||||||
right->x = displayX + displayW;
|
right->x = displayX + displayW + strip_w - button_w;
|
||||||
right->y = left->y;
|
right->y = left->y;
|
||||||
right->w = col_w;
|
right->w = button_w;
|
||||||
right->h = h;
|
right->h = h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,15 +756,11 @@ void
|
|||||||
|
|
||||||
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
||||||
|
|
||||||
SetStretchBltMode(mem, COLORONCOLOR);
|
//---------------------------------------------------------------
|
||||||
StretchDIBits(
|
// Buttons first, glass second: the MFD banks extend under the
|
||||||
mem,
|
// display so the picture is the click target, and the image
|
||||||
displayX, displayY, displayW, displayH,
|
// painted over them leaves only their indicator strips showing.
|
||||||
0, 0, sourceWidth, sourceHeight,
|
//---------------------------------------------------------------
|
||||||
pixels, (const BITMAPINFO *) blitHeader,
|
|
||||||
DIB_RGB_COLORS, SRCCOPY
|
|
||||||
);
|
|
||||||
|
|
||||||
for (int i = 0; i < buttonCount; ++i)
|
for (int i = 0; i < buttonCount; ++i)
|
||||||
{
|
{
|
||||||
const ScreenButton *button = &buttons[i];
|
const ScreenButton *button = &buttons[i];
|
||||||
@@ -375,6 +785,20 @@ void
|
|||||||
DeleteObject(frame);
|
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);
|
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
||||||
|
|
||||||
SelectObject(mem, old_surface);
|
SelectObject(mem, old_surface);
|
||||||
@@ -422,6 +846,20 @@ void
|
|||||||
// one message per frame, and queued WM_PAINTs (lowest priority) starve
|
// one message per frame, and queued WM_PAINTs (lowest priority) starve
|
||||||
// behind it - panes would freeze on their first frame.
|
// behind it - panes would freeze on their first frame.
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Off screen, and stay off: the pane keeps its pixels and geometry, it
|
||||||
|
// simply stops being shown. Repaint() on a hidden window is harmless.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
MFDSplitView::Hide()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
if (window != NULL)
|
||||||
|
{
|
||||||
|
ShowWindow((HWND) window, SW_HIDE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MFDSplitView::SetPosition(int x, int y)
|
MFDSplitView::SetPosition(int x, int y)
|
||||||
{
|
{
|
||||||
@@ -433,6 +871,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
|
void
|
||||||
MFDSplitView::Repaint()
|
MFDSplitView::Repaint()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,6 +65,26 @@ public:
|
|||||||
void
|
void
|
||||||
SetPosition(int x, int y);
|
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);
|
||||||
|
|
||||||
|
// Take the pane off screen. Used for the Winners Circle, which wants
|
||||||
|
// the whole viewscreen: the panes overlap it like the pod's bezels,
|
||||||
|
// so hiding them uncovers the full canvas underneath.
|
||||||
|
void
|
||||||
|
Hide();
|
||||||
|
|
||||||
|
// caption and HWND, for the sticky-placement file
|
||||||
|
const char *
|
||||||
|
Title() const
|
||||||
|
{ return paneTitle; }
|
||||||
|
void *
|
||||||
|
Window()
|
||||||
|
{ return window; }
|
||||||
|
|
||||||
~MFDSplitView();
|
~MFDSplitView();
|
||||||
|
|
||||||
Logical
|
Logical
|
||||||
@@ -128,4 +148,86 @@ protected:
|
|||||||
buttonCount;
|
buttonCount;
|
||||||
int
|
int
|
||||||
pressedIndex;
|
pressedIndex;
|
||||||
|
|
||||||
|
// kept so the bank can be rebuilt when the cockpit re-scales
|
||||||
|
ButtonStyle
|
||||||
|
buttonStyle;
|
||||||
|
int
|
||||||
|
buttonAnchorA,
|
||||||
|
buttonAnchorB;
|
||||||
|
|
||||||
|
// the window caption, and the key this pane is saved under
|
||||||
|
const char
|
||||||
|
*paneTitle;
|
||||||
|
// True for a pane of its own on the desktop (the exploded view); the
|
||||||
|
// composited cockpit's panes are chrome-less children and cannot be
|
||||||
|
// dragged, so there is nothing to remember for them.
|
||||||
|
Logical
|
||||||
|
ownWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Sticky window placement.
|
||||||
|
//
|
||||||
|
// The game window and, in the exploded view, each display pane are all
|
||||||
|
// draggable, but their placement is recomputed on every launch - so
|
||||||
|
// putting a window somewhere useful never survived the menu-race-menu
|
||||||
|
// loop. RP412MFDLAYOUT remembers it, in mfd_layout.cfg beside
|
||||||
|
// bindings.txt:
|
||||||
|
//
|
||||||
|
// off / 0 / unset computed placement only, no file (default)
|
||||||
|
// load / restore restore saved placement at startup, never write
|
||||||
|
// save / adjust restore, then rewrite on each finished drag and
|
||||||
|
// on teardown - the round trip
|
||||||
|
//
|
||||||
|
// One "<title>=x,y,w,h" line per window, plus an optional ",noframe"
|
||||||
|
// that takes that window's title bar and border off - for a cockpit that
|
||||||
|
// fills a monitor edge to edge without -fit's all-or-nothing, or an
|
||||||
|
// exploded pane photographed without chrome. A bare window has nothing
|
||||||
|
// to drag by, so place it first and add the flag after; Save carries the
|
||||||
|
// flag back out, since it is an instruction rather than something
|
||||||
|
// measured off the window. Windows are always built framed, so deleting
|
||||||
|
// the flag is all it takes to get the frame back.
|
||||||
|
//
|
||||||
|
// Whether the size comes back depends on the window, which is why
|
||||||
|
// Register takes it as a flag:
|
||||||
|
//
|
||||||
|
// display panes position only. A pane's size follows its content and
|
||||||
|
// its button banks, so an old size from a different
|
||||||
|
// build must not distort it. The plasma glass registers
|
||||||
|
// the same way - its size is L4PLASMASCALE's business.
|
||||||
|
// the game window position and size. Nothing derives that size - -res
|
||||||
|
// only decides how sharp the scene is, and the cockpit
|
||||||
|
// fits itself to whatever client area it is given - so
|
||||||
|
// a window sized to suit a monitor should come back
|
||||||
|
// that way, and half-restoring it would be the strange
|
||||||
|
// behaviour. RPL4.CPP registers it before the console
|
||||||
|
// screen, so the saved placement is there from the
|
||||||
|
// first frame rather than arriving when a race starts.
|
||||||
|
//
|
||||||
|
// A placement that lands on none of the monitors currently plugged in is
|
||||||
|
// ignored rather than applied - restoring the game window off screen
|
||||||
|
// would leave nothing to drag back.
|
||||||
|
//
|
||||||
|
// Lives here because the display panes were the first to need it; the
|
||||||
|
// game window joined later rather than grow a second copy of the same
|
||||||
|
// file format.
|
||||||
|
//
|
||||||
|
// Ported from BT411's BT_GLASS_LAYOUT.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
// Take part in the saved layout. title is the key in the file and must
|
||||||
|
// outlive the window (the callers pass string literals).
|
||||||
|
void
|
||||||
|
RPWindowLayout_Register(void *window, const char *title, Logical with_size);
|
||||||
|
void
|
||||||
|
RPWindowLayout_Forget(void *window);
|
||||||
|
|
||||||
|
// Apply the saved placement over the computed one. Call once everything
|
||||||
|
// has been built and placed.
|
||||||
|
void
|
||||||
|
RPWindowLayout_Load();
|
||||||
|
|
||||||
|
// Write every registered window's placement. No-op unless mode is save.
|
||||||
|
void
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
|||||||
+202
-28
@@ -2,6 +2,7 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4padbindings.h"
|
#include "l4padbindings.h"
|
||||||
|
#include "l4joy.h" // joyButtonCount / joyHatCount, the parse limits
|
||||||
|
|
||||||
#include <XInput.h>
|
#include <XInput.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -79,6 +80,20 @@ namespace
|
|||||||
{ "Throttle", BindAxisThrottle },
|
{ "Throttle", BindAxisThrottle },
|
||||||
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
||||||
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
|
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
|
||||||
|
{ "Pedals", BindAxisPedals },
|
||||||
|
};
|
||||||
|
|
||||||
|
// DirectInput's axis order, which is what the joy* rows name
|
||||||
|
const NameValue kJoyAxisNames[] =
|
||||||
|
{
|
||||||
|
{ "X", BindJoyAxisX }, { "Y", BindJoyAxisY }, { "Z", BindJoyAxisZ },
|
||||||
|
{ "RX", BindJoyAxisRX }, { "RY", BindJoyAxisRY }, { "RZ", BindJoyAxisRZ },
|
||||||
|
{ "SL0", BindJoyAxisSL0 }, { "SL1", BindJoyAxisSL1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameValue kJoyHatNames[] =
|
||||||
|
{
|
||||||
|
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
|
||||||
};
|
};
|
||||||
|
|
||||||
Logical NameEquals(const char *a, const char *b)
|
Logical NameEquals(const char *a, const char *b)
|
||||||
@@ -185,10 +200,83 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// One line of the profile grammar
|
// Shared tail of the two axis-source rows: [invert] [deadzone <d>]
|
||||||
|
// [rate <n>], in any order.
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
|
Logical ParseAxisOptions(
|
||||||
|
char *tokens[], int token_count, int first,
|
||||||
|
Logical *invert, Scalar *deadzone, Scalar *rate)
|
||||||
{
|
{
|
||||||
|
for (int i = first; i < token_count; ++i)
|
||||||
|
{
|
||||||
|
if (NameEquals(tokens[i], "invert"))
|
||||||
|
{
|
||||||
|
*invert = True;
|
||||||
|
}
|
||||||
|
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
||||||
|
{
|
||||||
|
if (!ParseNumber(tokens[++i], deadzone))
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
||||||
|
{
|
||||||
|
if (!ParseNumber(tokens[++i], rate))
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// One line of the profile grammar. joy_slot carries the joydev
|
||||||
|
// state forward from line to line - joy rows attach to the slot
|
||||||
|
// most recently declared.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
Logical ParseLine(
|
||||||
|
char *tokens[], int token_count, PadBindingProfile *profile,
|
||||||
|
int *joy_slot)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// joydev is the one row that can be two tokens long ("joydev 1"),
|
||||||
|
// so it is answered before the four-token floor below.
|
||||||
|
//
|
||||||
|
if (NameEquals(tokens[0], "joydev") && token_count >= 2)
|
||||||
|
{
|
||||||
|
int slot = -1;
|
||||||
|
if (sscanf(tokens[1], "%d", &slot) != 1 ||
|
||||||
|
slot < 0 || slot >= BindJoyDeviceSlots)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
*joy_slot = slot;
|
||||||
|
//
|
||||||
|
// The rest of the line is a product-name substring, rejoined
|
||||||
|
// with single spaces ("Saitek Pro Flight" is four tokens).
|
||||||
|
//
|
||||||
|
profile->joyDeviceMatch[slot][0] = '\0';
|
||||||
|
for (int i = 2; i < token_count; ++i)
|
||||||
|
{
|
||||||
|
if (i > 2)
|
||||||
|
{
|
||||||
|
strncat(profile->joyDeviceMatch[slot], " ",
|
||||||
|
sizeof(profile->joyDeviceMatch[slot]) -
|
||||||
|
strlen(profile->joyDeviceMatch[slot]) - 1);
|
||||||
|
}
|
||||||
|
strncat(profile->joyDeviceMatch[slot], tokens[i],
|
||||||
|
sizeof(profile->joyDeviceMatch[slot]) -
|
||||||
|
strlen(profile->joyDeviceMatch[slot]) - 1);
|
||||||
|
}
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
if (token_count < 4)
|
if (token_count < 4)
|
||||||
{
|
{
|
||||||
return False;
|
return False;
|
||||||
@@ -271,31 +359,75 @@ namespace
|
|||||||
memset(binding, 0, sizeof(*binding));
|
memset(binding, 0, sizeof(*binding));
|
||||||
binding->source = source;
|
binding->source = source;
|
||||||
binding->axis = axis;
|
binding->axis = axis;
|
||||||
for (int i = 4; i < token_count; ++i)
|
return ParseAxisOptions(tokens, token_count, 4,
|
||||||
|
&binding->invert, &binding->deadzone, &binding->rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Generic joystick rows, all attaching to the current joydev slot
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
if (NameEquals(tokens[0], "joyaxis") && NameEquals(tokens[2], "axis"))
|
||||||
|
{
|
||||||
|
int source = LookupTable(kJoyAxisNames,
|
||||||
|
sizeof(kJoyAxisNames) / sizeof(kJoyAxisNames[0]), tokens[1]);
|
||||||
|
int axis = LookupTable(kRioAxisNames,
|
||||||
|
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
|
||||||
|
if (source < 0 || axis < 0 ||
|
||||||
|
profile->joyAxisCount >= PadBindingProfile::maxJoyAxes)
|
||||||
{
|
{
|
||||||
if (NameEquals(tokens[i], "invert"))
|
return False;
|
||||||
{
|
|
||||||
binding->invert = True;
|
|
||||||
}
|
|
||||||
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
|
||||||
{
|
|
||||||
if (!ParseNumber(tokens[++i], &binding->deadzone))
|
|
||||||
{
|
|
||||||
return False;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
|
||||||
{
|
|
||||||
if (!ParseNumber(tokens[++i], &binding->rate))
|
|
||||||
{
|
|
||||||
return False;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return False;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
PadJoyAxisBinding *binding = &profile->joyAxes[profile->joyAxisCount++];
|
||||||
|
memset(binding, 0, sizeof(*binding));
|
||||||
|
binding->device = *joy_slot;
|
||||||
|
binding->source = source;
|
||||||
|
binding->axis = axis;
|
||||||
|
return ParseAxisOptions(tokens, token_count, 4,
|
||||||
|
&binding->invert, &binding->deadzone, &binding->rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameEquals(tokens[0], "joybutton") && NameEquals(tokens[2], "button"))
|
||||||
|
{
|
||||||
|
int button = -1;
|
||||||
|
int address;
|
||||||
|
if (sscanf(tokens[1], "%d", &button) != 1 ||
|
||||||
|
button < 0 || button >= joyButtonCount ||
|
||||||
|
!ParseAddress(tokens[3], &address) ||
|
||||||
|
profile->joyButtonCount >= PadBindingProfile::maxJoyButtons)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
|
||||||
|
PadJoyButtonBinding *binding =
|
||||||
|
&profile->joyButtons[profile->joyButtonCount++];
|
||||||
|
memset(binding, 0, sizeof(*binding));
|
||||||
|
binding->device = *joy_slot;
|
||||||
|
binding->button = button;
|
||||||
|
binding->address = address;
|
||||||
|
binding->toggle = toggle;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameEquals(tokens[0], "joyhat") && token_count >= 5 &&
|
||||||
|
NameEquals(tokens[3], "button"))
|
||||||
|
{
|
||||||
|
int hat = -1;
|
||||||
|
int direction = LookupTable(kJoyHatNames,
|
||||||
|
sizeof(kJoyHatNames) / sizeof(kJoyHatNames[0]), tokens[2]);
|
||||||
|
int address;
|
||||||
|
if (sscanf(tokens[1], "%d", &hat) != 1 ||
|
||||||
|
hat < 0 || hat >= joyHatCount || direction < 0 ||
|
||||||
|
!ParseAddress(tokens[4], &address) ||
|
||||||
|
profile->joyHatCount >= PadBindingProfile::maxJoyHats)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
PadJoyHatBinding *binding = &profile->joyHats[profile->joyHatCount++];
|
||||||
|
memset(binding, 0, sizeof(*binding));
|
||||||
|
binding->device = *joy_slot;
|
||||||
|
binding->hat = hat;
|
||||||
|
binding->direction = direction;
|
||||||
|
binding->address = address;
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +452,17 @@ namespace
|
|||||||
"# key <name> axis <axis> rate <n-per-second>\n"
|
"# key <name> axis <axis> rate <n-per-second>\n"
|
||||||
"# pad <button> button <addr> [toggle]\n"
|
"# pad <button> button <addr> [toggle]\n"
|
||||||
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||||
|
"# joydev <slot> [product-name substring]\n"
|
||||||
|
"# joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||||
|
"# joybutton <n> button <addr> [toggle]\n"
|
||||||
|
"# joyhat <n> <up|down|left|right> button <addr>\n"
|
||||||
"#\n"
|
"#\n"
|
||||||
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
||||||
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
|
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
|
||||||
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
||||||
|
"# | Pedals - a signed axis that works the pedal PAIR, positive\n"
|
||||||
|
"# for the right pedal and negative for the left, so one rudder\n"
|
||||||
|
"# bar or twist grip drives both.\n"
|
||||||
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
|
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
|
||||||
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
||||||
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
||||||
@@ -336,6 +475,31 @@ namespace
|
|||||||
"# back on release; 'rate' walks the axis by <n> per second and the\n"
|
"# back on release; 'rate' walks the axis by <n> per second and the\n"
|
||||||
"# position sticks (the throttle). Every lamp button is also clickable\n"
|
"# position sticks (the throttle). Every lamp button is also clickable\n"
|
||||||
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
|
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
|
||||||
|
"#\n"
|
||||||
|
"# ---- Flight sticks, HOTAS throttles and rudder pedals --------------\n"
|
||||||
|
"#\n"
|
||||||
|
"# EASIEST: run joyconfig.bat once. It asks you to move each control,\n"
|
||||||
|
"# works out which device and axis you moved and which way round it\n"
|
||||||
|
"# reads, and writes the joy* rows below a marker line at the end of\n"
|
||||||
|
"# this file. Everything you have written yourself is kept. Xbox-class\n"
|
||||||
|
"# pads need none of this - they are the pad* rows above.\n"
|
||||||
|
"#\n"
|
||||||
|
"# By hand: joydev picks the device for the rows that follow it - a\n"
|
||||||
|
"# name substring binds that product, a bare slot number binds the Nth\n"
|
||||||
|
"# stick Windows lists. <src> for joyaxis is the DirectInput axis name,\n"
|
||||||
|
"# X Y Z RX RY RZ SL0 SL1: a twist grip is usually RZ and a HOTAS\n"
|
||||||
|
"# throttle usually Z or SL0. A joyaxis on Throttle with no 'rate' is\n"
|
||||||
|
"# treated as a real lever and OWNS the channel - its full travel is\n"
|
||||||
|
"# the full throttle range, rather than nudging the position the way a\n"
|
||||||
|
"# spring-centred pad stick has to.\n"
|
||||||
|
"#\n"
|
||||||
|
"# joydev 0 T.16000M\n"
|
||||||
|
"# joyaxis X axis JoystickX invert deadzone 0.08\n"
|
||||||
|
"# joyaxis Y axis JoystickY invert deadzone 0.08\n"
|
||||||
|
"# joyaxis RZ axis Pedals deadzone 0.08\n"
|
||||||
|
"# joyaxis SL0 axis Throttle deadzone 0\n"
|
||||||
|
"# joybutton 0 button 0x40\n"
|
||||||
|
"# joyhat 0 up button 0x42\n"
|
||||||
"\n"
|
"\n"
|
||||||
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
||||||
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
||||||
@@ -376,8 +540,11 @@ namespace
|
|||||||
"pad DPadRight button 0x43 # Hat Right\n"
|
"pad DPadRight button 0x43 # Hat Right\n"
|
||||||
"pad LeftShoulder button 0x3D # Panic\n"
|
"pad LeftShoulder button 0x3D # Panic\n"
|
||||||
"pad RightShoulder button 0x3F # Throttle button (reverse)\n"
|
"pad RightShoulder button 0x3F # Throttle button (reverse)\n"
|
||||||
"pad Start button 0x37 # config 1\n"
|
"\n"
|
||||||
"pad Back button 0x36 # config 2\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"
|
"\n"
|
||||||
"# ---- Upper MFD bank: the number row is the top MFD row and the\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"
|
"# ---- QWERTY row is the row under it, left to right across the\n"
|
||||||
@@ -496,6 +663,7 @@ void
|
|||||||
char line[256];
|
char line[256];
|
||||||
int line_number = 0;
|
int line_number = 0;
|
||||||
int error_count = 0;
|
int error_count = 0;
|
||||||
|
int joy_slot = 0; // joy rows before any joydev belong to slot 0
|
||||||
const char *cursor = source;
|
const char *cursor = source;
|
||||||
while (*cursor != '\0')
|
while (*cursor != '\0')
|
||||||
{
|
{
|
||||||
@@ -520,7 +688,7 @@ void
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!ParseLine(tokens, token_count, profile))
|
if (!ParseLine(tokens, token_count, profile, &joy_slot))
|
||||||
{
|
{
|
||||||
++error_count;
|
++error_count;
|
||||||
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
||||||
@@ -539,4 +707,10 @@ void
|
|||||||
<< profile->padAxisCount << " pad axes"
|
<< profile->padAxisCount << " pad axes"
|
||||||
<< (error_count ? " (with rejected lines)" : "")
|
<< (error_count ? " (with rejected lines)" : "")
|
||||||
<< "\n" << std::flush;
|
<< "\n" << std::flush;
|
||||||
|
if (profile->joyAxisCount || profile->joyButtonCount || profile->joyHatCount)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "PadBindings: joystick - " << profile->joyAxisCount
|
||||||
|
<< " axes, " << profile->joyButtonCount << " buttons, "
|
||||||
|
<< profile->joyHatCount << " hat directions\n" << std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,25 @@
|
|||||||
// key <name> axis <axis> deflect <n> | rate <n>
|
// key <name> axis <axis> deflect <n> | rate <n>
|
||||||
// pad <button> button <addr> [toggle]
|
// pad <button> button <addr> [toggle]
|
||||||
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||||
|
// joydev <slot> [product-name substring...]
|
||||||
|
// joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||||
|
// joybutton <n> button <addr> [toggle]
|
||||||
|
// joyhat <n> <up|down|left|right> button <addr>
|
||||||
//
|
//
|
||||||
// <addr> is a RIO input address: lamp buttons 0x00-0x47, internal
|
// <addr> is a RIO input address: lamp buttons 0x00-0x47, internal
|
||||||
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
|
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
|
||||||
// bindings.txt beside the exe; written there (self-documenting, with
|
// bindings.txt beside the exe; written there (self-documenting, with
|
||||||
// the full default profile) on first run. Bad lines are logged and
|
// the full default profile) on first run. Bad lines are logged and
|
||||||
// skipped, good lines always win.
|
// skipped, good lines always win.
|
||||||
|
//
|
||||||
|
// The joy* rows drive generic DirectInput devices - flight sticks,
|
||||||
|
// HOTAS throttles, twist grips, rudder pedals (L4JOY.h). They attach to
|
||||||
|
// the most recent joydev slot, or slot 0 if no joydev came first. A slot
|
||||||
|
// naming a product substring binds THAT device; a bare slot binds the
|
||||||
|
// Nth attached non-XInput device. Source names follow the DirectInput
|
||||||
|
// layout - X Y Z RX RY RZ SL0 SL1 - where a twist grip is usually RZ
|
||||||
|
// and a HOTAS throttle usually Z or SL0. RP412JOYCONFIG=1 writes these
|
||||||
|
// rows for you by asking the player to move each control.
|
||||||
//########################################################################
|
//########################################################################
|
||||||
|
|
||||||
enum PadBindRioAxis
|
enum PadBindRioAxis
|
||||||
@@ -33,6 +46,14 @@ enum PadBindRioAxis
|
|||||||
BindAxisRightPedal,
|
BindAxisRightPedal,
|
||||||
BindAxisJoystickY,
|
BindAxisJoystickY,
|
||||||
BindAxisJoystickX,
|
BindAxisJoystickX,
|
||||||
|
//
|
||||||
|
// A signed composite, not a channel the pod has: positive presses
|
||||||
|
// the right pedal, negative the left. One physical control - a
|
||||||
|
// rudder bar, a twist grip, a stick axis - works the pedal pair the
|
||||||
|
// way a foot never could, one or the other and never both. It
|
||||||
|
// decomposes into the real pair when the poll applies it.
|
||||||
|
//
|
||||||
|
BindAxisPedals,
|
||||||
BindAxisCount
|
BindAxisCount
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +110,54 @@ struct PadPadAxisBinding
|
|||||||
Scalar rate; // 0 = direct position, >0 = speed integrate
|
Scalar rate; // 0 = direct position, >0 = speed integrate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generic joystick (DirectInput - L4JOY.h). device is a joydev SLOT,
|
||||||
|
// resolved to a live device at poll time by the slot's name substring or
|
||||||
|
// by its ordinal, so unplugging and replugging does not rewrite the file.
|
||||||
|
//
|
||||||
|
enum { BindJoyDeviceSlots = 4 };
|
||||||
|
|
||||||
|
enum PadBindJoyAxis
|
||||||
|
{
|
||||||
|
BindJoyAxisX = 0,
|
||||||
|
BindJoyAxisY,
|
||||||
|
BindJoyAxisZ,
|
||||||
|
BindJoyAxisRX,
|
||||||
|
BindJoyAxisRY,
|
||||||
|
BindJoyAxisRZ,
|
||||||
|
BindJoyAxisSL0,
|
||||||
|
BindJoyAxisSL1,
|
||||||
|
BindJoyAxisCount
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadJoyAxisBinding
|
||||||
|
{
|
||||||
|
int device; // joydev slot
|
||||||
|
int source; // PadBindJoyAxis
|
||||||
|
int axis; // PadBindRioAxis
|
||||||
|
Logical invert;
|
||||||
|
Scalar deadzone; // normalized 0..1
|
||||||
|
Scalar rate; // 0 = direct position, >0 = speed integrate
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadJoyButtonBinding
|
||||||
|
{
|
||||||
|
int device; // joydev slot
|
||||||
|
int button; // 0-31
|
||||||
|
int address;
|
||||||
|
Logical toggle;
|
||||||
|
Logical wasDown;
|
||||||
|
Logical latched;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadJoyHatBinding
|
||||||
|
{
|
||||||
|
int device; // joydev slot
|
||||||
|
int hat; // 0-3
|
||||||
|
int direction; // 0 up, 1 right, 2 down, 3 left
|
||||||
|
int address;
|
||||||
|
};
|
||||||
|
|
||||||
struct PadBindingProfile
|
struct PadBindingProfile
|
||||||
{
|
{
|
||||||
enum
|
enum
|
||||||
@@ -96,7 +165,10 @@ struct PadBindingProfile
|
|||||||
maxKeyButtons = 128,
|
maxKeyButtons = 128,
|
||||||
maxKeyAxes = 32,
|
maxKeyAxes = 32,
|
||||||
maxPadButtons = 32,
|
maxPadButtons = 32,
|
||||||
maxPadAxes = 16
|
maxPadAxes = 16,
|
||||||
|
maxJoyAxes = 24,
|
||||||
|
maxJoyButtons = 48,
|
||||||
|
maxJoyHats = 16
|
||||||
};
|
};
|
||||||
|
|
||||||
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
||||||
@@ -107,6 +179,15 @@ struct PadBindingProfile
|
|||||||
int padButtonCount;
|
int padButtonCount;
|
||||||
PadPadAxisBinding padAxes[maxPadAxes];
|
PadPadAxisBinding padAxes[maxPadAxes];
|
||||||
int padAxisCount;
|
int padAxisCount;
|
||||||
|
|
||||||
|
PadJoyAxisBinding joyAxes[maxJoyAxes];
|
||||||
|
int joyAxisCount;
|
||||||
|
PadJoyButtonBinding joyButtons[maxJoyButtons];
|
||||||
|
int joyButtonCount;
|
||||||
|
PadJoyHatBinding joyHats[maxJoyHats];
|
||||||
|
int joyHatCount;
|
||||||
|
// "" = take the slot's ordinal attached device
|
||||||
|
char joyDeviceMatch[BindJoyDeviceSlots][64];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load bindings.txt from the working directory into the profile,
|
// Load bindings.txt from the working directory into the profile,
|
||||||
|
|||||||
+214
-1
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "l4padrio.h"
|
#include "l4padrio.h"
|
||||||
#include "l4keylight.h"
|
#include "l4keylight.h"
|
||||||
|
#include "l4joy.h"
|
||||||
|
|
||||||
#include <XInput.h>
|
#include <XInput.h>
|
||||||
#pragma comment(lib, "xinput9_1_0.lib")
|
#pragma comment(lib, "xinput9_1_0.lib")
|
||||||
@@ -41,6 +42,51 @@ namespace
|
|||||||
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// A generic stick axis is already normalized -1..1, so the deadzone
|
||||||
|
// is a plain cut about centre with the remainder rescaled - press
|
||||||
|
// just past the edge and you get just past zero, not a step.
|
||||||
|
//
|
||||||
|
Scalar JoyAxisValue(Scalar raw, Scalar deadzone)
|
||||||
|
{
|
||||||
|
if (deadzone <= 0.0f)
|
||||||
|
{
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (raw > -deadzone && raw < deadzone)
|
||||||
|
{
|
||||||
|
return (Scalar) 0;
|
||||||
|
}
|
||||||
|
Scalar value = (raw > 0.0f)
|
||||||
|
? (raw - deadzone) / (1.0f - deadzone)
|
||||||
|
: (raw + deadzone) / (1.0f - deadzone);
|
||||||
|
if (value > 1.0f) value = 1.0f;
|
||||||
|
if (value < -1.0f) value = -1.0f;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// A POV hat reports centidegrees clockwise from up, or -1 centered.
|
||||||
|
// The 45-degree window each way is what makes the diagonals press
|
||||||
|
// both of their neighbours, which is how a four-way hat is read.
|
||||||
|
//
|
||||||
|
Logical JoyHatHeld(int centidegrees, int direction)
|
||||||
|
{
|
||||||
|
if (centidegrees < 0)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
int degrees = (centidegrees / 100) % 360;
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case 0: return (degrees >= 315 || degrees <= 45) ? True : False;
|
||||||
|
case 1: return (degrees >= 45 && degrees <= 135) ? True : False;
|
||||||
|
case 2: return (degrees >= 135 && degrees <= 225) ? True : False;
|
||||||
|
case 3: return (degrees >= 225 && degrees <= 315) ? True : False;
|
||||||
|
}
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
|
||||||
void KeyLightLog(const char *line)
|
void KeyLightLog(const char *line)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << line << "\n" << std::flush;
|
DEBUG_STREAM << line << "\n" << std::flush;
|
||||||
@@ -166,6 +212,29 @@ PadRIO::PadRIO()
|
|||||||
activeInstance = this;
|
activeInstance = this;
|
||||||
|
|
||||||
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Only open DirectInput when the profile actually asks for it. A
|
||||||
|
// player on keyboard and pad should not pay for an enumeration of
|
||||||
|
// every HID on the machine, and joyconfig.bat is what writes the
|
||||||
|
// rows that turn this on.
|
||||||
|
//
|
||||||
|
if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 ||
|
||||||
|
profile.joyHatCount > 0)
|
||||||
|
{
|
||||||
|
int found = RPJoyInit();
|
||||||
|
DEBUG_STREAM << "PadRIO: joystick bindings present, " << found
|
||||||
|
<< " generic device(s) attached\n" << std::flush;
|
||||||
|
for (int d = 0; d < found; ++d)
|
||||||
|
{
|
||||||
|
const RPJoyDeviceState *state = RPJoyDevice(d);
|
||||||
|
if (state != NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "PadRIO: [" << d << "] " << state->name
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PadRIO::~PadRIO()
|
PadRIO::~PadRIO()
|
||||||
@@ -244,6 +313,11 @@ void
|
|||||||
profile.padButtons[i].latched = False;
|
profile.padButtons[i].latched = False;
|
||||||
profile.padButtons[i].wasDown = False;
|
profile.padButtons[i].wasDown = False;
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i < profile.joyButtonCount; ++i)
|
||||||
|
{
|
||||||
|
profile.joyButtons[i].latched = False;
|
||||||
|
profile.joyButtons[i].wasDown = False;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -391,6 +465,81 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Generic joysticks. The slots are resolved every poll rather than
|
||||||
|
// cached, so a stick unplugged mid-race simply stops answering and
|
||||||
|
// one plugged back in picks up where it left off.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
int joyDevice[BindJoyDeviceSlots];
|
||||||
|
Logical joyLive = False;
|
||||||
|
for (int slot = 0; slot < BindJoyDeviceSlots; ++slot)
|
||||||
|
{
|
||||||
|
joyDevice[slot] = -1;
|
||||||
|
}
|
||||||
|
if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 ||
|
||||||
|
profile.joyHatCount > 0)
|
||||||
|
{
|
||||||
|
RPJoyPoll();
|
||||||
|
for (int slot = 0; slot < BindJoyDeviceSlots; ++slot)
|
||||||
|
{
|
||||||
|
joyDevice[slot] = (profile.joyDeviceMatch[slot][0] != '\0')
|
||||||
|
? RPJoyFindDevice(profile.joyDeviceMatch[slot])
|
||||||
|
: ((RPJoyDevice(slot) != NULL) ? slot : -1);
|
||||||
|
if (joyDevice[slot] >= 0)
|
||||||
|
{
|
||||||
|
joyLive = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < profile.joyButtonCount; ++i)
|
||||||
|
{
|
||||||
|
PadJoyButtonBinding *binding = &profile.joyButtons[i];
|
||||||
|
const RPJoyDeviceState *state =
|
||||||
|
(binding->device >= 0 && binding->device < BindJoyDeviceSlots)
|
||||||
|
? RPJoyDevice(joyDevice[binding->device]) : NULL;
|
||||||
|
Logical down = (state != NULL) &&
|
||||||
|
(state->buttons & (1u << binding->button)) != 0;
|
||||||
|
if (binding->toggle && down && !binding->wasDown)
|
||||||
|
{
|
||||||
|
binding->latched = !binding->latched;
|
||||||
|
}
|
||||||
|
binding->wasDown = down;
|
||||||
|
|
||||||
|
if (binding->toggle ? binding->latched : down)
|
||||||
|
{
|
||||||
|
if (binding->address < buttonUnits)
|
||||||
|
{
|
||||||
|
desired[binding->address] = 1;
|
||||||
|
}
|
||||||
|
else if (binding->address >= 0x50 &&
|
||||||
|
binding->address < 0x50 + keypadUnits)
|
||||||
|
{
|
||||||
|
keypadDesired[binding->address - 0x50] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < profile.joyHatCount; ++i)
|
||||||
|
{
|
||||||
|
const PadJoyHatBinding *binding = &profile.joyHats[i];
|
||||||
|
const RPJoyDeviceState *state =
|
||||||
|
(binding->device >= 0 && binding->device < BindJoyDeviceSlots)
|
||||||
|
? RPJoyDevice(joyDevice[binding->device]) : NULL;
|
||||||
|
if (state != NULL &&
|
||||||
|
JoyHatHeld(state->hat[binding->hat], binding->direction))
|
||||||
|
{
|
||||||
|
if (binding->address < buttonUnits)
|
||||||
|
{
|
||||||
|
desired[binding->address] = 1;
|
||||||
|
}
|
||||||
|
else if (binding->address >= 0x50 &&
|
||||||
|
binding->address < 0x50 + keypadUnits)
|
||||||
|
{
|
||||||
|
keypadDesired[binding->address - 0x50] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < buttonUnits; ++i)
|
for (int i = 0; i < buttonUnits; ++i)
|
||||||
{
|
{
|
||||||
if (screenButton[i])
|
if (screenButton[i])
|
||||||
@@ -502,6 +651,68 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Joystick axes. A physical throttle lever is the one source that
|
||||||
|
// does not add into the pile: it has an absolute position, so its
|
||||||
|
// full travel IS the channel and it takes ownership rather than
|
||||||
|
// nudging an accumulator that a spring-centred pad stick has to.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
Logical throttleLever = False;
|
||||||
|
Scalar throttleLeverValue = (Scalar) 0;
|
||||||
|
|
||||||
|
if (joyLive)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < profile.joyAxisCount; ++i)
|
||||||
|
{
|
||||||
|
const PadJoyAxisBinding *binding = &profile.joyAxes[i];
|
||||||
|
if (binding->device < 0 || binding->device >= BindJoyDeviceSlots)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const RPJoyDeviceState *state = RPJoyDevice(joyDevice[binding->device]);
|
||||||
|
if (state == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Scalar raw = (Scalar) state->axis[binding->source];
|
||||||
|
if (binding->invert)
|
||||||
|
{
|
||||||
|
raw = -raw;
|
||||||
|
}
|
||||||
|
if (binding->axis == BindAxisThrottle && binding->rate == 0.0f)
|
||||||
|
{
|
||||||
|
// -1..1 of lever travel onto the 0..1 the pod runs on
|
||||||
|
throttleLeverValue = (raw + 1.0f) * 0.5f;
|
||||||
|
throttleLever = True;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Scalar value = JoyAxisValue(raw, binding->deadzone);
|
||||||
|
if (binding->rate > 0.0f)
|
||||||
|
{
|
||||||
|
rate[binding->axis] += value * binding->rate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deflect[binding->axis] += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The composite pedal axis becomes the pair the pod actually has.
|
||||||
|
// One signed source presses one pedal or the other, never both,
|
||||||
|
// which is what a rudder bar or a twist grip does.
|
||||||
|
//
|
||||||
|
Scalar pedals = deflect[BindAxisPedals];
|
||||||
|
if (pedals > 0.0f)
|
||||||
|
{
|
||||||
|
deflect[BindAxisRightPedal] += pedals;
|
||||||
|
}
|
||||||
|
else if (pedals < 0.0f)
|
||||||
|
{
|
||||||
|
deflect[BindAxisLeftPedal] += -pedals;
|
||||||
|
}
|
||||||
|
|
||||||
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
|
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
|
||||||
|
|
||||||
Scalar x = deflect[BindAxisJoystickX];
|
Scalar x = deflect[BindAxisJoystickX];
|
||||||
@@ -511,7 +722,9 @@ void
|
|||||||
if (y > 1.0f) y = 1.0f;
|
if (y > 1.0f) y = 1.0f;
|
||||||
if (y < -1.0f) y = -1.0f;
|
if (y < -1.0f) y = -1.0f;
|
||||||
|
|
||||||
Throttle = Clamp01(throttleAccum + deflect[BindAxisThrottle]);
|
Throttle = throttleLever
|
||||||
|
? Clamp01(throttleLeverValue)
|
||||||
|
: Clamp01(throttleAccum + deflect[BindAxisThrottle]);
|
||||||
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
|
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
|
||||||
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
|
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
|
||||||
// The profile encodes the pod's stick sign convention; L4PADFLIP
|
// The profile encodes the pod's stick sign convention; L4PADFLIP
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "l4plasmascreen.h"
|
#include "l4plasmascreen.h"
|
||||||
#include "l4app.h"
|
#include "l4app.h"
|
||||||
|
#include "l4mfdview.h" // RPWindowLayout_*
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,15 @@ namespace
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
//
|
||||||
|
// Dragged to a new spot. Write the whole arrangement now rather
|
||||||
|
// than trusting teardown, so a hard kill still leaves what was
|
||||||
|
// on screen. No-op unless RP412MFDLAYOUT is save.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
return 0;
|
||||||
|
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
// The glass is part of the cockpit; just hide it.
|
// The glass is part of the cockpit; just hide it.
|
||||||
ShowWindow(hwnd, SW_HIDE);
|
ShowWindow(hwnd, SW_HIDE);
|
||||||
@@ -176,6 +186,7 @@ PlasmaScreen::~PlasmaScreen()
|
|||||||
}
|
}
|
||||||
if (window != NULL)
|
if (window != NULL)
|
||||||
{
|
{
|
||||||
|
RPWindowLayout_Forget(window);
|
||||||
RemovePropA((HWND) window, "PlasmaBitmapInfo");
|
RemovePropA((HWND) window, "PlasmaBitmapInfo");
|
||||||
RemovePropA((HWND) window, "PlasmaPixels");
|
RemovePropA((HWND) window, "PlasmaPixels");
|
||||||
DestroyWindow((HWND) window);
|
DestroyWindow((HWND) window);
|
||||||
@@ -275,6 +286,22 @@ void
|
|||||||
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
|
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
|
||||||
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
||||||
activeInstance = this;
|
activeInstance = this;
|
||||||
|
|
||||||
|
//
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Join the saved layout. Position only, like the display
|
||||||
|
// panes: the glass is 128x32 at L4PLASMASCALE, so its size
|
||||||
|
// is a setting rather than something to drag.
|
||||||
|
//
|
||||||
|
// Loading here rather than leaving it to SVGA16 keeps this
|
||||||
|
// independent of which is built first - the glass comes from
|
||||||
|
// the gauge renderer, the panes from the video mode, and
|
||||||
|
// whichever runs second re-applies a placement the first
|
||||||
|
// already has.
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
//
|
||||||
|
RPWindowLayout_Register(window, "Plasma Display", False);
|
||||||
|
RPWindowLayout_Load();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
+744
-78
@@ -3821,6 +3821,524 @@ static LRESULT CALLBACK
|
|||||||
return CallWindowProcA(gViewscreenBaseProc, hwnd, message, wParam, lParam);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// WM_EXITSIZEMOVE is not handled here: RPL4.CPP's own WndProc takes
|
||||||
|
// it, and this chains straight through to that, so the shell saves on
|
||||||
|
// every finished drag whether or not a cockpit has been built.
|
||||||
|
//
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Clear the glass off the screen for the Winners Circle.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
SVGA16::HideSecondaryDisplays()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
if (!splitViews)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int view = 0; view < SplitViewCount; ++view)
|
||||||
|
{
|
||||||
|
if (splitView[view] != NULL)
|
||||||
|
{
|
||||||
|
splitView[view]->Hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "SVGA16: secondary displays hidden for the podium\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// 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(
|
SVGA16::SVGA16(
|
||||||
int mode,
|
int mode,
|
||||||
int init_width,
|
int init_width,
|
||||||
@@ -3842,11 +4360,19 @@ SVGA16::SVGA16(
|
|||||||
//------------------------------------------------------------------
|
//------------------------------------------------------------------
|
||||||
splitViews = False;
|
splitViews = False;
|
||||||
cockpitViewscreen = NULL;
|
cockpitViewscreen = NULL;
|
||||||
|
Logical explodedViews = False;
|
||||||
{
|
{
|
||||||
const char *split_string = getenv("L4MFDSPLIT");
|
const char *split_string = getenv("L4MFDSPLIT");
|
||||||
if (split_string != NULL && atoi(split_string) != 0)
|
if (split_string != NULL && atoi(split_string) != 0)
|
||||||
{
|
{
|
||||||
splitViews = True;
|
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)
|
for (int view = 0; view < SplitViewCount; ++view)
|
||||||
@@ -3856,7 +4382,97 @@ SVGA16::SVGA16(
|
|||||||
|
|
||||||
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
|
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);
|
||||||
|
|
||||||
|
//
|
||||||
|
// ...and then let RP412MFDLAYOUT put everything back where it was
|
||||||
|
// dragged to last time. Windows the file does not mention keep the
|
||||||
|
// arrangement just computed above. The game window is registered
|
||||||
|
// already - RPL4.CPP does that before the console screen ever
|
||||||
|
// shows - so this picks it up alongside the panes.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Load();
|
||||||
|
}
|
||||||
|
else if (splitViews)
|
||||||
{
|
{
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// The 1920x1080 internal cockpit canvas.
|
// The 1920x1080 internal cockpit canvas.
|
||||||
@@ -3876,8 +4492,20 @@ SVGA16::SVGA16(
|
|||||||
|
|
||||||
HWND cockpit = ApplicationManager::GetCurrentManager()->GetHWnd();
|
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;
|
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;
|
RECT outer, inner;
|
||||||
GetWindowRect(cockpit, &outer);
|
GetWindowRect(cockpit, &outer);
|
||||||
@@ -3888,22 +4516,46 @@ SVGA16::SVGA16(
|
|||||||
|
|
||||||
RECT work;
|
RECT work;
|
||||||
work.left = 0; work.top = 0; work.right = 1920; work.bottom = 1080;
|
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_w = (work.right - work.left) - chrome_w;
|
||||||
int avail_h = (work.bottom - work.top) - chrome_h;
|
int avail_h = (work.bottom - work.top) - chrome_h;
|
||||||
|
|
||||||
int scale_num = 100;
|
//-----------------------------------------------------------
|
||||||
if (avail_w < canvas_w || avail_h < canvas_h)
|
// One uniform scale, up or down: the canvas keeps its 16:9
|
||||||
{
|
// shape whatever the monitor is, so a wider-than-16:9 desktop
|
||||||
int fit_w = (avail_w * 100) / canvas_w;
|
// letterboxes instead of stretching. Scaling UP is free
|
||||||
int fit_h = (avail_h * 100) / canvas_h;
|
// quality on the MFDs - their glass is a downscale of a
|
||||||
scale_num = (fit_w < fit_h) ? fit_w : fit_h;
|
// native 640x480 channel until about 200%.
|
||||||
if (scale_num < 25) scale_num = 25;
|
//-----------------------------------------------------------
|
||||||
|
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
|
DEBUG_STREAM << "SVGA16: cockpit canvas at " << scale_num
|
||||||
<< "% for the " << (work.right - work.left) << "x"
|
<< "% for the " << (work.right - work.left) << "x"
|
||||||
<< (work.bottom - work.top) << " work area\n" << std::flush;
|
<< (work.bottom - work.top) << " work area\n" << std::flush;
|
||||||
}
|
|
||||||
|
|
||||||
#define COCKPIT_CANVAS(v) (((v) * scale_num) / 100)
|
#define COCKPIT_CANVAS(v) (((v) * scale_num) / 100)
|
||||||
|
|
||||||
@@ -3915,12 +4567,35 @@ SVGA16::SVGA16(
|
|||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
if (cockpit != NULL)
|
if (cockpit != NULL)
|
||||||
{
|
{
|
||||||
SetWindowLongPtrA(cockpit, GWL_STYLE,
|
LONG_PTR style = GetWindowLongPtrA(cockpit, GWL_STYLE);
|
||||||
GetWindowLongPtrA(cockpit, GWL_STYLE) | WS_CLIPCHILDREN);
|
|
||||||
|
|
||||||
SetWindowPos(cockpit, NULL, work.left, work.top,
|
if (fit_display)
|
||||||
client_w + chrome_w, client_h + chrome_h,
|
{
|
||||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
//-------------------------------------------------------
|
||||||
|
// 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 +4636,78 @@ SVGA16::SVGA16(
|
|||||||
// 0x37.. / lower left 0x0F.. / lower right 0x07.. and the map
|
// 0x37.. / lower left 0x0F.. / lower right 0x07.. and the map
|
||||||
// flanked by Secondary 0x10-0x15 / Screen 0x18-0x1D.
|
// flanked by Secondary 0x10-0x15 / Screen 0x18-0x1D.
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// all five MFDs at the compact size, corners + top center
|
// each display at whatever the player asked for it
|
||||||
int top_glass_w = COCKPIT_CANVAS(320);
|
GlassSize glass[SplitViewCount];
|
||||||
int top_glass_h = COCKPIT_CANVAS(240);
|
CockpitGlassSizes(scale_num, glass);
|
||||||
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);
|
|
||||||
|
|
||||||
splitView[SplitMFDUpperLeft] = new MFDSplitView(
|
splitView[SplitMFDUpperLeft] = new MFDSplitView(
|
||||||
"MFD upper left", init_width, init_height,
|
"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);
|
MFDSplitView::MFDStrips, 0x2F, 0, cockpit);
|
||||||
splitView[SplitMFDUpperCenter] = new MFDSplitView(
|
splitView[SplitMFDUpperCenter] = new MFDSplitView(
|
||||||
"MFD upper center", init_width, init_height,
|
"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);
|
MFDSplitView::MFDStrips, 0x27, 0, cockpit);
|
||||||
splitView[SplitMFDUpperRight] = new MFDSplitView(
|
splitView[SplitMFDUpperRight] = new MFDSplitView(
|
||||||
"MFD upper right", init_width, init_height,
|
"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);
|
MFDSplitView::MFDStrips, 0x37, 0, cockpit);
|
||||||
splitView[SplitMFDLowerLeft] = new MFDSplitView(
|
splitView[SplitMFDLowerLeft] = new MFDSplitView(
|
||||||
"MFD lower left", init_width, init_height,
|
"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);
|
MFDSplitView::MFDStrips, 0x0F, 0, cockpit);
|
||||||
splitView[SplitMFDLowerRight] = new MFDSplitView(
|
splitView[SplitMFDLowerRight] = new MFDSplitView(
|
||||||
"MFD lower right", init_width, init_height,
|
"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);
|
MFDSplitView::MFDStrips, 0x07, 0, cockpit);
|
||||||
// map is portrait: source rotated 90 degrees clockwise
|
// map is portrait: source rotated 90 degrees clockwise
|
||||||
splitView[SplitMap] = new MFDSplitView(
|
splitView[SplitMap] = new MFDSplitView(
|
||||||
"Map", init_height, init_width,
|
"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);
|
MFDSplitView::SideColumns, 0x10, 0x18, cockpit);
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// Place the panes from their measured sizes: top row edge to
|
// Everything above built the panes at a starting size; the
|
||||||
// edge across the canvas, bottom cluster on the bottom edge.
|
// 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)
|
activeCockpit = this;
|
||||||
{
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// The panes go OVER the main display (pod bezel occlusion): pin
|
if (cockpit != NULL)
|
||||||
// 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,
|
RECT inner;
|
||||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
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);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Sticky placement for the shell. Position AND size: nothing
|
||||||
|
// derives that size here - -res only decides how sharp the
|
||||||
|
// scene is - so a window sized to suit a monitor should come
|
||||||
|
// back that way. RPL4.CPP registered it before the console
|
||||||
|
// screen; this reload undoes the sizing just done above, and
|
||||||
|
// runs after the subclass on purpose so its WM_SIZE puts
|
||||||
|
// LayoutCockpit over the restored client area.
|
||||||
|
//
|
||||||
|
// -fit sits it borderless over the whole monitor, so there is
|
||||||
|
// no placement of the player's to remember.
|
||||||
|
//
|
||||||
|
if (!fit_display)
|
||||||
|
{
|
||||||
|
RPWindowLayout_Load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LayoutCockpit(client_w, client_h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the plasma glass sits out for now
|
// the plasma glass sits out for now
|
||||||
PlasmaScreen::Hide();
|
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
|
#undef COCKPIT_CANVAS
|
||||||
}
|
}
|
||||||
//STUBBED: VIDEO RB 1/15/07
|
//STUBBED: VIDEO RB 1/15/07
|
||||||
@@ -4236,6 +4894,14 @@ SVGA16::~SVGA16()
|
|||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
//SVGASetSplitterClock(False);
|
//SVGASetSplitterClock(False);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Backstop for the sticky placement: every finished drag has already
|
||||||
|
// been written, but a window moved and then closed straight away would
|
||||||
|
// otherwise be missed. Must run before the panes go - it reads their
|
||||||
|
// live window rects, and the game window's.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
|
||||||
for (int view = 0; view < SplitViewCount; ++view)
|
for (int view = 0; view < SplitViewCount; ++view)
|
||||||
{
|
{
|
||||||
delete splitView[view];
|
delete splitView[view];
|
||||||
|
|||||||
@@ -308,6 +308,26 @@ private:
|
|||||||
SplitViewCount
|
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
|
void
|
||||||
FillSplitMFD(
|
FillSplitMFD(
|
||||||
SplitViewID view,
|
SplitViewID view,
|
||||||
@@ -315,12 +335,42 @@ private:
|
|||||||
int shift
|
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; }
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Take the six secondary displays off screen and leave the whole
|
||||||
|
// canvas to the viewscreen. The Winners Circle wants the podium
|
||||||
|
// uncluttered, and the panes are only bezels over the 3D - hiding
|
||||||
|
// them uncovers what is already being drawn behind.
|
||||||
|
//
|
||||||
|
// There is no matching show: the mission is over by the time this
|
||||||
|
// is called, and the next race builds a fresh cockpit.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
HideSecondaryDisplays();
|
||||||
|
|
||||||
|
protected:
|
||||||
Logical
|
Logical
|
||||||
splitViews;
|
splitViews;
|
||||||
MFDSplitView
|
MFDSplitView
|
||||||
*splitView[SplitViewCount];
|
*splitView[SplitViewCount];
|
||||||
HWND
|
HWND
|
||||||
cockpitViewscreen; // child pane the 3D scene presents into
|
cockpitViewscreen; // child pane the 3D scene presents into
|
||||||
|
|
||||||
|
static SVGA16
|
||||||
|
*activeCockpit; // the instance owning the cockpit window
|
||||||
};
|
};
|
||||||
|
|
||||||
//########################################################################
|
//########################################################################
|
||||||
|
|||||||
+437
-22
@@ -1797,6 +1797,19 @@ DPLRenderer::DPLRenderer(
|
|||||||
backgroundGreen = 0.0f;
|
backgroundGreen = 0.0f;
|
||||||
backgroundBlue = 0.0f;
|
backgroundBlue = 0.0f;
|
||||||
viewAngle = 30.0f;
|
viewAngle = 30.0f;
|
||||||
|
// the Winners Circle camera is off until the podium asks for it. No
|
||||||
|
// restore path is needed: a fresh DPLRenderer is built per mission, so
|
||||||
|
// the next race re-reads viewangle from RPDPL.INI.
|
||||||
|
mPresentationCamera = False;
|
||||||
|
mPresentationFog = False;
|
||||||
|
mPresentationAspect = 0.0f;
|
||||||
|
mPresentationFadeTime = 0.0f;
|
||||||
|
mPresentationFogRed = 0.0f;
|
||||||
|
mPresentationFogGreen = 0.0f;
|
||||||
|
mPresentationFogBlue = 0.0f;
|
||||||
|
mPresentationFogNear = 0.0f;
|
||||||
|
mPresentationFogFar = 0.0f;
|
||||||
|
D3DXMatrixIdentity(&mPresentationView);
|
||||||
dplMainView = NULL;
|
dplMainView = NULL;
|
||||||
dplDeathZone = NULL;
|
dplDeathZone = NULL;
|
||||||
dplMainZone = NULL;
|
dplMainZone = NULL;
|
||||||
@@ -2343,10 +2356,53 @@ void
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case winnersCircleFogStyle:
|
case winnersCircleFogStyle:
|
||||||
|
//
|
||||||
|
// The presentation shot, restored from the DPL body below. The
|
||||||
|
// stand sits far off the track in open ground, so the track's own
|
||||||
|
// fog leaves it in the dark - this is the lighter blue-violet the
|
||||||
|
// original used to lift it, with the fog pushed back to 100/1050.
|
||||||
//
|
//
|
||||||
// HACK!! This really shouldn't reset the clip planes, but since
|
// HACK!! This really shouldn't reset the clip planes, but since
|
||||||
// it only happens at the end of the review, it should be safe for now.
|
// it only happens at the end of the review, it should be safe for now.
|
||||||
//
|
//
|
||||||
|
fogRed = 0.32f;
|
||||||
|
fogGreen = 0.30f;
|
||||||
|
fogBlue = 0.65f;
|
||||||
|
fogNear = 100.0f;
|
||||||
|
fogFar = 1050.0f;
|
||||||
|
|
||||||
|
// the per-frame FOGSTART/FOGEND come from these
|
||||||
|
currentFogNear = fogNear;
|
||||||
|
currentFogFar = fogFar;
|
||||||
|
|
||||||
|
clipNear = 0.25f;
|
||||||
|
clipFar = 1100.0f;
|
||||||
|
|
||||||
|
// tells the end-of-mission fade to stand down
|
||||||
|
mPresentationFog = True;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Crop to the shape the stand was composed for. 4:3 unless
|
||||||
|
// RP412PODIUMASPECT says otherwise; 0 turns it off and lets
|
||||||
|
// the podium run full width.
|
||||||
|
//
|
||||||
|
mPresentationAspect = 4.0f / 3.0f;
|
||||||
|
{
|
||||||
|
const char *aspect = getenv("RP412PODIUMASPECT");
|
||||||
|
if (aspect != NULL)
|
||||||
|
{
|
||||||
|
mPresentationAspect = (float) atof(aspect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unconditional: fogUpdating is off while a vehicle drives its own
|
||||||
|
// headlight fog, and the podium overrides all of that
|
||||||
|
if (mDevice != NULL)
|
||||||
|
{
|
||||||
|
mDevice->SetRenderState(D3DRS_FOGCOLOR,
|
||||||
|
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
|
||||||
|
(int)(255 * fogBlue)));
|
||||||
|
}
|
||||||
// dpl_SetViewClipPlanes ( dplMainView, 0.25f, 1100.0f );
|
// dpl_SetViewClipPlanes ( dplMainView, 0.25f, 1100.0f );
|
||||||
// dpl_SetViewFog(
|
// dpl_SetViewFog(
|
||||||
// dplMainView,
|
// dplMainView,
|
||||||
@@ -5608,6 +5664,24 @@ void
|
|||||||
intersect_mask = INTERSECT_ALL;
|
intersect_mask = INTERSECT_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412RENDERDIAG=1: name what actually gets built, so a missing
|
||||||
|
// Winners Circle can be told from one that is simply out of shot.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static const char *diag = getenv("RP412RENDERDIAG");
|
||||||
|
if (diag != NULL && atoi(diag) != 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "RenderDiag: building class"
|
||||||
|
<< entity->GetClassID() << " res "
|
||||||
|
<< entity->GetResourceID() << " at "
|
||||||
|
<< entity->localOrigin.linearPosition.x << ","
|
||||||
|
<< entity->localOrigin.linearPosition.y << ","
|
||||||
|
<< entity->localOrigin.linearPosition.z
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logical first_object = True;
|
Logical first_object = True;
|
||||||
|
|
||||||
video_iterator.First();
|
video_iterator.First();
|
||||||
@@ -5997,12 +6071,110 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
if (mCamShipHUD)
|
if (mCamShipHUD)
|
||||||
mCamShipHUD->Execute();
|
mCamShipHUD->Execute();
|
||||||
|
|
||||||
|
//
|
||||||
|
// The eye renderable has just written D3DTS_VIEW from the viewpoint
|
||||||
|
// entity. For the Winners Circle the shot comes from off the stand
|
||||||
|
// instead, so stomp it here - before the read-back below that feeds
|
||||||
|
// every draw call.
|
||||||
|
//
|
||||||
|
if (mPresentationCamera)
|
||||||
|
{
|
||||||
|
mDevice->SetTransform(D3DTS_VIEW, &mPresentationView);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412RENDERDIAG=1: report what the frame is actually made of once the
|
||||||
|
// mission is ending, which is the only way to tell an empty scene from a
|
||||||
|
// misaimed camera.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static const char *diag = getenv("RP412RENDERDIAG");
|
||||||
|
static int reported = 0;
|
||||||
|
if (diag != NULL && atoi(diag) != 0 &&
|
||||||
|
currentAppState == Application::EndingMission && reported < 4)
|
||||||
|
{
|
||||||
|
++reported;
|
||||||
|
int dynamic_count = 0;
|
||||||
|
SChainIteratorOf<HierarchicalDrawComponent*> diag_iter(&mRenderables);
|
||||||
|
while (diag_iter.ReadAndNext() != NULL)
|
||||||
|
{
|
||||||
|
++dynamic_count;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "RenderDiag: ending frame " << reported
|
||||||
|
<< " dead=" << (l4_application->IsDead() ? 1 : 0)
|
||||||
|
<< " statics=" << (int) mConsolidatedStaticObjects.size()
|
||||||
|
<< " renderables=" << dynamic_count
|
||||||
|
<< " camera=" << (mPresentationCamera ? 1 : 0)
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gNumBatches = 0;
|
gNumBatches = 0;
|
||||||
static Time lastFrameTime = mTargetRenderTime;
|
static Time lastFrameTime = mTargetRenderTime;
|
||||||
Scalar dT = mTargetRenderTime - lastFrameTime;
|
Scalar dT = mTargetRenderTime - lastFrameTime;
|
||||||
lastFrameTime = mTargetRenderTime;
|
lastFrameTime = mTargetRenderTime;
|
||||||
currentFrameTime = Now();
|
currentFrameTime = Now();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Fade the presentation up out of black, if one is running. Same lever
|
||||||
|
// the end-of-mission fade pulls: scale the fog colour and both fog
|
||||||
|
// distances, here from nothing up to what the podium asked for.
|
||||||
|
//
|
||||||
|
if (mPresentationFadeTime > 0.0f)
|
||||||
|
{
|
||||||
|
Scalar remaining = mPresentationFadeEnd - Now();
|
||||||
|
Scalar fade = 1.0f - (remaining / mPresentationFadeTime);
|
||||||
|
if (fade >= 1.0f)
|
||||||
|
{
|
||||||
|
fade = 1.0f;
|
||||||
|
mPresentationFadeTime = 0.0f; // done
|
||||||
|
}
|
||||||
|
if (fade < 0.0f)
|
||||||
|
{
|
||||||
|
fade = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
fogRed = mPresentationFogRed * fade;
|
||||||
|
fogGreen = mPresentationFogGreen * fade;
|
||||||
|
fogBlue = mPresentationFogBlue * fade;
|
||||||
|
currentFogNear = mPresentationFogNear * fade;
|
||||||
|
currentFogFar = mPresentationFogFar * fade;
|
||||||
|
|
||||||
|
mDevice->SetRenderState(D3DRS_FOGCOLOR,
|
||||||
|
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
|
||||||
|
(int)(255 * fogBlue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Pillarbox: black the whole target first, then narrow the viewport so
|
||||||
|
// everything after this - clear, scene, reticle - lands inside the crop
|
||||||
|
// and the surround stays black. Restored after Present.
|
||||||
|
//
|
||||||
|
if (mPresentationAspect > 0.0f)
|
||||||
|
{
|
||||||
|
D3DVIEWPORT9 full;
|
||||||
|
full.X = 0;
|
||||||
|
full.Y = 0;
|
||||||
|
full.Width = mPresentParams.BackBufferWidth;
|
||||||
|
full.Height = mPresentParams.BackBufferHeight;
|
||||||
|
full.MinZ = 0.0f;
|
||||||
|
full.MaxZ = 1.0f;
|
||||||
|
mDevice->SetViewport(&full);
|
||||||
|
mDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
|
||||||
|
|
||||||
|
DWORD cropped = (DWORD)
|
||||||
|
(mPresentParams.BackBufferHeight * mPresentationAspect);
|
||||||
|
if (cropped > mPresentParams.BackBufferWidth)
|
||||||
|
{
|
||||||
|
cropped = mPresentParams.BackBufferWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DVIEWPORT9 crop = full;
|
||||||
|
crop.Width = cropped;
|
||||||
|
crop.X = (mPresentParams.BackBufferWidth - cropped) / 2;
|
||||||
|
mDevice->SetViewport(&crop);
|
||||||
|
}
|
||||||
|
|
||||||
DWORD currentFog;
|
DWORD currentFog;
|
||||||
mDevice->GetRenderState(D3DRS_FOGCOLOR, ¤tFog);
|
mDevice->GetRenderState(D3DRS_FOGCOLOR, ¤tFog);
|
||||||
hr = mDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, currentFog, 1.0f, 0);
|
hr = mDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, currentFog, 1.0f, 0);
|
||||||
@@ -6152,15 +6324,70 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
|
mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
|
||||||
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
||||||
|
|
||||||
if (mReticle && !l4_application->IsDead())
|
//
|
||||||
|
// No gunsight on the podium - the race is over and nothing is being
|
||||||
|
// aimed at. It is drawn in the 2D pass, so it survives everything else
|
||||||
|
// the presentation turns off.
|
||||||
|
//
|
||||||
|
if (mReticle && !l4_application->IsDead() && !InPresentation())
|
||||||
mReticle->Render(0, &viewTransform);
|
mReticle->Render(0, &viewTransform);
|
||||||
|
|
||||||
if (mCamShipHUD)
|
if (mCamShipHUD && !InPresentation())
|
||||||
mCamShipHUD->Render(0, &viewTransform);
|
mCamShipHUD->Render(0, &viewTransform);
|
||||||
|
|
||||||
hr = mDevice->EndScene();
|
hr = mDevice->EndScene();
|
||||||
|
|
||||||
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
|
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
|
||||||
|
|
||||||
|
// hand the whole target back
|
||||||
|
if (mPresentationAspect > 0.0f)
|
||||||
|
{
|
||||||
|
D3DVIEWPORT9 full;
|
||||||
|
full.X = 0;
|
||||||
|
full.Y = 0;
|
||||||
|
full.Width = mPresentParams.BackBufferWidth;
|
||||||
|
full.Height = mPresentParams.BackBufferHeight;
|
||||||
|
full.MinZ = 0.0f;
|
||||||
|
full.MaxZ = 1.0f;
|
||||||
|
mDevice->SetViewport(&full);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412RENDERDIAG=1: did the ending frame actually reach the window, and
|
||||||
|
// what colour did it clear to? A black clear with a blue-violet fog set
|
||||||
|
// means something is overwriting the fog behind us.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static const char *diag = getenv("RP412RENDERDIAG");
|
||||||
|
static int presented = 0;
|
||||||
|
if (diag != NULL && atoi(diag) != 0 &&
|
||||||
|
application->GetApplicationState() == Application::EndingMission &&
|
||||||
|
presented < 4)
|
||||||
|
{
|
||||||
|
++presented;
|
||||||
|
DWORD fog_now = 0;
|
||||||
|
mDevice->GetRenderState(D3DRS_FOGCOLOR, &fog_now);
|
||||||
|
HWND present_window = (HWND) gMainPresentWindow;
|
||||||
|
RECT present_rect;
|
||||||
|
present_rect.left = present_rect.top = 0;
|
||||||
|
present_rect.right = present_rect.bottom = 0;
|
||||||
|
if (present_window != NULL)
|
||||||
|
{
|
||||||
|
GetClientRect(present_window, &present_rect);
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "RenderDiag: present " << presented
|
||||||
|
<< " hr=0x" << std::hex << (unsigned int) hr << std::dec
|
||||||
|
<< " fogcolor=0x" << std::hex << (unsigned int) fog_now << std::dec
|
||||||
|
<< " window=" << (void*) present_window
|
||||||
|
<< " visible=" << (present_window != NULL &&
|
||||||
|
IsWindowVisible(present_window) ? 1 : 0)
|
||||||
|
<< " rect=" << present_rect.right << "x" << present_rect.bottom
|
||||||
|
<< " backbuf=" << mPresentParams.BackBufferWidth << "x"
|
||||||
|
<< mPresentParams.BackBufferHeight
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hr == D3DERR_DEVICELOST)
|
if (hr == D3DERR_DEVICELOST)
|
||||||
{
|
{
|
||||||
int bbCount = mPresentParams.BackBufferCount;
|
int bbCount = mPresentParams.BackBufferCount;
|
||||||
@@ -6736,27 +6963,167 @@ void
|
|||||||
}
|
}
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
// Un-stubbed for the Winners Circle, which asks for 45 degrees to get all
|
||||||
|
// eight racers in frame. The DPL body below it built a dpl_View; the D3D9
|
||||||
|
// path builds the same two matrices DPLReadINIPage does and pushes them,
|
||||||
|
// because mProjectionMatrix only reaches the device on the LaunchingMission
|
||||||
|
// edge and on the sky-pass restore - rebuilding alone would not show until
|
||||||
|
// the next frame.
|
||||||
|
//
|
||||||
|
// Pass degrees, as the INI does. RPDPL.INI ships viewangle=40, which is what
|
||||||
|
// to hand back afterwards - not the 30 in the constructor, which the INI
|
||||||
|
// overwrites at load.
|
||||||
|
//
|
||||||
void DPLRenderer::SetViewAngle(Degree new_angle)
|
void DPLRenderer::SetViewAngle(Degree new_angle)
|
||||||
{
|
{
|
||||||
//STUBBED: DPL RB 1/14/07
|
Check(this);
|
||||||
//Check(this);
|
|
||||||
////
|
viewAngle = (float) new_angle.angle;
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
//// Convert From Degree To Radian
|
Radian view_angle;
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
view_angle = new_angle;
|
||||||
////
|
// tan(half angle): the culling helper GetViewRatio() reads this, and
|
||||||
//Radian view_angle;
|
// nothing has written it since the DPL body was stubbed out
|
||||||
//view_angle = new_angle;
|
viewRatio = (float) tan(view_angle / 2.0f);
|
||||||
//viewAngle = view_angle;
|
aspectRatio = (float) y_size / (float) x_size;
|
||||||
//viewRatio = tan(viewAngle/2.0f);
|
|
||||||
////
|
// pillarboxed shots render into a narrower viewport, so the projection
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// has to use that shape or the scene comes out squashed rather than
|
||||||
//// Calc Aspect Ratio and Set View Projection
|
// cropped
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
float projection_aspect = (mPresentationAspect > 0.0f)
|
||||||
////
|
? mPresentationAspect
|
||||||
//aspectRatio = (float) y_size / (float) x_size;
|
: ((float) x_size / (float) y_size);
|
||||||
//dpl_SetViewProjection ( dplMainView, -1.0f, -aspectRatio, 1.0f, aspectRatio, 1.0f/viewRatio);
|
|
||||||
//dpl_FlushView(dplMainView);
|
D3DXMatrixIdentity(&mProjectionMatrix);
|
||||||
|
D3DXMatrixPerspectiveFovLH(
|
||||||
|
&mProjectionMatrix,
|
||||||
|
viewAngle * (PI / 180.0f),
|
||||||
|
projection_aspect,
|
||||||
|
clipNear,
|
||||||
|
clipFar);
|
||||||
|
mProjectionMatrix(0, 0) *= -1; // handedness flip - the view is RH
|
||||||
|
|
||||||
|
mDecalProjectionMatrix = mProjectionMatrix;
|
||||||
|
mDecalProjectionMatrix._33 -= mDecalEpsilon;
|
||||||
|
|
||||||
|
if (mDevice != NULL)
|
||||||
|
{
|
||||||
|
mDevice->SetTransform(D3DTS_PROJECTION, &mProjectionMatrix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// The presentation camera. DPLEyeRenderable writes D3DTS_VIEW from the
|
||||||
|
// viewpoint entity every frame; ExecuteImplementation reads it straight back
|
||||||
|
// out of the device and hands it to every draw call. Overriding it between
|
||||||
|
// those two points is enough to move the shot without touching the eye
|
||||||
|
// renderable or building a CameraShip.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DPLRenderer::SetPresentationCamera(const Point3D &eye, const Point3D &look_at)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
D3DXVECTOR3 from((float) eye.x, (float) eye.y, (float) eye.z);
|
||||||
|
D3DXVECTOR3 at((float) look_at.x, (float) look_at.y, (float) look_at.z);
|
||||||
|
|
||||||
|
//
|
||||||
|
// LookAt*LH*, to match the LH projection. The two differ by exactly the
|
||||||
|
// sign of the view direction, and RH here points the camera the opposite
|
||||||
|
// way: ask to look down at the stand and you get sky behind you.
|
||||||
|
//
|
||||||
|
// The engine's own eye renderable does use RH, and is right to - its
|
||||||
|
// forward and up come out of the entity matrix already in that
|
||||||
|
// convention. A camera aimed with plain world coordinates does not, so
|
||||||
|
// it wants the handedness the projection was built with.
|
||||||
|
//
|
||||||
|
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
|
||||||
|
|
||||||
|
D3DXMatrixLookAtLH(&mPresentationView, &from, &at, &up);
|
||||||
|
mPresentationCamera = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DPLRenderer::ClearPresentationCamera()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
mPresentationCamera = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Give the pilot a vehicle to look at. Their own is built insideEntity - a
|
||||||
|
// cockpit and no hull - so on the podium they would be the one empty spot.
|
||||||
|
// outsideEntity is what builds the exterior, and the renderables for it are
|
||||||
|
// added to what is already there.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DPLRenderer::ShowViewpointFromOutside()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
Entity *viewpoint = GetLinkedEntity();
|
||||||
|
if (viewpoint == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: no viewpoint entity to turn around\n"
|
||||||
|
<< std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add the exterior alongside what is already there rather than tearing
|
||||||
|
// the entity down and rebuilding it. The teardown path is safe for
|
||||||
|
// scenery going out of range, but not for the viewpoint entity: it is
|
||||||
|
// never uninteresting, the eye renderable goes with it, and doing it
|
||||||
|
// mid-mission stops the scene rendering altogether.
|
||||||
|
//
|
||||||
|
// This is what NotifyOfNewInterestingEntity does, minus the teardown
|
||||||
|
// and with the view type forced.
|
||||||
|
//
|
||||||
|
Check(application);
|
||||||
|
ResourceFile *resource_file = application->GetResourceFile();
|
||||||
|
Check(resource_file);
|
||||||
|
ResourceDescription *video_resource = resource_file->SearchList(
|
||||||
|
viewpoint->GetResourceID(),
|
||||||
|
ResourceDescription::VideoModelResourceType);
|
||||||
|
|
||||||
|
if (video_resource == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: own vehicle has no exterior model\n"
|
||||||
|
<< std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
video_resource->Lock();
|
||||||
|
MakeEntityRenderables(viewpoint, video_resource, outsideEntity);
|
||||||
|
video_resource->Unlock();
|
||||||
|
|
||||||
|
DEBUG_STREAM << "WinnersCircle: own vehicle given an exterior\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Come up out of the black. SetFogStyle(winnersCircleFogStyle) has already
|
||||||
|
// put the target fog in place, so remember it and ramp toward it - the same
|
||||||
|
// multiply the end-of-mission fade uses, run the other way.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DPLRenderer::StartPresentationFadeIn(Scalar seconds)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
mPresentationFogRed = fogRed;
|
||||||
|
mPresentationFogGreen = fogGreen;
|
||||||
|
mPresentationFogBlue = fogBlue;
|
||||||
|
mPresentationFogNear = currentFogNear;
|
||||||
|
mPresentationFogFar = currentFogFar;
|
||||||
|
|
||||||
|
mPresentationFadeTime = (seconds > 0.0f) ? seconds : 0.01f;
|
||||||
|
mPresentationFadeEnd = Now();
|
||||||
|
mPresentationFadeEnd += mPresentationFadeTime;
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -6800,8 +7167,56 @@ void DPLRenderer::SortAndReloadNameBitmaps()
|
|||||||
LoadBitSliceTexture(name_bitmap, mNameTextures[index]);
|
LoadBitSliceTexture(name_bitmap, mNameTextures[index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadOrdinalBitmaps();
|
LoadOrdinalBitmaps();
|
||||||
|
|
||||||
|
BindNamePlates();
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Put the callsigns on the Winners Circle plates.
|
||||||
|
//
|
||||||
|
// mNameTextures is indexed by finishing place (rank + 1), which is how the
|
||||||
|
// signs are numbered, so the plate beside each spot names whoever is standing
|
||||||
|
// on it. The plates are static geometry and have already been merged into the
|
||||||
|
// consolidated mesh by now, so it is the consolidated draw ops that have to be
|
||||||
|
// re-pointed - the objects they were built from are no longer drawn.
|
||||||
|
//
|
||||||
|
void DPLRenderer::BindNamePlates()
|
||||||
|
{
|
||||||
|
int name_count = (int) (sizeof(mNameTextures) / sizeof(mNameTextures[0]));
|
||||||
|
int bound = 0;
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
std::list<d3d_OBJECT*>::const_iterator iter;
|
||||||
|
for (iter = mConsolidatedStaticObjects.begin();
|
||||||
|
iter != mConsolidatedStaticObjects.end(); ++iter)
|
||||||
|
{
|
||||||
|
d3d_OBJECT *object = *iter;
|
||||||
|
if (object == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int op = 0; op < object->GetDrawOpCount(); ++op)
|
||||||
|
{
|
||||||
|
L4DRAWOP *draw_op = object->GetDrawOp(op);
|
||||||
|
int plate = d3d_OBJECT::NamePlateFor(draw_op->texture.texture);
|
||||||
|
if (plate == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++found;
|
||||||
|
if (plate < name_count && mNameTextures[plate] != NULL)
|
||||||
|
{
|
||||||
|
draw_op->texture.texture = mNameTextures[plate];
|
||||||
|
++bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_STREAM << "NamePlates: " << bound << " of " << found
|
||||||
|
<< " plates given a callsign\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -310,10 +310,57 @@ public:
|
|||||||
|
|
||||||
void SetViewAngle(Degree new_angle);
|
void SetViewAngle(Degree new_angle);
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Winners Circle presentation camera.
|
||||||
|
//
|
||||||
|
// The eye normally rides the viewpoint entity - your own vehicle -
|
||||||
|
// and DPLEyeRenderable writes D3DTS_VIEW from it each frame. For the
|
||||||
|
// podium the shot has to come from off the stand looking back, with
|
||||||
|
// every racer including you in frame, so this overrides that view
|
||||||
|
// for as long as it is set. Culling still runs from the viewpoint
|
||||||
|
// entity, which is standing on the podium, so the stand and everyone
|
||||||
|
// on it stay resident.
|
||||||
|
//
|
||||||
|
// Eye and target are world space. ClearPresentationCamera() hands
|
||||||
|
// the view back to the entity.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void SetPresentationCamera(const Point3D &eye, const Point3D &look_at);
|
||||||
|
void ClearPresentationCamera();
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// True once the Winners Circle has taken the screen. The end-of-
|
||||||
|
// mission fade-to-black checks this and stands down: it multiplies
|
||||||
|
// the fog to black over FADE_OUT_TIME, which would otherwise black
|
||||||
|
// out the podium no matter what is drawn.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
Logical InPresentation() const { return mPresentationFog; }
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Bring the presentation up out of black over the given seconds. The
|
||||||
|
// race's own fade-to-black has already run by this point; this is
|
||||||
|
// its mirror image, ramping the fog colour and both fog distances
|
||||||
|
// back up from nothing to what the Winners Circle asked for.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void StartPresentationFadeIn(Scalar seconds);
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Rebuild the viewpoint entity - the pilot's own vehicle - as an
|
||||||
|
// exterior. It is normally built insideEntity, which is a cockpit
|
||||||
|
// and no hull, so from any camera that is not in it you see nothing
|
||||||
|
// where your own vehicle should be. Disconnected_Eye is the engine's
|
||||||
|
// own switch for this: "so higher level renderers can fix the eye in
|
||||||
|
// one spot and watch the viewpoint entity drive around."
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void ShowViewpointFromOutside();
|
||||||
|
|
||||||
unsigned int* MakeBitSliceStorage();
|
unsigned int* MakeBitSliceStorage();
|
||||||
|
|
||||||
void SortAndReloadNameBitmaps();
|
void SortAndReloadNameBitmaps();
|
||||||
|
|
||||||
|
// re-point the Winners Circle sign faces at the current callsign
|
||||||
|
// textures; the plates live in the consolidated static mesh
|
||||||
|
void BindNamePlates();
|
||||||
|
|
||||||
void LoadNameBitmaps();
|
void LoadNameBitmaps();
|
||||||
|
|
||||||
void LoadOrdinalBitmaps();
|
void LoadOrdinalBitmaps();
|
||||||
@@ -438,6 +485,22 @@ private:
|
|||||||
D3DXMATRIX mDecalProjectionMatrix;
|
D3DXMATRIX mDecalProjectionMatrix;
|
||||||
float mDecalEpsilon;
|
float mDecalEpsilon;
|
||||||
|
|
||||||
|
// Winners Circle: overrides D3DTS_VIEW while set (see
|
||||||
|
// SetPresentationCamera)
|
||||||
|
D3DXMATRIX mPresentationView;
|
||||||
|
Logical mPresentationCamera;
|
||||||
|
Logical mPresentationFog;
|
||||||
|
// >0 pillarboxes the scene to this aspect. The Winners Circle was
|
||||||
|
// built to be looked at from a 4:3 pod monitor and its platform runs
|
||||||
|
// out at the sides of a 16:9 canvas, so the shot is cropped to the
|
||||||
|
// shape it was composed for and the surround left black.
|
||||||
|
float mPresentationAspect;
|
||||||
|
// fade-in: end time, duration, and the fog it is ramping toward
|
||||||
|
Time mPresentationFadeEnd;
|
||||||
|
Scalar mPresentationFadeTime;
|
||||||
|
float mPresentationFogRed, mPresentationFogGreen, mPresentationFogBlue;
|
||||||
|
float mPresentationFogNear, mPresentationFogFar;
|
||||||
|
|
||||||
void FindBestAdapterIndices(bool isWindowed);
|
void FindBestAdapterIndices(bool isWindowed);
|
||||||
|
|
||||||
float mCloudRed, mCloudGreen, mCloudBlue;
|
float mCloudRed, mCloudGreen, mCloudBlue;
|
||||||
|
|||||||
@@ -2283,6 +2283,20 @@ void
|
|||||||
//
|
//
|
||||||
case FadeOutState:
|
case FadeOutState:
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The Winners Circle has taken the screen, so leave it alone.
|
||||||
|
// This fade multiplies the fog colour and both fog distances
|
||||||
|
// toward zero every frame, which blacks out the whole scene -
|
||||||
|
// correct when the race just ends, fatal to a podium shown
|
||||||
|
// afterwards. Stand down and stop running.
|
||||||
|
//
|
||||||
|
if (myRenderer->InPresentation())
|
||||||
|
{
|
||||||
|
myState = WaitForStartState;
|
||||||
|
myRenderer->RemoveDynamicRenderable(this);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
percent_time_left = (myStateTimer - current_time)/FADE_OUT_TIME;
|
percent_time_left = (myStateTimer - current_time)/FADE_OUT_TIME;
|
||||||
if(percent_time_left <= 0.0f)
|
if(percent_time_left <= 0.0f)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -263,6 +263,7 @@
|
|||||||
<ConformanceMode>true</ConformanceMode>
|
<ConformanceMode>true</ConformanceMode>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include=".\L4PADBINDINGS.cpp" />
|
<ClCompile Include=".\L4PADBINDINGS.cpp" />
|
||||||
|
<ClCompile Include=".\L4JOY.cpp" />
|
||||||
<ClCompile Include=".\L4PADRIO.cpp" />
|
<ClCompile Include=".\L4PADRIO.cpp" />
|
||||||
<ClCompile Include=".\L4PCSPAK.cpp" />
|
<ClCompile Include=".\L4PCSPAK.cpp" />
|
||||||
<ClCompile Include=".\L4PLASMA.cpp" />
|
<ClCompile Include=".\L4PLASMA.cpp" />
|
||||||
@@ -454,6 +455,7 @@
|
|||||||
<ClInclude Include=".\L4NETTRANSPORT.h" />
|
<ClInclude Include=".\L4NETTRANSPORT.h" />
|
||||||
<ClInclude Include=".\L4KEYLIGHT.h" />
|
<ClInclude Include=".\L4KEYLIGHT.h" />
|
||||||
<ClInclude Include=".\L4PADBINDINGS.h" />
|
<ClInclude Include=".\L4PADBINDINGS.h" />
|
||||||
|
<ClInclude Include=".\L4JOY.h" />
|
||||||
<ClInclude Include=".\L4STEAMTRANSPORT.h" />
|
<ClInclude Include=".\L4STEAMTRANSPORT.h" />
|
||||||
<ClInclude Include=".\L4PARTICLES.h" />
|
<ClInclude Include=".\L4PARTICLES.h" />
|
||||||
<ClInclude Include=".\L4MFDVIEW.h" />
|
<ClInclude Include=".\L4MFDVIEW.h" />
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Red Planet 4.12 — the Steamification
|
# Red Planet 4.12 — the Steamification
|
||||||
|
|
||||||
**Red Planet** is VWE's pod-racing game: eight-player VTV races on Mars,
|
**Red Planet** is VWE's pod-racing game: eight-player VTV contests on Mars
|
||||||
originally played from inside full-motion cockpit pods. This repo is the
|
— **Martian Death Race** and **Martian Football** — originally played from
|
||||||
third life of that code:
|
inside VWE Tesla cockpit pods. This repo is the third life of that code:
|
||||||
|
|
||||||
| Generation | What it was |
|
| 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.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.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 sellable on Steam and playable over the internet with no cockpit hardware |
|
| **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
|
intact, with each pod-era dependency replaced by a consumer equivalent
|
||||||
behind the engine's existing seams.
|
behind the engine's existing seams.
|
||||||
|
|
||||||
@@ -19,23 +19,80 @@ behind the engine's existing seams.
|
|||||||
| RIO cockpit board (serial) | **PadRIO** — virtual RIO from XInput pad + keyboard, fully rebindable (`bindings.txt`, vRIO profile format) |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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) |
|
| 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 |
|
| Site network / fixed IPs | **Steam lobbies** — lobby owner is the console; members exchange FakeIPs and loadouts as lobby data |
|
||||||
|
|
||||||
**Status: it works.** Release
|
**Status: it works.**
|
||||||
[v4.12.1](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.1)
|
[v4.12.2](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.2)
|
||||||
carries the first verified end-to-end internet build: three machines, three
|
carried the first verified end-to-end internet build: three machines, three
|
||||||
Steam accounts, lobby → mesh → marshaled five-minute race → deaths and
|
Steam accounts, lobby → mesh → marshaled five-minute race → deaths and
|
||||||
respawns → timed stop → results on every machine → rematch from the same
|
respawns → timed stop → results on every machine → rematch from the same
|
||||||
lobby.
|
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.
|
||||||
|
|
||||||
|
[v4.12.4](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.4)
|
||||||
|
is about the lobby. Both rooms now show the host's mission setup — the track,
|
||||||
|
then time of day, weather and game length — since everyone flies the owner's
|
||||||
|
picks and it was the one thing in the room nobody could see for themselves.
|
||||||
|
The football team sheet names each player's VTV beside their team colours and
|
||||||
|
position. And a lobby holds **eight** players rather than four, a full grid as
|
||||||
|
the pod hall ran it, with the room sizing its roster to whatever space the
|
||||||
|
window gives it.
|
||||||
|
|
||||||
|
[v4.12.5](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.5)
|
||||||
|
restores the **Winners Circle**. The pod hall stood the finishers on a
|
||||||
|
numbered award platform when the race ended, and all of it was still in this
|
||||||
|
repo — the stand, eight ranked spots in every map, and the code to put racers
|
||||||
|
on them — wired only into the mission-review build and so never once run by a
|
||||||
|
pod. The race now fades out and fades back in on the platform: finishers in
|
||||||
|
finishing order, each pilot's callsign on the plate beside their spot, the
|
||||||
|
cockpit glass cleared away, held for a few seconds before the results screen.
|
||||||
|
Three pieces of it had been stubbed out in the D3D9 port and are working
|
||||||
|
again.
|
||||||
|
|
||||||
|
[v4.12.6](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.6)
|
||||||
|
is about the windows. Where you put them now survives the
|
||||||
|
menu-race-menu loop: `RP412MFDLAYOUT` remembers the game window, the
|
||||||
|
exploded view's display panes and the plasma glass in `mfd_layout.cfg`
|
||||||
|
beside `bindings.txt`. Append `,noframe` to a line to take that
|
||||||
|
window's title bar and border off — a cockpit filling the monitor edge
|
||||||
|
to edge at a rect you chose, rather than `-fit` taking the whole
|
||||||
|
screen — and the setup screen carries its own **EXIT GAME** button,
|
||||||
|
since a window with no title bar needs a way out. The Steam buttons
|
||||||
|
now dim and say *STEAM NOT RUNNING* instead of disappearing. And the
|
||||||
|
Winners Circle camera is framed off the award stand itself rather than
|
||||||
|
off whoever is standing on it, so the shot is the same one for every
|
||||||
|
player at every head count.
|
||||||
|
|
||||||
|
[v4.12.7](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.7)
|
||||||
|
is about sticks. Anything that is not an Xbox-class pad — a flight stick,
|
||||||
|
a HOTAS throttle, a twist grip, rudder pedals, a wheel — comes in through
|
||||||
|
DirectInput rather than XInput, and the game could not see any of it.
|
||||||
|
Now it can, and `joyconfig.bat` sets it up: the wizard asks you to move
|
||||||
|
each control in turn, works out which device and axis answered and which
|
||||||
|
way round it reads, and writes the joystick rows of `bindings.txt`,
|
||||||
|
leaving anything you have edited yourself alone. A twist grip or rudder
|
||||||
|
bar drives both pedals through one signed `Pedals` axis, and a real
|
||||||
|
throttle lever owns its channel outright rather than nudging a position
|
||||||
|
the way a spring-centred stick has to. Ported from the sibling BT411.
|
||||||
|
|
||||||
## Playing
|
## Playing
|
||||||
|
|
||||||
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
|
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
|
run `start-windowed.bat` — the game boots into the setup menu, where the
|
||||||
multiplayer: see
|
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
|
[docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) (until RP412
|
||||||
has its own AppID it runs under Spacewar, 480).
|
has its own AppID it runs under Spacewar, 480).
|
||||||
|
|
||||||
@@ -45,7 +102,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),
|
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
|
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
|
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
|
## Building
|
||||||
|
|
||||||
@@ -58,6 +116,7 @@ mission.
|
|||||||
|
|
||||||
| Doc | Contents |
|
| 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-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/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 |
|
| [docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) | Multiplayer test procedure, Steam Input notes, the abort key |
|
||||||
|
|||||||
@@ -11,6 +11,16 @@
|
|||||||
#include "..\munga\hostmgr.h"
|
#include "..\munga\hostmgr.h"
|
||||||
#include "..\munga\nttmgr.h"
|
#include "..\munga\nttmgr.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// How long the mission stays up after the buzzer, for the Winners Circle.
|
||||||
|
// This timer is the only thing holding the simulation and the renderer
|
||||||
|
// open once the race is over - when it runs out the player dispatches the
|
||||||
|
// StopMission that retires the application. The stock 3 seconds is the
|
||||||
|
// fade; the rest is the podium. Kept under the +30s LightsOut post so that
|
||||||
|
// never fires while the stand is up.
|
||||||
|
//
|
||||||
|
const Scalar winnersCircleHoldTime = 11.0f;
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//######################## RPPlayer__StatusMessage ######################
|
//######################## RPPlayer__StatusMessage ######################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -188,6 +198,24 @@ void
|
|||||||
ForceUpdate();
|
ForceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Hold the mission open for the Winners Circle.
|
||||||
|
//
|
||||||
|
// The base handler sets a 3 second fade, and when it runs out the
|
||||||
|
// player dispatches the StopMission that retires the application and
|
||||||
|
// takes the renderer with it. That is the only thing keeping the sim
|
||||||
|
// and the renderer alive after the race, so the podium gets exactly as
|
||||||
|
// long as this timer says. Sim and render both keep running throughout
|
||||||
|
// EndingMission - neither has a case for it that bails out.
|
||||||
|
//
|
||||||
|
// Only on a clean finish: an abort should still leave promptly.
|
||||||
|
//
|
||||||
|
if (application->GetApplicationState() == Application::EndingMission)
|
||||||
|
{
|
||||||
|
fadeTimeRemaining = winnersCircleHoldTime;
|
||||||
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -26,6 +26,8 @@
|
|||||||
#include "rpl4lobby.h"
|
#include "rpl4lobby.h"
|
||||||
#include "..\munga_l4\l4steamtransport.h"
|
#include "..\munga_l4\l4steamtransport.h"
|
||||||
#include "..\munga_l4\l4splr.h"
|
#include "..\munga_l4\l4splr.h"
|
||||||
|
#include "..\munga_l4\l4mfdview.h" // RPWindowLayout_*
|
||||||
|
#include "..\munga_l4\l4joy.h" // RPJoyConfigWizard
|
||||||
#include "rpl4ver.h"
|
#include "rpl4ver.h"
|
||||||
#include "..\munga\resver.h"
|
#include "..\munga\resver.h"
|
||||||
#include "..\munga\resource.h"
|
#include "..\munga\resource.h"
|
||||||
@@ -123,6 +125,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
//
|
||||||
|
// A drag or resize just finished. Here rather than in the
|
||||||
|
// cockpit's subclass so it also covers the console screen, which
|
||||||
|
// is where the window gets moved before there is any cockpit to
|
||||||
|
// subclass. No-op unless RP412MFDLAYOUT is save.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
@@ -177,7 +188,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
fclose(file);
|
fclose(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_STREAM << "Red Planet 4.12.2" << std::endl << std::flush;
|
DEBUG_STREAM << "Red Planet 4.12.7" << std::endl << std::flush;
|
||||||
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
|
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
|
||||||
|
|
||||||
#ifdef RP412_STEAM
|
#ifdef RP412_STEAM
|
||||||
@@ -254,6 +265,43 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
ShowWindow(hWnd, nShowCmd);
|
ShowWindow(hWnd, nShowCmd);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Sticky placement, from the first frame the player sees. SVGA16 also
|
||||||
|
// registers and reloads when it builds the cockpit, but that is not
|
||||||
|
// until a mission starts - without this the console screen would come
|
||||||
|
// up at the default rect with its title bar still on, and only jump
|
||||||
|
// to the saved placement once a race began. -fit takes the whole
|
||||||
|
// monitor and has no placement of the player's to restore.
|
||||||
|
//
|
||||||
|
if (!L4Application::GetFitDisplay() && !L4Application::GetFullscreen())
|
||||||
|
{
|
||||||
|
RPWindowLayout_Register(hWnd, "RPL4", True);
|
||||||
|
RPWindowLayout_Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// RP412JOYCONFIG=1 (joyconfig.bat): the joystick setup wizard, before
|
||||||
|
// anything else claims the screen. It asks the player to move each
|
||||||
|
// control on their stick, HOTAS or pedals and writes the joy* rows of
|
||||||
|
// bindings.txt, then falls through into the game so they can try them
|
||||||
|
// straight away.
|
||||||
|
//
|
||||||
|
// One shot: the variable is cleared from this process so a rebuilt
|
||||||
|
// PadRIO later in the session cannot run the wizard a second time.
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
{
|
||||||
|
const char *joyconfig = getenv("RP412JOYCONFIG");
|
||||||
|
if (joyconfig != NULL && *joyconfig != '\0' && atoi(joyconfig) != 0)
|
||||||
|
{
|
||||||
|
RPJoyConfigWizard();
|
||||||
|
SetEnvironmentVariableA("RP412JOYCONFIG", NULL);
|
||||||
|
_putenv("RP412JOYCONFIG=");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if !_DEBUG
|
#if !_DEBUG
|
||||||
// Arcade pods have no mouse - but desktop/windowed play needs the
|
// Arcade pods have no mouse - but desktop/windowed play needs the
|
||||||
// cursor for the on-screen cockpit buttons, so hide it only when
|
// cursor for the on-screen cockpit buttons, so hide it only when
|
||||||
@@ -475,6 +523,19 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Last word on the window's placement. Every finished drag has already
|
||||||
|
// written it and SVGA16 writes again as the cockpit comes down, but a
|
||||||
|
// session that never started a race has neither - and quitting from
|
||||||
|
// the console screen is exactly how somebody would leave after moving
|
||||||
|
// the window to where they want it. No-op unless the mode is save.
|
||||||
|
//
|
||||||
|
if (IsWindow(hWnd))
|
||||||
|
{
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
}
|
||||||
|
RPWindowLayout_Forget(hWnd);
|
||||||
|
|
||||||
#if !_DEBUG
|
#if !_DEBUG
|
||||||
// symmetric with the fullscreen-only hide at startup
|
// symmetric with the fullscreen-only hide at startup
|
||||||
if (L4Application::GetFullscreen())
|
if (L4Application::GetFullscreen())
|
||||||
|
|||||||
@@ -15,12 +15,56 @@
|
|||||||
#include "..\rp\vtv.h"
|
#include "..\rp\vtv.h"
|
||||||
#include "rpl4mppr.h"
|
#include "rpl4mppr.h"
|
||||||
#include "..\rp\rpplayer.h"
|
#include "..\rp\rpplayer.h"
|
||||||
|
#include "..\munga_l4\l4video.h"
|
||||||
|
#include "..\munga\dropzone.h"
|
||||||
|
#include "..\munga\director.h"
|
||||||
|
#include "..\munga\nttmgr.h"
|
||||||
|
#include "..\munga_l4\l4vb16.h"
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// RPL4Application
|
// RPL4Application
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
|
const Receiver::HandlerEntry
|
||||||
|
RPL4Application::MessageHandlerEntries[]=
|
||||||
|
{
|
||||||
|
MESSAGE_ENTRY(RPL4Application, StopMission),
|
||||||
|
MESSAGE_ENTRY(RPL4Application, WinnersCircle)
|
||||||
|
};
|
||||||
|
|
||||||
|
Receiver::MessageHandlerSet& RPL4Application::GetMessageHandlers()
|
||||||
|
{
|
||||||
|
static Receiver::MessageHandlerSet messageHandlers(
|
||||||
|
ELEMENTS(RPL4Application::MessageHandlerEntries),
|
||||||
|
RPL4Application::MessageHandlerEntries,
|
||||||
|
L4Application::GetMessageHandlers());
|
||||||
|
return messageHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Derivation* RPL4Application::GetClassDerivations()
|
||||||
|
{
|
||||||
|
static Derivation classDerivations(
|
||||||
|
L4Application::GetClassDerivations(), "RPL4Application");
|
||||||
|
return &classDerivations;
|
||||||
|
}
|
||||||
|
|
||||||
|
RPL4Application::SharedData
|
||||||
|
RPL4Application::DefaultData(
|
||||||
|
RPL4Application::GetClassDerivations(),
|
||||||
|
RPL4Application::GetMessageHandlers()
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// How long to sit on black between the race fading out and the podium
|
||||||
|
// fading in. The race fade is FADE_OUT_TIME (half a second), so this is
|
||||||
|
// that plus a beat, to land on black rather than on the tail of it.
|
||||||
|
//
|
||||||
|
const Scalar winnersCircleFadeOutTime = 0.7f;
|
||||||
|
|
||||||
|
// and how quickly the stand comes up out of the black afterwards
|
||||||
|
const Scalar winnersCircleFadeInTime = 0.45f;
|
||||||
|
|
||||||
RPL4Application::RPL4Application(
|
RPL4Application::RPL4Application(
|
||||||
HINSTANCE hInstance,
|
HINSTANCE hInstance,
|
||||||
HWND hWnd,
|
HWND hWnd,
|
||||||
@@ -43,6 +87,389 @@ RPL4Application::~RPL4Application()
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// ShowWinnersCircle
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The pod hall's award platform. Every map carries a "wcircle" stand at
|
||||||
|
// (1200,0,0) with eight ranked dropzones named win1..win8 on it - rank 1 at
|
||||||
|
// the front on the low tier, ranks 4-8 across the back on the high one. All
|
||||||
|
// this shipped; only the mission-review build ever drove it.
|
||||||
|
//
|
||||||
|
// Stand the finishers on their spots in finishing order, freeze them, and
|
||||||
|
// look back at the stand from in front of it.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPL4Application::ShowWinnersCircle()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
// RP412PODIUM=0 skips the whole thing
|
||||||
|
const char *podium_mode = getenv("RP412PODIUM");
|
||||||
|
if (podium_mode != NULL && atoi(podium_mode) == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: disabled\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityManager *entity_manager = GetEntityManager();
|
||||||
|
if (entity_manager == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
||||||
|
if (dropzones == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: no DropZones group\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Walk the finishing order. Player::CalcRanking has been ranking every
|
||||||
|
// scoring player by score each frame, in football as much as in a race
|
||||||
|
// (CalcFootballRanking exists but is never called), so rank order is
|
||||||
|
// meaningful in both.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// The stand's own geometry first, before anybody stands on it.
|
||||||
|
//
|
||||||
|
// The eight spots are fixed furniture on every map, so reading them up
|
||||||
|
// front gives one set of numbers to both jobs below: where to stand
|
||||||
|
// the finishers, and where to put the camera. Only the first is
|
||||||
|
// allowed to care who finished.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
enum { winnersSpotCount = 8 };
|
||||||
|
DropZone *spot[winnersSpotCount];
|
||||||
|
int spots_found = 0;
|
||||||
|
|
||||||
|
char winners_spot[] = "win?";
|
||||||
|
char *place = winners_spot + 3;
|
||||||
|
|
||||||
|
for (int i = 0; i < winnersSpotCount; ++i)
|
||||||
|
{
|
||||||
|
*place = (char) ('1' + i);
|
||||||
|
spot[i] = NULL;
|
||||||
|
|
||||||
|
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
||||||
|
DropZone *dropzone;
|
||||||
|
while ((dropzone = (DropZone*) iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
if (!strcmp(dropzone->GetDropZoneName(), winners_spot))
|
||||||
|
{
|
||||||
|
spot[i] = dropzone;
|
||||||
|
++spots_found;
|
||||||
|
Point3D at = dropzone->localOrigin.linearPosition;
|
||||||
|
DEBUG_STREAM << "WinnersCircle: " << winners_spot << " at "
|
||||||
|
<< at.x << "," << at.y << "," << at.z << "\n" << std::flush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spot[0] == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: no win1 spot on this map\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Stand the finishers up, in finishing order.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
int placed = 0;
|
||||||
|
Player *p;
|
||||||
|
for (int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; ++rank)
|
||||||
|
{
|
||||||
|
if (rank >= winnersSpotCount)
|
||||||
|
{
|
||||||
|
break; // only eight spots exist on the stand
|
||||||
|
}
|
||||||
|
if (spot[rank] == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity *vehicle = p->GetPlayerVehicle();
|
||||||
|
if (vehicle == NULL || vehicle->GetClassID() != VTVClassID)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VTV *vtv = (VTV*) vehicle;
|
||||||
|
vtv->Reset(spot[rank]->localOrigin, VTV::MissionReviewReset);
|
||||||
|
vtv->SetPerformance(&VTV::DoNothing);
|
||||||
|
vtv->FlushEvents();
|
||||||
|
++placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placed == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: nobody to place\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Switch the world back on.
|
||||||
|
//
|
||||||
|
// Dying collapses the view and sets the application "dead" until the
|
||||||
|
// pilot reincarnates - while that flag is up the renderer skips every
|
||||||
|
// static object, which is the whole map. A pilot killed near the buzzer
|
||||||
|
// is still waiting for a respawn that will never come, so the flag is
|
||||||
|
// still up and the podium would play out against a black screen.
|
||||||
|
//
|
||||||
|
// The mission-review build never hit this: it watches from a CameraShip,
|
||||||
|
// which cannot die, so nothing ever set the flag there.
|
||||||
|
//
|
||||||
|
// Standing the finishers up IS the resurrection, so clear it.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
SetIsDead(false);
|
||||||
|
|
||||||
|
DPLRenderer *dpl_renderer = GetVideoRenderer();
|
||||||
|
if (dpl_renderer == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Your own vehicle is built as a cockpit with no hull, so from any
|
||||||
|
// camera outside it there is nothing where you should be - on the stand
|
||||||
|
// you would be the one empty spot. Turn it inside out before the shot.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
dpl_renderer->ShowViewpointFromOutside();
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// The name plates are drawn per rank slot, so they have to be re-sorted
|
||||||
|
// now that the finishing order is final - otherwise the signs read in
|
||||||
|
// whatever order the players were created.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
dpl_renderer->SortAndReloadNameBitmaps();
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Widen to 45 degrees and pull back in front of the stand so the whole
|
||||||
|
// line-up frames up. The stand runs from z~3 (rank 1, low) to z~39
|
||||||
|
// (ranks 4-8, high) and x~1180..1219; at 45 degrees that needs roughly
|
||||||
|
// 30 units of standoff. Eye is above the top tier looking slightly
|
||||||
|
// down at the winner's spot.
|
||||||
|
//
|
||||||
|
// Framed off the stand, NOT off who is standing on it. This used to
|
||||||
|
// average the filled spots and anchor on the first one filled, which
|
||||||
|
// meant the camera moved with the head count: eight finishers put the
|
||||||
|
// anchor in the middle of the tiers and the eye about 58 units out,
|
||||||
|
// one finisher collapsed it onto the winner's spot and brought the eye
|
||||||
|
// in to 36. Same stand, different shot per race - and per machine, if
|
||||||
|
// a remote player's vehicle was not there to be placed. The furniture
|
||||||
|
// does not move, so the camera does not either.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
Point3D standFront = spot[0]->localOrigin.linearPosition;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The stand's own axis: back rows to the winner's spot, which points
|
||||||
|
// out of the front however the map has the stand turned. Taken from
|
||||||
|
// the geometry rather than assumed, so a map that mounts its stand at
|
||||||
|
// another angle still gets photographed from the front.
|
||||||
|
//
|
||||||
|
Point3D standBack(0.0f, 0.0f, 0.0f);
|
||||||
|
int back_count = 0;
|
||||||
|
for (int i = 1; i < winnersSpotCount; ++i)
|
||||||
|
{
|
||||||
|
if (spot[i] != NULL)
|
||||||
|
{
|
||||||
|
standBack += spot[i]->localOrigin.linearPosition;
|
||||||
|
++back_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (back_count > 0)
|
||||||
|
{
|
||||||
|
standBack.x /= (Scalar) back_count;
|
||||||
|
standBack.y /= (Scalar) back_count;
|
||||||
|
standBack.z /= (Scalar) back_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3D facing;
|
||||||
|
facing.x = standFront.x - standBack.x;
|
||||||
|
facing.y = 0.0f;
|
||||||
|
facing.z = standFront.z - standBack.z;
|
||||||
|
Scalar reach = (Scalar) sqrt(facing.x * facing.x + facing.z * facing.z);
|
||||||
|
if (reach < 0.01f)
|
||||||
|
{
|
||||||
|
// a one-spot stand, or all eight stacked: nothing to take a
|
||||||
|
// direction from, so fall back to the way the shipped stands face
|
||||||
|
facing.x = 0.0f; facing.z = -1.0f; reach = 1.0f;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Framing is tunable while the shot is being dialled in:
|
||||||
|
// RP412PODIUMSTANDOFF distance out in front of the stand
|
||||||
|
// RP412PODIUMHEIGHT eye height above the group
|
||||||
|
// RP412PODIUMAIM height of the aim point above the group
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Framed off the stand itself: down low and tilted up across the tiers,
|
||||||
|
// which is how you photograph a podium. It is a balance in both
|
||||||
|
// directions - drop the camera further or tilt harder and the sky takes
|
||||||
|
// the top half while the winner's spot slides off the bottom; tilt down
|
||||||
|
// instead and it becomes a floor plan. Pillarboxing to 4:3 is what lets
|
||||||
|
// it sit this close without the platform trailing off at the sides.
|
||||||
|
//
|
||||||
|
Scalar standoff = 36.0f;
|
||||||
|
Scalar height = 12.0f;
|
||||||
|
Scalar aim_lift = 2.0f;
|
||||||
|
const char *tune = getenv("RP412PODIUMSTANDOFF");
|
||||||
|
if (tune != NULL && atof(tune) != 0.0) standoff = (Scalar) atof(tune);
|
||||||
|
tune = getenv("RP412PODIUMHEIGHT");
|
||||||
|
if (tune != NULL && atof(tune) != 0.0) height = (Scalar) atof(tune);
|
||||||
|
tune = getenv("RP412PODIUMAIM");
|
||||||
|
if (tune != NULL) aim_lift = (Scalar) atof(tune);
|
||||||
|
|
||||||
|
Point3D eye;
|
||||||
|
eye.x = standFront.x + (facing.x / reach) * standoff;
|
||||||
|
eye.y = standFront.y + height;
|
||||||
|
eye.z = standFront.z + (facing.z / reach) * standoff;
|
||||||
|
|
||||||
|
Point3D aim = standFront;
|
||||||
|
aim.y += aim_lift;
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412PODIUMCAM=0 leaves the view in the cockpit, which is also the
|
||||||
|
// way to tell a bad camera from a scene that is not drawing at all.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Clear the cockpit glass away. The MFDs and the radar sit over the
|
||||||
|
// viewscreen like the pod's bezels and have nothing to say once the
|
||||||
|
// race is over; the podium gets the whole canvas.
|
||||||
|
//
|
||||||
|
SVGA16 *cockpit = SVGA16::GetCockpit();
|
||||||
|
if (cockpit != NULL)
|
||||||
|
{
|
||||||
|
cockpit->HideSecondaryDisplays();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Fog first: it pulls the clip plane in to 1100, and SetViewAngle is what
|
||||||
|
// rebuilds the projection that reads it.
|
||||||
|
//
|
||||||
|
dpl_renderer->SetFogStyle(DPLRenderer::winnersCircleFogStyle);
|
||||||
|
|
||||||
|
const char *camera_mode = getenv("RP412PODIUMCAM");
|
||||||
|
if (camera_mode == NULL || atoi(camera_mode) != 0)
|
||||||
|
{
|
||||||
|
dpl_renderer->SetViewAngle(Degree(45.0f));
|
||||||
|
dpl_renderer->SetPresentationCamera(eye, aim);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// still rebuild the projection so the new clip plane takes effect
|
||||||
|
dpl_renderer->SetViewAngle(Degree(40.0f));
|
||||||
|
}
|
||||||
|
if (camera_mode != NULL && atoi(camera_mode) == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: presentation camera disabled\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Everything is in place behind the black - bring it up.
|
||||||
|
// RP412PODIUMFADEIN sets the ramp in seconds.
|
||||||
|
//
|
||||||
|
Scalar fade_in = winnersCircleFadeInTime;
|
||||||
|
const char *fade_tune = getenv("RP412PODIUMFADEIN");
|
||||||
|
if (fade_tune != NULL && atof(fade_tune) > 0.0)
|
||||||
|
{
|
||||||
|
fade_in = (Scalar) atof(fade_tune);
|
||||||
|
}
|
||||||
|
dpl_renderer->StartPresentationFadeIn(fade_in);
|
||||||
|
|
||||||
|
//
|
||||||
|
// placed is reported for diagnosis only - the camera numbers beside it
|
||||||
|
// must not move when it does.
|
||||||
|
//
|
||||||
|
DEBUG_STREAM << "WinnersCircle: " << placed << " placed on "
|
||||||
|
<< spots_found << " spots; aim "
|
||||||
|
<< aim.x << "," << aim.y << "," << aim.z
|
||||||
|
<< " eye " << eye.x << "," << eye.y << "," << eye.z
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// StopMissionMessageHandler
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The mission is over. Show the podium, then let the base handler run - it
|
||||||
|
// puts the player into MissionEndingState, whose fade timer is what actually
|
||||||
|
// keeps the sim and the renderer alive until teardown.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPL4Application::StopMissionMessageHandler(StopMissionMessage *message)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
//
|
||||||
|
// StopMission arrives twice: once from the console at the buzzer, and
|
||||||
|
// again from the player when the ending fade runs out - that second one
|
||||||
|
// is what actually retires the application. Only the first is the end of
|
||||||
|
// the race.
|
||||||
|
//
|
||||||
|
if (GetApplicationState() != Application::EndingMission)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
//-------------------------------------------------------------
|
||||||
|
// Don't cut straight to the podium. The race gets its own
|
||||||
|
// fade-to-black first - that fade is already running by the time
|
||||||
|
// this returns - and the Winners Circle comes up out of the
|
||||||
|
// black afterwards. Standing everyone up now would just fade out
|
||||||
|
// the podium instead of the race.
|
||||||
|
//
|
||||||
|
// FADE_OUT_TIME is half a second; a beat more than that lands on
|
||||||
|
// black rather than on the tail of the fade.
|
||||||
|
//-------------------------------------------------------------
|
||||||
|
//
|
||||||
|
Receiver::Message podium_message(
|
||||||
|
WinnersCircleMessageID, sizeof(Receiver::Message));
|
||||||
|
|
||||||
|
Time event_time;
|
||||||
|
event_time = Now();
|
||||||
|
event_time += winnersCircleFadeOutTime;
|
||||||
|
|
||||||
|
Post(LowEventPriority, this, &podium_message, event_time);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "WinnersCircle: race over, fading out\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
L4Application::StopMissionMessageHandler(message);
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// WinnersCircleMessageHandler
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The race has faded to black. Set the stand up behind the black and fade
|
||||||
|
// back in to it.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPL4Application::WinnersCircleMessageHandler(Receiver::Message *)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "WinnersCircle: standing the finishers up\n" << std::flush;
|
||||||
|
ShowWinnersCircle();
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// MakeRegistry
|
// MakeRegistry
|
||||||
|
|||||||
@@ -26,4 +26,38 @@ private:
|
|||||||
Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources);
|
Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources);
|
||||||
|
|
||||||
Entity* MakeViewpointEntity(Entity__MakeMessage *);
|
Entity* MakeViewpointEntity(Entity__MakeMessage *);
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// The Winners Circle.
|
||||||
|
//
|
||||||
|
// The podium is engine content that only the mission-review build ever
|
||||||
|
// ran: geometry, the eight ranked dropzones and the presentation code all
|
||||||
|
// ship, but the sequence lived on RPL4PlaybackApplication behind a spool
|
||||||
|
// file. This is the same sequence on the path the pods actually race.
|
||||||
|
//
|
||||||
|
public:
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
// posted at the buzzer, fires once the race has faded out
|
||||||
|
WinnersCircleMessageID = L4Application::NextMessageID,
|
||||||
|
NextMessageID
|
||||||
|
};
|
||||||
|
|
||||||
|
static const HandlerEntry
|
||||||
|
MessageHandlerEntries[];
|
||||||
|
static MessageHandlerSet& GetMessageHandlers();
|
||||||
|
static Derivation *GetClassDerivations();
|
||||||
|
static SharedData DefaultData;
|
||||||
|
|
||||||
|
void
|
||||||
|
StopMissionMessageHandler(StopMissionMessage *message);
|
||||||
|
|
||||||
|
// the podium proper, once the screen is already black
|
||||||
|
void
|
||||||
|
WinnersCircleMessageHandler(Receiver::Message *message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// stand the finishers on their ranked spots and frame the shot
|
||||||
|
void
|
||||||
|
ShowWinnersCircle();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -498,6 +498,19 @@ namespace
|
|||||||
<< mission_seconds << "s\n" << std::flush;
|
<< mission_seconds << "s\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412MISSIONSECONDS overrides the menu's game length. The shortest
|
||||||
|
// the menu offers is 3:00, which is a long wait when what you are
|
||||||
|
// testing is what happens at the buzzer.
|
||||||
|
//
|
||||||
|
const char *seconds_override = getenv("RP412MISSIONSECONDS");
|
||||||
|
if (seconds_override != NULL && atoi(seconds_override) > 0)
|
||||||
|
{
|
||||||
|
mission_seconds = atoi(seconds_override);
|
||||||
|
DEBUG_STREAM << "LocalConsole: length overridden to "
|
||||||
|
<< mission_seconds << "s by RP412MISSIONSECONDS\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
gMissionSeconds = mission_seconds;
|
gMissionSeconds = mission_seconds;
|
||||||
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
|
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
|
||||||
gPhase = PhaseWaiting;
|
gPhase = PhaseWaiting;
|
||||||
|
|||||||
+661
-74
@@ -35,6 +35,52 @@ namespace
|
|||||||
{ "headoff", "Freezemoon's Freeway" },
|
{ "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[] =
|
const CatalogEntry kVehicles[] =
|
||||||
{
|
{
|
||||||
{ "quark", "Quark" },
|
{ "quark", "Quark" },
|
||||||
@@ -125,12 +171,32 @@ namespace
|
|||||||
GroupTime,
|
GroupTime,
|
||||||
GroupWeather,
|
GroupWeather,
|
||||||
GroupLength,
|
GroupLength,
|
||||||
|
GroupScenario,
|
||||||
|
GroupTeam, // football only
|
||||||
|
GroupPosition, // football only
|
||||||
GroupLaunch,
|
GroupLaunch,
|
||||||
GroupSteamHost, // buttons, not selections (Steam builds only)
|
GroupSteamHost, // buttons, not selections (Steam builds only)
|
||||||
GroupSteamJoin,
|
GroupSteamJoin,
|
||||||
|
GroupExit,
|
||||||
GroupCount
|
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
|
struct FEItem
|
||||||
{
|
{
|
||||||
int group;
|
int group;
|
||||||
@@ -185,6 +251,8 @@ namespace
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
char badge[24];
|
||||||
|
char team[32]; // football pick, "" = assign for them
|
||||||
|
char position[16];
|
||||||
};
|
};
|
||||||
|
|
||||||
// lobby-fed override (real personas + loadouts); empty = env parsing
|
// lobby-fed override (real personas + loadouts); empty = env parsing
|
||||||
@@ -208,6 +276,8 @@ namespace
|
|||||||
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
|
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
|
||||||
strcpy(extras[i].badge,
|
strcpy(extras[i].badge,
|
||||||
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
|
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
|
||||||
|
strcpy(extras[i].team, gHostedPilots[i].team);
|
||||||
|
strcpy(extras[i].position, gHostedPilots[i].position);
|
||||||
}
|
}
|
||||||
return gHostedPilotCount;
|
return gHostedPilotCount;
|
||||||
}
|
}
|
||||||
@@ -259,6 +329,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExtraPilot *pilot = &extras[extra_count];
|
ExtraPilot *pilot = &extras[extra_count];
|
||||||
|
memset(pilot, 0, sizeof(*pilot));
|
||||||
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
|
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
|
||||||
sprintf(pilot->name, "PILOT %d", extra_count + 2);
|
sprintf(pilot->name, "PILOT %d", extra_count + 2);
|
||||||
strcpy(pilot->vehicle, kVehicles[
|
strcpy(pilot->vehicle, kVehicles[
|
||||||
@@ -311,8 +382,10 @@ namespace
|
|||||||
int top = client_h / 7;
|
int top = client_h / 7;
|
||||||
|
|
||||||
int y = top;
|
int y = top;
|
||||||
AddGroupItems(fe, GroupMap, FE_COUNT(kMaps), col1, &y, row_h, col_w);
|
AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
|
||||||
int y_after_maps = y;
|
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, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
|
||||||
AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), 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);
|
AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);
|
||||||
@@ -321,8 +394,16 @@ namespace
|
|||||||
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
|
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
|
||||||
|
|
||||||
y = top + 2 * row_h; // leave room for the name edit + header
|
y = top + 2 * row_h; // leave room for the name edit + header
|
||||||
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
|
if (IsFootball(fe->selection))
|
||||||
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
|
{
|
||||||
|
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
|
// launch button
|
||||||
FEItem *launch = &fe->items[fe->itemCount++];
|
FEItem *launch = &fe->items[fe->itemCount++];
|
||||||
@@ -333,8 +414,9 @@ namespace
|
|||||||
launch->rect.right = col3 + col_w;
|
launch->rect.right = col3 + col_w;
|
||||||
launch->rect.bottom = client_h - row_h;
|
launch->rect.bottom = client_h - row_h;
|
||||||
|
|
||||||
// Steam lobby buttons (only when the Steam wire is live)
|
// Steam lobby buttons, offered whenever environ.ini asked for Steam.
|
||||||
if (RPL4Lobby_Available())
|
// Painting greys them out and says so if the wire never came up.
|
||||||
|
if (RPL4Lobby_Configured())
|
||||||
{
|
{
|
||||||
FEItem *host = &fe->items[fe->itemCount++];
|
FEItem *host = &fe->items[fe->itemCount++];
|
||||||
host->group = GroupSteamHost;
|
host->group = GroupSteamHost;
|
||||||
@@ -353,6 +435,23 @@ namespace
|
|||||||
join->rect.bottom = client_h - (7 * row_h) / 2;
|
join->rect.bottom = client_h - (7 * row_h) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Exit, bottom left. Diagonally opposite LAUNCH on purpose: it is
|
||||||
|
// the one button on this screen you cannot undo, so it does not go
|
||||||
|
// next to the one people are aiming for. Half width for the same
|
||||||
|
// reason - it is a way out, not a peer of LAUNCH.
|
||||||
|
//
|
||||||
|
// The window frame used to be the way out, and mfd_layout.cfg's
|
||||||
|
// ,noframe takes it away, so the screen has to offer its own.
|
||||||
|
//
|
||||||
|
FEItem *quit = &fe->items[fe->itemCount++];
|
||||||
|
quit->group = GroupExit;
|
||||||
|
quit->index = 0;
|
||||||
|
quit->rect.left = col1;
|
||||||
|
quit->rect.top = client_h - 2 * row_h;
|
||||||
|
quit->rect.right = col1 + col_w / 2;
|
||||||
|
quit->rect.bottom = client_h - row_h;
|
||||||
|
|
||||||
// pilot name edit sits at the top of column 3
|
// pilot name edit sits at the top of column 3
|
||||||
if (fe->nameEdit != NULL)
|
if (fe->nameEdit != NULL)
|
||||||
{
|
{
|
||||||
@@ -365,31 +464,46 @@ namespace
|
|||||||
{
|
{
|
||||||
switch (group)
|
switch (group)
|
||||||
{
|
{
|
||||||
|
case GroupScenario: return "SCENARIO";
|
||||||
case GroupMap: return "TRACK";
|
case GroupMap: return "TRACK";
|
||||||
case GroupVehicle: return "VEHICLE";
|
case GroupVehicle: return "VEHICLE";
|
||||||
case GroupColor: return "COLOR";
|
case GroupColor: return "COLOR";
|
||||||
case GroupBadge: return "BADGE";
|
case GroupBadge: return "BADGE";
|
||||||
|
case GroupTeam: return "TEAM";
|
||||||
|
case GroupPosition: return "POSITION";
|
||||||
case GroupTime: return "TIME OF DAY";
|
case GroupTime: return "TIME OF DAY";
|
||||||
case GroupWeather: return "WEATHER";
|
case GroupWeather: return "WEATHER";
|
||||||
case GroupLength: return "RACE LENGTH";
|
case GroupLength: return "GAME LENGTH";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the map rows need the active scenario's list
|
||||||
|
const int *gItemNameSelection = NULL;
|
||||||
|
|
||||||
const char *ItemName(int group, int index)
|
const char *ItemName(int group, int index)
|
||||||
{
|
{
|
||||||
switch (group)
|
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 GroupVehicle: return kVehicles[index].name;
|
||||||
case GroupColor: return kColors[index].name;
|
case GroupColor: return kColors[index].name;
|
||||||
case GroupBadge: return kBadges[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 GroupTime: return kTimes[index].name;
|
||||||
case GroupWeather: return kWeather[index].name;
|
case GroupWeather: return kWeather[index].name;
|
||||||
case GroupLength: return kLengths[index].name;
|
case GroupLength: return kLengths[index].name;
|
||||||
case GroupLaunch: return "L A U N C H R A C E";
|
case GroupLaunch: return "L A U N C H G A M E";
|
||||||
case GroupSteamHost: return "HOST STEAM RACE";
|
case GroupSteamHost: return "HOST STEAM GAME";
|
||||||
case GroupSteamJoin: return "JOIN STEAM RACE";
|
case GroupSteamJoin: return "JOIN STEAM GAME";
|
||||||
|
case GroupExit: return "EXIT GAME";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -413,13 +527,17 @@ namespace
|
|||||||
SetBkMode(mem, TRANSPARENT);
|
SetBkMode(mem, TRANSPARENT);
|
||||||
|
|
||||||
// title
|
// title
|
||||||
|
gItemNameSelection = fe->selection;
|
||||||
SelectObject(mem, fe->titleFont);
|
SelectObject(mem, fe->titleFont);
|
||||||
SetTextColor(mem, kGreenBright);
|
SetTextColor(mem, kGreenBright);
|
||||||
RECT title = client;
|
RECT title = client;
|
||||||
title.top = client.bottom / 28;
|
title.top = client.bottom / 28;
|
||||||
title.bottom = title.top + client.bottom / 10;
|
title.bottom = title.top + client.bottom / 10;
|
||||||
DrawTextA(mem, "RED PLANET - MARTIAN DEATH RACE", -1, &title,
|
DrawTextA(mem,
|
||||||
DT_CENTER | DT_TOP | DT_SINGLELINE);
|
IsFootball(fe->selection)
|
||||||
|
? "RED PLANET - MARTIAN FOOTBALL"
|
||||||
|
: "RED PLANET - MARTIAN DEATH RACE",
|
||||||
|
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||||
|
|
||||||
SelectObject(mem, fe->textFont);
|
SelectObject(mem, fe->textFont);
|
||||||
|
|
||||||
@@ -446,12 +564,38 @@ namespace
|
|||||||
RECT row = item->rect;
|
RECT row = item->rect;
|
||||||
if (item->group >= GroupLaunch)
|
if (item->group >= GroupLaunch)
|
||||||
{
|
{
|
||||||
HBRUSH launch_brush = CreateSolidBrush(kGreenBright);
|
//
|
||||||
|
// The lobby buttons are offered whenever environ.ini asked
|
||||||
|
// for Steam, but they only work once the client is actually
|
||||||
|
// there. Dim them and say why rather than leaving them out -
|
||||||
|
// two buttons quietly missing looks like a broken build.
|
||||||
|
//
|
||||||
|
Logical steam_button =
|
||||||
|
(item->group == GroupSteamHost || item->group == GroupSteamJoin);
|
||||||
|
Logical dead = steam_button && !RPL4Lobby_Available();
|
||||||
|
|
||||||
|
COLORREF ink = dead ? kGreenDim : kGreenBright;
|
||||||
|
HBRUSH launch_brush = CreateSolidBrush(ink);
|
||||||
FrameRect(mem, &row, launch_brush);
|
FrameRect(mem, &row, launch_brush);
|
||||||
DeleteObject(launch_brush);
|
DeleteObject(launch_brush);
|
||||||
SetTextColor(mem, kGreenBright);
|
SetTextColor(mem, ink);
|
||||||
DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
|
DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
|
||||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The reason, on its own line above the pair. It does not
|
||||||
|
// fit inside a button - they are about sixteen characters
|
||||||
|
// wide and this is more than twice that.
|
||||||
|
//
|
||||||
|
if (dead && item->group == GroupSteamHost)
|
||||||
|
{
|
||||||
|
RECT notice = row;
|
||||||
|
notice.bottom = row.top;
|
||||||
|
notice.top = row.top - (row.bottom - row.top);
|
||||||
|
SetTextColor(mem, kGreenDim);
|
||||||
|
DrawTextA(mem, "STEAM NOT RUNNING", -1, ¬ice,
|
||||||
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,12 +659,36 @@ namespace
|
|||||||
}
|
}
|
||||||
else if (item->group == GroupSteamHost || item->group == GroupSteamJoin)
|
else if (item->group == GroupSteamHost || item->group == GroupSteamJoin)
|
||||||
{
|
{
|
||||||
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
|
// dead until the Steam client is there; the button says so
|
||||||
|
if (RPL4Lobby_Available())
|
||||||
|
{
|
||||||
|
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
|
||||||
|
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item->group == GroupExit)
|
||||||
|
{
|
||||||
|
// the same door closing the window goes through
|
||||||
|
fe->closed = True;
|
||||||
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
|
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fe->selection[item->group] = item->index;
|
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);
|
InvalidateRect(fe->menuWindow, NULL, FALSE);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -655,13 +823,19 @@ namespace
|
|||||||
extern const char *kOrdinalsBlock;
|
extern const char *kOrdinalsBlock;
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// Egg assembly (console RPMission.ToEggString, race scenario).
|
// Egg assembly (console RPMission.ToEggString / RPFootballMission).
|
||||||
// Single player uses the standalone address; a hosted network
|
// Single player uses the standalone address; a hosted network game
|
||||||
// race lists the owner first plus one pilot per member pod.
|
// 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)
|
void BuildEggText(const FEState *fe, std::string &egg)
|
||||||
{
|
{
|
||||||
char line[256];
|
char line[256];
|
||||||
|
Logical football = IsFootball(fe->selection);
|
||||||
|
|
||||||
ExtraPilot extras[maxExtraPilots];
|
ExtraPilot extras[maxExtraPilots];
|
||||||
char owner_address[64];
|
char owner_address[64];
|
||||||
@@ -672,11 +846,176 @@ namespace
|
|||||||
extra_count = 0; // single player
|
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 += "[mission]\n";
|
||||||
egg += "adventure=Red Planet\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 += line;
|
||||||
egg += "scenario=race\n";
|
|
||||||
sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
|
sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
|
||||||
egg += line;
|
egg += line;
|
||||||
sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
|
sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
|
||||||
@@ -690,84 +1029,120 @@ namespace
|
|||||||
egg += line;
|
egg += line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// [pilots] + per-pilot sections
|
||||||
|
//
|
||||||
egg += "[pilots]\n";
|
egg += "[pilots]\n";
|
||||||
sprintf(line, "pilot=%s\n", owner_address);
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
egg += line;
|
|
||||||
for (int p = 0; p < extra_count; ++p)
|
|
||||||
{
|
{
|
||||||
sprintf(line, "pilot=%s\n", extras[p].address);
|
sprintf(line, "pilot=%s\n", pilots[p].address);
|
||||||
egg += line;
|
egg += line;
|
||||||
}
|
}
|
||||||
|
|
||||||
sprintf(line, "[%s]\n", owner_address);
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
sprintf(line, "[%s]\n", extras[p].address);
|
sprintf(line, "[%s]\n", pilots[p].address);
|
||||||
egg += line;
|
egg += line;
|
||||||
egg += "hostType=0\n";
|
egg += "hostType=0\n";
|
||||||
egg += "dropzone=one\n";
|
egg += "dropzone=one\n";
|
||||||
sprintf(line, "name=%s\n", extras[p].name);
|
sprintf(line, "name=%s\n", pilots[p].name);
|
||||||
egg += line;
|
egg += line;
|
||||||
sprintf(line, "bitmapindex=%d\n", p + 2);
|
sprintf(line, "bitmapindex=%d\n", p + 1);
|
||||||
egg += line;
|
egg += line;
|
||||||
egg += "loadzones=1\n";
|
egg += "loadzones=1\n";
|
||||||
sprintf(line, "vehicle=%s\n", extras[p].vehicle);
|
sprintf(line, "vehicle=%s\n", pilots[p].vehicle);
|
||||||
egg += line;
|
egg += line;
|
||||||
sprintf(line, "color=%s\n", extras[p].color);
|
sprintf(line, "color=%s\n", pilots[p].color);
|
||||||
egg += line;
|
egg += line;
|
||||||
sprintf(line, "badge=%s\n", extras[p].badge);
|
sprintf(line, "badge=%s\n", pilots[p].badge);
|
||||||
egg += line;
|
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);
|
// Football team blocks (console RPFootballMission layout)
|
||||||
egg += line;
|
//
|
||||||
for (int p = 0; p < extra_count; ++p)
|
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 += line;
|
||||||
}
|
}
|
||||||
egg += "[smallbitmap]\n";
|
egg += "[smallbitmap]\n";
|
||||||
sprintf(line, "bitmap=BitMap::Small::%s\n", fe->pilotName);
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
egg += line;
|
|
||||||
for (int p = 0; p < extra_count; ++p)
|
|
||||||
{
|
{
|
||||||
sprintf(line, "bitmap=BitMap::Small::%s\n", extras[p].name);
|
sprintf(line, "bitmap=BitMap::Small::%s\n", pilots[p].name);
|
||||||
egg += line;
|
egg += line;
|
||||||
}
|
}
|
||||||
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
sprintf(line, "[BitMap::Large::%s]\n", extras[p].name);
|
sprintf(line, "[BitMap::Large::%s]\n", pilots[p].name);
|
||||||
egg += line;
|
egg += line;
|
||||||
AppendNameBitmap(egg, 128, 32, extras[p].name);
|
AppendNameBitmap(egg, 128, 32, pilots[p].name);
|
||||||
egg += "width=8\n";
|
egg += "width=8\n";
|
||||||
sprintf(line, "[BitMap::Small::%s]\n", extras[p].name);
|
sprintf(line, "[BitMap::Small::%s]\n", pilots[p].name);
|
||||||
egg += line;
|
egg += line;
|
||||||
AppendNameBitmap(egg, 64, 16, extras[p].name);
|
AppendNameBitmap(egg, 64, 16, pilots[p].name);
|
||||||
egg += "width=4\n";
|
egg += "width=4\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,13 +1150,13 @@ namespace
|
|||||||
|
|
||||||
// [pilots]-order names for the network console's results
|
// [pilots]-order names for the network console's results
|
||||||
strcpy(gLastPilotNamesCsv, fe->pilotName);
|
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))
|
sizeof(gLastPilotNamesCsv))
|
||||||
{
|
{
|
||||||
strcat(gLastPilotNamesCsv, ",");
|
strcat(gLastPilotNamesCsv, ",");
|
||||||
strcat(gLastPilotNamesCsv, extras[p].name);
|
strcat(gLastPilotNamesCsv, pilots[p].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1061,6 +1436,218 @@ void
|
|||||||
strcpy(badge, kBadges[b].key);
|
strcpy(badge, kBadges[b].key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// The host's mission setup, as display names.
|
||||||
|
//
|
||||||
|
// The lobby shows everyone what they are about to fly into, and only the
|
||||||
|
// owner knows it - so the owner publishes these into lobby data and the
|
||||||
|
// members read them back. Names rather than catalog keys: the map lists
|
||||||
|
// differ between race and football, so resolving here means the room
|
||||||
|
// never has to know which scenario's table a key came from.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedMapName()
|
||||||
|
{
|
||||||
|
if (!gHavePersist)
|
||||||
|
{
|
||||||
|
return kMaps[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
const CatalogEntry *maps = ActiveMaps(gPersistSelection, &count);
|
||||||
|
int index = gPersistSelection[GroupMap];
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
{
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
return maps[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedTimeName()
|
||||||
|
{
|
||||||
|
int index = gHavePersist ? gPersistSelection[GroupTime] : 0;
|
||||||
|
if (index < 0 || index >= FE_COUNT(kTimes))
|
||||||
|
{
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
return kTimes[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedWeatherName()
|
||||||
|
{
|
||||||
|
int index = gHavePersist ? gPersistSelection[GroupWeather] : 0;
|
||||||
|
if (index < 0 || index >= FE_COUNT(kWeather))
|
||||||
|
{
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
return kWeather[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedLengthName()
|
||||||
|
{
|
||||||
|
// the menu defaults this one to 5:00, not to entry zero
|
||||||
|
int index = gHavePersist ? gPersistSelection[GroupLength] : 2;
|
||||||
|
if (index < 0 || index >= FE_COUNT(kLengths))
|
||||||
|
{
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
return kLengths[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Catalog key -> display name. Member data travels as keys, so the room
|
||||||
|
// needs these to show "Battle Barge" where the wire says "bttlbrg".
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const char *
|
||||||
|
NameForKey(const CatalogEntry *table, int count, const char *key)
|
||||||
|
{
|
||||||
|
if (key == NULL || key[0] == '\0')
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
if (strcmp(table[i].key, key) == 0)
|
||||||
|
{
|
||||||
|
return table[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key; // unknown to this build: show it raw rather than blank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_VehicleNameForKey(const char *key)
|
||||||
|
{
|
||||||
|
return NameForKey(kVehicles, FE_COUNT(kVehicles), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionNameForKey(const char *key)
|
||||||
|
{
|
||||||
|
return NameForKey(kPositions, FE_COUNT(kPositions), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamNameForKey(const char *key)
|
||||||
|
{
|
||||||
|
if (key == NULL || key[0] == '\0')
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (int i = 0; i < FE_COUNT(kTeams); ++i)
|
||||||
|
{
|
||||||
|
if (strcmp(kTeams[i].key, key) == 0)
|
||||||
|
{
|
||||||
|
return kTeams[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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
|
void
|
||||||
RPL4FrontEnd_SetHostedPilots(
|
RPL4FrontEnd_SetHostedPilots(
|
||||||
const char *owner_address,
|
const char *owner_address,
|
||||||
|
|||||||
@@ -50,6 +50,66 @@ int
|
|||||||
void
|
void
|
||||||
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// This player's mission setup as display names. Only the host's
|
||||||
|
// picks decide the mission, so the lobby owner publishes these for
|
||||||
|
// everyone else to read - names rather than keys, because the map
|
||||||
|
// catalog differs between race and football and resolving it here
|
||||||
|
// saves the room from knowing which table a key belongs to.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedMapName();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedTimeName();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedWeatherName();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedLengthName();
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Catalog key -> display name, for the keys that travel as lobby
|
||||||
|
// member data. Unknown keys come back unchanged rather than blank.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_VehicleNameForKey(const char *key);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionNameForKey(const char *key);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamNameForKey(const char *key);
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// 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
|
// Hosted-race pilots fed by the Steam lobby: overrides the
|
||||||
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
||||||
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
||||||
@@ -60,6 +120,9 @@ struct FEHostedPilot
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
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
|
void
|
||||||
RPL4FrontEnd_SetHostedPilots(
|
RPL4FrontEnd_SetHostedPilots(
|
||||||
|
|||||||
+313
-13
@@ -11,6 +11,7 @@
|
|||||||
//########################################################################
|
//########################################################################
|
||||||
|
|
||||||
Logical RPL4Lobby_Available() { return False; }
|
Logical RPL4Lobby_Available() { return False; }
|
||||||
|
Logical RPL4Lobby_Configured() { return False; }
|
||||||
Logical RPL4Lobby_InRoom() { return False; }
|
Logical RPL4Lobby_InRoom() { return False; }
|
||||||
int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||||
int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||||
@@ -36,7 +37,10 @@ void RPL4Lobby_PullRaceResults() { }
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
enum { kMaxLobbyMembers = 4 };
|
// A full grid is eight pods, as the pod hall ran it. The egg builder
|
||||||
|
// carries the owner plus maxExtraPilots (8), so eight members fit
|
||||||
|
// with room to spare.
|
||||||
|
enum { kMaxLobbyMembers = 8 };
|
||||||
const char kLobbyTagKey[] = "rp412";
|
const char kLobbyTagKey[] = "rp412";
|
||||||
const char kGoKey[] = "go";
|
const char kGoKey[] = "go";
|
||||||
|
|
||||||
@@ -45,6 +49,13 @@ namespace
|
|||||||
int gLastGoNonce = 0; // launches we already answered
|
int gLastGoNonce = 0; // launches we already answered
|
||||||
int gShownResultsNonce = 0; // score sheets we already displayed
|
int gShownResultsNonce = 0; // score sheets we already displayed
|
||||||
const char kResultsKey[] = "res";
|
const char kResultsKey[] = "res";
|
||||||
|
const char kScenarioKey[] = "sc";
|
||||||
|
|
||||||
|
// the owner's mission setup, shown to everyone in the room
|
||||||
|
const char kMapKey[] = "mp";
|
||||||
|
const char kTimeKey[] = "td";
|
||||||
|
const char kWeatherKey[] = "wx";
|
||||||
|
const char kLengthKey[] = "ln";
|
||||||
|
|
||||||
// async call-result plumbing
|
// async call-result plumbing
|
||||||
Logical gCallDone = False;
|
Logical gCallDone = False;
|
||||||
@@ -120,6 +131,12 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logical IsOwner()
|
||||||
|
{
|
||||||
|
return gInLobby &&
|
||||||
|
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
|
||||||
|
}
|
||||||
|
|
||||||
void PublishMemberData()
|
void PublishMemberData()
|
||||||
{
|
{
|
||||||
char value[64];
|
char value[64];
|
||||||
@@ -140,12 +157,57 @@ namespace
|
|||||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle);
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle);
|
||||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color);
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color);
|
||||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge);
|
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()));
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Only the owner's menu decides the mission, so the owner also
|
||||||
|
// publishes what it picked: the scenario (members need it to know
|
||||||
|
// whether their team/position pick matters) and the setup
|
||||||
|
// everyone is about to fly into.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
if (IsOwner())
|
||||||
|
{
|
||||||
|
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
||||||
|
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
||||||
|
SteamMatchmaking()->SetLobbyData(gLobby, kMapKey,
|
||||||
|
RPL4FrontEnd_SelectedMapName());
|
||||||
|
SteamMatchmaking()->SetLobbyData(gLobby, kTimeKey,
|
||||||
|
RPL4FrontEnd_SelectedTimeName());
|
||||||
|
SteamMatchmaking()->SetLobbyData(gLobby, kWeatherKey,
|
||||||
|
RPL4FrontEnd_SelectedWeatherName());
|
||||||
|
SteamMatchmaking()->SetLobbyData(gLobby, kLengthKey,
|
||||||
|
RPL4FrontEnd_SelectedLengthName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logical IsOwner()
|
Logical FootballLobby()
|
||||||
{
|
{
|
||||||
return gInLobby &&
|
if (!gInLobby)
|
||||||
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
const char *scenario = SteamMatchmaking()->GetLobbyData(gLobby, kScenarioKey);
|
||||||
|
return (scenario != NULL && strcmp(scenario, "football") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
// Lobby data read back as a string, empty when the owner has not
|
||||||
|
// published it yet (a member can be in the room a beat before the
|
||||||
|
// owner's first publish lands).
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
const char *LobbyText(const char *key)
|
||||||
|
{
|
||||||
|
if (!gInLobby)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const char *text = SteamMatchmaking()->GetLobbyData(gLobby, key);
|
||||||
|
return (text != NULL) ? text : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
@@ -162,6 +224,8 @@ namespace
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
char badge[24];
|
||||||
|
char team[32]; // football pick
|
||||||
|
char position[16];
|
||||||
Logical published;
|
Logical published;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,6 +260,12 @@ namespace
|
|||||||
strncpy(member->badge,
|
strncpy(member->badge,
|
||||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"),
|
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"),
|
||||||
sizeof(member->badge) - 1);
|
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->published =
|
||||||
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
||||||
}
|
}
|
||||||
@@ -241,6 +311,8 @@ namespace
|
|||||||
strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1);
|
strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1);
|
||||||
strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1);
|
strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1);
|
||||||
strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 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')
|
if (pods[0] != '\0')
|
||||||
{
|
{
|
||||||
@@ -274,8 +346,11 @@ namespace
|
|||||||
HFONT titleFont;
|
HFONT titleFont;
|
||||||
RECT launchRect; // owner only
|
RECT launchRect; // owner only
|
||||||
RECT leaveRect;
|
RECT leaveRect;
|
||||||
|
RECT teamRect; // football: cycle my team
|
||||||
|
RECT positionRect; // football: cycle my position
|
||||||
Logical launchClicked;
|
Logical launchClicked;
|
||||||
Logical leaveClicked;
|
Logical leaveClicked;
|
||||||
|
Logical pickChanged;
|
||||||
Logical closed;
|
Logical closed;
|
||||||
MemberInfo members[kMaxLobbyMembers];
|
MemberInfo members[kMaxLobbyMembers];
|
||||||
int memberCount;
|
int memberCount;
|
||||||
@@ -303,15 +378,148 @@ namespace
|
|||||||
RECT title = client;
|
RECT title = client;
|
||||||
title.top = client.bottom / 28;
|
title.top = client.bottom / 28;
|
||||||
title.bottom = title.top + client.bottom / 10;
|
title.bottom = title.top + client.bottom / 10;
|
||||||
DrawTextA(mem, "STEAM RACE LOBBY", -1, &title,
|
DrawTextA(mem,
|
||||||
DT_CENTER | DT_TOP | DT_SINGLELINE);
|
FootballLobby() ? "STEAM FOOTBALL LOBBY" : "STEAM RACE LOBBY",
|
||||||
|
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||||
|
|
||||||
SelectObject(mem, room->textFont);
|
SelectObject(mem, room->textFont);
|
||||||
int row_h = client.bottom / 16;
|
// wider than the old middle-half: the right column now carries
|
||||||
int top = client.bottom / 4;
|
// full catalog names, and in football three of them
|
||||||
int left = client.right / 4;
|
int left = client.right / 6;
|
||||||
int right = 3 * client.right / 4;
|
int right = client.right - client.right / 6;
|
||||||
char text[128];
|
char text[128];
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// The buttons are laid out upward from the bottom edge while the
|
||||||
|
// roster runs down from the top, so everything between the title
|
||||||
|
// and the topmost button has to share one band.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
int ceiling = client.bottom;
|
||||||
|
if (FootballLobby() && room->teamRect.top > 0)
|
||||||
|
{
|
||||||
|
ceiling = room->teamRect.top;
|
||||||
|
}
|
||||||
|
else if (IsOwner() && room->launchRect.top > 0)
|
||||||
|
{
|
||||||
|
ceiling = room->launchRect.top;
|
||||||
|
}
|
||||||
|
else if (room->leaveRect.top > 0)
|
||||||
|
{
|
||||||
|
ceiling = room->leaveRect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Size the rows to that band rather than to a fixed fraction of
|
||||||
|
// the window: a full lobby is eight players, and eight rows at a
|
||||||
|
// sixteenth each would run off the bottom of every window we
|
||||||
|
// support. Counted in half rows, the band holds
|
||||||
|
//
|
||||||
|
// setup lines + gap + kMaxLobbyMembers rows + gap + hint
|
||||||
|
//
|
||||||
|
// so the row height falls out of the space actually available.
|
||||||
|
// It never grows past the old sixteenth (a two-player lobby
|
||||||
|
// should not have circus-sized rows) and never shrinks below the
|
||||||
|
// font, which is what would actually break - overlapping text.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// tmHeight already includes the font's own leading, so a row that
|
||||||
|
// tall cannot clip or collide - asking for more than that just
|
||||||
|
// costs setup lines the room would rather keep.
|
||||||
|
TEXTMETRICA metrics;
|
||||||
|
GetTextMetricsA(mem, &metrics);
|
||||||
|
int min_row = metrics.tmHeight + 2;
|
||||||
|
int max_row = client.bottom / 16;
|
||||||
|
|
||||||
|
int setup_top = title.bottom + client.bottom / 64;
|
||||||
|
int band = ceiling - setup_top;
|
||||||
|
|
||||||
|
int setup_lines = 2;
|
||||||
|
int row_h = 0;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
// halves: setup, half gap, roster, half gap, hint
|
||||||
|
int halves = 2 * setup_lines + 1 + 2 * kMaxLobbyMembers + 1 + 2;
|
||||||
|
row_h = (2 * band) / halves;
|
||||||
|
if (row_h >= min_row || setup_lines == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--setup_lines; // buy room back from the setup lines
|
||||||
|
}
|
||||||
|
if (row_h > max_row) row_h = max_row;
|
||||||
|
if (row_h < 12) row_h = 12;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// On a window too short to give eight rows the room the standard
|
||||||
|
// font wants, draw this block smaller rather than letting the
|
||||||
|
// rows overlap each other or run under the buttons. Nobody plays
|
||||||
|
// at that size, but a squeezed roster still has to be readable.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
HFONT squeezed = NULL;
|
||||||
|
HFONT previous = NULL;
|
||||||
|
if (row_h < min_row)
|
||||||
|
{
|
||||||
|
squeezed = CreateFontA(-(row_h * 2 / 3), 0, 0, 0, FW_NORMAL,
|
||||||
|
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS,
|
||||||
|
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
||||||
|
FIXED_PITCH | FF_MODERN, "Consolas");
|
||||||
|
if (squeezed != NULL)
|
||||||
|
{
|
||||||
|
previous = (HFONT) SelectObject(mem, squeezed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int top = setup_top;
|
||||||
|
|
||||||
|
const char *map_name = LobbyText(kMapKey);
|
||||||
|
if (map_name[0] != '\0' && setup_lines > 0)
|
||||||
|
{
|
||||||
|
RECT setup;
|
||||||
|
setup.left = client.right / 12;
|
||||||
|
setup.right = client.right - client.right / 12;
|
||||||
|
setup.top = setup_top;
|
||||||
|
setup.bottom = setup.top + row_h;
|
||||||
|
|
||||||
|
SetTextColor(mem, kGreenBright);
|
||||||
|
DrawTextA(mem, map_name, -1, &setup,
|
||||||
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Conditions on the line below. Game length is the host's
|
||||||
|
// pick too, and the one people ask about first.
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
const char *time_name = LobbyText(kTimeKey);
|
||||||
|
const char *weather_name = LobbyText(kWeatherKey);
|
||||||
|
const char *length_name = LobbyText(kLengthKey);
|
||||||
|
text[0] = '\0';
|
||||||
|
if (time_name[0] != '\0')
|
||||||
|
{
|
||||||
|
strcat(text, time_name);
|
||||||
|
}
|
||||||
|
if (weather_name[0] != '\0')
|
||||||
|
{
|
||||||
|
if (text[0] != '\0') strcat(text, " - ");
|
||||||
|
strcat(text, weather_name);
|
||||||
|
}
|
||||||
|
if (length_name[0] != '\0')
|
||||||
|
{
|
||||||
|
if (text[0] != '\0') strcat(text, " - ");
|
||||||
|
strcat(text, length_name);
|
||||||
|
}
|
||||||
|
if (text[0] != '\0' && setup_lines > 1)
|
||||||
|
{
|
||||||
|
setup.top = setup.bottom;
|
||||||
|
setup.bottom = setup.top + row_h;
|
||||||
|
SetTextColor(mem, kGreenDim);
|
||||||
|
DrawTextA(mem, text, -1, &setup,
|
||||||
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setup.bottom + row_h / 2 > top)
|
||||||
|
{
|
||||||
|
top = setup.bottom + row_h / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < room->memberCount; ++i)
|
for (int i = 0; i < room->memberCount; ++i)
|
||||||
{
|
{
|
||||||
MemberInfo *member = &room->members[i];
|
MemberInfo *member = &room->members[i];
|
||||||
@@ -328,9 +536,29 @@ namespace
|
|||||||
is_owner_row ? " [HOST]" : "");
|
is_owner_row ? " [HOST]" : "");
|
||||||
DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
if (member->vehicle[0] != '\0')
|
//-----------------------------------------------------------
|
||||||
|
// The right column is what this player brings. Football is a
|
||||||
|
// team sheet - team colors and position - but the VTV still
|
||||||
|
// decides how they play it, so name it there too. Keys
|
||||||
|
// travel on the wire; the catalogs turn them back into
|
||||||
|
// names, so nobody reads "bttlbrg".
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
if (FootballLobby())
|
||||||
{
|
{
|
||||||
sprintf(text, "%s / %s", member->vehicle, member->color);
|
if (member->team[0] != '\0')
|
||||||
|
{
|
||||||
|
sprintf(text, "%s - %s - %s",
|
||||||
|
RPL4FrontEnd_VehicleNameForKey(member->vehicle),
|
||||||
|
RPL4FrontEnd_TeamNameForKey(member->team),
|
||||||
|
RPL4FrontEnd_PositionNameForKey(member->position));
|
||||||
|
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (member->vehicle[0] != '\0')
|
||||||
|
{
|
||||||
|
sprintf(text, "%s - %s",
|
||||||
|
RPL4FrontEnd_VehicleNameForKey(member->vehicle),
|
||||||
|
member->color);
|
||||||
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,12 +574,38 @@ namespace
|
|||||||
: "Waiting for the host to launch...",
|
: "Waiting for the host to launch...",
|
||||||
-1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
-1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
|
// back to the room font for the buttons, which have their own room
|
||||||
|
if (squeezed != NULL)
|
||||||
|
{
|
||||||
|
SelectObject(mem, previous);
|
||||||
|
DeleteObject(squeezed);
|
||||||
|
}
|
||||||
|
|
||||||
HBRUSH frame = CreateSolidBrush(kGreenBright);
|
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())
|
if (IsOwner())
|
||||||
{
|
{
|
||||||
FrameRect(mem, &room->launchRect, frame);
|
FrameRect(mem, &room->launchRect, frame);
|
||||||
SetTextColor(mem, kGreenBright);
|
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);
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
}
|
}
|
||||||
FrameRect(mem, &room->leaveRect, frame);
|
FrameRect(mem, &room->leaveRect, frame);
|
||||||
@@ -395,6 +649,18 @@ namespace
|
|||||||
{
|
{
|
||||||
room->leaveClicked = True;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -465,6 +731,16 @@ namespace
|
|||||||
room.leaveRect.top = client.bottom - 3 * row_h;
|
room.leaveRect.top = client.bottom - 3 * row_h;
|
||||||
room.leaveRect.bottom = client.bottom - 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;
|
gRoom = &room;
|
||||||
PublishMemberData();
|
PublishMemberData();
|
||||||
|
|
||||||
@@ -489,6 +765,18 @@ namespace
|
|||||||
outcome = LobbyRoomClosed;
|
outcome = LobbyRoomClosed;
|
||||||
break;
|
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)
|
if (room.leaveClicked)
|
||||||
{
|
{
|
||||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||||
@@ -632,6 +920,18 @@ Logical
|
|||||||
return SteamNetTransport_GetFakeAddressString()[0] != '\0';
|
return SteamNetTransport_GetFakeAddressString()[0] != '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The environ.ini switch, read the same way RPL4.CPP reads it to decide
|
||||||
|
// whether to install the transport at all. Says nothing about whether the
|
||||||
|
// Steam client was actually there.
|
||||||
|
//
|
||||||
|
Logical
|
||||||
|
RPL4Lobby_Configured()
|
||||||
|
{
|
||||||
|
const char *steam_switch = getenv("RP412STEAM");
|
||||||
|
return (steam_switch != NULL && atoi(steam_switch) != 0) ? True : False;
|
||||||
|
}
|
||||||
|
|
||||||
Logical
|
Logical
|
||||||
RPL4Lobby_InRoom()
|
RPL4Lobby_InRoom()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ enum RPL4LobbyOutcome
|
|||||||
Logical
|
Logical
|
||||||
RPL4Lobby_Available();
|
RPL4Lobby_Available();
|
||||||
|
|
||||||
|
// True when this build has Steam and environ.ini asked for it, whether
|
||||||
|
// or not it actually came up. The menu offers the lobby buttons on this
|
||||||
|
// and greys them out on Available() - a player who turned Steam on and
|
||||||
|
// then launched without the client running should be told so, not left
|
||||||
|
// looking at a menu that quietly has two fewer buttons than the last
|
||||||
|
// time they saw it.
|
||||||
|
Logical
|
||||||
|
RPL4Lobby_Configured();
|
||||||
|
|
||||||
// True while we sit in a lobby (races return to the room).
|
// True while we sit in a lobby (races return to the room).
|
||||||
Logical
|
Logical
|
||||||
RPL4Lobby_InRoom();
|
RPL4Lobby_InRoom();
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flight sticks, HOTAS and rudder pedals
|
||||||
|
|
||||||
|
Anything that is not an Xbox-class pad — a flight stick, a HOTAS
|
||||||
|
throttle, a twist grip, rudder pedals, a wheel — comes in through
|
||||||
|
DirectInput rather than XInput, and needs to be told which axis is
|
||||||
|
which. **Run `joyconfig.bat` once.** It asks you to move each control in
|
||||||
|
turn, works out which device and axis you moved and which way round it
|
||||||
|
reads, and writes the joystick rows of `bindings.txt`. Then it carries
|
||||||
|
on into the setup screen so you can try them straight away.
|
||||||
|
|
||||||
|
It only writes its own section, between two marker lines, so anything
|
||||||
|
you have edited yourself survives re-running it. Xbox-class controllers
|
||||||
|
need none of this — they are the pad rows above and work out of the box.
|
||||||
|
|
||||||
|
By hand, the rows look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
joydev 0 T.16000M # or a bare slot number
|
||||||
|
joyaxis X axis JoystickX invert deadzone 0.08
|
||||||
|
joyaxis Y axis JoystickY invert deadzone 0.08
|
||||||
|
joyaxis RZ axis Pedals deadzone 0.08 # twist grip works both pedals
|
||||||
|
joyaxis SL0 axis Throttle deadzone 0 # a real lever owns the channel
|
||||||
|
joybutton 0 button 0x40 # trigger
|
||||||
|
joyhat 0 up button 0x42
|
||||||
|
```
|
||||||
|
|
||||||
|
Axis names are DirectInput's — `X Y Z RX RY RZ SL0 SL1` — where a twist
|
||||||
|
grip is usually `RZ` and a HOTAS throttle usually `Z` or `SL0`.
|
||||||
|
|
||||||
|
Two things are worth knowing. `Pedals` is a signed axis that drives the
|
||||||
|
pedal *pair*, positive for the right pedal and negative for the left, so
|
||||||
|
one rudder bar or twist grip works both the way a foot never could. And
|
||||||
|
a `joyaxis` on `Throttle` with no `rate` is treated as a real lever: its
|
||||||
|
full travel *is* the throttle range, rather than nudging a position the
|
||||||
|
way a spring-centred pad stick has to.
|
||||||
|
|
||||||
|
## 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`, `Pedals`. 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++
|
**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.
|
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?
|
1. Front end location — Option A (in-engine) **confirmed and shipped**.
|
||||||
2. v1 scenario scope — Death Race only, or Football (needs teams UI) too?
|
2. v1 scenario scope — **both scenarios shipped** (2026-07-24). The setup
|
||||||
3. Results screen — post-race on the cockpit displays (replaces printout)?
|
menu has a SCENARIO group; picking Football swaps the map list to the
|
||||||
4. Steam model — lobby-owner-as-console confirmed? (Implies owner migration
|
football-legal set (no `pain`/`headoff`, adds `headmf`), replaces the
|
||||||
handling later; the egg re-issue path makes host migration plausible.)
|
COLOR/BADGE columns with TEAM/POSITION, and the egg builder emits
|
||||||
5. Pilot identity — Steam persona as pilot name; vehicle/color/badge
|
`scenario=football`, per-pilot `team=`/`position=`, and the
|
||||||
persisted per player (Steam Cloud later)?
|
`[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),
|
1. Steam client installed, logged in (different account per machine),
|
||||||
and RUNNING.
|
and RUNNING.
|
||||||
2. Copy the `dist\` folder onto the machine.
|
2. Copy the `dist\` folder onto the machine.
|
||||||
3. In the dist folder, create `steam_appid.txt` containing exactly:
|
3. Start with `start-windowed.bat` as usual.
|
||||||
`480`
|
|
||||||
(Spacewar, Valve's public dev AppID — replaced by the real AppID
|
The packed dist already contains `steam_appid.txt` (480 — Spacewar,
|
||||||
once the store page exists.)
|
Valve's public dev AppID, replaced by the real one once the store page
|
||||||
4. Add to `environ.ini`:
|
exists) and ships `RP412STEAM=1` in `environ.ini`. If a machine has an
|
||||||
`RP412STEAM=1`
|
older dist, check both before blaming the network: a missing appid
|
||||||
5. Start with `start-windowed.bat` as usual.
|
file makes SteamAPI_Init fail and the game quietly falls back to TCP.
|
||||||
|
|
||||||
Sanity check per machine: `rpl4.log` should show
|
Sanity check per machine: `rpl4.log` should show
|
||||||
`SteamNetTransport: up as 169.254.x.y (fake ports N console, M game)`.
|
`SteamNetTransport: up as 169.254.x.y (fake ports N console, M game)`.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+283
-31
@@ -47,6 +47,13 @@ if (Test-Path $pdb) { Copy-Item $pdb $dist }
|
|||||||
# only activates with RP412STEAM=1; plain desktop runs never touch it.
|
# 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
|
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 -------------------------------------------------------------
|
# --- game data -------------------------------------------------------------
|
||||||
foreach ($dir in 'AUDIO', 'GAUGE', 'VIDEO') {
|
foreach ($dir in 'AUDIO', 'GAUGE', 'VIDEO') {
|
||||||
Write-Host " copying $dir..."
|
Write-Host " copying $dir..."
|
||||||
@@ -57,6 +64,40 @@ foreach ($file in 'RPDPL.INI', 'JOYSTICK.INI', 'RPL4.RES', 'TEST.EGG',
|
|||||||
Copy-Item (Join-Path $assets $file) $dist
|
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
|
||||||
|
|
||||||
|
# The same map as a page, for anyone who would rather look at the
|
||||||
|
# diagrams than read them. Its source is the published artifact, which is
|
||||||
|
# a fragment - the publisher supplies the document shell - so wrap it to
|
||||||
|
# stand alone: without a doctype the browser drops into quirks mode, and
|
||||||
|
# without a charset the typography arrives as mojibake. Written without a
|
||||||
|
# BOM so the charset declaration is the only thing speaking.
|
||||||
|
$controlsPage = Get-Content (Join-Path $root 'docs\rp412-controls.html') -Raw -Encoding UTF8
|
||||||
|
$page = @"
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
$controlsPage
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"@
|
||||||
|
[System.IO.File]::WriteAllText(
|
||||||
|
"$dist\CONTROLS.html", $page, (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
|
||||||
# --- OpenAL runtime --------------------------------------------------------
|
# --- OpenAL runtime --------------------------------------------------------
|
||||||
# The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed
|
# The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed
|
||||||
# runtime beside the exe; oalinst.exe covers machines where that misses.
|
# runtime beside the exe; oalinst.exe covers machines where that misses.
|
||||||
@@ -85,27 +126,150 @@ Set-Content -Path "$dist\environ.ini" -Encoding ascii -Value @"
|
|||||||
|
|
||||||
# ---- Core (the shipped configuration) --------------------------------------
|
# ---- Core (the shipped configuration) --------------------------------------
|
||||||
|
|
||||||
# Control stack. PAD;KEYBOARD = the virtual RIO (XInput controller +
|
# Control stack: tokens separated by ; or , processed left to right.
|
||||||
# keyboard, rebindable via bindings.txt). RIO;KEYBOARD = real serial
|
# PAD the virtual RIO (XInput controller + keyboard,
|
||||||
# cockpit hardware. KEYBOARD = engine keyboard only (no virtual RIO).
|
# 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
|
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
|
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
|
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
|
L4GAUGE=640x480x16
|
||||||
|
|
||||||
# Plasma display: SCREEN renders the pod's plasma glass in-window
|
# Plasma display.
|
||||||
# (currently parked off-layout); hardware values drive real glass.
|
# 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
|
L4PLASMA=SCREEN
|
||||||
|
|
||||||
# 1 = the single-window cockpit: all seven displays composed on a
|
# 0 = classic separate gauge windows; 1 = the single-window glass
|
||||||
# locked 1920x1080 canvas around the viewscreen.
|
# 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
|
L4MFDSPLIT=1
|
||||||
|
|
||||||
# Simulation/render frame rate.
|
# The game window - and in the exploded view (L4MFDSPLIT=2) each display
|
||||||
|
# window - is placed fresh every launch, so moving one somewhere useful
|
||||||
|
# never survived the menu-race-menu loop. This remembers where you put
|
||||||
|
# them, in mfd_layout.cfg beside this file:
|
||||||
|
# off / 0 / unset computed placement only, no file (default)
|
||||||
|
# load put the windows back where they were saved
|
||||||
|
# save the same, and re-save on every finished drag
|
||||||
|
# The game window gets its size back too, so you can size the cockpit to
|
||||||
|
# suit your monitor once and keep it. The display windows get position
|
||||||
|
# only: their size follows their content and their button banks, so an
|
||||||
|
# old one is never restored over them. Arrange everything once with
|
||||||
|
# save, then leave it on load.
|
||||||
|
#
|
||||||
|
# The plasma display window takes part too, under "Plasma Display".
|
||||||
|
#
|
||||||
|
# Each line in mfd_layout.cfg reads <title>=<x>,<y>,<w>,<h>, and you can
|
||||||
|
# append ,noframe to take that window's title bar and border off - a
|
||||||
|
# cockpit that fills the monitor edge to edge without -fit taking the
|
||||||
|
# whole screen. Put the window where you want it first: a bare window
|
||||||
|
# has nothing to drag by. Delete the flag to get the frame back.
|
||||||
|
#RP412MFDLAYOUT=off
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# The Winners Circle: at the end of a race the finishers are stood on
|
||||||
|
# the award platform in finishing order, with each pilot's callsign on
|
||||||
|
# the plate beside their spot, and held there for a few seconds before
|
||||||
|
# the results screen. 1 = show it, 0 = straight to the results.
|
||||||
|
RP412PODIUM=1
|
||||||
|
|
||||||
|
# The shot is framed for you, but these move the camera if you want it
|
||||||
|
# somewhere else. Distances are in game units, measured from the middle
|
||||||
|
# of the group of finishers.
|
||||||
|
# STANDOFF how far out in front of the stand the camera sits
|
||||||
|
# HEIGHT how far above the group
|
||||||
|
# AIM height of the point it looks at, relative to the group -
|
||||||
|
# negative tilts down, positive tilts up
|
||||||
|
# ASPECT the stand was composed for a 4:3 pod monitor, so the shot
|
||||||
|
# is cropped to that shape with black either side. 0 runs it
|
||||||
|
# full width instead.
|
||||||
|
# FADEIN seconds to come up out of the black after the race fades
|
||||||
|
# CAM 0 watches from your own cockpit rather than off the stand
|
||||||
|
#RP412PODIUMSTANDOFF=36
|
||||||
|
#RP412PODIUMHEIGHT=12
|
||||||
|
#RP412PODIUMAIM=2
|
||||||
|
#RP412PODIUMASPECT=1.333
|
||||||
|
#RP412PODIUMFADEIN=0.45
|
||||||
|
#RP412PODIUMCAM=1
|
||||||
|
|
||||||
|
# Override the game length the menu picked, in seconds. The shortest the
|
||||||
|
# menu offers is 3:00, which is a long wait when what you are testing is
|
||||||
|
# what happens at the buzzer. Unset = use the menu's choice.
|
||||||
|
#RP412MISSIONSECONDS=20
|
||||||
|
|
||||||
|
# Simulation/render frame rate, integer frames/second. The desktop
|
||||||
|
# default is 60; the arcade pods shipped at 25.
|
||||||
TARGETFPS=60
|
TARGETFPS=60
|
||||||
|
|
||||||
# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client
|
# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client
|
||||||
@@ -116,48 +280,70 @@ RP412STEAM=1
|
|||||||
# ---- Optional ---------------------------------------------------------------
|
# ---- Optional ---------------------------------------------------------------
|
||||||
|
|
||||||
# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to
|
# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to
|
||||||
# lamp buttons glow with the panel, flash modes and all. Default on;
|
# lamp buttons glow with the panel, flash modes and all.
|
||||||
# set 0 to disable.
|
# Unset or nonzero = on (the default); 0 = off.
|
||||||
#RP412KEYLIGHT=0
|
#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
|
#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
|
#MULTISAMPLE=0
|
||||||
|
|
||||||
# Particle budget.
|
# Particle budget, integer. Default 8192.
|
||||||
#MAXPARTICLES=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
|
#L4PLASMASCALE=4
|
||||||
#L4PLASMAPOS=0,0
|
#L4PLASMAPOS=0,0
|
||||||
|
|
||||||
# Fixed random seed (repeatable runs).
|
# Fixed random seed (repeatable runs): any unsigned integer.
|
||||||
|
# Unset seeds from the clock.
|
||||||
#RANDOM=12345
|
#RANDOM=12345
|
||||||
|
|
||||||
# ---- LAN play without Steam -------------------------------------------------
|
# ---- LAN play without Steam -------------------------------------------------
|
||||||
# Host a race over plain TCP: list the member pods' console channels
|
# Host a race over plain TCP: list the member pods' console channels
|
||||||
# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501).
|
# (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
|
#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501
|
||||||
#RP412HOSTPORT=1501
|
#RP412HOSTPORT=1501
|
||||||
#RP412HOSTADDR=192.168.1.10
|
#RP412HOSTADDR=192.168.1.10
|
||||||
|
|
||||||
# ---- Developer / testing ----------------------------------------------------
|
# ---- 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+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
|
#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
|
#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
|
#RP412STEAMSELFTEST=1
|
||||||
|
|
||||||
# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ----------
|
# ---- 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
|
#PRIMGAUGE=1
|
||||||
#SECGAUGE=2
|
#SECGAUGE=2
|
||||||
#MFDGAUGE=3
|
#MFDGAUGE=3
|
||||||
@@ -171,19 +357,56 @@ RP412STEAM=1
|
|||||||
|
|
||||||
Set-Content -Path "$dist\start-windowed.bat" -Encoding ascii -Value @"
|
Set-Content -Path "$dist\start-windowed.bat" -Encoding ascii -Value @"
|
||||||
@echo off
|
@echo off
|
||||||
rem Red Planet 4.12 - desktop prototype. Boots into the race-setup
|
rem Red Planet 4.12 - desktop prototype. Boots into the game-setup
|
||||||
rem front end; the cockpit canvas is 1920x1080 and -res matches it.
|
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.)
|
rem (Add -egg TEST.EGG to skip the menu and run the canned mission.)
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
start rpl4opt.exe -windowed -res 1920 1080
|
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\joyconfig.bat" -Encoding ascii -Value @"
|
||||||
|
@echo off
|
||||||
|
rem Red Planet 4.12 - one-time JOYSTICK / HOTAS / rudder-pedal setup.
|
||||||
|
rem A console wizard asks you to move each control (stick, twist or
|
||||||
|
rem rudder, throttle lever, fire buttons); it works out what you moved
|
||||||
|
rem and which way it reads, and writes the joystick section of
|
||||||
|
rem bindings.txt. When it finishes the game carries on into the setup
|
||||||
|
rem screen so you can try the bindings straight away.
|
||||||
|
rem Xbox-class controllers need NO setup - they work out of the box.
|
||||||
|
rem Re-run this any time to redo the bindings; anything you have edited
|
||||||
|
rem yourself in bindings.txt is kept.
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set RP412JOYCONFIG=1
|
||||||
|
start /wait rpl4opt.exe -windowed -res 1920 1080
|
||||||
|
set RP412JOYCONFIG=
|
||||||
|
"@
|
||||||
|
|
||||||
Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @"
|
Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @"
|
||||||
Red Planet 4.12.2
|
Red Planet 4.12.7
|
||||||
=================
|
=================
|
||||||
|
|
||||||
Run start-windowed.bat (or: rpl4opt.exe -windowed -res 800 600 -egg TEST.EGG).
|
Run start-fullscreen.bat for borderless over the whole monitor, or
|
||||||
No cockpit hardware needed. If there is no sound, run oalinst.exe once.
|
start-windowed.bat to keep a title bar. No cockpit hardware needed.
|
||||||
|
If there is no sound, run oalinst.exe once.
|
||||||
|
|
||||||
|
Got a flight stick, HOTAS or rudder pedals? Run joyconfig.bat once. It
|
||||||
|
asks you to move each control, works out which axis you moved and which
|
||||||
|
way round it reads, and writes the joystick rows of bindings.txt. Xbox-
|
||||||
|
class controllers need none of that - they work out of the box.
|
||||||
|
|
||||||
Controls (XInput controller and/or keyboard) - EVERY input is
|
Controls (XInput controller and/or keyboard) - EVERY input is
|
||||||
rebindable: edit bindings.txt beside the exe (written with the full
|
rebindable: edit bindings.txt beside the exe (written with the full
|
||||||
@@ -196,11 +419,27 @@ documented default layout on first run; delete it to restore).
|
|||||||
A / Space / NumPad 0 joystick trigger
|
A / Space / NumPad 0 joystick trigger
|
||||||
RB / Alt reverse thrust
|
RB / Alt reverse thrust
|
||||||
DPad / arrow keys joystick hat (look)
|
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)
|
Number+letter rows MFD bank buttons (as printed on the panel)
|
||||||
F1-F12 secondary / screen columns
|
F1-F12 secondary / screen columns
|
||||||
Alt+Q abort the mission (score banked)
|
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
|
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
|
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
|
at its left, and the lower MFDs flanking the portrait map. The red
|
||||||
@@ -209,7 +448,11 @@ pod's real button banks: click them with the mouse, and they light up
|
|||||||
as the game commands their lamps.
|
as the game commands their lamps.
|
||||||
environ.ini is self-documenting: every option ships in the file with
|
environ.ini is self-documenting: every option ships in the file with
|
||||||
a comment (Steam networking, keyboard lighting, stick inversion, LAN
|
a comment (Steam networking, keyboard lighting, stick inversion, LAN
|
||||||
hosting, developer keys, and more).
|
hosting, developer keys, display scaling and radar placement, and more).
|
||||||
|
|
||||||
|
CONTROLS.html is the full controls map - open it in a browser for the
|
||||||
|
pad, keyboard and pod-panel diagrams. CONTROLS.txt is the same thing as
|
||||||
|
plain text.
|
||||||
|
|
||||||
Known prototype notes: pods race untextured (the player1-8 skins come
|
Known prototype notes: pods race untextured (the player1-8 skins come
|
||||||
from the presets system, not shipped data), and text drawn on the plasma
|
from the presets system, not shipped data), and text drawn on the plasma
|
||||||
@@ -223,7 +466,16 @@ $size = (Get-ChildItem $dist -Recurse | Measure-Object Length -Sum).Sum
|
|||||||
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
|
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
|
||||||
|
|
||||||
if ($Zip) {
|
if ($Zip) {
|
||||||
$zipPath = Join-Path $root 'RedPlanet-4.12.2.zip'
|
$zipPath = Join-Path $root 'RedPlanet-4.12.7.zip'
|
||||||
Write-Host "zipping to $zipPath..."
|
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