Skip to content

Commit dc95819

Browse files
committed
feat: ported all gamejoin apis to gamejoin v2
1 parent 77ea15e commit dc95819

14 files changed

Lines changed: 184 additions & 25 deletions

File tree

src/content/core/api.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { updateUserLocationIfChanged } from './utils/location.js';
1111
const activeRequests = new Map();
1212
let gameJoinErrorCount = 0;
1313
let lastGameJoinRequestTime = 0;
14+
const GAMEJOIN_TIMEOUT_MS = 2000;
1415

1516
const OAUTH_STORAGE_KEY = 'rovalra_oauth_verification';
1617
let cachedRovalraUserAgent = null;
@@ -72,6 +73,103 @@ function getRequestKey({
7273
return `${target}|${method.toUpperCase()}|${bodyStr}|${headersStr}`;
7374
}
7475

76+
function normalizeGameJoinEndpoint(endpoint) {
77+
if (typeof endpoint !== 'string') return endpoint;
78+
return endpoint.replace(/^\/v1\//, '/v2/');
79+
}
80+
81+
function isGameJoinTimeoutEnabled(endpoint) {
82+
if (typeof endpoint !== 'string') return true;
83+
return endpoint.split('?')[0] !== '/v2/join-game';
84+
}
85+
86+
function createGameJoinFullResponse() {
87+
return new Response(
88+
JSON.stringify({
89+
status: 22,
90+
message: 'Server full',
91+
rovalraTimedOut: true,
92+
}),
93+
{
94+
status: 200,
95+
headers: { 'Content-Type': 'application/json' },
96+
},
97+
);
98+
}
99+
100+
function parseServerSentEvents(text) {
101+
const events = [];
102+
const blocks = String(text || '').split(/\r?\n\r?\n/);
103+
104+
for (const block of blocks) {
105+
if (!block.trim()) continue;
106+
107+
const event = { event: 'message', data: '' };
108+
const dataLines = [];
109+
110+
for (const rawLine of block.split(/\r?\n/)) {
111+
if (!rawLine || rawLine.startsWith(':')) continue;
112+
113+
const separatorIndex = rawLine.indexOf(':');
114+
let field = rawLine;
115+
let value = '';
116+
117+
if (separatorIndex !== -1) {
118+
field = rawLine.slice(0, separatorIndex);
119+
value = rawLine.slice(separatorIndex + 1);
120+
if (value.charCodeAt(0) === 32) value = value.slice(1);
121+
}
122+
123+
if (field === 'event') event.event = value;
124+
else if (field === 'id') event.id = value;
125+
else if (field === 'retry') event.retry = value;
126+
else if (field === 'data') dataLines.push(value);
127+
}
128+
129+
event.data = dataLines.join('\n');
130+
events.push(event);
131+
}
132+
133+
return events;
134+
}
135+
136+
async function normalizeGameJoinResponse(response) {
137+
const contentType = (
138+
response.headers.get('Content-Type') || ''
139+
).toLowerCase();
140+
if (!contentType.includes('text/event-stream')) return response;
141+
142+
const text = await response.text();
143+
const events = parseServerSentEvents(text);
144+
const readyEvent =
145+
events.find((event) => event.event === 'ResponseReady') ||
146+
events.find((event) => event.data?.trim());
147+
148+
if (!readyEvent?.data) {
149+
return new Response(JSON.stringify({ status: 0 }), {
150+
status: response.status,
151+
statusText: response.statusText,
152+
headers: { 'Content-Type': 'application/json' },
153+
});
154+
}
155+
156+
try {
157+
JSON.parse(readyEvent.data);
158+
} catch (e) {
159+
return new Response(JSON.stringify({ status: 0 }), {
160+
status: response.status,
161+
statusText: response.statusText,
162+
headers: { 'Content-Type': 'application/json' },
163+
});
164+
}
165+
166+
return new Response(readyEvent.data, {
167+
status: response.status,
168+
statusText: response.statusText,
169+
headers: { 'Content-Type': 'application/json' },
170+
});
171+
}
172+
75173
function checkSimulatedDowntime() {
76174
return new Promise((resolve) => {
77175
if (
@@ -141,6 +239,13 @@ export function resetGameJoinErrorCount() {
141239
}
142240

143241
export async function callRobloxApi(options) {
242+
if (options.subdomain === 'gamejoin') {
243+
options = {
244+
...options,
245+
endpoint: normalizeGameJoinEndpoint(options.endpoint),
246+
};
247+
}
248+
144249
if (
145250
options.subdomain === 'gamejoin' &&
146251
(options.method || 'GET').toUpperCase() === 'POST'
@@ -541,10 +646,52 @@ export async function callRobloxApi(options) {
541646
}
542647
}
543648

649+
let timeoutId = null;
650+
let abortSignalCleanup = null;
651+
let didGameJoinTimeout = false;
652+
const shouldUseGameJoinTimeout =
653+
subdomain === 'gamejoin' && isGameJoinTimeoutEnabled(endpoint);
654+
655+
if (shouldUseGameJoinTimeout) {
656+
const controller = new AbortController();
657+
timeoutId = setTimeout(() => {
658+
didGameJoinTimeout = true;
659+
controller.abort();
660+
}, GAMEJOIN_TIMEOUT_MS);
661+
662+
if (signal) {
663+
if (signal.aborted) {
664+
controller.abort();
665+
} else {
666+
abortSignalCleanup = () => controller.abort();
667+
signal.addEventListener('abort', abortSignalCleanup, {
668+
once: true,
669+
});
670+
}
671+
}
672+
673+
fetchOptions.signal = controller.signal;
674+
}
675+
676+
const cleanupGameJoinTimeout = () => {
677+
if (timeoutId) {
678+
clearTimeout(timeoutId);
679+
timeoutId = null;
680+
}
681+
if (signal && abortSignalCleanup) {
682+
signal.removeEventListener('abort', abortSignalCleanup);
683+
abortSignalCleanup = null;
684+
}
685+
};
686+
544687
let response;
545688
try {
546689
response = await fetch(fullUrl, fetchOptions);
547690
} catch (error) {
691+
cleanupGameJoinTimeout();
692+
if (didGameJoinTimeout) {
693+
return createGameJoinFullResponse();
694+
}
548695
if (error.name === 'AbortError' || (signal && signal.aborted)) {
549696
return new Response(null, {
550697
status: 499,
@@ -563,6 +710,10 @@ export async function callRobloxApi(options) {
563710
try {
564711
response = await fetch(fullUrl, fetchOptions);
565712
} catch (error) {
713+
cleanupGameJoinTimeout();
714+
if (didGameJoinTimeout) {
715+
return createGameJoinFullResponse();
716+
}
566717
if (
567718
error.name === 'AbortError' ||
568719
(signal && signal.aborted)
@@ -577,6 +728,19 @@ export async function callRobloxApi(options) {
577728
}
578729
}
579730

731+
try {
732+
if (subdomain === 'gamejoin') {
733+
response = await normalizeGameJoinResponse(response);
734+
}
735+
} catch (error) {
736+
if (didGameJoinTimeout) {
737+
return createGameJoinFullResponse();
738+
}
739+
throw error;
740+
} finally {
741+
cleanupGameJoinTimeout();
742+
}
743+
580744
if (!response.ok) {
581745
console.error(
582746
`RoValra API: Request to ${fullUrl} failed with status ${response.status}.`,

src/content/core/apis/serverApi.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ export async function fetchServerRegion(placeId, serverId, options = {}) {
182182
}
183183

184184
const endpoint = options.isPrivate
185-
? '/v1/join-private-game'
186-
: '/v1/join-game-instance';
185+
? '/v2/join-private-game'
186+
: '/v2/join-game-instance';
187187
const body = options.isPrivate
188188
? {
189189
placeId: parseInt(placeId),
@@ -211,7 +211,6 @@ export async function fetchServerRegion(placeId, serverId, options = {}) {
211211
status: info.status,
212212
message: info.message,
213213
joinScript: info.joinScript,
214-
queuePosition: info.queuePosition,
215214
};
216215

217216
if (info.joinScript) {
@@ -303,7 +302,7 @@ export async function isServerActive(placeId, gameId) {
303302
try {
304303
const response = await callRobloxApi({
305304
subdomain: 'gamejoin',
306-
endpoint: '/v1/join-game-instance',
305+
endpoint: '/v2/join-game-instance',
307306
method: 'POST',
308307
body: {
309308
placeId: parseInt(placeId, 10),

src/content/core/games/servers/filters/regionfilters.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ async function getAndCacheServerRegion(server, placeId) {
688688
try {
689689
const res = await callRobloxApiJson({
690690
subdomain: 'gamejoin',
691-
endpoint: '/v1/join-game-instance',
691+
endpoint: '/v2/join-game-instance',
692692
method: 'POST',
693693
body: {
694694
placeId: parseInt(placeId, 10),

src/content/core/games/servers/serverdetails.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -804,10 +804,7 @@ export async function fetchAndDisplayRegion(
804804
if (info.status === 22) {
805805
if (isFullServerIndicatorsEnabled) {
806806
if (joinBtn) {
807-
joinBtn.textContent =
808-
info.queuePosition > 0
809-
? `Join (${info.queuePosition} In Queue)`
810-
: 'Server Full';
807+
joinBtn.textContent = 'Server Full';
811808
joinBtn.classList.replace(
812809
'btn-primary-md',
813810
'btn-secondary-md',

src/content/core/preferredregion.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async function isServerActive(placeId, gameId) {
6161
try {
6262
const response = await callRobloxApi({
6363
subdomain: 'gamejoin',
64-
endpoint: '/v1/join-game-instance',
64+
endpoint: '/v2/join-game-instance',
6565
method: 'POST',
6666
body: {
6767
placeId: parseInt(placeId, 10),

src/content/core/regionFinder/ClosestServer.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export async function findClosestServerViaApi(
104104
try {
105105
const joinRes = await callRobloxApi({
106106
subdomain: 'gamejoin',
107-
endpoint: '/v1/join-game-instance',
107+
endpoint: '/v2/join-game-instance',
108108
method: 'POST',
109109
body: {
110110
placeId: parseInt(placeId, 10),
@@ -143,7 +143,7 @@ export async function fetchServerRegion(server, placeId) {
143143
try {
144144
const res = await callRobloxApi({
145145
subdomain: 'gamejoin',
146-
endpoint: '/v1/join-game-instance',
146+
endpoint: '/v2/join-game-instance',
147147
method: 'POST',
148148
body: {
149149
placeId: parseInt(placeId, 10),
@@ -443,7 +443,7 @@ async function attemptJoinServers(
443443
try {
444444
const res = await callRobloxApi({
445445
subdomain: 'gamejoin',
446-
endpoint: '/v1/join-game-instance',
446+
endpoint: '/v2/join-game-instance',
447447
method: 'POST',
448448
body: {
449449
placeId: parseInt(placeId, 10),

src/content/core/settings/settingConfig.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export const SETTINGS_CONFIG = {
504504
label: 'Full Server Indicators',
505505
description: [
506506
'This adds indicators when a server is full',
507-
"Like the queue size, and text telling you the server is full if we don't have region data.",
507+
"Like text that tells you the server is full if we don't have region data.",
508508
],
509509
type: 'checkbox',
510510
default: true,

src/content/core/utils/location.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async function probeServerForLocation(placeId, serverId) {
9797
try {
9898
const res = await callRobloxApi({
9999
subdomain: 'gamejoin',
100-
endpoint: '/v1/join-game-instance',
100+
endpoint: '/v2/join-game-instance',
101101
method: 'POST',
102102
body: {
103103
placeId: parseInt(placeId, 10),
@@ -142,4 +142,4 @@ export async function updateUserLocationIfChanged(freshCoords) {
142142
} catch (e) {
143143
console.error("Location Util: Error in update", e);
144144
}
145-
}
145+
}

src/content/features/games/revertlogo.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ function initializeJoinDialogEnhancer() {
647647
if (isGenericJoin) {
648648
const response = await callRobloxApi({
649649
subdomain: 'gamejoin',
650-
endpoint: '/v1/join-game',
650+
endpoint: '/v2/join-game',
651651
method: 'POST',
652652
body: apiBody,
653653
});
@@ -670,7 +670,7 @@ function initializeJoinDialogEnhancer() {
670670
urlParams.get('userId');
671671
const response = await callRobloxApi({
672672
subdomain: 'gamejoin',
673-
endpoint: '/v1/play-with-user',
673+
endpoint: '/v2/play-with-user',
674674
method: 'POST',
675675
body: {
676676
userIdToFollow: parseInt(

src/content/features/games/serverlist/recentservers.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async function checkServerIsActive(placeId, gameId) {
164164
try {
165165
const info = await callRobloxApiJson({
166166
subdomain: 'gamejoin',
167-
endpoint: '/v1/join-game-instance',
167+
endpoint: '/v2/join-game-instance',
168168
method: 'POST',
169169
body: {
170170
placeId: parseInt(placeId, 10),
@@ -177,8 +177,7 @@ async function checkServerIsActive(placeId, gameId) {
177177

178178
if (
179179
info.joinScript ||
180-
info.status === 2 ||
181-
(info.queuePosition && info.queuePosition > 0)
180+
info.status === 2
182181
) {
183182
return true;
184183
}

0 commit comments

Comments
 (0)