diff --git a/packages/parser/src/config/scriptConfig.ts b/packages/parser/src/config/scriptConfig.ts index 4a5adf60d..1e1db77e9 100644 --- a/packages/parser/src/config/scriptConfig.ts +++ b/packages/parser/src/config/scriptConfig.ts @@ -38,6 +38,7 @@ export const SCRIPT_CONFIG = [ { scriptString: 'getUserInput', scriptType: commandType.getUserInput }, { scriptString: 'applyStyle', scriptType: commandType.applyStyle }, { scriptString: 'wait', scriptType: commandType.wait }, + { scriptString: 'callSteam', scriptType: commandType.callSteam }, ]; export const ADD_NEXT_ARG_LIST = [ commandType.bgm, @@ -53,6 +54,7 @@ export const ADD_NEXT_ARG_LIST = [ commandType.playEffect, commandType.setTransition, commandType.applyStyle, + commandType.callSteam, ]; export type ConfigMap = Map; diff --git a/packages/parser/src/interface/sceneInterface.ts b/packages/parser/src/interface/sceneInterface.ts index 8f30f34df..8a55387d6 100644 --- a/packages/parser/src/interface/sceneInterface.ts +++ b/packages/parser/src/interface/sceneInterface.ts @@ -39,6 +39,7 @@ export enum commandType { getUserInput, applyStyle, wait, + callSteam, // 调用Steam功能 } /** diff --git a/packages/webgal/src/Core/controller/scene/sceneInterface.ts b/packages/webgal/src/Core/controller/scene/sceneInterface.ts index 801149d86..9964f2a02 100644 --- a/packages/webgal/src/Core/controller/scene/sceneInterface.ts +++ b/packages/webgal/src/Core/controller/scene/sceneInterface.ts @@ -39,6 +39,7 @@ export enum commandType { getUserInput, applyStyle, wait, + callSteam, // 调用Steam功能 } /** diff --git a/packages/webgal/src/Core/gameScripts/callSteam.ts b/packages/webgal/src/Core/gameScripts/callSteam.ts new file mode 100644 index 000000000..f717f3a9b --- /dev/null +++ b/packages/webgal/src/Core/gameScripts/callSteam.ts @@ -0,0 +1,39 @@ +import { ISentence } from '@/Core/controller/scene/sceneInterface'; +import { IPerform } from '@/Core/Modules/perform/performInterface'; +import { WebGAL } from '@/Core/WebGAL'; +import { logger } from '@/Core/util/logger'; +import { getStringArgByKey } from '@/Core/util/getSentenceArg'; +import { WEBGAL_NONE } from '../constants'; + +/** + * Unlocks a Steam achievement via the renderer → Electron bridge. + * The script expects the first positional parameter to be the achievement id. + */ +export const callSteam = (sentence: ISentence): IPerform => { + for (const arg of sentence.args) { + if (arg.key === 'achievementId') { + const achievementId = getStringArgByKey(sentence, 'achievementId'); + if (achievementId) { + WebGAL.steam + .unlockAchievement(achievementId) + .then((result) => { + logger.info(`callSteam: achievement ${achievementId} unlock ${result ? 'succeeded' : 'failed'}`); + }) + .catch((error) => { + logger.error(`callSteam: achievement ${achievementId} unlock threw`, error); + }); + } + } + } + const noperform: IPerform = { + performName: WEBGAL_NONE, + duration: 0, + isHoldOn: false, + stopFunction: () => {}, + blockingNext: () => false, + blockingAuto: () => true, + stopTimeout: undefined, + }; + + return noperform; +}; diff --git a/packages/webgal/src/Core/integration/steamIntegration.ts b/packages/webgal/src/Core/integration/steamIntegration.ts new file mode 100644 index 000000000..609496db1 --- /dev/null +++ b/packages/webgal/src/Core/integration/steamIntegration.ts @@ -0,0 +1,76 @@ +import { logger } from '@/Core/util/logger'; + +interface SteamBridge { + initialize: (appId: string) => boolean | Promise; + unlockAchievement: (achievementId: string) => boolean | Promise; +} + +const isWindowAvailable = (): boolean => typeof window !== 'undefined'; + +/** + * Provides a thin bridge between the renderer process and the Electron Steam integration. + */ +export class SteamIntegration { + public appId: string | null = null; + private initialized = false; + + public get isInitialized(): boolean { + return this.initialized; + } + + public async initialize(appId: string): Promise { + this.appId = appId; + const bridge = this.getSteamBridge(); + if (!bridge?.initialize) { + logger.warn('Steam integration initialize call skipped: Electron bridge not present'); + this.initialized = false; + return false; + } + + try { + const result = await Promise.resolve(bridge.initialize(appId)); + if (result) { + logger.info(`Steam integration initialized with AppID ${appId}`); + } + this.initialized = result; + return result; + } catch (error) { + logger.error('Steam integration failed to initialize', error); + this.initialized = false; + return false; + } + } + + public async unlockAchievement(achievementId: string): Promise { + const bridge = this.getSteamBridge(); + if (!bridge?.unlockAchievement) { + logger.warn(`Steam integration unlock call skipped for ${achievementId}: Electron bridge not present`); + return false; + } + + if (!this.initialized) { + if (this.appId) { + await this.initialize(this.appId); + } else { + logger.warn('Steam integration unlock call skipped: AppID not set'); + return false; + } + } + + try { + const result = await Promise.resolve(bridge.unlockAchievement(achievementId)); + return result; + } catch (error) { + logger.error(`Steam integration failed to unlock achievement ${achievementId}`, error); + return false; + } + } + + private getSteamBridge(): SteamBridge | undefined { + if (!isWindowAvailable()) { + logger.debug('Steam integration unavailable: window is undefined'); + return undefined; + } + return window.electronFuncs?.steam; + } +} diff --git a/packages/webgal/src/Core/parser/sceneParser.ts b/packages/webgal/src/Core/parser/sceneParser.ts index dc3a92212..85b3ebea3 100644 --- a/packages/webgal/src/Core/parser/sceneParser.ts +++ b/packages/webgal/src/Core/parser/sceneParser.ts @@ -27,6 +27,7 @@ import { setTransform } from '@/Core/gameScripts/setTransform'; import { setTransition } from '@/Core/gameScripts/setTransition'; import { unlockBgm } from '@/Core/gameScripts/unlockBgm'; import { unlockCg } from '@/Core/gameScripts/unlockCg'; +import { callSteam } from '@/Core/gameScripts/callSteam'; import { end } from '../gameScripts/end'; import { jumpLabel } from '../gameScripts/jumpLabel'; import { pixiInit } from '../gameScripts/pixi/pixiInit'; @@ -72,6 +73,7 @@ export const SCRIPT_TAG_MAP = defineScripts({ getUserInput: ScriptConfig(commandType.getUserInput, getUserInput), applyStyle: ScriptConfig(commandType.applyStyle, applyStyle, { next: true }), wait: ScriptConfig(commandType.wait, wait), + callSteam: ScriptConfig(commandType.callSteam, callSteam, { next: true }), }); export const SCRIPT_CONFIG: IConfigInterface[] = Object.values(SCRIPT_TAG_MAP); diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 3ee48216c..860f24411 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -58,6 +58,10 @@ export const infoFetcher = (url: string) => { if (command === 'Legacy_Expression_Blend_Mode') { Live2D.legacyExpressionBlendMode = res === true; } + if (command === 'Steam_AppID') { + const appId = String(res); + WebGAL.steam.initialize(appId); + } } } }); diff --git a/packages/webgal/src/Core/webgalCore.ts b/packages/webgal/src/Core/webgalCore.ts index f1becd487..65ca6a7a7 100644 --- a/packages/webgal/src/Core/webgalCore.ts +++ b/packages/webgal/src/Core/webgalCore.ts @@ -4,6 +4,7 @@ import { SceneManager } from '@/Core/Modules/scene'; import { AnimationManager } from '@/Core/Modules/animations'; import { Gameplay } from './Modules/gamePlay'; import { Events } from '@/Core/Modules/events'; +import { SteamIntegration } from '@/Core/integration/steamIntegration'; export class WebgalCore { public sceneManager = new SceneManager(); @@ -13,4 +14,5 @@ export class WebgalCore { public gameName = ''; public gameKey = ''; public events = new Events(); + public steam = new SteamIntegration(); } diff --git a/packages/webgal/src/types/electron.d.ts b/packages/webgal/src/types/electron.d.ts new file mode 100644 index 000000000..b5f432a9a --- /dev/null +++ b/packages/webgal/src/types/electron.d.ts @@ -0,0 +1,12 @@ +export {}; + +declare global { + interface Window { + electronFuncs?: { + steam?: { + initialize: (appId: string) => boolean | Promise; + unlockAchievement: (achievementId: string) => boolean | Promise; + }; + }; + } +}