Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0c412102 | ||
|
|
bdc5df087e | ||
|
|
50399e016e | ||
|
|
2d70ae7075 | ||
|
|
bc309f7919 | ||
|
|
bc56ab5d48 | ||
|
|
ae939bcea7 | ||
|
|
47817ca6f5 | ||
|
|
e2bcb29a53 | ||
|
|
13a2109918 | ||
|
|
056ee0bdad | ||
|
|
3de30a14d7 | ||
|
|
06d4c91dad | ||
|
|
d00e03ae20 | ||
|
|
48a595b244 | ||
|
|
ef81b75058 | ||
|
|
6f63770f22 | ||
|
|
ae23c040ce | ||
|
|
46418ebb5c | ||
|
|
1596feb636 | ||
|
|
127b8077f5 | ||
|
|
f5ad3036cc | ||
|
|
94e1cf2cf0 | ||
|
|
b0b40559d5 | ||
|
|
d680cce5a2 | ||
|
|
331bc10365 | ||
|
|
d147c093b2 | ||
|
|
da7c35cac1 | ||
|
|
3c49bc4fd1 | ||
|
|
2bd824e16e | ||
|
|
5cd9783d38 | ||
|
|
9c886efd8d | ||
|
|
8d372ec067 | ||
|
|
9107fb4cd9 | ||
|
|
178714198a | ||
|
|
ec815b6216 | ||
|
|
e029113ade | ||
|
|
9a674e9241 | ||
|
|
5c7e218c98 | ||
|
|
46aef0691e | ||
|
|
adcb81e7fc | ||
|
|
9e8a94c436 | ||
|
|
449ed5d297 | ||
|
|
55e751648f | ||
|
|
7a3b117f61 | ||
|
|
d08404bf50 | ||
|
|
0371544b88 | ||
|
|
2adbeba6c4 | ||
|
|
04f72d4e3f | ||
|
|
1cd8911df0 | ||
|
|
af82cac5d2 | ||
|
|
aa2ca0376a | ||
|
|
1a5a4a220c | ||
|
|
487eaa3a36 | ||
|
|
a5c1e0291c | ||
|
|
ef4a5e501d | ||
|
|
0f4592c39a | ||
|
|
34abf40e7f | ||
|
|
67d57452ba | ||
|
|
c479cd48e9 | ||
|
|
af52476603 | ||
|
|
c2e2df1dce | ||
|
|
9643e02198 | ||
|
|
2a679ab571 | ||
|
|
5e47987508 | ||
|
|
74aa5ae98d | ||
|
|
1dd40be0a3 | ||
|
|
b7b2c3b148 | ||
|
|
827c5b295b | ||
|
|
4410febd4c | ||
|
|
3482de5147 | ||
|
|
de9a163f37 | ||
|
|
aa294071f9 | ||
|
|
769407ca24 | ||
|
|
28790de901 | ||
|
|
d35af59136 | ||
|
|
6e829f815e | ||
|
|
0e39075a20 | ||
|
|
9f0a77cc16 | ||
|
|
86bf6a934b | ||
|
|
4979193528 | ||
|
|
dc74fb3867 | ||
|
|
b1b82d5da1 | ||
|
|
53c4eac3fd | ||
|
|
911345703f | ||
|
|
1b603045d2 | ||
|
|
5ad55aa1eb | ||
|
|
8dcf738593 | ||
|
|
56b2af5208 | ||
|
|
b12eaa8bb2 | ||
|
|
4dc4f86702 | ||
|
|
f32494f0b7 | ||
|
|
5135476088 | ||
|
|
99030e4aac | ||
|
|
6c3127a94d | ||
|
|
6df309e9c4 | ||
|
|
17bbcb2049 | ||
|
|
b3ed7bc141 | ||
|
|
44f5a2c6fd | ||
|
|
3ed6498183 | ||
|
|
da7f675cca | ||
|
|
ff0e98a7a5 | ||
|
|
cf90698159 | ||
|
|
3ba79476fc | ||
|
|
f7c7000694 | ||
|
|
8e2e00d8d3 | ||
|
|
cf59ac9f4b | ||
|
|
05f993b9aa | ||
|
|
f83f56e14d | ||
|
|
f44cdbacca | ||
|
|
303758cb52 | ||
|
|
12f9ebefab | ||
|
|
a417175da8 | ||
|
|
943a15cef4 | ||
|
|
cdccb16251 | ||
|
|
a5faa6cf9b | ||
|
|
25e25260b1 | ||
|
|
4e8392fcfb | ||
|
|
523f713a30 | ||
|
|
e133d4c993 | ||
|
|
d361a0b8be | ||
|
|
ce1b0ab9c3 | ||
|
|
6ce729bab5 | ||
|
|
f4fef29428 | ||
|
|
eb17220dd5 | ||
|
|
8ba2d4fc86 | ||
|
|
a0a0ad51d1 | ||
|
|
82e733c1a6 | ||
|
|
68f5780efa | ||
|
|
4f34684b16 | ||
|
|
72bb3b394f | ||
|
|
a1d2de591c | ||
|
|
c1729e40c7 | ||
|
|
f20547cb25 | ||
|
|
52da65bc0d | ||
|
|
91420b5cb2 |
+30
@@ -45,6 +45,15 @@ rpl4.log
|
||||
# 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
|
||||
@@ -75,3 +84,24 @@ assets/**/last.spl
|
||||
|
||||
# packaged releases (attached to Gitea releases, not tracked)
|
||||
RedPlanet-*.zip
|
||||
|
||||
# Crash dumps sent in by testers. Read them with cdb against the matching
|
||||
# Release\rpl4opt.pdb - the PE timestamp in the dump says which build, and
|
||||
# the symbols only mean anything if it matches. They are not ours to keep
|
||||
# in the history: a minidump carries process memory and the sender's own
|
||||
# file paths.
|
||||
/Crashdmp/
|
||||
|
||||
# Runtime files the game writes beside itself when run from the tree.
|
||||
# environ.ini in particular is GENERATED from the template in
|
||||
# RP_L4/RPL4ENVIRON.cpp - a copy committed here would be a second source
|
||||
# of truth that nothing updates.
|
||||
bindings.txt
|
||||
environ.ini
|
||||
rpl4.log
|
||||
rpl4-fail.log
|
||||
last.spl
|
||||
SPOOLS/
|
||||
|
||||
# Playtest evidence - dumps, logs, symbol snapshots. Kept on disk, not in history.
|
||||
playtestlogs/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -97,10 +144,72 @@ textures now log `L4D3D.cpp couldn't load texture …` and render untextured;
|
||||
the game boots to a running window with `-windowed -res 640 480 -egg TEST.EGG`
|
||||
from a working copy like `assets/RP411/`.
|
||||
|
||||
**`steam_api.dll` is optional.** It is **delay-loaded**
|
||||
(`DelayLoadDLLs` in [RP_L4/RP_L4.vcxproj](RP_L4/RP_L4.vcxproj), with
|
||||
`delayimp.lib` supplying the helper), so a working copy that has never seen
|
||||
Steam — like `assets/RP411/`, which predates the Steam work — boots and races
|
||||
normally. Only Steam itself is switched off, logged as
|
||||
`Steam: steam_api.dll not found beside the exe - Steam features off, staying
|
||||
on TCP`.
|
||||
|
||||
Delay loading alone would only move the failure: the first call into a
|
||||
delay-loaded DLL that cannot be found raises the helper's fatal exception
|
||||
rather than returning an error. So every path that reaches a Steam symbol
|
||||
first asks `SteamNetTransport_ClientLibraryPresent()`
|
||||
([MUNGA_L4/L4STEAMTRANSPORT.cpp](MUNGA_L4/L4STEAMTRANSPORT.cpp)) — a cached
|
||||
`LoadLibrary` probe using the same plain-name lookup the helper does. The
|
||||
gates are `SteamNetTransport_Install` and the two lobby entries
|
||||
`RPL4Lobby_Host`/`_Join`; everything else in the transport and lobby is
|
||||
downstream of one of those. **Adding a new Steam call site means checking
|
||||
which gate covers it.**
|
||||
|
||||
For Steam features you need the DLL from
|
||||
[extern/steamworks_sdk_164/sdk/redistributable_bin/steam_api.dll](extern/steamworks_sdk_164/sdk/redistributable_bin/steam_api.dll)
|
||||
(the 32-bit one, not `win64\`) beside the exe, plus `steam_appid.txt` — with
|
||||
the DLL but no appid file `SteamAPI_Init` fails and the game falls back to TCP.
|
||||
|
||||
`environ.ini` must be written **without a BOM**. The parser matches key names
|
||||
from the start of the line, so a leading UTF-8 BOM silently invalidates the
|
||||
first key in the file — put `L4CONTROLS` there and it reverts to the built-in
|
||||
`KEYBOARD` default, which then fail-fasts on `0xC0000409` with
|
||||
`*****VTV has no controls mapping!*****` in the log (there is no keyboard-only
|
||||
pod mapper). PowerShell's `Set-Content -Encoding utf8` writes a BOM in 5.1;
|
||||
use `[System.IO.File]::WriteAllText` with an `ASCIIEncoding`. The log line
|
||||
`Environ: environ.ini does not mention N option(s)` naming a key that is
|
||||
plainly in the file is the tell.
|
||||
|
||||
For runtime debugging the v143 build produces full PDBs — run
|
||||
`cdb -g -G -lines -y Release rpl4opt.exe ...` from the working directory
|
||||
(cdb ships in this machine's Windows Kits).
|
||||
|
||||
### Debug keys
|
||||
|
||||
`RP412DEVKEYS=1` arms them, and they arrive through the engine keyboard
|
||||
handler, so `L4CONTROLS` must include `KEYBOARD`. That handler takes one key
|
||||
per frame off the front of the message queue and genuinely drops presses, so
|
||||
press again before concluding a key is broken.
|
||||
|
||||
| Key | State |
|
||||
|-----|-------|
|
||||
| **Alt+W** wireframe | **Live.** Reimplemented on D3D9 as a per-frame `D3DRS_FILLMODE` (`gWireframe`, [MUNGA_L4/L4VIDEO.cpp](MUNGA_L4/L4VIDEO.cpp)). The view clears to black and the sky pass is skipped, so the edges stand on their own — with the lit dome in place the far half of the scene is unreadable. The 2D pass (gunsight, cam-ship HUD) is held solid; particles are wireframed with everything else. Fog still applies, so distant edges tint toward the fog colour rather than staying white. |
|
||||
| **Alt+E** event-queue dump | **Live.** Reaches `GeneralEventQueue::DumpEventQueue`. |
|
||||
| Alt+V predator vision | Inert. |
|
||||
| Alt+F frame dump | Inert. |
|
||||
| Alt+/ perf stats | Inert. |
|
||||
| Alt+K free memory | Inert, silent. |
|
||||
| Alt+R dither, Alt+P eyepoint | Inert; they log "Function net yet enabled." |
|
||||
| Alt+Q abort | Always live, no `RP412DEVKEYS` needed. |
|
||||
|
||||
The inert ones call `DPLRenderer` methods whose bodies were commented out
|
||||
with the rest of the DPL calls in the 2007 port (`STUBBED: DPL RB 1/14/07`).
|
||||
Reviving one means writing it against D3D9 rather than un-commenting
|
||||
anything: the `dpl_*` types those bodies used are empty placeholder classes
|
||||
now ([DPLSTUB.h](DPLSTUB.h)), and `libDPL/` is reference headers that are not
|
||||
compiled. Alt+V is the worst of them — the DPL renderer implemented predator
|
||||
vision internally, reached by passing an out-of-band explosion effect type
|
||||
(`-1` on, `-2` off) with a NULL DCS, and nothing in this tree records what it
|
||||
actually looked like.
|
||||
|
||||
### Running without the cockpit (Workstream A prototype)
|
||||
|
||||
Two new environment options remove the hardware dependency entirely:
|
||||
|
||||
+242
-2
@@ -16,6 +16,7 @@
|
||||
#include "console.h"
|
||||
#include "appmsg.h"
|
||||
#include "evtstat.h"
|
||||
#include "inputscript.h"
|
||||
|
||||
#if defined(TRACE_FOREGROUND_PROCESSING)
|
||||
BitTrace Foreground_Processing("Foreground Processing");
|
||||
@@ -32,6 +33,25 @@
|
||||
Application *application = NULL;
|
||||
int Exit_Code = 0;
|
||||
Logical Application::suppressGauges = False;
|
||||
Logical Application::cameraStation = False;
|
||||
Logical Application::recordMission = False;
|
||||
|
||||
//
|
||||
// RP412CAMLOG - see app.h. Cached: the waiting trace asks once a second
|
||||
// for as long as a station sits unlaunched.
|
||||
//
|
||||
Logical
|
||||
RPCameraLog()
|
||||
{
|
||||
static int enabled = -1;
|
||||
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412CAMLOG");
|
||||
enabled = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
return enabled ? True : False;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//########################### Application ###############################
|
||||
@@ -273,6 +293,35 @@ Scalar
|
||||
return mgr->GetFrameRate();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// GetMissionElapsed
|
||||
//#############################################################################
|
||||
//
|
||||
Scalar
|
||||
Application::GetMissionElapsed()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
// gameStarted is only ever stamped by RunMissionMessageHandler, so before
|
||||
// the race it is uninitialized - and entities that are pre-runnable do get
|
||||
// performed before then. Answer zero until the clock actually exists.
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
if (
|
||||
GetApplicationState() != RunningMission
|
||||
&& GetApplicationState() != EndingMission
|
||||
)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Scalar elapsed = Now() - gameStarted;
|
||||
return (elapsed > 0.0f) ? elapsed : 0.0f;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Initialize
|
||||
@@ -577,6 +626,147 @@ Time startUpdate = Now();
|
||||
updateManager->Execute(start_of_frame);
|
||||
Time endUpdate = Now();
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
// RP412PHYSTRACE=1: the player's position, sampled on the SIMULATION's
|
||||
// own clock rather than per frame.
|
||||
//
|
||||
// This is the acceptance test for decoupling physics from frame rate.
|
||||
// Run the same egg at two frame rates and diff the traces: today they
|
||||
// diverge, because the simulation advances by whatever the last frame
|
||||
// happened to cost (SIMULATE.cpp, slice = till - lastPerformance), so a
|
||||
// 30 fps machine integrates in 33 ms steps and a 144 fps machine in 7 ms
|
||||
// ones and they are not the same race. Fixed-step them and the two
|
||||
// traces have to agree.
|
||||
//
|
||||
// Sampled every 0.25 s of SIM time on purpose - sampling per frame would
|
||||
// compare different instants and prove nothing.
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int physTrace = -1;
|
||||
|
||||
if (physTrace < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412PHYSTRACE");
|
||||
physTrace = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
//
|
||||
// The scripted-input harness shares this anchor: its clock has to
|
||||
// start at the same instant the vehicle is stopped, or the script
|
||||
// timeline shifts against the settling transient by however long
|
||||
// the load happened to take.
|
||||
//
|
||||
if ((physTrace || RPInputScript_Active()) &&
|
||||
GetApplicationState() == RunningMission)
|
||||
{
|
||||
static Logical traceStarted = False;
|
||||
static Time traceOrigin;
|
||||
static Scalar traceDue = (Scalar) 0;
|
||||
|
||||
if (!traceStarted)
|
||||
{
|
||||
traceStarted = True;
|
||||
traceOrigin = start_of_frame;
|
||||
traceDue = (Scalar) 0;
|
||||
|
||||
//
|
||||
//----------------------------------------------------------
|
||||
// Start the measurement from a known state, not merely a
|
||||
// known place.
|
||||
//
|
||||
// The pod sits on its pad simulating while the mission
|
||||
// loads, and a load is not the same length twice - two runs
|
||||
// of the same egg reached the green light 776 steps in and
|
||||
// 599 steps in. Same pad, same position, different VELOCITY,
|
||||
// and a trajectory compared from there measures the loader,
|
||||
// not the physics.
|
||||
//
|
||||
// So: stop the vehicle dead and put its clock on the same
|
||||
// mark. Every run then starts from rest at the same instant
|
||||
// and any difference that follows belongs to the simulation.
|
||||
//
|
||||
// Test scaffolding, and it only runs with the trace asked
|
||||
// for - it would be a cheat in a real race.
|
||||
//----------------------------------------------------------
|
||||
//
|
||||
Player *reset_player = GetMissionPlayer();
|
||||
Entity *reset_vehicle =
|
||||
(reset_player != NULL)
|
||||
? reset_player->GetPlayerVehicle() : NULL;
|
||||
|
||||
if (reset_vehicle != NULL &&
|
||||
reset_vehicle->IsDerivedFrom(*Mover::GetClassDerivations()))
|
||||
{
|
||||
Mover *reset_mover = (Mover *) reset_vehicle;
|
||||
|
||||
reset_mover->localVelocity = Motion::Identity;
|
||||
reset_mover->localAcceleration = Motion::Identity;
|
||||
|
||||
//
|
||||
// The clock is NOT touched. lastPerformance sits on the
|
||||
// vehicle's own step grid and the trace reads that grid
|
||||
// instead. The first version forced it to the frame
|
||||
// timestamp, which knocked the vehicle off its grid by
|
||||
// a random fraction of a step per run - and that read
|
||||
// as physics drift when it was only ever measurement.
|
||||
//
|
||||
traceOrigin = reset_mover->GetLastPerformance();
|
||||
|
||||
// the script's t=0 is this same instant
|
||||
RPInputScript_Arm(traceOrigin);
|
||||
|
||||
DEBUG_STREAM << "PhysTrace: vehicle stopped "
|
||||
<< "at the green light\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
if (physTrace)
|
||||
{
|
||||
|
||||
//
|
||||
// Sampled on the SIMULATION's clock - the vehicle's own
|
||||
// lastPerformance, which advances in whole fixed steps - so two
|
||||
// runs sample at identical step counts and their traces compare
|
||||
// exactly. Frame time samples mid-step at whatever phase the
|
||||
// frame happened to land on, which compares different instants
|
||||
// and calls the difference physics.
|
||||
//
|
||||
Player *clock_player = GetMissionPlayer();
|
||||
Entity *clock_vehicle =
|
||||
(clock_player != NULL) ? clock_player->GetPlayerVehicle() : NULL;
|
||||
|
||||
Scalar elapsed =
|
||||
(clock_vehicle != NULL)
|
||||
? (Scalar)(clock_vehicle->GetLastPerformance() - traceOrigin)
|
||||
: (Scalar)(start_of_frame - traceOrigin);
|
||||
if (elapsed >= traceDue)
|
||||
{
|
||||
traceDue += (Scalar) 0.25;
|
||||
|
||||
Player *trace_player = GetMissionPlayer();
|
||||
Entity *trace_vehicle =
|
||||
(trace_player != NULL) ? trace_player->GetPlayerVehicle() : NULL;
|
||||
|
||||
if (trace_vehicle != NULL)
|
||||
{
|
||||
extern long gPhysicsStepsTaken;
|
||||
char buffer[160];
|
||||
|
||||
sprintf(buffer,
|
||||
"PhysTrace: t=%7.3f steps=%6ld pos %12.5f %12.5f %12.5f\n",
|
||||
(double) elapsed,
|
||||
gPhysicsStepsTaken,
|
||||
(double) trace_vehicle->localOrigin.linearPosition.x,
|
||||
(double) trace_vehicle->localOrigin.linearPosition.y,
|
||||
(double) trace_vehicle->localOrigin.linearPosition.z);
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CLEAR_UPDATE_MANAGER();
|
||||
|
||||
//
|
||||
@@ -649,8 +839,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 +1330,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
|
||||
@@ -1148,7 +1359,15 @@ void
|
||||
|
||||
InterestManager *interest_mgr = GetInterestManager();
|
||||
Check(interest_mgr);
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: player data set - loading interest arenas\n" << std::flush;
|
||||
}
|
||||
interest_mgr->LoadInterestArenas(currentMission);
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: interest arenas loaded - mission created\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1184,7 +1403,16 @@ void
|
||||
trace_manager.StartTimingAnalysis();
|
||||
#endif
|
||||
Check(interestManager);
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: LoadMission handler - loading interest manager\n" << std::flush;
|
||||
}
|
||||
interestManager->LoadMission(currentMission);
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: interest manager loaded - about to make the player\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -1354,6 +1582,13 @@ void
|
||||
case CreatingMission:
|
||||
case LoadingMission:
|
||||
case WaitingForLaunch:
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: CheckLoad state=" << (int) applicationState.GetState()
|
||||
<< " minPriorityQueueEmpty="
|
||||
<< (int) (eventQueue->IsPriorityEmpty(MinEventPriority) ? 1 : 0)
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
if (eventQueue->IsPriorityEmpty(MinEventPriority))
|
||||
{
|
||||
Host *console_host;
|
||||
@@ -1406,6 +1641,11 @@ void
|
||||
RunMissionMessage run_mission_message;
|
||||
Post(DefaultEventPriority, this, &run_mission_message);
|
||||
Tell("Sent ready message to ourselves\n");
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: no console - posted RunMission to ourselves\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
@@ -56,6 +56,20 @@ class GeneralEventQueue;
|
||||
class Entity__MakeMessage;
|
||||
class ResourceFile;
|
||||
|
||||
//##########################################################################
|
||||
//######################### Live Cam tracing #############################
|
||||
//##########################################################################
|
||||
//
|
||||
// RP412CAMLOG=1 traces the camera-station bring-up: the registry choosing
|
||||
// a director, the director creating its camera ship, and the launch
|
||||
// handshake it waits on. The Live Cam path is selected purely by egg data
|
||||
// (hostType=1 plus vehicle=camera), so when it does not come up there is
|
||||
// nothing in the log to say how far it got - which is exactly the hole
|
||||
// this fills. Off by default; it prints once per second while waiting.
|
||||
//
|
||||
Logical
|
||||
RPCameraLog();
|
||||
|
||||
//##########################################################################
|
||||
//######################### Application ##############################
|
||||
//##########################################################################
|
||||
@@ -318,6 +332,15 @@ public:
|
||||
Scalar
|
||||
GetSecondsRemainingInGame()
|
||||
{return secondsRemainingInGame;}
|
||||
//
|
||||
// Seconds since the console's RunMission started the race, counting up.
|
||||
// Every machine anchors this on the same message, so anything derived
|
||||
// from it agrees across the mesh without being replicated - see the
|
||||
// clockwork doors in DOOR.cpp. Reads 0 outside a running mission
|
||||
// (gameStarted holds garbage until RunMission stamps it).
|
||||
//
|
||||
Scalar
|
||||
GetMissionElapsed();
|
||||
ApplicationID
|
||||
GetApplicationID()
|
||||
{return applicationID;}
|
||||
@@ -431,6 +454,32 @@ public:
|
||||
|
||||
static Logical DoSuppressGauges() { return suppressGauges; }
|
||||
|
||||
//
|
||||
// A Live Cam station. It has no pod, so none of the five instrument
|
||||
// MFDs have anything to put on them - the panes would just composite
|
||||
// as black holes over the view. It DOES keep the map, but landscape:
|
||||
// the pod's is portrait because that is how the glass was physically
|
||||
// mounted in the cabinet, and a camera has no cabinet.
|
||||
//
|
||||
// Settable because the role is picked in the lobby rather than passed
|
||||
// as -lc on the command line, so the front end has to hand it over.
|
||||
// That happens before the renderers are built - the single-binary race
|
||||
// loop makes a fresh application per race, after the menu.
|
||||
//
|
||||
static Logical IsCameraStation() { return cameraStation; }
|
||||
static void SetCameraStation(Logical state) { cameraStation = state; }
|
||||
|
||||
//
|
||||
// Whether to keep a spool of this race. Picked on the setup screen
|
||||
// under the role, and set the same way and for the same reason: it
|
||||
// has to be known before the network manager is built, which happens
|
||||
// after the menu, and it must be set either way because the same
|
||||
// process races again and a stale True would quietly record a session
|
||||
// nobody asked to keep.
|
||||
//
|
||||
static Logical IsRecording() { return recordMission; }
|
||||
static void SetRecording(Logical state) { recordMission = state; }
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Modules
|
||||
//
|
||||
@@ -466,6 +515,8 @@ protected:
|
||||
*modeManager;
|
||||
|
||||
static Logical suppressGauges;
|
||||
static Logical cameraStation;
|
||||
static Logical recordMission;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Module Creation
|
||||
|
||||
@@ -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)
|
||||
@@ -231,6 +234,74 @@ Background_Loop:
|
||||
}
|
||||
Time endBackground = Now();
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------
|
||||
// RP412GAUGEDIAG=1: where the frame actually goes.
|
||||
//
|
||||
// These four timestamps have been computed every frame since forever
|
||||
// and never reported. The whole cockpit problem is a question about
|
||||
// this split - the background loop only gets what the foreground
|
||||
// leaves - and it has been measurable all along.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int
|
||||
frameSplitDiag = -1;
|
||||
|
||||
if (frameSplitDiag < 0)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412GAUGEDIAG");
|
||||
|
||||
frameSplitDiag = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (frameSplitDiag)
|
||||
{
|
||||
static Scalar
|
||||
foregroundSum = (Scalar) 0,
|
||||
backgroundSum = (Scalar) 0,
|
||||
frameSum = (Scalar) 0;
|
||||
static int
|
||||
splitFrames = 0;
|
||||
static Logical
|
||||
splitStarted = False;
|
||||
static Time
|
||||
splitWindowStart;
|
||||
|
||||
Time
|
||||
splitNow = Now();
|
||||
|
||||
foregroundSum += (Scalar)(endForeground - startForeground);
|
||||
backgroundSum += (Scalar)(endBackground - startBackground);
|
||||
frameSum += (Scalar)(splitNow - beginFrameTimestamp);
|
||||
++splitFrames;
|
||||
|
||||
if (!splitStarted)
|
||||
{
|
||||
splitStarted = True;
|
||||
splitWindowStart = splitNow;
|
||||
}
|
||||
else if ((Scalar)(splitNow - splitWindowStart) >= (Scalar) 2)
|
||||
{
|
||||
char
|
||||
buffer[200];
|
||||
|
||||
sprintf(buffer,
|
||||
"FrameSplit: %d frames | foreground %.2f ms | "
|
||||
"background %.2f ms | whole frame %.2f ms\n",
|
||||
splitFrames,
|
||||
(double)(foregroundSum * 1000.0f / splitFrames),
|
||||
(double)(backgroundSum * 1000.0f / splitFrames),
|
||||
(double)(frameSum * 1000.0f / splitFrames));
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
|
||||
foregroundSum = backgroundSum = frameSum = (Scalar) 0;
|
||||
splitFrames = 0;
|
||||
splitWindowStart = splitNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//char str[256];
|
||||
//Scalar lastFrameLength = Now() - beginFrameTimestamp;
|
||||
//sprintf(str, "RPL4 - %.2f FPS", 1.0f / lastFrameLength);
|
||||
|
||||
@@ -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:
|
||||
|
||||
+10
-2
@@ -94,8 +94,16 @@ void
|
||||
}
|
||||
headEntitySocket.Add(entity);
|
||||
|
||||
alDistanceModel(AL_LINEAR_DISTANCE);
|
||||
alDopplerFactor(0.3f);
|
||||
// FIDELITY (docs/SOUND.md F3/F10): the engine computes the AUTHORED distance
|
||||
// attenuation curve (AUDIO.INI amplitude_rolloff knee/exponent ->
|
||||
// AudioLocation::distanceVolumeScale) and the AUTHORED doppler-cents model
|
||||
// (doppler_range=600 / speed_of_sound=250) on every spatial update. Disable
|
||||
// OpenAL's own models so they cannot double-apply or fight them:
|
||||
// AL_LINEAR_DISTANCE faded distant audio to zero on a straight line where the
|
||||
// authored curve still sits near 44% at the clip edge, and AL doppler ran at
|
||||
// the wrong constants with a sign-inverted velocity feed.
|
||||
alDistanceModel(AL_NONE);
|
||||
alDopplerFactor(0.0f);
|
||||
|
||||
#if 0
|
||||
//
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
//############################# CameraShip ################################
|
||||
//##########################################################################
|
||||
|
||||
//
|
||||
// Trackside camera cuts since the last RP412CAMLOG report - counted where
|
||||
// the cut happens and drained where it is reported, both in FollowGoal.
|
||||
//
|
||||
static int gCameraCuts = 0;
|
||||
|
||||
//#############################################################################
|
||||
// Shared Data Support
|
||||
//
|
||||
@@ -320,6 +326,127 @@ void
|
||||
Point3D target;
|
||||
target.Multiply(focusOffset, goalEntity->localToWorld);
|
||||
|
||||
//
|
||||
// RP412CAMLOG: is the thing we are following actually MOVING every
|
||||
// step, or arriving in jumps?
|
||||
//
|
||||
// This runs on the fixed simulation step, so it is called at a steady
|
||||
// rate whatever the frame rate. The entity it follows is a REMOTE pod,
|
||||
// whose position only changes when an update lands - so if the watched
|
||||
// point is identical on most steps and then leaps, the camera is
|
||||
// tracking a staircase and no amount of smoothing here will hide it.
|
||||
// Reported as: steps counted, how many of them saw any movement at
|
||||
// all, the largest single jump, and how often the trackside camera was
|
||||
// cut to a different one (a cut is a snap, not a glide, and would look
|
||||
// like jank of a different kind).
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
static Scalar next_say = 0.0f;
|
||||
static Point3D last_target(0.0f, 0.0f, 0.0f);
|
||||
static Logical have_last = False;
|
||||
static int steps = 0;
|
||||
static int moved = 0;
|
||||
static Scalar biggest = 0.0f;
|
||||
|
||||
//
|
||||
// A visible tick IS a step that moves much further, or much less,
|
||||
// than the steps around it. Measuring that directly beats measuring
|
||||
// anything about where the number came from: it does not care
|
||||
// whether the cause is the network, the catch-up rule, or something
|
||||
// nobody has thought of yet.
|
||||
//
|
||||
// Judged against a short running mean rather than an absolute
|
||||
// distance, because a pod at 75 m/s moves 1.5 m per step and one
|
||||
// sitting against a wall moves nothing - a fixed threshold would
|
||||
// call every acceleration a spike.
|
||||
//
|
||||
// What confirms: spikes running at a few per second. That is the
|
||||
// reported tick, and its rate is in the count.
|
||||
// What refutes: spikes and stalls near zero. Then entity motion is
|
||||
// smooth and the tick is not in the simulation at all, which points
|
||||
// at frame delivery or the map raster instead.
|
||||
//
|
||||
static Scalar mean_step = 0.0f;
|
||||
static int spikes = 0;
|
||||
static int stalls = 0;
|
||||
static int respawns = 0;
|
||||
static Scalar worst_ratio = 0.0f;
|
||||
|
||||
++steps;
|
||||
if (have_last)
|
||||
{
|
||||
Vector3D step_delta;
|
||||
step_delta.Subtract(target, last_target);
|
||||
Scalar distance = step_delta.Length();
|
||||
if (distance > 0.0f)
|
||||
{
|
||||
++moved;
|
||||
}
|
||||
if (distance > biggest)
|
||||
{
|
||||
biggest = distance;
|
||||
}
|
||||
|
||||
//
|
||||
// A respawn teleports hundreds of metres and is MEANT to be a
|
||||
// discontinuity. Counted, excluded, and the mean restarted so
|
||||
// one does not brand the following steps as stalls.
|
||||
//
|
||||
if (distance > 50.0f)
|
||||
{
|
||||
++respawns;
|
||||
mean_step = 0.0f;
|
||||
}
|
||||
else if (mean_step > 0.01f)
|
||||
{
|
||||
Scalar ratio = distance / mean_step;
|
||||
if (ratio > 2.5f)
|
||||
{
|
||||
++spikes;
|
||||
if (ratio > worst_ratio)
|
||||
{
|
||||
worst_ratio = ratio;
|
||||
}
|
||||
}
|
||||
else if (ratio < 0.4f)
|
||||
{
|
||||
++stalls;
|
||||
}
|
||||
mean_step = mean_step * 0.9f + distance * 0.1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
mean_step = distance;
|
||||
}
|
||||
}
|
||||
last_target = target;
|
||||
have_last = True;
|
||||
|
||||
if ((Scalar) Now() >= next_say)
|
||||
{
|
||||
if (next_say > 0.0f)
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: follow - " << steps << " steps, "
|
||||
<< moved << " moved, biggest jump " << biggest
|
||||
<< "m, " << gCameraCuts << " cut(s)\n" << std::flush;
|
||||
DEBUG_STREAM << "CamLog: smoothness - " << spikes
|
||||
<< " spike(s), " << stalls << " stall(s), "
|
||||
<< respawns << " respawn(s), worst " << worst_ratio
|
||||
<< "x the running mean of " << mean_step << "m\n" << std::flush;
|
||||
}
|
||||
next_say = ((Scalar) Now()) + 5.0f;
|
||||
steps = 0;
|
||||
moved = 0;
|
||||
biggest = 0.0f;
|
||||
spikes = 0;
|
||||
stalls = 0;
|
||||
respawns = 0;
|
||||
worst_ratio = 0.0f;
|
||||
gCameraCuts = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// If time has not yet expired on this camera, keep with it if can see the
|
||||
@@ -348,6 +475,7 @@ void
|
||||
//
|
||||
if (currentCamera != old_camera)
|
||||
{
|
||||
++gCameraCuts;
|
||||
AimCameraAtPoint(target);
|
||||
lastSwitch = lastPerformance;
|
||||
}
|
||||
|
||||
+19
-2
@@ -58,6 +58,23 @@
|
||||
|
||||
#define Check(p)
|
||||
#define Check_Signature(p)
|
||||
//#define Fail(m) Fail_To_Debugger(m,__FILE__,__LINE__)
|
||||
#define Fail(m) abort();
|
||||
|
||||
//
|
||||
// A release build used to answer Fail("clipping_radius not defined") with a
|
||||
// bare abort(), throwing the message away - and because the compiler merges
|
||||
// identical cold paths, every Fail in a function became the same anonymous
|
||||
// stub. A crash then told you the function and nothing else, which cost an
|
||||
// afternoon of disassembly to learn that a mission review build stops in
|
||||
// L4AudioRenderer::Initialize without ever saying which of its nine checks
|
||||
// was the one that tripped.
|
||||
//
|
||||
// The debug build has always routed this to a function that prints the
|
||||
// message with its file and line. Release now does too. The termination is
|
||||
// unchanged - still abort(), still exit code 0xC0000409 - so nothing
|
||||
// downstream sees anything different; it just says what happened on the way
|
||||
// out.
|
||||
//
|
||||
extern void Fail_With_Message(const char *message, const char *file, int line);
|
||||
|
||||
#define Fail(m) Fail_With_Message(m, __FILE__, __LINE__)
|
||||
#define Cast_Object(type, ptr) ((type)(ptr))
|
||||
+65
-140
@@ -72,172 +72,96 @@ Door::AttributeIndexSet& Door::GetAttributeIndex()
|
||||
//#############################################################################
|
||||
// Model Support
|
||||
//
|
||||
void
|
||||
Door::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(message);
|
||||
Subsystem::ReadUpdateRecord(message);
|
||||
UpdateRecord* record = (UpdateRecord*) message;
|
||||
|
||||
percentOpen = record->percentOpen;
|
||||
switch (GetSimulationState())
|
||||
{
|
||||
case Opening:
|
||||
case Closing:
|
||||
phaseTimeRemaining = travelTime;
|
||||
break;
|
||||
case Opened:
|
||||
case Closed:
|
||||
phaseTimeRemaining = deadTime;
|
||||
break;
|
||||
}
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door updated to state "
|
||||
// << GetSimulationState() << " @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
MoveCollisionVolume(percentOpen);
|
||||
Check_Fpu();
|
||||
}
|
||||
// There is no ReadUpdateRecord/WriteUpdateRecord pair here on purpose. Doors
|
||||
// are Hermit instances built independently on every host, so no door state is
|
||||
// ever sent or received - the phase function below is the only thing that
|
||||
// decides where a door is, and it reaches the same answer everywhere.
|
||||
//
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
Door::WriteUpdateRecord(Simulation::UpdateRecord *record, int update_model)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(record);
|
||||
|
||||
Subsystem::WriteUpdateRecord(record, update_model);
|
||||
|
||||
UpdateRecord *update = (UpdateRecord*)record;
|
||||
update->percentOpen = percentOpen;
|
||||
update->recordLength = sizeof(*update);
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
Door::SlideDoor(Scalar time_slice)
|
||||
Door::SlideDoor(Scalar)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
//------------------------------------------------------------
|
||||
// Advance the clock, then branch based upon our current state
|
||||
//------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
// The door is clockwork. Its position is a function of how long the race
|
||||
// has been running, not of a countdown integrated frame by frame, so:
|
||||
//
|
||||
int new_state;
|
||||
if (time_slice > 1.0f)
|
||||
// - every machine puts this door in the same place from the same mission
|
||||
// clock, without a byte crossing the wire,
|
||||
// - a frame hitch of any length costs nothing, because there is no
|
||||
// accumulated state left to fall behind (the old code dropped any slice
|
||||
// over a second outright and never got that time back).
|
||||
//
|
||||
// Phase zero is the instant the door starts to close, fully open, which is
|
||||
// where the original state machine began from DefaultState:
|
||||
//
|
||||
// [0, travel) Closing 1 -> 0
|
||||
// [travel, travel+dead) Closed 0
|
||||
// [travel+dead, 2travel+dead) Opening 0 -> 1
|
||||
// [2travel+dead, cycle) Opened 1
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
if (cycleTime <= 0.0f)
|
||||
{
|
||||
MoveCollisionVolume(0.0f);
|
||||
SetSimulationState(Closed);
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
phaseTimeRemaining -= time_slice;
|
||||
Scalar percent_open;
|
||||
switch (GetSimulationState())
|
||||
|
||||
Check(application);
|
||||
Scalar phase = fmod(application->GetMissionElapsed() - phaseOffset, cycleTime);
|
||||
if (phase < 0.0f)
|
||||
{
|
||||
phase += cycleTime;
|
||||
}
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// If the door is not done opening, set its new position, otherwise branch
|
||||
// to the opened state
|
||||
//------------------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
// Pick the band. Each division below is guarded by the comparison that
|
||||
// selected the branch, so a door with a zero travelTime or deadTime simply
|
||||
// loses that band rather than dividing by zero.
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
case Opening:
|
||||
Door_Opening:
|
||||
new_state = Opening;
|
||||
if (phaseTimeRemaining > 0.0f)
|
||||
{
|
||||
percent_open = 1.0f - phaseTimeRemaining/travelTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
phaseTimeRemaining += deadTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opened @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Opened;
|
||||
}
|
||||
currentVelocity.Subtract(
|
||||
worldExtent,
|
||||
GetEntity()->localOrigin.linearPosition
|
||||
);
|
||||
currentVelocity /= travelTime;
|
||||
Check_Fpu();
|
||||
break;
|
||||
Scalar open_start = travelTime + deadTime;
|
||||
int new_state;
|
||||
Scalar percent_open;
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------
|
||||
// If the door is ready to start closing, jump to closing state
|
||||
//-------------------------------------------------------------
|
||||
//
|
||||
case Opened:
|
||||
Door_Opened:
|
||||
new_state = Opened;
|
||||
if (phaseTimeRemaining <= 0.0f)
|
||||
{
|
||||
phaseTimeRemaining += travelTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closing @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Closing;
|
||||
}
|
||||
percent_open = 1.0f;
|
||||
currentVelocity = Vector3D::Identity;
|
||||
Check_Fpu();
|
||||
break;
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// If the door is not done closing, set its new position, otherwise branch
|
||||
// to the closed state
|
||||
//------------------------------------------------------------------------
|
||||
//
|
||||
case DefaultState:
|
||||
phaseTimeRemaining = travelTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door default @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
case Closing:
|
||||
Door_Closing:
|
||||
if (phase < travelTime)
|
||||
{
|
||||
new_state = Closing;
|
||||
if (phaseTimeRemaining > 0.0f)
|
||||
{
|
||||
percent_open = phaseTimeRemaining/travelTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
phaseTimeRemaining += deadTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closed @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Closed;
|
||||
}
|
||||
percent_open = 1.0f - phase/travelTime;
|
||||
currentVelocity.Subtract(
|
||||
GetEntity()->localOrigin.linearPosition,
|
||||
worldExtent
|
||||
);
|
||||
currentVelocity /= travelTime;
|
||||
Check_Fpu();
|
||||
break;
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------
|
||||
// If the door is ready to start opening, jump to opening state
|
||||
//-------------------------------------------------------------
|
||||
//
|
||||
case Closed:
|
||||
Door_Closed:
|
||||
}
|
||||
else if (phase < open_start)
|
||||
{
|
||||
new_state = Closed;
|
||||
if (phaseTimeRemaining <= 0.0f)
|
||||
{
|
||||
phaseTimeRemaining += travelTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opening @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Opening;
|
||||
}
|
||||
percent_open = 0.0f;
|
||||
currentVelocity = Vector3D::Identity;
|
||||
Check_Fpu();
|
||||
break;
|
||||
|
||||
}
|
||||
else if (phase < open_start + travelTime)
|
||||
{
|
||||
new_state = Opening;
|
||||
percent_open = (phase - open_start)/travelTime;
|
||||
currentVelocity.Subtract(
|
||||
worldExtent,
|
||||
GetEntity()->localOrigin.linearPosition
|
||||
);
|
||||
currentVelocity /= travelTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
new_state = Opened;
|
||||
percent_open = 1.0f;
|
||||
currentVelocity = Vector3D::Identity;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -344,7 +268,8 @@ Door::Door(
|
||||
//
|
||||
// Initialize variables
|
||||
//
|
||||
phaseTimeRemaining = 0.0f;
|
||||
phaseOffset = 0.0f;
|
||||
cycleTime = 2.0f*(travelTime + deadTime);
|
||||
currentPosition = Point3D::Identity;
|
||||
|
||||
SetPerformance(&Door::SlideDoor);
|
||||
|
||||
+16
-19
@@ -20,16 +20,11 @@ struct Door__SubsystemResource:
|
||||
collisionID;
|
||||
};
|
||||
|
||||
//##########################################################################
|
||||
//##################### Chute::UpdateRecord #####################
|
||||
//##########################################################################
|
||||
|
||||
struct Door__UpdateRecord :
|
||||
public Subsystem::UpdateRecord
|
||||
{
|
||||
Scalar
|
||||
percentOpen;
|
||||
};
|
||||
//
|
||||
// A door has no update record. It is Hermit clockwork - every host builds
|
||||
// its own out of the map stream and derives the position from the mission
|
||||
// clock, so there is nothing to publish and nothing to receive.
|
||||
//
|
||||
|
||||
//##########################################################################
|
||||
//######################### CLASS Door ########################
|
||||
@@ -91,7 +86,6 @@ public:
|
||||
|
||||
typedef void
|
||||
(Door::*Performance)(Scalar time_slice);
|
||||
typedef Door__UpdateRecord UpdateRecord;
|
||||
|
||||
void
|
||||
SetPerformance(Performance performance)
|
||||
@@ -109,12 +103,6 @@ public:
|
||||
GetFirstBoxedSolid()
|
||||
{Check(this); return collisionVolumes;}
|
||||
|
||||
protected:
|
||||
void
|
||||
WriteUpdateRecord(Simulation::UpdateRecord *message, int update_model);
|
||||
void
|
||||
ReadUpdateRecord(Simulation::UpdateRecord *message);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Construction and Destruction
|
||||
//
|
||||
@@ -152,9 +140,18 @@ private:
|
||||
worldExtent;
|
||||
|
||||
Scalar
|
||||
phaseTimeRemaining,
|
||||
travelTime,
|
||||
deadTime;
|
||||
deadTime,
|
||||
//
|
||||
// Where in the cycle this door sits at mission time zero, and the
|
||||
// length of one full open-close-open cycle. phaseOffset is not in
|
||||
// the subsystem resource yet: every door in the game is in lockstep,
|
||||
// and adding a field to Door__SubsystemResource changes its sizeof,
|
||||
// which invalidates every prebuilt .res. Wire it to a "PhaseOffset"
|
||||
// notation entry when there is a reason to rebuild resources.
|
||||
//
|
||||
phaseOffset,
|
||||
cycleTime;
|
||||
|
||||
int collisionVolumeCount;
|
||||
|
||||
|
||||
+8
-1
@@ -126,8 +126,15 @@ Logical
|
||||
}
|
||||
|
||||
creation_message->classToCreate = RegisteredClass::DoorFrameClassID;
|
||||
//
|
||||
// Hermit, not Master: every host builds its own doorframe out of the map
|
||||
// stream (see the DoorFrameClassID exemption in LoadMapStream) and runs it
|
||||
// off the mission clock. Hermit is the instance kind DynamicEntityCreation
|
||||
// does NOT broadcast, which is what stops N machines each announcing the
|
||||
// same doorframe and producing N-squared of them.
|
||||
//
|
||||
creation_message->instanceFlags =
|
||||
MasterInstance|DynamicFlag|MapFlag|TrappedFlag;
|
||||
HermitInstance|DynamicFlag|MapFlag|TrappedFlag;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+39
-1
@@ -224,9 +224,47 @@ void
|
||||
//-----------------------------------------------------
|
||||
//
|
||||
highest = highest - lowest + 1;
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// RP412SPAWNZONE pins which drop zone is tried first, so a test run can
|
||||
// be repeated.
|
||||
//
|
||||
// The pick below is Random(), and Random() is seeded - but the seed only
|
||||
// makes a run repeatable if the same NUMBER of draws happens first, and
|
||||
// that depends on how many frames the mission load took. So two runs of
|
||||
// the same egg with the same RANDOM= still start on different pads, over
|
||||
// different ground, and no two traces can be compared. That is not a
|
||||
// game bug, but it makes the physics unmeasurable.
|
||||
//
|
||||
// Pinned, the zone is tried first and the loop falls back to the random
|
||||
// walk if it is taken - so this can never wedge, and it changes nothing
|
||||
// unless it is set.
|
||||
//------------------------------------------------------------------------
|
||||
//
|
||||
static int
|
||||
pinnedZone = -2;
|
||||
|
||||
if (pinnedZone == -2)
|
||||
{
|
||||
const char *setting = getenv("RP412SPAWNZONE");
|
||||
pinnedZone = (setting != NULL) ? atoi(setting) : -1;
|
||||
}
|
||||
|
||||
Logical
|
||||
tryPinnedZone = (pinnedZone >= 0) ? True : False;
|
||||
|
||||
while (remaining)
|
||||
{
|
||||
i = lowest + Random(highest);
|
||||
if (tryPinnedZone)
|
||||
{
|
||||
tryPinnedZone = False;
|
||||
i = lowest + (pinnedZone % highest);
|
||||
}
|
||||
else
|
||||
{
|
||||
i = lowest + Random(highest);
|
||||
}
|
||||
Verify(i < dropZoneCount && i >= 0);
|
||||
if (IsAvailable(i))
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -410,6 +435,61 @@ void
|
||||
//-----------------------
|
||||
//
|
||||
UpdateRecord *update = (UpdateRecord*)record;
|
||||
|
||||
//
|
||||
// RP412CAMLOG: how hard does an arriving update MOVE this
|
||||
// entity, and how often?
|
||||
//
|
||||
// Between updates a replicant is dead-reckoned from
|
||||
// updateOrigin over (lastPerformance - lastUpdate). When the
|
||||
// next one lands the basis is replaced, so the drawn position
|
||||
// jumps by however far the prediction had drifted. Render
|
||||
// interpolation cannot hide that: it smooths within a step,
|
||||
// and this is a discontinuity in the stepped values
|
||||
// themselves. If the interval is a few per second and the
|
||||
// correction is tens of centimetres, that is a tick a few
|
||||
// times a second - which is the symptom being chased.
|
||||
//
|
||||
// Reported per entity, on a five second clock, so a busy race
|
||||
// does not bury the log.
|
||||
//
|
||||
if (RPCameraLog() && GetInstance() == ReplicantInstance)
|
||||
{
|
||||
static Scalar next_say = 0.0f;
|
||||
static int corrections = 0;
|
||||
static Scalar worst = 0.0f;
|
||||
static Scalar total = 0.0f;
|
||||
|
||||
Vector3D drift;
|
||||
drift.Subtract(
|
||||
update->localOrigin.linearPosition,
|
||||
localOrigin.linearPosition
|
||||
);
|
||||
Scalar distance = drift.Length();
|
||||
++corrections;
|
||||
total += distance;
|
||||
if (distance > worst)
|
||||
{
|
||||
worst = distance;
|
||||
}
|
||||
|
||||
if ((Scalar) Now() >= next_say)
|
||||
{
|
||||
if (next_say > 0.0f && corrections > 0)
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: replicant corrections - "
|
||||
<< corrections << " in 5s ("
|
||||
<< (corrections / 5) << "/s), mean "
|
||||
<< (total / corrections) << "m, worst "
|
||||
<< worst << "m\n" << std::flush;
|
||||
}
|
||||
next_say = ((Scalar) Now()) + 5.0f;
|
||||
corrections = 0;
|
||||
worst = 0.0f;
|
||||
total = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
updateOrigin = update->localOrigin;
|
||||
|
||||
//
|
||||
@@ -426,6 +506,13 @@ void
|
||||
{
|
||||
localOrigin = updateOrigin;
|
||||
localToWorld = localOrigin;
|
||||
|
||||
//
|
||||
// Set outside the step loop, so renderPreviousOrigin now
|
||||
// describes a step that never happened. Draw plainly until
|
||||
// a real one does.
|
||||
//
|
||||
renderStepTaken = False;
|
||||
}
|
||||
Simulation::ReadUpdateRecord(record);
|
||||
}
|
||||
@@ -720,6 +807,118 @@ void
|
||||
//
|
||||
if (GetInstance() != ReplicantInstance)
|
||||
{
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// Fixed-step: the subsystems and the entity advance TOGETHER,
|
||||
// one step at a time, because they read each other mid-flight.
|
||||
// The VTV's hover spring is computed from its thrusters'
|
||||
// measured heights, and each thruster measures from where the
|
||||
// vehicle IS - so thrusters stepped twice against a vehicle
|
||||
// that has not moved yet hand back two identical height
|
||||
// samples, and the spring fires twice on stale data. Measured,
|
||||
// that pod climbs at 30 fps and flies level at 144.
|
||||
//
|
||||
// So the step loop lives HERE, above both: everyone is walked
|
||||
// to the same sub-frame instant before anyone takes the next
|
||||
// step. Watchers and the update stream still run once per
|
||||
// frame, after the loop - stepping is physics, watching is
|
||||
// I/O, and only the first belongs inside.
|
||||
//
|
||||
// The interleave keys off the ENTITY's own clock so a
|
||||
// subsystem created mid-flight (they are made alongside their
|
||||
// owner) can never wedge the loop.
|
||||
//----------------------------------------------------------------
|
||||
//
|
||||
Scalar fixed_step = Simulation::FixedStep();
|
||||
|
||||
if (fixed_step > (Scalar) 0)
|
||||
{
|
||||
//
|
||||
// One grid for the whole vehicle. Every Simulation anchors
|
||||
// its own lastPerformance at its creation time, so an
|
||||
// entity and its subsystems were stepping on grids offset
|
||||
// by a random fraction of a step - deterministic within a
|
||||
// run, DIFFERENT between runs, because creation times ride
|
||||
// on load timing. The thrusters' measurements then landed
|
||||
// a different sub-step distance from the vehicle's
|
||||
// integration every launch, which is physics drift no seed
|
||||
// can pin. Snap the subsystems onto the entity's grid; the
|
||||
// interleave below then keeps everyone in lockstep by
|
||||
// construction, and once aligned this assignment is a
|
||||
// no-op every frame after.
|
||||
//
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->SetLastPerformance(
|
||||
GetLastPerformance());
|
||||
}
|
||||
}
|
||||
|
||||
Time step_till = GetLastPerformance();
|
||||
step_till += fixed_step;
|
||||
|
||||
while (step_till <= till)
|
||||
{
|
||||
//
|
||||
// BeginStep on the ENTITY comes before the subsystems
|
||||
// perform: the Mover's force accumulator is cleared
|
||||
// here, and the thrusters then ADD this step's forces
|
||||
// into a clean slate. The first version left that
|
||||
// clear on the per-frame path, so a two-step frame
|
||||
// integrated step one's thrust twice - and since how
|
||||
// many steps land in a frame rides on wall-clock
|
||||
// jitter, no two runs saw the same force history.
|
||||
// Identical configs measured 0.23 apart because of it.
|
||||
//
|
||||
BeginStep();
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->BeginStep();
|
||||
subsystemArray[i]->PerformTo(step_till);
|
||||
}
|
||||
}
|
||||
Simulation::PerformTo(step_till);
|
||||
step_till += fixed_step;
|
||||
}
|
||||
|
||||
//
|
||||
// How far past the last completed step the frame we are
|
||||
// about to draw falls, as a fraction of one step. This is
|
||||
// the leftover the fixed-step loop deliberately does not
|
||||
// simulate - see renderPreviousOrigin in entity.h.
|
||||
//
|
||||
{
|
||||
Scalar leftover = till - GetLastPerformance();
|
||||
Scalar fraction = (fixed_step > (Scalar) 0)
|
||||
? (leftover / fixed_step) : (Scalar) 0;
|
||||
if (fraction < (Scalar) 0) fraction = (Scalar) 0;
|
||||
if (fraction > (Scalar) 1) fraction = (Scalar) 1;
|
||||
renderStepFraction = fraction;
|
||||
}
|
||||
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->WatchAndWrite(update_stream);
|
||||
}
|
||||
}
|
||||
|
||||
SET_PERFORM_ENTITY();
|
||||
Simulation::WatchAndWrite(update_stream);
|
||||
Check_Fpu();
|
||||
CLEAR_PERFORM_ENTITY();
|
||||
CLEAR_PERFORM_SUBSYSTEMS();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i])
|
||||
@@ -937,6 +1136,16 @@ Entity::Entity(
|
||||
updateOrigin = localOrigin;
|
||||
localToWorld = localOrigin;
|
||||
|
||||
//
|
||||
// Render interpolation starts with nothing to blend: the previous
|
||||
// origin IS the current one and no fraction of a step is outstanding,
|
||||
// so GetRenderToWorld hands back localToWorld until the first step has
|
||||
// actually been taken.
|
||||
//
|
||||
renderPreviousOrigin = localOrigin;
|
||||
renderStepFraction = (Scalar) 0;
|
||||
renderStepTaken = False;
|
||||
|
||||
// initialize camera stuff
|
||||
cameraOffset = Origin::Identity;
|
||||
|
||||
@@ -1255,6 +1464,93 @@ Logical
|
||||
return IsDerivedFrom(*GetClassDerivations());
|
||||
}
|
||||
|
||||
//##########################################################################
|
||||
// Render interpolation - see entity.h for why, and for why it is render
|
||||
// only. RP412INTERP=0 turns it off so the stepping it removes can be seen
|
||||
// again on a test machine without a rebuild.
|
||||
//##########################################################################
|
||||
//
|
||||
static Logical
|
||||
RenderInterpolationEnabled()
|
||||
{
|
||||
static int enabled = -1;
|
||||
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412INTERP");
|
||||
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
||||
if (!enabled)
|
||||
{
|
||||
DEBUG_STREAM << "Render: interpolation off (RP412INTERP=0) - "
|
||||
<< "drawn motion steps at the physics rate\n" << std::flush;
|
||||
}
|
||||
}
|
||||
return enabled ? True : False;
|
||||
}
|
||||
|
||||
void
|
||||
Entity::SnapshotRenderOrigin()
|
||||
{
|
||||
Check(this);
|
||||
renderPreviousOrigin = localOrigin;
|
||||
renderStepTaken = True;
|
||||
}
|
||||
|
||||
void
|
||||
Entity::SetRenderStepFraction(Scalar fraction)
|
||||
{
|
||||
Check(this);
|
||||
renderStepFraction = fraction;
|
||||
}
|
||||
|
||||
void
|
||||
Entity::GetRenderToWorld(LinearMatrix *out)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(out);
|
||||
|
||||
//
|
||||
// No fixed step at all, interpolation switched off, or this entity has
|
||||
// not taken a step yet. Hand back exactly what every caller used before
|
||||
// this existed.
|
||||
//
|
||||
// The test used to be on the fraction rather than on renderStepTaken,
|
||||
// and that was wrong in a way that showed. Drawing at fraction f means
|
||||
// drawing at the start of the step plus f of it, so f = 0 means the
|
||||
// START of the step - while this early return hands back localToWorld,
|
||||
// which is its END. The two are a whole step apart, about a metre at
|
||||
// racing speed.
|
||||
//
|
||||
// behind is a whole number of milliseconds against a 20ms step, so it
|
||||
// lands on exactly zero about one frame in twenty. On those frames a
|
||||
// pod was drawn a full step ahead of itself and then snapped back on
|
||||
// the next one: a jump three times a second at 59fps, regular because
|
||||
// the beat between frame rate and step rate is regular, and worst when
|
||||
// a pod crosses the view quickly. It never appeared in any simulation
|
||||
// trace because the simulation was right - only the drawing was wrong.
|
||||
//
|
||||
// Interpolating at f = 0 is continuous with everything either side of
|
||||
// it. Each frame advances the drawn position by frame_time / step
|
||||
// whether or not a step boundary falls between the two, which is the
|
||||
// entire point of doing it.
|
||||
//
|
||||
if (!RenderInterpolationEnabled() || !renderStepTaken)
|
||||
{
|
||||
*out = localToWorld;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Origin::Lerp does the position and the shortest-arc quaternion, and
|
||||
// normalises - which over a 20 ms step is indistinguishable from a
|
||||
// true slerp and a good deal cheaper.
|
||||
//
|
||||
Origin blended;
|
||||
blended.Lerp(renderPreviousOrigin, localOrigin, renderStepFraction);
|
||||
*out = blended;
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//##########################################################################
|
||||
// Renderer Support
|
||||
//
|
||||
|
||||
@@ -141,6 +141,69 @@ public:
|
||||
int damageZoneCount;
|
||||
DamageZone **damageZones;
|
||||
|
||||
//######################################################################
|
||||
//################### Render interpolation #########################
|
||||
//######################################################################
|
||||
//
|
||||
// The simulation advances in whole fixed steps (RP412PHYSICSHZ) and the
|
||||
// renderer draws whenever it can, so at any frame rate that is not the
|
||||
// step rate the drawn position only changes 50 times a second and is
|
||||
// held for however many frames fall inside a step. That is visible as
|
||||
// stepping, and it gets WORSE the faster the machine: at 240 fps each
|
||||
// position is held for nearly five frames.
|
||||
//
|
||||
// So drawing interpolates. renderPreviousOrigin is this entity's origin
|
||||
// at the START of the step it is currently in, snapshotted by
|
||||
// Mover::BeginStep, and renderStepFraction is how far through that step
|
||||
// the frame being drawn falls. GetRenderToWorld blends the two.
|
||||
//
|
||||
// RENDER ONLY. localOrigin and localToWorld are untouched, so physics,
|
||||
// collision, scoring, the nav map's entity queries and the network
|
||||
// update records all keep seeing exact stepped values - which is what
|
||||
// keeps the simulation identical on every machine at every frame rate.
|
||||
// RP412PHYSTRACE is the proof of that and must not move.
|
||||
//
|
||||
// The picture therefore trails the simulation by up to one step (20 ms
|
||||
// at 50 Hz), which is the standard price and much the lesser evil:
|
||||
// extrapolating FORWARD instead has to guess, and overshoots into
|
||||
// shimmer every time the guess is corrected.
|
||||
//
|
||||
// A teleport must not be smoothed - sliding a pod 300 metres across the
|
||||
// map over 20 ms would be far worse than the cut it replaces. That
|
||||
// falls out for free: VTV::BeginStep applies a scheduled respawn and
|
||||
// THEN calls Mover::BeginStep, so the snapshot is taken after the
|
||||
// teleport and the blend has nothing to travel.
|
||||
//
|
||||
Origin renderPreviousOrigin;
|
||||
Scalar renderStepFraction;
|
||||
|
||||
//
|
||||
// Whether renderPreviousOrigin describes a step this entity actually
|
||||
// took. It has to be asked separately from the fraction, because a
|
||||
// fraction of zero is a perfectly ordinary place to be drawing - see
|
||||
// GetRenderToWorld for the one-step jump that testing the fraction
|
||||
// instead used to produce.
|
||||
//
|
||||
Logical renderStepTaken;
|
||||
|
||||
//
|
||||
// The transform to DRAW with. Falls back to localToWorld verbatim when
|
||||
// interpolation is off, when there is no fixed step to interpolate
|
||||
// within, or before the first snapshot exists - so the unfixed-step
|
||||
// path behaves exactly as it always did.
|
||||
//
|
||||
void
|
||||
GetRenderToWorld(LinearMatrix *out);
|
||||
|
||||
//
|
||||
// Filled by Simulation::PerformTo around each fixed step, for locally
|
||||
// simulated entities and replicants alike.
|
||||
//
|
||||
void
|
||||
SnapshotRenderOrigin();
|
||||
void
|
||||
SetRenderStepFraction(Scalar fraction);
|
||||
|
||||
|
||||
int
|
||||
GetDamageZoneIndex(const CString &damage_zone_name) const;
|
||||
|
||||
@@ -690,6 +690,23 @@ void
|
||||
DEBUG_STREAM << "." << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// The rate this gauge runs at, and which tier that puts it in. The
|
||||
// renderer walks a sixteen-step wheel and a gauge draws only on the
|
||||
// steps its rate names, so tier 4 is one turn of the wheel between
|
||||
// redraws - seconds, once a race has the passes down to a handful a
|
||||
// second. Without this the profile says how EXPENSIVE each gauge is
|
||||
// but not how RARELY it runs, and the second one is what makes a
|
||||
// display look stuck.
|
||||
//
|
||||
{
|
||||
char
|
||||
rate_buffer[32];
|
||||
|
||||
sprintf(rate_buffer, "%04x/t%d ", (unsigned) rate, DiscernTier());
|
||||
DEBUG_STREAM << rate_buffer << std::flush;
|
||||
}
|
||||
|
||||
if (profileCycles > 0)
|
||||
{
|
||||
Scalar
|
||||
|
||||
+100
-1
@@ -21,6 +21,31 @@
|
||||
BitTrace Gauge_Renderer("Gauge Renderer");
|
||||
#endif
|
||||
|
||||
//
|
||||
// How long a single background pass may spend drawing gauges, in
|
||||
// milliseconds. RP412GAUGESLICE tunes it; 0 restores the original
|
||||
// behaviour of exactly one gauge per pass.
|
||||
//
|
||||
static long
|
||||
GaugeSliceMs()
|
||||
{
|
||||
static long
|
||||
slice = -1L;
|
||||
|
||||
if (slice < 0L)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412GAUGESLICE");
|
||||
|
||||
slice = (setting != NULL) ? atol(setting) : 2L;
|
||||
if (slice < 0L)
|
||||
{
|
||||
slice = 0L;
|
||||
}
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
|
||||
//#######################################################################
|
||||
// Miscellaneous utilities
|
||||
//#######################################################################
|
||||
@@ -3672,6 +3697,60 @@ Logical
|
||||
Logical
|
||||
result;
|
||||
|
||||
//
|
||||
// RP412GAUGEPROFILE=<seconds> - dump the gauge profile on that
|
||||
// cadence. Off unless set.
|
||||
//
|
||||
// ProfileReport already exists and PROFILE_GAUGES is already on, so
|
||||
// the numbers are being collected whether anyone looks or not. It was
|
||||
// only reachable from F11 through the RIO controls mapper, which is
|
||||
// not the mapper a desktop player is running - so on PAD;KEYBOARD it
|
||||
// could not be reached at all. This gives it a way out.
|
||||
//
|
||||
// It reports every gauge with its rate, its tier, how many times it
|
||||
// ran and what it cost, then clears - so each dump covers the
|
||||
// interval since the last one rather than all of history.
|
||||
//
|
||||
{
|
||||
static long
|
||||
profileInterval = -1L;
|
||||
|
||||
if (profileInterval < 0L)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412GAUGEPROFILE");
|
||||
|
||||
profileInterval = (setting != NULL) ? atol(setting) : 0L;
|
||||
if (profileInterval < 0L)
|
||||
{
|
||||
profileInterval = 0L;
|
||||
}
|
||||
}
|
||||
if (profileInterval > 0L)
|
||||
{
|
||||
static Logical
|
||||
profileScheduled = False;
|
||||
static Time
|
||||
profileDue;
|
||||
|
||||
Time
|
||||
profileNow = Now();
|
||||
|
||||
if (!profileScheduled)
|
||||
{
|
||||
profileScheduled = True;
|
||||
profileDue = profileNow;
|
||||
profileDue += profileInterval * 1000L;
|
||||
}
|
||||
else if (profileDue < profileNow)
|
||||
{
|
||||
profileDue = profileNow;
|
||||
profileDue += profileInterval * 1000L;
|
||||
ProfileReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Time start, end;
|
||||
int oldTaskMode = taskMode;
|
||||
|
||||
@@ -3683,7 +3762,27 @@ Logical
|
||||
|
||||
case background:
|
||||
{
|
||||
result = ProcessOneActiveGauge();
|
||||
//-----------------------------------------------------------
|
||||
// Draw gauges until the slice is spent, rather than exactly
|
||||
// one per pass.
|
||||
//
|
||||
// The background loop is only guaranteed a single pass per
|
||||
// frame; it gets more only while time remains before the
|
||||
// frame is due. On a busy map the 3D foreground eats the
|
||||
// whole budget, so a cycle of ninety-odd gauges takes
|
||||
// ninety-odd frames to come round and the displays sit
|
||||
// frozen for seconds. Working to a slice makes the refresh
|
||||
// rate depend on elapsed time instead of on how much spare
|
||||
// frame there happened to be.
|
||||
//-----------------------------------------------------------
|
||||
Time slice_end = Now();
|
||||
slice_end += GaugeSliceMs();
|
||||
|
||||
do
|
||||
{
|
||||
result = ProcessOneActiveGauge();
|
||||
}
|
||||
while (result && taskMode == background && Now() < slice_end);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "munga.h"
|
||||
#pragma hdrstop
|
||||
|
||||
#include "inputscript.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
//##########################################################################
|
||||
// RP412INPUTSCRIPT - see the header for what and why. This file is the
|
||||
// how: a timeline of rows parsed once, held in a fixed array, evaluated
|
||||
// by walking to the last row at or before the asked-for time.
|
||||
//##########################################################################
|
||||
|
||||
namespace
|
||||
{
|
||||
enum { inputScriptMaxRows = 256 };
|
||||
|
||||
struct InputScriptRow
|
||||
{
|
||||
float t;
|
||||
float throttle;
|
||||
float stickX;
|
||||
float stickY;
|
||||
float pedals;
|
||||
};
|
||||
|
||||
InputScriptRow gRows[inputScriptMaxRows];
|
||||
int gRowCount = 0;
|
||||
int gLoaded = -1; // -1 not tried, 0 no script, 1 loaded
|
||||
Logical gArmed = False;
|
||||
Time gOrigin;
|
||||
|
||||
float ClampInto(float value, float low, float high)
|
||||
{
|
||||
if (value < low) return low;
|
||||
if (value > high) return high;
|
||||
return value;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
gLoaded = 0;
|
||||
|
||||
const char *path = getenv("RP412INPUTSCRIPT");
|
||||
if (path == NULL || *path == '\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = fopen(path, "rt");
|
||||
if (file == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "InputScript: cannot read '" << path
|
||||
<< "' - driving unscripted\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
char line[256];
|
||||
float last_t = -1.0f;
|
||||
while (fgets(line, sizeof(line), file) != NULL &&
|
||||
gRowCount < inputScriptMaxRows)
|
||||
{
|
||||
InputScriptRow row;
|
||||
if (sscanf(line, " %f %f %f %f %f",
|
||||
&row.t, &row.throttle, &row.stickX,
|
||||
&row.stickY, &row.pedals) != 5)
|
||||
{
|
||||
continue; // comments, blanks, ragged lines
|
||||
}
|
||||
//
|
||||
// Clamped HERE, not at sample time, so a script asking for
|
||||
// throttle 2.0 is corrected once and visibly rather than
|
||||
// silently every step - and the mapper's own Verify range
|
||||
// checks can never trip on scripted input.
|
||||
//
|
||||
row.throttle = ClampInto(row.throttle, 0.0f, 1.0f);
|
||||
row.stickX = ClampInto(row.stickX, -1.0f, 1.0f);
|
||||
row.stickY = ClampInto(row.stickY, -1.0f, 1.0f);
|
||||
row.pedals = ClampInto(row.pedals, -1.0f, 1.0f);
|
||||
if (row.t < last_t)
|
||||
{
|
||||
DEBUG_STREAM << "InputScript: row at t=" << row.t
|
||||
<< " is out of order - dropped\n" << std::flush;
|
||||
continue;
|
||||
}
|
||||
last_t = row.t;
|
||||
gRows[gRowCount++] = row;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
if (gRowCount > 0)
|
||||
{
|
||||
gLoaded = 1;
|
||||
DEBUG_STREAM << "InputScript: '" << path << "', " << gRowCount
|
||||
<< " row(s), last at t=" << gRows[gRowCount - 1].t
|
||||
<< "s\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "InputScript: '" << path
|
||||
<< "' held no usable rows - driving unscripted\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
RPInputScript_Active()
|
||||
{
|
||||
if (gLoaded < 0)
|
||||
{
|
||||
Load();
|
||||
}
|
||||
return (gLoaded == 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
void
|
||||
RPInputScript_Arm(const Time &origin)
|
||||
{
|
||||
if (!RPInputScript_Active())
|
||||
{
|
||||
return;
|
||||
}
|
||||
gOrigin = origin;
|
||||
gArmed = True;
|
||||
DEBUG_STREAM << "InputScript: armed at the green light\n" << std::flush;
|
||||
}
|
||||
|
||||
int
|
||||
RPInputScript_Sample(
|
||||
const Time &now,
|
||||
float *throttle_out,
|
||||
float *stick_x_out,
|
||||
float *stick_y_out,
|
||||
float *pedals_out
|
||||
)
|
||||
{
|
||||
if (!gArmed || gLoaded != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Scalar t = now - gOrigin;
|
||||
if (t < (Scalar) 0)
|
||||
{
|
||||
t = (Scalar) 0;
|
||||
}
|
||||
|
||||
//
|
||||
// The last row at or before t holds; before the first row, neutral.
|
||||
// A linear walk, but the list is tiny and already ordered.
|
||||
//
|
||||
const InputScriptRow *current = NULL;
|
||||
for (int i = 0; i < gRowCount; ++i)
|
||||
{
|
||||
if (gRows[i].t <= (float) t)
|
||||
{
|
||||
current = &gRows[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (current == NULL)
|
||||
{
|
||||
*throttle_out = 0.0f;
|
||||
*stick_x_out = 0.0f;
|
||||
*stick_y_out = 0.0f;
|
||||
*pedals_out = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
*throttle_out = current->throttle;
|
||||
*stick_x_out = current->stickX;
|
||||
*stick_y_out = current->stickY;
|
||||
*pedals_out = current->pedals;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
//##########################################################################
|
||||
// RP412INPUTSCRIPT - scripted analog input, on the simulation's clock.
|
||||
//
|
||||
// A race cannot be called deterministic until somebody DRIVES it, and a
|
||||
// human cannot drive the same lap twice. This feeds the four analog
|
||||
// channels the controls mapper interprets - throttle, stick X/Y, pedals -
|
||||
// from a timeline file instead, evaluated against the mapper's own step
|
||||
// clock, so the same script produces the same race at any frame rate.
|
||||
//
|
||||
// The file named by RP412INPUTSCRIPT= holds one row per change:
|
||||
//
|
||||
// # t throttle stickX stickY pedals
|
||||
// 0.0 0.0 0 0 0
|
||||
// 2.0 1.0 0 0 0
|
||||
// 6.0 1.0 0.5 0 0
|
||||
//
|
||||
// Times are seconds of SIMULATION time from the green light. Each row
|
||||
// HOLDS until the next row's time - a step function, no interpolation,
|
||||
// because interpolation would sample differently at different physics
|
||||
// rates and the whole point is that nothing does.
|
||||
//
|
||||
// Armed by the green-light anchor in Application::ExecuteForeground (the
|
||||
// same instant RP412PHYSTRACE stops the pod dead), so the script clock,
|
||||
// the trace clock and the vehicle's state all start together.
|
||||
//
|
||||
// Test harness: off unless the environment names a file, costs nothing
|
||||
// when off, and it would be a cheat in a real race.
|
||||
//##########################################################################
|
||||
|
||||
class Time;
|
||||
|
||||
// is a script named and readable? (parsed once, on first ask)
|
||||
int
|
||||
RPInputScript_Active();
|
||||
|
||||
// the green light: script time zero is this instant
|
||||
void
|
||||
RPInputScript_Arm(const Time &origin);
|
||||
|
||||
// evaluate at 'now' (a simulation clock, normally GetLastPerformance()).
|
||||
// Returns 0 before Arm or with no script - callers leave their own
|
||||
// values alone. Outputs are clamped to the mapper's legal ranges.
|
||||
int
|
||||
RPInputScript_Sample(
|
||||
const Time &now,
|
||||
float *throttle_out, // 0..1
|
||||
float *stick_x_out, // -1..1
|
||||
float *stick_y_out, // -1..1
|
||||
float *pedals_out // -1..1
|
||||
);
|
||||
+13
-1
@@ -411,8 +411,20 @@ void
|
||||
// supposed to
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Doorframes are exempt: they are clockwork, computed identically on
|
||||
// every machine from the mission clock, so each host builds its own
|
||||
// Hermit copy instead of one host owning it and replicating. That
|
||||
// also means they survive a peer dropping, which owned doors do not -
|
||||
// ownership transfer is not implemented. Note this changes how many
|
||||
// times the cursor below is advanced, so old and new builds deal the
|
||||
// remaining map entities differently: they cannot share a session.
|
||||
//
|
||||
Logical post_make_message = True;
|
||||
if (Entity::EntityFlagsIsMap(message->instanceFlags))
|
||||
if (
|
||||
Entity::EntityFlagsIsMap(message->instanceFlags)
|
||||
&& message->classToCreate != DoorFrameClassID
|
||||
)
|
||||
{
|
||||
Check(application);
|
||||
HostManager *host_manager = application->GetHostManager();
|
||||
|
||||
+653
-5
@@ -12,6 +12,117 @@
|
||||
#include "app.h"
|
||||
#include "notation.h"
|
||||
|
||||
//
|
||||
// The blend fraction the last dead-reckoned step used, and whether it
|
||||
// blended at all rather than snapping. Only read by the RP412CAMLOG trace
|
||||
// in Mover::DeadReckon, which needs them from the branch that computes
|
||||
// them a few lines earlier.
|
||||
//
|
||||
static Logical gLastLerpUsed = False;
|
||||
static Scalar gLastPercent = 0.0f;
|
||||
|
||||
//
|
||||
// The one replicant the RP412CAMLOG traces describe. Latched here because
|
||||
// the renderer reports on the same entity from the other end - what its
|
||||
// motion looks like on screen - and two traces about two different pods
|
||||
// would compare nothing.
|
||||
//
|
||||
static EntityID gTracedEntity = EntityID::Null;
|
||||
static Logical gTracedLatched = False;
|
||||
|
||||
EntityID
|
||||
MoverTracedEntity()
|
||||
{
|
||||
return gTracedEntity;
|
||||
}
|
||||
|
||||
//
|
||||
// Prediction-error totals for the RP412CAMLOG trace. Shared across
|
||||
// replicants deliberately: the question - does constant-velocity
|
||||
// extrapolation hold over one send interval - is about the model, not
|
||||
// about any one pod, so a whole grid contributing samples is a better
|
||||
// answer rather than a muddled one.
|
||||
//
|
||||
static int gPredictSamples = 0;
|
||||
static Scalar gPredictAlong = 0.0f;
|
||||
static Scalar gPredictAlongAbs = 0.0f;
|
||||
static Scalar gPredictAcross = 0.0f;
|
||||
static Scalar gPredictMilliseconds = 0.0f;
|
||||
static Scalar gPredictNextSay = 0.0f;
|
||||
|
||||
//
|
||||
// Bounds on the replication interval estimate, in seconds.
|
||||
//
|
||||
// The first pair decide what is allowed into the sample window at all: a
|
||||
// non-positive gap is a duplicate or a reordered packet, and a multi-second
|
||||
// one is a join, a pause or a stall. Neither says anything about the rate
|
||||
// the sender is actually keeping.
|
||||
//
|
||||
// The second pair are a backstop on the answer, set deliberately wide so
|
||||
// that in every sane case the median decides it and these never bind.
|
||||
//
|
||||
// Measured send rate on a live connection is about 30ms, so half a second
|
||||
// is already sixteen times slower than anything healthy.
|
||||
//
|
||||
static const Scalar kMinimumUpdateInterval = 0.001f;
|
||||
static const Scalar kOutlierUpdateInterval = 0.5f;
|
||||
static const Scalar kMinimumPredictedInterval = 0.010f;
|
||||
|
||||
//
|
||||
// Never predict further ahead than this, which puts a floor under the
|
||||
// dead reckoner's blend fraction: at a 20ms step the worst case becomes
|
||||
// 0.02/(0.25+0.02), near enough 7% of the gap per step, so a pod still
|
||||
// converges on its projection in a dozen steps instead of crawling.
|
||||
//
|
||||
static const Scalar kMaximumPredictedInterval = 0.25f;
|
||||
|
||||
//
|
||||
// A gap this long is a stall, not jitter - six times the observed rate.
|
||||
// A gap this short cannot be a sender keeping to 30ms, so it is a packet
|
||||
// that was already waiting when we finally got round to reading it.
|
||||
//
|
||||
static const Scalar kLongGapThreshold = 0.200f;
|
||||
static const Scalar kQueuedGapThreshold = 0.005f;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// RP412NETPREDICT=0 restores the original single-sample prediction, so the
|
||||
// two can be compared on the same build and the same connection.
|
||||
//
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// Every path out of PredictUpdateInterval goes through here. It used not
|
||||
// to, and the one that skipped it was the bug.
|
||||
//
|
||||
static Scalar
|
||||
ClampPredictedInterval(Scalar interval)
|
||||
{
|
||||
if (interval < kMinimumPredictedInterval)
|
||||
{
|
||||
return kMinimumPredictedInterval;
|
||||
}
|
||||
if (interval > kMaximumPredictedInterval)
|
||||
{
|
||||
return kMaximumPredictedInterval;
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
static Logical
|
||||
UseMedianPrediction()
|
||||
{
|
||||
static int cached = -1;
|
||||
|
||||
if (cached < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412NETPREDICT");
|
||||
cached = (setting && *setting == '0') ? 0 : 1;
|
||||
}
|
||||
return cached ? True : False;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//############################### Mover #################################
|
||||
//#############################################################################
|
||||
@@ -513,6 +624,10 @@ void
|
||||
Scalar percent =
|
||||
time_slice / ((nextUpdate - lastPerformance) + time_slice);
|
||||
|
||||
// for the RP412CAMLOG trace at the end of this function
|
||||
gLastLerpUsed = True;
|
||||
gLastPercent = percent;
|
||||
|
||||
//
|
||||
//------------------------------------------
|
||||
// Do a spherical lerp on the angular motion
|
||||
@@ -562,6 +677,7 @@ void
|
||||
}
|
||||
else
|
||||
{
|
||||
gLastLerpUsed = False; // snapped, not blended
|
||||
localOrigin = projectedOrigin;
|
||||
worldLinearVelocity = projectedVelocity.linearMotion;
|
||||
localVelocity.angularMotion = projectedVelocity.angularMotion;
|
||||
@@ -581,6 +697,219 @@ void
|
||||
localToWorld = localOrigin;
|
||||
}
|
||||
UpdateLocalMotion();
|
||||
|
||||
//
|
||||
// RP412CAMLOG: is a replicant's motion actually uniform?
|
||||
//
|
||||
// Measured HERE, in the replicant's own step, and nowhere else.
|
||||
// Every previous attempt at this question sampled from another
|
||||
// clock - the camera's step grid, or an arriving packet's
|
||||
// timestamp - and two independent clocks alias against each other
|
||||
// whatever the game is doing, so those numbers could never
|
||||
// separate a real hitch from the measurement's own beat. This one
|
||||
// has a single frame of reference: consecutive steps of the entity
|
||||
// being asked about.
|
||||
//
|
||||
// percent is the whole mechanism above: it is how far this step
|
||||
// moves toward the projected position, and it depends on
|
||||
// nextUpdate being a decent guess at when the next packet lands.
|
||||
// If that guess is poor the fraction swings, and swinging fraction
|
||||
// is uneven motion no matter how clean the packets were.
|
||||
//
|
||||
// One entity only - the first replicant seen - because these
|
||||
// counters are shared and a grid full of pods would blend into
|
||||
// noise.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
if (!gTracedLatched)
|
||||
{
|
||||
gTracedLatched = True;
|
||||
gTracedEntity = GetEntityID();
|
||||
}
|
||||
if (gTracedEntity == GetEntityID())
|
||||
{
|
||||
static Scalar next_say = 0.0f;
|
||||
static Point3D last_pos(0.0f, 0.0f, 0.0f);
|
||||
static Logical have_last = False;
|
||||
static int steps = 0;
|
||||
static int spikes = 0;
|
||||
static int stalls = 0;
|
||||
static int lerped = 0;
|
||||
static Scalar mean_step = 0.0f;
|
||||
static Scalar min_percent = 1.0f;
|
||||
static Scalar max_percent = 0.0f;
|
||||
static Scalar worst_error = 0.0f;
|
||||
static Scalar last_distance = 0.0f;
|
||||
static Scalar recent[16];
|
||||
static Scalar frozen[16];
|
||||
static int recent_next = 0;
|
||||
static int recent_count = 0;
|
||||
static Logical captured = False;
|
||||
static Scalar captured_ratio = 0.0f;
|
||||
static Scalar captured_percent = 0.0f;
|
||||
static int seq_stalls = 0;
|
||||
static int seq_spikes = 0;
|
||||
|
||||
++steps;
|
||||
if (have_last)
|
||||
{
|
||||
Vector3D moved;
|
||||
moved.Subtract(localOrigin.linearPosition, last_pos);
|
||||
Scalar distance = moved.Length();
|
||||
|
||||
//
|
||||
// The same test the renderer applies to drawn frames:
|
||||
// this step against the one before it, not against a
|
||||
// running mean.
|
||||
//
|
||||
// A running mean is blind to an alternating pattern -
|
||||
// high, low, high, low averages to the mean and nothing
|
||||
// ever looks anomalous - which is why this trace has
|
||||
// been reporting zero spikes and zero stalls while the
|
||||
// renderer, comparing consecutive frames, counted
|
||||
// fifteen to forty-six stalls in the same motion. The
|
||||
// mean test only ever ruled out DRIFT.
|
||||
//
|
||||
if (distance < 50.0f)
|
||||
{
|
||||
//
|
||||
// Keep the last sixteen steps rolling, and freeze a
|
||||
// copy the moment a stall is seen.
|
||||
//
|
||||
// The first version of this printed the first twelve
|
||||
// steps of each window and they came back immaculate
|
||||
// - 1.029, 1.031, 1.032, monotonic to a tenth of a
|
||||
// percent - while the same window counted sixteen
|
||||
// stalls among the other two hundred and thirty
|
||||
// nine. Sampling a calm quarter second says nothing
|
||||
// about a tick that happens elsewhere. The sample
|
||||
// has to be triggered BY the event.
|
||||
//
|
||||
recent[recent_next] = distance;
|
||||
recent_next = (recent_next + 1) % 16;
|
||||
if (recent_count < 16) { recent_count++; }
|
||||
|
||||
if (last_distance > 0.001f)
|
||||
{
|
||||
Scalar sequential = distance / last_distance;
|
||||
|
||||
if (sequential < 0.4f)
|
||||
{
|
||||
++seq_stalls;
|
||||
|
||||
if (!captured && recent_count == 16)
|
||||
{
|
||||
captured = True;
|
||||
captured_ratio = sequential;
|
||||
captured_percent = gLastPercent;
|
||||
for (int c = 0; c < 16; c++)
|
||||
{
|
||||
frozen[c] = recent[(recent_next + c) % 16];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (sequential > 2.5f) { ++seq_spikes; }
|
||||
}
|
||||
last_distance = distance;
|
||||
}
|
||||
|
||||
if (distance > 50.0f)
|
||||
{
|
||||
mean_step = 0.0f; // respawn, not motion
|
||||
}
|
||||
else if (mean_step > 0.01f)
|
||||
{
|
||||
Scalar ratio = distance / mean_step;
|
||||
if (ratio > 2.5f) { ++spikes; }
|
||||
else if (ratio < 0.4f) { ++stalls; }
|
||||
mean_step = mean_step * 0.9f + distance * 0.1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
mean_step = distance;
|
||||
}
|
||||
}
|
||||
last_pos = localOrigin.linearPosition;
|
||||
have_last = True;
|
||||
|
||||
if ((Scalar) Now() >= next_say)
|
||||
{
|
||||
if (next_say > 0.0f)
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: replicant motion - " << steps
|
||||
<< " own steps, " << spikes << " spike(s), "
|
||||
<< stalls << " stall(s), " << lerped
|
||||
<< " lerped, percent " << min_percent << ".."
|
||||
<< max_percent << ", mean step " << mean_step
|
||||
<< "m, predicting " << predictedInterval
|
||||
<< "s worst miss " << worst_error << "s\n"
|
||||
<< std::flush;
|
||||
|
||||
DEBUG_STREAM << "CamLog: replicant sequence - "
|
||||
<< seq_stalls << " stall(s), " << seq_spikes
|
||||
<< " spike(s) against the PREVIOUS step";
|
||||
|
||||
if (captured)
|
||||
{
|
||||
//
|
||||
// The fifteen steps leading into a stall and the
|
||||
// stall itself, last in the list.
|
||||
//
|
||||
DEBUG_STREAM << "; at a stall (ratio "
|
||||
<< captured_ratio << ", percent "
|
||||
<< captured_percent << "):";
|
||||
for (int s = 0; s < 16; s++)
|
||||
{
|
||||
DEBUG_STREAM << " " << frozen[s];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "; no stall caught this window";
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
|
||||
DEBUG_STREAM << "CamLog: replicant arrivals - widest gap "
|
||||
<< widestGap << "s, " << longGapCount
|
||||
<< " long, " << queuedGapCount
|
||||
<< " queued ("
|
||||
<< ((longGapCount > 0 && queuedGapCount > 0)
|
||||
? "our loop stalled"
|
||||
: (longGapCount > 0
|
||||
? "sender went quiet"
|
||||
: "clean"))
|
||||
<< ")\n" << std::flush;
|
||||
}
|
||||
widestGap = 0.0f;
|
||||
longGapCount = 0;
|
||||
queuedGapCount = 0;
|
||||
next_say = ((Scalar) Now()) + 5.0f;
|
||||
steps = 0;
|
||||
spikes = 0;
|
||||
stalls = 0;
|
||||
lerped = 0;
|
||||
min_percent = 1.0f;
|
||||
max_percent = 0.0f;
|
||||
worst_error = 0.0f;
|
||||
captured = False;
|
||||
seq_stalls = 0;
|
||||
seq_spikes = 0;
|
||||
}
|
||||
{
|
||||
Scalar missed =
|
||||
(predictionError < 0.0f) ? -predictionError : predictionError;
|
||||
|
||||
if (missed > worst_error) { worst_error = missed; }
|
||||
}
|
||||
if (gLastLerpUsed)
|
||||
{
|
||||
++lerped;
|
||||
if (gLastPercent < min_percent) { min_percent = gLastPercent; }
|
||||
if (gLastPercent > max_percent) { max_percent = gLastPercent; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -658,9 +987,14 @@ Bye_Bye:
|
||||
//
|
||||
//-----------------------------------------------
|
||||
// Make sure the position quaternion stays stable
|
||||
//
|
||||
// Frame-counting, so it only runs on the frame-coupled path - fixed
|
||||
// steps do the same thing in BeginStep, counted in STEPS, because
|
||||
// "every 20 frames" lands at a different point of the step sequence
|
||||
// on every machine and rounding at different points is drift.
|
||||
//-----------------------------------------------
|
||||
//
|
||||
if (++normalizeCount == 20)
|
||||
if (Simulation::FixedStep() <= (Scalar) 0 && ++normalizeCount >= 20)
|
||||
{
|
||||
localOrigin.angularPosition.Normalize();
|
||||
normalizeCount = 0;
|
||||
@@ -668,6 +1002,133 @@ Bye_Bye:
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// The per-STEP set-up. This is the same work Mover::PerformAndWatch does
|
||||
// once per frame above - and once per frame is exactly wrong under fixed
|
||||
// stepping: the thrusters ADD their forces into localAcceleration every
|
||||
// step, so an accumulator cleared per frame carries step one's thrust
|
||||
// into step two whenever a frame holds two steps. How many steps a frame
|
||||
// holds depends on wall-clock jitter, which made identical runs diverge
|
||||
// by a quarter of a metre while sitting still on the pad.
|
||||
//
|
||||
// Idempotent on purpose: the frame-level copy still runs first on every
|
||||
// path, and repeating this at each step start is a recompute from
|
||||
// current state, not an accumulation.
|
||||
//
|
||||
void
|
||||
Mover::BeginStep()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
localVelocity.linearMotion.MultiplyByInverse(
|
||||
worldLinearVelocity,
|
||||
localToWorld
|
||||
);
|
||||
localAcceleration = Motion::Identity;
|
||||
previousOrigin = localOrigin;
|
||||
|
||||
if (++normalizeCount >= 20)
|
||||
{
|
||||
localOrigin.angularPosition.Normalize();
|
||||
normalizeCount = 0;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
Mover::ResetUpdateIntervals()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
updateIntervalCount = 0;
|
||||
updateIntervalWrite = 0;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// Estimate how long until the next update for this entity arrives.
|
||||
//
|
||||
// This is not a cosmetic guess. DeadReckon blends toward the projected
|
||||
// origin by
|
||||
//
|
||||
// percent = time_slice / ((nextUpdate - lastPerformance) + time_slice)
|
||||
//
|
||||
// so the prediction sets how far every single step moves. The original code
|
||||
// predicted the next gap from the one previous gap. On a LAN that was fine,
|
||||
// because the gaps were all alike. Over the internet a late packet doubles
|
||||
// the prediction, percent collapses toward zero, the entity barely advances
|
||||
// for a step and then catches up on the following ones - which is a visible
|
||||
// tick. Measured on a live Steam connection at about 1.4 a second, with
|
||||
// percent bottoming out at 0.014 against a normal range of 0.27 to 0.95.
|
||||
//
|
||||
// A median has a breakdown point of half its samples, so one straggler - or
|
||||
// three - moves it not at all, while a real change in the send rate still
|
||||
// carries it within a few updates. That is the whole trick: ignore the
|
||||
// outlier, follow the trend.
|
||||
//
|
||||
Scalar
|
||||
Mover::PredictUpdateInterval(Scalar latest)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
// Only plausible gaps go into the window. Letting a join or a stall in
|
||||
// would poison the estimate for the next eight updates - precisely when
|
||||
// the entity is most conspicuous, just after it appears.
|
||||
//
|
||||
if (latest > kMinimumUpdateInterval && latest < kOutlierUpdateInterval)
|
||||
{
|
||||
updateIntervals[updateIntervalWrite] = latest;
|
||||
updateIntervalWrite = (updateIntervalWrite + 1) % UpdateIntervalSamples;
|
||||
if (updateIntervalCount < UpdateIntervalSamples)
|
||||
{
|
||||
updateIntervalCount++;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Too few samples to hold an opinion. Fall back to the gap we just saw
|
||||
// rather than inventing a rate we have no evidence for - but clamp it
|
||||
// like any other answer. Leaving this path unclamped let a 2.05s gap
|
||||
// through in the first updates after an entity appeared, which drove
|
||||
// the blend fraction to 0.0097 and stalled the step. That is every
|
||||
// respawn, and it is exactly when the pod is being watched.
|
||||
//
|
||||
if (updateIntervalCount < 3)
|
||||
{
|
||||
return ClampPredictedInterval(latest);
|
||||
}
|
||||
|
||||
//
|
||||
// Insertion sort - the window is eight samples, and this runs once per
|
||||
// arriving packet per entity.
|
||||
//
|
||||
Scalar sorted[UpdateIntervalSamples];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < updateIntervalCount; i++)
|
||||
{
|
||||
sorted[i] = updateIntervals[i];
|
||||
}
|
||||
for (i = 1; i < updateIntervalCount; i++)
|
||||
{
|
||||
Scalar value = sorted[i];
|
||||
int j = i - 1;
|
||||
|
||||
while (j >= 0 && sorted[j] > value)
|
||||
{
|
||||
sorted[j + 1] = sorted[j];
|
||||
j--;
|
||||
}
|
||||
sorted[j + 1] = value;
|
||||
}
|
||||
|
||||
return ClampPredictedInterval(sorted[updateIntervalCount / 2]);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
@@ -681,15 +1142,160 @@ void
|
||||
{
|
||||
|
||||
//
|
||||
//-------------------------------------------
|
||||
// HACK - precalculation for next update time
|
||||
//-------------------------------------------
|
||||
//---------------------------------------
|
||||
// Precalculation for next update time
|
||||
//---------------------------------------
|
||||
//
|
||||
nextUpdate = Now();
|
||||
Scalar diff = nextUpdate - lastUpdate;
|
||||
Scalar anchorInterval = (Scalar) 0;
|
||||
if (diff < 10.0f)
|
||||
{
|
||||
nextUpdate.ticks += nextUpdate.ticks - lastUpdate.ticks;
|
||||
if (UseMedianPrediction())
|
||||
{
|
||||
Scalar predicted = PredictUpdateInterval(diff);
|
||||
|
||||
//
|
||||
// Anchor the projection to the SENDER's timeline, below,
|
||||
// once Entity::ReadUpdateRecord has moved lastUpdate to
|
||||
// the sampling moment RP412NETCLOCK worked out.
|
||||
//
|
||||
anchorInterval = predicted;
|
||||
|
||||
//
|
||||
// Score the previous prediction against the gap that
|
||||
// actually just elapsed - a true one-step-ahead error,
|
||||
// kept per entity so a trace reads the entity it is
|
||||
// watching and not whichever one updated last.
|
||||
//
|
||||
if (predictedInterval > 0.0f)
|
||||
{
|
||||
predictionError = predictedInterval - diff;
|
||||
}
|
||||
predictedInterval = predicted;
|
||||
|
||||
//
|
||||
// Arrival statistics, for telling a quiet sender from
|
||||
// our own stalled loop. See the members.
|
||||
//
|
||||
if (diff > widestGap) { widestGap = diff; }
|
||||
if (diff > kLongGapThreshold) { longGapCount++; }
|
||||
if (diff < kQueuedGapThreshold) { queuedGapCount++; }
|
||||
}
|
||||
else
|
||||
{
|
||||
nextUpdate.ticks += nextUpdate.ticks - lastUpdate.ticks;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// The stream was interrupted - a join, a pause, a long
|
||||
// stall. Nothing recorded before it describes the rate
|
||||
// now, so start the window over.
|
||||
//
|
||||
ResetUpdateIntervals();
|
||||
}
|
||||
|
||||
//
|
||||
// RP412CAMLOG: is constant-velocity extrapolation actually
|
||||
// accurate over one interval, or is the pod manoeuvring?
|
||||
//
|
||||
// The corrections measured 0.25 to 0.66m against a step of
|
||||
// about a metre, which is what collapses one step to a third
|
||||
// and shows as the tick. At 52 m/s half a metre is ten
|
||||
// milliseconds of travel, so the question is whether we are
|
||||
// evaluating the projection at the wrong INSTANT or whether the
|
||||
// pod simply is not going in a straight line.
|
||||
//
|
||||
// This settles it without involving any clock we do not trust:
|
||||
// take the position and velocity the sender reported last time,
|
||||
// carry them forward by the difference between the two SENDER
|
||||
// timestamps, and compare against the position the sender
|
||||
// reports now. Both stamps come from the same machine, so
|
||||
// latency, clock offset and RP412NETCLOCK play no part - it
|
||||
// measures the prediction and nothing else.
|
||||
//
|
||||
// Split the error along the direction of travel and across it.
|
||||
// Error ALONG the path is time: divided by speed it IS the
|
||||
// number of milliseconds the window is out by, and its sign
|
||||
// says which way. Error ACROSS the path cannot be a timing
|
||||
// problem at all - that is a pod turning, and no clock fix
|
||||
// would touch it.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
UpdateRecord *sample = (UpdateRecord*)record;
|
||||
|
||||
if (haveSenderSample)
|
||||
{
|
||||
Scalar dt = sample->timeStamp - senderStamp;
|
||||
Scalar speed = senderVelocity.Length();
|
||||
|
||||
if (dt > 0.001f && dt < 1.0f && speed > 1.0f)
|
||||
{
|
||||
Vector3D error;
|
||||
error.x = sample->localOrigin.linearPosition.x
|
||||
- (senderPosition.x + senderVelocity.x * dt);
|
||||
error.y = sample->localOrigin.linearPosition.y
|
||||
- (senderPosition.y + senderVelocity.y * dt);
|
||||
error.z = sample->localOrigin.linearPosition.z
|
||||
- (senderPosition.z + senderVelocity.z * dt);
|
||||
|
||||
Scalar along =
|
||||
(error.x * senderVelocity.x
|
||||
+ error.y * senderVelocity.y
|
||||
+ error.z * senderVelocity.z) / speed;
|
||||
|
||||
Vector3D across;
|
||||
across.x = error.x - (senderVelocity.x / speed) * along;
|
||||
across.y = error.y - (senderVelocity.y / speed) * along;
|
||||
across.z = error.z - (senderVelocity.z / speed) * along;
|
||||
|
||||
gPredictSamples++;
|
||||
gPredictAlong += along;
|
||||
gPredictAlongAbs += (along < 0.0f) ? -along : along;
|
||||
gPredictAcross += across.Length();
|
||||
gPredictMilliseconds += (along / speed) * 1000.0f;
|
||||
|
||||
Scalar now_say = (Scalar) Now();
|
||||
|
||||
if (now_say >= gPredictNextSay)
|
||||
{
|
||||
if (gPredictNextSay > 0.0f && gPredictSamples > 0)
|
||||
{
|
||||
Scalar mean_along = gPredictAlong / gPredictSamples;
|
||||
Scalar mean_across = gPredictAcross / gPredictSamples;
|
||||
Scalar mean_ms =
|
||||
gPredictMilliseconds / gPredictSamples;
|
||||
|
||||
DEBUG_STREAM << "CamLog: prediction error - "
|
||||
<< gPredictSamples << " intervals, along "
|
||||
<< mean_along << "m (" << mean_ms
|
||||
<< "ms of travel), across " << mean_across
|
||||
<< "m, verdict "
|
||||
<< (((mean_along < 0.0f ? -mean_along : mean_along)
|
||||
> mean_across * 2.0f)
|
||||
? "TIMING - the window is off"
|
||||
: ((mean_across
|
||||
> (mean_along < 0.0f ? -mean_along : mean_along) * 2.0f)
|
||||
? "MANOEUVRE - the pod is turning"
|
||||
: "mixed"))
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
gPredictNextSay = now_say + 5.0f;
|
||||
gPredictSamples = 0;
|
||||
gPredictAlong = 0.0f;
|
||||
gPredictAlongAbs = 0.0f;
|
||||
gPredictAcross = 0.0f;
|
||||
gPredictMilliseconds = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
senderStamp = sample->timeStamp;
|
||||
senderPosition = sample->localOrigin.linearPosition;
|
||||
senderVelocity = sample->worldLinearVelocity;
|
||||
haveSenderSample = True;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -699,6 +1305,35 @@ void
|
||||
//
|
||||
Entity::ReadUpdateRecord(record);
|
||||
|
||||
//
|
||||
// Put the projection deadline on the SENDER's timeline.
|
||||
//
|
||||
// The dead reckoner projects to updateOrigin + velocity *
|
||||
// (nextUpdate - lastUpdate), so that difference is a DISTANCE
|
||||
// once multiplied by speed - and a pod at 52 m/s turns every
|
||||
// millisecond in it into 52mm of target.
|
||||
//
|
||||
// lastUpdate is the sampling moment RP412NETCLOCK computed, on
|
||||
// the sender's clock. Setting nextUpdate from Now() measured the
|
||||
// gap between two different timelines, so it came out as the
|
||||
// interval PLUS however late this particular packet happened to
|
||||
// be. Fifteen milliseconds of ordinary jitter became three
|
||||
// quarters of a metre of target error, which is enough to
|
||||
// collapse a one metre step to a third - and only on the packets
|
||||
// that ran late, which is exactly the intermittent tick that was
|
||||
// reported.
|
||||
//
|
||||
// Anchored to lastUpdate the difference is the predicted
|
||||
// interval exactly, so the target depends on what the sender
|
||||
// said and how fast it is going, and not at all on the route the
|
||||
// packet took to reach us.
|
||||
//
|
||||
if (anchorInterval > (Scalar) 0)
|
||||
{
|
||||
nextUpdate = lastUpdate;
|
||||
nextUpdate += anchorInterval;
|
||||
}
|
||||
|
||||
//
|
||||
//-----------------------
|
||||
// Update the motion data
|
||||
@@ -1708,6 +2343,19 @@ Mover::Mover(
|
||||
updateAcceleration.angularMotion = localAcceleration.linearMotion;
|
||||
nextUpdate = lastUpdate;
|
||||
|
||||
ResetUpdateIntervals();
|
||||
predictedInterval = 0.0f;
|
||||
predictionError = 0.0f;
|
||||
widestGap = 0.0f;
|
||||
longGapCount = 0;
|
||||
queuedGapCount = 0;
|
||||
haveSenderSample = False;
|
||||
senderStamp = lastUpdate;
|
||||
senderPosition = localOrigin.linearPosition;
|
||||
senderVelocity.x = 0.0f;
|
||||
senderVelocity.y = 0.0f;
|
||||
senderVelocity.z = 0.0f;
|
||||
|
||||
normalizeCount = 0;
|
||||
if (IsInitialStasis())
|
||||
{
|
||||
|
||||
@@ -265,6 +265,14 @@ protected:
|
||||
MemoryStream *update_stream
|
||||
);
|
||||
|
||||
//
|
||||
// Per-step set-up under fixed stepping: clears the force accumulator
|
||||
// the thrusters add into, so each step integrates only its own
|
||||
// forces. See the definition for the frame-jitter bug this closes.
|
||||
//
|
||||
void
|
||||
BeginStep();
|
||||
|
||||
int
|
||||
normalizeCount;
|
||||
Environment
|
||||
@@ -281,6 +289,61 @@ protected:
|
||||
Time
|
||||
nextUpdate;
|
||||
|
||||
//
|
||||
// Recent gaps between replication updates for this entity, as a ring,
|
||||
// and the running estimate drawn from them. The original code predicted
|
||||
// the next gap from the single previous gap; see PredictUpdateInterval
|
||||
// for why that stalls a step every time a packet runs late.
|
||||
//
|
||||
enum {UpdateIntervalSamples = 8};
|
||||
Scalar
|
||||
updateIntervals[UpdateIntervalSamples];
|
||||
int
|
||||
updateIntervalCount,
|
||||
updateIntervalWrite;
|
||||
|
||||
//
|
||||
// The interval last predicted, and how wrong the prediction before it
|
||||
// proved to be once the gap it described actually elapsed. Per entity,
|
||||
// so a trace reads the entity it is watching.
|
||||
//
|
||||
Scalar
|
||||
predictedInterval,
|
||||
predictionError;
|
||||
|
||||
//
|
||||
// Arrival statistics for the window a trace reports over. A long gap
|
||||
// followed by normal gaps means the sender went quiet; a long gap
|
||||
// followed by a burst of near-zero ones means OUR loop stalled and the
|
||||
// packets queued up behind it. The two look identical from inside the
|
||||
// dead reckoner and want opposite fixes.
|
||||
//
|
||||
Scalar
|
||||
widestGap;
|
||||
int
|
||||
longGapCount,
|
||||
queuedGapCount;
|
||||
|
||||
//
|
||||
// The last position, velocity and timestamp the SENDER reported, kept
|
||||
// so an arriving update can be scored against what the one before it
|
||||
// predicted. All three come from the same machine, so the comparison
|
||||
// owes nothing to latency or to clock alignment.
|
||||
//
|
||||
Time
|
||||
senderStamp;
|
||||
Point3D
|
||||
senderPosition;
|
||||
Vector3D
|
||||
senderVelocity;
|
||||
Logical
|
||||
haveSenderSample;
|
||||
|
||||
Scalar
|
||||
PredictUpdateInterval(Scalar latest);
|
||||
void
|
||||
ResetUpdateIntervals();
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Collision support
|
||||
//
|
||||
@@ -461,3 +524,11 @@ public:
|
||||
Logical
|
||||
TestInstance() const;
|
||||
};
|
||||
|
||||
//
|
||||
// The replicant the RP412CAMLOG traces are describing, so the renderer can
|
||||
// report on the same pod from the other end - what its motion looks like on
|
||||
// screen. Null until a replicant has stepped at least once.
|
||||
//
|
||||
EntityID
|
||||
MoverTracedEntity();
|
||||
|
||||
+34
-2
@@ -5,6 +5,7 @@
|
||||
#include "interest.h"
|
||||
#include "icom.h"
|
||||
#include "app.h"
|
||||
#include "spooler.h"
|
||||
#include "notation.h"
|
||||
#include "nttmgr.h"
|
||||
|
||||
@@ -72,10 +73,41 @@ Logical
|
||||
//
|
||||
void
|
||||
NetworkClient::ReceiveNetworkPacket(
|
||||
NetworkPacket*,
|
||||
NetworkPacket *packet,
|
||||
Receiver::Message *packet_message
|
||||
)
|
||||
{
|
||||
//
|
||||
// The recording tee.
|
||||
//
|
||||
// Every network client comes through here - the interest manager
|
||||
// carrying entity updates and the network manager carrying mission
|
||||
// control - which are the two places the review build spools
|
||||
// separately. One hook covers both, and it sits before Dispatch so a
|
||||
// packet is kept whether or not anything downstream makes use of it.
|
||||
//
|
||||
// The recorder arms itself on the first packet rather than at the
|
||||
// green light: playback rebuilds the world from the LoadMission and
|
||||
// RunMission packets, so a spool that starts at the flag cannot be
|
||||
// replayed.
|
||||
//
|
||||
if (Application::IsRecording())
|
||||
{
|
||||
SpoolRecorder *recorder = SpoolRecorder_Get();
|
||||
|
||||
if (!recorder->IsArmed())
|
||||
{
|
||||
if (!recorder->Arm())
|
||||
{
|
||||
//
|
||||
// Could not reserve the buffer - say so once, by turning
|
||||
// the request off, rather than asking again every packet.
|
||||
//
|
||||
Application::SetRecording(False);
|
||||
}
|
||||
}
|
||||
recorder->Record(packet);
|
||||
}
|
||||
Dispatch(packet_message);
|
||||
}
|
||||
|
||||
@@ -255,7 +287,7 @@ void
|
||||
networkEggNotationFile = new NotationFile();
|
||||
Register_Object(networkEggNotationFile);
|
||||
networkEggNotationFile->ReadText(eggTempBuffer, eggTempNext);
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
|
||||
//
|
||||
// Now turn the notation file into a mission
|
||||
|
||||
@@ -172,6 +172,11 @@ void
|
||||
// Create any MUNGA level vehicles
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: CreatePlayerVehicle, cameraShipPlayer="
|
||||
<< (int) (IsCameraShipPlayer() ? 1 : 0) << "\n" << std::flush;
|
||||
}
|
||||
if (IsCameraShipPlayer())
|
||||
{
|
||||
Check(application);
|
||||
@@ -186,6 +191,11 @@ void
|
||||
playerMission->GetGameModel(),
|
||||
ResourceDescription::ModelListResourceType
|
||||
);
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: model '" << playerMission->GetGameModel()
|
||||
<< "' resource " << (camera_res ? "FOUND" : "MISSING") << "\n" << std::flush;
|
||||
}
|
||||
Check(camera_res);
|
||||
CameraShip::MakeMessage
|
||||
create_camera(
|
||||
|
||||
+412
-12
@@ -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();
|
||||
}
|
||||
@@ -441,9 +621,238 @@ void*
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// RP412PHYSICSHZ - the size of one simulation step, as a rate in hertz.
|
||||
//
|
||||
// The engine simulates TO a timestamp: every entity keeps a lastPerformance
|
||||
// marking how far it has been simulated, and PerformAndWatch hands Perform()
|
||||
// the difference. That difference used to be however long the last frame
|
||||
// took, which made the frame rate part of the physics - explicitly so, since
|
||||
// Mover scales its bounce and penetration thresholds by delta_t.
|
||||
//
|
||||
// Advancing lastPerformance in fixed steps instead makes it the accumulator
|
||||
// a fixed-step loop needs, and every Perform() in the game gets an identical
|
||||
// dt without one of them being touched.
|
||||
//
|
||||
// 0 restores the old behaviour for comparison. The RATE is a game-feel
|
||||
// decision, not a technical one: thirty years of handling constants were
|
||||
// tuned against the DOS build's 40 ms steps, and RP412 has been running
|
||||
// ~18 ms variable ones, so the feel has already drifted. Whatever is chosen
|
||||
// here becomes the canonical physics for pods and PCs alike.
|
||||
//#############################################################################
|
||||
|
||||
static Scalar
|
||||
FixedPhysicsStep()
|
||||
{
|
||||
static Scalar
|
||||
step = (Scalar) -1;
|
||||
|
||||
if (step < (Scalar) 0)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412PHYSICSHZ");
|
||||
|
||||
//
|
||||
// 50 Hz is the default: a 20 ms step, exact on the millisecond
|
||||
// clock, and the rate whose settled hover ride height measured
|
||||
// closest to the frame-coupled physics the game has always run.
|
||||
// Proven before it was defaulted - a scripted lap with a crash,
|
||||
// a burn and two respawns runs bit-identical at 30, 60 and 144
|
||||
// fps, and identical runs reproduce exactly. 0 restores the
|
||||
// original frame-coupled behaviour, where the frame rate is
|
||||
// part of the physics.
|
||||
//
|
||||
int rate = (setting != NULL) ? atoi(setting) : 50;
|
||||
|
||||
//
|
||||
// Guard the arithmetic rather than the taste: a rate below the
|
||||
// frame rate is a legitimate choice (the pods ran at 25), but a
|
||||
// step of zero or a negative one is not a choice at all.
|
||||
//
|
||||
if (rate < 0)
|
||||
{
|
||||
rate = 0;
|
||||
}
|
||||
if (rate > 1000)
|
||||
{
|
||||
rate = 1000;
|
||||
}
|
||||
step = (rate > 0) ? ((Scalar) 1 / (Scalar) rate) : (Scalar) 0;
|
||||
|
||||
DEBUG_STREAM << "Physics: ";
|
||||
if (rate > 0)
|
||||
{
|
||||
DEBUG_STREAM << "fixed step, " << rate << " Hz";
|
||||
|
||||
//
|
||||
// The engine's clock counts MILLISECONDS, so a step is
|
||||
// really round(1000/rate) ms. A rate that does not divide
|
||||
// 1000 evenly therefore runs at a neighbouring rate wearing
|
||||
// this one's name - 60 asks for 16.67 ms and gets 17, which
|
||||
// is 58.8 Hz. Say so, and name the rates that mean what
|
||||
// they say.
|
||||
//
|
||||
if ((1000 % rate) != 0)
|
||||
{
|
||||
int step_ms = (1000 + rate / 2) / rate;
|
||||
DEBUG_STREAM << " - NOT millisecond-exact, steps will run "
|
||||
<< step_ms << " ms (" << (1000.0f / (float) step_ms)
|
||||
<< " Hz). 25, 50 and 100 are exact";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "frame-coupled (RP412PHYSICSHZ=0)";
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
//
|
||||
// How far behind one frame may catch up: a quarter second of simulation,
|
||||
// whatever the rate - enough to ride out a texture load or an alt-tab,
|
||||
// short of letting a stalled machine spiral. Counted in steps because the
|
||||
// loop is, so 6 steps at 25 Hz, 12 at 50, 25 at 100.
|
||||
//
|
||||
static int
|
||||
MaximumCatchUpSteps(Scalar step)
|
||||
{
|
||||
int steps = (int)((Scalar) 0.25 / step);
|
||||
return (steps < 4) ? 4 : steps;
|
||||
}
|
||||
|
||||
// how many fixed steps the whole simulation has taken - the trace prints it,
|
||||
// so 'is the step actually fixed' is answered by measurement not by reading
|
||||
long gPhysicsStepsTaken = 0;
|
||||
|
||||
//#############################################################################
|
||||
// Simulation Support
|
||||
//
|
||||
Scalar
|
||||
Simulation::FixedStep()
|
||||
{
|
||||
return FixedPhysicsStep();
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::PerformTo(const Time& till)
|
||||
{
|
||||
Check(this);
|
||||
Check(&till);
|
||||
|
||||
Scalar step = FixedPhysicsStep();
|
||||
|
||||
if (step > (Scalar) 0)
|
||||
{
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// Fixed step. The simulation advances in whole steps of the same
|
||||
// size on every machine, and whatever is left over waits for the
|
||||
// next frame - lastPerformance is the accumulator, and always was.
|
||||
//
|
||||
// Before this, the slice was simply however long the last frame
|
||||
// took, so a 30 fps machine integrated gravity in 33 ms steps and
|
||||
// a 144 fps machine in 7 ms ones. Nothing in any Perform()
|
||||
// changes: it is handed a dt it can rely on instead of one that
|
||||
// depended on the graphics card.
|
||||
//
|
||||
// NOTE the caller decides the interleaving. An entity's spring
|
||||
// forces are computed from its subsystems (the VTV reads its
|
||||
// thrusters' measured heights), so the subsystems and the entity
|
||||
// must advance TOGETHER, one step at a time -
|
||||
// Entity::PerformAndWatch owns that loop and hands everyone the
|
||||
// same sub-frame 'till'. Stepping a subsystem all the way to the
|
||||
// frame boundary before its owner moves at all is how the first
|
||||
// attempt at this produced a pod that climbed at 30 fps and flew
|
||||
// level at 144: two spring impulses from one stale height sample.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
Scalar behind = till - lastPerformance;
|
||||
int taken = 0;
|
||||
int max_steps = MaximumCatchUpSteps(step);
|
||||
|
||||
while (behind >= step && taken < max_steps)
|
||||
{
|
||||
//
|
||||
// Where this step STARTED, for drawing. Taken here rather than
|
||||
// in BeginStep so it covers replicants too, and taken after any
|
||||
// BeginStep teleport (a VTV's scheduled respawn) so a jump
|
||||
// stays a cut instead of becoming a slide.
|
||||
//
|
||||
SnapshotRenderOrigin();
|
||||
Perform(step);
|
||||
++gPhysicsStepsTaken;
|
||||
lastPerformance += step;
|
||||
behind -= step;
|
||||
++taken;
|
||||
}
|
||||
|
||||
//
|
||||
// How far past the last completed step the frame being drawn falls.
|
||||
// Entity::PerformAndWatch computes this again from the FRAME's till
|
||||
// after its interleave, because there this function is called once
|
||||
// per step and sees no leftover at all.
|
||||
//
|
||||
{
|
||||
Scalar fraction = behind / step;
|
||||
if (fraction < (Scalar) 0) fraction = (Scalar) 0;
|
||||
if (fraction > (Scalar) 1) fraction = (Scalar) 1;
|
||||
SetRenderStepFraction(fraction);
|
||||
}
|
||||
|
||||
//
|
||||
// A machine that cannot keep up must not try to buy back the whole
|
||||
// backlog next frame - that costs more time, which makes a bigger
|
||||
// backlog. Drop what could not be run and carry on: the game slows
|
||||
// down rather than seizing, and it does so identically everywhere.
|
||||
//
|
||||
if (taken >= max_steps && behind >= step)
|
||||
{
|
||||
lastPerformance = till;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Scalar slice = till - lastPerformance;
|
||||
lastPerformance = till;
|
||||
|
||||
Perform(slice);
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::BeginStep()
|
||||
{
|
||||
// nothing by default - see the header
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::SnapshotRenderOrigin()
|
||||
{
|
||||
// nothing by default - only an Entity has an origin to snapshot
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::SetRenderStepFraction(Scalar)
|
||||
{
|
||||
// nothing by default - see the header
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::WatchAndWrite(MemoryStream *update_stream)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (!AreWatchersDelayed())
|
||||
{
|
||||
ExecuteWatchers();
|
||||
}
|
||||
WriteSimulationUpdate(update_stream);
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::PerformAndWatch(
|
||||
const Time& till,
|
||||
@@ -453,17 +862,8 @@ void
|
||||
Check(this);
|
||||
Check(&till);
|
||||
|
||||
Scalar slice = till - lastPerformance;
|
||||
lastPerformance = till;
|
||||
|
||||
Perform(slice);
|
||||
if (!AreWatchersDelayed())
|
||||
{
|
||||
ExecuteWatchers();
|
||||
}
|
||||
WriteSimulationUpdate(update_stream);
|
||||
|
||||
Check_Fpu();
|
||||
PerformTo(till);
|
||||
WatchAndWrite(update_stream);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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;
|
||||
@@ -112,6 +147,53 @@ public:
|
||||
MemoryStream *update_stream
|
||||
);
|
||||
|
||||
//
|
||||
// The two halves of PerformAndWatch, so an ENTITY can interleave its
|
||||
// subsystems' physics with its own, step by step, and still run the
|
||||
// watchers and the update stream once per frame. PerformTo advances
|
||||
// the simulation to the given time - in fixed steps when
|
||||
// RP412PHYSICSHZ names a rate, in one variable slice otherwise.
|
||||
//
|
||||
void
|
||||
PerformTo(const Time& till);
|
||||
void
|
||||
WatchAndWrite(MemoryStream *update_stream);
|
||||
|
||||
//
|
||||
// Called by the entity interleave at the TOP of every fixed step,
|
||||
// before any subsystem adds its forces for that step. Per-frame set-up
|
||||
// work - clearing a force accumulator, deriving local velocity from
|
||||
// world state - belongs here when the fixed step is on, because "once
|
||||
// per frame" is a wall-clock cadence and the whole point is that wall
|
||||
// clock no longer reaches the physics. Default: nothing.
|
||||
//
|
||||
virtual void
|
||||
BeginStep();
|
||||
|
||||
//
|
||||
// Render interpolation hooks, called by PerformTo around the fixed
|
||||
// step. They live HERE rather than on the entity interleave because a
|
||||
// REPLICANT never runs that interleave - it reaches PerformTo through
|
||||
// Simulation::PerformAndWatch instead - and a replicant is exactly what
|
||||
// every remote pod is. Hanging the snapshot off BeginStep left the
|
||||
// watched car uninterpolated while the camera watching it was smooth,
|
||||
// which is most of the way to nowhere.
|
||||
//
|
||||
// Defaults do nothing; Entity overrides them because it owns the
|
||||
// origin. See Entity::GetRenderToWorld.
|
||||
//
|
||||
virtual void
|
||||
SnapshotRenderOrigin();
|
||||
virtual void
|
||||
SetRenderStepFraction(Scalar fraction);
|
||||
|
||||
//
|
||||
// The fixed step in seconds, 0 when frame-coupled. Global on purpose:
|
||||
// a mixed-rate simulation would be a worse bug than either mode.
|
||||
//
|
||||
static Scalar
|
||||
FixedStep();
|
||||
|
||||
void
|
||||
DoNothingOnce(Scalar time_slice);
|
||||
void
|
||||
@@ -120,6 +202,9 @@ public:
|
||||
void
|
||||
SetLastPerformance(const Time& when)
|
||||
{Check(this); Check(&when); lastPerformance = when;}
|
||||
const Time&
|
||||
GetLastPerformance() const
|
||||
{Check(this); return lastPerformance;}
|
||||
void
|
||||
RequestEncore(Encore encore);
|
||||
|
||||
|
||||
@@ -158,6 +158,310 @@ NetworkPacket*
|
||||
return (NetworkPacket*)GetPointer();
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//############################## SpoolRecorder ##############################
|
||||
//#############################################################################
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// How much memory to give a recording, in megabytes.
|
||||
//
|
||||
// The review build's SPOOL_SIZE is 6MB, which was a sensible arcade
|
||||
// number and is a silly desktop one: a full grid sends on the order of
|
||||
// 17KB a second, so six megabytes is about six minutes and a long race
|
||||
// would hit the end of it. A hundred megabytes is roughly an hour and a
|
||||
// half and costs nothing on any machine that can run this.
|
||||
//
|
||||
static size_t
|
||||
RecordSizeBytes()
|
||||
{
|
||||
static size_t cached = 0;
|
||||
|
||||
if (cached == 0)
|
||||
{
|
||||
const char *setting = getenv("RP412RECORDSIZE");
|
||||
int megabytes = (setting != NULL) ? atoi(setting) : 100;
|
||||
|
||||
if (megabytes < 1) { megabytes = 1; }
|
||||
if (megabytes > 512) { megabytes = 512; }
|
||||
cached = (size_t) megabytes * 1024 * 1024;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
SpoolRecorder::SpoolRecorder():
|
||||
buffer(NULL),
|
||||
spool(NULL),
|
||||
bufferSize(0),
|
||||
armed(False),
|
||||
full(False),
|
||||
headerWritten(False),
|
||||
packetsRecorded(0)
|
||||
{
|
||||
eggPath[0] = '\0';
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
SpoolRecorder::SetEggPath(const char *path)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (path == NULL)
|
||||
{
|
||||
eggPath[0] = '\0';
|
||||
return;
|
||||
}
|
||||
strncpy(eggPath, path, sizeof(eggPath) - 1);
|
||||
eggPath[sizeof(eggPath) - 1] = '\0';
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
SpoolRecorder::~SpoolRecorder()
|
||||
{
|
||||
Disarm();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
Logical
|
||||
SpoolRecorder::Arm()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (armed)
|
||||
{
|
||||
return True;
|
||||
}
|
||||
|
||||
bufferSize = RecordSizeBytes();
|
||||
buffer = new char[bufferSize];
|
||||
if (buffer == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "Record: could not reserve "
|
||||
<< (bufferSize / (1024 * 1024))
|
||||
<< "MB - recording disabled for this race\n" << std::flush;
|
||||
bufferSize = 0;
|
||||
return False;
|
||||
}
|
||||
|
||||
spool = new SpoolFile(buffer, bufferSize);
|
||||
spool->spoolState = SpoolFile::Spooling;
|
||||
armed = True;
|
||||
full = False;
|
||||
headerWritten = False;
|
||||
packetsRecorded = 0;
|
||||
|
||||
DEBUG_STREAM << "Record: armed, " << (bufferSize / (1024 * 1024))
|
||||
<< "MB (RP412RECORDSIZE)\n" << std::flush;
|
||||
return True;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
SpoolRecorder::Record(const NetworkPacket *packet)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (!armed || full || packet == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Check_Pointer(spool);
|
||||
|
||||
int length =
|
||||
packet->messageData.messageLength + sizeof(NetworkPacketHeader);
|
||||
|
||||
//
|
||||
// Ask before writing. SpoolFile::SpoolPacket answers a full buffer by
|
||||
// quitting the process, which would end the race this is recording -
|
||||
// so the recorder never lets it get that far, and stops instead.
|
||||
//
|
||||
if ((int) spool->GetBytesRemaining() < length)
|
||||
{
|
||||
full = True;
|
||||
DEBUG_STREAM << "Record: buffer full after " << packetsRecorded
|
||||
<< " packets - the race continues, the recording stops here."
|
||||
<< " Raise RP412RECORDSIZE to keep more.\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Where the copy is about to land, taken BEFORE the write so the
|
||||
// timestamp can be applied to the copy afterwards. The live packet is
|
||||
// never modified: its timeStamp is the sender's sampling moment, which
|
||||
// Simulation::ReadUpdateRecord hands to RP412NETCLOCK and from there to
|
||||
// the dead reckoner's projection. Restamping it in place - which is
|
||||
// what the review spooler does, harmlessly, having nothing else to
|
||||
// serve - would put arrival jitter straight into where remote pods are
|
||||
// drawn.
|
||||
//
|
||||
NetworkPacket *copy = (NetworkPacket*) spool->GetPointer();
|
||||
|
||||
spool->SpoolPacket((NetworkPacket*) packet);
|
||||
|
||||
//
|
||||
// Playback paces from these, and packets from different senders carry
|
||||
// different clock origins, so the spool needs them all on one clock:
|
||||
// ours, at the moment of arrival.
|
||||
//
|
||||
copy->timeStamp = Now();
|
||||
++packetsRecorded;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
SpoolRecorder::Save()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
// Never armed means not one packet arrived all race.
|
||||
//
|
||||
// Recording captures the packets this station RECEIVES, and a race
|
||||
// with no other machines in it sends and receives nothing at all -
|
||||
// L4NetworkManager::ExclusiveBroadcast walks the remote hosts, and a
|
||||
// solo race has none. So there is nothing to keep, and the reason is
|
||||
// worth saying rather than leaving an empty folder to be puzzled over.
|
||||
//
|
||||
// The same gap is why a RACER's recording is not the whole race: its
|
||||
// own pod is simulated locally and never arrives as a packet. A Live
|
||||
// Cam races nothing, so every pod reaches it over the wire, which
|
||||
// makes it the only station that hears the lot.
|
||||
//
|
||||
if (!armed || spool == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "Record: nothing to write - no packets were received"
|
||||
<< " this race. Recording keeps what arrives over the network,"
|
||||
<< " so a single-player race has nothing to keep.\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
if (packetsRecorded == 0)
|
||||
{
|
||||
DEBUG_STREAM << "Record: nothing captured, no file written\n"
|
||||
<< std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The review build writes here and so does this, so one folder holds
|
||||
// every spool however it was made and the playback build finds them
|
||||
// all in the same place.
|
||||
//
|
||||
CreateDirectoryA("SPOOLS", NULL);
|
||||
|
||||
struct tm newtime;
|
||||
__int64 ltime;
|
||||
|
||||
_time64(<ime);
|
||||
_gmtime64_s(&newtime, <ime);
|
||||
|
||||
char filename[MAX_PATH];
|
||||
|
||||
sprintf(
|
||||
filename,
|
||||
"SPOOLS\\%.4i_%.2i_%.2i_%.2i%.2i%.2i.spl",
|
||||
newtime.tm_year + 1900, newtime.tm_mon + 1, newtime.tm_mday,
|
||||
newtime.tm_hour, newtime.tm_min, newtime.tm_sec
|
||||
);
|
||||
|
||||
//
|
||||
// Read the size BEFORE saving: SaveAs rewinds the stream when it is
|
||||
// done, so asking afterwards reports nothing written at all - which
|
||||
// is what the first recording's log said, next to a 2.8MB file.
|
||||
//
|
||||
size_t written = spool->GetBytesUsed();
|
||||
|
||||
spool->SaveAs(filename);
|
||||
CopyFileA(filename, "last.spl", FALSE);
|
||||
|
||||
//
|
||||
// The egg beside it, under the same stem.
|
||||
//
|
||||
// A spool is only half a recording: it says what moved, never the
|
||||
// track it moved through, and playback will not start without the egg
|
||||
// the race was run on. frontend.egg is rewritten by the next race set
|
||||
// up on this machine, so a recording kept on its own quietly stops
|
||||
// being playable as soon as somebody picks another track. Kept
|
||||
// together they stay one artifact for as long as the folder does.
|
||||
//
|
||||
if (eggPath[0] != '\0')
|
||||
{
|
||||
char egg_copy[MAX_PATH];
|
||||
|
||||
strncpy(egg_copy, filename, sizeof(egg_copy) - 1);
|
||||
egg_copy[sizeof(egg_copy) - 1] = '\0';
|
||||
|
||||
size_t length = strlen(egg_copy);
|
||||
if (length > 4)
|
||||
{
|
||||
strcpy(egg_copy + length - 4, ".egg");
|
||||
if (CopyFileA(eggPath, egg_copy, FALSE))
|
||||
{
|
||||
CopyFileA(eggPath, "last.egg", FALSE);
|
||||
DEBUG_STREAM << "Record: kept the egg beside it as "
|
||||
<< egg_copy << "\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "Record: could NOT copy the egg '" << eggPath
|
||||
<< "' - the spool will not replay without it\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "Record: no egg path known, so none kept - this spool"
|
||||
<< " will need the matching egg supplied by hand to replay\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "Record: wrote " << filename << " - "
|
||||
<< packetsRecorded << " packets, " << (written / 1024) << "KB"
|
||||
<< (full ? " (truncated - buffer filled)" : "")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
SpoolRecorder::Disarm()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (spool != NULL)
|
||||
{
|
||||
delete spool;
|
||||
spool = NULL;
|
||||
}
|
||||
if (buffer != NULL)
|
||||
{
|
||||
delete [] buffer;
|
||||
buffer = NULL;
|
||||
}
|
||||
bufferSize = 0;
|
||||
armed = False;
|
||||
full = False;
|
||||
headerWritten = False;
|
||||
packetsRecorded = 0;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
SpoolRecorder *
|
||||
SpoolRecorder_Get()
|
||||
{
|
||||
static SpoolRecorder recorder;
|
||||
|
||||
return &recorder;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//################# MissionReviewApplicationManager #####################
|
||||
//#############################################################################
|
||||
|
||||
@@ -38,6 +38,104 @@ public:
|
||||
NextPacket();
|
||||
};
|
||||
|
||||
//##########################################################################
|
||||
//########################## SpoolRecorder #############################
|
||||
//##########################################################################
|
||||
//
|
||||
// Keeping a race from a station that is also PLAYING one.
|
||||
//
|
||||
// The review build already records by teeing: L4SpoolingNetworkManager
|
||||
// spools each packet and then hands it to the ordinary receive path. What
|
||||
// tied that to a review build was never the recording - it was where the
|
||||
// buffer came from. MissionReviewApplicationManager is only a pool
|
||||
// allocator, and SpoolFile takes whatever buffer it is handed, so a
|
||||
// recorder that owns one buffer needs none of it.
|
||||
//
|
||||
// Two things a live recorder must do that the review one did not:
|
||||
//
|
||||
// It must not touch the packet. The spooler restamps each packet with
|
||||
// local arrival time, which is right - playback paces off those stamps and
|
||||
// packets from different senders carry different clock origins, so they
|
||||
// have to be put on one clock. But doing it in place would overwrite the
|
||||
// sender's timestamp that Simulation::ReadUpdateRecord feeds to
|
||||
// RP412NETCLOCK and the dead reckoner, which is exactly the input behind
|
||||
// the projection tick. So it stamps the COPY, in the spool, after writing.
|
||||
//
|
||||
// And it must not take the race down with it. SpoolFile::SpoolPacket
|
||||
// answers a full buffer with PostQuitMessage, which for a review is a fair
|
||||
// end to a replay and for a live host is killing the race being recorded.
|
||||
// The recorder checks the room first and simply stops.
|
||||
//
|
||||
class SpoolRecorder
|
||||
{
|
||||
public:
|
||||
SpoolRecorder();
|
||||
~SpoolRecorder();
|
||||
|
||||
// Allocate and begin. Call before the mission loads: playback rebuilds
|
||||
// the world from the LoadMission and RunMission packets, so a spool
|
||||
// armed at the green light cannot be replayed.
|
||||
Logical
|
||||
Arm();
|
||||
|
||||
// Tee one received packet. Safe to call unarmed or after the buffer
|
||||
// has filled - both do nothing.
|
||||
void
|
||||
Record(const NetworkPacket *packet);
|
||||
|
||||
// Where the egg for this race lives, so it can be kept beside the
|
||||
// recording. A spool records what MOVED and never the track it moved
|
||||
// through, so the two are one artifact: without its egg a spool cannot
|
||||
// be replayed, and frontend.egg is overwritten by the next race set up
|
||||
// on this machine. Saving them together is what stops a recording
|
||||
// going stale the moment somebody picks a different track.
|
||||
void
|
||||
SetEggPath(const char *path);
|
||||
|
||||
// Write SPOOLS\<timestamp>.spl, the matching .egg beside it, and copy
|
||||
// the spool to last.spl.
|
||||
void
|
||||
Save();
|
||||
|
||||
void
|
||||
Disarm();
|
||||
|
||||
Logical
|
||||
IsArmed() const
|
||||
{ return armed; }
|
||||
|
||||
//
|
||||
// A spool opens with a header - the application ID, the resource
|
||||
// major version, and one (remote, hostID) pair per host named in the
|
||||
// egg - and playback refuses a spool without it. All of that is
|
||||
// network-layer knowledge, so it is written by the network manager
|
||||
// once the hosts exist, and these three are what let it.
|
||||
//
|
||||
SpoolFile*
|
||||
GetSpool()
|
||||
{ return spool; }
|
||||
Logical
|
||||
HeaderWritten() const
|
||||
{ return headerWritten; }
|
||||
void
|
||||
MarkHeaderWritten()
|
||||
{ headerWritten = True; }
|
||||
|
||||
protected:
|
||||
char *buffer;
|
||||
SpoolFile *spool;
|
||||
size_t bufferSize;
|
||||
Logical armed;
|
||||
Logical full;
|
||||
Logical headerWritten;
|
||||
int packetsRecorded;
|
||||
char eggPath[260];
|
||||
};
|
||||
|
||||
// The process-wide recorder, made on first use.
|
||||
SpoolRecorder *
|
||||
SpoolRecorder_Get();
|
||||
|
||||
//##########################################################################
|
||||
//############## MissionReviewApplicationManager #####################
|
||||
//##########################################################################
|
||||
|
||||
@@ -32,6 +32,12 @@ protected:
|
||||
static long ticksPerSecond;
|
||||
static __int64 perfCounterFreq;
|
||||
|
||||
//
|
||||
// The counter reading this process started at, so the clock counts from
|
||||
// launch rather than from the machine's boot. See GetRTC.
|
||||
//
|
||||
static __int64 perfCounterOrigin;
|
||||
|
||||
static long GetRTC();
|
||||
static double GetHiRes();
|
||||
static __int64 GetHiResTicks();
|
||||
|
||||
+12
-2
@@ -168,10 +168,20 @@ void
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------------
|
||||
// If update message is not null then send the change
|
||||
// If update message is not null then send the change.
|
||||
//
|
||||
// The dynamic master socket holds Independant and Hermit instances as
|
||||
// well as masters, and neither of those publishes: an Independant runs
|
||||
// its own simulation on every host, and a Hermit is not replicated at
|
||||
// all. EntityUpdateReplicants asserts MasterInstance, so the caller is
|
||||
// the one that has to make that true - the clockwork doorframes are
|
||||
// Hermits and would otherwise arrive there.
|
||||
//-----------------------------------------------------------------------
|
||||
//
|
||||
if (update_message != NULL)
|
||||
if (
|
||||
update_message != NULL
|
||||
&& entity->GetInstance() == Entity::MasterInstance
|
||||
)
|
||||
{
|
||||
Check(update_message);
|
||||
|
||||
|
||||
@@ -133,6 +133,70 @@ void Verify_Failed(char *Message, char *File, int Line)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
//#############################################################################
|
||||
//
|
||||
//
|
||||
// The release build's Fail. Says what went wrong before it goes.
|
||||
//
|
||||
// Terminates exactly as the old bare abort() did - same call, same
|
||||
// 0xC0000409 - so no caller, script or crash handler sees a change. The
|
||||
// only difference is the line in the log naming the check that failed and
|
||||
// where it lives.
|
||||
//
|
||||
// The emergency shutdowns are worth doing before the process ends even
|
||||
// though it is ending: the gauge renderer and the controls manager both
|
||||
// hold hardware, and a display left mid-mode is a nuisance to whoever has
|
||||
// to pick the machine up afterwards - which in a pod bay is not the
|
||||
// person who was playing.
|
||||
//
|
||||
void
|
||||
Fail_With_Message(const char *Message, const char *File, int Line)
|
||||
{
|
||||
if (!AlreadyFailed)
|
||||
{
|
||||
AlreadyFailed = True;
|
||||
|
||||
DEBUG_STREAM << "FAIL " << File << "(" << Line << "): " << Message
|
||||
<< "\n" << std::flush;
|
||||
|
||||
//
|
||||
// And again, somewhere that cannot be lost.
|
||||
//
|
||||
// rpl4.log is std::cout with its streambuf swapped for an ofstream's
|
||||
// (see WinMain), and the text written above did NOT survive to disk
|
||||
// across the exit - flushed as far as the stream and no further.
|
||||
// Rather than guess at where it stops, write the one line that
|
||||
// matters to its own file and close it: fclose is a promise the
|
||||
// bytes are on the disk, and a file opened and closed inside this
|
||||
// function cannot be left dangling by whatever happens next.
|
||||
//
|
||||
// Its own file rather than an append to rpl4.log because that one is
|
||||
// already open for writing and Windows will not share it.
|
||||
//
|
||||
FILE *record = fopen("rpl4-fail.log", "w");
|
||||
|
||||
if (record != NULL)
|
||||
{
|
||||
fprintf(record, "FAIL %s(%d): %s\n", File, Line, Message);
|
||||
fclose(record);
|
||||
}
|
||||
|
||||
//
|
||||
// Hardware last, and after the record is safely written: the gauge
|
||||
// renderer and the controls manager both hold devices, and a display
|
||||
// left mid-mode is a nuisance for whoever picks the machine up - in
|
||||
// a pod bay, not the person who was playing. If one of these faults
|
||||
// on the way down, the failure has already been recorded.
|
||||
//
|
||||
SystemClock::timer.Shutdown();
|
||||
ControlsManager::Shutdown();
|
||||
GaugeRenderer::EmergencyShutdown();
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
//#############################################################################
|
||||
|
||||
@@ -82,6 +82,10 @@ public:
|
||||
// -fit: borderless window filling the monitor, with the render size
|
||||
// chosen to match the cockpit canvas it will be presented into.
|
||||
static bool GetFitDisplay() { return mFitDisplay; }
|
||||
// -fit's borderless full-monitor placement. Applied once at startup so
|
||||
// the window is in its final shape before ANY mission builds a device
|
||||
// against it - see the definition for why the first race differed.
|
||||
static void FitWindowToMonitor(HWND window);
|
||||
static Logical GetSeeSolids() { return seeSolids; }
|
||||
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
|
||||
// The front end's multiplayer path turns network mode on at launch
|
||||
|
||||
+107
-6
@@ -240,6 +240,9 @@ Logical
|
||||
{
|
||||
DEBUG_STREAM << "\n" << argv[0] <<
|
||||
" -egg <filename> -net <memory_address> -solids -h -help\n"
|
||||
" -mr mission review: play a spool back and\n"
|
||||
" record a new one alongside it\n"
|
||||
" -pb play a spool back only, no recording\n"
|
||||
" -windowed windowed, title bar and all\n"
|
||||
" -fit borderless over the whole monitor,\n"
|
||||
" render size chosen to match\n"
|
||||
@@ -302,6 +305,72 @@ void
|
||||
<< monitor_w << "x" << monitor_h << " monitor\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// FitWindowToMonitor
|
||||
//#############################################################################
|
||||
//
|
||||
// -fit's borderless full-monitor placement, applied to the shell window.
|
||||
//
|
||||
// This has to happen BEFORE the first race, not during it. SVGA16 does the
|
||||
// same thing when it assembles the cockpit, but that is not until a mission
|
||||
// starts - and the D3D device is created just ahead of it, against whatever
|
||||
// the window is at that moment. So the first race got a device sized to a
|
||||
// still-bordered window and every race after it got one sized to the
|
||||
// borderless monitor: two different render targets, two different frame
|
||||
// costs, from one lobby and one set of settings.
|
||||
//
|
||||
// A racing sim cannot have that. The window reaches its final shape while
|
||||
// the front end is still up, so every mission of a session - the first one
|
||||
// included - is set up against exactly the same client area.
|
||||
//
|
||||
// SVGA16 still applies it when it builds the cockpit. That call becomes a
|
||||
// no-op rather than a change, which is the point.
|
||||
//
|
||||
void
|
||||
L4Application::FitWindowToMonitor(HWND window)
|
||||
{
|
||||
if (window == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RECT monitor_rect;
|
||||
monitor_rect.left = 0;
|
||||
monitor_rect.top = 0;
|
||||
monitor_rect.right = GetSystemMetrics(SM_CXSCREEN);
|
||||
monitor_rect.bottom = GetSystemMetrics(SM_CYSCREEN);
|
||||
|
||||
MONITORINFO monitor;
|
||||
memset(&monitor, 0, sizeof(monitor));
|
||||
monitor.cbSize = sizeof(monitor);
|
||||
HMONITOR handle = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
|
||||
if (GetMonitorInfoA(handle, &monitor))
|
||||
{
|
||||
monitor_rect = monitor.rcMonitor;
|
||||
}
|
||||
|
||||
//
|
||||
// Same style surgery SVGA16 performs, so the two agree exactly.
|
||||
//
|
||||
LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE);
|
||||
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
|
||||
WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME);
|
||||
style |= WS_POPUP | WS_CLIPCHILDREN;
|
||||
SetWindowLongPtrA(window, GWL_STYLE, style);
|
||||
|
||||
SetWindowPos(window, NULL,
|
||||
monitor_rect.left, monitor_rect.top,
|
||||
monitor_rect.right - monitor_rect.left,
|
||||
monitor_rect.bottom - monitor_rect.top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||||
|
||||
DEBUG_STREAM << "L4Application: -fit placed the window borderless at "
|
||||
<< (monitor_rect.right - monitor_rect.left) << "x"
|
||||
<< (monitor_rect.bottom - monitor_rect.top)
|
||||
<< " before the first mission\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// ParseCommandLine
|
||||
@@ -337,6 +406,23 @@ Logical
|
||||
{
|
||||
suppressGauges = TRUE;
|
||||
missionReviewMode = 1;
|
||||
}
|
||||
//
|
||||
// Play a spool back without recording a new one.
|
||||
//
|
||||
// Mode 2 was always in RPL4.CPP - it takes one spool file
|
||||
// instead of two and skips the spooling application - there was
|
||||
// simply no way to ask for it. -mr starts a recorder alongside
|
||||
// the playback, and that recorder is the half that falls over
|
||||
// on a machine which is not a pod bay: it walks the hosts named
|
||||
// in the egg expecting to find every one of them connected.
|
||||
//
|
||||
// Watching a race back does not need a recorder at all.
|
||||
//
|
||||
else if (!stricmp(W2A(argv[i]), "-pb"))
|
||||
{
|
||||
suppressGauges = TRUE;
|
||||
missionReviewMode = 2;
|
||||
} else if (!(*parser)(&i, argc, argv))
|
||||
{
|
||||
return False;
|
||||
@@ -801,6 +887,13 @@ void
|
||||
// The debug keys are for developers: RP412DEVKEYS=1 arms them.
|
||||
// Players get exactly one chord - Alt+Q, the deliberate abort.
|
||||
//
|
||||
// Only Alt+W and Alt+E actually do anything. The rest call DPLRenderer
|
||||
// methods whose bodies were commented out with the rest of the DPL
|
||||
// calls in the 2007 Direct3D port and have been empty ever since; each
|
||||
// is marked INERT below. Reviving one means writing it against D3D9,
|
||||
// not un-commenting anything - the dpl_* types it used are empty
|
||||
// placeholder classes now (DPLSTUB.h).
|
||||
//
|
||||
static int dev_keys = -1;
|
||||
if (dev_keys < 0)
|
||||
{
|
||||
@@ -844,6 +937,7 @@ void
|
||||
|
||||
//--------------------------------------------
|
||||
// FrameDump from Division card to Targa file
|
||||
// INERT: DPLFrameDump and dump_frame_buffer are both stubs.
|
||||
//--------------------------------------------
|
||||
case PCK_ALT_F:
|
||||
{
|
||||
@@ -859,6 +953,7 @@ void
|
||||
|
||||
//------------------------------------
|
||||
// Report current free memory in card
|
||||
// INERT: the body below is commented out, so this key is silent.
|
||||
//------------------------------------
|
||||
case PCK_ALT_K:
|
||||
{
|
||||
@@ -886,6 +981,7 @@ void
|
||||
|
||||
//---------------------------------------
|
||||
// Report performance statistics (Alt-?)
|
||||
// INERT: DPLReportPerfStats is a stub - it read DPL's own counters.
|
||||
//---------------------------------------
|
||||
case PCK_ALT_SLASH:
|
||||
{
|
||||
@@ -898,9 +994,10 @@ void
|
||||
}
|
||||
break;
|
||||
}
|
||||
//--------------------------
|
||||
// Toggle Wireframe display
|
||||
//--------------------------
|
||||
//--------------------------------------------------------------
|
||||
// Toggle Wireframe display. Live: reimplemented on D3D9 as a
|
||||
// per-frame D3DRS_FILLMODE (see gWireframe in L4VIDEO.cpp).
|
||||
//--------------------------------------------------------------
|
||||
case PCK_ALT_W:
|
||||
{
|
||||
if (!dev_keys) break;
|
||||
@@ -912,9 +1009,13 @@ void
|
||||
}
|
||||
break;
|
||||
}
|
||||
//--------------------------
|
||||
// Toggle "Predator-vision"
|
||||
//--------------------------
|
||||
//--------------------------------------------------------------
|
||||
// Toggle "Predator-vision" - a global false-colour/thermal mode
|
||||
// the DPL renderer implemented internally, reached by passing an
|
||||
// out-of-band explosion effect type (-1 on, -2 off) with a NULL
|
||||
// DCS. INERT: DPLTogglePVision is a stub, and what it looked
|
||||
// like is not recorded anywhere in this tree.
|
||||
//--------------------------------------------------------------
|
||||
case PCK_ALT_V:
|
||||
{
|
||||
if (!dev_keys) break;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//###########################################################################
|
||||
//
|
||||
// L4AUDEFX.cpp -- OpenAL EFX bridge (docs/SOUND.md, findings F9 and F11).
|
||||
// See L4AUDEFX.h for the fidelity rationale.
|
||||
//
|
||||
//###########################################################################
|
||||
#include "mungal4.h"
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4audefx.h"
|
||||
#include "openal/alc.h"
|
||||
#include "openal/efx.h"
|
||||
|
||||
#ifndef AL_EFFECT_EAXREVERB
|
||||
#define AL_EFFECT_EAXREVERB 0x8000 // newer efx.h constant; OpenAL Soft supports it
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
bool s_available = false;
|
||||
ALuint s_reverbSlot = 0;
|
||||
ALuint s_reverbEffect = 0;
|
||||
ALuint s_scratchFilter = 0;
|
||||
|
||||
LPALGENEFFECTS p_alGenEffects = 0;
|
||||
LPALEFFECTI p_alEffecti = 0;
|
||||
LPALEFFECTF p_alEffectf = 0;
|
||||
LPALGENAUXILIARYEFFECTSLOTS p_alGenAuxiliaryEffectSlots = 0;
|
||||
LPALAUXILIARYEFFECTSLOTI p_alAuxiliaryEffectSloti = 0;
|
||||
LPALAUXILIARYEFFECTSLOTF p_alAuxiliaryEffectSlotf = 0;
|
||||
LPALGENFILTERS p_alGenFilters = 0;
|
||||
LPALFILTERI p_alFilteri = 0;
|
||||
LPALFILTERF p_alFilterf = 0;
|
||||
}
|
||||
|
||||
bool EFX_Available()
|
||||
{
|
||||
return s_available;
|
||||
}
|
||||
|
||||
bool EFX_Initialize(float global_reverb_scale)
|
||||
{
|
||||
ALCcontext *context = alcGetCurrentContext();
|
||||
if (context == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ALCdevice *device = alcGetContextsDevice(context);
|
||||
if (device == 0 || !alcIsExtensionPresent(device, "ALC_EXT_EFX"))
|
||||
{
|
||||
Tell("L4AUDEFX: ALC_EXT_EFX not present - filters and reverb inert\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
p_alGenEffects = (LPALGENEFFECTS)alGetProcAddress("alGenEffects");
|
||||
p_alEffecti = (LPALEFFECTI)alGetProcAddress("alEffecti");
|
||||
p_alEffectf = (LPALEFFECTF)alGetProcAddress("alEffectf");
|
||||
p_alGenAuxiliaryEffectSlots = (LPALGENAUXILIARYEFFECTSLOTS)alGetProcAddress("alGenAuxiliaryEffectSlots");
|
||||
p_alAuxiliaryEffectSloti = (LPALAUXILIARYEFFECTSLOTI)alGetProcAddress("alAuxiliaryEffectSloti");
|
||||
p_alAuxiliaryEffectSlotf = (LPALAUXILIARYEFFECTSLOTF)alGetProcAddress("alAuxiliaryEffectSlotf");
|
||||
p_alGenFilters = (LPALGENFILTERS)alGetProcAddress("alGenFilters");
|
||||
p_alFilteri = (LPALFILTERI)alGetProcAddress("alFilteri");
|
||||
p_alFilterf = (LPALFILTERF)alGetProcAddress("alFilterf");
|
||||
|
||||
if (!p_alGenEffects || !p_alEffecti || !p_alEffectf
|
||||
|| !p_alGenAuxiliaryEffectSlots || !p_alAuxiliaryEffectSloti || !p_alAuxiliaryEffectSlotf
|
||||
|| !p_alGenFilters || !p_alFilteri || !p_alFilterf)
|
||||
{
|
||||
Tell("L4AUDEFX: EFX entry points missing - filters and reverb inert\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
alGetError();
|
||||
p_alGenAuxiliaryEffectSlots(1, &s_reverbSlot);
|
||||
p_alGenEffects(1, &s_reverbEffect);
|
||||
if (alGetError() != AL_NO_ERROR)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// EAXReverb where available (OpenAL Soft: yes), plain reverb otherwise.
|
||||
//
|
||||
p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB);
|
||||
if (alGetError() != AL_NO_ERROR)
|
||||
{
|
||||
p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB);
|
||||
}
|
||||
p_alAuxiliaryEffectSloti(s_reverbSlot, AL_EFFECTSLOT_EFFECT, (ALint)s_reverbEffect);
|
||||
|
||||
//
|
||||
// The authentic wet level: the original sent CC91 = global_reverb_scale on
|
||||
// every 3D channel, so one global slot gain reproduces the same uniform
|
||||
// send. RP authors 0.35 (AUDIO.INI); BT used 0.3.
|
||||
//
|
||||
p_alAuxiliaryEffectSlotf(s_reverbSlot, AL_EFFECTSLOT_GAIN,
|
||||
(global_reverb_scale < 0.0f) ? 0.0f :
|
||||
(global_reverb_scale > 1.0f) ? 1.0f : global_reverb_scale);
|
||||
|
||||
//
|
||||
// LOWPASS only, deliberately. A bandpass would have been convenient -- one
|
||||
// direct filter carrying both the authored brightness model and a bass trim
|
||||
// -- but the OpenAL this game ships (Creative's, via oalinst.exe; renderer
|
||||
// reports "Generic Software") implements ONLY AL_FILTER_LOWPASS. It rejects
|
||||
// both HIGHPASS and BANDPASS, verified on the build machine. Asking for one
|
||||
// leaves an error pending, which the check below would read as total EFX
|
||||
// failure and silently take the reverb down with it.
|
||||
//
|
||||
p_alGenFilters(1, &s_scratchFilter);
|
||||
p_alFilteri(s_scratchFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
|
||||
|
||||
if (alGetError() != AL_NO_ERROR)
|
||||
{
|
||||
//
|
||||
// No usable direct filter. The reverb slot above is independent of it,
|
||||
// so keep the bridge alive and just make the filter path a no-op rather
|
||||
// than losing F11 as well.
|
||||
//
|
||||
s_scratchFilter = 0;
|
||||
Tell("L4AUDEFX: no lowpass filter available - brightness path inert\n");
|
||||
}
|
||||
|
||||
s_available = (alGetError() == AL_NO_ERROR);
|
||||
Tell("L4AUDEFX: " << (s_available ? "ready" : "failed")
|
||||
<< " (reverb slot gain " << global_reverb_scale << ")\n");
|
||||
return s_available;
|
||||
}
|
||||
|
||||
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf)
|
||||
{
|
||||
if (!s_available || s_scratchFilter == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (gainhf < 0.001f) gainhf = 0.001f;
|
||||
if (gainhf > 1.0f) gainhf = 1.0f;
|
||||
|
||||
//
|
||||
// Nothing to do at unity -- detach rather than attach a filter that would
|
||||
// only cost mixing work to achieve nothing.
|
||||
//
|
||||
if (gainhf >= 0.999f)
|
||||
{
|
||||
alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL);
|
||||
alGetError();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Filter parameters are COPIED at attach time, so one scratch filter object
|
||||
// serves every source -- no per-source filter allocation is needed.
|
||||
//
|
||||
p_alFilterf(s_scratchFilter, AL_LOWPASS_GAIN, 1.0f);
|
||||
p_alFilterf(s_scratchFilter, AL_LOWPASS_GAINHF, gainhf);
|
||||
alSourcei(source, AL_DIRECT_FILTER, (ALint)s_scratchFilter);
|
||||
alGetError();
|
||||
}
|
||||
|
||||
void EFX_AttachReverbSend(ALuint source)
|
||||
{
|
||||
if (!s_available)
|
||||
{
|
||||
return;
|
||||
}
|
||||
alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)s_reverbSlot, 0, AL_FILTER_NULL);
|
||||
}
|
||||
|
||||
void EFX_ClearSourceEffects(ALuint source)
|
||||
{
|
||||
if (!s_available)
|
||||
{
|
||||
return;
|
||||
}
|
||||
alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL);
|
||||
alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL);
|
||||
alGetError(); // swallow any property complaint
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
//###########################################################################
|
||||
//
|
||||
// L4AUDEFX.h -- OpenAL EFX bridge for the authored filter/reverb chains
|
||||
// (docs/SOUND.md, findings F9 and F11).
|
||||
//
|
||||
// The original drove the AWE32's initial-filter-cutoff NRPN (21) every frame
|
||||
// -- brightness x the distance high-frequency rolloff -- and sent CC91 reverb
|
||||
// on the 3D channels (global_reverb_scale=0.35 in RP's AUDIO.INI) while
|
||||
// keeping the cockpit DirectPatch channels dry. The OpenAL port computed
|
||||
// both and applied neither: GetHighFreqCutoffScale() had no callers at all
|
||||
// and every CC91 send site sat inside a comment block, so RP played
|
||||
// spectrally full-bright at every distance and bone-dry everywhere.
|
||||
//
|
||||
// This bridge reproduces both through OpenAL Soft's EFX extension: one
|
||||
// EAXReverb auxiliary slot plus a scratch AL_FILTER_LOWPASS whose parameters
|
||||
// are copied at attach time. Without ALC_EXT_EFX it stays inert and every
|
||||
// entry point below is a no-op, so the game still runs on a bare OpenAL.
|
||||
//
|
||||
//###########################################################################
|
||||
|
||||
#include "openal/al.h"
|
||||
|
||||
//
|
||||
// Load the EFX entry points, create the reverb slot (gain = the authored
|
||||
// global_reverb_scale) and the scratch lowpass. Call once, with the AL
|
||||
// context current. Returns false (and stays inert) without ALC_EXT_EFX.
|
||||
//
|
||||
bool EFX_Initialize(float global_reverb_scale);
|
||||
|
||||
bool EFX_Available();
|
||||
|
||||
//
|
||||
// Per-frame direct-path filter: gainhf is the linear high-frequency gain at the
|
||||
// EFX 5 kHz reference, carrying the authored brightness x distance model.
|
||||
// Callers map the AWE cutoff through EFX_CutoffScaleToGainHF below.
|
||||
//
|
||||
// At unity the filter is detached rather than attached at no-op settings.
|
||||
//
|
||||
// NOTE: this is a LOWPASS and can only ever be one. The OpenAL this game ships
|
||||
// (Creative's) implements no other filter type -- see L4AUDEFX.cpp -- so the
|
||||
// bass trim could not ride here as a bandpass GAINLF and lives in the resource
|
||||
// loader instead (RPApplyBassTrim, L4AUDRES.cpp).
|
||||
//
|
||||
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf);
|
||||
|
||||
//
|
||||
// AWE NRPN 21 curve -> EFX gainhf. cutoff_scale is [0,1] of the 100-8000 Hz
|
||||
// span; approximated as the attenuation of a 2-pole lowpass at the 5 kHz
|
||||
// reference. Curve shape is approximate, endpoints exact.
|
||||
//
|
||||
inline float EFX_CutoffScaleToGainHF(float cutoff_scale)
|
||||
{
|
||||
if (cutoff_scale < 0.0f) cutoff_scale = 0.0f;
|
||||
if (cutoff_scale > 1.0f) cutoff_scale = 1.0f;
|
||||
float cutoff_hz = 100.0f + cutoff_scale * 7900.0f;
|
||||
float g = (cutoff_hz / 5000.0f) * (cutoff_hz / 5000.0f);
|
||||
return (g > 1.0f) ? 1.0f : ((g < 0.001f) ? 0.001f : g);
|
||||
}
|
||||
|
||||
//
|
||||
// Wet-exterior routing: attach the source's auxiliary send to the reverb slot
|
||||
// (Dynamic3D / Static3D). Direct cockpit sources stay dry.
|
||||
//
|
||||
void EFX_AttachReverbSend(ALuint source);
|
||||
|
||||
//
|
||||
// Drop both the direct-path filter and the reverb send. Required when a source
|
||||
// is recycled through the pool: without it a dry cockpit sound can inherit the
|
||||
// wet send of the 3D source that used the name before it, and a full-bright
|
||||
// source can inherit a distant source's lowpass.
|
||||
//
|
||||
void EFX_ClearSourceEffects(ALuint source);
|
||||
+269
-32
@@ -2,6 +2,7 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4audio.h"
|
||||
#include "l4audefx.h"
|
||||
#include "l4audlvl.h"
|
||||
#include "l4app.h"
|
||||
#include "l4audrnd.h"
|
||||
@@ -9,6 +10,49 @@
|
||||
#include "..\munga\player.h"
|
||||
#include "..\rp\vtv.h"
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md): the AWE32 played each patch at the requested MIDI
|
||||
// note relative to the sample root (60). RP's authored 4.10 content predates
|
||||
// NoteAudioControlID -- its AudioControlID enum stops at AttackTimeAudioControlID
|
||||
// -- so every source runs at DEFAULT_NOTE and this factor is 1.0 today. It is
|
||||
// applied anyway so the pitch path is complete if authored notes ever appear,
|
||||
// and to keep the shared MUNGA engine in step with the BT tree.
|
||||
//
|
||||
static inline float RPNotePitchFactor(int note_value)
|
||||
{
|
||||
return (float)pow(2.0, ((double)note_value - 60.0) / 12.0);
|
||||
}
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F12): the authored DirectPatchSource `position=`
|
||||
// enum picked a SOUND CARD (front pair for Front/FrontLeft/FrontRight, rear
|
||||
// pair for Rear/RearLeft/RearRight) and a MIDI pan (CC10 centre/left/right).
|
||||
// The port read audioPosition from the stream and then discarded it -- every
|
||||
// cockpit sound played dead centre because SetupPatch pins each source
|
||||
// AL_SOURCE_RELATIVE at the origin.
|
||||
//
|
||||
// Sources are listener-relative and no AL_ORIENTATION is ever set, so OpenAL's
|
||||
// default listener frame applies: facing -Z with +Y up. Front is therefore
|
||||
// -Z, rear +Z, left -X, right +X; the corner values combine both at equal
|
||||
// weight. RP's own content only ever authors Front (28 sites) and Rear (13),
|
||||
// but the corners are mapped for completeness since the enum allows them.
|
||||
//
|
||||
static void RPGetDirectPatchPosition(DirectPatchPosition p, float *x, float *z)
|
||||
{
|
||||
const float diag = 0.7071068f; // unit vector split across both axes
|
||||
|
||||
switch (p)
|
||||
{
|
||||
case FrontDirectPatchPosition: *x = 0.0f; *z = -1.0f; break;
|
||||
case RearDirectPatchPosition: *x = 0.0f; *z = 1.0f; break;
|
||||
case FrontLeftDirectPatchPosition: *x = -diag; *z = -diag; break;
|
||||
case FrontRightDirectPatchPosition: *x = diag; *z = -diag; break;
|
||||
case RearLeftDirectPatchPosition: *x = -diag; *z = diag; break;
|
||||
case RearRightDirectPatchPosition: *x = diag; *z = diag; break;
|
||||
default: *x = 0.0f; *z = 0.0f; break;
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//####################### L4AudioSpatialization #########################
|
||||
//#############################################################################
|
||||
@@ -658,6 +702,19 @@ L4AudioSource::L4AudioSource(
|
||||
AudioSource(stream, entity)
|
||||
{
|
||||
channelSet.count = GetAudioVoiceCount();
|
||||
|
||||
//
|
||||
// sources[] was left uninitialized here, and RequestAudioChannels decides
|
||||
// whether a slot already holds a source by asking alIsSource about it.
|
||||
// Garbage that happened to match a live name meant silently sharing another
|
||||
// source -- a real hazard now that the pool recycles small integer names.
|
||||
// 0 is never a valid AL name.
|
||||
//
|
||||
for (int i = 0; i < (int)(sizeof(channelSet.sources) / sizeof(channelSet.sources[0])); i++)
|
||||
{
|
||||
channelSet.sources[i] = 0;
|
||||
}
|
||||
|
||||
L4AudioSourceX();
|
||||
}
|
||||
|
||||
@@ -923,6 +980,22 @@ void
|
||||
patch_resource->SetDistance(GetDistanceToSource());
|
||||
patch_resource->SetupPatch(channelSet);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F12): place the source per the authored position
|
||||
// enum. SetupPatch has just pinned it AL_SOURCE_RELATIVE at the origin, so
|
||||
// this must run after it. With AL_NONE as the distance model the unit
|
||||
// radius costs no attenuation -- it only supplies direction.
|
||||
//
|
||||
{
|
||||
float pos_x, pos_z;
|
||||
|
||||
RPGetDirectPatchPosition(audioPosition, &pos_x, &pos_z);
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
alSource3f(channelSet.sources[i], AL_POSITION, pos_x, 0.0f, pos_z);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Set the channel to default control values
|
||||
//
|
||||
@@ -1039,6 +1112,8 @@ void
|
||||
// Apply filter scale
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
float direct_gainhf = 1.0f;
|
||||
|
||||
if (UseSourceBrightnessScale())
|
||||
{
|
||||
const MIDINRPNValue filter_resolution = 2;// HACK - should come from audio.ini
|
||||
@@ -1058,6 +1133,32 @@ void
|
||||
{
|
||||
lastMIDIFilterCutoff = midi_filter_cutoff;
|
||||
}
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9): this block previously computed the AWE
|
||||
// initial-filter-cutoff (NRPN 21) and then only updated its own
|
||||
// bookkeeping member -- the cutoff was never applied to anything, so
|
||||
// authored brightness (ctl 5) was inert. Route it through EFX instead.
|
||||
// Direct sources take brightness alone; the distance rolloff belongs to
|
||||
// the 3D paths.
|
||||
//
|
||||
direct_gainhf = EFX_CutoffScaleToGainHF(
|
||||
(float)midi_filter_cutoff / (float)MIDI_MAX_CONTROL_VALUE
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Applied OUTSIDE the brightness gate: a source that does not use brightness
|
||||
// still has to be told, because the same call carries the player's bass trim.
|
||||
// At unity on both axes it detaches the filter, so this costs nothing in the
|
||||
// default configuration.
|
||||
//
|
||||
if (EFX_Available())
|
||||
{
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
EFX_SetSourceLowpassGainHF(channelSet.sources[i], direct_gainhf);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1069,17 +1170,24 @@ void
|
||||
const MIDIValue volume_resolution = 2; // HACK - should come from audio.ini
|
||||
|
||||
volume_scale = CalculateSourceVolumeScale();
|
||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
||||
Check(application);
|
||||
L4AudioRenderer *audio_renderer =
|
||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
||||
Check(audio_renderer);
|
||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
||||
Check(audio_head);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F4): the original ended its volume path in MIDI
|
||||
// CC7, whose GM/SoundFont curve is concave -- amplitude ~ (v/127)^2. Writing
|
||||
// volume_scale linearly to AL_GAIN played every intermediate level about
|
||||
// +6 dB hot at mid-scale and compressed the authored dynamic range.
|
||||
//
|
||||
// AL_MAX_DISTANCE is no longer written here: the distance model is AL_NONE
|
||||
// (see MUNGA/AUDIO.cpp) so it has no effect, and DirectPatch is the
|
||||
// non-positional cockpit path which never took distance attenuation anyway.
|
||||
//
|
||||
const float direct_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||
PatchResource *direct_patch = Cast_Object(PatchResource*, GetAudioResource());
|
||||
for (int i=0; i < channelSet.count; i++)
|
||||
{
|
||||
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
|
||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN,
|
||||
volume_scale * volume_scale * direct_patch->GetZoneBassGain(i));
|
||||
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * direct_note_pitch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1206,6 +1314,17 @@ void
|
||||
patch_resource->SetDistance(GetDistanceToSource());
|
||||
patch_resource->SetupPatch(channelSet);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F11): wet exterior. The original sent CC91 =
|
||||
// global_reverb_scale on all four channels of a 3D source and CC91 = 0 on
|
||||
// the cockpit DirectPatch channels -- a deliberate outside/inside contrast
|
||||
// that the port lost when every send site was commented out.
|
||||
//
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
EFX_AttachReverbSend(channelSet.sources[i]);
|
||||
}
|
||||
|
||||
/*patch_resource->SetDistance(GetDistanceToSource());
|
||||
for (i = 0; i < AudioChannelSetSize; i++)
|
||||
{
|
||||
@@ -1405,15 +1524,71 @@ void
|
||||
|
||||
pitch_offset = CalculateSourcePitchOffset();
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F10): add the AUTHORED doppler. AUDIO.INI's
|
||||
// doppler_range=600 / speed_of_sound=250 are computed into
|
||||
// AudioLocation::dopplerCents on every spatial update, and the original
|
||||
// applied it on this dynamic path only -- static and direct sources stayed
|
||||
// doppler-free. GetDopplerCents() previously had no callers at all.
|
||||
//
|
||||
pitch_offset += GetAudioLocation()->GetDopplerCents();
|
||||
|
||||
double relativePitch = pow(2.0,pitch_offset/1200.0);
|
||||
Clamp(relativePitch,0.5,2.0);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md): relativePitch was computed here and never
|
||||
// applied -- there was no AL_PITCH call anywhere in the tree, so the whole
|
||||
// authored pitch chain (pitch_mix_offset / PitchAudioControlID, authored 97
|
||||
// times across RP's sequences) was inert along with doppler.
|
||||
//
|
||||
// AL_VELOCITY is still written for bookkeeping but is now inert: doppler
|
||||
// factor is 0 (see MUNGA/AUDIO.cpp) because this feed is sign-inverted
|
||||
// relative to the AL_POSITION frame and never subtracted head velocity.
|
||||
// AL_MAX_DISTANCE is dropped -- the distance model is AL_NONE and the
|
||||
// authored curve is applied in CalculateSourceVolumeScale instead.
|
||||
//
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9): the AUTHORED high-frequency rolloff. The
|
||||
// original drove the AWE filter cutoff on this path from
|
||||
// highFreqCutoffScale x brightnessScale, ungated, on all four quadrant
|
||||
// channels -- every moving 3D sound got duller with distance. AUDIO.INI
|
||||
// still computes highFreqCutoffScale each frame (rolloff 2.0, knee 60,
|
||||
// scale 0.005) and GetHighFreqCutoffScale() previously had zero callers.
|
||||
//
|
||||
float dynamic_gainhf = 1.0f;
|
||||
|
||||
if (EFX_Available())
|
||||
{
|
||||
PatchResource *filter_patch =
|
||||
Cast_Object(PatchResource*, GetAudioResource());
|
||||
Check(filter_patch);
|
||||
|
||||
Scalar filter_scale =
|
||||
GetAudioLocation()->GetHighFreqCutoffScale() *
|
||||
CalculateSourceBrightnessScale();
|
||||
Scalar max_cutoff = (Scalar)filter_patch->GetMaxMIDIFilterCutoff();
|
||||
Scalar midi_cutoff = filter_scale * max_cutoff;
|
||||
|
||||
dynamic_gainhf = EFX_CutoffScaleToGainHF(
|
||||
(float)(midi_cutoff / (Scalar)MIDI_MAX_CONTROL_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
const float dynamic_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||
PatchResource *dynamic_patch = Cast_Object(PatchResource*, GetAudioResource());
|
||||
for (int i=0; i < channelSet.count; i++)
|
||||
{
|
||||
alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN,
|
||||
volume_scale * volume_scale * dynamic_patch->GetZoneBassGain(i));
|
||||
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * dynamic_note_pitch);
|
||||
alSource3f(channelSet.sources[i],AL_VELOCITY,-relative_velocity.x,-relative_velocity.y,-relative_velocity.z);
|
||||
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
|
||||
|
||||
if (EFX_Available())
|
||||
{
|
||||
EFX_SetSourceLowpassGainHF(channelSet.sources[i], dynamic_gainhf);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1430,24 +1605,27 @@ AudioControlValue
|
||||
//
|
||||
// Call inherited method to calculate volume scale
|
||||
//
|
||||
Scalar
|
||||
Scalar
|
||||
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||
return volume_scale;
|
||||
|
||||
//
|
||||
// Update the spatial model that will result in the value
|
||||
// for distance related volume attenuation
|
||||
//
|
||||
/*Check(application);
|
||||
Check(application->GetAudioRenderer());
|
||||
UpdateSpatialModel(application->GetAudioRenderer()->GetAudioHead());
|
||||
|
||||
//
|
||||
// Apply distance attenuation to the volume scale
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F3): apply the AUTHORED distance attenuation.
|
||||
// AUDIO.INI's knee/rolloff curve (amplitude_rolloff=2.0, knee=60,
|
||||
// distance_scale=0.003, clipping_radius=550) is computed into
|
||||
// distanceVolumeScale on every spatial update; this multiply was commented
|
||||
// out behind an early return and AL_LINEAR_DISTANCE substituted, which faded
|
||||
// distant audio on a straight line to zero instead of the authored
|
||||
// 1/(1+(k(d-knee))^2). Restoring it also un-blinds the volume-based
|
||||
// transient cull, the AudioWeighting voice-steal, and the CalculateMix
|
||||
// ducking chain, all of which key off this value and were treating far
|
||||
// sources as full-presence.
|
||||
//
|
||||
// The spatial model is already refreshed each Execute, so the
|
||||
// UpdateSpatialModel call the original comment carried is not needed here.
|
||||
//
|
||||
Check(GetAudioLocation());
|
||||
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||
return volume_scale;*/
|
||||
return volume_scale;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
@@ -1468,6 +1646,26 @@ Static3DPatchSource::Static3DPatchSource(
|
||||
MemoryStream_Read(stream, &useInternalSpatialization);
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
//#############################################################################
|
||||
//
|
||||
AudioControlValue
|
||||
Static3DPatchSource::CalculateSourceVolumeScale()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F3): same authored distance attenuation as the
|
||||
// dynamic path. The spatial model computes distanceVolumeScale on every
|
||||
// execute; without this multiply statics were left to AL_LINEAR_DISTANCE.
|
||||
//
|
||||
Scalar volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||
Check(GetAudioLocation());
|
||||
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||
return volume_scale;
|
||||
}
|
||||
|
||||
Logical Static3DPatchSource::IsAudioSourceClipped(AudioHead *audio_head)
|
||||
{
|
||||
if (AudioSource::IsAudioSourceClipped(audio_head) || l4_application->GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() == VTV::BurningState)
|
||||
@@ -1694,6 +1892,16 @@ void
|
||||
Check(patch_resource);
|
||||
patch_resource->SetDistance(GetDistanceToSource());
|
||||
patch_resource->SetupPatch(channelSet);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F11): statics are exterior sources too, so they
|
||||
// take the same wet send as the dynamic path.
|
||||
//
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
EFX_AttachReverbSend(channelSet.sources[i]);
|
||||
}
|
||||
|
||||
/*for (i = 0; i < AudioChannelSetSize; i++)
|
||||
{
|
||||
if ((channel = channelSet.GetNth(i)) != NULL)
|
||||
@@ -1909,12 +2117,6 @@ void
|
||||
|
||||
Scalar volume_scale = CalculateSourceVolumeScale();
|
||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
||||
Check(application);
|
||||
L4AudioRenderer *audio_renderer =
|
||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
||||
Check(audio_renderer);
|
||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
||||
Check(audio_head);
|
||||
|
||||
Scalar pitch_offset;
|
||||
|
||||
@@ -1933,12 +2135,47 @@ void
|
||||
relative_position = audio_location->GetVectorToSource();
|
||||
}
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F4 + pitch): squared CC7 volume law, and the
|
||||
// authored pitch chain applied -- see the DirectPatch/Dynamic3D paths. The
|
||||
// original left static sources doppler-free, so no doppler term here.
|
||||
// AL_MAX_DISTANCE dropped with the AL_NONE distance model; the authored
|
||||
// curve is applied in CalculateSourceVolumeScale.
|
||||
//
|
||||
//Static models have their position freely available as relative positions and stand still
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9): statics took brightness alone in the
|
||||
// original -- no distance term on this path.
|
||||
//
|
||||
float static_gainhf = 1.0f;
|
||||
|
||||
if (EFX_Available() && UseSourceBrightnessScale())
|
||||
{
|
||||
PatchResource *filter_patch =
|
||||
Cast_Object(PatchResource*, GetAudioResource());
|
||||
Check(filter_patch);
|
||||
|
||||
Scalar midi_cutoff =
|
||||
CalculateSourceBrightnessScale() *
|
||||
(Scalar)filter_patch->GetMaxMIDIFilterCutoff();
|
||||
|
||||
static_gainhf = EFX_CutoffScaleToGainHF(
|
||||
(float)(midi_cutoff / (Scalar)MIDI_MAX_CONTROL_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
const float static_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||
for (int i=0; i < channelSet.count; i++)
|
||||
{
|
||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN,
|
||||
volume_scale * volume_scale * patch_resource->GetZoneBassGain(i));
|
||||
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * static_note_pitch);
|
||||
alSource3f(channelSet.sources[i],AL_POSITION,relative_position.x,relative_position.y,relative_position.z);
|
||||
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
|
||||
|
||||
if (EFX_Available())
|
||||
{
|
||||
EFX_SetSourceLowpassGainHF(channelSet.sources[i], static_gainhf);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -541,6 +541,13 @@ public:
|
||||
|
||||
virtual Logical IsAudioSourceClipped(AudioHead *audio_head);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Mix levels
|
||||
//
|
||||
public:
|
||||
AudioControlValue
|
||||
CalculateSourceVolumeScale();
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// SetPosition
|
||||
//
|
||||
|
||||
+34
-1
@@ -128,8 +128,25 @@ void
|
||||
// #endif
|
||||
SAMPLEINFO info;
|
||||
|
||||
//
|
||||
// Ask this patch for no more zones than it has.
|
||||
//
|
||||
// sourceSet.count was fixed when the audio source was built, from
|
||||
// whichever level of detail was selected at the time. SetDistance
|
||||
// re-picks the level of detail by distance immediately before this
|
||||
// runs (see Static3DPatchSource::StartImplementation), and a
|
||||
// further-away patch can have fewer zones than the one the source was
|
||||
// sized for - so the count outruns this patch's zone list, and the
|
||||
// zones past the end come back as "no such zone".
|
||||
//
|
||||
int zone_count = PRESET_getNumSamples(bankID,patchID);
|
||||
if (zone_count > sourceSet.count)
|
||||
{
|
||||
zone_count = sourceSet.count;
|
||||
}
|
||||
|
||||
//Attach buffers
|
||||
for (int i=0; i < sourceSet.count; i++)
|
||||
for (int i=0; i < zone_count; i++)
|
||||
{
|
||||
info = PRESET_getSampleInfo(bankID,patchID,i);
|
||||
if (info.bufferIndex >= 0)
|
||||
@@ -310,3 +327,19 @@ MIDINRPNValue
|
||||
Check(patch_level_of_detail);
|
||||
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
//#############################################################################
|
||||
//
|
||||
float
|
||||
PatchResource::GetZoneBassGain(int zone_index)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
PatchLevelOfDetail *patch_level_of_detail =
|
||||
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
||||
|
||||
Check(patch_level_of_detail);
|
||||
return patch_level_of_detail->GetZoneBassGain(zone_index);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ struct PRESETINFO
|
||||
|
||||
extern PRESETINFO allPresets[2][100];
|
||||
|
||||
//
|
||||
// Defined in L4AUDRES.cpp; declared here rather than including that header so
|
||||
// the level-of-detail and resource headers stay independent of each other.
|
||||
//
|
||||
float RPBufferBassGain(int buffer_index);
|
||||
|
||||
bool PRESET_isImplemented(int bank, int preset);
|
||||
int PRESET_getNumSamples(int bank, int preset);
|
||||
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
|
||||
@@ -69,6 +75,14 @@ public:
|
||||
GetVoiceCount()
|
||||
{return PRESET_getNumSamples(bankID,patchID);}
|
||||
|
||||
//
|
||||
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||
//
|
||||
float
|
||||
GetZoneBassGain(int zone_index)
|
||||
{return RPBufferBassGain(
|
||||
PRESET_getSampleInfo(bankID,patchID,zone_index).bufferIndex);}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------------
|
||||
// BuildFromPage
|
||||
@@ -163,6 +177,12 @@ public:
|
||||
void
|
||||
SetupPatch(SourceSet sourceSet);
|
||||
|
||||
//
|
||||
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||
//
|
||||
float
|
||||
GetZoneBassGain(int zone_index);
|
||||
|
||||
MIDINRPNValue
|
||||
GetMaxMIDIFilterCutoff();
|
||||
};
|
||||
|
||||
@@ -18,6 +18,130 @@
|
||||
ALuint *g_buffers;
|
||||
int g_numBuffers;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Bass trim ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// RP412AUDIOBASS, 0.0..1.0, default 1.0 (the mix exactly as authored), stepped
|
||||
// live by the Home/End keys.
|
||||
//
|
||||
// The arcade pod ran the game at unity and did its volume and tone shaping in
|
||||
// hardware -- an external amplifier and a 3-way crossover. A desktop player has
|
||||
// neither, so the low band needs a control in software. This is the crossover's
|
||||
// low trim; the master volume (L4AUDRND.cpp) is the amplifier's.
|
||||
//
|
||||
// It cannot be an EFX filter: the OpenAL this game ships implements only
|
||||
// AL_FILTER_LOWPASS, so there is no low shelf or bandpass to lean on, and the
|
||||
// one direct filter a source gets is already carrying the authored brightness
|
||||
// model. So the trim is a GAIN, applied per zone in the mix.
|
||||
//
|
||||
// That works because of HOW the low end is built. RP's soundbanks carry their
|
||||
// weight in discrete deep layer zones whose per-zone tuning bakes out to a very
|
||||
// low playback rate -- 13 zones sit below 8 kHz, between 3.4 and 5.2 octaves
|
||||
// below their recorded pitch, against 81% of the set at 22 kHz and up. A zone's
|
||||
// baked rate is therefore a reliable proxy for which band it occupies, so
|
||||
// attenuating the low-rate zones is a genuine low-band trim rather than a blunt
|
||||
// overall cut.
|
||||
//
|
||||
// Ramp: untouched at or above 22050 Hz, full trim at or below 5512 Hz, log
|
||||
// interpolated between, so nothing steps abruptly at a threshold. Each buffer's
|
||||
// DEPTH is fixed at load; the trim itself is read at mix time, which is what
|
||||
// lets the keys move it while sounds are playing.
|
||||
//
|
||||
static const ALsizei kBassTrimFullRate = 5512; // at/below: full trim
|
||||
static const ALsizei kBassTrimNoneRate = 22050; // at/above: untouched
|
||||
static const char kBassTrimFile[] = "bass.cfg";
|
||||
static const float kBassTrimStep = 0.05f;
|
||||
|
||||
static float *g_bufferBassDepth = NULL; // one per loaded buffer
|
||||
static float g_bassTrim = 1.0f;
|
||||
|
||||
//
|
||||
// How much of the trim a buffer at this rate takes: 0 = untouched, 1 = fully.
|
||||
//
|
||||
static float
|
||||
RPBassDepthForRate(ALsizei rate)
|
||||
{
|
||||
if (rate >= kBassTrimNoneRate) return 0.0f;
|
||||
if (rate <= kBassTrimFullRate) return 1.0f;
|
||||
|
||||
const float span = (float)log((double)kBassTrimNoneRate / (double)kBassTrimFullRate);
|
||||
|
||||
return (float)log((double)kBassTrimNoneRate / (double)rate) / span;
|
||||
}
|
||||
|
||||
void
|
||||
RPBassTrimInitialize()
|
||||
{
|
||||
g_bassTrim = 1.0f;
|
||||
|
||||
if (const char *setting = getenv("RP412AUDIOBASS"))
|
||||
{
|
||||
float value = (float)atof(setting);
|
||||
|
||||
if (value >= 0.0f && value <= 1.0f)
|
||||
{
|
||||
g_bassTrim = value;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Whatever the player last set with the keys wins, exactly as the master
|
||||
// volume behaves -- environ.ini only decides where an untouched machine
|
||||
// starts out.
|
||||
//
|
||||
if (FILE *cfg = fopen(kBassTrimFile, "rt"))
|
||||
{
|
||||
float value = -1.0f;
|
||||
|
||||
if (fscanf(cfg, "%f", &value) == 1 && value >= 0.0f && value <= 1.0f)
|
||||
{
|
||||
g_bassTrim = value;
|
||||
}
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
void
|
||||
RPBassTrimStep(int direction)
|
||||
{
|
||||
g_bassTrim += (direction > 0) ? kBassTrimStep : -kBassTrimStep;
|
||||
|
||||
if (g_bassTrim < 0.0f) g_bassTrim = 0.0f;
|
||||
if (g_bassTrim > 1.0f) g_bassTrim = 1.0f;
|
||||
|
||||
g_bassTrim = (float)((int)(g_bassTrim / kBassTrimStep + 0.5f)) * kBassTrimStep;
|
||||
|
||||
if (FILE *cfg = fopen(kBassTrimFile, "wt"))
|
||||
{
|
||||
fprintf(cfg, "%.2f\n", g_bassTrim);
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
float
|
||||
RPBassTrim()
|
||||
{
|
||||
return g_bassTrim;
|
||||
}
|
||||
|
||||
//
|
||||
// The gain a zone takes at the current trim. 1.0 whenever the player has not
|
||||
// touched it, so the default costs one multiply by one.
|
||||
//
|
||||
float
|
||||
RPBufferBassGain(int buffer_index)
|
||||
{
|
||||
if (g_bassTrim >= 0.999f || g_bufferBassDepth == NULL
|
||||
|| buffer_index < 0 || buffer_index >= g_numBuffers)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
return 1.0f - (1.0f - g_bassTrim) * g_bufferBassDepth[buffer_index];
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//####################### AudioObjectStream #############################
|
||||
//#############################################################################
|
||||
@@ -566,6 +690,18 @@ void
|
||||
g_buffers = NULL;
|
||||
g_numBuffers = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// Parallel to g_buffers: how much of the bass trim each zone takes.
|
||||
//
|
||||
RPBassTrimInitialize();
|
||||
g_bufferBassDepth = new float[g_numBuffers];
|
||||
for (int b = 0; b < g_numBuffers; b++)
|
||||
{
|
||||
g_bufferBassDepth[b] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int bufferInd = 0;
|
||||
@@ -647,6 +783,15 @@ void
|
||||
sf_read_raw(file,data,size);
|
||||
sf_close(file);
|
||||
|
||||
//
|
||||
// Record which band this zone sits in, for the Home/End bass
|
||||
// trim. Fixed per buffer; the trim itself is read at mix time.
|
||||
//
|
||||
if (g_bufferBassDepth != NULL)
|
||||
{
|
||||
g_bufferBassDepth[bufferInd] = RPBassDepthForRate(alSampleRate);
|
||||
}
|
||||
|
||||
//Feed the buffer
|
||||
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
|
||||
PRESET_setBufferIndex(i,j,k,bufferInd);
|
||||
@@ -726,6 +871,16 @@ void
|
||||
|
||||
ALuint AL_getBuffer(int index)
|
||||
{
|
||||
//
|
||||
// 0 is AL_NONE - "no buffer" - which alSourcei accepts and which detaches
|
||||
// the source rather than crashing. An index that is out of range means a
|
||||
// zone that does not exist, and the only thing an unchecked lookup here
|
||||
// can do about it is read whatever lies past the array.
|
||||
//
|
||||
if (g_buffers == NULL || index < 0 || index >= g_numBuffers)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return g_buffers[index];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,16 @@ ALuint AL_getBuffer(int index);
|
||||
extern ALuint *g_buffers;
|
||||
extern int g_numBuffers;
|
||||
|
||||
//
|
||||
// RP412AUDIOBASS low-band trim, stepped live by the Home/End keys. Applied as
|
||||
// a per-zone gain in the mix; see the comment block in L4AUDRES.cpp for why it
|
||||
// lives here and not in EFX.
|
||||
//
|
||||
void RPBassTrimInitialize();
|
||||
void RPBassTrimStep(int direction);
|
||||
float RPBassTrim();
|
||||
float RPBufferBassGain(int buffer_index);
|
||||
|
||||
|
||||
//class AudioHardware;
|
||||
|
||||
|
||||
+311
-28
@@ -2,9 +2,20 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4audrnd.h"
|
||||
#include "l4audefx.h"
|
||||
#include "..\munga\notation.h"
|
||||
#include "openal/alc.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
//
|
||||
// Master volume limits, shared by the startup load and the PgUp/PgDn step.
|
||||
// The file sits beside the exe with the other runtime state.
|
||||
//
|
||||
static const char kAudioVolumeFile[] = "volume.cfg";
|
||||
static const float kAudioVolumeStep = 0.05f;
|
||||
static const float kAudioVolumeMax = 2.0f;
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// L4AudioRenderer
|
||||
@@ -379,6 +390,66 @@ void
|
||||
{
|
||||
ALCcontext *context = alcCreateContext(device,NULL);
|
||||
alcMakeContextCurrent(context);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9/F11): bring up the EFX bridge that carries
|
||||
// the authored brightness/distance lowpass and the wet-exterior reverb
|
||||
// send. Needs the context current, and the reverb gain has already been
|
||||
// read from AUDIO.INI into the head above. Inert without ALC_EXT_EFX.
|
||||
//
|
||||
EFX_Initialize(audio_head->GetGlobalReverbScale());
|
||||
|
||||
//
|
||||
// Master volume. There was no listener gain at all before -- the mix
|
||||
// always ran at unity -- so restoring the authored dynamics gave players
|
||||
// no way to pull the whole thing down. This lives in environ.ini rather
|
||||
// than AUDIO.INI deliberately: AUDIO.INI is byte-identical to the file
|
||||
// that shipped in 1995 and is worth keeping that way.
|
||||
//
|
||||
// Default is 1.0, i.e. exactly the previous behaviour -- the knob only
|
||||
// does something when someone asks for it.
|
||||
//
|
||||
{
|
||||
float master_volume = 1.0f;
|
||||
|
||||
if (const char *setting = getenv("RP412AUDIOVOLUME"))
|
||||
{
|
||||
float value = (float)atof(setting);
|
||||
|
||||
if (value >= 0.0f && value <= kAudioVolumeMax)
|
||||
{
|
||||
master_volume = value;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Whatever the player last set with the volume keys wins over the
|
||||
// environ.ini figure: the keys are the amplifier knob, and a knob
|
||||
// stays where it was left. environ.ini sets where it starts on a
|
||||
// machine that has never been touched.
|
||||
//
|
||||
if (FILE *cfg = fopen(kAudioVolumeFile, "rt"))
|
||||
{
|
||||
float value = -1.0f;
|
||||
|
||||
if (fscanf(cfg, "%f", &value) == 1
|
||||
&& value >= 0.0f && value <= kAudioVolumeMax)
|
||||
{
|
||||
master_volume = value;
|
||||
}
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
gRPMasterVolume = master_volume;
|
||||
alListenerf(AL_GAIN, master_volume);
|
||||
Tell("Audio master volume " << (int)(master_volume * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
//
|
||||
// The bass trim is not set here: it is a per-zone gain owned by the
|
||||
// resource manager (L4AUDRES.cpp), which needs the buffers to exist
|
||||
// first. PreloadResources initialises it below.
|
||||
//
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1257,6 +1328,190 @@ Logical
|
||||
return resources_available;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Master volume ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// The pod ran at unity and left volume to an external amplifier, so the game
|
||||
// never had a level control. Standing in for that amplifier means the player
|
||||
// needs to reach it while playing, not only through environ.ini -- hence the
|
||||
// PgUp/PgDn binding in L4Application::KeyCommandMessageHandler.
|
||||
//
|
||||
// Page keys specifically: they produce no typed character, so they cannot
|
||||
// collide with any of the engine's character-keyed commands the way '+'/'-'
|
||||
// would, they are bound to nothing in any RP layout, and they exist on
|
||||
// tenkeyless keyboards.
|
||||
//
|
||||
float gRPMasterVolume = 1.0f;
|
||||
|
||||
void
|
||||
RPAudioMasterVolumeStep(int direction)
|
||||
{
|
||||
gRPMasterVolume += (direction > 0) ? kAudioVolumeStep : -kAudioVolumeStep;
|
||||
|
||||
if (gRPMasterVolume < 0.0f) gRPMasterVolume = 0.0f;
|
||||
if (gRPMasterVolume > kAudioVolumeMax) gRPMasterVolume = kAudioVolumeMax;
|
||||
|
||||
//
|
||||
// Snap to the step grid so repeated presses cannot drift on float error and
|
||||
// land somewhere that never reads back as a round number.
|
||||
//
|
||||
gRPMasterVolume =
|
||||
(float)((int)(gRPMasterVolume / kAudioVolumeStep + 0.5f)) * kAudioVolumeStep;
|
||||
|
||||
alListenerf(AL_GAIN, gRPMasterVolume);
|
||||
|
||||
//
|
||||
// Persist immediately. A pod operator setting the level expects it to still
|
||||
// be there after the cabinet is power-cycled, and there is no settings UI to
|
||||
// hang it off.
|
||||
//
|
||||
if (FILE *cfg = fopen(kAudioVolumeFile, "wt"))
|
||||
{
|
||||
fprintf(cfg, "%.2f\n", gRPMasterVolume);
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
Tell("Audio master volume " << (int)(gRPMasterVolume * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OpenAL source pool ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// Sources are expensive to create and destroy and are a HARD per-context
|
||||
// resource (this driver grants 256 mono). Generating one per sound event and
|
||||
// deleting it on release burns through that ceiling during busy play even
|
||||
// though steady-state demand is modest, which shows up as sounds silently
|
||||
// failing to start. Generate once, recycle forever.
|
||||
//
|
||||
// The cap sits below the driver grant with a reserve, so growth stops on our
|
||||
// terms rather than on an alGenSources failure. Growth also stops by itself if
|
||||
// a driver offers fewer sources than the cap -- a failed generate simply ends
|
||||
// growth and the pool recycles what it already has.
|
||||
//
|
||||
static const int kAudioPoolMax = 512; // free-list array size
|
||||
static const int kAudioPoolCap = 240; // grow no further than this
|
||||
|
||||
static ALuint gAudioPoolFree[kAudioPoolMax];
|
||||
static int gAudioPoolFreeCount = 0; // entries parked in gAudioPoolFree
|
||||
static int gAudioPoolTotal = 0; // sources ever generated (<= cap)
|
||||
static long gAudioPoolReuses = 0; // diagnostics
|
||||
|
||||
int RPAudioPoolSize() { return gAudioPoolTotal; }
|
||||
int RPAudioPoolFree() { return gAudioPoolFreeCount; }
|
||||
long RPAudioPoolReuses() { return gAudioPoolReuses; }
|
||||
|
||||
//
|
||||
// Reset a source to a neutral state so nothing carries across owners.
|
||||
//
|
||||
static void
|
||||
RPAudioScrubSource(ALuint src)
|
||||
{
|
||||
ALint state = AL_STOPPED;
|
||||
|
||||
alGetSourcei(src, AL_SOURCE_STATE, &state);
|
||||
if (state == AL_PLAYING || state == AL_PAUSED)
|
||||
{
|
||||
alSourceStop(src);
|
||||
}
|
||||
|
||||
alSourcei(src, AL_BUFFER, 0); // detach (nothing is queued here)
|
||||
alSourcei(src, AL_LOOPING, AL_FALSE); // or the next owner inherits a loop
|
||||
alSourcef(src, AL_GAIN, 1.0f);
|
||||
alSourcef(src, AL_PITCH, 1.0f);
|
||||
alSourcei(src, AL_SOURCE_RELATIVE, AL_FALSE);
|
||||
alSource3f(src, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
||||
alSource3f(src, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
//
|
||||
// Drop the EFX state too. Without this a recycled name can carry a 3D
|
||||
// source's reverb send into a dry cockpit sound, or a distant source's
|
||||
// lowpass into a close one.
|
||||
//
|
||||
EFX_ClearSourceEffects(src);
|
||||
|
||||
alGetError(); // swallow any property complaint
|
||||
}
|
||||
|
||||
//
|
||||
// Hand out a source: recycle first, generate only while under the cap.
|
||||
// False means genuinely out, and the caller retries after the steal loop runs.
|
||||
//
|
||||
Logical
|
||||
RPAudioPoolAcquire(ALuint *out)
|
||||
{
|
||||
Check_Pointer(out);
|
||||
|
||||
while (gAudioPoolFreeCount > 0)
|
||||
{
|
||||
ALuint src = gAudioPoolFree[--gAudioPoolFreeCount];
|
||||
|
||||
if (alIsSource(src)) // a context reset invalidates names
|
||||
{
|
||||
++gAudioPoolReuses;
|
||||
*out = src;
|
||||
return True;
|
||||
}
|
||||
--gAudioPoolTotal; // stale name: forget it
|
||||
}
|
||||
|
||||
if (gAudioPoolTotal >= kAudioPoolCap)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
|
||||
ALuint src = 0;
|
||||
|
||||
alGetError();
|
||||
alGenSources(1, &src);
|
||||
if (alGetError() != AL_NO_ERROR || !alIsSource(src))
|
||||
{
|
||||
return False; // driver said no before our cap
|
||||
}
|
||||
|
||||
++gAudioPoolTotal;
|
||||
|
||||
#if DEBUG_LEVEL>0
|
||||
{
|
||||
//
|
||||
// One line per high-water band, so a log shows how close real play gets
|
||||
// to the ceiling without spamming.
|
||||
//
|
||||
static int s_notified = 0;
|
||||
|
||||
if (gAudioPoolTotal >= s_notified + 25)
|
||||
{
|
||||
s_notified = gAudioPoolTotal;
|
||||
Tell("Audio source pool high-water: " << gAudioPoolTotal
|
||||
<< " of " << kAudioPoolCap << "\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
*out = src;
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
// Take a source back. Scrubbed and parked, never deleted.
|
||||
//
|
||||
void
|
||||
RPAudioPoolRelease(ALuint src)
|
||||
{
|
||||
if (!alIsSource(src))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RPAudioScrubSource(src);
|
||||
|
||||
if (gAudioPoolFreeCount < kAudioPoolMax)
|
||||
{
|
||||
gAudioPoolFree[gAudioPoolFreeCount++] = src;
|
||||
return;
|
||||
}
|
||||
|
||||
alDeleteSources(1, &src); // unreachable: cap < array size
|
||||
--gAudioPoolTotal;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// RequestAudioChannels
|
||||
@@ -1271,30 +1526,54 @@ Logical
|
||||
Check(this);
|
||||
Check(source_request);
|
||||
|
||||
//Do we have enough?
|
||||
//
|
||||
// SOURCE POOLING (docs/SOUND.md). This used to alGenSources per sound
|
||||
// event, with ReleaseSourceSet alDeleteSources'ing on release -- so play
|
||||
// activity CHURNED through OpenAL's per-context source limit (the driver
|
||||
// grants 256 mono here). Recovering the soundbanks took the voice count
|
||||
// per sound from about 1.1 zones to about 2.6, roughly doubling that churn.
|
||||
//
|
||||
// The BT tree measured this exact problem: raising the budget was NOT the
|
||||
// fix, recycling was, and it was a net CPU win besides. Sources are now
|
||||
// generated once and handed back to a free list, so steady-state play costs
|
||||
// no allocation at all.
|
||||
//
|
||||
int requested = source_request->count;
|
||||
|
||||
bool failed = true;
|
||||
|
||||
alGetError();
|
||||
if (requested > (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0])))
|
||||
{
|
||||
requested = (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0]));
|
||||
source_request->count = requested;
|
||||
}
|
||||
|
||||
for (int i = 0; i < requested; i++)
|
||||
{
|
||||
if (!alIsSource(source_request->sources[i]))
|
||||
if (source_request->sources[i] != 0 && alIsSource(source_request->sources[i]))
|
||||
{
|
||||
alGenSources(1, source_request->sources + i);
|
||||
continue; // slot already holds a live source
|
||||
}
|
||||
}
|
||||
|
||||
ALenum error = alGetError();
|
||||
if (error == AL_NO_ERROR)
|
||||
{
|
||||
failed = false;
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
return False;
|
||||
ALuint src = 0;
|
||||
|
||||
if (!RPAudioPoolAcquire(&src))
|
||||
{
|
||||
//
|
||||
// Out of sources. Hand back everything acquired on THIS attempt so a
|
||||
// failed request cannot strand voices -- the renderer's steal loop
|
||||
// will free some and retry.
|
||||
//
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
if (source_request->sources[j] != 0)
|
||||
{
|
||||
RPAudioPoolRelease(source_request->sources[j]);
|
||||
source_request->sources[j] = 0;
|
||||
}
|
||||
}
|
||||
return False;
|
||||
}
|
||||
|
||||
source_request->sources[i] = src;
|
||||
}
|
||||
|
||||
return True;
|
||||
@@ -1375,23 +1654,27 @@ Logical
|
||||
|
||||
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
||||
{
|
||||
//
|
||||
// SOURCE POOLING (docs/SOUND.md): park each source on the free list rather
|
||||
// than destroying it. RPAudioPoolRelease stops it, detaches its buffer and
|
||||
// scrubs the state -- including the EFX filter and reverb send -- so the
|
||||
// next owner starts clean.
|
||||
//
|
||||
// The bulk alDeleteSources(count, sources) this replaces was also a leak
|
||||
// waiting to happen: per the AL spec it is ATOMIC, so ONE invalid name in
|
||||
// the array (an empty slot of a partial set, or the old -1 sentinel on a
|
||||
// double release) meant NOTHING was deleted and the whole set leaked.
|
||||
// Slots are parked at 0, which is never a valid AL name -- unlike -1, which
|
||||
// alIsSource would be asked about as 0xFFFFFFFF.
|
||||
//
|
||||
for (int i = 0; i < sourceSet.count; i++)
|
||||
{
|
||||
ALenum state;
|
||||
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
|
||||
|
||||
if (state == AL_PLAYING)
|
||||
if (sourceSet.sources[i] != 0)
|
||||
{
|
||||
alSourceStop(sourceSet.sources[i]);
|
||||
RPAudioPoolRelease(sourceSet.sources[i]);
|
||||
sourceSet.sources[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
alDeleteSources(sourceSet.count, sourceSet.sources);
|
||||
|
||||
for (int i = 0; i < sourceSet.count; i++)
|
||||
{
|
||||
sourceSet.sources[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
#include "l4audres.h"
|
||||
#include "openal/al.h"
|
||||
|
||||
//
|
||||
// Master volume, standing in for the amplifier the cabinets had. Stepped by
|
||||
// PgUp/PgDn (L4APP.cpp) and persisted to volume.cfg; see L4AUDRND.cpp.
|
||||
//
|
||||
extern float gRPMasterVolume;
|
||||
|
||||
void RPAudioMasterVolumeStep(int direction);
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "l4ctrl.h"
|
||||
#include "l4keybd.h"
|
||||
#include "l4app.h"
|
||||
#include "l4audrnd.h" // RPAudioMasterVolumeStep, for the PgUp/PgDn keys
|
||||
#include "l4audres.h" // RPBassTrimStep, for the Home/End keys
|
||||
#include "l4dinput.h"
|
||||
#include "..\munga\appmgr.h"
|
||||
#include "dxutils.h"
|
||||
@@ -1513,6 +1515,78 @@ void
|
||||
// Update the PC keyboard mapping group
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// Master volume, PgUp louder / PgDn quieter.
|
||||
//
|
||||
// The cabinets ran the game at unity and left level to an external
|
||||
// amplifier and crossover; without that hardware the player has to be able
|
||||
// to reach the volume while playing.
|
||||
//
|
||||
// POLLED, not taken off the key message below, and that is deliberate. The
|
||||
// pump below only ever consumes WM_KEYUP / WM_SYSKEYUP / WM_CHAR from the
|
||||
// front of the queue, and the front-end runs message loops of its own, so
|
||||
// key messages are raced for and routinely lost -- measured here at roughly
|
||||
// two of every six presses arriving. That is survivable for a one-shot like
|
||||
// the abort chord; it is not survivable for a control you tap repeatedly to
|
||||
// find a level. Reading the key state directly costs nothing and cannot be
|
||||
// dropped.
|
||||
//
|
||||
// Page keys because they produce no typed character, so they cannot collide
|
||||
// with the character-keyed commands the pump feeds, nothing else in RP binds
|
||||
// them, and they exist on tenkeyless keyboards.
|
||||
//
|
||||
// The foreground check keeps an alt-tabbed game from eating the volume keys
|
||||
// of whatever the player switched to.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
// Home/End do the same for the bass trim - the crossover's low band to
|
||||
// PgUp/PgDn's amplifier.
|
||||
//
|
||||
{
|
||||
static int volume_up_held = 0;
|
||||
static int volume_down_held = 0;
|
||||
static int bass_up_held = 0;
|
||||
static int bass_down_held = 0;
|
||||
|
||||
int focused = 0;
|
||||
|
||||
if (HWND foreground = GetForegroundWindow())
|
||||
{
|
||||
DWORD foreground_process = 0;
|
||||
|
||||
GetWindowThreadProcessId(foreground, &foreground_process);
|
||||
focused = (foreground_process == GetCurrentProcessId());
|
||||
}
|
||||
|
||||
const int up = focused && (GetAsyncKeyState(VK_PRIOR) & 0x8000) != 0;
|
||||
const int down = focused && (GetAsyncKeyState(VK_NEXT) & 0x8000) != 0;
|
||||
const int bass_up = focused && (GetAsyncKeyState(VK_HOME) & 0x8000) != 0;
|
||||
const int bass_down = focused && (GetAsyncKeyState(VK_END) & 0x8000) != 0;
|
||||
|
||||
if (up && !volume_up_held)
|
||||
{
|
||||
RPAudioMasterVolumeStep(+1);
|
||||
}
|
||||
if (down && !volume_down_held)
|
||||
{
|
||||
RPAudioMasterVolumeStep(-1);
|
||||
}
|
||||
if (bass_up && !bass_up_held)
|
||||
{
|
||||
RPBassTrimStep(+1);
|
||||
}
|
||||
if (bass_down && !bass_down_held)
|
||||
{
|
||||
RPBassTrimStep(-1);
|
||||
}
|
||||
|
||||
volume_up_held = up;
|
||||
volume_down_held = down;
|
||||
bass_up_held = bass_up;
|
||||
bass_down_held = bass_down;
|
||||
}
|
||||
|
||||
if (flags.keyboardExists)
|
||||
{
|
||||
//RB 1/20/07
|
||||
|
||||
+1392
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);
|
||||
@@ -142,15 +142,48 @@ namespace
|
||||
//---------------------------------------------------------------
|
||||
void Worker()
|
||||
{
|
||||
bool ownsApartment = false;
|
||||
try
|
||||
{
|
||||
init_apartment();
|
||||
ownsApartment = true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// apartment already set on this thread; carry on
|
||||
}
|
||||
|
||||
//
|
||||
// C++/WinRT caches an activation factory the first time a type
|
||||
// is used, and that cache is PROCESS-wide - it outlives this
|
||||
// thread. The apartment does not: COM tears it down when the
|
||||
// worker exits and unloads the Lights server with it, because
|
||||
// by then nothing holds a reference.
|
||||
//
|
||||
// So the cached factory is left pointing into an address range
|
||||
// that no longer has a module in it, and the NEXT race's worker
|
||||
// calls straight through it - the crash was a call through the
|
||||
// stale vtable, on the second race, every time. A machine with
|
||||
// no Dynamic Lighting keyboard is not spared: asking for the
|
||||
// device selector is enough to populate the cache.
|
||||
//
|
||||
// Clear it before the apartment goes, and on every way out of
|
||||
// here rather than only the tidy one - the watcher setup below
|
||||
// returns early when Dynamic Lighting is unavailable.
|
||||
//
|
||||
struct WinRTExit
|
||||
{
|
||||
bool owns;
|
||||
~WinRTExit()
|
||||
{
|
||||
clear_factory_cache();
|
||||
if (owns)
|
||||
{
|
||||
uninit_apartment();
|
||||
}
|
||||
}
|
||||
} winrtExit{ ownsApartment };
|
||||
|
||||
std::mutex claimedLock;
|
||||
std::vector<ClaimedArray> claimed;
|
||||
bool anySeen = false;
|
||||
|
||||
+137
-3
@@ -24,6 +24,7 @@
|
||||
#include "..\munga\appmsg.h"
|
||||
#include "..\munga\mission.h"
|
||||
#include "..\munga\notation.h"
|
||||
#include "..\munga\spooler.h"
|
||||
//#include <netnub.hpp>
|
||||
|
||||
|
||||
@@ -213,7 +214,15 @@ L4NetworkManager::L4NetworkManager():
|
||||
}
|
||||
networkEggNotationFile = new NotationFile(egg_name);
|
||||
Register_Object(networkEggNotationFile);
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
//
|
||||
// NOT last.egg. That name now belongs to the egg saved beside
|
||||
// last.spl for playback, and this is something else entirely - a
|
||||
// debug dump of whatever egg was just loaded. The two collided, and
|
||||
// playback picking this one up as if it were a recording's companion
|
||||
// read a host table of the wrong length out of the spool header,
|
||||
// walked off the end of it, and corrupted the heap.
|
||||
//
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
currentNetworkState = NormalState;
|
||||
|
||||
ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10);
|
||||
@@ -378,7 +387,7 @@ void
|
||||
|
||||
networkEggNotationFile = new NotationFile(egg_path);
|
||||
Register_Object(networkEggNotationFile);
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
|
||||
// In network mode (the owner pod of a multiplayer race) the state
|
||||
// gate must open or CheckBuffers keeps dropping mesh packets - a
|
||||
@@ -447,6 +456,14 @@ void
|
||||
Check(application->GetHostManager());
|
||||
application->GetHostManager()->AdoptLocalHost(my_l4host);
|
||||
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: stand-alone local host '"
|
||||
<< (const char *) mission_host_data->GetAddressString()
|
||||
<< "' hostType=" << (int) mission_host_data->GetHostType()
|
||||
<< " - posting LoadMission\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------
|
||||
// Now, since this host creator is for stand-alone mode, send the load
|
||||
@@ -559,6 +576,85 @@ void
|
||||
nextOpenHostID++;
|
||||
}
|
||||
|
||||
//
|
||||
// A recording starts with a header, and this is where it can be written.
|
||||
//
|
||||
// A spool opens with the application ID, the resource major version,
|
||||
// and one (remote, hostID) pair per host named in the egg - playback
|
||||
// reads them straight back in L4PlaybackNetworkManager::StartConnecting
|
||||
// and refuses the file without them. The first recordings this build
|
||||
// made were packets only, so playback met a zero where the application
|
||||
// ID should be and said "Not a spool file for this application".
|
||||
//
|
||||
// It goes here rather than in the recorder because every part of it is
|
||||
// network-layer knowledge, and here is the moment all of it first
|
||||
// exists: the hosts have just been created and the mission is in hand.
|
||||
//
|
||||
if (Application::IsRecording())
|
||||
{
|
||||
SpoolRecorder *recorder = SpoolRecorder_Get();
|
||||
|
||||
if (!recorder->IsArmed())
|
||||
{
|
||||
recorder->Arm();
|
||||
}
|
||||
if (recorder->IsArmed() && !recorder->HeaderWritten())
|
||||
{
|
||||
SpoolFile *header_spool = recorder->GetSpool();
|
||||
ResourceFile *res_file = application->GetResourceFile();
|
||||
Check(res_file);
|
||||
|
||||
*(ApplicationID*)header_spool->GetPointer() =
|
||||
application->GetApplicationID();
|
||||
header_spool->AdvancePointer(sizeof(ApplicationID));
|
||||
|
||||
int major_version = res_file->versionArray[1];
|
||||
|
||||
*(int*)header_spool->GetPointer() = major_version;
|
||||
header_spool->AdvancePointer(sizeof(major_version));
|
||||
|
||||
HostManager *header_host_mgr = application->GetHostManager();
|
||||
Check(header_host_mgr);
|
||||
Mission::HostIterator header_iterator(mission);
|
||||
MissionHostData *header_host_data;
|
||||
int header_host_count = 0;
|
||||
|
||||
while ((header_host_data = header_iterator.ReadAndNext()) != NULL)
|
||||
{
|
||||
CString header_name(header_host_data->GetAddressString());
|
||||
SOCKADDR_IN header_address;
|
||||
|
||||
NetTransport_Get()->Resolve((LPSTR)header_name, &header_address);
|
||||
if (header_address.sin_port == 0)
|
||||
{
|
||||
header_address.sin_port = htons(localGamePort);
|
||||
}
|
||||
|
||||
Host *header_host = header_host_mgr->FindHost(header_address);
|
||||
|
||||
//
|
||||
// Same guard as the arcade spooler needs: a host can be in
|
||||
// the egg without being connected, and the pair still has to
|
||||
// be written or every entry after it shifts.
|
||||
//
|
||||
*(Logical*)header_spool->GetPointer() = (header_host == NULL)
|
||||
? True
|
||||
: (header_host != header_host_mgr->GetLocalHost());
|
||||
header_spool->AdvancePointer(sizeof(Logical));
|
||||
|
||||
*(HostID*)header_spool->GetPointer() = (header_host == NULL)
|
||||
? (HostID) 0
|
||||
: header_host->GetHostID();
|
||||
header_spool->AdvancePointer(sizeof(HostID));
|
||||
++header_host_count;
|
||||
}
|
||||
|
||||
recorder->MarkHeaderWritten();
|
||||
DEBUG_STREAM << "Record: spool header written, " << header_host_count
|
||||
<< " host(s) from the egg\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// All the hosts are created, the connects/listens done.
|
||||
//
|
||||
@@ -811,7 +907,7 @@ void
|
||||
#if defined(LAB_ONLY)
|
||||
DEBUG_STREAM << "Created egg\n";
|
||||
#endif
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
currentNetworkState = NormalState;
|
||||
|
||||
//
|
||||
@@ -1102,6 +1198,34 @@ void L4NetworkManager::Send(
|
||||
our_host_manager = application->GetHostManager();
|
||||
Check(our_host_manager);
|
||||
base_host = our_host_manager->GetRemoteHost(host_ID);
|
||||
|
||||
//
|
||||
// A host that is not in the table at all. Two playtest machines died
|
||||
// here on the same instruction - the podium was up, a peer had left,
|
||||
// and a drop zone assignment dispatched to a host GetRemoteHost had
|
||||
// no row for, so GetConnectStatus read offset 0x24 off a NULL. The
|
||||
// table's rows never leave it (disconnect only marks a host offline),
|
||||
// so an absent ID was never adopted: host -1 is the known case, the
|
||||
// ownerless map entities every log shows as 'Entity -1:106'. Whatever
|
||||
// the ID's provenance, a message to a host that does not exist gets
|
||||
// the same answer as one to a host that is offline - dropped - and
|
||||
// the log names the ID so the next occurrence explains itself.
|
||||
//
|
||||
if (base_host == NULL)
|
||||
{
|
||||
static int said = 0;
|
||||
|
||||
if (said < 8)
|
||||
{
|
||||
++said;
|
||||
DEBUG_STREAM << "Send: no host " << host_ID
|
||||
<< " in the table (client " << (int) client
|
||||
<< ", message " << message->messageID
|
||||
<< ") - dropped, not crashed\n" << std::flush;
|
||||
}
|
||||
CLEAR_SEND_PACKET();
|
||||
return;
|
||||
}
|
||||
l4host = Cast_Object(L4Host*, base_host);
|
||||
if(l4host->GetConnectStatus() != L4Host::OnLineConnectionStatus)
|
||||
{
|
||||
@@ -1233,6 +1357,16 @@ Logical L4NetworkManager::SendMessageToNetnub(
|
||||
Check(host_manager);
|
||||
|
||||
receiving_host = Cast_Object(L4Host*, host_manager->GetRemoteHost(host_ID));
|
||||
|
||||
//
|
||||
// Same guard as Send, one layer down: an ID with no row in the table
|
||||
// answers True - message consumed, nothing to retry - exactly like a
|
||||
// host that is offline.
|
||||
//
|
||||
if (receiving_host == NULL)
|
||||
{
|
||||
return True;
|
||||
}
|
||||
if (receiving_host->GetConnectStatus() != L4Host::OnLineConnectionStatus)
|
||||
{
|
||||
return True;
|
||||
|
||||
+180
-2
@@ -2,8 +2,138 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4nettransport.h"
|
||||
#include "..\munga\appmgr.h"
|
||||
#include <Ws2tcpip.h>
|
||||
|
||||
//########################################################################
|
||||
// Waiting for a peer without hanging the window
|
||||
//########################################################################
|
||||
|
||||
namespace
|
||||
{
|
||||
Logical gInWait = False; // re-entrancy guard
|
||||
Logical gWaitCancelled = False;
|
||||
char gOriginalTitle[256] = "";
|
||||
Logical gTitleSaved = False;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// How long Connect keeps redialling.
|
||||
//
|
||||
// It was two minutes, which was right for what it was written for: an
|
||||
// arcade console redialling a pod that is still booting and WILL answer,
|
||||
// on a LAN with nothing else to go wrong. Over Steam the same two minutes
|
||||
// is a host staring at a dead window, because a peer that has not answered
|
||||
// in twenty seconds is not coming - it never launched, or it is behind
|
||||
// something the relay cannot cross.
|
||||
//
|
||||
int
|
||||
NetTransport_ConnectWaitSeconds()
|
||||
{
|
||||
static int cached = -1;
|
||||
|
||||
if (cached < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412CONNECTWAIT");
|
||||
|
||||
cached = (setting != NULL) ? atoi(setting) : 20;
|
||||
if (cached < 2) { cached = 2; }
|
||||
if (cached > 300) { cached = 300; }
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
NetTransport_SetWaitProgress(const char *text)
|
||||
{
|
||||
if (ghWnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gTitleSaved)
|
||||
{
|
||||
GetWindowTextA(ghWnd, gOriginalTitle, sizeof(gOriginalTitle) - 1);
|
||||
gTitleSaved = True;
|
||||
}
|
||||
|
||||
//
|
||||
// The title bar because it is the one surface guaranteed to exist
|
||||
// here: this runs before the engine block, so there is no renderer to
|
||||
// draw a progress screen with yet.
|
||||
//
|
||||
SetWindowTextA(ghWnd, (text != NULL) ? text : gOriginalTitle);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
NetWaitResult
|
||||
NetTransport_PumpAndSleep(int milliseconds)
|
||||
{
|
||||
//
|
||||
// Dispatching a message can run application code, and that code can
|
||||
// reach a connect of its own. Nested pumping would then deliver the
|
||||
// same messages twice and let an inner wait consume the escape meant
|
||||
// for the outer one, so an inner call just sleeps.
|
||||
//
|
||||
if (gInWait)
|
||||
{
|
||||
Sleep(milliseconds);
|
||||
return NetWaitContinue;
|
||||
}
|
||||
|
||||
gInWait = True;
|
||||
|
||||
NetWaitResult result = NetWaitContinue;
|
||||
MSG message;
|
||||
|
||||
while (PeekMessage(&message, NULL, 0, 0, PM_REMOVE))
|
||||
{
|
||||
if (message.message == WM_QUIT)
|
||||
{
|
||||
//
|
||||
// Put it back: the message loop that owns the shutdown has
|
||||
// to see this, not us.
|
||||
//
|
||||
PostQuitMessage((int) message.wParam);
|
||||
result = NetWaitQuit;
|
||||
break;
|
||||
}
|
||||
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
|
||||
{
|
||||
gWaitCancelled = True;
|
||||
}
|
||||
TranslateMessage(&message);
|
||||
DispatchMessage(&message);
|
||||
}
|
||||
|
||||
if (result == NetWaitContinue && gWaitCancelled)
|
||||
{
|
||||
result = NetWaitCancelled;
|
||||
}
|
||||
if (result == NetWaitContinue)
|
||||
{
|
||||
Sleep(milliseconds);
|
||||
}
|
||||
|
||||
gInWait = False;
|
||||
return result;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// Cleared when a connect sequence starts, so an escape pressed during one
|
||||
// race does not cancel the next one.
|
||||
//
|
||||
void
|
||||
NetTransport_ClearWaitCancel()
|
||||
{
|
||||
gWaitCancelled = False;
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
// WinsockNetTransport - the TCP wire the arcade always used, moved
|
||||
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
|
||||
@@ -117,7 +247,28 @@ namespace
|
||||
// the peer may not be listening yet. A refused TCP socket
|
||||
// is dead - every attempt needs a fresh one.
|
||||
//
|
||||
DWORD deadline = GetTickCount() + 120 * 1000;
|
||||
//
|
||||
// NOTE the connect() below is still BLOCKING - the socket only
|
||||
// goes nonblocking once it has succeeded - so an unreachable
|
||||
// host (filtered rather than refused) sits in the OS SYN retry
|
||||
// for around twenty seconds with nothing we can do about it.
|
||||
// Pumping between redials fixes the refused case, which is the
|
||||
// common one; the unreachable case needs the socket made
|
||||
// nonblocking before connect() and a select() on our own
|
||||
// timeout. Not done here because this is the LAN/direct path -
|
||||
// a Steam host goes through SteamNetTransport::Connect.
|
||||
//
|
||||
DWORD deadline =
|
||||
GetTickCount() + (DWORD) NetTransport_ConnectWaitSeconds() * 1000;
|
||||
char progress[256];
|
||||
|
||||
sprintf(
|
||||
progress,
|
||||
"Red Planet - connecting to %s ...",
|
||||
inet_ntoa(remote->sin_addr)
|
||||
);
|
||||
NetTransport_SetWaitProgress(progress);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
@@ -164,6 +315,7 @@ namespace
|
||||
closesocket(sock);
|
||||
return InvalidConnection;
|
||||
}
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return (Connection) sock;
|
||||
}
|
||||
|
||||
@@ -174,9 +326,35 @@ namespace
|
||||
{
|
||||
DEBUG_STREAM << "ERROR: connect() failed with "
|
||||
<< wsa_error << "!\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
Sleep(250); // peer not up yet - redial
|
||||
|
||||
//
|
||||
// Peer not up yet - redial, answering the window meanwhile.
|
||||
//
|
||||
DWORD left = (DWORD)((LONG)(deadline - GetTickCount()) / 1000);
|
||||
|
||||
sprintf(
|
||||
progress,
|
||||
"Red Planet - connecting to %s ... %us left, ESC to cancel",
|
||||
inet_ntoa(remote->sin_addr),
|
||||
(unsigned) left
|
||||
);
|
||||
NetTransport_SetWaitProgress(progress);
|
||||
|
||||
for (int slept = 0; slept < 250; slept += 50)
|
||||
{
|
||||
NetWaitResult wait = NetTransport_PumpAndSleep(50);
|
||||
if (wait != NetWaitContinue)
|
||||
{
|
||||
DEBUG_STREAM << "Connect "
|
||||
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
||||
<< " by the user\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,3 +124,44 @@ NetTransport *
|
||||
NetTransport_Get();
|
||||
void
|
||||
NetTransport_Set(NetTransport *transport);
|
||||
|
||||
//########################################################################
|
||||
// Waiting for a peer without hanging the window.
|
||||
//
|
||||
// Connect retries until the peer answers, because the egg-ACK ordering
|
||||
// means it may still be booting - that part is deliberate. What was not
|
||||
// deliberate is that the wait slept without pumping messages, so Windows
|
||||
// saw a process that had stopped answering and painted the whole thing
|
||||
// "Not responding" for up to two minutes. It happens before the engine
|
||||
// block, so there is no render loop keeping the window alive either.
|
||||
//
|
||||
// Sleep through this instead. It pumps, so the window keeps drawing and
|
||||
// can be moved; it watches for a cancel; and it is re-entrancy guarded,
|
||||
// because dispatching a message can run application code that reaches a
|
||||
// connect of its own.
|
||||
//########################################################################
|
||||
|
||||
enum NetWaitResult
|
||||
{
|
||||
NetWaitContinue, // carry on waiting
|
||||
NetWaitCancelled, // the user pressed escape
|
||||
NetWaitQuit // WM_QUIT arrived; the caller must unwind
|
||||
};
|
||||
|
||||
NetWaitResult
|
||||
NetTransport_PumpAndSleep(int milliseconds);
|
||||
|
||||
// How long Connect keeps redialling, in seconds. RP412CONNECTWAIT
|
||||
// overrides; see the definition for why the default came down from 120.
|
||||
int
|
||||
NetTransport_ConnectWaitSeconds();
|
||||
|
||||
// Progress, shown in the window title while a connect is outstanding.
|
||||
// Pass NULL to put the original title back.
|
||||
void
|
||||
NetTransport_SetWaitProgress(const char *text);
|
||||
|
||||
// Forget a previous escape, so one race's cancel cannot cancel the next.
|
||||
// Call before starting a connect sequence.
|
||||
void
|
||||
NetTransport_ClearWaitCancel();
|
||||
|
||||
+242
-30
@@ -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,91 @@ namespace
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// One line of the profile grammar
|
||||
// Shared tail of the two axis-source rows: [invert] [deadzone <d>]
|
||||
// [rate <n>], in any order. 'lever' rides along for the rows that
|
||||
// can take it - a NULL lever means the word is not legal here, and
|
||||
// a padaxis row is exactly that: the pad's own triggers already
|
||||
// read 0..1, so there is no half-travel to rescue.
|
||||
//---------------------------------------------------------------
|
||||
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
|
||||
Logical ParseAxisOptions(
|
||||
char *tokens[], int token_count, int first,
|
||||
Logical *invert, Scalar *deadzone, Scalar *rate,
|
||||
Logical *lever = NULL)
|
||||
{
|
||||
for (int i = first; i < token_count; ++i)
|
||||
{
|
||||
if (NameEquals(tokens[i], "invert"))
|
||||
{
|
||||
*invert = True;
|
||||
}
|
||||
else if (NameEquals(tokens[i], "lever") && lever != NULL)
|
||||
{
|
||||
*lever = 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;
|
||||
@@ -267,35 +363,96 @@ namespace
|
||||
{
|
||||
return False;
|
||||
}
|
||||
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->source = source;
|
||||
binding->axis = axis;
|
||||
for (int i = 4; i < token_count; ++i)
|
||||
//
|
||||
// Built aside and only then committed: a row whose options
|
||||
// go bad half way through is a REJECTED row, and taking the
|
||||
// slot first would leave the good half of it bound anyway,
|
||||
// under a log line that says it was skipped.
|
||||
//
|
||||
PadPadAxisBinding candidate;
|
||||
memset(&candidate, 0, sizeof(candidate));
|
||||
candidate.source = source;
|
||||
candidate.axis = axis;
|
||||
if (!ParseAxisOptions(tokens, token_count, 4,
|
||||
&candidate.invert, &candidate.deadzone, &candidate.rate))
|
||||
{
|
||||
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;
|
||||
}
|
||||
profile->padAxes[profile->padAxisCount++] = candidate;
|
||||
return True;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// 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)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
PadJoyAxisBinding candidate;
|
||||
memset(&candidate, 0, sizeof(candidate));
|
||||
candidate.device = *joy_slot;
|
||||
candidate.source = source;
|
||||
candidate.axis = axis;
|
||||
if (!ParseAxisOptions(tokens, token_count, 4,
|
||||
&candidate.invert, &candidate.deadzone, &candidate.rate,
|
||||
&candidate.lever))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
profile->joyAxes[profile->joyAxisCount++] = candidate;
|
||||
return True;
|
||||
}
|
||||
|
||||
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 +477,20 @@ 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] [lever] [deadzone <d>] [rate <n>]\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. Two-pedal hardware - racing\n"
|
||||
"# pedals, rudder pedals with an axis per foot - skips the\n"
|
||||
"# composite and binds LeftPedal and RightPedal directly; the\n"
|
||||
"# game mixes the pair into yaw the way the pod always did.\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 +503,44 @@ 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"
|
||||
"# 'lever' marks a source that rests at one END of its travel instead of\n"
|
||||
"# in the middle - a floor pedal, a slider. Windows reports it as a full\n"
|
||||
"# -1..1 axis all the same, so without the word half the travel sits\n"
|
||||
"# below zero and the first half of the press does nothing; with it the\n"
|
||||
"# travel maps onto 0..1 and the deadzone measures from the released end.\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"
|
||||
"# ...and the same stick with racing pedals on a second device, one\n"
|
||||
"# axis per foot instead of the composite:\n"
|
||||
"#\n"
|
||||
"# joydev 1 Pedals\n"
|
||||
"# joyaxis Y axis LeftPedal lever deadzone 0.05\n"
|
||||
"# joyaxis RZ axis RightPedal lever deadzone 0.05\n"
|
||||
"\n"
|
||||
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
||||
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
||||
@@ -499,6 +704,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 +729,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 +748,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,32 @@
|
||||
// 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] [lever] [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.
|
||||
//
|
||||
// 'lever' says the source is a one-way control that rests at one end of
|
||||
// its travel rather than in the middle - a floor pedal, a throttle
|
||||
// slider. DirectInput reports it as a full -1..1 axis all the same, so
|
||||
// without the keyword half the travel sits below zero and the first
|
||||
// half of the press does nothing. It maps that travel onto 0..1, and
|
||||
// the deadzone then measures from the released end instead of centre.
|
||||
//########################################################################
|
||||
|
||||
enum PadBindRioAxis
|
||||
@@ -33,6 +53,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 +117,55 @@ 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;
|
||||
Logical lever; // rests at one end: -1..1 travel means 0..1
|
||||
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 +173,10 @@ struct PadBindingProfile
|
||||
maxKeyButtons = 128,
|
||||
maxKeyAxes = 32,
|
||||
maxPadButtons = 32,
|
||||
maxPadAxes = 16
|
||||
maxPadAxes = 16,
|
||||
maxJoyAxes = 24,
|
||||
maxJoyButtons = 48,
|
||||
maxJoyHats = 16
|
||||
};
|
||||
|
||||
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
||||
@@ -107,6 +187,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,
|
||||
|
||||
+312
-5
@@ -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,112 @@ namespace
|
||||
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
//
|
||||
// RP412INPUTFOCUS - do the controls answer only while the game is the
|
||||
// window in front? On unless the file says 0.
|
||||
//
|
||||
// The pod was the only thing running on its cabinet, so the virtual
|
||||
// RIO reads the key state directly rather than waiting on the message
|
||||
// pump. That is the right call for latency and it is why the pedals
|
||||
// feel like pedals - but a direct read is a read of the WHOLE
|
||||
// keyboard, whatever has focus, so a player alt-tabbed into a text
|
||||
// editor was flying the pod with every note they typed.
|
||||
//
|
||||
Logical InputNeedsFocus()
|
||||
{
|
||||
static int setting = -1;
|
||||
if (setting < 0)
|
||||
{
|
||||
const char *value = getenv("RP412INPUTFOCUS");
|
||||
setting = (value != NULL && *value == '0') ? 0 : 1;
|
||||
}
|
||||
return setting ? True : False;
|
||||
}
|
||||
|
||||
//
|
||||
// Whether the foreground window is one of OURS - the process, not one
|
||||
// particular handle. The cockpit is a shell full of child panes, the
|
||||
// exploded view is six windows of its own and the plasma glass
|
||||
// another, so any of them being in front is the game being in front.
|
||||
// Matching a single HWND would drop the controls the moment somebody
|
||||
// clicked an MFD.
|
||||
//
|
||||
Logical ProcessHasFocus()
|
||||
{
|
||||
HWND foreground = GetForegroundWindow();
|
||||
if (foreground == NULL)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
DWORD foreground_process = 0;
|
||||
GetWindowThreadProcessId(foreground, &foreground_process);
|
||||
return (foreground_process == GetCurrentProcessId()) ? True : False;
|
||||
}
|
||||
|
||||
//
|
||||
// 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 one-way control - a floor pedal, a slider - rests at one END of
|
||||
// its travel, not in the middle, and DirectInput still reports it as
|
||||
// a full -1..1 axis. Fold that travel onto 0..1 so the pedal starts
|
||||
// answering as soon as it moves instead of at half depression, and
|
||||
// measure the deadzone from the released end, where the slack in a
|
||||
// tired return spring actually lives.
|
||||
//
|
||||
Scalar JoyLeverValue(Scalar raw, Scalar deadzone)
|
||||
{
|
||||
Scalar value = (raw + 1.0f) * 0.5f;
|
||||
if (value <= deadzone)
|
||||
{
|
||||
return (Scalar) 0;
|
||||
}
|
||||
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 +273,34 @@ PadRIO::PadRIO()
|
||||
activeInstance = this;
|
||||
|
||||
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
||||
DEBUG_STREAM << "PadRIO: controls "
|
||||
<< (InputNeedsFocus()
|
||||
? "answer only while the game window has focus"
|
||||
: "answer whether the game has focus or not (RP412INPUTFOCUS=0)")
|
||||
<< "\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 +379,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
|
||||
@@ -297,6 +437,25 @@ void
|
||||
}
|
||||
lastPollTick = now;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Is the game the window in front? Every source below is gated on
|
||||
// this - keyboard, pad and stick alike.
|
||||
//
|
||||
// Gated rather than skipped, and that is the whole trick: each
|
||||
// source reads as RELEASED instead of the poll returning early, so
|
||||
// the diffs further down turn whatever was held at the moment you
|
||||
// switched away into proper release events. Bail out instead and a
|
||||
// key held on alt-tab stays down until you come back, which is the
|
||||
// stuck throttle this is meant to prevent rather than cause.
|
||||
//
|
||||
// The throttle accumulator is the deliberate exception. It is the
|
||||
// pod's one sticky axis and it integrates what the controls ask
|
||||
// for, so controls asking for nothing simply stop moving it - you
|
||||
// come back to the speed you left, not to a dead stop.
|
||||
//---------------------------------------------------------------
|
||||
Logical input_live =
|
||||
(!InputNeedsFocus() || ProcessHasFocus()) ? True : False;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Find / keep the XInput pad. Probing empty slots is slow, so an
|
||||
// absent pad is only re-probed every 3 seconds.
|
||||
@@ -305,7 +464,7 @@ void
|
||||
memset(&pad, 0, sizeof(pad));
|
||||
Logical pad_live = False;
|
||||
|
||||
if (padIndex >= 0)
|
||||
if (input_live && padIndex >= 0)
|
||||
{
|
||||
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
|
||||
if (!pad_live)
|
||||
@@ -314,7 +473,7 @@ void
|
||||
padIndex = -1;
|
||||
}
|
||||
}
|
||||
if (padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
||||
if (input_live && padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
||||
{
|
||||
lastPadCheckTick = now;
|
||||
for (DWORD i = 0; i < 4; ++i)
|
||||
@@ -349,7 +508,7 @@ void
|
||||
for (int i = 0; i < profile.keyButtonCount; ++i)
|
||||
{
|
||||
PadKeyButtonBinding *binding = &profile.keyButtons[i];
|
||||
Logical down = KeyDown(binding->virtualKey);
|
||||
Logical down = input_live && KeyDown(binding->virtualKey);
|
||||
if (binding->toggle && down && !binding->wasDown)
|
||||
{
|
||||
binding->latched = !binding->latched;
|
||||
@@ -391,6 +550,88 @@ 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;
|
||||
}
|
||||
//
|
||||
// Unfocused this whole block is skipped, which leaves every joyDevice
|
||||
// slot at -1 - so the button, hat and axis loops below find no device
|
||||
// and read released and centred on their own. The stick is opened
|
||||
// DISCL_BACKGROUND (it has to be, or it stops answering the moment a
|
||||
// pane takes focus), so not polling it is what makes it go quiet.
|
||||
//
|
||||
if (input_live && (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])
|
||||
@@ -446,7 +687,7 @@ void
|
||||
for (int i = 0; i < profile.keyAxisCount; ++i)
|
||||
{
|
||||
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
|
||||
if (KeyDown(binding->virtualKey))
|
||||
if (input_live && KeyDown(binding->virtualKey))
|
||||
{
|
||||
if (binding->mode == BindKeyRate)
|
||||
{
|
||||
@@ -502,6 +743,70 @@ 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 = binding->lever
|
||||
? JoyLeverValue(raw, binding->deadzone)
|
||||
: 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 +816,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
|
||||
|
||||
@@ -249,14 +249,51 @@ void ParticleEmitter::Execute()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Drop everything bound to the device we were last given.
|
||||
//
|
||||
// Null-safe, and it clears what it drops. Neither was true before: this
|
||||
// runs on the device-lost path ahead of a Reset, where a texture that
|
||||
// never loaded (a missing VIDEO\particles.png is enough) left one of
|
||||
// these NULL and took the Reset down with it, and a released pointer
|
||||
// left in place is a dangling one the moment anything looks again.
|
||||
//
|
||||
void ParticleEngine::Destroy()
|
||||
{
|
||||
mVertBuffer->Release();
|
||||
mParticleTexture->Release();
|
||||
if (mVertBuffer != NULL)
|
||||
{
|
||||
mVertBuffer->Release();
|
||||
mVertBuffer = NULL;
|
||||
}
|
||||
if (mParticleTexture != NULL)
|
||||
{
|
||||
mParticleTexture->Release();
|
||||
mParticleTexture = NULL;
|
||||
}
|
||||
//
|
||||
// The paint paths test this before touching anything, so clearing it
|
||||
// makes the gap between a Destroy and the next Initialize safe.
|
||||
//
|
||||
mDevice = NULL;
|
||||
}
|
||||
|
||||
void ParticleEngine::Initialize(LPDIRECT3DDEVICE9 device)
|
||||
{
|
||||
//
|
||||
// Whatever is still held belongs to the PREVIOUS device, and holding
|
||||
// it kept that device alive. A fresh renderer is built per mission,
|
||||
// so a new device used to arrive here while the old one's vertex
|
||||
// buffer (D3DPOOL_DEFAULT) and texture still referenced it -
|
||||
// ~DPLRenderer's release never reached zero and the whole device
|
||||
// survived the race that made it, back buffer and depth buffer and
|
||||
// all. That is one leaked render target per race.
|
||||
//
|
||||
// The device-lost path already released before re-initialising; this
|
||||
// is the same contract for the case where the device is not lost but
|
||||
// replaced.
|
||||
//
|
||||
Destroy();
|
||||
|
||||
mDevice = device;
|
||||
memset(mInstalledEffects, 0, sizeof(mInstalledEffects));
|
||||
|
||||
|
||||
+251
-2
@@ -124,10 +124,38 @@ void
|
||||
|
||||
ResolveAddress(host_name, &net_address);
|
||||
Host *host = host_mgr->FindHost(net_address);
|
||||
|
||||
|
||||
//
|
||||
// FindHost answers NULL for a host named in the egg that is not
|
||||
// actually connected, and this went straight on to call
|
||||
// host->GetHostID(). In an arcade every station in the egg is on
|
||||
// the wire, so the case could not arise; anywhere else it is the
|
||||
// ordinary state of affairs, and it crashed the spooling
|
||||
// application before the mission could even start.
|
||||
//
|
||||
// The table below is read back one pair per egg host, in egg
|
||||
// order, so a missing host cannot simply be skipped - that would
|
||||
// shift every entry after it. Write the pair, say what happened,
|
||||
// and carry on.
|
||||
//
|
||||
if (host == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "Spool: host '" << host_name
|
||||
<< "' is in the egg but not connected - recording it as"
|
||||
<< " remote with no ID. Packets from it will not map back.\n"
|
||||
<< std::flush;
|
||||
|
||||
*(Logical*)spool->GetPointer() = True;
|
||||
spool->AdvancePointer(sizeof(Logical));
|
||||
|
||||
*(HostID*)spool->GetPointer() = (HostID) 0;
|
||||
spool->AdvancePointer(sizeof(HostID));
|
||||
continue;
|
||||
}
|
||||
|
||||
*(Logical*)spool->GetPointer() = (host != host_mgr->GetLocalHost());
|
||||
spool->AdvancePointer(sizeof(Logical));
|
||||
|
||||
|
||||
*(HostID*)spool->GetPointer() = host->GetHostID();
|
||||
spool->AdvancePointer(sizeof(HostID));
|
||||
}
|
||||
@@ -167,11 +195,143 @@ void
|
||||
//###################### L4PlaybackNetworkManager #########################
|
||||
//#############################################################################
|
||||
|
||||
//
|
||||
// Whether the spool's header has been taken off the front of the stream, so
|
||||
// SpoolerTask::Execute knows when the cursor is standing on a packet rather
|
||||
// than on the header. Cleared when a playback manager is built, since the
|
||||
// single-binary loop can play a second spool in the same process.
|
||||
//
|
||||
namespace
|
||||
{
|
||||
Logical gSpoolHeaderConsumed = False;
|
||||
}
|
||||
|
||||
Logical
|
||||
SpoolHeaderConsumed()
|
||||
{
|
||||
return gSpoolHeaderConsumed;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
L4PlaybackNetworkManager::L4PlaybackNetworkManager():
|
||||
NetworkManager(L4PlaybackNetworkManager::DefaultData)
|
||||
{
|
||||
gSpoolHeaderConsumed = False;
|
||||
|
||||
//
|
||||
// Give this application its mission.
|
||||
//
|
||||
// Nothing else will. A spool is a record of what MOVED, not of the
|
||||
// world it moved through - no track, no models, no drop zones - and
|
||||
// playback has neither a wire to be sent an egg over nor a console to
|
||||
// send one. L4NetworkManager does exactly this for single user mode,
|
||||
// but that is the pod's network manager; this one descends from
|
||||
// NetworkManager and inherited none of it, so a playback build came up
|
||||
// with a cockpit, no world behind it, and nothing in the log to say
|
||||
// why. A black screen is what that looks like.
|
||||
//
|
||||
const char *egg_name =
|
||||
((L4Application *) application)->GetEggNotationFileName();
|
||||
|
||||
//
|
||||
// Fall back to the egg saved with the recording.
|
||||
//
|
||||
// Every spool is written with its egg beside it under the same stem,
|
||||
// and last.egg alongside last.spl, precisely so that a recording is one
|
||||
// self-contained thing. Having to name the egg by hand invites naming
|
||||
// the WRONG one - frontend.egg is rewritten by the next race set up on
|
||||
// the machine, and a spool played against a different track would load
|
||||
// happily and show nonsense.
|
||||
//
|
||||
static char found_egg[MAX_PATH];
|
||||
|
||||
if (egg_name == NULL || strlen(egg_name) == 0)
|
||||
{
|
||||
CString spool_name = ((L4Application *) application)->GetSpoolFileName();
|
||||
const char *spool_text =
|
||||
(!spool_name) ? "last.spl" : (const char *) spool_name;
|
||||
|
||||
strncpy(found_egg, spool_text, sizeof(found_egg) - 1);
|
||||
found_egg[sizeof(found_egg) - 1] = '\0';
|
||||
|
||||
size_t length = strlen(found_egg);
|
||||
if (length > 4)
|
||||
{
|
||||
strcpy(found_egg + length - 4, ".egg");
|
||||
|
||||
FILE *probe = fopen(found_egg, "r");
|
||||
if (probe != NULL)
|
||||
{
|
||||
fclose(probe);
|
||||
egg_name = found_egg;
|
||||
DEBUG_STREAM << "Playback: using the egg saved with the"
|
||||
<< " recording, '" << found_egg << "'\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (egg_name == NULL || strlen(egg_name) == 0)
|
||||
{
|
||||
DEBUG_STREAM << "Playback: no egg. A spool records the race but not the"
|
||||
<< " track it was run on, and no egg was found beside the spool -"
|
||||
<< " name one with -egg.\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "Playback: loading world from egg '" << egg_name
|
||||
<< "'\n" << std::flush;
|
||||
|
||||
networkEggNotationFile = new NotationFile(egg_name);
|
||||
Register_Object(networkEggNotationFile);
|
||||
|
||||
//
|
||||
// Decide now whether this is a camera station, not later.
|
||||
//
|
||||
// StartConnecting already works this out from the local host's type and
|
||||
// sets it - but StartConnecting runs after the mission is created, and
|
||||
// the cockpit is built before that. The log says so plainly: the egg is
|
||||
// read here, SVGA16 fits the cockpit eleven lines later, and the Live
|
||||
// Cam line arrives five lines after THAT. So a camera's recording came
|
||||
// back with a pod's cockpit over it - five instrument panes and the map
|
||||
// in the middle - because the answer arrived after the panes had been
|
||||
// made.
|
||||
//
|
||||
// The egg has it: the first entry in [pilots] is the station that
|
||||
// owned the race, and its own section carries the hostType. Reading it
|
||||
// here is early enough, and it is the same fact StartConnecting will
|
||||
// confirm from the host table afterwards.
|
||||
//
|
||||
const char *owner_address;
|
||||
|
||||
if (networkEggNotationFile->GetEntry("pilots", "pilot", &owner_address))
|
||||
{
|
||||
int owner_host_type = 0;
|
||||
|
||||
if (networkEggNotationFile->GetEntry(
|
||||
owner_address, "hostType", &owner_host_type))
|
||||
{
|
||||
Logical owner_is_camera =
|
||||
(owner_host_type == (int) CameraShipHostType);
|
||||
|
||||
Application::SetCameraStation(owner_is_camera);
|
||||
DEBUG_STREAM << "Playback: the owning station '" << owner_address
|
||||
<< "' recorded as hostType " << owner_host_type
|
||||
<< (owner_is_camera
|
||||
? " - a Live Cam, so no instrument panes and the map"
|
||||
" landscape in the corner"
|
||||
: " - a pod, so the full cockpit")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The handler is NetworkManager's, and it ends in CreateMission, which
|
||||
// is what calls StartConnecting below to read the host table back out
|
||||
// of the spool.
|
||||
//
|
||||
ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10);
|
||||
application->Post(DefaultEventPriority, this, &egg_message);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -243,9 +403,72 @@ void
|
||||
else
|
||||
{
|
||||
host_mgr->AdoptLocalHost(my_host);
|
||||
|
||||
//
|
||||
// Lay the cockpit out as whatever this station WAS.
|
||||
//
|
||||
// Application::SetCameraStation is normally the front end's
|
||||
// answer to a question asked on the setup screen, and playback
|
||||
// never sees the setup screen - so a recording made from a Live
|
||||
// Cam replayed with five instrument panes hung over the view
|
||||
// and the map back in the middle, which is a pod's cockpit, not
|
||||
// a camera's. The egg knows: it is the same egg the race ran
|
||||
// on, and it says what this host was.
|
||||
//
|
||||
Application::SetCameraStation(
|
||||
mission_host_data->GetHostType() == CameraShipHostType
|
||||
);
|
||||
if (mission_host_data->GetHostType() == CameraShipHostType)
|
||||
{
|
||||
DEBUG_STREAM << "Playback: this station recorded as a Live Cam"
|
||||
<< " - no instrument panes, map landscape\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Did the header end where the packets begin?
|
||||
//
|
||||
// The table above holds one pair per host named in the EGG, and the
|
||||
// reader trusts the egg to have as many hosts as the spool was written
|
||||
// with. Hand it a different egg and the count differs, the read pointer
|
||||
// stops short of - or past - the first packet, and every packet after
|
||||
// that is parsed from the middle of something else. The first symptom is
|
||||
// a nonsense message length, and the second is a corrupted heap: exit
|
||||
// 0xC0000374, no message, nothing in the log.
|
||||
//
|
||||
// A packet here should start with a plausible length. If it does not,
|
||||
// the egg does not belong to this spool, and saying so is worth more
|
||||
// than whatever the heap does next.
|
||||
//
|
||||
{
|
||||
NetworkPacket *first = (NetworkPacket*) spool->GetPointer();
|
||||
int first_length = (int) first->messageData.messageLength;
|
||||
|
||||
if (first_length < (int) sizeof(Receiver::Message)
|
||||
|| first_length > 65536
|
||||
|| first_length > (int) spool->GetBytesRemaining())
|
||||
{
|
||||
DEBUG_STREAM << "\n\nError - this egg does not belong to this spool."
|
||||
<< " The host table ran to the wrong length and the first packet"
|
||||
<< " reads as " << first_length << " bytes, which cannot be"
|
||||
<< " right. Play it back with the .egg that was saved beside"
|
||||
<< " it.\n" << std::flush;
|
||||
PostQuitMessage(AbortExitCodeID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The cursor is now standing on the first packet, so the spooler task
|
||||
// may start reading. Set only after the sanity check above, so a spool
|
||||
// whose header did not add up is never played at all.
|
||||
//
|
||||
gSpoolHeaderConsumed = True;
|
||||
DEBUG_STREAM << "Playback: header consumed, "
|
||||
<< (int) spool->GetBytesRemaining() << " bytes of packets to play\n"
|
||||
<< std::flush;
|
||||
|
||||
//
|
||||
// Now, just send the load message
|
||||
//
|
||||
@@ -663,6 +886,32 @@ void
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Nothing may be read until the header has been taken off the front.
|
||||
//
|
||||
// A spool opens with the application ID, the resource version and one
|
||||
// (remote, hostID) pair per host named in the egg, and the length of
|
||||
// that depends on the egg - so it can only be read once the mission
|
||||
// exists, which is what L4PlaybackNetworkManager::StartConnecting does.
|
||||
// Until then the cursor is sitting on the header, and this function
|
||||
// happily parsed it as packet one: the application ID read as a client
|
||||
// ID, and ours is 0, which is NetworkManagerClientID exactly. The pair
|
||||
// of host IDs after it read as a message length of 1 and a message ID
|
||||
// of 3 - and message 3 on the network manager is ReceiveEggFile, which
|
||||
// builds a Mission. From a packet. That was the heap corruption.
|
||||
//
|
||||
// The WaitingForEgg case below is for an arcade spool that CARRIES its
|
||||
// egg as network manager packets, which cannot work with this format
|
||||
// anyway: reading those packets means passing the header first, and
|
||||
// passing the header means already knowing the egg. A recording made
|
||||
// by this build keeps its egg beside it instead, which is why there is
|
||||
// no such circle to break.
|
||||
//
|
||||
if (!SpoolHeaderConsumed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------------
|
||||
// We have a spool file, so interpret it based upon the application state
|
||||
|
||||
@@ -54,6 +54,14 @@ public:
|
||||
//################### L4PlaybackNetworkManager #########################
|
||||
//##########################################################################
|
||||
|
||||
// True once a spool's header has been read off the front of the stream, so
|
||||
// the cursor is standing on a packet. SpoolerTask must not read before
|
||||
// this: the header's length depends on the egg's host count, so it can only
|
||||
// be taken off once the mission exists, and a task that reads early parses
|
||||
// the header as a packet. See SpoolerTask::Execute.
|
||||
Logical
|
||||
SpoolHeaderConsumed();
|
||||
|
||||
class L4PlaybackNetworkManager:
|
||||
public NetworkManager
|
||||
{
|
||||
|
||||
@@ -3,6 +3,41 @@
|
||||
|
||||
#include "l4steamtransport.h"
|
||||
|
||||
//########################################################################
|
||||
// Is steam_api.dll actually here?
|
||||
//
|
||||
// Defined outside the RP412_STEAM guard on purpose: callers ask without
|
||||
// caring how the build was configured, and a build without the SDK
|
||||
// truthfully has no client library. See the header for why every Steam
|
||||
// path has to come through here first.
|
||||
//
|
||||
// The answer is cached because it is asked on menu paint, and because a
|
||||
// DLL that appeared halfway through a session is not a case worth
|
||||
// supporting - the delay-load helper would have bound the first miss
|
||||
// anyway.
|
||||
//########################################################################
|
||||
Logical
|
||||
SteamNetTransport_ClientLibraryPresent()
|
||||
{
|
||||
#ifdef RP412_STEAM
|
||||
static int present = -1;
|
||||
|
||||
if (present < 0)
|
||||
{
|
||||
present = (LoadLibraryA("steam_api.dll") != NULL) ? 1 : 0;
|
||||
|
||||
if (!present)
|
||||
{
|
||||
DEBUG_STREAM << "Steam: steam_api.dll not found beside the exe - "
|
||||
<< "Steam features off, staying on TCP\n" << std::flush;
|
||||
}
|
||||
}
|
||||
return present ? True : False;
|
||||
#else
|
||||
return False;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef RP412_STEAM
|
||||
|
||||
#include "l4nettransport.h"
|
||||
@@ -350,18 +385,59 @@ namespace
|
||||
target.Clear();
|
||||
target.SetIPv4(remote_fake_ip, fake_port);
|
||||
|
||||
// mirror the TCP retry-while-refused loop, bounded: the
|
||||
// egg-ACK ordering means the peer may not be listening yet
|
||||
DWORD deadline = GetTickCount() + 120 * 1000;
|
||||
//
|
||||
// Mirror the TCP retry-while-refused loop, bounded: the
|
||||
// egg-ACK ordering means the peer may not be listening yet.
|
||||
//
|
||||
// Every wait below goes through NetTransport_PumpAndSleep, so
|
||||
// the window keeps painting and answering instead of being
|
||||
// declared "Not responding" for the whole attempt. There is no
|
||||
// render loop yet at this point - this runs before the engine
|
||||
// block - so nothing else is keeping it alive.
|
||||
//
|
||||
//
|
||||
// The deadline is PER ATTEMPT, and attempts are capped, rather
|
||||
// than one budget across all attempts.
|
||||
//
|
||||
// The 2026-08-11 playtest showed why, on every machine that met
|
||||
// the one badly-NATed peer: the first attempt goes for a direct
|
||||
// path, burns around ten seconds, and dies with 'timed out
|
||||
// attempting to connect' (5003) or 'negotiate rendezvous'
|
||||
// (5008); the SECOND attempt comes up through Valve's relay and
|
||||
// succeeds - whenever it is given time. Under one shared budget
|
||||
// the relay attempt inherited whatever the direct attempt left,
|
||||
// and the logs show it being cut off mid-connect ('attempt 2
|
||||
// ended in state 1' - still connecting) at the 20s mark. Races
|
||||
// then could not assemble, because the mesh needs every pair.
|
||||
//
|
||||
// Three attempts at RP412CONNECTWAIT each: the observed failure
|
||||
// connects on attempt 2 at about half a minute, the truly
|
||||
// unreachable peer costs about a minute instead of twenty
|
||||
// seconds, and ESC still works throughout.
|
||||
//
|
||||
DWORD wait_seconds = (DWORD) NetTransport_ConnectWaitSeconds();
|
||||
DWORD started = GetTickCount();
|
||||
int attempt = 0;
|
||||
const int kMaxAttempts = 3;
|
||||
char progress[256];
|
||||
|
||||
sprintf(
|
||||
progress,
|
||||
"Red Planet - connecting to %s ...",
|
||||
inet_ntoa(remote->sin_addr)
|
||||
);
|
||||
NetTransport_SetWaitProgress(progress);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
++attempt;
|
||||
DWORD deadline = GetTickCount() + wait_seconds * 1000;
|
||||
HSteamNetConnection handle =
|
||||
SteamNetworkingSockets()->ConnectByIPAddress(target, 0, NULL);
|
||||
if (handle == k_HSteamNetConnection_Invalid)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: ConnectByIPAddress refused the call\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
AddConnection(handle, remote->sin_addr.S_un.S_addr, remote->sin_port);
|
||||
@@ -390,7 +466,35 @@ namespace
|
||||
{
|
||||
break;
|
||||
}
|
||||
Sleep(25);
|
||||
|
||||
//
|
||||
// Count down out loud. Two minutes of a silent frozen
|
||||
// window gave a host nothing to act on - not which peer
|
||||
// was missing, not how long was left, not a way out.
|
||||
//
|
||||
DWORD left = (DWORD)((LONG)(deadline - GetTickCount()) / 1000);
|
||||
|
||||
sprintf(
|
||||
progress,
|
||||
"Red Planet - connecting to %s (try %d of %d) ... %us left, ESC to cancel",
|
||||
inet_ntoa(remote->sin_addr),
|
||||
attempt, kMaxAttempts,
|
||||
(unsigned) left
|
||||
);
|
||||
NetTransport_SetWaitProgress(progress);
|
||||
|
||||
NetWaitResult wait = NetTransport_PumpAndSleep(25);
|
||||
if (wait != NetWaitContinue)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: connect "
|
||||
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
||||
<< " after " << ((GetTickCount() - started) / 1000)
|
||||
<< "s\n" << std::flush;
|
||||
SteamNetworkingSockets()->CloseConnection(handle, 0, "cancelled", false);
|
||||
RemoveConnection(handle);
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
}
|
||||
if (state == k_ESteamNetworkingConnectionState_Connected)
|
||||
{
|
||||
@@ -401,20 +505,41 @@ namespace
|
||||
}
|
||||
DEBUG_STREAM << "SteamNetTransport: connect succeeded (attempt "
|
||||
<< attempt << ")\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return (Connection) handle;
|
||||
}
|
||||
|
||||
// attempt failed - drop it and retry until the deadline
|
||||
// attempt failed - drop it and retry with a fresh window
|
||||
DEBUG_STREAM << "SteamNetTransport: attempt " << attempt
|
||||
<< " ended in state " << (int) state << "\n" << std::flush;
|
||||
SteamNetworkingSockets()->CloseConnection(handle, 0, "retry", false);
|
||||
RemoveConnection(handle);
|
||||
if ((LONG)(GetTickCount() - deadline) >= 0)
|
||||
if (attempt >= kMaxAttempts)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: connect timed out\n" << std::flush;
|
||||
DEBUG_STREAM << "SteamNetTransport: gave up after "
|
||||
<< attempt << " attempts of " << wait_seconds
|
||||
<< "s each (RP412CONNECTWAIT), "
|
||||
<< ((GetTickCount() - started) / 1000)
|
||||
<< "s in all\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
Sleep(1000);
|
||||
|
||||
//
|
||||
// A second between redials, still answering the window.
|
||||
//
|
||||
for (int slept = 0; slept < 1000; slept += 50)
|
||||
{
|
||||
NetWaitResult wait = NetTransport_PumpAndSleep(50);
|
||||
if (wait != NetWaitContinue)
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: connect "
|
||||
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
||||
<< " between attempts\n" << std::flush;
|
||||
NetTransport_SetWaitProgress(NULL);
|
||||
return InvalidConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +811,16 @@ Logical
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
// Before ANY Steam symbol: steam_api.dll is delay-loaded, so calling
|
||||
// into it when it is missing raises the helper's fatal exception
|
||||
// rather than failing. This is the gate that makes the DLL optional.
|
||||
//
|
||||
if (!SteamNetTransport_ClientLibraryPresent())
|
||||
{
|
||||
return False;
|
||||
}
|
||||
|
||||
if (!SteamAPI_Init())
|
||||
{
|
||||
DEBUG_STREAM << "SteamNetTransport: SteamAPI_Init failed "
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
//########################################################################
|
||||
// SteamNetTransport - the retail wire (l4steamtransport.cpp). Built
|
||||
// only under RP412_STEAM (Steamworks SDK vendored at
|
||||
// extern\steamworks_sdk_164; steam_api.dll ships beside the exe).
|
||||
// extern\steamworks_sdk_164; steam_api.dll ships beside the exe, but is
|
||||
// delay-loaded and optional - see the note further down).
|
||||
//
|
||||
// Method mapping onto ISteamNetworkingSockets:
|
||||
//
|
||||
@@ -55,6 +56,25 @@
|
||||
// egg is distributed.
|
||||
//########################################################################
|
||||
|
||||
//########################################################################
|
||||
// steam_api.dll is DELAY-LOADED (see the DelayLoadDLLs setting in
|
||||
// RP_L4.vcxproj), so the game runs on a machine that has never seen
|
||||
// Steam - a plain import of a missing DLL kills the process at load time
|
||||
// with 0xC0000135, before a window or a single log line.
|
||||
//
|
||||
// The catch is that delay loading only MOVES the failure: the first call
|
||||
// to a delay-loaded function whose DLL cannot be found raises a fatal
|
||||
// exception instead. So every path that would reach a Steam symbol has
|
||||
// to ask this first. It is declared outside the RP412_STEAM guard, and
|
||||
// answers False in a build without the SDK, so callers need no #ifdef.
|
||||
//
|
||||
// Cheap and idempotent: one LoadLibrary on first call, cached after.
|
||||
// Deliberately the same plain-name load the delay-load helper itself
|
||||
// does, so a yes here means the helper will succeed too.
|
||||
//########################################################################
|
||||
Logical
|
||||
SteamNetTransport_ClientLibraryPresent();
|
||||
|
||||
#ifdef RP412_STEAM
|
||||
|
||||
// Bring Steam up and make this the process transport. False (with the
|
||||
|
||||
+42
-1
@@ -14,6 +14,7 @@
|
||||
SystemClock SystemClock::timer;
|
||||
long SystemClock::ticksPerSecond;
|
||||
__int64 SystemClock::perfCounterFreq;
|
||||
__int64 SystemClock::perfCounterOrigin;
|
||||
|
||||
//RB 1/20/07
|
||||
//volatile long fast_time = 0L;
|
||||
@@ -53,11 +54,42 @@ void Timer_Handler()
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
//
|
||||
// Milliseconds since this process started.
|
||||
//
|
||||
// It used to be milliseconds since the machine BOOTED, which is what
|
||||
// QueryPerformanceCounter counts from, and that had two consequences.
|
||||
//
|
||||
// The quiet one: Scalar is a 32-bit float, so an absolute time held in one
|
||||
// loses resolution as the number grows. Consecutive representable values
|
||||
// are 3.9ms apart after nine hours of uptime, 15.6ms after a day and a
|
||||
// half, and 31.25ms after three days - by which point the clock can no
|
||||
// longer resolve a single 20ms physics step. Anything computed by
|
||||
// subtracting two absolute times IN FLOAT inherits that, and the joystick
|
||||
// poll interval in L4CTRL is exactly such a subtraction: its 50ms test
|
||||
// quietly becomes 62.5ms after three days of uptime and 125ms after
|
||||
// twelve, so a player's controls grow less responsive the longer the
|
||||
// machine has been switched on. The smoke emitter in L4VIDRND has the same
|
||||
// defect, where adding a small interval to a large timestamp can round to
|
||||
// no change at all.
|
||||
//
|
||||
// The loud one: this returns a long, and milliseconds since boot overflows
|
||||
// one after 24.8 days.
|
||||
//
|
||||
// Counting from launch fixes both at the source and leaves every Time
|
||||
// arithmetic path untouched - those subtract ticks as integers and were
|
||||
// always exact. Peer machines already disagreed about this origin, having
|
||||
// booted at different moments, so the network is no worse off; reconciling
|
||||
// that is what RP412NETCLOCK does.
|
||||
//
|
||||
long SystemClock::GetRTC()
|
||||
{
|
||||
LARGE_INTEGER count;
|
||||
QueryPerformanceCounter(&count);
|
||||
return (long)((count.QuadPart * (__int64)1000) / SystemClock::perfCounterFreq);
|
||||
return (long)(
|
||||
((count.QuadPart - SystemClock::perfCounterOrigin) * (__int64)1000)
|
||||
/ SystemClock::perfCounterFreq
|
||||
);
|
||||
}
|
||||
|
||||
double SystemClock::GetHiRes()
|
||||
@@ -97,6 +129,15 @@ SystemClock::SystemClock()
|
||||
//SystemClock::ticksPerSecond = freq.QuadPart;
|
||||
SystemClock::perfCounterFreq = freq.QuadPart;
|
||||
SystemClock::ticksPerSecond = 1000L;
|
||||
|
||||
//
|
||||
// Time zero. Set here rather than on the first GetRTC call so that the
|
||||
// origin is fixed before anything can read the clock, and so no two
|
||||
// threads can race to establish it.
|
||||
//
|
||||
LARGE_INTEGER origin;
|
||||
QueryPerformanceCounter(&origin);
|
||||
SystemClock::perfCounterOrigin = origin.QuadPart;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
+332
-49
@@ -192,7 +192,7 @@ void SVGA16::BuildWindows(unsigned int width, unsigned int height, bool windowed
|
||||
mPresentParams[j].hDeviceWindow = gaugeWindows[j];
|
||||
mPresentParams[j].Flags = 0;
|
||||
mPresentParams[j].FullScreen_RefreshRateInHz = (windowed)?D3DPRESENT_RATE_DEFAULT:60;
|
||||
mPresentParams[j].PresentationInterval = D3DPRESENT_RATE_DEFAULT;
|
||||
mPresentParams[j].PresentationInterval = RPPresentationInterval();
|
||||
mPresentParams[j].BackBufferFormat = D3DFMT_R5G6B5;
|
||||
//pp.EnableAutoDepthStencil = TRUE;
|
||||
//pp.AutoDepthStencilFormat = D3DFMT_D24X8;
|
||||
@@ -3828,6 +3828,13 @@ static LRESULT CALLBACK
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
static WNDPROC gCockpitBaseProc = NULL;
|
||||
|
||||
//
|
||||
// Which window we subclassed, so the destructor can put its own proc
|
||||
// back. The shell is the GAME window: it outlives the cockpit and
|
||||
// carries the console screen from one race to the next.
|
||||
//
|
||||
static HWND gCockpitShellWindow = NULL;
|
||||
|
||||
static LRESULT CALLBACK
|
||||
CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
@@ -3974,9 +3981,25 @@ static RadarPlacement
|
||||
{ "RIGHTCENTRE", RadarMidRight }
|
||||
};
|
||||
|
||||
cached = RadarBottomCenter;
|
||||
//
|
||||
// The pod had it dead centre under the viewscreen, so that is the
|
||||
// default for a pod. A Live Cam is a shot, not a cockpit: dead
|
||||
// centre is the worst place to put a panel on a picture, so a
|
||||
// camera station defaults to the bottom-left corner instead.
|
||||
// L4RADARPOS still overrides either way.
|
||||
//
|
||||
// A pod and a Live Cam want this display in different places, and
|
||||
// the same host does both from one menu - so they get a knob each
|
||||
// and neither has to be edited when the role changes. L4RADARPOS
|
||||
// places the pod's radar, L4MAPPOS the camera's map. Defaults
|
||||
// differ too: dead centre under the viewscreen is where the
|
||||
// cabinet had it, and the worst place to put a panel on a shot.
|
||||
const Logical camera_station = Application::IsCameraStation();
|
||||
const char *variable = camera_station ? "L4MAPPOS" : "L4RADARPOS";
|
||||
|
||||
const char *text = getenv("L4RADARPOS");
|
||||
cached = camera_station ? RadarBottomLeft : RadarBottomCenter;
|
||||
|
||||
const char *text = getenv(variable);
|
||||
if (text != NULL)
|
||||
{
|
||||
for (int i = 0; i < (int) (sizeof(names) / sizeof(names[0])); ++i)
|
||||
@@ -3997,8 +4020,9 @@ static RadarPlacement
|
||||
"left side, centred",
|
||||
"right side, centred"
|
||||
};
|
||||
DEBUG_STREAM << "SVGA16: radar on the " << described[cached]
|
||||
<< "\n" << std::flush;
|
||||
DEBUG_STREAM << "SVGA16: " << (camera_station ? "map" : "radar")
|
||||
<< " on the " << described[cached]
|
||||
<< " (" << variable << ")\n" << std::flush;
|
||||
}
|
||||
return (RadarPlacement) cached;
|
||||
}
|
||||
@@ -4036,22 +4060,44 @@ void
|
||||
DisplayScalePercent("L4MFDSCALE_LL", group);
|
||||
percent[SplitMFDLowerRight] =
|
||||
DisplayScalePercent("L4MFDSCALE_LR", group);
|
||||
percent[SplitMap] = DisplayScalePercent("L4RADARSCALE", 100);
|
||||
//
|
||||
// Sized by its own knob for the same reason it is placed by one: a
|
||||
// camera's map and a pod's radar are the same pane doing different
|
||||
// jobs, and the host switches between them from the menu without
|
||||
// touching this file.
|
||||
//
|
||||
percent[SplitMap] = Application::IsCameraStation()
|
||||
? DisplayScalePercent("L4MAPSCALE", 100)
|
||||
: DisplayScalePercent("L4RADARSCALE", 100);
|
||||
|
||||
DEBUG_STREAM << "SVGA16: secondary displays at UL "
|
||||
<< percent[SplitMFDUpperLeft] << "% UC "
|
||||
<< percent[SplitMFDUpperCenter] << "% UR "
|
||||
<< percent[SplitMFDUpperRight] << "% LL "
|
||||
<< percent[SplitMFDLowerLeft] << "% LR "
|
||||
<< percent[SplitMFDLowerRight] << "% radar "
|
||||
<< percent[SplitMFDLowerRight] << "% "
|
||||
<< (Application::IsCameraStation() ? "map " : "radar ")
|
||||
<< percent[SplitMap] << "%\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// A Live Cam's map lies down: the pod's is 324x432 because the glass
|
||||
// was mounted portrait in the cabinet, and a camera station has no
|
||||
// cabinet, so the same pixels are shown the way they are drawn.
|
||||
//
|
||||
const Logical camera_station = Application::IsCameraStation();
|
||||
|
||||
GlassSize glass[SplitViewCount];
|
||||
for (int view = 0; view < SplitViewCount; ++view)
|
||||
{
|
||||
int base_w = (view == SplitMap) ? cockpitRadarBaseW : cockpitMfdBaseW;
|
||||
int base_h = (view == SplitMap) ? cockpitRadarBaseH : cockpitMfdBaseH;
|
||||
if (view == SplitMap && camera_station)
|
||||
{
|
||||
int swap = base_w;
|
||||
base_w = base_h;
|
||||
base_h = swap;
|
||||
}
|
||||
glass[view].w = (base_w * percent[view]) / 100;
|
||||
glass[view].h = (base_h * percent[view]) / 100;
|
||||
}
|
||||
@@ -4358,6 +4404,15 @@ SVGA16::SVGA16(
|
||||
// Split-view mode: decide before BuildWindows so the packed gauge
|
||||
// windows can stay hidden.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
// Before anything else: the constructor calls Update() below, and
|
||||
// both of these are read in there. mDisplayToUpdate was only being
|
||||
// set at the END of the constructor, so that first pass indexed the
|
||||
// display arrays with whatever was on the stack.
|
||||
//
|
||||
mDisplayToUpdate = 0;
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
|
||||
splitViews = False;
|
||||
cockpitViewscreen = NULL;
|
||||
Logical explodedViews = False;
|
||||
@@ -4640,31 +4695,53 @@ SVGA16::SVGA16(
|
||||
GlassSize glass[SplitViewCount];
|
||||
CockpitGlassSizes(scale_num, glass);
|
||||
|
||||
splitView[SplitMFDUpperLeft] = new MFDSplitView(
|
||||
"MFD upper left", init_width, init_height,
|
||||
glass[SplitMFDUpperLeft].w, glass[SplitMFDUpperLeft].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x2F, 0, cockpit);
|
||||
splitView[SplitMFDUpperCenter] = new MFDSplitView(
|
||||
"MFD upper center", init_width, init_height,
|
||||
glass[SplitMFDUpperCenter].w, glass[SplitMFDUpperCenter].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x27, 0, cockpit);
|
||||
splitView[SplitMFDUpperRight] = new MFDSplitView(
|
||||
"MFD upper right", init_width, init_height,
|
||||
glass[SplitMFDUpperRight].w, glass[SplitMFDUpperRight].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x37, 0, cockpit);
|
||||
splitView[SplitMFDLowerLeft] = new MFDSplitView(
|
||||
"MFD lower left", init_width, init_height,
|
||||
glass[SplitMFDLowerLeft].w, glass[SplitMFDLowerLeft].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x0F, 0, cockpit);
|
||||
splitView[SplitMFDLowerRight] = new MFDSplitView(
|
||||
"MFD lower right", init_width, init_height,
|
||||
glass[SplitMFDLowerRight].w, glass[SplitMFDLowerRight].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x07, 0, cockpit);
|
||||
// map is portrait: source rotated 90 degrees clockwise
|
||||
splitView[SplitMap] = new MFDSplitView(
|
||||
"Map", init_height, init_width,
|
||||
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
|
||||
MFDSplitView::SideColumns, 0x10, 0x18, cockpit);
|
||||
//---------------------------------------------------------------
|
||||
// A Live Cam gets the map and nothing else. The five instrument
|
||||
// MFDs belong to a pod: with no pod behind them their panes carry
|
||||
// no image at all and composite as black rectangles over the
|
||||
// viewscreen, which is what the arcade's camera cabinet avoided by
|
||||
// running with -lc. LayoutCockpit and FillSplitMFD both already
|
||||
// skip a NULL pane, so leaving them unbuilt is all it takes.
|
||||
//---------------------------------------------------------------
|
||||
if (!Application::IsCameraStation())
|
||||
{
|
||||
splitView[SplitMFDUpperLeft] = new MFDSplitView(
|
||||
"MFD upper left", init_width, init_height,
|
||||
glass[SplitMFDUpperLeft].w, glass[SplitMFDUpperLeft].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x2F, 0, cockpit);
|
||||
splitView[SplitMFDUpperCenter] = new MFDSplitView(
|
||||
"MFD upper center", init_width, init_height,
|
||||
glass[SplitMFDUpperCenter].w, glass[SplitMFDUpperCenter].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x27, 0, cockpit);
|
||||
splitView[SplitMFDUpperRight] = new MFDSplitView(
|
||||
"MFD upper right", init_width, init_height,
|
||||
glass[SplitMFDUpperRight].w, glass[SplitMFDUpperRight].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x37, 0, cockpit);
|
||||
splitView[SplitMFDLowerLeft] = new MFDSplitView(
|
||||
"MFD lower left", init_width, init_height,
|
||||
glass[SplitMFDLowerLeft].w, glass[SplitMFDLowerLeft].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x0F, 0, cockpit);
|
||||
splitView[SplitMFDLowerRight] = new MFDSplitView(
|
||||
"MFD lower right", init_width, init_height,
|
||||
glass[SplitMFDLowerRight].w, glass[SplitMFDLowerRight].h, 0, 0,
|
||||
MFDSplitView::MFDStrips, 0x07, 0, cockpit);
|
||||
}
|
||||
//
|
||||
// The map's source canvas is landscape and the pod rotates it 90
|
||||
// degrees clockwise to match its portrait mounting, which is why
|
||||
// the pane is built with the source dimensions swapped. A camera
|
||||
// shows it as drawn, so it is not swapped and not rotated (see the
|
||||
// copy in ExecuteBackground).
|
||||
//
|
||||
splitView[SplitMap] = Application::IsCameraStation()
|
||||
? new MFDSplitView(
|
||||
"Map", init_width, init_height,
|
||||
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
|
||||
MFDSplitView::SideColumns, 0x10, 0x18, cockpit)
|
||||
: new MFDSplitView(
|
||||
"Map", init_height, init_width,
|
||||
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
|
||||
MFDSplitView::SideColumns, 0x10, 0x18, cockpit);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Everything above built the panes at a starting size; the
|
||||
@@ -4679,9 +4756,25 @@ SVGA16::SVGA16(
|
||||
GetClientRect(cockpit, &inner);
|
||||
LayoutCockpit(inner.right, inner.bottom);
|
||||
|
||||
// catch maximise / restore / drag-resize and re-fit
|
||||
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
||||
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
||||
//
|
||||
// Catch maximise / restore / drag-resize and re-fit - ONCE.
|
||||
//
|
||||
// The destructor puts the original proc back, so ordinarily
|
||||
// this window is unsubclassed by the time a second race
|
||||
// builds a new cockpit. The guard is for the case where it
|
||||
// was not: subclassing an already-subclassed window makes
|
||||
// SetWindowLongPtr hand back CockpitShellProc itself as the
|
||||
// "original", and the proc below then chains to itself on
|
||||
// every single message until the stack runs out. That is a
|
||||
// stack overflow a few frames into the second race, with no
|
||||
// hint of a cause in the log.
|
||||
//
|
||||
if (gCockpitShellWindow != cockpit)
|
||||
{
|
||||
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
||||
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
||||
gCockpitShellWindow = cockpit;
|
||||
}
|
||||
|
||||
//
|
||||
// Sticky placement for the shell. Position AND size: nothing
|
||||
@@ -4917,6 +5010,30 @@ SVGA16::~SVGA16()
|
||||
cockpitViewscreen = NULL;
|
||||
}
|
||||
|
||||
//
|
||||
// Give the game window its own proc back, and stop answering for a
|
||||
// cockpit that is about to stop existing.
|
||||
//
|
||||
// The window survives us - it is the one that shows the console
|
||||
// screen between races - so both of these outlived their subject.
|
||||
// The subclass was the worse of the two: the next race re-subclassed
|
||||
// the same window and CockpitShellProc ended up chained to itself.
|
||||
// activeCockpit was the quieter one, left pointing at this object
|
||||
// after it was freed, ready for the next WM_SIZE to lay out a
|
||||
// cockpit that had already gone.
|
||||
//
|
||||
if (gCockpitShellWindow != NULL)
|
||||
{
|
||||
SetWindowLongPtrA(
|
||||
gCockpitShellWindow, GWLP_WNDPROC, (LONG_PTR) gCockpitBaseProc);
|
||||
gCockpitShellWindow = NULL;
|
||||
gCockpitBaseProc = NULL;
|
||||
}
|
||||
if (activeCockpit == this)
|
||||
{
|
||||
activeCockpit = NULL;
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -4997,12 +5114,16 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
GaugeRenderer *renderer = application->GetGaugeRenderer();
|
||||
if (!valid || renderer == NULL)
|
||||
{
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
CLEAR_SCREEN_COPY();
|
||||
return False; // Do no more!
|
||||
}
|
||||
|
||||
if (++mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
// one display per call; the rotation steps at the end of the function
|
||||
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
{
|
||||
mDisplayToUpdate = 0;
|
||||
}
|
||||
|
||||
//Top MFD's
|
||||
L4GraphicsPort *UL = static_cast<L4GraphicsPort*>(renderer->GetGraphicsPort("auxUL2"));
|
||||
@@ -5045,7 +5166,29 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
lrMask |= (lrMask << 16);
|
||||
} else
|
||||
{
|
||||
//No MFDs to draw, break out early
|
||||
//No MFDs to draw, break out early. The sweep counter resets
|
||||
//too: leaving it part-used would keep the renderer in its
|
||||
//copy phase, and it never draws another gauge while there.
|
||||
//
|
||||
// The ROTATION has to step on the way out as well, and did
|
||||
// not. It lives at the END of this function, so returning
|
||||
// from here parked mDisplayToUpdate on the display we cannot
|
||||
// service - and a station with no MFD ports can never
|
||||
// service it, so the counter stopped dead the first time it
|
||||
// landed there. Display 0's copy, which is the map, then
|
||||
// never ran again: a Live Cam, whose cameraInit page
|
||||
// configures the secondary port and nothing else, kept
|
||||
// whatever the one early pass had put on the pane. A blank
|
||||
// map, because nothing had registered with the renderer that
|
||||
// early, beside a score frozen at its opening value - while
|
||||
// the canvas underneath went on being drawn perfectly, which
|
||||
// is what made this so hard to see.
|
||||
mDisplayToUpdate++;
|
||||
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
{
|
||||
mDisplayToUpdate = 0;
|
||||
}
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
return False;
|
||||
}
|
||||
} else
|
||||
@@ -5057,7 +5200,14 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
||||
} else
|
||||
{
|
||||
//No secondary, skip
|
||||
//No secondary, skip - and end the sweep, as above, stepping
|
||||
//past the display we cannot service for the same reason.
|
||||
mDisplayToUpdate++;
|
||||
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
{
|
||||
mDisplayToUpdate = 0;
|
||||
}
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
return False;
|
||||
}
|
||||
}
|
||||
@@ -5072,23 +5222,83 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
{
|
||||
if (splitView[SplitMap] != NULL)
|
||||
{
|
||||
// Map is portrait-mounted: rotate 90 degrees clockwise.
|
||||
// dest(x,y) = source(row = srcH-1-x, col = y)
|
||||
Word *source_base = pixelBuffer.Data.MapPointer;
|
||||
unsigned long *dest = splitView[SplitMap]->Pixels();
|
||||
int src_w = pixelBuffer.Data.Size.x;
|
||||
int src_h = pixelBuffer.Data.Size.y;
|
||||
|
||||
for (int dy = 0; dy < src_w; ++dy)
|
||||
//
|
||||
// RP412CAMLOG. A camera's map showed content at mission
|
||||
// start and then went black, which is three different
|
||||
// faults wearing one symptom: the copy stopping, the
|
||||
// SOURCE canvas going blank, or the pane not repainting.
|
||||
// Counting non-zero source pixels as we pass tells them
|
||||
// apart - a live canvas with a black pane is the third,
|
||||
// a blank canvas is the second, and no line at all is the
|
||||
// first. Every five seconds; the count is a sample of one
|
||||
// row in sixteen, which is plenty to tell blank from not.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
for (int dx = 0; dx < src_h; ++dx)
|
||||
static Scalar next_copy_say = 0.0f;
|
||||
if ((Scalar) Now() >= next_copy_say)
|
||||
{
|
||||
Word pixel = source_base[(src_h - 1 - dx) * src_w + dy];
|
||||
PaletteTriplet *entry =
|
||||
&secPalette->paletteData.Color[pixel & secMask];
|
||||
*dest++ = ((unsigned long) entry->Red << 16) |
|
||||
((unsigned long) entry->Green << 8) |
|
||||
((unsigned long) entry->Blue);
|
||||
next_copy_say = ((Scalar) Now()) + 5.0f;
|
||||
|
||||
long lit = 0;
|
||||
for (int sy = 0; sy < src_h; sy += 16)
|
||||
{
|
||||
Word *row = source_base + sy * src_w;
|
||||
for (int sx = 0; sx < src_w; ++sx)
|
||||
{
|
||||
if (row[sx] != 0)
|
||||
{
|
||||
++lit;
|
||||
}
|
||||
}
|
||||
}
|
||||
DEBUG_STREAM << "CamLog: map copy running, source "
|
||||
<< src_w << "x" << src_h << ", " << lit
|
||||
<< " lit pixels sampled, mask 0x" << std::hex
|
||||
<< secMask << std::dec << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
if (Application::IsCameraStation())
|
||||
{
|
||||
//
|
||||
// Live Cam: straight through, row by row. The pod's
|
||||
// rotation below is about how the glass was bolted
|
||||
// into the cabinet, and there is no cabinet here.
|
||||
//
|
||||
for (int dy = 0; dy < src_h; ++dy)
|
||||
{
|
||||
for (int dx = 0; dx < src_w; ++dx)
|
||||
{
|
||||
Word pixel = source_base[dy * src_w + dx];
|
||||
PaletteTriplet *entry =
|
||||
&secPalette->paletteData.Color[pixel & secMask];
|
||||
*dest++ = ((unsigned long) entry->Red << 16) |
|
||||
((unsigned long) entry->Green << 8) |
|
||||
((unsigned long) entry->Blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Map is portrait-mounted: rotate 90 degrees clockwise.
|
||||
// dest(x,y) = source(row = srcH-1-x, col = y)
|
||||
for (int dy = 0; dy < src_w; ++dy)
|
||||
{
|
||||
for (int dx = 0; dx < src_h; ++dx)
|
||||
{
|
||||
Word pixel = source_base[(src_h - 1 - dx) * src_w + dy];
|
||||
PaletteTriplet *entry =
|
||||
&secPalette->paletteData.Color[pixel & secMask];
|
||||
*dest++ = ((unsigned long) entry->Red << 16) |
|
||||
((unsigned long) entry->Green << 8) |
|
||||
((unsigned long) entry->Blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
splitView[SplitMap]->Repaint();
|
||||
@@ -5227,7 +5437,80 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
// if (end.ticks - start.ticks > 100)
|
||||
// end = start;
|
||||
|
||||
return False; // True == 'more to do'
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// Step the rotation, and say "more to do" until every display has
|
||||
// had its turn.
|
||||
//
|
||||
// This used to return False unconditionally, which told the gauge
|
||||
// renderer its copy phase was over after a SINGLE display. A full
|
||||
// gauge sweep - one gauge per background pass, so as many passes as
|
||||
// there are active gauges - therefore refreshed one display, and the
|
||||
// map, one of three, came round only every third sweep.
|
||||
//
|
||||
// That is invisible with frame time to spare, because the background
|
||||
// loop keeps running until the frame budget is used up and gets
|
||||
// through several sweeps. On a big map the 3D foreground eats the
|
||||
// whole budget, the loop drops to the one pass per frame it is
|
||||
// guaranteed, and the map goes seconds between refreshes - which is
|
||||
// what the field reports describe, on exactly those maps. A death
|
||||
// makes the renderer skip every static object, the budget frees up,
|
||||
// and the backlog drains at once: the display appears to come back
|
||||
// to life, which is the tell that led here.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
mDisplayToUpdate++;
|
||||
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
{
|
||||
mDisplayToUpdate = 0;
|
||||
}
|
||||
|
||||
if (++mDisplaysCopiedThisPass < NUMGAUGEWINDOWS)
|
||||
{
|
||||
return True; // call again - there are displays waiting
|
||||
}
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
|
||||
//
|
||||
// RP412GAUGEDIAG=1 reports how often the displays are actually being
|
||||
// refreshed. Pixel-watching from outside cannot tell a display that
|
||||
// is not refreshing from one whose picture simply is not changing,
|
||||
// and that ambiguity is exactly what makes "my map froze" hard to
|
||||
// pin down. This counts the real thing.
|
||||
//
|
||||
{
|
||||
static int diagnostics = -1;
|
||||
if (diagnostics < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412GAUGEDIAG");
|
||||
diagnostics = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diagnostics)
|
||||
{
|
||||
static unsigned long window_start = 0;
|
||||
static int sweeps = 0;
|
||||
unsigned long now = GetTickCount();
|
||||
++sweeps;
|
||||
if (window_start == 0)
|
||||
{
|
||||
window_start = now;
|
||||
}
|
||||
else if (now - window_start >= 2000)
|
||||
{
|
||||
// tenths, by hand: whole sweeps per second rounds the
|
||||
// interesting cases - a starved pipeline managing two
|
||||
// thirds of a sweep a second reads as a flat "0/s".
|
||||
int tenths = sweeps * 10000 / (int)(now - window_start);
|
||||
DEBUG_STREAM << "GaugeDiag: " << sweeps << " display sweep(s) in "
|
||||
<< (now - window_start) << " ms ("
|
||||
<< (tenths / 10) << '.' << (tenths % 10) << "/s, "
|
||||
<< NUMGAUGEWINDOWS << " displays each)\n" << std::flush;
|
||||
window_start = now;
|
||||
sweeps = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return False; // the sweep is complete
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -293,6 +293,17 @@ private:
|
||||
|
||||
int mDisplayToUpdate;
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// How many displays this copy pass has refreshed.
|
||||
//
|
||||
// The gauge renderer's copy phase ends the moment Update() reports it
|
||||
// has finished, and Update() reported that after ONE display - so a
|
||||
// whole gauge sweep refreshed a single display, and the map, one of
|
||||
// three, came round only every third sweep. Counting them out means
|
||||
// one sweep refreshes all of them.
|
||||
//------------------------------------------------------------------
|
||||
int mDisplaysCopiedThisPass;
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and
|
||||
// the rotated map render as their own desktop windows; the packed
|
||||
|
||||
+346
-35
@@ -17,6 +17,10 @@
|
||||
#include "..\munga\nttmgr.h"
|
||||
#include "..\munga\app.h"
|
||||
#include "l4particles.h"
|
||||
#include "l4padrio.h" // PadRIO::IsActive, for the per-frame lamp sweep
|
||||
#include "..\munga\gaugrend.h"
|
||||
#include "..\munga\lamp.h"
|
||||
#include "..\munga\mode.h"
|
||||
#include "DXUtils.h"
|
||||
|
||||
using namespace std;
|
||||
@@ -27,10 +31,71 @@ using namespace std;
|
||||
|
||||
LPDIRECT3D9 gD3D = NULL;
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// RPPresentationInterval
|
||||
//#############################################################################
|
||||
//
|
||||
// Every device in this game is created vsync-locked, and on a machine with
|
||||
// a spare 23 cores that is the most expensive line in the build.
|
||||
//
|
||||
// The game is one thread: simulation, 3D, gauge drawing and the display
|
||||
// copies all take turns on it. The frame loop runs the foreground, then
|
||||
// spends whatever is LEFT of the frame on the background gauge work. A
|
||||
// Present that blocks until the panel's next retrace spends that remainder
|
||||
// doing nothing at all - and the gauge loop, guaranteed only a single step
|
||||
// per frame, gets exactly that single step. A pass over the gauge list
|
||||
// needs about twenty, so the cockpit falls to two passes a second and every
|
||||
// slow-tier instrument sits seconds behind.
|
||||
//
|
||||
// That is why lowering TARGETFPS "fixed" the instruments: it did not make
|
||||
// anything faster, it just made the frame long enough that there was time
|
||||
// left over after the wait.
|
||||
//
|
||||
// So this is a knob. 0 = IMMEDIATE, Present returns and the leftover frame
|
||||
// time goes to the gauges where it belongs. Tearing is the cost, and on a
|
||||
// pod cockpit whose instruments are the point, it is a cheap one.
|
||||
//
|
||||
DWORD
|
||||
RPPresentationInterval()
|
||||
{
|
||||
static DWORD
|
||||
interval = 0xFFFFFFFF;
|
||||
|
||||
if (interval == 0xFFFFFFFF)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412VSYNC");
|
||||
|
||||
interval = (setting != NULL && atoi(setting) == 0)
|
||||
? D3DPRESENT_INTERVAL_IMMEDIATE
|
||||
: D3DPRESENT_INTERVAL_DEFAULT;
|
||||
|
||||
DEBUG_STREAM << "Video: presentation interval "
|
||||
<< ((interval == D3DPRESENT_INTERVAL_IMMEDIATE)
|
||||
? "IMMEDIATE (RP412VSYNC=0)" : "vsync")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
// Single-window cockpit: viewscreen child window the scene presents into
|
||||
// (NULL = present to the device window as always).
|
||||
HWND gMainPresentWindow = NULL;
|
||||
|
||||
//
|
||||
// Alt+W wireframe (RP412DEVKEYS). File scope rather than a DPLRenderer
|
||||
// member because a fresh renderer is built per mission - a member would
|
||||
// drop the toggle every time the race restarted, which is exactly when
|
||||
// you are looking at geometry.
|
||||
//
|
||||
// DPLToggleWireframe only flips this; the fill mode is applied once per
|
||||
// frame in ExecuteImplementation. Setting it there rather than in the key
|
||||
// handler means it re-asserts itself after a device Reset, which reverts
|
||||
// the fill mode to D3DFILL_SOLID underneath us.
|
||||
//
|
||||
static Logical gWireframe = 0;
|
||||
|
||||
//STUBBED: DPL RB 1/14/07
|
||||
// when this is resolved it can be removed
|
||||
#include "..\DPLSTUB.h"
|
||||
@@ -1686,16 +1751,33 @@ DPLRenderer::DPLRenderer(
|
||||
mPresentParams.hDeviceWindow = hWnd;
|
||||
mPresentParams.Flags = 0;
|
||||
mPresentParams.FullScreen_RefreshRateInHz = (fullscreen)?60:D3DPRESENT_RATE_DEFAULT;
|
||||
mPresentParams.PresentationInterval = D3DPRESENT_RATE_DEFAULT;
|
||||
mPresentParams.PresentationInterval = RPPresentationInterval();
|
||||
mPresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
|
||||
mPresentParams.EnableAutoDepthStencil = TRUE;
|
||||
mPresentParams.AutoDepthStencilFormat = D3DFMT_D24X8;
|
||||
mPresentParams.Windowed = !fullscreen;
|
||||
if (fullscreen)
|
||||
{
|
||||
mPresentParams.BackBufferWidth = screenWidth;
|
||||
mPresentParams.BackBufferHeight = screenHeight;
|
||||
}
|
||||
//
|
||||
// The render size is asked for, not suggested - windowed as well as
|
||||
// full-screen. Left at zero, D3D sizes the back buffer to the device
|
||||
// window's client area AT THIS MOMENT, and everything downstream is
|
||||
// built from the size we asked for instead: the projection matrix
|
||||
// takes its aspect from it, and the reticle is centred on it. A
|
||||
// window that is not exactly that size therefore renders at the
|
||||
// wrong shape and gets rescaled on the way to the viewscreen pane.
|
||||
//
|
||||
// It only showed up on a SECOND race. A fresh renderer is built per
|
||||
// mission while the window carries its cockpit placement across, so
|
||||
// the first race creates its device against a still-bordered window
|
||||
// - near enough the asked-for size to pass - and the next one
|
||||
// against the borderless full-monitor client, which on a 3440x1440
|
||||
// panel meant a 1.778 image drawn across a 2.389 target.
|
||||
//
|
||||
// -fit picks a render size to land on the viewscreen 1:1, so honour
|
||||
// it: the back buffer is that size, and Present scales it to the
|
||||
// pane in one uniform step.
|
||||
//
|
||||
mPresentParams.BackBufferWidth = screenWidth;
|
||||
mPresentParams.BackBufferHeight = screenHeight;
|
||||
|
||||
HRESULT hr;
|
||||
|
||||
@@ -1725,6 +1807,15 @@ DPLRenderer::DPLRenderer(
|
||||
//}
|
||||
//DEBUG_STREAM<<"**************************"<<std::endl<<"**************************"<<std::endl<<std::flush;
|
||||
|
||||
//
|
||||
// NULL before anything can leave this constructor early. It was not
|
||||
// in the initialiser list, so until CreateDevice wrote it the member
|
||||
// held whatever was on the stack - and the bail-out below it, plus
|
||||
// the one that has always been here, both run the destructor and its
|
||||
// SAFE_RELEASE(mDevice) over exactly that.
|
||||
//
|
||||
mDevice = NULL;
|
||||
|
||||
if (mPrimaryIndex == NULL)
|
||||
{
|
||||
DEBUG_STREAM<<"Unable to locate a suitable primary device index."<<std::endl<<std::flush;
|
||||
@@ -1732,16 +1823,76 @@ DPLRenderer::DPLRenderer(
|
||||
return;
|
||||
}
|
||||
|
||||
V(gD3D->CreateDevice(*mPrimaryIndex, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &mPresentParams, &mDevice));
|
||||
//
|
||||
// RP412VERTEXPROC=hw asks the GPU to transform vertices instead of
|
||||
// this thread.
|
||||
//
|
||||
// The device has always been created SOFTWARE_VERTEXPROCESSING - every
|
||||
// vertex on the track transformed and lit on the CPU, on the one core
|
||||
// this game uses for everything. That was not a choice when the engine
|
||||
// was written; there was no hardware to hand it to. There is now, and
|
||||
// the foreground is spending 17 ms of an 18 ms frame while the gauge
|
||||
// loop starves on the 1 ms left over.
|
||||
//
|
||||
// On by default, and sw is the way back. Fixed-function T&L is not
|
||||
// bit-identical between the old software path and a driver, so the
|
||||
// escape hatch stays - but the picture was checked against both and
|
||||
// the difference is not the one worth defending. A cockpit whose
|
||||
// instruments update twice a second is.
|
||||
//
|
||||
DWORD vertex_processing = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
|
||||
{
|
||||
const char *setting = getenv("RP412VERTEXPROC");
|
||||
if (setting == NULL || (*setting != 's' && *setting != 'S'))
|
||||
{
|
||||
D3DCAPS9 caps;
|
||||
if (SUCCEEDED(gD3D->GetDeviceCaps(*mPrimaryIndex, D3DDEVTYPE_HAL, &caps))
|
||||
&& (caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) != 0)
|
||||
{
|
||||
vertex_processing = D3DCREATE_HARDWARE_VERTEXPROCESSING;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "Video: adapter has no hardware T&L - "
|
||||
<< "staying on software vertex processing\n" << std::flush;
|
||||
}
|
||||
}
|
||||
DEBUG_STREAM << "Video: vertex processing "
|
||||
<< ((vertex_processing == D3DCREATE_HARDWARE_VERTEXPROCESSING)
|
||||
? "HARDWARE" : "software (RP412VERTEXPROC=sw)")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
V(gD3D->CreateDevice(*mPrimaryIndex, D3DDEVTYPE_HAL, hWnd, vertex_processing, &mPresentParams, &mDevice));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
DEBUG_STREAM<<"Couldn't create HARDWARE_VERTEXPROCESSING device."<<std::endl<<std::flush;
|
||||
DEBUG_STREAM<<"Couldn't create the requested device - falling back to software vertex processing."<<std::endl<<std::flush;
|
||||
|
||||
V(gD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &mPresentParams, &mDevice));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
PostQuitMessage(1);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// PostQuitMessage is a message, not a return. The fallback used to
|
||||
// post one and then carry straight on into the Clear below, which
|
||||
// dereferenced a device that was never created - so a machine that
|
||||
// could not give us the mode we asked for died on an access
|
||||
// violation instead of saying so.
|
||||
//
|
||||
// What was asked for goes in the line, because that is the question
|
||||
// this failure raises: the back buffer is the requested size now,
|
||||
// windowed as well as full-screen, so a request the adapter will not
|
||||
// meet is the thing to look at first.
|
||||
//
|
||||
if (FAILED(hr) || mDevice == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "DPLRenderer: no D3D device for a "
|
||||
<< mPresentParams.BackBufferWidth << "x"
|
||||
<< mPresentParams.BackBufferHeight
|
||||
<< (mPresentParams.Windowed ? " windowed" : " full-screen")
|
||||
<< " back buffer (hr=0x" << std::hex << hr << std::dec
|
||||
<< ") - giving up\n" << std::flush;
|
||||
PostQuitMessage(1);
|
||||
return;
|
||||
}
|
||||
|
||||
mDevice->Clear(0, NULL, D3DCLEAR_TARGET, 0xFF000000, 0.0f, 0);
|
||||
@@ -3555,6 +3706,16 @@ DPLRenderer::~DPLRenderer()
|
||||
// the next race of the single-binary loop - drop them with the device
|
||||
d3d_OBJECT::FlushTextureCache();
|
||||
|
||||
//
|
||||
// The particle engine is one of those caches and was missed. Its
|
||||
// vertex buffer is D3DPOOL_DEFAULT and its texture belongs to this
|
||||
// device, so while they were held the release below never reached
|
||||
// zero: every race left a whole live device behind it, and the next
|
||||
// race's Initialize was the only thing that ever let one go. Drop
|
||||
// them here and the device dies with the mission that made it.
|
||||
//
|
||||
ParticleEngine::Destroy();
|
||||
|
||||
SAFE_RELEASE(mDevice);
|
||||
SAFE_RELEASE(gD3D);
|
||||
//STUBBED: DPL RB 1/14/07
|
||||
@@ -5983,11 +6144,84 @@ void
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Execute Method, performs the rendering of one frame
|
||||
//
|
||||
//
|
||||
//===========================================================================
|
||||
// RPSweepCockpitLamps
|
||||
//
|
||||
// Push the cockpit lamp STATE once per frame, instead of once per gauge
|
||||
// cycle.
|
||||
//
|
||||
// The on-screen vRIO buttons light themselves from PadRIO::GetLampState,
|
||||
// and they redraw with their MFD strip. What FILLS that store is
|
||||
// LampManager::Update -> AssertNewLampValue -> SetLamp, and that rides
|
||||
// the gauge renderer's FOREGROUND turn - which comes round only once per
|
||||
// full gauge cycle. On a busy map the cycle takes the best part of a
|
||||
// second, so the lit buttons froze and any flash stalled while the 3D
|
||||
// view, a separate per-frame render, stayed perfectly smooth. BT411 saw
|
||||
// the same thing on its glass surround and fixed it the same way.
|
||||
//
|
||||
// It is cheap: a sweep over the lamps, no raster, and AssertNewLampValue
|
||||
// already drops anything that has not changed - so this pushes no extra
|
||||
// traffic, it only stops changes arriving late.
|
||||
//
|
||||
// Only when a PadRIO is active, i.e. cockpit-less play. With real serial
|
||||
// hardware selected the pod keeps its authentic bandwidth-paced cadence,
|
||||
// untouched. RP412LAMPSWEEP=0 restores the once-per-cycle behaviour.
|
||||
//===========================================================================
|
||||
//
|
||||
static void
|
||||
RPSweepCockpitLamps()
|
||||
{
|
||||
static int
|
||||
enabled = -1;
|
||||
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412LAMPSWEEP");
|
||||
|
||||
enabled = (setting != NULL && setting[0] == '0') ? 0 : 1;
|
||||
}
|
||||
if (!enabled || !PadRIO::IsActive() || application == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Only while a mission is actually running. This is called from the
|
||||
// top of the frame, ahead of the state switch below, so it would
|
||||
// otherwise fire while the mission is still being built and the
|
||||
// gauges do not exist yet.
|
||||
//
|
||||
if (application->GetApplicationState() != Application::RunningMission)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GaugeRenderer
|
||||
*renderer = application->GetGaugeRenderer();
|
||||
ModeManager
|
||||
*modes = application->GetModeManager();
|
||||
|
||||
if (renderer != NULL && modes != NULL)
|
||||
{
|
||||
LampManager
|
||||
*lamps = renderer->GetLampManager();
|
||||
|
||||
if (lamps != NULL)
|
||||
{
|
||||
lamps->Update(modes->GetModeMask());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::InterestingEntityIterator* all_iterator)
|
||||
{
|
||||
Component *component;
|
||||
HRESULT hr;
|
||||
|
||||
RPSweepCockpitLamps(); // keep the lit buttons tracking the sim (see above)
|
||||
|
||||
// timing variables
|
||||
__int64 ticks = HiResNowTicks();
|
||||
#ifdef LOGFRAMERATE
|
||||
@@ -6177,10 +6411,31 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
|
||||
DWORD currentFog;
|
||||
mDevice->GetRenderState(D3DRS_FOGCOLOR, ¤tFog);
|
||||
hr = mDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, currentFog, 1.0f, 0);
|
||||
//
|
||||
// The frame is normally cleared to the fog colour so the horizon has
|
||||
// nothing to hide. Under Alt+W it clears to BLACK instead, and the sky
|
||||
// pass is skipped entirely (below) - so what backs the wireframe is
|
||||
// black rather than a lit dome, which is the whole point of asking for
|
||||
// wireframe. Nothing on the solid path changes.
|
||||
//
|
||||
hr = mDevice->Clear(
|
||||
0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
|
||||
gWireframe ? D3DCOLOR_XRGB(0, 0, 0) : currentFog,
|
||||
1.0f, 0
|
||||
);
|
||||
|
||||
hr = mDevice->BeginScene();
|
||||
|
||||
//
|
||||
// Alt+W wireframe. Set every frame from the flag rather than once at
|
||||
// toggle time - see gWireframe. It is turned back off for the 2D pass
|
||||
// below; everything between here and there draws as edges.
|
||||
//
|
||||
mDevice->SetRenderState(
|
||||
D3DRS_FILLMODE,
|
||||
gWireframe ? D3DFILL_WIREFRAME : D3DFILL_SOLID
|
||||
);
|
||||
|
||||
mDevice->SetFVF(L4VERTEX_FVF);
|
||||
|
||||
D3DXMATRIX viewTransform;
|
||||
@@ -6257,19 +6512,29 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
|
||||
mDevice->SetTransform(D3DTS_PROJECTION, &mProjectionMatrix);
|
||||
|
||||
|
||||
if (!l4_application->IsDead())
|
||||
//
|
||||
// The sky is not drawn at all under Alt+W - the black clear above
|
||||
// stands in for it. Drawing it solid lights the top of the screen and
|
||||
// drowns the edges you turned wireframe on to look at; drawing it in
|
||||
// wireframe is worse, filling the same area with the dome's own
|
||||
// tessellation. Skipping it costs nothing: the dome only ever covers
|
||||
// pixels the clear already owns.
|
||||
//
|
||||
if (!gWireframe)
|
||||
{
|
||||
std::list<d3d_OBJECT*>::const_iterator iter;
|
||||
|
||||
for (iter = this->mConsolidatedStaticObjects.begin(); iter != this->mConsolidatedStaticObjects.end(); ++iter)
|
||||
if (!l4_application->IsDead())
|
||||
{
|
||||
(*iter)->Draw(PASS_SKY, &viewTransform, mTargetRenderTime);
|
||||
}
|
||||
}
|
||||
std::list<d3d_OBJECT*>::const_iterator iter;
|
||||
|
||||
for (d3d_OBJECT *obj = mRenderLists[PASS_SKY]; obj != NULL; obj = obj->GetNext(PASS_SKY))
|
||||
obj->Draw(PASS_SKY, &viewTransform, mTargetRenderTime);
|
||||
for (iter = this->mConsolidatedStaticObjects.begin(); iter != this->mConsolidatedStaticObjects.end(); ++iter)
|
||||
{
|
||||
(*iter)->Draw(PASS_SKY, &viewTransform, mTargetRenderTime);
|
||||
}
|
||||
}
|
||||
|
||||
for (d3d_OBJECT *obj = mRenderLists[PASS_SKY]; obj != NULL; obj = obj->GetNext(PASS_SKY))
|
||||
obj->Draw(PASS_SKY, &viewTransform, mTargetRenderTime);
|
||||
}
|
||||
|
||||
//Reactivate fog
|
||||
mDevice->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(¤tFogNear)));
|
||||
@@ -6314,6 +6579,12 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
// Wrap it up by doing the 2D pass
|
||||
//
|
||||
mDevice->SetFVF(L4VERTEX_2D_FVF);
|
||||
//
|
||||
// Always solid from here on: the gunsight and the cam-ship HUD are
|
||||
// textured quads on this device, and in wireframe they come out as bare
|
||||
// diagonals.
|
||||
//
|
||||
mDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
|
||||
mDevice->SetRenderState(D3DRS_ZWRITEENABLE, true);
|
||||
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
||||
@@ -6339,6 +6610,48 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
|
||||
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
|
||||
|
||||
//
|
||||
// RP412GAUGEDIAG=1: frames per second, on the same 2-second window the
|
||||
// display-sweep line uses so the two read side by side.
|
||||
//
|
||||
// Without this the gauge rate has to be argued about rather than
|
||||
// measured. A sweep rate far below the frame rate means the gauges are
|
||||
// STARVED - the 3D is fine and the background loop is not getting
|
||||
// through its cycle. A sweep rate that tracks the frame rate means
|
||||
// there is nothing wrong with the gauges at all and the frame itself
|
||||
// is the problem. Those two want opposite fixes, and the sweep line
|
||||
// alone cannot tell them apart.
|
||||
//
|
||||
{
|
||||
static int diagnostics = -1;
|
||||
if (diagnostics < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412GAUGEDIAG");
|
||||
diagnostics = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diagnostics)
|
||||
{
|
||||
static unsigned long window_start = 0;
|
||||
static int frames = 0;
|
||||
unsigned long now = GetTickCount();
|
||||
++frames;
|
||||
if (window_start == 0)
|
||||
{
|
||||
window_start = now;
|
||||
}
|
||||
else if (now - window_start >= 2000)
|
||||
{
|
||||
int tenths = frames * 10000 / (int)(now - window_start);
|
||||
DEBUG_STREAM << "FrameDiag: " << frames << " frame(s) in "
|
||||
<< (now - window_start) << " ms ("
|
||||
<< (tenths / 10) << '.' << (tenths % 10) << "/s)\n"
|
||||
<< std::flush;
|
||||
window_start = now;
|
||||
frames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hand the whole target back
|
||||
if (mPresentationAspect > 0.0f)
|
||||
{
|
||||
@@ -6555,19 +6868,17 @@ void
|
||||
void
|
||||
DPLRenderer::DPLToggleWireframe()
|
||||
{
|
||||
//STUBBBED: DPL RB 1/14/07
|
||||
//static Logical wireframe_on = 0;
|
||||
//
|
||||
// The DPL original set a renderer property here
|
||||
// (dpl_render_prop_wireframe) and was stubbed out with the rest of the
|
||||
// DPL calls in the 2007 D3D port. D3D9 has no equivalent global: the
|
||||
// fill mode is device state, so all this does is record the intent and
|
||||
// let the frame apply it. See gWireframe at the top of this file.
|
||||
//
|
||||
gWireframe = !gWireframe;
|
||||
|
||||
//if ((wireframe_on ^= 1) != 0)
|
||||
//{
|
||||
// DEBUG_STREAM << "wireframe ON" << std::endl << std::flush;
|
||||
// dpl_SetRenderProperty(dpl_render_prop_wireframe, dpl_render_value_on, NULL );
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// DEBUG_STREAM << "wireframe OFF" << std::endl << std::flush;
|
||||
// dpl_SetRenderProperty(dpl_render_prop_wireframe, dpl_render_value_off, NULL );
|
||||
//}
|
||||
DEBUG_STREAM << "wireframe " << (gWireframe ? "ON" : "OFF")
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
//
|
||||
//#############################################################################
|
||||
|
||||
@@ -643,3 +643,11 @@ public:
|
||||
};
|
||||
|
||||
extern LPDIRECT3D9 gD3D;
|
||||
|
||||
//
|
||||
// The presentation interval every device is created with. RP412VSYNC=0
|
||||
// makes it IMMEDIATE, so Present returns instead of waiting for the
|
||||
// panel's retrace - see the definition in L4VIDEO.cpp for why that
|
||||
// matters far more here than tearing does.
|
||||
//
|
||||
DWORD RPPresentationInterval();
|
||||
|
||||
+558
-54
@@ -11,6 +11,16 @@
|
||||
#include "l4app.h"
|
||||
#include "..\RP\VTV.h"
|
||||
|
||||
//
|
||||
// Where the eye was this frame, and a counter of frames the eye has been
|
||||
// built for. Written by DPLEyeRenderable::Execute, read by
|
||||
// RootRenderable::Execute, which draws afterwards in the same frame - the
|
||||
// counter is what proves that rather than assuming it.
|
||||
//
|
||||
static D3DXVECTOR3 gEyeWorld(0.0f, 0.0f, 0.0f);
|
||||
static unsigned gEyeFrame = 0;
|
||||
static unsigned gLastSampledFrame = 0;
|
||||
|
||||
// RB 1/14/07
|
||||
//#include <dpl\dpl.h>
|
||||
//#include <dpl\dpl_2d.h>
|
||||
@@ -1082,8 +1092,310 @@ void
|
||||
// DPL_FLUSH_DCS ( myDCS );
|
||||
// }
|
||||
myRenderer->GetMatrixStack()->Push();
|
||||
//
|
||||
// The transform to DRAW with, blended across the fixed simulation
|
||||
// step this frame falls inside - see Entity::GetRenderToWorld. This
|
||||
// is the whole of render interpolation on the vehicle side: the
|
||||
// renderable already ran once per frame and simply re-read a value
|
||||
// that only changed at the physics rate.
|
||||
//
|
||||
LinearMatrix renderToWorld;
|
||||
myEntity->GetRenderToWorld(&renderToWorld);
|
||||
|
||||
Matrix4x4 tempMatrix;
|
||||
tempMatrix = myEntity->localToWorld;
|
||||
tempMatrix = renderToWorld;
|
||||
|
||||
//
|
||||
// RP412CAMLOG: what the tick actually looks like on screen.
|
||||
//
|
||||
// The complaint is a RHYTHMIC tick as a pod moves past the camera,
|
||||
// and rhythm is the clue: a fixed period points at a cadence in our
|
||||
// own code rather than at the network, which has no period. So
|
||||
// measure the angle the pod subtends at the eye, frame by frame,
|
||||
// and report the INTERVAL BETWEEN the lurches rather than merely
|
||||
// counting them. That interval names the culprit - about 0.02s is
|
||||
// the physics step, 0.03s the update rate, 0.4s the twenty-step
|
||||
// quaternion renormalisation in Mover::BeginStep.
|
||||
//
|
||||
// Angle rather than distance because a pod crossing the view moves
|
||||
// far across the screen while barely changing range, which is
|
||||
// exactly the geometry the tick was reported in.
|
||||
//
|
||||
// Both samples are taken in one frame - the eye's, marked by
|
||||
// gEyeFrame - because a pod and a camera read from different frames
|
||||
// alias against each other no matter what the game is doing.
|
||||
//
|
||||
if (RPCameraLog() && gEyeFrame != gLastSampledFrame)
|
||||
{
|
||||
EntityID traced = MoverTracedEntity();
|
||||
|
||||
if (traced != EntityID::Null && traced == myEntity->GetEntityID())
|
||||
{
|
||||
//
|
||||
// Scoped out here, not inside the range test below, because
|
||||
// the foreign-eye check has to read them before that test
|
||||
// runs.
|
||||
//
|
||||
static Vector3D last_dir;
|
||||
static Logical have_last = False;
|
||||
static Scalar next_say = 0.0f;
|
||||
static Scalar mean_angle = 0.0f;
|
||||
static Scalar last_event = 0.0f;
|
||||
static Scalar interval_sum = 0.0f;
|
||||
static Scalar interval_min = 1000.0f;
|
||||
static Scalar interval_max = 0.0f;
|
||||
static int interval_count = 0;
|
||||
static int frames = 0;
|
||||
static int lurches = 0;
|
||||
static int stalls = 0;
|
||||
static Scalar last_angle = 0.0f;
|
||||
static Logical have_angle = False;
|
||||
static Scalar fraction_sum = 0.0f;
|
||||
static int pinned_high = 0;
|
||||
static int pinned_low = 0;
|
||||
static Point3D last_pod(0.0f, 0.0f, 0.0f);
|
||||
static Point3D last_eye(0.0f, 0.0f, 0.0f);
|
||||
static Logical have_pod = False;
|
||||
static Scalar last_pod_step = 0.0f;
|
||||
static Scalar last_eye_step = 0.0f;
|
||||
static Scalar eye_travel = 0.0f;
|
||||
static int pod_stalls = 0;
|
||||
static int eye_stalls = 0;
|
||||
static int foreign_eye = 0;
|
||||
|
||||
gLastSampledFrame = gEyeFrame;
|
||||
|
||||
Vector3D to_pod;
|
||||
to_pod.x = renderToWorld(3,0) - gEyeWorld.x;
|
||||
to_pod.y = renderToWorld(3,1) - gEyeWorld.y;
|
||||
to_pod.z = renderToWorld(3,2) - gEyeWorld.z;
|
||||
|
||||
Scalar range = to_pod.Length();
|
||||
|
||||
//
|
||||
// Reject a sample taken through a DIFFERENT eye.
|
||||
//
|
||||
// A camera station draws the map on the gauge wheel as well
|
||||
// as the world, and that render runs the eye renderable too,
|
||||
// so gEyeFrame ticks for it and this trace was comparing the
|
||||
// map's viewpoint against the main one and calling the
|
||||
// difference a stall. The tell was the period: 0.34s with
|
||||
// its minimum and maximum identical to six figures, which is
|
||||
// twenty frames exactly. Nothing in the network or the
|
||||
// simulation keeps time that well - only a render schedule
|
||||
// does. The stall and lurch counts converging to the same
|
||||
// number said the same thing, since a stray viewpoint
|
||||
// produces one short step going out and one long one coming
|
||||
// back.
|
||||
//
|
||||
// A real camera at racing speed moves under a metre between
|
||||
// frames, so twenty is far outside anything legitimate while
|
||||
// still tolerating a genuine cut between trackside cameras.
|
||||
//
|
||||
Scalar eye_jump = 0.0f;
|
||||
if (have_pod)
|
||||
{
|
||||
Scalar dx = gEyeWorld.x - last_eye.x;
|
||||
Scalar dy = gEyeWorld.y - last_eye.y;
|
||||
Scalar dz = gEyeWorld.z - last_eye.z;
|
||||
eye_jump = (Scalar) sqrt((double)(dx*dx + dy*dy + dz*dz));
|
||||
}
|
||||
|
||||
if (eye_jump > 20.0f)
|
||||
{
|
||||
++foreign_eye;
|
||||
have_last = False; // do not bridge across it
|
||||
have_angle = False;
|
||||
have_pod = False;
|
||||
}
|
||||
else if (range > 0.1f)
|
||||
{
|
||||
Vector3D dir;
|
||||
dir.Divide(to_pod, range);
|
||||
++frames;
|
||||
|
||||
//
|
||||
// Split the pod's motion from the camera's.
|
||||
//
|
||||
// The angle above is measured BETWEEN the two, so a
|
||||
// hitch in the pan and a hitch in the pod are the same
|
||||
// reading - and the symptom is specifically a pod moving
|
||||
// past, which is when the pan rate peaks. Measuring each
|
||||
// on its own says which one to go and fix.
|
||||
//
|
||||
// Same ratio test as the angle, and for the same reason:
|
||||
// consecutive frames of smooth motion are near equal
|
||||
// whatever the speed.
|
||||
//
|
||||
Point3D pod_now;
|
||||
pod_now.x = renderToWorld(3,0);
|
||||
pod_now.y = renderToWorld(3,1);
|
||||
pod_now.z = renderToWorld(3,2);
|
||||
|
||||
if (have_pod)
|
||||
{
|
||||
Vector3D pod_moved;
|
||||
pod_moved.Subtract(pod_now, last_pod);
|
||||
|
||||
Vector3D eye_moved;
|
||||
eye_moved.x = gEyeWorld.x - last_eye.x;
|
||||
eye_moved.y = gEyeWorld.y - last_eye.y;
|
||||
eye_moved.z = gEyeWorld.z - last_eye.z;
|
||||
|
||||
Scalar pod_step = pod_moved.Length();
|
||||
Scalar eye_step = eye_moved.Length();
|
||||
|
||||
if (last_pod_step > 0.0001f && pod_step < last_pod_step * 0.4f)
|
||||
{
|
||||
++pod_stalls;
|
||||
}
|
||||
if (last_eye_step > 0.0001f && eye_step < last_eye_step * 0.4f)
|
||||
{
|
||||
++eye_stalls;
|
||||
}
|
||||
last_pod_step = pod_step;
|
||||
last_eye_step = eye_step;
|
||||
eye_travel += eye_step;
|
||||
}
|
||||
last_pod = pod_now;
|
||||
last_eye.x = gEyeWorld.x;
|
||||
last_eye.y = gEyeWorld.y;
|
||||
last_eye.z = gEyeWorld.z;
|
||||
have_pod = True;
|
||||
|
||||
//
|
||||
// How far through the physics step this frame is being
|
||||
// drawn. This is the number the interpolation actually
|
||||
// uses, so it is worth reading directly rather than
|
||||
// inferring from the picture. It should sweep smoothly
|
||||
// from 0 to 1 and wrap. Pinned at 1 means the
|
||||
// simulation is behind and interpolation has degenerated
|
||||
// to drawing the latest step over and over - which IS
|
||||
// stepping, at the physics rate, however smooth the
|
||||
// packets were.
|
||||
//
|
||||
Scalar fraction = myEntity->renderStepFraction;
|
||||
|
||||
fraction_sum += fraction;
|
||||
if (fraction > 0.99f) { ++pinned_high; }
|
||||
if (fraction < 0.01f) { ++pinned_low; }
|
||||
|
||||
if (have_last)
|
||||
{
|
||||
Scalar dot =
|
||||
dir.x*last_dir.x + dir.y*last_dir.y + dir.z*last_dir.z;
|
||||
|
||||
if (dot > 1.0f) { dot = 1.0f; }
|
||||
if (dot < -1.0f) { dot = -1.0f; }
|
||||
|
||||
Scalar angle = (Scalar) acos((double) dot);
|
||||
|
||||
//
|
||||
// Compare each frame against the frame BEFORE it,
|
||||
// not against a running mean. The angular step
|
||||
// varies six hundredfold between a pod at 119m and
|
||||
// the same pod at 5m, so a long mean cannot keep up
|
||||
// during a pass and reports its own lag as a lurch -
|
||||
// which is precisely what the previous version of
|
||||
// this trace did, in the one window that mattered.
|
||||
//
|
||||
// Consecutive frames of a smooth pass are nearly
|
||||
// equal however fast the sweep, so their ratio is
|
||||
// immune to range. A tick is one frame that barely
|
||||
// moves followed by one that catches up, so the
|
||||
// stall is the event worth timing.
|
||||
//
|
||||
if (have_angle && last_angle > 0.00002f)
|
||||
{
|
||||
Scalar ratio = angle / last_angle;
|
||||
|
||||
if (ratio > 2.5f)
|
||||
{
|
||||
++lurches;
|
||||
}
|
||||
else if (ratio < 0.4f)
|
||||
{
|
||||
++stalls;
|
||||
|
||||
Scalar now = (Scalar) Now();
|
||||
|
||||
if (last_event > 0.0f)
|
||||
{
|
||||
Scalar gap = now - last_event;
|
||||
|
||||
interval_sum += gap;
|
||||
++interval_count;
|
||||
if (gap < interval_min) { interval_min = gap; }
|
||||
if (gap > interval_max) { interval_max = gap; }
|
||||
}
|
||||
last_event = now;
|
||||
}
|
||||
}
|
||||
last_angle = angle;
|
||||
have_angle = True;
|
||||
|
||||
mean_angle = (mean_angle > 0.0f)
|
||||
? (mean_angle * 0.95f + angle * 0.05f)
|
||||
: angle;
|
||||
}
|
||||
last_dir = dir;
|
||||
have_last = True;
|
||||
|
||||
if ((Scalar) Now() >= next_say)
|
||||
{
|
||||
if (next_say > 0.0f)
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: on-screen motion - " << frames
|
||||
<< " frames, " << stalls << " stall(s), "
|
||||
<< lurches << " lurch(es), period "
|
||||
<< ((interval_count > 0)
|
||||
? (interval_sum / interval_count)
|
||||
: 0.0f)
|
||||
<< "s (" << interval_min << ".." << interval_max
|
||||
<< "), mean step " << (mean_angle * 1000.0f)
|
||||
<< " mrad, range " << range << "m\n" << std::flush;
|
||||
|
||||
DEBUG_STREAM << "CamLog: render fraction - mean "
|
||||
<< ((frames > 0) ? (fraction_sum / frames) : 0.0f)
|
||||
<< ", " << pinned_high << " pinned at 1, "
|
||||
<< pinned_low << " at 0, of " << frames
|
||||
<< " frames ("
|
||||
<< ((pinned_high * 4 > frames)
|
||||
? "simulation is behind, interpolation degenerate"
|
||||
: "sweeping normally")
|
||||
<< ")\n" << std::flush;
|
||||
|
||||
DEBUG_STREAM << "CamLog: motion split - pod "
|
||||
<< pod_stalls << " stall(s), eye "
|
||||
<< eye_stalls << " stall(s), " << foreign_eye
|
||||
<< " foreign-eye sample(s) rejected, eye travelled "
|
||||
<< eye_travel << "m, verdict "
|
||||
<< ((pod_stalls > eye_stalls * 2)
|
||||
? "the pod"
|
||||
: ((eye_stalls > pod_stalls * 2)
|
||||
? "the camera"
|
||||
: "both, or neither"))
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
next_say = ((Scalar) Now()) + 5.0f;
|
||||
frames = 0;
|
||||
lurches = 0;
|
||||
stalls = 0;
|
||||
interval_sum = 0.0f;
|
||||
interval_min = 1000.0f;
|
||||
interval_max = 0.0f;
|
||||
interval_count = 0;
|
||||
fraction_sum = 0.0f;
|
||||
pinned_high = 0;
|
||||
pinned_low = 0;
|
||||
pod_stalls = 0;
|
||||
eye_stalls = 0;
|
||||
eye_travel = 0.0f;
|
||||
foreign_eye = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
myRenderer->GetMatrixStack()->MultMatrix(&tempMatrix.ToD3DMatrix());
|
||||
//myLocalToWorld = *myRenderer->GetMatrixStack()->GetTop();
|
||||
@@ -2364,7 +2676,76 @@ ReticleRenderable::ReticleRenderable(
|
||||
|
||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
||||
|
||||
device->CreateVertexBuffer(sizeof(L4VERTEX_2D) * 8, D3DUSAGE_WRITEONLY, L4VERTEX_2D_FVF, D3DPOOL_MANAGED, &mVB, NULL);
|
||||
device->CreateVertexBuffer(sizeof(L4VERTEX_2D) * crosshairVertexCount, D3DUSAGE_WRITEONLY, L4VERTEX_2D_FVF, D3DPOOL_MANAGED, &mVB, NULL);
|
||||
|
||||
//
|
||||
// Nothing is written here. The crosshair is measured against the
|
||||
// render target it will actually be drawn into, and that is not
|
||||
// known to be the renderer's requested size - see RebuildCrosshair.
|
||||
//
|
||||
mBuiltWidth = 0.0f;
|
||||
mBuiltHeight = 0.0f;
|
||||
mBuiltOriginX = -1.0f;
|
||||
mBuiltOriginY = -1.0f;
|
||||
RebuildCrosshair();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Centre the crosshair on the target it is about to be drawn into.
|
||||
//
|
||||
// It used to be baked once, in the constructor, from the renderer's
|
||||
// GetWidth()/GetHeight() - the size the renderer ASKED for. A windowed
|
||||
// device does not necessarily get it: BackBufferWidth/Height are only
|
||||
// filled in for fullscreen (L4VIDEO.cpp), so windowed the back buffer is
|
||||
// whatever the device window's client area happened to be when the
|
||||
// device was created. A fresh renderer is built per mission while the
|
||||
// window carries its restored cockpit placement across, so the two agree
|
||||
// on the first race and can disagree on the next one - which put the
|
||||
// crosshair off-centre by half the difference, and only ever on a second
|
||||
// game.
|
||||
//
|
||||
// Measuring the viewport at draw time settles it for every case at once:
|
||||
// the windowed mismatch, a device reset, and the podium's pillarbox crop.
|
||||
//
|
||||
void ReticleRenderable::RebuildCrosshair()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (mVB == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
||||
if (device == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
D3DVIEWPORT9 viewport;
|
||||
if (FAILED(device->GetViewport(&viewport)) ||
|
||||
viewport.Width == 0 || viewport.Height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float width = (float) viewport.Width;
|
||||
float height = (float) viewport.Height;
|
||||
//
|
||||
// Origin included: a viewport that moves without resizing still
|
||||
// carries the crosshair with it.
|
||||
//
|
||||
float origin_x = (float) viewport.X;
|
||||
float origin_y = (float) viewport.Y;
|
||||
if (width == mBuiltWidth && height == mBuiltHeight &&
|
||||
origin_x == mBuiltOriginX && origin_y == mBuiltOriginY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mBuiltWidth = width;
|
||||
mBuiltHeight = height;
|
||||
mBuiltOriginX = origin_x;
|
||||
mBuiltOriginY = origin_y;
|
||||
|
||||
L4VERTEX_2D *verts;
|
||||
mVB->Lock(0, 0, (void**)&verts, 0);
|
||||
@@ -2372,62 +2753,125 @@ ReticleRenderable::ReticleRenderable(
|
||||
DWORD color = D3DCOLOR_XRGB(0, 128, 0);
|
||||
float segmentLen = 5.0f / 192.0f;
|
||||
float spread = 5.0f / 256.0f;
|
||||
float width = myRenderer->GetWidth();
|
||||
float height = myRenderer->GetHeight();
|
||||
float centerX = width / 2.0f;
|
||||
float centerY = height / 2.0f;
|
||||
//
|
||||
// Pre-transformed vertices are absolute screen pixels - the viewport
|
||||
// clips them but does not shift them - so its origin is carried here.
|
||||
//
|
||||
float centerX = (float) viewport.X + width / 2.0f;
|
||||
float centerY = (float) viewport.Y + height / 2.0f;
|
||||
|
||||
// top segment
|
||||
verts[0].x = centerX;
|
||||
verts[0].y = centerY - (spread + segmentLen) * height;
|
||||
verts[0].z = 0.0f;
|
||||
verts[0].rhw = 1.0f;
|
||||
verts[0].color = color;
|
||||
//
|
||||
// Land on the pixel CENTRE, not the corner, so the quads below span
|
||||
// whole pixels: an arm one pixel wide about a centre of x.5 runs from
|
||||
// x.0 to x+1.0 and covers exactly one column.
|
||||
//
|
||||
centerX = (float)(int) centerX + 0.5f;
|
||||
centerY = (float)(int) centerY + 0.5f;
|
||||
|
||||
verts[1].x = centerX;
|
||||
verts[1].y = centerY - spread * height;
|
||||
verts[1].z = 0.0f;
|
||||
verts[1].rhw = 1.0f;
|
||||
verts[1].color = color;
|
||||
//
|
||||
// Worth a line in the log: it names the target, the renderer's
|
||||
// requested size and where the crosshair actually landed, so a
|
||||
// report of it being off says which of the three moved.
|
||||
//
|
||||
DEBUG_STREAM << "Reticle: centred at " << centerX << "," << centerY
|
||||
<< " on the " << viewport.Width << "x" << viewport.Height
|
||||
<< " viewport at " << viewport.X << "," << viewport.Y
|
||||
<< " (renderer asked for " << myRenderer->GetWidth() << "x"
|
||||
<< myRenderer->GetHeight() << ")";
|
||||
if (viewport.Width != myRenderer->GetWidth() ||
|
||||
viewport.Height != myRenderer->GetHeight())
|
||||
{
|
||||
//
|
||||
// Not the crosshair's problem alone: the projection matrix is
|
||||
// built from the requested size too, so a target this does not
|
||||
// match is being rendered at the wrong aspect and rescaled on
|
||||
// the way to the pane.
|
||||
//
|
||||
DEBUG_STREAM << " - TARGET DISAGREES, aspect "
|
||||
<< ((float) viewport.Width / (float) viewport.Height)
|
||||
<< " drawn as "
|
||||
<< ((float) myRenderer->GetWidth() / (float) myRenderer->GetHeight());
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
|
||||
// right segment
|
||||
verts[2].x = centerX + (spread + segmentLen) * height;
|
||||
verts[2].y = centerY;
|
||||
verts[2].z = 0.0f;
|
||||
verts[2].rhw = 1.0f;
|
||||
verts[2].color = color;
|
||||
//
|
||||
// Each arm is a quad one pixel thick rather than a line. D3D9 line
|
||||
// rasterisation follows the diamond-exit rule and is free to differ
|
||||
// between drivers on a segment that runs along a pixel boundary,
|
||||
// which is how a crosshair loses one pair of arms and keeps the
|
||||
// other. Triangles have a fill rule that does not vary, so an arm
|
||||
// spanning whole pixels lands the same way everywhere - and on a
|
||||
// full-screen device, where both viewport dimensions are usually
|
||||
// even and BOTH pairs sit on boundaries, that is the difference
|
||||
// between a crosshair and nothing at all.
|
||||
//
|
||||
float inner = spread * height;
|
||||
float outer = (spread + segmentLen) * height;
|
||||
|
||||
verts[3].x = centerX + spread * height;
|
||||
verts[3].y = centerY;
|
||||
verts[3].z = 0.0f;
|
||||
verts[3].rhw = 1.0f;
|
||||
verts[3].color = color;
|
||||
//
|
||||
// Thickness follows the target the way the arms' length does, rather
|
||||
// than being one pixel whatever the resolution.
|
||||
//
|
||||
// One pixel is a width the PRESENTATION can lose. The scene goes to
|
||||
// the viewscreen pane through a stretch, and when the back buffer is
|
||||
// wider than the pane that stretch samples straight past a feature a
|
||||
// single pixel across. A second race rendering 3440 wide into the
|
||||
// 2553-wide pane is 0.74 across and 1.00 down, which took the
|
||||
// vertical arms and left the horizontal ones standing - the crosshair
|
||||
// was being drawn correctly and thrown away on the way to the glass.
|
||||
//
|
||||
// Scaling it also keeps the pod's proportions: one pixel at the 480
|
||||
// lines this was drawn for is three at 1440, not a hairline.
|
||||
//
|
||||
float thickness = (float)(int)(height / 480.0f);
|
||||
if (thickness < 1.0f)
|
||||
{
|
||||
thickness = 1.0f;
|
||||
}
|
||||
float half = thickness * 0.5f;
|
||||
|
||||
// bottom segment
|
||||
verts[4].x = centerX;
|
||||
verts[4].y = centerY + (spread + segmentLen) * height;
|
||||
verts[4].z = 0.0f;
|
||||
verts[4].rhw = 1.0f;
|
||||
verts[4].color = color;
|
||||
struct Arm
|
||||
{
|
||||
float x0, y0, x1, y1; // opposite corners, in pixels
|
||||
};
|
||||
const Arm arms[4] =
|
||||
{
|
||||
// top
|
||||
{ centerX - half, centerY - outer, centerX + half, centerY - inner },
|
||||
// right
|
||||
{ centerX + inner, centerY - half, centerX + outer, centerY + half },
|
||||
// bottom
|
||||
{ centerX - half, centerY + inner, centerX + half, centerY + outer },
|
||||
// left
|
||||
{ centerX - outer, centerY - half, centerX - inner, centerY + half }
|
||||
};
|
||||
|
||||
verts[5].x = centerX;
|
||||
verts[5].y = centerY + spread * height;
|
||||
verts[5].z = 0.0f;
|
||||
verts[5].rhw = 1.0f;
|
||||
verts[5].color = color;
|
||||
int v = 0;
|
||||
for (int a = 0; a < 4; ++a)
|
||||
{
|
||||
const Arm &arm = arms[a];
|
||||
//
|
||||
// Two triangles, corners in the order top-left, top-right,
|
||||
// bottom-left / top-right, bottom-right, bottom-left. Culling is
|
||||
// turned off for the draw, so the winding does not have to agree
|
||||
// with the renderer's global cull mode.
|
||||
//
|
||||
const float quad_x[6] =
|
||||
{ arm.x0, arm.x1, arm.x0, arm.x1, arm.x1, arm.x0 };
|
||||
const float quad_y[6] =
|
||||
{ arm.y0, arm.y0, arm.y1, arm.y0, arm.y1, arm.y1 };
|
||||
|
||||
// left segment
|
||||
verts[6].x = centerX - (spread + segmentLen) * height;
|
||||
verts[6].y = centerY;
|
||||
verts[6].z = 0.0f;
|
||||
verts[6].rhw = 1.0f;
|
||||
verts[6].color = color;
|
||||
|
||||
verts[7].x = centerX - spread * height;
|
||||
verts[7].y = centerY;
|
||||
verts[7].z = 0.0f;
|
||||
verts[7].rhw = 1.0f;
|
||||
verts[7].color = color;
|
||||
for (int c = 0; c < 6; ++c)
|
||||
{
|
||||
verts[v].x = quad_x[c];
|
||||
verts[v].y = quad_y[c];
|
||||
verts[v].z = 0.0f;
|
||||
verts[v].rhw = 1.0f;
|
||||
verts[v].color = color;
|
||||
++v;
|
||||
}
|
||||
}
|
||||
Verify(v == crosshairVertexCount);
|
||||
|
||||
mVB->Unlock();
|
||||
}
|
||||
@@ -2497,9 +2941,26 @@ void ReticleRenderable::Render(int pass, const D3DXMATRIX *viewTransform)
|
||||
{
|
||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
||||
|
||||
//
|
||||
// The target is whatever is bound right now, so ask it now. A
|
||||
// compare against the size already built means this costs one
|
||||
// GetViewport a frame and rewrites nothing until it moves.
|
||||
//
|
||||
RebuildCrosshair();
|
||||
|
||||
//
|
||||
// The arms are quads, and which way round they wind is not worth
|
||||
// making the renderer's global cull mode responsible for.
|
||||
//
|
||||
DWORD cull_mode;
|
||||
device->GetRenderState(D3DRS_CULLMODE, &cull_mode);
|
||||
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
|
||||
device->SetTexture(0, NULL);
|
||||
device->SetStreamSource(0, mVB, 0, sizeof(L4VERTEX_2D));
|
||||
device->DrawPrimitive(D3DPT_LINELIST, 0, 4);
|
||||
device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 8);
|
||||
|
||||
device->SetRenderState(D3DRS_CULLMODE, cull_mode);
|
||||
}
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -2656,6 +3117,7 @@ CameraShipHUDRenderable::CameraShipHUDRenderable(Entity *entity, ExecutionType e
|
||||
//
|
||||
playerRank = NULL;
|
||||
oldPlayerRank = NULL;
|
||||
rankCount = 0;
|
||||
nameDCS = NULL;
|
||||
rankDCS = NULL;
|
||||
nameInstance = NULL;
|
||||
@@ -2699,6 +3161,18 @@ CameraShipHUDRenderable::CameraShipHUDRenderable(Entity *entity, ExecutionType e
|
||||
Verify(camera_player_count <= MAX_PLAYER_NAMES);
|
||||
playerRank = new (int (*[camera_player_count]));
|
||||
Register_Pointer(playerRank);
|
||||
//
|
||||
// Cleared, because the fill below is by bitmap index and leaves
|
||||
// gaps: with a Live Cam host the racing players start at index 2
|
||||
// and nothing ever claims slot 0. new[] does not zero, so those
|
||||
// gaps were uninitialised heap read as int* - the access
|
||||
// violation in Execute.
|
||||
//
|
||||
rankCount = camera_player_count;
|
||||
for (int slot = 0; slot < rankCount; ++slot)
|
||||
{
|
||||
playerRank[slot] = NULL;
|
||||
}
|
||||
Player *active_player;
|
||||
if(player_group)
|
||||
{
|
||||
@@ -2946,8 +3420,14 @@ void CameraShipHUDRenderable::Execute()
|
||||
// whenever they change!
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
for(int ii=0; ii<playerCount; ++ii)
|
||||
// Over the whole array, not playerCount, and skipping the slots no
|
||||
// bitmap index claimed - see rankCount in the header.
|
||||
for(int ii=0; ii<rankCount; ++ii)
|
||||
{
|
||||
if (playerRank[ii] == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (oldPlayerRank[ii] != (*playerRank[ii]))
|
||||
{
|
||||
oldPlayerRank[ii] = *playerRank[ii];
|
||||
@@ -5483,7 +5963,17 @@ void
|
||||
// time based upon the setting of the eyepoint rotation
|
||||
//----------------------------------------------------------------------
|
||||
//
|
||||
if((myEyepointRotation && *myEyepointRotation != oldEyepointRotation) || oldLocalToWorld != myEntity->localToWorld || mForceUpdate)
|
||||
//
|
||||
// The last term is render interpolation. This gate rebuilt the view
|
||||
// only when the entity's localToWorld CHANGED, which happens at the
|
||||
// physics rate - so with the world now drawn on a blended transform the
|
||||
// eye would have gone on stepping, and the judder would simply have
|
||||
// moved from the scenery to the camera. While a blend is in progress
|
||||
// the view has to be rebuilt every frame; renderStepFraction is zero
|
||||
// whenever interpolation is off or idle, so the old behaviour is
|
||||
// untouched. See Entity::GetRenderToWorld.
|
||||
//
|
||||
if((myEyepointRotation && *myEyepointRotation != oldEyepointRotation) || oldLocalToWorld != myEntity->localToWorld || mForceUpdate || myEntity->renderStepTaken)
|
||||
{
|
||||
mForceUpdate = false;
|
||||
oldLocalToWorld = myEntity->localToWorld;
|
||||
@@ -5507,6 +5997,20 @@ void
|
||||
mat = mat4;
|
||||
|
||||
D3DXVECTOR3 pos(mat(3,0), mat(3, 1), mat(3,2));
|
||||
|
||||
//
|
||||
// Where the eye ended up this frame, for the on-screen motion
|
||||
// trace in RootRenderable::Execute. The eye is built once per
|
||||
// frame and before the world is drawn, so it also serves as the
|
||||
// frame marker that keeps the two samples in the same frame -
|
||||
// which is the whole point, since sampling a pod and a camera
|
||||
// from different frames is what made three earlier attempts at
|
||||
// this question meaningless.
|
||||
//
|
||||
gEyeWorld.x = mat(3,0);
|
||||
gEyeWorld.y = mat(3,1);
|
||||
gEyeWorld.z = mat(3,2);
|
||||
gEyeFrame++;
|
||||
mat(3,0) = 0;
|
||||
mat(3,1) = 0;
|
||||
mat(3,2) = 0;
|
||||
|
||||
@@ -783,6 +783,19 @@ class ReticleRenderable :
|
||||
void Render(int pass, const D3DXMATRIX *viewTransform);
|
||||
|
||||
protected:
|
||||
//
|
||||
// The crosshair is written as pre-transformed vertices - screen
|
||||
// PIXELS - so it is only centred for the target it was measured
|
||||
// against. Re-measure before drawing and rebuild when that
|
||||
// target changes, rather than baking it once at construction.
|
||||
//
|
||||
void RebuildCrosshair();
|
||||
|
||||
// four arms, two triangles each
|
||||
enum { crosshairVertexCount = 24 };
|
||||
|
||||
// viewport the vertex buffer currently describes
|
||||
float mBuiltWidth, mBuiltHeight, mBuiltOriginX, mBuiltOriginY;
|
||||
|
||||
// Last known position of the reticle
|
||||
Vector2DOf<float> myOldReticlePosition;
|
||||
@@ -833,6 +846,19 @@ class CameraShipHUDRenderable :
|
||||
int
|
||||
playerCount;
|
||||
|
||||
//
|
||||
// How long playerRank/oldPlayerRank actually are. NOT playerCount:
|
||||
// the arrays are keyed by playerBitmapIndex, which a CameraShip
|
||||
// player takes a slot in too, so they are sized for the racing
|
||||
// players plus the camera players and are SPARSE - a slot whose
|
||||
// bitmap index nobody claimed stays NULL. Walking them to
|
||||
// playerCount instead read past the last claimed slot, which is
|
||||
// how a Live Cam host (bitmap index 1, and not in "Players") took
|
||||
// the whole game down.
|
||||
//
|
||||
int
|
||||
rankCount;
|
||||
|
||||
int
|
||||
oldFollowedPlayerIndex,
|
||||
*followedPlayerIndex;
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
<ClCompile Include="..\MUNGA\INTEREST.cpp" />
|
||||
<ClCompile Include="..\MUNGA\INTORGN.cpp" />
|
||||
<ClCompile Include="..\MUNGA\ITERATOR.cpp" />
|
||||
<ClCompile Include="..\MUNGA\INPUTSCRIPT.cpp" />
|
||||
<ClCompile Include="..\MUNGA\JMOVER.cpp" />
|
||||
<ClCompile Include="..\MUNGA\JOINT.cpp" />
|
||||
<ClCompile Include="..\MUNGA\LAMP.cpp" />
|
||||
@@ -227,6 +228,7 @@
|
||||
<ClCompile Include="..\MUNGA\WRHOUS.cpp" />
|
||||
<ClCompile Include=".\DXUtils.cpp" />
|
||||
<ClCompile Include=".\L4APP.cpp" />
|
||||
<ClCompile Include=".\L4AUDEFX.cpp" />
|
||||
<ClCompile Include=".\L4AUDHDW.cpp" />
|
||||
<ClCompile Include=".\L4AUDIO.cpp" />
|
||||
<ClCompile Include=".\L4AUDLVL.cpp" />
|
||||
@@ -263,6 +265,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" />
|
||||
@@ -431,6 +434,7 @@
|
||||
<ClInclude Include="..\MUNGA\WRHOUS.h" />
|
||||
<ClInclude Include=".\DXUtils.h" />
|
||||
<ClInclude Include=".\L4APP.H" />
|
||||
<ClInclude Include=".\L4AUDEFX.h" />
|
||||
<ClInclude Include=".\L4AUDHDW.h" />
|
||||
<ClInclude Include=".\L4AUDIO.h" />
|
||||
<ClInclude Include=".\L4AUDLVL.h" />
|
||||
@@ -454,6 +458,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" />
|
||||
|
||||
@@ -486,6 +486,9 @@
|
||||
<ClCompile Include=".\L4AUDHDW.cpp">
|
||||
<Filter>Source Files\MUNGA_L4</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include=".\L4AUDEFX.cpp">
|
||||
<Filter>Source Files\MUNGA_L4</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include=".\L4AUDIO.cpp">
|
||||
<Filter>Source Files\MUNGA_L4</Filter>
|
||||
</ClCompile>
|
||||
@@ -1058,6 +1061,9 @@
|
||||
<ClInclude Include=".\L4APP.H">
|
||||
<Filter>Header Files\MUNGA_L4</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include=".\L4AUDEFX.h">
|
||||
<Filter>Header Files\MUNGA_L4</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include=".\L4AUDHDW.h">
|
||||
<Filter>Header Files\MUNGA_L4</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -32,6 +32,13 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd)
|
||||
default.file = "";
|
||||
default.implemented = false;
|
||||
default.loop = SampleLoop::LoopAtWill;
|
||||
//
|
||||
// -1 = no buffer. Every caller tests bufferIndex >= 0 before using it as
|
||||
// an index, and this one field was being left as whatever was on the
|
||||
// stack - so "this zone does not exist" read as a real buffer whenever
|
||||
// the garbage happened to be positive, and indexed g_buffers with it.
|
||||
//
|
||||
default.bufferIndex = -1;
|
||||
|
||||
if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum)
|
||||
{
|
||||
|
||||
@@ -75,6 +75,18 @@ 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:
|
||||
@@ -84,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
|
||||
|
||||
@@ -147,8 +147,17 @@ void
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: CreateRPCameraShip - making the camera ship\n"
|
||||
<< std::flush;
|
||||
}
|
||||
CameraDirector::CreateCameraShip(time_slice);
|
||||
SetPerformance(&RPCameraDirector::BeARPDirector);
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: camera ship up, now directing\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
@@ -172,6 +181,13 @@ RPCameraDirector::RPCameraDirector(
|
||||
{
|
||||
SetPerformance(&RPCameraDirector::CreateRPCameraShip);
|
||||
}
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: RPCameraDirector built, football="
|
||||
<< (int) (martianFootball ? 1 : 0)
|
||||
<< " replicant=" << (int) (GetInstance() == ReplicantInstance ? 1 : 0)
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
+158
-2
@@ -19,7 +19,40 @@
|
||||
// fade; the rest is the podium. Kept under the +30s LightsOut post so that
|
||||
// never fires while the stand is up.
|
||||
//
|
||||
const Scalar winnersCircleHoldTime = 11.0f;
|
||||
// RP412PODIUMHOLD tunes it, and RP412PODIUM=0 declines it entirely: that
|
||||
// option promises "straight to the results", and it used to get the full
|
||||
// eleven seconds against a black screen anyway, because this timer never
|
||||
// asked whether there was a podium to hold the mission open FOR. Zero
|
||||
// here means "do not override the base fade" - the stock 3 seconds.
|
||||
//
|
||||
static Scalar
|
||||
WinnersCircleHoldTime()
|
||||
{
|
||||
static Scalar
|
||||
hold = (Scalar) -1;
|
||||
|
||||
if (hold < (Scalar) 0)
|
||||
{
|
||||
const char *podium = getenv("RP412PODIUM");
|
||||
if (podium != NULL && atoi(podium) == 0)
|
||||
{
|
||||
hold = (Scalar) 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *setting = getenv("RP412PODIUMHOLD");
|
||||
hold = (setting != NULL) ? (Scalar) atof(setting) : (Scalar) 11;
|
||||
//
|
||||
// Under a second cuts into the race's own fade-out, and a
|
||||
// minute is a stuck-looking screen; both read as bugs, not
|
||||
// choices.
|
||||
//
|
||||
if (hold < (Scalar) 1) hold = (Scalar) 1;
|
||||
if (hold > (Scalar) 60) hold = (Scalar) 60;
|
||||
}
|
||||
}
|
||||
return hold;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//######################## RPPlayer__StatusMessage ######################
|
||||
@@ -214,7 +247,19 @@ void
|
||||
//
|
||||
if (application->GetApplicationState() == Application::EndingMission)
|
||||
{
|
||||
fadeTimeRemaining = winnersCircleHoldTime;
|
||||
Scalar hold = WinnersCircleHoldTime();
|
||||
if (hold > (Scalar) 0)
|
||||
{
|
||||
fadeTimeRemaining = hold;
|
||||
DEBUG_STREAM << "WinnersCircle: holding the mission open "
|
||||
<< hold << "s for the stand\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "WinnersCircle: podium off - the race fade "
|
||||
<< "stands (" << fadeTimeRemaining << "s) and the results "
|
||||
<< "come straight up\n" << std::flush;
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -382,6 +427,96 @@ void RPPlayer::ResetAfterDeath(DropZone::ReplyMessage *message)
|
||||
ForceUpdate();
|
||||
SetSimulationState(DropZoneAcquiredState);
|
||||
dropZoneLocation = message->dropZoneLocation;
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// Fixed-step: the RECOVERY goes on the vehicle's own step grid.
|
||||
//
|
||||
// The event queue runs on wall clock, so a Reset fired from it
|
||||
// lands between different sim steps on every run - the crash was
|
||||
// measured bit-identical between runs and the first divergence was
|
||||
// the step after the pod stood back up. The vehicle applies the
|
||||
// teleport itself at the first step one SIM second after now; the
|
||||
// message still makes its round trip below, but only for the
|
||||
// player-state bookkeeping - the handler leaves the physics to the
|
||||
// schedule it can see is pending.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
if (Simulation::FixedStep() > (Scalar) 0 &&
|
||||
playerVehicle != NULL && playerVehicle->GetClassID() == VTVClassID)
|
||||
{
|
||||
VTV *vtv = (VTV *) playerVehicle;
|
||||
|
||||
//
|
||||
// Anchored to the DEATH and quantized to a half-second grid.
|
||||
//
|
||||
// This handler runs when the drop-zone reply finally comes off
|
||||
// the event queue, and the whole death-to-here chain is wall
|
||||
// clock - the fry retries repost at Now()+2.0, and the measured
|
||||
// arrival is about five sim-seconds after death, give or take a
|
||||
// few STEPS of queue jitter. An anchor of death+1.0 is long past
|
||||
// by then, so "fire at the next step" inherited the jitter
|
||||
// whole.
|
||||
//
|
||||
// The death stamp is the last step-exact event in the chain, so:
|
||||
// take the measured gap, add the second the old code waited, and
|
||||
// round UP to the next half-second AFTER THE DEATH. The jitter
|
||||
// is hundredths; the nearest grid boundary is tenths away; every
|
||||
// run lands in the same cell and fires on the same step. The
|
||||
// felt delay is the same six-ish seconds it has always been.
|
||||
//
|
||||
Time due;
|
||||
Time death_mark;
|
||||
if (vtv->ConsumeDeathClock(&death_mark))
|
||||
{
|
||||
Scalar gap = vtv->GetLastPerformance() - death_mark;
|
||||
const Scalar quantum = (Scalar) 0.5;
|
||||
int cells = (int)((gap + (Scalar) 1.0) / quantum) + 1;
|
||||
|
||||
due = death_mark;
|
||||
due += quantum * (Scalar) cells;
|
||||
}
|
||||
else
|
||||
{
|
||||
due = vtv->GetLastPerformance();
|
||||
due += 1.0f;
|
||||
}
|
||||
vtv->ScheduleRespawn(
|
||||
message->dropZoneLocation, due,
|
||||
(goalEntity != NULL)
|
||||
? goalEntity->localOrigin.linearPosition
|
||||
: Point3D(0.0f, 0.0f, 0.0f),
|
||||
(goalEntity != NULL) ? True : False);
|
||||
respawnScheduled = True;
|
||||
|
||||
//
|
||||
// Under the trace, say what was scheduled in run-comparable
|
||||
// terms: the pad (identity by position), and how far ahead of
|
||||
// the vehicle's clock the due time sits. Two runs that disagree
|
||||
// here diverge before the physics gets a vote.
|
||||
//
|
||||
{
|
||||
static int diag = -1;
|
||||
if (diag < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412PHYSTRACE");
|
||||
diag = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diag)
|
||||
{
|
||||
char buffer[160];
|
||||
sprintf(buffer,
|
||||
"PhysTrace: respawn #%d scheduled, pad %.2f,%.2f "
|
||||
"due in %.4f sim-s\n",
|
||||
deathCount,
|
||||
(double) message->dropZoneLocation.linearPosition.x,
|
||||
(double) message->dropZoneLocation.linearPosition.z,
|
||||
(double)(Scalar)(due - vtv->GetLastPerformance()));
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Time when = Now();
|
||||
when += 1.0f;
|
||||
application->Post(HighEventPriority, this, message, when);
|
||||
@@ -422,6 +557,7 @@ void
|
||||
}
|
||||
AlwaysExecute();
|
||||
deathCount = 0;
|
||||
respawnScheduled = False;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -467,6 +603,26 @@ void
|
||||
{
|
||||
VTV *vtv = (VTV*)playerVehicle;
|
||||
Check(vtv);
|
||||
|
||||
//
|
||||
// A SCHEDULED respawn means the vehicle already holds - or has
|
||||
// already applied - its teleport and goal-flip, on its own step
|
||||
// grid. Resetting it AGAIN here, at whatever wall instant this
|
||||
// message came off the queue, would re-teleport it mid-step and
|
||||
// put the nondeterminism straight back.
|
||||
//
|
||||
// The flag, not "is fixed stepping on": this same leg also runs
|
||||
// for the FIRST spawn of the mission, where the Reset below is
|
||||
// what wakes a Mover out of its initial stasis. Gating on the
|
||||
// mode alone skipped that wake-up and parked the pod, frozen at
|
||||
// exactly its spawn point, for an entire race.
|
||||
//
|
||||
if (respawnScheduled)
|
||||
{
|
||||
respawnScheduled = False;
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
vtv->Reset(message->dropZoneLocation, VTV::RegularReset);
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,18 @@ public:
|
||||
Entity
|
||||
*goalEntity;
|
||||
|
||||
//
|
||||
// True between ResetAfterDeath handing the recovery to the vehicle's
|
||||
// step grid (fixed-step only) and the bookkeeping message coming back
|
||||
// round. The message handler must NOT Reset the vehicle again in that
|
||||
// window - but it MUST still Reset on the first spawn of the mission,
|
||||
// which is what wakes a Mover out of its initial stasis. Gating on
|
||||
// "is fixed stepping on" instead of on this flag skipped that wake-up
|
||||
// and froze the pod on its pad for the whole race.
|
||||
//
|
||||
Logical
|
||||
respawnScheduled;
|
||||
|
||||
private:
|
||||
|
||||
static const IndexEntry AttributePointers[];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "rpmssn.h"
|
||||
#include "rpplayer.h"
|
||||
#include "rpdirect.h"
|
||||
#include "..\munga\app.h" // RPCameraLog
|
||||
#include "crusher.h"
|
||||
#include "runner.h"
|
||||
#include "blocker.h"
|
||||
@@ -170,6 +171,11 @@ Player*
|
||||
|
||||
else
|
||||
{
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: registry making an RPCameraDirector (game model '"
|
||||
<< mission->GetGameModel() << "')\n" << std::flush;
|
||||
}
|
||||
RPCameraDirector::MakeMessage create_director(
|
||||
RPCameraDirector::MakeMessageID,
|
||||
sizeof(RPCameraDirector::MakeMessage),
|
||||
|
||||
+146
-3
@@ -803,9 +803,17 @@ void
|
||||
Check(*damageZones);
|
||||
(*damageZones)->TakeDamage(collision_damage);
|
||||
|
||||
localVelocity.angularMotion.x += 8.0f * Random - 4.0f;
|
||||
localVelocity.angularMotion.y += 8.0f * Random - 4.0f;
|
||||
localVelocity.angularMotion.z += 8.0f * Random - 4.0f;
|
||||
//
|
||||
// The tumble draws from the vehicle's OWN stream, not the
|
||||
// global Random - the global one is shared with the frame
|
||||
// loop's consumers (particles, mostly), so its position here
|
||||
// depended on how many frames had rendered. This kick goes
|
||||
// straight into physics state; it was the last wall-clocked
|
||||
// input left in the whole death cycle.
|
||||
//
|
||||
localVelocity.angularMotion.x += 8.0f * TumbleRandom() - 4.0f;
|
||||
localVelocity.angularMotion.y += 8.0f * TumbleRandom() - 4.0f;
|
||||
localVelocity.angularMotion.z += 8.0f * TumbleRandom() - 4.0f;
|
||||
}
|
||||
}
|
||||
worldLinearAcceleration = zippy_accel;
|
||||
@@ -1273,6 +1281,18 @@ VTV::VTV(
|
||||
boosterSmokeDensity = 0.0f;
|
||||
doorHitNormal = Vector3D::Identity;
|
||||
lastDoorHit = Time::Null;
|
||||
respawnPending = False;
|
||||
respawnHaveGoal = False;
|
||||
deathClockValid = False;
|
||||
|
||||
//
|
||||
// Creation order is deterministic, so each vehicle's tumble stream
|
||||
// is too - see TumbleRandom in the header.
|
||||
//
|
||||
{
|
||||
static unsigned long tumble_births = 0;
|
||||
tumbleSeed = 0x52503431UL + 7919UL * ++tumble_births;
|
||||
}
|
||||
heightAboveTerrain = 0.0f;
|
||||
forwardVelocity = 0.0f;
|
||||
hornBlast = -1;
|
||||
@@ -1682,6 +1702,119 @@ void
|
||||
subsystem->DeathReset(reset_command);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The controls mapper too. It is subsystem ZERO, below
|
||||
// BasicSubsystemCount, so the loop above has never reached it - which
|
||||
// was fine while it had nothing to reset. It does now: the respawn
|
||||
// throttle latch, which holds the pod at zero throttle until the
|
||||
// player's control has come back to zero. See
|
||||
// VTVControlsMapper::DeathReset for which resets latch and why.
|
||||
//
|
||||
{
|
||||
Subsystem *mapper = GetSubsystem(ControlsMapperSubsystem);
|
||||
|
||||
if (mapper)
|
||||
{
|
||||
Check(mapper);
|
||||
mapper->DeathReset(reset_command);
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
VTV::ScheduleRespawn(
|
||||
const Origin &new_origin,
|
||||
const Time &due,
|
||||
const Point3D &face_toward,
|
||||
Logical have_goal
|
||||
)
|
||||
{
|
||||
Check(this);
|
||||
Check(&new_origin);
|
||||
|
||||
respawnOrigin = new_origin;
|
||||
respawnDue = due;
|
||||
respawnGoal = face_toward;
|
||||
respawnHaveGoal = have_goal;
|
||||
respawnPending = True;
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// The pending respawn lands here, at the top of the first STEP whose clock
|
||||
// has reached its due time - the same step count after death on every run
|
||||
// and every frame rate. The old path applied the Reset from the event
|
||||
// queue, which runs on wall clock: the crash was deterministic and the
|
||||
// recovery was not, measured as two identical runs diverging on the first
|
||||
// step after the pod stood back up.
|
||||
//
|
||||
// The turn-to-face-the-scorezone flip is the same arithmetic
|
||||
// RPPlayer::PointVTVTowardGoal does, done here because it reads the
|
||||
// POST-reset heading - it belongs to the same step as the teleport.
|
||||
//
|
||||
void
|
||||
VTV::BeginStep()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (respawnPending && !(GetLastPerformance() < respawnDue))
|
||||
{
|
||||
respawnPending = False;
|
||||
|
||||
{
|
||||
static int diag = -1;
|
||||
if (diag < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412PHYSTRACE");
|
||||
diag = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diag)
|
||||
{
|
||||
char buffer[120];
|
||||
sprintf(buffer,
|
||||
"PhysTrace: respawn fired, %.4f sim-s late, pad %.2f,%.2f\n",
|
||||
(double)(Scalar)(GetLastPerformance() - respawnDue),
|
||||
(double) respawnOrigin.linearPosition.x,
|
||||
(double) respawnOrigin.linearPosition.z);
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
Reset(respawnOrigin, RegularReset);
|
||||
|
||||
if (respawnHaveGoal)
|
||||
{
|
||||
Vector3D to_goal;
|
||||
to_goal.Subtract(respawnGoal, localOrigin.linearPosition);
|
||||
|
||||
UnitVector current_heading;
|
||||
localToWorld.GetFromAxis(Z_Axis, ¤t_heading);
|
||||
|
||||
Scalar length_to_goal = to_goal.LengthSquared();
|
||||
if (length_to_goal > SMALL)
|
||||
{
|
||||
Scalar dot_prod =
|
||||
(to_goal * current_heading) / Sqrt(length_to_goal);
|
||||
if (dot_prod >= 0.0f)
|
||||
{
|
||||
Quaternion turn_around;
|
||||
Quaternion y_roll(0.0f, 1.0f, 0.0f, 0.0);
|
||||
|
||||
turn_around.Multiply(
|
||||
localOrigin.angularPosition, y_roll);
|
||||
localOrigin.angularPosition = turn_around;
|
||||
localToWorld = localOrigin;
|
||||
}
|
||||
}
|
||||
ForceUpdate();
|
||||
}
|
||||
}
|
||||
Mover::BeginStep();
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -3100,6 +3233,16 @@ void
|
||||
if (damageLevel >= 1.0f)
|
||||
{
|
||||
vtv->SetSimulationState(VTV::BurningState);
|
||||
|
||||
//
|
||||
// Stamp the death on the vehicle's own step clock, here at the
|
||||
// one site that declares it dead. The respawn schedule anchors
|
||||
// to this instant - the last step-exact event in the death
|
||||
// chain - so the recovery lands the same number of steps after
|
||||
// the crash on every run. Marked once; repeated damage while
|
||||
// already burning does not move it.
|
||||
//
|
||||
vtv->MarkDeathClock();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -504,6 +504,58 @@ public:
|
||||
void
|
||||
Reset(const Origin &new_origin, int reset_command);
|
||||
|
||||
//
|
||||
// A respawn that lands on this vehicle's own step grid. Under fixed
|
||||
// stepping the player's death recovery cannot ride the event queue -
|
||||
// the queue runs on wall clock, and a Reset that fires at a wall
|
||||
// instant lands between different sim steps on every run. Scheduled
|
||||
// here instead, BeginStep applies it at the first step whose clock
|
||||
// reaches 'due': same step count after death, every run, every
|
||||
// frame rate. The goal point rides along because the turn-to-face-
|
||||
// the-scorezone flip depends on the POST-reset heading, so it has
|
||||
// to happen in the same step as the teleport.
|
||||
//
|
||||
void
|
||||
ScheduleRespawn(
|
||||
const Origin &new_origin,
|
||||
const Time &due,
|
||||
const Point3D &face_toward,
|
||||
Logical have_goal
|
||||
);
|
||||
void
|
||||
BeginStep();
|
||||
|
||||
//
|
||||
// The instant this vehicle died, on its own step clock - stamped at
|
||||
// the single site that sets BurningState, which runs inside the step
|
||||
// machinery and is therefore already deterministic. The respawn
|
||||
// schedule anchors HERE rather than at the moment the drop-zone
|
||||
// reply happens to come off the event queue: the death is the last
|
||||
// step-exact event in the chain, so "one second after death" is the
|
||||
// same step count on every run. Marked once per death; consuming it
|
||||
// re-arms it for the next one.
|
||||
//
|
||||
void
|
||||
MarkDeathClock()
|
||||
{
|
||||
if (!deathClockValid)
|
||||
{
|
||||
deathClock = GetLastPerformance();
|
||||
deathClockValid = True;
|
||||
}
|
||||
}
|
||||
Logical
|
||||
ConsumeDeathClock(Time *when_out)
|
||||
{
|
||||
if (!deathClockValid)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
*when_out = deathClock;
|
||||
deathClockValid = False;
|
||||
return True;
|
||||
}
|
||||
|
||||
void
|
||||
DeathShutdown(int shutdown_command);
|
||||
|
||||
@@ -514,6 +566,38 @@ public:
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Navigation support
|
||||
protected:
|
||||
// the pending step-grid respawn - see ScheduleRespawn
|
||||
Origin
|
||||
respawnOrigin;
|
||||
Time
|
||||
respawnDue,
|
||||
deathClock;
|
||||
Point3D
|
||||
respawnGoal;
|
||||
Logical
|
||||
respawnPending,
|
||||
respawnHaveGoal,
|
||||
deathClockValid;
|
||||
|
||||
//
|
||||
// The out-of-world tumble's own random stream. The global Random is
|
||||
// shared with frame-cadence consumers - particles above all - so its
|
||||
// position when a burning pod draws from it depends on how many
|
||||
// frames have rendered, which is wall clock, which makes the tumble
|
||||
// differ between identical runs. A per-vehicle generator seeded by
|
||||
// creation order keeps the tumble looking random while drawing the
|
||||
// same kicks at the same steps every run.
|
||||
//
|
||||
unsigned long
|
||||
tumbleSeed;
|
||||
|
||||
Scalar
|
||||
TumbleRandom()
|
||||
{
|
||||
tumbleSeed = tumbleSeed * 1103515245UL + 12345UL;
|
||||
return (Scalar)((tumbleSeed >> 16) & 0x7FFF) / (Scalar) 32767;
|
||||
}
|
||||
|
||||
Scalar
|
||||
targetRangeExponent,
|
||||
currentRangeExponent;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "vtvpwr.h"
|
||||
#include "..\munga\icom.h"
|
||||
#include "..\munga\app.h"
|
||||
#include "..\munga\inputscript.h"
|
||||
#include "rpplayer.h"
|
||||
#include "vtv.h"
|
||||
|
||||
@@ -400,6 +401,28 @@ VTVControlsMapper::AttributeIndexSet& VTVControlsMapper::GetAttributeIndex()
|
||||
// Model Support
|
||||
//
|
||||
|
||||
//
|
||||
// How close to zero the throttle control must come to release the latch,
|
||||
// as a fraction of full travel. Wide enough for a resting pad trigger, a
|
||||
// wound-down keyboard axis, or a real lever's potentiometer sitting a few
|
||||
// counts off its stop - the pod bay hardware is where this will matter -
|
||||
// and narrow enough that it cannot be satisfied by easing off.
|
||||
//
|
||||
static const Scalar kThrottleLatchRelease = 0.05f;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
VTVControlsMapper::DeathReset(int reset_command)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (reset_command == VTV::RegularReset)
|
||||
{
|
||||
throttleLatched = True;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
@@ -414,6 +437,36 @@ void
|
||||
VTVPower *power_system =
|
||||
Cast_Object(VTVPower*, vtv->GetSubsystem(VTV::PowerSubsystem));
|
||||
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// RP412INPUTSCRIPT: scripted driving, on this subsystem's own step
|
||||
// clock.
|
||||
//
|
||||
// This is the one place every mapper - RIO, Thrustmaster, pad -
|
||||
// funnels through, and it runs per SIMULATION STEP, so a scripted
|
||||
// value lands on the same step of every run whatever the frame
|
||||
// rate. Overriding at the RIO or the controls manager would key
|
||||
// the timeline to the frame loop, which is wall clock, which is
|
||||
// the thing the whole harness exists to keep out of the physics.
|
||||
//
|
||||
// Only the player's own vehicle: replicants get their state from
|
||||
// the network, and the mapper does not run for them anyway.
|
||||
//----------------------------------------------------------------
|
||||
//
|
||||
if (RPInputScript_Active())
|
||||
{
|
||||
float script_throttle, script_x, script_y, script_pedals;
|
||||
|
||||
if (RPInputScript_Sample(GetLastPerformance(),
|
||||
&script_throttle, &script_x, &script_y, &script_pedals))
|
||||
{
|
||||
throttlePosition = script_throttle;
|
||||
stickPosition.x = script_x;
|
||||
stickPosition.y = script_y;
|
||||
pedalsPosition = script_pedals;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//----------------------------------------------
|
||||
// Make sure the control inputs are within range
|
||||
@@ -424,6 +477,43 @@ void
|
||||
Verify(stickPosition.y >= -1.0f && stickPosition.y <= 1.0f);
|
||||
Verify(pedalsPosition >= -1.0f && pedalsPosition <= 1.0f);
|
||||
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// The respawn throttle latch.
|
||||
//
|
||||
// A pod that dies at full throttle reappears under a hand still
|
||||
// holding full throttle, and used to launch on it. From a respawn
|
||||
// until the control has been seen back at (near) zero, the mapper
|
||||
// computes as though the throttle were zero.
|
||||
//
|
||||
// The zeroing is for this pass only - the true lever position is
|
||||
// put back at the bottom of the function - so the attribute the
|
||||
// cockpit gauge and the watchers read stays the player's actual
|
||||
// hand, which is the thing they need to see to bring it down. It
|
||||
// also means the release test reads the real control each step
|
||||
// rather than last step's overwrite: RIO analog events arrive on
|
||||
// CHANGE, so an overwritten attribute would otherwise sit at zero,
|
||||
// release the latch on its own, and hand back a live throttle the
|
||||
// moment the hand moved.
|
||||
//
|
||||
// Placed after the input script so a scripted run latches the same
|
||||
// way a hand-driven one does, per step, deterministically.
|
||||
//----------------------------------------------------------------
|
||||
//
|
||||
Scalar lever = throttlePosition;
|
||||
|
||||
if (throttleLatched)
|
||||
{
|
||||
if (lever <= kThrottleLatchRelease)
|
||||
{
|
||||
throttleLatched = False;
|
||||
}
|
||||
else
|
||||
{
|
||||
throttlePosition = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// Figure out the proper control model to use based upon the speed
|
||||
@@ -802,6 +892,12 @@ void
|
||||
intercom->SetPTTStatus((Logical) pttStatus > 0);
|
||||
}
|
||||
|
||||
//
|
||||
// Put the real lever position back - see the latch above. Identity
|
||||
// whenever the latch is idle.
|
||||
//
|
||||
throttlePosition = lever;
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -918,6 +1014,7 @@ VTVControlsMapper::VTVControlsMapper(
|
||||
stickPosition.y = 0.0f;
|
||||
throttlePosition = 0.0f;
|
||||
pedalsPosition = 0.0f;
|
||||
throttleLatched = False; // the first-spawn Reset sets it
|
||||
|
||||
reverseThrust = 0;
|
||||
liftCut = 0;
|
||||
|
||||
@@ -182,6 +182,16 @@ protected:
|
||||
ControlsButton
|
||||
previousPTTStatus; // used to detect change in PTT status
|
||||
|
||||
//
|
||||
// True from a respawn until the throttle control has been seen at
|
||||
// (near) zero. While set, InterpretControls behaves as though the
|
||||
// throttle were zero, whatever the hand is doing; the attribute
|
||||
// itself keeps the real lever value so the cockpit gauge shows the
|
||||
// player what they must bring down. See DeathReset.
|
||||
//
|
||||
Logical
|
||||
throttleLatched;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Model Support
|
||||
//
|
||||
@@ -206,6 +216,22 @@ public:
|
||||
void
|
||||
SetConfigurationState(Logical enter_config);
|
||||
|
||||
//
|
||||
// Latch the throttle at zero until the player's control comes back to
|
||||
// zero. Called by VTV::Reset on a regular reset - a death respawn, and
|
||||
// the first spawn - because the hand that was holding full throttle
|
||||
// when the pod died is still holding it when the pod reappears, and
|
||||
// the pod used to launch on it. The arcade never had the problem the
|
||||
// same way: its throttle was a physical lever the pilot had to move
|
||||
// regardless.
|
||||
//
|
||||
// Football and mission review resets do not latch: one repositions
|
||||
// every pod mid-game where the friction is unwanted, the other has no
|
||||
// controls at all.
|
||||
//
|
||||
void
|
||||
DeathReset(int reset_command);
|
||||
|
||||
virtual void
|
||||
CreateTemporaryEventMappings(
|
||||
Receiver *receiver,
|
||||
|
||||
+249
-25
@@ -22,12 +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
|
||||
@@ -39,6 +42,7 @@
|
||||
#include <strsafe.h>
|
||||
#include <direct.h>
|
||||
#include <shellapi.h>
|
||||
#include <time.h> // the test-build expiry check
|
||||
|
||||
#define SPOOL_SIZE 0x600000
|
||||
|
||||
@@ -148,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)
|
||||
@@ -162,34 +175,138 @@ 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"
|
||||
"Ask for the current one.",
|
||||
RP412_VERSION_LONG, RP412_EXPIRY_TEXT);
|
||||
MessageBoxA(NULL, notice, "Red Planet - test build expired",
|
||||
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// The last few days get a line in the log, so somebody reading a
|
||||
// report can see the build was nearly out rather than wondering.
|
||||
//
|
||||
struct tm expiry_day;
|
||||
memset(&expiry_day, 0, sizeof(expiry_day));
|
||||
expiry_day.tm_year = RP412_EXPIRY_YEAR - 1900;
|
||||
expiry_day.tm_mon = RP412_EXPIRY_MONTH - 1;
|
||||
expiry_day.tm_mday = RP412_EXPIRY_DAY;
|
||||
//
|
||||
// The END of the expiry day, because that is the rule the check
|
||||
// above enforces - the build is good for all of that date and
|
||||
// refuses the morning after. Anchoring at midday instead would
|
||||
// report one day fewer than the build actually has left.
|
||||
//
|
||||
expiry_day.tm_hour = 23;
|
||||
expiry_day.tm_min = 59;
|
||||
expiry_day.tm_sec = 59;
|
||||
expiry_day.tm_isdst = -1;
|
||||
__time64_t expiry_time = _mktime64(&expiry_day);
|
||||
if (expiry_time != (__time64_t) -1)
|
||||
{
|
||||
int days_left = (int)((expiry_time - raw_now) / (24 * 60 * 60));
|
||||
if (days_left <= 3)
|
||||
{
|
||||
DEBUG_STREAM << "Build expires " << RP412_EXPIRY_TEXT << " ("
|
||||
<< days_left << " day(s) left)\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
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.6" << std::endl << std::flush;
|
||||
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
|
||||
|
||||
//
|
||||
// The frame target and whether drawing interpolates, stated outright.
|
||||
// Both were previously invisible: TARGETFPS appeared nowhere in the log,
|
||||
// so a run could not be checked afterwards against what it was actually
|
||||
// asked for, and interpolation only announced itself when switched OFF.
|
||||
// Between them that cost a wasted test run and an ambiguous one.
|
||||
//
|
||||
// The mismatch note matters because a frame target that is not the
|
||||
// physics rate used to mean visibly stepped motion - it no longer does
|
||||
// while interpolation is on, which is exactly why the log should say
|
||||
// which of the two states it is in.
|
||||
//
|
||||
{
|
||||
const char *interp = getenv("RP412INTERP");
|
||||
Logical blending = !(interp != NULL && atoi(interp) == 0);
|
||||
const char *physics = getenv("RP412PHYSICSHZ");
|
||||
int fps = atoi(getenv("TARGETFPS"));
|
||||
int hz = (physics != NULL) ? atoi(physics) : 0;
|
||||
|
||||
DEBUG_STREAM << "Video: frame target " << fps << " fps, drawing "
|
||||
<< (blending ? "interpolated across the physics step" : "on exact physics steps")
|
||||
<< std::endl << std::flush;
|
||||
|
||||
if (!blending && hz > 0 && fps != hz)
|
||||
{
|
||||
DEBUG_STREAM << "Video: " << fps << " fps against a " << hz
|
||||
<< " Hz physics step with interpolation off - motion will step"
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef RP412_STEAM
|
||||
//
|
||||
// RP412STEAM=1 (environ.ini or a Steam launch) swaps the wire to
|
||||
@@ -256,12 +373,41 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
}
|
||||
|
||||
|
||||
DWORD wsStyle = WS_OVERLAPPED | WS_SYSMENU;
|
||||
if (L4Application::GetFullscreen())
|
||||
wsStyle = WS_POPUP;
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_OVERLAPPEDWINDOW, 0, 0, L4Application::GetScreenWidth(), L4Application::GetScreenHeight(), (HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
||||
if (!hWnd)
|
||||
return FALSE;
|
||||
//
|
||||
// The style the window is BORN with, rather than one worked out and
|
||||
// then thrown away - the old code computed a style and handed
|
||||
// CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless.
|
||||
//
|
||||
// Windowed keeps the full overlapped set on purpose: the sizing
|
||||
// border and the maximise box are how the player arranges the
|
||||
// cockpit, and mfd_layout.cfg remembers where they left it. The
|
||||
// borderless modes are born borderless instead of being restyled a
|
||||
// moment later, so there is no framed window on screen first.
|
||||
//
|
||||
DWORD wsStyle = WS_OVERLAPPEDWINDOW;
|
||||
if (L4Application::GetFullscreen() || L4Application::GetFitDisplay())
|
||||
{
|
||||
wsStyle = WS_POPUP;
|
||||
}
|
||||
|
||||
//
|
||||
// -res is a RENDER size, so it is the client area that has to be it.
|
||||
// Passed straight to CreateWindowEx it sets the OUTER rectangle and
|
||||
// the chrome comes out of the middle - which is how -res 640 480
|
||||
// came to present into a 624x441 client.
|
||||
//
|
||||
RECT wanted;
|
||||
wanted.left = 0;
|
||||
wanted.top = 0;
|
||||
wanted.right = (LONG) L4Application::GetScreenWidth();
|
||||
wanted.bottom = (LONG) L4Application::GetScreenHeight();
|
||||
AdjustWindowRect(&wanted, wsStyle, FALSE);
|
||||
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", wsStyle, 0, 0,
|
||||
wanted.right - wanted.left, wanted.bottom - wanted.top,
|
||||
(HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
||||
if (!hWnd)
|
||||
return FALSE;
|
||||
|
||||
ShowWindow(hWnd, nShowCmd);
|
||||
|
||||
@@ -278,6 +424,45 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
RPWindowLayout_Register(hWnd, "RPL4", True);
|
||||
RPWindowLayout_Load();
|
||||
}
|
||||
else if (L4Application::GetFitDisplay())
|
||||
{
|
||||
//
|
||||
// -fit has no saved placement to restore, but it does have a
|
||||
// shape to take, and it has to take it NOW. SVGA16 applies the
|
||||
// same borderless full-monitor rect when it assembles the
|
||||
// cockpit, which is after the first mission has already built
|
||||
// its D3D device against the window as it stands - so the first
|
||||
// race of a session used to render to a bordered client and
|
||||
// every race after it to the borderless monitor. Same lobby,
|
||||
// same settings, different target and different frame cost.
|
||||
//
|
||||
// Settle the window here and every mission of the session,
|
||||
// first included, is set up against the identical client area.
|
||||
//
|
||||
L4Application::FitWindowToMonitor(hWnd);
|
||||
}
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// RP412JOYCONFIG=1 (joyconfig.bat): the joystick setup wizard, before
|
||||
// anything else claims the screen. It asks the player to move each
|
||||
// control on their stick, HOTAS or pedals and writes the joy* rows of
|
||||
// bindings.txt, then falls through into the game so they can try them
|
||||
// straight away.
|
||||
//
|
||||
// One shot: the variable is cleared from this process so a rebuilt
|
||||
// PadRIO later in the session cannot run the wizard a second time.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
const char *joyconfig = getenv("RP412JOYCONFIG");
|
||||
if (joyconfig != NULL && *joyconfig != '\0' && atoi(joyconfig) != 0)
|
||||
{
|
||||
RPJoyConfigWizard();
|
||||
SetEnvironmentVariableA("RP412JOYCONFIG", NULL);
|
||||
_putenv("RP412JOYCONFIG=");
|
||||
}
|
||||
}
|
||||
|
||||
#if !_DEBUG
|
||||
// Arcade pods have no mouse - but desktop/windowed play needs the
|
||||
@@ -305,6 +490,31 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
|
||||
int last_launch_mode = FELaunchSingle;
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// A hand-fed egg run stays unmarshaled - no console, so the race never
|
||||
// ends - UNLESS it is given a length. RP412MISSIONSECONDS supplies one,
|
||||
// and with it '-egg' can play a whole race through to the buzzer, the
|
||||
// podium and the results.
|
||||
//
|
||||
// That is the difference between a shortcut that can only be watched and
|
||||
// one that can be TESTED: everything after the chequered flag - the fade,
|
||||
// the winners' circle, the teardown - was unreachable from the command
|
||||
// line, so it could only ever be exercised by hand through the menu.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
if (!front_end_mode && L4Application::GetEggNotationFileName())
|
||||
{
|
||||
const char *egg_seconds = getenv("RP412MISSIONSECONDS");
|
||||
if (egg_seconds != NULL && atoi(egg_seconds) > 0)
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: marshalling the hand-fed egg run ("
|
||||
<< atoi(egg_seconds) << "s, RP412MISSIONSECONDS)\n" << std::flush;
|
||||
L4Application::SetNetworkCommonFlatAddress(0);
|
||||
RPL4LocalConsole_Install(atoi(egg_seconds));
|
||||
}
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (front_end_mode)
|
||||
@@ -428,12 +638,26 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
if (!spoolFileName)
|
||||
spoolFileName = "last.spl";
|
||||
|
||||
//
|
||||
// Say which of the two this is. The choice was made in silence,
|
||||
// and the two look identical from outside for the first few
|
||||
// seconds - one plays a race back, the other sits there - so a
|
||||
// spool that failed to load was indistinguishable from a spool
|
||||
// that loaded and had not started yet.
|
||||
//
|
||||
SpoolFile *spool = spool_mgr->GetStoredSpoolFile(spoolFileName);
|
||||
if (spool)
|
||||
{
|
||||
DEBUG_STREAM << "Playback: " << spoolFileName
|
||||
<< " loaded, " << (int) spool->GetBytesRemaining()
|
||||
<< " bytes to play\n" << std::flush;
|
||||
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool);
|
||||
} else
|
||||
{
|
||||
DEBUG_STREAM << "Playback: could not load '" << spoolFileName
|
||||
<< "' - starting idle with nothing to show. It must exist, be"
|
||||
<< " no larger than " << (int) SPOOL_SIZE
|
||||
<< " bytes, and have been written by this build.\n" << std::flush;
|
||||
new_app = new RPL4IdleApplication(hInstance, hWnd, &resources);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "..\munga_l4\l4ctrl.h"
|
||||
#include "..\munga_l4\l4mppr.h"
|
||||
#include "..\munga\appmgr.h"
|
||||
#include "..\munga\spooler.h"
|
||||
#include "rpl4mode.h"
|
||||
#include "..\rp\vtv.h"
|
||||
#include "rpl4mppr.h"
|
||||
@@ -244,6 +245,14 @@ void
|
||||
// you would be the one empty spot. Turn it inside out before the shot.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Timed, because the black screen between the race fading out and the
|
||||
// stand fading in is several seconds long and the fade only accounts
|
||||
// for 0.7 of them. Nothing else runs while this does - the whole game
|
||||
// is one thread - so whatever these two cost IS that gap.
|
||||
//
|
||||
Time podiumOutsideStart = Now();
|
||||
|
||||
dpl_renderer->ShowViewpointFromOutside();
|
||||
|
||||
//
|
||||
@@ -253,8 +262,21 @@ void
|
||||
// whatever order the players were created.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
Time podiumNamesStart = Now();
|
||||
|
||||
dpl_renderer->SortAndReloadNameBitmaps();
|
||||
|
||||
{
|
||||
Time podiumNamesEnd = Now();
|
||||
char timing[160];
|
||||
|
||||
sprintf(timing,
|
||||
"WinnersCircle: exterior %.0f ms, name plates %.0f ms\n",
|
||||
(double)((Scalar)(podiumNamesStart - podiumOutsideStart) * 1000.0f),
|
||||
(double)((Scalar)(podiumNamesEnd - podiumNamesStart) * 1000.0f));
|
||||
DEBUG_STREAM << timing << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------
|
||||
// Widen to 45 degrees and pull back in front of the stand so the whole
|
||||
@@ -446,6 +468,18 @@ void
|
||||
Post(LowEventPriority, this, &podium_message, event_time);
|
||||
|
||||
DEBUG_STREAM << "WinnersCircle: race over, fading out\n" << std::flush;
|
||||
|
||||
//
|
||||
// Write the recording here rather than at teardown. This is the
|
||||
// first of the two StopMissions - the buzzer, not the fade timer -
|
||||
// so it is the end of the RACE, and everything worth keeping has
|
||||
// arrived. Waiting for teardown would risk the process going away
|
||||
// first and taking the spool with it.
|
||||
//
|
||||
if (Application::IsRecording())
|
||||
{
|
||||
SpoolRecorder_Get()->Save();
|
||||
}
|
||||
}
|
||||
|
||||
L4Application::StopMissionMessageHandler(message);
|
||||
|
||||
+518
-11
@@ -6,6 +6,7 @@
|
||||
#include "..\munga\appmgr.h"
|
||||
#include "..\munga\appmsg.h"
|
||||
#include "..\munga\console.h"
|
||||
#include "..\munga\spooler.h"
|
||||
#include "..\rp\rpcnsl.h"
|
||||
#include "..\munga_l4\l4app.h"
|
||||
#include "..\munga_l4\l4net.h"
|
||||
@@ -80,6 +81,9 @@ namespace
|
||||
NetTransport::Connection
|
||||
connection;
|
||||
int state; // last reported application state (-1 unknown)
|
||||
int lastScore; // last telemetry score heard mid-race
|
||||
int lastScoreHostID; // the host ID the pod itself reported
|
||||
Logical haveScore; // any telemetry score at all yet
|
||||
Logical eggAcknowledged;
|
||||
DWORD lastQueryTick;
|
||||
DWORD eggSentTick; // 0 = never sent
|
||||
@@ -164,6 +168,46 @@ namespace
|
||||
pod->connection, packet, (int) sizeof(NetworkPacketHeader) + size);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The same packet, delivered to the station on this machine.
|
||||
//
|
||||
// A real pod bay console sits on its own machine and every station
|
||||
// hears it over the wire. Here it is colocated, and the network stack
|
||||
// is quite right not to push bytes through a socket to reach a client
|
||||
// in the same process - but the console had gone further than that and
|
||||
// called application->Dispatch, stepping past the client's receive
|
||||
// entry altogether. Anything watching packets therefore never saw the
|
||||
// console speak: the first Live Cam recording held all 14,342 of the
|
||||
// racer's packets and not one LoadMission, RunMission or StopMission,
|
||||
// because those were the messages that came from inside the house.
|
||||
//
|
||||
// So build the packet SendWire would have built and hand it to the
|
||||
// client's own front door. No socket, no wire, no copy of the protocol
|
||||
// - just the delivery arriving where a delivery arrives.
|
||||
//---------------------------------------------------------------
|
||||
void DeliverLocal(int client_ID, const void *message, int size)
|
||||
{
|
||||
char packet[sizeof(NetworkPacketHeader) + 1400];
|
||||
|
||||
if (size > (int) sizeof(packet) - (int) sizeof(NetworkPacketHeader))
|
||||
{
|
||||
return;
|
||||
}
|
||||
memset(packet, 0, sizeof(NetworkPacketHeader));
|
||||
|
||||
NetworkPacketHeader *header = (NetworkPacketHeader *) packet;
|
||||
|
||||
header->clientID = (NetworkClient::ClientID) client_ID;
|
||||
header->gameID = 0;
|
||||
header->fromHost = 1; // the console's reserved host ID
|
||||
memcpy(packet + sizeof(NetworkPacketHeader), message, size);
|
||||
|
||||
NetworkPacket *received = (NetworkPacket *) packet;
|
||||
|
||||
Check(application);
|
||||
application->ReceiveNetworkPacket(received, &received->messageData);
|
||||
}
|
||||
|
||||
void SendEggTo(RemotePod *pod)
|
||||
{
|
||||
int chunk_count = (gEggWireSize + 999) / 1000;
|
||||
@@ -259,7 +303,27 @@ namespace
|
||||
(int) message->GetFinalScore());
|
||||
pod->scored = True;
|
||||
}
|
||||
// VTV telemetry (IDs 2-6) skips through for now
|
||||
//
|
||||
// Running telemetry. The pods have been sending it all
|
||||
// along (it fed the arcade console's status board); this
|
||||
// console skipped it - until a player Alt+Q'ed mid-race
|
||||
// and vanished from the score sheet, because a pod that
|
||||
// leaves never sends its EndMission score. Keep the last
|
||||
// score heard, and the host ID the pod itself reported
|
||||
// with it, so the buzzer can stand it in for a pod that
|
||||
// is not there to answer. Leaving early keeps the points
|
||||
// earned; it just stops earning more.
|
||||
//
|
||||
else if ((int) base->messageID ==
|
||||
ConsolePlayerVTVScoreUpdateMessageID)
|
||||
{
|
||||
ConsolePlayerVTVScoreUpdateMessage *message =
|
||||
(ConsolePlayerVTVScoreUpdateMessage *) base;
|
||||
pod->lastScore = (int) message->GetPlayerScore();
|
||||
pod->lastScoreHostID = (int) message->GetPlayerHostID();
|
||||
pod->haveScore = True;
|
||||
}
|
||||
// the rest of the telemetry (IDs 2-4, 6) skips through
|
||||
}
|
||||
else if ((int) header->clientID == (int) NetworkClient::NetworkManagerClientID)
|
||||
{
|
||||
@@ -348,10 +412,311 @@ namespace
|
||||
DEBUG_STREAM << "LocalConsole: stopping local pod\n" << std::flush;
|
||||
InterlockedExchange(&gMissionRunning, 0);
|
||||
Application::StopMissionMessage message(0);
|
||||
application->Dispatch(&message);
|
||||
DeliverLocal(
|
||||
NetworkClient::ApplicationClientID,
|
||||
&message,
|
||||
(int) message.messageLength
|
||||
);
|
||||
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 commit board: the pod bay ritual, on screen.
|
||||
//
|
||||
// In the pod bay the mission is COMMITTED first - every pod preps
|
||||
// up to, but not past, the launch - and then the operator presses
|
||||
// the launch button. The prep window is not dead time: the MFDs
|
||||
// and map buttons are alive before the mission starts, so pilots
|
||||
// use it to set maps and presets, and the wait is part of the
|
||||
// game's social fabric.
|
||||
//
|
||||
// This is that ritual for a lobby race. The host's menu button
|
||||
// says COMMIT; committing marshals everyone exactly as before, but
|
||||
// where the console used to fire RunMission the instant all pods
|
||||
// staged, it now arms a LAUNCH button on a small status board and
|
||||
// waits for the operator. The board lists every pod and what it is
|
||||
// doing - connecting, loading, READY - so the host can see who the
|
||||
// room is waiting on, and it gets out of the way the moment the
|
||||
// mission drops.
|
||||
//
|
||||
// Game thread throughout: created, painted, clicked and destroyed
|
||||
// inside ConsoleTick, so the game's own message pump (already
|
||||
// alive - that is why the MFDs work) delivers its input, and no
|
||||
// state crosses a thread. WS_EX_NOACTIVATE keeps the game window
|
||||
// focused: PadRIO controls answer only while it is, and a launch
|
||||
// click must not cost the host their controls.
|
||||
//---------------------------------------------------------------
|
||||
|
||||
HWND gStatusWindow = NULL;
|
||||
Logical gLaunchArmed = False; // everyone staged: button lit
|
||||
Logical gLaunchRequested = False; // the operator pressed it
|
||||
RECT gLaunchRect; // client coords, hit-tested
|
||||
int gShownStates[maxRemotePods + 2];
|
||||
|
||||
const COLORREF kBoardGreen = RGB(64, 255, 64);
|
||||
const COLORREF kBoardGreenDim = RGB(24, 140, 24);
|
||||
|
||||
const char *StateWord(int state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case -1: return "connecting";
|
||||
case Application::WaitingForEgg: return "waiting for egg";
|
||||
case Application::CreatingMission:
|
||||
case Application::LoadingMission: return "loading";
|
||||
case Application::WaitingForLaunch: return "READY";
|
||||
case Application::LaunchingMission:
|
||||
case Application::RunningMission: return "running";
|
||||
default: return "starting";
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK StatusBoardWndProc(
|
||||
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_MOUSEACTIVATE:
|
||||
// take the click, leave the focus with the game
|
||||
return MA_NOACTIVATE;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_LBUTTONDOWN:
|
||||
{
|
||||
int x = (int)(short) LOWORD(lParam);
|
||||
int y = (int)(short) HIWORD(lParam);
|
||||
|
||||
if (gLaunchArmed &&
|
||||
x >= gLaunchRect.left && x < gLaunchRect.right &&
|
||||
y >= gLaunchRect.top && y < gLaunchRect.bottom)
|
||||
{
|
||||
gLaunchRequested = True;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hwnd, &ps);
|
||||
RECT client;
|
||||
GetClientRect(hwnd, &client);
|
||||
|
||||
HDC mem = CreateCompatibleDC(hdc);
|
||||
HBITMAP surface = CreateCompatibleBitmap(
|
||||
hdc, client.right, client.bottom);
|
||||
HGDIOBJ old_surface = SelectObject(mem, surface);
|
||||
|
||||
FillRect(mem, &client,
|
||||
(HBRUSH) GetStockObject(BLACK_BRUSH));
|
||||
HBRUSH frame = CreateSolidBrush(kBoardGreenDim);
|
||||
FrameRect(mem, &client, frame);
|
||||
DeleteObject(frame);
|
||||
|
||||
HFONT font = CreateFontA(-14, 0, 0, 0, FW_NORMAL,
|
||||
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
|
||||
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
||||
DEFAULT_PITCH | FF_MODERN, "Consolas");
|
||||
HGDIOBJ old_font = SelectObject(mem, font);
|
||||
SetBkMode(mem, TRANSPARENT);
|
||||
|
||||
int row_h = 20;
|
||||
RECT line;
|
||||
line.left = 10;
|
||||
line.right = client.right - 10;
|
||||
line.top = 8;
|
||||
line.bottom = line.top + row_h;
|
||||
|
||||
SetTextColor(mem, kBoardGreen);
|
||||
DrawTextA(mem, "MISSION COMMITTED", -1, &line,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
line.top += row_h + 4;
|
||||
line.bottom += row_h + 4;
|
||||
|
||||
//
|
||||
// One row per pod: the host first, then the remotes in
|
||||
// [pilots] order. READY rows bright, the rest dim, so
|
||||
// who the room is waiting on reads at a glance.
|
||||
//
|
||||
// Every state on this board comes from gShownStates -
|
||||
// the values the TICK stored when it decided whether to
|
||||
// arm - and nothing is re-read at paint time. The first
|
||||
// version re-read application->GetApplicationState()
|
||||
// here, and the host's own row said 'connecting' under
|
||||
// a lit LAUNCH button: two reads of one fact from two
|
||||
// places, disagreeing - the exact mistake behind five
|
||||
// broken instruments in one night of this project. One
|
||||
// frame of reference: the tick decides, the paint
|
||||
// repeats what it decided.
|
||||
//
|
||||
char text[96];
|
||||
|
||||
sprintf(text, "%-14s %s",
|
||||
(gPilotNameCount > 0) ? gPilotNames[0] : "HOST",
|
||||
StateWord(gShownStates[0]));
|
||||
SetTextColor(mem,
|
||||
(gShownStates[0] == Application::WaitingForLaunch)
|
||||
? kBoardGreen : kBoardGreenDim);
|
||||
DrawTextA(mem, text, -1, &line,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
line.top += row_h;
|
||||
line.bottom += row_h;
|
||||
|
||||
const char *name = (i + 1 < gPilotNameCount)
|
||||
? gPilotNames[i + 1] : gRemotePods[i].address;
|
||||
|
||||
sprintf(text, "%-14s %s", name,
|
||||
StateWord(gShownStates[i + 1]));
|
||||
SetTextColor(mem,
|
||||
(gShownStates[i + 1] == Application::WaitingForLaunch)
|
||||
? kBoardGreen : kBoardGreenDim);
|
||||
DrawTextA(mem, text, -1, &line,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
//
|
||||
// The operator's button. Lit only when every system is
|
||||
// ready - the same condition that used to fire the run
|
||||
// automatically.
|
||||
//
|
||||
gLaunchRect.left = 10;
|
||||
gLaunchRect.right = client.right - 10;
|
||||
gLaunchRect.bottom = client.bottom - 8;
|
||||
gLaunchRect.top = gLaunchRect.bottom - 26;
|
||||
|
||||
HBRUSH button = CreateSolidBrush(
|
||||
gLaunchArmed ? kBoardGreen : kBoardGreenDim);
|
||||
FrameRect(mem, &gLaunchRect, button);
|
||||
DeleteObject(button);
|
||||
SetTextColor(mem,
|
||||
gLaunchArmed ? kBoardGreen : kBoardGreenDim);
|
||||
DrawTextA(mem,
|
||||
gLaunchArmed ? "L A U N C H" : "waiting for pods...",
|
||||
-1, &gLaunchRect,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
BitBlt(hdc, 0, 0, client.right, client.bottom,
|
||||
mem, 0, 0, SRCCOPY);
|
||||
SelectObject(mem, old_font);
|
||||
DeleteObject(font);
|
||||
SelectObject(mem, old_surface);
|
||||
DeleteObject(surface);
|
||||
DeleteDC(mem);
|
||||
EndPaint(hwnd, &ps);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcA(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void CreateStatusBoard()
|
||||
{
|
||||
static Logical class_registered = False;
|
||||
|
||||
if (!class_registered)
|
||||
{
|
||||
class_registered = True;
|
||||
WNDCLASSA window_class;
|
||||
memset(&window_class, 0, sizeof(window_class));
|
||||
window_class.lpfnWndProc = StatusBoardWndProc;
|
||||
window_class.hInstance = GetModuleHandleA(NULL);
|
||||
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
||||
window_class.lpszClassName = "RPCommitBoard";
|
||||
RegisterClassA(&window_class);
|
||||
}
|
||||
|
||||
int height = 8 + 24 + (gRemotePodCount + 1) * 20 + 12 + 26 + 8;
|
||||
int width = 280;
|
||||
|
||||
//
|
||||
// Top-right of the game window, inset a little - over the 3D
|
||||
// view, clear of the instrument panes along the edges.
|
||||
//
|
||||
RECT game;
|
||||
GetWindowRect(ghWnd, &game);
|
||||
|
||||
gStatusWindow = CreateWindowExA(
|
||||
WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
||||
"RPCommitBoard", "", WS_POPUP,
|
||||
game.right - width - 48, game.top + 64,
|
||||
width, height,
|
||||
ghWnd, NULL, GetModuleHandleA(NULL), NULL);
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
ShowWindow(gStatusWindow, SW_SHOWNOACTIVATE);
|
||||
}
|
||||
for (int i = 0; i < maxRemotePods + 2; ++i)
|
||||
{
|
||||
gShownStates[i] = -999;
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyStatusBoard()
|
||||
{
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
DestroyWindow(gStatusWindow);
|
||||
gStatusWindow = NULL;
|
||||
}
|
||||
gLaunchArmed = False;
|
||||
gLaunchRequested = False;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The game-thread tick: state reporting + engine-safe execution
|
||||
//---------------------------------------------------------------
|
||||
@@ -371,6 +736,32 @@ namespace
|
||||
switch (gPhase)
|
||||
{
|
||||
case PhaseWaiting:
|
||||
//
|
||||
// A mission can die before it ever runs - Alt+Q during the
|
||||
// commit hold, or while loading. The console only learned a
|
||||
// mission was over from PhaseRunning, so an abort during prep
|
||||
// left it in PhaseWaiting forever, MissionCompleted() answered
|
||||
// False, and WinMain's single-binary loop fell out to the
|
||||
// DESKTOP instead of returning to the setup screen. The commit
|
||||
// hold makes the prep window somewhere players actually stand,
|
||||
// so the hole finally had traffic. A prep death counts as a
|
||||
// completed mission now: back to the menu, lobby intact.
|
||||
//
|
||||
if (state == Application::EndingMission ||
|
||||
state == Application::StoppingMission ||
|
||||
state == Application::AbortingMission)
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: mission ended during prep - "
|
||||
<< "back to the menu\n" << std::flush;
|
||||
InterlockedExchange(&gMissionRunning, 0);
|
||||
gPhase = PhaseStopped;
|
||||
DestroyStatusBoard();
|
||||
if (gNetworkRace)
|
||||
{
|
||||
DisconnectRemotes();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (gNetworkRace)
|
||||
{
|
||||
MarshalRemotes();
|
||||
@@ -392,28 +783,104 @@ namespace
|
||||
}
|
||||
|
||||
//
|
||||
// Everyone staged: launch the race everywhere
|
||||
// The commit board, from the moment there is anything to
|
||||
// show. It repaints only when a state actually changes -
|
||||
// this ticks every frame.
|
||||
//
|
||||
if (gRemotePodCount > 0 && !gRunSent)
|
||||
{
|
||||
if (gStatusWindow == NULL)
|
||||
{
|
||||
CreateStatusBoard();
|
||||
}
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
Logical changed = (gShownStates[0] != state);
|
||||
|
||||
gShownStates[0] = state;
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
if (gShownStates[i + 1] != gRemotePods[i].state)
|
||||
{
|
||||
gShownStates[i + 1] = gRemotePods[i].state;
|
||||
changed = True;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(gStatusWindow, NULL, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Everyone staged: ARM. The console used to fire the run
|
||||
// itself right here; the run belongs to the operator now,
|
||||
// pod bay fashion, and the prep hold is the point - the
|
||||
// MFDs and map buttons are alive, so pilots set maps and
|
||||
// presets before the drop.
|
||||
//
|
||||
if (!gRunSent &&
|
||||
state == Application::WaitingForLaunch &&
|
||||
AllRemotesInState(Application::WaitingForLaunch))
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: all pods staged - RUN\n" << std::flush;
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
if (!gLaunchArmed)
|
||||
{
|
||||
Application::RunMissionMessage run;
|
||||
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
|
||||
&run, (int) run.messageLength);
|
||||
gLaunchArmed = True;
|
||||
DEBUG_STREAM << "LocalConsole: all pods staged - "
|
||||
<< "committed, LAUNCH is the operator's\n"
|
||||
<< std::flush;
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
InvalidateRect(gStatusWindow, NULL, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// A solo commit (no remote pods) launches itself: with
|
||||
// nobody to wait for there is no board, and the old
|
||||
// instant start is what a lone pilot expects.
|
||||
//
|
||||
if (gLaunchRequested || gRemotePodCount == 0)
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: RUN\n" << std::flush;
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
Application::RunMissionMessage run;
|
||||
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
|
||||
&run, (int) run.messageLength);
|
||||
}
|
||||
Application::RunMissionMessage local_run;
|
||||
DeliverLocal(
|
||||
NetworkClient::ApplicationClientID,
|
||||
&local_run,
|
||||
(int) local_run.messageLength
|
||||
);
|
||||
gRunSent = True;
|
||||
|
||||
// out of the way: the host has a race to fly
|
||||
DestroyStatusBoard();
|
||||
}
|
||||
}
|
||||
else if (gLaunchArmed && !gRunSent)
|
||||
{
|
||||
//
|
||||
// Somebody fell back out of readiness (a reload, a
|
||||
// drop). Disarm rather than launching a room that is
|
||||
// no longer whole.
|
||||
//
|
||||
gLaunchArmed = False;
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
InvalidateRect(gStatusWindow, NULL, FALSE);
|
||||
}
|
||||
Application::RunMissionMessage local_run;
|
||||
application->Dispatch(&local_run);
|
||||
gRunSent = True;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == Application::RunningMission)
|
||||
{
|
||||
gPhase = PhaseRunning;
|
||||
DestroyStatusBoard(); // however the run began
|
||||
gWatchedApp = application;
|
||||
gResultCount = 0;
|
||||
InterlockedExchange(&gRunStartTick, (LONG) GetTickCount());
|
||||
@@ -439,6 +906,7 @@ namespace
|
||||
// mission ended some other way (pilot exit etc.)
|
||||
InterlockedExchange(&gMissionRunning, 0);
|
||||
gPhase = PhaseStopped;
|
||||
DestroyStatusBoard();
|
||||
DisconnectRemotes();
|
||||
}
|
||||
else if (gStopRequested)
|
||||
@@ -469,6 +937,27 @@ namespace
|
||||
else if (AllRemotesScored() ||
|
||||
(LONG)(GetTickCount() - gRemoteStopTick) >= 5000)
|
||||
{
|
||||
//
|
||||
// Stand in for anyone who is not here to answer. A pod
|
||||
// that left mid-race - Alt+Q, a crash, a dropped link -
|
||||
// never sends EndMission, and used to vanish from the
|
||||
// score sheet as though it had not raced at all. Its
|
||||
// last telemetry score is the score it left with.
|
||||
//
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
if (!gRemotePods[i].scored && gRemotePods[i].haveScore)
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: "
|
||||
<< gRemotePods[i].address
|
||||
<< " never reported - keeping its last"
|
||||
<< " telemetry score\n" << std::flush;
|
||||
CollectFinalScore(
|
||||
gRemotePods[i].lastScoreHostID,
|
||||
gRemotePods[i].lastScore);
|
||||
gRemotePods[i].scored = True;
|
||||
}
|
||||
}
|
||||
DispatchLocalStop();
|
||||
DisconnectRemotes();
|
||||
}
|
||||
@@ -518,10 +1007,15 @@ namespace
|
||||
gRunSent = False;
|
||||
gRemoteStopsSent = False;
|
||||
gLocalEggFed = False;
|
||||
DestroyStatusBoard(); // no board survives into a new race
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -568,6 +1062,13 @@ Logical
|
||||
strncpy(gEggPath, egg_path, sizeof(gEggPath) - 1);
|
||||
gEggPath[sizeof(gEggPath) - 1] = '\0';
|
||||
|
||||
//
|
||||
// The recording needs this too. A spool says what moved; the egg says
|
||||
// what it moved through, and the console is where the egg's name is
|
||||
// actually known.
|
||||
//
|
||||
SpoolRecorder_Get()->SetEggPath(gEggPath);
|
||||
|
||||
//
|
||||
// The wire image of the egg: file newlines become NULs, exactly
|
||||
// what the arcade console sent (RPMission.ToEggFileMessages)
|
||||
@@ -634,6 +1135,12 @@ Logical
|
||||
// booting; runs before the engine block so nothing is waiting.
|
||||
//
|
||||
NetTransport_Get()->Startup();
|
||||
|
||||
//
|
||||
// An escape pressed during the last race must not cancel this one.
|
||||
//
|
||||
NetTransport_ClearWaitCancel();
|
||||
|
||||
const char *cursor = remote_pod_list;
|
||||
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
#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"
|
||||
"# Read the keyboard, pad and stick only while the game is the window in\n"
|
||||
"# front. The pod was the only thing running on its cabinet, so the\n"
|
||||
"# virtual RIO reads the key state directly rather than waiting on the\n"
|
||||
"# message pump - which means it reads it whatever is in front, and\n"
|
||||
"# switching to another window to type flies the pod around while you\n"
|
||||
"# type in it.\n"
|
||||
"# 1 controls go neutral when you switch away (default)\n"
|
||||
"# 0 read them regardless, as earlier builds did\n"
|
||||
"# Any window of the game counts as the game, so clicking an MFD pane or\n"
|
||||
"# the plasma glass does not drop your controls. Real RIO cockpit\n"
|
||||
"# hardware is unaffected either way - this is the keyboard, pad and\n"
|
||||
"# joystick path only.\n"
|
||||
"RP412INPUTFOCUS=1\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 same pane as a LIVE CAM, which is a different job: no pod, so no\n"
|
||||
"# instrument MFDs beside it, the map lies down landscape instead of the\n"
|
||||
"# pod's portrait mounting, and dead centre is the worst place to put a\n"
|
||||
"# panel on a shot. It therefore has its own position and size, taking\n"
|
||||
"# the same values as L4RADARPOS / L4RADARSCALE above and defaulting to\n"
|
||||
"# the bottom-left corner.\n"
|
||||
"#\n"
|
||||
"# Two settings rather than one on purpose: the host picks Racer or Live\n"
|
||||
"# Cam from the setup screen, so switching role must not mean editing\n"
|
||||
"# this file. Each role remembers its own.\n"
|
||||
"L4MAPPOS=LEFT\n"
|
||||
"L4MAPSCALE=100\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"
|
||||
"# How long the podium holds before the results screen, in seconds\n"
|
||||
"# (1-60). The stand is worth a look but eleven seconds of one parked\n"
|
||||
"# pod is a long look in single player. With RP412PODIUM=0 there is no\n"
|
||||
"# hold at all - straight to the results, as that option promises.\n"
|
||||
"#RP412PODIUMHOLD=11\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"
|
||||
"# The physics step, in steps per second. 50 is the default: the\n"
|
||||
"# simulation advances in fixed 20 ms steps whatever the display does,\n"
|
||||
"# so the SAME race plays out on every machine - measured bit-identical\n"
|
||||
"# at 30, 60 and 144 fps, through a scripted lap with a crash, a burn\n"
|
||||
"# and two respawns. A pod at 30 fps and a pod at 144 are finally in\n"
|
||||
"# the same gravity.\n"
|
||||
"#\n"
|
||||
"# The alternatives, all exact on the engine's millisecond clock:\n"
|
||||
"# 25 the arcade pods' rate - the step the original handling was\n"
|
||||
"# tuned against, coarsest contact response\n"
|
||||
"# 100 the smoothest contact and terrain response\n"
|
||||
"# 0 the original frame-coupled physics, where the frame rate is\n"
|
||||
"# part of the simulation - kept for comparison\n"
|
||||
"# (Rates that do not divide 1000 evenly - 60, say - quietly run at the\n"
|
||||
"# nearest millisecond step instead; the log says so if you try one.)\n"
|
||||
"#\n"
|
||||
"# What to feel for between rates: hover bounce, wall hits, how the pod\n"
|
||||
"# takes the crest of a hill. Report the rate with the verdict.\n"
|
||||
"RP412PHYSICSHZ=50\n"
|
||||
"\n"
|
||||
"# 0 = draw the simulation's exact stepped positions, as builds before\n"
|
||||
"# this one did. On by default, and it is what lets the frame rate be\n"
|
||||
"# anything you like.\n"
|
||||
"#\n"
|
||||
"# With a fixed physics step the drawn position only changes at the step\n"
|
||||
"# rate, so any other frame rate holds each position for a whole number\n"
|
||||
"# of frames and the motion visibly steps - worse the FASTER the machine,\n"
|
||||
"# since 240 fps holds each one for nearly five frames. Drawing therefore\n"
|
||||
"# blends across the step it is inside. The simulation is not touched: the\n"
|
||||
"# same script drives the same race bit for bit either way, which is what\n"
|
||||
"# RP412PHYSTRACE below is for.\n"
|
||||
"#\n"
|
||||
"# The cost is that the picture trails the simulation by up to one step\n"
|
||||
"# (20 ms at 50 Hz). Turn it off to see the stepping it removes, or if you\n"
|
||||
"# would rather have those 20 ms than smooth motion.\n"
|
||||
"#RP412INTERP=0\n"
|
||||
"\n"
|
||||
"# Set 0 to go back to the original guess at when the next update for\n"
|
||||
"# another player's pod will arrive. On by default.\n"
|
||||
"#\n"
|
||||
"# Between updates, another player's pod is moved by dead reckoning: it\n"
|
||||
"# advances toward where it is projected to be by a fraction of the gap\n"
|
||||
"# each step, and that fraction is decided by when the next update is\n"
|
||||
"# expected. The original code expected the next gap to match the one\n"
|
||||
"# before it, which held on a LAN where the gaps were all alike. Over the\n"
|
||||
"# internet one late packet doubles the expected gap, the fraction\n"
|
||||
"# collapses, the pod barely moves for a step and then catches up - a\n"
|
||||
"# tick roughly once a second on a live connection.\n"
|
||||
"#\n"
|
||||
"# The default instead takes the middle value of the last eight gaps, so\n"
|
||||
"# one straggler is ignored while a real change in the rate is still\n"
|
||||
"# followed. This changes how other players' pods MOVE, not merely how\n"
|
||||
"# they are drawn, so it affects collisions with them too - keep it the\n"
|
||||
"# same on every machine in a race.\n"
|
||||
"#RP412NETPREDICT=0\n"
|
||||
"\n"
|
||||
"# How long one background pass may spend drawing cockpit gauges, in\n"
|
||||
"# milliseconds. The gauges and the MFD/map displays are redrawn in the\n"
|
||||
"# time left over after the 3D view; on a big, busy map there is none\n"
|
||||
"# left, and at the original one-gauge-per-pass the map and the countdown\n"
|
||||
"# clock could sit frozen for seconds at a time - until something (a\n"
|
||||
"# death, say) lightened the 3D view enough for the backlog to drain.\n"
|
||||
"# Working to a slice ties the refresh rate to elapsed time instead. Set\n"
|
||||
"# 0 for the old behaviour; raise it to favour the displays over frame\n"
|
||||
"# rate.\n"
|
||||
"RP412GAUGESLICE=2\n"
|
||||
"\n"
|
||||
"# How many times the map redraws per turn of the gauge rate wheel, 1 to\n"
|
||||
"# 16. The renderer gives each gauge one step of a sixteen-step wheel and\n"
|
||||
"# a gauge redraws only on its own step, so a map left on one step waits a\n"
|
||||
"# whole turn. 16 = redraw on every step (default); 1 = whatever the gauge\n"
|
||||
"# data asks for, which is how it behaved before this existed. Each step\n"
|
||||
"# costs one map redraw against a pass that runs ninety gauges.\n"
|
||||
"RP412MAPRATE=16\n"
|
||||
"\n"
|
||||
"# 1 = log how many times a second every cockpit display is actually\n"
|
||||
"# refreshed, to rpl4.log. Watching the screen cannot tell a display that\n"
|
||||
"# has stopped refreshing from one whose picture simply is not changing.\n"
|
||||
"#RP412GAUGEDIAG=1\n"
|
||||
"\n"
|
||||
"# 0 = light the on-screen cockpit buttons on the same slow cadence the\n"
|
||||
"# arcade pod's serial hardware used. The lamp state is filled once per\n"
|
||||
"# gauge cycle, so under the load described above the lit buttons froze\n"
|
||||
"# and flashing ones stalled while the 3D view stayed perfectly smooth.\n"
|
||||
"# On by default: the buttons are refreshed every frame instead. Ignored\n"
|
||||
"# when real RIO hardware is selected - the pod keeps its own cadence.\n"
|
||||
"#RP412LAMPSWEEP=0\n"
|
||||
"\n"
|
||||
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs steam_api.dll, the\n"
|
||||
"# Steam client running, and steam_appid.txt beside the exe; missing any\n"
|
||||
"# of them logs the reason and falls back to plain TCP - nothing here can\n"
|
||||
"# stop the game starting, and steam_api.dll being absent altogether is\n"
|
||||
"# fine. 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"
|
||||
"# How long each connection attempt to another machine may take, in\n"
|
||||
"# seconds. 2 to 300, default 20, and up to three attempts are made.\n"
|
||||
"#\n"
|
||||
"# Per attempt matters. Steam tries a direct path first, and behind some\n"
|
||||
"# routers that burns ten seconds and fails; the second attempt comes up\n"
|
||||
"# through Valve's relay and succeeds - if it is given time. The first\n"
|
||||
"# playtest with six players showed a shared budget cutting that second\n"
|
||||
"# attempt off mid-connect, and races could not assemble because the\n"
|
||||
"# mesh needs every machine to reach every other. Three attempts of\n"
|
||||
"# twenty seconds each connects the awkward router in about half a\n"
|
||||
"# minute, and gives up on a truly unreachable one inside a minute.\n"
|
||||
"#\n"
|
||||
"# The window stays alive throughout, the title bar names the peer and\n"
|
||||
"# counts down each attempt, and ESC gives up immediately.\n"
|
||||
"RP412CONNECTWAIT=20\n"
|
||||
"\n"
|
||||
"# How much memory to set aside for a recording, in megabytes. 1 to 512,\n"
|
||||
"# default 100.\n"
|
||||
"#\n"
|
||||
"# RECORDING on the setup screen keeps a spool of the race in SPOOLS\\,\n"
|
||||
"# named for the time it finished, and copies it to last.spl. It records\n"
|
||||
"# what THIS machine received, so a Live Cam host - which watches rather\n"
|
||||
"# than races, and therefore hears every pod over the wire - keeps the\n"
|
||||
"# most complete account of a race there is.\n"
|
||||
"#\n"
|
||||
"# A full grid sends on the order of 17KB a second, so a hundred\n"
|
||||
"# megabytes is around an hour and a half. If a race outlasts the buffer\n"
|
||||
"# the recording simply stops and the race carries on - it is never worth\n"
|
||||
"# interrupting a race to protect a recording of it - and the log says so.\n"
|
||||
"RP412RECORDSIZE=100\n"
|
||||
"\n"
|
||||
"# ---- Test harness -----------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# The knobs that make a run repeatable and measurable. All are off\n"
|
||||
"# unless set and cost nothing when off; none belongs in a real race.\n"
|
||||
"# They exist so a claim about the game can be tested instead of argued.\n"
|
||||
"\n"
|
||||
"# Dump the gauge profile to rpl4.log every N seconds: every cockpit\n"
|
||||
"# display with its rate mask and tier, how often it ran and what it\n"
|
||||
"# cost. This is the engine's own ProfileReport, which was only ever\n"
|
||||
"# reachable from the arcade RIO mapper's F11 before.\n"
|
||||
"#RP412GAUGEPROFILE=8\n"
|
||||
"\n"
|
||||
"# 1 = log renderable construction and what each frame is made of, so a\n"
|
||||
"# model that never got built can be told from one that is simply out\n"
|
||||
"# of shot.\n"
|
||||
"#RP412RENDERDIAG=1\n"
|
||||
"\n"
|
||||
"# 1 = trace the player pod's position to rpl4.log on the SIMULATION's\n"
|
||||
"# own clock, stopping the pod dead at the green light so every run\n"
|
||||
"# starts from rest. Two runs of the same race then compare sample for\n"
|
||||
"# sample - this is the instrument that proved RP412PHYSICSHZ plays the\n"
|
||||
"# same race at every frame rate, bit for bit.\n"
|
||||
"#RP412PHYSTRACE=1\n"
|
||||
"\n"
|
||||
"# Try this drop zone first at spawn instead of a random pick. The pick\n"
|
||||
"# is seeded by RANDOM=, but a seed only repeats a run if the same\n"
|
||||
"# NUMBER of draws comes before the pick, and that count rides on load\n"
|
||||
"# timing - so pin the pad too, or two 'identical' runs start over\n"
|
||||
"# different ground. Falls back to the random walk if the zone is\n"
|
||||
"# taken, so it cannot wedge.\n"
|
||||
"#RP412SPAWNZONE=3\n"
|
||||
"\n"
|
||||
"# Drive the pod from a timeline file instead of the controls - the\n"
|
||||
"# same lap, exactly, every run. One row per change, held until the\n"
|
||||
"# next row: time-in-seconds throttle stickX stickY pedals, values\n"
|
||||
"# 0..1 for throttle and -1..1 elsewhere, # for comments. Times are\n"
|
||||
"# SIMULATION seconds from the green light, so with RP412PHYSICSHZ set\n"
|
||||
"# the same script is the same race at any frame rate - this is how\n"
|
||||
"# driving, not just settling, gets verified bit-identical.\n"
|
||||
"#RP412INPUTSCRIPT=testlap.txt\n"
|
||||
"\n"
|
||||
"# 1 = log the XInput-class controllers the generic-joystick scan skips\n"
|
||||
"# (attached DirectInput devices are always logged). For debugging a pad\n"
|
||||
"# that answers twice or a stick that does not answer at all.\n"
|
||||
"#RP412JOYLOG=1\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"
|
||||
"# The cabinets ran the game at unity and did all their volume and tone\n"
|
||||
"# shaping outside it, in an amplifier and a 3-way crossover. You almost\n"
|
||||
"# certainly have neither, so these two stand in for them. Both default\n"
|
||||
"# to leaving the mix exactly as the pod played it.\n"
|
||||
"\n"
|
||||
"# Master volume, 0.0 to 2.0, the amplifier's knob. 1.0 is unity. The\n"
|
||||
"# sound effects now carry the pitch, layering and dynamics the original\n"
|
||||
"# AWE32 soundbanks ask for, which is a good deal livelier than earlier\n"
|
||||
"# 4.12 builds - lower this if the whole thing sits too hot.\n"
|
||||
"#\n"
|
||||
"# PageUp and PageDown change it while you play, in steps of 0.05, and\n"
|
||||
"# whatever you leave it on is written to volume.cfg beside the exe and\n"
|
||||
"# used from then on - so this line only decides where a machine that has\n"
|
||||
"# never been touched starts out. Delete volume.cfg to come back here.\n"
|
||||
"#RP412AUDIOVOLUME=0.8\n"
|
||||
"\n"
|
||||
"# Bass trim, 0.0 to 1.0, the crossover's low band. 1.0 is the low end\n"
|
||||
"# exactly as authored. The soundbanks put real weight under collisions,\n"
|
||||
"# engines and explosions - deep layers earlier builds played at the\n"
|
||||
"# wrong rate, so they barely sounded at all. Lower this to pull that\n"
|
||||
"# back; it eases in below 22kHz of playback rate and reaches full cut\n"
|
||||
"# on the deepest layers, leaving the mid and top alone.\n"
|
||||
"#\n"
|
||||
"# Home and End change it while you play, in steps of 0.05, and what you\n"
|
||||
"# leave it on is written to bass.cfg beside the exe and used from then\n"
|
||||
"# on - so this line only decides where an untouched machine starts.\n"
|
||||
"# Delete bass.cfg to come back here.\n"
|
||||
"#RP412AUDIOBASS=0.7\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"
|
||||
"# Who transforms the vertices: hw hands it to the GPU, sw does it on the\n"
|
||||
"# CPU as this engine always has. There was no hardware to hand it to when\n"
|
||||
"# it was written; there is now, and it is not close - on a busy track the\n"
|
||||
"# 3D foreground drops from about 17ms a frame to under half a\n"
|
||||
"# millisecond, and all of that time goes back to the cockpit displays,\n"
|
||||
"# which is what makes the map, the clock and the gauges live rather than\n"
|
||||
"# updating every few seconds.\n"
|
||||
"# Falls back to sw by itself if the adapter has no hardware T&L. sw is\n"
|
||||
"# the way back if a driver's fixed-function lighting or fog looks wrong -\n"
|
||||
"# the two are not bit-identical.\n"
|
||||
"RP412VERTEXPROC=hw\n"
|
||||
"\n"
|
||||
"# 0 = present without waiting for the panel's retrace. Costs tearing,\n"
|
||||
"# buys latency. Measured to make very little difference to the frame\n"
|
||||
"# budget here - the frame is full of work, not waiting - so this is a\n"
|
||||
"# preference rather than a fix.\n"
|
||||
"#RP412VSYNC=0\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. Two of them do something in this build:\n"
|
||||
"# Alt+W wireframe. The sky is not drawn and the view clears to\n"
|
||||
"# black, so the edges stand on their own; the gunsight and\n"
|
||||
"# cam-ship HUD stay solid.\n"
|
||||
"# Alt+E write the event queue to the log.\n"
|
||||
"# The others are stubs the 2007 DPL->Direct3D port left behind and do\n"
|
||||
"# nothing at all. They are named here so a dead key is not mistaken for\n"
|
||||
"# a broken one: Alt+V predator vision, Alt+F frame dump, Alt+/ perf\n"
|
||||
"# stats and Alt+K free memory are silent; Alt+R dither pattern and\n"
|
||||
"# Alt+P eyepoint position at least say so in the log.\n"
|
||||
"#\n"
|
||||
"# All of them arrive through the engine keyboard handler, so L4CONTROLS\n"
|
||||
"# has to include KEYBOARD - the shipped stack does. That handler reads\n"
|
||||
"# one key per frame off the front of the message queue and so drops\n"
|
||||
"# presses; if a key seems dead, press it again before believing it.\n"
|
||||
"# 0 or unset = off. (Alt+Q, the mission abort, is always live.)\n"
|
||||
"#RP412DEVKEYS=1\n"
|
||||
"\n"
|
||||
"# Nonzero traces the Live Cam bring-up: the egg's host type and game\n"
|
||||
"# model, the registry choosing a camera director, the director making\n"
|
||||
"# its camera ship, and the launch handshake. A camera station is picked\n"
|
||||
"# purely by egg data (hostType=1 and vehicle=camera on that host's\n"
|
||||
"# entry), so without this there is nothing in the log to say how far it\n"
|
||||
"# got. Prints once a second while a station sits unlaunched.\n"
|
||||
"#RP412CAMLOG=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];
|
||||
|
||||
//
|
||||
// Keys already applied, so a second copy of one can be reported. The
|
||||
// file is applied line by line, so a later line silently beats an
|
||||
// earlier one - which is a genuinely expensive way to lose an evening:
|
||||
// a TARGETFPS added at the top of this file was overridden by the one
|
||||
// the template ships further down, and the test it was written for
|
||||
// looked like it had failed rather than never having run.
|
||||
//
|
||||
// Named rather than counted: knowing WHICH key and which lines is the
|
||||
// whole value.
|
||||
//
|
||||
char seen_keys[4096];
|
||||
int seen_length = 0;
|
||||
seen_keys[0] = '\0';
|
||||
|
||||
int line_number = 0;
|
||||
const char *cursor = text;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
++line_number;
|
||||
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;
|
||||
}
|
||||
//
|
||||
// Warn on a repeat before applying it, naming the key and both
|
||||
// lines. The later value is the one that survives, which is worth
|
||||
// stating outright rather than leaving to be deduced.
|
||||
//
|
||||
{
|
||||
char key[128];
|
||||
int key_length = 0;
|
||||
while (setting[key_length] != '\0' && setting[key_length] != '=' &&
|
||||
key_length < (int) sizeof(key) - 1)
|
||||
{
|
||||
key[key_length] = setting[key_length];
|
||||
++key_length;
|
||||
}
|
||||
key[key_length] = '\0';
|
||||
|
||||
// entries are stored as "KEY\tLINE\n"
|
||||
char needle[132];
|
||||
sprintf(needle, "\n%s\t", key);
|
||||
const char *found = (seen_length > 0) ? strstr(seen_keys, needle) : NULL;
|
||||
if (found != NULL)
|
||||
{
|
||||
DEBUG_STREAM << "Environ: " << key << " is set twice - line "
|
||||
<< atoi(found + strlen(needle)) << " and line " << line_number
|
||||
<< "; the LATER one wins\n" << std::flush;
|
||||
}
|
||||
else if (seen_length + key_length + 16 < (int) sizeof(seen_keys))
|
||||
{
|
||||
seen_length += sprintf(seen_keys + seen_length, "\n%s\t%d",
|
||||
key, line_number);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
+1380
-143
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,15 @@ int
|
||||
const char *
|
||||
RPL4FrontEnd_LastPilotNames();
|
||||
|
||||
//
|
||||
// The callsign the player typed on the setup screen (persisted in
|
||||
// pilot.cfg). The lobby publishes this as the member name: what somebody
|
||||
// writes in the box is what the room and the race should call them.
|
||||
// Empty, or still the untouched default, when they have never set one.
|
||||
//
|
||||
const char *
|
||||
RPL4FrontEnd_Callsign();
|
||||
|
||||
// How the last Run() ended: a plain race, hosting a network race (the
|
||||
// console marshals the other pods), or entering one as a member pod
|
||||
// (the lobby owner's console marshals us; no local egg, no console).
|
||||
@@ -110,6 +119,16 @@ void
|
||||
Logical
|
||||
RPL4FrontEnd_IsFootballSelected();
|
||||
|
||||
//
|
||||
// True when this player's setup menu has YOUR ROLE on Live Cam. Only the
|
||||
// HOST's pick is acted on - the host is the one that writes the egg - so
|
||||
// the lobby publishes this and then shows it against the owner's row
|
||||
// only. A member who picked it still races, which is what their row goes
|
||||
// on saying.
|
||||
//
|
||||
Logical
|
||||
RPL4FrontEnd_IsCameraRole();
|
||||
|
||||
// Hosted-race pilots fed by the Steam lobby: overrides the
|
||||
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
||||
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
||||
@@ -123,6 +142,11 @@ struct FEHostedPilot
|
||||
// football: the member's own picks, empty = assign one for them
|
||||
char team[32]; // team key ("Red/Pink", ...)
|
||||
char position[16]; // "runner" / "crusher" / "blocker"
|
||||
|
||||
// the member picked Live Cam: hostType=1, vehicle=camera in the egg.
|
||||
// Somebody still has to race - the egg builder strips every cam pick
|
||||
// if honouring them would leave nothing to point a camera at.
|
||||
Logical camera;
|
||||
};
|
||||
void
|
||||
RPL4FrontEnd_SetHostedPilots(
|
||||
|
||||
+333
-45
@@ -1,4 +1,5 @@
|
||||
#include "rpl4.h"
|
||||
|
||||
#pragma hdrstop
|
||||
|
||||
#define PRELOAD_ART
|
||||
@@ -1556,6 +1557,21 @@ void
|
||||
DrawNames(worldToView);
|
||||
localView.DetachRecorder();
|
||||
|
||||
//
|
||||
// RP412CAMLOG: the sweep reached the drawing phase at all. If the
|
||||
// counts above are non-zero and this never appears, the gauge
|
||||
// slice is not getting this display as far as phase 3.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
static int drew = 0;
|
||||
if (drew < 3)
|
||||
{
|
||||
++drew;
|
||||
DEBUG_STREAM << "CamLog: nav drew (phase 3 reached)\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
// deliberately falls through into default case
|
||||
|
||||
default:
|
||||
@@ -1630,6 +1646,26 @@ void
|
||||
Test_Tell("x= " << xMin << "..." << xMax << "\n");
|
||||
Test_Tell("y= " << yMin << "..." << yMax << "\n");
|
||||
Test_Tell("z= " << zMin << "..." << zMax << "\n");
|
||||
|
||||
//
|
||||
// RP412CAMLOG, on a clock rather than every sweep - this runs at the
|
||||
// gauge rate and would drown the log. Says whether the nav display has
|
||||
// a sane centre and scale, and repeats so the centre can be seen
|
||||
// tracking (or not tracking) as the camera moves.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
static Scalar next_bounds_say = 0.0f;
|
||||
if ((Scalar) Now() >= next_bounds_say)
|
||||
{
|
||||
next_bounds_say = ((Scalar) Now()) + 5.0f;
|
||||
DEBUG_STREAM << "CamLog: nav scale " << currentScale
|
||||
<< "m across, " << pixelsPerMeter << " px/m, centre "
|
||||
<< viewing_position.x << "," << viewing_position.z
|
||||
<< " bounds x " << xMin << ".." << xMax
|
||||
<< " z " << zMin << ".." << zMax << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1660,6 +1696,33 @@ void
|
||||
xMin, yMin, zMin,
|
||||
xMax, yMax, zMax
|
||||
);
|
||||
|
||||
//
|
||||
// RP412CAMLOG. The counts are the whole question: furniture with no
|
||||
// content means either nothing fell inside the bounds above, or the
|
||||
// drawing steps are not being reached. Reported for the first few
|
||||
// sweeps only - the lists are rebuilt at the gauge rate.
|
||||
//
|
||||
//
|
||||
// Sampled on a clock, NOT for the first N sweeps. The first sweeps run
|
||||
// while the mission is still coming up and nothing has registered
|
||||
// with the renderer yet, so they report zero on a station whose map
|
||||
// then works perfectly - which is exactly the false reading this trace
|
||||
// gave the first time it was written.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
static Scalar next_nav_say = 0.0f;
|
||||
if ((Scalar) Now() >= next_nav_say)
|
||||
{
|
||||
next_nav_say = ((Scalar) Now()) + 5.0f;
|
||||
|
||||
ChainIteratorOf<Entity*> statics(staticEntityList.GetInstanceList());
|
||||
ChainIteratorOf<Entity*> movers(movingEntityList.GetInstanceList());
|
||||
DEBUG_STREAM << "CamLog: nav in bounds - " << statics.GetSize()
|
||||
<< " static, " << movers.GetSize() << " moving\n" << std::flush;
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1951,7 +2014,75 @@ GPS::GPS(
|
||||
//-----------------------------------------------------------
|
||||
background = new Video8BitBuffered(width, height);
|
||||
Register_Object(background);
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------
|
||||
// RP412MAPRATE - how many of the sixteen rate steps the map redraws
|
||||
// on.
|
||||
//
|
||||
// The gauge renderer walks a 16-bit rate wheel: one bit per full
|
||||
// pass over every active gauge, shifted right each pass and reset at
|
||||
// the bottom (GAUGREND.cpp). A gauge redraws only on the step its
|
||||
// configured rate names, so a map on one bit redraws once per
|
||||
// SIXTEEN passes - and its update period is sixteen passes however
|
||||
// cheap the redraw is.
|
||||
//
|
||||
// That was fine when a pass was quick. Racing, the background loop
|
||||
// only gets what is left of the frame after the 3D, passes fall to
|
||||
// about five a second, and sixteen of them is over three seconds
|
||||
// between map updates - measured, on a 60 fps display with the 3D
|
||||
// perfectly smooth. Nothing is slow here; the map is just waiting
|
||||
// its turn on a wheel built for a machine that came round faster.
|
||||
//
|
||||
// Drawing on more of the steps costs one gauge's redraw per step,
|
||||
// against a pass that runs ninety of them. It does not make the
|
||||
// wheel turn faster - it stops the map needing a whole turn.
|
||||
//-----------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int updates = -1;
|
||||
if (updates < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412MAPRATE");
|
||||
updates = (setting != NULL) ? atoi(setting) : 16;
|
||||
// 1..16, and only the powers of two divide the wheel evenly
|
||||
if (updates > 16) updates = 16;
|
||||
if (updates < 1) updates = 1;
|
||||
}
|
||||
if (updates > 1)
|
||||
{
|
||||
GaugeRate mask = 0;
|
||||
for (int step = 0; step < 16; step += (16 / updates))
|
||||
{
|
||||
mask |= (GaugeRate)(0x8000 >> step);
|
||||
}
|
||||
//
|
||||
// QUALIFIED, both of them. This constructor's first
|
||||
// parameter is also called 'rate', so a bare assignment
|
||||
// here writes the PARAMETER and leaves the member holding
|
||||
// whatever the gauge data asked for - which is what it did,
|
||||
// silently, and cost a long hunt for a writer that did not
|
||||
// exist. oldRate is not shadowed, which is why it took the
|
||||
// value and the pair disagreed.
|
||||
//
|
||||
// Gauge::Disable(False) restores rate from oldRate when the
|
||||
// mode system enables a gauge, so both have to carry it or
|
||||
// the first activation puts the old rate back.
|
||||
//
|
||||
Gauge::rate = mask;
|
||||
Gauge::oldRate = mask;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
needsStaticUpdate = True;
|
||||
//
|
||||
// Nothing drawn yet, so there is no picture to add a placement to and
|
||||
// no scale it would be added at. The first pass builds both.
|
||||
//
|
||||
backgroundBuilt = False;
|
||||
builtMinX = builtMinY = builtMinZ = (Scalar) 0;
|
||||
builtMaxX = builtMaxY = builtMaxZ = (Scalar) 0;
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1989,6 +2120,95 @@ void
|
||||
|
||||
void
|
||||
GPS::NotifyOfNewInterestingEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
if (!entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
|
||||
{
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// A rebuild is already owed, or there is no picture to add to yet.
|
||||
//
|
||||
if (needsStaticUpdate || !backgroundBuilt)
|
||||
{
|
||||
needsStaticUpdate = True;
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Terrain arrives as you DRIVE - the interest system hands each piece
|
||||
// over as it comes into range, not all of it at load - and this used
|
||||
// to order a rebuild of the entire map for every one of them. A
|
||||
// rebuild redraws every placement on the track, so the cost of one
|
||||
// arriving piece was the whole track, and the map went seconds
|
||||
// between updates on the tracks that draw the most: Tour De Mars at
|
||||
// 351 placements and Ares' Armpits at 320, against 80-140 for a
|
||||
// typical one. It is also not interruptible - the gauge loop checks
|
||||
// its time slice BETWEEN gauges - so a rebuild stalled every other
|
||||
// display with it.
|
||||
//
|
||||
// But the bounds are what set the scale, and the scale is what the
|
||||
// whole cached picture was drawn at. An arrival that leaves the
|
||||
// bounds alone leaves every placement already on the map exactly
|
||||
// where it belongs, and the only thing missing from the picture is
|
||||
// the new one. Draw that, and nothing else.
|
||||
//
|
||||
// Only a piece that moves an EDGE of the track changes the scale, and
|
||||
// then the picture really is wrong everywhere and has to be redrawn.
|
||||
// That is rare, and it gets rarer as the track fills in.
|
||||
//
|
||||
// RPL4GaugeRenderer::NotifyOfNewInterestingEntity adds the entity to
|
||||
// staticEntities before chaining here, so these bounds already
|
||||
// account for the arrival being judged.
|
||||
//
|
||||
Scalar
|
||||
minX, minY, minZ,
|
||||
maxX, maxY, maxZ;
|
||||
|
||||
Check(renderer);
|
||||
renderer->GetStaticBounds(
|
||||
&minX, &minY, &minZ,
|
||||
&maxX, &maxY, &maxZ
|
||||
);
|
||||
|
||||
if (minX != builtMinX || minY != builtMinY || minZ != builtMinZ ||
|
||||
maxX != builtMaxX || maxY != builtMaxY || maxZ != builtMaxZ)
|
||||
{
|
||||
needsStaticUpdate = True;
|
||||
}
|
||||
else if (DrawStaticEntity(entity))
|
||||
{
|
||||
//
|
||||
// Only when something was actually drawn. Terrain without a
|
||||
// GaugeImage never reaches the map - on these tracks that is
|
||||
// most of it, 256 of Tour De Mars' 607 - and blitting the
|
||||
// background again for one of those is pure cost.
|
||||
//
|
||||
needsScreenUpdate = True;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// NotifyOfBecomingUninterestingEntity
|
||||
//#############################################################################
|
||||
//
|
||||
// The map showed only currently-interesting terrain before any of this,
|
||||
// and it still does. A placement cannot be un-drawn from a composited
|
||||
// picture, so a departure is the one case left that costs a full rebuild.
|
||||
//
|
||||
// Nothing used to listen for this at all: departures were swept up by the
|
||||
// rebuild that the very next ARRIVAL ordered, which is no longer ordered.
|
||||
// Without this the map would keep showing terrain that had gone until
|
||||
// something moved the bounds.
|
||||
//
|
||||
void
|
||||
GPS::NotifyOfBecomingUninterestingEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
@@ -2104,61 +2324,34 @@ void
|
||||
ChainIteratorOf<Entity*>
|
||||
i(staticEntityList.GetInstanceList());
|
||||
|
||||
Check(renderer);
|
||||
L4Warehouse
|
||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
||||
Check(warehouse);
|
||||
|
||||
Entity
|
||||
*entity;
|
||||
L4GaugeImage
|
||||
*gauge_image;
|
||||
|
||||
AffineMatrix
|
||||
worldToView,
|
||||
localToView;
|
||||
|
||||
Vector3D
|
||||
scaling_vector;
|
||||
//------------------------------------
|
||||
// Scale the display
|
||||
//------------------------------------
|
||||
|
||||
Verify(!Small_Enough(pixelsPerMeter));
|
||||
|
||||
scaling_vector.x = pixelsPerMeter;
|
||||
scaling_vector.y = pixelsPerMeter;
|
||||
scaling_vector.z = pixelsPerMeter;
|
||||
|
||||
worldToView.BuildIdentity();
|
||||
worldToView *= centeringOffset; // translation
|
||||
worldToView *= scaling_vector;
|
||||
int
|
||||
drawn = 0;
|
||||
|
||||
while ((entity=i.ReadAndNext()) != NULL)
|
||||
{
|
||||
Check(entity);
|
||||
//-------------------------------------
|
||||
// Draw image
|
||||
//-------------------------------------
|
||||
gauge_image = warehouse->
|
||||
gaugeImageBin.GetIfAlreadyExists(entity->GetResourceID());
|
||||
if (gauge_image != NULL)
|
||||
{
|
||||
Check(gauge_image);
|
||||
|
||||
localToView.Multiply(entity->localToWorld, worldToView);
|
||||
|
||||
gauge_image->Draw(
|
||||
LODIndex, // value set by creator
|
||||
metersPerPixel,
|
||||
&backgroundView,
|
||||
0, // default color
|
||||
(AffineMatrix &) localToView
|
||||
);
|
||||
|
||||
warehouse->gaugeImageBin.Release(entity->GetResourceID());
|
||||
}
|
||||
DrawStaticEntity(entity);
|
||||
}
|
||||
|
||||
//
|
||||
// The picture now matches these bounds, and they are what the scale
|
||||
// everything on it was drawn at came from. Remember them: an arrival
|
||||
// that leaves them alone can be added to this picture rather than
|
||||
// replacing it.
|
||||
//
|
||||
builtMinX = minX;
|
||||
builtMinY = minY;
|
||||
builtMinZ = minZ;
|
||||
builtMaxX = maxX;
|
||||
builtMaxY = maxY;
|
||||
builtMaxZ = maxZ;
|
||||
backgroundBuilt = True;
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Redraw new background, restart moving entity display
|
||||
//-----------------------------------------------------------
|
||||
@@ -2166,6 +2359,71 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// DrawStaticEntity
|
||||
//#############################################################################
|
||||
//
|
||||
// One placement onto the cached background, at the scale that background
|
||||
// was built at. Shared by the full rebuild above and by the single-arrival
|
||||
// path in NotifyOfNewInterestingEntity, so the two cannot drift into
|
||||
// drawing the same track two different ways.
|
||||
//
|
||||
Logical
|
||||
GPS::DrawStaticEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
Check(background);
|
||||
Check(renderer);
|
||||
|
||||
L4Warehouse
|
||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
||||
Check(warehouse);
|
||||
|
||||
L4GaugeImage
|
||||
*gauge_image =
|
||||
warehouse->gaugeImageBin.GetIfAlreadyExists(entity->GetResourceID());
|
||||
if (gauge_image == NULL)
|
||||
{
|
||||
return False; // nothing of it appears on the map
|
||||
}
|
||||
Check(gauge_image);
|
||||
|
||||
L4BytePort
|
||||
backgroundPort(background, "background", 0);
|
||||
GraphicsView
|
||||
backgroundView(&backgroundPort);
|
||||
backgroundView.SetOrigin(width>>1, height>>1);
|
||||
|
||||
Vector3D
|
||||
scaling_vector;
|
||||
scaling_vector.x = pixelsPerMeter;
|
||||
scaling_vector.y = pixelsPerMeter;
|
||||
scaling_vector.z = pixelsPerMeter;
|
||||
|
||||
AffineMatrix
|
||||
worldToView,
|
||||
localToView;
|
||||
|
||||
worldToView.BuildIdentity();
|
||||
worldToView *= centeringOffset; // translation
|
||||
worldToView *= scaling_vector;
|
||||
|
||||
localToView.Multiply(entity->localToWorld, worldToView);
|
||||
|
||||
gauge_image->Draw(
|
||||
LODIndex, // value set by creator
|
||||
metersPerPixel,
|
||||
&backgroundView,
|
||||
0, // default color
|
||||
(AffineMatrix &) localToView
|
||||
);
|
||||
|
||||
warehouse->gaugeImageBin.Release(entity->GetResourceID());
|
||||
return True;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GPS::Execute()
|
||||
@@ -2655,6 +2913,36 @@ void
|
||||
intercomEnabled = intercom->GetChannel() != Icom::undefinedChannel;
|
||||
}
|
||||
|
||||
//
|
||||
// RP412CAMLOG. Says what the ranking widget can see: how many
|
||||
// players are in the group and what rank and score each carries.
|
||||
// A score that never moves is either not replicated to a camera
|
||||
// host or simply has not changed - this tells the two apart. Every
|
||||
// few seconds, not every sweep.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
static Scalar next_say = 0.0f;
|
||||
if ((Scalar) Now() >= next_say)
|
||||
{
|
||||
next_say = ((Scalar) Now()) + 5.0f;
|
||||
|
||||
ChainIteratorOf<Node*> say(all_players->groupMembers);
|
||||
Player *seen;
|
||||
int shown = 0;
|
||||
DEBUG_STREAM << "CamLog: ranking sees " << say.GetSize()
|
||||
<< " player(s):";
|
||||
while ((seen = (Player*) say.ReadAndNext()) != NULL && shown < 8)
|
||||
{
|
||||
++shown;
|
||||
DEBUG_STREAM << " [bmp " << seen->playerBitmapIndex
|
||||
<< " rank " << seen->playerRanking
|
||||
<< " score " << (int) seen->currentScore << "]";
|
||||
}
|
||||
DEBUG_STREAM << " ourRank=" << currentRanking << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------
|
||||
// Clear the current array
|
||||
//-------------------------------------------
|
||||
|
||||
+27
-1
@@ -330,12 +330,27 @@ public:
|
||||
BecameActive(); // virtual function in 'GaugeBase'
|
||||
void
|
||||
NotifyOfNewInterestingEntity(Entity *entity);
|
||||
//
|
||||
// Terrain that leaves interest range is dropped from the map, and
|
||||
// there is no way to un-draw one placement from a composited picture
|
||||
// - so this is the one case that still costs a full rebuild.
|
||||
//
|
||||
void
|
||||
NotifyOfBecomingUninterestingEntity(Entity *entity);
|
||||
void
|
||||
Execute();
|
||||
|
||||
protected:
|
||||
void
|
||||
UpdateStaticEntities();
|
||||
//
|
||||
// Draw one placement into the cached background, at the scale that
|
||||
// background was built at. False if the entity has no GaugeImage and
|
||||
// so never appears on the map at all - which on the big tracks is
|
||||
// most of the terrain in them.
|
||||
//
|
||||
Logical
|
||||
DrawStaticEntity(Entity *entity);
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -344,7 +359,18 @@ protected:
|
||||
|
||||
Logical
|
||||
needsStaticUpdate,
|
||||
needsScreenUpdate;
|
||||
needsScreenUpdate,
|
||||
// is there a picture worth adding a single placement to?
|
||||
backgroundBuilt;
|
||||
//
|
||||
// The static bounds the background was last built for. They decide
|
||||
// the scale, so terrain arriving inside them can be drawn onto the
|
||||
// picture instead of forcing a new one - see
|
||||
// GPS::NotifyOfNewInterestingEntity.
|
||||
//
|
||||
Scalar
|
||||
builtMinX, builtMinY, builtMinZ,
|
||||
builtMaxX, builtMaxY, builtMaxZ;
|
||||
Scalar
|
||||
LODIndex,
|
||||
metersPerPixel,
|
||||
|
||||
+660
-94
@@ -13,7 +13,17 @@
|
||||
Logical RPL4Lobby_Available() { return False; }
|
||||
Logical RPL4Lobby_Configured() { return False; }
|
||||
Logical RPL4Lobby_InRoom() { return False; }
|
||||
int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||
int RPL4Lobby_HostOpen() { return HostOpenFailed; }
|
||||
int RPL4Lobby_JoinOpen(HWND) { return JoinOpenNothingFound; }
|
||||
int RPL4Lobby_MemberPoll() { return MemberPollClosed; }
|
||||
void RPL4Lobby_GetSetup(char *a, char *b, char *c, char *d, char *e)
|
||||
{ a[0] = b[0] = c[0] = d[0] = e[0] = '\0'; }
|
||||
Logical RPL4Lobby_IsOwner() { return False; }
|
||||
void RPL4Lobby_Pump() { }
|
||||
void RPL4Lobby_PublishSetup() { }
|
||||
int RPL4Lobby_RosterLines(char [][48], int) { return 0; }
|
||||
Logical RPL4Lobby_HostLaunch() { return False; }
|
||||
void RPL4Lobby_Leave() { }
|
||||
int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||
int RPL4Lobby_Room(HINSTANCE, HWND) { return LobbyRoomLeft; }
|
||||
void RPL4Lobby_PushRaceResults() { }
|
||||
@@ -23,6 +33,7 @@ void RPL4Lobby_PullRaceResults() { }
|
||||
|
||||
#include "rpl4fe.h"
|
||||
#include "rpl4console.h"
|
||||
#include "rpl4build.h" // generated: RP412_VERSION, for the build guard
|
||||
#include "..\munga_l4\l4steamtransport.h"
|
||||
|
||||
#pragma pack(push, 8)
|
||||
@@ -51,6 +62,40 @@ namespace
|
||||
const char kResultsKey[] = "res";
|
||||
const char kScenarioKey[] = "sc";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Simulation protocol revision. Bump this whenever a change makes
|
||||
// two builds simulate the same mission differently - it is not the
|
||||
// wire format alone. Map entity ownership is dealt by advancing a
|
||||
// shared cursor once per map entity, so anything that changes which
|
||||
// entities are dealt at all silently desynchronizes who owns what.
|
||||
//
|
||||
// 2 - doorframes became local Hermit clockwork and are no longer
|
||||
// dealt, which shifts every subsequent map entity's owner
|
||||
// 1 - the 3-machine verified Steam build
|
||||
//-------------------------------------------------------------------
|
||||
const char kNetRevision[] = "2";
|
||||
const char kNetRevKey[] = "nr";
|
||||
|
||||
// this member picked Live Cam rather than a grid slot
|
||||
const char kCamKey[] = "cam";
|
||||
|
||||
//
|
||||
// The exact build, so a room cannot mix them.
|
||||
//
|
||||
// kNetRevision above is hand-maintained and only bumped when someone
|
||||
// decides a change alters the simulation - which means two DIFFERENT
|
||||
// builds normally carry the same revision and will happily race each
|
||||
// other. That is fine when the difference really is cosmetic and
|
||||
// disastrous when the judgement was wrong, and it is not a judgement
|
||||
// anyone should have to make correctly every time. The patch number is
|
||||
// the repository's commit count, so this compares the actual binary.
|
||||
//
|
||||
// Both guards stay: the revision still refuses a mix that is known to
|
||||
// simulate differently even between builds that agree here, which
|
||||
// matters for anyone hand-editing a version.
|
||||
//
|
||||
const char kBuildKey[] = "bld";
|
||||
|
||||
// the owner's mission setup, shown to everyone in the room
|
||||
const char kMapKey[] = "mp";
|
||||
const char kTimeKey[] = "td";
|
||||
@@ -148,8 +193,27 @@ namespace
|
||||
sprintf(value, "%d", SteamNetTransport_GetFakeGamePort());
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "gp", value);
|
||||
|
||||
//
|
||||
// The name in the room is the CALLSIGN from the setup screen, not
|
||||
// the Steam persona. Publishing the persona meant the box a player
|
||||
// types their name into had no effect on anything they could see -
|
||||
// and because it never came from a file, wiping the install
|
||||
// directory did not shake it loose either.
|
||||
//
|
||||
// The persona is kept as the fallback for somebody who has never
|
||||
// set a callsign: better to appear as yourself than as "Pilot".
|
||||
//
|
||||
char name[32];
|
||||
SanitizeName(SteamFriends()->GetPersonaName(), name, sizeof(name));
|
||||
const char *callsign = RPL4FrontEnd_Callsign();
|
||||
if (callsign != NULL && callsign[0] != '\0' &&
|
||||
strcmp(callsign, "Pilot") != 0)
|
||||
{
|
||||
SanitizeName(callsign, name, sizeof(name));
|
||||
}
|
||||
else
|
||||
{
|
||||
SanitizeName(SteamFriends()->GetPersonaName(), name, sizeof(name));
|
||||
}
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "nm", name);
|
||||
|
||||
char vehicle[24], color[16], badge[24];
|
||||
@@ -158,12 +222,27 @@ namespace
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color);
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge);
|
||||
|
||||
//
|
||||
// Live Cam. The loadout above still goes out unchanged - the pick
|
||||
// is a role, not a vehicle, and it is only acted on for the host,
|
||||
// so a member's own row must go on showing what it will really
|
||||
// fly. The room screen reads this against the owner's row.
|
||||
//
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, kCamKey,
|
||||
RPL4FrontEnd_IsCameraRole() ? "1" : "0");
|
||||
|
||||
// football: this member's own team and position pick
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "tm",
|
||||
RPL4FrontEnd_TeamKey(RPL4FrontEnd_GetTeamIndex()));
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
|
||||
RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));
|
||||
|
||||
// what this build simulates like, so a mismatched room cannot launch
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, kNetRevKey, kNetRevision);
|
||||
|
||||
// and which build it actually IS - see kBuildKey
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, kBuildKey, RP412_VERSION);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Only the owner's menu decides the mission, so the owner also
|
||||
// publishes what it picked: the scenario (members need it to know
|
||||
@@ -172,6 +251,10 @@ namespace
|
||||
//---------------------------------------------------------------
|
||||
if (IsOwner())
|
||||
{
|
||||
// members check these before they act on the owner's go, and
|
||||
// a joiner checks the build before it even sits down
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kNetRevKey, kNetRevision);
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kBuildKey, RP412_VERSION);
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
||||
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kMapKey,
|
||||
@@ -226,6 +309,9 @@ namespace
|
||||
char badge[24];
|
||||
char team[32]; // football pick
|
||||
char position[16];
|
||||
char netRev[8]; // simulation protocol revision
|
||||
char build[24]; // the exact build, see kBuildKey
|
||||
Logical camera; // picked Live Cam (acted on for the host)
|
||||
Logical published;
|
||||
};
|
||||
|
||||
@@ -266,6 +352,15 @@ namespace
|
||||
strncpy(member->position,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
|
||||
sizeof(member->position) - 1);
|
||||
strncpy(member->netRev,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kNetRevKey),
|
||||
sizeof(member->netRev) - 1);
|
||||
strncpy(member->build,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kBuildKey),
|
||||
sizeof(member->build) - 1);
|
||||
member->camera = (atoi(
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kCamKey)) != 0)
|
||||
? True : False;
|
||||
member->published =
|
||||
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
||||
}
|
||||
@@ -313,6 +408,7 @@ namespace
|
||||
strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 1);
|
||||
strncpy(pilot->team, members[i].team, sizeof(pilot->team) - 1);
|
||||
strncpy(pilot->position, members[i].position, sizeof(pilot->position) - 1);
|
||||
pilot->camera = members[i].camera;
|
||||
|
||||
if (pods[0] != '\0')
|
||||
{
|
||||
@@ -333,6 +429,190 @@ namespace
|
||||
putenv(port_env);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Leaving and launching, callable from either screen.
|
||||
//
|
||||
// These began as blocks inside the room's message loop, which meant
|
||||
// only the room could do either - and the host no longer lives in
|
||||
// the room: hosting is claimed from the setup menu, where the track
|
||||
// is still changeable and the roster shows under GAME LENGTH. The
|
||||
// room keeps calling them; the menu calls them too.
|
||||
//---------------------------------------------------------------
|
||||
|
||||
void LeaveLobbyNow()
|
||||
{
|
||||
if (!gInLobby)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
gInLobby = False;
|
||||
// plain menu launches must not inherit lobby hosting
|
||||
RPL4FrontEnd_SetHostedPilots(NULL, NULL, 0);
|
||||
static char clear_env[] = "RP412HOSTPODS=";
|
||||
putenv(clear_env);
|
||||
}
|
||||
|
||||
//
|
||||
// The owner's go: verify the room, publish the roster, prime the
|
||||
// hosted-race path. Answers False when somebody has not published or
|
||||
// is the wrong build - the caller says so however it says things.
|
||||
//
|
||||
Logical OwnerLaunchNow()
|
||||
{
|
||||
MemberInfo members[kMaxLobbyMembers];
|
||||
int member_count = CollectMembers(members);
|
||||
Logical all_published = True;
|
||||
Logical all_same_build = True;
|
||||
|
||||
for (int i = 0; i < member_count; ++i)
|
||||
{
|
||||
if (!members[i].published)
|
||||
{
|
||||
all_published = False;
|
||||
}
|
||||
if (strcmp(members[i].netRev, kNetRevision) != 0)
|
||||
{
|
||||
all_same_build = False;
|
||||
DEBUG_STREAM << "Lobby: " << members[i].name
|
||||
<< " simulates like rev '" << members[i].netRev
|
||||
<< "', we are rev '" << kNetRevision << "'\n" << std::flush;
|
||||
}
|
||||
//
|
||||
// The exact build too. An empty string is an older build that
|
||||
// predates the key and cannot be trusted to match either.
|
||||
//
|
||||
if (strcmp(members[i].build, RP412_VERSION) != 0)
|
||||
{
|
||||
all_same_build = False;
|
||||
DEBUG_STREAM << "Lobby: " << members[i].name
|
||||
<< " is build '"
|
||||
<< (members[i].build[0] ? members[i].build : "(older)")
|
||||
<< "', we are '" << RP412_VERSION << "'\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (!all_published || !all_same_build || member_count < 1)
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: not everyone is ready yet\n" << std::flush;
|
||||
return False;
|
||||
}
|
||||
|
||||
++gLastGoNonce;
|
||||
char go[800];
|
||||
sprintf(go, "%d:", gLastGoNonce);
|
||||
for (int i = 0; i < member_count; ++i)
|
||||
{
|
||||
char entry[96];
|
||||
sprintf(entry, "%s|%d|%d|%I64u;", members[i].ip,
|
||||
members[i].consolePort, members[i].gamePort,
|
||||
members[i].id.ConvertToUint64());
|
||||
if (strlen(go) + strlen(entry) < sizeof(go))
|
||||
{
|
||||
strcat(go, entry);
|
||||
}
|
||||
}
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, go);
|
||||
RegisterRoster(members, member_count);
|
||||
PrimeHostedRace(members, member_count);
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
// The member's side of the same coin: has anything happened that this
|
||||
// station must act on? Answers in the header's RPL4LobbyMemberPoll
|
||||
// values - nothing / launch (peers already registered) / closed (we
|
||||
// have already left).
|
||||
//
|
||||
int MemberPollNow()
|
||||
{
|
||||
if (!gInLobby)
|
||||
{
|
||||
return MemberPollClosed;
|
||||
}
|
||||
|
||||
//
|
||||
// The host leaving hands Steam's lobby ownership to somebody
|
||||
// else - which would silently turn a joiner's page into a host's.
|
||||
// For now there is one host and it is whoever opened the lobby:
|
||||
// if ownership lands on us, the host is gone, so fold the room.
|
||||
//
|
||||
if (IsOwner())
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: the host left - closing\n" << std::flush;
|
||||
LeaveLobbyNow();
|
||||
return MemberPollClosed;
|
||||
}
|
||||
|
||||
//
|
||||
// A room whose owner simulates differently than we do would
|
||||
// desynchronize silently rather than fail, so sit the race out
|
||||
// instead of flying into it.
|
||||
//
|
||||
const char *owner_rev = LobbyText(kNetRevKey);
|
||||
if (owner_rev[0] != '\0' &&
|
||||
strcmp(owner_rev, kNetRevision) != 0)
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: owner simulates like rev '"
|
||||
<< owner_rev << "', we are rev '" << kNetRevision
|
||||
<< "' - not launching\n" << std::flush;
|
||||
LeaveLobbyNow();
|
||||
return MemberPollClosed;
|
||||
}
|
||||
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
|
||||
if (go != NULL && go[0] != '\0')
|
||||
{
|
||||
int nonce = atoi(go);
|
||||
if (nonce > gLastGoNonce)
|
||||
{
|
||||
gLastGoNonce = nonce;
|
||||
// register every rostered peer with the transport
|
||||
SteamNetTransport_SetEnginePorts(1501);
|
||||
const char *cursor = strchr(go, ':');
|
||||
cursor = (cursor != NULL) ? cursor + 1 : go;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
char ip[32];
|
||||
int console_port = 0, game_port = 0;
|
||||
unsigned __int64 steam_id = 0;
|
||||
int n = 0;
|
||||
while (cursor[n] != '\0' && cursor[n] != '|' &&
|
||||
n < (int) sizeof(ip) - 1)
|
||||
{
|
||||
ip[n] = cursor[n];
|
||||
++n;
|
||||
}
|
||||
ip[n] = '\0';
|
||||
cursor += n;
|
||||
if (*cursor == '|')
|
||||
{
|
||||
console_port = atoi(++cursor);
|
||||
while (*cursor != '\0' && *cursor != '|') ++cursor;
|
||||
}
|
||||
if (*cursor == '|')
|
||||
{
|
||||
game_port = atoi(++cursor);
|
||||
while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor;
|
||||
}
|
||||
if (*cursor == '|')
|
||||
{
|
||||
steam_id = _strtoui64(++cursor, NULL, 10);
|
||||
}
|
||||
while (*cursor != '\0' && *cursor != ';') ++cursor;
|
||||
if (*cursor == ';') ++cursor;
|
||||
|
||||
if (ip[0] != '\0' && console_port > 0 && game_port > 0)
|
||||
{
|
||||
SteamNetTransport_RegisterPeer(
|
||||
ip, console_port, game_port, steam_id);
|
||||
}
|
||||
}
|
||||
return MemberPollLaunch;
|
||||
}
|
||||
}
|
||||
return MemberPollNothing;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The room screen (front-end style: green on black)
|
||||
//---------------------------------------------------------------
|
||||
@@ -543,7 +823,29 @@ namespace
|
||||
// travel on the wire; the catalogs turn them back into
|
||||
// names, so nobody reads "bttlbrg".
|
||||
//-----------------------------------------------------------
|
||||
if (FootballLobby())
|
||||
//
|
||||
// A build the room cannot race with is worth saying before
|
||||
// anything about loadouts. A joiner is now turned away at the
|
||||
// door, so this should only ever show for a member who was
|
||||
// already seated when the guard arrived - but the launch check
|
||||
// still refuses on it, and a host is owed the reason.
|
||||
//
|
||||
if (member->published && member->build[0] != '\0' &&
|
||||
strcmp(member->build, RP412_VERSION) != 0)
|
||||
{
|
||||
sprintf(text, "BUILD %s", member->build);
|
||||
DrawTextA(mem, text, -1, &row,
|
||||
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
//
|
||||
// A Live Cam brings no vehicle, so the row says the role where
|
||||
// the loadout would go - ANY row now, since the egg honours a
|
||||
// member's pick the same as the host's. Football first, though:
|
||||
// there are no cameras in football (the egg builder strips the
|
||||
// pick), so a football room shows the team sheet and saying
|
||||
// LIVE CAM there would be the room lying about the grid.
|
||||
//
|
||||
else if (FootballLobby())
|
||||
{
|
||||
if (member->team[0] != '\0')
|
||||
{
|
||||
@@ -554,6 +856,11 @@ namespace
|
||||
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
}
|
||||
else if (member->camera)
|
||||
{
|
||||
DrawTextA(mem, "LIVE CAM", -1, &row,
|
||||
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
else if (member->vehicle[0] != '\0')
|
||||
{
|
||||
sprintf(text, "%s - %s",
|
||||
@@ -779,12 +1086,7 @@ namespace
|
||||
|
||||
if (room.leaveClicked)
|
||||
{
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
gInLobby = False;
|
||||
// plain menu launches must not inherit lobby hosting
|
||||
RPL4FrontEnd_SetHostedPilots(NULL, NULL, 0);
|
||||
static char clear_env[] = "RP412HOSTPODS=";
|
||||
putenv(clear_env);
|
||||
LeaveLobbyNow();
|
||||
outcome = LobbyRoomLeft;
|
||||
break;
|
||||
}
|
||||
@@ -795,38 +1097,11 @@ namespace
|
||||
if (room.launchClicked)
|
||||
{
|
||||
room.launchClicked = False;
|
||||
room.memberCount = CollectMembers(room.members);
|
||||
Logical all_published = True;
|
||||
for (int i = 0; i < room.memberCount; ++i)
|
||||
if (OwnerLaunchNow())
|
||||
{
|
||||
if (!room.members[i].published)
|
||||
{
|
||||
all_published = False;
|
||||
}
|
||||
}
|
||||
if (all_published && room.memberCount >= 1)
|
||||
{
|
||||
++gLastGoNonce;
|
||||
char go[800];
|
||||
sprintf(go, "%d:", gLastGoNonce);
|
||||
for (int i = 0; i < room.memberCount; ++i)
|
||||
{
|
||||
char entry[96];
|
||||
sprintf(entry, "%s|%d|%d|%I64u;", room.members[i].ip,
|
||||
room.members[i].consolePort, room.members[i].gamePort,
|
||||
room.members[i].id.ConvertToUint64());
|
||||
if (strlen(go) + strlen(entry) < sizeof(go))
|
||||
{
|
||||
strcat(go, entry);
|
||||
}
|
||||
}
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, go);
|
||||
RegisterRoster(room.members, room.memberCount);
|
||||
PrimeHostedRace(room.members, room.memberCount);
|
||||
outcome = LobbyLaunchHost;
|
||||
break;
|
||||
}
|
||||
DEBUG_STREAM << "Lobby: not everyone is ready yet\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -834,57 +1109,17 @@ namespace
|
||||
//
|
||||
if (!IsOwner())
|
||||
{
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
|
||||
if (go != NULL && go[0] != '\0')
|
||||
{
|
||||
int nonce = atoi(go);
|
||||
if (nonce > gLastGoNonce)
|
||||
{
|
||||
gLastGoNonce = nonce;
|
||||
// register every rostered peer with the transport
|
||||
SteamNetTransport_SetEnginePorts(1501);
|
||||
const char *cursor = strchr(go, ':');
|
||||
cursor = (cursor != NULL) ? cursor + 1 : go;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
char ip[32];
|
||||
int console_port = 0, game_port = 0;
|
||||
unsigned __int64 steam_id = 0;
|
||||
int n = 0;
|
||||
while (cursor[n] != '\0' && cursor[n] != '|' &&
|
||||
n < (int) sizeof(ip) - 1)
|
||||
{
|
||||
ip[n] = cursor[n];
|
||||
++n;
|
||||
}
|
||||
ip[n] = '\0';
|
||||
cursor += n;
|
||||
if (*cursor == '|')
|
||||
{
|
||||
console_port = atoi(++cursor);
|
||||
while (*cursor != '\0' && *cursor != '|') ++cursor;
|
||||
}
|
||||
if (*cursor == '|')
|
||||
{
|
||||
game_port = atoi(++cursor);
|
||||
while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor;
|
||||
}
|
||||
if (*cursor == '|')
|
||||
{
|
||||
steam_id = _strtoui64(++cursor, NULL, 10);
|
||||
}
|
||||
while (*cursor != '\0' && *cursor != ';') ++cursor;
|
||||
if (*cursor == ';') ++cursor;
|
||||
int poll = MemberPollNow();
|
||||
|
||||
if (ip[0] != '\0' && console_port > 0 && game_port > 0)
|
||||
{
|
||||
SteamNetTransport_RegisterPeer(
|
||||
ip, console_port, game_port, steam_id);
|
||||
}
|
||||
}
|
||||
outcome = LobbyLaunchMember;
|
||||
break;
|
||||
}
|
||||
if (poll == MemberPollLaunch)
|
||||
{
|
||||
outcome = LobbyLaunchMember;
|
||||
break;
|
||||
}
|
||||
if (poll == MemberPollClosed)
|
||||
{
|
||||
outcome = LobbyRoomLeft;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,9 +1173,67 @@ Logical
|
||||
return gInLobby;
|
||||
}
|
||||
|
||||
int
|
||||
RPL4Lobby_Host(HINSTANCE instance, HWND main_window)
|
||||
//
|
||||
// Host and Join are the two places that reach Steam without the
|
||||
// transport having got there first, so they carry their own guard.
|
||||
// steam_api.dll is delay-loaded and calling into it when it is absent
|
||||
// raises the helper's fatal exception - the menu already greys these on
|
||||
// Available(), but a dead DLL must not depend on the UI for safety.
|
||||
//
|
||||
namespace
|
||||
{
|
||||
//
|
||||
// The one worldwide search, shared by joining and by the one-host
|
||||
// rule: is there an open RP412 lobby right now?
|
||||
//
|
||||
Logical FindOpenLobby(CSteamID *found)
|
||||
{
|
||||
gCallDone = False;
|
||||
SteamMatchmaking()->AddRequestLobbyListStringFilter(
|
||||
kLobbyTagKey, "1", k_ELobbyComparisonEqual);
|
||||
// the default lobby search is distance-filtered (roughly same
|
||||
// region) - RP412 races are worldwide
|
||||
SteamMatchmaking()->AddRequestLobbyListDistanceFilter(
|
||||
k_ELobbyDistanceFilterWorldwide);
|
||||
SteamAPICall_t call = SteamMatchmaking()->RequestLobbyList();
|
||||
gCalls.listResult.Set(call, &gCalls, &LobbyCalls::OnList);
|
||||
if (!WaitForCall(15000))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
*found = gCallLobby;
|
||||
return True;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
RPL4Lobby_HostOpen()
|
||||
{
|
||||
if (!SteamNetTransport_ClientLibraryPresent() || gInLobby)
|
||||
{
|
||||
return gInLobby ? HostOpenCreated : HostOpenFailed;
|
||||
}
|
||||
|
||||
//
|
||||
// One host at a time, while the whole flow is young: if an RP412
|
||||
// lobby is already open anywhere, this button does not open a second
|
||||
// one - the caller says join it instead. Check-then-create is not
|
||||
// airtight against two people clicking in the same breath, but for a
|
||||
// playtest group coordinating over voice it is the rule they asked
|
||||
// for.
|
||||
//
|
||||
{
|
||||
CSteamID existing;
|
||||
|
||||
if (FindOpenLobby(&existing))
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: " << existing.ConvertToUint64()
|
||||
<< " is already open - one host at a time, join it instead\n"
|
||||
<< std::flush;
|
||||
return HostOpenLobbyExists;
|
||||
}
|
||||
}
|
||||
|
||||
gCallDone = False;
|
||||
SteamAPICall_t call =
|
||||
SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, kMaxLobbyMembers);
|
||||
@@ -948,20 +1241,240 @@ int
|
||||
if (!WaitForCall(15000))
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: CreateLobby failed\n" << std::flush;
|
||||
return LobbyRoomLeft;
|
||||
return HostOpenFailed;
|
||||
}
|
||||
gLobby = gCallLobby;
|
||||
gInLobby = True;
|
||||
gLastGoNonce = 0;
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kLobbyTagKey, "1");
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, "");
|
||||
DEBUG_STREAM << "Lobby: hosting " << gLobby.ConvertToUint64() << "\n" << std::flush;
|
||||
return RunRoom(instance, main_window);
|
||||
|
||||
//
|
||||
// The owner's row and the mission setup, published immediately: a
|
||||
// joiner can arrive seconds from now, and what they see first is
|
||||
// whatever is on the books.
|
||||
//
|
||||
PublishMemberData();
|
||||
DEBUG_STREAM << "Lobby: hosting " << gLobby.ConvertToUint64()
|
||||
<< " from the setup menu\n" << std::flush;
|
||||
return HostOpenCreated;
|
||||
}
|
||||
|
||||
//
|
||||
// Joining, the same shape as hosting: sit down in the lobby and return -
|
||||
// the setup menu stays up, the mission column greys out and mirrors the
|
||||
// host's picks, and the go arrives through RPL4Lobby_MemberPoll.
|
||||
//
|
||||
int
|
||||
RPL4Lobby_JoinOpen(HWND main_window)
|
||||
{
|
||||
if (!SteamNetTransport_ClientLibraryPresent() || gInLobby)
|
||||
{
|
||||
return gInLobby ? JoinOpenJoined : JoinOpenNothingFound;
|
||||
}
|
||||
|
||||
CSteamID target;
|
||||
|
||||
if (!FindOpenLobby(&target))
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: no open race lobby found\n" << std::flush;
|
||||
return JoinOpenNothingFound;
|
||||
}
|
||||
|
||||
gCallDone = False;
|
||||
SteamAPICall_t call = SteamMatchmaking()->JoinLobby(target);
|
||||
gCalls.enterResult.Set(call, &gCalls, &LobbyCalls::OnEnter);
|
||||
if (!WaitForCall(15000))
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: join failed\n" << std::flush;
|
||||
return JoinOpenNothingFound;
|
||||
}
|
||||
gLobby = gCallLobby;
|
||||
gInLobby = True;
|
||||
|
||||
//
|
||||
// Refuse a room running a different build, and refuse it HERE rather
|
||||
// than at launch - the same door guard the room flow has always had,
|
||||
// word for word. See the room version for why it is loud.
|
||||
//
|
||||
{
|
||||
const char *host_build = SteamMatchmaking()->GetLobbyData(gLobby, kBuildKey);
|
||||
if (host_build == NULL || strcmp(host_build, RP412_VERSION) != 0)
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: room is build '"
|
||||
<< ((host_build != NULL && host_build[0]) ? host_build : "(older)")
|
||||
<< "', we are '" << RP412_VERSION
|
||||
<< "' - leaving, everyone must run the same build\n" << std::flush;
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
gInLobby = False;
|
||||
|
||||
char said[256];
|
||||
sprintf(said,
|
||||
"That race is running Red Planet %s.\n"
|
||||
"You are running %s.\n\n"
|
||||
"Everyone in a race has to be on the same build - the\n"
|
||||
"simulation has to agree exactly. Swap to a matching\n"
|
||||
"build and join again.",
|
||||
(host_build != NULL && host_build[0]) ? host_build : "an older build",
|
||||
RP412_VERSION);
|
||||
MessageBoxA(main_window, said, "Different build",
|
||||
MB_OK | MB_ICONINFORMATION);
|
||||
return JoinOpenWrongBuild;
|
||||
}
|
||||
}
|
||||
|
||||
// answer only launches newer than anything already in the lobby
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
|
||||
gLastGoNonce = (go != NULL) ? atoi(go) : 0;
|
||||
|
||||
// our row, visible to the host's roster immediately
|
||||
PublishMemberData();
|
||||
DEBUG_STREAM << "Lobby: joined " << gLobby.ConvertToUint64()
|
||||
<< " from the setup menu\n" << std::flush;
|
||||
return JoinOpenJoined;
|
||||
}
|
||||
|
||||
int
|
||||
RPL4Lobby_MemberPoll()
|
||||
{
|
||||
if (!gInLobby)
|
||||
{
|
||||
return MemberPollClosed;
|
||||
}
|
||||
return MemberPollNow();
|
||||
}
|
||||
|
||||
//
|
||||
// The host's published mission setup, as display names, for the greyed
|
||||
// mission column to mirror. Empty strings before the first publish lands.
|
||||
//
|
||||
void
|
||||
RPL4Lobby_GetSetup(
|
||||
char *scenario, char *map, char *time_of_day,
|
||||
char *weather, char *length)
|
||||
{
|
||||
strncpy(scenario, LobbyText(kScenarioKey), 47); scenario[47] = '\0';
|
||||
strncpy(map, LobbyText(kMapKey), 47); map[47] = '\0';
|
||||
strncpy(time_of_day, LobbyText(kTimeKey), 47); time_of_day[47] = '\0';
|
||||
strncpy(weather, LobbyText(kWeatherKey), 47); weather[47] = '\0';
|
||||
strncpy(length, LobbyText(kLengthKey), 47); length[47] = '\0';
|
||||
}
|
||||
|
||||
Logical
|
||||
RPL4Lobby_IsOwner()
|
||||
{
|
||||
return gInLobby && IsOwner();
|
||||
}
|
||||
|
||||
void
|
||||
RPL4Lobby_Pump()
|
||||
{
|
||||
if (SteamNetTransport_ClientLibraryPresent())
|
||||
{
|
||||
SteamAPI_RunCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RPL4Lobby_PublishSetup()
|
||||
{
|
||||
if (gInLobby)
|
||||
{
|
||||
PublishMemberData();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The roster, as display lines for the setup menu: callsign plus what it
|
||||
// is bringing, a cam tag for a member who picked Live Cam, and a build
|
||||
// warning where launch would refuse - so the host can see WHY the room is
|
||||
// not ready, standing in the same screen the launch button is on.
|
||||
//
|
||||
int
|
||||
RPL4Lobby_RosterLines(char lines[][48], int max_lines)
|
||||
{
|
||||
if (!gInLobby || max_lines <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
MemberInfo members[kMaxLobbyMembers];
|
||||
int count = CollectMembers(members);
|
||||
int written = 0;
|
||||
|
||||
Logical football = FootballLobby();
|
||||
|
||||
for (int i = 0; i < count && written < max_lines; ++i)
|
||||
{
|
||||
const char *name = members[i].name[0] ? members[i].name : "(joining...)";
|
||||
const char *note = "";
|
||||
|
||||
if (strcmp(members[i].build, RP412_VERSION) != 0)
|
||||
{
|
||||
note = " WRONG BUILD";
|
||||
}
|
||||
else if (!members[i].published)
|
||||
{
|
||||
note = " ...";
|
||||
}
|
||||
|
||||
//
|
||||
// The whole loadout, not just the ride: colour and badge for a
|
||||
// race, team and position for football - what the host is about
|
||||
// to commit everyone AS, visible before the commit. A camera
|
||||
// brings none of it; the pick replaces the lot.
|
||||
//
|
||||
if (members[i].camera && !football)
|
||||
{
|
||||
_snprintf(lines[written], 47, "%-12s LIVE CAM%s", name, note);
|
||||
}
|
||||
else if (football)
|
||||
{
|
||||
_snprintf(lines[written], 47, "%-12s %s %s %s%s",
|
||||
name, members[i].vehicle,
|
||||
members[i].team[0] ? members[i].team : "(no team)",
|
||||
members[i].position[0] ? members[i].position : "",
|
||||
note);
|
||||
}
|
||||
else
|
||||
{
|
||||
_snprintf(lines[written], 47, "%-12s %s %s %s%s",
|
||||
name, members[i].vehicle, members[i].color,
|
||||
(members[i].badge[0] &&
|
||||
_stricmp(members[i].badge, "None") != 0)
|
||||
? members[i].badge : "",
|
||||
note);
|
||||
}
|
||||
lines[written][47] = '\0';
|
||||
++written;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
Logical
|
||||
RPL4Lobby_HostLaunch()
|
||||
{
|
||||
if (!gInLobby || !IsOwner())
|
||||
{
|
||||
return False;
|
||||
}
|
||||
return OwnerLaunchNow();
|
||||
}
|
||||
|
||||
void
|
||||
RPL4Lobby_Leave()
|
||||
{
|
||||
LeaveLobbyNow();
|
||||
}
|
||||
|
||||
int
|
||||
RPL4Lobby_Join(HINSTANCE instance, HWND main_window)
|
||||
{
|
||||
if (!SteamNetTransport_ClientLibraryPresent())
|
||||
{
|
||||
return LobbyRoomLeft;
|
||||
}
|
||||
|
||||
gCallDone = False;
|
||||
SteamMatchmaking()->AddRequestLobbyListStringFilter(
|
||||
kLobbyTagKey, "1", k_ELobbyComparisonEqual);
|
||||
@@ -988,6 +1501,50 @@ int
|
||||
}
|
||||
gLobby = gCallLobby;
|
||||
gInLobby = True;
|
||||
|
||||
//
|
||||
// Refuse a room running a different build, and refuse it HERE rather
|
||||
// than at launch. The owner's launch check would catch it, but only
|
||||
// after everyone has picked a loadout and pressed go, and all it can do
|
||||
// then is silently decline to start - which reads as the host's button
|
||||
// being broken. Far better to say so on the way in.
|
||||
//
|
||||
// The owner publishes its build as lobby data, so this costs one read.
|
||||
// An empty string means a host older than the key, which is equally a
|
||||
// mismatch: it cannot be trusted to be this build.
|
||||
//
|
||||
{
|
||||
const char *host_build = SteamMatchmaking()->GetLobbyData(gLobby, kBuildKey);
|
||||
if (host_build == NULL || strcmp(host_build, RP412_VERSION) != 0)
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: room is build '"
|
||||
<< ((host_build != NULL && host_build[0]) ? host_build : "(older)")
|
||||
<< "', we are '" << RP412_VERSION
|
||||
<< "' - leaving, everyone must run the same build\n" << std::flush;
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
gInLobby = False;
|
||||
|
||||
//
|
||||
// Say so on screen, not just in the log. A player who is bounced
|
||||
// out of a room with no explanation will try again, and again,
|
||||
// and then report that joining is broken - which is precisely
|
||||
// the afternoon this guard exists to prevent.
|
||||
//
|
||||
char said[256];
|
||||
sprintf(said,
|
||||
"That race is running Red Planet %s.\n"
|
||||
"You are running %s.\n\n"
|
||||
"Everyone in a race has to be on the same build - the\n"
|
||||
"simulation has to agree exactly. Swap to a matching\n"
|
||||
"build and join again.",
|
||||
(host_build != NULL && host_build[0]) ? host_build : "an older build",
|
||||
RP412_VERSION);
|
||||
MessageBoxA(main_window, said, "Different build",
|
||||
MB_OK | MB_ICONINFORMATION);
|
||||
return LobbyRoomLeft;
|
||||
}
|
||||
}
|
||||
|
||||
// answer only launches newer than anything already in the lobby
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
|
||||
gLastGoNonce = (go != NULL) ? atoi(go) : 0;
|
||||
@@ -1047,10 +1604,19 @@ void
|
||||
|
||||
//
|
||||
// The owner publishes right after its race teardown - ours may
|
||||
// finish first, so give the sheet a moment to arrive
|
||||
// finish first, so give the sheet a moment to arrive.
|
||||
//
|
||||
// Unless WE aborted. An Alt+Q leaves a race that is still running
|
||||
// everywhere else, and its sheet will not exist for minutes - the
|
||||
// eight second wait below was eight seconds of frozen window for a
|
||||
// player who had just asked to leave. Exit_Code carries the abort
|
||||
// (the race loop resets it after the menu); take one free look and
|
||||
// otherwise skip - the sheet's nonce keeps it eligible to show
|
||||
// after the race actually ends.
|
||||
//
|
||||
const char *sheet = NULL;
|
||||
DWORD deadline = GetTickCount() + 8 * 1000;
|
||||
DWORD deadline = (Exit_Code != 0)
|
||||
? GetTickCount() : GetTickCount() + 8 * 1000;
|
||||
for (;;)
|
||||
{
|
||||
SteamAPI_RunCallbacks();
|
||||
|
||||
+80
-2
@@ -46,9 +46,87 @@ Logical
|
||||
Logical
|
||||
RPL4Lobby_InRoom();
|
||||
|
||||
// Create a lobby / find-and-join one, then run the room screen.
|
||||
//------------------------------------------------------------------------
|
||||
// Hosting, from the setup menu.
|
||||
//
|
||||
// The host never leaves the configuration page: HostOpen claims the lobby
|
||||
// and returns immediately, the menu keeps showing (and changing) the
|
||||
// mission, joiners appear under GAME LENGTH via RosterLines, and LAUNCH
|
||||
// GAME goes through HostLaunch. The room screen is for members only.
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
enum RPL4LobbyHostOpen
|
||||
{
|
||||
HostOpenFailed = 0, // Steam said no (or is not there)
|
||||
HostOpenCreated, // the lobby is ours (also: already was)
|
||||
HostOpenLobbyExists // one host at a time - join the open one
|
||||
};
|
||||
|
||||
// Create the lobby and publish our row + the mission setup. Does not
|
||||
// open any window. Refuses when an RP412 lobby is already open anywhere:
|
||||
// one host at a time while the flow is young.
|
||||
int
|
||||
RPL4Lobby_Host(HINSTANCE instance, HWND main_window);
|
||||
RPL4Lobby_HostOpen();
|
||||
|
||||
enum RPL4LobbyJoinOpen
|
||||
{
|
||||
JoinOpenNothingFound = 0, // no open lobby, or Steam said no
|
||||
JoinOpenJoined, // seated; our row is published
|
||||
JoinOpenWrongBuild // bounced at the door (dialog shown)
|
||||
};
|
||||
|
||||
// Find the open lobby and sit down in it. Does not open any window: the
|
||||
// setup menu stays up, mission column greyed and mirroring the host.
|
||||
int
|
||||
RPL4Lobby_JoinOpen(HWND main_window);
|
||||
|
||||
enum RPL4LobbyMemberPoll
|
||||
{
|
||||
MemberPollNothing = 0, // keep waiting
|
||||
MemberPollLaunch, // the go arrived; peers registered - launch
|
||||
MemberPollClosed // the lobby is over (host left / mismatch)
|
||||
};
|
||||
|
||||
// A seated member's heartbeat question: anything to act on?
|
||||
int
|
||||
RPL4Lobby_MemberPoll();
|
||||
|
||||
// The host's published mission setup as display names (each buffer at
|
||||
// least 48 bytes). Empty strings before the first publish arrives.
|
||||
void
|
||||
RPL4Lobby_GetSetup(
|
||||
char *scenario, char *map, char *time_of_day,
|
||||
char *weather, char *length);
|
||||
|
||||
// True when we are in a lobby we own.
|
||||
Logical
|
||||
RPL4Lobby_IsOwner();
|
||||
|
||||
// Run the Steam callbacks once - the menu's timer calls this so member
|
||||
// joins/leaves and their data show up while no room screen is pumping.
|
||||
void
|
||||
RPL4Lobby_Pump();
|
||||
|
||||
// Republish our member row and (as owner) the mission setup. Call after
|
||||
// the host changes anything on the menu, so members' rooms track it.
|
||||
void
|
||||
RPL4Lobby_PublishSetup();
|
||||
|
||||
// The roster as display lines ("callsign vehicle [cam|WRONG BUILD]").
|
||||
// Returns the number written, 0 when not in a lobby.
|
||||
int
|
||||
RPL4Lobby_RosterLines(char lines[][48], int max_lines);
|
||||
|
||||
// The owner's go: verify everyone, publish the roster, prime the hosted
|
||||
// race. False (and a log line saying who) when the room is not ready.
|
||||
Logical
|
||||
RPL4Lobby_HostLaunch();
|
||||
|
||||
// Leave the lobby and clear the hosted-race environment.
|
||||
void
|
||||
RPL4Lobby_Leave();
|
||||
|
||||
// Find-and-join a lobby, then run the room screen (members).
|
||||
int
|
||||
RPL4Lobby_Join(HINSTANCE instance, HWND main_window);
|
||||
|
||||
|
||||
+118
-56
@@ -444,9 +444,18 @@ void
|
||||
//----------------------------------------
|
||||
// Notify of mode change
|
||||
//----------------------------------------
|
||||
//
|
||||
// Unqualified, so the platform's override is the one that runs. The
|
||||
// RIO carries the four mode lamps on the Upper Right MFD and lights
|
||||
// them from here (VTVRIOMapper::NotifyOfControlModeChange); naming
|
||||
// the class suppressed the virtual call and landed on the base's
|
||||
// no-op instead, so the lamps never followed the mode the pilot had
|
||||
// just selected. Its neighbour has always gone out this way - see
|
||||
// VTVControlsMapper::SetConfigurationState.
|
||||
//
|
||||
if (previous_mode != controlMode)
|
||||
{
|
||||
L4VTVControlsMapper::NotifyOfControlModeChange(controlMode);
|
||||
NotifyOfControlModeChange(controlMode);
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -725,6 +734,13 @@ void
|
||||
mode_manager->AddModeMask(previousPresetModeMask);
|
||||
}
|
||||
//-----------------------------------
|
||||
// Move the lamps with the mappings.
|
||||
// Doing it here rather than in the
|
||||
// switch handler keeps the keyboard
|
||||
// presets (1-6) in step as well.
|
||||
//-----------------------------------
|
||||
NotifyOfPresetChange(previousPresetNumber, preset_number);
|
||||
//-----------------------------------
|
||||
// Save the new preset number
|
||||
//-----------------------------------
|
||||
previousPresetNumber = preset_number;
|
||||
@@ -733,6 +749,19 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
L4VTVControlsMapper::NotifyOfPresetChange(
|
||||
int /*old_preset*/,
|
||||
int /*new_preset*/
|
||||
)
|
||||
{
|
||||
Check(this);
|
||||
// The base mapper has no preset lamps to move.
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//########################### ThrustmasterMapper ##############################
|
||||
//#############################################################################
|
||||
@@ -854,18 +883,18 @@ void
|
||||
//-------------------------------------------------------
|
||||
// Set driving modes
|
||||
//-------------------------------------------------------
|
||||
case 'b':
|
||||
case 'B': SetControlsMode(BasicMode); break;
|
||||
|
||||
case 's':
|
||||
case 'S': SetControlsMode(StandardMode); break;
|
||||
|
||||
case 'v':
|
||||
case 'V': SetControlsMode(VeteranMode); break;
|
||||
|
||||
case 'm':
|
||||
case 'M': SetControlsMode(MasterMode); break;
|
||||
|
||||
//
|
||||
// B / S / V / M used to drop straight into Basic, Standard,
|
||||
// Veteran and Master here. The driving mode is a panel
|
||||
// decision - the four buttons on the Upper Right MFD, with the
|
||||
// lamps that say which one you are in - and a bare letter key
|
||||
// changing it behind the player's back is not that. Worse in
|
||||
// 4.12 than it ever was in the pod: the whole letter board is
|
||||
// the MFD banks now, so those four letters are buttons in their
|
||||
// own right and would have fired twice.
|
||||
//
|
||||
// Nothing replaces them. Press the mode you want.
|
||||
//
|
||||
//-------------------------------------------------------
|
||||
// Configuration stuff
|
||||
//-------------------------------------------------------
|
||||
@@ -1400,45 +1429,13 @@ void
|
||||
if (message->dataContents > 0)
|
||||
{
|
||||
//-----------------------------------
|
||||
// Choose a new preset
|
||||
// Choose a new preset. PresetEnable
|
||||
// ignores a repeat of the lit switch
|
||||
// and moves the lamps itself.
|
||||
//-----------------------------------
|
||||
int
|
||||
current_preset_number = (message->dataContents - 1)
|
||||
- LBE4ControlsManager::ButtonSecondary7;
|
||||
|
||||
if (previousPresetNumber != current_preset_number)
|
||||
{
|
||||
//-----------------------------------
|
||||
// Set the old preset lamp to 'dim'
|
||||
//-----------------------------------
|
||||
if (previousPresetNumber >= 0)
|
||||
{
|
||||
Verify(previousPresetNumber < presetCount);
|
||||
|
||||
if (modeLamp[previousPresetNumber] != NULL)
|
||||
{
|
||||
Check(modeLamp[previousPresetNumber]);
|
||||
modeLamp[previousPresetNumber]->SetState(L4Lamp::LampStateDim);
|
||||
}
|
||||
}
|
||||
//-----------------------------------
|
||||
// Set the new preset lamp to 'on'
|
||||
//-----------------------------------
|
||||
if (current_preset_number >= 0)
|
||||
{
|
||||
Verify(current_preset_number < presetCount);
|
||||
|
||||
if (modeLamp[current_preset_number] != NULL)
|
||||
{
|
||||
Check(modeLamp[current_preset_number]);
|
||||
modeLamp[current_preset_number]->SetState(L4Lamp::LampStateOn);
|
||||
}
|
||||
}
|
||||
//-----------------------------------
|
||||
// Change presets
|
||||
//-----------------------------------
|
||||
PresetEnable(current_preset_number);
|
||||
}
|
||||
PresetEnable(
|
||||
(message->dataContents - 1) - LBE4ControlsManager::ButtonSecondary7
|
||||
);
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -1626,6 +1623,18 @@ void
|
||||
modeLamp[lamp_number]->SetState(L4Lamp::LampStateOn);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
// Remember what is lit
|
||||
//----------------------------------
|
||||
//
|
||||
// previousControlMode is the lamp the NEXT change dims, and nothing
|
||||
// used to write it after construction set it to -1. Every mode
|
||||
// therefore lit its own lamp against a dim that matched nothing, and
|
||||
// the panel accumulated lamps instead of following the selection.
|
||||
//
|
||||
previousControlMode = controlMode;
|
||||
|
||||
//-----------------------------------
|
||||
// Invoke ancestral method
|
||||
//-----------------------------------
|
||||
@@ -1655,6 +1664,44 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// The six amber switches down the map's right flank. Called by PresetEnable,
|
||||
// so the lamps follow the mappings no matter what asked for the change.
|
||||
//
|
||||
void
|
||||
VTVRIOMapper::NotifyOfPresetChange(int old_preset, int new_preset)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//----------------------------------
|
||||
// Set the old preset lamp to 'dim'
|
||||
//----------------------------------
|
||||
if (old_preset >= 0)
|
||||
{
|
||||
Verify(old_preset < presetCount);
|
||||
|
||||
if (presetLamp[old_preset] != NULL)
|
||||
{
|
||||
Check(presetLamp[old_preset]);
|
||||
presetLamp[old_preset]->SetState(L4Lamp::LampStateDim);
|
||||
}
|
||||
}
|
||||
//----------------------------------
|
||||
// Set the new preset lamp to 'on'
|
||||
//----------------------------------
|
||||
if (new_preset >= 0)
|
||||
{
|
||||
Verify(new_preset < presetCount);
|
||||
|
||||
if (presetLamp[new_preset] != NULL)
|
||||
{
|
||||
Check(presetLamp[new_preset]);
|
||||
presetLamp[new_preset]->SetState(L4Lamp::LampStateOn);
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// Construction and Destruction Support
|
||||
//
|
||||
@@ -1680,6 +1727,20 @@ VTVRIOMapper::VTVRIOMapper(
|
||||
leftPedal = 0.0f;
|
||||
rightPedal = 0.0f;
|
||||
|
||||
//------------------------------------------------
|
||||
// There are no lamps until the mapping blocks
|
||||
// below make them - and under NOMODES they never
|
||||
// do, so the notify methods must see NULLs.
|
||||
//------------------------------------------------
|
||||
{
|
||||
int
|
||||
i;
|
||||
|
||||
for(i=0; i<configLampCount; ++i) configLamp[i] = NULL;
|
||||
for(i=0; i<modeLampCount; ++i) modeLamp[i] = NULL;
|
||||
for(i=0; i<presetCount; ++i) presetLamp[i] = NULL;
|
||||
}
|
||||
|
||||
Check(application);
|
||||
LBE4ControlsManager
|
||||
*controls = Cast_Object(
|
||||
@@ -1915,13 +1976,14 @@ VTVRIOMapper::VTVRIOMapper(
|
||||
this
|
||||
);
|
||||
|
||||
// These lamps are explicitly controlled by SelectPresetMessageHandler
|
||||
modeLamp[i] = CreateControlledLamp(button_number[i]);
|
||||
// These lamps are explicitly controlled by NotifyOfPresetChange.
|
||||
// They are six, and they are NOT the four mode lamps above.
|
||||
presetLamp[i] = CreateControlledLamp(button_number[i]);
|
||||
|
||||
if (modeLamp[i] != NULL)
|
||||
if (presetLamp[i] != NULL)
|
||||
{
|
||||
Check(modeLamp[i]);
|
||||
modeLamp[i]->SetState(
|
||||
Check(presetLamp[i]);
|
||||
presetLamp[i]->SetState(
|
||||
(i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,12 @@ ModeMask
|
||||
//
|
||||
void
|
||||
PresetEnable(int preset_number);
|
||||
|
||||
// Announced by PresetEnable for EVERY preset change, whichever way it was
|
||||
// triggered - map-flank switch or keyboard. Platforms carrying preset
|
||||
// lamps move them here; the base mapper has none.
|
||||
virtual void
|
||||
NotifyOfPresetChange(int old_preset, int new_preset);
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Protected data
|
||||
//
|
||||
@@ -250,6 +256,9 @@ public:
|
||||
void
|
||||
NotifyOfConfigurationModeChange(Logical new_state);
|
||||
|
||||
void
|
||||
NotifyOfPresetChange(int old_preset, int new_preset);
|
||||
|
||||
void
|
||||
SetPerformance(Performance performance)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,12 @@ void
|
||||
gameModel = strdup(player_model);
|
||||
Register_Pointer(gameModel);
|
||||
|
||||
if (RPCameraLog())
|
||||
{
|
||||
DEBUG_STREAM << "CamLog: local player node '" << (const char *) player_node
|
||||
<< "' gameModel '" << gameModel << "'\n" << std::flush;
|
||||
}
|
||||
|
||||
dropZoneName = NULL;
|
||||
const char* drop_zone = NULL;
|
||||
notation_file->GetEntry(player_node, "dropzone", &drop_zone);
|
||||
|
||||
+62
-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();
|
||||
@@ -407,6 +419,54 @@ void
|
||||
NetworkManager *net_mgr = GetNetworkManager();
|
||||
Check(net_mgr);
|
||||
NetworkClient *client = net_mgr->GetNetworkClientPointer(packet->clientID);
|
||||
|
||||
//
|
||||
// Say what is being routed where, for the first few.
|
||||
//
|
||||
// Playback dies inside Mission::Mission, reached through the egg file
|
||||
// handler, while dispatching spooled packets - so a packet is arriving
|
||||
// at a client that is not the one it was recorded for. Client IDs and
|
||||
// message IDs are both small integers counted from the same base
|
||||
// (NetworkClient::NextMessageID is 3, so the interest manager's
|
||||
// NewDynamicEntity and the network manager's ReceiveEggFile are BOTH
|
||||
// message 3), which makes a mis-routed packet look perfectly valid to
|
||||
// whoever receives it. Naming the pair and the client it resolved to
|
||||
// ends the guessing.
|
||||
//
|
||||
// A NULL client is the other candidate: Check() is a no-op in a
|
||||
// release build, so a missing interest manager would be a call through
|
||||
// nothing rather than a complaint.
|
||||
//
|
||||
//
|
||||
// Behind RP412CAMLOG now that playback works. It earned its place -
|
||||
// one line of it identified the header being parsed as packet one - but
|
||||
// a station in a pod bay should not be writing a log line per packet
|
||||
// for the first two dozen packets of every replay.
|
||||
//
|
||||
if (RPCameraLog())
|
||||
{
|
||||
static int said = 0;
|
||||
|
||||
if (said < 24)
|
||||
{
|
||||
++said;
|
||||
DEBUG_STREAM << "Playback: packet client=" << (int) packet->clientID
|
||||
<< " message=" << (int) packet->messageData.messageID
|
||||
<< " length=" << (int) packet->messageData.messageLength
|
||||
<< " -> client " << (void *) client
|
||||
<< (client == NULL ? " (NULL!)" : "")
|
||||
<< (client == (NetworkClient *) net_mgr ? " = the NETWORK MANAGER" : "")
|
||||
<< (client == (NetworkClient *) this ? " = the application" : "")
|
||||
<< (client == (NetworkClient *) GetInterestManager()
|
||||
? " = the interest manager" : "")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
if (client == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Check(client);
|
||||
client->ReceiveNetworkPacket(packet, &packet->messageData);
|
||||
Check_Fpu();
|
||||
|
||||
+21
-1
@@ -66,10 +66,28 @@
|
||||
<AdditionalOptions>/FORCE:MULTIPLE %(AdditionalOptions)</AdditionalOptions>
|
||||
<AdditionalLibraryDirectories>..\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<!-- legacy_stdio_definitions satisfies the June-2010 dxerr.lib on modern MSVC. -->
|
||||
<AdditionalDependencies>ws2_32.lib;dinput8.lib;dxguid.lib;OpenAL32.lib;libsndfile-1.lib;d3d9.lib;legacy_stdio_definitions.lib;steam_api.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>ws2_32.lib;dinput8.lib;dxguid.lib;OpenAL32.lib;libsndfile-1.lib;d3d9.lib;legacy_stdio_definitions.lib;steam_api.lib;delayimp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<!-- steam_api.dll is DELAY-LOADED so the game runs without it: a
|
||||
statically imported DLL that is merely absent kills the process
|
||||
at load time, before any window or log line (0xC0000135). Now
|
||||
nothing touches it until Steam is actually asked for, and
|
||||
SteamNetTransport_ClientLibraryPresent() gates the call sites so
|
||||
a missing DLL falls back to TCP instead of raising the
|
||||
delay-load helper's fatal exception. delayimp.lib supplies that
|
||||
helper. -->
|
||||
<DelayLoadDLLs>steam_api.dll;%(DelayLoadDLLs)</DelayLoadDLLs>
|
||||
<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 +127,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 +166,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" />
|
||||
|
||||
+351
-384
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user