R3EXS 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/R3EXS/utils.rb CHANGED
@@ -3,1082 +3,428 @@
3
3
  require 'oj'
4
4
  require_relative 'RGSS3'
5
5
  require_relative 'RGSS3_R3EXS'
6
+ require_relative 'logger'
6
7
 
7
8
  module R3EXS
9
+ # 工具模块
10
+ # 主要用来提供一些读取,写入,转换等功能
11
+ module Utils
12
+ # 用来匹配读取的文件名到对应的R3EXS的类
13
+ FILE_BASENAME_TO_CLASS_R3EXS = {
14
+ /\AActors\z/ => R3EXS::Actor,
15
+ /\AAnimations\z/ => R3EXS::Animation,
16
+ /\AArmors\z/ => R3EXS::Armor,
17
+ /\AClasses\z/ => R3EXS::Class,
18
+ /\ACommonEvents\z/ => R3EXS::CommonEvent,
19
+ /\AEnemies\z/ => R3EXS::Enemy,
20
+ /\AItems\z/ => R3EXS::Item,
21
+ /\AMap\d{3}\z/ => R3EXS::Map,
22
+ /\AMapInfos\z/ => R3EXS::MapInfo,
23
+ /\AScripts\z/ => Array,
24
+ /\ASkills\z/ => R3EXS::Skill,
25
+ /\AStates\z/ => R3EXS::State,
26
+ /\ASystem\z/ => R3EXS::System,
27
+ /\ATilesets\z/ => R3EXS::Tileset,
28
+ /\ATroops\z/ => R3EXS::Troop,
29
+ /\AWeapons\z/ => R3EXS::Weapon
30
+ }.freeze
31
+
32
+ # 用来匹配读取的文件名到对应的RPG的类
33
+ FILE_BASENAME_TO_CLASS_RPG = {
34
+ /\AActors\z/ => RPG::Actor,
35
+ /\AAnimations\z/ => RPG::Animation,
36
+ /\AArmors\z/ => RPG::Armor,
37
+ /\AClasses\z/ => RPG::Class,
38
+ /\ACommonEvents\z/ => RPG::CommonEvent,
39
+ /\AEnemies\z/ => RPG::Enemy,
40
+ /\AItems\z/ => RPG::Item,
41
+ /\AMap\d{3}\z/ => RPG::Map,
42
+ /\AMapInfos\z/ => RPG::MapInfo,
43
+ /\AScripts\z/ => Array,
44
+ /\ASkills\z/ => RPG::Skill,
45
+ /\AStates\z/ => RPG::State,
46
+ /\ASystem\z/ => RPG::System,
47
+ /\ATilesets\z/ => RPG::Tileset,
48
+ /\ATroops\z/ => RPG::Troop,
49
+ /\AWeapons\z/ => RPG::Weapon
50
+ }.freeze
51
+
52
+ # 读取 target_dir 下的所有 rvdata2 文件,将其反序列化为对象,并调用 block
53
+ #
54
+ # @note 注意传入 block 的 obj
55
+ # - 如果 obj 是数组或哈希,则其中可能存在 nil 元素
56
+ # - 如果 obj 是单独一个对象,则不可能为 nil
57
+ #
58
+ # @param target_dir [Pathname] 目标目录
59
+ #
60
+ # @yieldparam obj [Object] rvdata2 文件反序列化后的对象
61
+ # @yieldparam klass [::Class] rvdata2 文件所对应 R3EXS 模块的类
62
+ # @yieldparam file_basename [Pathname] 文件名(不包含扩展名)
63
+ # @yieldparam relative_dir [Pathname] 文件相对 target_dir 的路径
64
+ # @yieldreturn [void]
65
+ #
66
+ # @raise [Rvdata2DirError] target_dir 不存在
67
+ #
68
+ # @return [void]
69
+ def self.all_rvdata2_files(target_dir)
70
+ # 检查 target_dir 目录是否存在
71
+ target_dir.exist? && target_dir.directory? or raise Rvdata2DirError, "rvdata2 directory not found: #{target_dir}"
72
+
73
+ # 递归获取 target_dir 下的所有 *.rvdata2 文件
74
+ target_dir.glob('**/*.rvdata2').each do |file_path|
75
+ file_basename = file_path.basename('.rvdata2')
76
+ # 获取文件名所对应的类,若无法匹配则跳过该文件
77
+ klass = name_class(file_basename, :R3EXS)
78
+ next if klass.nil?
79
+
80
+ obj = Marshal.load(file_path.binread, freeze: true)
81
+ Logger.debug("Deserialize #{file_path}")
82
+ yield obj, klass, file_basename, file_path
83
+ end
84
+ end
8
85
 
9
- # 工具模块
10
- # 主要用来提供一些读取,写入,转换等功能
11
- module Utils
12
-
13
- # 用来匹配读取的 rvdata2 文件名
14
- RVDATA2_FILE_NAME =
15
- [
16
- /\AActors\z/,
17
- /\AAnimations\z/,
18
- /\AArmors\z/,
19
- /\AClasses\z/,
20
- /\ACommonEvents\z/,
21
- /\AEnemies\z/,
22
- /\AItems\z/,
23
- /\AMap\d{3}\z/,
24
- /\AMapInfos\z/,
25
- /\AScripts\z/,
26
- /\ASkills\z/,
27
- /\AStates\z/,
28
- /\ASystem\z/,
29
- /\ATilesets\z/,
30
- /\ATroops\z/,
31
- /\AWeapons\z/
32
- ]
33
-
34
- # 用来匹配读取的 JSON 文件名
35
- JSON_FILE_NAME =
36
- [
37
- /\AActors\z/,
38
- /\AAnimations\z/,
39
- /\AArmors\z/,
40
- /\AClasses\z/,
41
- /\AEnemies\z/,
42
- /\AItems\z/,
43
- /\AMap\d{3}\z/,
44
- /\AMapInfos\z/,
45
- /\ASkills\z/,
46
- /\AStates\z/,
47
- /\ASystem\z/,
48
- /\ATilesets\z/,
49
- /\ATroops\z/,
50
- /\AWeapons\z/
51
- ]
52
-
53
- # 用来匹配读取的文件名到对应的R3EXS的类
54
- FILE_BASENAME_TO_CLASS_R3EXS = {
55
- /\AActors\z/ => R3EXS::Actor,
56
- /\AAnimations\z/ => R3EXS::Animation,
57
- /\AArmors\z/ => R3EXS::Armor,
58
- /\AClasses\z/ => R3EXS::Class,
59
- /\ACommonEvents\z/ => R3EXS::CommonEvent,
60
- /\AEnemies\z/ => R3EXS::Enemy,
61
- /\AItems\z/ => R3EXS::Item,
62
- /\AMap\d{3}\z/ => R3EXS::Map,
63
- /\AMapInfos\z/ => R3EXS::MapInfo,
64
- /\ASkills\z/ => R3EXS::Skill,
65
- /\AStates\z/ => R3EXS::State,
66
- /\ASystem\z/ => R3EXS::System,
67
- /\ATilesets\z/ => R3EXS::Tileset,
68
- /\ATroops\z/ => R3EXS::Troop,
69
- /\AWeapons\z/ => R3EXS::Weapon
70
- }
71
-
72
- # 用来匹配读取的文件名到对应的RPG的类
73
- FILE_BASENAME_TO_CLASS_RPG = {
74
- /\AActors\z/ => RPG::Actor,
75
- /\AAnimations\z/ => RPG::Animation,
76
- /\AArmors\z/ => RPG::Armor,
77
- /\AClasses\z/ => RPG::Class,
78
- /\ACommonEvents\z/ => RPG::CommonEvent,
79
- /\AEnemies\z/ => RPG::Enemy,
80
- /\AItems\z/ => RPG::Item,
81
- /\AMap\d{3}\z/ => RPG::Map,
82
- /\AMapInfos\z/ => RPG::MapInfo,
83
- /\ASkills\z/ => RPG::Skill,
84
- /\AStates\z/ => RPG::State,
85
- /\ASystem\z/ => RPG::System,
86
- /\ATilesets\z/ => RPG::Tileset,
87
- /\ATroops\z/ => RPG::Troop,
88
- /\AWeapons\z/ => RPG::Weapon
89
- }
90
-
91
- # 事件指令的命令名称
92
- EVENT_COMMANDS = {
93
- 0 => 'Empty',
94
-
95
- =begin
96
- [string(Face Graphic name)]---[int(Face Graphic index)]---[int:{0:Normal Window, 1:Dim Background, 2:Transparent}]---[int:{0:Top, 1:Middle, 2:Bottom}]---END
97
- =end
98
- 101 => 'ShowTextAttributes',
99
-
100
- =begin
101
- [Array<string>(Choices Array)]---[int:{0:Disallow, 1:Choice 1, 2:Choice 2, 3:Choice 3, 4:Choice 4, 5:Branch}(When Cancel)]---END
102
- =end
103
- 102 => 'ShowChoices',
104
-
105
- =begin
106
- [int(Variable for Number)]---[int:{1~8}(Digits)]---END
107
- =end
108
- 103 => 'InputNumber',
109
-
110
- =begin
111
- [int(Variable for Item ID)]---END
112
- =end
113
- 104 => 'SelectKeyItem',
114
-
115
- =begin
116
- [int:{1~8}(Speed)]---[bool(No Fast Forward)]---END
117
- =end
118
- 105 => 'ShowScrollingTextAttributes',
119
-
120
- =begin
121
- [string]---END
122
- =end
123
- 108 => 'Comment',
124
-
125
- =begin
126
- |--[int:0(Switch)]---[int(Switch ID)]---[int{0:ON, 1:OFF}]---END
127
- |
128
- | |--[int:0(Compare to Constant)]---[int(Number)]-------|
129
- |--[int:1(Variable)]---[int(Variable ID)]--| |--[int{0:==, 1:>=, 2:<=, 3:>, 4:<, 5:!=}]---END
130
- | |--[int:1(Compare to Variable)]---[int(Variable ID)]--|
131
- |
132
- |--[int:2(Self Switch)]---[string{'A', 'B', 'C', 'D'}]---[int{0:ON, 1:OFF}]---END
133
- |
134
- |--[int:3(Timer)]---[int:{0~5999}(sec)]---[int{0:>=, 1:<=}]---END
135
- |
136
- | |--[int:0(In the Party)]---END
137
- | |
138
- | |--[int:1(Name)]---[string]---END
139
- | |
140
- | |--[int:2(Class)]---[int(Class ID)]---END
141
- | |
142
- |--[int:4(Actor)]---[int(Actor ID)]--------|--[int:3(Skill)]---[int(Skill ID)]---END
143
- | |
144
- | |--[int:4(Weapon)]---[int(Wwapon ID)]---END
145
- | |
146
- | |--[int:5(Armor)]---[int(Armor ID)]---END
147
- | |
148
- | |--[int:6(State)]---[int(State ID)]---END
149
- |
150
- | |--[int:0(Appeared)---END
151
- |--[int:5(Enemy)]---[int(Enemy ID)]--------|
152
- | |--[int:1(State)]---[int(State ID)]---END
153
- |
154
- |--[int:6(Character)]---[int:{-1:Player, 0:This event, 1:EV001, ...}]---[int:{2:Down, 4:Left, 6:Right, 8:Up}]---END
155
- |
156
- |--[int:7(Gold)]---[int(Money)]---[int{0:>=, 1:<=, 2:<}]---END
157
- |
158
- |--[int:8(Item)]---[int(Item ID)]---END
159
- |
160
- |--[int:9(Weapon)]---[int(Weapon ID)]---[bool(Include Equipments)]---END
161
- |
162
- |--[int:10(Armor)]---[int(Armor ID)]---[bool(Include Equipments)]---END
163
- |
164
- |--[int:11(Button)]---[int:{2:Down, 4:Left, 6:Right, 8:Up, 11:A, 12:B, 13:C, 14:X, 15:Y, 16:Z, 17:L, 18:R}]---END
165
- |
166
- |--[int:12(Script)]---[string]---END
167
- |
168
- |--[int:13(Vehicle)]---[int:{0:Boat, 1:Ship, 2:Airship}]---END
169
- =end
170
- 111 => 'ConditionalBranch',
171
- 112 => 'Loop',
172
- 113 => 'BreakLoop',
173
- 115 => 'ExitEventProcessing',
174
-
175
- =begin
176
- [int(Common Event ID)]---END
177
- =end
178
- 117 => 'CallCommonEvent',
179
-
180
- =begin
181
- [string]---END
182
- =end
183
- 118 => 'Label',
184
-
185
- =begin
186
-
187
- [string]---END
188
- =end
189
- 119 => 'JumpToLabel',
190
-
191
- =begin
192
- [int(Switch Begin ID)]---[int(Switch End ID)]---[int:{0:ON, 1:OFF}]---END
193
- =end
194
- 121 => 'ControlSwitches',
195
-
196
- =begin
197
- |--[int:0(Constant)]---[int(Number)]---END
198
- |
199
- |--[int:1(Variable)]---[int(Variable ID)]---END
200
- |
201
- |--[int:2(Random)]---[int(Min)]---[int(Max)]---END
202
- |
203
- | |--[int:{0:Item, 1:Weapon, 2:Armor}]---[int(Corresponded Item ID)]---[int:0]---END
204
- | |
205
- [int(Variable Begin ID)]---[int(Variable End ID)]---[int:{0:Set, 1:Add, 2:Sub, 3:Mul, 4:Div, 5:Mod}]--| |--[int:3(Actor)]---[int:(Actor ID)]---[int:{0:Level, 1:EXP, 2:HP, 3:MP, 4:MHP, 5:MMP, 6:ATK, 7:DEF, 8:MAT, 9:MDF, 10:AGI, 11:LUK}]---END
206
- | |
207
- | |--[int:4(Enemy)]---[int:(Enemy ID)]---[int:{0:HP, 1:MP, 2:MHP, 3:MMP, 4:ATK, 5:DEF, 6:MAT, 7:MDF, 8:AGI, 9:LUK}]---END
208
- |--[int:3(Game Data)]--|
209
- | |--[int:5(Character)]---[int:{-1:Player, 0:This Event, 1:EV001, ...}]---[int:{0:Map X, 1:Map Y, 2:Direction, 3:Screen X ,4:Screen Y}]---END
210
- | |
211
- | |--[int:6(Party)]---[int:{0~7}(Member ID)]---[int:0]---END
212
- | |
213
- | |--[int:7(Other)]---[int:{0(Map ID), 1(Party Members), 2(Gold), 3(Steps), 4(Play Time), 5(Timer), 6(Save Count), 7(Battle Count)}]---[int:0]---END
214
- |
215
- |--[int:4(Scripts)]---[string]---END
216
- =end
217
- 122 => 'ControlVariables',
218
-
219
- =begin
220
- [string:{'A', 'B', 'C', 'D'}(Self Switch Name)]---[int:{0:ON, 1:OFF}]---END
221
- =end
222
- 123 => 'ControlSelfSwitch',
223
-
224
- =begin
225
- [int:{0:Start, 1:Stop}]---[int:{0~5999}(sec)]---END
226
- =end
227
- 124 => 'ControlTimer',
228
-
229
- =begin
230
- |--[int:0(Constant)]---[int:{0~9999999}(Number)]---END
231
- [int:{0:Increase, 1:Decrease}]--|
232
- |--[int:1(Variable)]---[int(Variable ID)]---END
233
- =end
234
- 125 => 'ChangeGold',
235
-
236
- =begin
237
- |--[int:0(Constant)]---[int:{0~9999999}(Number)]---END
238
- [int(Item ID)]---[int:{0:Increase, 1:Decrease}]--|
239
- |--[int:1(Variable)]---[int(Variable ID)]---END
240
- =end
241
- 126 => 'ChangeItems',
242
-
243
- =begin
244
- |--[int:0(Constant)]---[int:{0~9999999}(Number)]--|
245
- |--[int:0(Increase)]--| |--[bool:false(Include Equipment)]---END
246
- | |--[int:1(Variable)]---[int(Variable ID)]---------|
247
- [int(Weapon ID)]--|
248
- | |--[int:0(Constant)]---[int:{0~9999999}(Number)]--|
249
- |--[int:1(Decrease)]--| |--[bool(Include Equipment)]---END
250
- |--[int:1(Variable)]---[int(Variable ID)]---------|
251
- =end
252
- 127 => 'ChangeWeapons',
253
-
254
- =begin
255
- |--[int:0(Constant)]---[int:{0~9999999}(Number)]--|
256
- |--[int:0(Increase)]--| |--[bool:false(Include Equipment)]---END
257
- | |--[int:1(Variable)]---[int(Variable ID)]---------|
258
- [int(Armor ID)]--|
259
- | |--[int:0(Constant)]---[int:{0~9999999}(Number)]--|
260
- |--[int:1(Decrease)]--| |--[bool(Include Equipment)]---END
261
- |--[int:1(Variable)]---[int(Variable ID)]---------|
262
- =end
263
- 128 => 'ChangeArmor',
264
-
265
- =begin
266
- [int(Actor ID)]---[int:{0:Add, 1:Remove}]---[int:{0:NO, 1:YES}(Initialize)]---END
267
- =end
268
- 129 => 'ChangePartyMember',
269
-
270
- =begin
271
- [RPG::BGM]---END
272
- =end
273
- 132 => 'ChangeBattleBGM',
274
-
275
- =begin
276
- [RPG::ME]---END
277
- =end
278
- 133 => 'ChangeBattleEndME',
279
-
280
- =begin
281
- [int:{0:Disable, 1:Enable}]---END
282
- =end
283
- 134 => 'ChangeSaveAccess',
284
-
285
- =begin
286
- [int:{0:Disable, 1:Enable}]---END
287
- =end
288
- 135 => 'ChangeMenuAccess',
289
-
290
- =begin
291
- [int:{0:Disable, 1:Enable}]---END
292
- =end
293
- 136 => 'ChangeEncounter',
294
-
295
- =begin
296
- [int:{0:Disable, 1:Enable}]---END
297
- =end
298
- 137 => 'ChangeFormationAccess',
299
-
300
- =begin
301
- [RPG::Tone]---END
302
- =end
303
- 138 => 'ChangeWindowColor',
304
-
305
- =begin
306
- |--[int:0(Direct Designation)]---[int(Map ID)]---[int(Map X)]---[int(Map Y)]-------------------------------------------------------------------------------------|
307
- | |--[int:{0:Retain, 2:Down, 4:Left, 6:Right, 8:Up}(Direction)]---[int:{0:Normal, 1:White, 2:None}(Fade)]---END
308
- |--[int:1(Designation with Variables)]---[int(Map ID Corresponded Variable ID)]---[int(Map X Corresponded Variable ID)]---[int(Map Y Corresponded Variable ID)]--|
309
- =end
310
- 201 => 'TransferPlayer',
311
-
312
- =begin
313
- |--[int:0(Direct Designation)]---[int(Map ID)]---[int(Map X)]---[int(Map Y)]---END
314
- [int:{0:Boat, 1:Ship, 2:Airship}]--|
315
- |--[int:1(Designation with Variables)]---[int(Map ID Corresponded Variable ID)]---[int(Map X Corresponded Variable ID)]---[int(Map Y Corresponded Variable ID)]---END
316
- =end
317
- 202 => 'SetVehicleLocation',
318
-
319
- =begin
320
- |--[int:0(Direct Designation)]---[int(Map X)]---[int(Map Y)]------------------------------------------------------------|
321
- | |
322
- [int:{0:This Event, 1:EV001, ...}]----------|--[int:1(Designation with Variables)]---[int(Map X Corresponded Variable ID)]---[int(Map Y Corresponded Variable ID)]--|--[int:{0:Retain, 2:Down, 4:Left, 6:Right, 8:Up}(Direction)]---END
323
- | |
324
- |--[int:2(Exchange with Another Event)]---[int(Exchanged Event ID)]---[int:0]-------------------------------------------|
325
- =end
326
- 203 => 'SetEventLocation',
327
-
328
- =begin
329
- [int:{2:Down, 4:Left, 6:Right, 8:Up}]---[int:{0~100}(Distance)]---[int:{1:1/8 Speed, 2:1/4 Speed, 3:1/2 Speed, 4:Normal, 5:2 Speed, 6:4 Speed}]---END
330
- =end
331
- 204 => 'ScrollMap',
332
-
333
- =begin
334
- [int:{-1:Player, 0:This Event, 1:EV001, ...}]---[RPG::MoveRoute(45 is Script)]---END
335
- =end
336
- 205 => 'SetMoveRoute',
337
-
338
- =begin
339
- END
340
- =end
341
- 206 => 'GetSwitchVehicle',
342
-
343
- =begin
344
- [int:{0:ON, 1:OFF}]---END
345
- =end
346
- 211 => 'ChangeTransparency',
347
-
348
- =begin
349
- [int:{-1:Player, 0:This Event, 1:EV001, ...}]---[int(Animation ID)]---[bool(Wait for Completion)]---END
350
- =end
351
- 212 => 'ShowAnimation',
352
-
353
- =begin
354
- [int:{-1:Player, 0:This Event, 1:EV001, ...}]---[int(Ballon Icon ID)]---[bool(Wait for Completion)]---END
355
- =end
356
- 213 => 'ShowBalloonIcon',
357
- 214 => 'EraseEvent',
358
-
359
- =begin
360
- [int:{0:ON, 1:OFF}]---END
361
- =end
362
- 216 => 'ChangePlayerFollowers',
363
- 217 => 'GatherFollowers',
364
- 221 => 'FadeoutScreen',
365
- 222 => 'FadeinScreen',
366
-
367
- =begin
368
- [RPG::Tone]---[int:{0~600}(Time 1/60 sec)]---[bool(Wait for Completion)]---END
369
- =end
370
- 223 => 'TintScreen',
371
-
372
- =begin
373
- [RPG::Color]---[int:{0~600}(Time 1/60 sec)]---[bool(Wait for Completion)]---END
374
- =end
375
- 224 => 'FlashScreen',
376
-
377
- =begin
378
- [int:{1~9}(Power)]---[int:{1~9}(Speed)]---[int:{0~600}(Time 1/60 sec)]---[bool(Wait for Completion)]---END
379
- =end
380
- 225 => 'ShakeScreen',
381
-
382
- =begin
383
- [int:{0~999}(Time 1/60 sec)]---END
384
- =end
385
- 230 => 'Wait',
386
-
387
- =begin
388
- |--[int:0(Constant)]---[int:{-9999~9999}(Map X)]---[int:{-9999~9999}(Map Y)]--------------------------|
389
- [int:{1~100}(Number)]---[string(Picture Graphic Name)]---[int:{0:Upper Left, 1:Center}(Origin)]--| |--[int:{0~2000}(Width %)]---[int:{0~2000}(Height %)]---[int:{0~255}(Opacity)]---[int:{0:Normal, 1:Add, 2:Sub}]---END
390
- |--[int:1(Variable)]---[int(Map X Corresponded Variable ID)]---[int(Map Y Corresponded Variable ID)]--|
391
- =end
392
- 231 => 'ShowPicture',
393
-
394
- =begin
395
- |--[int:0(Constant)]---[int:{-9999~9999}(Map X)]---[int:{-9999~9999}(Map Y)]--------------------------|
396
- [int:{1~100}(Number)]---[int:{0:Upper Left, 1:Center}(Origin)]--| |--[int:{0~2000}(Width %)]---[int:{0~2000}(Height %)]---[int:{0~255}(Opacity)]---[int:{0:Normal, 1:Add, 2:Sub}]---[int:{0~600}(Time 1/60 sec)]---[bool(Wait for Completion)]---END
397
- |--[int:1(Variable)]---[int(Map X Corresponded Variable ID)]---[int(Map Y Corresponded Variable ID)]--|
398
- =end
399
- 232 => 'MovePicture',
400
-
401
- =begin
402
- [int:{1~100}(Number)]-[int:{-90~90}(Speed)]--END
403
- =end
404
- 233 => 'RotatePicture',
405
-
406
- =begin
407
- [int:{1~100}(Number)]---[RPG::Tone]---[int:{0~600}(Time 1/60 sec)]---[bool(Wait for Completion)]---END
408
- =end
409
- 234 => 'TintPicture',
410
-
411
- =begin
412
- [int:{1~100}(Number)]---END
413
- =end
414
- 235 => 'ErasePicture',
415
-
416
- =begin
417
- [string:{":none", ":rain", ":storm", ":snow"}]---[int:{0~9}(Power)]---[int:{0~600}(Time 1/60 sec)]---[bool(Wait for Completion)]---END
418
- =end
419
- 236 => 'SetWeatherEffects',
420
-
421
- =begin
422
- [RPG::BGM]---END
423
- =end
424
- 241 => 'PlayBGM',
425
-
426
- =begin
427
- [int:{1~60}(Time sec)]---END
428
- =end
429
- 242 => 'FadeoutBGM',
430
- 243 => 'SaveBGM',
431
- 244 => 'ReplayBGM',
432
-
433
- =begin
434
- [RPG::BGM]---END
435
- =end
436
- 245 => 'PlayBGS',
437
-
438
- =begin
439
- [int:{1~60}(Time sec)]---END
440
- =end
441
- 246 => 'FadeoutBGS',
442
-
443
- =begin
444
- [RPG::ME]---END
445
- =end
446
- 249 => 'PlayME',
447
-
448
- =begin
449
- [RPG::SE]---END
450
- =end
451
- 250 => 'PlaySE',
452
- 251 => 'StopSE',
453
-
454
- =begin
455
- [string(Movie Name)]---END
456
- =end
457
- 261 => 'PlayMovie',
458
-
459
- =begin
460
- [int:{0:ON, 1:OFF}]---END
461
- =end
462
- 281 => 'ChangeMapNameDisplay',
463
-
464
- =begin
465
- [int(Tileset ID)]---END
466
- =end
467
- 282 => 'ChangeTileset',
468
-
469
- =begin
470
- [string(Floor Picture)]---[string(Wall Picture)]---END
471
- =end
472
- 283 => 'ChangeBattleBack',
473
-
474
- =begin
475
- [string(Distant view Picture)]---[bool(Loop Horizontal)]---[bool(Loop Vertical)]---[int(-32~32)(Horizontal Scroll)]---[int(-32~32)(Vertical Scrool)]---END
476
- =end
477
- 284 => 'ChangeParallaxBack',
478
-
479
- =begin
480
- |--[int:0(Direct Designation)]---[int(Map X)]---[int(Map Y)]---END
481
- [int(Variable for Info)]---[int:{0:Terrain, 1:Event ID, 2:Tile ID(Layer 1), 3:Tile ID(Layer 2), 4:Tile ID(Layer 3), 5:Region ID}(Info Type)]--|
482
- |--[int:1(Designation with Variables)]---[int(Map X Corresponded Variable ID)]---[int(Map Y Corresponded Variable ID)]---END
483
- =end
484
- 285 => 'GetLocationInfo',
485
-
486
- =begin
487
- |--[int:0(Direct Designation)]---[int(Enemy ID)]-----------------------------------|
488
- | |--[bool(Can Escape)]---[bool(Continue Even When Loser)]---END
489
- |--[int:1(Designation with Variables)]---[int(Enemy ID Corresponded Variable ID)]--|
490
- =end
491
- 301 => 'BattleProcessing',
492
-
493
- =begin
494
- |--[int:0(Price: Standard)]---[int:0]------------------|
495
- [int:{0:Item, 1:Weapon, 2:Armor}]---[int(Corresponded Item ID)]--| |--[bool(Purchase Only)]---END
496
- |--[int:1(Price: Specify)]---[int:{0~9999999}(Price)]--|
497
- =end
498
- 302 => 'ShopProcessing',
499
-
500
- =begin
501
- [int(Actor ID)]---[int:{1~16}(Max Characters)]---END
502
- =end
503
- 303 => 'NameInputProcessing',
504
-
505
- =begin
506
- |--[int:0(Constant)]---[int:{1~9999}(Number)]--|
507
- |--[int:0(Increase)]--| |--[bool:false(Allow Knockout)]---END
508
- | |--[int:1(Variable)]---[int(Variable ID)]------|
509
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--|
510
- | | |--[int:0(Constant)]---[int:{1~9999}(Number)]--|
511
- | |--[int:1(Decrease)]--| |--[bool(Allow Knockout)]---END
512
- | |--[int:1(Variable)]---[int(Variable ID)]------|
513
- |
514
- | |--[int:0(Constant)]---[int:{1~9999}(Number)]--|
515
- | |--[int:0(Increase)]--| |--[bool:false(Allow Knockout)]---END
516
- | | |--[int:1(Variable)]---[int(Variable ID)]------|
517
- |--[int:1(Variable)]---[int(Variable ID)]-----------------------|
518
- | |--[int:0(Constant)]---[int:{1~9999}(Number)]--|
519
- |--[int:1(Decrease)]--| |--[bool(Allow Knockout)]---END
520
- |--[int:1(Variable)]---[int(Variable ID)]------|
521
- =end
522
- 311 => 'ChangeHP',
523
-
524
- =begin
525
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--| |--[int:0(Constant)]---[int:{1~9999}(Number)]---END
526
- | |--[int:{0:Increase, 1:Decrease}]--|
527
- |--[int:1(Variable)]---[int(Variable ID)]---------------------| |--[int:1(Variable)]---[int(Variable ID)]---END
528
- =end
529
- 312 => 'ChangeMP',
530
-
531
- =begin
532
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--|
533
- | |--[int:{0:Add, 1:Remove}]---[int(State ID)]---END
534
- |--[int:1(Variable)]---[int(Variable ID)]---------------------|
535
- =end
536
- 313 => 'ChangeState',
537
-
538
- =begin
539
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]---END
540
- |
541
- |--[int:1(Variable)]---[int(Variable ID)]---END
542
- =end
543
- 314 => 'RecoverAll',
544
-
545
- =begin
546
- |--[int:0(:Constant)]---[int:{1~9999999}(Number)]--|
547
- |--[int:0(Increase)]--| |--[bool(Show Level Up Message)]---END
548
- | |--[int:1(Variable)]---[int(Variable ID)]----------|
549
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--|
550
- | | |--[int:0(Constant)]---[int:{1~9999999}(Number)]--|
551
- | |--[int:1(Decrease)]--| |--[bool:false(Show Level Up Message)]---END
552
- | |--[int:1(Variable)]------------------------------|
553
- |
554
- | |--[int:0(Constant)]---[int:{1~9999999}(Number)]--|
555
- | |--[int:0(Increase)]--| |--[bool(Show Level Up Message)]---END
556
- | | |--[int:1(Variable)]---[int(Variable ID)]---------|
557
- |--[int:1(Variable)]---[int(Variable ID)]---------------------|
558
- | |--[int:0(Constant)]---[int:{1~9999999}(Number)]--|
559
- |--[int:1(Decrease)]--| |--[bool:false(Show Level Up Message)]---END
560
- |--[int:1(Variable)]---[int(Variable ID)]---------|
561
- =end
562
- 315 => 'ChangeEXP',
563
-
564
- =begin
565
- |--[int:0(Constant)]---[int:{1~98}(Number)]--|
566
- |--[int:0(Increase)]--| |--[bool(Show Level Up Message)]---END
567
- | |--[int:1(Variable)]---[int(Variable ID)]----|
568
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--|
569
- | | |--[int:0(Constant)]---[int:{1~98}(Number)]--|
570
- | |--[int:1(Decrease)]--| |--[bool:false(Show Level Up Message)]---END
571
- | |--[int:1(Variable)]---[int(Variable ID)]----|
572
- |
573
- | |--[int:0(Constant)]---[int:{1~98}(Number)]--|
574
- | |--[int:0(Increase)]--| |--[bool(Show Level Up Message)]---END
575
- | | |--[int:1(Variable)]---[int(Variable ID)]----|
576
- |--[int:1(Variable)]---[int(Variable ID)]---------------------|
577
- | |--[int:0(Constant)]---[int:{1~98}(Number)]--|
578
- |--[int:1(Decrease)]--| |--[bool:false(Show Level Up Message)]---END
579
- |--[int:1(Variable)]---[int(Variable ID)]------|
580
- =end
581
- 316 => 'ChangeLevel',
582
-
583
- =begin
584
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--| |--[int:0(Constant)]---[int:{0~9999999}(Number)]---END
585
- | |--[int:{0:MHP, 1:MMP, 2:ATK, 3:DEF, 4:MAT, 5:MDF, 6:AGI, 7:LUK}]---[int:{0:Increase, 1:Decrease}]--|
586
- |--[int:1(Variable)]---[int(Variable ID)]---------------------| |--[int:1(Variable)]---[int(Variable ID)]---END
587
- =end
588
- 317 => 'ChangeParameters',
589
-
590
- =begin
591
- |--[int:0(Fixed)]---[int:{0:Entire Party, 1:Actor 001, ...}]--|
592
- | |--[int:{0:Learn, 1:Forget}]---[int(Skill ID)]---END
593
- |--[int:1(Variable)]---[int(Variable ID)]---------------------|
594
- =end
595
- 318 => 'ChangeSkills',
596
-
597
- =begin
598
- [int(Actor ID)]---[int:{0:Weapon, 1:Shield, 2:Head, 3:Boby, 4:Accessory}]---[int:{0:None, 1:Equipment 001, ...}(Equipment ID)]---END
599
- =end
600
- 319 => 'ChangeEquipment',
601
-
602
- =begin
603
- [int(Actor ID)]---[string(New Actor Name)]---END
604
- =end
605
- 320 => 'ChangeActorName',
606
-
607
- =begin
608
- [int(Actor ID)]---[int(New Class ID)]---END
609
- =end
610
- 321 => 'ChangeActorClass',
611
-
612
- =begin
613
- [int(Actor ID)]---[string(New Actor walking Picture Name)]---[int(New Actor Walking Picture Index)]---[string(New Actor Portrait Picture Name)]---[int(New Actor Portrait Picture Index)]---END
614
- =end
615
- 322 => 'ChangeActorGraphic',
616
-
617
- =begin
618
- [int:{0:Boat, 1:Ship, 2:Airship}]---[string(New Vehicle Picture Name)]---[int(New Vehicle Picture Index)]---END
619
- =end
620
- 323 => 'ChangeVehicleGraphic',
621
-
622
- =begin
623
- [int(Actor ID)]---[string(New Actor Nickname)]---END
624
- =end
625
- 324 => 'ChangeActorNickname',
626
-
627
- =begin
628
- |--[int:0(Constant)]---[int:{1~999999}(Number)]--|
629
- |--[int:0(Increase)]--| |--[bool:false(Allow Knockout)]---END
630
- | |--[int:1(Variable)]---[int(Variable ID)]--------|
631
- [int:{-1:Entire Troop, 0:Troop 001, ...}]--|
632
- | |--[int:0(Constant)]---[int:{1~999999}(Number)]--|
633
- |--[int:1(Decrease)]--| |--[bool(Allow Knockout)]---END
634
- |--[int:1(Variable)]---[int(Variable ID)]--------|
635
-
636
- =end
637
- 331 => 'ChangeEnemyHP',
638
-
639
- =begin
640
- |--[int:0(Constant)]---[int:{1~9999}(Number)]---END
641
- [int:{-1:Entire Troop, 0:Troop 001, ...}]---[int:{0:Increase, 1:Decrease}]--|
642
- |--[int:1(Variable)]---[int(Variable ID)]---END
643
- =end
644
- 332 => 'ChangeEnemyMP',
645
-
646
- =begin
647
- [int:{-1:Entire Troop, 0:Troop 001, ...}]---[int:{0:Add, 1:Remove}]---[int(State ID)]---END
648
- =end
649
- 333 => 'ChangeEnemyState',
650
-
651
- =begin
652
- [int:{-1:Entire Troop, 0:Troop 001, ...}]---END
653
- =end
654
- 334 => 'EnemyRecoverAll',
655
-
656
- =begin
657
- [int:{-1:Entire Troop, 0:Troop 001, ...}]---END
658
- =end
659
- 335 => 'EnemyAppear',
660
-
661
- =begin
662
- [int(Troop ID)]---[int(Enemy ID)]---END
663
- =end
664
- 336 => 'EnemyTransform',
665
-
666
- =begin
667
- [int:{-1:Entire Troop, 0:Troop 001, ...}]---[int(Animation ID)]---END
668
- =end
669
- 337 => 'ShowBattleAnimation',
670
-
671
- =begin
672
- |--[int:0(Enemy)]---[int(Troop ID)]--|
673
- | |--[int(Skill ID)]---[int:{-2:Last Target, -1:Random, 0:Index 1,...}]---END
674
- |--[int:1(Actor)]---[int(Actor ID)]--|
675
- =end
676
- 339 => 'ForceAction',
677
- 340 => 'AbortBattle',
678
- 351 => 'OpenMenuScreen',
679
- 352 => 'OpenSaveScreen',
680
- 353 => 'GameOver',
681
- 354 => 'ReturnToTitleScreen',
682
-
683
- =begin
684
- [string]---END
685
- =end
686
- 355 => 'Script',
687
-
688
- =begin
689
- [string]---END
690
- =end
691
- 401 => 'ShowText',
692
-
693
- =begin
694
- [int(Choice Index)]---[string(Choice Name)]---END
695
- =end
696
- 402 => 'When',
697
- 403 => 'WhenCancel',
698
- 404 => 'ChoicesEnd',
699
-
700
- =begin
701
- [string]---END
702
- =end
703
- 405 => 'ShowScrollingText',
704
-
705
- =begin
706
- [string]---END
707
- =end
708
- 408 => 'CommentMore',
709
- 411 => 'Else',
710
- 412 => 'BranchEnd',
711
- 413 => 'RepeatAbove',
712
-
713
- =begin
714
- [RPG::MoveCommand(45 is script)]---END
715
- =end
716
- 505 => 'MoveRoute',
717
- 601 => 'IfWin',
718
- 602 => 'IfEscape',
719
- 603 => 'IfLose',
720
- 604 => 'BattleProcessingEnd',
721
-
722
- =begin
723
- |--[int:0(Price: Standard)]---[int:0]---END
724
- [int:{0:Item, 1:Weapon, 2:Armor}]---[int(Corresponded Item ID)]--|
725
- |--[int:1(Price: Specify)]---[int:{0~9999999}(Price)]---END
726
- =end
727
- 605 => 'ShopItem',
728
-
729
- =begin
730
- [string]---END
731
- =end
732
- 655 => 'ScriptMore' }
733
-
734
- # 红色
735
- RED_COLOR = "\e[31m"
736
-
737
- # 绿色
738
- GREEN_COLOR = "\e[32m"
739
-
740
- # 黄色
741
- YELLOW_COLOR = "\e[33m"
742
-
743
- # 蓝色
744
- BLUE_COLOR = "\e[34m"
745
-
746
- # 紫色
747
- MAGENTA_COLOR = "\e[35m"
748
-
749
- # 青色
750
- CYAN_COLOR = "\e[36m"
751
-
752
- # 重置颜色
753
- RESET_COLOR = "\e[0m"
754
-
755
- # 清除行
756
- ESCAPE = "\e[2K"
757
-
758
- # 根据 file_basename 检查 object 的类型在 module_name 中是否正确
759
- #
760
- # @param object [Object] 待检查的对象
761
- # @param file_basename [String] 文件名(不包含扩展名)
762
- # @param is_compact [Boolean] 是否为紧凑模式
763
- # @param module_name [Symbol] 模块名
764
- #
765
- # @raise [RPGTypeError] object 的类型不在 RPG 模块中
766
- # @raise [R3EXSTypeError] object 的类型不在 R3EXS 模块中
767
- # @raise [ModuleNameError] module_name 不是 :RPG 或 :R3EXS
768
- # @raise [FileBaseNameError] file_basename 无法匹配到对应的类
769
- #
770
- # @return [void]
771
- def Utils.check_type(object, file_basename, is_compact, module_name)
772
- case module_name
773
- when :RPG
774
- matched_class = FILE_BASENAME_TO_CLASS_RPG.find { |pattern, _| file_basename =~ pattern }&.last
775
- matched_class or raise FileBaseNameError.new(file_basename), "Invalid file basename: #{file_basename}"
776
- if object.is_a?(Array)
777
- if is_compact
778
- object.compact.all? { |item| item.is_a?(matched_class) } or raise RPGTypeError.new(object), "Invalid Object: #{object}, it's not an Array<#{matched_class}>"
779
- else
780
- object.all? { |item| item.is_a?(matched_class) } or raise RPGTypeError.new(object), "Invalid Object: #{object}, it's not an Array<#{matched_class}>"
781
- end
782
- elsif object.is_a?(Hash)
783
- if is_compact
784
- object.compact.values.all? { |item| item.is_a?(matched_class) } or raise RPGTypeError.new(object), "Invalid Object: #{object}, it's not a Hash<#{matched_class}>"
785
- else
786
- object.values.all? { |item| item.is_a?(matched_class) } or raise RPGTypeError.new(object), "Invalid Object: #{object}, it's not a Hash<#{matched_class}>"
787
- end
788
- else
789
- object.is_a?(matched_class) or raise RPGTypeError.new(object), "Invalid Object: #{object}, it's not a #{matched_class}"
790
- end
791
- when :R3EXS
792
- matched_class = FILE_BASENAME_TO_CLASS_R3EXS.find { |pattern, _| file_basename =~ pattern }&.last
793
- matched_class or raise FileBaseNameError.new(file_basename), "Invalid file basename: #{file_basename}"
794
- if object.is_a?(Array)
795
- if is_compact
796
- object.compact.all? { |item| item.is_a?(matched_class) } or raise R3EXSTypeError.new(object), "Invalid object: #{object}, it's not an Array<#{matched_class}>"
797
- else
798
- object.all? { |item| item.is_a?(matched_class) } or raise R3EXSTypeError.new(object), "Invalid object: #{object}, it's not an Array<#{matched_class}>"
799
- end
800
- elsif object.is_a?(Hash)
801
- if is_compact
802
- object.compact.values.all? { |item| item.is_a?(matched_class) } or raise R3EXSTypeError.new(object), "Invalid object: #{object}, it's not a Hash<#{matched_class}>"
803
- else
804
- object.values.all? { |item| item.is_a?(matched_class) } or raise R3EXSTypeError.new(object), "Invalid object: #{object}, it's not a Hash<#{matched_class}>"
805
- end
806
- else
807
- object.is_a?(matched_class) or raise R3EXSTypeError.new(object), "Invalid object: #{object}, it's not #{matched_class}"
808
- end
809
- else
810
- raise ModuleNameError.new(module_name), "Invalid module name: #{module_name}"
811
- end
812
-
86
+ # 读取 target_dir 下的所有常规 JSON 文件,将其反序列化为对象,并调用 block
87
+ #
88
+ # @note 注意传入 block 的 obj
89
+ # - 在 module_name 为 RPG 时,如果 obj 是数组或哈希,则其中可能存在 nil 元素。如果 obj 是单独一个对象,则不可能为 nil
90
+ # - module_name 为 R3EXS 时,object 不会为 nil
91
+ #
92
+ # @param target_dir [Pathname] 目标目录
93
+ # @param module_name [Symbol] 模块名
94
+ #
95
+ # @yieldparam obj [Object] JSON 文件反序列化后的对象
96
+ # @yieldparam file_path [Pathname] 文件路径
97
+ # @yieldreturn [void]
98
+ #
99
+ # @raise [JsonDirError] target_dir 不存在
100
+ # @raise [RPGJsonFileError] json 文件不是 RPG 模块中的对象
101
+ # @raise [R3EXSJsonFileError] json 文件不是 R3EXS 模块中的对象
102
+ # @raise [ModuleNameError] module_name 不是 :RPG 或 :R3EXS
103
+ #
104
+ # @return [void]
105
+ def self.all_regular_json_files(target_dir, module_name)
106
+ # 检查 target_dir 目录是否存在
107
+ target_dir.exist? && target_dir.directory? or raise JsonDirError, "JSON directory not found: #{target_dir}"
108
+
109
+ # 递归获取 target_dir 下的所有 *.json 文件
110
+ target_dir.glob('**/*.json').each do |file_path|
111
+ file_basename = file_path.basename('.json')
112
+ # 获取文件名所对应的类,若无法匹配则跳过该文件
113
+ klass = name_class(file_basename, module_name)
114
+ next if klass.nil?
115
+
116
+ obj = Oj.load_file(file_path.to_s)
117
+ Logger.debug("Deserialize #{file_path}")
118
+ case module_name
119
+ when :RPG
120
+ # 因为这是从 rvdata2 文件直接全部序列化后的 JSON 文件中读取的 object,其中可能存在 nil 元素
121
+ begin
122
+ check_type(obj, klass, true)
123
+ rescue TypeError
124
+ raise RPGJsonFileError, "Invalid RPG JSON file: #{file_path}"
125
+ end
126
+ when :R3EXS
127
+ # 因为这是从 R3EXS 模块的类序列化后的 JSON 文件中读取的 object,程序设计中不应该存在 nil 元素
128
+ begin
129
+ check_type(obj, klass, false)
130
+ rescue TypeError
131
+ raise R3EXSJsonFileError, "Invalid R3EXS JSON file: #{file_path}"
132
+ end
133
+ else
134
+ raise ModuleNameError, "Invalid module name: #{module_name}"
813
135
  end
136
+ yield obj, file_path
137
+ end
138
+ end
814
139
 
815
- # RPG 中的对象转化为 R3EXS 对象
816
- #
817
- # @param object [Object] 待转化的 RPG 对象
818
- # @param file_basename [String] 文件名(不包含扩展名)
819
- # @param with_notes [Boolean] 是否包含注释
820
- #
821
- # @raise [RPGTypeError] object 的类型不在 RPG 模块中
822
- # @raise [FileBaseNameError] file_basename 无法匹配到对应的类
823
- #
824
- # @return [Object]
825
- def Utils.rpg_r3exs(object, file_basename, with_notes)
826
- check_type(object, file_basename, true, :RPG)
827
-
828
- # 首先根据 file_basename 找到对应的类
829
- matched_class = Utils::FILE_BASENAME_TO_CLASS_R3EXS.find { |pattern, _| file_basename =~ pattern }.last
830
-
831
- # 然后根据 object 的类型进行处理
832
- # 如果 object 是数组,则遍历数组,对每个元素进行处理
833
- # 如果 object 是哈希,则遍历哈希,对每个值进行处理
834
- # 如果 object 是其他类型,则直接处理
835
- if object.is_a?(Array)
836
- temp = []
837
- object.each_with_index do |obj, index|
838
- next if obj.nil?
839
- obj_r3exs = matched_class.new(obj, index, with_notes)
840
- temp << obj_r3exs unless obj_r3exs.empty?
841
- end
842
- elsif object.is_a?(Hash) # 只有 RPG::MapInfo Hash,且 key 为整数
843
- temp = []
844
- object.each do |key, obj|
845
- next if obj.nil?
846
- temp << matched_class.new(obj, key, with_notes)
847
- end
848
- else
849
- # 只有 RPG::Map RPG::System 是单独一个对象,且不可能为 nil
850
- temp = matched_class.new(object, with_notes)
851
- end
852
- temp
140
+ # 读取 target_dir 下的所有 CommonEvent JSON 文件,将其反序列化为对象数组,并调用 block
141
+ #
142
+ # @note 注意传入 block obj
143
+ # - module_name RPG 时,object 可能存在 nil 元素
144
+ # - module_name R3EXS 时,object 不可能存在 nil 元素
145
+ #
146
+ # @param target_dir [Pathname] 目标目录
147
+ # @param module_name [Symbol] 模块名
148
+ #
149
+ # @yieldparam commonevents [Array<RPG::CommonEvent, R3EXS::CommonEvent>] CommonEvent JSON 文件反序列化后的数组
150
+ # @yieldparam commonevents_paths [Array<Pathname>] CommonEvent JSON 文件路径数组
151
+ # @yieldparam rvdata2_file_path [Pathname] 所属 CommonEvents.rvdata2 的路径
152
+ # @yieldreturn [void]
153
+ #
154
+ # @raise [JsonDirError] target_dir 不存在
155
+ # @raise [RPGJsonFileError] json 文件不是 RPG 模块中的对象
156
+ # @raise [R3EXSJsonFileError] json 文件不是 R3EXS 模块中的对象
157
+ # @raise [ModuleNameError] module_name 不是 :RPG 或 :R3EXS
158
+ #
159
+ # @return [void]
160
+ def self.all_commonevent_json_files(target_dir, module_name)
161
+ # 检查 target_dir 目录是否存在
162
+ target_dir.exist? && target_dir.directory? or raise JsonDirError, "JSON directory not found: #{target_dir}"
163
+
164
+ # 用两个Hash来存储每一个父目录下的所有的 CommonEvent_\d{5}.json 文件反序列化后的对象数组以及路径数组
165
+ # Hash 的键是父目录的路径,值是一个数组,存储该目录下的所有 CommonEvent_\d{5}.json 文件的反序列化后的对象数组以及其路径数组
166
+ commonevents_hash = Hash.new { |h, k| h[k] = [] }
167
+ commonevents_path_hash = Hash.new { |h, k| h[k] = [] }
168
+
169
+ # 递归获取 target_dir 下的所有 CommonEvent_\d{5}.json 文件
170
+ target_dir.glob('**/CommonEvent_[0-9][0-9][0-9][0-9][0-9].json').each do |file_path|
171
+ obj = Oj.load_file(file_path.to_s)
172
+ case module_name
173
+ when :RPG
174
+ # 因为这是从 rvdata2 文件直接全部序列化后的 JSON 文件中读取的 object,其中可能存在 nil 元素
175
+ begin
176
+ check_type(obj, RPG::CommonEvent, true)
177
+ rescue TypeError
178
+ raise RPGJsonFileError, 'Invalid RPG CommonEvents JSON file'
179
+ end
180
+ when :R3EXS
181
+ # 因为这是从 R3EXS 模块的类序列化后的 JSON 文件中读取的 object,程序设计中不应该存在 nil 元素
182
+ begin
183
+ check_type(obj, R3EXS::CommonEvent, false)
184
+ rescue TypeError
185
+ raise R3EXSJsonFileError, 'Invalid R3EXS CommonEvents JSON file'
186
+ end
187
+ else
188
+ raise ModuleNameError, "Invalid module name: #{module_name}"
853
189
  end
190
+ Logger.debug("Deserialize #{file_path}")
191
+ parent_dir = file_path.parent
192
+ commonevents_hash[parent_dir] << obj
193
+ commonevents_path_hash[parent_dir] << file_path
194
+ end
195
+
196
+ # 遍历每一个父目录的路径
197
+ commonevents_hash.each_key do |parent_dir|
198
+ commonevents = commonevents_hash[parent_dir]
199
+ commonevents_paths = commonevents_path_hash[parent_dir]
200
+ yield commonevents, commonevents_paths, parent_dir.parent.join('CommonEvents.rvdata2')
201
+ end
202
+ end
854
203
 
855
- # 读取 target_dir 下的所有 rvdata2 文件,将其反序列化为对象,并调用 block
856
- #
857
- # @note 注意传入 block 的 object
858
- # - 如果 object 是数组或哈希,则其中可能存在 nil 元素
859
- # - 如果 object 是单独一个对象,则不可能为 nil
860
- #
861
- # @param target_dir [Pathname] 目标目录
862
- #
863
- # @yieldparam object [Object] rvdata2 文件反序列化后的对象
864
- # @yieldparam file_basename [String] 文件名(不包含扩展名)
865
- # @yieldparam parent_relative_dir [Pathname] 文件所在目录的相对路径
866
- # @yieldreturn [void]
867
- #
868
- # @raise [Rvdata2FileError] rvdata2 文件可能损坏
869
- # @raise [Rvdata2DirError] target_dir 不存在
870
- #
871
- # @return [void]
872
- def Utils.all_rvdata2_files(target_dir)
873
- # 检查 target_dir 目录是否存在
874
- target_dir.exist? && target_dir.directory? or raise Rvdata2DirError.new(target_dir.to_s), "rvdata2 directory not found: #{target_dir}"
875
- # 递归获取 target_dir 下的所有 *.rvdata2 文件
876
- target_dir.glob('**/*.rvdata2').each do |file_path|
877
- file_basename = file_path.basename('.rvdata2').to_s
878
- # 检查文件名是否在 RVDATA2_FILE_NAME 中与其正则表达式匹配
879
- next unless RVDATA2_FILE_NAME.any? { |pattern| file_basename =~ pattern }
880
-
881
- print "#{ESCAPE}#{BLUE_COLOR}Reading and Deserializing from #{RESET_COLOR}#{file_path}...\r" if $global_options[:verbose]
882
- object = Marshal.load(file_path.binread)
204
+ # 读取 target_dir 下的所有 Ruby 源码文件,并调用 block
205
+ #
206
+ # @param target_dir [Pathname] 目标目录
207
+ #
208
+ # @yieldparam scripts [Array<String>] Ruby 源码文件数组
209
+ # @yieldparam scripts_info [Array<Hash>] Scripts_info.json 反序列化后的对象
210
+ # @yieldparam scripts_paths [Array<Pathname>] Ruby 源码文件名路径数组
211
+ # @yieldparam script_info_file_path [Pathname] Scripts_info.json 文件路径
212
+ # @yieldparam rvdata2_file_path [Pathname] 所属 Scripts.rvdata2 的路径
213
+ # @yieldreturn [void]
214
+ #
215
+ # @raise [JsonDirError] target_dir 不存在
216
+ # @raise [ScriptsInfoPathError] Scripts_info.json 不存在
217
+ #
218
+ # @return [void]
219
+ def self.all_rb_files(target_dir)
220
+ # 检查 target_dir 目录是否存在
221
+ target_dir.exist? && target_dir.directory? or raise JsonDirError, "JSON directory not found: #{target_dir}"
222
+
223
+ # 用两个Hash来存储每一个父目录下的所有的 Script_\d{3}.rb 文件反序列化后的对象数组以及路径数组
224
+ # Hash 的键是父目录的路径,值是一个数组,存储该目录下的所有 Script_\d{3}.rb 文件的反序列化后的对象数组以及其路径数组
225
+ scripts_hash = Hash.new { |h, k| h[k] = [] }
226
+ scripts_path_hash = Hash.new { |h, k| h[k] = [] }
227
+
228
+ # 递归获取 target_dir 下的所有 Script_\d{3}.rb 文件
229
+ target_dir.glob('**/Script_[0-9][0-9][0-9].rb').each do |file_path|
230
+ obj = file_path.read
231
+ Logger.debug("Read #{file_path}")
232
+ parent_dir = file_path.parent
233
+ scripts_hash[parent_dir] << obj
234
+ scripts_path_hash[parent_dir] << file_path
235
+ end
236
+
237
+ # 遍历每一个父目录的路径
238
+ scripts_hash.each_key do |parent_dir|
239
+ script_info_file_path = parent_dir.join('Scripts_info.json')
240
+ script_info_file_path.exist? or raise ScriptsInfoPathError, "Scripts_info.json not found: #{script_info_file_path}"
241
+
242
+ scripts = scripts_hash[parent_dir]
243
+ script_info = Oj.load_file(script_info_file_path.to_s)
244
+ scripts_paths = scripts_path_hash[parent_dir]
245
+ yield scripts, script_info, scripts_paths, script_info_file_path, parent_dir.parent.join('Scripts.rvdata2')
246
+ end
247
+ end
883
248
 
884
- # 如果文件名不是 'Scripts',则检查 object 的类型是否正确
885
- unless file_basename == 'Scripts'
886
- # 检查 object 的类型是否正确
887
- # 这里的类型检查要用紧凑模式,因为 rvdata2 文件中可能存在 nil 元素,必须忽略
888
- begin
889
- check_type(object, file_basename, true, :RPG)
890
- rescue RPGTypeError
891
- raise Rvdata2FileError.new(file_path.to_s), "Invalid rvdata2 file: #{file_path}"
892
- end
893
- end
249
+ # 检查 obj 的类型是否正确属于 klass
250
+ #
251
+ # @param obj [Object] 待检查的对象
252
+ # @param klass [::Class] 期望的类
253
+ # @param has_nil [Boolean] 是否允许 obj 中存在 nil 元素
254
+ #
255
+ # @raise [TypeError] obj 的类型不属于期望的类
256
+ #
257
+ # @return [void]
258
+ def self.check_type(obj, klass, has_nil)
259
+ if obj.is_a?(Array)
260
+ items = has_nil ? obj.compact : obj
261
+ items.all? { |item| item.is_a?(klass) } or raise TypeError, "Object isn't an Array<#{klass}>"
262
+ elsif obj.is_a?(Hash)
263
+ values = has_nil ? obj.compact.values : obj.values
264
+ values.all? { |item| item.is_a?(klass) } or raise TypeError, "Object isn't a Hash<#{klass}>"
265
+ elsif !obj.nil? # 需考虑 obj 为 nil的情况
266
+ obj.is_a?(klass) or raise TypeError, "Object isn't a #{klass}"
267
+ end
268
+ end
894
269
 
895
- yield object, file_basename, file_path.dirname.relative_path_from(target_dir)
896
- end
270
+ # 根据 file_basename 和 module_name 获取对应的类
271
+ #
272
+ # @param file_basename [Pathname] 文件名(不包含扩展名)
273
+ # @param module_name [Symbol] 模块名
274
+ #
275
+ # @raise [ModuleNameError] module_name 不是 :RPG 或 :R3EXS
276
+ #
277
+ # @return [::Class, nil] 若 file_basename 无法匹配则返回 nil
278
+ def self.name_class(file_basename, module_name)
279
+ table =
280
+ case module_name
281
+ when :RPG
282
+ FILE_BASENAME_TO_CLASS_RPG
283
+ when :R3EXS
284
+ FILE_BASENAME_TO_CLASS_R3EXS
285
+ else
286
+ raise ModuleNameError, "Invalid module name: #{module_name}"
897
287
  end
288
+ table.find { |pattern, _| file_basename.to_s =~ pattern }&.last
289
+ end
898
290
 
899
- # 读取 target_dir 下的所有常规 JSON 文件,将其反序列化为对象,并调用 block
900
- #
901
- # @note 注意传入 block object
902
- # - 在 module_name 为 RPG 时,如果 object 是数组或哈希,则其中可能存在 nil 元素。如果 object 是单独一个对象,则不可能为 nil
903
- # - module_name 为 R3EXS 时,object 不会为 nil
904
- #
905
- # @param target_dir [Pathname] 目标目录
906
- # @param module_name [Symbol] 模块名
907
- #
908
- # @yieldparam object [Object] JSON 文件反序列化后的对象
909
- # @yieldparam file_basename [String] 文件名(不包含扩展名)
910
- # @yieldparam parent_relative_dir [Pathname] 文件所在目录的相对路径
911
- # @yieldreturn [void]
912
- #
913
- # @raise [RPGJsonFileError] json 文件不是 RPG 模块中的对象
914
- # @raise [R3EXSJsonFileError] json 文件不是 R3EXS 模块中的对象
915
- # @raise [ModuleNameError] module_name 不是 :RPG 或 :R3EXS
916
- # @raise [JsonDirError] target_dir 不存在
917
- #
918
- # @return [void]
919
- def Utils.all_json_files(target_dir, module_name)
920
- # 检查 target_dir 目录是否存在
921
- target_dir.exist? && target_dir.directory? or raise JsonDirError.new(target_dir.to_s), "JSON directory not found: #{target_dir}"
922
- # 递归获取 target_dir 下的所有 *.json 文件
923
- target_dir.glob('**/*.json').each do |file_path|
924
- file_basename = file_path.basename('.json').to_s
925
- # 检查文件名是否在 JSON_FILE_NAME 中与其正则表达式匹配
926
- next unless JSON_FILE_NAME.any? { |pattern| file_basename =~ pattern }
927
-
928
- print "#{ESCAPE}#{BLUE_COLOR}Reading and Deserializing #{RESET_COLOR}#{file_path}...\r" if $global_options[:verbose]
929
- object = Oj.load_file(file_path.to_s)
930
-
931
- case module_name
932
- when :RPG
933
- # 这里的类型检查要用紧凑模式,因为这是从 rvdata2 文件直接全部序列化后的 JSON 文件中读取的 object,其中可能存在 nil 元素
934
- begin
935
- check_type(object, file_basename, true, module_name)
936
- rescue RPGTypeError
937
- raise RPGJsonFileError.new(file_path.to_s), "Invalid RPG JSON file: #{file_path}"
938
- end
939
- when :R3EXS
940
- # 这里的类型检查不能用紧凑模式,因为这是从 R3EXS 模块的类序列化后的 JSON 文件中读取的 object,程序设计中不应该存在 nil 元素
941
- begin
942
- check_type(object, file_basename, false, module_name)
943
- rescue R3EXSTypeError
944
- raise R3EXSJsonFileError.new(file_path.to_s), "Invalid R3EXS JSON file: #{file_path}"
945
- end
946
- else
947
- raise ModuleNameError.new(module_name), "Invalid module name: #{module_name}"
948
- end
291
+ # R3EXS 对象中的字符串注入 RPG 对象中
292
+ #
293
+ # @param obj [Object] 待转化的 R3EXS 对象
294
+ #
295
+ # @return [Object]
296
+ def self.r3exs_rpg(r3exs_obj, rpg_obj)
297
+ if r3exs_obj.is_a?(Array)
298
+ r3exs_obj.each { |obj| obj.inject_to(rpg_obj[obj.index]) }
299
+ elsif r3exs_obj.is_a?(Hash) # 只有 RPG::MapInfo 是 Hash,且 key 为整数
300
+ r3exs_obj.each { |key, obj| obj.inject_to(rpg_obj[key]) }
301
+ else
302
+ # 只有 RPG::Map RPG::System 是单独一个对象,且不可能为 nil
303
+ r3exs_obj.inject_to(rpg_obj)
304
+ end
305
+ end
949
306
 
950
- yield object, file_basename, file_path.dirname.relative_path_from(target_dir)
951
- end
307
+ # RPG 对象转化为 R3EXS 对象
308
+ #
309
+ # @param obj [Object] 待转化的 RPG 对象
310
+ # @param klass [::Class] 对应的 R3EXS 模块中的类
311
+ #
312
+ # @return [Object]
313
+ def self.rpg_r3exs(obj, klass)
314
+ if obj.is_a?(Array)
315
+ temp = []
316
+ obj.each_with_index do |obj, index|
317
+ next if obj.nil?
318
+
319
+ obj_r3exs = klass.new(obj, index)
320
+ temp << obj_r3exs unless obj_r3exs.empty?
952
321
  end
322
+ elsif obj.is_a?(Hash) # 只有 RPG::MapInfo 是 Hash,且 key 为整数
323
+ temp = {}
324
+ obj.each do |key, obj|
325
+ next if obj.nil?
953
326
 
954
- # 读取 target_dir 下的所有 CommonEvent JSON 文件,将其反序列化为对象数组,并调用 block
955
- #
956
- # @note 注意传入 block 的 object
957
- # - 在 module_name 为 RPG 时,object 可能存在 nil 元素
958
- # - 在 module_name 为 R3EXS 时,object 不可能存在 nil 元素
959
- #
960
- # @param target_dir [Pathname] 目标目录
961
- # @param module_name [Symbol] 模块名
962
- #
963
- # @yieldparam commonevents [Array<Object>] CommonEvent JSON 文件反序列化后的数组
964
- # @yieldparam commonevents_basenames [String] CommonEvent JSON 文件名数组(不包含扩展名)
965
- # @yieldparam parent_relative_dir [Pathname] 文件所在目录的相对路径
966
- # @yieldreturn [void]
967
- #
968
- # @raise [RPGJsonFileError] json 文件不是 RPG 模块中的对象
969
- # @raise [R3EXSJsonFileError] json 文件不是 R3EXS 模块中的对象
970
- # @raise [ModuleNameError] module_name 不是 :RPG 或 :R3EXS
971
- # @raise [JsonDirError] target_dir 不存在
972
- #
973
- # @return [void]
974
- def Utils.all_commonevent_json_files(target_dir, module_name)
975
- # 检查 target_dir 目录是否存在
976
- target_dir.exist? && target_dir.directory? or raise JsonDirError.new(target_dir.to_s), "JSON directory not found: #{target_dir}"
977
-
978
- # 用两个个Hash来存储每一个父目录下的所有的 CommonEvent_\d{5}.json 文件的反序列化后的对象数组以及其文件名数组
979
- # Hash 的键是父目录的路径,值是一个数组,存储该目录下的所有 CommonEvent_\d{5}.json 文件的反序列化后的对象数组以及其文件名数组
980
- commonevents_hash = Hash.new { |h, k| h[k] = [] }
981
- commonevents_basenames_hash = Hash.new { |h, k| h[k] = [] }
982
-
983
- # 递归获取 target_dir 下的所有 CommonEvent_\d{5}.json 文件
984
- target_dir.glob('**/CommonEvent_[0-9][0-9][0-9][0-9][0-9].json').each do |file_path|
985
- print "#{ESCAPE}#{BLUE_COLOR}Reading and Deserializing #{RESET_COLOR}#{file_path}...\r" if $global_options[:verbose]
986
- object = Oj.load_file(file_path.to_s)
987
- parent_dir = file_path.dirname
988
- commonevents_hash[parent_dir] << object
989
- commonevents_basenames_hash[parent_dir] << file_path.basename('.json').to_s
990
- end
991
-
992
- # 遍历每一个父目录的路径
993
- commonevents_hash.each_key do |parent_dir|
994
- commonevents = commonevents_hash[parent_dir]
995
- commonevents_basenames = commonevents_basenames_hash[parent_dir]
996
-
997
- case module_name
998
- when :RPG
999
- # 这里的类型检查要用紧凑模式,因为这是从 rvdata2 文件直接全部序列化后的 JSON 文件中读取的 object,其中可能存在 nil 元素
1000
- begin
1001
- check_type(commonevents, 'CommonEvents', true, module_name)
1002
- rescue RPGTypeError
1003
- raise RPGJsonFileError.new(parent_dir.to_s), "Invalid RPG CommonEvents JSON file"
1004
- end
1005
- when :R3EXS
1006
- # 这里的类型检查不能用紧凑模式,因为这是从 R3EXS 模块的类序列化后的 JSON 文件中读取的 object,程序设计中不应该存在 nil 元素
1007
- begin
1008
- check_type(commonevents, 'CommonEvents', false, module_name)
1009
- rescue R3EXSTypeError
1010
- raise R3EXSJsonFileError.new(parent_dir.to_s), "Invalid R3EXS CommonEvents JSON file"
1011
- end
1012
- else
1013
- raise ModuleNameError.new(module_name), "Invalid module name: #{module_name}"
1014
- end
1015
-
1016
- yield commonevents, commonevents_basenames, parent_dir.relative_path_from(target_dir)
1017
- end
327
+ obj_r3exs = klass.new(obj)
328
+ temp[key] = obj_r3exs unless obj_r3exs.empty?
1018
329
  end
330
+ else
331
+ # 只有 RPG::Map 和 RPG::System 是单独一个对象,且不可能为 nil
332
+ temp = klass.new(obj)
333
+ end
334
+ temp
335
+ end
1019
336
 
1020
- # 读取 target_dir 下的所有 Ruby 源码文件,并调用 block
1021
- #
1022
- # @note 注意这里以二进制方式读取文件,因为 Prism 里面的节点的位置是相对二进制下的位置
1023
- #
1024
- # @param target_dir [Pathname] 目标目录
1025
- #
1026
- # @yieldparam scripts [Array<String>] 读取的 Ruby 源码文件数组
1027
- # @yieldparam scripts_basenames [Array<String>] Ruby 源码文件名数组(不包含扩展名)
1028
- # @yieldparam parent_relative_dir [Pathname] 文件所在目录的相对路径
1029
- # @yieldreturn [void]
1030
- #
1031
- # @raise [JsonDirError] target_dir 不存在
1032
- #
1033
- # @return [void]
1034
- def Utils.all_rb_files(target_dir)
1035
- # 检查 target_dir 目录是否存在
1036
- target_dir.exist? && target_dir.directory? or raise JsonDirError.new(target_dir.to_s), "JSON directory not found: #{target_dir}"
1037
-
1038
- # 用两个个Hash来存储每一个父目录下的所有的 \d{3}.rb 文件的反序列化后的对象数组以及其文件名数组
1039
- # Hash 的键是父目录的路径,值是一个数组,存储该目录下的所有 \d{3}.rb 文件的反序列化后的对象数组以及其文件名数组
1040
- scripts_hash = Hash.new { |h, k| h[k] = [] }
1041
- scripts_basenames_hash = Hash.new { |h, k| h[k] = [] }
1042
-
1043
- # 递归获取 target_dir 下的所有 \d{5}.rb 文件
1044
- target_dir.glob('**/[0-9][0-9][0-9].rb').each do |file_path|
1045
- print "#{ESCAPE}#{BLUE_COLOR}Reading and Deserializing #{RESET_COLOR}#{file_path}...\r" if $global_options[:verbose]
1046
- object = file_path.binread
1047
- parent_dir = file_path.dirname
1048
- scripts_hash[parent_dir] << object
1049
- scripts_basenames_hash[parent_dir] << file_path.basename('.rb').to_s
1050
- end
337
+ # 提取 R3EXS 对象中的字符串
338
+ #
339
+ # @param obj [Object] 待注入的 R3EXS 对象
340
+ # @param manual_trans_hash[Hash{String => String}] 翻译结果
341
+ #
342
+ # @return [Array<String>]
343
+ def self.in_r3exs(obj, manual_trans_hash)
344
+ if obj.is_a?(Array)
345
+ obj.each { |obj| obj.in_strings(manual_trans_hash) }
346
+ elsif obj.is_a?(Hash)
347
+ obj.each_value { |obj| obj.in_strings(manual_trans_hash) }
348
+ else
349
+ obj.in_strings(manual_trans_hash)
350
+ end
351
+ end
1051
352
 
1052
- # 遍历每一个父目录的路径
1053
- scripts_hash.each_key do |parent_dir|
1054
- scripts = scripts_hash[parent_dir]
1055
- scripts_basenames = scripts_basenames_hash[parent_dir]
1056
- yield scripts, scripts_basenames, parent_dir.relative_path_from(target_dir)
1057
- end
1058
- end
353
+ # 提取 R3EXS 对象中的字符串
354
+ #
355
+ # @param obj [Object] 待提取的 R3EXS 对象
356
+ #
357
+ # @return [Array<String>]
358
+ def self.ex_r3exs(obj)
359
+ ex_strings = []
360
+ if obj.is_a?(Array)
361
+ obj.each { |obj| ex_strings.concat(obj.ex_strings) }
362
+ elsif obj.is_a?(Hash)
363
+ obj.each_value { |obj| ex_strings.concat(obj.ex_strings) }
364
+ else
365
+ ex_strings.concat(obj.ex_strings)
366
+ end
367
+ ex_strings
368
+ end
1059
369
 
1060
- # 将 object 序列化为 json 文件
1061
- #
1062
- # @param object [Object] 待序列化的对象
1063
- # @param output_file [Pathname] 输出文件路径
1064
- #
1065
- # @return [void]
1066
- def Utils.object_json(object, output_file)
1067
- output_file.dirname.mkpath unless output_file.dirname.exist?
1068
- output_file.write(Oj.dump(object, indent: 2))
1069
- end
370
+ # 将 script 序列化为 rb 文件
371
+ #
372
+ # @param script [String] rb 源码
373
+ # @param file_path [Pathname] 输出文件路径
374
+ #
375
+ # @return [void]
376
+ def self.script_rb(script, file_path)
377
+ file_path.parent.mkpath unless file_path.parent.exist?
378
+ file_path.write(script.encode(universal_newline: true))
379
+ end
1070
380
 
1071
- # object 序列化为 rvdata2 文件
1072
- #
1073
- # @param object [Object] 待序列化的对象
1074
- # @param output_file [Pathname] 输出文件路径
1075
- #
1076
- # @return [void]
1077
- def Utils.object_rvdata2(object, output_file)
1078
- output_file.dirname.mkpath unless output_file.dirname.exist?
1079
- output_file.binwrite(Marshal.dump(object))
1080
- end
381
+ # 将压缩 script 序列化为 rb 文件
382
+ #
383
+ # @param script [String] rb 压缩源码
384
+ # @param file_path [Pathname] 输出文件路径
385
+ #
386
+ # @return [void]
387
+ def self.script_rb_compressing(script, file_path)
388
+ script_rb(Zlib::Inflate.inflate(script), file_path)
389
+ end
1081
390
 
391
+ # 将 obj 序列化为 json 文件
392
+ #
393
+ # @param obj [Object] 待序列化的对象
394
+ # @param file_path [Pathname] 输出文件路径
395
+ #
396
+ # @return [void]
397
+ def self.object_json(obj, file_path)
398
+ file_path.parent.mkpath unless file_path.parent.exist?
399
+ file_path.write(Oj.dump(obj, indent: 2))
1082
400
  end
1083
401
 
1084
- end
402
+ # 将 obj 序列化为 rvdata2 文件
403
+ #
404
+ # @param obj [Object] 待序列化的对象
405
+ # @param file_path [Pathname] 输出文件路径
406
+ #
407
+ # @return [void]
408
+ def self.object_rvdata2(obj, file_path)
409
+ file_path.parent.mkpath unless file_path.parent.exist?
410
+ file_path.binwrite(Marshal.dump(obj))
411
+ end
412
+ end
413
+ end
414
+
415
+ class String
416
+ unless method_defined?(:blank?) && ' '.blank?
417
+ # Checks whether a string is blank. A string is considered blank if it
418
+ # is either empty or contains only whitespace characters.
419
+ #
420
+ # @return [Boolean] true is the string is blank, false otherwise
421
+ #
422
+ # @example
423
+ # ''.blank? #=> true
424
+ # ' '.blank? #=> true
425
+ # ' test'.blank? #=> false
426
+ def blank?
427
+ empty? || lstrip.empty?
428
+ end
429
+ end
430
+ end