Skip to content

Conversation

@TaranDahl
Copy link
Contributor

@TaranDahl TaranDahl commented Dec 8, 2025

Reopen #1453 again.
Documents:

[ ] Distribution Mode Spread / Filter / Enable

  • Now you can change the click action by using AllowSwitchNoMoveCommand hotkey. If the behavior to be executed by the current techno is different from the behavior displayed by the mouse, and the behavior to be executed will make the techno move near the target, the behavior will be replaced with area guard. Regardless of whether or not switch hotkey is used, default behavior can be changed through DefaultApplyNoMoveCommand.
  • Now you can also change the click action when hold down the specific hotkey if enabled AllowDistributionCommand. The new behavior is like using the selected objects one by one to click on each target within the range.
  • AllowDistributionCommand.SpreadMode & AllowDistributionCommand.FilterMode allow you to set spread range and target filter by hotkeys, which default to DefaultDistributionSpreadMode and DefaultDistributionFilterMode.
    • When the range is 0, it is the original default behavior of the game. The range can be adjusted to 4, 8 or 16 cells by another shortcut key. You can also adjust this by using the mouse wheel while holding down the specific hotkey if AllowDistributionCommand.SpreadModeScroll set to true;
      • The targets within the range will be allocated equally to the selected technos. Only when the behavior to be performed by the current techno is the same as that displayed by the mouse will it be allocated. Otherwise, it will return to the original default behavior of the game (it will not be effective for technos in the air). This will display a range ring.
    • When the filter is None, it is the default behavior of the game. If the range is not zero at this time, a green ring will be displayed. You can adjust the filter mode to:
      • Like - only targets with the same armor type (Completely identical Armor) will be selected among the targets allocated in the range. At this time, a blue ring will be displayed.
      • Type - only targets of the same type (like infantries, vehicles or buildings) will be selected among the targets allocated in the range. At this time, a yellow ring will be displayed.
      • Name - only targets of the same name (or with the same GroupAs) will be selected among the targets allocated in the range. At this time, a red ring will be displayed.
  • AllowDistributionCommand.AffectsAllies & AllowDistributionCommand.AffectsEnemies allow the distribution command to work on allies (including owner) or enemies target. If picking a target that's not eligible, it'll fallback to vanilla command.
  • It's possible to add a button for distribution mode in the bottom bar by adding DistributionMode in the ButtonList of AdvancedCommandBar and MultiplayerAdvancedCommandBar.
    • The positions of each button are hardcoded, so it'll only decide whether enable this button or not. Distribute Mode button is now always listed after all the vanilla ones.
    • The asset of these buttons should be added in sidec0x.mix files which correspond to different sides, with the name button12.shp.
  • For localization add TXT_SWITCH_NOMOVE, TXT_DISTR_SPREAD, TXT_DISTR_FILTER, TXT_DISTR_HOLDDOWN, TXT_SWITCH_NOMOVE_DESC, TXT_DISTR_SPREAD_DESC, TXT_DISTR_FILTER_DESC, TXT_DISTR_HOLDDOWN_DESC, MSG:DistributionModeOn, MSG:DistributionModeOff, TIP:DistributionMode into your .csf file.

In rulesmd.ini:

[GlobalControls]
AllowSwitchNoMoveCommand=false                      ; boolean
AllowDistributionCommand=false                      ; boolean
AllowDistributionCommand.SpreadMode=true            ; boolean
AllowDistributionCommand.SpreadModeScroll=true      ; boolean
AllowDistributionCommand.FilterMode=true            ; boolean
AllowDistributionCommand.AffectsAllies=true         ; boolean
AllowDistributionCommand.AffectsEnemies=true        ; boolean

[AudioVisual]
StartDistributionModeSound=                         ; sound entry
EndDistributionModeSound=                           ; sound entry
AddDistributionModeCommandSound=                    ; sound entry

In ra2md.ini:

[Phobos]
DefaultApplyNoMoveCommand=true                      ; boolean
DefaultDistributionSpreadMode=2                     ; integer, 0 - r=0 , 1 - r=4 , 2 - r=8 , 3 - r=16
DefaultDistributionFilterMode=2                     ; integer, 0 - None , 1 - Like , 2 - Type , 3 - Name

In uimd.ini:

[AdvancedCommandBar]
ButtonList=[Button1],DistributionMode,[ButtonX]     ; List of button entry

[MultiplayerAdvancedCommandBar]
ButtonList=[Button1],DistributionMode,[ButtonX]     ; List of button entry

CrimRecya and others added 30 commits December 16, 2024 00:48
…tion

# Conflicts:
#	CREDITS.md
#	docs/Whats-New.md
#	src/Commands/Commands.cpp
#	src/Phobos.INI.cpp
#	src/Phobos.h
…tion

# Conflicts:
#	CREDITS.md
#	docs/Whats-New.md
@github-actions
Copy link

github-actions bot commented Dec 8, 2025

Nightly build for this pull request:

This comment is automatic and is meant to allow guests to get latest nightly builds for this pull request without registering. It is updated on every successful build.

@TaranDahl
Copy link
Contributor Author

@Metadorius Any issue other than #1949 (review)?

@TaranDahl
Copy link
Contributor Author

TaranDahl commented Dec 8, 2025

TODO:
(Summarized from #1949 (review))

  • Create independent file for AdvancedCommandBarButtonClass.
  • Allow registering new AdvancedCommandBarButtonClass like MakeCommand.
  • Rewrite the spread range code to allow exact range.
  • Remove spread mode hot key.
  • Press-and-drag mode

@Metadorius Please confirm the above summary, or supplement/correct the incorrect parts.

@Metadorius
Copy link
Member

@TaranDahl yeah, correct, there was also a comment about implementing press-and-drag mode (and not sure if the "same type" (infantry/buildings/vehicles/etc) is needed, since we have "same armor" mode). CrimRecya said it's too problematic, however I don't really see why, since we already have drag selection and we could reuse drag selection to calculate the radius.

@TaranDahl
Copy link
Contributor Author

TaranDahl commented Dec 10, 2025

there was also a comment about implementing press-and-drag mode (and not sure if the "same type" (infantry/buildings/vehicles/etc) is needed

I think there is not enough labor force to add more features.
I think the existing work is sufficient in terms of completion. We should just perfect the existing features and then merge them.
As for more features, they should be another work, and can be implemented later, by anyone who wants them.
Breaking it into two PR can also help avoid the problem of having an excessive amount of code piled up in one PR, which may lead to no one willing to review it.

@Metadorius
Copy link
Member

I think there is not enough labor force to add more features. I think the existing work is sufficient in terms of completion. We should just perfect the existing features and then merge them. As for more features, they should be another work, and can be implemented later, by anyone who wants them. Breaking it into two PR can also help avoid the problem of having an excessive amount of code piled up in one PR, which may lead to no one willing to review it.

This isn't a feature though? It is just a somewhat small improvement that brings it in line with how modern games do it.

  • 0x6D2280 (exists in YRpp) for transforming screen coords into world coords (though Z is 0, maybe would want to account for that)
  • 0x4AC4CC is where the drag-selection band is set and the mouse hold is done
  • 0x4ABCEB is where the drag-selection release is handled
  • 0xD90 in TacticalClass is Rect that contains X, Y, Width, Height

From that you could calculate 2 points, get world coords via function above, use this info to draw a corresponding circle and set the mode to such. I am not sure what is complex here. If needed I can send my decompile for this.

@TaranDahl
Copy link
Contributor Author

@Metadorius Is there any difference between ShapeButtonClass and AdvancedCommandBarButtonClass?

@Metadorius
Copy link
Member

Metadorius commented Dec 24, 2025

@TaranDahl ShapeButtonClass is a vanilla engine class, AdvancedCommandBarButtonClass is something I propose to invent (perhaps a descendant from ShapeButtonClass) that stores the extra things that are currently stored as static arrays, for example.

@TaranDahl
Copy link
Contributor Author

@TaranDahl ShapeButtonClass is a vanilla engine class, AdvancedCommandBarButtonClass is something I propose to invent (perhaps a descendant from ShapeButtonClass) that stores the extra things that are currently stored as static arrays, for example.

Judging from the current code, there seems to be no need for it to inherit from ShapeButtonClass. After all, the game originally stores those attributes in a static array.
image

@Metadorius
Copy link
Member

After all, the game originally stores those attributes in a static array.

Which is a bad pattern. You have class fields and methods for that.

@TaranDahl
Copy link
Contributor Author

Yeah I will make a new class to arrange the new buttons. But for the vanilla buttons, maybe we should just let them be?

@Metadorius
Copy link
Member

Yeah I will make a new class to arrange the new buttons. But for the vanilla buttons, maybe we should just let them be?

Yeah I didn't mean we should be squeezing vanilla static array shitcode into proper classes necessarily, should be good.

@TaranDahl
Copy link
Contributor Author

If we want to implement dragging, how should we handle the selection range?
Right now, it's several specific values.

if (!DistributionModeHoldDownCommandClass::OffMessageShowed)
{
DistributionModeHoldDownCommandClass::OffMessageShowed = true;
MessageListClass::Instance.PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:DistributionModeOff", L"Distribution mode unabled."), RulesClass::Instance->MessageDelay, HouseClass::CurrentPlayer->ColorSchemeIndex, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MessageListClass::Instance.PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:DistributionModeOff", L"Distribution mode unabled."), RulesClass::Instance->MessageDelay, HouseClass::CurrentPlayer->ColorSchemeIndex, true);
MessageListClass::Instance.PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:DistributionModeOff", L"Distribution mode disabled."), RulesClass::Instance->MessageDelay, HouseClass::CurrentPlayer->ColorSchemeIndex, true);

Copy link
Member

@Metadorius Metadorius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partial review

Comment on lines +186 to +427
void __fastcall DistributionModeHoldDownCommandClass::ClickedWaypoint(ObjectClass* pSelect, int idxPath, signed char idxWP)
{
pSelect->AssignPlanningPath(idxPath, idxWP);

if (const auto pFoot = abstract_cast<FootClass*, true>(pSelect))
pFoot->unknown_bool_430 = false;
}

void __fastcall DistributionModeHoldDownCommandClass::ClickedTargetAction(ObjectClass* pSelect, Action action, ObjectClass* pTarget)
{
pSelect->ObjectClickedAction(action, pTarget, false);
Unsorted::MoveFeedback = false;
}

void __fastcall DistributionModeHoldDownCommandClass::ClickedCellAction(ObjectClass* pSelect, Action action, CellStruct* pCell, CellStruct* pSecondCell)
{
pSelect->CellClickedAction(action, pCell, pSecondCell, false);
Unsorted::MoveFeedback = false;
}

void __fastcall DistributionModeHoldDownCommandClass::AreaGuardAction(TechnoClass* pTechno)
{
pTechno->ClickedMission(Mission::Area_Guard, reinterpret_cast<ObjectClass*>(pTechno->GetCellAgain()), nullptr, nullptr);
Unsorted::MoveFeedback = false;
}

DEFINE_HOOK(0x4AE7B3, DisplayClass_ActiveClickWith_Iterate, 0x0)
{
enum { SkipGameCode = 0x4AE99B };

const int count = ObjectClass::CurrentObjects.Count;

if (count > 0)
{
{
GET_STACK(int, idxPath, STACK_OFFSET(0x18, -0x8));
GET_STACK(unsigned char, idxWP, STACK_OFFSET(0x18, -0xC));

for (const auto& pSelect : ObjectClass::CurrentObjects)
{
DistributionModeHoldDownCommandClass::ClickedWaypoint(pSelect, idxPath, idxWP);
}
}

GET_STACK(ObjectClass* const, pTarget, STACK_OFFSET(0x18, 0x4));
GET_STACK(Action const, action, STACK_OFFSET(0x18, 0xC));

if (pTarget)
{
const int spreadMode = Phobos::Config::DistributionSpreadMode;
const int filterMode = Phobos::Config::DistributionFilterMode;
const bool noMove = !Phobos::Config::ApplyNoMoveCommand;
const auto pTechno = abstract_cast<TechnoClass*, true>(pTarget);

// Distribution mode main
if (DistributionModeHoldDownCommandClass::Enabled
&& spreadMode
&& count > 1
&& action != Action::NoMove
&& !PlanningNodeClass::PlanningModeActive
&& pTechno
&& !pTechno->IsInAir()
&& (HouseClass::CurrentPlayer->IsAlliedWith(pTechno->Owner)
? Phobos::Config::AllowDistributionCommand_AffectsAllies
: Phobos::Config::AllowDistributionCommand_AffectsEnemies))
{
VocClass::PlayGlobal(RulesExt::Global()->AddDistributionModeCommandSound, 0x2000, 1.0);
const bool targetIsNeutral = pTechno->Owner->IsNeutral();
const auto pType = pTechno->GetTechnoType();
const int range = (2 << spreadMode);
const auto center = pTechno->GetCoords();
const auto pItems = Helpers::Alex::getCellSpreadItems(center, range);

std::vector<std::pair<TechnoClass*, int>> record;
const size_t maxSize = pItems.size();
record.reserve(maxSize);

int current = 1;

for (const auto& pItem : pItems)
{
if (pItem->IsDisguisedAs(HouseClass::CurrentPlayer))
continue;

if (pItem->CloakState == CloakState::Cloaked && !pItem->GetCell()->Sensors_InclHouse(HouseClass::CurrentPlayer->ArrayIndex))
continue;

auto coords = pItem->GetCoords();

if (!MapClass::Instance.IsWithinUsableArea(coords))
continue;

coords.Z = MapClass::Instance.GetCellFloorHeight(coords);

if (MapClass::Instance.GetCellAt(coords)->ContainsBridge())
coords.Z += CellClass::BridgeHeight;

if (!MapClass::Instance.IsLocationShrouded(coords))
record.emplace_back(pItem, 0);
}

const size_t recordSize = record.size();
std::sort(&record[0], &record[recordSize],[&center](const auto& pairA, const auto& pairB)
{
const auto coordsA = pairA.first->GetCoords();
const double distanceA = Point2D{coordsA.X, coordsA.Y}.DistanceFromSquared(Point2D{center.X, center.Y});

const auto coordsB = pairB.first->GetCoords();
const double distanceB = Point2D{coordsB.X, coordsB.Y}.DistanceFromSquared(Point2D{center.X, center.Y});

return distanceA < distanceB;
});

for (const auto& pSelect : ObjectClass::CurrentObjects)
{
size_t canTargetIndex = maxSize;
size_t newTargetIndex = maxSize;

for (size_t i = 0; i < recordSize; ++i)
{
const auto& [pItem, num] = record[i];

if (pSelect->MouseOverObject(pItem) != action)
continue;

if (!targetIsNeutral && pItem->Owner->IsNeutral())
continue;

if (filterMode)
{
const auto pItemType = pItem->GetTechnoType();

if (!pItemType)
continue;

if (TechnoTypeExt::ExtMap.Find(pType)->FakeOf != pItemType
&& TechnoTypeExt::ExtMap.Find(pItemType)->FakeOf != pType)
{
if (filterMode == 1)
{
if (pItemType->Armor != pType->Armor)
continue;
}
else if (filterMode == 2)
{
if (pItem->WhatAmI() != pTechno->WhatAmI())
continue;
}
else // filterMode == 3
{
if (TechnoTypeExt::GetSelectionGroupID(pItemType) != TechnoTypeExt::GetSelectionGroupID(pType))
continue;
}
}
}

canTargetIndex = i;

if (num < current)
{
newTargetIndex = i;
break;
}
}

if (newTargetIndex == maxSize && canTargetIndex != maxSize)
{
++current;
newTargetIndex = canTargetIndex;
}

if (newTargetIndex != maxSize)
{
auto& [pNewTarget, recordCount] = record[newTargetIndex];

DistributionModeHoldDownCommandClass::ClickedTargetAction(pSelect, action, pNewTarget);

++recordCount;
continue;
}

const auto currentAction = pSelect->MouseOverObject(pTechno);

if (noMove && currentAction == Action::NoMove && (pSelect->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None)
DistributionModeHoldDownCommandClass::AreaGuardAction(static_cast<TechnoClass*>(pSelect));
else
DistributionModeHoldDownCommandClass::ClickedTargetAction(pSelect, currentAction, pTechno);
}
}
else
{
for (const auto& pSelect : ObjectClass::CurrentObjects)
{
const auto currentAction = pSelect->MouseOverObject(pTarget);

if (noMove && action != Action::NoMove && currentAction == Action::NoMove && (pSelect->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None)
DistributionModeHoldDownCommandClass::AreaGuardAction(static_cast<TechnoClass*>(pSelect));
else
DistributionModeHoldDownCommandClass::ClickedTargetAction(pSelect, currentAction, pTarget);
}
}
}
else
{
LEA_STACK(CellStruct* const, pCell, STACK_OFFSET(0x18, 0x8));

auto invalidCell = CellStruct { -1, -1 };
auto pSecondCell = action == Action::Move || action == Action::PatrolWaypoint || action == Action::NoMove ? pCell : &invalidCell;

for (const auto& pSelect : ObjectClass::CurrentObjects)
{
const auto currentAction = pSelect->MouseOverCell(pCell, false, false);

DistributionModeHoldDownCommandClass::ClickedCellAction(pSelect, currentAction, pCell, pSecondCell);
}
}
}

Unsorted::MoveFeedback = true;

return SkipGameCode;
}

DEFINE_HOOK(0x6DBE74, TacticalClass_DrawAllRadialIndicators_DrawDistributionRange, 0x7)
{
if (!DistributionModeHoldDownCommandClass::Enabled && SystemTimer::GetTime() - DistributionModeHoldDownCommandClass::ShowTime > 30)
return 0;

const auto spreadMode = Phobos::Config::DistributionSpreadMode;
const auto filterMode = Phobos::Config::DistributionFilterMode;

if (spreadMode || filterMode)
{
const auto pCell = MapClass::Instance.GetCellAt(DisplayClass::Instance.CurrentFoundation_CenterCell);
const auto color = (filterMode > 1)
? ((filterMode == 3) ? ColorStruct { 255, 0, 0 } : ColorStruct { 200, 200, 0 })
: ((filterMode == 1) ? ColorStruct { 0, 100, 255 } : ColorStruct { 0, 255, 50 });
Game::DrawRadialIndicator(false, true, pCell->GetCoords(), color, static_cast<float>(spreadMode ? (2 << spreadMode) : 0.5), false, true);
}

return 0;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think since this is so complex and spans across than a single "execute" method -- the actual distribution mode handling should not be a part of the command declaration/definition but elsewhere, and command classes should just call what they need in execute method. Same for hooks.

Comment on lines +255 to +257
const int range = (2 << spreadMode);
const auto center = pTechno->GetCoords();
const auto pItems = Helpers::Alex::getCellSpreadItems(center, range);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You said that the code would need significant changes previously to accomodate arbitrary precise range, but I don't see what is significant here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we used shortcut keys because there were only a few available values. If there are too many available values, then the shortcut keys will become impractical and we need to delete them. There probably won't be much actual work, but there will be changes to the existing functions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a misunderstanding. I think shortcut keys could be left in with pre-set ranges, and dragging will mean an arbitrary range is selected. Both could exist in parallel.

Although if I am being honest I personally don't think there's a need in anything else other than drag-distribution. This is how it works in many modern RTS/RTT games with advanced controls and there's no customisation, I never heard it being a problem.

Unsorted::MoveFeedback = false;
}

DEFINE_HOOK(0x4AE7B3, DisplayClass_ActiveClickWith_Iterate, 0x0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this big hook should be split out into functions/methods called from a hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants