From a6f7676699acd4cbf577971f2feaead128c44c06 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sat, 3 Dec 2022 04:06:11 +0200 Subject: [PATCH] Implements TransformsInto for UnitTypes. --- src/extensions/house/houseext_hooks.cpp | 117 +++++++++ src/extensions/techno/technoext_hooks.cpp | 53 ++++ src/extensions/unit/unitext_hooks.cpp | 282 ++++++++++++++++++++++ src/extensions/unittype/unittypeext.cpp | 11 +- src/extensions/unittype/unittypeext.h | 10 + 5 files changed, 471 insertions(+), 2 deletions(-) diff --git a/src/extensions/house/houseext_hooks.cpp b/src/extensions/house/houseext_hooks.cpp index 534fe3130..71ceda52b 100644 --- a/src/extensions/house/houseext_hooks.cpp +++ b/src/extensions/house/houseext_hooks.cpp @@ -32,7 +32,12 @@ #include "house.h" #include "housetype.h" #include "technotype.h" +#include "buildingtype.h" +#include "techno.h" +#include "factory.h" #include "super.h" +#include "unittypeext.h" +#include "extension.h" #include "fatal.h" #include "debughandler.h" #include "asserthandler.h" @@ -154,6 +159,116 @@ DECLARE_PATCH(_HouseClass_Can_Build_BuildCheat_Patch) } +/** + * #issue-715 + * + * Gets the number of queued objects when determining whether a cameo + * should be disabled. + * + * Author: Rampastring + */ +int _HouseClass_ShouldDisableCameo_Get_Queued_Count(FactoryClass* factory, TechnoTypeClass* technotype) +{ + int count = factory->Total_Queued(*technotype); + TechnoClass* factoryobject = factory->Get_Object(); + + if (factoryobject == nullptr || count == 0) { + return 0; + } + + /** + * Check that the factory is trying to create the object that the player is trying to queue + * If not, we don't need to mess with the count + */ + if (factoryobject->Techno_Type_Class() != technotype) { + return count; + } + + /** + * #issue-715 + * + * If the object can transform into another object through our special logic, + * then check that doing so doesn't allow circumventing build limits + */ + if (technotype->What_Am_I() == RTTI_UNITTYPE) { + UnitTypeClass* unittype = reinterpret_cast(technotype); + UnitTypeClassExtension* unittypeext = Extension::Fetch(unittype); + + if (unittype->DeploysInto == nullptr && unittypeext->TransformsInto != nullptr) { + count += factory->House->UQuantity.Count_Of((UnitType)(unittypeext->TransformsInto->Get_Heap_ID())); + } + } + + return count; +} + + +/** + * #issue-715 + * + * Updates the build limit logic with unit queuing to + * take our unit transformation logic into account. + */ +DECLARE_PATCH(_HouseClass_ShouldDisableCameo_BuildLimit_Fix) +{ + GET_REGISTER_STATIC(FactoryClass*, factory, ecx); + GET_REGISTER_STATIC(TechnoTypeClass*, technotype, esi); + static int queuedcount; + + queuedcount = _HouseClass_ShouldDisableCameo_Get_Queued_Count(factory, technotype); + + _asm { mov eax, [queuedcount] } + JMP_REG(ecx, 0x004CB77D); +} + + +/** + * #issue-715 + * + * Take vehicles that can transform into other vehicles into acccount when + * determining whether a build limit has been met/exceeded. + * Otherwise these kinds of units could be used to bypass build limits + * (build a limited vehicle, transform it, now you can build another vehicle). + * + * Author: Rampastring + */ +DECLARE_PATCH(_HouseClass_Can_Build_BuildLimit_Handle_Vehicle_Transform) +{ + GET_REGISTER_STATIC(UnitTypeClass*, unittype, edi); + GET_REGISTER_STATIC(HouseClass*, house, ebp); + static UnitTypeClassExtension* unittypeext; + static int objectcount; + + unittypeext = Extension::Fetch(unittype); + + /** + * Stolen bytes / code. + */ + objectcount = house->UQuantity.Count_Of((UnitType)unittype->Get_Heap_ID()); + + /** + * Check whether this unit can deploy into a building. + * If it can, increment the object count by the number of buildings. + */ + if (unittype->DeploysInto != nullptr) { + objectcount += house->BQuantity.Count_Of((BuildingType)unittype->DeploysInto->Get_Heap_ID()); + } + else if (unittypeext->TransformsInto != nullptr) { + + /** + * This unit can transform into another unit, increment the object count + * by the number of transformed units. + */ + objectcount += house->UQuantity.Count_Of((UnitType)(unittypeext->TransformsInto->Get_Heap_ID())); + } + + _asm { mov esi, objectcount } + +continue_function: + JMP(0x004BC1B9); +} + + /** * Main function for patching the hooks. */ @@ -166,4 +281,6 @@ void HouseClassExtension_Hooks() Patch_Jump(0x004BBD26, &_HouseClass_Can_Build_BuildCheat_Patch); Patch_Jump(0x004BD30B, &_HouseClass_Super_Weapon_Handler_InstantRecharge_Patch); + Patch_Jump(0x004CB777, &_HouseClass_ShouldDisableCameo_BuildLimit_Fix); + Patch_Jump(0x004BC187, _HouseClass_Can_Build_BuildLimit_Handle_Vehicle_Transform); } diff --git a/src/extensions/techno/technoext_hooks.cpp b/src/extensions/techno/technoext_hooks.cpp index cf4afa7fa..2b335ee82 100644 --- a/src/extensions/techno/technoext_hooks.cpp +++ b/src/extensions/techno/technoext_hooks.cpp @@ -44,6 +44,8 @@ #include "infantry.h" #include "infantrytype.h" #include "infantrytypeext.h" +#include "unittype.h" +#include "unittypeext.h" #include "voc.h" #include "vinifera_util.h" #include "extension.h" @@ -684,6 +686,56 @@ DECLARE_PATCH(_TechnoClass_Null_House_Warning_Patch) } +/** + * #issue-356 + * + * Enables the deploy keyboard command to work for units that + * transform into a different unit on deploy. + * + * @author: Rampastring + */ +DECLARE_PATCH(_TechnoClass_2A0_Is_Allowed_To_Deploy_Unit_Transform_Patch) +{ + GET_REGISTER_STATIC(UnitTypeClass*, unittype, eax); + static UnitTypeClassExtension* unittypeext; + + /** + * Stolen bytes/code. + */ + if (unittype->DeploysInto != nullptr) { + goto has_deploy_ability; + + } else if (unittype->MaxPassengers > 0) { + goto has_deploy_ability; + + } else if (unittype->IsMobileEMP) { + goto has_deploy_ability; + } + + unittypeext = Extension::Fetch(unittype); + + if (unittypeext->TransformsInto != nullptr) { + goto has_deploy_ability; + } + + /** + * The unit has no ability that allows it to deploy / unload. + * Mark that and continue function after the check. + */ +has_no_deploy_ability: + _asm { mov eax, unittype } + JMP_REG(ecx, 0x006320E0); + + /** + * The unit has some kind of an ability that allows it to deploy / unload. + * Continue function after the check. + */ +has_deploy_ability: + _asm { mov eax, unittype } + JMP_REG(ecx, 0x006320E5); +} + + /** * Main function for patching the hooks. */ @@ -703,4 +755,5 @@ void TechnoClassExtension_Hooks() Patch_Jump(0x00630390, &_TechnoClass_Fire_At_Suicide_Patch); Patch_Jump(0x00631223, &_TechnoClass_Fire_At_Electric_Bolt_Patch); Patch_Jump(0x00636F09, &_TechnoClass_Is_Allowed_To_Retaliate_Can_Retaliate_Patch); + Patch_Jump(0x006320C2, &_TechnoClass_2A0_Is_Allowed_To_Deploy_Unit_Transform_Patch); } diff --git a/src/extensions/unit/unitext_hooks.cpp b/src/extensions/unit/unitext_hooks.cpp index 0e54d509f..e4c8794fe 100644 --- a/src/extensions/unit/unitext_hooks.cpp +++ b/src/extensions/unit/unitext_hooks.cpp @@ -36,6 +36,7 @@ #include "unit.h" #include "unittype.h" #include "unittypeext.h" +#include "tag.h" #include "target.h" #include "rules.h" #include "iomap.h" @@ -680,6 +681,284 @@ DECLARE_PATCH(_UnitClass_Per_Cell_Process_AutoHarvest_Assign_Harvest_Mission_Pat } +/** + * Helper function. + * Creates a unit based on an already existing unit. + * Returns the new unit if successful, otherwise null. + * + * @author: Rampastring + */ +UnitClass* Create_Transform_Unit(UnitClass* this_ptr) { + + UnitTypeClassExtension* unittypeext = Extension::Fetch(this_ptr->Class); + + UnitClass* newunit = reinterpret_cast(unittypeext->TransformsInto->Create_One_Of(this_ptr->House)); + if (newunit == nullptr) { + + /** + * Creating the new unit failed! Re-mark our occupation bits and return false. + */ + return nullptr; + } + + // Try_To_Deploy copies the tag this way at 0x0065112C + if (this_ptr->Tag != nullptr) { + newunit->Attach_Tag(this_ptr->Tag); + this_ptr->Tag->AttachCount--; + this_ptr->Tag = nullptr; + } + + newunit->ActLike = this_ptr->ActLike; + newunit->LimpetSpeedFactor = this_ptr->LimpetSpeedFactor; + newunit->field_214 = this_ptr->field_214; // also copied at 0x00650F4E + newunit->Veterancy.From_Integer(this_ptr->Veterancy.To_Integer()); + newunit->Group = this_ptr->Group; + newunit->BarrelFacing.Set(this_ptr->BarrelFacing.Current()); + newunit->BarrelFacing.Set_Desired(this_ptr->BarrelFacing.Desired()); + newunit->PrimaryFacing.Set(this_ptr->PrimaryFacing.Current()); + newunit->PrimaryFacing.Set_Desired(this_ptr->PrimaryFacing.Desired()); + newunit->SecondaryFacing.Set(this_ptr->SecondaryFacing.Current()); + newunit->SecondaryFacing.Set_Desired(this_ptr->SecondaryFacing.Desired()); + newunit->Strength = (int)(this_ptr->Health_Ratio() * (int)newunit->Class->MaxStrength); + newunit->ArmorBias = this_ptr->ArmorBias; + newunit->FirepowerBias = this_ptr->FirepowerBias; + newunit->SpeedBias = this_ptr->SpeedBias; + newunit->Coord = this_ptr->Coord; + newunit->EMPFramesRemaining = this_ptr->EMPFramesRemaining; + newunit->Ammo = this_ptr->Ammo; + + + if (newunit->Unlimbo(newunit->Coord, this_ptr->PrimaryFacing.Current().Get_Dir())) { + + /** + * Unlimbo successful, select our new unit and return it + */ + + if (PlayerPtr == newunit->Owning_House()) { + newunit->Select(); + } + + if (this_ptr->TarCom) { + newunit->Assign_Target(this_ptr->TarCom); + newunit->Assign_Mission(MISSION_ATTACK); + newunit->Commence(); + } + + return newunit; + } + + /** + * Unlimboing the new unit failed! Delete the new unit and return false. + */ + delete newunit; + return nullptr; +} + +/** + * Work-around function because the compiler likes smashing the stack by using ebp + * when calling locomotor functions :) + * + * Returns the address that the calling function should jump to after calling this. + * + * @author: Rampastring + */ +int _UnitClass_Try_To_Deploy_Transform_To_Vehicle_Patch_Func(UnitClass* this_ptr) { + + /** + * Stolen bytes/code. + */ + if (this_ptr->Class->DeploysInto != nullptr) { + + /** + * This unit is deployable rather than transformable, check whether it can deploy. + */ + goto original_code; + } + + UnitTypeClassExtension* unittypeext = Extension::Fetch(this_ptr->Class); + + if (unittypeext->TransformsInto != nullptr) { + + /** + * Use custom "transform to vehicle" logic if we don't need charge or we have enough of it. + */ + + if (unittypeext->IsTransformRequiresFullCharge && this_ptr->CurrentCharge < this_ptr->Class->MaxCharge) { + + /** + * We don't have enough charge, return false + */ + return 0x006511A0; + } + + this_ptr->Mark(MARK_UP); + this_ptr->Locomotor_Ptr()->Mark_All_Occupation_Bits(MARK_UP); + + UnitClass* newunit = Create_Transform_Unit(this_ptr); + + if (newunit != nullptr) { + + /** + * Creating transformed unit succeeded, erase the original unit and force function to return true + */ + return 0x0065114C; + } else { + + /** + * Creating transformed unit failed. Re-mark our occupation bits and return false. + */ + return 0x00651168; + } + } + + /** + * Continue to deployability check. + */ +original_code: + return 0x00650BC2; +} + + +/** + * #issue-715 + * + * Transforms a unit to another unit when a transformable unit deploys. + * + * @author: Rampastring + */ +DECLARE_PATCH(_UnitClass_Try_To_Deploy_Transform_To_Vehicle_Patch) { + GET_REGISTER_STATIC(UnitClass*, this_ptr, esi); + static int address; + address = _UnitClass_Try_To_Deploy_Transform_To_Vehicle_Patch_Func(this_ptr); + JMP(address); +} + + +/** + * #issue-715 + * + * Hack to display the the correct cursor for transformable units + * upon ACTION_SELF. + * + * @author: Rampastring + */ +DECLARE_PATCH(_UnitClass_What_Action_Self_Check_For_Vehicle_Transform_Patch) { + GET_REGISTER_STATIC(UnitClass*, this_ptr, esi); + static UnitTypeClass* unittype; + static UnitTypeClassExtension* unittypeext; + + unittype = this_ptr->Class; + unittypeext = Extension::Fetch(unittype); + + /** + * Stolen bytes/code. + */ + if (unittype->DeploysInto != nullptr) { + goto original_code; + } + + /** + * Check if this unit is able to transform into another unit. + * If not, we don't have anything else to do here. + */ + if (unittypeext->TransformsInto == nullptr) { + goto no_deploy_action; + } + + /** + * If this unit is able to transform to a different unit, check if it requires charge for it. + * If it does, then check whether we have enough charge. + */ + if (unittypeext->IsTransformRequiresFullCharge && this_ptr->CurrentCharge < this_ptr->Class->MaxCharge) { + + /** + * We don't have enough charge! + */ + goto return_no_deploy; + + } else if (this_ptr->entry_2A4()) { + + /** + * The unit is dying or under an EMP effect, don't allow it to transform. + */ + goto return_no_deploy; + } + + + /** + * We can transform and either don't need charge or we have enough of it, return ACTION_SELF + */ +return_self: + _asm { pop edi } + _asm { pop esi } + _asm { pop ebp } + _asm { pop ebx } + _asm { add esp, 10h } + _asm { mov eax, ACTION_SELF } + _asm { retn 8 } + + /** + * We don't have enough charge, return ACTION_NO_DEPLOY + */ +return_no_deploy: + _asm { pop edi } + _asm { pop esi } + _asm { pop ebp } + _asm { pop ebx } + _asm { add esp, 10h } + _asm { mov eax, ACTION_NO_DEPLOY } + _asm { retn 8 } + + + /** + * Check whether the unit can deploy. + */ +original_code: + JMP(0x0065602B); + + /** + * The unit can neither deploy nor transform. + * Continue function execution after the deployable check. + */ +no_deploy_action: + _asm{ mov eax, unittype } + JMP_REG(ecx, 0x00656344); +} + + +/** + * #issue-715 + * + * Check whether the unit is able to transform into another unit + * when performing the "Unload" mission. + * + * @author: Rampastring + */ +DECLARE_PATCH(_UnitClass_Mission_Unload_Transform_To_Vehicle_Patch) { + GET_REGISTER_STATIC(UnitTypeClass*, unittype, eax); + static UnitTypeClassExtension* unittypeext; + + /** + * Stolen bytes/code. + */ + if (unittype->IsToHarvest || unittype->IsToVeinHarvest) { +harvester_process: + JMP(0x006545A5); + } + + unittypeext = Extension::Fetch(unittype); + + if (unittype->DeploysInto != nullptr || unittypeext->TransformsInto != nullptr) { +deployable_process: + JMP(0x00654403); + } + +mobile_emp_process: + _asm { mov eax, unittype } + JMP_REG(edx, 0x006543FD); +} + + /** * Main function for patching the hooks. */ @@ -701,4 +980,7 @@ void UnitClassExtension_Hooks() Patch_Jump(0x0065665D, &_UnitClass_What_Action_ACTION_HARVEST_Block_On_Bridge_Patch); // IsToVeinHarvest //Patch_Jump(0x0065054F, &_UnitClass_Enter_Idle_Mode_Block_Harvesting_On_Bridge_Patch); // Removed, keeping code for reference. //Patch_Jump(0x00654AB0, &_UnitClass_Mission_Harvest_Block_Harvesting_On_Bridge_Patch); // Removed, keeping code for reference. + Patch_Jump(0x00650BAE, &_UnitClass_Try_To_Deploy_Transform_To_Vehicle_Patch); + Patch_Jump(0x00656017, &_UnitClass_What_Action_Self_Check_For_Vehicle_Transform_Patch); + Patch_Jump(0x006543DB, &_UnitClass_Mission_Unload_Transform_To_Vehicle_Patch); } diff --git a/src/extensions/unittype/unittypeext.cpp b/src/extensions/unittype/unittypeext.cpp index 65ada9019..ccdda8d57 100644 --- a/src/extensions/unittype/unittypeext.cpp +++ b/src/extensions/unittype/unittypeext.cpp @@ -30,6 +30,7 @@ #include "ccini.h" #include "tibsun_globals.h" #include "extension.h" +#include "vinifera_saveload.h" #include "asserthandler.h" #include "debughandler.h" @@ -45,7 +46,9 @@ UnitTypeClassExtension::UnitTypeClassExtension(const UnitTypeClass *this_ptr) : StartTurretFrame(-1), TurretFacings(32), // Must default to 32 as all Tiberian Sun units have 32 facings for turrets., StartIdleFrame(0), - IdleFrames(0) + IdleFrames(0), + TransformsInto(nullptr), + IsTransformRequiresFullCharge(false) { //if (this_ptr) EXT_DEBUG_TRACE("UnitTypeClassExtension::UnitTypeClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); @@ -113,6 +116,8 @@ HRESULT UnitTypeClassExtension::Load(IStream *pStm) new (this) UnitTypeClassExtension(NoInitClass()); + VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP(TransformsInto, "TransformsInto"); + return hr; } @@ -191,6 +196,8 @@ bool UnitTypeClassExtension::Read_INI(CCINIClass &ini) //} IsTotable = ini.Get_Bool(ini_name, "Totable", IsTotable); + TransformsInto = ini.Get_Unit(ini_name, "TransformsInto", TransformsInto); + IsTransformRequiresFullCharge = ini.Get_Bool(ini_name, "TransformRequiresFullCharge", IsTransformRequiresFullCharge); StartTurretFrame = ArtINI.Get_Int(graphic_name, "StartTurretFrame", StartTurretFrame); TurretFacings = ArtINI.Get_Int(graphic_name, "TurretFacings", TurretFacings); @@ -203,6 +210,6 @@ bool UnitTypeClassExtension::Read_INI(CCINIClass &ini) StartIdleFrame = ArtINI.Get_Int(graphic_name, "StartIdleFrame", StartIdleFrame); IdleFrames = ArtINI.Get_Int(graphic_name, "IdleFrames", IdleFrames); - + return true; } diff --git a/src/extensions/unittype/unittypeext.h b/src/extensions/unittype/unittypeext.h index 9fcf93919..1edfe8f5e 100644 --- a/src/extensions/unittype/unittypeext.h +++ b/src/extensions/unittype/unittypeext.h @@ -86,4 +86,14 @@ UnitTypeClassExtension final : public TechnoTypeClassExtension * The number of image frames for each of the idle animation sequences. */ unsigned IdleFrames; + + /** + * The unit type that this unit is meant to transform into upon deploying, if any. + */ + const UnitTypeClass *TransformsInto; + + /** + * If set, transforming to another unit will require this unit to have full charge. + */ + bool IsTransformRequiresFullCharge; };