elder_scrolls_plugin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: df7d9a2c3af98a5aae409dfa910c13f7c2d02db2cc4ed0d5e5b11e3f85ac66af
4
+ data.tar.gz: a2e4c6e7870479a9eee159b9505a6c2e73ff1e1ee932b8a3984f86c8f8151f78
5
+ SHA512:
6
+ metadata.gz: 5e78725ce4150b799f26e35f1eae10677d5f246ebbcfac2788b4d626ddedfaf9d629d9343eb766e2606895a6b7041589f8c0424d955b74b4c6a65a04319342a0
7
+ data.tar.gz: 9e2c4ceb94cba35a1451dcd6b7ddfa082c8df9ef9f7fc473dee6bb106748788def61a16547e3652fb7f361f67281992ead3b6259f7230af8f7dd09a2e2c3dbed
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'json'
4
+ require 'elder_scrolls_plugin'
5
+ require 'json-diff'
6
+
7
+ debug = false
8
+ read_only_tes4 = false
9
+ read_fields = false
10
+ output_unknown = false
11
+ output_masters = false
12
+ output_tree = false
13
+ output_json = false
14
+ output_form_ids = false
15
+ diff = false
16
+ OptionParser.new do |opts|
17
+ opts.banner = "Usage: #{$0} [options] files"
18
+ opts.on('-d', '--debug', 'Activate log debugs') do
19
+ debug = true
20
+ end
21
+ opts.on('-f', '--include-fields', 'Read the fields') do
22
+ read_fields = true
23
+ end
24
+ opts.on('-j', '--output-json', 'Output the tree of records as JSON') do
25
+ output_json = true
26
+ end
27
+ opts.on('-i', '--diff', 'Output a JSON of the differences between 2 esps. Requires 2 esps files to be given. Will display file2 - file1.') do
28
+ diff = true
29
+ end
30
+ opts.on('-m', '--output-masters', 'Output the masters list') do
31
+ output_masters = true
32
+ end
33
+ opts.on('-o', '--only-tes4', 'Read only the TES4 header') do
34
+ read_only_tes4 = true
35
+ end
36
+ opts.on('-r', '--output-form-ids', 'Output the absolute form IDs') do
37
+ output_form_ids = true
38
+ end
39
+ opts.on('-t', '--output-tree', 'Output the tree of records') do
40
+ output_tree = true
41
+ end
42
+ opts.on('-u', '--output-unknown', 'Output unknown chunks') do
43
+ output_unknown = true
44
+ end
45
+ end.parse!
46
+ files = ARGV.clone
47
+ raise 'Can\'t use --only-tes4 and --output-masters without --include-fields' if output_masters && read_only_tes4 && !read_fields
48
+ raise 'Need 2 files to be given when using --diff option' if diff && files.size != 2
49
+ raise '--diff can\'t be used with other --output-* options.' if diff && (output_json || output_masters || output_form_ids || output_tree || output_unknown)
50
+ @display_headers = files.size > 1 || [output_unknown, output_masters, output_tree, output_json, output_form_ids].select { |flag| flag }.size > 1
51
+
52
+ # Start a processing section with a message.
53
+ # If no headers need to be output, only execute the section's code.
54
+ #
55
+ # Parameters::
56
+ # * *title* (String): Section title
57
+ # * Proc: Section code called
58
+ def section(title)
59
+ puts "===== #{title} =====" if @display_headers
60
+ yield
61
+ puts if @display_headers
62
+ end
63
+
64
+ if diff
65
+ json1, json2 = files.map do |file|
66
+ ElderScrollsPlugin.new(
67
+ file,
68
+ debug: debug,
69
+ decode_only_tes4: read_only_tes4,
70
+ decode_fields: read_fields
71
+ ).to_json
72
+ end
73
+ # Compute the diff of 2 jsons
74
+ puts JSON.pretty_generate(
75
+ JsonDiff.diff(json1, json2, include_was: true, moves: false).map do |json_diff|
76
+ readable_path = ''
77
+ json_cursor = json1
78
+ # puts json1
79
+ json_diff['path'].split('/')[1..-1].each do |token|
80
+ # puts "----- #{token}"
81
+ if token.scan(/\D/).empty?
82
+ # JSON should be an Array
83
+ json_cursor = json_cursor[token.to_i]
84
+ else
85
+ json_cursor = json_cursor[token.to_sym]
86
+ end
87
+ readable_path << "/#{json_cursor[:name]}" if json_cursor.is_a?(Hash) && json_cursor.key?(:name)
88
+ end
89
+ json_diff.merge(
90
+ 'esp_path' => readable_path
91
+ )
92
+ end
93
+ )
94
+ else
95
+ files.each do |file|
96
+ esp = ElderScrollsPlugin.new(
97
+ file,
98
+ debug: debug,
99
+ decode_only_tes4: read_only_tes4,
100
+ ignore_unknown_chunks: output_unknown,
101
+ decode_fields: read_fields)
102
+ section("#{file} - Chunks tree") { esp.dump } if output_tree
103
+ section("#{file} - JSON") { puts JSON.pretty_generate(esp.to_json) } if output_json
104
+ section("#{file} - Masters") { esp.dump_masters } if output_masters
105
+ section("#{file} - Absolute Form IDs") { esp.dump_absolute_form_ids } if output_form_ids
106
+ section("#{file} - Unknown chunks") do
107
+ puts esp.unknown_chunks.map { |c| "#{c.name} [#{c.instance_variable_get(:@esp_info)[:type]}]" }.sort.uniq.join("\n")
108
+ end if output_unknown
109
+ end
110
+ end
@@ -0,0 +1,914 @@
1
+ require 'riffola'
2
+ require 'base64'
3
+ require 'bindata'
4
+
5
+ class ElderScrollsPlugin
6
+
7
+ # Set the current esp being read (useful for BinData decoding types that depend on the esp)
8
+ #
9
+ # Parameters::
10
+ # * *esp* (ElderScrollsPlugin): The current esp
11
+ def self.current_esp=(esp)
12
+ @esp = esp
13
+ end
14
+
15
+ # Get the current esp being read (useful for BinData decoding types that depend on the esp)
16
+ #
17
+ # Result::
18
+ # * ElderScrollsPlugin: The current esp
19
+ def self.current_esp
20
+ @esp
21
+ end
22
+
23
+ # Hash< Chunk or nil, Array<Chunk> >: The chunks tree, with nil being the root node
24
+ attr_reader :chunks_tree
25
+
26
+ # Array<String>: Ordered list of masters
27
+ attr_reader :masters
28
+
29
+ # Array<Riffola::Chunk>: Unknown chunks encountered
30
+ attr_reader :unknown_chunks
31
+
32
+ KNOWN_GRUP_RECORDS_WITHOUT_FIELDS = %w(
33
+ NAVM
34
+ CELL
35
+ LAND
36
+ NPC_
37
+ )
38
+
39
+ KNOWN_GRUP_RECORDS_WITH_FIELDS = %w(
40
+ AACT
41
+ ACHR
42
+ ACTI
43
+ ADDN
44
+ ALCH
45
+ AMMO
46
+ ANIO
47
+ APPA
48
+ ARMA
49
+ ARMO
50
+ ARTO
51
+ ASPC
52
+ ASTP
53
+ AVIF
54
+ BOOK
55
+ BPTD
56
+ CAMS
57
+ CLAS
58
+ CLDC
59
+ CLFM
60
+ CLMT
61
+ COBJ
62
+ COLL
63
+ CONT
64
+ CPTH
65
+ CSTY
66
+ DEBR
67
+ DIAL
68
+ DLBR
69
+ DLVW
70
+ DOBJ
71
+ DOOR
72
+ DUAL
73
+ ECZN
74
+ EFSH
75
+ ENCH
76
+ EQUP
77
+ EXPL
78
+ EYES
79
+ FACT
80
+ FLOR
81
+ FLST
82
+ FSTP
83
+ FSTS
84
+ FURN
85
+ GLOB
86
+ GMST
87
+ GRAS
88
+ HAIR
89
+ HAZD
90
+ HDPT
91
+ IDLE
92
+ IDLM
93
+ IMAD
94
+ IMGS
95
+ INFO
96
+ INGR
97
+ IPCT
98
+ IPDS
99
+ KEYM
100
+ KYWD
101
+ LCRT
102
+ LCTN
103
+ LGTM
104
+ LIGH
105
+ LSCR
106
+ LTEX
107
+ LVLI
108
+ LVLN
109
+ LVSP
110
+ MATO
111
+ MATT
112
+ MESG
113
+ MGEF
114
+ MISC
115
+ MOVT
116
+ MSTT
117
+ MUSC
118
+ MUST
119
+ NAVI
120
+ OTFT
121
+ PACK
122
+ PERK
123
+ PROJ
124
+ PWAT
125
+ QUST
126
+ RACE
127
+ REFR
128
+ REGN
129
+ RELA
130
+ REVB
131
+ RFCT
132
+ RGDL
133
+ SCEN
134
+ SCOL
135
+ SCPT
136
+ SCRL
137
+ SHOU
138
+ SLGM
139
+ SMBN
140
+ SMEN
141
+ SMQN
142
+ SNCT
143
+ SNDR
144
+ SOPM
145
+ SOUN
146
+ SPEL
147
+ SPGD
148
+ STAT
149
+ TACT
150
+ TREE
151
+ TXST
152
+ VTYP
153
+ WATR
154
+ WEAP
155
+ WOOP
156
+ WRLD
157
+ WTHR
158
+ )
159
+
160
+ KNOWN_FIELDS = %w(
161
+ 00TX
162
+ 10TX
163
+ 20TX
164
+ 30TX
165
+ 40TX
166
+ 50TX
167
+ 60TX
168
+ 70TX
169
+ 80TX
170
+ 90TX
171
+ :0TX
172
+ ;0TX
173
+ <0TX
174
+ =0TX
175
+ >0TX
176
+ ?0TX
177
+ @0TX
178
+ A0TX
179
+ ACEC
180
+ ACEP
181
+ ACID
182
+ ACPR
183
+ ACSR
184
+ ACUN
185
+ AHCF
186
+ AHCM
187
+ ALCA
188
+ ALCL
189
+ ALCO
190
+ ALDN
191
+ ALEA
192
+ ALED
193
+ ALEQ
194
+ ALFA
195
+ ALFC
196
+ ALFD
197
+ ALFE
198
+ ALFI
199
+ ALFL
200
+ ALFR
201
+ ALID
202
+ ALLS
203
+ ALNA
204
+ ALNT
205
+ ALPC
206
+ ALRT
207
+ ALSP
208
+ ALST
209
+ ALUA
210
+ ANAM
211
+ ATKD
212
+ ATKE
213
+ AVSK
214
+ B0TX
215
+ BAMT
216
+ BIDS
217
+ BNAM
218
+ BOD2
219
+ BPND
220
+ BPNI
221
+ BPNN
222
+ BPNT
223
+ BPTN
224
+ C0TX
225
+ CIS1
226
+ CIS2
227
+ CITC
228
+ CNAM
229
+ CNTO
230
+ COCT
231
+ COED
232
+ CRDT
233
+ CRVA
234
+ CTDA
235
+ D0TX
236
+ DALC
237
+ DATA
238
+ DEMO
239
+ DESC
240
+ DEST
241
+ DEVA
242
+ DFTF
243
+ DFTM
244
+ DMAX
245
+ DMDL
246
+ DMDS
247
+ DMDT
248
+ DMIN
249
+ DNAM
250
+ DODT
251
+ DSTD
252
+ DSTF
253
+ E0TX
254
+ EAMT
255
+ ECOR
256
+ EDID
257
+ EFID
258
+ EFIT
259
+ EITM
260
+ ENAM
261
+ ENIT
262
+ EPF2
263
+ EPF3
264
+ EPFD
265
+ EPFT
266
+ ETYP
267
+ F0TX
268
+ FLMV
269
+ FLTR
270
+ FLTV
271
+ FNAM
272
+ FNPR
273
+ FTSF
274
+ FTSM
275
+ FULL
276
+ G0TX
277
+ GNAM
278
+ H0TX
279
+ HCLF
280
+ HEAD
281
+ HEDR
282
+ HNAM
283
+ HTID
284
+ ICO2
285
+ ICON
286
+ IDLA
287
+ IDLC
288
+ IDLF
289
+ IDLT
290
+ IMSP
291
+ INAM
292
+ INCC
293
+ INDX
294
+ INTV
295
+ ITXT
296
+ JNAM
297
+ K0TX
298
+ KNAM
299
+ KSIZ
300
+ KWDA
301
+ L0TX
302
+ LLCT
303
+ LNAM
304
+ LTMP
305
+ LVLD
306
+ LVLF
307
+ LVLG
308
+ LVLI
309
+ LVLO
310
+ MDOB
311
+ MHDT
312
+ MNAM
313
+ MO2S
314
+ MO2T
315
+ MO3S
316
+ MO3T
317
+ MO4S
318
+ MO4T
319
+ MO5T
320
+ MOD2
321
+ MOD3
322
+ MOD4
323
+ MOD5
324
+ MODL
325
+ MODS
326
+ MODT
327
+ MPAI
328
+ MPAV
329
+ MTNM
330
+ NAM0
331
+ NAM1
332
+ NAM2
333
+ NAM3
334
+ NAM4
335
+ NAM5
336
+ NAM7
337
+ NAM8
338
+ NAM9
339
+ LCEC
340
+ LCEP
341
+ LCID
342
+ LCPR
343
+ LCSR
344
+ LCUN
345
+ NAMA
346
+ NAME
347
+ NEXT
348
+ NNAM
349
+ NVER
350
+ NVMI
351
+ NVPP
352
+ OBND
353
+ OCOR
354
+ ONAM
355
+ PDTO
356
+ PFIG
357
+ PFPC
358
+ PHTN
359
+ PHWT
360
+ PKC2
361
+ PKCU
362
+ PKDT
363
+ PLDT
364
+ PLVD
365
+ PNAM
366
+ POBA
367
+ POCA
368
+ POEA
369
+ PRCB
370
+ PRKC
371
+ PRKE
372
+ PRKF
373
+ PSDT
374
+ PTDA
375
+ QNAM
376
+ QOBJ
377
+ QSDT
378
+ QSTA
379
+ QTGL
380
+ RCEC
381
+ RCLR
382
+ RCPR
383
+ RCSR
384
+ RCUN
385
+ RDAT
386
+ RDMO
387
+ RDSA
388
+ RDWT
389
+ RNAM
390
+ RNMV
391
+ RPLD
392
+ RPLI
393
+ RPRF
394
+ RPRM
395
+ SDSC
396
+ SLCP
397
+ SNAM
398
+ SNDD
399
+ SNMV
400
+ SOUL
401
+ SPCT
402
+ SPIT
403
+ SPLO
404
+ SPMV
405
+ SWMV
406
+ TCLT
407
+ TIFC
408
+ TINC
409
+ TIND
410
+ TINI
411
+ TINL
412
+ TINP
413
+ TINT
414
+ TINV
415
+ TIRS
416
+ TNAM
417
+ TPIC
418
+ TRDT
419
+ TWAT
420
+ TX00
421
+ TX01
422
+ TX02
423
+ TX03
424
+ TX04
425
+ TX05
426
+ TX07
427
+ UNAM
428
+ UNES
429
+ VENC
430
+ VEND
431
+ VENV
432
+ VMAD
433
+ VNAM
434
+ VTCK
435
+ WBDT
436
+ WCTR
437
+ WKMV
438
+ WNAM
439
+ XACT
440
+ XALP
441
+ XAPD
442
+ XAPR
443
+ XCNT
444
+ XEMI
445
+ XESP
446
+ XEZN
447
+ XIS2
448
+ XLCM
449
+ XLCN
450
+ XLIB
451
+ XLIG
452
+ XLKR
453
+ XLOC
454
+ XLRL
455
+ XLRM
456
+ XLRT
457
+ XLTW
458
+ XMBO
459
+ XMBR
460
+ XMRK
461
+ XNAM
462
+ XNDP
463
+ XOCP
464
+ XOWN
465
+ XPOD
466
+ XPPA
467
+ XPRD
468
+ XPRM
469
+ XRDS
470
+ XRGB
471
+ XRGD
472
+ XRMR
473
+ XSCL
474
+ XTEL
475
+ XTNM
476
+ XTRI
477
+ XWCN
478
+ XWCU
479
+ XWEM
480
+ XXXX
481
+ YNAM
482
+ ZNAM
483
+ )
484
+
485
+ class FormId < BinData::Primitive
486
+ uint32le :form_id
487
+
488
+ def get
489
+ "#{ElderScrollsPlugin.current_esp.absolute_form_id(sprintf('%08X', self.form_id))} - #{sprintf('%08X', self.form_id)}"
490
+ end
491
+ end
492
+
493
+ class Label < BinData::Primitive
494
+ string :label, read_length: 4
495
+
496
+ def get
497
+ self.label.ascii_only? ? self.label : "<#{Base64.encode64(self.label)}>"
498
+ end
499
+ end
500
+
501
+ class RoundedFloat < BinData::Primitive
502
+ float_le :float
503
+ def get
504
+ self.float.round(4)
505
+ end
506
+ end
507
+
508
+ class Door < BinData::Record
509
+ uint32le :unknown
510
+ form_id :door_ref
511
+ end
512
+
513
+ class Vertex < BinData::Record
514
+ rounded_float :x
515
+ rounded_float :y
516
+ rounded_float :z
517
+ end
518
+
519
+ class Triangle < BinData::Record
520
+ uint16le :vertex_index_0
521
+ uint16le :vertex_index_1
522
+ uint16le :vertex_index_2
523
+ end
524
+
525
+ class IslandNavMesh < BinData::Record
526
+ rounded_float :x_min
527
+ rounded_float :y_min
528
+ rounded_float :z_min
529
+ rounded_float :x_max
530
+ rounded_float :y_max
531
+ rounded_float :z_max
532
+ uint32le :triangles_count
533
+ array :triangles, type: :triangle, initial_length: :triangles_count
534
+ uint32le :vertices_count
535
+ array :vertices, type: :vertex, initial_length: :vertices_count
536
+ end
537
+
538
+ module Headers
539
+ class GRUP < BinData::Record
540
+ label :label
541
+ int32le :grup_type
542
+ uint16le :date
543
+ uint16le :unknown_1
544
+ uint16le :version
545
+ uint16le :unknown_2
546
+ end
547
+ class TES4 < BinData::Record
548
+ uint32le :flags
549
+ uint32le :id
550
+ uint32le :revision
551
+ uint16le :version
552
+ uint16le :unknown
553
+ end
554
+ class All < BinData::Record
555
+ uint32le :flags
556
+ form_id :id
557
+ uint32le :revision
558
+ uint16le :version
559
+ uint16le :unknown
560
+ end
561
+ end
562
+
563
+ module Data
564
+ module ACHR
565
+ class ACHR_DATA < BinData::Record
566
+ rounded_float :x_pos
567
+ rounded_float :y_pos
568
+ rounded_float :z_pos
569
+ rounded_float :x_rot
570
+ rounded_float :y_rot
571
+ rounded_float :z_rot
572
+ end
573
+ class ACHR_NAME < BinData::Record
574
+ form_id :base_npc
575
+ end
576
+ end
577
+ module NVMI
578
+ class NVMI_NAVI < BinData::Record
579
+ form_id :nav_mesh
580
+ uint32le :unknown
581
+ rounded_float :x
582
+ rounded_float :y
583
+ rounded_float :z
584
+ uint32le :preferred_merges_flag
585
+ uint32le :merged_to_count
586
+ array :merged_to, type: :form_id, initial_length: :merged_to_count
587
+ uint32le :preferred_merges_count
588
+ array :preferred_merges, type: :form_id, initial_length: :preferred_merges_count
589
+ uint32le :doors_count
590
+ array :doors, type: :door, initial_length: :doors_count
591
+ uint8 :is_island_mesh_flag
592
+ island_nav_mesh :island_nav_mesh, onlyif: :has_island_nav_mesh?
593
+ uint32le :location_marker
594
+ form_id :world_space
595
+ form_id :cell, onlyif: :world_space_is_skyrim?
596
+ form_id :grid_x, onlyif: :world_space_is_not_skyrim?
597
+ form_id :grid_y, onlyif: :world_space_is_not_skyrim?
598
+
599
+ def world_space_is_skyrim?
600
+ world_space.downcase == 'skyrim.esm/00003C'
601
+ end
602
+ def world_space_is_not_skyrim?
603
+ !world_space_is_skyrim?
604
+ end
605
+ def has_island_nav_mesh?
606
+ is_island_mesh_flag.nonzero?
607
+ end
608
+ end
609
+ end
610
+ module REFR
611
+ class REFR_DATA < BinData::Record
612
+ rounded_float :x_pos
613
+ rounded_float :y_pos
614
+ rounded_float :z_pos
615
+ rounded_float :x_rot
616
+ rounded_float :y_rot
617
+ rounded_float :z_rot
618
+ end
619
+ class REFR_NAME < BinData::Record
620
+ form_id :form_id
621
+ end
622
+ class REFR_XLIG < BinData::Record
623
+ rounded_float :fov
624
+ rounded_float :fade
625
+ rounded_float :end_distance
626
+ rounded_float :shadow_depth
627
+ int32le :unknown
628
+ end
629
+ class REFR_XLKR < BinData::Record
630
+ form_id :form_id_1
631
+ form_id :form_id_2
632
+ end
633
+ class REFR_XLRL < BinData::Record
634
+ form_id :form_id
635
+ end
636
+ class REFR_XNDP < BinData::Record
637
+ form_id :form_id
638
+ int32le :unknown
639
+ end
640
+ class REFR_XPRM < BinData::Record
641
+ rounded_float :x_bound
642
+ rounded_float :y_bound
643
+ rounded_float :z_bound
644
+ rounded_float :r
645
+ rounded_float :g
646
+ rounded_float :b
647
+ rounded_float :unknown_1
648
+ int32le :unknown_2
649
+ end
650
+ class REFR_XTEL < BinData::Record
651
+ form_id :door_form_id
652
+ rounded_float :x_pos
653
+ rounded_float :y_pos
654
+ rounded_float :z_pos
655
+ rounded_float :x_rot
656
+ rounded_float :y_rot
657
+ rounded_float :z_rot
658
+ int32le :alarm
659
+ end
660
+ end
661
+ end
662
+
663
+ # Constructor
664
+ #
665
+ # Parameters::
666
+ # * *file_name* (String): ESP file name
667
+ # * *decode_only_tes4* (Boolean): Do we decode only the TES4 header? [default: false]
668
+ # * *ignore_unknown_chunks* (Boolean): Do we ignore unknown chunks? [default: false]
669
+ # * *decode_fields* (Boolean): Do we decode fields content? [default: true]
670
+ # * *warnings* (Boolean): Do we activate warnings? [default: true]
671
+ # * *debug* (Boolean): Do we activate debugging logs? [default: false]
672
+ def initialize(file_name, decode_only_tes4: false, ignore_unknown_chunks: false, decode_fields: true, warnings: true, debug: false)
673
+ @file_name = file_name
674
+ @decode_only_tes4 = decode_only_tes4
675
+ @ignore_unknown_chunks = ignore_unknown_chunks
676
+ @decode_fields = decode_fields
677
+ @warnings = warnings
678
+ @debug = debug
679
+ # Get the list of masters
680
+ @masters = []
681
+ # Internal mapping of first 2 digits of a FormID to the corresponding master name
682
+ @master_ids = {}
683
+ # List of form ids being defined
684
+ @form_ids = []
685
+ # Unknown chunks encountered during decoding
686
+ @unknown_chunks = []
687
+ # Configure the current parser
688
+ ElderScrollsPlugin.current_esp = self
689
+ # Tree of chunks (nil for root)
690
+ # Hash< Chunk or nil, Array<Chunk> >
691
+ @chunks_tree = {}
692
+ chunks = Riffola.read(@file_name, chunks_format: {
693
+ '*' => { header_size: 8 },
694
+ 'TES4' => { header_size: 16 },
695
+ 'GRUP' => { data_size_correction: -24, header_size: 16 }
696
+ }, debug: @debug, warnings: @warnings) do |chunk|
697
+ # Decode the TES4 to get the masters
698
+ read_chunk(chunk) if chunk.name == 'TES4'
699
+ !decode_only_tes4 || chunk.name != 'TES4'
700
+ end
701
+ # We just finished parsing TES4, update the masters index
702
+ @master_ids.merge!(sprintf('%.2x', @master_ids.size) => File.basename(@file_name))
703
+ p @master_ids
704
+ @chunks_tree[nil] = chunks
705
+ unless decode_only_tes4
706
+ chunks.each do |chunk|
707
+ # Don't read TES4 twice, especially because we already have our master IDs parsed
708
+ read_chunk(chunk) unless chunk.name == 'TES4'
709
+ end
710
+ end
711
+ end
712
+
713
+ # Output a node of the chunks tree
714
+ #
715
+ # Parameters::
716
+ # * *chunk* (Riffola::Chunk or nil): The node to be dumped, or nil for root [default = nil]
717
+ # * *output_prefix* (String): Output prefix [default = '']
718
+ def dump(chunk = nil, output_prefix = '')
719
+ esp_info = chunk.nil? ? nil : chunk.instance_variable_get(:@esp_info)
720
+ sub_chunks = @chunks_tree[chunk]
721
+ puts "#{output_prefix}+- #{chunk.nil? ? 'ROOT' : "#{chunk.name}#{esp_info[:description].nil? ? '' : " - #{esp_info[:description]}"}"}#{sub_chunks.empty? ? '' : " (#{sub_chunks.size} sub-chunks)"}"
722
+ sub_chunks.each.with_index do |sub_chunk, idx_sub_chunk|
723
+ dump(sub_chunk, "#{output_prefix}#{idx_sub_chunk == sub_chunks.size - 1 ? ' ' : '|'} ")
724
+ end
725
+ end
726
+
727
+ # Dump masters
728
+ def dump_masters
729
+ @masters.each.with_index do |master, idx|
730
+ puts "* [#{sprintf('%.2x', idx)}] - #{master}"
731
+ end
732
+ end
733
+
734
+ # Dump absolute Form IDs
735
+ def dump_absolute_form_ids
736
+ @form_ids.sort.each do |form_id|
737
+ puts "* [#{form_id}] - #{absolute_form_id(form_id)}"
738
+ end
739
+ end
740
+
741
+ # Return the esp content as JSON
742
+ #
743
+ # Parameters::
744
+ # * *chunk* (Riffola::Chunk or nil): The node to be dumped, or nil for root [default = nil]
745
+ # Result::
746
+ # * Hash: JSON object
747
+ def to_json(chunk = nil)
748
+ esp_info = chunk.nil? ? { type: :root, description: 'root' } : chunk.instance_variable_get(:@esp_info)
749
+ json = {
750
+ name: chunk.nil? ? 'ROOT' : chunk.name
751
+ }
752
+ json[:type] = esp_info[:type] unless esp_info[:type].nil?
753
+ json[:description] = esp_info[:description] unless esp_info[:description].nil?
754
+ json[:decoded_data] = esp_info[:decoded_data] unless esp_info[:decoded_data].nil?
755
+ json[:decoded_header] = esp_info[:decoded_header] unless esp_info[:decoded_header].nil?
756
+ unless chunk.nil?
757
+ if esp_info[:decoded_header].nil?
758
+ header = chunk.header
759
+ json[:header] = (header.ascii_only? ? header : Base64.encode64(header)) unless header.empty?
760
+ end
761
+ if esp_info[:type] == :field && esp_info[:decoded_data].nil?
762
+ data = chunk.data
763
+ json[:data] = (data.ascii_only? ? data : Base64.encode64(data))
764
+ end
765
+ end
766
+ json[:sub_chunks] = @chunks_tree[chunk].
767
+ map { |sub_chunk| to_json(sub_chunk) }.
768
+ sort_by { |chunk_json| [chunk_json[:name], chunk_json[:description], chunk_json[:data]] } if @chunks_tree[chunk].size > 0
769
+ json
770
+ end
771
+
772
+ # Convert a Form ID into its absolute form.
773
+ # An absolute form ID is not dependent on the order of the masters and includes the master name.
774
+ #
775
+ # Parameters::
776
+ # * *form_id* (String): The original form ID
777
+ # Result::
778
+ # * String: The absolute Form ID
779
+ def absolute_form_id(form_id)
780
+ "#{@master_ids.key?(form_id[0..1]) ? @master_ids[form_id[0..1]] : "!!!#{form_id[0..1]}"}/#{form_id[2..7]}"
781
+ end
782
+
783
+ private
784
+
785
+ # Read a given chunk info
786
+ #
787
+ # Parameters::
788
+ # * *chunk* (Riffola::Chunk): Chunk to be read
789
+ def read_chunk(chunk)
790
+ puts "[ESP DEBUG] - Read chunk #{chunk.name}..." if @debug
791
+ description = nil
792
+ decoded_data = nil
793
+ subchunks = []
794
+ header = chunk.header
795
+ case chunk.name
796
+ when 'TES4'
797
+ # Always read fields of TES4 as they define the masters, which are needed for others
798
+ puts "[ESP DEBUG] - Read children chunks of #{chunk}" if @debug
799
+ subchunks = chunk.sub_chunks(sub_chunks_format: {
800
+ '*' => { header_size: 0, size_length: 2 },
801
+ 'ONAM' => {
802
+ data_size_correction: proc do |file|
803
+ # Size of ONAM field is sometimes badly computed. Correct it.
804
+ file.seek(4, IO::SEEK_CUR)
805
+ stored_size = file.read(2).unpack('S').first
806
+ file.read(chunk.size).index('INTV') - stored_size
807
+ end
808
+ }
809
+ })
810
+ chunk_type = :record
811
+ when 'MAST'
812
+ description = chunk.data[0..-2].downcase
813
+ @masters << description
814
+ @master_ids[sprintf('%.2x', @master_ids.size)] = description
815
+ chunk_type = :field
816
+ when 'GRUP'
817
+ puts "[ESP DEBUG] - Read children chunks of #{chunk}" if @debug
818
+ subchunks = chunk.sub_chunks(sub_chunks_format: Hash[(['GRUP'] + KNOWN_GRUP_RECORDS_WITHOUT_FIELDS + KNOWN_GRUP_RECORDS_WITH_FIELDS).map do |known_sub_record_name|
819
+ [
820
+ known_sub_record_name,
821
+ {
822
+ header_size: 16,
823
+ data_size_correction: known_sub_record_name == 'GRUP' ? -24 : 0
824
+ }
825
+ ]
826
+ end])
827
+ chunk_type = :group
828
+ when *KNOWN_GRUP_RECORDS_WITHOUT_FIELDS
829
+ # GRUP record having no fields
830
+ form_id_str = sprintf('%.8x', header[4..7].unpack('L').first)
831
+ @form_ids << form_id_str
832
+ description = "FormID: #{form_id_str}"
833
+ puts "[WARNING] - #{chunk} seems to have fields: #{chunk.data.inspect}" if @warnings && chunk.data[0..3] =~ /^\w{4}$/
834
+ chunk_type = :record
835
+ when *KNOWN_GRUP_RECORDS_WITH_FIELDS
836
+ # GRUP record having fields
837
+ form_id_str = sprintf('%.8x', header[4..7].unpack('L').first)
838
+ @form_ids << form_id_str
839
+ description = "FormID: #{form_id_str}"
840
+ if @decode_fields
841
+ puts "[ESP DEBUG] - Read children chunks of #{chunk}" if @debug
842
+ subchunks = chunk.sub_chunks(sub_chunks_format: { '*' => { header_size: 0, size_length: 2 } })
843
+ end
844
+ chunk_type = :record
845
+ when *KNOWN_FIELDS
846
+ # Field
847
+ record_module_name =
848
+ if Data.const_defined?(chunk.parent_chunk.name.to_sym)
849
+ chunk.parent_chunk.name.to_sym
850
+ elsif Data.const_defined?(:All)
851
+ :All
852
+ else
853
+ nil
854
+ end
855
+ unless record_module_name.nil?
856
+ record_module = Data.const_get(record_module_name)
857
+ data_class_name =
858
+ if record_module.const_defined?("#{record_module_name}_#{chunk.name}".to_sym)
859
+ "#{record_module_name}_#{chunk.name}".to_sym
860
+ elsif record_module.const_defined?("#{record_module_name}_All".to_sym)
861
+ "#{record_module_name}_All".to_sym
862
+ else
863
+ nil
864
+ end
865
+ unless data_class_name.nil?
866
+ data_info = record_module.const_get(data_class_name)
867
+ decoded_data = {}
868
+ data_info.read(chunk.data).each_pair do |property, value|
869
+ decoded_data[property] = value
870
+ end
871
+ end
872
+ end
873
+ chunk_type = :field
874
+ else
875
+ warning_desc = "Unknown chunk: #{chunk}. Data: #{chunk.data.inspect}"
876
+ if @ignore_unknown_chunks
877
+ puts "[WARNING] - #{warning_desc}" if @warnings
878
+ @unknown_chunks << chunk
879
+ chunk_type = :unknown
880
+ else
881
+ raise warning_desc
882
+ end
883
+ end
884
+ # Decorate the chunk with our info
885
+ esp_info = {
886
+ description: description,
887
+ type: chunk_type
888
+ }
889
+ esp_info[:decoded_data] = decoded_data unless decoded_data.nil?
890
+ unless header.empty?
891
+ header_class_name =
892
+ if Headers.const_defined?(chunk.name.to_sym)
893
+ chunk.name.to_sym
894
+ elsif Headers.const_defined?(:All)
895
+ :All
896
+ else
897
+ nil
898
+ end
899
+ unless header_class_name.nil?
900
+ header_info = Headers.const_get(header_class_name)
901
+ esp_info[:decoded_header] = {}
902
+ header_info.read(header).each_pair do |property, value|
903
+ esp_info[:decoded_header][property] = value
904
+ end
905
+ end
906
+ end
907
+ chunk.instance_variable_set(:@esp_info, esp_info)
908
+ @chunks_tree[chunk] = subchunks
909
+ subchunks.each.with_index do |subchunk, idx_subchunk|
910
+ read_chunk(subchunk)
911
+ end
912
+ end
913
+
914
+ end
@@ -0,0 +1,5 @@
1
+ class ElderScrollsPlugin
2
+
3
+ VERSION = '0.0.1'
4
+
5
+ end
@@ -0,0 +1,100 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ RSpec.configure do |config|
17
+ # rspec-expectations config goes here. You can use an alternate
18
+ # assertion/expectation library such as wrong or the stdlib/minitest
19
+ # assertions if you prefer.
20
+ config.expect_with :rspec do |expectations|
21
+ # This option will default to `true` in RSpec 4. It makes the `description`
22
+ # and `failure_message` of custom matchers include text for helper methods
23
+ # defined using `chain`, e.g.:
24
+ # be_bigger_than(2).and_smaller_than(4).description
25
+ # # => "be bigger than 2 and smaller than 4"
26
+ # ...rather than:
27
+ # # => "be bigger than 2"
28
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29
+ end
30
+
31
+ # rspec-mocks config goes here. You can use an alternate test double
32
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
33
+ config.mock_with :rspec do |mocks|
34
+ # Prevents you from mocking or stubbing a method that does not exist on
35
+ # a real object. This is generally recommended, and will default to
36
+ # `true` in RSpec 4.
37
+ mocks.verify_partial_doubles = true
38
+ end
39
+
40
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41
+ # have no way to turn it off -- the option exists only for backwards
42
+ # compatibility in RSpec 3). It causes shared context metadata to be
43
+ # inherited by the metadata hash of host groups and examples, rather than
44
+ # triggering implicit auto-inclusion in groups with matching metadata.
45
+ config.shared_context_metadata_behavior = :apply_to_host_groups
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ =begin
50
+ # This allows you to limit a spec run to individual examples or groups
51
+ # you care about by tagging them with `:focus` metadata. When nothing
52
+ # is tagged with `:focus`, all examples get run. RSpec also provides
53
+ # aliases for `it`, `describe`, and `context` that include `:focus`
54
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55
+ config.filter_run_when_matching :focus
56
+
57
+ # Allows RSpec to persist some state between runs in order to support
58
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
59
+ # you configure your source control system to ignore this file.
60
+ config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ # Limits the available syntax to the non-monkey patched syntax that is
63
+ # recommended. For more details, see:
64
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
65
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
66
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
67
+ config.disable_monkey_patching!
68
+
69
+ # This setting enables warnings. It's recommended, but in some cases may
70
+ # be too noisy due to issues in dependencies.
71
+ config.warnings = true
72
+
73
+ # Many RSpec users commonly either run the entire suite or an individual
74
+ # file, and it's useful to allow more verbose output when running an
75
+ # individual spec file.
76
+ if config.files_to_run.one?
77
+ # Use the documentation formatter for detailed output,
78
+ # unless a formatter has already been configured
79
+ # (e.g. via a command-line flag).
80
+ config.default_formatter = "doc"
81
+ end
82
+
83
+ # Print the 10 slowest examples and example groups at the
84
+ # end of the spec run, to help surface which specs are running
85
+ # particularly slow.
86
+ config.profile_examples = 10
87
+
88
+ # Run specs in random order to surface order dependencies. If you find an
89
+ # order dependency and want to debug it, you can fix the order by providing
90
+ # the seed, which is printed after each run.
91
+ # --seed 1234
92
+ config.order = :random
93
+
94
+ # Seed global randomization in this process using the `--seed` CLI option.
95
+ # Setting this allows you to use `--seed` to deterministically reproduce
96
+ # test failures related to randomization by passing the same `--seed` value
97
+ # as the one that triggered the failure.
98
+ Kernel.srand config.seed
99
+ =end
100
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elder_scrolls_plugin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Muriel Salvan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: riffola
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bindata
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json-diff
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.10'
69
+ description: Library reading Bethesda's plugins files (.esp, .esm and .esl) files.
70
+ Provides a simple API to access plugins' data organized in chunks
71
+ email:
72
+ - muriel@x-aeon.com
73
+ executables:
74
+ - esp_dump
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - bin/esp_dump
79
+ - lib/elder_scrolls_plugin.rb
80
+ - lib/elder_scrolls_plugin/version.rb
81
+ - spec/spec_helper.rb
82
+ homepage: https://github.com/Muriel-Salvan/elder_scrolls_plugin
83
+ licenses:
84
+ - BSD-4-Clause
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.1.2
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Elder Scrolls Plugin - Reading Bethesda's esp, esm and esl files
105
+ test_files: []