Files
RP412/MUNGA/DROPZONE.cpp
T
CydandClaude Fable 5 2a679ab571 A race can be replayed exactly, so physics claims can be tested
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>
2026-08-09 18:20:03 -05:00

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());
}