MaxproGlitcher

Région Finder code Working on it

Aug 12th, 2026
60
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.77 KB | None | 0 0
  1. import { callRobloxApi } from '../api.js';
  2. import { getUserLocation } from '../utils/location.js';
  3. import { launchGame } from '../utils/launcher.js';
  4. import { showReviewPopup } from '../review/review.js';
  5. import { hideLoadingOverlay } from '../ui/startModal/gamelaunchmodal.js';
  6. import { REGIONS, serverIpMap, getStateCodeFromRegion } from '../regions.js';
  7.  
  8. export const FINDER_CONFIG = {
  9. logScores: true,
  10. maxServersPerRegion: 10,
  11. checkGameActivity: true,
  12. maxManualScanPages: 3,
  13. };
  14.  
  15. export const dataPromise = Promise.resolve();
  16.  
  17. export function getRegionName(regionId) {
  18. if (regionId === 'AUTO') return 'Automatic';
  19. const region = REGIONS[regionId];
  20. if (region) {
  21. const parts = [];
  22. if (region.city) parts.push(region.city);
  23. if (region.region) parts.push(region.region);
  24.  
  25. let country = region.country;
  26. if (country === 'US') country = 'USA';
  27. if (country) parts.push(country);
  28.  
  29. return parts.join(', ');
  30. }
  31. return String(regionId);
  32. }
  33.  
  34. export function getDistance(lat1, lon1, lat2, lon2) {
  35. const R = 6371;
  36. const dLat = deg2rad(lat2 - lat1);
  37. const dLon = deg2rad(lon2 - lon1);
  38. const a =
  39. Math.sin(dLat / 2) * Math.sin(dLat / 2) +
  40. Math.cos(deg2rad(lat1)) *
  41. Math.cos(deg2rad(lat2)) *
  42. Math.sin(dLon / 2) *
  43. Math.sin(dLon / 2);
  44. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  45. return R * c;
  46. }
  47.  
  48. function deg2rad(deg) {
  49. return deg * (Math.PI / 180);
  50. }
  51.  
  52. export function getRegionDistance(code1, code2) {
  53. const r1 = REGIONS[code1];
  54. const r2 = REGIONS[code2];
  55. if (!r1 || !r2) return Infinity;
  56. return getDistance(r1.latitude, r1.longitude, r2.latitude, r2.longitude);
  57. }
  58.  
  59. export async function findClosestServerViaApi(
  60. placeId,
  61. originRegionId,
  62. userRequestedStop,
  63. ) {
  64. const origin = REGIONS[originRegionId];
  65. if (!origin) return null;
  66.  
  67. const regionsWithDistance = Object.entries(REGIONS)
  68. .filter(([id, r]) => id !== originRegionId && id !== 'AUTO')
  69. .map(([id, region]) => ({
  70. id,
  71. region,
  72. distance: getDistance(
  73. origin.latitude,
  74. origin.longitude,
  75. region.latitude,
  76. region.longitude,
  77. ),
  78. }));
  79.  
  80. regionsWithDistance.sort((a, b) => a.distance - b.distance);
  81.  
  82. const regionsToCheck = regionsWithDistance.slice(0, 10);
  83.  
  84. for (const { id, region } of regionsToCheck) {
  85. if (userRequestedStop) return null;
  86.  
  87. let url = `/v1/servers/region?place_id=${placeId}`;
  88. if (region.country)
  89. url += `&country=${encodeURIComponent(region.country)}`;
  90. if (region.city) url += `&city=${encodeURIComponent(region.city)}`;
  91. url += '&cursor=0';
  92.  
  93. try {
  94. const response = await callRobloxApi({
  95. isRovalraApi: true,
  96. endpoint: url,
  97. });
  98. if (response.ok) {
  99. const data = await response.json();
  100. if (data.servers && data.servers.length > 0) {
  101. for (const server of data.servers) {
  102. if (userRequestedStop) return null;
  103.  
  104. try {
  105. const joinRes = await callRobloxApi({
  106. subdomain: 'gamejoin',
  107. endpoint: '/v2/join-game-instance',
  108. method: 'POST',
  109. body: {
  110. placeId: parseInt(placeId, 10),
  111. gameId: server.server_id,
  112. gameJoinAttemptId: crypto.randomUUID(),
  113. },
  114. });
  115.  
  116. if (joinRes.ok) {
  117. const joinInfo = await joinRes.json();
  118. if (joinInfo.joinScript) {
  119. return {
  120. server: {
  121. id: server.server_id,
  122. playing: server.playing,
  123. maxPlayers:
  124. server.max_players ||
  125. server.maxPlayers,
  126. },
  127. regionCode: id,
  128. };
  129. }
  130. }
  131. } catch (e) {}
  132. }
  133. }
  134. }
  135. } catch (e) {
  136. console.warn('Fallback API search failed for region', region.id, e);
  137. }
  138. }
  139. return null;
  140. }
  141.  
  142. export async function fetchServerRegion(server, placeId) {
  143. try {
  144. const res = await callRobloxApi({
  145. subdomain: 'gamejoin',
  146. endpoint: '/v2/join-game-instance',
  147. method: 'POST',
  148. body: {
  149. placeId: parseInt(placeId, 10),
  150. gameId: server.id,
  151. gameJoinAttemptId: crypto.randomUUID(),
  152. },
  153. });
  154.  
  155. if (!res.ok) return null;
  156. const info = await res.json();
  157. if (!info.joinScript) return null;
  158.  
  159. const dataCenterId = info.joinScript.DataCenterId;
  160. if (dataCenterId && serverIpMap[dataCenterId]) {
  161. const loc = serverIpMap[dataCenterId];
  162. let regionCode = loc.country;
  163. if (loc.country === 'US' && loc.region && loc.city) {
  164. const stateCode = getStateCodeFromRegion(loc.region);
  165. const cityCode = loc.city.replace(/\s+/g, '').toUpperCase();
  166. regionCode = `US-${stateCode}-${cityCode}`;
  167. } else if (loc.country === 'US' && loc.region) {
  168. regionCode = `US-${getStateCodeFromRegion(loc.region)}`;
  169. } else if (loc.city) {
  170. regionCode = `${loc.country}-${loc.city.replace(/\s+/g, '').toUpperCase()}`;
  171. }
  172. return regionCode;
  173. }
  174. } catch (error) {}
  175. return null;
  176. }
  177.  
  178. export async function findServerViaRobloxLatencyApi(
  179. placeId,
  180. joinedServerIds,
  181. userRequestedStopCheck,
  182. ) {
  183. try {
  184. const response = await callRobloxApi({
  185. subdomain: 'games',
  186. endpoint: `/v2/games/${placeId}/servers/Public?cursor=&sortOrder=Desc&excludeFullGames=true&orderBy=BestLatency`,
  187. });
  188. if (!response.ok) return null;
  189.  
  190. const data = await response.json();
  191. const candidates = (data.data || []).filter((server) => {
  192. const serverId = server.id || server.server_id;
  193. return (
  194. serverId &&
  195. !joinedServerIds.has(serverId) &&
  196. Number(server.playing) < Number(server.maxPlayers)
  197. );
  198. });
  199. if (!candidates.length || userRequestedStopCheck?.()) return null;
  200.  
  201. for (const candidate of candidates.slice(0, 10)) {
  202. if (userRequestedStopCheck?.()) return null;
  203.  
  204. const serverId = candidate.id || candidate.server_id;
  205. try {
  206. const joinResponse = await callRobloxApi({
  207. subdomain: 'gamejoin',
  208. endpoint: '/v2/join-game-instance',
  209. method: 'POST',
  210. body: {
  211. placeId: parseInt(placeId, 10),
  212. gameId: serverId,
  213. gameJoinAttemptId: crypto.randomUUID(),
  214. },
  215. });
  216.  
  217. if (joinResponse.ok) {
  218. const joinInfo = await joinResponse.json();
  219. if (joinInfo.joinScript) {
  220. return {
  221. ...candidate,
  222. id: serverId,
  223. maxPlayers:
  224. candidate.maxPlayers || candidate.max_players,
  225. };
  226. }
  227. }
  228. } catch (error) {}
  229. }
  230.  
  231. return null;
  232. } catch (error) {
  233. console.warn('Roblox BestLatency server search failed:', error);
  234. return null;
  235. }
  236. }
  237.  
  238. export async function findServerViaRovalraApi(
  239. placeId,
  240. universeId,
  241. preferredRegionId,
  242. failedRegionNames,
  243. joinedServerIds,
  244. userRequestedStopCheck,
  245. ) {
  246. try {
  247. if (FINDER_CONFIG.checkGameActivity && universeId) {
  248. const isActive = await checkGameActivity(universeId);
  249. if (!isActive) return { joined: false };
  250. }
  251.  
  252. const rankedRegions = await getRankedRegions(
  253. placeId,
  254. preferredRegionId,
  255. );
  256. if (rankedRegions.length === 0) return { joined: false };
  257.  
  258. let apiSucceededAtLeastOnce = false;
  259.  
  260. for (const { region } of rankedRegions) {
  261. if (userRequestedStopCheck && userRequestedStopCheck())
  262. return { status: 'STOPPED' };
  263.  
  264. const servers = await fetchServersForRegion(placeId, region);
  265. if (servers && servers.length > 0) {
  266. apiSucceededAtLeastOnce = true;
  267.  
  268. const unjoinedServers = servers.filter(
  269. (s) => !joinedServerIds.has(s.server_id || s.id),
  270. );
  271.  
  272. if (unjoinedServers.length > 0) {
  273. if (!preferredRegionId || region.id === preferredRegionId) {
  274. if (
  275. await attemptJoinServers(
  276. placeId,
  277. unjoinedServers,
  278. joinedServerIds,
  279. userRequestedStopCheck,
  280. )
  281. ) {
  282. return { status: 'JOINED' };
  283. }
  284. } else {
  285. return {
  286. status: 'FOUND_FALLBACK',
  287. servers: unjoinedServers,
  288. regionCode: region.id,
  289. };
  290. }
  291. } else {
  292. failedRegionNames.add(getRegionName(region.id));
  293. }
  294. } else {
  295. failedRegionNames.add(getRegionName(region.id));
  296. }
  297. }
  298.  
  299. const bestPossibleRegion = rankedRegions[0].region;
  300.  
  301. const manualCandidate = await findBestManualServer(
  302. placeId,
  303. rankedRegions,
  304. Infinity,
  305. joinedServerIds,
  306. userRequestedStopCheck,
  307. );
  308.  
  309. if (manualCandidate) {
  310. return {
  311. status: 'FOUND_FALLBACK',
  312. servers: [manualCandidate.server],
  313. regionCode: manualCandidate.region.id,
  314. };
  315. }
  316.  
  317. if (apiSucceededAtLeastOnce) {
  318. return { status: 'NO_SERVERS' };
  319. }
  320.  
  321. return { status: 'API_ERROR' };
  322. } catch (error) {
  323. console.error('Rovalra Search Error', error);
  324. return { status: 'API_ERROR' };
  325. }
  326. }
  327.  
  328. async function checkGameActivity(universeId) {
  329. try {
  330. const response = await callRobloxApi({
  331. subdomain: 'games',
  332. endpoint: `/v1/games?universeIds=${universeId}`,
  333. });
  334. if (response.ok) {
  335. const data = await response.json();
  336. if (
  337. !data.data ||
  338. data.data.length === 0 ||
  339. data.data[0].playing === 0
  340. ) {
  341. return false;
  342. }
  343. }
  344. } catch (e) {}
  345. return true;
  346. }
  347.  
  348. async function getRankedRegions(placeId, preferredRegionId) {
  349. let ranked = [];
  350. const locationData = await getUserLocation(placeId);
  351.  
  352. const userLat = locationData?.userLat;
  353. const userLon = locationData?.userLon;
  354.  
  355. ranked = Object.entries(REGIONS).map(([id, region]) => {
  356. let distance = Infinity;
  357. if (userLat != null && userLon != null && region.latitude != null) {
  358. distance = getDistance(
  359. userLat,
  360. userLon,
  361. region.latitude,
  362. region.longitude,
  363. );
  364. }
  365. let score = Math.floor(distance / 10) + 1;
  366.  
  367. if (preferredRegionId && id === preferredRegionId) {
  368. score = 0;
  369. distance = 0;
  370. }
  371. return { region: { ...region, id }, score, distance };
  372. });
  373.  
  374. ranked.sort((a, b) => a.score - b.score);
  375.  
  376. if (preferredRegionId) {
  377. const idx = ranked.findIndex((r) => r.region.id === preferredRegionId);
  378. if (idx > 0) {
  379. const [item] = ranked.splice(idx, 1);
  380. ranked.unshift(item);
  381. }
  382. }
  383.  
  384. if (FINDER_CONFIG.logScores) {
  385. console.log(`[RoValra] Region Scores for Place ${placeId}:`);
  386. ranked.forEach((r) => {
  387. console.log(
  388. ` - ${getRegionName(r.region.id)}: Score ${r.score} (${Math.round(r.distance)} km)`,
  389. );
  390. });
  391. }
  392.  
  393. return ranked;
  394. }
  395.  
  396. async function fetchServersForRegion(placeId, region) {
  397. let url = `/v1/servers/region?place_id=${placeId}`;
  398. if (region.country) url += `&country=${encodeURIComponent(region.country)}`;
  399. if (region.city) url += `&city=${encodeURIComponent(region.city)}`;
  400. url += '&cursor=0';
  401.  
  402. try {
  403. const response = await callRobloxApi({
  404. isRovalraApi: true,
  405. endpoint: url,
  406. });
  407. if (!response.ok) return null;
  408. const data = await response.json();
  409. return data.servers || [];
  410. } catch (e) {
  411. return null;
  412. }
  413. }
  414.  
  415. async function findBestManualServer(
  416. placeId,
  417. rankedRegions,
  418. beatDistance,
  419. joinedServerIds,
  420. stopCheck,
  421. ) {
  422. let cursor = null;
  423. const regionDistanceMap = {};
  424. rankedRegions.forEach((r) => {
  425. regionDistanceMap[r.region.id] = r.distance;
  426. });
  427.  
  428. for (let i = 0; i < FINDER_CONFIG.maxManualScanPages; i++) {
  429. if (stopCheck && stopCheck()) return null;
  430.  
  431. const url = `/v1/games/${placeId}/servers/Public?excludeFullGames=true&limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`;
  432. try {
  433. const response = await callRobloxApi({
  434. subdomain: 'games',
  435. endpoint: url,
  436. });
  437. if (!response.ok) break;
  438.  
  439. const data = await response.json();
  440. const servers = data.data || [];
  441. if (servers.length === 0) break;
  442.  
  443. const chunkSize = 5;
  444. for (let j = 0; j < servers.length; j += chunkSize) {
  445. if (stopCheck && stopCheck()) return null;
  446. const chunk = servers.slice(j, j + chunkSize);
  447.  
  448. await Promise.all(
  449. chunk.map(async (server) => {
  450. if (joinedServerIds.has(server.id)) return;
  451. const regionId = await fetchServerRegion(
  452. server,
  453. placeId,
  454. );
  455. if (
  456. regionId &&
  457. regionDistanceMap[regionId] !== undefined
  458. ) {
  459. server._tempDistance = regionDistanceMap[regionId];
  460. server._tempRegionId = regionId;
  461. }
  462. }),
  463. );
  464. }
  465.  
  466. const bestInPage = servers
  467. .filter((s) => s._tempDistance !== undefined)
  468. .sort((a, b) => a._tempDistance - b._tempDistance)[0];
  469.  
  470. if (bestInPage && bestInPage._tempDistance < beatDistance) {
  471. return {
  472. server: bestInPage,
  473. region: REGIONS[bestInPage._tempRegionId],
  474. distance: bestInPage._tempDistance,
  475. source: 'MANUAL',
  476. };
  477. }
  478.  
  479. cursor = data.nextPageCursor;
  480. if (!cursor) break;
  481. } catch (e) {
  482. break;
  483. }
  484. }
  485. return null;
  486. }
  487.  
  488. async function attemptJoinServers(
  489. placeId,
  490. servers,
  491. joinedServerIds,
  492. stopCheck,
  493. ) {
  494. let attempts = 0;
  495.  
  496. for (const server of servers) {
  497. if (attempts >= FINDER_CONFIG.maxServersPerRegion) break;
  498. const serverId = server.server_id || server.id;
  499. if (joinedServerIds.has(serverId)) continue;
  500. if (stopCheck && stopCheck()) return false;
  501.  
  502. attempts++;
  503. try {
  504. const res = await callRobloxApi({
  505. subdomain: 'gamejoin',
  506. endpoint: '/v2/join-game-instance',
  507. method: 'POST',
  508. body: {
  509. placeId: parseInt(placeId, 10),
  510. gameId: serverId,
  511. gameJoinAttemptId: crypto.randomUUID(),
  512. },
  513. });
  514. if (res.ok) {
  515. const info = await res.json();
  516. if (info.joinScript) {
  517. joinedServerIds.add(serverId);
  518. hideLoadingOverlay(true);
  519. launchGame(placeId, serverId);
  520. callRobloxApi({
  521. subdomain: 'games',
  522. endpoint: `/v1/games/${placeId}/servers/Public?limit=100`,
  523. }).catch(() => {
  524. /*fire and forget*/
  525. });
  526. showReviewPopup('region_filters');
  527. return true;
  528. }
  529. }
  530. } catch (e) {}
  531. }
  532. return false;
  533. }
Advertisement
Add Comment
Please, Sign In to add comment