modsvaskr 0.1.8 → 0.1.12
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.
- checksums.yaml +4 -4
- data/bin/modsvaskr +11 -11
- data/lib/modsvaskr/config.rb +78 -79
- data/lib/modsvaskr/encoding.rb +31 -0
- data/lib/modsvaskr/game.rb +208 -179
- data/lib/modsvaskr/games/skyrim_se.rb +106 -92
- data/lib/modsvaskr/in_game_tests_runner.rb +348 -348
- data/lib/modsvaskr/logger.rb +45 -44
- data/lib/modsvaskr/run_cmd.rb +23 -22
- data/lib/modsvaskr/tests_runner.rb +204 -205
- data/lib/modsvaskr/tests_suite.rb +70 -69
- data/lib/modsvaskr/tests_suites/exterior_cell.rb +120 -117
- data/lib/modsvaskr/tests_suites/interior_cell.rb +63 -63
- data/lib/modsvaskr/tests_suites/npc.rb +67 -39
- data/lib/modsvaskr/tests_suites/npc_head.rb +6 -10
- data/lib/modsvaskr/ui.rb +205 -205
- data/lib/modsvaskr/version.rb +5 -5
- data/lib/modsvaskr/xedit.rb +65 -52
- data/xedit_scripts/Modsvaskr_DumpInfo.pas +13 -0
- metadata +39 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c41c160023d7591e52e7b93fde57bc33462627d61714384aa3051c8f8052f99
|
4
|
+
data.tar.gz: 4e30b5822a983d6e57772ba65e19d7b8b657163c5b3c4d275bb55a61376c9e2c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61ced31f7a9f75bb9679e47045cdfd38d6071f1eb91587cab8a3aa83c8c6d2de559402e3995b65f020a1acf6ce31d2ca96e31341960906c87f0717991a669f1e
|
7
|
+
data.tar.gz: c7e3610bc8f62f290fa586e66489f5f4e69b40942814a0ba56237716c925cb5ee7878152f58f9dfc94746242d84dd48bd67f96a37e903db3169cafd19ee2f533
|
data/bin/modsvaskr
CHANGED
@@ -1,11 +1,11 @@
|
|
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 'Press Enter to exit.'
|
10
|
-
$stdin.gets
|
11
|
-
end
|
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 'Press Enter to exit.'
|
10
|
+
$stdin.gets
|
11
|
+
end
|
data/lib/modsvaskr/config.rb
CHANGED
@@ -1,79 +1,78 @@
|
|
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.
|
16
|
-
# Parse all game types plugins
|
17
|
-
# Hash<Symbol, Class>
|
18
|
-
@game_types =
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
end
|
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.safe_load(File.read(file)) || {}
|
16
|
+
# Parse all game types plugins
|
17
|
+
# Hash<Symbol, Class>
|
18
|
+
@game_types = Dir.glob("#{__dir__}/games/*.rb").map do |game_type_file|
|
19
|
+
require game_type_file
|
20
|
+
base_name = File.basename(game_type_file, '.rb')
|
21
|
+
[
|
22
|
+
base_name.to_sym,
|
23
|
+
Games.const_get(base_name.split('_').collect(&:capitalize).join.to_sym)
|
24
|
+
]
|
25
|
+
end.to_h
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the games list
|
29
|
+
#
|
30
|
+
# Result::
|
31
|
+
# * Array<Game>: List of games
|
32
|
+
def games
|
33
|
+
unless defined?(@games)
|
34
|
+
@games = (@config['games'] || []).map do |game_info|
|
35
|
+
game_type = game_info['type'].to_sym
|
36
|
+
raise "Unknown game type: #{game_type}. Available ones are #{@game_types.keys.join(', ')}" unless @game_types.key?(game_type)
|
37
|
+
|
38
|
+
@game_types[game_type].new(self, game_info)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@games
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return the xEdit path
|
45
|
+
#
|
46
|
+
# Result::
|
47
|
+
# * String: The xEdit path
|
48
|
+
def xedit_path
|
49
|
+
@config['xedit']
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return the 7-Zip path
|
53
|
+
#
|
54
|
+
# Result::
|
55
|
+
# * String: The 7-Zip path
|
56
|
+
def seven_zip_path
|
57
|
+
@config['7zip']
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the automated keys to apply
|
61
|
+
#
|
62
|
+
# Result::
|
63
|
+
# * Array<String>: The list of automated keys
|
64
|
+
def auto_keys
|
65
|
+
@config['auto_keys'] || []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return the no_prompt flag
|
69
|
+
#
|
70
|
+
# Result::
|
71
|
+
# * Boolean: no_prompt flag
|
72
|
+
def no_prompt
|
73
|
+
@config['no_prompt'] || false
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Modsvaskr
|
2
|
+
|
3
|
+
# Provide helpers to encode Windows and Linux strings to UTF-8
|
4
|
+
module Encoding
|
5
|
+
|
6
|
+
# Convert a string to UTF-8
|
7
|
+
#
|
8
|
+
# Parameters::
|
9
|
+
# * *str* (String): The string to convert
|
10
|
+
# Result::
|
11
|
+
# * String: The converted string
|
12
|
+
def self.to_utf_8(str)
|
13
|
+
orig_encoding = str.encoding
|
14
|
+
encoding = nil
|
15
|
+
begin
|
16
|
+
encoding = %w[
|
17
|
+
UTF-8
|
18
|
+
Windows-1252
|
19
|
+
ISO-8859-1
|
20
|
+
].find { |search_encoding| str.force_encoding(search_encoding).valid_encoding? }
|
21
|
+
ensure
|
22
|
+
str.force_encoding(orig_encoding)
|
23
|
+
end
|
24
|
+
raise "Unknown encoding for string #{str[0..127].inspect}" if encoding.nil?
|
25
|
+
|
26
|
+
str.encode('UTF-8', encoding)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
data/lib/modsvaskr/game.rb
CHANGED
@@ -1,179 +1,208 @@
|
|
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
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# * *
|
17
|
-
#
|
18
|
-
# * *
|
19
|
-
# * *
|
20
|
-
# * *
|
21
|
-
# * *
|
22
|
-
# * *
|
23
|
-
# * *
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
'
|
30
|
-
'
|
31
|
-
'
|
32
|
-
|
33
|
-
|
34
|
-
@
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
@xedit
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
running =
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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 RunCmd
|
11
|
+
include Logger
|
12
|
+
|
13
|
+
# Constructor
|
14
|
+
#
|
15
|
+
# Parameters::
|
16
|
+
# * *config* (Config): The config
|
17
|
+
# * *game_info* (Hash<String,Object>): Game info:
|
18
|
+
# * *name* (String): Game name
|
19
|
+
# * *path* (String): Game installation dir
|
20
|
+
# * *launch_exe* (String): Executable to be launched
|
21
|
+
# * *min_launch_time_secs* (Integer): Minimum expected lauch time for the game, in seconds [default: 10]
|
22
|
+
# * *tests_poll_secs* (Integer): Interval in seconds to be respected between 2 test statuses polling [default: 5]
|
23
|
+
# * *timeout_frozen_tests_secs* (Integer): Timeout in seconds of a frozen game [default: 300]
|
24
|
+
# * *timeout_interrupt_tests_secs* (Integer): Timeout in seconds for the player to interrupt a tests session before restarting the game [default: 10]
|
25
|
+
def initialize(config, game_info)
|
26
|
+
@config = config
|
27
|
+
# Set default values here
|
28
|
+
@game_info = {
|
29
|
+
'min_launch_time_secs' => 10,
|
30
|
+
'tests_poll_secs' => 5,
|
31
|
+
'timeout_frozen_tests_secs' => 300,
|
32
|
+
'timeout_interrupt_tests_secs' => 10
|
33
|
+
}.merge(game_info)
|
34
|
+
@name = name
|
35
|
+
@pid = nil
|
36
|
+
init if respond_to?(:init)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the game name
|
40
|
+
#
|
41
|
+
# Result::
|
42
|
+
# * String: Game name
|
43
|
+
def name
|
44
|
+
@game_info['name']
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the game path
|
48
|
+
#
|
49
|
+
# Result::
|
50
|
+
# * String: Game path
|
51
|
+
def path
|
52
|
+
@game_info['path']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return the launch executable
|
56
|
+
#
|
57
|
+
# Result::
|
58
|
+
# * String: Launch executable
|
59
|
+
def launch_exe
|
60
|
+
@game_info['launch_exe']
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the tests polling interval
|
64
|
+
#
|
65
|
+
# Result::
|
66
|
+
# * Integer: Tests polling interval
|
67
|
+
def tests_poll_secs
|
68
|
+
@game_info['tests_poll_secs']
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return the timeout to detect a frozen game
|
72
|
+
#
|
73
|
+
# Result::
|
74
|
+
# * Integer: Timeout to detect a frozen game
|
75
|
+
def timeout_frozen_tests_secs
|
76
|
+
@game_info['timeout_frozen_tests_secs']
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return the timeout before restarting a game tests session
|
80
|
+
#
|
81
|
+
# Result::
|
82
|
+
# * Integer: Timeout before restarting a game tests session
|
83
|
+
def timeout_interrupt_tests_secs
|
84
|
+
@game_info['timeout_interrupt_tests_secs']
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return an xEdit instance for this game
|
88
|
+
#
|
89
|
+
# Result::
|
90
|
+
# * Xedit: The xEdit instance
|
91
|
+
def xedit
|
92
|
+
@xedit = Xedit.new(@config.xedit_path, path) unless defined?(@xedit)
|
93
|
+
@xedit
|
94
|
+
end
|
95
|
+
|
96
|
+
# Launch the game, and wait for launch to be successful
|
97
|
+
#
|
98
|
+
# Parameters::
|
99
|
+
# * *autoload* (Boolean or String): If false, then launch the game using the normal launcher. If String, then use AutoLoad to load a given saved file (or empty to continue latest save) [default: false].
|
100
|
+
def launch(autoload: false)
|
101
|
+
# Launch the game
|
102
|
+
@idx_launch = 0 unless defined?(@idx_launch)
|
103
|
+
if autoload
|
104
|
+
log "[ Game #{name} ] - Launch game (##{@idx_launch}) using AutoLoad #{autoload}..."
|
105
|
+
autoload_file = "#{path}/Data/AutoLoad.cmd"
|
106
|
+
if File.exist?(autoload_file)
|
107
|
+
run_cmd(
|
108
|
+
{
|
109
|
+
dir: path,
|
110
|
+
exe: 'Data\AutoLoad.cmd',
|
111
|
+
args: [autoload]
|
112
|
+
}
|
113
|
+
)
|
114
|
+
else
|
115
|
+
log "[ Game #{name} ] - Missing file #{autoload_file}. Can't use AutoLoad to load game automatically. Please install the AutoLoad mod."
|
116
|
+
end
|
117
|
+
else
|
118
|
+
log "[ Game #{name} ] - Launch game (##{@idx_launch}) using configured launcher (#{launch_exe})..."
|
119
|
+
run_cmd(
|
120
|
+
{
|
121
|
+
dir: path,
|
122
|
+
exe: launch_exe
|
123
|
+
}
|
124
|
+
)
|
125
|
+
end
|
126
|
+
@idx_launch += 1
|
127
|
+
# The game launches asynchronously, so just wait a little bit and check for the process existence
|
128
|
+
sleep @game_info['min_launch_time_secs']
|
129
|
+
tasklist_stdout = nil
|
130
|
+
loop do
|
131
|
+
tasklist_stdout = `tasklist | find "#{running_exe}"`.strip
|
132
|
+
break unless tasklist_stdout.empty?
|
133
|
+
|
134
|
+
log "[ Game #{name} ] - #{running_exe} is not running. Wait for its startup..."
|
135
|
+
sleep 1
|
136
|
+
end
|
137
|
+
@pid = Integer(tasklist_stdout.split[1])
|
138
|
+
log "[ Game #{name} ] - #{running_exe} has started with PID #{@pid}"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Is the game currently running?
|
142
|
+
#
|
143
|
+
# Result::
|
144
|
+
# * Boolean: Is the game currently running?
|
145
|
+
def running?
|
146
|
+
if @pid
|
147
|
+
running = true
|
148
|
+
begin
|
149
|
+
# Process.kill does not work when the game has crashed (the process is still detected as zombie)
|
150
|
+
# running = Process.kill(0, @pid) == 1
|
151
|
+
tasklist_stdout = `tasklist | find "#{running_exe}"`.strip
|
152
|
+
running = !tasklist_stdout.empty?
|
153
|
+
# log "[ Game #{name} ] - Tasklist returned no #{running_exe}:\n#{tasklist_stdout}" unless running
|
154
|
+
rescue Errno::ESRCH
|
155
|
+
log "[ Game #{name} ] - Got error while waiting for #{running_exe} PID #{@pid}: #{$ERROR_INFO}"
|
156
|
+
running = false
|
157
|
+
end
|
158
|
+
@pid = nil unless running
|
159
|
+
running
|
160
|
+
else
|
161
|
+
false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Kill the game, and wait till it is killed
|
166
|
+
def kill
|
167
|
+
if @pid
|
168
|
+
first_time = true
|
169
|
+
while @pid
|
170
|
+
system "taskkill #{first_time ? '' : '/F '}/pid #{@pid}"
|
171
|
+
first_time = false
|
172
|
+
sleep 1
|
173
|
+
if running?
|
174
|
+
log "[ Game #{name} ] - #{running_exe} is still running (PID #{@pid}). Wait for its kill..."
|
175
|
+
sleep 5
|
176
|
+
end
|
177
|
+
end
|
178
|
+
else
|
179
|
+
log "[ Game #{name} ] - Game not started, so nothing to kill."
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get the load order.
|
184
|
+
# Keep a cache of it.
|
185
|
+
#
|
186
|
+
# Result::
|
187
|
+
# * Array<String>: List of all active plugins, including masters
|
188
|
+
def load_order
|
189
|
+
@cache_load_order = read_load_order unless defined?(@cache_load_order)
|
190
|
+
@cache_load_order
|
191
|
+
end
|
192
|
+
|
193
|
+
# Get the mod and base id corresponding to a given form id.
|
194
|
+
# Uses load order to determine it.
|
195
|
+
#
|
196
|
+
# Parameters::
|
197
|
+
# * *form_id* (String or Integer): Form ID, either as hexadecimal string, or numercial value
|
198
|
+
# Result::
|
199
|
+
# * String: Plugin name
|
200
|
+
# * Integer: Base form id, independent from the load order
|
201
|
+
def decode_form_id(form_id)
|
202
|
+
form_id = form_id.to_i(16) if form_id.is_a?(String)
|
203
|
+
[load_order[form_id / 16_777_216], form_id % 16_777_216]
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|