modsvaskr 0.1.11 → 0.2.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.
@@ -1,69 +1,70 @@
1
- require 'fileutils'
2
- require 'json'
3
- require 'modsvaskr/logger'
4
- require 'modsvaskr/run_cmd'
5
-
6
- module Modsvaskr
7
-
8
- # Common functionality for any tests suite
9
- class TestsSuite
10
-
11
- include Logger, RunCmd
12
-
13
- # Constructor
14
- #
15
- # Parameters::
16
- # * *tests_suite* (Symbol): The tests suite name
17
- # * *game* (Game): The game for which this test type is instantiated
18
- def initialize(tests_suite, game)
19
- @tests_suite = tests_suite
20
- @game = game
21
- end
22
-
23
- # Get test statuses
24
- #
25
- # Result::
26
- # * Array<[String, String]>: Ordered list of [test name, test status]
27
- def statuses
28
- File.exist?(json_statuses_file) ? JSON.parse(File.read(json_statuses_file)) : []
29
- end
30
-
31
- # Set test statuses.
32
- # Add new ones and overwrites existing ones.
33
- #
34
- # Parameters::
35
- # * *statuses* (Array<[String, String]>): Ordered list of [test name, test status]
36
- def set_statuses(statuses)
37
- current_statuses = self.statuses
38
- statuses.each do |(test_name, test_status)|
39
- test_status_info = current_statuses.find { |(search_test_name, _search_test_status)| search_test_name == test_name }
40
- if test_status_info.nil?
41
- # New one. Add it to the end.
42
- current_statuses << [test_name, test_status]
43
- else
44
- # Already existing. Just change its status.
45
- test_status_info[1] = test_status
46
- end
47
- end
48
- FileUtils.mkdir_p File.dirname(json_statuses_file)
49
- File.write(json_statuses_file, JSON.pretty_generate(current_statuses))
50
- end
51
-
52
- # Remove all tests from this suite
53
- def clear_tests
54
- File.unlink(json_statuses_file) if File.exist?(json_statuses_file)
55
- end
56
-
57
- private
58
-
59
- # Get the JSON statuses file name
60
- #
61
- # Result::
62
- # * String: The JSON statuses file name
63
- def json_statuses_file
64
- "#{@game.path}/Data/Modsvaskr/Tests/Statuses_#{@tests_suite}.json"
65
- end
66
-
67
- end
68
-
69
- end
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'modsvaskr/logger'
4
+ require 'modsvaskr/run_cmd'
5
+
6
+ module Modsvaskr
7
+
8
+ # Common functionality for any tests suite
9
+ class TestsSuite
10
+
11
+ include RunCmd
12
+ include Logger
13
+
14
+ # Constructor
15
+ #
16
+ # Parameters::
17
+ # * *tests_suite* (Symbol): The tests suite name
18
+ # * *game* (Game): The game for which this test type is instantiated
19
+ def initialize(tests_suite, game)
20
+ @tests_suite = tests_suite
21
+ @game = game
22
+ end
23
+
24
+ # Get test statuses
25
+ #
26
+ # Result::
27
+ # * Array<[String, String]>: Ordered list of [test name, test status]
28
+ def statuses
29
+ File.exist?(json_statuses_file) ? JSON.parse(File.read(json_statuses_file)) : []
30
+ end
31
+
32
+ # Set test statuses.
33
+ # Add new ones and overwrites existing ones.
34
+ #
35
+ # Parameters::
36
+ # * *statuses* (Array<[String, String]>): Ordered list of [test name, test status]
37
+ def statuses=(statuses)
38
+ current_statuses = self.statuses
39
+ statuses.each do |(test_name, test_status)|
40
+ test_status_info = current_statuses.find { |(search_test_name, _search_test_status)| search_test_name == test_name }
41
+ if test_status_info.nil?
42
+ # New one. Add it to the end.
43
+ current_statuses << [test_name, test_status]
44
+ else
45
+ # Already existing. Just change its status.
46
+ test_status_info[1] = test_status
47
+ end
48
+ end
49
+ FileUtils.mkdir_p File.dirname(json_statuses_file)
50
+ File.write(json_statuses_file, JSON.pretty_generate(current_statuses))
51
+ end
52
+
53
+ # Remove all tests from this suite
54
+ def clear_tests
55
+ FileUtils.rm_f(json_statuses_file)
56
+ end
57
+
58
+ private
59
+
60
+ # Get the JSON statuses file name
61
+ #
62
+ # Result::
63
+ # * String: The JSON statuses file name
64
+ def json_statuses_file
65
+ "#{@game.path}/Data/Modsvaskr/Tests/Statuses_#{@tests_suite}.json"
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -1,117 +1,120 @@
1
- require 'modsvaskr/in_game_tests_suite'
2
-
3
- module Modsvaskr
4
-
5
- module TestsSuites
6
-
7
- class ExteriorCell < TestsSuite
8
-
9
- include InGameTestsSuite
10
-
11
- # Return the in-game tests suite to which we forward the tests to be run
12
- #
13
- # Result::
14
- # * Symbol: In-game tests suite
15
- def in_game_tests_suite
16
- :locations
17
- end
18
-
19
- # Discover the list of tests information that could be run.
20
- # [API] - This method is mandatory
21
- #
22
- # Result::
23
- # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
24
- def discover_tests
25
- # List of exterior cells coordinates, per worldspace name, per plugin name
26
- # Hash< String, Hash< String, Array<[Integer, Integer]> > >
27
- exterior_cells = {}
28
- @game.xedit.run_script('DumpInfo', only_once: true)
29
- @game.xedit.parse_csv('Modsvaskr_ExportedDumpInfo') do |row|
30
- esp_name, record_type = row[0..1]
31
- if record_type.downcase == 'cell'
32
- cell_type, cell_name, cell_x, cell_y = row[3..6]
33
- if cell_type == 'cow'
34
- if cell_x.nil?
35
- log "!!! Invalid record: #{row}"
36
- else
37
- esp_name.downcase!
38
- exterior_cells[esp_name] = {} unless exterior_cells.key?(esp_name)
39
- exterior_cells[esp_name][cell_name] = [] unless exterior_cells[esp_name].key?(cell_name)
40
- exterior_cells[esp_name][cell_name] << [Integer(cell_x), Integer(cell_y)]
41
- end
42
- end
43
- end
44
- end
45
- # Test only exterior cells that have been changed by mods, and make sure we test the minimum, knowing that each cell loaded in game tests 5x5 cells around
46
- vanilla_esps = @game.game_esps
47
- vanilla_exterior_cells = vanilla_esps.inject({}) do |merged_worldspaces, esp_name|
48
- merged_worldspaces.merge(exterior_cells[esp_name] || {}) do |worldspace, ext_cells1, ext_cells2|
49
- (ext_cells1 + ext_cells2).sort.uniq
50
- end
51
- end
52
- changed_exterior_cells = {}
53
- exterior_cells.each do |esp_name, esp_exterior_cells|
54
- unless vanilla_esps.include?(esp_name)
55
- esp_exterior_cells.each do |worldspace, worldspace_exterior_cells|
56
- if vanilla_exterior_cells.key?(worldspace)
57
- changed_exterior_cells[worldspace] = [] unless changed_exterior_cells.key?(worldspace)
58
- changed_exterior_cells[worldspace].concat(vanilla_exterior_cells[worldspace] & worldspace_exterior_cells)
59
- end
60
- end
61
- end
62
- end
63
- tests = {}
64
- # Value taken from the ini file
65
- # TODO: Read it from there (uiGrid)
66
- loaded_grid = 5
67
- delta_cells = loaded_grid / 2
68
- changed_exterior_cells.each do |worldspace, worldspace_exterior_cells|
69
- # Make sure we select the minimum cells
70
- # Use a Hash of Hashes for the coordinates to speed-up their lookup.
71
- remaining_cells = {}
72
- worldspace_exterior_cells.each do |(cell_x, cell_y)|
73
- remaining_cells[cell_x] = {} unless remaining_cells.key?(cell_x)
74
- remaining_cells[cell_x][cell_y] = nil
75
- end
76
- while !remaining_cells.empty?
77
- cell_x, cell_ys = remaining_cells.first
78
- cell_y, _nil = cell_ys.first
79
- # We want to test cell_x, cell_y.
80
- # Knowing that we can test it by loading any cell in the range ((cell_x - delta_cells..cell_x + delta_cells), (cell_y - delta_cells..cell_y + delta_cells)),
81
- # check which cell would test the most wanted cells from our list
82
- best_cell_x, best_cell_y, best_cell_score = nil, nil, nil
83
- (cell_x - delta_cells..cell_x + delta_cells).each do |candidate_cell_x|
84
- (cell_y - delta_cells..cell_y + delta_cells).each do |candidate_cell_y|
85
- # Check the number of cells that would be tested if we were to test (candidate_cell_x, candidate_cell_y)
86
- nbr_tested_cells = remaining_cells.
87
- slice(*(candidate_cell_x - delta_cells..candidate_cell_x + delta_cells)).
88
- inject(0) { |sum_cells, (_cur_cell_x, cur_cell_ys)| sum_cells + cur_cell_ys.slice(*(candidate_cell_y - delta_cells..candidate_cell_y + delta_cells)).size }
89
- if best_cell_score.nil? || nbr_tested_cells > best_cell_score
90
- best_cell_score = nbr_tested_cells
91
- best_cell_x = candidate_cell_x
92
- best_cell_y = candidate_cell_y
93
- end
94
- end
95
- end
96
- # Remove the tested cells from the remaining ones
97
- (best_cell_x - delta_cells..best_cell_x + delta_cells).each do |cur_cell_x|
98
- if remaining_cells.key?(cur_cell_x)
99
- (best_cell_y - delta_cells..best_cell_y + delta_cells).each do |cur_cell_y|
100
- remaining_cells[cur_cell_x].delete(cur_cell_y)
101
- end
102
- remaining_cells.delete(cur_cell_x) if remaining_cells[cur_cell_x].empty?
103
- end
104
- end
105
- tests["#{worldspace}/#{best_cell_x}/#{best_cell_y}"] = {
106
- name: "Load #{worldspace} cell #{best_cell_x}, #{best_cell_y}"
107
- }
108
- end
109
- end
110
- tests
111
- end
112
-
113
- end
114
-
115
- end
116
-
117
- end
1
+ require 'modsvaskr/in_game_tests_suite'
2
+
3
+ module Modsvaskr
4
+
5
+ module TestsSuites
6
+
7
+ # Test exterior cells by using cow and camera spanning
8
+ class ExteriorCell < TestsSuite
9
+
10
+ include InGameTestsSuite
11
+
12
+ # Return the in-game tests suite to which we forward the tests to be run
13
+ #
14
+ # Result::
15
+ # * Symbol: In-game tests suite
16
+ def in_game_tests_suite
17
+ :locations
18
+ end
19
+
20
+ # Discover the list of tests information that could be run.
21
+ # [API] - This method is mandatory
22
+ #
23
+ # Result::
24
+ # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
25
+ def discover_tests
26
+ # List of exterior cells coordinates, per worldspace name, per plugin name
27
+ # Hash< String, Hash< String, Array<[Integer, Integer]> > >
28
+ exterior_cells = {}
29
+ @game.xedit.run_script('DumpInfo', only_once: true)
30
+ @game.xedit.parse_csv('Modsvaskr_ExportedDumpInfo') do |row|
31
+ esp_name, record_type = row[0..1]
32
+ if record_type.downcase == 'cell'
33
+ cell_type, cell_name, cell_x, cell_y = row[3..6]
34
+ if cell_type == 'cow'
35
+ if cell_x.nil?
36
+ log "!!! Invalid record: #{row}"
37
+ else
38
+ esp_name.downcase!
39
+ exterior_cells[esp_name] = {} unless exterior_cells.key?(esp_name)
40
+ exterior_cells[esp_name][cell_name] = [] unless exterior_cells[esp_name].key?(cell_name)
41
+ exterior_cells[esp_name][cell_name] << [Integer(cell_x), Integer(cell_y)]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ # Test only exterior cells that have been changed by mods, and make sure we test the minimum, knowing that each cell loaded in game tests 5x5 cells around
47
+ vanilla_esps = @game.game_esps
48
+ vanilla_exterior_cells = vanilla_esps.inject({}) do |merged_worldspaces, esp_name|
49
+ merged_worldspaces.merge(exterior_cells[esp_name] || {}) do |_worldspace, ext_cells_1, ext_cells_2|
50
+ (ext_cells_1 + ext_cells_2).sort.uniq
51
+ end
52
+ end
53
+ changed_exterior_cells = {}
54
+ exterior_cells.each do |esp_name, esp_exterior_cells|
55
+ next if vanilla_esps.include?(esp_name)
56
+
57
+ esp_exterior_cells.each do |worldspace, worldspace_exterior_cells|
58
+ if vanilla_exterior_cells.key?(worldspace)
59
+ changed_exterior_cells[worldspace] = [] unless changed_exterior_cells.key?(worldspace)
60
+ changed_exterior_cells[worldspace].concat(vanilla_exterior_cells[worldspace] & worldspace_exterior_cells)
61
+ end
62
+ end
63
+ end
64
+ tests = {}
65
+ # Value taken from the ini file
66
+ # TODO: Read it from there (uiGrid)
67
+ loaded_grid = 5
68
+ delta_cells = loaded_grid / 2
69
+ changed_exterior_cells.each do |worldspace, worldspace_exterior_cells|
70
+ # Make sure we select the minimum cells
71
+ # Use a Hash of Hashes for the coordinates to speed-up their lookup.
72
+ remaining_cells = {}
73
+ worldspace_exterior_cells.each do |(cell_x, cell_y)|
74
+ remaining_cells[cell_x] = {} unless remaining_cells.key?(cell_x)
75
+ remaining_cells[cell_x][cell_y] = nil
76
+ end
77
+ until remaining_cells.empty?
78
+ cell_x, cell_ys = remaining_cells.first
79
+ cell_y, _nil = cell_ys.first
80
+ # We want to test cell_x, cell_y.
81
+ # Knowing that we can test it by loading any cell in the range ((cell_x - delta_cells..cell_x + delta_cells), (cell_y - delta_cells..cell_y + delta_cells)),
82
+ # check which cell would test the most wanted cells from our list
83
+ best_cell_x = nil
84
+ best_cell_y = nil
85
+ best_cell_score = nil
86
+ (cell_x - delta_cells..cell_x + delta_cells).each do |candidate_cell_x|
87
+ (cell_y - delta_cells..cell_y + delta_cells).each do |candidate_cell_y|
88
+ # Check the number of cells that would be tested if we were to test (candidate_cell_x, candidate_cell_y)
89
+ nbr_tested_cells = remaining_cells.
90
+ slice(*(candidate_cell_x - delta_cells..candidate_cell_x + delta_cells)).
91
+ inject(0) { |sum_cells, (_cur_cell_x, cur_cell_ys)| sum_cells + cur_cell_ys.slice(*(candidate_cell_y - delta_cells..candidate_cell_y + delta_cells)).size }
92
+ next unless best_cell_score.nil? || nbr_tested_cells > best_cell_score
93
+
94
+ best_cell_score = nbr_tested_cells
95
+ best_cell_x = candidate_cell_x
96
+ best_cell_y = candidate_cell_y
97
+ end
98
+ end
99
+ # Remove the tested cells from the remaining ones
100
+ (best_cell_x - delta_cells..best_cell_x + delta_cells).each do |cur_cell_x|
101
+ next unless remaining_cells.key?(cur_cell_x)
102
+
103
+ (best_cell_y - delta_cells..best_cell_y + delta_cells).each do |cur_cell_y|
104
+ remaining_cells[cur_cell_x].delete(cur_cell_y)
105
+ end
106
+ remaining_cells.delete(cur_cell_x) if remaining_cells[cur_cell_x].empty?
107
+ end
108
+ tests["#{worldspace}/#{best_cell_x}/#{best_cell_y}"] = {
109
+ name: "Load #{worldspace} cell #{best_cell_x}, #{best_cell_y}"
110
+ }
111
+ end
112
+ end
113
+ tests
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -1,63 +1,63 @@
1
- require 'modsvaskr/in_game_tests_suite'
2
-
3
- module Modsvaskr
4
-
5
- module TestsSuites
6
-
7
- class InteriorCell < TestsSuite
8
-
9
- include InGameTestsSuite
10
-
11
- # Return the in-game tests suite to which we forward the tests to be run
12
- #
13
- # Result::
14
- # * Symbol: In-game tests suite
15
- def in_game_tests_suite
16
- :locations
17
- end
18
-
19
- # Discover the list of tests information that could be run.
20
- # [API] - This method is mandatory
21
- #
22
- # Result::
23
- # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
24
- def discover_tests
25
- # List of interior cells, per plugin name
26
- # Hash< String, Array<String> >
27
- interior_cells = {}
28
- @game.xedit.run_script('DumpInfo', only_once: true)
29
- @game.xedit.parse_csv('Modsvaskr_ExportedDumpInfo') do |row|
30
- esp_name, record_type = row[0..1]
31
- if record_type.downcase == 'cell'
32
- cell_type, cell_name = row[3..4]
33
- if cell_type == 'coc'
34
- esp_name.downcase!
35
- interior_cells[esp_name] = [] unless interior_cells.key?(esp_name)
36
- interior_cells[esp_name] << cell_name
37
- end
38
- end
39
- end
40
- # Test only interior cells that have been changed by mods
41
- vanilla_esps = @game.game_esps
42
- vanilla_interior_cells = vanilla_esps.map { |esp_name| interior_cells[esp_name] || [] }.flatten.sort.uniq
43
- Hash[interior_cells.
44
- map { |esp_name, esp_cells| vanilla_esps.include?(esp_name) ? [] : vanilla_interior_cells & esp_cells }.
45
- flatten.
46
- sort.
47
- uniq.
48
- map do |cell_name|
49
- [
50
- cell_name,
51
- {
52
- name: "Load cell #{cell_name}"
53
- }
54
- ]
55
- end
56
- ]
57
- end
58
-
59
- end
60
-
61
- end
62
-
63
- end
1
+ require 'modsvaskr/in_game_tests_suite'
2
+
3
+ module Modsvaskr
4
+
5
+ module TestsSuites
6
+
7
+ # Test interior cells by using coc and camera spanning
8
+ class InteriorCell < TestsSuite
9
+
10
+ include InGameTestsSuite
11
+
12
+ # Return the in-game tests suite to which we forward the tests to be run
13
+ #
14
+ # Result::
15
+ # * Symbol: In-game tests suite
16
+ def in_game_tests_suite
17
+ :locations
18
+ end
19
+
20
+ # Discover the list of tests information that could be run.
21
+ # [API] - This method is mandatory
22
+ #
23
+ # Result::
24
+ # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
25
+ def discover_tests
26
+ # List of interior cells, per plugin name
27
+ # Hash< String, Array<String> >
28
+ interior_cells = {}
29
+ @game.xedit.run_script('DumpInfo', only_once: true)
30
+ @game.xedit.parse_csv('Modsvaskr_ExportedDumpInfo') do |row|
31
+ esp_name, record_type = row[0..1]
32
+ if record_type.downcase == 'cell'
33
+ cell_type, cell_name = row[3..4]
34
+ if cell_type == 'coc'
35
+ esp_name.downcase!
36
+ interior_cells[esp_name] = [] unless interior_cells.key?(esp_name)
37
+ interior_cells[esp_name] << cell_name
38
+ end
39
+ end
40
+ end
41
+ # Test only interior cells that have been changed by mods
42
+ vanilla_esps = @game.game_esps
43
+ vanilla_interior_cells = vanilla_esps.map { |esp_name| interior_cells[esp_name] || [] }.flatten.sort.uniq
44
+ interior_cells.
45
+ map { |esp_name, esp_cells| vanilla_esps.include?(esp_name) ? [] : vanilla_interior_cells & esp_cells }.
46
+ flatten.
47
+ sort.
48
+ uniq.
49
+ to_h do |cell_name|
50
+ [
51
+ cell_name,
52
+ {
53
+ name: "Load cell #{cell_name}"
54
+ }
55
+ ]
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -1,64 +1,67 @@
1
- require 'modsvaskr/in_game_tests_suite'
2
-
3
- module Modsvaskr
4
-
5
- module TestsSuites
6
-
7
- class Npc < TestsSuite
8
-
9
- include InGameTestsSuite
10
-
11
- # Return the in-game tests suite to which we forward the tests to be run
12
- #
13
- # Result::
14
- # * Symbol: In-game tests suite
15
- def in_game_tests_suite
16
- :npcs
17
- end
18
-
19
- # Discover the list of tests information that could be run.
20
- # [API] - This method is mandatory
21
- #
22
- # Result::
23
- # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
24
- def discover_tests
25
- tests = {}
26
- @game.xedit.run_script('DumpInfo', only_once: true)
27
- # Keep track of masters, per plugin
28
- # Hash<String, Array<String> >
29
- masters = {}
30
- # Keep track of NPCs
31
- # Array< [String, Integer, String] >
32
- # Array< [Plugin, FormID, NPC ] >
33
- npcs = []
34
- @game.xedit.parse_csv('Modsvaskr_ExportedDumpInfo') do |row|
35
- case row[1].downcase
36
- when 'npc_'
37
- npcs << [row[0].downcase, row[2].to_i(16), row[3]]
38
- when 'tes4'
39
- masters[row[0].downcase] = row[3..-1].map(&:downcase)
40
- end
41
- end
42
- npcs.each do |(esp, form_id, npc_name)|
43
- raise "Esp #{esp} declares NPC FormID #{form_id} (#{npc_name}) but its masters could not be parsed" unless masters.key?(esp)
44
- # Know from which mod this ID comes from
45
- mod_idx = form_id / 16_777_216
46
- raise "NPC FormID #{form_id} (#{npc_name}) from #{esp} references an unknown master (known masters: #{masters[esp].join(', ')})" if mod_idx > masters[esp].size
47
- test_name = "#{mod_idx == masters[esp].size ? esp : masters[esp][mod_idx]}/#{form_id % 16_777_216}"
48
- if tests.key?(test_name)
49
- # Add the name of the mod to the description, so that we know which mod modifies which NPC.
50
- tests[test_name][:name] << "/#{esp}"
51
- else
52
- tests[test_name] = {
53
- name: "Take screenshot of #{npc_name} - #{esp}"
54
- }
55
- end
56
- end
57
- tests
58
- end
59
-
60
- end
61
-
62
- end
63
-
64
- end
1
+ require 'modsvaskr/in_game_tests_suite'
2
+
3
+ module Modsvaskr
4
+
5
+ module TestsSuites
6
+
7
+ # Test NPCs by taking screenshots
8
+ class Npc < TestsSuite
9
+
10
+ include InGameTestsSuite
11
+
12
+ # Return the in-game tests suite to which we forward the tests to be run
13
+ #
14
+ # Result::
15
+ # * Symbol: In-game tests suite
16
+ def in_game_tests_suite
17
+ :npcs
18
+ end
19
+
20
+ # Discover the list of tests information that could be run.
21
+ # [API] - This method is mandatory
22
+ #
23
+ # Result::
24
+ # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
25
+ def discover_tests
26
+ tests = {}
27
+ @game.xedit.run_script('DumpInfo', only_once: true)
28
+ # Keep track of masters, per plugin
29
+ # Hash<String, Array<String> >
30
+ masters = {}
31
+ # Keep track of NPCs
32
+ # Array< [String, Integer, String] >
33
+ # Array< [Plugin, FormID, NPC ] >
34
+ npcs = []
35
+ @game.xedit.parse_csv('Modsvaskr_ExportedDumpInfo') do |row|
36
+ case row[1].downcase
37
+ when 'npc_'
38
+ npcs << [row[0].downcase, row[2].to_i(16), row[3]]
39
+ when 'tes4'
40
+ masters[row[0].downcase] = row[3..].map(&:downcase)
41
+ end
42
+ end
43
+ npcs.each do |(esp, form_id, npc_name)|
44
+ raise "Esp #{esp} declares NPC FormID #{form_id} (#{npc_name}) but its masters could not be parsed" unless masters.key?(esp)
45
+
46
+ # Know from which mod this ID comes from
47
+ mod_idx = form_id / 16_777_216
48
+ raise "NPC FormID #{form_id} (#{npc_name}) from #{esp} references an unknown master (known masters: #{masters[esp].join(', ')})" if mod_idx > masters[esp].size
49
+
50
+ test_name = "#{mod_idx == masters[esp].size ? esp : masters[esp][mod_idx]}/#{form_id % 16_777_216}"
51
+ if tests.key?(test_name)
52
+ # Add the name of the mod to the description, so that we know which mod modifies which NPC.
53
+ tests[test_name][:name] << "/#{esp}"
54
+ else
55
+ tests[test_name] = {
56
+ name: "Take screenshot of #{npc_name} - #{esp}"
57
+ }
58
+ end
59
+ end
60
+ tests
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end