@@ -0,0 +1,776 @@
use crate ::commands ::BbmodelArgs ;
use crate ::entity ;
use crate ::error ::{ CliError , Result } ;
use crate ::utils ::file ;
use base64 ::Engine ;
use base64 ::engine ::general_purpose ::STANDARD ;
use serde_json ::{ Map , Number , Value , json } ;
use std ::collections ::HashMap ;
use std ::fs ;
use std ::path ::{ Path , PathBuf } ;
const DEFAULT_INPUT : & str = " ./model.bbmodel " ;
const TEXTURE_DATA_PREFIX : & str = " data:image/png;base64, " ;
struct TextureDesc {
identifier : String ,
width : u64 ,
height : u64 ,
terrain_texture : Option < String > ,
}
struct CommandOutput {
model_path : PathBuf ,
texture_count : usize ,
terrain_updated : bool ,
}
pub fn execute ( args : & BbmodelArgs ) {
match run_bbmodel ( args ) {
Ok ( output ) = > {
println! ( " 成功: 方块模型已生成到 {} " , output . model_path . display ( ) ) ;
if output . texture_count > 0 {
println! ( " 成功: 已导出 {} 张贴图 " , output . texture_count ) ;
}
if output . terrain_updated {
println! ( " 成功: terrain_texture.json 已更新 " ) ;
}
}
Err ( e ) = > {
eprintln! ( " 错误: {} " , e ) ;
}
}
}
fn run_bbmodel ( args : & BbmodelArgs ) -> Result < CommandOutput > {
let ( namespace , short_name ) = parse_identifier ( & args . identifier ) ? ;
let input_path = PathBuf ::from ( args . input . as_deref ( ) . unwrap_or ( DEFAULT_INPUT ) ) ;
let doc = parse_bbmodel ( & input_path ) ? ;
let texture_count = texture_count ( & doc ) ? ;
let bones = flatten_bones ( & doc , texture_count ) ? ;
let target = resolve_target ( args , short_name ) ? ;
if let Some ( parent ) = target . model_path . parent ( ) {
fs ::create_dir_all ( parent ) . map_err ( | e | file ::io_error ( " 创建模型目录 " , parent , e ) ) ? ;
}
let texture_descs = if args . no_textures {
collect_texture_descriptions ( & doc , namespace , short_name , None ) ?
} else {
let texture_dir = target
. texture_dir
. as_ref ( )
. ok_or_else ( | | CliError ::InvalidData ( " 贴图输出目录未初始化 " . into ( ) ) ) ? ;
extract_textures (
& doc ,
texture_dir ,
namespace ,
short_name ,
target . terrain_base . as_deref ( ) ,
) ?
} ;
let model_json = assemble_model_json ( & args . identifier , bones , & texture_descs ) ;
file ::write_json_to_file ( & target . model_path , & model_json ) ? ;
let mut terrain_updated = false ;
if ! args . no_textures {
if let Some ( res_pack ) = target . resource_pack . as_deref ( ) {
update_terrain_texture ( res_pack , & texture_descs ) ? ;
terrain_updated = ! texture_descs . is_empty ( ) ;
}
}
Ok ( CommandOutput {
model_path : target . model_path ,
texture_count : if args . no_textures {
0
} else {
texture_descs . len ( )
} ,
terrain_updated ,
} )
}
struct TargetPaths {
model_path : PathBuf ,
texture_dir : Option < PathBuf > ,
resource_pack : Option < PathBuf > ,
terrain_base : Option < String > ,
}
fn resolve_target ( args : & BbmodelArgs , short_name : & str ) -> Result < TargetPaths > {
if let Some ( output ) = args . output . as_deref ( ) {
let output_dir = PathBuf ::from ( output ) ;
return Ok ( TargetPaths {
model_path : output_dir . join ( format! ( " {} .json " , short_name ) ) ,
texture_dir : Some ( output_dir ) ,
resource_pack : None ,
terrain_base : None ,
} ) ;
}
let project_path = file ::find_project_dir ( & args . path ) ? ;
let project_info = entity ::get_current_release_info ( & project_path ) ? ;
let res_pack = project_path . join ( format! (
" resource_pack_ {} " ,
project_info . resource_identifier
) ) ;
Ok ( TargetPaths {
model_path : res_pack
. join ( " models " )
. join ( " netease_block " )
. join ( format! ( " {} .json " , short_name ) ) ,
texture_dir : Some ( res_pack . join ( " textures " ) . join ( " blocks " ) ) ,
resource_pack : Some ( res_pack ) ,
terrain_base : Some ( " textures/blocks " . to_string ( ) ) ,
} )
}
fn parse_identifier ( identifier : & str ) -> Result < ( & str , & str ) > {
let ( namespace , short_name ) = identifier . split_once ( ':' ) . ok_or_else ( | | {
CliError ::InvalidInput ( format! (
" 方块标识符 ' {} ' 无效,应为 <namespace>:<name> " ,
identifier
) )
} ) ? ;
if namespace . is_empty ( ) | | short_name . is_empty ( ) | | short_name . contains ( ':' ) {
return Err ( CliError ::InvalidInput ( format! (
" 方块标识符 ' {} ' 无效,应为 <namespace>:<name> " ,
identifier
) ) ) ;
}
if short_name . chars ( ) . any ( | c | c = = '/' | | c = = '\\' ) {
return Err ( CliError ::InvalidInput ( format! (
" 方块标识符 ' {} ' 不能包含路径分隔符 " ,
identifier
) ) ) ;
}
Ok ( ( namespace , short_name ) )
}
fn parse_bbmodel ( path : & Path ) -> Result < Value > {
if ! path . exists ( ) {
return Err ( CliError ::NotFound ( format! (
" .bbmodel 文件 {} 不存在 " ,
path . display ( )
) ) ) ;
}
let path_buf = path . to_path_buf ( ) ;
let doc = file ::read_file_to_json ( & path_buf ) ? ;
required_array_field ( & doc , " outliner " ) ? ;
optional_array_field ( & doc , " groups " ) ? ;
required_array_field ( & doc , " elements " ) ? ;
if doc
. get ( " meta " )
. and_then ( | meta | meta . get ( " box_uv " ) )
. and_then ( Value ::as_bool )
. unwrap_or ( false )
{
return Err ( CliError ::InvalidData (
" 当前只支持 per-face UV, 不支持 box_uv 模式 " . into ( ) ,
) ) ;
}
Ok ( doc )
}
fn texture_count ( doc : & Value ) -> Result < usize > {
Ok ( optional_array_field ( doc , " textures " ) ? . map_or ( 0 , Vec ::len ) )
}
fn flatten_bones ( doc : & Value , texture_count : usize ) -> Result < Vec < Value > > {
let groups = match optional_array_field ( doc , " groups " ) ? {
Some ( groups ) = > build_uuid_lookup ( groups , " groups " ) ? ,
None = > HashMap ::new ( ) ,
} ;
let elements = build_uuid_lookup ( required_array_field ( doc , " elements " ) ? , " elements " ) ? ;
let outliner = required_array_field ( doc , " outliner " ) ? ;
let mut bones = Vec ::new ( ) ;
for node in outliner {
visit_outliner_node (
node ,
None ,
None ,
& groups ,
& elements ,
texture_count ,
& mut bones ,
) ? ;
}
Ok ( bones )
}
fn build_uuid_lookup < ' a > ( items : & ' a [ Value ] , field : & str ) -> Result < HashMap < & ' a str , & ' a Value > > {
let mut lookup = HashMap ::with_capacity ( items . len ( ) ) ;
for item in items {
let uuid = item
. get ( " uuid " )
. and_then ( Value ::as_str )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " {} 中存在缺少 uuid 的条目 " , field ) ) ) ? ;
lookup . insert ( uuid , item ) ;
}
Ok ( lookup )
}
fn visit_outliner_node < ' a > (
node : & ' a Value ,
parent_name : Option < & ' a str > ,
current_bone_index : Option < usize > ,
groups : & HashMap < & ' a str , & ' a Value > ,
elements : & HashMap < & ' a str , & ' a Value > ,
texture_count : usize ,
bones : & mut Vec < Value > ,
) -> Result < ( ) > {
match node {
Value ::String ( uuid ) = > {
let element = elements . get ( uuid . as_str ( ) ) . ok_or_else ( | | {
CliError ::InvalidData ( format! ( " outliner 引用了不存在的 element uuid {} " , uuid ) )
} ) ? ;
if let Some ( cube ) = convert_element ( element , texture_count ) ? {
let bone_index = current_bone_index . ok_or_else ( | | {
CliError ::InvalidData ( format! (
" cube {} 不在任何 group/bone 下,无法生成 netease:block_geometry " ,
uuid
) )
} ) ? ;
append_cube ( & mut bones [ bone_index ] , cube ) ? ;
}
}
Value ::Object ( outliner_group ) = > {
let uuid = outliner_group
. get ( " uuid " )
. and_then ( Value ::as_str )
. ok_or_else ( | | CliError ::InvalidData ( " outliner group 缺少 uuid " . into ( ) ) ) ? ;
let group = groups . get ( uuid ) . copied ( ) . unwrap_or ( node ) ;
let group_name = required_string_field ( group , " name " ) ? ;
let mut bone = Map ::new ( ) ;
bone . insert ( " name " . to_string ( ) , Value ::String ( group_name . to_string ( ) ) ) ;
if let Some ( parent ) = parent_name {
bone . insert ( " parent " . to_string ( ) , Value ::String ( parent . to_string ( ) ) ) ;
}
bone . insert ( " pivot " . to_string ( ) , convert_pivot ( group , " group " ) ? ) ;
bone . insert ( " rotation " . to_string ( ) , convert_rotation ( group , " group " ) ? ) ;
bones . push ( Value ::Object ( bone ) ) ;
let bone_index = bones . len ( ) - 1 ;
let children = outliner_group
. get ( " children " )
. and_then ( Value ::as_array )
. ok_or_else ( | | {
CliError ::InvalidData ( format! ( " outliner group {} 缺少 children " , uuid ) )
} ) ? ;
for child in children {
match child {
Value ::String ( _ ) = > visit_outliner_node (
child ,
Some ( group_name ) ,
Some ( bone_index ) ,
groups ,
elements ,
texture_count ,
bones ,
) ? ,
Value ::Object ( _ ) = > { }
_ = > {
visit_outliner_node (
child ,
Some ( group_name ) ,
Some ( bone_index ) ,
groups ,
elements ,
texture_count ,
bones ,
) ? ;
}
}
}
for child in children . iter ( ) . rev ( ) {
if child . is_object ( ) {
visit_outliner_node (
child ,
Some ( group_name ) ,
Some ( bone_index ) ,
groups ,
elements ,
texture_count ,
bones ,
) ? ;
}
}
}
_ = > {
return Err ( CliError ::InvalidData (
" outliner 节点必须是 group 对象或 element uuid 字符串 " . into ( ) ,
) ) ;
}
}
Ok ( ( ) )
}
fn append_cube ( bone : & mut Value , cube : Value ) -> Result < ( ) > {
let bone_object = bone
. as_object_mut ( )
. ok_or_else ( | | CliError ::InvalidData ( " bone 不是 JSON 对象 " . into ( ) ) ) ? ;
if ! bone_object . contains_key ( " cubes " ) {
bone_object . insert ( " cubes " . to_string ( ) , Value ::Array ( Vec ::new ( ) ) ) ;
}
let cubes = bone_object
. get_mut ( " cubes " )
. and_then ( Value ::as_array_mut )
. ok_or_else ( | | CliError ::InvalidData ( " bone.cubes 不是数组 " . into ( ) ) ) ? ;
cubes . push ( cube ) ;
Ok ( ( ) )
}
fn convert_element ( element : & Value , texture_count : usize ) -> Result < Option < Value > > {
let element_type = required_string_field ( element , " type " ) ? ;
if element_type ! = " cube " {
let name = element
. get ( " name " )
. and_then ( Value ::as_str )
. unwrap_or ( " <unnamed> " ) ;
eprintln! (
" 警告: 跳过不支持的 Blockbench 元素 ' {} ', type= {} " ,
name , element_type
) ;
return Ok ( None ) ;
}
if element
. get ( " box_uv " )
. and_then ( Value ::as_bool )
. unwrap_or ( false )
{
let name = element
. get ( " name " )
. and_then ( Value ::as_str )
. unwrap_or ( " <unnamed> " ) ;
return Err ( CliError ::InvalidData ( format! (
" 元素 ' {} ' 使用 box_uv, 当前只支持 per-face UV " ,
name
) ) ) ;
}
Ok ( Some ( convert_cube ( element , texture_count ) ? ) )
}
fn convert_cube ( element : & Value , texture_count : usize ) -> Result < Value > {
let from = required_vec3 ( element , " from " ) ? ;
let to = required_vec3 ( element , " to " ) ? ;
let origin = required_vec3 ( element , " origin " ) ? ;
let rotation = optional_vec3 ( element , " rotation " ) ? ;
let faces = required_object_field ( element , " faces " ) ? ;
let mut cube = Map ::new ( ) ;
cube . insert (
" origin " . to_string ( ) ,
number_array ( & [ - to [ 0 ] , from [ 1 ] , from [ 2 ] ] ) ? ,
) ;
cube . insert (
" pivot " . to_string ( ) ,
number_array ( & [ - origin [ 0 ] , origin [ 1 ] , origin [ 2 ] ] ) ? ,
) ;
cube . insert (
" rotation " . to_string ( ) ,
number_array ( & [ - rotation [ 0 ] , - rotation [ 1 ] , rotation [ 2 ] ] ) ? ,
) ;
cube . insert (
" size " . to_string ( ) ,
number_array ( & [ to [ 0 ] - from [ 0 ] , to [ 1 ] - from [ 1 ] , to [ 2 ] - from [ 2 ] ] ) ? ,
) ;
let mut uv = Map ::new ( ) ;
for ( face_name , face ) in faces {
uv . insert (
face_name . to_string ( ) ,
convert_face ( face_name , face , texture_count ) ? ,
) ;
}
cube . insert ( " uv " . to_string ( ) , Value ::Object ( uv ) ) ;
Ok ( Value ::Object ( cube ) )
}
fn convert_face ( face_name : & str , face : & Value , texture_count : usize ) -> Result < Value > {
let uv_values = required_array_field ( face , " uv " ) ? ;
if uv_values . len ( ) ! = 4 {
return Err ( CliError ::InvalidData ( format! (
" face.uv 长度应为 4, 实际为 {} " ,
uv_values . len ( )
) ) ) ;
}
let u1 = number_as_f64 ( & uv_values [ 0 ] , " face.uv[0] " ) ? ;
let v1 = number_as_f64 ( & uv_values [ 1 ] , " face.uv[1] " ) ? ;
let u2 = number_as_f64 ( & uv_values [ 2 ] , " face.uv[2] " ) ? ;
let v2 = number_as_f64 ( & uv_values [ 3 ] , " face.uv[3] " ) ? ;
let texture = face
. get ( " texture " )
. ok_or_else ( | | CliError ::InvalidData ( " face 缺少 texture 字段 " . into ( ) ) ) ? ;
let texture_index = number_as_u64 ( texture , " face.texture " ) ? ;
let texture_index_usize = usize ::try_from ( texture_index ) . map_err ( | _ | {
CliError ::InvalidData ( format! ( " face.texture {} 超出平台索引范围 " , texture_index ) )
} ) ? ;
if texture_index_usize > = texture_count {
return Err ( CliError ::InvalidData ( format! (
" face.texture {} 超出 textures 数组范围 0.. {} " ,
texture_index ,
texture_count . saturating_sub ( 1 )
) ) ) ;
}
let mut out = Map ::new ( ) ;
out . insert (
" texture " . to_string ( ) ,
Value ::Number ( Number ::from ( texture_index ) ) ,
) ;
if face_name = = " up " {
out . insert (
" uv " . to_string ( ) ,
Value ::Array ( vec! [ uv_values [ 2 ] . clone ( ) , uv_values [ 3 ] . clone ( ) ] ) ,
) ;
out . insert ( " uv_size " . to_string ( ) , number_array ( & [ u1 - u2 , v1 - v2 ] ) ? ) ;
} else {
out . insert (
" uv " . to_string ( ) ,
Value ::Array ( vec! [ uv_values [ 0 ] . clone ( ) , uv_values [ 1 ] . clone ( ) ] ) ,
) ;
out . insert ( " uv_size " . to_string ( ) , number_array ( & [ u2 - u1 , v2 - v1 ] ) ? ) ;
}
if let Some ( rotation ) = face . get ( " rotation " ) {
let rot = number_as_i64 ( rotation , " face.rotation " ) ? ;
match rot {
0 = > { }
90 | 180 | 270 = > {
out . insert ( " rot " . to_string ( ) , Value ::Number ( Number ::from ( rot ) ) ) ;
}
_ = > {
return Err ( CliError ::InvalidData ( format! (
" face.rotation 只支持 0/90/180/270, 实际为 {} " ,
rot
) ) ) ;
}
}
}
Ok ( Value ::Object ( out ) )
}
fn convert_pivot ( value : & Value , owner : & str ) -> Result < Value > {
let origin = required_vec3 ( value , " origin " ) ? ;
number_array ( & [ - origin [ 0 ] , origin [ 1 ] , origin [ 2 ] ] )
. map_err ( | e | CliError ::InvalidData ( format! ( " {} pivot 转换失败: {} " , owner , e ) ) )
}
fn convert_rotation ( value : & Value , owner : & str ) -> Result < Value > {
let rotation = required_vec3 ( value , " rotation " ) ? ;
number_array ( & [ - rotation [ 0 ] , - rotation [ 1 ] , rotation [ 2 ] ] )
. map_err ( | e | CliError ::InvalidData ( format! ( " {} rotation 转换失败: {} " , owner , e ) ) )
}
fn collect_texture_descriptions (
doc : & Value ,
namespace : & str ,
short_name : & str ,
terrain_base : Option < & str > ,
) -> Result < Vec < TextureDesc > > {
let Some ( textures ) = optional_array_field ( doc , " textures " ) ? else {
return Ok ( Vec ::new ( ) ) ;
} ;
let mut descs = Vec ::with_capacity ( textures . len ( ) ) ;
for ( index , texture ) in textures . iter ( ) . enumerate ( ) {
let texture_index = index + 1 ;
let width = required_u64_field ( texture , " width " ) ? ;
let height = required_u64_field ( texture , " height " ) ? ;
let terrain_texture =
terrain_base . map ( | base | format! ( " {} / {} _ {} " , base , short_name , texture_index ) ) ;
descs . push ( TextureDesc {
identifier : format ! ( " {}:{}_{} " , namespace , short_name , texture_index ) ,
width ,
height ,
terrain_texture ,
} ) ;
}
Ok ( descs )
}
fn extract_textures (
doc : & Value ,
out_dir : & Path ,
namespace : & str ,
short_name : & str ,
terrain_base : Option < & str > ,
) -> Result < Vec < TextureDesc > > {
fs ::create_dir_all ( out_dir ) . map_err ( | e | file ::io_error ( " 创建贴图目录 " , out_dir , e ) ) ? ;
let Some ( textures ) = optional_array_field ( doc , " textures " ) ? else {
return Ok ( Vec ::new ( ) ) ;
} ;
let mut descs = Vec ::with_capacity ( textures . len ( ) ) ;
for ( index , texture ) in textures . iter ( ) . enumerate ( ) {
let texture_index = index + 1 ;
let width = required_u64_field ( texture , " width " ) ? ;
let height = required_u64_field ( texture , " height " ) ? ;
let source = required_string_field ( texture , " source " ) ? ;
let encoded = source . strip_prefix ( TEXTURE_DATA_PREFIX ) . ok_or_else ( | | {
CliError ::InvalidData ( format! ( " textures[ {} ].source 不是内嵌 PNG data URL " , index ) )
} ) ? ;
let bytes = STANDARD . decode ( encoded ) . map_err ( | e | {
CliError ::InvalidData ( format! ( " textures[ {} ].source base64 解码失败: {} " , index , e ) )
} ) ? ;
validate_png ( texture_index , & bytes , width , height ) ? ;
let png_path = out_dir . join ( format! ( " {} _ {} .png " , short_name , texture_index ) ) ;
fs ::write ( & png_path , bytes ) . map_err ( | e | file ::io_error ( " 写入贴图 " , & png_path , e ) ) ? ;
let terrain_texture =
terrain_base . map ( | base | format! ( " {} / {} _ {} " , base , short_name , texture_index ) ) ;
descs . push ( TextureDesc {
identifier : format ! ( " {}:{}_{} " , namespace , short_name , texture_index ) ,
width ,
height ,
terrain_texture ,
} ) ;
}
Ok ( descs )
}
fn validate_png ( texture_index : usize , bytes : & [ u8 ] , width : u64 , height : u64 ) -> Result < ( ) > {
const PNG_SIGNATURE : & [ u8 ; 8 ] = b " \x89 PNG \r \n \x1a \n " ;
if bytes . len ( ) < 24 | | & bytes [ .. 8 ] ! = PNG_SIGNATURE | | & bytes [ 12 .. 16 ] ! = b " IHDR " {
return Err ( CliError ::InvalidData ( format! (
" textures[ {} ].source 解码结果不是有效 PNG " ,
texture_index - 1
) ) ) ;
}
let actual_width = u32 ::from_be_bytes ( [ bytes [ 16 ] , bytes [ 17 ] , bytes [ 18 ] , bytes [ 19 ] ] ) as u64 ;
let actual_height = u32 ::from_be_bytes ( [ bytes [ 20 ] , bytes [ 21 ] , bytes [ 22 ] , bytes [ 23 ] ] ) as u64 ;
if actual_width ! = width | | actual_height ! = height {
return Err ( CliError ::InvalidData ( format! (
" textures[ {} ] 尺寸不一致:字段为 {} x {} , PNG IHDR 为 {} x {} " ,
texture_index - 1 ,
width ,
height ,
actual_width ,
actual_height
) ) ) ;
}
Ok ( ( ) )
}
fn update_terrain_texture ( res_pack : & Path , entries : & [ TextureDesc ] ) -> Result < ( ) > {
if entries . is_empty ( ) {
return Ok ( ( ) ) ;
}
let terrain_path = res_pack . join ( " textures " ) . join ( " terrain_texture.json " ) ;
if let Some ( parent ) = terrain_path . parent ( ) {
fs ::create_dir_all ( parent )
. map_err ( | e | file ::io_error ( " 创建 terrain_texture 目录 " , parent , e ) ) ? ;
}
let mut terrain = if terrain_path . exists ( ) {
file ::read_file_to_json ( & terrain_path ) ?
} else {
json! ( {
" resource_pack_name " : " vanilla " ,
" texture_data " : { } ,
" texture_name " : " atlas.terrain "
} )
} ;
let terrain_obj = terrain . as_object_mut ( ) . ok_or_else ( | | {
CliError ::InvalidData ( format! ( " {} 顶层必须是 JSON 对象 " , terrain_path . display ( ) ) )
} ) ? ;
if ! terrain_obj . contains_key ( " texture_data " ) {
terrain_obj . insert ( " texture_data " . to_string ( ) , json! ( { } ) ) ;
}
let texture_data = terrain_obj
. get_mut ( " texture_data " )
. and_then ( Value ::as_object_mut )
. ok_or_else ( | | {
CliError ::InvalidData ( format! (
" {} 的 texture_data 必须是 JSON 对象 " ,
terrain_path . display ( )
) )
} ) ? ;
for entry in entries {
let texture_path = entry . terrain_texture . as_ref ( ) . ok_or_else ( | | {
CliError ::InvalidData ( format! ( " {} 缺少 terrain_texture 路径 " , entry . identifier ) )
} ) ? ;
texture_data . insert (
entry . identifier . clone ( ) ,
json! ( {
" textures " : texture_path
} ) ,
) ;
}
file ::write_json_to_file ( & terrain_path , & terrain )
}
fn assemble_model_json (
identifier : & str ,
bones : Vec < Value > ,
texture_descs : & [ TextureDesc ] ,
) -> Value {
let textures = texture_descs
. iter ( )
. map ( | texture | Value ::String ( texture . identifier . clone ( ) ) )
. collect ::< Vec < _ > > ( ) ;
let texture_descriptions = texture_descs
. iter ( )
. map ( | texture | {
json! ( {
" width " : texture . width ,
" length " : texture . height
} )
} )
. collect ::< Vec < _ > > ( ) ;
json! ( {
" format_version " : " 1.13.0 " ,
" netease:block_geometry " : {
" bones " : bones ,
" description " : {
" identifier " : identifier ,
" textures " : textures ,
" textures_descriptions " : texture_descriptions ,
" use_ao " : false
}
}
} )
}
fn required_string_field < ' a > ( value : & ' a Value , field : & str ) -> Result < & ' a str > {
value
. get ( field )
. and_then ( Value ::as_str )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 缺少字符串字段 {} " , field ) ) )
}
fn required_u64_field ( value : & Value , field : & str ) -> Result < u64 > {
value
. get ( field )
. and_then ( Value ::as_u64 )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 缺少无符号整数字段 {} " , field ) ) )
}
fn required_array_field < ' a > ( value : & ' a Value , field : & str ) -> Result < & ' a Vec < Value > > {
value
. get ( field )
. and_then ( Value ::as_array )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 缺少数组字段 {} " , field ) ) )
}
fn optional_array_field < ' a > ( value : & ' a Value , field : & str ) -> Result < Option < & ' a Vec < Value > > > {
match value . get ( field ) {
Some ( Value ::Array ( items ) ) = > Ok ( Some ( items ) ) ,
Some ( _ ) = > Err ( CliError ::InvalidData ( format! ( " 字段 {} 必须是数组 " , field ) ) ) ,
None = > Ok ( None ) ,
}
}
fn required_object_field < ' a > ( value : & ' a Value , field : & str ) -> Result < & ' a Map < String , Value > > {
value
. get ( field )
. and_then ( Value ::as_object )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 缺少对象字段 {} " , field ) ) )
}
fn required_vec3 ( value : & Value , field : & str ) -> Result < [ f64 ; 3 ] > {
let array = required_array_field ( value , field ) ? ;
if array . len ( ) ! = 3 {
return Err ( CliError ::InvalidData ( format! (
" 字段 {} 长度应为 3, 实际为 {} " ,
field ,
array . len ( )
) ) ) ;
}
Ok ( [
number_as_f64 ( & array [ 0 ] , field ) ? ,
number_as_f64 ( & array [ 1 ] , field ) ? ,
number_as_f64 ( & array [ 2 ] , field ) ? ,
] )
}
fn optional_vec3 ( value : & Value , field : & str ) -> Result < [ f64 ; 3 ] > {
match value . get ( field ) {
Some ( _ ) = > required_vec3 ( value , field ) ,
None = > Ok ( [ 0.0 , 0.0 , 0.0 ] ) ,
}
}
fn number_array ( values : & [ f64 ] ) -> Result < Value > {
values
. iter ( )
. map ( | value | number_value ( * value ) )
. collect ::< Result < Vec < _ > > > ( )
. map ( Value ::Array )
}
fn number_value ( value : f64 ) -> Result < Value > {
let value = if value = = 0.0 { 0.0 } else { value } ;
Number ::from_f64 ( value )
. map ( Value ::Number )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 无法写入非有限数字 {} " , value ) ) )
}
fn number_as_f64 ( value : & Value , field : & str ) -> Result < f64 > {
value
. as_f64 ( )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 字段 {} 必须是数字 " , field ) ) )
}
fn number_as_u64 ( value : & Value , field : & str ) -> Result < u64 > {
value
. as_u64 ( )
. ok_or_else ( | | CliError ::InvalidData ( format! ( " 字段 {} 必须是非负整数 " , field ) ) )
}
fn number_as_i64 ( value : & Value , field : & str ) -> Result < i64 > {
if let Some ( value ) = value . as_i64 ( ) {
return Ok ( value ) ;
}
let Some ( value ) = value . as_u64 ( ) else {
return Err ( CliError ::InvalidData ( format! ( " 字段 {} 必须是整数 " , field ) ) ) ;
} ;
i64 ::try_from ( value ) . map_err ( | _ | CliError ::InvalidData ( format! ( " 字段 {} 超出 i64 范围 " , field ) ) )
}