diff --git a/CREDITS.md b/CREDITS.md index da8a223005..fd236d94c2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -320,6 +320,9 @@ This page lists all the individual contributions to the project by their author. - Flashing Technos on selecting - **ZivDero** - Allow giving ownership of buildings to players in Skirmish and MP using + - Re-enable the Veinhole Monster and Weeds from TS + - Recreate the weed-charging of SWs like the TS Chemical Missile + - Allow to change the speed of gas particles - **Ares developers** - YRpp and Syringe which are used, save/load, project foundation and generally useful code from Ares - unfinished RadTypes code diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 7cd780d98d..1ff7abb8a7 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -39,6 +39,8 @@ + + @@ -61,6 +63,7 @@ + @@ -189,6 +192,7 @@ + diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 3e9bf33596..6547d7d674 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -359,6 +359,18 @@ In `rulesmd.ini`: AdjustTargetCoordsOnRotation=true ; boolean ``` +## Particles + +### Customizable gas particle speed + +- Gas particles can now drift at a custom speed. + +In `rulesmd.ini`: +```ini +[GASPARTICLE] ; Particle with BehavesLike=Gas +Gas.MaxDriftSpeed=2 ; integer (TS default is 5) +``` + ## Projectiles ### Cluster scatter distance customization @@ -367,7 +379,7 @@ AdjustTargetCoordsOnRotation=true ; boolean In `rulesmd.ini`: ```ini -[SOMEPROJECTILE] ; Projectile +[SOMEPROJECTILE] ; Projectile ClusterScatter.Min=1.0 ; float, distance in cells ClusterScatter.Max=2.0 ; float, distance in cells ``` @@ -626,6 +638,8 @@ Powered.KillSpawns=false ; boolean - `Pips.Tiberiums.Frames` can be used to list frames (zero-based) of `pips.shp` (for buildings) or `pips2.shp` (for others) used for tiberium types, in the listed order corresponding to tiberium type index. Defaults to 5 for tiberium type index 1, otherwise 2. - `Pips.Tiberiums.EmptyFrame` can be used to set the frame for empty slots, defaults to 0. - `Pips.Tiberiums.DisplayOrder` controls in which order the tiberium type pips are displayed, takes a list of tiberium type indices. Any tiberium type not listed will be displayed in sequential order after the listed ones. + - `Pips.Tiberiums.WeedFrame` controls which frame is displayed on Technos with `Weeder=yes`, takes a (zero-based) index of a frame in `pips.shp` (for buildings) or `pips2.shp` (for others). Defaults to 1. + - `Pips.Tiberiums.WeedEmptyFrame` can be used to set the frame for empty weed slots, defaults to 0. In `rulesmd.ini`: ```ini @@ -637,6 +651,8 @@ Pips.Ammo.Buildings.Size=4,2 ; X,Y, increment in pixels to next pip Pips.Tiberiums.EmptyFrame=0 ; integer, frame of pips.shp (buildings) or pips2.shp (others) (zero-based) Pips.Tiberiums.Frames=2,5,2,2 ; list of integers, frames of pips.shp (buildings) or pips2.shp (others) (zero-based) Pips.Tiberiums.DisplayOrder=0,2,3,1 ; list of integers, tiberium type indices +Pips.Tiberiums.WeedEmptyFrame=0 ; integer, frame of pips.shp (buildings) or pips2.shp (others) (zero-based) +Pips.Tiberiums.WeedFrame=1 ; integer, frame of pips.shp (buildings) or pips2.shp (others) (zero-based) [SOMETECHNO] ; TechnoType AmmoPipFrame=13 ; integer, frame of pips2.shp (zero-based) @@ -863,6 +879,89 @@ Ammo.AddOnDeploy=0 ; integer ``` +## Veinholes & Weeds + +### Veinholes + +- Veinhole monsters now work like they used to in Tiberian Sun. +- Their core parameters are still loaded from `[General]` +- The Warhead used by veins is specified under `[CombatDamage]`. The warhead has to be properly listed under `[Warheads]` as well. The warhead has to have `Veinhole=yes` set. +- Veinholes are hardcoded to use several overlay types. +- The vein attack animation specified under `[AudioVisual]` is what deals the damage. The animation has to be properly listed under `[Animations]` as well. +- Units can be made immune to veins the same way as in Tiberian Sun. +- The monster itself is represented by the `VEINTREE` TerrainType, which has `IsVeinhole=true` set. Its strength is what determines the strength of the Veinhole. + +```{note} +Everything listed below functions identically to Tiberian Sun. +Many of the tags from Tiberian Sun have been re-enabled. The values provided below are identical to those found in TS and YR rules. You can read more about them on ModENC: +[VeinholeGrowthRate](https://modenc.renegadeprojects.com/VeinholeGrowthRate), [VeinholeShrinkRate](https://modenc.renegadeprojects.com/VeinholeShrinkRate), [MaxVeinholeGrowth](https://modenc.renegadeprojects.com/MaxVeinholeGrowth), [VeinDamage](https://modenc.renegadeprojects.com/VeinDamage), [VeinholeTypeClass](https://modenc.renegadeprojects.com/VeinholeTypeClass), +[VeinholeWarhead](https://modenc.renegadeprojects.com/VeinholeWarhead), [Veinhole](https://modenc.renegadeprojects.com/Veinhole), [VeinAttack](https://modenc.renegadeprojects.com/VeinAttack), [ImmuneToVeins](https://modenc.renegadeprojects.com/ImmuneToVeins), [IsVeinhole](https://modenc.renegadeprojects.com/IsVeinhole) +``` + +In `rulesmd.ini`: +```ini +[General] +VeinholeGrowthRate=300 ; integer +VeinholeShrinkRate=100 ; integer +MaxVeinholeGrowth=2000 ; integer +VeinDamage=5 ; integer +VeinholeTypeClass=VEINTREE ; TerrainType + +[CombatDamage] +VeinholeWarhead=VeinholeWH ; Warhead + +[VeinholeWH] +Veinhole=yes + +[AudioVisual] +VeinAttack=VEINATAC ; Animation + +[TechnoType] +EliteAbilities=VEIN_PROOF +ImmuneToVeins=yes + +[VEINTREE] +IsVeinhole=true +Strength=1000 ; integer - the strength of the Veinhole +``` + +```{warning} +The game expects certain overlays related to Veinholes to have certain indices, they are listed below. +``` + +In `rulesmd.ini`: +```ini +[OverlayTypes] +126=VEINS ; The veins (weeds) +167=VEINHOLE ; The Veinhole itself +178=VEINHOLEDUMMY ; A technical overlay +``` + + +### Weeds & Weed Eaters + +- Vehicles with `Weeder=yes` can now collect weeds. The weeds can then be deposited into a building with `Weeder=yes`. +- Weeds are not stored in a building's storage, but rather in a House's storage. The weed capacity is listed under `[General]->WeedCapacity`. +- Weeders now show the ore gathering animation. It can be customized the same way as for harvesters. +- Weeders can use the Teleport locomotor like chrono miners. + +### Weed-consuming superweapons + +- Superweapons can consume weeds to recharge, like the Chemical Missile special in Tiberian Sun. + +```{note} +As the code for the Chemical Missile had been removed, setting `Type=ChemMissile` will not work. +``` + +In `rulesmd.ini`: +```ini +[SuperWeaponType] +UseWeeds=no ; boolean - should the SW use weeds to recharge? +UseWeeds.Amount= ; integer - how many? default is General->WeedCapacity +UseWeeds.StorageTimer=no ; boolean - should the counter on the sidebar display the % of weeds stored? +UseWeeds.ReadinessAnimationPercentage=0.9 ; double - when this many weeds % are stored, the SW will show it's ready on the building (open nuke/open chrono, etc.) +``` + ## VoxelAnims ### Customizable debris & meteor impact and warhead detonation behaviour diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 11af4f927f..40e7ea8544 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -1,6 +1,6 @@ # What's New -This page lists the history of changes across stable Phobos releases and also all the stuff that requires modders to change something in their mods to accomodate. +This page lists the history of changes across stable Phobos releases and also all the stuff that requires modders to change something in their mods to accommodate. ## Migrating @@ -44,7 +44,7 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] - `Gravity=0` is not supported anymore as it will cause the projectile to fly backwards and be unable to hit the target which is not at the same height. Use `Straight` Trajectory instead. See [here](New-or-Enhanced-Logics.md#projectile-trajectories). - Automatic self-destruction logic logic has been reimplemented, `Death.NoAmmo`, `Death.Countdown` and `Death.Peaceful` tags have been remade/renamed and require adjustments to function. - `DetachedFromOwner` on weapons is deprecated. This has been replaced by `AllowDamageOnSelf` on warheads. -- Timed jump script actions now take the time measured in ingame seconds instead of frames. Divide your value by 15 to accomodate to this change. +- Timed jump script actions now take the time measured in ingame seconds instead of frames. Divide your value by 15 to accommodate to this change. - [Placement Preview](User-Interface.md#placement-preview) logic has been adjusted, `BuildingPlacementPreview.DefaultTranslucentLevel`, `BuildingPlacementGrid.TranslucentLevel`, `PlacementPreview.Show`, `PlacementPreview.TranslucentLevel` and `ShowBuildingPlacementPreview` tags have been remade/renamed and require adjustments to function. In addition, you must explicitly enable this feature by specifying `[AudioVisual]->PlacementPreview=yes`. - Existing script actions were renumbered, please use the migration utility to change the numbers to the correct ones. - `DiskLaser.Radius` values were misinterpreted by a factor of 1/2π. The default radius is now 240, please multiply your customized radii by 2π. @@ -362,6 +362,9 @@ New: - Game save option when starting campaigns (by Trsdy) - Carryall pickup voice (by Starkku) - Option to have `Grinding.Weapon` require accumulated credits from grinding (by Starkku) +- Re-enable the Veinhole Monster and Weeds from TS (by ZivDero) +- Recreate the weed-charging of SWs like the TS Chemical Missile (by ZivDero) +- Allow to change the speed of gas particles (by ZivDero) Vanilla fixes: - Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy) diff --git a/src/Ext/Anim/Body.cpp b/src/Ext/Anim/Body.cpp index 5e107ee536..3b90aef9ca 100644 --- a/src/Ext/Anim/Body.cpp +++ b/src/Ext/Anim/Body.cpp @@ -97,6 +97,42 @@ HouseClass* AnimExt::GetOwnerHouse(AnimClass* pAnim, HouseClass* pDefaultOwner) return pTechnoOwner ? pTechnoOwner : pDefaultOwner; } +void AnimExt::VeinAttackAI(AnimClass* pAnim) +{ + CellStruct pCoordinates = pAnim->GetMapCoords(); + CellClass* pCell = MapClass::Instance->GetCellAt(pCoordinates); + ObjectClass* pOccupier = pCell->FirstObject; + constexpr unsigned char fullyFlownWeedStart = 0x30; // Weeds starting from this overlay frame are fully grown + constexpr unsigned int weedOverlayIndex = 126; + + if (!pOccupier || pOccupier->GetHeight() > 0 || pCell->OverlayTypeIndex != weedOverlayIndex + || pCell->OverlayData < fullyFlownWeedStart || pCell->SlopeIndex) + { + pAnim->UnableToContinue = true; + } + + if (Unsorted::CurrentFrame % 2 == 0) + { + while (pOccupier != nullptr) + { + ObjectClass* pNext = pOccupier->NextObject; + int damage = RulesClass::Instance->VeinDamage; + + abstract_cast(pOccupier); + + TechnoClass* pTechno = abstract_cast(pOccupier); + + if (pTechno && !pTechno->GetTechnoType()->ImmuneToVeins && !pTechno->HasAbility(Ability::VeinProof) + && pTechno->Health > 0 && pTechno->IsAlive && pTechno->GetHeight() <= 5) + { + pTechno->ReceiveDamage(&damage, 0, RulesExt::Global()->VeinholeWarhead, nullptr, false, false, nullptr); + } + + pOccupier = pNext; + } + } +} + void AnimExt::HandleDebrisImpact(AnimTypeClass* pExpireAnim, AnimTypeClass* pWakeAnim, Iterator splashAnims, HouseClass* pOwner, WarheadTypeClass* pWarhead, int nDamage, CellClass* pCell, CoordStruct nLocation, bool heightFlag, bool isMeteor, bool warheadDetonate, bool explodeOnWater, bool splashAnimsPickRandom) { @@ -132,7 +168,7 @@ void AnimExt::HandleDebrisImpact(AnimTypeClass* pExpireAnim, AnimTypeClass* pWak if (pWakeAnim) pWakeAnimToUse = pWakeAnim; - if (splashAnims.size() > 0) + if (!splashAnims.empty()) { auto nIndexR = (splashAnims.size() - 1); auto nIndex = splashAnimsPickRandom ? diff --git a/src/Ext/Anim/Body.h b/src/Ext/Anim/Body.h index 8643979102..4db09de6d2 100644 --- a/src/Ext/Anim/Body.h +++ b/src/Ext/Anim/Body.h @@ -93,6 +93,8 @@ class AnimExt static bool SetAnimOwnerHouseKind(AnimClass* pAnim, HouseClass* pInvoker, HouseClass* pVictim, bool defaultToVictimOwner = true, bool defaultToInvokerOwner = false); static HouseClass* GetOwnerHouse(AnimClass* pAnim, HouseClass* pDefaultOwner = nullptr); + static void VeinAttackAI(AnimClass* pAnim); + static void HandleDebrisImpact(AnimTypeClass* pExpireAnim, AnimTypeClass* pWakeAnim, Iterator splashAnims, HouseClass* pOwner, WarheadTypeClass* pWarhead, int nDamage, CellClass* pCell, CoordStruct nLocation, bool heightFlag, bool isMeteor, bool warheadDetonate, bool explodeOnWater, bool splashAnimsPickRandom); }; diff --git a/src/Ext/ParticleType/Body.cpp b/src/Ext/ParticleType/Body.cpp new file mode 100644 index 0000000000..e9c8a2bfaf --- /dev/null +++ b/src/Ext/ParticleType/Body.cpp @@ -0,0 +1,104 @@ +#include "Body.h" + +ParticleTypeExt::ExtContainer ParticleTypeExt::ExtMap; + +// ============================= +// load / save + +template +void ParticleTypeExt::ExtData::Serialize(T& Stm) +{ + Stm + .Process(this->Gas_MaxDriftSpeed) + ; +} + +void ParticleTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) +{ + auto pThis = this->OwnerObject(); + const char* pSection = pThis->ID; + + if (!pINI->GetSection(pSection)) + return; + + INI_EX exINI(pINI); + + this->Gas_MaxDriftSpeed.Read(exINI, pSection, "Gas.MaxDriftSpeed"); +} + +void ParticleTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) +{ + Extension::LoadFromStream(Stm); + this->Serialize(Stm); +} + +void ParticleTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) +{ + Extension::SaveToStream(Stm); + this->Serialize(Stm); +} + +bool ParticleTypeExt::LoadGlobals(PhobosStreamReader& Stm) +{ + return Stm + .Success(); +} + +bool ParticleTypeExt::SaveGlobals(PhobosStreamWriter& Stm) +{ + return Stm + .Success(); +} + +// ============================= +// container + +ParticleTypeExt::ExtContainer::ExtContainer() : Container("ParticleTypeClass") { } +ParticleTypeExt::ExtContainer::~ExtContainer() = default; + +// ============================= +// container hooks + +DEFINE_HOOK(0x644DBB, ParticleTypeClass_CTOR, 0x5) +{ + GET(ParticleTypeClass*, pItem, ESI); + + ParticleTypeExt::ExtMap.TryAllocate(pItem); + + return 0; +} + +DEFINE_HOOK_AGAIN(0x6457A0, ParticleTypeClass_SaveLoad_Prefix, 0x5) +DEFINE_HOOK(0x645660, ParticleTypeClass_SaveLoad_Prefix, 0x7) +{ + GET_STACK(ParticleTypeClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + ParticleTypeExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK(0x64578C, ParticleTypeClass_Load_Suffix, 0x5) +{ + ParticleTypeExt::ExtMap.LoadStatic(); + + return 0; +} + +DEFINE_HOOK(0x64580A, ParticleTypeClass_Save_Suffix, 0x7) +{ + ParticleTypeExt::ExtMap.SaveStatic(); + + return 0; +} + +DEFINE_HOOK(0x6453FF, ParticleTypeClass_LoadFromINI, 0x6) +{ + GET(ParticleTypeClass*, pItem, ESI); + GET_STACK(CCINIClass*, pINI, STACK_OFFSET(0xDC, 0x4)); + + ParticleTypeExt::ExtMap.LoadFromINI(pItem, pINI); + + return 0; +} diff --git a/src/Ext/ParticleType/Body.h b/src/Ext/ParticleType/Body.h new file mode 100644 index 0000000000..91f24b4298 --- /dev/null +++ b/src/Ext/ParticleType/Body.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include +#include +#include +#include + +class ParticleTypeExt +{ +public: + using base_type = ParticleTypeClass; + + static constexpr DWORD Canary = 0xEAFEEAFE; + static constexpr size_t ExtPointerOffset = 0x18; + + class ExtData final : public Extension + { + public: + Valueable Gas_MaxDriftSpeed; + + ExtData(ParticleTypeClass* OwnerObject) : Extension(OwnerObject) + , Gas_MaxDriftSpeed { 2 } + { } + + virtual ~ExtData() = default; + + virtual void LoadFromINIFile(CCINIClass* pINI) override; + + virtual void InvalidatePointer(void* ptr, bool bRemoved) override { } + + virtual void LoadFromStream(PhobosStreamReader& Stm) override; + virtual void SaveToStream(PhobosStreamWriter& Stm) override; + + private: + template + void Serialize(T& Stm); + }; + + class ExtContainer final : public Container + { + public: + ExtContainer(); + ~ExtContainer(); + }; + + static ExtContainer ExtMap; + + static bool LoadGlobals(PhobosStreamReader& Stm); + static bool SaveGlobals(PhobosStreamWriter& Stm); +}; diff --git a/src/Ext/ParticleType/Hooks.cpp b/src/Ext/ParticleType/Hooks.cpp new file mode 100644 index 0000000000..5c17c2280d --- /dev/null +++ b/src/Ext/ParticleType/Hooks.cpp @@ -0,0 +1,26 @@ +#include "Body.h" + +#include + +DEFINE_HOOK(0x62BE30, ParticleClass_Gas_AI_DriftSpeed, 0x5) +{ + enum { ContinueAI = 0x62BE60 }; + + GET(ParticleClass*, pParticle, EBP); + + auto pExt = ParticleTypeExt::ExtMap.Find(pParticle->Type); + int maxDriftSpeed = pExt->Gas_MaxDriftSpeed; + int minDriftSpeed = -maxDriftSpeed; + + if (pParticle->Velocity.X > maxDriftSpeed) + pParticle->Velocity.X = maxDriftSpeed; + else if (pParticle->Velocity.X < minDriftSpeed) + pParticle->Velocity.X = minDriftSpeed; + + if (pParticle->Velocity.Y > maxDriftSpeed) + pParticle->Velocity.Y = maxDriftSpeed; + else if (pParticle->Velocity.Y < minDriftSpeed) + pParticle->Velocity.Y = minDriftSpeed; + + return ContinueAI; +} diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 3daed98a6e..ecd6f08a5f 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -80,6 +80,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->RadWarhead_Detonate.Read(exINI, GameStrings::Radiation, "RadSiteWarhead.Detonate"); this->RadHasOwner.Read(exINI, GameStrings::Radiation, "RadHasOwner"); this->RadHasInvoker.Read(exINI, GameStrings::Radiation, "RadHasInvoker"); + this->VeinholeWarhead.Read(exINI, GameStrings::CombatDamage, "VeinholeWarhead"); this->MissingCameo.Read(pINI, GameStrings::AudioVisual, "MissingCameo"); this->PlacementGrid_Translucency.Read(exINI, GameStrings::AudioVisual, "PlacementGrid.Translucency"); @@ -103,9 +104,11 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->Pips_Generic_Buildings_Size.Read(exINI, GameStrings::AudioVisual, "Pips.Generic.Buildings.Size"); this->Pips_Ammo_Size.Read(exINI, GameStrings::AudioVisual, "Pips.Ammo.Size"); this->Pips_Ammo_Buildings_Size.Read(exINI, GameStrings::AudioVisual, "Pips.Ammo.Buildings.Size"); - this->Pips_Tiberiums_EmptyFrame.Read(exINI, GameStrings::AudioVisual, "Pips.Tiberiums.EmptyFrame"); this->Pips_Tiberiums_Frames.Read(exINI, GameStrings::AudioVisual, "Pips.Tiberiums.Frames"); + this->Pips_Tiberiums_EmptyFrame.Read(exINI, GameStrings::AudioVisual, "Pips.Tiberiums.EmptyFrame"); this->Pips_Tiberiums_DisplayOrder.Read(exINI, GameStrings::AudioVisual, "Pips.Tiberiums.DisplayOrder"); + this->Pips_Tiberiums_WeedFrame.Read(exINI, GameStrings::AudioVisual, "Pips.Tiberiums.WeedFrame"); + this->Pips_Tiberiums_WeedEmptyFrame.Read(exINI, GameStrings::AudioVisual, "Pips.Tiberiums.WeedEmptyFrame"); this->ToolTip_Background_Color.Read(exINI, GameStrings::AudioVisual, "ToolTip.Background.Color"); this->ToolTip_Background_Opacity.Read(exINI, GameStrings::AudioVisual, "ToolTip.Background.Opacity"); this->ToolTip_Background_BlurSize.Read(exINI, GameStrings::AudioVisual, "ToolTip.Background.BlurSize"); @@ -230,6 +233,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->RadHasInvoker) .Process(this->JumpjetCrash) .Process(this->JumpjetNoWobbles) + .Process(this->VeinholeWarhead) .Process(this->MissingCameo) .Process(this->PlacementGrid_Translucency) .Process(this->PlacementGrid_TranslucencyWithPreview) @@ -251,9 +255,11 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->Pips_Generic_Buildings_Size) .Process(this->Pips_Ammo_Size) .Process(this->Pips_Ammo_Buildings_Size) - .Process(this->Pips_Tiberiums_EmptyFrame) .Process(this->Pips_Tiberiums_Frames) + .Process(this->Pips_Tiberiums_EmptyFrame) .Process(this->Pips_Tiberiums_DisplayOrder) + .Process(this->Pips_Tiberiums_WeedFrame) + .Process(this->Pips_Tiberiums_WeedEmptyFrame) .Process(this->AllowParallelAIQueues) .Process(this->ForbidParallelAIQueues_Aircraft) .Process(this->ForbidParallelAIQueues_Building) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 00ef1f8bb8..d908b28b7c 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -47,6 +47,8 @@ class RulesExt Valueable JumpjetCrash; Valueable JumpjetNoWobbles; + Nullable VeinholeWarhead; + PhobosFixedString<32u> MissingCameo; TranslucencyLevel PlacementGrid_Translucency; @@ -70,9 +72,11 @@ class RulesExt Valueable Pips_Generic_Buildings_Size; Valueable Pips_Ammo_Size; Valueable Pips_Ammo_Buildings_Size; - Valueable Pips_Tiberiums_EmptyFrame; ValueableVector Pips_Tiberiums_Frames; + Valueable Pips_Tiberiums_EmptyFrame; ValueableVector Pips_Tiberiums_DisplayOrder; + Valueable Pips_Tiberiums_WeedFrame; + Valueable Pips_Tiberiums_WeedEmptyFrame; Valueable AllowParallelAIQueues; Valueable ForbidParallelAIQueues_Aircraft; @@ -127,6 +131,7 @@ class RulesExt , RadHasInvoker { false } , JumpjetCrash { 5.0 } , JumpjetNoWobbles { false } + , VeinholeWarhead {} , MissingCameo { GameStrings::XXICON_SHP() } , PlacementGrid_Translucency { 0 } @@ -149,9 +154,11 @@ class RulesExt , Pips_Generic_Buildings_Size { { 4, 2 } } , Pips_Ammo_Size { { 4, 0 } } , Pips_Ammo_Buildings_Size { { 4, 2 } } - , Pips_Tiberiums_EmptyFrame { 0 } , Pips_Tiberiums_Frames {} + , Pips_Tiberiums_EmptyFrame { 0 } , Pips_Tiberiums_DisplayOrder {} + , Pips_Tiberiums_WeedFrame { 1 } + , Pips_Tiberiums_WeedEmptyFrame { 0 } , AllowParallelAIQueues { true } , ForbidParallelAIQueues_Aircraft { false } , ForbidParallelAIQueues_Building { false } diff --git a/src/Ext/SWType/Body.cpp b/src/Ext/SWType/Body.cpp index 18366f0f45..6058134f8e 100644 --- a/src/Ext/SWType/Body.cpp +++ b/src/Ext/SWType/Body.cpp @@ -45,6 +45,10 @@ void SWTypeExt::ExtData::Serialize(T& Stm) .Process(this->ShowTimer_Priority) .Process(this->Convert_Pairs) .Process(this->ShowDesignatorRange) + .Process(this->UseWeeds) + .Process(this->UseWeeds_Amount) + .Process(this->UseWeeds_StorageTimer) + .Process(this->UseWeeds_ReadinessAnimationPercentage) ; } @@ -144,6 +148,11 @@ void SWTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) TypeConvertGroup::Parse(this->Convert_Pairs, exINI, pSection, AffectedHouse::Owner); this->ShowDesignatorRange.Read(exINI, pSection, "ShowDesignatorRange"); + + this->UseWeeds.Read(exINI, pSection, "UseWeeds"); + this->UseWeeds_Amount.Read(exINI, pSection, "UseWeeds.Amount"); + this->UseWeeds_StorageTimer.Read(exINI, pSection, "UseWeeds.StorageTimer"); + this->UseWeeds_ReadinessAnimationPercentage.Read(exINI, pSection, "UseWeeds.ReadinessAnimationPercentage"); } void SWTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/SWType/Body.h b/src/Ext/SWType/Body.h index 945cd2154a..6c82e61bea 100644 --- a/src/Ext/SWType/Body.h +++ b/src/Ext/SWType/Body.h @@ -63,6 +63,11 @@ class SWTypeExt std::vector Convert_Pairs; + Valueable UseWeeds; + Valueable UseWeeds_Amount; + Valueable UseWeeds_StorageTimer; + Valueable UseWeeds_ReadinessAnimationPercentage; + ExtData(SuperWeaponTypeClass* OwnerObject) : Extension(OwnerObject) , Money_Amount { 0 } , SW_Inhibitors {} @@ -98,6 +103,10 @@ class SWTypeExt , ShowTimer_Priority { 0 } , Convert_Pairs {} , ShowDesignatorRange { true } + , UseWeeds { false } + , UseWeeds_Amount { RulesClass::Instance->WeedCapacity } + , UseWeeds_StorageTimer { false } + , UseWeeds_ReadinessAnimationPercentage { 0.9 } { } // Ares 0.A functions diff --git a/src/Ext/SWType/Hooks.cpp b/src/Ext/SWType/Hooks.cpp index 3e0fc992dd..cb0369c5ef 100644 --- a/src/Ext/SWType/Hooks.cpp +++ b/src/Ext/SWType/Hooks.cpp @@ -74,3 +74,111 @@ DEFINE_HOOK(0x6DBE74, Tactical_SuperLinesCircles_ShowDesignatorRange, 0x7) return 0; } + +DEFINE_HOOK(0x6CBEF4, SuperClass_AnimStage_UseWeeds, 0x6) +{ + enum + { + Ready = 0x6CBFEC, + NotReady = 0x6CC064, + ProgressInEax = 0x6CC066 + }; + + constexpr int maxCounterFrames = 54; + + GET(SuperClass*, pSuper, ECX); + GET(SuperWeaponTypeClass*, pSWType, EBX); + + auto pExt = SWTypeExt::ExtMap.Find(pSWType); + + if (pExt->UseWeeds) + { + if (pSuper->IsReady) + return Ready; + + if (pExt->UseWeeds_StorageTimer) + { + int progress = static_cast(pSuper->Owner->OwnedWeed.GetTotalAmount() * maxCounterFrames / pExt->UseWeeds_Amount); + if (progress > maxCounterFrames) + progress = maxCounterFrames; + + R->EAX(progress); + return ProgressInEax; + } + else + { + return NotReady; + } + } + + return 0; +} + +DEFINE_HOOK(0x6CBD2C, SuperClass_AI_UseWeeds, 0x6) +{ + enum + { + NothingChanged = 0x6CBE9D, + SomethingChanged = 0x6CBD48, + Charged = 0x6CBD73 + }; + + enum + { + SWReadyTimer = 0, + SWAlmostReadyTimer = 15, + SWNotReadyTimer = 915 + }; + + GET(SuperClass*, pSuper, ESI); + + auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); + + if (pExt->UseWeeds) + { + if (pSuper->Type->ShowTimer) + pSuper->Type->ShowTimer = false; + + if (pSuper->Owner->OwnedWeed.GetTotalAmount() >= pExt->UseWeeds_Amount) + { + pSuper->Owner->OwnedWeed.RemoveAmount(static_cast(pExt->UseWeeds_Amount), 0); + pSuper->RechargeTimer.Start(SWReadyTimer); // The Armageddon is here + return Charged; + } + + if (pSuper->Owner->OwnedWeed.GetTotalAmount() >= pExt->UseWeeds_ReadinessAnimationPercentage * pExt->UseWeeds_Amount) + { + pSuper->RechargeTimer.Start(SWAlmostReadyTimer); // The end is nigh! + } + else + { + pSuper->RechargeTimer.Start(SWNotReadyTimer); // 61 seconds > 60 seconds (animation activation threshold) + } + + int animStage = pSuper->AnimStage(); + if (pSuper->CameoChargeState != animStage) + { + pSuper->CameoChargeState = animStage; + return SomethingChanged; + } + + return NothingChanged; + } + + return 0; +} + +// This is pointless for SWs using weeds because their charge is tied to weed storage. +DEFINE_HOOK(0x6CC1E6, SuperClass_SetSWCharge_UseWeeds, 0x5) +{ + enum { Skip = 0x6CC251 }; + + GET(SuperClass*, pSuper, EDI); + + auto pExt = SWTypeExt::ExtMap.Find(pSuper->Type); + + if (pExt->UseWeeds) + return Skip; + + return 0; +} diff --git a/src/Ext/Techno/Hooks.Pips.cpp b/src/Ext/Techno/Hooks.Pips.cpp index fc9bb61879..064274c7d7 100644 --- a/src/Ext/Techno/Hooks.Pips.cpp +++ b/src/Ext/Techno/Hooks.Pips.cpp @@ -162,44 +162,99 @@ DEFINE_HOOK(0x70A1F6, TechnoClass_DrawPips_Tiberium, 0x6) GET(int, yOffset, ESI); Point2D position = { offset->X, offset->Y }; - int totalStorage = pThis->GetTechnoType()->Storage; + const int totalStorage = pThis->GetTechnoType()->Storage; - std::vector tibPipCounts(TiberiumClass::Array.get()->Count); + std::vector pipsToDraw; - for (size_t i = 0; i < tibPipCounts.size(); i++) + bool isWeeder = false; + switch (pThis->WhatAmI()) { - tibPipCounts[i] = static_cast(pThis->Tiberium.GetAmount(i) / totalStorage * maxPips + 0.5); + case AbstractType::Building: + isWeeder = static_cast(pThis)->Type->Weeder; + break; + case AbstractType::Unit: + isWeeder = static_cast(pThis)->Type->Weeder; + break; + default: + break; } - auto const& tibDisplayOrders = RulesExt::Global()->Pips_Tiberiums_DisplayOrder.size() ? RulesExt::Global()->Pips_Tiberiums_DisplayOrder : std::vector { 0, 2, 3, 1 }; - auto const& tibFrames = RulesExt::Global()->Pips_Tiberiums_Frames; + if (isWeeder) + { + const int fullWeedFrames = pThis->WhatAmI() == AbstractType::Building ? + static_cast(pThis->Owner->GetWeedStoragePercentage() * maxPips + 0.5) : + static_cast(pThis->Tiberium.GetTotalAmount() / totalStorage * maxPips + 0.5); - for (int i = 0; i < maxPips; i++) + for (int i = 0; i < maxPips; i++) + { + if (i < fullWeedFrames) + pipsToDraw.push_back(RulesExt::Global()->Pips_Tiberiums_WeedFrame); + else + pipsToDraw.push_back(RulesExt::Global()->Pips_Tiberiums_WeedEmptyFrame); + } + } + else { - int frame = RulesExt::Global()->Pips_Tiberiums_EmptyFrame; + std::vector tiberiumPipCounts(TiberiumClass::Array->Count); + + for (size_t i = 0; i < tiberiumPipCounts.size(); i++) + { + tiberiumPipCounts[i] = static_cast(pThis->Tiberium.GetAmount(i) / totalStorage * maxPips + 0.5); + } + + auto const rawPipOrder = RulesExt::Global()->Pips_Tiberiums_DisplayOrder.empty() ? std::vector{ 0, 2, 3, 1 } : RulesExt::Global()->Pips_Tiberiums_DisplayOrder; + auto const& pipFrames = RulesExt::Global()->Pips_Tiberiums_Frames; + int const emptyFrame = RulesExt::Global()->Pips_Tiberiums_EmptyFrame; + + std::vector pipOrder; - for (size_t orderIndex = 0; orderIndex < tibPipCounts.size(); orderIndex++) + // First make a new vector, removing all the duplicate and invalid tiberiums + for (int index : rawPipOrder) { - size_t tibTypeIndex = orderIndex; + if (std::find(pipOrder.begin(), pipOrder.end(), index) == pipOrder.end() && + index >= 0 && index < TiberiumClass::Array->Count) + { + pipOrder.push_back(index); + } + } - if (orderIndex < tibDisplayOrders.size()) - tibTypeIndex = tibDisplayOrders.at(orderIndex); + // Then add any tiberium types that are missing + for (int i = 0; i < TiberiumClass::Array->Count; i++) + { + if (std::find(pipOrder.begin(), pipOrder.end(), i) == pipOrder.end()) + { + pipOrder.push_back(i); + } + } + - if (tibPipCounts[tibTypeIndex] > 0) + for (int i = 0; i < maxPips; i++) + { + for (const int index : pipOrder) { - tibPipCounts[tibTypeIndex]--; + if (tiberiumPipCounts[index] > 0) + { + tiberiumPipCounts[index]--; - if (tibTypeIndex >= tibFrames.size()) - frame = tibTypeIndex == 1 ? 5 : 2; - else - frame = tibFrames.at(tibTypeIndex); + if (static_cast(index) >= pipFrames.size()) + pipsToDraw.push_back(index == 1 ? 5 : 2); + else + pipsToDraw.push_back(pipFrames.at(index)); - break; + break; + } } + + if (pipsToDraw.size() <= static_cast(i)) + pipsToDraw.push_back(emptyFrame); } + } - DSurface::Temp->DrawSHP(FileSystem::PALETTE_PAL, shape, frame, - &position, rect, BlitterFlags(0x600), 0, 0, ZGradient::Ground, 1000, 0, 0, 0, 0, 0); + for (int pip : pipsToDraw) + { + DSurface::Temp->DrawSHP(FileSystem::PALETTE_PAL, shape, pip, + &position, rect, BlitterFlags::Centered | BlitterFlags::bf_400, 0, 0, + ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0); position.X += offset->Width; position.Y += yOffset; diff --git a/src/Misc/Hooks.VeinholeMonster.cpp b/src/Misc/Hooks.VeinholeMonster.cpp new file mode 100644 index 0000000000..649608cf27 --- /dev/null +++ b/src/Misc/Hooks.VeinholeMonster.cpp @@ -0,0 +1,363 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +/// +/// Veinhole Monster +/// + +// Loads the veinhole monster art +// Call removed from YR by WW +DEFINE_HOOK(0x4AD097, DisplayClass_ReadIni_LoadVeinholeArt, 0x5) +{ + enum { ContinueReadIni = 0x4AD0A8 }; + + int theater = static_cast(ScenarioClass::Instance->Theater); + SmudgeTypeClass::LoadFromIniList(theater); + VeinholeMonsterClass::LoadVeinholeArt(theater); + + return ContinueReadIni; +} + +// Applies damage to the veinhole monster +DEFINE_HOOK(0x489671, Damage_at_Cell_Update_Veinhole, 0x6) +{ + enum + { + ContinueDrawWall = 0x48967B, + ContinueNotWall = 0x4896B2 + }; + + GET(OverlayTypeClass*, pOverlay, EAX); + GET(WarheadTypeClass*, pWH, ESI); + GET_STACK(CellStruct, pCell, STACK_OFFSET(0xE0, -0x4C)); + GET_STACK(int, damage, STACK_OFFSET(0xE0, -0xBC)); + GET_STACK(ObjectClass*, pAttacker, STACK_OFFSET(0xE0, 0x8)); + GET_STACK(HouseClass*, pAttackingHouse, STACK_OFFSET(0xE0, 0x14)); + + if (pOverlay->IsVeinholeMonster) + { + if (VeinholeMonsterClass* pVeinhole = VeinholeMonsterClass::GetVeinholeMonsterFrom(&pCell)) + pVeinhole->ReceiveDamage(&damage, 0, pWH, pAttacker, false, false, pAttackingHouse); + } + + return pOverlay->Wall ? ContinueDrawWall : ContinueNotWall; +} + +DEFINE_HOOK(0x6D4656, TacticalClass_Draw_Veinhole, 0x5) +{ + enum { ContinueDraw = 0x6D465B }; + + VeinholeMonsterClass::DrawAll(); + IonBlastClass::DrawAll(); + + return ContinueDraw; +} + +DEFINE_HOOK(0x5349A5, Map_ClearVectors_Veinhole, 0x5) +{ + VeinholeMonsterClass::DeleteAll(); + VeinholeMonsterClass::DeleteVeinholeGrowthData(); + return 0; +} + +DEFINE_HOOK(0x55B4E1, LogicClass_Update_Veinhole, 0x5) +{ + VeinholeMonsterClass::UpdateAllVeinholes(); + return 0; +} + +// Handles the veins' attack animation +DEFINE_HOOK(0x4243BC, AnimClass_Update_VeinholeAttack, 0x6) +{ + enum + { + ContinueDrawTiberium = 0x4243CC, + ContinueNotTiberium = 0x42442E + }; + + GET(AnimClass*, pAnim, ESI); + + if (pAnim->Type->IsVeins) + AnimExt::VeinAttackAI(pAnim); + + GET(AnimClass*, pAnim2, ESI); + + return pAnim2->Type->IsTiberium ? + ContinueDrawTiberium : ContinueNotTiberium; +} + +/// +/// Weeder +/// + +// These 2 I am not sure, maybe they have smth to do with AI, maybe they are for the unit queue at the refinery +DEFINE_HOOK(0x736823, UnitClass_Update_WeederMissionMove, 0x6) +{ + enum + { + Continue = 0x736831, + Skip = 0x736981 + }; + + GET(UnitTypeClass*, pUnitType, EAX); + + if (pUnitType->Harvester || pUnitType->Weeder) + return Continue; + + return Skip; +} + +DEFINE_HOOK(0x7368C6, UnitClass_Update_WeederMissionMove2, 0x6) +{ + enum + { + Continue = 0x7368D4, + Skip = 0x736981 + }; + + GET(BuildingTypeClass*, pBuildingType, EDX); + + if (pBuildingType->Refinery || pBuildingType->Weeder) + return Continue; + + return Skip; +} + +// Not sure if necessary +/* +// These 2 have something to do with ZAdjustment when unloading +DEFINE_HOOK(0x7043E7, TechnoClass_Get_ZAdjustment_Weeder, 0x6) +{ + enum + { + Continue = 0x7043F1, + Skip = 0x704421 + }; + + GET(UnitTypeClass*, pUnitType, ECX); + + if (pUnitType->Harvester || pUnitType->Weeder) + return Continue; + + return Skip; +} + +DEFINE_HOOK(0x70440C, TechnoClass_Get_ZAdjustment_Weeder2, 0x6) +{ + enum + { + Continue = 0x704416, + Skip = 0x704421 + }; + + GET(BuildingTypeClass*, pBuildingType, EAX); + + if (pBuildingType->Refinery || pBuildingType->Weeder) + return Continue; + + return Skip; +} + +DEFINE_HOOK(0x741C32, UnitClass_SetDestination_SpecialAnim_Weeder, 0x6) +{ + enum + { + CheckSpecialAnimExists = 0x741C3C, + Skip = 0x741C4F + }; + + GET(UnitTypeClass*, pUnitType, ECX); + + if (pUnitType->Harvester || pUnitType->Weeder) + return CheckSpecialAnimExists; + + return Skip; +} +*/ + +DEFINE_HOOK(0x73D0DB, UnitClass_DrawAt_Weeder_Oregath, 0x6) +{ + enum + { + DrawOregath = 0x73D0E9, + Skip = 0x73D298 + }; + + GET(UnitClass*, pUnit, ESI); + + if (pUnit->Type->Harvester || pUnit->Type->Weeder || pUnit->IsHarvesting) + return DrawOregath; + + return Skip; +} +/* +DEFINE_HOOK(0x73D2A6, UnitClass_DrawAt_Weeder_UnloadingClass, 0x6) +{ + enum + { + ShowUnloadingClass = 0x73D2B0, + Skip = 0x73D2CA + }; + + GET(UnitTypeClass*, pUnitType, EAX); + + if (pUnitType->Harvester || pUnitType->Weeder) + return ShowUnloadingClass; + + return Skip; +} +*/ + +// Enables the weeder to harvest veins +DEFINE_HOOK(0x73D49E, UnitClass_Harvesting_Weeder, 0x7) +{ + enum + { + Harvest = 0x73D4DA, + Skip = 0x73D5FE + }; + + GET(UnitClass*, pUnit, ESI); + GET(CellClass*, pCell, EBP); + constexpr unsigned char weedOverlayData = 0x30; + + bool harvesterCanHarvest = pUnit->Type->Harvester && pCell->LandType == LandType::Tiberium; + bool weederCanWeed = pUnit->Type->Weeder && pCell->LandType == LandType::Weeds && pCell->OverlayData >= weedOverlayData; + + + if ((harvesterCanHarvest || weederCanWeed) && pUnit->GetStoragePercentage() < 1.0) + return Harvest; + + return Skip; +} + +// Not sure if necessary +/* +DEFINE_HOOK(0x73E005, UnitClass_Unload_WeederAnim, 0x6) +{ + enum + { + ProceedWithAnim = 0x73E013, + Skip = 0x73E093 + }; + + GET(UnitTypeClass*, pUnitType, ECX); + + if (pUnitType->Harvester || pUnitType->Weeder) + return ProceedWithAnim; + + return Skip; +} +*/ + +// This lets the weeder actually enter the waste facility and unload +// WW removed weeders from this check in YR +DEFINE_HOOK(0x43C788, BuildingClass_ReceivedRadioCommand_Weeder_CompleteEnter, 0x6) +{ + enum + { + CompleteEnter = 0x43C796, + Skip = 0x43CE43 + }; + + GET(BuildingTypeClass*, pBuildingType, EAX); + + if (pBuildingType->DockUnload || pBuildingType->Weeder) + return CompleteEnter; + + return Skip; +} + +// This assigns the weeder to the "Harvest" mission when it is granted as a free unit +// Ares made the weeder receive the "Guard" command instead +DEFINE_HOOK(0x446EAD, BuildingClass_GrandOpening_FreeWeeder_Mission, 0x6) +{ + GET(UnitClass*, pUnit, EDI); + + if (pUnit->Type->Weeder) + pUnit->ForceMission(Mission::Harvest); + + pUnit->NextMission(); + + return 0x446EB7; +} + +// Teleport cooldown for weeders +DEFINE_HOOK(0x719580, TeleportLocomotion_Weeder, 0x6) +{ + enum + { + Skip = 0x7195A0, + TeleportChargeTimer = 0x7195BC + }; + + GET(TeleportLocomotionClass*, pTeleport, ESI); + + if (pTeleport->Owner->WhatAmI() == AbstractType::Unit) + { + UnitClass* pUnit = (UnitClass*)pTeleport->Owner; + if (pUnit->Type->Harvester || pUnit->Type->Weeder) + return Skip; + } + + return TeleportChargeTimer; +} + +// DockUnload bypass for Weeders when teleporting +DEFINE_HOOK(0x7424BD, UnitClass_AssignDestination_Weeder_Teleport, 0x6) +{ + GET(BuildingTypeClass*, pDestination, ECX); + + return pDestination->DockUnload || pDestination->Weeder ? 0x7424CB : 0x7425DB; +} + +//// Skip check for Weeder so that weeders go through teleport stuff +//DEFINE_JUMP(LJMP, 0x73E844, 0x73E793) +// +// +//DEFINE_HOOK(0x73E84A, UnitClass_Mission_Harvest, 0x6) +//{ +// GET(UnitClass*, pUnit, EBP); +// +// bool isOnTiberium; +// if (pUnit->Type->Weeder) +// isOnTiberium = pUnit->MoveToWeed(RulesClass::Instance->TiberiumLongScan / Unsorted::LeptonsPerCell); +// else +// isOnTiberium = pUnit->MoveToTiberium(RulesClass::Instance->TiberiumLongScan / Unsorted::LeptonsPerCell); +// +// R->EBX(isOnTiberium); +// return 0x73E86B; +//} + +DEFINE_HOOK(0x73E9A0, UnitClass_Weeder_StopHarvesting, 0x6) +{ + enum + { + StopHarvesting = 0x73E9CA, + Skip = 0x73EA8D + }; + + GET(UnitClass*, pUnit, EBP); + + if ((pUnit->Type->Harvester || pUnit->Type->Weeder) && pUnit->GetStoragePercentage() == 1.0) + { + return StopHarvesting; + } + + return Skip; +}