diff --git a/CREDITS.md b/CREDITS.md
index f977b2a3ed..eea6dd2815 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -398,6 +398,8 @@ This page lists all the individual contributions to the project by their author.
- **Joshy** - Original FlyingStrings implementation
- **CnCVK** - Original custom locomotors experiment
- **ZΞPHYɌUS** - win/lose themes code
+- **Aephiex** - Autoload hotkey command
+- **psi-cmd** - Autoload hotkey command
- **Neargye (Daniil Goncharov)** - [nameof library](https://github.com/Neargye/nameof) (MIT)
- **ayylmao** - help with docs, extensive and thorough testing
- **SMxReaver** - help with docs, extensive and thorough testing
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 79495c5f4c..7b40378eae 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -31,6 +31,7 @@
+
@@ -196,6 +197,7 @@
+
diff --git a/docs/User-Interface.md b/docs/User-Interface.md
index 2e1ad86969..619d0fd106 100644
--- a/docs/User-Interface.md
+++ b/docs/User-Interface.md
@@ -317,6 +317,19 @@ SelectionFlashDuration=0 ; integer, number of frames
- Switches on/off [frame by frame mode](Miscellanous.html#frame-step-in).
- For localization add `TXT_FRAME_BY_FRAME` and `TXT_FRAME_BY_FRAME_DESC` into your `.csf` file.
+### `[ ]` Auto Load
+- A shortcut to quickly command multiple units to board multiple transports at a same time. Select units and valid transports, then press the auto load hotkey, the units will be distributed among the transports and will be ordered to board them.
+ - Transports can be considered passengers if fully loaded, or `NoManualEnter=yes`, or it can't actually load anything from the selected passengers.
+ - Larger passengers are loaded first, and transports with smaller size limits are used first.
+ - At a given unit size and size limit, passengers will be diversely distributed into transports if possible.
+ - Ares `Passengers.Allowed=` and `Passengers.Disallowed=` are taken into account.
+- It also supports Bio Reactors, Tank Bunkers, and garrisonable structures. Select valid candidates and multiple said buildings while pressing Shift, then press the auto load hotkey, the units will be distributed among these buildings and will be ordered to enter them.
+ - If auto board transport can happen among selected units, then said logic takes precedence.
+ - A building is viewed as a Bio Reactor if it has `Passengers` >= 1, and it has `InfantryAbsorb=yes`.
+ - A building is viewed as a Tank Bunker if it has `Bunker=yes`.
+ - Neutral buildings can't be selected with own troops at a same time. Select either multiple Battle Bunkers or already partly garrisoned civilian structures to make use of the auto load hotkey.
+- For localization add `TXT_AUTO_LOAD` and `TXT_AUTO_LOAD_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 0b6f8c19c6..49a5abb4e3 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -470,6 +470,7 @@ New:
- `` can now be used as owner for pre-placed objects on skirmish and multiplayer maps (by Starkku)
- Allow customizing charge turret delays per burst on a weapon (by Starkku)
- Unit `Speed` setting now accepts floating point values (by Starkku)
+- Autoload hotkey command (by psi-cmd, Aephiex)
Vanilla fixes:
- Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy)
diff --git a/src/Commands/AutoLoad.cpp b/src/Commands/AutoLoad.cpp
new file mode 100644
index 0000000000..f4aff704ba
--- /dev/null
+++ b/src/Commands/AutoLoad.cpp
@@ -0,0 +1,493 @@
+// For selected units, pair the loadable vehicle and infantry.
+// 1. If transport can load vehicle, it won't load if the vehicle can be paired with infantry.
+// 2. It will always try to load the unit type with least amount of units, eg. 2 GI and 3 GGI, it will match 2 GI to transport first.
+// 3. For transport with multiple seats, it will try to fill the seats diversely.
+
+#include "AutoLoad.h"
+#include "Utilities/GeneralUtils.h"
+#include "Ext/Techno/Body.h"
+#include
+
+const char* AutoLoadCommandClass::GetName() const
+{
+ return "AutoLoad";
+}
+
+const wchar_t* AutoLoadCommandClass::GetUIName() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_AUTO_LOAD", L"Auto Load");
+}
+
+const wchar_t* AutoLoadCommandClass::GetUICategory() const
+{
+ return CATEGORY_SELECTION;
+}
+
+const wchar_t* AutoLoadCommandClass::GetUIDescription() const
+{
+ return GeneralUtils::LoadStringUnlessMissing("TXT_AUTO_LOAD_DESC", L"Auto Load");
+}
+
+void DebugPrintTransport(std::vector>& transports)
+{
+ Debug::Log("AutoLoadCommandClass::DebugPrintTransport: Transport count: %d\n", transports.size());
+ // print address of each transport and its passengers
+ for (auto transport : transports)
+ Debug::Log("AutoLoadCommandClass::DebugPrintTransport: Transport address: %p, Now size: %d, Virtual size: %d\n", transport.first, transport.first->Passengers.GetTotalSize(), transport.second);
+}
+
+void DebugPrintPassenger(std::vector& passengers)
+{
+ Debug::Log("AutoLoadCommandClass::DebugPrintPassenger: Passenger count: %d\n", passengers.size());
+ // print address of each passenger
+ for (auto passenger : passengers)
+ Debug::Log("AutoLoadCommandClass::DebugPrintPassenger: Passenger address: %p\n", passenger);
+}
+
+// tells if this techno is mind controlling something.
+inline static const bool IsMindControlling(TechnoClass* pTechno)
+{
+ return pTechno->CaptureManager && pTechno->CaptureManager->IsControllingSomething();
+}
+
+// tells if this techno is parasited (terror drone, giant squid, etc)
+inline static const bool IsParasited(TechnoClass* pTechno)
+{
+ return static_cast(pTechno)->ParasiteEatingMe;
+}
+
+// Gets the passenger budgets of a building.
+// If return value <= 0 then the building can't be a "transport".
+inline static const int GetBuildingPassengerBudget(BuildingClass* pBuilding)
+{
+ auto pBuildingType = pBuilding->Type;
+ // Bio Reactor
+ if (pBuildingType->Passengers > 0 && pBuildingType->InfantryAbsorb)
+ {
+ return pBuildingType->Passengers - pBuilding->Passengers.NumPassengers;
+ }
+ // garrisonable structure
+ else if (pBuildingType->CanBeOccupied)
+ {
+ return pBuildingType->MaxNumberOccupants - pBuilding->Occupants.Count;
+ }
+ // Tank Bunker
+ else if (pBuildingType->Bunker)
+ {
+ return pBuilding->BunkerLinkedItem ? 0 : 1;
+ }
+ return 0;
+}
+
+// Gets if a unit can potentially be a building's passenger.
+inline static const bool CanBeBuildingPassenger(TechnoClass* pPassenger)
+{
+ if (pPassenger->WhatAmI() == AbstractType::Infantry)
+ {
+ // Bio Reactor & garrisonable structure
+ return !IsMindControlling(pPassenger);
+ }
+ else if (pPassenger->WhatAmI() == AbstractType::Unit)
+ {
+ // Tank Bunker
+ // This is the YR function to check if something can be bunkered.
+ return reinterpret_cast(0x70FB50)(pPassenger);
+ }
+ return false;
+}
+
+// Gets the passenger budgets of a transport.
+// If return value <= 0 then it can't be a transport.
+inline static const int GetVehiclePassengerBudget(TechnoClass* pTransport)
+{
+ if (pTransport->WhatAmI() == AbstractType::Unit)
+ {
+ auto pTechnoType = pTransport->GetTechnoType();
+ auto pTypeExt = TechnoTypeExt::ExtMap.Find(pTechnoType);
+ if (pTechnoType->Passengers > 0 && !pTypeExt->NoManualEnter)
+ {
+ if (pTypeExt->Passengers_BySize)
+ return pTechnoType->Passengers - pTransport->Passengers.GetTotalSize();
+ else
+ return pTechnoType->Passengers - pTransport->Passengers.NumPassengers;
+ }
+ }
+ return 0;
+}
+
+// Gets if a unit can potentially be a vehicle's passenger.
+inline static const bool CanBeVehiclePassenger(TechnoClass* pPassenger)
+{
+ if (pPassenger->WhatAmI() == AbstractType::Infantry
+ || pPassenger->WhatAmI() == AbstractType::Unit)
+ {
+ return !IsMindControlling(pPassenger)
+ && !pPassenger->IsMindControlled()
+ && !IsParasited(pPassenger);
+ }
+ return false;
+}
+
+// Gets if the transport can load a passenger.
+inline static const bool CanHoldPassenger(TechnoClass* pTransport, TechnoClass* pPassenger)
+{
+ if (pTransport->WhatAmI() == AbstractType::Unit)
+ {
+ auto pTechnoType = pTransport->GetTechnoType();
+ auto pTypeExt = TechnoTypeExt::ExtMap.Find(pTechnoType);
+ // the check of MCing or MCed are redundant here, see inline function "CanBeVehiclePassenger".
+ return pTypeExt->CanLoadPassenger(pTransport, pPassenger);
+ }
+ else if (auto pBuilding = abstract_cast(pTransport))
+ {
+ auto pBuildingType = pBuilding->Type;
+ auto pTypeExt = TechnoTypeExt::ExtMap.Find(pBuildingType);
+ if (pBuildingType->Passengers > 0 && pBuildingType->InfantryAbsorb)
+ {
+ // Bio Reactor
+ // the check of MCing is redundant here, see inline function "CanBeBuildingPassenger".
+ return pPassenger->WhatAmI() == AbstractType::Infantry
+ && pTypeExt->CanLoadPassenger(pTransport, pPassenger);
+ }
+ else if (pBuildingType->CanBeOccupied)
+ {
+ // garrisonable structure
+ // the check of MCing is redundant here, see inline function "CanBeBuildingPassenger".
+ return pPassenger->WhatAmI() == AbstractType::Infantry
+ && !pPassenger->IsMindControlled()
+ && static_cast(pPassenger->GetTechnoType())->Occupier
+ && pTypeExt->CanBeOccupiedBy(pPassenger);
+ }
+ else if (pBuildingType->Bunker)
+ {
+ // Tank Bunker
+ // the check of "Turret=yes" and "Bunkerable=yes" are redundant here, see inline function "CanBeBuildingPassenger".
+ return pPassenger->WhatAmI() == AbstractType::Unit;
+ }
+ }
+ return false;
+}
+
+// Checks if this transport substrats passenger budget by passenger size.
+inline static const bool IsBySize(TechnoClass* pTransport)
+{
+ if (auto pUnit = abstract_cast(pTransport))
+ {
+ auto pType = pUnit->Type;
+ auto pTypeExt = TechnoTypeExt::ExtMap.Find(pType);
+ return pTypeExt->Passengers_BySize;
+ }
+ else if (pTransport->WhatAmI() == AbstractType::Building)
+ {
+ return false;
+ }
+ return true;
+}
+
+// Oddly, a Tank Bunker can't load anything when selected by the player.
+// Therefore it has to be deselected.
+inline static bool DeselectMe(TechnoClass* pTransport)
+{
+ if (auto pBuilding = abstract_cast(pTransport))
+ {
+ auto pBuildingType = pBuilding->Type;
+ if (pBuildingType->Bunker)
+ {
+ pTransport->Deselect();
+ return true;
+ }
+ }
+ return false;
+}
+
+template
+std::set SpreadPassengersToTransports(std::vector& passengers, std::vector>& transports)
+{
+ std::set foundTransportVector;
+ // 1. Get the least kind of passengers
+ // 2. Send the passengers to the transport in round robin, if the transport is full, remove it from the vector transports;
+ // if the pID vector is empty, remove it from the map, if not, move it to passengerMapIdle. We try to fill the transport evenly for each kind of passengers.
+ // 3. Repeat until all passengers are sent or the vector transports is empty.
+ std::unordered_map> passengerMap;
+ std::unordered_map> passengerMapIdle;
+
+ for (auto pPassenger : passengers)
+ {
+ auto pID = pPassenger->get_ID();
+ if (passengerMap.find(pID) == passengerMap.end())
+ passengerMap[pID] = std::vector();
+
+ passengerMap[pID].push_back(pPassenger);
+ }
+
+ while (true)
+ {
+ while (passengerMap.size() > 0 && transports.size() > 0)
+ {
+ const char* leastpID = nullptr;
+ unsigned int leastSize = std::numeric_limits::max();
+ for (auto const& [pID, pPassenger] : passengerMap)
+ {
+ if (pPassenger.size() < leastSize)
+ {
+ leastSize = pPassenger.size();
+ leastpID = pID;
+ }
+ }
+
+ {
+ unsigned int index = 0;
+ for (; index < leastSize && index < transports.size(); index++)
+ {
+ auto pPassenger = passengerMap[leastpID][index];
+ auto pTransport = transports[index].first;
+
+ // If this transport can't hold this passenger then skip.
+ if (!CanHoldPassenger(pTransport, pPassenger))
+ goto seeNextTransport; // "continue" causes to skip the passenger, not to skip the transport, so we shouldn't use "continue" here
+
+ // Gets the passenger slot budget that would be substracted.
+ int passengerSize;
+ if (IsBySize(pTransport))
+ {
+ // If by size then get the actual size of the potential passenger.
+ // Otherwise, every potential passenger counts as size 1.
+ passengerSize = static_cast(reinterpret_cast(pPassenger)->GetTechnoType()->Size);
+
+ // Check if the transport still has the budget for the new passenger.
+ if (transports[index].second < passengerSize)
+ goto seeNextTransport;
+ }
+ else
+ {
+ passengerSize = 1;
+ // Note that, the transport is momentarily removed from "tTransports" as soon as it's full,
+ // so it is redundant to check a transport's budget if it doesn't count by size.
+ }
+
+ if (pPassenger->GetCurrentMission() != Mission::Enter)
+ {
+ bool deselected = DeselectMe(pTransport);
+ bool moveFeedbackOld = std::exchange(Unsorted::MoveFeedback(), false);
+ pPassenger->ObjectClickedAction(Action::Enter, pTransport, true);
+ if (deselected)
+ pTransport->Select();
+ Unsorted::MoveFeedback = moveFeedbackOld;
+ transports[index].second -= passengerSize; // take away that much passenger slot budgets from the transport
+ foundTransportVector.insert(pPassenger);
+ }
+
+ seeNextTransport:;
+ }
+ passengerMap[leastpID].erase(passengerMap[leastpID].begin(), passengerMap[leastpID].begin() + index);
+ }
+
+ // Remove fully loaded transports from potential transports array.
+ transports.erase(
+ std::remove_if(transports.begin(), transports.end(),
+ [](auto transport)
+ {
+ return transport.second == 0;
+ }),
+ transports.end());
+
+ if (passengerMap[leastpID].size() == 0)
+ passengerMap.erase(leastpID);
+ else
+ passengerMapIdle[leastpID] = passengerMap[leastpID];
+ }
+
+ if (passengerMapIdle.size() != 0 && transports.size() != 0)
+ std::swap(passengerMap, passengerMapIdle);
+ else
+ break;
+ }
+ return foundTransportVector;
+}
+
+void AutoLoadCommandClass::Execute(WWKey eInput) const
+{
+ MapClass::Instance->SetTogglePowerMode(0);
+ MapClass::Instance->SetWaypointMode(0, false);
+ MapClass::Instance->SetRepairMode(0);
+ MapClass::Instance->SetSellMode(0);
+
+ std::map> passengerMap; // unit size -> a list of passengers of that size
+ std::set passengerSizes; // a sorted set of known passenger sizes
+
+ std::map>> transportMap; // size limit -> a list of transports of that size limit
+ std::set transportSizeLimits; // a sorted set of known size limits
+
+ std::map> ambiguousMap; // unit size -> a list of units in ambiguousity
+ std::set ambiguousSizes; // a sorted set of known ambiguous unit sizes
+
+ // This array is for Bio Reactors, garrisonable structures, and Tank Bunkers.
+ // A Bio Reactor is a building with "Passengers > 0" and "InfantryAbsorb=yes".
+ // A garrisonable structure is a building with "CanBeOccupied=yes".
+ // A Tank Bunker is a building with "Bunker=yes", and is not yet docked.
+ std::vector> enterableBuildingIndexArray;
+
+ // This array is for the candidates of enterable buildings.
+ std::vector enterableBuildingCandidateArray;
+
+ // Get current selected units.
+ // The first iteration, we find units that can't be transports, and add them to the passenger arrays.
+ // We also find Bio Reactors, Tank Bunkers, and candidates for them.
+ for (const auto& pUnit : ObjectClass::CurrentObjects())
+ {
+ // try to cast to TechnoClass
+ TechnoClass* pTechno = abstract_cast(pUnit);
+
+ // If not a techno, or is in air, or is bunkered, or is a loaded tank bunker, or is parasited, then exclude it from the first iteration.
+ // A unit on air can't be a passenger, a bunkered vehicle can't enter transports or further tank bunkers,
+ // and a loaded tank bunker can't load further vehicles.
+ if (!pTechno || pTechno->IsInAir() || pTechno->BunkerLinkedItem)
+ continue;
+
+ // Detect enterable buildings.
+ if (auto pBuilding = abstract_cast(pTechno))
+ {
+ int const budget = GetBuildingPassengerBudget(pBuilding);
+ if (budget > 0)
+ {
+ enterableBuildingIndexArray.push_back(std::make_pair(pTechno, budget));
+ }
+ continue;
+ }
+
+ // Detect candidates for enterable buildings
+ if (CanBeBuildingPassenger(pTechno))
+ {
+ enterableBuildingCandidateArray.push_back(pTechno);
+ }
+
+ // Detect candidates for enterable vehicles.
+ if (CanBeVehiclePassenger(pTechno) && GetVehiclePassengerBudget(pTechno) <= 0)
+ {
+ auto pTechnoType = pTechno->GetTechnoType();
+ int const size = static_cast(pTechnoType->Size);
+ passengerSizes.insert(size);
+ passengerMap[size].push_back(pTechno);
+ }
+ }
+
+ // Get current selected units.
+ // The second iteration, we find units that can be transports.
+ for (const auto& pUnit : ObjectClass::CurrentObjects())
+ {
+ // try to cast to TechnoClass
+ TechnoClass* pTechno = abstract_cast(pUnit);
+
+ // If not a techno, or is in air, or is bunkered, then exclude it from the second iteration.
+ // A unit on air can't be a transport, a bunkered vehicle can't load passengers.
+ if (!pTechno || pTechno->IsInAir() || pTechno->BunkerLinkedItem)
+ continue;
+
+ auto pTechnoType = pTechno->GetTechnoType();
+ auto pTypeExt = TechnoTypeExt::ExtMap.Find(pTechnoType);
+ if (pTechno->WhatAmI() == AbstractType::Unit)
+ {
+ int const budget = GetVehiclePassengerBudget(pTechno);
+ if (budget > 0)
+ {
+ if (pTypeExt->CanLoadAny(pTechno, passengerMap, passengerSizes))
+ {
+ int const sizeLimit = static_cast(pTechnoType->SizeLimit);
+ transportSizeLimits.insert(sizeLimit);
+ transportMap[sizeLimit].push_back(std::make_pair(pTechno, budget));
+ }
+ else
+ {
+ // If it can't actually load any clear passenger, then put it into ambiguousity.
+ int const size = static_cast(pTechnoType->Size);
+ ambiguousSizes.insert(size);
+ ambiguousMap[size].push_back(pTechno);
+ }
+ }
+ }
+ }
+
+ // Find a right position for units in ambiguousity.
+ // A unit in ambiguousity is a unit that can't load any of the clear passengers,
+ // but can potentially load other units in ambiguousity,
+ // so we have to deside here if they will be passengers or transports.
+ // We move units in ambiguousity from the smallest size to the largest size into passengers,
+ // and if an unit in ambiguousity can somehow load them this way, then it is a transport.
+ if (!ambiguousSizes.empty())
+ {
+ std::vector ambiguousPassengerVector;
+ for (auto ambiguousSize : ambiguousSizes)
+ {
+ if (ambiguousMap.contains(ambiguousSize))
+ {
+ auto const& ambiguousVector = ambiguousMap[ambiguousSize];
+ for (auto pAmbiguousTechno : ambiguousVector)
+ {
+ auto const pTechnoType = pAmbiguousTechno->GetTechnoType();
+ auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pTechnoType);
+ if (ambiguousPassengerVector.empty() || !pTypeExt->CanLoadAny(pAmbiguousTechno, ambiguousPassengerVector))
+ {
+ // This unit in ambiguousity is about to be added to passengers.
+ // Before that, check if it can be a vehicle's passenger.
+ // If not, this unit in ambiguousity is added to neither passengers nor transports.
+ if (!CanBeVehiclePassenger(pAmbiguousTechno))
+ continue;
+ ambiguousPassengerVector.push_back(pAmbiguousTechno);
+ int const size = static_cast(pTechnoType->Size);
+ passengerSizes.insert(size);
+ passengerMap[size].push_back(pAmbiguousTechno);
+ }
+ else
+ {
+ // This unit in ambiguousity is added to transports.
+ int const budget = GetVehiclePassengerBudget(pAmbiguousTechno);
+ int const sizeLimit = static_cast(pTechnoType->SizeLimit);
+ transportSizeLimits.insert(sizeLimit);
+ transportMap[sizeLimit].push_back(std::make_pair(pAmbiguousTechno, budget));
+ }
+ }
+ }
+ }
+ }
+
+ // Pair the passengers and the transports if possible.
+ if (!passengerSizes.empty() && !transportSizeLimits.empty())
+ {
+ // Reversed iteration, so we load larger passengers first.
+ for (auto passengerSizesItr = passengerSizes.rbegin();
+ passengerSizesItr != passengerSizes.rend();
+ ++passengerSizesItr)
+ {
+ auto passengerSize = *passengerSizesItr;
+ auto& passengerVector = passengerMap[passengerSize];
+
+ for (auto transportSizeLimit : transportSizeLimits)
+ {
+ // If the transports are too small for the passengers then skip them.
+ if (transportSizeLimit < passengerSize)
+ continue;
+
+ auto& transportVector = transportMap[transportSizeLimit];
+ if (transportVector.empty())
+ continue;
+
+ auto foundTransportVector = SpreadPassengersToTransports(passengerVector, transportVector);
+ passengerVector.erase(
+ std::remove_if(passengerVector.begin(), passengerVector.end(),
+ [foundTransportVector](auto pPassenger)
+ {
+ return foundTransportVector.contains(pPassenger);
+ }),
+ passengerVector.end());
+ if (passengerVector.empty())
+ break;
+ }
+ }
+ }
+ else
+ {
+ // If nothing can load then go find enterable buildings.
+ if (enterableBuildingIndexArray.size() > 0 && enterableBuildingCandidateArray.size() > 0)
+ SpreadPassengersToTransports(enterableBuildingCandidateArray, enterableBuildingIndexArray);
+ }
+}
diff --git a/src/Commands/AutoLoad.h b/src/Commands/AutoLoad.h
new file mode 100644
index 0000000000..9959240ea9
--- /dev/null
+++ b/src/Commands/AutoLoad.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "Commands.h"
+
+// Select next idle harvester
+class AutoLoadCommandClass : public CommandClass
+{
+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;
+};
diff --git a/src/Commands/Commands.cpp b/src/Commands/Commands.cpp
index 712a596467..e098fc1cb1 100644
--- a/src/Commands/Commands.cpp
+++ b/src/Commands/Commands.cpp
@@ -10,6 +10,7 @@
#include "ToggleDigitalDisplay.h"
#include "ToggleDesignatorRange.h"
#include "SaveVariablesToFile.h"
+#include "AutoLoad.h"
DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
{
@@ -19,6 +20,7 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6)
MakeCommand();
MakeCommand();
MakeCommand();
+ MakeCommand();
if (Phobos::Config::DevelopmentCommands)
{
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index 0091ceb22e..a443a15be6 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -32,6 +32,57 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor)
mtx->Translate(x, y, z);
}
+// Checks if a transport can load a passenger.
+// Note that this function only checks the size limit and Ares passenger whitelist and blacklist,
+// it doesn't check if this transport is actually a transport or not.
+bool TechnoTypeExt::ExtData::CanLoadPassenger(TechnoClass* pTransport, TechnoClass* pPassenger) const
+{
+ auto const pTransportType = pTransport->GetTechnoType();
+ auto const pPassengerType = pPassenger->GetTechnoType();
+ auto const sizeLimit = static_cast(pTransportType->SizeLimit);
+ auto const size = static_cast(pPassengerType->Size);
+ return (sizeLimit <= 0 || sizeLimit >= size)
+ && (!this->Passengers_BySize || (pTransport->Passengers.GetTotalSize() + size) <= pTransportType->Passengers)
+ && (this->PassengersWhitelist.empty() || this->PassengersWhitelist.Contains(pPassengerType))
+ && !this->PassengersBlacklist.Contains(pPassengerType);
+}
+
+// Checks if a transport can load any of the passengers inside a list.
+bool TechnoTypeExt::ExtData::CanLoadAny(TechnoClass* pTransport, std::vector pPassengerList) const
+{
+ for (auto pPassenger : pPassengerList)
+ if (this->CanLoadPassenger(pTransport, pPassenger))
+ return true;
+ return false;
+}
+
+// Checks if a transport can load any of the passengers inside a map.
+// It is assumed that the map's keys are the passengers' unit size and the values are the lists of passengers of said size.
+// It is assumed that every size in the map are present in the "passengerSizes" ordered set.
+bool TechnoTypeExt::ExtData::CanLoadAny(TechnoClass* pTransport, std::map> passengerMap, std::set passengerSizes) const
+{
+ auto const pTransportType = pTransport->GetTechnoType();
+ auto const sizeLimit = static_cast(pTransportType->SizeLimit);
+ for (auto const passengerSize : passengerSizes)
+ {
+ // there is no passenger small enough it can load, or the passenger list of this size is somehow empty
+ if ((sizeLimit > 0 && passengerSize > sizeLimit) || !passengerMap.contains(passengerSize) || passengerMap[passengerSize].empty())
+ continue;
+ // then check the function for list
+ if (this->CanLoadAny(pTransport, passengerMap[passengerSize]))
+ return true;
+ }
+ return false;
+}
+
+// Checks if a garrisonable structure can garrison a unit.
+// It is assumed that "whom" is an infantry.
+// This function only checks about the Ares occupier whitelist, it does not check if this is a garrisonable structure or not.
+bool TechnoTypeExt::ExtData::CanBeOccupiedBy(TechnoClass* whom) const
+{
+ return this->AllowedOccupiers.empty() || this->AllowedOccupiers.Contains(whom->GetTechnoType());
+}
+
// Ares 0.A source
const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const
{
@@ -391,6 +442,12 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->Passengers_SyncOwner.Read(exINI, pSection, "Passengers.SyncOwner");
this->Passengers_SyncOwner_RevertOnExit.Read(exINI, pSection, "Passengers.SyncOwner.RevertOnExit");
+ this->PassengersWhitelist.Read(exINI, pSection, "Passengers.Allowed");
+ this->PassengersBlacklist.Read(exINI, pSection, "Passengers.Disallowed");
+ this->AllowedOccupiers.Read(exINI, pSection, "CanBeOccupiedBy");
+ this->Passengers_BySize.Read(exINI, pSection, "Passengers.BySize");
+ this->NoManualEnter.Read(exINI, pSection, "NoManualEnter");
+
this->IronCurtain_KeptOnDeploy.Read(exINI, pSection, "IronCurtain.KeptOnDeploy");
this->IronCurtain_Effect.Read(exINI, pSection, "IronCurtain.Effect");
this->IronCurtain_KillWarhead.Read(exINI, pSection, "IronCurtain.KillWarhead");
@@ -744,6 +801,12 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm)
.Process(this->Passengers_SyncOwner)
.Process(this->Passengers_SyncOwner_RevertOnExit)
+ .Process(this->PassengersWhitelist)
+ .Process(this->PassengersBlacklist)
+ .Process(this->AllowedOccupiers)
+ .Process(this->Passengers_BySize)
+ .Process(this->NoManualEnter)
+
.Process(this->OnlyUseLandSequences)
.Process(this->PronePrimaryFireFLH)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index 3335acac05..9f7efcd243 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -158,6 +158,12 @@ class TechnoTypeExt
Valueable Passengers_SyncOwner;
Valueable Passengers_SyncOwner_RevertOnExit;
+ ValueableVector PassengersWhitelist;
+ ValueableVector PassengersBlacklist;
+ ValueableVector AllowedOccupiers;
+ Valueable Passengers_BySize;
+ Valueable NoManualEnter;
+
Nullable IronCurtain_KeptOnDeploy;
Nullable IronCurtain_Effect;
Nullable IronCurtain_KillWarhead;
@@ -372,6 +378,12 @@ class TechnoTypeExt
, Passengers_SyncOwner { false }
, Passengers_SyncOwner_RevertOnExit { true }
+ , PassengersWhitelist {}
+ , PassengersBlacklist {}
+ , AllowedOccupiers {}
+ , Passengers_BySize { true }
+ , NoManualEnter { false }
+
, OnlyUseLandSequences { false }
, PronePrimaryFireFLH {}
@@ -465,6 +477,11 @@ class TechnoTypeExt
void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0);
+ bool CanLoadPassenger(TechnoClass* pTransport, TechnoClass* pPassenger) const;
+ bool CanLoadAny(TechnoClass* pTransport, std::vector pPassengerList) const;
+ bool CanLoadAny(TechnoClass* pTransport, std::map> passengerMap, std::set passengerSizes) const;
+ bool CanBeOccupiedBy(TechnoClass* whom) const;
+
// Ares 0.A
const char* GetSelectionGroupID() const;