Compare commits
196
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3229a82380 | ||
|
|
29ad567ffb | ||
|
|
9fae4406b5 | ||
|
|
b07e328553 | ||
|
|
879b76c6e1 | ||
|
|
b3df9f0804 | ||
|
|
5c597edf68 | ||
|
|
f86b604015 | ||
|
|
b5b2efab28 | ||
|
|
74948fdd10 | ||
|
|
021ad36951 | ||
|
|
3b5eb992f1 | ||
|
|
54738d3259 | ||
|
|
d02117d87c | ||
|
|
d9149a6a8c | ||
|
|
835580ab91 | ||
|
|
aaedbd1043 | ||
|
|
e9603f4218 | ||
|
|
6467a5f930 | ||
|
|
51be84e89c | ||
|
|
49d6d93414 | ||
|
|
0c8c62a73b | ||
|
|
7d485c9672 | ||
|
|
3d94ac7158 | ||
|
|
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 | ||
|
|
fd61316a40 | ||
|
|
dce8818273 | ||
|
|
a1f9c0e3c0 | ||
|
|
bfd5fa163e | ||
|
|
a52fec80a9 | ||
|
|
40b00ddde1 | ||
|
|
16ce4dfbea | ||
|
|
2a23ec0923 | ||
|
|
1efd5137d6 | ||
|
|
b97dcce3a2 | ||
|
|
f31c8401c7 | ||
|
|
e1a3ef7cf1 | ||
|
|
858fb7fb42 | ||
|
|
28df53aa31 | ||
|
|
461dcfb6b9 | ||
|
|
9389ec2003 | ||
|
|
3a24de03e6 | ||
|
|
e4025c2aec | ||
|
|
43cf086ea4 | ||
|
|
abd720dd6e | ||
|
|
0c696c9952 | ||
|
|
ea9491d2d5 | ||
|
|
8dc6605a07 | ||
|
|
6b43971d2d | ||
|
|
b6045f3c94 | ||
|
|
0bec0f9640 | ||
|
|
8fb4b72f7a | ||
|
|
00cb87907c | ||
|
|
53228686b4 | ||
|
|
1bd6dd83e2 | ||
|
|
1ba3bde36a | ||
|
|
4b25f89e6f | ||
|
|
3194d3f973 | ||
|
|
0b84b32486 | ||
|
|
0cf5a23d2e | ||
|
|
13424cce50 |
+38
@@ -40,6 +40,20 @@ ipch/
|
|||||||
*.log
|
*.log
|
||||||
rpl4.log
|
rpl4.log
|
||||||
|
|
||||||
|
# Where the game window and the exploded view's panes were last dragged
|
||||||
|
# to (RP412MFDLAYOUT). Per-machine by nature - it is screen coordinates
|
||||||
|
# on somebody's desk.
|
||||||
|
mfd_layout.cfg
|
||||||
|
|
||||||
|
# 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
|
# Build-output static libs that land in lib/ (the two committed dependency
|
||||||
# libs, OpenAL32.lib and libsndfile-1.lib, stay tracked).
|
# libs, OpenAL32.lib and libsndfile-1.lib, stay tracked).
|
||||||
/lib/Munga_L4.lib
|
/lib/Munga_L4.lib
|
||||||
@@ -67,3 +81,27 @@ assets/RP411/startrp-800x600.ps1
|
|||||||
# ...but never the spool runtime output or Windows thumbnail caches.
|
# ...but never the spool runtime output or Windows thumbnail caches.
|
||||||
assets/**/*.spl
|
assets/**/*.spl
|
||||||
assets/**/last.spl
|
assets/**/last.spl
|
||||||
|
|
||||||
|
# packaged releases (attached to Gitea releases, not tracked)
|
||||||
|
RedPlanet-*.zip
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ Committed under [lib/](lib/), no install needed: `OpenAL32.lib`, `libsndfile-1.l
|
|||||||
`assets/RP411/oalinst.exe` — or beside the exe) and `libsndfile-1.dll` (beside the
|
`assets/RP411/oalinst.exe` — or beside the exe) and `libsndfile-1.dll` (beside the
|
||||||
exe; a copy ships in `assets/RP411/`).
|
exe; a copy ships in `assets/RP411/`).
|
||||||
|
|
||||||
|
It also needs **`d3dx9_43.dll`**, which is a hard import and is *not* part of
|
||||||
|
Windows — it ships only with the June 2010 DirectX End-User Runtime. A dev box
|
||||||
|
with the SDK installed has it system-wide and never notices; a clean Windows
|
||||||
|
10/11 fails to start the exe at all (0xC0000135, before `WinMain`).
|
||||||
|
[pack-dist.ps1](pack-dist.ps1) therefore extracts it from the SDK's
|
||||||
|
`Redist\Jun2010_d3dx9_43_x86.cab` into `dist\`. That same file is also what
|
||||||
|
makes the game run under Wine/Proton, whose builtin `d3dx9_43` stubs
|
||||||
|
`D3DXConcatenateMeshes` — see [docs/RP412-LINUX.md](docs/RP412-LINUX.md).
|
||||||
|
|
||||||
## 2. Building
|
## 2. Building
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -50,10 +59,57 @@ The solution is [WinTesla.sln](WinTesla.sln) with four v143 projects:
|
|||||||
Build order is resolved by `ProjectReference` (RP_L4 and RPL4TOOL both reference
|
Build order is resolved by `ProjectReference` (RP_L4 and RPL4TOOL both reference
|
||||||
Munga_L4).
|
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
|
**Packaging:** [pack-dist.ps1](pack-dist.ps1) assembles a runnable game into
|
||||||
`dist\` (exe + PDB, game data, OpenAL/libsndfile runtimes, desktop
|
`dist\` (exe + PDB, game data, OpenAL/libsndfile runtimes, launch scripts,
|
||||||
`environ.ini`, `start-windowed.bat`, README). Pass `-Zip` to also produce
|
HANDBOOK.html, README). It deliberately does **not** write `environ.ini` —
|
||||||
`dist\RedPlanet412-prototype.zip` for handing to someone else.
|
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)
|
## 3. VS2022 migration notes (what changed and why)
|
||||||
|
|
||||||
@@ -97,10 +153,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`
|
the game boots to a running window with `-windowed -res 640 480 -egg TEST.EGG`
|
||||||
from a working copy like `assets/RP411/`.
|
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_165/sdk/redistributable_bin/steam_api.dll](extern/steamworks_sdk_165/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
|
For runtime debugging the v143 build produces full PDBs — run
|
||||||
`cdb -g -G -lines -y Release rpl4opt.exe ...` from the working directory
|
`cdb -g -G -lines -y Release rpl4opt.exe ...` from the working directory
|
||||||
(cdb ships in this machine's Windows Kits).
|
(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)
|
### Running without the cockpit (Workstream A prototype)
|
||||||
|
|
||||||
Two new environment options remove the hardware dependency entirely:
|
Two new environment options remove the hardware dependency entirely:
|
||||||
|
|||||||
+384
-2
@@ -16,6 +16,7 @@
|
|||||||
#include "console.h"
|
#include "console.h"
|
||||||
#include "appmsg.h"
|
#include "appmsg.h"
|
||||||
#include "evtstat.h"
|
#include "evtstat.h"
|
||||||
|
#include "inputscript.h"
|
||||||
|
|
||||||
#if defined(TRACE_FOREGROUND_PROCESSING)
|
#if defined(TRACE_FOREGROUND_PROCESSING)
|
||||||
BitTrace Foreground_Processing("Foreground Processing");
|
BitTrace Foreground_Processing("Foreground Processing");
|
||||||
@@ -32,6 +33,46 @@
|
|||||||
Application *application = NULL;
|
Application *application = NULL;
|
||||||
int Exit_Code = 0;
|
int Exit_Code = 0;
|
||||||
Logical Application::suppressGauges = False;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412NETLOG - the race-end network summary, on unless =0. The latch
|
||||||
|
// keeps the summary to exactly one print per mission: RunMission arms
|
||||||
|
// it, the first stop or abort fires it (see NetLogReportRace, further
|
||||||
|
// down beside the stop handlers).
|
||||||
|
//
|
||||||
|
static Logical gNetRaceReported = True;
|
||||||
|
|
||||||
|
static Logical
|
||||||
|
NetLogEnabled()
|
||||||
|
{
|
||||||
|
static int enabled = -1;
|
||||||
|
|
||||||
|
if (enabled < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412NETLOG");
|
||||||
|
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
||||||
|
}
|
||||||
|
return enabled ? True : False;
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//########################### Application ###############################
|
//########################### Application ###############################
|
||||||
@@ -273,6 +314,35 @@ Scalar
|
|||||||
return mgr->GetFrameRate();
|
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
|
// Initialize
|
||||||
@@ -577,6 +647,147 @@ Time startUpdate = Now();
|
|||||||
updateManager->Execute(start_of_frame);
|
updateManager->Execute(start_of_frame);
|
||||||
Time endUpdate = Now();
|
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();
|
CLEAR_UPDATE_MANAGER();
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -625,6 +836,14 @@ Time endInterest = Now();
|
|||||||
// renderer executes this frame.
|
// renderer executes this frame.
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
// Flush queued sends before the render: the present can block on
|
||||||
|
// vsync, and this frame's state updates should be on the wire while
|
||||||
|
// that happens, not behind it.
|
||||||
|
//
|
||||||
|
Check(networkManager);
|
||||||
|
networkManager->FlushSends();
|
||||||
|
|
||||||
SET_RENDERER_MANAGER();
|
SET_RENDERER_MANAGER();
|
||||||
Time startRender = Now();
|
Time startRender = Now();
|
||||||
Check(rendererManager);
|
Check(rendererManager);
|
||||||
@@ -649,8 +868,23 @@ Time endIntercom = Now();
|
|||||||
//
|
//
|
||||||
if (GetApplicationState() == RunningMission)
|
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;
|
routePacketFinished = False;
|
||||||
|
|
||||||
@@ -1125,6 +1359,12 @@ void
|
|||||||
Check(this);
|
Check(this);
|
||||||
Check(egg_notation_file);
|
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
|
// Create mission from egg notation file
|
||||||
@@ -1148,7 +1388,15 @@ void
|
|||||||
|
|
||||||
InterestManager *interest_mgr = GetInterestManager();
|
InterestManager *interest_mgr = GetInterestManager();
|
||||||
Check(interest_mgr);
|
Check(interest_mgr);
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: player data set - loading interest arenas\n" << std::flush;
|
||||||
|
}
|
||||||
interest_mgr->LoadInterestArenas(currentMission);
|
interest_mgr->LoadInterestArenas(currentMission);
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: interest arenas loaded - mission created\n" << std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1184,7 +1432,16 @@ void
|
|||||||
trace_manager.StartTimingAnalysis();
|
trace_manager.StartTimingAnalysis();
|
||||||
#endif
|
#endif
|
||||||
Check(interestManager);
|
Check(interestManager);
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: LoadMission handler - loading interest manager\n" << std::flush;
|
||||||
|
}
|
||||||
interestManager->LoadMission(currentMission);
|
interestManager->LoadMission(currentMission);
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: interest manager loaded - about to make the player\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -1354,6 +1611,13 @@ void
|
|||||||
case CreatingMission:
|
case CreatingMission:
|
||||||
case LoadingMission:
|
case LoadingMission:
|
||||||
case WaitingForLaunch:
|
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))
|
if (eventQueue->IsPriorityEmpty(MinEventPriority))
|
||||||
{
|
{
|
||||||
Host *console_host;
|
Host *console_host;
|
||||||
@@ -1406,6 +1670,11 @@ void
|
|||||||
RunMissionMessage run_mission_message;
|
RunMissionMessage run_mission_message;
|
||||||
Post(DefaultEventPriority, this, &run_mission_message);
|
Post(DefaultEventPriority, this, &run_mission_message);
|
||||||
Tell("Sent ready message to ourselves\n");
|
Tell("Sent ready message to ourselves\n");
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: no console - posted RunMission to ourselves\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1480,6 +1749,20 @@ void
|
|||||||
Tell("Application::RunMissionMessageHandler - running mission\n");
|
Tell("Application::RunMissionMessageHandler - running mission\n");
|
||||||
applicationState.SetState(RunningMission);
|
applicationState.SetState(RunningMission);
|
||||||
gameStarted = Now();
|
gameStarted = Now();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Mission t0, in this machine's ticks (RP412NETLOG). Every
|
||||||
|
// machine stamps its own t0 when this message is PROCESSED, so
|
||||||
|
// the mission clocks - and everything phase-derived from them,
|
||||||
|
// the clockwork doors included - skew by the one-way spread of
|
||||||
|
// the console's RunMission sends plus frame quantization. This
|
||||||
|
// line plus the console's per-pod send ticks plus the netclock
|
||||||
|
// offsets are what let the skew be computed offline for the
|
||||||
|
// first time (plan item M4).
|
||||||
|
//
|
||||||
|
DEBUG_STREAM << "NetLog: mission t0 at tick " << gameStarted.ticks
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
gNetRaceReported = False; // arm the race-end summary
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WaitingForLaunch:
|
case WaitingForLaunch:
|
||||||
@@ -1615,6 +1898,96 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// The race-end network summary (RP412NETLOG, on unless =0).
|
||||||
|
//
|
||||||
|
// Playtest logs used to carry ZERO player-symptom lines - every reported
|
||||||
|
// tick, warp and stall lived in commit prose while sixteen logs said
|
||||||
|
// nothing. These few lines are the fix: per-replicant race totals
|
||||||
|
// (accumulated ungated in Entity::netRaceStats) and the per-peer clock
|
||||||
|
// lines, printed once when the first stop or abort lands, before
|
||||||
|
// teardown. The latch (gNetRaceReported, defined up beside RPCameraLog)
|
||||||
|
// is armed by RunMission, so menu traffic never prints and a second
|
||||||
|
// stop cannot print twice.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// A percentile out of the log2-millisecond histogram, reported as the
|
||||||
|
// bucket's lower bound: "~32 ms" means the value fell in [32, 64).
|
||||||
|
//
|
||||||
|
static unsigned long
|
||||||
|
NetLogPercentileMs(
|
||||||
|
const unsigned long *buckets,
|
||||||
|
unsigned long total,
|
||||||
|
unsigned long percent)
|
||||||
|
{
|
||||||
|
if (total == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
unsigned long target = (total * percent + 99) / 100;
|
||||||
|
unsigned long seen = 0;
|
||||||
|
for (int b = 0; b < 16; ++b)
|
||||||
|
{
|
||||||
|
seen += buckets[b];
|
||||||
|
if (seen >= target)
|
||||||
|
{
|
||||||
|
return (b == 0) ? 0 : (1UL << (b - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1UL << 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
NetLogReportRace(Application *the_application)
|
||||||
|
{
|
||||||
|
if (gNetRaceReported || !NetLogEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gNetRaceReported = True;
|
||||||
|
|
||||||
|
DEBUG_STREAM << "NetLog: race summary at tick " << Now().ticks
|
||||||
|
<< ", elapsed " << the_application->GetMissionElapsed()
|
||||||
|
<< " s\n" << std::flush;
|
||||||
|
|
||||||
|
HostManager::DynamicReplicantEntityIterator
|
||||||
|
replicants(the_application->GetHostManager());
|
||||||
|
Entity *entity;
|
||||||
|
|
||||||
|
while ((entity = replicants.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
Check(entity);
|
||||||
|
Entity::NetRaceStats *stats = &entity->netRaceStats;
|
||||||
|
if (stats->updateCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "NetLog: pod " << entity->GetEntityID()
|
||||||
|
<< " host " << entity->GetOwnerID()
|
||||||
|
<< ": " << stats->updateCount << " updates"
|
||||||
|
<< ", interval med/p95 ~"
|
||||||
|
<< NetLogPercentileMs(stats->gapHistogram, stats->updateCount, 50)
|
||||||
|
<< "/~"
|
||||||
|
<< NetLogPercentileMs(stats->gapHistogram, stats->updateCount, 95)
|
||||||
|
<< " ms, widest " << stats->widestGapSeconds << " s, "
|
||||||
|
<< stats->longGapCount << " long / "
|
||||||
|
<< stats->queuedGapCount << " queued, "
|
||||||
|
<< stats->snapCount << " snaps, corrections "
|
||||||
|
<< stats->correctionCount;
|
||||||
|
if (stats->correctionCount > 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << " mean "
|
||||||
|
<< (stats->correctionTotal / (Scalar) stats->correctionCount)
|
||||||
|
<< " m worst " << stats->correctionWorst << " m";
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << ", stale " << stats->staleCount
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetClock_ReportRaceStats();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// StopMissionMessageHandler
|
// StopMissionMessageHandler
|
||||||
@@ -1633,6 +2006,12 @@ void
|
|||||||
Check(message);
|
Check(message);
|
||||||
Verify(message->messageID == StopMissionMessageID);
|
Verify(message->messageID == StopMissionMessageID);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The network race summary goes out on the FIRST stop, while the
|
||||||
|
// replicants still exist (teardown is downstream of here).
|
||||||
|
//
|
||||||
|
NetLogReportRace(this);
|
||||||
|
|
||||||
//
|
//
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// If the application is already stopping then ignore the message
|
// If the application is already stopping then ignore the message
|
||||||
@@ -1690,6 +2069,9 @@ void
|
|||||||
Check(message);
|
Check(message);
|
||||||
Verify(message->messageID == AbortMissionMessageID);
|
Verify(message->messageID == AbortMissionMessageID);
|
||||||
|
|
||||||
|
// same first-stop summary as StopMission - an aborted race counts
|
||||||
|
NetLogReportRace(this);
|
||||||
|
|
||||||
//
|
//
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// If the application is already stopping then ignore the message
|
// If the application is already stopping then ignore the message
|
||||||
|
|||||||
+51
@@ -56,6 +56,20 @@ class GeneralEventQueue;
|
|||||||
class Entity__MakeMessage;
|
class Entity__MakeMessage;
|
||||||
class ResourceFile;
|
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 ##############################
|
//######################### Application ##############################
|
||||||
//##########################################################################
|
//##########################################################################
|
||||||
@@ -318,6 +332,15 @@ public:
|
|||||||
Scalar
|
Scalar
|
||||||
GetSecondsRemainingInGame()
|
GetSecondsRemainingInGame()
|
||||||
{return secondsRemainingInGame;}
|
{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
|
ApplicationID
|
||||||
GetApplicationID()
|
GetApplicationID()
|
||||||
{return applicationID;}
|
{return applicationID;}
|
||||||
@@ -431,6 +454,32 @@ public:
|
|||||||
|
|
||||||
static Logical DoSuppressGauges() { return suppressGauges; }
|
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
|
// Modules
|
||||||
//
|
//
|
||||||
@@ -466,6 +515,8 @@ protected:
|
|||||||
*modeManager;
|
*modeManager;
|
||||||
|
|
||||||
static Logical suppressGauges;
|
static Logical suppressGauges;
|
||||||
|
static Logical cameraStation;
|
||||||
|
static Logical recordMission;
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Module Creation
|
// Module Creation
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ Logical gConsoleMarshalsLaunch = False;
|
|||||||
// losing the console mid-mission ends it (lobby-member races)
|
// losing the console mid-mission ends it (lobby-member races)
|
||||||
Logical gConsoleLossEndsMission = False;
|
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::CurrentAppManager = NULL;
|
||||||
|
|
||||||
ApplicationManager::ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate) : Node(ApplicationManagerClassID), runningApplications(this)
|
ApplicationManager::ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate) : Node(ApplicationManagerClassID), runningApplications(this)
|
||||||
@@ -231,6 +234,74 @@ Background_Loop:
|
|||||||
}
|
}
|
||||||
Time endBackground = Now();
|
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];
|
//char str[256];
|
||||||
//Scalar lastFrameLength = Now() - beginFrameTimestamp;
|
//Scalar lastFrameLength = Now() - beginFrameTimestamp;
|
||||||
//sprintf(str, "RPL4 - %.2f FPS", 1.0f / lastFrameLength);
|
//sprintf(str, "RPL4 - %.2f FPS", 1.0f / lastFrameLength);
|
||||||
|
|||||||
@@ -20,6 +20,28 @@ extern Logical gConsoleMarshalsLaunch;
|
|||||||
// console to return, exactly as always.
|
// console to return, exactly as always.
|
||||||
extern Logical gConsoleLossEndsMission;
|
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
|
class ApplicationManager : public Node
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
+10
-2
@@ -94,8 +94,16 @@ void
|
|||||||
}
|
}
|
||||||
headEntitySocket.Add(entity);
|
headEntitySocket.Add(entity);
|
||||||
|
|
||||||
alDistanceModel(AL_LINEAR_DISTANCE);
|
// FIDELITY (docs/SOUND.md F3/F10): the engine computes the AUTHORED distance
|
||||||
alDopplerFactor(0.3f);
|
// 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
|
#if 0
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
//############################# CameraShip ################################
|
//############################# 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
|
// Shared Data Support
|
||||||
//
|
//
|
||||||
@@ -320,6 +326,127 @@ void
|
|||||||
Point3D target;
|
Point3D target;
|
||||||
target.Multiply(focusOffset, goalEntity->localToWorld);
|
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
|
// 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)
|
if (currentCamera != old_camera)
|
||||||
{
|
{
|
||||||
|
++gCameraCuts;
|
||||||
AimCameraAtPoint(target);
|
AimCameraAtPoint(target);
|
||||||
lastSwitch = lastPerformance;
|
lastSwitch = lastPerformance;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-2
@@ -58,6 +58,23 @@
|
|||||||
|
|
||||||
#define Check(p)
|
#define Check(p)
|
||||||
#define Check_Signature(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))
|
#define Cast_Object(type, ptr) ((type)(ptr))
|
||||||
+65
-140
@@ -72,172 +72,96 @@ Door::AttributeIndexSet& Door::GetAttributeIndex()
|
|||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Model Support
|
// Model Support
|
||||||
//
|
//
|
||||||
void
|
// There is no ReadUpdateRecord/WriteUpdateRecord pair here on purpose. Doors
|
||||||
Door::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
// 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
|
||||||
Check(this);
|
// decides where a door is, and it reaches the same answer everywhere.
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
Door::WriteUpdateRecord(Simulation::UpdateRecord *record, int update_model)
|
Door::SlideDoor(Scalar)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
Check(this);
|
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;
|
// - every machine puts this door in the same place from the same mission
|
||||||
if (time_slice > 1.0f)
|
// 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();
|
Check_Fpu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
phaseTimeRemaining -= time_slice;
|
|
||||||
Scalar percent_open;
|
Check(application);
|
||||||
switch (GetSimulationState())
|
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
|
// Pick the band. Each division below is guarded by the comparison that
|
||||||
// to the opened state
|
// selected the branch, so a door with a zero travelTime or deadTime simply
|
||||||
//------------------------------------------------------------------------
|
// loses that band rather than dividing by zero.
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
case Opening:
|
Scalar open_start = travelTime + deadTime;
|
||||||
Door_Opening:
|
int new_state;
|
||||||
new_state = Opening;
|
Scalar percent_open;
|
||||||
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;
|
|
||||||
|
|
||||||
//
|
if (phase < travelTime)
|
||||||
//-------------------------------------------------------------
|
{
|
||||||
// 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:
|
|
||||||
new_state = Closing;
|
new_state = Closing;
|
||||||
if (phaseTimeRemaining > 0.0f)
|
percent_open = 1.0f - phase/travelTime;
|
||||||
{
|
|
||||||
percent_open = phaseTimeRemaining/travelTime;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
phaseTimeRemaining += deadTime;
|
|
||||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closed @ "
|
|
||||||
// << application->GetSecondsRemainingInGame() << endl;
|
|
||||||
goto Door_Closed;
|
|
||||||
}
|
|
||||||
currentVelocity.Subtract(
|
currentVelocity.Subtract(
|
||||||
GetEntity()->localOrigin.linearPosition,
|
GetEntity()->localOrigin.linearPosition,
|
||||||
worldExtent
|
worldExtent
|
||||||
);
|
);
|
||||||
currentVelocity /= travelTime;
|
currentVelocity /= travelTime;
|
||||||
Check_Fpu();
|
}
|
||||||
break;
|
else if (phase < open_start)
|
||||||
|
{
|
||||||
//
|
|
||||||
//-------------------------------------------------------------
|
|
||||||
// If the door is ready to start opening, jump to opening state
|
|
||||||
//-------------------------------------------------------------
|
|
||||||
//
|
|
||||||
case Closed:
|
|
||||||
Door_Closed:
|
|
||||||
new_state = Closed;
|
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;
|
percent_open = 0.0f;
|
||||||
currentVelocity = Vector3D::Identity;
|
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
|
// Initialize variables
|
||||||
//
|
//
|
||||||
phaseTimeRemaining = 0.0f;
|
phaseOffset = 0.0f;
|
||||||
|
cycleTime = 2.0f*(travelTime + deadTime);
|
||||||
currentPosition = Point3D::Identity;
|
currentPosition = Point3D::Identity;
|
||||||
|
|
||||||
SetPerformance(&Door::SlideDoor);
|
SetPerformance(&Door::SlideDoor);
|
||||||
|
|||||||
+16
-19
@@ -20,16 +20,11 @@ struct Door__SubsystemResource:
|
|||||||
collisionID;
|
collisionID;
|
||||||
};
|
};
|
||||||
|
|
||||||
//##########################################################################
|
//
|
||||||
//##################### Chute::UpdateRecord #####################
|
// 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.
|
||||||
struct Door__UpdateRecord :
|
//
|
||||||
public Subsystem::UpdateRecord
|
|
||||||
{
|
|
||||||
Scalar
|
|
||||||
percentOpen;
|
|
||||||
};
|
|
||||||
|
|
||||||
//##########################################################################
|
//##########################################################################
|
||||||
//######################### CLASS Door ########################
|
//######################### CLASS Door ########################
|
||||||
@@ -91,7 +86,6 @@ public:
|
|||||||
|
|
||||||
typedef void
|
typedef void
|
||||||
(Door::*Performance)(Scalar time_slice);
|
(Door::*Performance)(Scalar time_slice);
|
||||||
typedef Door__UpdateRecord UpdateRecord;
|
|
||||||
|
|
||||||
void
|
void
|
||||||
SetPerformance(Performance performance)
|
SetPerformance(Performance performance)
|
||||||
@@ -109,12 +103,6 @@ public:
|
|||||||
GetFirstBoxedSolid()
|
GetFirstBoxedSolid()
|
||||||
{Check(this); return collisionVolumes;}
|
{Check(this); return collisionVolumes;}
|
||||||
|
|
||||||
protected:
|
|
||||||
void
|
|
||||||
WriteUpdateRecord(Simulation::UpdateRecord *message, int update_model);
|
|
||||||
void
|
|
||||||
ReadUpdateRecord(Simulation::UpdateRecord *message);
|
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Construction and Destruction
|
// Construction and Destruction
|
||||||
//
|
//
|
||||||
@@ -152,9 +140,18 @@ private:
|
|||||||
worldExtent;
|
worldExtent;
|
||||||
|
|
||||||
Scalar
|
Scalar
|
||||||
phaseTimeRemaining,
|
|
||||||
travelTime,
|
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;
|
int collisionVolumeCount;
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -126,8 +126,15 @@ Logical
|
|||||||
}
|
}
|
||||||
|
|
||||||
creation_message->classToCreate = RegisteredClass::DoorFrameClassID;
|
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 =
|
creation_message->instanceFlags =
|
||||||
MasterInstance|DynamicFlag|MapFlag|TrappedFlag;
|
HermitInstance|DynamicFlag|MapFlag|TrappedFlag;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-1
@@ -224,9 +224,47 @@ void
|
|||||||
//-----------------------------------------------------
|
//-----------------------------------------------------
|
||||||
//
|
//
|
||||||
highest = highest - lowest + 1;
|
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)
|
while (remaining)
|
||||||
{
|
{
|
||||||
i = lowest + Random(highest);
|
if (tryPinnedZone)
|
||||||
|
{
|
||||||
|
tryPinnedZone = False;
|
||||||
|
i = lowest + (pinnedZone % highest);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
i = lowest + Random(highest);
|
||||||
|
}
|
||||||
Verify(i < dropZoneCount && i >= 0);
|
Verify(i < dropZoneCount && i >= 0);
|
||||||
if (IsAvailable(i))
|
if (IsAvailable(i))
|
||||||
{
|
{
|
||||||
|
|||||||
+372
-1
@@ -377,8 +377,28 @@ void
|
|||||||
//------------------------------------------------------------------------
|
//------------------------------------------------------------------------
|
||||||
// Step through each block until there are no more remaining, and send the
|
// Step through each block until there are no more remaining, and send the
|
||||||
// update out the the simulation indicated by the subsystemID
|
// 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())
|
while (stream.GetBytesRemaining())
|
||||||
{
|
{
|
||||||
Simulation::UpdateRecord *update =
|
Simulation::UpdateRecord *update =
|
||||||
@@ -386,9 +406,28 @@ void
|
|||||||
Check_Pointer(update);
|
Check_Pointer(update);
|
||||||
Simulation *simulation = GetSimulation((int)update->subsystemID-1);
|
Simulation *simulation = GetSimulation((int)update->subsystemID-1);
|
||||||
Check(simulation);
|
Check(simulation);
|
||||||
simulation->ReadUpdateRecord(update);
|
|
||||||
|
//
|
||||||
|
// Skip a record the sender stamped before the one we already
|
||||||
|
// applied - see Simulation::AcceptUpdateStamp. Counted, because
|
||||||
|
// on an ordered stream this should never fire and the number
|
||||||
|
// saying so is the point of it.
|
||||||
|
//
|
||||||
|
if (simulation->AcceptUpdateStamp(update->timeStamp))
|
||||||
|
{
|
||||||
|
simulation->ReadUpdateRecord(update);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
netRaceStats.staleCount++;
|
||||||
|
}
|
||||||
stream.AdvancePointer(update->recordLength);
|
stream.AdvancePointer(update->recordLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remote_owner)
|
||||||
|
{
|
||||||
|
NetClock_EndUpdate();
|
||||||
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,6 +449,83 @@ void
|
|||||||
//-----------------------
|
//-----------------------
|
||||||
//
|
//
|
||||||
UpdateRecord *update = (UpdateRecord*)record;
|
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.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Race totals for the RP412NETLOG summary - per entity,
|
||||||
|
// ungated (one subtract and a length per arriving update),
|
||||||
|
// where the CamLog block below is windowed, gated, and
|
||||||
|
// shared across all replicants.
|
||||||
|
//
|
||||||
|
if (GetInstance() == ReplicantInstance)
|
||||||
|
{
|
||||||
|
Vector3D race_drift;
|
||||||
|
race_drift.Subtract(
|
||||||
|
update->localOrigin.linearPosition,
|
||||||
|
localOrigin.linearPosition
|
||||||
|
);
|
||||||
|
Scalar race_distance = race_drift.Length();
|
||||||
|
netRaceStats.correctionCount++;
|
||||||
|
netRaceStats.correctionTotal += race_distance;
|
||||||
|
if (race_distance > netRaceStats.correctionWorst)
|
||||||
|
{
|
||||||
|
netRaceStats.correctionWorst = race_distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
updateOrigin = update->localOrigin;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -426,6 +542,13 @@ void
|
|||||||
{
|
{
|
||||||
localOrigin = updateOrigin;
|
localOrigin = updateOrigin;
|
||||||
localToWorld = localOrigin;
|
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);
|
Simulation::ReadUpdateRecord(record);
|
||||||
}
|
}
|
||||||
@@ -720,6 +843,118 @@ void
|
|||||||
//
|
//
|
||||||
if (GetInstance() != ReplicantInstance)
|
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)
|
for (int i=0; i<subsystemCount; ++i)
|
||||||
{
|
{
|
||||||
if (subsystemArray[i])
|
if (subsystemArray[i])
|
||||||
@@ -937,6 +1172,16 @@ Entity::Entity(
|
|||||||
updateOrigin = localOrigin;
|
updateOrigin = localOrigin;
|
||||||
localToWorld = 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
|
// initialize camera stuff
|
||||||
cameraOffset = Origin::Identity;
|
cameraOffset = Origin::Identity;
|
||||||
|
|
||||||
@@ -1238,6 +1483,45 @@ void
|
|||||||
//
|
//
|
||||||
if (!IsCondemned())
|
if (!IsCondemned())
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Stop being anybody's vehicle before going on the row.
|
||||||
|
//
|
||||||
|
// Player::playerVehicle is a bare pointer that nothing ever
|
||||||
|
// cleared. The task that empties this row deletes one entity per
|
||||||
|
// frame, and a replicant Player runs GoToVehicle EVERY frame -
|
||||||
|
// it reads playerVehicle->localOrigin to follow its pod around.
|
||||||
|
// So from the moment the vehicle was fried, every one of those
|
||||||
|
// frames read freed memory. It normally survives on the garbage
|
||||||
|
// still lying in the block, which is its own kind of bug: a
|
||||||
|
// remote pod following coordinates that are no longer anything.
|
||||||
|
//
|
||||||
|
// Caught by the podium rig under full page heap, where the block
|
||||||
|
// is decommitted instead of merely stale: an access violation in
|
||||||
|
// Player::GoToVehicle reading a VTV whose free stack was
|
||||||
|
// FryDeathRowTask::Execute -> VTV::`scalar deleting destructor'.
|
||||||
|
//
|
||||||
|
// Cleared here rather than in ~Entity: the entity is still whole
|
||||||
|
// at this point, the groups are all still standing, and it
|
||||||
|
// happens exactly once. A destructor doing this would run during
|
||||||
|
// Shutdown too, when the Players group may already be gone.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
//
|
||||||
|
EntityGroup *player_group = entity_manager->FindGroup("Players");
|
||||||
|
if (player_group != NULL)
|
||||||
|
{
|
||||||
|
ChainIteratorOf<Node*> iterator(player_group->groupMembers);
|
||||||
|
Player *player;
|
||||||
|
|
||||||
|
while ((player = (Player*) iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
if (player->GetPlayerVehicle() == this)
|
||||||
|
{
|
||||||
|
player->ClearPlayerVehicle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entity_manager->deathRow->Add(this);
|
entity_manager->deathRow->Add(this);
|
||||||
SetCondemnedFlag();
|
SetCondemnedFlag();
|
||||||
NeverExecute();
|
NeverExecute();
|
||||||
@@ -1255,6 +1539,93 @@ Logical
|
|||||||
return IsDerivedFrom(*GetClassDerivations());
|
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
|
// Renderer Support
|
||||||
//
|
//
|
||||||
|
|||||||
+134
@@ -141,6 +141,140 @@ public:
|
|||||||
int damageZoneCount;
|
int damageZoneCount;
|
||||||
DamageZone **damageZones;
|
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;
|
||||||
|
|
||||||
|
//######################################################################
|
||||||
|
//################ Race-total network statistics ###################
|
||||||
|
//######################################################################
|
||||||
|
//
|
||||||
|
// Whole-race arrival statistics for a REPLICANT entity, accumulated
|
||||||
|
// ungated (the arithmetic is a subtract and a length per arriving
|
||||||
|
// update) and printed once at mission end under RP412NETLOG - so a
|
||||||
|
// playtest log finally carries the symptoms instead of nothing. The
|
||||||
|
// windowed CamLog counters near these update sites reset every trace
|
||||||
|
// print and cover one latched entity; these cover every replicant for
|
||||||
|
// the whole race.
|
||||||
|
//
|
||||||
|
// gapHistogram is log2 milliseconds: bucket 0 is a sub-millisecond
|
||||||
|
// gap, bucket b covers [2^(b-1), 2^b) ms, bucket 15 collects
|
||||||
|
// everything from ~16 s up. Median and p95 fall out of a cumulative
|
||||||
|
// walk at print time.
|
||||||
|
//
|
||||||
|
class NetRaceStats
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
unsigned long updateCount; // updates applied to this entity
|
||||||
|
Scalar widestGapSeconds; // worst inter-arrival gap (<10 s stream)
|
||||||
|
unsigned long longGapCount; // gaps > 0.200 s (sender quiet)
|
||||||
|
unsigned long queuedGapCount; // gaps < 0.005 s (our loop stalled)
|
||||||
|
unsigned long snapCount; // lerp->snap transitions while flowing
|
||||||
|
unsigned long staleCount; // rejected out-of-order records (0
|
||||||
|
// until the stale guard ships)
|
||||||
|
unsigned long correctionCount; // arriving updates that moved us
|
||||||
|
Scalar correctionTotal; // metres, for the mean
|
||||||
|
Scalar correctionWorst; // metres
|
||||||
|
unsigned long gapHistogram[16];
|
||||||
|
Logical wasSnapped; // edge detector for snapCount
|
||||||
|
|
||||||
|
NetRaceStats():
|
||||||
|
updateCount(0),
|
||||||
|
widestGapSeconds(0.0f),
|
||||||
|
longGapCount(0),
|
||||||
|
queuedGapCount(0),
|
||||||
|
snapCount(0),
|
||||||
|
staleCount(0),
|
||||||
|
correctionCount(0),
|
||||||
|
correctionTotal(0.0f),
|
||||||
|
correctionWorst(0.0f),
|
||||||
|
wasSnapped(False)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 16; ++i)
|
||||||
|
{
|
||||||
|
gapHistogram[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
CountGap(Scalar seconds)
|
||||||
|
{
|
||||||
|
if (seconds > widestGapSeconds)
|
||||||
|
{
|
||||||
|
widestGapSeconds = seconds;
|
||||||
|
}
|
||||||
|
unsigned long ms = (unsigned long) (seconds * 1000.0f);
|
||||||
|
int bucket = 0;
|
||||||
|
while (ms != 0 && bucket < 15)
|
||||||
|
{
|
||||||
|
ms >>= 1;
|
||||||
|
++bucket;
|
||||||
|
}
|
||||||
|
gapHistogram[bucket]++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NetRaceStats netRaceStats;
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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
|
int
|
||||||
GetDamageZoneIndex(const CString &damage_zone_name) const;
|
GetDamageZoneIndex(const CString &damage_zone_name) const;
|
||||||
|
|||||||
@@ -690,6 +690,23 @@ void
|
|||||||
DEBUG_STREAM << "." << std::flush;
|
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)
|
if (profileCycles > 0)
|
||||||
{
|
{
|
||||||
Scalar
|
Scalar
|
||||||
|
|||||||
+100
-1
@@ -21,6 +21,31 @@
|
|||||||
BitTrace Gauge_Renderer("Gauge Renderer");
|
BitTrace Gauge_Renderer("Gauge Renderer");
|
||||||
#endif
|
#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
|
// Miscellaneous utilities
|
||||||
//#######################################################################
|
//#######################################################################
|
||||||
@@ -3672,6 +3697,60 @@ Logical
|
|||||||
Logical
|
Logical
|
||||||
result;
|
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;
|
Time start, end;
|
||||||
int oldTaskMode = taskMode;
|
int oldTaskMode = taskMode;
|
||||||
|
|
||||||
@@ -3683,7 +3762,27 @@ Logical
|
|||||||
|
|
||||||
case background:
|
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;
|
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
|
// 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;
|
Logical post_make_message = True;
|
||||||
if (Entity::EntityFlagsIsMap(message->instanceFlags))
|
if (
|
||||||
|
Entity::EntityFlagsIsMap(message->instanceFlags)
|
||||||
|
&& message->classToCreate != DoorFrameClassID
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Check(application);
|
Check(application);
|
||||||
HostManager *host_manager = application->GetHostManager();
|
HostManager *host_manager = application->GetHostManager();
|
||||||
|
|||||||
@@ -7,10 +7,315 @@
|
|||||||
#include "notation.h"
|
#include "notation.h"
|
||||||
#include "namelist.h"
|
#include "namelist.h"
|
||||||
|
|
||||||
|
// EXCEPTION_EXECUTE_HANDLER, for the guarded read in RPReadDword below.
|
||||||
|
// excpt.h rather than windows.h: this layer does not otherwise want it.
|
||||||
|
#include <excpt.h>
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//############################ JointedMover #############################
|
//############################ JointedMover #############################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
|
|
||||||
|
//#############################################################################
|
||||||
|
// RP412SEGCHECK - segment validity at teardown
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The podium crash (three identical dumps, 2026-08-11) dies inside
|
||||||
|
// SocketIterator::DeletePlugs, called from ~JointedMover, calling through a
|
||||||
|
// segment whose FIRST DWORD - the vtable pointer - has been replaced by a
|
||||||
|
// small float (9.75 / 12.80 / 7.52 across the three machines). The table
|
||||||
|
// itself was untouched: numItems still 15, every TableEntry hooked up, which
|
||||||
|
// means nothing ever ran delete against that segment. Something wrote over
|
||||||
|
// it, or freed it behind the collection's back.
|
||||||
|
//
|
||||||
|
// Six configurations of the local repro rig - parked pods, driven pods,
|
||||||
|
// light page heap, full page heap, two, four and six pods - reached the
|
||||||
|
// podium and tore down clean. So this exists to make the next REAL playtest
|
||||||
|
// the instrument: look at each segment before calling through it, and if one
|
||||||
|
// is wrong, write what it looks like to rpl4-fail.log, which is fclose'd and
|
||||||
|
// survives the abort. Text written only to rpl4.log does not.
|
||||||
|
//
|
||||||
|
// This deliberately does NOT skip the delete, repair the pointer, or
|
||||||
|
// otherwise make the teardown survivable. The ownership bug is still
|
||||||
|
// unfixed, and a guard that hid it would cost exactly the evidence this is
|
||||||
|
// here to collect. It stops on the same object the crash would have died on,
|
||||||
|
// one step earlier, holding the forensics.
|
||||||
|
//
|
||||||
|
// RP412SEGCHECK=0 turns it off.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// The vtable every EntitySegment should carry, taken from the first one ever
|
||||||
|
// built - i.e. from fresh memory, before any race has run. Comparing against
|
||||||
|
// a captured value rather than a computed one keeps this independent of how
|
||||||
|
// the linker laid the image out.
|
||||||
|
//
|
||||||
|
static void
|
||||||
|
*gGoodSegmentVtable = NULL;
|
||||||
|
|
||||||
|
static int
|
||||||
|
RPSegCheckEnabled()
|
||||||
|
{
|
||||||
|
static int
|
||||||
|
enabled = -1;
|
||||||
|
|
||||||
|
if (enabled < 0)
|
||||||
|
{
|
||||||
|
const char
|
||||||
|
*setting = getenv("RP412SEGCHECK");
|
||||||
|
|
||||||
|
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
||||||
|
}
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Read a dword that may not be there any more. If the block has been freed
|
||||||
|
// under a page-heap build the pages are decommitted, and the plain read
|
||||||
|
// would fault inside the checker - losing the report we came for.
|
||||||
|
//
|
||||||
|
static int
|
||||||
|
RPReadDword(const void *address, unsigned long *out)
|
||||||
|
{
|
||||||
|
__try
|
||||||
|
{
|
||||||
|
*out = *(const unsigned long *) address;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// WHERE the corruption was first seen. Recorded in a static rather than
|
||||||
|
// simply logged, because rpl4.log does not reliably survive the abort that
|
||||||
|
// follows at teardown - so the answer has to travel inside the Fail report.
|
||||||
|
//
|
||||||
|
// This is the question the dumps cannot answer: the crash proves a segment
|
||||||
|
// is wrong by teardown, not whether it was already wrong when the race
|
||||||
|
// ended. Bracketing the podium separates "something during the race broke
|
||||||
|
// it" from "the podium's second MakeEntityRenderables broke it".
|
||||||
|
//
|
||||||
|
static char
|
||||||
|
gFirstBadWhen[64] = "";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public entry point for the bracket call sites (see RPL4APP.cpp). Counts
|
||||||
|
// corrupted segments on one entity and remembers the first moment any were
|
||||||
|
// seen. Reports; never aborts - the teardown check does that, with this
|
||||||
|
// answer folded in.
|
||||||
|
//
|
||||||
|
int
|
||||||
|
RPCheckJointedMoverSegments(Entity *entity, const char *when)
|
||||||
|
{
|
||||||
|
if (!RPSegCheckEnabled() || gGoodSegmentVtable == NULL || entity == NULL)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!entity->IsDerivedFrom(*JointedMover::GetClassDerivations()))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
JointedMover
|
||||||
|
*mover = (JointedMover *) entity;
|
||||||
|
EntitySegment::SegmentTableIterator
|
||||||
|
iterator(mover->segmentTable);
|
||||||
|
EntitySegment
|
||||||
|
*segment;
|
||||||
|
int
|
||||||
|
bad = 0;
|
||||||
|
|
||||||
|
iterator.First();
|
||||||
|
while ((segment = iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
unsigned long
|
||||||
|
vtable = 0;
|
||||||
|
|
||||||
|
if (!RPReadDword(segment, &vtable)
|
||||||
|
|| (void *) vtable != gGoodSegmentVtable)
|
||||||
|
{
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bad > 0 && gFirstBadWhen[0] == '\0')
|
||||||
|
{
|
||||||
|
strncpy(gFirstBadWhen, when, sizeof(gFirstBadWhen) - 1);
|
||||||
|
gFirstBadWhen[sizeof(gFirstBadWhen) - 1] = '\0';
|
||||||
|
DEBUG_STREAM << "SegCheck: " << bad
|
||||||
|
<< " segment(s) already corrupt at " << when << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
return bad;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
RPCheckSegmentsBeforeTeardown(
|
||||||
|
Entity *mover,
|
||||||
|
EntitySegment::SegmentTable &segment_table
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!RPSegCheckEnabled() || gGoodSegmentVtable == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Walk the table WITHOUT dereferencing anything: the iterator reads the
|
||||||
|
// TableEntry array, and GetPlug hands back the pointer without touching
|
||||||
|
// the object. The only read of segment memory is the guarded one below.
|
||||||
|
//
|
||||||
|
EntitySegment::SegmentTableIterator
|
||||||
|
iterator(segment_table);
|
||||||
|
EntitySegment
|
||||||
|
*segment,
|
||||||
|
*bad_segment = NULL,
|
||||||
|
*before_bad = NULL,
|
||||||
|
*after_bad = NULL;
|
||||||
|
unsigned long
|
||||||
|
bad_value = 0;
|
||||||
|
int
|
||||||
|
index = 0,
|
||||||
|
total = 0,
|
||||||
|
bad_count = 0,
|
||||||
|
bad_index = -1,
|
||||||
|
bad_readable = 0;
|
||||||
|
|
||||||
|
iterator.First();
|
||||||
|
while ((segment = iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
unsigned long
|
||||||
|
vtable = 0;
|
||||||
|
int
|
||||||
|
readable = RPReadDword(segment, &vtable);
|
||||||
|
|
||||||
|
if (!readable || (void *) vtable != gGoodSegmentVtable)
|
||||||
|
{
|
||||||
|
++bad_count;
|
||||||
|
if (bad_segment == NULL)
|
||||||
|
{
|
||||||
|
bad_segment = segment;
|
||||||
|
bad_index = index;
|
||||||
|
bad_value = vtable;
|
||||||
|
bad_readable = readable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (bad_segment == NULL)
|
||||||
|
{
|
||||||
|
before_bad = segment; // last good one before the first bad
|
||||||
|
}
|
||||||
|
else if (after_bad == NULL)
|
||||||
|
{
|
||||||
|
after_bad = segment; // first one after it
|
||||||
|
}
|
||||||
|
++index;
|
||||||
|
++total;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bad_count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// One buffer, one message: Fail_With_Message fprintf's the string it is
|
||||||
|
// given into rpl4-fail.log and closes the file, so everything worth
|
||||||
|
// having has to be inside it. Newlines are fine.
|
||||||
|
//
|
||||||
|
static char
|
||||||
|
report[1400];
|
||||||
|
char
|
||||||
|
*out = report;
|
||||||
|
Entity
|
||||||
|
*viewpoint = (application != NULL)
|
||||||
|
? application->GetViewpointEntity()
|
||||||
|
: NULL;
|
||||||
|
|
||||||
|
out += sprintf(out,
|
||||||
|
"segment plug corrupted BEFORE DeletePlugs\n"
|
||||||
|
" first seen: %s\n"
|
||||||
|
" entity %d:%d class%d, %s pod, owner %d\n"
|
||||||
|
" segments %d, corrupted %d, first bad index %d at 0x%08lX\n",
|
||||||
|
(gFirstBadWhen[0] != '\0') ? gFirstBadWhen
|
||||||
|
: "teardown (clean at every earlier check)",
|
||||||
|
(int) mover->entityID.GetHostID(), (int) mover->entityID,
|
||||||
|
(int) mover->GetClassID(),
|
||||||
|
(mover == viewpoint) ? "VIEWPOINT (local)" : "replicant",
|
||||||
|
(int) mover->GetOwnerID(),
|
||||||
|
total, bad_count, bad_index, (unsigned long) bad_segment);
|
||||||
|
|
||||||
|
if (!bad_readable)
|
||||||
|
{
|
||||||
|
out += sprintf(out,
|
||||||
|
" segment memory is NOT READABLE - freed and decommitted\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float
|
||||||
|
as_float;
|
||||||
|
|
||||||
|
memcpy(&as_float, &bad_value, sizeof(as_float));
|
||||||
|
out += sprintf(out,
|
||||||
|
" vtable expected 0x%08lX found 0x%08lX (as float %.6g)\n",
|
||||||
|
(unsigned long) gGoodSegmentVtable, bad_value, (double) as_float);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The first 8 dwords, hex and float. The dumps showed a float where
|
||||||
|
// the vtable belongs; how FAR the float data runs says whether this
|
||||||
|
// was a stray single write or something walking through the object.
|
||||||
|
//
|
||||||
|
for (int row = 0; row < 2; ++row)
|
||||||
|
{
|
||||||
|
char
|
||||||
|
hex[80],
|
||||||
|
flt[80];
|
||||||
|
int
|
||||||
|
hex_used = 0,
|
||||||
|
flt_used = 0;
|
||||||
|
|
||||||
|
for (int col = 0; col < 4; ++col)
|
||||||
|
{
|
||||||
|
const unsigned char
|
||||||
|
*at = (const unsigned char *) bad_segment
|
||||||
|
+ (row * 16) + (col * 4);
|
||||||
|
unsigned long
|
||||||
|
word = 0;
|
||||||
|
|
||||||
|
if (!RPReadDword(at, &word))
|
||||||
|
{
|
||||||
|
hex_used += sprintf(hex + hex_used, " ????????");
|
||||||
|
flt_used += sprintf(flt + flt_used, " ????????");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float
|
||||||
|
word_float;
|
||||||
|
|
||||||
|
memcpy(&word_float, &word, sizeof(word_float));
|
||||||
|
hex_used += sprintf(hex + hex_used, " %08lX", word);
|
||||||
|
flt_used += sprintf(flt + flt_used, " %9.4g", (double) word_float);
|
||||||
|
}
|
||||||
|
out += sprintf(out, " +%02X:%s |%s\n", row * 16, hex, flt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Heap adjacency: if the corruption came from a neighbour running past
|
||||||
|
// its end, the deltas say which side it came from and how far it reached.
|
||||||
|
//
|
||||||
|
out += sprintf(out,
|
||||||
|
" neighbours: prev good 0x%08lX (delta %ld), next 0x%08lX (delta %ld)\n",
|
||||||
|
(unsigned long) before_bad,
|
||||||
|
(before_bad != NULL)
|
||||||
|
? (long) ((char *) bad_segment - (char *) before_bad) : 0L,
|
||||||
|
(unsigned long) after_bad,
|
||||||
|
(after_bad != NULL)
|
||||||
|
? (long) ((char *) after_bad - (char *) bad_segment) : 0L);
|
||||||
|
|
||||||
|
Fail(report);
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Shared Data Support
|
// Shared Data Support
|
||||||
//
|
//
|
||||||
@@ -307,6 +612,17 @@ JointedMover::JointedMover(
|
|||||||
primary_dzone_index
|
primary_dzone_index
|
||||||
);
|
);
|
||||||
Register_Object(current_segment);
|
Register_Object(current_segment);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The reference vtable for RPCheckSegmentsBeforeTeardown, taken once
|
||||||
|
// from the very first segment ever built - fresh memory, before any
|
||||||
|
// race has run. See the RP412SEGCHECK block at the top of this file.
|
||||||
|
//
|
||||||
|
if (gGoodSegmentVtable == NULL)
|
||||||
|
{
|
||||||
|
gGoodSegmentVtable = *(void **) current_segment;
|
||||||
|
}
|
||||||
|
|
||||||
segmentTable.AddValue(current_segment, ii);
|
segmentTable.AddValue(current_segment, ii);
|
||||||
//
|
//
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -429,6 +745,13 @@ void
|
|||||||
//
|
//
|
||||||
JointedMover::~JointedMover()
|
JointedMover::~JointedMover()
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Look at the segments before calling through them. This is where the
|
||||||
|
// podium crash lands; see the RP412SEGCHECK block at the top of this
|
||||||
|
// file for what it records and why it does not try to survive.
|
||||||
|
//
|
||||||
|
RPCheckSegmentsBeforeTeardown((Entity *) this, segmentTable);
|
||||||
|
|
||||||
EntitySegment::SegmentTableIterator iterator(segmentTable);
|
EntitySegment::SegmentTableIterator iterator(segmentTable);
|
||||||
iterator.DeletePlugs();
|
iterator.DeletePlugs();
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
#include "slot.h"
|
#include "slot.h"
|
||||||
#include "segment.h"
|
#include "segment.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412SEGCHECK - count corrupted segment plugs on one entity and remember
|
||||||
|
// the first moment any were seen, so the teardown report can say WHERE it
|
||||||
|
// went wrong rather than only that it did. Safe on any entity: returns 0
|
||||||
|
// for anything that is not a JointedMover. Reports; never aborts.
|
||||||
|
// See the block comment at the top of JMOVER.cpp.
|
||||||
|
//
|
||||||
|
extern int
|
||||||
|
RPCheckJointedMoverSegments(Entity *entity, const char *when);
|
||||||
|
|
||||||
//##############################################################################
|
//##############################################################################
|
||||||
//######################## JointedMover ##################################
|
//######################## JointedMover ##################################
|
||||||
//##############################################################################
|
//##############################################################################
|
||||||
|
|||||||
+753
-5
@@ -12,6 +12,165 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "notation.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;
|
||||||
|
|
||||||
|
//
|
||||||
|
// How far a replicant will coast on a sender that has gone quiet.
|
||||||
|
//
|
||||||
|
// Past the moment the next update was due, velocity times elapsed time
|
||||||
|
// stops being a prediction and becomes a guess that nobody is correcting.
|
||||||
|
// The field logs bear it out: gaps of one to two seconds on every pod in
|
||||||
|
// an eight-minute race, and worst-case corrections of one to two and a
|
||||||
|
// half KILOMETRES against a mean under a metre. That is a pod that flew
|
||||||
|
// most of the way across the map on stale velocity and then got yanked
|
||||||
|
// back - through whatever it passed through on the way.
|
||||||
|
//
|
||||||
|
// So it coasts for a second and then parks, waiting to be told. A pod
|
||||||
|
// that stops and then snaps once reads as a dropped connection, which is
|
||||||
|
// what it is; a pod sliding confidently through scenery reads as a
|
||||||
|
// broken game. Parking is also far cheaper to collide with.
|
||||||
|
//
|
||||||
|
// MUST MATCH on every machine in a race. This moves replicants rather
|
||||||
|
// than merely drawing them, so it decides where they collide - the same
|
||||||
|
// class as RP412NETPREDICT, and the lobby's build guard already keeps a
|
||||||
|
// mismatched build out of the room.
|
||||||
|
//
|
||||||
|
static const Scalar kMaximumExtrapolationSeconds = 1.0f;
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412NETCOAST, in seconds; 0 restores the original unbounded coast so
|
||||||
|
// the two can be compared on one build and one connection. See
|
||||||
|
// kMaximumExtrapolationSeconds - MUST MATCH across the race.
|
||||||
|
//
|
||||||
|
static Scalar
|
||||||
|
MaximumExtrapolationSeconds()
|
||||||
|
{
|
||||||
|
static Logical read = False;
|
||||||
|
static Scalar seconds = kMaximumExtrapolationSeconds;
|
||||||
|
|
||||||
|
if (!read)
|
||||||
|
{
|
||||||
|
read = True;
|
||||||
|
|
||||||
|
const char *setting = getenv("RP412NETCOAST");
|
||||||
|
if (setting != NULL && *setting != '\0')
|
||||||
|
{
|
||||||
|
Scalar asked = (Scalar) atof(setting);
|
||||||
|
seconds = (asked > 0.0f) ? asked : 0.0f; // 0 = no limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//############################### Mover #################################
|
//############################### Mover #################################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -373,8 +532,18 @@ Logical
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Past the expected update: coasting, not interpolating. Bound
|
||||||
|
// it - see kMaximumExtrapolationSeconds.
|
||||||
|
//
|
||||||
time_slice = lastPerformance - lastUpdate;
|
time_slice = lastPerformance - lastUpdate;
|
||||||
lerp_mode = False;
|
lerp_mode = False;
|
||||||
|
|
||||||
|
Scalar coast_limit = MaximumExtrapolationSeconds();
|
||||||
|
if (coast_limit > 0.0f && time_slice > coast_limit)
|
||||||
|
{
|
||||||
|
time_slice = coast_limit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -427,8 +596,21 @@ Logical
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Past the expected update: coasting, not interpolating. Bound
|
||||||
|
// it - see kMaximumExtrapolationSeconds. It matters more here
|
||||||
|
// than in the linear reckoner: this branch also carries a*t*t/2,
|
||||||
|
// so an unbounded slice grows the error as the SQUARE of the
|
||||||
|
// silence.
|
||||||
|
//
|
||||||
time_slice = lastPerformance - lastUpdate;
|
time_slice = lastPerformance - lastUpdate;
|
||||||
lerp_mode = False;
|
lerp_mode = False;
|
||||||
|
|
||||||
|
Scalar coast_limit = MaximumExtrapolationSeconds();
|
||||||
|
if (coast_limit > 0.0f && time_slice > coast_limit)
|
||||||
|
{
|
||||||
|
time_slice = coast_limit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -513,6 +695,11 @@ void
|
|||||||
Scalar percent =
|
Scalar percent =
|
||||||
time_slice / ((nextUpdate - lastPerformance) + time_slice);
|
time_slice / ((nextUpdate - lastPerformance) + time_slice);
|
||||||
|
|
||||||
|
// for the RP412CAMLOG trace at the end of this function
|
||||||
|
gLastLerpUsed = True;
|
||||||
|
gLastPercent = percent;
|
||||||
|
netRaceStats.wasSnapped = False;
|
||||||
|
|
||||||
//
|
//
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
// Do a spherical lerp on the angular motion
|
// Do a spherical lerp on the angular motion
|
||||||
@@ -562,6 +749,20 @@ void
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
gLastLerpUsed = False; // snapped, not blended
|
||||||
|
|
||||||
|
//
|
||||||
|
// Race totals: count the TRANSITION into snap mode, and only
|
||||||
|
// while updates are flowing - a departed sender's replicant
|
||||||
|
// snaps every step forever and would bury the number.
|
||||||
|
//
|
||||||
|
if (!netRaceStats.wasSnapped &&
|
||||||
|
(lastPerformance - lastUpdate) < 2.0f)
|
||||||
|
{
|
||||||
|
netRaceStats.snapCount++;
|
||||||
|
}
|
||||||
|
netRaceStats.wasSnapped = True;
|
||||||
|
|
||||||
localOrigin = projectedOrigin;
|
localOrigin = projectedOrigin;
|
||||||
worldLinearVelocity = projectedVelocity.linearMotion;
|
worldLinearVelocity = projectedVelocity.linearMotion;
|
||||||
localVelocity.angularMotion = projectedVelocity.angularMotion;
|
localVelocity.angularMotion = projectedVelocity.angularMotion;
|
||||||
@@ -581,6 +782,219 @@ void
|
|||||||
localToWorld = localOrigin;
|
localToWorld = localOrigin;
|
||||||
}
|
}
|
||||||
UpdateLocalMotion();
|
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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -658,9 +1072,14 @@ Bye_Bye:
|
|||||||
//
|
//
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
// Make sure the position quaternion stays stable
|
// 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();
|
localOrigin.angularPosition.Normalize();
|
||||||
normalizeCount = 0;
|
normalizeCount = 0;
|
||||||
@@ -668,6 +1087,133 @@ Bye_Bye:
|
|||||||
Check_Fpu();
|
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
|
void
|
||||||
@@ -681,15 +1227,175 @@ void
|
|||||||
{
|
{
|
||||||
|
|
||||||
//
|
//
|
||||||
//-------------------------------------------
|
//---------------------------------------
|
||||||
// HACK - precalculation for next update time
|
// Precalculation for next update time
|
||||||
//-------------------------------------------
|
//---------------------------------------
|
||||||
//
|
//
|
||||||
nextUpdate = Now();
|
nextUpdate = Now();
|
||||||
Scalar diff = nextUpdate - lastUpdate;
|
Scalar diff = nextUpdate - lastUpdate;
|
||||||
|
Scalar anchorInterval = (Scalar) 0;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Race totals for the RP412NETLOG summary: every update
|
||||||
|
// counts; gaps only within a live stream (the same <10 s
|
||||||
|
// bound the predictor uses), so a menu pause or join does
|
||||||
|
// not pollute the histogram.
|
||||||
|
//
|
||||||
|
netRaceStats.updateCount++;
|
||||||
if (diff < 10.0f)
|
if (diff < 10.0f)
|
||||||
{
|
{
|
||||||
nextUpdate.ticks += nextUpdate.ticks - lastUpdate.ticks;
|
netRaceStats.CountGap(diff);
|
||||||
|
if (diff > kLongGapThreshold) { netRaceStats.longGapCount++; }
|
||||||
|
if (diff < kQueuedGapThreshold) { netRaceStats.queuedGapCount++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff < 10.0f)
|
||||||
|
{
|
||||||
|
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 +1405,35 @@ void
|
|||||||
//
|
//
|
||||||
Entity::ReadUpdateRecord(record);
|
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
|
// Update the motion data
|
||||||
@@ -1708,6 +2443,19 @@ Mover::Mover(
|
|||||||
updateAcceleration.angularMotion = localAcceleration.linearMotion;
|
updateAcceleration.angularMotion = localAcceleration.linearMotion;
|
||||||
nextUpdate = lastUpdate;
|
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;
|
normalizeCount = 0;
|
||||||
if (IsInitialStasis())
|
if (IsInitialStasis())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -265,6 +265,14 @@ protected:
|
|||||||
MemoryStream *update_stream
|
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
|
int
|
||||||
normalizeCount;
|
normalizeCount;
|
||||||
Environment
|
Environment
|
||||||
@@ -281,6 +289,61 @@ protected:
|
|||||||
Time
|
Time
|
||||||
nextUpdate;
|
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
|
// Collision support
|
||||||
//
|
//
|
||||||
@@ -461,3 +524,11 @@ public:
|
|||||||
Logical
|
Logical
|
||||||
TestInstance() const;
|
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 "interest.h"
|
||||||
#include "icom.h"
|
#include "icom.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "spooler.h"
|
||||||
#include "notation.h"
|
#include "notation.h"
|
||||||
#include "nttmgr.h"
|
#include "nttmgr.h"
|
||||||
|
|
||||||
@@ -72,10 +73,41 @@ Logical
|
|||||||
//
|
//
|
||||||
void
|
void
|
||||||
NetworkClient::ReceiveNetworkPacket(
|
NetworkClient::ReceiveNetworkPacket(
|
||||||
NetworkPacket*,
|
NetworkPacket *packet,
|
||||||
Receiver::Message *packet_message
|
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);
|
Dispatch(packet_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +287,7 @@ void
|
|||||||
networkEggNotationFile = new NotationFile();
|
networkEggNotationFile = new NotationFile();
|
||||||
Register_Object(networkEggNotationFile);
|
Register_Object(networkEggNotationFile);
|
||||||
networkEggNotationFile->ReadText(eggTempBuffer, eggTempNext);
|
networkEggNotationFile->ReadText(eggTempBuffer, eggTempNext);
|
||||||
networkEggNotationFile->WriteFile("last.egg");
|
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||||
|
|
||||||
//
|
//
|
||||||
// Now turn the notation file into a mission
|
// Now turn the notation file into a mission
|
||||||
|
|||||||
@@ -137,6 +137,16 @@ public:
|
|||||||
virtual Logical
|
virtual Logical
|
||||||
ExecuteBackground();
|
ExecuteBackground();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Retry sends a connection could not take earlier. The application
|
||||||
|
// calls this at its flush points; the base engine has no queues, so
|
||||||
|
// the default is a no-op (L4 forwards it to the wire transport).
|
||||||
|
//
|
||||||
|
virtual void
|
||||||
|
FlushSends()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
NetworkClient*
|
NetworkClient*
|
||||||
GetNetworkClientPointer(ClientID client_id);
|
GetNetworkClientPointer(ClientID client_id);
|
||||||
|
|
||||||
|
|||||||
+118
-45
@@ -172,6 +172,11 @@ void
|
|||||||
// Create any MUNGA level vehicles
|
// Create any MUNGA level vehicles
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
//
|
//
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: CreatePlayerVehicle, cameraShipPlayer="
|
||||||
|
<< (int) (IsCameraShipPlayer() ? 1 : 0) << "\n" << std::flush;
|
||||||
|
}
|
||||||
if (IsCameraShipPlayer())
|
if (IsCameraShipPlayer())
|
||||||
{
|
{
|
||||||
Check(application);
|
Check(application);
|
||||||
@@ -186,6 +191,11 @@ void
|
|||||||
playerMission->GetGameModel(),
|
playerMission->GetGameModel(),
|
||||||
ResourceDescription::ModelListResourceType
|
ResourceDescription::ModelListResourceType
|
||||||
);
|
);
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: model '" << playerMission->GetGameModel()
|
||||||
|
<< "' resource " << (camera_res ? "FOUND" : "MISSING") << "\n" << std::flush;
|
||||||
|
}
|
||||||
Check(camera_res);
|
Check(camera_res);
|
||||||
CameraShip::MakeMessage
|
CameraShip::MakeMessage
|
||||||
create_camera(
|
create_camera(
|
||||||
@@ -208,6 +218,81 @@ void
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// A drop zone is only usable if the machine that owns it is still with
|
||||||
|
// us. Zones are map entities dealt round-robin across the hosts at load,
|
||||||
|
// and ownership transfer is not implemented - so a leaver's pads stay in
|
||||||
|
// the DropZones group forever, a request dispatched to one is silently
|
||||||
|
// dropped at the send ('Send: no host N in the table'), and the fry
|
||||||
|
// retry re-dispatches to the same dead owner every two seconds: the
|
||||||
|
// player never respawns. Locally-owned zones are always usable.
|
||||||
|
//
|
||||||
|
static Logical
|
||||||
|
DropZoneOwnerReachable(DropZone *dropzone)
|
||||||
|
{
|
||||||
|
Check(dropzone);
|
||||||
|
if (dropzone->GetInstance() != Entity::ReplicantInstance)
|
||||||
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
Check(application);
|
||||||
|
HostManager *host_manager = application->GetHostManager();
|
||||||
|
Check(host_manager);
|
||||||
|
Host *owner = host_manager->GetRemoteHost(dropzone->GetOwnerID());
|
||||||
|
return
|
||||||
|
(owner != NULL &&
|
||||||
|
owner->GetConnectStatus() == Host::OnLineConnectionStatus) ?
|
||||||
|
True : False;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The closest non-podium drop zone, optionally restricted to pads whose
|
||||||
|
// owner is still connected. One function because the dead-owner case
|
||||||
|
// needs a second pass: when every pad's owner is gone the old behaviour
|
||||||
|
// (closest pad regardless) is the graceful floor to land on.
|
||||||
|
//
|
||||||
|
static DropZone *
|
||||||
|
Player_FindClosestDropZone(
|
||||||
|
Player *player,
|
||||||
|
Logical require_reachable_owner)
|
||||||
|
{
|
||||||
|
EntityManager *entity_manager = application->GetEntityManager();
|
||||||
|
Check(entity_manager);
|
||||||
|
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
||||||
|
Check(dropzones);
|
||||||
|
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
||||||
|
|
||||||
|
DropZone *dropzone;
|
||||||
|
DropZone *closest_dropzone = NULL;
|
||||||
|
Vector3D range;
|
||||||
|
Scalar closest_range = 0.0f;
|
||||||
|
|
||||||
|
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
Check(dropzone);
|
||||||
|
if (!strnicmp(dropzone->GetDropZoneName(), "win", 3))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (require_reachable_owner && !DropZoneOwnerReachable(dropzone))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
range.Subtract(
|
||||||
|
player->localOrigin.linearPosition,
|
||||||
|
dropzone->localOrigin.linearPosition
|
||||||
|
);
|
||||||
|
Scalar len = range.LengthSquared();
|
||||||
|
if (closest_dropzone == NULL || len < closest_range)
|
||||||
|
{
|
||||||
|
closest_dropzone = dropzone;
|
||||||
|
closest_range = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closest_dropzone;
|
||||||
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
@@ -232,11 +317,32 @@ void
|
|||||||
{
|
{
|
||||||
DropZone *dropzone;
|
DropZone *dropzone;
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// A pad assigned on an earlier try may have died with its owner - the
|
||||||
|
// fry retry reuses message->dropZoneID forever, so left unchecked it
|
||||||
|
// re-dispatches to the same dead host every two seconds. Reset it and
|
||||||
|
// let the closest-pad scan below pick a live one.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
if (message->dropZoneID != EntityID::Null)
|
||||||
|
{
|
||||||
|
HostManager *host_manager = application->GetHostManager();
|
||||||
|
Check(host_manager);
|
||||||
|
Entity *assigned = host_manager->GetEntityPointer(message->dropZoneID);
|
||||||
|
if (assigned == NULL || !DropZoneOwnerReachable((DropZone*)assigned))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "Respawn: assigned drop zone "
|
||||||
|
<< ((assigned == NULL) ? "is gone" : "belongs to a departed host")
|
||||||
|
<< " - picking a new pad\n" << std::flush;
|
||||||
|
message->dropZoneID = EntityID::Null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// If no dropzone has been assigned to this death, find the closest one
|
// If no dropzone has been assigned to this death, find the closest one
|
||||||
|
// whose owner is still connected
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
if (message->dropZoneID == EntityID::Null)
|
if (message->dropZoneID == EntityID::Null)
|
||||||
@@ -244,52 +350,19 @@ void
|
|||||||
|
|
||||||
//DEBUG_STREAM << " Find Closest" << endl << std::flush;
|
//DEBUG_STREAM << " Find Closest" << endl << std::flush;
|
||||||
|
|
||||||
EntityManager *entity_manager = application->GetEntityManager();
|
DropZone *closest_dropzone = Player_FindClosestDropZone(this, True);
|
||||||
Check(entity_manager);
|
if (closest_dropzone == NULL)
|
||||||
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
|
||||||
Check(dropzones);
|
|
||||||
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
|
||||||
|
|
||||||
DropZone *closest_dropzone;
|
|
||||||
Vector3D range;
|
|
||||||
Scalar closest_range;
|
|
||||||
|
|
||||||
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
|
|
||||||
{
|
{
|
||||||
Check(dropzone);
|
//
|
||||||
|
// Every pad's owner is gone. Fall back to the old behaviour
|
||||||
if (!strnicmp(dropzone->GetDropZoneName(), "win", 3))
|
// (closest pad regardless) so the retry loop keeps its old
|
||||||
{
|
// shape rather than crashing here.
|
||||||
continue;
|
//
|
||||||
}
|
DEBUG_STREAM << "Respawn: no drop zone with a live owner - "
|
||||||
|
<< "falling back to the closest regardless\n" << std::flush;
|
||||||
closest_dropzone = dropzone;
|
closest_dropzone = Player_FindClosestDropZone(this, False);
|
||||||
range.Subtract(
|
|
||||||
localOrigin.linearPosition,
|
|
||||||
dropzone->localOrigin.linearPosition
|
|
||||||
);
|
|
||||||
closest_range = range.LengthSquared();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
|
|
||||||
{
|
|
||||||
Check(dropzone);
|
|
||||||
if (!strnicmp(dropzone->GetDropZoneName(), "win", 3))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
range.Subtract(
|
|
||||||
localOrigin.linearPosition,
|
|
||||||
dropzone->localOrigin.linearPosition
|
|
||||||
);
|
|
||||||
Scalar len = range.LengthSquared();
|
|
||||||
if (len < closest_range)
|
|
||||||
{
|
|
||||||
closest_dropzone = dropzone;
|
|
||||||
closest_range = len;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Check(closest_dropzone);
|
||||||
message->dropZoneID = closest_dropzone->GetEntityID();
|
message->dropZoneID = closest_dropzone->GetEntityID();
|
||||||
dropzone = closest_dropzone;
|
dropzone = closest_dropzone;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,16 @@ public:
|
|||||||
playerVehicle = player_vehicle;
|
playerVehicle = player_vehicle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The vehicle is going away. SetPlayerVehicle cannot say this - it
|
||||||
|
// Checks its argument, so it will not take NULL - and every reader
|
||||||
|
// of playerVehicle tests it for NULL first, so this is all they need
|
||||||
|
// to stop following a pod that no longer exists.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
ClearPlayerVehicle()
|
||||||
|
{Check(this); playerVehicle = NULL;}
|
||||||
|
|
||||||
Mission*
|
Mission*
|
||||||
GetMission()
|
GetMission()
|
||||||
{
|
{
|
||||||
|
|||||||
+501
-12
@@ -264,6 +264,187 @@ Simulation::SharedData
|
|||||||
// Model support
|
// 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;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Race totals for the RP412NETLOG summary: how often a window
|
||||||
|
// close actually moved the estimate (>5 ms), and the worst such
|
||||||
|
// step. This is the decision input for whether a bounded slew is
|
||||||
|
// ever needed - a step mid-race means SDR rerouted or a clock
|
||||||
|
// drifted, and today it lands as one hard jump.
|
||||||
|
//
|
||||||
|
unsigned long raceStepCount;
|
||||||
|
long raceStepWorstMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
free_slot->raceStepCount = 0;
|
||||||
|
free_slot->raceStepWorstMs = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Race-end summary, one line per peer (RP412NETLOG). The offset's
|
||||||
|
// absolute value only says how far apart the two processes launched
|
||||||
|
// (clocks are ms-since-process-start); the movement counters are the
|
||||||
|
// health signal - a mid-race step means SDR rerouted or a clock
|
||||||
|
// drifted, and today each one lands as a hard jump in every replicant
|
||||||
|
// from that peer. This is the decision input for the deferred bounded
|
||||||
|
// slew (plan item D2).
|
||||||
|
//
|
||||||
|
void NetClock_ReportRaceStats()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < netClockMaxPeers; ++i)
|
||||||
|
{
|
||||||
|
PeerClock *peer = &gPeerClocks[i];
|
||||||
|
if (peer->inUse && peer->settled)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "NetLog: peer " << peer->host
|
||||||
|
<< ": netclock offset " << peer->offsetTicks
|
||||||
|
<< " ms, window steps >5ms " << peer->raceStepCount
|
||||||
|
<< ", worst " << peer->raceStepWorstMs << " ms\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// A record older than the one we last applied is a record that would
|
||||||
|
// wind this simulation backwards - the sender has already told us where
|
||||||
|
// it went next. Applying it drags lastUpdate back, and the dead reckoner
|
||||||
|
// then extrapolates from a position the sender has left, which is a
|
||||||
|
// correction the moment the next record lands.
|
||||||
|
//
|
||||||
|
// Today's streams are ordered, so this should read zero; the counter
|
||||||
|
// says so in the race summary and a nonzero reading is itself the
|
||||||
|
// finding. It is here because ordering is a property of the channel, not
|
||||||
|
// of the game, and the queue work has already made the channel something
|
||||||
|
// we change.
|
||||||
|
//
|
||||||
|
// A LARGE regression is not a late record, it is a different stream: a
|
||||||
|
// rejoin, a restart, a machine whose clock was set. Five seconds is well
|
||||||
|
// past any plausible reordering and well short of a session, so beyond
|
||||||
|
// it we take the new stamp as the truth and resync rather than ignoring
|
||||||
|
// the sender forever. Same reasoning as the predictor's own reset.
|
||||||
|
//
|
||||||
|
static const long kStaleRegressionLimitTicks = 5000; // ms; Time is ms here
|
||||||
|
|
||||||
|
Logical
|
||||||
|
Simulation::AcceptUpdateStamp(const Time &stamp)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
if (lastSenderStampValid)
|
||||||
|
{
|
||||||
|
long regression = lastSenderStamp.ticks - stamp.ticks;
|
||||||
|
|
||||||
|
if (regression > 0 && regression < kStaleRegressionLimitTicks)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSenderStampValid = True;
|
||||||
|
lastSenderStamp = stamp;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
@@ -272,7 +453,94 @@ void
|
|||||||
Check(this);
|
Check(this);
|
||||||
Check_Pointer(message);
|
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;
|
||||||
|
long moved_abs = (moved < 0) ? -moved : moved;
|
||||||
|
if (moved_abs > 5)
|
||||||
|
{
|
||||||
|
// race totals for the NetLog summary (see PeerClock)
|
||||||
|
peer->raceStepCount++;
|
||||||
|
if (moved_abs > peer->raceStepWorstMs)
|
||||||
|
{
|
||||||
|
peer->raceStepWorstMs = moved_abs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
SetSimulationState(message->simulationState);
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -349,6 +617,7 @@ Simulation::Simulation(
|
|||||||
SetSimulationState(DefaultState);
|
SetSimulationState(DefaultState);
|
||||||
lastPerformance = Now();
|
lastPerformance = Now();
|
||||||
lastUpdate = lastPerformance;
|
lastUpdate = lastPerformance;
|
||||||
|
lastSenderStampValid = False;
|
||||||
activePerformance = &Simulation::DoNothingOnce;
|
activePerformance = &Simulation::DoNothingOnce;
|
||||||
updateModel = 0;
|
updateModel = 0;
|
||||||
simulationFlags = 0;
|
simulationFlags = 0;
|
||||||
@@ -441,9 +710,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
|
// 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
|
void
|
||||||
Simulation::PerformAndWatch(
|
Simulation::PerformAndWatch(
|
||||||
const Time& till,
|
const Time& till,
|
||||||
@@ -453,17 +951,8 @@ void
|
|||||||
Check(this);
|
Check(this);
|
||||||
Check(&till);
|
Check(&till);
|
||||||
|
|
||||||
Scalar slice = till - lastPerformance;
|
PerformTo(till);
|
||||||
lastPerformance = till;
|
WatchAndWrite(update_stream);
|
||||||
|
|
||||||
Perform(slice);
|
|
||||||
if (!AreWatchersDelayed())
|
|
||||||
{
|
|
||||||
ExecuteWatchers();
|
|
||||||
}
|
|
||||||
WriteSimulationUpdate(update_stream);
|
|
||||||
|
|
||||||
Check_Fpu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -4,6 +4,42 @@
|
|||||||
#include "receiver.h"
|
#include "receiver.h"
|
||||||
#include "time.h"
|
#include "time.h"
|
||||||
#include "resource.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)
|
||||||
|
void NetClock_ReportRaceStats(); // RP412NETLOG race-end peer lines
|
||||||
|
|
||||||
class Simulation__SharedData;
|
class Simulation__SharedData;
|
||||||
class Simulation__IndexData;
|
class Simulation__IndexData;
|
||||||
@@ -112,6 +148,53 @@ public:
|
|||||||
MemoryStream *update_stream
|
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
|
void
|
||||||
DoNothingOnce(Scalar time_slice);
|
DoNothingOnce(Scalar time_slice);
|
||||||
void
|
void
|
||||||
@@ -120,9 +203,21 @@ public:
|
|||||||
void
|
void
|
||||||
SetLastPerformance(const Time& when)
|
SetLastPerformance(const Time& when)
|
||||||
{Check(this); Check(&when); lastPerformance = when;}
|
{Check(this); Check(&when); lastPerformance = when;}
|
||||||
|
const Time&
|
||||||
|
GetLastPerformance() const
|
||||||
|
{Check(this); return lastPerformance;}
|
||||||
void
|
void
|
||||||
RequestEncore(Encore encore);
|
RequestEncore(Encore encore);
|
||||||
|
|
||||||
|
//
|
||||||
|
// True if this record should be applied, False if it is an older
|
||||||
|
// one arriving late. Ask BEFORE ReadUpdateRecord: it is the caller
|
||||||
|
// that has to skip the record, because ReadUpdateRecord is virtual
|
||||||
|
// and not every override chains to the base.
|
||||||
|
//
|
||||||
|
Logical
|
||||||
|
AcceptUpdateStamp(const Time &stamp);
|
||||||
|
|
||||||
virtual void
|
virtual void
|
||||||
ReadUpdateRecord(UpdateRecord *message);
|
ReadUpdateRecord(UpdateRecord *message);
|
||||||
virtual void
|
virtual void
|
||||||
@@ -151,6 +246,17 @@ protected:
|
|||||||
lastPerformance;
|
lastPerformance;
|
||||||
Time
|
Time
|
||||||
lastUpdate;
|
lastUpdate;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The sender's own stamp on the last record we accepted, for the
|
||||||
|
// stale guard in AcceptUpdateStamp. Kept in the sender's clock, not
|
||||||
|
// ours - it is only ever compared against another stamp from the
|
||||||
|
// same sender, so no offset is needed and none is applied.
|
||||||
|
//
|
||||||
|
Time
|
||||||
|
lastSenderStamp;
|
||||||
|
Logical
|
||||||
|
lastSenderStampValid;
|
||||||
Word
|
Word
|
||||||
updateModel;
|
updateModel;
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,310 @@ NetworkPacket*
|
|||||||
return (NetworkPacket*)GetPointer();
|
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 #####################
|
//################# MissionReviewApplicationManager #####################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
|
|||||||
@@ -38,6 +38,104 @@ public:
|
|||||||
NextPacket();
|
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 #####################
|
//############## MissionReviewApplicationManager #####################
|
||||||
//##########################################################################
|
//##########################################################################
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ protected:
|
|||||||
static long ticksPerSecond;
|
static long ticksPerSecond;
|
||||||
static __int64 perfCounterFreq;
|
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 long GetRTC();
|
||||||
static double GetHiRes();
|
static double GetHiRes();
|
||||||
static __int64 GetHiResTicks();
|
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);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ public:
|
|||||||
static unsigned int GetScreenWidth() { return mScreenWidth; }
|
static unsigned int GetScreenWidth() { return mScreenWidth; }
|
||||||
static unsigned int GetScreenHeight() { return mScreenHeight; }
|
static unsigned int GetScreenHeight() { return mScreenHeight; }
|
||||||
static bool GetFullscreen() { return mFullscreen; }
|
static bool GetFullscreen() { return mFullscreen; }
|
||||||
|
// -fit: borderless window filling the monitor, with the render size
|
||||||
|
// chosen to match the cockpit canvas it will be presented into.
|
||||||
|
static bool GetFitDisplay() { return mFitDisplay; }
|
||||||
|
// -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 Logical GetSeeSolids() { return seeSolids; }
|
||||||
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
|
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
|
||||||
// The front end's multiplayer path turns network mode on at launch
|
// The front end's multiplayer path turns network mode on at launch
|
||||||
@@ -93,10 +100,17 @@ public:
|
|||||||
static CString GetRIORecordingFileName() { return rioRecordingFileName; }
|
static CString GetRIORecordingFileName() { return rioRecordingFileName; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
// Pick mScreenWidth/mScreenHeight from the monitor. Runs after the
|
||||||
|
// whole command line is parsed, so an explicit -res always wins no
|
||||||
|
// matter which side of -fit it appears on.
|
||||||
|
static void ChooseFitResolution();
|
||||||
|
|
||||||
static HINSTANCE mhInstance;
|
static HINSTANCE mhInstance;
|
||||||
static unsigned int mScreenWidth;
|
static unsigned int mScreenWidth;
|
||||||
static unsigned int mScreenHeight;
|
static unsigned int mScreenHeight;
|
||||||
static bool mFullscreen;
|
static bool mFullscreen;
|
||||||
|
static bool mFitDisplay;
|
||||||
|
static bool mResExplicit;
|
||||||
static Logical seeSolids;
|
static Logical seeSolids;
|
||||||
static CString eggNotationFileName;
|
static CString eggNotationFileName;
|
||||||
static CString spoolFileName;
|
static CString spoolFileName;
|
||||||
|
|||||||
+187
-7
@@ -94,6 +94,8 @@ HINSTANCE L4Application::mhInstance = NULL;
|
|||||||
unsigned int L4Application::mScreenWidth = 800;
|
unsigned int L4Application::mScreenWidth = 800;
|
||||||
unsigned int L4Application::mScreenHeight = 600;
|
unsigned int L4Application::mScreenHeight = 600;
|
||||||
bool L4Application::mFullscreen = true;
|
bool L4Application::mFullscreen = true;
|
||||||
|
bool L4Application::mFitDisplay = false;
|
||||||
|
bool L4Application::mResExplicit = false;
|
||||||
Logical L4Application::seeSolids = False;
|
Logical L4Application::seeSolids = False;
|
||||||
CString L4Application::eggNotationFileName;
|
CString L4Application::eggNotationFileName;
|
||||||
CString L4Application::spoolFileName;
|
CString L4Application::spoolFileName;
|
||||||
@@ -205,6 +207,7 @@ Logical
|
|||||||
{
|
{
|
||||||
mScreenWidth = atoi(W2A(argv[++(*arguement)]));
|
mScreenWidth = atoi(W2A(argv[++(*arguement)]));
|
||||||
mScreenHeight = atoi(W2A(argv[++(*arguement)]));
|
mScreenHeight = atoi(W2A(argv[++(*arguement)]));
|
||||||
|
mResExplicit = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -217,18 +220,157 @@ Logical
|
|||||||
mFullscreen = false;
|
mFullscreen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
// -fit: borderless window over the whole monitor, and a render size
|
||||||
|
// picked to match. Spelled out as -windowed-fullscreen too, since
|
||||||
|
// that is what the rest of the world calls it.
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
else if (
|
||||||
|
!stricmp(W2A(argv[*arguement]), "-fit") ||
|
||||||
|
!stricmp(W2A(argv[*arguement]), "-windowed-fullscreen")
|
||||||
|
)
|
||||||
|
{
|
||||||
|
mFullscreen = false;
|
||||||
|
mFitDisplay = true;
|
||||||
|
}
|
||||||
|
|
||||||
else if (
|
else if (
|
||||||
!stricmp(W2A(argv[*arguement]), "-h") || !stricmp(W2A(argv[*arguement]), "-help")
|
!stricmp(W2A(argv[*arguement]), "-h") || !stricmp(W2A(argv[*arguement]), "-help")
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "\n" << argv[0] <<
|
DEBUG_STREAM << "\n" << argv[0] <<
|
||||||
" -egg <filename> -net <memory_address> -solids -h -help\n";
|
" -egg <filename> -net <memory_address> -solids -h -help\n"
|
||||||
|
" -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"
|
||||||
|
" (long form: -windowed-fullscreen)\n"
|
||||||
|
" -res <width> <height> explicit render size; overrides the\n"
|
||||||
|
" size -fit would have picked\n";
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
|
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// ChooseFitResolution
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The cockpit presents the 3D into a viewscreen that fills its 1920x1080
|
||||||
|
// canvas, and that canvas is fitted to the window at one uniform scale.
|
||||||
|
// So the render size that lands 1:1 on screen is the canvas at the same
|
||||||
|
// scale the cockpit will choose - work it out with identical arithmetic
|
||||||
|
// here and the stretch becomes a copy.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
L4Application::ChooseFitResolution()
|
||||||
|
{
|
||||||
|
int monitor_w = GetSystemMetrics(SM_CXSCREEN);
|
||||||
|
int monitor_h = GetSystemMetrics(SM_CYSCREEN);
|
||||||
|
if (monitor_w <= 0 || monitor_h <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int canvas_w = 1920;
|
||||||
|
const int canvas_h = 1080;
|
||||||
|
|
||||||
|
int fit_w = (monitor_w * 100) / canvas_w;
|
||||||
|
int fit_h = (monitor_h * 100) / canvas_h;
|
||||||
|
int scale = (fit_w < fit_h) ? fit_w : fit_h;
|
||||||
|
if (scale < 25) scale = 25;
|
||||||
|
|
||||||
|
unsigned int width = (canvas_w * scale) / 100;
|
||||||
|
unsigned int height = (canvas_h * scale) / 100;
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
// A 1995 engine on a 5K panel would be asked for a back buffer well
|
||||||
|
// past anything it was built for; cap and let D3D scale the last bit.
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
if (width > 3840)
|
||||||
|
{
|
||||||
|
width = 3840;
|
||||||
|
height = 2160;
|
||||||
|
}
|
||||||
|
|
||||||
|
mScreenWidth = width;
|
||||||
|
mScreenHeight = height;
|
||||||
|
|
||||||
|
DEBUG_STREAM << "L4Application: -fit chose -res " << width << " "
|
||||||
|
<< height << " (" << scale << "% canvas) for the "
|
||||||
|
<< monitor_w << "x" << monitor_h << " monitor\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// 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
|
// ParseCommandLine
|
||||||
@@ -264,12 +406,35 @@ Logical
|
|||||||
{
|
{
|
||||||
suppressGauges = TRUE;
|
suppressGauges = TRUE;
|
||||||
missionReviewMode = 1;
|
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))
|
} else if (!(*parser)(&i, argc, argv))
|
||||||
{
|
{
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// after the whole line, so -res wins from either side of -fit
|
||||||
|
if (mFitDisplay && !mResExplicit)
|
||||||
|
{
|
||||||
|
ChooseFitResolution();
|
||||||
|
}
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,6 +887,13 @@ void
|
|||||||
// The debug keys are for developers: RP412DEVKEYS=1 arms them.
|
// The debug keys are for developers: RP412DEVKEYS=1 arms them.
|
||||||
// Players get exactly one chord - Alt+Q, the deliberate abort.
|
// 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;
|
static int dev_keys = -1;
|
||||||
if (dev_keys < 0)
|
if (dev_keys < 0)
|
||||||
{
|
{
|
||||||
@@ -765,6 +937,7 @@ void
|
|||||||
|
|
||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
// FrameDump from Division card to Targa file
|
// FrameDump from Division card to Targa file
|
||||||
|
// INERT: DPLFrameDump and dump_frame_buffer are both stubs.
|
||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
case PCK_ALT_F:
|
case PCK_ALT_F:
|
||||||
{
|
{
|
||||||
@@ -780,6 +953,7 @@ void
|
|||||||
|
|
||||||
//------------------------------------
|
//------------------------------------
|
||||||
// Report current free memory in card
|
// Report current free memory in card
|
||||||
|
// INERT: the body below is commented out, so this key is silent.
|
||||||
//------------------------------------
|
//------------------------------------
|
||||||
case PCK_ALT_K:
|
case PCK_ALT_K:
|
||||||
{
|
{
|
||||||
@@ -807,6 +981,7 @@ void
|
|||||||
|
|
||||||
//---------------------------------------
|
//---------------------------------------
|
||||||
// Report performance statistics (Alt-?)
|
// Report performance statistics (Alt-?)
|
||||||
|
// INERT: DPLReportPerfStats is a stub - it read DPL's own counters.
|
||||||
//---------------------------------------
|
//---------------------------------------
|
||||||
case PCK_ALT_SLASH:
|
case PCK_ALT_SLASH:
|
||||||
{
|
{
|
||||||
@@ -819,9 +994,10 @@ void
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
case PCK_ALT_W:
|
||||||
{
|
{
|
||||||
if (!dev_keys) break;
|
if (!dev_keys) break;
|
||||||
@@ -833,9 +1009,13 @@ void
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
case PCK_ALT_V:
|
||||||
{
|
{
|
||||||
if (!dev_keys) break;
|
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);
|
||||||
+266
-29
@@ -2,6 +2,7 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4audio.h"
|
#include "l4audio.h"
|
||||||
|
#include "l4audefx.h"
|
||||||
#include "l4audlvl.h"
|
#include "l4audlvl.h"
|
||||||
#include "l4app.h"
|
#include "l4app.h"
|
||||||
#include "l4audrnd.h"
|
#include "l4audrnd.h"
|
||||||
@@ -9,6 +10,49 @@
|
|||||||
#include "..\munga\player.h"
|
#include "..\munga\player.h"
|
||||||
#include "..\rp\vtv.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 #########################
|
//####################### L4AudioSpatialization #########################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -658,6 +702,19 @@ L4AudioSource::L4AudioSource(
|
|||||||
AudioSource(stream, entity)
|
AudioSource(stream, entity)
|
||||||
{
|
{
|
||||||
channelSet.count = GetAudioVoiceCount();
|
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();
|
L4AudioSourceX();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,6 +980,22 @@ void
|
|||||||
patch_resource->SetDistance(GetDistanceToSource());
|
patch_resource->SetDistance(GetDistanceToSource());
|
||||||
patch_resource->SetupPatch(channelSet);
|
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
|
// Set the channel to default control values
|
||||||
//
|
//
|
||||||
@@ -1039,6 +1112,8 @@ void
|
|||||||
// Apply filter scale
|
// Apply filter scale
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
|
float direct_gainhf = 1.0f;
|
||||||
|
|
||||||
if (UseSourceBrightnessScale())
|
if (UseSourceBrightnessScale())
|
||||||
{
|
{
|
||||||
const MIDINRPNValue filter_resolution = 2;// HACK - should come from audio.ini
|
const MIDINRPNValue filter_resolution = 2;// HACK - should come from audio.ini
|
||||||
@@ -1058,6 +1133,32 @@ void
|
|||||||
{
|
{
|
||||||
lastMIDIFilterCutoff = midi_filter_cutoff;
|
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
|
const MIDIValue volume_resolution = 2; // HACK - should come from audio.ini
|
||||||
|
|
||||||
volume_scale = CalculateSourceVolumeScale();
|
volume_scale = CalculateSourceVolumeScale();
|
||||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
|
||||||
Check(application);
|
//
|
||||||
L4AudioRenderer *audio_renderer =
|
// FIDELITY (docs/SOUND.md F4): the original ended its volume path in MIDI
|
||||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
// CC7, whose GM/SoundFont curve is concave -- amplitude ~ (v/127)^2. Writing
|
||||||
Check(audio_renderer);
|
// volume_scale linearly to AL_GAIN played every intermediate level about
|
||||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
// +6 dB hot at mid-scale and compressed the authored dynamic range.
|
||||||
Check(audio_head);
|
//
|
||||||
|
// 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++)
|
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,
|
||||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
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->SetDistance(GetDistanceToSource());
|
||||||
patch_resource->SetupPatch(channelSet);
|
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());
|
/*patch_resource->SetDistance(GetDistanceToSource());
|
||||||
for (i = 0; i < AudioChannelSetSize; i++)
|
for (i = 0; i < AudioChannelSetSize; i++)
|
||||||
{
|
{
|
||||||
@@ -1405,15 +1524,71 @@ void
|
|||||||
|
|
||||||
pitch_offset = CalculateSourcePitchOffset();
|
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);
|
double relativePitch = pow(2.0,pitch_offset/1200.0);
|
||||||
Clamp(relativePitch,0.5,2.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++)
|
for (int i=0; i < channelSet.count; i++)
|
||||||
{
|
{
|
||||||
alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z);
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1432,22 +1607,25 @@ AudioControlValue
|
|||||||
//
|
//
|
||||||
Scalar
|
Scalar
|
||||||
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||||
return volume_scale;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Update the spatial model that will result in the value
|
// FIDELITY (docs/SOUND.md F3): apply the AUTHORED distance attenuation.
|
||||||
// for distance related volume 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.
|
||||||
//
|
//
|
||||||
/*Check(application);
|
// The spatial model is already refreshed each Execute, so the
|
||||||
Check(application->GetAudioRenderer());
|
// UpdateSpatialModel call the original comment carried is not needed here.
|
||||||
UpdateSpatialModel(application->GetAudioRenderer()->GetAudioHead());
|
|
||||||
|
|
||||||
//
|
|
||||||
// Apply distance attenuation to the volume scale
|
|
||||||
//
|
//
|
||||||
Check(GetAudioLocation());
|
Check(GetAudioLocation());
|
||||||
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||||
return volume_scale;*/
|
return volume_scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -1468,6 +1646,26 @@ Static3DPatchSource::Static3DPatchSource(
|
|||||||
MemoryStream_Read(stream, &useInternalSpatialization);
|
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)
|
Logical Static3DPatchSource::IsAudioSourceClipped(AudioHead *audio_head)
|
||||||
{
|
{
|
||||||
if (AudioSource::IsAudioSourceClipped(audio_head) || l4_application->GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() == VTV::BurningState)
|
if (AudioSource::IsAudioSourceClipped(audio_head) || l4_application->GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() == VTV::BurningState)
|
||||||
@@ -1694,6 +1892,16 @@ void
|
|||||||
Check(patch_resource);
|
Check(patch_resource);
|
||||||
patch_resource->SetDistance(GetDistanceToSource());
|
patch_resource->SetDistance(GetDistanceToSource());
|
||||||
patch_resource->SetupPatch(channelSet);
|
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++)
|
/*for (i = 0; i < AudioChannelSetSize; i++)
|
||||||
{
|
{
|
||||||
if ((channel = channelSet.GetNth(i)) != NULL)
|
if ((channel = channelSet.GetNth(i)) != NULL)
|
||||||
@@ -1909,12 +2117,6 @@ void
|
|||||||
|
|
||||||
Scalar volume_scale = CalculateSourceVolumeScale();
|
Scalar volume_scale = CalculateSourceVolumeScale();
|
||||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
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;
|
Scalar pitch_offset;
|
||||||
|
|
||||||
@@ -1933,12 +2135,47 @@ void
|
|||||||
relative_position = audio_location->GetVectorToSource();
|
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
|
//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++)
|
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);
|
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);
|
virtual Logical IsAudioSourceClipped(AudioHead *audio_head);
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Mix levels
|
||||||
|
//
|
||||||
|
public:
|
||||||
|
AudioControlValue
|
||||||
|
CalculateSourceVolumeScale();
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// SetPosition
|
// SetPosition
|
||||||
//
|
//
|
||||||
|
|||||||
+34
-1
@@ -128,8 +128,25 @@ void
|
|||||||
// #endif
|
// #endif
|
||||||
SAMPLEINFO info;
|
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
|
//Attach buffers
|
||||||
for (int i=0; i < sourceSet.count; i++)
|
for (int i=0; i < zone_count; i++)
|
||||||
{
|
{
|
||||||
info = PRESET_getSampleInfo(bankID,patchID,i);
|
info = PRESET_getSampleInfo(bankID,patchID,i);
|
||||||
if (info.bufferIndex >= 0)
|
if (info.bufferIndex >= 0)
|
||||||
@@ -310,3 +327,19 @@ MIDINRPNValue
|
|||||||
Check(patch_level_of_detail);
|
Check(patch_level_of_detail);
|
||||||
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
|
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];
|
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);
|
bool PRESET_isImplemented(int bank, int preset);
|
||||||
int PRESET_getNumSamples(int bank, int preset);
|
int PRESET_getNumSamples(int bank, int preset);
|
||||||
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
|
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
|
||||||
@@ -69,6 +75,14 @@ public:
|
|||||||
GetVoiceCount()
|
GetVoiceCount()
|
||||||
{return PRESET_getNumSamples(bankID,patchID);}
|
{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
|
// BuildFromPage
|
||||||
@@ -163,6 +177,12 @@ public:
|
|||||||
void
|
void
|
||||||
SetupPatch(SourceSet sourceSet);
|
SetupPatch(SourceSet sourceSet);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||||
|
//
|
||||||
|
float
|
||||||
|
GetZoneBassGain(int zone_index);
|
||||||
|
|
||||||
MIDINRPNValue
|
MIDINRPNValue
|
||||||
GetMaxMIDIFilterCutoff();
|
GetMaxMIDIFilterCutoff();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,130 @@
|
|||||||
ALuint *g_buffers;
|
ALuint *g_buffers;
|
||||||
int g_numBuffers;
|
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 #############################
|
//####################### AudioObjectStream #############################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -566,6 +690,18 @@ void
|
|||||||
g_buffers = NULL;
|
g_buffers = NULL;
|
||||||
g_numBuffers = 0;
|
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;
|
int bufferInd = 0;
|
||||||
@@ -647,6 +783,15 @@ void
|
|||||||
sf_read_raw(file,data,size);
|
sf_read_raw(file,data,size);
|
||||||
sf_close(file);
|
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
|
//Feed the buffer
|
||||||
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
|
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
|
||||||
PRESET_setBufferIndex(i,j,k,bufferInd);
|
PRESET_setBufferIndex(i,j,k,bufferInd);
|
||||||
@@ -726,6 +871,16 @@ void
|
|||||||
|
|
||||||
ALuint AL_getBuffer(int index)
|
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];
|
return g_buffers[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ ALuint AL_getBuffer(int index);
|
|||||||
extern ALuint *g_buffers;
|
extern ALuint *g_buffers;
|
||||||
extern int g_numBuffers;
|
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;
|
//class AudioHardware;
|
||||||
|
|
||||||
|
|||||||
+310
-27
@@ -2,9 +2,20 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4audrnd.h"
|
#include "l4audrnd.h"
|
||||||
|
#include "l4audefx.h"
|
||||||
#include "..\munga\notation.h"
|
#include "..\munga\notation.h"
|
||||||
#include "openal/alc.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
|
// L4AudioRenderer
|
||||||
@@ -379,6 +390,66 @@ void
|
|||||||
{
|
{
|
||||||
ALCcontext *context = alcCreateContext(device,NULL);
|
ALCcontext *context = alcCreateContext(device,NULL);
|
||||||
alcMakeContextCurrent(context);
|
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;
|
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
|
// RequestAudioChannels
|
||||||
@@ -1271,30 +1526,54 @@ Logical
|
|||||||
Check(this);
|
Check(this);
|
||||||
Check(source_request);
|
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;
|
int requested = source_request->count;
|
||||||
|
|
||||||
bool failed = true;
|
if (requested > (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0])))
|
||||||
|
{
|
||||||
alGetError();
|
requested = (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0]));
|
||||||
|
source_request->count = requested;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < requested; i++)
|
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();
|
ALuint src = 0;
|
||||||
if (error == AL_NO_ERROR)
|
|
||||||
{
|
|
||||||
failed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed)
|
if (!RPAudioPoolAcquire(&src))
|
||||||
{
|
{
|
||||||
return False;
|
//
|
||||||
|
// 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;
|
return True;
|
||||||
@@ -1375,23 +1654,27 @@ Logical
|
|||||||
|
|
||||||
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
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++)
|
for (int i = 0; i < sourceSet.count; i++)
|
||||||
{
|
{
|
||||||
ALenum state;
|
if (sourceSet.sources[i] != 0)
|
||||||
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
|
|
||||||
|
|
||||||
if (state == AL_PLAYING)
|
|
||||||
{
|
{
|
||||||
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 ~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
#include "l4audres.h"
|
#include "l4audres.h"
|
||||||
#include "openal/al.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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "l4ctrl.h"
|
#include "l4ctrl.h"
|
||||||
#include "l4keybd.h"
|
#include "l4keybd.h"
|
||||||
#include "l4app.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 "l4dinput.h"
|
||||||
#include "..\munga\appmgr.h"
|
#include "..\munga\appmgr.h"
|
||||||
#include "dxutils.h"
|
#include "dxutils.h"
|
||||||
@@ -1513,6 +1515,78 @@ void
|
|||||||
// Update the PC keyboard mapping group
|
// 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)
|
if (flags.keyboardExists)
|
||||||
{
|
{
|
||||||
//RB 1/20/07
|
//RB 1/20/07
|
||||||
|
|||||||
+78
-1
@@ -13,6 +13,7 @@ L4TEXOP::WrapType d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
|
|||||||
bool d3d_OBJECT::mLastTexturingState = true;
|
bool d3d_OBJECT::mLastTexturingState = true;
|
||||||
long d3d_OBJECT::mNextID = 1;
|
long d3d_OBJECT::mNextID = 1;
|
||||||
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;
|
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;
|
||||||
|
LPDIRECT3DTEXTURE9 d3d_OBJECT::mNamePlateMarkers[d3d_OBJECT::kNamePlateCount + 1] = { NULL };
|
||||||
|
|
||||||
void chgext(char *filePath, const char *newExtension)
|
void chgext(char *filePath, const char *newExtension)
|
||||||
{
|
{
|
||||||
@@ -156,7 +157,36 @@ d3d_OBJECT *d3d_OBJECT::LoadObject(LPDIRECT3DDEVICE9 device, char *fileName)
|
|||||||
{
|
{
|
||||||
sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
|
sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
|
||||||
}
|
}
|
||||||
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
|
//
|
||||||
|
// playerN is not a file - it is a pilot's callsign, drawn into
|
||||||
|
// a texture at run time, so loading it always fails. Give the
|
||||||
|
// plate its marker texture instead: it keeps the eight sign
|
||||||
|
// faces as eight separate draw ops through consolidation, and
|
||||||
|
// the Winners Circle swaps the real callsign in later.
|
||||||
|
//
|
||||||
|
const char *base = strrchr(textureFilename, '\\');
|
||||||
|
base = (base != NULL) ? base + 1 : textureFilename;
|
||||||
|
int plate = 0;
|
||||||
|
if (_strnicmp(base, "player", 6) == 0 &&
|
||||||
|
base[6] >= '1' && base[6] <= '8')
|
||||||
|
{
|
||||||
|
plate = base[6] - '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plate != 0)
|
||||||
|
{
|
||||||
|
memset(&object->mDrawOps[i].texture, 0, sizeof(L4TEXOP));
|
||||||
|
object->mDrawOps[i].texture.texture =
|
||||||
|
NamePlateMarker(device, plate);
|
||||||
|
if (object->mDrawOps[i].texture.texture != NULL)
|
||||||
|
{
|
||||||
|
object->mDrawOps[i].texture.texture->AddRef();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
|
if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
|
||||||
@@ -286,6 +316,53 @@ void d3d_OBJECT::FlushTextureCache()
|
|||||||
(*iter).second.texture->Release();
|
(*iter).second.texture->Release();
|
||||||
}
|
}
|
||||||
mTextureCache.clear();
|
mTextureCache.clear();
|
||||||
|
|
||||||
|
for (int plate = 0; plate <= kNamePlateCount; ++plate)
|
||||||
|
{
|
||||||
|
if (mNamePlateMarkers[plate] != NULL)
|
||||||
|
{
|
||||||
|
mNamePlateMarkers[plate]->Release();
|
||||||
|
mNamePlateMarkers[plate] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// A distinct 1x1 texture per plate, so the eight sign faces stay eight
|
||||||
|
// separate draw ops through mesh consolidation instead of merging into one.
|
||||||
|
// Its content never shows - the callsign replaces it before the podium.
|
||||||
|
//
|
||||||
|
LPDIRECT3DTEXTURE9 d3d_OBJECT::NamePlateMarker(LPDIRECT3DDEVICE9 device, int plate)
|
||||||
|
{
|
||||||
|
if (plate < 1 || plate > kNamePlateCount || device == NULL)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (mNamePlateMarkers[plate] == NULL)
|
||||||
|
{
|
||||||
|
device->CreateTexture(1, 1, 1, 0, D3DFMT_A4R4G4B4, D3DPOOL_MANAGED,
|
||||||
|
&mNamePlateMarkers[plate], NULL);
|
||||||
|
}
|
||||||
|
return mNamePlateMarkers[plate];
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Which plate a texture is the marker for, or 0 if it is not one.
|
||||||
|
//
|
||||||
|
int d3d_OBJECT::NamePlateFor(LPDIRECT3DTEXTURE9 texture)
|
||||||
|
{
|
||||||
|
if (texture == NULL)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (int plate = 1; plate <= kNamePlateCount; ++plate)
|
||||||
|
{
|
||||||
|
if (mNamePlateMarkers[plate] == texture)
|
||||||
|
{
|
||||||
|
return plate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
|
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
|
||||||
|
|||||||
@@ -111,6 +111,27 @@ public:
|
|||||||
// race loop tears the renderer down between missions).
|
// race loop tears the renderer down between missions).
|
||||||
static void FlushTextureCache();
|
static void FlushTextureCache();
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Pilot name plates.
|
||||||
|
//
|
||||||
|
// The Winners Circle signs ask for textures called player1..player8,
|
||||||
|
// which are not files - the renderer draws each pilot's callsign into
|
||||||
|
// a texture at run time. Loading them fails and the plates come out
|
||||||
|
// blank, so the draw ops that wanted them are noted here as the
|
||||||
|
// geometry loads, and bound to the real textures once the finishing
|
||||||
|
// order is known.
|
||||||
|
//
|
||||||
|
// Cleared with the texture cache: the entries point at objects that
|
||||||
|
// belong to the mission being torn down.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Each plate is given a distinct marker texture as it loads. Without
|
||||||
|
// one they are eight identical untextured draw ops, and consolidation
|
||||||
|
// merges every static mesh by material - all eight plates would
|
||||||
|
// collapse into a single shared op that can only ever show one name.
|
||||||
|
enum { kNamePlateCount = 8 };
|
||||||
|
static LPDIRECT3DTEXTURE9 NamePlateMarker(LPDIRECT3DDEVICE9 device, int plate);
|
||||||
|
static int NamePlateFor(LPDIRECT3DTEXTURE9 texture);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
|
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
|
||||||
|
|
||||||
@@ -150,6 +171,9 @@ private:
|
|||||||
|
|
||||||
static long mNextID;
|
static long mNextID;
|
||||||
static stdext::hash_map< std::string , L4TEXOP > mTextureCache;
|
static stdext::hash_map< std::string , L4TEXOP > mTextureCache;
|
||||||
|
|
||||||
|
// index 1..8; [0] unused so the index is the plate number
|
||||||
|
static LPDIRECT3DTEXTURE9 mNamePlateMarkers[kNamePlateCount + 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
extern int gNumBatches;
|
extern int gNumBatches;
|
||||||
|
|||||||
+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()
|
void Worker()
|
||||||
{
|
{
|
||||||
|
bool ownsApartment = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
init_apartment();
|
init_apartment();
|
||||||
|
ownsApartment = true;
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
// apartment already set on this thread; carry on
|
// 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::mutex claimedLock;
|
||||||
std::vector<ClaimedArray> claimed;
|
std::vector<ClaimedArray> claimed;
|
||||||
bool anySeen = false;
|
bool anySeen = false;
|
||||||
|
|||||||
+537
-31
@@ -10,6 +10,141 @@ namespace
|
|||||||
|
|
||||||
const int buttonGap = 4;
|
const int buttonGap = 4;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Sticky placement for the exploded view. See L4MFDVIEW.h; ported
|
||||||
|
// from BT411's BT_GLASS_LAYOUT.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
enum { LayoutOff = 0, LayoutLoad, LayoutSave };
|
||||||
|
|
||||||
|
const char layoutFileName[] = "mfd_layout.cfg";
|
||||||
|
|
||||||
|
// every window taking part, so Save can walk them
|
||||||
|
enum { maxLayoutWindows = 12 };
|
||||||
|
struct LayoutWindow
|
||||||
|
{
|
||||||
|
void *window;
|
||||||
|
const char *title;
|
||||||
|
Logical withSize;
|
||||||
|
Logical bare; // frame stripped, per the file
|
||||||
|
};
|
||||||
|
LayoutWindow layoutWindows[maxLayoutWindows];
|
||||||
|
int layoutWindowCount = 0;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Take a window's frame off: no title bar, no border, nothing to
|
||||||
|
// drag it by. WS_SYSMENU stays - it draws nothing once the caption
|
||||||
|
// is gone, but without one DefWindowProc will not honour Alt+F4,
|
||||||
|
// and a bare window with no way to close it is a trap.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
void StripWindowFrame(HWND window)
|
||||||
|
{
|
||||||
|
LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE);
|
||||||
|
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX |
|
||||||
|
WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME);
|
||||||
|
style |= WS_POPUP;
|
||||||
|
SetWindowLongPtrA(window, GWL_STYLE, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayoutMode()
|
||||||
|
{
|
||||||
|
static int mode = -1;
|
||||||
|
if (mode < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412MFDLAYOUT");
|
||||||
|
if (setting == NULL || setting[0] == '\0')
|
||||||
|
{
|
||||||
|
mode = LayoutOff;
|
||||||
|
}
|
||||||
|
else if (!stricmp(setting, "off") || !stricmp(setting, "0"))
|
||||||
|
{
|
||||||
|
mode = LayoutOff;
|
||||||
|
}
|
||||||
|
else if (!stricmp(setting, "load") || !stricmp(setting, "restore"))
|
||||||
|
{
|
||||||
|
mode = LayoutLoad;
|
||||||
|
}
|
||||||
|
else // save / adjust / on / 1
|
||||||
|
{
|
||||||
|
mode = LayoutSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode != LayoutOff)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: RP412MFDLAYOUT="
|
||||||
|
<< ((mode == LayoutSave) ? "save" : "load")
|
||||||
|
<< " (" << layoutFileName << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Button banks run UNDER the glass: a button reaches this far in
|
||||||
|
// from the display edge, but only the indicator strip clears that
|
||||||
|
// edge - the rest sits behind the picture, which is what the
|
||||||
|
// player actually clicks. Applies to both the MFD strips (depth
|
||||||
|
// measured vertically) and the map's side columns (horizontally).
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// 240 against the 480 glass: the two banks reach in far enough to
|
||||||
|
// meet in the middle bar the strips' own width, so practically the
|
||||||
|
// whole display is a press target and neither bank overlaps the
|
||||||
|
// other. The clamps below keep that true on a scaled-down cockpit.
|
||||||
|
const int buttonDepth = 240;
|
||||||
|
const int indicatorStrip = 10;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Extra pixels of indicator strip, on top of the 10 above, so a
|
||||||
|
// FLASHING lamp is easier to notice on desktop glass - the pod's
|
||||||
|
// buttons were physical and backlit and never had that problem.
|
||||||
|
// Only the strip moves, so the button keeps its width and reaches
|
||||||
|
// the same distance behind the glass: this makes the lamp a
|
||||||
|
// LONGER bar, not a fatter face. The client size, the display
|
||||||
|
// offset and both banks all derive from strip_h/strip_w below, so
|
||||||
|
// the window grows to fit and nothing needs to know about it.
|
||||||
|
//
|
||||||
|
// DEFAULT 0 - Red Planet keeps the pod's geometry unless a player
|
||||||
|
// asks otherwise. (BattleTech 4.11 carries the same knob defaulted
|
||||||
|
// to 10, where the players asked for it; the two games are tuned
|
||||||
|
// separately on purpose.) Ported alongside RP412MFDLAYOUT, which
|
||||||
|
// took the same route from BT411's BT_GLASS_LAYOUT.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const int defaultProtrude = 0;
|
||||||
|
|
||||||
|
int ProtrudeBonus()
|
||||||
|
{
|
||||||
|
static int bonus = -1;
|
||||||
|
if (bonus < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412MFDPROTRUDE");
|
||||||
|
bonus = (setting != NULL && setting[0] != '\0')
|
||||||
|
? atoi(setting) // a word parses to 0, i.e. off
|
||||||
|
: defaultProtrude;
|
||||||
|
if (bonus < 0) bonus = 0;
|
||||||
|
if (bonus > 200) bonus = 200;
|
||||||
|
|
||||||
|
if (bonus != 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDView: RP412MFDPROTRUDE=" << bonus
|
||||||
|
<< " (indicator strip " << indicatorStrip << " -> "
|
||||||
|
<< (indicatorStrip + bonus) << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// The map paints its own legend beside each side button, and that
|
||||||
|
// grid is not the naive height/6: measured off the 640-tall map,
|
||||||
|
// the first cell starts 13 rows down and the six cells are 102
|
||||||
|
// tall on a 105 pitch (13 + 6*102 + 5*3 = 640). Spacing the
|
||||||
|
// buttons evenly from the top instead left every one of them
|
||||||
|
// riding high of its label. Scaled to the live glass height.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const int mapLegendSpan = 640;
|
||||||
|
const int mapLegendTop = 13;
|
||||||
|
const int mapLegendCell = 102;
|
||||||
|
const int mapLegendPitch = 105;
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
|
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
|
||||||
// bright), red family for MFD strips, amber for the side columns.
|
// bright), red family for MFD strips, amber for the side columns.
|
||||||
@@ -81,6 +216,15 @@ namespace
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
//
|
||||||
|
// A drag just finished. Write the whole arrangement now rather
|
||||||
|
// than trusting teardown, so a hard kill still leaves the
|
||||||
|
// layout that was on screen. No-op unless the mode is save.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
return 0;
|
||||||
|
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
// Part of the cockpit; just hide it.
|
// Part of the cockpit; just hide it.
|
||||||
ShowWindow(hwnd, SW_HIDE);
|
ShowWindow(hwnd, SW_HIDE);
|
||||||
@@ -121,6 +265,8 @@ MFDSplitView::MFDSplitView(
|
|||||||
sourceHeight = source_height;
|
sourceHeight = source_height;
|
||||||
buttonCount = 0;
|
buttonCount = 0;
|
||||||
pressedIndex = -1;
|
pressedIndex = -1;
|
||||||
|
paneTitle = title;
|
||||||
|
ownWindow = (parent == NULL) ? True : False;
|
||||||
|
|
||||||
pixels = new unsigned long[source_width * source_height];
|
pixels = new unsigned long[source_width * source_height];
|
||||||
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
|
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
|
||||||
@@ -135,6 +281,10 @@ MFDSplitView::MFDSplitView(
|
|||||||
header->biCompression = BI_RGB;
|
header->biCompression = BI_RGB;
|
||||||
blitHeader = header;
|
blitHeader = header;
|
||||||
|
|
||||||
|
buttonStyle = button_style;
|
||||||
|
buttonAnchorA = anchorA;
|
||||||
|
buttonAnchorB = anchorB;
|
||||||
|
|
||||||
int client_width = display_width;
|
int client_width = display_width;
|
||||||
int client_height = display_height;
|
int client_height = display_height;
|
||||||
LayoutButtons(button_style, anchorA, anchorB,
|
LayoutButtons(button_style, anchorA, anchorB,
|
||||||
@@ -199,6 +349,14 @@ MFDSplitView::MFDSplitView(
|
|||||||
{
|
{
|
||||||
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
|
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
|
||||||
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
||||||
|
|
||||||
|
// only the exploded view's own windows can be dragged, so only
|
||||||
|
// those have a placement worth remembering. Position only - see
|
||||||
|
// RPWindowLayout_Register.
|
||||||
|
if (ownWindow)
|
||||||
|
{
|
||||||
|
RPWindowLayout_Register(window, paneTitle, False);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -210,6 +368,9 @@ MFDSplitView::~MFDSplitView()
|
|||||||
{
|
{
|
||||||
Check_Pointer(this);
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
// out of the layout registry before the window goes
|
||||||
|
RPWindowLayout_Forget(window);
|
||||||
|
|
||||||
if (window != NULL)
|
if (window != NULL)
|
||||||
{
|
{
|
||||||
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
|
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
|
||||||
@@ -228,6 +389,272 @@ Logical
|
|||||||
return pixels != NULL;
|
return pixels != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Join the saved layout. Registering twice just refreshes the entry, so
|
||||||
|
// a window re-registering after being rebuilt is harmless.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
RPWindowLayout_Register(void *window, const char *title, Logical with_size)
|
||||||
|
{
|
||||||
|
if (window == NULL || title == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
if (layoutWindows[i].window == window)
|
||||||
|
{
|
||||||
|
layoutWindows[i].title = title;
|
||||||
|
layoutWindows[i].withSize = with_size;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (layoutWindowCount >= maxLayoutWindows)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layoutWindows[layoutWindowCount].window = window;
|
||||||
|
layoutWindows[layoutWindowCount].title = title;
|
||||||
|
layoutWindows[layoutWindowCount].withSize = with_size;
|
||||||
|
layoutWindows[layoutWindowCount].bare = False;
|
||||||
|
++layoutWindowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RPWindowLayout_Forget(void *window)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
if (layoutWindows[i].window == window)
|
||||||
|
{
|
||||||
|
layoutWindows[i] = layoutWindows[--layoutWindowCount];
|
||||||
|
layoutWindows[layoutWindowCount].window = NULL;
|
||||||
|
layoutWindows[layoutWindowCount].title = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Sticky placement: restore the saved placement over the computed one.
|
||||||
|
//
|
||||||
|
// Called once everything has been built and placed, so a window the file
|
||||||
|
// does not mention simply keeps where it was put. Size comes back only
|
||||||
|
// for windows registered with it - see the header.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
RPWindowLayout_Load()
|
||||||
|
{
|
||||||
|
if (LayoutMode() == LayoutOff || layoutWindowCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *file = fopen(layoutFileName, "rt");
|
||||||
|
if (file == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: no " << layoutFileName
|
||||||
|
<< " yet (using the computed arrangement)\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int restored = 0;
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), file) != NULL)
|
||||||
|
{
|
||||||
|
char *cursor = line;
|
||||||
|
while (*cursor == ' ' || *cursor == '\t')
|
||||||
|
{
|
||||||
|
++cursor;
|
||||||
|
}
|
||||||
|
if (*cursor == '#' || *cursor == '\r' || *cursor == '\n' || *cursor == '\0')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// titles carry no '=', so the first one splits the line
|
||||||
|
char *equals = strchr(cursor, '=');
|
||||||
|
if (equals == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*equals = '\0';
|
||||||
|
char *end = equals;
|
||||||
|
while (end > cursor && (end[-1] == ' ' || end[-1] == '\t'))
|
||||||
|
{
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
*end = '\0';
|
||||||
|
|
||||||
|
int x = 0, y = 0, w = 0, h = 0;
|
||||||
|
if (sscanf(equals + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Optional trailing keyword, "x,y,w,h,noframe". Searched for
|
||||||
|
// rather than parsed in place so a hand-edited line survives a
|
||||||
|
// stray space or a missing comma.
|
||||||
|
//
|
||||||
|
Logical bare = (strstr(equals + 1, "noframe") != NULL) ? True : False;
|
||||||
|
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
LayoutWindow &entry = layoutWindows[i];
|
||||||
|
if (entry.title == NULL || strcmp(entry.title, cursor) != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HWND target = (HWND) entry.window;
|
||||||
|
if (target == NULL || !IsWindow(target))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a saved size of nothing is a corrupt line, not an instruction
|
||||||
|
Logical size_it = (entry.withSize && w > 0 && h > 0) ? True : False;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Saved coordinates belong to whatever monitors were plugged
|
||||||
|
// in that day. If the placement lands on none of today's, drop
|
||||||
|
// it and keep the computed one - restoring the game window
|
||||||
|
// somewhere invisible would leave nothing to drag back.
|
||||||
|
//
|
||||||
|
RECT landing;
|
||||||
|
landing.left = x;
|
||||||
|
landing.top = y;
|
||||||
|
if (size_it)
|
||||||
|
{
|
||||||
|
landing.right = x + w;
|
||||||
|
landing.bottom = y + h;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RECT current;
|
||||||
|
if (!GetWindowRect(target, ¤t))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
landing.right = x + (current.right - current.left);
|
||||||
|
landing.bottom = y + (current.bottom - current.top);
|
||||||
|
}
|
||||||
|
if (MonitorFromRect(&landing, MONITOR_DEFAULTTONULL) == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: " << cursor << " was saved at "
|
||||||
|
<< x << "," << y << " - off every monitor now, ignoring\n"
|
||||||
|
<< std::flush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Frame off, if the line asked. A bare window's rect IS its
|
||||||
|
// client rect, so whatever client area it has now is the size
|
||||||
|
// to hand back - which also makes the round trip stable: once
|
||||||
|
// bare, what Save records is already the client.
|
||||||
|
//
|
||||||
|
entry.bare = bare;
|
||||||
|
if (bare)
|
||||||
|
{
|
||||||
|
if (!size_it)
|
||||||
|
{
|
||||||
|
RECT client;
|
||||||
|
if (GetClientRect(target, &client))
|
||||||
|
{
|
||||||
|
w = client.right;
|
||||||
|
h = client.bottom;
|
||||||
|
size_it = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StripWindowFrame(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetWindowPos(target, NULL, x, y, w, h,
|
||||||
|
SWP_NOZORDER | SWP_NOACTIVATE |
|
||||||
|
(size_it ? 0 : SWP_NOSIZE) | (bare ? SWP_FRAMECHANGED : 0));
|
||||||
|
++restored;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "MFDLayout: restored " << restored << " window placement(s) from "
|
||||||
|
<< layoutFileName << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Write where every registered window currently is. The whole file each
|
||||||
|
// time - it is a handful of lines, and a rewrite leaves nothing
|
||||||
|
// half-written for a hard kill to catch.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
RPWindowLayout_Save()
|
||||||
|
{
|
||||||
|
if (LayoutMode() != LayoutSave || layoutWindowCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *file = fopen(layoutFileName, "wt");
|
||||||
|
if (file == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "MFDLayout: could not write " << layoutFileName
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fputs("# RP412 window placement. RP412MFDLAYOUT=save writes this on each\n"
|
||||||
|
"# finished drag and on exit; =load restores it.\n"
|
||||||
|
"# <title>=<x>,<y>,<w>,<h>. The game window gets its size back too;\n"
|
||||||
|
"# for the display panes the size is reference only, since theirs\n"
|
||||||
|
"# follows their content.\n"
|
||||||
|
"# Append ,noframe to take that window's title bar and border off.\n"
|
||||||
|
"# Put it where you want it first - a bare window has nothing to\n"
|
||||||
|
"# drag by, so its line stops changing once the frame is gone.\n", file);
|
||||||
|
|
||||||
|
int wrote = 0;
|
||||||
|
for (int i = 0; i < layoutWindowCount; ++i)
|
||||||
|
{
|
||||||
|
LayoutWindow &entry = layoutWindows[i];
|
||||||
|
if (entry.title == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HWND entry_window = (HWND) entry.window;
|
||||||
|
if (entry_window == NULL || !IsWindow(entry_window))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// rcNormalPosition rather than GetWindowRect: a minimised window
|
||||||
|
// reports a nonsense rect and a maximised one reports the screen,
|
||||||
|
// and neither is what to come back to. This is the restored
|
||||||
|
// placement whatever state the window is in, so quitting from
|
||||||
|
// maximised still records where the window will reappear.
|
||||||
|
//
|
||||||
|
WINDOWPLACEMENT placement;
|
||||||
|
memset(&placement, 0, sizeof(placement));
|
||||||
|
placement.length = sizeof(placement);
|
||||||
|
if (!GetWindowPlacement(entry_window, &placement))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RECT bounds = placement.rcNormalPosition;
|
||||||
|
// the flag is the player's instruction, not something measured off
|
||||||
|
// the window, so a rewrite has to carry it back out
|
||||||
|
fprintf(file, "%s=%ld,%ld,%ld,%ld%s\n", entry.title,
|
||||||
|
(long) bounds.left, (long) bounds.top,
|
||||||
|
(long) (bounds.right - bounds.left),
|
||||||
|
(long) (bounds.bottom - bounds.top),
|
||||||
|
entry.bare ? ",noframe" : "");
|
||||||
|
++wrote;
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "MFDLayout: saved " << wrote << " window placement(s) to "
|
||||||
|
<< layoutFileName << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Compute the display rectangle and the button rectangles, growing the
|
// Compute the display rectangle and the button rectangles, growing the
|
||||||
// client area to make room for the strips.
|
// client area to make room for the strips.
|
||||||
@@ -255,10 +682,20 @@ void
|
|||||||
// edges (no border against the viewscreen behind); RIO
|
// edges (no border against the viewscreen behind); RIO
|
||||||
// addresses descend from the anchor, row-major (vRIO
|
// addresses descend from the anchor, row-major (vRIO
|
||||||
// CockpitLayout::Mfd).
|
// CockpitLayout::Mfd).
|
||||||
|
//
|
||||||
|
// The banks reach BEHIND the display: each button is
|
||||||
|
// mfdButtonHeight tall but only mfdIndicatorStrip of it
|
||||||
|
// clears the glass edge, so the lamp reads as a thin strip
|
||||||
|
// and the picture above/below it is the click target. Paint
|
||||||
|
// draws the buttons before the MFD image for that reason.
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
int strip_h = display_height / 8;
|
int strip_h = indicatorStrip + ProtrudeBonus();
|
||||||
if (strip_h < 18) strip_h = 18;
|
int button_h = buttonDepth;
|
||||||
if (strip_h > 40) strip_h = 40;
|
|
||||||
|
// on a scaled-down cockpit, keep the two banks from meeting
|
||||||
|
int limit = (display_height + 2 * strip_h) / 2;
|
||||||
|
if (button_h > limit) button_h = limit;
|
||||||
|
if (button_h < strip_h) button_h = strip_h;
|
||||||
|
|
||||||
int button_w = (display_width - 3 * buttonGap) / 4;
|
int button_w = (display_width - 3 * buttonGap) / 4;
|
||||||
|
|
||||||
@@ -267,56 +704,73 @@ void
|
|||||||
|
|
||||||
for (int i = 0; i < 8; ++i)
|
for (int i = 0; i < 8; ++i)
|
||||||
{
|
{
|
||||||
int row = i / 4; // 0 = top strip, 1 = bottom strip
|
int row = i / 4; // 0 = top bank, 1 = bottom bank
|
||||||
int col = i % 4;
|
int col = i % 4;
|
||||||
|
|
||||||
ScreenButton *button = &buttons[buttonCount++];
|
ScreenButton *button = &buttons[buttonCount++];
|
||||||
button->unit = anchorA - i;
|
button->unit = anchorA - i;
|
||||||
button->amber = 0;
|
button->amber = 0;
|
||||||
button->x = col * (button_w + buttonGap);
|
button->x = col * (button_w + buttonGap);
|
||||||
button->y = row ? (displayY + displayH) : 0;
|
button->y = row
|
||||||
|
? (displayY + displayH + strip_h - button_h)
|
||||||
|
: 0;
|
||||||
button->w = (col == 3)
|
button->w = (col == 3)
|
||||||
? (display_width - 3 * (button_w + buttonGap))
|
? (display_width - 3 * (button_w + buttonGap))
|
||||||
: button_w;
|
: button_w;
|
||||||
button->h = strip_h;
|
button->h = button_h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (button_style == SideColumns)
|
else if (button_style == SideColumns)
|
||||||
{
|
{
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
// 6 buttons down each side of the glass, stacked contiguous
|
// 6 buttons down each side of the glass (the pod wires only 6
|
||||||
// and flush to the pane edges, like the pod's strips (the
|
// of each column's 8 addresses to buttons; the rest are Tesla
|
||||||
// pod wires only 6 of each column's 8 addresses to buttons;
|
// relays). Left = anchorA ascending, right = anchorB.
|
||||||
// the rest are Tesla relays). Left = anchorA ascending,
|
//
|
||||||
// right = anchorB.
|
// Same trick as the MFD banks, turned on its side: each
|
||||||
|
// button reaches buttonDepth in behind the map and leaves an
|
||||||
|
// indicatorStrip clearing the edge, so the lamp reads as a
|
||||||
|
// slim column and the map itself is the click target.
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
int col_w = display_width / 8;
|
int strip_w = indicatorStrip + ProtrudeBonus();
|
||||||
if (col_w < 20) col_w = 20;
|
int button_w = buttonDepth;
|
||||||
if (col_w > 40) col_w = 40;
|
|
||||||
|
|
||||||
int button_h = display_height / 6;
|
// keep the two columns from meeting behind a narrow map
|
||||||
|
int limit = (display_width + 2 * strip_w) / 2;
|
||||||
|
if (button_w > limit) button_w = limit;
|
||||||
|
if (button_w < strip_w) button_w = strip_w;
|
||||||
|
|
||||||
displayX = col_w;
|
displayX = strip_w;
|
||||||
*client_width = display_width + 2 * col_w;
|
*client_width = display_width + 2 * strip_w;
|
||||||
|
|
||||||
for (int i = 0; i < 6; ++i)
|
for (int i = 0; i < 6; ++i)
|
||||||
{
|
{
|
||||||
int h = (i == 5) ? (display_height - 5 * button_h) : button_h;
|
//-------------------------------------------------------
|
||||||
|
// Take top and bottom off the legend grid separately and
|
||||||
|
// subtract: scaling a height directly would let rounding
|
||||||
|
// drift the buttons out of step with the labels.
|
||||||
|
//-------------------------------------------------------
|
||||||
|
int cell_top = mapLegendTop + i * mapLegendPitch;
|
||||||
|
int top = (cell_top * display_height) / mapLegendSpan;
|
||||||
|
int bottom =
|
||||||
|
((cell_top + mapLegendCell) * display_height) / mapLegendSpan;
|
||||||
|
int h = bottom - top;
|
||||||
|
if (h < 1) h = 1;
|
||||||
|
|
||||||
ScreenButton *left = &buttons[buttonCount++];
|
ScreenButton *left = &buttons[buttonCount++];
|
||||||
left->unit = anchorA + i;
|
left->unit = anchorA + i;
|
||||||
left->amber = 1;
|
left->amber = 1;
|
||||||
left->x = 0;
|
left->x = 0;
|
||||||
left->y = i * button_h;
|
left->y = top;
|
||||||
left->w = col_w;
|
left->w = button_w;
|
||||||
left->h = h;
|
left->h = h;
|
||||||
|
|
||||||
ScreenButton *right = &buttons[buttonCount++];
|
ScreenButton *right = &buttons[buttonCount++];
|
||||||
right->unit = anchorB + i;
|
right->unit = anchorB + i;
|
||||||
right->amber = 1;
|
right->amber = 1;
|
||||||
right->x = displayX + displayW;
|
right->x = displayX + displayW + strip_w - button_w;
|
||||||
right->y = left->y;
|
right->y = left->y;
|
||||||
right->w = col_w;
|
right->w = button_w;
|
||||||
right->h = h;
|
right->h = h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,15 +796,11 @@ void
|
|||||||
|
|
||||||
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
||||||
|
|
||||||
SetStretchBltMode(mem, COLORONCOLOR);
|
//---------------------------------------------------------------
|
||||||
StretchDIBits(
|
// Buttons first, glass second: the MFD banks extend under the
|
||||||
mem,
|
// display so the picture is the click target, and the image
|
||||||
displayX, displayY, displayW, displayH,
|
// painted over them leaves only their indicator strips showing.
|
||||||
0, 0, sourceWidth, sourceHeight,
|
//---------------------------------------------------------------
|
||||||
pixels, (const BITMAPINFO *) blitHeader,
|
|
||||||
DIB_RGB_COLORS, SRCCOPY
|
|
||||||
);
|
|
||||||
|
|
||||||
for (int i = 0; i < buttonCount; ++i)
|
for (int i = 0; i < buttonCount; ++i)
|
||||||
{
|
{
|
||||||
const ScreenButton *button = &buttons[i];
|
const ScreenButton *button = &buttons[i];
|
||||||
@@ -375,6 +825,20 @@ void
|
|||||||
DeleteObject(frame);
|
DeleteObject(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HALFTONE area-averages the downscale (the compact glass shows the
|
||||||
|
// full 640x480 canvas at ~half size); COLORONCOLOR dropped every other
|
||||||
|
// row/column, which shredded the 1-bit vector strokes and small text.
|
||||||
|
// HALFTONE requires the brush origin be set (MSDN).
|
||||||
|
SetStretchBltMode(mem, HALFTONE);
|
||||||
|
SetBrushOrgEx(mem, 0, 0, NULL);
|
||||||
|
StretchDIBits(
|
||||||
|
mem,
|
||||||
|
displayX, displayY, displayW, displayH,
|
||||||
|
0, 0, sourceWidth, sourceHeight,
|
||||||
|
pixels, (const BITMAPINFO *) blitHeader,
|
||||||
|
DIB_RGB_COLORS, SRCCOPY
|
||||||
|
);
|
||||||
|
|
||||||
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
||||||
|
|
||||||
SelectObject(mem, old_surface);
|
SelectObject(mem, old_surface);
|
||||||
@@ -422,6 +886,20 @@ void
|
|||||||
// one message per frame, and queued WM_PAINTs (lowest priority) starve
|
// one message per frame, and queued WM_PAINTs (lowest priority) starve
|
||||||
// behind it - panes would freeze on their first frame.
|
// behind it - panes would freeze on their first frame.
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Off screen, and stay off: the pane keeps its pixels and geometry, it
|
||||||
|
// simply stops being shown. Repaint() on a hidden window is harmless.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
MFDSplitView::Hide()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
if (window != NULL)
|
||||||
|
{
|
||||||
|
ShowWindow((HWND) window, SW_HIDE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MFDSplitView::SetPosition(int x, int y)
|
MFDSplitView::SetPosition(int x, int y)
|
||||||
{
|
{
|
||||||
@@ -433,6 +911,34 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Re-scale to a new glass size: the pixel buffer keeps its native
|
||||||
|
// source resolution, so only the destination rectangle, the button
|
||||||
|
// geometry and the window size change.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
void
|
||||||
|
MFDSplitView::Resize(int display_width, int display_height)
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
buttonCount = 0;
|
||||||
|
pressedIndex = -1;
|
||||||
|
|
||||||
|
int client_width = display_width;
|
||||||
|
int client_height = display_height;
|
||||||
|
LayoutButtons(buttonStyle, buttonAnchorA, buttonAnchorB,
|
||||||
|
display_width, display_height, &client_width, &client_height);
|
||||||
|
clientWidth = client_width;
|
||||||
|
clientHeight = client_height;
|
||||||
|
|
||||||
|
if (window != NULL)
|
||||||
|
{
|
||||||
|
SetWindowPos((HWND) window, NULL, 0, 0,
|
||||||
|
client_width, client_height,
|
||||||
|
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MFDSplitView::Repaint()
|
MFDSplitView::Repaint()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,6 +65,26 @@ public:
|
|||||||
void
|
void
|
||||||
SetPosition(int x, int y);
|
SetPosition(int x, int y);
|
||||||
|
|
||||||
|
// Re-scale the glass (cockpit resized): the source buffer is
|
||||||
|
// unchanged, only the destination size and the button geometry
|
||||||
|
// derived from it.
|
||||||
|
void
|
||||||
|
Resize(int display_width, int display_height);
|
||||||
|
|
||||||
|
// Take the pane off screen. Used for the Winners Circle, which wants
|
||||||
|
// the whole viewscreen: the panes overlap it like the pod's bezels,
|
||||||
|
// so hiding them uncovers the full canvas underneath.
|
||||||
|
void
|
||||||
|
Hide();
|
||||||
|
|
||||||
|
// caption and HWND, for the sticky-placement file
|
||||||
|
const char *
|
||||||
|
Title() const
|
||||||
|
{ return paneTitle; }
|
||||||
|
void *
|
||||||
|
Window()
|
||||||
|
{ return window; }
|
||||||
|
|
||||||
~MFDSplitView();
|
~MFDSplitView();
|
||||||
|
|
||||||
Logical
|
Logical
|
||||||
@@ -128,4 +148,86 @@ protected:
|
|||||||
buttonCount;
|
buttonCount;
|
||||||
int
|
int
|
||||||
pressedIndex;
|
pressedIndex;
|
||||||
|
|
||||||
|
// kept so the bank can be rebuilt when the cockpit re-scales
|
||||||
|
ButtonStyle
|
||||||
|
buttonStyle;
|
||||||
|
int
|
||||||
|
buttonAnchorA,
|
||||||
|
buttonAnchorB;
|
||||||
|
|
||||||
|
// the window caption, and the key this pane is saved under
|
||||||
|
const char
|
||||||
|
*paneTitle;
|
||||||
|
// True for a pane of its own on the desktop (the exploded view); the
|
||||||
|
// composited cockpit's panes are chrome-less children and cannot be
|
||||||
|
// dragged, so there is nothing to remember for them.
|
||||||
|
Logical
|
||||||
|
ownWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Sticky window placement.
|
||||||
|
//
|
||||||
|
// The game window and, in the exploded view, each display pane are all
|
||||||
|
// draggable, but their placement is recomputed on every launch - so
|
||||||
|
// putting a window somewhere useful never survived the menu-race-menu
|
||||||
|
// loop. RP412MFDLAYOUT remembers it, in mfd_layout.cfg beside
|
||||||
|
// bindings.txt:
|
||||||
|
//
|
||||||
|
// off / 0 / unset computed placement only, no file (default)
|
||||||
|
// load / restore restore saved placement at startup, never write
|
||||||
|
// save / adjust restore, then rewrite on each finished drag and
|
||||||
|
// on teardown - the round trip
|
||||||
|
//
|
||||||
|
// One "<title>=x,y,w,h" line per window, plus an optional ",noframe"
|
||||||
|
// that takes that window's title bar and border off - for a cockpit that
|
||||||
|
// fills a monitor edge to edge without -fit's all-or-nothing, or an
|
||||||
|
// exploded pane photographed without chrome. A bare window has nothing
|
||||||
|
// to drag by, so place it first and add the flag after; Save carries the
|
||||||
|
// flag back out, since it is an instruction rather than something
|
||||||
|
// measured off the window. Windows are always built framed, so deleting
|
||||||
|
// the flag is all it takes to get the frame back.
|
||||||
|
//
|
||||||
|
// Whether the size comes back depends on the window, which is why
|
||||||
|
// Register takes it as a flag:
|
||||||
|
//
|
||||||
|
// display panes position only. A pane's size follows its content and
|
||||||
|
// its button banks, so an old size from a different
|
||||||
|
// build must not distort it. The plasma glass registers
|
||||||
|
// the same way - its size is L4PLASMASCALE's business.
|
||||||
|
// the game window position and size. Nothing derives that size - -res
|
||||||
|
// only decides how sharp the scene is, and the cockpit
|
||||||
|
// fits itself to whatever client area it is given - so
|
||||||
|
// a window sized to suit a monitor should come back
|
||||||
|
// that way, and half-restoring it would be the strange
|
||||||
|
// behaviour. RPL4.CPP registers it before the console
|
||||||
|
// screen, so the saved placement is there from the
|
||||||
|
// first frame rather than arriving when a race starts.
|
||||||
|
//
|
||||||
|
// A placement that lands on none of the monitors currently plugged in is
|
||||||
|
// ignored rather than applied - restoring the game window off screen
|
||||||
|
// would leave nothing to drag back.
|
||||||
|
//
|
||||||
|
// Lives here because the display panes were the first to need it; the
|
||||||
|
// game window joined later rather than grow a second copy of the same
|
||||||
|
// file format.
|
||||||
|
//
|
||||||
|
// Ported from BT411's BT_GLASS_LAYOUT.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
// Take part in the saved layout. title is the key in the file and must
|
||||||
|
// outlive the window (the callers pass string literals).
|
||||||
|
void
|
||||||
|
RPWindowLayout_Register(void *window, const char *title, Logical with_size);
|
||||||
|
void
|
||||||
|
RPWindowLayout_Forget(void *window);
|
||||||
|
|
||||||
|
// Apply the saved placement over the computed one. Call once everything
|
||||||
|
// has been built and placed.
|
||||||
|
void
|
||||||
|
RPWindowLayout_Load();
|
||||||
|
|
||||||
|
// Write every registered window's placement. No-op unless mode is save.
|
||||||
|
void
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
|||||||
+355
-15
@@ -24,6 +24,7 @@
|
|||||||
#include "..\munga\appmsg.h"
|
#include "..\munga\appmsg.h"
|
||||||
#include "..\munga\mission.h"
|
#include "..\munga\mission.h"
|
||||||
#include "..\munga\notation.h"
|
#include "..\munga\notation.h"
|
||||||
|
#include "..\munga\spooler.h"
|
||||||
//#include <netnub.hpp>
|
//#include <netnub.hpp>
|
||||||
|
|
||||||
|
|
||||||
@@ -63,12 +64,22 @@
|
|||||||
#define CLEAR_LOST_DATA()
|
#define CLEAR_LOST_DATA()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define BATCHED_TRANSMIT True // true sends broadcast messages with a single call to netnub
|
//
|
||||||
#define BATCHED_RECEIVE True // true receives from all streams with a single call to netnub
|
// These three were written as `True`, which is an ENUM (style.h), not a
|
||||||
#define REPORT_LOST_DATA False // Print a message if data is lost (may be toxic)
|
// macro - so the preprocessor evaluated the bare identifiers as 0 and
|
||||||
|
// the batched/buffered netnub paths never compiled. That accident is
|
||||||
|
// what actually shipped in every arcade and every 4.12 build; the dead
|
||||||
|
// branches reference netnub symbols that no longer exist, so "enabling"
|
||||||
|
// them cannot compile. Spelled as literal 0 now so the guards say what
|
||||||
|
// the build does. Send-side loss handling lives in the transport's
|
||||||
|
// send queue these days (RP412NETSENDQ, l4nettransport.h).
|
||||||
|
//
|
||||||
|
#define BATCHED_TRANSMIT 0 // dead netnub path - see note above
|
||||||
|
#define BATCHED_RECEIVE 0 // dead netnub path - see note above
|
||||||
|
#define REPORT_LOST_DATA 0 // dead netnub path - see note above
|
||||||
|
|
||||||
#if !defined(MESSAGE_BUFFERING)
|
#if !defined(MESSAGE_BUFFERING)
|
||||||
#define MESSAGE_BUFFERING True // true uses munga level buffering for dropped packets
|
#define MESSAGE_BUFFERING 0 // dead netnub path - see note above
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define CONSOLE_NET_PORT 1501 // Port number the console will connect on
|
#define CONSOLE_NET_PORT 1501 // Port number the console will connect on
|
||||||
@@ -213,7 +224,15 @@ L4NetworkManager::L4NetworkManager():
|
|||||||
}
|
}
|
||||||
networkEggNotationFile = new NotationFile(egg_name);
|
networkEggNotationFile = new NotationFile(egg_name);
|
||||||
Register_Object(networkEggNotationFile);
|
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;
|
currentNetworkState = NormalState;
|
||||||
|
|
||||||
ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10);
|
ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10);
|
||||||
@@ -378,7 +397,7 @@ void
|
|||||||
|
|
||||||
networkEggNotationFile = new NotationFile(egg_path);
|
networkEggNotationFile = new NotationFile(egg_path);
|
||||||
Register_Object(networkEggNotationFile);
|
Register_Object(networkEggNotationFile);
|
||||||
networkEggNotationFile->WriteFile("last.egg");
|
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||||
|
|
||||||
// In network mode (the owner pod of a multiplayer race) the state
|
// In network mode (the owner pod of a multiplayer race) the state
|
||||||
// gate must open or CheckBuffers keeps dropping mesh packets - a
|
// gate must open or CheckBuffers keeps dropping mesh packets - a
|
||||||
@@ -407,6 +426,13 @@ void
|
|||||||
// the game port is the console listening port + 1
|
// the game port is the console listening port + 1
|
||||||
unsigned short localGamePort = (unsigned short)((L4Application *)application)->GetNetworkCommonFlatAddress() + 1;
|
unsigned short localGamePort = (unsigned short)((L4Application *)application)->GetNetworkCommonFlatAddress() + 1;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Flush before the connect sequence: the egg-ACK ordering the mesh
|
||||||
|
// relies on must not have console bytes parked in a queue while a
|
||||||
|
// blocking connect holds the frame.
|
||||||
|
//
|
||||||
|
NetTransport_Get()->FlushSends();
|
||||||
|
|
||||||
//
|
//
|
||||||
// This should be entered with no network connections to other game hosts up,
|
// This should be entered with no network connections to other game hosts up,
|
||||||
// so we initialize this count to zero.
|
// so we initialize this count to zero.
|
||||||
@@ -447,6 +473,14 @@ void
|
|||||||
Check(application->GetHostManager());
|
Check(application->GetHostManager());
|
||||||
application->GetHostManager()->AdoptLocalHost(my_l4host);
|
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
|
// Now, since this host creator is for stand-alone mode, send the load
|
||||||
@@ -467,6 +501,39 @@ void
|
|||||||
// Iterate through all the host addresses in the egg and create all the host
|
// Iterate through all the host addresses in the egg and create all the host
|
||||||
// structures, note that there MUST be an entry in the egg for us.
|
// structures, note that there MUST be an entry in the egg for us.
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
//------------------------------------------------------------------------
|
||||||
|
// Answer the door before knocking on anyone else's.
|
||||||
|
//
|
||||||
|
// This walk connects out to every host listed ahead of us in the egg and
|
||||||
|
// listens for every host after us - so in list order the connects all come
|
||||||
|
// first, and the listener used to be created lazily on the first host of
|
||||||
|
// the second kind, which is to say AFTER every outbound connect had
|
||||||
|
// already been made. A connect to a pod that is not ready yet blocks for
|
||||||
|
// RP412CONNECTWAIT seconds, three times over, so a pod could spend a
|
||||||
|
// minute unable to answer its door while the very peers it was waiting on
|
||||||
|
// were knocking on it. Their connects to us then timed out too and the
|
||||||
|
// failure went round the ring: playtest logs show pods that never reached
|
||||||
|
// "listening on engine port 1502" at all, and the same pair connecting in
|
||||||
|
// one round and timing out in the next.
|
||||||
|
//
|
||||||
|
// So the walk now only CREATES the hosts, and the outbound connects are
|
||||||
|
// made afterwards. The listener is still opened exactly when it was, on
|
||||||
|
// the first host that will call us - but by then nothing has blocked, so
|
||||||
|
// it happens in the first moments rather than a minute in.
|
||||||
|
//
|
||||||
|
// Note this is why we do not simply open a listener up front: Connect()
|
||||||
|
// and Listen() both bind localGamePort, so a pod at the END of the list -
|
||||||
|
// which listens for nobody - would be binding that port twice and its one
|
||||||
|
// outgoing connection would be fighting its own listener for it.
|
||||||
|
//------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
enum { maxPendingConnects = 16 };
|
||||||
|
L4Host
|
||||||
|
*pending_connect[maxPendingConnects];
|
||||||
|
int
|
||||||
|
pending_connects = 0;
|
||||||
|
|
||||||
listen = False;
|
listen = False;
|
||||||
remoteHostCount = 0;
|
remoteHostCount = 0;
|
||||||
while ((mission_host_data = mission_host_iterator.ReadAndNext()) != NULL)
|
while ((mission_host_data = mission_host_iterator.ReadAndNext()) != NULL)
|
||||||
@@ -531,10 +598,14 @@ void
|
|||||||
socket_ptr = OpenConnection(NETNUB_TCP_LISTEN, localGamePort, 0, net_address.sin_addr.S_un.S_addr);
|
socket_ptr = OpenConnection(NETNUB_TCP_LISTEN, localGamePort, 0, net_address.sin_addr.S_un.S_addr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
socket_ptr = OpenConnection(NETNUB_TCP_OPEN, localGamePort, ntohs(net_address.sin_port), net_address.sin_addr.S_un.S_addr);
|
{
|
||||||
|
//
|
||||||
if (!listen && socket_ptr == INVALID_SOCKET)
|
// Connected after the walk, not here - see above. The
|
||||||
DEBUG_STREAM << "Could not open connection to " << host_name << ".\n" << std::flush;
|
// host is built now so it keeps its place in the list
|
||||||
|
// and its host ID; only the socket arrives late.
|
||||||
|
//
|
||||||
|
socket_ptr = INVALID_SOCKET;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Now we can create the remote host and get the application to adopt
|
// Now we can create the remote host and get the application to adopt
|
||||||
@@ -555,10 +626,196 @@ void
|
|||||||
Check(application);
|
Check(application);
|
||||||
Check(application->GetHostManager());
|
Check(application->GetHostManager());
|
||||||
application->GetHostManager()->AdoptRemoteHost(my_l4host);
|
application->GetHostManager()->AdoptRemoteHost(my_l4host);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Every host we have to call, in list order. Each one is
|
||||||
|
// already counted in remoteHostCount, so until its connection
|
||||||
|
// is made the mesh cannot report itself complete and this pod
|
||||||
|
// sits in front of a loading screen for as long as anyone
|
||||||
|
// lets it.
|
||||||
|
//
|
||||||
|
if (!listen && pending_connects < maxPendingConnects)
|
||||||
|
{
|
||||||
|
pending_connect[pending_connects++] = my_l4host;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nextOpenHostID++;
|
nextOpenHostID++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//------------------------------------------------------------------------
|
||||||
|
// Now make the calls.
|
||||||
|
//
|
||||||
|
// Deferred to here so that our listener - opened during the walk, on the
|
||||||
|
// first host that will be calling US - is already up. Whatever these
|
||||||
|
// connects cost, our peers can be completing their half of the mesh
|
||||||
|
// throughout, instead of finding a pod that will not answer until it has
|
||||||
|
// finished waiting on somebody else.
|
||||||
|
//
|
||||||
|
// A peer that does not answer is tried again, because the logs show
|
||||||
|
// exactly that: the same pair timing out on one attempt and connecting on
|
||||||
|
// the next, as the slower pod catches up. Bounded on purpose - the
|
||||||
|
// transport already makes three attempts inside each call, so a round here
|
||||||
|
// is expensive, and a loop of them would turn a bad connection into a hung
|
||||||
|
// game. RP412MESHRETRY sets the extra rounds; 0 is one pass and no retry.
|
||||||
|
//------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
if (pending_connects > 0)
|
||||||
|
{
|
||||||
|
static int retry_rounds = -1;
|
||||||
|
if (retry_rounds < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412MESHRETRY");
|
||||||
|
retry_rounds = (setting != NULL) ? atoi(setting) : 1;
|
||||||
|
if (retry_rounds < 0) { retry_rounds = 0; }
|
||||||
|
if (retry_rounds > 3) { retry_rounds = 3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int round = 0; round <= retry_rounds && pending_connects > 0; ++round)
|
||||||
|
{
|
||||||
|
int still_pending = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < pending_connects; ++i)
|
||||||
|
{
|
||||||
|
L4Host *peer_host = pending_connect[i];
|
||||||
|
Check(peer_host);
|
||||||
|
|
||||||
|
SOCKADDR_IN *peer = peer_host->GetNetworkAddress();
|
||||||
|
Check_Pointer(peer);
|
||||||
|
|
||||||
|
CString peer_name = peer_host->GetSymbolicName();
|
||||||
|
|
||||||
|
if (round > 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "Mesh: retrying " << (LPSTR) peer_name
|
||||||
|
<< " (round " << round << " of " << retry_rounds
|
||||||
|
<< ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET peer_socket = OpenConnection(
|
||||||
|
NETNUB_TCP_OPEN,
|
||||||
|
localGamePort,
|
||||||
|
ntohs(peer->sin_port),
|
||||||
|
peer->sin_addr.S_un.S_addr);
|
||||||
|
|
||||||
|
if (peer_socket != INVALID_SOCKET)
|
||||||
|
{
|
||||||
|
peer_host->SetNetworkSocket((unsigned long) peer_socket);
|
||||||
|
if (round > 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "Mesh: " << (LPSTR) peer_name
|
||||||
|
<< " answered on the retry\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "Could not open connection to "
|
||||||
|
<< (LPSTR) peer_name << ".\n" << std::flush;
|
||||||
|
pending_connect[still_pending++] = peer_host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending_connects = still_pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending_connects > 0)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Say so plainly. Every one of these is counted in
|
||||||
|
// remoteHostCount, so the mesh cannot complete and this pod
|
||||||
|
// will never be told to load the mission - it will simply
|
||||||
|
// appear to the host as a player who never got ready.
|
||||||
|
//
|
||||||
|
DEBUG_STREAM << "Mesh: INCOMPLETE - " << pending_connects
|
||||||
|
<< " peer(s) unreachable, this pod cannot start the mission:\n"
|
||||||
|
<< std::flush;
|
||||||
|
for (int i = 0; i < pending_connects; ++i)
|
||||||
|
{
|
||||||
|
CString gone = pending_connect[i]->GetSymbolicName();
|
||||||
|
DEBUG_STREAM << "Mesh: unreachable: " << (LPSTR) gone
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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.
|
// All the hosts are created, the connects/listens done.
|
||||||
//
|
//
|
||||||
@@ -811,7 +1068,7 @@ void
|
|||||||
#if defined(LAB_ONLY)
|
#if defined(LAB_ONLY)
|
||||||
DEBUG_STREAM << "Created egg\n";
|
DEBUG_STREAM << "Created egg\n";
|
||||||
#endif
|
#endif
|
||||||
networkEggNotationFile->WriteFile("last.egg");
|
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||||
currentNetworkState = NormalState;
|
currentNetworkState = NormalState;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1102,6 +1359,34 @@ void L4NetworkManager::Send(
|
|||||||
our_host_manager = application->GetHostManager();
|
our_host_manager = application->GetHostManager();
|
||||||
Check(our_host_manager);
|
Check(our_host_manager);
|
||||||
base_host = our_host_manager->GetRemoteHost(host_ID);
|
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);
|
l4host = Cast_Object(L4Host*, base_host);
|
||||||
if(l4host->GetConnectStatus() != L4Host::OnLineConnectionStatus)
|
if(l4host->GetConnectStatus() != L4Host::OnLineConnectionStatus)
|
||||||
{
|
{
|
||||||
@@ -1233,6 +1518,16 @@ Logical L4NetworkManager::SendMessageToNetnub(
|
|||||||
Check(host_manager);
|
Check(host_manager);
|
||||||
|
|
||||||
receiving_host = Cast_Object(L4Host*, host_manager->GetRemoteHost(host_ID));
|
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)
|
if (receiving_host->GetConnectStatus() != L4Host::OnLineConnectionStatus)
|
||||||
{
|
{
|
||||||
return True;
|
return True;
|
||||||
@@ -1986,8 +2281,25 @@ Logical
|
|||||||
// line, waiting for connections and so on.
|
// line, waiting for connections and so on.
|
||||||
//
|
//
|
||||||
//WinSock support :ADB 01/06/07
|
//WinSock support :ADB 01/06/07
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// L4NetworkManager::FlushSends Retry anything the wire refused when it was
|
||||||
|
// first sent. Forwards to the transport's queue drain; called from the
|
||||||
|
// application's flush points (before the render, at the top of the receive
|
||||||
|
// pump, and before a connect sequence starts).
|
||||||
|
//
|
||||||
|
void
|
||||||
|
L4NetworkManager::FlushSends()
|
||||||
|
{
|
||||||
|
NetTransport_Get()->FlushSends();
|
||||||
|
}
|
||||||
|
|
||||||
Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet)
|
Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Every receive-pump round is also a send flush point, so bytes a
|
||||||
|
// stalled peer could not take retry at least as often as we poll.
|
||||||
|
//
|
||||||
|
NetTransport_Get()->FlushSends();
|
||||||
//char *current_receive_ptr;
|
//char *current_receive_ptr;
|
||||||
int
|
int
|
||||||
// i,
|
// i,
|
||||||
@@ -2379,7 +2691,8 @@ Logical
|
|||||||
{
|
{
|
||||||
short
|
short
|
||||||
i,
|
i,
|
||||||
host_count,
|
host_count;
|
||||||
|
int
|
||||||
move_size,
|
move_size,
|
||||||
receive_packet_size;
|
receive_packet_size;
|
||||||
Host
|
Host
|
||||||
@@ -2432,8 +2745,35 @@ Logical
|
|||||||
// figure how many bytes the complete packet should be
|
// figure how many bytes the complete packet should be
|
||||||
//
|
//
|
||||||
incoming_packet = (NetworkPacket*)remote_host->pad_buffer;
|
incoming_packet = (NetworkPacket*)remote_host->pad_buffer;
|
||||||
receive_packet_size = (short)(incoming_packet->messageData.messageLength + sizeof(NetworkPacketHeader));
|
//
|
||||||
Verify(receive_packet_size > 0);
|
// The length prefix is untrusted input off the wire. Framing on
|
||||||
|
// this stream is recovered purely from it, so a corrupt or
|
||||||
|
// sheared length is unrecoverable: too small and the stream
|
||||||
|
// never advances, too large and it either overruns the caller's
|
||||||
|
// NETWORKMANAGER_BUFFER_SIZE buffer or exceeds the pad so the
|
||||||
|
// packet can never complete. Any of those means the stream can
|
||||||
|
// no longer be re-framed - treat it exactly like a hard
|
||||||
|
// disconnect (the Verify that used to stand here compiles away
|
||||||
|
// in release). The lower bound mirrors Send's own floor of 8.
|
||||||
|
//
|
||||||
|
if((incoming_packet->messageData.messageLength < 8) ||
|
||||||
|
(incoming_packet->messageData.messageLength >
|
||||||
|
NETWORKMANAGER_BUFFER_SIZE - sizeof(NetworkPacketHeader)))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "Receive: host " << remote_host->GetHostID()
|
||||||
|
<< " framing broken (message length "
|
||||||
|
<< (unsigned long)incoming_packet->messageData.messageLength
|
||||||
|
<< ") - dropping the connection\n" << std::flush;
|
||||||
|
remote_host->pad_tail = 0;
|
||||||
|
HostDisconnectedMessage myHostDisconnected(
|
||||||
|
remote_host->GetHostID(),
|
||||||
|
remote_host->GetNetworkSocket());
|
||||||
|
NetworkClient *dead_stream_client = GetNetworkClientPointer(NetworkManagerClientID);
|
||||||
|
Check(dead_stream_client);
|
||||||
|
dead_stream_client->ReceiveNetworkPacket(NULL, &myHostDisconnected);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
receive_packet_size = (int)(incoming_packet->messageData.messageLength + sizeof(NetworkPacketHeader));
|
||||||
// Are there enough bytes in the buffer to make up this packet?
|
// Are there enough bytes in the buffer to make up this packet?
|
||||||
if(remote_host->pad_tail >= receive_packet_size)
|
if(remote_host->pad_tail >= receive_packet_size)
|
||||||
{
|
{
|
||||||
@@ -2444,7 +2784,7 @@ Logical
|
|||||||
remote_host->pad_buffer,
|
remote_host->pad_buffer,
|
||||||
receive_packet_size,
|
receive_packet_size,
|
||||||
NETWORKMANAGER_BUFFER_SIZE);
|
NETWORKMANAGER_BUFFER_SIZE);
|
||||||
move_size = (short)(remote_host->pad_tail - receive_packet_size);
|
move_size = (int)(remote_host->pad_tail - receive_packet_size);
|
||||||
// don't do the next step if the buffer is empty
|
// don't do the next step if the buffer is empty
|
||||||
// we need to use memmove because the source and destination addresses overlap
|
// we need to use memmove because the source and destination addresses overlap
|
||||||
// and memcopy doesn't know how to deal with that.
|
// and memcopy doesn't know how to deal with that.
|
||||||
|
|||||||
@@ -209,6 +209,9 @@ public:
|
|||||||
|
|
||||||
void StartConnecting(Mission *mission);
|
void StartConnecting(Mission *mission);
|
||||||
|
|
||||||
|
// forwards to the wire transport's queue drain (l4nettransport.h)
|
||||||
|
void FlushSends();
|
||||||
|
|
||||||
Logical Shutdown();
|
Logical Shutdown();
|
||||||
|
|
||||||
Logical CheckBuffers(NetworkPacket *packet);
|
Logical CheckBuffers(NetworkPacket *packet);
|
||||||
|
|||||||
+608
-6
@@ -2,8 +2,345 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4nettransport.h"
|
#include "l4nettransport.h"
|
||||||
|
#include "..\munga\appmgr.h"
|
||||||
#include <Ws2tcpip.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Outbound send queue - shared machinery for both transports.
|
||||||
|
// See the block comment in l4nettransport.h for why this exists.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
Logical
|
||||||
|
NetTransport_SendQueueEnabled()
|
||||||
|
{
|
||||||
|
static int cached = -1;
|
||||||
|
|
||||||
|
if (cached < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412NETSENDQ");
|
||||||
|
|
||||||
|
cached = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
||||||
|
}
|
||||||
|
return cached ? True : False;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
NetTransport_SendQueueCapBytes()
|
||||||
|
{
|
||||||
|
static int cached = -1;
|
||||||
|
|
||||||
|
if (cached < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412NETQUEUEKB");
|
||||||
|
int kilobytes = (setting != NULL) ? atoi(setting) : 256;
|
||||||
|
|
||||||
|
if (kilobytes < 16) { kilobytes = 16; }
|
||||||
|
if (kilobytes > 4096) { kilobytes = 4096; }
|
||||||
|
cached = kilobytes * 1024;
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical
|
||||||
|
NetTransport_StatsEnabled()
|
||||||
|
{
|
||||||
|
static int cached = -1;
|
||||||
|
|
||||||
|
if (cached < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412NETSTATS");
|
||||||
|
cached = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
return cached ? True : False;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetSendQueueTable::NetSendQueueTable()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < MaxQueues; ++i)
|
||||||
|
{
|
||||||
|
queues[i].connection = NetTransport::InvalidConnection;
|
||||||
|
queues[i].buffer = NULL;
|
||||||
|
queues[i].head = 0;
|
||||||
|
queues[i].tail = 0;
|
||||||
|
queues[i].dead = False;
|
||||||
|
queues[i].sentMessages = 0;
|
||||||
|
queues[i].sentBytes = 0;
|
||||||
|
queues[i].wireWrites = 0;
|
||||||
|
queues[i].partialWrites = 0;
|
||||||
|
queues[i].wouldBlocks = 0;
|
||||||
|
queues[i].queuedHighWater = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NetSendQueueTable::~NetSendQueueTable()
|
||||||
|
{
|
||||||
|
ReleaseAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
NetSendQueue *
|
||||||
|
NetSendQueueTable::Find(NetTransport::Connection connection)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < MaxQueues; ++i)
|
||||||
|
{
|
||||||
|
if (queues[i].connection == connection)
|
||||||
|
{
|
||||||
|
return &queues[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetSendQueue *
|
||||||
|
NetSendQueueTable::FindOrAdopt(NetTransport::Connection connection)
|
||||||
|
{
|
||||||
|
NetSendQueue *queue = Find(connection);
|
||||||
|
|
||||||
|
if (queue != NULL)
|
||||||
|
{
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < MaxQueues; ++i)
|
||||||
|
{
|
||||||
|
if (queues[i].connection == NetTransport::InvalidConnection)
|
||||||
|
{
|
||||||
|
queue = &queues[i];
|
||||||
|
queue->connection = connection;
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = 0;
|
||||||
|
queue->dead = False;
|
||||||
|
queue->sentMessages = 0;
|
||||||
|
queue->sentBytes = 0;
|
||||||
|
queue->wireWrites = 0;
|
||||||
|
queue->partialWrites = 0;
|
||||||
|
queue->wouldBlocks = 0;
|
||||||
|
queue->queuedHighWater = 0;
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "NetSendQueue: table full - connection "
|
||||||
|
<< (unsigned long) connection << " gets no queue\n" << std::flush;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
NetSendQueueTable::Release(NetTransport::Connection connection)
|
||||||
|
{
|
||||||
|
NetSendQueue *queue = Find(connection);
|
||||||
|
|
||||||
|
if (queue == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queue->buffer != NULL)
|
||||||
|
{
|
||||||
|
free(queue->buffer);
|
||||||
|
queue->buffer = NULL;
|
||||||
|
}
|
||||||
|
queue->connection = NetTransport::InvalidConnection;
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = 0;
|
||||||
|
queue->dead = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
NetSendQueueTable::ReleaseAll()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < MaxQueues; ++i)
|
||||||
|
{
|
||||||
|
if (queues[i].connection != NetTransport::InvalidConnection)
|
||||||
|
{
|
||||||
|
Release(queues[i].connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical
|
||||||
|
NetSendQueueTable::Append(
|
||||||
|
NetSendQueue *queue,
|
||||||
|
const void *data,
|
||||||
|
int size)
|
||||||
|
{
|
||||||
|
int capacity = NetTransport_SendQueueCapBytes();
|
||||||
|
int pending = queue->PendingBytes();
|
||||||
|
|
||||||
|
if (pending + size > capacity)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Overflow. These are reliable ordered messages - dropping any of
|
||||||
|
// them desyncs the stream - so the only honest answer is to give
|
||||||
|
// up on the connection and let the disconnect path run.
|
||||||
|
//
|
||||||
|
DEBUG_STREAM << "NetSendQueue: connection "
|
||||||
|
<< (unsigned long) queue->connection << " overflowed ("
|
||||||
|
<< pending << " pending + " << size << " > " << capacity
|
||||||
|
<< ") - declaring it dead\n" << std::flush;
|
||||||
|
if (queue->buffer != NULL)
|
||||||
|
{
|
||||||
|
free(queue->buffer);
|
||||||
|
queue->buffer = NULL;
|
||||||
|
}
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = 0;
|
||||||
|
queue->dead = True;
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
if (queue->buffer == NULL)
|
||||||
|
{
|
||||||
|
queue->buffer = (unsigned char *) malloc(capacity);
|
||||||
|
if (queue->buffer == NULL)
|
||||||
|
{
|
||||||
|
queue->dead = True;
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Rejustify before appending when the tail would run off the end -
|
||||||
|
// the bytes ahead of head are already on the wire.
|
||||||
|
//
|
||||||
|
if (queue->tail + size > capacity && queue->head > 0)
|
||||||
|
{
|
||||||
|
memmove(queue->buffer, &queue->buffer[queue->head], pending);
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = pending;
|
||||||
|
}
|
||||||
|
memcpy(&queue->buffer[queue->tail], data, size);
|
||||||
|
queue->tail += size;
|
||||||
|
if ((unsigned long) queue->PendingBytes() > queue->queuedHighWater)
|
||||||
|
{
|
||||||
|
queue->queuedHighWater = (unsigned long) queue->PendingBytes();
|
||||||
|
}
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
//########################################################################
|
//########################################################################
|
||||||
// WinsockNetTransport - the TCP wire the arcade always used, moved
|
// WinsockNetTransport - the TCP wire the arcade always used, moved
|
||||||
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
|
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
|
||||||
@@ -22,7 +359,8 @@ namespace
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WinsockNetTransport():
|
WinsockNetTransport():
|
||||||
started(False)
|
started(False),
|
||||||
|
statsNextTick(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +387,8 @@ namespace
|
|||||||
{
|
{
|
||||||
if (started)
|
if (started)
|
||||||
{
|
{
|
||||||
|
FlushSends();
|
||||||
|
sendQueues.ReleaseAll();
|
||||||
WSACleanup();
|
WSACleanup();
|
||||||
started = False;
|
started = False;
|
||||||
}
|
}
|
||||||
@@ -117,7 +457,28 @@ namespace
|
|||||||
// the peer may not be listening yet. A refused TCP socket
|
// the peer may not be listening yet. A refused TCP socket
|
||||||
// is dead - every attempt needs a fresh one.
|
// 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 (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
@@ -164,6 +525,7 @@ namespace
|
|||||||
closesocket(sock);
|
closesocket(sock);
|
||||||
return InvalidConnection;
|
return InvalidConnection;
|
||||||
}
|
}
|
||||||
|
NetTransport_SetWaitProgress(NULL);
|
||||||
return (Connection) sock;
|
return (Connection) sock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +536,35 @@ namespace
|
|||||||
{
|
{
|
||||||
DEBUG_STREAM << "ERROR: connect() failed with "
|
DEBUG_STREAM << "ERROR: connect() failed with "
|
||||||
<< wsa_error << "!\n" << std::flush;
|
<< wsa_error << "!\n" << std::flush;
|
||||||
|
NetTransport_SetWaitProgress(NULL);
|
||||||
return InvalidConnection;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +640,19 @@ namespace
|
|||||||
void
|
void
|
||||||
Close(Connection connection)
|
Close(Connection connection)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Best-effort last drain, so an orderly close does not strand
|
||||||
|
// bytes a slow connection still owes the wire.
|
||||||
|
//
|
||||||
|
NetSendQueue *queue = sendQueues.Find(connection);
|
||||||
|
if (queue != NULL)
|
||||||
|
{
|
||||||
|
if (!queue->dead && queue->PendingBytes() > 0)
|
||||||
|
{
|
||||||
|
DrainQueue(queue);
|
||||||
|
}
|
||||||
|
sendQueues.Release(connection);
|
||||||
|
}
|
||||||
shutdown((SOCKET) connection, SD_BOTH);
|
shutdown((SOCKET) connection, SD_BOTH);
|
||||||
closesocket((SOCKET) connection);
|
closesocket((SOCKET) connection);
|
||||||
}
|
}
|
||||||
@@ -263,7 +664,151 @@ namespace
|
|||||||
int size
|
int size
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return send((SOCKET) connection, (const char *) data, size, 0);
|
NetSendQueue *queue = sendQueues.Find(connection);
|
||||||
|
|
||||||
|
if (queue != NULL && queue->dead)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NetTransport_SendQueueEnabled())
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Legacy direct send (RP412NETSENDQ=0) - but count and
|
||||||
|
// name the losses the queue would have prevented.
|
||||||
|
//
|
||||||
|
int sent = send((SOCKET) connection, (const char *) data, size, 0);
|
||||||
|
if (sent != size)
|
||||||
|
{
|
||||||
|
static int said = 0;
|
||||||
|
if (said < 8)
|
||||||
|
{
|
||||||
|
++said;
|
||||||
|
DEBUG_STREAM << "WinsockNetTransport::Send: legacy mode "
|
||||||
|
<< ((sent < 0) ? "lost a message of " : "sheared the stream at ")
|
||||||
|
<< size << " bytes (send returned " << sent
|
||||||
|
<< ", error " << WSAGetLastError() << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Every live connection gets a table slot (the buffer itself
|
||||||
|
// stays unallocated until something actually queues), so the
|
||||||
|
// RP412NETSTATS counters exist for healthy connections too.
|
||||||
|
//
|
||||||
|
if (queue == NULL)
|
||||||
|
{
|
||||||
|
queue = sendQueues.FindOrAdopt(connection);
|
||||||
|
if (queue == NULL)
|
||||||
|
{
|
||||||
|
// table full: behave exactly like the legacy send
|
||||||
|
return send((SOCKET) connection, (const char *) data, size, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue->sentMessages++;
|
||||||
|
queue->sentBytes += size;
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIFO discipline: bytes already queued must go first, so a
|
||||||
|
// queued connection only ever appends.
|
||||||
|
//
|
||||||
|
if (queue->PendingBytes() > 0)
|
||||||
|
{
|
||||||
|
if (!sendQueues.Append(queue, data, size))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
DrainQueue(queue);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sent = send((SOCKET) connection, (const char *) data, size, 0);
|
||||||
|
if (sent == size)
|
||||||
|
{
|
||||||
|
queue->wireWrites++;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The wire would not take all of it. Keep the byte-exact
|
||||||
|
// remainder - a partial send left unhealed is a sheared
|
||||||
|
// stream, the failure this queue exists to close.
|
||||||
|
//
|
||||||
|
if (sent < 0)
|
||||||
|
{
|
||||||
|
DWORD error = WSAGetLastError();
|
||||||
|
if (error != WSAEWOULDBLOCK)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinsockNetTransport::Send: hard error "
|
||||||
|
<< error << " - declaring the connection dead\n" << std::flush;
|
||||||
|
queue->dead = True;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
queue->wouldBlocks++;
|
||||||
|
sent = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
queue->wireWrites++;
|
||||||
|
queue->partialWrites++;
|
||||||
|
}
|
||||||
|
if (!sendQueues.Append(queue, (const char *) data + sent, size - sent))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
FlushSends()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < NetSendQueueTable::MaxQueues; ++i)
|
||||||
|
{
|
||||||
|
NetSendQueue *queue = &sendQueues.queues[i];
|
||||||
|
|
||||||
|
if (queue->connection != InvalidConnection &&
|
||||||
|
!queue->dead &&
|
||||||
|
queue->PendingBytes() > 0)
|
||||||
|
{
|
||||||
|
DrainQueue(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412NETSTATS: 5 s per-connection rollup, from here because
|
||||||
|
// FlushSends is the one transport entry the engine calls on a
|
||||||
|
// steady cadence.
|
||||||
|
//
|
||||||
|
if (NetTransport_StatsEnabled())
|
||||||
|
{
|
||||||
|
DWORD now = GetTickCount();
|
||||||
|
if ((LONG) (now - statsNextTick) >= 0)
|
||||||
|
{
|
||||||
|
statsNextTick = now + 5000;
|
||||||
|
for (int j = 0; j < NetSendQueueTable::MaxQueues; ++j)
|
||||||
|
{
|
||||||
|
NetSendQueue *queue = &sendQueues.queues[j];
|
||||||
|
if (queue->connection == InvalidConnection ||
|
||||||
|
queue->sentMessages == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "NetStats: tcp conn "
|
||||||
|
<< (unsigned long) queue->connection << ": "
|
||||||
|
<< queue->sentMessages << " msgs "
|
||||||
|
<< queue->sentBytes << " B, "
|
||||||
|
<< queue->wireWrites << " writes ("
|
||||||
|
<< queue->partialWrites << " partial, "
|
||||||
|
<< queue->wouldBlocks << " would-block), queued high "
|
||||||
|
<< queue->queuedHighWater << " B, pending "
|
||||||
|
<< queue->PendingBytes()
|
||||||
|
<< (queue->dead ? " B, DEAD" : " B")
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
@@ -273,6 +818,16 @@ namespace
|
|||||||
int size
|
int size
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// A connection the send side declared dead reports as a
|
||||||
|
// disconnect here, so the one disconnect path upstairs runs.
|
||||||
|
//
|
||||||
|
NetSendQueue *queue = sendQueues.Find(connection);
|
||||||
|
if (queue != NULL && queue->dead)
|
||||||
|
{
|
||||||
|
return ReceiveDisconnected;
|
||||||
|
}
|
||||||
|
|
||||||
int received = recv((SOCKET) connection, (char *) buffer, size, 0);
|
int received = recv((SOCKET) connection, (char *) buffer, size, 0);
|
||||||
if (received > 0)
|
if (received > 0)
|
||||||
{
|
{
|
||||||
@@ -310,8 +865,55 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Logical started;
|
//
|
||||||
WSADATA winsockData;
|
// Push a queue's pending bytes at the wire until it is empty or
|
||||||
|
// the wire refuses. Partial progress advances head byte-exactly.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DrainQueue(NetSendQueue *queue)
|
||||||
|
{
|
||||||
|
while (queue->PendingBytes() > 0 && !queue->dead)
|
||||||
|
{
|
||||||
|
int sent = send(
|
||||||
|
(SOCKET) queue->connection,
|
||||||
|
(const char *) &queue->buffer[queue->head],
|
||||||
|
queue->PendingBytes(), 0);
|
||||||
|
|
||||||
|
if (sent > 0)
|
||||||
|
{
|
||||||
|
queue->wireWrites++;
|
||||||
|
queue->head += sent;
|
||||||
|
if (queue->PendingBytes() == 0)
|
||||||
|
{
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
queue->partialWrites++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD error = WSAGetLastError();
|
||||||
|
if (sent < 0 && error == WSAEWOULDBLOCK)
|
||||||
|
{
|
||||||
|
queue->wouldBlocks++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "WinsockNetTransport::DrainQueue: hard error "
|
||||||
|
<< error << " with " << queue->PendingBytes()
|
||||||
|
<< " bytes pending - declaring the connection dead\n" << std::flush;
|
||||||
|
queue->dead = True;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical started;
|
||||||
|
WSADATA winsockData;
|
||||||
|
NetSendQueueTable sendQueues;
|
||||||
|
DWORD statsNextTick;
|
||||||
};
|
};
|
||||||
|
|
||||||
WinsockNetTransport gWinsockTransport;
|
WinsockNetTransport gWinsockTransport;
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ public:
|
|||||||
int size
|
int size
|
||||||
) = 0;
|
) = 0;
|
||||||
|
|
||||||
|
// Retry anything a connection could not take when Send was called.
|
||||||
|
// The engine calls this at its flush points (before the render, at
|
||||||
|
// the top of the receive pump, before a connect sequence); default
|
||||||
|
// is a no-op so a transport without queues needs nothing.
|
||||||
|
virtual void
|
||||||
|
FlushSends()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// remote endpoint of a live connection (mesh identity checks)
|
// remote endpoint of a live connection (mesh identity checks)
|
||||||
virtual Logical
|
virtual Logical
|
||||||
GetRemoteAddress(
|
GetRemoteAddress(
|
||||||
@@ -124,3 +133,136 @@ NetTransport *
|
|||||||
NetTransport_Get();
|
NetTransport_Get();
|
||||||
void
|
void
|
||||||
NetTransport_Set(NetTransport *transport);
|
NetTransport_Set(NetTransport *transport);
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Outbound send queue (RP412NETSENDQ).
|
||||||
|
//
|
||||||
|
// Historically every send result was discarded. On the 1 ms arcade LAN
|
||||||
|
// the socket buffer never filled, so nothing was ever lost - but over
|
||||||
|
// the internet a peer stalled in its 10-30 s mission load stops reading,
|
||||||
|
// its window closes, and our nonblocking send starts returning
|
||||||
|
// would-block (message silently vanished) or a PARTIAL count. A partial
|
||||||
|
// send is the worse of the two: framing on the stream is recovered
|
||||||
|
// purely from each message's length prefix, so the bytes that never
|
||||||
|
// followed shear the stream for good.
|
||||||
|
//
|
||||||
|
// The queue closes both holes: a connection that cannot take the bytes
|
||||||
|
// now keeps them, byte-exact, and FlushSends() retries at the engine's
|
||||||
|
// flush points. Nothing is ever dropped from the middle of the stream -
|
||||||
|
// these are reliable, ordered, non-idempotent messages, so the only
|
||||||
|
// legal overflow response is to declare the connection dead and let the
|
||||||
|
// normal disconnect path run (Receive() reports it).
|
||||||
|
//
|
||||||
|
// RP412NETSENDQ=0 restores the old direct sends but keeps counting and
|
||||||
|
// logging the losses it would have prevented.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
Logical
|
||||||
|
NetTransport_SendQueueEnabled(); // RP412NETSENDQ, default on
|
||||||
|
int
|
||||||
|
NetTransport_SendQueueCapBytes(); // RP412NETQUEUEKB * 1024, default 256K
|
||||||
|
Logical
|
||||||
|
NetTransport_StatsEnabled(); // RP412NETSTATS, default off: 5 s
|
||||||
|
// per-connection rollups (and, on
|
||||||
|
// Steam, a 1 Hz ping/quality poll)
|
||||||
|
// printed from FlushSends
|
||||||
|
|
||||||
|
class NetSendQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NetTransport::Connection
|
||||||
|
connection; // InvalidConnection marks a free slot
|
||||||
|
unsigned char
|
||||||
|
*buffer; // allocated on first append
|
||||||
|
int
|
||||||
|
head, // first unsent byte
|
||||||
|
tail; // one past the last queued byte
|
||||||
|
Logical
|
||||||
|
dead; // overflow or hard error; Receive() turns this
|
||||||
|
// into ReceiveDisconnected
|
||||||
|
|
||||||
|
// telemetry, printed under RP412NETSTATS
|
||||||
|
unsigned long
|
||||||
|
sentMessages, // engine Send() calls accepted
|
||||||
|
sentBytes,
|
||||||
|
wireWrites, // actual writes on the wire
|
||||||
|
partialWrites, // wire writes that took only part of the bytes
|
||||||
|
wouldBlocks, // wire writes refused outright
|
||||||
|
queuedHighWater; // worst PendingBytes() seen
|
||||||
|
|
||||||
|
int
|
||||||
|
PendingBytes() const
|
||||||
|
{
|
||||||
|
return tail - head;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class NetSendQueueTable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum { MaxQueues = 24 }; // 8 pods' mesh + console + listeners + slack
|
||||||
|
|
||||||
|
NetSendQueueTable();
|
||||||
|
~NetSendQueueTable();
|
||||||
|
|
||||||
|
NetSendQueue *
|
||||||
|
Find(NetTransport::Connection connection);
|
||||||
|
NetSendQueue *
|
||||||
|
FindOrAdopt(NetTransport::Connection connection);
|
||||||
|
void
|
||||||
|
Release(NetTransport::Connection connection);
|
||||||
|
void
|
||||||
|
ReleaseAll();
|
||||||
|
|
||||||
|
// Append bytes to a queue. False = overflow: the pending bytes are
|
||||||
|
// freed and the queue is marked dead (see the block comment above).
|
||||||
|
Logical
|
||||||
|
Append(
|
||||||
|
NetSendQueue *queue,
|
||||||
|
const void *data,
|
||||||
|
int size);
|
||||||
|
|
||||||
|
NetSendQueue
|
||||||
|
queues[MaxQueues];
|
||||||
|
};
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// 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();
|
||||||
|
|||||||
+247
-32
@@ -2,6 +2,7 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4padbindings.h"
|
#include "l4padbindings.h"
|
||||||
|
#include "l4joy.h" // joyButtonCount / joyHatCount, the parse limits
|
||||||
|
|
||||||
#include <XInput.h>
|
#include <XInput.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -79,6 +80,20 @@ namespace
|
|||||||
{ "Throttle", BindAxisThrottle },
|
{ "Throttle", BindAxisThrottle },
|
||||||
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
||||||
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
|
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
|
||||||
|
{ "Pedals", BindAxisPedals },
|
||||||
|
};
|
||||||
|
|
||||||
|
// DirectInput's axis order, which is what the joy* rows name
|
||||||
|
const NameValue kJoyAxisNames[] =
|
||||||
|
{
|
||||||
|
{ "X", BindJoyAxisX }, { "Y", BindJoyAxisY }, { "Z", BindJoyAxisZ },
|
||||||
|
{ "RX", BindJoyAxisRX }, { "RY", BindJoyAxisRY }, { "RZ", BindJoyAxisRZ },
|
||||||
|
{ "SL0", BindJoyAxisSL0 }, { "SL1", BindJoyAxisSL1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameValue kJoyHatNames[] =
|
||||||
|
{
|
||||||
|
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
|
||||||
};
|
};
|
||||||
|
|
||||||
Logical NameEquals(const char *a, const char *b)
|
Logical NameEquals(const char *a, const char *b)
|
||||||
@@ -185,10 +200,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)
|
if (token_count < 4)
|
||||||
{
|
{
|
||||||
return False;
|
return False;
|
||||||
@@ -267,35 +363,96 @@ namespace
|
|||||||
{
|
{
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
|
//
|
||||||
memset(binding, 0, sizeof(*binding));
|
// Built aside and only then committed: a row whose options
|
||||||
binding->source = source;
|
// go bad half way through is a REJECTED row, and taking the
|
||||||
binding->axis = axis;
|
// slot first would leave the good half of it bound anyway,
|
||||||
for (int i = 4; i < token_count; ++i)
|
// 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"))
|
return False;
|
||||||
{
|
|
||||||
binding->invert = True;
|
|
||||||
}
|
|
||||||
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
|
||||||
{
|
|
||||||
if (!ParseNumber(tokens[++i], &binding->deadzone))
|
|
||||||
{
|
|
||||||
return False;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
|
||||||
{
|
|
||||||
if (!ParseNumber(tokens[++i], &binding->rate))
|
|
||||||
{
|
|
||||||
return False;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return False;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +477,20 @@ namespace
|
|||||||
"# key <name> axis <axis> rate <n-per-second>\n"
|
"# key <name> axis <axis> rate <n-per-second>\n"
|
||||||
"# pad <button> button <addr> [toggle]\n"
|
"# pad <button> button <addr> [toggle]\n"
|
||||||
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||||
|
"# joydev <slot> [product-name substring]\n"
|
||||||
|
"# joyaxis <src> axis <axis> [invert] [lever] [deadzone <d>] [rate <n>]\n"
|
||||||
|
"# joybutton <n> button <addr> [toggle]\n"
|
||||||
|
"# joyhat <n> <up|down|left|right> button <addr>\n"
|
||||||
"#\n"
|
"#\n"
|
||||||
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
||||||
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
|
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
|
||||||
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
||||||
|
"# | Pedals - a signed axis that works the pedal PAIR, positive\n"
|
||||||
|
"# for the right pedal and negative for the left, so one rudder\n"
|
||||||
|
"# bar or twist grip drives both. 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"
|
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
|
||||||
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
||||||
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
||||||
@@ -336,6 +503,44 @@ namespace
|
|||||||
"# back on release; 'rate' walks the axis by <n> per second and the\n"
|
"# back on release; 'rate' walks the axis by <n> per second and the\n"
|
||||||
"# position sticks (the throttle). Every lamp button is also clickable\n"
|
"# position sticks (the throttle). Every lamp button is also clickable\n"
|
||||||
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
|
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
|
||||||
|
"#\n"
|
||||||
|
"# ---- Flight sticks, HOTAS throttles and rudder pedals --------------\n"
|
||||||
|
"#\n"
|
||||||
|
"# EASIEST: run joyconfig.bat once. It asks you to move each control,\n"
|
||||||
|
"# works out which device and axis you moved and which way round it\n"
|
||||||
|
"# reads, and writes the joy* rows below a marker line at the end of\n"
|
||||||
|
"# this file. Everything you have written yourself is kept. Xbox-class\n"
|
||||||
|
"# pads need none of this - they are the pad* rows above.\n"
|
||||||
|
"#\n"
|
||||||
|
"# By hand: joydev picks the device for the rows that follow it - a\n"
|
||||||
|
"# name substring binds that product, a bare slot number binds the Nth\n"
|
||||||
|
"# stick Windows lists. <src> for joyaxis is the DirectInput axis name,\n"
|
||||||
|
"# X Y Z RX RY RZ SL0 SL1: a twist grip is usually RZ and a HOTAS\n"
|
||||||
|
"# throttle usually Z or SL0. A joyaxis on Throttle with no 'rate' is\n"
|
||||||
|
"# treated as a real lever and OWNS the channel - its full travel is\n"
|
||||||
|
"# the full throttle range, rather than nudging the position the way a\n"
|
||||||
|
"# spring-centred pad stick has to.\n"
|
||||||
|
"#\n"
|
||||||
|
"# '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"
|
"\n"
|
||||||
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
||||||
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
||||||
@@ -376,8 +581,11 @@ namespace
|
|||||||
"pad DPadRight button 0x43 # Hat Right\n"
|
"pad DPadRight button 0x43 # Hat Right\n"
|
||||||
"pad LeftShoulder button 0x3D # Panic\n"
|
"pad LeftShoulder button 0x3D # Panic\n"
|
||||||
"pad RightShoulder button 0x3F # Throttle button (reverse)\n"
|
"pad RightShoulder button 0x3F # Throttle button (reverse)\n"
|
||||||
"pad Start button 0x37 # config 1\n"
|
"\n"
|
||||||
"pad Back button 0x36 # config 2\n"
|
"# Start and Back are left free - map them to whatever you want, e.g.\n"
|
||||||
|
"# the config buttons on the Upper Right MFD:\n"
|
||||||
|
"#pad Start button 0x37\n"
|
||||||
|
"#pad Back button 0x36\n"
|
||||||
"\n"
|
"\n"
|
||||||
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
|
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
|
||||||
"# ---- QWERTY row is the row under it, left to right across the\n"
|
"# ---- QWERTY row is the row under it, left to right across the\n"
|
||||||
@@ -496,6 +704,7 @@ void
|
|||||||
char line[256];
|
char line[256];
|
||||||
int line_number = 0;
|
int line_number = 0;
|
||||||
int error_count = 0;
|
int error_count = 0;
|
||||||
|
int joy_slot = 0; // joy rows before any joydev belong to slot 0
|
||||||
const char *cursor = source;
|
const char *cursor = source;
|
||||||
while (*cursor != '\0')
|
while (*cursor != '\0')
|
||||||
{
|
{
|
||||||
@@ -520,7 +729,7 @@ void
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!ParseLine(tokens, token_count, profile))
|
if (!ParseLine(tokens, token_count, profile, &joy_slot))
|
||||||
{
|
{
|
||||||
++error_count;
|
++error_count;
|
||||||
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
||||||
@@ -539,4 +748,10 @@ void
|
|||||||
<< profile->padAxisCount << " pad axes"
|
<< profile->padAxisCount << " pad axes"
|
||||||
<< (error_count ? " (with rejected lines)" : "")
|
<< (error_count ? " (with rejected lines)" : "")
|
||||||
<< "\n" << std::flush;
|
<< "\n" << std::flush;
|
||||||
|
if (profile->joyAxisCount || profile->joyButtonCount || profile->joyHatCount)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "PadBindings: joystick - " << profile->joyAxisCount
|
||||||
|
<< " axes, " << profile->joyButtonCount << " buttons, "
|
||||||
|
<< profile->joyHatCount << " hat directions\n" << std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,32 @@
|
|||||||
// key <name> axis <axis> deflect <n> | rate <n>
|
// key <name> axis <axis> deflect <n> | rate <n>
|
||||||
// pad <button> button <addr> [toggle]
|
// pad <button> button <addr> [toggle]
|
||||||
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||||
|
// joydev <slot> [product-name substring...]
|
||||||
|
// joyaxis <src> axis <axis> [invert] [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
|
// <addr> is a RIO input address: lamp buttons 0x00-0x47, internal
|
||||||
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
|
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
|
||||||
// bindings.txt beside the exe; written there (self-documenting, with
|
// bindings.txt beside the exe; written there (self-documenting, with
|
||||||
// the full default profile) on first run. Bad lines are logged and
|
// the full default profile) on first run. Bad lines are logged and
|
||||||
// skipped, good lines always win.
|
// skipped, good lines always win.
|
||||||
|
//
|
||||||
|
// The joy* rows drive generic DirectInput devices - flight sticks,
|
||||||
|
// HOTAS throttles, twist grips, rudder pedals (L4JOY.h). They attach to
|
||||||
|
// the most recent joydev slot, or slot 0 if no joydev came first. A slot
|
||||||
|
// naming a product substring binds THAT device; a bare slot binds the
|
||||||
|
// Nth attached non-XInput device. Source names follow the DirectInput
|
||||||
|
// layout - X Y Z RX RY RZ SL0 SL1 - where a twist grip is usually RZ
|
||||||
|
// and a HOTAS throttle usually Z or SL0. RP412JOYCONFIG=1 writes these
|
||||||
|
// rows for you by asking the player to move each control.
|
||||||
|
//
|
||||||
|
// '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
|
enum PadBindRioAxis
|
||||||
@@ -33,6 +53,14 @@ enum PadBindRioAxis
|
|||||||
BindAxisRightPedal,
|
BindAxisRightPedal,
|
||||||
BindAxisJoystickY,
|
BindAxisJoystickY,
|
||||||
BindAxisJoystickX,
|
BindAxisJoystickX,
|
||||||
|
//
|
||||||
|
// A signed composite, not a channel the pod has: positive presses
|
||||||
|
// the right pedal, negative the left. One physical control - a
|
||||||
|
// rudder bar, a twist grip, a stick axis - works the pedal pair the
|
||||||
|
// way a foot never could, one or the other and never both. It
|
||||||
|
// decomposes into the real pair when the poll applies it.
|
||||||
|
//
|
||||||
|
BindAxisPedals,
|
||||||
BindAxisCount
|
BindAxisCount
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +117,55 @@ struct PadPadAxisBinding
|
|||||||
Scalar rate; // 0 = direct position, >0 = speed integrate
|
Scalar rate; // 0 = direct position, >0 = speed integrate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generic joystick (DirectInput - L4JOY.h). device is a joydev SLOT,
|
||||||
|
// resolved to a live device at poll time by the slot's name substring or
|
||||||
|
// by its ordinal, so unplugging and replugging does not rewrite the file.
|
||||||
|
//
|
||||||
|
enum { BindJoyDeviceSlots = 4 };
|
||||||
|
|
||||||
|
enum PadBindJoyAxis
|
||||||
|
{
|
||||||
|
BindJoyAxisX = 0,
|
||||||
|
BindJoyAxisY,
|
||||||
|
BindJoyAxisZ,
|
||||||
|
BindJoyAxisRX,
|
||||||
|
BindJoyAxisRY,
|
||||||
|
BindJoyAxisRZ,
|
||||||
|
BindJoyAxisSL0,
|
||||||
|
BindJoyAxisSL1,
|
||||||
|
BindJoyAxisCount
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadJoyAxisBinding
|
||||||
|
{
|
||||||
|
int device; // joydev slot
|
||||||
|
int source; // PadBindJoyAxis
|
||||||
|
int axis; // PadBindRioAxis
|
||||||
|
Logical invert;
|
||||||
|
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
|
struct PadBindingProfile
|
||||||
{
|
{
|
||||||
enum
|
enum
|
||||||
@@ -96,7 +173,10 @@ struct PadBindingProfile
|
|||||||
maxKeyButtons = 128,
|
maxKeyButtons = 128,
|
||||||
maxKeyAxes = 32,
|
maxKeyAxes = 32,
|
||||||
maxPadButtons = 32,
|
maxPadButtons = 32,
|
||||||
maxPadAxes = 16
|
maxPadAxes = 16,
|
||||||
|
maxJoyAxes = 24,
|
||||||
|
maxJoyButtons = 48,
|
||||||
|
maxJoyHats = 16
|
||||||
};
|
};
|
||||||
|
|
||||||
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
||||||
@@ -107,6 +187,15 @@ struct PadBindingProfile
|
|||||||
int padButtonCount;
|
int padButtonCount;
|
||||||
PadPadAxisBinding padAxes[maxPadAxes];
|
PadPadAxisBinding padAxes[maxPadAxes];
|
||||||
int padAxisCount;
|
int padAxisCount;
|
||||||
|
|
||||||
|
PadJoyAxisBinding joyAxes[maxJoyAxes];
|
||||||
|
int joyAxisCount;
|
||||||
|
PadJoyButtonBinding joyButtons[maxJoyButtons];
|
||||||
|
int joyButtonCount;
|
||||||
|
PadJoyHatBinding joyHats[maxJoyHats];
|
||||||
|
int joyHatCount;
|
||||||
|
// "" = take the slot's ordinal attached device
|
||||||
|
char joyDeviceMatch[BindJoyDeviceSlots][64];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load bindings.txt from the working directory into the profile,
|
// Load bindings.txt from the working directory into the profile,
|
||||||
|
|||||||
+312
-5
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "l4padrio.h"
|
#include "l4padrio.h"
|
||||||
#include "l4keylight.h"
|
#include "l4keylight.h"
|
||||||
|
#include "l4joy.h"
|
||||||
|
|
||||||
#include <XInput.h>
|
#include <XInput.h>
|
||||||
#pragma comment(lib, "xinput9_1_0.lib")
|
#pragma comment(lib, "xinput9_1_0.lib")
|
||||||
@@ -41,6 +42,112 @@ namespace
|
|||||||
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
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)
|
void KeyLightLog(const char *line)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << line << "\n" << std::flush;
|
DEBUG_STREAM << line << "\n" << std::flush;
|
||||||
@@ -166,6 +273,34 @@ PadRIO::PadRIO()
|
|||||||
activeInstance = this;
|
activeInstance = this;
|
||||||
|
|
||||||
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
||||||
|
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()
|
PadRIO::~PadRIO()
|
||||||
@@ -244,6 +379,11 @@ void
|
|||||||
profile.padButtons[i].latched = False;
|
profile.padButtons[i].latched = False;
|
||||||
profile.padButtons[i].wasDown = False;
|
profile.padButtons[i].wasDown = False;
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i < profile.joyButtonCount; ++i)
|
||||||
|
{
|
||||||
|
profile.joyButtons[i].latched = False;
|
||||||
|
profile.joyButtons[i].wasDown = False;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -297,6 +437,25 @@ void
|
|||||||
}
|
}
|
||||||
lastPollTick = now;
|
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
|
// Find / keep the XInput pad. Probing empty slots is slow, so an
|
||||||
// absent pad is only re-probed every 3 seconds.
|
// absent pad is only re-probed every 3 seconds.
|
||||||
@@ -305,7 +464,7 @@ void
|
|||||||
memset(&pad, 0, sizeof(pad));
|
memset(&pad, 0, sizeof(pad));
|
||||||
Logical pad_live = False;
|
Logical pad_live = False;
|
||||||
|
|
||||||
if (padIndex >= 0)
|
if (input_live && padIndex >= 0)
|
||||||
{
|
{
|
||||||
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
|
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
|
||||||
if (!pad_live)
|
if (!pad_live)
|
||||||
@@ -314,7 +473,7 @@ void
|
|||||||
padIndex = -1;
|
padIndex = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
if (input_live && padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
||||||
{
|
{
|
||||||
lastPadCheckTick = now;
|
lastPadCheckTick = now;
|
||||||
for (DWORD i = 0; i < 4; ++i)
|
for (DWORD i = 0; i < 4; ++i)
|
||||||
@@ -349,7 +508,7 @@ void
|
|||||||
for (int i = 0; i < profile.keyButtonCount; ++i)
|
for (int i = 0; i < profile.keyButtonCount; ++i)
|
||||||
{
|
{
|
||||||
PadKeyButtonBinding *binding = &profile.keyButtons[i];
|
PadKeyButtonBinding *binding = &profile.keyButtons[i];
|
||||||
Logical down = KeyDown(binding->virtualKey);
|
Logical down = input_live && KeyDown(binding->virtualKey);
|
||||||
if (binding->toggle && down && !binding->wasDown)
|
if (binding->toggle && down && !binding->wasDown)
|
||||||
{
|
{
|
||||||
binding->latched = !binding->latched;
|
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)
|
for (int i = 0; i < buttonUnits; ++i)
|
||||||
{
|
{
|
||||||
if (screenButton[i])
|
if (screenButton[i])
|
||||||
@@ -446,7 +687,7 @@ void
|
|||||||
for (int i = 0; i < profile.keyAxisCount; ++i)
|
for (int i = 0; i < profile.keyAxisCount; ++i)
|
||||||
{
|
{
|
||||||
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
|
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
|
||||||
if (KeyDown(binding->virtualKey))
|
if (input_live && KeyDown(binding->virtualKey))
|
||||||
{
|
{
|
||||||
if (binding->mode == BindKeyRate)
|
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);
|
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
|
||||||
|
|
||||||
Scalar x = deflect[BindAxisJoystickX];
|
Scalar x = deflect[BindAxisJoystickX];
|
||||||
@@ -511,7 +816,9 @@ void
|
|||||||
if (y > 1.0f) y = 1.0f;
|
if (y > 1.0f) y = 1.0f;
|
||||||
if (y < -1.0f) y = -1.0f;
|
if (y < -1.0f) y = -1.0f;
|
||||||
|
|
||||||
Throttle = Clamp01(throttleAccum + deflect[BindAxisThrottle]);
|
Throttle = throttleLever
|
||||||
|
? Clamp01(throttleLeverValue)
|
||||||
|
: Clamp01(throttleAccum + deflect[BindAxisThrottle]);
|
||||||
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
|
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
|
||||||
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
|
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
|
||||||
// The profile encodes the pod's stick sign convention; L4PADFLIP
|
// The profile encodes the pod's stick sign convention; L4PADFLIP
|
||||||
|
|||||||
@@ -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()
|
void ParticleEngine::Destroy()
|
||||||
{
|
{
|
||||||
mVertBuffer->Release();
|
if (mVertBuffer != NULL)
|
||||||
mParticleTexture->Release();
|
{
|
||||||
|
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)
|
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;
|
mDevice = device;
|
||||||
memset(mInstalledEffects, 0, sizeof(mInstalledEffects));
|
memset(mInstalledEffects, 0, sizeof(mInstalledEffects));
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "l4plasmascreen.h"
|
#include "l4plasmascreen.h"
|
||||||
#include "l4app.h"
|
#include "l4app.h"
|
||||||
|
#include "l4mfdview.h" // RPWindowLayout_*
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,15 @@ namespace
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
//
|
||||||
|
// Dragged to a new spot. Write the whole arrangement now rather
|
||||||
|
// than trusting teardown, so a hard kill still leaves what was
|
||||||
|
// on screen. No-op unless RP412MFDLAYOUT is save.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
return 0;
|
||||||
|
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
// The glass is part of the cockpit; just hide it.
|
// The glass is part of the cockpit; just hide it.
|
||||||
ShowWindow(hwnd, SW_HIDE);
|
ShowWindow(hwnd, SW_HIDE);
|
||||||
@@ -176,6 +186,7 @@ PlasmaScreen::~PlasmaScreen()
|
|||||||
}
|
}
|
||||||
if (window != NULL)
|
if (window != NULL)
|
||||||
{
|
{
|
||||||
|
RPWindowLayout_Forget(window);
|
||||||
RemovePropA((HWND) window, "PlasmaBitmapInfo");
|
RemovePropA((HWND) window, "PlasmaBitmapInfo");
|
||||||
RemovePropA((HWND) window, "PlasmaPixels");
|
RemovePropA((HWND) window, "PlasmaPixels");
|
||||||
DestroyWindow((HWND) window);
|
DestroyWindow((HWND) window);
|
||||||
@@ -275,6 +286,22 @@ void
|
|||||||
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
|
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
|
||||||
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
||||||
activeInstance = this;
|
activeInstance = this;
|
||||||
|
|
||||||
|
//
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Join the saved layout. Position only, like the display
|
||||||
|
// panes: the glass is 128x32 at L4PLASMASCALE, so its size
|
||||||
|
// is a setting rather than something to drag.
|
||||||
|
//
|
||||||
|
// Loading here rather than leaving it to SVGA16 keeps this
|
||||||
|
// independent of which is built first - the glass comes from
|
||||||
|
// the gauge renderer, the panes from the video mode, and
|
||||||
|
// whichever runs second re-applies a placement the first
|
||||||
|
// already has.
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
//
|
||||||
|
RPWindowLayout_Register(window, "Plasma Display", False);
|
||||||
|
RPWindowLayout_Load();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -125,6 +125,34 @@ void
|
|||||||
ResolveAddress(host_name, &net_address);
|
ResolveAddress(host_name, &net_address);
|
||||||
Host *host = host_mgr->FindHost(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());
|
*(Logical*)spool->GetPointer() = (host != host_mgr->GetLocalHost());
|
||||||
spool->AdvancePointer(sizeof(Logical));
|
spool->AdvancePointer(sizeof(Logical));
|
||||||
|
|
||||||
@@ -167,11 +195,143 @@ void
|
|||||||
//###################### L4PlaybackNetworkManager #########################
|
//###################### 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():
|
L4PlaybackNetworkManager::L4PlaybackNetworkManager():
|
||||||
NetworkManager(L4PlaybackNetworkManager::DefaultData)
|
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
|
else
|
||||||
{
|
{
|
||||||
host_mgr->AdoptLocalHost(my_host);
|
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
|
// Now, just send the load message
|
||||||
//
|
//
|
||||||
@@ -663,6 +886,32 @@ void
|
|||||||
return;
|
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
|
// We have a spool file, so interpret it based upon the application state
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ public:
|
|||||||
//################### L4PlaybackNetworkManager #########################
|
//################### 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:
|
class L4PlaybackNetworkManager:
|
||||||
public NetworkManager
|
public NetworkManager
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,41 @@
|
|||||||
|
|
||||||
#include "l4steamtransport.h"
|
#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
|
#ifdef RP412_STEAM
|
||||||
|
|
||||||
#include "l4nettransport.h"
|
#include "l4nettransport.h"
|
||||||
@@ -266,6 +301,11 @@ namespace
|
|||||||
public NetTransport
|
public NetTransport
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
SteamNetTransport():
|
||||||
|
statsNextTick(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
Logical
|
Logical
|
||||||
Startup()
|
Startup()
|
||||||
{
|
{
|
||||||
@@ -279,6 +319,29 @@ namespace
|
|||||||
{
|
{
|
||||||
// mission teardown (single-binary race loop): drop the
|
// mission teardown (single-binary race loop): drop the
|
||||||
// wire but keep the Steam API up - the lobby lives on
|
// wire but keep the Steam API up - the lobby lives on
|
||||||
|
FlushSends();
|
||||||
|
sendQueues.ReleaseAll();
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412NETSTATS: one detailed status per connection on the
|
||||||
|
// way out - the route description (direct vs which relay)
|
||||||
|
// lives nowhere else.
|
||||||
|
//
|
||||||
|
if (NetTransport_StatsEnabled())
|
||||||
|
{
|
||||||
|
for (int d = 0; d < gConnectionCount; ++d)
|
||||||
|
{
|
||||||
|
char detail[2048];
|
||||||
|
if (SteamNetworkingSockets()->GetDetailedConnectionStatus(
|
||||||
|
gConnections[d].handle, detail, sizeof(detail)) >= 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "NetStats: steam conn "
|
||||||
|
<< (unsigned long) gConnections[d].handle
|
||||||
|
<< " detail:\n" << detail << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < gConnectionCount; ++i)
|
for (int i = 0; i < gConnectionCount; ++i)
|
||||||
{
|
{
|
||||||
SteamNetworkingSockets()->CloseConnection(
|
SteamNetworkingSockets()->CloseConnection(
|
||||||
@@ -350,18 +413,59 @@ namespace
|
|||||||
target.Clear();
|
target.Clear();
|
||||||
target.SetIPv4(remote_fake_ip, fake_port);
|
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
|
// Mirror the TCP retry-while-refused loop, bounded: the
|
||||||
DWORD deadline = GetTickCount() + 120 * 1000;
|
// 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;
|
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 (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
++attempt;
|
++attempt;
|
||||||
|
DWORD deadline = GetTickCount() + wait_seconds * 1000;
|
||||||
HSteamNetConnection handle =
|
HSteamNetConnection handle =
|
||||||
SteamNetworkingSockets()->ConnectByIPAddress(target, 0, NULL);
|
SteamNetworkingSockets()->ConnectByIPAddress(target, 0, NULL);
|
||||||
if (handle == k_HSteamNetConnection_Invalid)
|
if (handle == k_HSteamNetConnection_Invalid)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "SteamNetTransport: ConnectByIPAddress refused the call\n" << std::flush;
|
DEBUG_STREAM << "SteamNetTransport: ConnectByIPAddress refused the call\n" << std::flush;
|
||||||
|
NetTransport_SetWaitProgress(NULL);
|
||||||
return InvalidConnection;
|
return InvalidConnection;
|
||||||
}
|
}
|
||||||
AddConnection(handle, remote->sin_addr.S_un.S_addr, remote->sin_port);
|
AddConnection(handle, remote->sin_addr.S_un.S_addr, remote->sin_port);
|
||||||
@@ -390,7 +494,35 @@ namespace
|
|||||||
{
|
{
|
||||||
break;
|
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)
|
if (state == k_ESteamNetworkingConnectionState_Connected)
|
||||||
{
|
{
|
||||||
@@ -401,20 +533,41 @@ namespace
|
|||||||
}
|
}
|
||||||
DEBUG_STREAM << "SteamNetTransport: connect succeeded (attempt "
|
DEBUG_STREAM << "SteamNetTransport: connect succeeded (attempt "
|
||||||
<< attempt << ")\n" << std::flush;
|
<< attempt << ")\n" << std::flush;
|
||||||
|
NetTransport_SetWaitProgress(NULL);
|
||||||
return (Connection) handle;
|
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
|
DEBUG_STREAM << "SteamNetTransport: attempt " << attempt
|
||||||
<< " ended in state " << (int) state << "\n" << std::flush;
|
<< " ended in state " << (int) state << "\n" << std::flush;
|
||||||
SteamNetworkingSockets()->CloseConnection(handle, 0, "retry", false);
|
SteamNetworkingSockets()->CloseConnection(handle, 0, "retry", false);
|
||||||
RemoveConnection(handle);
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +717,20 @@ namespace
|
|||||||
listener->pendingCount = 0;
|
listener->pendingCount = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// Best-effort last drain so an orderly close does not strand
|
||||||
|
// queued bytes, then let the connection linger (true) so
|
||||||
|
// Steam delivers what it already accepted.
|
||||||
|
//
|
||||||
|
NetSendQueue *queue = sendQueues.Find(connection);
|
||||||
|
if (queue != NULL)
|
||||||
|
{
|
||||||
|
if (!queue->dead && queue->PendingBytes() > 0)
|
||||||
|
{
|
||||||
|
DrainQueue(queue);
|
||||||
|
}
|
||||||
|
sendQueues.Release(connection);
|
||||||
|
}
|
||||||
SteamNetworkingSockets()->CloseConnection(
|
SteamNetworkingSockets()->CloseConnection(
|
||||||
(HSteamNetConnection) connection, 0, "closed", true);
|
(HSteamNetConnection) connection, 0, "closed", true);
|
||||||
RemoveConnection((HSteamNetConnection) connection);
|
RemoveConnection((HSteamNetConnection) connection);
|
||||||
@@ -576,10 +743,163 @@ namespace
|
|||||||
int size
|
int size
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
NetSendQueue *queue = sendQueues.Find(connection);
|
||||||
|
|
||||||
|
if (queue != NULL && queue->dead)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NetTransport_SendQueueEnabled())
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Legacy direct send (RP412NETSENDQ=0) - but name the
|
||||||
|
// losses the queue would have prevented. Backpressure
|
||||||
|
// (LimitExceeded) used to vanish into this return code.
|
||||||
|
//
|
||||||
|
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
|
||||||
|
(HSteamNetConnection) connection, data, (uint32) size,
|
||||||
|
k_nSteamNetworkingSend_Reliable, NULL);
|
||||||
|
if (result != k_EResultOK)
|
||||||
|
{
|
||||||
|
static int said = 0;
|
||||||
|
if (said < 8)
|
||||||
|
{
|
||||||
|
++said;
|
||||||
|
DEBUG_STREAM << "SteamNetTransport::Send: legacy mode lost a "
|
||||||
|
<< size << " byte message (result " << (int) result
|
||||||
|
<< ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue == NULL)
|
||||||
|
{
|
||||||
|
queue = sendQueues.FindOrAdopt(connection);
|
||||||
|
if (queue == NULL)
|
||||||
|
{
|
||||||
|
// table full: behave exactly like the legacy send
|
||||||
|
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
|
||||||
|
(HSteamNetConnection) connection, data, (uint32) size,
|
||||||
|
k_nSteamNetworkingSend_Reliable, NULL);
|
||||||
|
return (result == k_EResultOK) ? size : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue->sentMessages++;
|
||||||
|
queue->sentBytes += size;
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIFO discipline: anything already queued goes first.
|
||||||
|
//
|
||||||
|
if (queue->PendingBytes() > 0)
|
||||||
|
{
|
||||||
|
if (!sendQueues.Append(queue, data, size))
|
||||||
|
{
|
||||||
|
MarkSendDead(connection, queue);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
DrainQueue(queue);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
|
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
|
||||||
(HSteamNetConnection) connection, data, (uint32) size,
|
(HSteamNetConnection) connection, data, (uint32) size,
|
||||||
k_nSteamNetworkingSend_Reliable, NULL);
|
k_nSteamNetworkingSend_Reliable, NULL);
|
||||||
return (result == k_EResultOK) ? size : -1;
|
if (result == k_EResultOK)
|
||||||
|
{
|
||||||
|
queue->wireWrites++;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
if (result == k_EResultLimitExceeded)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Steam's send buffer is full (a peer stalled in its
|
||||||
|
// mission load, most likely). Keep the message; the
|
||||||
|
// flush points retry it in order.
|
||||||
|
//
|
||||||
|
queue->wouldBlocks++;
|
||||||
|
if (!sendQueues.Append(queue, data, size))
|
||||||
|
{
|
||||||
|
MarkSendDead(connection, queue);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "SteamNetTransport::Send: result " << (int) result
|
||||||
|
<< " - declaring the connection dead\n" << std::flush;
|
||||||
|
MarkSendDead(connection, queue);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
FlushSends()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < NetSendQueueTable::MaxQueues; ++i)
|
||||||
|
{
|
||||||
|
NetSendQueue *queue = &sendQueues.queues[i];
|
||||||
|
|
||||||
|
if (queue->connection != InvalidConnection &&
|
||||||
|
!queue->dead &&
|
||||||
|
queue->PendingBytes() > 0)
|
||||||
|
{
|
||||||
|
DrainQueue(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412NETSTATS: the 5 s rollup, plus the first read this
|
||||||
|
// codebase has ever taken of Steam's own connection health -
|
||||||
|
// ping, quality, and how much is sitting unsent or unacked
|
||||||
|
// (the backpressure the old send path silently discarded).
|
||||||
|
//
|
||||||
|
if (NetTransport_StatsEnabled())
|
||||||
|
{
|
||||||
|
DWORD now = GetTickCount();
|
||||||
|
if ((LONG) (now - statsNextTick) >= 0)
|
||||||
|
{
|
||||||
|
statsNextTick = now + 5000;
|
||||||
|
|
||||||
|
for (int j = 0; j < NetSendQueueTable::MaxQueues; ++j)
|
||||||
|
{
|
||||||
|
NetSendQueue *queue = &sendQueues.queues[j];
|
||||||
|
if (queue->connection == InvalidConnection ||
|
||||||
|
queue->sentMessages == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "NetStats: steam conn "
|
||||||
|
<< (unsigned long) queue->connection << ": "
|
||||||
|
<< queue->sentMessages << " msgs "
|
||||||
|
<< queue->sentBytes << " B, "
|
||||||
|
<< queue->wireWrites << " writes ("
|
||||||
|
<< queue->wouldBlocks << " limit-exceeded), queued high "
|
||||||
|
<< queue->queuedHighWater << " B, pending "
|
||||||
|
<< queue->PendingBytes()
|
||||||
|
<< (queue->dead ? " B, DEAD" : " B")
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int k = 0; k < gConnectionCount; ++k)
|
||||||
|
{
|
||||||
|
SteamNetConnectionRealTimeStatus_t status;
|
||||||
|
if (SteamNetworkingSockets()->GetConnectionRealTimeStatus(
|
||||||
|
gConnections[k].handle, &status, 0, NULL) == k_EResultOK)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "NetStats: steam conn "
|
||||||
|
<< (unsigned long) gConnections[k].handle
|
||||||
|
<< ": ping " << status.m_nPing
|
||||||
|
<< " ms, quality " << status.m_flConnectionQualityLocal
|
||||||
|
<< "/" << status.m_flConnectionQualityRemote
|
||||||
|
<< ", pending reliable " << status.m_cbPendingReliable
|
||||||
|
<< " B, unacked " << status.m_cbSentUnackedReliable
|
||||||
|
<< " B, queue " << (long) status.m_usecQueueTime
|
||||||
|
<< " us\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
@@ -669,6 +989,83 @@ namespace
|
|||||||
address->sin_port = record->remoteEnginePort;
|
address->sin_port = record->remoteEnginePort;
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
//
|
||||||
|
// Wire messages are capped so the receiver's leftover staging
|
||||||
|
// buffer can always hold the tail of one: the pump reads with at
|
||||||
|
// least MAX_RECEIVE_DATA_SIZE (1600) of room, so a 2048-byte
|
||||||
|
// message leaves at most 448 bytes over, against 4096 of staging.
|
||||||
|
//
|
||||||
|
enum { maxWireMessageBytes = 2048 };
|
||||||
|
|
||||||
|
//
|
||||||
|
// Send-side death has to surface where the engine looks for it:
|
||||||
|
// Receive() reports ConnectionRecord::dead as a disconnect once
|
||||||
|
// the leftovers drain. Mark both sides so drains stop too.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
MarkSendDead(
|
||||||
|
Connection connection,
|
||||||
|
NetSendQueue *queue)
|
||||||
|
{
|
||||||
|
if (queue != NULL)
|
||||||
|
{
|
||||||
|
queue->dead = True;
|
||||||
|
}
|
||||||
|
ConnectionRecord *record = FindConnection((HSteamNetConnection) connection);
|
||||||
|
if (record != NULL)
|
||||||
|
{
|
||||||
|
record->dead = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Push pending bytes at the connection in wire-message chunks.
|
||||||
|
// A MUNGA message split across two wire messages is fine - the
|
||||||
|
// receiver treats the message sequence as a byte stream and
|
||||||
|
// re-frames from the length prefixes.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DrainQueue(NetSendQueue *queue)
|
||||||
|
{
|
||||||
|
while (queue->PendingBytes() > 0 && !queue->dead)
|
||||||
|
{
|
||||||
|
int chunk = queue->PendingBytes();
|
||||||
|
if (chunk > maxWireMessageBytes)
|
||||||
|
{
|
||||||
|
chunk = maxWireMessageBytes;
|
||||||
|
}
|
||||||
|
EResult result = SteamNetworkingSockets()->SendMessageToConnection(
|
||||||
|
(HSteamNetConnection) queue->connection,
|
||||||
|
&queue->buffer[queue->head], (uint32) chunk,
|
||||||
|
k_nSteamNetworkingSend_Reliable, NULL);
|
||||||
|
if (result == k_EResultOK)
|
||||||
|
{
|
||||||
|
queue->wireWrites++;
|
||||||
|
queue->head += chunk;
|
||||||
|
if (queue->PendingBytes() == 0)
|
||||||
|
{
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (result == k_EResultLimitExceeded)
|
||||||
|
{
|
||||||
|
queue->wouldBlocks++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "SteamNetTransport::DrainQueue: result " << (int) result
|
||||||
|
<< " with " << queue->PendingBytes()
|
||||||
|
<< " bytes pending - declaring the connection dead\n" << std::flush;
|
||||||
|
MarkSendDead(queue->connection, queue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NetSendQueueTable sendQueues;
|
||||||
|
DWORD statsNextTick;
|
||||||
};
|
};
|
||||||
|
|
||||||
SteamNetTransport gSteamTransport;
|
SteamNetTransport gSteamTransport;
|
||||||
@@ -686,6 +1083,16 @@ Logical
|
|||||||
return True;
|
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())
|
if (!SteamAPI_Init())
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "SteamNetTransport: SteamAPI_Init failed "
|
DEBUG_STREAM << "SteamNetTransport: SteamAPI_Init failed "
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
//########################################################################
|
//########################################################################
|
||||||
// SteamNetTransport - the retail wire (l4steamtransport.cpp). Built
|
// SteamNetTransport - the retail wire (l4steamtransport.cpp). Built
|
||||||
// only under RP412_STEAM (Steamworks SDK vendored at
|
// only under RP412_STEAM (Steamworks SDK vendored at
|
||||||
// extern\steamworks_sdk_164; steam_api.dll ships beside the exe).
|
// extern\steamworks_sdk_165; steam_api.dll ships beside the exe, but is
|
||||||
|
// delay-loaded and optional - see the note further down).
|
||||||
//
|
//
|
||||||
// Method mapping onto ISteamNetworkingSockets:
|
// Method mapping onto ISteamNetworkingSockets:
|
||||||
//
|
//
|
||||||
@@ -55,6 +56,25 @@
|
|||||||
// egg is distributed.
|
// 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
|
#ifdef RP412_STEAM
|
||||||
|
|
||||||
// Bring Steam up and make this the process transport. False (with the
|
// Bring Steam up and make this the process transport. False (with the
|
||||||
|
|||||||
+42
-1
@@ -14,6 +14,7 @@
|
|||||||
SystemClock SystemClock::timer;
|
SystemClock SystemClock::timer;
|
||||||
long SystemClock::ticksPerSecond;
|
long SystemClock::ticksPerSecond;
|
||||||
__int64 SystemClock::perfCounterFreq;
|
__int64 SystemClock::perfCounterFreq;
|
||||||
|
__int64 SystemClock::perfCounterOrigin;
|
||||||
|
|
||||||
//RB 1/20/07
|
//RB 1/20/07
|
||||||
//volatile long fast_time = 0L;
|
//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()
|
long SystemClock::GetRTC()
|
||||||
{
|
{
|
||||||
LARGE_INTEGER count;
|
LARGE_INTEGER count;
|
||||||
QueryPerformanceCounter(&count);
|
QueryPerformanceCounter(&count);
|
||||||
return (long)((count.QuadPart * (__int64)1000) / SystemClock::perfCounterFreq);
|
return (long)(
|
||||||
|
((count.QuadPart - SystemClock::perfCounterOrigin) * (__int64)1000)
|
||||||
|
/ SystemClock::perfCounterFreq
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double SystemClock::GetHiRes()
|
double SystemClock::GetHiRes()
|
||||||
@@ -97,6 +129,15 @@ SystemClock::SystemClock()
|
|||||||
//SystemClock::ticksPerSecond = freq.QuadPart;
|
//SystemClock::ticksPerSecond = freq.QuadPart;
|
||||||
SystemClock::perfCounterFreq = freq.QuadPart;
|
SystemClock::perfCounterFreq = freq.QuadPart;
|
||||||
SystemClock::ticksPerSecond = 1000L;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
+1060
-111
File diff suppressed because it is too large
Load Diff
@@ -293,6 +293,17 @@ private:
|
|||||||
|
|
||||||
int mDisplayToUpdate;
|
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
|
// Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and
|
||||||
// the rotated map render as their own desktop windows; the packed
|
// the rotated map render as their own desktop windows; the packed
|
||||||
@@ -308,6 +319,26 @@ private:
|
|||||||
SplitViewCount
|
SplitViewCount
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Glass size of one secondary display, in whatever units the caller
|
||||||
|
// asked for (canvas units while clamping, device pixels on return).
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
struct GlassSize
|
||||||
|
{
|
||||||
|
int w, h;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected:
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// The six displays sized for a given canvas scale, honouring the
|
||||||
|
// player's per-display scaling and clamped to keep the pod
|
||||||
|
// arrangement legal. Every layout goes through here, first and
|
||||||
|
// resized alike, so the two can never disagree.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
static void
|
||||||
|
CockpitGlassSizes(int scale, GlassSize sizes[SplitViewCount]);
|
||||||
|
|
||||||
void
|
void
|
||||||
FillSplitMFD(
|
FillSplitMFD(
|
||||||
SplitViewID view,
|
SplitViewID view,
|
||||||
@@ -315,12 +346,42 @@ private:
|
|||||||
int shift
|
int shift
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public:
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Fit the cockpit into a client area: one uniform scale keeps the
|
||||||
|
// 1920x1080 canvas at its own aspect (so an ultrawide letterboxes
|
||||||
|
// rather than stretching), centred in whatever space it is given.
|
||||||
|
// Re-run whenever the cockpit window is resized.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
LayoutCockpit(int client_w, int client_h);
|
||||||
|
|
||||||
|
static SVGA16 *
|
||||||
|
GetCockpit()
|
||||||
|
{ return activeCockpit; }
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Take the six secondary displays off screen and leave the whole
|
||||||
|
// canvas to the viewscreen. The Winners Circle wants the podium
|
||||||
|
// uncluttered, and the panes are only bezels over the 3D - hiding
|
||||||
|
// them uncovers what is already being drawn behind.
|
||||||
|
//
|
||||||
|
// There is no matching show: the mission is over by the time this
|
||||||
|
// is called, and the next race builds a fresh cockpit.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
HideSecondaryDisplays();
|
||||||
|
|
||||||
|
protected:
|
||||||
Logical
|
Logical
|
||||||
splitViews;
|
splitViews;
|
||||||
MFDSplitView
|
MFDSplitView
|
||||||
*splitView[SplitViewCount];
|
*splitView[SplitViewCount];
|
||||||
HWND
|
HWND
|
||||||
cockpitViewscreen; // child pane the 3D scene presents into
|
cockpitViewscreen; // child pane the 3D scene presents into
|
||||||
|
|
||||||
|
static SVGA16
|
||||||
|
*activeCockpit; // the instance owning the cockpit window
|
||||||
};
|
};
|
||||||
|
|
||||||
//########################################################################
|
//########################################################################
|
||||||
|
|||||||
+782
-56
@@ -17,6 +17,10 @@
|
|||||||
#include "..\munga\nttmgr.h"
|
#include "..\munga\nttmgr.h"
|
||||||
#include "..\munga\app.h"
|
#include "..\munga\app.h"
|
||||||
#include "l4particles.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"
|
#include "DXUtils.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@@ -27,10 +31,71 @@ using namespace std;
|
|||||||
|
|
||||||
LPDIRECT3D9 gD3D = NULL;
|
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
|
// Single-window cockpit: viewscreen child window the scene presents into
|
||||||
// (NULL = present to the device window as always).
|
// (NULL = present to the device window as always).
|
||||||
HWND gMainPresentWindow = NULL;
|
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
|
//STUBBED: DPL RB 1/14/07
|
||||||
// when this is resolved it can be removed
|
// when this is resolved it can be removed
|
||||||
#include "..\DPLSTUB.h"
|
#include "..\DPLSTUB.h"
|
||||||
@@ -1686,16 +1751,33 @@ DPLRenderer::DPLRenderer(
|
|||||||
mPresentParams.hDeviceWindow = hWnd;
|
mPresentParams.hDeviceWindow = hWnd;
|
||||||
mPresentParams.Flags = 0;
|
mPresentParams.Flags = 0;
|
||||||
mPresentParams.FullScreen_RefreshRateInHz = (fullscreen)?60:D3DPRESENT_RATE_DEFAULT;
|
mPresentParams.FullScreen_RefreshRateInHz = (fullscreen)?60:D3DPRESENT_RATE_DEFAULT;
|
||||||
mPresentParams.PresentationInterval = D3DPRESENT_RATE_DEFAULT;
|
mPresentParams.PresentationInterval = RPPresentationInterval();
|
||||||
mPresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
|
mPresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
|
||||||
mPresentParams.EnableAutoDepthStencil = TRUE;
|
mPresentParams.EnableAutoDepthStencil = TRUE;
|
||||||
mPresentParams.AutoDepthStencilFormat = D3DFMT_D24X8;
|
mPresentParams.AutoDepthStencilFormat = D3DFMT_D24X8;
|
||||||
mPresentParams.Windowed = !fullscreen;
|
mPresentParams.Windowed = !fullscreen;
|
||||||
if (fullscreen)
|
//
|
||||||
{
|
// The render size is asked for, not suggested - windowed as well as
|
||||||
mPresentParams.BackBufferWidth = screenWidth;
|
// full-screen. Left at zero, D3D sizes the back buffer to the device
|
||||||
mPresentParams.BackBufferHeight = screenHeight;
|
// 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;
|
HRESULT hr;
|
||||||
|
|
||||||
@@ -1725,6 +1807,15 @@ DPLRenderer::DPLRenderer(
|
|||||||
//}
|
//}
|
||||||
//DEBUG_STREAM<<"**************************"<<std::endl<<"**************************"<<std::endl<<std::flush;
|
//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)
|
if (mPrimaryIndex == NULL)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM<<"Unable to locate a suitable primary device index."<<std::endl<<std::flush;
|
DEBUG_STREAM<<"Unable to locate a suitable primary device index."<<std::endl<<std::flush;
|
||||||
@@ -1732,16 +1823,76 @@ DPLRenderer::DPLRenderer(
|
|||||||
return;
|
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))
|
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));
|
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);
|
mDevice->Clear(0, NULL, D3DCLEAR_TARGET, 0xFF000000, 0.0f, 0);
|
||||||
@@ -1797,6 +1948,19 @@ DPLRenderer::DPLRenderer(
|
|||||||
backgroundGreen = 0.0f;
|
backgroundGreen = 0.0f;
|
||||||
backgroundBlue = 0.0f;
|
backgroundBlue = 0.0f;
|
||||||
viewAngle = 30.0f;
|
viewAngle = 30.0f;
|
||||||
|
// the Winners Circle camera is off until the podium asks for it. No
|
||||||
|
// restore path is needed: a fresh DPLRenderer is built per mission, so
|
||||||
|
// the next race re-reads viewangle from RPDPL.INI.
|
||||||
|
mPresentationCamera = False;
|
||||||
|
mPresentationFog = False;
|
||||||
|
mPresentationAspect = 0.0f;
|
||||||
|
mPresentationFadeTime = 0.0f;
|
||||||
|
mPresentationFogRed = 0.0f;
|
||||||
|
mPresentationFogGreen = 0.0f;
|
||||||
|
mPresentationFogBlue = 0.0f;
|
||||||
|
mPresentationFogNear = 0.0f;
|
||||||
|
mPresentationFogFar = 0.0f;
|
||||||
|
D3DXMatrixIdentity(&mPresentationView);
|
||||||
dplMainView = NULL;
|
dplMainView = NULL;
|
||||||
dplDeathZone = NULL;
|
dplDeathZone = NULL;
|
||||||
dplMainZone = NULL;
|
dplMainZone = NULL;
|
||||||
@@ -2343,10 +2507,53 @@ void
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case winnersCircleFogStyle:
|
case winnersCircleFogStyle:
|
||||||
|
//
|
||||||
|
// The presentation shot, restored from the DPL body below. The
|
||||||
|
// stand sits far off the track in open ground, so the track's own
|
||||||
|
// fog leaves it in the dark - this is the lighter blue-violet the
|
||||||
|
// original used to lift it, with the fog pushed back to 100/1050.
|
||||||
//
|
//
|
||||||
// HACK!! This really shouldn't reset the clip planes, but since
|
// HACK!! This really shouldn't reset the clip planes, but since
|
||||||
// it only happens at the end of the review, it should be safe for now.
|
// it only happens at the end of the review, it should be safe for now.
|
||||||
//
|
//
|
||||||
|
fogRed = 0.32f;
|
||||||
|
fogGreen = 0.30f;
|
||||||
|
fogBlue = 0.65f;
|
||||||
|
fogNear = 100.0f;
|
||||||
|
fogFar = 1050.0f;
|
||||||
|
|
||||||
|
// the per-frame FOGSTART/FOGEND come from these
|
||||||
|
currentFogNear = fogNear;
|
||||||
|
currentFogFar = fogFar;
|
||||||
|
|
||||||
|
clipNear = 0.25f;
|
||||||
|
clipFar = 1100.0f;
|
||||||
|
|
||||||
|
// tells the end-of-mission fade to stand down
|
||||||
|
mPresentationFog = True;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Crop to the shape the stand was composed for. 4:3 unless
|
||||||
|
// RP412PODIUMASPECT says otherwise; 0 turns it off and lets
|
||||||
|
// the podium run full width.
|
||||||
|
//
|
||||||
|
mPresentationAspect = 4.0f / 3.0f;
|
||||||
|
{
|
||||||
|
const char *aspect = getenv("RP412PODIUMASPECT");
|
||||||
|
if (aspect != NULL)
|
||||||
|
{
|
||||||
|
mPresentationAspect = (float) atof(aspect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unconditional: fogUpdating is off while a vehicle drives its own
|
||||||
|
// headlight fog, and the podium overrides all of that
|
||||||
|
if (mDevice != NULL)
|
||||||
|
{
|
||||||
|
mDevice->SetRenderState(D3DRS_FOGCOLOR,
|
||||||
|
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
|
||||||
|
(int)(255 * fogBlue)));
|
||||||
|
}
|
||||||
// dpl_SetViewClipPlanes ( dplMainView, 0.25f, 1100.0f );
|
// dpl_SetViewClipPlanes ( dplMainView, 0.25f, 1100.0f );
|
||||||
// dpl_SetViewFog(
|
// dpl_SetViewFog(
|
||||||
// dplMainView,
|
// dplMainView,
|
||||||
@@ -3499,6 +3706,16 @@ DPLRenderer::~DPLRenderer()
|
|||||||
// the next race of the single-binary loop - drop them with the device
|
// the next race of the single-binary loop - drop them with the device
|
||||||
d3d_OBJECT::FlushTextureCache();
|
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(mDevice);
|
||||||
SAFE_RELEASE(gD3D);
|
SAFE_RELEASE(gD3D);
|
||||||
//STUBBED: DPL RB 1/14/07
|
//STUBBED: DPL RB 1/14/07
|
||||||
@@ -5608,6 +5825,24 @@ void
|
|||||||
intersect_mask = INTERSECT_ALL;
|
intersect_mask = INTERSECT_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412RENDERDIAG=1: name what actually gets built, so a missing
|
||||||
|
// Winners Circle can be told from one that is simply out of shot.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static const char *diag = getenv("RP412RENDERDIAG");
|
||||||
|
if (diag != NULL && atoi(diag) != 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "RenderDiag: building class"
|
||||||
|
<< entity->GetClassID() << " res "
|
||||||
|
<< entity->GetResourceID() << " at "
|
||||||
|
<< entity->localOrigin.linearPosition.x << ","
|
||||||
|
<< entity->localOrigin.linearPosition.y << ","
|
||||||
|
<< entity->localOrigin.linearPosition.z
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logical first_object = True;
|
Logical first_object = True;
|
||||||
|
|
||||||
video_iterator.First();
|
video_iterator.First();
|
||||||
@@ -5909,11 +6144,84 @@ void
|
|||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Execute Method, performs the rendering of one frame
|
// 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)
|
void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::InterestingEntityIterator* all_iterator)
|
||||||
{
|
{
|
||||||
Component *component;
|
Component *component;
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
|
RPSweepCockpitLamps(); // keep the lit buttons tracking the sim (see above)
|
||||||
|
|
||||||
// timing variables
|
// timing variables
|
||||||
__int64 ticks = HiResNowTicks();
|
__int64 ticks = HiResNowTicks();
|
||||||
#ifdef LOGFRAMERATE
|
#ifdef LOGFRAMERATE
|
||||||
@@ -5997,18 +6305,137 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
if (mCamShipHUD)
|
if (mCamShipHUD)
|
||||||
mCamShipHUD->Execute();
|
mCamShipHUD->Execute();
|
||||||
|
|
||||||
|
//
|
||||||
|
// The eye renderable has just written D3DTS_VIEW from the viewpoint
|
||||||
|
// entity. For the Winners Circle the shot comes from off the stand
|
||||||
|
// instead, so stomp it here - before the read-back below that feeds
|
||||||
|
// every draw call.
|
||||||
|
//
|
||||||
|
if (mPresentationCamera)
|
||||||
|
{
|
||||||
|
mDevice->SetTransform(D3DTS_VIEW, &mPresentationView);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412RENDERDIAG=1: report what the frame is actually made of once the
|
||||||
|
// mission is ending, which is the only way to tell an empty scene from a
|
||||||
|
// misaimed camera.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static const char *diag = getenv("RP412RENDERDIAG");
|
||||||
|
static int reported = 0;
|
||||||
|
if (diag != NULL && atoi(diag) != 0 &&
|
||||||
|
currentAppState == Application::EndingMission && reported < 4)
|
||||||
|
{
|
||||||
|
++reported;
|
||||||
|
int dynamic_count = 0;
|
||||||
|
SChainIteratorOf<HierarchicalDrawComponent*> diag_iter(&mRenderables);
|
||||||
|
while (diag_iter.ReadAndNext() != NULL)
|
||||||
|
{
|
||||||
|
++dynamic_count;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "RenderDiag: ending frame " << reported
|
||||||
|
<< " dead=" << (l4_application->IsDead() ? 1 : 0)
|
||||||
|
<< " statics=" << (int) mConsolidatedStaticObjects.size()
|
||||||
|
<< " renderables=" << dynamic_count
|
||||||
|
<< " camera=" << (mPresentationCamera ? 1 : 0)
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gNumBatches = 0;
|
gNumBatches = 0;
|
||||||
static Time lastFrameTime = mTargetRenderTime;
|
static Time lastFrameTime = mTargetRenderTime;
|
||||||
Scalar dT = mTargetRenderTime - lastFrameTime;
|
Scalar dT = mTargetRenderTime - lastFrameTime;
|
||||||
lastFrameTime = mTargetRenderTime;
|
lastFrameTime = mTargetRenderTime;
|
||||||
currentFrameTime = Now();
|
currentFrameTime = Now();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Fade the presentation up out of black, if one is running. Same lever
|
||||||
|
// the end-of-mission fade pulls: scale the fog colour and both fog
|
||||||
|
// distances, here from nothing up to what the podium asked for.
|
||||||
|
//
|
||||||
|
if (mPresentationFadeTime > 0.0f)
|
||||||
|
{
|
||||||
|
Scalar remaining = mPresentationFadeEnd - Now();
|
||||||
|
Scalar fade = 1.0f - (remaining / mPresentationFadeTime);
|
||||||
|
if (fade >= 1.0f)
|
||||||
|
{
|
||||||
|
fade = 1.0f;
|
||||||
|
mPresentationFadeTime = 0.0f; // done
|
||||||
|
}
|
||||||
|
if (fade < 0.0f)
|
||||||
|
{
|
||||||
|
fade = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
fogRed = mPresentationFogRed * fade;
|
||||||
|
fogGreen = mPresentationFogGreen * fade;
|
||||||
|
fogBlue = mPresentationFogBlue * fade;
|
||||||
|
currentFogNear = mPresentationFogNear * fade;
|
||||||
|
currentFogFar = mPresentationFogFar * fade;
|
||||||
|
|
||||||
|
mDevice->SetRenderState(D3DRS_FOGCOLOR,
|
||||||
|
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
|
||||||
|
(int)(255 * fogBlue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Pillarbox: black the whole target first, then narrow the viewport so
|
||||||
|
// everything after this - clear, scene, reticle - lands inside the crop
|
||||||
|
// and the surround stays black. Restored after Present.
|
||||||
|
//
|
||||||
|
if (mPresentationAspect > 0.0f)
|
||||||
|
{
|
||||||
|
D3DVIEWPORT9 full;
|
||||||
|
full.X = 0;
|
||||||
|
full.Y = 0;
|
||||||
|
full.Width = mPresentParams.BackBufferWidth;
|
||||||
|
full.Height = mPresentParams.BackBufferHeight;
|
||||||
|
full.MinZ = 0.0f;
|
||||||
|
full.MaxZ = 1.0f;
|
||||||
|
mDevice->SetViewport(&full);
|
||||||
|
mDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
|
||||||
|
|
||||||
|
DWORD cropped = (DWORD)
|
||||||
|
(mPresentParams.BackBufferHeight * mPresentationAspect);
|
||||||
|
if (cropped > mPresentParams.BackBufferWidth)
|
||||||
|
{
|
||||||
|
cropped = mPresentParams.BackBufferWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DVIEWPORT9 crop = full;
|
||||||
|
crop.Width = cropped;
|
||||||
|
crop.X = (mPresentParams.BackBufferWidth - cropped) / 2;
|
||||||
|
mDevice->SetViewport(&crop);
|
||||||
|
}
|
||||||
|
|
||||||
DWORD currentFog;
|
DWORD currentFog;
|
||||||
mDevice->GetRenderState(D3DRS_FOGCOLOR, ¤tFog);
|
mDevice->GetRenderState(D3DRS_FOGCOLOR, ¤tFog);
|
||||||
hr = mDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, currentFog, 1.0f, 0);
|
//
|
||||||
|
// 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();
|
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);
|
mDevice->SetFVF(L4VERTEX_FVF);
|
||||||
|
|
||||||
D3DXMATRIX viewTransform;
|
D3DXMATRIX viewTransform;
|
||||||
@@ -6085,19 +6512,29 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
|
|
||||||
mDevice->SetTransform(D3DTS_PROJECTION, &mProjectionMatrix);
|
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;
|
if (!l4_application->IsDead())
|
||||||
|
|
||||||
for (iter = this->mConsolidatedStaticObjects.begin(); iter != this->mConsolidatedStaticObjects.end(); ++iter)
|
|
||||||
{
|
{
|
||||||
(*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))
|
for (iter = this->mConsolidatedStaticObjects.begin(); iter != this->mConsolidatedStaticObjects.end(); ++iter)
|
||||||
obj->Draw(PASS_SKY, &viewTransform, mTargetRenderTime);
|
{
|
||||||
|
(*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
|
//Reactivate fog
|
||||||
mDevice->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(¤tFogNear)));
|
mDevice->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(¤tFogNear)));
|
||||||
@@ -6142,6 +6579,12 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
// Wrap it up by doing the 2D pass
|
// Wrap it up by doing the 2D pass
|
||||||
//
|
//
|
||||||
mDevice->SetFVF(L4VERTEX_2D_FVF);
|
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->SetRenderState(D3DRS_ZWRITEENABLE, true);
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
||||||
@@ -6152,15 +6595,112 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
|
mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
|
||||||
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
||||||
|
|
||||||
if (mReticle && !l4_application->IsDead())
|
//
|
||||||
|
// No gunsight on the podium - the race is over and nothing is being
|
||||||
|
// aimed at. It is drawn in the 2D pass, so it survives everything else
|
||||||
|
// the presentation turns off.
|
||||||
|
//
|
||||||
|
if (mReticle && !l4_application->IsDead() && !InPresentation())
|
||||||
mReticle->Render(0, &viewTransform);
|
mReticle->Render(0, &viewTransform);
|
||||||
|
|
||||||
if (mCamShipHUD)
|
if (mCamShipHUD && !InPresentation())
|
||||||
mCamShipHUD->Render(0, &viewTransform);
|
mCamShipHUD->Render(0, &viewTransform);
|
||||||
|
|
||||||
hr = mDevice->EndScene();
|
hr = mDevice->EndScene();
|
||||||
|
|
||||||
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
|
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
D3DVIEWPORT9 full;
|
||||||
|
full.X = 0;
|
||||||
|
full.Y = 0;
|
||||||
|
full.Width = mPresentParams.BackBufferWidth;
|
||||||
|
full.Height = mPresentParams.BackBufferHeight;
|
||||||
|
full.MinZ = 0.0f;
|
||||||
|
full.MaxZ = 1.0f;
|
||||||
|
mDevice->SetViewport(&full);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412RENDERDIAG=1: did the ending frame actually reach the window, and
|
||||||
|
// what colour did it clear to? A black clear with a blue-violet fog set
|
||||||
|
// means something is overwriting the fog behind us.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static const char *diag = getenv("RP412RENDERDIAG");
|
||||||
|
static int presented = 0;
|
||||||
|
if (diag != NULL && atoi(diag) != 0 &&
|
||||||
|
application->GetApplicationState() == Application::EndingMission &&
|
||||||
|
presented < 4)
|
||||||
|
{
|
||||||
|
++presented;
|
||||||
|
DWORD fog_now = 0;
|
||||||
|
mDevice->GetRenderState(D3DRS_FOGCOLOR, &fog_now);
|
||||||
|
HWND present_window = (HWND) gMainPresentWindow;
|
||||||
|
RECT present_rect;
|
||||||
|
present_rect.left = present_rect.top = 0;
|
||||||
|
present_rect.right = present_rect.bottom = 0;
|
||||||
|
if (present_window != NULL)
|
||||||
|
{
|
||||||
|
GetClientRect(present_window, &present_rect);
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "RenderDiag: present " << presented
|
||||||
|
<< " hr=0x" << std::hex << (unsigned int) hr << std::dec
|
||||||
|
<< " fogcolor=0x" << std::hex << (unsigned int) fog_now << std::dec
|
||||||
|
<< " window=" << (void*) present_window
|
||||||
|
<< " visible=" << (present_window != NULL &&
|
||||||
|
IsWindowVisible(present_window) ? 1 : 0)
|
||||||
|
<< " rect=" << present_rect.right << "x" << present_rect.bottom
|
||||||
|
<< " backbuf=" << mPresentParams.BackBufferWidth << "x"
|
||||||
|
<< mPresentParams.BackBufferHeight
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hr == D3DERR_DEVICELOST)
|
if (hr == D3DERR_DEVICELOST)
|
||||||
{
|
{
|
||||||
int bbCount = mPresentParams.BackBufferCount;
|
int bbCount = mPresentParams.BackBufferCount;
|
||||||
@@ -6328,19 +6868,17 @@ void
|
|||||||
void
|
void
|
||||||
DPLRenderer::DPLToggleWireframe()
|
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 " << (gWireframe ? "ON" : "OFF")
|
||||||
//{
|
<< std::endl << std::flush;
|
||||||
// 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 );
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -6736,27 +7274,167 @@ void
|
|||||||
}
|
}
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
// Un-stubbed for the Winners Circle, which asks for 45 degrees to get all
|
||||||
|
// eight racers in frame. The DPL body below it built a dpl_View; the D3D9
|
||||||
|
// path builds the same two matrices DPLReadINIPage does and pushes them,
|
||||||
|
// because mProjectionMatrix only reaches the device on the LaunchingMission
|
||||||
|
// edge and on the sky-pass restore - rebuilding alone would not show until
|
||||||
|
// the next frame.
|
||||||
|
//
|
||||||
|
// Pass degrees, as the INI does. RPDPL.INI ships viewangle=40, which is what
|
||||||
|
// to hand back afterwards - not the 30 in the constructor, which the INI
|
||||||
|
// overwrites at load.
|
||||||
|
//
|
||||||
void DPLRenderer::SetViewAngle(Degree new_angle)
|
void DPLRenderer::SetViewAngle(Degree new_angle)
|
||||||
{
|
{
|
||||||
//STUBBED: DPL RB 1/14/07
|
Check(this);
|
||||||
//Check(this);
|
|
||||||
////
|
viewAngle = (float) new_angle.angle;
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
//// Convert From Degree To Radian
|
Radian view_angle;
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
view_angle = new_angle;
|
||||||
////
|
// tan(half angle): the culling helper GetViewRatio() reads this, and
|
||||||
//Radian view_angle;
|
// nothing has written it since the DPL body was stubbed out
|
||||||
//view_angle = new_angle;
|
viewRatio = (float) tan(view_angle / 2.0f);
|
||||||
//viewAngle = view_angle;
|
aspectRatio = (float) y_size / (float) x_size;
|
||||||
//viewRatio = tan(viewAngle/2.0f);
|
|
||||||
////
|
// pillarboxed shots render into a narrower viewport, so the projection
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// has to use that shape or the scene comes out squashed rather than
|
||||||
//// Calc Aspect Ratio and Set View Projection
|
// cropped
|
||||||
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
float projection_aspect = (mPresentationAspect > 0.0f)
|
||||||
////
|
? mPresentationAspect
|
||||||
//aspectRatio = (float) y_size / (float) x_size;
|
: ((float) x_size / (float) y_size);
|
||||||
//dpl_SetViewProjection ( dplMainView, -1.0f, -aspectRatio, 1.0f, aspectRatio, 1.0f/viewRatio);
|
|
||||||
//dpl_FlushView(dplMainView);
|
D3DXMatrixIdentity(&mProjectionMatrix);
|
||||||
|
D3DXMatrixPerspectiveFovLH(
|
||||||
|
&mProjectionMatrix,
|
||||||
|
viewAngle * (PI / 180.0f),
|
||||||
|
projection_aspect,
|
||||||
|
clipNear,
|
||||||
|
clipFar);
|
||||||
|
mProjectionMatrix(0, 0) *= -1; // handedness flip - the view is RH
|
||||||
|
|
||||||
|
mDecalProjectionMatrix = mProjectionMatrix;
|
||||||
|
mDecalProjectionMatrix._33 -= mDecalEpsilon;
|
||||||
|
|
||||||
|
if (mDevice != NULL)
|
||||||
|
{
|
||||||
|
mDevice->SetTransform(D3DTS_PROJECTION, &mProjectionMatrix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// The presentation camera. DPLEyeRenderable writes D3DTS_VIEW from the
|
||||||
|
// viewpoint entity every frame; ExecuteImplementation reads it straight back
|
||||||
|
// out of the device and hands it to every draw call. Overriding it between
|
||||||
|
// those two points is enough to move the shot without touching the eye
|
||||||
|
// renderable or building a CameraShip.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DPLRenderer::SetPresentationCamera(const Point3D &eye, const Point3D &look_at)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
D3DXVECTOR3 from((float) eye.x, (float) eye.y, (float) eye.z);
|
||||||
|
D3DXVECTOR3 at((float) look_at.x, (float) look_at.y, (float) look_at.z);
|
||||||
|
|
||||||
|
//
|
||||||
|
// LookAt*LH*, to match the LH projection. The two differ by exactly the
|
||||||
|
// sign of the view direction, and RH here points the camera the opposite
|
||||||
|
// way: ask to look down at the stand and you get sky behind you.
|
||||||
|
//
|
||||||
|
// The engine's own eye renderable does use RH, and is right to - its
|
||||||
|
// forward and up come out of the entity matrix already in that
|
||||||
|
// convention. A camera aimed with plain world coordinates does not, so
|
||||||
|
// it wants the handedness the projection was built with.
|
||||||
|
//
|
||||||
|
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
|
||||||
|
|
||||||
|
D3DXMatrixLookAtLH(&mPresentationView, &from, &at, &up);
|
||||||
|
mPresentationCamera = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DPLRenderer::ClearPresentationCamera()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
mPresentationCamera = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Give the pilot a vehicle to look at. Their own is built insideEntity - a
|
||||||
|
// cockpit and no hull - so on the podium they would be the one empty spot.
|
||||||
|
// outsideEntity is what builds the exterior, and the renderables for it are
|
||||||
|
// added to what is already there.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DPLRenderer::ShowViewpointFromOutside()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
Entity *viewpoint = GetLinkedEntity();
|
||||||
|
if (viewpoint == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: no viewpoint entity to turn around\n"
|
||||||
|
<< std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add the exterior alongside what is already there rather than tearing
|
||||||
|
// the entity down and rebuilding it. The teardown path is safe for
|
||||||
|
// scenery going out of range, but not for the viewpoint entity: it is
|
||||||
|
// never uninteresting, the eye renderable goes with it, and doing it
|
||||||
|
// mid-mission stops the scene rendering altogether.
|
||||||
|
//
|
||||||
|
// This is what NotifyOfNewInterestingEntity does, minus the teardown
|
||||||
|
// and with the view type forced.
|
||||||
|
//
|
||||||
|
Check(application);
|
||||||
|
ResourceFile *resource_file = application->GetResourceFile();
|
||||||
|
Check(resource_file);
|
||||||
|
ResourceDescription *video_resource = resource_file->SearchList(
|
||||||
|
viewpoint->GetResourceID(),
|
||||||
|
ResourceDescription::VideoModelResourceType);
|
||||||
|
|
||||||
|
if (video_resource == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: own vehicle has no exterior model\n"
|
||||||
|
<< std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
video_resource->Lock();
|
||||||
|
MakeEntityRenderables(viewpoint, video_resource, outsideEntity);
|
||||||
|
video_resource->Unlock();
|
||||||
|
|
||||||
|
DEBUG_STREAM << "WinnersCircle: own vehicle given an exterior\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Come up out of the black. SetFogStyle(winnersCircleFogStyle) has already
|
||||||
|
// put the target fog in place, so remember it and ramp toward it - the same
|
||||||
|
// multiply the end-of-mission fade uses, run the other way.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
DPLRenderer::StartPresentationFadeIn(Scalar seconds)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
mPresentationFogRed = fogRed;
|
||||||
|
mPresentationFogGreen = fogGreen;
|
||||||
|
mPresentationFogBlue = fogBlue;
|
||||||
|
mPresentationFogNear = currentFogNear;
|
||||||
|
mPresentationFogFar = currentFogFar;
|
||||||
|
|
||||||
|
mPresentationFadeTime = (seconds > 0.0f) ? seconds : 0.01f;
|
||||||
|
mPresentationFadeEnd = Now();
|
||||||
|
mPresentationFadeEnd += mPresentationFadeTime;
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -6802,6 +7480,54 @@ void DPLRenderer::SortAndReloadNameBitmaps()
|
|||||||
}
|
}
|
||||||
|
|
||||||
LoadOrdinalBitmaps();
|
LoadOrdinalBitmaps();
|
||||||
|
|
||||||
|
BindNamePlates();
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Put the callsigns on the Winners Circle plates.
|
||||||
|
//
|
||||||
|
// mNameTextures is indexed by finishing place (rank + 1), which is how the
|
||||||
|
// signs are numbered, so the plate beside each spot names whoever is standing
|
||||||
|
// on it. The plates are static geometry and have already been merged into the
|
||||||
|
// consolidated mesh by now, so it is the consolidated draw ops that have to be
|
||||||
|
// re-pointed - the objects they were built from are no longer drawn.
|
||||||
|
//
|
||||||
|
void DPLRenderer::BindNamePlates()
|
||||||
|
{
|
||||||
|
int name_count = (int) (sizeof(mNameTextures) / sizeof(mNameTextures[0]));
|
||||||
|
int bound = 0;
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
std::list<d3d_OBJECT*>::const_iterator iter;
|
||||||
|
for (iter = mConsolidatedStaticObjects.begin();
|
||||||
|
iter != mConsolidatedStaticObjects.end(); ++iter)
|
||||||
|
{
|
||||||
|
d3d_OBJECT *object = *iter;
|
||||||
|
if (object == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int op = 0; op < object->GetDrawOpCount(); ++op)
|
||||||
|
{
|
||||||
|
L4DRAWOP *draw_op = object->GetDrawOp(op);
|
||||||
|
int plate = d3d_OBJECT::NamePlateFor(draw_op->texture.texture);
|
||||||
|
if (plate == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++found;
|
||||||
|
if (plate < name_count && mNameTextures[plate] != NULL)
|
||||||
|
{
|
||||||
|
draw_op->texture.texture = mNameTextures[plate];
|
||||||
|
++bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_STREAM << "NamePlates: " << bound << " of " << found
|
||||||
|
<< " plates given a callsign\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -310,10 +310,57 @@ public:
|
|||||||
|
|
||||||
void SetViewAngle(Degree new_angle);
|
void SetViewAngle(Degree new_angle);
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Winners Circle presentation camera.
|
||||||
|
//
|
||||||
|
// The eye normally rides the viewpoint entity - your own vehicle -
|
||||||
|
// and DPLEyeRenderable writes D3DTS_VIEW from it each frame. For the
|
||||||
|
// podium the shot has to come from off the stand looking back, with
|
||||||
|
// every racer including you in frame, so this overrides that view
|
||||||
|
// for as long as it is set. Culling still runs from the viewpoint
|
||||||
|
// entity, which is standing on the podium, so the stand and everyone
|
||||||
|
// on it stay resident.
|
||||||
|
//
|
||||||
|
// Eye and target are world space. ClearPresentationCamera() hands
|
||||||
|
// the view back to the entity.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void SetPresentationCamera(const Point3D &eye, const Point3D &look_at);
|
||||||
|
void ClearPresentationCamera();
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// True once the Winners Circle has taken the screen. The end-of-
|
||||||
|
// mission fade-to-black checks this and stands down: it multiplies
|
||||||
|
// the fog to black over FADE_OUT_TIME, which would otherwise black
|
||||||
|
// out the podium no matter what is drawn.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
Logical InPresentation() const { return mPresentationFog; }
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Bring the presentation up out of black over the given seconds. The
|
||||||
|
// race's own fade-to-black has already run by this point; this is
|
||||||
|
// its mirror image, ramping the fog colour and both fog distances
|
||||||
|
// back up from nothing to what the Winners Circle asked for.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void StartPresentationFadeIn(Scalar seconds);
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Rebuild the viewpoint entity - the pilot's own vehicle - as an
|
||||||
|
// exterior. It is normally built insideEntity, which is a cockpit
|
||||||
|
// and no hull, so from any camera that is not in it you see nothing
|
||||||
|
// where your own vehicle should be. Disconnected_Eye is the engine's
|
||||||
|
// own switch for this: "so higher level renderers can fix the eye in
|
||||||
|
// one spot and watch the viewpoint entity drive around."
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
void ShowViewpointFromOutside();
|
||||||
|
|
||||||
unsigned int* MakeBitSliceStorage();
|
unsigned int* MakeBitSliceStorage();
|
||||||
|
|
||||||
void SortAndReloadNameBitmaps();
|
void SortAndReloadNameBitmaps();
|
||||||
|
|
||||||
|
// re-point the Winners Circle sign faces at the current callsign
|
||||||
|
// textures; the plates live in the consolidated static mesh
|
||||||
|
void BindNamePlates();
|
||||||
|
|
||||||
void LoadNameBitmaps();
|
void LoadNameBitmaps();
|
||||||
|
|
||||||
void LoadOrdinalBitmaps();
|
void LoadOrdinalBitmaps();
|
||||||
@@ -438,6 +485,22 @@ private:
|
|||||||
D3DXMATRIX mDecalProjectionMatrix;
|
D3DXMATRIX mDecalProjectionMatrix;
|
||||||
float mDecalEpsilon;
|
float mDecalEpsilon;
|
||||||
|
|
||||||
|
// Winners Circle: overrides D3DTS_VIEW while set (see
|
||||||
|
// SetPresentationCamera)
|
||||||
|
D3DXMATRIX mPresentationView;
|
||||||
|
Logical mPresentationCamera;
|
||||||
|
Logical mPresentationFog;
|
||||||
|
// >0 pillarboxes the scene to this aspect. The Winners Circle was
|
||||||
|
// built to be looked at from a 4:3 pod monitor and its platform runs
|
||||||
|
// out at the sides of a 16:9 canvas, so the shot is cropped to the
|
||||||
|
// shape it was composed for and the surround left black.
|
||||||
|
float mPresentationAspect;
|
||||||
|
// fade-in: end time, duration, and the fog it is ramping toward
|
||||||
|
Time mPresentationFadeEnd;
|
||||||
|
Scalar mPresentationFadeTime;
|
||||||
|
float mPresentationFogRed, mPresentationFogGreen, mPresentationFogBlue;
|
||||||
|
float mPresentationFogNear, mPresentationFogFar;
|
||||||
|
|
||||||
void FindBestAdapterIndices(bool isWindowed);
|
void FindBestAdapterIndices(bool isWindowed);
|
||||||
|
|
||||||
float mCloudRed, mCloudGreen, mCloudBlue;
|
float mCloudRed, mCloudGreen, mCloudBlue;
|
||||||
@@ -580,3 +643,11 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
extern LPDIRECT3D9 gD3D;
|
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();
|
||||||
|
|||||||
+572
-54
@@ -11,6 +11,16 @@
|
|||||||
#include "l4app.h"
|
#include "l4app.h"
|
||||||
#include "..\RP\VTV.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
|
// RB 1/14/07
|
||||||
//#include <dpl\dpl.h>
|
//#include <dpl\dpl.h>
|
||||||
//#include <dpl\dpl_2d.h>
|
//#include <dpl\dpl_2d.h>
|
||||||
@@ -1082,8 +1092,310 @@ void
|
|||||||
// DPL_FLUSH_DCS ( myDCS );
|
// DPL_FLUSH_DCS ( myDCS );
|
||||||
// }
|
// }
|
||||||
myRenderer->GetMatrixStack()->Push();
|
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;
|
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());
|
myRenderer->GetMatrixStack()->MultMatrix(&tempMatrix.ToD3DMatrix());
|
||||||
//myLocalToWorld = *myRenderer->GetMatrixStack()->GetTop();
|
//myLocalToWorld = *myRenderer->GetMatrixStack()->GetTop();
|
||||||
@@ -2283,6 +2595,20 @@ void
|
|||||||
//
|
//
|
||||||
case FadeOutState:
|
case FadeOutState:
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The Winners Circle has taken the screen, so leave it alone.
|
||||||
|
// This fade multiplies the fog colour and both fog distances
|
||||||
|
// toward zero every frame, which blacks out the whole scene -
|
||||||
|
// correct when the race just ends, fatal to a podium shown
|
||||||
|
// afterwards. Stand down and stop running.
|
||||||
|
//
|
||||||
|
if (myRenderer->InPresentation())
|
||||||
|
{
|
||||||
|
myState = WaitForStartState;
|
||||||
|
myRenderer->RemoveDynamicRenderable(this);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
percent_time_left = (myStateTimer - current_time)/FADE_OUT_TIME;
|
percent_time_left = (myStateTimer - current_time)/FADE_OUT_TIME;
|
||||||
if(percent_time_left <= 0.0f)
|
if(percent_time_left <= 0.0f)
|
||||||
{
|
{
|
||||||
@@ -2350,7 +2676,76 @@ ReticleRenderable::ReticleRenderable(
|
|||||||
|
|
||||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
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;
|
L4VERTEX_2D *verts;
|
||||||
mVB->Lock(0, 0, (void**)&verts, 0);
|
mVB->Lock(0, 0, (void**)&verts, 0);
|
||||||
@@ -2358,62 +2753,125 @@ ReticleRenderable::ReticleRenderable(
|
|||||||
DWORD color = D3DCOLOR_XRGB(0, 128, 0);
|
DWORD color = D3DCOLOR_XRGB(0, 128, 0);
|
||||||
float segmentLen = 5.0f / 192.0f;
|
float segmentLen = 5.0f / 192.0f;
|
||||||
float spread = 5.0f / 256.0f;
|
float spread = 5.0f / 256.0f;
|
||||||
float width = myRenderer->GetWidth();
|
//
|
||||||
float height = myRenderer->GetHeight();
|
// Pre-transformed vertices are absolute screen pixels - the viewport
|
||||||
float centerX = width / 2.0f;
|
// clips them but does not shift them - so its origin is carried here.
|
||||||
float centerY = height / 2.0f;
|
//
|
||||||
|
float centerX = (float) viewport.X + width / 2.0f;
|
||||||
|
float centerY = (float) viewport.Y + height / 2.0f;
|
||||||
|
|
||||||
// top segment
|
//
|
||||||
verts[0].x = centerX;
|
// Land on the pixel CENTRE, not the corner, so the quads below span
|
||||||
verts[0].y = centerY - (spread + segmentLen) * height;
|
// whole pixels: an arm one pixel wide about a centre of x.5 runs from
|
||||||
verts[0].z = 0.0f;
|
// x.0 to x+1.0 and covers exactly one column.
|
||||||
verts[0].rhw = 1.0f;
|
//
|
||||||
verts[0].color = color;
|
centerX = (float)(int) centerX + 0.5f;
|
||||||
|
centerY = (float)(int) centerY + 0.5f;
|
||||||
|
|
||||||
verts[1].x = centerX;
|
//
|
||||||
verts[1].y = centerY - spread * height;
|
// Worth a line in the log: it names the target, the renderer's
|
||||||
verts[1].z = 0.0f;
|
// requested size and where the crosshair actually landed, so a
|
||||||
verts[1].rhw = 1.0f;
|
// report of it being off says which of the three moved.
|
||||||
verts[1].color = color;
|
//
|
||||||
|
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;
|
// Each arm is a quad one pixel thick rather than a line. D3D9 line
|
||||||
verts[2].y = centerY;
|
// rasterisation follows the diamond-exit rule and is free to differ
|
||||||
verts[2].z = 0.0f;
|
// between drivers on a segment that runs along a pixel boundary,
|
||||||
verts[2].rhw = 1.0f;
|
// which is how a crosshair loses one pair of arms and keeps the
|
||||||
verts[2].color = color;
|
// 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;
|
// Thickness follows the target the way the arms' length does, rather
|
||||||
verts[3].z = 0.0f;
|
// than being one pixel whatever the resolution.
|
||||||
verts[3].rhw = 1.0f;
|
//
|
||||||
verts[3].color = color;
|
// 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
|
struct Arm
|
||||||
verts[4].x = centerX;
|
{
|
||||||
verts[4].y = centerY + (spread + segmentLen) * height;
|
float x0, y0, x1, y1; // opposite corners, in pixels
|
||||||
verts[4].z = 0.0f;
|
};
|
||||||
verts[4].rhw = 1.0f;
|
const Arm arms[4] =
|
||||||
verts[4].color = color;
|
{
|
||||||
|
// 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;
|
int v = 0;
|
||||||
verts[5].y = centerY + spread * height;
|
for (int a = 0; a < 4; ++a)
|
||||||
verts[5].z = 0.0f;
|
{
|
||||||
verts[5].rhw = 1.0f;
|
const Arm &arm = arms[a];
|
||||||
verts[5].color = color;
|
//
|
||||||
|
// 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
|
for (int c = 0; c < 6; ++c)
|
||||||
verts[6].x = centerX - (spread + segmentLen) * height;
|
{
|
||||||
verts[6].y = centerY;
|
verts[v].x = quad_x[c];
|
||||||
verts[6].z = 0.0f;
|
verts[v].y = quad_y[c];
|
||||||
verts[6].rhw = 1.0f;
|
verts[v].z = 0.0f;
|
||||||
verts[6].color = color;
|
verts[v].rhw = 1.0f;
|
||||||
|
verts[v].color = color;
|
||||||
verts[7].x = centerX - spread * height;
|
++v;
|
||||||
verts[7].y = centerY;
|
}
|
||||||
verts[7].z = 0.0f;
|
}
|
||||||
verts[7].rhw = 1.0f;
|
Verify(v == crosshairVertexCount);
|
||||||
verts[7].color = color;
|
|
||||||
|
|
||||||
mVB->Unlock();
|
mVB->Unlock();
|
||||||
}
|
}
|
||||||
@@ -2483,9 +2941,26 @@ void ReticleRenderable::Render(int pass, const D3DXMATRIX *viewTransform)
|
|||||||
{
|
{
|
||||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
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->SetTexture(0, NULL);
|
||||||
device->SetStreamSource(0, mVB, 0, sizeof(L4VERTEX_2D));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -2642,6 +3117,7 @@ CameraShipHUDRenderable::CameraShipHUDRenderable(Entity *entity, ExecutionType e
|
|||||||
//
|
//
|
||||||
playerRank = NULL;
|
playerRank = NULL;
|
||||||
oldPlayerRank = NULL;
|
oldPlayerRank = NULL;
|
||||||
|
rankCount = 0;
|
||||||
nameDCS = NULL;
|
nameDCS = NULL;
|
||||||
rankDCS = NULL;
|
rankDCS = NULL;
|
||||||
nameInstance = NULL;
|
nameInstance = NULL;
|
||||||
@@ -2685,6 +3161,18 @@ CameraShipHUDRenderable::CameraShipHUDRenderable(Entity *entity, ExecutionType e
|
|||||||
Verify(camera_player_count <= MAX_PLAYER_NAMES);
|
Verify(camera_player_count <= MAX_PLAYER_NAMES);
|
||||||
playerRank = new (int (*[camera_player_count]));
|
playerRank = new (int (*[camera_player_count]));
|
||||||
Register_Pointer(playerRank);
|
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;
|
Player *active_player;
|
||||||
if(player_group)
|
if(player_group)
|
||||||
{
|
{
|
||||||
@@ -2932,8 +3420,14 @@ void CameraShipHUDRenderable::Execute()
|
|||||||
// whenever they change!
|
// 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]))
|
if (oldPlayerRank[ii] != (*playerRank[ii]))
|
||||||
{
|
{
|
||||||
oldPlayerRank[ii] = *playerRank[ii];
|
oldPlayerRank[ii] = *playerRank[ii];
|
||||||
@@ -5469,7 +5963,17 @@ void
|
|||||||
// time based upon the setting of the eyepoint rotation
|
// 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;
|
mForceUpdate = false;
|
||||||
oldLocalToWorld = myEntity->localToWorld;
|
oldLocalToWorld = myEntity->localToWorld;
|
||||||
@@ -5493,6 +5997,20 @@ void
|
|||||||
mat = mat4;
|
mat = mat4;
|
||||||
|
|
||||||
D3DXVECTOR3 pos(mat(3,0), mat(3, 1), mat(3,2));
|
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,0) = 0;
|
||||||
mat(3,1) = 0;
|
mat(3,1) = 0;
|
||||||
mat(3,2) = 0;
|
mat(3,2) = 0;
|
||||||
|
|||||||
@@ -783,6 +783,19 @@ class ReticleRenderable :
|
|||||||
void Render(int pass, const D3DXMATRIX *viewTransform);
|
void Render(int pass, const D3DXMATRIX *viewTransform);
|
||||||
|
|
||||||
protected:
|
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
|
// Last known position of the reticle
|
||||||
Vector2DOf<float> myOldReticlePosition;
|
Vector2DOf<float> myOldReticlePosition;
|
||||||
@@ -833,6 +846,19 @@ class CameraShipHUDRenderable :
|
|||||||
int
|
int
|
||||||
playerCount;
|
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
|
int
|
||||||
oldFollowedPlayerIndex,
|
oldFollowedPlayerIndex,
|
||||||
*followedPlayerIndex;
|
*followedPlayerIndex;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<TargetName>Munga_l4</TargetName>
|
<TargetName>Munga_l4</TargetName>
|
||||||
<!-- Legacy DirectX SDK (June 2010) after the Windows SDK so modern headers/libs win;
|
<!-- Legacy DirectX SDK (June 2010) after the Windows SDK so modern headers/libs win;
|
||||||
only d3dx9 and dxerr actually come from it. -->
|
only d3dx9 and dxerr actually come from it. -->
|
||||||
<IncludePath>$(IncludePath);$(DXSDK_DIR)Include;..\extern\steamworks_sdk_164\sdk\public</IncludePath>
|
<IncludePath>$(IncludePath);$(DXSDK_DIR)Include;..\extern\steamworks_sdk_165\sdk\public</IncludePath>
|
||||||
<LibraryPath>$(LibraryPath);$(DXSDK_DIR)Lib\x86</LibraryPath>
|
<LibraryPath>$(LibraryPath);$(DXSDK_DIR)Lib\x86</LibraryPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemDefinitionGroup>
|
<ItemDefinitionGroup>
|
||||||
@@ -151,6 +151,7 @@
|
|||||||
<ClCompile Include="..\MUNGA\INTEREST.cpp" />
|
<ClCompile Include="..\MUNGA\INTEREST.cpp" />
|
||||||
<ClCompile Include="..\MUNGA\INTORGN.cpp" />
|
<ClCompile Include="..\MUNGA\INTORGN.cpp" />
|
||||||
<ClCompile Include="..\MUNGA\ITERATOR.cpp" />
|
<ClCompile Include="..\MUNGA\ITERATOR.cpp" />
|
||||||
|
<ClCompile Include="..\MUNGA\INPUTSCRIPT.cpp" />
|
||||||
<ClCompile Include="..\MUNGA\JMOVER.cpp" />
|
<ClCompile Include="..\MUNGA\JMOVER.cpp" />
|
||||||
<ClCompile Include="..\MUNGA\JOINT.cpp" />
|
<ClCompile Include="..\MUNGA\JOINT.cpp" />
|
||||||
<ClCompile Include="..\MUNGA\LAMP.cpp" />
|
<ClCompile Include="..\MUNGA\LAMP.cpp" />
|
||||||
@@ -227,6 +228,7 @@
|
|||||||
<ClCompile Include="..\MUNGA\WRHOUS.cpp" />
|
<ClCompile Include="..\MUNGA\WRHOUS.cpp" />
|
||||||
<ClCompile Include=".\DXUtils.cpp" />
|
<ClCompile Include=".\DXUtils.cpp" />
|
||||||
<ClCompile Include=".\L4APP.cpp" />
|
<ClCompile Include=".\L4APP.cpp" />
|
||||||
|
<ClCompile Include=".\L4AUDEFX.cpp" />
|
||||||
<ClCompile Include=".\L4AUDHDW.cpp" />
|
<ClCompile Include=".\L4AUDHDW.cpp" />
|
||||||
<ClCompile Include=".\L4AUDIO.cpp" />
|
<ClCompile Include=".\L4AUDIO.cpp" />
|
||||||
<ClCompile Include=".\L4AUDLVL.cpp" />
|
<ClCompile Include=".\L4AUDLVL.cpp" />
|
||||||
@@ -263,6 +265,7 @@
|
|||||||
<ConformanceMode>true</ConformanceMode>
|
<ConformanceMode>true</ConformanceMode>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include=".\L4PADBINDINGS.cpp" />
|
<ClCompile Include=".\L4PADBINDINGS.cpp" />
|
||||||
|
<ClCompile Include=".\L4JOY.cpp" />
|
||||||
<ClCompile Include=".\L4PADRIO.cpp" />
|
<ClCompile Include=".\L4PADRIO.cpp" />
|
||||||
<ClCompile Include=".\L4PCSPAK.cpp" />
|
<ClCompile Include=".\L4PCSPAK.cpp" />
|
||||||
<ClCompile Include=".\L4PLASMA.cpp" />
|
<ClCompile Include=".\L4PLASMA.cpp" />
|
||||||
@@ -431,6 +434,7 @@
|
|||||||
<ClInclude Include="..\MUNGA\WRHOUS.h" />
|
<ClInclude Include="..\MUNGA\WRHOUS.h" />
|
||||||
<ClInclude Include=".\DXUtils.h" />
|
<ClInclude Include=".\DXUtils.h" />
|
||||||
<ClInclude Include=".\L4APP.H" />
|
<ClInclude Include=".\L4APP.H" />
|
||||||
|
<ClInclude Include=".\L4AUDEFX.h" />
|
||||||
<ClInclude Include=".\L4AUDHDW.h" />
|
<ClInclude Include=".\L4AUDHDW.h" />
|
||||||
<ClInclude Include=".\L4AUDIO.h" />
|
<ClInclude Include=".\L4AUDIO.h" />
|
||||||
<ClInclude Include=".\L4AUDLVL.h" />
|
<ClInclude Include=".\L4AUDLVL.h" />
|
||||||
@@ -454,6 +458,7 @@
|
|||||||
<ClInclude Include=".\L4NETTRANSPORT.h" />
|
<ClInclude Include=".\L4NETTRANSPORT.h" />
|
||||||
<ClInclude Include=".\L4KEYLIGHT.h" />
|
<ClInclude Include=".\L4KEYLIGHT.h" />
|
||||||
<ClInclude Include=".\L4PADBINDINGS.h" />
|
<ClInclude Include=".\L4PADBINDINGS.h" />
|
||||||
|
<ClInclude Include=".\L4JOY.h" />
|
||||||
<ClInclude Include=".\L4STEAMTRANSPORT.h" />
|
<ClInclude Include=".\L4STEAMTRANSPORT.h" />
|
||||||
<ClInclude Include=".\L4PARTICLES.h" />
|
<ClInclude Include=".\L4PARTICLES.h" />
|
||||||
<ClInclude Include=".\L4MFDVIEW.h" />
|
<ClInclude Include=".\L4MFDVIEW.h" />
|
||||||
|
|||||||
@@ -486,6 +486,9 @@
|
|||||||
<ClCompile Include=".\L4AUDHDW.cpp">
|
<ClCompile Include=".\L4AUDHDW.cpp">
|
||||||
<Filter>Source Files\MUNGA_L4</Filter>
|
<Filter>Source Files\MUNGA_L4</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include=".\L4AUDEFX.cpp">
|
||||||
|
<Filter>Source Files\MUNGA_L4</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include=".\L4AUDIO.cpp">
|
<ClCompile Include=".\L4AUDIO.cpp">
|
||||||
<Filter>Source Files\MUNGA_L4</Filter>
|
<Filter>Source Files\MUNGA_L4</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -1058,6 +1061,9 @@
|
|||||||
<ClInclude Include=".\L4APP.H">
|
<ClInclude Include=".\L4APP.H">
|
||||||
<Filter>Header Files\MUNGA_L4</Filter>
|
<Filter>Header Files\MUNGA_L4</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include=".\L4AUDEFX.h">
|
||||||
|
<Filter>Header Files\MUNGA_L4</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include=".\L4AUDHDW.h">
|
<ClInclude Include=".\L4AUDHDW.h">
|
||||||
<Filter>Header Files\MUNGA_L4</Filter>
|
<Filter>Header Files\MUNGA_L4</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd)
|
|||||||
default.file = "";
|
default.file = "";
|
||||||
default.implemented = false;
|
default.implemented = false;
|
||||||
default.loop = SampleLoop::LoopAtWill;
|
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)
|
if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,37 +1,142 @@
|
|||||||
# Red Planet 4.12
|
# Red Planet 4.12 — the Steamification
|
||||||
|
|
||||||
**Red Planet** is the VWE pod-racing game built on the in-house **MUNGA** engine
|
**Red Planet** is VWE's pod-racing game: eight-player VTV contests on Mars
|
||||||
(Win32 / DirectX 9, `MUNGA_L4` platform layer). The **4.12** line is the
|
— **Martian Death Race** and **Martian Football** — originally played from
|
||||||
consumer port: a version of Red Planet that can be **sold on Steam** and played
|
inside VWE Tesla cockpit pods. This repo is the third life of that code:
|
||||||
over **internet multiplayer**, without cockpit-pod hardware.
|
|
||||||
|
|
||||||
Forked from [RP411](https://gitea.mysticmachines.com/VWE/RP411.git)
|
| Generation | What it was |
|
||||||
(the arcade/cockpit 4.11 line) with full history preserved.
|
|------------|-------------|
|
||||||
|
| **Red Planet 4.10** | The original game, running on the **Tesla 1** pod platform (DOS-era MUNGA engine, serial RIO cockpit hardware, operator console, batch-file relaunch between missions) |
|
||||||
|
| **Red Planet 4.11** | The **Win32 port** of 4.10 ([RP411](https://gitea.mysticmachines.com/VWE/RP411.git)) — DirectX 9, WinSock TCP, TeslaConsole/TeslaLauncher session control; still runs in pods, still a LAN |
|
||||||
|
| **Red Planet 4.12** | **This repo: the Steamification of 4.11** — the same engine, the same wire protocol, the same missions, made distributable on Steam and playable over the internet with no cockpit hardware |
|
||||||
|
|
||||||
## Goals
|
The architecture is deliberately conservative: the VWE's design survives
|
||||||
|
intact, with each pod-era dependency replaced by a consumer equivalent
|
||||||
|
behind the engine's existing seams.
|
||||||
|
|
||||||
1. **Steam distribution** — Steamworks integration, SteamPipe builds, store-ready packaging.
|
| Arcade (4.10/4.11) | 4.12 replacement |
|
||||||
2. **Internet multiplayer** — take the engine's LAN-era WinSock networking online
|
|--------------------|------------------|
|
||||||
(NAT traversal, matchmaking, latency tolerance).
|
| RIO cockpit board (serial) | **PadRIO** — virtual RIO from XInput pad + keyboard, fully rebindable (`bindings.txt`, vRIO profile format) |
|
||||||
3. **No cockpit required** — keyboard / mouse / gamepad input and on-screen
|
| Seven physical displays | **Single-window cockpit** — all displays composed on a locked 1920×1080 canvas around the viewscreen, with the real button banks lamp-lit and clickable |
|
||||||
replacements for the pod's RIO panel and plasma display, drawing on
|
| Pod button lamps | On-screen lamps, plus an **RGB keyboard mirror** (Windows Dynamic Lighting) |
|
||||||
[vRIO](https://gitea.mysticmachines.com/VWE/VRIO.git).
|
| TeslaConsole operator | **In-game front end** — race setup menu builds the mission egg locally; an in-process console marshals every race (missions still only end on a console stop, exactly as designed in 1994) |
|
||||||
4. **Self-hosted sessions** — in-game race setup/join replacing the operator-driven
|
| TeslaLauncher relaunch-per-mission | **Single binary** — menu → race → results → menu in one process |
|
||||||
[TeslaConsole](https://gitea.mysticmachines.com/VWE/TeslaSuite.git) flow.
|
| WinSock TCP LAN mesh | **NetTransport seam** — the deterministic pod mesh unchanged, running over plain TCP (LAN/dev) or **Steam Networking Sockets** (FakeIP + Steam Datagram Relay) |
|
||||||
|
| Site network / fixed IPs | **Steam lobbies** — lobby owner is the console; members exchange FakeIPs and loadouts as lobby data |
|
||||||
|
|
||||||
See [docs/RP412-ROADMAP.md](docs/RP412-ROADMAP.md) for the plan and the
|
**Status: it works.**
|
||||||
Steam-multiplayer logistics.
|
[v4.12.2](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.2)
|
||||||
|
carried the first verified end-to-end internet build: three machines, three
|
||||||
|
Steam accounts, lobby → mesh → marshaled five-minute race → deaths and
|
||||||
|
respawns → timed stop → results on every machine → rematch from the same
|
||||||
|
lobby.
|
||||||
|
|
||||||
|
[v4.12.3](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.3)
|
||||||
|
is about the screen. The cockpit now scales up as well as down and re-fits
|
||||||
|
whenever the window changes, keeping 16:9 so a wide panel letterboxes
|
||||||
|
instead of stretching; `-fit` runs it borderless over the whole monitor and
|
||||||
|
picks the render size to land on the viewscreen 1:1. The displays are the
|
||||||
|
player's to arrange — each of the six can be scaled on its own and the
|
||||||
|
radar moved out of the middle of the road (`environ.ini`). Each display's
|
||||||
|
button bank now reaches under its glass, so the picture itself is the press
|
||||||
|
target rather than a sliver at the edge.
|
||||||
|
|
||||||
|
[v4.12.4](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.4)
|
||||||
|
is about the lobby. Both rooms now show the host's mission setup — the track,
|
||||||
|
then time of day, weather and game length — since everyone flies the owner's
|
||||||
|
picks and it was the one thing in the room nobody could see for themselves.
|
||||||
|
The football team sheet names each player's VTV beside their team colours and
|
||||||
|
position. And a lobby holds **eight** players rather than four, a full grid as
|
||||||
|
the pod hall ran it, with the room sizing its roster to whatever space the
|
||||||
|
window gives it.
|
||||||
|
|
||||||
|
[v4.12.5](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.5)
|
||||||
|
restores the **Winners Circle**. The pod hall stood the finishers on a
|
||||||
|
numbered award platform when the race ended, and all of it was still in this
|
||||||
|
repo — the stand, eight ranked spots in every map, and the code to put racers
|
||||||
|
on them — wired only into the mission-review build and so never once run by a
|
||||||
|
pod. The race now fades out and fades back in on the platform: finishers in
|
||||||
|
finishing order, each pilot's callsign on the plate beside their spot, the
|
||||||
|
cockpit glass cleared away, held for a few seconds before the results screen.
|
||||||
|
Three pieces of it had been stubbed out in the D3D9 port and are working
|
||||||
|
again.
|
||||||
|
|
||||||
|
[v4.12.6](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.6)
|
||||||
|
is about the windows. Where you put them now survives the
|
||||||
|
menu-race-menu loop: `RP412MFDLAYOUT` remembers the game window, the
|
||||||
|
exploded view's display panes and the plasma glass in `mfd_layout.cfg`
|
||||||
|
beside `bindings.txt`. Append `,noframe` to a line to take that
|
||||||
|
window's title bar and border off — a cockpit filling the monitor edge
|
||||||
|
to edge at a rect you chose, rather than `-fit` taking the whole
|
||||||
|
screen — and the setup screen carries its own **EXIT GAME** button,
|
||||||
|
since a window with no title bar needs a way out. The Steam buttons
|
||||||
|
now dim and say *STEAM NOT RUNNING* instead of disappearing. And the
|
||||||
|
Winners Circle camera is framed off the award stand itself rather than
|
||||||
|
off whoever is standing on it, so the shot is the same one for every
|
||||||
|
player at every head count.
|
||||||
|
|
||||||
|
[v4.12.7](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.7)
|
||||||
|
is about sticks. Anything that is not an Xbox-class pad — a flight stick,
|
||||||
|
a HOTAS throttle, a twist grip, rudder pedals, a wheel — comes in through
|
||||||
|
DirectInput rather than XInput, and the game could not see any of it.
|
||||||
|
Now it can, and `joyconfig.bat` sets it up: the wizard asks you to move
|
||||||
|
each control in turn, works out which device and axis answered and which
|
||||||
|
way round it reads, and writes the joystick rows of `bindings.txt`,
|
||||||
|
leaving anything you have edited yourself alone. A twist grip or rudder
|
||||||
|
bar drives both pedals through one signed `Pedals` axis, and a real
|
||||||
|
throttle lever owns its channel outright rather than nudging a position
|
||||||
|
the way a spring-centred stick has to. Ported from the sibling BT411.
|
||||||
|
|
||||||
|
## Playing
|
||||||
|
|
||||||
|
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
|
||||||
|
run `start-windowed.bat` — the game boots into the setup menu, where the
|
||||||
|
SCENARIO group picks Death Race or Football (Football swaps in the
|
||||||
|
team/position columns and its own track list). Steam multiplayer: see
|
||||||
|
[docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md). It runs
|
||||||
|
under Spacewar (480) for now; Red Planet's own AppID, 5108000, is parked
|
||||||
|
until its Steam permissions are sorted.
|
||||||
|
|
||||||
|
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
|
||||||
|
mission. Full map with pad and keyboard diagrams:
|
||||||
|
[docs/CONTROLS.md](docs/CONTROLS.md).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Unchanged from 4.11 for now — see [BUILD.md](BUILD.md)
|
**VS 2022 (v143)** + DirectX SDK June 2010 + the vendored Steamworks SDK
|
||||||
(VS 2005/2008 + DirectX SDK June 2010, `Release|Win32`, output `Release\rpl4opt.exe`).
|
(`extern/steamworks_sdk_165`) — see [BUILD.md](BUILD.md). Solution
|
||||||
Toolchain modernization is a roadmap item.
|
`WinTesla.sln`, configuration `Release|Win32`, output
|
||||||
|
`Release\rpl4opt.exe`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Doc | Contents |
|
||||||
|
|-----|----------|
|
||||||
|
| [docs/CONTROLS.md](docs/CONTROLS.md) | Controls map — pad and keyboard diagrams, the panel button banks, rebinding |
|
||||||
|
| [docs/RP412-ROADMAP.md](docs/RP412-ROADMAP.md) | The original plan and workstreams |
|
||||||
|
| [docs/RP412-FRONTEND-DESIGN.md](docs/RP412-FRONTEND-DESIGN.md) | TeslaConsole analysis, the egg format, the console protocol, and the Steam mapping — with status notes as each layer landed |
|
||||||
|
| [docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) | Multiplayer test procedure, Steam Input notes, the abort key |
|
||||||
|
| [docs/RP412-LINUX.md](docs/RP412-LINUX.md) | Linux compatibility assessment — Proton path vs native port; parked until late playtesting |
|
||||||
|
| [docs/NET-TEST.md](docs/NET-TEST.md) | Network test protocol — WAN emulation profiles, the NetLog/NetStats telemetry, the stall-recovery test, `-spoolstats` |
|
||||||
|
| [docs/RP412-UNIFIED-BUILD.md](docs/RP412-UNIFIED-BUILD.md) | Proposal (not scheduled) — one source tree building native on Windows and Linux; the portability survey behind it |
|
||||||
|
| [BUILD.md](BUILD.md) | Toolchain and build steps |
|
||||||
|
|
||||||
|
Dev tooling: `tools/two-pod-test.ps1` races two pods on loopback,
|
||||||
|
marshaled by a console feeder speaking the arcade Munga protocol.
|
||||||
|
|
||||||
## Related repositories
|
## Related repositories
|
||||||
|
|
||||||
| Repo | Role |
|
| Repo | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [RP411](https://gitea.mysticmachines.com/VWE/RP411.git) | Upstream arcade source (this repo's base) |
|
| [RP411](https://gitea.mysticmachines.com/VWE/RP411.git) | Upstream: the Win32 arcade port this repo Steamifies (full history preserved; remote `rp411` for cross-pulling fixes) |
|
||||||
| [VRIO](https://gitea.mysticmachines.com/VWE/VRIO.git) | Virtual RIO panel + vPLASMA display — input bindings and display emulation to fold in |
|
| [VRIO](https://gitea.mysticmachines.com/VWE/VRIO.git) | Virtual RIO panel + vPLASMA — source of the bindings format, the cockpit layout, and the keyboard lamp mirror |
|
||||||
| [TeslaSuite](https://gitea.mysticmachines.com/VWE/TeslaSuite.git) | TeslaConsole / Launcher / vPOD — the arcade session-control stack 4.12 replaces (and a reference for the Munga control protocol, TCP 1501) |
|
| [TeslaSuite](https://gitea.mysticmachines.com/VWE/TeslaSuite.git) | TeslaConsole / Launcher / vPOD — the arcade session-control stack 4.12 absorbed (and the reference implementation of the Munga control protocol, TCP 1501) |
|
||||||
|
|||||||
@@ -147,8 +147,17 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: CreateRPCameraShip - making the camera ship\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
CameraDirector::CreateCameraShip(time_slice);
|
CameraDirector::CreateCameraShip(time_slice);
|
||||||
SetPerformance(&RPCameraDirector::BeARPDirector);
|
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);
|
SetPerformance(&RPCameraDirector::CreateRPCameraShip);
|
||||||
}
|
}
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: RPCameraDirector built, football="
|
||||||
|
<< (int) (martianFootball ? 1 : 0)
|
||||||
|
<< " replicant=" << (int) (GetInstance() == ReplicantInstance ? 1 : 0)
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
+184
@@ -11,6 +11,49 @@
|
|||||||
#include "..\munga\hostmgr.h"
|
#include "..\munga\hostmgr.h"
|
||||||
#include "..\munga\nttmgr.h"
|
#include "..\munga\nttmgr.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// How long the mission stays up after the buzzer, for the Winners Circle.
|
||||||
|
// This timer is the only thing holding the simulation and the renderer
|
||||||
|
// open once the race is over - when it runs out the player dispatches the
|
||||||
|
// StopMission that retires the application. The stock 3 seconds is the
|
||||||
|
// fade; the rest is the podium. Kept under the +30s LightsOut post so that
|
||||||
|
// never fires while the stand is up.
|
||||||
|
//
|
||||||
|
// 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 ######################
|
//######################## RPPlayer__StatusMessage ######################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -188,6 +231,36 @@ void
|
|||||||
ForceUpdate();
|
ForceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Hold the mission open for the Winners Circle.
|
||||||
|
//
|
||||||
|
// The base handler sets a 3 second fade, and when it runs out the
|
||||||
|
// player dispatches the StopMission that retires the application and
|
||||||
|
// takes the renderer with it. That is the only thing keeping the sim
|
||||||
|
// and the renderer alive after the race, so the podium gets exactly as
|
||||||
|
// long as this timer says. Sim and render both keep running throughout
|
||||||
|
// EndingMission - neither has a case for it that bails out.
|
||||||
|
//
|
||||||
|
// Only on a clean finish: an abort should still leave promptly.
|
||||||
|
//
|
||||||
|
if (application->GetApplicationState() == Application::EndingMission)
|
||||||
|
{
|
||||||
|
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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +427,96 @@ void RPPlayer::ResetAfterDeath(DropZone::ReplyMessage *message)
|
|||||||
ForceUpdate();
|
ForceUpdate();
|
||||||
SetSimulationState(DropZoneAcquiredState);
|
SetSimulationState(DropZoneAcquiredState);
|
||||||
dropZoneLocation = message->dropZoneLocation;
|
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();
|
Time when = Now();
|
||||||
when += 1.0f;
|
when += 1.0f;
|
||||||
application->Post(HighEventPriority, this, message, when);
|
application->Post(HighEventPriority, this, message, when);
|
||||||
@@ -394,6 +557,7 @@ void
|
|||||||
}
|
}
|
||||||
AlwaysExecute();
|
AlwaysExecute();
|
||||||
deathCount = 0;
|
deathCount = 0;
|
||||||
|
respawnScheduled = False;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -439,6 +603,26 @@ void
|
|||||||
{
|
{
|
||||||
VTV *vtv = (VTV*)playerVehicle;
|
VTV *vtv = (VTV*)playerVehicle;
|
||||||
Check(vtv);
|
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);
|
vtv->Reset(message->dropZoneLocation, VTV::RegularReset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,18 @@ public:
|
|||||||
Entity
|
Entity
|
||||||
*goalEntity;
|
*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:
|
private:
|
||||||
|
|
||||||
static const IndexEntry AttributePointers[];
|
static const IndexEntry AttributePointers[];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "rpmssn.h"
|
#include "rpmssn.h"
|
||||||
#include "rpplayer.h"
|
#include "rpplayer.h"
|
||||||
#include "rpdirect.h"
|
#include "rpdirect.h"
|
||||||
|
#include "..\munga\app.h" // RPCameraLog
|
||||||
#include "crusher.h"
|
#include "crusher.h"
|
||||||
#include "runner.h"
|
#include "runner.h"
|
||||||
#include "blocker.h"
|
#include "blocker.h"
|
||||||
@@ -170,6 +171,11 @@ Player*
|
|||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (RPCameraLog())
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "CamLog: registry making an RPCameraDirector (game model '"
|
||||||
|
<< mission->GetGameModel() << "')\n" << std::flush;
|
||||||
|
}
|
||||||
RPCameraDirector::MakeMessage create_director(
|
RPCameraDirector::MakeMessage create_director(
|
||||||
RPCameraDirector::MakeMessageID,
|
RPCameraDirector::MakeMessageID,
|
||||||
sizeof(RPCameraDirector::MakeMessage),
|
sizeof(RPCameraDirector::MakeMessage),
|
||||||
|
|||||||
+146
-3
@@ -803,9 +803,17 @@ void
|
|||||||
Check(*damageZones);
|
Check(*damageZones);
|
||||||
(*damageZones)->TakeDamage(collision_damage);
|
(*damageZones)->TakeDamage(collision_damage);
|
||||||
|
|
||||||
localVelocity.angularMotion.x += 8.0f * Random - 4.0f;
|
//
|
||||||
localVelocity.angularMotion.y += 8.0f * Random - 4.0f;
|
// The tumble draws from the vehicle's OWN stream, not the
|
||||||
localVelocity.angularMotion.z += 8.0f * Random - 4.0f;
|
// 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;
|
worldLinearAcceleration = zippy_accel;
|
||||||
@@ -1273,6 +1281,18 @@ VTV::VTV(
|
|||||||
boosterSmokeDensity = 0.0f;
|
boosterSmokeDensity = 0.0f;
|
||||||
doorHitNormal = Vector3D::Identity;
|
doorHitNormal = Vector3D::Identity;
|
||||||
lastDoorHit = Time::Null;
|
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;
|
heightAboveTerrain = 0.0f;
|
||||||
forwardVelocity = 0.0f;
|
forwardVelocity = 0.0f;
|
||||||
hornBlast = -1;
|
hornBlast = -1;
|
||||||
@@ -1682,6 +1702,119 @@ void
|
|||||||
subsystem->DeathReset(reset_command);
|
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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3100,6 +3233,16 @@ void
|
|||||||
if (damageLevel >= 1.0f)
|
if (damageLevel >= 1.0f)
|
||||||
{
|
{
|
||||||
vtv->SetSimulationState(VTV::BurningState);
|
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
|
void
|
||||||
Reset(const Origin &new_origin, int reset_command);
|
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
|
void
|
||||||
DeathShutdown(int shutdown_command);
|
DeathShutdown(int shutdown_command);
|
||||||
|
|
||||||
@@ -514,6 +566,38 @@ public:
|
|||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Navigation support
|
// Navigation support
|
||||||
protected:
|
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
|
Scalar
|
||||||
targetRangeExponent,
|
targetRangeExponent,
|
||||||
currentRangeExponent;
|
currentRangeExponent;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "vtvpwr.h"
|
#include "vtvpwr.h"
|
||||||
#include "..\munga\icom.h"
|
#include "..\munga\icom.h"
|
||||||
#include "..\munga\app.h"
|
#include "..\munga\app.h"
|
||||||
|
#include "..\munga\inputscript.h"
|
||||||
#include "rpplayer.h"
|
#include "rpplayer.h"
|
||||||
#include "vtv.h"
|
#include "vtv.h"
|
||||||
|
|
||||||
@@ -400,6 +401,28 @@ VTVControlsMapper::AttributeIndexSet& VTVControlsMapper::GetAttributeIndex()
|
|||||||
// Model Support
|
// 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
|
void
|
||||||
@@ -414,6 +437,36 @@ void
|
|||||||
VTVPower *power_system =
|
VTVPower *power_system =
|
||||||
Cast_Object(VTVPower*, vtv->GetSubsystem(VTV::PowerSubsystem));
|
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
|
// Make sure the control inputs are within range
|
||||||
@@ -424,6 +477,43 @@ void
|
|||||||
Verify(stickPosition.y >= -1.0f && stickPosition.y <= 1.0f);
|
Verify(stickPosition.y >= -1.0f && stickPosition.y <= 1.0f);
|
||||||
Verify(pedalsPosition >= -1.0f && pedalsPosition <= 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
|
// Figure out the proper control model to use based upon the speed
|
||||||
@@ -802,6 +892,12 @@ void
|
|||||||
intercom->SetPTTStatus((Logical) pttStatus > 0);
|
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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,6 +1014,7 @@ VTVControlsMapper::VTVControlsMapper(
|
|||||||
stickPosition.y = 0.0f;
|
stickPosition.y = 0.0f;
|
||||||
throttlePosition = 0.0f;
|
throttlePosition = 0.0f;
|
||||||
pedalsPosition = 0.0f;
|
pedalsPosition = 0.0f;
|
||||||
|
throttleLatched = False; // the first-spawn Reset sets it
|
||||||
|
|
||||||
reverseThrust = 0;
|
reverseThrust = 0;
|
||||||
liftCut = 0;
|
liftCut = 0;
|
||||||
|
|||||||
@@ -182,6 +182,16 @@ protected:
|
|||||||
ControlsButton
|
ControlsButton
|
||||||
previousPTTStatus; // used to detect change in PTT status
|
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
|
// Model Support
|
||||||
//
|
//
|
||||||
@@ -206,6 +216,22 @@ public:
|
|||||||
void
|
void
|
||||||
SetConfigurationState(Logical enter_config);
|
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
|
virtual void
|
||||||
CreateTemporaryEventMappings(
|
CreateTemporaryEventMappings(
|
||||||
Receiver *receiver,
|
Receiver *receiver,
|
||||||
|
|||||||
+451
-26
@@ -22,11 +22,15 @@
|
|||||||
|
|
||||||
#include "rpl4pb.h"
|
#include "rpl4pb.h"
|
||||||
#include "rpl4fe.h"
|
#include "rpl4fe.h"
|
||||||
|
#include "rpl4environ.h"
|
||||||
#include "rpl4console.h"
|
#include "rpl4console.h"
|
||||||
#include "rpl4lobby.h"
|
#include "rpl4lobby.h"
|
||||||
#include "..\munga_l4\l4steamtransport.h"
|
#include "..\munga_l4\l4steamtransport.h"
|
||||||
#include "..\munga_l4\l4splr.h"
|
#include "..\munga_l4\l4splr.h"
|
||||||
|
#include "..\munga_l4\l4mfdview.h" // RPWindowLayout_*
|
||||||
|
#include "..\munga_l4\l4joy.h" // RPJoyConfigWizard
|
||||||
#include "rpl4ver.h"
|
#include "rpl4ver.h"
|
||||||
|
#include "rpl4build.h" // generated: RP412_VERSION / RP412_VERSION_LONG
|
||||||
#include "..\munga\resver.h"
|
#include "..\munga\resver.h"
|
||||||
#include "..\munga\resource.h"
|
#include "..\munga\resource.h"
|
||||||
// added for game status drawing support
|
// added for game status drawing support
|
||||||
@@ -36,8 +40,11 @@
|
|||||||
// end add
|
// end add
|
||||||
#include <DbgHelp.h>
|
#include <DbgHelp.h>
|
||||||
#include <strsafe.h>
|
#include <strsafe.h>
|
||||||
|
#include <fstream> // the log is rolled before it is opened
|
||||||
|
#include <ctime> // and stamped with the wall clock
|
||||||
#include <direct.h>
|
#include <direct.h>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
|
#include <time.h> // the test-build expiry check
|
||||||
|
|
||||||
#define SPOOL_SIZE 0x600000
|
#define SPOOL_SIZE 0x600000
|
||||||
|
|
||||||
@@ -76,6 +83,49 @@ HWND hWnd;
|
|||||||
//
|
//
|
||||||
static LONG WINAPI RPL4CrashDumpFilter(EXCEPTION_POINTERS *exception)
|
static LONG WINAPI RPL4CrashDumpFilter(EXCEPTION_POINTERS *exception)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Wine raises 0x80000100 (EXCEPTION_WINE_STUB) when the game calls an
|
||||||
|
// API Wine declares but has never implemented, and puts the module and
|
||||||
|
// function names in the first two exception parameters as plain ANSI
|
||||||
|
// strings. Wine prints that to its own stderr - which a player running
|
||||||
|
// through Steam never sees - and then this filter caught the exception
|
||||||
|
// and wrote a minidump, so a perfectly self-describing failure arrived
|
||||||
|
// as a mystery crash. That is exactly how D3DXConcatenateMeshes cost a
|
||||||
|
// full investigation on 4.12.233: one stubbed function out of the
|
||||||
|
// twenty d3dx9 imports, and nothing in our log named it.
|
||||||
|
//
|
||||||
|
// Unlike an access violation this is an orderly raise from Wine with
|
||||||
|
// the heap intact, so the log stream is safe to use here. Written
|
||||||
|
// before the dump, in case the dump is what fails.
|
||||||
|
//
|
||||||
|
if (exception != NULL &&
|
||||||
|
exception->ExceptionRecord != NULL &&
|
||||||
|
exception->ExceptionRecord->ExceptionCode == 0x80000100 &&
|
||||||
|
exception->ExceptionRecord->NumberParameters >= 2)
|
||||||
|
{
|
||||||
|
const char *stub_module =
|
||||||
|
(const char *) exception->ExceptionRecord->ExceptionInformation[0];
|
||||||
|
const char *stub_function =
|
||||||
|
(const char *) exception->ExceptionRecord->ExceptionInformation[1];
|
||||||
|
|
||||||
|
if (stub_module != NULL && stub_function != NULL &&
|
||||||
|
!IsBadStringPtrA(stub_module, 128) &&
|
||||||
|
!IsBadStringPtrA(stub_function, 128))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "\n*** WINE STUB: " << stub_module << "."
|
||||||
|
<< stub_function << " is not implemented by this Wine.\n"
|
||||||
|
<< "*** The game called an API Wine only declares. Shipping the"
|
||||||
|
<< " real DLL beside the exe overrides Wine's builtin and is"
|
||||||
|
<< " usually the whole fix; see docs/RP412-LINUX.md.\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "\n*** WINE STUB: an unimplemented API was called"
|
||||||
|
<< " (names unreadable)\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HMODULE dbghelp = LoadLibraryA("dbghelp.dll");
|
HMODULE dbghelp = LoadLibraryA("dbghelp.dll");
|
||||||
if (dbghelp != NULL)
|
if (dbghelp != NULL)
|
||||||
{
|
{
|
||||||
@@ -123,6 +173,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
//
|
||||||
|
// A drag or resize just finished. Here rather than in the
|
||||||
|
// cockpit's subclass so it also covers the console screen, which
|
||||||
|
// is where the window gets moved before there is any cockpit to
|
||||||
|
// subclass. No-op unless RP412MFDLAYOUT is save.
|
||||||
|
//
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
@@ -132,12 +191,111 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
char filename[50];
|
char filename[50];
|
||||||
strcpy_s(filename, 50, "rpl4.log");
|
strcpy_s(filename, 50, "rpl4.log");
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// The log keeps the whole night, not just the last race.
|
||||||
|
//
|
||||||
|
// Every launch used to truncate it, so a tester who played six races
|
||||||
|
// and hit trouble in the second had nothing to send: the evidence was
|
||||||
|
// overwritten by the races that followed. Collecting it meant copying
|
||||||
|
// the file aside between games, which nobody remembers to do while
|
||||||
|
// everyone is waiting to start.
|
||||||
|
//
|
||||||
|
// Appending costs a separator, below, so the runs can still be told
|
||||||
|
// apart. RP412LOGAPPEND=0 restores the old behaviour.
|
||||||
|
//
|
||||||
|
// Rolled once when it gets big rather than left to grow: a session
|
||||||
|
// with the per-frame diagnostics turned on writes fast, and a log
|
||||||
|
// nobody can open is no more use than one that was overwritten. The
|
||||||
|
// previous generation is kept beside it.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
const char *append_setting = getenv("RP412LOGAPPEND");
|
||||||
|
bool append_log = !(append_setting != NULL && append_setting[0] == '0');
|
||||||
|
|
||||||
|
if (append_log)
|
||||||
|
{
|
||||||
|
const long kMaximumLogBytes = 64L * 1024L * 1024L;
|
||||||
|
|
||||||
|
std::ifstream probe(filename, std::ios::binary | std::ios::ate);
|
||||||
|
if (probe.is_open())
|
||||||
|
{
|
||||||
|
std::streamoff grown = probe.tellg();
|
||||||
|
probe.close();
|
||||||
|
if (grown > kMaximumLogBytes)
|
||||||
|
{
|
||||||
|
remove("rpl4.prev.log");
|
||||||
|
rename(filename, "rpl4.prev.log");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::ofstream logfile;
|
std::ofstream logfile;
|
||||||
logfile.open(filename);
|
logfile.open(filename, append_log ? (std::ios::out | std::ios::app)
|
||||||
|
: std::ios::out);
|
||||||
std::cout.rdbuf(logfile.rdbuf());
|
std::cout.rdbuf(logfile.rdbuf());
|
||||||
|
|
||||||
|
//
|
||||||
|
// Where one run stops and the next starts. Wall-clock time, because
|
||||||
|
// that is what a tester reports trouble against ("the third race,
|
||||||
|
// about nine") and nothing else in the log carries it.
|
||||||
|
//
|
||||||
|
if (append_log)
|
||||||
|
{
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm local;
|
||||||
|
char stamp[32];
|
||||||
|
|
||||||
|
if (localtime_s(&local, &now) == 0 &&
|
||||||
|
strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", &local) > 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "\n"
|
||||||
|
<< "========================================================\n"
|
||||||
|
<< " run started " << stamp << "\n"
|
||||||
|
<< "========================================================\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SetUnhandledExceptionFilter(RPL4CrashDumpFilter);
|
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;
|
||||||
|
|
||||||
|
//
|
||||||
|
// And whether this is Windows at all.
|
||||||
|
//
|
||||||
|
// Wine exports wine_get_version from ntdll and Windows does not, so
|
||||||
|
// one GetProcAddress separates the two. It matters because the
|
||||||
|
// failure modes differ: Wine implements Direct3D and the D3DX helpers
|
||||||
|
// itself, and where a helper is only a stub the game dies at the call
|
||||||
|
// with no hint in its own log (see the filter above). A tester's log
|
||||||
|
// saying which platform it came from turns that from an investigation
|
||||||
|
// into a lookup. Never true on Windows, where the lookup fails and
|
||||||
|
// nothing is printed.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
|
||||||
|
if (ntdll != NULL)
|
||||||
|
{
|
||||||
|
typedef const char * (__cdecl *WineGetVersionFn)(void);
|
||||||
|
WineGetVersionFn wine_get_version =
|
||||||
|
(WineGetVersionFn) GetProcAddress(ntdll, "wine_get_version");
|
||||||
|
if (wine_get_version != NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "Platform: Wine " << wine_get_version()
|
||||||
|
<< " (not Windows) - D3DX comes from Wine's own"
|
||||||
|
<< " implementation\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load up our environment variables
|
// load up our environment variables
|
||||||
//controls
|
//controls
|
||||||
if(getenv("L4CONTROLS") == NULL)
|
if(getenv("L4CONTROLS") == NULL)
|
||||||
@@ -152,34 +310,138 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
putenv("TARGETFPS=60");
|
putenv("TARGETFPS=60");
|
||||||
if(getenv("MAXPARTICLES") == NULL)
|
if(getenv("MAXPARTICLES") == NULL)
|
||||||
putenv("MAXPARTICLES=8192");
|
putenv("MAXPARTICLES=8192");
|
||||||
FILE *file;
|
//
|
||||||
char line[1024];
|
// environ.ini: written on first run and read here. The exe owns the
|
||||||
if (fopen_s(&file, "environ.ini", "r") == 0)
|
// 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--)
|
DEBUG_STREAM << "Build expired on " << RP412_EXPIRY_TEXT
|
||||||
if (line[i] == '\n' || line[i] == '\r')
|
<< " - refusing to run\n" << std::flush;
|
||||||
line[i] = 0;
|
|
||||||
// the file is self-documenting: skip comments, blanks,
|
char notice[512];
|
||||||
// and anything that is not KEY=VALUE
|
sprintf(notice,
|
||||||
char *setting = line;
|
"This Red Planet test build has expired.\n\n"
|
||||||
while (*setting == ' ' || *setting == '\t')
|
" Build %s\n"
|
||||||
++setting;
|
" Expired %s\n\n"
|
||||||
if (*setting == '\0' || *setting == '#' || *setting == ';' ||
|
"Test builds are good for a fortnight so that nobody spends an "
|
||||||
strchr(setting, '=') == NULL)
|
"afternoon chasing something that was fixed a week ago.\n\n"
|
||||||
continue;
|
"Ask for the current one.",
|
||||||
putenv(setting);
|
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.1" << std::endl << std::flush;
|
|
||||||
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << 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
|
#ifdef RP412_STEAM
|
||||||
//
|
//
|
||||||
// RP412STEAM=1 (environ.ini or a Steam launch) swaps the wire to
|
// RP412STEAM=1 (environ.ini or a Steam launch) swaps the wire to
|
||||||
@@ -210,6 +472,34 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
//
|
//
|
||||||
int argc;
|
int argc;
|
||||||
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||||
|
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// -spoolstats <file.spl>: headless spool analysis (RPL4SPOOLSTATS.cpp).
|
||||||
|
// Runs before any window, renderer, or engine object exists and exits
|
||||||
|
// with the analyzer's code. Output lands on the parent console when
|
||||||
|
// launched from one, or a fresh console otherwise.
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
for (int spool_arg = 1; spool_arg < argc - 1; ++spool_arg)
|
||||||
|
{
|
||||||
|
if (_wcsicmp(argv[spool_arg], L"-spoolstats") == 0)
|
||||||
|
{
|
||||||
|
char spool_path[512];
|
||||||
|
size_t converted = 0;
|
||||||
|
wcstombs_s(&converted, spool_path, sizeof(spool_path),
|
||||||
|
argv[spool_arg + 1], _TRUNCATE);
|
||||||
|
if (!AttachConsole(ATTACH_PARENT_PROCESS))
|
||||||
|
{
|
||||||
|
AllocConsole();
|
||||||
|
}
|
||||||
|
FILE *spool_out = NULL;
|
||||||
|
freopen_s(&spool_out, "CONOUT$", "w", stdout);
|
||||||
|
extern int RPL4SpoolStats_Run(const char *path);
|
||||||
|
return RPL4SpoolStats_Run(spool_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logical run_application = RPL4Application::ParseCommandLine(argc, argv);
|
Logical run_application = RPL4Application::ParseCommandLine(argc, argv);
|
||||||
if (!run_application)
|
if (!run_application)
|
||||||
{
|
{
|
||||||
@@ -246,14 +536,97 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
DWORD wsStyle = WS_OVERLAPPED | WS_SYSMENU;
|
//
|
||||||
if (L4Application::GetFullscreen())
|
// The style the window is BORN with, rather than one worked out and
|
||||||
wsStyle = WS_POPUP;
|
// then thrown away - the old code computed a style and handed
|
||||||
hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_OVERLAPPEDWINDOW, 0, 0, L4Application::GetScreenWidth(), L4Application::GetScreenHeight(), (HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
// CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless.
|
||||||
if (!hWnd)
|
//
|
||||||
return FALSE;
|
// 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);
|
ShowWindow(hWnd, nShowCmd);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Sticky placement, from the first frame the player sees. SVGA16 also
|
||||||
|
// registers and reloads when it builds the cockpit, but that is not
|
||||||
|
// until a mission starts - without this the console screen would come
|
||||||
|
// up at the default rect with its title bar still on, and only jump
|
||||||
|
// to the saved placement once a race began. -fit takes the whole
|
||||||
|
// monitor and has no placement of the player's to restore.
|
||||||
|
//
|
||||||
|
if (!L4Application::GetFitDisplay() && !L4Application::GetFullscreen())
|
||||||
|
{
|
||||||
|
RPWindowLayout_Register(hWnd, "RPL4", True);
|
||||||
|
RPWindowLayout_Load();
|
||||||
|
}
|
||||||
|
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
|
#if !_DEBUG
|
||||||
// Arcade pods have no mouse - but desktop/windowed play needs the
|
// Arcade pods have no mouse - but desktop/windowed play needs the
|
||||||
// cursor for the on-screen cockpit buttons, so hide it only when
|
// cursor for the on-screen cockpit buttons, so hide it only when
|
||||||
@@ -280,6 +653,31 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
|
|
||||||
int last_launch_mode = FELaunchSingle;
|
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 (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
if (front_end_mode)
|
if (front_end_mode)
|
||||||
@@ -403,12 +801,26 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
if (!spoolFileName)
|
if (!spoolFileName)
|
||||||
spoolFileName = "last.spl";
|
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);
|
SpoolFile *spool = spool_mgr->GetStoredSpoolFile(spoolFileName);
|
||||||
if (spool)
|
if (spool)
|
||||||
{
|
{
|
||||||
|
DEBUG_STREAM << "Playback: " << spoolFileName
|
||||||
|
<< " loaded, " << (int) spool->GetBytesRemaining()
|
||||||
|
<< " bytes to play\n" << std::flush;
|
||||||
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool);
|
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool);
|
||||||
} else
|
} 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);
|
new_app = new RPL4IdleApplication(hInstance, hWnd, &resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,6 +887,19 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Last word on the window's placement. Every finished drag has already
|
||||||
|
// written it and SVGA16 writes again as the cockpit comes down, but a
|
||||||
|
// session that never started a race has neither - and quitting from
|
||||||
|
// the console screen is exactly how somebody would leave after moving
|
||||||
|
// the window to where they want it. No-op unless the mode is save.
|
||||||
|
//
|
||||||
|
if (IsWindow(hWnd))
|
||||||
|
{
|
||||||
|
RPWindowLayout_Save();
|
||||||
|
}
|
||||||
|
RPWindowLayout_Forget(hWnd);
|
||||||
|
|
||||||
#if !_DEBUG
|
#if !_DEBUG
|
||||||
// symmetric with the fullscreen-only hide at startup
|
// symmetric with the fullscreen-only hide at startup
|
||||||
if (L4Application::GetFullscreen())
|
if (L4Application::GetFullscreen())
|
||||||
|
|||||||
@@ -11,16 +11,61 @@
|
|||||||
#include "..\munga_l4\l4ctrl.h"
|
#include "..\munga_l4\l4ctrl.h"
|
||||||
#include "..\munga_l4\l4mppr.h"
|
#include "..\munga_l4\l4mppr.h"
|
||||||
#include "..\munga\appmgr.h"
|
#include "..\munga\appmgr.h"
|
||||||
|
#include "..\munga\spooler.h"
|
||||||
#include "rpl4mode.h"
|
#include "rpl4mode.h"
|
||||||
#include "..\rp\vtv.h"
|
#include "..\rp\vtv.h"
|
||||||
#include "rpl4mppr.h"
|
#include "rpl4mppr.h"
|
||||||
#include "..\rp\rpplayer.h"
|
#include "..\rp\rpplayer.h"
|
||||||
|
#include "..\munga_l4\l4video.h"
|
||||||
|
#include "..\munga\dropzone.h"
|
||||||
|
#include "..\munga\director.h"
|
||||||
|
#include "..\munga\nttmgr.h"
|
||||||
|
#include "..\munga_l4\l4vb16.h"
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// RPL4Application
|
// RPL4Application
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
|
const Receiver::HandlerEntry
|
||||||
|
RPL4Application::MessageHandlerEntries[]=
|
||||||
|
{
|
||||||
|
MESSAGE_ENTRY(RPL4Application, StopMission),
|
||||||
|
MESSAGE_ENTRY(RPL4Application, WinnersCircle)
|
||||||
|
};
|
||||||
|
|
||||||
|
Receiver::MessageHandlerSet& RPL4Application::GetMessageHandlers()
|
||||||
|
{
|
||||||
|
static Receiver::MessageHandlerSet messageHandlers(
|
||||||
|
ELEMENTS(RPL4Application::MessageHandlerEntries),
|
||||||
|
RPL4Application::MessageHandlerEntries,
|
||||||
|
L4Application::GetMessageHandlers());
|
||||||
|
return messageHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Derivation* RPL4Application::GetClassDerivations()
|
||||||
|
{
|
||||||
|
static Derivation classDerivations(
|
||||||
|
L4Application::GetClassDerivations(), "RPL4Application");
|
||||||
|
return &classDerivations;
|
||||||
|
}
|
||||||
|
|
||||||
|
RPL4Application::SharedData
|
||||||
|
RPL4Application::DefaultData(
|
||||||
|
RPL4Application::GetClassDerivations(),
|
||||||
|
RPL4Application::GetMessageHandlers()
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// How long to sit on black between the race fading out and the podium
|
||||||
|
// fading in. The race fade is FADE_OUT_TIME (half a second), so this is
|
||||||
|
// that plus a beat, to land on black rather than on the tail of it.
|
||||||
|
//
|
||||||
|
const Scalar winnersCircleFadeOutTime = 0.7f;
|
||||||
|
|
||||||
|
// and how quickly the stand comes up out of the black afterwards
|
||||||
|
const Scalar winnersCircleFadeInTime = 0.45f;
|
||||||
|
|
||||||
RPL4Application::RPL4Application(
|
RPL4Application::RPL4Application(
|
||||||
HINSTANCE hInstance,
|
HINSTANCE hInstance,
|
||||||
HWND hWnd,
|
HWND hWnd,
|
||||||
@@ -43,6 +88,442 @@ RPL4Application::~RPL4Application()
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// ShowWinnersCircle
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The pod hall's award platform. Every map carries a "wcircle" stand at
|
||||||
|
// (1200,0,0) with eight ranked dropzones named win1..win8 on it - rank 1 at
|
||||||
|
// the front on the low tier, ranks 4-8 across the back on the high one. All
|
||||||
|
// this shipped; only the mission-review build ever drove it.
|
||||||
|
//
|
||||||
|
// Stand the finishers on their spots in finishing order, freeze them, and
|
||||||
|
// look back at the stand from in front of it.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPL4Application::ShowWinnersCircle()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
// RP412PODIUM=0 skips the whole thing
|
||||||
|
const char *podium_mode = getenv("RP412PODIUM");
|
||||||
|
if (podium_mode != NULL && atoi(podium_mode) == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: disabled\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityManager *entity_manager = GetEntityManager();
|
||||||
|
if (entity_manager == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
||||||
|
if (dropzones == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: no DropZones group\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Walk the finishing order. Player::CalcRanking has been ranking every
|
||||||
|
// scoring player by score each frame, in football as much as in a race
|
||||||
|
// (CalcFootballRanking exists but is never called), so rank order is
|
||||||
|
// meaningful in both.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// The stand's own geometry first, before anybody stands on it.
|
||||||
|
//
|
||||||
|
// The eight spots are fixed furniture on every map, so reading them up
|
||||||
|
// front gives one set of numbers to both jobs below: where to stand
|
||||||
|
// the finishers, and where to put the camera. Only the first is
|
||||||
|
// allowed to care who finished.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
enum { winnersSpotCount = 8 };
|
||||||
|
DropZone *spot[winnersSpotCount];
|
||||||
|
int spots_found = 0;
|
||||||
|
|
||||||
|
char winners_spot[] = "win?";
|
||||||
|
char *place = winners_spot + 3;
|
||||||
|
|
||||||
|
for (int i = 0; i < winnersSpotCount; ++i)
|
||||||
|
{
|
||||||
|
*place = (char) ('1' + i);
|
||||||
|
spot[i] = NULL;
|
||||||
|
|
||||||
|
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
||||||
|
DropZone *dropzone;
|
||||||
|
while ((dropzone = (DropZone*) iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
if (!strcmp(dropzone->GetDropZoneName(), winners_spot))
|
||||||
|
{
|
||||||
|
spot[i] = dropzone;
|
||||||
|
++spots_found;
|
||||||
|
Point3D at = dropzone->localOrigin.linearPosition;
|
||||||
|
DEBUG_STREAM << "WinnersCircle: " << winners_spot << " at "
|
||||||
|
<< at.x << "," << at.y << "," << at.z << "\n" << std::flush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spot[0] == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: no win1 spot on this map\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Stand the finishers up, in finishing order.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
int placed = 0;
|
||||||
|
Player *p;
|
||||||
|
for (int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; ++rank)
|
||||||
|
{
|
||||||
|
if (rank >= winnersSpotCount)
|
||||||
|
{
|
||||||
|
break; // only eight spots exist on the stand
|
||||||
|
}
|
||||||
|
if (spot[rank] == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity *vehicle = p->GetPlayerVehicle();
|
||||||
|
if (vehicle == NULL || vehicle->GetClassID() != VTVClassID)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VTV *vtv = (VTV*) vehicle;
|
||||||
|
vtv->Reset(spot[rank]->localOrigin, VTV::MissionReviewReset);
|
||||||
|
vtv->SetPerformance(&VTV::DoNothing);
|
||||||
|
vtv->FlushEvents();
|
||||||
|
++placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placed == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: nobody to place\n" << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Switch the world back on.
|
||||||
|
//
|
||||||
|
// Dying collapses the view and sets the application "dead" until the
|
||||||
|
// pilot reincarnates - while that flag is up the renderer skips every
|
||||||
|
// static object, which is the whole map. A pilot killed near the buzzer
|
||||||
|
// is still waiting for a respawn that will never come, so the flag is
|
||||||
|
// still up and the podium would play out against a black screen.
|
||||||
|
//
|
||||||
|
// The mission-review build never hit this: it watches from a CameraShip,
|
||||||
|
// which cannot die, so nothing ever set the flag there.
|
||||||
|
//
|
||||||
|
// Standing the finishers up IS the resurrection, so clear it.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
SetIsDead(false);
|
||||||
|
|
||||||
|
DPLRenderer *dpl_renderer = GetVideoRenderer();
|
||||||
|
if (dpl_renderer == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Your own vehicle is built as a cockpit with no hull, so from any
|
||||||
|
// camera outside it there is nothing where you should be - on the stand
|
||||||
|
// you would be the one empty spot. Turn it inside out before the shot.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Bracket the exterior build. The podium crash kills the pod at teardown
|
||||||
|
// with a segment plug whose vtable has been overwritten; these two calls
|
||||||
|
// say whether it was ALREADY wrong when the podium began, or whether this
|
||||||
|
// second MakeEntityRenderables on the own pod is what did it. The answer
|
||||||
|
// rides in the teardown's rpl4-fail.log report - see RP412SEGCHECK in
|
||||||
|
// MUNGA\JMOVER.cpp. Costs a handful of pointer compares, twice, once a
|
||||||
|
// race.
|
||||||
|
//
|
||||||
|
RPCheckJointedMoverSegments(GetViewpointEntity(), "podium: before exterior");
|
||||||
|
|
||||||
|
dpl_renderer->ShowViewpointFromOutside();
|
||||||
|
|
||||||
|
RPCheckJointedMoverSegments(GetViewpointEntity(), "podium: after exterior");
|
||||||
|
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// The name plates are drawn per rank slot, so they have to be re-sorted
|
||||||
|
// now that the finishing order is final - otherwise the signs read in
|
||||||
|
// whatever order the players were created.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
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
|
||||||
|
// line-up frames up. The stand runs from z~3 (rank 1, low) to z~39
|
||||||
|
// (ranks 4-8, high) and x~1180..1219; at 45 degrees that needs roughly
|
||||||
|
// 30 units of standoff. Eye is above the top tier looking slightly
|
||||||
|
// down at the winner's spot.
|
||||||
|
//
|
||||||
|
// Framed off the stand, NOT off who is standing on it. This used to
|
||||||
|
// average the filled spots and anchor on the first one filled, which
|
||||||
|
// meant the camera moved with the head count: eight finishers put the
|
||||||
|
// anchor in the middle of the tiers and the eye about 58 units out,
|
||||||
|
// one finisher collapsed it onto the winner's spot and brought the eye
|
||||||
|
// in to 36. Same stand, different shot per race - and per machine, if
|
||||||
|
// a remote player's vehicle was not there to be placed. The furniture
|
||||||
|
// does not move, so the camera does not either.
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
Point3D standFront = spot[0]->localOrigin.linearPosition;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The stand's own axis: back rows to the winner's spot, which points
|
||||||
|
// out of the front however the map has the stand turned. Taken from
|
||||||
|
// the geometry rather than assumed, so a map that mounts its stand at
|
||||||
|
// another angle still gets photographed from the front.
|
||||||
|
//
|
||||||
|
Point3D standBack(0.0f, 0.0f, 0.0f);
|
||||||
|
int back_count = 0;
|
||||||
|
for (int i = 1; i < winnersSpotCount; ++i)
|
||||||
|
{
|
||||||
|
if (spot[i] != NULL)
|
||||||
|
{
|
||||||
|
standBack += spot[i]->localOrigin.linearPosition;
|
||||||
|
++back_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (back_count > 0)
|
||||||
|
{
|
||||||
|
standBack.x /= (Scalar) back_count;
|
||||||
|
standBack.y /= (Scalar) back_count;
|
||||||
|
standBack.z /= (Scalar) back_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3D facing;
|
||||||
|
facing.x = standFront.x - standBack.x;
|
||||||
|
facing.y = 0.0f;
|
||||||
|
facing.z = standFront.z - standBack.z;
|
||||||
|
Scalar reach = (Scalar) sqrt(facing.x * facing.x + facing.z * facing.z);
|
||||||
|
if (reach < 0.01f)
|
||||||
|
{
|
||||||
|
// a one-spot stand, or all eight stacked: nothing to take a
|
||||||
|
// direction from, so fall back to the way the shipped stands face
|
||||||
|
facing.x = 0.0f; facing.z = -1.0f; reach = 1.0f;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Framing is tunable while the shot is being dialled in:
|
||||||
|
// RP412PODIUMSTANDOFF distance out in front of the stand
|
||||||
|
// RP412PODIUMHEIGHT eye height above the group
|
||||||
|
// RP412PODIUMAIM height of the aim point above the group
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Framed off the stand itself: down low and tilted up across the tiers,
|
||||||
|
// which is how you photograph a podium. It is a balance in both
|
||||||
|
// directions - drop the camera further or tilt harder and the sky takes
|
||||||
|
// the top half while the winner's spot slides off the bottom; tilt down
|
||||||
|
// instead and it becomes a floor plan. Pillarboxing to 4:3 is what lets
|
||||||
|
// it sit this close without the platform trailing off at the sides.
|
||||||
|
//
|
||||||
|
Scalar standoff = 36.0f;
|
||||||
|
Scalar height = 12.0f;
|
||||||
|
Scalar aim_lift = 2.0f;
|
||||||
|
const char *tune = getenv("RP412PODIUMSTANDOFF");
|
||||||
|
if (tune != NULL && atof(tune) != 0.0) standoff = (Scalar) atof(tune);
|
||||||
|
tune = getenv("RP412PODIUMHEIGHT");
|
||||||
|
if (tune != NULL && atof(tune) != 0.0) height = (Scalar) atof(tune);
|
||||||
|
tune = getenv("RP412PODIUMAIM");
|
||||||
|
if (tune != NULL) aim_lift = (Scalar) atof(tune);
|
||||||
|
|
||||||
|
Point3D eye;
|
||||||
|
eye.x = standFront.x + (facing.x / reach) * standoff;
|
||||||
|
eye.y = standFront.y + height;
|
||||||
|
eye.z = standFront.z + (facing.z / reach) * standoff;
|
||||||
|
|
||||||
|
Point3D aim = standFront;
|
||||||
|
aim.y += aim_lift;
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412PODIUMCAM=0 leaves the view in the cockpit, which is also the
|
||||||
|
// way to tell a bad camera from a scene that is not drawing at all.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Clear the cockpit glass away. The MFDs and the radar sit over the
|
||||||
|
// viewscreen like the pod's bezels and have nothing to say once the
|
||||||
|
// race is over; the podium gets the whole canvas.
|
||||||
|
//
|
||||||
|
SVGA16 *cockpit = SVGA16::GetCockpit();
|
||||||
|
if (cockpit != NULL)
|
||||||
|
{
|
||||||
|
cockpit->HideSecondaryDisplays();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Fog first: it pulls the clip plane in to 1100, and SetViewAngle is what
|
||||||
|
// rebuilds the projection that reads it.
|
||||||
|
//
|
||||||
|
dpl_renderer->SetFogStyle(DPLRenderer::winnersCircleFogStyle);
|
||||||
|
|
||||||
|
const char *camera_mode = getenv("RP412PODIUMCAM");
|
||||||
|
if (camera_mode == NULL || atoi(camera_mode) != 0)
|
||||||
|
{
|
||||||
|
dpl_renderer->SetViewAngle(Degree(45.0f));
|
||||||
|
dpl_renderer->SetPresentationCamera(eye, aim);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// still rebuild the projection so the new clip plane takes effect
|
||||||
|
dpl_renderer->SetViewAngle(Degree(40.0f));
|
||||||
|
}
|
||||||
|
if (camera_mode != NULL && atoi(camera_mode) == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "WinnersCircle: presentation camera disabled\n"
|
||||||
|
<< std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Everything is in place behind the black - bring it up.
|
||||||
|
// RP412PODIUMFADEIN sets the ramp in seconds.
|
||||||
|
//
|
||||||
|
Scalar fade_in = winnersCircleFadeInTime;
|
||||||
|
const char *fade_tune = getenv("RP412PODIUMFADEIN");
|
||||||
|
if (fade_tune != NULL && atof(fade_tune) > 0.0)
|
||||||
|
{
|
||||||
|
fade_in = (Scalar) atof(fade_tune);
|
||||||
|
}
|
||||||
|
dpl_renderer->StartPresentationFadeIn(fade_in);
|
||||||
|
|
||||||
|
//
|
||||||
|
// placed is reported for diagnosis only - the camera numbers beside it
|
||||||
|
// must not move when it does.
|
||||||
|
//
|
||||||
|
DEBUG_STREAM << "WinnersCircle: " << placed << " placed on "
|
||||||
|
<< spots_found << " spots; aim "
|
||||||
|
<< aim.x << "," << aim.y << "," << aim.z
|
||||||
|
<< " eye " << eye.x << "," << eye.y << "," << eye.z
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// StopMissionMessageHandler
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The mission is over. Show the podium, then let the base handler run - it
|
||||||
|
// puts the player into MissionEndingState, whose fade timer is what actually
|
||||||
|
// keeps the sim and the renderer alive until teardown.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPL4Application::StopMissionMessageHandler(StopMissionMessage *message)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
//
|
||||||
|
// StopMission arrives twice: once from the console at the buzzer, and
|
||||||
|
// again from the player when the ending fade runs out - that second one
|
||||||
|
// is what actually retires the application. Only the first is the end of
|
||||||
|
// the race.
|
||||||
|
//
|
||||||
|
if (GetApplicationState() != Application::EndingMission)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
//-------------------------------------------------------------
|
||||||
|
// Don't cut straight to the podium. The race gets its own
|
||||||
|
// fade-to-black first - that fade is already running by the time
|
||||||
|
// this returns - and the Winners Circle comes up out of the
|
||||||
|
// black afterwards. Standing everyone up now would just fade out
|
||||||
|
// the podium instead of the race.
|
||||||
|
//
|
||||||
|
// FADE_OUT_TIME is half a second; a beat more than that lands on
|
||||||
|
// black rather than on the tail of the fade.
|
||||||
|
//-------------------------------------------------------------
|
||||||
|
//
|
||||||
|
Receiver::Message podium_message(
|
||||||
|
WinnersCircleMessageID, sizeof(Receiver::Message));
|
||||||
|
|
||||||
|
Time event_time;
|
||||||
|
event_time = Now();
|
||||||
|
event_time += winnersCircleFadeOutTime;
|
||||||
|
|
||||||
|
Post(LowEventPriority, this, &podium_message, event_time);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "WinnersCircle: race over, fading out\n" << std::flush;
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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);
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// WinnersCircleMessageHandler
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The race has faded to black. Set the stand up behind the black and fade
|
||||||
|
// back in to it.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPL4Application::WinnersCircleMessageHandler(Receiver::Message *)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
DEBUG_STREAM << "WinnersCircle: standing the finishers up\n" << std::flush;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The earliest of the three checks: anything corrupt HERE was broken by
|
||||||
|
// the race itself, before the podium touched anything.
|
||||||
|
//
|
||||||
|
RPCheckJointedMoverSegments(GetViewpointEntity(), "podium: entry (race just ended)");
|
||||||
|
|
||||||
|
ShowWinnersCircle();
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// MakeRegistry
|
// MakeRegistry
|
||||||
|
|||||||
@@ -26,4 +26,38 @@ private:
|
|||||||
Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources);
|
Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources);
|
||||||
|
|
||||||
Entity* MakeViewpointEntity(Entity__MakeMessage *);
|
Entity* MakeViewpointEntity(Entity__MakeMessage *);
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// The Winners Circle.
|
||||||
|
//
|
||||||
|
// The podium is engine content that only the mission-review build ever
|
||||||
|
// ran: geometry, the eight ranked dropzones and the presentation code all
|
||||||
|
// ship, but the sequence lived on RPL4PlaybackApplication behind a spool
|
||||||
|
// file. This is the same sequence on the path the pods actually race.
|
||||||
|
//
|
||||||
|
public:
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
// posted at the buzzer, fires once the race has faded out
|
||||||
|
WinnersCircleMessageID = L4Application::NextMessageID,
|
||||||
|
NextMessageID
|
||||||
|
};
|
||||||
|
|
||||||
|
static const HandlerEntry
|
||||||
|
MessageHandlerEntries[];
|
||||||
|
static MessageHandlerSet& GetMessageHandlers();
|
||||||
|
static Derivation *GetClassDerivations();
|
||||||
|
static SharedData DefaultData;
|
||||||
|
|
||||||
|
void
|
||||||
|
StopMissionMessageHandler(StopMissionMessage *message);
|
||||||
|
|
||||||
|
// the podium proper, once the screen is already black
|
||||||
|
void
|
||||||
|
WinnersCircleMessageHandler(Receiver::Message *message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// stand the finishers on their ranked spots and frame the shot
|
||||||
|
void
|
||||||
|
ShowWinnersCircle();
|
||||||
};
|
};
|
||||||
|
|||||||
+555
-11
@@ -6,6 +6,7 @@
|
|||||||
#include "..\munga\appmgr.h"
|
#include "..\munga\appmgr.h"
|
||||||
#include "..\munga\appmsg.h"
|
#include "..\munga\appmsg.h"
|
||||||
#include "..\munga\console.h"
|
#include "..\munga\console.h"
|
||||||
|
#include "..\munga\spooler.h"
|
||||||
#include "..\rp\rpcnsl.h"
|
#include "..\rp\rpcnsl.h"
|
||||||
#include "..\munga_l4\l4app.h"
|
#include "..\munga_l4\l4app.h"
|
||||||
#include "..\munga_l4\l4net.h"
|
#include "..\munga_l4\l4net.h"
|
||||||
@@ -80,6 +81,9 @@ namespace
|
|||||||
NetTransport::Connection
|
NetTransport::Connection
|
||||||
connection;
|
connection;
|
||||||
int state; // last reported application state (-1 unknown)
|
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;
|
Logical eggAcknowledged;
|
||||||
DWORD lastQueryTick;
|
DWORD lastQueryTick;
|
||||||
DWORD eggSentTick; // 0 = never sent
|
DWORD eggSentTick; // 0 = never sent
|
||||||
@@ -164,6 +168,46 @@ namespace
|
|||||||
pod->connection, packet, (int) sizeof(NetworkPacketHeader) + size);
|
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)
|
void SendEggTo(RemotePod *pod)
|
||||||
{
|
{
|
||||||
int chunk_count = (gEggWireSize + 999) / 1000;
|
int chunk_count = (gEggWireSize + 999) / 1000;
|
||||||
@@ -259,7 +303,27 @@ namespace
|
|||||||
(int) message->GetFinalScore());
|
(int) message->GetFinalScore());
|
||||||
pod->scored = True;
|
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)
|
else if ((int) header->clientID == (int) NetworkClient::NetworkManagerClientID)
|
||||||
{
|
{
|
||||||
@@ -348,10 +412,321 @@ namespace
|
|||||||
DEBUG_STREAM << "LocalConsole: stopping local pod\n" << std::flush;
|
DEBUG_STREAM << "LocalConsole: stopping local pod\n" << std::flush;
|
||||||
InterlockedExchange(&gMissionRunning, 0);
|
InterlockedExchange(&gMissionRunning, 0);
|
||||||
Application::StopMissionMessage message(0);
|
Application::StopMissionMessage message(0);
|
||||||
application->Dispatch(&message);
|
DeliverLocal(
|
||||||
|
NetworkClient::ApplicationClientID,
|
||||||
|
&message,
|
||||||
|
(int) message.messageLength
|
||||||
|
);
|
||||||
gPhase = PhaseStopped;
|
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 of the game window, centred - over the upper centre MFD
|
||||||
|
// rather than the upper right one it used to sit on. The host is
|
||||||
|
// reading the roster and then reaching for LAUNCH, and the middle
|
||||||
|
// of the panel is where the eye already is; the right-hand corner
|
||||||
|
// put it out at the edge of vision, furthest from everything else
|
||||||
|
// the host is doing.
|
||||||
|
//
|
||||||
|
RECT game;
|
||||||
|
GetWindowRect(ghWnd, &game);
|
||||||
|
|
||||||
|
int board_x = game.left + ((game.right - game.left) - width) / 2;
|
||||||
|
if (board_x < game.left)
|
||||||
|
{
|
||||||
|
board_x = game.left; // narrower window than the board
|
||||||
|
}
|
||||||
|
|
||||||
|
gStatusWindow = CreateWindowExA(
|
||||||
|
WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
||||||
|
"RPCommitBoard", "", WS_POPUP,
|
||||||
|
board_x, 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
|
// The game-thread tick: state reporting + engine-safe execution
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
@@ -371,6 +746,32 @@ namespace
|
|||||||
switch (gPhase)
|
switch (gPhase)
|
||||||
{
|
{
|
||||||
case PhaseWaiting:
|
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)
|
if (gNetworkRace)
|
||||||
{
|
{
|
||||||
MarshalRemotes();
|
MarshalRemotes();
|
||||||
@@ -392,28 +793,118 @@ 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 &&
|
if (!gRunSent &&
|
||||||
state == Application::WaitingForLaunch &&
|
state == Application::WaitingForLaunch &&
|
||||||
AllRemotesInState(Application::WaitingForLaunch))
|
AllRemotesInState(Application::WaitingForLaunch))
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "LocalConsole: all pods staged - RUN\n" << std::flush;
|
if (!gLaunchArmed)
|
||||||
for (int i = 0; i < gRemotePodCount; ++i)
|
|
||||||
{
|
{
|
||||||
Application::RunMissionMessage run;
|
gLaunchArmed = True;
|
||||||
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
|
DEBUG_STREAM << "LocalConsole: all pods staged - "
|
||||||
&run, (int) run.messageLength);
|
<< "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;
|
||||||
|
//
|
||||||
|
// Per-pod send tick (RP412NETLOG / plan M4):
|
||||||
|
// with each pod's own "mission t0 at tick"
|
||||||
|
// line and the netclock offsets, these let
|
||||||
|
// the mission-clock skew between machines be
|
||||||
|
// computed offline. Expected to show the
|
||||||
|
// serialization cost here is microseconds
|
||||||
|
// and the real skew is path spread.
|
||||||
|
//
|
||||||
|
DEBUG_STREAM << "NetLog: RunMission to "
|
||||||
|
<< gRemotePods[i].address << " at tick "
|
||||||
|
<< Now().ticks << "\n" << std::flush;
|
||||||
|
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
|
||||||
|
&run, (int) run.messageLength);
|
||||||
|
}
|
||||||
|
Application::RunMissionMessage local_run;
|
||||||
|
DEBUG_STREAM << "NetLog: RunMission local at tick "
|
||||||
|
<< Now().ticks << "\n" << std::flush;
|
||||||
|
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)
|
if (state == Application::RunningMission)
|
||||||
{
|
{
|
||||||
gPhase = PhaseRunning;
|
gPhase = PhaseRunning;
|
||||||
|
DestroyStatusBoard(); // however the run began
|
||||||
gWatchedApp = application;
|
gWatchedApp = application;
|
||||||
gResultCount = 0;
|
gResultCount = 0;
|
||||||
InterlockedExchange(&gRunStartTick, (LONG) GetTickCount());
|
InterlockedExchange(&gRunStartTick, (LONG) GetTickCount());
|
||||||
@@ -439,6 +930,7 @@ namespace
|
|||||||
// mission ended some other way (pilot exit etc.)
|
// mission ended some other way (pilot exit etc.)
|
||||||
InterlockedExchange(&gMissionRunning, 0);
|
InterlockedExchange(&gMissionRunning, 0);
|
||||||
gPhase = PhaseStopped;
|
gPhase = PhaseStopped;
|
||||||
|
DestroyStatusBoard();
|
||||||
DisconnectRemotes();
|
DisconnectRemotes();
|
||||||
}
|
}
|
||||||
else if (gStopRequested)
|
else if (gStopRequested)
|
||||||
@@ -469,6 +961,27 @@ namespace
|
|||||||
else if (AllRemotesScored() ||
|
else if (AllRemotesScored() ||
|
||||||
(LONG)(GetTickCount() - gRemoteStopTick) >= 5000)
|
(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();
|
DispatchLocalStop();
|
||||||
DisconnectRemotes();
|
DisconnectRemotes();
|
||||||
}
|
}
|
||||||
@@ -498,6 +1011,19 @@ namespace
|
|||||||
<< mission_seconds << "s\n" << std::flush;
|
<< mission_seconds << "s\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412MISSIONSECONDS overrides the menu's game length. The shortest
|
||||||
|
// the menu offers is 3:00, which is a long wait when what you are
|
||||||
|
// testing is what happens at the buzzer.
|
||||||
|
//
|
||||||
|
const char *seconds_override = getenv("RP412MISSIONSECONDS");
|
||||||
|
if (seconds_override != NULL && atoi(seconds_override) > 0)
|
||||||
|
{
|
||||||
|
mission_seconds = atoi(seconds_override);
|
||||||
|
DEBUG_STREAM << "LocalConsole: length overridden to "
|
||||||
|
<< mission_seconds << "s by RP412MISSIONSECONDS\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
gMissionSeconds = mission_seconds;
|
gMissionSeconds = mission_seconds;
|
||||||
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
|
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
|
||||||
gPhase = PhaseWaiting;
|
gPhase = PhaseWaiting;
|
||||||
@@ -505,10 +1031,15 @@ namespace
|
|||||||
gRunSent = False;
|
gRunSent = False;
|
||||||
gRemoteStopsSent = False;
|
gRemoteStopsSent = False;
|
||||||
gLocalEggFed = False;
|
gLocalEggFed = False;
|
||||||
|
DestroyStatusBoard(); // no board survives into a new race
|
||||||
|
|
||||||
// game-thread execution point
|
// game-thread execution point
|
||||||
gPerFrameHook = &ConsoleTick;
|
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
|
// results intake from the RP layer
|
||||||
gConsoleScoreSink = &CollectFinalScore;
|
gConsoleScoreSink = &CollectFinalScore;
|
||||||
|
|
||||||
@@ -555,6 +1086,13 @@ Logical
|
|||||||
strncpy(gEggPath, egg_path, sizeof(gEggPath) - 1);
|
strncpy(gEggPath, egg_path, sizeof(gEggPath) - 1);
|
||||||
gEggPath[sizeof(gEggPath) - 1] = '\0';
|
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
|
// The wire image of the egg: file newlines become NULs, exactly
|
||||||
// what the arcade console sent (RPMission.ToEggFileMessages)
|
// what the arcade console sent (RPMission.ToEggFileMessages)
|
||||||
@@ -621,6 +1159,12 @@ Logical
|
|||||||
// booting; runs before the engine block so nothing is waiting.
|
// booting; runs before the engine block so nothing is waiting.
|
||||||
//
|
//
|
||||||
NetTransport_Get()->Startup();
|
NetTransport_Get()->Startup();
|
||||||
|
|
||||||
|
//
|
||||||
|
// An escape pressed during the last race must not cancel this one.
|
||||||
|
//
|
||||||
|
NetTransport_ClearWaitCancel();
|
||||||
|
|
||||||
const char *cursor = remote_pod_list;
|
const char *cursor = remote_pod_list;
|
||||||
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
|
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,939 @@
|
|||||||
|
#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"
|
||||||
|
"# How far the lit buttons around each display reach past its edge, in\n"
|
||||||
|
"# EXTRA pixels. The pod's buttons were physical and backlit; on a\n"
|
||||||
|
"# desktop panel they are a thin bar along the glass edge, and a\n"
|
||||||
|
"# FLASHING one is easy to miss. This lengthens that bar - the button\n"
|
||||||
|
"# keeps its width and still reaches the same distance behind the\n"
|
||||||
|
"# picture, so the display and the click target are unchanged. The red\n"
|
||||||
|
"# MFD strips and the amber map columns only.\n"
|
||||||
|
"#\n"
|
||||||
|
"# 0 - the default - is the pod's own geometry, exactly as it shipped.\n"
|
||||||
|
"# 1-200 adds that many pixels. Give it a number: a word counts as 0.\n"
|
||||||
|
"# (BattleTech 4.11 carries the same setting defaulted to 10, where its\n"
|
||||||
|
"# players asked for it; the two games are tuned separately.)\n"
|
||||||
|
"#RP412MFDPROTRUDE=10\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 many seconds another player's pod will coast when their updates\n"
|
||||||
|
"# stop arriving, before it parks and waits. Default 1.\n"
|
||||||
|
"#\n"
|
||||||
|
"# Between updates a pod is carried forward on its last known velocity.\n"
|
||||||
|
"# That is a good guess for the length of one gap and a worse one every\n"
|
||||||
|
"# moment after: a race log from four pods showed gaps of one to two\n"
|
||||||
|
"# seconds each, and pods being corrected by as much as two and a half\n"
|
||||||
|
"# kilometres - most of the way across the map, through whatever was in\n"
|
||||||
|
"# the way, then snapped back. Stopping after a second reads as what it\n"
|
||||||
|
"# actually is, somebody's connection going quiet.\n"
|
||||||
|
"#\n"
|
||||||
|
"# 0 restores the old behaviour of coasting indefinitely. Like\n"
|
||||||
|
"# RP412NETPREDICT this changes how other pods MOVE and so where they\n"
|
||||||
|
"# collide - keep it the same on every machine in a race.\n"
|
||||||
|
"#RP412NETCOAST=1\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"
|
||||||
|
"#\n"
|
||||||
|
"# steam_appid.txt has to be there for a copy started from its own\n"
|
||||||
|
"# folder. It currently holds 480 - Valve's public Spacewar - so that is\n"
|
||||||
|
"# the name Steam shows you as playing. Red Planet's own AppID is on the\n"
|
||||||
|
"# way once its permissions are settled.\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"
|
||||||
|
"# 0 = no race-end network summary. On by default: when a race stops,\n"
|
||||||
|
"# rpl4.log gets a few NetLog lines - per remote pod, how many updates\n"
|
||||||
|
"# arrived and how evenly, how often its motion snapped instead of\n"
|
||||||
|
"# blending, and how far arriving updates moved it; per peer, whether\n"
|
||||||
|
"# the clock alignment ever had to step mid-race. Playtest logs used to\n"
|
||||||
|
"# say nothing at all about how the network felt; these lines are how a\n"
|
||||||
|
"# report like 'pod four was ticking' comes with numbers attached.\n"
|
||||||
|
"#RP412NETLOG=0\n"
|
||||||
|
"\n"
|
||||||
|
"# 0 = send network data the way the arcade did: fire and forget. On by\n"
|
||||||
|
"# default: bytes a connection cannot take right now are kept, in order,\n"
|
||||||
|
"# and retried - nothing is silently lost, and a partially-sent message\n"
|
||||||
|
"# can no longer shear the stream for good. The arcade LAN never filled\n"
|
||||||
|
"# a buffer so the old code never noticed; over the internet a peer\n"
|
||||||
|
"# stalled in its 10-30 second mission load stops reading and every\n"
|
||||||
|
"# machine still racing used to drop or shear whatever it sent them.\n"
|
||||||
|
"# The 0 setting still logs what it would have lost, for comparing.\n"
|
||||||
|
"#RP412NETSENDQ=0\n"
|
||||||
|
"\n"
|
||||||
|
"# How much a single connection may hold waiting to retry, in kilobytes.\n"
|
||||||
|
"# 16 to 4096, default 256 - seventeen-plus seconds of a full grid's\n"
|
||||||
|
"# traffic, far past the longest mission-load stall. A connection that\n"
|
||||||
|
"# overflows even this is declared dead rather than have the stream\n"
|
||||||
|
"# drop something from its middle.\n"
|
||||||
|
"#RP412NETQUEUEKB=256\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"
|
||||||
|
"# Extra passes at any pod that did not answer when the mesh was built.\n"
|
||||||
|
"# 1 by default, 0 for a single pass, 3 at most.\n"
|
||||||
|
"#\n"
|
||||||
|
"# Every pod has to reach every other pod before a race can start, and a\n"
|
||||||
|
"# pod that was still getting its own listener up when it was first\n"
|
||||||
|
"# called will not answer. The playtest logs show exactly that: the same\n"
|
||||||
|
"# pair timing out on one attempt and connecting on the next. One extra\n"
|
||||||
|
"# pass costs nothing when the room is healthy - there is nobody left to\n"
|
||||||
|
"# call - and rescues the room when a pod was merely slow.\n"
|
||||||
|
"#\n"
|
||||||
|
"# Each pass is expensive, since the transport already makes three tries\n"
|
||||||
|
"# of its own inside every call, which is why this is not a loop.\n"
|
||||||
|
"#RP412MESHRETRY=1\n"
|
||||||
|
"\n"
|
||||||
|
"# 0 = start rpl4.log fresh every launch, as builds before this one did.\n"
|
||||||
|
"#\n"
|
||||||
|
"# It now keeps the whole night instead. Each run is separated by a line\n"
|
||||||
|
"# of equals signs and the wall-clock time it started, so a report of\n"
|
||||||
|
"# \"the third race, about nine\" can be found in it. Nobody has to\n"
|
||||||
|
"# remember to copy the file aside between games any more - play the\n"
|
||||||
|
"# evening, send one log.\n"
|
||||||
|
"#\n"
|
||||||
|
"# It rolls itself to rpl4.prev.log once past 64 MB, which only the\n"
|
||||||
|
"# per-frame diagnostics can manage in an evening.\n"
|
||||||
|
"#RP412LOGAPPEND=0\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"
|
||||||
|
"# 1 = transport telemetry to rpl4.log: a five-second rollup per network\n"
|
||||||
|
"# connection (messages, bytes, wire writes, partial writes and refusals,\n"
|
||||||
|
"# how much sat queued for retry), and on Steam the connection's own\n"
|
||||||
|
"# health - ping, quality, unsent and unacknowledged bytes - plus one\n"
|
||||||
|
"# detailed route description per connection at teardown (direct or\n"
|
||||||
|
"# which relay). The first read anything in this codebase has ever\n"
|
||||||
|
"# taken of what the wire is actually doing.\n"
|
||||||
|
"#RP412NETSTATS=1\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"
|
||||||
|
"# Segment validity check at pod teardown, on by default. The winners'\n"
|
||||||
|
"# circle crash of 2026-08-11 died deleting a pod whose skeleton segment\n"
|
||||||
|
"# had been overwritten; this looks at the segments before deleting them\n"
|
||||||
|
"# and, if one is wrong, writes what it looks like - and where it first\n"
|
||||||
|
"# went wrong - to rpl4-fail.log, which survives the abort. It costs a\n"
|
||||||
|
"# handful of pointer compares per pod per race. 0 turns it off.\n"
|
||||||
|
"#RP412SEGCHECK=0\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();
|
||||||
+2027
-194
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,15 @@ int
|
|||||||
const char *
|
const char *
|
||||||
RPL4FrontEnd_LastPilotNames();
|
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
|
// 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
|
// console marshals the other pods), or entering one as a member pod
|
||||||
// (the lobby owner's console marshals us; no local egg, no console).
|
// (the lobby owner's console marshals us; no local egg, no console).
|
||||||
@@ -50,6 +59,76 @@ int
|
|||||||
void
|
void
|
||||||
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// This player's mission setup as display names. Only the host's
|
||||||
|
// picks decide the mission, so the lobby owner publishes these for
|
||||||
|
// everyone else to read - names rather than keys, because the map
|
||||||
|
// catalog differs between race and football and resolving it here
|
||||||
|
// saves the room from knowing which table a key belongs to.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedMapName();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedTimeName();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedWeatherName();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_SelectedLengthName();
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Catalog key -> display name, for the keys that travel as lobby
|
||||||
|
// member data. Unknown keys come back unchanged rather than blank.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_VehicleNameForKey(const char *key);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionNameForKey(const char *key);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamNameForKey(const char *key);
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Football team / position: the catalogs, and this player's picks.
|
||||||
|
// The lobby publishes the picks as member data and lets members
|
||||||
|
// cycle them in the room; the host's egg builder honors them.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_TeamCount();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamName(int index);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamKey(int index);
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_PositionCount();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionName(int index);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionKey(int index);
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_GetTeamIndex();
|
||||||
|
void
|
||||||
|
RPL4FrontEnd_SetTeamIndex(int index);
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_GetPositionIndex();
|
||||||
|
void
|
||||||
|
RPL4FrontEnd_SetPositionIndex(int index);
|
||||||
|
|
||||||
|
// True when this player's setup menu has Football selected (the host's
|
||||||
|
// pick decides the mission; members use it to know what to show).
|
||||||
|
Logical
|
||||||
|
RPL4FrontEnd_IsFootballSelected();
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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
|
// Hosted-race pilots fed by the Steam lobby: overrides the
|
||||||
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
||||||
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
||||||
@@ -60,6 +139,14 @@ struct FEHostedPilot
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
char badge[24];
|
||||||
|
// football: the member's own picks, empty = assign one for them
|
||||||
|
char team[32]; // team key ("Red/Pink", ...)
|
||||||
|
char position[16]; // "runner" / "crusher" / "blocker"
|
||||||
|
|
||||||
|
// 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
|
void
|
||||||
RPL4FrontEnd_SetHostedPilots(
|
RPL4FrontEnd_SetHostedPilots(
|
||||||
|
|||||||
+333
-45
@@ -1,4 +1,5 @@
|
|||||||
#include "rpl4.h"
|
#include "rpl4.h"
|
||||||
|
|
||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#define PRELOAD_ART
|
#define PRELOAD_ART
|
||||||
@@ -1556,6 +1557,21 @@ void
|
|||||||
DrawNames(worldToView);
|
DrawNames(worldToView);
|
||||||
localView.DetachRecorder();
|
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
|
// deliberately falls through into default case
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1630,6 +1646,26 @@ void
|
|||||||
Test_Tell("x= " << xMin << "..." << xMax << "\n");
|
Test_Tell("x= " << xMin << "..." << xMax << "\n");
|
||||||
Test_Tell("y= " << yMin << "..." << yMax << "\n");
|
Test_Tell("y= " << yMin << "..." << yMax << "\n");
|
||||||
Test_Tell("z= " << zMin << "..." << zMax << "\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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1660,6 +1696,33 @@ void
|
|||||||
xMin, yMin, zMin,
|
xMin, yMin, zMin,
|
||||||
xMax, yMax, zMax
|
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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1951,7 +2014,75 @@ GPS::GPS(
|
|||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
background = new Video8BitBuffered(width, height);
|
background = new Video8BitBuffered(width, height);
|
||||||
Register_Object(background);
|
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;
|
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();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1989,6 +2120,95 @@ void
|
|||||||
|
|
||||||
void
|
void
|
||||||
GPS::NotifyOfNewInterestingEntity(Entity *entity)
|
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(this);
|
||||||
Check(entity);
|
Check(entity);
|
||||||
@@ -2104,61 +2324,34 @@ void
|
|||||||
ChainIteratorOf<Entity*>
|
ChainIteratorOf<Entity*>
|
||||||
i(staticEntityList.GetInstanceList());
|
i(staticEntityList.GetInstanceList());
|
||||||
|
|
||||||
Check(renderer);
|
|
||||||
L4Warehouse
|
|
||||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
|
||||||
Check(warehouse);
|
|
||||||
|
|
||||||
Entity
|
Entity
|
||||||
*entity;
|
*entity;
|
||||||
L4GaugeImage
|
|
||||||
*gauge_image;
|
|
||||||
|
|
||||||
AffineMatrix
|
|
||||||
worldToView,
|
|
||||||
localToView;
|
|
||||||
|
|
||||||
Vector3D
|
|
||||||
scaling_vector;
|
|
||||||
//------------------------------------
|
|
||||||
// Scale the display
|
|
||||||
//------------------------------------
|
|
||||||
|
|
||||||
Verify(!Small_Enough(pixelsPerMeter));
|
Verify(!Small_Enough(pixelsPerMeter));
|
||||||
|
|
||||||
scaling_vector.x = pixelsPerMeter;
|
int
|
||||||
scaling_vector.y = pixelsPerMeter;
|
drawn = 0;
|
||||||
scaling_vector.z = pixelsPerMeter;
|
|
||||||
|
|
||||||
worldToView.BuildIdentity();
|
|
||||||
worldToView *= centeringOffset; // translation
|
|
||||||
worldToView *= scaling_vector;
|
|
||||||
|
|
||||||
while ((entity=i.ReadAndNext()) != NULL)
|
while ((entity=i.ReadAndNext()) != NULL)
|
||||||
{
|
{
|
||||||
Check(entity);
|
Check(entity);
|
||||||
//-------------------------------------
|
DrawStaticEntity(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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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
|
// Redraw new background, restart moving entity display
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
@@ -2166,6 +2359,71 @@ void
|
|||||||
Check_Fpu();
|
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
|
void
|
||||||
GPS::Execute()
|
GPS::Execute()
|
||||||
@@ -2655,6 +2913,36 @@ void
|
|||||||
intercomEnabled = intercom->GetChannel() != Icom::undefinedChannel;
|
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
|
// Clear the current array
|
||||||
//-------------------------------------------
|
//-------------------------------------------
|
||||||
|
|||||||
+27
-1
@@ -330,12 +330,27 @@ public:
|
|||||||
BecameActive(); // virtual function in 'GaugeBase'
|
BecameActive(); // virtual function in 'GaugeBase'
|
||||||
void
|
void
|
||||||
NotifyOfNewInterestingEntity(Entity *entity);
|
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
|
void
|
||||||
Execute();
|
Execute();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void
|
void
|
||||||
UpdateStaticEntities();
|
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
|
enum
|
||||||
{
|
{
|
||||||
@@ -344,7 +359,18 @@ protected:
|
|||||||
|
|
||||||
Logical
|
Logical
|
||||||
needsStaticUpdate,
|
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
|
Scalar
|
||||||
LODIndex,
|
LODIndex,
|
||||||
metersPerPixel,
|
metersPerPixel,
|
||||||
|
|||||||
+1002
-115
File diff suppressed because it is too large
Load Diff
+89
-2
@@ -33,13 +33,100 @@ enum RPL4LobbyOutcome
|
|||||||
Logical
|
Logical
|
||||||
RPL4Lobby_Available();
|
RPL4Lobby_Available();
|
||||||
|
|
||||||
|
// True when this build has Steam and environ.ini asked for it, whether
|
||||||
|
// or not it actually came up. The menu offers the lobby buttons on this
|
||||||
|
// and greys them out on Available() - a player who turned Steam on and
|
||||||
|
// then launched without the client running should be told so, not left
|
||||||
|
// looking at a menu that quietly has two fewer buttons than the last
|
||||||
|
// time they saw it.
|
||||||
|
Logical
|
||||||
|
RPL4Lobby_Configured();
|
||||||
|
|
||||||
// True while we sit in a lobby (races return to the room).
|
// True while we sit in a lobby (races return to the room).
|
||||||
Logical
|
Logical
|
||||||
RPL4Lobby_InRoom();
|
RPL4Lobby_InRoom();
|
||||||
|
|
||||||
// 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
|
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
|
int
|
||||||
RPL4Lobby_Join(HINSTANCE instance, HWND main_window);
|
RPL4Lobby_Join(HINSTANCE instance, HWND main_window);
|
||||||
|
|
||||||
|
|||||||
+118
-56
@@ -444,9 +444,18 @@ void
|
|||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
// Notify of mode change
|
// 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)
|
if (previous_mode != controlMode)
|
||||||
{
|
{
|
||||||
L4VTVControlsMapper::NotifyOfControlModeChange(controlMode);
|
NotifyOfControlModeChange(controlMode);
|
||||||
}
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -725,6 +734,13 @@ void
|
|||||||
mode_manager->AddModeMask(previousPresetModeMask);
|
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
|
// Save the new preset number
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
previousPresetNumber = preset_number;
|
previousPresetNumber = preset_number;
|
||||||
@@ -733,6 +749,19 @@ void
|
|||||||
Check_Fpu();
|
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 ##############################
|
//########################### ThrustmasterMapper ##############################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -854,18 +883,18 @@ void
|
|||||||
//-------------------------------------------------------
|
//-------------------------------------------------------
|
||||||
// Set driving modes
|
// Set driving modes
|
||||||
//-------------------------------------------------------
|
//-------------------------------------------------------
|
||||||
case 'b':
|
//
|
||||||
case 'B': SetControlsMode(BasicMode); break;
|
// B / S / V / M used to drop straight into Basic, Standard,
|
||||||
|
// Veteran and Master here. The driving mode is a panel
|
||||||
case 's':
|
// decision - the four buttons on the Upper Right MFD, with the
|
||||||
case 'S': SetControlsMode(StandardMode); break;
|
// 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
|
||||||
case 'v':
|
// 4.12 than it ever was in the pod: the whole letter board is
|
||||||
case 'V': SetControlsMode(VeteranMode); break;
|
// the MFD banks now, so those four letters are buttons in their
|
||||||
|
// own right and would have fired twice.
|
||||||
case 'm':
|
//
|
||||||
case 'M': SetControlsMode(MasterMode); break;
|
// Nothing replaces them. Press the mode you want.
|
||||||
|
//
|
||||||
//-------------------------------------------------------
|
//-------------------------------------------------------
|
||||||
// Configuration stuff
|
// Configuration stuff
|
||||||
//-------------------------------------------------------
|
//-------------------------------------------------------
|
||||||
@@ -1400,45 +1429,13 @@ void
|
|||||||
if (message->dataContents > 0)
|
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
|
PresetEnable(
|
||||||
current_preset_number = (message->dataContents - 1)
|
(message->dataContents - 1) - LBE4ControlsManager::ButtonSecondary7
|
||||||
- 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -1626,6 +1623,18 @@ void
|
|||||||
modeLamp[lamp_number]->SetState(L4Lamp::LampStateOn);
|
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
|
// Invoke ancestral method
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
@@ -1655,6 +1664,44 @@ void
|
|||||||
Check_Fpu();
|
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
|
// Construction and Destruction Support
|
||||||
//
|
//
|
||||||
@@ -1680,6 +1727,20 @@ VTVRIOMapper::VTVRIOMapper(
|
|||||||
leftPedal = 0.0f;
|
leftPedal = 0.0f;
|
||||||
rightPedal = 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);
|
Check(application);
|
||||||
LBE4ControlsManager
|
LBE4ControlsManager
|
||||||
*controls = Cast_Object(
|
*controls = Cast_Object(
|
||||||
@@ -1915,13 +1976,14 @@ VTVRIOMapper::VTVRIOMapper(
|
|||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
|
||||||
// These lamps are explicitly controlled by SelectPresetMessageHandler
|
// These lamps are explicitly controlled by NotifyOfPresetChange.
|
||||||
modeLamp[i] = CreateControlledLamp(button_number[i]);
|
// 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]);
|
Check(presetLamp[i]);
|
||||||
modeLamp[i]->SetState(
|
presetLamp[i]->SetState(
|
||||||
(i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim
|
(i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user