diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d49f9c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,267 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+ReaderLib/**/Config
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+# *.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# gen cache meta file
+.cache.meta
+
diff --git a/README.en-us.md b/README.en-us.md
new file mode 100644
index 0000000..5a72d76
--- /dev/null
+++ b/README.en-us.md
@@ -0,0 +1,62 @@
+# Birght Gen
+
+## What is Bright Gen
+
+* Aim
+ * Less time for trival & repeated work, more time with creativity & leisure .
+
+* Philosophy
+ * Simpler, Faster
+
+* [Home Page](https://focus-creative-games.github.io/bright_gen/index.html)
+
+* Read this in other languages: [English](README.en-us.md), [简体中文](README.md)
+
+
+## Features
+ * [x] multi data source (json, excel, folder)
+ * [ ] data type system supported
+ * [ ] polymorphism
+ * [ ] rich embeded type
+ * [ ] user defined type
+ * [ ] client/server structure & export faster
+ * [ ] shared cache
+ * [ ] customizable validataor
+ * [ ] customizable export format
+ * [ ] easy to extend with new feature
+ * [ ] enhanced support with excel
+ * [ ] dual key
+ * [ ] horizontal list
+ * [ ] shared cache for export time optimization
+ * [ ] sophisticated/polished source available
+ * [ ] localization & region support
+
+## How to set self hosted server up
+* Windows
+ * run xxx.bat
+
+* Docker
+ * run xxx.bat/xxx.sh
+
+* Other
+ * any .Net core environment
+
+## How to set development up
+* VS2019 commuity
+
+## How can I contribute?
+
+We welcome contributions! Many people all over the world have helped make this project better.
+
+* [Contributing](CONTRIBUTING.md) explains what kinds of changes we welcome
+- [Workflow Instructions](docs/workflow/README.md) explains how to build and test
+
+## Useful Links
+
+* [.NET Core source index](https://source.dot.net) / [.NET Framework source index](https://referencesource.microsoft.com)
+* other implementation
+ * [tabtoy](https://github.com/davyxu/tabtoy)
+
+## License
+
+Birght Gen is licensed under the [MIT](LICENSE.TXT) license.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..caf5654
--- /dev/null
+++ b/README.md
@@ -0,0 +1,88 @@
+[//]: # (Author: bug)
+[//]: # (Date: 2020-10-20 20:24:07)
+
+# Gen
+
+## 什么是 Gen
+
+Gen 是一个强大的生成与缓存工具,用于但不限于 游戏配置、消息、资源格式转换 之类的生成。
+
+相比传统简单的以excel为中心的表格导出工具,它提供了一个**完整的游戏配置数据解决方案**,有以下功能:
+>
+> * 数据定义
+> * 数据编辑
+> * 数据导出
+> * 前后端代码生成
+> * 本地化
+> * 编辑器数据load&save代码生成
+
+Gen能够良好满足小型、中型、大型及超大型游戏项目的配置需求。
+
+Gen 工具不仅适用于游戏行业,也非常适合传统的互联网项目。
+
+
+## 文档
+* [主页](https://focus-creative-games.github.io/bright_gen/index.html)
+* 各语言的简介: [English](README.en-us.md), [简体中文](README.md)
+
+## 使用示例
+ * Lua 使用示例
+ ``` Lua
+ local data = require("TbDataFromJson")
+ local cfg = data[32]
+ print(cfg.name)
+ ```
+
+ * [更多语言的例子](docs/samples.md)
+
+## 特性
+ * [完备的数据类型支持](docs/feature.md#支持的数据类型)
+ * [多类型数据源支持](docs/feature.md#多数据源支持)
+ * [多种数据表模式](docs/feature.md#多种数据表模式)
+ * [按组导出数据](docs/feature.md#如何自定义导出分组)
+ * [生成速度快](docs/feature.md#生成极快)
+ * [增强 Excel 的表达](docs/feature.md#增强的-excel-格式)
+ * [代码提示支持](docs/feature.md#代码编辑器支持)
+ * [根据开发效率需求定制的数据输出格式](docs/feature.md#支持多种导出数据格式)
+ * [本地化支持](docs/feature.md#本地化支持)
+ * [代码提示支持](docs/feature.md#代码编辑器支持)
+ * [强大的数据校验能力](docs/feature.md#强大的数据校验能力)
+ * [资源导出支持](docs/feature.md#资源导出支持)
+ * [自动代码生成](docs/feature.md#优秀的代码生成)
+ * [数据分组](docs/feature.md#良好的数据组织)
+ * [多语言支持](docs/feature.md#支持的语言-覆盖主流的语言)
+ * [多服务器引擎支持](docs/feature.md#支持的服务器引擎-满足语言版本的情况下)
+ * [多客户端引擎支持](docs/feature.md#支持的客户端引擎-满足语言版本的情况下)
+ * [扩展能力](docs/feature.md#强大的扩展能力)
+ * [ ] 提供定制开发服务 ^_^
+
+## RoadMap
+- [ ] 新增 unity 内置编辑器
+- [ ] 新增 unreal 内置编辑器
+- [ ] 补充单元测试
+- [x] 支持 python
+
+## 布署
+ TODO
+
+## 开发环境架设
+* 安装 VS2019 社区版
+* 安装 .dotnet core sdk 3.1
+
+## 如何贡献?
+* [Contributing](CONTRIBUTING.md) explains what kinds of changes we welcome
+- [Workflow Instructions](docs/workflow/README.md) explains how to build and test
+
+## Useful Links
+
+* [.NET Core source index](https://source.dot.net)
+* 社区的其它实现
+ * [tabtoy](https://github.com/davyxu/tabtoy)
+
+## 支持和联系
+ QQ 群: 692890842
+ 邮箱: taojingjian#gmail.com
+
+## License
+
+Birght Gen is licensed under the [MIT](LICENSE.TXT) license.
\ No newline at end of file
diff --git a/config/Datas/test/full_type.xlsx b/config/Datas/test/full_type.xlsx
new file mode 100644
index 0000000..eba555b
Binary files /dev/null and b/config/Datas/test/full_type.xlsx differ
diff --git a/config/Datas/test/json_datas/tb_role.json b/config/Datas/test/json_datas/tb_role.json
new file mode 100644
index 0000000..faa901b
--- /dev/null
+++ b/config/Datas/test/json_datas/tb_role.json
@@ -0,0 +1,30 @@
+ {
+ "x1":true,
+ "x2":3,
+ "x3":128,
+ "x4":1211,
+ "x5":11223344,
+ "x6":1.2,
+ "x7":1.23432,
+ "x8_0":12312,
+ "x8":112233,
+ "x9":223344,
+ "x10":"hq",
+ "x12": { "x1":10},
+ "x13":"B",
+ "x14":{"__type__": "DemoD2", "x1":1, "x2":2},
+ "v2":{"x":1, "y":2},
+ "v3":{"x":1.1, "y":2.2, "z":3.4},
+ "v4":{"x":10.1, "y":11.2, "z":12.3, "w":13.4},
+ "t1":"1970-01-01 00:00:00",
+ "k1":[1,2],
+ "k2":[2,3],
+ "k3":[1,3],
+ "k4":[1,5],
+ "k5":[1,6],
+ "k6":[1,7],
+ "k7":[2,3],
+ "k8":[[2,2],[4,10]],
+ "k9":[{"y1":1, "y2":true},{"y1":2, "y2":false}],
+ "k15":[{"__type__": "DemoD2", "x1":1, "x2":2}]
+ }
\ No newline at end of file
diff --git a/config/Datas/test/lua_datas/demo.lua b/config/Datas/test/lua_datas/demo.lua
new file mode 100644
index 0000000..5d8daf3
--- /dev/null
+++ b/config/Datas/test/lua_datas/demo.lua
@@ -0,0 +1,31 @@
+return
+{
+ x1 = false,
+ x2 = 2,
+ x3 = 128,
+ x4 = 1122,
+ x5 = 112233445566,
+ x6 = 1.3,
+ x7 = 1122,
+ x8 = 12,
+ x8_0 = 13,
+ x9 = 123,
+ x10 = "yf",
+ x12 = {x1=1},
+ x13 = "D",
+ x14 = { __type__="DemoD2", x1 = 1, x2=3},
+ v2 = {x= 1,y = 2},
+ v3 = {x=0.1, y= 0.2,z=0.3},
+ v4 = {x=1,y=2,z=3.5,w=4},
+ t1 = "1970-01-01 00:00:00",
+ k1 = {1,2},
+ k2 = {2,3},
+ k3 = {3,4},
+ k4 = {1,2},
+ k5 = {1,3},
+ k6 = {1,2},
+ k7 = {1,8},
+ k8 = {[2]=10,[3]=12},
+ k9 = {{y1=1,y2=true}, {y1=10,y2=false}},
+ k15 = {{ __type__="DemoD2", x1 = 1, x2=3}},
+}
\ No newline at end of file
diff --git a/config/Datas/test/multi_level_title.xlsx b/config/Datas/test/multi_level_title.xlsx
new file mode 100644
index 0000000..612735c
Binary files /dev/null and b/config/Datas/test/multi_level_title.xlsx differ
diff --git a/config/Datas/test/multi_rows_record.xlsx b/config/Datas/test/multi_rows_record.xlsx
new file mode 100644
index 0000000..bc0fa38
Binary files /dev/null and b/config/Datas/test/multi_rows_record.xlsx differ
diff --git a/config/Datas/test/table_one.xlsx b/config/Datas/test/table_one.xlsx
new file mode 100644
index 0000000..2fa9c5a
Binary files /dev/null and b/config/Datas/test/table_one.xlsx differ
diff --git a/config/Datas/test/tb_role_csv.csv b/config/Datas/test/tb_role_csv.csv
new file mode 100644
index 0000000..00465d9
--- /dev/null
+++ b/config/Datas/test/tb_role_csv.csv
@@ -0,0 +1,12 @@
+##,align:true,row:true,,,,,,,,,,,,,,,,,,,,,,
+__type__,x1,x2,x3,x4,x5,x6,x7,x8,x8_0,x9,x10,x11,x12,x13,,,k1,k2,k3,k4,k5,k6,k7,k8
+,ֹ,x2:byte,x3:short,x4:int,x5:long, x6:float,x7:double,,,,,,,,,,array:int,array:int,array:int,array:int,array:int,array:int,array:int,map:int:int
+ DemoD2,TRUE,5,5,10000,13234234234,1.28,1.23457891,1234,1234,111111111,huang,,1988,A,,,"1,2,3","1,2,4","1,2,5","1,2,6","1,2,7","1,2,8","1,2,9","1,2,3,4"
+,FALSE,0,6,198704,34523452345,2.5,19870421.2,453234,-345,112233445566 ,qiang,,1987,B,,,"2,4,6","2,4,7","2,4,8","2,4,9","2,4,10","2,4,11","2,4,12","1,10,2,20"
+,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,
diff --git a/config/Datas/test/tb_role_one_xlsx.xlsx b/config/Datas/test/tb_role_one_xlsx.xlsx
new file mode 100644
index 0000000..656fb05
Binary files /dev/null and b/config/Datas/test/tb_role_one_xlsx.xlsx differ
diff --git a/config/Datas/test/tb_role_xlsx.xlsx b/config/Datas/test/tb_role_xlsx.xlsx
new file mode 100644
index 0000000..40578d5
Binary files /dev/null and b/config/Datas/test/tb_role_xlsx.xlsx differ
diff --git a/config/Datas/test/tbrole_datas/tb_role.json b/config/Datas/test/tbrole_datas/tb_role.json
new file mode 100644
index 0000000..681057d
--- /dev/null
+++ b/config/Datas/test/tbrole_datas/tb_role.json
@@ -0,0 +1,24 @@
+ {
+ "x1" : true,
+ "x2" : 5,
+ "x3" : 1234,
+ "x4" : 120000,
+ "x5" : 12345566778899,
+ "x6" : 1.28,
+ "x7" : 123456789.1234567,
+ "x8" : 1234,
+ "x8_0" : 1122,
+ "x9" : 112233445566,
+ "x10": "huang",
+ "x11": "hiasf",
+ "x12" : { "x1":1987 },
+ "x13" : "B",
+ "k1" : [1,2,3],
+ "k2" : [11,22,33],
+ "k3" : [1,2,3],
+ "k4": [11,22],
+ "k5" : [2,3],
+ "k6" : [4,5],
+ "k7" : [10,20],
+ "k8" : { "1":100, "2":200, "3":300}
+ }
\ No newline at end of file
diff --git a/config/Datas/test/xml_datas/demo.xml b/config/Datas/test/xml_datas/demo.xml
new file mode 100644
index 0000000..13f9422
--- /dev/null
+++ b/config/Datas/test/xml_datas/demo.xml
@@ -0,0 +1,78 @@
+
+ true
+ 4
+ 128
+ 1122
+ 112233445566
+ 1.3
+ 1112232.43123
+ 112233
+ 123
+ 112334
+ yf
+
+ 1
+
+ C
+
+ 1
+ 2
+
+
+ 1,2
+ 1.2,2.3,3.4
+ 1.2,2.2,3.2,4.3
+
+ 1970-01-01 00:00:00
+
+
+ - 1
+ - 2
+
+
+ - 1
+ - 2
+
+
+ - 1
+ - 2
+
+
+ - 1
+ - 2
+
+
+ - 1
+ - 2
+
+
+ - 1
+ - 2
+
+
+ - 1
+ - 3
+
+
+
+ - 210
+ - 330
+
+
+
+ -
+ 1
+ true
+
+ -
+ 2
+ false
+
+
+
+ -
+ 1
+ 2
+
+
+
\ No newline at end of file
diff --git a/config/Defines/root.xml b/config/Defines/root.xml
new file mode 100644
index 0000000..f9b27dd
--- /dev/null
+++ b/config/Defines/root.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/Defines/test.xml b/config/Defines/test.xml
new file mode 100644
index 0000000..0b64bf5
--- /dev/null
+++ b/config/Defines/test.xml
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 多态数据结构
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 多态数据结构
+
+
+
+
+
+
+
+ 使用;来分隔
+
+
+
+
+
+
+
+
+
+
+
+
+ 最常见的普通 key-value表
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 单例表,只有一个记录
+
+ 普通表,不过数据从tbrole_datas目录递归读入,每个文件是一个记录
+
+ 普通表,不过数据从tbrole_datas目录递归读入,每个文件是一个记录
+
+ 普通表,不过数据从tbrole_datas目录递归读入,每个文件是一个记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 多态数据结构
+
+
+
+
+
+
+
+
+ 使用;来分隔
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持在一个定义文件中 定义多个模块。 一般来说一个定义文件中一个模块比较好,但有些情况下为了方便可以定义多个。
+
+
+
+
+
\ No newline at end of file
diff --git a/config/生成code_cs_bin和data_bin.bat b/config/生成code_cs_bin和data_bin.bat
new file mode 100644
index 0000000..fa7fc0b
--- /dev/null
+++ b/config/生成code_cs_bin和data_bin.bat
@@ -0,0 +1,3 @@
+..\src\Luban.Client\bin\Debug\netcoreapp3.1\gen.client -h 127.0.0.1 -j cfg -- -d Defines/root.xml --input_data_dir Datas --output_code_dir output_code --output_data_dir output_data -s server --gen_types code_cs_bin,data_bin --export_test_data
+
+pause
\ No newline at end of file
diff --git a/src/.dockerignore b/src/.dockerignore
new file mode 100644
index 0000000..5967294
--- /dev/null
+++ b/src/.dockerignore
@@ -0,0 +1,2 @@
+**/bin/
+**/obj/
diff --git a/src/Luban.Client/.editorconfig b/src/Luban.Client/.editorconfig
new file mode 100644
index 0000000..b5aa556
--- /dev/null
+++ b/src/Luban.Client/.editorconfig
@@ -0,0 +1,13 @@
+[*.cs]
+
+# CA1303: 请不要将文本作为本地化参数传递
+dotnet_diagnostic.CA1303.severity = none
+
+# CA1305: 指定 IFormatProvider
+dotnet_diagnostic.CA1305.severity = none
+
+# CA1307: 指定 StringComparison
+dotnet_diagnostic.CA1307.severity = none
+
+# CA1031: 不捕获常规异常类型
+dotnet_diagnostic.CA1031.severity = none
diff --git a/src/Luban.Client/Luban.Client.csproj b/src/Luban.Client/Luban.Client.csproj
new file mode 100644
index 0000000..352b043
--- /dev/null
+++ b/src/Luban.Client/Luban.Client.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Luban.Client/Properties/launchSettings.json b/src/Luban.Client/Properties/launchSettings.json
new file mode 100644
index 0000000..4d18b70
--- /dev/null
+++ b/src/Luban.Client/Properties/launchSettings.json
@@ -0,0 +1,81 @@
+{
+ "profiles": {
+ "Luban.Client": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j cfg -a \"{ 'define_file':'../../CfgDefines/root.xml', 'input_data_dir':'../../CfgDatas', 'validate_root_dir':'E:/NikkiP4_D/X6Game/Content', 'output_data_dir':'output_data', 'service':'server', 'export_test_data':true,'gen_types':'code_cs_bin,data_bin', 'output_code_dir' : 'Gen' }\"",
+ "workingDirectory": "E:\\workspace\\bright_gen\\DemoProjects\\Csharp_Server_DotNetCore"
+ },
+ "Luban.Client-db": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j db -d ../../../../Test/root.db.xml -c F:\\workspace\\perfect_core\\BaseDemo\\Gen\\Db2 -t server -l cs"
+ },
+ "gen_gate_proto": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j proto -d F:\\workspace\\all_server\\ProtoDefines\\gate.xml -c F:\\workspace\\all_server\\Gate\\Source\\Gen\\Proto -t server -l cs"
+ },
+ "gen_client_proto": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j proto -d F:\\workspace\\all_server\\ProtoDefines\\client.xml -c F:\\workspace\\all_server\\DemoClient\\Source\\Gen\\Proto -t server -l cs"
+ },
+ "gen_base_db": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j db -d F:\\workspace\\all_server\\DbDefines\\base.xml -c F:\\workspace\\all_server\\Base\\Source\\Gen\\Db -t server -l cs"
+ },
+ "gen_base_proto": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j proto -d F:\\workspace\\all_server\\ProtoDefines\\base.xml -c F:\\workspace\\all_server\\Base\\Source\\Gen\\Proto -t server -l cs"
+ },
+ "gen_base_proto_remote": {
+ "commandName": "Project",
+ "commandLineArgs": " -j proto -d F:\\workspace\\all_server\\ProtoDefines\\base.xml -c F:\\workspace\\all_server\\Base\\Source\\Gen\\Proto -t server -l cs"
+ },
+ "gen_base_db_remote": {
+ "commandName": "Project",
+ "commandLineArgs": " -j db -d F:\\workspace\\all_server\\DbDefines\\base.xml -c F:\\workspace\\all_server\\Base\\Source\\Gen\\Db -t server -l cs"
+ },
+ "gen_client_proto_lua": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j proto -d F:\\workspace\\all_server\\ProtoDefines\\client.xml -c F:\\workspace\\x6p4\\X6Game\\Content\\Script\\Gen\\Proto -t client -l lua"
+ },
+ "gen_proto_lua_test": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j proto -d ../../../../Test/root.proto.xml -c F:\\Gen\\Proto -t server -l lua"
+ },
+ "gen_client_proto_app_code_data": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j config -d F:\\workspace\\perfect_gen_cs\\Test\\csv\\root.xml --outputappcodedir F:\\workspace\\all_server\\DemoClient\\Source\\Gen\\Cfg --outputappdatadir F:\\workspace\\all_server\\DemoClient\\Config -t server -l cs"
+ },
+ "gen_client_cfg_lua": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j config -d D:\\NikkiP4_D\\DesignerConfig\\root.xml --outputappcodedir D:\\NikkiP4_D\\X6Game\\Content\\Script\\Gen\\Cfg --outputappdatadir D:\\NikkiP4_D\\X6Game\\Content\\Config -t server -l lua"
+ },
+ "gen_base_cfg_code_data": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j config -d F:\\workspace\\perfect_gen_cs\\Test\\csv\\root.xml --outputappcodedir F:\\workspace\\all_server\\Base\\Source\\Gen\\Cfg --outputappdatadir F:\\workspace\\all_server\\Base\\Config -t server -l cs"
+ },
+ "gen_base_cfg_code_data_remote": {
+ "commandName": "Project",
+ "commandLineArgs": "-j config -d F:\\workspace\\perfect_gen_cs\\Test\\csv\\root.xml --outputappcodedir F:\\workspace\\all_server\\Base\\Source\\Gen\\Cfg --outputappdatadir F:\\workspace\\all_server\\Base\\Config -t server -l cs"
+ },
+ "gen_x6_base_db": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j db -d D:\\NikkiP4_D\\X6Server\\DbDefines\\base.xml -c D:\\NikkiP4_D\\X6Server\\Online\\Source\\Gen\\Db -t server -l cs"
+ },
+ "gen_x6_base_proto": {
+ "commandName": "Project",
+ "commandLineArgs": " -j proto -d e:\\workspace\\x6_server\\ProtoDefines\\base.xml -c e:\\workspace\\x6_server\\GenShare\\Source\\Gen\\Proto --outputsynccodedir e:\\workspace\\x6_server\\Map\\Source\\Gen\\Objects -t server -l cs"
+ },
+ "gen_x6_client_proto": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j proto -d D:\\workspace\\x6_server\\ProtoDefines\\client.xml -c D:\\NikkiP4_D\\X6Game\\Content\\Script\\Gen\\Proto --outputsynccodedir D:\\NikkiP4_D\\X6Game\\Content\\Script\\Gen\\Proto -t client -l lua"
+ },
+ "gen_x6_client_cfg_lua": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j config -d D:\\NikkiP4_D\\X6Game\\DesignerConfigs\\root.xml --outputappcodedir D:\\NikkiP4_D\\X6Game\\Content\\Script\\Gen\\Cfg --outputappdatadir D:\\NikkiP4_D\\X6Game\\Content\\Config -t server -l lua"
+ },
+ "gen_cfg_export_debug": {
+ "commandName": "Project",
+ "commandLineArgs": "-h 127.0.0.1 -p 8899 -j config -d D:\\NikkiP4_D\\DesignerConfig\\root.xml --outputdatadir ./config -t server --outputdatatype json --exporttestdata"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Luban.Client/Source/Net/GenClient.cs b/src/Luban.Client/Source/Net/GenClient.cs
new file mode 100644
index 0000000..c54292b
--- /dev/null
+++ b/src/Luban.Client/Source/Net/GenClient.cs
@@ -0,0 +1,198 @@
+using Bright.Net;
+using Bright.Net.Bootstraps;
+using Bright.Net.Channels;
+using Bright.Net.Codecs;
+using Bright.Net.ServiceModes.Managers;
+using Bright.Time;
+using Luban.Client.Common.Utils;
+using Luban.Common.Protos;
+using Luban.Common.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Luban.Client.Common.Net
+{
+ public class Session : SessionBase
+ {
+ public override void OnActive()
+ {
+
+ }
+
+ public override void OnInactive()
+ {
+ }
+ }
+
+ public class GenClient : ClientManager
+ {
+
+ private static readonly NLog.Logger s_logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public static GenClient Ins { get; } = new GenClient();
+
+ public async Task Start(string host, int port, Dictionary factories)
+ {
+ var c = new TcpClientBootstrap
+ {
+ RemoteAddress = new IPEndPoint(IPAddress.Parse(host), port),
+ ConnectTimeoutMills = 100,
+ EventLoop = new EventLoop(null),
+ InitHandler = ch =>
+ {
+ ch.Pipeline.AddLast(new ProtocolFrameCodec(20_000_000, new RecycleByteBufPool(100, 10), new DefaultProtocolAllocator(factories)));
+ ch.Pipeline.AddLast(this);
+ }
+ };
+
+ var ch = await c.ConnectAsync().ConfigureAwait(false);
+ }
+
+ public override void HandleProtocol(Protocol proto)
+ {
+
+ switch (proto.GetTypeId())
+ {
+ case GetInputFile.ID:
+ {
+ Task.Run(() => _ = OnGetInputFileAsync((GetInputFile)proto));
+ break;
+ }
+ case GetImportFileOrDirectory.ID:
+ {
+ Task.Run(() => _ = OnGetImportFileOrDirectoryAsync((GetImportFileOrDirectory)proto));
+ break;
+ }
+ case QueryFilesExists.ID:
+ {
+ Task.Run(() => Process((QueryFilesExists)proto));
+ break;
+ }
+ case PushLog.ID:
+ {
+ Process((PushLog)proto);
+ break;
+ }
+ case PushException.ID:
+ {
+ Process((PushException)proto);
+ break;
+ }
+ default:
+ {
+ s_logger.Error("unknown proto:{proto}", proto);
+ break;
+ }
+ }
+ }
+
+ private async Task OnGetImportFileOrDirectoryAsync(GetImportFileOrDirectory rpc)
+ {
+ long t1 = TimeUtil.NowMillis;
+ var file = rpc.Arg.FileOrDirName;
+ var re = new GetImportFileOrDirectoryRes()
+ {
+ SubFiles = new List(),
+ };
+
+ try
+ {
+ if (Directory.Exists(file))
+ {
+ re.Err = 0;
+ re.IsFile = false;
+ foreach (var subFile in Directory.GetFiles(file, "*", SearchOption.AllDirectories))
+ {
+ if (FileUtil.IsValidInputFile(subFile))
+ {
+ var md5 = await CacheMetaManager.Ins.GetOrUpdateFileMd5Async(subFile);
+ re.SubFiles.Add(new Luban.Common.Protos.FileInfo() { FilePath = FileUtil.Standardize(subFile), MD5 = md5 });
+ }
+ }
+
+ }
+ else if (File.Exists(file))
+ {
+ re.IsFile = true;
+ re.Md5 = await CacheMetaManager.Ins.GetOrUpdateFileMd5Async(file);
+ }
+ else
+ {
+ re.Err = Luban.Common.EErrorCode.FILE_OR_DIR_NOT_EXISTS;
+ }
+ }
+ catch (Exception e)
+ {
+ re.Err = Luban.Common.EErrorCode.READ_FILE_FAIL;
+ s_logger.Error(e);
+ }
+
+ s_logger.Trace(" GetImportFileOrDirectory file:{file} err:{err} cost:{time}", file, re.Err, TimeUtil.NowMillis - t1);
+
+ Session.ReplyRpc(rpc, re);
+ }
+
+ private async Task OnGetInputFileAsync(GetInputFile rpc)
+ {
+ long t1 = TimeUtil.NowMillis;
+ var res = new GetInputFileRes() { Err = Luban.Common.EErrorCode.OK };
+ try
+ {
+ res.Content = await FileUtil.ReadAllBytesAsync(rpc.Arg.File);
+ //res.Content = FileUtil.ReadAllBytes(rpc.Arg.File);
+ res.Err = Luban.Common.EErrorCode.OK;
+ }
+ catch (Exception)
+ {
+ res.Err = Luban.Common.EErrorCode.READ_FILE_FAIL;
+ }
+ s_logger.Info(" get input file:{file} err:{err} cost:{time}", rpc.Arg.File, res.Err, TimeUtil.NowMillis - t1);
+
+ Session.ReplyRpc(rpc, res);
+ }
+
+ private void Process(QueryFilesExists p)
+ {
+ var root = p.Arg.Root;
+ var files = p.Arg.Files;
+ var re = new QueryFilesExistsRes() { Exists = new List(files.Count) };
+ foreach (var f in files)
+ {
+ re.Exists.Add(File.Exists(Path.Combine(root, f)));
+ }
+ Session.ReplyRpc(p, re);
+ }
+
+ private void Process(PushLog p)
+ {
+ switch (p.Level)
+ {
+ case "trace":
+ {
+ s_logger.Trace(p.LogContent);
+ break;
+ }
+ case "info":
+ {
+ s_logger.Info(p.LogContent);
+ break;
+ }
+ default:
+ {
+ s_logger.Error(p.LogContent);
+ break;
+ }
+
+ }
+ }
+
+ private void Process(PushException p)
+ {
+ s_logger.Error(p.LogContent);
+ s_logger.Error(p.StackTrace);
+ }
+ }
+}
diff --git a/src/Luban.Client/Source/Program.cs b/src/Luban.Client/Source/Program.cs
new file mode 100644
index 0000000..f2a5575
--- /dev/null
+++ b/src/Luban.Client/Source/Program.cs
@@ -0,0 +1,217 @@
+using Luban.Client.Common.Net;
+using Luban.Client.Common.Utils;
+using Luban.Common.Protos;
+using Luban.Common.Utils;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Luban.Client
+{
+ class Program
+ {
+ public class CommandLineOptions
+ {
+ public string Host { get; set; }
+
+ public int Port { get; set; } = 8899;
+
+ public string JobType { get; set; }
+
+ public List JobArguments { get; set; } = new List();
+
+ public bool Verbose { get; set; }
+
+ public string CacheMetaInfoFile { get; set; } = ".cache.meta";
+ }
+
+ private static NLog.Logger s_logger;
+
+ private static void PrintUsage(string err)
+ {
+ Console.WriteLine("ERRORS:");
+ Console.WriteLine("\t" + err);
+ Console.WriteLine(@"
+Luban.Client ... [-- [job options]]
+e.g.
+ Luban.Client -h 127.0.0.1 -p 2234 -j cfg -- --name abc
+
+Options:
+ -h, --host Required. host ip
+ -p --port port. default 8899
+ -j --job Required. job type. avaliable value: cfg
+ -v --verbose verbose print
+ -c --cachemetafile cache meta file name
+ -h --help show usage
+");
+ }
+
+ private static (object, CommandLineOptions) ParseArgs(string[] args)
+ {
+ var ops = new CommandLineOptions();
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ var arg = args[i];
+ try
+ {
+ switch (arg)
+ {
+ case "-h":
+ case "--host":
+ {
+
+ ops.Host = args[++i];
+ break;
+ }
+ case "-p":
+ case "--port":
+ {
+ ops.Port = int.Parse(args[++i]);
+ break;
+ }
+ case "-j":
+ case "--job":
+ {
+ ops.JobType = args[++i];
+ break;
+ }
+ case "-v":
+ case "--verbose":
+ {
+ ops.Verbose = true;
+ break;
+ }
+ case "-c":
+ case "--cachemetafile":
+ {
+ ops.CacheMetaInfoFile = args[++i];
+ break;
+ }
+ case "--":
+ {
+ ++i;
+ ops.JobArguments = args.ToList().GetRange(i, args.Length - i);
+ return (null, ops);
+ }
+ default:
+ {
+ return ($"unknown argument:{arg}", null);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ return ($"argument:{arg} err", null);
+ }
+ }
+ if (ops.Host == null)
+ {
+ return ("--host missing", null);
+ }
+ if (ops.JobType == null)
+ {
+ return ("--job missing", null);
+ }
+
+ return (null, ops);
+ }
+
+ static void Main(string[] args)
+ {
+ var profile = new ProfileTimer();
+
+ profile.StartPhase("all");
+
+ var parseResult = ParseArgs(args);
+ if (parseResult.Item1 != null)
+ {
+ PrintUsage((string)parseResult.Item1);
+ Environment.Exit(1);
+ return;
+ }
+ CommandLineOptions options = parseResult.Item2;
+
+ profile.StartPhase("init logger");
+
+ LogUtil.InitSimpleNLogConfigure(NLog.LogLevel.Info);
+ s_logger = NLog.LogManager.GetCurrentClassLogger();
+ profile.EndPhaseAndLog();
+
+ ThreadPool.SetMinThreads(4, 5);
+ ThreadPool.SetMaxThreads(64, 10);
+
+ int exitCode;
+ try
+ {
+ profile.StartPhase("load cache meta file");
+ CacheMetaManager.Ins.Load(options.CacheMetaInfoFile);
+ profile.EndPhaseAndLog();
+ profile.StartPhase("connect server");
+ var conn = GenClient.Ins.Start(options.Host, options.Port, ProtocolStub.Factories);
+ conn.Wait();
+ profile.EndPhaseAndLog();
+
+ profile.StartPhase("gen job");
+ exitCode = SubmitGenJob(options);
+ profile.EndPhaseAndLog();
+ }
+ catch (Exception e)
+ {
+ exitCode = 1;
+ s_logger.Error(e);
+ }
+
+ CacheMetaManager.Ins.Save();
+ profile.EndPhaseAndLog();
+ if (exitCode == 0)
+ {
+ s_logger.Info("== succ ==");
+ }
+ else
+ {
+ s_logger.Error("== fail ==");
+ }
+ Environment.Exit(exitCode);
+ }
+
+ const int GEN_JOB_TIMEOUT = 30;
+
+ private static int SubmitGenJob(CommandLineOptions options)
+ {
+ var res = GenClient.Ins.Session.CallRpcAsync(new GenJobArg()
+ {
+ JobType = options.JobType,
+ JobArguments = options.JobArguments,
+ Verbose = options.Verbose,
+ }, GEN_JOB_TIMEOUT).Result;
+
+ if (res.ErrCode != 0)
+ {
+ if (res.ErrCode == Luban.Common.EErrorCode.JOB_ARGUMENT_ERROR)
+ {
+ s_logger.Error("job argument error");
+ Console.WriteLine(res.ErrMsg);
+ }
+ else
+ {
+ s_logger.Error("GenJob fail. err:{err} msg:{msg}", res.ErrCode, res.ErrMsg);
+ }
+
+ return 1;
+ }
+
+ var tasks = new List();
+
+ foreach (var fg in res.FileGroups)
+ {
+ tasks.Add(DownloadFileUtil.DownloadGeneratedFiles(fg.Dir, fg.Files));
+ }
+
+ Task.WaitAll(tasks.ToArray());
+ return 0;
+ }
+ }
+}
diff --git a/src/Luban.Client/Source/Utils/CacheMetaManager.cs b/src/Luban.Client/Source/Utils/CacheMetaManager.cs
new file mode 100644
index 0000000..6a51404
--- /dev/null
+++ b/src/Luban.Client/Source/Utils/CacheMetaManager.cs
@@ -0,0 +1,213 @@
+using Luban.Common.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Luban.Client.Common.Utils
+{
+ public class CacheMetaManager
+ {
+ private static readonly NLog.Logger s_logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public static CacheMetaManager Ins { get; } = new CacheMetaManager();
+
+
+ class MetaInfo
+ {
+ public string FullPath { get; set; }
+
+ public string Md5 { get; set; }
+
+ public long FileLength { get; set; }
+
+ public long FileModifiedTime { get; set; }
+
+ public bool Visited { get; set; }
+ }
+
+ private readonly object _lock = new object();
+
+ private string _metaFile;
+ private volatile bool _dirty;
+ private readonly SortedDictionary _cacheFileMetas = new SortedDictionary();
+
+ public void Load(string metaFile)
+ {
+ _metaFile = metaFile;
+ _dirty = false;
+
+ try
+ {
+ if (File.Exists(metaFile))
+ {
+ foreach (string line in File.ReadAllLines(metaFile, Encoding.UTF8))
+ {
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+ string[] args = line.Split(',');
+ if (args.Length != 4)
+ {
+ _dirty = true;
+ s_logger.Error("corrupt line:{line}", line);
+ continue;
+ }
+ var fullPath = args[0];
+ string md5 = args[1];
+ long fileLength = long.Parse(args[2]);
+ long fileModifiedTime = long.Parse(args[3]);
+ if (!File.Exists(fullPath))
+ {
+ _dirty = true;
+ s_logger.Debug("[drop] cache file:{file} not exist.", fullPath);
+ continue;
+ }
+
+ var fileInfo = new FileInfo(fullPath);
+
+ long actualFileLength = fileInfo.Length;
+ if (actualFileLength != fileLength)
+ {
+ _dirty = true;
+ s_logger.Debug("[drop] cache file:{file} length not match. cache length:{cl} actual length:{al}", fullPath, fileLength, actualFileLength);
+ continue;
+ }
+ long actualLastModifiedTime = ((DateTimeOffset)fileInfo.LastWriteTime).ToUnixTimeMilliseconds();
+ if (actualLastModifiedTime != fileModifiedTime)
+ {
+ _dirty = true;
+ s_logger.Debug("[drop] cache file:{file} last modified time not match. cache:{cl} actual:{al}", fullPath, fileModifiedTime, actualLastModifiedTime);
+ continue;
+ }
+ _cacheFileMetas[fullPath] = new MetaInfo
+ {
+ FullPath = fullPath,
+ Md5 = md5,
+ FileLength = fileLength,
+ FileModifiedTime = fileModifiedTime,
+ };
+ s_logger.Debug("load cache. file:{file} md5:{md5} length:{length} last modified time:{time}", fullPath, md5, fileLength, fileModifiedTime);
+ }
+ }
+ else
+ {
+ s_logger.Info("meta file:{meta} not exist. ignore load", metaFile);
+ }
+ }
+ catch (Exception e)
+ {
+ s_logger.Error(e, "load meta file fail");
+ }
+
+ }
+
+ public void Save()
+ {
+ if (!_dirty)
+ {
+ return;
+ }
+
+ lock (_lock)
+ {
+ _dirty = false;
+ var content = _cacheFileMetas.Values.Select(meta => $"{meta.FullPath},{meta.Md5},{meta.FileLength},{meta.FileModifiedTime}");
+ File.WriteAllLines(_metaFile, content, Encoding.UTF8);
+ }
+ s_logger.Info("[Save] meta file:{metaFile} updated!", _metaFile);
+ }
+
+ private MetaInfo GetMetaInfo(string file)
+ {
+ lock (_lock)
+ {
+ var fullPath = Path.GetFullPath(file).Replace('\\', '/');
+ return _cacheFileMetas.TryGetValue(fullPath, out var meta) ? meta : null;
+ }
+ }
+
+ private static async Task BuildMetaInfo(string file, string md5 = null)
+ {
+ var fullPath = Path.GetFullPath(file).Replace('\\', '/');
+ if (md5 == null)
+ {
+ s_logger.Info("comput md5. file:{file}", file);
+ md5 = FileUtil.CalcMD5(await FileUtil.ReadAllBytesAsync(file));
+ }
+ var fileInfo = new FileInfo(fullPath);
+ long actualFileLength = fileInfo.Length;
+ long actualLastModifiedTime = ((DateTimeOffset)fileInfo.LastWriteTime).ToUnixTimeMilliseconds();
+ return new MetaInfo()
+ {
+ FullPath = fullPath,
+ Md5 = md5,
+ FileLength = actualFileLength,
+ FileModifiedTime = actualLastModifiedTime,
+ };
+ }
+
+ public async Task GetOrUpdateFileMd5Async(string file)
+ {
+ var meta = GetMetaInfo(file);
+
+ if (meta == null)
+ {
+ meta = await BuildMetaInfo(file);
+ lock (_lock)
+ {
+ _dirty = true;
+ _cacheFileMetas.Add(meta.FullPath, meta);
+ }
+ s_logger.Debug("[add] meta not find, build it. file:{file} path:{path} md5:{md5} length:{length}", file, meta.FullPath, meta.Md5, meta.FileLength);
+ }
+ else
+ {
+ s_logger.Debug("[cache hit] file:{file} path:{path} md5:{md5} length:{length}", file, meta.FullPath, meta.Md5, meta.FileLength);
+ }
+ return meta.Md5;
+ }
+
+ public async Task CheckFileChangeAsync(string relateDir, string filePath, string md5)
+ {
+ var outputPath = relateDir != null ? FileUtil.Combine(relateDir, filePath) : filePath;
+
+ var meta = GetMetaInfo(outputPath);
+ if (meta == null)
+ {
+ if (!File.Exists(outputPath))
+ {
+ return true;
+ }
+ meta = await BuildMetaInfo(outputPath);
+ lock (_lock)
+ {
+ _dirty = true;
+ _cacheFileMetas.Add(meta.FullPath, meta);
+ }
+ s_logger.Debug("[add] meta not find, create it. file:{file} path:{path} md5:{md5} length:{length}", outputPath, meta.FullPath, meta.Md5, meta.FileLength);
+ }
+ if (meta.Md5 != md5)
+ {
+ s_logger.Debug("[add] meta md5 not match, file:{file} path:{path} md5:{md5} length:{length}", outputPath, meta.FullPath, meta.Md5, meta.FileLength);
+ return true;
+ }
+ return false;
+ }
+
+ public async Task UpdateFileAsync(string relateDir, string filePath, string md5)
+ {
+ var file = relateDir != null ? FileUtil.Combine(relateDir, filePath) : filePath;
+ var meta = await BuildMetaInfo(file, md5);
+ lock (_lock)
+ {
+ _dirty = true;
+ _cacheFileMetas[meta.FullPath] = meta;
+ }
+ s_logger.Debug("[update] file:{file} path:{path} md5:{md5} length:{length}", file, meta.FullPath, meta.Md5, meta.FileLength);
+ }
+ }
+}
diff --git a/src/Luban.Client/Source/Utils/DownloadFileUtil.cs b/src/Luban.Client/Source/Utils/DownloadFileUtil.cs
new file mode 100644
index 0000000..b5c9c60
--- /dev/null
+++ b/src/Luban.Client/Source/Utils/DownloadFileUtil.cs
@@ -0,0 +1,58 @@
+using Luban.Client.Common.Net;
+using Luban.Common.Protos;
+using Luban.Common.Utils;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Luban.Client.Common.Utils
+{
+ public static class DownloadFileUtil
+ {
+ private static readonly NLog.Logger s_logger = NLog.LogManager.GetCurrentClassLogger();
+
+ const int DOWNLOAD_TIMTOUT = 10;
+
+ public static async Task DownloadGeneratedFiles(string outputDir, List newFiles)
+ {
+ List tasks = new List();
+ foreach (var file in newFiles)
+ {
+ if (!await CacheMetaManager.Ins.CheckFileChangeAsync(outputDir, file.FilePath, file.MD5))
+ {
+ continue;
+ }
+ tasks.Add(Task.Run(async () =>
+ {
+ s_logger.Trace("new code file:{@file}", file);
+ GetOutputFileRes res = await GenClient.Ins.Session.CallRpcAsync(new GetOutputFileArg()
+ {
+ MD5 = file.MD5,
+ }, DOWNLOAD_TIMTOUT);
+
+ await FileUtil.SaveFileAsync(outputDir, file.FilePath, res.FileContent);
+ await CacheMetaManager.Ins.UpdateFileAsync(outputDir, file.FilePath, file.MD5);
+ }));
+ }
+ await Task.WhenAll(tasks);
+
+ // todo 感觉有点问题哈,不是每个生成目录都需要clean up 的
+ FileCleaner.Clean(outputDir, newFiles);
+
+ }
+
+ public static async Task DownloadGeneratedFile(FileInfo file)
+ {
+ if (!await CacheMetaManager.Ins.CheckFileChangeAsync(null, file.FilePath, file.MD5))
+ {
+ return;
+ }
+ GetOutputFileRes res = await GenClient.Ins.Session.CallRpcAsync(new GetOutputFileArg()
+ {
+ MD5 = file.MD5,
+ }, DOWNLOAD_TIMTOUT).ConfigureAwait(false);
+
+ await FileUtil.SaveFileAsync(null, file.FilePath, res.FileContent);
+ await CacheMetaManager.Ins.UpdateFileAsync(null, file.FilePath, file.MD5);
+ }
+ }
+}
diff --git a/src/Luban.Client/Source/Utils/FileCleaner.cs b/src/Luban.Client/Source/Utils/FileCleaner.cs
new file mode 100644
index 0000000..8051038
--- /dev/null
+++ b/src/Luban.Client/Source/Utils/FileCleaner.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Luban.Client.Common.Utils
+{
+ class FileCleaner
+ {
+ private static readonly NLog.Logger s_logger = NLog.LogManager.GetCurrentClassLogger();
+
+ private readonly HashSet _outputDirs = new HashSet();
+ private readonly HashSet _savedFileOrDirs = new HashSet();
+ private readonly HashSet _ignoreFileExtensions = new HashSet();
+
+
+ public void AddIgnoreExtension(string ext)
+ {
+ _ignoreFileExtensions.Add(ext);
+ }
+
+ public void AddOutputDir(string dir)
+ {
+ _outputDirs.Add(dir);
+ }
+
+ public void AddSavedFile(string file)
+ {
+ file = file.Replace('\\', '/');
+ while (true)
+ {
+ _savedFileOrDirs.Add(file);
+ s_logger.Trace("add save file:{file}", file);
+ int sepIndex = file.LastIndexOf('/');
+ if (sepIndex < 0)
+ {
+ break;
+ }
+
+ file = file[..sepIndex];
+ }
+ }
+
+
+ public void RemoveUnusedFiles()
+ {
+ foreach (var dir in _outputDirs)
+ {
+ RemoveUnusedFileInDir(dir);
+ }
+ }
+
+ private void RemoveUnusedFileInDir(string dir)
+ {
+ if (!Directory.Exists(dir))
+ {
+ return;
+ }
+
+ var fullRootPath = Path.GetFullPath(dir);
+ s_logger.Trace("full path:{path}", fullRootPath);
+
+ foreach (var file in Directory.GetFiles(dir, "*", SearchOption.AllDirectories))
+ {
+ s_logger.Trace("file:{file}", file);
+ string fullSubFilePath = Path.GetFullPath(file);
+ var relateFile = fullSubFilePath[(fullRootPath.Length + 1)..].Replace('\\', '/');
+ if (_savedFileOrDirs.Contains(relateFile))
+ {
+ s_logger.Trace("remain file:{file}", file);
+ }
+ else
+ {
+ s_logger.Info("[remove] file: {file}", file);
+ File.Delete(file);
+ }
+ }
+
+ // 清除空目录
+ var subDirs = new List(Directory.GetDirectories(dir, "*", SearchOption.AllDirectories));
+ subDirs.Sort((a, b) => -a.CompareTo(b));
+ foreach (var subDir in subDirs)
+ {
+ string fullSubDirPath = Path.GetFullPath(subDir);
+ var relateDir = fullSubDirPath[(fullRootPath.Length + 1)..].Replace('\\', '/');
+ if (_savedFileOrDirs.Contains(relateDir))
+ {
+ s_logger.Trace("remain directory:{dir}", relateDir);
+ }
+ else
+ {
+ s_logger.Info("[remove] dir: {dir}", subDir);
+ Directory.Delete(subDir);
+ }
+ }
+ }
+
+
+ public static void Clean(string outputDir, List savedFiles)
+ {
+ var cleaner = new FileCleaner();
+ cleaner.AddOutputDir(outputDir);
+ cleaner.AddIgnoreExtension("meta"); // for unity
+ foreach (var file in savedFiles)
+ {
+ cleaner.AddSavedFile(file.FilePath);
+ }
+ cleaner.RemoveUnusedFiles();
+ }
+
+ }
+}
diff --git a/src/Luban.Common/.editorconfig b/src/Luban.Common/.editorconfig
new file mode 100644
index 0000000..ea2620e
--- /dev/null
+++ b/src/Luban.Common/.editorconfig
@@ -0,0 +1,25 @@
+[*.cs]
+
+# CA1707: 标识符不应包含下划线
+dotnet_diagnostic.CA1707.severity = none
+
+# CA1305: 指定 IFormatProvider
+dotnet_diagnostic.CA1305.severity = none
+
+# CA1303: 请不要将文本作为本地化参数传递
+dotnet_diagnostic.CA1303.severity = none
+
+# CA1062: 验证公共方法的参数
+dotnet_diagnostic.CA1062.severity = none
+
+# CA2227: 集合属性应为只读
+dotnet_diagnostic.CA2227.severity = none
+
+# CA1819: 属性不应返回数组
+dotnet_diagnostic.CA1819.severity = none
+
+# CA1304: 指定 CultureInfo
+dotnet_diagnostic.CA1304.severity = none
+
+# CA1031: 不捕获常规异常类型
+dotnet_diagnostic.CA1031.severity = none
diff --git a/src/Luban.Common/Luban.Common.csproj b/src/Luban.Common/Luban.Common.csproj
new file mode 100644
index 0000000..2cc6075
--- /dev/null
+++ b/src/Luban.Common/Luban.Common.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netcoreapp3.1
+
+
+
+
+
+
+
diff --git a/src/Luban.Common/Source/EErrorCode.cs b/src/Luban.Common/Source/EErrorCode.cs
new file mode 100644
index 0000000..d96e1f2
--- /dev/null
+++ b/src/Luban.Common/Source/EErrorCode.cs
@@ -0,0 +1,12 @@
+namespace Luban.Common
+{
+ public enum EErrorCode
+ {
+ OK,
+ UNKNOWN_JOB_TYPE,
+ FILE_OR_DIR_NOT_EXISTS,
+ READ_FILE_FAIL,
+ JOB_ARGUMENT_ERROR,
+ JOB_EXCEPTION,
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/FileInfo.cs b/src/Luban.Common/Source/Protos/FileInfo.cs
new file mode 100644
index 0000000..5493015
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/FileInfo.cs
@@ -0,0 +1,28 @@
+using Bright.Serialization;
+
+namespace Luban.Common.Protos
+{
+ public class FileInfo : BeanBase
+ {
+ public string FilePath { get; set; }
+
+ public string MD5 { get; set; }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(FilePath);
+ os.WriteString(MD5);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ FilePath = os.ReadString();
+ MD5 = os.ReadString();
+ }
+
+ public override int GetTypeId()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/GenJob.cs b/src/Luban.Common/Source/Protos/GenJob.cs
new file mode 100644
index 0000000..94383e6
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/GenJob.cs
@@ -0,0 +1,109 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+using System;
+using System.Collections.Generic;
+
+namespace Luban.Common.Protos
+{
+ public class GenJobArg : BeanBase
+ {
+ public string JobType { get; set; }
+
+ public List JobArguments { get; set; }
+
+ public bool Verbose { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(JobType);
+ Bright.Common.SerializationUtil.Serialize(os, JobArguments);
+ os.WriteBool(Verbose);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ JobType = os.ReadString();
+ Bright.Common.SerializationUtil.Deserialize(os, JobArguments = new List());
+ Verbose = os.ReadBool();
+ }
+ }
+
+
+ public class FileGroup : BeanBase
+ {
+ public string Dir { get; set; }
+
+ public List Files { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(Dir);
+ Bright.Common.SerializationUtil.Serialize(os, Files);
+
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ Dir = os.ReadString();
+ Bright.Common.SerializationUtil.Deserialize(os, Files = new List());
+ }
+ }
+
+
+ public class GenJobRes : BeanBase
+ {
+ public EErrorCode ErrCode { get; set; }
+
+ public string ErrMsg { get; set; }
+
+ public List FileGroups { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteInt((int)ErrCode);
+ os.WriteString(ErrMsg);
+ Bright.Common.SerializationUtil.Serialize(os, FileGroups);
+ }
+ public override void Deserialize(ByteBuf os)
+ {
+ ErrCode = (EErrorCode)os.ReadInt();
+ ErrMsg = os.ReadString();
+ Bright.Common.SerializationUtil.Deserialize(os, FileGroups = new List());
+ }
+ }
+
+ public class GenJob : Rpc
+ {
+ public const int ID = 100;
+
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public override object Clone()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Reset()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/GetImportFileOrDirectory.cs b/src/Luban.Common/Source/Protos/GetImportFileOrDirectory.cs
new file mode 100644
index 0000000..f54966b
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/GetImportFileOrDirectory.cs
@@ -0,0 +1,79 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+using System;
+using System.Collections.Generic;
+
+namespace Luban.Common.Protos
+{
+ public class GetImportFileOrDirectoryArg : BeanBase
+ {
+ public string FileOrDirName { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(FileOrDirName);
+ }
+ public override void Deserialize(ByteBuf os)
+ {
+ FileOrDirName = os.ReadString();
+ }
+ }
+
+ public class GetImportFileOrDirectoryRes : BeanBase
+ {
+ public EErrorCode Err { get; set; }
+
+ public bool IsFile { get; set; }
+
+ public string Md5 { get; set; }
+
+ //public byte[] Content { get; set; }
+
+ public List SubFiles { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteInt((int)Err);
+ os.WriteBool(IsFile);
+ os.WriteString(Md5);
+ Bright.Common.SerializationUtil.Serialize(os, SubFiles);
+ }
+ public override void Deserialize(ByteBuf os)
+ {
+ Err = (EErrorCode)os.ReadInt();
+ IsFile = os.ReadBool();
+ Md5 = os.ReadString();
+ Bright.Common.SerializationUtil.Deserialize(os, SubFiles = new List());
+ }
+ }
+
+ public class GetImportFileOrDirectory : Rpc
+ {
+ public const int ID = 108;
+
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public override void Reset()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override object Clone()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/GetInputFile.cs b/src/Luban.Common/Source/Protos/GetInputFile.cs
new file mode 100644
index 0000000..6ea9c0b
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/GetInputFile.cs
@@ -0,0 +1,71 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+
+namespace Luban.Common.Protos
+{
+ public class GetInputFileArg : BeanBase
+ {
+ public string File { get; set; }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(File);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ File = os.ReadString();
+ }
+
+ public override int GetTypeId()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+
+
+
+ public class GetInputFileRes : BeanBase
+ {
+ public EErrorCode Err { get; set; }
+
+ public byte[] Content { get; set; }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteInt((int)Err);
+ os.WriteBytes(Content);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ Err = (EErrorCode)os.ReadInt();
+ Content = os.ReadBytes();
+ }
+
+ public override int GetTypeId()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+
+ public class GetInputFile : Rpc
+ {
+ public const int ID = 102;
+
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public override void Reset()
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override object Clone()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/GetOutputFile.cs b/src/Luban.Common/Source/Protos/GetOutputFile.cs
new file mode 100644
index 0000000..e61849d
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/GetOutputFile.cs
@@ -0,0 +1,80 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+
+namespace Luban.Common.Protos
+{
+
+ public class GetOutputFile : Rpc
+ {
+ public const int ID = 103;
+
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public override void Reset()
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override object Clone()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+
+
+ public class GetOutputFileArg : BeanBase
+ {
+ //public string Type { get; set; }
+
+ //public string RelatePath { get; set; }
+
+ public string MD5 { get; set; }
+
+ public override int GetTypeId()
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ //os.WriteString(Type);
+ //os.WriteString(RelatePath);
+ os.WriteString(MD5);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ //Type = os.ReadString();
+ //RelatePath = os.ReadString();
+ MD5 = os.ReadString();
+ }
+ }
+
+
+
+ public class GetOutputFileRes : BeanBase
+ {
+ public bool Exists { get; set; }
+ public byte[] FileContent { get; set; }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteBool(Exists);
+ os.WriteBytes(FileContent);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ Exists = os.ReadBool();
+ FileContent = os.ReadBytes();
+ }
+
+ public override int GetTypeId()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/ProtocolStub.cs b/src/Luban.Common/Source/Protos/ProtocolStub.cs
new file mode 100644
index 0000000..4988b35
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/ProtocolStub.cs
@@ -0,0 +1,19 @@
+using Bright.Net.Codecs;
+using System.Collections.Generic;
+
+namespace Luban.Common.Protos
+{
+ public static class ProtocolStub
+ {
+ public static Dictionary Factories { get; } = new Dictionary
+ {
+ [GetInputFile.ID] = () => new GetInputFile(),
+ [GetOutputFile.ID] = () => new GetOutputFile(),
+ [PushLog.ID] = () => new PushLog(),
+ [PushException.ID] = () => new PushException(),
+ [GenJob.ID] = () => new GenJob(),
+ [GetImportFileOrDirectory.ID] = () => new GetImportFileOrDirectory(),
+ [QueryFilesExists.ID] = () => new QueryFilesExists(),
+ };
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/PushException.cs b/src/Luban.Common/Source/Protos/PushException.cs
new file mode 100644
index 0000000..5e66ac6
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/PushException.cs
@@ -0,0 +1,44 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+
+namespace Luban.Common.Protos
+{
+ public class PushException : Protocol
+ {
+ public const int ID = 105;
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public string LogContent { get; set; }
+
+ public string Message { get; set; }
+
+ public string StackTrace { get; set; }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(LogContent);
+ os.WriteString(Message);
+ os.WriteString(StackTrace);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ LogContent = os.ReadString();
+ Message = os.ReadString();
+ StackTrace = os.ReadString();
+ }
+
+ public override object Clone()
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override void Reset()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/PushLog.cs b/src/Luban.Common/Source/Protos/PushLog.cs
new file mode 100644
index 0000000..12c8323
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/PushLog.cs
@@ -0,0 +1,41 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+
+namespace Luban.Common.Protos
+{
+
+ public class PushLog : Protocol
+ {
+ public const int ID = 104;
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public string Level { get; set; }
+
+ public string LogContent { get; set; }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(Level);
+ os.WriteString(LogContent);
+ }
+
+ public override void Deserialize(ByteBuf os)
+ {
+ Level = os.ReadString();
+ LogContent = os.ReadString();
+ }
+
+ public override object Clone()
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override void Reset()
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Protos/QueryFilesExists.cs b/src/Luban.Common/Source/Protos/QueryFilesExists.cs
new file mode 100644
index 0000000..f9b2af0
--- /dev/null
+++ b/src/Luban.Common/Source/Protos/QueryFilesExists.cs
@@ -0,0 +1,78 @@
+using Bright.Net.Codecs;
+using Bright.Serialization;
+using System;
+using System.Collections.Generic;
+
+namespace Luban.Common.Protos
+{
+ public class QueryFilesExistsArg : BeanBase
+ {
+ public string Root { get; set; }
+
+ public List Files { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteString(Root);
+ Bright.Common.SerializationUtil.Serialize(os, Files);
+ }
+ public override void Deserialize(ByteBuf os)
+ {
+ Root = os.ReadString();
+ Bright.Common.SerializationUtil.Deserialize(os, Files = new List());
+ }
+ }
+
+ public class QueryFilesExistsRes : BeanBase
+ {
+ public List Exists { get; set; }
+
+ public override int GetTypeId()
+ {
+ return 0;
+ }
+
+ public override void Serialize(ByteBuf os)
+ {
+ os.WriteSize(Exists.Count);
+ foreach (var v in Exists)
+ {
+ os.WriteBool(v);
+ }
+ }
+ public override void Deserialize(ByteBuf os)
+ {
+ int n = os.ReadSize();
+ Exists = new List();
+ for (int i = 0; i < n; i++)
+ {
+ Exists.Add(os.ReadBool());
+ }
+ }
+ }
+
+ public class QueryFilesExists : Rpc
+ {
+ public const int ID = 106;
+
+ public override int GetTypeId()
+ {
+ return ID;
+ }
+
+ public override object Clone()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Reset()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Utils/FileUtil.cs b/src/Luban.Common/Source/Utils/FileUtil.cs
new file mode 100644
index 0000000..07b393b
--- /dev/null
+++ b/src/Luban.Common/Source/Utils/FileUtil.cs
@@ -0,0 +1,145 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Luban.Common.Utils
+{
+ public static class FileUtil
+ {
+ private static readonly NLog.Logger s_logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public static string GetFileName(string path)
+ {
+ int index = path.Replace('\\', '/').LastIndexOf('/');
+ return index >= 0 ? path[(index + 1)..] : path;
+ }
+
+ public static string GetParent(string path)
+ {
+ int index = path.Replace('\\', '/').LastIndexOf('/');
+ return index >= 0 ? path[..index] : path;
+ }
+
+ public static string GetPathRelateRootFile(string rootFile, string file)
+ {
+ return Combine(GetParent(rootFile), file);
+ }
+
+ ///
+ /// 忽略以 文件名以 '.' '_' '~' 开头的文件
+ ///
+ ///
+ ///
+ public static bool IsValidInputFile(string file)
+ {
+ if (!File.Exists(file))
+ {
+ return false;
+ }
+ var f = new FileInfo(file);
+ string fname = f.Name;
+ return !fname.StartsWith('.') && !fname.StartsWith('_') && !fname.StartsWith('~');
+ }
+
+ [ThreadStatic]
+ private static MD5 s_cacheMd5;
+
+ private static MD5 CacheMd5
+ {
+ get
+ {
+ var md5 = s_cacheMd5 ??= MD5.Create();
+ md5.Clear();
+ return md5;
+ }
+ }
+
+ public static string CalcMD5(byte[] srcBytes)
+ {
+ using MD5 md5 = MD5.Create();
+ var md5Bytes = md5.ComputeHash(srcBytes);
+ var s = new StringBuilder(md5Bytes.Length * 2);
+ foreach (var b in md5Bytes)
+ {
+ s.Append(b.ToString("X"));
+ }
+ return s.ToString();
+ }
+
+ public static string Standardize(string path)
+ {
+ return path.Replace('\\', '/');
+ }
+
+ public static string Combine(string parent, string sub)
+ {
+ return Standardize(Path.Combine(parent, sub));
+ }
+
+ public static async Task SaveFileAsync(string relateDir, string filePath, byte[] content)
+ {
+ // 调用此接口时,已保证 文件必然是改变的,不用再检查对比文件
+ var outputPath = Standardize(relateDir != null ? System.IO.Path.Combine(relateDir, filePath) : filePath);
+ Directory.GetParent(outputPath).Create();
+ if (File.Exists(outputPath))
+ {
+ //if (CheckFileNotChange(outputPath, content))
+ //{
+ // s_logger.Trace("[not change] {file}", outputPath);
+ // return;
+ //}
+ //else
+ //{
+ // s_logger.Info("[override] {file}", outputPath);
+ // if (File.GetAttributes(outputPath).HasFlag(FileAttributes.ReadOnly))
+ // {
+ // File.SetAttributes(outputPath, FileAttributes.Normal);
+ // }
+ //}
+ s_logger.Info("[override] {file}", outputPath);
+ if (File.GetAttributes(outputPath).HasFlag(FileAttributes.ReadOnly))
+ {
+ File.SetAttributes(outputPath, FileAttributes.Normal);
+ }
+ }
+ else
+ {
+ s_logger.Info("[new] {file}", outputPath);
+ }
+
+ await File.WriteAllBytesAsync(outputPath, content);
+ }
+
+ public static async Task ReadAllBytesAsync(string file)
+ {
+ // File.ReadAllBytesAsync 无法读取被打开的excel文件,只好重新实现了一个
+ using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ long count = fs.Length;
+ var bytes = new byte[count];
+ int writeIndex = 0;
+ while (writeIndex < count)
+ {
+ int n = await fs.ReadAsync(bytes, writeIndex, (int)count - writeIndex, default);
+ writeIndex += n;
+ }
+ return bytes;
+ }
+
+ public static byte[] ReadAllBytes(string file)
+ {
+ // File.ReadAllBytesAsync 无法读取被打开的excel文件,只好重新实现了一个
+ using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ long count = fs.Length;
+ var bytes = new byte[count];
+ int writeIndex = 0;
+ while (writeIndex < count)
+ {
+ int n = fs.Read(bytes, writeIndex, (int)count - writeIndex);
+ writeIndex += n;
+ }
+ return bytes;
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Utils/LogUtil.cs b/src/Luban.Common/Source/Utils/LogUtil.cs
new file mode 100644
index 0000000..3c99316
--- /dev/null
+++ b/src/Luban.Common/Source/Utils/LogUtil.cs
@@ -0,0 +1,15 @@
+namespace Luban.Common.Utils
+{
+ public static class LogUtil
+ {
+ public static void InitSimpleNLogConfigure(NLog.LogLevel minConsoleLogLevel)
+ {
+ var logConfig = new NLog.Config.LoggingConfiguration();
+ //var layout = NLog.Layouts.Layout.FromString("${longdate}|${level:uppercase=true}|${threadid}|${message}${onexception:${newline}${exception:format=tostring}${exception:format=StackTrace}}");
+ var layout = NLog.Layouts.Layout.FromString("${longdate}|${message}${onexception:${newline}${exception:format=tostring}${exception:format=StackTrace}}");
+ logConfig.AddTarget("console", new NLog.Targets.ColoredConsoleTarget() { Layout = layout });
+ logConfig.AddRule(minConsoleLogLevel, NLog.LogLevel.Fatal, "console");
+ NLog.LogManager.Configuration = logConfig;
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Utils/ProfileTimer.cs b/src/Luban.Common/Source/Utils/ProfileTimer.cs
new file mode 100644
index 0000000..70d65f3
--- /dev/null
+++ b/src/Luban.Common/Source/Utils/ProfileTimer.cs
@@ -0,0 +1,43 @@
+using Bright.Time;
+using System.Collections.Generic;
+
+namespace Luban.Common.Utils
+{
+ public class Phase
+ {
+ public string Name { get; set; }
+
+ public long StartTime { get; set; }
+
+ public long EndTime { get; set; }
+
+ public long ElapseTime { get; set; }
+ }
+
+ public class ProfileTimer
+ {
+ private static readonly NLog.Logger s_logger = NLog.LogManager.GetCurrentClassLogger();
+
+ private readonly Stack phaseStack = new Stack();
+
+ public void StartPhase(string name)
+ {
+ phaseStack.Push(new Phase() { Name = name, StartTime = TimeUtil.NowMillis });
+ }
+
+ private Phase EndPhase()
+ {
+ var phase = phaseStack.Pop();
+ phase.EndTime = TimeUtil.NowMillis;
+ phase.ElapseTime = phase.EndTime - phase.StartTime;
+ return phase;
+ }
+
+ public Phase EndPhaseAndLog()
+ {
+ var phase = EndPhase();
+ s_logger.Info("====== {name} cost {time} ms ======", phase.Name, phase.ElapseTime);
+ return phase;
+ }
+ }
+}
diff --git a/src/Luban.Common/Source/Utils/TypeUtil.cs b/src/Luban.Common/Source/Utils/TypeUtil.cs
new file mode 100644
index 0000000..c2b5aba
--- /dev/null
+++ b/src/Luban.Common/Source/Utils/TypeUtil.cs
@@ -0,0 +1,207 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Luban.Common.Utils
+{
+ public static class TypeUtil
+ {
+
+ public static (string, string) SplitFullName(string fullName)
+ {
+ int index = fullName.LastIndexOf('.');
+ return (fullName.Substring(0, index), fullName.Substring(index + 1));
+ }
+
+ public static string MakeFullName(Stack