mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-02 11:44:28 +08:00
feat(game): 添加游戏内容配置系统和VS Code插件支持
- 实现基于YAML的配置源文件和JSON Schema结构验证 - 提供运行时只读查询和Source Generator代码生成 - 添加VS Code插件实现配置浏览、编辑和轻量校验功能 - 支持开发期热重载和跨表引用校验 - 实现批量编辑和表单预览功能
This commit is contained in:
parent
15761c6677
commit
5b8099cd98
@ -169,8 +169,9 @@ var hotReload = loader.EnableHotReload(
|
||||
- 打开匹配的 schema 文件
|
||||
- 对必填字段、未知顶层字段、基础标量类型和标量数组元素做轻量校验
|
||||
- 对顶层标量字段和顶层标量数组提供轻量表单入口
|
||||
- 对同一配置域内的多份 YAML 文件执行批量字段更新
|
||||
|
||||
当前仍建议把复杂数组、嵌套对象和批量修改放在 raw YAML 中完成。
|
||||
当前批量编辑入口适合对同域文件统一改动顶层标量字段和顶层标量数组;复杂数组、嵌套对象仍建议放在 raw YAML 中完成。
|
||||
|
||||
## 当前限制
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ Minimal VS Code extension scaffold for the GFramework AI-First config workflow.
|
||||
- Open matching schema files from `schemas/`
|
||||
- Run lightweight schema validation for required fields, unknown top-level fields, scalar types, and scalar array items
|
||||
- Open a lightweight form preview for top-level scalar fields and top-level scalar arrays
|
||||
- Batch edit one config domain across multiple files for top-level scalar and scalar-array fields
|
||||
|
||||
## Validation Coverage
|
||||
|
||||
@ -32,7 +33,7 @@ node --test ./test/*.test.js
|
||||
|
||||
- Multi-root workspaces use the first workspace folder
|
||||
- Validation only covers a minimal subset of JSON Schema
|
||||
- Form editing currently supports top-level scalar fields and top-level scalar arrays
|
||||
- Form and batch editing currently support top-level scalar fields and top-level scalar arrays
|
||||
- Nested objects and complex arrays should still be edited in raw YAML
|
||||
|
||||
## Workspace Settings
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"onCommand:gframeworkConfig.openRaw",
|
||||
"onCommand:gframeworkConfig.openSchema",
|
||||
"onCommand:gframeworkConfig.openFormPreview",
|
||||
"onCommand:gframeworkConfig.batchEditDomain",
|
||||
"onCommand:gframeworkConfig.validateAll"
|
||||
],
|
||||
"main": "./src/extension.js",
|
||||
@ -49,6 +50,10 @@
|
||||
"command": "gframeworkConfig.openFormPreview",
|
||||
"title": "GFramework Config: Open Form Preview"
|
||||
},
|
||||
{
|
||||
"command": "gframeworkConfig.batchEditDomain",
|
||||
"title": "GFramework Config: Batch Edit Domain"
|
||||
},
|
||||
{
|
||||
"command": "gframeworkConfig.validateAll",
|
||||
"title": "GFramework Config: Validate All"
|
||||
@ -82,6 +87,11 @@
|
||||
"command": "gframeworkConfig.openFormPreview",
|
||||
"when": "view == gframeworkConfigExplorer && viewItem == gframeworkConfigFile",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "gframeworkConfig.batchEditDomain",
|
||||
"when": "view == gframeworkConfigExplorer && viewItem == domain",
|
||||
"group": "navigation"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -41,6 +41,44 @@ function parseSchemaContent(content) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect top-level schema fields that the current tooling can edit in bulk.
|
||||
* The bulk editor intentionally stays aligned with the lightweight form editor:
|
||||
* top-level scalars and scalar arrays are supported, while nested objects and
|
||||
* complex array items remain raw-YAML-only.
|
||||
*
|
||||
* @param {{required: string[], properties: Record<string, {type: string, itemType?: string}>}} schemaInfo Parsed schema info.
|
||||
* @returns {Array<{key: string, type: string, itemType?: string, inputKind: "scalar" | "array", required: boolean}>} Editable field descriptors.
|
||||
*/
|
||||
function getEditableSchemaFields(schemaInfo) {
|
||||
const editableFields = [];
|
||||
const requiredSet = new Set(Array.isArray(schemaInfo.required) ? schemaInfo.required : []);
|
||||
|
||||
for (const [key, property] of Object.entries(schemaInfo.properties || {})) {
|
||||
if (isEditableScalarType(property.type)) {
|
||||
editableFields.push({
|
||||
key,
|
||||
type: property.type,
|
||||
inputKind: "scalar",
|
||||
required: requiredSet.has(key)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.type === "array" && isEditableScalarType(property.itemType || "")) {
|
||||
editableFields.push({
|
||||
key,
|
||||
type: property.type,
|
||||
itemType: property.itemType,
|
||||
inputKind: "array",
|
||||
required: requiredSet.has(key)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return editableFields.sort((left, right) => left.key.localeCompare(right.key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a minimal top-level YAML structure for config validation and form
|
||||
* preview. This parser intentionally focuses on the repository's current
|
||||
@ -203,6 +241,20 @@ function validateParsedConfig(schemaInfo, parsedYaml) {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the current schema type can be edited through the
|
||||
* lightweight form or batch-edit tooling.
|
||||
*
|
||||
* @param {string} schemaType Schema type.
|
||||
* @returns {boolean} True when the type is supported by the lightweight editors.
|
||||
*/
|
||||
function isEditableScalarType(schemaType) {
|
||||
return schemaType === "string" ||
|
||||
schemaType === "integer" ||
|
||||
schemaType === "number" ||
|
||||
schemaType === "boolean";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a scalar value matches a minimal schema type.
|
||||
*
|
||||
@ -308,6 +360,19 @@ function applyScalarUpdates(originalYaml, updates) {
|
||||
return applyFormUpdates(originalYaml, {scalars: updates});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the batch editor's comma-separated array input.
|
||||
*
|
||||
* @param {string} value Raw input value.
|
||||
* @returns {string[]} Parsed array items.
|
||||
*/
|
||||
function parseBatchArrayValue(value) {
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a scalar value for YAML output.
|
||||
*
|
||||
@ -455,7 +520,9 @@ module.exports = {
|
||||
applyScalarUpdates,
|
||||
findTopLevelBlocks,
|
||||
formatYamlScalar,
|
||||
getEditableSchemaFields,
|
||||
isScalarCompatible,
|
||||
parseBatchArrayValue,
|
||||
parseSchemaContent,
|
||||
parseTopLevelYaml,
|
||||
unquoteScalar,
|
||||
|
||||
@ -3,6 +3,8 @@ const path = require("path");
|
||||
const vscode = require("vscode");
|
||||
const {
|
||||
applyFormUpdates,
|
||||
getEditableSchemaFields,
|
||||
parseBatchArrayValue,
|
||||
parseSchemaContent,
|
||||
parseTopLevelYaml,
|
||||
unquoteScalar,
|
||||
@ -36,6 +38,9 @@ function activate(context) {
|
||||
vscode.commands.registerCommand("gframeworkConfig.openFormPreview", async (item) => {
|
||||
await openFormPreview(item, diagnostics);
|
||||
}),
|
||||
vscode.commands.registerCommand("gframeworkConfig.batchEditDomain", async (item) => {
|
||||
await openBatchEdit(item, diagnostics, provider);
|
||||
}),
|
||||
vscode.commands.registerCommand("gframeworkConfig.validateAll", async () => {
|
||||
await validateAllConfigs(diagnostics);
|
||||
}),
|
||||
@ -364,6 +369,142 @@ async function validateConfigFile(configUri, diagnostics) {
|
||||
diagnostics.set(configUri, fileDiagnostics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a minimal batch editor for one config domain.
|
||||
* The workflow intentionally focuses on one schema-bound directory at a time
|
||||
* so designers can apply the same top-level scalar or scalar-array values
|
||||
* across multiple files without dropping down to repetitive raw-YAML edits.
|
||||
*
|
||||
* @param {ConfigTreeItem | { kind?: string, resourceUri?: vscode.Uri }} item Tree item.
|
||||
* @param {vscode.DiagnosticCollection} diagnostics Diagnostic collection.
|
||||
* @param {ConfigTreeDataProvider} provider Tree provider.
|
||||
* @returns {Promise<void>} Async task.
|
||||
*/
|
||||
async function openBatchEdit(item, diagnostics, provider) {
|
||||
const workspaceRoot = getWorkspaceRoot();
|
||||
const domainUri = item && item.resourceUri;
|
||||
if (!workspaceRoot || !domainUri || item.kind !== "domain") {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileItems = fs.readdirSync(domainUri.fsPath, {withFileTypes: true})
|
||||
.filter((entry) => entry.isFile() && isYamlPath(entry.name))
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
.map((entry) => {
|
||||
const fileUri = vscode.Uri.joinPath(domainUri, entry.name);
|
||||
return {
|
||||
label: entry.name,
|
||||
description: path.relative(workspaceRoot.uri.fsPath, fileUri.fsPath),
|
||||
fileUri,
|
||||
picked: true
|
||||
};
|
||||
});
|
||||
|
||||
if (fileItems.length === 0) {
|
||||
void vscode.window.showWarningMessage("No YAML config files were found in the selected domain.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFiles = await vscode.window.showQuickPick(fileItems, {
|
||||
canPickMany: true,
|
||||
title: `Batch Edit: ${path.basename(domainUri.fsPath)}`,
|
||||
placeHolder: "Select the config files to update."
|
||||
});
|
||||
if (!selectedFiles || selectedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaInfo = await loadSchemaInfoForConfig(selectedFiles[0].fileUri, workspaceRoot);
|
||||
if (!schemaInfo.exists) {
|
||||
void vscode.window.showWarningMessage("Batch edit requires a matching schema file for the selected domain.");
|
||||
return;
|
||||
}
|
||||
|
||||
const editableFields = getEditableSchemaFields(schemaInfo);
|
||||
if (editableFields.length === 0) {
|
||||
void vscode.window.showWarningMessage(
|
||||
"No top-level scalar or scalar-array fields were found in the matching schema.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFields = await vscode.window.showQuickPick(
|
||||
editableFields.map((field) => ({
|
||||
label: field.key,
|
||||
description: field.inputKind === "array"
|
||||
? `array<${field.itemType}>`
|
||||
: field.type,
|
||||
detail: field.required ? "required" : undefined,
|
||||
field
|
||||
})),
|
||||
{
|
||||
canPickMany: true,
|
||||
title: `Batch Edit Fields: ${path.basename(domainUri.fsPath)}`,
|
||||
placeHolder: "Select the fields to apply across the chosen files."
|
||||
});
|
||||
if (!selectedFields || selectedFields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = {
|
||||
scalars: {},
|
||||
arrays: {}
|
||||
};
|
||||
|
||||
for (const selectedField of selectedFields) {
|
||||
const field = selectedField.field;
|
||||
const rawValue = await promptBatchFieldValue(field);
|
||||
if (rawValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.inputKind === "array") {
|
||||
updates.arrays[field.key] = parseBatchArrayValue(rawValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
updates.scalars[field.key] = rawValue;
|
||||
}
|
||||
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
const touchedDocuments = [];
|
||||
let changedFileCount = 0;
|
||||
|
||||
for (const fileItem of selectedFiles) {
|
||||
const document = await vscode.workspace.openTextDocument(fileItem.fileUri);
|
||||
const originalYaml = document.getText();
|
||||
const updatedYaml = applyFormUpdates(originalYaml, updates);
|
||||
if (updatedYaml === originalYaml) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullRange = new vscode.Range(
|
||||
document.positionAt(0),
|
||||
document.positionAt(originalYaml.length));
|
||||
edit.replace(fileItem.fileUri, fullRange, updatedYaml);
|
||||
touchedDocuments.push(document);
|
||||
changedFileCount += 1;
|
||||
}
|
||||
|
||||
if (changedFileCount === 0) {
|
||||
void vscode.window.showInformationMessage("Batch edit did not change any selected config files.");
|
||||
return;
|
||||
}
|
||||
|
||||
const applied = await vscode.workspace.applyEdit(edit);
|
||||
if (!applied) {
|
||||
throw new Error("VS Code rejected the batch edit workspace update.");
|
||||
}
|
||||
|
||||
for (const document of touchedDocuments) {
|
||||
await document.save();
|
||||
await validateConfigFile(document.uri, diagnostics);
|
||||
}
|
||||
|
||||
provider.refresh();
|
||||
void vscode.window.showInformationMessage(
|
||||
`Batch updated ${changedFileCount} config file(s) in '${path.basename(domainUri.fsPath)}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load schema info for a config file.
|
||||
*
|
||||
@ -582,6 +723,28 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for one batch-edit field value.
|
||||
*
|
||||
* @param {{key: string, type: string, itemType?: string, inputKind: "scalar" | "array", required: boolean}} field Editable field descriptor.
|
||||
* @returns {Promise<string | undefined>} User input, or undefined when cancelled.
|
||||
*/
|
||||
async function promptBatchFieldValue(field) {
|
||||
if (field.inputKind === "array") {
|
||||
return vscode.window.showInputBox({
|
||||
title: `Batch Edit Array: ${field.key}`,
|
||||
prompt: `Enter comma-separated items for '${field.key}' (expected array<${field.itemType}>). Leave empty to clear the array.`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
}
|
||||
|
||||
return vscode.window.showInputBox({
|
||||
title: `Batch Edit Field: ${field.key}`,
|
||||
prompt: `Enter the new value for '${field.key}' (expected ${field.type}).`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate all YAML files recursively.
|
||||
*
|
||||
|
||||
@ -3,6 +3,8 @@ const assert = require("node:assert/strict");
|
||||
const {
|
||||
applyFormUpdates,
|
||||
applyScalarUpdates,
|
||||
getEditableSchemaFields,
|
||||
parseBatchArrayValue,
|
||||
parseSchemaContent,
|
||||
parseTopLevelYaml,
|
||||
validateParsedConfig
|
||||
@ -138,3 +140,40 @@ test("applyFormUpdates should replace top-level scalar arrays and preserve unrel
|
||||
assert.match(updated, /^reward:$/mu);
|
||||
assert.match(updated, /^ gold: 10$/mu);
|
||||
});
|
||||
|
||||
test("getEditableSchemaFields should expose only scalar and scalar-array properties", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "dropItems"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"reward": { "type": "object" },
|
||||
"dropItems": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"waypoints": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
assert.deepEqual(getEditableSchemaFields(schema), [
|
||||
{key: "dropItems", type: "array", itemType: "string", inputKind: "array", required: true},
|
||||
{key: "id", type: "integer", inputKind: "scalar", required: true},
|
||||
{key: "name", type: "string", inputKind: "scalar", required: false}
|
||||
]);
|
||||
});
|
||||
|
||||
test("parseBatchArrayValue should split comma-separated items and drop empty segments", () => {
|
||||
assert.deepEqual(parseBatchArrayValue(" potion, hi potion , ,bomb "), [
|
||||
"potion",
|
||||
"hi potion",
|
||||
"bomb"
|
||||
]);
|
||||
assert.deepEqual(parseBatchArrayValue(""), []);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user