diff --git a/gulp/preloader/preloader.js b/gulp/preloader/preloader.js index 553cd9c1..7e2fe518 100644 --- a/gulp/preloader/preloader.js +++ b/gulp/preloader/preloader.js @@ -2,6 +2,20 @@ var loadTimeout = null; var callbackDone = false; + var searchString = window.location.search; + if (searchString.includes("steam_sso_auth_token=")) { + var pos = searchString.indexOf("steam_sso_auth_token"); + const authToken = searchString.substring(pos + 21, pos + 57); + try { + window.localStorage.setItem("steam_sso_auth_token", authToken); + window.location.replace(window.location.protocol + "//" + window.location.host); + } catch (ex) { + alert("Failed to login via Steam SSO: " + ex); + window.location.replace("https://shapez.io"); + } + return; + } + // Catch load errors function errorHandler(event, source, lineno, colno, error) { diff --git a/res/ui/steam_signin.png b/res/ui/steam_signin.png new file mode 100644 index 00000000..cd3120f8 Binary files /dev/null and b/res/ui/steam_signin.png differ diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index ef02501e..0d16163b 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -49,6 +49,45 @@ } } + .steamSso { + position: fixed; + @include S(top, 0px); + @include S(left, 10px); + background: rgba(88, 92, 102, 0.4); + @include SuperSmallText; + color: #fff; + @include S(max-width, 150px); + @include S(border-radius, $globalBorderRadius); + border-top-left-radius: 0; + border-top-right-radius: 0; + @include S(padding, 5px); + box-shadow: 0 D(5px) D(15px) rgba(#000, 0.1); + display: flex; + color: #000; + flex-direction: column; + + a.ssoSignIn { + background: #171a23 uiResource("steam_signin.png") center center / contain no-repeat; + @include S(width, 110px); + @include S(height, 19px); + display: inline-flex; + @include S(border-radius, $globalBorderRadius); + @include S(margin-top, 3px); + overflow: hidden; + text-indent: -999em; + &:hover { + opacity: 0.95; + } + } + + @include DarkThemeOverride { + color: #333539; + a { + color: #111; + } + } + } + .fullscreenBackgroundVideo { // display: none !important; z-index: -1; diff --git a/src/css/states/mods.scss b/src/css/states/mods.scss index ac082ec6..60912510 100644 --- a/src/css/states/mods.scss +++ b/src/css/states/mods.scss @@ -24,6 +24,9 @@ justify-content: center; height: 100%; flex-direction: column; + text-align: center; + max-width: 80%; + align-self: center; .steamLink { @include S(height, 50px); diff --git a/src/css/states/settings.scss b/src/css/states/settings.scss index 8435a418..98135909 100644 --- a/src/css/states/settings.scss +++ b/src/css/states/settings.scss @@ -181,10 +181,16 @@ pointer-events: all; display: flex; align-items: center; + z-index: 100; justify-content: center; background: rgba(#fff, 0.5); text-transform: uppercase; color: $colorRedBright; + @include S(border-radius, $globalBorderRadius); + + @include DarkThemeOverride { + background: rgba(#55585f, 0.95); + } } } diff --git a/src/js/core/restriction_manager.js b/src/js/core/restriction_manager.js index d775c06d..33783b58 100644 --- a/src/js/core/restriction_manager.js +++ b/src/js/core/restriction_manager.js @@ -3,6 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { ExplainedResult } from "./explained_result"; import { ReadWriteProxy } from "./read_write_proxy"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso"; export class RestrictionManager extends ReadWriteProxy { /** @@ -64,6 +65,10 @@ export class RestrictionManager extends ReadWriteProxy { return false; } + if (WEB_STEAM_SSO_AUTHENTICATED) { + return false; + } + if (G_IS_DEV) { return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0; } diff --git a/src/js/core/steam_sso.js b/src/js/core/steam_sso.js new file mode 100644 index 00000000..f04fb3bc --- /dev/null +++ b/src/js/core/steam_sso.js @@ -0,0 +1,81 @@ +import { T } from "../translations"; +import { openStandaloneLink } from "./config"; + +export let WEB_STEAM_SSO_AUTHENTICATED = false; + +export async function authorizeViaSSOToken(app, dialogs) { + if (G_IS_STANDALONE) { + return; + } + + if (window.location.search.includes("sso_logout_silent")) { + window.localStorage.setItem("steam_sso_auth_token", ""); + window.location.replace("/"); + return new Promise(() => null); + } + + if (window.location.search.includes("sso_logout")) { + const { ok } = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc); + window.localStorage.setItem("steam_sso_auth_token", ""); + ok.add(() => window.location.replace("/")); + return new Promise(() => null); + } + + if (window.location.search.includes("steam_sso_no_ownership")) { + const { ok, getStandalone } = dialogs.showWarning( + T.dialogs.steamSsoNoOwnership.title, + T.dialogs.steamSsoNoOwnership.desc, + ["ok", "getStandalone:good"] + ); + window.localStorage.setItem("steam_sso_auth_token", ""); + getStandalone.add(() => { + openStandaloneLink(app, "sso_ownership"); + window.location.replace("/"); + }); + ok.add(() => window.location.replace("/")); + return new Promise(() => null); + } + + const token = window.localStorage.getItem("steam_sso_auth_token"); + if (!token) { + return Promise.resolve(); + } + + const apiUrl = app.clientApi.getEndpoint(); + console.warn("Authorizing via token:", token); + + const verify = async () => { + const token = window.localStorage.getItem("steam_sso_auth_token"); + if (!token) { + window.location.replace("?sso_logout"); + return; + } + + const response = await Promise.race([ + fetch(apiUrl + "/v1/sso/refresh", { + method: "POST", + body: token, + headers: { + "x-api-key": "d5c54aaa491f200709afff082c153ef2", + }, + }), + new Promise((resolve, reject) => { + setTimeout(() => reject("timeout exceeded"), 20000); + }), + ]); + const responseText = await response.json(); + if (!responseText.token) { + console.warn("Failed to register"); + window.localStorage.setItem("steam_sso_auth_token", ""); + window.location.replace("?sso_logout"); + return; + } + + window.localStorage.setItem("steam_sso_auth_token", responseText.token); + app.clientApi.token = responseText.token; + WEB_STEAM_SSO_AUTHENTICATED = true; + }; + + await verify(); + setInterval(verify, 120000); +} diff --git a/src/js/core/utils.js b/src/js/core/utils.js index e75789b9..9e2126b1 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -1,5 +1,6 @@ import { T } from "../translations"; import { rando } from "@nastyox/rando.js"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso"; const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"]; @@ -764,7 +765,7 @@ export function getLogoSprite() { return "logo_cn.png"; } - if (G_IS_STANDALONE) { + if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) { return "logo.png"; } @@ -777,6 +778,7 @@ export function getLogoSprite() { /** * Rejects a promise after X ms + * @param {Promise} promise */ export function timeoutPromise(promise, timeout = 30000) { return Promise.race([ diff --git a/src/js/game/modes/levels.js b/src/js/game/modes/levels.js index bd404dc5..1976c8c7 100644 --- a/src/js/game/modes/levels.js +++ b/src/js/game/modes/levels.js @@ -1,3 +1,4 @@ +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; import { enumHubGoalRewards } from "../tutorial_goals"; export const finalGameShape = "RuCw--Cw:----Ru--"; @@ -356,7 +357,7 @@ const STANDALONE_LEVELS = () => [ export function generateLevelsForVariant() { if (G_IS_STEAM_DEMO) { return STEAM_DEMO_LEVELS(); - } else if (G_IS_STANDALONE) { + } else if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) { return STANDALONE_LEVELS(); } return WEB_DEMO_LEVELS(); diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 7712582f..f0d62211 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -36,7 +36,8 @@ import { HUDInteractiveTutorial } from "../hud/parts/interactive_tutorial"; import { MetaBlockBuilding } from "../buildings/block"; import { MetaItemProducerBuilding } from "../buildings/item_producer"; import { MOD_SIGNALS } from "../../mods/mod_signals"; -import { finalGameShape, generateLevelsForVariant, LevelSetVariant } from "./levels"; +import { finalGameShape, generateLevelsForVariant } from "./levels"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; /** @typedef {{ * shape: string, @@ -377,7 +378,7 @@ export class RegularGameMode extends GameMode { } get difficultyMultiplicator() { - if (G_IS_STANDALONE) { + if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) { if (G_IS_STEAM_DEMO) { return 0.75; } diff --git a/src/js/platform/api.js b/src/js/platform/api.js index 95398ca8..5dbac1da 100644 --- a/src/js/platform/api.js +++ b/src/js/platform/api.js @@ -3,6 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { createLogger } from "../core/logging"; import { compressX64 } from "../core/lzstring"; +import { timeoutPromise } from "../core/utils"; import { T } from "../translations"; const logger = createLogger("puzzle-api"); @@ -53,23 +54,23 @@ export class ClientAPI { headers["x-token"] = this.token; } - return Promise.race([ + return timeoutPromise( fetch(this.getEndpoint() + endpoint, { cache: "no-cache", mode: "cors", headers, method: options.method || "GET", body: options.body ? JSON.stringify(options.body) : undefined, + }), + 15000 + ) + .then(res => { + if (res.status !== 200) { + throw "bad-status: " + res.status + " / " + res.statusText; + } + return res; }) - .then(res => { - if (res.status !== 200) { - throw "bad-status: " + res.status + " / " + res.statusText; - } - return res; - }) - .then(res => res.json()), - new Promise((resolve, reject) => setTimeout(() => reject("timeout"), 15000)), - ]) + .then(res => res.json()) .then(data => { if (data && data.error) { logger.warn("Got error from api:", data); @@ -100,22 +101,17 @@ export class ClientAPI { */ apiTryLogin() { if (!G_IS_STANDALONE) { - let token = window.localStorage.getItem("dev_api_auth_token"); - if (!token) { + let token = window.localStorage.getItem("steam_sso_auth_token"); + if (!token && G_IS_DEV) { token = window.prompt( "Please enter the auth token for the puzzle DLC (If you have none, you can't login):" ); - } - if (token) { window.localStorage.setItem("dev_api_auth_token", token); } return Promise.resolve({ token }); } - return Promise.race([ - ipcRenderer.invoke("steam:get-ticket"), - new Promise((resolve, reject) => setTimeout(() => reject("timeout"), 15000)), - ]).then( + return timeoutPromise(ipcRenderer.invoke("steam:get-ticket"), 15000).then( ticket => { logger.log("Got auth ticket:", ticket); return this._request("/v1/public/login", { diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 747f25ef..7694ad4d 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -13,6 +13,7 @@ import { FILE_NOT_FOUND } from "../storage"; import OR from "@openreplay/tracker"; import OR_fetch from "@openreplay/tracker-fetch"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; let eventConnector; if (!G_IS_STANDALONE && !G_IS_DEV) { @@ -57,6 +58,10 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { return "steam"; } + if (WEB_STEAM_SSO_AUTHENTICATED) { + return "prod-full"; + } + if (G_IS_RELEASE) { return "prod"; } diff --git a/src/js/platform/browser/wrapper.js b/src/js/platform/browser/wrapper.js index 3610b533..267fce08 100644 --- a/src/js/platform/browser/wrapper.js +++ b/src/js/platform/browser/wrapper.js @@ -1,6 +1,7 @@ import { globalConfig, IS_MOBILE } from "../../core/config"; import { createLogger } from "../../core/logging"; import { queryParamOptions } from "../../core/query_parameters"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; import { clamp } from "../../core/utils"; import { GamedistributionAdProvider } from "../ad_providers/gamedistribution"; import { NoAdProvider } from "../ad_providers/no_ad_provider"; @@ -24,7 +25,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { iogLink: true, }; - if (!G_IS_STANDALONE && queryParamOptions.embedProvider) { + if (!G_IS_STANDALONE && !WEB_STEAM_SSO_AUTHENTICATED && queryParamOptions.embedProvider) { const providerId = queryParamOptions.embedProvider; this.embedProvider.iframed = true; this.embedProvider.iogLink = false; diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 281b532c..33dd9e84 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -511,6 +511,12 @@ export class ApplicationSettings extends ReadWriteProxy { return ExplainedResult.bad("Bad settings object"); } + // MODS + if (!THEMES[data.settings.theme] || !this.app.restrictionMgr.getHasExtendedSettings()) { + console.log("Resetting theme because its no longer available: " + data.settings.theme); + data.settings.theme = "light"; + } + const settings = data.settings; for (let i = 0; i < this.settingHandles.length; ++i) { diff --git a/src/js/profile/setting_types.js b/src/js/profile/setting_types.js index 943e8e53..ccc90d70 100644 --- a/src/js/profile/setting_types.js +++ b/src/js/profile/setting_types.js @@ -3,6 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { createLogger } from "../core/logging"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso"; import { T } from "../translations"; const logger = createLogger("setting_types"); @@ -149,9 +150,16 @@ export class EnumSetting extends BaseSetting { */ getHtml(app) { const available = this.getIsAvailable(app); + return `
- ${available ? "" : `${T.demo.settingNotAvailable}`} + ${ + available + ? "" + : `${ + WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable + }` + }
@@ -229,7 +237,13 @@ export class BoolSetting extends BaseSetting { const available = this.getIsAvailable(app); return `
- ${available ? "" : `${T.demo.settingNotAvailable}`} + ${ + available + ? "" + : `${ + WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable + }` + }
@@ -289,7 +303,13 @@ export class RangeSetting extends BaseSetting { const available = this.getIsAvailable(app); return `
- ${available ? "" : `${T.demo.settingNotAvailable}`} + ${ + available + ? "" + : `${ + WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable + }` + }
diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 7c26ab17..f4ebdfbe 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -4,6 +4,7 @@ import { GameState } from "../core/game_state"; import { DialogWithForm } from "../core/modal_dialog_elements"; import { FormElementInput } from "../core/modal_dialog_forms"; import { ReadWriteProxy } from "../core/read_write_proxy"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso"; import { formatSecondsToTimeAgo, generateFileDownload, @@ -39,7 +40,8 @@ export class MainMenuState extends GameState { getInnerHTML() { const showLanguageIcon = !G_CHINA_VERSION && !G_WEGAME_VERSION; const showExitAppButton = G_IS_STANDALONE; - const showPuzzleDLC = !G_WEGAME_VERSION && G_IS_STANDALONE && !G_IS_STEAM_DEMO; + const showPuzzleDLC = + !G_WEGAME_VERSION && (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) && !G_IS_STEAM_DEMO; const showWegameFooter = G_WEGAME_VERSION; const hasMods = MODS.anyModsActive(); @@ -117,6 +119,26 @@ export class MainMenuState extends GameState { ${showExitAppButton ? `` : ""}
+ ${ + G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED + ? "" + : `
+ ${T.mainMenu.playFullVersion} + Sign in +
` + } + ${ + WEB_STEAM_SSO_AUTHENTICATED + ? ` +
${T.mainMenu.playingFullVersion} + ${T.mainMenu.logout} +
+ ` + : "" + } + diff --git a/src/js/states/mods.js b/src/js/states/mods.js index f4e6396c..07e47bb4 100644 --- a/src/js/states/mods.js +++ b/src/js/states/mods.js @@ -1,12 +1,9 @@ import { openStandaloneLink, THIRDPARTY_URLS } from "../core/config"; -import { queryParamOptions } from "../core/query_parameters"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso"; import { TextualGameState } from "../core/textual_game_state"; import { MODS } from "../mods/modloader"; import { T } from "../translations"; -const MODS_SUPPORTED = - !G_IS_STEAM_DEMO && (G_IS_STANDALONE || (G_IS_DEV && !window.location.href.includes("demo"))); - export class ModsState extends TextualGameState { constructor() { super("ModsState"); @@ -16,6 +13,14 @@ export class ModsState extends TextualGameState { return T.mods.title; } + get modsSupported() { + return ( + !WEB_STEAM_SSO_AUTHENTICATED && + !G_IS_STEAM_DEMO && + (G_IS_STANDALONE || (G_IS_DEV && !window.location.href.includes("demo"))) + ); + } + internalGetFullHtml() { let headerHtml = `
@@ -23,12 +28,12 @@ export class ModsState extends TextualGameState {
${ - MODS_SUPPORTED && MODS.mods.length > 0 + this.modsSupported && MODS.mods.length > 0 ? `` : "" } ${ - MODS_SUPPORTED + this.modsSupported ? `` : "" } @@ -45,11 +50,11 @@ export class ModsState extends TextualGameState { } getMainContentHTML() { - if (!MODS_SUPPORTED) { + if (!this.modsSupported) { return `
-

${T.mods.noModSupport}

+

${WEB_STEAM_SSO_AUTHENTICATED ? T.mods.browserNoSupport : T.mods.noModSupport}


Get on Steam! diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 3706555f..e4a0d11e 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -3,7 +3,8 @@ import { cachebust } from "../core/cachebust"; import { globalConfig } from "../core/config"; import { GameState } from "../core/game_state"; import { createLogger } from "../core/logging"; -import { getLogoSprite } from "../core/utils"; +import { authorizeViaSSOToken } from "../core/steam_sso"; +import { getLogoSprite, timeoutPromise } from "../core/utils"; import { getRandomHint } from "../game/hints"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; @@ -45,12 +46,7 @@ export class PreloadState extends GameState { } async fetchDiscounts() { - await Promise.race([ - new Promise((resolve, reject) => { - setTimeout(() => { - reject("Failed to resolve steam discounts within timeout"); - }, 2000); - }), + await timeoutPromise( fetch("https://analytics.shapez.io/v1/discounts") .then(res => res.json()) .then(data => { @@ -59,7 +55,8 @@ export class PreloadState extends GameState { ); logger.log("Fetched current discount:", globalConfig.currentDiscount); }), - ]).catch(err => { + 2000 + ).catch(err => { logger.warn("Failed to fetch current discount:", err); }); } @@ -72,6 +69,8 @@ export class PreloadState extends GameState { this.setStatus("Booting") .then(() => this.setStatus("Creating platform wrapper", 3)) + .then(() => authorizeViaSSOToken(this.app, this.dialogs)) + .then(() => this.app.platformWrapper.initialize()) .then(() => this.setStatus("Initializing local storage", 6)) diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 27f00080..ae5b763d 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -131,6 +131,10 @@ mainMenu: helpTranslate: Help translate! madeBy: Made by + playFullVersion: Sign in to play the full version in your Browser! + playingFullVersion: You are now playing the full version! Not all features work yet, but I'm working on it! + logout: Logout + # This is shown when using firefox and other browsers which are not supported. browserWarning: >- Sorry, but the game is known to run slowly on your browser! Get the full version or download Google Chrome for the full experience. @@ -468,6 +472,19 @@ dialogs:

Error Message: + steamSsoError: + title: Full Version Logout + desc: >- + You have been logged out from the Full Browser Version since either your network connection is unstable or you are playing on another device.

+ Please make sure you don't have shapez open in any other browser tab or another computer with the same Steam account.

+ You can login again in the main menu. + + steamSsoNoOwnership: + title: Full Edition not owned + desc: >- + In order to play the Full Edition in your Browser, you need to own both the base game and the Puzzle DLC on your Steam account.

+ Please make sure you own both, signed in with the correct Steam account and then try again. + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -1155,6 +1172,7 @@ mods: modsInfo: >- To install and manage mods, copy them to the mods folder (use the 'Open Mods Folder' button). Be sure to restart the game afterwards, otherwise the mods will not show up. noModSupport: Get the full version on Steam to install mods! + browserNoSupport: Due to browser restrictions it is currently only possible to install mods in the Steam version - Sorry! togglingComingSoon: title: Coming Soon