Skip to content

Commit

Permalink
Merge pull request #77 from Muriel-Salvan/add_mo_menu
Browse files Browse the repository at this point in the history
[#13] [Feature] Add a new ModOrganizer menu listing mods and running ModOrganizer
  • Loading branch information
Muriel-Salvan authored Mar 6, 2023
2 parents 07717a0 + edad529 commit ff8ef9e
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 8 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,16 @@ Example from `Modsvaskr/Tests/Statuses_interior_cell.json`
When selecting your game, you can ask Modsvaskr to easily install SKSE in it. This is useful when SKSE gets upgraded.
By selecting the `Install SKSE64` item in the game menu, Modsvaskr will download and install the latest SKSE version in your game folder.

This feature cannot be used if run under Mod Organizer, as it needs to modify the game base directory. Just run `Modsvaskr.cmd` outside of Mod Organizer to use this feature.
If you run this feature under Mod Organizer, don't forget to save the modified files from the Overwrite mode in a dedicated ModOrganizer mod to not lose them.

![Installing SKSE](docs/example_install_skse.png)

### Using Modsvaskr with ModOrganizer

Modsvaskr has a menu dedicated to ModOrganizer, where you can see the various mods and plugins configured in Modsvaskr.

![ModOrganizer mods](docs/example_mod_organizer_mods.png)

## Troubleshooting

Logs of execution of Modsvaskr are stored in the current folder: `Modsvaskr.log`.
Expand Down
3 changes: 2 additions & 1 deletion bin/modsvaskr
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/env ruby
# require 'mod_organizer'
require 'English'
require 'modsvaskr/config'
require 'modsvaskr/ui'

begin
Modsvaskr::Ui.new(config: Modsvaskr::Config.new('./modsvaskr.yaml')).run
rescue
puts "An exception has occurred: #{$ERROR_INFO}\n#{$ERROR_INFO.backtrace.join("\n")}"
puts 'Press Enter to exit.'
$stdin.gets
end
Binary file added docs/example_mod_organizer_mods.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions lib/modsvaskr/config.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'mod_organizer'
require 'yaml'
require 'modsvaskr/game'
require 'modsvaskr/xedit'
Expand Down Expand Up @@ -73,6 +74,19 @@ def no_prompt
@config['no_prompt'] || false
end

# Return the ModOrganizer instance, if configured.
#
# Result::
# * ModOrganizer or nil: The ModOrganizer instance, or nil if none
def mod_organizer
return nil unless @config['mod_organizer']

ModOrganizer.new(
@config['mod_organizer']['installation_dir'],
instance_name: @config['mod_organizer']['instance_name']
)
end

end

end
101 changes: 101 additions & 0 deletions lib/modsvaskr/ui.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'English'
require 'curses_menu'
require 'launchy'
require 'modsvaskr/logger'
require 'modsvaskr/tests_runner'
require 'modsvaskr/run_cmd'
Expand All @@ -20,8 +21,12 @@ class Ui
def initialize(config:)
log "Launch Modsvaskr UI v#{Modsvaskr::VERSION} - Logs in #{Logger.log_file}"
@config = config
@mod_organizer = @config.mod_organizer
end

# Time that should be before any file time of mods (used for sorting)
TIME_BEGIN = Time.parse('2000-01-01')

# Run the UI
def run
last_modsvaskr_version = nil
Expand Down Expand Up @@ -203,6 +208,102 @@ def run
end
end
end
unless @mod_organizer.nil?
main_menu.item 'Mods Organizer' do
CursesMenu.new(
'Modsvaskr - Stronghold of Mods > Mods Organizer',
key_presses:
) do |mo_menu|
# Show status
mo_menu.item 'Run Mod Organizer' do
@mod_organizer.run
end
mo_menu.item "#{@mod_organizer.mod_names.size} mods (#{@mod_organizer.mod_names.select { |mod_name| @mod_organizer.mod(name: mod_name).enabled? }.size} enabled)" do
CursesMenu.new(
'Modsvaskr - Stronghold of Mods > Mods Organizer > Mods',
key_presses:
) do |mods_menu|
mod_names = @mod_organizer.mods_list
idx_size = (mod_names.size - 1).to_s.size
mod_names.each.with_index do |mod_name, idx|
mod = @mod_organizer.mod(name: mod_name)
# need_update = false
mods_menu.item(
proc do
sections = {
enabled_cell: {
text: mod.enabled? ? 'X' : ' ',
begin_with: '[',
end_with: ']',
fixed_size: 3
},
idx_cell: {
text: idx.to_s,
fixed_size: idx_size
},
name_cell: {
text: mod_name,
color_pair: CursesMenu::COLORS_GREEN,
fixed_size: 64
},
categories_cell: {
text: mod.categories.map { |category| "[#{category}]" }.join(' '),
fixed_size: 16
},
esps_cell: {
text: "#{mod.plugins.size} plugins",
fixed_size: 10
}
}
mod.sources.sort_by { |source| source.download.nil? ? TIME_BEGIN : source.download.downloaded_date }.each.with_index do |source, src_idx|
source_name = src_idx.zero? ? '' : '+ '
source_name <<
case source.type
when :nexus_mods
"Nexus Mod #{source.nexus_mod_id}/" +
if source.download&.nexus_file_name
source.download&.nexus_file_name
elsif source.file_name
source.file_name
else
'<Unknown file>'
end
when :unknown
source.file_name || '<Unknown source>'
else
raise "Unknown source type: #{source.type} for #{mod.name}"
end
sections["source_#{src_idx}".to_sym] = {
text: source_name,
color_pair: CursesMenu::COLORS_WHITE
}
end
CursesMenu::CursesRow.new(sections)
end,
actions: proc do
actions = {}
# TODO: Use downloads' URL instead of mod's URL as it is very often more accurate
if mod.url
actions['v'] = {
name: 'Visit',
execute: proc { Launchy.open(mod.url) }
}
end
actions
end
)
end
mods_menu.item 'Back' do
:menu_exit
end
end
end
mo_menu.item 'Back' do
:menu_exit
end
end
end
end
main_menu.item 'See logs' do
CursesMenu.new(
'Modsvaskr - Stronghold of Mods > Logs',
Expand Down
10 changes: 6 additions & 4 deletions modsvaskr.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ Gem::Specification.new do |spec|
spec.executables << File.basename(exec_name)
end

spec.add_dependency 'curses_menu', '~> 0.0'
spec.add_dependency 'curses_menu', '~> 0.2'
spec.add_dependency 'elder_scrolls_plugin', '~> 0.0'
spec.add_dependency 'nokogiri', '~> 1.13'
spec.add_dependency 'launchy', '~> 2.5'
spec.add_dependency 'mod_organizer', '~> 1.0'
spec.add_dependency 'nokogiri', '~> 1.14'

# Development dependencies (tests, build)
# Test framework
Expand All @@ -30,7 +32,7 @@ Gem::Specification.new do |spec|
# Automatic semantic releasing
spec.add_development_dependency 'sem_ver_components', '~> 0.3'
# Lint checker
spec.add_development_dependency 'rubocop', '~> 1.41'
spec.add_development_dependency 'rubocop', '~> 1.47'
# Lint checker for rspec
spec.add_development_dependency 'rubocop-rspec', '~> 2.16'
spec.add_development_dependency 'rubocop-rspec', '~> 2.18'
end
24 changes: 22 additions & 2 deletions spec/modsvaskr_test/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'yaml'
require 'modsvaskr/config'
require 'modsvaskr/ui'
require 'modsvaskr_test/mocked_mod_organizer/mod_organizer'

module ModsvaskrTest

Expand Down Expand Up @@ -127,6 +128,25 @@ def run_and_discover(csv: '', run: false, expect_tests: {}, mock_tests_statuses:
end
end

# Run Modsvaskr with a ModOrganizer test configuration setup and a set of mocked mods.
# Use a test MO instance on a test path.
#
# Parameters::
# * *mods* (Hash<String, Hash<Symbol,Object>>): Mocked mods information [default: {}]
# * *keys* (Array<String>): List of keys to be entered once in the menu [default: []]
def run_modsvaskr_with_mo(mods: {}, keys: [])
expect(::ModOrganizer).to receive(:new).with('/path/to/mo', hash_including(instance_name: 'TestMOInstance')).and_return(MockedModOrganizer::ModOrganizer.new(mods:))
run_modsvaskr(
config: {
'mod_organizer' => {
'installation_dir' => '/path/to/mo',
'instance_name' => 'TestMOInstance'
}
},
keys:
)
end

# Expect logs to include a given line
#
# Parameters::
Expand Down Expand Up @@ -189,9 +209,9 @@ def expect_menu_item_to_include(menu_item_idx, line, menu_idx: @menu_index)
EO_ERROR_MESSAGE
end
if line.is_a?(Regexp)
expect(ModsvaskrTest.screenshots[menu_idx][3 + menu_item_idx].match(line)).to be(true), error_msg_proc
expect(ModsvaskrTest.screenshots[menu_idx][3 + menu_item_idx]).to match(line), error_msg_proc
else
expect(ModsvaskrTest.screenshots[menu_idx][3 + menu_item_idx].include?(line)).to be(true), error_msg_proc
expect(ModsvaskrTest.screenshots[menu_idx][3 + menu_item_idx]).to include(line), error_msg_proc
end
end

Expand Down
35 changes: 35 additions & 0 deletions spec/modsvaskr_test/mocked_mod_organizer/download.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module ModsvaskrTest

module MockedModOrganizer

class Download

attr_reader :downloaded_file_path, :downloaded_date, :nexus_file_name, :nexus_mod_id, :nexus_file_id

# Constructor
#
# Parameters::
# * *downloaded_file_path* (String or nil): Full downloaded file path, or nil if does not exist [default: nil]
# * *downloaded_date* (Time or nil): Download date of this source, or nil if no file [default: Time.now]
# * *nexus_file_name* (String): Original file name from NexusMods [default: 'nexus_mods_file.7z']
# * *nexus_mod_id* (Integer): Mod ID from NexusMods [default: 42]
# * *nexus_file_id* (Integer): File ID from NexusMods [default: 666]
def initialize(
downloaded_file_path: nil,
downloaded_date: Time.now,
nexus_file_name: 'nexus_mods_file.7z',
nexus_mod_id: 42,
nexus_file_id: 666
)
@downloaded_file_path = downloaded_file_path
@downloaded_date = downloaded_date
@nexus_file_name = nexus_file_name
@nexus_mod_id = nexus_mod_id
@nexus_file_id = nexus_file_id
end

end

end

end
47 changes: 47 additions & 0 deletions spec/modsvaskr_test/mocked_mod_organizer/mod.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'modsvaskr_test/mocked_mod_organizer/source'

module ModsvaskrTest

module MockedModOrganizer

class Mod

attr_reader :categories, :plugins, :url

# Constructor
#
# Parameters::
# * *enabled* (Boolean): Is the mod enabled? [default: true]
# * *categories* (Array<String>): List of this mod's categories [default: []]
# * *plugins* (Array<String>): List of this mod's plugins [default: []]
# * *sources* (Array< Hash<Symbol,Object> >): List of this mod's sources [default: []]
# * *url* (String): This mod's URL [default: 'https://my_test_mod.com']
def initialize(enabled: true, categories: [], plugins: [], sources: [], url: 'https://my_test_mod.com')
@enabled = enabled
@categories = categories
@plugins = plugins
@sources = sources
@url = url
end

# Is the mod enabled?
#
# Result::
# * Boolean: Is the mod enabled?
def enabled?
@enabled
end

# Return the list of sources this mod belongs to
#
# Result::
# * Array<MockedSource>: List of source information
def sources
@sources.map { |source| Source.new(**source) }
end

end

end

end
57 changes: 57 additions & 0 deletions spec/modsvaskr_test/mocked_mod_organizer/mod_organizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require 'modsvaskr_test/mocked_mod_organizer/mod'

module ModsvaskrTest

module MockedModOrganizer

class ModOrganizer

attr_reader :run_called, :mods_list

# Constructor
#
# Parameters::
# * *mods* (Hash<String, Hash<Symbol,Object>>): List of mods to mock, per mod name [default: {}]
# * *enabled* (Boolean): Is the mod enabled? [default: true]
# * *categories* (Array<String>): List of this mod's categories [default: []]
# * *plugins* (Array<String>): List of this mod's plugins [default: []]
# * *sources* (Array< Hash<Symbol,Object> >): List of this mod's sources [default: []]
# * *mods_list* (Array<String>): The ordered list of mod names [default: mods.keys]
# * *categories* (Hash<Integer, String>): Categories to return, or nil for default (built from mods' categories) [default: nil]
def initialize(
mods: {},
mods_list: mods.keys
)
@mods = mods
@mods_list = mods_list
@run_called = false
end

# Return mod_names
#
# Result::
# * Array<String>: Mod names
def mod_names
@mods.keys
end

# Return a mod of a given name
#
# Parameters::
# * *name* (String): Mod namd
# Result::
# * MockedMod: The mocked mod
def mod(name:)
Mod.new(**@mods[name])
end

# Run ModOrganizer
def run
@run_called = true
end

end

end

end
Loading

0 comments on commit ff8ef9e

Please sign in to comment.