Skip to content

Commit 6bb9e1f

Browse files
ZivDeroRampastring
andcommitted
Add band box selection filtering (#1106)
Co-authored-by: Rampastring <[email protected]>
1 parent 00c6c72 commit 6bb9e1f

File tree

9 files changed

+284
-7
lines changed

9 files changed

+284
-7
lines changed

CREDITS.md

+2
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ This page lists all the individual contributions to the project by their author.
155155
- Fix a bug where a vehicle transport could end up attached to its own cargo, causing the transport to disappear upon unloading.
156156
- Fix a bug where a harvester could be ordered to dock with a refinery that wasn't listed in the harvester's `Dock=` key.
157157
- Fix a bug where house firepower bonus, veterancy and crate upgrade damage modifiers were not applied to railgun `AmbientDamage=`.
158+
- Implement `FilterFromBandBoxSelection`.
158159
- **secsome**:
159160
- Add support for up to 32767 waypoints to be used in scenarios.
160161
- **ZivDero**:
@@ -179,4 +180,5 @@ This page lists all the individual contributions to the project by their author.
179180
- Make it so that it is no longer required to list all Tiberiums in a map to override some Tiberium's properties.
180181
- Add `PipWrap`.
181182
- Adjustments to the band box, action line, target laser and NavCom queue line customization features.
183+
- Implement `FilterFromBandBoxSelection`.
182184

docs/New-Features-and-Enhancements.md

+21
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,27 @@ PipWrap=0 ; integer, the number of ammo pips to draw using pip wrap.
354354
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`.
355355
```
356356

357+
### Selection Filtering
358+
359+
- Vinifera adds the ability to exclude some TechnoTypes from band selection.
360+
361+
In `RULES.INI`:
362+
```ini
363+
[SOMETECHNO] ; TechnoType
364+
FilterFromBandBoxSelection=yes ; boolean, should this Techno be excluded from band box selection when it contains units without this flag?
365+
```
366+
367+
- 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.
368+
- By holding `ALT` it is possible to temporarily ignore this logic and select all types of objects.
369+
370+
- It is also possible to disable this behavior in `SUN.INI`.
371+
372+
In `SUN.INI`:
373+
```ini
374+
[Options]
375+
FilterBandBoxSelection=yes ; boolean, should the band box selection be filtered?
376+
```
377+
357378
## Terrain
358379

359380
### Light Sources

docs/Whats-New.md

+1
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ New:
147147
- Implement various controls to customise action lines (by CCHyper/tomsons26)
148148
- Implement various controls to customise target lasers line (by CCHyper/tomsons26)
149149
- Implement various controls to show and customise NavCom queue lines (by CCHyper/tomsons26)
150+
- Implement `FilterFromBandBoxSelection` (by ZivDero/Rampastring)
150151

151152

152153
Vanilla fixes:

src/extensions/options/optionsext.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
*/
4545
OptionsClassExtension::OptionsClassExtension(const OptionsClass *this_ptr) :
4646
GlobalExtensionClass(this_ptr),
47-
SortDefensesAsLast(true)
47+
SortDefensesAsLast(true),
48+
FilterBandBoxSelection(true)
4849
{
4950
//EXT_DEBUG_TRACE("OptionsClassExtension::OptionsClassExtension - 0x%08X\n", (uintptr_t)(This()));
5051
}
@@ -163,6 +164,7 @@ void OptionsClassExtension::Load_Settings()
163164
sun_ini.Load(file, false);
164165

165166
SortDefensesAsLast = sun_ini.Get_Bool("Options", "SortDefensesAsLast", SortDefensesAsLast);
167+
FilterBandBoxSelection = sun_ini.Get_Bool("Options", "FilterBandBoxSelection", FilterBandBoxSelection);
166168
}
167169

168170
/**

src/extensions/options/optionsext.h

+5
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,9 @@ class OptionsClassExtension final : public GlobalExtensionClass<OptionsClass>
6969
* Should cameos of defenses (including walls and gates) be sorted to the bottom of the sidebar?
7070
*/
7171
bool SortDefensesAsLast;
72+
73+
/**
74+
* Are harvesters and MCVs excluded from a band-box selection that includes combat units?
75+
*/
76+
bool FilterBandBoxSelection;
7277
};

src/extensions/sidebar/sidebarext_hooks.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -1120,8 +1120,8 @@ static int __cdecl BuildType_Comparison(const void* p1, const void* p2)
11201120
BCAT_DEFENSE
11211121
};
11221122

1123-
int building_category1 = (b1->IsWall || b1->IsFirestormWall || b1->IsLaserFencePost || b1->IsLaserFence) ? BCAT_WALL : (b1->IsGate ? BCAT_GATE : (ext1->SortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL));
1124-
int building_category2 = (b2->IsWall || b2->IsFirestormWall || b2->IsLaserFencePost || b2->IsLaserFence) ? BCAT_WALL : (b2->IsGate ? BCAT_GATE : (ext2->SortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL));
1123+
int building_category1 = (b1->IsWall || b1->IsFirestormWall || b1->IsLaserFencePost || b1->IsLaserFence) ? BCAT_WALL : (b1->IsGate ? BCAT_GATE : (ext1->IsSortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL));
1124+
int building_category2 = (b2->IsWall || b2->IsFirestormWall || b2->IsLaserFencePost || b2->IsLaserFence) ? BCAT_WALL : (b2->IsGate ? BCAT_GATE : (ext2->IsSortCameoAsBaseDefense ? BCAT_DEFENSE : BCAT_NORMAL));
11251125

11261126
// Compare based on category priority
11271127
if (building_category1 != building_category2)

src/extensions/tactical/tacticalext_hooks.cpp

+238
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,18 @@
4545
#include "fatal.h"
4646
#include "debughandler.h"
4747
#include "asserthandler.h"
48+
#include "optionsext.h"
49+
#include "object.h"
50+
#include "house.h"
51+
#include "technotype.h"
52+
#include "building.h"
53+
#include "buildingtype.h"
54+
4855
#include <timeapi.h>
4956

5057
#include "hooker.h"
5158
#include "hooker_macros.h"
59+
#include "technotypeext.h"
5260
#include "uicontrol.h"
5361

5462

@@ -65,8 +73,23 @@ class TacticalExt : public Tactical
6573
{
6674
public:
6775
void _Draw_Band_Box();
76+
void _Select_These(Rect& rect, void (*selection_func)(ObjectClass* obj));
77+
78+
public:
79+
80+
/**
81+
* Static variables for selection filtering, need to be static
82+
* so that the selection predicate can use them.
83+
*/
84+
static bool SelectionContainsNonCombatants;
85+
static int SelectedCount;
86+
static bool FilterSelection;
6887
};
6988

89+
bool TacticalExt::SelectionContainsNonCombatants = false;
90+
int TacticalExt::SelectedCount = 0;
91+
bool TacticalExt::FilterSelection = false;
92+
7093

7194
/**
7295
* Reimplements Tactical::Draw_Band_Box.
@@ -185,6 +208,217 @@ void TacticalExt::_Draw_Band_Box()
185208
}
186209

187210

211+
/**
212+
* Helper function.
213+
* Checks whether a specific object should be filtered
214+
* out from selection if the selection includes combatants.
215+
*/
216+
static bool Should_Exclude_From_Selection(ObjectClass* obj)
217+
{
218+
/**
219+
* Don't exclude objects that we don't own.
220+
*/
221+
if (obj->Owning_House() != nullptr && !obj->Owning_House()->IsPlayerControl) {
222+
return false;
223+
}
224+
225+
/**
226+
* Exclude objects that aren't a selectable combatant per rules.
227+
*/
228+
if (obj->Is_Techno()) {
229+
return Extension::Fetch<TechnoTypeClassExtension>(obj->Techno_Type_Class())->FilterFromBandBoxSelection;
230+
}
231+
232+
return false;
233+
}
234+
235+
236+
/**
237+
* Filters the selection from any non-combatants.
238+
*
239+
* @author: Petroglyph (Remaster), Rampastring, ZivDero
240+
*/
241+
static void Filter_Selection()
242+
{
243+
if (!OptionsExtension->FilterBandBoxSelection) {
244+
return;
245+
}
246+
247+
bool any_to_exclude = false;
248+
bool all_to_exclude = true;
249+
250+
for (int i = 0; i < CurrentObjects.Count(); i++) {
251+
const bool exclude = Should_Exclude_From_Selection(CurrentObjects[i]);
252+
any_to_exclude |= exclude;
253+
all_to_exclude &= exclude;
254+
}
255+
256+
if (any_to_exclude && !all_to_exclude) {
257+
for (int i = 0; i < CurrentObjects.Count(); i++) {
258+
if (Should_Exclude_From_Selection(CurrentObjects[i])) {
259+
260+
const int count_before = CurrentObjects.Count();
261+
CurrentObjects[i]->Unselect();
262+
const int count_after = CurrentObjects.Count();
263+
if (count_after < count_before) {
264+
i--;
265+
}
266+
}
267+
}
268+
}
269+
}
270+
271+
272+
/**
273+
* Checks if the player has currently any non-combatants selected.
274+
*
275+
* @author: ZivDero
276+
*/
277+
static bool Has_NonCombatants_Selected()
278+
{
279+
for (int i = 0; i < CurrentObjects.Count(); i++)
280+
{
281+
if (CurrentObjects[i]->Is_Techno() && Extension::Fetch<TechnoTypeClassExtension>(CurrentObjects[i]->Techno_Type_Class())->FilterFromBandBoxSelection)
282+
return true;
283+
}
284+
285+
return false;
286+
}
287+
288+
289+
/**
290+
* Reimplements Tactical::Select_These to filter non-combatants.
291+
*
292+
* @author: ZivDero
293+
*/
294+
void TacticalExt::_Select_These(Rect& rect, void (*selection_func)(ObjectClass* obj))
295+
{
296+
SelectionContainsNonCombatants = Has_NonCombatants_Selected();
297+
SelectedCount = CurrentObjects.Count();
298+
FilterSelection = false;
299+
300+
AllowVoice = true;
301+
302+
if (rect.Width > 0 && rect.Height > 0 && DirtyObjectCount > 0)
303+
{
304+
for (int i = 0; i < DirtyObjectCount; i++)
305+
{
306+
const auto dirty = DirtyObjects[i];
307+
if (dirty.Object && dirty.Object->IsActive)
308+
{
309+
Point2D position = dirty.Position - field_5C;
310+
if (rect.Is_Within(position))
311+
{
312+
if (selection_func)
313+
{
314+
selection_func(dirty.Object);
315+
}
316+
else
317+
{
318+
bool is_selectable_building = false;
319+
if (dirty.Object->What_Am_I() == RTTI_BUILDING)
320+
{
321+
const auto bclass = static_cast<BuildingClass*>(dirty.Object)->Class;
322+
if (bclass->UndeploysInto && !bclass->IsConstructionYard && !bclass->IsMobileWar)
323+
{
324+
is_selectable_building = true;
325+
}
326+
}
327+
328+
HouseClass* owner = dirty.Object->Owning_House();
329+
if (owner && owner->Is_Player_Control())
330+
{
331+
if (dirty.Object->Class_Of()->IsSelectable)
332+
{
333+
if (dirty.Object->What_Am_I() != RTTI_BUILDING || is_selectable_building)
334+
{
335+
if (dirty.Object->Select())
336+
AllowVoice = false;
337+
}
338+
339+
}
340+
341+
}
342+
}
343+
}
344+
}
345+
}
346+
347+
}
348+
349+
/**
350+
* If player-controlled units are non-additively selected,
351+
* remove non-combatants if they aren't the only types of units selected
352+
*/
353+
if (FilterSelection)
354+
Filter_Selection();
355+
356+
AllowVoice = true;
357+
}
358+
359+
360+
/**
361+
* Band box selection predicate replacement.
362+
*
363+
* @author: ZivDero
364+
*/
365+
static void Vinifera_Bandbox_Select(ObjectClass* obj)
366+
{
367+
HouseClass* house = obj->Owning_House();
368+
BuildingClass* building = Target_As_Building(obj);
369+
370+
/**
371+
* Don't select objects that we don't own.
372+
*/
373+
if (!house || !house->Is_Player_Control())
374+
return;
375+
376+
/**
377+
* Don't select objects that aren't selectable.
378+
*/
379+
if (!obj->Class_Of()->IsSelectable)
380+
return;
381+
382+
/**
383+
* Don't select buildings, unless it undeploys into something other than
384+
* a construction yard or a war factory (for example, a deploying artillery).
385+
*/
386+
if (building && (!building->Class->UndeploysInto || building->Class->IsConstructionYard || building->Class->IsMobileWar))
387+
return;
388+
389+
/**
390+
* Don't select limboed objects.
391+
*/
392+
if (obj->IsInLimbo)
393+
return;
394+
395+
/**
396+
* If this is a Techno that's not a combatant, and the selection isn't new and doesn't
397+
* already contain non-combatants, don't select it.
398+
*/
399+
const TechnoClass* techno = Target_As_Techno(obj);
400+
if (techno && OptionsExtension->FilterBandBoxSelection
401+
&& TacticalExt::SelectedCount > 0 && !TacticalExt::SelectionContainsNonCombatants
402+
&& !WWKeyboard->Down(VK_ALT))
403+
{
404+
const auto ext = Extension::Fetch<TechnoTypeClassExtension>(techno->Techno_Type_Class());
405+
if (ext->FilterFromBandBoxSelection)
406+
return;
407+
}
408+
409+
if (obj->Select())
410+
{
411+
AllowVoice = false;
412+
413+
/**
414+
* If this is a new selection, filter it at the end.
415+
*/
416+
if (TacticalExt::SelectedCount == 0 && !WWKeyboard->Down(VK_ALT))
417+
TacticalExt::FilterSelection = true;
418+
}
419+
}
420+
421+
188422
/**
189423
* #issue-315
190424
*
@@ -572,6 +806,10 @@ void TacticalExtension_Hooks()
572806
* @authors: CCHyper
573807
*/
574808
Patch_Dword(0x006171C8+1, (TPF_CENTER|TPF_EFNT|TPF_FULLSHADOW));
809+
575810
Patch_Jump(0x00616FDA, &_Tactical_Draw_Waypoint_Paths_Text_Color_Patch);
576811
Patch_Jump(0x00616560, &TacticalExt::_Draw_Band_Box);
812+
813+
Patch_Jump(0x00616940, &TacticalExt::_Select_These);
814+
Patch_Jump(0x00479150, &Vinifera_Bandbox_Select);
577815
}

src/extensions/technotype/technotypeext.cpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ TechnoTypeClassExtension::TechnoTypeClassExtension(const TechnoTypeClass *this_p
6969
PipWrap(0),
7070
IdleRate(0),
7171
CameoImageSurface(nullptr),
72-
SortCameoAsBaseDefense(false),
73-
Description("")
72+
IsSortCameoAsBaseDefense(false),
73+
Description(""),
74+
FilterFromBandBoxSelection(false)
7475
{
7576
//if (this_ptr) EXT_DEBUG_TRACE("TechnoTypeClassExtension::TechnoTypeClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This()));
7677
}
@@ -283,7 +284,8 @@ bool TechnoTypeClassExtension::Read_INI(CCINIClass &ini)
283284
CameoImageSurface = imagesurface;
284285
}
285286

286-
SortCameoAsBaseDefense = ini.Get_Bool(ini_name, "SortCameoAsBaseDefense", SortCameoAsBaseDefense);
287+
IsSortCameoAsBaseDefense = ini.Get_Bool(ini_name, "SortCameoAsBaseDefense", IsSortCameoAsBaseDefense);
288+
FilterFromBandBoxSelection = ini.Get_Bool(ini_name, "FilterFromBandBoxSelection", FilterFromBandBoxSelection);
287289

288290
return true;
289291
}

0 commit comments

Comments
 (0)