Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -84,3 +84,10 @@ 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/
|
||||
|
||||
+171
@@ -16,6 +16,7 @@
|
||||
#include "console.h"
|
||||
#include "appmsg.h"
|
||||
#include "evtstat.h"
|
||||
#include "inputscript.h"
|
||||
|
||||
#if defined(TRACE_FOREGROUND_PROCESSING)
|
||||
BitTrace Foreground_Processing("Foreground Processing");
|
||||
@@ -273,6 +274,35 @@ Scalar
|
||||
return mgr->GetFrameRate();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// GetMissionElapsed
|
||||
//#############################################################################
|
||||
//
|
||||
Scalar
|
||||
Application::GetMissionElapsed()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
// gameStarted is only ever stamped by RunMissionMessageHandler, so before
|
||||
// the race it is uninitialized - and entities that are pre-runnable do get
|
||||
// performed before then. Answer zero until the clock actually exists.
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
if (
|
||||
GetApplicationState() != RunningMission
|
||||
&& GetApplicationState() != EndingMission
|
||||
)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Scalar elapsed = Now() - gameStarted;
|
||||
return (elapsed > 0.0f) ? elapsed : 0.0f;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Initialize
|
||||
@@ -577,6 +607,147 @@ Time startUpdate = Now();
|
||||
updateManager->Execute(start_of_frame);
|
||||
Time endUpdate = Now();
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
// RP412PHYSTRACE=1: the player's position, sampled on the SIMULATION's
|
||||
// own clock rather than per frame.
|
||||
//
|
||||
// This is the acceptance test for decoupling physics from frame rate.
|
||||
// Run the same egg at two frame rates and diff the traces: today they
|
||||
// diverge, because the simulation advances by whatever the last frame
|
||||
// happened to cost (SIMULATE.cpp, slice = till - lastPerformance), so a
|
||||
// 30 fps machine integrates in 33 ms steps and a 144 fps machine in 7 ms
|
||||
// ones and they are not the same race. Fixed-step them and the two
|
||||
// traces have to agree.
|
||||
//
|
||||
// Sampled every 0.25 s of SIM time on purpose - sampling per frame would
|
||||
// compare different instants and prove nothing.
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int physTrace = -1;
|
||||
|
||||
if (physTrace < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412PHYSTRACE");
|
||||
physTrace = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
//
|
||||
// The scripted-input harness shares this anchor: its clock has to
|
||||
// start at the same instant the vehicle is stopped, or the script
|
||||
// timeline shifts against the settling transient by however long
|
||||
// the load happened to take.
|
||||
//
|
||||
if ((physTrace || RPInputScript_Active()) &&
|
||||
GetApplicationState() == RunningMission)
|
||||
{
|
||||
static Logical traceStarted = False;
|
||||
static Time traceOrigin;
|
||||
static Scalar traceDue = (Scalar) 0;
|
||||
|
||||
if (!traceStarted)
|
||||
{
|
||||
traceStarted = True;
|
||||
traceOrigin = start_of_frame;
|
||||
traceDue = (Scalar) 0;
|
||||
|
||||
//
|
||||
//----------------------------------------------------------
|
||||
// Start the measurement from a known state, not merely a
|
||||
// known place.
|
||||
//
|
||||
// The pod sits on its pad simulating while the mission
|
||||
// loads, and a load is not the same length twice - two runs
|
||||
// of the same egg reached the green light 776 steps in and
|
||||
// 599 steps in. Same pad, same position, different VELOCITY,
|
||||
// and a trajectory compared from there measures the loader,
|
||||
// not the physics.
|
||||
//
|
||||
// So: stop the vehicle dead and put its clock on the same
|
||||
// mark. Every run then starts from rest at the same instant
|
||||
// and any difference that follows belongs to the simulation.
|
||||
//
|
||||
// Test scaffolding, and it only runs with the trace asked
|
||||
// for - it would be a cheat in a real race.
|
||||
//----------------------------------------------------------
|
||||
//
|
||||
Player *reset_player = GetMissionPlayer();
|
||||
Entity *reset_vehicle =
|
||||
(reset_player != NULL)
|
||||
? reset_player->GetPlayerVehicle() : NULL;
|
||||
|
||||
if (reset_vehicle != NULL &&
|
||||
reset_vehicle->IsDerivedFrom(*Mover::GetClassDerivations()))
|
||||
{
|
||||
Mover *reset_mover = (Mover *) reset_vehicle;
|
||||
|
||||
reset_mover->localVelocity = Motion::Identity;
|
||||
reset_mover->localAcceleration = Motion::Identity;
|
||||
|
||||
//
|
||||
// The clock is NOT touched. lastPerformance sits on the
|
||||
// vehicle's own step grid and the trace reads that grid
|
||||
// instead. The first version forced it to the frame
|
||||
// timestamp, which knocked the vehicle off its grid by
|
||||
// a random fraction of a step per run - and that read
|
||||
// as physics drift when it was only ever measurement.
|
||||
//
|
||||
traceOrigin = reset_mover->GetLastPerformance();
|
||||
|
||||
// the script's t=0 is this same instant
|
||||
RPInputScript_Arm(traceOrigin);
|
||||
|
||||
DEBUG_STREAM << "PhysTrace: vehicle stopped "
|
||||
<< "at the green light\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
if (physTrace)
|
||||
{
|
||||
|
||||
//
|
||||
// Sampled on the SIMULATION's clock - the vehicle's own
|
||||
// lastPerformance, which advances in whole fixed steps - so two
|
||||
// runs sample at identical step counts and their traces compare
|
||||
// exactly. Frame time samples mid-step at whatever phase the
|
||||
// frame happened to land on, which compares different instants
|
||||
// and calls the difference physics.
|
||||
//
|
||||
Player *clock_player = GetMissionPlayer();
|
||||
Entity *clock_vehicle =
|
||||
(clock_player != NULL) ? clock_player->GetPlayerVehicle() : NULL;
|
||||
|
||||
Scalar elapsed =
|
||||
(clock_vehicle != NULL)
|
||||
? (Scalar)(clock_vehicle->GetLastPerformance() - traceOrigin)
|
||||
: (Scalar)(start_of_frame - traceOrigin);
|
||||
if (elapsed >= traceDue)
|
||||
{
|
||||
traceDue += (Scalar) 0.25;
|
||||
|
||||
Player *trace_player = GetMissionPlayer();
|
||||
Entity *trace_vehicle =
|
||||
(trace_player != NULL) ? trace_player->GetPlayerVehicle() : NULL;
|
||||
|
||||
if (trace_vehicle != NULL)
|
||||
{
|
||||
extern long gPhysicsStepsTaken;
|
||||
char buffer[160];
|
||||
|
||||
sprintf(buffer,
|
||||
"PhysTrace: t=%7.3f steps=%6ld pos %12.5f %12.5f %12.5f\n",
|
||||
(double) elapsed,
|
||||
gPhysicsStepsTaken,
|
||||
(double) trace_vehicle->localOrigin.linearPosition.x,
|
||||
(double) trace_vehicle->localOrigin.linearPosition.y,
|
||||
(double) trace_vehicle->localOrigin.linearPosition.z);
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CLEAR_UPDATE_MANAGER();
|
||||
|
||||
//
|
||||
|
||||
@@ -318,6 +318,15 @@ public:
|
||||
Scalar
|
||||
GetSecondsRemainingInGame()
|
||||
{return secondsRemainingInGame;}
|
||||
//
|
||||
// Seconds since the console's RunMission started the race, counting up.
|
||||
// Every machine anchors this on the same message, so anything derived
|
||||
// from it agrees across the mesh without being replicated - see the
|
||||
// clockwork doors in DOOR.cpp. Reads 0 outside a running mission
|
||||
// (gameStarted holds garbage until RunMission stamps it).
|
||||
//
|
||||
Scalar
|
||||
GetMissionElapsed();
|
||||
ApplicationID
|
||||
GetApplicationID()
|
||||
{return applicationID;}
|
||||
|
||||
@@ -234,6 +234,74 @@ Background_Loop:
|
||||
}
|
||||
Time endBackground = Now();
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------
|
||||
// RP412GAUGEDIAG=1: where the frame actually goes.
|
||||
//
|
||||
// These four timestamps have been computed every frame since forever
|
||||
// and never reported. The whole cockpit problem is a question about
|
||||
// this split - the background loop only gets what the foreground
|
||||
// leaves - and it has been measurable all along.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int
|
||||
frameSplitDiag = -1;
|
||||
|
||||
if (frameSplitDiag < 0)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412GAUGEDIAG");
|
||||
|
||||
frameSplitDiag = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (frameSplitDiag)
|
||||
{
|
||||
static Scalar
|
||||
foregroundSum = (Scalar) 0,
|
||||
backgroundSum = (Scalar) 0,
|
||||
frameSum = (Scalar) 0;
|
||||
static int
|
||||
splitFrames = 0;
|
||||
static Logical
|
||||
splitStarted = False;
|
||||
static Time
|
||||
splitWindowStart;
|
||||
|
||||
Time
|
||||
splitNow = Now();
|
||||
|
||||
foregroundSum += (Scalar)(endForeground - startForeground);
|
||||
backgroundSum += (Scalar)(endBackground - startBackground);
|
||||
frameSum += (Scalar)(splitNow - beginFrameTimestamp);
|
||||
++splitFrames;
|
||||
|
||||
if (!splitStarted)
|
||||
{
|
||||
splitStarted = True;
|
||||
splitWindowStart = splitNow;
|
||||
}
|
||||
else if ((Scalar)(splitNow - splitWindowStart) >= (Scalar) 2)
|
||||
{
|
||||
char
|
||||
buffer[200];
|
||||
|
||||
sprintf(buffer,
|
||||
"FrameSplit: %d frames | foreground %.2f ms | "
|
||||
"background %.2f ms | whole frame %.2f ms\n",
|
||||
splitFrames,
|
||||
(double)(foregroundSum * 1000.0f / splitFrames),
|
||||
(double)(backgroundSum * 1000.0f / splitFrames),
|
||||
(double)(frameSum * 1000.0f / splitFrames));
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
|
||||
foregroundSum = backgroundSum = frameSum = (Scalar) 0;
|
||||
splitFrames = 0;
|
||||
splitWindowStart = splitNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//char str[256];
|
||||
//Scalar lastFrameLength = Now() - beginFrameTimestamp;
|
||||
//sprintf(str, "RPL4 - %.2f FPS", 1.0f / lastFrameLength);
|
||||
|
||||
+10
-2
@@ -94,8 +94,16 @@ void
|
||||
}
|
||||
headEntitySocket.Add(entity);
|
||||
|
||||
alDistanceModel(AL_LINEAR_DISTANCE);
|
||||
alDopplerFactor(0.3f);
|
||||
// FIDELITY (docs/SOUND.md F3/F10): the engine computes the AUTHORED distance
|
||||
// attenuation curve (AUDIO.INI amplitude_rolloff knee/exponent ->
|
||||
// AudioLocation::distanceVolumeScale) and the AUTHORED doppler-cents model
|
||||
// (doppler_range=600 / speed_of_sound=250) on every spatial update. Disable
|
||||
// OpenAL's own models so they cannot double-apply or fight them:
|
||||
// AL_LINEAR_DISTANCE faded distant audio to zero on a straight line where the
|
||||
// authored curve still sits near 44% at the clip edge, and AL doppler ran at
|
||||
// the wrong constants with a sign-inverted velocity feed.
|
||||
alDistanceModel(AL_NONE);
|
||||
alDopplerFactor(0.0f);
|
||||
|
||||
#if 0
|
||||
//
|
||||
|
||||
+65
-140
@@ -72,172 +72,96 @@ Door::AttributeIndexSet& Door::GetAttributeIndex()
|
||||
//#############################################################################
|
||||
// Model Support
|
||||
//
|
||||
void
|
||||
Door::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(message);
|
||||
Subsystem::ReadUpdateRecord(message);
|
||||
UpdateRecord* record = (UpdateRecord*) message;
|
||||
|
||||
percentOpen = record->percentOpen;
|
||||
switch (GetSimulationState())
|
||||
{
|
||||
case Opening:
|
||||
case Closing:
|
||||
phaseTimeRemaining = travelTime;
|
||||
break;
|
||||
case Opened:
|
||||
case Closed:
|
||||
phaseTimeRemaining = deadTime;
|
||||
break;
|
||||
}
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door updated to state "
|
||||
// << GetSimulationState() << " @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
MoveCollisionVolume(percentOpen);
|
||||
Check_Fpu();
|
||||
}
|
||||
// There is no ReadUpdateRecord/WriteUpdateRecord pair here on purpose. Doors
|
||||
// are Hermit instances built independently on every host, so no door state is
|
||||
// ever sent or received - the phase function below is the only thing that
|
||||
// decides where a door is, and it reaches the same answer everywhere.
|
||||
//
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
Door::WriteUpdateRecord(Simulation::UpdateRecord *record, int update_model)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(record);
|
||||
|
||||
Subsystem::WriteUpdateRecord(record, update_model);
|
||||
|
||||
UpdateRecord *update = (UpdateRecord*)record;
|
||||
update->percentOpen = percentOpen;
|
||||
update->recordLength = sizeof(*update);
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
Door::SlideDoor(Scalar time_slice)
|
||||
Door::SlideDoor(Scalar)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
//------------------------------------------------------------
|
||||
// Advance the clock, then branch based upon our current state
|
||||
//------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
// The door is clockwork. Its position is a function of how long the race
|
||||
// has been running, not of a countdown integrated frame by frame, so:
|
||||
//
|
||||
int new_state;
|
||||
if (time_slice > 1.0f)
|
||||
// - every machine puts this door in the same place from the same mission
|
||||
// clock, without a byte crossing the wire,
|
||||
// - a frame hitch of any length costs nothing, because there is no
|
||||
// accumulated state left to fall behind (the old code dropped any slice
|
||||
// over a second outright and never got that time back).
|
||||
//
|
||||
// Phase zero is the instant the door starts to close, fully open, which is
|
||||
// where the original state machine began from DefaultState:
|
||||
//
|
||||
// [0, travel) Closing 1 -> 0
|
||||
// [travel, travel+dead) Closed 0
|
||||
// [travel+dead, 2travel+dead) Opening 0 -> 1
|
||||
// [2travel+dead, cycle) Opened 1
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
if (cycleTime <= 0.0f)
|
||||
{
|
||||
MoveCollisionVolume(0.0f);
|
||||
SetSimulationState(Closed);
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
phaseTimeRemaining -= time_slice;
|
||||
Scalar percent_open;
|
||||
switch (GetSimulationState())
|
||||
|
||||
Check(application);
|
||||
Scalar phase = fmod(application->GetMissionElapsed() - phaseOffset, cycleTime);
|
||||
if (phase < 0.0f)
|
||||
{
|
||||
phase += cycleTime;
|
||||
}
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// If the door is not done opening, set its new position, otherwise branch
|
||||
// to the opened state
|
||||
//------------------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
// Pick the band. Each division below is guarded by the comparison that
|
||||
// selected the branch, so a door with a zero travelTime or deadTime simply
|
||||
// loses that band rather than dividing by zero.
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
case Opening:
|
||||
Door_Opening:
|
||||
new_state = Opening;
|
||||
if (phaseTimeRemaining > 0.0f)
|
||||
{
|
||||
percent_open = 1.0f - phaseTimeRemaining/travelTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
phaseTimeRemaining += deadTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opened @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Opened;
|
||||
}
|
||||
currentVelocity.Subtract(
|
||||
worldExtent,
|
||||
GetEntity()->localOrigin.linearPosition
|
||||
);
|
||||
currentVelocity /= travelTime;
|
||||
Check_Fpu();
|
||||
break;
|
||||
Scalar open_start = travelTime + deadTime;
|
||||
int new_state;
|
||||
Scalar percent_open;
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------
|
||||
// If the door is ready to start closing, jump to closing state
|
||||
//-------------------------------------------------------------
|
||||
//
|
||||
case Opened:
|
||||
Door_Opened:
|
||||
new_state = Opened;
|
||||
if (phaseTimeRemaining <= 0.0f)
|
||||
{
|
||||
phaseTimeRemaining += travelTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closing @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Closing;
|
||||
}
|
||||
percent_open = 1.0f;
|
||||
currentVelocity = Vector3D::Identity;
|
||||
Check_Fpu();
|
||||
break;
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// If the door is not done closing, set its new position, otherwise branch
|
||||
// to the closed state
|
||||
//------------------------------------------------------------------------
|
||||
//
|
||||
case DefaultState:
|
||||
phaseTimeRemaining = travelTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door default @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
case Closing:
|
||||
Door_Closing:
|
||||
if (phase < travelTime)
|
||||
{
|
||||
new_state = Closing;
|
||||
if (phaseTimeRemaining > 0.0f)
|
||||
{
|
||||
percent_open = phaseTimeRemaining/travelTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
phaseTimeRemaining += deadTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closed @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Closed;
|
||||
}
|
||||
percent_open = 1.0f - phase/travelTime;
|
||||
currentVelocity.Subtract(
|
||||
GetEntity()->localOrigin.linearPosition,
|
||||
worldExtent
|
||||
);
|
||||
currentVelocity /= travelTime;
|
||||
Check_Fpu();
|
||||
break;
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------
|
||||
// If the door is ready to start opening, jump to opening state
|
||||
//-------------------------------------------------------------
|
||||
//
|
||||
case Closed:
|
||||
Door_Closed:
|
||||
}
|
||||
else if (phase < open_start)
|
||||
{
|
||||
new_state = Closed;
|
||||
if (phaseTimeRemaining <= 0.0f)
|
||||
{
|
||||
phaseTimeRemaining += travelTime;
|
||||
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opening @ "
|
||||
// << application->GetSecondsRemainingInGame() << endl;
|
||||
goto Door_Opening;
|
||||
}
|
||||
percent_open = 0.0f;
|
||||
currentVelocity = Vector3D::Identity;
|
||||
Check_Fpu();
|
||||
break;
|
||||
|
||||
}
|
||||
else if (phase < open_start + travelTime)
|
||||
{
|
||||
new_state = Opening;
|
||||
percent_open = (phase - open_start)/travelTime;
|
||||
currentVelocity.Subtract(
|
||||
worldExtent,
|
||||
GetEntity()->localOrigin.linearPosition
|
||||
);
|
||||
currentVelocity /= travelTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
new_state = Opened;
|
||||
percent_open = 1.0f;
|
||||
currentVelocity = Vector3D::Identity;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -344,7 +268,8 @@ Door::Door(
|
||||
//
|
||||
// Initialize variables
|
||||
//
|
||||
phaseTimeRemaining = 0.0f;
|
||||
phaseOffset = 0.0f;
|
||||
cycleTime = 2.0f*(travelTime + deadTime);
|
||||
currentPosition = Point3D::Identity;
|
||||
|
||||
SetPerformance(&Door::SlideDoor);
|
||||
|
||||
+16
-19
@@ -20,16 +20,11 @@ struct Door__SubsystemResource:
|
||||
collisionID;
|
||||
};
|
||||
|
||||
//##########################################################################
|
||||
//##################### Chute::UpdateRecord #####################
|
||||
//##########################################################################
|
||||
|
||||
struct Door__UpdateRecord :
|
||||
public Subsystem::UpdateRecord
|
||||
{
|
||||
Scalar
|
||||
percentOpen;
|
||||
};
|
||||
//
|
||||
// A door has no update record. It is Hermit clockwork - every host builds
|
||||
// its own out of the map stream and derives the position from the mission
|
||||
// clock, so there is nothing to publish and nothing to receive.
|
||||
//
|
||||
|
||||
//##########################################################################
|
||||
//######################### CLASS Door ########################
|
||||
@@ -91,7 +86,6 @@ public:
|
||||
|
||||
typedef void
|
||||
(Door::*Performance)(Scalar time_slice);
|
||||
typedef Door__UpdateRecord UpdateRecord;
|
||||
|
||||
void
|
||||
SetPerformance(Performance performance)
|
||||
@@ -109,12 +103,6 @@ public:
|
||||
GetFirstBoxedSolid()
|
||||
{Check(this); return collisionVolumes;}
|
||||
|
||||
protected:
|
||||
void
|
||||
WriteUpdateRecord(Simulation::UpdateRecord *message, int update_model);
|
||||
void
|
||||
ReadUpdateRecord(Simulation::UpdateRecord *message);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Construction and Destruction
|
||||
//
|
||||
@@ -152,9 +140,18 @@ private:
|
||||
worldExtent;
|
||||
|
||||
Scalar
|
||||
phaseTimeRemaining,
|
||||
travelTime,
|
||||
deadTime;
|
||||
deadTime,
|
||||
//
|
||||
// Where in the cycle this door sits at mission time zero, and the
|
||||
// length of one full open-close-open cycle. phaseOffset is not in
|
||||
// the subsystem resource yet: every door in the game is in lockstep,
|
||||
// and adding a field to Door__SubsystemResource changes its sizeof,
|
||||
// which invalidates every prebuilt .res. Wire it to a "PhaseOffset"
|
||||
// notation entry when there is a reason to rebuild resources.
|
||||
//
|
||||
phaseOffset,
|
||||
cycleTime;
|
||||
|
||||
int collisionVolumeCount;
|
||||
|
||||
|
||||
+8
-1
@@ -126,8 +126,15 @@ Logical
|
||||
}
|
||||
|
||||
creation_message->classToCreate = RegisteredClass::DoorFrameClassID;
|
||||
//
|
||||
// Hermit, not Master: every host builds its own doorframe out of the map
|
||||
// stream (see the DoorFrameClassID exemption in LoadMapStream) and runs it
|
||||
// off the mission clock. Hermit is the instance kind DynamicEntityCreation
|
||||
// does NOT broadcast, which is what stops N machines each announcing the
|
||||
// same doorframe and producing N-squared of them.
|
||||
//
|
||||
creation_message->instanceFlags =
|
||||
MasterInstance|DynamicFlag|MapFlag|TrappedFlag;
|
||||
HermitInstance|DynamicFlag|MapFlag|TrappedFlag;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+39
-1
@@ -224,9 +224,47 @@ void
|
||||
//-----------------------------------------------------
|
||||
//
|
||||
highest = highest - lowest + 1;
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------------
|
||||
// RP412SPAWNZONE pins which drop zone is tried first, so a test run can
|
||||
// be repeated.
|
||||
//
|
||||
// The pick below is Random(), and Random() is seeded - but the seed only
|
||||
// makes a run repeatable if the same NUMBER of draws happens first, and
|
||||
// that depends on how many frames the mission load took. So two runs of
|
||||
// the same egg with the same RANDOM= still start on different pads, over
|
||||
// different ground, and no two traces can be compared. That is not a
|
||||
// game bug, but it makes the physics unmeasurable.
|
||||
//
|
||||
// Pinned, the zone is tried first and the loop falls back to the random
|
||||
// walk if it is taken - so this can never wedge, and it changes nothing
|
||||
// unless it is set.
|
||||
//------------------------------------------------------------------------
|
||||
//
|
||||
static int
|
||||
pinnedZone = -2;
|
||||
|
||||
if (pinnedZone == -2)
|
||||
{
|
||||
const char *setting = getenv("RP412SPAWNZONE");
|
||||
pinnedZone = (setting != NULL) ? atoi(setting) : -1;
|
||||
}
|
||||
|
||||
Logical
|
||||
tryPinnedZone = (pinnedZone >= 0) ? True : False;
|
||||
|
||||
while (remaining)
|
||||
{
|
||||
i = lowest + Random(highest);
|
||||
if (tryPinnedZone)
|
||||
{
|
||||
tryPinnedZone = False;
|
||||
i = lowest + (pinnedZone % highest);
|
||||
}
|
||||
else
|
||||
{
|
||||
i = lowest + Random(highest);
|
||||
}
|
||||
Verify(i < dropZoneCount && i >= 0);
|
||||
if (IsAvailable(i))
|
||||
{
|
||||
|
||||
@@ -745,6 +745,103 @@ void
|
||||
//
|
||||
if (GetInstance() != ReplicantInstance)
|
||||
{
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// Fixed-step: the subsystems and the entity advance TOGETHER,
|
||||
// one step at a time, because they read each other mid-flight.
|
||||
// The VTV's hover spring is computed from its thrusters'
|
||||
// measured heights, and each thruster measures from where the
|
||||
// vehicle IS - so thrusters stepped twice against a vehicle
|
||||
// that has not moved yet hand back two identical height
|
||||
// samples, and the spring fires twice on stale data. Measured,
|
||||
// that pod climbs at 30 fps and flies level at 144.
|
||||
//
|
||||
// So the step loop lives HERE, above both: everyone is walked
|
||||
// to the same sub-frame instant before anyone takes the next
|
||||
// step. Watchers and the update stream still run once per
|
||||
// frame, after the loop - stepping is physics, watching is
|
||||
// I/O, and only the first belongs inside.
|
||||
//
|
||||
// The interleave keys off the ENTITY's own clock so a
|
||||
// subsystem created mid-flight (they are made alongside their
|
||||
// owner) can never wedge the loop.
|
||||
//----------------------------------------------------------------
|
||||
//
|
||||
Scalar fixed_step = Simulation::FixedStep();
|
||||
|
||||
if (fixed_step > (Scalar) 0)
|
||||
{
|
||||
//
|
||||
// One grid for the whole vehicle. Every Simulation anchors
|
||||
// its own lastPerformance at its creation time, so an
|
||||
// entity and its subsystems were stepping on grids offset
|
||||
// by a random fraction of a step - deterministic within a
|
||||
// run, DIFFERENT between runs, because creation times ride
|
||||
// on load timing. The thrusters' measurements then landed
|
||||
// a different sub-step distance from the vehicle's
|
||||
// integration every launch, which is physics drift no seed
|
||||
// can pin. Snap the subsystems onto the entity's grid; the
|
||||
// interleave below then keeps everyone in lockstep by
|
||||
// construction, and once aligned this assignment is a
|
||||
// no-op every frame after.
|
||||
//
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->SetLastPerformance(
|
||||
GetLastPerformance());
|
||||
}
|
||||
}
|
||||
|
||||
Time step_till = GetLastPerformance();
|
||||
step_till += fixed_step;
|
||||
|
||||
while (step_till <= till)
|
||||
{
|
||||
//
|
||||
// BeginStep on the ENTITY comes before the subsystems
|
||||
// perform: the Mover's force accumulator is cleared
|
||||
// here, and the thrusters then ADD this step's forces
|
||||
// into a clean slate. The first version left that
|
||||
// clear on the per-frame path, so a two-step frame
|
||||
// integrated step one's thrust twice - and since how
|
||||
// many steps land in a frame rides on wall-clock
|
||||
// jitter, no two runs saw the same force history.
|
||||
// Identical configs measured 0.23 apart because of it.
|
||||
//
|
||||
BeginStep();
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->BeginStep();
|
||||
subsystemArray[i]->PerformTo(step_till);
|
||||
}
|
||||
}
|
||||
Simulation::PerformTo(step_till);
|
||||
step_till += fixed_step;
|
||||
}
|
||||
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->WatchAndWrite(update_stream);
|
||||
}
|
||||
}
|
||||
|
||||
SET_PERFORM_ENTITY();
|
||||
Simulation::WatchAndWrite(update_stream);
|
||||
Check_Fpu();
|
||||
CLEAR_PERFORM_ENTITY();
|
||||
CLEAR_PERFORM_SUBSYSTEMS();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i])
|
||||
|
||||
@@ -690,6 +690,23 @@ void
|
||||
DEBUG_STREAM << "." << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// The rate this gauge runs at, and which tier that puts it in. The
|
||||
// renderer walks a sixteen-step wheel and a gauge draws only on the
|
||||
// steps its rate names, so tier 4 is one turn of the wheel between
|
||||
// redraws - seconds, once a race has the passes down to a handful a
|
||||
// second. Without this the profile says how EXPENSIVE each gauge is
|
||||
// but not how RARELY it runs, and the second one is what makes a
|
||||
// display look stuck.
|
||||
//
|
||||
{
|
||||
char
|
||||
rate_buffer[32];
|
||||
|
||||
sprintf(rate_buffer, "%04x/t%d ", (unsigned) rate, DiscernTier());
|
||||
DEBUG_STREAM << rate_buffer << std::flush;
|
||||
}
|
||||
|
||||
if (profileCycles > 0)
|
||||
{
|
||||
Scalar
|
||||
|
||||
+100
-1
@@ -21,6 +21,31 @@
|
||||
BitTrace Gauge_Renderer("Gauge Renderer");
|
||||
#endif
|
||||
|
||||
//
|
||||
// How long a single background pass may spend drawing gauges, in
|
||||
// milliseconds. RP412GAUGESLICE tunes it; 0 restores the original
|
||||
// behaviour of exactly one gauge per pass.
|
||||
//
|
||||
static long
|
||||
GaugeSliceMs()
|
||||
{
|
||||
static long
|
||||
slice = -1L;
|
||||
|
||||
if (slice < 0L)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412GAUGESLICE");
|
||||
|
||||
slice = (setting != NULL) ? atol(setting) : 2L;
|
||||
if (slice < 0L)
|
||||
{
|
||||
slice = 0L;
|
||||
}
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
|
||||
//#######################################################################
|
||||
// Miscellaneous utilities
|
||||
//#######################################################################
|
||||
@@ -3672,6 +3697,60 @@ Logical
|
||||
Logical
|
||||
result;
|
||||
|
||||
//
|
||||
// RP412GAUGEPROFILE=<seconds> - dump the gauge profile on that
|
||||
// cadence. Off unless set.
|
||||
//
|
||||
// ProfileReport already exists and PROFILE_GAUGES is already on, so
|
||||
// the numbers are being collected whether anyone looks or not. It was
|
||||
// only reachable from F11 through the RIO controls mapper, which is
|
||||
// not the mapper a desktop player is running - so on PAD;KEYBOARD it
|
||||
// could not be reached at all. This gives it a way out.
|
||||
//
|
||||
// It reports every gauge with its rate, its tier, how many times it
|
||||
// ran and what it cost, then clears - so each dump covers the
|
||||
// interval since the last one rather than all of history.
|
||||
//
|
||||
{
|
||||
static long
|
||||
profileInterval = -1L;
|
||||
|
||||
if (profileInterval < 0L)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412GAUGEPROFILE");
|
||||
|
||||
profileInterval = (setting != NULL) ? atol(setting) : 0L;
|
||||
if (profileInterval < 0L)
|
||||
{
|
||||
profileInterval = 0L;
|
||||
}
|
||||
}
|
||||
if (profileInterval > 0L)
|
||||
{
|
||||
static Logical
|
||||
profileScheduled = False;
|
||||
static Time
|
||||
profileDue;
|
||||
|
||||
Time
|
||||
profileNow = Now();
|
||||
|
||||
if (!profileScheduled)
|
||||
{
|
||||
profileScheduled = True;
|
||||
profileDue = profileNow;
|
||||
profileDue += profileInterval * 1000L;
|
||||
}
|
||||
else if (profileDue < profileNow)
|
||||
{
|
||||
profileDue = profileNow;
|
||||
profileDue += profileInterval * 1000L;
|
||||
ProfileReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Time start, end;
|
||||
int oldTaskMode = taskMode;
|
||||
|
||||
@@ -3683,7 +3762,27 @@ Logical
|
||||
|
||||
case background:
|
||||
{
|
||||
result = ProcessOneActiveGauge();
|
||||
//-----------------------------------------------------------
|
||||
// Draw gauges until the slice is spent, rather than exactly
|
||||
// one per pass.
|
||||
//
|
||||
// The background loop is only guaranteed a single pass per
|
||||
// frame; it gets more only while time remains before the
|
||||
// frame is due. On a busy map the 3D foreground eats the
|
||||
// whole budget, so a cycle of ninety-odd gauges takes
|
||||
// ninety-odd frames to come round and the displays sit
|
||||
// frozen for seconds. Working to a slice makes the refresh
|
||||
// rate depend on elapsed time instead of on how much spare
|
||||
// frame there happened to be.
|
||||
//-----------------------------------------------------------
|
||||
Time slice_end = Now();
|
||||
slice_end += GaugeSliceMs();
|
||||
|
||||
do
|
||||
{
|
||||
result = ProcessOneActiveGauge();
|
||||
}
|
||||
while (result && taskMode == background && Now() < slice_end);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "munga.h"
|
||||
#pragma hdrstop
|
||||
|
||||
#include "inputscript.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
//##########################################################################
|
||||
// RP412INPUTSCRIPT - see the header for what and why. This file is the
|
||||
// how: a timeline of rows parsed once, held in a fixed array, evaluated
|
||||
// by walking to the last row at or before the asked-for time.
|
||||
//##########################################################################
|
||||
|
||||
namespace
|
||||
{
|
||||
enum { inputScriptMaxRows = 256 };
|
||||
|
||||
struct InputScriptRow
|
||||
{
|
||||
float t;
|
||||
float throttle;
|
||||
float stickX;
|
||||
float stickY;
|
||||
float pedals;
|
||||
};
|
||||
|
||||
InputScriptRow gRows[inputScriptMaxRows];
|
||||
int gRowCount = 0;
|
||||
int gLoaded = -1; // -1 not tried, 0 no script, 1 loaded
|
||||
Logical gArmed = False;
|
||||
Time gOrigin;
|
||||
|
||||
float ClampInto(float value, float low, float high)
|
||||
{
|
||||
if (value < low) return low;
|
||||
if (value > high) return high;
|
||||
return value;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
gLoaded = 0;
|
||||
|
||||
const char *path = getenv("RP412INPUTSCRIPT");
|
||||
if (path == NULL || *path == '\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = fopen(path, "rt");
|
||||
if (file == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "InputScript: cannot read '" << path
|
||||
<< "' - driving unscripted\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
char line[256];
|
||||
float last_t = -1.0f;
|
||||
while (fgets(line, sizeof(line), file) != NULL &&
|
||||
gRowCount < inputScriptMaxRows)
|
||||
{
|
||||
InputScriptRow row;
|
||||
if (sscanf(line, " %f %f %f %f %f",
|
||||
&row.t, &row.throttle, &row.stickX,
|
||||
&row.stickY, &row.pedals) != 5)
|
||||
{
|
||||
continue; // comments, blanks, ragged lines
|
||||
}
|
||||
//
|
||||
// Clamped HERE, not at sample time, so a script asking for
|
||||
// throttle 2.0 is corrected once and visibly rather than
|
||||
// silently every step - and the mapper's own Verify range
|
||||
// checks can never trip on scripted input.
|
||||
//
|
||||
row.throttle = ClampInto(row.throttle, 0.0f, 1.0f);
|
||||
row.stickX = ClampInto(row.stickX, -1.0f, 1.0f);
|
||||
row.stickY = ClampInto(row.stickY, -1.0f, 1.0f);
|
||||
row.pedals = ClampInto(row.pedals, -1.0f, 1.0f);
|
||||
if (row.t < last_t)
|
||||
{
|
||||
DEBUG_STREAM << "InputScript: row at t=" << row.t
|
||||
<< " is out of order - dropped\n" << std::flush;
|
||||
continue;
|
||||
}
|
||||
last_t = row.t;
|
||||
gRows[gRowCount++] = row;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
if (gRowCount > 0)
|
||||
{
|
||||
gLoaded = 1;
|
||||
DEBUG_STREAM << "InputScript: '" << path << "', " << gRowCount
|
||||
<< " row(s), last at t=" << gRows[gRowCount - 1].t
|
||||
<< "s\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "InputScript: '" << path
|
||||
<< "' held no usable rows - driving unscripted\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
RPInputScript_Active()
|
||||
{
|
||||
if (gLoaded < 0)
|
||||
{
|
||||
Load();
|
||||
}
|
||||
return (gLoaded == 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
void
|
||||
RPInputScript_Arm(const Time &origin)
|
||||
{
|
||||
if (!RPInputScript_Active())
|
||||
{
|
||||
return;
|
||||
}
|
||||
gOrigin = origin;
|
||||
gArmed = True;
|
||||
DEBUG_STREAM << "InputScript: armed at the green light\n" << std::flush;
|
||||
}
|
||||
|
||||
int
|
||||
RPInputScript_Sample(
|
||||
const Time &now,
|
||||
float *throttle_out,
|
||||
float *stick_x_out,
|
||||
float *stick_y_out,
|
||||
float *pedals_out
|
||||
)
|
||||
{
|
||||
if (!gArmed || gLoaded != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Scalar t = now - gOrigin;
|
||||
if (t < (Scalar) 0)
|
||||
{
|
||||
t = (Scalar) 0;
|
||||
}
|
||||
|
||||
//
|
||||
// The last row at or before t holds; before the first row, neutral.
|
||||
// A linear walk, but the list is tiny and already ordered.
|
||||
//
|
||||
const InputScriptRow *current = NULL;
|
||||
for (int i = 0; i < gRowCount; ++i)
|
||||
{
|
||||
if (gRows[i].t <= (float) t)
|
||||
{
|
||||
current = &gRows[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (current == NULL)
|
||||
{
|
||||
*throttle_out = 0.0f;
|
||||
*stick_x_out = 0.0f;
|
||||
*stick_y_out = 0.0f;
|
||||
*pedals_out = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
*throttle_out = current->throttle;
|
||||
*stick_x_out = current->stickX;
|
||||
*stick_y_out = current->stickY;
|
||||
*pedals_out = current->pedals;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
//##########################################################################
|
||||
// RP412INPUTSCRIPT - scripted analog input, on the simulation's clock.
|
||||
//
|
||||
// A race cannot be called deterministic until somebody DRIVES it, and a
|
||||
// human cannot drive the same lap twice. This feeds the four analog
|
||||
// channels the controls mapper interprets - throttle, stick X/Y, pedals -
|
||||
// from a timeline file instead, evaluated against the mapper's own step
|
||||
// clock, so the same script produces the same race at any frame rate.
|
||||
//
|
||||
// The file named by RP412INPUTSCRIPT= holds one row per change:
|
||||
//
|
||||
// # t throttle stickX stickY pedals
|
||||
// 0.0 0.0 0 0 0
|
||||
// 2.0 1.0 0 0 0
|
||||
// 6.0 1.0 0.5 0 0
|
||||
//
|
||||
// Times are seconds of SIMULATION time from the green light. Each row
|
||||
// HOLDS until the next row's time - a step function, no interpolation,
|
||||
// because interpolation would sample differently at different physics
|
||||
// rates and the whole point is that nothing does.
|
||||
//
|
||||
// Armed by the green-light anchor in Application::ExecuteForeground (the
|
||||
// same instant RP412PHYSTRACE stops the pod dead), so the script clock,
|
||||
// the trace clock and the vehicle's state all start together.
|
||||
//
|
||||
// Test harness: off unless the environment names a file, costs nothing
|
||||
// when off, and it would be a cheat in a real race.
|
||||
//##########################################################################
|
||||
|
||||
class Time;
|
||||
|
||||
// is a script named and readable? (parsed once, on first ask)
|
||||
int
|
||||
RPInputScript_Active();
|
||||
|
||||
// the green light: script time zero is this instant
|
||||
void
|
||||
RPInputScript_Arm(const Time &origin);
|
||||
|
||||
// evaluate at 'now' (a simulation clock, normally GetLastPerformance()).
|
||||
// Returns 0 before Arm or with no script - callers leave their own
|
||||
// values alone. Outputs are clamped to the mapper's legal ranges.
|
||||
int
|
||||
RPInputScript_Sample(
|
||||
const Time &now,
|
||||
float *throttle_out, // 0..1
|
||||
float *stick_x_out, // -1..1
|
||||
float *stick_y_out, // -1..1
|
||||
float *pedals_out // -1..1
|
||||
);
|
||||
+13
-1
@@ -411,8 +411,20 @@ void
|
||||
// supposed to
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Doorframes are exempt: they are clockwork, computed identically on
|
||||
// every machine from the mission clock, so each host builds its own
|
||||
// Hermit copy instead of one host owning it and replicating. That
|
||||
// also means they survive a peer dropping, which owned doors do not -
|
||||
// ownership transfer is not implemented. Note this changes how many
|
||||
// times the cursor below is advanced, so old and new builds deal the
|
||||
// remaining map entities differently: they cannot share a session.
|
||||
//
|
||||
Logical post_make_message = True;
|
||||
if (Entity::EntityFlagsIsMap(message->instanceFlags))
|
||||
if (
|
||||
Entity::EntityFlagsIsMap(message->instanceFlags)
|
||||
&& message->classToCreate != DoorFrameClassID
|
||||
)
|
||||
{
|
||||
Check(application);
|
||||
HostManager *host_manager = application->GetHostManager();
|
||||
|
||||
+40
-1
@@ -658,9 +658,48 @@ Bye_Bye:
|
||||
//
|
||||
//-----------------------------------------------
|
||||
// Make sure the position quaternion stays stable
|
||||
//
|
||||
// Frame-counting, so it only runs on the frame-coupled path - fixed
|
||||
// steps do the same thing in BeginStep, counted in STEPS, because
|
||||
// "every 20 frames" lands at a different point of the step sequence
|
||||
// on every machine and rounding at different points is drift.
|
||||
//-----------------------------------------------
|
||||
//
|
||||
if (++normalizeCount == 20)
|
||||
if (Simulation::FixedStep() <= (Scalar) 0 && ++normalizeCount >= 20)
|
||||
{
|
||||
localOrigin.angularPosition.Normalize();
|
||||
normalizeCount = 0;
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -265,6 +265,14 @@ protected:
|
||||
MemoryStream *update_stream
|
||||
);
|
||||
|
||||
//
|
||||
// Per-step set-up under fixed stepping: clears the force accumulator
|
||||
// the thrusters add into, so each step integrates only its own
|
||||
// forces. See the definition for the frame-jitter bug this closes.
|
||||
//
|
||||
void
|
||||
BeginStep();
|
||||
|
||||
int
|
||||
normalizeCount;
|
||||
Environment
|
||||
|
||||
+199
-11
@@ -621,9 +621,206 @@ void*
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// RP412PHYSICSHZ - the size of one simulation step, as a rate in hertz.
|
||||
//
|
||||
// The engine simulates TO a timestamp: every entity keeps a lastPerformance
|
||||
// marking how far it has been simulated, and PerformAndWatch hands Perform()
|
||||
// the difference. That difference used to be however long the last frame
|
||||
// took, which made the frame rate part of the physics - explicitly so, since
|
||||
// Mover scales its bounce and penetration thresholds by delta_t.
|
||||
//
|
||||
// Advancing lastPerformance in fixed steps instead makes it the accumulator
|
||||
// a fixed-step loop needs, and every Perform() in the game gets an identical
|
||||
// dt without one of them being touched.
|
||||
//
|
||||
// 0 restores the old behaviour for comparison. The RATE is a game-feel
|
||||
// decision, not a technical one: thirty years of handling constants were
|
||||
// tuned against the DOS build's 40 ms steps, and RP412 has been running
|
||||
// ~18 ms variable ones, so the feel has already drifted. Whatever is chosen
|
||||
// here becomes the canonical physics for pods and PCs alike.
|
||||
//#############################################################################
|
||||
|
||||
static Scalar
|
||||
FixedPhysicsStep()
|
||||
{
|
||||
static Scalar
|
||||
step = (Scalar) -1;
|
||||
|
||||
if (step < (Scalar) 0)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412PHYSICSHZ");
|
||||
|
||||
//
|
||||
// 50 Hz is the default: a 20 ms step, exact on the millisecond
|
||||
// clock, and the rate whose settled hover ride height measured
|
||||
// closest to the frame-coupled physics the game has always run.
|
||||
// Proven before it was defaulted - a scripted lap with a crash,
|
||||
// a burn and two respawns runs bit-identical at 30, 60 and 144
|
||||
// fps, and identical runs reproduce exactly. 0 restores the
|
||||
// original frame-coupled behaviour, where the frame rate is
|
||||
// part of the physics.
|
||||
//
|
||||
int rate = (setting != NULL) ? atoi(setting) : 50;
|
||||
|
||||
//
|
||||
// Guard the arithmetic rather than the taste: a rate below the
|
||||
// frame rate is a legitimate choice (the pods ran at 25), but a
|
||||
// step of zero or a negative one is not a choice at all.
|
||||
//
|
||||
if (rate < 0)
|
||||
{
|
||||
rate = 0;
|
||||
}
|
||||
if (rate > 1000)
|
||||
{
|
||||
rate = 1000;
|
||||
}
|
||||
step = (rate > 0) ? ((Scalar) 1 / (Scalar) rate) : (Scalar) 0;
|
||||
|
||||
DEBUG_STREAM << "Physics: ";
|
||||
if (rate > 0)
|
||||
{
|
||||
DEBUG_STREAM << "fixed step, " << rate << " Hz";
|
||||
|
||||
//
|
||||
// The engine's clock counts MILLISECONDS, so a step is
|
||||
// really round(1000/rate) ms. A rate that does not divide
|
||||
// 1000 evenly therefore runs at a neighbouring rate wearing
|
||||
// this one's name - 60 asks for 16.67 ms and gets 17, which
|
||||
// is 58.8 Hz. Say so, and name the rates that mean what
|
||||
// they say.
|
||||
//
|
||||
if ((1000 % rate) != 0)
|
||||
{
|
||||
int step_ms = (1000 + rate / 2) / rate;
|
||||
DEBUG_STREAM << " - NOT millisecond-exact, steps will run "
|
||||
<< step_ms << " ms (" << (1000.0f / (float) step_ms)
|
||||
<< " Hz). 25, 50 and 100 are exact";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "frame-coupled (RP412PHYSICSHZ=0)";
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
//
|
||||
// How far behind one frame may catch up: a quarter second of simulation,
|
||||
// whatever the rate - enough to ride out a texture load or an alt-tab,
|
||||
// short of letting a stalled machine spiral. Counted in steps because the
|
||||
// loop is, so 6 steps at 25 Hz, 12 at 50, 25 at 100.
|
||||
//
|
||||
static int
|
||||
MaximumCatchUpSteps(Scalar step)
|
||||
{
|
||||
int steps = (int)((Scalar) 0.25 / step);
|
||||
return (steps < 4) ? 4 : steps;
|
||||
}
|
||||
|
||||
// how many fixed steps the whole simulation has taken - the trace prints it,
|
||||
// so 'is the step actually fixed' is answered by measurement not by reading
|
||||
long gPhysicsStepsTaken = 0;
|
||||
|
||||
//#############################################################################
|
||||
// Simulation Support
|
||||
//
|
||||
Scalar
|
||||
Simulation::FixedStep()
|
||||
{
|
||||
return FixedPhysicsStep();
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::PerformTo(const Time& till)
|
||||
{
|
||||
Check(this);
|
||||
Check(&till);
|
||||
|
||||
Scalar step = FixedPhysicsStep();
|
||||
|
||||
if (step > (Scalar) 0)
|
||||
{
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// Fixed step. The simulation advances in whole steps of the same
|
||||
// size on every machine, and whatever is left over waits for the
|
||||
// next frame - lastPerformance is the accumulator, and always was.
|
||||
//
|
||||
// Before this, the slice was simply however long the last frame
|
||||
// took, so a 30 fps machine integrated gravity in 33 ms steps and
|
||||
// a 144 fps machine in 7 ms ones. Nothing in any Perform()
|
||||
// changes: it is handed a dt it can rely on instead of one that
|
||||
// depended on the graphics card.
|
||||
//
|
||||
// NOTE the caller decides the interleaving. An entity's spring
|
||||
// forces are computed from its subsystems (the VTV reads its
|
||||
// thrusters' measured heights), so the subsystems and the entity
|
||||
// must advance TOGETHER, one step at a time -
|
||||
// Entity::PerformAndWatch owns that loop and hands everyone the
|
||||
// same sub-frame 'till'. Stepping a subsystem all the way to the
|
||||
// frame boundary before its owner moves at all is how the first
|
||||
// attempt at this produced a pod that climbed at 30 fps and flew
|
||||
// level at 144: two spring impulses from one stale height sample.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
Scalar behind = till - lastPerformance;
|
||||
int taken = 0;
|
||||
int max_steps = MaximumCatchUpSteps(step);
|
||||
|
||||
while (behind >= step && taken < max_steps)
|
||||
{
|
||||
Perform(step);
|
||||
++gPhysicsStepsTaken;
|
||||
lastPerformance += step;
|
||||
behind -= step;
|
||||
++taken;
|
||||
}
|
||||
|
||||
//
|
||||
// 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::WatchAndWrite(MemoryStream *update_stream)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (!AreWatchersDelayed())
|
||||
{
|
||||
ExecuteWatchers();
|
||||
}
|
||||
WriteSimulationUpdate(update_stream);
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
void
|
||||
Simulation::PerformAndWatch(
|
||||
const Time& till,
|
||||
@@ -633,17 +830,8 @@ void
|
||||
Check(this);
|
||||
Check(&till);
|
||||
|
||||
Scalar slice = till - lastPerformance;
|
||||
lastPerformance = till;
|
||||
|
||||
Perform(slice);
|
||||
if (!AreWatchersDelayed())
|
||||
{
|
||||
ExecuteWatchers();
|
||||
}
|
||||
WriteSimulationUpdate(update_stream);
|
||||
|
||||
Check_Fpu();
|
||||
PerformTo(till);
|
||||
WatchAndWrite(update_stream);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -147,6 +147,36 @@ public:
|
||||
MemoryStream *update_stream
|
||||
);
|
||||
|
||||
//
|
||||
// The two halves of PerformAndWatch, so an ENTITY can interleave its
|
||||
// subsystems' physics with its own, step by step, and still run the
|
||||
// watchers and the update stream once per frame. PerformTo advances
|
||||
// the simulation to the given time - in fixed steps when
|
||||
// RP412PHYSICSHZ names a rate, in one variable slice otherwise.
|
||||
//
|
||||
void
|
||||
PerformTo(const Time& till);
|
||||
void
|
||||
WatchAndWrite(MemoryStream *update_stream);
|
||||
|
||||
//
|
||||
// Called by the entity interleave at the TOP of every fixed step,
|
||||
// before any subsystem adds its forces for that step. Per-frame set-up
|
||||
// work - clearing a force accumulator, deriving local velocity from
|
||||
// world state - belongs here when the fixed step is on, because "once
|
||||
// per frame" is a wall-clock cadence and the whole point is that wall
|
||||
// clock no longer reaches the physics. Default: nothing.
|
||||
//
|
||||
virtual void
|
||||
BeginStep();
|
||||
|
||||
//
|
||||
// The fixed step in seconds, 0 when frame-coupled. Global on purpose:
|
||||
// a mixed-rate simulation would be a worse bug than either mode.
|
||||
//
|
||||
static Scalar
|
||||
FixedStep();
|
||||
|
||||
void
|
||||
DoNothingOnce(Scalar time_slice);
|
||||
void
|
||||
@@ -155,6 +185,9 @@ public:
|
||||
void
|
||||
SetLastPerformance(const Time& when)
|
||||
{Check(this); Check(&when); lastPerformance = when;}
|
||||
const Time&
|
||||
GetLastPerformance() const
|
||||
{Check(this); return lastPerformance;}
|
||||
void
|
||||
RequestEncore(Encore encore);
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
|
||||
@@ -82,6 +82,10 @@ public:
|
||||
// -fit: borderless window filling the monitor, with the render size
|
||||
// chosen to match the cockpit canvas it will be presented into.
|
||||
static bool GetFitDisplay() { return mFitDisplay; }
|
||||
// -fit's borderless full-monitor placement. Applied once at startup so
|
||||
// the window is in its final shape before ANY mission builds a device
|
||||
// against it - see the definition for why the first race differed.
|
||||
static void FitWindowToMonitor(HWND window);
|
||||
static Logical GetSeeSolids() { return seeSolids; }
|
||||
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
|
||||
// The front end's multiplayer path turns network mode on at launch
|
||||
|
||||
@@ -302,6 +302,72 @@ void
|
||||
<< monitor_w << "x" << monitor_h << " monitor\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// FitWindowToMonitor
|
||||
//#############################################################################
|
||||
//
|
||||
// -fit's borderless full-monitor placement, applied to the shell window.
|
||||
//
|
||||
// This has to happen BEFORE the first race, not during it. SVGA16 does the
|
||||
// same thing when it assembles the cockpit, but that is not until a mission
|
||||
// starts - and the D3D device is created just ahead of it, against whatever
|
||||
// the window is at that moment. So the first race got a device sized to a
|
||||
// still-bordered window and every race after it got one sized to the
|
||||
// borderless monitor: two different render targets, two different frame
|
||||
// costs, from one lobby and one set of settings.
|
||||
//
|
||||
// A racing sim cannot have that. The window reaches its final shape while
|
||||
// the front end is still up, so every mission of a session - the first one
|
||||
// included - is set up against exactly the same client area.
|
||||
//
|
||||
// SVGA16 still applies it when it builds the cockpit. That call becomes a
|
||||
// no-op rather than a change, which is the point.
|
||||
//
|
||||
void
|
||||
L4Application::FitWindowToMonitor(HWND window)
|
||||
{
|
||||
if (window == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RECT monitor_rect;
|
||||
monitor_rect.left = 0;
|
||||
monitor_rect.top = 0;
|
||||
monitor_rect.right = GetSystemMetrics(SM_CXSCREEN);
|
||||
monitor_rect.bottom = GetSystemMetrics(SM_CYSCREEN);
|
||||
|
||||
MONITORINFO monitor;
|
||||
memset(&monitor, 0, sizeof(monitor));
|
||||
monitor.cbSize = sizeof(monitor);
|
||||
HMONITOR handle = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
|
||||
if (GetMonitorInfoA(handle, &monitor))
|
||||
{
|
||||
monitor_rect = monitor.rcMonitor;
|
||||
}
|
||||
|
||||
//
|
||||
// Same style surgery SVGA16 performs, so the two agree exactly.
|
||||
//
|
||||
LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE);
|
||||
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
|
||||
WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME);
|
||||
style |= WS_POPUP | WS_CLIPCHILDREN;
|
||||
SetWindowLongPtrA(window, GWL_STYLE, style);
|
||||
|
||||
SetWindowPos(window, NULL,
|
||||
monitor_rect.left, monitor_rect.top,
|
||||
monitor_rect.right - monitor_rect.left,
|
||||
monitor_rect.bottom - monitor_rect.top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||||
|
||||
DEBUG_STREAM << "L4Application: -fit placed the window borderless at "
|
||||
<< (monitor_rect.right - monitor_rect.left) << "x"
|
||||
<< (monitor_rect.bottom - monitor_rect.top)
|
||||
<< " before the first mission\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// ParseCommandLine
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "l4audio.h"
|
||||
#include "l4audefx.h"
|
||||
#include "l4audlvl.h"
|
||||
#include "l4app.h"
|
||||
#include "l4audrnd.h"
|
||||
@@ -9,6 +10,49 @@
|
||||
#include "..\munga\player.h"
|
||||
#include "..\rp\vtv.h"
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md): the AWE32 played each patch at the requested MIDI
|
||||
// note relative to the sample root (60). RP's authored 4.10 content predates
|
||||
// NoteAudioControlID -- its AudioControlID enum stops at AttackTimeAudioControlID
|
||||
// -- so every source runs at DEFAULT_NOTE and this factor is 1.0 today. It is
|
||||
// applied anyway so the pitch path is complete if authored notes ever appear,
|
||||
// and to keep the shared MUNGA engine in step with the BT tree.
|
||||
//
|
||||
static inline float RPNotePitchFactor(int note_value)
|
||||
{
|
||||
return (float)pow(2.0, ((double)note_value - 60.0) / 12.0);
|
||||
}
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F12): the authored DirectPatchSource `position=`
|
||||
// enum picked a SOUND CARD (front pair for Front/FrontLeft/FrontRight, rear
|
||||
// pair for Rear/RearLeft/RearRight) and a MIDI pan (CC10 centre/left/right).
|
||||
// The port read audioPosition from the stream and then discarded it -- every
|
||||
// cockpit sound played dead centre because SetupPatch pins each source
|
||||
// AL_SOURCE_RELATIVE at the origin.
|
||||
//
|
||||
// Sources are listener-relative and no AL_ORIENTATION is ever set, so OpenAL's
|
||||
// default listener frame applies: facing -Z with +Y up. Front is therefore
|
||||
// -Z, rear +Z, left -X, right +X; the corner values combine both at equal
|
||||
// weight. RP's own content only ever authors Front (28 sites) and Rear (13),
|
||||
// but the corners are mapped for completeness since the enum allows them.
|
||||
//
|
||||
static void RPGetDirectPatchPosition(DirectPatchPosition p, float *x, float *z)
|
||||
{
|
||||
const float diag = 0.7071068f; // unit vector split across both axes
|
||||
|
||||
switch (p)
|
||||
{
|
||||
case FrontDirectPatchPosition: *x = 0.0f; *z = -1.0f; break;
|
||||
case RearDirectPatchPosition: *x = 0.0f; *z = 1.0f; break;
|
||||
case FrontLeftDirectPatchPosition: *x = -diag; *z = -diag; break;
|
||||
case FrontRightDirectPatchPosition: *x = diag; *z = -diag; break;
|
||||
case RearLeftDirectPatchPosition: *x = -diag; *z = diag; break;
|
||||
case RearRightDirectPatchPosition: *x = diag; *z = diag; break;
|
||||
default: *x = 0.0f; *z = 0.0f; break;
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//####################### L4AudioSpatialization #########################
|
||||
//#############################################################################
|
||||
@@ -658,6 +702,19 @@ L4AudioSource::L4AudioSource(
|
||||
AudioSource(stream, entity)
|
||||
{
|
||||
channelSet.count = GetAudioVoiceCount();
|
||||
|
||||
//
|
||||
// sources[] was left uninitialized here, and RequestAudioChannels decides
|
||||
// whether a slot already holds a source by asking alIsSource about it.
|
||||
// Garbage that happened to match a live name meant silently sharing another
|
||||
// source -- a real hazard now that the pool recycles small integer names.
|
||||
// 0 is never a valid AL name.
|
||||
//
|
||||
for (int i = 0; i < (int)(sizeof(channelSet.sources) / sizeof(channelSet.sources[0])); i++)
|
||||
{
|
||||
channelSet.sources[i] = 0;
|
||||
}
|
||||
|
||||
L4AudioSourceX();
|
||||
}
|
||||
|
||||
@@ -923,6 +980,22 @@ void
|
||||
patch_resource->SetDistance(GetDistanceToSource());
|
||||
patch_resource->SetupPatch(channelSet);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F12): place the source per the authored position
|
||||
// enum. SetupPatch has just pinned it AL_SOURCE_RELATIVE at the origin, so
|
||||
// this must run after it. With AL_NONE as the distance model the unit
|
||||
// radius costs no attenuation -- it only supplies direction.
|
||||
//
|
||||
{
|
||||
float pos_x, pos_z;
|
||||
|
||||
RPGetDirectPatchPosition(audioPosition, &pos_x, &pos_z);
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
alSource3f(channelSet.sources[i], AL_POSITION, pos_x, 0.0f, pos_z);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Set the channel to default control values
|
||||
//
|
||||
@@ -1039,6 +1112,8 @@ void
|
||||
// Apply filter scale
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
float direct_gainhf = 1.0f;
|
||||
|
||||
if (UseSourceBrightnessScale())
|
||||
{
|
||||
const MIDINRPNValue filter_resolution = 2;// HACK - should come from audio.ini
|
||||
@@ -1058,6 +1133,32 @@ void
|
||||
{
|
||||
lastMIDIFilterCutoff = midi_filter_cutoff;
|
||||
}
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9): this block previously computed the AWE
|
||||
// initial-filter-cutoff (NRPN 21) and then only updated its own
|
||||
// bookkeeping member -- the cutoff was never applied to anything, so
|
||||
// authored brightness (ctl 5) was inert. Route it through EFX instead.
|
||||
// Direct sources take brightness alone; the distance rolloff belongs to
|
||||
// the 3D paths.
|
||||
//
|
||||
direct_gainhf = EFX_CutoffScaleToGainHF(
|
||||
(float)midi_filter_cutoff / (float)MIDI_MAX_CONTROL_VALUE
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Applied OUTSIDE the brightness gate: a source that does not use brightness
|
||||
// still has to be told, because the same call carries the player's bass trim.
|
||||
// At unity on both axes it detaches the filter, so this costs nothing in the
|
||||
// default configuration.
|
||||
//
|
||||
if (EFX_Available())
|
||||
{
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
EFX_SetSourceLowpassGainHF(channelSet.sources[i], direct_gainhf);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1069,17 +1170,24 @@ void
|
||||
const MIDIValue volume_resolution = 2; // HACK - should come from audio.ini
|
||||
|
||||
volume_scale = CalculateSourceVolumeScale();
|
||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
||||
Check(application);
|
||||
L4AudioRenderer *audio_renderer =
|
||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
||||
Check(audio_renderer);
|
||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
||||
Check(audio_head);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F4): the original ended its volume path in MIDI
|
||||
// CC7, whose GM/SoundFont curve is concave -- amplitude ~ (v/127)^2. Writing
|
||||
// volume_scale linearly to AL_GAIN played every intermediate level about
|
||||
// +6 dB hot at mid-scale and compressed the authored dynamic range.
|
||||
//
|
||||
// AL_MAX_DISTANCE is no longer written here: the distance model is AL_NONE
|
||||
// (see MUNGA/AUDIO.cpp) so it has no effect, and DirectPatch is the
|
||||
// non-positional cockpit path which never took distance attenuation anyway.
|
||||
//
|
||||
const float direct_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||
PatchResource *direct_patch = Cast_Object(PatchResource*, GetAudioResource());
|
||||
for (int i=0; i < channelSet.count; i++)
|
||||
{
|
||||
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
|
||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN,
|
||||
volume_scale * volume_scale * direct_patch->GetZoneBassGain(i));
|
||||
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * direct_note_pitch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1206,6 +1314,17 @@ void
|
||||
patch_resource->SetDistance(GetDistanceToSource());
|
||||
patch_resource->SetupPatch(channelSet);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F11): wet exterior. The original sent CC91 =
|
||||
// global_reverb_scale on all four channels of a 3D source and CC91 = 0 on
|
||||
// the cockpit DirectPatch channels -- a deliberate outside/inside contrast
|
||||
// that the port lost when every send site was commented out.
|
||||
//
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
EFX_AttachReverbSend(channelSet.sources[i]);
|
||||
}
|
||||
|
||||
/*patch_resource->SetDistance(GetDistanceToSource());
|
||||
for (i = 0; i < AudioChannelSetSize; i++)
|
||||
{
|
||||
@@ -1405,15 +1524,71 @@ void
|
||||
|
||||
pitch_offset = CalculateSourcePitchOffset();
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F10): add the AUTHORED doppler. AUDIO.INI's
|
||||
// doppler_range=600 / speed_of_sound=250 are computed into
|
||||
// AudioLocation::dopplerCents on every spatial update, and the original
|
||||
// applied it on this dynamic path only -- static and direct sources stayed
|
||||
// doppler-free. GetDopplerCents() previously had no callers at all.
|
||||
//
|
||||
pitch_offset += GetAudioLocation()->GetDopplerCents();
|
||||
|
||||
double relativePitch = pow(2.0,pitch_offset/1200.0);
|
||||
Clamp(relativePitch,0.5,2.0);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md): relativePitch was computed here and never
|
||||
// applied -- there was no AL_PITCH call anywhere in the tree, so the whole
|
||||
// authored pitch chain (pitch_mix_offset / PitchAudioControlID, authored 97
|
||||
// times across RP's sequences) was inert along with doppler.
|
||||
//
|
||||
// AL_VELOCITY is still written for bookkeeping but is now inert: doppler
|
||||
// factor is 0 (see MUNGA/AUDIO.cpp) because this feed is sign-inverted
|
||||
// relative to the AL_POSITION frame and never subtracted head velocity.
|
||||
// AL_MAX_DISTANCE is dropped -- the distance model is AL_NONE and the
|
||||
// authored curve is applied in CalculateSourceVolumeScale instead.
|
||||
//
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9): the AUTHORED high-frequency rolloff. The
|
||||
// original drove the AWE filter cutoff on this path from
|
||||
// highFreqCutoffScale x brightnessScale, ungated, on all four quadrant
|
||||
// channels -- every moving 3D sound got duller with distance. AUDIO.INI
|
||||
// still computes highFreqCutoffScale each frame (rolloff 2.0, knee 60,
|
||||
// scale 0.005) and GetHighFreqCutoffScale() previously had zero callers.
|
||||
//
|
||||
float dynamic_gainhf = 1.0f;
|
||||
|
||||
if (EFX_Available())
|
||||
{
|
||||
PatchResource *filter_patch =
|
||||
Cast_Object(PatchResource*, GetAudioResource());
|
||||
Check(filter_patch);
|
||||
|
||||
Scalar filter_scale =
|
||||
GetAudioLocation()->GetHighFreqCutoffScale() *
|
||||
CalculateSourceBrightnessScale();
|
||||
Scalar max_cutoff = (Scalar)filter_patch->GetMaxMIDIFilterCutoff();
|
||||
Scalar midi_cutoff = filter_scale * max_cutoff;
|
||||
|
||||
dynamic_gainhf = EFX_CutoffScaleToGainHF(
|
||||
(float)(midi_cutoff / (Scalar)MIDI_MAX_CONTROL_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
const float dynamic_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||
PatchResource *dynamic_patch = Cast_Object(PatchResource*, GetAudioResource());
|
||||
for (int i=0; i < channelSet.count; i++)
|
||||
{
|
||||
alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN,
|
||||
volume_scale * volume_scale * dynamic_patch->GetZoneBassGain(i));
|
||||
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * dynamic_note_pitch);
|
||||
alSource3f(channelSet.sources[i],AL_VELOCITY,-relative_velocity.x,-relative_velocity.y,-relative_velocity.z);
|
||||
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
|
||||
|
||||
if (EFX_Available())
|
||||
{
|
||||
EFX_SetSourceLowpassGainHF(channelSet.sources[i], dynamic_gainhf);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1432,22 +1607,25 @@ AudioControlValue
|
||||
//
|
||||
Scalar
|
||||
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||
return volume_scale;
|
||||
|
||||
//
|
||||
// Update the spatial model that will result in the value
|
||||
// for distance related volume attenuation
|
||||
// FIDELITY (docs/SOUND.md F3): apply the AUTHORED distance attenuation.
|
||||
// AUDIO.INI's knee/rolloff curve (amplitude_rolloff=2.0, knee=60,
|
||||
// distance_scale=0.003, clipping_radius=550) is computed into
|
||||
// distanceVolumeScale on every spatial update; this multiply was commented
|
||||
// out behind an early return and AL_LINEAR_DISTANCE substituted, which faded
|
||||
// distant audio on a straight line to zero instead of the authored
|
||||
// 1/(1+(k(d-knee))^2). Restoring it also un-blinds the volume-based
|
||||
// transient cull, the AudioWeighting voice-steal, and the CalculateMix
|
||||
// ducking chain, all of which key off this value and were treating far
|
||||
// sources as full-presence.
|
||||
//
|
||||
/*Check(application);
|
||||
Check(application->GetAudioRenderer());
|
||||
UpdateSpatialModel(application->GetAudioRenderer()->GetAudioHead());
|
||||
|
||||
//
|
||||
// Apply distance attenuation to the volume scale
|
||||
// The spatial model is already refreshed each Execute, so the
|
||||
// UpdateSpatialModel call the original comment carried is not needed here.
|
||||
//
|
||||
Check(GetAudioLocation());
|
||||
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||
return volume_scale;*/
|
||||
return volume_scale;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
@@ -1468,6 +1646,26 @@ Static3DPatchSource::Static3DPatchSource(
|
||||
MemoryStream_Read(stream, &useInternalSpatialization);
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
//#############################################################################
|
||||
//
|
||||
AudioControlValue
|
||||
Static3DPatchSource::CalculateSourceVolumeScale()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F3): same authored distance attenuation as the
|
||||
// dynamic path. The spatial model computes distanceVolumeScale on every
|
||||
// execute; without this multiply statics were left to AL_LINEAR_DISTANCE.
|
||||
//
|
||||
Scalar volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||
Check(GetAudioLocation());
|
||||
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||
return volume_scale;
|
||||
}
|
||||
|
||||
Logical Static3DPatchSource::IsAudioSourceClipped(AudioHead *audio_head)
|
||||
{
|
||||
if (AudioSource::IsAudioSourceClipped(audio_head) || l4_application->GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() == VTV::BurningState)
|
||||
@@ -1694,6 +1892,16 @@ void
|
||||
Check(patch_resource);
|
||||
patch_resource->SetDistance(GetDistanceToSource());
|
||||
patch_resource->SetupPatch(channelSet);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F11): statics are exterior sources too, so they
|
||||
// take the same wet send as the dynamic path.
|
||||
//
|
||||
for (int i = 0; i < channelSet.count; i++)
|
||||
{
|
||||
EFX_AttachReverbSend(channelSet.sources[i]);
|
||||
}
|
||||
|
||||
/*for (i = 0; i < AudioChannelSetSize; i++)
|
||||
{
|
||||
if ((channel = channelSet.GetNth(i)) != NULL)
|
||||
@@ -1909,12 +2117,6 @@ void
|
||||
|
||||
Scalar volume_scale = CalculateSourceVolumeScale();
|
||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
||||
Check(application);
|
||||
L4AudioRenderer *audio_renderer =
|
||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
||||
Check(audio_renderer);
|
||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
||||
Check(audio_head);
|
||||
|
||||
Scalar pitch_offset;
|
||||
|
||||
@@ -1933,12 +2135,47 @@ void
|
||||
relative_position = audio_location->GetVectorToSource();
|
||||
}
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F4 + pitch): squared CC7 volume law, and the
|
||||
// authored pitch chain applied -- see the DirectPatch/Dynamic3D paths. The
|
||||
// original left static sources doppler-free, so no doppler term here.
|
||||
// AL_MAX_DISTANCE dropped with the AL_NONE distance model; the authored
|
||||
// curve is applied in CalculateSourceVolumeScale.
|
||||
//
|
||||
//Static models have their position freely available as relative positions and stand still
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9): statics took brightness alone in the
|
||||
// original -- no distance term on this path.
|
||||
//
|
||||
float static_gainhf = 1.0f;
|
||||
|
||||
if (EFX_Available() && UseSourceBrightnessScale())
|
||||
{
|
||||
PatchResource *filter_patch =
|
||||
Cast_Object(PatchResource*, GetAudioResource());
|
||||
Check(filter_patch);
|
||||
|
||||
Scalar midi_cutoff =
|
||||
CalculateSourceBrightnessScale() *
|
||||
(Scalar)filter_patch->GetMaxMIDIFilterCutoff();
|
||||
|
||||
static_gainhf = EFX_CutoffScaleToGainHF(
|
||||
(float)(midi_cutoff / (Scalar)MIDI_MAX_CONTROL_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
const float static_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||
for (int i=0; i < channelSet.count; i++)
|
||||
{
|
||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
||||
alSourcef(channelSet.sources[i], AL_GAIN,
|
||||
volume_scale * volume_scale * patch_resource->GetZoneBassGain(i));
|
||||
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * static_note_pitch);
|
||||
alSource3f(channelSet.sources[i],AL_POSITION,relative_position.x,relative_position.y,relative_position.z);
|
||||
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
|
||||
|
||||
if (EFX_Available())
|
||||
{
|
||||
EFX_SetSourceLowpassGainHF(channelSet.sources[i], static_gainhf);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -541,6 +541,13 @@ public:
|
||||
|
||||
virtual Logical IsAudioSourceClipped(AudioHead *audio_head);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Mix levels
|
||||
//
|
||||
public:
|
||||
AudioControlValue
|
||||
CalculateSourceVolumeScale();
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// SetPosition
|
||||
//
|
||||
|
||||
+34
-1
@@ -128,8 +128,25 @@ void
|
||||
// #endif
|
||||
SAMPLEINFO info;
|
||||
|
||||
//
|
||||
// Ask this patch for no more zones than it has.
|
||||
//
|
||||
// sourceSet.count was fixed when the audio source was built, from
|
||||
// whichever level of detail was selected at the time. SetDistance
|
||||
// re-picks the level of detail by distance immediately before this
|
||||
// runs (see Static3DPatchSource::StartImplementation), and a
|
||||
// further-away patch can have fewer zones than the one the source was
|
||||
// sized for - so the count outruns this patch's zone list, and the
|
||||
// zones past the end come back as "no such zone".
|
||||
//
|
||||
int zone_count = PRESET_getNumSamples(bankID,patchID);
|
||||
if (zone_count > sourceSet.count)
|
||||
{
|
||||
zone_count = sourceSet.count;
|
||||
}
|
||||
|
||||
//Attach buffers
|
||||
for (int i=0; i < sourceSet.count; i++)
|
||||
for (int i=0; i < zone_count; i++)
|
||||
{
|
||||
info = PRESET_getSampleInfo(bankID,patchID,i);
|
||||
if (info.bufferIndex >= 0)
|
||||
@@ -310,3 +327,19 @@ MIDINRPNValue
|
||||
Check(patch_level_of_detail);
|
||||
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
//#############################################################################
|
||||
//
|
||||
float
|
||||
PatchResource::GetZoneBassGain(int zone_index)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
PatchLevelOfDetail *patch_level_of_detail =
|
||||
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
||||
|
||||
Check(patch_level_of_detail);
|
||||
return patch_level_of_detail->GetZoneBassGain(zone_index);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ struct PRESETINFO
|
||||
|
||||
extern PRESETINFO allPresets[2][100];
|
||||
|
||||
//
|
||||
// Defined in L4AUDRES.cpp; declared here rather than including that header so
|
||||
// the level-of-detail and resource headers stay independent of each other.
|
||||
//
|
||||
float RPBufferBassGain(int buffer_index);
|
||||
|
||||
bool PRESET_isImplemented(int bank, int preset);
|
||||
int PRESET_getNumSamples(int bank, int preset);
|
||||
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
|
||||
@@ -69,6 +75,14 @@ public:
|
||||
GetVoiceCount()
|
||||
{return PRESET_getNumSamples(bankID,patchID);}
|
||||
|
||||
//
|
||||
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||
//
|
||||
float
|
||||
GetZoneBassGain(int zone_index)
|
||||
{return RPBufferBassGain(
|
||||
PRESET_getSampleInfo(bankID,patchID,zone_index).bufferIndex);}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------------
|
||||
// BuildFromPage
|
||||
@@ -163,6 +177,12 @@ public:
|
||||
void
|
||||
SetupPatch(SourceSet sourceSet);
|
||||
|
||||
//
|
||||
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||
//
|
||||
float
|
||||
GetZoneBassGain(int zone_index);
|
||||
|
||||
MIDINRPNValue
|
||||
GetMaxMIDIFilterCutoff();
|
||||
};
|
||||
|
||||
@@ -18,6 +18,130 @@
|
||||
ALuint *g_buffers;
|
||||
int g_numBuffers;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Bass trim ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// RP412AUDIOBASS, 0.0..1.0, default 1.0 (the mix exactly as authored), stepped
|
||||
// live by the Home/End keys.
|
||||
//
|
||||
// The arcade pod ran the game at unity and did its volume and tone shaping in
|
||||
// hardware -- an external amplifier and a 3-way crossover. A desktop player has
|
||||
// neither, so the low band needs a control in software. This is the crossover's
|
||||
// low trim; the master volume (L4AUDRND.cpp) is the amplifier's.
|
||||
//
|
||||
// It cannot be an EFX filter: the OpenAL this game ships implements only
|
||||
// AL_FILTER_LOWPASS, so there is no low shelf or bandpass to lean on, and the
|
||||
// one direct filter a source gets is already carrying the authored brightness
|
||||
// model. So the trim is a GAIN, applied per zone in the mix.
|
||||
//
|
||||
// That works because of HOW the low end is built. RP's soundbanks carry their
|
||||
// weight in discrete deep layer zones whose per-zone tuning bakes out to a very
|
||||
// low playback rate -- 13 zones sit below 8 kHz, between 3.4 and 5.2 octaves
|
||||
// below their recorded pitch, against 81% of the set at 22 kHz and up. A zone's
|
||||
// baked rate is therefore a reliable proxy for which band it occupies, so
|
||||
// attenuating the low-rate zones is a genuine low-band trim rather than a blunt
|
||||
// overall cut.
|
||||
//
|
||||
// Ramp: untouched at or above 22050 Hz, full trim at or below 5512 Hz, log
|
||||
// interpolated between, so nothing steps abruptly at a threshold. Each buffer's
|
||||
// DEPTH is fixed at load; the trim itself is read at mix time, which is what
|
||||
// lets the keys move it while sounds are playing.
|
||||
//
|
||||
static const ALsizei kBassTrimFullRate = 5512; // at/below: full trim
|
||||
static const ALsizei kBassTrimNoneRate = 22050; // at/above: untouched
|
||||
static const char kBassTrimFile[] = "bass.cfg";
|
||||
static const float kBassTrimStep = 0.05f;
|
||||
|
||||
static float *g_bufferBassDepth = NULL; // one per loaded buffer
|
||||
static float g_bassTrim = 1.0f;
|
||||
|
||||
//
|
||||
// How much of the trim a buffer at this rate takes: 0 = untouched, 1 = fully.
|
||||
//
|
||||
static float
|
||||
RPBassDepthForRate(ALsizei rate)
|
||||
{
|
||||
if (rate >= kBassTrimNoneRate) return 0.0f;
|
||||
if (rate <= kBassTrimFullRate) return 1.0f;
|
||||
|
||||
const float span = (float)log((double)kBassTrimNoneRate / (double)kBassTrimFullRate);
|
||||
|
||||
return (float)log((double)kBassTrimNoneRate / (double)rate) / span;
|
||||
}
|
||||
|
||||
void
|
||||
RPBassTrimInitialize()
|
||||
{
|
||||
g_bassTrim = 1.0f;
|
||||
|
||||
if (const char *setting = getenv("RP412AUDIOBASS"))
|
||||
{
|
||||
float value = (float)atof(setting);
|
||||
|
||||
if (value >= 0.0f && value <= 1.0f)
|
||||
{
|
||||
g_bassTrim = value;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Whatever the player last set with the keys wins, exactly as the master
|
||||
// volume behaves -- environ.ini only decides where an untouched machine
|
||||
// starts out.
|
||||
//
|
||||
if (FILE *cfg = fopen(kBassTrimFile, "rt"))
|
||||
{
|
||||
float value = -1.0f;
|
||||
|
||||
if (fscanf(cfg, "%f", &value) == 1 && value >= 0.0f && value <= 1.0f)
|
||||
{
|
||||
g_bassTrim = value;
|
||||
}
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
void
|
||||
RPBassTrimStep(int direction)
|
||||
{
|
||||
g_bassTrim += (direction > 0) ? kBassTrimStep : -kBassTrimStep;
|
||||
|
||||
if (g_bassTrim < 0.0f) g_bassTrim = 0.0f;
|
||||
if (g_bassTrim > 1.0f) g_bassTrim = 1.0f;
|
||||
|
||||
g_bassTrim = (float)((int)(g_bassTrim / kBassTrimStep + 0.5f)) * kBassTrimStep;
|
||||
|
||||
if (FILE *cfg = fopen(kBassTrimFile, "wt"))
|
||||
{
|
||||
fprintf(cfg, "%.2f\n", g_bassTrim);
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
float
|
||||
RPBassTrim()
|
||||
{
|
||||
return g_bassTrim;
|
||||
}
|
||||
|
||||
//
|
||||
// The gain a zone takes at the current trim. 1.0 whenever the player has not
|
||||
// touched it, so the default costs one multiply by one.
|
||||
//
|
||||
float
|
||||
RPBufferBassGain(int buffer_index)
|
||||
{
|
||||
if (g_bassTrim >= 0.999f || g_bufferBassDepth == NULL
|
||||
|| buffer_index < 0 || buffer_index >= g_numBuffers)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
return 1.0f - (1.0f - g_bassTrim) * g_bufferBassDepth[buffer_index];
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//####################### AudioObjectStream #############################
|
||||
//#############################################################################
|
||||
@@ -566,6 +690,18 @@ void
|
||||
g_buffers = NULL;
|
||||
g_numBuffers = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// Parallel to g_buffers: how much of the bass trim each zone takes.
|
||||
//
|
||||
RPBassTrimInitialize();
|
||||
g_bufferBassDepth = new float[g_numBuffers];
|
||||
for (int b = 0; b < g_numBuffers; b++)
|
||||
{
|
||||
g_bufferBassDepth[b] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int bufferInd = 0;
|
||||
@@ -647,6 +783,15 @@ void
|
||||
sf_read_raw(file,data,size);
|
||||
sf_close(file);
|
||||
|
||||
//
|
||||
// Record which band this zone sits in, for the Home/End bass
|
||||
// trim. Fixed per buffer; the trim itself is read at mix time.
|
||||
//
|
||||
if (g_bufferBassDepth != NULL)
|
||||
{
|
||||
g_bufferBassDepth[bufferInd] = RPBassDepthForRate(alSampleRate);
|
||||
}
|
||||
|
||||
//Feed the buffer
|
||||
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
|
||||
PRESET_setBufferIndex(i,j,k,bufferInd);
|
||||
@@ -726,6 +871,16 @@ void
|
||||
|
||||
ALuint AL_getBuffer(int index)
|
||||
{
|
||||
//
|
||||
// 0 is AL_NONE - "no buffer" - which alSourcei accepts and which detaches
|
||||
// the source rather than crashing. An index that is out of range means a
|
||||
// zone that does not exist, and the only thing an unchecked lookup here
|
||||
// can do about it is read whatever lies past the array.
|
||||
//
|
||||
if (g_buffers == NULL || index < 0 || index >= g_numBuffers)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return g_buffers[index];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,16 @@ ALuint AL_getBuffer(int index);
|
||||
extern ALuint *g_buffers;
|
||||
extern int g_numBuffers;
|
||||
|
||||
//
|
||||
// RP412AUDIOBASS low-band trim, stepped live by the Home/End keys. Applied as
|
||||
// a per-zone gain in the mix; see the comment block in L4AUDRES.cpp for why it
|
||||
// lives here and not in EFX.
|
||||
//
|
||||
void RPBassTrimInitialize();
|
||||
void RPBassTrimStep(int direction);
|
||||
float RPBassTrim();
|
||||
float RPBufferBassGain(int buffer_index);
|
||||
|
||||
|
||||
//class AudioHardware;
|
||||
|
||||
|
||||
+310
-27
@@ -2,9 +2,20 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4audrnd.h"
|
||||
#include "l4audefx.h"
|
||||
#include "..\munga\notation.h"
|
||||
#include "openal/alc.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
//
|
||||
// Master volume limits, shared by the startup load and the PgUp/PgDn step.
|
||||
// The file sits beside the exe with the other runtime state.
|
||||
//
|
||||
static const char kAudioVolumeFile[] = "volume.cfg";
|
||||
static const float kAudioVolumeStep = 0.05f;
|
||||
static const float kAudioVolumeMax = 2.0f;
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// L4AudioRenderer
|
||||
@@ -379,6 +390,66 @@ void
|
||||
{
|
||||
ALCcontext *context = alcCreateContext(device,NULL);
|
||||
alcMakeContextCurrent(context);
|
||||
|
||||
//
|
||||
// FIDELITY (docs/SOUND.md F9/F11): bring up the EFX bridge that carries
|
||||
// the authored brightness/distance lowpass and the wet-exterior reverb
|
||||
// send. Needs the context current, and the reverb gain has already been
|
||||
// read from AUDIO.INI into the head above. Inert without ALC_EXT_EFX.
|
||||
//
|
||||
EFX_Initialize(audio_head->GetGlobalReverbScale());
|
||||
|
||||
//
|
||||
// Master volume. There was no listener gain at all before -- the mix
|
||||
// always ran at unity -- so restoring the authored dynamics gave players
|
||||
// no way to pull the whole thing down. This lives in environ.ini rather
|
||||
// than AUDIO.INI deliberately: AUDIO.INI is byte-identical to the file
|
||||
// that shipped in 1995 and is worth keeping that way.
|
||||
//
|
||||
// Default is 1.0, i.e. exactly the previous behaviour -- the knob only
|
||||
// does something when someone asks for it.
|
||||
//
|
||||
{
|
||||
float master_volume = 1.0f;
|
||||
|
||||
if (const char *setting = getenv("RP412AUDIOVOLUME"))
|
||||
{
|
||||
float value = (float)atof(setting);
|
||||
|
||||
if (value >= 0.0f && value <= kAudioVolumeMax)
|
||||
{
|
||||
master_volume = value;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Whatever the player last set with the volume keys wins over the
|
||||
// environ.ini figure: the keys are the amplifier knob, and a knob
|
||||
// stays where it was left. environ.ini sets where it starts on a
|
||||
// machine that has never been touched.
|
||||
//
|
||||
if (FILE *cfg = fopen(kAudioVolumeFile, "rt"))
|
||||
{
|
||||
float value = -1.0f;
|
||||
|
||||
if (fscanf(cfg, "%f", &value) == 1
|
||||
&& value >= 0.0f && value <= kAudioVolumeMax)
|
||||
{
|
||||
master_volume = value;
|
||||
}
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
gRPMasterVolume = master_volume;
|
||||
alListenerf(AL_GAIN, master_volume);
|
||||
Tell("Audio master volume " << (int)(master_volume * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
//
|
||||
// The bass trim is not set here: it is a per-zone gain owned by the
|
||||
// resource manager (L4AUDRES.cpp), which needs the buffers to exist
|
||||
// first. PreloadResources initialises it below.
|
||||
//
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1257,6 +1328,190 @@ Logical
|
||||
return resources_available;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Master volume ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// The pod ran at unity and left volume to an external amplifier, so the game
|
||||
// never had a level control. Standing in for that amplifier means the player
|
||||
// needs to reach it while playing, not only through environ.ini -- hence the
|
||||
// PgUp/PgDn binding in L4Application::KeyCommandMessageHandler.
|
||||
//
|
||||
// Page keys specifically: they produce no typed character, so they cannot
|
||||
// collide with any of the engine's character-keyed commands the way '+'/'-'
|
||||
// would, they are bound to nothing in any RP layout, and they exist on
|
||||
// tenkeyless keyboards.
|
||||
//
|
||||
float gRPMasterVolume = 1.0f;
|
||||
|
||||
void
|
||||
RPAudioMasterVolumeStep(int direction)
|
||||
{
|
||||
gRPMasterVolume += (direction > 0) ? kAudioVolumeStep : -kAudioVolumeStep;
|
||||
|
||||
if (gRPMasterVolume < 0.0f) gRPMasterVolume = 0.0f;
|
||||
if (gRPMasterVolume > kAudioVolumeMax) gRPMasterVolume = kAudioVolumeMax;
|
||||
|
||||
//
|
||||
// Snap to the step grid so repeated presses cannot drift on float error and
|
||||
// land somewhere that never reads back as a round number.
|
||||
//
|
||||
gRPMasterVolume =
|
||||
(float)((int)(gRPMasterVolume / kAudioVolumeStep + 0.5f)) * kAudioVolumeStep;
|
||||
|
||||
alListenerf(AL_GAIN, gRPMasterVolume);
|
||||
|
||||
//
|
||||
// Persist immediately. A pod operator setting the level expects it to still
|
||||
// be there after the cabinet is power-cycled, and there is no settings UI to
|
||||
// hang it off.
|
||||
//
|
||||
if (FILE *cfg = fopen(kAudioVolumeFile, "wt"))
|
||||
{
|
||||
fprintf(cfg, "%.2f\n", gRPMasterVolume);
|
||||
fclose(cfg);
|
||||
}
|
||||
|
||||
Tell("Audio master volume " << (int)(gRPMasterVolume * 100.0f + 0.5f) << "%\n");
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OpenAL source pool ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// Sources are expensive to create and destroy and are a HARD per-context
|
||||
// resource (this driver grants 256 mono). Generating one per sound event and
|
||||
// deleting it on release burns through that ceiling during busy play even
|
||||
// though steady-state demand is modest, which shows up as sounds silently
|
||||
// failing to start. Generate once, recycle forever.
|
||||
//
|
||||
// The cap sits below the driver grant with a reserve, so growth stops on our
|
||||
// terms rather than on an alGenSources failure. Growth also stops by itself if
|
||||
// a driver offers fewer sources than the cap -- a failed generate simply ends
|
||||
// growth and the pool recycles what it already has.
|
||||
//
|
||||
static const int kAudioPoolMax = 512; // free-list array size
|
||||
static const int kAudioPoolCap = 240; // grow no further than this
|
||||
|
||||
static ALuint gAudioPoolFree[kAudioPoolMax];
|
||||
static int gAudioPoolFreeCount = 0; // entries parked in gAudioPoolFree
|
||||
static int gAudioPoolTotal = 0; // sources ever generated (<= cap)
|
||||
static long gAudioPoolReuses = 0; // diagnostics
|
||||
|
||||
int RPAudioPoolSize() { return gAudioPoolTotal; }
|
||||
int RPAudioPoolFree() { return gAudioPoolFreeCount; }
|
||||
long RPAudioPoolReuses() { return gAudioPoolReuses; }
|
||||
|
||||
//
|
||||
// Reset a source to a neutral state so nothing carries across owners.
|
||||
//
|
||||
static void
|
||||
RPAudioScrubSource(ALuint src)
|
||||
{
|
||||
ALint state = AL_STOPPED;
|
||||
|
||||
alGetSourcei(src, AL_SOURCE_STATE, &state);
|
||||
if (state == AL_PLAYING || state == AL_PAUSED)
|
||||
{
|
||||
alSourceStop(src);
|
||||
}
|
||||
|
||||
alSourcei(src, AL_BUFFER, 0); // detach (nothing is queued here)
|
||||
alSourcei(src, AL_LOOPING, AL_FALSE); // or the next owner inherits a loop
|
||||
alSourcef(src, AL_GAIN, 1.0f);
|
||||
alSourcef(src, AL_PITCH, 1.0f);
|
||||
alSourcei(src, AL_SOURCE_RELATIVE, AL_FALSE);
|
||||
alSource3f(src, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
||||
alSource3f(src, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
//
|
||||
// Drop the EFX state too. Without this a recycled name can carry a 3D
|
||||
// source's reverb send into a dry cockpit sound, or a distant source's
|
||||
// lowpass into a close one.
|
||||
//
|
||||
EFX_ClearSourceEffects(src);
|
||||
|
||||
alGetError(); // swallow any property complaint
|
||||
}
|
||||
|
||||
//
|
||||
// Hand out a source: recycle first, generate only while under the cap.
|
||||
// False means genuinely out, and the caller retries after the steal loop runs.
|
||||
//
|
||||
Logical
|
||||
RPAudioPoolAcquire(ALuint *out)
|
||||
{
|
||||
Check_Pointer(out);
|
||||
|
||||
while (gAudioPoolFreeCount > 0)
|
||||
{
|
||||
ALuint src = gAudioPoolFree[--gAudioPoolFreeCount];
|
||||
|
||||
if (alIsSource(src)) // a context reset invalidates names
|
||||
{
|
||||
++gAudioPoolReuses;
|
||||
*out = src;
|
||||
return True;
|
||||
}
|
||||
--gAudioPoolTotal; // stale name: forget it
|
||||
}
|
||||
|
||||
if (gAudioPoolTotal >= kAudioPoolCap)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
|
||||
ALuint src = 0;
|
||||
|
||||
alGetError();
|
||||
alGenSources(1, &src);
|
||||
if (alGetError() != AL_NO_ERROR || !alIsSource(src))
|
||||
{
|
||||
return False; // driver said no before our cap
|
||||
}
|
||||
|
||||
++gAudioPoolTotal;
|
||||
|
||||
#if DEBUG_LEVEL>0
|
||||
{
|
||||
//
|
||||
// One line per high-water band, so a log shows how close real play gets
|
||||
// to the ceiling without spamming.
|
||||
//
|
||||
static int s_notified = 0;
|
||||
|
||||
if (gAudioPoolTotal >= s_notified + 25)
|
||||
{
|
||||
s_notified = gAudioPoolTotal;
|
||||
Tell("Audio source pool high-water: " << gAudioPoolTotal
|
||||
<< " of " << kAudioPoolCap << "\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
*out = src;
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
// Take a source back. Scrubbed and parked, never deleted.
|
||||
//
|
||||
void
|
||||
RPAudioPoolRelease(ALuint src)
|
||||
{
|
||||
if (!alIsSource(src))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RPAudioScrubSource(src);
|
||||
|
||||
if (gAudioPoolFreeCount < kAudioPoolMax)
|
||||
{
|
||||
gAudioPoolFree[gAudioPoolFreeCount++] = src;
|
||||
return;
|
||||
}
|
||||
|
||||
alDeleteSources(1, &src); // unreachable: cap < array size
|
||||
--gAudioPoolTotal;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// RequestAudioChannels
|
||||
@@ -1271,30 +1526,54 @@ Logical
|
||||
Check(this);
|
||||
Check(source_request);
|
||||
|
||||
//Do we have enough?
|
||||
//
|
||||
// SOURCE POOLING (docs/SOUND.md). This used to alGenSources per sound
|
||||
// event, with ReleaseSourceSet alDeleteSources'ing on release -- so play
|
||||
// activity CHURNED through OpenAL's per-context source limit (the driver
|
||||
// grants 256 mono here). Recovering the soundbanks took the voice count
|
||||
// per sound from about 1.1 zones to about 2.6, roughly doubling that churn.
|
||||
//
|
||||
// The BT tree measured this exact problem: raising the budget was NOT the
|
||||
// fix, recycling was, and it was a net CPU win besides. Sources are now
|
||||
// generated once and handed back to a free list, so steady-state play costs
|
||||
// no allocation at all.
|
||||
//
|
||||
int requested = source_request->count;
|
||||
|
||||
bool failed = true;
|
||||
|
||||
alGetError();
|
||||
if (requested > (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0])))
|
||||
{
|
||||
requested = (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0]));
|
||||
source_request->count = requested;
|
||||
}
|
||||
|
||||
for (int i = 0; i < requested; i++)
|
||||
{
|
||||
if (!alIsSource(source_request->sources[i]))
|
||||
if (source_request->sources[i] != 0 && alIsSource(source_request->sources[i]))
|
||||
{
|
||||
alGenSources(1, source_request->sources + i);
|
||||
continue; // slot already holds a live source
|
||||
}
|
||||
}
|
||||
|
||||
ALenum error = alGetError();
|
||||
if (error == AL_NO_ERROR)
|
||||
{
|
||||
failed = false;
|
||||
}
|
||||
ALuint src = 0;
|
||||
|
||||
if (failed)
|
||||
{
|
||||
return False;
|
||||
if (!RPAudioPoolAcquire(&src))
|
||||
{
|
||||
//
|
||||
// Out of sources. Hand back everything acquired on THIS attempt so a
|
||||
// failed request cannot strand voices -- the renderer's steal loop
|
||||
// will free some and retry.
|
||||
//
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
if (source_request->sources[j] != 0)
|
||||
{
|
||||
RPAudioPoolRelease(source_request->sources[j]);
|
||||
source_request->sources[j] = 0;
|
||||
}
|
||||
}
|
||||
return False;
|
||||
}
|
||||
|
||||
source_request->sources[i] = src;
|
||||
}
|
||||
|
||||
return True;
|
||||
@@ -1375,23 +1654,27 @@ Logical
|
||||
|
||||
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
||||
{
|
||||
//
|
||||
// SOURCE POOLING (docs/SOUND.md): park each source on the free list rather
|
||||
// than destroying it. RPAudioPoolRelease stops it, detaches its buffer and
|
||||
// scrubs the state -- including the EFX filter and reverb send -- so the
|
||||
// next owner starts clean.
|
||||
//
|
||||
// The bulk alDeleteSources(count, sources) this replaces was also a leak
|
||||
// waiting to happen: per the AL spec it is ATOMIC, so ONE invalid name in
|
||||
// the array (an empty slot of a partial set, or the old -1 sentinel on a
|
||||
// double release) meant NOTHING was deleted and the whole set leaked.
|
||||
// Slots are parked at 0, which is never a valid AL name -- unlike -1, which
|
||||
// alIsSource would be asked about as 0xFFFFFFFF.
|
||||
//
|
||||
for (int i = 0; i < sourceSet.count; i++)
|
||||
{
|
||||
ALenum state;
|
||||
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
|
||||
|
||||
if (state == AL_PLAYING)
|
||||
if (sourceSet.sources[i] != 0)
|
||||
{
|
||||
alSourceStop(sourceSet.sources[i]);
|
||||
RPAudioPoolRelease(sourceSet.sources[i]);
|
||||
sourceSet.sources[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
alDeleteSources(sourceSet.count, sourceSet.sources);
|
||||
|
||||
for (int i = 0; i < sourceSet.count; i++)
|
||||
{
|
||||
sourceSet.sources[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
#include "l4audres.h"
|
||||
#include "openal/al.h"
|
||||
|
||||
//
|
||||
// Master volume, standing in for the amplifier the cabinets had. Stepped by
|
||||
// PgUp/PgDn (L4APP.cpp) and persisted to volume.cfg; see L4AUDRND.cpp.
|
||||
//
|
||||
extern float gRPMasterVolume;
|
||||
|
||||
void RPAudioMasterVolumeStep(int direction);
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "l4ctrl.h"
|
||||
#include "l4keybd.h"
|
||||
#include "l4app.h"
|
||||
#include "l4audrnd.h" // RPAudioMasterVolumeStep, for the PgUp/PgDn keys
|
||||
#include "l4audres.h" // RPBassTrimStep, for the Home/End keys
|
||||
#include "l4dinput.h"
|
||||
#include "..\munga\appmgr.h"
|
||||
#include "dxutils.h"
|
||||
@@ -1513,6 +1515,78 @@ void
|
||||
// Update the PC keyboard mapping group
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// Master volume, PgUp louder / PgDn quieter.
|
||||
//
|
||||
// The cabinets ran the game at unity and left level to an external
|
||||
// amplifier and crossover; without that hardware the player has to be able
|
||||
// to reach the volume while playing.
|
||||
//
|
||||
// POLLED, not taken off the key message below, and that is deliberate. The
|
||||
// pump below only ever consumes WM_KEYUP / WM_SYSKEYUP / WM_CHAR from the
|
||||
// front of the queue, and the front-end runs message loops of its own, so
|
||||
// key messages are raced for and routinely lost -- measured here at roughly
|
||||
// two of every six presses arriving. That is survivable for a one-shot like
|
||||
// the abort chord; it is not survivable for a control you tap repeatedly to
|
||||
// find a level. Reading the key state directly costs nothing and cannot be
|
||||
// dropped.
|
||||
//
|
||||
// Page keys because they produce no typed character, so they cannot collide
|
||||
// with the character-keyed commands the pump feeds, nothing else in RP binds
|
||||
// them, and they exist on tenkeyless keyboards.
|
||||
//
|
||||
// The foreground check keeps an alt-tabbed game from eating the volume keys
|
||||
// of whatever the player switched to.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
// Home/End do the same for the bass trim - the crossover's low band to
|
||||
// PgUp/PgDn's amplifier.
|
||||
//
|
||||
{
|
||||
static int volume_up_held = 0;
|
||||
static int volume_down_held = 0;
|
||||
static int bass_up_held = 0;
|
||||
static int bass_down_held = 0;
|
||||
|
||||
int focused = 0;
|
||||
|
||||
if (HWND foreground = GetForegroundWindow())
|
||||
{
|
||||
DWORD foreground_process = 0;
|
||||
|
||||
GetWindowThreadProcessId(foreground, &foreground_process);
|
||||
focused = (foreground_process == GetCurrentProcessId());
|
||||
}
|
||||
|
||||
const int up = focused && (GetAsyncKeyState(VK_PRIOR) & 0x8000) != 0;
|
||||
const int down = focused && (GetAsyncKeyState(VK_NEXT) & 0x8000) != 0;
|
||||
const int bass_up = focused && (GetAsyncKeyState(VK_HOME) & 0x8000) != 0;
|
||||
const int bass_down = focused && (GetAsyncKeyState(VK_END) & 0x8000) != 0;
|
||||
|
||||
if (up && !volume_up_held)
|
||||
{
|
||||
RPAudioMasterVolumeStep(+1);
|
||||
}
|
||||
if (down && !volume_down_held)
|
||||
{
|
||||
RPAudioMasterVolumeStep(-1);
|
||||
}
|
||||
if (bass_up && !bass_up_held)
|
||||
{
|
||||
RPBassTrimStep(+1);
|
||||
}
|
||||
if (bass_down && !bass_down_held)
|
||||
{
|
||||
RPBassTrimStep(-1);
|
||||
}
|
||||
|
||||
volume_up_held = up;
|
||||
volume_down_held = down;
|
||||
bass_up_held = bass_up;
|
||||
bass_down_held = bass_down;
|
||||
}
|
||||
|
||||
if (flags.keyboardExists)
|
||||
{
|
||||
//RB 1/20/07
|
||||
|
||||
+387
-47
@@ -508,13 +508,67 @@ int
|
||||
// common, and no amount of documentation gets a player to work out which
|
||||
// they own.
|
||||
//
|
||||
// The same principle runs deeper than the sign. A player should not have
|
||||
// to know the SHAPE of their own rig either, so the wizard works that
|
||||
// out too, and the two controls that cannot simply be watched are asked
|
||||
// for differently:
|
||||
//
|
||||
// yaw asked for twice, right then left. One axis answering both
|
||||
// is a twist grip or rudder bar - the signed Pedals
|
||||
// composite. Two different axes are two real pedals, one per
|
||||
// foot, the pod's own arrangement, bound to the real pair.
|
||||
//
|
||||
// throttle a lever sits wherever it was left, so no movement of it
|
||||
// says which end is open. The player is asked to put it at
|
||||
// ZERO and say so; the reading is taken there, and the
|
||||
// direction it travels from a known idle means power.
|
||||
//
|
||||
//########################################################################
|
||||
|
||||
#include <conio.h>
|
||||
#include <XInput.h>
|
||||
#include "l4padbindings.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
//
|
||||
// Xbox-class pads are kept out of the capture on purpose: their
|
||||
// layout is fixed and NAMED, so unlike a DirectInput axis there is
|
||||
// nothing to identify by watching, and letting one answer a prompt
|
||||
// would only bind it twice. Invisible is the wrong answer though -
|
||||
// a player whose whole rig is a pad, or a wheel running in XInput
|
||||
// mode, should be told it is already mapped rather than left reading
|
||||
// "no devices found" and wondering what is broken.
|
||||
//
|
||||
int WizardXInputSlot(void)
|
||||
{
|
||||
XINPUT_STATE state;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
if (XInputGetState((DWORD) i, &state) == ERROR_SUCCESS)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//
|
||||
// What an Xbox-class pad already does, said once and in one place.
|
||||
// The triggers are the interesting half: XInput reports each as its
|
||||
// own 0..255 byte rather than two halves of a shared axis, which is
|
||||
// the pod's two-pedal arrangement exactly, and unipolar already - no
|
||||
// 'lever' to fold, no sign to discover.
|
||||
//
|
||||
void WizardReportXInput(int slot)
|
||||
{
|
||||
printf(" [XInput slot %d] Xbox-class controller - ALREADY MAPPED, and\n"
|
||||
" not part of this setup. Its two triggers are the pod's\n"
|
||||
" left and right pedals, the left stick is the joystick and\n"
|
||||
" the right stick the throttle. Edit the pad rows of\n"
|
||||
" bindings.txt by hand to change any of that.\n", slot);
|
||||
}
|
||||
|
||||
struct WizardCapture
|
||||
{
|
||||
int used;
|
||||
@@ -532,6 +586,39 @@ namespace
|
||||
return (axis >= 0 && axis < joyAxisCount) ? names[axis] : "?";
|
||||
}
|
||||
|
||||
//
|
||||
// A pedal is a ONE-WAY control: its spring holds it at the released
|
||||
// end of its travel, so the direction of the press is the whole
|
||||
// story and the row it writes says 'lever' - the -1..1 axis the
|
||||
// driver reports then folds onto the 0..1 the channel runs on
|
||||
// instead of throwing away the half that reads below zero.
|
||||
//
|
||||
// Where it RESTS is what decides that, and the wizard can see it.
|
||||
// An axis sitting near the MIDDLE is not a pedal at all - a stick
|
||||
// axis pressed into service as one - and already reads zero at
|
||||
// rest, so 'lever' would jam it at half depression for good.
|
||||
//
|
||||
void WizardWritePedal(WizardCapture *capture, int axis, float rest,
|
||||
float delta, const char *channel)
|
||||
{
|
||||
capture->invert = (delta < 0.0f);
|
||||
if (rest > 0.5f || rest < -0.5f)
|
||||
{
|
||||
sprintf(capture->line, "joyaxis %s axis %s%s lever deadzone 0.05",
|
||||
JoyAxisToken(axis), channel,
|
||||
capture->invert ? " invert" : "");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(" (%s rests near centre rather than at one end, so it\n"
|
||||
" is bound as a plain axis rather than as a pedal)\n",
|
||||
JoyAxisToken(axis));
|
||||
sprintf(capture->line, "joyaxis %s axis %s%s deadzone 0.08",
|
||||
JoyAxisToken(axis), channel,
|
||||
capture->invert ? " invert" : "");
|
||||
}
|
||||
}
|
||||
|
||||
void WizardBaseline(float baseline[joyMaxDevices][joyAxisCount])
|
||||
{
|
||||
//
|
||||
@@ -746,11 +833,30 @@ int
|
||||
PadBindings_Load(&ensure_default);
|
||||
}
|
||||
|
||||
int xinput_slot = WizardXInputSlot();
|
||||
|
||||
if (RPJoyInit() == 0)
|
||||
{
|
||||
printf("No generic (non-Xbox) game devices found.\n");
|
||||
printf("Plug in the stick, throttle or pedals and run joyconfig again.\n");
|
||||
printf("(Xbox-class controllers already work - no setup needed.)\n\n");
|
||||
if (xinput_slot >= 0)
|
||||
{
|
||||
//
|
||||
// Not a failure, and it should not read like one: the pad IS
|
||||
// the rig, and it is already configured. Say what it does
|
||||
// rather than asking for hardware they have not got.
|
||||
//
|
||||
printf("Nothing here needs configuring.\n\n");
|
||||
WizardReportXInput(xinput_slot);
|
||||
printf("\nThere are no generic (DirectInput) sticks, throttles or\n");
|
||||
printf("pedals attached, and those are the only thing this setup\n");
|
||||
printf("has to work out. Plug one in and run joyconfig again if\n");
|
||||
printf("you add one.\n\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("No generic (non-Xbox) game devices found.\n");
|
||||
printf("Plug in the stick, throttle or pedals and run joyconfig again.\n");
|
||||
printf("(Xbox-class controllers already work - no setup needed.)\n\n");
|
||||
}
|
||||
printf("Press any key to exit.\n");
|
||||
_getch();
|
||||
return 1;
|
||||
@@ -783,48 +889,51 @@ int
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
//
|
||||
// Listed with the rest so a player who squeezes a trigger at a
|
||||
// prompt and sees nothing happen knows why, rather than deciding
|
||||
// the wizard cannot see their pad.
|
||||
//
|
||||
if (xinput_slot >= 0)
|
||||
{
|
||||
WizardReportXInput(xinput_slot);
|
||||
}
|
||||
}
|
||||
printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
|
||||
"skip it, ESC to abort. Keep everything else still.\n\n");
|
||||
|
||||
WizardCapture captures[16];
|
||||
memset(captures, 0, sizeof(captures));
|
||||
int capture_count = 0;
|
||||
|
||||
//
|
||||
// The pod's analog channels. wants_negative says the asked-for move
|
||||
// should read NEGATIVE in the pod's sign convention, which is what
|
||||
// decides whether the captured axis gets an invert:
|
||||
// The stick, whose two axes are spring-centred and so give their
|
||||
// sign away the moment they move. wants_negative says the asked-for
|
||||
// move should read NEGATIVE in the pod's sign convention, which is
|
||||
// what decides whether the captured axis gets an invert:
|
||||
//
|
||||
// JoystickX left +1, right -1
|
||||
// JoystickY forward -1, back +1
|
||||
// Pedals right +1, left -1 (the composite that decomposes
|
||||
// into the pod's two pedals)
|
||||
//
|
||||
// Yaw and the throttle are not this simple and are asked for below.
|
||||
//
|
||||
struct AxisStep
|
||||
{
|
||||
const char *prompt;
|
||||
const char *channel;
|
||||
int wants_negative;
|
||||
int lever; // full-travel lever: sign from where
|
||||
// it ENDS, not which way it moved
|
||||
int allow_skip;
|
||||
};
|
||||
static const AxisStep axisSteps[] =
|
||||
{
|
||||
{ "STEER: push the STICK / turn the WHEEL fully RIGHT",
|
||||
"JoystickX", 1, 0, 0 },
|
||||
"JoystickX", 1 },
|
||||
{ "PITCH: push the STICK fully FORWARD\n"
|
||||
" (add or remove the word invert on that line in\n"
|
||||
" bindings.txt to flip it later)",
|
||||
"JoystickY", 1, 0, 0 },
|
||||
{ "PEDALS: twist the stick / press the RIGHT rudder pedal\n"
|
||||
" (SPACE if you have neither)",
|
||||
"Pedals", 0, 0, 1 },
|
||||
{ "THROTTLE: move the throttle lever to FULL (SPACE if none)",
|
||||
"Throttle", 0, 1, 1 }
|
||||
"JoystickY", 1 }
|
||||
};
|
||||
|
||||
printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
|
||||
"skip it, ESC to abort. Keep everything else still.\n\n");
|
||||
|
||||
float baseline[joyMaxDevices][joyAxisCount];
|
||||
|
||||
for (int s = 0; s < (int)(sizeof(axisSteps) / sizeof(axisSteps[0])); ++s)
|
||||
@@ -834,7 +943,7 @@ int
|
||||
int device, axis;
|
||||
float delta, final_value;
|
||||
int got = WizardCaptureAxis(baseline, captures, capture_count,
|
||||
axisSteps[s].allow_skip, &device, &axis, &delta, &final_value);
|
||||
0, &device, &axis, &delta, &final_value);
|
||||
if (got < 0)
|
||||
{
|
||||
printf("\nAborted - nothing written.\n");
|
||||
@@ -842,38 +951,18 @@ int
|
||||
_getch();
|
||||
return 1;
|
||||
}
|
||||
if (got == 0)
|
||||
{
|
||||
printf(" skipped.\n\n");
|
||||
continue;
|
||||
}
|
||||
WizardCapture &capture = captures[capture_count++];
|
||||
capture.used = 1;
|
||||
capture.device = device;
|
||||
capture.axis = axis;
|
||||
capture.button = -1;
|
||||
|
||||
if (axisSteps[s].lever)
|
||||
{
|
||||
//
|
||||
// A lever has no rest position to move away from, so the
|
||||
// sign comes from where it finished: full-forward reading
|
||||
// negative means the axis runs backwards for us.
|
||||
//
|
||||
capture.invert = (final_value < 0.0f);
|
||||
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0",
|
||||
JoyAxisToken(axis), axisSteps[s].channel,
|
||||
capture.invert ? " invert" : "");
|
||||
}
|
||||
else
|
||||
{
|
||||
int went_negative = (delta < 0.0f);
|
||||
capture.invert = axisSteps[s].wants_negative
|
||||
? !went_negative : went_negative;
|
||||
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0.08",
|
||||
JoyAxisToken(axis), axisSteps[s].channel,
|
||||
capture.invert ? " invert" : "");
|
||||
}
|
||||
int went_negative = (delta < 0.0f);
|
||||
capture.invert = axisSteps[s].wants_negative
|
||||
? !went_negative : went_negative;
|
||||
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0.08",
|
||||
JoyAxisToken(axis), axisSteps[s].channel,
|
||||
capture.invert ? " invert" : "");
|
||||
//
|
||||
// The move is reported, not just the axis: a capture nobody made
|
||||
// shows up here as a small delta, and a player who wonders why
|
||||
@@ -887,6 +976,257 @@ int
|
||||
Sleep(800); // let the control come back to rest
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Yaw. The pod steered on two foot pedals mixed into the turn, and
|
||||
// hardware answers that in two shapes - but a player should not have
|
||||
// to know which shape they own, and plenty do not. So ask for RIGHT,
|
||||
// then ask for LEFT, and watch WHICH axis answers each time:
|
||||
//
|
||||
// the same axis twice one control covering both directions - a
|
||||
// twist grip, a rudder bar, pedals whose
|
||||
// driver has already mixed them - which is
|
||||
// the signed Pedals composite
|
||||
//
|
||||
// two different axes two real pedals, one per foot, which is
|
||||
// what the pod itself had. They bind to the
|
||||
// pod's own pair and the game does the
|
||||
// mixing, so both at once does what both at
|
||||
// once did in the pod.
|
||||
//
|
||||
// The LEFT capture is deliberately offered the RIGHT axis again -
|
||||
// the usual claimed-axis exclusion would make every rig look like a
|
||||
// pair, since "the same axis answered twice" is the measurement.
|
||||
//---------------------------------------------------------------
|
||||
{
|
||||
printf("YAW RIGHT: press the RIGHT rudder pedal, or twist / push\n"
|
||||
" the stick RIGHT (SPACE if you have no yaw control) ...\n");
|
||||
WizardBaseline(baseline);
|
||||
int right_device, right_axis;
|
||||
float right_delta, right_final;
|
||||
int got = WizardCaptureAxis(baseline, captures, capture_count, 1,
|
||||
&right_device, &right_axis, &right_delta, &right_final);
|
||||
if (got < 0)
|
||||
{
|
||||
printf("\nAborted - nothing written.\n");
|
||||
printf("Press any key to continue into the game.\n");
|
||||
_getch();
|
||||
return 1;
|
||||
}
|
||||
if (got == 0)
|
||||
{
|
||||
printf(" skipped - no yaw control.\n\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
float right_rest = baseline[right_device][right_axis];
|
||||
printf(" -> device %d (%s) axis %s [moved %+.2f]\n",
|
||||
right_device,
|
||||
(RPJoyDevice(right_device) != NULL)
|
||||
? RPJoyDevice(right_device)->name : "?",
|
||||
JoyAxisToken(right_axis), right_delta);
|
||||
Sleep(800); // let it come back to rest before we re-baseline
|
||||
|
||||
printf("YAW LEFT: now the other way - press the LEFT pedal, or\n"
|
||||
" twist / push the stick LEFT ...\n");
|
||||
WizardBaseline(baseline);
|
||||
int left_device, left_axis;
|
||||
float left_delta, left_final;
|
||||
int got_left = WizardCaptureAxis(baseline, captures, capture_count,
|
||||
1, &left_device, &left_axis, &left_delta, &left_final);
|
||||
if (got_left < 0)
|
||||
{
|
||||
printf("\nAborted - nothing written.\n");
|
||||
printf("Press any key to continue into the game.\n");
|
||||
_getch();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int same_axis = (got_left == 0) ||
|
||||
(left_device == right_device && left_axis == right_axis);
|
||||
if (got_left != 0)
|
||||
{
|
||||
printf(" -> device %d (%s) axis %s [moved %+.2f]\n",
|
||||
left_device,
|
||||
(RPJoyDevice(left_device) != NULL)
|
||||
? RPJoyDevice(left_device)->name : "?",
|
||||
JoyAxisToken(left_axis), left_delta);
|
||||
}
|
||||
|
||||
if (same_axis)
|
||||
{
|
||||
//
|
||||
// One axis, both ways: the signed composite, positive
|
||||
// for the right pedal. Signed from the RIGHT answer,
|
||||
// which is the one the convention is written in.
|
||||
//
|
||||
WizardCapture &capture = captures[capture_count++];
|
||||
capture.used = 1;
|
||||
capture.device = right_device;
|
||||
capture.axis = right_axis;
|
||||
capture.button = -1;
|
||||
capture.invert = (right_delta < 0.0f);
|
||||
sprintf(capture.line, "joyaxis %s axis Pedals%s deadzone 0.08",
|
||||
JoyAxisToken(right_axis), capture.invert ? " invert" : "");
|
||||
|
||||
if (got_left == 0)
|
||||
{
|
||||
printf(" left skipped - taking %s as one control that\n"
|
||||
" covers both ways.\n", JoyAxisToken(right_axis));
|
||||
}
|
||||
else if ((left_delta < 0.0f) == (right_delta < 0.0f))
|
||||
{
|
||||
//
|
||||
// Both moves read the same way, which no single
|
||||
// control does. Say so rather than write a row that
|
||||
// turns one way only and let them wonder.
|
||||
//
|
||||
printf(" NOTE: both moves pushed %s the SAME way"
|
||||
" (%+.2f then %+.2f).\n"
|
||||
" Bound as one control anyway - check that line if"
|
||||
" yaw only turns\n one way.\n",
|
||||
JoyAxisToken(right_axis), right_delta, left_delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(" ONE axis both ways%s: bound as the pedal PAIR,\n"
|
||||
" a twist grip or rudder bar working both pedals.\n",
|
||||
capture.invert ? " (inverted)" : "");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// Two axes: the pod's own arrangement, one pedal per
|
||||
// foot, so they bind to the real pair rather than to the
|
||||
// composite that stands in for it.
|
||||
//
|
||||
WizardCapture &right_capture = captures[capture_count++];
|
||||
right_capture.used = 1;
|
||||
right_capture.device = right_device;
|
||||
right_capture.axis = right_axis;
|
||||
right_capture.button = -1;
|
||||
WizardWritePedal(&right_capture, right_axis, right_rest,
|
||||
right_delta, "RightPedal");
|
||||
|
||||
WizardCapture &left_capture = captures[capture_count++];
|
||||
left_capture.used = 1;
|
||||
left_capture.device = left_device;
|
||||
left_capture.axis = left_axis;
|
||||
left_capture.button = -1;
|
||||
WizardWritePedal(&left_capture, left_axis,
|
||||
baseline[left_device][left_axis], left_delta, "LeftPedal");
|
||||
|
||||
printf(" TWO axes: %s is the right pedal, %s the left - the\n"
|
||||
" pod's own arrangement, and the game mixes them into"
|
||||
" the turn.\n",
|
||||
JoyAxisToken(right_axis), JoyAxisToken(left_axis));
|
||||
}
|
||||
printf("\n");
|
||||
Sleep(800);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The throttle, which cannot be read the way everything else is. A
|
||||
// lever sits wherever it was last left - halfway, or hard against
|
||||
// the stop that happens to read +1 - so watching it move says
|
||||
// nothing about which END means power. Nor can the wizard ask the
|
||||
// player which end that is: nobody knows what their driver reports.
|
||||
//
|
||||
// So it asks for the one thing the player DOES know - where zero is
|
||||
// - and takes the reading there. Everything after that follows: the
|
||||
// direction it travels from a known idle is the direction that
|
||||
// means open.
|
||||
//---------------------------------------------------------------
|
||||
{
|
||||
printf("THROTTLE: set the lever to ZERO - idle, fully closed - and\n"
|
||||
" press SPACE. Here SPACE means \"it is at zero now\",\n"
|
||||
" not skip; press S if you have no throttle lever ...\n");
|
||||
|
||||
int have_throttle = 0;
|
||||
for (;;)
|
||||
{
|
||||
int key = _getch();
|
||||
if (key == 27)
|
||||
{
|
||||
printf("\nAborted - nothing written.\n");
|
||||
printf("Press any key to continue into the game.\n");
|
||||
_getch();
|
||||
return 1;
|
||||
}
|
||||
if (key == ' ')
|
||||
{
|
||||
have_throttle = 1;
|
||||
break;
|
||||
}
|
||||
if (key == 's' || key == 'S')
|
||||
{
|
||||
printf(" skipped - no throttle lever.\n\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (have_throttle)
|
||||
{
|
||||
printf(" reading zero ...\n");
|
||||
WizardBaseline(baseline);
|
||||
printf(" now OPEN the throttle to FULL"
|
||||
" (SPACE to skip) ...\n");
|
||||
int device, axis;
|
||||
float delta, final_value;
|
||||
int got = WizardCaptureAxis(baseline, captures, capture_count, 1,
|
||||
&device, &axis, &delta, &final_value);
|
||||
if (got < 0)
|
||||
{
|
||||
printf("\nAborted - nothing written.\n");
|
||||
printf("Press any key to continue into the game.\n");
|
||||
_getch();
|
||||
return 1;
|
||||
}
|
||||
if (got == 0)
|
||||
{
|
||||
printf(" skipped.\n\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
float idle = baseline[device][axis];
|
||||
WizardCapture &capture = captures[capture_count++];
|
||||
capture.used = 1;
|
||||
capture.device = device;
|
||||
capture.axis = axis;
|
||||
capture.button = -1;
|
||||
capture.invert = (delta < 0.0f);
|
||||
sprintf(capture.line, "joyaxis %s axis Throttle%s deadzone 0",
|
||||
JoyAxisToken(axis), capture.invert ? " invert" : "");
|
||||
printf(" -> device %d (%s) axis %s%s"
|
||||
" [zero at %+.2f, opened %+.2f]\n",
|
||||
device,
|
||||
(RPJoyDevice(device) != NULL) ? RPJoyDevice(device)->name : "?",
|
||||
JoyAxisToken(axis), capture.invert ? " (inverted)" : "",
|
||||
idle, delta);
|
||||
if (idle > -0.5f && idle < 0.5f)
|
||||
{
|
||||
//
|
||||
// Zero somewhere in the middle of the travel. The
|
||||
// lever owns the channel outright, so its whole
|
||||
// -1..1 range becomes 0-100% and an idle at the
|
||||
// centre is half power. Worth saying plainly.
|
||||
//
|
||||
printf(" NOTE: your zero reads %+.2f rather than an end"
|
||||
" stop, and a\n"
|
||||
" throttle's FULL travel becomes the pod's 0-100%%"
|
||||
" - so at that\n"
|
||||
" position the pod would sit near half power. Use"
|
||||
" the lever's\n"
|
||||
" real closed stop, or edit that row by hand.\n",
|
||||
idle);
|
||||
}
|
||||
printf("\n");
|
||||
Sleep(800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The pod's stick-head buttons, at their RIO addresses.
|
||||
//
|
||||
|
||||
@@ -142,15 +142,48 @@ namespace
|
||||
//---------------------------------------------------------------
|
||||
void Worker()
|
||||
{
|
||||
bool ownsApartment = false;
|
||||
try
|
||||
{
|
||||
init_apartment();
|
||||
ownsApartment = true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// apartment already set on this thread; carry on
|
||||
}
|
||||
|
||||
//
|
||||
// C++/WinRT caches an activation factory the first time a type
|
||||
// is used, and that cache is PROCESS-wide - it outlives this
|
||||
// thread. The apartment does not: COM tears it down when the
|
||||
// worker exits and unloads the Lights server with it, because
|
||||
// by then nothing holds a reference.
|
||||
//
|
||||
// So the cached factory is left pointing into an address range
|
||||
// that no longer has a module in it, and the NEXT race's worker
|
||||
// calls straight through it - the crash was a call through the
|
||||
// stale vtable, on the second race, every time. A machine with
|
||||
// no Dynamic Lighting keyboard is not spared: asking for the
|
||||
// device selector is enough to populate the cache.
|
||||
//
|
||||
// Clear it before the apartment goes, and on every way out of
|
||||
// here rather than only the tidy one - the watcher setup below
|
||||
// returns early when Dynamic Lighting is unavailable.
|
||||
//
|
||||
struct WinRTExit
|
||||
{
|
||||
bool owns;
|
||||
~WinRTExit()
|
||||
{
|
||||
clear_factory_cache();
|
||||
if (owns)
|
||||
{
|
||||
uninit_apartment();
|
||||
}
|
||||
}
|
||||
} winrtExit{ ownsApartment };
|
||||
|
||||
std::mutex claimedLock;
|
||||
std::vector<ClaimedArray> claimed;
|
||||
bool anySeen = false;
|
||||
|
||||
+58
-17
@@ -201,11 +201,15 @@ namespace
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Shared tail of the two axis-source rows: [invert] [deadzone <d>]
|
||||
// [rate <n>], in any order.
|
||||
// [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 ParseAxisOptions(
|
||||
char *tokens[], int token_count, int first,
|
||||
Logical *invert, Scalar *deadzone, Scalar *rate)
|
||||
Logical *invert, Scalar *deadzone, Scalar *rate,
|
||||
Logical *lever = NULL)
|
||||
{
|
||||
for (int i = first; i < token_count; ++i)
|
||||
{
|
||||
@@ -213,6 +217,10 @@ namespace
|
||||
{
|
||||
*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))
|
||||
@@ -355,12 +363,23 @@ namespace
|
||||
{
|
||||
return False;
|
||||
}
|
||||
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->source = source;
|
||||
binding->axis = axis;
|
||||
return ParseAxisOptions(tokens, token_count, 4,
|
||||
&binding->invert, &binding->deadzone, &binding->rate);
|
||||
//
|
||||
// Built aside and only then committed: a row whose options
|
||||
// go bad half way through is a REJECTED row, and taking the
|
||||
// slot first would leave the good half of it bound anyway,
|
||||
// under a log line that says it was skipped.
|
||||
//
|
||||
PadPadAxisBinding candidate;
|
||||
memset(&candidate, 0, sizeof(candidate));
|
||||
candidate.source = source;
|
||||
candidate.axis = axis;
|
||||
if (!ParseAxisOptions(tokens, token_count, 4,
|
||||
&candidate.invert, &candidate.deadzone, &candidate.rate))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
profile->padAxes[profile->padAxisCount++] = candidate;
|
||||
return True;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
@@ -377,13 +396,19 @@ namespace
|
||||
{
|
||||
return False;
|
||||
}
|
||||
PadJoyAxisBinding *binding = &profile->joyAxes[profile->joyAxisCount++];
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->device = *joy_slot;
|
||||
binding->source = source;
|
||||
binding->axis = axis;
|
||||
return ParseAxisOptions(tokens, token_count, 4,
|
||||
&binding->invert, &binding->deadzone, &binding->rate);
|
||||
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"))
|
||||
@@ -453,7 +478,7 @@ namespace
|
||||
"# pad <button> button <addr> [toggle]\n"
|
||||
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||
"# joydev <slot> [product-name substring]\n"
|
||||
"# joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||
"# 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"
|
||||
@@ -462,7 +487,10 @@ namespace
|
||||
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
||||
"# | Pedals - a signed axis that works the pedal PAIR, positive\n"
|
||||
"# for the right pedal and negative for the left, so one rudder\n"
|
||||
"# bar or twist grip drives both.\n"
|
||||
"# bar or twist grip drives both. Two-pedal hardware - racing\n"
|
||||
"# pedals, rudder pedals with an axis per foot - skips the\n"
|
||||
"# composite and binds LeftPedal and RightPedal directly; the\n"
|
||||
"# game mixes the pair into yaw the way the pod always did.\n"
|
||||
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
|
||||
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
||||
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
||||
@@ -493,6 +521,12 @@ namespace
|
||||
"# 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"
|
||||
@@ -500,6 +534,13 @@ namespace
|
||||
"# joyaxis SL0 axis Throttle deadzone 0\n"
|
||||
"# joybutton 0 button 0x40\n"
|
||||
"# joyhat 0 up button 0x42\n"
|
||||
"#\n"
|
||||
"# ...and the same stick with racing pedals on a second device, one\n"
|
||||
"# axis per foot instead of the composite:\n"
|
||||
"#\n"
|
||||
"# joydev 1 Pedals\n"
|
||||
"# joyaxis Y axis LeftPedal lever deadzone 0.05\n"
|
||||
"# joyaxis RZ axis RightPedal lever deadzone 0.05\n"
|
||||
"\n"
|
||||
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
||||
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// pad <button> button <addr> [toggle]
|
||||
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||
// joydev <slot> [product-name substring...]
|
||||
// joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||
// joyaxis <src> axis <axis> [invert] [lever] [deadzone <d>] [rate <n>]
|
||||
// joybutton <n> button <addr> [toggle]
|
||||
// joyhat <n> <up|down|left|right> button <addr>
|
||||
//
|
||||
@@ -37,6 +37,13 @@
|
||||
// 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
|
||||
@@ -136,6 +143,7 @@ struct PadJoyAxisBinding
|
||||
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
|
||||
};
|
||||
|
||||
+101
-7
@@ -42,6 +42,48 @@ namespace
|
||||
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
//
|
||||
// RP412INPUTFOCUS - do the controls answer only while the game is the
|
||||
// window in front? On unless the file says 0.
|
||||
//
|
||||
// The pod was the only thing running on its cabinet, so the virtual
|
||||
// RIO reads the key state directly rather than waiting on the message
|
||||
// pump. That is the right call for latency and it is why the pedals
|
||||
// feel like pedals - but a direct read is a read of the WHOLE
|
||||
// keyboard, whatever has focus, so a player alt-tabbed into a text
|
||||
// editor was flying the pod with every note they typed.
|
||||
//
|
||||
Logical InputNeedsFocus()
|
||||
{
|
||||
static int setting = -1;
|
||||
if (setting < 0)
|
||||
{
|
||||
const char *value = getenv("RP412INPUTFOCUS");
|
||||
setting = (value != NULL && *value == '0') ? 0 : 1;
|
||||
}
|
||||
return setting ? True : False;
|
||||
}
|
||||
|
||||
//
|
||||
// Whether the foreground window is one of OURS - the process, not one
|
||||
// particular handle. The cockpit is a shell full of child panes, the
|
||||
// exploded view is six windows of its own and the plasma glass
|
||||
// another, so any of them being in front is the game being in front.
|
||||
// Matching a single HWND would drop the controls the moment somebody
|
||||
// clicked an MFD.
|
||||
//
|
||||
Logical ProcessHasFocus()
|
||||
{
|
||||
HWND foreground = GetForegroundWindow();
|
||||
if (foreground == NULL)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
DWORD foreground_process = 0;
|
||||
GetWindowThreadProcessId(foreground, &foreground_process);
|
||||
return (foreground_process == GetCurrentProcessId()) ? True : False;
|
||||
}
|
||||
|
||||
//
|
||||
// A generic stick axis is already normalized -1..1, so the deadzone
|
||||
// is a plain cut about centre with the remainder rescaled - press
|
||||
@@ -65,6 +107,25 @@ namespace
|
||||
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
|
||||
@@ -212,6 +273,11 @@ PadRIO::PadRIO()
|
||||
activeInstance = this;
|
||||
|
||||
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
||||
DEBUG_STREAM << "PadRIO: controls "
|
||||
<< (InputNeedsFocus()
|
||||
? "answer only while the game window has focus"
|
||||
: "answer whether the game has focus or not (RP412INPUTFOCUS=0)")
|
||||
<< "\n" << std::flush;
|
||||
|
||||
//
|
||||
// Only open DirectInput when the profile actually asks for it. A
|
||||
@@ -371,6 +437,25 @@ void
|
||||
}
|
||||
lastPollTick = now;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Is the game the window in front? Every source below is gated on
|
||||
// this - keyboard, pad and stick alike.
|
||||
//
|
||||
// Gated rather than skipped, and that is the whole trick: each
|
||||
// source reads as RELEASED instead of the poll returning early, so
|
||||
// the diffs further down turn whatever was held at the moment you
|
||||
// switched away into proper release events. Bail out instead and a
|
||||
// key held on alt-tab stays down until you come back, which is the
|
||||
// stuck throttle this is meant to prevent rather than cause.
|
||||
//
|
||||
// The throttle accumulator is the deliberate exception. It is the
|
||||
// pod's one sticky axis and it integrates what the controls ask
|
||||
// for, so controls asking for nothing simply stop moving it - you
|
||||
// come back to the speed you left, not to a dead stop.
|
||||
//---------------------------------------------------------------
|
||||
Logical input_live =
|
||||
(!InputNeedsFocus() || ProcessHasFocus()) ? True : False;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Find / keep the XInput pad. Probing empty slots is slow, so an
|
||||
// absent pad is only re-probed every 3 seconds.
|
||||
@@ -379,7 +464,7 @@ void
|
||||
memset(&pad, 0, sizeof(pad));
|
||||
Logical pad_live = False;
|
||||
|
||||
if (padIndex >= 0)
|
||||
if (input_live && padIndex >= 0)
|
||||
{
|
||||
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
|
||||
if (!pad_live)
|
||||
@@ -388,7 +473,7 @@ void
|
||||
padIndex = -1;
|
||||
}
|
||||
}
|
||||
if (padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
||||
if (input_live && padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
||||
{
|
||||
lastPadCheckTick = now;
|
||||
for (DWORD i = 0; i < 4; ++i)
|
||||
@@ -423,7 +508,7 @@ void
|
||||
for (int i = 0; i < profile.keyButtonCount; ++i)
|
||||
{
|
||||
PadKeyButtonBinding *binding = &profile.keyButtons[i];
|
||||
Logical down = KeyDown(binding->virtualKey);
|
||||
Logical down = input_live && KeyDown(binding->virtualKey);
|
||||
if (binding->toggle && down && !binding->wasDown)
|
||||
{
|
||||
binding->latched = !binding->latched;
|
||||
@@ -476,8 +561,15 @@ void
|
||||
{
|
||||
joyDevice[slot] = -1;
|
||||
}
|
||||
if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 ||
|
||||
profile.joyHatCount > 0)
|
||||
//
|
||||
// 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)
|
||||
@@ -595,7 +687,7 @@ void
|
||||
for (int i = 0; i < profile.keyAxisCount; ++i)
|
||||
{
|
||||
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
|
||||
if (KeyDown(binding->virtualKey))
|
||||
if (input_live && KeyDown(binding->virtualKey))
|
||||
{
|
||||
if (binding->mode == BindKeyRate)
|
||||
{
|
||||
@@ -686,7 +778,9 @@ void
|
||||
throttleLever = True;
|
||||
continue;
|
||||
}
|
||||
Scalar value = JoyAxisValue(raw, binding->deadzone);
|
||||
Scalar value = binding->lever
|
||||
? JoyLeverValue(raw, binding->deadzone)
|
||||
: JoyAxisValue(raw, binding->deadzone);
|
||||
if (binding->rate > 0.0f)
|
||||
{
|
||||
rate[binding->axis] += value * binding->rate;
|
||||
|
||||
@@ -249,14 +249,51 @@ void ParticleEmitter::Execute()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Drop everything bound to the device we were last given.
|
||||
//
|
||||
// Null-safe, and it clears what it drops. Neither was true before: this
|
||||
// runs on the device-lost path ahead of a Reset, where a texture that
|
||||
// never loaded (a missing VIDEO\particles.png is enough) left one of
|
||||
// these NULL and took the Reset down with it, and a released pointer
|
||||
// left in place is a dangling one the moment anything looks again.
|
||||
//
|
||||
void ParticleEngine::Destroy()
|
||||
{
|
||||
mVertBuffer->Release();
|
||||
mParticleTexture->Release();
|
||||
if (mVertBuffer != NULL)
|
||||
{
|
||||
mVertBuffer->Release();
|
||||
mVertBuffer = NULL;
|
||||
}
|
||||
if (mParticleTexture != NULL)
|
||||
{
|
||||
mParticleTexture->Release();
|
||||
mParticleTexture = NULL;
|
||||
}
|
||||
//
|
||||
// The paint paths test this before touching anything, so clearing it
|
||||
// makes the gap between a Destroy and the next Initialize safe.
|
||||
//
|
||||
mDevice = NULL;
|
||||
}
|
||||
|
||||
void ParticleEngine::Initialize(LPDIRECT3DDEVICE9 device)
|
||||
{
|
||||
//
|
||||
// Whatever is still held belongs to the PREVIOUS device, and holding
|
||||
// it kept that device alive. A fresh renderer is built per mission,
|
||||
// so a new device used to arrive here while the old one's vertex
|
||||
// buffer (D3DPOOL_DEFAULT) and texture still referenced it -
|
||||
// ~DPLRenderer's release never reached zero and the whole device
|
||||
// survived the race that made it, back buffer and depth buffer and
|
||||
// all. That is one leaked render target per race.
|
||||
//
|
||||
// The device-lost path already released before re-initialising; this
|
||||
// is the same contract for the case where the device is not lost but
|
||||
// replaced.
|
||||
//
|
||||
Destroy();
|
||||
|
||||
mDevice = device;
|
||||
memset(mInstalledEffects, 0, sizeof(mInstalledEffects));
|
||||
|
||||
|
||||
+145
-8
@@ -192,7 +192,7 @@ void SVGA16::BuildWindows(unsigned int width, unsigned int height, bool windowed
|
||||
mPresentParams[j].hDeviceWindow = gaugeWindows[j];
|
||||
mPresentParams[j].Flags = 0;
|
||||
mPresentParams[j].FullScreen_RefreshRateInHz = (windowed)?D3DPRESENT_RATE_DEFAULT:60;
|
||||
mPresentParams[j].PresentationInterval = D3DPRESENT_RATE_DEFAULT;
|
||||
mPresentParams[j].PresentationInterval = RPPresentationInterval();
|
||||
mPresentParams[j].BackBufferFormat = D3DFMT_R5G6B5;
|
||||
//pp.EnableAutoDepthStencil = TRUE;
|
||||
//pp.AutoDepthStencilFormat = D3DFMT_D24X8;
|
||||
@@ -3828,6 +3828,13 @@ static LRESULT CALLBACK
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
static WNDPROC gCockpitBaseProc = NULL;
|
||||
|
||||
//
|
||||
// Which window we subclassed, so the destructor can put its own proc
|
||||
// back. The shell is the GAME window: it outlives the cockpit and
|
||||
// carries the console screen from one race to the next.
|
||||
//
|
||||
static HWND gCockpitShellWindow = NULL;
|
||||
|
||||
static LRESULT CALLBACK
|
||||
CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
@@ -4358,6 +4365,15 @@ SVGA16::SVGA16(
|
||||
// Split-view mode: decide before BuildWindows so the packed gauge
|
||||
// windows can stay hidden.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
// Before anything else: the constructor calls Update() below, and
|
||||
// both of these are read in there. mDisplayToUpdate was only being
|
||||
// set at the END of the constructor, so that first pass indexed the
|
||||
// display arrays with whatever was on the stack.
|
||||
//
|
||||
mDisplayToUpdate = 0;
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
|
||||
splitViews = False;
|
||||
cockpitViewscreen = NULL;
|
||||
Logical explodedViews = False;
|
||||
@@ -4679,9 +4695,25 @@ SVGA16::SVGA16(
|
||||
GetClientRect(cockpit, &inner);
|
||||
LayoutCockpit(inner.right, inner.bottom);
|
||||
|
||||
// catch maximise / restore / drag-resize and re-fit
|
||||
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
||||
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
||||
//
|
||||
// Catch maximise / restore / drag-resize and re-fit - ONCE.
|
||||
//
|
||||
// The destructor puts the original proc back, so ordinarily
|
||||
// this window is unsubclassed by the time a second race
|
||||
// builds a new cockpit. The guard is for the case where it
|
||||
// was not: subclassing an already-subclassed window makes
|
||||
// SetWindowLongPtr hand back CockpitShellProc itself as the
|
||||
// "original", and the proc below then chains to itself on
|
||||
// every single message until the stack runs out. That is a
|
||||
// stack overflow a few frames into the second race, with no
|
||||
// hint of a cause in the log.
|
||||
//
|
||||
if (gCockpitShellWindow != cockpit)
|
||||
{
|
||||
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
||||
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
||||
gCockpitShellWindow = cockpit;
|
||||
}
|
||||
|
||||
//
|
||||
// Sticky placement for the shell. Position AND size: nothing
|
||||
@@ -4917,6 +4949,30 @@ SVGA16::~SVGA16()
|
||||
cockpitViewscreen = NULL;
|
||||
}
|
||||
|
||||
//
|
||||
// Give the game window its own proc back, and stop answering for a
|
||||
// cockpit that is about to stop existing.
|
||||
//
|
||||
// The window survives us - it is the one that shows the console
|
||||
// screen between races - so both of these outlived their subject.
|
||||
// The subclass was the worse of the two: the next race re-subclassed
|
||||
// the same window and CockpitShellProc ended up chained to itself.
|
||||
// activeCockpit was the quieter one, left pointing at this object
|
||||
// after it was freed, ready for the next WM_SIZE to lay out a
|
||||
// cockpit that had already gone.
|
||||
//
|
||||
if (gCockpitShellWindow != NULL)
|
||||
{
|
||||
SetWindowLongPtrA(
|
||||
gCockpitShellWindow, GWLP_WNDPROC, (LONG_PTR) gCockpitBaseProc);
|
||||
gCockpitShellWindow = NULL;
|
||||
gCockpitBaseProc = NULL;
|
||||
}
|
||||
if (activeCockpit == this)
|
||||
{
|
||||
activeCockpit = NULL;
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -4997,12 +5053,16 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
GaugeRenderer *renderer = application->GetGaugeRenderer();
|
||||
if (!valid || renderer == NULL)
|
||||
{
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
CLEAR_SCREEN_COPY();
|
||||
return False; // Do no more!
|
||||
}
|
||||
|
||||
if (++mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
// one display per call; the rotation steps at the end of the function
|
||||
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
{
|
||||
mDisplayToUpdate = 0;
|
||||
}
|
||||
|
||||
//Top MFD's
|
||||
L4GraphicsPort *UL = static_cast<L4GraphicsPort*>(renderer->GetGraphicsPort("auxUL2"));
|
||||
@@ -5045,7 +5105,10 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
lrMask |= (lrMask << 16);
|
||||
} else
|
||||
{
|
||||
//No MFDs to draw, break out early
|
||||
//No MFDs to draw, break out early. The sweep counter resets
|
||||
//too: leaving it part-used would keep the renderer in its
|
||||
//copy phase, and it never draws another gauge while there.
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
return False;
|
||||
}
|
||||
} else
|
||||
@@ -5057,7 +5120,8 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
||||
} else
|
||||
{
|
||||
//No secondary, skip
|
||||
//No secondary, skip - and end the sweep, as above.
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
return False;
|
||||
}
|
||||
}
|
||||
@@ -5227,7 +5291,80 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
// if (end.ticks - start.ticks > 100)
|
||||
// end = start;
|
||||
|
||||
return False; // True == 'more to do'
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// Step the rotation, and say "more to do" until every display has
|
||||
// had its turn.
|
||||
//
|
||||
// This used to return False unconditionally, which told the gauge
|
||||
// renderer its copy phase was over after a SINGLE display. A full
|
||||
// gauge sweep - one gauge per background pass, so as many passes as
|
||||
// there are active gauges - therefore refreshed one display, and the
|
||||
// map, one of three, came round only every third sweep.
|
||||
//
|
||||
// That is invisible with frame time to spare, because the background
|
||||
// loop keeps running until the frame budget is used up and gets
|
||||
// through several sweeps. On a big map the 3D foreground eats the
|
||||
// whole budget, the loop drops to the one pass per frame it is
|
||||
// guaranteed, and the map goes seconds between refreshes - which is
|
||||
// what the field reports describe, on exactly those maps. A death
|
||||
// makes the renderer skip every static object, the budget frees up,
|
||||
// and the backlog drains at once: the display appears to come back
|
||||
// to life, which is the tell that led here.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
mDisplayToUpdate++;
|
||||
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||
{
|
||||
mDisplayToUpdate = 0;
|
||||
}
|
||||
|
||||
if (++mDisplaysCopiedThisPass < NUMGAUGEWINDOWS)
|
||||
{
|
||||
return True; // call again - there are displays waiting
|
||||
}
|
||||
mDisplaysCopiedThisPass = 0;
|
||||
|
||||
//
|
||||
// RP412GAUGEDIAG=1 reports how often the displays are actually being
|
||||
// refreshed. Pixel-watching from outside cannot tell a display that
|
||||
// is not refreshing from one whose picture simply is not changing,
|
||||
// and that ambiguity is exactly what makes "my map froze" hard to
|
||||
// pin down. This counts the real thing.
|
||||
//
|
||||
{
|
||||
static int diagnostics = -1;
|
||||
if (diagnostics < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412GAUGEDIAG");
|
||||
diagnostics = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diagnostics)
|
||||
{
|
||||
static unsigned long window_start = 0;
|
||||
static int sweeps = 0;
|
||||
unsigned long now = GetTickCount();
|
||||
++sweeps;
|
||||
if (window_start == 0)
|
||||
{
|
||||
window_start = now;
|
||||
}
|
||||
else if (now - window_start >= 2000)
|
||||
{
|
||||
// tenths, by hand: whole sweeps per second rounds the
|
||||
// interesting cases - a starved pipeline managing two
|
||||
// thirds of a sweep a second reads as a flat "0/s".
|
||||
int tenths = sweeps * 10000 / (int)(now - window_start);
|
||||
DEBUG_STREAM << "GaugeDiag: " << sweeps << " display sweep(s) in "
|
||||
<< (now - window_start) << " ms ("
|
||||
<< (tenths / 10) << '.' << (tenths % 10) << "/s, "
|
||||
<< NUMGAUGEWINDOWS << " displays each)\n" << std::flush;
|
||||
window_start = now;
|
||||
sweeps = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return False; // the sweep is complete
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -293,6 +293,17 @@ private:
|
||||
|
||||
int mDisplayToUpdate;
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// How many displays this copy pass has refreshed.
|
||||
//
|
||||
// The gauge renderer's copy phase ends the moment Update() reports it
|
||||
// has finished, and Update() reported that after ONE display - so a
|
||||
// whole gauge sweep refreshed a single display, and the map, one of
|
||||
// three, came round only every third sweep. Counting them out means
|
||||
// one sweep refreshes all of them.
|
||||
//------------------------------------------------------------------
|
||||
int mDisplaysCopiedThisPass;
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and
|
||||
// the rotated map render as their own desktop windows; the packed
|
||||
|
||||
+275
-12
@@ -17,6 +17,10 @@
|
||||
#include "..\munga\nttmgr.h"
|
||||
#include "..\munga\app.h"
|
||||
#include "l4particles.h"
|
||||
#include "l4padrio.h" // PadRIO::IsActive, for the per-frame lamp sweep
|
||||
#include "..\munga\gaugrend.h"
|
||||
#include "..\munga\lamp.h"
|
||||
#include "..\munga\mode.h"
|
||||
#include "DXUtils.h"
|
||||
|
||||
using namespace std;
|
||||
@@ -27,6 +31,54 @@ using namespace std;
|
||||
|
||||
LPDIRECT3D9 gD3D = NULL;
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// RPPresentationInterval
|
||||
//#############################################################################
|
||||
//
|
||||
// Every device in this game is created vsync-locked, and on a machine with
|
||||
// a spare 23 cores that is the most expensive line in the build.
|
||||
//
|
||||
// The game is one thread: simulation, 3D, gauge drawing and the display
|
||||
// copies all take turns on it. The frame loop runs the foreground, then
|
||||
// spends whatever is LEFT of the frame on the background gauge work. A
|
||||
// Present that blocks until the panel's next retrace spends that remainder
|
||||
// doing nothing at all - and the gauge loop, guaranteed only a single step
|
||||
// per frame, gets exactly that single step. A pass over the gauge list
|
||||
// needs about twenty, so the cockpit falls to two passes a second and every
|
||||
// slow-tier instrument sits seconds behind.
|
||||
//
|
||||
// That is why lowering TARGETFPS "fixed" the instruments: it did not make
|
||||
// anything faster, it just made the frame long enough that there was time
|
||||
// left over after the wait.
|
||||
//
|
||||
// So this is a knob. 0 = IMMEDIATE, Present returns and the leftover frame
|
||||
// time goes to the gauges where it belongs. Tearing is the cost, and on a
|
||||
// pod cockpit whose instruments are the point, it is a cheap one.
|
||||
//
|
||||
DWORD
|
||||
RPPresentationInterval()
|
||||
{
|
||||
static DWORD
|
||||
interval = 0xFFFFFFFF;
|
||||
|
||||
if (interval == 0xFFFFFFFF)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412VSYNC");
|
||||
|
||||
interval = (setting != NULL && atoi(setting) == 0)
|
||||
? D3DPRESENT_INTERVAL_IMMEDIATE
|
||||
: D3DPRESENT_INTERVAL_DEFAULT;
|
||||
|
||||
DEBUG_STREAM << "Video: presentation interval "
|
||||
<< ((interval == D3DPRESENT_INTERVAL_IMMEDIATE)
|
||||
? "IMMEDIATE (RP412VSYNC=0)" : "vsync")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
// Single-window cockpit: viewscreen child window the scene presents into
|
||||
// (NULL = present to the device window as always).
|
||||
HWND gMainPresentWindow = NULL;
|
||||
@@ -1686,16 +1738,33 @@ DPLRenderer::DPLRenderer(
|
||||
mPresentParams.hDeviceWindow = hWnd;
|
||||
mPresentParams.Flags = 0;
|
||||
mPresentParams.FullScreen_RefreshRateInHz = (fullscreen)?60:D3DPRESENT_RATE_DEFAULT;
|
||||
mPresentParams.PresentationInterval = D3DPRESENT_RATE_DEFAULT;
|
||||
mPresentParams.PresentationInterval = RPPresentationInterval();
|
||||
mPresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
|
||||
mPresentParams.EnableAutoDepthStencil = TRUE;
|
||||
mPresentParams.AutoDepthStencilFormat = D3DFMT_D24X8;
|
||||
mPresentParams.Windowed = !fullscreen;
|
||||
if (fullscreen)
|
||||
{
|
||||
mPresentParams.BackBufferWidth = screenWidth;
|
||||
mPresentParams.BackBufferHeight = screenHeight;
|
||||
}
|
||||
//
|
||||
// The render size is asked for, not suggested - windowed as well as
|
||||
// full-screen. Left at zero, D3D sizes the back buffer to the device
|
||||
// window's client area AT THIS MOMENT, and everything downstream is
|
||||
// built from the size we asked for instead: the projection matrix
|
||||
// takes its aspect from it, and the reticle is centred on it. A
|
||||
// window that is not exactly that size therefore renders at the
|
||||
// wrong shape and gets rescaled on the way to the viewscreen pane.
|
||||
//
|
||||
// It only showed up on a SECOND race. A fresh renderer is built per
|
||||
// mission while the window carries its cockpit placement across, so
|
||||
// the first race creates its device against a still-bordered window
|
||||
// - near enough the asked-for size to pass - and the next one
|
||||
// against the borderless full-monitor client, which on a 3440x1440
|
||||
// panel meant a 1.778 image drawn across a 2.389 target.
|
||||
//
|
||||
// -fit picks a render size to land on the viewscreen 1:1, so honour
|
||||
// it: the back buffer is that size, and Present scales it to the
|
||||
// pane in one uniform step.
|
||||
//
|
||||
mPresentParams.BackBufferWidth = screenWidth;
|
||||
mPresentParams.BackBufferHeight = screenHeight;
|
||||
|
||||
HRESULT hr;
|
||||
|
||||
@@ -1725,6 +1794,15 @@ DPLRenderer::DPLRenderer(
|
||||
//}
|
||||
//DEBUG_STREAM<<"**************************"<<std::endl<<"**************************"<<std::endl<<std::flush;
|
||||
|
||||
//
|
||||
// NULL before anything can leave this constructor early. It was not
|
||||
// in the initialiser list, so until CreateDevice wrote it the member
|
||||
// held whatever was on the stack - and the bail-out below it, plus
|
||||
// the one that has always been here, both run the destructor and its
|
||||
// SAFE_RELEASE(mDevice) over exactly that.
|
||||
//
|
||||
mDevice = NULL;
|
||||
|
||||
if (mPrimaryIndex == NULL)
|
||||
{
|
||||
DEBUG_STREAM<<"Unable to locate a suitable primary device index."<<std::endl<<std::flush;
|
||||
@@ -1732,16 +1810,76 @@ DPLRenderer::DPLRenderer(
|
||||
return;
|
||||
}
|
||||
|
||||
V(gD3D->CreateDevice(*mPrimaryIndex, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &mPresentParams, &mDevice));
|
||||
//
|
||||
// RP412VERTEXPROC=hw asks the GPU to transform vertices instead of
|
||||
// this thread.
|
||||
//
|
||||
// The device has always been created SOFTWARE_VERTEXPROCESSING - every
|
||||
// vertex on the track transformed and lit on the CPU, on the one core
|
||||
// this game uses for everything. That was not a choice when the engine
|
||||
// was written; there was no hardware to hand it to. There is now, and
|
||||
// the foreground is spending 17 ms of an 18 ms frame while the gauge
|
||||
// loop starves on the 1 ms left over.
|
||||
//
|
||||
// On by default, and sw is the way back. Fixed-function T&L is not
|
||||
// bit-identical between the old software path and a driver, so the
|
||||
// escape hatch stays - but the picture was checked against both and
|
||||
// the difference is not the one worth defending. A cockpit whose
|
||||
// instruments update twice a second is.
|
||||
//
|
||||
DWORD vertex_processing = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
|
||||
{
|
||||
const char *setting = getenv("RP412VERTEXPROC");
|
||||
if (setting == NULL || (*setting != 's' && *setting != 'S'))
|
||||
{
|
||||
D3DCAPS9 caps;
|
||||
if (SUCCEEDED(gD3D->GetDeviceCaps(*mPrimaryIndex, D3DDEVTYPE_HAL, &caps))
|
||||
&& (caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) != 0)
|
||||
{
|
||||
vertex_processing = D3DCREATE_HARDWARE_VERTEXPROCESSING;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "Video: adapter has no hardware T&L - "
|
||||
<< "staying on software vertex processing\n" << std::flush;
|
||||
}
|
||||
}
|
||||
DEBUG_STREAM << "Video: vertex processing "
|
||||
<< ((vertex_processing == D3DCREATE_HARDWARE_VERTEXPROCESSING)
|
||||
? "HARDWARE" : "software (RP412VERTEXPROC=sw)")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
V(gD3D->CreateDevice(*mPrimaryIndex, D3DDEVTYPE_HAL, hWnd, vertex_processing, &mPresentParams, &mDevice));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
DEBUG_STREAM<<"Couldn't create HARDWARE_VERTEXPROCESSING device."<<std::endl<<std::flush;
|
||||
DEBUG_STREAM<<"Couldn't create the requested device - falling back to software vertex processing."<<std::endl<<std::flush;
|
||||
|
||||
V(gD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &mPresentParams, &mDevice));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
PostQuitMessage(1);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// PostQuitMessage is a message, not a return. The fallback used to
|
||||
// post one and then carry straight on into the Clear below, which
|
||||
// dereferenced a device that was never created - so a machine that
|
||||
// could not give us the mode we asked for died on an access
|
||||
// violation instead of saying so.
|
||||
//
|
||||
// What was asked for goes in the line, because that is the question
|
||||
// this failure raises: the back buffer is the requested size now,
|
||||
// windowed as well as full-screen, so a request the adapter will not
|
||||
// meet is the thing to look at first.
|
||||
//
|
||||
if (FAILED(hr) || mDevice == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "DPLRenderer: no D3D device for a "
|
||||
<< mPresentParams.BackBufferWidth << "x"
|
||||
<< mPresentParams.BackBufferHeight
|
||||
<< (mPresentParams.Windowed ? " windowed" : " full-screen")
|
||||
<< " back buffer (hr=0x" << std::hex << hr << std::dec
|
||||
<< ") - giving up\n" << std::flush;
|
||||
PostQuitMessage(1);
|
||||
return;
|
||||
}
|
||||
|
||||
mDevice->Clear(0, NULL, D3DCLEAR_TARGET, 0xFF000000, 0.0f, 0);
|
||||
@@ -3555,6 +3693,16 @@ DPLRenderer::~DPLRenderer()
|
||||
// the next race of the single-binary loop - drop them with the device
|
||||
d3d_OBJECT::FlushTextureCache();
|
||||
|
||||
//
|
||||
// The particle engine is one of those caches and was missed. Its
|
||||
// vertex buffer is D3DPOOL_DEFAULT and its texture belongs to this
|
||||
// device, so while they were held the release below never reached
|
||||
// zero: every race left a whole live device behind it, and the next
|
||||
// race's Initialize was the only thing that ever let one go. Drop
|
||||
// them here and the device dies with the mission that made it.
|
||||
//
|
||||
ParticleEngine::Destroy();
|
||||
|
||||
SAFE_RELEASE(mDevice);
|
||||
SAFE_RELEASE(gD3D);
|
||||
//STUBBED: DPL RB 1/14/07
|
||||
@@ -5983,11 +6131,84 @@ void
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Execute Method, performs the rendering of one frame
|
||||
//
|
||||
//
|
||||
//===========================================================================
|
||||
// RPSweepCockpitLamps
|
||||
//
|
||||
// Push the cockpit lamp STATE once per frame, instead of once per gauge
|
||||
// cycle.
|
||||
//
|
||||
// The on-screen vRIO buttons light themselves from PadRIO::GetLampState,
|
||||
// and they redraw with their MFD strip. What FILLS that store is
|
||||
// LampManager::Update -> AssertNewLampValue -> SetLamp, and that rides
|
||||
// the gauge renderer's FOREGROUND turn - which comes round only once per
|
||||
// full gauge cycle. On a busy map the cycle takes the best part of a
|
||||
// second, so the lit buttons froze and any flash stalled while the 3D
|
||||
// view, a separate per-frame render, stayed perfectly smooth. BT411 saw
|
||||
// the same thing on its glass surround and fixed it the same way.
|
||||
//
|
||||
// It is cheap: a sweep over the lamps, no raster, and AssertNewLampValue
|
||||
// already drops anything that has not changed - so this pushes no extra
|
||||
// traffic, it only stops changes arriving late.
|
||||
//
|
||||
// Only when a PadRIO is active, i.e. cockpit-less play. With real serial
|
||||
// hardware selected the pod keeps its authentic bandwidth-paced cadence,
|
||||
// untouched. RP412LAMPSWEEP=0 restores the once-per-cycle behaviour.
|
||||
//===========================================================================
|
||||
//
|
||||
static void
|
||||
RPSweepCockpitLamps()
|
||||
{
|
||||
static int
|
||||
enabled = -1;
|
||||
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char
|
||||
*setting = getenv("RP412LAMPSWEEP");
|
||||
|
||||
enabled = (setting != NULL && setting[0] == '0') ? 0 : 1;
|
||||
}
|
||||
if (!enabled || !PadRIO::IsActive() || application == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Only while a mission is actually running. This is called from the
|
||||
// top of the frame, ahead of the state switch below, so it would
|
||||
// otherwise fire while the mission is still being built and the
|
||||
// gauges do not exist yet.
|
||||
//
|
||||
if (application->GetApplicationState() != Application::RunningMission)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GaugeRenderer
|
||||
*renderer = application->GetGaugeRenderer();
|
||||
ModeManager
|
||||
*modes = application->GetModeManager();
|
||||
|
||||
if (renderer != NULL && modes != NULL)
|
||||
{
|
||||
LampManager
|
||||
*lamps = renderer->GetLampManager();
|
||||
|
||||
if (lamps != NULL)
|
||||
{
|
||||
lamps->Update(modes->GetModeMask());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::InterestingEntityIterator* all_iterator)
|
||||
{
|
||||
Component *component;
|
||||
HRESULT hr;
|
||||
|
||||
RPSweepCockpitLamps(); // keep the lit buttons tracking the sim (see above)
|
||||
|
||||
// timing variables
|
||||
__int64 ticks = HiResNowTicks();
|
||||
#ifdef LOGFRAMERATE
|
||||
@@ -6339,6 +6560,48 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
|
||||
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
|
||||
|
||||
//
|
||||
// RP412GAUGEDIAG=1: frames per second, on the same 2-second window the
|
||||
// display-sweep line uses so the two read side by side.
|
||||
//
|
||||
// Without this the gauge rate has to be argued about rather than
|
||||
// measured. A sweep rate far below the frame rate means the gauges are
|
||||
// STARVED - the 3D is fine and the background loop is not getting
|
||||
// through its cycle. A sweep rate that tracks the frame rate means
|
||||
// there is nothing wrong with the gauges at all and the frame itself
|
||||
// is the problem. Those two want opposite fixes, and the sweep line
|
||||
// alone cannot tell them apart.
|
||||
//
|
||||
{
|
||||
static int diagnostics = -1;
|
||||
if (diagnostics < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412GAUGEDIAG");
|
||||
diagnostics = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diagnostics)
|
||||
{
|
||||
static unsigned long window_start = 0;
|
||||
static int frames = 0;
|
||||
unsigned long now = GetTickCount();
|
||||
++frames;
|
||||
if (window_start == 0)
|
||||
{
|
||||
window_start = now;
|
||||
}
|
||||
else if (now - window_start >= 2000)
|
||||
{
|
||||
int tenths = frames * 10000 / (int)(now - window_start);
|
||||
DEBUG_STREAM << "FrameDiag: " << frames << " frame(s) in "
|
||||
<< (now - window_start) << " ms ("
|
||||
<< (tenths / 10) << '.' << (tenths % 10) << "/s)\n"
|
||||
<< std::flush;
|
||||
window_start = now;
|
||||
frames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hand the whole target back
|
||||
if (mPresentationAspect > 0.0f)
|
||||
{
|
||||
|
||||
@@ -643,3 +643,11 @@ public:
|
||||
};
|
||||
|
||||
extern LPDIRECT3D9 gD3D;
|
||||
|
||||
//
|
||||
// The presentation interval every device is created with. RP412VSYNC=0
|
||||
// makes it IMMEDIATE, so Present returns instead of waiting for the
|
||||
// panel's retrace - see the definition in L4VIDEO.cpp for why that
|
||||
// matters far more here than tearing does.
|
||||
//
|
||||
DWORD RPPresentationInterval();
|
||||
|
||||
+200
-51
@@ -2364,7 +2364,76 @@ ReticleRenderable::ReticleRenderable(
|
||||
|
||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
||||
|
||||
device->CreateVertexBuffer(sizeof(L4VERTEX_2D) * 8, D3DUSAGE_WRITEONLY, L4VERTEX_2D_FVF, D3DPOOL_MANAGED, &mVB, NULL);
|
||||
device->CreateVertexBuffer(sizeof(L4VERTEX_2D) * crosshairVertexCount, D3DUSAGE_WRITEONLY, L4VERTEX_2D_FVF, D3DPOOL_MANAGED, &mVB, NULL);
|
||||
|
||||
//
|
||||
// Nothing is written here. The crosshair is measured against the
|
||||
// render target it will actually be drawn into, and that is not
|
||||
// known to be the renderer's requested size - see RebuildCrosshair.
|
||||
//
|
||||
mBuiltWidth = 0.0f;
|
||||
mBuiltHeight = 0.0f;
|
||||
mBuiltOriginX = -1.0f;
|
||||
mBuiltOriginY = -1.0f;
|
||||
RebuildCrosshair();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Centre the crosshair on the target it is about to be drawn into.
|
||||
//
|
||||
// It used to be baked once, in the constructor, from the renderer's
|
||||
// GetWidth()/GetHeight() - the size the renderer ASKED for. A windowed
|
||||
// device does not necessarily get it: BackBufferWidth/Height are only
|
||||
// filled in for fullscreen (L4VIDEO.cpp), so windowed the back buffer is
|
||||
// whatever the device window's client area happened to be when the
|
||||
// device was created. A fresh renderer is built per mission while the
|
||||
// window carries its restored cockpit placement across, so the two agree
|
||||
// on the first race and can disagree on the next one - which put the
|
||||
// crosshair off-centre by half the difference, and only ever on a second
|
||||
// game.
|
||||
//
|
||||
// Measuring the viewport at draw time settles it for every case at once:
|
||||
// the windowed mismatch, a device reset, and the podium's pillarbox crop.
|
||||
//
|
||||
void ReticleRenderable::RebuildCrosshair()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (mVB == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
||||
if (device == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
D3DVIEWPORT9 viewport;
|
||||
if (FAILED(device->GetViewport(&viewport)) ||
|
||||
viewport.Width == 0 || viewport.Height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float width = (float) viewport.Width;
|
||||
float height = (float) viewport.Height;
|
||||
//
|
||||
// Origin included: a viewport that moves without resizing still
|
||||
// carries the crosshair with it.
|
||||
//
|
||||
float origin_x = (float) viewport.X;
|
||||
float origin_y = (float) viewport.Y;
|
||||
if (width == mBuiltWidth && height == mBuiltHeight &&
|
||||
origin_x == mBuiltOriginX && origin_y == mBuiltOriginY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mBuiltWidth = width;
|
||||
mBuiltHeight = height;
|
||||
mBuiltOriginX = origin_x;
|
||||
mBuiltOriginY = origin_y;
|
||||
|
||||
L4VERTEX_2D *verts;
|
||||
mVB->Lock(0, 0, (void**)&verts, 0);
|
||||
@@ -2372,62 +2441,125 @@ ReticleRenderable::ReticleRenderable(
|
||||
DWORD color = D3DCOLOR_XRGB(0, 128, 0);
|
||||
float segmentLen = 5.0f / 192.0f;
|
||||
float spread = 5.0f / 256.0f;
|
||||
float width = myRenderer->GetWidth();
|
||||
float height = myRenderer->GetHeight();
|
||||
float centerX = width / 2.0f;
|
||||
float centerY = height / 2.0f;
|
||||
//
|
||||
// Pre-transformed vertices are absolute screen pixels - the viewport
|
||||
// clips them but does not shift them - so its origin is carried here.
|
||||
//
|
||||
float centerX = (float) viewport.X + width / 2.0f;
|
||||
float centerY = (float) viewport.Y + height / 2.0f;
|
||||
|
||||
// top segment
|
||||
verts[0].x = centerX;
|
||||
verts[0].y = centerY - (spread + segmentLen) * height;
|
||||
verts[0].z = 0.0f;
|
||||
verts[0].rhw = 1.0f;
|
||||
verts[0].color = color;
|
||||
//
|
||||
// Land on the pixel CENTRE, not the corner, so the quads below span
|
||||
// whole pixels: an arm one pixel wide about a centre of x.5 runs from
|
||||
// x.0 to x+1.0 and covers exactly one column.
|
||||
//
|
||||
centerX = (float)(int) centerX + 0.5f;
|
||||
centerY = (float)(int) centerY + 0.5f;
|
||||
|
||||
verts[1].x = centerX;
|
||||
verts[1].y = centerY - spread * height;
|
||||
verts[1].z = 0.0f;
|
||||
verts[1].rhw = 1.0f;
|
||||
verts[1].color = color;
|
||||
//
|
||||
// Worth a line in the log: it names the target, the renderer's
|
||||
// requested size and where the crosshair actually landed, so a
|
||||
// report of it being off says which of the three moved.
|
||||
//
|
||||
DEBUG_STREAM << "Reticle: centred at " << centerX << "," << centerY
|
||||
<< " on the " << viewport.Width << "x" << viewport.Height
|
||||
<< " viewport at " << viewport.X << "," << viewport.Y
|
||||
<< " (renderer asked for " << myRenderer->GetWidth() << "x"
|
||||
<< myRenderer->GetHeight() << ")";
|
||||
if (viewport.Width != myRenderer->GetWidth() ||
|
||||
viewport.Height != myRenderer->GetHeight())
|
||||
{
|
||||
//
|
||||
// Not the crosshair's problem alone: the projection matrix is
|
||||
// built from the requested size too, so a target this does not
|
||||
// match is being rendered at the wrong aspect and rescaled on
|
||||
// the way to the pane.
|
||||
//
|
||||
DEBUG_STREAM << " - TARGET DISAGREES, aspect "
|
||||
<< ((float) viewport.Width / (float) viewport.Height)
|
||||
<< " drawn as "
|
||||
<< ((float) myRenderer->GetWidth() / (float) myRenderer->GetHeight());
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
|
||||
// right segment
|
||||
verts[2].x = centerX + (spread + segmentLen) * height;
|
||||
verts[2].y = centerY;
|
||||
verts[2].z = 0.0f;
|
||||
verts[2].rhw = 1.0f;
|
||||
verts[2].color = color;
|
||||
//
|
||||
// Each arm is a quad one pixel thick rather than a line. D3D9 line
|
||||
// rasterisation follows the diamond-exit rule and is free to differ
|
||||
// between drivers on a segment that runs along a pixel boundary,
|
||||
// which is how a crosshair loses one pair of arms and keeps the
|
||||
// other. Triangles have a fill rule that does not vary, so an arm
|
||||
// spanning whole pixels lands the same way everywhere - and on a
|
||||
// full-screen device, where both viewport dimensions are usually
|
||||
// even and BOTH pairs sit on boundaries, that is the difference
|
||||
// between a crosshair and nothing at all.
|
||||
//
|
||||
float inner = spread * height;
|
||||
float outer = (spread + segmentLen) * height;
|
||||
|
||||
verts[3].x = centerX + spread * height;
|
||||
verts[3].y = centerY;
|
||||
verts[3].z = 0.0f;
|
||||
verts[3].rhw = 1.0f;
|
||||
verts[3].color = color;
|
||||
//
|
||||
// Thickness follows the target the way the arms' length does, rather
|
||||
// than being one pixel whatever the resolution.
|
||||
//
|
||||
// One pixel is a width the PRESENTATION can lose. The scene goes to
|
||||
// the viewscreen pane through a stretch, and when the back buffer is
|
||||
// wider than the pane that stretch samples straight past a feature a
|
||||
// single pixel across. A second race rendering 3440 wide into the
|
||||
// 2553-wide pane is 0.74 across and 1.00 down, which took the
|
||||
// vertical arms and left the horizontal ones standing - the crosshair
|
||||
// was being drawn correctly and thrown away on the way to the glass.
|
||||
//
|
||||
// Scaling it also keeps the pod's proportions: one pixel at the 480
|
||||
// lines this was drawn for is three at 1440, not a hairline.
|
||||
//
|
||||
float thickness = (float)(int)(height / 480.0f);
|
||||
if (thickness < 1.0f)
|
||||
{
|
||||
thickness = 1.0f;
|
||||
}
|
||||
float half = thickness * 0.5f;
|
||||
|
||||
// bottom segment
|
||||
verts[4].x = centerX;
|
||||
verts[4].y = centerY + (spread + segmentLen) * height;
|
||||
verts[4].z = 0.0f;
|
||||
verts[4].rhw = 1.0f;
|
||||
verts[4].color = color;
|
||||
struct Arm
|
||||
{
|
||||
float x0, y0, x1, y1; // opposite corners, in pixels
|
||||
};
|
||||
const Arm arms[4] =
|
||||
{
|
||||
// top
|
||||
{ centerX - half, centerY - outer, centerX + half, centerY - inner },
|
||||
// right
|
||||
{ centerX + inner, centerY - half, centerX + outer, centerY + half },
|
||||
// bottom
|
||||
{ centerX - half, centerY + inner, centerX + half, centerY + outer },
|
||||
// left
|
||||
{ centerX - outer, centerY - half, centerX - inner, centerY + half }
|
||||
};
|
||||
|
||||
verts[5].x = centerX;
|
||||
verts[5].y = centerY + spread * height;
|
||||
verts[5].z = 0.0f;
|
||||
verts[5].rhw = 1.0f;
|
||||
verts[5].color = color;
|
||||
int v = 0;
|
||||
for (int a = 0; a < 4; ++a)
|
||||
{
|
||||
const Arm &arm = arms[a];
|
||||
//
|
||||
// Two triangles, corners in the order top-left, top-right,
|
||||
// bottom-left / top-right, bottom-right, bottom-left. Culling is
|
||||
// turned off for the draw, so the winding does not have to agree
|
||||
// with the renderer's global cull mode.
|
||||
//
|
||||
const float quad_x[6] =
|
||||
{ arm.x0, arm.x1, arm.x0, arm.x1, arm.x1, arm.x0 };
|
||||
const float quad_y[6] =
|
||||
{ arm.y0, arm.y0, arm.y1, arm.y0, arm.y1, arm.y1 };
|
||||
|
||||
// left segment
|
||||
verts[6].x = centerX - (spread + segmentLen) * height;
|
||||
verts[6].y = centerY;
|
||||
verts[6].z = 0.0f;
|
||||
verts[6].rhw = 1.0f;
|
||||
verts[6].color = color;
|
||||
|
||||
verts[7].x = centerX - spread * height;
|
||||
verts[7].y = centerY;
|
||||
verts[7].z = 0.0f;
|
||||
verts[7].rhw = 1.0f;
|
||||
verts[7].color = color;
|
||||
for (int c = 0; c < 6; ++c)
|
||||
{
|
||||
verts[v].x = quad_x[c];
|
||||
verts[v].y = quad_y[c];
|
||||
verts[v].z = 0.0f;
|
||||
verts[v].rhw = 1.0f;
|
||||
verts[v].color = color;
|
||||
++v;
|
||||
}
|
||||
}
|
||||
Verify(v == crosshairVertexCount);
|
||||
|
||||
mVB->Unlock();
|
||||
}
|
||||
@@ -2497,9 +2629,26 @@ void ReticleRenderable::Render(int pass, const D3DXMATRIX *viewTransform)
|
||||
{
|
||||
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
|
||||
|
||||
//
|
||||
// The target is whatever is bound right now, so ask it now. A
|
||||
// compare against the size already built means this costs one
|
||||
// GetViewport a frame and rewrites nothing until it moves.
|
||||
//
|
||||
RebuildCrosshair();
|
||||
|
||||
//
|
||||
// The arms are quads, and which way round they wind is not worth
|
||||
// making the renderer's global cull mode responsible for.
|
||||
//
|
||||
DWORD cull_mode;
|
||||
device->GetRenderState(D3DRS_CULLMODE, &cull_mode);
|
||||
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
|
||||
device->SetTexture(0, NULL);
|
||||
device->SetStreamSource(0, mVB, 0, sizeof(L4VERTEX_2D));
|
||||
device->DrawPrimitive(D3DPT_LINELIST, 0, 4);
|
||||
device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 8);
|
||||
|
||||
device->SetRenderState(D3DRS_CULLMODE, cull_mode);
|
||||
}
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -783,6 +783,19 @@ class ReticleRenderable :
|
||||
void Render(int pass, const D3DXMATRIX *viewTransform);
|
||||
|
||||
protected:
|
||||
//
|
||||
// The crosshair is written as pre-transformed vertices - screen
|
||||
// PIXELS - so it is only centred for the target it was measured
|
||||
// against. Re-measure before drawing and rebuild when that
|
||||
// target changes, rather than baking it once at construction.
|
||||
//
|
||||
void RebuildCrosshair();
|
||||
|
||||
// four arms, two triangles each
|
||||
enum { crosshairVertexCount = 24 };
|
||||
|
||||
// viewport the vertex buffer currently describes
|
||||
float mBuiltWidth, mBuiltHeight, mBuiltOriginX, mBuiltOriginY;
|
||||
|
||||
// Last known position of the reticle
|
||||
Vector2DOf<float> myOldReticlePosition;
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
<ClCompile Include="..\MUNGA\INTEREST.cpp" />
|
||||
<ClCompile Include="..\MUNGA\INTORGN.cpp" />
|
||||
<ClCompile Include="..\MUNGA\ITERATOR.cpp" />
|
||||
<ClCompile Include="..\MUNGA\INPUTSCRIPT.cpp" />
|
||||
<ClCompile Include="..\MUNGA\JMOVER.cpp" />
|
||||
<ClCompile Include="..\MUNGA\JOINT.cpp" />
|
||||
<ClCompile Include="..\MUNGA\LAMP.cpp" />
|
||||
@@ -227,6 +228,7 @@
|
||||
<ClCompile Include="..\MUNGA\WRHOUS.cpp" />
|
||||
<ClCompile Include=".\DXUtils.cpp" />
|
||||
<ClCompile Include=".\L4APP.cpp" />
|
||||
<ClCompile Include=".\L4AUDEFX.cpp" />
|
||||
<ClCompile Include=".\L4AUDHDW.cpp" />
|
||||
<ClCompile Include=".\L4AUDIO.cpp" />
|
||||
<ClCompile Include=".\L4AUDLVL.cpp" />
|
||||
@@ -432,6 +434,7 @@
|
||||
<ClInclude Include="..\MUNGA\WRHOUS.h" />
|
||||
<ClInclude Include=".\DXUtils.h" />
|
||||
<ClInclude Include=".\L4APP.H" />
|
||||
<ClInclude Include=".\L4AUDEFX.h" />
|
||||
<ClInclude Include=".\L4AUDHDW.h" />
|
||||
<ClInclude Include=".\L4AUDIO.h" />
|
||||
<ClInclude Include=".\L4AUDLVL.h" />
|
||||
|
||||
@@ -486,6 +486,9 @@
|
||||
<ClCompile Include=".\L4AUDHDW.cpp">
|
||||
<Filter>Source Files\MUNGA_L4</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include=".\L4AUDEFX.cpp">
|
||||
<Filter>Source Files\MUNGA_L4</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include=".\L4AUDIO.cpp">
|
||||
<Filter>Source Files\MUNGA_L4</Filter>
|
||||
</ClCompile>
|
||||
@@ -1058,6 +1061,9 @@
|
||||
<ClInclude Include=".\L4APP.H">
|
||||
<Filter>Header Files\MUNGA_L4</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include=".\L4AUDEFX.h">
|
||||
<Filter>Header Files\MUNGA_L4</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include=".\L4AUDHDW.h">
|
||||
<Filter>Header Files\MUNGA_L4</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -32,6 +32,13 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd)
|
||||
default.file = "";
|
||||
default.implemented = false;
|
||||
default.loop = SampleLoop::LoopAtWill;
|
||||
//
|
||||
// -1 = no buffer. Every caller tests bufferIndex >= 0 before using it as
|
||||
// an index, and this one field was being left as whatever was on the
|
||||
// stack - so "this zone does not exist" read as a real buffer whenever
|
||||
// the garbage happened to be positive, and indexed g_buffers with it.
|
||||
//
|
||||
default.bufferIndex = -1;
|
||||
|
||||
if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum)
|
||||
{
|
||||
|
||||
+158
-2
@@ -19,7 +19,40 @@
|
||||
// fade; the rest is the podium. Kept under the +30s LightsOut post so that
|
||||
// never fires while the stand is up.
|
||||
//
|
||||
const Scalar winnersCircleHoldTime = 11.0f;
|
||||
// RP412PODIUMHOLD tunes it, and RP412PODIUM=0 declines it entirely: that
|
||||
// option promises "straight to the results", and it used to get the full
|
||||
// eleven seconds against a black screen anyway, because this timer never
|
||||
// asked whether there was a podium to hold the mission open FOR. Zero
|
||||
// here means "do not override the base fade" - the stock 3 seconds.
|
||||
//
|
||||
static Scalar
|
||||
WinnersCircleHoldTime()
|
||||
{
|
||||
static Scalar
|
||||
hold = (Scalar) -1;
|
||||
|
||||
if (hold < (Scalar) 0)
|
||||
{
|
||||
const char *podium = getenv("RP412PODIUM");
|
||||
if (podium != NULL && atoi(podium) == 0)
|
||||
{
|
||||
hold = (Scalar) 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *setting = getenv("RP412PODIUMHOLD");
|
||||
hold = (setting != NULL) ? (Scalar) atof(setting) : (Scalar) 11;
|
||||
//
|
||||
// Under a second cuts into the race's own fade-out, and a
|
||||
// minute is a stuck-looking screen; both read as bugs, not
|
||||
// choices.
|
||||
//
|
||||
if (hold < (Scalar) 1) hold = (Scalar) 1;
|
||||
if (hold > (Scalar) 60) hold = (Scalar) 60;
|
||||
}
|
||||
}
|
||||
return hold;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//######################## RPPlayer__StatusMessage ######################
|
||||
@@ -214,7 +247,19 @@ void
|
||||
//
|
||||
if (application->GetApplicationState() == Application::EndingMission)
|
||||
{
|
||||
fadeTimeRemaining = winnersCircleHoldTime;
|
||||
Scalar hold = WinnersCircleHoldTime();
|
||||
if (hold > (Scalar) 0)
|
||||
{
|
||||
fadeTimeRemaining = hold;
|
||||
DEBUG_STREAM << "WinnersCircle: holding the mission open "
|
||||
<< hold << "s for the stand\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "WinnersCircle: podium off - the race fade "
|
||||
<< "stands (" << fadeTimeRemaining << "s) and the results "
|
||||
<< "come straight up\n" << std::flush;
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -382,6 +427,96 @@ void RPPlayer::ResetAfterDeath(DropZone::ReplyMessage *message)
|
||||
ForceUpdate();
|
||||
SetSimulationState(DropZoneAcquiredState);
|
||||
dropZoneLocation = message->dropZoneLocation;
|
||||
|
||||
//
|
||||
//------------------------------------------------------------------
|
||||
// Fixed-step: the RECOVERY goes on the vehicle's own step grid.
|
||||
//
|
||||
// The event queue runs on wall clock, so a Reset fired from it
|
||||
// lands between different sim steps on every run - the crash was
|
||||
// measured bit-identical between runs and the first divergence was
|
||||
// the step after the pod stood back up. The vehicle applies the
|
||||
// teleport itself at the first step one SIM second after now; the
|
||||
// message still makes its round trip below, but only for the
|
||||
// player-state bookkeeping - the handler leaves the physics to the
|
||||
// schedule it can see is pending.
|
||||
//------------------------------------------------------------------
|
||||
//
|
||||
if (Simulation::FixedStep() > (Scalar) 0 &&
|
||||
playerVehicle != NULL && playerVehicle->GetClassID() == VTVClassID)
|
||||
{
|
||||
VTV *vtv = (VTV *) playerVehicle;
|
||||
|
||||
//
|
||||
// Anchored to the DEATH and quantized to a half-second grid.
|
||||
//
|
||||
// This handler runs when the drop-zone reply finally comes off
|
||||
// the event queue, and the whole death-to-here chain is wall
|
||||
// clock - the fry retries repost at Now()+2.0, and the measured
|
||||
// arrival is about five sim-seconds after death, give or take a
|
||||
// few STEPS of queue jitter. An anchor of death+1.0 is long past
|
||||
// by then, so "fire at the next step" inherited the jitter
|
||||
// whole.
|
||||
//
|
||||
// The death stamp is the last step-exact event in the chain, so:
|
||||
// take the measured gap, add the second the old code waited, and
|
||||
// round UP to the next half-second AFTER THE DEATH. The jitter
|
||||
// is hundredths; the nearest grid boundary is tenths away; every
|
||||
// run lands in the same cell and fires on the same step. The
|
||||
// felt delay is the same six-ish seconds it has always been.
|
||||
//
|
||||
Time due;
|
||||
Time death_mark;
|
||||
if (vtv->ConsumeDeathClock(&death_mark))
|
||||
{
|
||||
Scalar gap = vtv->GetLastPerformance() - death_mark;
|
||||
const Scalar quantum = (Scalar) 0.5;
|
||||
int cells = (int)((gap + (Scalar) 1.0) / quantum) + 1;
|
||||
|
||||
due = death_mark;
|
||||
due += quantum * (Scalar) cells;
|
||||
}
|
||||
else
|
||||
{
|
||||
due = vtv->GetLastPerformance();
|
||||
due += 1.0f;
|
||||
}
|
||||
vtv->ScheduleRespawn(
|
||||
message->dropZoneLocation, due,
|
||||
(goalEntity != NULL)
|
||||
? goalEntity->localOrigin.linearPosition
|
||||
: Point3D(0.0f, 0.0f, 0.0f),
|
||||
(goalEntity != NULL) ? True : False);
|
||||
respawnScheduled = True;
|
||||
|
||||
//
|
||||
// Under the trace, say what was scheduled in run-comparable
|
||||
// terms: the pad (identity by position), and how far ahead of
|
||||
// the vehicle's clock the due time sits. Two runs that disagree
|
||||
// here diverge before the physics gets a vote.
|
||||
//
|
||||
{
|
||||
static int diag = -1;
|
||||
if (diag < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412PHYSTRACE");
|
||||
diag = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||
}
|
||||
if (diag)
|
||||
{
|
||||
char buffer[160];
|
||||
sprintf(buffer,
|
||||
"PhysTrace: respawn #%d scheduled, pad %.2f,%.2f "
|
||||
"due in %.4f sim-s\n",
|
||||
deathCount,
|
||||
(double) message->dropZoneLocation.linearPosition.x,
|
||||
(double) message->dropZoneLocation.linearPosition.z,
|
||||
(double)(Scalar)(due - vtv->GetLastPerformance()));
|
||||
DEBUG_STREAM << buffer << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Time when = Now();
|
||||
when += 1.0f;
|
||||
application->Post(HighEventPriority, this, message, when);
|
||||
@@ -422,6 +557,7 @@ void
|
||||
}
|
||||
AlwaysExecute();
|
||||
deathCount = 0;
|
||||
respawnScheduled = False;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -467,6 +603,26 @@ void
|
||||
{
|
||||
VTV *vtv = (VTV*)playerVehicle;
|
||||
Check(vtv);
|
||||
|
||||
//
|
||||
// A SCHEDULED respawn means the vehicle already holds - or has
|
||||
// already applied - its teleport and goal-flip, on its own step
|
||||
// grid. Resetting it AGAIN here, at whatever wall instant this
|
||||
// message came off the queue, would re-teleport it mid-step and
|
||||
// put the nondeterminism straight back.
|
||||
//
|
||||
// The flag, not "is fixed stepping on": this same leg also runs
|
||||
// for the FIRST spawn of the mission, where the Reset below is
|
||||
// what wakes a Mover out of its initial stasis. Gating on the
|
||||
// mode alone skipped that wake-up and parked the pod, frozen at
|
||||
// exactly its spawn point, for an entire race.
|
||||
//
|
||||
if (respawnScheduled)
|
||||
{
|
||||
respawnScheduled = False;
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
vtv->Reset(message->dropZoneLocation, VTV::RegularReset);
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,18 @@ public:
|
||||
Entity
|
||||
*goalEntity;
|
||||
|
||||
//
|
||||
// True between ResetAfterDeath handing the recovery to the vehicle's
|
||||
// step grid (fixed-step only) and the bookkeeping message coming back
|
||||
// round. The message handler must NOT Reset the vehicle again in that
|
||||
// window - but it MUST still Reset on the first spawn of the mission,
|
||||
// which is what wakes a Mover out of its initial stasis. Gating on
|
||||
// "is fixed stepping on" instead of on this flag skipped that wake-up
|
||||
// and froze the pod on its pad for the whole race.
|
||||
//
|
||||
Logical
|
||||
respawnScheduled;
|
||||
|
||||
private:
|
||||
|
||||
static const IndexEntry AttributePointers[];
|
||||
|
||||
+128
-3
@@ -803,9 +803,17 @@ void
|
||||
Check(*damageZones);
|
||||
(*damageZones)->TakeDamage(collision_damage);
|
||||
|
||||
localVelocity.angularMotion.x += 8.0f * Random - 4.0f;
|
||||
localVelocity.angularMotion.y += 8.0f * Random - 4.0f;
|
||||
localVelocity.angularMotion.z += 8.0f * Random - 4.0f;
|
||||
//
|
||||
// The tumble draws from the vehicle's OWN stream, not the
|
||||
// global Random - the global one is shared with the frame
|
||||
// loop's consumers (particles, mostly), so its position here
|
||||
// depended on how many frames had rendered. This kick goes
|
||||
// straight into physics state; it was the last wall-clocked
|
||||
// input left in the whole death cycle.
|
||||
//
|
||||
localVelocity.angularMotion.x += 8.0f * TumbleRandom() - 4.0f;
|
||||
localVelocity.angularMotion.y += 8.0f * TumbleRandom() - 4.0f;
|
||||
localVelocity.angularMotion.z += 8.0f * TumbleRandom() - 4.0f;
|
||||
}
|
||||
}
|
||||
worldLinearAcceleration = zippy_accel;
|
||||
@@ -1273,6 +1281,18 @@ VTV::VTV(
|
||||
boosterSmokeDensity = 0.0f;
|
||||
doorHitNormal = Vector3D::Identity;
|
||||
lastDoorHit = Time::Null;
|
||||
respawnPending = False;
|
||||
respawnHaveGoal = False;
|
||||
deathClockValid = False;
|
||||
|
||||
//
|
||||
// Creation order is deterministic, so each vehicle's tumble stream
|
||||
// is too - see TumbleRandom in the header.
|
||||
//
|
||||
{
|
||||
static unsigned long tumble_births = 0;
|
||||
tumbleSeed = 0x52503431UL + 7919UL * ++tumble_births;
|
||||
}
|
||||
heightAboveTerrain = 0.0f;
|
||||
forwardVelocity = 0.0f;
|
||||
hornBlast = -1;
|
||||
@@ -1685,6 +1705,101 @@ void
|
||||
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();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
@@ -3100,6 +3215,16 @@ void
|
||||
if (damageLevel >= 1.0f)
|
||||
{
|
||||
vtv->SetSimulationState(VTV::BurningState);
|
||||
|
||||
//
|
||||
// Stamp the death on the vehicle's own step clock, here at the
|
||||
// one site that declares it dead. The respawn schedule anchors
|
||||
// to this instant - the last step-exact event in the death
|
||||
// chain - so the recovery lands the same number of steps after
|
||||
// the crash on every run. Marked once; repeated damage while
|
||||
// already burning does not move it.
|
||||
//
|
||||
vtv->MarkDeathClock();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -504,6 +504,58 @@ public:
|
||||
void
|
||||
Reset(const Origin &new_origin, int reset_command);
|
||||
|
||||
//
|
||||
// A respawn that lands on this vehicle's own step grid. Under fixed
|
||||
// stepping the player's death recovery cannot ride the event queue -
|
||||
// the queue runs on wall clock, and a Reset that fires at a wall
|
||||
// instant lands between different sim steps on every run. Scheduled
|
||||
// here instead, BeginStep applies it at the first step whose clock
|
||||
// reaches 'due': same step count after death, every run, every
|
||||
// frame rate. The goal point rides along because the turn-to-face-
|
||||
// the-scorezone flip depends on the POST-reset heading, so it has
|
||||
// to happen in the same step as the teleport.
|
||||
//
|
||||
void
|
||||
ScheduleRespawn(
|
||||
const Origin &new_origin,
|
||||
const Time &due,
|
||||
const Point3D &face_toward,
|
||||
Logical have_goal
|
||||
);
|
||||
void
|
||||
BeginStep();
|
||||
|
||||
//
|
||||
// The instant this vehicle died, on its own step clock - stamped at
|
||||
// the single site that sets BurningState, which runs inside the step
|
||||
// machinery and is therefore already deterministic. The respawn
|
||||
// schedule anchors HERE rather than at the moment the drop-zone
|
||||
// reply happens to come off the event queue: the death is the last
|
||||
// step-exact event in the chain, so "one second after death" is the
|
||||
// same step count on every run. Marked once per death; consuming it
|
||||
// re-arms it for the next one.
|
||||
//
|
||||
void
|
||||
MarkDeathClock()
|
||||
{
|
||||
if (!deathClockValid)
|
||||
{
|
||||
deathClock = GetLastPerformance();
|
||||
deathClockValid = True;
|
||||
}
|
||||
}
|
||||
Logical
|
||||
ConsumeDeathClock(Time *when_out)
|
||||
{
|
||||
if (!deathClockValid)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
*when_out = deathClock;
|
||||
deathClockValid = False;
|
||||
return True;
|
||||
}
|
||||
|
||||
void
|
||||
DeathShutdown(int shutdown_command);
|
||||
|
||||
@@ -514,6 +566,38 @@ public:
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Navigation support
|
||||
protected:
|
||||
// the pending step-grid respawn - see ScheduleRespawn
|
||||
Origin
|
||||
respawnOrigin;
|
||||
Time
|
||||
respawnDue,
|
||||
deathClock;
|
||||
Point3D
|
||||
respawnGoal;
|
||||
Logical
|
||||
respawnPending,
|
||||
respawnHaveGoal,
|
||||
deathClockValid;
|
||||
|
||||
//
|
||||
// The out-of-world tumble's own random stream. The global Random is
|
||||
// shared with frame-cadence consumers - particles above all - so its
|
||||
// position when a burning pod draws from it depends on how many
|
||||
// frames have rendered, which is wall clock, which makes the tumble
|
||||
// differ between identical runs. A per-vehicle generator seeded by
|
||||
// creation order keeps the tumble looking random while drawing the
|
||||
// same kicks at the same steps every run.
|
||||
//
|
||||
unsigned long
|
||||
tumbleSeed;
|
||||
|
||||
Scalar
|
||||
TumbleRandom()
|
||||
{
|
||||
tumbleSeed = tumbleSeed * 1103515245UL + 12345UL;
|
||||
return (Scalar)((tumbleSeed >> 16) & 0x7FFF) / (Scalar) 32767;
|
||||
}
|
||||
|
||||
Scalar
|
||||
targetRangeExponent,
|
||||
currentRangeExponent;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "vtvpwr.h"
|
||||
#include "..\munga\icom.h"
|
||||
#include "..\munga\app.h"
|
||||
#include "..\munga\inputscript.h"
|
||||
#include "rpplayer.h"
|
||||
#include "vtv.h"
|
||||
|
||||
@@ -414,6 +415,36 @@ void
|
||||
VTVPower *power_system =
|
||||
Cast_Object(VTVPower*, vtv->GetSubsystem(VTV::PowerSubsystem));
|
||||
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// RP412INPUTSCRIPT: scripted driving, on this subsystem's own step
|
||||
// clock.
|
||||
//
|
||||
// This is the one place every mapper - RIO, Thrustmaster, pad -
|
||||
// funnels through, and it runs per SIMULATION STEP, so a scripted
|
||||
// value lands on the same step of every run whatever the frame
|
||||
// rate. Overriding at the RIO or the controls manager would key
|
||||
// the timeline to the frame loop, which is wall clock, which is
|
||||
// the thing the whole harness exists to keep out of the physics.
|
||||
//
|
||||
// Only the player's own vehicle: replicants get their state from
|
||||
// the network, and the mapper does not run for them anyway.
|
||||
//----------------------------------------------------------------
|
||||
//
|
||||
if (RPInputScript_Active())
|
||||
{
|
||||
float script_throttle, script_x, script_y, script_pedals;
|
||||
|
||||
if (RPInputScript_Sample(GetLastPerformance(),
|
||||
&script_throttle, &script_x, &script_y, &script_pedals))
|
||||
{
|
||||
throttlePosition = script_throttle;
|
||||
stickPosition.x = script_x;
|
||||
stickPosition.y = script_y;
|
||||
pedalsPosition = script_pedals;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//----------------------------------------------
|
||||
// Make sure the control inputs are within range
|
||||
|
||||
+78
-8
@@ -229,8 +229,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
" Expired %s\n\n"
|
||||
"Test builds are good for a fortnight so that nobody spends an "
|
||||
"afternoon chasing something that was fixed a week ago.\n\n"
|
||||
"Grab the current one:\n"
|
||||
"https://gitea.mysticmachines.com/VWE/RP412/releases",
|
||||
"Ask for the current one.",
|
||||
RP412_VERSION_LONG, RP412_EXPIRY_TEXT);
|
||||
MessageBoxA(NULL, notice, "Red Planet - test build expired",
|
||||
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
||||
@@ -343,12 +342,41 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
}
|
||||
|
||||
|
||||
DWORD wsStyle = WS_OVERLAPPED | WS_SYSMENU;
|
||||
if (L4Application::GetFullscreen())
|
||||
wsStyle = WS_POPUP;
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_OVERLAPPEDWINDOW, 0, 0, L4Application::GetScreenWidth(), L4Application::GetScreenHeight(), (HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
||||
if (!hWnd)
|
||||
return FALSE;
|
||||
//
|
||||
// The style the window is BORN with, rather than one worked out and
|
||||
// then thrown away - the old code computed a style and handed
|
||||
// CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless.
|
||||
//
|
||||
// Windowed keeps the full overlapped set on purpose: the sizing
|
||||
// border and the maximise box are how the player arranges the
|
||||
// cockpit, and mfd_layout.cfg remembers where they left it. The
|
||||
// borderless modes are born borderless instead of being restyled a
|
||||
// moment later, so there is no framed window on screen first.
|
||||
//
|
||||
DWORD wsStyle = WS_OVERLAPPEDWINDOW;
|
||||
if (L4Application::GetFullscreen() || L4Application::GetFitDisplay())
|
||||
{
|
||||
wsStyle = WS_POPUP;
|
||||
}
|
||||
|
||||
//
|
||||
// -res is a RENDER size, so it is the client area that has to be it.
|
||||
// Passed straight to CreateWindowEx it sets the OUTER rectangle and
|
||||
// the chrome comes out of the middle - which is how -res 640 480
|
||||
// came to present into a 624x441 client.
|
||||
//
|
||||
RECT wanted;
|
||||
wanted.left = 0;
|
||||
wanted.top = 0;
|
||||
wanted.right = (LONG) L4Application::GetScreenWidth();
|
||||
wanted.bottom = (LONG) L4Application::GetScreenHeight();
|
||||
AdjustWindowRect(&wanted, wsStyle, FALSE);
|
||||
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", wsStyle, 0, 0,
|
||||
wanted.right - wanted.left, wanted.bottom - wanted.top,
|
||||
(HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
||||
if (!hWnd)
|
||||
return FALSE;
|
||||
|
||||
ShowWindow(hWnd, nShowCmd);
|
||||
|
||||
@@ -365,6 +393,23 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
RPWindowLayout_Register(hWnd, "RPL4", True);
|
||||
RPWindowLayout_Load();
|
||||
}
|
||||
else if (L4Application::GetFitDisplay())
|
||||
{
|
||||
//
|
||||
// -fit has no saved placement to restore, but it does have a
|
||||
// shape to take, and it has to take it NOW. SVGA16 applies the
|
||||
// same borderless full-monitor rect when it assembles the
|
||||
// cockpit, which is after the first mission has already built
|
||||
// its D3D device against the window as it stands - so the first
|
||||
// race of a session used to render to a bordered client and
|
||||
// every race after it to the borderless monitor. Same lobby,
|
||||
// same settings, different target and different frame cost.
|
||||
//
|
||||
// Settle the window here and every mission of the session,
|
||||
// first included, is set up against the identical client area.
|
||||
//
|
||||
L4Application::FitWindowToMonitor(hWnd);
|
||||
}
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
@@ -414,6 +459,31 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
|
||||
int last_launch_mode = FELaunchSingle;
|
||||
|
||||
//
|
||||
//-------------------------------------------------------------------------
|
||||
// A hand-fed egg run stays unmarshaled - no console, so the race never
|
||||
// ends - UNLESS it is given a length. RP412MISSIONSECONDS supplies one,
|
||||
// and with it '-egg' can play a whole race through to the buzzer, the
|
||||
// podium and the results.
|
||||
//
|
||||
// That is the difference between a shortcut that can only be watched and
|
||||
// one that can be TESTED: everything after the chequered flag - the fade,
|
||||
// the winners' circle, the teardown - was unreachable from the command
|
||||
// line, so it could only ever be exercised by hand through the menu.
|
||||
//-------------------------------------------------------------------------
|
||||
//
|
||||
if (!front_end_mode && L4Application::GetEggNotationFileName())
|
||||
{
|
||||
const char *egg_seconds = getenv("RP412MISSIONSECONDS");
|
||||
if (egg_seconds != NULL && atoi(egg_seconds) > 0)
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: marshalling the hand-fed egg run ("
|
||||
<< atoi(egg_seconds) << "s, RP412MISSIONSECONDS)\n" << std::flush;
|
||||
L4Application::SetNetworkCommonFlatAddress(0);
|
||||
RPL4LocalConsole_Install(atoi(egg_seconds));
|
||||
}
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (front_end_mode)
|
||||
|
||||
@@ -244,6 +244,14 @@ void
|
||||
// you would be the one empty spot. Turn it inside out before the shot.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Timed, because the black screen between the race fading out and the
|
||||
// stand fading in is several seconds long and the fade only accounts
|
||||
// for 0.7 of them. Nothing else runs while this does - the whole game
|
||||
// is one thread - so whatever these two cost IS that gap.
|
||||
//
|
||||
Time podiumOutsideStart = Now();
|
||||
|
||||
dpl_renderer->ShowViewpointFromOutside();
|
||||
|
||||
//
|
||||
@@ -253,8 +261,21 @@ void
|
||||
// whatever order the players were created.
|
||||
//---------------------------------------------------------------------
|
||||
//
|
||||
Time podiumNamesStart = Now();
|
||||
|
||||
dpl_renderer->SortAndReloadNameBitmaps();
|
||||
|
||||
{
|
||||
Time podiumNamesEnd = Now();
|
||||
char timing[160];
|
||||
|
||||
sprintf(timing,
|
||||
"WinnersCircle: exterior %.0f ms, name plates %.0f ms\n",
|
||||
(double)((Scalar)(podiumNamesStart - podiumOutsideStart) * 1000.0f),
|
||||
(double)((Scalar)(podiumNamesEnd - podiumNamesStart) * 1000.0f));
|
||||
DEBUG_STREAM << timing << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------
|
||||
// Widen to 45 degrees and pull back in front of the stand so the whole
|
||||
|
||||
@@ -50,6 +50,20 @@ namespace
|
||||
"# 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"
|
||||
@@ -176,6 +190,12 @@ namespace
|
||||
"#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"
|
||||
@@ -185,6 +205,58 @@ namespace
|
||||
"# 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"
|
||||
"# 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 the Steam client\n"
|
||||
"# running and steam_appid.txt beside the exe; without them the game\n"
|
||||
"# logs the reason and falls back to plain TCP. 0 = TCP only.\n"
|
||||
@@ -198,6 +270,52 @@ namespace
|
||||
"# the old arrival-time behaviour if you want to compare.\n"
|
||||
"#RP412NETCLOCK=0\n"
|
||||
"\n"
|
||||
"# ---- Test harness -----------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# The knobs that make a run repeatable and measurable. All are off\n"
|
||||
"# unless set and cost nothing when off; none belongs in a real race.\n"
|
||||
"# They exist so a claim about the game can be tested instead of argued.\n"
|
||||
"\n"
|
||||
"# Dump the gauge profile to rpl4.log every N seconds: every cockpit\n"
|
||||
"# display with its rate mask and tier, how often it ran and what it\n"
|
||||
"# cost. This is the engine's own ProfileReport, which was only ever\n"
|
||||
"# reachable from the arcade RIO mapper's F11 before.\n"
|
||||
"#RP412GAUGEPROFILE=8\n"
|
||||
"\n"
|
||||
"# 1 = log renderable construction and what each frame is made of, so a\n"
|
||||
"# model that never got built can be told from one that is simply out\n"
|
||||
"# of shot.\n"
|
||||
"#RP412RENDERDIAG=1\n"
|
||||
"\n"
|
||||
"# 1 = trace the player pod's position to rpl4.log on the SIMULATION's\n"
|
||||
"# own clock, stopping the pod dead at the green light so every run\n"
|
||||
"# starts from rest. Two runs of the same race then compare sample for\n"
|
||||
"# sample - this is the instrument that proved RP412PHYSICSHZ plays the\n"
|
||||
"# same race at every frame rate, bit for bit.\n"
|
||||
"#RP412PHYSTRACE=1\n"
|
||||
"\n"
|
||||
"# Try this drop zone first at spawn instead of a random pick. The pick\n"
|
||||
"# is seeded by RANDOM=, but a seed only repeats a run if the same\n"
|
||||
"# NUMBER of draws comes before the pick, and that count rides on load\n"
|
||||
"# timing - so pin the pad too, or two 'identical' runs start over\n"
|
||||
"# different ground. Falls back to the random walk if the zone is\n"
|
||||
"# taken, so it cannot wedge.\n"
|
||||
"#RP412SPAWNZONE=3\n"
|
||||
"\n"
|
||||
"# Drive the pod from a timeline file instead of the controls - the\n"
|
||||
"# same lap, exactly, every run. One row per change, held until the\n"
|
||||
"# next row: time-in-seconds throttle stickX stickY pedals, values\n"
|
||||
"# 0..1 for throttle and -1..1 elsewhere, # for comments. Times are\n"
|
||||
"# SIMULATION seconds from the green light, so with RP412PHYSICSHZ set\n"
|
||||
"# the same script is the same race at any frame rate - this is how\n"
|
||||
"# driving, not just settling, gets verified bit-identical.\n"
|
||||
"#RP412INPUTSCRIPT=testlap.txt\n"
|
||||
"\n"
|
||||
"# 1 = log the XInput-class controllers the generic-joystick scan skips\n"
|
||||
"# (attached DirectInput devices are always logged). For debugging a pad\n"
|
||||
"# that answers twice or a stick that does not answer at all.\n"
|
||||
"#RP412JOYLOG=1\n"
|
||||
"\n"
|
||||
"# ---- Optional ---------------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to\n"
|
||||
@@ -205,10 +323,57 @@ namespace
|
||||
"# 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"
|
||||
|
||||
+367
-76
@@ -33,6 +33,17 @@ namespace
|
||||
{ "brewers", "Brewer's Bane" },
|
||||
{ "pain", "Paingod's Passage" },
|
||||
{ "headoff", "Freezemoon's Freeway" },
|
||||
|
||||
// Maps that came with the promoted resource file. The console
|
||||
// config brackets their names in dashes, which is how it marks
|
||||
// the ones that are not original arcade tracks.
|
||||
{ "arena", "-- Arena (Small) --" },
|
||||
{ "demoderb", "-- Arena (Large) --" },
|
||||
{ "donut", "-- Donut --" },
|
||||
{ "tourdemars", "-- Tour De Mars --" },
|
||||
{ "trough", "-- Ares' Armpits --" },
|
||||
{ "wiserguys", "-- Wiserguy's --" },
|
||||
{ "rpweekend2014", "-- Zaxxis --" },
|
||||
};
|
||||
|
||||
// Football excludes pain/headoff and adds the football build of
|
||||
@@ -110,6 +121,23 @@ namespace
|
||||
{ "spitter", "Spitter" },
|
||||
{ "puck", "Armadillo" },
|
||||
{ "dragon", "Dragon" },
|
||||
|
||||
// Restored with the promoted resource file: cut from the .bld
|
||||
// after 4.10 shipped, back now that RPL4.RES carries them again.
|
||||
// Names from the console's own RPConfig.xml.
|
||||
{ "dark", "Blacker Puck" },
|
||||
{ "blktrn", "Black Tarantula" },
|
||||
{ "neut", "Neutrino" },
|
||||
|
||||
// Community variants that came with the same archive.
|
||||
{ "blkrpuck", "Blacker Armadillo" },
|
||||
{ "blkrbroc", "Blacker Broccoli" },
|
||||
{ "blkrdragon", "Blacker Dragon" },
|
||||
{ "blkrlepton", "Blacker Lepton" },
|
||||
{ "blkrquark", "Blacker Quark" },
|
||||
{ "blkrspk", "Blacker Speck" },
|
||||
{ "blkrtran", "Blacker Tarantula" },
|
||||
{ "blkrwasp", "Blacker Wasp" },
|
||||
};
|
||||
|
||||
const CatalogEntry kColors[] =
|
||||
@@ -214,6 +242,13 @@ namespace
|
||||
|
||||
HWND menuWindow;
|
||||
HWND nameEdit;
|
||||
|
||||
// Every list group is a drop-down. They are owner-drawn so they
|
||||
// keep the green-on-black panel look instead of arriving in
|
||||
// system colours, and their labels are painted by PaintMenu.
|
||||
HWND combo[GroupCount];
|
||||
RECT comboLabel[GroupCount];
|
||||
|
||||
HFONT textFont;
|
||||
HFONT titleFont;
|
||||
HBRUSH editBrush;
|
||||
@@ -223,6 +258,9 @@ namespace
|
||||
};
|
||||
|
||||
FEState *gFE = NULL;
|
||||
|
||||
// ItemName() reads this to answer with the right track list.
|
||||
const int *gItemNameSelection = NULL;
|
||||
int gLastMissionSeconds = 0;
|
||||
|
||||
// carried across races so cycling back to the menu keeps the
|
||||
@@ -584,69 +622,207 @@ namespace
|
||||
//---------------------------------------------------------------
|
||||
// Layout: three columns of lists + the launch button
|
||||
//---------------------------------------------------------------
|
||||
void AddGroupItems(
|
||||
FEState *fe, int group, int count,
|
||||
int x, int *y, int row_h, int width)
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The groups that became drop-downs, in the order they are laid
|
||||
// out. Scenario is deliberately NOT one of them: it decides what
|
||||
// the others contain, so it stays visible as a pair of buttons.
|
||||
//---------------------------------------------------------------
|
||||
int ComboGroups(const int *selection, int *groups)
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
int n = 0;
|
||||
groups[n++] = GroupMap;
|
||||
groups[n++] = GroupTime;
|
||||
groups[n++] = GroupWeather;
|
||||
groups[n++] = GroupLength;
|
||||
groups[n++] = GroupVehicle;
|
||||
if (IsFootball(selection))
|
||||
{
|
||||
FEItem *item = &fe->items[fe->itemCount++];
|
||||
item->group = group;
|
||||
item->index = i;
|
||||
item->rect.left = x;
|
||||
item->rect.top = *y;
|
||||
item->rect.right = x + width;
|
||||
item->rect.bottom = *y + row_h;
|
||||
*y += row_h;
|
||||
groups[n++] = GroupTeam;
|
||||
groups[n++] = GroupPosition;
|
||||
}
|
||||
*y += row_h; // gap below the group
|
||||
else
|
||||
{
|
||||
groups[n++] = GroupColor;
|
||||
groups[n++] = GroupBadge;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// The combo id is its group, so CBN_SELCHANGE says which list moved.
|
||||
const int kComboIdBase = 100;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// CBS_OWNERDRAWFIXED only hands us the item area: the frame around
|
||||
// the closed box and its drop arrow are still drawn by the system,
|
||||
// which lands a white/grey Windows control in the middle of a black
|
||||
// panel. So the closed box is painted here instead - black field,
|
||||
// dim green border, bright green text and arrow - and the control
|
||||
// keeps doing everything else, including the list it drops.
|
||||
//---------------------------------------------------------------
|
||||
WNDPROC gComboProc = NULL;
|
||||
|
||||
LRESULT CALLBACK ComboSubclassProc(
|
||||
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (message == WM_ERASEBKGND)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (message == WM_PAINT)
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC dc = BeginPaint(hwnd, &ps);
|
||||
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
|
||||
HBRUSH field = CreateSolidBrush(kBlack);
|
||||
FillRect(dc, &rc, field);
|
||||
DeleteObject(field);
|
||||
|
||||
HBRUSH edge = CreateSolidBrush(kGreenDim);
|
||||
FrameRect(dc, &rc, edge);
|
||||
DeleteObject(edge);
|
||||
|
||||
int arrow_w = rc.bottom - rc.top;
|
||||
|
||||
char text[128];
|
||||
text[0] = '\0';
|
||||
int sel = (int) SendMessageA(hwnd, CB_GETCURSEL, 0, 0);
|
||||
if (sel >= 0)
|
||||
{
|
||||
SendMessageA(hwnd, CB_GETLBTEXT, sel, (LPARAM) text);
|
||||
}
|
||||
|
||||
HFONT font = (HFONT) SendMessageA(hwnd, WM_GETFONT, 0, 0);
|
||||
HGDIOBJ old_font = (font != NULL) ? SelectObject(dc, font) : NULL;
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, kGreenBright);
|
||||
|
||||
RECT label = rc;
|
||||
label.left += 6;
|
||||
label.right -= arrow_w;
|
||||
DrawTextA(dc, text, -1, &label,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
|
||||
// the arrow, a plain triangle rather than a themed button
|
||||
int cx = rc.right - arrow_w / 2;
|
||||
int cy = (rc.top + rc.bottom) / 2;
|
||||
int r = arrow_w / 6;
|
||||
POINT tri[3];
|
||||
tri[0].x = cx - r; tri[0].y = cy - r / 2;
|
||||
tri[1].x = cx + r; tri[1].y = cy - r / 2;
|
||||
tri[2].x = cx; tri[2].y = cy + r;
|
||||
HBRUSH tip = CreateSolidBrush(kGreenBright);
|
||||
HGDIOBJ old_brush = SelectObject(dc, tip);
|
||||
HGDIOBJ old_pen = SelectObject(dc, GetStockObject(NULL_PEN));
|
||||
Polygon(dc, tri, 3);
|
||||
SelectObject(dc, old_pen);
|
||||
SelectObject(dc, old_brush);
|
||||
DeleteObject(tip);
|
||||
|
||||
if (old_font != NULL) SelectObject(dc, old_font);
|
||||
EndPaint(hwnd, &ps);
|
||||
return 0;
|
||||
}
|
||||
return CallWindowProcA(gComboProc, hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void DestroyCombos(FEState *fe)
|
||||
{
|
||||
for (int g = 0; g < GroupCount; ++g)
|
||||
{
|
||||
if (fe->combo[g] != NULL)
|
||||
{
|
||||
DestroyWindow(fe->combo[g]);
|
||||
fe->combo[g] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// defined below, next to the catalogs it reads
|
||||
const char *ItemName(int group, int index);
|
||||
|
||||
void LayoutMenu(FEState *fe, int client_w, int client_h)
|
||||
{
|
||||
fe->itemCount = 0;
|
||||
gItemNameSelection = fe->selection;
|
||||
|
||||
int row_h = client_h / 36;
|
||||
if (row_h < 18) row_h = 18;
|
||||
if (row_h > 30) row_h = 30;
|
||||
|
||||
int col_w = client_w / 5;
|
||||
int col1 = client_w / 14;
|
||||
int col2 = col1 + col_w + client_w / 28;
|
||||
int col3 = col2 + col_w + client_w / 28;
|
||||
int top = client_h / 7;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Every list is a drop-down, so the menu no longer grows with the
|
||||
// content: eight controls instead of ninety-odd rows. Two columns
|
||||
// of label-over-box, settings on the left and loadout on the right,
|
||||
// with the scenario left as visible buttons because it decides what
|
||||
// the other lists contain.
|
||||
//---------------------------------------------------------------
|
||||
int margin = client_w / 14;
|
||||
int gap = client_w / 20;
|
||||
int col_w = (client_w - 2 * margin - gap) / 2;
|
||||
if (col_w < 1) col_w = 1;
|
||||
int col2 = margin + col_w + gap;
|
||||
|
||||
int block_h = row_h * 5 / 2; // label, box, breathing room
|
||||
|
||||
// scenario: buttons, top of the left column
|
||||
int y = top;
|
||||
AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
|
||||
int map_count;
|
||||
ActiveMaps(fe->selection, &map_count);
|
||||
AddGroupItems(fe, GroupMap, map_count, col1, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), col1, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);
|
||||
|
||||
y = top;
|
||||
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
|
||||
|
||||
y = top + 2 * row_h; // leave room for the name edit + header
|
||||
if (IsFootball(fe->selection))
|
||||
fe->comboLabel[GroupScenario].left = margin;
|
||||
fe->comboLabel[GroupScenario].right = margin + col_w;
|
||||
fe->comboLabel[GroupScenario].top = y;
|
||||
fe->comboLabel[GroupScenario].bottom = y + row_h;
|
||||
y += row_h;
|
||||
for (int i = 0; i < FE_COUNT(kScenarios); ++i)
|
||||
{
|
||||
AddGroupItems(fe, GroupTeam, FE_COUNT(kTeams), col3, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupPosition, FE_COUNT(kPositions), col3, &y, row_h, col_w);
|
||||
FEItem *item = &fe->items[fe->itemCount++];
|
||||
item->group = GroupScenario;
|
||||
item->index = i;
|
||||
item->rect.left = margin + i * (col_w / 2);
|
||||
item->rect.top = y;
|
||||
item->rect.right = item->rect.left + col_w / 2 - row_h / 3;
|
||||
item->rect.bottom = y + row_h;
|
||||
}
|
||||
else
|
||||
y += row_h + row_h / 2;
|
||||
|
||||
int groups[GroupCount];
|
||||
int group_count = ComboGroups(fe->selection, groups);
|
||||
|
||||
// left column takes the mission settings, right the loadout; the
|
||||
// split is where the vehicle list starts. The pilot name heads
|
||||
// the loadout column - it is who you are, so it reads before
|
||||
// what you are driving.
|
||||
int left_y = y;
|
||||
int right_y = top;
|
||||
int name_y = right_y;
|
||||
right_y += block_h;
|
||||
for (int g = 0; g < group_count; ++g)
|
||||
{
|
||||
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
|
||||
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
|
||||
int group = groups[g];
|
||||
Logical left = (group == GroupMap || group == GroupTime ||
|
||||
group == GroupWeather || group == GroupLength);
|
||||
int x = left ? margin : col2;
|
||||
int *slot = left ? &left_y : &right_y;
|
||||
|
||||
fe->comboLabel[group].left = x;
|
||||
fe->comboLabel[group].right = x + col_w;
|
||||
fe->comboLabel[group].top = *slot;
|
||||
fe->comboLabel[group].bottom = *slot + row_h;
|
||||
*slot += block_h;
|
||||
}
|
||||
|
||||
|
||||
// launch button
|
||||
FEItem *launch = &fe->items[fe->itemCount++];
|
||||
launch->group = GroupLaunch;
|
||||
launch->index = 0;
|
||||
launch->rect.left = col3;
|
||||
launch->rect.left = col2;
|
||||
launch->rect.top = client_h - 3 * row_h;
|
||||
launch->rect.right = col3 + col_w;
|
||||
launch->rect.right = col2 + col_w;
|
||||
launch->rect.bottom = client_h - row_h;
|
||||
|
||||
// Steam lobby buttons, offered whenever environ.ini asked for Steam.
|
||||
@@ -656,17 +832,17 @@ namespace
|
||||
FEItem *host = &fe->items[fe->itemCount++];
|
||||
host->group = GroupSteamHost;
|
||||
host->index = 0;
|
||||
host->rect.left = col3;
|
||||
host->rect.left = col2;
|
||||
host->rect.top = client_h - 6 * row_h;
|
||||
host->rect.right = col3 + col_w;
|
||||
host->rect.right = col2 + col_w;
|
||||
host->rect.bottom = client_h - 5 * row_h;
|
||||
|
||||
FEItem *join = &fe->items[fe->itemCount++];
|
||||
join->group = GroupSteamJoin;
|
||||
join->index = 0;
|
||||
join->rect.left = col3;
|
||||
join->rect.left = col2;
|
||||
join->rect.top = client_h - (9 * row_h) / 2;
|
||||
join->rect.right = col3 + col_w;
|
||||
join->rect.right = col2 + col_w;
|
||||
join->rect.bottom = client_h - (7 * row_h) / 2;
|
||||
}
|
||||
|
||||
@@ -682,16 +858,67 @@ namespace
|
||||
FEItem *quit = &fe->items[fe->itemCount++];
|
||||
quit->group = GroupExit;
|
||||
quit->index = 0;
|
||||
quit->rect.left = col1;
|
||||
quit->rect.left = margin;
|
||||
quit->rect.top = client_h - 2 * row_h;
|
||||
quit->rect.right = col1 + col_w / 2;
|
||||
quit->rect.right = margin + col_w / 2;
|
||||
quit->rect.bottom = client_h - row_h;
|
||||
|
||||
// pilot name edit sits at the top of column 3
|
||||
// pilot name, under the loadout column
|
||||
fe->comboLabel[GroupLaunch].left = col2; // reused: name label
|
||||
fe->comboLabel[GroupLaunch].right = col2 + col_w;
|
||||
fe->comboLabel[GroupLaunch].top = name_y;
|
||||
fe->comboLabel[GroupLaunch].bottom = name_y + row_h;
|
||||
if (fe->nameEdit != NULL)
|
||||
{
|
||||
MoveWindow(fe->nameEdit,
|
||||
col3, top - row_h / 4, col_w, row_h, TRUE);
|
||||
MoveWindow(fe->nameEdit, col2, name_y + row_h, col_w, row_h, TRUE);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The drop-downs themselves. Rebuilt rather than moved, because
|
||||
// the scenario swaps two of them outright (team/position for
|
||||
// colour/badge) and reshuffles the track list.
|
||||
//---------------------------------------------------------------
|
||||
DestroyCombos(fe);
|
||||
for (int g = 0; g < group_count; ++g)
|
||||
{
|
||||
int group = groups[g];
|
||||
const RECT &label = fe->comboLabel[group];
|
||||
int count = GroupSize(group, fe->selection);
|
||||
|
||||
// The height given at creation is the DROPPED height - what the
|
||||
// closed box shows is the item height - so ask for enough to
|
||||
// show a dozen rows without a scrollbar where the list is short.
|
||||
int drop_rows = (count < 12) ? count : 12;
|
||||
HWND box = CreateWindowExA(
|
||||
0, "COMBOBOX", "",
|
||||
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
|
||||
CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS,
|
||||
label.left, label.top + row_h,
|
||||
label.right - label.left, row_h + drop_rows * row_h,
|
||||
fe->menuWindow, (HMENU)(INT_PTR)(kComboIdBase + group),
|
||||
(HINSTANCE) GetWindowLongPtr(fe->menuWindow, GWLP_HINSTANCE), NULL);
|
||||
if (box == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SendMessageA(box, WM_SETFONT, (WPARAM) fe->textFont, TRUE);
|
||||
SendMessageA(box, CB_SETITEMHEIGHT, (WPARAM) -1, row_h);
|
||||
SendMessageA(box, CB_SETITEMHEIGHT, 0, row_h);
|
||||
|
||||
// paint the closed box ourselves - see ComboSubclassProc
|
||||
WNDPROC previous = (WNDPROC) SetWindowLongPtrA(
|
||||
box, GWLP_WNDPROC, (LONG_PTR) ComboSubclassProc);
|
||||
if (gComboProc == NULL)
|
||||
{
|
||||
gComboProc = previous;
|
||||
}
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
SendMessageA(box, CB_ADDSTRING, 0, (LPARAM) ItemName(group, i));
|
||||
}
|
||||
if (fe->selection[group] >= count) fe->selection[group] = 0;
|
||||
SendMessageA(box, CB_SETCURSEL, fe->selection[group], 0);
|
||||
fe->combo[group] = box;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +941,6 @@ namespace
|
||||
}
|
||||
|
||||
// the map rows need the active scenario's list
|
||||
const int *gItemNameSelection = NULL;
|
||||
|
||||
const char *ItemName(int group, int index)
|
||||
{
|
||||
@@ -776,21 +1002,31 @@ namespace
|
||||
|
||||
SelectObject(mem, fe->textFont);
|
||||
|
||||
// group headers (drawn above each group's first item)
|
||||
int previous_group = -1;
|
||||
// Labels: one above each drop-down, one above the scenario
|
||||
// buttons, one above the pilot name box. The drop-downs draw
|
||||
// themselves (WM_DRAWITEM), so all that is left here is their
|
||||
// captions.
|
||||
SetTextColor(mem, kGreenBright);
|
||||
for (int g = 0; g < GroupCount; ++g)
|
||||
{
|
||||
Logical labelled = (g == GroupScenario) || (fe->combo[g] != NULL);
|
||||
if (!labelled && g != GroupLaunch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
RECT label = fe->comboLabel[g];
|
||||
if (label.right <= label.left)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const char *caption = (g == GroupLaunch) ? "PILOT NAME" : GroupTitle(g);
|
||||
DrawTextA(mem, caption, -1, &label,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
for (int i = 0; i < fe->itemCount; ++i)
|
||||
{
|
||||
const FEItem *item = &fe->items[i];
|
||||
if (item->group != previous_group && item->group < GroupLaunch)
|
||||
{
|
||||
previous_group = item->group;
|
||||
RECT header = item->rect;
|
||||
header.top -= (item->rect.bottom - item->rect.top);
|
||||
header.bottom = item->rect.top;
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, GroupTitle(item->group), -1, &header,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
Logical selected =
|
||||
(item->group >= GroupLaunch) ||
|
||||
@@ -848,26 +1084,14 @@ namespace
|
||||
}
|
||||
RECT text = row;
|
||||
text.left += 6;
|
||||
// On a narrow window the longest names (Screaming Broccoli,
|
||||
// Blacker Tarantula) outrun their column. Ellipsis rather than
|
||||
// a glyph sliced down the middle.
|
||||
DrawTextA(mem, ItemName(item->group, item->index), -1, &text,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
}
|
||||
|
||||
// pilot-name header
|
||||
if (fe->nameEdit != NULL)
|
||||
{
|
||||
RECT edit_rect;
|
||||
GetWindowRect(fe->nameEdit, &edit_rect);
|
||||
POINT corner = { edit_rect.left, edit_rect.top };
|
||||
ScreenToClient(fe->menuWindow, &corner);
|
||||
RECT header;
|
||||
header.left = corner.x;
|
||||
header.right = corner.x + 300;
|
||||
header.bottom = corner.y;
|
||||
header.top = corner.y - 26;
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, "PILOT NAME", -1, &header,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
// (the pilot-name caption is drawn with the other labels above)
|
||||
|
||||
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
||||
|
||||
@@ -954,6 +1178,65 @@ namespace
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_COMMAND:
|
||||
if (fe != NULL && HIWORD(wParam) == CBN_SELCHANGE)
|
||||
{
|
||||
int group = LOWORD(wParam) - kComboIdBase;
|
||||
if (group >= 0 && group < GroupCount && fe->combo[group] != NULL)
|
||||
{
|
||||
int pick = (int) SendMessageA(fe->combo[group], CB_GETCURSEL, 0, 0);
|
||||
if (pick >= 0)
|
||||
{
|
||||
fe->selection[group] = pick;
|
||||
}
|
||||
// the closed box is ours to redraw now
|
||||
InvalidateRect(fe->combo[group], NULL, FALSE);
|
||||
InvalidateRect(fe->menuWindow, NULL, FALSE);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
//
|
||||
// The drop-downs are owner-drawn so they read as part of the
|
||||
// panel rather than as system widgets: green on black, and the
|
||||
// highlight inverted rather than tinted.
|
||||
//
|
||||
case WM_DRAWITEM:
|
||||
if (fe != NULL)
|
||||
{
|
||||
DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT *) lParam;
|
||||
if (di->CtlType == ODT_COMBOBOX && (int) di->itemID >= 0)
|
||||
{
|
||||
Logical hot =
|
||||
((di->itemState & (ODS_SELECTED | ODS_COMBOBOXEDIT)) == ODS_SELECTED);
|
||||
HBRUSH back = CreateSolidBrush(hot ? kGreenDim : kBlack);
|
||||
FillRect(di->hDC, &di->rcItem, back);
|
||||
DeleteObject(back);
|
||||
|
||||
char text[128];
|
||||
text[0] = '\0';
|
||||
SendMessageA(di->hwndItem, CB_GETLBTEXT, di->itemID, (LPARAM) text);
|
||||
RECT label = di->rcItem;
|
||||
label.left += 6;
|
||||
SetBkMode(di->hDC, TRANSPARENT);
|
||||
SetTextColor(di->hDC, hot ? kBlack : kGreenBright);
|
||||
DrawTextA(di->hDC, text, -1, &label,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CTLCOLORLISTBOX:
|
||||
if (fe != NULL)
|
||||
{
|
||||
SetTextColor((HDC) wParam, kGreenBright);
|
||||
SetBkColor((HDC) wParam, kBlack);
|
||||
return (LRESULT) fe->editBrush;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CTLCOLOREDIT:
|
||||
if (fe != NULL)
|
||||
{
|
||||
@@ -1483,6 +1766,14 @@ Logical
|
||||
// LobbyRoomLeft: fall through to the setup menu
|
||||
}
|
||||
|
||||
// NOTE: the vehicle catalog below is hand-written while RPL4.RES is
|
||||
// built elsewhere, so the two can drift - this menu offered 'blkspk'
|
||||
// for a while before any vehicle resource backed it. Validating here
|
||||
// does NOT work: the front end runs before the resource file is
|
||||
// opened, so GetResourceFile() has nothing to search yet. If this is
|
||||
// worth guarding, do it offline against the built RPL4.RES (see
|
||||
// tools/resbuild) rather than at menu time.
|
||||
|
||||
for (;;)
|
||||
{
|
||||
FEState fe;
|
||||
|
||||
+241
-45
@@ -1,4 +1,5 @@
|
||||
#include "rpl4.h"
|
||||
|
||||
#pragma hdrstop
|
||||
|
||||
#define PRELOAD_ART
|
||||
@@ -1951,7 +1952,75 @@ GPS::GPS(
|
||||
//-----------------------------------------------------------
|
||||
background = new Video8BitBuffered(width, height);
|
||||
Register_Object(background);
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------
|
||||
// RP412MAPRATE - how many of the sixteen rate steps the map redraws
|
||||
// on.
|
||||
//
|
||||
// The gauge renderer walks a 16-bit rate wheel: one bit per full
|
||||
// pass over every active gauge, shifted right each pass and reset at
|
||||
// the bottom (GAUGREND.cpp). A gauge redraws only on the step its
|
||||
// configured rate names, so a map on one bit redraws once per
|
||||
// SIXTEEN passes - and its update period is sixteen passes however
|
||||
// cheap the redraw is.
|
||||
//
|
||||
// That was fine when a pass was quick. Racing, the background loop
|
||||
// only gets what is left of the frame after the 3D, passes fall to
|
||||
// about five a second, and sixteen of them is over three seconds
|
||||
// between map updates - measured, on a 60 fps display with the 3D
|
||||
// perfectly smooth. Nothing is slow here; the map is just waiting
|
||||
// its turn on a wheel built for a machine that came round faster.
|
||||
//
|
||||
// Drawing on more of the steps costs one gauge's redraw per step,
|
||||
// against a pass that runs ninety of them. It does not make the
|
||||
// wheel turn faster - it stops the map needing a whole turn.
|
||||
//-----------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int updates = -1;
|
||||
if (updates < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412MAPRATE");
|
||||
updates = (setting != NULL) ? atoi(setting) : 16;
|
||||
// 1..16, and only the powers of two divide the wheel evenly
|
||||
if (updates > 16) updates = 16;
|
||||
if (updates < 1) updates = 1;
|
||||
}
|
||||
if (updates > 1)
|
||||
{
|
||||
GaugeRate mask = 0;
|
||||
for (int step = 0; step < 16; step += (16 / updates))
|
||||
{
|
||||
mask |= (GaugeRate)(0x8000 >> step);
|
||||
}
|
||||
//
|
||||
// QUALIFIED, both of them. This constructor's first
|
||||
// parameter is also called 'rate', so a bare assignment
|
||||
// here writes the PARAMETER and leaves the member holding
|
||||
// whatever the gauge data asked for - which is what it did,
|
||||
// silently, and cost a long hunt for a writer that did not
|
||||
// exist. oldRate is not shadowed, which is why it took the
|
||||
// value and the pair disagreed.
|
||||
//
|
||||
// Gauge::Disable(False) restores rate from oldRate when the
|
||||
// mode system enables a gauge, so both have to carry it or
|
||||
// the first activation puts the old rate back.
|
||||
//
|
||||
Gauge::rate = mask;
|
||||
Gauge::oldRate = mask;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
needsStaticUpdate = True;
|
||||
//
|
||||
// Nothing drawn yet, so there is no picture to add a placement to and
|
||||
// no scale it would be added at. The first pass builds both.
|
||||
//
|
||||
backgroundBuilt = False;
|
||||
builtMinX = builtMinY = builtMinZ = (Scalar) 0;
|
||||
builtMaxX = builtMaxY = builtMaxZ = (Scalar) 0;
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1989,6 +2058,95 @@ void
|
||||
|
||||
void
|
||||
GPS::NotifyOfNewInterestingEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
if (!entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
|
||||
{
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// A rebuild is already owed, or there is no picture to add to yet.
|
||||
//
|
||||
if (needsStaticUpdate || !backgroundBuilt)
|
||||
{
|
||||
needsStaticUpdate = True;
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Terrain arrives as you DRIVE - the interest system hands each piece
|
||||
// over as it comes into range, not all of it at load - and this used
|
||||
// to order a rebuild of the entire map for every one of them. A
|
||||
// rebuild redraws every placement on the track, so the cost of one
|
||||
// arriving piece was the whole track, and the map went seconds
|
||||
// between updates on the tracks that draw the most: Tour De Mars at
|
||||
// 351 placements and Ares' Armpits at 320, against 80-140 for a
|
||||
// typical one. It is also not interruptible - the gauge loop checks
|
||||
// its time slice BETWEEN gauges - so a rebuild stalled every other
|
||||
// display with it.
|
||||
//
|
||||
// But the bounds are what set the scale, and the scale is what the
|
||||
// whole cached picture was drawn at. An arrival that leaves the
|
||||
// bounds alone leaves every placement already on the map exactly
|
||||
// where it belongs, and the only thing missing from the picture is
|
||||
// the new one. Draw that, and nothing else.
|
||||
//
|
||||
// Only a piece that moves an EDGE of the track changes the scale, and
|
||||
// then the picture really is wrong everywhere and has to be redrawn.
|
||||
// That is rare, and it gets rarer as the track fills in.
|
||||
//
|
||||
// RPL4GaugeRenderer::NotifyOfNewInterestingEntity adds the entity to
|
||||
// staticEntities before chaining here, so these bounds already
|
||||
// account for the arrival being judged.
|
||||
//
|
||||
Scalar
|
||||
minX, minY, minZ,
|
||||
maxX, maxY, maxZ;
|
||||
|
||||
Check(renderer);
|
||||
renderer->GetStaticBounds(
|
||||
&minX, &minY, &minZ,
|
||||
&maxX, &maxY, &maxZ
|
||||
);
|
||||
|
||||
if (minX != builtMinX || minY != builtMinY || minZ != builtMinZ ||
|
||||
maxX != builtMaxX || maxY != builtMaxY || maxZ != builtMaxZ)
|
||||
{
|
||||
needsStaticUpdate = True;
|
||||
}
|
||||
else if (DrawStaticEntity(entity))
|
||||
{
|
||||
//
|
||||
// Only when something was actually drawn. Terrain without a
|
||||
// GaugeImage never reaches the map - on these tracks that is
|
||||
// most of it, 256 of Tour De Mars' 607 - and blitting the
|
||||
// background again for one of those is pure cost.
|
||||
//
|
||||
needsScreenUpdate = True;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// NotifyOfBecomingUninterestingEntity
|
||||
//#############################################################################
|
||||
//
|
||||
// The map showed only currently-interesting terrain before any of this,
|
||||
// and it still does. A placement cannot be un-drawn from a composited
|
||||
// picture, so a departure is the one case left that costs a full rebuild.
|
||||
//
|
||||
// Nothing used to listen for this at all: departures were swept up by the
|
||||
// rebuild that the very next ARRIVAL ordered, which is no longer ordered.
|
||||
// Without this the map would keep showing terrain that had gone until
|
||||
// something moved the bounds.
|
||||
//
|
||||
void
|
||||
GPS::NotifyOfBecomingUninterestingEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
@@ -2104,61 +2262,34 @@ void
|
||||
ChainIteratorOf<Entity*>
|
||||
i(staticEntityList.GetInstanceList());
|
||||
|
||||
Check(renderer);
|
||||
L4Warehouse
|
||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
||||
Check(warehouse);
|
||||
|
||||
Entity
|
||||
*entity;
|
||||
L4GaugeImage
|
||||
*gauge_image;
|
||||
|
||||
AffineMatrix
|
||||
worldToView,
|
||||
localToView;
|
||||
|
||||
Vector3D
|
||||
scaling_vector;
|
||||
//------------------------------------
|
||||
// Scale the display
|
||||
//------------------------------------
|
||||
|
||||
Verify(!Small_Enough(pixelsPerMeter));
|
||||
|
||||
scaling_vector.x = pixelsPerMeter;
|
||||
scaling_vector.y = pixelsPerMeter;
|
||||
scaling_vector.z = pixelsPerMeter;
|
||||
|
||||
worldToView.BuildIdentity();
|
||||
worldToView *= centeringOffset; // translation
|
||||
worldToView *= scaling_vector;
|
||||
int
|
||||
drawn = 0;
|
||||
|
||||
while ((entity=i.ReadAndNext()) != NULL)
|
||||
{
|
||||
Check(entity);
|
||||
//-------------------------------------
|
||||
// Draw image
|
||||
//-------------------------------------
|
||||
gauge_image = warehouse->
|
||||
gaugeImageBin.GetIfAlreadyExists(entity->GetResourceID());
|
||||
if (gauge_image != NULL)
|
||||
{
|
||||
Check(gauge_image);
|
||||
|
||||
localToView.Multiply(entity->localToWorld, worldToView);
|
||||
|
||||
gauge_image->Draw(
|
||||
LODIndex, // value set by creator
|
||||
metersPerPixel,
|
||||
&backgroundView,
|
||||
0, // default color
|
||||
(AffineMatrix &) localToView
|
||||
);
|
||||
|
||||
warehouse->gaugeImageBin.Release(entity->GetResourceID());
|
||||
}
|
||||
DrawStaticEntity(entity);
|
||||
}
|
||||
|
||||
//
|
||||
// The picture now matches these bounds, and they are what the scale
|
||||
// everything on it was drawn at came from. Remember them: an arrival
|
||||
// that leaves them alone can be added to this picture rather than
|
||||
// replacing it.
|
||||
//
|
||||
builtMinX = minX;
|
||||
builtMinY = minY;
|
||||
builtMinZ = minZ;
|
||||
builtMaxX = maxX;
|
||||
builtMaxY = maxY;
|
||||
builtMaxZ = maxZ;
|
||||
backgroundBuilt = True;
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Redraw new background, restart moving entity display
|
||||
//-----------------------------------------------------------
|
||||
@@ -2166,6 +2297,71 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// DrawStaticEntity
|
||||
//#############################################################################
|
||||
//
|
||||
// One placement onto the cached background, at the scale that background
|
||||
// was built at. Shared by the full rebuild above and by the single-arrival
|
||||
// path in NotifyOfNewInterestingEntity, so the two cannot drift into
|
||||
// drawing the same track two different ways.
|
||||
//
|
||||
Logical
|
||||
GPS::DrawStaticEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
Check(background);
|
||||
Check(renderer);
|
||||
|
||||
L4Warehouse
|
||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
||||
Check(warehouse);
|
||||
|
||||
L4GaugeImage
|
||||
*gauge_image =
|
||||
warehouse->gaugeImageBin.GetIfAlreadyExists(entity->GetResourceID());
|
||||
if (gauge_image == NULL)
|
||||
{
|
||||
return False; // nothing of it appears on the map
|
||||
}
|
||||
Check(gauge_image);
|
||||
|
||||
L4BytePort
|
||||
backgroundPort(background, "background", 0);
|
||||
GraphicsView
|
||||
backgroundView(&backgroundPort);
|
||||
backgroundView.SetOrigin(width>>1, height>>1);
|
||||
|
||||
Vector3D
|
||||
scaling_vector;
|
||||
scaling_vector.x = pixelsPerMeter;
|
||||
scaling_vector.y = pixelsPerMeter;
|
||||
scaling_vector.z = pixelsPerMeter;
|
||||
|
||||
AffineMatrix
|
||||
worldToView,
|
||||
localToView;
|
||||
|
||||
worldToView.BuildIdentity();
|
||||
worldToView *= centeringOffset; // translation
|
||||
worldToView *= scaling_vector;
|
||||
|
||||
localToView.Multiply(entity->localToWorld, worldToView);
|
||||
|
||||
gauge_image->Draw(
|
||||
LODIndex, // value set by creator
|
||||
metersPerPixel,
|
||||
&backgroundView,
|
||||
0, // default color
|
||||
(AffineMatrix &) localToView
|
||||
);
|
||||
|
||||
warehouse->gaugeImageBin.Release(entity->GetResourceID());
|
||||
return True;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GPS::Execute()
|
||||
|
||||
+27
-1
@@ -330,12 +330,27 @@ public:
|
||||
BecameActive(); // virtual function in 'GaugeBase'
|
||||
void
|
||||
NotifyOfNewInterestingEntity(Entity *entity);
|
||||
//
|
||||
// Terrain that leaves interest range is dropped from the map, and
|
||||
// there is no way to un-draw one placement from a composited picture
|
||||
// - so this is the one case that still costs a full rebuild.
|
||||
//
|
||||
void
|
||||
NotifyOfBecomingUninterestingEntity(Entity *entity);
|
||||
void
|
||||
Execute();
|
||||
|
||||
protected:
|
||||
void
|
||||
UpdateStaticEntities();
|
||||
//
|
||||
// Draw one placement into the cached background, at the scale that
|
||||
// background was built at. False if the entity has no GaugeImage and
|
||||
// so never appears on the map at all - which on the big tracks is
|
||||
// most of the terrain in them.
|
||||
//
|
||||
Logical
|
||||
DrawStaticEntity(Entity *entity);
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -344,7 +359,18 @@ protected:
|
||||
|
||||
Logical
|
||||
needsStaticUpdate,
|
||||
needsScreenUpdate;
|
||||
needsScreenUpdate,
|
||||
// is there a picture worth adding a single placement to?
|
||||
backgroundBuilt;
|
||||
//
|
||||
// The static bounds the background was last built for. They decide
|
||||
// the scale, so terrain arriving inside them can be drawn onto the
|
||||
// picture instead of forcing a new one - see
|
||||
// GPS::NotifyOfNewInterestingEntity.
|
||||
//
|
||||
Scalar
|
||||
builtMinX, builtMinY, builtMinZ,
|
||||
builtMaxX, builtMaxY, builtMaxZ;
|
||||
Scalar
|
||||
LODIndex,
|
||||
metersPerPixel,
|
||||
|
||||
+50
-1
@@ -51,6 +51,20 @@ namespace
|
||||
const char kResultsKey[] = "res";
|
||||
const char kScenarioKey[] = "sc";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Simulation protocol revision. Bump this whenever a change makes
|
||||
// two builds simulate the same mission differently - it is not the
|
||||
// wire format alone. Map entity ownership is dealt by advancing a
|
||||
// shared cursor once per map entity, so anything that changes which
|
||||
// entities are dealt at all silently desynchronizes who owns what.
|
||||
//
|
||||
// 2 - doorframes became local Hermit clockwork and are no longer
|
||||
// dealt, which shifts every subsequent map entity's owner
|
||||
// 1 - the 3-machine verified Steam build
|
||||
//-------------------------------------------------------------------
|
||||
const char kNetRevision[] = "2";
|
||||
const char kNetRevKey[] = "nr";
|
||||
|
||||
// the owner's mission setup, shown to everyone in the room
|
||||
const char kMapKey[] = "mp";
|
||||
const char kTimeKey[] = "td";
|
||||
@@ -164,6 +178,9 @@ namespace
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
|
||||
RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));
|
||||
|
||||
// what this build simulates like, so a mismatched room cannot launch
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, kNetRevKey, kNetRevision);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Only the owner's menu decides the mission, so the owner also
|
||||
// publishes what it picked: the scenario (members need it to know
|
||||
@@ -172,6 +189,8 @@ namespace
|
||||
//---------------------------------------------------------------
|
||||
if (IsOwner())
|
||||
{
|
||||
// members check this before they act on the owner's go
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kNetRevKey, kNetRevision);
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
||||
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kMapKey,
|
||||
@@ -226,6 +245,7 @@ namespace
|
||||
char badge[24];
|
||||
char team[32]; // football pick
|
||||
char position[16];
|
||||
char netRev[8]; // simulation protocol revision
|
||||
Logical published;
|
||||
};
|
||||
|
||||
@@ -266,6 +286,9 @@ namespace
|
||||
strncpy(member->position,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
|
||||
sizeof(member->position) - 1);
|
||||
strncpy(member->netRev,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kNetRevKey),
|
||||
sizeof(member->netRev) - 1);
|
||||
member->published =
|
||||
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
||||
}
|
||||
@@ -797,14 +820,22 @@ namespace
|
||||
room.launchClicked = False;
|
||||
room.memberCount = CollectMembers(room.members);
|
||||
Logical all_published = True;
|
||||
Logical all_same_build = True;
|
||||
for (int i = 0; i < room.memberCount; ++i)
|
||||
{
|
||||
if (!room.members[i].published)
|
||||
{
|
||||
all_published = False;
|
||||
}
|
||||
if (strcmp(room.members[i].netRev, kNetRevision) != 0)
|
||||
{
|
||||
all_same_build = False;
|
||||
DEBUG_STREAM << "Lobby: " << room.members[i].name
|
||||
<< " simulates like rev '" << room.members[i].netRev
|
||||
<< "', we are rev '" << kNetRevision << "'\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (all_published && room.memberCount >= 1)
|
||||
if (all_published && all_same_build && room.memberCount >= 1)
|
||||
{
|
||||
++gLastGoNonce;
|
||||
char go[800];
|
||||
@@ -834,6 +865,24 @@ namespace
|
||||
//
|
||||
if (!IsOwner())
|
||||
{
|
||||
//
|
||||
// A room whose owner simulates differently than we do would
|
||||
// desynchronize silently rather than fail, so sit the race out
|
||||
// instead of flying into it.
|
||||
//
|
||||
const char *owner_rev = LobbyText(kNetRevKey);
|
||||
if (owner_rev[0] != '\0' &&
|
||||
strcmp(owner_rev, kNetRevision) != 0)
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: owner simulates like rev '"
|
||||
<< owner_rev << "', we are rev '" << kNetRevision
|
||||
<< "' - not launching\n" << std::flush;
|
||||
outcome = LobbyRoomLeft;
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
gInLobby = False;
|
||||
break;
|
||||
}
|
||||
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
|
||||
if (go != NULL && go[0] != '\0')
|
||||
{
|
||||
|
||||
+118
-56
@@ -444,9 +444,18 @@ void
|
||||
//----------------------------------------
|
||||
// Notify of mode change
|
||||
//----------------------------------------
|
||||
//
|
||||
// Unqualified, so the platform's override is the one that runs. The
|
||||
// RIO carries the four mode lamps on the Upper Right MFD and lights
|
||||
// them from here (VTVRIOMapper::NotifyOfControlModeChange); naming
|
||||
// the class suppressed the virtual call and landed on the base's
|
||||
// no-op instead, so the lamps never followed the mode the pilot had
|
||||
// just selected. Its neighbour has always gone out this way - see
|
||||
// VTVControlsMapper::SetConfigurationState.
|
||||
//
|
||||
if (previous_mode != controlMode)
|
||||
{
|
||||
L4VTVControlsMapper::NotifyOfControlModeChange(controlMode);
|
||||
NotifyOfControlModeChange(controlMode);
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -725,6 +734,13 @@ void
|
||||
mode_manager->AddModeMask(previousPresetModeMask);
|
||||
}
|
||||
//-----------------------------------
|
||||
// Move the lamps with the mappings.
|
||||
// Doing it here rather than in the
|
||||
// switch handler keeps the keyboard
|
||||
// presets (1-6) in step as well.
|
||||
//-----------------------------------
|
||||
NotifyOfPresetChange(previousPresetNumber, preset_number);
|
||||
//-----------------------------------
|
||||
// Save the new preset number
|
||||
//-----------------------------------
|
||||
previousPresetNumber = preset_number;
|
||||
@@ -733,6 +749,19 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
void
|
||||
L4VTVControlsMapper::NotifyOfPresetChange(
|
||||
int /*old_preset*/,
|
||||
int /*new_preset*/
|
||||
)
|
||||
{
|
||||
Check(this);
|
||||
// The base mapper has no preset lamps to move.
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
//########################### ThrustmasterMapper ##############################
|
||||
//#############################################################################
|
||||
@@ -854,18 +883,18 @@ void
|
||||
//-------------------------------------------------------
|
||||
// Set driving modes
|
||||
//-------------------------------------------------------
|
||||
case 'b':
|
||||
case 'B': SetControlsMode(BasicMode); break;
|
||||
|
||||
case 's':
|
||||
case 'S': SetControlsMode(StandardMode); break;
|
||||
|
||||
case 'v':
|
||||
case 'V': SetControlsMode(VeteranMode); break;
|
||||
|
||||
case 'm':
|
||||
case 'M': SetControlsMode(MasterMode); break;
|
||||
|
||||
//
|
||||
// B / S / V / M used to drop straight into Basic, Standard,
|
||||
// Veteran and Master here. The driving mode is a panel
|
||||
// decision - the four buttons on the Upper Right MFD, with the
|
||||
// lamps that say which one you are in - and a bare letter key
|
||||
// changing it behind the player's back is not that. Worse in
|
||||
// 4.12 than it ever was in the pod: the whole letter board is
|
||||
// the MFD banks now, so those four letters are buttons in their
|
||||
// own right and would have fired twice.
|
||||
//
|
||||
// Nothing replaces them. Press the mode you want.
|
||||
//
|
||||
//-------------------------------------------------------
|
||||
// Configuration stuff
|
||||
//-------------------------------------------------------
|
||||
@@ -1400,45 +1429,13 @@ void
|
||||
if (message->dataContents > 0)
|
||||
{
|
||||
//-----------------------------------
|
||||
// Choose a new preset
|
||||
// Choose a new preset. PresetEnable
|
||||
// ignores a repeat of the lit switch
|
||||
// and moves the lamps itself.
|
||||
//-----------------------------------
|
||||
int
|
||||
current_preset_number = (message->dataContents - 1)
|
||||
- LBE4ControlsManager::ButtonSecondary7;
|
||||
|
||||
if (previousPresetNumber != current_preset_number)
|
||||
{
|
||||
//-----------------------------------
|
||||
// Set the old preset lamp to 'dim'
|
||||
//-----------------------------------
|
||||
if (previousPresetNumber >= 0)
|
||||
{
|
||||
Verify(previousPresetNumber < presetCount);
|
||||
|
||||
if (modeLamp[previousPresetNumber] != NULL)
|
||||
{
|
||||
Check(modeLamp[previousPresetNumber]);
|
||||
modeLamp[previousPresetNumber]->SetState(L4Lamp::LampStateDim);
|
||||
}
|
||||
}
|
||||
//-----------------------------------
|
||||
// Set the new preset lamp to 'on'
|
||||
//-----------------------------------
|
||||
if (current_preset_number >= 0)
|
||||
{
|
||||
Verify(current_preset_number < presetCount);
|
||||
|
||||
if (modeLamp[current_preset_number] != NULL)
|
||||
{
|
||||
Check(modeLamp[current_preset_number]);
|
||||
modeLamp[current_preset_number]->SetState(L4Lamp::LampStateOn);
|
||||
}
|
||||
}
|
||||
//-----------------------------------
|
||||
// Change presets
|
||||
//-----------------------------------
|
||||
PresetEnable(current_preset_number);
|
||||
}
|
||||
PresetEnable(
|
||||
(message->dataContents - 1) - LBE4ControlsManager::ButtonSecondary7
|
||||
);
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -1626,6 +1623,18 @@ void
|
||||
modeLamp[lamp_number]->SetState(L4Lamp::LampStateOn);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
// Remember what is lit
|
||||
//----------------------------------
|
||||
//
|
||||
// previousControlMode is the lamp the NEXT change dims, and nothing
|
||||
// used to write it after construction set it to -1. Every mode
|
||||
// therefore lit its own lamp against a dim that matched nothing, and
|
||||
// the panel accumulated lamps instead of following the selection.
|
||||
//
|
||||
previousControlMode = controlMode;
|
||||
|
||||
//-----------------------------------
|
||||
// Invoke ancestral method
|
||||
//-----------------------------------
|
||||
@@ -1655,6 +1664,44 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// The six amber switches down the map's right flank. Called by PresetEnable,
|
||||
// so the lamps follow the mappings no matter what asked for the change.
|
||||
//
|
||||
void
|
||||
VTVRIOMapper::NotifyOfPresetChange(int old_preset, int new_preset)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
//----------------------------------
|
||||
// Set the old preset lamp to 'dim'
|
||||
//----------------------------------
|
||||
if (old_preset >= 0)
|
||||
{
|
||||
Verify(old_preset < presetCount);
|
||||
|
||||
if (presetLamp[old_preset] != NULL)
|
||||
{
|
||||
Check(presetLamp[old_preset]);
|
||||
presetLamp[old_preset]->SetState(L4Lamp::LampStateDim);
|
||||
}
|
||||
}
|
||||
//----------------------------------
|
||||
// Set the new preset lamp to 'on'
|
||||
//----------------------------------
|
||||
if (new_preset >= 0)
|
||||
{
|
||||
Verify(new_preset < presetCount);
|
||||
|
||||
if (presetLamp[new_preset] != NULL)
|
||||
{
|
||||
Check(presetLamp[new_preset]);
|
||||
presetLamp[new_preset]->SetState(L4Lamp::LampStateOn);
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// Construction and Destruction Support
|
||||
//
|
||||
@@ -1680,6 +1727,20 @@ VTVRIOMapper::VTVRIOMapper(
|
||||
leftPedal = 0.0f;
|
||||
rightPedal = 0.0f;
|
||||
|
||||
//------------------------------------------------
|
||||
// There are no lamps until the mapping blocks
|
||||
// below make them - and under NOMODES they never
|
||||
// do, so the notify methods must see NULLs.
|
||||
//------------------------------------------------
|
||||
{
|
||||
int
|
||||
i;
|
||||
|
||||
for(i=0; i<configLampCount; ++i) configLamp[i] = NULL;
|
||||
for(i=0; i<modeLampCount; ++i) modeLamp[i] = NULL;
|
||||
for(i=0; i<presetCount; ++i) presetLamp[i] = NULL;
|
||||
}
|
||||
|
||||
Check(application);
|
||||
LBE4ControlsManager
|
||||
*controls = Cast_Object(
|
||||
@@ -1915,13 +1976,14 @@ VTVRIOMapper::VTVRIOMapper(
|
||||
this
|
||||
);
|
||||
|
||||
// These lamps are explicitly controlled by SelectPresetMessageHandler
|
||||
modeLamp[i] = CreateControlledLamp(button_number[i]);
|
||||
// These lamps are explicitly controlled by NotifyOfPresetChange.
|
||||
// They are six, and they are NOT the four mode lamps above.
|
||||
presetLamp[i] = CreateControlledLamp(button_number[i]);
|
||||
|
||||
if (modeLamp[i] != NULL)
|
||||
if (presetLamp[i] != NULL)
|
||||
{
|
||||
Check(modeLamp[i]);
|
||||
modeLamp[i]->SetState(
|
||||
Check(presetLamp[i]);
|
||||
presetLamp[i]->SetState(
|
||||
(i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,12 @@ ModeMask
|
||||
//
|
||||
void
|
||||
PresetEnable(int preset_number);
|
||||
|
||||
// Announced by PresetEnable for EVERY preset change, whichever way it was
|
||||
// triggered - map-flank switch or keyboard. Platforms carrying preset
|
||||
// lamps move them here; the base mapper has none.
|
||||
virtual void
|
||||
NotifyOfPresetChange(int old_preset, int new_preset);
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Protected data
|
||||
//
|
||||
@@ -250,6 +256,9 @@ public:
|
||||
void
|
||||
NotifyOfConfigurationModeChange(Logical new_state);
|
||||
|
||||
void
|
||||
NotifyOfPresetChange(int old_preset, int new_preset);
|
||||
|
||||
void
|
||||
SetPerformance(Performance performance)
|
||||
{
|
||||
|
||||
+350
-383
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user