diff --git a/CREDITS.md b/CREDITS.md
index 234911d92d..19f9413ad5 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -555,6 +555,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 cd41a8a686..0e609984fc 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -58,6 +58,7 @@
+
@@ -242,6 +243,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 a7056c4d12..bfce9074ad 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 502c2eadab..768047d2e7 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -457,6 +457,7 @@ New:
- Fast access structure (by FlyStar)
- Toggle off laser trail and shake effects (by Ollerus)
- [Dehardcode the `ZAdjust` of warhead anim](Fixed-or-Improved-Logics.md#dehardcode-the-zadjust-of-warhead-anim) (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/Commands.cpp b/src/Commands/Commands.cpp
index 0e5ad329af..2324a51f33 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,83 @@ DEFINE_HOOK(0x533F50, Game_ScrollSidebar_Skip, 0x5)
enum { SkipScrollSidebar = 0x533FC3 };
return CheckSkipScrollSidebar() ? SkipScrollSidebar : 0;
}
+
+#pragma endregion
+
+#pragma region ShapeButton
+
+int ShapeButtonHelper::NewButtonIndexes[ShapeButtonHelper::NewButtonCount] =
+{
+ -1 // DistributionMode
+ // New button initialize here
+};
+
+DEFINE_HOOK(0x6CFD08, ShapeButtonClass_FindIndex_FindNewButton, 0x5)
+{
+ enum { SetButtonIndex = 0x6CFD0D };
+
+ GET(const char*, name, ECX);
+
+ for (int i = 0; i < ShapeButtonHelper::NewButtonCount; ++i)
+ {
+ if (_strcmpi(name, ShapeButtonHelper::NewButtonNames[i]) == 0)
+ {
+ R->EAX(i + ShapeButtonHelper::OldButtonCount);
+ return SetButtonIndex;
+ }
+ }
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D0233, TabClass_Init_InitNewButtonIndex, 0x6)
+{
+ for (int i = 0; i < ShapeButtonHelper::NewButtonCount; ++i)
+ ShapeButtonHelper::NewButtonIndexes[i] = ShapeButtonClass::FindIndex(ShapeButtonHelper::NewButtonNames[i]);
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D0827, TabClass_Update_UpdateNewButton, 0x6)
+{
+ GET(const int, index, EAX);
+
+ if (ShapeButtonHelper::NewButtonIndexes[0] == index)
+ {
+ if (ShapeButtonClass::GetButton(index)->IsOn)
+ DistributionModeHoldDownCommandClass::DistributionModeOn();
+ else
+ DistributionModeHoldDownCommandClass::DistributionModeOff();
+ }
+
+ // New button trigger here
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D10DF, TabClass_InitButtonIO_InitNewHoldDownButton, 0x6)
+{
+ const int distributionModeButtonIndex = ShapeButtonHelper::NewButtonIndexes[0];
+
+ if (distributionModeButtonIndex != -1)
+ {
+ if (const auto pButton = ShapeButtonClass::GetButton(distributionModeButtonIndex))
+ {
+ // Clicking the button is different from holding down the hotkey
+ pButton->ToggleType = 1;
+ pButton->UseFlash = true;
+ }
+ }
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6D14DD, TabClass_InitToolTip_InitNewButtonToolTip, 0x5)
+{
+ for (int i = 0; i < ShapeButtonHelper::NewButtonCount; ++i)
+ ShapeButtonClass::SetToolTip(ShapeButtonClass::GetButton(ShapeButtonHelper::NewButtonIndexes[i]), ShapeButtonHelper::NewButtonTipNames[i]);
+
+ return 0;
+}
+
+#pragma endregion
diff --git a/src/Commands/Commands.h b/src/Commands/Commands.h
index ae3c89f503..286eef89c8 100644
--- a/src/Commands/Commands.h
+++ b/src/Commands/Commands.h
@@ -14,6 +14,39 @@ T* MakeCommand()
return command;
};
+class ShapeButtonHelper
+{
+public:
+ static constexpr int MaxButtonCount = 25;
+ static constexpr int InUseButtonCount = 11;
+ static constexpr int UnusedButtonCount = 1;
+ static constexpr int OldButtonCount = InUseButtonCount + UnusedButtonCount;
+ static constexpr int NewButtonCount = std::min(1, (MaxButtonCount - OldButtonCount));
+ // 1. Team01
+ // 2. Team02
+ // 3. Team03
+ // 4. TypeSelect
+ // 5. Deploy
+ // 6. AttackMove
+ // 7. Guard
+ // 8. Beacon
+ // 9. Stop
+ // 10. PlanningMode
+ // 11. Cheer
+ // 12. MoveToDeploy
+ static constexpr const char* NewButtonNames[NewButtonCount] =
+ {
+ /* 13. */ "DistributionMode"
+ /* New button name here */
+ };
+ static constexpr const char* NewButtonTipNames[NewButtonCount] =
+ {
+ "Tip:DistributionMode"
+ // New button tip here
+ };
+ static int NewButtonIndexes[NewButtonCount];
+};
+
#define CATEGORY_TEAM StringTable::LoadString(GameStrings::TXT_TEAM)
#define CATEGORY_INTERFACE StringTable::LoadString(GameStrings::TXT_INTERFACE)
#define CATEGORY_TAUNT StringTable::LoadString(GameStrings::TXT_TAUNT)
diff --git a/src/Commands/DistributionMode.cpp b/src/Commands/DistributionMode.cpp
new file mode 100644
index 0000000000..a86bc5566c
--- /dev/null
+++ b/src/Commands/DistributionMode.cpp
@@ -0,0 +1,433 @@
+#include "DistributionMode.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 = reinterpret_cast(0x6CFD40)(ShapeButtonHelper::NewButtonIndexes[0]))
+ {
+ 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 = reinterpret_cast(0x6CFD40)(ShapeButtonHelper::NewButtonIndexes[0]))
+ {
+ 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;
+}
+
+/*
+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..fb21391e77
--- /dev/null
+++ b/src/Commands/DistributionMode.h
@@ -0,0 +1,61 @@
+#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);
+};
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index 98c1ee65cb..4ad9881715 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -260,6 +260,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");
@@ -551,6 +555,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 60e4682e51..f7c8e4c2b1 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -202,6 +202,10 @@ class RulesExt
Valueable CombatAlert_UseAttackVoice;
Valueable CombatAlert_UseEVA;
+ ValueableIdx StartDistributionModeSound;
+ ValueableIdx EndDistributionModeSound;
+ ValueableIdx AddDistributionModeCommandSound;
+
Nullable> VoxelLightSource;
// Nullable> VoxelShadowLightSource;
Valueable UseFixedVoxelLighting;
@@ -432,6 +436,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 cf59f3baac..91bc079c08 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -1023,6 +1023,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");
char tempBuffer[40];
@@ -1410,6 +1411,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 efe25a53e1..a20dbe1adc 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -178,6 +178,7 @@ class TechnoTypeExt
Valueable DeployingAnim_UseUnitDrawer;
Valueable EnemyUIName;
+ Valueable FakeOf;
bool ForceWeapon_Check;
Valueable ForceWeapon_Naval_Decloaked;
@@ -569,6 +570,7 @@ class TechnoTypeExt
, CombatAlert_EVA {}
, EnemyUIName {}
+ , FakeOf {}
, VoiceCreated {}
, VoicePickup {}
diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp
index ffded10550..77cb070620 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;
};