feat: Initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
.idea/
|
||||
.claude/
|
||||
.qoder/
|
||||
2257
Cargo.lock
generated
Normal file
2257
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = []
|
||||
|
||||
[package]
|
||||
name = "emod-cli"
|
||||
version = "0.1.0-dev"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.32", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
zip = "2.2.2"
|
||||
walkdir = "2"
|
||||
anyhow = "1.0.97"
|
||||
dirs = "5.0"
|
||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# emod-cli
|
||||
|
||||
> 🚧 WIP 请勿使用,项目处于快速原型开发实验中。
|
||||
|
||||
## 介绍
|
||||
|
||||
基于网易我的世界组件开发的 cli 工具,用于管理组件的创建、打包等
|
||||
|
||||
## 如何使用
|
||||
|
||||
```bash
|
||||
# 创建一个 Addon 项目
|
||||
emod-cli create --name <项目名> --target [目标例子]
|
||||
# 打包一个 Addon 项目
|
||||
emod-cli release --path <项目路径> --version [发布版本]
|
||||
```
|
||||
|
||||
## 未来计划
|
||||
|
||||
- [ ] 重构代码,使代码结构更加合理
|
||||
- [ ] 更新预设命令,可一行创建预设资源
|
||||
10
examples/VARIABLES.md
Normal file
10
examples/VARIABLES.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# VARIABLES
|
||||
|
||||
示例中的变量替换
|
||||
|
||||
## 变量列表
|
||||
|
||||
|变量名|描述|
|
||||
|:----|:----|
|
||||
|`__mod_name__`|项目名|
|
||||
|`__mod_name_lower__`|项目名小写驼峰|
|
||||
4
examples/default/.gitignore
vendored
Normal file
4
examples/default/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
studio.json
|
||||
.mcs/editorSave.json
|
||||
.mcs/images/
|
||||
0
examples/default/behavior_pack/BoxData/.gitkeep
Normal file
0
examples/default/behavior_pack/BoxData/.gitkeep
Normal file
0
examples/default/behavior_pack/Parts/.gitkeep
Normal file
0
examples/default/behavior_pack/Parts/.gitkeep
Normal file
0
examples/default/behavior_pack/Presets/.gitkeep
Normal file
0
examples/default/behavior_pack/Presets/.gitkeep
Normal file
0
examples/default/behavior_pack/entities/.gitkeep
Normal file
0
examples/default/behavior_pack/entities/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
class ServerEvents(object):
|
||||
BlockRandomTickServerEvent = "BlockRandomTickServerEvent"
|
||||
ServerBlockEntityTickEvent = "ServerBlockEntityTickEvent"
|
||||
@@ -0,0 +1,25 @@
|
||||
from mod.common.system.baseSystem import BaseSystem
|
||||
|
||||
|
||||
def listen(event, namespace=None, system_name=None):
|
||||
def decorator(func):
|
||||
func._annotation_listen = event
|
||||
func._listen_namespace = namespace
|
||||
func._listen_system_name = system_name
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def inject_listener(instance, system, namespace, system_name): # type: (object, BaseSystem, str, str) -> None
|
||||
for name, method in instance.__class__.__dict__.items():
|
||||
if (callable(method) and hasattr(method, '_annotation_listen')
|
||||
and hasattr(method, '_listen_namespace')
|
||||
and hasattr(method, '_listen_system_name')):
|
||||
event = method._annotation_listen
|
||||
anno_namespace = method._listen_namespace
|
||||
anno_system_name = method._listen_system_name
|
||||
final_namespace = anno_namespace if anno_system_name is not None else namespace
|
||||
final_system_name = anno_system_name if anno_system_name is not None else system_name
|
||||
system.ListenForEvent(final_namespace, final_system_name, event, instance, method)
|
||||
print(event)
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import mod.server.extraServerApi as serverApi
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
from mod.common.system.baseSystem import BaseSystem
|
||||
|
||||
from __mod_name_lower__Scripts.listen.listen import inject_listener
|
||||
|
||||
|
||||
class EasyModBaseSystem(serverApi.GetServerSystemCls()):
|
||||
|
||||
def __init__(self, namespace, system_name, engine_namespace, engine_system_name):
|
||||
super(EasyModBaseSystem, self).__init__(namespace, system_name)
|
||||
inject_listener(self.__class__, self, engine_namespace, engine_system_name)
|
||||
|
||||
class EasyModServerSystem(EasyModBaseSystem, ServerSystem):
|
||||
|
||||
def __init__(self, namespace, system_name, engine_namespace, engine_system_name):
|
||||
super(EasyModServerSystem, self).__init__(namespace, system_name, engine_namespace, engine_system_name)
|
||||
|
||||
class EasyModClientSystem(EasyModBaseSystem):
|
||||
@@ -0,0 +1,7 @@
|
||||
ProjectName = "Circus"
|
||||
|
||||
ServerSystemName = "CircusServerSystem"
|
||||
ServerSystemPath = "circusScripts.modServer.serverSystem.CircusServerSystem"
|
||||
|
||||
ClientSystemName = "CircusClientSystem"
|
||||
ClientSystemPath = "circusScripts.modClient.clientSystem.CircusClientSystem"
|
||||
28
examples/default/behavior_pack/exampleScripts/modMain.py
Normal file
28
examples/default/behavior_pack/exampleScripts/modMain.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from mod.common.mod import Mod
|
||||
import mod.server.extraServerApi as serverApi
|
||||
|
||||
|
||||
@Mod.Binding(name="__mod_name__", version="0.0.1")
|
||||
class __mod_name__(object):
|
||||
|
||||
def __init__(self):
|
||||
print("===== init __mod_name__ mod =====")
|
||||
|
||||
@Mod.InitServer()
|
||||
def on_server_init(self):
|
||||
print("===== init __mod_name__ server =====")
|
||||
serverApi.RegisterSystem("__mod_name__", "__mod_name__ServerSystem", "__mod_name_lower__Scripts.modServer.serverSystem.__mod_name__ServerSystem")
|
||||
|
||||
@Mod.DestroyServer()
|
||||
def on_server_destroy(self):
|
||||
pass
|
||||
|
||||
@Mod.InitClient()
|
||||
def on_init_client(self):
|
||||
pass
|
||||
|
||||
@Mod.DestroyClient()
|
||||
def on_init_destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import mod.server.extraServerApi as serverApi
|
||||
|
||||
from __mod_name_lower__Scripts.modCommon.emodSystem import EasyModBaseSystem
|
||||
|
||||
ServerSystem = serverApi.GetServerSystemCls()
|
||||
compFactory = serverApi.GetEngineCompFactory()
|
||||
namespace = serverApi.GetEngineNamespace()
|
||||
engineSystemName = serverApi.GetEngineSystemName()
|
||||
|
||||
|
||||
class __mod_name__ServerSystem(EasyModServerSystem):
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
super(__mod_name__ServerSystem, self).__init__(namespace, systemName)
|
||||
print("===== __mod_name__ServerSystem init =====")
|
||||
|
||||
0
examples/default/behavior_pack/items/.gitkeep
Normal file
0
examples/default/behavior_pack/items/.gitkeep
Normal file
0
examples/default/behavior_pack/loot_tables/.gitkeep
Normal file
0
examples/default/behavior_pack/loot_tables/.gitkeep
Normal file
BIN
examples/default/behavior_pack/pack_icon.jpg
Normal file
BIN
examples/default/behavior_pack/pack_icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
31
examples/default/behavior_pack/pack_manifest.json
Normal file
31
examples/default/behavior_pack/pack_manifest.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"description": "",
|
||||
"type": "data",
|
||||
"uuid": "{behavior_module_uuid}",
|
||||
"version": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"header": {
|
||||
"description": "",
|
||||
"name": "behavior_pack",
|
||||
"uuid": "{behavior_pack_uuid}",
|
||||
"version": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
18,
|
||||
0
|
||||
]
|
||||
},
|
||||
"dependencies": [],
|
||||
"format_version": 1
|
||||
}
|
||||
0
examples/default/behavior_pack/recipes/.gitkeep
Normal file
0
examples/default/behavior_pack/recipes/.gitkeep
Normal file
0
examples/default/behavior_pack/spawn_rules/.gitkeep
Normal file
0
examples/default/behavior_pack/spawn_rules/.gitkeep
Normal file
0
examples/default/behavior_pack/structures/.gitkeep
Normal file
0
examples/default/behavior_pack/structures/.gitkeep
Normal file
0
examples/default/behavior_pack/trading/.gitkeep
Normal file
0
examples/default/behavior_pack/trading/.gitkeep
Normal file
0
examples/default/resource_pack/animations/.gitkeep
Normal file
0
examples/default/resource_pack/animations/.gitkeep
Normal file
0
examples/default/resource_pack/attachables/.gitkeep
Normal file
0
examples/default/resource_pack/attachables/.gitkeep
Normal file
7
examples/default/resource_pack/blocks.json
Normal file
7
examples/default/resource_pack/blocks.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"format_version": [
|
||||
1,
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
0
examples/default/resource_pack/effects/.gitkeep
Normal file
0
examples/default/resource_pack/effects/.gitkeep
Normal file
0
examples/default/resource_pack/entity/.gitkeep
Normal file
0
examples/default/resource_pack/entity/.gitkeep
Normal file
0
examples/default/resource_pack/font/.gitkeep
Normal file
0
examples/default/resource_pack/font/.gitkeep
Normal file
0
examples/default/resource_pack/materials/.gitkeep
Normal file
0
examples/default/resource_pack/materials/.gitkeep
Normal file
0
examples/default/resource_pack/models/mesh/.gitkeep
Normal file
0
examples/default/resource_pack/models/mesh/.gitkeep
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"skeleton": [
|
||||
{
|
||||
"name": "root",
|
||||
"parent": "root",
|
||||
"initpos": [
|
||||
0.0,
|
||||
0.0,
|
||||
-0.0
|
||||
],
|
||||
"initquaternion": [
|
||||
-0.0000016026669982238673,
|
||||
-0.0000015070728522914579,
|
||||
0.697554349899292,
|
||||
0.7165318727493286
|
||||
],
|
||||
"initscale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"offmtx": [
|
||||
[
|
||||
0.026835869997739793,
|
||||
0.9996398091316223,
|
||||
-1.1985919456947159e-7,
|
||||
-2.22485263889673e-17
|
||||
],
|
||||
[
|
||||
-0.9996398091316223,
|
||||
0.026835869997739793,
|
||||
-0.000004400427314976696,
|
||||
-8.168171115038121e-16
|
||||
],
|
||||
[
|
||||
-0.000004395626092446037,
|
||||
2.3790533987266827e-7,
|
||||
1.0,
|
||||
1.856222120455442e-10
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"boundingbox": [
|
||||
[
|
||||
-0.007048234809190035,
|
||||
-7.17939763195119e-10,
|
||||
-0.012045030482113362
|
||||
],
|
||||
[
|
||||
0.007048234809190035,
|
||||
0.01892676018178463,
|
||||
0.012045029550790787
|
||||
]
|
||||
]
|
||||
}
|
||||
BIN
examples/default/resource_pack/pack_icon.jpg
Normal file
BIN
examples/default/resource_pack/pack_icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
30
examples/default/resource_pack/pack_manifest.json
Normal file
30
examples/default/resource_pack/pack_manifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"header": {
|
||||
"description": "",
|
||||
"min_engine_version": [
|
||||
1,
|
||||
18,
|
||||
0
|
||||
],
|
||||
"name": "resource_pack",
|
||||
"uuid": "{resource_pack_uuid}",
|
||||
"version": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"description": "",
|
||||
"type": "resources",
|
||||
"uuid": "{resource_module_uuid}",
|
||||
"version": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
0
examples/default/resource_pack/sounds/.gitkeep
Normal file
0
examples/default/resource_pack/sounds/.gitkeep
Normal file
0
examples/default/resource_pack/texts/zh_CN.lang
Normal file
0
examples/default/resource_pack/texts/zh_CN.lang
Normal file
BIN
examples/default/resource_pack/textures/blocks/custom_brah.png
Normal file
BIN
examples/default/resource_pack/textures/blocks/custom_brah.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 286 B |
BIN
examples/default/resource_pack/textures/blocks/custom_dirt.png
Normal file
BIN
examples/default/resource_pack/textures/blocks/custom_dirt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 295 B |
BIN
examples/default/resource_pack/textures/items/custom_apple.png
Normal file
BIN
examples/default/resource_pack/textures/items/custom_apple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 257 B |
BIN
examples/default/resource_pack/textures/sfxs/custom_sun.png
Normal file
BIN
examples/default/resource_pack/textures/sfxs/custom_sun.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"resource_pack_name": "vanilla",
|
||||
"texture_data": {
|
||||
|
||||
},
|
||||
"texture_name": "atlas.terrain"
|
||||
}
|
||||
0
examples/default/resource_pack/textures/ui/.gitkeep
Normal file
0
examples/default/resource_pack/textures/ui/.gitkeep
Normal file
0
examples/default/resource_pack/ui/.gitkeep
Normal file
0
examples/default/resource_pack/ui/.gitkeep
Normal file
11
examples/default/world_behavior_packs.json
Normal file
11
examples/default/world_behavior_packs.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"pack_id": "{behavior_pack_uuid}",
|
||||
"type": "Addon",
|
||||
"version": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
11
examples/default/world_resource_packs.json
Normal file
11
examples/default/world_resource_packs.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"pack_id": "{resource_pack_uuid}",
|
||||
"type": "Addon",
|
||||
"version": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
222
src/commands/components.rs
Normal file
222
src/commands/components.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use crate::commands::ComponentsArgs;
|
||||
use crate::entity;
|
||||
use crate::utils::file;
|
||||
use crate::error::Result;
|
||||
use serde_json::{json, to_string_pretty};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const COMPONENT_3D_ITEM: &str = "3ditem";
|
||||
|
||||
pub fn execute(args: &ComponentsArgs) {
|
||||
if let Err(e) = run_components(args) {
|
||||
eprintln!("错误: {}", e);
|
||||
return;
|
||||
}
|
||||
println!("成功: 组件已创建");
|
||||
}
|
||||
|
||||
fn run_components(args: &ComponentsArgs) -> Result<()> {
|
||||
let project_path = file::find_project_dir(&args.path)?;
|
||||
|
||||
validate_input_files(&args.geo, &args.texture)?;
|
||||
|
||||
let identifier = args.identifier.as_deref().unwrap_or("unknown");
|
||||
|
||||
match args.component.as_str() {
|
||||
COMPONENT_3D_ITEM => create_3dmodel(
|
||||
args.geo.as_deref().unwrap_or("./model.geo.json"),
|
||||
args.texture.as_deref().unwrap_or("./texture.png"),
|
||||
identifier,
|
||||
&project_path
|
||||
),
|
||||
_ => Err(crate::error::CliError::NotFound(
|
||||
format!("组件 '{}' 不存在", args.component)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_input_files(geo: &Option<String>, texture: &Option<String>) -> Result<()> {
|
||||
let geo_path = geo.as_deref().unwrap_or("./model.geo.json");
|
||||
let texture_path = texture.as_deref().unwrap_or("./texture.png");
|
||||
|
||||
if !PathBuf::from(geo_path).exists() {
|
||||
return Err(crate::error::CliError::NotFound(
|
||||
format!("几何文件 {} 不存在", geo_path)
|
||||
));
|
||||
}
|
||||
|
||||
if !PathBuf::from(texture_path).exists() {
|
||||
return Err(crate::error::CliError::NotFound(
|
||||
format!("材质文件 {} 不存在", texture_path)
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_3dmodel(
|
||||
geo: &str,
|
||||
texture: &str,
|
||||
identifier: &str,
|
||||
project_path: &PathBuf,
|
||||
) -> Result<()> {
|
||||
let project_info = entity::get_current_release_info(&project_path)?;
|
||||
|
||||
let beh_path = project_path.join(format!(
|
||||
"behavior_pack_{}",
|
||||
project_info.behavior_identifier
|
||||
));
|
||||
let res_path = project_path.join(format!(
|
||||
"resource_pack_{}",
|
||||
project_info.resource_identifier
|
||||
));
|
||||
|
||||
create_item_files(&beh_path, &res_path, identifier)?;
|
||||
copy_assets(&res_path, geo, texture, identifier)?;
|
||||
create_attachable_file(&res_path, identifier)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_item_files(beh_path: &PathBuf, res_path: &PathBuf, identifier: &str) -> Result<()> {
|
||||
let behavior_item = create_behavior_item_json(identifier);
|
||||
let resource_item = create_resource_item_json(identifier);
|
||||
|
||||
let f_identifier = identifier.replace(":", "_");
|
||||
|
||||
let items_beh_dir = beh_path.join("netease_items_beh");
|
||||
let items_res_dir = res_path.join("netease_items_res");
|
||||
|
||||
fs::create_dir_all(&items_beh_dir)?;
|
||||
fs::create_dir_all(&items_res_dir)?;
|
||||
|
||||
let beh_item_path = items_beh_dir.join(format!("{}.json", f_identifier));
|
||||
let res_item_path = items_res_dir.join(format!("{}.json", f_identifier));
|
||||
|
||||
fs::write(&beh_item_path, to_string_pretty(&behavior_item)?)?;
|
||||
fs::write(&res_item_path, to_string_pretty(&resource_item)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_behavior_item_json(identifier: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"format_version": "1.10",
|
||||
"minecraft:item": {
|
||||
"components": {
|
||||
"minecraft:max_damage": 10,
|
||||
"netease:armor": {
|
||||
"armor_slot": 3,
|
||||
"defense": 20,
|
||||
"enchantment": 10
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"category": "Equipment",
|
||||
"identifier": identifier,
|
||||
"register_to_create_menu": true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_resource_item_json(identifier: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"format_version": "1.10",
|
||||
"minecraft:item": {
|
||||
"components": {
|
||||
"minecraft:icon": identifier
|
||||
},
|
||||
"description": {
|
||||
"category": "Equipment",
|
||||
"identifier": identifier,
|
||||
"register_to_create_menu": true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn copy_assets(
|
||||
res_path: &PathBuf,
|
||||
geo: &str,
|
||||
texture: &str,
|
||||
identifier: &str,
|
||||
) -> Result<()> {
|
||||
let f_identifier = identifier.replace(":", "_");
|
||||
|
||||
copy_texture(res_path, texture, &f_identifier)?;
|
||||
copy_geometry(res_path, geo, identifier, &f_identifier)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_texture(res_path: &PathBuf, texture: &str, f_identifier: &str) -> Result<()> {
|
||||
let texture_dir = res_path.join("textures/models");
|
||||
fs::create_dir_all(&texture_dir)?;
|
||||
|
||||
let target_texture = texture_dir.join(format!("{}.png", f_identifier));
|
||||
fs::copy(texture, target_texture)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_geometry(
|
||||
res_path: &PathBuf,
|
||||
geo: &str,
|
||||
identifier: &str,
|
||||
f_identifier: &str,
|
||||
) -> Result<()> {
|
||||
let geo_dir = res_path.join("models/entity");
|
||||
fs::create_dir_all(&geo_dir)?;
|
||||
|
||||
let mut geo_value = file::read_file_to_json(&PathBuf::from(geo))?;
|
||||
|
||||
let geo_name = format!("geometry.{}", identifier.replace(":", "."));
|
||||
geo_value["format_version"] = json!("1.12.0");
|
||||
geo_value["minecraft:geometry"][0]["description"]["identifier"] = json!(geo_name);
|
||||
|
||||
let target_geo = geo_dir.join(format!("{}.geo.json", f_identifier));
|
||||
file::write_json_to_file(&target_geo, &geo_value)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_attachable_file(res_path: &PathBuf, identifier: &str) -> Result<()> {
|
||||
let attachable_dir = res_path.join("attachables");
|
||||
fs::create_dir_all(&attachable_dir)?;
|
||||
|
||||
let f_identifier = identifier.replace(":", "_");
|
||||
let geo_name = identifier.replace(":", ".");
|
||||
|
||||
let attachable = json!({
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"geometry": {
|
||||
"default": format!("geometry.{}", &geo_name)
|
||||
},
|
||||
"identifier": identifier,
|
||||
"materials": {
|
||||
"default": "armor",
|
||||
"enchanted": "armor_enchanted"
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.armor"
|
||||
],
|
||||
"scripts": {
|
||||
"parent_setup": "variable.chest_layer_visible = 0.0;"
|
||||
},
|
||||
"textures": {
|
||||
"default": format!("textures/models/{}", &f_identifier),
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let target_file = attachable_dir.join(format!("{}.json", &f_identifier));
|
||||
fs::write(target_file, to_string_pretty(&attachable)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
183
src/commands/create.rs
Normal file
183
src/commands/create.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
entity::project::ProjectInfo,
|
||||
utils::{file, http::HttpClient},
|
||||
};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::commands::CreateArgs;
|
||||
use crate::error::Result;
|
||||
use crate::utils::git;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn execute(args: &CreateArgs, temp_dir: &PathBuf) {
|
||||
if let Err(e) = create_project(&args.name, args.target.as_deref(), temp_dir) {
|
||||
eprintln!("错误: {}", e);
|
||||
return;
|
||||
}
|
||||
println!("成功: 项目已创建");
|
||||
}
|
||||
|
||||
fn create_project(name: &str, target: Option<&str>, temp_dir: &PathBuf) -> Result<()> {
|
||||
let target = target.unwrap_or("default");
|
||||
|
||||
check_example_exists(target)?;
|
||||
|
||||
let local_dir = PathBuf::from(format!("./{}", name));
|
||||
fs::create_dir(&local_dir)?;
|
||||
|
||||
clone_and_copy_template(target, temp_dir, &local_dir)?;
|
||||
|
||||
initialize_project(&local_dir, name)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_example_exists(target: &str) -> Result<()> {
|
||||
let check_url = format!(
|
||||
"https://api.github.com/repos/AiYo-Studio/emod-cli/contents/examples/{}",
|
||||
target
|
||||
);
|
||||
|
||||
let client = if cfg!(debug_assertions) {
|
||||
HttpClient::new_with_proxy("http://127.0.0.1:1080")?
|
||||
} else {
|
||||
HttpClient::new()?
|
||||
};
|
||||
|
||||
let resp = client.get(&check_url)?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(crate::error::CliError::NotFound(format!(
|
||||
"示例模板 '{}' 不存在",
|
||||
target
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clone_and_copy_template(target: &str, temp_dir: &PathBuf, local_dir: &PathBuf) -> Result<()> {
|
||||
let _ = fs::remove_dir_all(format!("{}/tmp", temp_dir.display()));
|
||||
|
||||
let config = Config::load();
|
||||
let url = &config.repo_url;
|
||||
git::clone_remote_project(url.to_string(), temp_dir)?;
|
||||
|
||||
let target_dir = PathBuf::from(format!("{}/tmp/examples/{}", temp_dir.display(), target));
|
||||
file::copy_folder(&target_dir, local_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initialize_project(local_dir: &PathBuf, name: &str) -> Result<()> {
|
||||
let lower_name = format!(
|
||||
"{}{}",
|
||||
name.chars().next().unwrap().to_lowercase(),
|
||||
&name[1..]
|
||||
);
|
||||
|
||||
println!("项目名称: {}", name);
|
||||
println!("标识名称: {}", lower_name);
|
||||
|
||||
let scripts_dir = local_dir.join(format!("behavior_pack/{}Scripts", lower_name));
|
||||
fs::rename(local_dir.join("behavior_pack/exampleScripts"), &scripts_dir)?;
|
||||
|
||||
let project_info = generate_project_info(name, &lower_name);
|
||||
|
||||
apply_project_info(local_dir, &scripts_dir, &project_info)?;
|
||||
|
||||
rename_pack_folders(local_dir, &project_info)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_project_info(name: &str, lower_name: &str) -> ProjectInfo {
|
||||
ProjectInfo {
|
||||
name: name.to_string(),
|
||||
lower_name: lower_name.to_string(),
|
||||
behavior_pack_uuid: Uuid::new_v4().to_string(),
|
||||
resource_pack_uuid: Uuid::new_v4().to_string(),
|
||||
behavior_module_uuid: Uuid::new_v4().to_string(),
|
||||
resource_module_uuid: Uuid::new_v4().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_project_info(
|
||||
local_dir: &PathBuf,
|
||||
scripts_dir: &PathBuf,
|
||||
info: &ProjectInfo,
|
||||
) -> Result<()> {
|
||||
let manifest_files = vec![
|
||||
local_dir.join("world_behavior_packs.json"),
|
||||
local_dir.join("world_resource_packs.json"),
|
||||
local_dir.join("behavior_pack/pack_manifest.json"),
|
||||
local_dir.join("resource_pack/pack_manifest.json"),
|
||||
];
|
||||
|
||||
for path in manifest_files {
|
||||
apply_info_to_json(&path, info)?;
|
||||
}
|
||||
|
||||
process_python_files(scripts_dir, info)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_info_to_json(path: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
||||
println!(" - 修改文件: {}", path.display());
|
||||
file::update_json_file(path, |json| {
|
||||
let content = serde_json::to_string(json)?;
|
||||
let updated = content
|
||||
.replace("{behavior_pack_uuid}", &info.behavior_pack_uuid)
|
||||
.replace("{resource_pack_uuid}", &info.resource_pack_uuid)
|
||||
.replace("{behavior_module_uuid}", &info.behavior_module_uuid)
|
||||
.replace("{resource_module_uuid}", &info.resource_module_uuid)
|
||||
.replace("__mod_name__", &info.name)
|
||||
.replace("__mod_name_lower__", &info.lower_name);
|
||||
*json = serde_json::from_str(&updated)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn process_python_files(dir: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
||||
if !dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
process_python_files(&entry.path(), info)?;
|
||||
}
|
||||
} else if dir.extension().and_then(|s| s.to_str()) == Some("py") {
|
||||
apply_info_to_python(dir, info)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_info_to_python(path: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let updated = content
|
||||
.replace("__mod_name__", &info.name)
|
||||
.replace("__mod_name_lower__", &info.lower_name);
|
||||
fs::write(path, updated)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rename_pack_folders(local_dir: &PathBuf, info: &ProjectInfo) -> Result<()> {
|
||||
let behavior_suffix: String = info.behavior_pack_uuid.chars().take(8).collect();
|
||||
let resource_suffix: String = info.resource_pack_uuid.chars().take(8).collect();
|
||||
|
||||
fs::rename(
|
||||
local_dir.join("behavior_pack"),
|
||||
local_dir.join(format!("behavior_pack_{}", behavior_suffix)),
|
||||
)?;
|
||||
fs::rename(
|
||||
local_dir.join("resource_pack"),
|
||||
local_dir.join(format!("resource_pack_{}", resource_suffix)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
68
src/commands/mod.rs
Normal file
68
src/commands/mod.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use clap::{arg, Args, Parser, Subcommand};
|
||||
|
||||
pub mod components;
|
||||
pub mod create;
|
||||
pub mod release;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "emod-cli",
|
||||
version = "1.0.0",
|
||||
about = "Convenient Management of NetEase Minecraft Mod Project",
|
||||
allow_external_subcommands = true,
|
||||
long_about = None,
|
||||
propagate_version = true
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Release a new version
|
||||
Release(ReleaseArgs),
|
||||
/// Create a new mod project
|
||||
Create(CreateArgs),
|
||||
/// Create a new component
|
||||
Components(ComponentsArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ReleaseArgs {
|
||||
/// The path of the project
|
||||
#[arg(short, long)]
|
||||
pub path: Option<String>,
|
||||
/// The version of the project
|
||||
#[arg(short, long)]
|
||||
pub ver: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct CreateArgs {
|
||||
/// The name of the mod
|
||||
#[arg(short, long)]
|
||||
pub name: String,
|
||||
/// Example target, default example is 'default'
|
||||
#[arg(short, long)]
|
||||
pub target: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ComponentsArgs {
|
||||
/// The path of the project
|
||||
#[arg(short, long)]
|
||||
pub path: Option<String>,
|
||||
/// The name of the component
|
||||
#[arg(short, long)]
|
||||
pub component: String,
|
||||
/// Import the path of the geo file.
|
||||
#[arg(short, long)]
|
||||
pub geo: Option<String>,
|
||||
/// Import the path of the texture file.
|
||||
#[arg(short, long)]
|
||||
pub texture: Option<String>,
|
||||
/// The item's identifier
|
||||
#[arg(short, long)]
|
||||
pub identifier: Option<String>
|
||||
}
|
||||
200
src/commands/release.rs
Normal file
200
src/commands/release.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use walkdir;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
use crate::commands::ReleaseArgs;
|
||||
use crate::utils::file;
|
||||
use crate::{entity, entity::project::ReleaseInfo};
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn execute(args: &ReleaseArgs) {
|
||||
if let Err(e) = run_release(args) {
|
||||
eprintln!("❌ 组件打包失败: {}", e);
|
||||
return;
|
||||
}
|
||||
println!("🍀 组件打包完成");
|
||||
}
|
||||
|
||||
fn run_release(args: &ReleaseArgs) -> Result<()> {
|
||||
let project_dir = file::find_project_dir(&args.path)?;
|
||||
let release_info = entity::get_current_release_info(&project_dir)?;
|
||||
|
||||
println!("🔖 当前行为包版本: {:?}", release_info.behavior_version);
|
||||
println!("🔖 当前资源包版本: {:?}", release_info.resource_version);
|
||||
|
||||
release(&args.ver, &project_dir, &release_info)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(
|
||||
version: &Option<String>,
|
||||
project_dir: &PathBuf,
|
||||
release_info: &ReleaseInfo,
|
||||
) -> Result<()> {
|
||||
let new_version = calculate_version(version, &release_info.behavior_version)?;
|
||||
let version_value = Value::Array(new_version.iter().map(|v| Value::from(*v)).collect());
|
||||
|
||||
println!("📦 开始打包, 版本号: {:?}", &new_version);
|
||||
|
||||
update_versions(&project_dir, &release_info, &version_value)?;
|
||||
|
||||
let version_str = format!("{}.{}.{}", new_version[0], new_version[1], new_version[2]);
|
||||
let output_path = package_project(&project_dir, &release_info, &version_str)?;
|
||||
|
||||
println!("📦 打包完成: {}", output_path.replace("\\", "/"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_version(version: &Option<String>, current: &[u32]) -> Result<Vec<u32>> {
|
||||
if let Some(ver_str) = version {
|
||||
ver_str
|
||||
.split(".")
|
||||
.map(|s| s.parse::<u32>().map_err(|e| e.into()))
|
||||
.collect()
|
||||
} else {
|
||||
Ok(vec![current[0], current[1], current[2] + 1])
|
||||
}
|
||||
}
|
||||
|
||||
fn update_versions(
|
||||
project_dir: &PathBuf,
|
||||
release_info: &ReleaseInfo,
|
||||
version: &Value,
|
||||
) -> Result<()> {
|
||||
update_pack_json(&project_dir, &version)?;
|
||||
update_manifest_json(&project_dir, &release_info, &version)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_pack_json(project_dir: &PathBuf, version: &Value) -> Result<()> {
|
||||
let paths = vec![
|
||||
project_dir.join("world_behavior_packs.json"),
|
||||
project_dir.join("world_resource_packs.json"),
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
file::update_json_file(&path, |json| {
|
||||
json[0]["version"] = version.clone();
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_manifest_json(
|
||||
project_dir: &PathBuf,
|
||||
release_info: &ReleaseInfo,
|
||||
version: &Value,
|
||||
) -> Result<()> {
|
||||
let behavior_dir = project_dir.join(format!(
|
||||
"behavior_pack_{}",
|
||||
release_info.behavior_identifier
|
||||
));
|
||||
let resource_dir = project_dir.join(format!(
|
||||
"resource_pack_{}",
|
||||
release_info.resource_identifier
|
||||
));
|
||||
|
||||
for pack_dir in [behavior_dir, resource_dir] {
|
||||
let manifest_path = pack_dir.join("pack_manifest.json");
|
||||
file::update_json_file(&manifest_path, |json| {
|
||||
json["header"]["version"] = version.clone();
|
||||
json["modules"][0]["version"] = version.clone();
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_project(
|
||||
project_dir: &PathBuf,
|
||||
release_info: &ReleaseInfo,
|
||||
version: &str,
|
||||
) -> Result<String> {
|
||||
let output_path = format!("{}/release_{}.zip", project_dir.display(), version);
|
||||
let file = fs::File::create(&output_path)?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
let behavior_dir = project_dir.join(format!(
|
||||
"behavior_pack_{}",
|
||||
release_info.behavior_identifier
|
||||
));
|
||||
let resource_dir = project_dir.join(format!(
|
||||
"resource_pack_{}",
|
||||
release_info.resource_identifier
|
||||
));
|
||||
|
||||
add_directory_to_zip(&mut zip, &project_dir, &behavior_dir)?;
|
||||
add_directory_to_zip(&mut zip, &project_dir, &resource_dir)?;
|
||||
|
||||
zip.finish()?;
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
fn add_directory_to_zip(
|
||||
zip: &mut zip::ZipWriter<File>,
|
||||
project_dir: &PathBuf,
|
||||
src_dir: &PathBuf,
|
||||
) -> Result<()> {
|
||||
if !src_dir.is_dir() {
|
||||
return Err(crate::error::CliError::InvalidData(
|
||||
format!("{} 不是目录", src_dir.display())
|
||||
));
|
||||
}
|
||||
|
||||
if count_files(src_dir)? == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let options = SimpleFileOptions::default();
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
for entry in walkdir::WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let relative_path = path.strip_prefix(project_dir)
|
||||
.map_err(|e| crate::error::CliError::InvalidData(e.to_string()))?;
|
||||
|
||||
let path_str = relative_path
|
||||
.to_str()
|
||||
.ok_or_else(|| crate::error::CliError::InvalidData(
|
||||
format!("{:?} 不是有效的 UTF-8 路径", relative_path)
|
||||
))?;
|
||||
|
||||
if path.is_file() {
|
||||
if path_str.ends_with(".gitkeep") {
|
||||
continue;
|
||||
}
|
||||
zip.start_file(path_str, options)?;
|
||||
let mut f = File::open(path)?;
|
||||
f.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&buffer)?;
|
||||
buffer.clear();
|
||||
} else if !relative_path.as_os_str().is_empty() && count_files(path)? > 0 {
|
||||
zip.add_directory(path_str, options)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn count_files(dir: &Path) -> Result<usize> {
|
||||
let mut count = 0;
|
||||
for entry in walkdir::WalkDir::new(dir) {
|
||||
let entry = entry?;
|
||||
if entry.path().is_file()
|
||||
&& !entry.path().display().to_string().ends_with(".gitkeep") {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
40
src/config.rs
Normal file
40
src/config.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_repo_url")]
|
||||
pub repo_url: String,
|
||||
}
|
||||
|
||||
fn default_repo_url() -> String {
|
||||
"https://github.com/AiYo-Studio/emod-cli.git".to_string()
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
repo_url: default_repo_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Self {
|
||||
let config_path = Self::config_path();
|
||||
|
||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||
if let Ok(config) = serde_json::from_str(&content) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn config_path() -> PathBuf {
|
||||
let home = dirs::home_dir().expect("无法获取用户主目录");
|
||||
home.join(".emod-cli.json")
|
||||
}
|
||||
}
|
||||
50
src/entity/mod.rs
Normal file
50
src/entity/mod.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::entity::project::ReleaseInfo;
|
||||
use crate::error::Result;
|
||||
use crate::utils::file::read_file_to_json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod project;
|
||||
|
||||
pub fn get_current_release_info(project_dir: &PathBuf) -> Result<ReleaseInfo> {
|
||||
let behavior_path = project_dir.join("world_behavior_packs.json");
|
||||
let resource_path = project_dir.join("world_resource_packs.json");
|
||||
|
||||
let behavior_json = read_file_to_json(&behavior_path)?;
|
||||
let resource_json = read_file_to_json(&resource_path)?;
|
||||
|
||||
let behavior_version = parse_version_array(&behavior_json[0]["version"])?;
|
||||
let resource_version = parse_version_array(&resource_json[0]["version"])?;
|
||||
|
||||
let behavior_pack_uuid = behavior_json[0]["pack_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| crate::error::CliError::InvalidData("无效的 behavior pack_id".into()))?
|
||||
.to_string();
|
||||
|
||||
let resource_pack_uuid = resource_json[0]["pack_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| crate::error::CliError::InvalidData("无效的 resource pack_id".into()))?
|
||||
.to_string();
|
||||
|
||||
let behavior_identifier: String = behavior_pack_uuid.chars().take(8).collect();
|
||||
let resource_identifier: String = resource_pack_uuid.chars().take(8).collect();
|
||||
|
||||
Ok(ReleaseInfo {
|
||||
behavior_version,
|
||||
resource_version,
|
||||
behavior_identifier,
|
||||
resource_identifier,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_version_array(value: &serde_json::Value) -> Result<Vec<u32>> {
|
||||
value
|
||||
.as_array()
|
||||
.ok_or_else(|| crate::error::CliError::InvalidData("版本格式无效".into()))?
|
||||
.iter()
|
||||
.map(|v| {
|
||||
v.as_u64()
|
||||
.map(|n| n as u32)
|
||||
.ok_or_else(|| crate::error::CliError::InvalidData("版本号格式无效".into()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
15
src/entity/project.rs
Normal file
15
src/entity/project.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
pub struct ProjectInfo {
|
||||
pub name: String,
|
||||
pub lower_name: String,
|
||||
pub behavior_pack_uuid: String,
|
||||
pub resource_pack_uuid: String,
|
||||
pub behavior_module_uuid: String,
|
||||
pub resource_module_uuid: String,
|
||||
}
|
||||
|
||||
pub struct ReleaseInfo {
|
||||
pub behavior_version: Vec<u32>,
|
||||
pub resource_version: Vec<u32>,
|
||||
pub behavior_identifier: String,
|
||||
pub resource_identifier: String,
|
||||
}
|
||||
78
src/error.rs
Normal file
78
src/error.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::io;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CliError {
|
||||
Io(io::Error),
|
||||
Json(serde_json::Error),
|
||||
Network(reqwest::Error),
|
||||
Anyhow(anyhow::Error),
|
||||
Zip(zip::result::ZipError),
|
||||
Walkdir(walkdir::Error),
|
||||
Parse(ParseIntError),
|
||||
NotFound(String),
|
||||
InvalidData(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CliError::Io(e) => write!(f, "IO错误: {}", e),
|
||||
CliError::Json(e) => write!(f, "JSON解析错误: {}", e),
|
||||
CliError::Network(e) => write!(f, "网络错误: {}", e),
|
||||
CliError::Anyhow(e) => write!(f, "{}", e),
|
||||
CliError::Zip(e) => write!(f, "压缩错误: {}", e),
|
||||
CliError::Walkdir(e) => write!(f, "目录遍历错误: {}", e),
|
||||
CliError::Parse(e) => write!(f, "解析错误: {}", e),
|
||||
CliError::NotFound(msg) => write!(f, "未找到: {}", msg),
|
||||
CliError::InvalidData(msg) => write!(f, "无效数据: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CliError {}
|
||||
|
||||
impl From<io::Error> for CliError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
CliError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for CliError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
CliError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for CliError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
CliError::Network(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for CliError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
CliError::Anyhow(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<zip::result::ZipError> for CliError {
|
||||
fn from(err: zip::result::ZipError) -> Self {
|
||||
CliError::Zip(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<walkdir::Error> for CliError {
|
||||
fn from(err: walkdir::Error) -> Self {
|
||||
CliError::Walkdir(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for CliError {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
CliError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CliError>;
|
||||
28
src/main.rs
Normal file
28
src/main.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
mod commands;
|
||||
mod entity;
|
||||
mod utils;
|
||||
mod error;
|
||||
mod config;
|
||||
|
||||
use crate::commands::{Cli, Commands};
|
||||
use clap::Parser;
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let temp_dir = check_temp_dir();
|
||||
match &cli.command {
|
||||
Commands::Release(args) => commands::release::execute(args),
|
||||
Commands::Create(args) => commands::create::execute(args, &temp_dir),
|
||||
Commands::Components(args) => commands::components::execute(args),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_temp_dir() -> PathBuf {
|
||||
let mut temp_dir = env::temp_dir();
|
||||
temp_dir.push("emod-cli");
|
||||
if let Err(e) = fs::create_dir_all(&temp_dir) {
|
||||
eprintln!("Error: Failed to create temp directory: {}", e);
|
||||
}
|
||||
temp_dir
|
||||
}
|
||||
53
src/utils/file.rs
Normal file
53
src/utils/file.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::error::Result;
|
||||
use serde_json::Value;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
pub fn copy_folder(src: &PathBuf, dest: &PathBuf) -> Result<()> {
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return Err(crate::error::CliError::NotFound(format!(
|
||||
"源目录不存在: {}",
|
||||
src.display()
|
||||
)));
|
||||
}
|
||||
if !dest.exists() {
|
||||
fs::create_dir_all(dest)?;
|
||||
}
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
let dest_path = dest.join(src_path.file_name().unwrap());
|
||||
if src_path.is_file() {
|
||||
fs::copy(&src_path, &dest_path)?;
|
||||
} else if src_path.is_dir() {
|
||||
copy_folder(&src_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_file_to_json(path: &PathBuf) -> Result<Value> {
|
||||
let file = fs::read_to_string(path)?;
|
||||
let json: Value = serde_json::from_str(&file)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
pub fn write_json_to_file(path: &PathBuf, value: &Value) -> Result<()> {
|
||||
let content = serde_json::to_string_pretty(value)?;
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_json_file<F>(path: &PathBuf, updater: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Value) -> Result<()>,
|
||||
{
|
||||
let mut json = read_file_to_json(path)?;
|
||||
updater(&mut json)?;
|
||||
write_json_to_file(path, &json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_project_dir(path: &Option<String>) -> Result<PathBuf> {
|
||||
let path = path.as_deref().unwrap_or(".");
|
||||
Ok(PathBuf::from(path))
|
||||
}
|
||||
11
src/utils/git.rs
Normal file
11
src/utils/git.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use std::path::PathBuf;
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn clone_remote_project(url: String, temp_dir: &PathBuf) -> Result<()> {
|
||||
std::process::Command::new("git")
|
||||
.arg("clone")
|
||||
.arg(url)
|
||||
.arg(format!("{}/tmp", temp_dir.display()))
|
||||
.output()?;
|
||||
Ok(())
|
||||
}
|
||||
26
src/utils/http.rs
Normal file
26
src/utils/http.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use reqwest::blocking::Client;
|
||||
use crate::error::Result;
|
||||
|
||||
pub struct HttpClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub fn new() -> Result<Self> {
|
||||
let client = Client::builder().build()?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
pub fn new_with_proxy(proxy_url: &str) -> Result<Self> {
|
||||
let proxy = reqwest::Proxy::all(proxy_url)?;
|
||||
let client = Client::builder().proxy(proxy).build()?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
pub fn get(&self, url: &str) -> Result<reqwest::blocking::Response> {
|
||||
Ok(self.client
|
||||
.get(url)
|
||||
.header("User-Agent", "emod-cli")
|
||||
.send()?)
|
||||
}
|
||||
}
|
||||
3
src/utils/mod.rs
Normal file
3
src/utils/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod git;
|
||||
pub mod file;
|
||||
pub mod http;
|
||||
Reference in New Issue
Block a user