modsvaskr 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: 35bcc4ccf0026926ed167c88235b0da1d1b86735e348b9a5059203ed4389ab89
4
+ data.tar.gz: 550b218d7b1f3d5c779d68a8e303da98117484219d2009848a31b5f51a3714a9
5
+ SHA512:
6
+ metadata.gz: deefc507a16b05a96c799f3749dc519d9b3c286bb8a035066e844102e4c6a86138642bd057046b1bd6ea06493a95344d9282eae514920dcfe477b624c8f41f86
7
+ data.tar.gz: d7b7d7e0944f24eb07cbbbcc0f9bc02c56b687ef4d9dcc8c5edbe3a136c23720e58c61583179bb4b50eac69c698ed53567f66466e743a5db03b1b86cb3a130e4
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # require 'mod_organizer'
3
+ require 'modsvaskr/config'
4
+ require 'modsvaskr/ui'
5
+
6
+ begin
7
+ Modsvaskr::Ui.new(config: Modsvaskr::Config.new('./modsvaskr.yaml')).run
8
+ rescue
9
+ puts "Unhandled exception: #{$!}\n#{$!.backtrace.join("\n")}"
10
+ puts 'Press Enter to exit.'
11
+ $stdin.gets
12
+ end
@@ -0,0 +1,42 @@
1
+ require 'yaml'
2
+ require 'modsvaskr/game'
3
+ require 'modsvaskr/xedit'
4
+
5
+ module Modsvaskr
6
+
7
+ # Configuration
8
+ class Config
9
+
10
+ # Constructor
11
+ #
12
+ # Parameters::
13
+ # * *file* (String): File containing configuration
14
+ def initialize(file)
15
+ @config = YAML.load(File.read(file))
16
+ end
17
+
18
+ # Get the games list
19
+ #
20
+ # Result::
21
+ # * Array<Game>: List of games
22
+ def games
23
+ unless defined?(@games)
24
+ @games = @config['games'].map do |game_info|
25
+ require "#{__dir__}/games/#{game_info['type']}.rb"
26
+ Games.const_get(game_info['type'].to_s.split('_').collect(&:capitalize).join.to_sym).new(self, game_info)
27
+ end
28
+ end
29
+ @games
30
+ end
31
+
32
+ # Return the xEdit path
33
+ #
34
+ # Result::
35
+ # * String: The xEdit path
36
+ def xedit_path
37
+ @config['xedit']
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,61 @@
1
+ require 'yaml'
2
+ require 'modsvaskr/logger'
3
+ require 'modsvaskr/run_cmd'
4
+
5
+ module Modsvaskr
6
+
7
+ # Common functionality for any Game
8
+ class Game
9
+
10
+ include Logger, RunCmd
11
+
12
+ # Constructor
13
+ #
14
+ # Parameters::
15
+ # * *config* (Config): The config
16
+ # * *game_info* (Hash<String,Object>): Game info:
17
+ # * *name* (String): Game name
18
+ # * *path* (String): Game installation dir
19
+ def initialize(config, game_info)
20
+ @config = config
21
+ @game_info = game_info
22
+ @name = name
23
+ init if self.respond_to?(:init)
24
+ end
25
+
26
+ # Return the game name
27
+ #
28
+ # Result::
29
+ # * String: Game name
30
+ def name
31
+ @game_info['name']
32
+ end
33
+
34
+ # Return the game path
35
+ #
36
+ # Result::
37
+ # * String: Game path
38
+ def path
39
+ @game_info['path']
40
+ end
41
+
42
+ # Return the launch executable
43
+ #
44
+ # Result::
45
+ # * String: Launch executable
46
+ def launch_exe
47
+ @game_info['launch_exe']
48
+ end
49
+
50
+ # Return an xEdit instance for this game
51
+ #
52
+ # Result::
53
+ # * Xedit: The xEdit instance
54
+ def xedit
55
+ @xedit = Xedit.new(@config.xedit_path, path) unless defined?(@xedit)
56
+ @xedit
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,91 @@
1
+ require 'open-uri'
2
+ require 'tmpdir'
3
+ require 'nokogiri'
4
+
5
+ module Modsvaskr
6
+
7
+ module Games
8
+
9
+ # Handle a Skyrim installation
10
+ class SkyrimSe < Game
11
+
12
+ SEVEN_ZIP_CMD = {
13
+ dir: 'C:\Program Files\7-Zip',
14
+ exe: '7z.exe'
15
+ }
16
+
17
+ # Initialize the game
18
+ # [API] - This method is optional
19
+ def init
20
+ @tmp_dir = "#{Dir.tmpdir}/modsvaskr"
21
+ end
22
+
23
+ # Complete the game menu
24
+ # [API] - This method is optional
25
+ #
26
+ # Parameters::
27
+ # * *menu* (CursesMenu): Menu to complete
28
+ def complete_game_menu(menu)
29
+ menu.item 'Install SKSE64' do
30
+ install_skse64
31
+ puts 'Press Enter to continue...'
32
+ $stdin.gets
33
+ end
34
+ end
35
+
36
+ # Get the game running executable name (that can be found in a tasks manager)
37
+ # [API] - This method is mandatory
38
+ #
39
+ # Result::
40
+ # * String: The running exe name
41
+ def running_exe
42
+ 'SkyrimSE.exe'
43
+ end
44
+
45
+ # List of default esps present in the game (the ones in the Data folder when 0 mod is being used)
46
+ #
47
+ # Result::
48
+ # * Array<String>: List of esp/esm/esl base file names.
49
+ def game_esps
50
+ %w[
51
+ skyrim.esm
52
+ update.esm
53
+ dawnguard.esm
54
+ hearthfires.esm
55
+ dragonborn.esm
56
+ ]
57
+ end
58
+
59
+ private
60
+
61
+ # Install SKSE64 corresponding to our game
62
+ def install_skse64
63
+ doc = Nokogiri::HTML(open('https://skse.silverlock.org/'))
64
+ p_element = doc.css('p').find { |el| el.text.strip =~ /^Current SE build .+: 7z archive$/ }
65
+ if p_element.nil?
66
+ log '!!! Can\'t get SKSE64 from https://skse.silverlock.org/. It looks like the page structure has changed. Please update the code or install it manually.'
67
+ else
68
+ url = "https://skse.silverlock.org/#{p_element.at('a')['href']}"
69
+ path = "#{@tmp_dir}/skse64.7z"
70
+ FileUtils.mkdir_p File.dirname(path)
71
+ log "Download from #{url} => #{path}..."
72
+ open(url, 'rb') do |web_io|
73
+ File.write(path, web_io.read, mode: 'wb')
74
+ end
75
+ skse64_tmp_dir = "#{@tmp_dir}/skse64"
76
+ log "Unzip into #{skse64_tmp_dir}..."
77
+ FileUtils.rm_rf skse64_tmp_dir
78
+ FileUtils.mkdir_p skse64_tmp_dir
79
+ run_cmd SEVEN_ZIP_CMD, args: ['x', "\"#{path}\"", "-o\"#{skse64_tmp_dir}\"", '-r']
80
+ skse64_subdir = Dir.glob("#{skse64_tmp_dir}/*").first
81
+ log "Move files from #{skse64_subdir} to #{self.path}..."
82
+ FileUtils.cp_r "#{skse64_subdir}/.", self.path, remove_destination: true
83
+ log 'SKSE64 installed successfully.'
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,25 @@
1
+ module Modsvaskr
2
+
3
+ # Mixin adding logging functionality, both on screen and in file
4
+ module Logger
5
+
6
+ class << self
7
+ attr_accessor :log_file
8
+ end
9
+ @log_file = File.expand_path('Modsvaskr.log')
10
+
11
+ # Log on screen and in log file
12
+ #
13
+ # Parameters::
14
+ # * *msg* (String): Message to log
15
+ def log(msg)
16
+ complete_msg = "[ #{Time.now.strftime('%F %T')} ] - #{msg}"
17
+ puts complete_msg
18
+ File.open(Logger.log_file, 'a') do |f|
19
+ f.puts complete_msg
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,22 @@
1
+ module Modsvaskr
2
+
3
+ module RunCmd
4
+
5
+ # Run a given command with eventual parameters
6
+ #
7
+ # Parameters::
8
+ # * *cmd* (Hash<Symbol,Object>): The command description:
9
+ # * *dir* (String): Directory in which the command is found
10
+ # * *exe* (String): Name of the executable of the command
11
+ # * *args* (Array<String>): Default arguments of the command [default = []]
12
+ # * *args* (Array<String>): Additional arguments to give the command [default: []]
13
+ def run_cmd(cmd, args: [])
14
+ Dir.chdir cmd[:dir] do
15
+ cmd_line = "#{cmd[:exe]} #{((cmd.key?(:args) ? cmd[:args] : []) + args).join(' ')}"
16
+ raise "Unable to execute command line from \"#{dir}\": #{cmd_line}" unless system cmd_line
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,430 @@
1
+ require 'json'
2
+ require 'time'
3
+ require 'modsvaskr/tests_suite'
4
+
5
+ module Modsvaskr
6
+
7
+ class TestsRunner
8
+
9
+ include Logger
10
+
11
+ # Constructor.
12
+ # Default values are for a standard Skyrim SE installation.
13
+ #
14
+ # Parameters::
15
+ # * *game* (Game): Game for which we run tests
16
+ # * *tests_poll_secs* (Integer): Number of seconds to poll for the game run and tests changes [default: 5]
17
+ # * *timeout_frozen_tests_secs* (Integer): Time out (in seconds) between tests updates: if no test has been updated before this timeout, consider the game has crashed. [default: 300]
18
+ def initialize(
19
+ game,
20
+ tests_poll_secs: 5,
21
+ timeout_frozen_tests_secs: 300
22
+ )
23
+ @game = game
24
+ @tests_poll_secs = tests_poll_secs
25
+ @timeout_frozen_tests_secs = timeout_frozen_tests_secs
26
+ # Parse tests suites
27
+ @tests_suites = Hash[Dir.glob("#{__dir__}/tests_suites/*.rb").map do |tests_suite_file|
28
+ tests_suite = File.basename(tests_suite_file, '.rb').to_sym
29
+ require "#{__dir__}/tests_suites/#{tests_suite}.rb"
30
+ [
31
+ tests_suite,
32
+ TestsSuites.const_get(tests_suite.to_s.split('_').collect(&:capitalize).join.to_sym).new(tests_suite, @game)
33
+ ]
34
+ end]
35
+ @tests_info_file = "#{@game.path}/Data/Modsvaskr/Tests/TestsInfo.json"
36
+ end
37
+
38
+ # Return tests suites
39
+ #
40
+ # Result::
41
+ # * Array<Symbol>: List of tests suites
42
+ def tests_suites
43
+ @tests_suites.keys
44
+ end
45
+
46
+ # Return test names for a given tests suite
47
+ #
48
+ # Parameters::
49
+ # * *tests_suite* (Symbol): The tests suite
50
+ # Result::
51
+ # * Array<String>: Test names of this suite
52
+ def discover_tests_for(tests_suite)
53
+ discovered_tests = @tests_suites[tests_suite].discover_tests
54
+ # Complete our tests information
55
+ complete_info = tests_info
56
+ discovered_tests.each do |test_name, test_info|
57
+ complete_info[tests_suite] = {} unless complete_info.key?(tests_suite)
58
+ complete_info[tests_suite][test_name] = test_info
59
+ end
60
+ update_tests_info(complete_info)
61
+ discovered_tests.keys
62
+ end
63
+
64
+ # Get test statuses for a given tests suite
65
+ #
66
+ # Parameters::
67
+ # * *tests_suite* (Symbol): The tests suite
68
+ # Result::
69
+ # * Array<[String, String]>: Ordered list of couples [test name, test status]
70
+ def statuses_for(tests_suite)
71
+ @tests_suites[tests_suite].statuses
72
+ end
73
+
74
+ # Set test statuses for a given tests suite
75
+ #
76
+ # Parameters::
77
+ # * *tests_suite* (Symbol): The tests suite
78
+ # * *statuses* (Array<[String, String]>): Ordered list of couples [test name, test status])
79
+ def set_statuses_for(tests_suite, statuses)
80
+ @tests_suites[tests_suite].set_statuses(statuses)
81
+ end
82
+
83
+ # Return test information
84
+ #
85
+ # Parameters::
86
+ # * *tests_suite* (Symbol): The tests suite
87
+ # * *test_name* (String): The test name
88
+ # Result::
89
+ # * Hash<Symbol,Object>: The test information (all properties are optional):
90
+ # * *name* (String): The test full name
91
+ def test_info(tests_suite, test_name)
92
+ tests_info.dig(tests_suite, test_name) || {}
93
+ end
94
+
95
+ # Clear tests for a given tests suite
96
+ #
97
+ # Parameters::
98
+ # * *tests_suite* (Symbol): The tests suite
99
+ def clear_tests_for(tests_suite)
100
+ @tests_suites[tests_suite].clear_tests
101
+ end
102
+
103
+ # Run tests in a loop until they are all tested
104
+ #
105
+ # Parameters::
106
+ # * *selected_tests* (Hash<Symbol, Array<String> >): Ordered list of tests to be run, per tests suite
107
+ def run(selected_tests)
108
+ # Test names (ordered) to be performed in game, per tests suite
109
+ # Hash< Symbol, Array<String> >
110
+ in_game_tests = {}
111
+ selected_tests.each do |tests_suite, suite_selected_tests|
112
+ if @tests_suites[tests_suite].respond_to?(:run_test)
113
+ # Simple synchronous tests
114
+ suite_selected_tests.each do |test_name|
115
+ # Store statuses after each test just in case of crash
116
+ set_statuses_for(tests_suite, [[test_name, @tests_suites[tests_suite].run_test(test_name)]])
117
+ end
118
+ end
119
+ if @tests_suites[tests_suite].respond_to?(:auto_tests_for)
120
+ # We run the tests from the game itself, using AutoTest mod.
121
+ in_game_tests[tests_suite] = suite_selected_tests
122
+ end
123
+ end
124
+ unless in_game_tests.empty?
125
+ # Get the list of AutoTest we have to run and that we will monitor
126
+ in_game_auto_tests = in_game_tests.inject({}) do |merged_auto_tests, (tests_suite, suite_selected_tests)|
127
+ merged_auto_tests.merge(@tests_suites[tests_suite].auto_tests_for(suite_selected_tests)) do |auto_test_suite, auto_test_tests1, auto_test_tests2|
128
+ (auto_test_tests1 + auto_test_tests2).uniq
129
+ end
130
+ end
131
+ in_game_auto_tests.each do |auto_test_suite, auto_test_tests|
132
+ # Write the JSON file that contains the list of tests to run
133
+ File.write(
134
+ "#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{auto_test_suite}_Run.json",
135
+ JSON.pretty_generate(
136
+ 'stringList' => {
137
+ 'tests_to_run' => auto_test_tests
138
+ }
139
+ )
140
+ )
141
+ # Clear the AutoTest test statuses
142
+ File.unlink("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{auto_test_suite}_Statuses.json") if File.exist?("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{auto_test_suite}_Statuses.json")
143
+ end
144
+ auto_test_config_file = "#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_Config.json"
145
+ # Write the JSON file that contains the configuration of the AutoTest tests runner
146
+ File.write(
147
+ auto_test_config_file,
148
+ JSON.pretty_generate(
149
+ 'string' => {
150
+ 'on_start' => 'run',
151
+ 'on_stop' => 'exit'
152
+ }
153
+ )
154
+ )
155
+ puts ''
156
+ puts '=========================================='
157
+ puts '= In-game tests are about to be launched ='
158
+ puts '=========================================='
159
+ puts ''
160
+ puts 'Here is what you need to do once the game will be launched (don\'t launch it by yourself, the test framework will launch it for you):'
161
+ puts '* Load the game save you want to test (or start a new game).'
162
+ puts ''
163
+ puts 'This will execute all in-game tests automatically.'
164
+ puts ''
165
+ puts 'It is possible that the game crashes during tests:'
166
+ puts '* That\'s a normal situation, as tests don\'t mimick a realistic gaming experience, and the Bethesda engine is not meant to be stressed like that.'
167
+ puts '* In case of game crash (CTD), the Modsvaskr test framework will relaunch it automatically and resume testing from when it crashed.'
168
+ puts '* In case of repeated CTD on the same test, the Modsvaskr test framework will detect it and skip the crashing test automatically.'
169
+ puts '* In case of a game freeze without CTD, the Modsvaskr test framework will detect it after a few minutes and automatically kill the game before re-launching it to resume testing.'
170
+ puts ''
171
+ puts 'If you want to interrupt in-game testing: invoke the console with ~ key and type stop_tests followed by Enter.'
172
+ puts ''
173
+ puts 'Press enter to start in-game testing (this will lauch your game automatically)...'
174
+ $stdin.gets
175
+ idx_launch = 0
176
+ old_statuses = auto_test_statuses
177
+ loop do
178
+ launch_game(use_autoload: idx_launch > 0)
179
+ idx_launch += 1
180
+ monitor_game_tests
181
+ # Check tests again
182
+ new_statuses = auto_test_statuses
183
+ # Update the tests results based on what has been run in-game
184
+ in_game_tests.each do |tests_suite, suite_selected_tests|
185
+ @tests_suites[tests_suite].set_statuses(
186
+ @tests_suites[tests_suite].
187
+ parse_auto_tests_statuses_for(suite_selected_tests, new_statuses).
188
+ select { |(test_name, _test_status)| suite_selected_tests.include?(test_name) }
189
+ )
190
+ end
191
+ log 'Test statuses after game run:'
192
+ print_test_statuses(new_statuses)
193
+ # Careful as this JSON file can be written by Papyrus that treat strings as case insensitive.
194
+ # cf. https://github.com/xanderdunn/skaar/wiki/Common-Tasks
195
+ auto_test_config = Hash[JSON.parse(File.read(auto_test_config_file))['string'].map { |key, value| [key.downcase, value.downcase] }]
196
+ if auto_test_config.dig('stopped_by') == 'user'
197
+ log 'Tests have been stopped by user. Stop looping.'
198
+ break
199
+ end
200
+ if auto_test_config.dig('tests_execution') == 'end'
201
+ log 'Tests have finished running. Stop looping.'
202
+ break
203
+ end
204
+ if new_statuses == old_statuses
205
+ log 'No changes in AutoTest tests statuses in the game run. Stop looping.'
206
+ break
207
+ end
208
+ old_statuses = new_statuses
209
+ # We will start again. Leave some time to interrupt if we want.
210
+ log 'We are going to start again in 10 seconds. Press Enter now to interrupt it.'
211
+ key_pressed =
212
+ begin
213
+ Timeout.timeout(10) { $stdin.gets }
214
+ rescue
215
+ nil
216
+ end
217
+ if key_pressed
218
+ log 'Run interrupted by user.'
219
+ break
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ private
226
+
227
+ # Return all tests info.
228
+ # Keep a cache of it.
229
+ #
230
+ # Result::
231
+ # * Hash< Symbol, Hash< String, Hash<Symbol,Object> > >: The tests info, per test name, per tests suite
232
+ def tests_info
233
+ unless defined?(@tests_info_cache)
234
+ @tests_info_cache =
235
+ if File.exist?(@tests_info_file)
236
+ Hash[JSON.parse(File.read(@tests_info_file)).map do |tests_suite_str, tests_suite_info|
237
+ [
238
+ tests_suite_str.to_sym,
239
+ Hash[tests_suite_info.map { |test_name, test_info| [test_name, test_info.transform_keys(&:to_sym)] }]
240
+ ]
241
+ end]
242
+ else
243
+ {}
244
+ end
245
+ end
246
+ @tests_info_cache
247
+ end
248
+
249
+ # Update tests info.
250
+ #
251
+ # Parameters::
252
+ # * *tests_info* (Hash< Symbol, Hash< String, Hash<Symbol,Object> > >): The tests info, per test name, per tests suite
253
+ def update_tests_info(tests_info)
254
+ # Persist the tests information on disk
255
+ FileUtils.mkdir_p File.dirname(@tests_info_file)
256
+ File.write(@tests_info_file, JSON.pretty_generate(tests_info))
257
+ @tests_info_cache = tests_info
258
+ end
259
+
260
+ # Launch the game, and wait for launch to be successful (get a PID)
261
+ #
262
+ # Parameters::
263
+ # * *use_autoload* (Boolean): If true, then launch the game using AutoLoad
264
+ def launch_game(use_autoload:)
265
+ # Launch the game
266
+ @idx_launch = 0 unless defined?(@idx_launch)
267
+ Dir.chdir(@game.path) do
268
+ if use_autoload
269
+ log "Launch Game (##{@idx_launch}) using AutoLoad..."
270
+ system "\"#{@game.path}/Data/AutoLoad.cmd\" auto_test"
271
+ else
272
+ log "Launch Game (##{@idx_launch}) using configured launcher..."
273
+ system "\"#{@game.path}/#{@game.launch_exe}\""
274
+ end
275
+ end
276
+ @idx_launch += 1
277
+ # The game launches asynchronously, so just wait a little bit and check for the process
278
+ sleep 10
279
+ tasklist_stdout = nil
280
+ loop do
281
+ tasklist_stdout = `tasklist | find "#{@game.running_exe}"`.strip
282
+ break unless tasklist_stdout.empty?
283
+ log "#{@game.running_exe} is not running. Wait for its startup..."
284
+ sleep 1
285
+ end
286
+ @game_pid = Integer(tasklist_stdout.split(' ')[1])
287
+ log "#{@game.running_exe} has started with PID #{@game_pid}"
288
+ end
289
+
290
+ # Kill the game, and wait till it is killed
291
+ def kill_game
292
+ first_time = true
293
+ loop do
294
+ system "taskkill #{first_time ? '' : '/F'} /pid #{@game_pid}"
295
+ first_time = false
296
+ sleep 1
297
+ tasklist_stdout = `tasklist | find "#{@game.running_exe}"`.strip
298
+ break if tasklist_stdout.empty?
299
+ log "#{@game.running_exe} is still running. Wait for its kill..."
300
+ sleep 5
301
+ end
302
+ end
303
+
304
+ # Get the list of AutoTest statuses
305
+ #
306
+ # Result::
307
+ # * Hash<String, Array<[String, String]> >: Ordered list of AutoTest [test name, test status], per AutoTest tests suite
308
+ def auto_test_statuses
309
+ statuses = {}
310
+ `dir "#{@game.path}/Data/SKSE/Plugins/StorageUtilData" /B`.split("\n").each do |file|
311
+ if file =~ /^AutoTest_(.+)_Statuses\.json$/
312
+ auto_test_suite = $1
313
+ # Careful as this JSON file can be written by Papyrus that treat strings as case insensitive.
314
+ # cf. https://github.com/xanderdunn/skaar/wiki/Common-Tasks
315
+ statuses[auto_test_suite] = JSON.parse(File.read("#{@game.path}/Data/SKSE/Plugins/StorageUtilData/AutoTest_#{auto_test_suite}_Statuses.json"))['string'].map do |test_name, test_status|
316
+ [test_name.downcase, test_status.downcase]
317
+ end
318
+ end
319
+ end
320
+ statuses
321
+ end
322
+
323
+ # Loop while monitoring game tests progression.
324
+ # If the game exits, then exit also the loop.
325
+ # In case no test is being updated for a long period of time, kill the game (we consider it was frozen)
326
+ # Prerequisite: launch_game has to be called before.
327
+ def monitor_game_tests
328
+ log 'Start monitoring game testing...'
329
+ last_time_tests_changed = Time.now
330
+ # Get a picture of current tests
331
+ current_statuses = auto_test_statuses
332
+ loop do
333
+ still_running = true
334
+ begin
335
+ # Process.kill does not work when the game has crashed (the process is still detected as zombie)
336
+ # still_running = Process.kill(0, @game_pid) == 1
337
+ tasklist_stdout = `tasklist 2>&1`
338
+ still_running = tasklist_stdout.split("\n").any? { |line| line =~ /#{Regexp.escape(@game.running_exe)}/ }
339
+ # log "Tasklist returned no Skyrim:\n#{tasklist_stdout}" unless still_running
340
+ rescue Errno::ESRCH
341
+ log "Got error while waiting for #{@game.running_exe} PID: #{$!}"
342
+ still_running = false
343
+ end
344
+ # Log a diff in tests
345
+ new_statuses = auto_test_statuses
346
+ diff_statuses = diff_statuses(current_statuses, new_statuses)
347
+ unless diff_statuses.empty?
348
+ # Tests have progressed
349
+ last_time_tests_changed = Time.now
350
+ log '===== Test statuses changes:'
351
+ diff_statuses.each do |tests_suite, tests_statuses|
352
+ log "* #{tests_suite}:"
353
+ tests_statuses.each do |(test_name, test_status)|
354
+ log " * #{test_name}: #{test_status}"
355
+ end
356
+ end
357
+ end
358
+ break unless still_running
359
+ # If the tests haven't changed for too long, consider the game has frozen, but not crashed. So kill it.
360
+ if Time.now - last_time_tests_changed > @timeout_frozen_tests_secs
361
+ log "Last time test have changed is #{last_time_tests_changed.strftime('%F %T')}. Consider the game is frozen, so kill it."
362
+ kill_game
363
+ break
364
+ end
365
+ current_statuses = new_statuses
366
+ sleep @tests_poll_secs
367
+ end
368
+ log 'End monitoring game testing.'
369
+ end
370
+
371
+ # Diff between test statuses.
372
+ #
373
+ # Parameters::
374
+ # * *statuses1* (Hash<String, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
375
+ # * *statuses2* (Hash<String, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
376
+ # Result::
377
+ # * Hash<String, Array<[String, String]> >: statuses2 - statuses1
378
+ def diff_statuses(statuses1, statuses2)
379
+ statuses = {}
380
+ statuses1.each do |tests_suite, tests_info|
381
+ if statuses2.key?(tests_suite)
382
+ # Handle Hashes as it will be faster
383
+ statuses1_for_test = Hash[tests_info]
384
+ statuses2_for_test = Hash[statuses2[tests_suite]]
385
+ statuses1_for_test.each do |test_name, status1|
386
+ if statuses2_for_test.key?(test_name)
387
+ if statuses2_for_test[test_name] != status1
388
+ # Change in status
389
+ statuses[tests_suite] = [] unless statuses.key?(tests_suite)
390
+ statuses[tests_suite] << [test_name, statuses2_for_test[test_name]]
391
+ end
392
+ else
393
+ # This test has been removed
394
+ statuses[tests_suite] = [] unless statuses.key?(tests_suite)
395
+ statuses[tests_suite] << [test_name, 'Deleted']
396
+ end
397
+ end
398
+ statuses2_for_test.each do |test_name, status2|
399
+ unless statuses1_for_test.key?(test_name)
400
+ # This test has been added
401
+ statuses[tests_suite] = [] unless statuses.key?(tests_suite)
402
+ statuses[tests_suite] << [test_name, status2]
403
+ end
404
+ end
405
+ else
406
+ # All test statuses have been removed
407
+ statuses[tests_suite] = tests_info.map { |(test_name, _test_status)| [test_name, 'Deleted'] }
408
+ end
409
+ end
410
+ statuses2.each do |tests_suite, tests_info|
411
+ # All test statuses have been added
412
+ statuses[tests_suite] = tests_info unless statuses1.key?(tests_suite)
413
+ end
414
+ statuses
415
+ end
416
+
417
+ # Print test statuses
418
+ #
419
+ # Parameters::
420
+ # * *statuses* (Hash<String, Array<[String, String]> >): Test statuses, per test name (in a sorted list), per tests suite
421
+ def print_test_statuses(statuses)
422
+ statuses.each do |tests_suite, statuses_for_type|
423
+ next_test_name, _next_test_status = statuses_for_type.find { |(_name, status)| status != 'ok' }
424
+ log "[ #{tests_suite} ] - #{statuses_for_type.select { |(_name, status)| status == 'ok' }.size} / #{statuses_for_type.size} - Next test to perform: #{next_test_name.nil? ? 'None' : next_test_name}"
425
+ end
426
+ end
427
+
428
+ end
429
+
430
+ end
@@ -0,0 +1,69 @@
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
@@ -0,0 +1,130 @@
1
+ module Modsvaskr
2
+
3
+ module TestsSuites
4
+
5
+ class ExteriorCell < TestsSuite
6
+
7
+ # Discover the list of tests information that could be run.
8
+ # [API] - This method is mandatory
9
+ #
10
+ # Result::
11
+ # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
12
+ def discover_tests
13
+ # List of exterior cells coordinates, per worldspace name, per plugin name
14
+ # Hash< String, Hash< String, Array<[Integer, Integer]> > >
15
+ exterior_cells = {}
16
+ @game.xedit.run_script('DumpInfo', only_once: true)
17
+ CSV.read("#{@game.xedit.install_path}/Edit Scripts/Modsvaskr_ExportedDumpInfo.csv", encoding: 'windows-1251:utf-8').each do |row|
18
+ esp_name, record_type = row[0..1]
19
+ if record_type.downcase == 'cell'
20
+ cell_type, cell_name, cell_x, cell_y = row[3..6]
21
+ if cell_type == 'cow'
22
+ if cell_x.nil?
23
+ log "!!! Invalid record: #{row}"
24
+ else
25
+ esp_name.downcase!
26
+ exterior_cells[esp_name] = {} unless exterior_cells.key?(esp_name)
27
+ exterior_cells[esp_name][cell_name] = [] unless exterior_cells[esp_name].key?(cell_name)
28
+ exterior_cells[esp_name][cell_name] << [Integer(cell_x), Integer(cell_y)]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ # 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
34
+ vanilla_esps = @game.game_esps
35
+ vanilla_exterior_cells = vanilla_esps.inject({}) do |merged_worldspaces, esp_name|
36
+ merged_worldspaces.merge(exterior_cells[esp_name]) do |worldspace, ext_cells1, ext_cells2|
37
+ (ext_cells1 + ext_cells2).sort.uniq
38
+ end
39
+ end
40
+ changed_exterior_cells = {}
41
+ exterior_cells.each do |esp_name, esp_exterior_cells|
42
+ unless vanilla_esps.include?(esp_name)
43
+ esp_exterior_cells.each do |worldspace, worldspace_exterior_cells|
44
+ if vanilla_exterior_cells.key?(worldspace)
45
+ changed_exterior_cells[worldspace] = [] unless changed_exterior_cells.key?(worldspace)
46
+ changed_exterior_cells[worldspace].concat(vanilla_exterior_cells[worldspace] & worldspace_exterior_cells)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ tests = {}
52
+ # Value taken from the ini file
53
+ # TODO: Read it from there (uiGrid)
54
+ loaded_grid = 5
55
+ delta_cells = loaded_grid / 2
56
+ changed_exterior_cells.each do |worldspace, worldspace_exterior_cells|
57
+ # Make sure we select the minimum cells
58
+ # Use a Hash of Hashes for the coordinates to speed-up their lookup.
59
+ remaining_cells = {}
60
+ worldspace_exterior_cells.each do |(cell_x, cell_y)|
61
+ remaining_cells[cell_x] = {} unless remaining_cells.key?(cell_x)
62
+ remaining_cells[cell_x][cell_y] = nil
63
+ end
64
+ while !remaining_cells.empty?
65
+ cell_x, cell_ys = remaining_cells.first
66
+ cell_y, _nil = cell_ys.first
67
+ # We want to test cell_x, cell_y.
68
+ # 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)),
69
+ # check which cell would test the most wanted cells from our list
70
+ best_cell_x, best_cell_y, best_cell_score = nil, nil, nil
71
+ (cell_x - delta_cells..cell_x + delta_cells).each do |candidate_cell_x|
72
+ (cell_y - delta_cells..cell_y + delta_cells).each do |candidate_cell_y|
73
+ # Check the number of cells that would be tested if we were to test (candidate_cell_x, candidate_cell_y)
74
+ nbr_tested_cells = remaining_cells.
75
+ slice(*(candidate_cell_x - delta_cells..candidate_cell_x + delta_cells)).
76
+ 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 }
77
+ if best_cell_score.nil? || nbr_tested_cells > best_cell_score
78
+ nbr_tested_cells = best_cell_score
79
+ best_cell_x = candidate_cell_x
80
+ best_cell_y = candidate_cell_y
81
+ end
82
+ end
83
+ end
84
+ # Remove the tested cells from the remaining ones
85
+ (best_cell_x - delta_cells..best_cell_x + delta_cells).each do |cur_cell_x|
86
+ if remaining_cells.key?(cur_cell_x)
87
+ (best_cell_y - delta_cells..best_cell_y + delta_cells).each do |cur_cell_y|
88
+ remaining_cells[cur_cell_x].delete(cur_cell_y)
89
+ end
90
+ remaining_cells.delete(cur_cell_x) if remaining_cells[cur_cell_x].empty?
91
+ end
92
+ end
93
+ tests["#{worldspace}/#{best_cell_x}/#{best_cell_y}"] = {
94
+ name: "Load #{worldspace} cell #{best_cell_x}, #{best_cell_y}"
95
+ }
96
+ end
97
+ end
98
+ tests
99
+ end
100
+
101
+ # Get the list of tests to be run in the AutoTest mod for a given list of test names.
102
+ # AutoTest names are case insensitive.
103
+ # [API] - This method is mandatory for tests needing to be run in-game.
104
+ #
105
+ # Parameters::
106
+ # * *tests* (Array<String>): List of test names
107
+ # Result::
108
+ # * Hash<String, Array<String> >: List of AutoTest mod test names, per AutoTest mod tests suite
109
+ def auto_tests_for(tests)
110
+ { 'Locations' => tests }
111
+ end
112
+
113
+ # Set statuses based on the result of AutoTest statuses.
114
+ # AutoTest names are case insensitive.
115
+ # [API] - This method is mandatory for tests needing to be run in-game.
116
+ #
117
+ # Parameters::
118
+ # * *tests* (Array<String>): List of test names
119
+ # * *auto_test_statuses* (Hash<String, Array<[String, String]> >): Ordered list of AutoTest [test name, test status], per AutoTest tests suite
120
+ # Result::
121
+ # * Array<[String, String]>: Corresponding list of [test name, test status]
122
+ def parse_auto_tests_statuses_for(tests, auto_test_statuses)
123
+ auto_test_statuses.key?('Locations') ? auto_test_statuses['Locations'] : []
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+ end
@@ -0,0 +1,76 @@
1
+ module Modsvaskr
2
+
3
+ module TestsSuites
4
+
5
+ class InteriorCell < TestsSuite
6
+
7
+ # Discover the list of tests information that could be run.
8
+ # [API] - This method is mandatory
9
+ #
10
+ # Result::
11
+ # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
12
+ def discover_tests
13
+ # List of interior cells, per plugin name
14
+ # Hash< String, Array<String> >
15
+ interior_cells = {}
16
+ @game.xedit.run_script('DumpInfo', only_once: true)
17
+ CSV.read("#{@game.xedit.install_path}/Edit Scripts/Modsvaskr_ExportedDumpInfo.csv", encoding: 'windows-1251:utf-8').each do |row|
18
+ esp_name, record_type = row[0..1]
19
+ if record_type.downcase == 'cell'
20
+ cell_type, cell_name = row[3..4]
21
+ if cell_type == 'coc'
22
+ esp_name.downcase!
23
+ interior_cells[esp_name] = [] unless interior_cells.key?(esp_name)
24
+ interior_cells[esp_name] << cell_name
25
+ end
26
+ end
27
+ end
28
+ # Test only interior cells that have been changed by mods
29
+ vanilla_esps = @game.game_esps
30
+ vanilla_interior_cells = vanilla_esps.map { |esp_name| interior_cells[esp_name] }.flatten.sort.uniq
31
+ Hash[interior_cells.
32
+ map { |esp_name, esp_cells| vanilla_esps.include?(esp_name) ? [] : vanilla_interior_cells & esp_cells }.
33
+ flatten.
34
+ sort.
35
+ uniq.
36
+ map do |cell_name|
37
+ [
38
+ cell_name,
39
+ {
40
+ name: "Load cell #{cell_name}"
41
+ }
42
+ ]
43
+ end
44
+ ]
45
+ end
46
+
47
+ # Get the list of tests to be run in the AutoTest mod for a given list of test names.
48
+ # AutoTest names are case insensitive.
49
+ # [API] - This method is mandatory for tests needing to be run in-game.
50
+ #
51
+ # Parameters::
52
+ # * *tests* (Array<String>): List of test names
53
+ # Result::
54
+ # * Hash<String, Array<String> >: List of AutoTest mod test names, per AutoTest mod tests suite
55
+ def auto_tests_for(tests)
56
+ { 'Locations' => tests }
57
+ end
58
+
59
+ # Set statuses based on the result of AutoTest statuses.
60
+ # AutoTest names are case insensitive.
61
+ # [API] - This method is mandatory for tests needing to be run in-game.
62
+ #
63
+ # Parameters::
64
+ # * *tests* (Array<String>): List of test names
65
+ # * *auto_test_statuses* (Hash<String, Array<[String, String]> >): Ordered list of AutoTest [test name, test status], per AutoTest tests suite
66
+ # Result::
67
+ # * Array<[String, String]>: Corresponding list of [test name, test status]
68
+ def parse_auto_tests_statuses_for(tests, auto_test_statuses)
69
+ auto_test_statuses.key?('Locations') ? auto_test_statuses['Locations'] : []
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,52 @@
1
+ module Modsvaskr
2
+
3
+ module TestsSuites
4
+
5
+ class Npc < TestsSuite
6
+
7
+ # Discover the list of tests information that could be run.
8
+ # [API] - This method is mandatory
9
+ #
10
+ # Result::
11
+ # * Hash< String, Hash<Symbol,Object> >: Ordered hash of test information, per test name
12
+ def discover_tests
13
+ tests = {}
14
+ @game.xedit.run_script('DumpInfo', only_once: true)
15
+ CSV.read("#{@game.xedit.install_path}/Edit Scripts/Modsvaskr_ExportedDumpInfo.csv", encoding: 'windows-1251:utf-8').each do |row|
16
+ tests["#{row[0].downcase}/#{row[2].to_i(16)}"] = {
17
+ name: "Take screenshot of #{row[0]} - #{row[3]}"
18
+ } if row[1].downcase == 'npc_'
19
+ end
20
+ tests
21
+ end
22
+
23
+ # Get the list of tests to be run in the AutoTest mod for a given list of test names.
24
+ # AutoTest names are case insensitive.
25
+ # [API] - This method is mandatory for tests needing to be run in-game.
26
+ #
27
+ # Parameters::
28
+ # * *tests* (Array<String>): List of test names
29
+ # Result::
30
+ # * Hash<String, Array<String> >: List of AutoTest mod test names, per AutoTest mod tests suite
31
+ def auto_tests_for(tests)
32
+ { 'NPCs' => tests }
33
+ end
34
+
35
+ # Set statuses based on the result of AutoTest statuses.
36
+ # AutoTest names are case insensitive.
37
+ # [API] - This method is mandatory for tests needing to be run in-game.
38
+ #
39
+ # Parameters::
40
+ # * *tests* (Array<String>): List of test names
41
+ # * *auto_test_statuses* (Hash<String, Array<[String, String]> >): Ordered list of AutoTest [test name, test status], per AutoTest tests suite
42
+ # Result::
43
+ # * Array<[String, String]>: Corresponding list of [test name, test status]
44
+ def parse_auto_tests_statuses_for(tests, auto_test_statuses)
45
+ auto_test_statuses.key?('NPCs') ? auto_test_statuses['NPCs'] : []
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,167 @@
1
+ require 'csv'
2
+ require 'launchy'
3
+ require 'curses_menu'
4
+ require 'modsvaskr/logger'
5
+ require 'modsvaskr/tests_runner'
6
+ require 'modsvaskr/run_cmd'
7
+ require 'modsvaskr/version'
8
+
9
+ module Modsvaskr
10
+
11
+ class Ui
12
+
13
+ include Logger, RunCmd
14
+
15
+ # Constructor
16
+ #
17
+ # Parameters::
18
+ # * *config* (Config): Configuration object
19
+ def initialize(config:, skyrim: nil)
20
+ log "Launch Modsvaskr UI v#{Modsvaskr::VERSION} - Logs in #{Logger.log_file}"
21
+ @config = config
22
+ end
23
+
24
+ # Run the UI
25
+ def run
26
+ begin
27
+ CursesMenu.new 'Modsvaskr - Stronghold of Mods' do |main_menu|
28
+ @config.games.each do |game|
29
+ main_menu.item "#{game.name} (#{game.path})" do
30
+ CursesMenu.new "Modsvaskr - Stronghold of Mods > #{game.name}" do |game_menu|
31
+ game_menu.item 'Testing' do
32
+ # Read tests info
33
+ tests_runner = TestsRunner.new(game)
34
+ # Selected test names, per test type
35
+ # Hash< Symbol, Hash< String, nil > >
36
+ selected_tests_suites = {}
37
+ CursesMenu.new "Modsvaskr - Stronghold of Mods > #{game.name} > Testing" do |test_menu|
38
+ tests_runner.tests_suites.each do |tests_suite|
39
+ statuses_for_suite = tests_runner.statuses_for(tests_suite)
40
+ all_tests_selected = selected_tests_suites.key?(tests_suite) &&
41
+ selected_tests_suites[tests_suite].keys.sort == statuses_for_suite.map { |(test_name, _test_status)| test_name }.sort
42
+ test_menu.item(
43
+ "[#{
44
+ if all_tests_selected
45
+ '*'
46
+ elsif selected_tests_suites.key?(tests_suite)
47
+ '+'
48
+ else
49
+ ' '
50
+ end
51
+ }] #{tests_suite} - #{statuses_for_suite.select { |(_name, status)| status == 'ok' }.size} / #{statuses_for_suite.size}",
52
+ actions: {
53
+ 'd' => {
54
+ name: 'Details',
55
+ execute: proc do
56
+ CursesMenu.new "Modsvaskr - Stronghold of Mods > #{game.name} > Testing > Tests #{tests_suite}" do |tests_suite_menu|
57
+ statuses_for_suite.each do |(test_name, test_status)|
58
+ test_selected = selected_tests_suites.key?(tests_suite) && selected_tests_suites[tests_suite].key?(test_name)
59
+ tests_suite_menu.item "[#{test_selected ? '*' : ' '}] #{test_name} - #{test_status} - #{tests_runner.test_info(tests_suite, test_name)[:name]}" do
60
+ if test_selected
61
+ selected_tests_suites[tests_suite].delete(test_name)
62
+ selected_tests_suites.delete(tests_suite) if selected_tests_suites[tests_suite].empty?
63
+ else
64
+ selected_tests_suites[tests_suite] = {} unless selected_tests_suites.key?(tests_suite)
65
+ selected_tests_suites[tests_suite][test_name] = nil
66
+ end
67
+ :menu_refresh
68
+ end
69
+ end
70
+ tests_suite_menu.item 'Back' do
71
+ :menu_exit
72
+ end
73
+ end
74
+ :menu_refresh
75
+ end
76
+ }
77
+ }
78
+ ) do
79
+ if all_tests_selected
80
+ selected_tests_suites.delete(tests_suite)
81
+ else
82
+ selected_tests_suites[tests_suite] = Hash[statuses_for_suite.map { |(test_name, _test_status)| [test_name, nil] }]
83
+ end
84
+ :menu_refresh
85
+ end
86
+ end
87
+ test_menu.item 'Select tests that are not ok' do
88
+ selected_tests_suites = {}
89
+ tests_runner.tests_suites.map do |tests_suite|
90
+ tests_not_ok = {}
91
+ tests_runner.statuses_for(tests_suite).each do |(test_name, test_status)|
92
+ tests_not_ok[test_name] = nil unless test_status == 'ok'
93
+ end
94
+ selected_tests_suites[tests_suite] = tests_not_ok unless tests_not_ok.empty?
95
+ end
96
+ :menu_refresh
97
+ end
98
+ test_menu.item 'Register tests from selected test types' do
99
+ selected_tests_suites.keys.each do |tests_suite|
100
+ tests_runner.set_statuses_for(
101
+ tests_suite,
102
+ (
103
+ tests_runner.discover_tests_for(tests_suite) -
104
+ tests_runner.statuses_for(tests_suite).map { |(test_name, _test_status)| test_name }
105
+ ).map { |test_name| [test_name, ''] }
106
+ )
107
+ end
108
+ :menu_refresh
109
+ end
110
+ test_menu.item 'Unregister tests from selected test types' do
111
+ selected_tests_suites.keys.each do |tests_suite|
112
+ tests_runner.clear_tests_for(tests_suite)
113
+ end
114
+ :menu_refresh
115
+ end
116
+ test_menu.item 'Clear selected test statuses' do
117
+ selected_tests_suites.each do |tests_suite, test_names_set|
118
+ tests_runner.set_statuses_for(tests_suite, test_names_set.keys.map { |test_name| [test_name, ''] })
119
+ end
120
+ :menu_refresh
121
+ end
122
+ test_menu.item 'Run remaining selected tests' do
123
+ tests_runner.run(
124
+ selected_tests_suites.map do |selected_tests_suite, selected_test_names_set|
125
+ [
126
+ selected_tests_suite,
127
+ # Make sure tests to be run are ordered from the registered list
128
+ tests_runner.
129
+ statuses_for(selected_tests_suite).map { |(test_name, _test_status)| test_name }.
130
+ select { |test_name| selected_test_names_set.key?(test_name) }
131
+ ]
132
+ end
133
+ )
134
+ :menu_refresh
135
+ end
136
+ test_menu.item 'Back' do
137
+ :menu_exit
138
+ end
139
+ end
140
+ end
141
+ game.complete_game_menu(game_menu) if game.respond_to?(:complete_game_menu)
142
+ game_menu.item 'Back' do
143
+ :menu_exit
144
+ end
145
+ end
146
+ end
147
+ end
148
+ main_menu.item 'See logs' do
149
+ CursesMenu.new 'Modsvaskr - Stronghold of Mods > Logs' do |logs_menu|
150
+ File.read(Logger.log_file).split("\n").each do |line|
151
+ logs_menu.item line
152
+ end
153
+ logs_menu.item 'Back' do
154
+ :menu_exit
155
+ end
156
+ end
157
+ end
158
+ main_menu.item 'Quit' do
159
+ :menu_exit
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ end
166
+
167
+ end
@@ -0,0 +1,5 @@
1
+ module Modsvaskr
2
+
3
+ VERSION = '0.0.1'
4
+
5
+ end
@@ -0,0 +1,53 @@
1
+ require 'modsvaskr/run_cmd'
2
+
3
+ module Modsvaskr
4
+
5
+ # Helper to use an instance of xEdit
6
+ class Xedit
7
+
8
+ include RunCmd
9
+
10
+ # String: Installation path
11
+ attr_reader :install_path
12
+
13
+ # Constructor
14
+ #
15
+ # Parameters::
16
+ # * *install_path* (String): Installation path of xEdit
17
+ # * *game_path* (String): Installation path of the game to use xEdit on
18
+ def initialize(install_path, game_path)
19
+ @install_path = install_path
20
+ @game_path = game_path
21
+ # Set of scripts that have been run
22
+ @runs = {}
23
+ end
24
+
25
+ # Run an xEdit script
26
+ #
27
+ # Parameters::
28
+ # * *script* (String): Script name, as defined in xedit_scripts (without the Modsvaskr_ prefix and .pas suffix)
29
+ # * *only_once* (Boolean): If true, then make sure this script is run only once by instance [default: false]
30
+ def run_script(script, only_once: false)
31
+ if false
32
+ # if !only_once || !@runs.key?(script)
33
+ FileUtils.cp "#{__dir__}/../../xedit_scripts/Modsvaskr_#{script}.pas", "#{@install_path}/Edit Scripts/Modsvaskr_#{script}.pas"
34
+ run_cmd(
35
+ {
36
+ dir: @install_path,
37
+ exe: 'SSEEdit.exe'
38
+ },
39
+ args: %W[
40
+ -IKnowWhatImDoing
41
+ -AllowMasterFilesEdit
42
+ -SSE
43
+ -autoload
44
+ -script:"Modsvaskr_#{script}.pas"
45
+ ]
46
+ )
47
+ @runs[script] = nil
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: modsvaskr
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-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: curses_menu
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: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ description:
42
+ email:
43
+ - muriel@x-aeon.com
44
+ executables:
45
+ - modsvaskr
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - bin/modsvaskr
50
+ - lib/modsvaskr/config.rb
51
+ - lib/modsvaskr/game.rb
52
+ - lib/modsvaskr/games/skyrim_se.rb
53
+ - lib/modsvaskr/logger.rb
54
+ - lib/modsvaskr/run_cmd.rb
55
+ - lib/modsvaskr/tests_runner.rb
56
+ - lib/modsvaskr/tests_suite.rb
57
+ - lib/modsvaskr/tests_suites/exterior_cell.rb
58
+ - lib/modsvaskr/tests_suites/interior_cell.rb
59
+ - lib/modsvaskr/tests_suites/npc.rb
60
+ - lib/modsvaskr/ui.rb
61
+ - lib/modsvaskr/version.rb
62
+ - lib/modsvaskr/xedit.rb
63
+ homepage: http://x-aeon.com
64
+ licenses:
65
+ - BSD-3-Clause
66
+ metadata:
67
+ homepage_uri: http://x-aeon.com
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.0.3
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: 'Stronghold for mods acting like companions: The Modsvaskr'
87
+ test_files: []