diff --git a/CREDITS.md b/CREDITS.md
index 538c7fb5ee..8ec5603c07 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -565,6 +565,7 @@ This page lists all the individual contributions to the project by their author.
- Fix an issue that units' `LaserTrails` will always lags behind by one frame
- Fix an issue that the currently hovered planning node not update up-to-date, such as using hotkeys to select technos
- Allow the aircraft to enter area guard mission and not crash immediately without any airport
+ - Distribution click action mode
- **Ollerus**:
- Build limit group enhancement
- Customizable rocker amplitude
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 2def0db9a8..18126db27d 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -18,6 +18,7 @@
+
@@ -58,6 +59,7 @@
+
@@ -217,6 +219,7 @@
+
@@ -244,6 +247,7 @@
+
diff --git a/YRpp b/YRpp
index 5af96790ce..91ae0bc814 160000
--- a/YRpp
+++ b/YRpp
@@ -1 +1 @@
-Subproject commit 5af96790ce73e4ea068a390c60c124dccbc220e1
+Subproject commit 91ae0bc814feb1146908ddc53e0040f37f3e5e75
diff --git a/docs/User-Interface.md b/docs/User-Interface.md
index 28212091ca..cd310578a4 100644
--- a/docs/User-Interface.md
+++ b/docs/User-Interface.md
@@ -502,6 +502,57 @@ For this command to work in multiplayer - you need to use a version of [YRpp spa
- These vanilla CSF entries will be used: `TXT_SAVING_GAME`, `TXT_GAME_WAS_SAVED` and `TXT_ERROR_SAVING_GAME`.
- The save should be looks like `Allied Mission 25: Esther's Money - QuickSaved`.
+### `[ ]` 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`:
+```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`:
+```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`:
+```ini
+[AdvancedCommandBar]
+ButtonList=[Button1],DistributionMode,[ButtonX] ; List of button entry
+
+[MultiplayerAdvancedCommandBar]
+ButtonList=[Button1],DistributionMode,[ButtonX] ; List of button entry
+```
+
### `[ ]` Toggle Message Label
- Switches on/off [Task subtitles' label in the middle of the screen](#task-subtitles-display-in-the-middle-of-the-screen).
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 14aff0e43d..cc03f987c0 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -466,6 +466,7 @@ New:
- [Customize type selection for IFV](Fixed-or-Improved-Logics.md#customize-type-selection-for-ifv) (by NetsuNegi)
- CellSpread in cylinder shape (by TaranDahl)
- CellSpread damage check if victim is in air or on floor (by TaranDahl)
+- Distribution click action mode (by CrimRecya)
Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
diff --git a/src/Commands/AdvancedCommandBarButtons.cpp b/src/Commands/AdvancedCommandBarButtons.cpp
new file mode 100644
index 0000000000..20cc80ce3b
--- /dev/null
+++ b/src/Commands/AdvancedCommandBarButtons.cpp
@@ -0,0 +1,110 @@
+
+#include "AdvancedCommandBarButtons.h"
+
+
+#include "DistributionMode.h"
+
+#include
+#include "Commands.h"
+
+#include
+
+void AdvancedCommandBarButton::RegisterButtons()
+{
+ // Vanilla buttons
+ AdvancedCommandBarButton::TotalButtonCount = AdvancedCommandBarButton::OldButtonCount;
+
+ // New button register here
+ AdvancedCommandBarButton::Array.push_back(std::make_unique());
+}
+
+ShapeButtonClass* AdvancedCommandBarButton::GetShapeButton(const char* name)
+{
+ for (const auto& ptr : AdvancedCommandBarButton::Array)
+ {
+ AdvancedCommandBarButton* pNewButton = ptr.get();
+ if (_strcmpi(pNewButton->GetName(), name) == 0)
+ {
+ return ShapeButtonClass::GetButton(pNewButton->ID);
+ }
+ }
+
+ return nullptr;
+}
+
+#pragma region Hooks
+
+DEFINE_HOOK(0x6CFD08, AdvancedCommandBarClass_GetButtonIdxByName_NewButton, 0x5)
+{
+ enum { SetButtonIndex = 0x6CFD0D };
+
+ GET(const char*, name, ECX);
+
+ for (const auto& ptr : AdvancedCommandBarButton::Array)
+ {
+ AdvancedCommandBarButton* pButton = ptr.get();
+ if (_strcmpi(name, pButton->GetName()) == 0)
+ {
+ R->EAX(pButton->ID);
+ return SetButtonIndex;
+ }
+ }
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D0233, AdvancedCommandBarClass_Init_InitNewButtonIndex, 0x6)
+{
+ AdvancedCommandBarButton::RegisterButtons();
+ return 0;
+}
+
+DEFINE_HOOK(0x6D0827, AdvancedCommandBarClass_Update_UpdateNewButton, 0x6)
+{
+ GET(const int, index, EAX);
+
+ for (const auto& ptr : AdvancedCommandBarButton::Array)
+ {
+ AdvancedCommandBarButton* pNewButton = ptr.get();
+
+ if (pNewButton->ID == index)
+ {
+ pNewButton->Execute(ShapeButtonClass::GetButton(index)->IsOn);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D10DF, AdvancedCommandBarClass_InitButtonIO_InitNewHoldDownButton, 0x6)
+{
+ for (const auto& ptr : AdvancedCommandBarButton::Array)
+ {
+ AdvancedCommandBarButton* pNewButton = ptr.get();
+
+ if (!pNewButton->CanHoldDown())
+ continue;
+
+ if (auto pButton = ShapeButtonClass::GetButton(pNewButton->ID))
+ {
+ pButton->ToggleType = 1;
+ pButton->UseFlash = true;
+ }
+ }
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D14DD, AdvancedCommandBarClass_InitToolTip_InitNewButtonToolTip, 0x5)
+{
+ for (const auto& ptr : AdvancedCommandBarButton::Array)
+ {
+ AdvancedCommandBarButton* pNewButton = ptr.get();
+ ShapeButtonClass::SetToolTip(ShapeButtonClass::GetButton(pNewButton->ID), pNewButton->GetTipName());
+ }
+
+ return 0;
+}
+
+#pragma endregion
diff --git a/src/Commands/AdvancedCommandBarButtons.h b/src/Commands/AdvancedCommandBarButtons.h
new file mode 100644
index 0000000000..1bc02c4b12
--- /dev/null
+++ b/src/Commands/AdvancedCommandBarButtons.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+
+class AdvancedCommandBarButton
+{
+public:
+ static std::vector> Array;
+
+ static constexpr int MaxButtonCount = 25;
+ static constexpr int InUseButtonCount = 11;
+ static constexpr int UnusedButtonCount = 1;
+ static constexpr int OldButtonCount = InUseButtonCount + UnusedButtonCount;
+
+ static int TotalButtonCount;
+
+ static void RegisterButtons();
+ static ShapeButtonClass* GetShapeButton(const char* name);
+
+ virtual const char* GetName() const R0;
+ virtual const char* GetTipName() const R0;
+ virtual bool CanHoldDown() const R0;
+ virtual void Execute(bool isOn) const RX;
+
+ AdvancedCommandBarButton()
+ {
+ this->ID = TotalButtonCount;
+ TotalButtonCount++;
+ }
+
+ int ID;
+};
diff --git a/src/Commands/Commands.cpp b/src/Commands/Commands.cpp
index 0e5ad329af..84d8acccb0 100644
--- a/src/Commands/Commands.cpp
+++ b/src/Commands/Commands.cpp
@@ -9,16 +9,20 @@
#include "ToggleDigitalDisplay.h"
#include "ToggleDesignatorRange.h"
#include "SaveVariablesToFile.h"
+#include "DistributionMode.h"
#include "ToggleSWSidebar.h"
#include "FireTacticalSW.h"
#include "ToggleMessageList.h"
#include
+#include
#include
#include
#include
+#pragma region HotkeyCommand
+
DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
{
// Load it after Ares'
@@ -44,6 +48,20 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
SWSidebarClass::Commands[9] = MakeCommand>();
}
+ if (Phobos::Config::AllowSwitchNoMoveCommand)
+ MakeCommand();
+
+ if (Phobos::Config::AllowDistributionCommand)
+ {
+ if (Phobos::Config::AllowDistributionCommand_SpreadMode)
+ MakeCommand();
+
+ if (Phobos::Config::AllowDistributionCommand_FilterMode)
+ MakeCommand();
+
+ MakeCommand();
+ }
+
if (Phobos::Config::DevelopmentCommands)
{
MakeCommand();
@@ -61,14 +79,24 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
return 0;
}
+#pragma endregion
+
+#pragma region MouseScroll
+
static void MouseWheelDownCommand()
{
+ if (DistributionModeHoldDownCommandClass::Enabled && Phobos::Config::AllowDistributionCommand_SpreadModeScroll)
+ DistributionModeHoldDownCommandClass::DistributionSpreadModeReduce();
+
if (MessageColumnClass::Instance.IsHovering())
MessageColumnClass::Instance.ScrollDown();
}
static void MouseWheelUpCommand()
{
+ if (DistributionModeHoldDownCommandClass::Enabled && Phobos::Config::AllowDistributionCommand_SpreadModeScroll)
+ DistributionModeHoldDownCommandClass::DistributionSpreadModeExpand();
+
if (MessageColumnClass::Instance.IsHovering())
MessageColumnClass::Instance.ScrollUp();
}
@@ -87,7 +115,8 @@ DEFINE_HOOK(0x777998, Game_WndProc_ScrollMouseWheel, 0x6)
static inline bool CheckSkipScrollSidebar()
{
- return MessageColumnClass::Instance.IsHovering();
+ return DistributionModeHoldDownCommandClass::Enabled
+ || MessageColumnClass::Instance.IsHovering();
}
DEFINE_HOOK(0x533F50, Game_ScrollSidebar_Skip, 0x5)
@@ -95,3 +124,5 @@ DEFINE_HOOK(0x533F50, Game_ScrollSidebar_Skip, 0x5)
enum { SkipScrollSidebar = 0x533FC3 };
return CheckSkipScrollSidebar() ? SkipScrollSidebar : 0;
}
+
+#pragma endregion
diff --git a/src/Commands/DistributionMode.cpp b/src/Commands/DistributionMode.cpp
new file mode 100644
index 0000000000..dd3066e8e1
--- /dev/null
+++ b/src/Commands/DistributionMode.cpp
@@ -0,0 +1,457 @@
+#include "DistributionMode.h"
+
+#include "AdvancedCommandBarButtons.h"
+#include
+#include
+#include
+
+#include
+#include
+
+bool DistributionModeHoldDownCommandClass::Enabled = false;
+bool DistributionModeHoldDownCommandClass::OnMessageShowed = false;
+bool DistributionModeHoldDownCommandClass::OffMessageShowed = false;
+int DistributionModeHoldDownCommandClass::ShowTime = 0;
+
+const char* SwitchNoMoveCommandClass::GetName() const
+{
+ return "Switch No Move Command";
+}
+
+const wchar_t* SwitchNoMoveCommandClass::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_SWITCH_NOMOVE", L"Switch no-move");
+}
+
+const wchar_t* SwitchNoMoveCommandClass::GetUICategory() const
+{
+ return CATEGORY_CONTROL;
+}
+
+const wchar_t* SwitchNoMoveCommandClass::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_SWITCH_NOMOVE_DESC", L"Make unit does not move around the target when receiving no-move command");
+}
+
+void SwitchNoMoveCommandClass::Execute(WWKey eInput) const
+{
+ Phobos::Config::ApplyNoMoveCommand = !Phobos::Config::ApplyNoMoveCommand;
+}
+
+const char* DistributionModeSpreadCommandClass::GetName() const
+{
+ return "Distribution Mode Spread";
+}
+
+const wchar_t* DistributionModeSpreadCommandClass::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_DISTR_SPREAD", L"Distribution spread");
+}
+
+const wchar_t* DistributionModeSpreadCommandClass::GetUICategory() const
+{
+ return CATEGORY_CONTROL;
+}
+
+const wchar_t* DistributionModeSpreadCommandClass::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_DISTR_SPREAD_DESC", L"Automatically and averagely select similar targets around the original target. This is for changing the search range");
+}
+
+void DistributionModeSpreadCommandClass::Execute(WWKey eInput) const
+{
+ Phobos::Config::DistributionSpreadMode = ((Phobos::Config::DistributionSpreadMode + 1) & 3);
+ DistributionModeHoldDownCommandClass::ShowTime = SystemTimer::GetTime();
+}
+
+const char* DistributionModeFilterCommandClass::GetName() const
+{
+ return "Distribution Mode Filter";
+}
+
+const wchar_t* DistributionModeFilterCommandClass::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_DISTR_FILTER", L"Distribution filter");
+}
+
+const wchar_t* DistributionModeFilterCommandClass::GetUICategory() const
+{
+ return CATEGORY_CONTROL;
+}
+
+const wchar_t* DistributionModeFilterCommandClass::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_DISTR_FILTER_DESC", L"Automatically and averagely select similar targets around the original target. This is for changing the filter criteria");
+}
+
+void DistributionModeFilterCommandClass::Execute(WWKey eInput) const
+{
+ Phobos::Config::DistributionFilterMode = ((Phobos::Config::DistributionFilterMode + 1) & 3);
+ DistributionModeHoldDownCommandClass::ShowTime = SystemTimer::GetTime();
+}
+
+const char* DistributionModeHoldDownCommandClass::GetName() const
+{
+ return "Distribution Mode Hold Down";
+}
+
+const wchar_t* DistributionModeHoldDownCommandClass::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_DISTR_HOLDDOWN", L"Distribution hold down");
+}
+
+const wchar_t* DistributionModeHoldDownCommandClass::GetUICategory() const
+{
+ return CATEGORY_CONTROL;
+}
+
+const wchar_t* DistributionModeHoldDownCommandClass::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_DISTR_HOLDDOWN_DESC", L"Automatically and averagely select similar targets around the original target. This is for holding down to toggle on/off");
+}
+
+bool DistributionModeHoldDownCommandClass::ExtraTriggerCondition(WWKey eInput) const
+{
+ return true;
+}
+
+void DistributionModeHoldDownCommandClass::Execute(WWKey eInput) const
+{
+ if (eInput & WWKey::Release)
+ DistributionModeHoldDownCommandClass::DistributionModeOff();
+ else
+ DistributionModeHoldDownCommandClass::DistributionModeOn();
+}
+
+void DistributionModeHoldDownCommandClass::DistributionModeOn()
+{
+ if (DistributionModeHoldDownCommandClass::Enabled)
+ return;
+
+ if (SessionClass::Instance.MultiplayerObserver)
+ return;
+
+ DistributionModeHoldDownCommandClass::Enabled = true;
+
+ if (const auto pButton = AdvancedCommandBarButton::GetShapeButton("DistributionMode"))
+ {
+ if (!pButton->IsOn)
+ pButton->TurnOn();
+ }
+
+ VocClass::PlayGlobal(RulesExt::Global()->StartDistributionModeSound, 0x2000, 1.0);
+
+ if (!DistributionModeHoldDownCommandClass::OnMessageShowed)
+ {
+ DistributionModeHoldDownCommandClass::OnMessageShowed = true;
+ MessageListClass::Instance.PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:DistributionModeOn", L"Distribution mode enabled."), RulesClass::Instance->MessageDelay, HouseClass::CurrentPlayer->ColorSchemeIndex, true);
+ }
+}
+
+void DistributionModeHoldDownCommandClass::DistributionModeOff()
+{
+ if (!DistributionModeHoldDownCommandClass::Enabled)
+ return;
+
+ DistributionModeHoldDownCommandClass::Enabled = false;
+
+ if (const auto pButton = AdvancedCommandBarButton::GetShapeButton("DistributionMode"))
+ {
+ if (pButton->IsOn)
+ pButton->TurnOff();
+ }
+
+ if (SessionClass::Instance.MultiplayerObserver)
+ return;
+
+ VocClass::PlayGlobal(RulesExt::Global()->EndDistributionModeSound, 0x2000, 1.0);
+
+ if (!DistributionModeHoldDownCommandClass::OffMessageShowed)
+ {
+ DistributionModeHoldDownCommandClass::OffMessageShowed = true;
+ MessageListClass::Instance.PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:DistributionModeOff", L"Distribution mode unabled."), RulesClass::Instance->MessageDelay, HouseClass::CurrentPlayer->ColorSchemeIndex, true);
+ }
+}
+
+void DistributionModeHoldDownCommandClass::DistributionSpreadModeExpand()
+{
+ Phobos::Config::DistributionSpreadMode = std::min(3, Phobos::Config::DistributionSpreadMode + 1);
+}
+
+void DistributionModeHoldDownCommandClass::DistributionSpreadModeReduce()
+{
+ Phobos::Config::DistributionSpreadMode = std::max(0, Phobos::Config::DistributionSpreadMode - 1);
+}
+
+void __fastcall DistributionModeHoldDownCommandClass::ClickedWaypoint(ObjectClass* pSelect, int idxPath, signed char idxWP)
+{
+ pSelect->AssignPlanningPath(idxPath, idxWP);
+
+ if (const auto pFoot = abstract_cast(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(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(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> 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],[¢er](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(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(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(spreadMode ? (2 << spreadMode) : 0.5), false, true);
+ }
+
+ return 0;
+}
+
+const char* DistributionModeHoldDownButtonClass::GetName() const
+{
+ return "DistributionMode";
+}
+
+const char* DistributionModeHoldDownButtonClass::GetTipName() const
+{
+ return "Tip:DistributionMode";
+}
+
+bool DistributionModeHoldDownButtonClass::CanHoldDown() const
+{
+ return true;
+}
+
+void DistributionModeHoldDownButtonClass::Execute(bool isOn) const
+{
+ if (isOn)
+ DistributionModeHoldDownCommandClass::DistributionModeOn();
+ else
+ DistributionModeHoldDownCommandClass::DistributionModeOff();
+}
+
+/*
+TODO
+- More flexible range adjustment
+- Drag mouse to adjust the range
+- Target highlight within the range
+*/
diff --git a/src/Commands/DistributionMode.h b/src/Commands/DistributionMode.h
new file mode 100644
index 0000000000..dccedafa9c
--- /dev/null
+++ b/src/Commands/DistributionMode.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "Commands.h"
+
+#include
+
+class SwitchNoMoveCommandClass : public CommandClass
+{
+public:
+ virtual const char* GetName() const override;
+ virtual const wchar_t* GetUIName() const override;
+ virtual const wchar_t* GetUICategory() const override;
+ virtual const wchar_t* GetUIDescription() const override;
+ virtual void Execute(WWKey eInput) const override;
+};
+
+class DistributionModeSpreadCommandClass : public CommandClass
+{
+public:
+ virtual const char* GetName() const override;
+ virtual const wchar_t* GetUIName() const override;
+ virtual const wchar_t* GetUICategory() const override;
+ virtual const wchar_t* GetUIDescription() const override;
+ virtual void Execute(WWKey eInput) const override;
+};
+
+class DistributionModeFilterCommandClass : public CommandClass
+{
+public:
+ virtual const char* GetName() const override;
+ virtual const wchar_t* GetUIName() const override;
+ virtual const wchar_t* GetUICategory() const override;
+ virtual const wchar_t* GetUIDescription() const override;
+ virtual void Execute(WWKey eInput) const override;
+};
+
+class DistributionModeHoldDownCommandClass : public CommandClass
+{
+public:
+ static bool Enabled;
+ static bool OnMessageShowed;
+ static bool OffMessageShowed;
+ static int ShowTime;
+
+ virtual const char* GetName() const override;
+ virtual const wchar_t* GetUIName() const override;
+ virtual const wchar_t* GetUICategory() const override;
+ virtual const wchar_t* GetUIDescription() const override;
+ virtual bool ExtraTriggerCondition(WWKey eInput) const override;
+ virtual void Execute(WWKey eInput) const override;
+
+ static void DistributionModeOn();
+ static void DistributionModeOff();
+ static void DistributionSpreadModeExpand();
+ static void DistributionSpreadModeReduce();
+
+ static void __fastcall ClickedWaypoint(ObjectClass* pSelect, int idxPath, signed char idxWP);
+ static void __fastcall ClickedTargetAction(ObjectClass* pSelect, Action action, ObjectClass* pTarget);
+ static void __fastcall ClickedCellAction(ObjectClass* pSelect, Action action, CellStruct* pCell, CellStruct* pSecondCell);
+ static void __fastcall AreaGuardAction(TechnoClass* pTechno);
+};
+
+class DistributionModeHoldDownButtonClass : public AdvancedCommandBarButton
+{
+ virtual const char* GetName() const override;
+ virtual const char* GetTipName() const override;
+ virtual bool CanHoldDown() const override;
+ virtual void Execute(bool isOn) const override;
+};
diff --git a/src/Commands/Dummy.cpp b/src/Commands/Dummy.cpp
index 67a2c3a021..adcfaad058 100644
--- a/src/Commands/Dummy.cpp
+++ b/src/Commands/Dummy.cpp
@@ -27,3 +27,32 @@ void DummyCommandClass::Execute(WWKey eInput) const
Debug::Log("[Phobos] Dummy command runs.\n");
MessageListClass::Instance.PrintMessage(L"[Phobos] Dummy command runs.");
}
+
+const char* DummyButtonClass::GetName() const
+{
+ return "PhobosDummy";
+}
+
+const char* DummyButtonClass::GetTipName() const
+{
+ return "Phobos Dummy Tool Tip";
+}
+
+bool DummyButtonClass::CanHoldDown() const
+{
+ return true;
+}
+
+void DummyButtonClass::Execute(bool isOn) const
+{
+ if (isOn)
+ {
+ Debug::Log("[Phobos] Dummy button toggle on.\n");
+ MessageListClass::Instance.PrintMessage(L"[Phobos] Dummy button toggle on.");
+ }
+ else
+ {
+ Debug::Log("[Phobos] Dummy button toggle off.\n");
+ MessageListClass::Instance.PrintMessage(L"[Phobos] Dummy button toggle off.");
+ }
+}
diff --git a/src/Commands/Dummy.h b/src/Commands/Dummy.h
index c0527e9c45..1e34d444d0 100644
--- a/src/Commands/Dummy.h
+++ b/src/Commands/Dummy.h
@@ -13,3 +13,11 @@ class DummyCommandClass : public CommandClass
virtual const wchar_t* GetUIDescription() const override;
virtual void Execute(WWKey eInput) const override;
};
+
+class DummyButtonClass : public AdvancedCommandBarButton
+{
+ virtual const char* GetName() const override;
+ virtual const char* GetTipName() const override;
+ virtual bool CanHoldDown() const override;
+ virtual void Execute(bool isOn) const override;
+};
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index 78ef8cf566..36a97fb5d7 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -264,6 +264,10 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->CombatAlert_UseAttackVoice.Read(exINI, GameStrings::AudioVisual, "CombatAlert.UseAttackVoice");
this->CombatAlert_UseEVA.Read(exINI, GameStrings::AudioVisual, "CombatAlert.UseEVA");
+ this->StartDistributionModeSound.Read(exINI, GameStrings::AudioVisual, "StartDistributionModeSound");
+ this->EndDistributionModeSound.Read(exINI, GameStrings::AudioVisual, "EndDistributionModeSound");
+ this->AddDistributionModeCommandSound.Read(exINI, GameStrings::AudioVisual, "AddDistributionModeCommandSound");
+
this->ReplaceVoxelLightSources();
this->UseFixedVoxelLighting.Read(exINI, GameStrings::AudioVisual, "UseFixedVoxelLighting");
@@ -561,6 +565,9 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->CombatAlert_UseFeedbackVoice)
.Process(this->CombatAlert_UseAttackVoice)
.Process(this->CombatAlert_UseEVA)
+ .Process(this->StartDistributionModeSound)
+ .Process(this->EndDistributionModeSound)
+ .Process(this->AddDistributionModeCommandSound)
.Process(this->UseFixedVoxelLighting)
.Process(this->AIAutoDeployMCV)
.Process(this->AISetBaseCenter)
diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h
index 629192e822..1ca71fa6fa 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -206,6 +206,10 @@ class RulesExt
Valueable CombatAlert_UseAttackVoice;
Valueable CombatAlert_UseEVA;
+ ValueableIdx StartDistributionModeSound;
+ ValueableIdx EndDistributionModeSound;
+ ValueableIdx AddDistributionModeCommandSound;
+
Nullable> VoxelLightSource;
// Nullable> VoxelShadowLightSource;
Valueable UseFixedVoxelLighting;
@@ -447,6 +451,9 @@ class RulesExt
, CombatAlert_UseFeedbackVoice { true }
, CombatAlert_UseAttackVoice { true }
, CombatAlert_UseEVA { true }
+ , StartDistributionModeSound { -1 }
+ , EndDistributionModeSound { -1 }
+ , AddDistributionModeCommandSound { -1 }
, UseFixedVoxelLighting { false }
, AIAutoDeployMCV { true }
, AISetBaseCenter { true }
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index faa10a6775..8c7ca7bf34 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -1052,6 +1052,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
// Ares 2.0
this->Passengers_BySize.Read(exINI, pSection, "Passengers.BySize");
+ this->FakeOf.Read(exINI, pSection, "FakeOf");
if (pThis->Gunner)
{
@@ -1442,6 +1443,7 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm)
.Process(this->DeployingAnim_UseUnitDrawer)
.Process(this->EnemyUIName)
+ .Process(this->FakeOf)
.Process(this->ForceWeapon_Check)
.Process(this->ForceWeapon_Naval_Decloaked)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index 35c5ac4966..44e8f22b11 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -183,6 +183,7 @@ class TechnoTypeExt
Valueable DeployingAnim_UseUnitDrawer;
Valueable EnemyUIName;
+ Valueable FakeOf;
bool ForceWeapon_Check;
Valueable ForceWeapon_Naval_Decloaked;
@@ -585,6 +586,7 @@ class TechnoTypeExt
, CombatAlert_EVA {}
, EnemyUIName {}
+ , FakeOf {}
, VoiceCreated {}
, VoicePickup {}
diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp
index 270d8e9770..e4b1868c70 100644
--- a/src/Phobos.INI.cpp
+++ b/src/Phobos.INI.cpp
@@ -74,6 +74,16 @@ bool Phobos::Config::HideLaserTrailEffects = true;
bool Phobos::Config::HideShakeEffects = true;
bool Phobos::Config::ShowFlashOnSelecting = false;
bool Phobos::Config::UnitPowerDrain = false;
+bool Phobos::Config::AllowSwitchNoMoveCommand = false;
+bool Phobos::Config::AllowDistributionCommand = false;
+bool Phobos::Config::AllowDistributionCommand_SpreadMode = true;
+bool Phobos::Config::AllowDistributionCommand_SpreadModeScroll = true;
+bool Phobos::Config::AllowDistributionCommand_FilterMode = true;
+bool Phobos::Config::AllowDistributionCommand_AffectsAllies = true;
+bool Phobos::Config::AllowDistributionCommand_AffectsEnemies = true;
+bool Phobos::Config::ApplyNoMoveCommand = true;
+int Phobos::Config::DistributionSpreadMode = 2;
+int Phobos::Config::DistributionFilterMode = 2;
int Phobos::Config::SuperWeaponSidebar_RequiredSignificance = 0;
bool Phobos::Misc::CustomGS = false;
@@ -109,6 +119,12 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
Phobos::Config::ShowFlashOnSelecting = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowFlashOnSelecting", false);
Phobos::Config::SuperWeaponSidebar_RequiredSignificance = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "SuperWeaponSidebar.RequiredSignificance", 0);
+ Phobos::Config::ApplyNoMoveCommand = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "DefaultApplyNoMoveCommand", true);
+ Phobos::Config::DistributionSpreadMode = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "DefaultDistributionSpreadMode", 2);
+ Phobos::Config::DistributionSpreadMode = std::clamp(Phobos::Config::DistributionSpreadMode, 0, 3);
+ Phobos::Config::DistributionFilterMode = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "DefaultDistributionFilterMode", 2);
+ Phobos::Config::DistributionFilterMode = std::clamp(Phobos::Config::DistributionFilterMode, 0, 3);
+
// Custom game speeds, 6 - i so that GS6 is index 0, just like in the engine
Phobos::Config::CampaignDefaultGameSpeed = 6 - CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "CampaignDefaultGameSpeed", 4);
if (Phobos::Config::CampaignDefaultGameSpeed > 6 || Phobos::Config::CampaignDefaultGameSpeed < 0)
@@ -285,5 +301,13 @@ DEFINE_HOOK(0x52D21F, InitRules_ThingsThatShouldntBeSerailized, 0x6)
Phobos::Config::SuperWeaponSidebarCommands = pINI_RULESMD->ReadBool("GlobalControls", "SuperWeaponSidebarKeysEnabled", Phobos::Config::SuperWeaponSidebarCommands);
Phobos::Config::ShowPlanningPath = pINI_RULESMD->ReadBool("GlobalControls", "DebugPlanningPaths", Phobos::Config::ShowPlanningPath);
+ Phobos::Config::AllowSwitchNoMoveCommand = pINI_RULESMD->ReadBool("GlobalControls", "AllowSwitchNoMoveCommand", Phobos::Config::AllowDistributionCommand);
+ Phobos::Config::AllowDistributionCommand = pINI_RULESMD->ReadBool("GlobalControls", "AllowDistributionCommand", Phobos::Config::AllowDistributionCommand);
+ Phobos::Config::AllowDistributionCommand_SpreadMode = pINI_RULESMD->ReadBool("GlobalControls", "AllowDistributionCommand.SpreadMode", Phobos::Config::AllowDistributionCommand_SpreadMode);
+ Phobos::Config::AllowDistributionCommand_SpreadModeScroll = pINI_RULESMD->ReadBool("GlobalControls", "AllowDistributionCommand.SpreadModeScroll", Phobos::Config::AllowDistributionCommand_SpreadModeScroll);
+ Phobos::Config::AllowDistributionCommand_FilterMode = pINI_RULESMD->ReadBool("GlobalControls", "AllowDistributionCommand.FilterMode", Phobos::Config::AllowDistributionCommand_FilterMode);
+ Phobos::Config::AllowDistributionCommand_AffectsAllies = pINI_RULESMD->ReadBool("GlobalControls", "AllowDistributionCommand.AffectsAllies", Phobos::Config::AllowDistributionCommand_AffectsAllies);
+ Phobos::Config::AllowDistributionCommand_AffectsEnemies = pINI_RULESMD->ReadBool("GlobalControls", "AllowDistributionCommand.AffectsEnemies", Phobos::Config::AllowDistributionCommand_AffectsEnemies);
+
return 0;
}
diff --git a/src/Phobos.h b/src/Phobos.h
index e925be2e77..3c49f0cb37 100644
--- a/src/Phobos.h
+++ b/src/Phobos.h
@@ -110,6 +110,16 @@ class Phobos
static bool HideShakeEffects;
static bool ShowFlashOnSelecting;
static bool UnitPowerDrain;
+ static bool AllowSwitchNoMoveCommand;
+ static bool AllowDistributionCommand;
+ static bool AllowDistributionCommand_SpreadMode;
+ static bool AllowDistributionCommand_SpreadModeScroll;
+ static bool AllowDistributionCommand_FilterMode;
+ static bool AllowDistributionCommand_AffectsAllies;
+ static bool AllowDistributionCommand_AffectsEnemies;
+ static bool ApplyNoMoveCommand;
+ static int DistributionSpreadMode;
+ static int DistributionFilterMode;
static int SuperWeaponSidebar_RequiredSignificance;
};