Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up PHP with Xdebug
uses: shivammathur/setup-php@v2

Check failure on line 18 in .github/workflows/sonarcloud.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=OGSteam_mod-xtense&issues=AaGHvXJEJb__eoK2Fdij&open=AaGHvXJEJb__eoK2Fdij&pullRequest=64
with:
php-version: '8.4'
coverage: xdebug
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: Run PHPUnit with coverage
run: php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-clover=coverage.xml
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ vendor/
# PHPUnit cache
.phpunit.result.cache

# PHPUnit coverage output
coverage.xml

46 changes: 46 additions & 0 deletions Handler/AllyListHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

if (!defined('IN_SPYOGAME')) {
die('Hacking Attempt!');
}

class AllyListHandler extends AbstractHandler
{
public function handle(array $data): void
{
if (!$this->requireGrant($this->getRequiredGrant())) {
return;
}
if (!isset($data['tag'], $data['allyList'])) {
throw new UnexpectedValueException('Allylist: Missing Value');
}

$tag = filter_var($data['tag']);
$members = [];
foreach ($data['allyList'] as $member) {
if (!isset($member['player'], $member['points'], $member['rank'], $member['coords'])) {
throw new UnexpectedValueException('Allylist: Missing Ally Detailed list');
}
$members[] = [
'pseudo' => filter_var($member['player']),
'points' => filter_var($member['points'], FILTER_SANITIZE_NUMBER_INT),
'coords' => explode(':', $member['coords']),
'rang' => filter_var($member['rank'], FILTER_SANITIZE_NUMBER_INT),
];
}

$this->registerCallback('ally_list', ['list' => $members, 'tag' => $tag]);
$this->io->set(['type' => 'ally_list', 'tag' => $tag]);
$this->logAction('ally_list', ['tag' => $tag]);
}

public function getType(): string
{
return 'ally_list';
}

public function getRequiredGrant(): string
{
return 'ranking';
}
}
70 changes: 70 additions & 0 deletions Handler/BuildingsHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

if (!defined('IN_SPYOGAME')) {
die('Hacking Attempt!');
}

class BuildingsHandler extends AbstractHandler
{
public function handle(array $data): void
{
if (!$this->requireGrant($this->getRequiredGrant())) {
return;
}

$coordinates = filter_var($data['planet']['coords']);
$planetName = filter_var($data['planet']['name']);
$planetType = filter_var($data['planet']['type']);
$planetId = filter_var($data['planet']['id'], FILTER_VALIDATE_INT);
if (!isset($coordinates, $planetName, $planetType)) {
throw new UnexpectedValueException('Buildings- Missing data');
}

$buildings = $data['buildings'];
$coordinates = Check::coords($coordinates);
[$galaxy, $system, $position] = explode(':', $coordinates);
$planetType = $this->resolvePlanetType($planetType);

$columns = ['id', 'galaxy', 'system', 'row', 'name'];
$values = [$planetId, $galaxy, $system, $position, $planetName];
foreach ($this->database['buildings'] as $code) {
if (isset($buildings[$code])) {
$columns[] = $code;
$values[] = (int)$buildings[$code];
}
}

$updatePairs = ['name = VALUES(name)', 'galaxy = VALUES(galaxy)', 'system = VALUES(system)', 'row = VALUES(row)'];
foreach ($this->database['buildings'] as $code) {
if (isset($buildings[$code])) {
$updatePairs[] = "`$code` = VALUES(`$code`)";
}
}

$query = "INSERT INTO " . TABLE_USER_BUILDING . " (`" . implode('`, `', $columns) . "`)
VALUES (" . implode(', ', array_map(static function ($value) {
return is_numeric($value) ? $value : "'$value'";
}, $values)) . ")
ON DUPLICATE KEY UPDATE " . implode(', ', $updatePairs);
$this->db->sql_query($query);

$this->setPageUpdatedResponse('buildings', $coordinates);
$this->registerCallback('buildings', [
'coords' => explode(':', $coordinates),
'planet_type' => $planetType,
'planet_name' => $planetName,
'buildings' => $buildings,
]);
$this->logAction('buildings', ['coords' => $coordinates, 'planet_name' => $planetName]);
}

public function getType(): string
{
return 'buildings';
}

public function getRequiredGrant(): string
{
return 'empire';
}
}
218 changes: 218 additions & 0 deletions Handler/CombatReportHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
<?php

if (!defined('IN_SPYOGAME')) {
die('Hacking Attempt!');
}

class CombatReportHandler extends AbstractHandler
{
private const FLEET_TECHNOLOGIES = [
202 => 'PT', 203 => 'GT', 204 => 'CLE', 205 => 'CLO', 206 => 'CR',
207 => 'VB', 208 => 'VC', 209 => 'REC', 210 => 'SE', 211 => 'BMD',
212 => 'SAT', 213 => 'DST', 214 => 'EDLM', 215 => 'TRA', 217 => 'FOR',
218 => 'FAU', 219 => 'ECL',
];

private const DEFENSE_TECHNOLOGIES = [
401 => 'LM', 402 => 'LLE', 403 => 'LLO', 404 => 'CG', 405 => 'AI',
406 => 'LP', 407 => 'PB', 408 => 'GB', 502 => 'MIC', 503 => 'MIP',
];

public function handle(array $data): void
{
$reportType = $data['_xtense_request_type'] ?? 'rc';
$rawPayload = $data['_xtense_raw_payload'] ?? json_encode($data);
unset($data['_xtense_request_type'], $data['_xtense_raw_payload']);
$api = filter_var($data['ogapilnk'] ?? '', FILTER_DEFAULT) ?: '';

if (!isset($data['timestamp'])) {
throw new UnexpectedValueException('Combat Report: Timestamp not sent');
}
if (!$this->requireGrant($this->getRequiredGrant())) {
return;
}

$this->registerCallback('rc', ['json' => $rawPayload, 'api' => $api]);
$timestamp = (int)$data['timestamp'];
$existing = $this->db->sql_fetch_row($this->db->sql_query(
"SELECT `id_rc` FROM " . TABLE_PARSEDRC . " WHERE `dateRC` = '{$timestamp}'"
));
if (isset($existing[0])) {
$this->io->set(['type' => $reportType]);
$this->logAction($reportType);
return;
}

$coordinates = $this->parseCoordinates($data['coordinates'])['coords'];
$rounds = $data['combatRounds'] ?? [];
$winner = match ($data['result']['winner'] ?? null) {
'draw' => 'N',
'attacker' => 'A',
'defender' => 'D',
default => throw new UnexpectedValueException('Combat Report: Result not found'),
};

$loot = $this->resourceAmounts($data['result']['loot']['resources'] ?? []);
$debris = $this->resourceAmounts($data['result']['debris']['resources'] ?? []);
$losses = ['attacker' => 0, 'defender' => 0];
foreach ($data['result']['totalValueOfUnitsLost'] ?? [] as $loss) {
if (isset($losses[$loss['side'] ?? ''])) {
$losses[$loss['side']] = (int)($loss['value'] ?? 0);
}
}
$moonChance = (int)($data['result']['moonCreation']['chance'] ?? 0);

$defenderObject = $data['defenderSpaceObject'] ?? [];
$objectType = $this->db->sql_escape_string((string)($defenderObject['type'] ?? ''));
$parsedCoordinates = $this->parseCoordinates($coordinates);
$objectResult = $this->db->sql_query("SELECT id FROM " . TABLE_USER_BUILDING . "
WHERE galaxy = {$parsedCoordinates['galaxy']} AND system = {$parsedCoordinates['system']}
AND row = {$parsedCoordinates['row']} AND type = '{$objectType}'
ORDER BY last_update DESC LIMIT 1");
$objectRow = $this->db->sql_fetch_row($objectResult);
$astroObjectId = isset($objectRow[0]) ? (int)$objectRow[0] : null;
if ($astroObjectId === null) {
throw new UnexpectedValueException("Combat Report: Astro object not found for coordinates {$coordinates} with type {$objectType}");
}

$this->db->sql_query("INSERT INTO " . TABLE_PARSEDRC . " (
`dateRC`, `astro_object_id`, `nb_rounds`, `victoire`, `pertes_A`, `pertes_D`,
`gain_M`, `gain_C`, `gain_D`, `debris_M`, `debris_C`, `lune`
) VALUES (
{$timestamp}, {$astroObjectId}, " . count($rounds) . ", '{$winner}', {$losses['attacker']}, {$losses['defender']},
{$loot['metal']}, {$loot['crystal']}, {$loot['deuterium']}, {$debris['metal']}, {$debris['crystal']}, {$moonChance}
)");
$reportId = (int)$this->db->sql_insertid();

$combatants = $this->indexCombatants($data['fleets'] ?? []);
foreach ($rounds as $roundNumber => $round) {
$statistics = $this->roundStatistics($round['statistics'] ?? []);
$this->db->sql_query("INSERT INTO " . TABLE_PARSEDRCROUND . " (
`id_rc`, `numround`, `attaque_tir`, `attaque_puissance`, `defense_bouclier`,
`attaque_bouclier`, `defense_tir`, `defense_puissance`
) VALUES (
{$reportId}, " . (int)$roundNumber . ", {$statistics['attacker']['hits']}, {$statistics['attacker']['strength']},
{$statistics['defender']['absorbedDamage']}, {$statistics['attacker']['absorbedDamage']},
{$statistics['defender']['hits']}, {$statistics['defender']['strength']}
)");
$roundId = (int)$this->db->sql_insertid();
if ($roundId === 0) {
throw new UnexpectedValueException("Combat Report: Failed to insert round {$roundNumber}");
}

foreach ($round['fleets'] ?? [] as $fleet) {
$this->insertRoundFleet($roundId, $astroObjectId, $fleet, $combatants);
}
}

$this->io->set(['type' => $reportType]);
$this->logAction($reportType);
}

private function resourceAmounts(array $resources): array
{
$amounts = ['metal' => 0, 'crystal' => 0, 'deuterium' => 0];
foreach ($resources as $resource) {
$name = $resource['resource'] ?? '';
if (array_key_exists($name, $amounts)) {
$amounts[$name] = (int)($resource['amount'] ?? 0);
}
}
return $amounts;
}

private function indexCombatants(array $fleets): array
{
$combatants = ['attacker' => [], 'defender' => []];
foreach ($fleets as $fleet) {
$side = $fleet['side'] ?? '';
if (!isset($combatants[$side])) {
continue;
}
$research = ['armor' => 0, 'weapon' => 0, 'shield' => 0];
foreach ($fleet['combatResearchPercentage'] ?? [] as $technology) {
$key = match ((int)($technology['id'] ?? 0)) {
109 => 'armor',
110 => 'weapon',
111 => 'shield',
default => null,
};
if ($key !== null) {
$research[$key] = (int)($technology['percentage'] ?? 0);
}
}
$player = $fleet['player'] ?? [];
$combatants[$side][(string)($player['name'] ?? '')] = $research;
}
return $combatants;
}

private function roundStatistics(array $statistics): array
{
$result = [
'attacker' => ['hits' => 0, 'strength' => 0, 'absorbedDamage' => 0],
'defender' => ['hits' => 0, 'strength' => 0, 'absorbedDamage' => 0],
];
foreach ($statistics as $stat) {
$side = $stat['side'] ?? '';
if (!isset($result[$side])) {
continue;
}
foreach (['hits', 'strength', 'absorbedDamage'] as $field) {
$result[$side][$field] = (int)($stat[$field] ?? 0);
}
}
return $result;
}

private function insertRoundFleet(int $roundId, int $astroObjectId, array $fleet, array $combatants): void
{
$side = $fleet['side'] ?? '';
if (!in_array($side, ['attacker', 'defender'], true) || !isset($fleet['player'])) {
return;
}

$player = $fleet['player'];
$playerName = $this->db->sql_escape_string((string)($player['name'] ?? 'Unknown'));
$playerId = (int)($player['id'] ?? -1);
$research = $combatants[$side][(string)($player['name'] ?? '')] ?? ['armor' => 0, 'weapon' => 0, 'shield' => 0];
$units = $side === 'attacker'
? $this->database['fleet']
: array_values(array_unique(array_merge($this->database['fleet'], ['SAT', 'FOR'], $this->database['defense'])));
$mapping = self::FLEET_TECHNOLOGIES + ($side === 'defender' ? self::DEFENSE_TECHNOLOGIES : []);
$unitCounts = array_fill_keys($units, 0);

foreach ($fleet['technologies'] ?? [] as $technology) {
$technologyId = (int)($technology['technologyId'] ?? 0);
$unitCode = $mapping[$technologyId] ?? null;
if ($unitCode !== null && array_key_exists($unitCode, $unitCounts)) {
$unitCounts[$unitCode] = (int)($technology['remaining'] ?? 0);
}
}

$unitValues = [];
foreach ($units as $unit) {
$unitValues[] = in_array($unit, ['PB', 'GB'], true)
? (int)($unitCounts[$unit] > 0)
: $unitCounts[$unit];
}
$table = $side === 'attacker' ? TABLE_ROUND_ATTACK : TABLE_ROUND_DEFENSE;
$columns = $side === 'attacker'
? '`id_rcround`, `player`, `astro_object_id`, `Armes`, `Bouclier`, `Protection`'
: '`id_rcround`, `player_id`, `astro_object_id`, `Armes`, `Bouclier`, `Protection`';
$playerValue = $side === 'attacker' ? "'{$playerName}'" : (string)$playerId;
$unitColumns = '`' . implode('`, `', $units) . '`';
$this->db->sql_query("INSERT INTO {$table} ({$columns}, {$unitColumns}) VALUES (
{$roundId}, {$playerValue}, {$astroObjectId}, {$research['weapon']}, {$research['shield']}, {$research['armor']}, " . implode(', ', $unitValues) . ')');
}

public function getType(): string
{
return 'rc';
}

public function getRequiredGrant(): string
{
return 'messages';
}
}
Loading
Loading