//============================================================================//
// File:	ammobin.cpp                                                          //
// Project: BattleTech																			//
// Contents: Ammunition bin (AmmoBin : HeatWatcher) -- feed + cook-off          //
//----------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved  //
//                   	PROPRIETARY AND CONFIDENTIAL                          //
//============================================================================//
//
// RECONSTRUCTED from the shipped binary.  Only AMMOBIN.TCP survives
// (AmmoBin::TestClass -> True, AmmoBin::ResetToInitialState -> empty).  Behaviour
// follows the Ghidra pseudo-C in part_013.c; each non-trivial method cites the
// originating @ADDR.  Names past the HeatWatcher base are best-effort.
//
// Coverage:
//   confident : ctor @004bd5c4 (HeatWatcher chain, resource copy, alarm/cook-off
//               init), dtor thunk @004bd6b0, CreateStreamedSubsystem @004bd6f0,
//               FeedAmmo @004bd4f4, DumpAmmo @004bd588, CookOff @004bd300,
//               HandleMessage @004bdb94, ReadUpdate @004bd2c0,
//               AmmoBinSimulation @004bd394, TestInstance @004bd1e0
//   best-effort: the cook-off heat-injection descriptor layout (@0x1F0..0x21C),
//               the precise meaning of base status word @0x40 (subsystem damage
//               state) and the alarm level names
//   excluded   : the 0x41xxxx engine vtable slots and the HeatWatcher /
//               HeatableSubsystem base bodies (FUN_004aeb40 / FUN_004ac644 chain)
//
// Decoded constant:
//      _DAT_004bd4e8 = 0.0f   (feed-tick guard: advance only when time_slice > 0)
//
// Resource classID stamps (CreateStreamedSubsystem @004bd6f0):
//      "ProjectileClassID" -> 0x0BD1     "MissileClassID" -> 0x0BBA
//      AmmoBin record:  classID @+0x20 = 0x0BCB,  size @+0x24 = 0x104
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_004aeb40  HeatWatcher base constructor (owner,id,resource,name,shared)
//      FUN_004aebe8  HeatWatcher destructor        FUN_004aec54  HeatWatcher CSS
//      FUN_004aea84  HeatWatcher::ReadUpdate (slot 9 base)
//      FUN_004aeac4  HeatWatcher::WatchSimulation (drive watch alarm from temp)
//      FUN_004ac1d4  HeatableSubsystem::HandleMessage (slot 8 base)
//      FUN_004ac274  HeatableSubsystem::InjectHeat(descriptor)
//      FUN_0041b9ec  AlarmIndicator::Init(levels)  FUN_0041baa4  AlarmIndicator::Destroy
//      FUN_0041bbd8  AlarmIndicator::SetLevel      FUN_0041db7c  init heat descriptor
//      FUN_00414b60  GameClock::Now()              FUN_004dcd94  random delay
//      FUN_0041a1a4  IsDerivedFrom                 FUN_004022d0  operator delete
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(AMMOBIN_HPP)
#	include <ammobin.hpp>
#endif
#if !defined(TESTBT_HPP)
#	include <testbt.hpp>
#endif

//
// Feed-tick guard, read as a read-only global in the decomp.
//
static const Scalar AmmoFeedTickFloor = 0.0f;		// _DAT_004bd4e8

//
// Reconstruction support: small proxy types + unrecovered-helper stand-ins for
// the cook-off heat descriptor, the streamed-resource parser and the game clock.
//
struct HeatRecord     { Scalar amount; };
struct AmmoModelRecord{ int index; };
static const Scalar _DAT_004bdb74 = -1.0f;			// FeedRate "unset" sentinel

static void InitCookOffHeatSource(void *) {}					// FUN_0041db7c
static void InjectHeat(void *) {}								// FUN_004ac274
static void SetStatusLevel(int) {}								// FUN_0041bbd8 (status alarm)
static int  ReadInt(NotationFile *f, const char *p, const char *k, int *v)		{ return f ? f->GetEntry(p, k, v) : 0; }
static int  ReadScalar(NotationFile *f, const char *p, const char *k, Scalar *v)	{ return f ? f->GetEntry(p, k, v) : 0; }
static int  ReadString(NotationFile *f, const char *p, const char *k, const char **v){ return f ? f->GetEntry(p, k, v) : 0; }
static void ReportMissing(const char *, const char *) {}		// FUN_004dbb24
static AmmoModelRecord *LookupModel(NotationFile *, const char *, int, int)	{ return 0; }	// FUN_00406ff8
namespace GameClock { static int Now() { return 0; } }			// FUN_00414b60
static int  RandomDelay() { return 0; }							// FUN_004dcd94

//###########################################################################
//###########################################################################
//					AmmoBin
//###########################################################################
//###########################################################################

//#############################################################################
// Shared Data Support
//
Derivation
	AmmoBin::ClassDerivations(
		HeatWatcher::GetClassDerivations(),
		"AmmoBin"
	);

Receiver::MessageHandlerSet		AmmoBin::MessageHandlers;
Simulation::AttributeIndexSet	AmmoBin::AttributeIndex;

AmmoBin::SharedData
	AmmoBin::DefaultData(						// resolved as &DAT_005125bc
		&AmmoBin::ClassDerivations,
		AmmoBin::MessageHandlers,
		AmmoBin::AttributeIndex,
		AmmoBin::StateCount
	);

//#############################################################################
// Construction
//
// @004bd5c4  AmmoBin::AmmoBin(Mech *owner, int id, SubsystemResource *resource)
//
//   Chains the HeatWatcher base ctor (which sets up the temperature watch and
//   its degradation/failure thresholds), stamps the AmmoBin vtable, builds the
//   6-level feed alarm and the cook-off heat descriptor, and copies the ammo
//   resource fields.  The feed timer starts at 0 and the alarm at Loaded(1).
//   The active cook-off/feed Performance (PTR_FUN_00512654 -> AmmoBinSimulation)
//   is installed only for an authoritative (non-ghost) mech.
//
AmmoBin::AmmoBin(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*resource)
:
	// HeatWatcher base ctor (owner, id, resource, shared default data).
	HeatWatcher(owner, subsystem_ID, resource, DefaultData)	// @004aeb40
{
	// vtable installed by the compiler (PTR_FUN_0051286c)

	// (WAVE 4) statusState=0 removed -- the base MechSubsystem ctor owns
	// simulationState@0x40; the reads below use the inherited member.
	ammoAlarm.Initialize(6);						// @0x194  FUN_0041b9ec(this+0x65, 6)
	InitCookOffHeatSource(&cookOffHeatSource);		// @0x1F0  FUN_0041db7c(this+0x7c)
	reserved = 0;									// @0x228

	ammoCount			= resource->ammoCount;			// @0x180  resource +0xFC
	feedRate			= resource->feedRate;			// @0x224  resource +0x100
	ammoClassID			= resource->ammoClassID;		// @0x188  resource +0xF0
	ammoModelFile		= resource->ammoModelFile;		// @0x1E8  resource +0xF4
	explosionModelFile	= resource->explosionModelFile;	// @0x1EC  resource +0xF8
	feedTimer			= 0;							// @0x184

	// install the feed/cook-off Performance for the authoritative simulation only
	if ((owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100) != 0)	// owner+0x28
		SetPerformance(&AmmoBin::AmmoBinSimulation);	// PTR_FUN_00512654

	ammoAlarm.SetLevel(Loaded);						// @0x1A8  FUN_0041bbd8(this+0x65, 1)
	initialAmmoCount = ammoCount;					// @0x220
	cookOffArmed     = 0;							// @0x18C
}

//#############################################################################
// Destruction
//
// @004bd6b0  ~AmmoBin() -- restores the vtable, tears down the feed alarm, then
// chains the HeatWatcher base destructor.
//
AmmoBin::~AmmoBin()
{
	// vtable reset, alarm teardown and base ~HeatWatcher chaining handled by the
	// compiler.
}

//#############################################################################
// FeedAmmo -- request one round (called by the linked ProjectileWeapon)
//
// @004bd4f4
//
//   Returns 1 when a round was dispensed, 0 when the bin cannot deliver
//   (destroyed / mid-feed / empty).  Only the Loaded state dispenses: it switches
//   to Feeding, reloads the feed timer to feedRate, decrements ammoCount (clamped
//   at 0) and raises the Empty alarm if that was the last round.
//
int AmmoBin::FeedAmmo()
{
	if (simulationState == 1)							// this+0x40 == 1 (destroyed)
		return 0;

	switch (ammoAlarm.Level())						// this+0x1A8
	{
	case Loaded:									// 1
		ammoAlarm.SetLevel(Feeding);				// 0
		feedTimer = feedRate;						// this+0x184 <- this+0x224
		--ammoCount;								// this+0x180
		if (ammoCount < 1)
			ammoAlarm.SetLevel(Empty);				// 2
		if (ammoCount < 0)
			ammoCount = 0;							// clamp
		return 1;									// dispensed

	case Feeding:	// 0 -- still chambering
	case Empty:		// 2
	default:
		return 0;
	}
}

//#############################################################################
// DumpAmmo -- forcibly empty the bin (eject / strip)
//
// @004bd588
//
void AmmoBin::DumpAmmo()
{
	if (ammoAlarm.Level() != Empty)					// this+0x1A8 != 2
	{
		ammoCount = 0;								// this+0x180
		ammoAlarm.SetLevel(Dumped);					// 5 (transient pulse)
		ammoAlarm.SetLevel(Empty);					// 2
	}
}

//#############################################################################
// CookOff -- detonate the remaining rounds
//
// @004bd300
//
//   Loads the prebuilt heat descriptor, sets its heat amount to
//   (ammoCount * heatPerRound), injects it into the mech via the HeatableSubsystem
//   base, then zeroes the bin and raises the Empty alarm.
//
void AmmoBin::CookOff()
{
	HeatRecord record = *(HeatRecord *)&cookOffHeatSource;		// words 0x7C, 0x7E..0x87
	record.amount = (Scalar)ammoCount * heatPerRound;			// this[0x60] * this[0x7D]
	InjectHeat(&record);										// FUN_004ac274(this, &record)
	ammoCount = 0;												// this+0x180
	ammoAlarm.SetLevel(Empty);									// 2
}

//#############################################################################
// AmmoBinSimulation -- the registered Performance
//
// @004bd394
//
//   1. Drive the watch alarm from the monitored temperature (HeatWatcher base).
//   2. If destroyed, force Empty.
//   3. Tick the feed timer while Feeding; on expiry, chamber (-> Loaded).
//   4. When the watch alarm hits FAILURE (level 2) and rounds remain, arm a
//      randomised cook-off; on expiry damage the subsystem, fire the explosion
//      notification (vtable slot 13) and CookOff().  Cancel if the bin empties.
//   5. Trip the Empty alarm whenever ammoCount drops below 1.
//
void AmmoBin::AmmoBinSimulation(Scalar time_slice)
{
	WatchSimulation(time_slice);					// FUN_004aeac4(this) -- drive this+0x140

	if (simulationState == 1)							// this+0x40 (destroyed)
		ammoAlarm.SetLevel(Empty);

	// feed timer (only while a round is in transit)
	if (AmmoFeedTickFloor < time_slice && ammoAlarm.Level() == Feeding)
	{
		if (time_slice < feedTimer)
			feedTimer -= time_slice;
		else
		{
			feedTimer = 0;
			ammoAlarm.SetLevel(Loaded);
		}
	}

	// arm cook-off when overheated and not already empty
	if (heatAlarm.Level() == 2 /* FAILURE, this+0x140 */ &&
		ammoAlarm.Level() != Empty && cookOffArmed == 0)
	{
		cookOffArmed = 1;							// this+0x18C
		cookOffTime  = GameClock::Now() + RandomDelay();	// this+0x190
	}

	// fire / cancel cook-off
	if (cookOffArmed != 0)
	{
		if (ammoAlarm.Level() == Empty)
			cookOffArmed = 0;						// nothing left -- cancel
		else if (cookOffTime < GameClock::Now())
		{
			cookOffArmed = 0;
			SetStatusLevel(1);						// FUN_0041bbd8(this+0xb, 1) -- subsystem damaged
			// slot 13 (explosion notify): authoritative-only virtual call in the
			// shipped code; runs through the installed vtable at run time.
			CookOff();								// @004bd300
		}
	}

	if (ammoCount < 1)
		ammoAlarm.SetLevel(Empty);
}

//#############################################################################
// HandleMessage  (slot 8, @004bdb94)
//
//   Message 1 begins the cook-off countdown (arm the randomised timer once);
//   any other message delegates to the HeatableSubsystem base.
//
Logical AmmoBin::HandleMessage(int message)
{
	if (message != 1)
		return MechSubsystem::HandleMessage(message);	// base delegate (AmmoBin : HeatWatcher : MechSubsystem)

	if (cookOffArmed == 0)
	{
		cookOffArmed = 1;
		cookOffTime  = GameClock::Now() + RandomDelay();	// FUN_00414b60 + FUN_004dcd94
		return True;
	}
	return False;
}

//#############################################################################
// ReadUpdate  (slot 9, @004bd2c0)
//
//   Applies a network update via the HeatWatcher base; if the synced status has
//   just transitioned to destroyed (state 1), dump all rounds and mark Empty.
//
void AmmoBin::ReadUpdateRecord(UpdateRecord *update)
{
	int previousState = simulationState;			// this+0x40 (inherited MechSubsystem)
	HeatWatcher::ReadUpdateRecord(update);			// FUN_004aea84
	if (simulationState == 1 && previousState != 1)
	{
		ammoCount = 0;								// this+0x180
		ammoAlarm.SetLevel(Empty);					// 2
	}
}

//#############################################################################
// ResetToInitialState  (slot 10)
//
//   AMMOBIN.TCP ships this as an empty override.
//
void AmmoBin::ResetToInitialState()
{
}

//#############################################################################
// CreateStreamedSubsystem
//
// @004bd6f0  (record size 0x104, classID 0x0BCB)
//
//   Chains the HeatWatcher resource parser, stamps the AmmoBin classID/size, then
//   reads the ammo block.  "AmmoClassID" accepts the symbolic forms
//   "ProjectileClassID" (-> 0x0BD1) and "MissileClassID" (-> 0x0BBA).  The model
//   files are resolved to indices; every key is required (missing -> fail).
//
int AmmoBin::CreateStreamedSubsystem(
	NotationFile				*model_file,
	const char					*model_name,
	const char					*subsystem_name,
	SubsystemResource			*resource,
	NotationFile				*subsystem_file,
	const ResourceDirectories	*directories,
	int							passes)
{
	if (!HeatWatcher::CreateStreamedSubsystem(model_file, model_name, subsystem_name, resource,	// FUN_004aec54
			subsystem_file, directories, passes))
		return 0;

	resource->classID            = (RegisteredClass::ClassID)0x0BCB;	// resource +0x20
	resource->subsystemModelSize = 0x104;							// resource +0x24

	if (passes == 1)
	{
		resource->ammoCount  = -1;				// +0xFC sentinel
		resource->feedRate   = -1.0f;			// +0x100 sentinel
		resource->ammoClassID = -1;				// +0xF0 sentinel
		resource->ammoModelFile = -1;			// +0xF4 sentinel
		resource->explosionModelFile = -1;		// +0xF8 sentinel
	}

	if (!ReadInt(subsystem_file, subsystem_name, "AmmoCount", &resource->ammoCount) &&
		resource->ammoCount == -1)
		{ ReportMissing(subsystem_name, "missing AmmoCount"); return 0; }

	if (!ReadScalar(subsystem_file, subsystem_name, "FeedRate", &resource->feedRate) &&
		resource->feedRate == _DAT_004bdb74 /* sentinel */)
		{ ReportMissing(subsystem_name, "missing FeedRate"); return 0; }

	const char *id = "Unspecified";
	ReadString(subsystem_file, subsystem_name, "AmmoClassID", &id);
	if (strcmp(id, "Unspecified") == 0 && resource->ammoClassID == -1)
		{ ReportMissing(subsystem_name, "missing AmmoClassID"); return 0; }
	if (strcmp(id, "Unspecified") != 0)
	{
		if      (strcmp(id, "ProjectileClassID") == 0) resource->ammoClassID = 0x0BD1;
		else if (strcmp(id, "MissileClassID")    == 0) resource->ammoClassID = 0x0BBA;
		else if (resource->ammoClassID == -1)
			{ ReportMissing(subsystem_name, "invalid AmmoClassID"); return 0; }
	}

	const char *ammoModel = "Unspecified";
	ReadString(subsystem_file, subsystem_name, "AmmoModelFile", &ammoModel);
	if (strcmp(ammoModel, "Unspecified") != 0)
	{
		AmmoModelRecord *m = LookupModel(model_file, ammoModel, 1, -1);	// FUN_00406ff8
		if (m == 0 && resource->ammoModelFile == -1)
			{ ReportMissing(ammoModel, "must be declared before .bld"); return 0; }
		if (m != 0) resource->ammoModelFile = m->index;
	}
	else if (resource->ammoModelFile == -1)
		{ ReportMissing(subsystem_name, "missing AmmoModelFile"); return 0; }

	const char *exModel = "Unspecified";
	ReadString(subsystem_file, subsystem_name, "ExplosionModelFile", &exModel);
	if (strcmp(exModel, "Unspecified") != 0)
	{
		AmmoModelRecord *m = LookupModel(model_file, exModel, 1, -1);
		if (m == 0 && resource->explosionModelFile == -1)
			{ ReportMissing(exModel, "cannot find in resource file"); return 0; }
		if (m != 0) resource->explosionModelFile = m->index;
	}
	else if (resource->explosionModelFile == -1)
		{ ReportMissing(model_name, "missing ExplosionModelFile"); return 0; }

	return 1;
}

//#############################################################################
// TestInstance
//
// @004bd1e0  IsDerivedFrom(ClassDerivations tag 0x512488).
//
Logical AmmoBin::TestInstance() const
{
	return IsDerivedFrom(AmmoBin::ClassDerivations);
}

//#############################################################################
// TestClass  (AMMOBIN.TCP)
//
Logical AmmoBin::TestClass(Mech &)
{
	return True;
}


//#############################################################################
// WAVE 4 -- compile-time layout locks (ZERO-headroom: sizeof MUST be 0x22C).
//   Own fields are protected -> the friend AmmoBinLayoutCheck grants offsetof.
//   The `ammoModelFile == 0x1E8` lock is LOAD-BEARING: it fails the build if
//   either the ammoAlarm retype (HeatAlarm 8B -> WatcherGaugeAlarm 0x54) or the
//   statusState deletion is missed.  HeatWatcher is locked at 0x180.
//#############################################################################
struct AmmoBinLayoutCheck
{
	static_assert(sizeof(AmmoBin)                       == 0x22C, "sizeof(AmmoBin) must be 0x22C (factory alloc, part_012.c:10091)");
	static_assert(offsetof(AmmoBin, ammoCount)          == 0x180, "AmmoBin own block starts at 0x180 (HeatWatcher ends 0x180)");
	static_assert(offsetof(AmmoBin, ammoAlarm)          == 0x194, "AmmoBin ammoAlarm @0x194 (0x54-byte WatcherGaugeAlarm)");
	static_assert(offsetof(AmmoBin, ammoModelFile)      == 0x1E8, "AmmoBin ammoModelFile @0x1E8 (proves ammoAlarm==0x54 AND statusState deleted)");
	static_assert(offsetof(AmmoBin, explosionModelFile) == 0x1EC, "AmmoBin explosionModelFile @0x1EC");
	static_assert(offsetof(AmmoBin, heatPerRound)       == 0x1F4, "AmmoBin heatPerRound @0x1F4");
	static_assert(offsetof(AmmoBin, initialAmmoCount)   == 0x220, "AmmoBin initialAmmoCount @0x220");
	static_assert(offsetof(AmmoBin, feedRate)           == 0x224, "AmmoBin feedRate @0x224");
	static_assert(offsetof(AmmoBin, reserved)           == 0x228, "AmmoBin reserved @0x228 (+4 => sizeof 0x22C)");
};


//===========================================================================//
// Factory bridge -- AmmoBin (factory case 0xBCB, "JumpJet" label).
//   The real class at 0xBCB (ctor @004bd5c4) is AmmoBin; the factory built a
//   JumpJet RECON_SUBSYS stub.  alloc 0x22c, ctor(owner,id,resource) -- the ctor
//   supplies AmmoBin::DefaultData to the HeatWatcher base itself.  No cache
//   write; not a weapon (raw part_012.c:10090-10099 stores roster-only).
//   NOTE: the Blackhawk (all-laser) carries NO 0xBCB; exercise with a ballistic/
//   missile mech (MECH2.EGG = Mad Cat, LRMs).  Its FeedAmmo consumer
//   (ProjectileWeapon/MissileLauncher 0xBCD/0xBD0) is still stubbed, so full
//   effect couples to the weapons wave -- but the bin's own feed/cook-off ticks.
//===========================================================================//
Subsystem *CreateAmmoBinSubsystem(Mech *owner, int id, void *seg)
{
	return (Subsystem *) new (Memory::Allocate(0x22c))
		AmmoBin(owner, id, (AmmoBin::SubsystemResource *)seg);
}
