Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Client/game_sa/CFxManagerSA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ void CFxManagerSA::OnFxSystemSAInterfaceDestroyed(CFxSystemSAInterface* pFxSyste
CFxSystemSA* pFxSystemSA = GetFxSystem(pFxSystemSAInterface);
if (pFxSystemSA)
delete pFxSystemSA;

MapRemove(m_NitroSystemMap, pFxSystemSAInterface);
}

CFxSystemBPSAInterface* CFxManagerSA::GetFxSystemBlueprintByName(SString sName)
Expand Down Expand Up @@ -105,3 +107,19 @@ CFxSystemSA* CFxManagerSA::GetFxSystem(CFxSystemSAInterface* pFxSystemSAInterfac
{
return MapFindRef(m_FxInterfaceMap, pFxSystemSAInterface);
}

void CFxManagerSA::RegisterNitroSystem(CFxSystemSAInterface* pFxSystemSAInterface, CVehicle* pVehicle)
{
if (pFxSystemSAInterface)
MapSet(m_NitroSystemMap, pFxSystemSAInterface, pVehicle);
}

CVehicle* CFxManagerSA::GetVehicleFromNitroSystem(CFxSystemSAInterface* pFxSystemSAInterface)
{
return MapFindRef(m_NitroSystemMap, pFxSystemSAInterface);
}

void CFxManagerSA::UnregisterVehicleNitroSystems(CVehicle* pVehicle)
{
MapRemoveByValue(m_NitroSystemMap, pVehicle);
}
6 changes: 6 additions & 0 deletions Client/game_sa/CFxManagerSA.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CFxSystemBPSAInterface;
class CFxSystemSAInterface;
class CFxEmitterSAInterface;
class CFxSystemSA;
class CVehicle;

class CFxMemoryPoolSAInterface
{
Expand Down Expand Up @@ -74,7 +75,12 @@ class CFxManagerSA : public CFxManager
void AddToList(CFxSystemSAInterface* pFxSystemSAInterface, CFxSystemSA* pFxSystemSA);
void RemoveFromList(CFxSystemSA* pFxSystemSA);

void RegisterNitroSystem(CFxSystemSAInterface* pFxSystemSAInterface, CVehicle* pVehicle);
CVehicle* GetVehicleFromNitroSystem(CFxSystemSAInterface* pFxSystemSAInterface);
void UnregisterVehicleNitroSystems(CVehicle* pVehicle);

private:
CFxManagerSAInterface* m_pInterface;
CFastHashMap<CFxSystemSAInterface*, CFxSystemSA*> m_FxInterfaceMap;
CFastHashMap<CFxSystemSAInterface*, CVehicle*> m_NitroSystemMap;
};
3 changes: 2 additions & 1 deletion Client/game_sa/CFxSystemBPSA.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class CFxSystemBPSAInterface
CFxSystemBPSAInterface* pNext; // 0x04

// Actual members
char* szNameHash; // 0x08
// CRC-32 (no final XOR) of the lowercased blueprint name, e.g. "nitro" - NOT a string pointer
uint32_t uiNameHash; // 0x08
float fLength; // 0x0C
float fLoopIntervalMinimum; // 0x10
float fLoopIntervalLength; // 0x14
Expand Down
222 changes: 222 additions & 0 deletions Client/game_sa/CFxSystemSA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,231 @@
*****************************************************************************/

#include "StdInc.h"
#include <optional>
#include <CMatrix.h>
#include <game/CVehicle.h>
#include "CFxSystemBPSA.h"
#include "CFxSystemSA.h"
#include "CGameSA.h"

extern CGameSA* pGame;

// CRC-32/JAMCRC of the uppercased blueprint name "NITRO" (see CKeyGen::GetUppercaseKey)
static constexpr uint32_t FX_BLUEPRINT_HASH_NITRO = 0x3D591CC6;

// One animated colour block ("FX_INFO_COLOUR_DATA"/"FX_INFO_COLOURBRIGHT_DATA"/
// "FX_INFO_COLOURRANGE_DATA") of the "nitro" blueprint's R/G/B/A(/Range) channels, plus their
// original (unmodified) keyframe values so a per-vehicle tint can be re-derived from scratch
// every time a different vehicle's nitro particles are about to be rendered.
struct SNitroColorBlock
{
uint8_t nNumKeyframes;
uint8_t nStride; // 1 = single value per component, 2 = value + randomisation range per component

// Indexed by [component (0=R, 1=G, 2=B, 3=A)][stride slot (0=value, 1=range)]; null if the
// blueprint has no such channel.
uint16_t* pChannelValues[4][2]{};
std::vector<uint16_t> originalValues[4][2];
};

// Original destination blend mode id of a "nitro" emitter, so ApplyNitroColor can switch
// between the effect's original (additive) blending and standard alpha blending.
struct SNitroEmitterBlend
{
CFxEmitterBPSAInterface* pEmitterBP;
uint8_t nOriginalDstBlendId;
};

// Blend mode ids index a table (0x8A6230) that maps id -> RwBlendFunction(id + 1),
// so id 5 = rwBLENDINVSRCALPHA
static constexpr uint8_t FX_BLEND_ID_INVSRCALPHA = 5;

static std::vector<SNitroColorBlock> ms_NitroColorBlocks;
static std::vector<SNitroEmitterBlend> ms_NitroEmitterBlends;
static bool ms_bNitroColorChannelsCached = false;
static CFxSystemSAInterface* ms_pLastNitroFxSystem = nullptr;
static std::optional<SColor> ms_LastAppliedNitroColor;

//////////////////////////////////////////////////////////////////////////////////////////
//
// CacheNitroColorChannels
//
// Collects every R/G/B/A(/Range) keyframe channel of the "nitro" blueprint's colour data
// and each emitter's original blend mode, so that ApplyNitroColor can later re-tint or
// restore them on a per-vehicle basis without losing precision.
//
//////////////////////////////////////////////////////////////////////////////////////////
static void CacheNitroColorChannels(CFxSystemBPSAInterface* pBlueprint)
{
if (ms_bNitroColorChannelsCached)
return;

for (uint8_t i = 0; i < (uint8_t)pBlueprint->cNumOfPrims; ++i)
{
CFxEmitterBPSAInterface* pEmitterBP = (CFxEmitterBPSAInterface*)pBlueprint->pPrims[i];

ms_NitroEmitterBlends.push_back({pEmitterBP, pEmitterBP->m_nDstBlendId});

for (uint32_t j = 0; j < pEmitterBP->m_infoManager.m_nNumInfos; ++j)
{
FxInfoSAInterface* pInfo = pEmitterBP->m_infoManager.m_pInfos[j];

uint8_t stride = 0;
if (pInfo->nType == FX_INFO_COLOUR_DATA || pInfo->nType == FX_INFO_COLOURBRIGHT_DATA)
stride = 1;
else if (pInfo->nType == FX_INFO_COLOURRANGE_DATA)
stride = 2;

if (stride == 0)
continue;

// Each colour component spans `stride` consecutive channels, with alpha last and
// never randomised (e.g. ColourRange stores R, RRange, G, GRange, B, BRange, A).
FxInfoColorSAInterface* pColorInfo = (FxInfoColorSAInterface*)pInfo;

SNitroColorBlock block;
block.nNumKeyframes = (uint8_t)pColorInfo->nNumKeyframes;
block.nStride = stride;

for (int component = 0; component < 4; ++component)
{
for (int s = 0; s < stride; ++s)
{
int channel = component * stride + s;
if (channel >= pColorInfo->nNumChannels)
break;

uint16_t* pValues = pColorInfo->ppChannelValues[channel];
block.pChannelValues[component][s] = pValues;
block.originalValues[component][s].assign(pValues, pValues + block.nNumKeyframes);
}
}
ms_NitroColorBlocks.push_back(std::move(block));
}
}
ms_bNitroColorChannelsCached = true;
}

//////////////////////////////////////////////////////////////////////////////////////////
//
// ApplyNitroColor
//
// If `color` has no value, restores every cached "nitro" colour channel and the emitters'
// blend modes to their original values (i.e. the game's default additive cyan effect).
//
// Otherwise, re-tints every cached channel using `color` and switches the emitters to
// standard alpha blending, so dark colours (including black) remain visible instead of
// fading out as the additive blending's contribution approaches zero. For R/G/B, rather
// than multiplying the original (cyan) values by the requested colour - which would mix the
// two hues together - the brightest of a block's original R/G/B values at each keyframe is
// used as a brightness envelope, and every channel is recoloured from scratch using that
// envelope. This allows any colour, including white, to fully replace the original hue.
// Alpha and the randomisation ranges instead scale the original keyframes, preserving the
// flame's fade animation.
//
// The blend mode is shared by every nitro particle rendered in a frame, so vehicles using
// the original colours share it whenever both kinds are on screen at once.
//
// Always derived from the cached originals, so it can be called repeatedly for different
// vehicles without ever needing to restore the blueprint first.
//
//////////////////////////////////////////////////////////////////////////////////////////
static void ApplyNitroColor(const std::optional<SColor>& color)
{
for (auto& blend : ms_NitroEmitterBlends)
blend.pEmitterBP->m_nDstBlendId = color.has_value() ? FX_BLEND_ID_INVSRCALPHA : blend.nOriginalDstBlendId;

for (auto& block : ms_NitroColorBlocks)
{
if (!color.has_value())
{
for (int component = 0; component < 4; ++component)
{
for (int s = 0; s < block.nStride; ++s)
{
if (uint16_t* pValues = block.pChannelValues[component][s])
std::copy(block.originalValues[component][s].begin(), block.originalValues[component][s].end(), pValues);
}
}
continue;
}

const uint32_t components[4] = {color->R, color->G, color->B, color->A};

for (uint8_t keyframe = 0; keyframe < block.nNumKeyframes; ++keyframe)
{
uint16_t brightness = 0;
for (int component = 0; component < 3; ++component)
brightness = std::max(brightness, block.originalValues[component][0][keyframe]);

for (int component = 0; component < 4; ++component)
{
for (int s = 0; s < block.nStride; ++s)
{
uint16_t* pValues = block.pChannelValues[component][s];
if (!pValues)
continue;

const uint16_t source = (component < 3 && s == 0) ? brightness : block.originalValues[component][s][keyframe];
pValues[keyframe] = (uint16_t)(source * components[component] / 255);
}
}
}
}
}

//////////////////////////////////////////////////////////////////////////////////////////
//
// FxEmitterBP_c::Render
//
// Called once per particle while the "nitro" blueprint's particles are rendered.
// Re-tints the shared blueprint's colour data with the owning vehicle's nitro colour
// just before its colour keyframes are sampled.
//
//////////////////////////////////////////////////////////////////////////////////////////
__declspec(noinline) static void OnFxParticleProcessRenderInfo(CFxParticleSAInterface* pParticle)
{
CFxSystemSAInterface* pFxSystem = pParticle->pSystem;
if (!pFxSystem || !pFxSystem->pBlueprint || pFxSystem->pBlueprint->uiNameHash != FX_BLUEPRINT_HASH_NITRO)
return;

CVehicle* pVehicle = pGame->GetFxManagerSA()->GetVehicleFromNitroSystem(pFxSystem);
const std::optional<SColor> color = pVehicle ? pVehicle->GetNitroColor() : std::nullopt;

if (pFxSystem == ms_pLastNitroFxSystem && color == ms_LastAppliedNitroColor)
return;

CacheNitroColorChannels(pFxSystem->pBlueprint);
ApplyNitroColor(color);

ms_pLastNitroFxSystem = pFxSystem;
ms_LastAppliedNitroColor = color;
}

// Hook info
// Redirects the CALL that initialises a particle's default render colour (at the start of
// FxEmitterBP_c::Render's per-particle loop) through our hook first.
// At this call site: EBX = current particle (CFxParticleSAInterface*, callee-saved).
#define HOOKPOS_FxEmitterBP_c_Render_ProcessRenderInfo 0x4A2E31
DWORD ORIGINAL_FxEmitterBP_c_Render_ProcessRenderInfo = 0x4A4A80;
static void __declspec(naked) HOOK_FxEmitterBP_c_Render_ProcessRenderInfo()
{
MTA_VERIFY_HOOK_LOCAL_SIZE;

// clang-format off
__asm
{
pushad
push ebx
call OnFxParticleProcessRenderInfo
add esp, 4
popad

jmp ORIGINAL_FxEmitterBP_c_Render_ProcessRenderInfo
}
// clang-format on
}

// Variables used in the hooks
static CFxSystemSAInterface* ms_pUsingFxSystemSAInterface = NULL;
static float ms_fUsingDrawDistance = 0;
Expand Down Expand Up @@ -278,6 +496,10 @@ void CFxSystemSA::StaticSetHooks()
EZHookInstall(FxSystem_c_Update_MidA);
EZHookInstall(FxSystem_c_Update_MidB);

// Redirect the per-particle render info CALL in FxEmitterBP_c::Render so we can apply
// per-vehicle nitro colours just before they are sampled
HookInstallCall(HOOKPOS_FxEmitterBP_c_Render_ProcessRenderInfo, (DWORD)HOOK_FxEmitterBP_c_Render_ProcessRenderInfo);

// Redirect these constants so we can change them
MemPut<float*>(VAR_FxSystemUpdateCullDistMultiplier, &ms_fFxSystemUpdateCullDistMultiplier);
MemPut<float*>(VAR_FxCreateParticleCullDistMultiplierA, &ms_fFxCreateParticleCullDistMultiplier);
Expand Down
47 changes: 42 additions & 5 deletions Client/game_sa/CFxSystemSA.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,35 @@ class CAEFireAudioEntitySAInterface
};
static_assert(sizeof(CAEFireAudioEntitySAInterface) == 0x88, "Invalid size for CAEFireAudioEntitySAInterface");

class FxInfoSAInterface;
// Internal SA Name: FxInfo_c
// nType is one of the eFxInfoType values (e.g. FX_INFO_COLOUR_DATA)
class FxInfoSAInterface
{
public:
void* vmt; // 0x00
uint16_t nType; // 0x04
};

constexpr uint16_t FX_INFO_COLOUR_DATA = 0x4001;
constexpr uint16_t FX_INFO_COLOURBRIGHT_DATA = 0x4400;
constexpr uint16_t FX_INFO_COLOURRANGE_DATA = 0x4100;

// Internal SA Name: FxInfoColour_c (FxInfo_c + an embedded FxInterpInfoU255_c)
// Holds ppChannelValues[channel][keyframe] for nNumChannels channels (4 = R, G, B, A) of
// nNumKeyframes keyframes each. Values are fixed-point, scaled by 1/256.
class FxInfoColorSAInterface : public FxInfoSAInterface
{
public:
void* pInterpInfoVmt; // 0x08
bool bLooped; // 0x0C
int8_t nNumKeyframes; // 0x0D
int8_t nNumChannels; // 0x0E
char pad; // 0x0F
uint16_t* pTimes; // 0x10
uint16_t** ppChannelValues; // 0x14
};
static_assert(sizeof(FxInfoColorSAInterface) == 0x18, "Invalid size for FxInfoColorSAInterface");

class CFxSystemBPSAInterface;

class CFxSystemSAInterface // Internal SA Name: FxSystem_c
Expand Down Expand Up @@ -103,10 +131,10 @@ class CFxSystemSA : public CFxSystem
class FxInfoManagerSAInterface
{
public:
uint32_t m_nNumInfos; // 0x00
FxInfoSAInterface* m_pInfos; // 0x04
uint8_t m_nFirstMovementInfo; // 0x08
uint8_t m_nFirstRenderInfo; // 0x09
uint32_t m_nNumInfos; // 0x00
FxInfoSAInterface** m_pInfos; // 0x04
uint8_t m_nFirstMovementInfo; // 0x08
uint8_t m_nFirstRenderInfo; // 0x09
};
static_assert(sizeof(FxInfoManagerSAInterface) == 0xC, "Invalid size for FxInfoManagerSAInterface");

Expand Down Expand Up @@ -144,3 +172,12 @@ class CFxEmitterSAInterface
CFxSystemSAInterface* pOwner; // 0x08
// TODO the rest
};

// Internal SA Name: FxParticle_c
// A single particle instance, owned by an FxSystem_c and rendered via its FxEmitterBP_c.
class CFxParticleSAInterface
{
public:
uint8_t pad[0x28]; // 0x00
CFxSystemSAInterface* pSystem; // 0x28
};
Loading