diff --git a/CREDITS.md b/CREDITS.md
index 13ec35cbe6..37a6598a35 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -878,3 +878,4 @@ This page lists all the individual contributions to the project by their author.
- **Chang_zhi**:
- Interop export interface for accessing scenario local/global variables
- Add `ClampToScreen` tag for `BannerType` to control whether banner position is clamped to the visible area
+- **ABCZ (CZHczhABCZ)** - Passengers Cameo Display
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index f5437cd6ff..df818f4921 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -47,6 +47,7 @@
+
@@ -310,6 +311,7 @@
+
diff --git a/YRpp b/YRpp
index 85b5b5e185..f304f4fa13 160000
--- a/YRpp
+++ b/YRpp
@@ -1 +1 @@
-Subproject commit 85b5b5e1857a6e06e1341dc87d01c38b1104272e
+Subproject commit f304f4fa13bdaab0495a6c847a91bf8af2b9d932
diff --git a/docs/User-Interface.md b/docs/User-Interface.md
index 26fc2b8418..f37ad94ab3 100644
--- a/docs/User-Interface.md
+++ b/docs/User-Interface.md
@@ -526,6 +526,59 @@ In `RA2MD.INI`:
ShowPowerPlantEnhancerRange=false ; boolean
```
+### Show Cameo
+
+- You can now configure the game to display cameo icons and their counts above a selected vehicle or building, showing both passengers (`Passengers=`) and garrisoned occupants (`Occupier=`). Only shown when a single unit is selected (not box-selection).
+- Two activation modes are available:
+ - **Permanent mode**: Set `ShowCameo=true` (globally or per-unit) to always display cameos. Per-unit setting overrides the global setting. If a per-unit `ShowCameo` is explicitly set to `false`, cameos are never shown for that unit.
+ - **Toggleable mode**: Set `ShowCameo=false` (global) and `ShowCameo.Toggleable=true` (global), then use the [Toggle Cameos](#toggle-cameos) hotkey to switch the display on/off. The master switch `ShowCameo.Enable` in `RA2MD.INI` controls the toggle state.
+- Cameos are arranged in horizontal rows, centered above the selection bracket. When the number of cameo types exceeds `ShowCameo.PerRow`, additional rows are stacked above the bottom row. The first-entered unit is displayed at the top.
+- Translucency can be set via `ShowCameo.Translucency` (0/25/50/75, default 0). SHP cameos are affected; PCX cameos are not.
+- `ShowCameo.OverlapXY` and `ShowCameo.OffsetXY` control overlap between adjacent cameos. `ShowCameo.OverlapPrimary` (default true) determines whether the earlier cameo covers the later one. On the same row, only X overlap applies; when wrapping to a new row, only Y overlap applies.
+- A custom cameo shape (SHP) can be specified per passenger/occupant type via `ShowCameo.CustomShape` in `artmd.ini`. Animated SHPs are supported. A custom PCX can be specified via `ShowCameo.CustomPCX`. A custom palette can be specified via `ShowCameo.CustomPalette`; if not set, the palette falls back to `CameoPalette` or the default `cameo.pal`.
+
+```{hint}
+For example, with `ShowCameo.PerRow=1` (vertical stacking), `ShowCameo.OverlapPrimary=true` means the bottom cameo covers the one above it, preserving the bottom cameo's text label from being hidden.
+```
+
+- When toggled via the hotkey, messages are shown from CSF labels `MSG:ShowCameoEnabled` (fallback: "Cameos Display: Enabled") and `MSG:ShowCameoDisabled` (fallback: "Cameos Display: Disabled").
+
+In `RA2MD.INI`:
+```ini
+[Phobos]
+ShowCameo.Enable=true ; boolean
+```
+
+In `rulesmd.ini`:
+```ini
+[AudioVisual]
+ShowCameo=false ; boolean
+ShowCameo.Toggleable=false ; boolean
+ShowCameo.PerRow=5 ; integer
+ShowCameo.BottomOffset=0,0 ; X,Y
+ShowCameo.Translucency=0 ; translucency level (0/25/50/75)
+ShowCameo.OverlapXY=0,0 ; X,Y overlap percentage
+ShowCameo.OffsetXY=0,0 ; X,Y pixel offset
+ShowCameo.OverlapPrimary=true ; boolean
+
+[SOMETECHNO] ; TechnoType
+ShowCameo= ; boolean, default true
+ShowCameo.PerRow= ; integer, defaults to [AudioVisual]
+ShowCameo.BottomOffset= ; X,Y, defaults to [AudioVisual]
+ShowCameo.Translucency= ; translucency level (0/25/50/75), defaults to [AudioVisual]
+ShowCameo.OverlapXY= ; X,Y, defaults to [AudioVisual]
+ShowCameo.OffsetXY= ; X,Y, defaults to [AudioVisual]
+ShowCameo.OverlapPrimary= ; boolean, defaults to [AudioVisual]
+```
+
+In `artmd.ini`:
+```ini
+[SOMETECHNO] ; TechnoType
+ShowCameo.CustomShape= ; SHP filename
+ShowCameo.CustomPCX= ; PCX filename
+ShowCameo.CustomPalette= ; PAL filename
+```
+
## Hotkey Commands
### `[ ]` Display Damage Numbers
@@ -585,6 +638,11 @@ For this command to work in multiplayer - you need to use a version of [YRpp spa
- Deselect 1 or 5 object(s) from current selected objects.
- For localization add `TXT_DESELECT`, `TXT_DESELECT_DESC`, `TXT_DESELECT5` and `TXT_DESELECT5_DESC` into your `.csf` file.
+### `[ ]` Toggle Cameos
+
+- Switches on/off [cameo display](#show-cameo) in toggleable mode. Has no effect in permanent mode.
+- For localization add `TXT_SHOWCAMEO` and `TXT_SHOWCAMEO_DESC` into your `.csf` file.
+
## Loading screen
- PCX files can now be used as loadscreen images.
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 057e57276d..ec386108c0 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -607,6 +607,7 @@ HideShakeEffects=false ; boolean
- [Customize the step limit of the credits indicator](User-Interface.md#customize-the-step-limit-of-the-credits-indicator) (by Noble_Fish)
- [Disable the credits indicator smooth transition](User-Interface.md#disable-the-credits-indicator-smooth-transition) (by Noble_Fish)
- Add `selling`, `undeploying` and `harvesting` conditions to `DiscardOn` (by Noble_Fish)
+- [Show Cameo feature to display passenger/occupant cameos above selected vehicles/buildings](User-Interface.md#show-cameo) (by ABCZ)
#### 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 14b9044f56..ef90486e81 100644
--- a/src/Commands/Commands.cpp
+++ b/src/Commands/Commands.cpp
@@ -14,6 +14,7 @@
#include "ToggleMessageList.h"
#include "DeselectObject.h"
#include "DeselectObject5.h"
+#include "ToggleCameos.h"
#include
@@ -32,6 +33,7 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
MakeCommand();
MakeCommand();
MakeCommand();
+ MakeCommand();
if (Phobos::Config::SuperWeaponSidebarCommands)
{
diff --git a/src/Commands/ToggleCameos.cpp b/src/Commands/ToggleCameos.cpp
new file mode 100644
index 0000000000..5c9ad1355c
--- /dev/null
+++ b/src/Commands/ToggleCameos.cpp
@@ -0,0 +1,54 @@
+#include "ToggleCameos.h"
+
+#include
+#include
+#include
+#include
+
+const char* ToggleCameosCommandClass::GetName() const
+{
+ return "Toggle Cameos";
+}
+
+const wchar_t* ToggleCameosCommandClass::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_SHOWCAMEO", L"Toggle Cameos");
+}
+
+const wchar_t* ToggleCameosCommandClass::GetUICategory() const
+{
+ return CATEGORY_INTERFACE;
+}
+
+const wchar_t* ToggleCameosCommandClass::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_SHOWCAMEO_DESC", L"Show/hide cameo display.");
+}
+
+void ToggleCameosCommandClass::Execute(WWKey eInput) const
+{
+ // Ignore hotkey in permanent mode
+ if (RulesExt::Global()->ShowCameo)
+ return;
+
+ // Ignore hotkey if toggleable is off
+ if (!RulesExt::Global()->ShowCameo_Toggleable)
+ return;
+
+ Phobos::Config::ShowCameo_Enable = !Phobos::Config::ShowCameo_Enable;
+
+ auto PrintMessage = [](const wchar_t* pMessage)
+ {
+ MessageListClass::Instance.PrintMessage(
+ pMessage,
+ RulesClass::Instance->MessageDelay,
+ HouseClass::CurrentPlayer->ColorSchemeIndex,
+ true
+ );
+ };
+
+ if (Phobos::Config::ShowCameo_Enable)
+ PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:ShowCameoEnabled", L"Cameos Display: Enabled"));
+ else
+ PrintMessage(GeneralUtils::LoadStringUnlessMissing("MSG:ShowCameoDisabled", L"Cameos Display: Disabled"));
+}
\ No newline at end of file
diff --git a/src/Commands/ToggleCameos.h b/src/Commands/ToggleCameos.h
new file mode 100644
index 0000000000..d77beb37f7
--- /dev/null
+++ b/src/Commands/ToggleCameos.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "Commands.h"
+
+class ToggleCameosCommandClass : 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;
+};
\ No newline at end of file
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index be0e1d88d4..5cc0f735c8 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -453,6 +453,15 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->AutoRemoveEarliestBeacon.Read(exINI, GameStrings::General, "AutoRemoveEarliestBeacon");
this->AllowBeaconHotKeyInSinglePlayer.Read(exINI, GameStrings::General, "AllowBeaconHotKeyInSinglePlayer");
+ this->ShowCameo.Read(exINI, GameStrings::AudioVisual, "ShowCameo");
+ this->ShowCameo_Toggleable.Read(exINI, GameStrings::AudioVisual, "ShowCameo.Toggleable");
+ this->ShowCameo_PerRow.Read(exINI, GameStrings::AudioVisual, "ShowCameo.PerRow");
+ this->ShowCameo_BottomOffset.Read(exINI, GameStrings::AudioVisual, "ShowCameo.BottomOffset");
+ this->ShowCameo_Translucency.Read(exINI, GameStrings::AudioVisual, "ShowCameo.Translucency");
+ this->ShowCameo_OverlapXY.Read(exINI, GameStrings::AudioVisual, "ShowCameo.OverlapXY");
+ this->ShowCameo_OffsetXY.Read(exINI, GameStrings::AudioVisual, "ShowCameo.OffsetXY");
+ this->ShowCameo_OverlapPrimary.Read(exINI, GameStrings::AudioVisual, "ShowCameo.OverlapPrimary");
+
// Section AITargetTypes
int itemsCount = pINI->GetKeyCount("AITargetTypes");
for (int i = 0; i < itemsCount; ++i)
@@ -804,6 +813,14 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->SecondaryFireSequenceLandOnly)
.Process(this->AutoRemoveEarliestBeacon)
.Process(this->AllowBeaconHotKeyInSinglePlayer)
+ .Process(this->ShowCameo)
+ .Process(this->ShowCameo_Toggleable)
+ .Process(this->ShowCameo_PerRow)
+ .Process(this->ShowCameo_BottomOffset)
+ .Process(this->ShowCameo_Translucency)
+ .Process(this->ShowCameo_OverlapXY)
+ .Process(this->ShowCameo_OffsetXY)
+ .Process(this->ShowCameo_OverlapPrimary)
;
}
diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h
index a00603d273..d793c90f2e 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -384,6 +384,15 @@ class RulesExt
Valueable AutoRemoveEarliestBeacon;
Valueable AllowBeaconHotKeyInSinglePlayer;
+ Valueable ShowCameo;
+ Valueable ShowCameo_Toggleable;
+ Valueable ShowCameo_PerRow;
+ Valueable ShowCameo_BottomOffset;
+ TranslucencyLevel ShowCameo_Translucency;
+ Valueable ShowCameo_OverlapXY;
+ Valueable ShowCameo_OffsetXY;
+ Valueable ShowCameo_OverlapPrimary;
+
ExtData(RulesClass* OwnerObject) : Extension(OwnerObject)
, Storage_TiberiumIndex { -1 }
, HarvesterDumpAmount { 0.0f }
@@ -708,6 +717,15 @@ class RulesExt
, AutoRemoveEarliestBeacon { false }
, AllowBeaconHotKeyInSinglePlayer { false }
+
+ , ShowCameo { false }
+ , ShowCameo_Toggleable { false }
+ , ShowCameo_PerRow { 5 }
+ , ShowCameo_BottomOffset { { 0, 0 } }
+ , ShowCameo_Translucency { 0 }
+ , ShowCameo_OverlapXY { { 0, 0 } }
+ , ShowCameo_OffsetXY { { 0, 0 } }
+ , ShowCameo_OverlapPrimary { true }
{ }
virtual ~ExtData() = default;
diff --git a/src/Ext/Techno/Body.Visuals.cpp b/src/Ext/Techno/Body.Visuals.cpp
index ff47accb46..838d08f58e 100644
--- a/src/Ext/Techno/Body.Visuals.cpp
+++ b/src/Ext/Techno/Body.Visuals.cpp
@@ -3,6 +3,8 @@
#include
#include
#include
+#include
+#include
void TechnoExt::DrawSelfHealPips(TechnoClass* pThis, Point2D* pLocation, RectangleStruct* pBounds)
{
@@ -873,3 +875,345 @@ void TechnoExt::ShowPromoteAnim(TechnoClass* pThis)
else if (!eliteAnims.empty())
AnimExt::CreateRandomAnim(eliteAnims, pThis->GetCenterCoords(), pThis, pThis->Owner, true, true);
}
+
+void TechnoExt::DrawCameos(TechnoClass* pThis)
+{
+ if (ObjectClass::CurrentObjects.Count != 1)
+ return;
+
+ if (ObjectClass::CurrentObjects.GetItem(0) != pThis)
+ return;
+
+ const auto whatAmI = pThis->WhatAmI();
+
+ const auto pType = pThis->GetTechnoType();
+ const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pType);
+ const auto pRulesExt = RulesExt::Global();
+
+ // Per-unit explicitly disabled - skip entirely
+ if (pTypeExt->ShowCameo.isset() && !pTypeExt->ShowCameo.Get())
+ return;
+
+ // Determine if permanent mode is on (permanent mode bypasses master switch and toggleable)
+ const bool permanentMode = pTypeExt->ShowCameo.isset()
+ ? pTypeExt->ShowCameo.Get()
+ : pRulesExt->ShowCameo;
+
+ // Master switch check (skipped for permanent mode)
+ if (!permanentMode && !Phobos::Config::ShowCameo_Enable)
+ return;
+
+ // Toggleable check (skipped for permanent mode)
+ if (!permanentMode && !pRulesExt->ShowCameo_Toggleable)
+ return;
+
+ // Collect cameos: passengers + occupants
+ // Passengers are stored in LIFO linked list; for buildings we reverse to FIFO
+ // so that Y flip produces correct top-to-bottom layout.
+ // Occupants are stored in FIFO DynamicVectorClass and do NOT need reversing.
+ std::vector> cameoCounts;
+
+ // Step 1: Collect passengers into a temp vector (LIFO order)
+ std::vector> passengerCounts;
+ for (auto pUnit = pThis->Passengers.GetFirstPassenger(); pUnit; pUnit = abstract_cast(pUnit->NextObject))
+ {
+ const auto pUnitType = pUnit->GetTechnoType();
+ auto it = std::find_if(passengerCounts.begin(), passengerCounts.end(),
+ [pUnitType](const auto& pair) { return pair.first == pUnitType; });
+ if (it != passengerCounts.end())
+ it->second++;
+ else
+ passengerCounts.emplace_back(pUnitType, 1);
+ }
+
+ // For buildings, reverse passenger order: LIFO -> FIFO
+ if (whatAmI == AbstractType::Building)
+ std::reverse(passengerCounts.begin(), passengerCounts.end());
+
+ // Merge passengers into cameoCounts
+ for (const auto& item : passengerCounts)
+ cameoCounts.emplace_back(item.first, item.second);
+
+ // Step 2: Collect occupants (FIFO order, no reverse needed)
+ if (whatAmI == AbstractType::Building)
+ {
+ const auto pBuilding = static_cast(pThis);
+ for (int i = 0; i < pBuilding->Occupants.Count; i++)
+ {
+ const auto pOccupant = pBuilding->Occupants.GetItem(i);
+ const auto pOccupantType = pOccupant->GetTechnoType();
+ auto it = std::find_if(cameoCounts.begin(), cameoCounts.end(),
+ [pOccupantType](const auto& pair) { return pair.first == pOccupantType; });
+ if (it != cameoCounts.end())
+ it->second++;
+ else
+ cameoCounts.emplace_back(pOccupantType, 1);
+ }
+ }
+
+ if (cameoCounts.empty())
+ return;
+
+ const auto bracketPos = whatAmI == AbstractType::Building
+ ? GetBuildingSelectBracketPosition(pThis, BuildingSelectBracketPosition::Top)
+ : GetFootSelectBracketPosition(pThis, Anchor(HorizontalPosition::Center, VerticalPosition::Top));
+ Point2D offset = pTypeExt->ShowCameo_BottomOffset.isset()
+ ? pTypeExt->ShowCameo_BottomOffset.Get()
+ : pRulesExt->ShowCameo_BottomOffset.Get();
+ Point2D basePos = bracketPos;
+ basePos.X += offset.X;
+ basePos.Y += offset.Y + pType->PixelSelectionBracketDelta;
+
+ const int perRow = pTypeExt->ShowCameo_PerRow.isset()
+ ? pTypeExt->ShowCameo_PerRow.Get()
+ : pRulesExt->ShowCameo_PerRow.Get();
+ const int count = static_cast(cameoCounts.size());
+ const int rows = (count + perRow - 1) / perRow;
+
+ // Get overlap/offset/overlapPrimary settings
+ const Point2D overlapXY = pTypeExt->ShowCameo_OverlapXY.isset()
+ ? pTypeExt->ShowCameo_OverlapXY.Get()
+ : pRulesExt->ShowCameo_OverlapXY.Get();
+ const Point2D cameoOffsetXY = pTypeExt->ShowCameo_OffsetXY.isset()
+ ? pTypeExt->ShowCameo_OffsetXY.Get()
+ : pRulesExt->ShowCameo_OffsetXY.Get();
+ const bool overlapPrimary = pTypeExt->ShowCameo_OverlapPrimary.isset()
+ ? pTypeExt->ShowCameo_OverlapPrimary.Get()
+ : pRulesExt->ShowCameo_OverlapPrimary.Get();
+ // overlapPrimary=true: primary (earlier) covers secondary (later)
+ // overlapPrimary=false: secondary (later) covers primary (earlier)
+
+ // Pre-compute canvas sizes for each cameo (standard 60x48, or custom SHP dimensions)
+ struct CameoCanvas { int width; int height; };
+ std::vector canvases;
+ canvases.reserve(count);
+ for (const auto& item : cameoCounts)
+ {
+ const auto pCameoType = item.first;
+ const auto pCameoTypeExt = TechnoTypeExt::ExtMap.Find(pCameoType);
+ int w = 60, h = 48;
+ if (SHPStruct* pCustomCameo = pCameoTypeExt->ShowCameo_CustomShape)
+ {
+ w = pCustomCameo->Width;
+ h = pCustomCameo->Height;
+ }
+ else if (auto pCustomPCX = pCameoTypeExt->ShowCameo_CustomPCX.GetSurface())
+ {
+ w = pCustomPCX->GetWidth();
+ h = pCustomPCX->GetHeight();
+ }
+ canvases.push_back({ w, h });
+ }
+
+ // Step 1-2-3: Calculate positions sequentially (row layout + OverlapXY + OffsetXY)
+ // Overlap formula: overlap = coveredCanvasSize * (OverlapXY% / 100) + OffsetXY
+ // OverlapPrimary=true: overlap based on secondary cameo's size (secondary is covered)
+ // OverlapPrimary=false: overlap based on primary cameo's size (primary is covered)
+ // Base gap between adjacent icons: 2 pixels (hGap / vGap)
+ const int hGap = 2;
+ const int vGap = 2;
+ // Iterate from bottom row to top row, so that each row can reference the row below it
+ std::vector cameoPositions(count);
+ for (int row = 0; row < rows; row++)
+ {
+ const int firstIdx = row * perRow;
+ const int lastIdx = Math::min(firstIdx + perRow - 1, count - 1);
+ for (int idx = firstIdx; idx <= lastIdx; idx++)
+ {
+ const int col = idx - firstIdx;
+ const int curW = canvases[idx].width;
+ const int curH = canvases[idx].height;
+
+ if (row == 0)
+ {
+ // Bottom row: anchor at base position (bottom edge fixed at basePos.Y)
+ if (col == 0)
+ {
+ cameoPositions[idx].X = basePos.X;
+ cameoPositions[idx].Y = basePos.Y - curH;
+ }
+ else
+ {
+ // Same row: position to the right of the cameo directly to the left
+ const int prevIdx = idx - 1;
+ const int prevX = cameoPositions[prevIdx].X;
+ const int prevW = canvases[prevIdx].width;
+
+ const int overlapX = overlapPrimary
+ ? (curW * overlapXY.X / 100 + cameoOffsetXY.X)
+ : (prevW * overlapXY.X / 100 + cameoOffsetXY.X);
+
+ cameoPositions[idx].X = prevX + prevW + hGap - overlapX;
+ cameoPositions[idx].Y = cameoPositions[prevIdx].Y;
+ }
+ }
+ else
+ {
+ if (col == 0)
+ {
+ // First in row: position above the cameo directly below
+ const int belowIdx = (row - 1) * perRow;
+ const int belowY = cameoPositions[belowIdx].Y;
+ const int belowH = canvases[belowIdx].height;
+
+ const int overlapY = overlapPrimary
+ ? (curH * overlapXY.Y / 100 + cameoOffsetXY.Y)
+ : (belowH * overlapXY.Y / 100 + cameoOffsetXY.Y);
+
+ cameoPositions[idx].X = basePos.X;
+ cameoPositions[idx].Y = belowY - curH - vGap + overlapY;
+ }
+ else
+ {
+ // Same row: position to the right of the cameo directly to the left
+ const int prevIdx = idx - 1;
+ const int prevX = cameoPositions[prevIdx].X;
+ const int prevW = canvases[prevIdx].width;
+
+ const int overlapX = overlapPrimary
+ ? (curW * overlapXY.X / 100 + cameoOffsetXY.X)
+ : (prevW * overlapXY.X / 100 + cameoOffsetXY.X);
+
+ cameoPositions[idx].X = prevX + prevW + hGap - overlapX;
+ cameoPositions[idx].Y = cameoPositions[prevIdx].Y;
+ }
+ }
+ }
+ }
+
+ // Step 4: Center each row horizontally
+ for (int row = 0; row < rows; row++)
+ {
+ const int firstIdx = row * perRow;
+ const int lastIdx = Math::min(firstIdx + perRow - 1, count - 1);
+ const int rowWidth = cameoPositions[lastIdx].X + canvases[lastIdx].width - cameoPositions[firstIdx].X;
+ const int centerOffset = basePos.X - rowWidth / 2 - cameoPositions[firstIdx].X;
+ for (int i = firstIdx; i <= lastIdx; i++)
+ cameoPositions[i].X += centerOffset;
+ }
+
+ // For buildings, flip Y axis: first-entered goes to top, last-entered to bottom
+ if (whatAmI == AbstractType::Building)
+ {
+ int topY = INT_MAX;
+ for (int i = 0; i < count; i++)
+ topY = Math::min(topY, cameoPositions[i].Y);
+
+ for (int i = 0; i < count; i++)
+ cameoPositions[i].Y = basePos.Y - canvases[i].height - (cameoPositions[i].Y - topY);
+ }
+
+ const auto pSurface = DSurface::Composite;
+ RectangleStruct bounds = DSurface::Composite->GetRect();
+
+ // Determine BlitterFlags for translucency
+ const TranslucencyLevel translucency = pTypeExt->ShowCameo_Translucency.isset()
+ ? pTypeExt->ShowCameo_Translucency.Get()
+ : pRulesExt->ShowCameo_Translucency;
+ const BlitterFlags translucencyFlag = translucency.GetBlitterFlags();
+
+ // For buildings, draw in reverse so that visual top (earlier-entered) covers visual bottom.
+ // This is needed because Y flip reverses the visual order but overlap calculation
+ // still follows the original cameoCounts (bottom-to-top) order.
+ int idx = (whatAmI == AbstractType::Building) ? count - 1 : 0;
+ const int idxEnd = (whatAmI == AbstractType::Building) ? -1 : count;
+ const int idxStep = (whatAmI == AbstractType::Building) ? -1 : 1;
+ for (; idx != idxEnd; idx += idxStep)
+ {
+ const auto& item = cameoCounts[idx];
+ const auto pCameoType = item.first;
+ const int cameoCount = item.second;
+ const int curW = canvases[idx].width;
+ const int curH = canvases[idx].height;
+
+ Point2D iconPos = cameoPositions[idx];
+
+ // Skip icons whose any part exceeds the screen top boundary (Y < 0) to prevent crash
+ // Only the top boundary matters because cameos are always above the selection bracket
+ if (iconPos.Y < 0)
+ continue;
+
+ bool drawn = false;
+
+ const auto pCameoTypeExt = TechnoTypeExt::ExtMap.Find(pCameoType);
+
+ // Use custom ShowCameo SHP if specified
+ if (SHPStruct* pCustomCameo = pCameoTypeExt->ShowCameo_CustomShape)
+ {
+ const auto pCameoConvert = pCameoTypeExt->CameoPalette.GetOrDefaultConvert(FileSystem::CAMEO_PAL);
+ const auto pConvert = pCameoTypeExt->ShowCameo_CustomPalette.GetOrDefaultConvert(pCameoConvert);
+ const int frameCount = pCustomCameo->Frames > 0 ? pCustomCameo->Frames : 1;
+ const int frame = (Unsorted::CurrentFrame % frameCount);
+ pSurface->DrawSHP(
+ pConvert,
+ pCustomCameo,
+ frame,
+ &iconPos,
+ &bounds,
+ BlitterFlags::bf_400 | BlitterFlags::Alpha | translucencyFlag,
+ 0, 0,
+ ZGradient::Ground,
+ 1000, 0, nullptr, 0, 0, 0
+ );
+ drawn = true;
+ }
+
+ if (!drawn)
+ {
+ if (const auto pCustomPCX = pCameoTypeExt->ShowCameo_CustomPCX.GetSurface())
+ {
+ RectangleStruct pcxBounds = { iconPos.X, iconPos.Y, curW, curH };
+ PCX::Instance.BlitToSurface(&pcxBounds, pSurface, pCustomPCX);
+ drawn = true;
+ }
+ }
+
+ if (!drawn)
+ {
+ if (const auto pCameoPCX = pCameoTypeExt->CameoPCX.GetSurface())
+ {
+ RectangleStruct pcxBounds = { iconPos.X, iconPos.Y, curW, curH };
+ PCX::Instance.BlitToSurface(&pcxBounds, pSurface, pCameoPCX);
+ drawn = true;
+ }
+ }
+
+ if (!drawn)
+ {
+ SHPStruct* pCameo = pCameoType->GetCameo();
+ if (pCameo)
+ {
+ const auto pConvert = pCameoTypeExt->CameoPalette.GetOrDefaultConvert(FileSystem::CAMEO_PAL);
+ pSurface->DrawSHP(
+ pConvert,
+ pCameo,
+ 0,
+ &iconPos,
+ &bounds,
+ BlitterFlags::bf_400 | BlitterFlags::Alpha | translucencyFlag,
+ 0, 0,
+ ZGradient::Ground,
+ 1000, 0, nullptr, 0, 0, 0
+ );
+ drawn = true;
+ }
+ }
+
+ if (drawn)
+ {
+ wchar_t countText[16];
+ _snwprintf_s(countText, _countof(countText), _TRUNCATE, L"%d", cameoCount);
+
+ Point2D textPos = { iconPos.X + curW - 4, iconPos.Y + 2 };
+
+ pSurface->DrawTextA(
+ countText,
+ &bounds,
+ &textPos,
+ Drawing::RGB_To_Int({ 0, 255, 0 }),
+ 0,
+ TextPrintType::Center | TextPrintType::FullShadow
+ );
+ }
+ }
+}
diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h
index bb31ec98a5..9651713f25 100644
--- a/src/Ext/Techno/Body.h
+++ b/src/Ext/Techno/Body.h
@@ -321,6 +321,8 @@ class TechnoExt
static bool EjectSurvivor(FootClass* pSurvivor, CoordStruct coords, bool select);
static bool __fastcall ApplyKillDriver(TechnoClass** pData, void*, HouseClass* pToHouse, TechnoClass* pKiller, bool resetVeterancy);
+ static void DrawCameos(TechnoClass* pThis);
+
// WeaponHelpers.cpp
static int PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, AbstractClass* pTarget, int weaponIndexOne, int weaponIndexTwo, bool allowFallback = true, bool allowAAFallback = true);
static void FireWeaponAtSelf(TechnoClass* pThis, WeaponTypeClass* pWeaponType);
diff --git a/src/Ext/Techno/Hooks.Pips.cpp b/src/Ext/Techno/Hooks.Pips.cpp
index 075299f870..7d8b842aa4 100644
--- a/src/Ext/Techno/Hooks.Pips.cpp
+++ b/src/Ext/Techno/Hooks.Pips.cpp
@@ -94,6 +94,8 @@ DEFINE_HOOK(0x6F65D1, TechnoClass_DrawHealthBar_Buildings, 0x6)
TechnoExt::ProcessDigitalDisplays(pThis);
+ TechnoExt::DrawCameos(pThis);
+
return 0;
}
@@ -124,6 +126,8 @@ DEFINE_HOOK(0x6F683C, TechnoClass_DrawHealthBar_Units, 0x7)
TechnoExt::ProcessDigitalDisplays(pThis);
+ TechnoExt::DrawCameos(pThis);
+
if (pExt->TypeExtData->HealthBar_HidePips)
{
R->EDI(pLocation);
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index ecdfdd15c7..3ebe2eb2b6 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -1343,7 +1343,11 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->ShadowIndex_Frame.Read(exArtINI, pArtSection, "ShadowIndex.Frame");
+ this->CameoPCX.Read(pArtINI, pArtSection, "CameoPCX");
this->AltCameoPCX.Read(pArtINI, pArtSection, "AltCameoPCX");
+ this->ShowCameo_CustomPalette.LoadFromINI(pArtINI, pArtSection, "ShowCameo.CustomPalette");
+ this->ShowCameo_CustomShape.Read(exArtINI, pArtSection, "ShowCameo.CustomShape");
+ this->ShowCameo_CustomPCX.Read(pArtINI, pArtSection, "ShowCameo.CustomPCX");
this->LaserTrailData.clear();
for (size_t i = 0; ; ++i)
@@ -1463,6 +1467,15 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
// VoiceIFVRepair from Ares 0.2
this->VoiceIFVRepair.Read(exINI, pSection, "VoiceIFVRepair");
this->ParseVoiceWeaponAttacks(exINI, pSection, this->VoiceWeaponAttacks, this->VoiceEliteWeaponAttacks);
+
+ this->ShowCameo.Read(exINI, pSection, "ShowCameo");
+ this->ShowCameo_PerRow.Read(exINI, pSection, "ShowCameo.PerRow");
+ this->ShowCameo_BottomOffset.Read(exINI, pSection, "ShowCameo.BottomOffset");
+ this->ShowCameo_Translucency.Read(exINI, pSection, "ShowCameo.Translucency");
+ this->ShowCameo_OverlapXY.Read(exINI, pSection, "ShowCameo.OverlapXY");
+ this->ShowCameo_OffsetXY.Read(exINI, pSection, "ShowCameo.OffsetXY");
+ this->ShowCameo_OverlapPrimary.Read(exINI, pSection, "ShowCameo.OverlapPrimary");
+ this->CameoPalette.LoadFromINI(pArtINI, pArtSection, "CameoPalette");
}
void TechnoTypeExt::ExtData::LoadFromINIByWhatAmI(INI_EX& exINI, const char* pSection, INI_EX& exArtINI, const char* pArtSection)
@@ -1532,7 +1545,11 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm)
.Process(this->ImmuneToCrit)
.Process(this->MultiMindControl_ReleaseVictim)
.Process(this->CameoPriority)
+ .Process(this->CameoPCX)
.Process(this->AltCameoPCX)
+ .Process(this->ShowCameo_CustomPalette)
+ .Process(this->ShowCameo_CustomShape)
+ .Process(this->ShowCameo_CustomPCX)
.Process(this->NoManualMove)
.Process(this->InitialStrength)
.Process(this->ReloadInTransport)
@@ -1973,6 +1990,18 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm)
.Process(this->Missile_Cruise)
.Process(this->Missile_TakeOffAnim)
.Process(this->Missile_TakeOffSeparation)
+
+ .Process(this->ShowCameo)
+ .Process(this->ShowCameo_PerRow)
+ .Process(this->ShowCameo_BottomOffset)
+ .Process(this->ShowCameo_Translucency)
+ .Process(this->ShowCameo_OverlapXY)
+ .Process(this->ShowCameo_OffsetXY)
+ .Process(this->ShowCameo_OverlapPrimary)
+ .Process(this->CameoPalette)
+ .Process(this->ShowCameo_CustomPalette)
+ .Process(this->ShowCameo_CustomShape)
+ .Process(this->ShowCameo_CustomPCX)
;
}
void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index 66b264dc26..c0616dec48 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -66,6 +66,7 @@ class TechnoTypeExt
Valueable ImmuneToCrit;
Valueable MultiMindControl_ReleaseVictim;
Valueable CameoPriority;
+ PhobosPCXFile CameoPCX;
PhobosPCXFile AltCameoPCX;
Valueable NoManualMove;
Nullable InitialStrength;
@@ -523,6 +524,18 @@ class TechnoTypeExt
Valueable Missile_TakeOffAnim;
Valueable Missile_TakeOffSeparation;
+ Nullable ShowCameo;
+ Nullable ShowCameo_PerRow;
+ Nullable ShowCameo_BottomOffset;
+ Nullable ShowCameo_Translucency;
+ Nullable ShowCameo_OverlapXY;
+ Nullable ShowCameo_OffsetXY;
+ Nullable ShowCameo_OverlapPrimary;
+ CustomPalette CameoPalette;
+ CustomPalette ShowCameo_CustomPalette;
+ TheaterSpecificSHP ShowCameo_CustomShape;
+ PhobosPCXFile ShowCameo_CustomPCX;
+
ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject)
, HealthBar_Hide { false }
, HealthBar_HidePips { false }
@@ -564,6 +577,7 @@ class TechnoTypeExt
, ImmuneToCrit { false }
, MultiMindControl_ReleaseVictim { false }
, CameoPriority { 0 }
+ , CameoPCX {}
, AltCameoPCX {}
, NoManualMove { false }
, InitialStrength {}
@@ -998,6 +1012,18 @@ class TechnoTypeExt
, Missile_Cruise { false }
, Missile_TakeOffAnim { nullptr }
, Missile_TakeOffSeparation { 24 }
+
+ , ShowCameo {}
+ , ShowCameo_PerRow {}
+ , ShowCameo_BottomOffset {}
+ , ShowCameo_Translucency {}
+ , ShowCameo_OverlapXY {}
+ , ShowCameo_OffsetXY {}
+ , ShowCameo_OverlapPrimary {}
+ , CameoPalette {}
+ , ShowCameo_CustomPalette {}
+ , ShowCameo_CustomShape {}
+ , ShowCameo_CustomPCX {}
{ }
virtual ~ExtData() = default;
diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp
index ac9f7649b2..2fbea55fb6 100644
--- a/src/Phobos.INI.cpp
+++ b/src/Phobos.INI.cpp
@@ -80,6 +80,7 @@ bool Phobos::Config::HideShakeEffects = true;
bool Phobos::Config::ShowFlashOnSelecting = false;
bool Phobos::Config::UnitPowerDrain = false;
int Phobos::Config::SuperWeaponSidebar_RequiredSignificance = 0;
+bool Phobos::Config::ShowCameo_Enable = true;
bool Phobos::Misc::CustomGS = false;
int Phobos::Misc::CustomGS_ChangeInterval[7] = { -1, -1, -1, -1, -1, -1, -1 };
@@ -115,6 +116,7 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
Phobos::Config::HideShakeEffects = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "HideShakeEffects", false);
Phobos::Config::ShowFlashOnSelecting = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowFlashOnSelecting", false);
Phobos::Config::SuperWeaponSidebar_RequiredSignificance = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "SuperWeaponSidebar.RequiredSignificance", 0);
+ Phobos::Config::ShowCameo_Enable = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowCameo.Enable", true);
// 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);
diff --git a/src/Phobos.h b/src/Phobos.h
index 6bdb4073ec..fc76be3878 100644
--- a/src/Phobos.h
+++ b/src/Phobos.h
@@ -116,6 +116,7 @@ class Phobos
static bool ShowFlashOnSelecting;
static bool UnitPowerDrain;
static int SuperWeaponSidebar_RequiredSignificance;
+ static bool ShowCameo_Enable;
};
class Misc
diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h
index 2e9d9645f4..922b95c87f 100644
--- a/src/Utilities/Enum.h
+++ b/src/Utilities/Enum.h
@@ -275,6 +275,7 @@ enum class VerticalPosition : BYTE
Center = 1,
Bottom = 2
};
+
//hexagon
enum class BuildingSelectBracketPosition :BYTE
{