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 7 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
3 changes: 3 additions & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,9 @@ This page lists all the individual contributions to the project by their author.
- Fix an issue that if the garrison unload occupants when there is no open space around it would result in the disappearance of the occupants
- Fix an issue where Ares' `Convert.Deploy` triggers repeatedly when the unit is turning or moving
- Reverse engineer warhead
- Restore turret recoil effect
- Fix an issue that `FireAngle` was not taken into account when drawing barrel in `TurretShadow`
- Fix an issue that barrel anim data will be incorrectly overwritten by turret anim data if the techno's section exists in the map file
- **Ollerus**:
- Build limit group enhancement
- Customizable rocker amplitude
Expand Down
28 changes: 28 additions & 0 deletions docs/Fixed-or-Improved-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho
- Fixed the bug that hover vehicle will sink if destroyed on bridge.
- Fixed the fact that when the selected unit is in a rearmed state, it can unconditionally use attack mouse on the target.
- When `Speed=0` or the TechnoTypes cell cannot move due to `MovementRestrictedTo`, vehicles cannot attack targets beyond the weapon's range. `Area Guard` and `Hunt` missions will also become ineffective.
- Fixed an issue that barrel anim data will be incorrectly overwritten by turret anim data if the techno's section exists in the map file.

## Fixes / interactions with other extensions

Expand Down Expand Up @@ -1817,6 +1818,33 @@ 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
```

```{note}
This is not a 1:1 restoration but a separate thing, not like it was in *Tiberian Sun*.
```

### 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
3 changes: 3 additions & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ New:
- [Auto deploy for GI-like infantry](Fixed-or-Improved-Logics.md#auto-deploy-for-gi-like-infantry) (by TaranDahl)
- When the vehicle loses its target, you can customize whether to align the turret direction with the vehicle body (by FlyStar)
- Reverse engineer warhead (by CrimRecya)
- 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 @@ -443,10 +444,12 @@ Vanilla fixes:
- Fixed an issue where airstrike flare line drawn to target at lower elevation would clip (by Starkku)
- Fixed the bug that damaged particle dont disappear after building has repaired by engineer (by NetsuNegi)
- Projectiles with `Vertical=true` now drop straight down if fired off by AircraftTypes instead of behaving erratically (by Starkku)
- Fixed an issue that barrel anim data will be incorrectly overwritten by turret anim data if the techno's section exists in the map file (by CrimRecya)

Phobos fixes:
- Fixed the bug that `AllowAirstrike=no` cannot completely prevent air strikes from being launched against it (by NetsuNegi)
- When `Speed=0` or the TechnoTypes cell cannot move due to `MovementRestrictedTo`, vehicles cannot attack targets beyond the weapon's range. `Area Guard` and `Hunt` missions will also become ineffective (by FlyStar)
- 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 @@ -642,6 +642,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
216 changes: 173 additions & 43 deletions src/Ext/TechnoType/Hooks.MatrixOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,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(TechnoTypeClass* 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;

TechnoTypeExt::ApplyTurretOffset(technoType, mtx, Pixel_Per_Lepton);
// 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);

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 @@ -414,16 +541,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 @@ -553,6 +670,7 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
if (uTypeExt->ShadowIndices.empty())
{
if (pType->ShadowIndex >= 0 && pType->ShadowIndex < main_vxl->HVA->LayerCount)
{
pThis->DrawVoxelShadow(
main_vxl,
pType->ShadowIndex,
Expand All @@ -565,10 +683,12 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
surface,
shadow_point
);
}
}
else
{
for (auto& [index, _] : uTypeExt->ShadowIndices)
{
pThis->DrawVoxelShadow(
main_vxl,
index,
Expand All @@ -581,49 +701,54 @@ DEFINE_HOOK(0x73C47A, UnitClass_DrawAsVXL_Shadow, 0x5)
surface,
shadow_point
);
}
}
}

if (main_vxl == &pType->TurretVoxel || (!pType->UseTurretShadow && !uTypeExt->TurretShadow.Get(RulesExt::Global()->DrawTurretShadow)))
return SkipDrawing;

auto GetTurretVoxel = [pType](int idx) ->VoxelStruct*
{
if (pType->TurretCount == 0 || pType->IsGattling || idx < 0)
return &pType->TurretVoxel;
{
if (pType->TurretCount == 0 || pType->IsGattling || idx < 0)
return &pType->TurretVoxel;

if (idx < 18)
return &pType->ChargerTurrets[idx];
if (idx < 18)
return &pType->ChargerTurrets[idx];

if (AresHelper::CanUseAres)
{
auto* aresTypeExt = reinterpret_cast<DummyTypeExtHere*>(pType->align_2FC);
return &aresTypeExt->ChargerTurrets[idx - 18];
}
if (AresHelper::CanUseAres)
{
auto* aresTypeExt = reinterpret_cast<DummyTypeExtHere*>(pType->align_2FC);
return &aresTypeExt->ChargerTurrets[idx - 18];
}

return nullptr;
};
return nullptr;
};

auto GetBarrelVoxel = [pType](int idx)->VoxelStruct*
{
if (pType->TurretCount == 0 || pType->IsGattling || idx < 0)
return &pType->BarrelVoxel;
{
if (pType->TurretCount == 0 || pType->IsGattling || idx < 0)
return &pType->BarrelVoxel;

if (idx < 18)
return &pType->ChargerBarrels[idx];
if (idx < 18)
return &pType->ChargerBarrels[idx];

if (AresHelper::CanUseAres)
{
auto* aresTypeExt = reinterpret_cast<DummyTypeExtHere*>(pType->align_2FC);
return &aresTypeExt->ChargerBarrels[idx - 18];
}
if (AresHelper::CanUseAres)
{
auto* aresTypeExt = reinterpret_cast<DummyTypeExtHere*>(pType->align_2FC);
return &aresTypeExt->ChargerBarrels[idx - 18];
}

return nullptr;
};
return nullptr;
};

uTypeExt->ApplyTurretOffset(&mtx, Pixel_Per_Lepton);
mtx.RotateZ(static_cast<float>(pThis->SecondaryFacing.Current().GetRadian<32>() - pThis->PrimaryFacing.Current().GetRadian<32>()));

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

const auto tur = GetTurretVoxel(pThis->CurrentTurretNumber);
if (!(tur && tur->VXL && tur->HVA))
return SkipDrawing;
Expand Down Expand Up @@ -651,18 +776,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 (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 Down
2 changes: 2 additions & 0 deletions src/Ext/WeaponType/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,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 @@ -195,6 +196,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
Loading
Loading