$method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, ]; if ($body !== null) { $headers[] = 'Content-Type: application/json'; $options[CURLOPT_POSTFIELDS] = json_encode($body, JSON_UNESCAPED_SLASHES); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($ch, $options); $raw = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); $decoded = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($decoded)) { throw new RuntimeException('Răspuns API invalid, HTTP ' . $code); } $decoded['_http'] = $code; return $decoded; } function m2h_start(string $playerRef): array { if (!preg_match('/^[A-Za-z0-9_.:@-]{1,64}$/', $playerRef)) { throw new InvalidArgumentException('Identificatorul jucătorului nu este acceptat.'); } return m2h_request('POST', '/api/v1/servers/{server}/vote-intents', [ 'player_ref' => $playerRef, 'idempotency_key' => bin2hex(random_bytes(16)), ]); } function m2h_reward_pdo(): PDO { return new PDO( (string) getenv('M2H_REWARD_DSN'), (string) getenv('M2H_REWARD_USER'), (string) getenv('M2H_REWARD_PASSWORD'), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } /** * Înlocuiește corpul acestei funcții cu recompensa serverului tău. * Dacă recompensa și jurnalul sunt în aceeași bază, ține-le în aceeași tranzacție. * Dacă sunt sisteme diferite, păstrează jurnalul local înainte de confirmare * și reia din jurnal la următoarea rulare. Metin2-Help nu promite livrare unică * către joc; confirmarea repetată a aceluiași Vote ID nu mai creează o confirmare nouă. */ function m2h_grant_local(PDO $pdo, int $voteId, string $playerRef): void { $pdo->beginTransaction(); try { $seen = $pdo->prepare('INSERT INTO owner_vote_rewards (vote_id, player_ref, granted_at) VALUES (?, ?, ?)'); $seen->execute([$voteId, $playerRef, gmdate('Y-m-d H:i:s')]); // Exemplu de înlocuit: $pdo->prepare('UPDATE account SET coins = coins + ? WHERE id = ?')->execute([10, $playerRef]); $pdo->commit(); } catch (PDOException $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } if ((string) $exception->getCode() !== '23000') { throw $exception; } } } function m2h_poll(): void { $pdo = m2h_reward_pdo(); $pdo->exec('CREATE TABLE IF NOT EXISTS owner_vote_rewards (vote_id BIGINT NOT NULL PRIMARY KEY, player_ref VARCHAR(64) NOT NULL, granted_at DATETIME NOT NULL)'); $cursor = 0; do { $page = m2h_request('GET', '/api/v1/servers/{server}/votes/unclaimed?limit=50&cursor=' . $cursor); $rows = $page['data'] ?? []; $ids = []; foreach ($rows as $row) { if (!is_array($row) || empty($row['vote_id']) || empty($row['player_ref'])) { continue; } m2h_grant_local($pdo, (int) $row['vote_id'], (string) $row['player_ref']); $ids[] = (int) $row['vote_id']; } if ($ids !== []) { $claim = m2h_request('POST', '/api/v1/servers/{server}/votes/claim', ['vote_ids' => $ids]); foreach ($claim['data'] ?? [] as $result) { // already_claimed nu dovedește recompensa locală. Dovada este rândul din owner_vote_rewards. if (($result['result'] ?? '') === 'rejected') { error_log('Metin2-Help a respins vote_id ' . (int) ($result['vote_id'] ?? 0)); } } } $cursor = (int) ($page['next_cursor'] ?? 0); } while ($cursor > 0); } if (PHP_SAPI === 'cli' && isset($argv[1]) && $argv[1] === 'poll') { m2h_poll(); }