Cyd asked for an analysis of the networking stack and what would make the simulation feel better over the internet. The analysis found something more urgent than latency: the transport has been losing data silently since the arcade, and nothing in the game could see it happen. Every send result was discarded - L4NET, the console, all of it. On the 1ms arcade LAN the socket buffer never filled, so it never mattered. Over the internet it matters twice. A peer stalled in its own 10-30 second mission load stops reading, its window closes, and our nonblocking send starts answering would-block, which threw the message away; or worse, answering a PARTIAL count, and since framing on that stream is recovered purely from each message's length prefix, the bytes that never followed sheared it for good. Both are reachable in an ordinary race, because every race has a load in it. So sends go through a bounded per-connection queue now. What the wire will not take is kept, byte-exact, and retried at three flush points - before the render (the present blocks on vsync, and this frame's state should be travelling while it does), at the top of the receive pump, and before a connect sequence. Nothing is ever dropped from the middle: these are reliable ordered messages carrying entity creation, damage and race control, so a queue that overflows its 256K declares the connection dead and lets the disconnect path run rather than quietly desyncing the stream. RP412NETSENDQ=0 restores the old behaviour and still logs what it would have lost, which is the honest way to A/B it. On Steam the same queue finally surfaces k_EResultLimitExceeded, which the old code collapsed into -1 and discarded - that was backpressure, unlogged. The receive side gained the check the release build never had. The length prefix is untrusted input; Verify() compiles away in release, so a corrupt one went to memmove as a negative, or copied 4096 bytes of assembled packet into a 1600-byte stack buffer, or named a size the pad could never complete and wedged the connection forever. It is now validated against the same bounds the sender works to, and a stream that fails them is dropped like any other lost peer. And a fry that never ends: drop zones are map entities dealt round-robin at load, ownership transfer is not implemented, so a leaver's pads stay in the DropZones group. The respawn request dispatched to one goes to a host that is gone - dropped at the send, the 'no host N in the table' path - and the two-second retry re-dispatches to the same dead owner forever. The pad scan now skips zones whose owner has left, and re-validates one assigned earlier before reusing it. The rest is measurement, because the symptoms this work exists to chase are all reported in prose and none of them are in any log. Sixteen logs from the six-player night contain zero player-facing latency lines. A race now ends with a NetLog summary: per remote pod, how many updates arrived and how evenly (median and p95 out of a log2 histogram), the widest gap, how many gaps were long enough to mean a quiet sender versus short enough to mean OUR loop stalled, how often its motion snapped instead of blending, and how far arriving updates moved it. Per peer, whether the clock alignment ever had to step mid-race - which is the input for deciding if it needs slewing, rather than guessing. The mission t0 tick goes in the log too, alongside the console's per-pod RunMission send ticks, because nothing has ever measured how far apart the machines actually start; the clockwork doors inherit that skew directly. RP412NETSTATS adds the transport's own view - per connection: messages, bytes, wire writes, partials, refusals, how much sat queued - and on Steam the first read this codebase has ever taken of GetConnectionRealTime Status. Ping, quality, pending and unacked bytes, and one route description per connection at teardown. The API was vendored and never called; there was no RTT number anywhere in the game. Finally, rpl4opt -spoolstats reads any recording offline. The data was already in every spool ever made and nothing read it that way: the recorder restamps each packet with local arrival time while the update records inside keep the sender's sim-grid stamp, so the difference is clock offset plus one-way delay, and the same running-minimum estimator the game runs live separates them. It prints delay above the per-host minimum, and decomposes each entity's gaps into sender pacing versus delivery jitter - which no live counter can do. It lives in the game exe rather than RPL4TOOL because the tool is deliberately not /Zp1 and would misread every struct in the file. Verified on the two-pod loopback harness: mesh up, egg fed, 60s raced, stopped on command, scores collected, and both summaries reading exactly what a pair of PARKED pods should read - heartbeat cadence, one snap per heartbeat, sub-quarter-metre corrections, no clock steps. The t0 ticks and the netclock offsets agree with each other to the two seconds the pods launched apart. The latency tier is deliberately NOT here. TCP_NODELAY, the Steam NoNagle flag, per-frame coalescing and the pre-sim receive drain are all scoped and all wait on this build's numbers, because the point of shipping measurement first is to find out whether the thing we would fix is the thing that hurts. Nagle is still on. Interest management is still inert. The wire format is untouched, so this build and the last one still race each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
950 lines
25 KiB
C++
950 lines
25 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "player.h"
|
|
#include "icom.h"
|
|
#include "camship.h"
|
|
#include "dropzone.h"
|
|
#include "mission.h"
|
|
#include "audio.h"
|
|
#include "director.h"
|
|
#include "app.h"
|
|
#include "hostmgr.h"
|
|
#include "nttmgr.h"
|
|
#include "console.h"
|
|
#include "appmsg.h"
|
|
|
|
//#############################################################################
|
|
//####################### Player::StatusMessage #########################
|
|
//#############################################################################
|
|
|
|
Player__StatusMessage::Player__StatusMessage(Player *player_involved, int message_type, Scalar display_time)
|
|
{
|
|
playerInvolved = player_involved;
|
|
messageType = message_type;
|
|
displayTime = display_time;
|
|
}
|
|
|
|
//#############################################################################
|
|
//############################### Player ################################
|
|
//#############################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation* Player::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Entity::GetClassDerivations(), "Player");
|
|
return &classDerivations;
|
|
}
|
|
|
|
Player::SharedData
|
|
Player::DefaultData(
|
|
Player::GetClassDerivations(),
|
|
Player::GetMessageHandlers(),
|
|
Player::GetAttributeIndex(),
|
|
Player::StateCount,
|
|
(Entity::MakeHandler)Player::Make
|
|
);
|
|
|
|
//#############################################################################
|
|
// Attribute Support
|
|
//
|
|
|
|
const Player::IndexEntry
|
|
Player::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(Player, CurrentScore, currentScore),
|
|
ATTRIBUTE_ENTRY(Player, PlayerRanking, playerRanking),
|
|
ATTRIBUTE_ENTRY(Player, DropZoneLocation, dropZoneLocation),
|
|
ATTRIBUTE_ENTRY(Player, StatusMessagePointer,statusMessagePointer),
|
|
ATTRIBUTE_ENTRY(Player, PlayerBitmapIndex, playerBitmapIndex),
|
|
ATTRIBUTE_ENTRY(Player, PlayerHighlighted, playerHighlighted)
|
|
};
|
|
|
|
Player::AttributeIndexSet& Player::GetAttributeIndex()
|
|
{
|
|
static Player::AttributeIndexSet attributeIndex(ELEMENTS(Player::AttributePointers),
|
|
Player::AttributePointers,
|
|
Entity::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Message Support
|
|
//
|
|
const Receiver::HandlerEntry
|
|
Player::MessageHandlerEntries[]=
|
|
{
|
|
MESSAGE_ENTRY(Player, DropZoneReply),
|
|
MESSAGE_ENTRY(Player, Score),
|
|
MESSAGE_ENTRY(Player, VehicleDead),
|
|
MESSAGE_ENTRY(Player, MissionStarting),
|
|
MESSAGE_ENTRY(Player, MissionEnding)
|
|
};
|
|
|
|
Receiver::MessageHandlerSet& Player::GetMessageHandlers()
|
|
{
|
|
static Receiver::MessageHandlerSet messageHandlers(ELEMENTS(Player::MessageHandlerEntries), Player::MessageHandlerEntries, Entity::GetMessageHandlers());
|
|
return messageHandlers;
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::MissionStartingMessageHandler(Message *)
|
|
{
|
|
Check(this);
|
|
Tell("Player::MissionStartingMessageHandler - starting fade in\n");
|
|
fadeTimeRemaining = 3.0f;
|
|
SetSimulationState(MissionStartingState);
|
|
Check_Fpu();
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::MissionEndingMessageHandler(Message *)
|
|
{
|
|
Check(this);
|
|
fadeTimeRemaining = 3.0f;
|
|
ForceUpdate();
|
|
SetSimulationState(MissionEndingState);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Enumeration
|
|
Player::GetAudioRepresentation(
|
|
#if DEBUG_LEVEL>0
|
|
Entity *linked_entity
|
|
#else
|
|
Entity *
|
|
#endif
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(linked_entity);
|
|
Check_Fpu();
|
|
|
|
return
|
|
(GetInstance() == MasterInstance) ?
|
|
InternalAudioRepresentation : ExternalAudioRepresentation;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::ScoreMessageHandler(ScoreMessage *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
currentScore += message->scoreAward;
|
|
|
|
//
|
|
// HACK - ECH 7/6/95 - Allow the player vehicle to respond to score
|
|
// messages, allows attribute system to be used for scoring
|
|
//
|
|
Check(playerVehicle);
|
|
playerVehicle->RespondToScoreMessage(message);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::DropZoneReplyMessageHandler(
|
|
DropZone::ReplyMessage * //message
|
|
)
|
|
{
|
|
Check(this);
|
|
Fail("Drop zone reply should not be handled by base player class!\n");
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::CreatePlayerVehicle(Origin camera_origin)
|
|
{
|
|
Check(this);
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Create any MUNGA level vehicles
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
if (RPCameraLog())
|
|
{
|
|
DEBUG_STREAM << "CamLog: CreatePlayerVehicle, cameraShipPlayer="
|
|
<< (int) (IsCameraShipPlayer() ? 1 : 0) << "\n" << std::flush;
|
|
}
|
|
if (IsCameraShipPlayer())
|
|
{
|
|
Check(application);
|
|
ResourceFile *resources = application->GetResourceFile();
|
|
Check(resources);
|
|
Check_Pointer(playerMission->GetGameModel());
|
|
|
|
HostManager *host_manager = application->GetHostManager();
|
|
Check(host_manager);
|
|
ResourceDescription *camera_res =
|
|
resources->FindResourceDescription(
|
|
playerMission->GetGameModel(),
|
|
ResourceDescription::ModelListResourceType
|
|
);
|
|
if (RPCameraLog())
|
|
{
|
|
DEBUG_STREAM << "CamLog: model '" << playerMission->GetGameModel()
|
|
<< "' resource " << (camera_res ? "FOUND" : "MISSING") << "\n" << std::flush;
|
|
}
|
|
Check(camera_res);
|
|
CameraShip::MakeMessage
|
|
create_camera(
|
|
CameraShip::MakeMessageID,
|
|
sizeof(CameraShip::MakeMessage),
|
|
EntityID(host_manager->GetLocalHostID()),
|
|
CameraShipClassID,
|
|
EntityID::Null,
|
|
camera_res->resourceID,
|
|
CameraShip::DefaultFlags,
|
|
camera_origin,
|
|
Motion::Identity,
|
|
Motion::Identity
|
|
);
|
|
CameraShip *camera =
|
|
(CameraShip*)application->MakeAndLinkViewpointEntity(&create_camera);
|
|
Register_Object(camera);
|
|
playerVehicle = camera;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// A drop zone is only usable if the machine that owns it is still with
|
|
// us. Zones are map entities dealt round-robin across the hosts at load,
|
|
// and ownership transfer is not implemented - so a leaver's pads stay in
|
|
// the DropZones group forever, a request dispatched to one is silently
|
|
// dropped at the send ('Send: no host N in the table'), and the fry
|
|
// retry re-dispatches to the same dead owner every two seconds: the
|
|
// player never respawns. Locally-owned zones are always usable.
|
|
//
|
|
static Logical
|
|
DropZoneOwnerReachable(DropZone *dropzone)
|
|
{
|
|
Check(dropzone);
|
|
if (dropzone->GetInstance() != Entity::ReplicantInstance)
|
|
{
|
|
return True;
|
|
}
|
|
Check(application);
|
|
HostManager *host_manager = application->GetHostManager();
|
|
Check(host_manager);
|
|
Host *owner = host_manager->GetRemoteHost(dropzone->GetOwnerID());
|
|
return
|
|
(owner != NULL &&
|
|
owner->GetConnectStatus() == Host::OnLineConnectionStatus) ?
|
|
True : False;
|
|
}
|
|
|
|
//
|
|
// The closest non-podium drop zone, optionally restricted to pads whose
|
|
// owner is still connected. One function because the dead-owner case
|
|
// needs a second pass: when every pad's owner is gone the old behaviour
|
|
// (closest pad regardless) is the graceful floor to land on.
|
|
//
|
|
static DropZone *
|
|
Player_FindClosestDropZone(
|
|
Player *player,
|
|
Logical require_reachable_owner)
|
|
{
|
|
EntityManager *entity_manager = application->GetEntityManager();
|
|
Check(entity_manager);
|
|
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
|
Check(dropzones);
|
|
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
|
|
|
DropZone *dropzone;
|
|
DropZone *closest_dropzone = NULL;
|
|
Vector3D range;
|
|
Scalar closest_range = 0.0f;
|
|
|
|
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(dropzone);
|
|
if (!strnicmp(dropzone->GetDropZoneName(), "win", 3))
|
|
{
|
|
continue;
|
|
}
|
|
if (require_reachable_owner && !DropZoneOwnerReachable(dropzone))
|
|
{
|
|
continue;
|
|
}
|
|
range.Subtract(
|
|
player->localOrigin.linearPosition,
|
|
dropzone->localOrigin.linearPosition
|
|
);
|
|
Scalar len = range.LengthSquared();
|
|
if (closest_dropzone == NULL || len < closest_range)
|
|
{
|
|
closest_dropzone = dropzone;
|
|
closest_range = len;
|
|
}
|
|
}
|
|
return closest_dropzone;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::VehicleDeadMessageHandler(VehicleDeadMessage *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
Check(application);
|
|
|
|
|
|
//DEBUG_STREAM << "VEHICLEDEAD : " << endl << std::flush;
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// If we have let the player fry, send a message to the dropzone to give us
|
|
// a new location to return to
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
if (
|
|
message->deathCount == deathCount
|
|
&& GetSimulationState() != DropZoneAcquiredState)
|
|
{
|
|
DropZone *dropzone;
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// A pad assigned on an earlier try may have died with its owner - the
|
|
// fry retry reuses message->dropZoneID forever, so left unchecked it
|
|
// re-dispatches to the same dead host every two seconds. Reset it and
|
|
// let the closest-pad scan below pick a live one.
|
|
//---------------------------------------------------------------------
|
|
//
|
|
if (message->dropZoneID != EntityID::Null)
|
|
{
|
|
HostManager *host_manager = application->GetHostManager();
|
|
Check(host_manager);
|
|
Entity *assigned = host_manager->GetEntityPointer(message->dropZoneID);
|
|
if (assigned == NULL || !DropZoneOwnerReachable((DropZone*)assigned))
|
|
{
|
|
DEBUG_STREAM << "Respawn: assigned drop zone "
|
|
<< ((assigned == NULL) ? "is gone" : "belongs to a departed host")
|
|
<< " - picking a new pad\n" << std::flush;
|
|
message->dropZoneID = EntityID::Null;
|
|
}
|
|
}
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// If no dropzone has been assigned to this death, find the closest one
|
|
// whose owner is still connected
|
|
//---------------------------------------------------------------------
|
|
//
|
|
if (message->dropZoneID == EntityID::Null)
|
|
{
|
|
|
|
//DEBUG_STREAM << " Find Closest" << endl << std::flush;
|
|
|
|
DropZone *closest_dropzone = Player_FindClosestDropZone(this, True);
|
|
if (closest_dropzone == NULL)
|
|
{
|
|
//
|
|
// Every pad's owner is gone. Fall back to the old behaviour
|
|
// (closest pad regardless) so the retry loop keeps its old
|
|
// shape rather than crashing here.
|
|
//
|
|
DEBUG_STREAM << "Respawn: no drop zone with a live owner - "
|
|
<< "falling back to the closest regardless\n" << std::flush;
|
|
closest_dropzone = Player_FindClosestDropZone(this, False);
|
|
}
|
|
Check(closest_dropzone);
|
|
message->dropZoneID = closest_dropzone->GetEntityID();
|
|
dropzone = closest_dropzone;
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------
|
|
// Otherwise, get this one from the host manager
|
|
//----------------------------------------------
|
|
//
|
|
else
|
|
{
|
|
//DEBUG_STREAM << " Use Assigned" << endl << std::flush;
|
|
|
|
HostManager *host = application->GetHostManager();
|
|
Check(host);
|
|
dropzone =
|
|
Cast_Object(
|
|
DropZone*,
|
|
host->GetEntityPointer(message->dropZoneID)
|
|
);
|
|
}
|
|
|
|
Check(dropzone);
|
|
DropZone::AssignDropZoneMessage
|
|
request(
|
|
DropZone::AssignDropZoneMessageID,
|
|
sizeof(DropZone::AssignDropZoneMessage),
|
|
GetEntityID(),
|
|
DropZoneReplyMessageID,
|
|
deathCount
|
|
);
|
|
dropzone->Dispatch(&request);
|
|
Time when = Now();
|
|
when += 2.0f;
|
|
application->Post(HighEventPriority, this, message, when);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::HuntForDropZone(Scalar)
|
|
{
|
|
Check(this);
|
|
Check(playerMission);
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// See if our designated drop zone is around yet. If the dropzone group
|
|
// does not exist, then no dropzones at all are available
|
|
//----------------------------------------------------------------------
|
|
//
|
|
Check(application);
|
|
EntityManager *entity_manager = application->GetEntityManager();
|
|
Check(entity_manager);
|
|
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
|
if (!dropzones)
|
|
{
|
|
Check_Fpu();
|
|
return;
|
|
}
|
|
|
|
Check(dropzones);
|
|
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
|
DropZone *dropzone;
|
|
const char* zone_name = playerMission->GetDropZoneName();
|
|
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
|
|
{
|
|
if (!strcmp(dropzone->GetDropZoneName(), zone_name))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// If it is, reset our performance, and send a message to the drop zone to
|
|
// give us a location
|
|
//------------------------------------------------------------------------
|
|
//
|
|
if (dropzone)
|
|
{
|
|
|
|
//DEBUG_STREAM << "FOUND DROPZONE" << endl << std::flush;
|
|
|
|
Check(dropzone);
|
|
SetPerformance(&Player::DoNothingOnce);
|
|
VehicleDeadMessage
|
|
dead(
|
|
VehicleDeadMessageID,
|
|
sizeof(VehicleDeadMessage),
|
|
dropzone->GetEntityID()
|
|
);
|
|
dead.deathCount = -2;
|
|
|
|
DropZone::AssignDropZoneMessage
|
|
request(
|
|
DropZone::AssignDropZoneMessageID,
|
|
sizeof(DropZone::AssignDropZoneMessage),
|
|
GetEntityID(),
|
|
DropZoneReplyMessageID,
|
|
deathCount
|
|
);
|
|
dropzone->Dispatch(&request);
|
|
Time when = Now();
|
|
when += 2.0f;
|
|
application->Post(HighEventPriority, this, &dead, when);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::ManageApplicationStatus(Scalar time_slice)
|
|
{
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// If we are in the game start/stop phase, count the timer down and send the
|
|
// appropriate message to the application when done
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Check(this);
|
|
Check(application);
|
|
switch (GetSimulationState())
|
|
{
|
|
case MissionStartingState:
|
|
Verify(fadeTimeRemaining <= 3.0f);
|
|
fadeTimeRemaining -= time_slice;
|
|
if (fadeTimeRemaining <= 0.0f)
|
|
{
|
|
SetSimulationState(VehicleTranslocatedState);
|
|
Check(application);
|
|
switch (application->GetApplicationState())
|
|
{
|
|
case Application::LaunchingMission:
|
|
{
|
|
Application::RunMissionMessage message;
|
|
application->Dispatch(&message);
|
|
}
|
|
break;
|
|
case Application::ResumingMission:
|
|
{
|
|
Application::ResumeMissionMessage message;
|
|
application->Dispatch(&message);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case MissionEndingState:
|
|
fadeTimeRemaining -= time_slice;
|
|
if (fadeTimeRemaining <= 0.0f)
|
|
{
|
|
Check(application);
|
|
switch (application->GetApplicationState())
|
|
{
|
|
case Application::EndingMission:
|
|
{
|
|
// Fill in appropriate exit code here. GAH
|
|
Application::StopMissionMessage message(NullExitCodeID);
|
|
application->Dispatch(&message);
|
|
}
|
|
break;
|
|
case Application::AbortingMission:
|
|
{
|
|
// Fill in appropriate exit code here. GAH
|
|
Application::AbortMissionMessage message(NullExitCodeID);
|
|
application->Dispatch(&message);
|
|
}
|
|
break;
|
|
case Application::SuspendingMission:
|
|
{
|
|
Application::SuspendMissionMessage message;
|
|
application->Dispatch(&message);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::CameraShipSimulation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
CalcRanking();
|
|
|
|
ManageApplicationStatus(time_slice);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::PlayerSimulation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
//-------------------------------
|
|
// Calculate the player's ranking
|
|
//-------------------------------
|
|
CalcRanking();
|
|
|
|
//----------------------------------
|
|
// ...manage the application status?
|
|
//----------------------------------
|
|
ManageApplicationStatus(time_slice);
|
|
|
|
//-------------------------------
|
|
// Copy vehicle position
|
|
//-------------------------------
|
|
if (playerVehicle)
|
|
{
|
|
Check(playerVehicle);
|
|
localOrigin = playerVehicle->localOrigin;
|
|
localToWorld = playerVehicle->localToWorld;
|
|
}
|
|
|
|
//---------------------------------
|
|
// Service the status message queue
|
|
//---------------------------------
|
|
StatusMessageUpdate(time_slice);
|
|
|
|
Check_Fpu();
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::GoToVehicle(Scalar)
|
|
{
|
|
Check(this);
|
|
if (playerVehicle)
|
|
{
|
|
Check(playerVehicle);
|
|
localOrigin = playerVehicle->localOrigin;
|
|
localToWorld = playerVehicle->localToWorld;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
int
|
|
Player::CalcRanking()
|
|
{
|
|
|
|
//
|
|
//---------------------------------------
|
|
// Get all players from the entity group
|
|
//---------------------------------------
|
|
//
|
|
EntityGroup *player_group =
|
|
application->GetEntityManager()->FindGroup("Players");
|
|
if(player_group)
|
|
{
|
|
int num_players = 0;
|
|
Player *active_player;
|
|
//
|
|
//-----------------------------------------------------
|
|
// Iterate through the players add to the sorted chain
|
|
//-----------------------------------------------------
|
|
//
|
|
ChainIteratorOf<Node*> iterator(player_group->groupMembers);
|
|
VChainOf<Player*, Scalar> player_rank(NULL, False);
|
|
while ((active_player = (Player*) iterator.ReadAndNext()) != NULL)
|
|
{
|
|
if(active_player->IsScoringPlayer())
|
|
{
|
|
player_rank.AddValue(active_player, active_player->currentScore);
|
|
++num_players;
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Reset Player Highlighted
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
active_player->playerHighlighted = False;
|
|
}
|
|
else
|
|
{
|
|
active_player->playerRanking = -1;
|
|
}
|
|
|
|
}
|
|
--num_players;
|
|
//
|
|
//------------------------------------------------
|
|
// Iterate through the chain assigning the rank
|
|
//------------------------------------------------
|
|
//
|
|
VChainIteratorOf<Player*, Scalar> rank_iterator(player_rank);
|
|
|
|
|
|
while ((active_player = rank_iterator.ReadAndNext()) != NULL)
|
|
{
|
|
active_player->playerRanking = num_players;
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// If player in first place turn on player highlighted
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
if (!active_player->playerRanking)
|
|
{
|
|
active_player->playerHighlighted = True;
|
|
}
|
|
--num_players;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
return 1;
|
|
}
|
|
//#############################################################################
|
|
// Update Support
|
|
//
|
|
void
|
|
Player::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
Entity::ReadUpdateRecord(message);
|
|
UpdateRecord* record = (UpdateRecord*) message;
|
|
|
|
currentScore = record->currentScore;
|
|
dropZoneLocation = record->dropZoneLocation;
|
|
Check_Fpu();
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::WriteUpdateRecord(
|
|
Simulation::UpdateRecord *record,
|
|
int update_model
|
|
)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(record);
|
|
|
|
Entity::WriteUpdateRecord(record, update_model);
|
|
|
|
UpdateRecord* update = (UpdateRecord*) record;
|
|
|
|
update->recordLength = sizeof(*update);
|
|
update->currentScore = currentScore;
|
|
update->dropZoneLocation = dropZoneLocation;
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Player::Player(
|
|
Player::MakeMessage *creation_message,
|
|
Player::SharedData &virtual_data
|
|
):
|
|
Entity(creation_message, virtual_data),
|
|
statusMessageQueue(NULL)
|
|
{
|
|
Check_Pointer(this);
|
|
Check_Pointer(creation_message);
|
|
|
|
scenarioRole = NULL;
|
|
//---------------------------------
|
|
// Initialize data
|
|
//---------------------------------
|
|
statusMessagePointer = NULL;
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Initialize the playerMission
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
playerMission = application->GetCurrentMission();
|
|
Check(playerMission);
|
|
//
|
|
//---------------------------------
|
|
// Set the resource ID for a player
|
|
//---------------------------------
|
|
//
|
|
Check(application);
|
|
Check(application->GetResourceFile());
|
|
ResourceDescription *resource_description =
|
|
application->GetResourceFile()->FindResourceDescription(
|
|
"player",
|
|
ResourceDescription::ModelListResourceType
|
|
);
|
|
if (resource_description != NULL)
|
|
{
|
|
Verify(resourceID == ResourceDescription::NullResourceID);
|
|
resourceID = resource_description->resourceID;
|
|
}
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Get the host type associated with the entity ID
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
HostType host_type;
|
|
|
|
HostID host_ID = entityID.GetHostID();
|
|
Check(application);
|
|
HostManager *host_mgr = application->GetHostManager();
|
|
Check(host_mgr);
|
|
Host *local_host = host_mgr->GetLocalHost();
|
|
Check(local_host);
|
|
if (host_ID == local_host->GetHostID())
|
|
{
|
|
host_type = local_host->GetHostType();
|
|
}
|
|
else
|
|
{
|
|
HostManager::RemoteHostIterator iterator(host_mgr);
|
|
local_host = iterator.Find(host_ID);
|
|
Check(local_host);
|
|
host_type = local_host->GetHostType();
|
|
}
|
|
|
|
if (IsCameraShipPlayer())
|
|
{
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Make A CameraPlayers Entity Group
|
|
// since cameras are not involved in
|
|
// standard game play they need a
|
|
// separate Entity Group!
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
playerBitmapIndex = -1;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Make A Regular Players Entity Group
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Check(application);
|
|
EntityManager *entity_manager = application->GetEntityManager();
|
|
Check(entity_manager);
|
|
EntityGroup *players = entity_manager->UseGroup("Players");
|
|
Check(players);
|
|
players->Add(this);
|
|
playerBitmapIndex = creation_message->playerBitmapIndex;
|
|
}
|
|
|
|
SetValidFlag();
|
|
playerVehicle = NULL;
|
|
if (GetInstance() != ReplicantInstance)
|
|
{
|
|
Check(application->GetCurrentMission());
|
|
playerMission = application->GetCurrentMission();
|
|
SetPerformance(&Player::HuntForDropZone);
|
|
}
|
|
else
|
|
{
|
|
SetPerformance(&Player::GoToVehicle);
|
|
playerMission = NULL;
|
|
}
|
|
|
|
playerRanking = 0;
|
|
deathCount = -2;
|
|
|
|
//-------------------------------------
|
|
// Clear score
|
|
//-------------------------------------
|
|
currentScore = 0.0f;
|
|
//-------------------------------------
|
|
// Create intercom object
|
|
//-------------------------------------
|
|
Check(application);
|
|
IcomManager
|
|
*intercom_manager = application->GetIntercomManager();
|
|
Check(intercom_manager);
|
|
|
|
intercomPointer = intercom_manager->MakeIcom(GetOwnerID());
|
|
Check(intercomPointer);
|
|
// registered in MakeIcom()
|
|
|
|
playerHighlighted = False;
|
|
fadeTimeRemaining = 0.0f;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Player* Player::Make(Player::MakeMessage *creation_message)
|
|
{
|
|
Check_Fpu();
|
|
return new Player(creation_message, DefaultData);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Player::~Player()
|
|
{
|
|
Check(this);
|
|
|
|
//--------------------------------------------
|
|
// Delete intercom object
|
|
//--------------------------------------------
|
|
Check(intercomPointer);
|
|
Unregister_Object(intercomPointer);
|
|
delete intercomPointer;
|
|
intercomPointer = NULL;
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::AddStatusMessage(StatusMessage *status_message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(status_message);
|
|
statusMessageQueue.Add(status_message);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Player::StatusMessageUpdate(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
//-------------------------------------
|
|
// We handle this queue as a STACK.
|
|
// The most recent message is presented
|
|
// first, causing older messages to
|
|
// wait "below" them on the stack.
|
|
// This allows a default message to
|
|
// sit at the "bottom" of the stack
|
|
// and allow others to override it
|
|
// temporarily.
|
|
//-------------------------------------
|
|
//
|
|
ChainIteratorOf<StatusMessage*>
|
|
iterator(statusMessageQueue);
|
|
//----------------------------------------
|
|
// ...we really want iterator.GetLast(),
|
|
// but since it doesn't exist we'll
|
|
// do it directly.
|
|
//----------------------------------------
|
|
statusMessagePointer = NULL;
|
|
{
|
|
StatusMessage
|
|
*the_message;
|
|
|
|
while ((the_message = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
statusMessagePointer = the_message;
|
|
}
|
|
}
|
|
//----------------------------------------
|
|
// If a message exists, decrement the
|
|
// timer: discard it if timed out.
|
|
//----------------------------------------
|
|
if (statusMessagePointer != NULL)
|
|
{
|
|
Check_Pointer(statusMessagePointer);
|
|
|
|
statusMessagePointer->displayTime -= time_slice;
|
|
if (statusMessagePointer->displayTime <= (Scalar) 0.0f)
|
|
{
|
|
Unregister_Object(statusMessagePointer);
|
|
delete statusMessagePointer;
|
|
statusMessagePointer = NULL;
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
Logical
|
|
Player::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|