From ddae413059113621baa380e23bd1ccf35a2bf706 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 7 Apr 2025 13:00:20 -0500 Subject: [PATCH 01/17] Add a window showing the active mods in a world --- gui/mod-manager.lua | 117 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 51df00d677..82c17c1249 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -10,6 +10,7 @@ local utils = require('utils') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' +local INSTALLED_MODS_PATH = 'data/installed_mods/' -- get_newregion_viewscreen and get_modlist_fields are declared as global functions -- so external tools can call them to get the DF mod list @@ -401,6 +402,120 @@ function ModmanageScreen:init() } end +ModlistMenu = defclass(ModlistMenu, widgets.Window) +ModlistMenu.ATTRS { + view_id = "modlist_menu", + frame_title = "Active Modlist", + frame_style = gui.WINDOW_FRAME, + + resize_min = { w = 30, h = 15 }, + frame = { w = 40, t = 10, b = 15 }, + + resizable = true, + autoarrange_subviews=false, +} + +local function get_mod_id_and_version(path) + local idfile = path .. '/info.txt' + local ok, lines = pcall(io.lines, idfile) + if not ok then return end + local id, version + for line in lines do + if not id then + _,_,id = line:find('^%[ID:([^%]]+)%]') + end + if not version then + -- note this doesn't include the closing brace since some people put + -- non-number characters in here, and DF only reads the leading digits + -- as the numeric version + _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)') + end + -- note that we do *not* want to break out of this loop early since + -- lines has to hit EOF to close the file + end + return id, version +end + +local function add_mod_paths(mod_paths, id, base_path, subdir) + local sep = base_path:endswith('/') and '' or '/' + local path = ('%s%s%s'):format(base_path, sep, subdir) + if dfhack.filesystem.isdir(path) then + table.insert(mod_paths, {id=id, path=path}) + end +end + +local function getWorldModlist() + -- ordered map of mod id -> {handled=bool, versions=map of version -> path} + local mods = utils.OrderedTable() + local mod_paths = {} + + -- if a world is loaded, process active mods first, and lock to active version + if dfhack.isWorldLoaded() then + for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do + path = tostring(path.value) + -- skip vanilla "mods" + if not path:startswith(INSTALLED_MODS_PATH) then goto continue end + local id = get_mod_id_and_version(path) + if not id then goto continue end + mods[id] = {handled=true} + add_mod_paths(mod_paths, id, path, '.') + ::continue:: + end + local modlist = {} + for _,mod in ipairs(mod_paths) do + table.insert(modlist,mod.id) + end + return modlist + end + qerror('No world is loaded') +end + +function ModlistMenu:init() + local modlist = widgets.List{ + view_id='modlist', + frame = {t=3}, + choices = getWorldModlist() + } + self:addviews{ + widgets.Label{ + frame = { l=0, t=0 }, + text = {'Active mods:'}, + }, + widgets.HotkeyLabel{ + view_id='copy', + frame={t=1, r=1}, + label='Copy modlist to clipboard', + text_pen=COLOR_YELLOW, + auto_width=true, + on_activate=function() + local mods = '' + for _,mod in ipairs(getWorldModlist()) do + mods = (mods == '' and mod) or (mods .. ', ' .. mod) + end + dfhack.internal.setClipboardTextCp437(mods) + end, + enabled=function() return #modlist:getChoices() > 0 end, + }, + modlist + } +end + + +ModlistScreen = defclass(ModlistScreen, gui.ZScreen) +ModlistScreen.ATTRS { + focus_path = "modlist", +} + +function ModlistScreen:init() + self:addviews{ + ModlistMenu{} + } +end + +function ModlistScreen:onDismiss() + view = nil +end + ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) ModmanageOverlay.ATTRS { frame = { w=16, h=3 }, @@ -496,3 +611,5 @@ end -- TODO: when invoked as a command, should show information on which mods are loaded -- and give the player the option to export the list (or at least copy it to the clipboard) + +view = view and view:raise() or ModlistScreen{}:show() From eaa3fc986c9d51e340a8a457761240d0ccef4fca Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 11 Apr 2025 11:00:37 -0500 Subject: [PATCH 02/17] Update docs --- docs/gui/mod-manager.rst | 3 ++- gui/mod-manager.lua | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 8972fece72..7fa89bae45 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -2,9 +2,10 @@ gui/mod-manager =============== .. dfhack-tool:: - :summary: Save and restore lists of active mods. + :summary: Manange your active mods. :tags: dfhack interface +In a loaded world, shows a list of active mods with the ability to copyto clipboard. Adds an optional overlay to the mod list screen that allows you to save and load mod list presets, as well as set a default mod list preset for new worlds. diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 82c17c1249..6de6067e93 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -609,7 +609,4 @@ if dfhack_flags.module then return end --- TODO: when invoked as a command, should show information on which mods are loaded --- and give the player the option to export the list (or at least copy it to the clipboard) - view = view and view:raise() or ModlistScreen{}:show() From e36429990889599661a0f683c400019f35f94aa0 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 11 Apr 2025 11:02:12 -0500 Subject: [PATCH 03/17] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 213e521fd3..d03a2061fd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/mod-manager`: when run in a loaded world, opens a copyable list of active mods. ## Fixes - `list-agreements`: fix date math when determining petition age From 765d1c8809737652c4c585b6956528ceed975350 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:33:21 -0500 Subject: [PATCH 04/17] Update mod-manager.rst Signed-off-by: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> --- docs/gui/mod-manager.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 7fa89bae45..8972a13f6c 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -5,7 +5,7 @@ gui/mod-manager :summary: Manange your active mods. :tags: dfhack interface -In a loaded world, shows a list of active mods with the ability to copyto clipboard. +In a loaded world, shows a list of active mods with the ability to copy to clipboard. Adds an optional overlay to the mod list screen that allows you to save and load mod list presets, as well as set a default mod list preset for new worlds. From f2970b1249c1e1ea79b7775f08eaafff75c7df56 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 17:28:10 -0500 Subject: [PATCH 05/17] Apply suggestions from code review Co-authored-by: Myk --- gui/mod-manager.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 6de6067e93..edd6fd0bbd 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -404,15 +404,12 @@ end ModlistMenu = defclass(ModlistMenu, widgets.Window) ModlistMenu.ATTRS { - view_id = "modlist_menu", frame_title = "Active Modlist", - frame_style = gui.WINDOW_FRAME, resize_min = { w = 30, h = 15 }, frame = { w = 40, t = 10, b = 15 }, resizable = true, - autoarrange_subviews=false, } local function get_mod_id_and_version(path) From 13beea87a15b97a8a7ebcd276aad25a80266e298 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 17:43:38 -0500 Subject: [PATCH 06/17] Update mod-manager.rst --- docs/gui/mod-manager.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 8972a13f6c..52f652efd4 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -6,8 +6,7 @@ gui/mod-manager :tags: dfhack interface In a loaded world, shows a list of active mods with the ability to copy to clipboard. -Adds an optional overlay to the mod list screen that allows you to save and -load mod list presets, as well as set a default mod list preset for new worlds. + Usage ----- @@ -15,3 +14,20 @@ Usage :: gui/mod-manager + +Overlay +------- + +This tool also provides one overlay that is managed by the `overlay` +framework. + +gui/mod-manager.button +~~~~~~~~~~~~~~~~~~~~~~ + +Adds an optional overlay to the mod list screen that allows you to save and +load mod list presets, as well as set a default mod list preset for new worlds. + +gui/mod-manager.notification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Displays a message when a mod preset has been auto-applied. From 89c7d1396f9376e076c43b846aaa6f0e7aa237c1 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 17:51:08 -0500 Subject: [PATCH 07/17] Update mod-manager.lua --- gui/mod-manager.lua | 42 ++++++------------------------------------ 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index edd6fd0bbd..b4a36d0ff8 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -8,6 +8,8 @@ local dialogs = require('gui.dialogs') local json = require('json') local utils = require('utils') +local scriptmanager = require('script-manager') + local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' local INSTALLED_MODS_PATH = 'data/installed_mods/' @@ -407,40 +409,11 @@ ModlistMenu.ATTRS { frame_title = "Active Modlist", resize_min = { w = 30, h = 15 }, - frame = { w = 40, t = 10, b = 15 }, + frame = { w = 40, h = 15 }, resizable = true, } -local function get_mod_id_and_version(path) - local idfile = path .. '/info.txt' - local ok, lines = pcall(io.lines, idfile) - if not ok then return end - local id, version - for line in lines do - if not id then - _,_,id = line:find('^%[ID:([^%]]+)%]') - end - if not version then - -- note this doesn't include the closing brace since some people put - -- non-number characters in here, and DF only reads the leading digits - -- as the numeric version - _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)') - end - -- note that we do *not* want to break out of this loop early since - -- lines has to hit EOF to close the file - end - return id, version -end - -local function add_mod_paths(mod_paths, id, base_path, subdir) - local sep = base_path:endswith('/') and '' or '/' - local path = ('%s%s%s'):format(base_path, sep, subdir) - if dfhack.filesystem.isdir(path) then - table.insert(mod_paths, {id=id, path=path}) - end -end - local function getWorldModlist() -- ordered map of mod id -> {handled=bool, versions=map of version -> path} local mods = utils.OrderedTable() @@ -452,10 +425,10 @@ local function getWorldModlist() path = tostring(path.value) -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end - local id = get_mod_id_and_version(path) + local id = scriptmanager.get_mod_id_and_version(path) if not id then goto continue end mods[id] = {handled=true} - add_mod_paths(mod_paths, id, path, '.') + scriptmanager.add_mod_paths(mod_paths, id, path, '.') ::continue:: end local modlist = {} @@ -485,10 +458,7 @@ function ModlistMenu:init() text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() - local mods = '' - for _,mod in ipairs(getWorldModlist()) do - mods = (mods == '' and mod) or (mods .. ', ' .. mod) - end + local mods = table.concat(getWorldModlist(), ', ') dfhack.internal.setClipboardTextCp437(mods) end, enabled=function() return #modlist:getChoices() > 0 end, From f76c0d2dfba01fd05eef89dfdcdca331937f4222 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 17:54:11 -0500 Subject: [PATCH 08/17] Update docs/gui/mod-manager.rst --- docs/gui/mod-manager.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 52f652efd4..0b957cd2a7 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -18,7 +18,7 @@ Usage Overlay ------- -This tool also provides one overlay that is managed by the `overlay` +This tool also provides two overlays that are managed by the `overlay` framework. gui/mod-manager.button From 02801ab286d375d019c96244c65bb316e113f6cd Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 17:58:02 -0500 Subject: [PATCH 09/17] Update mod-manager.lua --- gui/mod-manager.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index b4a36d0ff8..a3f2e767b4 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -433,7 +433,7 @@ local function getWorldModlist() end local modlist = {} for _,mod in ipairs(mod_paths) do - table.insert(modlist,mod.id) + table.insert(modlist,('%s %s (%s)'):format(mod.name, mod.version, mod.id)) end return modlist end @@ -441,11 +441,6 @@ local function getWorldModlist() end function ModlistMenu:init() - local modlist = widgets.List{ - view_id='modlist', - frame = {t=3}, - choices = getWorldModlist() - } self:addviews{ widgets.Label{ frame = { l=0, t=0 }, @@ -461,9 +456,13 @@ function ModlistMenu:init() local mods = table.concat(getWorldModlist(), ', ') dfhack.internal.setClipboardTextCp437(mods) end, - enabled=function() return #modlist:getChoices() > 0 end, + enabled=function() return #self.subviews.modlist:getChoices() > 0 end, }, - modlist + widgets.List{ + view_id='modlist', + frame = {t=3}, + choices = getWorldModlist() + } } end From 4e6998c73b3215d2f0d42c334ca0b9aecc989699 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 18:05:29 -0500 Subject: [PATCH 10/17] Update mod-manager.lua --- gui/mod-manager.lua | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index a3f2e767b4..33885416e0 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -414,7 +414,7 @@ ModlistMenu.ATTRS { resizable = true, } -local function getWorldModlist() +local function getWorldModlist(detailed) -- ordered map of mod id -> {handled=bool, versions=map of version -> path} local mods = utils.OrderedTable() local mod_paths = {} @@ -433,7 +433,11 @@ local function getWorldModlist() end local modlist = {} for _,mod in ipairs(mod_paths) do - table.insert(modlist,('%s %s (%s)'):format(mod.name, mod.version, mod.id)) + if detailed then + table.insert(modlist,('%s %s (%s)'):format(mod.name, mod.version, mod.id)) + else + table.insert(modlist,mod.name) + end end return modlist end @@ -458,9 +462,21 @@ function ModlistMenu:init() end, enabled=function() return #self.subviews.modlist:getChoices() > 0 end, }, + widgets.HotkeyLabel{ + view_id='copy', + frame={t=1, r=1}, + label='Copy mod and details to clipboard', + text_pen=COLOR_YELLOW, + auto_width=true, + on_activate=function() + local mods = table.concat(getWorldModlist(true), NEWLINE) + dfhack.internal.setClipboardTextCp437Multiline(mods) + end, + enabled=function() return #self.subviews.modlist:getChoices() > 0 end, + }, widgets.List{ view_id='modlist', - frame = {t=3}, + frame = {t=4}, choices = getWorldModlist() } } From 52a355832be708a1ad1136ccbb49e39f041e372d Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 18:08:04 -0500 Subject: [PATCH 11/17] Update mod-manager.lua --- gui/mod-manager.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 33885416e0..dcf25d31cd 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -453,7 +453,7 @@ function ModlistMenu:init() widgets.HotkeyLabel{ view_id='copy', frame={t=1, r=1}, - label='Copy modlist to clipboard', + label='Copy mod names to clipboard', text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() @@ -465,7 +465,7 @@ function ModlistMenu:init() widgets.HotkeyLabel{ view_id='copy', frame={t=1, r=1}, - label='Copy mod and details to clipboard', + label='Copy list to clipboard', text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() @@ -477,7 +477,7 @@ function ModlistMenu:init() widgets.List{ view_id='modlist', frame = {t=4}, - choices = getWorldModlist() + choices = getWorldModlist(true) } } end From 00fd97b4a72eabfc6298d113f863cecf88a6cf5c Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 18:48:43 -0500 Subject: [PATCH 12/17] Update mod-manager.lua --- gui/mod-manager.lua | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index dcf25d31cd..f4891c106e 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -425,18 +425,23 @@ local function getWorldModlist(detailed) path = tostring(path.value) -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end - local id = scriptmanager.get_mod_id_and_version(path) - if not id then goto continue end - mods[id] = {handled=true} + local id, version, name, steam_id= scriptmanager.get_mod_id_and_version(path) + if not id or not version then goto continue end + mods[id]= {handled=true, name=name, version=version, steam_id=steam_id} scriptmanager.add_mod_paths(mod_paths, id, path, '.') ::continue:: end local modlist = {} for _,mod in ipairs(mod_paths) do + printall_recurse(mods) if detailed then - table.insert(modlist,('%s %s (%s)'):format(mod.name, mod.version, mod.id)) + local url + if mods[mod.id].steam_id then + url = 'https://steamcommunity.com/sharedfiles/filedetails/?id='.. mods[mod.id].steam_id + end + table.insert(modlist,('%s %s (%s): %s'):format(mods[mod.id].name or mod.id, mods[mod.id].version or '', mod.id, url or '')) else - table.insert(modlist,mod.name) + table.insert(modlist,mods[mod.id].name or mod.id) end end return modlist @@ -464,7 +469,7 @@ function ModlistMenu:init() }, widgets.HotkeyLabel{ view_id='copy', - frame={t=1, r=1}, + frame={t=2, r=1}, label='Copy list to clipboard', text_pen=COLOR_YELLOW, auto_width=true, From 11cb1deea89651ccb4372142b63cc58a6a194276 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 18:57:49 -0500 Subject: [PATCH 13/17] Update mod-manager.lua --- gui/mod-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index f4891c106e..2edd503b55 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -433,7 +433,6 @@ local function getWorldModlist(detailed) end local modlist = {} for _,mod in ipairs(mod_paths) do - printall_recurse(mods) if detailed then local url if mods[mod.id].steam_id then From 119b6b22689f6f5718fb69301f4490969a14e667 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 21:26:45 -0500 Subject: [PATCH 14/17] Update mod-manager.lua --- gui/mod-manager.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 2edd503b55..7a7897c816 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -425,9 +425,9 @@ local function getWorldModlist(detailed) path = tostring(path.value) -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end - local id, version, name, steam_id= scriptmanager.get_mod_id_and_version(path) - if not id or not version then goto continue end - mods[id]= {handled=true, name=name, version=version, steam_id=steam_id} + local id, numerical_version, name, steam_id, display_version = scriptmanager.get_mod_info(path) + if not id or not numerical_version then goto continue end + mods[id]= {handled=true, name=name, version=display_version, steam_id=steam_id} scriptmanager.add_mod_paths(mod_paths, id, path, '.') ::continue:: end @@ -436,9 +436,9 @@ local function getWorldModlist(detailed) if detailed then local url if mods[mod.id].steam_id then - url = 'https://steamcommunity.com/sharedfiles/filedetails/?id='.. mods[mod.id].steam_id + url = ': https://steamcommunity.com/sharedfiles/filedetails/?id='.. mods[mod.id].steam_id end - table.insert(modlist,('%s %s (%s): %s'):format(mods[mod.id].name or mod.id, mods[mod.id].version or '', mod.id, url or '')) + table.insert(modlist,('%s %s (%s)%s'):format(mods[mod.id].name or mod.id, mods[mod.id].version or '', mod.id, url or '')) else table.insert(modlist,mods[mod.id].name or mod.id) end From a6f80ba323ac616259793d3a7a88b8fba9671b92 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 13 Apr 2025 14:46:21 -0500 Subject: [PATCH 15/17] Update mod-manager.lua --- gui/mod-manager.lua | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7a7897c816..69cd2ae804 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,7 +12,6 @@ local scriptmanager = require('script-manager') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' -local INSTALLED_MODS_PATH = 'data/installed_mods/' -- get_newregion_viewscreen and get_modlist_fields are declared as global functions -- so external tools can call them to get the DF mod list @@ -414,23 +413,14 @@ ModlistMenu.ATTRS { resizable = true, } -local function getWorldModlist(detailed) +local function getWorldModlist(detailed, include_vanilla) -- ordered map of mod id -> {handled=bool, versions=map of version -> path} local mods = utils.OrderedTable() local mod_paths = {} -- if a world is loaded, process active mods first, and lock to active version if dfhack.isWorldLoaded() then - for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do - path = tostring(path.value) - -- skip vanilla "mods" - if not path:startswith(INSTALLED_MODS_PATH) then goto continue end - local id, numerical_version, name, steam_id, display_version = scriptmanager.get_mod_info(path) - if not id or not numerical_version then goto continue end - mods[id]= {handled=true, name=name, version=display_version, steam_id=steam_id} - scriptmanager.add_mod_paths(mod_paths, id, path, '.') - ::continue:: - end + scriptmanager.getAllModsInfo(include_vanilla, mods, mod_paths) local modlist = {} for _,mod in ipairs(mod_paths) do if detailed then @@ -449,39 +439,45 @@ local function getWorldModlist(detailed) end function ModlistMenu:init() + local include_vanilla = false self:addviews{ widgets.Label{ frame = { l=0, t=0 }, text = {'Active mods:'}, }, widgets.HotkeyLabel{ - view_id='copy', + view_id='copy_names', frame={t=1, r=1}, label='Copy mod names to clipboard', text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() - local mods = table.concat(getWorldModlist(), ', ') + local mods = table.concat(getWorldModlist(false, include_vanilla), ', ') dfhack.internal.setClipboardTextCp437(mods) end, enabled=function() return #self.subviews.modlist:getChoices() > 0 end, }, widgets.HotkeyLabel{ - view_id='copy', + view_id='copy_list', frame={t=2, r=1}, label='Copy list to clipboard', text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() - local mods = table.concat(getWorldModlist(true), NEWLINE) + local mods = table.concat(getWorldModlist(true,include_vanilla), NEWLINE) dfhack.internal.setClipboardTextCp437Multiline(mods) end, enabled=function() return #self.subviews.modlist:getChoices() > 0 end, }, widgets.List{ view_id='modlist', - frame = {t=4}, - choices = getWorldModlist(true) + frame = {t=4,b=2}, + choices = getWorldModlist(true,include_vanilla) + }, + widgets.HotkeyLabel{ + frame={b=1}, + label='Include Vanilla Mods: ' .. ((include_vanilla and 'Yes') or 'No'), + on_activate=function () include_vanilla = not include_vanilla end } } end From fca078817e0951e8e579a7f4de45959d55b7d350 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 13 Apr 2025 16:07:33 -0500 Subject: [PATCH 16/17] Update mod-manager.lua --- gui/mod-manager.lua | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 69cd2ae804..6284eb1c73 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -408,7 +408,7 @@ ModlistMenu.ATTRS { frame_title = "Active Modlist", resize_min = { w = 30, h = 15 }, - frame = { w = 40, h = 15 }, + frame = { w = 40, h = 20 }, resizable = true, } @@ -439,7 +439,7 @@ local function getWorldModlist(detailed, include_vanilla) end function ModlistMenu:init() - local include_vanilla = false + self.include_vanilla = self.include_vanilla or false self:addviews{ widgets.Label{ frame = { l=0, t=0 }, @@ -452,7 +452,7 @@ function ModlistMenu:init() text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() - local mods = table.concat(getWorldModlist(false, include_vanilla), ', ') + local mods = table.concat(getWorldModlist(false, self.include_vanilla), ', ') dfhack.internal.setClipboardTextCp437(mods) end, enabled=function() return #self.subviews.modlist:getChoices() > 0 end, @@ -464,7 +464,7 @@ function ModlistMenu:init() text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() - local mods = table.concat(getWorldModlist(true,include_vanilla), NEWLINE) + local mods = table.concat(getWorldModlist(true, self.include_vanilla), NEWLINE) dfhack.internal.setClipboardTextCp437Multiline(mods) end, enabled=function() return #self.subviews.modlist:getChoices() > 0 end, @@ -472,12 +472,18 @@ function ModlistMenu:init() widgets.List{ view_id='modlist', frame = {t=4,b=2}, - choices = getWorldModlist(true,include_vanilla) + choices = getWorldModlist(true,self.include_vanilla) }, widgets.HotkeyLabel{ - frame={b=1}, - label='Include Vanilla Mods: ' .. ((include_vanilla and 'Yes') or 'No'), - on_activate=function () include_vanilla = not include_vanilla end + view_id='include_vanilla', + frame={b=0}, + key='CUSTOM_V', + label='Include Vanilla Mods: ' .. ((self.include_vanilla and 'Yes') or 'No'), + on_activate=function () + self.include_vanilla = not self.include_vanilla + self.subviews.include_vanilla:setLabel('Include Vanilla Mods: ' .. ((self.include_vanilla and 'Yes') or 'No')) + self.subviews.modlist:setChoices(getWorldModlist(true,self.include_vanilla)) + end } } end From 92d867a6175e3ff710defa8aca44262922928997 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:45:00 -0700 Subject: [PATCH 17/17] revise gui/mod-manager UI --- changelog.txt | 2 +- docs/gui/mod-manager.rst | 9 +- gui/mod-manager.lua | 197 ++++++++++++++++++++++++--------------- 3 files changed, 130 insertions(+), 78 deletions(-) diff --git a/changelog.txt b/changelog.txt index 21b32850d2..26badf3a66 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools ## New Features -- `gui/mod-manager`: when run in a loaded world, opens a copyable list of active mods. +- `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 0b957cd2a7..56b1538f29 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -5,8 +5,8 @@ gui/mod-manager :summary: Manange your active mods. :tags: dfhack interface -In a loaded world, shows a list of active mods with the ability to copy to clipboard. - +When run with a world loaded, shows a list of active mods. You can copy the +list to the system clipboard for easy sharing or posting. Usage ----- @@ -24,8 +24,9 @@ framework. gui/mod-manager.button ~~~~~~~~~~~~~~~~~~~~~~ -Adds an optional overlay to the mod list screen that allows you to save and -load mod list presets, as well as set a default mod list preset for new worlds. +Adds a widget to the mod list screen that allows you to save and load mod list +presets. You can also set a default mod list preset for new worlds so you don't +have to manualy re-select the same mods every time you generate a world. gui/mod-manager.notification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 6284eb1c73..ee3a489767 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -1,14 +1,13 @@ --- Save and restore lists of active mods. +-- Show, save, and restore lists of active mods. --@ module = true -local overlay = require('plugins.overlay') -local gui = require('gui') -local widgets = require('gui.widgets') local dialogs = require('gui.dialogs') +local gui = require('gui') local json = require('json') -local utils = require('utils') - +local overlay = require('plugins.overlay') local scriptmanager = require('script-manager') +local utils = require('utils') +local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' @@ -121,6 +120,9 @@ local function swap_modlist(viewscreen, modlist) return failures end +-------------------- +-- ModmanageMenu + ModmanageMenu = defclass(ModmanageMenu, widgets.Window) ModmanageMenu.ATTRS { view_id = "modman_menu", @@ -391,6 +393,9 @@ function ModmanageMenu:init() } end +-------------------- +-- ModmanageScreen + ModmanageScreen = defclass(ModmanageScreen, gui.ZScreen) ModmanageScreen.ATTRS { focus_path = "mod-manager", @@ -403,100 +408,139 @@ function ModmanageScreen:init() } end -ModlistMenu = defclass(ModlistMenu, widgets.Window) -ModlistMenu.ATTRS { - frame_title = "Active Modlist", - - resize_min = { w = 30, h = 15 }, - frame = { w = 40, h = 20 }, +-------------------- +-- ModlistWindow - resizable = true, +ModlistWindow = defclass(ModlistWindow, widgets.Window) +ModlistWindow.ATTRS{ + frame_title="Active Mods", + frame={w=55, h=20}, + resizable=true, } -local function getWorldModlist(detailed, include_vanilla) - -- ordered map of mod id -> {handled=bool, versions=map of version -> path} - local mods = utils.OrderedTable() - local mod_paths = {} - - -- if a world is loaded, process active mods first, and lock to active version - if dfhack.isWorldLoaded() then - scriptmanager.getAllModsInfo(include_vanilla, mods, mod_paths) - local modlist = {} - for _,mod in ipairs(mod_paths) do - if detailed then - local url - if mods[mod.id].steam_id then - url = ': https://steamcommunity.com/sharedfiles/filedetails/?id='.. mods[mod.id].steam_id - end - table.insert(modlist,('%s %s (%s)%s'):format(mods[mod.id].name or mod.id, mods[mod.id].version or '', mod.id, url or '')) - else - table.insert(modlist,mods[mod.id].name or mod.id) - end +local function get_num_vanilla_mods() + local count = 0 + for _,mod in ipairs(scriptmanager.get_active_mods()) do + if mod.vanilla then + count = count + 1 + end + end + return count +end + +local function get_num_non_vanilla_mods() + local count = 0 + for _,mod in ipairs(scriptmanager.get_active_mods()) do + if not mod.vanilla then + count = count + 1 end - return modlist end - qerror('No world is loaded') + return count end -function ModlistMenu:init() - self.include_vanilla = self.include_vanilla or false +function ModlistWindow:init() self:addviews{ - widgets.Label{ - frame = { l=0, t=0 }, - text = {'Active mods:'}, - }, - widgets.HotkeyLabel{ - view_id='copy_names', - frame={t=1, r=1}, - label='Copy mod names to clipboard', - text_pen=COLOR_YELLOW, - auto_width=true, - on_activate=function() - local mods = table.concat(getWorldModlist(false, self.include_vanilla), ', ') - dfhack.internal.setClipboardTextCp437(mods) - end, - enabled=function() return #self.subviews.modlist:getChoices() > 0 end, + widgets.CycleHotkeyLabel{ + view_id='vanilla', + frame={l=0, t=0, w=24}, + key='CUSTOM_V', + label='Vanilla mods:', + options={ + {label='Include', value=true, pen=COLOR_LIGHTBLUE}, + {label='Exclude', value=false, pen=COLOR_LIGHTRED}, + }, + initial_option=false, + on_change=function() self:refresh_list() end, }, widgets.HotkeyLabel{ - view_id='copy_list', - frame={t=2, r=1}, + frame={t=0, r=0}, label='Copy list to clipboard', text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() - local mods = table.concat(getWorldModlist(true, self.include_vanilla), NEWLINE) - dfhack.internal.setClipboardTextCp437Multiline(mods) + local text = {} + for _,choice in ipairs(self.subviews.list:getChoices()) do + table.insert(text, choice.export_text) + end + dfhack.internal.setClipboardTextCp437Multiline(table.concat(text, NEWLINE)) end, - enabled=function() return #self.subviews.modlist:getChoices() > 0 end, + enabled=function() return #self.subviews.list:getChoices() > 0 end, + }, + widgets.Divider{ + frame={t=2, h=1}, + frame_style=gui.FRAME_THIN, + frame_style_l=false, + frame_style_r=false, + }, + widgets.Label{ + frame={l=0, t=3}, + text={ + 'Load', + NEWLINE, + 'order', + }, + }, + widgets.Label{ + frame={l=7, t=4}, + text='Mod', }, widgets.List{ - view_id='modlist', - frame = {t=4,b=2}, - choices = getWorldModlist(true,self.include_vanilla) + view_id='list', + frame={t=6, b=2}, + }, + widgets.Label{ + frame={l=0, b=0}, + text={ + {text=('%d'):format(get_num_vanilla_mods()), pen=COLOR_LIGHTBLUE}, + ' vanilla mods', + {text=function() return self.subviews.vanilla:getOptionValue() and '' or ' (hidden)' end}, + ', ', + {text=('%d'):format(get_num_non_vanilla_mods()), pen=COLOR_BROWN}, + ' non-vanilla mods', + }, }, - widgets.HotkeyLabel{ - view_id='include_vanilla', - frame={b=0}, - key='CUSTOM_V', - label='Include Vanilla Mods: ' .. ((self.include_vanilla and 'Yes') or 'No'), - on_activate=function () - self.include_vanilla = not self.include_vanilla - self.subviews.include_vanilla:setLabel('Include Vanilla Mods: ' .. ((self.include_vanilla and 'Yes') or 'No')) - self.subviews.modlist:setChoices(getWorldModlist(true,self.include_vanilla)) - end - } } + + self:refresh_list() +end + +function ModlistWindow:refresh_list() + local include_vanilla = self.subviews.vanilla:getOptionValue() + + local choices = {} + for idx,mod in ipairs(scriptmanager.get_active_mods()) do + if not include_vanilla and mod.vanilla then goto continue end + local steam_id = scriptmanager.get_mod_info_metadata(mod.path, 'STEAM_FILE_ID').STEAM_FILE_ID + local url = steam_id and (': https://steamcommunity.com/sharedfiles/filedetails/?id=%s'):format(steam_id) or '' + table.insert(choices, { + text={ + {text=idx, width=2, rjustify=true}, + ') ', + {text=mod.name, gap=3}, + ' (', + {text=mod.version, pen=COLOR_LIGHTGREEN}, + ')', + }, + data=mod, + export_text=('- %s (%s)%s'):format(mod.name, mod.version, url), + }) + ::continue:: + end + + self.subviews.list:setChoices(choices) end +-------------------- +-- ModlistScreen ModlistScreen = defclass(ModlistScreen, gui.ZScreen) -ModlistScreen.ATTRS { - focus_path = "modlist", +ModlistScreen.ATTRS{ + focus_path="mod-manager", } function ModlistScreen:init() self:addviews{ - ModlistMenu{} + ModlistWindow{} } end @@ -504,6 +548,9 @@ function ModlistScreen:onDismiss() view = nil end +-------------------- +-- Overlays + ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) ModmanageOverlay.ATTRS { frame = { w=16, h=3 }, @@ -597,4 +644,8 @@ if dfhack_flags.module then return end +if not dfhack.isWorldLoaded() then + qerror("Please load a game before using the mod manager to see active mods.") +end + view = view and view:raise() or ModlistScreen{}:show()