From f30ce5670d39778ee5af9cf5b9aef2131f0fd051 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sat, 3 Dec 2022 17:33:18 +0200 Subject: [PATCH 1/2] Improves the harvester AI Resolves #201, #203 --- src/extensions/building/buildingext_hooks.cpp | 29 +++ src/extensions/foot/footext_hooks.cpp | 116 ++++++++++++ src/extensions/rules/rulesext.cpp | 5 +- src/extensions/rules/rulesext.h | 7 + src/extensions/unit/unitext.cpp | 13 +- src/extensions/unit/unitext.h | 9 + src/extensions/unit/unitext_hooks.cpp | 171 ++++++++++++++++++ 7 files changed, 348 insertions(+), 2 deletions(-) diff --git a/src/extensions/building/buildingext_hooks.cpp b/src/extensions/building/buildingext_hooks.cpp index 3a7f72dc6..6353aeccf 100644 --- a/src/extensions/building/buildingext_hooks.cpp +++ b/src/extensions/building/buildingext_hooks.cpp @@ -35,6 +35,8 @@ #include "building.h" #include "buildingtype.h" #include "buildingtypeext.h" +#include "unit.h"; +#include "unitext.h" #include "technotype.h" #include "technotypeext.h" #include "aircraft.h" @@ -954,6 +956,32 @@ DECLARE_PATCH(_BuildingClass_Captured_DontScore_Patch) JMP(0x0042F7BB); } +/** + * #issue-203 + * + * Assigns the last docked building of a spawned free unit on + * building placement complete (the "grand opening"). + * This allows harvesters to know which refinery they spawned from. + * + * @author: Rampastring + */ +DECLARE_PATCH(_BuildingClass_Grand_Opening_Assign_FreeUnit_LastDockedBuilding_Patch) +{ + GET_REGISTER_STATIC(BuildingClass*, this_ptr, esi); + GET_REGISTER_STATIC(UnitClass*, unit, edi); + static UnitClassExtension* unitext; + + unitext = Extension::Fetch(unit); + unitext->LastDockedBuilding = this_ptr; + + /** + * Continue the FreeUnit down-placing process. + */ + _asm { movsx eax, bp } + _asm { movsx ecx, bx } + JMP_REG(edx, 0x0042E5FB); +} + /** * Main function for patching the hooks. @@ -987,4 +1015,5 @@ void BuildingClassExtension_Hooks() Patch_Jump(0x00430F2B, &_BuildingClass_Mission_Deconstruction_Double_Survivors_Patch); Patch_Jump(0x0049436A, &_EventClass_Execute_Archive_Selling_Patch); Patch_Jump(0x0042F799, &_BuildingClass_Captured_DontScore_Patch); + Patch_Jump(0x0042E5F5, &_BuildingClass_Grand_Opening_Assign_FreeUnit_LastDockedBuilding_Patch); } diff --git a/src/extensions/foot/footext_hooks.cpp b/src/extensions/foot/footext_hooks.cpp index 685f7c21d..f27c895cc 100644 --- a/src/extensions/foot/footext_hooks.cpp +++ b/src/extensions/foot/footext_hooks.cpp @@ -36,9 +36,14 @@ #include "textprint.h" #include "clipline.h" #include "convert.h" +#include "house.h" #include "iomap.h" #include "rules.h" #include "rulesext.h" +#include "session.h" +#include "unit.h" +#include "unitext.h" +#include "unittype.h" #include "extension.h" #include "fatal.h" #include "asserthandler.h" @@ -64,6 +69,7 @@ class FootClassExt final : public FootClass void _Draw_Action_Line() const; void _Draw_NavComQueue_Lines() const; void _Death_Announcement(TechnoClass* source) const; + Cell _Search_For_Tiberium(int rad, bool a2); private: void _Draw_Line(Coordinate& start_coord, Coordinate& end_coord, bool is_dashed, bool is_thick, bool is_dropshadow, unsigned line_color, unsigned drop_color, int rate) const; @@ -361,6 +367,115 @@ void FootClassExt::_Draw_Action_Line() const } +/** + * #issue-203 + * + * Evaluates the value of Tiberium on a single cell. + * + * Author: Rampastring + */ +void _Vinifera_FootClass_Search_For_Tiberium_Check_Tiberium_Value_Of_Cell(FootClass* this_ptr, Cell& cell_coords, Cell* besttiberiumcell, int* besttiberiumvalue, UnitClassExtension* unitext) +{ + if (this_ptr->Tiberium_Check(cell_coords)) { + + CellClass* cell = &Map[cell_coords]; + int tiberiumvalue = cell->Get_Tiberium_Value(); + + /** + * #issue-203 + * + * Consider distance to refinery when selecting the next tiberium patch to harvest. + * Prefer the most resourceful tiberium patch, but if there's a tie, prefer one that's + * closer to our refinery. Original game only cares about the value. + * + * @author: Rampastring + */ + if (unitext && unitext->LastDockedBuilding && unitext->LastDockedBuilding->IsActive && !unitext->LastDockedBuilding->IsInLimbo) { + tiberiumvalue *= 100; + tiberiumvalue -= ::Distance(cell_coords, unitext->LastDockedBuilding->Get_Cell()); + } + + if (tiberiumvalue > *besttiberiumvalue) + { + *besttiberiumvalue = tiberiumvalue; + *besttiberiumcell = cell_coords; + } + } +} + + +/** + * #issue-203 + * + * Smarter replacement for the Search_For_Tiberium method. + * Makes harvesters consider the distance to their refinery when + * looking for the cell of tiberium to harvest. + * + * Author: Rampastring + */ +Cell FootClassExt::_Search_For_Tiberium(int rad, bool a2) +{ + if (!Owning_House()->Is_Human_Control() && + What_Am_I() == RTTI_UNIT && + ((UnitClass*)this)->Class->IsToHarvest && + a2 && + Session.Type != GAME_NORMAL) + { + /** + * Use weighted tiberium-seeking algorithm for AI in multiplayer. + */ + + return Search_For_Tiberium_Weighted(rad); + } + + Coordinate center_coord = Center_Coord(); + Cell cell_coords = Coord_Cell(center_coord); + Cell unit_cell_coords = cell_coords; + + if (Map[unit_cell_coords].Land_Type() == LAND_TIBERIUM) { + + /** + * If we're already standing on tiberium, then we don't need to move anywhere. + */ + + return unit_cell_coords; + } + + int besttiberiumvalue = -1; + Cell besttiberiumcell = Cell(0, 0); + + UnitClassExtension* unitext = nullptr; + if (What_Am_I() == RTTI_UNIT) { + unitext = Extension::Fetch(this); + } + + /** + * Perform a ring search outward from the center. + */ + for (int radius = 1; radius < rad; radius++) { + for (int x = -radius; x <= radius; x++) { + + cell_coords = Cell(unit_cell_coords.X + x, unit_cell_coords.Y - radius); + _Vinifera_FootClass_Search_For_Tiberium_Check_Tiberium_Value_Of_Cell(this, cell_coords, &besttiberiumcell, &besttiberiumvalue, unitext); + + cell_coords = Cell(unit_cell_coords.X + x, unit_cell_coords.Y + radius); + _Vinifera_FootClass_Search_For_Tiberium_Check_Tiberium_Value_Of_Cell(this, cell_coords, &besttiberiumcell, &besttiberiumvalue, unitext); + + cell_coords = Cell(unit_cell_coords.X - radius, unit_cell_coords.Y + x); + _Vinifera_FootClass_Search_For_Tiberium_Check_Tiberium_Value_Of_Cell(this, cell_coords, &besttiberiumcell, &besttiberiumvalue, unitext); + + cell_coords = Cell(unit_cell_coords.X + radius, unit_cell_coords.Y + x); + _Vinifera_FootClass_Search_For_Tiberium_Check_Tiberium_Value_Of_Cell(this, cell_coords, &besttiberiumcell, &besttiberiumvalue, unitext); + } + + if (besttiberiumvalue != -1) + break; + } + + return besttiberiumcell; +} + + /** * #issue-593 * @@ -578,4 +693,5 @@ void FootClassExtension_Hooks() Patch_Jump(0x004A102F, &_FootClass_Mission_Move_Can_Passive_Acquire_Patch); Patch_Jump(0x004A6A40, &FootClassExt::_Draw_Action_Line); Patch_Jump(0x004A4D60, &FootClassExt::_Death_Announcement); + Patch_Jump(0x004A76F0, &FootClassExt::_Search_For_Tiberium); } diff --git a/src/extensions/rules/rulesext.cpp b/src/extensions/rules/rulesext.cpp index a0039a2d8..ca4e14b95 100644 --- a/src/extensions/rules/rulesext.cpp +++ b/src/extensions/rules/rulesext.cpp @@ -81,7 +81,8 @@ RulesClassExtension::RulesClassExtension(const RulesClass *this_ptr) : IsBuildOffAlly(true), IsShowSuperWeaponTimers(true), IceStrength(0), - WeedPipIndex(1) + WeedPipIndex(1), + MaxFreeRefineryDistanceBias(16) { //if (this_ptr) EXT_DEBUG_TRACE("RulesClassExtension::RulesClassExtension - 0x%08X\n", (uintptr_t)(ThisPtr)); @@ -211,6 +212,7 @@ void RulesClassExtension::Compute_CRC(WWCRCEngine &crc) const crc(IsBuildOffAlly); crc(IsShowSuperWeaponTimers); crc(IceStrength); + crc(MaxFreeRefineryDistanceBias); } @@ -605,6 +607,7 @@ bool RulesClassExtension::General(CCINIClass &ini) * @author: CCHyper */ This()->EngineerDamage = ini.Get_Float(GENERAL, "EngineerDamage", This()->EngineerDamage); + MaxFreeRefineryDistanceBias = ini.Get_Int(GENERAL, "MaxFreeRefineryDistanceBias", MaxFreeRefineryDistanceBias); return true; } diff --git a/src/extensions/rules/rulesext.h b/src/extensions/rules/rulesext.h index 10b511a92..a82e9ec90 100644 --- a/src/extensions/rules/rulesext.h +++ b/src/extensions/rules/rulesext.h @@ -110,4 +110,11 @@ class RulesClassExtension final : public GlobalExtensionClass * Customizable maximum counts for drawing different pips. */ TypeList MaxPips; + + /** + * When looking for refineries, harvesters will prefer a distant free + * refinery over a closer occupied refinery if the refineries' distance + * difference in cells is less than this. + */ + int MaxFreeRefineryDistanceBias; }; diff --git a/src/extensions/unit/unitext.cpp b/src/extensions/unit/unitext.cpp index 276e79b45..326fe7b77 100644 --- a/src/extensions/unit/unitext.cpp +++ b/src/extensions/unit/unitext.cpp @@ -27,6 +27,8 @@ ******************************************************************************/ #include "unitext.h" #include "unit.h" +#include "building.h" +#include "vinifera_saveload.h" #include "wwcrc.h" #include "extension.h" #include "asserthandler.h" @@ -39,7 +41,8 @@ * @author: CCHyper */ UnitClassExtension::UnitClassExtension(const UnitClass *this_ptr) : - FootClassExtension(this_ptr) + FootClassExtension(this_ptr), + LastDockedBuilding(nullptr) { //if (this_ptr) EXT_DEBUG_TRACE("UnitClassExtension::UnitClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); @@ -107,6 +110,8 @@ HRESULT UnitClassExtension::Load(IStream *pStm) new (this) UnitClassExtension(NoInitClass()); + VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP(LastDockedBuilding, "LastDockedBuilding"); + return hr; } @@ -152,6 +157,10 @@ void UnitClassExtension::Detach(TARGET target, bool all) //EXT_DEBUG_TRACE("UnitClassExtension::Detach - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); FootClassExtension::Detach(target, all); + + if (LastDockedBuilding == target) { + LastDockedBuilding = nullptr; + } } @@ -163,4 +172,6 @@ void UnitClassExtension::Detach(TARGET target, bool all) void UnitClassExtension::Compute_CRC(WWCRCEngine &crc) const { //EXT_DEBUG_TRACE("UnitClassExtension::Compute_CRC - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); + + crc(LastDockedBuilding != nullptr ? LastDockedBuilding->Fetch_ID() : 0); } diff --git a/src/extensions/unit/unitext.h b/src/extensions/unit/unitext.h index 4142ec103..42f76e39a 100644 --- a/src/extensions/unit/unitext.h +++ b/src/extensions/unit/unitext.h @@ -29,6 +29,7 @@ #include "footext.h" #include "unit.h" +#include "building.h" class DECLSPEC_UUID(UUID_UNIT_EXTENSION) @@ -60,4 +61,12 @@ UnitClassExtension final : public FootClassExtension virtual RTTIType What_Am_I() const override { return RTTI_UNIT; } public: + /** + * #issue-203 + * + * The building that this unit last docked with. + * Used by harvesters for considering the distance to their last refinery + * when picking a tiberium cell to harvest from. + */ + BuildingClass *LastDockedBuilding; }; diff --git a/src/extensions/unit/unitext_hooks.cpp b/src/extensions/unit/unitext_hooks.cpp index e5700f891..369a12ba7 100644 --- a/src/extensions/unit/unitext_hooks.cpp +++ b/src/extensions/unit/unitext_hooks.cpp @@ -36,10 +36,13 @@ #include "technotypeext.h" #include "warheadtype.h" #include "unit.h" +#include "unitext.h" #include "unittype.h" #include "unittypeext.h" +#include "tag.h" #include "target.h" #include "rules.h" +#include "rulesext.h" #include "iomap.h" #include "infantry.h" #include "voc.h" @@ -1168,6 +1171,173 @@ DECLARE_PATCH(_UnitClass_Mission_Unload_Transform_To_Vehicle_Patch) { } +/** + * Finds the nearest docking bay for a specific unit. + * + * @author: Rampastring + */ +void UnitClassExtension_Find_Nearest_Refinery(UnitClass* this_ptr, BuildingClass** building_addr, int* distance_addr, bool include_reserved) +{ + int nearest_refinery_distance = INT_MAX; + BuildingClass* nearest_refinery = nullptr; + + /** + * Find_Docking_Bay looks also through occupied docking bays if ScenarioInit is set + */ + if (include_reserved) { + ScenarioInit++; + } + + for (int i = 0; i < this_ptr->Class->Dock.Count(); i++) { + BuildingTypeClass* dockbuildingtype = this_ptr->Class->Dock[i]; + + BuildingClass* dockbuilding = this_ptr->Find_Docking_Bay(dockbuildingtype, false, false); + if (dockbuilding == nullptr) + continue; + + int distance = this_ptr->Distance(dockbuilding); + + if (distance < nearest_refinery_distance) { + nearest_refinery_distance = distance; + nearest_refinery = dockbuilding; + } + } + + if (include_reserved) { + ScenarioInit--; + } + + *building_addr = nearest_refinery; + *distance_addr = nearest_refinery_distance; +} + + +/** + * #issue-201 + * + * A "quality of life" patch for harvesters so they don't discriminate against dock + * buildings that are not the first on their Dock= list. Also makes harvesters + * smarter by making them prefer queuing for nearby occupied refineries instead + * of wandering to distant free refineries. + * + * @author: Rampastring + */ +DECLARE_PATCH(_UnitClass_Mission_Harvest_FINDHOME_Find_Nearest_Refinery_Patch) +{ + /** + * Enum for MISSION_HARVEST status constants. + */ + enum { + LOOKING, + HARVESTING, + FINDHOME, + HEADINGHOME, + GOINGTOIDLE, + }; + + + GET_REGISTER_STATIC(UnitClass*, harvester, esi); + static RadioMessageType response; + static UnitClassExtension* unitext; + static int free_refinery_distance_bias; + static BuildingClass* nearest_free_refinery; + static int nearest_free_refinery_distance; + static BuildingClass* nearest_possibly_occupied_refinery; + static int nearest_possibly_occupied_refinery_distance; + static bool reserve_free_refinery; + + /** + * Find the nearest refinery that is not occupied. + */ + UnitClassExtension_Find_Nearest_Refinery(harvester, &nearest_free_refinery, &nearest_free_refinery_distance, false); + + /** + * Find the nearest refinery, regardless of whether it's occupied. + */ + UnitClassExtension_Find_Nearest_Refinery(harvester, &nearest_possibly_occupied_refinery, &nearest_possibly_occupied_refinery_distance, true); + + reserve_free_refinery = true; + + if (nearest_free_refinery == nullptr) { + + /** + * There was no free refinery, check if there was an occupied one. + */ + if (nearest_possibly_occupied_refinery == nullptr) { + + /** + * No refinery existed at all! We have nothing to do here. + */ + goto set_mission_delay_and_return; + } + + /** + * There was an occupied refinery, queue for it instead. + */ + reserve_free_refinery = false; + } + else if (nearest_free_refinery != nearest_possibly_occupied_refinery) { + + /** + * There was a free refinery as well as an occupied one. + * Check if the occupied refinery is significantly closer to us than the free refinery. + */ + + free_refinery_distance_bias = RuleExtension->MaxFreeRefineryDistanceBias; + + if (nearest_free_refinery_distance > + nearest_possibly_occupied_refinery_distance + Cell_To_Lepton(free_refinery_distance_bias)) { + + reserve_free_refinery = false; + } + } + + unitext = Extension::Fetch(harvester); + + if (reserve_free_refinery) { + + /** + * We want to contact the free refinery, send a radio message to it. + */ + response = harvester->Transmit_Message(RADIO_HELLO, nearest_free_refinery); + + /** + * Check if the refinery answered as expected. If not, we'll queue for it instead. + */ + if (response == RADIO_ROGER) { + + /** + * The refinery accepted us! Change mission status to HEADINGHOME and jump to original code. + */ + harvester->Status = HEADINGHOME; + + unitext->LastDockedBuilding = nearest_free_refinery; + + goto set_mission_delay_and_return; + } + } + + + /** + * Re-use the original game's code for queueing to an occupied refinery. + * The game expects the occupied refinery pointer to be in edi. + */ +queue_to_occupied: + + unitext->LastDockedBuilding = nearest_possibly_occupied_refinery; + + _asm { mov edi, [nearest_possibly_occupied_refinery] }; + JMP(0x00654FAA); + + + /** + * Set mission delay and return from function. + */ +set_mission_delay_and_return: + JMP(0x00655226); +} + + /** * Main function for patching the hooks. */ @@ -1193,6 +1363,7 @@ void UnitClassExtension_Hooks() Patch_Jump(0x006543DB, &_UnitClass_Mission_Unload_Transform_To_Vehicle_Patch); Patch_Jump(0x0064E920, &UnitClassExt::_Firing_AI); Patch_Jump(0x006527B1, &_UnitClass_Draw_Voxel_Patch); + Patch_Jump(0x00654EEE, &_UnitClass_Mission_Harvest_FINDHOME_Find_Nearest_Refinery_Patch); //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. } From d9758c001b56983b6dde434d5d4947690f5e9ae3 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 20 Oct 2024 17:02:02 +0300 Subject: [PATCH 2/2] Update docs --- CREDITS.md | 2 ++ docs/New-Features-and-Enhancements.md | 13 +++++++++++++ docs/Whats-New.md | 2 ++ 3 files changed, 17 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index 94d5000b9..73184f622 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -157,6 +157,8 @@ This page lists all the individual contributions to the project by their author. - Fix a bug where house firepower bonus, veterancy and crate upgrade damage modifiers were not applied to railgun `AmbientDamage=`. - Implement `FilterFromBandBoxSelection`. - Add the possibility to customize the UI and Tooltip colors per-side. + - Harvesters' refinery-seeking algorithm now considers both free and occupied refineries when figuring out which refinery to unload at (by Rampastring) + - Harvesters now consider distance to refinery when moving from one Tiberium patch to another (by Rampastring) - **secsome**: - Add support for up to 32767 waypoints to be used in scenarios. - **ZivDero**: diff --git a/docs/New-Features-and-Enhancements.md b/docs/New-Features-and-Enhancements.md index d163bc8f3..0da6df50a 100644 --- a/docs/New-Features-and-Enhancements.md +++ b/docs/New-Features-and-Enhancements.md @@ -88,6 +88,19 @@ EngineerChance=0 ; integer (%), what is the chance that an engineer will exit t It is not recommended to set `EngineerChance=100`, as this may put the game into an infinite loop when it insists an infantry other than an engineer exits the building. ``` +## Harvesters + +- In the original game, harvesters always prefer free refineries over occupied ones, even if the free refinery was much farther away than the occupied refinery. Vinifera fixes this so that harvesters now prefer queueing to occupied refineries if they are much closer than free refineries. The distance for this preference is customizable. + +In `RULES.INI`: +``` +[General] +; When looking for refineries, harvesters will prefer a distant free +; refinery over a closer occupied refinery if the refineries' distance +; difference in cells is less than this. +MaxFreeRefineryDistanceBias=16 +``` + ## Ice - Ice strength can now be customized. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 97f16429e..9a111fe03 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -152,6 +152,8 @@ New: - Add per-side crew customization (by ZivDero) - Reimplement aircraft carriers and missile launchers from Red Alert 2 (by ZivDero) - Implement `DontScore` (by ZivDero) +- Harvesters' refinery-seeking algorithm now considers both free and occupied refineries when figuring out which refinery to unload at (by Rampastring) +- Harvesters now consider distance to refinery when moving from one Tiberium patch to another (by Rampastring) Vanilla fixes: