From f281bb89762da352f0e44e09c6508a04954f7c51 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sat, 4 Apr 2026 12:10:52 +0200 Subject: [PATCH 1/3] Add shipyard ai features --- .../GameClient/Drawable/Draw/W3DModelDraw.cpp | 1 + .../Include/GameLogic/AISkirmishPlayer.h | 2 + .../Code/GameEngine/Include/Common/Player.h | 3 + .../GameEngine/Include/GameLogic/AIPlayer.h | 1 + .../Include/GameLogic/AISkirmishPlayer.h | 4 + .../Include/GameLogic/ScriptActions.h | 1 + .../Include/GameLogic/ScriptConditions.h | 1 + .../GameEngine/Include/GameLogic/Scripts.h | 7 ++ .../Include/GameLogic/TerrainLogic.h | 8 ++ .../GameEngine/Source/Common/RTS/Player.cpp | 8 ++ .../Source/Common/System/BuildAssistant.cpp | 15 ++- .../GameEngine/Source/GameClient/InGameUI.cpp | 38 +------- .../Source/GameLogic/AI/AIPlayer.cpp | 21 ++++- .../Source/GameLogic/AI/AISkirmishPlayer.cpp | 77 +++++++++++++++- .../Source/GameLogic/Map/TerrainLogic.cpp | 91 +++++++++++++++++++ .../GameLogic/ScriptEngine/ScriptActions.cpp | 12 +++ .../ScriptEngine/ScriptConditions.cpp | 8 ++ .../GameLogic/ScriptEngine/ScriptEngine.cpp | 27 +++++- GeneralsMD/Code/Main/WinMain.cpp | 2 +- .../Tools/WorldBuilder/src/EditParameter.cpp | 8 ++ .../Tools/WorldBuilder/src/ScriptDialog.cpp | 4 +- 21 files changed, 291 insertions(+), 48 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index 0aab43dec02..b0d30cb6b48 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -1442,6 +1442,7 @@ void W3DModelDrawModuleData::parseConditionState(INI* ini, void *instance, void { "AltTurretArtAngle", INI::parseAngleReal, nullptr, offsetof(ModelConditionInfo, m_turrets[1].m_turretArtAngle) }, { "AltTurretPitch", parseBoneNameKey, nullptr, offsetof(ModelConditionInfo, m_turrets[1].m_turretPitchNameKey) }, { "AltTurretArtPitch", INI::parseAngleReal, nullptr, offsetof(ModelConditionInfo, m_turrets[1].m_turretArtPitch) }, + { "Turret1", parseBoneNameKey, nullptr, offsetof(ModelConditionInfo, m_turrets[0].m_turretAngleNameKey) }, { "Turret1ArtAngle", INI::parseAngleReal, nullptr, offsetof(ModelConditionInfo, m_turrets[0].m_turretArtAngle) }, { "Turret1Pitch", parseBoneNameKey, nullptr, offsetof(ModelConditionInfo, m_turrets[0].m_turretPitchNameKey) }, { "Turret1ArtPitch", INI::parseAngleReal, nullptr, offsetof(ModelConditionInfo, m_turrets[0].m_turretArtPitch) }, diff --git a/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h b/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h index 6c80a102ed4..757eca4b8c0 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h +++ b/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h @@ -34,6 +34,7 @@ class BuildListInfo; class SpecialPowerTemplate; +typedef BitFlags<10> UsedShipyardLocationMask; /** * The computer-controlled opponent. @@ -110,6 +111,7 @@ class AISkirmishPlayer : public AIPlayer Real m_curLeftFlankRightDefenseAngle; Real m_curRightFlankLeftDefenseAngle; Real m_curRightFlankRightDefenseAngle; + UsedShipyardLocationMask m_usedShipyards; UnsignedInt m_frameToCheckEnemy; Player *m_currentEnemy; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Player.h b/GeneralsMD/Code/GameEngine/Include/Common/Player.h index 9536e9f1407..ef47af80734 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Player.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Player.h @@ -615,6 +615,9 @@ class Player : public Snapshot /// Build structure type on front or flank of base. Gets passed to aiPlayer. void buildBaseDefenseStructure(const AsciiString &thingName, Bool flank); + /// Build shipyard. Gets passed to aiPlayer. + void buildShipyard(const AsciiString& thingName); + /// Recruits an instance of a specific team. Gets passed to aiPlayer. void recruitSpecificTeam(TeamPrototype *teamProto, Real recruitRadius); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPlayer.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPlayer.h index f7ded4d1742..ec7c545160a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPlayer.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPlayer.h @@ -175,6 +175,7 @@ class AIPlayer : public MemoryPoolObject, virtual void buildSpecificAIBuilding(const AsciiString &thingName); ///< Builds this building as soon as possible. + virtual void buildAIShipyard(const AsciiString& thingName); ///< Build structure at shipyard location virtual void recruitSpecificAITeam(TeamPrototype *teamProto, Real recruitRadius); ///< Builds this team immediately. diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h index 623e2ef2492..57ca3dab340 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h @@ -34,6 +34,7 @@ class BuildListInfo; class SpecialPowerTemplate; +typedef BitFlags<10> UsedShipyardsMask; /** * The computer-controlled opponent. @@ -66,6 +67,8 @@ class AISkirmishPlayer : public AIPlayer virtual void recruitSpecificAITeam(TeamPrototype *teamProto, Real recruitRadius); ///< Builds this team immediately. + virtual void buildAIShipyard(const AsciiString& thingName) override; ///< Build structure at shipyard location + virtual Bool isSkirmishAI(void) {return true;} virtual Bool checkBridges(Object *unit, Waypoint *way); @@ -110,6 +113,7 @@ class AISkirmishPlayer : public AIPlayer Real m_curLeftFlankRightDefenseAngle; Real m_curRightFlankLeftDefenseAngle; Real m_curRightFlankRightDefenseAngle; + UsedShipyardsMask m_usedShipyards; UnsignedInt m_frameToCheckEnemy; Player *m_currentEnemy; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptActions.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptActions.h index f94f20b351b..bf4d8933931 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptActions.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptActions.h @@ -138,6 +138,7 @@ class ScriptActions : public ScriptActionsInterface void doBuildUpgrade(const AsciiString& playerName, const AsciiString& upgrade); void doBuildBaseDefense(Bool flank); void doBuildBaseStructure(const AsciiString& buildingType, Bool flank); + void doBuildShipyard(const AsciiString& buildingType); void createUnitOnTeamAt(const AsciiString& unitName, const AsciiString& objType, const AsciiString& teamName, const AsciiString& waypoint); void doNamedAttackArea(const AsciiString& unitName, const AsciiString& areaName); void doNamedAttackTeam(const AsciiString& unitName, const AsciiString& teamName); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptConditions.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptConditions.h index 28d7e5bcf79..9efb81b0bf3 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptConditions.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptConditions.h @@ -178,6 +178,7 @@ class ScriptConditions : public ScriptConditionsInterface Bool evaluateSkirmishSupplySourceSafe(Condition *pCondition, Parameter *pSkirmishPlayerParm, Parameter *pMinAmountOfSupplies ); Bool evaluateSkirmishSupplySourceAttacked(Parameter *pSkirmishPlayerParm ); Bool evaluateSkirmishStartPosition(Parameter *pSkirmishPlayerParm, Parameter *startNdx ); + Bool evaluateSkirmishShipsEnabled(void); // Stubs diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h index ac05f72c48f..2298fe5a149 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h @@ -54,6 +54,7 @@ #define SKIRMISH_FLANK "Flank" #define SKIRMISH_BACKDOOR "Backdoor" #define SKIRMISH_SPECIAL "Special" +#define SKIRMISH_SHIPYARD "Shipyard" // Skirmish Player Areas #define SKIRMISH_AREA_HOME_BASE "Home Base" @@ -62,11 +63,14 @@ // Skirmish trigger names. #define MY_INNER_PERIMETER "[Skirmish]MyInnerPerimeter" #define MY_OUTER_PERIMETER "[Skirmish]MyOuterPerimeter" +#define MY_NAVAL_PERIMETER "[Skirmish]MyNavalPerimeter" #define ENEMY_OUTER_PERIMETER "[Skirmish]EnemyOuterPerimeter" #define ENEMY_INNER_PERIMETER "[Skirmish]EnemyInnerPerimeter" +#define ENEMY_NAVAL_PERIMETER "[Skirmish]EnemyNavalPerimeter" #define INNER_PERIMETER "InnerPerimeter" #define OUTER_PERIMETER "OuterPerimeter" +#define NAVAL_PERIMETER "NavalPerimeter" class Parameter; class Script; @@ -543,6 +547,7 @@ class ScriptAction : public MemoryPoolObject // This is the action class. TEAM_SET_BOOBYTRAPPED, ///< Add boobytrap to all units on team. SHOW_WEATHER, ///< show map defined weather. AI_PLAYER_BUILD_TYPE_NEAREST_TEAM, ///< Tell the ai player to build an object nearest team. + SKIRMISH_BUILD_SHIPYARD, ///< Tell the ai player to build a shipyard // add new items here, please NUM_ITEMS }; @@ -971,6 +976,8 @@ class Condition : public MemoryPoolObject // This is the conditional class. START_POSITION_IS, // True if our start position matches. NAMED_HAS_FREE_CONTAINER_SLOTS, ///< Kris -- Checks if any given container has any free slots. + SKIRMISH_SHIPS_ENABLED, // True if ships are enabled on the played map + NUM_ITEMS // Always the last condition. }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h index ab229a80407..94c95e1bfdb 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h @@ -266,6 +266,9 @@ class TerrainLogic : public Snapshot, /// Return the closest waypoint on the labeled path virtual Waypoint *getClosestWaypointOnPath( const Coord3D *pos, AsciiString label ); + /// Return a vector of shipyard locations for this Label, expected to be ShipyardN where N is player index starting with 1 + virtual std::vector getShipyardBuildPositions(const Coord3D* pos, AsciiString label); + /// Return true if the waypoint path containing pWay is labeled with the label. virtual Bool isPurposeOfPath( Waypoint *pWay, AsciiString label ); @@ -298,6 +301,9 @@ class TerrainLogic : public Snapshot, virtual void updateBridgeDamageStates(void); ///< Updates bridge's damage info. + /// Check if a point is in a NO_SHIPYARD area + virtual bool isInNoShipyardZone(const Coord3D * pos); + Bool anyBridgesDamageStatesChanged(void) {return m_bridgeDamageStatesChanged; } ///< Bridge damage states updated. Bool isBridgeRepaired(const Object *bridge); ///< Is bridge repaired? Bool isBridgeBroken(const Object *bridge); ///< Is bridge Broken? @@ -318,6 +324,8 @@ class TerrainLogic : public Snapshot, void flattenTerrain(Object *obj); ///< Flatten the terrain under a building. void createCraterInTerrain(Object *obj); ///< Flatten the terrain under a building. + Real getShipyardPlacementAngle(const Coord3D& worldPos, const ThingTemplate* thing); + protected: // snapshot methods diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp index 789a5a16344..c50c5557de2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp @@ -2495,6 +2495,14 @@ void Player::buildBaseDefenseStructure(const AsciiString &thingName, Bool flank) } } +//============================================================================= +void Player::buildShipyard(const AsciiString &thingName) { + if (m_ai) + { + m_ai->buildAIShipyard(thingName); + } +} + //============================================================================= void Player::buildSpecificBuilding(const AsciiString &thingName) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/BuildAssistant.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/BuildAssistant.cpp index f31e71a2c95..c678c891215 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/BuildAssistant.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/BuildAssistant.cpp @@ -384,7 +384,15 @@ Object *BuildAssistant::buildObjectNow( Object *constructorObject, const ThingTe Coord3D groundPos; groundPos.x = pos->x; groundPos.y = pos->y; - groundPos.z = TheTerrainLogic->getGroundHeight( groundPos.x, groundPos.y ); + if (!what->isKindOf(KINDOF_SHIPYARD)) { + groundPos.z = TheTerrainLogic->getGroundHeight(groundPos.x, groundPos.y); + } + else { + //If shipyard adjust to water height + Real waterZ{ 0.0 }, terrainZ{ 0.0 }; + TheTerrainLogic->isUnderwater(groundPos.x, groundPos.y, &waterZ, &terrainZ); + groundPos.z = std::max(waterZ, terrainZ); + } obj->setPosition( &groundPos ); obj->setOrientation( angle ); @@ -1152,6 +1160,11 @@ LegalBuildCode BuildAssistant::isLocationLegalToBuild( const Coord3D *worldPos, else { // IF Shipyard need some special code + // check if building center is in a NO_SHIPYARD map area + if (TheTerrainLogic->isInNoShipyardZone(worldPos)) { + return LBC_RESTRICTED_TERRAIN; + } + // // check the footprint of where the structure would go to be clear of any non-buildable // tiles and to make sure there isn't a restricted tile and to make sure it's "flat" enough diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 263f4b451b1..9a267dd088a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1673,7 +1673,6 @@ void InGameUI::handleBuildPlacements( void ) if (m_pendingPlaceType->isKindOf(KINDOF_SHIPYARD)) { // For shipyard sample the terrain an rotate according to descending terrain (water is lower) - const GeometryInfo& geom = m_pendingPlaceType->getTemplateGeometryInfo(); Coord3D worldPos; TheTacticalView->screenToTerrain(&loc, &worldPos); @@ -1683,42 +1682,7 @@ void InGameUI::handleBuildPlacements( void ) TheTerrainLogic->isUnderwater(worldPos.x, worldPos.y, &waterZ, &terrainZ); worldPos.z = std::max(terrainZ, waterZ); - Real check_radius = 0.0f; - if (geom.getGeomType() == GEOMETRY_BOX) - { - check_radius = std::max(geom.getMinorRadius(), geom.getMajorRadius()); - } // end if - else if (geom.getGeomType() == GEOMETRY_SPHERE || - geom.getGeomType() == GEOMETRY_CYLINDER) - { - check_radius = geom.getBoundingCircleRadius(); - } // end else if - else - { - DEBUG_ASSERTCRASH(0, ("InGameUI::handleBuildPlacements (Shipyard placement): Undefined geometry '%d' for '%s'", geom.getGeomType(), m_pendingPlaceType->getName().str())); - return; - } // end else - - //Check 4 sample points - Real hx1 = TheTerrainLogic->getGroundHeight(worldPos.x + check_radius, worldPos.y); - Real hx2 = TheTerrainLogic->getGroundHeight(worldPos.x - check_radius, worldPos.y); - Real hy1 = TheTerrainLogic->getGroundHeight(worldPos.x, worldPos.y + check_radius); - Real hy2 = TheTerrainLogic->getGroundHeight(worldPos.x, worldPos.y - check_radius); - - Real dx = hx1 - hx2; - Real dy = hy1 - hy2; - - Coord2D v; - v.x = dx; - v.y = dy; - constexpr Real pi2 = PI * 2.0f; - angle = v.toAngle() + m_pendingPlaceType->getPlacementViewAngle(); - if (angle < 0.0f) { - angle += pi2; - } - else if (angle > pi2) { - angle -= pi2; - } + angle = TheTerrainLogic->getShipyardPlacementAngle(worldPos, m_pendingPlaceType); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 1fa890c1ef5..e297ba90d71 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -521,7 +521,14 @@ Object *AIPlayer::buildStructureWithDozer(const ThingTemplate *bldgPlan, BuildLi } // construct the building Coord3D pos = *info->getLocation(); - pos.z += TheTerrainLogic->getGroundHeight(pos.x, pos.y); + if (bldgPlan->isKindOf(KINDOF_SHIPYARD)) { + Real waterZ, terrainZ; + TheTerrainLogic->isUnderwater(pos.x, pos.y, &waterZ, &terrainZ); + pos.z = std::max(waterZ, terrainZ); + } + else { + pos.z += TheTerrainLogic->getGroundHeight(pos.x, pos.y); + } if( !dozer->getAIUpdateInterface() ) { return nullptr; @@ -1756,6 +1763,18 @@ void AIPlayer::buildSpecificAIBuilding(const AsciiString &thingName) TheScriptEngine->AppendDebugMessage(teamStr, false); } +// ------------------------------------------------------------------------------------------------ +/** Build a structure at shipyard location */ +// ------------------------------------------------------------------------------------------------ +void AIPlayer::buildAIShipyard(const AsciiString &thingName) +{ + // + AsciiString teamStr = "Error : Solo ai doesn't support BuildShipyard. '"; + teamStr.concat(thingName); + teamStr.concat("' not built."); + TheScriptEngine->AppendDebugMessage(teamStr, false); +} + // ------------------------------------------------------------------------------------------------ /** Build an upgrade. */ // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp index 018acdb1e20..9d366e57299 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp @@ -59,13 +59,28 @@ #define USE_DOZER 1 - +template<> +const char* const UsedShipyardsMask::s_bitNameList[] = +{ + "SHIPYARD_1_USED", + "SHIPYARD_2_USED", + "SHIPYARD_3_USED", + "SHIPYARD_4_USED", + "SHIPYARD_5_USED", + "SHIPYARD_6_USED", + "SHIPYARD_7_USED", + "SHIPYARD_8_USED", + "SHIPYARD_9_USED", + "SHIPYARD_10_USED", + nullptr +}; +static_assert(ARRAY_SIZE(UsedShipyardsMask::s_bitNameList) == UsedShipyardsMask::NumBits + 1, "Incorrect array size"); /////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE DATA /////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////// -AISkirmishPlayer::AISkirmishPlayer( Player *p ) : AIPlayer(p), +AISkirmishPlayer::AISkirmishPlayer(Player* p) : AIPlayer(p), m_curFlankBaseDefense(0), m_curFrontBaseDefense(0), m_curFrontLeftDefenseAngle(0), @@ -74,6 +89,7 @@ m_curLeftFlankLeftDefenseAngle(0), m_curLeftFlankRightDefenseAngle(0), m_curRightFlankLeftDefenseAngle(0), m_curRightFlankRightDefenseAngle(0), +m_usedShipyards(0), m_frameToCheckEnemy(0), m_currentEnemy(nullptr) @@ -847,6 +863,61 @@ void AISkirmishPlayer::recruitSpecificAITeam(TeamPrototype *teamProto, Real recr } } +void AISkirmishPlayer::buildAIShipyard(const AsciiString& thingName) +{ + const ThingTemplate* tTemplate = TheThingFactory->findTemplate(thingName); + if (tTemplate == nullptr) { + DEBUG_CRASH(("Couldn't find shipyard structure '%s' for side %s", thingName.str(), m_player->getSide().str())); + return; + } + + if (!tTemplate->isKindOf(KINDOF_SHIPYARD)) { + DEBUG_CRASH(("Cannot instruct AI to build non-Shipyard '%s' at shipyard position for side %s", thingName.str(), m_player->getSide().str())); + } + + AsciiString wpLabel; + Int index{ 1 }; + wpLabel.format("%s%d_%d", SKIRMISH_SHIPYARD, m_player->getMpStartIndex() + 1, index); + const Waypoint* shipyardWp = TheTerrainLogic->getWaypointByName(wpLabel); + + while (shipyardWp != nullptr && index <= UsedShipyardsMask::NumBits) { + /* See if we can build there. */ + Coord3D buildPos = *shipyardWp->getLocation(); + + + //DEBUG_LOG(("SHIPYARD_BULDING: player %d flags: %u", m_player->getMpStartIndex() + 1, m_usedShipyards.toUnsignedInt())); + + // skip used index + if (!m_usedShipyards.test(index - 1)) { + + //Adjust to water height + Real terrainZ{ 0 }; + Real waterZ{ 0 }; + TheTerrainLogic->isUnderwater(buildPos.x, buildPos.y, &waterZ, &terrainZ); + buildPos.z = std::max(terrainZ, waterZ); + + Real placeAngle = TheTerrainLogic->getShipyardPlacementAngle(buildPos, tTemplate); + + LegalBuildCode code = TheBuildAssistant->isLocationLegalToBuild(&buildPos, tTemplate, placeAngle, + BuildAssistant::TERRAIN_RESTRICTIONS | BuildAssistant::NO_OBJECT_OVERLAP, nullptr, m_player); + + Bool canBuild = LBC_OK == code; + + DEBUG_LOG(("SHIPYARD_BULDING: Location %d Legal to build for player %d (%s): %s, code: %d", index, m_player->getMpStartIndex() + 1, shipyardWp->getName().str(), canBuild ? "true" : "false", static_cast(code))); + + TheTerrainVisual->removeAllBibs(); // isLocationLegalToBuild adds bib feedback, turn it off. jba. + if (canBuild) { + m_usedShipyards.set(index - 1, 1); + m_player->addToPriorityBuildList(thingName, &buildPos, placeAngle); + break; + } + } + index++; + wpLabel.format("%s%d_%d", SKIRMISH_SHIPYARD, m_player->getMpStartIndex() + 1, index); + shipyardWp = TheTerrainLogic->getWaypointByName(wpLabel); + } +} + @@ -1223,6 +1294,8 @@ void AISkirmishPlayer::xfer( Xfer *xfer ) // right flank right defense angle xfer->xferReal( &m_curRightFlankRightDefenseAngle ); + m_usedShipyards.xfer(xfer); + } // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index 1c07c160d46..730d592a14e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp @@ -1608,6 +1608,38 @@ Waypoint *TerrainLogic::getClosestWaypointOnPath( const Coord3D *pos, AsciiStrin return pClosestWay; } +//------------------------------------------------------------------------------------------------- +/** Return shipyard building locations for a player sorted by distance to pos */ +//------------------------------------------------------------------------------------------------- +std::vector TerrainLogic::getShipyardBuildPositions(const Coord3D* pos, AsciiString label) +{ + std::vector ret; + if (label.isEmpty()) { + DEBUG_LOG(("***Warning - asking for empty path label.")); + return ret; + } + + for (Waypoint* way = m_waypointListHead; way; way = way->getNext()) { + Bool match = false; + if (label.compareNoCase(way->getPathLabel1()) == 0) match = true; + if (label.compareNoCase(way->getPathLabel2()) == 0) match = true; + if (label.compareNoCase(way->getPathLabel3()) == 0) match = true; + if (match) { + ret.push_back(way); + } + } + + std::sort(ret.begin(), ret.end(), [=](const Waypoint* a, const Waypoint* b) { + const Coord3D* posA = a->getLocation(); + const Coord3D* posB = b->getLocation(); + Real distAsqr = (posA->x - pos->x) * (posA->x - pos->x) + (posA->y - pos->y) * (posA->y - pos->y); + Real distBsqr = (posB->x - pos->x) * (posB->x - pos->x) + (posB->y - pos->y) * (posB->y - pos->y); + return distAsqr < distBsqr; + }); + + return ret; +} + //------------------------------------------------------------------------------------------------- /** Return true if the waypoint path containing pWay is labeled with the label. */ //------------------------------------------------------------------------------------------------- @@ -1640,6 +1672,21 @@ PolygonTrigger *TerrainLogic::getTriggerAreaByName( AsciiString name ) return nullptr; } +//------------------------------------------------------------------------------------------------- +/** check if point is in NO_SHIPYARD polygon */ +//------------------------------------------------------------------------------------------------- +bool TerrainLogic::isInNoShipyardZone(const Coord3D* pos) { + ICoord3D iPos {std::round(pos->x), std::round(pos->y), std::round(pos->z)}; + for (PolygonTrigger* pTrig = PolygonTrigger::getFirstPolygonTrigger(); pTrig; pTrig = pTrig->getNext()) { + if (pTrig->getTriggerName().startsWithNoCase("NO_SHIPYARD")) { + if (pTrig->pointInTrigger(iPos)) + { + return true; + } + } + } + return false; +} //------------------------------------------------------------------------------------------------- /** Finds the bridge at a given x/y coordinate. */ @@ -2899,7 +2946,51 @@ void TerrainLogic::flattenTerrain(Object *obj) } +Real TerrainLogic::getShipyardPlacementAngle(const Coord3D & worldPos, const ThingTemplate* thing) { + Real angle{ 0.0f }; + if (thing == nullptr) return 0.0f; + + const GeometryInfo& geom = thing->getTemplateGeometryInfo(); + + Real check_radius = 0.0f; + if (geom.getGeomType() == GEOMETRY_BOX) + { + check_radius = std::max(geom.getMinorRadius(), geom.getMajorRadius()); + } // end if + else if (geom.getGeomType() == GEOMETRY_SPHERE || + geom.getGeomType() == GEOMETRY_CYLINDER) + { + check_radius = geom.getBoundingCircleRadius(); + } // end else if + else + { + DEBUG_ASSERTCRASH(0, ("InGameUI::handleBuildPlacements (Shipyard placement): Undefined geometry '%d' for '%s'", geom.getGeomType(), thing->getName().str())); + return 0.0f; + } // end else + + //Check 4 sample points + Real hx1 = TheTerrainLogic->getGroundHeight(worldPos.x + check_radius, worldPos.y); + Real hx2 = TheTerrainLogic->getGroundHeight(worldPos.x - check_radius, worldPos.y); + Real hy1 = TheTerrainLogic->getGroundHeight(worldPos.x, worldPos.y + check_radius); + Real hy2 = TheTerrainLogic->getGroundHeight(worldPos.x, worldPos.y - check_radius); + + Real dx = hx1 - hx2; + Real dy = hy1 - hy2; + + Coord2D v; + v.x = dx; + v.y = dy; + constexpr Real pi2 = PI * 2.0f; + angle = v.toAngle() + thing->getPlacementViewAngle(); + if (angle < 0.0f) { + angle += pi2; + } + else if (angle > pi2) { + angle -= pi2; + } + return angle; +} // ------------------------------------------------------------------------------------------------ /** Dig a deep circular gorge into the terrain beneath an object. */ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp index a40902c7676..39e26f0d3e2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp @@ -1166,6 +1166,15 @@ void ScriptActions::doBuildBaseStructure(const AsciiString& buildingType, Bool f } } +void ScriptActions::doBuildShipyard(const AsciiString& buildingType) +{ + // This action ALWAYS occur on the current player. + Player* thePlayer = TheScriptEngine->getCurrentPlayer(); + if (thePlayer) { + thePlayer->buildShipyard(buildingType); + } +} + //------------------------------------------------------------------------------------------------- /** createUnitOnTeamAt */ @@ -6610,6 +6619,9 @@ void ScriptActions::executeAction( ScriptAction *pAction ) case ScriptAction::SKIRMISH_BUILD_STRUCTURE_FLANK: doBuildBaseStructure(pAction->getParameter(0)->getString(), true); return; + case ScriptAction::SKIRMISH_BUILD_SHIPYARD: + doBuildShipyard(pAction->getParameter(0)->getString()); + return; case ScriptAction::RECRUIT_TEAM: doRecruitTeam(pAction->getParameter(0)->getString(), pAction->getParameter(1)->getReal()); return; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptConditions.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptConditions.cpp index ee4633fb9b7..b156b99d39f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptConditions.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptConditions.cpp @@ -59,6 +59,7 @@ #include "GameLogic/ScriptEngine.h" #include "GameLogic/Scripts.h" #include "GameLogic/VictoryConditions.h" +#include class ObjectTypesTemp @@ -2607,6 +2608,11 @@ Bool ScriptConditions::evaluateSkirmishStartPosition(Parameter *pSkirmishPlayerP return ndx == startNdx; } +Bool ScriptConditions::evaluateSkirmishShipsEnabled() +{ + return TheMapData->m_enableShips; +} + //------------------------------------------------------------------------------------------------- Bool ScriptConditions::evaluateSkirmishPlayerHasBeenAttackedByPlayer(Parameter *pSkirmishPlayerParm, Parameter *pAttackedByParm ) { @@ -2981,6 +2987,8 @@ Bool ScriptConditions::evaluateCondition( Condition *pCondition ) case Condition::PLAYER_LOST_OBJECT_TYPE: return evaluatePlayerLostObjectType(pCondition->getParameter(0), pCondition->getParameter(1)); + case Condition::SKIRMISH_SHIPS_ENABLED: + return evaluateSkirmishShipsEnabled(); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp index 1e294316578..f1f6eb8e392 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp @@ -4089,6 +4089,14 @@ void ScriptEngine::init( void ) curTemplate->m_uiStrings[0] = "Disable "; curTemplate->m_uiStrings[1] = "'s ambient sound."; + curTemplate = &m_actionTemplates[ScriptAction::SKIRMISH_BUILD_SHIPYARD]; + curTemplate->m_internalName = "SKIRMISH_BUILD_SHIPYARD"; + curTemplate->m_uiName = "Skirmish Only/Build/Build a Shipyard."; + curTemplate->m_numParameters = 1; + curTemplate->m_parameters[0] = Parameter::OBJECT_TYPE; + curTemplate->m_numUiStrings = 2; + curTemplate->m_uiStrings[0] = "Build a shipyard "; + curTemplate->m_uiStrings[1] = ", at next free shipyard waypoint."; /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -5228,6 +5236,13 @@ void ScriptEngine::init( void ) curTemplate->m_uiStrings[1] = " has lost an object of type "; curTemplate->m_uiStrings[2] = " (can be an object type list)."; + curTemplate = &m_conditionTemplates[Condition::SKIRMISH_SHIPS_ENABLED]; + curTemplate->m_internalName = "SKIRMISH_SHIPS_ENABLED"; + curTemplate->m_uiName = "Skirmish/ Ships are enabled."; + curTemplate->m_numParameters = 0; + curTemplate->m_numUiStrings = 1; + curTemplate->m_uiStrings[0] = "Ships are enabled on this map"; + curTemplate = &m_actionTemplates[ScriptAction::SHOW_WEATHER]; curTemplate->m_internalName = "SHOW_WEATHER"; curTemplate->m_uiName = "Map/Environment/Show Weather."; @@ -5905,18 +5920,20 @@ Handles skirmish name qualification. */ //------------------------------------------------------------------------------------------------- PolygonTrigger *ScriptEngine::getQualifiedTriggerAreaByName( AsciiString name ) { - if (name == MY_INNER_PERIMETER || name == MY_OUTER_PERIMETER) { + if (name == MY_INNER_PERIMETER || name == MY_OUTER_PERIMETER || name == MY_NAVAL_PERIMETER) { if (m_currentPlayer) { Int ndx = m_currentPlayer->getMpStartIndex()+1; - if (name==MY_INNER_PERIMETER) { + if (name == MY_INNER_PERIMETER) { name.format("%s%d", INNER_PERIMETER, ndx); + } else if (name == MY_NAVAL_PERIMETER) { + name.format("%s%d", NAVAL_PERIMETER, ndx); } else { name.format("%s%d", OUTER_PERIMETER, ndx); } } else { return nullptr; } - } else if (name == ENEMY_INNER_PERIMETER || name == ENEMY_OUTER_PERIMETER) { + } else if (name == ENEMY_INNER_PERIMETER || name == ENEMY_OUTER_PERIMETER || name == ENEMY_NAVAL_PERIMETER) { Int mpNdx; mpNdx = -1; @@ -5926,8 +5943,10 @@ PolygonTrigger *ScriptEngine::getQualifiedTriggerAreaByName( AsciiString name ) mpNdx = enemy->getMpStartIndex()+1; } } - if (name==ENEMY_INNER_PERIMETER) { + if (name == ENEMY_INNER_PERIMETER) { name.format("%s%d", INNER_PERIMETER, mpNdx); + } else if (name == ENEMY_NAVAL_PERIMETER) { + name.format("%s%d", NAVAL_PERIMETER, mpNdx); } else { name.format("%s%d", OUTER_PERIMETER, mpNdx); } diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index d558bb009ed..514cdedc069 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -820,7 +820,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, { Int exitcode = 1; #ifdef _DEBUG - //WaitForDebugger(); //in debug build, wait for debugger attachment + WaitForDebugger(); //in debug build, wait for debugger attachment #endif #ifdef RTS_PROFILE diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/EditParameter.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/EditParameter.cpp index d9189705eae..f152ffad57a 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/EditParameter.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/EditParameter.cpp @@ -1222,6 +1222,12 @@ Bool EditParameter::loadTriggerAreas(CComboBox *pCombo, AsciiString match) trigName = WATER_GRID; if (pCombo) pCombo->AddString(trigName.str()); if ((match==trigName)) didMatch = true; + trigName = MY_NAVAL_PERIMETER; + if (pCombo) pCombo->AddString(trigName.str()); + if ((match == trigName)) didMatch = true; + trigName = ENEMY_NAVAL_PERIMETER; + if (pCombo) pCombo->AddString(trigName.str()); + if ((match == trigName)) didMatch = true; return didMatch; } @@ -1904,6 +1910,8 @@ BOOL EditParameter::OnInitDialog() pList->InsertString(-1, "Backdoor"); pList->InsertString(-1, "Flank"); pList->InsertString(-1, "Special"); + pList->InsertString(-1, "Naval"); + pList->InsertString(-1, "NavalFlank"); i = pList->FindStringExact(-1, m_parameter->getString().str()); if (i!=CB_ERR) pList->SetCurSel(i); diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/ScriptDialog.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/ScriptDialog.cpp index e98375ed77f..00117dda069 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/ScriptDialog.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/ScriptDialog.cpp @@ -1577,8 +1577,8 @@ void ScriptDialog::OnLoad() Int j; for (j=0; jgetDict()->getAsciiString(TheKey_playerName); - if (name == m_readPlayerNames[j]) { + AsciiString name = m_sides.getSideInfo(j)->getDict()->getAsciiString(TheKey_playerName); + if (name == m_readPlayerNames[i]) { curSide = j; break; } From 22170126b94e0089b858c6a12164b4b7cc61ba64 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sat, 4 Apr 2026 13:13:28 +0200 Subject: [PATCH 2/3] revert generals file --- Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h b/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h index 757eca4b8c0..6c80a102ed4 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h +++ b/Generals/Code/GameEngine/Include/GameLogic/AISkirmishPlayer.h @@ -34,7 +34,6 @@ class BuildListInfo; class SpecialPowerTemplate; -typedef BitFlags<10> UsedShipyardLocationMask; /** * The computer-controlled opponent. @@ -111,7 +110,6 @@ class AISkirmishPlayer : public AIPlayer Real m_curLeftFlankRightDefenseAngle; Real m_curRightFlankLeftDefenseAngle; Real m_curRightFlankRightDefenseAngle; - UsedShipyardLocationMask m_usedShipyards; UnsignedInt m_frameToCheckEnemy; Player *m_currentEnemy; From 93d26e6b3aea2d02e180771927c65ecbfc162fbe Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sat, 4 Apr 2026 13:17:29 +0200 Subject: [PATCH 3/3] cleanup --- .../Include/GameLogic/TerrainLogic.h | 3 -- .../Source/GameLogic/Map/TerrainLogic.cpp | 32 ------------------- 2 files changed, 35 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h index 94c95e1bfdb..2b734de82d0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h @@ -266,9 +266,6 @@ class TerrainLogic : public Snapshot, /// Return the closest waypoint on the labeled path virtual Waypoint *getClosestWaypointOnPath( const Coord3D *pos, AsciiString label ); - /// Return a vector of shipyard locations for this Label, expected to be ShipyardN where N is player index starting with 1 - virtual std::vector getShipyardBuildPositions(const Coord3D* pos, AsciiString label); - /// Return true if the waypoint path containing pWay is labeled with the label. virtual Bool isPurposeOfPath( Waypoint *pWay, AsciiString label ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index 730d592a14e..4e3fc7054b3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp @@ -1608,38 +1608,6 @@ Waypoint *TerrainLogic::getClosestWaypointOnPath( const Coord3D *pos, AsciiStrin return pClosestWay; } -//------------------------------------------------------------------------------------------------- -/** Return shipyard building locations for a player sorted by distance to pos */ -//------------------------------------------------------------------------------------------------- -std::vector TerrainLogic::getShipyardBuildPositions(const Coord3D* pos, AsciiString label) -{ - std::vector ret; - if (label.isEmpty()) { - DEBUG_LOG(("***Warning - asking for empty path label.")); - return ret; - } - - for (Waypoint* way = m_waypointListHead; way; way = way->getNext()) { - Bool match = false; - if (label.compareNoCase(way->getPathLabel1()) == 0) match = true; - if (label.compareNoCase(way->getPathLabel2()) == 0) match = true; - if (label.compareNoCase(way->getPathLabel3()) == 0) match = true; - if (match) { - ret.push_back(way); - } - } - - std::sort(ret.begin(), ret.end(), [=](const Waypoint* a, const Waypoint* b) { - const Coord3D* posA = a->getLocation(); - const Coord3D* posB = b->getLocation(); - Real distAsqr = (posA->x - pos->x) * (posA->x - pos->x) + (posA->y - pos->y) * (posA->y - pos->y); - Real distBsqr = (posB->x - pos->x) * (posB->x - pos->x) + (posB->y - pos->y) * (posB->y - pos->y); - return distAsqr < distBsqr; - }); - - return ret; -} - //------------------------------------------------------------------------------------------------- /** Return true if the waypoint path containing pWay is labeled with the label. */ //-------------------------------------------------------------------------------------------------