Skip to content

[Vanilla Enhancement] Restore turret recoil effect #1625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 16, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ This page lists all the individual contributions to the project by their author.
- Aggressive attack move mission
- Amphibious access vehicle
- Fix an issue that spawned `Strafe` aircraft on aircraft carriers may not be able to return normally if aircraft carriers moved a short distance when the aircraft is landing
- Restore turret recoil effect
- Fix an issue that `FireAngle` was not taken into account when drawing barrel in `TurretShadow`
- **Ollerus**:
- Build limit group enhancement
- Customizable rocker amplitude
Expand Down
23 changes: 23 additions & 0 deletions docs/Fixed-or-Improved-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,29 @@ In `artmd.ini`:
TurretShadow= ; boolean
```

### Turret recoil

- Now you can use `TurretRecoil` to control units’ turret/barrel recoil effect when firing.
- `TurretTravel` and `BarrelTravel` control the maximum recoil distance.
- `TurretRecoil.Suppress` can prevent the weapon from producing this effect when firing.

In `rulesmd.ini`:
```ini
[SOMEVEHICLE] ; VehicleType
TurretRecoil=no ; boolean
TurretTravel=2 ; integer, pixels
TurretCompressFrames=1 ; integer, game frames
TurretHoldFrames=1 ; integer, game frames
TurretRecoverFrames=1 ; integer, game frames
BarrelTravel=2 ; integer, pixels
BarrelCompressFrames=1 ; integer, game frames
BarrelHoldFrames=1 ; integer, game frames
BarrelRecoverFrames=1 ; integer, game frames

[SOMEWEAPON] ; WeaponType
TurretRecoil.Suppress=no ; boolean
```

### Customize harvester dump amount

- Now you can limit how much ore the harvester can dump out per time, like it in Tiberium Sun.
Expand Down
2 changes: 2 additions & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ New:
- Amphibious access vehicle (by CrimRecya)
- Allow miners do area guard (by TaranDahl)
- Make harvesters do addtional scan after unload (by TaranDahl)
- Restore turret recoil effect (by CrimRecya)

Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
Expand All @@ -398,6 +399,7 @@ Phobos fixes:
- Fixed an issue that MCV will self-destruct when using trigger 107 to teleport (by CrimRecya)
- Fixed the bug that 'AllowAirstrike=no' cannot completely prevent air strikes from being launched against it (by NetsuNegi)
- Fixed an issue that spawned `Strafe` aircraft on aircraft carriers may not be able to return normally if aircraft carriers moved a short distance when the aircraft is landing (by CrimRecya)
- Fixed an issue that `FireAngle` was not taken into account when drawing barrel in `TurretShadow` (by CrimRecya)

Fixes / interactions with other extensions:
- Allowed `AuxBuilding` and Ares' `SW.Aux/NegBuildings` to count building upgrades (by Ollerus)
Expand Down
9 changes: 9 additions & 0 deletions src/Ext/Techno/Hooks.Firing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,15 @@ DEFINE_HOOK(0x6FF43F, TechnoClass_FireAt_FeedbackWeapon, 0x6)
return 0;
}

DEFINE_HOOK(0x6FF0DD, TechnoClass_FireAt_TurretRecoil, 0x6)
{
enum { SkipGameCode = 0x6FF15B };

GET_STACK(WeaponTypeClass* const, pWeapon, STACK_OFFSET(0xB0, -0x70));

return WeaponTypeExt::ExtMap.Find(pWeapon)->TurretRecoil_Suppress ? SkipGameCode : 0;
}

DEFINE_HOOK(0x6FF905, TechnoClass_FireAt_FireOnce, 0x6)
{
GET(TechnoClass*, pThis, ESI);
Expand Down
164 changes: 146 additions & 18 deletions src/Ext/TechnoType/Hooks.MatrixOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,141 @@ DEFINE_HOOK(0x73B780, UnitClass_DrawVXL_TurretMultiOffset, 0x0)
return 0x73B790;
}

DEFINE_HOOK(0x73BA4C, UnitClass_DrawVXL_TurretMultiOffset1, 0x0)
struct DummyTypeExtHere
{
LEA_STACK(Matrix3D*, mtx, STACK_OFFSET(0x1D0, -0x13C));
GET(TechnoTypeClass*, technoType, EBX);
char _[0xA4];
std::vector<VoxelStruct> ChargerTurrets;
std::vector<VoxelStruct> ChargerBarrels;
char __[0x120];
UnitTypeClass* WaterImage;
VoxelStruct NoSpawnAltVXL;
};

DEFINE_HOOK(0x73BA12, UnitClass_DrawAsVXL_RewriteTurretDrawing, 0x6)
{
enum { SkipGameCode = 0x73BEA4 };

GET(UnitClass* const, pThis, EBP);
GET(UnitTypeClass* const, pDrawType, EBX);
GET_STACK(const bool, haveTurretCache, STACK_OFFSET(0x1C4, -0x1B3));
GET_STACK(const bool, haveBar, STACK_OFFSET(0x1C4, -0x1B2));
GET(const bool, haveBarrelCache, EAX);
REF_STACK(Matrix3D, draw_matrix, STACK_OFFSET(0x1C4, -0x130));
GET_STACK(const int, flags, STACK_OFFSET(0x1C4, -0x198));
GET_STACK(const int, brightness, STACK_OFFSET(0x1C4, 0x1C));
GET_STACK(const int, hvaFrameIdx, STACK_OFFSET(0x1C4, -0x18C));
GET_STACK(const int, currentTurretNumber, STACK_OFFSET(0x1C4, -0x1A8));
LEA_STACK(Point2D* const, center, STACK_OFFSET(0x1C4, -0x194));
LEA_STACK(RectangleStruct* const, rect, STACK_OFFSET(0x1C4, -0x164));

// base matrix
const auto mtx = Matrix3D::VoxelDefaultMatrix * draw_matrix;

const auto pDrawTypeExt = TechnoTypeExt::ExtMap.Find(pDrawType);
const bool notChargeTurret = pThis->Type->TurretCount <= 0 || pThis->Type->IsGattling;

auto getTurretVoxel = [pDrawType, notChargeTurret, currentTurretNumber]() -> VoxelStruct*
{
if (notChargeTurret)
return &pDrawType->TurretVoxel;

// Not considering the situation where there is no Ares and the limit is exceeded
if (currentTurretNumber < 18 || !AresHelper::CanUseAres)
return &pDrawType->ChargerTurrets[currentTurretNumber];

auto* aresTypeExt = reinterpret_cast<DummyTypeExtHere*>(pDrawType->align_2FC);
return &aresTypeExt->ChargerTurrets[currentTurretNumber - 18];
};
const auto pTurretVoxel = getTurretVoxel();

// When in recoiling or have no cache, need to recalculate drawing matrix
const bool inRecoil = pDrawType->TurretRecoil && (pThis->TurretRecoil.State != RecoilData::RecoilState::Inactive || pThis->BarrelRecoil.State != RecoilData::RecoilState::Inactive);
const bool shouldRedraw = !haveTurretCache || haveBar && !haveBarrelCache || inRecoil;

// When in recoiling, need to bypass cache and draw without saving
const auto turKey = inRecoil ? -1 : flags;
const auto turCache = inRecoil ? nullptr : reinterpret_cast<IndexClass<int, int>*>(&pDrawType->VoxelTurretWeaponCache);

auto getTurretMatrix = [=, &mtx]() -> Matrix3D
{
auto mtx_turret = mtx;
pDrawTypeExt->ApplyTurretOffset(&mtx_turret, Pixel_Per_Lepton);
mtx_turret.RotateZ(static_cast<float>(pThis->SecondaryFacing.Current().GetRadian<32>() - pThis->PrimaryFacing.Current().GetRadian<32>()));

if (pThis->TurretRecoil.State != RecoilData::RecoilState::Inactive)
mtx_turret.TranslateX(-pThis->TurretRecoil.TravelSoFar);

return mtx_turret;
};
auto mtx_turret = shouldRedraw ? getTurretMatrix() : mtx;

// 10240u -> (BlitterFlags::Alpha | BlitterFlags::Flat);

// Only when there is a barrel will its calculation and drawing be considered
if (haveBar)
{
auto drawBarrel = [=, &mtx_turret, &mtx]()
{
// When in recoiling, need to bypass cache and draw without saving
const auto brlKey = inRecoil ? -1 : flags;
const auto brlCache = inRecoil ? nullptr : reinterpret_cast<IndexClass<int, int>*>(&pDrawType->VoxelTurretBarrelCache);

auto getBarrelMatrix = [=, &mtx_turret, &mtx]() -> Matrix3D
{
auto mtx_barrel = mtx_turret;
mtx_barrel.Translate(-mtx.Row[0].W, -mtx.Row[1].W, -mtx.Row[2].W);
mtx_barrel.RotateY(static_cast<float>(-pThis->BarrelFacing.Current().GetRadian<32>()));

if (pThis->BarrelRecoil.State != RecoilData::RecoilState::Inactive)
mtx_barrel.TranslateX(-pThis->BarrelRecoil.TravelSoFar);

mtx_barrel.Translate(mtx.Row[0].W, mtx.Row[1].W, mtx.Row[2].W);
return mtx_barrel;
};
auto mtx_barrel = shouldRedraw ? getBarrelMatrix() : mtx;

auto getBarrelVoxel = [pDrawType, notChargeTurret, currentTurretNumber]() -> VoxelStruct*
{
if (notChargeTurret)
return &pDrawType->BarrelVoxel;

// Not considering the situation where there is no Ares and the limit is exceeded
if (currentTurretNumber < 18 || !AresHelper::CanUseAres)
return &pDrawType->ChargerBarrels[currentTurretNumber];

auto* aresTypeExt = reinterpret_cast<DummyTypeExtHere*>(pDrawType->align_2FC);
return &aresTypeExt->ChargerBarrels[currentTurretNumber - 18];
};
const auto pBarrelVoxel = getBarrelVoxel();

// draw barrel
pThis->Draw_A_VXL(pBarrelVoxel, hvaFrameIdx, brlKey, brlCache, rect, center, &mtx_barrel, brightness, 10240u, 0);
};

const auto turretDir = pThis->SecondaryFacing.Current().GetFacing<4>();

// The orientation of the turret can affect the layer order of the barrel and turret
if (turretDir != 0 && turretDir != 3)
{
// draw turret
pThis->Draw_A_VXL(pTurretVoxel, hvaFrameIdx, turKey, turCache, rect, center, &mtx_turret, brightness, 10240u, 0);

TechnoTypeExt::ApplyTurretOffset(technoType, mtx, Pixel_Per_Lepton);
drawBarrel();
}
else
{
drawBarrel();

return 0x73BA68;
// draw turret
pThis->Draw_A_VXL(pTurretVoxel, hvaFrameIdx, turKey, turCache, rect, center, &mtx_turret, brightness, 10240u, 0);
}
}
else
{
pThis->Draw_A_VXL(pTurretVoxel, hvaFrameIdx, turKey, turCache, rect, center, &mtx_turret, brightness, 10240u, 0);
}

return SkipGameCode;
}

DEFINE_HOOK(0x73C890, UnitClass_DrawSHP_BarrelMultiOffset, 0x0)
Expand Down Expand Up @@ -333,16 +460,6 @@ Matrix3D* __stdcall TunnelLocomotionClass_ShadowMatrix(ILocomotion* iloco, Matri
}
DEFINE_FUNCTION_JUMP(VTABLE, 0x7F5A4C, TunnelLocomotionClass_ShadowMatrix);

struct DummyTypeExtHere
{
char _[0xA4];
std::vector<VoxelStruct> ChargerTurrets;
std::vector<VoxelStruct> ChargerBarrels;
char __[0x120];
UnitTypeClass* WaterImage;
VoxelStruct NoSpawnAltVXL;
};

DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
{
GET(UnitClass*, pThis, EBP);
Expand Down Expand Up @@ -536,6 +653,10 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
uTypeExt->ApplyTurretOffset(&mtx, Pixel_Per_Lepton);
mtx.RotateZ(static_cast<float>(pThis->SecondaryFacing.Current().GetRadian<32>() - pThis->PrimaryFacing.Current().GetRadian<32>()));

auto inRecoil = pType->TurretRecoil && pThis->TurretRecoil.State != RecoilData::RecoilState::Inactive;
if (inRecoil)
mtx.TranslateX(-pThis->TurretRecoil.TravelSoFar);

auto tur = GetTurretVoxel(pThis->CurrentTurretNumber);
if (!(tur && tur->VXL && tur->HVA))
return SkipDrawing;
Expand All @@ -560,17 +681,23 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
pThis->DrawVoxelShadow(
tur,
0,
vxl_index_key,
cache,
(inRecoil ? VoxelIndexKey(-1) : vxl_index_key),
(inRecoil ? nullptr : cache),
bnd,
&why,
&mtx,
cache != nullptr,
(!inRecoil && cache != nullptr),
surface,
shadow_point
);

if (haveBar)// you are utterly fucked, for now
{
if (pType->TurretRecoil && pThis->BarrelRecoil.State != RecoilData::RecoilState::Inactive)
mtx.TranslateX(-pThis->BarrelRecoil.TravelSoFar);

mtx.ScaleX(static_cast<float>(Math::cos(-pThis->BarrelFacing.Current().GetRadian<32>())));

pThis->DrawVoxelShadow(
bar,
0,
Expand All @@ -583,6 +710,7 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
surface,
shadow_point
);
}

return SkipDrawing;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Ext/WeaponType/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->ChargeTurret_Delays.Read(exINI, pSection, "ChargeTurret.Delays");
this->OmniFire_TurnToTarget.Read(exINI, pSection, "OmniFire.TurnToTarget");
this->FireOnce_ResetSequence.Read(exINI, pSection, "FireOnce.ResetSequence");
this->TurretRecoil_Suppress.Read(exINI, pSection, "TurretRecoil.Suppress");
this->ExtraWarheads.Read(exINI, pSection, "ExtraWarheads");
this->ExtraWarheads_DamageOverrides.Read(exINI, pSection, "ExtraWarheads.DamageOverrides");
this->ExtraWarheads_DetonationChances.Read(exINI, pSection, "ExtraWarheads.DetonationChances");
Expand Down Expand Up @@ -160,6 +161,7 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm)
.Process(this->ChargeTurret_Delays)
.Process(this->OmniFire_TurnToTarget)
.Process(this->FireOnce_ResetSequence)
.Process(this->TurretRecoil_Suppress)
.Process(this->ExtraWarheads)
.Process(this->ExtraWarheads_DamageOverrides)
.Process(this->ExtraWarheads_DetonationChances)
Expand Down
2 changes: 2 additions & 0 deletions src/Ext/WeaponType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class WeaponTypeExt
ValueableVector<int> ChargeTurret_Delays;
Valueable<bool> OmniFire_TurnToTarget;
Valueable<bool> FireOnce_ResetSequence;
Valueable<bool> TurretRecoil_Suppress;
ValueableVector<WarheadTypeClass*> ExtraWarheads;
ValueableVector<int> ExtraWarheads_DamageOverrides;
ValueableVector<double> ExtraWarheads_DetonationChances;
Expand Down Expand Up @@ -102,6 +103,7 @@ class WeaponTypeExt
, ChargeTurret_Delays {}
, OmniFire_TurnToTarget { false }
, FireOnce_ResetSequence { true }
, TurretRecoil_Suppress { false }
, ExtraWarheads {}
, ExtraWarheads_DamageOverrides {}
, ExtraWarheads_DetonationChances {}
Expand Down
8 changes: 8 additions & 0 deletions src/Misc/Hooks.BugFixes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,14 @@ DEFINE_HOOK(0x6FC617, TechnoClass_GetFireError_Spawner, 0x8)

#pragma endregion

#pragma region TurretRecoilReadFix

// Skip incorrect copy, why do copy like this?
DEFINE_JUMP(LJMP, 0x715326, 0x715333); // TechnoTypeClass::LoadFromINI
// Then EDI is BarrelAnimData now, not incorrect TurretAnimData

#pragma endregion

#pragma region TeamCloseRangeFix

int __fastcall Check2DDistanceInsteadOf3D(ObjectClass* pSource, void* _, AbstractClass* pTarget)
Expand Down