//===========================================================================//
// File:	mechsub.cpp                                                         //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: MechSubsystem -- BT damage-coupled base for all mech subsystems  //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/95  ??   Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BEST-EFFORT).  Behaviour follows the
// Ghidra pseudo-C for the cluster @0x4ac07c..@0x4ac9ec.  No function in the
// cluster is linker-tagged (all file=?); the module name "mechsub.cpp" is taken
// from the surviving MECHTECH.HPP `#include <mechsub.hpp>`.  This cluster was
// previously attributed to heat.cpp's "Subsystem/HeatableSubsystem base"; the
// classID 0xBBB / model 0xE4 streamer @0x4ac9ec, the "MechSubsystem"
// Derivation name (@0x50def5), the DamageZone coupling, and the absence of any
// thermal field access show it is MechSubsystem.  See mechsub.hpp for the full
// reconciliation note and the CLASSMAP correction.  Each method cites its @ADDR.
//
// Float constants converted to decimal:
//      _DAT_004ac140 = 0x3f800000 = 1.0f   (status threshold)
//      _DAT_004ac18c = 0x3f800000 = 1.0f   (GetStatusFlags upper threshold)
//      _DAT_004ac190 = 0.0f                (GetStatusFlags lower threshold)
//      _DAT_004ac860 = 0x3f800000 = 1.0f   (armour-inversion numerator)
//      _DAT_004ac864 = 0x38d1b717 ~= 1e-4f (armour-inversion epsilon)
//      DAT_004e0f80  = identity ResourceID
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_0041c5c0  Subsystem ctor (name + classID overload)
//      FUN_0041c52c  Subsystem ctor (resource overload)
//      FUN_0041c648  Subsystem dtor          FUN_0041c684 Subsystem::CreateStreamed
//      FUN_0041bbd8  AlarmIndicator::SetLevel(n)
//      FUN_0041de1c  DamageZone ctor (0x160) FUN_004023f4 RefCount ctor
//      FUN_00402298  operator new            FUN_004022d0 operator delete
//      FUN_00402a98  TextString connection bind
//      FUN_00408440  ResourceID copy
//      FUN_0041c800  (assert / GenerateFault default)
//      FUN_004dcd00  fabsf()
//      FUN_004dbb24  DebugStream::operator<<  FUN_004d9c38 endl
//      FUN_00404088/0118/0190  NotationFile::Read(char**/float*/int*)
//      FUN_004274f8  Get_Segment_Index        FUN_004d4b58 Strcmp()
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(MECHSUB_HPP)
#	include <mechsub.hpp>
#endif

//
// Tuning constants observed as read-only globals in the decomp.
//
static const Scalar StatusThreshold		= 1.0f;		// _DAT_004ac140 / _DAT_004ac18c
static const Scalar StatusFloor			= 0.0f;		// _DAT_004ac190
static const Scalar ArmourNumerator		= 1.0f;		// _DAT_004ac860
static const Scalar ArmourEpsilon		= 1.0e-4f;	// _DAT_004ac864
static const Scalar ResourceUnset		= -1.0f;	// 0xbf800000

//
// Status-state name table @0x50de74 walked by LookupStatusType (@0x4ac194).
//
struct StatusNameEntry { const char *name; int value; };
static const StatusNameEntry StatusNameTable[] =
{
	{ "Destroyed",		0 },
	{ "Damaged",		1 },
	{ "CoolantLeaking",	2 },
	{ "Overheating",	3 },
	{ "AmmoBurning",	4 },
	{ "Jammed",			5 },
	{ "BadPower",		6 },
	{ 0,				0 }
};

//
// Status-state name table @0x50de74 -> {char* name, int value} pairs, walked by
// LookupStatusType (@0x4ac194).  Names @0x50df17.
//
// PTR_s_Destroyed_0050de74 = {
//     "Destroyed",0, "Damaged",1, "CoolantLeaking",2, "Overheating",3,
//     "AmmoBurning",4, "Jammed",5, "BadPower",6, 0
// };

//#############################################################################
// Shared Data Support
//
MechSubsystem::SharedData
	MechSubsystem::DefaultData(
		&MechSubsystem::ClassDerivations,
		ReconMessageHandlers,
		ReconAttributeIndex,
		MechSubsystem::StateCount
	);

Derivation
	MechSubsystem::ClassDerivations(
		Subsystem::GetClassDerivations(),
		"MechSubsystem"					// @0x50def5
	);


//#############################################################################
// Construction / Destruction
//

//
// @0x4ac530 -- light constructor (name + classID).  Chains the MUNGA Subsystem
// name/classID ctor, lays the vtable @0x50e210, allocates the status memory
// block and a DamageZone (0x160 bytes) named "None", and primes flags.
//
MechSubsystem::MechSubsystem(
	Mech						*owner,
	int							subsystem_ID,
	const char					*subsystem_name,
	RegisteredClass::ClassID	class_id,
	SharedData					&shared_data
):
	Subsystem(owner, subsystem_ID, subsystem_name, class_id, shared_data)	// FUN_0041c5c0
{
	refCount		= NewRefCount(0x10);		// this[0x3d]
	this->owner		= owner;					// this[0x34] -- the owning Mech (canonical)
	hostEntity		= owner;					// this[0x3a] = param_7
	subsystemId2	= subsystem_ID;				// this[0x3b] = param_8
	videoObjectFlag	= 0;						// this[0x39]
	BindName(refCount, "None");					// FUN_00402a98(.,this+0x3d,"None") @0x50df7a
	alarmModel = 0;							// identity ResourceID							// FUN_00408440(this+0x3e, identity)
	criticalReference			= 0;			// this[0x42]
	collisionCriticalHitWeight	= 0;			// this[0x43]
	printSimulationState		= 0;			// this[0x41]
	vitalSubsystemIndex			= -1;			// this[0x44]
	damageZone		= (ReconDamageZone *)new DamageZone(this, 0);	// this[0x38] = FUN_0041de1c(new 0x160, this, 0)
}

//
// @0x4ac644 -- resource constructor.  Chains the MUNGA Subsystem resource ctor,
// lays the same vtable @0x50e210, builds the DamageZone, and seeds it from the
// resource: structureReference -> DamageZone+0x140, armorByFacing[5] ->
// DamageZone+0x144..+0x154.  Each armour scalar is then inverted to a
// per-facing absorption coefficient (1.0 / (armour * reference)) where the
// reciprocal exceeds the epsilon.
// (heat.cpp mislabels this ctor as "HeatableSubsystem"; the thermal fields it
// claims at this+0x114 are never written here -- they belong to HeatSink.)
//
MechSubsystem::MechSubsystem(
	Mech						*owner,
	int							subsystem_ID,
	SubsystemResource			*subsystem_resource,
	SharedData					&shared_data
):
	Subsystem(owner, subsystem_ID, subsystem_resource, shared_data)		// FUN_0041c52c
{
	refCount		= NewRefCount(0x10);		// this[0x3d]
	this->owner		= owner;					// this[0x34] -- the owning Mech.  (The recon
												// ctor only set hostEntity; subclasses read
												// `owner` widely -> must be set or it is garbage.)
	statusAlarm.SetLevel(0);					// FUN_0041bbd8(this+0xb, 0)
	hostEntity		= (Mech *)subsystem_resource->segmentIndex;	// this[0x3a]=param_6
	subsystemId2	= subsystem_ID;				// this[0x3b]=param_7
	resource		= subsystem_resource;		// this[0x3c]
	vitalSubsystemIndex	= -1;					// this[0x44]
	videoObjectFlag	= (subsystem_resource->vitalSubsystemIndex == 1);	// res+0x48==1

	BindName(refCount, subsystem_resource->videoObjectName);	// res+0x4c
	alarmModel = subsystem_resource->alarmModel;				// res+0xcc -> this[0x3e]
	printSimulationState		= subsystem_resource->printSimulationState;		// res+0xd8
	criticalReference			= subsystem_resource->criticalReference;		// res+0xe0
	collisionCriticalHitWeight	= subsystem_resource->collisionCriticalHitWeight;	// res+0xdc

	damageZone = (ReconDamageZone *)new DamageZone(this, 0);		// this[0x38]
	BindName((char *)damageZone + 0x15c, GetName());	// FUN_00402a98(., dz+0x15c, name)

	// structure reference and per-facing armour into the DamageZone
	damageZone->structureReference = subsystem_resource->structureReference;	// dz+0x140 = res+0x44
	for (int i = 0; i < 5; ++i)
	{
		damageZone->armour[i] = subsystem_resource->armorByFacing[i];	// dz+0x144+i = res+0x30+i
	}
	// invert each facing into an absorption coefficient
	for (int i = 0; i < 5; ++i)
	{
		Scalar a = damageZone->armour[i] - ArmourNumerator / damageZone->structureReference;
		if (ArmourEpsilon < fabsf(a))			// FUN_004dcd00
		{
			damageZone->armour[i] =
				ArmourNumerator / (damageZone->armour[i] * damageZone->structureReference);
		}
	}
}

//
// @0x4ac868 -- releases the DamageZone (virtual slot0, arg 3), drops the status
// memory block, then chains the MUNGA Subsystem dtor.
//
MechSubsystem::~MechSubsystem()
{
	// *this = &PTR_FUN_0050e210; damageZone->~DamageZone(); release(refCount);
	// FUN_0041c648(this, 0)  (Subsystem dtor)
}

Logical MechSubsystem::TestClass(Mech &)		{ return True; }
Logical MechSubsystem::TestInstance() const		{ return IsDerivedFrom(ClassDerivations); }


//#############################################################################
// Subsystem virtual overrides
//

//
// @0x4ac144 -- damage tier from the DamageZone structure level (dz+0x158):
//   >= 1.0  -> 1   (intact / nominal),  > 0.0 -> 2   (damaged),  else 0 (dead).
//
LWord
	MechSubsystem::GetStatusFlags()
{
	Scalar structure = damageZone->structureLevel;		// *(this[0x38]+0x158)
	if (StatusThreshold <= structure)	return 1;		// _DAT_004ac18c
	if (StatusFloor < structure)		return 2;		// _DAT_004ac190
	return 0;
}

//
// @0x4ac0bc -- per-frame structure watchdog.  Forwards the message to the base
// (vtable+0x18), and when the DamageZone structure has recovered to >= 1.0,
// raises the status alarm to level 1, fires the alarm-changed callback, pins
// structure to exactly 1.0, and (if a model is attached) raises alarm 9 on the
// owner's status block.
//
Logical
	MechSubsystem::HandleMessage(int message)
{
	(*BaseSlot0x18())(this, message);					// *(this[0x38].vtable+0x18)
	if (StatusThreshold <= damageZone->structureLevel)	// _DAT_004ac140
	{
		statusAlarm.SetLevel(1);						// FUN_0041bbd8(this+0xb, 1)
		if (printSimulationState != 0)					// this[0x41]
		{
			OnAlarmChanged();							// (*this.vtable+0x34)(this)
		}
		damageZone->structureLevel = 1.0f;				// dz+0x158 = 1.0
		if (videoObjectFlag != 0)						// this[0x39]
		{
			((Mech *)owner)->RaiseStatusAlarm(9);		// FUN_0041bbd8(owner+0x2c, 9)
		}
	}
	return True;
}

//
// @0x4ac1d4 -- reset.  On powered==0: restore structure to 1.0, set status
// alarm level 1 and fire the alarm-changed callback.  Otherwise delegate to the
// fault/assert path (FUN_0041c800).
//
void
	MechSubsystem::ResetToInitialState(Logical powered)
{
	if (!powered)
	{
		if (damageZone != 0)
		{
			damageZone->structureLevel = 1.0f;			// dz+0x158 = 1.0
		}
		statusAlarm.SetLevel(1);						// FUN_0041bbd8(this+0xb, 1)
		if (printSimulationState != 0)
		{
			OnAlarmChanged();							// (*this.vtable+0x34)(this)
		}
	}
	else
	{
		GenerateFaultDefault();							// FUN_0041c800
	}
}

//
// @0x4ac22c -- ClearStatus: zero the structure and both alarms (the status
// alarm and the DamageZone's own alarm), then fire the alarm-changed callback.
//
void
	MechSubsystem::ClearStatus()
{
	damageZone->structureLevel = 0.0f;					// dz+0x158 = 0
	damageZone->alarm.SetLevel(0);						// FUN_0041bbd8(dz+0x10, 0)
	statusAlarm.SetLevel(0);							// FUN_0041bbd8(this+0xb, 0)
	if (printSimulationState != 0)
	{
		OnAlarmChanged();
	}
}

//
// @0x4ac8c0 -- prints "<name> = <SimulationState>".  Status @this+0x40.
//
void
	MechSubsystem::PrintState()
{
	switch (simulationState)							// *(this+0x40)
	{
	case DefaultState:		DebugStream << GetName() << " = DefaultState" << endl; break;	// 0050df99
	case DestroyedState:	DebugStream << GetName() << " = Destroyed"    << endl; break;	// 0050df7f
	case ExplodingState:	DebugStream << GetName() << " = Exploding"    << endl; break;	// 0050df8c
	default:				DebugStream << GetName() << " = UnknownState!!!  : "
								<< simulationState << endl; break;								// 0050dfa9
	}
}

//
// @0x4ac9c8 -- IsDamaged: true (dead) when the owning mech's electrical bus is
// down -- *(*(*(owner+0xd0)+0x190)+0x274) == 0.
//
Logical
	MechSubsystem::IsDamaged()
{
	return (*(int *)(*(int *)(*(int *)((char *)owner + 0xd0) + 0x190) + 0x274) == 0);
}


//#############################################################################
// Damage / critical-hit helpers
//

//
// @0x4ac07c -- run TakeDamage (virtual slot+0x24) and return the change in the
// DamageZone structure level it produced.
//
Scalar
	MechSubsystem::ApplyDamageAndMeasure(Damage &damage)
{
	Scalar before = damageZone->structureLevel;			// dz+0x158
	TakeDamage(damage);									// (*this.vtable+0x24)(this, &damage)
	return damageZone->structureLevel - before;
}

//
// @0x4ac274 -- distribute a critical hit across the contained critical
// subsystems.  Drives the status alarm through level 2 then 1, pins structure
// to 1.0, then iterates the critical-subsystem chain (ClassID 0x4E entries),
// divides the incoming damage amount by the critical count, and re-applies the
// scaled damage to each, logging "ammo explosion damaging " per hit.
//
void
	MechSubsystem::DistributeCriticalHit(Damage &damage)
{
	statusAlarm.SetLevel(2);  if (printSimulationState) OnAlarmChanged();
	statusAlarm.SetLevel(1);  if (printSimulationState) OnAlarmChanged();
	damageZone->structureLevel = 1.0f;

	CriticalChain criticals(this);						// FUN_004acfa9
	int count = criticals.CountOfType(0x4E);			// entries whose +4 == 0x4E
	damage.damageAmount /= (Scalar)count;						// per-critical share

	for (CriticalEntry *c = criticals.First(); c != 0; c = criticals.Next())
	{
		DebugStream << "ammo explosion damaging " << c->Subsystem()->GetName();	// 0050df61
		c->Subsystem()->TakeDamage(damage);				// (*owner.vtable+0xc)(...)
	}
}

//
// @0x4ac194 -- map a status-state name to its TechStatusType value via the
// table @0x50de74.  Returns False if not found.
//
Logical
	MechSubsystem::LookupStatusType(const char *name, int *out_value)
{
	for (const StatusNameEntry *e = StatusNameTable; e->name != 0; ++e)	// PTR @0x50de74
	{
		if (Strcmp(name, e->name) == 0)				// FUN_004d4b58
		{
			*out_value = e->value;
			return True;
		}
	}
	return False;
}


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- MechSubsystem  (@0x4ac9ec)
//
// Stamps classID 0xBBB / model size 0xE4 and parses the damage/critical fields
// on top of the base Subsystem record.
//
int
	MechSubsystem::CreateStreamedSubsystem(
		NotationFile				*model_file,
		const char					*model_name,
		const char					*subsystem_name,
		SubsystemResource			*subsystem_resource,
		NotationFile				*subsystem_file,
		const ResourceDirectories	*directories,
		int							passes
	)
{
	if (!Subsystem::CreateStreamedSubsystem(			// FUN_0041c684
			model_file, model_name, subsystem_name,
			subsystem_resource, subsystem_file, directories))
	{
		return False;
	}

	subsystem_resource->subsystemModelSize	= 0xE4;								// res+0x24
	subsystem_resource->classID = (RegisteredClass::ClassID)0x0BBB;	// MechSubsystemClassID, res+0x20

	if (passes == 1)
	{
		// first pass: prime to "unset"
		subsystem_resource->criticalReference	= ResourceUnset;	// res+0xe0 = -1.0f
		for (int i = 0; i < 5; ++i)
			subsystem_resource->armorByFacing[i] = ResourceUnset;	// res+0x30.. = -1.0f
		subsystem_resource->structureReference	= ResourceUnset;	// res+0x44
		subsystem_resource->vitalSubsystemIndex	= -1;				// res+0x48
		memset(subsystem_resource->videoObjectName, 0, 128);		// res+0x4c
		strcpy(subsystem_resource->videoObjectName, "None");		// DAT_0050dfc0
		subsystem_resource->alarmModel = 0;							// identity ResourceID						// res+0xcc
		subsystem_resource->printSimulationState	= 0;			// res+0xd8
		subsystem_resource->collisionCriticalHitWeight = 0;			// res+0xdc
	}

	model_file->GetEntry(subsystem_name, "PrintSimulationState",
		&subsystem_resource->printSimulationState);					// 0050dfc5
	model_file->GetEntry(subsystem_name, "CollisionCriticalHitWeight",
		&subsystem_resource->collisionCriticalHitWeight);			// 0050dfda

	const char *videoName = "Unspecified";							// 0050dff5
	model_file->GetEntry(subsystem_name, "VideoObjectName", &videoName);	// 0050e001
	if (strcmp(videoName, "Unspecified") != 0)						// 0050e011
	{
		strcpy(subsystem_resource->videoObjectName, videoName);		// res+0x4c
	}

	const char *vitalName = "Unspecified";							// 0050e01d
	if (!model_file->GetEntry(subsystem_name, "VitalSubsystem", &vitalName)	// 0050e029
		&& subsystem_resource->vitalSubsystemIndex == -1)
	{
		// VitalSubsystem optional; resolved to a segment index when present
	}
	if (strcmp(vitalName, "Unspecified") != 0)
	{
		subsystem_resource->vitalSubsystemIndex =
			Get_Segment_Index(model_file, model_name, directories, vitalName);				// FUN_004274f8
	}

	Check_Fpu();
	return True;
}
