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>
1890 lines
47 KiB
C++
1890 lines
47 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "entity.h"
|
|
#include "interest.h"
|
|
#include "exptbl.h"
|
|
#include "hostmgr.h"
|
|
#include "player.h"
|
|
#include "audio.h"
|
|
#include "fileutil.h"
|
|
#include "registry.h"
|
|
#include "event.h"
|
|
#include "app.h"
|
|
#include "nttmgr.h"
|
|
#include "subsystm.h"
|
|
#include "namelist.h"
|
|
#include "notation.h"
|
|
|
|
#if defined(TRACE_EXECUTE_ENTITY)
|
|
static BitTrace Execute_Entity("Execute Entity");
|
|
#define SET_EXECUTE_ENTITY() Execute_Entity.Set()
|
|
#define CLEAR_EXECUTE_ENTITY() Execute_Entity.Clear()
|
|
#else
|
|
#define SET_EXECUTE_ENTITY()
|
|
#define CLEAR_EXECUTE_ENTITY()
|
|
#endif
|
|
|
|
#if defined(TRACE_PERFORM_ENTITY)
|
|
static BitTrace Perform_Entity("Perform Entity");
|
|
#define SET_PERFORM_ENTITY() Perform_Entity.Set()
|
|
#define CLEAR_PERFORM_ENTITY() Perform_Entity.Clear()
|
|
#else
|
|
#define SET_PERFORM_ENTITY()
|
|
#define CLEAR_PERFORM_ENTITY()
|
|
#endif
|
|
|
|
#if defined(TRACE_PERFORM_SUBSYSTEMS)
|
|
static BitTrace Perform_Subsystems("Perform Subsystems");
|
|
#define SET_PERFORM_SUBSYSTEMS() Perform_Subsystems.Set()
|
|
#define CLEAR_PERFORM_SUBSYSTEMS() Perform_Subsystems.Clear()
|
|
#else
|
|
#define SET_PERFORM_SUBSYSTEMS()
|
|
#define CLEAR_PERFORM_SUBSYSTEMS()
|
|
#endif
|
|
|
|
//#############################################################################
|
|
//############################## Entity #################################
|
|
//#############################################################################
|
|
|
|
//#############################################################################
|
|
// Virtual Data support
|
|
//
|
|
Derivation* Entity::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Simulation::GetClassDerivations(), "Entity");
|
|
return &classDerivations;
|
|
}
|
|
|
|
Entity::SharedData
|
|
Entity::DefaultData(
|
|
Entity::GetClassDerivations(),
|
|
Entity::GetMessageHandlers(),
|
|
Entity::GetAttributeIndex(),
|
|
Entity::StateCount,
|
|
Entity::Make
|
|
);
|
|
|
|
//#############################################################################
|
|
// Message Support
|
|
//
|
|
const Receiver::HandlerEntry
|
|
Entity::MessageHandlerEntries[]=
|
|
{
|
|
{
|
|
Entity::MakeMessageID,
|
|
"Make",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::MakeReadyMessageID,
|
|
"MakeReady",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::RemakeEntityMessageID,
|
|
"RemakeEntity",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::RemakeReadyMessageID,
|
|
"RemakeReady",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::RemakeCompleteMessageID,
|
|
"RemakeComplete",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::RemakeIncompleteMessageID,
|
|
"RemakeIncomplete",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
MESSAGE_ENTRY(Entity, DestroyEntity),
|
|
{
|
|
Entity::TransferEntityMessageID,
|
|
"TransferEntity",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::TransferCompleteMessageID,
|
|
"TransferComplete",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::TransferIncompleteMessageID,
|
|
"TransferIncomplete",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
MESSAGE_ENTRY(Entity, BecomeInteresting),
|
|
MESSAGE_ENTRY(Entity, BecomeUninteresting),
|
|
MESSAGE_ENTRY(Entity, Update),
|
|
{
|
|
Entity::SubscribeReplicantMessageID,
|
|
"SubscribeReplicant",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
{
|
|
Entity::UnsubscribeReplicantMessageID,
|
|
"UnsubscribeReplicant",
|
|
(Entity::Handler)&Entity::DefaultMessageHandler
|
|
},
|
|
MESSAGE_ENTRY(Entity, TakeDamage),
|
|
MESSAGE_ENTRY(Entity, PlayerLink),
|
|
MESSAGE_ENTRY(Entity, TakeDamageStream)
|
|
};
|
|
|
|
Entity::MessageHandlerSet& Entity::GetMessageHandlers()
|
|
{
|
|
static Entity::MessageHandlerSet messageHandlers(ELEMENTS(Entity::MessageHandlerEntries), Entity::MessageHandlerEntries, Receiver::GetMessageHandlers());
|
|
return messageHandlers;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::Receive(Event* event)
|
|
{
|
|
Check(this);
|
|
Check(event);
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// If the entity is being transferred, post the message to the new owner of
|
|
// the entity
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// If the object is not valid, filter the message to determine which ones to
|
|
// defer and which ones to immediately process
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
if (!IsValid())
|
|
{
|
|
//switch case commented out by Ryan Bunker on 1/6/07
|
|
//switch (event->messageToSend->messageID)
|
|
{
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// The messages which should be allowed through should just break out of
|
|
// this switch
|
|
//----------------------------------------------------------------------
|
|
//
|
|
|
|
//default:
|
|
//
|
|
//-------------------
|
|
// Defer the messages
|
|
//-------------------
|
|
//
|
|
event->Defer();
|
|
return;
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------
|
|
// Go ahead and dispatch the message to the entity
|
|
//------------------------------------------------
|
|
//
|
|
Receiver::Receive(event);
|
|
|
|
#if defined(USE_FPU_ANALYSIS)
|
|
if (!Fpu_Ok())
|
|
{
|
|
Registry *registry = application->GetRegistry();
|
|
Receiver::SharedData *shared_data =
|
|
registry->GetStaticData(GetClassID());
|
|
Derivation *derivation = shared_data->derivedClasses;
|
|
Receiver::MessageHandlerSet *handlers =
|
|
shared_data->activeMessageHandlers;
|
|
DEBUG_STREAM << derivation->className << ',' << GetInstance() << endl << std::flush;
|
|
DEBUG_STREAM << handlers->GetName(event->messageToSend->messageID)
|
|
<< endl;
|
|
Fail("Event corrupted FPU!");
|
|
}
|
|
#else
|
|
Check_Fpu();
|
|
#endif
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::Dispatch(Receiver::Message* incoming)
|
|
{
|
|
Message *message = (Message*) incoming;
|
|
Check(this);
|
|
Check(message);
|
|
|
|
//
|
|
// Call Receiver if it is not entity aware!!!!!
|
|
//
|
|
if (message->messageID < Receiver::NextMessageID)
|
|
{
|
|
Receiver::Dispatch(message);
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------
|
|
// Initialize the entity information fields
|
|
//-----------------------------------------
|
|
//
|
|
message->entityID = entityID;
|
|
message->interestZoneID = interestZoneID;
|
|
|
|
//
|
|
//-------------------------------------------------------------------
|
|
// If this is a replicant instance, reroute the message to the master
|
|
//-------------------------------------------------------------------
|
|
//
|
|
if (GetInstance() == ReplicantInstance)
|
|
{
|
|
application->SendMessage(
|
|
ownerID,
|
|
EntityManager::EntityManagerClientID,
|
|
message
|
|
);
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------------
|
|
// If this entity is invalid, post the message as an event
|
|
//--------------------------------------------------------
|
|
//
|
|
else if (!IsValid())
|
|
{
|
|
// ECH 1/28/96 - application->Post(DefaultEventPriority, this, message);
|
|
application->Post(EntityInvalidEventPriority, this, message);
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------
|
|
// Otherwise, go ahead and receive the message
|
|
//--------------------------------------------
|
|
//
|
|
else
|
|
{
|
|
Receiver::Receive(message);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::DispatchToReplicants(Message *what)
|
|
{
|
|
Check(this);
|
|
Check(what);
|
|
|
|
what->entityID = entityID;
|
|
what->interestZoneID = interestZoneID;
|
|
Check(application);
|
|
InterestManager *int_mgr = application->GetInterestManager();
|
|
Check(int_mgr);
|
|
int_mgr->EntityBroadcastToReplicants(this, what);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Attribute Support
|
|
//
|
|
const Entity::IndexEntry
|
|
Entity::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(Entity, LocalToWorld, localToWorld),
|
|
ATTRIBUTE_ENTRY(Entity, LocalOrigin, localOrigin),
|
|
ATTRIBUTE_ENTRY(Entity, DamageZoneCount, damageZoneCount),
|
|
ATTRIBUTE_ENTRY(Entity, DamageZones, damageZones)
|
|
};
|
|
|
|
Entity::AttributeIndexSet& Entity::GetAttributeIndex()
|
|
{
|
|
static Entity::AttributeIndexSet attributeIndex(ELEMENTS(Entity::AttributePointers),
|
|
Entity::AttributePointers,
|
|
Simulation::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Model Support
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::WriteUpdateRecord(
|
|
Simulation::UpdateRecord *record,
|
|
int update_model
|
|
)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(record);
|
|
|
|
switch (update_model)
|
|
{
|
|
case DefaultUpdateModelBit:
|
|
{
|
|
Simulation::WriteUpdateRecord(record, update_model);
|
|
|
|
//
|
|
//----------------------------
|
|
// Write out the record update
|
|
//----------------------------
|
|
//
|
|
UpdateRecord *update = (UpdateRecord*)record;
|
|
update->localOrigin = localOrigin;
|
|
updateOrigin = localOrigin;
|
|
update->recordLength = sizeof(*update);
|
|
}
|
|
break;
|
|
case DamageZoneUpdateModelBit:
|
|
record->timeStamp = lastPerformance;
|
|
record->recordID = (Word)update_model;
|
|
WriteDamageUpdateRecord(record);
|
|
break;
|
|
|
|
default:
|
|
Simulation::WriteUpdateRecord(record, update_model);
|
|
break;
|
|
}
|
|
record->subsystemID = (Word)(EntitySubsystemID + 1);
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::UpdateMessageHandler(UpdateMessage *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
//
|
|
//------------------------------------------------------
|
|
// Position to the beginning of the update record blocks
|
|
//------------------------------------------------------
|
|
//
|
|
MemoryStream stream(message, message->messageLength, sizeof(*message));
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// Step through each block until there are no more remaining, and send the
|
|
// update out the the simulation indicated by the subsystemID
|
|
//
|
|
// This is the only point on the receive path that knows WHOSE update
|
|
// this is - the records themselves carry a timestamp but not an owner -
|
|
// so the sender is published here for the net clock to align against.
|
|
// Every record in the message, and the damage zones nested inside them,
|
|
// came from the same machine in the same frame.
|
|
//------------------------------------------------------------------------
|
|
//
|
|
//
|
|
// Only for an entity somebody else owns. Our own clock needs no
|
|
// aligning, and an update we somehow handed ourselves would otherwise
|
|
// drag lastUpdate back by a frame for no reason.
|
|
//
|
|
Check(application);
|
|
Check(application->GetHostManager());
|
|
Logical remote_owner =
|
|
GetOwnerID() != application->GetHostManager()->GetLocalHostID();
|
|
if (remote_owner)
|
|
{
|
|
NetClock_BeginUpdate(GetOwnerID());
|
|
}
|
|
|
|
while (stream.GetBytesRemaining())
|
|
{
|
|
Simulation::UpdateRecord *update =
|
|
(Simulation::UpdateRecord*)stream.GetPointer();
|
|
Check_Pointer(update);
|
|
Simulation *simulation = GetSimulation((int)update->subsystemID-1);
|
|
Check(simulation);
|
|
simulation->ReadUpdateRecord(update);
|
|
stream.AdvancePointer(update->recordLength);
|
|
}
|
|
|
|
if (remote_owner)
|
|
{
|
|
NetClock_EndUpdate();
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::ReadUpdateRecord(Simulation::UpdateRecord *record)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(record);
|
|
|
|
switch(record->recordID)
|
|
{
|
|
case DefaultUpdateModelBit:
|
|
{
|
|
//
|
|
//-----------------------
|
|
// Read the record update
|
|
//-----------------------
|
|
//
|
|
UpdateRecord *update = (UpdateRecord*)record;
|
|
|
|
//
|
|
// RP412CAMLOG: how hard does an arriving update MOVE this
|
|
// entity, and how often?
|
|
//
|
|
// Between updates a replicant is dead-reckoned from
|
|
// updateOrigin over (lastPerformance - lastUpdate). When the
|
|
// next one lands the basis is replaced, so the drawn position
|
|
// jumps by however far the prediction had drifted. Render
|
|
// interpolation cannot hide that: it smooths within a step,
|
|
// and this is a discontinuity in the stepped values
|
|
// themselves. If the interval is a few per second and the
|
|
// correction is tens of centimetres, that is a tick a few
|
|
// times a second - which is the symptom being chased.
|
|
//
|
|
// Reported per entity, on a five second clock, so a busy race
|
|
// does not bury the log.
|
|
//
|
|
//
|
|
// Race totals for the RP412NETLOG summary - per entity,
|
|
// ungated (one subtract and a length per arriving update),
|
|
// where the CamLog block below is windowed, gated, and
|
|
// shared across all replicants.
|
|
//
|
|
if (GetInstance() == ReplicantInstance)
|
|
{
|
|
Vector3D race_drift;
|
|
race_drift.Subtract(
|
|
update->localOrigin.linearPosition,
|
|
localOrigin.linearPosition
|
|
);
|
|
Scalar race_distance = race_drift.Length();
|
|
netRaceStats.correctionCount++;
|
|
netRaceStats.correctionTotal += race_distance;
|
|
if (race_distance > netRaceStats.correctionWorst)
|
|
{
|
|
netRaceStats.correctionWorst = race_distance;
|
|
}
|
|
}
|
|
|
|
if (RPCameraLog() && GetInstance() == ReplicantInstance)
|
|
{
|
|
static Scalar next_say = 0.0f;
|
|
static int corrections = 0;
|
|
static Scalar worst = 0.0f;
|
|
static Scalar total = 0.0f;
|
|
|
|
Vector3D drift;
|
|
drift.Subtract(
|
|
update->localOrigin.linearPosition,
|
|
localOrigin.linearPosition
|
|
);
|
|
Scalar distance = drift.Length();
|
|
++corrections;
|
|
total += distance;
|
|
if (distance > worst)
|
|
{
|
|
worst = distance;
|
|
}
|
|
|
|
if ((Scalar) Now() >= next_say)
|
|
{
|
|
if (next_say > 0.0f && corrections > 0)
|
|
{
|
|
DEBUG_STREAM << "CamLog: replicant corrections - "
|
|
<< corrections << " in 5s ("
|
|
<< (corrections / 5) << "/s), mean "
|
|
<< (total / corrections) << "m, worst "
|
|
<< worst << "m\n" << std::flush;
|
|
}
|
|
next_say = ((Scalar) Now()) + 5.0f;
|
|
corrections = 0;
|
|
worst = 0.0f;
|
|
total = 0.0f;
|
|
}
|
|
}
|
|
|
|
updateOrigin = update->localOrigin;
|
|
|
|
//
|
|
//------------------------------------------------------------------
|
|
// If we aren't able to run yet, we must update local origin as well
|
|
//------------------------------------------------------------------
|
|
//
|
|
Check(application);
|
|
int app_state = application->GetApplicationState();
|
|
if (
|
|
app_state < Application::RunningMission
|
|
|| app_state == Application::CreatingMission
|
|
)
|
|
{
|
|
localOrigin = updateOrigin;
|
|
localToWorld = localOrigin;
|
|
|
|
//
|
|
// Set outside the step loop, so renderPreviousOrigin now
|
|
// describes a step that never happened. Draw plainly until
|
|
// a real one does.
|
|
//
|
|
renderStepTaken = False;
|
|
}
|
|
Simulation::ReadUpdateRecord(record);
|
|
}
|
|
break;
|
|
case DamageZoneUpdateModelBit:
|
|
ReadDamageUpdateRecord(record);
|
|
break;
|
|
default:
|
|
Simulation::ReadUpdateRecord(record);
|
|
}
|
|
|
|
|
|
|
|
Check_Fpu();
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::ReadDamageUpdateRecord(Simulation::UpdateRecord *update_record)
|
|
{
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// Step through each block until there are no more remaining, and send the
|
|
// update out the the damageZone indicated by the damageZoneIndex
|
|
//------------------------------------------------------------------------
|
|
//
|
|
MemoryStream stream(
|
|
update_record,
|
|
update_record->recordLength,
|
|
sizeof(*update_record)
|
|
);
|
|
|
|
while (stream.GetBytesRemaining())
|
|
{
|
|
DamageZone::UpdateRecord *damage_zone_record =
|
|
(DamageZone::UpdateRecord*)stream.GetPointer();
|
|
int damage_zone_index = damage_zone_record->damageZoneIndex;
|
|
damageZones[damage_zone_index]->ReadUpdateRecord(damage_zone_record);
|
|
stream.AdvancePointer(damage_zone_record->recordLength);
|
|
}
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
#define MAX_UPDATE_LENGTH 1400
|
|
|
|
static long Update_Buffer[MAX_UPDATE_LENGTH >> 2];
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::WriteDamageUpdateRecord(Simulation::UpdateRecord *update_record)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(update_record);
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Make a memory stream this size of a network packet
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
MemoryStream stream(
|
|
update_record,
|
|
sizeof(Update_Buffer) - ((char*)update_record - (char*)Update_Buffer),
|
|
sizeof(*update_record)
|
|
);
|
|
DamageZone::UpdateRecord *record_start =
|
|
(DamageZone::UpdateRecord*)stream.GetPointer();
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Update Every DamageZone For NOW
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
for(int ii=0;ii<damageZoneCount;++ii)
|
|
{
|
|
DamageZone::UpdateRecord *damage_zone_update =
|
|
(DamageZone::UpdateRecord*)stream.GetPointer();
|
|
damageZones[ii]->WriteUpdateRecord(damage_zone_update);
|
|
stream.AdvancePointer(damage_zone_update->recordLength);
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------
|
|
// Check to see if an update message is necessary
|
|
//-----------------------------------------------
|
|
//
|
|
if (stream.GetPointer() != record_start)
|
|
{
|
|
Simulation::UpdateRecord *damage_update =
|
|
(Simulation::UpdateRecord*)update_record;
|
|
damage_update->recordLength =
|
|
(char*)stream.GetPointer() - (char*)update_record;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Entity::UpdateMessage*
|
|
Entity::Execute(const Time& till)
|
|
{
|
|
SET_EXECUTE_ENTITY();
|
|
Check(this);
|
|
Check(&till);
|
|
|
|
MemoryStream
|
|
stream(Update_Buffer, sizeof(Update_Buffer), sizeof(UpdateMessage));
|
|
char *record_start = (char*)stream.GetPointer();
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Check to see if the application will let us run. If We are starting a
|
|
// mission, only the player class objects can execute
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
Check(application);
|
|
if (
|
|
application->GetApplicationState() == Application::RunningMission
|
|
|| application->GetApplicationState() == Application::EndingMission
|
|
|| IsPreRunnable()
|
|
)
|
|
{
|
|
PerformAndWatch(till, &stream);
|
|
}
|
|
else
|
|
{
|
|
WriteSimulationUpdate(&stream);
|
|
}
|
|
|
|
#if defined(USE_FPU_ANALYSIS)
|
|
if (!Fpu_Ok())
|
|
{
|
|
Registry *registry = application->GetRegistry();
|
|
Receiver::SharedData *shared_data =
|
|
registry->GetStaticData(GetClassID());
|
|
Derivation *derivation = shared_data->derivedClasses;
|
|
DEBUG_STREAM << derivation->className << ',' << GetInstance() << endl << std::flush;
|
|
Fail("Simulation corrupted FPU!");
|
|
}
|
|
#endif
|
|
|
|
//
|
|
//-----------------------------------------------
|
|
// Check to see if an update message is necessary
|
|
//-----------------------------------------------
|
|
//
|
|
if (stream.GetPointer() != record_start)
|
|
{
|
|
UpdateMessage *update = (UpdateMessage*)Update_Buffer;
|
|
update->messageID = UpdateMessageID;
|
|
update->messageLength = (char*)stream.GetPointer() - (char*)Update_Buffer;
|
|
update->messageFlags = 0;
|
|
CLEAR_EXECUTE_ENTITY();
|
|
return update;
|
|
}
|
|
|
|
//
|
|
//-------------------------------------
|
|
// Otherwise, return NULL for no update
|
|
//-------------------------------------
|
|
//
|
|
Check_Fpu();
|
|
CLEAR_EXECUTE_ENTITY();
|
|
return NULL;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Simulation*
|
|
Entity::GetSimulation(int index)
|
|
{
|
|
Check(this);
|
|
|
|
if (index == EntitySubsystemID)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
Check_Fpu();
|
|
if ((unsigned)index < subsystemCount)
|
|
{
|
|
return subsystemArray[index];
|
|
}
|
|
else
|
|
{
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Subsystem*
|
|
Entity::FindSubsystem(const char* name)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(name);
|
|
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i])
|
|
{
|
|
Check(subsystemArray[i]);
|
|
if (!stricmp(subsystemArray[i]->GetName(), name))
|
|
{
|
|
return subsystemArray[i];
|
|
}
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
return NULL;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
#define MAX_SUB_FILE_NAME (256)
|
|
|
|
int
|
|
Entity::FindSubsytemID
|
|
(
|
|
#if DEBUG_LEVEL>0
|
|
const char *model_name,
|
|
const char *subsystem_name
|
|
#else
|
|
const char *,
|
|
const char *subsystem_name
|
|
#endif
|
|
)
|
|
{
|
|
Check_Pointer(model_name);
|
|
Check_Pointer(subsystem_name);
|
|
|
|
int subsytem_id = EntitySubsystemID;
|
|
int sub_count = 0;
|
|
|
|
CString sub_file_string;
|
|
CString sub_directory_string("models\\");
|
|
CString model_name_string("model_name");
|
|
CString sub_post_string(".sub");
|
|
|
|
sub_file_string =
|
|
sub_directory_string + model_name_string + sub_post_string;
|
|
|
|
NotationFile sub_file(sub_file_string);
|
|
Check(&sub_file);
|
|
|
|
NameList *page_list = sub_file.MakePageList();
|
|
Register_Object(page_list);
|
|
NameList::Entry *entry=page_list->GetFirstEntry();
|
|
while (entry != NULL)
|
|
{
|
|
Check(entry);
|
|
Check_Pointer(entry->GetName());
|
|
if (!stricmp(subsystem_name, entry->GetName()))
|
|
{
|
|
subsytem_id = sub_count;
|
|
break;
|
|
}
|
|
|
|
++sub_count;
|
|
entry = entry->GetNextEntry();
|
|
}
|
|
|
|
Unregister_Object(page_list);
|
|
delete page_list;
|
|
|
|
return subsytem_id;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::PerformAndWatch(
|
|
const Time &till,
|
|
MemoryStream *update_stream
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(&till);
|
|
|
|
//
|
|
//-----------------------------------------
|
|
// Execute all the subsystems, then ourself
|
|
//-----------------------------------------
|
|
//
|
|
SET_PERFORM_SUBSYSTEMS();
|
|
if (subsystemArray)
|
|
{
|
|
Check_Pointer(subsystemArray);
|
|
|
|
//
|
|
//---------------------------------------------------
|
|
// If this is not a replicant, execute all subsystems
|
|
//---------------------------------------------------
|
|
//
|
|
if (GetInstance() != ReplicantInstance)
|
|
{
|
|
//
|
|
//----------------------------------------------------------------
|
|
// Fixed-step: the subsystems and the entity advance TOGETHER,
|
|
// one step at a time, because they read each other mid-flight.
|
|
// The VTV's hover spring is computed from its thrusters'
|
|
// measured heights, and each thruster measures from where the
|
|
// vehicle IS - so thrusters stepped twice against a vehicle
|
|
// that has not moved yet hand back two identical height
|
|
// samples, and the spring fires twice on stale data. Measured,
|
|
// that pod climbs at 30 fps and flies level at 144.
|
|
//
|
|
// So the step loop lives HERE, above both: everyone is walked
|
|
// to the same sub-frame instant before anyone takes the next
|
|
// step. Watchers and the update stream still run once per
|
|
// frame, after the loop - stepping is physics, watching is
|
|
// I/O, and only the first belongs inside.
|
|
//
|
|
// The interleave keys off the ENTITY's own clock so a
|
|
// subsystem created mid-flight (they are made alongside their
|
|
// owner) can never wedge the loop.
|
|
//----------------------------------------------------------------
|
|
//
|
|
Scalar fixed_step = Simulation::FixedStep();
|
|
|
|
if (fixed_step > (Scalar) 0)
|
|
{
|
|
//
|
|
// One grid for the whole vehicle. Every Simulation anchors
|
|
// its own lastPerformance at its creation time, so an
|
|
// entity and its subsystems were stepping on grids offset
|
|
// by a random fraction of a step - deterministic within a
|
|
// run, DIFFERENT between runs, because creation times ride
|
|
// on load timing. The thrusters' measurements then landed
|
|
// a different sub-step distance from the vehicle's
|
|
// integration every launch, which is physics drift no seed
|
|
// can pin. Snap the subsystems onto the entity's grid; the
|
|
// interleave below then keeps everyone in lockstep by
|
|
// construction, and once aligned this assignment is a
|
|
// no-op every frame after.
|
|
//
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i] &&
|
|
subsystemArray[i]->IsNonReplicantExecutable())
|
|
{
|
|
subsystemArray[i]->SetLastPerformance(
|
|
GetLastPerformance());
|
|
}
|
|
}
|
|
|
|
Time step_till = GetLastPerformance();
|
|
step_till += fixed_step;
|
|
|
|
while (step_till <= till)
|
|
{
|
|
//
|
|
// BeginStep on the ENTITY comes before the subsystems
|
|
// perform: the Mover's force accumulator is cleared
|
|
// here, and the thrusters then ADD this step's forces
|
|
// into a clean slate. The first version left that
|
|
// clear on the per-frame path, so a two-step frame
|
|
// integrated step one's thrust twice - and since how
|
|
// many steps land in a frame rides on wall-clock
|
|
// jitter, no two runs saw the same force history.
|
|
// Identical configs measured 0.23 apart because of it.
|
|
//
|
|
BeginStep();
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i] &&
|
|
subsystemArray[i]->IsNonReplicantExecutable())
|
|
{
|
|
subsystemArray[i]->BeginStep();
|
|
subsystemArray[i]->PerformTo(step_till);
|
|
}
|
|
}
|
|
Simulation::PerformTo(step_till);
|
|
step_till += fixed_step;
|
|
}
|
|
|
|
//
|
|
// How far past the last completed step the frame we are
|
|
// about to draw falls, as a fraction of one step. This is
|
|
// the leftover the fixed-step loop deliberately does not
|
|
// simulate - see renderPreviousOrigin in entity.h.
|
|
//
|
|
{
|
|
Scalar leftover = till - GetLastPerformance();
|
|
Scalar fraction = (fixed_step > (Scalar) 0)
|
|
? (leftover / fixed_step) : (Scalar) 0;
|
|
if (fraction < (Scalar) 0) fraction = (Scalar) 0;
|
|
if (fraction > (Scalar) 1) fraction = (Scalar) 1;
|
|
renderStepFraction = fraction;
|
|
}
|
|
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i] &&
|
|
subsystemArray[i]->IsNonReplicantExecutable())
|
|
{
|
|
subsystemArray[i]->WatchAndWrite(update_stream);
|
|
}
|
|
}
|
|
|
|
SET_PERFORM_ENTITY();
|
|
Simulation::WatchAndWrite(update_stream);
|
|
Check_Fpu();
|
|
CLEAR_PERFORM_ENTITY();
|
|
CLEAR_PERFORM_SUBSYSTEMS();
|
|
return;
|
|
}
|
|
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i])
|
|
{
|
|
Subsystem *subsystem = subsystemArray[i];
|
|
Check(subsystem);
|
|
if (subsystem->IsNonReplicantExecutable())
|
|
{
|
|
subsystem->PerformAndWatch(till, update_stream);
|
|
#if defined(USE_FPU_ANALYSIS)
|
|
if (!Fpu_Ok())
|
|
{
|
|
Registry *registry = application->GetRegistry();
|
|
Receiver::SharedData *shared_data =
|
|
registry->GetStaticData(GetClassID());
|
|
Derivation *derivation = shared_data->derivedClasses;
|
|
DEBUG_STREAM << derivation->className
|
|
<< "\nExecuting subsystem[" << i << "], "
|
|
<< subsystem->GetName() << endl;
|
|
Fail("Simulation corrupted FPU!");
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------------------------
|
|
// If this is a replicant, execute only those subsystems which need to
|
|
//--------------------------------------------------------------------
|
|
//
|
|
else
|
|
{
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i])
|
|
{
|
|
Subsystem *subsystem = subsystemArray[i];
|
|
Check(subsystem);
|
|
if (subsystem->IsReplicantExecutable())
|
|
{
|
|
subsystem->PerformAndWatch(till, update_stream);
|
|
#if defined(USE_FPU_ANALYSIS)
|
|
if (!Fpu_Ok())
|
|
{
|
|
Registry *registry = application->GetRegistry();
|
|
Receiver::SharedData *shared_data =
|
|
registry->GetStaticData(GetClassID());
|
|
Derivation *derivation = shared_data->derivedClasses;
|
|
DEBUG_STREAM << derivation->className
|
|
<< " replicant\nExecuting subsystem[" << i << "], "
|
|
<< subsystem->GetName() << endl;
|
|
Fail("Simulation corrupted FPU!");
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CLEAR_PERFORM_SUBSYSTEMS();
|
|
|
|
SET_PERFORM_ENTITY();
|
|
Simulation::PerformAndWatch(till, update_stream);
|
|
Check_Fpu();
|
|
CLEAR_PERFORM_ENTITY();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::PlayerLinkMessageHandler(PlayerLinkMessage *message)
|
|
{
|
|
Check(application);
|
|
HostManager *host = application->GetHostManager();
|
|
Check(host);
|
|
playerLink =(Player*) host->GetEntityPointer(message->playerID);
|
|
}
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::TakeDamageStreamMessageHandler(TakeDamageStreamMessage *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
//
|
|
//-------------------------------------------------
|
|
// Extract the common information out of the stream
|
|
//-------------------------------------------------
|
|
//
|
|
Damage damage_data;
|
|
EntityID shooting_entity = message->shootingEntity;
|
|
int damage_zone_index = message->damageZoneIndex;
|
|
damage_data.impactPoint = message->impactPoint;
|
|
|
|
//
|
|
//---------------------------------------------------
|
|
// Figure out how many damage messages there are here
|
|
//---------------------------------------------------
|
|
//
|
|
DynamicMessage damage_stream(
|
|
message,
|
|
message->messageLength,
|
|
sizeof(*message)
|
|
);
|
|
int entry_count;
|
|
damage_stream >> entry_count;
|
|
|
|
for (int i=0; i<entry_count; ++i)
|
|
{
|
|
//
|
|
//-------------------------------------------------
|
|
// Read in damageType, damageAmount and subsystemID
|
|
//-------------------------------------------------
|
|
//
|
|
damage_stream >> damage_data.damageType >> damage_data.damageAmount;
|
|
int subsystem;
|
|
damage_stream >> subsystem;
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Apply the damage to the appropriate damageZone
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
TakeDamageMessage
|
|
message(
|
|
TakeDamageMessageID,
|
|
sizeof(TakeDamageMessage),
|
|
shooting_entity,
|
|
damage_zone_index,
|
|
damage_data,
|
|
subsystem
|
|
);
|
|
Dispatch(&message);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::TakeDamageMessageHandler(TakeDamageMessage *message)
|
|
{
|
|
//
|
|
//-------------------------------------
|
|
// YOU MUST CHECK FOR -1 DamageZone!!!!
|
|
//-------------------------------------
|
|
//
|
|
if (!damageZones || message->damageZone == -1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// If we have no damage zones allocated, we ignore any damage sent to us
|
|
//----------------------------------------------------------------------
|
|
//
|
|
Verify(message->damageZone < damageZoneCount);
|
|
Verify(message->damageZone >= 0 );
|
|
Check_Pointer(damageZones);
|
|
Check(damageZones[message->damageZone]);
|
|
damageZones[message->damageZone]->TakeDamage(message->damageData);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Entity::Entity(
|
|
Entity::MakeMessage *creation_message,
|
|
Entity::SharedData &virtual_data
|
|
):
|
|
Simulation(creation_message->classToCreate,virtual_data),
|
|
staticVideoSocket(NULL),
|
|
dynamicVideoSocket(NULL),
|
|
audioSocket(NULL)
|
|
{
|
|
Check_Pointer(this);
|
|
Check(creation_message);
|
|
|
|
//
|
|
// Get host manager pointer
|
|
//
|
|
HostManager *host_manager;
|
|
|
|
Check(application);
|
|
host_manager = application->GetHostManager();
|
|
Check(host_manager);
|
|
|
|
//
|
|
//-----------------------------
|
|
// Initialize the entity fields
|
|
//-----------------------------
|
|
//
|
|
playerLink = NULL;
|
|
resourceID = creation_message->resourceID;
|
|
owningPlayer = (Player*)host_manager->GetEntityPointer(
|
|
creation_message->owningPlayerID
|
|
);
|
|
simulationFlags = creation_message->instanceFlags;
|
|
entityID = creation_message->entityID;
|
|
ownerID = creation_message->ownerID;
|
|
creationTime = Now();
|
|
interestZoneID = creation_message->interestZoneID;
|
|
interestCount = 0;
|
|
|
|
// initialize attributes
|
|
localOrigin = creation_message->localOrigin;
|
|
updateOrigin = localOrigin;
|
|
localToWorld = localOrigin;
|
|
|
|
//
|
|
// Render interpolation starts with nothing to blend: the previous
|
|
// origin IS the current one and no fraction of a step is outstanding,
|
|
// so GetRenderToWorld hands back localToWorld until the first step has
|
|
// actually been taken.
|
|
//
|
|
renderPreviousOrigin = localOrigin;
|
|
renderStepFraction = (Scalar) 0;
|
|
renderStepTaken = False;
|
|
|
|
// initialize camera stuff
|
|
cameraOffset = Origin::Identity;
|
|
|
|
SetInvalidFlag();
|
|
|
|
// initialize model stuff
|
|
lastPerformance = creationTime;
|
|
subsystemCount = 0;
|
|
subsystemArray = NULL;
|
|
damageZones = NULL;
|
|
damageZoneCount = 0;
|
|
|
|
//
|
|
//--------------------------------
|
|
// Allocate the damageZones array
|
|
//--------------------------------
|
|
//
|
|
|
|
//
|
|
// Read In DamageZones and Create DamageZoneID's
|
|
//
|
|
if (resourceID != ResourceDescription::NullResourceID)
|
|
{
|
|
ResourceDescription *dmg_res =
|
|
application->GetResourceFile()->SearchList(
|
|
resourceID,
|
|
ResourceDescription::DamageZoneStreamResourceType
|
|
);
|
|
|
|
//
|
|
//--------------------------------------------
|
|
// Allocate damageZones for this entity only
|
|
// if they were defined in the resource
|
|
//--------------------------------------------
|
|
//
|
|
if (dmg_res)
|
|
{
|
|
dmg_res->Lock();
|
|
DynamicMemoryStream damage_zone_stream(
|
|
dmg_res->resourceAddress,
|
|
dmg_res->resourceSize);
|
|
|
|
damage_zone_stream >> damageZoneCount;
|
|
damageZones = new (::DamageZone(*[damageZoneCount]));
|
|
Register_Pointer(damageZones);
|
|
dmg_res->Unlock();
|
|
}
|
|
}
|
|
|
|
//
|
|
//---------------------------------------------------------
|
|
// Create a new entity ID and assign ownership to this host
|
|
// If (this is a master entity) or
|
|
// (this is a dynamic hermit) or
|
|
// (this is the initial independent)
|
|
//---------------------------------------------------------
|
|
//
|
|
if (
|
|
(GetInstance() == MasterInstance) ||
|
|
(GetInstance() == HermitInstance && IsDynamic()) ||
|
|
(
|
|
(GetInstance() == IndependantInstance) &&
|
|
(creation_message->ownerID == NullHostID ||
|
|
creation_message->ownerID == MapHostID)
|
|
)
|
|
)
|
|
{
|
|
entityID = host_manager->MakeUniqueEntityID();
|
|
ownerID = host_manager->GetLocalHostID();
|
|
creation_message->entityID = entityID;
|
|
creation_message->ownerID = ownerID;
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------
|
|
// Tell the host manager about this new entity
|
|
//--------------------------------------------
|
|
//
|
|
Check(host_manager);
|
|
host_manager->NotifyOfEntityCreation(this, creation_message);
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Entity*
|
|
Entity::Make(Entity::MakeMessage *creation_message)
|
|
{
|
|
return new Entity(creation_message,DefaultData);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
Entity::CreateMakeMessage(
|
|
MakeMessage *creation_message,
|
|
#if DEBUG_LEVEL>0
|
|
NotationFile *model_file,
|
|
#else
|
|
NotationFile *,
|
|
#endif
|
|
const ResourceDirectories *
|
|
)
|
|
{
|
|
Check(creation_message);
|
|
Check(model_file);
|
|
char *p;
|
|
|
|
creation_message->messageLength = sizeof(Entity::MakeMessage);
|
|
creation_message->messageID = Entity::MakeMessageID;
|
|
creation_message->owningPlayerID = EntityID::Null;
|
|
creation_message->messageFlags = 0;
|
|
creation_message->classToCreate = RegisteredClass::TrivialEntityClassID;
|
|
creation_message->instanceFlags = 0; //DefaultFlags;
|
|
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "X position coordinate missing!\n";
|
|
return False;
|
|
}
|
|
creation_message->localOrigin.linearPosition.x = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Y position coordinate missing!\n";
|
|
return False;
|
|
}
|
|
creation_message->localOrigin.linearPosition.y = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Z position coordinate missing!\n";
|
|
return False;
|
|
}
|
|
creation_message->localOrigin.linearPosition.z = atof(p);
|
|
|
|
EulerAngles pyr;
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Pitch missing!\n";
|
|
return False;
|
|
}
|
|
pyr.pitch = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Yaw missing!\n";
|
|
return False;
|
|
}
|
|
pyr.yaw = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Roll missing!\n";
|
|
return False;
|
|
}
|
|
pyr.roll = atof(p);
|
|
creation_message->localOrigin.angularPosition = pyr;
|
|
return True;
|
|
}
|
|
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
Entity::CreateMakeMapMessage(
|
|
MakeMapMessage *creation_message,
|
|
ResourceDescription::ResourceID resource_id,
|
|
#if DEBUG_LEVEL>0
|
|
NotationFile *model_file,
|
|
#else
|
|
NotationFile *,
|
|
#endif
|
|
const ResourceDirectories *
|
|
)
|
|
{
|
|
Check(creation_message);
|
|
Check(model_file);
|
|
char *p;
|
|
|
|
creation_message->messageLength = sizeof(Entity::MakeMapMessage);
|
|
creation_message->messageID = Entity::MakeMessageID;
|
|
creation_message->messageFlags = Entity::MakeMessage::MapStreamMarkerFlag;
|
|
|
|
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "X position coordinate missing!\n";
|
|
return False;
|
|
}
|
|
creation_message->localOrigin.linearPosition.x = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Y position coordinate missing!\n";
|
|
return False;
|
|
}
|
|
creation_message->localOrigin.linearPosition.y = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Z position coordinate missing!\n";
|
|
return False;
|
|
}
|
|
creation_message->localOrigin.linearPosition.z = atof(p);
|
|
|
|
EulerAngles pyr = EulerAngles::Identity;
|
|
#if 0
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Pitch missing!\n";
|
|
return False;
|
|
}
|
|
pyr.pitch = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Yaw missing!\n";
|
|
return False;
|
|
}
|
|
pyr.yaw = atof(p);
|
|
if ((p = strtok(NULL, " ")) == NULL)
|
|
{
|
|
std::cerr << "Roll missing!\n";
|
|
return False;
|
|
}
|
|
pyr.roll = atof(p);
|
|
#endif
|
|
|
|
creation_message->localOrigin.angularPosition = pyr;
|
|
creation_message->resourceID = resource_id;
|
|
|
|
return True;
|
|
|
|
}
|
|
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::DestroyEntityMessageHandler(Message *)
|
|
{
|
|
CondemnToDeathRow();
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Entity::~Entity()
|
|
{
|
|
//
|
|
// Notify the host manager that we are going away
|
|
//
|
|
Check(application);
|
|
application->GetHostManager()->NotifyOfEntityDestruction(this);
|
|
|
|
//
|
|
// Delete the subsystem array
|
|
//
|
|
if (subsystemArray)
|
|
{
|
|
Check_Pointer(subsystemArray);
|
|
for (int i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i])
|
|
{
|
|
Unregister_Object(subsystemArray[i]);
|
|
delete subsystemArray[i];
|
|
}
|
|
}
|
|
Unregister_Pointer(subsystemArray);
|
|
delete subsystemArray;
|
|
}
|
|
if (damageZones)
|
|
{
|
|
Check_Pointer(damageZones);
|
|
for (int i=0; i<damageZoneCount; ++i)
|
|
{
|
|
if (damageZones[i])
|
|
{
|
|
Unregister_Object(damageZones[i]);
|
|
delete damageZones[i];
|
|
}
|
|
}
|
|
Unregister_Pointer(damageZones);
|
|
delete damageZones;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Entity::CondemnToDeathRow()
|
|
{
|
|
Check(this);
|
|
Check(application);
|
|
EntityManager *entity_manager = application->GetEntityManager();
|
|
Check(entity_manager);
|
|
|
|
//
|
|
//-------------------------------------------------
|
|
// Make sure we are not entered into deathrow twice
|
|
//-------------------------------------------------
|
|
//
|
|
if (!IsCondemned())
|
|
{
|
|
entity_manager->deathRow->Add(this);
|
|
SetCondemnedFlag();
|
|
NeverExecute();
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Test Support
|
|
//
|
|
|
|
Logical
|
|
Entity::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|
|
|
|
//##########################################################################
|
|
// Render interpolation - see entity.h for why, and for why it is render
|
|
// only. RP412INTERP=0 turns it off so the stepping it removes can be seen
|
|
// again on a test machine without a rebuild.
|
|
//##########################################################################
|
|
//
|
|
static Logical
|
|
RenderInterpolationEnabled()
|
|
{
|
|
static int enabled = -1;
|
|
|
|
if (enabled < 0)
|
|
{
|
|
const char *setting = getenv("RP412INTERP");
|
|
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
|
if (!enabled)
|
|
{
|
|
DEBUG_STREAM << "Render: interpolation off (RP412INTERP=0) - "
|
|
<< "drawn motion steps at the physics rate\n" << std::flush;
|
|
}
|
|
}
|
|
return enabled ? True : False;
|
|
}
|
|
|
|
void
|
|
Entity::SnapshotRenderOrigin()
|
|
{
|
|
Check(this);
|
|
renderPreviousOrigin = localOrigin;
|
|
renderStepTaken = True;
|
|
}
|
|
|
|
void
|
|
Entity::SetRenderStepFraction(Scalar fraction)
|
|
{
|
|
Check(this);
|
|
renderStepFraction = fraction;
|
|
}
|
|
|
|
void
|
|
Entity::GetRenderToWorld(LinearMatrix *out)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(out);
|
|
|
|
//
|
|
// No fixed step at all, interpolation switched off, or this entity has
|
|
// not taken a step yet. Hand back exactly what every caller used before
|
|
// this existed.
|
|
//
|
|
// The test used to be on the fraction rather than on renderStepTaken,
|
|
// and that was wrong in a way that showed. Drawing at fraction f means
|
|
// drawing at the start of the step plus f of it, so f = 0 means the
|
|
// START of the step - while this early return hands back localToWorld,
|
|
// which is its END. The two are a whole step apart, about a metre at
|
|
// racing speed.
|
|
//
|
|
// behind is a whole number of milliseconds against a 20ms step, so it
|
|
// lands on exactly zero about one frame in twenty. On those frames a
|
|
// pod was drawn a full step ahead of itself and then snapped back on
|
|
// the next one: a jump three times a second at 59fps, regular because
|
|
// the beat between frame rate and step rate is regular, and worst when
|
|
// a pod crosses the view quickly. It never appeared in any simulation
|
|
// trace because the simulation was right - only the drawing was wrong.
|
|
//
|
|
// Interpolating at f = 0 is continuous with everything either side of
|
|
// it. Each frame advances the drawn position by frame_time / step
|
|
// whether or not a step boundary falls between the two, which is the
|
|
// entire point of doing it.
|
|
//
|
|
if (!RenderInterpolationEnabled() || !renderStepTaken)
|
|
{
|
|
*out = localToWorld;
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Origin::Lerp does the position and the shortest-arc quaternion, and
|
|
// normalises - which over a 20 ms step is indistinguishable from a
|
|
// true slerp and a good deal cheaper.
|
|
//
|
|
Origin blended;
|
|
blended.Lerp(renderPreviousOrigin, localOrigin, renderStepFraction);
|
|
*out = blended;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//##########################################################################
|
|
// Renderer Support
|
|
//
|
|
void
|
|
Entity::AddStaticVideoComponent(Component *component)
|
|
{
|
|
Check(component);
|
|
Check(&staticVideoSocket);
|
|
staticVideoSocket.Add(component);
|
|
Check_Fpu();
|
|
}
|
|
|
|
void
|
|
Entity::AddDynamicVideoComponent(Component *component)
|
|
{
|
|
Check(component);
|
|
Check(&dynamicVideoSocket);
|
|
dynamicVideoSocket.Add(component);
|
|
Check_Fpu();
|
|
}
|
|
|
|
void
|
|
Entity::AddAudioComponent(Component *component)
|
|
{
|
|
Check(component);
|
|
Check(&audioSocket);
|
|
audioSocket.Add(component);
|
|
Check_Fpu();
|
|
}
|
|
|
|
Enumeration
|
|
Entity::GetAudioRepresentation(Entity *linked_entity)
|
|
{
|
|
Check(this);
|
|
Check(linked_entity);
|
|
|
|
return
|
|
(linked_entity == this) ?
|
|
InternalAudioRepresentation : ExternalAudioRepresentation;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Interest Support
|
|
//
|
|
void
|
|
Entity::BecomeInteresting()
|
|
{
|
|
BecomeInterestingMessage
|
|
message(
|
|
Entity::BecomeInterestingMessageID,
|
|
sizeof(BecomeInterestingMessage)
|
|
);
|
|
|
|
Receiver::Dispatch(&message);
|
|
Check_Fpu();
|
|
}
|
|
|
|
void Entity::BecomeInterestingMessageHandler(const Receiver::Message*)
|
|
{
|
|
++interestCount;
|
|
Check_Fpu();
|
|
}
|
|
|
|
void
|
|
Entity::BecomeUninterestingMessageHandler()
|
|
{
|
|
--interestCount;
|
|
Check_Fpu();
|
|
}
|
|
|
|
Enumeration
|
|
Entity::GetInterestPriority(Entity *linked_entity)
|
|
{
|
|
Check(this);
|
|
Check(linked_entity);
|
|
|
|
return (linked_entity == this) ? HighInterestPriority : LowInterestPriority;
|
|
}
|
|
|
|
|
|
|
|
int
|
|
Entity::GetDamageZoneIndex(const CString &damage_zone_name) const
|
|
{
|
|
|
|
for(int ii=0;ii<damageZoneCount;++ii)
|
|
{
|
|
if(damageZones[ii]->damageZoneName.Compare(damage_zone_name) == 0)
|
|
{
|
|
return ii;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
int
|
|
Entity::CreateDamageZoneStream(
|
|
ResourceFile *resource_file,
|
|
const char *model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
)
|
|
{
|
|
Check(resource_file);
|
|
Check_Pointer(model_name);
|
|
Check(model_file);
|
|
Check_Pointer(directories);
|
|
|
|
//
|
|
//-------------------
|
|
// Find the .dmg file
|
|
//-------------------
|
|
//
|
|
const char* dmg_file_cname;
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"DamageZones",
|
|
&dmg_file_cname
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " is missing .dmg file specification!\n";
|
|
return -1;
|
|
}
|
|
|
|
CString dmg_filename =
|
|
MakePathedFilename(directories->modelDirectory, dmg_file_cname);
|
|
|
|
NotationFile *dmg_file = new NotationFile(dmg_filename);
|
|
Register_Object(dmg_file);
|
|
int page_count = dmg_file->PageCount();
|
|
if (!page_count)
|
|
{
|
|
std::cerr << dmg_filename << " is empty or missing!\n";
|
|
Unregister_Object(dmg_file);
|
|
delete dmg_file;
|
|
return -1;
|
|
}
|
|
|
|
DynamicMemoryStream damage_stream;
|
|
|
|
damage_stream << page_count;
|
|
NameList *dmg_namelist = dmg_file->MakePageList();
|
|
Register_Object(dmg_namelist);
|
|
|
|
NameList::Entry *dmg_entry = dmg_namelist->GetFirstEntry();
|
|
|
|
CString dmg_page_name;
|
|
while(dmg_entry)
|
|
{
|
|
dmg_page_name = dmg_entry->GetName();
|
|
DamageZone::CreateStreamedDamageZone(
|
|
model_file,
|
|
model_name,
|
|
NULL,
|
|
dmg_page_name,
|
|
&damage_stream,
|
|
dmg_file,
|
|
directories
|
|
);
|
|
dmg_entry = dmg_entry->GetNextEntry();
|
|
}
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Add Stream to Resource
|
|
//~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
ResourceDescription *new_res =
|
|
resource_file->AddResourceMemoryStream(
|
|
model_name,
|
|
ResourceDescription::DamageZoneStreamResourceType,
|
|
1,
|
|
ResourceDescription::Preload,
|
|
&damage_stream
|
|
);
|
|
Check(new_res);
|
|
|
|
Unregister_Object(dmg_file);
|
|
delete dmg_file;
|
|
Unregister_Object(dmg_namelist);
|
|
delete dmg_namelist;
|
|
|
|
return new_res->resourceID;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
ResourceDescription::ResourceID
|
|
Entity::CreateExplosionTableStream(
|
|
ResourceFile *resource_file,
|
|
const char *model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
)
|
|
{
|
|
Check(resource_file);
|
|
Check_Pointer(model_name);
|
|
Check(model_file);
|
|
Check_Pointer(directories);
|
|
//
|
|
//~~~~~~~~~~~~~~~~~
|
|
// Find *.dmg file
|
|
//~~~~~~~~~~~~~~~~~
|
|
//
|
|
const char *entry_data;
|
|
if (!model_file->GetEntry(
|
|
"gamedata",
|
|
"DamageZones",
|
|
&entry_data
|
|
)
|
|
)
|
|
{
|
|
DEBUG_STREAM << model_name << " is missing .dmg file specification!\n" << std::flush;
|
|
return -1;
|
|
}
|
|
CString filename =
|
|
MakePathedFilename(directories->modelDirectory, entry_data);
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Make a notation file of the DMG Entries
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
NotationFile *dmg_file = new NotationFile(filename);
|
|
Register_Object(dmg_file);
|
|
|
|
if(dmg_file->IsEmpty())
|
|
{
|
|
DEBUG_STREAM << model_name << " *.dmg file is empty or missing!\n" << std::flush;
|
|
Unregister_Object(dmg_file);
|
|
delete dmg_file;
|
|
return -1;
|
|
}
|
|
|
|
DynamicMemoryStream explosion_stream;
|
|
NameList *dmg_namelist = dmg_file->MakePageList();
|
|
Register_Object(dmg_namelist);
|
|
|
|
NameList::Entry *dmg_entry = dmg_namelist->GetFirstEntry();
|
|
while(dmg_entry)
|
|
{
|
|
const char *entry_data;
|
|
if (dmg_file->GetEntry(
|
|
dmg_entry->GetName(),
|
|
"ExplosionTable",
|
|
&entry_data
|
|
)
|
|
)
|
|
{
|
|
CString filename =
|
|
MakePathedFilename(directories->modelDirectory, entry_data);
|
|
|
|
NotationFile *exp_file = new NotationFile(filename);
|
|
Register_Object(exp_file);
|
|
|
|
if(exp_file->IsEmpty())
|
|
{
|
|
std::cerr<<model_name<<" : "<<filename<<" is emoty or missing! \n";
|
|
Unregister_Object(dmg_file);
|
|
delete dmg_file;
|
|
Unregister_Object(dmg_namelist);
|
|
delete dmg_namelist;
|
|
Unregister_Object(exp_file);
|
|
delete exp_file;
|
|
return ResourceDescription::NullResourceID;
|
|
}
|
|
else
|
|
{
|
|
ExplosionTable::CreateStreamedExplosionTable(
|
|
resource_file,
|
|
model_name,
|
|
exp_file,
|
|
filename,
|
|
&explosion_stream
|
|
);
|
|
}
|
|
Unregister_Object(exp_file);
|
|
delete exp_file;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Write Out no Explosions for this DamageZone
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
int zero_int(0);
|
|
explosion_stream << zero_int;
|
|
}
|
|
dmg_entry = dmg_entry->GetNextEntry();
|
|
}
|
|
|
|
ResourceDescription *new_res =
|
|
resource_file->AddResourceMemoryStream(
|
|
model_name,
|
|
ResourceDescription::ExplosionTableStreamResourceType,
|
|
1,
|
|
ResourceDescription::Preload,
|
|
&explosion_stream
|
|
);
|
|
Check(new_res);
|
|
Unregister_Object(dmg_file);
|
|
delete dmg_file;
|
|
Unregister_Object(dmg_namelist);
|
|
delete dmg_namelist;
|
|
return new_res->resourceID;
|
|
}
|
|
|
|
#if defined(TEST_CLASS)
|
|
# include "entity.tcp"
|
|
#endif
|