Skip to content
Draft
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
25 changes: 25 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -2984,6 +2984,31 @@ UnlimboDetonate.KeepSelected=false ; boolean

## Weapons

## Allow Laser drawing position update

- Now you can define whether the endpoints of a laser drawing are updated during its duration.
- `None`: No update.
- `Firer`: The start point follows the firer's FLH; if the firer dies, the update stops.
- Since `DiskLaser`'s FLH actually determines the center of the ring, this would cause the beam's start point after charging to become the FLH, so this scenario may not be suitable for using `Firer`.
- `Target`: The end point follows the target; if the target object dies, the update stops.
- `All`: Equivalent to specifying both `Firer` and `Target`.

```{note}
For a sub-weapon created by `ShrapnelWeapon`, its start point is the position where the parent weapon detonates, not the firer's FLH.
- If `Firer` is set, it will be treated as `None`.
- If `All` is set, it will be treated as `Target`.
```

In `rulesmd.ini`:
```ini
[SOMEWEAPON] ; WeaponType with IsLaser=yes or DiskLaser=yes
LaserPositionUpdate=none ; Position Follow Enumeration (none|firer|target|all)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a global value would be useful?

Copy link
Copy Markdown
Collaborator Author

@DeathFishAtEase DeathFishAtEase May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, I've gone to the corresponding topic on Forum and started a poll to see.

```

```{warning}
If the weapon sets this logic to a non-`None` value while also using other logics that change the drawing position, such as `FlakScatter`, then after initially drawing the laser according to those other logics, the drawing position will be forced to change due to the update rules.
```

### AreaFire target customization

- You can now specify how AreaFire weapon picks its target. By default it targets the base cell the firer is currently on, but this can now be changed to fire on the firer itself or at a random cell within the radius of the weapon's `Range` by setting `AreaFire.Target` to `self` or `random` respectively.
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ New:
- [Electric bolt Z-adjust](Fixed-or-Improved-Logics.md#electric-bolt-z-adjust) (by Noble_Fish)
- Allow disabling the processing of the Z-depth of EBolt drawn by BuildingType being clamped to non-positive numbers (by Noble_Fish)
- Add the `Bolt.ZAdjust` setting item to the LaserTrailType with `DrawType=ebolt` (by Noble_Fish)
- Allow Laser drawing position update (by Noble_Fish)
Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
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 @@ -117,6 +117,7 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->AreaFire_Target.Read(exINI, pSection, "AreaFire.Target");
this->FeedbackWeapon.Read<true>(exINI, pSection, "FeedbackWeapon");
this->Laser_IsSingleColor.Read(exINI, pSection, "IsSingleColor");
this->LaserPositionUpdate.Read(exINI, pSection, "LaserPositionUpdate");
this->LaserZAdjust.Read(exINI, pSection, "LaserZAdjust");
this->EBoltZAdjust.Read(exINI, pSection, "EBoltZAdjust");
this->EBoltZAdjust_ClampInitialDepthForBuilding.Read(exINI, pSection, "EBoltZAdjust.ClampInitialDepthForBuilding");
Expand Down Expand Up @@ -216,6 +217,7 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm)
.Process(this->AreaFire_Target)
.Process(this->FeedbackWeapon)
.Process(this->Laser_IsSingleColor)
.Process(this->LaserPositionUpdate)
.Process(this->LaserZAdjust)
.Process(this->EBoltZAdjust)
.Process(this->EBoltZAdjust_ClampInitialDepthForBuilding)
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 @@ -45,6 +45,7 @@ class WeaponTypeExt
Valueable<AreaFireTarget> AreaFire_Target;
Valueable<WeaponTypeClass*> FeedbackWeapon;
Valueable<bool> Laser_IsSingleColor;
Valueable<PositionFollow> LaserPositionUpdate;
Nullable<int> LaserZAdjust;
Nullable<int> EBoltZAdjust;
Nullable<bool> EBoltZAdjust_ClampInitialDepthForBuilding;
Expand Down Expand Up @@ -134,6 +135,7 @@ class WeaponTypeExt
, AreaFire_Target { AreaFireTarget::Base }
, FeedbackWeapon {}
, Laser_IsSingleColor { false }
, LaserPositionUpdate { PositionFollow::None }
, LaserZAdjust {}
, EBoltZAdjust {}
, EBoltZAdjust_ClampInitialDepthForBuilding {}
Expand Down
291 changes: 291 additions & 0 deletions src/Misc/Hooks.LaserDraw.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <Ext\WeaponType\Body.h>
#include <Ext/Techno/Body.h>
#include <Helpers/Macro.h>
#include <Utilities/GeneralUtils.h>
#include <unordered_map>

namespace LaserDrawTemp
{
Expand Down Expand Up @@ -53,3 +55,292 @@ DEFINE_HOOK(0x6FD3FD, TechnoClass_LaserZap_ZAdjust, 0x5)

return 0;
}

#pragma region LaserPositionUpdate

namespace LaserRT
{
struct TrackingData
{
TechnoClass* Shooter = nullptr;
ObjectClass* Target = nullptr;
bool IsFloorTarget = false;
int WeaponIndex = 0;
bool IsActive = false;
PositionFollow FollowMode = PositionFollow::None;
CoordStruct SavedRelativeFLH { 0, 0, 0 };
};

std::unordered_map<LaserDrawClass*, TrackingData> g_Trackers;
ObjectClass* g_PendingShrapnelTarget = nullptr;

static CoordStruct GetRelativeFLH(TechnoClass* pShooter, int weaponIndex)
{
bool flhFound = false;
CoordStruct relFLH = TechnoExt::GetBurstFLH(pShooter, weaponIndex, flhFound);
if (!flhFound)
{
relFLH = pShooter->GetWeapon(weaponIndex)->FLH;
if (pShooter->CurrentBurstIndex % 2 != 0)
relFLH.Y = -relFLH.Y;
}
return relFLH;
}
}

static void RemoveLaserTracking(LaserDrawClass* pLaser)
{
LaserRT::g_Trackers.erase(pLaser);
}

// IsLaser ctor/dtor/remove hooks
DEFINE_HOOK(0x54FE60, LaserDrawClass_CTOR_Update, 5)
{
GET(LaserDrawClass*, pLaser, ECX);
LaserRT::g_Trackers[pLaser] = LaserRT::TrackingData {};
return 0;
}

DEFINE_HOOK(0x54FFB0, LaserDrawClass_DTOR_Update, 7)
{
GET(LaserDrawClass*, pLaser, ECX);
RemoveLaserTracking(pLaser);
return 0;
}

DEFINE_HOOK(0x550016, LaserDrawClass_Remove1_Update, 6)
{
GET(LaserDrawClass*, pLaser, ECX);
RemoveLaserTracking(pLaser);
return 0;
}

DEFINE_HOOK(0x5500EF, LaserDrawClass_Remove2_Update, 5)
{
GET(LaserDrawClass*, pLaser, ECX);
RemoveLaserTracking(pLaser);
return 0;
}

DEFINE_HOOK(0x5501D7, LaserDrawClass_Remove3_Update, 5)
{
GET(LaserDrawClass*, pLaser, ECX);
RemoveLaserTracking(pLaser);
return 0;
}

// IsLaser building fire hook
DEFINE_HOOK(0x6FF50C, AfterCreateLaser_Building_Activate, 6)
{
R->ECX(0x8871E0);
GET(LaserDrawClass*, pLaser, EAX);
GET(WeaponTypeClass*, pWeapon, EBX);
GET(ObjectClass*, pTarget, EDI);
GET(TechnoClass*, pShooter, ESI);
int idxWeapon = *(int*)(R->EBP() + 0x0C);

if (!pWeapon) return 0;
auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate;
if (mode != PositionFollow::None && pLaser)
{
LaserRT::TrackingData data;
data.Shooter = pShooter;
data.Target = pTarget;
data.IsFloorTarget = (pTarget && pTarget->WhatAmI() == AbstractType::Cell);
data.WeaponIndex = idxWeapon;
data.IsActive = true;
data.FollowMode = mode;
data.SavedRelativeFLH = LaserRT::GetRelativeFLH(pShooter, idxWeapon);
LaserRT::g_Trackers[pLaser] = data;
}
return 0;
}

// IsLaser non‑building fire hook
DEFINE_HOOK(0x6FF563, AfterCreateLaser_NonBuilding_Activate, 6)
{
R->CL(*(reinterpret_cast<BYTE*>(R->EBX() + 0x14D)));
GET(LaserDrawClass*, pLaser, EAX);
GET(WeaponTypeClass*, pWeapon, EBX);
GET(ObjectClass*, pTarget, EDI);
GET(TechnoClass*, pShooter, ESI);
int idxWeapon = *(int*)(R->EBP() + 0x0C);

if (!pWeapon) return 0;
auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate;
if (mode != PositionFollow::None && pLaser)
{
LaserRT::TrackingData data;
data.Shooter = pShooter;
data.Target = pTarget;
data.IsFloorTarget = (pTarget && pTarget->WhatAmI() == AbstractType::Cell);
data.WeaponIndex = idxWeapon;
data.IsActive = true;
data.FollowMode = mode;
data.SavedRelativeFLH = LaserRT::GetRelativeFLH(pShooter, idxWeapon);
LaserRT::g_Trackers[pLaser] = data;
}
return 0;
}

// DiskLaser main beam activation
DEFINE_HOOK(0x4A7696, DiskLaser_Update_ActivateMainBeam, 6)
{
GET(LaserDrawClass*, pLaser, EAX);
R->EAX(*(DWORD*)(R->ESI() + 0x2C));
R->EDX(*(DWORD*)(R->ESI() + 0x24));

if (pLaser)
{
GET(DiskLaserClass*, pDiskLaser, ESI);
WeaponTypeClass* pWeapon = pDiskLaser->Weapon;
if (!pWeapon) return 0;
auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate;
if (mode != PositionFollow::None)
{
LaserRT::TrackingData data;
data.Shooter = pDiskLaser->Owner;
data.Target = pDiskLaser->Target;
data.IsFloorTarget = (data.Target && data.Target->WhatAmI() == AbstractType::Cell);
data.WeaponIndex = 0;
data.IsActive = true;
data.FollowMode = mode;
data.SavedRelativeFLH = LaserRT::GetRelativeFLH(data.Shooter, data.WeaponIndex);
LaserRT::g_Trackers[pLaser] = data;
}
}
return 0;
}

DEFINE_HOOK(0x4A7809, DiskLaser_Update_NoActivate2, 7)
{
R->EAX(*(DWORD*)(0x8A0180 + R->EBX() * 8));
return 0;
}

DEFINE_HOOK(0x4A78E4, DiskLaser_Update_NoActivate3, 10)
{
R->EAX(*(DWORD*)(R->ESI() + 0x38));
*(DWORD*)(R->ESI() + 0x30) = 1;
return 0;
}

// ShrapnelWeapon branch 1 (target is cell content)
DEFINE_HOOK(0x46A8B1, Shrapnel_CreateLaser1_Adjust, 6)
{
GET(LaserDrawClass*, pLaser, EAX);
GET(WeaponTypeClass*, pWeapon, ESI);
GET(ObjectClass*, pTarget, EBP);

R->AL(*(BYTE*)(R->ESI() + 0x151));

if (!pWeapon) return 0;
auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate;
if (pLaser)
{
if (mode == PositionFollow::Firer)
mode = PositionFollow::None;
else if (mode == PositionFollow::All)
mode = PositionFollow::Target;

if (mode != PositionFollow::None)
{
LaserRT::TrackingData data;
data.Shooter = nullptr;
data.Target = pTarget;
data.IsFloorTarget = (pTarget && pTarget->WhatAmI() == AbstractType::Cell);
data.WeaponIndex = 0;
data.IsActive = true;
data.FollowMode = mode;
LaserRT::g_Trackers[pLaser] = data;
}
}
return 0;
}

// ShrapnelWeapon branch 2 (target is the cell itself) – part 1
DEFINE_HOOK(0x46AD7A, Shrapnel_CaptureTargetCell, 5)
{
R->ECX(*(DWORD*)(R->EDI() + 0x0B0));
LaserRT::g_PendingShrapnelTarget = (ObjectClass*)R->EAX();
return 0;
}

// ShrapnelWeapon branch 2 – part 2
DEFINE_HOOK(0x46AD86, Shrapnel_CreateLaser2_Adjust, 6)
{
GET(LaserDrawClass*, pLaser, EAX);
GET(WeaponTypeClass*, pWeapon, ESI);

R->AL(*(BYTE*)(R->ESI() + 0x151));

ObjectClass* pTarget = LaserRT::g_PendingShrapnelTarget;
if (!pWeapon || !pTarget || !pLaser) return 0;
auto mode = WeaponTypeExt::ExtMap.Find(pWeapon)->LaserPositionUpdate;
if (mode == PositionFollow::Firer)
mode = PositionFollow::None;
else if (mode == PositionFollow::All)
mode = PositionFollow::Target;

if (mode != PositionFollow::None)
{
LaserRT::TrackingData data;
data.Shooter = nullptr;
data.Target = pTarget;
data.IsFloorTarget = (pTarget->WhatAmI() == AbstractType::Cell);
data.WeaponIndex = 0;
data.IsActive = true;
data.FollowMode = mode;
LaserRT::g_Trackers[pLaser] = data;
}
return 0;
}

// Per‑frame coordinate update
DEFINE_HOOK(0x550173, LaserDrawClass_Update_UpdateCoords, 6)
{
GET(LaserDrawClass*, pLaser, ESI);

auto it = LaserRT::g_Trackers.find(pLaser);
if (it == LaserRT::g_Trackers.end() || !it->second.IsActive)
return 0;

auto& info = it->second;
const bool followFirer = (info.Shooter != nullptr) &&
(info.FollowMode == PositionFollow::Firer || info.FollowMode == PositionFollow::All);
const bool followTarget = (info.Target != nullptr) &&
(info.FollowMode == PositionFollow::Target || info.FollowMode == PositionFollow::All);

if (followFirer)
{
if (ObjectClass::Array.FindItemIndex(info.Shooter) != -1 && !info.Shooter->IsDead())
{
CoordStruct flh = TechnoExt::GetFLHAbsoluteCoords(info.Shooter, info.SavedRelativeFLH, true);
pLaser->Source = flh;
}
else
{
info.IsActive = false;
return 0;
}
}

if (followTarget)
{
if (!info.IsFloorTarget && ObjectClass::Array.FindItemIndex(info.Target) != -1 && !info.Target->IsDead())
{
CoordStruct tgt;
info.Target->GetTargetCoords(&tgt);
pLaser->Target = tgt;
}
else if (!info.IsFloorTarget)
{
info.IsActive = false;
return 0;
}
}

return 0;
}

#pragma endregion
8 changes: 8 additions & 0 deletions src/Utilities/Enum.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ enum class SlaveChangeOwnerType
Neutral = 4,
};

enum class PositionFollow : BYTE
{
None = 0,
Firer = 1,
Target = 2,
All = 3
};

enum class AutoDeathBehavior
{
Kill = 0, // default death option
Expand Down
Loading
Loading