diff --git a/CREDITS.md b/CREDITS.md
index f3b359298d..ad068c6bd9 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -966,5 +966,7 @@ This page lists all the individual contributions to the project by their author.
- **obsidianus** - Automatic conversion based on health
- **Nuke** - Reload speed adjustment on promotion
- **frg2089 (舰队的偶像-岛风酱!)** - Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed
+- **dh381-1(dh381)**
+ - Add country initial elite unit lists
- **weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral houses
- **Sovietianqi** - Customizable crew type per country
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 67dc28e6ee..a172ae1b7b 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -108,6 +108,7 @@
+
diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md
index f0eb3f32c7..36de23268b 100644
--- a/docs/New-or-Enhanced-Logics.md
+++ b/docs/New-or-Enhanced-Logics.md
@@ -823,6 +823,29 @@ SpyEffect.VictimSuperWeapon= ; SuperWeaponType
SpyEffect.InfiltratorSuperWeapon= ; SuperWeaponType
```
+## Countries
+
+### Set country initial elite
+
+- Now you can add `TechnoTypes` in the three lists. They will appear at an elite initial level when produced, and cloned infantry will also be affected by this effect.
+
+In `rulesmd.ini`:
+```ini
+[SOMECOUNTRY] ; Country
+EliteInfantry= ; List of InfantryTypes
+EliteUnits= ; List of UnitTypes
+EliteAircraft= ; List of AircraftTypes
+```
+
+- For each `TechnoType`, you can define `SpareCameo` and `SpareCameoPCX` to specify the elite‑level icon. When a `TechnoType` is added to the initial elite list, it will preferentially use the SpareCameo as its icon.
+
+In `artmd.ini`:
+```ini
+[SOMETECHNO] ; TechnoType
+SpareCameo= ; filename
+SpareCameoPCX= ; filename - include ".pcx" extension
+```
+
## Infantry
### Allow infantry to perform type conversion when deploying and undeploying
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 2cf117e54f..33ebb2226b 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -444,6 +444,7 @@ HideShakeEffects=false ; boolean
- Country-based attached effects (by Ollerus)
- [Customizable crew type per country](Fixed-or-Improved-Logics.md#customizable-crew-type-per-country) (by Sovietianqi, FlyStar, Noble_Fish)
- [Country-specific veteran buildings](Fixed-or-Improved-Logics.md#country-specific-veteran-buildings) (by Noble_Fish)
+- [Set country initial elite](New-or-Enhanced-Logics.md#set-country-initial-elite) (by dh381)
#### 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)
diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp
index e53d1d2ad9..cc78b14fc9 100644
--- a/src/Ext/House/Body.cpp
+++ b/src/Ext/House/Body.cpp
@@ -713,6 +713,7 @@ void HouseExt::Serialize(T& Stm)
.Process(this->ForceRadar)
.Process(this->PlayerAutoRepair)
//.Process(this->BeaconsPlacedOrder) beacon is not saved, so this follows it.
+ .Process(this->TechnoPCX_IsLoad)
;
}
diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h
index df5ba629b6..ba6ac3737a 100644
--- a/src/Ext/House/Body.h
+++ b/src/Ext/House/Body.h
@@ -81,6 +81,8 @@ class HouseExt final : public AbstractExt, public Detach::Listener BeaconsPlacedOrder;
+ ValueableVector TechnoPCX_IsLoad;
+
HouseExt(HouseClass* OwnerObject) : AbstractExt(OwnerObject)
, PowerPlantEnhancers {}
, OwnedLimboDeliveredBuildings {}
@@ -116,6 +118,7 @@ class HouseExt final : public AbstractExt, public Detach::ListenerAttachEffects.LoadFromINI(pINI, pSection);
this->AttachEffects_AttachOnOwnerChange.Read(exINI, pSection, "AttachEffect.AttachOnOwnerChange");
+ this->Crew.Read(exINI, pSection, "Crew");
+
+ this->EliteInfantry.Read(exINI, pSection, "EliteInfantry");
+ this->EliteUnits.Read(exINI, pSection, "EliteUnits");
+ this->EliteAircraft.Read(exINI, pSection, "EliteAircraft");
this->Crew.Read(exINI, pSection, "Crew");
this->VeteranBuildings.Read(exINI, pSection, "VeteranBuildings");
@@ -33,9 +38,14 @@ void HouseTypeExt::Serialize(T& Stm)
{
Stm
.Process(this->EVATag)
+
.Process(this->AttachEffects)
.Process(this->AttachEffects_AttachOnOwnerChange)
.Process(this->Crew)
+
+ .Process(this->EliteInfantry)
+ .Process(this->EliteUnits)
+ .Process(this->EliteAircraft)
.Process(this->VeteranBuildings)
.Process(this->VeteranDefenses)
;
diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h
index 1536aa1e78..3ee414dea1 100644
--- a/src/Ext/HouseType/Body.h
+++ b/src/Ext/HouseType/Body.h
@@ -32,6 +32,9 @@ class HouseTypeExt final : public AbstractTypeExt
Nullable AttachEffects_AttachOnOwnerChange;
Nullable Crew;
+ ValueableVector EliteInfantry;
+ ValueableVector EliteUnits;
+ ValueableVector EliteAircraft;
ValueableVector VeteranBuildings;
ValueableVector VeteranDefenses;
@@ -40,6 +43,9 @@ class HouseTypeExt final : public AbstractTypeExt
, AttachEffects {}
, AttachEffects_AttachOnOwnerChange {}
, Crew {}
+ , EliteInfantry {}
+ , EliteUnits {}
+ , EliteAircraft {}
, VeteranBuildings {}
, VeteranDefenses {}
{ }
diff --git a/src/Ext/HouseType/Hooks.InitialElite.cpp b/src/Ext/HouseType/Hooks.InitialElite.cpp
new file mode 100644
index 0000000000..107b55eda4
--- /dev/null
+++ b/src/Ext/HouseType/Hooks.InitialElite.cpp
@@ -0,0 +1,152 @@
+#include "Body.h"
+
+#include
+#include
+
+static SHPStruct* LoadSHPCameo(TechnoTypeClass* pType)
+{
+ auto pTypeExt = TechnoTypeExt::Fetch(pType);
+
+ if(pTypeExt->SHPCameo_IsLoad)
+ return pTypeExt->SpareCameo;
+
+ char pFileName[0x20];
+
+ strcpy_s(pFileName, pTypeExt->SpareCameoFile.data());
+ _strlwr_s(pFileName);
+
+ if(!_stricmp(pFileName, pTypeExt->SpareCameoFile.data())
+ && !strstr(pFileName, ".pcx"))
+ {
+ std::string Filename(pFileName);
+ Filename += ".shp";
+ SHPStruct* pSHP = FileSystem::LoadSHPFile(Filename.c_str());
+ pTypeExt->SHPCameo_IsLoad = true;
+ pTypeExt->SpareCameo = pSHP;
+ return pSHP;
+ }
+ return nullptr;
+}
+
+static bool DrawPCXCameo(TechnoTypeClass* pType, const int destX, const int destY)
+{
+ auto pTypeExt = TechnoTypeExt::Fetch(pType);
+ char pFileName[0x20];
+
+ strcpy_s(pFileName, pTypeExt->SpareCameoPCX.GetFilename());
+ _strlwr_s(pFileName);
+
+ if(!_stricmp(pFileName, pTypeExt->SpareCameoPCX.GetFilename())
+ && strstr(pFileName, ".pcx"))
+ {
+ PCX::Instance.LoadFile(pFileName);
+ if (const auto pCameoPCX = PCX::Instance.GetSurface(pFileName))
+ {
+ RectangleStruct bounds = { destX, destY, 60, 48 };
+ return PCX::Instance.BlitToSurface(&bounds, DSurface::Sidebar, pCameoPCX);
+ }
+ }
+ return false;
+}
+
+DEFINE_HOOK(0x6A980A, StripClass_Draw_DrawSHPCameo, 0x8)
+{
+ GET(TechnoTypeClass*, pType, EBX);
+
+ auto pHouseExt = HouseExt::Fetch(HouseClass::CurrentPlayer);
+ auto pHouseTypeExt = HouseTypeExt::Fetch(HouseClass::CurrentPlayer->Type);
+
+ SHPStruct* pSHP = nullptr;
+
+ switch(pType->WhatAmI())
+ {
+ case AbstractType::InfantryType:
+ {
+ if(pHouseTypeExt->EliteInfantry.Contains(pType))
+ {
+ pHouseExt->TechnoPCX_IsLoad.push_back(pType);
+ pSHP = LoadSHPCameo(pType);
+ }
+ break;
+ }
+ case AbstractType::UnitType:
+ {
+ if(pHouseTypeExt->EliteUnits.Contains(pType))
+ {
+ pHouseExt->TechnoPCX_IsLoad.push_back(pType);
+ pSHP = LoadSHPCameo(pType);
+ }
+ break;
+ }
+ case AbstractType::AircraftType:
+ {
+ if(pHouseTypeExt->EliteAircraft.Contains(pType))
+ {
+ pHouseExt->TechnoPCX_IsLoad.push_back(pType);
+ pSHP = LoadSHPCameo(pType);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if(pSHP)
+ R->EAX(pSHP);
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6A99E7, StripClass_Draw_DrawPCXCameo, 0x6)
+{
+ enum { SkipDrawSHP = 0x6A9A43 };
+
+ GET(const int, destX, ESI);
+ GET(const int, destY, EBP);
+ GET_STACK(SHPStruct*, pSHP, STACK_OFFSET(0x48C, -0x444));
+
+ auto pHouseExt = HouseExt::Fetch(HouseClass::CurrentPlayer);
+
+ for(auto pType : pHouseExt->TechnoPCX_IsLoad)
+ {
+ if(pSHP == pType->Cameo || pSHP == pType->AltCameo)
+ {
+ if(DrawPCXCameo(pType, destX, destY))
+ return SkipDrawSHP;
+ }
+ }
+ return 0;
+}
+
+DEFINE_HOOK(0x443C71, BuildingClass_ExitObject_CountryInitialElite, 0x6)
+{
+ GET(TechnoClass*, pTechno, EDI);
+
+ const auto pHouseTypeExt = HouseTypeExt::Fetch(pTechno->Owner->Type);
+
+ switch(pTechno->WhatAmI())
+ {
+ case AbstractType::Infantry:
+ {
+ if(pHouseTypeExt->EliteInfantry.Contains(pTechno->GetTechnoType()))
+ pTechno->Veterancy.SetElite();
+ break;
+ }
+ case AbstractType::Unit:
+ {
+ if(pHouseTypeExt->EliteUnits.Contains(pTechno->GetTechnoType()))
+ pTechno->Veterancy.SetElite();
+ break;
+ }
+ case AbstractType::Aircraft:
+ {
+ if(pHouseTypeExt->EliteAircraft.Contains(pTechno->GetTechnoType()))
+ pTechno->Veterancy.SetElite();
+ break;
+ }
+ default:
+ break;
+ }
+
+ return 0;
+}
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index c5b99aab19..16d2c72139 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -1441,6 +1441,9 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI)
// VoiceIFVRepair from Ares 0.2
this->VoiceIFVRepair.Read(exINI, pSection, "VoiceIFVRepair");
this->ParseVoiceWeaponAttacks(exINI, pSection, this->VoiceWeaponAttacks, this->VoiceEliteWeaponAttacks);
+
+ this->SpareCameoFile.Read(pArtINI, pArtSection, "SpareCameo");
+ this->SpareCameoPCX.Read(pArtINI, pArtSection, "SpareCameoPCX");
}
template
@@ -1838,6 +1841,11 @@ void TechnoTypeExt::Serialize(T& Stm)
.Process(this->DecloakAnims)
.Process(this->Cloak_KickOutParasite)
+ .Process(this->SpareCameoFile)
+ .Process(this->SpareCameo)
+ .Process(this->SHPCameo_IsLoad)
+ .Process(this->SpareCameoPCX)
+
// Ares 0.2
.Process(this->RadarJamRadius)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index fda8f6f3dc..3cf2ed0af6 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -463,6 +463,12 @@ class TechnoTypeExt : public ObjectTypeExt
Nullable Unsellable;
Nullable KeepAlive;
+ PhobosFixedString<0x20> SpareCameoFile;
+ SHPStruct* SpareCameo;
+ bool SHPCameo_IsLoad;
+ PhobosPCXFile SpareCameoPCX;
+
+
TechnoTypeExt(TechnoTypeClass* OwnerObject) : ObjectTypeExt(OwnerObject)
, HealthBar_Hide { false }
, HealthBar_HidePips { false }
@@ -855,6 +861,11 @@ class TechnoTypeExt : public ObjectTypeExt
, DecloakAnims {}
, Cloak_KickOutParasite {}
+ , SpareCameoFile { NONE_STR }
+ , SpareCameo { nullptr }
+ , SHPCameo_IsLoad { false }
+ , SpareCameoPCX {}
+
// Ares 0.2
, RadarJamRadius { 0 }