diff --git a/CREDITS.md b/CREDITS.md
index a8c1c4742f..42e696c651 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -399,6 +399,7 @@ This page lists all the individual contributions to the project by their author.
- Fix the bug that ships can travel on elevated bridges
- Original `Arcing` elevation inaccuracy fix
- Fix the bug that uncontrolled scatter when elite techno attacked by aircraft or some unit try crush it
+ - Exclusive SuperWeapon Sidebar
- **Apollo** - Translucent SHP drawing patches
- **ststl**:
- Customizable `ShowTimer` priority of superweapons
@@ -473,6 +474,7 @@ This page lists all the individual contributions to the project by their author.
- Aggressive attack move mission
- Amphibious access vehicle
- Fix an issue that spawned `Strafe` aircraft on aircraft carriers may not be able to return normally if aircraft carriers moved a short distance when the aircraft is landing
+ - Exclusive SuperWeapon Sidebar
- **Ollerus**:
- Build limit group enhancement
- Customizable rocker amplitude
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 9085e61db9..6d5a59f83c 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -23,6 +23,11 @@
+
+
+
+
+
@@ -200,6 +205,12 @@
+
+
+
+
+
+
diff --git a/docs/User-Interface.md b/docs/User-Interface.md
index a07fd43cbc..4cd348e580 100644
--- a/docs/User-Interface.md
+++ b/docs/User-Interface.md
@@ -678,3 +678,58 @@ In `RA2MD.INI`:
[Phobos]
ToolTipBlur=false ; boolean, whether the blur effect of tooltips will be enabled.
```
+
+### Exclusive SuperWeapon Sidebar
+
+
+
+
+- It is possible to put sw cameos on the left of screen like C&C3 when `SuperWeaponSidebar` is true. Cameos arranged in a pyramid shape. In theory, it should be compatible with Ares.
+ - `SuperWeaponSidebar.Interval` controls the distance between two column cameos (excluding the background). When you need to make a background, the width of the background should be (`SuperWeaponSidebar.Interval` + cameo fixed width 60).
+ - `SuperWeaponSidebar.LeftOffset` controls the distance between the left side of cameo and the left side of its column (background). This will not be greater than `SuperWeaponSidebar.Interval`.
+ - `SuperWeaponSidebar.CameoHeight` controls the distance from the top of the previous cameo to the top of the next cameo. That is, the space between the upper and lower cameos is (`SuperWeaponSidebar.CameoHeight` - cameo fixed height 48). This will not be less than 48. When you need to make a background, this is the height of the background.
+ - `SuperWeaponSidebar.Max` controls the maximum number of cameos on the leftmost column, which also depends on the current game resolution.
+ - `SuperWeaponSidebar.MaxColumns` controls that maximum count of columns.
+ - Only sw with `SuperWeaponSidebar.Significance` not lower than `SuperWeaponSidebar.RequiredSignificance` are allowed to be added to the sw sidebar.
+- `SuperWeaponSidebarKeysEnabled` should be true that you can use hotkeys about superweapon sidebar.
+- You can also launch first 10 SW by hotkey in INTERFACE category.
+ - For localization of hotkey, add `TXT_FIRE_TACTICAL_SW_XX`, `TXT_FIRE_TACTICAL_SW_XX_DESC`, `TXT_TOGGLE_SW_SIDEBAR` and `TXT_TOGGLE_SW_SIDEBAR_DESC` into your `.csf` file.
+
+In `uimd.ini`:
+```ini
+[Sidebar]
+SuperWeaponSidebar=false ; boolean
+SuperWeaponSidebar.Interval=0 ; integer, pixels
+SuperWeaponSidebar.LeftOffset=0 ; integer, pixels
+SuperWeaponSidebar.CameoHeight=48 ; integer, pixels
+SuperWeaponSidebar.Max=0 ; integer
+SuperWeaponSidebar.MaxColumns= ; integer
+```
+
+In `rulesmd.ini`
+```ini
+[GlobalControls]
+SuperWeaponSidebarKeysEnabled=false ; boolean
+
+[AudioVisual]
+SuperWeaponSidebar.AllowByDefault=false ; boolean
+
+[SOMESIDE]
+SuperWeaponSidebar.OnPCX= ; filename - including the .pcx extension
+SuperWeaponSidebar.OffPCX= ; filename - including the .pcx extension
+SuperWeaponSidebar.TopPCX= ; filename - including the .pcx extension
+SuperWeaponSidebar.CenterPCX= ; filename - including the .pcx extension
+SuperWeaponSidebar.BottomPCX= ; filename - including the .pcx extension
+
+[SOMESW]
+SuperWeaponSidebar.Allow= ; boolean
+SuperWeaponSidebar.PriorityHouses= ; list of house types
+SuperWeaponSidebar.RequiredHouses= ; list of house types
+SuperWeaponSidebar.Significance=0 ; integer
+```
+
+In `ra2md.ini`
+```ini
+[Phobos]
+SuperWeaponSidebar.RequiredSignificance=0 ; integer
+```
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 67ad42dd39..15edfdcbe5 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -381,6 +381,7 @@ New:
- Dehardcoded 255 limit of `OverlayType` (by secsome)
- [Customizable airstrike flare colors](Fixed-or-Improved-Logics.md#airstrike-flare-customizations) (by Starkku)
- Allowed player's self-healing effects to be benefited by allied or `PlayerControl=true` houses (by Ollerus)
+- Exclusive SuperWeapon Sidebar (by NetsuNegi & 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/docs/_static/images/sw_sidebar-01-on.png b/docs/_static/images/sw_sidebar-01-on.png
new file mode 100644
index 0000000000..81fd868162
Binary files /dev/null and b/docs/_static/images/sw_sidebar-01-on.png differ
diff --git a/docs/_static/images/sw_sidebar-02-on.png b/docs/_static/images/sw_sidebar-02-on.png
new file mode 100644
index 0000000000..db96e181c2
Binary files /dev/null and b/docs/_static/images/sw_sidebar-02-on.png differ
diff --git a/src/Commands/Commands.cpp b/src/Commands/Commands.cpp
index 712a596467..8d9d2611a1 100644
--- a/src/Commands/Commands.cpp
+++ b/src/Commands/Commands.cpp
@@ -10,6 +10,9 @@
#include "ToggleDigitalDisplay.h"
#include "ToggleDesignatorRange.h"
#include "SaveVariablesToFile.h"
+#include "ToggleSWSidebar.h"
+#include "FireTacticalSW.h"
+#include
DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
{
@@ -19,6 +22,21 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
MakeCommand();
MakeCommand();
MakeCommand();
+ MakeCommand();
+
+ if (Phobos::Config::SuperWeaponSidebarCommands)
+ {
+ SWSidebarClass::Commands[0] = MakeCommand>();
+ SWSidebarClass::Commands[1] = MakeCommand>();
+ SWSidebarClass::Commands[2] = MakeCommand>();
+ SWSidebarClass::Commands[3] = MakeCommand>();
+ SWSidebarClass::Commands[4] = MakeCommand>();
+ SWSidebarClass::Commands[5] = MakeCommand>();
+ SWSidebarClass::Commands[6] = MakeCommand>();
+ SWSidebarClass::Commands[7] = MakeCommand>();
+ SWSidebarClass::Commands[8] = MakeCommand>();
+ SWSidebarClass::Commands[9] = MakeCommand>();
+ }
if (Phobos::Config::DevelopmentCommands)
{
diff --git a/src/Commands/Commands.h b/src/Commands/Commands.h
index 9b66ed02f1..ae3c89f503 100644
--- a/src/Commands/Commands.h
+++ b/src/Commands/Commands.h
@@ -7,10 +7,11 @@
#include
template
-void MakeCommand()
+T* MakeCommand()
{
T* command = GameCreate();
CommandClass::Array.AddItem(command);
+ return command;
};
#define CATEGORY_TEAM StringTable::LoadString(GameStrings::TXT_TEAM)
diff --git a/src/Commands/FireTacticalSW.h b/src/Commands/FireTacticalSW.h
new file mode 100644
index 0000000000..e4777a2dd2
--- /dev/null
+++ b/src/Commands/FireTacticalSW.h
@@ -0,0 +1,61 @@
+#pragma once
+#include "Commands.h"
+
+#include
+#include
+
+template
+class FireTacticalSWCommandClass : public CommandClass
+{
+ 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;
+};
+
+template
+inline const char* FireTacticalSWCommandClass::GetName() const
+{
+ _snprintf_s(Phobos::readBuffer, Phobos::readLength, "FireTacticalSW%d", Index + 1);
+ return Phobos::readBuffer;
+}
+
+template
+inline const wchar_t* FireTacticalSWCommandClass::GetUIName() const
+{
+ const wchar_t* csfString = StringTable::TryFetchString("TXT_FIRE_TACTICAL_SW_XX", L"Fire Super Weapon %d");
+ _snwprintf_s(Phobos::wideBuffer, std::size(Phobos::wideBuffer), csfString, Index + 1);
+ return Phobos::wideBuffer;
+}
+
+template
+inline const wchar_t* FireTacticalSWCommandClass::GetUICategory() const
+{
+ return CATEGORY_INTERFACE;
+}
+
+template
+inline const wchar_t* FireTacticalSWCommandClass::GetUIDescription() const
+{
+ const wchar_t* csfString = StringTable::TryFetchString("TXT_FIRE_TACTICAL_SW_XX_DESC", L"Fires the Super Weapon at position %d in the Super Weapon sidebar.");
+ _snwprintf_s(Phobos::wideBuffer, std::size(Phobos::wideBuffer), csfString, Index + 1);
+ return Phobos::wideBuffer;
+}
+
+template
+inline void FireTacticalSWCommandClass::Execute(WWKey eInput) const
+{
+ if (!SWSidebarClass::IsEnabled())
+ return;
+
+ const auto& columns = SWSidebarClass::Instance.Columns;
+
+ if (columns.empty())
+ return;
+
+ const auto& buttons = columns.front()->Buttons;
+
+ if (Index < buttons.size())
+ buttons[Index]->LaunchSuper();
+}
diff --git a/src/Commands/ToggleSWSidebar.cpp b/src/Commands/ToggleSWSidebar.cpp
new file mode 100644
index 0000000000..49369828c7
--- /dev/null
+++ b/src/Commands/ToggleSWSidebar.cpp
@@ -0,0 +1,33 @@
+#include "ToggleSWSidebar.h"
+#include
+
+#include
+#include
+
+const char* ToggleSWSidebar::GetName() const
+{
+ return "Toggle Super Weapon Sidebar";
+}
+
+const wchar_t* ToggleSWSidebar::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_TOGGLE_SW_SIDEBAR", L"Toggle Super Weapon Sidebar");
+}
+
+const wchar_t* ToggleSWSidebar::GetUICategory() const
+{
+ return CATEGORY_INTERFACE;
+}
+
+const wchar_t* ToggleSWSidebar::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_TOGGLE_SW_SIDEBAR_DESC", L"Toggle the Super Weapon Sidebar.");
+}
+
+void ToggleSWSidebar::Execute(WWKey eInput) const
+{
+ ToggleSWButtonClass::SwitchSidebar();
+
+ if (SWSidebarClass::Instance.CurrentColumn)
+ MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false);
+}
diff --git a/src/Commands/ToggleSWSidebar.h b/src/Commands/ToggleSWSidebar.h
new file mode 100644
index 0000000000..e12ac13364
--- /dev/null
+++ b/src/Commands/ToggleSWSidebar.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "Commands.h"
+
+// Display damage strings
+class ToggleSWSidebar : 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;
+};
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index 00ad26ccd7..d824e9cd86 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -105,6 +105,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->PlacementPreview.Read(exINI, GameStrings::AudioVisual, "PlacementPreview");
this->PlacementPreview_Translucency.Read(exINI, GameStrings::AudioVisual, "PlacementPreview.Translucency");
+ this->SuperWeaponSidebar_AllowByDefault.Read(exINI, GameStrings::AudioVisual, "SuperWeaponSidebar.AllowByDefault");
+
this->ConditionYellow_Terrain.Read(exINI, GameStrings::AudioVisual, "ConditionYellow.Terrain");
this->Shield_ConditionYellow.Read(exINI, GameStrings::AudioVisual, "Shield.ConditionYellow");
this->Shield_ConditionRed.Read(exINI, GameStrings::AudioVisual, "Shield.ConditionRed");
@@ -370,6 +372,7 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->PlacementGrid_TranslucencyWithPreview)
.Process(this->PlacementPreview)
.Process(this->PlacementPreview_Translucency)
+ .Process(this->SuperWeaponSidebar_AllowByDefault)
.Process(this->ConditionYellow_Terrain)
.Process(this->Shield_ConditionYellow)
.Process(this->Shield_ConditionRed)
diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h
index d3d9c1a7c3..6492f09b32 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -69,6 +69,8 @@ class RulesExt
Valueable PlacementPreview;
TranslucencyLevel PlacementPreview_Translucency;
+ Valueable SuperWeaponSidebar_AllowByDefault;
+
Nullable ConditionYellow_Terrain;
Nullable Shield_ConditionYellow;
Nullable Shield_ConditionRed;
@@ -259,6 +261,8 @@ class RulesExt
, PlacementPreview { false }
, PlacementPreview_Translucency { 75 }
+ , SuperWeaponSidebar_AllowByDefault { false }
+
, Shield_ConditionYellow { }
, Shield_ConditionRed { }
, Pips_Shield_Background { }
diff --git a/src/Ext/SWType/Body.cpp b/src/Ext/SWType/Body.cpp
index c009999ab2..a937ed5e6f 100644
--- a/src/Ext/SWType/Body.cpp
+++ b/src/Ext/SWType/Body.cpp
@@ -6,6 +6,14 @@
SWTypeExt::ExtContainer SWTypeExt::ExtMap;
+void SWTypeExt::ExtData::Initialize()
+{
+ this->EVA_InsufficientFunds = VoxClass::FindIndex(GameStrings::EVA_InsufficientFunds);
+ this->EVA_SelectTarget = VoxClass::FindIndex("EVA_SelectTarget");
+
+ this->Message_CannotFire = CSFText("MSG:CannotFire");
+}
+
// =============================
// load / save
@@ -15,6 +23,14 @@ void SWTypeExt::ExtData::Serialize(T& Stm)
Stm
.Process(this->TypeID)
.Process(this->Money_Amount)
+ .Process(this->EVA_Impatient)
+ .Process(this->EVA_InsufficientFunds)
+ .Process(this->EVA_SelectTarget)
+ .Process(this->SW_UseAITargeting)
+ .Process(this->SW_AutoFire)
+ .Process(this->SW_ManualFire)
+ .Process(this->SW_ShowCameo)
+ .Process(this->SW_Unstoppable)
.Process(this->SW_Inhibitors)
.Process(this->SW_AnyInhibitor)
.Process(this->SW_Designators)
@@ -29,6 +45,10 @@ void SWTypeExt::ExtData::Serialize(T& Stm)
.Process(this->SW_PostDependent)
.Process(this->SW_MaxCount)
.Process(this->SW_Shots)
+ .Process(this->Message_CannotFire)
+ .Process(this->Message_InsufficientFunds)
+ .Process(this->Message_ColorScheme)
+ .Process(this->Message_FirerColor)
.Process(this->UIDescription)
.Process(this->CameoPriority)
.Process(this->LimboDelivery_Types)
@@ -53,6 +73,12 @@ void SWTypeExt::ExtData::Serialize(T& Stm)
.Process(this->Convert_Pairs)
.Process(this->ShowDesignatorRange)
.Process(this->TabIndex)
+ .Process(this->SuperWeaponSidebar_Allow)
+ .Process(this->SuperWeaponSidebar_PriorityHouses)
+ .Process(this->SuperWeaponSidebar_RequiredHouses)
+ .Process(this->SuperWeaponSidebar_Significance)
+ .Process(this->SidebarPal)
+ .Process(this->SidebarPCX)
.Process(this->UseWeeds)
.Process(this->UseWeeds_Amount)
.Process(this->UseWeeds_StorageTimer)
@@ -80,6 +106,14 @@ void SWTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
// from ares
this->Money_Amount.Read(exINI, pSection, "Money.Amount");
+ this->EVA_Impatient.Read(exINI, pSection, "EVA.Impatient");
+ this->EVA_InsufficientFunds.Read(exINI, pSection, "EVA.InsufficientFunds");
+ this->EVA_SelectTarget.Read(exINI, pSection, "EVA.SelectTarget");
+ this->SW_UseAITargeting.Read(exINI, pSection, "SW.UseAITargeting");
+ this->SW_AutoFire.Read(exINI, pSection, "SW.AutoFire");
+ this->SW_ManualFire.Read(exINI, pSection, "SW.ManualFire");
+ this->SW_ShowCameo.Read(exINI, pSection, "SW.ShowCameo");
+ this->SW_Unstoppable.Read(exINI, pSection, "SW.Unstoppable");
this->SW_Inhibitors.Read(exINI, pSection, "SW.Inhibitors");
this->SW_AnyInhibitor.Read(exINI, pSection, "SW.AnyInhibitor");
this->SW_Designators.Read(exINI, pSection, "SW.Designators");
@@ -95,6 +129,18 @@ void SWTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->SW_MaxCount.Read(exINI, pSection, "SW.MaxCount");
this->SW_Shots.Read(exINI, pSection, "SW.Shots");
+ this->Message_CannotFire.Read(exINI, pSection, "Message.CannotFire");
+ this->Message_InsufficientFunds.Read(exINI, pSection, "Message.InsufficientFunds");
+
+ // messages and their properties
+ this->Message_FirerColor.Read(exINI, pSection, "Message.FirerColor");
+ if (pINI->ReadString(pSection, "Message.Color", NONE_STR, Phobos::readBuffer))
+ {
+ this->Message_ColorScheme = ColorScheme::FindIndex(Phobos::readBuffer);
+ if (this->Message_ColorScheme < 0)
+ Debug::INIParseFailed(pSection, "Message.Color", Phobos::readBuffer, "Expected a valid color scheme name.");
+ }
+
this->UIDescription.Read(exINI, pSection, "UIDescription");
this->CameoPriority.Read(exINI, pSection, "CameoPriority");
this->LimboDelivery_Types.Read(exINI, pSection, "LimboDelivery.Types");
@@ -182,6 +228,14 @@ void SWTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->TabIndex.Read(exINI, pSection, "TabIndex");
GeneralUtils::IntValidCheck(&this->TabIndex, pSection, "TabIndex", 1, 0, 3);
+ this->SuperWeaponSidebar_Allow.Read(exINI, pSection, "SuperWeaponSidebar.Allow");
+ this->SuperWeaponSidebar_PriorityHouses = pINI->ReadHouseTypesList(pSection, "SuperWeaponSidebar.PriorityHouses", this->SuperWeaponSidebar_PriorityHouses);
+ this->SuperWeaponSidebar_RequiredHouses = pINI->ReadHouseTypesList(pSection, "SuperWeaponSidebar.RequiredHouses", this->SuperWeaponSidebar_RequiredHouses);
+ this->SuperWeaponSidebar_Significance.Read(exINI, pSection, "SuperWeaponSidebar.Significance");
+
+ this->SidebarPal.LoadFromINI(pINI, pSection, "SidebarPalette");
+ this->SidebarPCX.Read(pINI, pSection, "SidebarPCX");
+
this->UseWeeds.Read(exINI, pSection, "UseWeeds");
this->UseWeeds_Amount.Read(exINI, pSection, "UseWeeds.Amount");
this->UseWeeds_StorageTimer.Read(exINI, pSection, "UseWeeds.StorageTimer");
diff --git a/src/Ext/SWType/Body.h b/src/Ext/SWType/Body.h
index aae1713b62..e4fd387560 100644
--- a/src/Ext/SWType/Body.h
+++ b/src/Ext/SWType/Body.h
@@ -26,6 +26,14 @@ class SWTypeExt
//Ares 0.A
Valueable Money_Amount;
+ ValueableIdx EVA_Impatient;
+ ValueableIdx EVA_InsufficientFunds;
+ ValueableIdx EVA_SelectTarget;
+ Valueable SW_UseAITargeting;
+ Valueable SW_AutoFire;
+ Valueable SW_ManualFire;
+ Valueable SW_ShowCameo;
+ Valueable SW_Unstoppable;
ValueableVector SW_Inhibitors;
Valueable SW_AnyInhibitor;
ValueableVector SW_Designators;
@@ -42,6 +50,11 @@ class SWTypeExt
ValueableIdx SW_PostDependent;
Valueable SW_MaxCount;
+ Valueable Message_CannotFire;
+ Valueable Message_InsufficientFunds;
+ Valueable Message_ColorScheme;
+ Valueable Message_FirerColor;
+
Valueable UIDescription;
Valueable CameoPriority;
ValueableVector LimboDelivery_Types;
@@ -67,6 +80,14 @@ class SWTypeExt
Valueable TabIndex;
+ Nullable SuperWeaponSidebar_Allow;
+ DWORD SuperWeaponSidebar_PriorityHouses;
+ DWORD SuperWeaponSidebar_RequiredHouses;
+ Valueable SuperWeaponSidebar_Significance;
+
+ CustomPalette SidebarPal;
+ PhobosPCXFile SidebarPCX;
+
std::vector> LimboDelivery_RandomWeightsData;
std::vector> SW_Next_RandomWeightsData;
@@ -85,6 +106,14 @@ class SWTypeExt
ExtData(SuperWeaponTypeClass* OwnerObject) : Extension(OwnerObject)
, TypeID { "" }
, Money_Amount { 0 }
+ , EVA_Impatient { -1 }
+ , EVA_InsufficientFunds { -1 }
+ , EVA_SelectTarget { -1 }
+ , SW_UseAITargeting { false }
+ , SW_AutoFire { false }
+ , SW_ManualFire { true }
+ , SW_ShowCameo { true }
+ , SW_Unstoppable { false }
, SW_Inhibitors {}
, SW_AnyInhibitor { false }
, SW_Designators { }
@@ -99,6 +128,10 @@ class SWTypeExt
, SW_PostDependent {}
, SW_MaxCount { -1 }
, SW_Shots { -1 }
+ , Message_CannotFire {}
+ , Message_InsufficientFunds {}
+ , Message_ColorScheme { -1 }
+ , Message_FirerColor { false }
, UIDescription {}
, CameoPriority { 0 }
, LimboDelivery_Types {}
@@ -123,6 +156,12 @@ class SWTypeExt
, Convert_Pairs {}
, ShowDesignatorRange { true }
, TabIndex { 1 }
+ , SuperWeaponSidebar_Allow {}
+ , SuperWeaponSidebar_PriorityHouses { 0u }
+ , SuperWeaponSidebar_RequiredHouses { 0xFFFFFFFFu }
+ , SuperWeaponSidebar_Significance { 0 }
+ , SidebarPal {}
+ , SidebarPCX {}
, UseWeeds { false }
, UseWeeds_Amount { RulesClass::Instance->WeedCapacity }
, UseWeeds_StorageTimer { false }
@@ -144,6 +183,7 @@ class SWTypeExt
bool IsLaunchSite(BuildingClass* pBuilding) const;
std::pair GetLaunchSiteRange(BuildingClass* pBuilding = nullptr) const;
bool IsAvailable(HouseClass* pHouse) const;
+ void PrintMessage(const CSFText& message, HouseClass* pFirer) const;
void ApplyLimboDelivery(HouseClass* pHouse);
void ApplyLimboKill(HouseClass* pHouse);
@@ -155,6 +195,8 @@ class SWTypeExt
std::pair GetEMPulseCannonRange(BuildingClass* pBuilding) const;
virtual void LoadFromINIFile(CCINIClass* pINI) override;
+ virtual void Initialize() override;
+
virtual ~ExtData() = default;
virtual void InvalidatePointer(void* ptr, bool bRemoved) override { }
diff --git a/src/Ext/SWType/SWHelpers.cpp b/src/Ext/SWType/SWHelpers.cpp
index 2d95c15233..985d4038f0 100644
--- a/src/Ext/SWType/SWHelpers.cpp
+++ b/src/Ext/SWType/SWHelpers.cpp
@@ -1,4 +1,5 @@
#include "Body.h"
+#include
// Universal handler of the rolls-weights system
std::vector SWTypeExt::ExtData::WeightedRollsHandler(ValueableVector* rolls, std::vector>* weights, size_t size)
@@ -268,3 +269,35 @@ std::pair SWTypeExt::ExtData::GetEMPulseCannonRange(BuildingClas
return std::make_pair(this->SW_RangeMinimum.Get(), this->SW_RangeMaximum.Get());
}
+
+void SWTypeExt::ExtData::PrintMessage(const CSFText& message, HouseClass* pFirer) const
+{
+ if (message.empty())
+ return;
+
+ int color = ColorScheme::FindIndex("Gold");
+ if (this->Message_FirerColor)
+ {
+ // firer color
+ if (pFirer)
+ {
+ color = pFirer->ColorSchemeIndex;
+ }
+ }
+ else
+ {
+ if (this->Message_ColorScheme > -1)
+ {
+ // user defined color
+ color = this->Message_ColorScheme;
+ }
+ else if (const auto pCurrent = HouseClass::CurrentPlayer)
+ {
+ // default way: the current player's color
+ color = pCurrent->ColorSchemeIndex;
+ }
+ }
+
+ // print the message
+ MessageListClass::Instance.PrintMessage(message, RulesClass::Instance->MessageDelay, color);
+}
diff --git a/src/Ext/Scenario/Body.cpp b/src/Ext/Scenario/Body.cpp
index c4bdaaaeb5..0a81b06fdd 100644
--- a/src/Ext/Scenario/Body.cpp
+++ b/src/Ext/Scenario/Body.cpp
@@ -161,6 +161,8 @@ void ScenarioExt::ExtData::Serialize(T& Stm)
.Process(this->BriefingTheme)
.Process(this->AutoDeathObjects)
.Process(this->TransportReloaders)
+ .Process(this->SWSidebar_Enable)
+ .Process(this->SWSidebar_Indices)
;
}
diff --git a/src/Ext/Scenario/Body.h b/src/Ext/Scenario/Body.h
index 3d296a5c5f..6a51659d2b 100644
--- a/src/Ext/Scenario/Body.h
+++ b/src/Ext/Scenario/Body.h
@@ -36,6 +36,9 @@ class ScenarioExt
std::vector AutoDeathObjects;
std::vector TransportReloaders; // Objects that can reload ammo in limbo
+ bool SWSidebar_Enable;
+ std::vector SWSidebar_Indices;
+
ExtData(ScenarioClass* OwnerObject) : Extension(OwnerObject)
, ShowBriefing { false }
, BriefingTheme { -1 }
@@ -43,6 +46,8 @@ class ScenarioExt
, Variables { }
, AutoDeathObjects {}
, TransportReloaders {}
+ , SWSidebar_Enable { true }
+ , SWSidebar_Indices {}
{ }
void SetVariableToByID(bool bIsGlobal, int nIndex, char bState);
diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp
index d763574cb1..8784fab9cf 100644
--- a/src/Ext/Side/Body.cpp
+++ b/src/Ext/Side/Body.cpp
@@ -42,6 +42,11 @@ void SideExt::ExtData::LoadFromINIFile(CCINIClass* pINI)
this->ToolTip_Background_Opacity.Read(exINI, pSection, "ToolTip.Background.Opacity");
this->ToolTip_Background_BlurSize.Read(exINI, pSection, "ToolTip.Background.BlurSize");
this->BriefingTheme = pINI->ReadTheme(pSection, "BriefingTheme", this->BriefingTheme);
+ this->SuperWeaponSidebar_OnPCX.Read(pINI, pSection, "SuperWeaponSidebar.OnPCX");
+ this->SuperWeaponSidebar_OffPCX.Read(pINI, pSection, "SuperWeaponSidebar.OffPCX");
+ this->SuperWeaponSidebar_TopPCX.Read(pINI, pSection, "SuperWeaponSidebar.TopPCX");
+ this->SuperWeaponSidebar_CenterPCX.Read(pINI, pSection, "SuperWeaponSidebar.CenterPCX");
+ this->SuperWeaponSidebar_BottomPCX.Read(pINI, pSection, "SuperWeaponSidebar.BottomPCX");
}
// =============================
@@ -71,6 +76,11 @@ void SideExt::ExtData::Serialize(T& Stm)
.Process(this->IngameScore_WinTheme)
.Process(this->IngameScore_LoseTheme)
.Process(this->BriefingTheme)
+ .Process(this->SuperWeaponSidebar_OnPCX)
+ .Process(this->SuperWeaponSidebar_OffPCX)
+ .Process(this->SuperWeaponSidebar_TopPCX)
+ .Process(this->SuperWeaponSidebar_CenterPCX)
+ .Process(this->SuperWeaponSidebar_BottomPCX)
;
}
diff --git a/src/Ext/Side/Body.h b/src/Ext/Side/Body.h
index 88e694aa0c..e0f3362ef0 100644
--- a/src/Ext/Side/Body.h
+++ b/src/Ext/Side/Body.h
@@ -36,6 +36,11 @@ class SideExt
Nullable ToolTip_Background_Opacity;
Nullable ToolTip_Background_BlurSize;
Valueable BriefingTheme;
+ PhobosPCXFile SuperWeaponSidebar_OnPCX;
+ PhobosPCXFile SuperWeaponSidebar_OffPCX;
+ PhobosPCXFile SuperWeaponSidebar_TopPCX;
+ PhobosPCXFile SuperWeaponSidebar_CenterPCX;
+ PhobosPCXFile SuperWeaponSidebar_BottomPCX;
ExtData(SideClass* OwnerObject) : Extension(OwnerObject)
, ArrayIndex { -1 }
@@ -58,6 +63,11 @@ class SideExt
, ToolTip_Background_Opacity { }
, ToolTip_Background_BlurSize { }
, BriefingTheme { -1 }
+ , SuperWeaponSidebar_OnPCX {}
+ , SuperWeaponSidebar_OffPCX {}
+ , SuperWeaponSidebar_TopPCX {}
+ , SuperWeaponSidebar_CenterPCX {}
+ , SuperWeaponSidebar_BottomPCX {}
{ }
virtual ~ExtData() = default;
diff --git a/src/Ext/Sidebar/Body.cpp b/src/Ext/Sidebar/Body.cpp
index 3f9baa9cc1..a463cc63ca 100644
--- a/src/Ext/Sidebar/Body.cpp
+++ b/src/Ext/Sidebar/Body.cpp
@@ -1,4 +1,5 @@
#include "Body.h"
+#include "SWSidebar/SWSidebarClass.h"
#include
#include
@@ -37,7 +38,12 @@ bool __stdcall SidebarExt::AresTabCameo_RemoveCameo(BuildType* pItem)
const auto& supers = pCurrent->Supers;
if (supers.ValidIndex(pItem->ItemIndex) && supers[pItem->ItemIndex]->IsPresent)
- return false;
+ {
+ if (SWSidebarClass::Instance.AddButton(pItem->ItemIndex))
+ ScenarioExt::Global()->SWSidebar_Indices.emplace_back(pItem->ItemIndex);
+ else
+ return false;
+ }
}
// The following sections have been modified
@@ -45,6 +51,7 @@ bool __stdcall SidebarExt::AresTabCameo_RemoveCameo(BuildType* pItem)
if (pItem->ItemType == AbstractType::BuildingType || pItem->ItemType == AbstractType::Building)
{
+ __assume(pTechnoType != nullptr);
buildCat = static_cast(pTechnoType)->BuildCat;
auto& pDisplay = DisplayClass::Instance;
pDisplay.SetActiveFoundation(nullptr);
diff --git a/src/Ext/Sidebar/Hooks.cpp b/src/Ext/Sidebar/Hooks.cpp
index 4a41be815a..dbee38ba07 100644
--- a/src/Ext/Sidebar/Hooks.cpp
+++ b/src/Ext/Sidebar/Hooks.cpp
@@ -1,4 +1,5 @@
#include "Body.h"
+#include "SWSidebar/SWSidebarClass.h"
#include
#include
@@ -97,3 +98,31 @@ DEFINE_HOOK(0x72FCB5, InitSideRectangles_CenterBackground, 0x5)
return 0;
}
+
+#pragma region SWSidebarButtonsRelated
+
+DEFINE_HOOK(0x692419, DisplayClass_ProcessClickCoords_SWSidebar, 0x7)
+{
+ enum { DoNothing = 0x6925FC };
+
+ if (SWSidebarClass::IsEnabled() && SWSidebarClass::Instance.CurrentColumn)
+ return DoNothing;
+
+ const auto toggleButton = SWSidebarClass::Instance.ToggleButton;
+
+ return toggleButton && toggleButton->IsHovering ? DoNothing : 0;
+}
+
+DEFINE_HOOK(0x6A5082, SidebarClass_InitClear_InitializeSWSidebar, 0x5)
+{
+ SWSidebarClass::Instance.InitClear();
+ return 0;
+}
+
+DEFINE_HOOK(0x6A5839, SidebarClass_InitIO_InitializeSWSidebar, 0x5)
+{
+ SWSidebarClass::Instance.InitIO();
+ return 0;
+}
+
+#pragma endregion
diff --git a/src/Ext/Sidebar/SWSidebar/SWButtonClass.cpp b/src/Ext/Sidebar/SWSidebar/SWButtonClass.cpp
new file mode 100644
index 0000000000..9ab43f48a0
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/SWButtonClass.cpp
@@ -0,0 +1,229 @@
+#include "SWButtonClass.h"
+#include "SWSidebarClass.h"
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+SWButtonClass::SWButtonClass(unsigned int id, int superIdx, int x, int y, int width, int height)
+ : ControlClass(id, x, y, width, height, (GadgetFlag::LeftPress | GadgetFlag::RightPress), false)
+ , SuperIndex(superIdx)
+{
+ if (const auto backColumn = SWSidebarClass::Instance.Columns.back())
+ backColumn->Buttons.emplace_back(this);
+
+ this->Disabled = !SWSidebarClass::IsEnabled();
+}
+
+bool SWButtonClass::Draw(bool forced)
+{
+ if (!forced)
+ return false;
+
+ const auto pSurface = DSurface::Composite;
+ auto bounds = pSurface->GetRect();
+ Point2D location = { this->X, this->Y };
+ RectangleStruct destRect = { location.X, location.Y, this->Width, this->Height };
+
+ const auto pCurrent = HouseClass::CurrentPlayer;
+ const auto pSuper = pCurrent->Supers[this->SuperIndex];
+ const auto pSWExt = SWTypeExt::ExtMap.Find(pSuper->Type);
+
+ // support for pcx cameos
+ if (const auto pPCXCameo = pSWExt->SidebarPCX.GetSurface())
+ {
+ PCX::Instance.BlitToSurface(&destRect, pSurface, pPCXCameo);
+ }
+ else if (const auto pCameo = pSuper->Type->SidebarImage) // old shp cameos, fixed palette
+ {
+ const auto pCameoRef = pCameo->AsReference();
+ char pFilename[0x20];
+ strcpy_s(pFilename, RulesExt::Global()->MissingCameo.data());
+ _strlwr_s(pFilename);
+
+ if (!_stricmp(pCameoRef->Filename, GameStrings::XXICON_SHP) && strstr(pFilename, ".pcx"))
+ {
+ PCX::Instance.LoadFile(pFilename);
+
+ if (const auto CameoPCX = PCX::Instance.GetSurface(pFilename))
+ PCX::Instance.BlitToSurface(&destRect, pSurface, CameoPCX);
+ }
+ else
+ {
+ const auto pConvert = pSWExt->SidebarPal.Convert ? pSWExt->SidebarPal.GetConvert() : FileSystem::CAMEO_PAL;
+ pSurface->DrawSHP(pConvert, pCameo, 0, &location, &bounds, BlitterFlags::bf_400, 0, 0, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0);
+ }
+ }
+
+ if (this->IsHovering)
+ {
+ RectangleStruct cameoRect = { location.X, location.Y, this->Width, this->Height };
+ const COLORREF tooltipColor = Drawing::RGB_To_Int(Drawing::TooltipColor);
+ pSurface->DrawRect(&cameoRect, tooltipColor);
+ }
+
+ if (pSuper->IsReady && !pCurrent->CanTransactMoney(pSWExt->Money_Amount) ||
+ (pSWExt->SW_UseAITargeting && AresFunctions::IsTargetConstraintsEligible && !AresFunctions::IsTargetConstraintsEligible(AresFunctions::SWTypeExtMap_Find(pSuper->Type), HouseClass::CurrentPlayer, true)))
+ {
+ RectangleStruct darkenBounds { 0, 0, location.X + this->Width, location.Y + this->Height };
+ pSurface->DrawSHP(FileSystem::SIDEBAR_PAL, FileSystem::DARKEN_SHP, 0, &location, &darkenBounds, BlitterFlags::bf_400 | BlitterFlags::Darken, 0, 0, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0);
+ }
+
+ const bool ready = !pSuper->IsSuspended && (pSuper->Type->UseChargeDrain ? pSuper->ChargeDrainState == ChargeDrainState::Ready : pSuper->IsReady);
+ bool drawReadiness = true;
+
+ if (ready && this->ColumnIndex == 0)
+ {
+ auto& buttons = SWSidebarClass::Instance.Columns[this->ColumnIndex]->Buttons;
+ const int buttonId = std::distance(buttons.begin(), std::find(buttons.begin(), buttons.end(), this));
+
+ if (buttonId < 10)
+ {
+ unsigned short hotkey = 0;
+
+ for (int idx = 0; idx < CommandClass::Hotkeys.IndexCount; idx++)
+ {
+ if (CommandClass::Hotkeys.IndexTable[idx].Data == SWSidebarClass::Commands[buttonId])
+ hotkey = CommandClass::Hotkeys.IndexTable[idx].ID;
+ }
+
+ Point2D textLoc = { location.X + this->Width / 2, location.Y };
+ const COLORREF foreColor = Drawing::RGB_To_Int(Drawing::TooltipColor);
+ constexpr TextPrintType printType = TextPrintType::FullShadow | TextPrintType::Point8 | TextPrintType::Background | TextPrintType::Center;
+
+ wchar_t buffer[64];
+ UI::GetKeyboardKeyString(hotkey, buffer);
+
+ if (std::wcslen(buffer))
+ {
+ pSurface->DrawTextA(buffer, &bounds, &textLoc, foreColor, 0, printType);
+ drawReadiness = false;
+ }
+ }
+ }
+
+ if (drawReadiness)
+ {
+ if (const auto buffer = pSuper->NameReadiness())
+ {
+ Point2D textLoc = { location.X + this->Width / 2, location.Y };
+ const COLORREF foreColor = Drawing::RGB_To_Int(Drawing::TooltipColor);
+ constexpr TextPrintType printType = TextPrintType::FullShadow | TextPrintType::Point8 | TextPrintType::Background | TextPrintType::Center;
+
+ pSurface->DrawTextA(buffer, &bounds, &textLoc, foreColor, 0, printType);
+ }
+ }
+
+ if (pSuper->ShouldDrawProgress())
+ {
+ Point2D loc = { location.X, location.Y };
+ pSurface->DrawSHP(FileSystem::SIDEBAR_PAL, FileSystem::GCLOCK2_SHP, pSuper->AnimStage() + 1, &loc, &bounds, BlitterFlags::bf_400 | BlitterFlags::TransLucent50, 0, 0, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0);
+ }
+
+ return true;
+}
+
+void SWButtonClass::OnMouseEnter()
+{
+ if (!SWSidebarClass::IsEnabled())
+ return;
+
+ this->IsHovering = true;
+ SWSidebarClass::Instance.CurrentButton = this;
+ SWSidebarClass::Instance.Columns[this->ColumnIndex]->OnMouseEnter();
+ CCToolTip::Instance->SaveTimerDelay();
+ CCToolTip::Instance->SetTimerDelay(0);
+}
+
+void SWButtonClass::OnMouseLeave()
+{
+ this->IsHovering = false;
+ SWSidebarClass::Instance.CurrentButton = nullptr;
+ SWSidebarClass::Instance.Columns[this->ColumnIndex]->OnMouseLeave();
+ CCToolTip::Instance->RestoreTimeDelay();
+}
+
+bool SWButtonClass::Action(GadgetFlag flags, DWORD* pKey, KeyModifier modifier)
+{
+ if (!SWSidebarClass::IsEnabled())
+ return false;
+
+ if (flags & GadgetFlag::RightPress)
+ DisplayClass::Instance.CurrentSWTypeIndex = -1;
+
+ if (flags & GadgetFlag::LeftPress)
+ {
+ MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false);
+ VocClass::PlayGlobal(RulesClass::Instance->GUIBuildSound, 0x2000, 1.0);
+ this->LaunchSuper();
+ }
+
+ // this->ControlClass::Action(flags, pKey, KeyModifier::None);
+ reinterpret_cast(0x48E5A0)(this, flags, pKey, KeyModifier::None);
+ return true;
+}
+
+void SWButtonClass::SetColumn(int column)
+{
+ this->ColumnIndex = column;
+}
+
+bool SWButtonClass::LaunchSuper() const
+{
+ const auto pCurrent = HouseClass::CurrentPlayer;
+ const auto pSuper = pCurrent->Supers[this->SuperIndex];
+ const auto pSWExt = SWTypeExt::ExtMap.Find(pSuper->Type);
+ const bool manual = !pSWExt->SW_ManualFire && pSWExt->SW_AutoFire;
+ const bool unstoppable = pSuper->Type->UseChargeDrain && pSuper->ChargeDrainState == ChargeDrainState::Draining && pSWExt->SW_Unstoppable;
+
+ if (!pSuper->CanFire() && !manual)
+ {
+ VoxClass::PlayIndex(pSWExt->EVA_Impatient);
+ return false;
+ }
+
+ if (!pCurrent->CanTransactMoney(pSWExt->Money_Amount))
+ {
+ VoxClass::PlayIndex(pSWExt->EVA_InsufficientFunds);
+ pSWExt->PrintMessage(pSWExt->Message_InsufficientFunds, pCurrent);
+ }
+ else if (!pSWExt->SW_UseAITargeting || (AresFunctions::IsTargetConstraintsEligible && AresFunctions::IsTargetConstraintsEligible(AresFunctions::SWTypeExtMap_Find(pSuper->Type), HouseClass::CurrentPlayer, true)))
+ {
+ if (!manual && !unstoppable)
+ {
+ const auto swIndex = pSuper->Type->ArrayIndex;
+
+ if (pSuper->Type->Action == Action::None || pSWExt->SW_UseAITargeting)
+ {
+ EventClass Event = EventClass(pCurrent->ArrayIndex, EventType::SpecialPlace, swIndex, CellStruct::Empty);
+ EventClass::AddEvent(Event);
+ }
+ else
+ {
+ DisplayClass::Instance.CurrentBuilding = nullptr;
+ DisplayClass::Instance.CurrentBuildingType = nullptr;
+ DisplayClass::Instance.CurrentBuildingOwnerArrayIndex = -1;
+ DisplayClass::Instance.SetActiveFoundation(nullptr);
+ MapClass::Instance.SetRepairMode(0);
+ MapClass::Instance.SetSellMode(0);
+ DisplayClass::Instance.PowerToggleMode = false;
+ DisplayClass::Instance.PlanningMode = false;
+ DisplayClass::Instance.PlaceBeaconMode = false;
+ DisplayClass::Instance.CurrentSWTypeIndex = swIndex;
+ MapClass::Instance.UnselectAll();
+ VoxClass::PlayIndex(pSWExt->EVA_SelectTarget);
+ }
+
+ return true;
+ }
+ }
+ else
+ {
+ pSWExt->PrintMessage(pSWExt->Message_CannotFire, pCurrent);
+ }
+
+ return false;
+}
diff --git a/src/Ext/Sidebar/SWSidebar/SWButtonClass.h b/src/Ext/Sidebar/SWSidebar/SWButtonClass.h
new file mode 100644
index 0000000000..3defc7e7ff
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/SWButtonClass.h
@@ -0,0 +1,26 @@
+#pragma once
+#include
+
+class SWButtonClass : public ControlClass
+{
+public:
+ SWButtonClass() = default;
+ SWButtonClass(unsigned int id, int superIdx, int x, int y, int width, int height);
+
+ ~SWButtonClass() = default;
+
+ virtual bool Draw(bool forced) override;
+ virtual void OnMouseEnter() override;
+ virtual void OnMouseLeave() override;
+ virtual bool Action(GadgetFlag fags, DWORD* pKey, KeyModifier modifier) override;
+
+ void SetColumn(int column);
+ bool LaunchSuper() const;
+
+public:
+ static constexpr int StartID = 2200;
+
+ bool IsHovering { false };
+ int ColumnIndex { -1 };
+ int SuperIndex { -1 };
+};
diff --git a/src/Ext/Sidebar/SWSidebar/SWColumnClass.cpp b/src/Ext/Sidebar/SWSidebar/SWColumnClass.cpp
new file mode 100644
index 0000000000..50073f6dd3
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/SWColumnClass.cpp
@@ -0,0 +1,173 @@
+#include "SWColumnClass.h"
+#include "SWSidebarClass.h"
+
+#include
+#include
+
+SWColumnClass::SWColumnClass(unsigned int id, int maxButtons, int x, int y, int width, int height)
+ : ControlClass(id, x, y, width, height, static_cast(0), false)
+ , MaxButtons(maxButtons)
+{
+ SWSidebarClass::Instance.Columns.emplace_back(this);
+ this->Disabled = !SWSidebarClass::IsEnabled();
+}
+
+bool SWColumnClass::Draw(bool forced)
+{
+ if (!SWSidebarClass::IsEnabled())
+ return false;
+
+ const auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex]);
+ const int cameoWidth = 60, cameoHeight = 48;
+ const int cameoBackgroundWidth = Phobos::UI::SuperWeaponSidebar_Interval + cameoWidth;
+
+ if (const auto pCenterPCX = pSideExt->SuperWeaponSidebar_CenterPCX.GetSurface())
+ {
+ const int cameoHarfInterval = (Phobos::UI::SuperWeaponSidebar_CameoHeight - cameoHeight) / 2;
+
+ for (const auto button : this->Buttons)
+ {
+ RectangleStruct drawRect { this->X, button->Y - cameoHarfInterval, cameoBackgroundWidth, Phobos::UI::SuperWeaponSidebar_CameoHeight };
+ PCX::Instance.BlitToSurface(&drawRect, DSurface::Composite, pCenterPCX);
+ }
+ }
+
+ if (const auto pTopPCX = pSideExt->SuperWeaponSidebar_TopPCX.GetSurface())
+ {
+ const int height = pTopPCX->GetHeight();
+ RectangleStruct drawRect { this->X, this->Y, cameoBackgroundWidth, height };
+ PCX::Instance.BlitToSurface(&drawRect, DSurface::Composite, pTopPCX);
+ }
+
+ if (const auto pBottomPCX = pSideExt->SuperWeaponSidebar_BottomPCX.GetSurface())
+ {
+ const int height = pBottomPCX->GetHeight();
+ RectangleStruct drawRect { this->X, this->Y + this->Height - height, cameoBackgroundWidth, height };
+ PCX::Instance.BlitToSurface(&drawRect, DSurface::Composite, pBottomPCX);
+ }
+
+ for (const auto button : this->Buttons)
+ button->Draw(true);
+
+ return true;
+}
+
+void SWColumnClass::OnMouseEnter()
+{
+ if (!SWSidebarClass::IsEnabled())
+ return;
+
+ SWSidebarClass::Instance.CurrentColumn = this;
+ MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false);
+}
+
+void SWColumnClass::OnMouseLeave()
+{
+ SWSidebarClass::Instance.CurrentColumn = nullptr;
+ MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false);
+}
+
+bool SWColumnClass::Clicked(DWORD* pKey, GadgetFlag flags, int x, int y, KeyModifier modifier)
+{
+ return false;
+}
+
+bool SWColumnClass::AddButton(int superIdx)
+{
+ auto& buttons = this->Buttons;
+ const int buttonCount = static_cast(buttons.size());
+ auto& sidebar = SWSidebarClass::Instance;
+
+ if (buttonCount >= this->MaxButtons && !SWSidebarClass::Instance.AddColumn())
+ {
+ const unsigned int ownerBits = 1u << HouseClass::CurrentPlayer->Type->ArrayIndex;
+
+ auto Compare = [ownerBits](const int left, const int right)
+ {
+ const auto pExtA = SWTypeExt::ExtMap.Find(SuperWeaponTypeClass::Array.GetItemOrDefault(left));
+ const auto pExtB = SWTypeExt::ExtMap.Find(SuperWeaponTypeClass::Array.GetItemOrDefault(right));
+
+ if (pExtB && (pExtB->SuperWeaponSidebar_PriorityHouses & ownerBits) && (!pExtA || !(pExtA->SuperWeaponSidebar_PriorityHouses & ownerBits)))
+ return false;
+
+ if ((!pExtB || !(pExtB->SuperWeaponSidebar_PriorityHouses & ownerBits)) && pExtA && (pExtA->SuperWeaponSidebar_PriorityHouses & ownerBits))
+ return true;
+
+ return BuildType::SortsBefore(AbstractType::Special, left, AbstractType::Special, right);
+ };
+
+ const int backIdx = buttons.back()->SuperIndex;
+
+ if (!Compare(superIdx, backIdx))
+ return false;
+
+ this->RemoveButton(backIdx);
+ sidebar.DisableEntry = true;
+ SidebarClass::Instance.AddCameo(AbstractType::Special, backIdx);
+ SidebarClass::Instance.RepaintSidebar(SidebarClass::GetObjectTabIdx(AbstractType::Super, backIdx, false));
+ sidebar.DisableEntry = false;
+ }
+
+ const int cameoWidth = 60, cameoHeight = 48;
+ const auto button = GameCreate(SWButtonClass::StartID + superIdx, superIdx, 0, 0, cameoWidth, cameoHeight);
+
+ if (!button)
+ return false;
+
+ button->Zap();
+ GScreenClass::Instance.AddButton(button);
+ SWSidebarClass::Instance.SortButtons();
+
+ if (const auto toggleButton = SWSidebarClass::Instance.ToggleButton)
+ toggleButton->UpdatePosition();
+
+ return true;
+}
+
+bool SWColumnClass::RemoveButton(int superIdx)
+{
+ auto& buttons = this->Buttons;
+
+ const auto it = std::find_if(buttons.begin(), buttons.end(), [superIdx](SWButtonClass* const button) { return button->SuperIndex == superIdx; });
+
+ if (it == buttons.end())
+ return false;
+
+ AnnounceInvalidPointer(SWSidebarClass::Instance.CurrentButton, *it);
+
+ auto& indices = ScenarioExt::Global()->SWSidebar_Indices;
+ const auto it_Idx = std::find(indices.cbegin(), indices.cend(), superIdx);
+
+ if (it_Idx != indices.cend())
+ indices.erase(it_Idx);
+
+ GScreenClass::Instance.RemoveButton(*it);
+ buttons.erase(it);
+ return true;
+}
+
+void SWColumnClass::ClearButtons(bool remove)
+{
+ auto& buttons = this->Buttons;
+
+ if (remove)
+ {
+ for (const auto button : buttons)
+ GScreenClass::Instance.RemoveButton(button);
+ }
+
+ buttons.clear();
+}
+
+void SWColumnClass::SetHeight(int height)
+{
+ const auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex]);
+
+ this->Height = height;
+
+ if (const auto pTopPCX = pSideExt->SuperWeaponSidebar_TopPCX.GetSurface())
+ this->Height += pTopPCX->GetHeight();
+
+ if (const auto pBottomPCX = pSideExt->SuperWeaponSidebar_BottomPCX.GetSurface())
+ this->Height += pBottomPCX->GetHeight();
+}
diff --git a/src/Ext/Sidebar/SWSidebar/SWColumnClass.h b/src/Ext/Sidebar/SWSidebar/SWColumnClass.h
new file mode 100644
index 0000000000..4f3181a091
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/SWColumnClass.h
@@ -0,0 +1,28 @@
+#pragma once
+#include "SWButtonClass.h"
+#include
+
+#include
+
+class SWColumnClass : public ControlClass
+{
+public:
+ SWColumnClass() = default;
+ SWColumnClass(unsigned int id, int maxButtons, int x, int y, int width, int height);
+
+ ~SWColumnClass() = default;
+
+ virtual bool Draw(bool forced) override;
+ virtual void OnMouseEnter() override;
+ virtual void OnMouseLeave() override;
+ virtual bool Clicked(DWORD* pKey, GadgetFlag flags, int x, int y, KeyModifier modifier) override;
+
+ bool AddButton(int superIdx);
+ bool RemoveButton(int superIdx);
+ void ClearButtons(bool remove = true);
+
+ void SetHeight(int height);
+
+ std::vector Buttons {};
+ int MaxButtons { 0 };
+};
diff --git a/src/Ext/Sidebar/SWSidebar/SWSidebarClass.cpp b/src/Ext/Sidebar/SWSidebar/SWSidebarClass.cpp
new file mode 100644
index 0000000000..b96e71b574
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/SWSidebarClass.cpp
@@ -0,0 +1,342 @@
+#include "SWSidebarClass.h"
+#include
+
+#include
+#include
+#include
+
+SWSidebarClass SWSidebarClass::Instance;
+CommandClass* SWSidebarClass::Commands[10];
+
+// =============================
+// functions
+
+bool SWSidebarClass::AddColumn()
+{
+ auto& columns = this->Columns;
+
+ if (static_cast(columns.size()) >= Phobos::UI::SuperWeaponSidebar_MaxColumns)
+ return false;
+
+ const int maxButtons = Phobos::UI::SuperWeaponSidebar_Max - static_cast(columns.size());
+
+ if (maxButtons <= 0)
+ return false;
+
+ const int cameoWidth = 60;
+ const auto column = GameCreate(SWButtonClass::StartID + SuperWeaponTypeClass::Array.Count + 1 + static_cast(columns.size()), maxButtons, 0, 0, cameoWidth + Phobos::UI::SuperWeaponSidebar_Interval, Phobos::UI::SuperWeaponSidebar_CameoHeight);
+
+ if (!column)
+ return false;
+
+ column->Zap();
+ GScreenClass::Instance.AddButton(column);
+ return true;
+}
+
+bool SWSidebarClass::RemoveColumn()
+{
+ auto& columns = this->Columns;
+
+ if (columns.empty())
+ return false;
+
+ if (const auto backColumn = columns.back())
+ {
+ AnnounceInvalidPointer(SWSidebarClass::Instance.CurrentColumn, backColumn);
+ GScreenClass::Instance.RemoveButton(backColumn);
+
+ columns.erase(columns.end() - 1);
+ return true;
+ }
+
+ return false;
+}
+
+void SWSidebarClass::InitClear()
+{
+ this->CurrentColumn = nullptr;
+ this->CurrentButton = nullptr;
+
+ if (const auto toggleButton = this->ToggleButton)
+ {
+ this->ToggleButton = nullptr;
+ GScreenClass::Instance.RemoveButton(toggleButton);
+ }
+
+ auto& columns = this->Columns;
+
+ for (const auto column : columns)
+ {
+ column->ClearButtons();
+ GScreenClass::Instance.RemoveButton(column);
+ }
+
+ columns.clear();
+}
+
+void SWSidebarClass::InitIO()
+{
+ if (!Phobos::UI::SuperWeaponSidebar || Unsorted::ArmageddonMode)
+ return;
+
+ if (const auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex]))
+ {
+ const auto pOnPCX = pSideExt->SuperWeaponSidebar_OnPCX.GetSurface();
+ const auto pOffPCX = pSideExt->SuperWeaponSidebar_OffPCX.GetSurface();
+ int width = 0, height = 0;
+
+ if (pOnPCX)
+ {
+ if (pOffPCX)
+ {
+ width = std::max(pOnPCX->GetWidth(), pOffPCX->GetWidth());
+ height = std::max(pOnPCX->GetHeight(), pOffPCX->GetHeight());
+ }
+ else
+ {
+ width = pOnPCX->GetWidth();
+ height = pOnPCX->GetHeight();
+ }
+ }
+ else if (pOffPCX)
+ {
+ width = pOffPCX->GetWidth();
+ height = pOffPCX->GetHeight();
+ }
+
+ if (width > 0 && height > 0)
+ {
+ if (const auto toggleButton = GameCreate(SWButtonClass::StartID + SuperWeaponTypeClass::Array.Count, 0, 0, width, height))
+ {
+ toggleButton->Zap();
+ GScreenClass::Instance.AddButton(toggleButton);
+ SWSidebarClass::Instance.ToggleButton = toggleButton;
+ toggleButton->UpdatePosition();
+ }
+ }
+ }
+
+ for (const auto superIdx : ScenarioExt::Global()->SWSidebar_Indices)
+ SWSidebarClass::Instance.AddButton(superIdx);
+}
+
+bool SWSidebarClass::AddButton(int superIdx)
+{
+ if (!Phobos::UI::SuperWeaponSidebar || this->DisableEntry)
+ return false;
+
+ const auto pSWType = SuperWeaponTypeClass::Array.GetItemOrDefault(superIdx);
+
+ if (!pSWType)
+ return false;
+
+ const auto pSWExt = SWTypeExt::ExtMap.Find(pSWType);
+
+ if (!pSWExt->SW_ShowCameo || !pSWExt->SuperWeaponSidebar_Allow.Get(RulesExt::Global()->SuperWeaponSidebar_AllowByDefault))
+ return false;
+
+ const unsigned int ownerBits = 1u << HouseClass::CurrentPlayer->Type->ArrayIndex;
+
+ if ((pSWExt->SuperWeaponSidebar_RequiredHouses & ownerBits) == 0)
+ return false;
+
+ if (pSWExt->SuperWeaponSidebar_Significance < Phobos::Config::SuperWeaponSidebar_RequiredSignificance)
+ return false;
+
+ auto& columns = this->Columns;
+
+ if (columns.empty() && !this->AddColumn())
+ return false;
+
+ if (std::any_of(columns.begin(), columns.end(), [superIdx](SWColumnClass* column) { return std::any_of(column->Buttons.begin(), column->Buttons.end(), [superIdx](SWButtonClass* button) { return button->SuperIndex == superIdx; }); }))
+ return true;
+
+ return columns.back()->AddButton(superIdx);
+}
+
+void SWSidebarClass::SortButtons()
+{
+ auto& columns = this->Columns;
+
+ if (columns.empty())
+ {
+ if (const auto toggleButton = this->ToggleButton)
+ toggleButton->UpdatePosition();
+
+ return;
+ }
+
+ std::vector vec_Buttons;
+ vec_Buttons.reserve(this->GetMaximumButtonCount());
+
+ for (const auto column : columns)
+ {
+ for (const auto button : column->Buttons)
+ vec_Buttons.emplace_back(button);
+
+ column->ClearButtons(false);
+ }
+
+ const unsigned int ownerBits = 1u << HouseClass::CurrentPlayer->Type->ArrayIndex;
+
+ std::stable_sort(vec_Buttons.begin(), vec_Buttons.end(), [ownerBits](SWButtonClass* const a, SWButtonClass* const b)
+ {
+ const auto pExtA = SWTypeExt::ExtMap.Find(SuperWeaponTypeClass::Array.GetItemOrDefault(a->SuperIndex));
+ const auto pExtB = SWTypeExt::ExtMap.Find(SuperWeaponTypeClass::Array.GetItemOrDefault(b->SuperIndex));
+
+ if (pExtB && (pExtB->SuperWeaponSidebar_PriorityHouses & ownerBits) && (!pExtA || !(pExtA->SuperWeaponSidebar_PriorityHouses & ownerBits)))
+ return false;
+
+ if ((!pExtB || !(pExtB->SuperWeaponSidebar_PriorityHouses & ownerBits)) && pExtA && (pExtA->SuperWeaponSidebar_PriorityHouses & ownerBits))
+ return true;
+
+ return BuildType::SortsBefore(AbstractType::Special, a->SuperIndex, AbstractType::Special, b->SuperIndex);
+ });
+
+ const int buttonCount = static_cast(vec_Buttons.size());
+ const int cameoWidth = 60, cameoHeight = 48;
+ const int maximum = Phobos::UI::SuperWeaponSidebar_Max;
+ const int cameoHarfInterval = (Phobos::UI::SuperWeaponSidebar_CameoHeight - cameoHeight) / 2;
+ int location_Y = (DSurface::ViewBounds.Height - std::min(buttonCount, maximum) * Phobos::UI::SuperWeaponSidebar_CameoHeight) / 2;
+ Point2D location = { Phobos::UI::SuperWeaponSidebar_LeftOffset, location_Y + cameoHarfInterval };
+ int rowIdx = 0, columnIdx = 0;
+
+ for (const auto button : vec_Buttons)
+ {
+ const auto column = columns[columnIdx];
+
+ if (rowIdx == 0)
+ {
+ const auto pTopPCX = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex])->SuperWeaponSidebar_TopPCX.GetSurface();
+ column->SetPosition(location.X - Phobos::UI::SuperWeaponSidebar_LeftOffset, location_Y - (pTopPCX ? pTopPCX->GetHeight() : 0));
+ }
+
+ column->Buttons.emplace_back(button);
+ button->SetColumn(columnIdx);
+ button->SetPosition(location.X, location.Y);
+ rowIdx++;
+
+ if (rowIdx >= maximum - columnIdx)
+ {
+ rowIdx = 0;
+ columnIdx++;
+ location_Y += Phobos::UI::SuperWeaponSidebar_CameoHeight / 2;
+ location.X += cameoWidth + Phobos::UI::SuperWeaponSidebar_Interval;
+ location.Y = location_Y + cameoHarfInterval;
+ }
+ else
+ {
+ location.Y += Phobos::UI::SuperWeaponSidebar_CameoHeight;
+ }
+ }
+
+ for (const auto column : columns)
+ column->SetHeight(column->Buttons.size() * Phobos::UI::SuperWeaponSidebar_CameoHeight);
+}
+
+int SWSidebarClass::GetMaximumButtonCount()
+{
+ const int firstColumn = Phobos::UI::SuperWeaponSidebar_Max;
+ const int columns = std::min(firstColumn, Phobos::UI::SuperWeaponSidebar_MaxColumns);
+ return (firstColumn + (firstColumn - (columns - 1))) * columns / 2;
+}
+
+bool SWSidebarClass::IsEnabled()
+{
+ return ScenarioExt::Global()->SWSidebar_Enable;
+}
+
+void SWSidebarClass::RecheckCameo()
+{
+ auto& sidebar = SWSidebarClass::Instance;
+
+ for (const auto& column : sidebar.Columns)
+ {
+ std::vector removeButtons;
+
+ for (const auto& button : column->Buttons)
+ {
+ if (HouseClass::CurrentPlayer->Supers[button->SuperIndex]->IsPresent)
+ continue;
+
+ removeButtons.push_back(button->SuperIndex);
+ }
+
+ if (removeButtons.size())
+ HouseClass::CurrentPlayer->RecheckTechTree = true;
+
+ for (const auto& index : removeButtons)
+ column->RemoveButton(index);
+ }
+
+ sidebar.SortButtons();
+ int removes = 0;
+
+ for (const auto& column : sidebar.Columns)
+ {
+ if (column->Buttons.empty())
+ ++removes;
+ }
+
+ for (; removes > 0; --removes)
+ sidebar.RemoveColumn();
+
+ if (const auto toggleButton = sidebar.ToggleButton)
+ toggleButton->UpdatePosition();
+}
+
+// Hooks
+
+DEFINE_HOOK(0x4F92FB, HouseClass_UpdateTechTree_SWSidebar, 0x7)
+{
+ enum { SkipGameCode = 0x4F9302 };
+
+ GET(HouseClass*, pHouse, ESI);
+
+ pHouse->AISupers();
+
+ if (pHouse->IsCurrentPlayer())
+ SWSidebarClass::RecheckCameo();
+
+ return SkipGameCode;
+}
+
+DEFINE_HOOK(0x6A6316, SidebarClass_AddCameo_SuperWeapon_SWSidebar, 0x6)
+{
+ enum { ReturnFalse = 0x6A65FF };
+
+ GET_STACK(AbstractType, whatAmI, STACK_OFFSET(0x14, 0x4));
+
+ if (whatAmI != AbstractType::Special && whatAmI != AbstractType::SuperWeaponType && whatAmI != AbstractType::Super)
+ return 0;
+
+ GET_STACK(int, index, STACK_OFFSET(0x14, 0x8));
+
+ if (SWSidebarClass::Instance.AddButton(index))
+ {
+ ScenarioExt::Global()->SWSidebar_Indices.emplace_back(index);
+ return ReturnFalse;
+ }
+
+ return 0;
+}
+
+DEFINE_HOOK(0x6AA790, StripClass_RecheckCameo_RemoveCameo, 0x6)
+{
+ enum { ShouldRemove = 0x6AA7B6, ShouldNotRemove = 0x6AAA68 };
+
+ GET(BuildType*, pItem, ESI);
+ const auto pCurrent = HouseClass::CurrentPlayer;
+ const auto& supers = pCurrent->Supers;
+
+ if (supers.ValidIndex(pItem->ItemIndex) && supers[pItem->ItemIndex]->IsPresent)
+ {
+ if (SWSidebarClass::Instance.AddButton(pItem->ItemIndex))
+ ScenarioExt::Global()->SWSidebar_Indices.emplace_back(pItem->ItemIndex);
+ else
+ return ShouldNotRemove;
+ }
+
+ return ShouldRemove;
+}
diff --git a/src/Ext/Sidebar/SWSidebar/SWSidebarClass.h b/src/Ext/Sidebar/SWSidebar/SWSidebarClass.h
new file mode 100644
index 0000000000..5cfaf415a9
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/SWSidebarClass.h
@@ -0,0 +1,35 @@
+#pragma once
+#include "SWButtonClass.h"
+#include "SWColumnClass.h"
+#include "ToggleSWButtonClass.h"
+#include
+#include
+
+class SWSidebarClass
+{
+public:
+ bool AddColumn();
+ bool RemoveColumn();
+
+ void InitClear();
+ void InitIO();
+
+ bool AddButton(int superIdx);
+ void SortButtons();
+
+ int GetMaximumButtonCount();
+
+ static bool IsEnabled();
+ static void RecheckCameo();
+
+ static SWSidebarClass Instance;
+
+public:
+ std::vector Columns {};
+ SWColumnClass* CurrentColumn { nullptr };
+ SWButtonClass* CurrentButton { nullptr };
+ ToggleSWButtonClass* ToggleButton { nullptr };
+ bool DisableEntry { false };
+
+ static CommandClass* Commands[10];
+};
diff --git a/src/Ext/Sidebar/SWSidebar/ToggleSWButtonClass.cpp b/src/Ext/Sidebar/SWSidebar/ToggleSWButtonClass.cpp
new file mode 100644
index 0000000000..92204e1c1f
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/ToggleSWButtonClass.cpp
@@ -0,0 +1,130 @@
+#include "ToggleSWButtonClass.h"
+#include "SWSidebarClass.h"
+#include
+#include
+
+#include
+
+ToggleSWButtonClass::ToggleSWButtonClass(unsigned int id, int x, int y, int width, int height)
+ : ControlClass(id, x, y, width, height, (GadgetFlag::LeftPress | GadgetFlag::LeftRelease), false)
+{
+ SWSidebarClass::Instance.ToggleButton = this;
+}
+
+bool ToggleSWButtonClass::Draw(bool forced)
+{
+ auto& columns = SWSidebarClass::Instance.Columns;
+
+ if (columns.empty())
+ return false;
+
+ const auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex]);
+ const auto pTogglePCX = SWSidebarClass::IsEnabled() ? pSideExt->SuperWeaponSidebar_OnPCX.GetSurface() : pSideExt->SuperWeaponSidebar_OffPCX.GetSurface();
+
+ if (!pTogglePCX)
+ return false;
+
+ RectangleStruct destRect { this->X, this->Y, this->Width, this->Height };
+ PCX::Instance.BlitToSurface(&destRect, DSurface::Composite, pTogglePCX);
+
+ if (this->IsHovering)
+ {
+ const COLORREF tooltipColor = Drawing::RGB_To_Int(Drawing::TooltipColor);
+ DSurface::Composite->DrawRect(&destRect, tooltipColor);
+ }
+
+ return true;
+}
+
+void ToggleSWButtonClass::OnMouseEnter()
+{
+ auto& columns = SWSidebarClass::Instance.Columns;
+
+ if (columns.empty())
+ return;
+
+ this->IsHovering = true;
+ MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false);
+}
+
+void ToggleSWButtonClass::OnMouseLeave()
+{
+ this->IsHovering = false;
+ this->IsPressed = false;
+ MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false);
+}
+
+bool ToggleSWButtonClass::Action(GadgetFlag flags, DWORD* pKey, KeyModifier modifier)
+{
+ auto& columns = SWSidebarClass::Instance.Columns;
+
+ if (columns.empty())
+ return false;
+
+ const auto pSideExt = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex]);
+
+ if (SWSidebarClass::IsEnabled() ? !pSideExt->SuperWeaponSidebar_OnPCX.GetSurface() : !pSideExt->SuperWeaponSidebar_OffPCX.GetSurface())
+ return false;
+
+ if (flags & GadgetFlag::LeftPress)
+ this->IsPressed = true;
+
+ if ((flags & GadgetFlag::LeftRelease) && this->IsPressed)
+ {
+ this->IsPressed = false;
+ ToggleSWButtonClass::SwitchSidebar();
+ }
+
+ // this->ControlClass::Action(flags, pKey, KeyModifier::None);
+ reinterpret_cast(0x48E5A0)(this, flags, pKey, KeyModifier::None);
+ return true;
+}
+
+void ToggleSWButtonClass::UpdatePosition()
+{
+ Point2D position = Point2D::Empty;
+ auto& columns = SWSidebarClass::Instance.Columns;
+
+ if (!columns.empty())
+ {
+ const auto backColumn = columns.back();
+ position.X = SWSidebarClass::Instance.IsEnabled() ? backColumn->X + backColumn->Width : 0;
+ position.Y = backColumn->Y + (backColumn->Height - this->Height) / 2;
+ }
+ else
+ {
+ position.X = 0;
+ position.Y = (GameOptionsClass::Instance.ScreenHeight - this->Height) / 2;
+ }
+
+ this->SetPosition(position.X, position.Y);
+}
+
+bool ToggleSWButtonClass::SwitchSidebar()
+{
+ VocClass::PlayGlobal(RulesClass::Instance->GUIMainButtonSound, 0x2000, 1.0);
+ ScenarioExt::Global()->SWSidebar_Enable = !ScenarioExt::Global()->SWSidebar_Enable;
+
+ const bool disabled = !SWSidebarClass::IsEnabled();
+ const auto& columns = SWSidebarClass::Instance.Columns;
+
+ if (!columns.empty())
+ {
+ for (const auto& pColumn : columns)
+ {
+ pColumn->Disabled = disabled;
+ const auto& buttons = pColumn->Buttons;
+
+ if (!buttons.empty())
+ {
+ for (const auto& pButton : buttons)
+ pButton->Disabled = disabled;
+ }
+ }
+ }
+
+ if (const auto pToggle = SWSidebarClass::Instance.ToggleButton)
+ pToggle->UpdatePosition();
+
+ return !disabled;
+}
diff --git a/src/Ext/Sidebar/SWSidebar/ToggleSWButtonClass.h b/src/Ext/Sidebar/SWSidebar/ToggleSWButtonClass.h
new file mode 100644
index 0000000000..45e293f1ce
--- /dev/null
+++ b/src/Ext/Sidebar/SWSidebar/ToggleSWButtonClass.h
@@ -0,0 +1,24 @@
+#pragma once
+#include
+
+class ToggleSWButtonClass : public ControlClass
+{
+public:
+ ToggleSWButtonClass() = default;
+ ToggleSWButtonClass(unsigned int id, int x, int y, int width, int height);
+
+ ~ToggleSWButtonClass() = default;
+
+ virtual bool Draw(bool forced) override;
+ virtual void OnMouseEnter() override;
+ virtual void OnMouseLeave() override;
+ virtual bool Action(GadgetFlag fags, DWORD* pKey, KeyModifier modifier) override;
+
+ void UpdatePosition();
+
+ static bool SwitchSidebar();
+
+public:
+ bool IsHovering { false };
+ bool IsPressed { false };
+};
diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp
index 73bf7c06c0..6321cacf6e 100644
--- a/src/Misc/PhobosToolTip.cpp
+++ b/src/Misc/PhobosToolTip.cpp
@@ -17,6 +17,7 @@
#include
#include
#include
+#include
#include
#include
@@ -230,6 +231,41 @@ DEFINE_HOOK(0x6A9316, SidebarClass_StripClass_HelpText, 0x6)
return 0x6A93DE;
}
+DEFINE_HOOK(0x4AE51E, DisplayClass_GetToolTip_HelpText, 0x6)
+{
+ enum { ApplyToolTip = 0x4AE69D };
+
+ if (SWSidebarClass::IsEnabled())
+ {
+ const auto& swSidebar = SWSidebarClass::Instance;
+
+ if (const auto button = swSidebar.CurrentButton)
+ {
+ PhobosToolTip::Instance.IsCameo = true;
+
+ if (PhobosToolTip::Instance.IsEnabled())
+ {
+ PhobosToolTip::Instance.HelpText_Super(button->SuperIndex);
+ R->EAX(PhobosToolTip::Instance.GetBuffer());
+ }
+ else
+ {
+ const auto pSuper = HouseClass::CurrentPlayer->Supers[button->SuperIndex];
+ R->EAX(pSuper->Type->UIName);
+ }
+
+ return ApplyToolTip;
+ }
+ else if (swSidebar.CurrentColumn || (swSidebar.ToggleButton && swSidebar.ToggleButton->IsHovering))
+ {
+ R->EAX(0);
+ return ApplyToolTip;
+ }
+ }
+
+ return 0;
+}
+
// TODO: reimplement CCToolTip::Draw2 completely
DEFINE_HOOK(0x478EE1, CCToolTip_Draw2_SetBuffer, 0x6)
@@ -282,7 +318,6 @@ DEFINE_HOOK(0x478EF8, CCToolTip_Draw2_SetMaxWidth, 0x5)
R->EAX(Phobos::UI::MaxToolTipWidth);
else
R->EAX(DSurface::ViewBounds.Width);
-
}
return 0;
}
@@ -337,14 +372,57 @@ DEFINE_HOOK(0x478FDC, CCToolTip_Draw2_FillRect, 0x5)
const bool isCameo = PhobosToolTip::Instance.IsCameo;
- if (isCameo && Phobos::UI::AnchoredToolTips && PhobosToolTip::Instance.IsEnabled() && Phobos::Config::ToolTipDescriptions)
+ if (isCameo && Phobos::UI::AnchoredToolTips
+ && PhobosToolTip::Instance.IsEnabled()
+ && Phobos::Config::ToolTipDescriptions)
{
- LEA_STACK(LTRBStruct*, a2, STACK_OFFSET(0x44, -0x20));
- auto x = DSurface::SidebarBounds.X - pRect->Width - 2;
+ LEA_STACK(LTRBStruct*, pTextRect, STACK_OFFSET(0x44, -0x20));
+
+ if (const auto pButton = SWSidebarClass::Instance.CurrentButton)
+ {
+ // Being too far away may actually be bad
+ /*
+ const auto& columns = SWSidebarClass::Instance.Columns;
+ const int size = columns.size();
+ const int y = pButton->Y + 3;
+
+ auto pColumn = columns.back();
+ if (columns.size() > 1 && y > (pColumn->Y + pColumn->Height))
+ pColumn = columns[size - 2];
+
+ const int x = pColumn->X + pColumn->Width + 2;
+ */
+ const auto pColumn = SWSidebarClass::Instance.Columns[pButton->ColumnIndex];
+ const int x = pColumn->X + pColumn->Width + 2;
+ const int y = pButton->Y + 3;
+ pRect->X = x;
+ pTextRect->Right += (x - pTextRect->Left);
+ pTextRect->Left = x;
+ pRect->Y = y;
+ pTextRect->Bottom += (y - pTextRect->Top);
+ pTextRect->Top = y;
+ }
+ else
+ {
+ const int x = DSurface::SidebarBounds.X - pRect->Width - 2;
+ pRect->X = x;
+ pTextRect->Left = x;
+ pRect->Y -= 40;
+ pTextRect->Top -= 40;
+ }
+ }
+ else if (const auto pButton = SWSidebarClass::Instance.CurrentButton)
+ {
+ LEA_STACK(LTRBStruct*, pTextRect, STACK_OFFSET(0x44, -0x20));
+
+ const int x = pButton->X + pButton->Width;
+ const int y = pButton->Y + 43;
pRect->X = x;
- a2->Left = x;
- pRect->Y -= 40;
- a2->Top -= 40;
+ pTextRect->Right += (x - pTextRect->Left);
+ pTextRect->Left = x;
+ pRect->Y = y;
+ pTextRect->Bottom += (y - pTextRect->Top);
+ pTextRect->Top = y;
}
// Should we make some SideExt items as static to improve the effeciency?
diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp
index e22bdedde8..552cc8f8f6 100644
--- a/src/Phobos.INI.cpp
+++ b/src/Phobos.INI.cpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
@@ -32,6 +33,12 @@ bool Phobos::UI::PowerDelta_Show = false;
double Phobos::UI::PowerDelta_ConditionYellow = 0.75;
double Phobos::UI::PowerDelta_ConditionRed = 1.0;
bool Phobos::UI::CenterPauseMenuBackground = false;
+bool Phobos::UI::SuperWeaponSidebar = false;
+int Phobos::UI::SuperWeaponSidebar_Interval = 0;
+int Phobos::UI::SuperWeaponSidebar_LeftOffset = 0;
+int Phobos::UI::SuperWeaponSidebar_CameoHeight = 48;
+int Phobos::UI::SuperWeaponSidebar_Max = 0;
+int Phobos::UI::SuperWeaponSidebar_MaxColumns = INT32_MAX;
bool Phobos::UI::WeedsCounter_Show = false;
bool Phobos::UI::AnchoredToolTips = false;
@@ -39,6 +46,7 @@ bool Phobos::Config::ToolTipDescriptions = true;
bool Phobos::Config::ToolTipBlur = false;
bool Phobos::Config::PrioritySelectionFiltering = true;
bool Phobos::Config::DevelopmentCommands = true;
+bool Phobos::Config::SuperWeaponSidebarCommands = false;
bool Phobos::Config::ShowPlanningPath = false;
bool Phobos::Config::ArtImageSwap = false;
bool Phobos::Config::ShowPlacementPreview = false;
@@ -58,6 +66,7 @@ bool Phobos::Config::ShowWeedsCounter = false;
bool Phobos::Config::HideLightFlashEffects = true;
bool Phobos::Config::ShowFlashOnSelecting = false;
bool Phobos::Config::UnitPowerDrain = false;
+int Phobos::Config::SuperWeaponSidebar_RequiredSignificance = 0;
bool Phobos::Misc::CustomGS = false;
int Phobos::Misc::CustomGS_ChangeInterval[7] = { -1, -1, -1, -1, -1, -1, -1 };
@@ -66,24 +75,27 @@ int Phobos::Misc::CustomGS_DefaultDelay[7] = { 0, 1, 2, 3, 4, 5, 6 };
DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
{
- Phobos::Config::ToolTipDescriptions = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ToolTipDescriptions", true);
- Phobos::Config::ToolTipBlur = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ToolTipBlur", false);
- Phobos::Config::PrioritySelectionFiltering = CCINIClass::INI_RA2MD.ReadBool("Phobos", "PrioritySelectionFiltering", true);
- Phobos::Config::ShowPlacementPreview = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowPlacementPreview", true);
- Phobos::Config::RealTimeTimers = CCINIClass::INI_RA2MD.ReadBool("Phobos", "RealTimeTimers", false);
- Phobos::Config::RealTimeTimers_Adaptive = CCINIClass::INI_RA2MD.ReadBool("Phobos", "RealTimeTimers.Adaptive", false);
- Phobos::Config::EnableSelectBox = CCINIClass::INI_RA2MD.ReadBool("Phobos", "EnableSelectBox", false);
- Phobos::Config::DigitalDisplay_Enable = CCINIClass::INI_RA2MD.ReadBool("Phobos", "DigitalDisplay.Enable", false);
- Phobos::Config::SaveGameOnScenarioStart = CCINIClass::INI_RA2MD.ReadBool("Phobos", "SaveGameOnScenarioStart", true);
- Phobos::Config::ShowBriefing = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowBriefing", true);
- Phobos::Config::ShowPowerDelta = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowPowerDelta", true);
- Phobos::Config::ShowHarvesterCounter = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowHarvesterCounter", true);
- Phobos::Config::ShowWeedsCounter = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowWeedsCounter", true);
- Phobos::Config::HideLightFlashEffects = CCINIClass::INI_RA2MD.ReadBool("Phobos", "HideLightFlashEffects", false);
- Phobos::Config::ShowFlashOnSelecting = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowFlashOnSelecting", false);
+ const auto phobosSection = "Phobos";
+
+ Phobos::Config::ToolTipDescriptions = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ToolTipDescriptions", true);
+ Phobos::Config::ToolTipBlur = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ToolTipBlur", false);
+ Phobos::Config::PrioritySelectionFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PrioritySelectionFiltering", true);
+ Phobos::Config::ShowPlacementPreview = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowPlacementPreview", true);
+ Phobos::Config::RealTimeTimers = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "RealTimeTimers", false);
+ Phobos::Config::RealTimeTimers_Adaptive = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "RealTimeTimers.Adaptive", false);
+ Phobos::Config::EnableSelectBox = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "EnableSelectBox", false);
+ Phobos::Config::DigitalDisplay_Enable = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "DigitalDisplay.Enable", false);
+ Phobos::Config::SaveGameOnScenarioStart = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "SaveGameOnScenarioStart", true);
+ Phobos::Config::ShowBriefing = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowBriefing", true);
+ Phobos::Config::ShowPowerDelta = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowPowerDelta", true);
+ Phobos::Config::ShowHarvesterCounter = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowHarvesterCounter", true);
+ Phobos::Config::ShowWeedsCounter = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowWeedsCounter", true);
+ Phobos::Config::HideLightFlashEffects = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "HideLightFlashEffects", false);
+ Phobos::Config::ShowFlashOnSelecting = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowFlashOnSelecting", false);
+ Phobos::Config::SuperWeaponSidebar_RequiredSignificance = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "SuperWeaponSidebar.RequiredSignificance", 0);
// Custom game speeds, 6 - i so that GS6 is index 0, just like in the engine
- Phobos::Config::CampaignDefaultGameSpeed = 6 - CCINIClass::INI_RA2MD.ReadInteger("Phobos", "CampaignDefaultGameSpeed", 4);
+ Phobos::Config::CampaignDefaultGameSpeed = 6 - CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "CampaignDefaultGameSpeed", 4);
if (Phobos::Config::CampaignDefaultGameSpeed > 6 || Phobos::Config::CampaignDefaultGameSpeed < 0)
{
Phobos::Config::CampaignDefaultGameSpeed = 2;
@@ -96,7 +108,7 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
Patch::Apply_RAW(0x55D78D, { temp }); // when speed control is off. Doesn't need a hook.
}
- Phobos::Config::ShowDesignatorRange = CCINIClass::INI_RA2MD.ReadBool("Phobos", "ShowDesignatorRange", false);
+ Phobos::Config::ShowDesignatorRange = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowDesignatorRange", false);
CCINIClass ini_uimd {};
ini_uimd.LoadFromFile(GameStrings::UIMD_INI);
@@ -165,6 +177,36 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
Phobos::UI::CenterPauseMenuBackground =
ini_uimd.ReadBool(SIDEBAR_SECTION, "CenterPauseMenuBackground", Phobos::UI::CenterPauseMenuBackground);
+
+ Phobos::UI::SuperWeaponSidebar =
+ ini_uimd.ReadBool(SIDEBAR_SECTION, "SuperWeaponSidebar", Phobos::UI::SuperWeaponSidebar);
+
+ Phobos::UI::SuperWeaponSidebar_Interval =
+ ini_uimd.ReadInteger(SIDEBAR_SECTION, "SuperWeaponSidebar.Interval", Phobos::UI::SuperWeaponSidebar_Interval);
+
+ Phobos::UI::SuperWeaponSidebar_LeftOffset =
+ ini_uimd.ReadInteger(SIDEBAR_SECTION, "SuperWeaponSidebar.LeftOffset", Phobos::UI::SuperWeaponSidebar_LeftOffset);
+
+ Phobos::UI::SuperWeaponSidebar_LeftOffset = std::min(Phobos::UI::SuperWeaponSidebar_Interval, Phobos::UI::SuperWeaponSidebar_LeftOffset);
+
+ Phobos::UI::SuperWeaponSidebar_CameoHeight =
+ ini_uimd.ReadInteger(SIDEBAR_SECTION, "SuperWeaponSidebar.CameoHeight", Phobos::UI::SuperWeaponSidebar_CameoHeight);
+
+ Phobos::UI::SuperWeaponSidebar_CameoHeight = std::max(48, Phobos::UI::SuperWeaponSidebar_CameoHeight);
+
+ Phobos::UI::SuperWeaponSidebar_Max =
+ ini_uimd.ReadInteger(SIDEBAR_SECTION, "SuperWeaponSidebar.Max", Phobos::UI::SuperWeaponSidebar_Max);
+
+ const int reserveHeight = 96;
+ const int screenHeight = GameOptionsClass::Instance.ScreenHeight - reserveHeight;
+
+ if (Phobos::UI::SuperWeaponSidebar_Max > 0)
+ Phobos::UI::SuperWeaponSidebar_Max = std::min(Phobos::UI::SuperWeaponSidebar_Max, screenHeight / Phobos::UI::SuperWeaponSidebar_CameoHeight);
+ else
+ Phobos::UI::SuperWeaponSidebar_Max = screenHeight / Phobos::UI::SuperWeaponSidebar_CameoHeight;
+
+ Phobos::UI::SuperWeaponSidebar_MaxColumns =
+ ini_uimd.ReadInteger(SIDEBAR_SECTION, "SuperWeaponSidebar.MaxColumns", Phobos::UI::SuperWeaponSidebar_MaxColumns);
}
// UISettings
@@ -222,6 +264,7 @@ DEFINE_HOOK(0x52D21F, InitRules_ThingsThatShouldntBeSerailized, 0x6)
#ifndef DEBUG
Phobos::Config::DevelopmentCommands = pINI_RULESMD->ReadBool("GlobalControls", "DebugKeysEnabled", Phobos::Config::DevelopmentCommands);
#endif
+ Phobos::Config::SuperWeaponSidebarCommands = pINI_RULESMD->ReadBool("GlobalControls", "SuperWeaponSidebarKeysEnabled", Phobos::Config::SuperWeaponSidebarCommands);
Phobos::Config::ShowPlanningPath = pINI_RULESMD->ReadBool("GlobalControls", "DebugPlanningPaths", Phobos::Config::ShowPlanningPath);
return 0;
diff --git a/src/Phobos.h b/src/Phobos.h
index d7e993fd7e..3573722d64 100644
--- a/src/Phobos.h
+++ b/src/Phobos.h
@@ -54,6 +54,12 @@ class Phobos
static double PowerDelta_ConditionYellow;
static double PowerDelta_ConditionRed;
static bool CenterPauseMenuBackground;
+ static bool SuperWeaponSidebar;
+ static int SuperWeaponSidebar_Interval;
+ static int SuperWeaponSidebar_LeftOffset;
+ static int SuperWeaponSidebar_CameoHeight;
+ static int SuperWeaponSidebar_Max;
+ static int SuperWeaponSidebar_MaxColumns;
static bool WeedsCounter_Show;
static bool AnchoredToolTips;
@@ -74,6 +80,7 @@ class Phobos
static bool ToolTipBlur;
static bool PrioritySelectionFiltering;
static bool DevelopmentCommands;
+ static bool SuperWeaponSidebarCommands;
static bool ArtImageSwap;
static bool ShowPlacementPreview;
static bool EnableBuildingPlacementPreview;
@@ -94,6 +101,7 @@ class Phobos
static bool HideLightFlashEffects;
static bool ShowFlashOnSelecting;
static bool UnitPowerDrain;
+ static int SuperWeaponSidebar_RequiredSignificance;
};
class Misc
diff --git a/src/Utilities/AresAddressInit.cpp b/src/Utilities/AresAddressInit.cpp
index a6eca44b4e..1c4830c43c 100644
--- a/src/Utilities/AresAddressInit.cpp
+++ b/src/Utilities/AresAddressInit.cpp
@@ -6,6 +6,7 @@
decltype(AresFunctions::ConvertTypeTo) AresFunctions::ConvertTypeTo = nullptr;
decltype(AresFunctions::CreateAresEBolt) AresFunctions::CreateAresEBolt = nullptr;
decltype(AresFunctions::SpawnSurvivors) AresFunctions::SpawnSurvivors = nullptr;
+decltype(AresFunctions::IsTargetConstraintsEligible) AresFunctions::IsTargetConstraintsEligible = nullptr;
std::function AresFunctions::SWTypeExtMap_Find;
PhobosMap* AresFunctions::AlphaExtMap = nullptr;
@@ -27,7 +28,11 @@ void AresFunctions::InitAres3_0()
Patch::Apply_RAW(AresHelper::AresBaseAddress + 0x48C69, { 0x30 });
}
else
+ {
NOTE_ARES_FUN(SpawnSurvivors, 0x464C0);
+ }
+
+ NOTE_ARES_FUN(IsTargetConstraintsEligible, 0x032110);
NOTE_ARES_FUN(_SWTypeExtMapFind, 0x57C70);
NOTE_ARES_FUN(_SWTypeExtMap, 0xC1C54);
@@ -52,7 +57,11 @@ void AresFunctions::InitAres3_0p1()
Patch::Apply_RAW(AresHelper::AresBaseAddress + 0x498B9, { 0x30 });
}
else
+ {
NOTE_ARES_FUN(SpawnSurvivors, 0x47030);
+ }
+
+ NOTE_ARES_FUN(IsTargetConstraintsEligible, 0x032AF0);
NOTE_ARES_FUN(_SWTypeExtMapFind, 0x58900);
NOTE_ARES_FUN(_SWTypeExtMap, 0xC2C50);
diff --git a/src/Utilities/AresFunctions.h b/src/Utilities/AresFunctions.h
index 83dcf1c7f7..2e3ff0ef58 100644
--- a/src/Utilities/AresFunctions.h
+++ b/src/Utilities/AresFunctions.h
@@ -31,10 +31,14 @@ class AresFunctions
static EBolt* (__stdcall* CreateAresEBolt)(WeaponTypeClass* pWeapon);
static void(__stdcall* SpawnSurvivors)(FootClass* pThis, TechnoClass* pKiller, bool Select, bool IgnoreDefenses);
+
+ static bool(__thiscall* IsTargetConstraintsEligible)(void*, HouseClass*, bool);
+
static std::function SWTypeExtMap_Find;
static PhobosMap* AlphaExtMap;
private:
+
static constexpr bool _maybe = false;
static constexpr bool AresWasWrongAboutSpawnSurvivors = _maybe;