//===========================================================================//
// File:	heat.cpp                                                            //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Heatable subsystems -- temperature model, heat sinks, condenser  //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/95  ??   Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary.  Behaviour follows the Ghidra
// pseudo-C in heat_cluster.c; method names / member names follow HEAT.TCP
// and the sibling subsystem sources (gauss.cpp, sensor.hpp, ppc.hpp).
// Each non-trivial method cites the originating @ADDR.  Hex float constants
// have been converted to decimal:
//      0x3f800000 = 1.0f   0x3f000000 = 0.5f   0x3ecccccd = 0.4f
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_004ac644  HeatableSubsystem base constructor
//      FUN_0041c648  Subsystem destructor
//      FUN_004ac22c  Subsystem::ResetToInitialState
//      FUN_004ac144  Subsystem::GetStatusFlags
//      FUN_004ac1d4  Subsystem::HandleMessage
//      FUN_004ac0bc  Subsystem (slot 9 base impl)
//      FUN_004ac8c0  Subsystem::PrintState
//      FUN_0043ad4f  FilteredScalar::Initialize(count,value)
//      FUN_0043ade4  FilteredScalar::AddSample(value)
//      FUN_0043ae0b  FilteredScalar::Average()
//      FUN_00417ab4  SharedData::Resolve() -> linked HeatSink*
//      FUN_0041b9ec  AlarmIndicator(levels)
//      FUN_0041bbd8  AlarmIndicator::SetLevel(n)
//      FUN_004dca38  expf()      FUN_004dcd00  fabsf()
//      FUN_0040385c  Verify()/assert(msg,file,line)
//      FUN_0041a1a4  IsDerivedFrom(classDerivations)
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(HEAT_HPP)
#	include <heat.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
#	include <testbt.hpp>
#endif

#define JM_CLOSE_ENOUGH	0.0005f

//
// Tuning constants RECOVERED from the shipped image (.data / inline literal
// pool addresses below; raw bytes read from decomp/recovered/section_dump.txt).
//      _DAT_004ad870 : 80-bit extended `3b df 4f 8d 97 6e 12 83 f6 3f`
//                      = 1.023992 x 2^-9 == 0.002f  (heat-load normalising scale:
//                      radiatedHeat ~= currentTemp*coolant ~= 300 -> band [0,1]).
//      _DAT_004ad87c : 00 00 00 00       == 0.0f
//      _DAT_004ad880 : 00 00 80 3f       == 1.0f
//      _DAT_004ada90 : 00 00 80 3f       == 1.0f   (the "1 - exp(...)" unit term)
//      _DAT_004adbf4 : 17 b7 d1 38       == 1.0e-4f (heat-equalise threshold)
//      _DAT_004adcfc : 17 b7 d1 38       == 1.0e-4f (coolant-draw threshold)
//
static const Scalar HeatLoadScale   = 0.002f;	// _DAT_004ad870 (80-bit extended)
static const Scalar HeatLoadMinimum = 0.0f;		// _DAT_004ad87c
static const Scalar HeatLoadMaximum = 1.0f;		// _DAT_004ad880
static const Scalar HeatEqualizeEpsilon = 1.0e-4f;	// _DAT_004adbf4
static const Scalar CoolantDrawEpsilon  = 1.0e-4f;	// _DAT_004adcfc / 0050e3d8 / 0050e3d4


//###########################################################################
//###########################################################################
//					HeatableSubsystem
//###########################################################################
//###########################################################################

//#############################################################################
// Shared Data Support
//
HeatableSubsystem::SharedData
	HeatableSubsystem::DefaultData(
		HeatableSubsystem::GetClassDerivations(),
		HeatableSubsystem::GetMessageHandlers(),
		HeatableSubsystem::GetAttributeIndex(),
		HeatableSubsystem::StateCount
	);

Derivation*
	HeatableSubsystem::GetClassDerivations()
	{
		static Derivation classDerivations(Subsystem::GetClassDerivations(), "HeatableSubsystem");
		return &classDerivations;
	}


//#############################################################################
// Construction / Destruction
//
// @004ac644 (base ctor body lies below the captured decomp window).
//
HeatableSubsystem::HeatableSubsystem(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	SharedData			&shared_data
):
	MechSubsystem(owner, subsystem_ID,
		(MechSubsystem::SubsystemResource *)subsystem_resource, shared_data)
{
	Check(owner);
	Check_Pointer(subsystem_resource);
	// owner + simulationState are set by the MechSubsystem base ctor; the old
	// flags/statusFlags/statusBits/destroyed were shadows of base state (removed).
	ResetToInitialState(True);
	Check_Fpu();
}

//
// @004ac868 -- releases the shared model object and the ref-counted resource
// then chains to ~Subsystem.
//
HeatableSubsystem::~HeatableSubsystem()
{
	Check(this);
	Check_Fpu();
}

//###########################################################################
//					ResetToInitialState -- HeatableSubsystem
//
// HEAT.TCP ground truth.
//
void
	HeatableSubsystem::ResetToInitialState(Logical /*powered*/)
{
	currentTemperature = 300.0f;
	heatLoad           = 0.0f;
}

//###########################################################################
//					TestClass -- HeatableSubsystem
//
// HEAT.TCP ground truth.
//
Logical
	HeatableSubsystem::TestClass(Mech &)
{
	return True;
}

Logical
	HeatableSubsystem::TestInstance() const
{
	return IsDerivedFrom(*GetClassDerivations());
}

//
// Base damageable-subsystem virtual surface.  The engine `Subsystem` does not
// carry these; the heat/power families override them.  Base implementations
// are the neutral defaults the recovered overrides chain into.
//
LWord
	HeatableSubsystem::GetStatusFlags()			{ return 0; }
Logical
	HeatableSubsystem::HandleMessage(int)		{ return False; }
void
	HeatableSubsystem::PrintState()				{}
void
	HeatableSubsystem::Simulation(Scalar)		{}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- HeatableSubsystem
//
// @004ac9ec parses the base "damageable subsystem" record (WeaponDamagePoints,
// Collision/Ballistic/Explosive/Laser/EnergyDamagePoints, VitalSubsystem,
// SiteOffset, VideoObjectName, PrintSimulationState, CriticalHitScoreBonus).
// That logic belongs to the shared damageable base; reproduced here in
// summary form -- a human should fold it back into the proper base class.
//
int
	HeatableSubsystem::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(
			model_file, model_name, subsystem_name,
			subsystem_resource, subsystem_file, directories
		)
	)
	{
		return False;
	}

	subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);

	// TODO: verify against @004ac9ec -- parses damage-point fields, the
	// "VitalSubsystem" True/False flag, "SiteOffset", "VideoObjectName",
	// "PrintSimulationState" and "CriticalHitScoreBonus" and validates that
	// WeaponDamagePoints / CriticalHitScoreBonus are present.

	Check_Fpu();
	return True;
}


//###########################################################################
//###########################################################################
//					Condenser
//###########################################################################
//###########################################################################
//
// NOTE: the shipped Condenser implementation lies past the captured decomp
// window (@0x4ae1xx onward).  Bodies below are reconstructed from HEAT.TCP
// plus the standard subsystem pattern and are BEST-EFFORT.
//

//#############################################################################
// Shared Data Support
//
Condenser::SharedData
	Condenser::DefaultData(
		Condenser::GetClassDerivations(),
		Condenser::GetMessageHandlers(),
		Condenser::GetAttributeIndex(),
		Condenser::StateCount
	);

Derivation*
	Condenser::GetClassDerivations()
	{
		static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "Condenser");
		return &classDerivations;
	}

// ~~~ Condenser ctor/dtor: the REAL bodies live in heatfamily_reslice.cpp
// (@4ae568, which sets valveState=1 / refrigerationFactor / condenserNumber).
// These STUBs were ODR-duplicates that WON under /FORCE (heat.obj links before
// heatfamily_reslice.obj) -> valveState was left 0xCDCDCDCD (garbage valve gauge).
// #if 0'd so the real ctor is the sole definition.  DefaultData / GetClassDerivations
// / ResetToInitialState below are NOT duplicated in the reslice TU -- kept here.
#if 0
Condenser::Condenser(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	SharedData			&shared_data
):
	HeatSink(owner, subsystem_ID, subsystem_resource, shared_data)
{
	Check(owner);
	Check_Pointer(subsystem_resource);
	Check_Fpu();
}

Condenser::~Condenser()
{
	Check(this);
	Check_Fpu();
}
#endif

//###########################################################################
//					ResetToInitialState -- Condenser  (HEAT.TCP)
//
void
	Condenser::ResetToInitialState(Logical /*powered*/)
{
	HeatableSubsystem::ResetToInitialState(True);
}

//###########################################################################
//					TestClass / TestInstance / CreateStreamedSubsystem -- Condenser
//
// The REAL bodies live in heatfamily_reslice.cpp (@4ae63c / @4ae658, the latter
// parsing "RefrigerationFactor").  These STUBs were ODR-duplicates -- #if 0'd so
// the reslice definitions are the sole ones (see the ctor note above).
//
#if 0
Logical
	Condenser::TestClass(Mech &)
{
	return True;
}

Logical
	Condenser::TestInstance() const
{
	return IsDerivedFrom(*GetClassDerivations());
}

int
	Condenser::CreateStreamedSubsystem(
		NotationFile				*model_file,
		const char					*model_name,
		const char					*subsystem_name,
		SubsystemResource			*subsystem_resource,
		NotationFile				*subsystem_file,
		const ResourceDirectories	*directories,
		int							passes
	)
{
	// TODO: verify -- decomp for the Condenser parser was not captured.
	if (
		!HeatableSubsystem::CreateStreamedSubsystem(
			model_file, model_name, subsystem_name,
			subsystem_resource, subsystem_file, directories, passes
		)
	)
	{
		return False;
	}
	subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);
	Check_Fpu();
	return True;
}
#endif


//###########################################################################
//###########################################################################
//					HeatSink
//###########################################################################
//###########################################################################

//#############################################################################
// Attribute Support
//
// The named attributes the cockpit gauges bind to (config content/GAUGE/L4GAUGE.CFG:
// HeatSink/CoolantMass, HeatSink/CoolantCapacity, HeatSink/CurrentTemperature).
// Chained to the parent index so SimulationState (id 1) is preserved; the ids run
// contiguously from HeatableSubsystem::NextAttributeID so the built index stays dense
// (AttributeIndexSet::Find strcmps every slot -- a gap would read a garbage name).
//
const HeatSink::IndexEntry
	HeatSink::AttributePointers[]=
{
	ATTRIBUTE_ENTRY(HeatSink, CoolantMass,        coolantLevel),		// @0x12C (live: UpdateCoolant depletes it)
	ATTRIBUTE_ENTRY(HeatSink, CoolantCapacity,    thermalCapacity),	// @0x128
	ATTRIBUTE_ENTRY(HeatSink, CurrentTemperature, currentTemperature),	// @0x114 (inherited from HeatableSubsystem)
	// --- gauge data-binding wave: the config's GenericHeatGauges vertBar binds
	// Condenser<N>/DegradationTemperature (@4=warn) + FailureTemperature (@5=max);
	// without these the two-part temp bar's warn/max endpoints were NULL -> the
	// bar could not scale (division by a zero max).  Condenser/Reservoir inherit
	// this table, so publishing here covers all 6 condensers + both coolant banks.
	ATTRIBUTE_ENTRY(HeatSink, DegradationTemperature, degradationTemperature),	// @0x118 (constant warn threshold)
	ATTRIBUTE_ENTRY(HeatSink, FailureTemperature,     failureTemperature),		// @0x11C (constant max threshold)
	ATTRIBUTE_ENTRY(HeatSink, NormalizedPressure,     heatLoad),				// @0x120 (smoothed radiated heat)
	ATTRIBUTE_ENTRY(HeatSink, DegradationPressure,    coolantEfficiency),		// @0x124
	ATTRIBUTE_ENTRY(HeatSink, CoolantMassLeakRate,    coolantDraw),				// @0x130 (LeakGauge; damage-driven)
	ATTRIBUTE_ENTRY(HeatSink, HeatSink,               linkedSinks),				// @0x164 (Eng linked-sink temp readout)
	ATTRIBUTE_ENTRY(HeatSink, ValveSetting,           coolantFlowScale)			// @0x15C (condenser valve slider @2; init 1.0f)
};

HeatSink::AttributeIndexSet&
	HeatSink::GetAttributeIndex()
{
	static HeatSink::AttributeIndexSet attributeIndex(
		ELEMENTS(HeatSink::AttributePointers),
		HeatSink::AttributePointers,
		HeatableSubsystem::GetAttributeIndex()
	);
	return attributeIndex;
}

//#############################################################################
// Shared Data Support
//
HeatSink::SharedData
	HeatSink::DefaultData(
		HeatSink::GetClassDerivations(),
		HeatSink::GetMessageHandlers(),
		HeatSink::GetAttributeIndex(),
		HeatSink::StateCount
	);

Derivation*
	HeatSink::GetClassDerivations()
	{
		static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "HeatSink");
		return &classDerivations;
	}


//#############################################################################
// Construction / Destruction
//
// @004adda0  (the one function tagged file=bt/heat.cpp in the decomp).
//
HeatSink::HeatSink(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	SharedData			&shared_data
):
	HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data),
	linkedSinks(),
	heatAlarm(3)							// FUN_0041b9ec(...,3) -- 3 alarm levels
{
	// heatState/heatModelFlag/field_1d0 deleted: the heat-state code lives INSIDE
	// heatAlarm (+0x14 == subsystem+0x184, so heatAlarm.GetLevel()); heatModelFlag was
	// a spurious duplicate of coolantActive@0x138; field_1d0 was a spurious tail slot.
	Check(owner);
	Check_Pointer(subsystem_resource);

	currentTemperature		= subsystem_resource->startingTemperature;		// +0xE4
	degradationTemperature	= subsystem_resource->degradationTemperature;	// +0xE8
	failureTemperature		= subsystem_resource->failureTemperature;		// +0xEC

	heatLoad				= 0.0f;
	coolantEfficiency		= 0.5f;
	thermalCapacity			= 1.0f;
	coolantLevel			= thermalCapacity;
	coolantDraw				= 0.0f;
	coolantAvailable		= 1;
	coolantActive			= 0;

	startingTemperature		= currentTemperature;
	thermalConductance		= subsystem_resource->thermalConductance;		// +0xF0

	heatFilter.Initialize(15, 0.0f);	// FUN_0043ad4f(this+0x144, 0xF, 0)
	filterDecay				= 0.4f;
	thermalMass				= subsystem_resource->thermalMass;				// +0xF4
	heatEnergy				= thermalMass * startingTemperature;
	coolantFlowScale		= 1.0f;
	massScale				= 1.0f;

	pendingHeat				= 0.0f;

	//
	// A "master" heat sink (segment flagged 0x100, not a sub-/damaged copy)
	// drives the per-frame thermal simulation.
	//
	// INTEGRATION (gate reconcile): the binary master-gate reads the OWNER's
	// simulationFlags (param_2+0x28), NOT the per-segment resource flags.  The
	// recovered C across all subsystem ctors is uniformly
	//   (*(uint*)(param_2+0x28) & 0xc)==0 && (*(uint*)(param_2+0x28) & 0x100)!=0
	// (owner+0x28), which is the authoritative path the working AmmoBin/projweap
	// gate already used.  Reading subsystem_resource->subsystemFlags here was a
	// reconstruction mis-attribution (that field streams 0 in our data → the gate
	// never armed → HeatSinkSimulation was never installed → the sink decayed to
	// DoNothingOnce after frame 1).  The SAME correction applies to the linked-sink
	// block below: the raw decomp of @004adda0 reads the OWNER flags (param_2+0x28)
	// there too (lines 47/56), NOT the per-segment resource flags.  With subsystemFlags
	// streaming 0 the Add-gate was dead → linkedSinks empty → weapon/subsystem heat
	// never conducted to its designated sink → the mech never heated.  Fixed to owner
	// flags to match the binary; the per-subsystem TARGET is still heatSinkIndex.
	if (
		(owner->simulationFlags & SegmentCopyMask) == 0		// (owner flags & 0xC) == 0
		&& (owner->simulationFlags & MasterHeatSinkFlag) != 0	// owner flags & 0x100
	)
	{
		SetPerformance(&HeatSink::HeatSinkSimulation);		// this[7..9] = &HeatSinkSimulation
	}

	resource = subsystem_resource;

	//
	// Resolve and attach the linked heat sink referenced by heatSinkIndex.
	// (Skipped for "Condenser"-class segments -- see @0041a1a4 guard.)
	//
	if (!IsDerivedFrom(*Condenser::GetClassDerivations()))		// FUN_0041a1a4(*this[3], 0x50e590)
	{
		// ⚠ ROOT-CAUSE FIX (the BGF-load heap corruption): heatSinkIndex indexes the
		// owner's SUBSYSTEM ROSTER, NOT the skeleton segment table.  Raw @004adda0
		// (part_012.c:16999):  if (res->heatSinkIndex < owner->subsystemCount /*+0x124*/)
		//     linked = owner->subsystemArray[heatSinkIndex] /*+0x128*/;
		// The earlier draft resolved it via owner->GetSegment(index) — an EntitySegment
		// (288 bytes) cast to HeatSink* — so every per-frame ConductHeat/BalanceCoolant
		// wrote pendingHeat/coolantLevel 100/20 bytes PAST that block: thousands of OOB
		// heap writes during fire, detected later at an unrelated free (bld08.bgf load).
		// subsystemArray is zeroed up front (mech.cpp), so a not-yet-built roster slot
		// reads NULL -> the binary's "missing" warn path, exactly as the oracle.
		Subsystem *linked = 0;
		if (subsystem_resource->heatSinkIndex >= 0
			&& subsystem_resource->heatSinkIndex < owner->GetSubsystemCount())
		{
			linked = owner->GetSubsystem(subsystem_resource->heatSinkIndex);
		}
		else
		{
			// @004adda0: "Bad subsystem resource ->heatSink"  HEAT.CPP:0x25F
			Verify(False, "Bad subsystem resource ->heatSink", __FILE__, 0x25F);
		}

		if (
			(owner->simulationFlags & SegmentCopyMask) == 0		// param_2+0x28 & 0xc == 0
			&& (owner->simulationFlags & MasterHeatSinkFlag) != 0	// param_2+0x28 & 0x100
		)
		{
			if (linked == 0)
			{
				// HEAT.CPP:0x26B
				Verify(False, "Master heatable subsystem is missing", __FILE__, 0x26B);
			}
			else
			{
				linkedSinks.Add(linked);	// (**(this[0x59]+4))(this+0x59, linked)
			}
		}
		else if (
			(owner->simulationFlags & SegmentCopyMask) == 4		// param_2+0x28 & 0xc == 4
			&& linked != 0
		)
		{
			linkedSinks.Add(linked);
		}
	}

	UpdateHeatLoad();					// FUN_004ad7f0
	Check_Fpu();
}

//
// @004adfd4
//
HeatSink::~HeatSink()
{
	Check(this);
	// members (heatAlarm @0x5C, linkedSinks @0x59, heatFilter @0x51)
	// are torn down by their own destructors; base chain handles the rest.
	Check_Fpu();
}

Logical
	HeatSink::TestInstance() const
{
	// @004ae034 -> FUN_0041a1a4(**this[3], 0x50e3ec)
	return IsDerivedFrom(*GetClassDerivations());
}

//###########################################################################
//					TestClass -- HeatSink  (HEAT.TCP)
//
Logical
	HeatSink::TestClass(Mech &)
{
	return True;
}

//###########################################################################
//					ResetToInitialState -- HeatSink
//
// @004ad760
//
void
	HeatSink::ResetToInitialState(Logical /*powered*/)
{
	currentTemperature	= startingTemperature;
	heatEnergy			= startingTemperature * thermalMass;
	coolantLevel		= thermalCapacity;
	coolantDraw			= 0.0f;
	coolantAvailable	= 1;
	coolantFlowScale	= 1.0f;

	ClearHeatFilter();		// FUN_004ad884
	UpdateHeatLoad();		// FUN_004ad7f0
	coolantActive		= 0;

	HeatableSubsystem::ResetToInitialState(True);	// FUN_004ac22c
}


//#############################################################################
// Per-frame simulation
//

//
// @004ad924 -- the registered Performance for a master heat sink.
//
void
	HeatSink::HeatSinkSimulation(Scalar time_slice)
{
	Check(this);

	if (HeatModelActive())				// FUN_004ad7d4 (entity heat-model flag)
	{
		heatEnergy         += pendingHeat;
		currentTemperature  = heatEnergy / thermalMass;
		UpdateHeatLoad();

		// BRING-UP verify (1 Hz, viewpoint mech only): show the mech heat climbing as
		// weapons fire and relaxing when they stop.  pendingHeat just absorbed above.
		static Scalar s_heatLog = 0.0f;
		s_heatLog += time_slice;
		if (s_heatLog >= 1.0f && application != 0
			&& (Entity *)owner == application->GetViewpointEntity() && pendingHeat > 0.0f)
		{
			s_heatLog = 0.0f;
			DEBUG_STREAM << "[heat] sink temp=" << currentTemperature
				<< " heatEnergy=" << heatEnergy << " absorbed=" << pendingHeat
				<< " heatLoad=" << heatLoad << "\n" << std::flush;
		}

		pendingHeat         = 0.0f;
		ConductHeat(time_slice);		// FUN_004ad8ac
	}

	if (HeatModelActive())
	{
		UpdateCoolant(time_slice);		// FUN_004adbf8
	}

	//
	// Drive the degradation / failure alarm.
	//
	if (currentTemperature > failureTemperature)
	{
		heatAlarm.SetLevel(FailureHeat);		// FUN_0041bbd8(this+0x5C, 2)
	}
	else if (currentTemperature > degradationTemperature)
	{
		heatAlarm.SetLevel(DegradationHeat);	// 1
	}
	else
	{
		heatAlarm.SetLevel(NormalHeat);			// 0
	}

	Check_Fpu();
}

//
// @004ad7f0 -- recompute the radiated/instantaneous heat and feed it through
// the running-average filter to produce the smoothed heatLoad reading.
//
void
	HeatSink::UpdateHeatLoad()
{
	radiatedHeat = currentTemperature * coolantLevel;

	Scalar sample = HeatLoadScale * radiatedHeat;
	if (sample < HeatLoadMinimum)
	{
		sample = HeatLoadMinimum;
	}
	else if (sample > HeatLoadMaximum)
	{
		sample = HeatLoadMaximum;
	}

	heatFilter.AddSample(sample);		// FUN_0043ade4
	heatLoad = heatFilter.Average();	// FUN_0043ae0b
}

//
// @004ad884 -- flush the 15-sample filter back to zero.
//
void
	HeatSink::ClearHeatFilter()
{
	for (int i = 0; i < 15; ++i)
	{
		heatFilter.AddSample(0.0f);
	}
}

//
// @004ad8ac -- conduct heat into the linked sink and rebalance coolant.
//
void
	HeatSink::ConductHeat(Scalar time_slice)
{
	HeatSink *other = (HeatSink *)linkedSinks.Resolve();	// FUN_00417ab4(this+0x164)
	if (other != 0 && coolantAvailable != 0)
	{
		Scalar flow = ComputeHeatFlow(other, time_slice);	// FUN_004ad9ec
		other->pendingHeat += flow;
		pendingHeat        -= flow;
		BalanceCoolant(time_slice);							// FUN_004ada94
	}
}

//
// @004ad9ec -- conductive heat-exchange between this sink and 'other'.
//
//   tau   = thermalMass / massScale
//   denom = tau + other->thermalMass
//   q     = (currentTemperature*massScale
//             - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom)
//           * tau
//           * (1 - exp( -dt * thermalConductance
//                          * (coolantLevel / thermalCapacity)
//                          * coolantFlowScale / denom ))
//
Scalar
	HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice)
{
	Scalar tau   = thermalMass / massScale;					// this+0x154 / this+0x160
	Scalar denom = tau + other->thermalMass;				// + other+0x154

	Scalar exponent =
		(-time_slice
			* thermalConductance						// this+0x140
			* (coolantLevel / thermalCapacity)			// this+0x12C / this+0x128
			* coolantFlowScale)							// this+0x15C
		/ denom;

	Scalar decay = expf(exponent);						// FUN_004dca38

	return (currentTemperature * massScale
				- (other->heatEnergy + other->pendingHeat + heatEnergy) / denom)
			* tau
			* (1.0f - decay);							// _DAT_004ada90 == 1.0f
}

//
// @004ada94 -- move coolant between this sink and its linked sink so that the
// hotter side sheds load.  Clamped on both ends so neither sink goes below 0
// or above its capacity.
//
void
	HeatSink::BalanceCoolant(Scalar time_slice)
{
	HeatSink *other = (HeatSink *)linkedSinks.Resolve();	// FUN_00417ab4(this+0x164)
	if (other == 0)
	{
		return;
	}

	if (fabsf(radiatedHeat - other->radiatedHeat) <= HeatEqualizeEpsilon)	// FUN_004dcd00
	{
		return;
	}

	Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel;

	// clamp delta to +/- (thermalCapacity * dt)
	Scalar limit = thermalCapacity * time_slice;
	if (delta < -limit)
	{
		delta = -limit;
	}
	else if (delta > limit)
	{
		delta = limit;
	}

	// clamp so this sink stays within [0, thermalCapacity]
	Scalar hi = thermalCapacity - coolantLevel;
	Scalar lo = -coolantLevel;
	if (delta < lo)			delta = lo;
	else if (delta > hi)	delta = hi;

	// clamp so the other sink stays within its own [0, thermalCapacity]
	Scalar otherLo = -(other->thermalCapacity - other->coolantLevel);
	if (delta < otherLo)				delta = otherLo;
	else if (delta > other->coolantLevel)	delta = other->coolantLevel;

	delta = coolantFlowScale * delta;
	coolantLevel        += delta;
	other->coolantLevel -= delta;
}

//
// @004adbf8 -- consume coolant proportional to current load, request a
// top-up from the central cooling system, and update the draw state machine.
//
void
	HeatSink::UpdateCoolant(Scalar time_slice)
{
	// AUTHENTIC (heatmodel decode, FUN_004adbf8): the binary reads *(this[0x38]+0x158)
	// = this subsystem's OWN DamageZone.damageLevel (the engine base zone at @0xE0,
	// word 0x38), NOT linkedSinks->heatEnergy.  The earlier reconstruction confused
	// word 0x38/@0xE0 (the DamageZone) with linkedSinks@0x164, so when the link
	// resolved (heatEnergy ~1.3e7) coolantDraw became ~2e6 and would SLAM coolantLevel
	// to empty every frame -- the opposite of the authentic near-static behavior.
	// An undamaged subsystem has damageLevel 0 -> coolantDraw 0 -> NO leak (the coolant
	// bars stay full on a pristine mech); the draw rises only as the heat sink /
	// condenser itself takes battle damage.  (Read the qualified engine zone -- the
	// nearer MechSubsystem::damageZone is a shim SHADOW; see mechweap.cpp:252.)
	::DamageZone *ownZone = this->Subsystem::damageZone;	// @0xE0 (word 0x38)
	Scalar zoneDamage = (ownZone != 0) ? ownZone->damageLevel : 0.0f;	// +0x158
	coolantDraw = zoneDamage * heatLoad;					// *(this[0x38]+0x158) * this[0x48]
	if (coolantDraw < CoolantDrawEpsilon)
	{
		coolantDraw = 0.0f;
	}

	Scalar amount = coolantDraw * time_slice;
	if (coolantLevel < amount)
	{
		amount = coolantLevel;
	}
	coolantLevel -= amount;

	if (fabsf(amount) > CoolantDrawEpsilon)
	{
		coolantLevel += DrawCoolant(amount);	// virtual: (**(*this+0x38))(this, amount)
	}

	//
	// Draw state machine (this+0x138).
	//
	if (coolantActive == 0 && coolantDraw > CoolantDrawEpsilon)
	{
		coolantActive = 1;
	}
	else if (coolantActive == 1 && coolantDraw < CoolantDrawEpsilon)
	{
		coolantActive = 0;
	}
}

//
// vtable slot 14 (@vtable+0x38).  Base sinks supply nothing on their own;
// a central-cooling-system subclass overrides this.
// TODO: verify against the real slot-14 target (not captured in decomp).
//
Scalar
	HeatSink::DrawCoolant(Scalar /*requested*/)
{
	return 0.0f;
}


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

//
// @004add30 -- base flags, plus heat-active / coolant bits.
//
LWord
	HeatSink::GetStatusFlags()
{
	LWord flags = HeatableSubsystem::GetStatusFlags();		// FUN_004ac144

	if (heatAlarm.GetLevel() != 0)					// this+0x184 (heat-state Normal/Deg/Fail)
	{
		flags |= 0x8;
	}
	if (coolantActive != 0							// this+0x138
		&& HeatModelActive())						// FUN_004ad7d4
	{
		flags |= 0x4;
	}
	return flags;
}

//
// @004add6c -- message 1 forces the linked master's heatEnergy to a known
// value; everything else falls through to the base handler.
//
Logical
	HeatSink::HandleMessage(int message)
{
	if (message == 1)
	{
		// @004add6c:16948  *(this[0x38]+0x158) = 0.5  ==  damageZone->damageLevel = 0.5
		//   this[0x38] is the inherited MechSubsystem damageZone (@0xE0), NOT linkedSinks
		//   (@0x164): a message-driven "set this sink to half structural damage".  The
		//   prior read targeted the linked master's heatEnergy -- wrong object + field.
		SetSubsystemDamageLevel(0.5f);
		return True;
	}
	return HeatableSubsystem::HandleMessage(message);		// FUN_004ac1d4
}

//
// @004ae050 -- prints "<name> NormalHeat | DegradationHeat | FailureHeat".
//
void
	HeatSink::PrintState()
{
	HeatableSubsystem::PrintState();						// FUN_004ac8c0

	switch (heatAlarm.GetLevel())					// this+0x184 (heat-state field, inside heatAlarm)
	{
	case NormalHeat:
		DebugStream << GetName() << " NormalHeat" << endl;
		break;
	case DegradationHeat:
		DebugStream << GetName() << " DegradationHeat" << endl;
		break;
	case FailureHeat:
		DebugStream << GetName() << " FailureHeat" << endl;
		break;
	default:
		DebugStream << GetName() << " Unknown Heat State!" << endl;
		break;
	}
}


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- HeatSink
//
// @004ae150
//
int
	HeatSink::CreateStreamedSubsystem(
		NotationFile				*model_file,
		const char					*model_name,
		const char					*subsystem_name,
		SubsystemResource			*subsystem_resource,
		NotationFile				*subsystem_file,
		const ResourceDirectories	*directories,
		int							passes
	)
{
	if (
		!HeatableSubsystem::CreateStreamedSubsystem(	// FUN_004ac9ec
			model_file, model_name, subsystem_name,
			subsystem_resource, subsystem_file, directories, passes
		)
	)
	{
		return False;
	}

	subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);
	subsystem_resource->classID            = RegisteredClass::HeatSinkClassID;

	if (passes == 1)
	{
		// first pass: prime all heat fields to "unset" (-1.0f / -1)
		subsystem_resource->startingTemperature    = -1.0f;
		subsystem_resource->degradationTemperature = -1.0f;
		subsystem_resource->failureTemperature     = -1.0f;
		subsystem_resource->thermalConductance     = -1.0f;
		subsystem_resource->thermalMass            = -1.0f;
		subsystem_resource->heatSinkIndex          = -1;
	}

	if (
		!model_file->GetEntry(subsystem_name, "StartingTemperature",
			&subsystem_resource->startingTemperature)
		&& subsystem_resource->startingTemperature == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing StartingTemperature!";
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "DegradationTemperature",
			&subsystem_resource->degradationTemperature)
		&& subsystem_resource->degradationTemperature == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing DegradationTemperature!";
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "FailureTemperature",
			&subsystem_resource->failureTemperature)
		&& subsystem_resource->failureTemperature == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing FailureTemperature!";
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "ThermalConductance",
			&subsystem_resource->thermalConductance)
		&& subsystem_resource->thermalConductance == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing ThermalConductance!";
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "ThermalMass",
			&subsystem_resource->thermalMass)
		&& subsystem_resource->thermalMass == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing ThermalMass!";
		return False;
	}

	//
	// "HeatSink" names the segment that this sink links to.  Resolve the
	// name to a segment index (biased by +2 to leave room for sentinels).
	//
	const char *heatSinkName = "Unspecified";
	int found = model_file->GetEntry(subsystem_name, "HeatSink", &heatSinkName);
	if (!found && subsystem_resource->heatSinkIndex == -1)
	{
		DebugStream << subsystem_name << " missing HeatSink!";
		return False;
	}

	if (strcmp(heatSinkName, "Unspecified") != 0)
	{
		subsystem_resource->heatSinkIndex =
			Get_Segment_Index(model_file, model_name, directories, heatSinkName);	// FUN_004215b0
	}

	if (subsystem_resource->heatSinkIndex < 0)
	{
		DebugStream << subsystem_name << " has an invalid heat sink!";
		return False;
	}

	subsystem_resource->heatSinkIndex += 2;
	Check_Fpu();
	return True;
}

//===========================================================================//
// WAVE 2 factory bridges -- construct a real heat subsystem for mech.cpp's
// roster factory (which can't #include heat.hpp: its local RECON_SUBSYS stubs
// collide).  Keep the binary's alloc SIZE; the Check guard turns a sizeof
// overrun (placement-new heap corruption) into an immediate assert.
//===========================================================================//
Subsystem *CreateCondenserSubsystem(Mech *owner, int id, void *seg)
{
	Check(sizeof(Condenser) <= 0x230);
	return (Subsystem *) new (Memory::Allocate(0x230))
		Condenser(owner, id, (Condenser::SubsystemResource *)seg, Condenser::DefaultData);
}

// CreateHeatSinkBankSubsystem (0xBBE) now lives in heatfamily_reslice.cpp -- it
// builds the real AggregateHeatSink (which needs that TU's class definition).
