Skip to content
Merged

4.6.3 #1019

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
915df1a
fix: resolve the transform calculation order of the -next chain in fa…
A-kirami Jun 25, 2026
71c5639
fix: resolve the settlement order of the `-next` chain when using fas…
A-kirami Jun 25, 2026
c99458c
feat: add opus support
MakinoharaShoko Jul 23, 2026
d5ae557
lint
MakinoharaShoko Jul 23, 2026
136ac14
Merge branch 'dev' into pr/994
MakinoharaShoko Jul 25, 2026
89439eb
refactor animation register
MakinoharaShoko Jul 25, 2026
8741d16
fix: 拓展对话语法
MakinoharaShoko Jul 25, 2026
5b13a39
feat: callScene supports args
MakinoharaShoko Jul 25, 2026
0c0a8f1
add figure position
MakinoharaShoko Jul 25, 2026
30bce07
fix: 修复场景调用错误时未弹回帧的问题
MakinoharaShoko Jul 25, 2026
6a7e1b1
fix: transform null 值问题
MakinoharaShoko Jul 25, 2026
685d16a
Merge pull request #1012 from OpenWebGAL/add-fig-pos
MakinoharaShoko Jul 29, 2026
c48eddc
Merge pull request #1011 from OpenWebGAL/callscene
MakinoharaShoko Jul 29, 2026
cd782ab
Merge remote-tracking branch 'origin/dev' into pr/994
MakinoharaShoko Jul 29, 2026
1493d92
Merge pull request #994 from A-kirami/fix/fast-preview-next-transform…
MakinoharaShoko Jul 29, 2026
d873a9c
update parser to support multiple line
MakinoharaShoko Jul 31, 2026
b39bad9
fix: 空行处理问题
MakinoharaShoko Jul 31, 2026
c43058f
feat: setVar can set local variables
MakinoharaShoko Aug 1, 2026
1d24b18
fix: 实时预览遇到占位行问题
MakinoharaShoko Aug 1, 2026
90d3784
fix: flowchart problem
MakinoharaShoko Aug 1, 2026
1bf00ae
v4.6.3
MakinoharaShoko Aug 1, 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@webgal/base",
"version": "4.6.2",
"version": "4.6.3",
"description": "A brand new web Visual Novel engine.",
"repository": "https://github.com/OpenWebGAL/WebGAL.git",
"author": "Mahiru <Mahiru_@outlook.com>",
Expand Down
27 changes: 27 additions & 0 deletions packages/parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# WebGAL Parser Changelog

## [4.6.3] - 2026-08-01

正式发布 4.6.3-beta.1 / beta.2 的多行语句支持,另含以下改动。

### Added
- 新增 `return` 命令,用于从被 `callScene` 调用的场景提前返回
- `sceneEntry` 新增 `locals` / `writeReturnTo`,承载 `callScene` 传入的局部变量与返回值写回目标
- 语音参数识别 `.opus` 扩展名

## [4.6.3-beta.2] - 2026-07-31

### Fixed
- `sceneTextPreProcess`:折叠序列的首行是空行时(例如空行后面紧跟一条续行),整条序列会被静默丢弃,导致预处理后行数变少。这会击穿「解析后语句 index == 文件行号」,进而让存档进度错位。改用 `null` 哨兵区分「不在折叠序列中」与「首行内容为空串」

## [4.6.3-beta.1] - 2026-07-31

### Added
- 正式启用多行语句支持:`sceneParser` 现在会先经过 `sceneTextPreProcess`,把以空白开头且首字符为 `-` 或 `|` 的续行折叠进上一条语句
- `ISentence` 新增行范围字段 `startLine` / `endLine`(0-based,含首尾)与 `isLineBreakHolder`,供图形编辑器按行范围整体替换语句
- 导出 `WEBGAL_LINE_BREAK_MARK` 与 `isLineBreakPlaceholder`

### Fixed
- `sceneTextPreProcess`:移除逐行 `console.log`;场景首行不再被误判为续行;场景末行以 `\` 结尾时不再丢失整段累积内容

### Technical Notes
- 续行会被替换成 `;_WEBGAL_LINE_BREAK_` 占位注释行,**预处理前后行数严格一致**。因此「解析后语句 index == 文件行号」这一不变量继续成立,引擎的存读档、Backlog、场景栈、已读记录(都存语句 index)无需改动,旧存档也不会错位

## [4.5.13] - 2025-07-05

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webgal-parser",
"version": "4.6.2",
"version": "4.6.3",
"description": "WebGAL script parser",
"scripts": {
"test": "vitest",
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/config/scriptConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const SCRIPT_CONFIG = [
{ scriptString: 'applyStyle', scriptType: commandType.applyStyle },
{ scriptString: 'wait', scriptType: commandType.wait },
{ scriptString: 'callSteam', scriptType: commandType.callSteam },
{ scriptString: 'return', scriptType: commandType.return },
];
export const ADD_NEXT_ARG_LIST = [
commandType.bgm,
Expand Down
8 changes: 6 additions & 2 deletions packages/parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { fileType } from './interface/assets';
import { IAsset } from './interface/sceneInterface';
import { sceneParser } from './sceneParser';
import { IWebGALStyleObj, scss2cssinjsParser } from "./styleParser";
import { sceneTextPreProcess } from "./sceneTextPreProcessor";
import {
isLineBreakPlaceholder,
sceneTextPreProcess,
WEBGAL_LINE_BREAK_MARK,
} from "./sceneTextPreProcessor";

export default class SceneParser {
private readonly SCRIPT_CONFIG_MAP: ConfigMap;
Expand Down Expand Up @@ -77,4 +81,4 @@ export default class SceneParser {
}

export { ADD_NEXT_ARG_LIST, SCRIPT_CONFIG };
export { sceneTextPreProcess };
export { sceneTextPreProcess, isLineBreakPlaceholder, WEBGAL_LINE_BREAK_MARK };
2 changes: 2 additions & 0 deletions packages/parser/src/interface/runtimeInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface sceneEntry {
sceneName: string; // 场景名称
sceneUrl: string; // 场景url
continueLine: number; // 继续原场景的行号
locals?: Record<string, any>; // 该帧的局部变量
writeReturnTo?: string; // 返回值写回该帧的哪个变量
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/parser/src/interface/sceneInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export enum commandType {
applyStyle,
wait,
callSteam, // 调用Steam功能
return, // 从被调用的场景返回
}

/**
Expand Down Expand Up @@ -74,6 +75,15 @@ export interface ISentence {
sentenceAssets: Array<IAsset>; // 语句携带的资源列表
subScene: Array<string>; // 语句包含子场景列表
inlineComment: string; // 行内注释
/**
* 本语句在原始场景文件中占据的行范围(0-based,含首尾)。
* 单行语句 startLine === endLine;多行语句 endLine 指向最后一条续行。
* 图形编辑器据此做「只替换被编辑语句所占的行」。
*/
startLine: number;
endLine: number;
/** 本条是多行语句被折叠掉的续行占位,仅用于补齐行数,不应展示给用户 */
isLineBreakHolder: boolean;
}

/**
Expand Down
37 changes: 36 additions & 1 deletion packages/parser/src/sceneParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
import { scriptParser } from './scriptParser/scriptParser';
import { fileType } from './interface/assets';
import { ConfigMap } from './config/scriptConfig';
import {
isLineBreakPlaceholder,
sceneTextPreProcess,
} from './sceneTextPreProcessor';

/**
* 场景解析器
Expand All @@ -28,7 +32,9 @@ export const sceneParser = (
ADD_NEXT_ARG_LIST: commandType[],
SCRIPT_CONFIG_MAP: ConfigMap,
): IScene => {
const rawSentenceList = rawScene.replaceAll('\r', '').split('\n'); // 原始句子列表
// 预处理把多行语句折叠进它的首行,并用占位行补齐被折叠掉的行,
// 因此这里的行数与原始场景严格一致,「语句 index == 文件行号」始终成立。
const rawSentenceList = sceneTextPreProcess(rawScene).split('\n'); // 原始句子列表

// 去分号留到后面去做了,现在注释要单独处理
const rawSentenceListWithoutEmpty = rawSentenceList;
Expand All @@ -52,6 +58,8 @@ export const sceneParser = (
},
);

markMultilineRanges(sentenceList, rawSentenceList);

// 开始资源的预加载
assetsList = deduplicateAssets(assetsList);
assetsPrefetcher(assetsList);
Expand All @@ -65,6 +73,33 @@ export const sceneParser = (
};
};

/**
* 回填多行语句的行范围。
*
* 预处理后,一条多行语句的完整内容都落在它的首行,紧跟其后的若干占位行
* 就是被折叠掉的续行。这里把占位行标记出来,并把首行语句的 endLine
* 推到最后一条续行,图形编辑器据此按整个行范围替换语句。
*
* @param sentenceList 解析后的语句列表
* @param processedLines 预处理后的行,与 sentenceList 一一对应
*/
const markMultilineRanges = (
sentenceList: ISentence[],
processedLines: string[],
) => {
let headIndex = -1; // 最近一条非占位语句的下标,即当前多行语句的首行
for (let index = 0; index < sentenceList.length; index++) {
if (!isLineBreakPlaceholder(processedLines[index])) {
headIndex = index;
continue;
}
sentenceList[index].isLineBreakHolder = true;
if (headIndex >= 0) {
sentenceList[headIndex].endLine = index;
}
}
};

const deduplicateAssets = (assetsList: IAsset[]): IAsset[] => {
const seenAssets = new Set<string>();
return assetsList.filter((asset) => {
Expand Down
127 changes: 39 additions & 88 deletions packages/parser/src/sceneTextPreProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,11 @@ function sceneTextPreProcessPassOne(lines: string[]): string[] {
let thisLineIsMultiline = false;

for (const line of lines) {
thisLineIsMultiline = false;

if (canBeMultiline(line)) {
thisLineIsMultiline = true;
}

if (shouldNotBeMultiline(line, lastLineIsMultiline)) {
thisLineIsMultiline = false;
}
// 续行需要接到上一行,所以场景的第一行永远不会是续行。
thisLineIsMultiline =
processedLines.length > 0 &&
canBeMultiline(line) &&
!shouldNotBeMultiline(line, lastLineIsMultiline);

if (thisLineIsMultiline) {
processedLines[processedLines.length - 1] += '\\';
Expand Down Expand Up @@ -98,7 +94,10 @@ function isEmptyLine(line: string): boolean {
*/
function sceneTextPreProcessPassTwo(lines: string[]): string[] {
const processedLines: string[] = [];
let currentMultilineContent = "";
// null 表示当前不在多行序列里。这里不能用空串来判断:
// 序列的首行本身可能就是空行(比如空行后面紧跟着一条续行),
// 那样整条序列会被静默丢弃,破坏「预处理前后行数一致」这个前提。
let currentMultilineContent: string | null = null;
let placeHolderLines: string[] = [];

function concat(line: string) {
Expand All @@ -110,12 +109,18 @@ function sceneTextPreProcessPassTwo(lines: string[]): string[] {
placeHolderLines.push(placeholderLine(line));
}

/** 把折叠好的整条语句连同补齐行数的占位行一起输出 */
function flushMultiline() {
processedLines.push(currentMultilineContent as string, ...placeHolderLines);
placeHolderLines = [];
currentMultilineContent = null;
}

for (const line of lines) {
console.log(line);
if (line.endsWith('\\')) {
const trueLine = line.slice(0, -1);

if (currentMultilineContent === "") {
if (currentMultilineContent === null) {
// first line
currentMultilineContent = trueLine;
} else {
Expand All @@ -125,23 +130,38 @@ function sceneTextPreProcessPassTwo(lines: string[]): string[] {
continue;
}

if (currentMultilineContent !== "") {
if (currentMultilineContent !== null) {
// end line
concat(line);
processedLines.push(currentMultilineContent);
processedLines.push(...placeHolderLines);

placeHolderLines = [];
currentMultilineContent = "";
flushMultiline();
continue;
}

processedLines.push(line);
}

// 场景末行仍以 \ 结尾时循环里没有机会收尾,在这里把累积的内容补出去。
if (currentMultilineContent !== null) {
flushMultiline();
}

return processedLines;
}

/**
* 占位行的前缀。占位行本身是一条注释,引擎会空跑掉;
* 它的作用是让预处理后的行数与原始场景一致,从而保持
* 「解析后语句 index == 文件行号」这一不变量。
*/
export const WEBGAL_LINE_BREAK_MARK = ';_WEBGAL_LINE_BREAK_';

/**
* 判断预处理后的某一行是否是占位行,即它是上一条多行语句折叠掉的续行。
*/
export function isLineBreakPlaceholder(processedLine: string): boolean {
return processedLine.startsWith(WEBGAL_LINE_BREAK_MARK);
}

/**
* Placeholder Line. Adding this line preserves the original number of lines
* in the scene text, so that it can be compatible with the graphical editor.
Expand All @@ -150,74 +170,5 @@ function sceneTextPreProcessPassTwo(lines: string[]): string[] {
* @returns The placeholder line
*/
function placeholderLine(content = "") {
return ";_WEBGAL_LINE_BREAK_" + content;
return WEBGAL_LINE_BREAK_MARK + content;
}

// export function sceneTextPreProcess(sceneText: string): string {
// const lines = sceneText.replaceAll('\r', '').split('\n');
// const processedLines: string[] = [];
// let lastNonMultilineIndex = -1;
// let isInMultilineSequence = false;

// function isMultiline(line: string): boolean {
// if (!line.startsWith(' ')) return false;
// const trimmedLine = line.trimStart();
// return trimmedLine.startsWith('|') || trimmedLine.startsWith('-');
// }

// for (let i = 0; i < lines.length; i++) {
// const line = lines[i];

// if (line.trim() === '') {
// // Empty line handling
// if (isInMultilineSequence) {
// // Check if the next line is a multiline line

// let isStillInMulti = false;
// for (let j = i + 1; j < lines.length; j++) {
// const lookForwardLine = lines[j] || '';
// // 遇到正常语句了,直接中断
// if (lookForwardLine.trim() !== '' && !isMultiline(lookForwardLine)) {
// isStillInMulti = false;
// break;
// }
// // 必须找到后面接的是参数,并且中间没有遇到任何正常语句才行
// if (lookForwardLine.trim() !== '' && isMultiline(lookForwardLine)) {
// isStillInMulti = true;
// break;
// }
// }
// if (isStillInMulti) {
// // Still within a multiline sequence
// processedLines.push(';_WEBGAL_LINE_BREAK_');
// } else {
// // End of multiline sequence
// isInMultilineSequence = false;
// processedLines.push(line);
// }
// } else {
// // Preserve empty lines outside of multiline sequences
// processedLines.push(line);
// }
// } else if (isMultiline(line)) {
// // Multiline statement handling
// if (lastNonMultilineIndex >= 0) {
// // Concatenate to the previous non-multiline statement
// const trimedLine = line.trimStart();
// const addBlank = trimedLine.startsWith('-') ? ' ' : '';
// processedLines[lastNonMultilineIndex] += addBlank + trimedLine;
// }

// // Add the special comment line
// processedLines.push(';_WEBGAL_LINE_BREAK_' + line);
// isInMultilineSequence = true;
// } else {
// // Non-multiline statement handling
// processedLines.push(line);
// lastNonMultilineIndex = processedLines.length - 1;
// isInMultilineSequence = false;
// }
// }

// return processedLines.join('\n');
// }
2 changes: 1 addition & 1 deletion packages/parser/src/scriptParser/argsParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function argsParser(
argValue = undefined;
}
// 判断是不是语音参数
if (argName.toLowerCase().match(/.ogg|.mp3|.wav/)) {
if (argName.toLowerCase().match(/.ogg|.mp3|.wav|.opus/)) {
returnArrayList.push({
key: 'vocal',
value: assetSetter(e, fileType.vocal),
Expand Down
7 changes: 7 additions & 0 deletions packages/parser/src/scriptParser/scriptParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export const scriptParser = (
sentenceAssets: [], // 语句携带的资源列表
subScene: [], // 语句携带的子场景
inlineComment: '', // 行内注释
startLine: lineNumber,
endLine: lineNumber,
isLineBreakHolder: false,
};
}
// 截取命令
Expand Down Expand Up @@ -116,5 +119,9 @@ export const scriptParser = (
sentenceAssets: sentenceAssets, // 语句携带的资源列表
subScene: subScene, // 语句携带的子场景
inlineComment: sentenceComment.trim(), // 行内注释
// 单行语句的行范围就是自己,多行语句的 endLine 由 sceneParser 回填
startLine: lineNumber,
endLine: lineNumber,
isLineBreakHolder: false,
};
};
Loading
Loading