Repository navigation
Expand file tree
/
Copy pathcraftengine-interpreter.js
More file actions
5580 lines (5479 loc) · 278 KB
/
Copy pathcraftengine-interpreter.js
File metadata and controls
5580 lines (5479 loc) · 278 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* ChoTenEditor CraftEngine (CE) 配置可视化编辑器
* 依赖: js-yaml (jsyaml 全局), I18N, playSound, window.codeMirrorEditor, window.updateStatus
* CE 配置特征: 顶层集合键 (items:/blocks:/furniture:/recipes:/...) + namespace:path 条目, 无顶层 type 字段
* 分段键 items#0: / items#extra_item: 同文件多段合并; :: 扁平嵌套; $$ 版本条件键
* !!type 值 (!!long/!!IntArray 等) 解析为包装对象 {__ceTag, v}, 渲染时解包显示, 写回时恢复前缀
* 已知限制 (与 chemdah 一致): 同步为全文件重建, 注释与引号风格丢失
*/
(function () {
'use strict';
var ROOT = typeof window !== 'undefined' ? window : globalThis;
var YAML = (typeof jsyaml !== 'undefined') ? jsyaml : require('js-yaml');
function _t(key, params) {
return (typeof I18N !== 'undefined' && I18N.t) ? I18N.t(key, params) : key;
}
// 带兜底文案的 i18n (字典尚未加载/缺键时使用 fallback, 并自行做 {x} 插值)
function _tf(key, fallback, params) {
var v = null;
try {
if (typeof I18N !== 'undefined' && I18N.t) {
v = I18N.t(key);
if (v === key) v = null;
}
} catch (e) { v = null; }
if (v == null) v = fallback != null ? fallback : key;
if (params) {
v = String(v).replace(/\{(\w+)\}/g, function (m, n) { return params[n] != null ? params[n] : m; });
}
return v;
}
function _sound(name) {
try { if (typeof playSound === 'function') playSound(name); } catch (e) {}
}
function _escHtml(str) {
return String(str == null ? '' : str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
function _safeJsonStringify(val) {
try { return JSON.stringify(val); } catch (e) { return '[Circular or unserializable data]'; }
}
// ============ 常量 ============
var SECTION_KEYS = [
'items', 'blocks', 'furniture', 'recipes', 'equipments', 'images', 'emoji',
'categories', 'global_variables', 'jukebox_songs', 'loot_sources', 'paintings',
'sounds', 'translations', 'placed_features', 'templates', 'lang',
];
// 条目类型: 决定表单分支; 未列出的 section 走通用 JSON 编辑
var TYPE_SECTIONS = {
items: 'item', blocks: 'block', furniture: 'furniture', recipes: 'recipe',
equipments: 'equipment', images: 'image', categories: 'category', sounds: 'sound',
emoji: 'emoji', jukebox_songs: 'jukeboxSong', paintings: 'painting',
global_variables: 'globalVariable', translations: 'translation', lang: 'lang',
loot_sources: 'lootSource', placed_features: 'placedFeature', templates: 'template',
};
// 条目键不带 namespace:path 的 section(新建/重命名用单 ID 输入)
var KEY_ONLY_SECTIONS = { global_variables: 1, translations: 1, lang: 1 };
var KEY_ONLY_RE = /^[a-zA-Z0-9_.-]+$/;
var SECTION_BASE_RE = /^([A-Za-z0-9_]+)(#.*)?$/;
var EQUIPMENT_TYPES = ['component', 'trim'];
var EQUIPMENT_LAYERS = ['humanoid', 'humanoid_leggings', 'wings', 'wolf_body', 'horse_body', 'llama_body', 'pig_saddle', 'strider_saddle', 'camel_saddle', 'horse_saddle', 'donkey_saddle', 'mule_saddle', 'skeleton_horse_saddle', 'zombie_horse_saddle', 'happy_ghast_body', 'camel_husk_saddle', 'nautilus_body'];
var LOOT_SOURCE_TYPES = ['block_break', 'entity_death', 'fishing', 'piglin_barter', 'container', 'archaeology', 'entity_drop', 'harvest', 'shear_block', 'vault', 'advancement'];
var LOOT_OVERWRITE = ['none', 'items', 'experience', 'all'];
// 使用选项卡分页表单的条目类型 (基础/模型/物品数据/行为/事件)
var TABBED_TYPES = { item: 1, block: 1, furniture: 1 };
var ITEM_BEHAVIOR_TYPES = [
'item', 'block_item', 'ceiling_block_item', 'wall_block_item', 'ground_block_item',
'double_high_block_item', 'multi_high_block_item', 'liquid_collision_block_item',
'furniture_item', 'liquid_collision_furniture_item', 'compostable_item', 'range_mining_item',
];
var BLOCK_BEHAVIOR_TYPES = [
'crop_block', 'door_block', 'fence_block', 'stairs_block', 'slab_block', 'falling_block',
'strippable_block', 'budding_block', 'button_block', 'trapdoor_block', 'pressure_plate_block',
'seat_block', 'sofa_block', 'grass_block', 'spreading_block', 'sapling_block', 'stem_block',
'bush_block', 'lamp_block', 'toggleable_lamp_block', 'decay_block', 'leaves_block',
'snowy_block', 'concrete_powder_block', 'drop_experience_block', 'display_item_block',
'simple_storage_block', 'liquid_flowable_block', 'near_liquid_block', 'on_liquid_block',
'hangable_block', 'hanging_block', 'multi_high_block', 'double_high_block',
'directional_attached_block', 'face_attached_horizontal_directional_block',
'attached_stem_block', 'change_over_time_block', 'chime_block', 'drawer_block',
'fence_gate_block', 'item_frame_block', 'simple_particle_block', 'stackable_block',
'sturdy_base_block', 'surface_spreading_block', 'tint_source_block', 'vertical_crop_block',
'wall_torch_particle_block', 'bouncing_block', 'drop_exp_block',
];
var FURNITURE_BEHAVIOR_TYPES = ['simple_storage_furniture', 'glowing_furniture', 'display_item_furniture'];
// 每个 behavior 的建模字段: [path, type] 或 [path, 'select', [options]]
// type: text / number / bool / lines / select / json; 数据源: wiki i18n/zh-Hans behaviors 文档
var BEHAVIOR_FIELDS = {
// ---- block behaviors ----
attached_stem_block: [['fruit', 'text'], ['stem', 'text']],
bouncing_block: [['bounce_height', 'number'], ['fall_damage_multiplier', 'number'], ['sync_player_position', 'bool']],
budding_block: [['growth_chance', 'number'], ['blocks', 'lines']],
bush_block: [['blacklist', 'bool'], ['stackable', 'bool'], ['max_height', 'number'], ['delay', 'number'], ['bottom_blocks', 'lines'], ['bottom_block_tags', 'lines']],
button_block: [['ticks_to_stay_pressed', 'number'], ['can_be_activated_by_arrows', 'bool'], ['sounds', 'json']],
change_over_time_block: [['change_speed', 'number'], ['next_block', 'text'], ['excluded_properties', 'lines']],
chime_block: [['sounds', 'json']],
concrete_powder_block: [['solid_block', 'text']],
crop_block: [['grow_speed', 'number'], ['light_requirement', 'number'], ['max_light_requirement', 'number'], ['spawn_light_requirement', 'number'], ['max_spawn_light_requirement', 'number'], ['is_bone_meal_target', 'bool'], ['bone_meal_age_bonus', 'json']],
decay_block: [['decay_into', 'text'], ['delay', 'text'], ['chance', 'number'], ['required_light', 'number']],
directional_attached_block: [['blacklist', 'bool'], ['attached_blocks', 'lines'], ['attached_block_tags', 'lines']],
display_item_block: [['position', 'text'], ['has_signal', 'bool'], ['data_key', 'text'], ['tint_source', 'bool'], ['sounds', 'json']],
door_block: [['can_open_with_hand', 'bool'], ['can_open_by_wind_charge', 'bool'], ['sounds', 'json']],
drawer_block: [['max_stacks', 'number'], ['has_signal', 'bool'], ['allow_input', 'bool'], ['allow_output', 'bool'], ['item_position', 'text'], ['text_position', 'text'], ['item_scale', 'text'], ['text_scale', 'text'], ['data_key', 'text'], ['compatible_mode', 'bool'], ['sounds', 'json']],
drop_exp_block: [['amount', 'text'], ['conditions', 'json']],
drop_experience_block: [['amount', 'text'], ['conditions', 'json']],
face_attached_horizontal_directional_block: [['blacklist', 'bool'], ['attached_blocks', 'lines'], ['attached_block_tags', 'lines']],
falling_block: [['hurt_amount', 'number'], ['max_hurt', 'number'], ['sounds', 'json']],
fence_block: [['connectable_block_tag', 'text'], ['can_leash', 'bool']],
fence_gate_block: [['can_open_with_hand', 'bool'], ['can_open_by_wind_charge', 'bool'], ['sounds', 'json']],
grass_block: [['feature', 'text']],
hanging_block: [['blacklist', 'bool'], ['stackable', 'bool'], ['max_height', 'number'], ['delay', 'number'], ['above_blocks', 'lines'], ['above_block_tags', 'lines']],
item_frame_block: [['position', 'text'], ['glow', 'bool'], ['invisible', 'bool'], ['render_map_item', 'bool'], ['data_key', 'text'], ['sounds', 'json']],
liquid_flowable_block: [['drop_item', 'bool']],
multi_high_block: [['property', 'text']],
near_liquid_block: [['liquid_type', 'lines'], ['stackable', 'bool'], ['positions', 'lines']],
on_liquid_block: [['liquid_type', 'lines'], ['stackable', 'bool']],
pressure_plate_block: [['sensitivity', 'select', ['all', 'mob']], ['pressed_time', 'number'], ['sounds', 'json']],
sapling_block: [['feature', 'text'], ['light_requirement', 'number'], ['max_light_requirement', 'number'], ['grow_speed', 'number'], ['bone_meal_success_chance', 'number']],
seat_block: [['seats', 'lines']],
simple_particle_block: [['tick_interval', 'number'], ['particles', 'json']],
simple_storage_block: [['title', 'text'], ['rows', 'number'], ['has_signal', 'bool'], ['allow_input', 'bool'], ['allow_output', 'bool'], ['data_key', 'text'], ['sounds', 'json']],
spreading_block: [['target_block', 'text']],
stackable_block: [['property', 'text'], ['items', 'lines']],
stem_block: [['fruit', 'text'], ['attached_stem', 'text'], ['light_requirement', 'number'], ['max_light_requirement', 'number'], ['fruit_bottom_blocks', 'lines'], ['fruit_bottom_block_tags', 'lines']],
strippable_block: [['stripped', 'text'], ['excluded_properties', 'lines'], ['tools', 'lines'], ['sound', 'text']],
sturdy_base_block: [['direction', 'text'], ['support_types', 'lines'], ['stackable', 'bool'], ['max_height', 'number'], ['delay', 'number']],
surface_spreading_block: [['light_requirement', 'number'], ['max_light_requirement', 'number'], ['base_block', 'text']],
tint_source_block: [['drop_item', 'bool'], ['data_key', 'text']],
toggleable_lamp_block: [['can_open_with_hand', 'bool']],
trapdoor_block: [['can_open_with_hand', 'bool'], ['can_open_by_wind_charge', 'bool'], ['sounds', 'json']],
vertical_crop_block: [['max_height', 'number'], ['grow_speed', 'number'], ['direction', 'select', ['up', 'down']]],
wall_torch_particle_block: [['tick_interval', 'number'], ['particles', 'json']],
// ---- item behaviors ----
block_item: [['block', 'text']],
ceiling_block_item: [['block', 'text']],
compostable_item: [['chance', 'number']],
double_high_block_item: [['block', 'text']],
furniture_item: [['furniture', 'text'], ['rules', 'json'], ['ignore_placer', 'bool'], ['ignore_entities', 'bool']],
ground_block_item: [['block', 'text']],
liquid_collision_block_item: [['offset_y', 'number'], ['block', 'text']],
liquid_collision_furniture_item: [['furniture', 'text'], ['rules', 'json'], ['ignore_placer', 'bool'], ['ignore_entities', 'bool'], ['source_only', 'bool'], ['liquid_type', 'lines']],
multi_high_block_item: [['block', 'text']],
range_mining_item: [['conditions', 'json'], ['range', 'lines']],
wall_block_item: [['block', 'text']],
// ---- furniture behaviors ----
display_item_furniture: [['data_key', 'text'], ['sounds', 'json'], ['variants', 'json']],
glowing_furniture: [['lights', 'json'], ['variants', 'json']],
simple_storage_furniture: [['title', 'text'], ['rows', 'number'], ['data_key', 'text'], ['sounds', 'json']],
};
var RECIPE_TYPES = [
'shaped', 'shapeless', 'shaped_transform', 'shapeless_transform', 'smelting', 'blasting',
'smoking', 'campfire_cooking', 'stonecutting', 'smithing_transform', 'smithing_trim', 'brewing',
];
var VETO_NAME_RE = /^pack\.ya?ml$/;
var NS_RE = /^[a-z0-9_]+$/;
var PATH_RE = /^[a-zA-Z0-9_./-]+$/;
// CE config.yml 特有顶层键 (用于区分其它插件 config.yml; 经服务器实测至少需 2 个)
var CE_CONFIG_KEYS_RE = /^[ \t]*(resource-pack|light-system|chunk-system|client-optimization|forced-locale)\s*:/gm;
function _looksLikeCeConfig(fname, content) {
if (!/^config\.ya?ml$/.test(fname)) return false;
if (!/^[ \t]*config-version\s*:/m.test(content || '')) return false;
var m = String(content || '').match(CE_CONFIG_KEYS_RE);
return m !== null && m.length >= 2;
}
// ============ 检测 ============
// 策略: 路径优先。CE 工程文件仅在 resources/<命名空间>/configuration(s)/ 目录内识别;
// config.yml (插件配置) 高优先级单独识别。
function detectFileType(content, filePath) {
if (typeof content !== 'string' || !content.trim()) return 'unknown';
var fpath = filePath ? String(filePath).replace(/\\/g, '/') : '';
var fname = fpath.split('/').pop() || '';
if (VETO_NAME_RE.test(fname)) return 'unknown';
// CE 工程文件: 仅 resources/<namespace>/configuration(s)/ 目录内
if (fpath) {
var parts = fpath.split('/');
for (var i = 0; i + 2 < parts.length; i++) {
if (parts[i] === 'resources' && (parts[i + 2] === 'configuration' || parts[i + 2] === 'configurations')) {
return 'craftengine';
}
}
}
// [高优先级] CE 插件 config.yml
if (_looksLikeCeConfig(fname, content)) return 'craftengine';
return 'unknown';
}
// ============ 工程根回溯(渲染层经 IPC) ============
var _projectCache = Object.create(null); // dir -> Promise
function resolveProjectRoot(filePath) {
if (!filePath || !ROOT.electronAPI || !ROOT.electronAPI.ce) {
return Promise.resolve({ found: false });
}
var dir = filePath.replace(/\\/g, '/').replace(/\/[^/]*$/, '');
if (_projectCache[dir]) return _projectCache[dir];
var p = ROOT.electronAPI.ce.resolveProjectRoot(filePath).catch(function () { return { found: false }; });
if (Object.keys(_projectCache).length > 50) {
var oldest = Object.keys(_projectCache)[0];
delete _projectCache[oldest];
}
_projectCache[dir] = p;
return p;
}
// ============ 工程级检测(打开项目目录时) ============
// 仿 ChemdahInterpreter.detectProjectTypes: 在项目根一层内识别 CE 特征
// 返回 { isCraftEngine, namespace: string|null }
// 特征 (任一即命中):
// a) 子目录 resources/ 内任意 pack.yml (读头 4KB 校验 namespace: 字段) → 取该 namespace
// b) 根下 config.yml 含 config-version: 且同目录存在 mappings.yml|commands.yml|translations → 插件根特征
var _projectTypesCache = { path: null, result: null };
async function detectProjectTypes(projectPath) {
var result = { isCraftEngine: false, namespace: null };
if (!projectPath || typeof projectPath !== 'string') return result;
if (!ROOT.electronAPI || !ROOT.electronAPI.readdir) return result;
if (_projectTypesCache.path === projectPath && _projectTypesCache.result) return _projectTypesCache.result;
var normPath = String(projectPath).replace(/\\/g, '/').replace(/\/+$/, '');
var readHead = async function (p) {
try {
var r = await ROOT.electronAPI.readFile(p);
if (r && r.success && typeof r.content === 'string') {
return r.content.slice(0, 4096);
}
} catch (e) {}
return '';
};
try {
var entries = await ROOT.electronAPI.readdir(normPath);
if (entries && entries.success && Array.isArray(entries.files)) {
var names = {};
for (var i = 0; i < entries.files.length; i++) names[entries.files[i].name] = entries.files[i];
// b) 插件根特征 (config.yml + 版本键 + 兄弟特征); 版本键: 旧版 config-version:, 新版 ___version___:
if (names['config.yml'] && !names['config.yml'].isDirectory) {
var head = await readHead(normPath + '/config.yml');
if (/^[\t ]*(config-version|___version___)\s*:/m.test(head) &&
(names['mappings.yml'] || names['commands.yml'] || (names['translations'] && names['translations'].isDirectory))) {
result.isCraftEngine = true;
}
}
// a) resources/<pack>/... : pack.yml 可选 (缺失时按目录结构判定)。
// 即使 b) 已命中 (插件根), 也继续扫 namespace —— b) 只证插件在, 不给命名空间。
if (names['resources'] && names['resources'].isDirectory) {
try {
var resEntries = await ROOT.electronAPI.readdir(normPath + '/resources');
if (resEntries && resEntries.success && Array.isArray(resEntries.files)) {
for (var j = 0; j < resEntries.files.length; j++) {
var packDir = resEntries.files[j];
if (!packDir.isDirectory) continue;
var packBase = normPath + '/resources/' + packDir.name;
// a1) pack.yml 带 namespace: (最权威)
var packHead = await readHead(packBase + '/pack.yml');
var m = packHead.match(/^namespace:\s*(\S+)/m);
if (m) {
result.isCraftEngine = true;
result.namespace = m[1];
break;
}
// a2) 无 pack.yml: 看包内是否有 configuration(s)/ 或 resourcepack/ 特征目录
var packEntries = await ROOT.electronAPI.readdir(packBase);
if (packEntries && packEntries.success && Array.isArray(packEntries.files)) {
var pkNames = {};
for (var k = 0; k < packEntries.files.length; k++) pkNames[packEntries.files[k].name] = packEntries.files[k];
var hasStruct = (pkNames['configuration'] && pkNames['configuration'].isDirectory) ||
(pkNames['configurations'] && pkNames['configurations'].isDirectory) ||
(pkNames['resourcepack'] && pkNames['resourcepack'].isDirectory);
if (hasStruct) {
result.isCraftEngine = true;
if (!result.namespace) result.namespace = packDir.name;
break;
}
}
}
}
} catch (e) {}
}
}
} catch (e) {}
_projectTypesCache = { path: projectPath, result: result };
return result;
}
// ============ !!type 值包装 (configuration.mdx Expanded Value Structure) ============
// 11 种 Java 类型 tag 注册到自定义 schema: 解析时构造包装对象 {__ceTag, v},
// 渲染汇聚点 (_sfScalarText/_sfYvCtrl/...) 解包显示, 序列化汇聚点恢复 !!前缀
function _ceIsWrap(v) { return v !== null && typeof v === 'object' && typeof v.__ceTag === 'string'; }
function _ceUnwrap(v) { return _ceIsWrap(v) ? v.v : v; }
var _ceSchema = null;
function _ceYamlSchema() {
if (_ceSchema) return _ceSchema;
var scNum = function (name, conv) {
return new YAML.Type('tag:yaml.org,2002:' + name, {
kind: 'scalar', resolve: function () { return true; },
construct: function (d) { return { __ceTag: name, v: conv(d) }; },
});
};
var scSeq = function (name, conv) {
return new YAML.Type('tag:yaml.org,2002:' + name, {
kind: 'sequence', resolve: function () { return true; },
construct: function (d) { return { __ceTag: name, v: conv(d) }; },
});
};
var i32 = function (d) { var n = parseInt(String(d), 10); return isNaN(n) ? String(d) : n; };
var f64 = function (d) { var n = parseFloat(String(d)); return isNaN(n) ? String(d) : n; };
var arrN = function (fn) { return function (a) { return a.map(fn); }; };
_ceSchema = YAML.DEFAULT_SCHEMA.extend({ explicit: [
scNum('long', i32), scNum('float', f64), scNum('byte', i32), scNum('short', i32),
scSeq('ByteArray', arrN(i32)), scSeq('IntArray', arrN(i32)), scSeq('LongArray', arrN(i32)),
scSeq('DoubleArray', arrN(f64)), scSeq('IntList', arrN(i32)), scSeq('LongList', arrN(i32)),
scSeq('DoubleList', arrN(f64)),
]});
return _ceSchema;
}
// ============ 解析 ============
function parse(content) {
var doc = null;
var strippedTags = 0;
try {
doc = YAML.load(content, { schema: _ceYamlSchema() });
} catch (e) {
if (/unknown tag/.test(String(e.message || e))) {
var fixed = content.replace(/!![A-Za-z][A-Za-z0-9]* /g, function () { strippedTags++; return ''; });
try { doc = YAML.load(fixed, { schema: _ceYamlSchema() }); } catch (e2) { return { error: e2.message || String(e2) }; }
} else {
return { error: e.message || String(e) };
}
}
var parsed = {
sections: [],
_fileLevelRaw: {},
_topOrder: [],
_strippedTags: strippedTags,
_comments: { head: [], tail: [], top: {}, topInline: {}, sub: {}, subInline: {}, afterKey: {}, afterSection: {} },
};
if (doc === null || doc === undefined) {
_collectComments(content, parsed);
return parsed;
}
if (typeof doc !== 'object' || Array.isArray(doc)) {
return { error: 'top-level must be a mapping' };
}
var order = Object.keys(doc);
for (var i = 0; i < order.length; i++) {
var key = order[i];
var m = key.match(SECTION_BASE_RE);
if (m && SECTION_KEYS.indexOf(m[1]) !== -1 && doc[key] && typeof doc[key] === 'object' && !Array.isArray(doc[key])) {
var section = {
key: key,
base: m[1],
segment: m[2] || '',
entries: [],
entryOrder: Object.keys(doc[key]),
_comments: { beforeEntry: {}, inlineEntry: {} },
};
var entries = Object.keys(doc[key]);
var isRootMap = _sfSectionIsRootMap(m[1]);
for (var e = 0; e < entries.length; e++) {
var ekey = entries[e];
var edata = doc[key][ekey];
// 版本键分组 (equipments 等 root-map / items 等条目 section): 组内条目展开为条目, 版本键只作分组标记;
// 非 root-map section 要求组内键全部为 namespace:path 条目 (排除把物品字段直接写在版本键下的扁平写法)
if (_sfVersionKeyRe.test(ekey) && edata !== null && typeof edata === 'object' &&
!Array.isArray(edata) && Object.keys(edata).length > 0 &&
(isRootMap || (TYPE_SECTIONS[m[1]] && _sfVersionGroupChildrenAreEntries(edata)))) {
var gkeys = Object.keys(edata);
var shareC = { before: {}, inline: {}, afterKey: {} };
for (var g = 0; g < gkeys.length; g++) {
var gdata = edata[gkeys[g]];
section.entries.push({
key: gkeys[g],
_group: ekey,
data: gdata,
_rawOrder: (gdata && typeof gdata === 'object' && !Array.isArray(gdata)) ? Object.keys(gdata) : [],
_comments: shareC,
});
}
continue;
}
section.entries.push({
key: ekey,
data: edata,
_rawOrder: (edata && typeof edata === 'object' && !Array.isArray(edata)) ? Object.keys(edata) : [],
_comments: { before: {}, inline: {}, afterKey: {} },
});
}
parsed.sections.push(section);
parsed._topOrder.push({ kind: 'section', key: key });
} else {
parsed._fileLevelRaw[key] = doc[key];
parsed._topOrder.push({ kind: 'raw', key: key });
}
}
_collectComments(content, parsed);
return parsed;
}
// ---- 注释采集 ----
// 按行扫描原文件, 把注释按"键路径"记录到解析模型, 供序列化时回写
function _splitInline(line) {
var inS = false, inD = false;
for (var i = 0; i < line.length; i++) {
var c = line[i];
if (c === '\\' && (inS || inD)) { i++; continue; }
if (c === "'" && !inD) inS = !inS;
else if (c === '"' && !inS) inD = !inD;
else if (c === '#' && !inS && !inD && (i === 0 || line[i - 1] === ' ' || line[i - 1] === '\t')) {
return { value: line.slice(0, i).replace(/\s+$/, ''), comment: line.slice(i) };
}
}
return { value: line, comment: null };
}
function _collectComments(content, parsed) {
var C = parsed._comments;
var secByKey = Object.create(null);
for (var s = 0; s < parsed.sections.length; s++) secByKey[parsed.sections[s].key] = parsed.sections[s];
var lines = content.split(/\r?\n/);
var stack = [{ indent: -1, kind: 'root', path: '', sec: null, entry: null, entryKey: null }];
var pending = [];
var inBlock = false, blockBase = -1;
var inDq = false; // 多行双引号标量 (续行以 # 开头时不应视为注释)
var prevTopKey = null;
function dqOpen(line) {
var n = 0, esc = false;
for (var qi = 0; qi < line.length; qi++) {
var ch = line[qi];
if (ch === '\\' && !esc) { esc = true; continue; }
if (ch === '"' && !esc) n++;
esc = false;
}
return (n % 2) === 1;
}
function pushTo(b, key, text) {
var arr = b[key] || (b[key] = []);
arr.push(text);
}
function bucket(scope, inline) {
if (scope.kind === 'section' || scope.kind === 'filetop') return inline ? C.topInline : C.top;
if (scope.kind === 'entry') {
// 版本键分组条目: 注释存进组内共享容器 (回写按 gCom.before[entryKey] 读取)
if (scope.entry && scope.entry._group) {
return inline ? scope.entry._comments.inline : scope.entry._comments.before;
}
var ec = scope.sec._comments;
return inline ? ec.inlineEntry : ec.beforeEntry;
}
if (scope.kind === 'map') {
return scope.entry
? (inline ? scope.entry._comments.inline : scope.entry._comments.before)
: (inline ? C.subInline : C.sub);
}
return null;
}
function afterBucket(scope) {
return scope.entry ? scope.entry._comments.afterKey : C.afterKey;
}
function targetOf(scope, inline) {
var b = (scope.kind === 'seq' || scope.kind === 'itemmap') ? afterBucket(scope) : bucket(scope, inline);
if (!b) return null;
return { b: b, key: scope.kind === 'entry' ? scope.entryKey : scope.path };
}
function flushText(scope, inline, text) {
var t = targetOf(scope, inline);
if (t) pushTo(t.b, t.key, text);
}
function flushPending(scope) {
var t = targetOf(scope, false);
if (!t) return;
for (var i = 0; i < pending.length; i++) pushTo(t.b, t.key, pending[i].text);
pending = [];
}
function newScope(indent, kind, path, sec, entry, entryKey) {
return { indent: indent, kind: kind, path: path, sec: sec, entry: entry, entryKey: entryKey || null };
}
for (var li = 0; li < lines.length; li++) {
var line = lines[li];
var indent = 0;
while (indent < line.length && line[indent] === ' ') indent++;
if (inBlock) {
if (!line.trim() || indent > blockBase) continue;
inBlock = false;
}
if (inDq) {
inDq = dqOpen(line);
continue;
}
if (/^\s*#/.test(line)) { pending.push({ text: line.replace(/^\s+/, ''), indent: indent }); continue; }
if (!line.trim()) continue;
inDq = dqOpen(line);
var sp = _splitInline(line);
var content = sp.value;
var dash = content.substr(indent, 2) === '- ' || content.substr(indent) === '-';
if (dash) {
while (stack.length > 1 && stack[stack.length - 1].indent > indent) stack.pop();
var top = stack[stack.length - 1];
if (top.kind === 'seq' && top.indent === indent) {
flushPending(top);
} else {
var seqScope = newScope(indent, 'seq', top.path, top.sec, top.entry, null);
flushPending(seqScope);
stack.push(seqScope);
top = seqScope;
}
if (sp.comment) flushText(top, false, sp.comment);
var rest = content.substr(indent).replace(/^- ?/, '');
if (/^[|>][-+]?\d*\s*$/.test(rest)) { inBlock = true; blockBase = indent; }
else {
var km = rest.match(/^(\S.*?):(\s|$)/);
if (km) stack.push(newScope(indent + 2, 'itemmap', top.path, top.sec, top.entry, null));
}
continue;
}
var keyM = content.substr(indent).match(/^(\S.*?):(\s|$)/);
if (keyM) {
var key = keyM[1];
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
var top = stack[stack.length - 1];
if (top.kind === 'seq' || top.kind === 'itemmap') {
flushPending(top);
if (sp.comment) flushText(top, false, sp.comment);
stack.push(newScope(indent, 'itemmap', top.path, top.sec, top.entry, null));
continue;
}
var parent = top;
var scope;
if (parent.kind === 'root') {
var isSec = !!secByKey[key];
for (var pi = 0; pi < pending.length; pi++) {
if (pending[pi].indent === 0) pushTo(C.top, key, pending[pi].text);
else if (prevTopKey) pushTo(C.afterSection, prevTopKey, pending[pi].text);
else C.head.push(pending[pi].text);
}
pending = [];
scope = newScope(indent, isSec ? 'section' : 'filetop', key, isSec ? secByKey[key] : null, null, null);
prevTopKey = key;
} else if (parent.kind === 'section') {
var en = null;
for (var eni = 0; eni < parent.sec.entries.length; eni++) {
var e2 = parent.sec.entries[eni];
// 版本键分组: 文件里的分组键匹配组内条目 (共享注释容器)
if (e2.key === key || (e2._group && e2._group === key)) { en = e2; break; }
}
scope = newScope(indent, 'entry', '', parent.sec, en, key);
} else {
var p = parent.path;
scope = newScope(indent, 'map', p ? p + '.' + key : key, parent.sec, parent.entry, null);
}
flushPending(scope);
if (sp.comment) flushText(scope, true, sp.comment);
stack.push(scope);
if (/:\s*[|>][-+]?\d*\s*$/.test(content.substr(indent))) { inBlock = true; blockBase = indent; }
continue;
}
// 其它续行: 保持作用域, 不采集
}
for (var ti = 0; ti < pending.length; ti++) {
if (pending[ti].indent === 0) C.tail.push(pending[ti].text);
else if (prevTopKey) pushTo(C.afterSection, prevTopKey, pending[ti].text);
else C.tail.push(pending[ti].text);
}
}
// ============ 序列化 ============
var _SPECIAL_START_RE = /^[-?!#`"&*|>%@<\s,\[\]{}]/;
var _NUMBER_RE = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
var _BOOL_RE = /^(true|false|yes|no|on|off|null|none|~)$/i;
function _quoteKey(key) {
if (/:\s/.test(key) || /\s#/.test(key) || /[:#\s]$/.test(key) || _SPECIAL_START_RE.test(key) || key === '' || key !== String(key) ||
_NUMBER_RE.test(key) || _BOOL_RE.test(key)) {
return "'" + String(key).replace(/'/g, "''") + "'";
}
return String(key);
}
function _genFlow(val) {
if (_ceIsWrap(val)) return '!!' + val.__ceTag + ' ' + _genFlow(val.v);
if (Array.isArray(val)) return '[' + val.map(_genFlow).join(', ') + ']';
if (val && typeof val === 'object' && !(val instanceof Date)) {
return '{' + Object.keys(val).map(function (k) { return _quoteKey(k) + ': ' + _genFlow(val[k]); }).join(', ') + '}';
}
return _genScalar(val);
}
function _genScalarRaw(val) {
if (typeof val === 'number') {
// YAML 特殊数值符号, 避免 .inf/.nan 被静默改成 null
if (!isFinite(val)) return isNaN(val) ? '.nan' : (val > 0 ? '.inf' : '-.inf');
return String(val);
}
if (typeof val === 'boolean') return String(val);
var s = String(val);
if (s === '') return "''";
var needsQuote =
_SPECIAL_START_RE.test(s) || /:\s/.test(s) || /\s#/.test(s) || /[\s:#]$/.test(s) ||
_NUMBER_RE.test(s) || _BOOL_RE.test(s);
if (!needsQuote) return s;
if (s.indexOf("'") !== -1) {
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
}
return "'" + s + "'";
}
function _genScalar(val) {
if (_ceIsWrap(val)) {
// 数值型 tag (!!long 等) 的值可能经输入框写回为数字字符串, 保持裸数字输出, 避免 !!long '7'
var raw;
if (Array.isArray(val.v)) raw = _genFlow(val.v);
else if (typeof val.v === 'number') raw = _genScalarRaw(val.v);
else if (typeof val.v === 'string' && _NUMBER_RE.test(val.v)) raw = val.v;
else raw = _genScalarRaw(val.v);
return '!!' + val.__ceTag + ' ' + raw;
}
return _genScalarRaw(val);
}
function _fmtDate(d) {
var y = d.getUTCFullYear(), mo = d.getUTCMonth() + 1, da = d.getUTCDate();
return y + '-' + (mo < 10 ? '0' : '') + mo + '-' + (da < 10 ? '0' : '') + da;
}
// 生成 map 块的行(含缩进); cm 为注释表 { before, inline, afterKey }, prefix 为当前路径前缀
function _genMapLines(map, indent, cm, prefix) {
var pad = ' '.repeat(indent);
var lines = [];
var keys = Object.keys(map);
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
var path = prefix ? prefix + '.' + k : k;
if (cm && cm.before && cm.before[path]) {
var bcs = cm.before[path];
for (var bci = 0; bci < bcs.length; bci++) lines.push(pad + bcs[bci]);
}
var inline = (cm && cm.inline && cm.inline[path]) || null;
var after = (cm && cm.afterKey && cm.afterKey[path]) || null;
var v = map[k];
var kq = _quoteKey(k);
var header;
if (_ceIsWrap(v)) header = pad + kq + ': ' + _genScalar(v);
else if (v === null || v === undefined) header = pad + kq + ': ~';
else if (typeof v === 'string' && v.indexOf('\n') !== -1) header = pad + kq + ': |-';
else if (typeof v === 'object') {
if (v instanceof Date) header = pad + kq + ': ' + _genScalar(_fmtDate(v));
else if (Array.isArray(v)) header = (v.length === 0) ? pad + kq + ': []' : pad + kq + ':';
else header = (Object.keys(v).length === 0) ? pad + kq + ': {}' : pad + kq + ':';
} else header = pad + kq + ': ' + _genScalar(v);
if (inline) header += ' ' + inline;
lines.push(header);
// 值体
if (typeof v === 'string' && v.indexOf('\n') !== -1) {
var vs = v.split('\n');
for (var li = 0; li < vs.length; li++) lines.push(pad + ' ' + vs[li]);
} else if (v !== null && typeof v === 'object' && !(v instanceof Date) && !_ceIsWrap(v)) {
if (Array.isArray(v)) {
if (v.length > 0) {
if (after) for (var aci = 0; aci < after.length; aci++) lines.push(pad + ' ' + after[aci]);
for (var ai = 0; ai < v.length; ai++) {
var item = v[ai];
if (_ceIsWrap(item)) {
lines.push(pad + ' - !!' + item.__ceTag + ' ' + _genFlow(item.v));
} else if (item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date)) {
if (Object.keys(item).length === 0) { lines.push(pad + ' - {}'); continue; }
var rendered = _genMapLines(item, indent + 2);
lines.push(rendered[0].replace(pad + ' ', pad + ' - '));
for (var ri = 1; ri < rendered.length; ri++) lines.push(rendered[ri]);
} else if (typeof item === 'string' && item.indexOf('\n') !== -1) {
lines.push(pad + ' - |-');
var is = item.split('\n');
for (var ii = 0; ii < is.length; ii++) lines.push(pad + ' ' + is[ii]);
} else if (Array.isArray(item)) {
lines.push(pad + ' - ' + _genFlow(item));
} else {
lines.push(pad + ' - ' + _genScalar(item));
}
}
}
} else if (Object.keys(v).length > 0) {
var sub = _genMapLines(v, indent + 1, cm, path);
for (var si = 0; si < sub.length; si++) lines.push(sub[si]);
}
}
}
return lines;
}
// 序列化单个顶层键值对(file-level raw 键); cm 为文件级注释表, prefix 为路径前缀
function _genTopBlock(key, val, cm, prefix) {
var kq = _quoteKey(key);
var path = prefix ? prefix + '.' + key : key;
var inline = (cm && cm.topInline && cm.topInline[key]) || null;
if (val === null || val === undefined) {
var l0 = kq + ': ~';
return inline ? l0 + ' ' + inline : l0;
}
if (_ceIsWrap(val)) {
var lw = kq + ': ' + _genScalar(val);
return inline ? lw + ' ' + inline : lw;
}
if (val instanceof Date) {
var ld = kq + ': ' + _genScalar(_fmtDate(val));
return inline ? ld + ' ' + inline : ld;
}
if (typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0) {
var l1 = kq + ': {}';
return inline ? l1 + ' ' + inline : l1;
}
if (typeof val === 'object' && !Array.isArray(val) && !(val instanceof Date)) {
var lines = [kq + ':'];
if (cm && cm.afterKey && cm.afterKey[path]) {
var a = cm.afterKey[path];
for (var ai = 0; ai < a.length; ai++) lines.push(' ' + a[ai]);
}
var sub = _genMapLines(val, 1, { before: cm.sub, inline: cm.subInline, afterKey: cm.afterKey }, key);
for (var si = 0; si < sub.length; si++) lines.push(sub[si]);
if (inline) lines[0] += ' ' + inline;
return lines.join('\n');
}
var l2 = kq + ': ' + _genScalar(val);
return inline ? l2 + ' ' + inline : l2;
}
function _entryKeyOrder(entry) {
var keys = [];
for (var i = 0; i < (entry._rawOrder || []).length; i++) keys.push(entry._rawOrder[i]);
if (entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)) {
var all = Object.keys(entry.data);
for (var j = 0; j < all.length; j++) {
if (keys.indexOf(all[j]) === -1) keys.push(all[j]);
}
}
return keys;
}
function generateYAML(parsed) {
var C = parsed._comments || { head: [], tail: [], top: {}, topInline: {}, afterSection: {} };
var blocks = [];
if (!parsed._topOrder || parsed._topOrder.length === 0) {
var emptyLines = (C.head || []).concat(C.tail || []);
return emptyLines.length ? emptyLines.join('\n') + '\n{}' : '{}';
}
for (var hi = 0; hi < (C.head || []).length; hi++) blocks.push(C.head[hi]);
for (var i = 0; i < parsed._topOrder.length; i++) {
var t = parsed._topOrder[i];
var topC = (C.top || {})[t.key] || null;
if (topC) for (var tci = 0; tci < topC.length; tci++) blocks.push(topC[tci]);
var inlineTop = (C.topInline || {})[t.key] || null;
if (t.kind === 'raw') {
blocks.push(_genTopBlock(t.key, parsed._fileLevelRaw[t.key], C, t.key));
} else {
var section = null;
for (var s = 0; s < parsed.sections.length; s++) {
if (parsed.sections[s].key === t.key) { section = parsed.sections[s]; break; }
}
if (section) {
var kq = _quoteKey(section.key);
if (section.entries.length === 0) {
var es = kq + ': {}';
if (inlineTop) es += ' ' + inlineTop;
blocks.push(es);
} else {
var lines = [kq + ':'];
if (inlineTop) lines[0] += ' ' + inlineTop;
var secC = section._comments || { beforeEntry: {}, inlineEntry: {} };
var curGroup = null;
var gCom = null; // 版本键分组的共享注释容器
for (var e = 0; e < section.entries.length; e++) {
var entry = section.entries[e];
var ec = entry._comments || { before: {}, inline: {}, afterKey: {} };
var g = entry._group || null;
var padE = g !== null ? ' ' : ' ';
var padArr = g !== null ? ' ' : ' ';
if (g !== curGroup) {
curGroup = g;
if (g !== null) {
gCom = ec;
// 分组键注释与组内条目同存共享容器: 采集端 entry 级注释也写入 entry._comments(shareC)
var gbf = (gCom.before && gCom.before[g]) || null;
if (gbf) for (var gbi = 0; gbi < gbf.length; gbi++) lines.push(' ' + gbf[gbi]);
var gInl = (gCom.inline && gCom.inline[g]) || null;
lines.push(' ' + _quoteKey(g) + ':' + (gInl ? ' ' + gInl : ''));
}
}
var bf = (g !== null ? (gCom.before ? gCom.before[entry.key] : null) : (secC.beforeEntry[entry.key] || null));
if (bf) for (var bi = 0; bi < bf.length; bi++) lines.push(padE + bf[bi]);
var eq = _quoteKey(entry.key);
var inlineEntry = g !== null ? (gCom.inline ? gCom.inline[entry.key] : null) : (secC.inlineEntry[entry.key] || null);
var ed = entry.data;
var el;
if (ed === null || ed === undefined) { el = padE + eq + ': ~'; }
else if (typeof ed === 'object' && !Array.isArray(ed) && !(ed instanceof Date)) {
el = (Object.keys(ed).length === 0) ? padE + eq + ': {}' : padE + eq + ':';
if (inlineEntry) el += ' ' + inlineEntry;
lines.push(el);
el = null;
if (Object.keys(ed).length > 0) {
// prefix 必须为空: 采集端嵌套注释按 "键名" 记录, 不带条目前缀
var em = _genMapLines(ed, g !== null ? 3 : 2, g !== null ? gCom : ec, '');
for (var mi = 0; mi < em.length; mi++) lines.push(em[mi]);
}
} else if (Array.isArray(ed)) {
el = (ed.length === 0) ? padE + eq + ': []' : padE + eq + ':';
if (inlineEntry) el += ' ' + inlineEntry;
lines.push(el);
el = null;
if (ed.length > 0) {
var aE = ec.afterKey[''] || null;
if (aE) for (var ae = 0; ae < aE.length; ae++) lines.push(padArr + aE[ae]);
for (var li2 = 0; li2 < ed.length; li2++) {
var it2 = ed[li2];
if (it2 !== null && typeof it2 === 'object' && !Array.isArray(it2) && !(it2 instanceof Date)) {
if (Object.keys(it2).length === 0) { lines.push(padArr + '- {}'); continue; }
var r2 = _genMapLines(it2, 3);
lines.push(r2[0].replace(' ', padArr + '- '));
for (var rj = 1; rj < r2.length; rj++) lines.push(r2[rj]);
} else if (typeof it2 === 'string' && it2.indexOf('\n') !== -1) {
lines.push(padArr + '- |-');
var i3 = it2.split('\n');
for (var i4 = 0; i4 < i3.length; i4++) lines.push(padArr + ' ' + i3[i4]);
} else if (Array.isArray(it2)) {
lines.push(padArr + '- ' + _genFlow(it2));
} else {
lines.push(padArr + '- ' + _genScalar(it2));
}
}
}
} else { el = padE + eq + ': ' + _genScalar(ed); }
if (el !== null) {
if (inlineEntry) el += ' ' + inlineEntry;
lines.push(el);
}
}
blocks.push(lines.join('\n'));
}
} /* if (section) */
} /* else (section block) */
var afterS = (C.afterSection || {})[t.key] || null;
if (afterS) for (var asi = 0; asi < afterS.length; asi++) blocks.push(afterS[asi]);
}
for (var tli = 0; tli < (C.tail || []).length; tli++) blocks.push(C.tail[tli]);
return blocks.join('\n');
}
function syncToSource(parsed) {
if (parsed && parsed._isPopup) return; // 弹窗内临时数据, 不写回源码
var yaml = generateYAML(parsed);
if (ROOT.codeMirrorEditor) {
ROOT.codeMirrorEditor.setValue(yaml);
if (typeof ROOT.updateStatus === 'function') ROOT.updateStatus(_t('craftengine.syncedToSource'));
}
if (parsed) parsed._visualDirty = false;
}
// ============ Schema 表单引擎 (craftengine-schemas.js) ============
var _sfSchemas = (typeof CESchemas !== 'undefined' && CESchemas) ? CESchemas : null;
var _sfDatalistMap = null;
var _sfUidSeq = 0;
var _sfUidMap = {};
var _sfActiveContainer = null; // 当前渲染/交互中的容器 (events widget 导航状态按容器持久)
var _sfL = function (zh, en) { return { zh: zh, en: en }; };
var _sfNumRe = /^-?\d+(\.\d+)?$/;
var _sfLastParsed = null; // 最近一次 render 的 parsed, 用于标记"尚未同步到源码"的更改
function _sfMarkDirty(parsed) {
if (parsed && !parsed._visualDirty) parsed._visualDirty = true;
}
function _sfInit() {
if (_sfSchemas === null && typeof CESchemas !== 'undefined') _sfSchemas = CESchemas;
if (_sfDatalistMap) return;
_sfDatalistMap = {};
if (_sfSchemas && _sfSchemas.constants) {
var cs = _sfSchemas.constants;
Object.keys(cs).forEach(function (k) {
if (Array.isArray(cs[k])) _sfDatalistMap[k] = cs[k].slice();
});
var vi = cs.vanillaItems || [];
_sfDatalistMap.items = vi.slice();
_sfDatalistMap.blocks = vi.slice();
}
}
// ---- Minecraft 资源注册表 (ce-mcassets.js) 接入 ----
// 原版 resources 索引就绪/重扫后, 把资源列表合并进 datalist;
// 未配置资源目录时保持原样 (只有内置的原版常量列表)。
var _ceAssetsBound = false;
var _ceAssetsKindBound = {};
function _sfMergeList(name, arr) {
_sfInit();
if (!_sfDatalistMap) return false;
if (!arr || !arr.length) return false;
var base = _sfDatalistMap[name] || [];
var seen = Object.create(null);
var out = [];
for (var i = 0; i < base.length; i++) { var a = base[i]; if (!seen[a]) { seen[a] = 1; out.push(a); } }
for (var j = 0; j < arr.length; j++) { var b = arr[j]; if (!seen[b]) { seen[b] = 1; out.push(b); } }
out.sort();
_sfDatalistMap[name] = out;
return true;
}
function _ceAssetsBind() {
var A = (typeof CEMCAssets !== 'undefined') ? CEMCAssets : null;
if (!A || !A.status || A.status().state !== 'ready') return;
_sfInit();
var kinds = ['items', 'blocks', 'textures', 'blockTextures', 'itemTextures', 'guiTextures',
'models', 'blockModels', 'itemModels', 'particles', 'fonts', 'equipments', 'atlases',
'soundFiles', 'soundEvents', 'enchantments', 'potionEffects', 'entities', 'biomes',
'attributes', 'paintings', 'jukeboxSongs'];
var changed = false;
for (var i = 0; i < kinds.length; i++) {
var k = kinds[i];
var list = A.listFor(k);
if (list && list.length) {
if (_sfMergeList(k, list)) { changed = true; _ceAssetsKindBound[k] = 1; }
_ceElemRefreshDatalist(k);
_ceElemRefreshDatalist(k + 's');
}
}
_ceAssetsBound = true;
// 资源就绪后补挂 picker 按钮 / 刷新 datalist: 重新渲染当前表单
if (changed && _sfActiveContainer && _sfActiveContainer._ceRenderFn) {
try { _sfActiveContainer._ceRenderFn(); } catch (e) { /* 忽略渲染异常 */ }
}
}
function _ceAssetsEnsureBind() {
if (_ceAssetsBound) return;
if (typeof CEMCAssets === 'undefined' || !CEMCAssets) return;
CEMCAssets.onReady(function () { _ceAssetsBind(); });
if (CEMCAssets.status && CEMCAssets.status().state === 'ready') _ceAssetsBind();
}
// ---- CE 元素预载扫描 (picker 数据源) ----
// 扫描工程 configuration/<section> 目录收集条目键 (items/blocks/furniture/categories/templates),
// 合并进 _sfDatalistMap (items/blocks 与 vanilla 去重合并, 其余直接存储); 结果按 configuration 目录缓存
var _CE_ELEM_SECTIONS = {
items: 1, blocks: 1, furniture: 1, categories: 1, templates: 1,
equipments: 1, recipes: 1, loot_sources: 1, images: 1, emoji: 1,
sounds: 1, jukebox_songs: 1, paintings: 1, global_variables: 1,
placed_features: 1, translations: 1, lang: 1,
};
// 工程内可被 <image:ns:id> / <global:id> / <i18n:id> 引用的段 (供预览与诊断使用)
var _CE_EXTRA_SECTIONS = { images: 1, global_variables: 1, emoji: 1, translations: 1, lang: 1 };
var _ceElemCache = Object.create(null); // configDir -> { state: 'loading'|'done', data: {sec:[keys]} }
var _ceElemListeners = [];
// 同步: 文件路径中显式含 configuration(s) 段 (标准布局)
function _ceElemConfigDir(filePath) {
var parts = String(filePath || '').replace(/\\/g, '/').split('/');
for (var i = parts.length - 2; i >= 1; i--) {
if (parts[i] === 'configuration' || parts[i] === 'configurations') return parts.slice(0, i + 1).join('/');
}
return null;
}
// 标准布局 resources/<命名空间>/configuration(s): 上溯两级得 resources 根; 非标准布局返回 null
function _ceElemResourcesRoot(configDir) {
var parts = String(configDir || '').replace(/\\/g, '/').split('/');
var last = parts[parts.length - 1] || '';
if (last !== 'configuration' && last !== 'configurations') return null;
parts.pop(); // configuration
parts.pop(); // <命名空间>
if (parts.length < 2) return null;
return parts.join('/');
}
// 当前文件的命名空间 (resources/<ns>/configuration 的 <ns>)
function _sfCurNamespace(st) {
if (!st || !st.file) return '';
var cfg = _ceElemConfigDir(st.file);
if (!cfg) return '';
var parts = cfg.split('/');
return parts.length >= 2 ? parts[parts.length - 2] : '';
}
// 异步: 配置目录名可自定义 (如"工程内容"), 从文件所在目录向上探测,
// 第一个含 section 子目录 (items/blocks/categories/...) 或根下散落 section 单文件
// (categories.yml 等, 兼容单文件布局) 的目录即配置目录
function _ceElemLocate(filePath) {
var cfg = _ceElemConfigDir(filePath);
if (cfg) return Promise.resolve(cfg);
var api = ROOT.electronAPI;
if (!api || !api.readdir) return Promise.resolve(null);
var parts = String(filePath || '').replace(/\\/g, '/').split('/');
if (parts.length < 3) return Promise.resolve(null);
return _ceElemProbeUp(parts.slice(0, -1).join('/'), 0, api);
}
function _ceElemProbeUp(dir, depth, api) {
if (depth > 8) return Promise.resolve(null);
return api.readdir(dir).then(function (res) {
if (!res || !res.success) return null;
for (var i = 0; i < res.files.length; i++) {
if (res.files[i].isDirectory && _CE_ELEM_SECTIONS[res.files[i].name]) return dir;
}