Compare commits

..

12 Commits

16 changed files with 194 additions and 167 deletions

View File

@@ -1,8 +1,16 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true root = true
[*] [*]
indent_style = tab indent_style = tab
indent_size = 4 indent_size = 4
end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = false insert_final_newline = true
[*.{yml,yaml,tiny,yaml.txt,html}]
indent_style = space
indent_size = 2

3
.vscode/launch.json vendored
View File

@@ -14,7 +14,8 @@
"webpack:///./*": "${workspaceRoot}/*", "webpack:///./*": "${workspaceRoot}/*",
"webpack:///*": "*" "webpack:///*": "*"
}, },
"cwd": "D:/work/repositories/zombie/config/新配表", "cwd": "C:/Users/Geequlim/Desktop/sheet",
// "cwd": "D:/work/repositories/zombie/config/新配表",
// "cwd": "${workspaceFolder}", // "cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/dist/binary.js", "program": "${workspaceFolder}/dist/binary.js",
"args": [ "args": [

29
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"files.associations": {
"*.tiny": "yaml",
"*.yaml.txt": "yaml",
"*.js.txt": "javascript",
"*.meta": "yaml",
"*.manifest": "yaml",
"*.asset": "yaml",
"project.tiny": "yaml"
},
"files.exclude": {
"Library": true,
"Logs": true,
"Temp": true,
"**/*.meta": true,
"Packages/packages-lock.json": true,
".temp": true,
"UI/.objs": true,
"yarn.lock": true,
"TextToolDatas": true,
"ProjectSettings": true,
"UserSettings": true,
"node_modules": true,
"qrcodes": true,
"obj": true,
"*.csproj": true,
"*.sln": true
},
}

View File

@@ -3,11 +3,11 @@
将 Excel 配置表中的数据导出为方便程序读取和使用的数据。 将 Excel 配置表中的数据导出为方便程序读取和使用的数据。
## 支持将Excel配置表导出为: ## 支持将Excel配置表导出为:
- [x] JSON 文件 - [x] JSON 数据文件
- [x] YAML 文件 - [x] YAML 数据文件
- [x] C# 类型声明 - [x] C# 类型声明
- [x] TypeScript interface类型声明、class类型定义可用 `instanceof` 进行类型检查) - [x] TypeScript interface类型声明、class类型定义可用 `instanceof` 进行类型检查)
- [ ] Godot 引擎的 GDScript 脚本文件 - [x] 能够扩展导出其他数据格式和代码声明
## 表格格式说明 ## 表格格式说明
@@ -15,15 +15,46 @@
* 表名为 `@skip` 或以 `@skip` 开头的表会被忽略,不会导出数据文件 * 表名为 `@skip` 或以 `@skip` 开头的表会被忽略,不会导出数据文件
* 第一列值为 `@skip` 的行会被忽略,视为无效数据行 * 第一列值为 `@skip` 的行会被忽略,视为无效数据行
* 整行所有列为空的行会被忽略,视为无效数据行 * 整行所有列为空的行会被忽略,视为无效数据行
* 每张表的**第一个有效数据行**用作字段名,决定了导出数据所拥有的属性,**字段名必须符合标识符命名规范** * 第一列名为`@field`的行用作字段名,决定了导出数据所拥有的属性,**字段名必须符合标识符命名规范**;该行中不填名称的列视为空字段,该列的数据在导出时会被忽略,可以用于辅助数据列
* 字段名所在的行中不填名称的列视为空字段,该列的数据在导出时会被忽略 * 第一列名为`@comment`的行用作字段注释文档,用于描述其底下对应的字段
* 相同名称的字段导出时会被合并为数组 * 相同名称的字段导出时会被合并为数组
* 导出属性的数据类型由**整列所填写的数据类型**决定,支持以下数据类型 * 导出属性的数据类型由**整列所填写的数据类型**决定,支持以下数据类型
* 字符串 * 字符串
* 数值(优先使用整形) * 数值(优先使用整形)
* 布尔值 * 布尔值
* 空(`null`) * 空(`null`)
* 该工具设计原则是简单易用,表格字段可由策划自由调整,不支持数据引用,暂不支持结构体 * 该工具设计原则是简单易用,表格字段可由策划自由调整,不支持数据引用
### 表格式示例
![](screentshot-sheet-example.png)
以 TypeScript 为例,上图所示的表格将被导出为下面的数据格式,每行数据可被表示为一个 `EffectSequenceData` 类型的对象
```ts
export class EffectSequenceData {
readonly id: number;
/** 关键帧 */
readonly frames: readonly {
/** 时间 */
readonly time: number;
/** 事件 */
readonly event: string;
/** 特效 */
readonly effect: string;
/** 音效 */
readonly audio: string;
}[];
/** 动作 */
readonly animation: string;
/** 时长 */
readonly length: number;
static $bind_rows(rows: object[]) {
for (const row of rows) {
Object.setPrototypeOf(row, EffectSequenceData.prototype);
}
}
}
```
## 安装 ## 安装
- 安装 NodeJS 和 NPM, 注意将 Node 和 NPM 添加到环境变量 `PATH` - 安装 NodeJS 和 NPM, 注意将 Node 和 NPM 添加到环境变量 `PATH`
@@ -41,33 +72,28 @@ npm run build
### 配置示例 ### 配置示例
```yaml ```yaml
parser:
first_row_as_field_comment: true
input: input:
- file: 配置表.xlsx - 特效表.xlsx
encode: GBK parser:
first_column_as_id: true # 第一列用作 ID 列
constant_array_length: [
# 这里填入需要固定数组长度的表名称
]
output: output:
json: json:
enabled: true enabled: false
directory: output/json directory: "../../project/Assets/res/data/excel"
indent: "\t" indent: "\t"
yaml: yaml:
enabled: true enabled: true
directory: output/yaml directory: "../../project/Assets/res/data/excel"
indent: 2 indent: 2
csharp:
enabled: true
directory: output/csharp
namespace: game.data
base_type: tiny.data.UniqueIDObject
file_name: data
ignore_id: true
typescript: typescript:
enabled: true enabled: true
declaration: false declaration: false
type: class type: class
class_name_prefix: '' class_name_prefix: ''
class_name_extension: Data class_name_extension: Data
directory: output/typescript directory: "../../project/Scripts/src/game/configs"
file_name: data file_name: excel
``` ```

View File

@@ -1,10 +1,12 @@
input: input:
- 配置表.xlsx # - 配置表.xlsx
- 车辆.xlsx
parser: parser:
first_column_as_id: true # 第一列用作 ID 列 first_column_as_id: true # 第一列用作 ID 列
constant_array_length: [ constant_array_length: [
# 这里填入需要固定数组长度的表名称 # 这里填入需要固定数组长度的表名称
] ]
output: output:
json: json:
enabled: true enabled: true
@@ -14,13 +16,6 @@ output:
enabled: true enabled: true
directory: output/yaml directory: output/yaml
indent: 2 indent: 2
csharp:
enabled: true
directory: output/csharp
namespace: game.data
base_type: tiny.data.UniqueIDObject
file_name: data
ignore_id: true
typescript: typescript:
enabled: true enabled: true
declaration: false declaration: false
@@ -29,3 +24,6 @@ output:
class_name_extension: Data class_name_extension: Data
directory: output/typescript directory: output/typescript
file_name: data file_name: data
override:
车辆/Vehicle: output/车辆/Vehicle

View File

@@ -21,6 +21,7 @@
"dependencies": { "dependencies": {
"colors": "^1.4.0", "colors": "^1.4.0",
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",
"source-map-support": "^0.5.21",
"xlsx": "^0.16.0" "xlsx": "^0.16.0"
} }
} }

10
project.tiny Normal file
View File

@@ -0,0 +1,10 @@
develop:
actions:
- name: dev
title: 脚本编译服务
description: 脚本编译服务
command: yarn dev
- name: test
title: 单元测试
description: 单元测试
command: node ./dist/binary.js ./excel-exporter.yaml

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -2,25 +2,28 @@ import { FileAccess, ModeFlags } from "tiny/io";
import { ParserConfigs, TableParser, TableData } from "./TableParser"; import { ParserConfigs, TableParser, TableData } from "./TableParser";
import { ExporterConfigs, TableExporter } from "./TableExporter"; import { ExporterConfigs, TableExporter } from "./TableExporter";
import { JSONExporter } from "./exporters/JSONExporter"; import { JSONExporter } from "./exporters/JSONExporter";
import { CSharpExporter } from "./exporters/CSharpExporter";
import * as colors from "colors"; import * as colors from "colors";
import { TypeScriptExporter } from "./exporters/TypeScriptExporter"; import { TypeScriptExporter } from "./exporters/TypeScriptExporter";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { YAMLExporter } from "./exporters/YAMLExporter"; import { YAMLExporter } from "./exporters/YAMLExporter";
import * as path from "path";
import * as fs from "fs";
export interface Configurations { export interface Configurations {
/** 解析配置 */ /** 解析配置 */
parser?: ParserConfigs, parser?: ParserConfigs,
/** 要读取的 XLSL 文档 */ /** 要读取的 XLSL 文档 */
input: {"file": string, encode: string}[], input: string[],
/** 导出配置 */ /** 导出配置 */
output: { [key: string]: ExporterConfigs } output: {
[key: string]: ExporterConfigs,
}
override?: Record<string, string>,
} }
const exporters: {[key:string]: new(config: ExporterConfigs) => TableExporter } = { const exporters: {[key:string]: new(config: ExporterConfigs) => TableExporter } = {
json: JSONExporter, json: JSONExporter,
csharp: CSharpExporter,
typescript: TypeScriptExporter, typescript: TypeScriptExporter,
yaml: YAMLExporter, yaml: YAMLExporter,
} }
@@ -53,8 +56,14 @@ export class ExcelExporterApplication {
for (const file of this.configs.input) { for (const file of this.configs.input) {
console.log(colors.grey(`解析配表文件: ${file}`)); console.log(colors.grey(`解析配表文件: ${file}`));
let sheets = this.parser.parse_xlsl(file); let sheets = this.parser.parse_xlsl(file);
const base = path.basename(file).replace(/\.xlsx?$/, '');
for (const name in sheets) { for (const name in sheets) {
this.tables[name] = sheets[name]; const table = sheets[name];
if (this.configs.override) {
const override = this.configs.override[`${base}/${name}`];
table.output = override;
}
this.tables[name] = table;
} }
} }
console.log(colors.green(`解析所有配表文件完成`)); console.log(colors.green(`解析所有配表文件完成`));
@@ -66,7 +75,18 @@ export class ExcelExporterApplication {
if (exporter.configs.enabled) { if (exporter.configs.enabled) {
console.log(colors.white(`执行 ${exporter.name} 导出:`)); console.log(colors.white(`执行 ${exporter.name} 导出:`));
for (const name in this.tables) { for (const name in this.tables) {
exporter.export(name, this.tables[name]); const table = this.tables[name];
const data = exporter.export(name, table);
if (data) {
let file = path.join(exporter.configs.directory, `${name}.${exporter.extension}`);
if (table.output) {
file = table.output + `.${exporter.extension}`;
}
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(file, data);
console.log(colors.white(`\t ${name} => ${file}`));
}
} }
exporter.finalize(); exporter.finalize();
console.log(); console.log();

View File

@@ -5,6 +5,7 @@ import { path } from "tiny/path";
export interface ExporterConfigs { export interface ExporterConfigs {
enabled: boolean, enabled: boolean,
directory: string, directory: string,
extension?: string;
} }
export class TableExporter { export class TableExporter {
@@ -15,7 +16,7 @@ export class TableExporter {
this.configs = configs; this.configs = configs;
} }
get extension(): string { return ''} get extension(): string { return this.configs.extension || ''; }
protected line(text = "", indent = 0) { protected line(text = "", indent = 0) {
return this.indent_text(text, indent) + '\n'; return this.indent_text(text, indent) + '\n';
@@ -44,7 +45,9 @@ export class TableExporter {
* @param name 表名称 * @param name 表名称
* @param table 表数据 * @param table 表数据
*/ */
export(name: string, table: TableData) { } export(name: string, table: TableData): string | Buffer {
return null;
}
/** 全部配置表导出完毕后保存文件 */ /** 全部配置表导出完毕后保存文件 */
finalize() {} finalize() {}

View File

@@ -1,6 +1,7 @@
import * as xlsl from "xlsx"; import * as xlsl from "xlsx";
import { FileAccess, ModeFlags } from "tiny/io"; import { FileAccess, ModeFlags } from "tiny/io";
import * as colors from "colors"; import * as colors from "colors";
import * as path from 'path';
export enum Keywords { export enum Keywords {
SKIP = '@skip', SKIP = '@skip',
@@ -29,6 +30,12 @@ interface RawTableCell extends xlsl.CellObject {
type RawTableData = RawTableCell[][]; type RawTableData = RawTableCell[][];
export interface TableData {
struct: Field;
data: {[key: string]: any}[];
output?: string;
}
export interface ParserConfigs { export interface ParserConfigs {
/** 第一列作为ID */ /** 第一列作为ID */
first_column_as_id: boolean; first_column_as_id: boolean;
@@ -52,6 +59,8 @@ export class Field {
type?: DataType; type?: DataType;
/** 保持数组长度和配置表中的列数量一致,没填的数据使用 null 填充 */ /** 保持数组长度和配置表中的列数量一致,没填的数据使用 null 填充 */
constant_array_length?: boolean; constant_array_length?: boolean;
/** 所属字段 */
parent: Field;
/** 添加子字段 */ /** 添加子字段 */
add_field(field: Field) { add_field(field: Field) {
@@ -126,11 +135,18 @@ export class Field {
} }
} }
} }
if (type === DataType.null) {
console.log(colors.red(`\t\t${name}(${xlsl.utils.encode_col(fields[0].columns.start)}列) 没有填入有效数据, 无法推断其数据类型`));
}
} }
} }
for (const c of this.children) { for (const c of this.children) {
c.constant_array_length = this.constant_array_length; c.constant_array_length = this.constant_array_length;
c.parent = this;
c.build(); c.build();
if (c.type === DataType.null && !c._is_array) {
console.log(colors.red(`\t\t${c.name}(${xlsl.utils.encode_col(c.columns.start)}列) 没有填入有效数据, 无法推断其数据类型`));
}
} }
} }
} }
@@ -155,17 +171,18 @@ export class Field {
let obj = {}; let obj = {};
let isAllNullish = true; let isAllNullish = true;
for (const c of this.children) { for (const c of this.children) {
let value = c.parse_row(row); const value = c.parse_row(row);
const is_null = this.check_is_null(row);
if (c.is_array) { if (c.is_array) {
let arr: any[] = obj[c.name] || []; let arr: any[] = obj[c.name] || [];
if (this.constant_array_length || value != null) { if (this.constant_array_length || value !== null) {
arr.push(value); arr.push(value);
} }
obj[c.name] = arr; obj[c.name] = arr;
} else { } else {
obj[c.name] = value; obj[c.name] = value;
} }
isAllNullish = isAllNullish && value == null; isAllNullish = isAllNullish && is_null;
} }
return isAllNullish ? null : obj; return isAllNullish ? null : obj;
} }
@@ -174,7 +191,7 @@ export class Field {
protected get_cell_value(cell: RawTableCell, type: DataType) { protected get_cell_value(cell: RawTableCell, type: DataType) {
switch (type) { switch (type) {
case DataType.bool: case DataType.bool:
return cell && cell.v as boolean == true; return cell ? cell.v as boolean == true : false;
case DataType.int: case DataType.int:
return cell ? cell.v as number : 0; return cell ? cell.v as number : 0;
case DataType.float: case DataType.float:
@@ -185,26 +202,22 @@ export class Field {
return null; return null;
} }
} }
}
const TypeCompatibility = { protected check_is_null(row: RawTableCell[]): boolean {
string: 5, if (this.children) {
float: 4, let isAllNullish = true;
int: 3, for (const c of this.children) {
bool: 2, isAllNullish = isAllNullish && c.check_is_null(row);
null: 1 if (!isAllNullish) {
}; return false;
}
export interface ColumnDescription { }
type: DataType; return isAllNullish;
name: string; } else {
is_array?: boolean; let cell = row[this.columns.start];
comment?: string; return !cell || cell.t === 'z';
} }
}
export interface TableData {
struct: Field;
data: {[key: string]: any}[];
} }
export class TableParser { export class TableParser {
@@ -219,8 +232,8 @@ export class TableParser {
return this.load_raw_xlsl_data(path); return this.load_raw_xlsl_data(path);
} }
protected load_raw_xlsl_data(path: string): { [key: string]: TableData } { protected load_raw_xlsl_data(filePath: string): { [key: string]: TableData } {
var file = FileAccess.open(path, ModeFlags.READ); var file = FileAccess.open(filePath, ModeFlags.READ);
let wb = xlsl.read(file.get_as_array()); let wb = xlsl.read(file.get_as_array());
file.close(); file.close();
let raw_tables: {[key: string]: RawTableData } = {}; let raw_tables: {[key: string]: RawTableData } = {};

View File

@@ -1,85 +0,0 @@
import { TableExporter, ExporterConfigs } from "excel-exporter/TableExporter";
import { TableData, DataType } from "excel-exporter/TableParser";
import { path } from "tiny/path";
import * as colors from "colors";
interface CSharpExporterConfigs extends ExporterConfigs {
namespace: string,
base_type: string,
file_name: string,
ignore_id: boolean
}
export class CSharpExporter extends TableExporter {
protected declear_content = "";
protected classes: string[] = [];
get extension(): string { return 'cs' }
constructor(configs: ExporterConfigs) {
super(configs);
if ( typeof ((this.configs as CSharpExporterConfigs).namespace) != 'string') {
(this.configs as CSharpExporterConfigs).namespace = "game.data";
}
if ( typeof ((this.configs as CSharpExporterConfigs).base_type) != 'string') {
(this.configs as CSharpExporterConfigs).namespace = "object";
}
if ( typeof ((this.configs as CSharpExporterConfigs).file_name) != 'string') {
(this.configs as CSharpExporterConfigs).file_name = "data";
}
this.declear_content += this.line("// Tool generated file DO NOT MODIFY");
this.declear_content += this.line("using System;");
this.declear_content += this.line();
this.declear_content += this.line("namespace " + (this.configs as CSharpExporterConfigs).namespace + " {")
this.declear_content += this.line("%CLASSES%");
this.declear_content += this.line("}");
}
export(name: string, table: TableData) {
const base_type = (this.configs as CSharpExporterConfigs).base_type;
let body = "";
for (const field of table.headers) {
if (field.name == 'id' && (this.configs as CSharpExporterConfigs).ignore_id) {
continue;
}
let type = "object";
switch (field.type) {
case DataType.bool:
case DataType.float:
case DataType.string:
case DataType.int:
type = field.type;
break;
default:
type = "object";
break;
}
if (field.is_array) {
type += "[]";
}
if (field.comment) {
let comment = field.comment.split("\r\n").join("\t");
comment = comment.split("\n").join("\t");
body += this.line(`/// <summary>${comment}</summary>`, 1);
}
body += this.line(`public ${type} ${field.name};`, 1);
}
let class_text = this.line(`public class ${name} : ${base_type} {\n${body}\n}`);
this.classes.push(class_text);
}
finalize() {
let class_text = "";
for (const cls of this.classes) {
class_text += cls;
class_text += this.line();
}
let file = path.join(this.configs.directory, (this.configs as CSharpExporterConfigs).file_name);
if (!file.endsWith(".cs")) {
file += "." + this.extension;
}
this.save_text(file, this.declear_content.replace("%CLASSES%", class_text));
console.log(colors.green(`\t${file}`));
}
}

View File

@@ -17,7 +17,7 @@ export class JSONExporter extends TableExporter {
} }
} }
get extension(): string { return 'json'} get extension(): string { return this.configs.extension || 'json'; }
protected recursively_order_keys(unordered: object | Array<object>) { protected recursively_order_keys(unordered: object | Array<object>) {
// If it's an array - recursively order any // If it's an array - recursively order any
@@ -59,9 +59,7 @@ export class JSONExporter extends TableExporter {
} }
export(name: string, table: TableData) { export(name: string, table: TableData) {
const file = path.join(this.configs.directory, `${name}.${this.extension}`);
const text = JSON.stringify(this.export_json_object(name, table), null, this.indent); const text = JSON.stringify(this.export_json_object(name, table), null, this.indent);
this.save_text(file, text); return text;
console.log(colors.green(`\t ${name} ==> ${file}`));
} }
} }

View File

@@ -29,6 +29,7 @@ export class TypeScriptExporter extends TableExporter {
(this.configs as TypeScriptExporterConfigs).type, (this.configs as TypeScriptExporterConfigs).type,
(this.configs as TypeScriptExporterConfigs).declaration, (this.configs as TypeScriptExporterConfigs).declaration,
); );
return null;
} }
protected export_field(field: Field, indent = 0, ignore_root = false) { protected export_field(field: Field, indent = 0, ignore_root = false) {
@@ -57,6 +58,9 @@ export class TypeScriptExporter extends TableExporter {
} }
type += ignore_root ? '' : this.indent_text('}', indent); type += ignore_root ? '' : this.indent_text('}', indent);
} break; } break;
case DataType.null:
type = "null";
break;
default: default:
type = "any"; type = "any";
break; break;

View File

@@ -12,7 +12,7 @@ interface YAMLExporterConfigs extends ExporterConfigs {
export class YAMLExporter extends JSONExporter { export class YAMLExporter extends JSONExporter {
get extension(): string { return 'yaml'} get extension(): string { return this.configs.extension || 'yaml'; }
constructor(configs: ExporterConfigs) { constructor(configs: ExporterConfigs) {
super(configs); super(configs);
@@ -22,7 +22,6 @@ export class YAMLExporter extends JSONExporter {
} }
export(name: string, table: TableData) { export(name: string, table: TableData) {
const file = path.join(this.configs.directory, `${name}.${this.extension}`);
const text = yaml.dump( const text = yaml.dump(
this.export_json_object(name, table), this.export_json_object(name, table),
{ {
@@ -30,7 +29,6 @@ export class YAMLExporter extends JSONExporter {
sortKeys: true, sortKeys: true,
} }
); );
this.save_text(file, text); return text;
console.log(colors.green(`\t ${name} ==> ${file}`));
} }
} }

View File

@@ -8,7 +8,10 @@ module.exports = (env) => {
console.log("Compile environment:", env); console.log("Compile environment:", env);
return ({ return ({
target: 'node', target: 'node',
entry: path.join(workSpaceDir, 'src/main.ts'), entry: [
'source-map-support/register',
path.join(workSpaceDir, 'src/main.ts'),
],
output: { output: {
path: path.join(workSpaceDir, 'dist'), path: path.join(workSpaceDir, 'dist'),
filename: 'binary.js' filename: 'binary.js'