diff --git a/CREDITS.md b/CREDITS.md index 9513761bc1..86cdbb15e9 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1003,7 +1003,12 @@ This page lists all the individual contributions to the project by their author. - Add `ClampToScreen` tag for `BannerType` to control whether banner position is clamped to the visible area - **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 +- **frg2089 (舰队的偶像-岛风酱!)**: + - Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed + - Convert TechnoType into any other type, including to and from `BuildingType`s + - Infantry walk to the centre of their cell before converting into another type + - Carry over health percentage, veteran status, attribute multipliers and slave manager on conversion + - Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo` - **weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral houses - **dh381-1**: - Detonate ivan bomb on impact diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index e5fec4265e..1854bcac5b 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -341,6 +341,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - Fixed a crash or memory corruption when a wave (e.g. `IsSonic=true`) was drawn while its firer was at the same horizontal position as its target. - Fixed the bug that crashing or sinking unit can be recruit into taskforce team. - Units in Tank Bunker with deploy functions no longer perform them when deployed out of Tank Bunker. +- Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over the health percentage, veteran status, armor / firepower / speed multipliers and slave manager of the source techno to the new one, in addition to the mind control, shields, invulnerability and attached effects that were already transferred. ## Fixes / interactions with other extensions diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 5d44d10e45..bd1beb10ab 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -886,12 +886,14 @@ SpyEffect.InfiltratorSuperWeapon= ; SuperWeaponType - Now infantry can perform type conversion immediately after the `Deploy` or `Undeploy` sequence action has been executed. - If a deployable infantry has no deploy sequence, it will immediately execute `Convert.Deploy` after a deploy order is issued, just like deployable vehicles. + - The new type does not have to be an infantry type (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). If it is not, the infantry first walks to the centre of the cell it is occupying. + - If the new type is a `BuildingType`, the deploy order is only carried out when the building can be placed on the spot the infantry stands on. Otherwise the `NoDeploy` cursor is shown and the *Cannot deploy here* EVA is played. In `rulesmd.ini`: ```ini [SOMEINFANTRY] ; InfantryType -Convert.Deploy= ; InfantryType -Convert.Undeploy= ; InfantryType +Convert.Deploy= ; TechnoType +Convert.Undeploy= ; TechnoType ``` ### Customizable FLH when infantry is prone or deployed @@ -1408,7 +1410,7 @@ SW.TechLevel=0 ; integer ### Convert TechnoType -- Warheads can now change TechnoTypes of affected units to other Types in the same category (infantry to infantry, vehicles to vehicles, aircraft to aircraft). +- Warheads can now change TechnoTypes of affected units to any other TechnoType, no matter which category it belongs to, including `BuildingType`s. - `ConvertN.From` (where N is 0, 1, 2...) specifies which TechnoTypes are valid for conversion. This entry can have many types listed, meanging that many types will be converted at once. When no types are included, conversion will affect all valid targets. - `ConvertN.To` specifies the TechnoType which is the result of conversion. - `ConvertN.AffectsHouse` specifies whose units can be converted. @@ -1437,7 +1439,7 @@ Convert.AffectsHouse=owner ; List of Affected House Enumeration (none|owner ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} @@ -1707,7 +1709,7 @@ AutoTargetAI.NoThreatBuildings=true ; boolean - Units can now be converted into another unit by ammo count. - `Ammo.AutoConvertMinimumAmount` determines the minimal number of ammo at which a unit converts automatically after the ammo update. - `Ammo.AutoConvertMaximumAmount` determines the maximum number of ammo at which a unit converts automatically after the ammo update. -- `Ammo.AutoConvertType` specify the new techno after the conversion. This unit must be of the same type of the original (infantry -> infantry, vehicle -> vehicle or aircraft -> aircraft). +- `Ammo.AutoConvertType` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). - Setting a negative number will disable the ammo count check, and when both checks are disabled, conversion will not occur. In `rulesmd.ini`: @@ -1719,7 +1721,7 @@ Ammo.AutoConvertType= ; TechnoType, after conversion ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} @@ -1731,7 +1733,8 @@ This feature requires Ares 3.0 or higher to function! When Ares 3.0+ is not dete - Units can now be converted into another unit by health percentage. - `Convert.Health.AbovePercent` determines the minimal health percentage at which a unit converts automatically. - `Convert.Health.BelowPercent` determines the maximum health percentage at which a unit converts automatically. -- `Convert.Health` specify the new techno after the conversion. This unit must be of the same type of the original (infantry -> infantry, vehicle -> vehicle or aircraft -> aircraft). +- `Convert.Health` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). +- `Convert.Health.AfterBuildup`, if set to true, delays the conversion of a `BuildingType` until its buildup has finished. - Setting a negative number will disable the HP check, and when both checks are disabled, conversion will not occur. In `rulesmd.ini`: @@ -1740,10 +1743,13 @@ In `rulesmd.ini`: Convert.Health.AbovePercent=-1.0 ; floating point value, percents or absolute Convert.Health.BelowPercent=-1.0 ; floating point value, percents or absolute Convert.Health= ; TechnoType, after conversion + +[SOMEBUILDING] ; BuildingType +Convert.Health.AfterBuildup=false ; boolean ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} @@ -1874,9 +1880,39 @@ DecloakAnims= ; List of Animation, default to [AudioVisual] -> D Cloak.KickOutParasite= ; boolean, default to [General] -> Cloak.KickOutParasite ``` +### Convert TechnoType into any other type + +- Now any TechnoType can be converted into any other TechnoType, no matter which category it belongs to, including to and from `BuildingType`s. + - This applies to every form of type conversion: warhead and superweapon `Convert` pairs, `Convert.Deploy`, `Convert.Undeploy`, `Convert.Health`, `Ammo.AutoConvertType`, `Convert.HumanToComputer` / `Convert.ComputerToHuman` and Ares' type conversion. + - If the new type is a `BuildingType`, it is placed on the spot the source techno stands on, as long as its whole foundation fits there. If it does not fit, the conversion does not happen; a deploy order for `Convert.Deploy` is refused with the `NoDeploy` cursor and the *Cannot deploy here* EVA. + - Infantry that deploy or undeploy into a type other than an infantry type first walk to the centre of the cell they are occupying, so the conversion animation is played in the middle of the cell. + - The new techno inherits the health percentage, veteran status, mind control, shields, invulnerability, attached effects, armor / firepower / speed multipliers and slave manager of the source techno. This also applies to vanilla `DeploysInto`, `UndeploysInto` and selling. + - `Convert.Direction` determines the direction the new techno faces when converting from a `BuildingType`. If not set, the new techno keeps the facing of the source. + - The building animations played while converting to or from a building are customizable with the following tags. Each of them can also be set in `[General]` to define its global default value. + - `Convert.ShowBuildupFrom` makes the building being converted play its sell animation instead of disappearing immediately. Requires the building to be sellable. + - `Convert.ShowBuildupTo` makes the newly created building play its buildup animation and plays `[AudioVisual] -> BuildingSlam` at its location instead of appearing finished right away. Requires `Buildup=` (the name of an SHP file) to be set for the new building type in `artmd.ini`. + - `Convert.ShowBuildup` enables both of the above at once. + +In `rulesmd.ini`: +```ini +[General] +Convert.ShowBuildup=false ; boolean +Convert.ShowBuildupFrom=false ; boolean +Convert.ShowBuildupTo=false ; boolean + +[SOMETECHNO] ; TechnoType +Convert.ShowBuildup= ; boolean, default to [General] -> Convert.ShowBuildup +Convert.ShowBuildupFrom= ; boolean, default to [General] -> Convert.ShowBuildupFrom +Convert.ShowBuildupTo= ; boolean, default to [General] -> Convert.ShowBuildupTo + +[SOMEBUILDING] ; BuildingType +Convert.Direction= ; Direction type (integers from 0-255) +``` + ### Convert TechnoType on owner house change - You can now change a unit's type when changing ownership from human to computer or from computer to human. + - This also works for `BuildingType`s (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). In `rulesmd.ini`: ```ini @@ -3128,7 +3164,7 @@ ChangeOwner.MindControlAnim= ; Animation ![image](_static/images/convertwh.gif) *Vehicle version of Genetic Converter in [NanoStorm](https://www.bilibili.com/opus/896077937747427433)* -- Warheads can now change TechnoTypes of affected units to other Types in the same category (infantry to infantry, vehicles to vehicles, aircraft to aircraft). +- Warheads can now change TechnoTypes of affected units to any other TechnoType, no matter which category it belongs to, including `BuildingType`s. - `ConvertN.From` (where N is 0, 1, 2...) specifies which TechnoTypes are valid for conversion. This entry can have many types listed, meanging that many types will be converted at once. When no types are included, conversion will affect all valid targets. - `ConvertN.To` specifies the TechnoType which is the result of conversion. - `ConvertN.AffectsHouse` specifies whose units can be converted. @@ -3156,7 +3192,7 @@ Convert.AffectsHouse=all ; List of Affected House Enumeration (none|owner ``` ```{warning} -This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html). This feature does not support BuildingTypes. +This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type). ``` ```{warning} diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 9b165176e5..4e1088e112 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -2061,6 +2061,10 @@ PreserveCSFWhitespace=false ; boolean - [Light flash toggle for ParticleType alpha images](User-Interface.md#visual-effects-toggling) (by Starkku) - [Allow customizing if `IsSimpleDeployer` unit can deploy on top of (passable) buildings](Fixed-or-Improved-Logics.md#issimpledeployer-customizations) (by Starkku) - [Preserve whitespace in CSF](User-Interface.md#preserve-whitespace-in-csf) (by Noble_Fish) +- [Convert TechnoType into any other type](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type) (by frg2089) +- [Infantry deploying or undeploying into a type other than an infantry type now walk to the centre of their cell first](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (by frg2089) +- Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over health percentage, veteran status, attribute multipliers and slave manager of the source techno (by frg2089) +- Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo` (by frg2089) #### 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/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 7d959b4502..ce7c536cc2 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -3444,12 +3444,23 @@ msgstr "**obsidianus** - 自动根据血量变形" msgid "**Nuke** - Reload speed adjustment on promotion" msgstr "**Nuke** - 升级时调整装填速率" -msgid "" -"**frg2089 (舰队的偶像-岛风酱!)** - Fix `Slaved.OwnerWhenMasterKilled` not being " -"respected when the master is sold or self-destructed" -msgstr "" -"**frg2089 (舰队的偶像-岛风酱!)** - 修复了奴隶矿场被出售或自毁时 `Slaved.OwnerWhenMasterKilled` " -"不被遵守的问题" +msgid "**frg2089 (舰队的偶像-岛风酱!)**:" +msgstr "**frg2089 (舰队的偶像-岛风酱!)**:" + +msgid "Fix `Slaved.OwnerWhenMasterKilled` not being respected when the master is sold or self-destructed" +msgstr "修复了奴隶矿场被出售或自毁时 `Slaved.OwnerWhenMasterKilled` 不被遵守的问题" + +msgid "Convert TechnoType into any other type, including to and from `BuildingType`s" +msgstr "允许任意科技类型转换为任意其他科技类型,包括与 `BuildingType` 之间的相互转换" + +msgid "Infantry walk to the centre of their cell before converting into another type" +msgstr "步兵在转换为其他类型前会先走到所在单元格的中央" + +msgid "Carry over health percentage, veteran status, attribute multipliers and slave manager on conversion" +msgstr "转换时继承生命值百分比、老兵等级、属性倍增器与奴隶管理器" + +msgid "Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo`" +msgstr "`Convert.ShowBuildup`、`Convert.ShowBuildupFrom` 和 `Convert.ShowBuildupTo` 的全局默认值" msgid "" "**weiyongxuan** - Extended `CanTargetHouses` to allow targeting neutral " diff --git a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po index ebac0b7b69..8474be8123 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po @@ -1767,6 +1767,9 @@ msgid "" "Fixed the bug that crashing or sinking unit can be recruit into taskforce team." msgstr "修复了坠落或沉没中的单位可以被招募进特遣小队的 Bug" +msgid "Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over the health percentage, veteran status, armor / firepower / speed multipliers and slave manager of the source techno to the new one, in addition to the mind control, shields, invulnerability and attached effects that were already transferred." +msgstr "原版的 `DeploysInto`、`UndeploysInto` 与出售现在除了原本就会传递的精神控制、护盾、无敌效果与附加效果之外,还会将源单位的生命值百分比、老兵等级、护甲/火力/速度倍增器与奴隶管理器传递给新单位。" + msgid "Fixes / interactions with other extensions" msgstr "修复或与其他扩展的交互" diff --git a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po index 9545930317..2b89ad348e 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po @@ -2163,6 +2163,12 @@ msgid "" "deployable vehicles." msgstr "如果一个可部署的步兵没有部署序列,那么它将会像可部署的载具那样在下达部署命令后立即执行 `Convert.Deploy`。" +msgid "The new type does not have to be an infantry type (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)). If it is not, the infantry first walks to the centre of the cell it is occupying." +msgstr "新类型不必是步兵类型(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。若不是步兵类型,步兵会先走到其所在单元格的中央。" + +msgid "If the new type is a `BuildingType`, the deploy order is only carried out when the building can be placed on the spot the infantry stands on. Otherwise the `NoDeploy` cursor is shown and the *Cannot deploy here* EVA is played." +msgstr "若新类型是 `BuildingType`,只有当建筑可以放置在步兵所处的位置时部署命令才会被执行。否则将显示 `NoDeploy` 光标并播放 *Cannot deploy here* 语音。" + msgid "Customizable FLH when infantry is prone or deployed" msgstr "自定义步兵匍匐或部署状态的 FLH" @@ -3330,11 +3336,8 @@ msgstr "" msgid "Convert TechnoType" msgstr "单位转换超武" -msgid "" -"Warheads can now change TechnoTypes of affected units to other Types in " -"the same category (infantry to infantry, vehicles to vehicles, aircraft " -"to aircraft)." -msgstr "现在弹头可以将受影响单位的科技类型更改为同大类的其他科技类型(步兵到步兵,车辆到车辆,战机到战机)。" +msgid "Warheads can now change TechnoTypes of affected units to any other TechnoType, no matter which category it belongs to, including `BuildingType`s." +msgstr "现在弹头可以将受影响单位的科技类型更改为任意其他科技类型,无论其属于哪个大类,包括 `BuildingType`。" msgid "" "`ConvertN.From` (where N is 0, 1, 2...) specifies which TechnoTypes are " @@ -3372,13 +3375,8 @@ msgid "" "`SOLDIERA` and `SOLDIERB` to `NEWSOLDIER`:" msgstr "例如,这个超级武器可以将所有被影响的己方和友方 `SOLDIERA` 和 `SOLDIERB` 转换为 `NEWSOLDIER`:" -msgid "" -"This feature has the same limitations as [Ares' Type Conversion](https" -"://ares-developers.github.io/Ares-docs/new/typeconversion.html). This " -"feature does not support BuildingTypes." -msgstr "" -"此功能与 [Ares 的单位转换](https://ares-developers.github.io/Ares-" -"docs/new/typeconversion.html) 存在同样的限制。此功能不支持建筑类型。" +msgid "This feature has the same limitations as [Ares' Type Conversion](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) when converting between TechnoTypes of the same category. Conversions to or from `BuildingType`s are handled by Phobos itself, see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type)." +msgstr "当在同一大类的科技类型之间转换时,此功能与 [Ares 的单位转换](https://ares-developers.github.io/Ares-docs/new/typeconversion.html) 存在同样的限制。与 `BuildingType` 之间的转换由 Phobos 自行处理,详见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type)。" msgid "" "This feature requires Ares 3.0 or higher to function! When Ares 3.0+ is " @@ -3920,11 +3918,8 @@ msgid "" "which a unit converts automatically after the ammo update." msgstr "`Ammo.AutoConvertMaximumAmount` 决定触发自动变形的最大弹药量。" -msgid "" -"`Ammo.AutoConvertType` specify the new techno after the conversion. This " -"unit must be of the same type of the original (infantry -> infantry, " -"vehicle -> vehicle or aircraft -> aircraft)." -msgstr "`Ammo.AutoConvertType` 指定变形成哪个新单位。该单位必须与原单位同属一类(载具变载具,步兵变步兵,战机变战机)。" +msgid "`Ammo.AutoConvertType` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type))." +msgstr "`Ammo.AutoConvertType` 指定变形成哪个新单位。新单位可以是任意科技类型,无论其属于哪个大类,包括 `BuildingType`(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。" msgid "" "Setting a negative number will disable the ammo count check, and when " @@ -3947,11 +3942,11 @@ msgid "" " which a unit converts automatically." msgstr "`Convert.Health.BelowPercent` 决定了触发单位自动变身的最大生命值百分比。" -msgid "" -"`Convert.Health` specify the new techno after the conversion. This unit " -"must be of the same type of the original (infantry -> infantry, vehicle " -"-> vehicle or aircraft -> aircraft)." -msgstr "`Convert.Health` 指定变形成哪个新单位。该单位必须与原单位同属一类(载具变载具,步兵变步兵,战机变战机)。" +msgid "`Convert.Health` specify the new techno after the conversion. The new techno can be of any TechnoType, no matter which category it belongs to, including a `BuildingType` (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type))." +msgstr "`Convert.Health` 指定变形成哪个新单位。新单位可以是任意科技类型,无论其属于哪个大类,包括 `BuildingType`(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。" + +msgid "`Convert.Health.AfterBuildup`, if set to true, delays the conversion of a `BuildingType` until its buildup has finished." +msgstr "`Convert.Health.AfterBuildup` 设为 true 时,会将 `BuildingType` 的转换推迟到其建造动画结束之后。" msgid "" "Setting a negative number will disable the HP check, and when both checks" @@ -4212,6 +4207,39 @@ msgstr "当单位开始或停止隐形时,可以在其位置播放动画。" msgid "You can also set whether cloak units kick out parasites." msgstr "您也可以设置隐形单位是否踢出寄生者。" +msgid "Convert TechnoType into any other type" +msgstr "允许转换为任意其他科技类型" + +msgid "Now any TechnoType can be converted into any other TechnoType, no matter which category it belongs to, including to and from `BuildingType`s." +msgstr "现在任意科技类型都可以转换为任意其他科技类型,无论其属于哪个大类,包括与 `BuildingType` 之间的相互转换。" + +msgid "This applies to every form of type conversion: warhead and superweapon `Convert` pairs, `Convert.Deploy`, `Convert.Undeploy`, `Convert.Health`, `Ammo.AutoConvertType`, `Convert.HumanToComputer` / `Convert.ComputerToHuman` and Ares' type conversion." +msgstr "这一改动适用于所有类型的转换方式:弹头与超级武器的 `Convert` 组合、`Convert.Deploy`、`Convert.Undeploy`、`Convert.Health`、`Ammo.AutoConvertType`、`Convert.HumanToComputer` / `Convert.ComputerToHuman` 以及 Ares 的类型转换。" + +msgid "If the new type is a `BuildingType`, it is placed on the spot the source techno stands on, as long as its whole foundation fits there. If it does not fit, the conversion does not happen; a deploy order for `Convert.Deploy` is refused with the `NoDeploy` cursor and the *Cannot deploy here* EVA." +msgstr "若新类型是 `BuildingType`,它会被放置在源单位所处的位置,前提是其完整地基能够放下。若放不下则不会执行转换;`Convert.Deploy` 的部署命令会被拒绝并显示 `NoDeploy` 光标,同时播放 *Cannot deploy here* 语音。" + +msgid "Infantry that deploy or undeploy into a type other than an infantry type first walk to the centre of the cell they are occupying, so the conversion animation is played in the middle of the cell." +msgstr "步兵在部署或反部署为非步兵类型时,会先走到其所在单元格的中央,以便在单元格中央播放转换动画。" + +msgid "The new techno inherits the health percentage, veteran status, mind control, shields, invulnerability, attached effects, armor / firepower / speed multipliers and slave manager of the source techno. This also applies to vanilla `DeploysInto`, `UndeploysInto` and selling." +msgstr "新单位会继承源单位的生命值百分比、老兵等级、精神控制、护盾、无敌效果、附加效果、护甲/火力/速度倍增器与奴隶管理器。这一点同样适用于原版的 `DeploysInto`、`UndeploysInto` 与出售。" + +msgid "`Convert.Direction` determines the direction the new techno faces when converting from a `BuildingType`. If not set, the new techno keeps the facing of the source." +msgstr "`Convert.Direction` 决定从该 `BuildingType` 转换而出的新单位的朝向。未设置时新单位保持源单位的朝向。" + +msgid "The building animations played while converting to or from a building are customizable with the following tags. Each of them can also be set in `[General]` to define its global default value." +msgstr "转换为建筑或从建筑转换时播放的建筑动画可以通过以下标签自定义。这些标签也都可以在 `[General]` 中设置以定义其全局默认值。" + +msgid "`Convert.ShowBuildupFrom` makes the building being converted play its sell animation instead of disappearing immediately. Requires the building to be sellable." +msgstr "`Convert.ShowBuildupFrom` 使被转换的建筑播放其出售动画而不是立即消失。要求该建筑可以被出售。" + +msgid "`Convert.ShowBuildupTo` makes the newly created building play its buildup animation and plays `[AudioVisual] -> BuildingSlam` at its location instead of appearing finished right away. Requires `Buildup=` (the name of an SHP file) to be set for the new building type in `artmd.ini`." +msgstr "`Convert.ShowBuildupTo` 使新建的建筑播放其建造动画,并在其位置播放 `[AudioVisual] -> BuildingSlam`,而不是立即以完成状态出现。要求新建筑类型在 `artmd.ini` 中的 `[BuildingType]` 下 `Buildup=`(shp 文件名)非空。" + +msgid "`Convert.ShowBuildup` enables both of the above at once." +msgstr "`Convert.ShowBuildup` 同时启用以上两项。" + msgid "Convert TechnoType on owner house change" msgstr "根据操作者变形" @@ -4220,6 +4248,9 @@ msgid "" "computer or from computer to human." msgstr "现在你可以在操作者从人类改为 AI 玩家或从 AI 玩家改为人类玩家时执行单位转换。" +msgid "This also works for `BuildingType`s (see [Convert TechnoType into any other type](#convert-technotype-into-any-other-type))." +msgstr "该功能同样适用于 `BuildingType`(参见 [任意科技类型之间的转换](#convert-technotype-into-any-other-type))。" + msgid "Custom tint on TechnoTypes" msgstr "染色" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 89feda3b8f..df270e7bee 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -1155,6 +1155,18 @@ msgid "" msgstr "" "[保留 CSF 中的空白](User-Interface.md#preserve-whitespace-in-csf)(by Noble_Fish)" +msgid "[Convert TechnoType into any other type](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type) (by frg2089)" +msgstr "[允许转换为任意其他科技类型](New-or-Enhanced-Logics.md#convert-technotype-into-any-other-type)(by frg2089)" + +msgid "[Infantry deploying or undeploying into a type other than an infantry type now walk to the centre of their cell first](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (by frg2089)" +msgstr "[步兵部署或反部署为非步兵类型时会先走到所在单元格的中央](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying)(by frg2089)" + +msgid "Vanilla `DeploysInto`, `UndeploysInto` and selling now also carry over health percentage, veteran status, attribute multipliers and slave manager of the source techno (by frg2089)" +msgstr "原版的 `DeploysInto`、`UndeploysInto` 与出售现在同样会将源单位的生命值百分比、老兵等级、属性倍增器与奴隶管理器传递给新单位(by frg2089)" + +msgid "Global default value for `Convert.ShowBuildup`, `Convert.ShowBuildupFrom` and `Convert.ShowBuildupTo` (by frg2089)" +msgstr "`Convert.ShowBuildup`、`Convert.ShowBuildupFrom` 和 `Convert.ShowBuildupTo` 的全局默认值(by frg2089)" + msgid "Vanilla fixes:" msgstr "原版问题修复:" diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 1eccc42a25..d30a356a7d 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -566,6 +566,20 @@ bool BuildingTypeExt::AutoUpgradeBuilding(BuildingClass* pBuilding) return false; } +// Computes the top-left cell of the building that would be placed if the given +// foot deployed into the given building type at its current location. Mirrors the +// vanilla deploy logic, which offsets the top-left cell by (-1,-1) for any +// foundation larger than 2x2. +CellStruct BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(BuildingTypeClass* pBuilding, const CellStruct& cell) +{ + auto mapCoords = cell; + + if (pBuilding->GetFoundationWidth() > 2 || pBuilding->GetFoundationHeight(false) > 2) + mapCoords += CellStruct { -1, -1 }; + + return mapCoords; +} + void BuildingTypeExt::Initialize() { TechnoTypeExt::Initialize(); @@ -756,6 +770,8 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) } this->AutoUpgrade.Read(exINI, pSection, "AutoUpgrade"); + this->Convert_Health_AfterBuildup.Read(exINI, pSection, "Convert.Health.AfterBuildup"); + this->Convert_Direction.Read(exINI, pSection, "Convert.Direction"); // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); @@ -926,6 +942,8 @@ void BuildingTypeExt::Serialize(T& Stm) .Process(this->Academy_Country_Types) .Process(this->Academy_Country_Ignore) .Process(this->AutoUpgrade) + .Process(this->Convert_Health_AfterBuildup) + .Process(this->Convert_Direction) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 15654aae89..e5dcb12c56 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -150,6 +150,8 @@ class BuildingTypeExt final : public TechnoTypeExt ValueableVector Academy_Country_Ignore[16]; Valueable AutoUpgrade; + Valueable Convert_Health_AfterBuildup; + Nullable Convert_Direction; // Ares 0.2 Valueable CloningFacility; @@ -272,6 +274,8 @@ class BuildingTypeExt final : public TechnoTypeExt , Academy_Country_Ignore {} , AutoUpgrade { false } + , Convert_Health_AfterBuildup { false } + , Convert_Direction {} // Ares 0.2 , CloningFacility { false } @@ -345,5 +349,6 @@ class BuildingTypeExt final : public TechnoTypeExt static bool CleanUpBuildingSpace(BuildingTypeClass* pBuildingType, CellStruct topLeftCell, HouseClass* pHouse, TechnoClass* pExceptTechno = nullptr); static bool IsSameBuildingType(BuildingTypeClass* pType1, BuildingTypeClass* pType2); static bool AutoUpgradeBuilding(BuildingClass* pBuilding); + static CellStruct GetBuildingTopLeftCellFromDeployCell(BuildingTypeClass* pBuilding, const CellStruct &cell); }; diff --git a/src/Ext/BuildingType/Hooks.Placing.cpp b/src/Ext/BuildingType/Hooks.Placing.cpp index 6211d50d39..2e0ce8b757 100644 --- a/src/Ext/BuildingType/Hooks.Placing.cpp +++ b/src/Ext/BuildingType/Hooks.Placing.cpp @@ -3,8 +3,10 @@ #include #include +#include #include #include +#include #include #include @@ -171,7 +173,7 @@ static inline bool IsSameFenceType(const BuildingTypeClass* const pPostType, con static inline bool CheckCanNotExistHere(FootClass* const pTechno, HouseClass* const pOwner, bool expand, bool& skipFlag, bool& landFootOnly) { - if (pTechno == UnitExt::Deployer) + if (pTechno == TechnoExt::Deployer) { skipFlag = true; return false; @@ -185,6 +187,34 @@ static inline bool CheckCanNotExistHere(FootClass* const pTechno, HouseClass* co return false; } +// The deployer may be either a foot unit or a building (a building can deploy / sell +// into another one). Foot units only cover the cell they currently stand on, while +// buildings cover every cell of their foundation, so the two cases need handling +// separately - BuildingClass has no CurrentMapCoords. +static bool IsDeployerOnCell(const CellStruct& cellCoords) +{ + auto const pDeployer = TechnoExt::Deployer; + + if (!pDeployer) + return false; + + if (auto const pFoot = abstract_cast(pDeployer)) + return pFoot->CurrentMapCoords == cellCoords; + + if (auto const pBuilding = abstract_cast(pDeployer)) + { + auto const foundationCells = BuildingExt::GetFoundationCells(pBuilding, pBuilding->GetMapCoords(), true); + + for (auto const& foundationCell : foundationCells) + { + if (foundationCell == cellCoords) + return true; + } + } + + return false; +} + // Buildable-upon TechnoTypes Helper namespace ProximityTemp { @@ -241,11 +271,17 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else if (pBuildingType->LaserFencePost || pBuildingType->Gate) { - bool skipFlag = UnitExt::Deployer ? UnitExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; + bool skipFlag = IsDeployerOnCell(pCell->MapCoords); bool builtOnCanBeBuiltOn = false; for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { + if (pObject == TechnoExt::Deployer) + { + skipFlag = true; + continue; + } + switch (pObject->WhatAmI()) { case AbstractType::Aircraft: @@ -315,11 +351,17 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } else { - bool skipFlag = UnitExt::Deployer ? UnitExt::Deployer->CurrentMapCoords == pCell->MapCoords : false; + bool skipFlag = IsDeployerOnCell(pCell->MapCoords); bool builtOnCanBeBuiltOn = false; for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { + if (pObject == TechnoExt::Deployer) + { + skipFlag = true; + continue; + } + switch (pObject->WhatAmI()) { case AbstractType::Aircraft: @@ -358,7 +400,7 @@ DEFINE_HOOK(0x47C640, CellClass_CanThisExistHere_IgnoreSomething, 0x6) } } - if (!landFootOnly && !builtOnCanBeBuiltOn && (pCell->OccupationFlags & (skipFlag ? 0x1F : 0x3F))) + if (!landFootOnly && !builtOnCanBeBuiltOn && (pCell->OccupationFlags & (skipFlag ? 0x02 : 0x3F))) { if (expand) landFootOnly = true; diff --git a/src/Ext/Cell/Body.h b/src/Ext/Cell/Body.h index 03055ff1a1..48fd10e435 100644 --- a/src/Ext/Cell/Body.h +++ b/src/Ext/Cell/Body.h @@ -47,6 +47,15 @@ class CellExt final : public AbstractExt std::vector RadLevels { }; int InfantryCount{ 0 }; + // Transient, deliberately not serialized: it only gates whether infantry walking + // within this cell may use the centre sub-position instead of a corner one + // (CellClass::FindInfantrySubposition, see Ext/Cell/Hooks.cpp). + // + // The caller that sets it is responsible for clearing it again, otherwise every + // infantry crossing this cell keeps using the centre. Leaving it out of Serialize + // means a loaded game starts with it false, which is the safe default. + int InfantryCenterAllowed { 0 }; + CellExt(CellClass* OwnerObject) : AbstractExt(OwnerObject) { } diff --git a/src/Ext/Cell/Hooks.cpp b/src/Ext/Cell/Hooks.cpp index 161a5463ee..b41ad82ddf 100644 --- a/src/Ext/Cell/Hooks.cpp +++ b/src/Ext/Cell/Hooks.cpp @@ -9,3 +9,68 @@ DEFINE_HOOK(0x480EA8, CellClass_DamageWall_AdjacentWallDamage, 0x5) pThis->DamageWall(RulesExt::Global()->AdjacentWallDamage); return SkipGameCode; } + +// CellClass::FindInfantrySubposition (0x481180) resolves the coordinate an infantry +// should occupy inside a cell. The per-cell sub-position offsets live in the table +// at 0x89E9F0, which holds five triples, and entry 0 is (0x80, 0x80, 0) - the exact +// cell centre. +// +// The engine only ever derives index 0 when the *requested* coordinate lies within +// 0x3C leptons of the centre (0x4811EC). Otherwise the derivation at +// 0x4811EC..0x481212 produces {2, 3, 4}: it sets bit 0 for X > 0x80 and bit 1 for +// Y > 0x80, then adds 1, which both shifts the pair into 2..4 and skips index 1 (the +// reserved "Top" slot, CellClass occupation bit 0x2). Index 1 is therefore +// unreachable and index 0 means "centre was requested". +// +// Either way index 0 can never survive the selection logic: +// * 0x481316 `TEST EDI,EDI / JZ 0x48138A` sends it to a random roll over the +// corner tables at 0x81CC84 / 0x81CC98, and those only contain indices 1..4, and +// * the per-infantry scan loop at 0x48136A explicitly skips indices 0 and 1 +// (`TEST ECX,ECX / JZ`, `CMP ECX,1 / JZ`). +// Infantry therefore only ever stand on the three corner/lower sub-positions. +// +// The shared tail at 0x481437 indexes the offset table with EDI before it reuses the +// register, so entering it with EDI == 0 produces the centre entry. This hook adds +// that route for infantry that are deliberately walking to the centre +// (CellExt::InfantryCenterAllowed). +// +// The flag is tested *before* dispatching on EDI, and the centre is forced for every +// EDI when it is set. That ordering matters: InfantryClass::UpdatePosition (0x51B967) +// calls this function again on every frame of the walk, re-deriving the index from +// the infantry's live position, which is still off-centre during the approach and so +// yields 2/3/4. Diverting only EDI == 0 would let the vanilla scan snap the infantry +// back onto a corner each frame and it could never reach the centre at all. +// +// The hook replaces the whole index-selection sequence (TEST/JZ/CMP/JZ, 9 bytes) and +// returns an explicit address on every path, so no branch ever falls through the +// patched bytes and no flag state has to be reconstructed. +DEFINE_HOOK(0x481316, CellClass_FindInfantrySubposition_AllowCenter, 0x9) +{ + enum + { + // Vanilla EDI == 0: roll a random corner out of the 0x81CC98 table. + RandomSubposition = 0x48138A, + // Vanilla EDI == 1 (unreachable per the derivation above, kept for fidelity). + SubpositionOne = 0x481356, + // Vanilla EDI >= 2: scan the 0x81CC84 table for a free sub-position. + ScanSubposition = 0x48131F, + // Shared tail; consumes EDI as the offset-table index. + ApplySubposition = 0x481437, + }; + + GET(CellClass*, pThis, ESI); + + auto const pExt = CellExt::TryFetch(pThis); + if (pExt && pExt->InfantryCenterAllowed) + { + // Force the centre entry, whatever index the engine derived. + R->EDI(0); + return ApplySubposition; + } + + // Not centring: reproduce the instructions this hook replaced. + if (R->EDI() == 0) + return RandomSubposition; + + return R->EDI() == 1 ? SubpositionOne : ScanSubposition; +} diff --git a/src/Ext/Foot/Body.cpp b/src/Ext/Foot/Body.cpp index b4b9052bb2..c2f2b1a87b 100644 --- a/src/Ext/Foot/Body.cpp +++ b/src/Ext/Foot/Body.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -920,23 +921,28 @@ void FootExt::AmmoAutoConvertActions() TechnoExt::ConvertToType(pThis, pTypeExt->Ammo_AutoConvertType); } -void FootExt::HealthAutoConvertActions() +// Checks if vehicle can deploy into a building at its current location. If unit has no DeploysInto set returns noDeploysIntoDefaultValue (def = false) instead. +bool FootExt::CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue) { - const auto pTypeExt = this->TypeExtData; - - if (!pTypeExt->Convert_Health.isset()) - return; + if (!pThis) + return false; - const double min = pTypeExt->Convert_Health_AbovePercent; - const double max = pTypeExt->Convert_Health_BelowPercent; + BuildingTypeClass* pDeployType = nullptr; + if (auto const pUnit = abstract_cast(pThis)) + pDeployType = pUnit->Type->DeploysInto; - if (min < 0 && max < 0) - return; + if (!pDeployType) + { + if (auto const pType = TechnoTypeExt::Fetch(pThis->GetTechnoType())->Convert_Deploy.Get()) + pDeployType = abstract_cast(pType); + } - const auto pThis = this->OwnerObject(); + if (!pDeployType) + return noDeploysIntoDefaultValue; - if (TechnoExt::IsHealthInThreshold(pThis, min, max)) - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Health); + // The vanilla game used an inappropriate approach here, resulting in potential risk of desync. + // Now, through additional checks, we can directly exclude the unit who want to deploy. + return TechnoExt::CanCreateBuildingHere(pThis, pDeployType); } double FootExt::GetCurrentSpeedMultiplier(FootClass* pThis) diff --git a/src/Ext/Foot/Body.h b/src/Ext/Foot/Body.h index 24e320be0c..6495c57962 100644 --- a/src/Ext/Foot/Body.h +++ b/src/Ext/Foot/Body.h @@ -74,13 +74,14 @@ class FootExt : public TechnoExt void UpdateOnTunnelEnter(); void UpdateOnTunnelExit(); void UpdateTypeData(TechnoTypeClass* pCurrentType); - void HealthAutoConvertActions(); void AmmoAutoConvertActions(); void HandleTemporaryZeroSpeed(); virtual void LoadFromStream(PhobosStreamReader& Stm) override; virtual void SaveToStream(PhobosStreamWriter& Stm) override; + static bool CanDeployIntoBuilding(FootClass* pThis, bool noDeploysIntoDefaultValue = false); + private: template void Serialize(T& Stm); diff --git a/src/Ext/House/Hooks.cpp b/src/Ext/House/Hooks.cpp index 784b57d29e..048d2026ee 100644 --- a/src/Ext/House/Hooks.cpp +++ b/src/Ext/House/Hooks.cpp @@ -287,25 +287,22 @@ DEFINE_HOOK(0x7015C9, TechnoClass_Captured_UpdateTracking, 0x6) } } - if (pFoot) + if (humanAndComputer) { - if (humanAndComputer) + if (const auto pConvertTo = I_am_human + ? pTypeExt->Convert_HumanToComputer.Get() + : pTypeExt->Convert_ComputerToHuman.Get()) { - if (const auto pConvertTo = I_am_human - ? pTypeExt->Convert_HumanToComputer.Get() - : pTypeExt->Convert_ComputerToHuman.Get()) - { - TechnoExt::ConvertToType(pFoot, pConvertTo); - } - - if (!I_am_human) - TechnoExt::ChangeOwnerMissionFix(pFoot, pType); + TechnoExt::ConvertToType(pThis, pConvertTo); } - pThis->Owner->RecheckTechTree = true; - pNewOwner->RecheckTechTree = true; + if (pFoot && !I_am_human) + TechnoExt::ChangeOwnerMissionFix(pFoot, pType); } + pThis->Owner->RecheckTechTree = true; + pNewOwner->RecheckTechTree = true; + for (const auto& pTrail : pExt->LaserTrails) { if (pTrail->Type->IsHouseColor) diff --git a/src/Ext/Infantry/Body.cpp b/src/Ext/Infantry/Body.cpp index 7bd1131ee4..6924a429db 100644 --- a/src/Ext/Infantry/Body.cpp +++ b/src/Ext/Infantry/Body.cpp @@ -1,5 +1,6 @@ #include "Body.h" +#include #include InfantryExt::ExtContainer InfantryExt::ExtMap; @@ -40,6 +41,125 @@ CoordStruct InfantryExt::GetSimpleFLH(InfantryClass* pThis, int weaponIndex, boo return FLH; } +// A conversion into something that is not an infantry has to happen on the centre of the +// infantry's cell, but an infantry stands on a sub-position of it. Starts the walk there and +// remembers the conversion; returns true while the caller has to wait for it to finish +// (UpdatePendingConvert does that once the infantry has arrived). +// +// MoveTo cannot do the walk. The walk locomotor's AI (0x75AEC0) asks the pathfinder for a +// route first (FootClass::UpdatePathfinding, 0x4D3920) and a destination inside the infantry's +// own cell yields none, so it falls through to a distance test (0x75B046). A sub-position -> +// centre hop is only ~90 leptons, well under the "already arrived" threshold at +// Rules+0x1718, so the AI calls foot->vt[0x480](0, 1) and stops: the order is silently +// dropped. +// +// ILocomotion::Force_Immediate_Destination is the generic route to that routine, and it is a +// plain ILocomotion method rather than a WalkLocomotionClass speciality: it sits at vtable +// slot 0x78 for every locomotor (identified by the slot's stack-cleanup arity matching +// `__stdcall (CoordStruct)`, i.e. RET 0x10). The other ten locomotors share a no-op stub +// (0x55AC00, a bare RET 0x10), while the walk locomotor overrides it at 0x75AE30 with a call +// to FUN_0075C240 - the same "resolve this cell's sub-position and set HeadToCoord" routine +// the AI reaches at 0x75BC13. +// +// Note that YRpp annotates this method with "Don't use this function." (Interfaces.h). Its +// behaviour here is exactly what is needed and it does not set IsMoving, relying on the AI +// picking HeadToCoord up on the next frame, so it is worth in-game verification rather than +// assuming. +bool InfantryExt::PrepareConvertToType(InfantryClass* pThis, TechnoTypeClass* pToType) +{ + if (!pThis->Locomotor || !pToType) + return false; + + if (!TechnoExt::CanConvertToType(pThis, pToType)) + return false; + + // Infantry to infantry keeps its sub-position: the new infantry simply takes over. + if (pToType->WhatAmI() == AbstractType::InfantryType) + return TechnoExt::ConvertToType(pThis, pToType); + + auto const pCell = MapClass::Instance.TryGetCellAt(pThis->GetCoords()); + + if (!pCell) + return false; + + auto const pExt = InfantryExt::Fetch(pThis); + + // Already walking there for a conversion that was ordered before this one; keep it, so the + // cell's flag is only raised once for one walk. + if (pExt->PendingConvertType) + return true; + + pExt->PendingConvertType = pToType; + pThis->PlayAnim(Sequence::Ready, true); + auto const center = pCell->GetCenterCoords(); + auto const pCellExt = CellExt::Fetch(pCell); + + // CellClass::FindInfantrySubposition only yields the centre while InfantryCenterAllowed is + // raised (see the hook in Ext/Cell/Hooks.cpp). It has to stay raised for the whole walk: + // the AI re-enters FUN_0075C240 every frame (0x75BE18, with the Empty sentinel, so the + // routine re-resolves the foot's current position) and would otherwise overwrite + // HeadToCoord with a corner again. ClearPendingConvertToTypeAtCenter lowers it again. + + pCellExt->InfantryCenterAllowed++; + pThis->Locomotor->Force_Immediate_Destination(center); + pCellExt->InfantryCenterAllowed--; + + return true; +} + +// Finishes a conversion that was deferred until the infantry reached the centre of its cell. +// The spot is checked again on arrival: the world may have changed while walking, and a +// conversion that cannot be done there fails instead of stacking two objects on one cell. +void InfantryExt::UpdatePendingConvert(InfantryClass* pThis) +{ + auto const pThisExt = InfantryExt::Fetch(pThis); + auto const pToType = pThisExt->PendingConvertType; + + if (!pToType) + return; + + // Still on the way. Force_Immediate_Destination does not set IsMoving, the locomotor does + // that itself when it picks HeadToCoord up, so this only waits for an actual walk. + if (pThis->Locomotor && pThis->Locomotor->Is_Moving()) + return; + + auto const current = pThis->GetCoords(); + auto const center = MapClass::Instance.TryGetCellAt(current)->GetCenterCoords(); + if (current != center) + return; + + const bool canConvert = InfantryExt::CanConvertAtCellCenter(pThis) + && TechnoExt::CanConvertToType(pThis, pToType); + + pThisExt->PendingConvertType = nullptr; + + if (canConvert) + { + TechnoExt::ConvertToType(pThis, pToType); + } + else if (pThis->Owner->IsCurrentPlayer() && (pThisExt->HasDeployConverted || pThisExt->HasUndeployConverted)) + { + VoxClass::PlayIndex(VoxClass::FindIndex(GameStrings::EVA_CannotDeployHere)); + } +} + +// Nobody else may stand on the spot the converted object needs. +bool InfantryExt::CanConvertAtCellCenter(InfantryClass* pThis) +{ + auto const pCell = MapClass::Instance.TryGetCellAt(pThis->GetCoords()); + + if (!pCell) + return false; + + for (auto pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) + { + if (pObject != pThis && (pObject->AbstractFlags & AbstractFlags::Techno)) + return false; + } + + return true; +} + // ============================= // load / save diff --git a/src/Ext/Infantry/Body.h b/src/Ext/Infantry/Body.h index fcd816b6e4..1eef255d11 100644 --- a/src/Ext/Infantry/Body.h +++ b/src/Ext/Infantry/Body.h @@ -15,11 +15,13 @@ class InfantryExt final : public FootExt bool SkipTargetChangeResetSequence; bool HasDeployConverted; bool HasUndeployConverted; + TechnoTypeClass* PendingConvertType; explicit InfantryExt(InfantryClass* const OwnerObject) : FootExt(OwnerObject) , SkipTargetChangeResetSequence { false } , HasDeployConverted { false } , HasUndeployConverted { false } + , PendingConvertType { nullptr } { } InfantryClass* OwnerObject() const @@ -34,6 +36,9 @@ class InfantryExt final : public FootExt } static CoordStruct GetSimpleFLH(InfantryClass* pThis, int weaponIndex, bool& FLHFound); + static bool PrepareConvertToType(InfantryClass* pThis, TechnoTypeClass* pToType); + static void UpdatePendingConvert(InfantryClass* pThis); + static bool CanConvertAtCellCenter(InfantryClass* pThis); class ExtContainer final : public Container { diff --git a/src/Ext/Infantry/Hooks.cpp b/src/Ext/Infantry/Hooks.cpp index 2d7642ed69..61f882be23 100644 --- a/src/Ext/Infantry/Hooks.cpp +++ b/src/Ext/Infantry/Hooks.cpp @@ -30,7 +30,7 @@ DEFINE_HOOK(0x51F738, InfantryClass_Mission_Unload_ConvertDeploy_NoSequence, 0x5 // instead: it forces Mission_Guard, so Mission_Unload cannot run a second time for one // deploy command. A build with the guard and both assignments removed behaved identically. - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Deploy); + InfantryExt::PrepareConvertToType(pThis, pTypeExt->Convert_Deploy); return FinishMission; } @@ -56,7 +56,7 @@ DEFINE_HOOK(0x520B3E, InfantryClass_DoingAI_DeployConvert_Deploy, 0x6) { pExt->HasDeployConverted = true; pExt->HasUndeployConverted = false; - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Deploy); + InfantryExt::PrepareConvertToType(pThis, pTypeExt->Convert_Deploy); } return 0; @@ -73,7 +73,7 @@ DEFINE_HOOK(0x520B99, InfantryClass_DoingAI_DeployConvert_Undeploy, 0x6) { pExt->HasUndeployConverted = true; pExt->HasDeployConverted = false; - TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Undeploy); + InfantryExt::PrepareConvertToType(pThis, pTypeExt->Convert_Undeploy); } return 0; diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 0103583af7..bbe9265620 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -221,6 +221,9 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->DestroyAnim_Random.Read(exINI, GameStrings::General, "DestroyAnim.Random"); this->UseDisguiseMovementSpeed.Read(exINI, GameStrings::General, "UseDisguiseMovementSpeed"); this->Convert_ResetMindControl.Read(exINI, GameStrings::General, "Convert.ResetMindControl"); + this->Convert_ShowBuildup.Read(exINI, GameStrings::General, "Convert.ShowBuildup"); + this->Convert_ShowBuildupFrom.Read(exINI, GameStrings::General, "Convert.ShowBuildupFrom"); + this->Convert_ShowBuildupTo.Read(exINI, GameStrings::General, "Convert.ShowBuildupTo"); this->BuildLimitGroup_ContentIfAnyMatch.Read(exINI, GameStrings::General, "BuildLimitGroup.ContentIfAnyMatch"); this->BuildLimitGroup_NotBuildableIfQueueMatch.Read(exINI, GameStrings::General, "BuildLimitGroup.NotBuildableIfQueueMatch"); this->DigitalDisplay_Health_FakeAtDisguise.Read(exINI, GameStrings::AudioVisual, "DigitalDisplay.Health.FakeAtDisguise"); @@ -845,6 +848,9 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->DestroyAnim_Random) .Process(this->UseDisguiseMovementSpeed) .Process(this->Convert_ResetMindControl) + .Process(this->Convert_ShowBuildup) + .Process(this->Convert_ShowBuildupFrom) + .Process(this->Convert_ShowBuildupTo) .Process(this->BuildLimitGroup_ContentIfAnyMatch) .Process(this->BuildLimitGroup_NotBuildableIfQueueMatch) .Process(this->ForceWeapon_InRange_TechnoOnly) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index f8f9dfdc0f..8deea82210 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -127,6 +127,9 @@ class RulesExt Valueable DestroyAnim_Random; Valueable UseDisguiseMovementSpeed; Valueable Convert_ResetMindControl; + Valueable Convert_ShowBuildup; + Valueable Convert_ShowBuildupFrom; + Valueable Convert_ShowBuildupTo; Valueable BuildLimitGroup_ContentIfAnyMatch; Valueable BuildLimitGroup_NotBuildableIfQueueMatch; Valueable DigitalDisplay_Health_FakeAtDisguise; @@ -673,6 +676,9 @@ class RulesExt , DestroyAnim_Random { true } , UseDisguiseMovementSpeed { false } , Convert_ResetMindControl { false } + , Convert_ShowBuildup { false } + , Convert_ShowBuildupFrom { false } + , Convert_ShowBuildupTo { false } , BuildLimitGroup_ContentIfAnyMatch { false } , BuildLimitGroup_NotBuildableIfQueueMatch { false } , DigitalDisplay_Health_FakeAtDisguise { true } diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 4f75d9cf08..dca605efd4 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,9 @@ void TechnoExt::OnEarlyUpdate() return; this->ApplyInterceptor(); + + if (auto const pInfantry = abstract_cast(this->OwnerObject())) + InfantryExt::UpdatePendingConvert(pInfantry); } void TechnoExt::ApplyInterceptor() diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index a675df62e8..7eba78e396 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -1,6 +1,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -333,19 +336,206 @@ bool TechnoExt::AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, Ta return true; } +// Can a building of the given type be created where pFromTechno currently stands? The source +// itself is excluded from the placement check through TechnoExt::Deployer, which +// CellClass::CanThisExistHere understands. +bool TechnoExt::CanCreateBuildingHere(TechnoClass* pFromTechno, BuildingTypeClass* pToBuildingType) +{ + if (!pFromTechno || !pToBuildingType) + return false; + + CellStruct location; + if (auto const pFromBuilding = abstract_cast(pFromTechno)) + { + auto const buildingCell = CellClass::Coord2Cell(pFromTechno->GetCoords()); + auto const tmpCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pFromBuilding->Type, buildingCell); + location = buildingCell + buildingCell - tmpCell; + } + else + { + location = CellClass::Coord2Cell(pFromTechno->GetCoords()); + } + + location = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pToBuildingType, location); + + auto const pPreviousDeployer = TechnoExt::Deployer; + TechnoExt::Deployer = pFromTechno; + const bool canCreate = pToBuildingType->CanCreateHere(location, pFromTechno->Owner); + TechnoExt::Deployer = pPreviousDeployer; + + return canCreate; +} + +// First of the two conversion steps: may pFromTechno be replaced by pToType right now? +// Unlike ConvertToType this has no side effects, so callers that only need to know (deploy +// checks, the infantry walk to the cell centre) can ask without changing anything. +bool TechnoExt::CanConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) +{ + if (!pFromTechno || !pToType || pFromTechno->GetTechnoType() == pToType) + return false; + + // A building that is busy with its own mission (building up, selling, ...) has no state + // to hand over yet. + if (auto const pFromBuilding = abstract_cast(pFromTechno)) + { + if (pFromBuilding->CurrentMission == Mission::None) + return false; + } + + // A building target needs its whole foundation free where the source stands, everything + // else is created on the spot the source already occupies. + if (auto const pToBuildingType = abstract_cast(pToType)) + return TechnoExt::CanCreateBuildingHere(pFromTechno, pToBuildingType); + + return true; +} + +// Second step: performs the conversion. Every caller is expected to have asked +// CanConvertToType first, but it is checked again here so nothing can bypass it. +static bool XConvertToType(TechnoClass* pFromTechno, TechnoTypeClass* pToType) +{ + auto const pToTechno = static_cast(pToType->CreateObject(pFromTechno->Owner)); + if (!pToTechno) + return false; + + auto const wasSelected = pFromTechno->IsSelected; + + TechnoExt::TransferStatus(pFromTechno, pToTechno); + + pToTechno->SetTarget(pFromTechno->Target); + + auto direction = pFromTechno->PrimaryFacing.Current().GetDir(); + auto location = pFromTechno->GetCoords(); + auto const pFromFoot = abstract_cast(pFromTechno); + auto const pFromBuilding = abstract_cast(pFromTechno); + auto const pToBuilding = abstract_cast(pToTechno); + auto const pToInfantry = abstract_cast(pToTechno); + if (pFromBuilding) + { + auto const buildingCell = CellClass::Coord2Cell(location); + auto const tmpCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pFromBuilding->Type, buildingCell); + location = CellClass::Cell2Coord(buildingCell + buildingCell - tmpCell, location.Z); + + auto const pFromBuildingTypeExt = BuildingTypeExt::Fetch(pFromBuilding->Type); + direction = pFromBuildingTypeExt->Convert_Direction.Get(direction); + } + if (pToBuilding) + { + auto const deployCell = CellClass::Coord2Cell(location); + auto const buildingCell = BuildingTypeExt::GetBuildingTopLeftCellFromDeployCell(pToBuilding->Type, deployCell); + location = CellClass::Cell2Coord(buildingCell, location.Z); + } + + if (pFromFoot) + pToTechno->SetDestination(pFromFoot->Destination, true); + pToTechno->QueueMission(pFromTechno->CurrentMission, false); + + if (pFromBuilding && pFromBuilding->Occupants.Count > 0) + { + if (pToBuilding + && pToBuilding->Type->CanBeOccupied + && pToBuilding->Type->MaxNumberOccupants >= pFromBuilding->Occupants.Count) + { + pToBuilding->Occupants = pFromBuilding->Occupants; + } + else + { + pFromBuilding->Mission_Unload(); + } + } + + if (pFromFoot && pFromFoot->IsDeploying) + { + if (pToInfantry->Type->Sequence->GetSequence(Sequence::Deployed).CountFrames) + { + pToInfantry->PlayAnim(Sequence::Deployed, true); + } + else + { + pToInfantry->PlayAnim(Sequence::Ready, true); + } + } + + Unsorted::ScenarioInit++; + pToTechno->Unlimbo(location, direction); + Unsorted::ScenarioInit--; + + // Keep selection + if (wasSelected) + pToTechno->Select(); + + bool showBuildupFrom = false; + if (pFromBuilding && pFromBuilding->CanBeSold() && !pFromBuilding->IsStrange()) + { + auto const pFromBuildingExt = BuildingExt::Fetch(pFromBuilding); + showBuildupFrom = pFromBuildingExt->TypeExtData->Convert_ShowBuildupFrom.Get(RulesExt::Global()->Convert_ShowBuildupFrom) + || pFromBuildingExt->TypeExtData->Convert_ShowBuildup.Get(RulesExt::Global()->Convert_ShowBuildup); + } + + if (showBuildupFrom) + { + pFromBuilding->SetArchiveTarget(nullptr); + pFromBuilding->Sell(1); + } + else + { + pFromTechno->Limbo(); + pFromTechno->UnInit(); + } + + + if (!pToBuilding) + return true; + + auto const pToBuildingExt = BuildingExt::Fetch(pToBuilding); + bool showBuildupTo = false; + if (pToBuilding->Type->Buildup) + showBuildupTo = pToBuildingExt->TypeExtData->Convert_ShowBuildupTo.Get(RulesExt::Global()->Convert_ShowBuildupTo) + || pToBuildingExt->TypeExtData->Convert_ShowBuildup.Get(RulesExt::Global()->Convert_ShowBuildup); + + if (showBuildupTo) + { + pToBuilding->BeginMode(BStateType::Construction); + pToBuilding->QueueMission(Mission::Construction, false); + VoxClass::PlayAtPos(RulesClass::Instance->BuildingSlam, &location); + } + else + { + pToBuilding->BeginMode(BStateType::Idle); + pToBuilding->QueueMission(Mission::Guard, false); + pToBuilding->NextMission(); + } + + pToBuilding->Place(!pFromTechno->Owner->IsNeutral() || pFromTechno->Owner->IsCurrentPlayer()); + + return true; +} + // Feature for common usage : TechnoType conversion -- Trsdy // BTW, who said it was merely a Type pointer replacement and he could make a better one than Ares? -bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) +bool TechnoExt::ConvertToType(TechnoClass* pThisTechno, TechnoTypeClass* pToType) { - const auto pType = pThis->GetTechnoType(); + const auto pType = pThisTechno->GetTechnoType(); // It really should be at the beginning. - if (pType == pToType || pType->WhatAmI() != pToType->WhatAmI()) + if (pType == pToType) { - Debug::Log("Incompatible types between %s and %s\n", pThis->get_ID(), pToType->get_ID()); + Debug::Log("Incompatible types between %s and %s\n", pThisTechno->get_ID(), pToType->get_ID()); return false; } + if (!TechnoExt::CanConvertToType(pThisTechno, pToType)) + return false; + + const auto absType = pType->WhatAmI(); + + if (absType != pToType->WhatAmI() || absType == AbstractType::BuildingType) + return XConvertToType(pThisTechno, pToType); + + auto const pThis = abstract_cast(pThisTechno); + if (!pThis) + return false; + if (AresFunctions::ConvertTypeTo) { if (AresFunctions::ConvertTypeTo(pThis, pToType)) @@ -456,6 +646,127 @@ bool TechnoExt::ConvertToType(FootClass* pThis, TechnoTypeClass* pToType) return true; } +void TechnoExt::HealthAutoConvertActions() +{ + const auto pTypeExt = this->TypeExtData; + + if (!pTypeExt->Convert_Health.isset()) + return; + + const double min = pTypeExt->Convert_Health_AbovePercent; + const double max = pTypeExt->Convert_Health_BelowPercent; + + if (min < 0 && max < 0) + return; + + if (const auto pBuilding = abstract_cast(this->OwnerObject())) + { + if (BuildingTypeExt::Fetch(pBuilding->Type)->Convert_Health_AfterBuildup.Get()) + { + switch (pBuilding->CurrentMission) + { + case Mission::None: + case Mission::Construction: + case Mission::Selling: + return; + } + } + } + + const auto pThis = this->OwnerObject(); + + if (TechnoExt::IsHealthInThreshold(pThis, min, max)) + TechnoExt::ConvertToType(pThis, pTypeExt->Convert_Health); +} + +void TechnoExt::TransferStatus(TechnoClass* pFrom, TechnoClass* pTo) +{ + pTo->SetHealthPercentage(pFrom->GetHealthPercentage()); + pTo->Veterancy = pFrom->Veterancy; + TechnoExt::TransferMindControlOnDeploy(pFrom, pTo); + ShieldClass::SyncShieldToAnother(pFrom, pTo); + TechnoExt::SyncInvulnerability(pFrom, pTo); + AttachEffectClass::TransferAttachedEffects(pFrom, pTo); + pTo->ArmorMultiplier = pFrom->ArmorMultiplier; + pTo->FirepowerMultiplier = pFrom->FirepowerMultiplier; + if (auto const pFromFoot = abstract_cast(pFrom), + pToFoot = abstract_cast(pTo); + pFromFoot && pToFoot) + { + pToFoot->SpeedMultiplier = pFromFoot->SpeedMultiplier; + } + + if (pFrom->SlaveManager) + pFrom->SlaveManager->SetOwner(pTo); +} + +void TechnoExt::TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo) +{ + const auto pAnimType = pTechnoFrom->MindControlRingAnim + ? pTechnoFrom->MindControlRingAnim->Type + : TechnoExt::Fetch(pTechnoFrom)->MindControlRingAnimType; + + if (const auto pController = pTechnoFrom->MindControlledBy) + { + if (const auto pManager = pController->CaptureManager) + { + CaptureManagerExt::FreeUnit(pManager, pTechnoFrom, true); + + if (CaptureManagerExt::CaptureUnit(pManager, pTechnoTo, false, pAnimType, true)) + { + if (const auto pBld = abstract_cast(pTechnoTo)) + { + // Capturing the building after unlimbo before buildup has finished or even started appears to throw certain things off, + // Hopefully this is enough to fix most of it like anims playing prematurely etc. + pBld->ActuallyPlacedOnMap = false; + pBld->DestroyNthAnim(BuildingAnimSlot::All); + + pBld->BeginMode(BStateType::Construction); + pBld->QueueMission(Mission::Construction, false); + } + } + else + { + int nSound = pTechnoTo->GetTechnoType()->MindClearedSound; + if (nSound == -1) + nSound = RulesClass::Instance->MindClearedSound; + + if (nSound != -1) + VocClass::PlayIndexAtPos(nSound, pTechnoTo->Location); + } + } + } + else if (const auto phMC = pTechnoFrom->MindControlledByHouse) + { + pTechnoTo->MindControlledByHouse = phMC; + pTechnoFrom->MindControlledByHouse = nullptr; + } + else if (pTechnoFrom->MindControlledByAUnit) // Perma MC + { + pTechnoTo->MindControlledByAUnit = true; + + const auto pBuilding = abstract_cast(pTechnoTo); + CoordStruct location = pTechnoTo->GetCoords(); + + location.Z += pBuilding + ? pBuilding->Type->Height * Unsorted::LevelHeight + : pTechnoTo->GetTechnoType()->MindControlRingOffset; + + const auto pAnim = pAnimType + ? GameCreate(pAnimType, location, 0, 1) + : nullptr; + + if (pAnim) + { + if (pBuilding) + pAnim->ZAdjust = -1024; + + pTechnoTo->MindControlRingAnim = pAnim; + pAnim->SetOwnerObject(pTechnoTo); + } + } +} + bool TechnoExt::IsTypeImmune(TechnoClass* pThis, TechnoTypeClass* pType, TechnoClass* pSource) { if (!pSource || !pType->TypeImmune) @@ -764,7 +1075,7 @@ void TechnoExt::ClickedApproachObject(FootClass* pThis, ObjectClass* pObject) if (Unsorted::MoveFeedback) pThis->VoiceMove(); - EventExt event {}; + EventExt event { }; event.Type = EventTypeExt::ApproachObject; event.HouseIndex = static_cast(pThis->Owner->ArrayIndex); event.Frame = Unsorted::CurrentFrame; @@ -775,38 +1086,38 @@ void TechnoExt::ClickedApproachObject(FootClass* pThis, ObjectClass* pObject) bool TechnoExt::CanBeRecruitedFix(FootClass* pThis, HouseClass* pHouse) { - if (pThis->Team != nullptr || - !pThis->IsAlive || - pThis->Health <= 0 || - pThis->InLimbo || + if (pThis->Team != nullptr || + !pThis->IsAlive || + pThis->Health <= 0 || + pThis->InLimbo || pThis->IsSinking || - pThis->Owner != pHouse) - { - return false; - } - - if (!(pThis->RecruitableA && pThis->RecruitableB)) - { - return false; - } - - const Mission mission = pThis->GetCurrentMission(); - if (!MissionClass::IsRecruitableMission(mission)) - { - return false; - } - - if (pThis->ShouldEnterAbsorber || - pThis->ShouldEnterOccupiable || - pThis->ShouldGarrisonStructure || - pThis->DrainTarget != nullptr || - pThis->BunkerLinkedItem || - pThis->LocomotorSource != nullptr) - { - return false; - } - - return true; + pThis->Owner != pHouse) + { + return false; + } + + if (!(pThis->RecruitableA && pThis->RecruitableB)) + { + return false; + } + + const Mission mission = pThis->GetCurrentMission(); + if (!MissionClass::IsRecruitableMission(mission)) + { + return false; + } + + if (pThis->ShouldEnterAbsorber || + pThis->ShouldEnterOccupiable || + pThis->ShouldGarrisonStructure || + pThis->DrainTarget != nullptr || + pThis->BunkerLinkedItem || + pThis->LocomotorSource != nullptr) + { + return false; + } + + return true; } struct AresTechnoExt diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 56ee6e8214..263b8a83b9 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -189,6 +189,7 @@ class TechnoExt : public RadioExt, public Detach::Listener void UpdateTintValues(); void UpdateLastTargetCrd(); int GetSight(); + void HealthAutoConvertActions(); static bool CanReceiveEvent(TechnoClass* pThis, HouseClass* pHouse); @@ -217,6 +218,7 @@ class TechnoExt : public RadioExt, public Detach::Listener // deprecated stand-in for the pre-rework container of all TechnoClass extensions static inline CompatExtMap ExtMap {}; + static inline TechnoClass* Deployer { nullptr }; static bool LoadGlobals(PhobosStreamReader& Stm); static bool SaveGlobals(PhobosStreamWriter& Stm); @@ -250,7 +252,11 @@ class TechnoExt : public RadioExt, public Detach::Listener static CoordStruct PassengerKickOutLocation(TechnoClass* pThis, FootClass* pPassenger, int maxAttempts); static bool AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, TargetZoneScanType zoneScanType, WeaponTypeClass* pWeapon = nullptr, bool useZone = false, int zone = -1); static void UpdateAttachedAnimLayers(TechnoClass* pThis); - static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); + static bool CanConvertToType(TechnoClass* pThis, TechnoTypeClass* pToType); + static bool ConvertToType(TechnoClass* pThis, TechnoTypeClass* pToType); + static bool CanCreateBuildingHere(TechnoClass* pThis, BuildingTypeClass* pToBuildingType); + static void TransferStatus(TechnoClass* pFrom, TechnoClass* pTo); + static void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo); static bool IsTypeImmune(TechnoClass* pThis, TechnoTypeClass* pType, TechnoClass* pSource); static int GetTintColor(TechnoClass* pThis, bool invulnerability, bool airstrike, bool berserk); static int GetCustomTintColor(TechnoClass* pThis); diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index bd0b92429c..6e9f8d45f8 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -31,7 +31,9 @@ DEFINE_HOOK(0x6F9E50, TechnoClass_AI, 0x5) { GET(TechnoClass*, pThis, ECX); - TechnoExt::Fetch(pThis)->OnEarlyUpdate(); + auto const pExt = TechnoExt::Fetch(pThis); + pExt->OnEarlyUpdate(); + pExt->HealthAutoConvertActions(); return 0; } @@ -45,7 +47,6 @@ DEFINE_HOOK(0x4DA54E, FootClass_AI, 0x6) pExt->UpdateWarpInDelay(); pExt->UpdateTiberiumEater(); pExt->AmmoAutoConvertActions(); - pExt->HealthAutoConvertActions(); if (pExt->AttackMoveFollowerTempCount) pExt->AttackMoveFollowerTempCount--; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 0d9eda2f22..a3953dfdcb 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1257,6 +1257,9 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->Convert_Health_AbovePercent.Read(exINI, pSection, "Convert.Health.AbovePercent"); this->Convert_Health_BelowPercent.Read(exINI, pSection, "Convert.Health.BelowPercent"); this->Convert_Health.Read(exINI, pSection, "Convert.Health"); + this->Convert_ShowBuildup.Read(exINI, pSection, "Convert.ShowBuildup"); + this->Convert_ShowBuildupFrom.Read(exINI, pSection, "Convert.ShowBuildupFrom"); + this->Convert_ShowBuildupTo.Read(exINI, pSection, "Convert.ShowBuildupTo"); if (this->Convert_Health_AbovePercent > this->Convert_Health_BelowPercent) Debug::Log("[Developer warning][%s] Convert.Health.AbovePercent is greater than Convert.Health.BelowPercent, resulting in no conversion.\n", pSection); @@ -1837,6 +1840,9 @@ void TechnoTypeExt::Serialize(T& Stm) .Process(this->Convert_Health_AbovePercent) .Process(this->Convert_Health_BelowPercent) .Process(this->Convert_Health) + .Process(this->Convert_ShowBuildup) + .Process(this->Convert_ShowBuildupFrom) + .Process(this->Convert_ShowBuildupTo) .Process(this->ExitThroughRoof) .Process(this->PsychicDetectable) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index c23db119c9..e5729b4300 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -500,6 +500,9 @@ class TechnoTypeExt : public ObjectTypeExt Valueable Convert_Health_AbovePercent; Valueable Convert_Health_BelowPercent; Nullable Convert_Health; + Nullable Convert_ShowBuildup; + Nullable Convert_ShowBuildupFrom; + Nullable Convert_ShowBuildupTo; Nullable ExitThroughRoof; Valueable PsychicDetectable; @@ -926,6 +929,9 @@ class TechnoTypeExt : public ObjectTypeExt , Convert_Health_AbovePercent { -1.0 } , Convert_Health_BelowPercent { -1.0 } , Convert_Health {} + , Convert_ShowBuildup {} + , Convert_ShowBuildupFrom {} + , Convert_ShowBuildupTo {} , PsychicDetectable { true } diff --git a/src/Ext/Unit/Body.cpp b/src/Ext/Unit/Body.cpp index a9e0535e94..8951152b56 100644 --- a/src/Ext/Unit/Body.cpp +++ b/src/Ext/Unit/Body.cpp @@ -9,8 +9,6 @@ UnitExt::ExtContainer UnitExt::ExtMap; -UnitClass* UnitExt::Deployer = nullptr; - UnitExt::~UnitExt() { const auto pThis = this->OwnerObject(); @@ -303,31 +301,6 @@ bool UnitExt::SimpleDeployerAllowedToDeploy(UnitClass* pThis, bool defaultValue, return true; } -// Checks if vehicle can deploy into a building at its current location. If unit has no DeploysInto set returns noDeploysIntoDefaultValue (def = false) instead. -bool UnitExt::CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue) -{ - if (!pThis) - return false; - - auto const pDeployType = pThis->Type->DeploysInto; - - if (!pDeployType) - return noDeploysIntoDefaultValue; - - auto mapCoords = CellClass::Coord2Cell(pThis->GetCoords()); - - if (pDeployType->GetFoundationWidth() > 2 || pDeployType->GetFoundationHeight(false) > 2) - mapCoords += CellStruct { -1, -1 }; - - // The vanilla game used an inappropriate approach here, resulting in potential risk of desync. - // Now, through additional checks, we can directly exclude the unit who want to deploy. - UnitExt::Deployer = pThis; - const bool canDeploy = pDeployType->CanCreateHere(mapCoords, pThis->Owner); - UnitExt::Deployer = nullptr; - - return canDeploy; -} - UnitTypeClass* UnitExt::GetUnitTypeExtra(UnitClass* pUnit, UnitTypeExt* pData) { if (pUnit->IsGreenHP()) diff --git a/src/Ext/Unit/Body.h b/src/Ext/Unit/Body.h index a57cbd49f4..27554bc8d0 100644 --- a/src/Ext/Unit/Body.h +++ b/src/Ext/Unit/Body.h @@ -46,12 +46,10 @@ class UnitExt final : public FootExt void UpdateRecoilData(); void RecordRecoilData(); - static UnitClass* Deployer; - + static bool CannotMove(UnitClass* pThis); static bool HasAmmoToDeploy(UnitClass* pThis); static void HandleOnDeployAmmoChange(UnitClass* pThis, int maxAmmoOverride = -1); static bool SimpleDeployerAllowedToDeploy(UnitClass* pThis, bool defaultValue, bool alwaysCheckLandTypes); - static bool CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue = false); static UnitTypeClass* GetUnitTypeExtra(UnitClass* pUnit, UnitTypeExt* pData); UnitClass* OwnerObject() const diff --git a/src/Ext/Unit/Hooks.DeployFire.cpp b/src/Ext/Unit/Hooks.DeployFire.cpp index 7e9bd4c1b8..ccd1ae98e3 100644 --- a/src/Ext/Unit/Hooks.DeployFire.cpp +++ b/src/Ext/Unit/Hooks.DeployFire.cpp @@ -28,6 +28,21 @@ DEFINE_HOOK(0x4C77E4, EventClass_Execute_DeployCommand, 0x6) } } + if (auto const pFoot = abstract_cast(pThis)) + { + const auto pExt = TechnoExt::Fetch(pThis); + if (auto const pConvertToType = pExt->TypeExtData->Convert_Deploy.Get(); + pConvertToType + && pConvertToType->WhatAmI() == AbstractType::BuildingType + && !FootExt::CanDeployIntoBuilding(pFoot, false)) + { + if (pThis->IsOwnedByCurrentPlayer) + VoxClass::PlayIndex(VoxClass::FindIndex(GameStrings::EVA_CannotDeployHere)); + + return DoNotExecute; + } + } + return 0; } diff --git a/src/Ext/Unit/Hooks.DeploysInto.cpp b/src/Ext/Unit/Hooks.DeploysInto.cpp index 9ee33c831d..6a513e7061 100644 --- a/src/Ext/Unit/Hooks.DeploysInto.cpp +++ b/src/Ext/Unit/Hooks.DeploysInto.cpp @@ -3,7 +3,6 @@ #include "Body.h" #include -#include #include #pragma region AllowDeployControlledMCV @@ -18,82 +17,12 @@ DEFINE_HOOK(0x700ED0, TechnoClass_AllowDeployControlledMCV, 0x6)// UnitClass::Ca #pragma endregion -static inline void TransferMindControlOnDeploy(TechnoClass* pTechnoFrom, TechnoClass* pTechnoTo) -{ - const auto pAnimType = pTechnoFrom->MindControlRingAnim - ? pTechnoFrom->MindControlRingAnim->Type - : TechnoExt::Fetch(pTechnoFrom)->MindControlRingAnimType; - - if (const auto Controller = pTechnoFrom->MindControlledBy) - { - if (const auto Manager = Controller->CaptureManager) - { - CaptureManagerExt::FreeUnit(Manager, pTechnoFrom, true); - - if (CaptureManagerExt::CaptureUnit(Manager, pTechnoTo, false, pAnimType, true)) - { - if (const auto pBld = abstract_cast(pTechnoTo)) - { - // Capturing the building after unlimbo before buildup has finished or even started appears to throw certain things off, - // Hopefully this is enough to fix most of it like anims playing prematurely etc. - pBld->ActuallyPlacedOnMap = false; - pBld->DestroyNthAnim(BuildingAnimSlot::All); - - pBld->BeginMode(BStateType::Construction); - pBld->QueueMission(Mission::Construction, false); - } - } - else - { - int nSound = pTechnoTo->GetTechnoType()->MindClearedSound; - if (nSound == -1) - nSound = RulesClass::Instance->MindClearedSound; - - if (nSound != -1) - VocClass::PlayIndexAtPos(nSound, pTechnoTo->Location); - } - } - } - else if (const auto MCHouse = pTechnoFrom->MindControlledByHouse) - { - pTechnoTo->MindControlledByHouse = MCHouse; - pTechnoFrom->MindControlledByHouse = nullptr; - } - else if (pTechnoFrom->MindControlledByAUnit) // Perma MC - { - pTechnoTo->MindControlledByAUnit = true; - - const auto pBuilding = abstract_cast(pTechnoTo); - CoordStruct location = pTechnoTo->GetCoords(); - - location.Z += pBuilding - ? pBuilding->Type->Height * Unsorted::LevelHeight - : pTechnoTo->GetTechnoType()->MindControlRingOffset; - - const auto pAnim = pAnimType - ? GameCreate(pAnimType, location, 0, 1) - : nullptr; - - if (pAnim) - { - if (pBuilding) - pAnim->ZAdjust = -1024; - - pTechnoTo->MindControlRingAnim = pAnim; - pAnim->SetOwnerObject(pTechnoTo); - } - } -} - DEFINE_HOOK(0x739956, UnitClass_Deploy_Transfer, 0x6) { GET(UnitClass*, pUnit, EBP); GET(BuildingClass*, pStructure, EBX); - TransferMindControlOnDeploy(pUnit, pStructure); - ShieldClass::SyncShieldToAnother(pUnit, pStructure); - TechnoExt::SyncInvulnerability(pUnit, pStructure); - AttachEffectClass::TransferAttachedEffects(pUnit, pStructure); + TechnoExt::TransferStatus(pUnit, pStructure); return 0; } @@ -103,10 +32,7 @@ DEFINE_HOOK(0x44A03C, BuildingClass_Mi_Selling_Transfer, 0x6) GET(BuildingClass*, pStructure, EBP); GET(UnitClass*, pUnit, EBX); - TransferMindControlOnDeploy(pStructure, pUnit); - ShieldClass::SyncShieldToAnother(pStructure, pUnit); - TechnoExt::SyncInvulnerability(pStructure, pUnit); - AttachEffectClass::TransferAttachedEffects(pStructure, pUnit); + TechnoExt::TransferStatus(pStructure, pUnit); // This line will break the bahavior of UnDeploysInto buildings. However, it might serve a purpose that no one knows yet // Comment out the line instead of removing it for now, so we can turn to it if something related goes wrong in the future @@ -129,6 +55,23 @@ DEFINE_HOOK(0x449E2E, BuildingClass_Mi_Selling_CreateUnit, 0x6) return 0x449E34; } +DEFINE_HOOK(0x7000DF, TechnoClass_WhatAction_Deploy, 0x5) +{ + GET(TechnoClass*, pThis, ESI); + + if (auto pInfantry = abstract_cast(pThis)) + { + if (!FootExt::CanDeployIntoBuilding(pInfantry, true)) + { + R->EAX(Action::NoDeploy); + + return 0x7000E4; + } + } + + return 0; +} + DEFINE_HOOK(0x7396AD, UnitClass_Deploy_CreateBuilding, 0x6) { GET(UnitClass*, pUnit, EBP); @@ -151,7 +94,7 @@ DEFINE_HOOK(0x73FEC1, UnitClass_WhatAction_DeploysIntoDesyncFix, 0x6) GET(UnitClass* const, pThis, ESI); REF_STACK(Action, action, STACK_OFFSET(0x20, 0x8)); - if (!UnitExt::CanDeployIntoBuilding(pThis)) + if (!FootExt::CanDeployIntoBuilding(pThis)) action = Action::NoDeploy; return SkipGameCode; diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp index 6195797c69..eaf2c9ea58 100644 --- a/src/Ext/WarheadType/Detonate.cpp +++ b/src/Ext/WarheadType/Detonate.cpp @@ -678,12 +678,7 @@ void WarheadTypeExt::InterceptBullets(TechnoClass* pOwner, BulletClass* pInterce void WarheadTypeExt::ApplyConvert(HouseClass* pHouse, TechnoClass* pTarget) { - const auto pTargetFoot = abstract_cast(pTarget); - - if (!pTargetFoot) - return; - - TypeConvertGroup::Convert(pTargetFoot, this->Convert_Pairs, pHouse); + TypeConvertGroup::Convert(pTarget, this->Convert_Pairs, pHouse); } void WarheadTypeExt::ApplyLocomotorInfliction(TechnoClass* pTarget) diff --git a/src/Interop/TechnoExt.cpp b/src/Interop/TechnoExt.cpp index 8881d0312f..4b7346f98b 100644 --- a/src/Interop/TechnoExt.cpp +++ b/src/Interop/TechnoExt.cpp @@ -5,7 +5,7 @@ std::vector TechnoExtInterop::CalculateExtraThreatCallbacks = {}; std::vector TechnoExtInterop::CalculateSightCallbacks = {}; -DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, FootClass* pThis, TechnoTypeClass* toType) +DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, TechnoClass* pThis, TechnoTypeClass* toType) { if (!pThis || !toType) return E_POINTER; diff --git a/src/Interop/TechnoExt.h b/src/Interop/TechnoExt.h index e163828197..6953a31f5f 100644 --- a/src/Interop/TechnoExt.h +++ b/src/Interop/TechnoExt.h @@ -31,10 +31,10 @@ class TechnoExtInterop /// /// Converts a unit to a different type. /// -/// Pointer to the FootClass instance to convert +/// Pointer to the TechnoClass instance to convert /// Pointer to the target TechnoTypeClass /// S_OK if conversion was successful, E_INVALIDARG if types are incompatible, E_FAIL if conversion failed -DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, FootClass* pThis, TechnoTypeClass* toType); +DEFINE_EXPORT(HRESULT, ConvertToType_Phobos, TechnoClass* pThis, TechnoTypeClass* toType); DEFINE_EXPORT(HRESULT, RegisterCalculateExtraThreatCallback, CalculateExtraThreatCallback callback); diff --git a/src/Misc/Hooks.Ares.cpp b/src/Misc/Hooks.Ares.cpp index 6cfa6f2595..7b9ab2fc6a 100644 --- a/src/Misc/Hooks.Ares.cpp +++ b/src/Misc/Hooks.Ares.cpp @@ -65,10 +65,7 @@ static void __fastcall LetGo(TemporalClass* pTemporal) static bool __stdcall ConvertToType(TechnoClass* pThis, TechnoTypeClass* pToType) { - if (const auto pFoot = abstract_cast(pThis)) - return TechnoExt::ConvertToType(pFoot, pToType); - - return false; + return TechnoExt::ConvertToType(pThis, pToType); } // Technically this replaces GetTechnoType() call. diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 099e91e5e8..27289814a6 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -780,7 +780,7 @@ DEFINE_HOOK(0x4D580B, FootClass_ApproachTarget_DeployToFire, 0x6) GET(UnitClass*, pThis, EBX); - R->EAX(UnitExt::CanDeployIntoBuilding(pThis, true)); + R->EAX(FootExt::CanDeployIntoBuilding(pThis, true)); return SkipGameCode; } @@ -791,7 +791,7 @@ DEFINE_HOOK(0x741050, UnitClass_CanFire_DeployToFire, 0x6) GET(UnitClass*, pThis, ESI); - if (pThis->Type->DeployToFire && pThis->CanDeployNow() && !UnitExt::CanDeployIntoBuilding(pThis, true)) + if (pThis->Type->DeployToFire && pThis->CanDeployNow() && !FootExt::CanDeployIntoBuilding(pThis, true)) return MustDeploy; return SkipGameCode; diff --git a/src/New/Type/Affiliated/TypeConvertGroup.cpp b/src/New/Type/Affiliated/TypeConvertGroup.cpp index 306dd23508..828ca75cbf 100644 --- a/src/New/Type/Affiliated/TypeConvertGroup.cpp +++ b/src/New/Type/Affiliated/TypeConvertGroup.cpp @@ -1,33 +1,33 @@ #include #include "TypeConvertGroup.h" -void TypeConvertGroup::Convert(FootClass* pTargetFoot, const std::vector& convertPairs, HouseClass* pOwner) +void TypeConvertGroup::Convert(TechnoClass* pTarget, const std::vector& convertPairs, HouseClass* pOwner) { for (const auto& [fromTypes, toType, affectedHouses] : convertPairs) { if (!toType.Get()) continue; - if (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTargetFoot->Owner)) + if (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTarget->Owner)) continue; if (fromTypes.size()) { - const auto pType = pTargetFoot->GetTechnoType(); + const auto pType = pTarget->GetTechnoType(); for (const auto& from : fromTypes) { // Check if the target matches upgrade-from TechnoType and it has something to upgrade to if (from == pType) { - TechnoExt::ConvertToType(pTargetFoot, toType); + TechnoExt::ConvertToType(pTarget, toType); goto end; // Breaking out of nested loops without extra checks one of the very few remaining valid usecases for goto, leave it be. } } } else { - TechnoExt::ConvertToType(pTargetFoot, toType); + TechnoExt::ConvertToType(pTarget, toType); break; } } @@ -57,20 +57,18 @@ void TypeConvertGroup::ConvertSW(const std::vector& convertPai for (const auto pTarget : items) { - const auto pTargetFoot = abstract_cast(pTarget); - - if (!pTargetFoot || (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTargetFoot->Owner))) + if (!pTarget || (pOwner && !EnumFunctions::CanTargetHouse(affectedHouses, pOwner, pTarget->Owner))) continue; - TechnoExt::ConvertToType(pTargetFoot, toType); + TechnoExt::ConvertToType(pTarget, toType); } } } else { - for (auto const pTargetFoot : FootClass::Array) + for (auto const pTarget : TechnoClass::Array) { - TypeConvertGroup::Convert(pTargetFoot, convertPairs, pOwner); + TypeConvertGroup::Convert(pTarget, convertPairs, pOwner); } } } diff --git a/src/New/Type/Affiliated/TypeConvertGroup.h b/src/New/Type/Affiliated/TypeConvertGroup.h index cc8178ef08..8ee636549b 100644 --- a/src/New/Type/Affiliated/TypeConvertGroup.h +++ b/src/New/Type/Affiliated/TypeConvertGroup.h @@ -15,7 +15,7 @@ class TypeConvertGroup bool Load(PhobosStreamReader& stm, bool registerForChange); bool Save(PhobosStreamWriter& stm) const; - static void Convert(FootClass* pTargetFoot, const std::vector& convertPairs, HouseClass* pOwner); + static void Convert(TechnoClass* pTarget, const std::vector& convertPairs, HouseClass* pOwner); static void ConvertSW(const std::vector& convertPairs, HouseClass* pOwner); private: