Battle Message System¶
Verification status. Everything below is directly verified in the Ghidra project against
SoulcaliburVI.exe(image base0x140000000). Every address on this page corresponds to a function or data symbol that has been renamed and plate-commented in Ghidra. Items marked Unverified describe what I have not found in the binary — read those sections as gaps, not conclusions.Note on display strings. The actual on-screen text ("Counter Hit", "Back Escape", "Lethal Hit", etc.) is localised via
Game.locres— none of those user-visible strings appear inSoulcaliburVI.exeas ASCII or UTF-16, so grepping the binary for "Back Escape" turns up nothing. The binary holds only the enum names (e.g.EMS_ThrowEscapeB) and the reflection metadata that ties an enum value to its localisation key. The mapping fromEMS_ThrowEscapeBto "Back Escape" lives inGame.locresinside the game pak — extract that file (e.g. with UnrealLocres) to read the actual display strings.Best educated guess at the localisation, based on enum naming and SC6 community parlance:
Enum Likely localised text EMS_ThrowEscapeA(0x1A)Front Escape (or A Escape) — broke an A-button throw EMS_ThrowEscapeB(0x1B)Back Escape (or B Escape) — broke a B-button throw EMS_AThrow(0x1E)A Throw (forward A-button throw command) EMS_BThrow(0x1F)B Throw (forward B-button throw command) EMS_UThrow(0x20)U Throw — most likely back-throw / unbreakable from-behind This mapping is not verified from binary — it's inferred from naming. Confirm against
Game.locres.
The "Battle Message" system is the on-screen banner that flashes during a fight when something noteworthy happens (e.g. Counter Hit, Punish Attack, Wall Hit, Back Escape, Lethal Hit, Critical Edge). Its reflection layer is split across three pieces:
ELuxBattleMessage— UEnum listing every message type the HUD can show.FLuxBattleMessageParam— UScriptStruct; the payload carried by a message (type id, owning player, an integer "Value").ULuxBattleMessageReceiverInterface— UInterface declaring the two UFunctions that deliver a message:OnReceiveMessageandSendMessageToOtherLevel.
Two C++ broadcast helpers walk the actor tree and call the interface UFunction on every receiver; one Native UFunction wrapper exposes the broadcast to Blueprint.
ELuxBattleMessage — verified enum values¶
The enum is declared in /Script/LuxorGame with underlying type uint8. It
has 39 named entries (numeric values 0..0x26), extracted directly from
UEnum_ELuxBattleMessage_RegisterValues @ 0x140984920:
| Value | Name | Value | Name |
|---|---|---|---|
| 0x00 | EMS_Combo |
0x14 | EMS_Revenge |
| 0x01 | EMS_BreakAttack |
0x15 | EMS_AdrenalineRush |
| 0x02 | EMS_UnBlockable |
0x16 | EMS_WallHit |
| 0x03 | EMS_ReversalEdge |
0x17 | EMS_Stun |
| 0x04 | EMS_PunishAttack |
0x18 | EMS_GuardBurst |
| 0x05 | EMS_SitAttack |
0x19 | EMS_Ukemi |
| 0x06 | EMS_WhileRisingAttack |
0x1A | EMS_ThrowEscapeA |
| 0x07 | EMS_SimultaneousAttack |
0x1B | EMS_ThrowEscapeB |
| 0x08 | EMS_8WayRunAttack |
0x1C | EMS_WeaponSkill |
| 0x09 | EMS_ReversalEdgeCrush |
0x1D | EMS_FoodEffect |
| 0x0A | EMS_GuardImpactCrush |
0x1E | EMS_AThrow |
| 0x0B | EMS_AttackCounter |
0x1F | EMS_BThrow |
| 0x0C | EMS_BreakCounter |
0x20 | EMS_UThrow |
| 0x0D | EMS_RunCounter |
0x21 | EMS_TorophyBraveEdge |
| 0x0E | EMS_ImpactCounter |
0x22 | EMS_TorophyCannon |
| 0x0F | EMS_LethalHit |
0x23 | EMS_TorophyShadow |
| 0x10 | EMS_CriticalEdge |
0x24 | EMS_SoulAttack |
| 0x11 | EMS_SoulCharge |
0x25 | EMS_MightyImpact |
| 0x12 | EMS_GuardImpact |
0x26 | ELuxBattleMessage_MAX |
| 0x13 | EMS_ReverseImpact |
ENUM_MAX (the C++-side trailing entry) appears in the .rdata strings at
0x1432723d8, next to ELuxBattleMessage_MAX (the UEnum-style trailing entry
at 0x14338f940). Both are reflection bookkeeping; only ELuxBattleMessage_MAX
is in the registered value list.
The string "Torophy" (sic) in the trophy-prefixed entries is a transliteration
from the Japanese トロフィー. It is what appears in the binary and is the actual
identifier used in the source.
Where the enum lives in Ghidra¶
| Symbol | Address |
|---|---|
UEnum_ELuxBattleMessage_StaticEnum |
0x140978320 |
UEnum_ELuxBattleMessage_RegisterValues |
0x140984920 |
Z_RegisterEnum_ELuxBattleMessage |
0x14016b720 |
| Cached UEnum* | DAT_14414e008 |
| Namespace strings (no xref) | .rdata 0x143270bc8 .. 0x143272380 |
A Ghidra-side enum named ELuxBattleMessage (size 1 byte) has been created with
all 39 values.
FLuxBattleMessageParam — verified struct¶
A UScriptStruct registered in /Script/LuxorGame as LuxBattleMessageParam. It
is the payload of every battle-message UFunction call.
Reflection metadata (from UScriptStruct_LuxBattleMessageParam_StaticStruct
at 0x14097a860):
| Field | Value |
|---|---|
| Native size | 0x0c (12 bytes) |
| Alignment | 4 |
| Cached UScriptStruct* | DAT_14414e010 |
Field layout — verified by reading
Z_Construct_UScriptStruct_LuxBattleMessageParam @ 0x140992430. Properties are
added to the reflection layer in declaration order, and the struct size of 0xc
with alignment 4 constrains the layout to:
struct FLuxBattleMessageParam // 0x0c bytes
{
int32_t Value; // +0x00 IntProperty
int32_t PlayerIndex; // +0x04 IntProperty
ELuxBattleMessage Type; // +0x08 ByteProperty + UEnum
uint8_t _pad[3]; // +0x09 to round to align(4)
};
The Ghidra struct has been created as FLuxBattleMessageParam with these three
fields (the trailing 3-byte pad is implicit from align(4)).
Caveat — what Value actually holds. The reflection layer types it as
int32, but I have not verified what callers stash in it. From the enum
names, plausible guesses are:
- For
EMS_Combo, the combo hit count. - For
EMS_WallHit/EMS_LethalHitetc.,0or a flag. - For trophy-prefixed entries, a counter.
These are guesses. Don't rely on them without checking the firing site for the specific message you care about.
Where the struct lives in Ghidra¶
| Symbol | Address |
|---|---|
UScriptStruct_LuxBattleMessageParam_StaticStruct |
0x14097a860 |
Z_Construct_UScriptStruct_LuxBattleMessageParam |
0x140992430 |
Z_RegisterPackage_LuxBattleMessageParam |
0x14016bc20 |
Z_RegisterDynamicCpp_LuxBattleMessageParam |
0x140975440 |
ULuxBattleMessageReceiverInterface — verified UInterface¶
A UInterface registered in /Script/LuxorGame. It declares two UFUNCTION
entries, both of which take a struct parameter named Message:
UINTERFACE(MinimalAPI, Blueprintable)
class ULuxBattleMessageReceiverInterface : public UInterface { /* … */ };
class ILuxBattleMessageReceiverInterface
{
GENERATED_IINTERFACE_BODY()
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Battle|Message")
void OnReceiveMessage(const /* struct */& Message);
UFUNCTION(BlueprintCallable, Category="Battle|Message")
void SendMessageToOtherLevel(const /* struct */& Message);
};
The UFunction flag bits read from
Z_Construct_UFunction_OnReceiveMessage @ 0x140aa5050 (0x8020800) and
Z_Construct_UFunction_SendMessageToOtherLevel @ 0x140aa5270 (0x4020401) are
consistent with BlueprintNativeEvent and Native + BlueprintCallable
respectively. I have not cross-referenced every individual EFunctionFlags
bit, so treat the BP-event annotations above as probable, not certain.
Why Message isn't typed FLuxBattleMessageParam in the reflection layer¶
In Z_Construct_UFunction_OnReceiveMessage the Message parameter is created
via FUN_1408df7e0(0x78, …) (the FStructProperty constructor), but the struct
class it points at is not hard-bound to FLuxBattleMessageParam — it is a
generic FStructProperty.
That matches the observed usage: the existing in-engine callers pass any
struct or wstring buffer through this interface. Specifically,
LuxBattle_BroadcastMessageToActorAndOwners is invoked from the
character-creation menu code (FUN_1406ffa80, FUN_1406a0a40, etc.) carrying
wstring-style payloads such as "ChangeChromaKeyColor%f,%f,%f",
"ChangeSituation*", and "DisplayPoseInfo" — not FLuxBattleMessageParam.
So OnReceiveMessage is a generic Lux message bus: it can carry
FLuxBattleMessageParam but is not restricted to it. Whether the on-screen
ELuxBattleMessage HUD banners fire through this same bus or through a
different path is unverified — see the Unverified section below.
Where the interface lives in Ghidra¶
| Symbol | Address |
|---|---|
Z_Construct_UClass_ULuxBattleMessageReceiverInterface |
0x140a993f0 |
Z_Construct_UFunction_OnReceiveMessage |
0x140aa5050 |
Z_Construct_UFunction_SendMessageToOtherLevel |
0x140aa5270 |
| Cached UClass* (interface) | DAT_144153a80 |
Cached UFunction* (OnReceiveMessage) |
DAT_144153a70 |
Cached UFunction* (SendMessageToOtherLevel) |
DAT_144153a78 |
| Cached FName "OnReceiveMessage" | DAT_144153040 |
Init_FName_OnReceiveMessage |
0x1401829e0 |
Dispatch — verified¶
There is one shared dispatcher and two broadcast helpers.
ILuxBattleMessageReceiver_Execute_OnReceiveMessage — 0x140a98140¶
void ILuxBattleMessageReceiver_Execute_OnReceiveMessage(UObject* receiver, const void* Message)
{
char paramBuf[20]; // CDataManager_Impl_InitSlot
CDataManager_Impl_InitSlot(paramBuf);
FUN_1408dfa90(paramBuf, Message); // copy struct/buffer into slot
UFunction* fn = UObject_FindFunctionChecked(receiver,
fname_OnReceiveMessage);
receiver->vtable[0x1f8](receiver, fn, paramBuf); // UObject::ProcessEvent
FMemory_FreeViaGMalloc(paramBuf);
}
This is the only place in the binary that loads the cached OnReceiveMessage
FName (DAT_144153040). Vtable index 0x1f8 is UObject::ProcessEvent. Every
battle-message delivery in the game passes through this function.
LuxBattle_BroadcastToSiblingActors — 0x1403cc980¶
Walks the level the actor lives in and calls Execute_OnReceiveMessage on
every actor in the same level that implements
ULuxBattleMessageReceiverInterface, skipping the caller itself.
void LuxBattle_BroadcastToSiblingActors(AActor* self, const void* Message)
{
ULevel* level = self->vtable[0x138](self); // GetLevel/GetWorld
TArray<AActor*> actors;
FUN_1421bb820(level, &actors); // collect actors
for (AActor* actor : actors) {
if (actor != self
&& implements_interface(actor, ULuxBattleMessageReceiverInterface)) {
ILuxBattleMessageReceiver_Execute_OnReceiveMessage(actor, Message);
}
}
}
There is exactly one caller in the binary:
execLuxBattle_BroadcastToSiblingActors at 0x140abe260 — the Native
UFunction handler that exposes the broadcast to Blueprint.
LuxBattle_BroadcastMessageToActorAndOwners — 0x14053d380¶
Two-phase variant that hits the target actor first, then walks
level->OwnedActors[] and dispatches to every owned actor that implements the
interface. Frees the message buffer at the end.
13 callers in the binary. The confirmed callers are mostly
character-creation menu code (FUN_1406ffa80, FUN_1406a0a40,
FUN_1406a2a10, FUN_1406a3280, FUN_1406a98f0, FUN_14073c530,
FUN_140c03e00), and they pass wstring payloads, not
FLuxBattleMessageParam. Those are not battle-HUD messages — they are
creation-menu state events that happen to ride the same interface.
Where the dispatchers live in Ghidra¶
| Symbol | Address | Purpose |
|---|---|---|
ILuxBattleMessageReceiver_Execute_OnReceiveMessage |
0x140a98140 |
The single ProcessEvent call site |
LuxBattle_BroadcastToSiblingActors |
0x1403cc980 |
Broadcast to siblings (skips self) |
LuxBattle_BroadcastMessageToActorAndOwners |
0x14053d380 |
Target + level owners |
execLuxBattle_BroadcastToSiblingActors |
0x140abe260 |
BP-callable wrapper |
ALuxBattleHUDManager — the renderer¶
The actual HUD class is registered in Ghidra at 0x140160810
(ALuxBattleHUDManager_StaticClass).
| Field | Value |
|---|---|
Short name (in /Script/LuxorGame) |
LuxBattleHUDManager |
| Native C++ class size | 0x3c0 |
| Hash | 0x4b72d1ff |
| Cached UClass* | DAT_14414c648 |
| StaticClass thunk | ALuxBattleHUDManager_StaticClass @ 0x140160810 |
| Z_Construct_UClass | Z_Construct_UClass_ALuxBattleHUDManager @ 0x1409149d0 |
| Package register | Z_RegisterPackage_ALuxBattleHUDManager @ 0x140161830 |
Source path strings (proof of file name):
A debug-log format string "BattleMessage(%s): " lives at 0x143276258 in the
same .rdata block. Ghidra's wide-string xref index did not recover the
reference site, so the exact C++ function that emits this log is not yet
identified in this project. The file name strongly implies that
ALuxBattleHUDManager has a LogTempBattleMessage-style macro that fires
per-message and prints the enum entry name (%s → UEnum::GetNameStringByValue
on ELuxBattleMessage). Unverified — the call site has not been decompiled.
What's not verified yet¶
These items were investigated but not pinned down. They are potential follow-ups, not conclusions.
Where ELuxBattleMessage values are fired¶
I did not isolate a single C++ function that builds a FLuxBattleMessageParam
on the stack, sets Type = EMS_ThrowEscapeA / EMS_LethalHit / etc., and
broadcasts it.
What I tried:
search_byte_patternsforC6 44 24 ?? 1A(mov byte [rsp+disp8], 0x1A) finds only 3 sites in the entire binary, none in obvious throw-escape paths.0x1AisEMS_ThrowEscapeA.- xrefs to
LuxBattle_BroadcastMessageToActorAndOwnersare all in character-creation-menu code, not in battle hit-resolution code. - xrefs to
LuxBattle_BroadcastToSiblingActorscome only from its Native exec wrapper (Blueprint dispatch), so the Blueprint-side callers aren't visible. - Direct xrefs to the
FLuxBattleMessageParamUScriptStruct cached pointer (DAT_14414e010) are only the construction code, not callers.
So one of the following is true:
- The HUD banners are dispatched from Blueprint (the
BP_BattleHUD_*widget) rather than from C++ — meaning the firing side lives in a.uassetBP, not in this binary. - The HUD banners use a different dispatch path not yet found (e.g. a direct
member-function call on
ALuxBattleHUDManagerrather than a broadcast). - The firing site is in C++ but loads
EMS_ThrowEscapeAvia something other than a 1-byte stack write (e.g. amovfrom a register sourced from an enum lookup).
Until that is resolved, how a custom HUD banner should be triggered cannot be stated with certainty.
Value field semantics¶
Typed as int32 in reflection. Whether it is a hit count, a damage number, a
flag, or a context-specific tag varies per message type. Unverified.
"BattleMessage(%s)" log call site¶
The format string exists at 0x143276258, but Ghidra's wide-string xref
analyzer did not pick up the call site. Manual byte-pattern searches for the
address bytes and for a lea r??, [rip+disp32] pattern produced no hit. This is
likely recoverable by re-running Ghidra's "Auto Analyze" with wide-string
xref detection re-enabled, or by manually computing disp32 = 0x143276258 - rip
— neither was done in this session.
Whether ALuxBattleHUDManager implements ULuxBattleMessageReceiverInterface¶
The class size (0x3c0) is large enough to host the interface vtable thunk,
and the class name strongly implies it. This has not been confirmed from the
Z_Construct_UClass_ALuxBattleHUDManager body. Unverified.
Modder feasibility — can we send our own messages?¶
Path A — broadcast through the interface (most likely workable)¶
Build an FLuxBattleMessageParam on the stack and call
LuxBattle_BroadcastToSiblingActors directly from a native hook:
// addr = 0x1403cc980 (LuxBattle_BroadcastToSiblingActors)
using FnBroadcast = void (*)(AActor* self, FLuxBattleMessageParam* msg);
FLuxBattleMessageParam m{};
m.Value = 1;
m.PlayerIndex = 0;
m.Type = 0x1A; // EMS_ThrowEscapeA
reinterpret_cast<FnBroadcast>(0x140000000 + 0x3cc980)(myActor, &m);
myActor must live in the same ULevel as the HUD; otherwise GetLevel()
returns a different level from the HUD's owning level and the broadcast never
reaches it. In a battle, any ALuxBattleChara* or the ALuxBattleManager
should work.
Caveats:
- This fires
OnReceiveMessageonly on actors that implementULuxBattleMessageReceiverInterface. WhetherALuxBattleHUDManagerimplements that interface is unverified; if it does not, this will not show the banner. - The
MessageUFunction param is a generic FStructProperty, and the receiver's BP/native handler dictates how the buffer is interpreted. So even if the HUD receives the call, it may expect aFStringpayload (per the creation-menu observation) rather thanFLuxBattleMessageParam. Test before shipping.
Path B — direct call on the HUD (more reliable but unverified entry point)¶
If we identify the C++ method on ALuxBattleHUDManager that takes a
FLuxBattleMessageParam and pushes it onto the visible queue, we can call
that method directly. That entry point is where EMS_* values would normally
flow internally.
That method has not been located yet (see Unverified).
Path C — native ProcessEvent diagnostic¶
If the HUD is implemented partially in BP_BattleHUDManager.uasset, the
firing path is the OnReceiveMessage BP event handler. A temporary native
detour on UObject::ProcessEvent can filter for that UFunction and dump the
receiver plus parameter buffer while you trigger a known message.
This does not send messages — it intercepts whatever path is already in use. Useful for verifying which struct type the HUD actually expects.
Reverse-engineering checklist for the next pass¶
- Open
Z_Construct_UClass_ALuxBattleHUDManager @ 0x1409149d0in Ghidra and list every registered UFunction. If one isOnReceiveMessage, the HUD does implement the interface. If one is namedShowMessage,SetMessage,PostMessage, or similar, that is the direct entry point (Path B above). - Use a
lea r??, [rip+disp32]search to locate the call site for0x143276258("BattleMessage(%s): ") — almost certainly aUE_LOG(LogBattle, …)insideALuxBattleHUDManager::SetMessageor similar. - If neither step identifies the firing path, install a temporary native
UObject::ProcessEventdetour and filter forOnReceiveMessagewhile triggering a throw escape in a training-room match. The capturedparamsbuffer reveals both the struct layout the HUD actually receives and which actor is calling.