Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/parser/src/config/scriptConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -53,6 +54,7 @@ export const ADD_NEXT_ARG_LIST = [
commandType.playEffect,
commandType.setTransition,
commandType.applyStyle,
commandType.callSteam,
];

export type ConfigMap = Map<string, ConfigItem>;
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/interface/sceneInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum commandType {
getUserInput,
applyStyle,
wait,
callSteam, // 调用Steam功能
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum commandType {
getUserInput,
applyStyle,
wait,
callSteam, // 调用Steam功能
}

/**
Expand Down
39 changes: 39 additions & 0 deletions packages/webgal/src/Core/gameScripts/callSteam.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
}
Comment on lines +13 to +27

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The for...of loop is unnecessary and potentially incorrect. You iterate over all arguments, and for each one with the key achievementId, you call getStringArgByKey. getStringArgByKey itself finds the first argument with the given key. This means if you have multiple arguments, you might be re-evaluating getStringArgByKey unnecessarily. If there are multiple arguments with key: 'achievementId', you'll be trying to unlock the same (first) achievement multiple times. It's more efficient and correct to call getStringArgByKey once.

  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;
};
76 changes: 76 additions & 0 deletions packages/webgal/src/Core/integration/steamIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { logger } from '@/Core/util/logger';

interface SteamBridge {
initialize: (appId: string) => boolean | Promise<boolean>;
unlockAchievement: (achievementId: string) => boolean | Promise<boolean>;
}

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<boolean> {
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<boolean> {
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;
}
}
Comment on lines +51 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In unlockAchievement, if the integration is not initialized, you attempt to initialize it. However, you don't check if this initialization was successful before proceeding. If await this.initialize(this.appId) fails (returns false), this.initialized will remain false, but the function will continue and attempt to call bridge.unlockAchievement, which is not what's intended. You should check the initialization status again after the attempt and return early if it failed.

    if (!this.initialized) {
      if (this.appId) {
        await this.initialize(this.appId);
        if (!this.initialized) {
          logger.warn(`Steam integration unlock call skipped for ${achievementId}: initialization failed`);
          return false;
        }
      } 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;
}
}
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/parser/sceneParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
});
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/webgalCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -13,4 +14,5 @@ export class WebgalCore {
public gameName = '';
public gameKey = '';
public events = new Events();
public steam = new SteamIntegration();
}
12 changes: 12 additions & 0 deletions packages/webgal/src/types/electron.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export {};

declare global {
interface Window {
electronFuncs?: {
steam?: {
initialize: (appId: string) => boolean | Promise<boolean>;
unlockAchievement: (achievementId: string) => boolean | Promise<boolean>;
};
};
}
}