c++bug修复

This commit is contained in:
Evan
2026-09-17 11:53:14 +08:00
parent d83654837a
commit 782f76170f
14 changed files with 627 additions and 170 deletions

View File

@@ -28,6 +28,8 @@
"test:lint": "eslint . --ext .js,.jsx",
"test:unit": "jest test[\\\\/]unit[\\\\/]addons",
"test:smoke": "jest --runInBand test[\\\\/]smoke",
"test:cpp": "node scripts/test-cpp-regressions.cjs",
"test:devtool": "node scripts/test-devtool-control.cjs",
"watch": "webpack --colors --watch"
},
"config": {

View File

@@ -0,0 +1,71 @@
// Read-only diagnostic: exercise the current editor interpreter with mocked game APIs.
// Run from scratch-gui: node scripts/diagnose-cpp.cjs
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const babel = require('@babel/core');
const filename = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
const transformed = babel.transformSync(fs.readFileSync(filename, 'utf8'), {
filename, configFile: false, babelrc: false,
plugins: ['@babel/plugin-transform-modules-commonjs']
}).code;
const context = {
exports: {}, require, setTimeout,
console: {log() {}, warn() {}, error() {}}, window: {}
};
vm.runInNewContext(transformed, context, {filename});
const {CppCodeExecutor, validateCppSyntax} = context.exports;
const main = body => `int main() {\n${body}\n}`;
const customer = `void test() {
for (int i = 0; i < 2; i++) {
}
}
int main() {
turnLeft(-1);
for (int i = 0; i < 1; i++) {
test();
playerMove(i);
}
}`;
const cases = [
['customer_local_i', customer, {moves: [0]}],
['customer_renamed_inner_j', customer.replace('int i = 0; i < 2; i++', 'int j = 0; j < 2; j++'), {moves: [0]}],
['cout_literal', main('cout << "Hello" << endl;'), {output: ['Hello\n']}],
['cout_variable', main('int i = 7;\ncout << "i=" << i << endl;'), {output: ['i=7\n']}],
['cout_string_assignment_shape', main('cout << "value=42" << endl;'), {output: ['value=42\n']}],
['cout_increment_expression', main('int i = 0;\ncout << i++ << endl;\nplayerMove(i);'), {output: ['0\n'], moves: [1]}],
['cout_increment_literal', main('cout << "C++" << endl;'), {output: ['C++\n']}],
['cout_std_qualified', main('std::cout << "Hello" << std::endl;'), {output: ['Hello\n']}],
['main_return_zero', main('return 0;'), {error: null}],
['legal_space_before_call', main('playerMove (1);'), {valid: true, moves: [1]}],
['nested_loop_shadowing', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
['negative_integer_division', main('int a = -3;\na /= 2;\nplayerMove(a);'), {moves: [-1]}],
['function_return_value', 'int answer() {\nreturn 7;\n}\n' + main('int n = answer();\nplayerMove(n);'), {moves: [7]}],
['nested_if_break', main('for (int i = 0; i < 3; i++) {\nif (i == 1) {\nbreak;\n}\nplayerMove(i);\n}'), {moves: [0]}],
['floating_point', main('double x = 1.5;\ncout << x << endl;'), {output: ['1.5\n']}],
['array_index', main('int a[2] = {3, 7};\nplayerMove(a[1]);'), {moves: [7]}],
['unknown_variable', main('playerMove(missing);'), {valid: false}],
['early_return_zero', 'int stop() {\nreturn 0;\nplayerMove(9);\n}\n' + main('stop();'), {moves: []}]
];
(async () => {
const results = [];
for (const [name, code, expected] of cases) {
const actual = {valid: false, moves: [], turns: [], output: [], error: null};
context.window = {isScriptRunOver: false, cppGameAPI: {
playerMove: async n => actual.moves.push(n),
turnLeft: async n => actual.turns.push(n),
cmdPrint: text => actual.output.push(text)
}};
const validation = await validateCppSyntax(code);
actual.valid = validation.isValid;
if (!actual.valid) actual.validationErrors = validation.errors;
const executor = new CppCodeExecutor(code, null);
if (actual.valid) {
try { await executor.run(); } catch (error) {actual.error = error.message;}
}
const matchesExpected = Object.entries(expected).every(([key, value]) =>
JSON.stringify(actual[key]) === JSON.stringify(value));
results.push({name, expected, actual, matchesExpected});
}
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
})().catch(error => {console.error(error); process.exitCode = 1;});

View File

@@ -0,0 +1,162 @@
// Regression tests for the editor's real interpreter and output callback.
// Run: node scripts/test-cpp-regressions.cjs
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const babel = require('@babel/core');
const parserPath = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
const source = fs.readFileSync(parserPath, 'utf8');
const compiled = babel.transformSync(source, {
filename: parserPath, configFile: false, babelrc: false,
plugins: ['@babel/plugin-transform-modules-commonjs'],
presets: process.argv.includes('--transpiled') ? ['@babel/preset-env'] : []
}).code;
const silentConsole = {log() {}, warn() {}, error() {}};
const context = {exports: {}, require, setTimeout, console: silentConsole, window: {}};
vm.runInNewContext(compiled, context, {filename: parserPath});
const {CppCodeExecutor, validateCppSyntax} = context.exports;
const main = body => `int main() {\n${body}\n}`;
// Complete program from the first email screenshot; the second repeats this issue.
const customerEmailCode = `void test() {
for (int i = 0; i < 2; i++) {
}
}
int main() {
// 在这里编写你的C++代码来控制角色
turnLeft(-1);
for (int i = 0; i < 1; i++) {
test();
playerMove(i);
}
return 0;
}`;
// The third screenshot reports cout without supplying a cout example.
// Add observable output to the supplied program to check both fixes together.
const customerEmailWithOutput = customerEmailCode.replace(' playerMove(i);',
' cout << "i=" << i << endl;\n playerMove(i);');
async function run(code, expected, configure) {
const moves = [];
const turns = [];
const output = [];
context.window = {isScriptRunOver: false, cppGameAPI: {
playerMove: async n => moves.push(n),
turnLeft: async n => turns.push(n),
cmdPrint: text => output.push(text)
}};
const executor = new CppCodeExecutor(code, null);
if (configure) configure(executor, context.window);
const validation = await validateCppSyntax(code);
assert.strictEqual(validation.isValid, true, JSON.stringify(validation.errors));
await executor.run();
if (expected.moves) assert.deepStrictEqual(moves, expected.moves);
if (expected.turns) assert.deepStrictEqual(turns, expected.turns);
if (expected.output) assert.deepStrictEqual(output, expected.output);
assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution');
assert.strictEqual(executor.callStack.length, 0);
return executor;
}
const cases = [
['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}],
['customer email with variable output (screenshot 3)', customerEmailWithOutput, {moves: [0], turns: [-1], output: ['i=0\n']}],
['nested loops', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
['loop initializer restores shadowed value', main('int i = 9;\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [9]}],
['loop assignment updates existing value', main('int i = 9;\nfor (i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [2]}],
['block declaration versus assignment', main('int n = 1;\nif (true) {\nint n = 7;\nplayerMove(n);\n}\nif (true) {\nn = 3;\n}\nplayerMove(n);'), {moves: [7, 3]}],
['string block shadowing', main('string msg = "outer";\nif (true) {\nstring msg = "C++";\ncout << msg;\n}\ncout << msg;'), {output: ['C++', 'outer']}],
['for body scope per iteration', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nplayerMove(x);\nint x = 2;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
['while body scope per iteration', main('int x = 9;\nint i = 0;\nwhile (i < 2) {\nplayerMove(x);\nint x = 2;\ni++;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
['function locals', 'void f() {\nint n = 7;\nplayerMove(n);\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [7, 2]}],
['all arguments evaluated before binding', 'void f(int a, int b) {\nplayerMove(a);\nplayerMove(b);\n}\n' + main('int a = 2;\nf(7, a);\nplayerMove(a);'), {moves: [7, 2, 2]}],
['recursive locals and parameters', 'void f(int n) {\nint saved = n;\nif (n > 0) {\nf(n - 1);\n}\nplayerMove(saved);\n}\n' + main('f(2);'), {moves: [0, 1, 2]}],
['early void return unwinds scope', 'void f() {\nint n = 8;\nreturn;\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [2]}],
['continue unwinds body scope', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nint x = 2;\ncontinue;\n}\nplayerMove(x);'), {moves: [9]}],
['break does not increment', main('int i = 0;\nfor (i = 0; i < 2; i++) {\nbreak;\n}\nplayerMove(i);'), {moves: [0]}],
['cout with equals', main('int i = 7;\ncout << "i=" << i << endl;'), {output: ['i=7\n']}],
['cout with operator text', main('cout << "value=42 C++ -- +=" << endl;'), {output: ['value=42 C++ -- +=\n']}],
['cout with game API text', main('cout << "playerMove(2)" << endl;'), {moves: [], output: ['playerMove(2)\n']}],
['qualified cout', main('std::cout << "Hello" << std::endl;'), {output: ['Hello\n']}],
['cout postfix and prefix', main('int i = 0;\ncout << i++ << endl;\ncout << ++i << endl;\ncout << i-- << endl;\ncout << --i << endl;'), {output: ['0\n', '2\n', '2\n', '0\n']}],
['cout arithmetic', main('int i = 3;\ncout << "result=" << (i * 2 + 1) << endl;'), {output: ['result=7\n']}],
['escaped quotes and stream delimiters', main(String.raw`cout << "a\"<<b\nC++" << endl;`), {output: ['a"<<b\nC++\n']}],
['consecutive output fragments', main('cout << "A";\ncout << "B" << endl;'), {output: ['A', 'B\n']}]
];
(async () => {
for (const [name, code, expected] of cases) {
try { await run(code, expected); } catch (error) {error.message = `${name}: ${error.message}`; throw error;}
console.log(`PASS ${name}`);
}
// Seed the interpreter's global environment to verify lexical lookup and writes.
const globalExecutor = await run('void f() {\ng += 1;\nplayerMove(g);\n}\n' + main('int g = 9;\nf();\nplayerMove(g);'), {moves: [2, 9]}, executor => executor.variables.set('g', 1));
assert.strictEqual(globalExecutor.variables.get('g'), 2);
console.log('PASS global writes survive function calls; caller locals are not visible');
for (const stop of [false, true]) {
const executor = new CppCodeExecutor('void f(int n) {\nfor (int i = 0; i < 2; i++) {\nplayerMove(i);\n}\n}\n' + main('f(2);'), null);
executor.variables.set('n', 9);
context.window = {isScriptRunOver: false, cppGameAPI: {playerMove: async () => {
if (stop) context.window.isScriptRunOver = true;
else throw new Error('game failure');
}}};
if (stop) await executor.run();
else await assert.rejects(executor.run(), /game failure/);
assert.strictEqual(executor.variables, executor.globalVariables);
assert.strictEqual(executor.variables.get('n'), 9);
assert.strictEqual(executor.variables.has('i'), false);
assert.strictEqual(executor.callStack.length, 0);
assert.strictEqual(executor.currentExecutionContext, null);
console.log(`PASS scope cleanup on ${stop ? 'stop' : 'error'}`);
}
// Extract the actual React component callback, avoiding a game-engine dependency.
const uiPath = path.resolve(__dirname, '../src/components/stage-cpp/stage-cpp-clang.jsx');
const uiSource = fs.readFileSync(uiPath, 'utf8');
const ast = babel.parseSync(uiSource, {configFile: false, babelrc: false, parserOpts: {plugins: ['jsx']}});
let callback;
function visit(node) {
if (!node || typeof node !== 'object') return;
if (node.type === 'ObjectProperty' && node.key.name === 'cmdPrint') callback = node.value;
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach(visit);
else if (value && typeof value === 'object') visit(value);
}
}
visit(ast);
assert(callback, 'cmdPrint callback exists');
for (const engineState of ['absent', 'ready', 'throwing']) {
let displayed = '';
const sent = [];
const window = {};
if (engineState !== 'absent') window.unityInstance = {SendMessage: (...args) => {
if (engineState === 'throwing') throw new Error('engine unavailable');
sent.push(args);
}};
const cmdPrint = vm.runInNewContext(`(${uiSource.slice(callback.start, callback.end)})`, {
window, console: silentConsole, setCompilationOutput: update => {displayed = update(displayed);}
});
cmdPrint('A');
cmdPrint('B\n');
assert.strictEqual(displayed, 'AB\n');
assert.strictEqual(window.cmd_print_text, 'AB\n');
if (engineState === 'ready') assert.deepStrictEqual(sent[1], ['UIMain', 'SetText', 'AB\n']);
console.log(`PASS output callback with ${engineState} engine`);
// Connect the real interpreter to the real UI callback, including repeated runs.
for (let attempt = 0; attempt < 2; attempt++) {
displayed = '';
window.cmd_print_text = '';
await run(customerEmailWithOutput, {moves: [0], turns: [-1]}, (executor, runtimeWindow) => {
runtimeWindow.cppGameAPI.cmdPrint = cmdPrint;
});
assert.strictEqual(displayed, 'i=0\n');
assert.strictEqual(window.cmd_print_text, 'i=0\n');
if (engineState === 'ready') assert.deepStrictEqual(sent[sent.length - 1], ['UIMain', 'SetText', 'i=0\n']);
}
console.log(`PASS customer program through output callback with ${engineState} engine`);
}
console.log(`${cases.length + 9} regression checks passed`);
})().catch(error => {console.error(error); process.exitCode = 1;});

View File

@@ -0,0 +1,166 @@
// Run: npm run test:devtool
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const babel = require('@babel/core');
const root = path.resolve(__dirname, '..');
const controlPath = 'src/playground/devtool-control.js';
const compile = file => babel.transformFileSync(path.join(root, file), {
configFile: false, babelrc: false, presets: ['@babel/preset-env', '@babel/preset-react']
}).code;
const controlCode = compile(controlPath);
const homeCode = compile('src/playground/home.js');
let checks = 0;
function test(name, action) {
action();
checks++;
console.log(`PASS ${name}`);
}
function page(storage = new Map(), {enabled = true, brokenStorage = false, immediate = false} = {}) {
const starts = [];
const effects = [];
function detector(options) {
starts.push(options);
// Simulate a library probe firing during initialization with F12 already open.
if (immediate) options.ondevtoolopen('already-open');
}
detector.isSuspend = false;
const window = {
location: {href: '/editor.html'}, onbeforeunload: () => {},
cppCodeStopRun: () => effects.push('stop C++'),
loadCppProject: () => effects.push('clear C++'),
pythonCodeStopRun: () => effects.push('stop Python'),
loadPythonProject: () => effects.push('clear Python'),
unityInstance: {Quit: () => effects.push('quit engine')}
};
const sessionStorage = {
getItem(key) {if (brokenStorage) throw new Error('storage denied'); return storage.get(key) || null;},
setItem(key, value) {if (brokenStorage) throw new Error('storage denied'); storage.set(key, value);}
};
const sandbox = {
exports: {}, window, sessionStorage,
localStorage: {getItem: () => '/index.html'},
vm: {
stopAll: () => effects.push('stop Scratch'),
editingTarget: {blocks: {deleteAllBlocks: () => effects.push('clear Scratch')}},
emitWorkspaceUpdate: () => effects.push('update workspace')
},
require(name) {
if (name === 'disable-devtool') return detector;
if (name === './config.js') return {disabledev: enabled};
if (name === './devtool-control.js') return api;
throw new Error(`Unexpected import: ${name}`);
}
};
const context = vm.createContext(sandbox);
vm.runInContext(controlCode, context, {filename: controlPath});
const api = sandbox.exports;
return {api, starts, effects, detector, window, context};
}
// Use the actual options and destructive callback from each editor mode.
function stageOptions(file) {
const source = fs.readFileSync(path.join(root, file), 'utf8');
const ast = babel.parseSync(source, {configFile: false, babelrc: false, parserOpts: {plugins: ['jsx']}});
let options;
function visit(node) {
if (!node || typeof node !== 'object') return;
if (node.type === 'ImportDeclaration') {
assert.notStrictEqual(node.source.value, 'disable-devtool', `${file} bypasses shared control`);
}
if (node.type === 'CallExpression' && node.callee.name === 'initDisableDevtool') options = node.arguments[0];
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach(visit);
else if (value && typeof value === 'object') visit(value);
}
}
visit(ast);
assert(options, `${file} must initialize through shared control`);
// Also verify the entire JSX file remains compatible with project Babel presets.
compile(file);
return source.slice(options.start, options.end);
}
const storage = new Map();
test('home exposes stopDev and persists it for navigation', () => {
const home = page(storage);
vm.runInContext(homeCode, home.context);
home.window.stopDev();
assert.strictEqual(storage.get('stopDev'), '1');
assert.strictEqual(home.detector.isSuspend, true);
});
for (const mode of ['stage-unity', 'stage-python-unity', 'stage-cpp-unity']) {
const optionSource = stageOptions(`src/components/${mode}/stage-unity.jsx`);
test(`${mode}: home -> editor with F12 open does not start detection or clear code`, () => {
const editor = page(storage, {immediate: true});
const options = vm.runInContext(`(${optionSource})`, editor.context);
editor.api.initDisableDevtool(options);
assert.strictEqual(editor.starts.length, 0);
assert.strictEqual(editor.detector.isSuspend, true);
assert.strictEqual(editor.window.location.href, '/editor.html');
assert.deepStrictEqual(editor.effects, []);
// A refresh or another mode initialization in this tab must stay stopped.
editor.api.initDisableDevtool(options);
assert.strictEqual(editor.starts.length, 0);
});
test(`${mode}: stopDev blocks an already queued destructive callback`, () => {
const editor = page();
const options = vm.runInContext(`(${optionSource})`, editor.context);
editor.api.initDisableDevtool(options);
assert.strictEqual(editor.starts.length, 1);
const callback = editor.starts[0];
editor.window.stopDev();
assert.strictEqual(callback.ignore(), true);
callback.ondevtoolopen('queued');
assert.strictEqual(editor.window.location.href, '/editor.html');
assert.deepStrictEqual(editor.effects, []);
});
}
test('normal session retains default detection and redirect', () => {
const normal = page();
normal.api.initDisableDevtool();
assert.strictEqual(normal.starts.length, 1);
assert.strictEqual(normal.starts[0].ignore(), false);
normal.starts[0].ondevtoolopen();
assert.strictEqual(normal.window.location.href, '/index.html');
});
test('custom options, ignore and callback arguments remain intact', () => {
const normal = page();
const seen = [];
const next = () => {};
normal.api.initDisableDevtool({interval: 123, ignore: () => true, ondevtoolopen: (...args) => seen.push(args)});
assert.strictEqual(normal.starts[0].interval, 123);
assert.strictEqual(normal.starts[0].ignore(), true);
normal.starts[0].ondevtoolopen(7, next);
assert.deepStrictEqual(seen, [[7, next]]);
});
test('disabled configuration does not initialize; stopDev remains available', () => {
const development = page(new Map(), {enabled: false});
development.api.initDisableDevtool();
assert.strictEqual(development.starts.length, 0);
assert.strictEqual(typeof development.window.stopDev, 'function');
});
test('blocked storage still permits immediate stop on current page', () => {
const broken = page(new Map(), {brokenStorage: true});
broken.api.initDisableDevtool();
broken.window.stopDev();
assert.strictEqual(broken.detector.isSuspend, true);
assert.strictEqual(broken.starts[0].ignore(), true);
broken.starts[0].ondevtoolopen();
broken.api.initDisableDevtool();
assert.strictEqual(broken.starts.length, 1);
assert.strictEqual(broken.window.location.href, '/editor.html');
});
for (const file of ['src/playground/competition.js', 'src/playground/competition_sum.js']) {
const optionsSource = stageOptions(file);
test(`${file}: same stopDev state is honored`, () => {
const competition = page(storage, {immediate: true});
competition.api.initDisableDevtool(vm.runInContext(`(${optionsSource})`, competition.context));
assert.strictEqual(competition.starts.length, 0);
});
}
console.log(`${checks} devtool regression checks passed`);

View File

@@ -11,7 +11,6 @@ import RankBadgeGenerator from '../../playground/rank-badge-generator.js';
// const friendToggle = new FriendToggle();
window.RankBadgeGenerator = RankBadgeGenerator;
window.CONFIG = CONFIG;
import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from '../../playground/devtool-control.js';
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
import { debug } from 'scratch-vm/src/util/log.js';
@@ -561,7 +560,7 @@ const UnityComponent = function (props) {
useEffect(() => {
if (CONFIG.disabledev) {
disableDevtool({
initDisableDevtool({
ondevtoolopen: () => {
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗

View File

@@ -510,10 +510,49 @@ export class CppSyntaxValidator {
}
}
// 声明写入当前作用域,赋值更新最近的已有变量;函数不继承调用者的局部变量。
class CppVariableScope extends Map {
constructor(parent = null) {
super();
this.parent = parent;
}
declare(name, value) {
return super.set(name, value);
}
has(name) {
return super.has(name) || Boolean(this.parent && this.parent.has(name));
}
get(name) {
return super.has(name) ? super.get(name) : this.parent && this.parent.get(name);
}
set(name, value) {
if (!super.has(name) && this.parent && this.parent.has(name)) {
this.parent.set(name, value);
return this;
}
return super.set(name, value);
}
*entries() {
const visible = new Map(this.parent ? this.parent.entries() : []);
for (const [name, value] of super.entries()) visible.set(name, value);
yield* visible.entries();
}
[Symbol.iterator]() {
return this.entries();
}
}
export class CppCodeExecutor {
constructor(code, setHighlightedLine) {
this.code = code;
this.variables = new Map();
this.globalVariables = new CppVariableScope();
this.variables = this.globalVariables;
this.functions = new Map(); // 存储自定义函数
this.currentLine = 0;
this.lines = [];
@@ -527,7 +566,7 @@ export class CppCodeExecutor {
try {
this.isRunning = true;
this.parseCode();
await this.executeMain();
await this.withVariableScope(() => this.executeMain(), this.globalVariables);
} catch (error) {
console.error('C++执行错误:', error);
throw error;
@@ -544,6 +583,16 @@ export class CppCodeExecutor {
return !this.isRunning || window.isScriptRunOver;
}
async withVariableScope(action, parent = this.variables) {
const previous = this.variables;
this.variables = new CppVariableScope(parent);
try {
return await action();
} finally {
this.variables = previous;
}
}
parseCode() {
debugLog('🔍 开始解析C++代码,支持自定义函数');
@@ -1300,10 +1349,10 @@ export class CppCodeExecutor {
await this.executeSimpleStatement(statement.content);
break;
case 'for':
await this.executeForLoop(statement);
await this.withVariableScope(() => this.executeForLoop(statement));
break;
case 'if':
await this.executeIfStatement(statement);
await this.withVariableScope(() => this.executeIfStatement(statement));
break;
case 'while':
await this.executeWhileLoop(statement);
@@ -1324,6 +1373,20 @@ export class CppCodeExecutor {
async executeSimpleStatement(content) {
debugLog(`🔍 执行简单语句: ${content}`);
// 先识别输出语句,避免把字符串里的 =、++、游戏函数名当作代码执行。
if (/^\s*(?:std\s*::\s*)?cout\s*<</.test(content)) {
this.executeCoutStatement(content);
return;
}
// 声明必须留在当前作用域,不能按普通赋值覆盖外层同名变量。
const declaration = content.match(/^\s*(int|bool|Position|string)\s+(\w+)\s*=\s*(.+);\s*$/);
if (declaration) {
const [, , name, value] = declaration;
this.variables.declare(name, this.evaluateExpression(value));
return;
}
// 检查是否是自定义函数调用(包括带括号的实参,如 f((n + 1));
const customCall = this.parseCustomFunctionCall(content);
if (customCall && this.functions.has(customCall.functionName)) {
@@ -1355,40 +1418,6 @@ export class CppCodeExecutor {
}
}
// 变量声明
if (content.includes('int ') && content.includes('=')) {
const match = content.match(/int\s+(\w+)\s*=\s*(.+?);/);
if (match) {
const [, varName, value] = match;
this.variables.set(varName, this.evaluateExpression(value));
debugLog('声明int变量 ' + varName + ' = ' + this.variables.get(varName));
return;
}
}
// bool类型变量声明
if (content.includes('bool ') && content.includes('=')) {
const match = content.match(/bool\s+(\w+)\s*=\s*(.+?);/);
if (match) {
const [, varName, value] = match;
this.variables.set(varName, this.evaluateExpression(value));
debugLog('声明bool变量 ' + varName + ' = ' + this.variables.get(varName));
return;
}
}
// Position类型变量声明
if (content.includes('Position ') && content.includes('=')) {
const match = content.match(/Position\s+(\w+)\s*=\s*(.+?);/);
if (match) {
const [, varName, value] = match;
const position = this.evaluateExpression(value);
this.variables.set(varName, position);
debugLog(`声明Position变量 ${varName}:`, position);
return;
}
}
// 复合赋值运算符 (+=, -=, *=, /=)
if (content.includes('+=')) {
const match = content.match(/(\w+)\s*\+=\s*(.+?);/);
@@ -1513,9 +1542,6 @@ export class CppCodeExecutor {
}
} else if (content.includes('cout')) {
// 解析复杂的cout输出语句
this.executeCoutStatement(content);
}
}
@@ -1553,6 +1579,8 @@ export class CppCodeExecutor {
let shouldContinue = false;
try {
await this.withVariableScope(async () => {
for (const bodyStatement of forStatement.body) {
if (this.shouldStop()) break;
@@ -1585,9 +1613,12 @@ export class CppCodeExecutor {
await this.executeStatement(adjustedStatement);
}
});
} finally {
this.currentExecutionContext = oldContext;
}
if (this.shouldStop()) break;
if (this.shouldStop() || shouldBreak) break;
// 如果遇到continue跳过执行增量直接进入下次循环
if (shouldContinue) {
@@ -1730,6 +1761,8 @@ export class CppCodeExecutor {
let shouldContinue = false;
try {
await this.withVariableScope(async () => {
for (const bodyStatement of whileStatement.body) {
if (this.shouldStop()) break;
@@ -1765,7 +1798,10 @@ export class CppCodeExecutor {
debugLog(`🔍 语句执行后重新检查条件: ${whileStatement.condition} = ${this.evaluateCondition(whileStatement.condition)}`);
}
});
} finally {
this.currentExecutionContext = oldContext;
}
if (shouldContinue) {
continue; // 跳过本次循环的剩余部分,重新开始
@@ -1804,6 +1840,11 @@ export class CppCodeExecutor {
throw new Error(`函数调用栈溢出: ${functionName} (可能存在无限递归)`);
}
// 所有实参先在调用者作用域求值,避免前一个形参覆盖后一个实参。
const argumentValues = callArguments.map(arg => this.evaluateExpression(arg));
const callerVariables = this.variables;
this.variables = new CppVariableScope(this.globalVariables);
// 将函数调用信息压入调用栈
this.callStack.push({
functionName: functionName,
@@ -1812,21 +1853,14 @@ export class CppCodeExecutor {
});
try {
// 最小改动:防递归数据污染,只备份会被覆盖的同名参数变量
const savedParams = new Map();
for (let i = 0; i < functionInfo.parameters.length; i++) {
const pName = functionInfo.parameters[i].name;
if (this.variables.has(pName)) savedParams.set(pName, this.variables.get(pName));
}
// 设置函数参数变量
for (let i = 0; i < functionInfo.parameters.length; i++) {
const param = functionInfo.parameters[i];
const argValue = this.evaluateExpression(callArguments[i]);
const argValue = argumentValues[i];
// 类型检查和转换
const convertedValue = this.convertArgumentToType(argValue, param.type);
this.variables.set(param.name, convertedValue);
this.variables.declare(param.name, convertedValue);
console.log(`📋 设置参数: ${param.name} (${param.type}) = ${convertedValue}`);
}
@@ -1859,19 +1893,10 @@ export class CppCodeExecutor {
if (e.message !== 'RETURN_INTERRUPT') throw e;
}
// 最小改动:仅恢复递归调用前的参数状态,保留全局变量的修改
for (let i = 0; i < functionInfo.parameters.length; i++) {
const pName = functionInfo.parameters[i].name;
if (savedParams.has(pName)) {
this.variables.set(pName, savedParams.get(pName));
} else {
this.variables.delete(pName);
}
}
console.log(`✅ 函数${functionName}执行完成`);
} finally {
this.variables = callerVariables;
// 从调用栈中弹出当前函数
const callInfo = this.callStack.pop();
const executionTime = Date.now() - callInfo.startTime;
@@ -2201,7 +2226,7 @@ export class CppCodeExecutor {
// 处理复杂的cout语句
executeCoutStatement(content) {
// 移除cout和分号获取输出内容
let coutContent = content.replace(/^\s*cout\s*/, '').replace(/;\s*$/, '');
const coutContent = content.replace(/^\s*(?:std\s*::\s*)?cout\s*/, '').replace(/;\s*$/, '');
// 按 << 分割
const parts = this.splitCoutParts(coutContent);
@@ -2211,14 +2236,14 @@ export class CppCodeExecutor {
for (const part of parts) {
const trimmedPart = part.trim();
if (trimmedPart === 'endl') {
if (/^(?:std\s*::\s*)?endl$/.test(trimmedPart)) {
output += '\n';
} else if (trimmedPart.startsWith('"') && trimmedPart.endsWith('"')) {
// 字符串字面量
output += trimmedPart.slice(1, -1);
output += this.decodeStringLiteral(trimmedPart);
} else if (trimmedPart.startsWith("'") && trimmedPart.endsWith("'")) {
// 字符字面量
output += trimmedPart.slice(1, -1);
output += this.decodeStringLiteral(trimmedPart);
} else if (trimmedPart.startsWith('(') && trimmedPart.endsWith(')')) {
// 括号表达式
const expr = trimmedPart.slice(1, -1);
@@ -2233,7 +2258,13 @@ export class CppCodeExecutor {
window.cppGameAPI.cmdPrint(output);
}
// 智能分割cout的各个部分
// 解码输出中的常用 C++ 转义字符。
decodeStringLiteral(literal) {
const escapes = {n: '\n', r: '\r', t: '\t', '0': '\0', '\\': '\\', '"': '"', "'": "'"};
return literal.slice(1, -1).replace(/\\([nrt0\\"'])/g, (match, char) => escapes[char]);
}
// 按字符串和括号边界分割输出流。
splitCoutParts(content) {
const parts = [];
let current = '';
@@ -2245,6 +2276,12 @@ export class CppCodeExecutor {
const char = content[i];
const nextChar = content[i + 1];
if (inString && char === '\\' && nextChar !== undefined) {
current += char + nextChar;
i++;
continue;
}
if (!inString && inParens === 0 && char === '<' && nextChar === '<') {
// 遇到 << 分隔符
if (current.trim()) {
@@ -2284,6 +2321,11 @@ export class CppCodeExecutor {
expr = expr.trim().replace(/;$/, '');
debugLog(`🔍 计算表达式: ${expr}`);
// 字符串中的 ++、-- 和函数名都是文本,不参与表达式运算。
if (/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')$/.test(expr)) {
return this.decodeStringLiteral(expr);
}
// 布尔常量
if (expr === 'true') {
debugLog(`🔍 布尔常量: true = 1`);
@@ -2338,7 +2380,7 @@ export class CppCodeExecutor {
const newValue = currentValue + 1;
this.variables.set(varName, newValue);
console.log(`🔄 自增操作: ${expr} (${currentValue} -> ${newValue})`);
return newValue;
return expr.startsWith('++') ? newValue : currentValue;
}
if (expr.includes('--')) {
@@ -2356,7 +2398,7 @@ export class CppCodeExecutor {
const newValue = currentValue - 1;
this.variables.set(varName, newValue);
console.log(`🔄 自减操作: ${expr} (${currentValue} -> ${newValue})`);
return newValue;
return expr.startsWith('--') ? newValue : currentValue;
}
// 游戏API函数调用
@@ -2386,12 +2428,6 @@ export class CppCodeExecutor {
return position;
}
// 整个表达式是字符串字面量时才当字符串;含引号的 API 调用走后面的算术解析
if ((expr.startsWith('"') && expr.endsWith('"')) ||
(expr.startsWith("'") && expr.endsWith("'"))) {
return expr.slice(1, -1);
}
return this.parseComplexExpression(expr);
}

View File

@@ -645,17 +645,21 @@ const StageWrapperCppClang = () => {
textStr = String(text);
}
window.cmd_print_text += textStr + '\n';
// cout 自己决定何时换行,连续输出应保持在同一行。
window.cmd_print_text += textStr;
const lines = window.cmd_print_text.split('\n');
if (lines.length > 20) {
window.cmd_print_text = lines.slice(lines.length - 20).join('\n');
}
setCompilationOutput(prev => prev + textStr);
if (window.unityInstance) {
try {
window.unityInstance.SendMessage("UIMain", "SetText", window.cmd_print_text);
} catch (error) {
console.warn('游戏画面输出失败,文本仍可在运行输出中查看', error);
}
}
setCompilationOutput(prev => prev + textStr + "\n");
},
player_data: (direction) => {
@@ -795,6 +799,7 @@ const StageWrapperCppClang = () => {
setCompilationOutput(prev => prev + "语法检查通过!\n");
// 重置停止标志
window.cmd_print_text = '';
window.isScriptRunOver = false;
setIsRunning(true);
setOverlayActive(true);
@@ -969,7 +974,7 @@ const StageWrapperCppClang = () => {
<div className={styles.errorModalOverlay}>
<div className={styles.errorModal}>
<div className={styles.errorModalHeader}>
<h3>编译信息</h3>
<h3>运行输出</h3>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={() => setCompilationOutput('')}
@@ -1121,6 +1126,13 @@ const StageWrapperCppClang = () => {
<div className={styles.apiDrawer}>
<div className={styles.apiDrawerHeader}>
<h3>🔧 C++ API参考</h3>
<button
type="button"
className={styles.apiDrawerInsertButton}
onClick={() => setCompilationModalVisible(true)}
>
查看输出
</button>
</div>
<div className={styles.apiDrawerContent}>

View File

@@ -11,7 +11,6 @@ import RankBadgeGenerator from '../../playground/rank-badge-generator.js';
// const friendToggle = new FriendToggle();
window.RankBadgeGenerator = RankBadgeGenerator;
window.CONFIG = CONFIG;
import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from '../../playground/devtool-control.js';
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
import { debug } from 'scratch-vm/src/util/log.js';
@@ -561,7 +560,7 @@ const UnityComponent = function (props) {
useEffect(() => {
if (CONFIG.disabledev) {
disableDevtool({
initDisableDevtool({
ondevtoolopen: () => {
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗

View File

@@ -11,7 +11,6 @@ import RankBadgeGenerator from '../../playground/rank-badge-generator.js';
// const friendToggle = new FriendToggle();
window.RankBadgeGenerator = RankBadgeGenerator;
window.CONFIG = CONFIG;
import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from '../../playground/devtool-control.js';
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
import { debug } from 'scratch-vm/src/util/log.js';
@@ -755,7 +754,7 @@ const UnityComponent = function (props) {
useEffect(() => {
if (CONFIG.disabledev) {
disableDevtool({
initDisableDevtool({
ondevtoolopen: () => {
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗

View File

@@ -1325,7 +1325,7 @@
<h2 class="cta-title">灵丌编程001CODE城市合伙人招募中</h2>
</div>
<div class="cta-grid">
<a href="mailto:contact@001Code.com" class="contact-card">
<a href="mailto:support@shxshf.com" class="contact-card">
<div class="icon-box">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="4" width="20" height="16" rx="2"></rect>
@@ -1334,11 +1334,11 @@
</div>
<div class="card-info">
<span class="card-label">邮箱地址</span>
<span class="card-value">wangshenxing@shxshf.com</span>
<span class="card-value">support@shxshf.com</span>
</div>
</a>
<button class="contact-card" onclick="navigator.clipboard.writeText('17380126742')">
<button class="contact-card" onclick="navigator.clipboard.writeText('400-659-1659')">
<div class="icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -1348,7 +1348,7 @@
</div>
<div class="card-info">
<span class="card-label">咨询热线</span>
<span class="card-value">135-2066-2111</span>
<span class="card-value">400-659-1659</span>
</div>
</button>
</div>

View File

@@ -1,6 +1,6 @@
// 导入配置信息
import CONFIG from './config.js';
import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from './devtool-control.js';
// 获取 URL 参数的函数
@@ -166,7 +166,7 @@ function showPlatformGuide() {
document.addEventListener('DOMContentLoaded', function () {
if (CONFIG.disabledev) {
disableDevtool({
initDisableDevtool({
ondevtoolopen: () => {
// window.location.href = '/index.html';
// window.location.href = localStorage.getItem('returnUrl');

View File

@@ -1,11 +1,11 @@
import CONFIG from './config.js';
import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from './devtool-control.js';
let gamelvdata = {};
document.addEventListener('DOMContentLoaded', function () {
if (CONFIG.disabledev) {
disableDevtool({
initDisableDevtool({
ondevtoolopen: () => {
},
interval: 500,

View File

@@ -183,7 +183,7 @@ const CONFIG = {
disabledev: false,
disabledev: true,
usecdn: false,
lvlock: false,
lvlock_normal_learn: !(typeof localStorage !== 'undefined' &&

View File

@@ -2,8 +2,10 @@ import disableDevtool from 'disable-devtool';
import CONFIG from './config.js';
const STOP_DEV_KEY = 'stopDev';
let stoppedForPage = false;
function isStopped() {
if (stoppedForPage) return true;
try {
return sessionStorage.getItem(STOP_DEV_KEY) === '1';
} catch (e) {
@@ -16,6 +18,8 @@ function isStopped() {
* 可在控制台直接执行stopDev()
*/
export function stopDev() {
// 即使浏览器拒绝写入 sessionStorage当前页面仍应立即停止检测。
stoppedForPage = true;
try {
sessionStorage.setItem(STOP_DEV_KEY, '1');
} catch (e) {
@@ -29,6 +33,8 @@ export function initDisableDevtool(options = {}) {
if (isStopped()) {
disableDevtool.isSuspend = true;
// F12 已打开时,库可能在初始化期间立即触发检测,不能继续初始化。
return;
}
if (!CONFIG.disabledev) {
@@ -36,14 +42,19 @@ export function initDisableDevtool(options = {}) {
}
const userIgnore = options.ignore;
disableDevtool({
ondevtoolopen: () => {
const onDevtoolOpen = options.ondevtoolopen || (() => {
window.location.href = localStorage.getItem('returnUrl');
},
});
disableDevtool({
interval: 500,
clearLog: true,
disableMenu: true,
...options,
ondevtoolopen: (...args) => {
// stopDev() 之后,已排队的检测回调也不能清空代码或跳转。
if (isStopped()) return;
return onDevtoolOpen(...args);
},
ignore: () => {
if (isStopped()) {
return true;