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
1 change: 1 addition & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -930,3 +930,4 @@ This page lists all the individual contributions to the project by their author.
- Interop export interface for accessing scenario local/global variables
- Add `ClampToScreen` tag for `BannerType` to control whether banner position is clamped to the visible area
- **obsidianus** - Automatic conversion based on health
- **Nuke** - Veteran reload time customization
26 changes: 26 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -2454,6 +2454,32 @@ TiberiumEater.AnimMove=true ; boolean
TiberiumEater.UnderEMP=false ; boolean
```

### Veteran reload time customization

- In vanilla, veteran and elite technos only support the predefined ability list. Now you can add the following abilities to `VeteranAbilities` and `EliteAbilities` to speed up or slow down their ammo reload.
- `RELOAD` scales the duration of normal reload cycles.
- `EMPTY_RELOAD` scales the duration of the reload cycle that starts when the clip is completely empty, which is only used when the TechnoType has a positive `EmptyReload`.
- Abilities from `VeteranAbilities` keep working at elite level, and `EliteAbilities` are added on top, matching how vanilla abilities accumulate.
- `VeteranReload` and `VeteranEmptyReload` are multipliers applied to the final reload duration computed by the game, so other reload modifiers such as `ReloadIncrement` are still taken into account.
- Values greater than `1.0` lengthen the reload, values smaller than `1.0` shorten it, and `1.0` (the default) leaves it unchanged.

```{note}
For `EMPTY_RELOAD` to have any effect, the TechnoType must have a positive `EmptyReload` value, otherwise the empty clip uses the regular `Reload` duration instead.
```

In `rulesmd.ini`:
```ini
[General]
VeteranReload=1.0 ; floating point value, multiplier
VeteranEmptyReload= ; floating point value, multiplier, default to [General] -> VeteranReload

[SOMETECHNO] ; TechnoType
VeteranReload= ; floating point value, multiplier, default to [General] -> VeteranReload
VeteranEmptyReload= ; floating point value, multiplier, default to [General] -> VeteranEmptyReload
VeteranAbilities= ; Ability, `RELOAD` and `EMPTY_RELOAD` in addition to the vanilla abilities
EliteAbilities= ; Ability, `RELOAD` and `EMPTY_RELOAD` in addition to the vanilla abilities
```

### Weapons fired on warping in / out

- It is now possible to add weapons that are fired on a teleporting TechnoType when it warps in or out. They are at the same time as the appropriate animations (`WarpIn` / `WarpOut`) are displayed.
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ HideShakeEffects=false ; boolean
- [Allow the unit to keep pursuing the target during ApproachTarget](Fixed-or-Improved-Logics.md#keep-pursuing-the-target-during-approachtarget) (by TaranDahl)
- [Customize whether warhead can prevent crew escape from techno](New-or-Enhanced-Logics.md#customize-whether-warhead-can-prevent-crew-escape-from-techno) (by NetsuNegi)
- [Disable AlphaImage during Buildup](Fixed-or-Improved-Logics.md#disable-alphaimage-during-buildup) (by Noble_Fish)
- [Veteran reload time customization](New-or-Enhanced-Logics.md#veteran-reload-time-customization) (by Nuke)

#### 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 docs/locale/zh_CN/LC_MESSAGES/CREDITS.po
Original file line number Diff line number Diff line change
Expand Up @@ -3178,3 +3178,5 @@ msgstr "为 `BannerType` 添加了 `ClampToScreen` 标签以控制横幅位置
msgid "**obsidianus** - Automatic conversion based on health"
msgstr "**obsidianus** - 自动根据血量变形"

msgid "**Nuke** - Veteran reload time customization"
msgstr "**Nuke** - 自定义老兵与精英装填时间"
47 changes: 47 additions & 0 deletions docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po
Original file line number Diff line number Diff line change
Expand Up @@ -5230,6 +5230,53 @@ msgid ""
msgstr ""
"如果 `TiberiumEater.UnderEMP` 设为 true 则在单位受 EMP 影响或瘫痪时继续执行吃矿操作。"

msgid "Veteran reload time customization"
msgstr "自定义老兵与精英装填时间"

msgid ""
"In vanilla, veteran and elite technos only support the predefined ability "
"list. Now you can add the following abilities to `VeteranAbilities` and "
"`EliteAbilities` to speed up or slow down their ammo reload."
msgstr ""
"原版中,老兵和精英单位只能使用预定义的能力列表。现在你可以在 `VeteranAbilities` 和 `EliteAbilities` "
"中添加以下能力,以加快或减慢其弹药装填速度。"

msgid "`RELOAD` scales the duration of normal reload cycles."
msgstr "`RELOAD` 缩放普通装填周期的时长。"

msgid ""
"`EMPTY_RELOAD` scales the duration of the reload cycle that starts when the "
"clip is completely empty, which is only used when the TechnoType has a "
"positive `EmptyReload`."
msgstr "`EMPTY_RELOAD` 缩放弹夹完全打空后开始的空仓装填周期时长,仅当单位具有正的 `EmptyReload` 时才会使用。"

msgid ""
"Abilities from `VeteranAbilities` keep working at elite level, and "
"`EliteAbilities` are added on top, matching how vanilla abilities "
"accumulate."
msgstr ""
"`VeteranAbilities` 中的能力在精英等级仍然生效,`EliteAbilities` 则在此基础上额外生效,这与原版能力的叠加方式一致。"

msgid ""
"`VeteranReload` and `VeteranEmptyReload` are multipliers applied to the "
"final reload duration computed by the game, so other reload modifiers such "
"as `ReloadIncrement` are still taken into account."
msgstr ""
"`VeteranReload` 和 `VeteranEmptyReload` 是对游戏最终计算出的装填时长的倍率,因此 "
"`ReloadIncrement` 等其他装填修正仍然会生效。"

msgid ""
"Values greater than `1.0` lengthen the reload, values smaller than `1.0` "
"shorten it, and `1.0` (the default) leaves it unchanged."
msgstr "大于 `1.0` 的数值会延长装填时间,小于 `1.0` 的数值会缩短装填时间,`1.0`(默认值)则保持不变。"

msgid ""
"For `EMPTY_RELOAD` to have any effect, the TechnoType must have a positive "
"`EmptyReload` value, otherwise the empty clip uses the regular `Reload` "
"duration instead."
msgstr ""
"要使 `EMPTY_RELOAD` 生效,单位必须设置正的 `EmptyReload` 值,否则打空弹夹时仍会使用普通的 `Reload` 时长。"

msgid "Weapons fired on warping in / out"
msgstr "武器在传送时开火"

Expand Down
8 changes: 8 additions & 0 deletions docs/locale/zh_CN/LC_MESSAGES/Whats-New.po
Original file line number Diff line number Diff line change
Expand Up @@ -2459,6 +2459,14 @@ msgstr ""
"[自定义弹头是否阻止生还者逃出](New-or-Enhanced-Logics.md#customize-whether-warhead-can-"
"prevent-crew-escape-from-techno)(by NetsuNegi)"

msgid ""
"[Veteran reload time customization](New-or-Enhanced-Logics.md#veteran-"
"reload-time-customization) (by Nuke)"
msgstr ""
"[自定义老兵与精英装填时间](New-or-Enhanced-Logics.md#veteran-reload-time-"
"customization)(by Nuke)"


msgid "Vanilla fixes:"
msgstr "原版问题修复:"

Expand Down
22 changes: 22 additions & 0 deletions src/Ext/Rules/Body.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "Body.h"

#include <cmath>

#include <Ext/TechnoType/Body.h>
#include <New/Type/RadTypeClass.h>
#include <New/Type/ShieldTypeClass.h>
Expand Down Expand Up @@ -352,6 +354,24 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->NoRearm_Temporal.Read(exINI, GameStrings::General, "NoRearm.Temporal");
this->NoReload_UnderEMP.Read(exINI, GameStrings::General, "NoReload.UnderEMP");
this->NoReload_Temporal.Read(exINI, GameStrings::General, "NoReload.Temporal");

this->VeteranReload.Read(exINI, GameStrings::General, "VeteranReload");
this->VeteranEmptyReload.Read(exINI, GameStrings::General, "VeteranEmptyReload");

const auto validateReloadMultiplier = [](const char* pKey, Valueable<double>& value)
{
if (!std::isfinite(value.Get()) || value.Get() <= 0.0)
{
Debug::INIParseFailed(GameStrings::General, pKey, "<invalid>", "Expected a finite value greater than 0.0");
value = 1.0;
}
};

validateReloadMultiplier("VeteranReload", this->VeteranReload);

if (this->VeteranEmptyReload.isset())
validateReloadMultiplier("VeteranEmptyReload", this->VeteranEmptyReload);

this->NoTurret_TrackTarget.Read(exINI, GameStrings::General, "NoTurret.TrackTarget");

this->GatherWhenMCVDeploy.Read(exINI, GameStrings::General, "GatherWhenMCVDeploy");
Expand Down Expand Up @@ -901,6 +921,8 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->NoRearm_Temporal)
.Process(this->NoReload_UnderEMP)
.Process(this->NoReload_Temporal)
.Process(this->VeteranReload)
.Process(this->VeteranEmptyReload)
.Process(this->NoTurret_TrackTarget)
.Process(this->GatherWhenMCVDeploy)
.Process(this->AIFireSale)
Expand Down
4 changes: 4 additions & 0 deletions src/Ext/Rules/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ class RulesExt
Valueable<bool> NoRearm_Temporal;
Valueable<bool> NoReload_UnderEMP;
Valueable<bool> NoReload_Temporal;
Valueable<double> VeteranReload;
Nullable<double> VeteranEmptyReload;
Valueable<bool> NoTurret_TrackTarget;

Valueable<bool> GatherWhenMCVDeploy;
Expand Down Expand Up @@ -771,6 +773,8 @@ class RulesExt
, NoRearm_Temporal { false }
, NoReload_UnderEMP { false }
, NoReload_Temporal { false }
, VeteranReload { 1.0 }
, VeteranEmptyReload {}
, NoTurret_TrackTarget { false }
, GatherWhenMCVDeploy { true }
, AIFireSale { true }
Expand Down
17 changes: 17 additions & 0 deletions src/Ext/Techno/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ void TechnoExt::SyncInvulnerability(TechnoClass* pFrom, TechnoClass* pTo)
}
}

bool TechnoExt::HasAdditionalAbility(TechnoClass* pThis, AdditionalAbility ability)
{
if (!pThis || (!pThis->Veterancy.IsVeteran() && !pThis->Veterancy.IsElite()))
return false;

const auto index = static_cast<size_t>(ability);
const auto pTypeExt = TechnoExt::Fetch(pThis)->TypeExtData;

if (pThis->Veterancy.IsElite())
{
return pTypeExt->AdditionalVeteranAbilities.test(index)
|| pTypeExt->AdditionalEliteAbilities.test(index);
}

return pTypeExt->AdditionalVeteranAbilities.test(index);
}

double TechnoExt::GetCurrentSpeedMultiplier(FootClass* pThis)
{
double houseMultiplier = 1.0;
Expand Down
1 change: 1 addition & 0 deletions src/Ext/Techno/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ class TechnoExt : public RadioExt, public Detach::Listener<AirstrikeClass>
static void KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, const std::vector<AnimTypeClass*>& pVanishAnimation, bool isInLimbo = false);
static void ObjectKilledBy(TechnoClass* pThis, TechnoClass* pKiller);
static void UpdateSharedAmmo(TechnoClass* pThis);
static bool HasAdditionalAbility(TechnoClass* pThis, AdditionalAbility ability);
static double GetCurrentSpeedMultiplier(FootClass* pThis);
static double GetCurrentFirepowerMultiplier(TechnoClass* pThis);
static double GetCurrentArmorMultiplier(TechnoClass* pThis, TechnoTypeClass* pType, HouseClass* pSourceHouse = nullptr, WarheadTypeClass* pWarhead = nullptr);
Expand Down
40 changes: 40 additions & 0 deletions src/Ext/Techno/Hooks.Firing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
#include <Ext/Building/Body.h>
#include <Ext/Bullet/Body.h>
#include <Ext/Infantry/Body.h>
#include <Ext/Rules/Body.h>
#include <Ext/Unit/Body.h>
#include <Ext/WarheadType/Body.h>
#include <Ext/WeaponType/Body.h>
#include <Utilities/GeneralUtils.h>

#include <cmath>

#pragma region TechnoClass_SelectWeapon

Expand Down Expand Up @@ -1157,6 +1161,42 @@ DEFINE_HOOK(0x6FB086, TechnoClass_Reload_ReloadAmount, 0x8)
return 0;
}

static inline int ScaleReloadDurationForVeterancy(TechnoClass* pThis, int duration, AdditionalAbility ability)
{
if (duration <= 0 || !TechnoExt::HasAdditionalAbility(pThis, ability))
return duration;

const double multiplier = ability == AdditionalAbility::EmptyReload
? TechnoExt::Fetch(pThis)->TypeExtData->VeteranEmptyReload.Get(RulesExt::Global()->VeteranEmptyReload.Get(RulesExt::Global()->VeteranReload))
: TechnoExt::Fetch(pThis)->TypeExtData->VeteranReload.Get(RulesExt::Global()->VeteranReload);

return Math::max(1, GeneralUtils::SafeMultiply(duration, multiplier));
}

// Scale the reload cycle that uses the EmptyReload duration (clip empty and `EmptyReload` is set).
// Hooked at `add esi, 1FCh` in StartReloading: EAX holds the duration, ESI holds `this`.
DEFINE_HOOK(0x6FB0CF, TechnoClass_StartReloading_EmptyReload_Veterancy, 0x6)
{
GET(TechnoClass* const, pThis, ESI);
GET(const int, duration, EAX);

R->EAX(ScaleReloadDurationForVeterancy(pThis, duration, AdditionalAbility::EmptyReload));

return 0;
}

// Scale the normal reload cycle duration for veterancy. The duration in EAX is the final
// value computed by the game, including the ReloadIncrement adjustment.
DEFINE_HOOK(0x6FB14C, TechnoClass_StartReloading_Reload_Veterancy, 0x6)
{
GET(TechnoClass* const, pThis, ESI);
GET(const int, duration, EAX);

R->EAX(ScaleReloadDurationForVeterancy(pThis, duration, AdditionalAbility::Reload));

return 0;
}

// Author: Otamaa
DEFINE_HOOK(0x5223B3, InfantryClass_Approach_Target_DeployFireWeapon, 0x6)
{
Expand Down
50 changes: 50 additions & 0 deletions src/Ext/TechnoType/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,43 @@

#include <Utilities/AresHelper.h>

void ReadAdditionalAbilities(
INI_EX& parser,
const char* section,
const char* key,
std::bitset<AdditionalAbilityCount>& result)
{
std::vector<std::string> values;

if (!parser.ParseStringList(values, section, key))
return;

// When the key is present, fully replace the previous value with this
// list so that map INIs can override rules values.
result.reset();

for (const auto& value : values)
{
if (!_stricmp(value.c_str(), "RELOAD"))
{
result.set(static_cast<size_t>(AdditionalAbility::Reload));
}
else if (!_stricmp(value.c_str(), "EMPTY_RELOAD"))
{
result.set(static_cast<size_t>(AdditionalAbility::EmptyReload));
}
}
}

void ValidateReloadMultiplier(const char* pSection, const char* pKey, Nullable<double>& value)
{
if (value.isset() && (!std::isfinite(value.Get()) || value.Get() <= 0.0))
{
Debug::INIParseFailed(pSection, pKey, "<invalid>", "Expected a finite value greater than 0.0");
value.Reset();
}
}

bool TechnoTypeExt::SelectWeaponMutex = false;

void TechnoTypeExt::ApplyTurretOffset(Matrix3D* mtx, double factor)
Expand Down Expand Up @@ -1010,6 +1047,15 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI)
this->NoReload_UnderEMP.Read(exINI, pSection, "NoReload.UnderEMP");
this->NoReload_Temporal.Read(exINI, pSection, "NoReload.Temporal");

ReadAdditionalAbilities(exINI, pSection, "VeteranAbilities", this->AdditionalVeteranAbilities);
ReadAdditionalAbilities(exINI, pSection, "EliteAbilities", this->AdditionalEliteAbilities);

this->VeteranReload.Read(exINI, pSection, "VeteranReload");
this->VeteranEmptyReload.Read(exINI, pSection, "VeteranEmptyReload");

ValidateReloadMultiplier(pSection, "VeteranReload", this->VeteranReload);
ValidateReloadMultiplier(pSection, "VeteranEmptyReload", this->VeteranEmptyReload);

this->Wake.Read(exINI, pSection, "Wake");
this->Wake_Grapple.Read(exINI, pSection, "Wake.Grapple");
this->Wake_Sinking.Read(exINI, pSection, "Wake.Sinking");
Expand Down Expand Up @@ -1641,6 +1687,10 @@ void TechnoTypeExt::Serialize(T& Stm)
.Process(this->NoRearm_Temporal)
.Process(this->NoReload_UnderEMP)
.Process(this->NoReload_Temporal)
.Process(this->AdditionalVeteranAbilities)
.Process(this->AdditionalEliteAbilities)
.Process(this->VeteranReload)
.Process(this->VeteranEmptyReload)

.Process(this->Wake)
.Process(this->Wake_Grapple)
Expand Down
10 changes: 10 additions & 0 deletions src/Ext/TechnoType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ class TechnoTypeExt : public ObjectTypeExt
Nullable<bool> NoReload_UnderEMP;
Nullable<bool> NoReload_Temporal;

std::bitset<AdditionalAbilityCount> AdditionalVeteranAbilities;
std::bitset<AdditionalAbilityCount> AdditionalEliteAbilities;
Nullable<double> VeteranReload;
Nullable<double> VeteranEmptyReload;

Nullable<AnimTypeClass*> Wake;
Nullable<AnimTypeClass*> Wake_Grapple;
Nullable<AnimTypeClass*> Wake_Sinking;
Expand Down Expand Up @@ -690,6 +695,11 @@ class TechnoTypeExt : public ObjectTypeExt
, NoReload_UnderEMP {}
, NoReload_Temporal {}

, AdditionalVeteranAbilities {}
, AdditionalEliteAbilities {}
, VeteranReload {}
, VeteranEmptyReload {}

, Wake { }
, Wake_Grapple { }
, Wake_Sinking { }
Expand Down
13 changes: 13 additions & 0 deletions src/Utilities/Enum.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#pragma once

#include <cstddef>
#include <GeneralDefinitions.h>

enum class AttachedAnimFlag
Expand Down Expand Up @@ -440,3 +441,15 @@ enum class EdgeType : BYTE
Closest = 1,
Random = 2
};

// Phobos extension abilities that augment the vanilla veteran/elite ability
// lists (VeteranAbilities / EliteAbilities). Do not extend the vanilla Ability
// enum, whose storage is a fixed-size AbilitiesStruct.
enum class AdditionalAbility : unsigned char
{
Reload = 0,
EmptyReload = 1,
Count
};

constexpr size_t AdditionalAbilityCount = static_cast<size_t>(AdditionalAbility::Count);
Loading