Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb17220dd5 | ||
|
|
8ba2d4fc86 | ||
|
|
a0a0ad51d1 | ||
|
|
82e733c1a6 | ||
|
|
68f5780efa | ||
|
|
4f34684b16 | ||
|
|
72bb3b394f | ||
|
|
a1d2de591c | ||
|
|
c1729e40c7 | ||
|
|
f20547cb25 | ||
|
|
52da65bc0d | ||
|
|
91420b5cb2 | ||
|
|
fd61316a40 | ||
|
|
dce8818273 | ||
|
|
a1f9c0e3c0 | ||
|
|
bfd5fa163e | ||
|
|
a52fec80a9 | ||
|
|
40b00ddde1 | ||
|
|
16ce4dfbea | ||
|
|
2a23ec0923 |
+14
@@ -40,6 +40,20 @@ ipch/
|
||||
*.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
|
||||
|
||||
# The pilot's callsign, remembered between sessions. Whoever is sitting
|
||||
# at this machine, which is not the repo's business.
|
||||
pilot.cfg
|
||||
|
||||
# Generated by stamp-version.ps1 as RP_L4's pre-build step. The patch
|
||||
# number in it IS this repository's commit count, so a committed copy
|
||||
# would be stale the moment it was committed.
|
||||
/RP_L4/rpl4build.h
|
||||
|
||||
# Build-output static libs that land in lib/ (the two committed dependency
|
||||
# libs, OpenAL32.lib and libsndfile-1.lib, stay tracked).
|
||||
/lib/Munga_L4.lib
|
||||
|
||||
@@ -50,10 +50,57 @@ The solution is [WinTesla.sln](WinTesla.sln) with four v143 projects:
|
||||
Build order is resolved by `ProjectReference` (RP_L4 and RPL4TOOL both reference
|
||||
Munga_L4).
|
||||
|
||||
**Versioning:** the patch number *is* the repository's commit count, so a
|
||||
build always names the commit it came from and there is no question about
|
||||
which changes a given binary contains.
|
||||
[stamp-version.ps1](stamp-version.ps1) runs as RP_L4's pre-build step and
|
||||
writes the generated, uncommitted `RP_L4\rpl4build.h`:
|
||||
|
||||
```
|
||||
#define RP412_VERSION "4.12.96"
|
||||
#define RP412_VERSION_LONG "4.12.96 (a1b2c3d)"
|
||||
```
|
||||
|
||||
The game logs the long form on its first line. A trailing `+` on the hash
|
||||
means the tree had uncommitted changes to tracked files when it was built —
|
||||
useful when a test machine reports something a clean build cannot reproduce.
|
||||
Only the `4.12` product line is set by hand, at the top of the script.
|
||||
|
||||
**Test builds expire.** `$expireDays` at the top of the same script is the
|
||||
shelf life in days (currently **14**, counted from the day it was *built*,
|
||||
not the day the code was written — rebuilding an old commit gives a usable
|
||||
binary rather than one born stale). An expired build says so in a dialog,
|
||||
names its version and expiry date, points at the releases page, and exits
|
||||
without running. It stops a tester spending an afternoon on something that
|
||||
was fixed a week ago.
|
||||
|
||||
> ⚠️ **Set `$expireDays = 0` for a real release.** A shipped build that
|
||||
> expires is a catastrophe, and that one line is what decides it.
|
||||
|
||||
`RP412NOEXPIRY=1` waives the check when an old build has to be run on
|
||||
purpose, and says so in the log so a waived build is never mistaken for a
|
||||
current one. It is deliberately **not** listed in `environ.ini` — a bypass
|
||||
every tester can see is a bypass every tester will use. Negative
|
||||
`$expireDays` backdates the expiry, which is how the refusal gets tested
|
||||
without touching the machine's clock.
|
||||
|
||||
The header is deliberately not committed: the commit that recorded a
|
||||
hardcoded number would itself change the count, so the file would be stale
|
||||
the moment it landed. It is rewritten only when the stamp actually changes,
|
||||
so ordinary rebuilds do not recompile `RPL4.CPP` for nothing. Building
|
||||
outside a git checkout stamps `4.12.x (no repository)` rather than inventing
|
||||
a number that would sort against real ones.
|
||||
|
||||
**Packaging:** [pack-dist.ps1](pack-dist.ps1) assembles a runnable game into
|
||||
`dist\` (exe + PDB, game data, OpenAL/libsndfile runtimes, desktop
|
||||
`environ.ini`, `start-windowed.bat`, README). Pass `-Zip` to also produce
|
||||
`dist\RedPlanet412-prototype.zip` for handing to someone else.
|
||||
`dist\` (exe + PDB, game data, OpenAL/libsndfile runtimes, launch scripts,
|
||||
HANDBOOK.html, README). It deliberately does **not** write `environ.ini` —
|
||||
the exe carries that template and writes it on first run
|
||||
([RP_L4/RPL4ENVIRON.cpp](RP_L4/RPL4ENVIRON.cpp)), so a tester can drop a new
|
||||
build over an old folder without losing their settings. Pass `-Zip` to also produce
|
||||
`RedPlanet-<version>.zip` for handing to someone else. It reads the version
|
||||
from `rpl4build.h` rather than asking git again, so the package and the
|
||||
binary inside it cannot disagree, and it warns if the build it is packing
|
||||
came from a modified tree.
|
||||
|
||||
## 3. VS2022 migration notes (what changed and why)
|
||||
|
||||
|
||||
+23
-2
@@ -649,8 +649,23 @@ Time endIntercom = Now();
|
||||
//
|
||||
if (GetApplicationState() == RunningMission)
|
||||
{
|
||||
secondsRemainingInGame =
|
||||
currentMission->GetGameLength() - (Now() - gameStarted);
|
||||
//
|
||||
// Ask the console first: it owns the clock that actually ends the
|
||||
// race, so this is the countdown the buzzer will agree with. Its
|
||||
// own reckoning is the fallback for everything with no console of
|
||||
// its own - see gMissionClockHook in APPMGR.h.
|
||||
//
|
||||
Scalar console_remaining;
|
||||
if (gMissionClockHook != NULL &&
|
||||
(*gMissionClockHook)(&console_remaining))
|
||||
{
|
||||
secondsRemainingInGame = console_remaining;
|
||||
}
|
||||
else
|
||||
{
|
||||
secondsRemainingInGame =
|
||||
currentMission->GetGameLength() - (Now() - gameStarted);
|
||||
}
|
||||
}
|
||||
routePacketFinished = False;
|
||||
|
||||
@@ -1125,6 +1140,12 @@ void
|
||||
Check(this);
|
||||
Check(egg_notation_file);
|
||||
|
||||
//
|
||||
// Forget every peer's clock offset: the hosts in the next race are not
|
||||
// the hosts in the last one, and a HostID gets reused.
|
||||
//
|
||||
NetClock_Reset();
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
// Create mission from egg notation file
|
||||
|
||||
@@ -15,6 +15,9 @@ Logical gConsoleMarshalsLaunch = False;
|
||||
// losing the console mid-mission ends it (lobby-member races)
|
||||
Logical gConsoleLossEndsMission = False;
|
||||
|
||||
// the console's countdown, when a console is marshalling (see APPMGR.h)
|
||||
Logical (*gMissionClockHook)(Scalar *seconds_remaining) = NULL;
|
||||
|
||||
ApplicationManager* ApplicationManager::CurrentAppManager = NULL;
|
||||
|
||||
ApplicationManager::ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate) : Node(ApplicationManagerClassID), runningApplications(this)
|
||||
|
||||
@@ -20,6 +20,28 @@ extern Logical gConsoleMarshalsLaunch;
|
||||
// console to return, exactly as always.
|
||||
extern Logical gConsoleLossEndsMission;
|
||||
|
||||
//
|
||||
// The console's own countdown, when there is a console to ask.
|
||||
//
|
||||
// A mission ends when the console says so, but secondsRemainingInGame was
|
||||
// computed here from the engine clock and its own idea of when the race
|
||||
// started - a different clock, from a different epoch, than the one that
|
||||
// actually fires the buzzer. The two agree to within a frame or so, which
|
||||
// is why nobody noticed, but they are not the same number: the cockpit
|
||||
// clock could read 0:00 with the race still running, and the camera
|
||||
// directors' "last 30 seconds" behaviour switched on the engine's reading
|
||||
// rather than on the real remaining time.
|
||||
//
|
||||
// Set by the console when it is marshalling; NULL restores the engine's
|
||||
// own reckoning, which is what the arcade -net pods, lobby members and
|
||||
// mission review all use (none of them run a console locally, and their
|
||||
// clock is anchored by the console's RunMission arriving anyway).
|
||||
//
|
||||
// Returns False when it has no answer yet - the window between the
|
||||
// application reaching RunningMission and the console noticing.
|
||||
//
|
||||
extern Logical (*gMissionClockHook)(Scalar *seconds_remaining);
|
||||
|
||||
class ApplicationManager : public Node
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -377,8 +377,28 @@ void
|
||||
//------------------------------------------------------------------------
|
||||
// Step through each block until there are no more remaining, and send the
|
||||
// update out the the simulation indicated by the subsystemID
|
||||
//
|
||||
// This is the only point on the receive path that knows WHOSE update
|
||||
// this is - the records themselves carry a timestamp but not an owner -
|
||||
// so the sender is published here for the net clock to align against.
|
||||
// Every record in the message, and the damage zones nested inside them,
|
||||
// came from the same machine in the same frame.
|
||||
//------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Only for an entity somebody else owns. Our own clock needs no
|
||||
// aligning, and an update we somehow handed ourselves would otherwise
|
||||
// drag lastUpdate back by a frame for no reason.
|
||||
//
|
||||
Check(application);
|
||||
Check(application->GetHostManager());
|
||||
Logical remote_owner =
|
||||
GetOwnerID() != application->GetHostManager()->GetLocalHostID();
|
||||
if (remote_owner)
|
||||
{
|
||||
NetClock_BeginUpdate(GetOwnerID());
|
||||
}
|
||||
|
||||
while (stream.GetBytesRemaining())
|
||||
{
|
||||
Simulation::UpdateRecord *update =
|
||||
@@ -389,6 +409,11 @@ void
|
||||
simulation->ReadUpdateRecord(update);
|
||||
stream.AdvancePointer(update->recordLength);
|
||||
}
|
||||
|
||||
if (remote_owner)
|
||||
{
|
||||
NetClock_EndUpdate();
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
|
||||
+181
-1
@@ -264,6 +264,109 @@ Simulation::SharedData
|
||||
// Model support
|
||||
//
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
//##########################################################################
|
||||
// Net clock - see SIMULATE.h for why the sender's timestamp is estimated
|
||||
// rather than used as it stands.
|
||||
//##########################################################################
|
||||
|
||||
namespace
|
||||
{
|
||||
enum
|
||||
{
|
||||
netClockMaxPeers = 16,
|
||||
|
||||
// Samples per rolling minimum. A peer sends one record per
|
||||
// simulation per frame, so at eight vehicles and 60 fps this is
|
||||
// well under a second - fast enough to follow a route change,
|
||||
// long enough that the minimum means something.
|
||||
netClockWindow = 128,
|
||||
|
||||
// The furthest back we will believe a timestamp. Beyond this the
|
||||
// packet is stale or the estimate is wrong, and extrapolating a
|
||||
// vehicle half a second forward does more harm than the lag we
|
||||
// are correcting.
|
||||
netClockMaxLagTicks = 500
|
||||
};
|
||||
|
||||
struct PeerClock
|
||||
{
|
||||
HostID host;
|
||||
Logical inUse;
|
||||
Logical settled;
|
||||
long offsetTicks; // our clock - their clock
|
||||
long windowMinTicks;
|
||||
int windowCount;
|
||||
};
|
||||
|
||||
PeerClock gPeerClocks[netClockMaxPeers];
|
||||
HostID gUpdateSender = 0;
|
||||
Logical gUpdateSenderValid = False;
|
||||
|
||||
Logical NetClockEnabled()
|
||||
{
|
||||
static int enabled = -1;
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412NETCLOCK");
|
||||
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
||||
if (!enabled)
|
||||
{
|
||||
DEBUG_STREAM << "NetClock: disabled by RP412NETCLOCK=0 - "
|
||||
<< "replicants dead-reckon from arrival time\n" << std::flush;
|
||||
}
|
||||
}
|
||||
return enabled ? True : False;
|
||||
}
|
||||
|
||||
PeerClock *FindPeer(HostID host)
|
||||
{
|
||||
PeerClock *free_slot = NULL;
|
||||
for (int i = 0; i < netClockMaxPeers; ++i)
|
||||
{
|
||||
if (gPeerClocks[i].inUse)
|
||||
{
|
||||
if (gPeerClocks[i].host == host)
|
||||
{
|
||||
return &gPeerClocks[i];
|
||||
}
|
||||
}
|
||||
else if (free_slot == NULL)
|
||||
{
|
||||
free_slot = &gPeerClocks[i];
|
||||
}
|
||||
}
|
||||
if (free_slot != NULL)
|
||||
{
|
||||
free_slot->inUse = True;
|
||||
free_slot->host = host;
|
||||
free_slot->settled = False;
|
||||
free_slot->offsetTicks = 0;
|
||||
free_slot->windowMinTicks = 0;
|
||||
free_slot->windowCount = 0;
|
||||
}
|
||||
return free_slot;
|
||||
}
|
||||
}
|
||||
|
||||
void NetClock_BeginUpdate(HostID sender)
|
||||
{
|
||||
gUpdateSender = sender;
|
||||
gUpdateSenderValid = True;
|
||||
}
|
||||
|
||||
void NetClock_EndUpdate()
|
||||
{
|
||||
gUpdateSenderValid = False;
|
||||
}
|
||||
|
||||
void NetClock_Reset()
|
||||
{
|
||||
memset(gPeerClocks, 0, sizeof(gPeerClocks));
|
||||
gUpdateSenderValid = False;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
@@ -272,7 +375,84 @@ void
|
||||
Check(this);
|
||||
Check_Pointer(message);
|
||||
|
||||
lastUpdate = Now(); // HACK - should be based upon message->timeStamp
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// When this update arrived is not when it was taken. Put lastUpdate
|
||||
// at the sender's sampling moment, expressed in our clock, so the
|
||||
// dead reckoner extrapolates over the network latency instead of
|
||||
// starting from scratch once it has already elapsed.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
long now_ticks = Now().ticks;
|
||||
long local_ticks = now_ticks;
|
||||
|
||||
PeerClock *peer = gUpdateSenderValid && NetClockEnabled()
|
||||
? FindPeer(gUpdateSender) : NULL;
|
||||
if (peer != NULL)
|
||||
{
|
||||
//
|
||||
// sample = trueOffset + oneWayLatency, so the running minimum
|
||||
// converges on the offset from above.
|
||||
//
|
||||
long sample = now_ticks - message->timeStamp.ticks;
|
||||
|
||||
if (!peer->settled)
|
||||
{
|
||||
peer->settled = True;
|
||||
peer->offsetTicks = sample;
|
||||
peer->windowMinTicks = sample;
|
||||
peer->windowCount = 0;
|
||||
DEBUG_STREAM << "NetClock: host " << peer->host
|
||||
<< " first sample, offset " << sample << " ms\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sample < peer->windowMinTicks)
|
||||
{
|
||||
peer->windowMinTicks = sample;
|
||||
}
|
||||
if (sample < peer->offsetTicks)
|
||||
{
|
||||
peer->offsetTicks = sample; // a shorter path: believe it now
|
||||
}
|
||||
if (++peer->windowCount >= netClockWindow)
|
||||
{
|
||||
//
|
||||
// Close the window: adopt its minimum even if it is
|
||||
// LARGER than the running estimate, which is how the
|
||||
// figure follows clock drift and a route that got
|
||||
// slower rather than staying pinned to one old packet.
|
||||
//
|
||||
long moved = peer->windowMinTicks - peer->offsetTicks;
|
||||
if (moved > 50 || moved < -50)
|
||||
{
|
||||
DEBUG_STREAM << "NetClock: host " << peer->host
|
||||
<< " offset " << peer->offsetTicks << " -> "
|
||||
<< peer->windowMinTicks << " ms\n" << std::flush;
|
||||
}
|
||||
peer->offsetTicks = peer->windowMinTicks;
|
||||
peer->windowMinTicks = sample;
|
||||
peer->windowCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
local_ticks = message->timeStamp.ticks + peer->offsetTicks;
|
||||
|
||||
//
|
||||
// Never ahead of our own clock, and never further back than we
|
||||
// are willing to extrapolate.
|
||||
//
|
||||
if (local_ticks > now_ticks)
|
||||
{
|
||||
local_ticks = now_ticks;
|
||||
}
|
||||
else if (now_ticks - local_ticks > netClockMaxLagTicks)
|
||||
{
|
||||
local_ticks = now_ticks - netClockMaxLagTicks;
|
||||
}
|
||||
}
|
||||
|
||||
lastUpdate.ticks = local_ticks;
|
||||
SetSimulationState(message->simulationState);
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,41 @@
|
||||
#include "receiver.h"
|
||||
#include "time.h"
|
||||
#include "resource.h"
|
||||
#include "hostid.h"
|
||||
|
||||
//##########################################################################
|
||||
//########################### Net clock ##############################
|
||||
//##########################################################################
|
||||
//
|
||||
// Aligning a peer's clock with ours, so a replicant is dead-reckoned from
|
||||
// when its update was SENT rather than when it happened to arrive.
|
||||
//
|
||||
// Every update record carries the sender's own timestamp. The receiver
|
||||
// used to throw it away and stamp lastUpdate with its own Now() - the
|
||||
// original code says so: "HACK - should be based upon message->timeStamp".
|
||||
// The dead reckoner then extrapolates over (lastPerformance - lastUpdate),
|
||||
// so starting that clock at ARRIVAL rather than at SEND leaves every
|
||||
// remote vehicle exactly one network latency behind where it should be.
|
||||
// On the 1 ms arcade LAN that was invisible. Over Steam Datagram Relay it
|
||||
// is a constant 50-150 ms of positional lag - a bias, not jitter.
|
||||
//
|
||||
// The timestamp cannot be used raw: two machines' clocks share no epoch,
|
||||
// both being QueryPerformanceCounter since their own boot. So we estimate
|
||||
// the offset per peer. Each arriving record gives
|
||||
//
|
||||
// sample = ourNow - theirStamp = trueOffset + oneWayLatency
|
||||
//
|
||||
// and since latency is never negative, the SMALLEST sample seen is the
|
||||
// closest to the true offset. Taking a minimum over a short rolling
|
||||
// window tracks crystal drift and re-adapts when the route changes,
|
||||
// instead of being pinned forever by one lucky packet.
|
||||
//
|
||||
// RP412NETCLOCK=0 turns the whole thing off and restores the arrival-time
|
||||
// behaviour, so a test machine can A/B it without a rebuild.
|
||||
//
|
||||
void NetClock_BeginUpdate(HostID sender); // around one message's records
|
||||
void NetClock_EndUpdate();
|
||||
void NetClock_Reset(); // forget every peer (new mission)
|
||||
|
||||
class Simulation__SharedData;
|
||||
class Simulation__IndexData;
|
||||
|
||||
+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);
|
||||
@@ -10,6 +10,74 @@ namespace
|
||||
|
||||
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
|
||||
@@ -108,6 +176,15 @@ namespace
|
||||
}
|
||||
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:
|
||||
// Part of the cockpit; just hide it.
|
||||
ShowWindow(hwnd, SW_HIDE);
|
||||
@@ -148,6 +225,8 @@ MFDSplitView::MFDSplitView(
|
||||
sourceHeight = source_height;
|
||||
buttonCount = 0;
|
||||
pressedIndex = -1;
|
||||
paneTitle = title;
|
||||
ownWindow = (parent == NULL) ? True : False;
|
||||
|
||||
pixels = new unsigned long[source_width * source_height];
|
||||
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
|
||||
@@ -230,6 +309,14 @@ MFDSplitView::MFDSplitView(
|
||||
{
|
||||
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
|
||||
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
|
||||
{
|
||||
@@ -241,6 +328,9 @@ MFDSplitView::~MFDSplitView()
|
||||
{
|
||||
Check_Pointer(this);
|
||||
|
||||
// out of the layout registry before the window goes
|
||||
RPWindowLayout_Forget(window);
|
||||
|
||||
if (window != NULL)
|
||||
{
|
||||
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
|
||||
@@ -259,6 +349,272 @@ Logical
|
||||
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
|
||||
// client area to make room for the strips.
|
||||
|
||||
@@ -77,6 +77,14 @@ public:
|
||||
void
|
||||
Hide();
|
||||
|
||||
// caption and HWND, for the sticky-placement file
|
||||
const char *
|
||||
Title() const
|
||||
{ return paneTitle; }
|
||||
void *
|
||||
Window()
|
||||
{ return window; }
|
||||
|
||||
~MFDSplitView();
|
||||
|
||||
Logical
|
||||
@@ -147,4 +155,79 @@ protected:
|
||||
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();
|
||||
|
||||
+197
-26
@@ -2,6 +2,7 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4padbindings.h"
|
||||
#include "l4joy.h" // joyButtonCount / joyHatCount, the parse limits
|
||||
|
||||
#include <XInput.h>
|
||||
#include <stdio.h>
|
||||
@@ -79,6 +80,20 @@ namespace
|
||||
{ "Throttle", BindAxisThrottle },
|
||||
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
||||
{ "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)
|
||||
@@ -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)
|
||||
{
|
||||
return False;
|
||||
@@ -271,31 +359,75 @@ namespace
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->source = source;
|
||||
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"))
|
||||
{
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -320,10 +452,17 @@ namespace
|
||||
"# key <name> axis <axis> rate <n-per-second>\n"
|
||||
"# pad <button> button <addr> [toggle]\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"
|
||||
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
||||
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\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"
|
||||
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\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"
|
||||
"# position sticks (the throttle). Every lamp button is also clickable\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"
|
||||
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
||||
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
||||
@@ -499,6 +663,7 @@ void
|
||||
char line[256];
|
||||
int line_number = 0;
|
||||
int error_count = 0;
|
||||
int joy_slot = 0; // joy rows before any joydev belong to slot 0
|
||||
const char *cursor = source;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
@@ -523,7 +688,7 @@ void
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ParseLine(tokens, token_count, profile))
|
||||
if (!ParseLine(tokens, token_count, profile, &joy_slot))
|
||||
{
|
||||
++error_count;
|
||||
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
||||
@@ -542,4 +707,10 @@ void
|
||||
<< profile->padAxisCount << " pad axes"
|
||||
<< (error_count ? " (with rejected lines)" : "")
|
||||
<< "\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>
|
||||
// pad <button> button <addr> [toggle]
|
||||
// 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
|
||||
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
|
||||
// bindings.txt beside the exe; written there (self-documenting, with
|
||||
// the full default profile) on first run. Bad lines are logged and
|
||||
// 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
|
||||
@@ -33,6 +46,14 @@ enum PadBindRioAxis
|
||||
BindAxisRightPedal,
|
||||
BindAxisJoystickY,
|
||||
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
|
||||
};
|
||||
|
||||
@@ -89,6 +110,54 @@ struct PadPadAxisBinding
|
||||
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
|
||||
{
|
||||
enum
|
||||
@@ -96,7 +165,10 @@ struct PadBindingProfile
|
||||
maxKeyButtons = 128,
|
||||
maxKeyAxes = 32,
|
||||
maxPadButtons = 32,
|
||||
maxPadAxes = 16
|
||||
maxPadAxes = 16,
|
||||
maxJoyAxes = 24,
|
||||
maxJoyButtons = 48,
|
||||
maxJoyHats = 16
|
||||
};
|
||||
|
||||
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
||||
@@ -107,6 +179,15 @@ struct PadBindingProfile
|
||||
int padButtonCount;
|
||||
PadPadAxisBinding padAxes[maxPadAxes];
|
||||
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,
|
||||
|
||||
+214
-1
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "l4padrio.h"
|
||||
#include "l4keylight.h"
|
||||
#include "l4joy.h"
|
||||
|
||||
#include <XInput.h>
|
||||
#pragma comment(lib, "xinput9_1_0.lib")
|
||||
@@ -41,6 +42,51 @@ namespace
|
||||
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)
|
||||
{
|
||||
DEBUG_STREAM << line << "\n" << std::flush;
|
||||
@@ -166,6 +212,29 @@ PadRIO::PadRIO()
|
||||
activeInstance = this;
|
||||
|
||||
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()
|
||||
@@ -244,6 +313,11 @@ void
|
||||
profile.padButtons[i].latched = 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
|
||||
@@ -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)
|
||||
{
|
||||
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);
|
||||
|
||||
Scalar x = deflect[BindAxisJoystickX];
|
||||
@@ -511,7 +722,9 @@ void
|
||||
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]);
|
||||
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
|
||||
// The profile encodes the pod's stick sign convention; L4PADFLIP
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "l4plasmascreen.h"
|
||||
#include "l4app.h"
|
||||
#include "l4mfdview.h" // RPWindowLayout_*
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -45,6 +46,15 @@ namespace
|
||||
}
|
||||
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:
|
||||
// The glass is part of the cockpit; just hide it.
|
||||
ShowWindow(hwnd, SW_HIDE);
|
||||
@@ -176,6 +186,7 @@ PlasmaScreen::~PlasmaScreen()
|
||||
}
|
||||
if (window != NULL)
|
||||
{
|
||||
RPWindowLayout_Forget(window);
|
||||
RemovePropA((HWND) window, "PlasmaBitmapInfo");
|
||||
RemovePropA((HWND) window, "PlasmaPixels");
|
||||
DestroyWindow((HWND) window);
|
||||
@@ -275,6 +286,22 @@ void
|
||||
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
|
||||
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
||||
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
|
||||
{
|
||||
|
||||
@@ -3839,6 +3839,11 @@ static LRESULT CALLBACK
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -4457,6 +4462,15 @@ SVGA16::SVGA16(
|
||||
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)
|
||||
{
|
||||
@@ -4668,6 +4682,23 @@ SVGA16::SVGA16(
|
||||
// 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
|
||||
{
|
||||
@@ -4863,6 +4894,14 @@ SVGA16::~SVGA16()
|
||||
//---------------------------------------------------------
|
||||
//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)
|
||||
{
|
||||
delete splitView[view];
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<ClCompile Include=".\L4PADBINDINGS.cpp" />
|
||||
<ClCompile Include=".\L4JOY.cpp" />
|
||||
<ClCompile Include=".\L4PADRIO.cpp" />
|
||||
<ClCompile Include=".\L4PCSPAK.cpp" />
|
||||
<ClCompile Include=".\L4PLASMA.cpp" />
|
||||
@@ -454,6 +455,7 @@
|
||||
<ClInclude Include=".\L4NETTRANSPORT.h" />
|
||||
<ClInclude Include=".\L4KEYLIGHT.h" />
|
||||
<ClInclude Include=".\L4PADBINDINGS.h" />
|
||||
<ClInclude Include=".\L4JOY.h" />
|
||||
<ClInclude Include=".\L4STEAMTRANSPORT.h" />
|
||||
<ClInclude Include=".\L4PARTICLES.h" />
|
||||
<ClInclude Include=".\L4MFDVIEW.h" />
|
||||
|
||||
@@ -61,6 +61,32 @@ 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
|
||||
|
||||
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
|
||||
@@ -70,9 +96,13 @@ team/position columns and its own track list). Steam multiplayer: see
|
||||
[docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) (until RP412
|
||||
has its own AppID it runs under Spacewar, 480).
|
||||
|
||||
The two config files beside the exe are self-documenting: **environ.ini**
|
||||
(every engine option, commented) and **bindings.txt** (every key, pad
|
||||
button, and axis; written with the full default layout on first run).
|
||||
The config files beside the exe are self-documenting and none of them
|
||||
ship: the game writes each one the first time it needs it and then leaves
|
||||
it alone, so a new build dropped over an existing folder keeps every
|
||||
setting. **environ.ini** is every engine option, commented; **bindings.txt**
|
||||
every key, pad button and axis; **pilot.cfg** your callsign and loadout;
|
||||
**mfd_layout.cfg** where you dragged the windows. Delete any of them to
|
||||
start that part over with the current defaults.
|
||||
Default controls: numpad flies (8/2/4/6 stick, 7/9 pedals, 0 trigger),
|
||||
Shift/Ctrl throttle, Alt reverse, arrows look, Space fires, letter rows
|
||||
are the MFD button banks as printed on the panel. **Alt+Q** aborts a
|
||||
|
||||
+166
-19
@@ -22,11 +22,15 @@
|
||||
|
||||
#include "rpl4pb.h"
|
||||
#include "rpl4fe.h"
|
||||
#include "rpl4environ.h"
|
||||
#include "rpl4console.h"
|
||||
#include "rpl4lobby.h"
|
||||
#include "..\munga_l4\l4steamtransport.h"
|
||||
#include "..\munga_l4\l4splr.h"
|
||||
#include "..\munga_l4\l4mfdview.h" // RPWindowLayout_*
|
||||
#include "..\munga_l4\l4joy.h" // RPJoyConfigWizard
|
||||
#include "rpl4ver.h"
|
||||
#include "rpl4build.h" // generated: RP412_VERSION / RP412_VERSION_LONG
|
||||
#include "..\munga\resver.h"
|
||||
#include "..\munga\resource.h"
|
||||
// added for game status drawing support
|
||||
@@ -38,6 +42,7 @@
|
||||
#include <strsafe.h>
|
||||
#include <direct.h>
|
||||
#include <shellapi.h>
|
||||
#include <time.h> // the test-build expiry check
|
||||
|
||||
#define SPOOL_SIZE 0x600000
|
||||
|
||||
@@ -123,6 +128,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_EXITSIZEMOVE:
|
||||
//
|
||||
// A drag or resize just finished. Here rather than in the
|
||||
// cockpit's subclass so it also covers the console screen, which
|
||||
// is where the window gets moved before there is any cockpit to
|
||||
// subclass. No-op unless RP412MFDLAYOUT is save.
|
||||
//
|
||||
RPWindowLayout_Save();
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
@@ -138,6 +152,15 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
|
||||
SetUnhandledExceptionFilter(RPL4CrashDumpFilter);
|
||||
|
||||
//
|
||||
// Which build this is, before anything else can fail. The patch number
|
||||
// is this repository's commit count and the hash beside it names the
|
||||
// commit, so a log from a test machine says exactly where it came from.
|
||||
// A trailing '+' means the tree had uncommitted changes when it was
|
||||
// built. See stamp-version.ps1.
|
||||
//
|
||||
DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush;
|
||||
|
||||
// load up our environment variables
|
||||
//controls
|
||||
if(getenv("L4CONTROLS") == NULL)
|
||||
@@ -152,32 +175,106 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
putenv("TARGETFPS=60");
|
||||
if(getenv("MAXPARTICLES") == NULL)
|
||||
putenv("MAXPARTICLES=8192");
|
||||
FILE *file;
|
||||
char line[1024];
|
||||
if (fopen_s(&file, "environ.ini", "r") == 0)
|
||||
//
|
||||
// environ.ini: written on first run and read here. The exe owns the
|
||||
// template rather than the packaging script laying one down on every
|
||||
// unzip, so a tester can drop a new build over an old folder and keep
|
||||
// their settings. See rpl4environ.h.
|
||||
//
|
||||
RPL4Environ_Load();
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// Test builds have a shelf life.
|
||||
//
|
||||
// A tester still racing a fortnight-old binary reports things that were
|
||||
// fixed a week ago, and the afternoon spent chasing them is gone. So the
|
||||
// build says plainly that it is out of date and stops, rather than
|
||||
// running on and being quietly wrong about what it is.
|
||||
//
|
||||
// This is a nudge, not a lock: the date comes from the machine's own
|
||||
// clock, so anyone determined can wind it back, and RP412NOEXPIRY=1 is
|
||||
// there for us when an old build has to be run on purpose. It is
|
||||
// deliberately not listed in environ.ini - a bypass every tester can see
|
||||
// is a bypass every tester will use, and then the build never goes stale
|
||||
// for the one person it was meant to stop.
|
||||
//
|
||||
// $expireDays in stamp-version.ps1 is what sets this, and 0 turns it off
|
||||
// for a real release.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
#if RP412_EXPIRES
|
||||
{
|
||||
while (!feof(file))
|
||||
const char *no_expiry = getenv("RP412NOEXPIRY");
|
||||
Logical overridden = (no_expiry != NULL && atoi(no_expiry) != 0);
|
||||
|
||||
__time64_t raw_now = _time64(NULL);
|
||||
struct tm today;
|
||||
if (!overridden && _localtime64_s(&today, &raw_now) == 0)
|
||||
{
|
||||
if (fgets(line, sizeof(line), file))
|
||||
int now_stamp =
|
||||
(today.tm_year + 1900) * 10000 + (today.tm_mon + 1) * 100 + today.tm_mday;
|
||||
int expiry_stamp =
|
||||
RP412_EXPIRY_YEAR * 10000 + RP412_EXPIRY_MONTH * 100 + RP412_EXPIRY_DAY;
|
||||
|
||||
if (now_stamp > expiry_stamp)
|
||||
{
|
||||
for (int i = strlen(line); i >= 0; i--)
|
||||
if (line[i] == '\n' || line[i] == '\r')
|
||||
line[i] = 0;
|
||||
// the file is self-documenting: skip comments, blanks,
|
||||
// and anything that is not KEY=VALUE
|
||||
char *setting = line;
|
||||
while (*setting == ' ' || *setting == '\t')
|
||||
++setting;
|
||||
if (*setting == '\0' || *setting == '#' || *setting == ';' ||
|
||||
strchr(setting, '=') == NULL)
|
||||
continue;
|
||||
putenv(setting);
|
||||
DEBUG_STREAM << "Build expired on " << RP412_EXPIRY_TEXT
|
||||
<< " - refusing to run\n" << std::flush;
|
||||
|
||||
char notice[512];
|
||||
sprintf(notice,
|
||||
"This Red Planet test build has expired.\n\n"
|
||||
" Build %s\n"
|
||||
" Expired %s\n\n"
|
||||
"Test builds are good for a fortnight so that nobody spends an "
|
||||
"afternoon chasing something that was fixed a week ago.\n\n"
|
||||
"Grab the current one:\n"
|
||||
"https://gitea.mysticmachines.com/VWE/RP412/releases",
|
||||
RP412_VERSION_LONG, RP412_EXPIRY_TEXT);
|
||||
MessageBoxA(NULL, notice, "Red Planet - test build expired",
|
||||
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// The last few days get a line in the log, so somebody reading a
|
||||
// report can see the build was nearly out rather than wondering.
|
||||
//
|
||||
struct tm expiry_day;
|
||||
memset(&expiry_day, 0, sizeof(expiry_day));
|
||||
expiry_day.tm_year = RP412_EXPIRY_YEAR - 1900;
|
||||
expiry_day.tm_mon = RP412_EXPIRY_MONTH - 1;
|
||||
expiry_day.tm_mday = RP412_EXPIRY_DAY;
|
||||
//
|
||||
// The END of the expiry day, because that is the rule the check
|
||||
// above enforces - the build is good for all of that date and
|
||||
// refuses the morning after. Anchoring at midday instead would
|
||||
// report one day fewer than the build actually has left.
|
||||
//
|
||||
expiry_day.tm_hour = 23;
|
||||
expiry_day.tm_min = 59;
|
||||
expiry_day.tm_sec = 59;
|
||||
expiry_day.tm_isdst = -1;
|
||||
__time64_t expiry_time = _mktime64(&expiry_day);
|
||||
if (expiry_time != (__time64_t) -1)
|
||||
{
|
||||
int days_left = (int)((expiry_time - raw_now) / (24 * 60 * 60));
|
||||
if (days_left <= 3)
|
||||
{
|
||||
DEBUG_STREAM << "Build expires " << RP412_EXPIRY_TEXT << " ("
|
||||
<< days_left << " day(s) left)\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
else if (overridden)
|
||||
{
|
||||
DEBUG_STREAM << "Build expiry (" << RP412_EXPIRY_TEXT
|
||||
<< ") waived by RP412NOEXPIRY\n" << std::flush;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
DEBUG_STREAM << "Red Planet 4.12.5" << std::endl << std::flush;
|
||||
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
|
||||
|
||||
#ifdef RP412_STEAM
|
||||
@@ -254,6 +351,43 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
return FALSE;
|
||||
|
||||
ShowWindow(hWnd, nShowCmd);
|
||||
|
||||
//
|
||||
// Sticky placement, from the first frame the player sees. SVGA16 also
|
||||
// registers and reloads when it builds the cockpit, but that is not
|
||||
// until a mission starts - without this the console screen would come
|
||||
// up at the default rect with its title bar still on, and only jump
|
||||
// to the saved placement once a race began. -fit takes the whole
|
||||
// monitor and has no placement of the player's to restore.
|
||||
//
|
||||
if (!L4Application::GetFitDisplay() && !L4Application::GetFullscreen())
|
||||
{
|
||||
RPWindowLayout_Register(hWnd, "RPL4", True);
|
||||
RPWindowLayout_Load();
|
||||
}
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// RP412JOYCONFIG=1 (joyconfig.bat): the joystick setup wizard, before
|
||||
// anything else claims the screen. It asks the player to move each
|
||||
// control on their stick, HOTAS or pedals and writes the joy* rows of
|
||||
// bindings.txt, then falls through into the game so they can try them
|
||||
// straight away.
|
||||
//
|
||||
// One shot: the variable is cleared from this process so a rebuilt
|
||||
// PadRIO later in the session cannot run the wizard a second time.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
const char *joyconfig = getenv("RP412JOYCONFIG");
|
||||
if (joyconfig != NULL && *joyconfig != '\0' && atoi(joyconfig) != 0)
|
||||
{
|
||||
RPJoyConfigWizard();
|
||||
SetEnvironmentVariableA("RP412JOYCONFIG", NULL);
|
||||
_putenv("RP412JOYCONFIG=");
|
||||
}
|
||||
}
|
||||
|
||||
#if !_DEBUG
|
||||
// Arcade pods have no mouse - but desktop/windowed play needs the
|
||||
// cursor for the on-screen cockpit buttons, so hide it only when
|
||||
@@ -475,6 +609,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
|
||||
// symmetric with the fullscreen-only hide at startup
|
||||
if (L4Application::GetFullscreen())
|
||||
|
||||
+95
-39
@@ -133,20 +133,27 @@ void
|
||||
// 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;
|
||||
int placed = 0;
|
||||
Point3D standFront(0.0f, 0.0f, 0.0f);
|
||||
Point3D standCentre(0.0f, 0.0f, 0.0f);
|
||||
|
||||
Player *p;
|
||||
for (int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; ++rank)
|
||||
for (int i = 0; i < winnersSpotCount; ++i)
|
||||
{
|
||||
if (rank > 7)
|
||||
{
|
||||
break; // only eight spots exist on the stand
|
||||
}
|
||||
*place = (char) ('1' + rank);
|
||||
*place = (char) ('1' + i);
|
||||
spot[i] = NULL;
|
||||
|
||||
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
||||
DropZone *dropzone;
|
||||
@@ -154,10 +161,36 @@ void
|
||||
{
|
||||
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 (dropzone == NULL)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -168,20 +201,9 @@ void
|
||||
continue;
|
||||
}
|
||||
VTV *vtv = (VTV*) vehicle;
|
||||
vtv->Reset(dropzone->localOrigin, VTV::MissionReviewReset);
|
||||
vtv->Reset(spot[rank]->localOrigin, VTV::MissionReviewReset);
|
||||
vtv->SetPerformance(&VTV::DoNothing);
|
||||
vtv->FlushEvents();
|
||||
|
||||
Point3D spot = dropzone->localOrigin.linearPosition;
|
||||
if (placed == 0)
|
||||
{
|
||||
// remember where the front of the stand is - the shot is framed
|
||||
// off it rather than off hardcoded map coordinates
|
||||
standFront = spot;
|
||||
}
|
||||
standCentre += spot;
|
||||
DEBUG_STREAM << "WinnersCircle: " << winners_spot << " at "
|
||||
<< spot.x << "," << spot.y << "," << spot.z << "\n" << std::flush;
|
||||
++placed;
|
||||
}
|
||||
|
||||
@@ -239,25 +261,52 @@ void
|
||||
// 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 middle of the group.
|
||||
// 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.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
standCentre.x /= (Scalar) placed;
|
||||
standCentre.y /= (Scalar) placed;
|
||||
standCentre.z /= (Scalar) placed;
|
||||
Point3D standFront = spot[0]->localOrigin.linearPosition;
|
||||
|
||||
//
|
||||
// Stand off along the line from the middle of the group out through the
|
||||
// front spot, so the shot faces the stand however the map has it turned,
|
||||
// and lift the eye above the top tier to look down on the line-up.
|
||||
// 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 - standCentre.x;
|
||||
facing.x = standFront.x - standBack.x;
|
||||
facing.y = 0.0f;
|
||||
facing.z = standFront.z - standCentre.z;
|
||||
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;
|
||||
}
|
||||
//
|
||||
@@ -285,10 +334,12 @@ void
|
||||
if (tune != NULL) aim_lift = (Scalar) atof(tune);
|
||||
|
||||
Point3D eye;
|
||||
eye.x = standCentre.x + (facing.x / reach) * standoff;
|
||||
eye.y = standCentre.y + height;
|
||||
eye.z = standCentre.z + (facing.z / reach) * standoff;
|
||||
standCentre.y += aim_lift;
|
||||
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
|
||||
@@ -315,7 +366,7 @@ void
|
||||
if (camera_mode == NULL || atoi(camera_mode) != 0)
|
||||
{
|
||||
dpl_renderer->SetViewAngle(Degree(45.0f));
|
||||
dpl_renderer->SetPresentationCamera(eye, standCentre);
|
||||
dpl_renderer->SetPresentationCamera(eye, aim);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -340,8 +391,13 @@ void
|
||||
}
|
||||
dpl_renderer->StartPresentationFadeIn(fade_in);
|
||||
|
||||
DEBUG_STREAM << "WinnersCircle: " << placed << " placed; centre "
|
||||
<< standCentre.x << "," << standCentre.y << "," << standCentre.z
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -352,6 +352,55 @@ namespace
|
||||
gPhase = PhaseStopped;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The countdown the engine shows, taken from the clock that will
|
||||
// actually end the race (gMissionClockHook - see APPMGR.h).
|
||||
//
|
||||
// Called on the game thread, reading two volatile LONGs the console
|
||||
// thread writes with InterlockedExchange. Aligned 32-bit reads, and
|
||||
// a torn value could only mistime the cockpit clock by one tick of
|
||||
// a countdown nobody reads to the millisecond - not worth a lock on
|
||||
// the frame path.
|
||||
//---------------------------------------------------------------
|
||||
Logical MissionClock(Scalar *seconds_remaining)
|
||||
{
|
||||
//
|
||||
// Only answer for the race this console is actually marshalling.
|
||||
// Nothing ever uninstalls the hook, so a player who hosts a race
|
||||
// and then joins somebody else's lobby still has it wired up -
|
||||
// and in that race the console is a bystander whose gLengthMs and
|
||||
// gRunStartTick belong to the previous mission entirely.
|
||||
//
|
||||
if (gWatchedApp == NULL || gWatchedApp != application)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
if (!gMissionRunning)
|
||||
{
|
||||
return False; // not started, or already stopped
|
||||
}
|
||||
LONG length_ms = gLengthMs;
|
||||
if (length_ms <= 0)
|
||||
{
|
||||
return False; // endless: nothing to count down
|
||||
}
|
||||
|
||||
// DWORD subtraction, so a GetTickCount wrap costs nothing
|
||||
LONG elapsed_ms = (LONG)(GetTickCount() - (DWORD) gRunStartTick);
|
||||
LONG left_ms = length_ms - elapsed_ms;
|
||||
if (left_ms < 0)
|
||||
{
|
||||
//
|
||||
// The console polls at 250 ms, so the clock reaches zero
|
||||
// slightly before the stop is dispatched. Hold at zero
|
||||
// rather than showing negative time in the cockpit.
|
||||
//
|
||||
left_ms = 0;
|
||||
}
|
||||
*seconds_remaining = (Scalar) left_ms / 1000.0f;
|
||||
return True;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The game-thread tick: state reporting + engine-safe execution
|
||||
//---------------------------------------------------------------
|
||||
@@ -522,6 +571,10 @@ namespace
|
||||
// game-thread execution point
|
||||
gPerFrameHook = &ConsoleTick;
|
||||
|
||||
// the cockpit clock now counts down the same clock that will stop
|
||||
// the race, rather than the engine's own reckoning of it
|
||||
gMissionClockHook = &MissionClock;
|
||||
|
||||
// results intake from the RP layer
|
||||
gConsoleScoreSink = &CollectFinalScore;
|
||||
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
#include "rpl4.h"
|
||||
#pragma hdrstop
|
||||
|
||||
#include "rpl4environ.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
//########################################################################
|
||||
// environ.ini - see rpl4environ.h for why the exe owns this rather than
|
||||
// the packaging script.
|
||||
//########################################################################
|
||||
|
||||
namespace
|
||||
{
|
||||
const char kEnvironFileName[] = "environ.ini";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// The shipped configuration, verbatim. Lifted out of pack-dist.ps1
|
||||
// so there is one source of truth and the exe alone can produce a
|
||||
// working install.
|
||||
//-------------------------------------------------------------------
|
||||
const char kEnvironTemplate[] =
|
||||
"# ============================================================================\n"
|
||||
"# environ.ini - Red Planet 4.12 configuration\n"
|
||||
"# ============================================================================\n"
|
||||
"# One KEY=VALUE per line, read at game start. Lines starting with # or ;\n"
|
||||
"# are comments; anything without an = is ignored. Delete a line (or\n"
|
||||
"# comment it out) to fall back to the built-in default.\n"
|
||||
"#\n"
|
||||
"# Input bindings live in bindings.txt beside the exe (written with the\n"
|
||||
"# full documented layout on first run; delete it to restore defaults).\n"
|
||||
"#\n"
|
||||
"# Your callsign and loadout are remembered in pilot.cfg beside the exe.\n"
|
||||
"# Set them on the setup screen once and they come back every session,\n"
|
||||
"# however you left - launching, joining a lobby, or quitting. Delete\n"
|
||||
"# that file to start over.\n"
|
||||
"\n"
|
||||
"# ---- Core (the shipped configuration) --------------------------------------\n"
|
||||
"\n"
|
||||
"# Control stack: tokens separated by ; or , processed left to right.\n"
|
||||
"# PAD the virtual RIO (XInput controller + keyboard,\n"
|
||||
"# rebindable via bindings.txt)\n"
|
||||
"# RIO real serial cockpit hardware on COM1\n"
|
||||
"# RIO:COMn same, on another port (RIO:COM3, ...)\n"
|
||||
"# KEYBOARD the engine keyboard handler\n"
|
||||
"# MOUSE, JOYSTICK, FLIGHTSTICKPRO, THRUSTMASTER, DIJOYSTICK\n"
|
||||
"# legacy pointer/joystick drivers (untested here)\n"
|
||||
"# Unset falls back to KEYBOARD alone.\n"
|
||||
"L4CONTROLS=PAD;KEYBOARD\n"
|
||||
"\n"
|
||||
"# Renderer bring-up argument. Only its presence is checked (the DPL\n"
|
||||
"# resolution parsing it once fed is gone) and the game refuses to start\n"
|
||||
"# without it - any non-empty value works. Leave as shipped.\n"
|
||||
"DPLARG=1\n"
|
||||
"\n"
|
||||
"# DPL (renderer/scene) configuration file, searched beside the exe.\n"
|
||||
"# Any notation file name; RPDPL.INI is the one that ships.\n"
|
||||
"L4DPLCFG=RPDPL.INI\n"
|
||||
"\n"
|
||||
"# Gauge (MFD/instrument) canvas. Must name a page of GAUGE\\L4GAUGE.INI:\n"
|
||||
"# 640x480x8 | 640x480x16 | 800x600x16\n"
|
||||
"# Unset disables the gauge renderer (and with it all MFDs).\n"
|
||||
"L4GAUGE=640x480x16\n"
|
||||
"\n"
|
||||
"# Plasma display.\n"
|
||||
"# SCREEN render the pod's plasma glass in-window (currently\n"
|
||||
"# parked off-layout)\n"
|
||||
"# COM1, COM2... drive real plasma glass on that serial port\n"
|
||||
"# (9600 baud, N81)\n"
|
||||
"# Unset = no plasma display.\n"
|
||||
"L4PLASMA=SCREEN\n"
|
||||
"\n"
|
||||
"# 0 = classic separate gauge windows; 1 = the single-window glass\n"
|
||||
"# cockpit (all seven displays composed on a locked 1920x1080 canvas\n"
|
||||
"# around the viewscreen); 2 = exploded diagnostic view (each display\n"
|
||||
"# in its own native-resolution desktop window - MFDs 640x480, map\n"
|
||||
"# 480x640 - decoded exactly as the pod's VDB split them, no downscale).\n"
|
||||
"L4MFDSPLIT=1\n"
|
||||
"\n"
|
||||
"# The game window - and in the exploded view (L4MFDSPLIT=2) each display\n"
|
||||
"# window - is placed fresh every launch, so moving one somewhere useful\n"
|
||||
"# never survived the menu-race-menu loop. This remembers where you put\n"
|
||||
"# them, in mfd_layout.cfg beside this file:\n"
|
||||
"# off / 0 / unset computed placement only, no file (default)\n"
|
||||
"# load put the windows back where they were saved\n"
|
||||
"# save the same, and re-save on every finished drag\n"
|
||||
"# The game window gets its size back too, so you can size the cockpit to\n"
|
||||
"# suit your monitor once and keep it. The display windows get position\n"
|
||||
"# only: their size follows their content and their button banks, so an\n"
|
||||
"# old one is never restored over them. Arrange everything once with\n"
|
||||
"# save, then leave it on load.\n"
|
||||
"#\n"
|
||||
"# The plasma display window takes part too, under \"Plasma Display\".\n"
|
||||
"#\n"
|
||||
"# Each line in mfd_layout.cfg reads <title>=<x>,<y>,<w>,<h>, and you can\n"
|
||||
"# append ,noframe to take that window's title bar and border off - a\n"
|
||||
"# cockpit that fills the monitor edge to edge without -fit taking the\n"
|
||||
"# whole screen. Put the window where you want it first: a bare window\n"
|
||||
"# has nothing to drag by. Delete the flag to get the frame back.\n"
|
||||
"#RP412MFDLAYOUT=off\n"
|
||||
"\n"
|
||||
"# Size of the six secondary displays in the glass cockpit, as a\n"
|
||||
"# percentage of their pod size. The pod bolted them down at one size;\n"
|
||||
"# on a big panel there is room to trade viewscreen for instrument, so\n"
|
||||
"# turn these up if you want to actually read the other displays while\n"
|
||||
"# you fly. 100 = as the pod had them. Range 25-200 (out-of-range and\n"
|
||||
"# unreadable values fall back to the group setting, then to 100).\n"
|
||||
"#\n"
|
||||
"# The scaling is applied in canvas units, before the cockpit is fitted\n"
|
||||
"# to your window, so a given number looks the same on every monitor.\n"
|
||||
"# The layout stays legal whatever you ask for - the panes are clamped\n"
|
||||
"# against their actual neighbours, shrinking uniformly so a display\n"
|
||||
"# never comes out stretched. They do overlap the viewscreen, exactly\n"
|
||||
"# as the pod's bezels did, but never each other.\n"
|
||||
"#\n"
|
||||
"# L4MFDSCALE sets all five green MFDs at once.\n"
|
||||
"L4MFDSCALE=100\n"
|
||||
"\n"
|
||||
"# ...and any single display can override it. Uncomment one to size it\n"
|
||||
"# on its own - useful if you only care about, say, the damage readout.\n"
|
||||
"# UL upper left UC upper center UR upper right\n"
|
||||
"# LL lower left LR lower right\n"
|
||||
"#L4MFDSCALE_UL=100\n"
|
||||
"#L4MFDSCALE_UC=100\n"
|
||||
"#L4MFDSCALE_UR=100\n"
|
||||
"#L4MFDSCALE_LL=100\n"
|
||||
"#L4MFDSCALE_LR=100\n"
|
||||
"\n"
|
||||
"# The portrait radar/map, sized on its own (it already sits at 1.35x\n"
|
||||
"# the MFDs by default). It shares the canvas with whichever MFD is\n"
|
||||
"# above it, so at extreme settings one of the two gives way.\n"
|
||||
"L4RADARSCALE=100\n"
|
||||
"\n"
|
||||
"# Where the radar sits:\n"
|
||||
"# CENTER bottom centre, under the viewscreen, as the pod had it\n"
|
||||
"# (default; BOTTOM and CENTRE mean the same)\n"
|
||||
"# LEFT bottom left corner (or BOTTOMLEFT)\n"
|
||||
"# RIGHT bottom right corner (or BOTTOMRIGHT)\n"
|
||||
"# MIDLEFT left edge, halfway up (or LEFTCENTER / LEFTCENTRE)\n"
|
||||
"# MIDRIGHT right edge, halfway up (or RIGHTCENTER / RIGHTCENTRE)\n"
|
||||
"# Anywhere but CENTER stops it blocking the middle of the road, which\n"
|
||||
"# is worth having on a wide screen.\n"
|
||||
"#\n"
|
||||
"# In a bottom corner it is one of three panes along the bottom, and the\n"
|
||||
"# lower MFD whose corner it takes slides inboard beside it. Halfway up\n"
|
||||
"# a side it leaves the bottom row entirely and sits between that side's\n"
|
||||
"# two MFDs - roomy on a tall radar, but if the MFDs on that side are\n"
|
||||
"# also scaled up, the radar is the one that gives way (it has to clear\n"
|
||||
"# both of them, and it grows from the middle in both directions).\n"
|
||||
"L4RADARPOS=CENTER\n"
|
||||
"\n"
|
||||
"# The Winners Circle: at the end of a race the finishers are stood on\n"
|
||||
"# the award platform in finishing order, with each pilot's callsign on\n"
|
||||
"# the plate beside their spot, and held there for a few seconds before\n"
|
||||
"# the results screen. 1 = show it, 0 = straight to the results.\n"
|
||||
"RP412PODIUM=1\n"
|
||||
"\n"
|
||||
"# The shot is framed for you, but these move the camera if you want it\n"
|
||||
"# somewhere else. Distances are in game units, measured from the middle\n"
|
||||
"# of the group of finishers.\n"
|
||||
"# STANDOFF how far out in front of the stand the camera sits\n"
|
||||
"# HEIGHT how far above the group\n"
|
||||
"# AIM height of the point it looks at, relative to the group -\n"
|
||||
"# negative tilts down, positive tilts up\n"
|
||||
"# ASPECT the stand was composed for a 4:3 pod monitor, so the shot\n"
|
||||
"# is cropped to that shape with black either side. 0 runs it\n"
|
||||
"# full width instead.\n"
|
||||
"# FADEIN seconds to come up out of the black after the race fades\n"
|
||||
"# CAM 0 watches from your own cockpit rather than off the stand\n"
|
||||
"#RP412PODIUMSTANDOFF=36\n"
|
||||
"#RP412PODIUMHEIGHT=12\n"
|
||||
"#RP412PODIUMAIM=2\n"
|
||||
"#RP412PODIUMASPECT=1.333\n"
|
||||
"#RP412PODIUMFADEIN=0.45\n"
|
||||
"#RP412PODIUMCAM=1\n"
|
||||
"\n"
|
||||
"# Override the game length the menu picked, in seconds. The shortest the\n"
|
||||
"# menu offers is 3:00, which is a long wait when what you are testing is\n"
|
||||
"# what happens at the buzzer. Unset = use the menu's choice.\n"
|
||||
"#RP412MISSIONSECONDS=20\n"
|
||||
"\n"
|
||||
"# Simulation/render frame rate, integer frames/second. The desktop\n"
|
||||
"# default is 60; the arcade pods shipped at 25.\n"
|
||||
"TARGETFPS=60\n"
|
||||
"\n"
|
||||
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n"
|
||||
"# running and steam_appid.txt beside the exe; without them the game\n"
|
||||
"# logs the reason and falls back to plain TCP. 0 = TCP only.\n"
|
||||
"RP412STEAM=1\n"
|
||||
"\n"
|
||||
"# Line up each remote player's clock with ours, so their vehicle is\n"
|
||||
"# extrapolated from when its update was SENT rather than when it\n"
|
||||
"# arrived. Without it every remote pod sits one network latency behind\n"
|
||||
"# where it should be - invisible on the 1ms arcade LAN the engine was\n"
|
||||
"# written for, a constant 50-150ms of lag over the internet. 0 restores\n"
|
||||
"# the old arrival-time behaviour if you want to compare.\n"
|
||||
"#RP412NETCLOCK=0\n"
|
||||
"\n"
|
||||
"# ---- Optional ---------------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to\n"
|
||||
"# lamp buttons glow with the panel, flash modes and all.\n"
|
||||
"# Unset or nonzero = on (the default); 0 = off.\n"
|
||||
"#RP412KEYLIGHT=0\n"
|
||||
"\n"
|
||||
"# Invert the stick on top of whatever bindings.txt produces:\n"
|
||||
"# X = invert X only, Y = invert Y only, XY = both (case-insensitive).\n"
|
||||
"#L4PADFLIP=XY\n"
|
||||
"\n"
|
||||
"# Anti-aliasing sample count, passed straight to Direct3D 9:\n"
|
||||
"# 0 = off, else 2..16 as the GPU supports (1 selects the driver's\n"
|
||||
"# \"nonmaskable\" mode; unsupported counts fail device creation).\n"
|
||||
"#MULTISAMPLE=0\n"
|
||||
"\n"
|
||||
"# Particle budget, integer. Default 8192.\n"
|
||||
"#MAXPARTICLES=8192\n"
|
||||
"\n"
|
||||
"# On-screen plasma glass (L4PLASMA=SCREEN only). SCALE = integer pixel\n"
|
||||
"# size 1..16, default 4 (out-of-range values are ignored). POS = window\n"
|
||||
"# top-left as X,Y screen coordinates; unset = auto, parked below the\n"
|
||||
"# main window.\n"
|
||||
"#L4PLASMASCALE=4\n"
|
||||
"#L4PLASMAPOS=0,0\n"
|
||||
"\n"
|
||||
"# Fixed random seed (repeatable runs): any unsigned integer.\n"
|
||||
"# Unset seeds from the clock.\n"
|
||||
"#RANDOM=12345\n"
|
||||
"\n"
|
||||
"# ---- LAN play without Steam -------------------------------------------------\n"
|
||||
"# Host a race over plain TCP: list the member pods' console channels\n"
|
||||
"# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501).\n"
|
||||
"# RP412HOSTPODS comma-separated IP[:port] list, one entry per member\n"
|
||||
"# pod; port defaults to 1501 per entry\n"
|
||||
"# RP412HOSTPORT this machine's console port, integer > 0\n"
|
||||
"# (default 1501)\n"
|
||||
"# RP412HOSTADDR this machine's LAN IP as members can reach it\n"
|
||||
"# (default 127.0.0.1)\n"
|
||||
"#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501\n"
|
||||
"#RP412HOSTPORT=1501\n"
|
||||
"#RP412HOSTADDR=192.168.1.10\n"
|
||||
"\n"
|
||||
"# ---- Developer / testing ----------------------------------------------------\n"
|
||||
"\n"
|
||||
"# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision,\n"
|
||||
"# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump.\n"
|
||||
"# 0 or unset = off. (Alt+Q, the mission abort, is always live.)\n"
|
||||
"#RP412DEVKEYS=1\n"
|
||||
"\n"
|
||||
"# Console race-length override, integer seconds (short test races).\n"
|
||||
"# Values <= 0 are ignored.\n"
|
||||
"#L4CONSOLELEN=30\n"
|
||||
"\n"
|
||||
"# Nonzero = Steam transport loopback self-test at boot (logs PASS/FAIL).\n"
|
||||
"#RP412STEAMSELFTEST=1\n"
|
||||
"\n"
|
||||
"# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ----------\n"
|
||||
"# PRIMGAUGE / SECGAUGE / MFDGAUGE / MFDGAUGE2 pin a display to a monitor\n"
|
||||
"# by adapter index (0, 1, 2...). SPANDISABLE: 0 = let the MFDs span one\n"
|
||||
"# wide surface, nonzero = separate windows (setting MFDGAUGE2 alone also\n"
|
||||
"# forces spanning off). L4EYES = \"x y z xrot yrot zrot [type]\" floats\n"
|
||||
"# for a detached camera; a type starting with r offsets it relative to\n"
|
||||
"# the pod. L4INTERCOM enables the crew intercom - only its presence\n"
|
||||
"# matters (traditionally COM2). NOMODES skips the mode/lamp programming;\n"
|
||||
"# presence alone triggers it, even NOMODES=0. LOGSIZE > 0 sizes the\n"
|
||||
"# trace log in dev builds compiled with tracing.\n"
|
||||
"#PRIMGAUGE=1\n"
|
||||
"#SECGAUGE=2\n"
|
||||
"#MFDGAUGE=3\n"
|
||||
"#MFDGAUGE2=4\n"
|
||||
"#SPANDISABLE=1\n"
|
||||
"#L4EYES=1\n"
|
||||
"#L4INTERCOM=COM2\n"
|
||||
"#NOMODES=1\n"
|
||||
"#LOGSIZE=1000000\n"
|
||||
;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Does the player's file mention this key at all - set, or commented
|
||||
// out, or with whitespace in front of it?
|
||||
//
|
||||
// Deliberately generous: a key that is mentioned in ANY form is left
|
||||
// alone. The alternative failure is worse than a missed notice, since
|
||||
// environ.ini is applied line by line and a second copy of a key
|
||||
// further down the file would silently override the player's own.
|
||||
//-------------------------------------------------------------------
|
||||
Logical FileMentionsKey(const char *text, const char *key, int key_length)
|
||||
{
|
||||
const char *cursor = text;
|
||||
while ((cursor = strstr(cursor, key)) != NULL)
|
||||
{
|
||||
//
|
||||
// Must be a whole key: preceded by start-of-line, whitespace
|
||||
// or a comment mark, and followed by '='.
|
||||
//
|
||||
const char *after = cursor + key_length;
|
||||
Logical starts_token =
|
||||
(cursor == text) ||
|
||||
(cursor[-1] == '\n') || (cursor[-1] == '\r') ||
|
||||
(cursor[-1] == ' ') || (cursor[-1] == '\t') ||
|
||||
(cursor[-1] == '#') || (cursor[-1] == ';');
|
||||
if (starts_token)
|
||||
{
|
||||
const char *scan = after;
|
||||
while (*scan == ' ' || *scan == '\t')
|
||||
{
|
||||
++scan;
|
||||
}
|
||||
if (*scan == '=')
|
||||
{
|
||||
return True;
|
||||
}
|
||||
}
|
||||
cursor = after;
|
||||
}
|
||||
return False;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Name every template key the player's file has never heard of. Not
|
||||
// a fix - their file stays theirs - but it puts the reason for a
|
||||
// missing feature in the log we already ask testers for.
|
||||
//-------------------------------------------------------------------
|
||||
void ReportUnmentionedKeys(const char *file_text)
|
||||
{
|
||||
char missing[1024]; // what gets printed
|
||||
char seen[1024]; // the same keys as "KEY=", so the mention
|
||||
// test above can dedupe against them
|
||||
missing[0] = '\0';
|
||||
seen[0] = '\0';
|
||||
int count = 0; // how many are missing
|
||||
int listed = 0; // how many fitted in the line
|
||||
|
||||
const char *cursor = kEnvironTemplate;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
const char *line = cursor;
|
||||
const char *end = strchr(line, '\n');
|
||||
int length = (end != NULL) ? (int)(end - line) : (int) strlen(line);
|
||||
cursor = (end != NULL) ? (end + 1) : (line + length);
|
||||
|
||||
//
|
||||
// A template key line is "KEY=..." or "#KEY=..." - the
|
||||
// commented ones are options that ship switched off, and a
|
||||
// player who has never seen them wants to know they exist.
|
||||
//
|
||||
const char *scan = line;
|
||||
int remaining = length;
|
||||
if (remaining > 0 && *scan == '#')
|
||||
{
|
||||
++scan;
|
||||
--remaining;
|
||||
}
|
||||
if (remaining <= 0 || !(isalpha((unsigned char) *scan) || *scan == '_'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int key_length = 0;
|
||||
while (key_length < remaining &&
|
||||
(isalnum((unsigned char) scan[key_length]) || scan[key_length] == '_'))
|
||||
{
|
||||
++key_length;
|
||||
}
|
||||
if (key_length >= remaining || scan[key_length] != '=' || key_length > 60)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char key[64];
|
||||
memcpy(key, scan, key_length);
|
||||
key[key_length] = '\0';
|
||||
|
||||
if (FileMentionsKey(file_text, key, key_length))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//
|
||||
// Templates list some keys twice (documented once, shown
|
||||
// again in an example); do not name one twice.
|
||||
//
|
||||
if (FileMentionsKey(seen, key, key_length))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
++count;
|
||||
if (strlen(seen) + key_length + 3 < sizeof(seen))
|
||||
{
|
||||
strcat(seen, key);
|
||||
strcat(seen, "=\n");
|
||||
}
|
||||
if (strlen(missing) + key_length + 3 < sizeof(missing))
|
||||
{
|
||||
if (missing[0] != '\0')
|
||||
{
|
||||
strcat(missing, ", ");
|
||||
}
|
||||
strcat(missing, key);
|
||||
++listed;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
//
|
||||
// Say when the list is short of the count rather than letting
|
||||
// a full buffer quietly shorten the answer.
|
||||
//
|
||||
DEBUG_STREAM << "Environ: " << kEnvironFileName << " does not mention "
|
||||
<< count << " option(s) this build knows: " << missing;
|
||||
if (listed < count)
|
||||
{
|
||||
DEBUG_STREAM << ", and " << (count - listed) << " more";
|
||||
}
|
||||
DEBUG_STREAM << "\nEnviron: they are at their built-in defaults - delete "
|
||||
<< kEnvironFileName << " to get the documented file back\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RPL4Environ_Load()
|
||||
{
|
||||
//
|
||||
// First run: lay down the documented default. From here on the file
|
||||
// belongs to whoever is sitting at this machine.
|
||||
//
|
||||
FILE *file = fopen(kEnvironFileName, "rb");
|
||||
if (file == NULL)
|
||||
{
|
||||
FILE *out = fopen(kEnvironFileName, "wb");
|
||||
if (out != NULL)
|
||||
{
|
||||
fwrite(kEnvironTemplate, 1, strlen(kEnvironTemplate), out);
|
||||
fclose(out);
|
||||
DEBUG_STREAM << "Environ: wrote default " << kEnvironFileName
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "Environ: could not write " << kEnvironFileName
|
||||
<< " - running on built-in defaults\n" << std::flush;
|
||||
}
|
||||
file = fopen(kEnvironFileName, "rb");
|
||||
}
|
||||
if (file == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(file, 0, SEEK_END);
|
||||
long size = ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
if (size <= 0)
|
||||
{
|
||||
fclose(file);
|
||||
return;
|
||||
}
|
||||
char *text = new char[size + 1];
|
||||
size_t read = fread(text, 1, size, file);
|
||||
text[read] = '\0';
|
||||
fclose(file);
|
||||
|
||||
//
|
||||
// One KEY=VALUE per line. Comments, blanks and anything without an
|
||||
// '=' are skipped; everything else goes into the environment, which
|
||||
// is why a line here beats a variable set in the shell.
|
||||
//
|
||||
int applied = 0;
|
||||
char line[1024];
|
||||
const char *cursor = text;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
int length = 0;
|
||||
while (cursor[length] != '\0' && cursor[length] != '\n' &&
|
||||
length < (int) sizeof(line) - 1)
|
||||
{
|
||||
line[length] = cursor[length];
|
||||
++length;
|
||||
}
|
||||
line[length] = '\0';
|
||||
cursor += length;
|
||||
while (*cursor == '\n' || *cursor == '\r')
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
for (int i = length - 1; i >= 0; --i)
|
||||
{
|
||||
if (line[i] == '\r' || line[i] == '\n')
|
||||
{
|
||||
line[i] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
char *setting = line;
|
||||
while (*setting == ' ' || *setting == '\t')
|
||||
{
|
||||
++setting;
|
||||
}
|
||||
if (*setting == '\0' || *setting == '#' || *setting == ';' ||
|
||||
strchr(setting, '=') == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
putenv(setting);
|
||||
++applied;
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "Environ: " << applied << " setting(s) from "
|
||||
<< kEnvironFileName << "\n" << std::flush;
|
||||
|
||||
ReportUnmentionedKeys(text);
|
||||
|
||||
delete[] text;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//===========================================================================//
|
||||
// File: rpl4environ.h //
|
||||
// Project: MUNGA Brick: Red Planet LBE Application //
|
||||
// Contents: environ.ini - written on first run, then the player's //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||
// PROPRIETARY AND CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "..\munga\style.h"
|
||||
|
||||
//########################################################################
|
||||
//
|
||||
// environ.ini is the game's configuration: one KEY=VALUE per line, read
|
||||
// once at startup and pushed into the environment, so every option the
|
||||
// engine reads through getenv can be set from a file a player can open.
|
||||
//
|
||||
// The exe owns the template and writes it when the file is absent, the
|
||||
// same way bindings.txt works, rather than the packaging script laying
|
||||
// one down on every unzip. That is what lets a tester drop a new build
|
||||
// over an old folder and keep their settings: the file is theirs from
|
||||
// the moment it exists, and nothing overwrites it.
|
||||
//
|
||||
// It cannot simply be optional. Without it L4GAUGE is unset, which
|
||||
// disables the gauge renderer and takes every MFD with it, and
|
||||
// L4MFDSPLIT is unset, which is the packed-window arcade layout rather
|
||||
// than the glass cockpit. The shipped values are the desktop game; the
|
||||
// built-in getenv fallbacks are the 1995 pod.
|
||||
//
|
||||
// The cost of a file that is never overwritten is that a tester carrying
|
||||
// one across many builds stops being offered new options. Options added
|
||||
// later default to "behave as before", so nothing breaks - but it does
|
||||
// go unnoticed, so the load names any template key the player's file
|
||||
// does not mention. That line in rpl4.log is what turns "the podium does
|
||||
// not work" into "your environ.ini predates RP412PODIUM".
|
||||
//
|
||||
//########################################################################
|
||||
|
||||
// Write environ.ini if it is not there, then read it into the
|
||||
// environment. Call once, before anything reads a setting.
|
||||
void
|
||||
RPL4Environ_Load();
|
||||
+336
-15
@@ -177,6 +177,7 @@ namespace
|
||||
GroupLaunch,
|
||||
GroupSteamHost, // buttons, not selections (Steam builds only)
|
||||
GroupSteamJoin,
|
||||
GroupExit,
|
||||
GroupCount
|
||||
};
|
||||
|
||||
@@ -230,6 +231,241 @@ namespace
|
||||
Logical gHavePersist = False;
|
||||
char gLastPilotName[24] = "Pilot";
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// What the player set last time, in pilot.cfg beside bindings.txt:
|
||||
// the callsign they typed and the loadout they picked.
|
||||
//
|
||||
// The loadout has always survived a race - gPersistSelection below
|
||||
// is why the menu reopens the way you left it - but only for as
|
||||
// long as the process lives. BT411 keeps the same things in
|
||||
// fe_last.ini and had to, since it relaunches between missions;
|
||||
// RP412 stayed in one process and so never needed a file. Closing
|
||||
// the game was still a reset, which is what this fixes.
|
||||
//
|
||||
// KEY=VALUE like environ.ini, one line per group.
|
||||
//---------------------------------------------------------------
|
||||
const char kPilotFileName[] = "pilot.cfg";
|
||||
|
||||
//
|
||||
// Stable file keys, independent of the on-screen headings - those
|
||||
// carry spaces ("TIME OF DAY") and are free to be reworded.
|
||||
//
|
||||
struct GroupKey
|
||||
{
|
||||
int group;
|
||||
const char *key;
|
||||
};
|
||||
const GroupKey kGroupKeys[] =
|
||||
{
|
||||
{ GroupScenario, "scenario" }, { GroupMap, "track" },
|
||||
{ GroupVehicle, "vehicle" }, { GroupColor, "color" },
|
||||
{ GroupBadge, "badge" }, { GroupTeam, "team" },
|
||||
{ GroupPosition, "position" }, { GroupTime, "time" },
|
||||
{ GroupWeather, "weather" }, { GroupLength, "length" },
|
||||
};
|
||||
|
||||
//
|
||||
// How many rows a group offers, so a stale or hand-edited index
|
||||
// cannot select past the end of a list. The track list is the one
|
||||
// that moves - football and the death race carry different maps -
|
||||
// so it answers for whichever scenario is selected.
|
||||
//
|
||||
int GroupSize(int group, const int *selection)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case GroupScenario: return FE_COUNT(kScenarios);
|
||||
case GroupMap:
|
||||
{
|
||||
int count = 0;
|
||||
ActiveMaps(selection, &count);
|
||||
return count;
|
||||
}
|
||||
case GroupVehicle: return FE_COUNT(kVehicles);
|
||||
case GroupColor: return FE_COUNT(kColors);
|
||||
case GroupBadge: return FE_COUNT(kBadges);
|
||||
case GroupTeam: return FE_COUNT(kTeams);
|
||||
case GroupPosition: return FE_COUNT(kPositions);
|
||||
case GroupTime: return FE_COUNT(kTimes);
|
||||
case GroupWeather: return FE_COUNT(kWeather);
|
||||
case GroupLength: return FE_COUNT(kLengths);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// A callsign is quoted into frontend.egg, joined into a
|
||||
// comma-separated list for the results screen, and published as
|
||||
// Steam lobby member data. Anything that could end a token early
|
||||
// therefore has to go - a comma alone would split one pilot into
|
||||
// two on the score sheet. Applied to what is typed as well as to
|
||||
// what is read back, so the file cannot hold what the game will
|
||||
// not accept.
|
||||
//
|
||||
void SanitizeCallsign(char *name, int size)
|
||||
{
|
||||
char clean[64];
|
||||
int out = 0;
|
||||
for (int i = 0; name[i] != '\0' && out < (int) sizeof(clean) - 1; ++i)
|
||||
{
|
||||
unsigned char c = (unsigned char) name[i];
|
||||
if (c < 32 || c > 126) continue; // controls, high bytes
|
||||
if (c == ',' || c == '"') continue; // egg and CSV delimiters
|
||||
if (c == '#' || c == ';') continue; // pilot.cfg comment marks
|
||||
clean[out++] = (char) c;
|
||||
}
|
||||
clean[out] = '\0';
|
||||
|
||||
char *start = clean;
|
||||
while (*start == ' ' || *start == '\t')
|
||||
{
|
||||
++start;
|
||||
}
|
||||
int end = (int) strlen(start);
|
||||
while (end > 0 && (start[end - 1] == ' ' || start[end - 1] == '\t'))
|
||||
{
|
||||
start[--end] = '\0';
|
||||
}
|
||||
|
||||
if (start[0] == '\0')
|
||||
{
|
||||
strcpy(start, "Pilot");
|
||||
}
|
||||
strncpy(name, start, size - 1);
|
||||
name[size - 1] = '\0';
|
||||
}
|
||||
|
||||
void SavePilotSettings(const char *name, const int *selection)
|
||||
{
|
||||
FILE *file = fopen(kPilotFileName, "wt");
|
||||
if (file == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "FrontEnd: could not write " << kPilotFileName
|
||||
<< "\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
fputs("# RP412 pilot settings, written by the game on the way out of\n"
|
||||
"# the setup screen. Delete this file to start over.\n", file);
|
||||
fprintf(file, "callsign=%s\n", name);
|
||||
if (selection != NULL)
|
||||
{
|
||||
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
|
||||
{
|
||||
fprintf(file, "%s=%d\n", kGroupKeys[i].key,
|
||||
selection[kGroupKeys[i].group]);
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
DEBUG_STREAM << "FrontEnd: saved callsign \"" << name << "\""
|
||||
<< ((selection != NULL) ? " and loadout" : "")
|
||||
<< " to " << kPilotFileName << "\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// Read once per run. Absent or unreadable simply leaves the
|
||||
// built-in defaults in place - a missing file is a first run, not
|
||||
// an error, and every value is range-checked so a hand-edited or
|
||||
// out-of-date file cannot select past the end of a list.
|
||||
//
|
||||
void EnsurePilotSettingsLoaded()
|
||||
{
|
||||
static Logical loaded = False;
|
||||
if (loaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
loaded = True;
|
||||
|
||||
FILE *file = fopen(kPilotFileName, "rt");
|
||||
if (file == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int selection[GroupCount];
|
||||
memset(selection, 0, sizeof(selection));
|
||||
selection[GroupLength] = 2; // 5:00, as the menu defaults
|
||||
Logical have_loadout = False;
|
||||
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), file) != NULL)
|
||||
{
|
||||
char *cursor = line;
|
||||
while (*cursor == ' ' || *cursor == '\t')
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
if (*cursor == '#' || *cursor == ';')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
char *equals = strchr(cursor, '=');
|
||||
if (equals == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
*equals = '\0';
|
||||
char *key = cursor;
|
||||
char *value = equals + 1;
|
||||
while (*value == ' ' || *value == '\t')
|
||||
{
|
||||
++value;
|
||||
}
|
||||
char *newline = strpbrk(value, "\r\n");
|
||||
if (newline != NULL)
|
||||
{
|
||||
*newline = '\0';
|
||||
}
|
||||
|
||||
if (_stricmp(key, "callsign") == 0)
|
||||
{
|
||||
char candidate[24];
|
||||
strncpy(candidate, value, sizeof(candidate) - 1);
|
||||
candidate[sizeof(candidate) - 1] = '\0';
|
||||
SanitizeCallsign(candidate, sizeof(candidate));
|
||||
strcpy(gLastPilotName, candidate);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
|
||||
{
|
||||
if (_stricmp(key, kGroupKeys[i].key) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int index = atoi(value);
|
||||
if (index >= 0 && index < GroupSize(kGroupKeys[i].group, selection))
|
||||
{
|
||||
selection[kGroupKeys[i].group] = index;
|
||||
have_loadout = True;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
//
|
||||
// The track list belongs to the scenario, and the file is read in
|
||||
// whatever order it was written, so re-check the track once the
|
||||
// scenario is settled - the same clamp the menu applies when the
|
||||
// scenario is switched by hand.
|
||||
//
|
||||
if (have_loadout)
|
||||
{
|
||||
int map_count = 0;
|
||||
ActiveMaps(selection, &map_count);
|
||||
if (selection[GroupMap] >= map_count)
|
||||
{
|
||||
selection[GroupMap] = 0;
|
||||
}
|
||||
memcpy(gPersistSelection, selection, sizeof(gPersistSelection));
|
||||
gHavePersist = True;
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "FrontEnd: callsign \"" << gLastPilotName << "\""
|
||||
<< (have_loadout ? " and loadout" : "")
|
||||
<< " from " << kPilotFileName << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// [pilots]-order names of the last launched race (owner first),
|
||||
// comma separated - the network console labels results with them
|
||||
char gLastPilotNamesCsv[256] = "";
|
||||
@@ -413,8 +649,9 @@ namespace
|
||||
launch->rect.right = col3 + col_w;
|
||||
launch->rect.bottom = client_h - row_h;
|
||||
|
||||
// Steam lobby buttons (only when the Steam wire is live)
|
||||
if (RPL4Lobby_Available())
|
||||
// Steam lobby buttons, offered whenever environ.ini asked for Steam.
|
||||
// Painting greys them out and says so if the wire never came up.
|
||||
if (RPL4Lobby_Configured())
|
||||
{
|
||||
FEItem *host = &fe->items[fe->itemCount++];
|
||||
host->group = GroupSteamHost;
|
||||
@@ -433,6 +670,23 @@ namespace
|
||||
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
|
||||
if (fe->nameEdit != NULL)
|
||||
{
|
||||
@@ -484,6 +738,7 @@ namespace
|
||||
case GroupLaunch: return "L A U N C H G A M E";
|
||||
case GroupSteamHost: return "HOST STEAM GAME";
|
||||
case GroupSteamJoin: return "JOIN STEAM GAME";
|
||||
case GroupExit: return "EXIT GAME";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -544,12 +799,38 @@ namespace
|
||||
RECT row = item->rect;
|
||||
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);
|
||||
DeleteObject(launch_brush);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
SetTextColor(mem, ink);
|
||||
DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -613,7 +894,17 @@ namespace
|
||||
}
|
||||
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);
|
||||
}
|
||||
else
|
||||
@@ -1163,6 +1454,10 @@ Logical
|
||||
{
|
||||
gLastLaunchMode = FELaunchSingle;
|
||||
|
||||
// before the lobby branch below: a member rejoining a room publishes
|
||||
// the callsign as member data without the menu ever opening
|
||||
EnsurePilotSettingsLoaded();
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Coming back from a race while still in a lobby: straight to
|
||||
// the room (the lobby outlives races - single binary payoff)
|
||||
@@ -1192,6 +1487,7 @@ Logical
|
||||
{
|
||||
FEState fe;
|
||||
memset(&fe, 0, sizeof(fe));
|
||||
EnsurePilotSettingsLoaded();
|
||||
strcpy(fe.pilotName, gLastPilotName);
|
||||
if (gHavePersist)
|
||||
{
|
||||
@@ -1290,20 +1586,41 @@ Logical
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Harvest the loadout whichever way we leave the menu - the
|
||||
// lobby publishes it as member data, launches build from it
|
||||
// Harvest the callsign whichever way we leave the menu, INCLUDING
|
||||
// a close: typing a name and then quitting is how somebody sets it
|
||||
// for next time, and losing it there would be the one case that
|
||||
// makes the whole thing feel unreliable. The loadout still only
|
||||
// persists on a real exit - it is picked, not typed, and the menu
|
||||
// reopens with it visible anyway.
|
||||
//---------------------------------------------------------------
|
||||
if (!fe.closed)
|
||||
if (fe.nameEdit != NULL)
|
||||
{
|
||||
fe.pilotName[0] = '\0';
|
||||
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
|
||||
if (fe.pilotName[0] == '\0')
|
||||
{
|
||||
strcpy(fe.pilotName, "Pilot");
|
||||
}
|
||||
strcpy(gLastPilotName, fe.pilotName);
|
||||
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
|
||||
gHavePersist = True;
|
||||
SanitizeCallsign(fe.pilotName, sizeof(fe.pilotName));
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(fe.pilotName, gLastPilotName);
|
||||
}
|
||||
|
||||
strcpy(gLastPilotName, fe.pilotName);
|
||||
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
|
||||
gHavePersist = True;
|
||||
|
||||
//
|
||||
// Written on the way out however the player leaves - launching,
|
||||
// stepping into a lobby, or quitting. BT411 saves only on a
|
||||
// launch, which loses a callsign typed by somebody who then
|
||||
// changed their mind, and that is the one moment this feature
|
||||
// exists for.
|
||||
//
|
||||
// Unconditionally, rather than only when something changed: the
|
||||
// file is a few hundred bytes, and writing it every time means a
|
||||
// value that was hand-edited out of range comes back corrected
|
||||
// instead of being quietly re-rejected on every launch forever.
|
||||
//
|
||||
SavePilotSettings(gLastPilotName, gPersistSelection);
|
||||
|
||||
Logical launched = fe.launched;
|
||||
Logical closed = fe.closed;
|
||||
@@ -1769,6 +2086,10 @@ Logical
|
||||
return False;
|
||||
}
|
||||
|
||||
// this screen labels a row with the pilot's own callsign, and in the
|
||||
// -egg and lobby paths it can be the first screen of the session
|
||||
EnsurePilotSettingsLoaded();
|
||||
|
||||
ResultsState rs;
|
||||
memset(&rs, 0, sizeof(rs));
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//########################################################################
|
||||
|
||||
Logical RPL4Lobby_Available() { return False; }
|
||||
Logical RPL4Lobby_Configured() { return False; }
|
||||
Logical RPL4Lobby_InRoom() { return False; }
|
||||
int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||
int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||
@@ -919,6 +920,18 @@ Logical
|
||||
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
|
||||
RPL4Lobby_InRoom()
|
||||
{
|
||||
|
||||
@@ -33,6 +33,15 @@ enum RPL4LobbyOutcome
|
||||
Logical
|
||||
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).
|
||||
Logical
|
||||
RPL4Lobby_InRoom();
|
||||
|
||||
+14
-2
@@ -357,8 +357,20 @@ Logical
|
||||
//
|
||||
if (GetApplicationState() == RunningMission)
|
||||
{
|
||||
secondsRemainingInGame =
|
||||
currentMission->GetGameLength() - (Now() - gameStarted);
|
||||
// same rule as Application::ExecuteForeground - the console's
|
||||
// countdown when there is one, our own reckoning otherwise. There
|
||||
// is no console in mission review, so this takes the fallback.
|
||||
Scalar console_remaining;
|
||||
if (gMissionClockHook != NULL &&
|
||||
(*gMissionClockHook)(&console_remaining))
|
||||
{
|
||||
secondsRemainingInGame = console_remaining;
|
||||
}
|
||||
else
|
||||
{
|
||||
secondsRemainingInGame =
|
||||
currentMission->GetGameLength() - (Now() - gameStarted);
|
||||
}
|
||||
}
|
||||
|
||||
CLEAR_FOREGROUND_PROCESSING();
|
||||
|
||||
@@ -70,6 +70,15 @@
|
||||
<RandomizedBaseAddress>false</RandomizedBaseAddress>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<!-- rpl4build.h is generated, not committed: the patch number is the
|
||||
repository's commit count, so a hardcoded one would be stale the
|
||||
moment it was committed. The script rewrites the header only when
|
||||
the stamp actually changes, so this does not drag RPL4.CPP through
|
||||
a recompile on every build. -->
|
||||
<PreBuildEvent>
|
||||
<Command>powershell -NoProfile -ExecutionPolicy Bypass -File "$(ProjectDir)..\stamp-version.ps1"</Command>
|
||||
<Message>Stamping the build version from git</Message>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
@@ -109,6 +118,7 @@
|
||||
<ClCompile Include=".\RPL4APP.cpp" />
|
||||
<ClCompile Include=".\RPL4CONSOLE.cpp" />
|
||||
<ClCompile Include=".\RPL4FE.cpp" />
|
||||
<ClCompile Include=".\RPL4ENVIRON.cpp" />
|
||||
<ClCompile Include=".\RPL4LOBBY.cpp" />
|
||||
<ClCompile Include=".\RPL4ARND.cpp" />
|
||||
<ClCompile Include=".\RPL4GAUG.cpp" />
|
||||
@@ -147,6 +157,7 @@
|
||||
<ClInclude Include=".\RPL4APP.h" />
|
||||
<ClInclude Include=".\RPL4CONSOLE.h" />
|
||||
<ClInclude Include=".\RPL4FE.h" />
|
||||
<ClInclude Include=".\rpl4environ.h" />
|
||||
<ClInclude Include=".\RPL4LOBBY.h" />
|
||||
<ClInclude Include=".\RPL4ARND.h" />
|
||||
<ClInclude Include=".\RPL4GAUG.h" />
|
||||
|
||||
+37
-1
@@ -144,6 +144,42 @@ 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
|
||||
@@ -163,7 +199,7 @@ padaxis LeftStickX axis JoystickX deadzone 0.15 # drop 'invert' to flip X
|
||||
|
||||
Addresses: `0x00`–`0x47` are the lamp buttons above, `0x50`–`0x6F` the
|
||||
keypads. Axis names: `Throttle`, `LeftPedal`, `RightPedal`, `JoystickX`,
|
||||
`JoystickY`. Key names follow the .NET `Keys` naming (`A`–`Z`, `D0`–`D9`
|
||||
`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`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<title>Red Planet 4.12.3 — Controls</title>
|
||||
<title>Red Planet 4.12.7 — Handbook</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
@@ -754,13 +754,14 @@
|
||||
<div class="wrap">
|
||||
|
||||
<header class="masthead">
|
||||
<p class="eyebrow">Virtual World Entertainment · Tesla pod · RP 4.12.3</p>
|
||||
<h1>Red Planet<br>Controls Map</h1>
|
||||
<p class="eyebrow">Virtual World Entertainment · Tesla pod · RP 4.12.7</p>
|
||||
<h1>Red Planet<br>Handbook</h1>
|
||||
<p class="lede">
|
||||
Every input the pod answers to, on the gamepad and the keyboard. The
|
||||
keyboard is the pod's button board: the letter and number rows are the
|
||||
MFD banks laid out as they are printed on the panel, and flight moved to
|
||||
the number pad so the board stays free.
|
||||
Every input the pod answers to, on the gamepad and the keyboard, and
|
||||
every file it keeps beside the exe. The keyboard is the pod's button
|
||||
board: the letter and number rows are the MFD banks laid out as they are
|
||||
printed on the panel, and flight moved to the number pad so the board
|
||||
stays free.
|
||||
</p>
|
||||
<p class="provenance">Generated from the default <code>bindings.txt</code> · all of it rebindable</p>
|
||||
</header>
|
||||
@@ -1149,6 +1150,119 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Bring your own stick</h2>
|
||||
<p class="sub">
|
||||
A gamepad speaks XInput, and the game reads it without being asked.
|
||||
Everything else — a flight stick, a HOTAS throttle, a twist grip,
|
||||
rudder pedals, a wheel — speaks <b>DirectInput</b>, which names its axes
|
||||
<span class="mono">X Y Z RX RY RZ SL0 SL1</span> and says nothing about
|
||||
what they are <i>for</i>. Somebody has to decide that your twist grip is
|
||||
the rudder and that pushing it right means right. That is what
|
||||
<code>joyconfig.bat</code> is.
|
||||
</p>
|
||||
|
||||
<div class="panel">
|
||||
<dl class="glance">
|
||||
<div>
|
||||
<dt>Run it once</dt>
|
||||
<dd>joyconfig.bat<small>beside the exe; re-run any time</small></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>It asks</dt>
|
||||
<dd>Move that one<small>steer · pitch · pedals · throttle · buttons</small></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>It watches</dt>
|
||||
<dd>Which way<small>the direction you moved sets the sign</small></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Then</dt>
|
||||
<dd>Straight in<small>the game carries on to the setup screen</small></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
<p>
|
||||
<strong>The direction is the whole point.</strong> A stick that reads
|
||||
positive pushed right and one that reads negative are equally common,
|
||||
and nothing printed on the box tells you which you own. So the wizard
|
||||
does not ask you to know — it asks you to <i>move</i>, and reads the
|
||||
answer off the movement. Skip a prompt with <code>SPACE</code> if you
|
||||
have no such control, <code>ESC</code> to abort without writing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="sub">
|
||||
It writes only its own section of <code>bindings.txt</code>, between two
|
||||
marker lines, so anything you have written yourself is kept — including a
|
||||
deadzone you tuned by hand after trying it. Before it starts it prints
|
||||
every axis <i>at rest</i>: a stick whose driver reports an odd range
|
||||
shows up there as <span class="mono">X +1.00</span> on an untouched
|
||||
stick, rather than as a mystery later. Xbox-class pads are excluded from
|
||||
all of this, so nothing ever counts twice.
|
||||
</p>
|
||||
|
||||
<div class="two-col">
|
||||
<pre><i># what it writes</i>
|
||||
<b>joydev</b> 0 Logitech Extreme 3D
|
||||
<b>joyaxis</b> X <b>axis</b> JoystickX invert deadzone 0.08
|
||||
<b>joyaxis</b> Y <b>axis</b> JoystickY deadzone 0.08
|
||||
<b>joyaxis</b> RZ <b>axis</b> Pedals deadzone 0.08
|
||||
<b>joyaxis</b> SL0 <b>axis</b> Throttle invert deadzone 0
|
||||
<b>joybutton</b> 0 <b>button</b> 0x40
|
||||
<b>joyhat</b> 0 up <b>button</b> 0x42</pre>
|
||||
|
||||
<pre><i># grammar</i>
|
||||
<b>joydev</b> <slot> [product-name substring]
|
||||
<b>joyaxis</b> <src> <b>axis</b> <axis> [invert]
|
||||
[deadzone <d>] [rate <n>]
|
||||
<b>joybutton</b> <n> <b>button</b> <addr> [toggle]
|
||||
<b>joyhat</b> <n> <up|down|left|right>
|
||||
<b>button</b> <addr>
|
||||
|
||||
<i># a named slot binds that product; a bare
|
||||
# one binds the Nth stick Windows lists</i></pre>
|
||||
</div>
|
||||
|
||||
<div class="tbl-scroll" style="margin-top:22px">
|
||||
<table>
|
||||
<caption>Two rules the pod's shape asks for</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="mono">Pedals</th>
|
||||
<td>
|
||||
A <b>signed</b> axis that works the pedal <b>pair</b> — positive
|
||||
for the right pedal, negative for the left. The pod has one
|
||||
under each foot; a twist grip or rudder bar is a single control
|
||||
that presses one or the other and never both, which is exactly
|
||||
what this says. It is a channel name like any other, so a gamepad
|
||||
stick can drive the turn with it too.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mono">Throttle</th>
|
||||
<td>
|
||||
A <span class="mono">joyaxis</span> on the throttle with no
|
||||
<span class="mono">rate</span> is treated as a <b>real lever</b>
|
||||
and owns the channel: its full travel <i>is</i> the throttle
|
||||
range. Spring-centred sticks have to nudge a position instead,
|
||||
which is what <span class="mono">rate</span> does — a lever that
|
||||
stays where you put it needs none of that.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="sub" style="margin-bottom:0">
|
||||
A twist grip is usually <span class="mono">RZ</span>; a HOTAS throttle is
|
||||
usually <span class="mono">Z</span> or <span class="mono">SL0</span>. Set
|
||||
<code>RP412JOYLOG=1</code> to log devices as they attach and drop away.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Rebinding</h2>
|
||||
<p class="sub">
|
||||
@@ -1166,6 +1280,8 @@
|
||||
<b>pad</b> <button> <b>button</b> <addr> [toggle]
|
||||
<b>padaxis</b> <src> <b>axis</b> <axis> [invert]
|
||||
[deadzone <d>] [rate <n>]
|
||||
<b>joydev joyaxis joybutton joyhat</b>
|
||||
<i>see above</i>
|
||||
|
||||
<i># deflect springs back, rate sticks where
|
||||
# you leave it (that is the throttle)</i></pre>
|
||||
@@ -1185,21 +1301,107 @@
|
||||
<table>
|
||||
<caption>Reference — axes, address space, key names</caption>
|
||||
<tbody>
|
||||
<tr><th>Axes</th><td class="mono">Throttle · LeftPedal · RightPedal · JoystickX · JoystickY</td></tr>
|
||||
<tr><th>Axes</th><td class="mono">Throttle · LeftPedal · RightPedal · JoystickX · JoystickY · Pedals</td></tr>
|
||||
<tr><th>Buttons</th><td class="addr">0x00 – 0x47</td></tr>
|
||||
<tr><th>Keypads</th><td class="addr">0x50 – 0x6F <span style="color:var(--ink-quiet);font-family:var(--sans)">— unbound by default; the game never reads them</span></td></tr>
|
||||
<tr><th>Key names</th><td class="mono">A–Z · D0–D9 · F1–F12 · NumPad0–9 · Up Down Left Right · Space · Return · Shift · Ctrl · Alt · OemMinus · Oemplus · Oemcomma · OemPeriod</td></tr>
|
||||
<tr><th>Pad</th><td class="mono">A B X Y · DPadUp/Down/Left/Right · Start · Back · LeftShoulder · RightShoulder · LeftThumb · RightThumb</td></tr>
|
||||
<tr><th>Pad axes</th><td class="mono">LeftStickX/Y · RightStickX/Y · LeftTrigger · RightTrigger</td></tr>
|
||||
<tr><th>Joystick axes</th><td class="mono">X · Y · Z · RX · RY · RZ · SL0 · SL1 <span style="color:var(--ink-quiet);font-family:var(--sans)">— DirectInput's own names</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Files beside the exe</h2>
|
||||
<p class="sub">
|
||||
Four files in the game folder are yours. <b>None of them ship.</b> The
|
||||
game writes each one the first time it needs it and then never touches
|
||||
it again, so a new build dropped over this folder keeps everything you
|
||||
have set — and deleting any of them simply starts that part over with
|
||||
the current defaults.
|
||||
</p>
|
||||
|
||||
<div class="tbl-scroll">
|
||||
<table>
|
||||
<caption>Yours to edit</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="mono">environ.ini</th>
|
||||
<td>
|
||||
Every engine option, commented in place — displays, renderer,
|
||||
Steam, the podium, the lot. The one to read first. Written on
|
||||
first run; without it the MFDs do not come up at all, so the
|
||||
game will always put one back.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mono">bindings.txt</th>
|
||||
<td>
|
||||
Every key, pad button, axis and joystick row. Written with the
|
||||
full documented default layout the first time the game runs.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mono">pilot.cfg</th>
|
||||
<td>
|
||||
Your callsign and loadout, saved on the way out of the setup
|
||||
screen so they are there next time.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mono">mfd_layout.cfg</th>
|
||||
<td>
|
||||
Where you dragged the windows, and the <code>,noframe</code>
|
||||
flag. Only written when <code>RP412MFDLAYOUT=save</code>.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
<p>
|
||||
<strong>Two that catch people out.</strong>
|
||||
<code>environ.ini</code> is applied <i>over</i> the environment, so a
|
||||
variable you set in a shell loses to an uncommented line in the file —
|
||||
comment the line out rather than fighting it. And nothing here is ever
|
||||
overwritten once it exists, which is exactly what lets you keep a
|
||||
folder across builds — but it means a file carried through several
|
||||
updates stops being offered new options. The game names any it has not
|
||||
heard of in <code>rpl4.log</code>; <b>delete the file</b> to get the
|
||||
fully documented current one back.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>The rest of the folder</h3>
|
||||
<p class="sub">
|
||||
Shipped data the engine reads. Nothing here is meant to be edited, but
|
||||
it is worth knowing what is what.
|
||||
</p>
|
||||
|
||||
<div class="tbl-scroll">
|
||||
<table>
|
||||
<caption>Engine data</caption>
|
||||
<tbody>
|
||||
<tr><th class="mono">RPDPL.INI</th><td>Renderer and scene configuration. Named by <code>L4DPLCFG=</code>, so it can be swapped.</td></tr>
|
||||
<tr><th class="mono">GAUGE\L4GAUGE.INI</th><td>Gauge canvas pages. <code>L4GAUGE=</code> picks one by name — a name that is not in here switches the MFDs off entirely.</td></tr>
|
||||
<tr><th class="mono">GAUGE\L4GAUGE.CFG</th><td>The gauge layout data that page points at.</td></tr>
|
||||
<tr><th class="mono">AUDIO\AUDIO.INI</th><td>Sound banks. <span class="mono">AUDIOMR.INI</span> is the mission-review twin.</td></tr>
|
||||
<tr><th class="mono">JOYSTICK.INI</th><td>The 1995 single-stick calibration, for the legacy <code>L4CONTROLS=DIJOYSTICK</code> path only. Modern sticks are configured in <code>bindings.txt</code> — see above.</td></tr>
|
||||
<tr><th class="mono">*.CFG <span style="color:var(--ink-quiet);font-family:var(--sans)">in AUDIO\</span></th><td>Pod audio-hardware mixer tables. Dead weight on a desktop; kept because the arcade path still reads them.</td></tr>
|
||||
<tr><th class="mono">RPL4.RES</th><td>Resource blob, not text.</td></tr>
|
||||
<tr><th class="mono">steam_appid.txt</th><td>Steam runs under Spacewar (480) until Red Planet has its own AppID.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Red Planet 4.12.3</span>
|
||||
<span>Red Planet 4.12.7</span>
|
||||
<span>gitea.mysticmachines.com/VWE/RP412</span>
|
||||
<span>docs/CONTROLS.md · CONTROLS.txt ships with the game</span>
|
||||
<span>docs/rp412-handbook.html · HANDBOOK.html ships with the game</span>
|
||||
<span>RGB keyboards mirror the pod lamps · RP412KEYLIGHT=0 to disable</span>
|
||||
</footer>
|
||||
</div>
|
||||
+137
-240
@@ -8,13 +8,15 @@
|
||||
# TEST.EGG) - but not the arcade launch scripts or the old 4.10 exe
|
||||
# - libsndfile-1.dll beside the exe; OpenAL32.dll copied from the system
|
||||
# when installed, with oalinst.exe included as the fallback installer
|
||||
# - a desktop environ.ini (PAD;KEYBOARD controls, on-screen plasma)
|
||||
# - start-windowed.bat and a README
|
||||
# (environ.ini is NOT shipped - the exe writes it on first run)
|
||||
# - start/joyconfig scripts, HANDBOOK.html, CONTROLS.txt and a README
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File pack-dist.ps1 [-Zip]
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File pack-dist.ps1 [-Zip] [-Fresh]
|
||||
#
|
||||
param(
|
||||
[switch]$Zip
|
||||
[switch]$Zip,
|
||||
# Wipe the player's files too, for testing what a first run does.
|
||||
[switch]$Fresh
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -27,6 +29,23 @@ if (-not (Test-Path $exe)) {
|
||||
throw "Release\rpl4opt.exe not found - build first (see BUILD.md 2)."
|
||||
}
|
||||
|
||||
# --- version ---------------------------------------------------------------
|
||||
# Read the stamp the exe was BUILT with rather than asking git again: a
|
||||
# commit between the build and the pack would otherwise have the package
|
||||
# claiming a version the binary inside it does not report.
|
||||
$buildHeader = Join-Path $root 'RP_L4\rpl4build.h'
|
||||
if (-not (Test-Path $buildHeader)) {
|
||||
throw "RP_L4\rpl4build.h not found - build first, or run stamp-version.ps1."
|
||||
}
|
||||
$stamp = Get-Content $buildHeader -Raw
|
||||
$version = ([regex]::Match($stamp, '#define\s+RP412_VERSION\s+"([^"]+)"')).Groups[1].Value
|
||||
$versionLong = ([regex]::Match($stamp, '#define\s+RP412_VERSION_LONG\s+"([^"]+)"')).Groups[1].Value
|
||||
if (-not $version) { throw "could not read RP412_VERSION from $buildHeader" }
|
||||
if ($stamp -match '#define\s+RP412_BUILD_DIRTY\s+1') {
|
||||
Write-Warning "packing a build made from a modified tree ($versionLong)"
|
||||
}
|
||||
Write-Host "Version $versionLong"
|
||||
|
||||
# Refuse to touch a dist the game is currently running from.
|
||||
$running = Get-Process rpl4opt -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Path -like "$dist\*" }
|
||||
@@ -35,6 +54,27 @@ if ($running) {
|
||||
}
|
||||
|
||||
Write-Host "Packing into $dist"
|
||||
|
||||
# --- the player's own files ------------------------------------------------
|
||||
# None of these ship: the game writes each one the first time it needs it and
|
||||
# then leaves it alone, so a new build dropped over a folder keeps every
|
||||
# setting. This script rebuilds dist\ from scratch, which would throw exactly
|
||||
# those away - the one place the promise did not hold, and it is the folder we
|
||||
# do most of our own testing in. Carry them across. -Fresh to start over.
|
||||
$keepFiles = @('environ.ini', 'bindings.txt', 'pilot.cfg', 'mfd_layout.cfg')
|
||||
$kept = @{}
|
||||
if (-not $Fresh) {
|
||||
foreach ($name in $keepFiles) {
|
||||
$path = Join-Path $dist $name
|
||||
if (Test-Path $path) { $kept[$name] = [System.IO.File]::ReadAllBytes($path) }
|
||||
}
|
||||
if ($kept.Count -gt 0) {
|
||||
Write-Host " keeping $($kept.Keys -join ', ')"
|
||||
}
|
||||
} elseif (Test-Path $dist) {
|
||||
Write-Host " -Fresh: the player's files go too"
|
||||
}
|
||||
|
||||
if (Test-Path $dist) { Remove-Item -Recurse -Force $dist }
|
||||
New-Item -ItemType Directory -Force "$dist\SPOOLS" | Out-Null
|
||||
|
||||
@@ -64,9 +104,8 @@ foreach ($file in 'RPDPL.INI', 'JOYSTICK.INI', 'RPL4.RES', 'TEST.EGG',
|
||||
Copy-Item (Join-Path $assets $file) $dist
|
||||
}
|
||||
|
||||
# The controls map travels with the game (players get the diagrams
|
||||
# without needing the repo). Flattened to ASCII so it reads correctly
|
||||
# in Notepad - the markdown source keeps its typography.
|
||||
# The controls half as plain text, for Notepad. Flattened to ASCII so it
|
||||
# reads correctly there - 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, "'"),
|
||||
@@ -76,13 +115,18 @@ foreach ($pair in @(
|
||||
}
|
||||
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
|
||||
# The handbook as a page: the controls map with the diagrams, plus the
|
||||
# joystick setup and what every file in the folder is for. 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
|
||||
$handbookPage = Get-Content (Join-Path $root 'docs\rp412-handbook.html') -Raw -Encoding UTF8
|
||||
# Stamp the shipped copy with the build's own version. The source keeps a
|
||||
# readable one for publishing; only "4.12.<n>" is touched, which on this
|
||||
# page is always the version and never anything else.
|
||||
$handbookPage = [regex]::Replace($handbookPage, '4\.12\.\d+', $version)
|
||||
$page = @"
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -91,12 +135,12 @@ $page = @"
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
$controlsPage
|
||||
$handbookPage
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
[System.IO.File]::WriteAllText(
|
||||
"$dist\CONTROLS.html", $page, (New-Object System.Text.UTF8Encoding $false))
|
||||
"$dist\HANDBOOK.html", $page, (New-Object System.Text.UTF8Encoding $false))
|
||||
|
||||
# --- OpenAL runtime --------------------------------------------------------
|
||||
# The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed
|
||||
@@ -113,225 +157,12 @@ if (Test-Path $openal) {
|
||||
}
|
||||
|
||||
# --- desktop configuration -------------------------------------------------
|
||||
Set-Content -Path "$dist\environ.ini" -Encoding ascii -Value @"
|
||||
# ============================================================================
|
||||
# environ.ini - Red Planet 4.12 configuration
|
||||
# ============================================================================
|
||||
# One KEY=VALUE per line, read at game start. Lines starting with # or ;
|
||||
# are comments; anything without an = is ignored. Delete a line (or
|
||||
# comment it out) to fall back to the built-in default.
|
||||
#
|
||||
# Input bindings live in bindings.txt beside the exe (written with the
|
||||
# full documented layout on first run; delete it to restore defaults).
|
||||
|
||||
# ---- Core (the shipped configuration) --------------------------------------
|
||||
|
||||
# Control stack: tokens separated by ; or , processed left to right.
|
||||
# PAD the virtual RIO (XInput controller + keyboard,
|
||||
# rebindable via bindings.txt)
|
||||
# RIO real serial cockpit hardware on COM1
|
||||
# RIO:COMn same, on another port (RIO:COM3, ...)
|
||||
# KEYBOARD the engine keyboard handler
|
||||
# MOUSE, JOYSTICK, FLIGHTSTICKPRO, THRUSTMASTER, DIJOYSTICK
|
||||
# legacy pointer/joystick drivers (untested here)
|
||||
# Unset falls back to KEYBOARD alone.
|
||||
L4CONTROLS=PAD;KEYBOARD
|
||||
|
||||
# Renderer bring-up argument. Only its presence is checked (the DPL
|
||||
# resolution parsing it once fed is gone) and the game refuses to start
|
||||
# without it - any non-empty value works. Leave as shipped.
|
||||
DPLARG=1
|
||||
|
||||
# DPL (renderer/scene) configuration file, searched beside the exe.
|
||||
# Any notation file name; RPDPL.INI is the one that ships.
|
||||
L4DPLCFG=RPDPL.INI
|
||||
|
||||
# Gauge (MFD/instrument) canvas. Must name a page of GAUGE\L4GAUGE.INI:
|
||||
# 640x480x8 | 640x480x16 | 800x600x16
|
||||
# Unset disables the gauge renderer (and with it all MFDs).
|
||||
L4GAUGE=640x480x16
|
||||
|
||||
# Plasma display.
|
||||
# SCREEN 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
|
||||
|
||||
# 0 = classic separate gauge windows; 1 = the single-window glass
|
||||
# cockpit (all seven displays composed on a locked 1920x1080 canvas
|
||||
# around the viewscreen); 2 = exploded diagnostic view (each display
|
||||
# in its own native-resolution desktop window - MFDs 640x480, map
|
||||
# 480x640 - decoded exactly as the pod's VDB split them, no downscale).
|
||||
L4MFDSPLIT=1
|
||||
|
||||
# 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
|
||||
|
||||
# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client
|
||||
# running and steam_appid.txt beside the exe; without them the game
|
||||
# logs the reason and falls back to plain TCP. 0 = TCP only.
|
||||
RP412STEAM=1
|
||||
|
||||
# ---- Optional ---------------------------------------------------------------
|
||||
|
||||
# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to
|
||||
# lamp buttons glow with the panel, flash modes and all.
|
||||
# Unset or nonzero = on (the default); 0 = off.
|
||||
#RP412KEYLIGHT=0
|
||||
|
||||
# Invert the stick on top of whatever bindings.txt produces:
|
||||
# X = invert X only, Y = invert Y only, XY = both (case-insensitive).
|
||||
#L4PADFLIP=XY
|
||||
|
||||
# Anti-aliasing sample count, passed straight to Direct3D 9:
|
||||
# 0 = off, else 2..16 as the GPU supports (1 selects the driver's
|
||||
# "nonmaskable" mode; unsupported counts fail device creation).
|
||||
#MULTISAMPLE=0
|
||||
|
||||
# Particle budget, integer. Default 8192.
|
||||
#MAXPARTICLES=8192
|
||||
|
||||
# On-screen plasma glass (L4PLASMA=SCREEN only). SCALE = integer pixel
|
||||
# size 1..16, default 4 (out-of-range values are ignored). POS = window
|
||||
# top-left as X,Y screen coordinates; unset = auto, parked below the
|
||||
# main window.
|
||||
#L4PLASMASCALE=4
|
||||
#L4PLASMAPOS=0,0
|
||||
|
||||
# Fixed random seed (repeatable runs): any unsigned integer.
|
||||
# Unset seeds from the clock.
|
||||
#RANDOM=12345
|
||||
|
||||
# ---- LAN play without Steam -------------------------------------------------
|
||||
# Host a race over plain TCP: list the member pods' console channels
|
||||
# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501).
|
||||
# RP412HOSTPODS comma-separated IP[:port] list, one entry per member
|
||||
# pod; port defaults to 1501 per entry
|
||||
# RP412HOSTPORT this machine's console port, integer > 0
|
||||
# (default 1501)
|
||||
# RP412HOSTADDR this machine's LAN IP as members can reach it
|
||||
# (default 127.0.0.1)
|
||||
#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501
|
||||
#RP412HOSTPORT=1501
|
||||
#RP412HOSTADDR=192.168.1.10
|
||||
|
||||
# ---- Developer / testing ----------------------------------------------------
|
||||
|
||||
# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision,
|
||||
# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump.
|
||||
# 0 or unset = off. (Alt+Q, the mission abort, is always live.)
|
||||
#RP412DEVKEYS=1
|
||||
|
||||
# Console race-length override, integer seconds (short test races).
|
||||
# Values <= 0 are ignored.
|
||||
#L4CONSOLELEN=30
|
||||
|
||||
# Nonzero = Steam transport loopback self-test at boot (logs PASS/FAIL).
|
||||
#RP412STEAMSELFTEST=1
|
||||
|
||||
# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ----------
|
||||
# PRIMGAUGE / SECGAUGE / MFDGAUGE / MFDGAUGE2 pin a display to a monitor
|
||||
# by adapter index (0, 1, 2...). SPANDISABLE: 0 = let the MFDs span one
|
||||
# wide surface, nonzero = separate windows (setting MFDGAUGE2 alone also
|
||||
# forces spanning off). L4EYES = "x y z xrot yrot zrot [type]" floats
|
||||
# for a detached camera; a type starting with r offsets it relative to
|
||||
# the pod. L4INTERCOM enables the crew intercom - only its presence
|
||||
# matters (traditionally COM2). NOMODES skips the mode/lamp programming;
|
||||
# presence alone triggers it, even NOMODES=0. LOGSIZE > 0 sizes the
|
||||
# trace log in dev builds compiled with tracing.
|
||||
#PRIMGAUGE=1
|
||||
#SECGAUGE=2
|
||||
#MFDGAUGE=3
|
||||
#MFDGAUGE2=4
|
||||
#SPANDISABLE=1
|
||||
#L4EYES=1
|
||||
#L4INTERCOM=COM2
|
||||
#NOMODES=1
|
||||
#LOGSIZE=1000000
|
||||
"@
|
||||
# --- desktop configuration -------------------------------------------------
|
||||
# environ.ini is NOT shipped. The exe carries the template and writes it on
|
||||
# first run (RPL4ENVIRON.cpp), the same way it writes bindings.txt - so a
|
||||
# tester can drop a new build over an old folder and keep every setting they
|
||||
# have changed. Laying one down here would overwrite their file on every
|
||||
# unzip, which is the whole problem.
|
||||
|
||||
Set-Content -Path "$dist\start-windowed.bat" -Encoding ascii -Value @"
|
||||
@echo off
|
||||
@@ -356,14 +187,36 @@ 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 @"
|
||||
Red Planet 4.12.5
|
||||
Red Planet $version
|
||||
=================
|
||||
|
||||
Run start-fullscreen.bat for borderless over the whole monitor, or
|
||||
start-windowed.bat to keep a title bar. No cockpit hardware needed.
|
||||
If there is no sound, run oalinst.exe once.
|
||||
|
||||
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
|
||||
rebindable: edit bindings.txt beside the exe (written with the full
|
||||
documented default layout on first run; delete it to restore).
|
||||
@@ -402,13 +255,42 @@ at its left, and the lower MFDs flanking the portrait map. The red
|
||||
buttons around each MFD and the amber buttons beside the map are the
|
||||
pod's real button banks: click them with the mouse, and they light up
|
||||
as the game commands their lamps.
|
||||
environ.ini is self-documenting: every option ships in the file with
|
||||
a comment (Steam networking, keyboard lighting, stick inversion, LAN
|
||||
hosting, developer keys, display scaling and radar placement, and more).
|
||||
environ.ini is self-documenting: the game writes it on first run with
|
||||
every option in it and a comment on each (Steam networking, keyboard
|
||||
lighting, stick inversion, LAN 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.
|
||||
HANDBOOK.html is the full manual - open it in a browser for the pad,
|
||||
keyboard and pod-panel diagrams, the joystick setup, and what every file
|
||||
in this folder is for. CONTROLS.txt is the controls half as plain text.
|
||||
|
||||
Four files here are yours. None of them ship - the game writes each one
|
||||
the first time it needs it and then leaves it alone, so a new build
|
||||
unzipped over this folder keeps everything you have set. Delete any of
|
||||
them to start that part over:
|
||||
|
||||
environ.ini every engine option, commented in place
|
||||
bindings.txt every key, pad button, axis and joystick row
|
||||
pilot.cfg your callsign and loadout
|
||||
mfd_layout.cfg where you dragged the windows (RP412MFDLAYOUT)
|
||||
|
||||
Two that catch people out: environ.ini is applied OVER the environment,
|
||||
so a variable set in a shell loses to an uncommented line in the file;
|
||||
and none of these four is ever overwritten once it exists, which is what
|
||||
lets you keep a folder across builds. The trade is that a file carried
|
||||
through several updates stops being offered new options - rpl4.log names
|
||||
any it has not heard of, and deleting the file brings back the fully
|
||||
documented current one.
|
||||
|
||||
THIS IS A TEST BUILD AND IT GOES STALE. Two weeks after it was built it
|
||||
will tell you so and refuse to start, rather than let you spend an
|
||||
afternoon on something that was fixed the week before. When that happens,
|
||||
grab the current build:
|
||||
|
||||
https://gitea.mysticmachines.com/VWE/RP412/releases
|
||||
|
||||
Unzip it over this folder - your environ.ini, bindings.txt, pilot.cfg and
|
||||
mfd_layout.cfg all survive.
|
||||
|
||||
Known prototype notes: pods race untextured (the player1-8 skins come
|
||||
from the presets system, not shipped data), and text drawn on the plasma
|
||||
@@ -422,9 +304,13 @@ $size = (Get-ChildItem $dist -Recurse | Measure-Object Length -Sum).Sum
|
||||
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
|
||||
|
||||
if ($Zip) {
|
||||
$zipPath = Join-Path $root 'RedPlanet-4.12.5.zip'
|
||||
$zipPath = Join-Path $root "RedPlanet-$version.zip"
|
||||
Write-Host "zipping to $zipPath..."
|
||||
|
||||
# Taken BEFORE the player's files go back, so a release never carries
|
||||
# somebody's callsign, key bindings or window positions to everyone who
|
||||
# downloads it. A fresh unzip must look like a first run.
|
||||
#
|
||||
# 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.
|
||||
@@ -435,3 +321,14 @@ if ($Zip) {
|
||||
Compress-Archive -Path "$stage\RP412" -DestinationPath $zipPath -Force
|
||||
Remove-Item -Recurse -Force $stage
|
||||
}
|
||||
|
||||
# --- the player's own files, back where they were --------------------------
|
||||
# Last of all: after the rebuilt tree, so nothing the pack writes can land on
|
||||
# top of them, and after the zip, so the release stays clean. Zipping should
|
||||
# not cost you your own settings.
|
||||
if ($kept.Count -gt 0) {
|
||||
foreach ($name in $kept.Keys) {
|
||||
[System.IO.File]::WriteAllBytes((Join-Path $dist $name), $kept[$name])
|
||||
}
|
||||
Write-Host " restored $($kept.Keys -join ', ')"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# ============================================================================
|
||||
# stamp-version.ps1 - write RP_L4\rpl4build.h from the repository's own state
|
||||
# ============================================================================
|
||||
#
|
||||
# The patch number IS the commit count, so a build names the commit it came
|
||||
# from and there is never a question about which changes are in a binary
|
||||
# somebody is holding. Run as RP_L4's pre-build step; also readable by
|
||||
# pack-dist.ps1, so the package and the exe cannot disagree.
|
||||
#
|
||||
# A hardcoded version could not do this: the commit that records "4.12.96"
|
||||
# is itself commit 96, so the file is stale the moment it is committed.
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File stamp-version.ps1
|
||||
#
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# The product line. Bump this by hand when the line moves; the patch
|
||||
# number after it looks after itself.
|
||||
$line = '4.12'
|
||||
|
||||
# How many days a build stays good for. Test builds go stale so nobody is
|
||||
# still racing a fortnight-old binary and reporting things that were fixed
|
||||
# a week ago - the expired build says so and stops, rather than quietly
|
||||
# wasting everyone's afternoon.
|
||||
#
|
||||
# SET THIS TO 0 FOR A REAL RELEASE. A shipped build that expires is a
|
||||
# catastrophe, and this is the one line that decides it.
|
||||
$expireDays = 14
|
||||
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$header = Join-Path $root 'RP_L4\rpl4build.h'
|
||||
|
||||
$count = 0
|
||||
$commit = 'nogit'
|
||||
$dirty = 0
|
||||
|
||||
#
|
||||
# Every git call goes through cmd so that stderr never reaches PowerShell's
|
||||
# error stream. Windows PowerShell turns a native command's stderr into
|
||||
# ErrorRecords, and with $ErrorActionPreference = 'Stop' git's routine
|
||||
# "LF will be replaced by CRLF" warning is enough to throw - which silently
|
||||
# skipped the dirty check and stamped every modified build as clean.
|
||||
#
|
||||
try {
|
||||
Push-Location $root
|
||||
$c = cmd /c "git rev-list --count HEAD 2>NUL"
|
||||
if ($LASTEXITCODE -eq 0 -and $c) {
|
||||
$count = [int]$c
|
||||
$commit = (cmd /c "git rev-parse --short HEAD 2>NUL").Trim()
|
||||
#
|
||||
# Tracked modifications only. An untracked scratch file in the tree
|
||||
# is not in the binary, and marking every build dirty for one would
|
||||
# make the marker mean nothing.
|
||||
#
|
||||
cmd /c "git diff --quiet HEAD -- 2>NUL"
|
||||
if ($LASTEXITCODE -ne 0) { $dirty = 1 }
|
||||
}
|
||||
} catch {
|
||||
# no git, or not a repository: fall through to the placeholder below
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
if ($count -eq 0) {
|
||||
# Built outside the repository (a source drop, say). Say so plainly
|
||||
# rather than inventing a number that would sort against real ones.
|
||||
$version = "$line.x"
|
||||
$long = "$line.x (no repository)"
|
||||
} else {
|
||||
$version = "$line.$count"
|
||||
$long = "$version ($commit$(if ($dirty) { '+' } else { '' }))"
|
||||
}
|
||||
|
||||
#
|
||||
# Expiry, from the day it was BUILT rather than the day the code was
|
||||
# written: rebuilding an old commit to chase something should give a
|
||||
# usable binary, not one that was born stale.
|
||||
#
|
||||
# The date is what makes this header change from one day to the next, so
|
||||
# the first build of each day recompiles RPL4.CPP and the rest of that
|
||||
# day's builds do not. One file, a couple of seconds.
|
||||
#
|
||||
# Only 0 means "never". A NEGATIVE count backdates the expiry, which is how
|
||||
# the refusal gets tested without touching the machine's clock.
|
||||
if ($expireDays -ne 0) {
|
||||
$expiry = (Get-Date).Date.AddDays($expireDays)
|
||||
$expires = 1
|
||||
$expiryY = $expiry.Year
|
||||
$expiryM = $expiry.Month
|
||||
$expiryD = $expiry.Day
|
||||
$expiryTxt = $expiry.ToString('d MMMM yyyy', [Globalization.CultureInfo]::InvariantCulture)
|
||||
} else {
|
||||
$expires = 0
|
||||
$expiryY = 0
|
||||
$expiryM = 0
|
||||
$expiryD = 0
|
||||
$expiryTxt = 'never'
|
||||
}
|
||||
|
||||
$content = @"
|
||||
//===========================================================================//
|
||||
// File: rpl4build.h GENERATED - do not edit, do not commit //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Written by stamp-version.ps1 as RP_L4's pre-build step. The patch number //
|
||||
// is the repository's commit count and the hash beside it names the exact //
|
||||
// commit, so a running build always says where it came from. A trailing '+' //
|
||||
// means the tree had uncommitted changes to tracked files when it was built. //
|
||||
// //
|
||||
// EXPIRES is the test-build shelf life - see \$expireDays in the script. A //
|
||||
// release must be built with it at 0. //
|
||||
//===========================================================================//
|
||||
|
||||
#pragma once
|
||||
|
||||
#define RP412_BUILD_COUNT $count
|
||||
#define RP412_BUILD_COMMIT "$commit"
|
||||
#define RP412_BUILD_DIRTY $dirty
|
||||
#define RP412_VERSION "$version"
|
||||
#define RP412_VERSION_LONG "$long"
|
||||
|
||||
#define RP412_EXPIRES $expires
|
||||
#define RP412_EXPIRY_YEAR $expiryY
|
||||
#define RP412_EXPIRY_MONTH $expiryM
|
||||
#define RP412_EXPIRY_DAY $expiryD
|
||||
#define RP412_EXPIRY_TEXT "$expiryTxt"
|
||||
"@
|
||||
|
||||
#
|
||||
# Only rewrite when something actually changed: an unconditional write
|
||||
# would touch the header on every build and drag RPL4.CPP through a
|
||||
# recompile each time.
|
||||
#
|
||||
$existing = if (Test-Path $header) { [IO.File]::ReadAllText($header) } else { '' }
|
||||
$note = if ($expires) { "expires $expiryTxt" } else { "no expiry" }
|
||||
if ($existing -ne $content) {
|
||||
[IO.File]::WriteAllText($header, $content, (New-Object System.Text.ASCIIEncoding))
|
||||
Write-Host "stamp-version: $long, $note"
|
||||
} else {
|
||||
Write-Host "stamp-version: $long, $note (unchanged)"
|
||||
}
|
||||
|
||||
#
|
||||
# Explicitly: this runs as a pre-build step, and the last thing above it is
|
||||
# "git diff --quiet", which exits 1 to mean "there are changes". Letting that
|
||||
# escape would fail the build on every modified tree - the exact case a
|
||||
# developer builds in.
|
||||
#
|
||||
exit 0
|
||||
Reference in New Issue
Block a user