From 210376f2724b2482ce5ab547f300ca42b0583063 Mon Sep 17 00:00:00 2001
From: jonschz <17198703+jonschz@users.noreply.github.com>
Date: Wed, 17 Jul 2024 16:03:02 +0200
Subject: [PATCH] Implement `LegoRaceCar::HandleSkeletonKicks` and dependents
 (#1065)

* Implement `LegoRaceCar::HandleSkeletonKicks` and dependents

* Fix typo

* Spike to fix array comparisons (needs refactor)

* Refactor: Dedicated method for array element matching

* Address review comments

* Reformat with new version of black

* Apply more review comments

* Address more review comments

---------

Co-authored-by: jonschz <jonschz@users.noreply.github.com>
---
 LEGO1/lego/legoomni/include/carrace.h         |  3 +
 LEGO1/lego/legoomni/include/legoracers.h      | 22 ++++-
 LEGO1/lego/legoomni/include/raceskel.h        |  4 +-
 LEGO1/lego/legoomni/src/race/legoracers.cpp   | 81 ++++++++++++++++---
 LEGO1/lego/legoomni/src/race/raceskel.cpp     | 12 +++
 LEGO1/lego/sources/geom/legowegedge.h         |  2 +
 tools/isledecomp/isledecomp/compare/core.py   | 67 ++++++++++++++-
 .../isledecomp/isledecomp/cvdump/analysis.py  |  5 +-
 8 files changed, 179 insertions(+), 17 deletions(-)

diff --git a/LEGO1/lego/legoomni/include/carrace.h b/LEGO1/lego/legoomni/include/carrace.h
index fd33c536..5a43f420 100644
--- a/LEGO1/lego/legoomni/include/carrace.h
+++ b/LEGO1/lego/legoomni/include/carrace.h
@@ -52,6 +52,9 @@ public:
 	MxLong HandleEndAction(MxEndActionNotificationParam&) override;     // vtable+0x74
 	MxLong HandleType0Notification(MxNotificationParam&) override;      // vtable+0x78
 
+	// FUNCTION: BETA10 0x100cd060
+	undefined4 GetUnk0x150() { return m_unk0x150; }
+
 	// SYNTHETIC: LEGO1 0x10016c70
 	// CarRace::`scalar deleting destructor'
 
diff --git a/LEGO1/lego/legoomni/include/legoracers.h b/LEGO1/lego/legoomni/include/legoracers.h
index 290a849f..d66349a5 100644
--- a/LEGO1/lego/legoomni/include/legoracers.h
+++ b/LEGO1/lego/legoomni/include/legoracers.h
@@ -4,12 +4,24 @@
 #include "legocarraceactor.h"
 #include "legoracemap.h"
 
+#define LEGORACECAR_UNKNOWN_STATE 0
+#define LEGORACECAR_KICK1 2 // name guessed
+#define LEGORACECAR_KICK2 4 // name validated by BETA10 0x100cb659
+
 // SIZE 0x08
 struct EdgeReference {
 	const char* m_name;       // 0x00
 	LegoPathBoundary* m_data; // 0x04
 };
 
+// SIZE 0x10
+struct SkeletonKickPhase {
+	EdgeReference* m_edgeRef; // 0x00
+	float m_lower;            // 0x04
+	float m_upper;            // 0x08
+	MxU8 m_userState;         // 0x0c
+};
+
 // VTABLE: LEGO1 0x100d58a0 LegoRaceActor
 // VTABLE: LEGO1 0x100d58a8 LegoAnimActor
 // VTABLE: LEGO1 0x100d58b8 LegoPathActor
@@ -41,7 +53,7 @@ public:
 		return !strcmp(p_name, LegoRaceCar::ClassName()) || LegoCarRaceActor::IsA(p_name);
 	}
 
-	void ParseAction(char*) override;                  // vtable+0x20
+	void ParseAction(char* p_extra) override;          // vtable+0x20
 	void SetWorldSpeed(MxFloat p_worldSpeed) override; // vtable+0x30
 	MxU32 VTable0x6c(
 		LegoPathBoundary* p_boundary,
@@ -58,8 +70,8 @@ public:
 	MxResult VTable0x9c() override; // vtable+0x9c
 
 	virtual void SetMaxLinearVelocity(float p_maxLinearVelocity);
-	virtual void FUN_10012ff0(float);
-	virtual MxBool FUN_10013130(float);
+	virtual void FUN_10012ff0(float p_param);
+	virtual MxU32 HandleSkeletonKicks(float p_param1);
 
 	// SYNTHETIC: LEGO1 0x10014240
 	// LegoRaceCar::`scalar deleting destructor'
@@ -74,7 +86,9 @@ private:
 	LegoPathBoundary* m_unk0x7c;    // 0x7c
 
 	static EdgeReference g_edgeReferences[];
-	static const EdgeReference* g_pEdgeReferences;
+	static const SkeletonKickPhase g_skeletonKickPhases[]; // TODO: better name
+
+	static const char* g_soundSkel3;
 };
 
 #endif // LEGORACERS_H
diff --git a/LEGO1/lego/legoomni/include/raceskel.h b/LEGO1/lego/legoomni/include/raceskel.h
index 6d9a270b..fca57dde 100644
--- a/LEGO1/lego/legoomni/include/raceskel.h
+++ b/LEGO1/lego/legoomni/include/raceskel.h
@@ -12,8 +12,10 @@ class RaceSkel : public LegoAnimActor {
 public:
 	RaceSkel();
 
+	void GetCurrentAnimData(float* p_outCurAnimPosition, float* p_outCurAnimDuration);
+
 private:
-	undefined4 m_unk0x1c; // 0x1c
+	float m_animPosition; // 0x1c
 };
 
 #endif // RACESKEL_H
diff --git a/LEGO1/lego/legoomni/src/race/legoracers.cpp b/LEGO1/lego/legoomni/src/race/legoracers.cpp
index 2a4e2185..720ea13c 100644
--- a/LEGO1/lego/legoomni/src/race/legoracers.cpp
+++ b/LEGO1/lego/legoomni/src/race/legoracers.cpp
@@ -1,15 +1,21 @@
 #include "legoracers.h"
 
 #include "anim/legoanim.h"
+#include "carrace.h"
 #include "define.h"
+#include "legocachesoundmanager.h"
 #include "legocameracontroller.h"
 #include "legorace.h"
+#include "legosoundmanager.h"
 #include "misc.h"
+#include "mxdebug.h"
 #include "mxmisc.h"
 #include "mxnotificationmanager.h"
 #include "mxutilities.h"
+#include "raceskel.h"
 
 DECOMP_SIZE_ASSERT(EdgeReference, 0x08)
+DECOMP_SIZE_ASSERT(SkeletonKickPhase, 0x10)
 DECOMP_SIZE_ASSERT(LegoRaceCar, 0x200)
 
 // GLOBAL: LEGO1 0x100f0a20
@@ -41,7 +47,24 @@ EdgeReference LegoRaceCar::g_edgeReferences[] = {
 };
 
 // GLOBAL: LEGO1 0x100f0a50
-const EdgeReference* LegoRaceCar::g_pEdgeReferences = g_edgeReferences;
+const SkeletonKickPhase LegoRaceCar::g_skeletonKickPhases[] = {
+	{&LegoRaceCar::g_edgeReferences[0], 0.1, 0.2, LEGORACECAR_KICK2},
+	{&LegoRaceCar::g_edgeReferences[1], 0.2, 0.3, LEGORACECAR_KICK2},
+	{&LegoRaceCar::g_edgeReferences[2], 0.3, 0.4, LEGORACECAR_KICK2},
+	{&LegoRaceCar::g_edgeReferences[2], 0.6, 0.7, LEGORACECAR_KICK1},
+	{&LegoRaceCar::g_edgeReferences[1], 0.7, 0.8, LEGORACECAR_KICK1},
+	{&LegoRaceCar::g_edgeReferences[0], 0.8, 0.9, LEGORACECAR_KICK1},
+	{&LegoRaceCar::g_edgeReferences[3], 0.1, 0.2, LEGORACECAR_KICK1},
+	{&LegoRaceCar::g_edgeReferences[4], 0.2, 0.3, LEGORACECAR_KICK1},
+	{&LegoRaceCar::g_edgeReferences[5], 0.3, 0.4, LEGORACECAR_KICK1},
+	{&LegoRaceCar::g_edgeReferences[5], 0.6, 0.7, LEGORACECAR_KICK2},
+	{&LegoRaceCar::g_edgeReferences[4], 0.7, 0.8, LEGORACECAR_KICK2},
+	{&LegoRaceCar::g_edgeReferences[3], 0.8, 0.9, LEGORACECAR_KICK2},
+};
+
+// GLOBAL: LEGO1 0x100f0b70
+// STRING: LEGO1 0x100f08bc
+const char* LegoRaceCar::g_soundSkel3 = "skel3";
 
 // FUNCTION: LEGO1 0x10012950
 LegoRaceCar::LegoRaceCar()
@@ -140,13 +163,11 @@ void LegoRaceCar::FUN_10012ff0(float p_param)
 	LegoAnimActorStruct* a; // called `a` in BETA10
 	float deltaTime;
 
-	if (m_userState == 2) {
+	if (m_userState == LEGORACECAR_KICK1) {
 		a = m_unk0x70;
 	}
 	else {
-		// TODO: Possibly an enum?
-		const char legoracecarKick2 = 4; // original name: LEGORACECAR_KICK2
-		assert(m_userState == legoracecarKick2);
+		assert(m_userState == LEGORACECAR_KICK2);
 		a = m_unk0x74;
 	}
 
@@ -156,7 +177,7 @@ void LegoRaceCar::FUN_10012ff0(float p_param)
 		deltaTime = p_param - m_unk0x58;
 
 		if (a->GetDuration() <= deltaTime || deltaTime < 0.0) {
-			if (m_userState == 2) {
+			if (m_userState == LEGORACECAR_KICK1) {
 				LegoEdge** edges = m_unk0x78->GetEdges();
 				m_destEdge = (LegoUnknown100db7f4*) (edges[2]);
 				m_boundary = m_unk0x78;
@@ -167,7 +188,7 @@ void LegoRaceCar::FUN_10012ff0(float p_param)
 				m_boundary = m_unk0x7c;
 			}
 
-			m_userState = 0;
+			m_userState = LEGORACECAR_UNKNOWN_STATE;
 		}
 		else if (a->GetAnimTreePtr()->GetCamAnim()) {
 			MxMatrix transformationMatrix;
@@ -189,10 +210,50 @@ void LegoRaceCar::FUN_10012ff0(float p_param)
 	}
 }
 
-// STUB: LEGO1 0x10013130
-MxBool LegoRaceCar::FUN_10013130(float)
+// FUNCTION: LEGO1 0x10013130
+// FUNCTION: BETA10 0x100cce50
+MxU32 LegoRaceCar::HandleSkeletonKicks(float p_param1)
 {
-	// TODO
+	const SkeletonKickPhase* current = g_skeletonKickPhases;
+
+	// TODO: Type is guesswork so far
+	CarRace* r = (CarRace*) CurrentWorld(); // called `r` in BETA10
+	assert(r);
+
+	RaceSkel* s = (RaceSkel*) r->GetUnk0x150(); // called `s` in BETA10
+	assert(s);
+
+	float skeletonCurAnimPosition;
+	float skeletonCurAnimDuration;
+
+	s->GetCurrentAnimData(&skeletonCurAnimPosition, &skeletonCurAnimDuration);
+
+	float skeletonCurAnimPhase = skeletonCurAnimPosition / skeletonCurAnimDuration;
+
+	for (MxS32 i = 0; i < sizeOfArray(g_skeletonKickPhases); i++) {
+		if (m_boundary == current->m_edgeRef->m_data && current->m_lower <= skeletonCurAnimPhase &&
+			skeletonCurAnimPhase <= current->m_upper) {
+			m_userState = current->m_userState;
+		}
+		current = &current[1];
+	}
+
+	if (m_userState != LEGORACECAR_KICK1 && m_userState != LEGORACECAR_KICK2) {
+		MxTrace(
+			// STRING: BETA10 0x101f64c8
+			"Got kicked in boundary %s %d %g:%g %g\n",
+			// TODO: same as in above comparison
+			m_boundary->GetName(),
+			skeletonCurAnimPosition,
+			skeletonCurAnimDuration,
+			skeletonCurAnimPhase
+		);
+		return FALSE;
+	}
+
+	m_unk0x58 = p_param1;
+	SoundManager()->GetCacheSoundManager()->Play(g_soundSkel3, NULL, FALSE);
+
 	return TRUE;
 }
 
diff --git a/LEGO1/lego/legoomni/src/race/raceskel.cpp b/LEGO1/lego/legoomni/src/race/raceskel.cpp
index 5ef950e2..dd7e87f3 100644
--- a/LEGO1/lego/legoomni/src/race/raceskel.cpp
+++ b/LEGO1/lego/legoomni/src/race/raceskel.cpp
@@ -1,5 +1,7 @@
 #include "raceskel.h"
 
+#include <cassert>
+
 DECOMP_SIZE_ASSERT(RaceSkel, 0x178)
 
 // STUB: LEGO1 0x100719b0
@@ -7,3 +9,13 @@ RaceSkel::RaceSkel()
 {
 	// TODO
 }
+
+// FUNCTION: LEGO1 0x10071cb0
+// FUNCTION: BETA10 0x100f158b
+void RaceSkel::GetCurrentAnimData(float* p_outCurAnimPosition, float* p_outCurAnimDuration)
+{
+	*p_outCurAnimPosition = m_animPosition;
+
+	assert(m_curAnim >= 0);
+	*p_outCurAnimDuration = m_animMaps[m_curAnim]->GetDuration();
+}
diff --git a/LEGO1/lego/sources/geom/legowegedge.h b/LEGO1/lego/sources/geom/legowegedge.h
index 716f830f..23bdf340 100644
--- a/LEGO1/lego/sources/geom/legowegedge.h
+++ b/LEGO1/lego/sources/geom/legowegedge.h
@@ -43,6 +43,8 @@ public:
 	LegoU32 GetFlag0x10() { return m_flags & c_bit5 ? FALSE : TRUE; }
 	Mx4DPointFloat* GetUnknown0x14() { return &m_unk0x14; }
 	Mx4DPointFloat* GetEdgeNormal(int index) { return &m_edgeNormals[index]; }
+
+	// FUNCTION: BETA10 0x1001c9b0
 	LegoChar* GetName() { return m_name; }
 
 	void SetFlag0x10(LegoU32 p_disable)
diff --git a/tools/isledecomp/isledecomp/compare/core.py b/tools/isledecomp/isledecomp/compare/core.py
index 1587ef81..3cf827e9 100644
--- a/tools/isledecomp/isledecomp/compare/core.py
+++ b/tools/isledecomp/isledecomp/compare/core.py
@@ -8,6 +8,7 @@ from typing import Any, Callable, Iterable, List, Optional
 from isledecomp.bin import Bin as IsleBin, InvalidVirtualAddressError
 from isledecomp.cvdump.demangler import demangle_string_const
 from isledecomp.cvdump import Cvdump, CvdumpAnalysis
+from isledecomp.cvdump.types import scalar_type_pointer
 from isledecomp.parser import DecompCodebase
 from isledecomp.dir import walk_source_dir
 from isledecomp.types import SymbolType
@@ -220,7 +221,8 @@ class Compare:
                     var.offset, var.name, var.parent_function
                 )
             else:
-                self._db.match_variable(var.offset, var.name)
+                if self._db.match_variable(var.offset, var.name):
+                    self._check_if_array_and_match_elements(var.offset, var.name)
 
         for tbl in codebase.iter_vtables():
             self._db.match_vtable(tbl.offset, tbl.name, tbl.base_class)
@@ -245,6 +247,69 @@ class Compare:
 
             self._db.match_string(string.offset, string.name)
 
+    def _check_if_array_and_match_elements(self, orig_addr: int, name: str):
+        """
+        Checks if the global variable at `orig_addr` is an array.
+        If yes, adds a match for all its elements. If it is an array of structs, all fields in that struct are also matched.
+        Note that there is no recursion, so an array of arrays would not be handled entirely.
+        This step is necessary e.g. for `0x100f0a20` (LegoRacers.cpp).
+        """
+
+        def _add_match_in_array(
+            name: str, type_id: str, orig_addr: int, recomp_addr: int
+        ):
+            self._db.set_recomp_symbol(
+                recomp_addr,
+                SymbolType.POINTER if scalar_type_pointer(type_id) else SymbolType.DATA,
+                name,
+                name,
+                # we only need the matches when they are referenced elsewhere, hence we don't need the size
+                size=None,
+            )
+            self._db.set_pair(orig_addr, recomp_addr)
+
+        matchinfo = self._db.get_by_orig(orig_addr)
+        if matchinfo is None or matchinfo.recomp_addr is None:
+            return
+        recomp_addr = matchinfo.recomp_addr
+
+        node = next(
+            (x for x in self.cvdump_analysis.nodes if x.addr == recomp_addr),
+            None,
+        )
+        if node is None or node.data_type is None:
+            return
+
+        if not node.data_type.key.startswith("0x"):
+            # scalar type, so clearly not an array
+            return
+
+        data_type = self.cv.types.keys[node.data_type.key.lower()]
+
+        if data_type["type"] == "LF_ARRAY":
+            array_element_type = self.cv.types.get(data_type["array_type"])
+
+            assert node.data_type.members is not None
+
+            for array_element in node.data_type.members:
+                orig_element_base_addr = orig_addr + array_element.offset
+                recomp_element_base_addr = recomp_addr + array_element.offset
+                if array_element_type.members is None:
+                    _add_match_in_array(
+                        f"{name}{array_element.name}",
+                        array_element_type.key,
+                        orig_element_base_addr,
+                        recomp_element_base_addr,
+                    )
+                else:
+                    for member in array_element_type.members:
+                        _add_match_in_array(
+                            f"{name}{array_element.name}.{member.name}",
+                            array_element_type.key,
+                            orig_element_base_addr + member.offset,
+                            recomp_element_base_addr + member.offset,
+                        )
+
     def _find_original_strings(self):
         """Go to the original binary and look for the specified string constants
         to find a match. This is a (relatively) expensive operation so we only
diff --git a/tools/isledecomp/isledecomp/cvdump/analysis.py b/tools/isledecomp/isledecomp/cvdump/analysis.py
index 40ef292e..e030035e 100644
--- a/tools/isledecomp/isledecomp/cvdump/analysis.py
+++ b/tools/isledecomp/isledecomp/cvdump/analysis.py
@@ -5,7 +5,7 @@ from isledecomp.cvdump import SymbolsEntry
 from isledecomp.types import SymbolType
 from .parser import CvdumpParser
 from .demangler import demangle_string_const, demangle_vtable
-from .types import CvdumpKeyError, CvdumpIntegrityError
+from .types import CvdumpKeyError, CvdumpIntegrityError, TypeInfo
 
 
 class CvdumpNode:
@@ -35,6 +35,8 @@ class CvdumpNode:
     section_contribution: Optional[int] = None
     addr: Optional[int] = None
     symbol_entry: Optional[SymbolsEntry] = None
+    # Preliminary - only used for non-static variables at the moment
+    data_type: Optional[TypeInfo] = None
 
     def __init__(self, section: int, offset: int) -> None:
         self.section = section
@@ -127,6 +129,7 @@ class CvdumpAnalysis:
                 # get information for built-in "T_" types.
                 g_info = parser.types.get(glo.type)
                 node_dict[key].confirmed_size = g_info.size
+                node_dict[key].data_type = g_info
                 # Previously we set the symbol type to POINTER here if
                 # the variable was known to be a pointer. We can derive this
                 # information later when it's time to compare the variable,