From b36cd0038c1972a4e3d9f8ca9f1249186826cc9c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Jul 2026 21:53:48 +0000 Subject: [PATCH 1/2] Fix fleet and battle messages when planets or moons are destroyed Use coordinate/debris fallbacks instead of empty [planet] tags when planet IDs are missing, redirect active return missions to the parent planet before moon deletion, and snapshot attacker origin data in battle reports. Fixes #1341 Co-authored-by: Cursor --- app/GameMessages/BattleReport.php | 14 +++--- app/GameMissions/Abstracts/GameMission.php | 39 +++++++++------ app/GameMissions/AttackMission.php | 7 +-- app/GameMissions/DeploymentMission.php | 9 ++-- app/GameMissions/EspionageMission.php | 5 +- app/GameMissions/MoonDestructionMission.php | 4 +- app/GameMissions/RecycleMission.php | 3 +- app/GameMissions/TransportMission.php | 9 ++-- app/Services/FleetMissionService.php | 5 +- app/Services/PlanetService.php | 17 +++++-- app/Support/FleetMissionPlanetFormatter.php | 44 ++++++++++++++++ .../Unit/FleetMissionPlanetFormatterTest.php | 50 +++++++++++++++++++ 12 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 app/Support/FleetMissionPlanetFormatter.php create mode 100644 tests/Unit/FleetMissionPlanetFormatterTest.php diff --git a/app/GameMessages/BattleReport.php b/app/GameMessages/BattleReport.php index 979172ed7..517f0c7bc 100644 --- a/app/GameMessages/BattleReport.php +++ b/app/GameMessages/BattleReport.php @@ -181,15 +181,15 @@ private function getBattleReportParams(): array $attacker_planet_coords = $attackerPlanet->getPlanetCoordinates()->asString(); $attacker_planet_type = $attackerPlanet->getPlanetType() === PlanetType::Moon ? 'Moon' : 'Planet'; } else { - $attacker_planet_name = __('Unknown'); - $attacker_planet_coords = ''; - $attacker_planet_type = ''; + $attacker_planet_name = $this->battleReportModel->attacker['planet_name'] ?? __('Unknown'); + $attacker_planet_coords = $this->battleReportModel->attacker['planet_coords'] ?? ''; + $attacker_planet_type = $this->battleReportModel->attacker['planet_type'] ?? ''; } } catch (Throwable $e) { - // If attacker planet can't be loaded (e.g., planet deleted), use defaults - $attacker_planet_name = __('Unknown'); - $attacker_planet_coords = ''; - $attacker_planet_type = ''; + // If attacker planet can't be loaded (e.g., planet deleted), use snapshotted data + $attacker_planet_name = $this->battleReportModel->attacker['planet_name'] ?? __('Unknown'); + $attacker_planet_coords = $this->battleReportModel->attacker['planet_coords'] ?? ''; + $attacker_planet_type = $this->battleReportModel->attacker['planet_type'] ?? ''; } } else { // No planet info available diff --git a/app/GameMissions/Abstracts/GameMission.php b/app/GameMissions/Abstracts/GameMission.php index e1e276472..4e33315ef 100644 --- a/app/GameMissions/Abstracts/GameMission.php +++ b/app/GameMissions/Abstracts/GameMission.php @@ -21,6 +21,7 @@ use OGame\Models\FleetUnion; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; +use OGame\Support\FleetMissionPlanetFormatter; use OGame\Services\FleetMissionService; use OGame\Services\FleetUnionService; use OGame\Services\MessageService; @@ -574,21 +575,8 @@ protected function sendFleetReturnMessage(FleetMission $mission, PlayerService $ { $return_resources = $this->fleetMissionService->getResources($mission); - // Define from string based on whether the planet is available or not. - $from = "[coordinates]{$mission->galaxy_from}:{$mission->system_from}:{$mission->position_from}[/coordinates]"; - switch ($mission->type_from) { - case PlanetType::Planet->value: - case PlanetType::Moon->value: - if ($mission->planet_id_from !== null) { - $from = __('planet') . " [planet]{$mission->planet_id_from}[/planet]"; - } - break; - case PlanetType::DebrisField->value: - $from = "[debrisfield]{$mission->galaxy_from}:{$mission->system_from}:{$mission->position_from}[/debrisfield]"; - break; - } - - $to = __('planet') . " [planet]{$mission->planet_id_to}[/planet]"; + $from = FleetMissionPlanetFormatter::returnEndpointLabel($mission, 'from'); + $to = FleetMissionPlanetFormatter::returnEndpointLabel($mission, 'to'); if ($return_resources->any()) { $params = [ @@ -717,4 +705,25 @@ abstract protected function processArrival(FleetMission $mission): void; * @return void */ abstract protected function processReturn(FleetMission $mission): void; + + /** + * @return array{planet_coords?: string, planet_name?: string, planet_type?: string} + */ + protected function buildAttackerPlanetSnapshot(?int $planetId): array + { + if ($planetId === null) { + return []; + } + + $planet = $this->planetServiceFactory->make($planetId, true); + if ($planet === null) { + return []; + } + + return [ + 'planet_coords' => $planet->getPlanetCoordinates()->asString(), + 'planet_name' => $planet->getPlanetName(), + 'planet_type' => $planet->getPlanetType() === PlanetType::Moon ? 'Moon' : 'Planet', + ]; + } } diff --git a/app/GameMissions/AttackMission.php b/app/GameMissions/AttackMission.php index 025269ab8..4b09e039a 100644 --- a/app/GameMissions/AttackMission.php +++ b/app/GameMissions/AttackMission.php @@ -16,6 +16,7 @@ use OGame\Models\BattleReport; use OGame\Models\Enums\PlanetType; use OGame\Models\FleetMission; +use OGame\Support\FleetMissionPlanetFormatter; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; use OGame\Services\CharacterClassService; @@ -449,7 +450,7 @@ protected function processArrival(FleetMission $mission): void $reaperCargoCapacity = $reaperObject->properties->capacity->calculate($attackerPlayer)->totalValue * $reaperCount; $this->messageService->sendSystemMessageToPlayer($attackerPlayer, DebrisFieldHarvest::class, [ - 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', + 'from' => FleetMissionPlanetFormatter::tag($mission, 'from'), 'to' => '[debrisfield]' . $defenderPlanet->getPlanetCoordinates()->asString(). '[/debrisfield]', 'coordinates' => '[coordinates]' . $defenderPlanet->getPlanetCoordinates()->asString() . '[/coordinates]', 'ship_name' => $reaperObject->title, @@ -649,7 +650,7 @@ private function createBattleReport(PlayerService $attackPlayer, PlanetService $ $attackerCharacterClass = $characterClassService->getCharacterClass($attackPlayer->getUser()); $defenderCharacterClass = $characterClassService->getCharacterClass($defenderPlanet->getPlayer()->getUser()); - $report->attacker = [ + $report->attacker = array_merge([ 'player_id' => $attackPlayer->getId(), 'resource_loss' => $battleResult->attackerResourceLoss->sum(), 'units' => $battleResult->attackerUnitsStart->toArray(), @@ -658,7 +659,7 @@ private function createBattleReport(PlayerService $attackPlayer, PlanetService $ 'armor_technology' => $battleResult->attackerArmorLevel, 'planet_id' => $battleResult->attackerPlanetId, 'character_class' => $attackerCharacterClass?->getName(), - ]; + ], $this->buildAttackerPlanetSnapshot($battleResult->attackerPlanetId)); // TODO: Enhance battle reports to show individual participating fleets/defenders // Currently shows aggregated defender data (combined units, planet owner's tech, single player_id) diff --git a/app/GameMissions/DeploymentMission.php b/app/GameMissions/DeploymentMission.php index d702f4d34..1382bf431 100644 --- a/app/GameMissions/DeploymentMission.php +++ b/app/GameMissions/DeploymentMission.php @@ -11,6 +11,7 @@ use OGame\GameObjects\Models\Units\UnitCollection; use OGame\Models\Enums\PlanetType; use OGame\Models\FleetMission; +use OGame\Support\FleetMissionPlanetFormatter; use OGame\Models\Planet\Coordinate; use OGame\Services\PlanetService; @@ -69,16 +70,16 @@ protected function processArrival(FleetMission $mission): void // Send a message to the player that the mission has arrived if ($resources->any()) { $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), FleetDeploymentWithResources::class, [ - 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', - 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'from' => FleetMissionPlanetFormatter::tag($mission, 'from'), + 'to' => FleetMissionPlanetFormatter::tag($mission, 'to'), 'metal' => (string)$mission->metal, 'crystal' => (string)$mission->crystal, 'deuterium' => (string)($mission->deuterium + ($mission->deuterium_consumption / 2)), //if mission deployment: Add half of the consumed deuterium ]); } else { $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), FleetDeployment::class, [ - 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', - 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'from' => FleetMissionPlanetFormatter::tag($mission, 'from'), + 'to' => FleetMissionPlanetFormatter::tag($mission, 'to'), ]); } diff --git a/app/GameMissions/EspionageMission.php b/app/GameMissions/EspionageMission.php index f3b8341e2..1e9b7f924 100644 --- a/app/GameMissions/EspionageMission.php +++ b/app/GameMissions/EspionageMission.php @@ -18,6 +18,7 @@ use OGame\Models\Enums\PlanetType; use OGame\Models\EspionageReport; use OGame\Models\FleetMission; +use OGame\Support\FleetMissionPlanetFormatter; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; use OGame\Services\CounterEspionageService; @@ -170,8 +171,8 @@ protected function processArrival(FleetMission $mission): void $params = [ // IMPORTANT: pass the raw mission planet id inside [planet]...[/planet] - 'planet' => '[planet]' . $mission->planet_id_from . '[/planet]', - 'defender' => '[planet]' . $mission->planet_id_to . '[/planet]', // defender planet + 'planet' => FleetMissionPlanetFormatter::tag($mission, 'from'), + 'defender' => FleetMissionPlanetFormatter::tag($mission, 'to'), 'attacker_name' => $attackerName, 'chance' => $counterEspionageChance, ]; diff --git a/app/GameMissions/MoonDestructionMission.php b/app/GameMissions/MoonDestructionMission.php index ad0be977c..10dab5812 100644 --- a/app/GameMissions/MoonDestructionMission.php +++ b/app/GameMissions/MoonDestructionMission.php @@ -412,7 +412,7 @@ private function createBattleReport(PlayerService $attackPlayer, PlanetService $ 'moon_created' => false, ]; - $report->attacker = [ + $report->attacker = array_merge([ 'player_id' => $attackPlayer->getId(), 'resource_loss' => $battleResult->attackerResourceLoss->sum(), 'units' => $battleResult->attackerUnitsStart->toArray(), @@ -420,7 +420,7 @@ private function createBattleReport(PlayerService $attackPlayer, PlanetService $ 'shielding_technology' => $battleResult->attackerShieldLevel, 'armor_technology' => $battleResult->attackerArmorLevel, 'planet_id' => $battleResult->attackerPlanetId, - ]; + ], $this->buildAttackerPlanetSnapshot($battleResult->attackerPlanetId)); $report->defender = [ 'player_id' => $defenderMoon->getPlayer()->getId(), diff --git a/app/GameMissions/RecycleMission.php b/app/GameMissions/RecycleMission.php index c5e4d754f..f1d571ded 100644 --- a/app/GameMissions/RecycleMission.php +++ b/app/GameMissions/RecycleMission.php @@ -14,6 +14,7 @@ use OGame\Models\FleetMission; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; +use OGame\Support\FleetMissionPlanetFormatter; use OGame\Services\DebrisFieldService; use OGame\Services\ObjectService; use OGame\Services\PlanetService; @@ -119,7 +120,7 @@ protected function processArrival(FleetMission $mission): void // Send a message to the player that the mission has arrived and the resources (if any) have been collected. $this->messageService->sendSystemMessageToPlayer($originPlanet->getPlayer(), DebrisFieldHarvest::class, [ - 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', + 'from' => FleetMissionPlanetFormatter::tag($mission, 'from'), 'to' => '[debrisfield]' . $targetCoordinate->asString(). '[/debrisfield]', 'coordinates' => '[coordinates]' . $targetCoordinate->asString() . '[/coordinates]', 'ship_name' => $harvesterShip->title, diff --git a/app/GameMissions/TransportMission.php b/app/GameMissions/TransportMission.php index 6228ad1f4..96e558298 100644 --- a/app/GameMissions/TransportMission.php +++ b/app/GameMissions/TransportMission.php @@ -11,6 +11,7 @@ use OGame\GameObjects\Models\Units\UnitCollection; use OGame\Models\Enums\PlanetType; use OGame\Models\FleetMission; +use OGame\Support\FleetMissionPlanetFormatter; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; use OGame\Services\PlanetService; @@ -66,8 +67,8 @@ protected function processArrival(FleetMission $mission): void // Send a message to the origin player that the mission has arrived $this->messageService->sendSystemMessageToPlayer($origin_planet->getPlayer(), TransportArrived::class, [ - 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', - 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'from' => FleetMissionPlanetFormatter::tag($mission, 'from'), + 'to' => FleetMissionPlanetFormatter::tag($mission, 'to'), 'metal' => (string)$mission->metal, 'crystal' => (string)$mission->crystal, 'deuterium' => (string)$mission->deuterium, @@ -76,8 +77,8 @@ protected function processArrival(FleetMission $mission): void if ($origin_planet->getPlayer()->getId() !== $target_planet->getPlayer()->getId()) { // Send a message to the target player that the mission has arrived $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), TransportReceived::class, [ - 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', - 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'from' => FleetMissionPlanetFormatter::tag($mission, 'from'), + 'to' => FleetMissionPlanetFormatter::tag($mission, 'to'), 'metal' => (string)$mission->metal, 'crystal' => (string)$mission->crystal, 'deuterium' => (string)$mission->deuterium, diff --git a/app/Services/FleetMissionService.php b/app/Services/FleetMissionService.php index f9682255e..d8bf3cfb8 100644 --- a/app/Services/FleetMissionService.php +++ b/app/Services/FleetMissionService.php @@ -17,6 +17,7 @@ use OGame\Models\FleetMission; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; +use OGame\Support\FleetMissionPlanetFormatter; /** * Class FleetMissionService. @@ -619,12 +620,12 @@ private function sendAcsDefendArrivalMessages(FleetMission $mission): void // Send message to sender (Fleet Command) $this->messageService->sendSystemMessageToPlayer($origin_planet->getPlayer(), AcsDefendArrivalSender::class, [ - 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'to' => FleetMissionPlanetFormatter::tag($mission, 'to'), ]); // Send message to host/target (Space Monitoring) $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), AcsDefendArrivalHost::class, [ - 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'to' => FleetMissionPlanetFormatter::tag($mission, 'to'), ]); } diff --git a/app/Services/PlanetService.php b/app/Services/PlanetService.php index c702703f1..fb292702d 100644 --- a/app/Services/PlanetService.php +++ b/app/Services/PlanetService.php @@ -248,7 +248,7 @@ public function abandonPlanet(): void // Moon-origin fleets must keep a valid home planet so they can still return after the moon is gone. if ($this->isMoon()) { - $this->redirectActiveOutgoingMoonMissionsToParentPlanet(); + $this->redirectActiveMoonMissionsToParentPlanet(); } // Fleet missions @@ -279,10 +279,9 @@ public function abandonPlanet(): void } /** - * Rebind active missions launched from a moon to its parent planet before the moon is deleted. - * This preserves the original flight while ensuring the fleet returns to the planet instead. + * Rebind active missions involving a moon to its parent planet before the moon is deleted. */ - private function redirectActiveOutgoingMoonMissionsToParentPlanet(): void + private function redirectActiveMoonMissionsToParentPlanet(): void { $parentPlanet = $this->getParentPlanet(); if ($parentPlanet === null) { @@ -300,6 +299,16 @@ private function redirectActiveOutgoingMoonMissionsToParentPlanet(): void 'position_from' => $parentCoordinates->position, 'type_from' => PlanetType::Planet->value, ]); + + FleetMission::where('planet_id_to', $this->planet->id) + ->where('processed', 0) + ->update([ + 'planet_id_to' => $parentPlanet->getPlanetId(), + 'galaxy_to' => $parentCoordinates->galaxy, + 'system_to' => $parentCoordinates->system, + 'position_to' => $parentCoordinates->position, + 'type_to' => PlanetType::Planet->value, + ]); } /** diff --git a/app/Support/FleetMissionPlanetFormatter.php b/app/Support/FleetMissionPlanetFormatter.php new file mode 100644 index 000000000..efcd449a0 --- /dev/null +++ b/app/Support/FleetMissionPlanetFormatter.php @@ -0,0 +1,44 @@ +planet_id_from : $mission->planet_id_to; + $galaxy = $endpoint === 'from' ? $mission->galaxy_from : $mission->galaxy_to; + $system = $endpoint === 'from' ? $mission->system_from : $mission->system_to; + $position = $endpoint === 'from' ? $mission->position_from : $mission->position_to; + $type = $endpoint === 'from' ? $mission->type_from : $mission->type_to; + + if ($type === PlanetType::DebrisField->value) { + return "[debrisfield]{$galaxy}:{$system}:{$position}[/debrisfield]"; + } + + if ($planetId !== null) { + return "[planet]{$planetId}[/planet]"; + } + + return "[coordinates]{$galaxy}:{$system}:{$position}[/coordinates]"; + } + + public static function returnEndpointLabel(FleetMission $mission, string $endpoint): string + { + $tag = self::tag($mission, $endpoint); + $type = $endpoint === 'from' ? $mission->type_from : $mission->type_to; + + if (in_array($type, [PlanetType::Planet->value, PlanetType::Moon->value], true) + && str_starts_with($tag, '[planet]')) { + return __('planet') . ' ' . $tag; + } + + return $tag; + } +} diff --git a/tests/Unit/FleetMissionPlanetFormatterTest.php b/tests/Unit/FleetMissionPlanetFormatterTest.php new file mode 100644 index 000000000..66f8d3546 --- /dev/null +++ b/tests/Unit/FleetMissionPlanetFormatterTest.php @@ -0,0 +1,50 @@ + 42, + 'galaxy_from' => 1, + 'system_from' => 2, + 'position_from' => 3, + 'type_from' => PlanetType::Planet->value, + ]); + + $this->assertSame('[planet]42[/planet]', FleetMissionPlanetFormatter::tag($mission, 'from')); + } + + public function testTagFallsBackToCoordinatesWhenPlanetIdIsNull(): void + { + $mission = new FleetMission([ + 'planet_id_from' => null, + 'galaxy_from' => 1, + 'system_from' => 2, + 'position_from' => 3, + 'type_from' => PlanetType::Moon->value, + ]); + + $this->assertSame('[coordinates]1:2:3[/coordinates]', FleetMissionPlanetFormatter::tag($mission, 'from')); + } + + public function testTagUsesDebrisFieldPlaceholder(): void + { + $mission = new FleetMission([ + 'planet_id_to' => null, + 'galaxy_to' => 4, + 'system_to' => 5, + 'position_to' => 6, + 'type_to' => PlanetType::DebrisField->value, + ]); + + $this->assertSame('[debrisfield]4:5:6[/debrisfield]', FleetMissionPlanetFormatter::tag($mission, 'to')); + } +} From b943d5925c055372c41a962e4019c30431e1c226 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Jul 2026 22:42:32 +0000 Subject: [PATCH 2/2] fix: address CI failures for destroyed moon message helpers Use int|null for PHPStan noShortNullableType, and assign FleetMission attributes directly in unit tests to avoid mass-assignment exceptions. Co-authored-by: Cursor --- app/GameMissions/Abstracts/GameMission.php | 2 +- .../Unit/FleetMissionPlanetFormatterTest.php | 39 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/app/GameMissions/Abstracts/GameMission.php b/app/GameMissions/Abstracts/GameMission.php index 4e33315ef..25828c1b4 100644 --- a/app/GameMissions/Abstracts/GameMission.php +++ b/app/GameMissions/Abstracts/GameMission.php @@ -709,7 +709,7 @@ abstract protected function processReturn(FleetMission $mission): void; /** * @return array{planet_coords?: string, planet_name?: string, planet_type?: string} */ - protected function buildAttackerPlanetSnapshot(?int $planetId): array + protected function buildAttackerPlanetSnapshot(int|null $planetId): array { if ($planetId === null) { return []; diff --git a/tests/Unit/FleetMissionPlanetFormatterTest.php b/tests/Unit/FleetMissionPlanetFormatterTest.php index 66f8d3546..ca02597e6 100644 --- a/tests/Unit/FleetMissionPlanetFormatterTest.php +++ b/tests/Unit/FleetMissionPlanetFormatterTest.php @@ -11,39 +11,36 @@ class FleetMissionPlanetFormatterTest extends UnitTestCase { public function testTagUsesPlanetIdWhenAvailable(): void { - $mission = new FleetMission([ - 'planet_id_from' => 42, - 'galaxy_from' => 1, - 'system_from' => 2, - 'position_from' => 3, - 'type_from' => PlanetType::Planet->value, - ]); + $mission = new FleetMission(); + $mission->planet_id_from = 42; + $mission->galaxy_from = 1; + $mission->system_from = 2; + $mission->position_from = 3; + $mission->type_from = PlanetType::Planet->value; $this->assertSame('[planet]42[/planet]', FleetMissionPlanetFormatter::tag($mission, 'from')); } public function testTagFallsBackToCoordinatesWhenPlanetIdIsNull(): void { - $mission = new FleetMission([ - 'planet_id_from' => null, - 'galaxy_from' => 1, - 'system_from' => 2, - 'position_from' => 3, - 'type_from' => PlanetType::Moon->value, - ]); + $mission = new FleetMission(); + $mission->planet_id_from = null; + $mission->galaxy_from = 1; + $mission->system_from = 2; + $mission->position_from = 3; + $mission->type_from = PlanetType::Moon->value; $this->assertSame('[coordinates]1:2:3[/coordinates]', FleetMissionPlanetFormatter::tag($mission, 'from')); } public function testTagUsesDebrisFieldPlaceholder(): void { - $mission = new FleetMission([ - 'planet_id_to' => null, - 'galaxy_to' => 4, - 'system_to' => 5, - 'position_to' => 6, - 'type_to' => PlanetType::DebrisField->value, - ]); + $mission = new FleetMission(); + $mission->planet_id_to = null; + $mission->galaxy_to = 4; + $mission->system_to = 5; + $mission->position_to = 6; + $mission->type_to = PlanetType::DebrisField->value; $this->assertSame('[debrisfield]4:5:6[/debrisfield]', FleetMissionPlanetFormatter::tag($mission, 'to')); }