'O', '1' => 'I']); return $s; } function generate_code(): string { $n = strlen(LINK_CODE_ALPHABET); $out = ''; for ($i = 0; $i < LINK_CODE_LEN; $i++) { $out .= LINK_CODE_ALPHABET[random_int(0, $n - 1)]; } return $out; } function format_code_display(string $code): string { if (strlen($code) === 8) { return substr($code, 0, 4) . '-' . substr($code, 4, 4); } return $code; } function code_is_expired($lastUsedAt): bool { if (!$lastUsedAt) { return false; } $ts = strtotime((string)$lastUsedAt); if ($ts === false) { return false; } return (time() - $ts) > (LINK_CODE_TTL_DAYS * 86400); } /** ลบรหัสที่หมดอายุออกจากทุกบัญชี (lazy GC) — แก้ใน $store ตรง ๆ */ function gc_expired_codes(array &$store): bool { $changed = false; foreach ($store['accounts'] as $i => $a) { if (!empty($a['linkCode']) && code_is_expired($a['linkCodeLastUsedAt'] ?? null)) { unset($store['accounts'][$i]['linkCode']); $store['accounts'][$i]['linkCodeLastUsedAt'] = null; $changed = true; } } return $changed; } function code_in_use(array $store, string $code, ?string $exceptKey = null): bool { foreach ($store['accounts'] as $a) { if (($a['linkCode'] ?? '') !== $code) { continue; } if ($exceptKey !== null && ($a['providerUserId'] ?? '') === $exceptKey) { continue; } if (!code_is_expired($a['linkCodeLastUsedAt'] ?? null)) { return true; } } return false; } function profile_payload(array $a): array { return [ 'playerKey' => $a['providerUserId'] ?? '', 'coins' => max(0, (int)($a['coins'] ?? 0)), 'colorThemeIndex' => ($v = (int)($a['lobbyColorThemeIndex'] ?? 0)) >= 1 && $v <= 8 ? $v : null, 'skinToneIndex' => ($s = (int)($a['lobbySkinToneIndex'] ?? 0)) >= 1 && $s <= 3 ? $s : null, 'displayName' => (string)($a['displayName'] ?? ''), 'agentId' => (string)($a['agentDisplayId'] ?? ''), ]; } $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; if ($method === 'GET') { $action = (string)($_GET['action'] ?? 'code'); if ($action !== 'code') { json_response(['ok' => false, 'error' => 'unknown action'], 400); } $key = trim((string)($_GET['playerKey'] ?? '')); if (!valid_player_key($key)) { json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400); } $displayName = sanitize_display_name((string)($_GET['displayName'] ?? '')); $agentId = sanitize_agent_id((string)($_GET['agentId'] ?? '')); $store = read_store(); gc_expired_codes($store); $now = gmdate('c'); $idx = -1; foreach ($store['accounts'] as $i => $a) { if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { $idx = $i; break; } } if ($idx < 0) { $store['accounts'][] = [ 'id' => new_id(), 'email' => '', 'displayName' => $displayName, 'loginType' => 'guest', 'providerUserId' => $key, 'notes' => 'auto: player-link', 'blocked' => false, 'coins' => 0, 'agentDisplayId' => $agentId, 'createdAt' => $now, 'updatedAt' => $now, ]; $idx = count($store['accounts']) - 1; } else { if ($displayName !== '') { $store['accounts'][$idx]['displayName'] = $displayName; } if ($agentId !== '') { $store['accounts'][$idx]['agentDisplayId'] = $agentId; } $store['accounts'][$idx]['updatedAt'] = $now; } $code = (string)($store['accounts'][$idx]['linkCode'] ?? ''); if ($code === '' || code_is_expired($store['accounts'][$idx]['linkCodeLastUsedAt'] ?? null)) { do { $code = generate_code(); } while (code_in_use($store, $code, $key)); $store['accounts'][$idx]['linkCode'] = $code; } $store['accounts'][$idx]['linkCodeLastUsedAt'] = $now; if (!write_store($store)) { json_response(['ok' => false, 'error' => 'สร้างรหัสไม่สำเร็จ'], 500); } json_response(['ok' => true, 'code' => $code, 'codeDisplay' => format_code_display($code)]); } if ($method === 'POST') { $body = require_json_body(); $action = (string)($body['action'] ?? 'redeem'); if ($action !== 'redeem') { json_response(['ok' => false, 'error' => 'unknown action'], 400); } $code = normalize_code((string)($body['code'] ?? '')); if (!preg_match('/^[A-Z2-9]{' . LINK_CODE_LEN . '}$/', $code)) { json_response(['ok' => false, 'error' => 'รูปแบบรหัสไม่ถูกต้อง'], 400); } $store = read_store(); $sweep = gc_expired_codes($store); $now = gmdate('c'); $found = -1; foreach ($store['accounts'] as $i => $a) { if (($a['linkCode'] ?? '') === $code && !code_is_expired($a['linkCodeLastUsedAt'] ?? null)) { $found = $i; break; } } if ($found < 0) { if ($sweep) { write_store($store); } json_response(['ok' => false, 'error' => 'ไม่พบรหัสนี้ หรือรหัสหมดอายุแล้ว'], 404); } $store['accounts'][$found]['linkCodeLastUsedAt'] = $now; $store['accounts'][$found]['updatedAt'] = $now; write_store($store); $payload = profile_payload($store['accounts'][$found]); $payload['ok'] = true; json_response($payload); } json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);