From 745a7bcac84f8583b9cfe6a0e9ffbf323af642d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=AA=E5=91=B3=E9=BA=BB=E9=85=B1?= <93972760+TaranDahl@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:07:36 +0800 Subject: [PATCH 1/2] core --- CREDITS.md | 2 +- Phobos.vcxproj | 3 + docs/AI-Scripting-and-Mapping.md | 20 ++++ docs/Whats-New.md | 2 +- .../LC_MESSAGES/AI-Scripting-and-Mapping.po | 45 +++++++++ docs/locale/zh_CN/LC_MESSAGES/CREDITS.po | 7 +- docs/locale/zh_CN/LC_MESSAGES/Whats-New.po | 10 +- src/Ext/Rules/Body.cpp | 2 + src/Ext/Rules/Body.h | 2 + src/Ext/TeamType/Body.cpp | 99 +++++++++++++++++++ src/Ext/TeamType/Body.h | 47 +++++++++ src/Misc/Hooks.BugFixes.cpp | 12 ++- src/Phobos.Ext.cpp | 2 + 13 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 src/Ext/TeamType/Body.cpp create mode 100644 src/Ext/TeamType/Body.h diff --git a/CREDITS.md b/CREDITS.md index 4090c19e99..e7897be6f2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -742,7 +742,6 @@ This page lists all the individual contributions to the project by their author. - Fix an issue that the AI would look for the first house in the array as an enemy instead of the nearest one when there were no enemies - `AllowBerzerkOnAllies` - Fix an issue that retaliation will make the unit keep switching among multiple targets with the same amount of threat - - Fix an issue where units recruited by a team with `AreTeamMembersRecruitable=false` cannot be recruited even if they have been liberated by that team - Global default value for `DefaultToGuardArea` - Weapon range finding in cylinder - Allow jumpjet climbing ignore building height @@ -766,6 +765,7 @@ This page lists all the individual contributions to the project by their author. - Framework for dynamic sight - Fix voxel projectile and animation lighting issues - Export interface for external call + - [Adjust recruitable status on team member discharge](AI-Scripting-and-Mapping.md#adjust-recruitable-status-on-team-member-discharge) - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 5d2d15eca0..665785fc04 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -151,6 +151,8 @@ + + @@ -341,6 +343,7 @@ + diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 3935e4bb7e..d8f5f96ffa 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -965,3 +965,23 @@ In `mycampaign.map`: ID=EventCount,...,606,2,0,[AttachEffectType],... ... ``` + +## Teams + +### Adjust recruitable status on team member discharge + +- In vanilla, when a unit is added to a team, its `RecruitableB` flag is overwritten by the team's `AreTeamMembersRecruitable` setting. When the unit is discharged from the team, the flag is not restored. The following settings allow a team to reset this flag when discharging its members. + - `[General] → IsDischargedMemberAutocreateRecruitable` sets the global default value. + - `[TeamType] → IsDischargedMemberAutocreateRecruitable` overrides the global default for a specific team type. + - If set to a value **greater than 0**, the discharged unit is forcibly marked as recruitable. + - If set to **0**, the discharged unit is forcibly marked as not recruitable. + - If set to a value **less than 0** (default: `-1`), the original game behavior is preserved. + +In `rulesmd.ini`: +```ini +[General] +IsDischargedMemberAutocreateRecruitable=-1 ; integer + +[SOMETEAMTYPE] ; TeamType +IsDischargedMemberAutocreateRecruitable= ; integer, default to [General] -> IsDischargedMemberAutocreateRecruitable +``` diff --git a/docs/Whats-New.md b/docs/Whats-New.md index ca71d62fdf..f0b80cfd28 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -584,6 +584,7 @@ HideShakeEffects=false ; boolean - Introduce weight selection rules for ExtraWarheads (by Noble_Fish) - [Building turret idle/firing/low power animations](Fixed-or-Improved-Logics.md#building-turret-animations) (by Starkku) - [Add action `512 Set Follower for Associated Unit...`](AI-Scripting-and-Mapping.md#set-follower-for-associated-unit) (by Noble_Fish) +- [Adjust recruitable status on team member discharge](AI-Scripting-and-Mapping.md#adjust-recruitable-status-on-team-member-discharge) (by TaranDahl) #### Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) @@ -638,7 +639,6 @@ HideShakeEffects=false ; boolean - Fixed an issue where mining vehicles could not move after leaving a tank bunker (by FlyStar) - Fixed the bug where selected technos would lose their selection if their regular mind control was replaced with permanent mind control or with the control from the Psychic Dominator superweapon (by NetsuNegi) - Fixed an issue that retaliation will make the unit keep switching among multiple targets with the same amount of threat (by TaranDahl) -- Fixed the issue where units recruited by a team with `AreTeamMembersRecruitable=false` cannot be recruited even if they have been liberated by that team (by TaranDahl) - Fixed the bug that cause technos teleport to cell 0,0 by ChronoSphere superweapon (by NetsuNegi) - Fixed the bug that techno in attack move will move to target if it cannot attack it (by NetsuNegi) - Fixed the bug in AI scripts 56 and 57 that forced the launch of superweapons with index numbers 3 and 4 (by FlyStar) diff --git a/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po b/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po index c08ebea4ae..1a49eaab01 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po +++ b/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po @@ -1727,3 +1727,48 @@ msgid "" "same type of attached effect to the techno." msgstr "外源 AE:赋予时、对同一单位赋予同一 AE 使其刷新时。" +msgid "Teams" +msgstr "小队" + +msgid "Adjust recruitable status on team member discharge" +msgstr "小队成员解散时调整可招募性" + +msgid "" +"In vanilla, when a unit is added to a team, its `RecruitableB` flag is " +"overwritten by the team's `AreTeamMembersRecruitable` setting. When the " +"unit is discharged from the team, the flag is not restored. The following " +"settings allow a team to reset this flag when discharging its members." +msgstr "" +"在原版游戏中,当单位被加入到小队中时,其 `RecruitableB` 字段会被小队的设置 " +"`AreTeamMembersRecruitable` 覆写。单位被小队解散时,该字段也不会被恢复。以下设置" +"允许小队在解散时重新设置该字段。" + +msgid "" +"`[General] → IsDischargedMemberAutocreateRecruitable` sets the global " +"default value." +msgstr "" +"`[General] → IsDischargedMemberAutocreateRecruitable` 设置全局默认值。" + +msgid "" +"`[TeamType] → IsDischargedMemberAutocreateRecruitable` overrides the " +"global default for a specific team type." +msgstr "" +"`[TeamType] → IsDischargedMemberAutocreateRecruitable` 对特定小队类型覆盖全局默认值。" + +msgid "" +"If set to a value greater than 0, the discharged unit is forcibly marked " +"as recruitable." +msgstr "" +"如果值大于 0,解散的单位会被强制标记为可招募。" + +msgid "" +"If set to 0, the discharged unit is forcibly marked as not recruitable." +msgstr "" +"如果值为 0,解散的单位会被强制标记为不可招募。" + +msgid "" +"If set to a value less than 0 (default: -1), the original game behavior " +"is preserved." +msgstr "" +"如果值小于 0(默认值:-1),则保留原版游戏行为。" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index af9bc7b7d0..70ff311a79 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -2482,11 +2482,8 @@ msgid "" "multiple targets with the same amount of threat" msgstr "修复了反击行为会让单位在多个相同威胁值的目标间不断切换的问题" -msgid "" -"Fix an issue where units recruited by a team with " -"`AreTeamMembersRecruitable=false` cannot be recruited even if they have " -"been liberated by that team" -msgstr "修复了单位被 `AreTeamMembersRecruitable=false` 的小队招募过则即便原小队解散也无法再被其他小队招募的 Bug" +msgid "Adjust recruitable status on team member discharge" +msgstr "小队成员解散时调整可招募性" msgid "Global default value for `DefaultToGuardArea`" msgstr "`DefaultToGuardArea` 的全局默认值" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index db2be809f7..b1646bc542 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -2229,12 +2229,12 @@ msgid "" msgstr "修复了反击行为会让单位在多个相同威胁值的目标间不断切换的问题(by TaranDahl)" msgid "" -"Fixed the issue where units recruited by a team with " -"`AreTeamMembersRecruitable=false` cannot be recruited even if they have " -"been liberated by that team (by TaranDahl)" +"- [Adjust recruitable status on team member discharge](AI-Scripting-and-" +"Mapping.md#adjust-recruitable-status-on-team-member-discharge) (by " +"TaranDahl)" msgstr "" -"修复了单位被 `AreTeamMembersRecruitable=false` 的小队招募过则即便原小队解散也无法再被其他小队招募的 " -"Bug(by TaranDahl)" +"- [小队成员解散时调整可招募性](AI-Scripting-and-Mapping.md#adjust-recruitable-status-on-team-member-discharge)(by " +"TaranDahl)" msgid "" "Fixed the bug that cause technos teleport to cell 0,0 by ChronoSphere " diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 9f9385eb10..06277e32fc 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -224,6 +224,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->IsVoiceCreatedGlobal.Read(exINI, GameStrings::AudioVisual, "IsVoiceCreatedGlobal"); this->SelectionFlashDuration.Read(exINI, GameStrings::AudioVisual, "SelectionFlashDuration"); + this->IsDischargedMemberAutocreateRecruitable.Read(exINI, GameStrings::General, "IsDischargedMemberAutocreateRecruitable"); this->DrawInsignia_OnlyOnSelected.Read(exINI, GameStrings::AudioVisual, "DrawInsignia.OnlyOnSelected"); this->DrawInsignia_AdjustPos_Infantry.Read(exINI, GameStrings::AudioVisual, "DrawInsignia.AdjustPos.Infantry"); this->DrawInsignia_AdjustPos_Buildings.Read(exINI, GameStrings::AudioVisual, "DrawInsignia.AdjustPos.Buildings"); @@ -609,6 +610,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->DrawTurretShadow) .Process(this->IsVoiceCreatedGlobal) .Process(this->SelectionFlashDuration) + .Process(this->IsDischargedMemberAutocreateRecruitable) .Process(this->DrawInsignia_OnlyOnSelected) .Process(this->DrawInsignia_AdjustPos_Infantry) .Process(this->DrawInsignia_AdjustPos_Buildings) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 9bbec79fa8..b77b3e3396 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -176,6 +176,7 @@ class RulesExt Valueable ShowPowerPlantEnhancerRange; Valueable IsVoiceCreatedGlobal; Valueable SelectionFlashDuration; + Valueable IsDischargedMemberAutocreateRecruitable; Nullable DropPodTrailer; AnimTypeClass* DropPodDefaultTrailer; SHPStruct* PodImage; @@ -486,6 +487,7 @@ class RulesExt , DrawTurretShadow { false } , IsVoiceCreatedGlobal { false } , SelectionFlashDuration { 0 } + , IsDischargedMemberAutocreateRecruitable { -1 } , DrawInsignia_OnlyOnSelected { false } , DrawInsignia_AdjustPos_Infantry { { 5, 2 } } , DrawInsignia_AdjustPos_Buildings { { 10, 6 } } diff --git a/src/Ext/TeamType/Body.cpp b/src/Ext/TeamType/Body.cpp new file mode 100644 index 0000000000..815605178b --- /dev/null +++ b/src/Ext/TeamType/Body.cpp @@ -0,0 +1,99 @@ +#include "Body.h" + +TeamTypeExt::ExtContainer TeamTypeExt::ExtMap; + +// ============================= +// load / save + +void TeamTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) +{ + auto pThis = this->OwnerObject(); + const char* pSection = pThis->ID; + INI_EX exINI(pINI); + + this->IsDischargedMemberAutocreateRecruitable.Read(exINI, pSection, "IsDischargedMemberAutocreateRecruitable"); +} + +template +void TeamTypeExt::ExtData::Serialize(T& Stm) +{ + Stm + .Process(this->IsDischargedMemberAutocreateRecruitable) + ; +} + +void TeamTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) +{ + Extension::LoadFromStream(Stm); + this->Serialize(Stm); +} + +void TeamTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) +{ + Extension::SaveToStream(Stm); + this->Serialize(Stm); +} + +// ============================= +// container + +TeamTypeExt::ExtContainer::ExtContainer() : Container("TeamTypeClass") { } + +TeamTypeExt::ExtContainer::~ExtContainer() = default; + +// ============================= +// container hooks + +DEFINE_HOOK(0x6F08E6, TeamTypeClass_CTOR, 0x6) +{ + GET(TeamTypeClass*, pItem, EAX); + + TeamTypeExt::ExtMap.TryAllocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x6F20D0, TeamTypeClass_DTOR, 0x6) +{ + GET(TeamTypeClass*, pItem, ECX); + + TeamTypeExt::ExtMap.Remove(pItem); + + return 0; +} + +DEFINE_HOOK_AGAIN(0x6F1BB0, TeamTypeClass_SaveLoad_Prefix, 0x5) +DEFINE_HOOK(0x6F1B90, TeamTypeClass_SaveLoad_Prefix, 0x8) +{ + GET_STACK(TeamTypeClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + TeamTypeExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK(0x6F1C35, TeamTypeClass_Load_Suffix, 0x5) +{ + TeamTypeExt::ExtMap.LoadStatic(); + + return 0; +} + +DEFINE_HOOK(0x6F1BAA, TeamTypeClass_Save_Suffix, 0x5) +{ + TeamTypeExt::ExtMap.SaveStatic(); + + return 0; +} + +DEFINE_HOOK_AGAIN(0x6F1535, TeamTypeClass_LoadFromINI, 0xA) +DEFINE_HOOK(0x6F1528, TeamTypeClass_LoadFromINI, 0xA) +{ + GET(TeamTypeClass*, pItem, ESI); + GET_STACK(CCINIClass*, pINI, 0xCC); + + TeamTypeExt::ExtMap.LoadFromINI(pItem, pINI); + + return 0; +} diff --git a/src/Ext/TeamType/Body.h b/src/Ext/TeamType/Body.h new file mode 100644 index 0000000000..100fecf870 --- /dev/null +++ b/src/Ext/TeamType/Body.h @@ -0,0 +1,47 @@ +#pragma once +#include + +#include +#include + +class TeamTypeExt +{ +public: + using base_type = TeamTypeClass; + + static constexpr DWORD Canary = 0xABCDEF01; + static constexpr size_t ExtPointerOffset = 0xBC; + + class ExtData final : public Extension + { + public: + ExtData(TeamTypeClass* OwnerObject) : Extension(OwnerObject) + , IsDischargedMemberAutocreateRecruitable { } + { } + + virtual ~ExtData() = default; + + virtual void LoadFromINIFile(CCINIClass* pINI) override; + // virtual void Initialize() override; + + Nullable IsDischargedMemberAutocreateRecruitable; + + 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; +}; diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index a2931a9486..1aaeff019c 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -10,6 +10,7 @@ #include #include #include +#include /* Allow usage of TileSet of 255 and above without making NE-SW broken bridges unrepairable @@ -3215,7 +3216,16 @@ DEFINE_HOOK(0x708A81, TechnoClass_CanRetaliate_CheckThreat, 0x5) DEFINE_HOOK(0x6EA870, TeamClass_LiberateMember_Start, 0x6) { GET_STACK(FootClass*, pMember, 0x4); - pMember->RecruitableB = true; + GET(TeamClass*, pTeam, ECX); + + const auto pTeamTypeExt = TeamTypeExt::ExtMap.Find(pTeam->Type) + const int value = pTeamTypeExt->IsDischargedMemberAutocreateRecruitable.Get(RulesExt::Global()->IsDischargedMemberAutocreateRecruitable); + + if (value > 0) + pMember->RecruitableB = true; + else if (value == 0) + pMember->RecruitableB = false; + return 0; } diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index 1c0acf5406..f0ef55225e 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -225,6 +226,7 @@ using PhobosTypeRegistry = TypeRegistry < SWTypeExt, TActionExt, TeamExt, + TeamTypeExt, TechnoExt, TechnoTypeExt, TerrainTypeExt, From 9cc4bc3e8506bcbf12801df7bff98f4f155cb341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=AA=E5=91=B3=E9=BA=BB=E9=85=B1?= <93972760+TaranDahl@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:17 +0800 Subject: [PATCH 2/2] move to team/hooks.cpp --- src/Ext/Team/Hooks.cpp | 17 +++++++++++++++++ src/Misc/Hooks.BugFixes.cpp | 18 ------------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Ext/Team/Hooks.cpp b/src/Ext/Team/Hooks.cpp index fc6524fd53..9f549238a9 100644 --- a/src/Ext/Team/Hooks.cpp +++ b/src/Ext/Team/Hooks.cpp @@ -1,6 +1,7 @@ #include "Body.h" #include #include +#include // Bugfix: TAction 7,80,107. DEFINE_HOOK(0x65DF67, TeamTypeClass_CreateMembers_LoadOntoTransport, 0x6) @@ -147,3 +148,19 @@ DEFINE_HOOK(0x6EF57F, TeamClass_GetTaskForceMissingMemberTypes_Consideration, 0x return SkipThisMember; } + +DEFINE_HOOK(0x6EA870, TeamClass_LiberateMember_Start, 0x6) +{ + GET_STACK(FootClass*, pMember, 0x4); + GET(TeamClass*, pTeam, ECX); + + const auto pTeamTypeExt = TeamTypeExt::ExtMap.Find(pTeam->Type); + const int value = pTeamTypeExt->IsDischargedMemberAutocreateRecruitable.Get(RulesExt::Global()->IsDischargedMemberAutocreateRecruitable); + + if (value > 0) + pMember->RecruitableB = true; + else if (value == 0) + pMember->RecruitableB = false; + + return 0; +} diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 1aaeff019c..a8054dd5a1 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -10,7 +10,6 @@ #include #include #include -#include /* Allow usage of TileSet of 255 and above without making NE-SW broken bridges unrepairable @@ -3212,23 +3211,6 @@ DEFINE_HOOK(0x708A81, TechnoClass_CanRetaliate_CheckThreat, 0x5) return pThis->ThreatCoeffients(pAttacker, &CoordStruct::Empty) <= pThis->ThreatCoeffients((ObjectClass*)(pThis->Target), &CoordStruct::Empty) ? SkipRetaliate : GoOtherChecks; } -// Fixed the issue where units recruited by a team with `AreTeamMembersRecruitable=false` cannot be recruited even if they have been liberated by that team. -DEFINE_HOOK(0x6EA870, TeamClass_LiberateMember_Start, 0x6) -{ - GET_STACK(FootClass*, pMember, 0x4); - GET(TeamClass*, pTeam, ECX); - - const auto pTeamTypeExt = TeamTypeExt::ExtMap.Find(pTeam->Type) - const int value = pTeamTypeExt->IsDischargedMemberAutocreateRecruitable.Get(RulesExt::Global()->IsDischargedMemberAutocreateRecruitable); - - if (value > 0) - pMember->RecruitableB = true; - else if (value == 0) - pMember->RecruitableB = false; - - return 0; -} - DEFINE_HOOK_AGAIN(0x6F845D, TechnoClass_CanAutoTargetObject_Garrisonable, 0x6); DEFINE_HOOK(0x6F833E, TechnoClass_CanAutoTargetObject_Garrisonable, 0x6) {