elder_scrolls_plugin 0.0.1

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.
@@ -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: []