//===========================================================================//
// File:	dmgtable.cpp                                                        //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Damage lookup table -- layered / pie-slice hit-location selector //
//---------------------------------------------------------------------------//
//   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 for the cluster @0x49de14..@0x49f5fb.  No function in the cluster
// was linker-tagged (all file=?), so the module name "dmgtable.cpp" is
// inferred from CLASSMAP and the .tbl section keywords; the cluster was
// recovered via the DamageZone (mechdmg.cpp) adjacency, the vtables
// @0x50bd84/0x50bd90/0x50bd9c, and the assertion / format strings
// @0x50bc00..0x50bd00.  Each non-trivial method cites the originating @ADDR.
//
// Float constants converted to decimal:
//      _DAT_0049e520 = 0x3f800000 = 1.0f   (percentage-sum target)
//      _DAT_0049e72c = 0x40c90fdb = 6.2831853f = 2*PI  (angular wrap)
//      _DAT_0049e810 = 0x40c90fdb = 2*PI               (slice-angle span)
//      _DAT_0049e730 = 0.0f                            (angle floor)
//      _DAT_0049f369 = 0x38d1b717 ~= 1.0e-4f           (sort epsilon)
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_004178cc  Plug ctor                  FUN_0041790c Plug::Clear
//      FUN_00417858  Plug::Detach
//      FUN_00402298  operator new               FUN_004022d0 operator delete
//      FUN_004023f4  MemoryBlock/RefCount ctor   FUN_004024d8 RefCount release
//      FUN_00402460  TextString ctor
//      FUN_00408050  RandomUnit()  -> Scalar in [0,1)
//      FUN_004dcd10  (float)floor() setup        FUN_004dcd94 lrint()/truncate
//      FUN_004dcd00  fabsf()
//      FUN_004dd3ec  itoa(value, buf, radix)
//      FUN_004274f8  Get_Segment_Index(skeletonModel, zoneName)
//      FUN_00404088  NotationFile::Read(name,key,char**)
//      FUN_00404118  NotationFile::Read(name,key,float*)
//      FUN_00404190  NotationFile::Read(name,key,int*)
//      FUN_00404720  NotationFile::GetPage(name,key)
//      FUN_00403f84  NotationFile::PageExists(name)
//      FUN_004d7f84  strcmp() / format match     FUN_004d4b58 stricmp()
//      FUN_004dbb24  DebugStream::operator<<(char*)  FUN_004d9c38 endl
//      The TableOf<>/PlugOf<> template slots (ctor/dtor/iterator) live at
//      @0x49f204..@0x49f5b8 (vtables 0x50bd30/0x50bd50/0x50bd64) and are
//      engine template instantiations, not damage-table logic.
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(DMGTABLE_HPP)
#	include <dmgtable.hpp>
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>
#endif

//
// Tuning constants observed as read-only globals in the decomp.
//
static const Scalar PercentSumTarget	= 1.0f;			// _DAT_0049e520
static const Scalar TwoPi				= 6.2831853f;	// _DAT_0049e72c / _DAT_0049e810
static const Scalar SortEpsilon			= 1.0e-4f;		// _DAT_0049f369

// Per-entry byte layout inside the sorted TableOf<int> entry pool:
//      entry+0x0c   int    zone segment index   (payload returned by the roll)
//      entry+0x14   Scalar cumulative percentage (ascending sort key)


//###########################################################################
//###########################################################################
//					DamageZonePercentTable          (leaf)
//###########################################################################
//###########################################################################

//
// @0x49deb0 -- stream constructor.  Lays down the vtable @0x50bd9c, stores the
// owner (param_1[3]), allocates the entry-table memory block (param_1[4]),
// constructs the empty TableOf at +0x14 (FUN_0049f204), then rebuilds the
// entries from the object stream (ReadEntries @0x49e5e4).
//
DamageZonePercentTable::DamageZonePercentTable(
	Mech					*owner,
	GenericObjectStream		*stream
):
	Plug()								// FUN_004178cc(this, 2)
{
	this->owner = owner;				// param_1[3]
	refCount    = NewRefCount(0x10);	// param_1[4]
	entries.Init();						// FUN_0049f204(this+5, 0, 0)
	ReadEntries(stream);				// FUN_0049e5e4
}

//
// @0x49df80 -- detaches the entry table, releases the memory block, clears the
// Plug base.
//
DamageZonePercentTable::~DamageZonePercentTable()
{
	// *this = &PTR_FUN_0050bd9c; Detach(entries); release(refCount); Plug::Clear
}

Logical
	DamageZonePercentTable::TestInstance() const	// @0x49e000
{
	return True;
}

//
// @0x49de14 -- the weighted random roll (the "selector").  Iterates the entry
// table in ascending cumulative order, draws a single uniform sample in [0,1)
// and returns the zone index of the first entry whose cumulative percentage
// exceeds the draw.  -1 when the table is empty.
//
int
	DamageZonePercentTable::SelectZone() const
{
	int			selected = -1;
	Scalar		roll     = RandomUnit();				// FUN_00408050
	ReconTableIter it(entries);						// FUN_0040139b(.,this+0x14)

	it.First();										// slot+4
	for (void *entry = it.Current(); entry != 0 && selected == -1; entry = it.Current())
	{
		Scalar threshold = *(Scalar *)((char *)entry + 0x14);	// cumulative %
		if (roll < threshold)
		{
			selected = *(int *)((char *)entry + 0xc);			// zone segment index
		}
		it.Next();									// slot+0xc
		// it.Current() re-read at slot+0x30 (end test)
	}
	return selected;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Object-stream I/O
//
// @0x49e524 -- write count, then each entry as {cumulativePercent, zoneIndex}.
//
void
	DamageZonePercentTable::WriteEntries(GenericObjectStream *stream) const
{
	stream->Write(&entries.count, 4);
	ReconTableIter it(entries);
	for (void *entry = it.First(); entry != 0; entry = it.Next())
	{
		stream->Write((char *)entry + 0x14, 4);		// cumulative percentage
		stream->Write((char *)entry + 0x0c, 4);		// zone index
	}
}

//
// @0x49e5e4 -- read count, then that many {cumulativePercent, zoneIndex} pairs,
// allocating a TableEntry (FUN_00424716) per pair and inserting into the table.
//
void
	DamageZonePercentTable::ReadEntries(GenericObjectStream *stream)
{
	int n;
	stream->Read(&n, 4);
	for (int i = 0; i < n; ++i)
	{
		int		zoneIndex;
		Scalar	cumulative;
		stream->Read(&cumulative, 4);
		stream->Read(&zoneIndex,  4);
		entries.Insert(NewTableEntry(zoneIndex), &cumulative);	// FUN_00424716 + slot+8
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BuildFromNotation -- DamageZonePercentTable  (@0x49e00c)
//
// Parses a "DamageZone" page and validates that the percentages total 1.0.
//
int
	DamageZonePercentTable::BuildFromNotation(
		NotationFile			*model_file,
		const char				*model_name,
		const char				*page_name,
		NotationFile			*table_file,
		Mech					*owner,
		GenericObjectStream		*stream
	)
{
	if (!model_file->PageExists(page_name))			// FUN_00403f84
	{
		DebugStream << page_name << " does not exist in " << model_file->GetFileName();
		return False;
	}

	DamageZonePercentTable	table;
	NotationLine			*line = ReconGetPage(model_file, page_name, "DamageZone");	// FUN_00404720 + 0050bc00

	// "video"/"skeleton" model -> resolve to a skeleton so zone names map to
	// segment indices.
	const char *skeletonName = 0;
	model_file->GetEntry("video", "skeleton", &skeletonName);	// FUN_00404088 (0050bc0b/0050bc11)
	Skeleton *skeleton = LoadSkeleton(ReconDir(table_file), skeletonName);
	if (skeleton->SegmentCount() == 0)
	{
		DebugStream << skeletonName << " is empty or missing!";	// 0050bc1a
		return False;
	}

	Scalar runningTotal = 0.0f;
	for (; line != 0; line = line->Next())
	{
		// every line must be "<zoneName> <percentage>"  ("%s %f")
		if (strcmp(line->Format(), "%s %f") != 0)		// FUN_004d7f84 / 0050bc30
		{
			DebugStream << model_name << " " << model_file->GetFileName()
						<< ": " << page_name << " error in DamageZone Table Format!";	// 0050bc3b
			return False;	// -> cleanup @LAB_0049e1a9
		}

		char	zoneName[64];
		Scalar	percentage;
		line->Parse(zoneName, &percentage);

		int segmentIndex = ReconSegIndex(skeleton, zoneName);	// FUN_004274f8
		if (segmentIndex == -1)
		{
			DebugStream << model_file->GetFileName() << " " << page_name
						<< " " << zoneName << " Not Found!";			// 0050bc63
			return False;
		}

		runningTotal += percentage;
		table.entries.Insert(NewTableEntry(segmentIndex), &runningTotal);	// FUN_00424716 + slot+8
	}

	if (runningTotal != PercentSumTarget)				// _DAT_0049e520 == local_18
	{
		DebugStream << model_file->GetFileName() << " " << page_name
					<< ": percentages do not sum to 100%(i.e., 1.0)!";	// 0050bc72
		return False;
	}

	table.WriteEntries(stream);							// FUN_0049e524(param_6, &table)
	return True;
}


//###########################################################################
//###########################################################################
//					PieSlice
//###########################################################################
//###########################################################################

//
// @0x49e740 -- stream constructor (vtable 0x50bd90).  Reads owner@+0xc and the
// slice count@+0x2c from the stream, then builds 'sliceCount' leaf tables, each
// stamped with the cumulative slice angle i*(2*PI/sliceCount).  Caches the
// torso orientation source pointer = owner+0x438 (used by SelectSlice).
//
PieSlice::PieSlice(
	Mech					*owner,
	GenericObjectStream		*stream
):
	Plug()								// FUN_004178cc(this, 2)
{
	slices.Init();						// FUN_0049f2b2(this+5, 0, 1)
	stream->Read(&this->owner, 4);		// param_1+3   (param_3 slot+0x1c)
	stream->Read(&sliceCount, 4);		// param_1+0xb

	Scalar sliceSpan = TwoPi / (Scalar)sliceCount;	// _DAT_0049e810 / count
	for (int i = 1; i <= sliceCount; ++i)
	{
		DamageZonePercentTable *slice =
			new DamageZonePercentTable(owner, stream);	// FUN_0049deb0 (size 0x2c)
		Scalar cumulativeAngle = (Scalar)i * sliceSpan;
		slices.Insert(slice, &cumulativeAngle);			// slot+8
	}
	orientationSource = owner->TorsoOrientationSource();	// param_1[4] = *(owner+0x438)
}

PieSlice::~PieSlice()
{
	// detaches 'slices', clears Plug base
}

//
// @0x49e678 -- choose the slice for incoming angle 'theta'.  If the slice wheel
// rotates with the torso, add the live torso orientation
// (orientationSource+0x1d8) and wrap into [0, 2*PI).  Then index the slice
// table by floor(theta * sliceCount / (2*PI)) and return its leaf table.
//
DamageZonePercentTable *
	PieSlice::SelectSlice(Scalar theta) const
{
	if (rotateWithTorso)									// param_1+0xc != 0
	{
		theta += *(Scalar *)((char *)orientationSource + 0x1d8);	// torso heading
		if (TwoPi <= theta)		theta -= TwoPi;
		if (theta < 0.0f)		theta += TwoPi;			// _DAT_0049e730
	}

	int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi)) + 1;	// FUN_004dcd10/94
	return (DamageZonePercentTable *)slices.Lookup(&index);		// slot+0xc on this+0x14
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BuildFromNotation -- PieSlice  (@0x49e88c)
//
int
	PieSlice::BuildFromNotation(
		NotationFile			*model_file,
		const char				*model_name,
		const char				*page_name,
		NotationFile			*table_file,
		Mech					*owner,
		GenericObjectStream		*stream
	)
{
	int rotateWithTorso;
	if (!model_file->GetEntry(page_name, "RotateWithTorso", &rotateWithTorso))	// FUN_00404190 / 0050bc9f
	{
		DebugStream << page_name << " missing RotateWithTorso!";			// 0050bcaf
		return False;
	}
	stream->Write(&rotateWithTorso, 4);					// param_6 slot+0x20

	NotationLine *line = ReconGetPage(model_file, page_name, "PieSlice");	// FUN_00404720 / 0050bcca
	int sliceCount = line->Count();						// FUN_00403b60
	stream->Write(&sliceCount, 4);

	for (; line != 0; line = line->Next())
	{
		const char *sliceName = line->Value();
		if (sliceName[0] == '\0')						// puVar6[2] == 0
		{
			DebugStream << model_name << " error in damage table format " << page_name;	// 0050bcd3
			return -1;
		}
		// each named sub-page is a leaf percentage table
		DamageZonePercentTable::BuildFromNotation(
			model_file, model_name, sliceName, table_file, owner, stream);	// FUN_0049e00c
	}
	return True;
}


//###########################################################################
//###########################################################################
//					DamageLookupTable          (top -- layered)
//###########################################################################
//###########################################################################

//
// @0x49ea48 -- stream constructor (vtable 0x50bd84).  Reads the layer count and
// builds that many PieSlice layers from the stream.
//
DamageLookupTable::DamageLookupTable(
	Mech					*owner,
	GenericObjectStream		*stream
):
	Plug()								// FUN_004178cc(this, 2)
{
	this->owner = owner;				// param_1[3]
	layers.Init();						// FUN_0049f3d5(this+4, 0, 1)
	stream->Read(&layerCount, 4);		// param_1[10]

	for (int i = 0; i < layerCount; ++i)
	{
		PieSlice *layer = new PieSlice(owner, stream);	// FUN_0049e740 (size 0x30)
		layers.Insert(layer, &i);						// slot+8 on this+4
	}
}

DamageLookupTable::~DamageLookupTable()		// @0x49eadc
{
	// detaches 'layers', clears Plug base
}

Logical
	DamageLookupTable::TestInstance() const	// @0x49eb48 / @0x49e880
{
	return True;
}

//
// @0x49eb54 -- resolve an impact point to a damage zone.  Transforms 'impact'
// into the mech's local frame (owner+0xd0), then derives a normalised
// penetration-depth fraction (clamped against the armour reference held at
// owner+0x2ec[+0xc]).  That fraction selects a layer; the chosen PieSlice is
// then handed an incident angle -- either computed from the in-plane hit
// direction (atan2-style, FUN_004dc8ec) or, when the direction is degenerate,
// a uniform random angle in [0, 2*PI) -- and PieSlice::SelectSlice resolves it.
//
void
	DamageLookupTable::ResolveHit(const Point3D &impact) const
{
	Point3D local = owner->WorldToLocal(impact);			// FUN_00408bf8 / owner+0xd0

	Scalar armourRef = *(Scalar *)(*(int *)((char *)owner + 0x2ec) + 0xc);
	Scalar depth     = local.y;								// local_2c
	if (0.0f <= depth)										// _DAT_0049ed00
	{
		if (armourRef < depth)	depth = armourRef;			// clamp to deepest layer
	}

	int layerIndex = (int)floorf((Scalar)layerCount * (depth / armourRef));	// FUN_004dcd10/94
	if (layerIndex < 0)					layerIndex = 0;
	if (layerCount - 1 < layerIndex)	layerIndex = layerCount - 1;

	PieSlice *layer = (PieSlice *)layers.Lookup(&layerIndex);		// slot+0xc

	// incident angle from the in-plane (x,z) hit direction, else random
	Scalar theta;
	if (fabsf(local.z) > SortEpsilon && fabsf(local.x) > SortEpsilon)	// _DAT_0049ed04
	{
		theta = atan2f(local.z, local.x);					// FUN_004dc8ec
		if (theta < 0.0f)	theta += TwoPi;
	}
	else
	{
		theta = RandomUnit() * TwoPi;							// FUN_00408050 * _DAT_0049ed08
	}

	layer->SelectSlice(theta);								// FUN_0049e678
	// (caller then rolls the returned leaf table via SelectZone @0x49de14)
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BuildFromNotation -- DamageLookupTable  (@0x49ed28)
//
// Reads consecutive "Layer1", "Layer2", ... pages (number appended via itoa,
// FUN_004dd3ec) and parses each as a PieSlice page.  When no layers are found
// the page is malformed.
//
int
	DamageLookupTable::BuildFromNotation(
		NotationFile			*model_file,
		const char				*model_name,
		NotationFile			*table_file,
		Mech					*owner,
		GenericObjectStream		*stream
	)
{
	char	numbered[20];
	int		layer = 1;
	// "Layer" + itoa(layer) -> "Layer1"
	TextString key = TextString("Layer") + itoa(layer, numbered, 10);	// 0050bcf2 / FUN_004dd3ec

	while (model_file->PageExists(key))					// FUN_00403f84
	{
		++layer;
		key = TextString("Layer") + itoa(layer, numbered, 10);
		// each "LayerN" page is a PieSlice page
		PieSlice::BuildFromNotation(
			model_file, model_name, key, table_file, owner, stream);	// FUN_0049e88c (@line 8809)
	}

	if (layer == 1)										// no layers parsed
	{
		DebugStream << model_file->GetFileName() << " has bad format!";	// 0050bcf8
		return False;
	}
	return True;
}


//###########################################################################
// Tool support
//
// @0x49f301 -- table-entry comparator: orders by cumulative threshold (+0x14)
// within SortEpsilon.  Returns -1 / 0 / +1.
//
int
	CompareTableEntryThreshold(const void *a, const void *b)
{
	Scalar diff = *(Scalar *)((char *)a + 0x14) - *(Scalar *)((char *)b + 0x14);
	if (fabsf(diff) < SortEpsilon)								// _DAT_0049f369
		return 0;
	return (*(Scalar *)((char *)a + 0x14) <= *(Scalar *)((char *)b + 0x14)) ? -1 : 1;
}
