From a307ea7fa3730e6036a38e03580de17455d6ab21 Mon Sep 17 00:00:00 2001 From: Kirill Andriiashin Date: Fri, 18 Oct 2024 01:04:44 +0300 Subject: [PATCH] Add band box selection filtering (#1106) Co-authored-by: Rampastring --- CREDITS.md | 2 + docs/New-Features-and-Enhancements.md | 21 ++ docs/Whats-New.md | 1 + src/extensions/options/optionsext.cpp | 4 +- src/extensions/options/optionsext.h | 5 + src/extensions/sidebar/sidebarext_hooks.cpp | 4 +- src/extensions/tactical/tacticalext_hooks.cpp | 238 ++++++++++++++++++ src/extensions/technotype/technotypeext.cpp | 8 +- src/extensions/technotype/technotypeext.h | 8 +- 9 files changed, 284 insertions(+), 7 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index e6507a134..ed1337454 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -155,6 +155,7 @@ This page lists all the individual contributions to the project by their author. - Fix a bug where a vehicle transport could end up attached to its own cargo, causing the transport to disappear upon unloading. - Fix a bug where a harvester could be ordered to dock with a refinery that wasn't listed in the harvester's `Dock=` key. - Fix a bug where house firepower bonus, veterancy and crate upgrade damage modifiers were not applied to railgun `AmbientDamage=`. + - Implement `FilterFromBandBoxSelection`. - **secsome**: - Add support for up to 32767 waypoints to be used in scenarios. - **ZivDero**: @@ -179,4 +180,5 @@ This page lists all the individual contributions to the project by their author. - Make it so that it is no longer required to list all Tiberiums in a map to override some Tiberium's properties. - Add `PipWrap`. - Adjustments to the band box, action line, target laser and NavCom queue line customization features. + - Implement `FilterFromBandBoxSelection`. diff --git a/docs/New-Features-and-Enhancements.md b/docs/New-Features-and-Enhancements.md index 061d75af9..ef7ec4210 100644 --- a/docs/New-Features-and-Enhancements.md +++ b/docs/New-Features-and-Enhancements.md @@ -354,6 +354,27 @@ PipWrap=0 ; integer, the number of ammo pips to draw using pip wrap. For `PipWrap` to function, new pips need to be added to `pips2.shp`. The pip at index 7 (1-based) is still used by ammo when `PipWrap=0`, pips starting from index 8 are used by `PipWrap`. ``` +### Selection Filtering + +- Vinifera adds the ability to exclude some TechnoTypes from band selection. + +In `RULES.INI`: +```ini +[SOMETECHNO] ; TechnoType +FilterFromBandBoxSelection=no ; boolean, should this Techno be excluded from band box selection when it contains units without this flag? +``` + +- Technos with `FilterFromBandBoxSelection=yes` will only be selected if the current selection contains any units with `FilterFromBandBoxSelection=yes`, or the player is making a new selection and only Technos with `FilterFromBandBoxSelection=yes` are in the selection box. +- By holding `ALT` it is possible to temporarily ignore this logic and select all types of objects. + +- It is also possible to disable this behavior in `SUN.INI`. + +In `SUN.INI`: +```ini +[Options] +FilterBandBoxSelection=yes ; boolean, should the band box selection be filtered? +``` + ## Terrain ### Light Sources diff --git a/docs/Whats-New.md b/docs/Whats-New.md index b91284776..eb0faacd3 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -147,6 +147,7 @@ New: - Implement various controls to customise action lines (by CCHyper/tomsons26) - Implement various controls to customise target lasers line (by CCHyper/tomsons26) - Implement various controls to show and customise NavCom queue lines (by CCHyper/tomsons26) +- Implement `FilterFromBandBoxSelection` (by ZivDero/Rampastring) Vanilla fixes: diff --git a/src/extensions/options/optionsext.cpp b/src/extensions/options/optionsext.cpp index 9800c0010..3f0248bbd 100644 --- a/src/extensions/options/optionsext.cpp +++ b/src/extensions/options/optionsext.cpp @@ -44,7 +44,8 @@ */ OptionsClassExtension::OptionsClassExtension(const OptionsClass *this_ptr) : GlobalExtensionClass(this_ptr), - SortDefensesAsLast(true) + SortDefensesAsLast(true), + FilterBandBoxSelection(true) { //EXT_DEBUG_TRACE("OptionsClassExtension::OptionsClassExtension - 0x%08X\n", (uintptr_t)(This())); } @@ -163,6 +164,7 @@ void OptionsClassExtension::Load_Settings() sun_ini.Load(file, false); SortDefensesAsLast = sun_ini.Get_Bool("Options", "SortDefensesAsLast", SortDefensesAsLast); + FilterBandBoxSelection = sun_ini.Get_Bool("Options", "FilterBandBoxSelection", FilterBandBoxSelection); } /** diff --git a/src/extensions/options/optionsext.h b/src/extensions/options/optionsext.h index 43fc6baad..3c478aabb 100644 --- a/src/extensions/options/optionsext.h +++ b/src/extensions/options/optionsext.h @@ -69,4 +69,9 @@ class OptionsClassExtension final : public GlobalExtensionClass * Should cameos of defenses (including walls and gates) be sorted to the bottom of the sidebar? */ bool SortDefensesAsLast; + + /** + * Are harvesters and MCVs excluded from a band-box selection that includes combat units? + */ + bool FilterBandBoxSelection; }; diff --git a/src/extensions/sidebar/sidebarext_hooks.cpp b/src/extensions/sidebar/sidebarext_hooks.cpp index f91a34966..14a5c7180 100644 --- a/src/extensions/sidebar/sidebarext_hooks.cpp +++ b/src/extensions/sidebar/sidebarext_hooks.cpp @@ -1120,8 +1120,8 @@ static int __cdecl BuildType_Comparison(const void* p1, const void* p2) BCAT_DEFENSE }; - int building_category1 = (b1->IsWall || b1->IsFirestormWall || b1->IsLaserFencePost || b1->IsLaserFence) ? BCAT_WALL : (b1->IsGate ? BCAT_GATE : (ext1->SortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL)); - int building_category2 = (b2->IsWall || b2->IsFirestormWall || b2->IsLaserFencePost || b2->IsLaserFence) ? BCAT_WALL : (b2->IsGate ? BCAT_GATE : (ext2->SortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL)); + int building_category1 = (b1->IsWall || b1->IsFirestormWall || b1->IsLaserFencePost || b1->IsLaserFence) ? BCAT_WALL : (b1->IsGate ? BCAT_GATE : (ext1->IsSortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL)); + int building_category2 = (b2->IsWall || b2->IsFirestormWall || b2->IsLaserFencePost || b2->IsLaserFence) ? BCAT_WALL : (b2->IsGate ? BCAT_GATE : (ext2->IsSortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL)); // Compare based on category priority if (building_category1 != building_category2) diff --git a/src/extensions/tactical/tacticalext_hooks.cpp b/src/extensions/tactical/tacticalext_hooks.cpp index dd1a53db1..b0957e34c 100644 --- a/src/extensions/tactical/tacticalext_hooks.cpp +++ b/src/extensions/tactical/tacticalext_hooks.cpp @@ -45,10 +45,18 @@ #include "fatal.h" #include "debughandler.h" #include "asserthandler.h" +#include "optionsext.h" +#include "object.h" +#include "house.h" +#include "technotype.h" +#include "building.h" +#include "buildingtype.h" + #include #include "hooker.h" #include "hooker_macros.h" +#include "technotypeext.h" #include "uicontrol.h" @@ -65,8 +73,23 @@ class TacticalExt : public Tactical { public: void _Draw_Band_Box(); + void _Select_These(Rect& rect, void (*selection_func)(ObjectClass* obj)); + +public: + + /** + * Static variables for selection filtering, need to be static + * so that the selection predicate can use them. + */ + static bool SelectionContainsNonCombatants; + static int SelectedCount; + static bool FilterSelection; }; +bool TacticalExt::SelectionContainsNonCombatants = false; +int TacticalExt::SelectedCount = 0; +bool TacticalExt::FilterSelection = false; + /** * Reimplements Tactical::Draw_Band_Box. @@ -185,6 +208,217 @@ void TacticalExt::_Draw_Band_Box() } +/** + * Helper function. + * Checks whether a specific object should be filtered + * out from selection if the selection includes combatants. + */ +static bool Should_Exclude_From_Selection(ObjectClass* obj) +{ + /** + * Don't exclude objects that we don't own. + */ + if (obj->Owning_House() != nullptr && !obj->Owning_House()->IsPlayerControl) { + return false; + } + + /** + * Exclude objects that aren't a selectable combatant per rules. + */ + if (obj->Is_Techno()) { + return Extension::Fetch(obj->Techno_Type_Class())->FilterFromBandBoxSelection; + } + + return false; +} + + +/** + * Filters the selection from any non-combatants. + * + * @author: Petroglyph (Remaster), Rampastring, ZivDero + */ +static void Filter_Selection() +{ + if (!OptionsExtension->FilterBandBoxSelection) { + return; + } + + bool any_to_exclude = false; + bool all_to_exclude = true; + + for (int i = 0; i < CurrentObjects.Count(); i++) { + const bool exclude = Should_Exclude_From_Selection(CurrentObjects[i]); + any_to_exclude |= exclude; + all_to_exclude &= exclude; + } + + if (any_to_exclude && !all_to_exclude) { + for (int i = 0; i < CurrentObjects.Count(); i++) { + if (Should_Exclude_From_Selection(CurrentObjects[i])) { + + const int count_before = CurrentObjects.Count(); + CurrentObjects[i]->Unselect(); + const int count_after = CurrentObjects.Count(); + if (count_after < count_before) { + i--; + } + } + } + } +} + + +/** + * Checks if the player has currently any non-combatants selected. + * + * @author: ZivDero + */ +static bool Has_NonCombatants_Selected() +{ + for (int i = 0; i < CurrentObjects.Count(); i++) + { + if (CurrentObjects[i]->Is_Techno() && Extension::Fetch(CurrentObjects[i]->Techno_Type_Class())->FilterFromBandBoxSelection) + return true; + } + + return false; +} + + +/** + * Reimplements Tactical::Select_These to filter non-combatants. + * + * @author: ZivDero + */ +void TacticalExt::_Select_These(Rect& rect, void (*selection_func)(ObjectClass* obj)) +{ + SelectionContainsNonCombatants = Has_NonCombatants_Selected(); + SelectedCount = CurrentObjects.Count(); + FilterSelection = false; + + AllowVoice = true; + + if (rect.Width > 0 && rect.Height > 0 && DirtyObjectCount > 0) + { + for (int i = 0; i < DirtyObjectCount; i++) + { + const auto dirty = DirtyObjects[i]; + if (dirty.Object && dirty.Object->IsActive) + { + Point2D position = dirty.Position - field_5C; + if (rect.Is_Within(position)) + { + if (selection_func) + { + selection_func(dirty.Object); + } + else + { + bool is_selectable_building = false; + if (dirty.Object->What_Am_I() == RTTI_BUILDING) + { + const auto bclass = static_cast(dirty.Object)->Class; + if (bclass->UndeploysInto && !bclass->IsConstructionYard && !bclass->IsMobileWar) + { + is_selectable_building = true; + } + } + + HouseClass* owner = dirty.Object->Owning_House(); + if (owner && owner->Is_Player_Control()) + { + if (dirty.Object->Class_Of()->IsSelectable) + { + if (dirty.Object->What_Am_I() != RTTI_BUILDING || is_selectable_building) + { + if (dirty.Object->Select()) + AllowVoice = false; + } + + } + + } + } + } + } + } + + } + + /** + * If player-controlled units are non-additively selected, + * remove non-combatants if they aren't the only types of units selected + */ + if (FilterSelection) + Filter_Selection(); + + AllowVoice = true; +} + + +/** + * Band box selection predicate replacement. + * + * @author: ZivDero + */ +static void Vinifera_Bandbox_Select(ObjectClass* obj) +{ + HouseClass* house = obj->Owning_House(); + BuildingClass* building = Target_As_Building(obj); + + /** + * Don't select objects that we don't own. + */ + if (!house || !house->Is_Player_Control()) + return; + + /** + * Don't select objects that aren't selectable. + */ + if (!obj->Class_Of()->IsSelectable) + return; + + /** + * Don't select buildings, unless it undeploys into something other than + * a construction yard or a war factory (for example, a deploying artillery). + */ + if (building && (!building->Class->UndeploysInto || building->Class->IsConstructionYard || building->Class->IsMobileWar)) + return; + + /** + * Don't select limboed objects. + */ + if (obj->IsInLimbo) + return; + + /** + * If this is a Techno that's not a combatant, and the selection isn't new and doesn't + * already contain non-combatants, don't select it. + */ + const TechnoClass* techno = Target_As_Techno(obj); + if (techno && OptionsExtension->FilterBandBoxSelection + && TacticalExt::SelectedCount > 0 && !TacticalExt::SelectionContainsNonCombatants + && !WWKeyboard->Down(VK_ALT)) + { + const auto ext = Extension::Fetch(techno->Techno_Type_Class()); + if (ext->FilterFromBandBoxSelection) + return; + } + + if (obj->Select()) + { + AllowVoice = false; + + /** + * If this is a new selection, filter it at the end. + */ + if (TacticalExt::SelectedCount == 0 && !WWKeyboard->Down(VK_ALT)) + TacticalExt::FilterSelection = true; + } +} + + /** * #issue-315 * @@ -572,6 +806,10 @@ void TacticalExtension_Hooks() * @authors: CCHyper */ Patch_Dword(0x006171C8+1, (TPF_CENTER|TPF_EFNT|TPF_FULLSHADOW)); + Patch_Jump(0x00616FDA, &_Tactical_Draw_Waypoint_Paths_Text_Color_Patch); Patch_Jump(0x00616560, &TacticalExt::_Draw_Band_Box); + + Patch_Jump(0x00616940, &TacticalExt::_Select_These); + Patch_Jump(0x00479150, &Vinifera_Bandbox_Select); } diff --git a/src/extensions/technotype/technotypeext.cpp b/src/extensions/technotype/technotypeext.cpp index a53a511e4..2fc4e5f85 100644 --- a/src/extensions/technotype/technotypeext.cpp +++ b/src/extensions/technotype/technotypeext.cpp @@ -69,8 +69,9 @@ TechnoTypeClassExtension::TechnoTypeClassExtension(const TechnoTypeClass *this_p PipWrap(0), IdleRate(0), CameoImageSurface(nullptr), - SortCameoAsBaseDefense(false), - Description("") + IsSortCameoAsBaseDefense(false), + Description(""), + FilterFromBandBoxSelection(false) { //if (this_ptr) EXT_DEBUG_TRACE("TechnoTypeClassExtension::TechnoTypeClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); } @@ -283,7 +284,8 @@ bool TechnoTypeClassExtension::Read_INI(CCINIClass &ini) CameoImageSurface = imagesurface; } - SortCameoAsBaseDefense = ini.Get_Bool(ini_name, "SortCameoAsBaseDefense", SortCameoAsBaseDefense); + IsSortCameoAsBaseDefense = ini.Get_Bool(ini_name, "SortCameoAsBaseDefense", IsSortCameoAsBaseDefense); + FilterFromBandBoxSelection = ini.Get_Bool(ini_name, "FilterFromBandBoxSelection", FilterFromBandBoxSelection); return true; } diff --git a/src/extensions/technotype/technotypeext.h b/src/extensions/technotype/technotypeext.h index 93e2c1c75..b543ea273 100644 --- a/src/extensions/technotype/technotypeext.h +++ b/src/extensions/technotype/technotypeext.h @@ -172,10 +172,16 @@ class TechnoTypeClassExtension : public ObjectTypeClassExtension /** * Should this be considered a base defense when sorting cameos on the sidebar? */ - bool SortCameoAsBaseDefense; + bool IsSortCameoAsBaseDefense; /** * Description for the extended sidebar tooltip. */ char Description[200]; + + /** + * If this property is set to true, this object will not be selected when band box selecting + * if any objects in the selection have it set to false (e. g., harvesters and MCVs won't be selected with tanks). + */ + bool FilterFromBandBoxSelection; };