Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
010696c
feat: add createFrame and removeFrame
xiaoxustudio Mar 11, 2026
ab43666
feat: add iframe api and reactive store
xiaoxustudio Mar 12, 2026
8d5c075
refactor: extract iframe interfaces
xiaoxustudio Mar 12, 2026
a073635
feat: add iframe wait and return value support
xiaoxustudio Mar 12, 2026
2a83f74
fix: handle iframe load error
xiaoxustudio Mar 12, 2026
e1976ee
chore: resolve some issues
xiaoxustudio Mar 12, 2026
dd454ff
fix: restrict iframe postMessage origin
xiaoxustudio Mar 12, 2026
3b82c42
feat: add iframe setGameVar and closeFrame API
xiaoxustudio Mar 12, 2026
d422882
refactor: simplify iframe component logic
xiaoxustudio Mar 13, 2026
3f00f64
feat: add iframe nextSentence and isBlockSentence API
xiaoxustudio Mar 13, 2026
029788c
feat: add iframe save and load events
xiaoxustudio Mar 13, 2026
3722839
fix: iframe width and height is invalid
xiaoxustudio Mar 13, 2026
a10af41
chore: unified name
xiaoxustudio Mar 14, 2026
33aee01
fix: unified function name
xiaoxustudio Mar 14, 2026
0cc054f
fix: build error
xiaoxustudio Mar 14, 2026
36b6017
fix: rename closeFrame to closeIframe
xiaoxustudio Mar 15, 2026
3545310
fix: reset iframe state when continue game
xiaoxustudio Mar 15, 2026
e65cc77
fix: isBlockSentence always return false when the second
xiaoxustudio Mar 15, 2026
d43cdb4
feat: add iframe persistent data API
xiaoxustudio Mar 15, 2026
32f6aee
feat: add iframe open/close and isActive state
xiaoxustudio Mar 17, 2026
ddbce3f
fix: set iframe isActive to true when adding
xiaoxustudio Mar 17, 2026
60440ed
feat: add command input parameters
xiaoxustudio Mar 19, 2026
ea691a8
feat: add postIframeMessage API
xiaoxustudio Mar 19, 2026
8ba8618
feat: add hidden parameter and style support for iframe
xiaoxustudio Mar 19, 2026
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
9 changes: 7 additions & 2 deletions packages/parser/src/config/scriptConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {commandType} from '../interface/sceneInterface';
import { commandType } from '../interface/sceneInterface';

export const SCRIPT_CONFIG = [
{ scriptString: 'say', scriptType: commandType.say },
Expand Down Expand Up @@ -31,14 +31,19 @@ export const SCRIPT_CONFIG = [
{ scriptString: 'setTextbox', scriptType: commandType.setTextbox },
{ scriptString: 'setAnimation', scriptType: commandType.setAnimation },
{ scriptString: 'playEffect', scriptType: commandType.playEffect },
{ scriptString: 'setTempAnimation', scriptType: commandType.setTempAnimation },
{
scriptString: 'setTempAnimation',
scriptType: commandType.setTempAnimation,
},
// comment?
{ scriptString: 'setTransform', scriptType: commandType.setTransform },
{ scriptString: 'setTransition', scriptType: commandType.setTransition },
{ scriptString: 'getUserInput', scriptType: commandType.getUserInput },
{ scriptString: 'applyStyle', scriptType: commandType.applyStyle },
{ scriptString: 'wait', scriptType: commandType.wait },
{ scriptString: 'callSteam', scriptType: commandType.callSteam },
{ scriptString: 'createIframe', scriptType: commandType.createIframe },
{ scriptString: 'removeIframe', scriptType: commandType.removeIframe },
];
export const ADD_NEXT_ARG_LIST = [
commandType.bgm,
Expand Down
2 changes: 2 additions & 0 deletions packages/parser/src/interface/sceneInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export enum commandType {
applyStyle,
wait,
callSteam, // 调用Steam功能
createIframe, // 创建框架
removeIframe, // 移除框架
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/Modules/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class Events {
public fullscreenDbClick = formEvent('fullscreen-dbclick');
public styleUpdate = formEvent('style-update');
public afterStyleUpdate = formEvent('after-style-update');
public save = formEvent('save');
public load = formEvent('load');
}

const eventBus = mitt();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { restorePerform } from '@/Core/controller/storage/jumpFromBacklog';

import { hasFastSaveRecord, loadFastSaveGame } from '@/Core/controller/storage/fastSaveLoad';
import { WebGAL } from '@/Core/WebGAL';
import { stageActions } from '@/store/stageReducer';

/**
* 从头开始游戏
Expand Down Expand Up @@ -40,6 +41,7 @@ export async function continueGame() {
await loadFastSaveGame();
return;
}
webgalStore.dispatch(stageActions.resetIframe());
if (
WebGAL.sceneManager.sceneData.currentSentenceId === 0 &&
WebGAL.sceneManager.sceneData.currentScene.sceneName === 'start.txt'
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/controller/scene/sceneInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export enum commandType {
applyStyle,
wait,
callSteam, // 调用Steam功能
createIframe, // 创建框架
removeIframe, // 移除框架
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/webgal/src/Core/controller/stage/resetStage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { initState, resetStageState, setStage } from '@/store/stageReducer';
import { initState, resetStageState, setStage, stageActions } from '@/store/stageReducer';
import { webgalStore } from '@/store/store';
import cloneDeep from 'lodash/cloneDeep';
import { WebGAL } from '@/Core/WebGAL';
Expand Down Expand Up @@ -27,4 +27,7 @@ export const resetStage = (resetBacklog: boolean, resetSceneAndVar = true) => {
if (!resetSceneAndVar) {
webgalStore.dispatch(setStage({ key: 'GameVar', value: currentVars }));
}

// 清空frames
webgalStore.dispatch(stageActions.resetIframe());
};
16 changes: 15 additions & 1 deletion packages/webgal/src/Core/controller/storage/loadGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { logger } from '../../util/logger';
import { sceneFetcher } from '../scene/sceneFetcher';
import { sceneParser } from '../../parser/sceneParser';
import { webgalStore } from '@/store/store';
import { resetStageState } from '@/store/stageReducer';
import { resetStageState, stageActions } from '@/store/stageReducer';
import { setVisibility } from '@/store/GUIReducer';
import { restorePerform } from './jumpFromBacklog';
import { stopAllPerform } from '@/Core/controller/gamePlay/stopAllPerform';
Expand All @@ -25,6 +25,7 @@ export const loadGame = (index: number) => {
logger.debug('读取的存档数据', loadFile);
// 加载存档
loadGameFromStageData(loadFile);
WebGAL.events.load.emit(index);
};

export function loadGameFromStageData(stageData: ISaveData) {
Expand All @@ -51,6 +52,8 @@ export function loadGameFromStageData(stageData: ISaveData) {

// 强制停止所有演出
stopAllPerform();
// 清空frames
webgalStore.dispatch(stageActions.resetIframe());

// 恢复backlog
const newBacklog = loadFile.backlog;
Expand All @@ -61,8 +64,19 @@ export function loadGameFromStageData(stageData: ISaveData) {

// 恢复舞台状态
const newStageState = cloneDeep(loadFile.nowStageState);
// 保存iframes的持久化数据
const iframePersistentData = new Map<string, Record<string, any>>();
newStageState.iframes.forEach((iframe) => {
if (iframe.persistentData) {
iframePersistentData.set(iframe.id, iframe.persistentData);
}
});
// iframes将被指令创建,我们不需要使用存档中的iframes
newStageState.iframes = [];
const dispatch = webgalStore.dispatch;
dispatch(resetStageState(newStageState));
// 将持久化数据存储到全局变量中,供后续创建iframe时使用
(window as any).__iframePersistentData = iframePersistentData;

// 恢复演出
setTimeout(restorePerform, 0);
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/Core/controller/storage/saveGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const saveGame = (index: number) => {
const saveData: ISaveData = generateCurrentStageData(index);
webgalStore.dispatch(saveActions.saveGame({ index, saveData }));
dumpSavesToStorage(index, index);
WebGAL.events.save.emit(index);
};

/**
Expand Down
141 changes: 141 additions & 0 deletions packages/webgal/src/Core/gameScripts/createIframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { ISentence } from '@/Core/controller/scene/sceneInterface';
import { IPerform } from '@/Core/Modules/perform/performInterface';
import { getBooleanArgByKey, getStringArgByKey } from '../util/getSentenceArg';
import { IIFrame } from '@/store/stageInterface';
import { webgalStore } from '@/store/store';
import { stageActions } from '@/store/stageReducer';
import { CSSProperties } from 'react';

const allSandboxProperties = {
'allow-forms': 'allowForms', // 允许iframe内提交表单
'allow-scripts': 'allowScripts', // 允许iframe内执行JavaScript脚本(包括定时器、事件等)
'allow-same-origin': 'allowSameOrigin', // 允许iframe内容拥有同源身份,可访问自身Cookie/LocalStorage等
'allow-top-navigation': 'allowTopNavigation', // 允许iframe内的链接跳转到父页面(主页面)的上下文
'allow-popups': 'allowPopups', // 允许iframe通过window.open()等方式弹出新窗口
'allow-modals': 'allowModals', // 允许iframe弹出模态窗口(如alert()、confirm()、prompt())
'allow-pointer-lock': 'allowPointerLock', // 允许iframe使用Pointer Lock API(如游戏鼠标锁定)
'allow-popups-to-escape-sandbox': 'allowPopupsToEscapeSandbox', // 允许iframe弹出的新窗口不受当前沙箱限制
'allow-downloads': 'allowDownloads', // 允许iframe内触发文件下载操作
'allow-presentation': 'allowPresentation', // 允许iframe使用Presentation API(投屏/演示功能)
'allow-top-navigation-by-user-activation': 'allowTopNavigationByUserActivation', // 仅允许用户主动触发(如点击)的顶级导航操作
'allow-storage-access-by-user-activation': 'allowStorageAccessByUserActivation', // 允许用户主动触发后访问父页面的存储权限
'allow-orientation-lock': 'allowOrientationLock', // 允许iframe使用Screen Orientation API锁定屏幕方向
};

/**
* 创建框架
* @param sentence
*/
export const createIframe = (sentence: ISentence): IPerform => {
const src = sentence.content;
const id = getStringArgByKey(sentence, 'id') ?? '';
const wait = getBooleanArgByKey(sentence, 'wait') ?? false;
const hidden = getBooleanArgByKey(sentence, 'hidden') ?? false;
const returnValue = getStringArgByKey(sentence, 'returnValue') ?? null;
const width = getStringArgByKey(sentence, 'width') ?? undefined;
const height = getStringArgByKey(sentence, 'height') ?? undefined;
if (!id || !src) {
return {
performName: 'none',
duration: 0,
isHoldOn: false,
stopFunction: () => {},
blockingNext: () => false,
blockingAuto: () => true,
stopTimeout: undefined,
};
}

// 可能后续会增加更多的样式,所以我们先定义一个空对象
// 还没想好如何优雅的使用指令定义样式,所以暂时使用已知参数定义样式
let styleCSSProperties: CSSProperties = {};
if (hidden) {
styleCSSProperties.display = 'none';
styleCSSProperties.opacity = 0;
}

let rawSrc = src;
// 处理src
if (!rawSrc.startsWith('http://') && !rawSrc.startsWith('https://')) {
rawSrc = './game/' + rawSrc;
}

// 查询所有参数(以@开头)
const args = sentence.args;
const injectArgs =
args.filter((arg) => arg.key.startsWith('@')).map((arg) => ({ key: arg.key.slice(1), value: arg.value })) ?? {};

const frameData: IIFrame = {
id,
src: rawSrc,
sandbox: '',
width,
height,
isActive: true,
wait,
returnValue,
injectArgs,
style: styleCSSProperties,
};

for (const [key, value] of Object.entries(allSandboxProperties)) {
const v = getStringArgByKey(sentence, value) ?? '';
if (v) {
frameData.sandbox += key + ' ';
}
}

webgalStore.dispatch(stageActions.addIframe(frameData));

// 如果需要等待iframe完成,则返回阻塞的perform
if (wait) {
let isCompleted = false;
// 监听iframe完成消息
const handleFrameComplete = (event: MessageEvent) => {
if (
event.data &&
typeof event.data === 'object' &&
event.data.type === 'webgal-frame-complete' &&
event.data.frameId === id
) {
isCompleted = true;
// 如果有returnValue,则存储到游戏变量中
if (returnValue && event.data.returnValue !== undefined) {
webgalStore.dispatch(
stageActions.setStageVar({
key: returnValue,
value: event.data.returnValue,
}),
);
}
// 移除事件监听器
window.removeEventListener('message', handleFrameComplete);
}
};

// 添加事件监听器
window.addEventListener('message', handleFrameComplete);

return {
performName: `frame-wait-${id}`,
duration: 0,
isHoldOn: true,
stopFunction: () => {
window.removeEventListener('message', handleFrameComplete);
},
blockingNext: () => !isCompleted,
blockingAuto: () => !isCompleted,
stopTimeout: undefined,
};
}

return {
performName: 'none',
duration: 0,
isHoldOn: false,
stopFunction: () => {},
blockingNext: () => false,
blockingAuto: () => true,
stopTimeout: undefined,
};
};
37 changes: 37 additions & 0 deletions packages/webgal/src/Core/gameScripts/removeIframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ISentence } from '@/Core/controller/scene/sceneInterface';
import { IPerform } from '@/Core/Modules/perform/performInterface';
import { stageActions } from '@/store/stageReducer';
import { webgalStore } from '@/store/store';
import { getBooleanArgByKey } from '../util/getSentenceArg';

/**
* 移除框架
* @param sentence
*/
export const removeIframe = (sentence: ISentence): IPerform => {
const id = sentence.content;
const save = getBooleanArgByKey(sentence, 'save') ?? false;
if (!id) {
return {
performName: 'none',
duration: 0,
isHoldOn: false,
stopFunction: () => {},
blockingNext: () => false,
blockingAuto: () => true,
stopTimeout: undefined,
};
}

webgalStore.dispatch(stageActions.removeIframe({ id, isActive: save }));

return {
performName: 'none',
duration: 0,
isHoldOn: false,
stopFunction: () => {},
blockingNext: () => false,
blockingAuto: () => true,
stopTimeout: undefined,
};
};
4 changes: 4 additions & 0 deletions packages/webgal/src/Core/parser/sceneParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ 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 { createIframe } from '@/Core/gameScripts/createIframe';
import { removeIframe } from '@/Core/gameScripts/removeIframe';
import { end } from '../gameScripts/end';
import { jumpLabel } from '../gameScripts/jumpLabel';
import { pixiInit } from '../gameScripts/pixi/pixiInit';
Expand Down Expand Up @@ -74,6 +76,8 @@ export const SCRIPT_TAG_MAP = defineScripts({
applyStyle: ScriptConfig(commandType.applyStyle, applyStyle, { next: true }),
wait: ScriptConfig(commandType.wait, wait),
callSteam: ScriptConfig(commandType.callSteam, callSteam, { next: true }),
createIframe: ScriptConfig(commandType.createIframe, createIframe),
removeIframe: ScriptConfig(commandType.removeIframe, removeIframe),
});

export const SCRIPT_CONFIG: IConfigInterface[] = Object.values(SCRIPT_TAG_MAP);
Expand Down
Loading
Loading