Compare commits
3 Commits
d83654837a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb78d8bf5c | ||
|
|
c4641aee19 | ||
|
|
782f76170f |
BIN
scratch-gui/competitio_login/assets/gongan-beian.png
Normal file
BIN
scratch-gui/competitio_login/assets/gongan-beian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -384,7 +384,7 @@
|
||||
<footer>
|
||||
<div class="footer-line" role="presentation"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="assets/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
71
scratch-gui/scripts/diagnose-cpp.cjs
Normal file
71
scratch-gui/scripts/diagnose-cpp.cjs
Normal 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;});
|
||||
@@ -21,7 +21,7 @@ function getConfigForEnv(env) {
|
||||
case 'test':
|
||||
return { url: 'https://test-service.001code.cn', disabledev: false };
|
||||
case 'prod':
|
||||
return { url: 'https://service.001code.cn', disabledev: true };
|
||||
return { url: 'https://service.001code.com', disabledev: true };
|
||||
case 'start':
|
||||
default:
|
||||
return { url: 'http://127.0.0.1:8000', disabledev: false };
|
||||
|
||||
139
scratch-gui/scripts/test-cpp-regressions.cjs
Normal file
139
scratch-gui/scripts/test-cpp-regressions.cjs
Normal file
@@ -0,0 +1,139 @@
|
||||
// Regression tests for variable scopes and return control flow in the editor interpreter.
|
||||
// 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, createCppExecutor} = 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;
|
||||
}`;
|
||||
async function run(code, expected, configure) {
|
||||
const moves = [];
|
||||
const turns = [];
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {
|
||||
playerMove: async n => moves.push(n),
|
||||
turnLeft: async n => turns.push(n),
|
||||
cmdPrint: () => {}
|
||||
}};
|
||||
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));
|
||||
const exitCode = await executor.run();
|
||||
if (Object.prototype.hasOwnProperty.call(expected, 'exitCode')) assert.strictEqual(exitCode, expected.exitCode);
|
||||
if (expected.moves) assert.deepStrictEqual(moves, expected.moves);
|
||||
if (expected.turns) assert.deepStrictEqual(turns, expected.turns);
|
||||
assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution');
|
||||
assert.strictEqual(executor.callStack.length, 0);
|
||||
assert.strictEqual(executor.currentExecutionContext, null);
|
||||
assert.strictEqual(executor.isRunning, false);
|
||||
return executor;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
['screenshot: main return 1 is a normal exit', main('playerMove(4);\nreturn 1;'), {moves: [4], exitCode: 1}],
|
||||
['return 1 skips remaining main statements', main('return 1;\nplayerMove(9);'), {moves: [], exitCode: 1}],
|
||||
['return 0 also stops main early', main('return 0;\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
||||
['negative exit code', main('return -1;'), {moves: [], exitCode: -1}],
|
||||
['parenthesized return expression', main('int status = 2;\nreturn (status + 1);'), {moves: [], exitCode: 3}],
|
||||
['main fallthrough returns zero', main('playerMove(1);'), {moves: [1], exitCode: 0}],
|
||||
['return from for loop exits main', main('for (int i = 0; i < 3; i++) {\nplayerMove(i);\nreturn 1;\n}\nplayerMove(9);'), {moves: [0], exitCode: 1}],
|
||||
['return zero inside loop is not ignored', main('for (int i = 0; i < 3; i++) {\nreturn 0;\n}\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
||||
['return through while and if exits main', main('while (true) {\nif (true) {\nreturn 2;\n}\nplayerMove(9);\n}\nplayerMove(8);'), {moves: [], exitCode: 2}],
|
||||
['function return 0 skips only its own remaining statements', 'int f() {\nreturn 0;\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);\nreturn 1;'), {moves: [1], exitCode: 1}],
|
||||
['function return inside loop resumes caller', 'int f() {\nfor (int i = 0; i < 3; i++) {\nreturn 1;\n}\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);'), {moves: [1], exitCode: 0}],
|
||||
['identifier starting with return is not a return statement', main('int returnValue = 1;\nreturnValue = 2;\nplayerMove(returnValue);\nreturn 0;'), {moves: [2], exitCode: 0}],
|
||||
['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}],
|
||||
['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]}],
|
||||
['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]}],
|
||||
];
|
||||
|
||||
(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}`);
|
||||
}
|
||||
const strings = new CppCodeExecutor('', null);
|
||||
await strings.executeSimpleStatement('string msg = "outer";');
|
||||
await strings.withVariableScope(async () => {
|
||||
await strings.executeSimpleStatement('string msg = "inner";');
|
||||
assert.strictEqual(strings.variables.get('msg'), 'inner');
|
||||
});
|
||||
assert.strictEqual(strings.variables.get('msg'), 'outer');
|
||||
console.log('PASS string block shadowing');
|
||||
|
||||
// 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'}`);
|
||||
}
|
||||
|
||||
const highlighted = [];
|
||||
context.window = {isScriptRunOver: false, setHighlightedLine: line => highlighted.push(line)};
|
||||
const manager = createCppExecutor();
|
||||
for (const status of [1, 0]) {
|
||||
assert.strictEqual(await manager.execute(main(`return ${status};`)), status);
|
||||
assert.strictEqual(manager.currentExecutor, null);
|
||||
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
||||
}
|
||||
console.log('PASS editor executor manager resolves nonzero exit and can run again');
|
||||
|
||||
context.window.cppGameAPI = {playerMove: async () => {throw new Error('RETURN_INTERRUPT');}};
|
||||
await assert.rejects(manager.execute('void f() {\nplayerMove(1);\n}\n' + main('f();')), /RETURN_INTERRUPT/);
|
||||
assert.strictEqual(manager.currentExecutor, null);
|
||||
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
||||
console.log('PASS real game errors are not mistaken for a return signal');
|
||||
|
||||
console.log(`${cases.length + 6} regression checks passed`);
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
166
scratch-gui/scripts/test-devtool-control.cjs
Normal file
166
scratch-gui/scripts/test-devtool-control.cjs
Normal 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`);
|
||||
@@ -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 事件监听,以防止弹出确认离开弹窗
|
||||
|
||||
@@ -510,10 +510,57 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
// return 是跨越循环/条件块的控制信号,只在所属函数边界消费。
|
||||
// 使用独立类型,避免吞掉恰好带有相同消息的真实运行错误。
|
||||
class CppReturnSignal {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
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 +574,7 @@ export class CppCodeExecutor {
|
||||
try {
|
||||
this.isRunning = true;
|
||||
this.parseCode();
|
||||
await this.executeMain();
|
||||
return await this.withVariableScope(() => this.executeMain(), this.globalVariables);
|
||||
} catch (error) {
|
||||
console.error('C++执行错误:', error);
|
||||
throw error;
|
||||
@@ -544,6 +591,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++代码,支持自定义函数');
|
||||
|
||||
@@ -765,12 +822,26 @@ export class CppCodeExecutor {
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i].trim();
|
||||
if (!line || line.startsWith('//') || line === '{' || line === '}' || line === 'return 0;' ||
|
||||
if (!line || line.startsWith('//') || line === '{' || line === '}' ||
|
||||
line.match(/^\s*}\s*else\s*\{\s*$/) || line.startsWith('else')) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// return 不能标记成 continue,否则循环会直接跳过它。
|
||||
if (/^return\b/.test(line)) {
|
||||
statements.push({
|
||||
type: 'control',
|
||||
subtype: 'return',
|
||||
content: line,
|
||||
lineNumber: isMainFunction ? this.getActualLineNumber(i) : i + 1,
|
||||
originalContent: line,
|
||||
contextPath: isMainFunction ? 'main' : 'function'
|
||||
});
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否是自定义函数调用(包括带括号的实参,如 f((n + 1));)
|
||||
const customCall = this.parseCustomFunctionCall(line);
|
||||
if (customCall) {
|
||||
@@ -799,8 +870,8 @@ export class CppCodeExecutor {
|
||||
statements.push(whileLoop);
|
||||
i = whileLoop.endIndex;
|
||||
} else {
|
||||
// 检查是否是break或continue或return语句
|
||||
if (line === 'break;' || line === 'continue;' || line.startsWith('return')) {
|
||||
// 检查是否是break或continue语句
|
||||
if (line === 'break;' || line === 'continue;') {
|
||||
statements.push({
|
||||
type: 'control',
|
||||
subtype: line === 'break;' ? 'break' : 'continue',
|
||||
@@ -1278,10 +1349,17 @@ export class CppCodeExecutor {
|
||||
}
|
||||
|
||||
async executeMain() {
|
||||
for (const statement of this.statements) {
|
||||
if (this.shouldStop()) break;
|
||||
await this.executeStatement(statement);
|
||||
try {
|
||||
for (const statement of this.statements) {
|
||||
if (this.shouldStop()) break;
|
||||
await this.executeStatement(statement);
|
||||
}
|
||||
} catch (signal) {
|
||||
if (!(signal instanceof CppReturnSignal)) throw signal;
|
||||
return signal.value;
|
||||
}
|
||||
// main 正常执行到末尾,相当于 return 0。
|
||||
return 0;
|
||||
}
|
||||
|
||||
async executeStatement(statement) {
|
||||
@@ -1300,10 +1378,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);
|
||||
@@ -1312,8 +1390,11 @@ export class CppCodeExecutor {
|
||||
await this.executeFunctionCall(statement);
|
||||
break;
|
||||
case 'control':
|
||||
if (statement.content.startsWith('return')) {
|
||||
throw new Error('RETURN_INTERRUPT');
|
||||
if (statement.subtype === 'return') {
|
||||
const match = statement.content.match(/^return\b([\s\S]*?);(?:\s*\/\/.*)?$/);
|
||||
if (!match) throw new Error(`无法解析return语句: ${statement.content}`);
|
||||
const expression = match[1].trim();
|
||||
throw new CppReturnSignal(expression ? this.evaluateExpression(expression) : undefined);
|
||||
}
|
||||
// break和continue在各自的循环中处理,这里只记录
|
||||
debugLog(`🔄 控制语句: ${statement.subtype}`);
|
||||
@@ -1324,6 +1405,14 @@ export class CppCodeExecutor {
|
||||
async executeSimpleStatement(content) {
|
||||
debugLog(`🔍 执行简单语句: ${content}`);
|
||||
|
||||
// 声明必须留在当前作用域,不能按普通赋值覆盖外层同名变量。
|
||||
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 +1444,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*(.+?);/);
|
||||
@@ -1553,41 +1608,46 @@ export class CppCodeExecutor {
|
||||
|
||||
let shouldContinue = false;
|
||||
|
||||
for (const bodyStatement of forStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
try {
|
||||
await this.withVariableScope(async () => {
|
||||
for (const bodyStatement of forStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出for循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出for循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
}
|
||||
|
||||
});
|
||||
} finally {
|
||||
this.currentExecutionContext = oldContext;
|
||||
}
|
||||
|
||||
this.currentExecutionContext = oldContext;
|
||||
|
||||
if (this.shouldStop()) break;
|
||||
if (this.shouldStop() || shouldBreak) break;
|
||||
|
||||
// 如果遇到continue,跳过执行增量,直接进入下次循环
|
||||
if (shouldContinue) {
|
||||
@@ -1730,43 +1790,48 @@ export class CppCodeExecutor {
|
||||
|
||||
let shouldContinue = false;
|
||||
|
||||
for (const bodyStatement of whileStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
try {
|
||||
await this.withVariableScope(async () => {
|
||||
for (const bodyStatement of whileStatement.body) {
|
||||
if (this.shouldStop()) break;
|
||||
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出while循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
// 检查是否是break或continue语句
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'break;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'break')) {
|
||||
debugLog('🔄 遇到break语句,退出while循环');
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
if ((bodyStatement.content && bodyStatement.content.trim() === 'continue;') ||
|
||||
(bodyStatement.type === 'control' && bodyStatement.subtype === 'continue')) {
|
||||
debugLog('🔄 遇到continue语句,跳过本次循环剩余部分');
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
// 使用预先计算好的精确行号,如果没有则使用调整后的行号
|
||||
let adjustedStatement;
|
||||
if (bodyStatement.exactLineNumber) {
|
||||
adjustedStatement = {
|
||||
...bodyStatement,
|
||||
lineNumber: bodyStatement.exactLineNumber
|
||||
};
|
||||
debugLog(`🎯 使用精确行号: "${bodyStatement.content}" -> 第${bodyStatement.exactLineNumber}行`);
|
||||
} else {
|
||||
adjustedStatement = this.adjustStatementLineNumber.call(this, bodyStatement);
|
||||
}
|
||||
await this.executeStatement(adjustedStatement);
|
||||
|
||||
// 在每个语句执行后重新检查循环条件(特别重要!)
|
||||
debugLog(`🔍 语句执行后重新检查条件: ${whileStatement.condition} = ${this.evaluateCondition(whileStatement.condition)}`);
|
||||
// 在每个语句执行后重新检查循环条件(特别重要!)
|
||||
debugLog(`🔍 语句执行后重新检查条件: ${whileStatement.condition} = ${this.evaluateCondition(whileStatement.condition)}`);
|
||||
}
|
||||
|
||||
});
|
||||
} finally {
|
||||
this.currentExecutionContext = oldContext;
|
||||
}
|
||||
|
||||
this.currentExecutionContext = oldContext;
|
||||
|
||||
if (shouldContinue) {
|
||||
continue; // 跳过本次循环的剩余部分,重新开始
|
||||
}
|
||||
@@ -1804,6 +1869,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 +1882,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}`);
|
||||
}
|
||||
@@ -1856,22 +1919,14 @@ export class CppCodeExecutor {
|
||||
await this.executeStatement(adjustedStatement);
|
||||
}
|
||||
} catch (e) {
|
||||
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);
|
||||
}
|
||||
if (!(e instanceof CppReturnSignal)) throw e;
|
||||
return e.value;
|
||||
}
|
||||
|
||||
console.log(`✅ 函数${functionName}执行完成`);
|
||||
|
||||
} finally {
|
||||
this.variables = callerVariables;
|
||||
// 从调用栈中弹出当前函数
|
||||
const callInfo = this.callStack.pop();
|
||||
const executionTime = Date.now() - callInfo.startTime;
|
||||
@@ -2697,7 +2752,7 @@ export const createCppExecutor = () => {
|
||||
const executor = new CppCodeExecutor(code, window.setHighlightedLine);
|
||||
this.currentExecutor = executor;
|
||||
try {
|
||||
await executor.run();
|
||||
return await executor.run();
|
||||
} finally {
|
||||
this.currentExecutor = null;
|
||||
}
|
||||
|
||||
@@ -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 事件监听,以防止弹出确认离开弹窗
|
||||
|
||||
@@ -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 事件监听,以防止弹出确认离开弹窗
|
||||
|
||||
@@ -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>
|
||||
@@ -1358,7 +1358,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -1537,7 +1537,7 @@
|
||||
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<footer>
|
||||
<div class="footer-line"></div>
|
||||
<p class="footer-text">ICP备案号:<a style="color:#478ac9 ;" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a style="color:#478ac9 ;" href="https://beian.mps.gov.cn/" target="_blank">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -917,7 +917,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
<footer>
|
||||
<div class="footer-line"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// config.js
|
||||
|
||||
// const CONFIG = {
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// version : "2025010301",
|
||||
// unitycdndir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826',
|
||||
// unitycdnbuilddir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826/Build',
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
// // 可以根据需要添加其他配置
|
||||
|
||||
// disabledev: false,
|
||||
// disabledev: true,
|
||||
// usecdn :true, //资源走CDN
|
||||
// lvlock :false, //关卡5关解锁,下一关加锁
|
||||
// lvlock_normal_learn :true, //常规关起锁
|
||||
@@ -78,12 +78,12 @@
|
||||
|
||||
|
||||
// const CONFIG = {
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// // DataServerBaseUrl: "https://service.001code.com",
|
||||
// version : "2025011001",
|
||||
// unitycdndir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826',
|
||||
// unitycdnbuilddir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826/Build',
|
||||
@@ -91,8 +91,8 @@
|
||||
|
||||
// // 可以根据需要添加其他配置
|
||||
|
||||
// // disabledev: false,
|
||||
// disabledev: false,
|
||||
// // disabledev: true,
|
||||
// disabledev: true,
|
||||
// usecdn :true, //资源走CDN
|
||||
// lvlock :false, //关卡5关解锁,下一关加锁
|
||||
// lvlock_normal_learn : !(typeof localStorage !== 'undefined' &&
|
||||
@@ -157,12 +157,12 @@
|
||||
|
||||
|
||||
const CONFIG = {
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
DataServerBaseUrl: "https://service.001code.cn",
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://test-service.001code.cn",
|
||||
// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
DataServerBaseUrl: "https://service.001code.com",
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
// DataServerBaseUrl: "https://service.001code.com",
|
||||
|
||||
version: "2026071701",
|
||||
/** OSS 真实地址(生产环境浏览器直连) */
|
||||
@@ -183,7 +183,7 @@ const CONFIG = {
|
||||
|
||||
|
||||
|
||||
disabledev: false,
|
||||
disabledev: true,
|
||||
usecdn: false,
|
||||
lvlock: false,
|
||||
lvlock_normal_learn: !(typeof localStorage !== 'undefined' &&
|
||||
|
||||
@@ -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;
|
||||
const onDevtoolOpen = options.ondevtoolopen || (() => {
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
});
|
||||
disableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
},
|
||||
interval: 500,
|
||||
clearLog: true,
|
||||
disableMenu: true,
|
||||
...options,
|
||||
ondevtoolopen: (...args) => {
|
||||
// stopDev() 之后,已排队的检测回调也不能清空代码或跳转。
|
||||
if (isStopped()) return;
|
||||
return onDevtoolOpen(...args);
|
||||
},
|
||||
ignore: () => {
|
||||
if (isStopped()) {
|
||||
return true;
|
||||
|
||||
@@ -1759,7 +1759,7 @@ top: 2032px; */
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<footer class="site-footer">
|
||||
<div class="footer-divider"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p></p>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -94,8 +94,7 @@
|
||||
<div class="footer-inner">
|
||||
<div class="footer-text" ">
|
||||
ICP备案号:<a style=" color: #387cbd;" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">沪ICP备2026042325号</a>
|
||||
<!-- <span> 公安备案号:<a style=" color: #387cbd;" href="https://beian.mps.gov.cn/"
|
||||
target="_blank">川公网安备51011202000980号</a></span> -->
|
||||
<span>公安备案号:<a style="color:#387cbd;" href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a></span>
|
||||
<span>© 灵丌编程(001CODE.COM)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -361,7 +361,7 @@
|
||||
<footer class="footer" aria-label="页脚">
|
||||
<div class="footer-line"></div>
|
||||
<div class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)
|
||||
<span class="qr" tabindex="0">
|
||||
<img class="qr-icon" src="../images/qrcode_gery.svg" alt="公众号小图标" />
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
<div class="footer-inner">
|
||||
<div class="footer-text">
|
||||
ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank">川公网安备51011202000980号</a> --> © 灵丌编程(001CODE.COM)
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank"><img src="../images/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a> © 灵丌编程(001CODE.COM)
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
BIN
scratch-gui/static/images/gongan-beian.png
Normal file
BIN
scratch-gui/static/images/gongan-beian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user