From 2c14d36e60f2b1fb6659dbbbf748317563a237ae Mon Sep 17 00:00:00 2001 From: Noble_Fish <1065703286@qq.com> Date: Fri, 19 Jun 2026 01:17:16 +0800 Subject: [PATCH] initial --- CREDITS.md | 1 + docs/Fixed-or-Improved-Logics.md | 17 ++++ docs/Whats-New.md | 1 + src/Ext/Building/Body.cpp | 2 + src/Ext/Building/Body.h | 4 + src/Ext/Building/Hooks.cpp | 151 +++++++++++++++++++++++++++++++ src/Ext/BuildingType/Body.cpp | 29 ++++++ src/Ext/BuildingType/Body.h | 20 +++- 8 files changed, 224 insertions(+), 1 deletion(-) diff --git a/CREDITS.md b/CREDITS.md index 8a983e6e7d..fb12bb12a0 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -688,6 +688,7 @@ This page lists all the individual contributions to the project by their author. - Customize whether technos with `Locomotor=Fly` wobble - Customize the landing animation of technos that have `Locomotor=Fly` - Allow infantry to use `Convert.Deploy` without requiring `IsSimpleDeployer=true` + - Customize the images for building sell and undeploy - **Ollerus**: - Build limit group enhancement - Customizable rocker amplitude diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index e1968448f8..0693305e00 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1189,6 +1189,23 @@ Overpower.ChargeWeapon=1 ; integer, negative values mean that weapons can never Ares' [Battery Super Weapon](https://ares-developers.github.io/Ares-docs/new/superweapons/types/battery.html) won't be affected by this. ``` +### Customize the images for building sell and undeploy + +- In vanilla, when a building is sold or undeployed, it plays the reversed `Buildup` image. Now you can customize the images for these two cases and set whether to reverse them separately. + +In `artmd.ini`: +```ini +[SOMEBUILDING] ; BuildingType +Sell= ; filename - excluding the .shp extension +Sell.Reverse=true ; boolean +Undeploy= ; filename - excluding the .shp extension +Undeploy.Reverse=true ; boolean +``` + +```{note} +When the replacement image is not explicitly set, the `Buildup` image will be used, and reversal can still work independently. +``` + ### Disable `DamageSound` - Now you can disable `DamageSound` of a building. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 7c594a485a..97d134e04d 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -597,6 +597,7 @@ HideShakeEffects=false ; boolean - Add `ClampToScreen` tag for `BannerType` (defaults to `true`) to control whether banner position is clamped to the visible area (by Chang_zhi) - [Customizable Berzerk mission](New-or-Enhanced-Logics.md#enhanced-berzerk-behavior) (by TaranDahl) - [Tank Bunker foundation and state update delay improvements](Fixed-or-Improved-Logics.md#tank-bunker-improvements) (by Starkku) +- [Customize the images for building sell and undeploy](Fixed-or-Improved-Logics.md#customize-the-images-for-building-sell-and-undeploy) (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) diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index af9b7a5bf4..6f5927fde8 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -566,6 +566,8 @@ void BuildingExt::ExtData::Serialize(T& Stm) .Process(this->TurretAnimIdleFrame) .Process(this->TurretAnimFiringFrame) .Process(this->TurretAnimRateTick) + .Process(this->UseCustomSellFrames) + .Process(this->CustomSellFrameCount) //.Process(this->IsFiringNow) It is set and reset within a same function. ; } diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 2539c353b9..9103db965d 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -30,6 +30,8 @@ class BuildingExt int TurretAnimIdleFrame; int TurretAnimFiringFrame; int TurretAnimRateTick; + bool UseCustomSellFrames; + int CustomSellFrameCount; ExtData(BuildingClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } @@ -48,6 +50,8 @@ class BuildingExt , TurretAnimIdleFrame { 0 } , TurretAnimFiringFrame { -1 } , TurretAnimRateTick { 0 } + , UseCustomSellFrames { false } + , CustomSellFrameCount { 0 } { } void DisplayIncomeString(); diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index fb0d1762fd..3b8dffec5e 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1300,3 +1300,154 @@ DEFINE_HOOK(0x44C976, BuildingClass_Mission_Repair_TankBunker, 0x5) } #pragma endregion + +static SHPStruct* LoadTheaterSHP(const char* baseName) +{ + char filename[MAX_PATH]; + _snprintf_s(filename, _TRUNCATE, "%s", baseName); + + if (isalpha(static_cast(filename[0]))) + { + auto const c1 = static_cast(filename[1]) & ~0x20; + if (c1 == 'A' || c1 == 'T') + { + auto const theater = ScenarioClass::Instance->Theater; + filename[1] = Theater::GetTheater(theater).Letter[0]; + } + } + + char fullPath[MAX_PATH]; + _snprintf_s(fullPath, _TRUNCATE, "%s.SHP", filename); + SHPStruct* pSHP = static_cast(FileSystem::LoadFile(fullPath, false)); + + if (!pSHP) + { + filename[1] = 'G'; + _snprintf_s(fullPath, _TRUNCATE, "%s.SHP", filename); + pSHP = static_cast(FileSystem::LoadFile(fullPath, false)); + } + + if (!pSHP) + { + _snprintf_s(fullPath, _TRUNCATE, "%s.SHP", baseName); + pSHP = static_cast(FileSystem::LoadFile(fullPath, false)); + } + + return pSHP; +} + +DEFINE_HOOK(0x43D2B5, BuildingClass_Draw_Sell, 0x6) +{ + enum { Continue = 0x43D2C5 }; + + GET(BuildingClass*, pThis, ESI); + auto const pBldExt = BuildingExt::ExtMap.Find(pThis); + + if (pThis->CurrentMission != Mission::Selling || pThis->BState != 0) + { + pBldExt->UseCustomSellFrames = false; + return 0; + } + + auto const pTypeExt = BuildingTypeExt::ExtMap.Find(pThis->Type); + SHPStruct* pSHP = nullptr; + + if (pThis->ArchiveTarget) + { + if (pTypeExt->UndeployFileName[0]) + { + if (!pTypeExt->Undeploy) + pTypeExt->Undeploy = LoadTheaterSHP(pTypeExt->UndeployFileName); + pSHP = pTypeExt->Undeploy; + } + } + else + { + if (pTypeExt->SellFileName[0]) + { + if (!pTypeExt->Sell) + pTypeExt->Sell = LoadTheaterSHP(pTypeExt->SellFileName); + pSHP = pTypeExt->Sell; + } + } + + if (pSHP) + { + if (!pBldExt->UseCustomSellFrames) + { + pBldExt->UseCustomSellFrames = true; + int effectiveFrames = pSHP->Frames / 2; + pBldExt->CustomSellFrameCount = effectiveFrames; + + double sellTimeMinutes = pTypeExt->SellTime.isset() + ? pTypeExt->SellTime.Get() + : (pTypeExt->BuildupTime.isset() + ? pTypeExt->BuildupTime.Get() + : RulesClass::Instance->BuildupTime); + int totalDuration = static_cast(sellTimeMinutes * 900); + int newRate = Math::max(1, totalDuration / effectiveFrames); + + pThis->Animation.Start(newRate); + pThis->Animation.Value = 1; + pThis->Animation.Timer.StartTime = Unsorted::CurrentFrame - newRate; + } + + R->EAX(pSHP); + R->Stack(0x14, pSHP); + return Continue; + } + + pBldExt->UseCustomSellFrames = false; + return 0; +} + +DEFINE_HOOK(0x43F000, BuildingClass_GetCurrentFrame_Sell, 0x6) +{ + enum { Continue = 0x43F029 }; + + GET(BuildingClass*, pThis, ESI); + auto const pBldExt = BuildingExt::ExtMap.Find(pThis); + + if (!pBldExt->UseCustomSellFrames) + return 0; + + auto const pTypeExt = BuildingTypeExt::ExtMap.Find(pThis->Type); + bool reverse = pThis->ArchiveTarget ? pTypeExt->Undeploy_Reverse : pTypeExt->Sell_Reverse; + + int effectiveFrames = pBldExt->CustomSellFrameCount; + int value = pThis->Animation.Value; + int frameIndex = reverse ? (effectiveFrames - value - 1) : value; + if (frameIndex < 0) frameIndex = 0; + if (frameIndex >= effectiveFrames) frameIndex = effectiveFrames - 1; + + R->EAX(frameIndex); + return Continue; +} + +DEFINE_HOOK(0x4511F1, BuildingClass_UpdateAnimations_CustomFrameCount_End, 0x6) +{ + GET(BuildingClass*, pThis, ESI); + auto const pBldExt = BuildingExt::ExtMap.Find(pThis); + + int total = pThis->Type->BuildingAnimFrame[pThis->BState].dwUnknown + + pThis->Type->BuildingAnimFrame[pThis->BState].FrameCount; + + if (pBldExt->UseCustomSellFrames) + total = pBldExt->CustomSellFrameCount; + + return (pThis->Animation.Value < total) ? 0x4511FC : 0x4511F7; +} + +DEFINE_HOOK(0x4511A5, BuildingClass_UpdateAnimations_CustomFrameCount_Ready, 0x6) +{ + GET(BuildingClass*, pThis, ESI); + auto const pBldExt = BuildingExt::ExtMap.Find(pThis); + + int totalMinusOne = pThis->Type->BuildingAnimFrame[pThis->BState].dwUnknown + + pThis->Type->BuildingAnimFrame[pThis->BState].FrameCount - 1; + + if (pBldExt->UseCustomSellFrames) + totalMinusOne = pBldExt->CustomSellFrameCount - 1; + + return (pThis->Animation.Value == totalMinusOne) ? 0x4511DF : 0x4511B3; +} diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 44e6217482..02b39cf265 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -241,6 +241,29 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->TurretAnim_LowPowerFiringFrames.Read(exINI, pSection, "TurretAnim.LowPowerFiringFrames"); this->TurretAnim_IdleRate.Read(exINI, pSection, "TurretAnim.IdleRate"); this->TurretAnim_FiringRate.Read(exINI, pSection, "TurretAnim.FiringRate"); + this->BuildupTime.Read(exINI, pSection, "BuildupTime"); + this->SellTime.Read(exINI, pSection, "SellTime"); + + if (exArtINI.ReadString(pArtSection, "Sell") > 0) + { + strcpy_s(this->SellFileName, exArtINI.value()); + this->Sell = nullptr; + } + else + { + this->SellFileName[0] = '\0'; + } + this->Sell_Reverse.Read(exArtINI, pArtSection, "Sell.Reverse"); + if (exArtINI.ReadString(pArtSection, "Undeploy") > 0) + { + strcpy_s(this->UndeployFileName, exArtINI.value()); + this->Undeploy = nullptr; + } + else + { + this->UndeployFileName[0] = '\0'; + } + this->Undeploy_Reverse.Read(exArtINI, pArtSection, "Undeploy.Reverse"); if (pThis->PowersUpBuilding[0] == NULL && this->PowersUp_Buildings.size() > 0) { @@ -418,6 +441,12 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->TurretAnim_LowPowerFiringFrames) .Process(this->TurretAnim_IdleRate) .Process(this->TurretAnim_FiringFrames) + .Process(this->Sell) + .Process(this->Sell_Reverse) + .Process(this->Undeploy) + .Process(this->Undeploy_Reverse) + .Process(this->BuildupTime) + .Process(this->SellTime) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index c49efbfc53..2a8d93cebb 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -115,6 +115,15 @@ class BuildingTypeExt Valueable TurretAnim_IdleRate; Valueable TurretAnim_FiringRate; + Valueable Sell; + Valueable Sell_Reverse; + Valueable Undeploy; + Valueable Undeploy_Reverse; + Nullable BuildupTime; + Nullable SellTime; + char SellFileName[0x20]; + char UndeployFileName[0x20]; + // Ares 0.2 Valueable CloningFacility; @@ -203,6 +212,12 @@ class BuildingTypeExt , TurretAnim_LowPowerFiringFrames { 0 } , TurretAnim_IdleRate { 1 } , TurretAnim_FiringRate { 1 } + , Sell { nullptr } + , Sell_Reverse { true } + , Undeploy { nullptr } + , Undeploy_Reverse { true } + , BuildupTime {} + , SellTime {} // Ares 0.2 , CloningFacility { false } @@ -213,7 +228,10 @@ class BuildingTypeExt // Ares 3.0 , UnitSell {} - { } + { + SellFileName[0] = '\0'; + UndeployFileName[0] = '\0'; + } // Ares 0.A functions int GetSuperWeaponCount() const;