Three pieces of harness, all env-gated and inert in normal play, that turn "do two frame rates play the same race?" from an argument into a number: - RP412PHYSTRACE=1 samples the player vehicle's position on the SIMULATION's own clock - the vehicle's lastPerformance, which advances in whole fixed steps - so two runs sample at identical step counts and their traces compare exactly. Frame-time sampling compares different instants and calls the difference physics; an earlier version of this trace did exactly that, and its noise was chased as if it were drift. At the green light it stops the vehicle dead, because the pod simulates on its pad while the mission loads and a load is never the same length twice: two runs reached the start 776 and 599 steps in, same position, different velocity. - RP412SPAWNZONE pins which drop zone is tried first. The pick is Random(), and Random() is seeded - but a seed only repeats a run if the same NUMBER of draws precedes the pick, and that count rides on load timing. Same seed, different pad, incomparable traces. Pinned, the zone is tried first and falls back to the random walk if taken, so it cannot wedge and changes nothing unless set. - The trace prints the global step counter, which is what caught the force-accumulator bug: the position columns can look plausible while the step column says the physics ran a different number of times. With these three and RANDOM= (which already existed), a race is repeatable to the bit, and the determinism matrix - rates by frame rates by repeats, run as parallel sandboxed instances - is a regression suite: any mismatch in any cell is a real bug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
486 lines
12 KiB
C++
486 lines
12 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "dropzone.h"
|
|
#include "random.h"
|
|
#include "app.h"
|
|
#include "hostmgr.h"
|
|
#include "nttmgr.h"
|
|
#include "notation.h"
|
|
#include "namelist.h"
|
|
#include "player.h"
|
|
|
|
#define DOWN_TIME 5.0f
|
|
|
|
//#############################################################################
|
|
//############################### DropZone #################################
|
|
//#############################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation* DropZone::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Entity::GetClassDerivations(),"DropZone");
|
|
return &classDerivations;
|
|
}
|
|
|
|
DropZone::SharedData
|
|
DropZone::DefaultData(
|
|
DropZone::GetClassDerivations(),
|
|
DropZone::GetMessageHandlers(),
|
|
DropZone::GetAttributeIndex(),
|
|
DropZone::StateCount,
|
|
(Entity::MakeHandler)DropZone::Make
|
|
);
|
|
|
|
//#############################################################################
|
|
// Message Support
|
|
//
|
|
const Receiver::HandlerEntry
|
|
DropZone::MessageHandlerEntries[]=
|
|
{
|
|
MESSAGE_ENTRY(DropZone, AssignDropZone)
|
|
};
|
|
|
|
Receiver::MessageHandlerSet& DropZone::GetMessageHandlers()
|
|
{
|
|
static Receiver::MessageHandlerSet messageHandlers(ELEMENTS(DropZone::MessageHandlerEntries), DropZone::MessageHandlerEntries, Entity::GetMessageHandlers());
|
|
return messageHandlers;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
DropZone::DropZone(
|
|
DropZone::MakeMessage *creation_message,
|
|
DropZone::SharedData &virtual_data
|
|
):
|
|
Entity(creation_message, virtual_data)
|
|
{
|
|
Check_Pointer(this);
|
|
Check_Pointer(creation_message);
|
|
|
|
SetValidFlag();
|
|
dropZoneCount = 0;
|
|
dropZones = NULL;
|
|
|
|
//
|
|
//--------------------------------------------------------------
|
|
// Name the drop zone, and enter the zone in the drop zone group
|
|
//--------------------------------------------------------------
|
|
//
|
|
Str_Copy(dropZoneName, creation_message->dropZoneName, sizeof(dropZoneName));
|
|
EntityManager *entity_manager = application->GetEntityManager();
|
|
Check(entity_manager);
|
|
EntityGroup* drop_zones = entity_manager->UseGroup("DropZones");
|
|
Check(drop_zones);
|
|
drop_zones->Add(this);
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Now, interpret the data to create the appropriate number of drop zones
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
MemoryStream
|
|
stream(
|
|
creation_message,
|
|
creation_message->messageLength,
|
|
sizeof(*creation_message)
|
|
);
|
|
dropZoneCount = creation_message->dropZoneCount;
|
|
Verify(dropZoneCount > 0);
|
|
dropZones = new Origin[dropZoneCount];
|
|
Register_Pointer(dropZones);
|
|
lastUsageTime = new Time[dropZoneCount];
|
|
Register_Pointer(lastUsageTime);
|
|
lastUsedBy = new EntityID[dropZoneCount];
|
|
Register_Pointer(lastUsedBy);
|
|
lastDeathCount = new int[dropZoneCount];
|
|
Register_Pointer(lastDeathCount);
|
|
|
|
for (int i=0; i<dropZoneCount; ++i)
|
|
{
|
|
dropZones[i] = *(Origin*)stream.GetPointer();
|
|
lastUsageTime[i] = Time::Null;
|
|
lastUsedBy[i] = EntityID::Null;
|
|
lastDeathCount[i] = -3;
|
|
stream.AdvancePointer(sizeof(Origin));
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
DropZone*
|
|
DropZone::Make(DropZone::MakeMessage *creation_message)
|
|
{
|
|
return new DropZone(creation_message);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
DropZone::~DropZone()
|
|
{
|
|
if (dropZones)
|
|
{
|
|
Unregister_Pointer(dropZones);
|
|
delete[] dropZones;
|
|
Unregister_Pointer(lastUsageTime);
|
|
delete[] lastUsageTime;
|
|
Unregister_Pointer(lastUsedBy);
|
|
delete[] lastUsedBy;
|
|
Unregister_Pointer(lastDeathCount);
|
|
delete[] lastDeathCount;
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// Dropzone assignment
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
DropZone::IsAvailable(int index)
|
|
{
|
|
Check(this);
|
|
Verify(index >= 0 && index < dropZoneCount);
|
|
Check(application);
|
|
|
|
return
|
|
!lastUsageTime[index].ticks
|
|
|| Now() - lastUsageTime[index] > DOWN_TIME
|
|
&& application->GetApplicationState() == Application::RunningMission;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
DropZone::AssignDropZoneMessageHandler(AssignDropZoneMessage* message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
Check(application);
|
|
HostManager *host = application->GetHostManager();
|
|
Check(host);
|
|
Entity *entity = host->GetEntityPointer(message->requestingEntity);
|
|
Check(entity);
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// If we have allocated a dropzone within the last ten seconds for this
|
|
// player, resend the data
|
|
//---------------------------------------------------------------------
|
|
//
|
|
Origin *drop_zone = NULL;
|
|
int
|
|
lowest, highest, remaining, i;
|
|
for (i=0; i<dropZoneCount; ++i)
|
|
{
|
|
if (
|
|
!IsAvailable(i)
|
|
&& lastUsedBy[i] == message->requestingEntity
|
|
&& lastDeathCount[i] == message->deathCount
|
|
)
|
|
{
|
|
lastUsageTime[i] = Now();
|
|
drop_zone = &dropZones[i];
|
|
goto Found_One;
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// Figure out how many drop zones are available. If none are, repost this
|
|
// message to ourself
|
|
//------------------------------------------------------------------------
|
|
//
|
|
lowest = dropZoneCount;
|
|
highest = -1;
|
|
remaining = 0;
|
|
for (i=0; i<dropZoneCount; ++i)
|
|
{
|
|
if (IsAvailable(i))
|
|
{
|
|
++remaining;
|
|
if (i < lowest)
|
|
{
|
|
lowest = i;
|
|
}
|
|
if (i > highest)
|
|
{
|
|
highest = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------
|
|
// Now, randomly look about until we find one available
|
|
//-----------------------------------------------------
|
|
//
|
|
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)
|
|
{
|
|
if (tryPinnedZone)
|
|
{
|
|
tryPinnedZone = False;
|
|
i = lowest + (pinnedZone % highest);
|
|
}
|
|
else
|
|
{
|
|
i = lowest + Random(highest);
|
|
}
|
|
Verify(i < dropZoneCount && i >= 0);
|
|
if (IsAvailable(i))
|
|
{
|
|
//
|
|
//-------------------------------------------------------------------
|
|
// Check to see if vehicle attached to a player is within 2 meters of
|
|
// this drop zone. If so, mark it unavailable for 3 seconds
|
|
//-------------------------------------------------------------------
|
|
//
|
|
EntityManager *entity_mgr = application->GetEntityManager();
|
|
Check(entity_mgr);
|
|
EntityGroup *player_group = entity_mgr->FindGroup("Players");
|
|
if (
|
|
player_group
|
|
&& application->GetApplicationState()
|
|
== Application::RunningMission
|
|
)
|
|
{
|
|
Player *player;
|
|
ChainIteratorOf<Node*> iterator(player_group->groupMembers);
|
|
while ((player = (Player*)iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(player);
|
|
Entity *vehicle = player->GetPlayerVehicle();
|
|
if (vehicle && player != entity)
|
|
{
|
|
Check(vehicle);
|
|
Vector3D distance;
|
|
distance.Subtract(
|
|
vehicle->localOrigin.linearPosition,
|
|
dropZones[i].linearPosition
|
|
);
|
|
if (distance.LengthSquared() <= 4.0f)
|
|
{
|
|
lastUsedBy[i] = NULL;
|
|
lastUsageTime[i] = Now();
|
|
lastUsageTime[i] -= DOWN_TIME - 1.0f;
|
|
--remaining;
|
|
if (i == lowest)
|
|
{
|
|
++lowest;
|
|
--highest;
|
|
}
|
|
else if (i == lowest + highest - 1)
|
|
{
|
|
--highest;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------------------
|
|
// If someone was in this spot, keep looking for another one
|
|
//----------------------------------------------------------
|
|
//
|
|
if (player)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
lastUsageTime[i] = Now();
|
|
drop_zone = &dropZones[i];
|
|
lastUsedBy[i] = message->requestingEntity;
|
|
lastDeathCount[i] = message->deathCount;
|
|
remaining = 0;
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
//----------------------------------------------------------------
|
|
// If no drop_zone could be found, repost the message to ourselves
|
|
//----------------------------------------------------------------
|
|
//
|
|
if (!drop_zone)
|
|
{
|
|
Time when=Now();
|
|
when += 0.1f;
|
|
application->Post(MaxEventPriority, this, message, when);
|
|
return;
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------------------------
|
|
// Send a message back to the requesting object what sent this message
|
|
//--------------------------------------------------------------------
|
|
//
|
|
Found_One:
|
|
ReplyMessage
|
|
reply(
|
|
message->replyMessageID,
|
|
sizeof(ReplyMessage),
|
|
*drop_zone,
|
|
message->deathCount
|
|
);
|
|
entity->Dispatch(&reply);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
DropZone::CreateMakeMessage(
|
|
MakeMessage *creation_message,
|
|
const char* model_name,
|
|
const char* resource_name,
|
|
NotationFile *map_file
|
|
)
|
|
{
|
|
Check(creation_message);
|
|
Check(map_file);
|
|
Check_Pointer(model_name);
|
|
Check_Pointer(resource_name);
|
|
|
|
if (!Entity::CreateMakeMessage(creation_message, map_file, NULL))
|
|
{
|
|
return False;
|
|
}
|
|
|
|
creation_message->classToCreate = RegisteredClass::DropZoneClassID;
|
|
creation_message->instanceFlags = MapFlag|TrappedFlag;
|
|
|
|
//
|
|
//-----------------------------------------------
|
|
// Count up the number of drop zones in our model
|
|
//-----------------------------------------------
|
|
//
|
|
Enumeration save_modes = map_file->GetModes();
|
|
map_file->SetMode(NotationFile::CleanListMode);
|
|
|
|
NameList *zone_list =
|
|
map_file->MakeEntryList(model_name, "dropzone");
|
|
Register_Object(zone_list);
|
|
creation_message->dropZoneCount = zone_list->EntryCount();
|
|
creation_message->messageLength =
|
|
sizeof(*creation_message)
|
|
+ creation_message->dropZoneCount*sizeof(Origin);
|
|
if (!creation_message->dropZoneCount)
|
|
{
|
|
std::cout << "Error: " << model_name << " hasn't specified any drop zones!\n";
|
|
Dump_And_Die:
|
|
Unregister_Object(zone_list);
|
|
delete zone_list;
|
|
map_file->PutModes(save_modes);
|
|
return False;
|
|
}
|
|
|
|
//
|
|
//---------------------------------------------
|
|
// Allocate a buffer to hold the drop zone data
|
|
//---------------------------------------------
|
|
//
|
|
MemoryStream
|
|
stream(
|
|
creation_message,
|
|
creation_message->messageLength,
|
|
sizeof(*creation_message)
|
|
);
|
|
|
|
//
|
|
//----------------------------
|
|
// zero out the drop zone name
|
|
//----------------------------
|
|
//
|
|
for (int ii=0; ii<sizeof(creation_message->dropZoneName); ii++)
|
|
{
|
|
creation_message->dropZoneName[ii] = '\0';
|
|
}
|
|
|
|
Str_Copy(
|
|
creation_message->dropZoneName,
|
|
resource_name,
|
|
sizeof(creation_message->dropZoneName)
|
|
);
|
|
|
|
//
|
|
//-----------------
|
|
// Read in the data
|
|
//-----------------
|
|
//
|
|
NameList::Entry *drop_zone = zone_list->GetFirstEntry();
|
|
while (drop_zone)
|
|
{
|
|
Point3D translation;
|
|
EulerAngles tmp_rotation;
|
|
|
|
const char* zone_data = drop_zone->GetChar();
|
|
sscanf(
|
|
zone_data,
|
|
"%f %f %f %f %f %f",
|
|
&translation.x,
|
|
&translation.y,
|
|
&translation.z,
|
|
&tmp_rotation.pitch,
|
|
&tmp_rotation.yaw,
|
|
&tmp_rotation.roll
|
|
);
|
|
|
|
((Origin*)stream.GetPointer())->linearPosition = translation;
|
|
((Origin*)stream.GetPointer())->angularPosition = tmp_rotation;
|
|
stream.AdvancePointer(sizeof(Origin));
|
|
drop_zone = drop_zone->GetNextEntry();
|
|
}
|
|
|
|
Unregister_Object(zone_list);
|
|
delete zone_list;
|
|
map_file->PutModes(save_modes);
|
|
|
|
return True;
|
|
}
|
|
|
|
Logical
|
|
DropZone::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|