Skip to content

Commit 4cce67f

Browse files
Merge branch 'dev' into refactor-state-mgmt
# Conflicts: # packages/webgal/package.json # packages/webgal/public/game/template/template.json # packages/webgal/public/webgal-engine.json # packages/webgal/src/Core/Modules/scene.ts # packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts # packages/webgal/src/Core/gameScripts/vocal/index.ts
2 parents 55af05c + 6faf573 commit 4cce67f

21 files changed

Lines changed: 491 additions & 202 deletions

File tree

.github/workflows/release.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,56 @@ on:
55
tags:
66
- '*.*'
77

8+
permissions:
9+
contents: write
10+
811
jobs:
12+
build-webgal-static-webpage:
13+
name: Build WebGAL Static Webpage
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v2
18+
with:
19+
persist-credentials: false
20+
21+
- name: Setup Node.js environment
22+
uses: actions/setup-node@v3
23+
with:
24+
node-version-file: package.json
25+
cache: 'yarn'
26+
27+
- name: Install
28+
run: npm install yarn -g && yarn install
29+
30+
- name: Build
31+
run: yarn build
32+
33+
- name: Package WebGAL Engine Web
34+
run: |
35+
cd packages/webgal/dist
36+
zip -r "$GITHUB_WORKSPACE/webgal-engine-web.zip" .
37+
38+
- name: Upload WebGAL Engine Artifact
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: webgal-engine-web
42+
path: webgal-engine-web.zip
43+
if-no-files-found: error
44+
945
release:
1046
name: Release
1147
runs-on: ubuntu-latest
48+
needs: build-webgal-static-webpage
1249
steps:
1350
- name: Checkout
1451
uses: actions/checkout@v2
1552

53+
- name: Download WebGAL Engine Artifact
54+
uses: actions/download-artifact@v4
55+
with:
56+
name: webgal-engine-web
57+
1658
- name: Create Release
1759
id: create_release
1860
uses: actions/create-release@v1
@@ -24,3 +66,13 @@ jobs:
2466
body_path: releasenote.md
2567
draft: true
2668
prerelease: false
69+
70+
- name: Upload WebGAL Engine Web
71+
uses: actions/upload-release-asset@v1
72+
env:
73+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74+
with:
75+
upload_url: ${{ steps.create_release.outputs.upload_url }}
76+
asset_path: webgal-engine-web.zip
77+
asset_name: WebGAL-${{ github.ref_name }}-web.zip
78+
asset_content_type: application/zip

packages/parser/rollup.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default [
4040
tsconfigOverride: {
4141
compilerOptions: {
4242
sourceMap: !isProd,
43+
rootDir: "src",
4344
declarationDir: "build/cjs"
4445
}, include: ["src"]
4546
}
@@ -61,6 +62,7 @@ export default [
6162
tsconfigOverride: {
6263
compilerOptions: {
6364
sourceMap: !isProd,
65+
rootDir: "src",
6466
declarationDir: "build/types"
6567
}, include: ["src"]
6668
}

packages/parser/src/sceneParser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ export const sceneParser = (
3838
let assetsList: Array<IAsset> = []; // 场景资源列表
3939
let subSceneList: Array<string> = []; // 子场景列表
4040
const sentenceList: Array<ISentence> = rawSentenceListWithoutEmpty.map(
41-
(sentence) => {
41+
(sentence, index) => {
4242
const returnSentence: ISentence = scriptParser(
4343
sentence,
4444
assetSetter,
4545
ADD_NEXT_ARG_LIST,
4646
SCRIPT_CONFIG_MAP,
47+
index,
4748
);
4849
// 在这里解析出语句可能携带的资源和场景,合并到 assetsList 和 subSceneList
4950
assetsList = [...assetsList, ...returnSentence.sentenceAssets];

packages/parser/src/scriptParser/assetsScanner.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const assetsScanner = (
1212
command: commandType,
1313
content: string,
1414
args: Array<arg>,
15+
lineNumber: number,
1516
): Array<IAsset> => {
1617
let hasVocalArg = false;
1718
const returnAssetsList: Array<IAsset> = [];
@@ -22,7 +23,7 @@ export const assetsScanner = (
2223
returnAssetsList.push({
2324
name: e.value as string,
2425
url: e.value as string,
25-
lineNumber: 0,
26+
lineNumber,
2627
type: fileType.vocal,
2728
});
2829
}
@@ -36,39 +37,39 @@ export const assetsScanner = (
3637
returnAssetsList.push({
3738
name: content,
3839
url: content,
39-
lineNumber: 0,
40+
lineNumber,
4041
type: fileType.background,
4142
});
4243
}
4344
if (command === commandType.changeFigure) {
4445
returnAssetsList.push({
4546
name: content,
4647
url: content,
47-
lineNumber: 0,
48+
lineNumber,
4849
type: fileType.figure,
4950
});
5051
}
5152
if (command === commandType.miniAvatar) {
5253
returnAssetsList.push({
5354
name: content,
5455
url: content,
55-
lineNumber: 0,
56+
lineNumber,
5657
type: fileType.figure,
5758
});
5859
}
5960
if (command === commandType.video) {
6061
returnAssetsList.push({
6162
name: content,
6263
url: content,
63-
lineNumber: 0,
64+
lineNumber,
6465
type: fileType.video,
6566
});
6667
}
6768
if (command === commandType.bgm) {
6869
returnAssetsList.push({
6970
name: content,
7071
url: content,
71-
lineNumber: 0,
72+
lineNumber,
7273
type: fileType.bgm,
7374
});
7475
}

packages/parser/src/scriptParser/scriptParser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const scriptParser = (
2424
assetSetter: any,
2525
ADD_NEXT_ARG_LIST: commandType[],
2626
SCRIPT_CONFIG_MAP: ConfigMap,
27+
lineNumber = 0,
2728
): ISentence => {
2829
let command: commandType; // 默认为对话
2930
let content: string; // 语句内容
@@ -105,7 +106,7 @@ export const scriptParser = (
105106
}
106107

107108
content = contentParser(newSentenceRaw.trim(), command, assetSetter); // 将语句内容里的文件名转为相对或绝对路径
108-
sentenceAssets = assetsScanner(command, content, args); // 扫描语句携带资源
109+
sentenceAssets = assetsScanner(command, content, args, lineNumber); // 扫描语句携带资源
109110
subScene = subSceneScanner(command, content); // 扫描语句携带子场景
110111
return {
111112
command: command, // 语句类型

packages/parser/test/parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ test("args", async () => {
4949
{ key: "left", value: true },
5050
{ key: "next", value: true }
5151
],
52-
sentenceAssets: [{ name: "m2.png", url: 'm2.png', type: fileType.figure, lineNumber: 0 }],
52+
sentenceAssets: [{ name: "m2.png", url: 'm2.png', type: fileType.figure, lineNumber: 24 }],
5353
subScene: [],
5454
inlineComment: ""
5555
};

packages/webgal/index.html

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -320,16 +320,68 @@
320320
<script>
321321
(() => {
322322
const isIOS = window.__WEBGAL_DEVICE_INFO__?.isIOS ?? false;
323-
if ('serviceWorker' in navigator && !isIOS) {
324-
navigator.serviceWorker
325-
.register('./webgal-serviceworker.js')
326-
.then((reg) => {
327-
console.log('Registration succeeded. Scope is ' + reg.scope);
328-
})
329-
.catch((error) => {
330-
console.log('Registration failed with ' + error);
331-
});
323+
const localHostnames = ['localhost', '127.0.0.1', '::1'];
324+
const isLocalPreview = localHostnames.includes(window.location.hostname) || window.location.protocol === 'file:';
325+
const isElectron = Boolean(window.electronFuncs) || /Electron/i.test(navigator.userAgent || '');
326+
const shouldBypassServiceWorker = isIOS || isLocalPreview || isElectron;
327+
const webgalServiceWorkerUrl = new URL('./webgal-serviceworker.js', window.location.href).href;
328+
const bypassReloadKey = 'webgal-sw-bypass-reloaded';
329+
const isWebGALRegistration = (registration) => {
330+
const workers = [registration.active, registration.installing, registration.waiting];
331+
return workers.some((worker) => worker?.scriptURL === webgalServiceWorkerUrl);
332+
};
333+
const markBypassReload = () => {
334+
try {
335+
if (sessionStorage.getItem(bypassReloadKey) === 'true') {
336+
return false;
337+
}
338+
sessionStorage.setItem(bypassReloadKey, 'true');
339+
return true;
340+
} catch {
341+
return false;
342+
}
343+
};
344+
const clearBypassReloadMark = () => {
345+
try {
346+
sessionStorage.removeItem(bypassReloadKey);
347+
} catch {}
348+
};
349+
const clearWebGALServiceWorker = async () => {
350+
try {
351+
const registrations = await navigator.serviceWorker.getRegistrations();
352+
const webgalRegistrations = registrations.filter(isWebGALRegistration);
353+
const hasWebGALController = navigator.serviceWorker.controller?.scriptURL === webgalServiceWorkerUrl;
354+
await Promise.all(webgalRegistrations.map((registration) => registration.unregister()));
355+
if ('caches' in window) {
356+
const cacheKeys = await caches.keys();
357+
await Promise.all(cacheKeys.filter((key) => key.startsWith('webgal-')).map((key) => caches.delete(key)));
358+
}
359+
if ((webgalRegistrations.length > 0 || hasWebGALController) && navigator.serviceWorker.controller && markBypassReload()) {
360+
window.location.reload();
361+
}
362+
} catch (error) {
363+
console.warn('Failed to clear WebGAL Service Worker cache', error);
364+
}
365+
};
366+
if (!('serviceWorker' in navigator)) {
367+
return;
368+
}
369+
if (shouldBypassServiceWorker) {
370+
clearWebGALServiceWorker();
371+
return;
332372
}
373+
if (!window.isSecureContext) {
374+
return;
375+
}
376+
clearBypassReloadMark();
377+
navigator.serviceWorker
378+
.register('./webgal-serviceworker.js')
379+
.then((reg) => {
380+
console.log('Registration succeeded. Scope is ' + reg.scope);
381+
})
382+
.catch((error) => {
383+
console.log('Registration failed with ' + error);
384+
});
333385
})();
334386
</script>
335387
<!-- 首屏加载 -->

packages/webgal/public/webgal-serviceworker.js

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
const CACHE_NAME = 'webgal-critical-assets-v3';
2-
const GAME_PREFIX = '/game/';
3-
const CRITICAL_PATHS = ['/game/background/', '/game/figure/', '/game/bgm/', '/game/vocal/', '/game/video/'];
1+
const CACHE_PREFIX = 'webgal-';
2+
const CACHE_NAME = 'webgal-build-assets-v1';
43
const LOG_PREFIX = '[WebGAL SW]';
4+
const HASHED_BUILD_ASSET_RE = /(^|\/)assets\/[^/?#]+-[A-Za-z0-9_-]{8,}\.(?:js|css|ttf|woff|woff2)$/;
55
const loggedKeys = new Set();
66

77
function logOnce(key, ...args) {
@@ -20,66 +20,41 @@ self.addEventListener('activate', (event) => {
2020
event.waitUntil(
2121
(async () => {
2222
const keys = await caches.keys();
23-
await Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)));
23+
await Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
2424
await self.clients.claim();
2525
})(),
2626
);
2727
});
2828

29-
function isCriticalGameRequest(request) {
29+
function isHashedBuildAssetRequest(request) {
3030
if (request.method !== 'GET') return false;
3131
const url = new URL(request.url);
3232
if (url.origin !== self.location.origin) return false;
33-
if (!url.pathname.startsWith(GAME_PREFIX)) return false;
34-
return CRITICAL_PATHS.some((prefix) => url.pathname.startsWith(prefix));
33+
return HASHED_BUILD_ASSET_RE.test(url.pathname);
3534
}
3635

37-
// Stale-while-revalidate: return cached response immediately, then update cache in background.
38-
async function staleWhileRevalidate(request) {
36+
async function cacheFirst(request) {
3937
const cache = await caches.open(CACHE_NAME);
40-
const cached = await cache.match(request.url);
41-
42-
const fetchAndUpdate = async () => {
43-
try {
44-
const response = await fetch(request);
45-
if (response.ok) {
46-
await cache.put(request.url, response.clone());
47-
}
48-
return response;
49-
} catch (e) {
50-
return null;
51-
}
52-
};
53-
38+
const cached = await cache.match(request);
5439
if (cached) {
55-
logOnce(`hit:${request.url}`, 'cache hit (revalidating):', new URL(request.url).pathname);
56-
// Revalidate in background — don't await
57-
fetchAndUpdate();
40+
logOnce(`hit:${request.url}`, 'cache hit:', new URL(request.url).pathname);
5841
return cached;
5942
}
6043

61-
// No cache — must wait for network
6244
const response = await fetch(request);
63-
if (response.ok) {
64-
await cache.put(request.url, response.clone());
45+
if (response.ok && response.status === 200) {
46+
await cache.put(request, response.clone());
6547
logOnce(`cache:${request.url}`, 'cached:', new URL(request.url).pathname);
6648
}
6749
return response;
6850
}
6951

7052
self.addEventListener('fetch', (event) => {
7153
const { request } = event;
72-
if (!isCriticalGameRequest(request)) return;
73-
74-
// Audio/video range requests are passed through to avoid partial-content edge cases.
75-
if (request.headers.has('range')) {
76-
logOnce(`range:${request.url}`, 'range passthrough:', new URL(request.url).pathname);
77-
event.respondWith(fetch(request));
78-
return;
79-
}
54+
if (!isHashedBuildAssetRequest(request)) return;
8055

8156
event.respondWith(
82-
staleWhileRevalidate(request).catch(() => {
57+
cacheFirst(request).catch(() => {
8358
return fetch(request);
8459
}),
8560
);

packages/webgal/src/Core/Modules/scene.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export const initSceneData = {
2424
};
2525

2626
export class SceneManager {
27-
public settledScenes: Array<string> = [];
28-
public settledAssets: Array<string> = [];
27+
public settledScenes: Set<string> = new Set();
28+
public settledAssets: Set<string> = new Set();
2929
public sceneData: ISceneData = cloneDeep(initSceneData);
3030
public lockSceneWrite = false;
3131
public sceneWritePromise: Promise<void> | null = null;
@@ -35,5 +35,7 @@ export class SceneManager {
3535
this.sceneData.sceneStack = [];
3636
this.sceneData.currentScene = cloneDeep(initSceneData.currentScene);
3737
this.sceneWritePromise = null;
38+
this.settledScenes.clear();
39+
this.settledAssets.clear();
3840
}
3941
}

packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { WebGAL } from '@/Core/WebGAL';
1111
import { getBooleanArgByKey, getStringArgByKey } from '@/Core/util/getSentenceArg';
1212
import { stageStateManager } from '@/Core/Modules/stage/stageStateManager';
1313
import { jumpToLabel } from '@/Core/gameScripts/label/jumpToLabel';
14+
import { prefetchCurrentSceneByProgress } from '@/Core/util/prefetcher/progressPrefetcher';
1415

1516
const MAX_FORWARD_SCRIPT_EXECUTION = 10000;
1617

@@ -44,6 +45,7 @@ export const scriptExecutor = (depth = 0) => {
4445
return;
4546
}
4647

48+
prefetchCurrentSceneByProgress();
4749
// 超过总语句数量,则从场景栈拿出一个需要继续的场景,然后继续流程。若场景栈清空,则停止流程
4850
if (
4951
WebGAL.sceneManager.sceneData.currentSentenceId >

0 commit comments

Comments
 (0)