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
4 changes: 3 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@
"autoBest": "Auto (Best)",
"searchPlaceholder": "Search regions...",
"refreshPing": "Refresh ping",
"noRegionsMatch": "No regions match \"{{query}}\""
"noRegionsMatch": "No regions match \"{{query}}\"",
"autoRejoin": "Auto Rejoin",
"autoRejoinHint": "Automatically picks the best server using your ping and queue, and automatically rejoins the game when your session ends."
},
"persistentStorage": {
"title": "Persistent Storage",
Expand Down
2 changes: 2 additions & 0 deletions opennow-stable/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,8 @@ function registerIpcHandlers(): void {
},
);



// Logs export IPC handler
ipcMain.handle(
IPC_CHANNELS.LOGS_EXPORT,
Expand Down
30 changes: 21 additions & 9 deletions opennow-stable/src/main/services/printedWaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
PrintedWasteServerMapping,
} from "@shared/gfn";
import { fetchWithTimeout, withTimeout } from "./requestTimeout";
import { fetchWithOptionalProxy } from "../gfn/proxyFetch";

const PRINTEDWASTE_TIMEOUT_MS = 7000;
const PRINTEDWASTE_QUEUE_URL = "https://api.printedwaste.com/gfn/queue/";
Expand All @@ -11,18 +12,29 @@ const PRINTEDWASTE_SERVER_MAPPING_URL =

export async function fetchPrintedWasteQueue(
appVersion: string,
proxyUrl?: string,
): Promise<PrintedWasteQueueData> {
Comment thread
liwa-dev marked this conversation as resolved.
const response = await fetchWithTimeout(
PRINTEDWASTE_QUEUE_URL,
{
headers: {
"User-Agent": `opennow/${appVersion}`,
Accept: "application/json",
},
},
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(new Error("PrintedWaste queue request timed out after 7000ms")),
PRINTEDWASTE_TIMEOUT_MS,
"PrintedWaste queue request",
);
let response: Response;
try {
response = await fetchWithOptionalProxy(
PRINTEDWASTE_QUEUE_URL,
{
headers: {
"User-Agent": `opennow/${appVersion}`,
Accept: "application/json",
},
signal: controller.signal,
},
proxyUrl,
);
} finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
throw new Error(`PrintedWaste API returned HTTP ${response.status}`);
}
Comment thread
liwa-dev marked this conversation as resolved.
Expand Down
3 changes: 3 additions & 0 deletions opennow-stable/src/main/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export interface Settings {
autoCheckForUpdates: boolean;
/** When true, pressing Escape will exit fullscreen; when false Escape is sent to the game while pointer-locked */
allowEscapeToExitFullscreen?: boolean;
/** Automatically select a server and rejoin after a free-tier session ends. */
enableFastQueueJoin: boolean;
/** Last version for which the release highlights modal was acknowledged (empty = never) */
lastSeenReleaseHighlightsVersion: string;
/** Client-side GPU post-processing shaders applied to the stream (web client mode) */
Expand Down Expand Up @@ -259,6 +261,7 @@ const DEFAULT_SETTINGS: Settings = {
discordRichPresence: false,
autoCheckForUpdates: true,
allowEscapeToExitFullscreen: false,
enableFastQueueJoin: false,
lastSeenReleaseHighlightsVersion: "",
videoShader: { ...DEFAULT_VIDEO_SHADER_SETTINGS },
};
Expand Down
225 changes: 220 additions & 5 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import {
sortLibraryGames,
} from "./lib/gameCatalog";
import { chooseAccountLinked, getEpicOwnershipLaunchError, resolveInstallToPlayStorageRegionUrl } from "./lib/launchOwnership";
import { hasAnyEligiblePrintedWasteZone, isAllianceStreamingBaseUrl } from "./lib/printedWaste";
import { hasAnyEligiblePrintedWasteZone, isAllianceStreamingBaseUrl, pickBestPrintedWasteZone, constructPrintedWasteZoneUrl, isStandardPrintedWasteZone, isPrintedWasteZoneFresh } from "./lib/printedWaste";
import {
mergePolledSessionState,
normalizeMembershipTier,
Expand Down Expand Up @@ -548,6 +548,7 @@ export function App(): JSX.Element {
autoCheckForUpdates: true,
lastSeenReleaseHighlightsVersion: "",
videoShader: { ...DEFAULT_VIDEO_SHADER_SETTINGS },
enableFastQueueJoin: false,
});
const [settingsLoaded, setSettingsLoaded] = useState(false);
const [releaseHighlightsPayload, setReleaseHighlightsPayload] = useState<ReleaseHighlightsPayload | null>(null);
Expand Down Expand Up @@ -637,6 +638,13 @@ export function App(): JSX.Element {
streamingGameRef.current = streamingGame;
}, [streamingGame]);

const handlePlayGameRef = useRef<((game: GameInfo, options?: { bypassGuards?: boolean; streamingBaseUrl?: string; variantId?: string }) => Promise<void>) | null>(null);
const prePingSmartUrlRef = useRef<string | null>(null);
const prePingInFlightRef = useRef<boolean>(false);
const prePingPromiseRef = useRef<Promise<string | null> | null>(null);
const consecutiveAutoRejoinAttemptsRef = useRef<number>(0);
const autoRejoinSessionIdRef = useRef<string | null>(null);

const resetStatsOverlayToPreference = useCallback((): void => {
setShowStatsOverlay(settings.showStatsOnLaunch);
}, [settings.showStatsOnLaunch]);
Expand Down Expand Up @@ -773,6 +781,9 @@ export function App(): JSX.Element {
window.clearTimeout(stableRecoveryResetTimerRef.current);
stableRecoveryResetTimerRef.current = null;
}
prePingSmartUrlRef.current = null;
prePingInFlightRef.current = false;
prePingPromiseRef.current = null;
if (remoteIceGraceTimerRef.current !== null) {
window.clearTimeout(remoteIceGraceTimerRef.current);
remoteIceGraceTimerRef.current = null;
Expand Down Expand Up @@ -1673,6 +1684,81 @@ export function App(): JSX.Element {
previousFreeTierRemainingSecondsRef.current = freeTierSessionRemainingSeconds;
}, [freeTierSessionRemainingSeconds]);

// Auto-Rejoin: pre-fetch best server URL using the same weighted algorithm as QueueServerSelectModal
// (75% ping weight + 25% queue weight). Runs when ~15s remain in the session.
useEffect(() => {
if (!settings.enableFastQueueJoin) return;
const activeProvider = authSession?.provider ?? selectedProvider;
const isNvidiaAccount = isNvidiaProvider(activeProvider);
const isAllianceServer = isAllianceStreamingBaseUrl(effectiveStreamingBaseUrl);
if (!isNvidiaAccount || isAllianceServer) return;

if (
sessionTimeRemainingSeconds === null ||
sessionTimeRemainingSeconds > 15 ||
prePingSmartUrlRef.current !== null ||
prePingInFlightRef.current
) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const targetSessionId = sessionRef.current?.sessionId;
prePingInFlightRef.current = true;
console.log("[AutoRejoin] Fetching queue + pinging servers at", sessionTimeRemainingSeconds, "s remaining");

const prefetchPromise = (async (): Promise<string | null> => {
try {
const [queueData, serverMapping] = await Promise.all([
window.openNow.fetchPrintedWasteQueue(),
window.openNow.fetchPrintedWasteServerMapping().catch(() => null),
]);

const nukedIds = new Set<string>();
if (serverMapping) {
for (const [zoneId, meta] of Object.entries(serverMapping)) {
if (meta.nuked) nukedIds.add(zoneId);
}
}

const candidates = Object.entries(queueData)
.filter(([zoneId, zone]) => isStandardPrintedWasteZone(zoneId) && isPrintedWasteZoneFresh(zone) && !nukedIds.has(zoneId))
.map(([zoneId]) => ({
zoneId,
routingUrl: constructPrintedWasteZoneUrl(zoneId),
}));

if (candidates.length === 0) {
console.warn("[AutoRejoin] No valid candidate zones found");
return null;
}

const regionsToTest = candidates.map((c) => ({ name: c.zoneId, url: c.routingUrl }));
const pingResults = await window.openNow.pingRegions(regionsToTest);
const pingMap = new Map<string, number | null>(pingResults.map((r) => [r.url, r.pingMs]));

const best = pickBestPrintedWasteZone(queueData, serverMapping, pingMap);
if (!best) {
console.warn("[AutoRejoin] No best server found by the picker algorithm");
return null;
}

if (!prePingInFlightRef.current || (targetSessionId && sessionRef.current?.sessionId !== targetSessionId)) {
console.log("[AutoRejoin] Pre-fetch completed after session reset or end — ignoring stale result");
return null;
}

prePingSmartUrlRef.current = best.routingUrl;
Comment thread
liwa-dev marked this conversation as resolved.
console.log(`[AutoRejoin] Best server: ${best.zoneId} (ping: ${best.pingMs}ms, queue: ${best.queuePosition})`);
return best.routingUrl;
} catch (err) {
console.error("[AutoRejoin] Pre-fetch failed:", err);
return null;
} finally {
prePingInFlightRef.current = false;
}
})();

prePingPromiseRef.current = prefetchPromise;
}, [authSession, effectiveStreamingBaseUrl, selectedProvider, sessionTimeRemainingSeconds, settings.enableFastQueueJoin]);

useEffect(() => {
if (!localSessionTimerWarning) return;

Expand Down Expand Up @@ -2956,6 +3042,118 @@ export function App(): JSX.Element {
resolveSubscriptionInfoForLaunch,
]);

const startAutoRejoin = useCallback((reason: string, testGameOverride?: GameInfo | null): boolean => {
const game = testGameOverride !== undefined ? testGameOverride : streamingGameRef.current;
const playGame = handlePlayGameRef.current;
const cachedUrl = prePingSmartUrlRef.current;

console.log("[AutoRejoin] *** startAutoRejoin called ***", {
reason,
enabled: settings.enableFastQueueJoin,
cachedUrl,
prefetchInFlight: prePingInFlightRef.current,
game: game?.title ?? null,
hasLaunchHandler: Boolean(playGame),
consecutiveAttempts: consecutiveAutoRejoinAttemptsRef.current,
});

if (!settings.enableFastQueueJoin) {
console.log("[AutoRejoin] Skipping: enableFastQueueJoin is OFF");
return false;
}
Comment thread
liwa-dev marked this conversation as resolved.

const currentSessionId = sessionRef.current?.sessionId;
if (currentSessionId && autoRejoinSessionIdRef.current === currentSessionId) {
console.log("[AutoRejoin] Already handling auto-rejoin for session:", currentSessionId);
return true;
}

if (consecutiveAutoRejoinAttemptsRef.current >= 3) {
console.warn("[AutoRejoin] Max consecutive auto-rejoin attempts reached (3), stopping to prevent infinite loop.");
consecutiveAutoRejoinAttemptsRef.current = 0;
return false;
}

autoRejoinSessionIdRef.current = currentSessionId ?? null;
consecutiveAutoRejoinAttemptsRef.current += 1;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!game) {
console.error("[AutoRejoin] Skipping: streamingGameRef is null (no active game)");
autoRejoinSessionIdRef.current = null;
return false;
}
if (!playGame) {
console.error("[AutoRejoin] Skipping: handlePlayGameRef is null (ref not synced yet)");
autoRejoinSessionIdRef.current = null;
return false;
}

const activeVariantId = sessionRef.current?.appId ?? (game ? variantByGameId[game.id] : undefined);

const launch = (streamingBaseUrl?: string): void => {
console.log("[AutoRejoin] ✅ Launching rejoin to:", streamingBaseUrl ?? "default region");
prePingSmartUrlRef.current = null;
prePingInFlightRef.current = false;
prePingPromiseRef.current = null;
resetLaunchRuntime();
void refreshNavbarActiveSession();
launchInFlightRef.current = false;
window.setTimeout(() => {
void playGame(game, { bypassGuards: true, streamingBaseUrl, variantId: activeVariantId }).catch((error) => {
Comment thread
liwa-dev marked this conversation as resolved.
console.error("[AutoRejoin] Rejoin launch failed:", error);
});
}, 500);
};

const activeProvider = authSession?.provider ?? selectedProvider;
const isNvidiaAccount = isNvidiaProvider(activeProvider);
const isAllianceServer = isAllianceStreamingBaseUrl(effectiveStreamingBaseUrl);

if (!isNvidiaAccount || isAllianceServer) {
console.log("[AutoRejoin] Non-NVIDIA or Alliance provider detected — bypassing PrintedWaste server picker and rejoining via provider routing");
launch(undefined);
return true;
}

if (cachedUrl) {
console.log("[AutoRejoin] Using pre-fetched URL:", cachedUrl);
launch(cachedUrl);
} else if (prePingInFlightRef.current && prePingPromiseRef.current) {
console.log("[AutoRejoin] Pre-fetch is still in flight — awaiting in-flight result...");
void prePingPromiseRef.current.then((inFlightUrl) => {
launch(inFlightUrl ?? undefined);
});
} else {
Comment thread
liwa-dev marked this conversation as resolved.
console.warn("[AutoRejoin] No cached or in-flight URL available — attempting live server pick fallback...");
void (async () => {
try {
const [queueData, serverMapping] = await Promise.all([
window.openNow.fetchPrintedWasteQueue(),
window.openNow.fetchPrintedWasteServerMapping().catch(() => null),
]);
const candidates = Object.entries(queueData)
.filter(([zoneId, zone]) => isStandardPrintedWasteZone(zoneId) && isPrintedWasteZoneFresh(zone) && serverMapping?.[zoneId]?.nuked !== true)
.map(([zoneId]) => ({ zoneId, routingUrl: constructPrintedWasteZoneUrl(zoneId) }));
if (candidates.length > 0) {
const regionsToTest = candidates.map((c) => ({ name: c.zoneId, url: c.routingUrl }));
const pingResults = await window.openNow.pingRegions(regionsToTest).catch(() => []);
const pingMap = new Map<string, number | null>(pingResults.map((r) => [r.url, r.pingMs]));
const best = pickBestPrintedWasteZone(queueData, serverMapping, pingMap);
if (best) {
launch(best.routingUrl);
return;
}
}
} catch (err) {
console.warn("[AutoRejoin] Live server pick fallback failed:", err);
}
// Fallback: launch using default region routing
launch(undefined);
})();
}
return true;
}, [authSession, effectiveStreamingBaseUrl, refreshNavbarActiveSession, resetLaunchRuntime, selectedProvider, settings.enableFastQueueJoin, variantByGameId]);

const handleExpectedNativeSessionClose = useCallback((reason: string): void => {
console.log("[Recovery] Treating signaling close as ended session:", reason);
const activeGameId = streamingGameRef.current?.id;
Expand All @@ -2966,9 +3164,12 @@ export function App(): JSX.Element {
clientRef.current?.dispose();
clientRef.current = null;
launchInFlightRef.current = false;
resetLaunchRuntime();
void refreshNavbarActiveSession();
}, [endPlaytimeSession, markExplicitSignalingShutdown, refreshNavbarActiveSession, resetLaunchRuntime]);
const rejoining = startAutoRejoin(reason);
if (!rejoining) {
resetLaunchRuntime();
void refreshNavbarActiveSession();
}
}, [endPlaytimeSession, markExplicitSignalingShutdown, refreshNavbarActiveSession, resetLaunchRuntime, startAutoRejoin]);

// Signaling events
useEffect(() => {
Expand Down Expand Up @@ -3079,6 +3280,9 @@ export function App(): JSX.Element {
setStreamStatus("streaming");
markDiscordStreamStarted();
scheduleStableRecoveryReset(activeSession.sessionId);
// Session confirmed stable — reset auto-rejoin counter so it doesn't
// block future rejoins after successful sessions.
consecutiveAutoRejoinAttemptsRef.current = 0;
};

const unsubscribe = window.openNow.onSignalingEvent(async (event: MainToRendererSignalingEvent) => {
Expand Down Expand Up @@ -3148,6 +3352,9 @@ export function App(): JSX.Element {
setStreamStatus("streaming");
markDiscordStreamStarted();
scheduleStableRecoveryReset(activeSession.sessionId);
// Session confirmed stable — reset auto-rejoin counter so it doesn't
// block future rejoins after successful sessions.
consecutiveAutoRejoinAttemptsRef.current = 0;
console.log(
"[Stream] Offer applied; use [WebRTC] logs for ICE/video dimensions. signalingServer=%s media=%s",
activeSession.signalingServer,
Expand Down Expand Up @@ -3356,10 +3563,13 @@ export function App(): JSX.Element {
});

return () => unsubscribe();
}, [attemptSessionRecovery, diagnosticsStore, handleExpectedNativeSessionClose, markDiscordStreamStarted, nativeInputBridgeReady, refreshNavbarActiveSession, resetLaunchRuntime, scheduleStableRecoveryReset, settings, streamMicLevel, streamVolume, t]);
}, [attemptSessionRecovery, diagnosticsStore, handleExpectedNativeSessionClose, markDiscordStreamStarted, nativeInputBridgeReady, refreshNavbarActiveSession, resetLaunchRuntime, scheduleStableRecoveryReset, settings, startAutoRejoin, streamMicLevel, streamVolume, t]);

// Play game handler
const handlePlayGame = useCallback(async (game: GameInfo, options?: { bypassGuards?: boolean; streamingBaseUrl?: string; variantId?: string }) => {
if (!options?.bypassGuards) {
consecutiveAutoRejoinAttemptsRef.current = 0;
}
if (!selectedProvider) return;

console.log("handlePlayGame entry", {
Expand Down Expand Up @@ -3679,6 +3889,10 @@ export function App(): JSX.Element {
warmNativeStreamerForLaunch,
]);

useEffect(() => {
handlePlayGameRef.current = handlePlayGame;
}, [handlePlayGame]);

useEffect(() => {
const request = pendingDirectLaunchRequest;
if (!request || handledDirectLaunchIdsRef.current.has(request.id)) return;
Expand Down Expand Up @@ -4178,6 +4392,7 @@ export function App(): JSX.Element {
}
}, [endPlaytimeSession, markExplicitSignalingShutdown, refreshNavbarActiveSession, resetLaunchRuntime, resolveExitPrompt, stopSessionByTarget, streamingGame]);


const handleDismissLaunchError = useCallback(async () => {
markExplicitSignalingShutdown();
await disconnectSignalingControlled();
Expand Down
Loading