modsvaskr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 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: []