Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Template for new versions:
## Fixes
- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed
- `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files
- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset
- `gui/mod-manager`: now supports arena mode
- `uniform-unstick`: resolve overlap with new buttons in 51.13

## Misc Improvements
Expand Down
137 changes: 107 additions & 30 deletions gui/mod-manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,27 @@ local widgets = require('gui.widgets')
local presets_file = json.open("dfhack-config/mod-manager.json")
local GLOBAL_KEY = 'mod-manager'

-- get_newregion_viewscreen and get_modlist_fields are declared as global functions
-- so external tools can call them to get the DF mod list
function get_newregion_viewscreen()
local function vanilla(dir)
return dir:startswith('data/vanilla')
end

-- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared
-- as global functions so external tools can call them to get the DF mod list
function get_moddable_viewscreen(type)
local vs = nil
if type == 'region' then
vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0)
elseif type == 'arena' then
vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_arenast, 0)
end
return vs
end

function get_any_moddable_viewscreen()
local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0)
if not vs then
vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_arenast, 0)
end
return vs
end

Expand Down Expand Up @@ -55,21 +72,30 @@ function get_modlist_fields(kind, viewscreen)
end
end

---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found.
---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt
local function move_mod_entry(viewscreen, to, from, mod_id, mod_version)
local to_fields = get_modlist_fields(to, viewscreen)
local from_fields = get_modlist_fields(from, viewscreen)

local mod_index = nil
local loaded_version = nil
for i, v in ipairs(from_fields.id) do
local version = from_fields.numeric_version[i]
if v.value == mod_id and version == mod_version then
local src_dir = from_fields.src_dir[i]
local displayed_version = from_fields.displayed_version[i].value
-- assumes that vanilla mods will not have multiple possible indices.
if v.value == mod_id and (vanilla(src_dir) or version == mod_version) then
if version ~= mod_version then
loaded_version = displayed_version
end
mod_index = i
break
end
end

if mod_index == nil then
return false
return false, nil
end

for k, v in pairs(to_fields) do
Expand All @@ -80,17 +106,21 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version)
end
end

for k, v in pairs(from_fields) do
for _, v in pairs(from_fields) do
v:erase(mod_index)
end

return true
return true, loaded_version
end

---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found.
---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt
local function enable_mod(viewscreen, mod_id, mod_version)
return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version)
end

---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found.
---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt
local function disable_mod(viewscreen, mod_id, mod_version)
return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version)
end
Expand All @@ -105,19 +135,25 @@ local function get_active_modlist(viewscreen)
return t
end

--- @return string[]
--- @return { id: string, new: string }[]
local function swap_modlist(viewscreen, modlist)
local current = get_active_modlist(viewscreen)
for _, v in ipairs(current) do
disable_mod(viewscreen, v.id, v.version)
end

local failures = {}
local changed = {}
for _, v in ipairs(modlist) do
if not enable_mod(viewscreen, v.id, v.version) then
local success, version = enable_mod(viewscreen, v.id, v.version)
if not success then
table.insert(failures, v.id)
elseif version then
table.insert(changed, { id= v.id, new= version })
end
end
return failures
return failures, changed
end

--------------------
Expand All @@ -137,7 +173,7 @@ ModmanageMenu.ATTRS {
}

local function save_new_preset(preset_name)
local viewscreen = get_newregion_viewscreen()
local viewscreen = get_any_moddable_viewscreen()
local modlist = get_active_modlist(viewscreen)
table.insert(presets_file.data, { name = preset_name, modlist = modlist })
presets_file:write()
Expand All @@ -157,27 +193,17 @@ local function overwrite_preset(idx)
return
end

local viewscreen = get_newregion_viewscreen()
local viewscreen = get_any_moddable_viewscreen()
local modlist = get_active_modlist(viewscreen)
presets_file.data[idx].modlist = modlist
presets_file:write()
end

local function load_preset(idx, unset_default_on_failure)
if idx > #presets_file.data then
return
end
local function prepare_warning(text, failed, changed, unset_default_on_failure)
if not failed and not changed then return end

local viewscreen = get_newregion_viewscreen()
local modlist = presets_file.data[idx].modlist
local failures = swap_modlist(viewscreen, modlist)

if #failures > 0 then
local text = {}
if failed then
if unset_default_on_failure then
presets_file.data[idx].default = false
presets_file:write()

table.insert(text, {
text='Failed to load some mods from your default preset.',
pen=COLOR_LIGHTRED,
Expand All @@ -193,19 +219,70 @@ local function load_preset(idx, unset_default_on_failure)
pen=COLOR_LIGHTRED,
})
end
end

if failed and changed then
table.insert(text, NEWLINE)
table.insert(text, NEWLINE)
table.insert(text, 'Please re-create your preset with mods you currently have installed.')
table.insert(text, NEWLINE)
end

if changed then
table.insert(text, {
text='Some vanilla mods have been updated.',
pen=COLOR_LIGHTRED,
})
end
table.insert(text, NEWLINE)
table.insert(text, 'Please re-create your preset with mods you currently have installed.')
table.insert(text, NEWLINE)
table.insert(text, NEWLINE)
end

local function load_preset(idx, unset_default_on_failure)
if idx > #presets_file.data then
return
end

local viewscreen = get_any_moddable_viewscreen()
local modlist = presets_file.data[idx].modlist
local failures, changes = swap_modlist(viewscreen, modlist)
local text = {}

local failed = #failures > 0
local changed = #changes > 0

prepare_warning(text, failed, changed)
if failed and unset_default_on_failure then
presets_file.data[idx].default = false
presets_file:write()
end

if failed then
table.insert(text, 'Here are the mods that failed to load:')
table.insert(text, NEWLINE)
table.insert(text, NEWLINE)
for _, v in ipairs(failures) do
table.insert(text, ('- %s'):format(v))
table.insert(text, NEWLINE)
end
end

if failed and changed then
table.insert(text, NEWLINE) -- just to separate the sections
end

if changed then
table.insert(text, 'Here are the vanilla mods that have been updated:')
table.insert(text, NEWLINE)
table.insert(text, NEWLINE)
for _, v in ipairs(changes) do
table.insert(text, ('- %s to %s'):format(v.id, v.new))
table.insert(text, NEWLINE)
end
end

if failed or changed then
dialogs.showMessage("Warning", text)
end
end
end

local function find_preset_by_name(name)
Expand Down Expand Up @@ -573,7 +650,7 @@ ModmanageOverlay.ATTRS {
desc = "Adds a link to the mod selection screen for accessing the mod manager.",
default_pos = { x=5, y=-6 },
version = 2,
viewscreens = { "new_region/Mods" },
viewscreens = { "new_region/Mods", "new_arena/Mods" },
default_enabled=true,
}

Expand Down Expand Up @@ -636,7 +713,7 @@ notification_timer_fn()
local default_applied = false
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc == SC_VIEWSCREEN_CHANGED then
local vs = get_newregion_viewscreen()
local vs = get_any_moddable_viewscreen()
if vs and not default_applied then
default_applied = true
for i, v in ipairs(presets_file.data) do
Expand Down