Skip to content
Merged
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 @@ -758,6 +758,7 @@ This page lists all the individual contributions to the project by their author.
- Customize whether the unit can be detected by psychic detector
- Fix the bug that setting `WalkRate=0` on a TechnoType crashed the game (integer divide-by-zero) the moment an object of that type started moving
- Fix the bug where `Ranged=true` causes projectiles using the new Trajectory to ignore settings such as `BounceTimes`
- Customizable infantry sequence rates
- **Ollerus**:
- Build limit group enhancement
- Customizable rocker amplitude
Expand Down
83 changes: 83 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,89 @@ DeployedPrimaryFireFLH= ; integer - Forward,Lateral,Height
DeployedSecondaryFireFLH= ; integer - Forward,Lateral,Height
```

### Customizable infantry sequence rates

In vanilla, the number of game frames each animation frame of the infantry sequence stays (i.e., its playback rate) is hardcoded. Now it can be customized for each sequence in each infantry's sequence section.
- `<Sequence>Rate` defines the number of game logic frames that 1 animation frame stays; 0 means still, for example, the `Ready` and `Guard` sequences in vanilla are still.
- `<Sequence>Normalized` defines whether the playback rate is affected by game speed, for example, the `Idle1`, `Idle2`, `WetIdle1`, `WetIdle2`, `Hover`, and `Cheer` sequences in vanilla are affected by game speed.

```{note}
The effective rate of a normalized sequence is `rate` adjusted by the game speed:

- `rate < 5`: from the table below (`rate` row, `GameSpeed` column).
- `rate >= 5`: `rate * 8 / (GameSpeed + 1)`.

| Rate \ GameSpeed | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| ---------------- | - | - | - | - | - | - | - |
| 1 | 2 | 2 | 1 | 1 | 1 | 1 | 1 |
| 2 | 3 | 3 | 3 | 2 | 2 | 2 | 1 |
| 3 | 5 | 4 | 4 | 3 | 3 | 2 | 2 |
| 4 | 7 | 6 | 5 | 4 | 4 | 4 | 3 |

`GameSpeed` goes from `0` (fastest) to `6` (slowest).
```

In `rulesmd.ini`:
```ini
[AudioVisual]
Sequence.<Sequence>.DefaultRate=0 ; integer
Sequence.<Sequence>.DefaultNormalized=no ; boolean
```

```{dropdown} Default value table
| Name | DefaultRate | Normalized |
|-----------------|-------------|------------|
| Ready | 0 | false |
| Guard | 0 | false |
| Prone | 6 | false |
| Walk | 3 | false |
| FireUp | 1 | false |
| Down | 1 | false |
| Crawl | 1 | false |
| Up | 1 | false |
| FireProne | 1 | false |
| Idle1 | 3 | true |
| Idle2 | 3 | true |
| Die1 | 1 | false |
| Die2 | 1 | false |
| Die3 | 1 | false |
| Die4 | 1 | false |
| Die5 | 1 | false |
| Tread | 3 | false |
| Swim | 1 | false |
| WetIdle1 | 3 | true |
| WetIdle2 | 3 | true |
| WetDie1 | 1 | false |
| WetDie2 | 1 | false |
| WetAttack | 1 | false |
| Hover | 2 | true |
| Fly | 1 | false |
| Tumble | 1 | false |
| FireFly | 1 | false |
| Deploy | 1 | false |
| Deployed | 1 | false |
| DeployedFire | 1 | false |
| DeployedIdle | 1 | false |
| Undeploy | 1 | false |
| Cheer | 3 | true |
| Paradrop | 1 | false |
| AirDeathStart | 3 | false |
| AirDeathFalling | 1 | false |
| AirDeathFinish | 3 | false |
| Panic | 4 | false |
| Shovel | 6 | false |
| Carry | 3 | false |
| SecondaryFire | 1 | false |
| SecondaryProne | 1 | false |
```

In `artmd.ini`:
```ini
[SEQUENCE] ; Sequence
<Sequence>Rate= ; integer, defaults to [AudioVisual] -> Sequence.<Sequence>.DefaultRate
<Sequence>Normalized= ; boolean, defaults to [AudioVisual] -> Sequence.<Sequence>.DefaultNormalized
```
Comment thread
Coronia marked this conversation as resolved.

### Customizable `SlavesFreeSound`

- `SlavesFreeSound` can now be set individually for each enslavable infantry type.
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ HideShakeEffects=false ; boolean
- [Customize whether the unit can be detected by psychic detector](Fixed-or-Improved-Logics.md#customize-whether-the-unit-can-be-detected-by-psychic-detector) (by Noble_Fish)
- Cloak Enhancement (by NetsuNegi)
- [`513` Set mission timer properties](AI-Scripting-and-Mapping.md#set-mission-timer-properties) (by Ollerus)
- [Customizable infantry sequence rates](New-or-Enhanced-Logics.md#customizable-infantry-sequence-rates) (by Noble_Fish)

#### Vanilla fixes:
- Fixed the bug where a building with `Factory=BuildingType` owned by the AI did not play `ProductionAnim` when placing a produced building (by Noble_Fish)
Expand Down
3 changes: 3 additions & 0 deletions docs/locale/zh_CN/LC_MESSAGES/CREDITS.po
Original file line number Diff line number Diff line change
Expand Up @@ -2579,6 +2579,9 @@ msgstr "天窗生产动画"
msgid "Customize whether the unit exits from the roof"
msgstr "自定义单位是否从天窗退出"

msgid "Customizable infantry sequence rates"
msgstr "自定义步兵序列播放速率"

msgid "**Ollerus**:"
msgstr "**Ollerus**:"

Expand Down
39 changes: 39 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 @@ -1928,6 +1928,45 @@ msgstr ""
"现在步兵处于匍匐(爪巴)或部署状态的 FLH 可以独立于 `PrimaryFireFLH` 和 `SecondaryFireFLH` "
"进行设置。同样对 [Burst 开火坐标](#firing-offsets-for-specific-burst-shots) 生效。"

msgid "Customizable infantry sequence rates"
msgstr "自定义步兵序列播放速率"

msgid ""
"In vanilla, the number of game frames each animation frame of the "
"infantry sequence stays (i.e., its playback rate) is hardcoded. Now it "
"can be customized for each sequence in each infantry's sequence section."
msgstr ""
"在原版中,步兵序列每个动画帧所停留的游戏帧数(即其播放速率)是硬编码的。现在可以在每个步兵的序列段落中为每个序列自定义。"

msgid ""
"`<Sequence>Rate` defines the number of game logic frames that 1 animation"
" frame stays; 0 means still, for example, the `Ready` and `Guard` "
"sequences in vanilla are still."
msgstr ""
"`<Sequence>Rate` 用于定义 1 动画帧所停留的游戏逻辑帧数,0 代表静止,例如原版中的 `Ready` 和 `Guard` 序列是静止的。"

msgid ""
"`<Sequence>Normalized` defines whether the playback rate is affected by "
"game speed, for example, the `Idle1`, `Idle2`, `WetIdle1`, `WetIdle2`, "
"`Hover`, and `Cheer` sequences in vanilla are affected by game speed."
msgstr ""
"`<Sequence>Normalized` 用于定义播放速率是否受游戏速度影响,例如原版中的 `Idle1`、`Idle2`、`WetIdle1`、`WetIdle2`、`Hover` 和 `Cheer` 序列受游戏速度影响。"

msgid ""
"The effective rate of a normalized sequence is `rate` adjusted by the "
"game speed:"
msgstr ""
"`Normalized` 化序列的有效速率为经过游戏速度调整后的 `rate`:"

msgid "`rate < 5`: from the table below (`rate` row, `GameSpeed` column)."
msgstr "`rate < 5`:使用下表(`rate` 行,`GameSpeed` 列)。"

msgid "`rate >= 5`: `rate * 8 / (GameSpeed + 1)`."
msgstr "`rate >= 5`:`rate * 8 / (GameSpeed + 1)`。"

msgid "`GameSpeed` goes from `0` (fastest) to `6` (slowest)."
msgstr "`GameSpeed` 的取值范围为 `0`(最快)到 `6`(最慢)。"

msgid "Customizable `SlavesFreeSound`"
msgstr "自定义 `SlavesFreeSound`"

Expand Down
6 changes: 6 additions & 0 deletions docs/locale/zh_CN/LC_MESSAGES/Whats-New.po
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,12 @@ msgstr ""
msgid "Cloak Enhancement (by NetsuNegi)"
msgstr "隐形增强(by NetsuNegi)"

msgid ""
"[Customizable infantry sequence rates](New-or-Enhanced-Logics.md"
"#customizable-infantry-sequence-rates) (by Noble_Fish)"
msgstr ""
"[自定义步兵序列播放速率](New-or-Enhanced-Logics.md#customizable-infantry-sequence-rates)(by Noble_Fish)"

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

Expand Down
66 changes: 66 additions & 0 deletions src/Ext/Infantry/Hooks.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#include <Ext/BuildingType/Body.h>
#include <Ext/Infantry/Body.h>
#include <Ext/InfantryType/Body.h>
#include <Ext/Rules/Body.h>

#include <InputManagerClass.h>
#include <GameOptionsClass.h>
#include <Utilities/SequenceRates.h>

DEFINE_HOOK(0x51B2BD, InfantryClass_UpdateTarget_IsControlledByHuman, 0x6)
{
Expand Down Expand Up @@ -209,3 +213,65 @@ DEFINE_HOOK(0x51A002, InfantryClass_UpdatePosition_InfiltrateBuilding, 0x6)

return 0;
}

#pragma region CustomInfantrySequenceRates

// The vanilla hardcoded per-sequence rates used as the final fallback live in SequenceRates::Entries (see Utilities/SequenceRates.h).

namespace SequenceRateHooks
{
int GetCustomRate(InfantryClass* pThis, int sequence)
{
if (sequence < 0 || sequence >= static_cast<int>(SequenceRates::Entries.size()))
return -1;

const int typeRate = InfantryTypeExt::Fetch(pThis->Type)->CustomSequenceRates[sequence];
const int customRate = typeRate >= 0 ? typeRate : RulesExt::Global()->CustomSequenceRates[sequence];

return customRate;
}

bool IsNormalized(InfantryClass* pThis, int sequence)
{
if (sequence < 0 || sequence >= static_cast<int>(SequenceRates::Entries.size()))
return false;

const int typeFlag = InfantryTypeExt::Fetch(pThis->Type)->CustomSequenceNormalized[sequence];
if (typeFlag >= 0)
return typeFlag != 0;

const int globalFlag = RulesExt::Global()->CustomSequenceNormalized[sequence];
if (globalFlag >= 0)
return globalFlag != 0;

return SequenceRates::Entries[sequence].Normalized;
}

int GetFinalRate(InfantryClass* pThis, int sequence)
{
const int customRate = GetCustomRate(pThis, sequence);
const int rate = customRate >= 0 ? customRate : SequenceRates::Entries[sequence].DefaultRate;

return IsNormalized(pThis, sequence)
? GameOptionsClass::Instance.GetAnimSpeed(rate)
: rate;
}
}

DEFINE_HOOK(0x51DA44, InfantryClass_DoAction_SequenceRate_Store, 0x6)
{
GET(InfantryClass*, pThis, ESI);
GET(int, sequence, EDI);

if (sequence >= 0 && sequence < static_cast<int>(SequenceRates::Entries.size()))
{
const int finalRate = SequenceRateHooks::GetFinalRate(pThis, sequence);

pThis->Animation.Timer.TimeLeft = finalRate;
pThis->Animation.Rate = finalRate;
}

return 0x51DA4A;
}

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

#include <Utilities/SequenceRates.h>

InfantryTypeExt::ExtContainer InfantryTypeExt::ExtMap;

// =============================
Expand Down Expand Up @@ -33,6 +35,24 @@ void InfantryTypeExt::LoadFromINIFile(CCINIClass* const pINI)
this->ProneSecondaryFireFLH.Read(exArtINI, pArtSection, "ProneSecondaryFireFLH");
this->DeployedPrimaryFireFLH.Read(exArtINI, pArtSection, "DeployedPrimaryFireFLH");
this->DeployedSecondaryFireFLH.Read(exArtINI, pArtSection, "DeployedSecondaryFireFLH");

// Per-sequence animation rates
char sequenceSection[0x20];
const char* pRateSection = pArtSection;
if (pArtINI->ReadString(pArtSection, "Sequence", "", sequenceSection, sizeof(sequenceSection)) > 0)
pRateSection = sequenceSection;

for (size_t i = 0; i < SequenceRates::Entries.size(); ++i)
{
char key[64];
std::snprintf(key, sizeof(key), "%sRate", SequenceRates::Entries[i].Name);
exArtINI.ReadInteger(pRateSection, key, &this->CustomSequenceRates[i]);

bool normalized;
std::snprintf(key, sizeof(key), "%sNormalized", SequenceRates::Entries[i].Name);
if (exArtINI.ReadBool(pRateSection, key, &normalized))
this->CustomSequenceNormalized[i] = normalized ? 1 : 0;
}
}

template <typename T>
Expand All @@ -55,6 +75,8 @@ void InfantryTypeExt::Serialize(T& Stm)
.Process(this->DeployedWeaponBurstFLHs)
.Process(this->EliteDeployedWeaponBurstFLHs)
.Process(this->InfantryAutoDeploy)
.Process(this->CustomSequenceRates)
.Process(this->CustomSequenceNormalized)
;
}

Expand Down
8 changes: 8 additions & 0 deletions src/Ext/InfantryType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class InfantryTypeExt final : public TechnoTypeExt
std::vector<std::vector<CoordStruct>> EliteDeployedWeaponBurstFLHs;
Nullable<bool> InfantryAutoDeploy;

// Per-sequence animation rates read from the infantry's art section
std::vector<int> CustomSequenceRates;

// Per-sequence game-speed normalization flags read from the infantry's art section
std::vector<int> CustomSequenceNormalized;

explicit InfantryTypeExt(InfantryTypeClass* const OwnerObject) : TechnoTypeExt(OwnerObject)
, Slaved_OwnerWhenMasterKilled { SlaveChangeOwnerType::Killer }
, SlavesFreeSound {}
Expand All @@ -41,6 +47,8 @@ class InfantryTypeExt final : public TechnoTypeExt
, DeployedPrimaryFireFLH {}
, DeployedSecondaryFireFLH {}
, InfantryAutoDeploy {}
, CustomSequenceRates(42, -1)
, CustomSequenceNormalized(42, -1)
{ }

InfantryTypeClass* OwnerObject() const
Expand Down
18 changes: 18 additions & 0 deletions src/Ext/Rules/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <cmath>

#include <Utilities/SequenceRates.h>

#include <Ext/TechnoType/Body.h>
#include <New/Type/RadTypeClass.h>
#include <New/Type/ShieldTypeClass.h>
Expand Down Expand Up @@ -665,6 +667,20 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)

this->AIScriptsLists.emplace_back(std::move(objectsList));
}

// Global default per-sequence animation rates for infantry.
for (size_t i = 0; i < SequenceRates::Entries.size(); ++i)
{
char key[64];
std::snprintf(key, sizeof(key), "Sequence.%s.DefaultRate", SequenceRates::Entries[i].Name);
exINI.ReadInteger(GameStrings::AudioVisual, key, &this->CustomSequenceRates[i]);

bool normalized;
std::snprintf(key, sizeof(key), "Sequence.%s.DefaultNormalized", SequenceRates::Entries[i].Name);
if (exINI.ReadBool(GameStrings::AudioVisual, key, &normalized))
this->CustomSequenceNormalized[i] = normalized ? 1 : 0;
}

}

// this should load everything that TypeData is not dependant on
Expand Down Expand Up @@ -1116,6 +1132,8 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->CloakAnims)
.Process(this->DecloakAnims)
.Process(this->Cloak_KickOutParasite)
.Process(this->CustomSequenceRates)
.Process(this->CustomSequenceNormalized)
;
}

Expand Down
9 changes: 9 additions & 0 deletions src/Ext/Rules/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,12 @@ class RulesExt
ValueableVector<AnimTypeClass*> DecloakAnims;
Valueable<bool> Cloak_KickOutParasite;

// Global default per-sequence animation rates for infantry
std::vector<int> CustomSequenceRates;

// Global default per-sequence game-speed normalization flags for infantry
std::vector<int> CustomSequenceNormalized;

ExtData(RulesClass* OwnerObject) : Extension<RulesClass>(OwnerObject)
, Storage_TiberiumIndex { -1 }
, HarvesterDumpAmount { 0.0f }
Expand Down Expand Up @@ -1036,6 +1042,9 @@ class RulesExt
, CloakAnims {}
, DecloakAnims {}
, Cloak_KickOutParasite { false }

, CustomSequenceRates(42, -1)
, CustomSequenceNormalized(42, -1)
{ }

virtual ~ExtData() = default;
Expand Down
Loading
Loading