Skip to content

Commit

Permalink
Add band box selection filtering (#1106)
Browse files Browse the repository at this point in the history
Co-authored-by: Rampastring <[email protected]>
  • Loading branch information
ZivDero and Rampastring committed Oct 17, 2024
1 parent 00c6c72 commit a307ea7
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand All @@ -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`.

21 changes: 21 additions & 0 deletions docs/New-Features-and-Enhancements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/extensions/options/optionsext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/extensions/options/optionsext.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ class OptionsClassExtension final : public GlobalExtensionClass<OptionsClass>
* 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;
};
4 changes: 2 additions & 2 deletions src/extensions/sidebar/sidebarext_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
238 changes: 238 additions & 0 deletions src/extensions/tactical/tacticalext_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <timeapi.h>

#include "hooker.h"
#include "hooker_macros.h"
#include "technotypeext.h"
#include "uicontrol.h"


Expand All @@ -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.
Expand Down Expand Up @@ -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<TechnoTypeClassExtension>(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<TechnoTypeClassExtension>(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<BuildingClass*>(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<TechnoTypeClassExtension>(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
*
Expand Down Expand Up @@ -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);
}
8 changes: 5 additions & 3 deletions src/extensions/technotype/technotypeext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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;
}
Loading

0 comments on commit a307ea7

Please sign in to comment.