From 9da692565a28d69e9e4bfb6ce9bdcb85164e4dc8 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 16:18:45 -0600 Subject: [PATCH 01/84] New Feature: `gym` Code for dwarves to hit the gym when they yearn for the gains. Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training --- gym.lua | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 gym.lua diff --git a/gym.lua b/gym.lua new file mode 100644 index 000000000..f4e99f440 --- /dev/null +++ b/gym.lua @@ -0,0 +1,261 @@ +-- Code for dwarves to hit the gym when they yearn for the gains. +--[====[ +Gym +================= + +Tags: Fort| Needs | BugFix | Units + +Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also Passively builds military skills and physical stats. + +CRITICAL SETUP: +01-Minimum 1 squad with the name "Gym" +02-An assigned squadleader in "Gym" +03-An assigned Barracks for the squad "Gym" +04-Active Training orders for the squad "Gym" + +This should be a new non military use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. +Set the squad's schedule to full time training with at least 8 or 9 training. +The squad doesn't need months off. The members leave the squad once they have gotten their gains. + +NOTE-Dwarfs with the labor "Fish Dissection" enabled are ignored +Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. + +Usage +-------- + + Gym [] + +Examples +-------- + +Gym + Current status of script + +Gym -start + checks to see if you have fullfilled the creation of a training gym + searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. + Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. + +Gym -stop + Dwarves currently in the squad ,with the exception of the squadleader, will be unassigned and no new dwarves will be added to the squad. + +Options +------- + -start + Starts the script + If there is no squad named GYM with a squadleader assigned it will not proceed. + + -stop + Stops the script + + -t + Use integer values. (Default 3000) + The negative need threshhold to trigger for each citizen + The greater the number the longer before a dwarf is added to the waiting list. +]====] + +local repeatUtil = require 'repeat-util' +local utils=require('utils') + +validArgs = utils.invert({ + 'start', + 'stop', + 't' +}) + +local args = utils.processArgs({...}, validArgs) +local scriptname = "Gym" +local ignore_flag = 43 -- Fish Dissection labor id +local ignore_count = 0 +local need_id = 14 +local squadname ="Gym" + + +--###### +--Functions +--###### +function getAllCititzen() + local citizen = {} + local my_civ = df.global.world.world_data.active_site[0].entity_links[0].entity_id + for n, unit in ipairs(df.global.world.units.all) do + if unit.civ_id == my_civ and dfhack.units.isCitizen(unit) then + if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then + if ( not unit.status.labors[ignore_flag] ) then + table.insert(citizen, unit) + else + ignore_count = ignore_count +1 + end + end + end + end + return citizen +end + +local citizen = getAllCititzen() + +function findNeed(unit,need_id) + local needs = unit.status.current_soul.personality.needs + local need_index = -1 + for k = #needs-1,0,-1 do + if needs[k].id == need_id then + need_index = k + break + end + end if (need_index ~= -1 ) then + return needs[need_index] + end + return nil +end + +--###### +--Main +--###### + +function getByID(id) + for n, unit in ipairs(citizen) do + if (unit.hist_figure_id == id) then + return unit + end + end + + return nil +end + +-- Find all training squads +-- Abort if no squads found +function checkSquads() + local squads = {} + local count = 0 + for n, mil in ipairs(df.global.world.squads.all) do + if (mil.alias == squadname) then + local leader = mil.positions[0].occupant + if ( leader ~= -1) then + table.insert(squads,mil) + count = count +1 + end + end + end + + if (count == 0) then + dfhack.print(scriptname.." | ") + dfhack.printerr('ERROR: You need a squad with the name ' .. squadname) + dfhack.print(scriptname.." | ") + dfhack.printerr('That has an active Squad Leader') + dfhack.color(-1) + return nil + end + + return squads +end + +function addTraining(squads,unit) + for n, squad in ipairs(squads) do + for i=1,9,1 do + if (unit.hist_figure_id == squad.positions[i].occupant) then + return true + end + + if (unit.military.squad_id ~= -1) then + return false + end + + if ( squad.positions[i].occupant == -1 ) then + squad.positions[i].occupant = unit.hist_figure_id + return true + end + end + end + + return false +end + +function removeTraining(squads,unit) + for n, squad in ipairs(squads) do + for i=1,9,1 do + if ( unit.hist_figure_id == squad.positions[i].occupant ) then + unit.military.squad_id = -1 + unit.military.squad_position = -1 + squad.positions[i].occupant = -1 + return true + end + end + end + return false +end + +function removeAll(squads) + if ( squads == nil) then return end + for n, squad in ipairs(squads) do + for i=1,9,1 do + local dwarf = getByID(squad.positions[i].occupant) + if (dwarf ~= nil) then + dwarf.military.squad_id = -1 + dwarf.military.squad_position = -1 + squad.positions[i].occupant = -1 + end + end + end +end + + +function check() + local squads = checkSquads() + local intraining_count = 0 + local inque_count = 0 + if ( squads == nil)then return end + for n, unit in ipairs(citizen) do + local need = findNeed(unit,need_id) + if ( need ~= nil ) then + if ( need.focus_level < threshold ) then + local bol = addTraining(squads,unit) + if ( bol ) then + intraining_count = intraining_count +1 + else + inque_count = inque_count +1 + end + else + removeTraining(squads,unit) + end + end + end + + dfhack.println(scriptname .. " | IGN: " .. ignore_count .. " TRAIN: " .. intraining_count .. " QUE: " ..inque_count ) +end + + +function start() + threshold = -5000 + dfhack.println(scriptname .. " | START") + + if (args.t) then + threshold = 0-tonumber(args.t) + end + + running = true + repeatUtil.scheduleEvery(scriptname,1000,'ticks',check) +end + +function stop() + repeatUtil.cancel(scriptname) + local squads = checkSquads() + removeAll(squads) + running = false + dfhack.println(scriptname .. " | STOP") +end + +if (args.stop) then + if (running) then stop() end + return +end + +if (args.start) then + if (running) then stop() end + start() + return +end + +if ( running ) then + dfhack.println(scriptname .." | Enabled") +else + dfhack.println(scriptname .." | Disabled") +end From 81127f2d2ab0ae506cecb27ee792c99107455224 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 16:20:31 -0600 Subject: [PATCH 02/84] Fix whitespace --- gym.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gym.lua b/gym.lua index f4e99f440..06afa0c77 100644 --- a/gym.lua +++ b/gym.lua @@ -81,9 +81,9 @@ function getAllCititzen() if unit.civ_id == my_civ and dfhack.units.isCitizen(unit) then if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then if ( not unit.status.labors[ignore_flag] ) then - table.insert(citizen, unit) - else - ignore_count = ignore_count +1 + table.insert(citizen, unit) + else + ignore_count = ignore_count +1 end end end @@ -101,7 +101,7 @@ function findNeed(unit,need_id) need_index = k break end - end if (need_index ~= -1 ) then + end if (need_index ~= -1 ) then return needs[need_index] end return nil @@ -226,9 +226,9 @@ end function start() threshold = -5000 dfhack.println(scriptname .. " | START") - + if (args.t) then - threshold = 0-tonumber(args.t) + threshold = 0-tonumber(args.t) end running = true From 5c08fb6aaa8e7b28a4e4b6208cebbe495043ab6c Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 16:21:02 -0600 Subject: [PATCH 03/84] missed some --- gym.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gym.lua b/gym.lua index 06afa0c77..685a46669 100644 --- a/gym.lua +++ b/gym.lua @@ -128,7 +128,7 @@ function checkSquads() local count = 0 for n, mil in ipairs(df.global.world.squads.all) do if (mil.alias == squadname) then - local leader = mil.positions[0].occupant + local leader = mil.positions[0].occupant if ( leader ~= -1) then table.insert(squads,mil) count = count +1 @@ -160,7 +160,7 @@ function addTraining(squads,unit) end if ( squad.positions[i].occupant == -1 ) then - squad.positions[i].occupant = unit.hist_figure_id + squad.positions[i].occupant = unit.hist_figure_id return true end end From edb3e830b56f78f2468876313617818244a206e1 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 16:26:49 -0600 Subject: [PATCH 04/84] MORE whitespace (and some other cleanup) --- gym.lua | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/gym.lua b/gym.lua index 685a46669..6af96a143 100644 --- a/gym.lua +++ b/gym.lua @@ -1,7 +1,7 @@ -- Code for dwarves to hit the gym when they yearn for the gains. --[====[ Gym -================= +=== Tags: Fort| Needs | BugFix | Units @@ -93,7 +93,7 @@ end local citizen = getAllCititzen() -function findNeed(unit,need_id) +function findNeed(unit,need_id) local needs = unit.status.current_soul.personality.needs local need_index = -1 for k = #needs-1,0,-1 do @@ -111,7 +111,7 @@ end --Main --###### -function getByID(id) +function getByID(id) for n, unit in ipairs(citizen) do if (unit.hist_figure_id == id) then return unit @@ -169,7 +169,7 @@ function addTraining(squads,unit) return false end -function removeTraining(squads,unit) +function removeTraining(squads,unit) for n, squad in ipairs(squads) do for i=1,9,1 do if ( unit.hist_figure_id == squad.positions[i].occupant ) then @@ -183,7 +183,7 @@ function removeTraining(squads,unit) return false end -function removeAll(squads) +function removeAll(squads) if ( squads == nil) then return end for n, squad in ipairs(squads) do for i=1,9,1 do @@ -208,9 +208,9 @@ function check() if ( need ~= nil ) then if ( need.focus_level < threshold ) then local bol = addTraining(squads,unit) - if ( bol ) then + if ( bol ) then intraining_count = intraining_count +1 - else + else inque_count = inque_count +1 end else @@ -223,11 +223,11 @@ function check() end -function start() +function start() threshold = -5000 dfhack.println(scriptname .. " | START") - if (args.t) then + if (args.t) then threshold = 0-tonumber(args.t) end @@ -246,9 +246,9 @@ end if (args.stop) then if (running) then stop() end return -end +end -if (args.start) then +if (args.start) then if (running) then stop() end start() return From 0c4f5b5729cc495a6ed4e24b48821a536a49644b Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 16:29:11 -0600 Subject: [PATCH 05/84] Update gym.lua --- gym.lua | 220 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/gym.lua b/gym.lua index 6af96a143..199ceae37 100644 --- a/gym.lua +++ b/gym.lua @@ -21,7 +21,7 @@ NOTE-Dwarfs with the labor "Fish Dissection" enabled are ignored Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. Usage --------- +----- Gym [] @@ -60,7 +60,7 @@ local utils=require('utils') validArgs = utils.invert({ 'start', 'stop', - 't' + 't' }) local args = utils.processArgs({...}, validArgs) @@ -75,20 +75,20 @@ local squadname ="Gym" --Functions --###### function getAllCititzen() - local citizen = {} - local my_civ = df.global.world.world_data.active_site[0].entity_links[0].entity_id - for n, unit in ipairs(df.global.world.units.all) do + local citizen = {} + local my_civ = df.global.world.world_data.active_site[0].entity_links[0].entity_id + for n, unit in ipairs(df.global.world.units.all) do if unit.civ_id == my_civ and dfhack.units.isCitizen(unit) then if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then if ( not unit.status.labors[ignore_flag] ) then - table.insert(citizen, unit) - else - ignore_count = ignore_count +1 - end + table.insert(citizen, unit) + else + ignore_count = ignore_count +1 + end end end - end - return citizen + end + return citizen end local citizen = getAllCititzen() @@ -112,135 +112,135 @@ end --###### function getByID(id) - for n, unit in ipairs(citizen) do - if (unit.hist_figure_id == id) then - return unit - end - end + for n, unit in ipairs(citizen) do + if (unit.hist_figure_id == id) then + return unit + end + end - return nil + return nil end -- Find all training squads -- Abort if no squads found function checkSquads() - local squads = {} - local count = 0 - for n, mil in ipairs(df.global.world.squads.all) do - if (mil.alias == squadname) then - local leader = mil.positions[0].occupant - if ( leader ~= -1) then - table.insert(squads,mil) - count = count +1 - end - end - end - - if (count == 0) then - dfhack.print(scriptname.." | ") + local squads = {} + local count = 0 + for n, mil in ipairs(df.global.world.squads.all) do + if (mil.alias == squadname) then + local leader = mil.positions[0].occupant + if ( leader ~= -1) then + table.insert(squads,mil) + count = count +1 + end + end + end + + if (count == 0) then + dfhack.print(scriptname.." | ") dfhack.printerr('ERROR: You need a squad with the name ' .. squadname) - dfhack.print(scriptname.." | ") - dfhack.printerr('That has an active Squad Leader') - dfhack.color(-1) - return nil - end + dfhack.print(scriptname.." | ") + dfhack.printerr('That has an active Squad Leader') + dfhack.color(-1) + return nil + end - return squads + return squads end function addTraining(squads,unit) - for n, squad in ipairs(squads) do - for i=1,9,1 do - if (unit.hist_figure_id == squad.positions[i].occupant) then - return true - end - - if (unit.military.squad_id ~= -1) then - return false - end - - if ( squad.positions[i].occupant == -1 ) then - squad.positions[i].occupant = unit.hist_figure_id - return true - end - end - end - - return false + for n, squad in ipairs(squads) do + for i=1,9,1 do + if (unit.hist_figure_id == squad.positions[i].occupant) then + return true + end + + if (unit.military.squad_id ~= -1) then + return false + end + + if ( squad.positions[i].occupant == -1 ) then + squad.positions[i].occupant = unit.hist_figure_id + return true + end + end + end + + return false end function removeTraining(squads,unit) - for n, squad in ipairs(squads) do - for i=1,9,1 do - if ( unit.hist_figure_id == squad.positions[i].occupant ) then - unit.military.squad_id = -1 - unit.military.squad_position = -1 - squad.positions[i].occupant = -1 - return true - end - end - end - return false + for n, squad in ipairs(squads) do + for i=1,9,1 do + if ( unit.hist_figure_id == squad.positions[i].occupant ) then + unit.military.squad_id = -1 + unit.military.squad_position = -1 + squad.positions[i].occupant = -1 + return true + end + end + end + return false end function removeAll(squads) - if ( squads == nil) then return end - for n, squad in ipairs(squads) do - for i=1,9,1 do - local dwarf = getByID(squad.positions[i].occupant) - if (dwarf ~= nil) then - dwarf.military.squad_id = -1 - dwarf.military.squad_position = -1 - squad.positions[i].occupant = -1 - end - end - end + if ( squads == nil) then return end + for n, squad in ipairs(squads) do + for i=1,9,1 do + local dwarf = getByID(squad.positions[i].occupant) + if (dwarf ~= nil) then + dwarf.military.squad_id = -1 + dwarf.military.squad_position = -1 + squad.positions[i].occupant = -1 + end + end + end end function check() - local squads = checkSquads() - local intraining_count = 0 - local inque_count = 0 - if ( squads == nil)then return end - for n, unit in ipairs(citizen) do - local need = findNeed(unit,need_id) - if ( need ~= nil ) then - if ( need.focus_level < threshold ) then - local bol = addTraining(squads,unit) - if ( bol ) then - intraining_count = intraining_count +1 - else - inque_count = inque_count +1 - end - else - removeTraining(squads,unit) - end - end - end - - dfhack.println(scriptname .. " | IGN: " .. ignore_count .. " TRAIN: " .. intraining_count .. " QUE: " ..inque_count ) + local squads = checkSquads() + local intraining_count = 0 + local inque_count = 0 + if ( squads == nil)then return end + for n, unit in ipairs(citizen) do + local need = findNeed(unit,need_id) + if ( need ~= nil ) then + if ( need.focus_level < threshold ) then + local bol = addTraining(squads,unit) + if ( bol ) then + intraining_count = intraining_count +1 + else + inque_count = inque_count +1 + end + else + removeTraining(squads,unit) + end + end + end + + dfhack.println(scriptname .. " | IGN: " .. ignore_count .. " TRAIN: " .. intraining_count .. " QUE: " ..inque_count ) end function start() - threshold = -5000 - dfhack.println(scriptname .. " | START") + threshold = -5000 + dfhack.println(scriptname .. " | START") - if (args.t) then - threshold = 0-tonumber(args.t) - end + if (args.t) then + threshold = 0-tonumber(args.t) + end - running = true - repeatUtil.scheduleEvery(scriptname,1000,'ticks',check) + running = true + repeatUtil.scheduleEvery(scriptname,1000,'ticks',check) end function stop() - repeatUtil.cancel(scriptname) - local squads = checkSquads() - removeAll(squads) - running = false - dfhack.println(scriptname .. " | STOP") + repeatUtil.cancel(scriptname) + local squads = checkSquads() + removeAll(squads) + running = false + dfhack.println(scriptname .. " | STOP") end if (args.stop) then From 50d6a963fcd0c4aed2f157961f8d4ea5de5bdf8a Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 22:16:15 -0600 Subject: [PATCH 06/84] Create gym.rst --- docs/gym.rst | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/gym.rst diff --git a/docs/gym.rst b/docs/gym.rst new file mode 100644 index 000000000..e82cc9035 --- /dev/null +++ b/docs/gym.rst @@ -0,0 +1,54 @@ +Gym +=== + +.. dfhack-tool:: + :summary: Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training + :tags: Fort Needs BugFix Units + +Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also Passively builds military skills and physical stats. + +CRITICAL SETUP: +01-Minimum 1 squad with the name "Gym" +02-An assigned squadleader in "Gym" +03-An assigned Barracks for the squad "Gym" +04-Active Training orders for the squad "Gym" + +This should be a new non military use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. +Set the squad's schedule to full time training with at least 8 or 9 training. +The squad doesn't need months off. The members leave the squad once they have gotten their gains. + +NOTE-Dwarfs with the labor "Fish Dissection" enabled are ignored +Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. + +Usage +----- + + ``gym []`` + +Examples +-------- + +``gym`` + Current status of script + +``gym -start`` + checks to see if you have fullfilled the creation of a training gym + searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. + Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. + +``gym -stop`` + Dwarves currently in the squad ,with the exception of the squadleader, will be unassigned and no new dwarves will be added to the squad. + +Options +------- + ``-start`` + Starts the script + If there is no squad named GYM with a squadleader assigned it will not proceed. + + ``-stop`` + Stops the script + + ``-t`` + Use integer values. (Default 3000) + The negative need threshhold to trigger for each citizen + The greater the number the longer before a dwarf is added to the waiting list. \ No newline at end of file From deca155bc6a2735dc4307af3194ae50df6190354 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 22:17:03 -0600 Subject: [PATCH 07/84] Fix EOF --- docs/gym.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gym.rst b/docs/gym.rst index e82cc9035..bee3dbbbf 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -51,4 +51,4 @@ Options ``-t`` Use integer values. (Default 3000) The negative need threshhold to trigger for each citizen - The greater the number the longer before a dwarf is added to the waiting list. \ No newline at end of file + The greater the number the longer before a dwarf is added to the waiting list. From 74784f89725c77c9de933a110a828aa345860951 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 22:19:22 -0600 Subject: [PATCH 08/84] Update gym.rst --- docs/gym.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gym.rst b/docs/gym.rst index bee3dbbbf..860a3374b 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -1,4 +1,4 @@ -Gym +gym === .. dfhack-tool:: From f4cf0a06c7c358b9c3f413fff5b0cd52ae5ee591 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 22:22:09 -0600 Subject: [PATCH 09/84] fix key error --- docs/gym.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gym.rst b/docs/gym.rst index 860a3374b..f711bd6bd 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -3,7 +3,7 @@ gym .. dfhack-tool:: :summary: Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training - :tags: Fort Needs BugFix Units + :tags: fort Needs BugFix Units Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also Passively builds military skills and physical stats. From 39f2c9c0d616dd06902933178e9fbcfe58f5a549 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 1 Mar 2025 22:25:12 -0600 Subject: [PATCH 10/84] more key errors --- docs/gym.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gym.rst b/docs/gym.rst index f711bd6bd..f3ccb7d58 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -3,7 +3,7 @@ gym .. dfhack-tool:: :summary: Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training - :tags: fort Needs BugFix Units + :tags: fort auto bugfix units Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also Passively builds military skills and physical stats. From a5d6c0ab875d931bbdb86443c1b18eca63eb8b73 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 2 Mar 2025 19:01:38 -0600 Subject: [PATCH 11/84] Update the documentation --- docs/gym.rst | 26 +++++++++++++------------- gym.lua | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/gym.rst b/docs/gym.rst index f3ccb7d58..f6eac1a12 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -5,20 +5,20 @@ gym :summary: Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training :tags: fort auto bugfix units -Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also Passively builds military skills and physical stats. +Also passively builds military skills and physical stats. -CRITICAL SETUP: -01-Minimum 1 squad with the name "Gym" -02-An assigned squadleader in "Gym" -03-An assigned Barracks for the squad "Gym" -04-Active Training orders for the squad "Gym" +Critical setup: -This should be a new non military use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. +- Minimum 1 squad with the name "Gym" +- An assigned squadleader in "Gym" +- An assigned Barracks for the squad "Gym" +- Active Training orders for the squad "Gym" + +This should be a new non-military-use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. Set the squad's schedule to full time training with at least 8 or 9 training. The squad doesn't need months off. The members leave the squad once they have gotten their gains. -NOTE-Dwarfs with the labor "Fish Dissection" enabled are ignored -Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. +NOTE: Dwarfs with the labor "Fish Dissection" enabled are ignored. Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. Usage ----- @@ -32,18 +32,18 @@ Examples Current status of script ``gym -start`` - checks to see if you have fullfilled the creation of a training gym - searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. + Checks to see if you have fullfilled the creation of a training gym. + Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. ``gym -stop`` - Dwarves currently in the squad ,with the exception of the squadleader, will be unassigned and no new dwarves will be added to the squad. + Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. Options ------- ``-start`` Starts the script - If there is no squad named GYM with a squadleader assigned it will not proceed. + If there is no squad named ``Gym`` with a squadleader assigned it will not proceed. ``-stop`` Stops the script diff --git a/gym.lua b/gym.lua index 199ceae37..b423c4a14 100644 --- a/gym.lua +++ b/gym.lua @@ -5,20 +5,20 @@ Gym Tags: Fort| Needs | BugFix | Units -Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also Passively builds military skills and physical stats. +Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also passively builds military skills and physical stats. CRITICAL SETUP: -01-Minimum 1 squad with the name "Gym" -02-An assigned squadleader in "Gym" -03-An assigned Barracks for the squad "Gym" -04-Active Training orders for the squad "Gym" -This should be a new non military use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. +- Minimum 1 squad with the name "Gym" +- An assigned squadleader in "Gym" +- An assigned Barracks for the squad "Gym" +- Active Training orders for the squad "Gym" + +This should be a new non-military-use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. Set the squad's schedule to full time training with at least 8 or 9 training. The squad doesn't need months off. The members leave the squad once they have gotten their gains. -NOTE-Dwarfs with the labor "Fish Dissection" enabled are ignored -Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. +NOTE: Dwarfs with the labor "Fish Dissection" enabled are ignored. Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. Usage ----- @@ -32,18 +32,18 @@ Gym Current status of script Gym -start - checks to see if you have fullfilled the creation of a training gym - searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. + Checks to see if you have fullfilled the creation of a training gym. + Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. Gym -stop - Dwarves currently in the squad ,with the exception of the squadleader, will be unassigned and no new dwarves will be added to the squad. + Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. Options ------- -start Starts the script - If there is no squad named GYM with a squadleader assigned it will not proceed. + If there is no squad named `Gym` with a squadleader assigned it will not proceed. -stop Stops the script From 6c532159713c2319e97fa2211a3f1d9d7876f7d9 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 6 Mar 2025 13:40:56 -0600 Subject: [PATCH 12/84] Use the enable/disable stuff not args to start or stop --- gym.lua | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/gym.lua b/gym.lua index b423c4a14..81d78e43d 100644 --- a/gym.lua +++ b/gym.lua @@ -23,43 +23,42 @@ NOTE: Dwarfs with the labor "Fish Dissection" enabled are ignored. Make a Dwarve Usage ----- - Gym [] + gym [] Examples -------- -Gym +gym Current status of script -Gym -start +enable gym Checks to see if you have fullfilled the creation of a training gym. Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. -Gym -stop +disable gym Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. Options ------- - -start - Starts the script - If there is no squad named `Gym` with a squadleader assigned it will not proceed. - - -stop - Stops the script -t Use integer values. (Default 3000) The negative need threshhold to trigger for each citizen The greater the number the longer before a dwarf is added to the waiting list. ]====] +--@ enable = true +--@ module = true + +enabled = enabled or false +function isEnabled() + return enabled +end local repeatUtil = require 'repeat-util' local utils=require('utils') validArgs = utils.invert({ - 'start', - 'stop', 't' }) @@ -222,7 +221,6 @@ function check() dfhack.println(scriptname .. " | IGN: " .. ignore_count .. " TRAIN: " .. intraining_count .. " QUE: " ..inque_count ) end - function start() threshold = -5000 dfhack.println(scriptname .. " | START") @@ -243,19 +241,22 @@ function stop() dfhack.println(scriptname .. " | STOP") end -if (args.stop) then - if (running) then stop() end - return +if dfhack_flags.enable then + if dfhack_flags.enable_state then + start() + enabled = true + else + stop() + enabled = false + end end -if (args.start) then - if (running) then stop() end - start() +if dfhack_flags.module then return end if ( running ) then dfhack.println(scriptname .." | Enabled") else - dfhack.println(scriptname .." | Disabled") + dfhack.println(scriptname .." | Disabled") end From a6761bd2b444ba766a27fb3feb3e136e34721a58 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 15:13:37 -0500 Subject: [PATCH 13/84] Do the documentation in one place --- docs/gym.rst | 14 ++++---------- gym.lua | 49 ------------------------------------------------- 2 files changed, 4 insertions(+), 59 deletions(-) diff --git a/docs/gym.rst b/docs/gym.rst index f6eac1a12..ae3e266b8 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -5,7 +5,7 @@ gym :summary: Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training :tags: fort auto bugfix units -Also passively builds military skills and physical stats. +Code for dwarves to hit the gym when they yearn for the gains. Also passively builds military skills and physical stats. Critical setup: @@ -31,23 +31,17 @@ Examples ``gym`` Current status of script -``gym -start`` +``enable gym`` Checks to see if you have fullfilled the creation of a training gym. + If there is no squad named ``Gym`` with a squadleader assigned it will not proceed. Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. -``gym -stop`` +``disable gym`` Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. Options ------- - ``-start`` - Starts the script - If there is no squad named ``Gym`` with a squadleader assigned it will not proceed. - - ``-stop`` - Stops the script - ``-t`` Use integer values. (Default 3000) The negative need threshhold to trigger for each citizen diff --git a/gym.lua b/gym.lua index 81d78e43d..dc4f7e968 100644 --- a/gym.lua +++ b/gym.lua @@ -1,52 +1,3 @@ --- Code for dwarves to hit the gym when they yearn for the gains. ---[====[ -Gym -=== - -Tags: Fort| Needs | BugFix | Units - -Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training. Also passively builds military skills and physical stats. - -CRITICAL SETUP: - -- Minimum 1 squad with the name "Gym" -- An assigned squadleader in "Gym" -- An assigned Barracks for the squad "Gym" -- Active Training orders for the squad "Gym" - -This should be a new non-military-use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. -Set the squad's schedule to full time training with at least 8 or 9 training. -The squad doesn't need months off. The members leave the squad once they have gotten their gains. - -NOTE: Dwarfs with the labor "Fish Dissection" enabled are ignored. Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. - -Usage ------ - - gym [] - -Examples --------- - -gym - Current status of script - -enable gym - Checks to see if you have fullfilled the creation of a training gym. - Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. - Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. - -disable gym - Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. - -Options -------- - - -t - Use integer values. (Default 3000) - The negative need threshhold to trigger for each citizen - The greater the number the longer before a dwarf is added to the waiting list. -]====] --@ enable = true --@ module = true From 0acd601deb1decb7bdefa5284a3dc0d887746c27 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 16:02:06 -0500 Subject: [PATCH 14/84] Various fixes - Clean up documentation - Add option to change squad name. - persist the enabled state, the threshold, and the squad name. - fixed findNeed function - renamed script to `autotraining` - made the ignore flag more clear and more changable - fixed 1 sided military link in `addTraining` --- docs/gym.rst | 16 +++++---- gym.lua | 95 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 40 deletions(-) diff --git a/docs/gym.rst b/docs/gym.rst index ae3e266b8..6b681ac08 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -9,10 +9,10 @@ Code for dwarves to hit the gym when they yearn for the gains. Also passively bu Critical setup: -- Minimum 1 squad with the name "Gym" -- An assigned squadleader in "Gym" -- An assigned Barracks for the squad "Gym" -- Active Training orders for the squad "Gym" +- Minimum 1 squad with the correct name (default is "Gym") +- An assigned squad leader in the squad +- An assigned Barracks for the squad +- Active Training orders for the squad This should be a new non-military-use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. Set the squad's schedule to full time training with at least 8 or 9 training. @@ -33,7 +33,7 @@ Examples ``enable gym`` Checks to see if you have fullfilled the creation of a training gym. - If there is no squad named ``Gym`` with a squadleader assigned it will not proceed. + If there is no squad named ``Gym`` with a squad leader assigned it will not proceed. Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. @@ -43,6 +43,10 @@ Examples Options ------- ``-t`` - Use integer values. (Default 3000) + Use integer values. (Default 5000) The negative need threshhold to trigger for each citizen The greater the number the longer before a dwarf is added to the waiting list. + + ``-n`` + Use a string. (Default 'Gym') + Pick a different name for the squad the script looks for. diff --git a/gym.lua b/gym.lua index dc4f7e968..b0a49d86d 100644 --- a/gym.lua +++ b/gym.lua @@ -1,24 +1,51 @@ --@ enable = true --@ module = true -enabled = enabled or false -function isEnabled() - return enabled -end - local repeatUtil = require 'repeat-util' local utils=require('utils') validArgs = utils.invert({ - 't' + 't', + 'n' }) local args = utils.processArgs({...}, validArgs) -local scriptname = "Gym" -local ignore_flag = 43 -- Fish Dissection labor id +local GLOBAL_KEY = "autotraining" +local ignore_flag = df.unit_labor['DISSECT_FISH'] local ignore_count = 0 local need_id = 14 -local squadname ="Gym" + +local function get_default_state() + return { + enabled=false, + threshold=-5000, + squadname='Gym' + } +end + +state = state or get_default_state() + +function isEnabled() + return state.enabled +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + -- the state changed, is a map loaded and is that map in fort mode? + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + -- no its isnt, so bail + return + end + -- yes it was so: + -- retrieve state saved in game. merge with default state so config + -- saved from previous versions can pick up newer defaults. + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) +end + +-- Save any configurations in the save data +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end --###### @@ -45,14 +72,10 @@ local citizen = getAllCititzen() function findNeed(unit,need_id) local needs = unit.status.current_soul.personality.needs - local need_index = -1 - for k = #needs-1,0,-1 do - if needs[k].id == need_id then - need_index = k - break + for _, need in ipairs(needs) do + if need.id == need_id then + return need end - end if (need_index ~= -1 ) then - return needs[need_index] end return nil end @@ -77,7 +100,7 @@ function checkSquads() local squads = {} local count = 0 for n, mil in ipairs(df.global.world.squads.all) do - if (mil.alias == squadname) then + if (mil.alias == state.squadname) then local leader = mil.positions[0].occupant if ( leader ~= -1) then table.insert(squads,mil) @@ -87,9 +110,9 @@ function checkSquads() end if (count == 0) then - dfhack.print(scriptname.." | ") - dfhack.printerr('ERROR: You need a squad with the name ' .. squadname) - dfhack.print(scriptname.." | ") + dfhack.print(GLOBAL_KEY .." | ") + dfhack.printerr('ERROR: You need a squad with the name ' .. state.squadname) + dfhack.print(GLOBAL_KEY .." | ") dfhack.printerr('That has an active Squad Leader') dfhack.color(-1) return nil @@ -111,6 +134,8 @@ function addTraining(squads,unit) if ( squad.positions[i].occupant == -1 ) then squad.positions[i].occupant = unit.hist_figure_id + unit.military.squad_id = squad.id + unit.military.squad_position = i return true end end @@ -169,36 +194,38 @@ function check() end end - dfhack.println(scriptname .. " | IGN: " .. ignore_count .. " TRAIN: " .. intraining_count .. " QUE: " ..inque_count ) + dfhack.println(GLOBAL_KEY .. " | IGN: " .. ignore_count .. " TRAIN: " .. intraining_count .. " QUE: " ..inque_count ) end function start() - threshold = -5000 - dfhack.println(scriptname .. " | START") + dfhack.println(GLOBAL_KEY .. " | START") if (args.t) then - threshold = 0-tonumber(args.t) + state.threshold = 0-tonumber(args.t) + end + if (args.n) then + state.squadname = args.n end - running = true - repeatUtil.scheduleEvery(scriptname,1000,'ticks',check) + repeatUtil.scheduleEvery(GLOBAL_KEY, 997, 'ticks', check) -- 997 is the closest prime to 1000 end function stop() - repeatUtil.cancel(scriptname) + repeatUtil.cancel(GLOBAL_KEY) local squads = checkSquads() removeAll(squads) - running = false - dfhack.println(scriptname .. " | STOP") + dfhack.println(GLOBAL_KEY .. " | STOP") end if dfhack_flags.enable then if dfhack_flags.enable_state then start() - enabled = true + state.enabled = true + persist_state() else stop() - enabled = false + state.enabled = false + persist_state() end end @@ -206,8 +233,8 @@ if dfhack_flags.module then return end -if ( running ) then - dfhack.println(scriptname .." | Enabled") +if ( state.enabled ) then + dfhack.println(GLOBAL_KEY .." | Enabled") else - dfhack.println(scriptname .." | Disabled") + dfhack.println(GLOBAL_KEY .." | Disabled") end From 9463d7995900bfc59553314ab26302bf5910faa5 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 16:52:20 -0500 Subject: [PATCH 15/84] More cleanup Also tell the user when data was persisted (mostly for debugging) --- gym.lua | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/gym.lua b/gym.lua index b0a49d86d..b1da10a2e 100644 --- a/gym.lua +++ b/gym.lua @@ -13,7 +13,7 @@ local args = utils.processArgs({...}, validArgs) local GLOBAL_KEY = "autotraining" local ignore_flag = df.unit_labor['DISSECT_FISH'] local ignore_count = 0 -local need_id = 14 +local need_id = df.need_type['MartialTraining'] local function get_default_state() return { @@ -35,11 +35,14 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) -- no its isnt, so bail return end - -- yes it was so: + -- yes it was, so: -- retrieve state saved in game. merge with default state so config -- saved from previous versions can pick up newer defaults. state = get_default_state() utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + if state.enabled then + dfhack.print(GLOBAL_KEY .." was persisted with the following data:\nThreshold: ".. state.threshold .. ' | Squad name: '..state.squadname ..'.\n') + end end -- Save any configurations in the save data @@ -52,25 +55,21 @@ end --Functions --###### function getAllCititzen() - local citizen = {} - local my_civ = df.global.world.world_data.active_site[0].entity_links[0].entity_id - for n, unit in ipairs(df.global.world.units.all) do - if unit.civ_id == my_civ and dfhack.units.isCitizen(unit) then - if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then - if ( not unit.status.labors[ignore_flag] ) then - table.insert(citizen, unit) - else - ignore_count = ignore_count +1 - end + local ret = {} + local citizen = dfhack.units.getCitizens(true) + for _, unit in ipairs(citizen) do + if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then + if ( not unit.status.labors[ignore_flag] ) then + table.insert(ret, unit) + else + ignore_count = ignore_count +1 end end end - return citizen + return ret end -local citizen = getAllCititzen() - -function findNeed(unit,need_id) +function findNeed(unit) local needs = unit.status.current_soul.personality.needs for _, need in ipairs(needs) do if need.id == need_id then @@ -85,6 +84,7 @@ end --###### function getByID(id) + local citizen = getAllCititzen() for n, unit in ipairs(citizen) do if (unit.hist_figure_id == id) then return unit @@ -178,10 +178,11 @@ function check() local intraining_count = 0 local inque_count = 0 if ( squads == nil)then return end + local citizen = getAllCititzen() for n, unit in ipairs(citizen) do - local need = findNeed(unit,need_id) + local need = findNeed(unit) if ( need ~= nil ) then - if ( need.focus_level < threshold ) then + if ( need.focus_level < state.threshold ) then local bol = addTraining(squads,unit) if ( bol ) then intraining_count = intraining_count +1 @@ -198,7 +199,7 @@ function check() end function start() - dfhack.println(GLOBAL_KEY .. " | START") + dfhack.println(GLOBAL_KEY .. " | START") if (args.t) then state.threshold = 0-tonumber(args.t) @@ -234,7 +235,7 @@ if dfhack_flags.module then end if ( state.enabled ) then - dfhack.println(GLOBAL_KEY .." | Enabled") + dfhack.println(GLOBAL_KEY .." | Enabled") else - dfhack.println(GLOBAL_KEY .." | Disabled") + dfhack.println(GLOBAL_KEY .." | Disabled") end From 7dba7e437a00c80f9862002baaaf2f0b07400d87 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 16:58:50 -0500 Subject: [PATCH 16/84] rename the script itself and update the docs to account. --- gym.lua => autotraining.lua | 0 docs/{gym.rst => autotraining.rst} | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) rename gym.lua => autotraining.lua (100%) rename docs/{gym.rst => autotraining.rst} (72%) diff --git a/gym.lua b/autotraining.lua similarity index 100% rename from gym.lua rename to autotraining.lua diff --git a/docs/gym.rst b/docs/autotraining.rst similarity index 72% rename from docs/gym.rst rename to docs/autotraining.rst index 6b681ac08..52afec63d 100644 --- a/docs/gym.rst +++ b/docs/autotraining.rst @@ -1,15 +1,15 @@ -gym +autotraining === .. dfhack-tool:: - :summary: Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training + :summary: Assigns citizens to a military squad until they have fulfilled their need for Martial Training :tags: fort auto bugfix units -Code for dwarves to hit the gym when they yearn for the gains. Also passively builds military skills and physical stats. +Automation script for citizens to hit the gym when they yearn for the gains. Also passively builds military skills and physical stats. Critical setup: -- Minimum 1 squad with the correct name (default is "Gym") +- Minimum 1 squad with the correct name (default is ``Gym``) - An assigned squad leader in the squad - An assigned Barracks for the squad - Active Training orders for the squad @@ -23,21 +23,21 @@ NOTE: Dwarfs with the labor "Fish Dissection" enabled are ignored. Make a Dwarve Usage ----- - ``gym []`` + ``autotraining []`` Examples -------- -``gym`` +``autotraining`` Current status of script -``enable gym`` +``enable autotraining`` Checks to see if you have fullfilled the creation of a training gym. - If there is no squad named ``Gym`` with a squad leader assigned it will not proceed. + If there is no squad with the correct name (default: ``Gym``) with a squad leader assigned it will not proceed. Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. -``disable gym`` +``disable autotraining`` Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. Options @@ -48,5 +48,5 @@ Options The greater the number the longer before a dwarf is added to the waiting list. ``-n`` - Use a string. (Default 'Gym') + Use a string. (Default ``Gym``) Pick a different name for the squad the script looks for. From 86967d7573dd40a2b1612cabdbc23338fc76f8ca Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 17:00:06 -0500 Subject: [PATCH 17/84] fix docs --- docs/autotraining.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autotraining.rst b/docs/autotraining.rst index 52afec63d..1434ef0bf 100644 --- a/docs/autotraining.rst +++ b/docs/autotraining.rst @@ -1,5 +1,5 @@ autotraining -=== +============ .. dfhack-tool:: :summary: Assigns citizens to a military squad until they have fulfilled their need for Martial Training From eaa1d8629b1b9f81f6e7bbdd899f4df67dd5f255 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 17:10:32 -0500 Subject: [PATCH 18/84] Add credit where credit is due --- autotraining.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/autotraining.lua b/autotraining.lua index b1da10a2e..e04ecc928 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -1,3 +1,4 @@ +-- Based on the original code by RNGStrategist (who also got some help from Uncle Danny) --@ enable = true --@ module = true From 1892f62b4a8dfa59209c4fb07cc5489baca16348 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 12 Mar 2025 18:19:39 -0500 Subject: [PATCH 19/84] add to control panel alert the user if the squad cant be found (since we cant reliably make a squad ourselves... yet) --- autotraining.lua | 47 ++++++++++++++++++----------- internal/control-panel/registry.lua | 2 ++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index e04ecc928..3441217f9 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -2,8 +2,9 @@ --@ enable = true --@ module = true -local repeatUtil = require 'repeat-util' +local repeatUtil = require('repeat-util') local utils=require('utils') +local dlg = require('gui.dialogs') validArgs = utils.invert({ 't', @@ -30,6 +31,11 @@ function isEnabled() return state.enabled end +-- Save any configurations in the save data +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + dfhack.onStateChange[GLOBAL_KEY] = function(sc) -- the state changed, is a map loaded and is that map in fort mode? if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then @@ -37,18 +43,21 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end -- yes it was, so: + -- retrieve state saved in game. merge with default state so config -- saved from previous versions can pick up newer defaults. state = get_default_state() utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + if ( state.enabled ) then + start() + else + stop() + end + -- start can change the enabled state if the squad cant be found if state.enabled then dfhack.print(GLOBAL_KEY .." was persisted with the following data:\nThreshold: ".. state.threshold .. ' | Squad name: '..state.squadname ..'.\n') end -end - --- Save any configurations in the save data -local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, state) + persist_state() end @@ -111,11 +120,11 @@ function checkSquads() end if (count == 0) then - dfhack.print(GLOBAL_KEY .." | ") - dfhack.printerr('ERROR: You need a squad with the name ' .. state.squadname) - dfhack.print(GLOBAL_KEY .." | ") - dfhack.printerr('That has an active Squad Leader') - dfhack.color(-1) + local message = '' + message = message .. (GLOBAL_KEY .." | ") + message = message .. ('ERROR: You need a squad with the name ' .. state.squadname) + message = message .. (' that has an active Squad Leader.') + dlg.showMessage('Could not enable autotraining', message, COLOR_WHITE) return nil end @@ -178,7 +187,11 @@ function check() local squads = checkSquads() local intraining_count = 0 local inque_count = 0 - if ( squads == nil)then return end + if ( squads == nil)then + repeatUtil.cancel(GLOBAL_KEY) + state.enabled = false + dfhack.println(GLOBAL_KEY .. " | STOP") + return end local citizen = getAllCititzen() for n, unit in ipairs(citizen) do local need = findNeed(unit) @@ -214,21 +227,16 @@ end function stop() repeatUtil.cancel(GLOBAL_KEY) - local squads = checkSquads() - removeAll(squads) dfhack.println(GLOBAL_KEY .. " | STOP") end if dfhack_flags.enable then if dfhack_flags.enable_state then - start() state.enabled = true - persist_state() else - stop() state.enabled = false - persist_state() end + persist_state() end if dfhack_flags.module then @@ -236,7 +244,10 @@ if dfhack_flags.module then end if ( state.enabled ) then + start() dfhack.println(GLOBAL_KEY .." | Enabled") else + stop() dfhack.println(GLOBAL_KEY .." | Disabled") end +persist_state() diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 37cd56c4e..6cd4229fc 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -34,6 +34,8 @@ COMMANDS_BY_IDX = { desc='Automatically shear creatures that are ready for shearing.', params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, {command='autoslab', group='automation', mode='enable'}, + {command='autotraining', group='automation', mode='enable', + desc='Automation script for citizens to hit the gym when they yearn for the gains.'}, {command='ban-cooking all', group='automation', mode='run'}, {command='buildingplan set boulders false', group='automation', mode='run', desc='Enable if you usually don\'t want to use boulders for construction.'}, From e69aba55b0d9b3605ddaf602dc06031c7f35cbd8 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Mar 2025 17:24:36 -0500 Subject: [PATCH 20/84] Check the squad's entity_id to make sure we get *our* Gym --- autotraining.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index 3441217f9..673955fb0 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -109,12 +109,16 @@ end function checkSquads() local squads = {} local count = 0 - for n, mil in ipairs(df.global.world.squads.all) do - if (mil.alias == state.squadname) then - local leader = mil.positions[0].occupant - if ( leader ~= -1) then - table.insert(squads,mil) - count = count +1 + for _, mil in ipairs(df.global.world.squads.all) do + for _, link in ipairs(dfhack.world.getCurrentSite().entity_links) do + if mil.entity_id == link.entity_id then + if (mil.alias == state.squadname) then + local leader = mil.positions[0].occupant + if ( leader ~= -1) then + table.insert(squads,mil) + count = count +1 + end + end end end end From 0d0ef1e48e9c150d4ad33c19f6308cd2b761a2d6 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Mar 2025 17:25:02 -0500 Subject: [PATCH 21/84] Update autotraining.lua remove the `.` because it could lead to confusion --- autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 673955fb0..03d57da7d 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -55,7 +55,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) end -- start can change the enabled state if the squad cant be found if state.enabled then - dfhack.print(GLOBAL_KEY .." was persisted with the following data:\nThreshold: ".. state.threshold .. ' | Squad name: '..state.squadname ..'.\n') + dfhack.print(GLOBAL_KEY .." was persisted with the following data:\nThreshold: ".. state.threshold .. ' | Squad name: '..state.squadname ..'\n') end persist_state() end From c7c73adf4463b1edcc69a92ec73c108e23a83d85 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Mar 2025 20:20:59 -0500 Subject: [PATCH 22/84] Fix the ignore count never being reset --- autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 03d57da7d..3b423d2c0 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -14,7 +14,6 @@ validArgs = utils.invert({ local args = utils.processArgs({...}, validArgs) local GLOBAL_KEY = "autotraining" local ignore_flag = df.unit_labor['DISSECT_FISH'] -local ignore_count = 0 local need_id = df.need_type['MartialTraining'] local function get_default_state() @@ -67,6 +66,7 @@ end function getAllCititzen() local ret = {} local citizen = dfhack.units.getCitizens(true) + local ignore_count = 0 for _, unit in ipairs(citizen) do if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then if ( not unit.status.labors[ignore_flag] ) then From bceb905b902e2b6fe835b7f0db3d704fab76029c Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Mar 2025 20:59:13 -0500 Subject: [PATCH 23/84] Fix units that need training but are already doing so being reported as queued --- autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 3b423d2c0..5117bc6e6 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -143,7 +143,7 @@ function addTraining(squads,unit) end if (unit.military.squad_id ~= -1) then - return false + return (squad.alias == state.squadname) end if ( squad.positions[i].occupant == -1 ) then From 8803e82209b08f1adccf460b2a4083093d5b63b5 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Mar 2025 21:16:19 -0500 Subject: [PATCH 24/84] fix the ignore count (it should be global) --- autotraining.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 5117bc6e6..898f2b4d6 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -15,6 +15,7 @@ local args = utils.processArgs({...}, validArgs) local GLOBAL_KEY = "autotraining" local ignore_flag = df.unit_labor['DISSECT_FISH'] local need_id = df.need_type['MartialTraining'] +local ignore_count = 0 local function get_default_state() return { @@ -66,7 +67,7 @@ end function getAllCititzen() local ret = {} local citizen = dfhack.units.getCitizens(true) - local ignore_count = 0 + ignore_count = 0 for _, unit in ipairs(citizen) do if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then if ( not unit.status.labors[ignore_flag] ) then From 82d3acd0c1d3afde2a225ffb76f19c0e11c9979e Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 15 Mar 2025 15:04:41 -0500 Subject: [PATCH 25/84] Apply suggestions from code review --- autotraining.lua | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index 898f2b4d6..11394f028 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -64,12 +64,12 @@ end --###### --Functions --###### -function getAllCititzen() +function getTrainingCandidates() local ret = {} local citizen = dfhack.units.getCitizens(true) ignore_count = 0 for _, unit in ipairs(citizen) do - if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then + if dfhack.units.isAdult(unit) then if ( not unit.status.labors[ignore_flag] ) then table.insert(ret, unit) else @@ -95,8 +95,7 @@ end --###### function getByID(id) - local citizen = getAllCititzen() - for n, unit in ipairs(citizen) do + for n, unit in ipairs(getTrainingCandidates()) do if (unit.hist_figure_id == id) then return unit end @@ -111,14 +110,12 @@ function checkSquads() local squads = {} local count = 0 for _, mil in ipairs(df.global.world.squads.all) do - for _, link in ipairs(dfhack.world.getCurrentSite().entity_links) do - if mil.entity_id == link.entity_id then - if (mil.alias == state.squadname) then - local leader = mil.positions[0].occupant - if ( leader ~= -1) then - table.insert(squads,mil) - count = count +1 - end + if squad.entity_id == df.global.plotinfo.group_id then + if (mil.alias == state.squadname) then + local leader = mil.positions[0].occupant + if ( leader ~= -1) then + table.insert(squads,mil) + count = count +1 end end end @@ -197,8 +194,7 @@ function check() state.enabled = false dfhack.println(GLOBAL_KEY .. " | STOP") return end - local citizen = getAllCititzen() - for n, unit in ipairs(citizen) do + for n, unit in ipairs(getTrainingCandidates()) do local need = findNeed(unit) if ( need ~= nil ) then if ( need.focus_level < state.threshold ) then From 00e883ffefbe47c67b03a152d9aa9dd5c45aa4c8 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 15 Mar 2025 21:50:11 -0500 Subject: [PATCH 26/84] fix typo --- autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 11394f028..acab778f7 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -110,7 +110,7 @@ function checkSquads() local squads = {} local count = 0 for _, mil in ipairs(df.global.world.squads.all) do - if squad.entity_id == df.global.plotinfo.group_id then + if mil.entity_id == df.global.plotinfo.group_id then if (mil.alias == state.squadname) then local leader = mil.positions[0].occupant if ( leader ~= -1) then From 663412009894d74a302cf1dc2d59e3e1e132659e Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 15 Mar 2025 23:30:22 -0500 Subject: [PATCH 27/84] fix to actually check the unit's squad --- autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index acab778f7..114998cd9 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -141,7 +141,7 @@ function addTraining(squads,unit) end if (unit.military.squad_id ~= -1) then - return (squad.alias == state.squadname) + return (df.squad.find(unit.military.squad_id).alias == state.squadname) end if ( squad.positions[i].occupant == -1 ) then From ed76a08fc36bafca9497624fb4f9b4d80fff15a1 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 16 Mar 2025 01:26:45 -0500 Subject: [PATCH 28/84] Update for gui usage --- autotraining.lua | 128 ++++++++++++++++++------------ internal/notify/notifications.lua | 18 +++++ 2 files changed, 97 insertions(+), 49 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index 114998cd9..7ec0fda01 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -7,8 +7,7 @@ local utils=require('utils') local dlg = require('gui.dialogs') validArgs = utils.invert({ - 't', - 'n' + 't' }) local args = utils.processArgs({...}, validArgs) @@ -21,7 +20,8 @@ local function get_default_state() return { enabled=false, threshold=-5000, - squadname='Gym' + ignored={}, + training_squads = {}, } end @@ -31,9 +31,46 @@ function isEnabled() return state.enabled end --- Save any configurations in the save data +-- persisting a table with numeric keys results in a json array with a huge number of null entries +-- therefore, we convert the keys to strings for persistence +-- also, we clear the frame counter values since the frame counter gets reset on load +local function to_persist(persistable) + local persistable_ignored = {} + for thing in pairs(persistable) do + persistable_ignored[tostring(thing)] = -1 + end + return persistable_ignored +end + +-- loads both from the older array format and the new string table format +local function from_persist(persistable) + if not persistable then + return + end + local ret = {} + for thing in pairs(persistable) do + ret[tonumber(thing)] = -1 + end + return ret +end + local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, state) + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + threshold=threshold, + ignored=to_persist(state.ignored), + training_squads=to_persist(state.training_squads) + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + state.enabled = persisted_data.enabled or false + state.threshold = persisted_data.threshold or -5000 + state.allowed = from_persist(persisted_data.ignored) or {} + state.training_squads = from_persist(persisted_data.training_squads) or {} end dfhack.onStateChange[GLOBAL_KEY] = function(sc) @@ -47,7 +84,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) -- retrieve state saved in game. merge with default state so config -- saved from previous versions can pick up newer defaults. state = get_default_state() - utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + utils.assign(state, load_state()) if ( state.enabled ) then start() else @@ -55,7 +92,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) end -- start can change the enabled state if the squad cant be found if state.enabled then - dfhack.print(GLOBAL_KEY .." was persisted with the following data:\nThreshold: ".. state.threshold .. ' | Squad name: '..state.squadname ..'\n') + dfhack.print(GLOBAL_KEY .." was persisted with the following data:\nThreshold: ".. state.threshold .. '\n') end persist_state() end @@ -70,7 +107,7 @@ function getTrainingCandidates() ignore_count = 0 for _, unit in ipairs(citizen) do if dfhack.units.isAdult(unit) then - if ( not unit.status.labors[ignore_flag] ) then + if ( not state.ignored[unit.id] ) then table.insert(ret, unit) else ignore_count = ignore_count +1 @@ -90,12 +127,20 @@ function findNeed(unit) return nil end +function ignoreUnit(unit) + state.ignored[unit.id] = true +end + +function unignoreUnit(unit) + state.ignored[unit.id] = false +end + --###### --Main --###### function getByID(id) - for n, unit in ipairs(getTrainingCandidates()) do + for _, unit in ipairs(getTrainingCandidates()) do if (unit.hist_figure_id == id) then return unit end @@ -108,42 +153,35 @@ end -- Abort if no squads found function checkSquads() local squads = {} - local count = 0 - for _, mil in ipairs(df.global.world.squads.all) do + for _, squad_id in ipairs(state.training_squads) do + local mil = df.squad:find(squad_id) if mil.entity_id == df.global.plotinfo.group_id then - if (mil.alias == state.squadname) then - local leader = mil.positions[0].occupant - if ( leader ~= -1) then - table.insert(squads,mil) - count = count +1 - end + local leader = mil.positions[0].occupant + if ( leader ~= -1) then + table.insert(squads,mil) end end end - if (count == 0) then - local message = '' - message = message .. (GLOBAL_KEY .." | ") - message = message .. ('ERROR: You need a squad with the name ' .. state.squadname) - message = message .. (' that has an active Squad Leader.') - dlg.showMessage('Could not enable autotraining', message, COLOR_WHITE) + if (#squads == 0) then return nil end return squads end -function addTraining(squads,unit) - for n, squad in ipairs(squads) do - for i=1,9,1 do - if (unit.hist_figure_id == squad.positions[i].occupant) then - return true - end - - if (unit.military.squad_id ~= -1) then - return (df.squad.find(unit.military.squad_id).alias == state.squadname) +function addTraining(unit) + if (unit.military.squad_id ~= -1) then + local inTraining = false + for _, squad in ipairs(state.training_squads) do + if unit.military.squad_id == squad then + inTraining = true end - + end + return inTraining + end + for _, squad in ipairs(state.training_squads) do + for i=1,9,1 do if ( squad.positions[i].occupant == -1 ) then squad.positions[i].occupant = unit.hist_figure_id unit.military.squad_id = squad.id @@ -156,8 +194,8 @@ function addTraining(squads,unit) return false end -function removeTraining(squads,unit) - for n, squad in ipairs(squads) do +function removeTraining(unit) + for n, squad in ipairs(state.training_squads) do for i=1,9,1 do if ( unit.hist_figure_id == squad.positions[i].occupant ) then unit.military.squad_id = -1 @@ -170,9 +208,9 @@ function removeTraining(squads,unit) return false end -function removeAll(squads) - if ( squads == nil) then return end - for n, squad in ipairs(squads) do +function removeAll() + if ( state.training_squads == nil) then return end + for n, squad in ipairs(state.training_squads) do for i=1,9,1 do local dwarf = getByID(squad.positions[i].occupant) if (dwarf ~= nil) then @@ -189,23 +227,19 @@ function check() local squads = checkSquads() local intraining_count = 0 local inque_count = 0 - if ( squads == nil)then - repeatUtil.cancel(GLOBAL_KEY) - state.enabled = false - dfhack.println(GLOBAL_KEY .. " | STOP") - return end + if ( squads == nil) then return end for n, unit in ipairs(getTrainingCandidates()) do local need = findNeed(unit) if ( need ~= nil ) then if ( need.focus_level < state.threshold ) then - local bol = addTraining(squads,unit) + local bol = addTraining(unit) if ( bol ) then intraining_count = intraining_count +1 else inque_count = inque_count +1 end else - removeTraining(squads,unit) + removeTraining(unit) end end end @@ -219,10 +253,6 @@ function start() if (args.t) then state.threshold = 0-tonumber(args.t) end - if (args.n) then - state.squadname = args.n - end - repeatUtil.scheduleEvery(GLOBAL_KEY, 997, 'ticks', check) -- 997 is the closest prime to 1000 end diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 8af7c2c18..c0c91c5ca 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -366,6 +366,24 @@ NOTIFICATIONS_BY_IDX = { dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE) end, }, + { + name='auto_train', + desc='Notifies when there are no squads set up for training', + default=true, + dwarf_fn=function() + local at = reqscript('autotraining') + if (at.checkSquads() == nil) then + return {{text="autotraining: no squads selected",pen=COLOR_LIGHTRED}} + end + end, + on_click=function() + local message = + "You have no squads selected for training.\n".. + "You should have a squad set up to be constantly training for around 8 months.\n".. + "Then you can select that squad for training in the config.\n\nWould you like to open the config? Alternatively, simply close this popup to go create a squad." + dlg.showYesNoPrompt('Training Squads not configured', message, COLOR_WHITE, function () dfhack.run_command('gui/autotraining') end) + end, + }, { name='traders_ready', desc='Notifies when traders are ready to trade at the depot.', From 92076ba632f3b846d2e9e3cdf725a8cc9f36780e Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 16 Mar 2025 01:38:32 -0500 Subject: [PATCH 29/84] clean up --- autotraining.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index 7ec0fda01..4141f4ee9 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -12,7 +12,6 @@ validArgs = utils.invert({ local args = utils.processArgs({...}, validArgs) local GLOBAL_KEY = "autotraining" -local ignore_flag = df.unit_labor['DISSECT_FISH'] local need_id = df.need_type['MartialTraining'] local ignore_count = 0 @@ -56,8 +55,8 @@ end local function persist_state() dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled=enabled, - threshold=threshold, + enabled=state.enabled, + threshold=state.threshold, ignored=to_persist(state.ignored), training_squads=to_persist(state.training_squads) }) @@ -67,10 +66,11 @@ end local function load_state() -- load persistent data local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - state.enabled = persisted_data.enabled or false - state.threshold = persisted_data.threshold or -5000 - state.allowed = from_persist(persisted_data.ignored) or {} - state.training_squads = from_persist(persisted_data.training_squads) or {} + state.enabled = persisted_data.enabled or state.enabled + state.threshold = persisted_data.threshold or state.threshold + state.allowed = from_persist(persisted_data.ignored) or state.allowed + state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads + return state end dfhack.onStateChange[GLOBAL_KEY] = function(sc) @@ -83,8 +83,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) -- retrieve state saved in game. merge with default state so config -- saved from previous versions can pick up newer defaults. - state = get_default_state() - utils.assign(state, load_state()) + load_state() if ( state.enabled ) then start() else From fc832a3a568b6b395e18e96d51148d5229de50fc Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 16 Mar 2025 09:25:22 -0500 Subject: [PATCH 30/84] initial gui and update from code review --- autotraining.lua | 52 ++++----- gui/autotraining.lua | 173 ++++++++++++++++++++++++++++++ internal/notify/notifications.lua | 2 +- 3 files changed, 202 insertions(+), 25 deletions(-) create mode 100644 gui/autotraining.lua diff --git a/autotraining.lua b/autotraining.lua index 4141f4ee9..018def32c 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -32,11 +32,10 @@ end -- persisting a table with numeric keys results in a json array with a huge number of null entries -- therefore, we convert the keys to strings for persistence --- also, we clear the frame counter values since the frame counter gets reset on load local function to_persist(persistable) local persistable_ignored = {} - for thing in pairs(persistable) do - persistable_ignored[tostring(thing)] = -1 + for k, v in pairs(persistable) do + persistable_ignored[tostring(k)] = v end return persistable_ignored end @@ -47,13 +46,13 @@ local function from_persist(persistable) return end local ret = {} - for thing in pairs(persistable) do - ret[tonumber(thing)] = -1 + for k, v in pairs(persistable) do + ret[tonumber(k)] = v end return ret end -local function persist_state() +function persist_state() dfhack.persistent.saveSiteData(GLOBAL_KEY, { enabled=state.enabled, threshold=state.threshold, @@ -68,7 +67,7 @@ local function load_state() local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) state.enabled = persisted_data.enabled or state.enabled state.threshold = persisted_data.threshold or state.threshold - state.allowed = from_persist(persisted_data.ignored) or state.allowed + state.ignored = from_persist(persisted_data.ignored) or state.ignored state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads return state end @@ -116,6 +115,20 @@ function getTrainingCandidates() return ret end +function getTrainingSquads() + local squads = {} + for squad_id, _ in pairs(state.training_squads) do + local squad = df.squad.find(squad_id) + if squad then + table.insert(squads, squad) + else + -- setting to nil during iteration is permitted by lua + state.training_squads[squad_id] = nil + end + end + return squads +end + function findNeed(unit) local needs = unit.status.current_soul.personality.needs for _, need in ipairs(needs) do @@ -126,14 +139,6 @@ function findNeed(unit) return nil end -function ignoreUnit(unit) - state.ignored[unit.id] = true -end - -function unignoreUnit(unit) - state.ignored[unit.id] = false -end - --###### --Main --###### @@ -152,12 +157,11 @@ end -- Abort if no squads found function checkSquads() local squads = {} - for _, squad_id in ipairs(state.training_squads) do - local mil = df.squad:find(squad_id) - if mil.entity_id == df.global.plotinfo.group_id then - local leader = mil.positions[0].occupant + for _, squad in ipairs(getTrainingSquads()) do + if squad.entity_id == df.global.plotinfo.group_id then + local leader = squad.positions[0].occupant if ( leader ~= -1) then - table.insert(squads,mil) + table.insert(squads,squad) end end end @@ -172,14 +176,14 @@ end function addTraining(unit) if (unit.military.squad_id ~= -1) then local inTraining = false - for _, squad in ipairs(state.training_squads) do + for _, squad in ipairs(getTrainingSquads()) do if unit.military.squad_id == squad then inTraining = true end end return inTraining end - for _, squad in ipairs(state.training_squads) do + for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do if ( squad.positions[i].occupant == -1 ) then squad.positions[i].occupant = unit.hist_figure_id @@ -194,7 +198,7 @@ function addTraining(unit) end function removeTraining(unit) - for n, squad in ipairs(state.training_squads) do + for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do if ( unit.hist_figure_id == squad.positions[i].occupant ) then unit.military.squad_id = -1 @@ -209,7 +213,7 @@ end function removeAll() if ( state.training_squads == nil) then return end - for n, squad in ipairs(state.training_squads) do + for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do local dwarf = getByID(squad.positions[i].occupant) if (dwarf ~= nil) then diff --git a/gui/autotraining.lua b/gui/autotraining.lua new file mode 100644 index 000000000..fa5515df7 --- /dev/null +++ b/gui/autotraining.lua @@ -0,0 +1,173 @@ +---@diagnostic disable: missing-fields + +local gui = require('gui') +local widgets = require('gui.widgets') + +local autotraining = reqscript('autotraining') + +local training_squads = autotraining.state.training_squads +local ignored_units = autotraining.state.ignored + +AutoTrain = defclass(AutoTrain, widgets.Window) +AutoTrain.ATTRS { + frame_title='Training Setup', + frame={w=55, h=45}, + resizable=true, -- if resizing makes sense for your dialog + resize_min={w=55, h=20}, -- try to allow users to shrink your windows +} + +local SELECTED_ICON = dfhack.pen.parse{ch=string.char(251), fg=COLOR_LIGHTGREEN} +function AutoTrain:getSquadIcon(squad_id) + if training_squads[squad_id] then + return SELECTED_ICON + end + return nil +end + +function AutoTrain:getSquads() + local squads = {} + for _, squad in ipairs(df.global.world.squads.all) do + if not (squad.entity_id == df.global.plotinfo.group_id) then + goto continue + end + table.insert(squads, { + text = dfhack.translation.translateName(squad.name, true), + icon = self:callback("getSquadIcon", squad.id ), + id = squad.id + }) + + ::continue:: + end + return squads +end + +function AutoTrain:toggleSquad(_, choice) + training_squads[choice.id] = not training_squads[choice.id] + autotraining.persist_state() + self:updateLayout() +end + +local IGNORED_ICON = dfhack.pen.parse{ch='x', fg=COLOR_RED} +function AutoTrain:getUnitIcon(unit_id) + if ignored_units[unit_id] then + return IGNORED_ICON + end + return nil +end + +function AutoTrain:getUnits() + local unit_choices = {} + for _, unit in ipairs(dfhack.units.getCitizens(true,false)) do + if not dfhack.units.isAdult(unit) then + goto continue + end + + table.insert(unit_choices, { + text = dfhack.units.getReadableName(unit), + icon = self:callback("getUnitIcon", unit.id ), + id = unit.id + }) + ::continue:: + end + return unit_choices +end + +function AutoTrain:toggleUnit(_, choice) + ignored_units[choice.id] = not ignored_units[choice.id] + autotraining.persist_state() + self:updateLayout() +end + +function AutoTrain:init() + + -- TODO: provide actual values, and write to configuration + -- (once the base tool actually supports this) + local position_options = { + { label = "none", val = nil, pen = COLOR_LIGHTCYAN }, + { label = "manager", val = nil, pen = COLOR_LIGHTCYAN }, + { label = "manager and chief medical dwarf", val = nil, pen = COLOR_LIGHTCYAN }, + } + + self:addviews{ + widgets.Label{ + frame={ t = 0 , h = 1 }, + text = "Select squads for automatic training", + }, + widgets.List{ + view_id = "squad_list", + icon_width = 2, + frame = { t = 2, h = 10 }, + choices = self:getSquads(), + on_submit=self:callback("toggleSquad") + }, + widgets.Divider{ frame={t=12, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 13 , h = 1 }, + text = "General options", + }, + widgets.EditField { + view_id = "threshold", + frame={ t = 15 , h = 1 }, + key = "CUSTOM_T", + label_text = "Need threshold for training: ", + text = tostring(-autotraining.state.threshold), + on_char = function (char, _) + return tonumber(char,10) + end, + on_submit = function (text) + -- still necessary, because on_char does not check pasted text + local entered_number = tonumber(text,10) or 5000 + autotraining.state.threshold = -entered_number + autotraining.persist_state() + -- make sure that the auto correction is reflected in the EditField + self.subviews.threshold:setText(tostring(entered_number)) + end + }, + widgets.CycleHotkeyLabel { + view_id = "ignored_positions", + frame={ t = 16 , h = 2 }, + key = "CUSTOM_P", + label = "Positions to keep from training: ", + label_below = true, + options = position_options, + initial_option = 3 + }, + widgets.Divider{ frame={t=19, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 20 , h = 1 }, + text = "Select units to exclude from automatic training" + }, + widgets.FilteredList{ + frame = { t = 22 }, + view_id = "unit_list", + edit_key = "CUSTOM_CTRL_F", + icon_width = 2, + choices = self:getUnits(), + on_submit=self:callback("toggleUnit") + } + } + --self.subviews.unit_list:setChoices(unit_choices) +end + +function AutoTrain:onDismiss() + view = nil +end + +AutoTrainScreen = defclass(AutoTrainScreen, gui.ZScreen) +AutoTrainScreen.ATTRS { + focus_path='autotrain', +} + +function AutoTrainScreen:init() + self:addviews{AutoTrain{}} +end + +function AutoTrainScreen:onDismiss() + view = nil +end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('gui/autotrain requires a fortress map to be loaded') +end + +view = view and view:raise() or AutoTrainScreen{}:show() diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index c0c91c5ca..303f2f76e 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -372,7 +372,7 @@ NOTIFICATIONS_BY_IDX = { default=true, dwarf_fn=function() local at = reqscript('autotraining') - if (at.checkSquads() == nil) then + if (at.isEnabled() and at.checkSquads() == nil) then return {{text="autotraining: no squads selected",pen=COLOR_LIGHTRED}} end end, From 55ddbfe999dc40f917b02f6940855f49acbadac7 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 16 Mar 2025 09:27:10 -0500 Subject: [PATCH 31/84] show alias in gui too --- gui/autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/autotraining.lua b/gui/autotraining.lua index fa5515df7..f64e667e0 100644 --- a/gui/autotraining.lua +++ b/gui/autotraining.lua @@ -31,7 +31,7 @@ function AutoTrain:getSquads() goto continue end table.insert(squads, { - text = dfhack.translation.translateName(squad.name, true), + text = dfhack.translation.translateName(squad.name, true)..' ('..squad.alias..')', icon = self:callback("getSquadIcon", squad.id ), id = squad.id }) From f1edec292c0d8db2921189c4c3ff3eaf3693c893 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 16 Mar 2025 09:40:24 -0500 Subject: [PATCH 32/84] clean up --- autotraining.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 018def32c..d2faf4c11 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -4,7 +4,6 @@ local repeatUtil = require('repeat-util') local utils=require('utils') -local dlg = require('gui.dialogs') validArgs = utils.invert({ 't' From 3906eb0f730dbb96d4cf5d535f0aa8cb2376612f Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 09:44:46 -0500 Subject: [PATCH 33/84] Create gui docs --- docs/gui/autotraining.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/gui/autotraining.rst diff --git a/docs/gui/autotraining.rst b/docs/gui/autotraining.rst new file mode 100644 index 000000000..a86b28adf --- /dev/null +++ b/docs/gui/autotraining.rst @@ -0,0 +1,15 @@ +gui/autotraining +================ + +.. dfhack-tool:: + :summary: GUI interface for ``autotraining`` + :tags: fort auto interface + +This is an in-game configuration interface for `autotraining`. You can pick squads for training, select ignored units, and set the needs threshold. + +Usage +----- + +:: + + gui/autotraining From 59d53f5fe81d35bca3ec4231212cd7aa7d4a01eb Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 15:44:41 -0500 Subject: [PATCH 34/84] update the docs --- docs/autotraining.rst | 17 +++++------------ internal/notify/notifications.lua | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/autotraining.rst b/docs/autotraining.rst index 1434ef0bf..d1d67f90e 100644 --- a/docs/autotraining.rst +++ b/docs/autotraining.rst @@ -7,18 +7,11 @@ autotraining Automation script for citizens to hit the gym when they yearn for the gains. Also passively builds military skills and physical stats. -Critical setup: - -- Minimum 1 squad with the correct name (default is ``Gym``) -- An assigned squad leader in the squad -- An assigned Barracks for the squad -- Active Training orders for the squad - -This should be a new non-military-use squad. The uniform should be set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. -Set the squad's schedule to full time training with at least 8 or 9 training. +You need to have at least one squad that is set up for training. This should be a new non-military-use squad. The uniform should be +set to "No Uniform" and the squad should be set to "Constant Training" in the military screen. Edit the squad's schedule to full time training with around 8 units training. The squad doesn't need months off. The members leave the squad once they have gotten their gains. -NOTE: Dwarfs with the labor "Fish Dissection" enabled are ignored. Make a Dwarven labour with only the Fish Dissection enabled, set to "Only selected do this" and assign it to a dwarf to ignore them. +Once you have made squads for training use `gui/autotraining` to select the squads and ignored units, as well as the needs threshhold. Usage ----- @@ -33,12 +26,12 @@ Examples ``enable autotraining`` Checks to see if you have fullfilled the creation of a training gym. - If there is no squad with the correct name (default: ``Gym``) with a squad leader assigned it will not proceed. + If there is no squad marked for training use, a clickable notification will appear letting you know to set one up/ Searches your fort for dwarves with a need to go to the gym, and begins assigning them to said gym. Once they have fulfilled their need they will be removed from the gym squad to be replaced by the next dwarf in the list. ``disable autotraining`` - Dwarves currently in the Gym squad, with the exception of the squad leader, will be unassigned and no new dwarves will be added to the squad. + Stops adding new units to the squad. Options ------- diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 303f2f76e..653d3887d 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -379,7 +379,7 @@ NOTIFICATIONS_BY_IDX = { on_click=function() local message = "You have no squads selected for training.\n".. - "You should have a squad set up to be constantly training for around 8 months.\n".. + "You should have a squad set up to be constantly training with about 8 units needed for training.\n".. "Then you can select that squad for training in the config.\n\nWould you like to open the config? Alternatively, simply close this popup to go create a squad." dlg.showYesNoPrompt('Training Squads not configured', message, COLOR_WHITE, function () dfhack.run_command('gui/autotraining') end) end, From 612936be53e5554398e1710bc08cff69bc68c351 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 16:00:19 -0500 Subject: [PATCH 35/84] remove non-existant name args in docs --- docs/autotraining.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/autotraining.rst b/docs/autotraining.rst index d1d67f90e..c64790551 100644 --- a/docs/autotraining.rst +++ b/docs/autotraining.rst @@ -39,7 +39,3 @@ Options Use integer values. (Default 5000) The negative need threshhold to trigger for each citizen The greater the number the longer before a dwarf is added to the waiting list. - - ``-n`` - Use a string. (Default ``Gym``) - Pick a different name for the squad the script looks for. From f59f6de78701ce07e609fd7a2f90f893532c7205 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 18:11:57 -0500 Subject: [PATCH 36/84] fix typo in message --- gui/autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/autotraining.lua b/gui/autotraining.lua index f64e667e0..3388e980b 100644 --- a/gui/autotraining.lua +++ b/gui/autotraining.lua @@ -167,7 +167,7 @@ function AutoTrainScreen:onDismiss() end if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then - qerror('gui/autotrain requires a fortress map to be loaded') + qerror('gui/autotraining requires a fortress map to be loaded') end view = view and view:raise() or AutoTrainScreen{}:show() From 1c427c69d64f6fc40de1e08ed3989007d326b6c3 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 18:26:35 -0500 Subject: [PATCH 37/84] fix trainees being labeled as queued --- autotraining.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index d2faf4c11..11d5c2d58 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -174,13 +174,12 @@ end function addTraining(unit) if (unit.military.squad_id ~= -1) then - local inTraining = false for _, squad in ipairs(getTrainingSquads()) do - if unit.military.squad_id == squad then - inTraining = true + if unit.military.squad_id == squad.id then + return true end end - return inTraining + return false end for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do From 648ae90333ad04c8d15d39688a307d26d6593f14 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 21 Mar 2025 14:38:10 -0500 Subject: [PATCH 38/84] add ignore nobles --- autotraining.lua | 25 +++++++-- gui/autotraining.lua | 119 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 118 insertions(+), 26 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index 11d5c2d58..d6ec90968 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -19,6 +19,7 @@ local function get_default_state() enabled=false, threshold=-5000, ignored={}, + ignored_nobles={}, training_squads = {}, } end @@ -56,6 +57,7 @@ function persist_state() enabled=state.enabled, threshold=state.threshold, ignored=to_persist(state.ignored), + ignored_nobles=state.ignored_nobles, training_squads=to_persist(state.training_squads) }) end @@ -67,6 +69,7 @@ local function load_state() state.enabled = persisted_data.enabled or state.enabled state.threshold = persisted_data.threshold or state.threshold state.ignored = from_persist(persisted_data.ignored) or state.ignored + state.ignored_nobles = persisted_data.ignored_nobles or state.ignored_nobles state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads return state end @@ -104,9 +107,25 @@ function getTrainingCandidates() ignore_count = 0 for _, unit in ipairs(citizen) do if dfhack.units.isAdult(unit) then + local noblePos = dfhack.units.getNoblePositions(unit) + local isIgnNoble = false if ( not state.ignored[unit.id] ) then - table.insert(ret, unit) + if noblePos ~=nil then + for _, position in ipairs(noblePos) do + if state.ignored_nobles[position.position.code] then + isIgnNoble = true + break + end + end + end + if not isIgnNoble then + table.insert(ret, unit) + else + removeTraining(unit) + ignore_count = ignore_count +1 + end else + removeTraining(unit) ignore_count = ignore_count +1 end end @@ -229,7 +248,7 @@ function check() local intraining_count = 0 local inque_count = 0 if ( squads == nil) then return end - for n, unit in ipairs(getTrainingCandidates()) do + for _, unit in ipairs(getTrainingCandidates()) do local need = findNeed(unit) if ( need ~= nil ) then if ( need.focus_level < state.threshold ) then @@ -254,7 +273,7 @@ function start() if (args.t) then state.threshold = 0-tonumber(args.t) end - repeatUtil.scheduleEvery(GLOBAL_KEY, 997, 'ticks', check) -- 997 is the closest prime to 1000 + repeatUtil.scheduleEvery(GLOBAL_KEY, 1, 'days', check) -- 997 is the closest prime to 1000 end function stop() diff --git a/gui/autotraining.lua b/gui/autotraining.lua index 3388e980b..bd43d8c17 100644 --- a/gui/autotraining.lua +++ b/gui/autotraining.lua @@ -7,6 +7,7 @@ local autotraining = reqscript('autotraining') local training_squads = autotraining.state.training_squads local ignored_units = autotraining.state.ignored +local ignored_nobles = autotraining.state.ignored_nobles AutoTrain = defclass(AutoTrain, widgets.Window) AutoTrain.ATTRS { @@ -55,6 +56,13 @@ function AutoTrain:getUnitIcon(unit_id) return nil end +function AutoTrain:getNobleIcon(noble_code) + if ignored_nobles[noble_code] then + return IGNORED_ICON + end + return nil +end + function AutoTrain:getUnits() local unit_choices = {} for _, unit in ipairs(dfhack.units.getCitizens(true,false)) do @@ -78,16 +86,78 @@ function AutoTrain:toggleUnit(_, choice) self:updateLayout() end -function AutoTrain:init() +local function to_title_case(str) + return dfhack.capitalizeStringWords(dfhack.lowerCp437(str:gsub('_', ' '))) +end - -- TODO: provide actual values, and write to configuration - -- (once the base tool actually supports this) - local position_options = { - { label = "none", val = nil, pen = COLOR_LIGHTCYAN }, - { label = "manager", val = nil, pen = COLOR_LIGHTCYAN }, - { label = "manager and chief medical dwarf", val = nil, pen = COLOR_LIGHTCYAN }, - } +function toSet(list) + local set = {} + for _, v in ipairs(list) do + set[v] = true + end + return set +end + +local function add_positions(positions, entity) + if not entity then return end + for _,position in pairs(entity.positions.own) do + print(position.code..' '..position.id) + positions[position.id] = { + id=position.id+1, + code=position.code, + } + end +end + +function AutoTrain:getPositions() + local positions = {} + local excludedPositions = toSet({ + 'MILITIA_CAPTAIN', + 'MILITIA_COMMANDER', + 'OUTPOST_LIAISON', + 'CAPTAIN_OF_THE_GUARD', + }) + + add_positions(positions, df.historical_entity.find(df.global.plotinfo.civ_id)) + add_positions(positions, df.historical_entity.find(df.global.plotinfo.group_id)) + + -- Step 1: Extract values into a sortable array + local sortedPositions = {} + for _, val in pairs(positions) do + if val and not excludedPositions[val.code] then + table.insert(sortedPositions, val) + end + end + -- Step 2: Sort the positions (optional, adjust sorting criteria) + table.sort(sortedPositions, function(a, b) + return a.id < b.id -- Sort alphabetically by code + end) + + -- Step 3: Rebuild the table without gaps + positions = {} -- Reset positions table + for i, val in ipairs(sortedPositions) do + positions[i] = { + text = to_title_case(val.code), + value = val.code, + pen = COLOR_LIGHTCYAN, + icon = self:callback("getNobleIcon", val.code), + id = val.id + } + end + + return positions +end + + + +function AutoTrain:toggleNoble(_, choice) + ignored_nobles[choice.value] = not ignored_nobles[choice.value] + autotraining.persist_state() + self:updateLayout() +end + +function AutoTrain:init() self:addviews{ widgets.Label{ frame={ t = 0 , h = 1 }, @@ -96,18 +166,18 @@ function AutoTrain:init() widgets.List{ view_id = "squad_list", icon_width = 2, - frame = { t = 2, h = 10 }, + frame = { t = 2, h = 5 }, choices = self:getSquads(), on_submit=self:callback("toggleSquad") }, - widgets.Divider{ frame={t=12, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Divider{ frame={t=6, h=1}, frame_style_l = false, frame_style_r = false}, widgets.Label{ - frame={ t = 13 , h = 1 }, + frame={ t = 7 , h = 1 }, text = "General options", }, widgets.EditField { view_id = "threshold", - frame={ t = 15 , h = 1 }, + frame={ t = 8 , h = 1 }, key = "CUSTOM_T", label_text = "Need threshold for training: ", text = tostring(-autotraining.state.threshold), @@ -123,22 +193,25 @@ function AutoTrain:init() self.subviews.threshold:setText(tostring(entered_number)) end }, - widgets.CycleHotkeyLabel { - view_id = "ignored_positions", - frame={ t = 16 , h = 2 }, - key = "CUSTOM_P", - label = "Positions to keep from training: ", - label_below = true, - options = position_options, - initial_option = 3 + widgets.Divider{ frame={t=9, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 10 , h = 1 }, + text = "Ignored noble positions", + }, + widgets.List{ + frame = { t = 11 , h = 11}, + view_id = "nobles_list", + icon_width = 2, + choices = self:getPositions(), + on_submit=self:callback("toggleNoble") }, - widgets.Divider{ frame={t=19, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Divider{ frame={t=22, h=1}, frame_style_l = false, frame_style_r = false}, widgets.Label{ - frame={ t = 20 , h = 1 }, + frame={ t = 23 , h = 1 }, text = "Select units to exclude from automatic training" }, widgets.FilteredList{ - frame = { t = 22 }, + frame = { t = 24 }, view_id = "unit_list", edit_key = "CUSTOM_CTRL_F", icon_width = 2, From 36125933e9b79de2a7d69b80a08606828d5c1566 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 21 Mar 2025 14:57:53 -0500 Subject: [PATCH 39/84] Remove more debug code --- gui/autotraining.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/autotraining.lua b/gui/autotraining.lua index bd43d8c17..396edb739 100644 --- a/gui/autotraining.lua +++ b/gui/autotraining.lua @@ -101,7 +101,6 @@ end local function add_positions(positions, entity) if not entity then return end for _,position in pairs(entity.positions.own) do - print(position.code..' '..position.id) positions[position.id] = { id=position.id+1, code=position.code, From a9bf6e6dc34ab7e0c3637353fe9228dbadae1e06 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 21 Mar 2025 15:45:32 -0500 Subject: [PATCH 40/84] Gui cleanup --- gui/autotraining.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/autotraining.lua b/gui/autotraining.lua index 396edb739..03ac18326 100644 --- a/gui/autotraining.lua +++ b/gui/autotraining.lua @@ -160,19 +160,19 @@ function AutoTrain:init() self:addviews{ widgets.Label{ frame={ t = 0 , h = 1 }, - text = "Select squads for automatic training", + text = "Select squads for automatic training:", }, widgets.List{ view_id = "squad_list", icon_width = 2, - frame = { t = 2, h = 5 }, + frame = { t = 1, h = 5 }, choices = self:getSquads(), on_submit=self:callback("toggleSquad") }, widgets.Divider{ frame={t=6, h=1}, frame_style_l = false, frame_style_r = false}, widgets.Label{ frame={ t = 7 , h = 1 }, - text = "General options", + text = "General options:", }, widgets.EditField { view_id = "threshold", @@ -195,7 +195,7 @@ function AutoTrain:init() widgets.Divider{ frame={t=9, h=1}, frame_style_l = false, frame_style_r = false}, widgets.Label{ frame={ t = 10 , h = 1 }, - text = "Ignored noble positions", + text = "Ignored noble positions:", }, widgets.List{ frame = { t = 11 , h = 11}, @@ -207,7 +207,7 @@ function AutoTrain:init() widgets.Divider{ frame={t=22, h=1}, frame_style_l = false, frame_style_r = false}, widgets.Label{ frame={ t = 23 , h = 1 }, - text = "Select units to exclude from automatic training" + text = "Select units to exclude from automatic training:" }, widgets.FilteredList{ frame = { t = 24 }, From 236c54eda2df5e35129e97546354dd57e03a27cb Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 28 Mar 2025 15:25:42 -0500 Subject: [PATCH 41/84] Add toolbar button for `gui/design` --- gui/design.lua | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/gui/design.lua b/gui/design.lua index 103ab59c8..018a4ec34 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -38,6 +38,13 @@ local util = reqscript('internal/design/util') local utils = require('utils') local widgets = require('gui.widgets') +local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/design_toolbar.png', 8, 12) + +function launch_design() + dfhack.run_script('gui/design') +end + + local Point = util.Point local getMousePoint = util.getMousePoint @@ -1565,6 +1572,90 @@ function DesignScreen:onDismiss() view = nil end + +-- -------------------------------- +-- DesignToolbarOverlay +-- + +DesignToolbarOverlay = defclass(DesignToolbarOverlay, overlay.OverlayWidget) +DesignToolbarOverlay.ATTRS{ + desc='Adds a button to the toolbar at the bottom of the screen for launching gui/design.', + default_pos={x=40, y=-1}, + default_enabled=true, + viewscreens='dwarfmode', + frame={w=28, h=10}, +} + +function DesignToolbarOverlay:init() + local button_chars = { + {218, 196, 196, 191}, + {179, '[', ']', 179}, + {192, 196, 196, 217}, + } + + self:addviews{ + widgets.Panel{ + frame={t=0, l=0, w=20, h=6}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + frame_inset={l=1, r=1}, + visible=function() return self.subviews.icon:getMousePos() end, + subviews={ + widgets.Label{ + text={ + 'Open the design', NEWLINE, + 'interface.', NEWLINE, + NEWLINE, + {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_CTRL_D'}, + }, + }, + }, + }, + widgets.Panel{ + view_id='icon', + frame={b=0, l=0, w=4, h=3}, + subviews={ + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens={ + {COLOR_GRAY, COLOR_GRAY, COLOR_GRAY, COLOR_GRAY}, + {COLOR_GRAY, COLOR_BLUE, COLOR_BLUE, COLOR_GRAY}, + {COLOR_GRAY, COLOR_GRAY, COLOR_GRAY, COLOR_GRAY}, + }, + tileset=toolbar_textures, + tileset_offset=1, + tileset_stride=8, + }, + on_click=launch_sitemap, + visible=function () return not self.subviews.icon:getMousePos() end, + }, + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens={ + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + {COLOR_WHITE, COLOR_BLUE, COLOR_BLUE, COLOR_WHITE}, + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + }, + tileset=toolbar_textures, + tileset_offset=5, + tileset_stride=8, + }, + on_click=launch_design, + visible=function() return self.subviews.icon:getMousePos() end, + }, + }, + }, + } +end + +function DesignToolbarOverlay:onInput(keys) + return DesignToolbarOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = {toolbar=DesignToolbarOverlay} + if dfhack_flags.module then return end if not dfhack.isMapLoaded() then From a1e850078dc76444b6efdc159d993e885a2468d3 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 28 Mar 2025 15:28:17 -0500 Subject: [PATCH 42/84] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index cd1830ff1..86773995e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,6 +32,7 @@ Template for new versions: - `gui/mass-remove`: add a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` - `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold - `gui/sitemap`: add a button to the toolbar at the bottom left corner of the screen for launching `gui/sitemap` +- `gui/design`: add a button to the toolbar at the bottom left corner of the screen for launching `gui/design` ## Fixes - `idle-crafting`: check that units still have crafting needs before creating a job for them From 00a13243cede649de9c823095fc594b4696e90e8 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 28 Mar 2025 15:31:54 -0500 Subject: [PATCH 43/84] Fix the positioning --- gui/design.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/design.lua b/gui/design.lua index 018a4ec34..61d71da79 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1580,7 +1580,7 @@ end DesignToolbarOverlay = defclass(DesignToolbarOverlay, overlay.OverlayWidget) DesignToolbarOverlay.ATTRS{ desc='Adds a button to the toolbar at the bottom of the screen for launching gui/design.', - default_pos={x=40, y=-1}, + default_pos={x=50, y=-1}, default_enabled=true, viewscreens='dwarfmode', frame={w=28, h=10}, From d17b1cace7220b40242db9298b35c0fff3586932 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 28 Mar 2025 15:45:45 -0500 Subject: [PATCH 44/84] fix up a non-bug --- gui/design.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/design.lua b/gui/design.lua index 61d71da79..9f409ebea 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1627,7 +1627,7 @@ function DesignToolbarOverlay:init() tileset_offset=1, tileset_stride=8, }, - on_click=launch_sitemap, + on_click=launch_design, visible=function () return not self.subviews.icon:getMousePos() end, }, widgets.Label{ From 9600a5734a2fdff2e2488d44a7dfc93800be00e0 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Fri, 28 Mar 2025 21:43:49 -0500 Subject: [PATCH 45/84] data-driven position info for DF toolbars --- internal/df-bottom-toolbars.lua | 478 ++++++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 internal/df-bottom-toolbars.lua diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua new file mode 100644 index 000000000..192e76133 --- /dev/null +++ b/internal/df-bottom-toolbars.lua @@ -0,0 +1,478 @@ +-- Provide data-driven locations for the DF toolbars at the bottom of the +-- screen. Not quite as nice as getting the data from DF directly, but better +-- than hand-rolling calculations for each "interesting" button. + +--@module = true + +TOOLBAR_HEIGHT = 3 +SECONDARY_TOOLBAR_HEIGHT = 3 +MINIMUM_INTERFACE_RECT = require('gui').mkdims_wh(0, 0, 114, 46) + +---@generic T +---@param sequences T[][] +---@return T[] +local function concat_sequences(sequences) + local collected = {} + for _, sequence in ipairs(sequences) do + table.move(sequence, 1, #sequence, #collected + 1, collected) + end + return collected +end + +---@alias NamedWidth table -- single entry, value is width +---@alias NamedOffsets table -- multiple entries, values are offsets + +---@class Toolbar +---@field button_offsets NamedOffsets +---@field width integer + +---@class Toolbar.Widget.frame +---@field l integer Gap between the left edge of the frame and the parent. +---@field t integer Gap between the top edge of the frame and the parent. +---@field r integer Gap between the right edge of the frame and the parent. +---@field b integer Gap between the bottom edge of the frame and the parent. +---@field w integer Width +---@field h integer Height + +---@param widths NamedWidth[] single-name entries only! +---@return Toolbar +local function button_widths_to_offsets(widths) + local offsets = {} + local offset = 0 + for _, ww in ipairs(widths) do + local name, w = next(ww) + if name then + if not name:startswith('_') then + offsets[name] = offset + end + offset = offset + w + end + end + return { button_offsets = offsets, width = offset } +end + +---@param buttons string[] +---@return NamedWidth[] +local function buttons_to_widths(buttons) + local widths = {} + for _, button_name in ipairs(buttons) do + table.insert(widths, { [button_name] = 4 }) + end + return widths +end + +---@param buttons string[] +---@return Toolbar +local function buttons_to_offsets(buttons) + return button_widths_to_offsets(buttons_to_widths(buttons)) +end + +-- Fortress mode toolbar definitions +fort = {} + +---@class LeftToolbar : Toolbar +fort.left = buttons_to_offsets{ + 'citizens', 'tasks', 'places', 'labor', + 'orders', 'nobles', 'objects', 'justice', +} + +---@param interface_rect gui.dimension +---@return Toolbar.Widget.frame +function fort.left:frame(interface_rect) + return { + l = 0, + w = self.width, + r = interface_rect.width - self.width, + + t = interface_rect.height - TOOLBAR_HEIGHT, + h = TOOLBAR_HEIGHT, + b = 0, + } +end + +fort.left_center_gap_minimum = 7 + +---@class CenterToolbar: Toolbar +fort.center = button_widths_to_offsets{ + { _left_border = 1 }, + { dig = 4 }, { chop = 4 }, { gather = 4 }, { smooth = 4 }, { erase = 4 }, + { _divider = 1 }, + { build = 4 }, { stockpile = 4 }, { zone = 4 }, + { _divider = 1 }, + { burrow = 4 }, { cart = 4 }, { traffic = 4 }, + { _divider = 1 }, + { mass_designation = 4 }, + { _right_border = 1 }, +} + +---@param interface_rect gui.dimension +---@return Toolbar.Widget.frame +function fort.center:frame(interface_rect) + -- center toolbar is "centered" in interface area, but never closer to the + -- left toolbar than fort.left_center_gap_minimum + + local interface_offset_centered = math.ceil((interface_rect.width - self.width + 1) / 2) + local interface_offset_min = fort.left.width + fort.left_center_gap_minimum + local interface_offset = math.max(interface_offset_min, interface_offset_centered) + + return { + l = interface_offset, + w = self.width, + r = interface_rect.width - interface_offset - self.width, + + t = interface_rect.height - TOOLBAR_HEIGHT, + h = TOOLBAR_HEIGHT, + b = 0, + } +end + +---@alias CenterToolbarToolNames 'dig' | 'chop' | 'gather' | 'smooth' | 'erase' | 'build' | 'stockpile' | 'zone' | 'burrow' | 'cart' | 'traffic' | 'mass_designation' +---@alias CenterToolbarSecondaryToolbarNames 'dig' | 'chop' | 'gather' | 'smooth' | 'erase' | 'stockpile' | 'stockpile_paint' | 'burrow_paint' | 'traffic' | 'mass_designation' + +---@param interface_rect gui.dimension +---@param toolbar_name CenterToolbarSecondaryToolbarNames +---@return Toolbar.Widget.frame +function fort.center:secondary_toolbar_frame(interface_rect, toolbar_name) + local secondary_toolbar = self.secondary_toolbars[toolbar_name] or + dfhack.error('invalid toolbar name: ' .. toolbar_name) + + ---@type CenterToolbarToolNames + local tool_name + if toolbar_name == 'stockpile_paint' then + tool_name = 'stockpile' + elseif toolbar_name == 'burrow_paint' then + tool_name = 'burrow' + else + tool_name = toolbar_name --[[@as CenterToolbarToolNames]] + end + local toolbar_offset = self:frame(interface_rect).l + local toolbar_button_offset = self.button_offsets[tool_name] or dfhack.error('invalid tool name: ' .. tool_name) + + -- Ideally, the secondary toolbar is positioned directly above the (main) toolbar button + local ideal_offset = toolbar_offset + toolbar_button_offset + + -- In "narrow" interfaces conditions, a wide secondary toolbar (pretty much + -- any tool that has "advanced" options) that was ideally positioned above + -- its tool's button would extend past the right edge of the interface area. + -- Such wide secondary toolbars are instead right justified with a bit of + -- padding. + + -- padding necessary to line up width-constrained secondaries + local secondary_padding = 5 + local width_constrained_offset = math.max(0, interface_rect.width - (secondary_toolbar.width + secondary_padding)) + + -- Use whichever position is left-most. + local l = math.min(ideal_offset, width_constrained_offset) + return { + l = l, + w = secondary_toolbar.width, + r = interface_rect.width - l - secondary_toolbar.width, + + t = interface_rect.height - TOOLBAR_HEIGHT - SECONDARY_TOOLBAR_HEIGHT, + h = SECONDARY_TOOLBAR_HEIGHT, + b = TOOLBAR_HEIGHT, + } +end + +---@type table +fort.center.secondary_toolbars = { + dig = buttons_to_offsets{ + 'dig', 'stairs', 'ramp', 'channel', 'remove_construction', '_gap', + 'rectangle', 'draw', '_gap', + 'advanced_toggle', '_gap', + 'all', 'auto', 'ore_gem', 'gem', '_gap', + 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', + 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', + }, + chop = buttons_to_offsets{ + 'chop', '_gap', + 'rectangle', 'draw', '_gap', + 'advanced_toggle', '_gap', + 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', + 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', + }, + gather = buttons_to_offsets{ + 'gather', '_gap', + 'rectangle', 'draw', '_gap', + 'advanced_toggle', '_gap', + 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', + 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', + }, + smooth = buttons_to_offsets{ + 'smooth', 'engrave', 'carve_track', 'carve_fortification', '_gap', + 'rectangle', 'draw', '_gap', + 'advanced_toggle', '_gap', + 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', + 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', + }, + erase = buttons_to_offsets{ + 'rectangle', + 'draw', + }, + -- build -- completely different and quite variable + stockpile = buttons_to_offsets{ 'add_stockpile' }, + stockpile_paint = buttons_to_offsets{ + 'rectangle', 'draw', 'erase_toggle', 'remove', + }, + -- zone -- no secondary toolbar + -- burrow -- no direct secondary toolbar + burrow_paint = buttons_to_offsets{ + 'rectangle', 'draw', 'erase_toggle', 'remove', + }, + -- cart -- no secondary toolbar + traffic = button_widths_to_offsets( + concat_sequences{ buttons_to_widths{ + 'high', 'normal', 'low', 'restricted', '_gap', + 'rectangle', 'draw', '_gap', + 'advanced_toggle', '_gap', + }, { + { weight_which = 4 }, + { weight_slider = 26 }, + { weight_input = 6 }, + } } + ), + mass_designation = buttons_to_offsets{ + 'claim', 'forbid', 'dump', 'no_dump', 'melt', 'no_melt', 'hidden', 'visible', '_gap', + 'rectangle', 'draw', + }, +} + +---@class RightToolbar: Toolbar +fort.right = buttons_to_offsets{ + 'squads', 'world', +} + +---@param interface_rect gui.dimension +---@return Toolbar.Widget.frame +function fort.right:frame(interface_rect) + return { + l = interface_rect.width - self.width, + w = self.width, + r = 0, + + t = interface_rect.height - TOOLBAR_HEIGHT, + h = TOOLBAR_HEIGHT, + b = 0, + } +end + +---@param interface_rect gui.dimension +---@return gui.dimension +function fort.right:rect(interface_rect) + local width = self.width + local height = 3 + return gui.mkdims_wh(interface_rect.x2 - (width - 1), interface_rect.y2 - (height - 1), width, height) +end + +if dfhack_flags.module then return end + +if not dfhack.world.isFortressMode() then + qerror('Demo only supports fort mode.') +end + +local gui = require('gui') +local Panel = require('gui.widgets.containers.panel') +local Label = require('gui.widgets.labels.label') +local Window = require('gui.widgets.containers.window') +local Toggle = require('gui.widgets.labels.toggle_hotkey_label') + +local screen + +local visible_when_not_focused = true + +local function visible() + return visible_when_not_focused or screen and screen:isActive() and not screen.defocused +end + +ToolbarDemoPanel = defclass(ToolbarDemoPanel, Panel) +ToolbarDemoPanel.ATTRS{ + frame_style = function(...) + local style = gui.FRAME_THIN(...) + style.signature_pen = false + return style + end, + visible_override = true, + visible = visible, + frame_background = { ch = 32, bg = COLOR_BLACK }, +} + +local temp_x, temp_y = 10, 10 +local label_width = 9 -- max len of words left, right, center, secondary +local demo_panel_width = label_width + 4 +local demo_panel_height = 3 + +local left_toolbar_demo = ToolbarDemoPanel{ + frame = { l = temp_x, t = temp_y, w = demo_panel_width, h = demo_panel_height }, + subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'left' } }, +} +local center_toolbar_demo = ToolbarDemoPanel{ + frame = { l = temp_x + demo_panel_width, t = temp_y, w = demo_panel_width, h = demo_panel_height }, + subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'center' } }, +} +local right_toolbar_demo = ToolbarDemoPanel{ + frame = { l = temp_x + 2 * demo_panel_width, t = temp_y, w = demo_panel_width, h = demo_panel_height }, + subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'right' } }, +} +local secondary_visible = false +local secondary_toolbar_demo +secondary_toolbar_demo = ToolbarDemoPanel{ + frame = { l = temp_x + demo_panel_width, t = temp_y - demo_panel_height, w = demo_panel_width, h = demo_panel_height }, + subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'secondary' } }, + visible = function() return visible() and secondary_visible end, +} + +---@param secondary CenterToolbarSecondaryToolbarNames +function update_demonstrations(secondary) + -- by default, draw primary toolbar demonstrations right above the primary toolbars: + -- {l demo} {c demo} {r demo} + -- [l tool] [c tool] [r tool] (bottom of UI) + local toolbar_demo_dy = -TOOLBAR_HEIGHT + local ir = gui.get_interface_rect() + local function update(v, frame) + v.frame = { + w = frame.w, + h = frame.h, + l = frame.l + ir.x1, + t = frame.t + ir.y1 + toolbar_demo_dy, + } + end + if secondary then + -- a secondary toolbar is active, move the primary demonstration up to + -- let the secondary be demonstrated right above the actual secondary: + -- {l demo} {c demo} {r demo} + -- {s demo} + -- [s tool] + -- [l tool] [c tool] [r tool] (bottom of UI) + update(secondary_toolbar_demo, fort.center:secondary_toolbar_frame(ir, secondary)) + secondary_visible = true + toolbar_demo_dy = toolbar_demo_dy - 2 * SECONDARY_TOOLBAR_HEIGHT + else + secondary_visible = false + end + + update(left_toolbar_demo, fort.left:frame(ir)) + update(right_toolbar_demo, fort.right:frame(ir)) + update(center_toolbar_demo, fort.center:frame(ir)) +end + +local tool_from_designation = { + -- from df.main_designation_type + NONE = nil, + DIG_DIG = 'dig', + DIG_REMOVE_STAIRS_RAMPS = 'dig', + DIG_STAIR_UP = 'dig', + DIG_STAIR_UPDOWN = 'dig', + DIG_STAIR_DOWN = 'dig', + DIG_RAMP = 'dig', + DIG_CHANNEL = 'dig', + CHOP = 'chop', + GATHER = 'gather', + SMOOTH = 'smooth', + TRACK = 'smooth', + ENGRAVE = 'smooth', + FORTIFY = 'smooth', + -- REMOVE_CONSTRUCTION -- not used? + CLAIM = 'mass_designation', + UNCLAIM = 'mass_designation', + MELT = 'mass_designation', + NO_MELT = 'mass_designation', + DUMP = 'mass_designation', + NO_DUMP = 'mass_designation', + HIDE = 'mass_designation', + NO_HIDE = 'mass_designation', + -- TOGGLE_ENGRAVING -- not used? + DIG_FROM_MARKER = 'dig', + DIG_TO_MARKER = 'dig', + CHOP_FROM_MARKER = 'chop', + CHOP_TO_MARKER = 'chop', + GATHER_FROM_MARKER = 'gather', + GATHER_TO_MARKER = 'gather', + SMOOTH_FROM_MARKER = 'smooth', + SMOOTH_TO_MARKER = 'smooth', + DESIGNATE_TRAFFIC_HIGH = 'traffic', + DESIGNATE_TRAFFIC_NORMAL = 'traffic', + DESIGNATE_TRAFFIC_LOW = 'traffic', + DESIGNATE_TRAFFIC_RESTRICTED = 'traffic', + ERASE = 'erase', +} +local tool_from_bottom = { + -- from df.main_bottom_mode_type + -- NONE + -- BUILDING + -- BUILDING_PLACEMENT + -- BUILDING_PICK_MATERIALS + -- ZONE + -- ZONE_PAINT + STOCKPILE = 'stockpile', + STOCKPILE_PAINT = 'stockpile_paint', + -- BURROW + BURROW_PAINT = 'burrow_paint' + -- HAULING + -- ARENA_UNIT + -- ARENA_TREE + -- ARENA_WATER_PAINT + -- ARENA_MAGMA_PAINT + -- ARENA_SNOW_PAINT + -- ARENA_MUD_PAINT + -- ARENA_REMOVE_PAINT +} +---@return CenterToolbarSecondaryToolbarNames +local function active_secondary() + local designation = df.global.game.main_interface.main_designation_selected + if designation ~= df.main_designation_type.NONE then + return tool_from_designation[df.main_designation_type[designation]] + end + local bottom = df.global.game.main_interface.bottom_mode_selected + if bottom ~= df.main_bottom_mode_type.NONE then + return tool_from_bottom[df.main_bottom_mode_type[bottom]] + end +end + +DemoWindow = defclass(DemoWindow, Window) +DemoWindow.ATTRS{ + frame_title = 'DF "bottom toolbars" module demo', + frame = { w = 39, h = 5 }, + resizable = true, +} + +function DemoWindow:init() + self:addviews{ + Toggle{ + label = 'Demos visible when not focused?', + initial_option = visible_when_not_focused, + on_change = function(new, old) + visible_when_not_focused = new + end + } + } +end + +DemoScreen = defclass(DemoScreen, gui.ZScreen) +function DemoScreen:init() + self:addviews{ + DemoWindow{}, + left_toolbar_demo, + center_toolbar_demo, + right_toolbar_demo, + secondary_toolbar_demo, + } +end + +local secondary, if_percentage +function DemoScreen:render(...) + if visible_when_not_focused then + local new_secondary = active_secondary() + local new_if_percentage = df.global.init.display.max_interface_percentage + if new_secondary ~= secondary or new_if_percentage ~= if_percentage then + secondary = new_secondary + self:updateLayout() + end + end + return DemoScreen.super.render(self, ...) +end + +function DemoScreen:postComputeFrame(frame_body) + update_demonstrations(active_secondary()) +end + +screen = DemoScreen{}:show() From 237143bad61d131df52a0693ab3da10d3a3f9a8b Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 30 Mar 2025 06:43:20 -0500 Subject: [PATCH 46/84] gui/mass-remove.toolbar: use toolbar positioning info Also, de-magic many of the numbers, and explain why we update width and height instead of the l/r/t/b direct positioning fields. --- gui/mass-remove.lua | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 75a62009d..af24acf11 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -399,13 +399,44 @@ end -- MassRemoveToolbarOverlay -- +-- The overlay is positioned so that (by default) its button is placed next to +-- the right-most button of the secondary toolbar displayed for the "erase" tool +-- (used to remove designations). + +-- This would be straightforward in a full_interface overlay (just set its +-- l/r/t/b frame positioning fields). However, to preserve player-positioning, +-- we take a more circuitous route. + +-- The button and tooltip are anchored to the right of the overlay's frame. +-- Then, to respond to different interface widths, we adjust the width of the +-- overlay's frame to arrange for the button to land in its expected place. + +-- This resize handling breaks down a bit if the player switches the overlay to +-- be anchored to the top or right edges, but it still works okay for +-- windows/interfaces that don't change size, which is probably the common case. + +local tb = reqscript('internal/df-bottom-toolbars') + +local function mass_remove_button_offset(interface_rect) + local remove_buttons = tb.fort.center:secondary_toolbar_frame(interface_rect, 'erase') + return remove_buttons.l + remove_buttons.w -- to the right of the right-most button +end + +local MR_BUTTON_WIDTH = 4 +local MR_BUTTON_HEIGHT = 3 +local MR_TOOLTIP_WIDTH = 26 +local MR_TOOLTIP_HEIGHT = 6 +local MR_WIDTH = math.max(MR_BUTTON_WIDTH, MR_TOOLTIP_WIDTH) +local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[empty line]] + MR_BUTTON_HEIGHT +local MR_MIN_OFFSET = mass_remove_button_offset(tb.MINIMUM_INTERFACE_RECT) + MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ desc='Adds a button to the erase toolbar to open the mass removal tool.', - default_pos={x=42, y=-4}, + default_pos={x=MR_MIN_OFFSET+1, y=-(tb.TOOLBAR_HEIGHT+1)}, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', - frame={w=26, h=11}, + frame={w=MR_WIDTH, h=MR_HEIGHT}, } function MassRemoveToolbarOverlay:init() @@ -417,7 +448,7 @@ function MassRemoveToolbarOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, r=0, w=26, h=6}, + frame={t=0, r=0, w=MR_WIDTH, h=MR_TOOLTIP_HEIGHT}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, @@ -435,7 +466,7 @@ function MassRemoveToolbarOverlay:init() }, widgets.Panel{ view_id='icon', - frame={b=0, r=22, w=4, h=3}, + frame={b=0, r=MR_WIDTH-MR_BUTTON_WIDTH, w=MR_BUTTON_WIDTH, h=tb.SECONDARY_TOOLBAR_HEIGHT}, subviews={ widgets.Label{ text=widgets.makeButtonLabelText{ @@ -469,7 +500,8 @@ function MassRemoveToolbarOverlay:init() end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - self.frame.w = (parent_rect.width+1)//2 - 16 + local extra_width = mass_remove_button_offset(parent_rect) - MR_MIN_OFFSET + self.frame.w = MR_WIDTH + extra_width end function MassRemoveToolbarOverlay:onInput(keys) From 43cc86ddc1efd71b29dfc35abb28fb7ab7727d1b Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 30 Mar 2025 05:37:52 -0500 Subject: [PATCH 47/84] gui/mass-remove.toolbar: handle right- and top-anchored overlay positions The preUpdateLayout code is growing a bit hairy, but player-changed overlay positions work even when the overlay position is "anchored" to the right or top (usually left and bottom). --- gui/mass-remove.lua | 53 +++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index af24acf11..f615fc1da 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -407,33 +407,36 @@ end -- l/r/t/b frame positioning fields). However, to preserve player-positioning, -- we take a more circuitous route. --- The button and tooltip are anchored to the right of the overlay's frame. --- Then, to respond to different interface widths, we adjust the width of the +-- The button and tooltip are anchored to one side of the overlay's frame (which +-- one depends on which edge the overlay itself is anchored to). Then, to +-- respond to different interface sizes, we adjust the width and height of the -- overlay's frame to arrange for the button to land in its expected place. --- This resize handling breaks down a bit if the player switches the overlay to --- be anchored to the top or right edges, but it still works okay for --- windows/interfaces that don't change size, which is probably the common case. - local tb = reqscript('internal/df-bottom-toolbars') -local function mass_remove_button_offset(interface_rect) - local remove_buttons = tb.fort.center:secondary_toolbar_frame(interface_rect, 'erase') - return remove_buttons.l + remove_buttons.w -- to the right of the right-most button -end - local MR_BUTTON_WIDTH = 4 local MR_BUTTON_HEIGHT = 3 local MR_TOOLTIP_WIDTH = 26 local MR_TOOLTIP_HEIGHT = 6 local MR_WIDTH = math.max(MR_BUTTON_WIDTH, MR_TOOLTIP_WIDTH) local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[empty line]] + MR_BUTTON_HEIGHT -local MR_MIN_OFFSET = mass_remove_button_offset(tb.MINIMUM_INTERFACE_RECT) + +local function mass_remove_button_offsets(interface_rect) + local remove_buttons = tb.CENTER_TOOLBAR:secondary_toolbar_frame(interface_rect, 'erase') + return { + l = remove_buttons.l + remove_buttons.w, + r = remove_buttons.r - MR_BUTTON_WIDTH, + t = remove_buttons.t, + b = remove_buttons.b, + } +end + +local MR_MIN_OFFSETS = mass_remove_button_offsets(tb.MINIMUM_INTERFACE_RECT) MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ desc='Adds a button to the erase toolbar to open the mass removal tool.', - default_pos={x=MR_MIN_OFFSET+1, y=-(tb.TOOLBAR_HEIGHT+1)}, + default_pos={x=MR_MIN_OFFSETS.l+1, y=-(MR_MIN_OFFSETS.b+1)}, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', frame={w=MR_WIDTH, h=MR_HEIGHT}, @@ -448,6 +451,7 @@ function MassRemoveToolbarOverlay:init() self:addviews{ widgets.Panel{ + view_id='tooltip', frame={t=0, r=0, w=MR_WIDTH, h=MR_TOOLTIP_HEIGHT}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, @@ -500,8 +504,29 @@ function MassRemoveToolbarOverlay:init() end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - local extra_width = mass_remove_button_offset(parent_rect) - MR_MIN_OFFSET + local extra_width + local offsets = mass_remove_button_offsets(parent_rect) + if self.frame.l then + extra_width = offsets.l - MR_MIN_OFFSETS.l + self.subviews.tooltip.frame.l = nil + self.subviews.tooltip.frame.r = 0 + self.subviews.icon.frame.l = nil + self.subviews.icon.frame.r = MR_WIDTH-MR_BUTTON_WIDTH + else + extra_width = offsets.r - MR_MIN_OFFSETS.r + self.subviews.tooltip.frame.r = nil + self.subviews.tooltip.frame.l = 0 + self.subviews.icon.frame.r = nil + self.subviews.icon.frame.l = 0 + end + local extra_height + if self.frame.b then + extra_height = offsets.b - MR_MIN_OFFSETS.b + else + extra_height = offsets.t - MR_MIN_OFFSETS.t + end self.frame.w = MR_WIDTH + extra_width + self.frame.h = MR_HEIGHT + extra_height end function MassRemoveToolbarOverlay:onInput(keys) From a659d13c4d4b2cb543f8f6b0bbc99d5ec356bee0 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 30 Mar 2025 22:59:07 -0500 Subject: [PATCH 48/84] toolbars: remove fort.right:rect It was a remnant of earlier implementation. --- internal/df-bottom-toolbars.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index 192e76133..47a3bb3ba 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -256,14 +256,6 @@ function fort.right:frame(interface_rect) } end ----@param interface_rect gui.dimension ----@return gui.dimension -function fort.right:rect(interface_rect) - local width = self.width - local height = 3 - return gui.mkdims_wh(interface_rect.x2 - (width - 1), interface_rect.y2 - (height - 1), width, height) -end - if dfhack_flags.module then return end if not dfhack.world.isFortressMode() then From 456b6c976b0a1dd99197c2544901484101eda067 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 30 Mar 2025 23:05:36 -0500 Subject: [PATCH 49/84] toolbars: (demo) directly use designation and bottom values Conversion to string was a part of an earlier implementation detail that was discarded. --- internal/df-bottom-toolbars.lua | 116 ++++++++++++++++---------------- 1 file changed, 57 insertions(+), 59 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index 47a3bb3ba..821470538 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -348,75 +348,73 @@ function update_demonstrations(secondary) end local tool_from_designation = { - -- from df.main_designation_type - NONE = nil, - DIG_DIG = 'dig', - DIG_REMOVE_STAIRS_RAMPS = 'dig', - DIG_STAIR_UP = 'dig', - DIG_STAIR_UPDOWN = 'dig', - DIG_STAIR_DOWN = 'dig', - DIG_RAMP = 'dig', - DIG_CHANNEL = 'dig', - CHOP = 'chop', - GATHER = 'gather', - SMOOTH = 'smooth', - TRACK = 'smooth', - ENGRAVE = 'smooth', - FORTIFY = 'smooth', - -- REMOVE_CONSTRUCTION -- not used? - CLAIM = 'mass_designation', - UNCLAIM = 'mass_designation', - MELT = 'mass_designation', - NO_MELT = 'mass_designation', - DUMP = 'mass_designation', - NO_DUMP = 'mass_designation', - HIDE = 'mass_designation', - NO_HIDE = 'mass_designation', - -- TOGGLE_ENGRAVING -- not used? - DIG_FROM_MARKER = 'dig', - DIG_TO_MARKER = 'dig', - CHOP_FROM_MARKER = 'chop', - CHOP_TO_MARKER = 'chop', - GATHER_FROM_MARKER = 'gather', - GATHER_TO_MARKER = 'gather', - SMOOTH_FROM_MARKER = 'smooth', - SMOOTH_TO_MARKER = 'smooth', - DESIGNATE_TRAFFIC_HIGH = 'traffic', - DESIGNATE_TRAFFIC_NORMAL = 'traffic', - DESIGNATE_TRAFFIC_LOW = 'traffic', - DESIGNATE_TRAFFIC_RESTRICTED = 'traffic', - ERASE = 'erase', + -- df.main_designation_type.NONE -- not a tool + [df.main_designation_type.DIG_DIG] = 'dig', + [df.main_designation_type.DIG_REMOVE_STAIRS_RAMPS] = 'dig', + [df.main_designation_type.DIG_STAIR_UP] = 'dig', + [df.main_designation_type.DIG_STAIR_UPDOWN] = 'dig', + [df.main_designation_type.DIG_STAIR_DOWN] = 'dig', + [df.main_designation_type.DIG_RAMP] = 'dig', + [df.main_designation_type.DIG_CHANNEL] = 'dig', + [df.main_designation_type.CHOP] = 'chop', + [df.main_designation_type.GATHER] = 'gather', + [df.main_designation_type.SMOOTH] = 'smooth', + [df.main_designation_type.TRACK] = 'smooth', + [df.main_designation_type.ENGRAVE] = 'smooth', + [df.main_designation_type.FORTIFY] = 'smooth', + -- df.main_designation_type.REMOVE_CONSTRUCTION -- not used? + [df.main_designation_type.CLAIM] = 'mass_designation', + [df.main_designation_type.UNCLAIM] = 'mass_designation', + [df.main_designation_type.MELT] = 'mass_designation', + [df.main_designation_type.NO_MELT] = 'mass_designation', + [df.main_designation_type.DUMP] = 'mass_designation', + [df.main_designation_type.NO_DUMP] = 'mass_designation', + [df.main_designation_type.HIDE] = 'mass_designation', + [df.main_designation_type.NO_HIDE] = 'mass_designation', + -- df.main_designation_type.TOGGLE_ENGRAVING -- not used? + [df.main_designation_type.DIG_FROM_MARKER] = 'dig', + [df.main_designation_type.DIG_TO_MARKER] = 'dig', + [df.main_designation_type.CHOP_FROM_MARKER] = 'chop', + [df.main_designation_type.CHOP_TO_MARKER] = 'chop', + [df.main_designation_type.GATHER_FROM_MARKER] = 'gather', + [df.main_designation_type.GATHER_TO_MARKER] = 'gather', + [df.main_designation_type.SMOOTH_FROM_MARKER] = 'smooth', + [df.main_designation_type.SMOOTH_TO_MARKER] = 'smooth', + [df.main_designation_type.DESIGNATE_TRAFFIC_HIGH] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_NORMAL] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_LOW] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_RESTRICTED] = 'traffic', + [df.main_designation_type.ERASE] = 'erase', } local tool_from_bottom = { - -- from df.main_bottom_mode_type - -- NONE - -- BUILDING - -- BUILDING_PLACEMENT - -- BUILDING_PICK_MATERIALS - -- ZONE - -- ZONE_PAINT - STOCKPILE = 'stockpile', - STOCKPILE_PAINT = 'stockpile_paint', - -- BURROW - BURROW_PAINT = 'burrow_paint' - -- HAULING - -- ARENA_UNIT - -- ARENA_TREE - -- ARENA_WATER_PAINT - -- ARENA_MAGMA_PAINT - -- ARENA_SNOW_PAINT - -- ARENA_MUD_PAINT - -- ARENA_REMOVE_PAINT + -- df.main_bottom_mode_type.NONE + -- df.main_bottom_mode_type.BUILDING + -- df.main_bottom_mode_type.BUILDING_PLACEMENT + -- df.main_bottom_mode_type.BUILDING_PICK_MATERIALS + -- df.main_bottom_mode_type.ZONE + -- df.main_bottom_mode_type.ZONE_PAINT + [df.main_bottom_mode_type.STOCKPILE] = 'stockpile', + [df.main_bottom_mode_type.STOCKPILE_PAINT] = 'stockpile_paint', + -- df.main_bottom_mode_type.BURROW + [df.main_bottom_mode_type.BURROW_PAINT] = 'burrow_paint' + -- df.main_bottom_mode_type.HAULING + -- df.main_bottom_mode_type.ARENA_UNIT + -- df.main_bottom_mode_type.ARENA_TREE + -- df.main_bottom_mode_type.ARENA_WATER_PAINT + -- df.main_bottom_mode_type.ARENA_MAGMA_PAINT + -- df.main_bottom_mode_type.ARENA_SNOW_PAINT + -- df.main_bottom_mode_type.ARENA_MUD_PAINT + -- df.main_bottom_mode_type.ARENA_REMOVE_PAINT } ---@return CenterToolbarSecondaryToolbarNames local function active_secondary() local designation = df.global.game.main_interface.main_designation_selected if designation ~= df.main_designation_type.NONE then - return tool_from_designation[df.main_designation_type[designation]] + return tool_from_designation[designation] end local bottom = df.global.game.main_interface.bottom_mode_selected if bottom ~= df.main_bottom_mode_type.NONE then - return tool_from_bottom[df.main_bottom_mode_type[bottom]] + return tool_from_bottom[bottom] end end From 9c54dac5c4ebf898d724c51665bacfd4275dab81 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 30 Mar 2025 23:11:01 -0500 Subject: [PATCH 50/84] toolbars: (demo) fix type flaw --- internal/df-bottom-toolbars.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index 821470538..c78f05625 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -313,7 +313,7 @@ secondary_toolbar_demo = ToolbarDemoPanel{ visible = function() return visible() and secondary_visible end, } ----@param secondary CenterToolbarSecondaryToolbarNames +---@param secondary? CenterToolbarSecondaryToolbarNames function update_demonstrations(secondary) -- by default, draw primary toolbar demonstrations right above the primary toolbars: -- {l demo} {c demo} {r demo} @@ -406,7 +406,7 @@ local tool_from_bottom = { -- df.main_bottom_mode_type.ARENA_MUD_PAINT -- df.main_bottom_mode_type.ARENA_REMOVE_PAINT } ----@return CenterToolbarSecondaryToolbarNames +---@return CenterToolbarSecondaryToolbarNames? local function active_secondary() local designation = df.global.game.main_interface.main_designation_selected if designation ~= df.main_designation_type.NONE then From 143c473461d3edf819b5621e6500736a662ffd66 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 31 Mar 2025 11:23:20 -0500 Subject: [PATCH 51/84] Clean up all --- gui/design.lua | 61 +++++++++++++++++++++++++++++++++++++++++---- gui/mass-remove.lua | 2 +- gui/sitemap.lua | 57 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 9f409ebea..7cbcf5414 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -1577,13 +1577,36 @@ end -- DesignToolbarOverlay -- +local tb = reqscript('internal/df-bottom-toolbars') + +local BP_BUTTON_WIDTH = 4 +local BP_BUTTON_HEIGHT = 3 +local BP_TOOLTIP_WIDTH = 20 +local BP_TOOLTIP_HEIGHT = 6 +local BP_WIDTH = math.max(BP_BUTTON_WIDTH, BP_TOOLTIP_WIDTH) +local BP_HEIGHT = BP_TOOLTIP_HEIGHT + 1 --[[empty line]] + BP_BUTTON_HEIGHT + +local function design_button_offsets(interface_rect) + local center_bar = tb.fort.center:frame(interface_rect) + return { + l = center_bar.l + center_bar.w, + r = center_bar.r - BP_BUTTON_WIDTH, + t = center_bar.t, + b = center_bar.b, + } +end + +local BP_MIN_OFFSETS = design_button_offsets(tb.MINIMUM_INTERFACE_RECT) + + + DesignToolbarOverlay = defclass(DesignToolbarOverlay, overlay.OverlayWidget) DesignToolbarOverlay.ATTRS{ desc='Adds a button to the toolbar at the bottom of the screen for launching gui/design.', - default_pos={x=50, y=-1}, + default_pos={x=BP_MIN_OFFSETS.l+1, y=-(BP_MIN_OFFSETS.b+1)}, default_enabled=true, - viewscreens='dwarfmode', - frame={w=28, h=10}, + viewscreens='dwarfmode/Default', + frame={w=BP_WIDTH, h=BP_HEIGHT}, } function DesignToolbarOverlay:init() @@ -1595,7 +1618,8 @@ function DesignToolbarOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, l=0, w=20, h=6}, + view_id='tooltip', + frame={t=0, r=0, w=BP_WIDTH, h=BP_TOOLTIP_HEIGHT}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, @@ -1613,7 +1637,7 @@ function DesignToolbarOverlay:init() }, widgets.Panel{ view_id='icon', - frame={b=0, l=0, w=4, h=3}, + frame={b=0, r=BP_WIDTH-BP_BUTTON_WIDTH, w=BP_BUTTON_WIDTH, h=tb.TOOLBAR_HEIGHT}, subviews={ widgets.Label{ text=widgets.makeButtonLabelText{ @@ -1650,6 +1674,33 @@ function DesignToolbarOverlay:init() } end +function DesignToolbarOverlay:preUpdateLayout(parent_rect) + self.frame.w = (parent_rect.width+1)//2 - 16 + local extra_width + local offsets = design_button_offsets(parent_rect) + if self.frame.l then + extra_width = offsets.l - BP_MIN_OFFSETS.l + self.subviews.tooltip.frame.l = nil + self.subviews.tooltip.frame.r = 0 + self.subviews.icon.frame.l = nil + self.subviews.icon.frame.r = BP_WIDTH-BP_BUTTON_WIDTH + else + extra_width = offsets.r - BP_MIN_OFFSETS.r + self.subviews.tooltip.frame.r = nil + self.subviews.tooltip.frame.l = 0 + self.subviews.icon.frame.r = nil + self.subviews.icon.frame.l = 0 + end + local extra_height + if self.frame.b then + extra_height = offsets.b - BP_MIN_OFFSETS.b + else + extra_height = offsets.t - BP_MIN_OFFSETS.t + end + self.frame.w = BP_WIDTH + extra_width + self.frame.h = BP_HEIGHT + extra_height +end + function DesignToolbarOverlay:onInput(keys) return DesignToolbarOverlay.super.onInput(self, keys) end diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index f615fc1da..c96c80e29 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -422,7 +422,7 @@ local MR_WIDTH = math.max(MR_BUTTON_WIDTH, MR_TOOLTIP_WIDTH) local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[empty line]] + MR_BUTTON_HEIGHT local function mass_remove_button_offsets(interface_rect) - local remove_buttons = tb.CENTER_TOOLBAR:secondary_toolbar_frame(interface_rect, 'erase') + local remove_buttons = tb.fort.center:secondary_toolbar_frame(interface_rect, 'erase') return { l = remove_buttons.l + remove_buttons.w, r = remove_buttons.r - MR_BUTTON_WIDTH, diff --git a/gui/sitemap.lua b/gui/sitemap.lua index b3fbd796d..27a1c5ab4 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -365,13 +365,34 @@ end -- SitemapToolbarOverlay -- +local tb = reqscript('internal/df-bottom-toolbars') + +local BP_BUTTON_WIDTH = 4 +local BP_BUTTON_HEIGHT = 3 +local BP_TOOLTIP_WIDTH = 27 +local BP_TOOLTIP_HEIGHT = 6 +local BP_WIDTH = math.max(BP_BUTTON_WIDTH, BP_TOOLTIP_WIDTH) +local BP_HEIGHT = BP_TOOLTIP_HEIGHT + 1 --[[empty line]] + BP_BUTTON_HEIGHT + +local function design_button_offsets(interface_rect) + local center_bar = tb.fort.left:frame(interface_rect) + return { + l = center_bar.l + center_bar.w, + r = center_bar.r - BP_BUTTON_WIDTH, + t = center_bar.t, + b = center_bar.b, + } +end + +local BP_MIN_OFFSETS = design_button_offsets(tb.MINIMUM_INTERFACE_RECT) + SitemapToolbarOverlay = defclass(SitemapToolbarOverlay, overlay.OverlayWidget) SitemapToolbarOverlay.ATTRS{ desc='Adds a button to the toolbar at the bottom left corner of the screen for launching gui/sitemap.', - default_pos={x=35, y=-1}, + default_pos={x=BP_MIN_OFFSETS.l+1, y=-(BP_MIN_OFFSETS.b+1)}, default_enabled=true, viewscreens='dwarfmode', - frame={w=28, h=10}, + frame={w=BP_WIDTH, h=BP_HEIGHT}, } function SitemapToolbarOverlay:init() @@ -383,7 +404,8 @@ function SitemapToolbarOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, l=0, w=27, h=6}, + view_id='tooltip', + frame={t=0, r=0, w=BP_WIDTH, h=BP_TOOLTIP_HEIGHT}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, @@ -401,7 +423,7 @@ function SitemapToolbarOverlay:init() }, widgets.Panel{ view_id='icon', - frame={b=0, l=0, w=4, h=3}, + frame={b=0, r=BP_WIDTH-BP_BUTTON_WIDTH, w=BP_BUTTON_WIDTH, h=tb.TOOLBAR_HEIGHT}, subviews={ widgets.Label{ text=widgets.makeButtonLabelText{ @@ -434,6 +456,33 @@ function SitemapToolbarOverlay:init() } end +function SitemapToolbarOverlay:preUpdateLayout(parent_rect) + self.frame.w = (parent_rect.width+1)//2 - 16 + local extra_width + local offsets = design_button_offsets(parent_rect) + if self.frame.l then + extra_width = offsets.l - BP_MIN_OFFSETS.l + self.subviews.tooltip.frame.l = nil + self.subviews.tooltip.frame.r = 0 + self.subviews.icon.frame.l = nil + self.subviews.icon.frame.r = BP_WIDTH-BP_BUTTON_WIDTH + else + extra_width = offsets.r - BP_MIN_OFFSETS.r + self.subviews.tooltip.frame.r = nil + self.subviews.tooltip.frame.l = 0 + self.subviews.icon.frame.r = nil + self.subviews.icon.frame.l = 0 + end + local extra_height + if self.frame.b then + extra_height = offsets.b - BP_MIN_OFFSETS.b + else + extra_height = offsets.t - BP_MIN_OFFSETS.t + end + self.frame.w = BP_WIDTH + extra_width + self.frame.h = BP_HEIGHT + extra_height +end + function SitemapToolbarOverlay:onInput(keys) return SitemapToolbarOverlay.super.onInput(self, keys) end From 3d5f1805746c096c174d2db4e328831298653e1a Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Mon, 31 Mar 2025 22:35:26 -0500 Subject: [PATCH 52/84] gui/mass-remove.toolbar: fix toolbars library use Oops, I moved the toolbar definitions into the "fort" namespace at the last minute and forget to update the non-demo user... --- gui/mass-remove.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index f615fc1da..c96c80e29 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -422,7 +422,7 @@ local MR_WIDTH = math.max(MR_BUTTON_WIDTH, MR_TOOLTIP_WIDTH) local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[empty line]] + MR_BUTTON_HEIGHT local function mass_remove_button_offsets(interface_rect) - local remove_buttons = tb.CENTER_TOOLBAR:secondary_toolbar_frame(interface_rect, 'erase') + local remove_buttons = tb.fort.center:secondary_toolbar_frame(interface_rect, 'erase') return { l = remove_buttons.l + remove_buttons.w, r = remove_buttons.r - MR_BUTTON_WIDTH, From 70262e048a7a401aa24d9e12cd7490bd0eeee94e Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 00:32:10 -0500 Subject: [PATCH 53/84] gui/mass-remove.toolbar: extra panel to support changes in anchor sides --- gui/mass-remove.lua | 105 +++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index c96c80e29..735b1d335 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -451,52 +451,57 @@ function MassRemoveToolbarOverlay:init() self:addviews{ widgets.Panel{ - view_id='tooltip', - frame={t=0, r=0, w=MR_WIDTH, h=MR_TOOLTIP_HEIGHT}, - frame_style=gui.FRAME_PANEL, - frame_background=gui.CLEAR_PEN, - frame_inset={l=1, r=1}, - visible=function() return self.subviews.icon:getMousePos() end, - subviews={ - widgets.Label{ - text={ - 'Open mass removal', NEWLINE, - 'interface.', NEWLINE, - NEWLINE, - {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, - }, - }, - }, - }, - widgets.Panel{ - view_id='icon', - frame={b=0, r=MR_WIDTH-MR_BUTTON_WIDTH, w=MR_BUTTON_WIDTH, h=tb.SECONDARY_TOOLBAR_HEIGHT}, - subviews={ - widgets.Label{ - text=widgets.makeButtonLabelText{ - chars=button_chars, - pens=COLOR_GRAY, - tileset=toolbar_textures, - tileset_offset=1, - tileset_stride=8, + view_id='tt_and_icon', + frame={ r=0, t=0, w=MR_WIDTH, h=MR_HEIGHT }, + subviews={ + widgets.Panel{ + frame={t=0, l=0, w=MR_WIDTH, h=MR_TOOLTIP_HEIGHT}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + frame_inset={l=1, r=1}, + visible=function() return self.subviews.icon:getMousePos() end, + subviews={ + widgets.Label{ + text={ + 'Open mass removal', NEWLINE, + 'interface.', NEWLINE, + NEWLINE, + {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, + }, + }, }, - on_click=launch_mass_remove, - visible=function () return not self.subviews.icon:getMousePos() end, }, - widgets.Label{ - text=widgets.makeButtonLabelText{ - chars=button_chars, - pens={ - {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, - {COLOR_WHITE, COLOR_GRAY, COLOR_GRAY, COLOR_WHITE}, - {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + widgets.Panel{ + view_id='icon', + frame={b=0, l=0, w=MR_BUTTON_WIDTH, h=tb.SECONDARY_TOOLBAR_HEIGHT}, + subviews={ + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens=COLOR_GRAY, + tileset=toolbar_textures, + tileset_offset=1, + tileset_stride=8, + }, + on_click=launch_mass_remove, + visible=function () return not self.subviews.icon:getMousePos() end, + }, + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens={ + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + {COLOR_WHITE, COLOR_GRAY, COLOR_GRAY, COLOR_WHITE}, + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + }, + tileset=toolbar_textures, + tileset_offset=5, + tileset_stride=8, + }, + on_click=launch_mass_remove, + visible=function() return self.subviews.icon:getMousePos() end, }, - tileset=toolbar_textures, - tileset_offset=5, - tileset_stride=8, }, - on_click=launch_mass_remove, - visible=function() return self.subviews.icon:getMousePos() end, }, }, }, @@ -508,22 +513,22 @@ function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) local offsets = mass_remove_button_offsets(parent_rect) if self.frame.l then extra_width = offsets.l - MR_MIN_OFFSETS.l - self.subviews.tooltip.frame.l = nil - self.subviews.tooltip.frame.r = 0 - self.subviews.icon.frame.l = nil - self.subviews.icon.frame.r = MR_WIDTH-MR_BUTTON_WIDTH + self.subviews.tt_and_icon.frame.l = nil + self.subviews.tt_and_icon.frame.r = 0 else extra_width = offsets.r - MR_MIN_OFFSETS.r - self.subviews.tooltip.frame.r = nil - self.subviews.tooltip.frame.l = 0 - self.subviews.icon.frame.r = nil - self.subviews.icon.frame.l = 0 + self.subviews.tt_and_icon.frame.l = 0 + self.subviews.tt_and_icon.frame.r = nil end local extra_height if self.frame.b then extra_height = offsets.b - MR_MIN_OFFSETS.b + self.subviews.tt_and_icon.frame.t = 0 + self.subviews.tt_and_icon.frame.b = nil else extra_height = offsets.t - MR_MIN_OFFSETS.t + self.subviews.tt_and_icon.frame.t = nil + self.subviews.tt_and_icon.frame.b = 0 end self.frame.w = MR_WIDTH + extra_width self.frame.h = MR_HEIGHT + extra_height From 3c325cc623a72bbfcc7a6288b0a751a1e9ebccbb Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 01:12:00 -0500 Subject: [PATCH 54/84] gui/mass-remove.toolbar: position is constrained to minimum-size interface This limits the possible positions, but prevents a couple of problems when the overlay's "anchor edges" change (i.e., after "touching" a different interface edge while repositioning the overlay): - unexpected "jumps" of the overlay's effective position - "stale" overlay size representation in `gui/overlay` Both of these were caused by swapping the "padding" from one side of the overlay's effective position to the other when swapping anchor edges. Always apply both paddings to keep things consistent. --- gui/mass-remove.lua | 47 ++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 735b1d335..abeecde86 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -407,10 +407,17 @@ end -- l/r/t/b frame positioning fields). However, to preserve player-positioning, -- we take a more circuitous route. --- The button and tooltip are anchored to one side of the overlay's frame (which --- one depends on which edge the overlay itself is anchored to). Then, to --- respond to different interface sizes, we adjust the width and height of the --- overlay's frame to arrange for the button to land in its expected place. +-- In a minimum-size interface area, the player is allowed to place the overlay +-- (a combination of the button and the tooltip) anywhere inside the interface +-- area. When the interface area is resized (usually through window resizes, but +-- also through interface percentage changes), placement is maintained relative +-- to the "ideal" position of the button (i.e. positions are relative to the +-- "ideal" position). When repositioning the overlay in a larger-than-minimum +-- interface area the overlays size is artificially inflated so that the overlay +-- can not be positioned (with respect to the "ideal" button position) farther +-- away than is possible in a minimum-size interface area. This limits the +-- positioning, but keeps the (relative) position consistent across all possible +-- resizes. local tb = reqscript('internal/df-bottom-toolbars') @@ -509,29 +516,17 @@ function MassRemoveToolbarOverlay:init() end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - local extra_width local offsets = mass_remove_button_offsets(parent_rect) - if self.frame.l then - extra_width = offsets.l - MR_MIN_OFFSETS.l - self.subviews.tt_and_icon.frame.l = nil - self.subviews.tt_and_icon.frame.r = 0 - else - extra_width = offsets.r - MR_MIN_OFFSETS.r - self.subviews.tt_and_icon.frame.l = 0 - self.subviews.tt_and_icon.frame.r = nil - end - local extra_height - if self.frame.b then - extra_height = offsets.b - MR_MIN_OFFSETS.b - self.subviews.tt_and_icon.frame.t = 0 - self.subviews.tt_and_icon.frame.b = nil - else - extra_height = offsets.t - MR_MIN_OFFSETS.t - self.subviews.tt_and_icon.frame.t = nil - self.subviews.tt_and_icon.frame.b = 0 - end - self.frame.w = MR_WIDTH + extra_width - self.frame.h = MR_HEIGHT + extra_height + local r = offsets.r - MR_MIN_OFFSETS.r + local l = offsets.l - MR_MIN_OFFSETS.l + local t = offsets.t - MR_MIN_OFFSETS.t + local b = offsets.b - MR_MIN_OFFSETS.b + self.frame.w = MR_WIDTH + l + r + self.frame.h = MR_HEIGHT + t + b + self.subviews.tt_and_icon.frame.l = l + self.subviews.tt_and_icon.frame.r = r + self.subviews.tt_and_icon.frame.t = t + self.subviews.tt_and_icon.frame.b = b end function MassRemoveToolbarOverlay:onInput(keys) From 0eb3df54dd9c546504dcbbc5b2d41a7ff782f31f Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 04:06:21 -0500 Subject: [PATCH 55/84] toolbars: type fix and internal function rename Originally, what is now the "Toolbar" type was just a sequence of offsets, thus the old name and the mismatched type of secondary_toolbars. --- internal/df-bottom-toolbars.lua | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index c78f05625..8088b097d 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -36,7 +36,7 @@ end ---@param widths NamedWidth[] single-name entries only! ---@return Toolbar -local function button_widths_to_offsets(widths) +local function button_widths_to_toolbar(widths) local offsets = {} local offset = 0 for _, ww in ipairs(widths) do @@ -63,15 +63,15 @@ end ---@param buttons string[] ---@return Toolbar -local function buttons_to_offsets(buttons) - return button_widths_to_offsets(buttons_to_widths(buttons)) +local function buttons_to_toolbar(buttons) + return button_widths_to_toolbar(buttons_to_widths(buttons)) end -- Fortress mode toolbar definitions fort = {} ---@class LeftToolbar : Toolbar -fort.left = buttons_to_offsets{ +fort.left = buttons_to_toolbar{ 'citizens', 'tasks', 'places', 'labor', 'orders', 'nobles', 'objects', 'justice', } @@ -93,7 +93,7 @@ end fort.left_center_gap_minimum = 7 ---@class CenterToolbar: Toolbar -fort.center = button_widths_to_offsets{ +fort.center = button_widths_to_toolbar{ { _left_border = 1 }, { dig = 4 }, { chop = 4 }, { gather = 4 }, { smooth = 4 }, { erase = 4 }, { _divider = 1 }, @@ -174,9 +174,9 @@ function fort.center:secondary_toolbar_frame(interface_rect, toolbar_name) } end ----@type table +---@type table fort.center.secondary_toolbars = { - dig = buttons_to_offsets{ + dig = buttons_to_toolbar{ 'dig', 'stairs', 'ramp', 'channel', 'remove_construction', '_gap', 'rectangle', 'draw', '_gap', 'advanced_toggle', '_gap', @@ -184,43 +184,43 @@ fort.center.secondary_toolbars = { 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', }, - chop = buttons_to_offsets{ + chop = buttons_to_toolbar{ 'chop', '_gap', 'rectangle', 'draw', '_gap', 'advanced_toggle', '_gap', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', }, - gather = buttons_to_offsets{ + gather = buttons_to_toolbar{ 'gather', '_gap', 'rectangle', 'draw', '_gap', 'advanced_toggle', '_gap', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', }, - smooth = buttons_to_offsets{ + smooth = buttons_to_toolbar{ 'smooth', 'engrave', 'carve_track', 'carve_fortification', '_gap', 'rectangle', 'draw', '_gap', 'advanced_toggle', '_gap', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', '_gap', 'blueprint', 'blueprint_to_standard', 'standard_to_blueprint', }, - erase = buttons_to_offsets{ + erase = buttons_to_toolbar{ 'rectangle', 'draw', }, -- build -- completely different and quite variable - stockpile = buttons_to_offsets{ 'add_stockpile' }, - stockpile_paint = buttons_to_offsets{ + stockpile = buttons_to_toolbar{ 'add_stockpile' }, + stockpile_paint = buttons_to_toolbar{ 'rectangle', 'draw', 'erase_toggle', 'remove', }, -- zone -- no secondary toolbar -- burrow -- no direct secondary toolbar - burrow_paint = buttons_to_offsets{ + burrow_paint = buttons_to_toolbar{ 'rectangle', 'draw', 'erase_toggle', 'remove', }, -- cart -- no secondary toolbar - traffic = button_widths_to_offsets( + traffic = button_widths_to_toolbar( concat_sequences{ buttons_to_widths{ 'high', 'normal', 'low', 'restricted', '_gap', 'rectangle', 'draw', '_gap', @@ -231,14 +231,14 @@ fort.center.secondary_toolbars = { { weight_input = 6 }, } } ), - mass_designation = buttons_to_offsets{ + mass_designation = buttons_to_toolbar{ 'claim', 'forbid', 'dump', 'no_dump', 'melt', 'no_melt', 'hidden', 'visible', '_gap', 'rectangle', 'draw', }, } ---@class RightToolbar: Toolbar -fort.right = buttons_to_offsets{ +fort.right = buttons_to_toolbar{ 'squads', 'world', } From f832e5de1e2fc2856da247d617493295eea9e2b2 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 04:08:01 -0500 Subject: [PATCH 56/84] toolbars: (demo) show computed button positions --- internal/df-bottom-toolbars.lua | 38 +++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index 8088b097d..b3ef3f881 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -265,6 +265,7 @@ end local gui = require('gui') local Panel = require('gui.widgets.containers.panel') local Label = require('gui.widgets.labels.label') +local utils = require('utils') local Window = require('gui.widgets.containers.window') local Toggle = require('gui.widgets.labels.toggle_hotkey_label') @@ -294,22 +295,26 @@ local demo_panel_width = label_width + 4 local demo_panel_height = 3 local left_toolbar_demo = ToolbarDemoPanel{ + frame_title = 'left toolbar', frame = { l = temp_x, t = temp_y, w = demo_panel_width, h = demo_panel_height }, - subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'left' } }, + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, } local center_toolbar_demo = ToolbarDemoPanel{ + frame_title = 'center toolbar', frame = { l = temp_x + demo_panel_width, t = temp_y, w = demo_panel_width, h = demo_panel_height }, - subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'center' } }, + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, } local right_toolbar_demo = ToolbarDemoPanel{ + frame_title = 'right toolbar', frame = { l = temp_x + 2 * demo_panel_width, t = temp_y, w = demo_panel_width, h = demo_panel_height }, - subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'right' } }, + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, } local secondary_visible = false local secondary_toolbar_demo secondary_toolbar_demo = ToolbarDemoPanel{ + frame_title = 'secondary toolbar', frame = { l = temp_x + demo_panel_width, t = temp_y - demo_panel_height, w = demo_panel_width, h = demo_panel_height }, - subviews = { Label{ frame = { xalign = 0.5, w = label_width }, text = 'secondary' } }, + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, visible = function() return visible() and secondary_visible end, } @@ -320,13 +325,27 @@ function update_demonstrations(secondary) -- [l tool] [c tool] [r tool] (bottom of UI) local toolbar_demo_dy = -TOOLBAR_HEIGHT local ir = gui.get_interface_rect() - local function update(v, frame) + local function update(v, frame, buttons) v.frame = { w = frame.w, h = frame.h, l = frame.l + ir.x1, t = frame.t + ir.y1 + toolbar_demo_dy, } + local sorted = {} + for _, offset in pairs(buttons) do + utils.insert_sorted(sorted, offset) + end + local buttons = '' + for i, o in ipairs(sorted) do + if o > #buttons then + buttons = buttons .. (' '):rep(o - #buttons) + end + buttons = buttons .. '/--\\' + end + v.subviews.buttons:setText( + buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 + ) end if secondary then -- a secondary toolbar is active, move the primary demonstration up to @@ -335,16 +354,17 @@ function update_demonstrations(secondary) -- {s demo} -- [s tool] -- [l tool] [c tool] [r tool] (bottom of UI) - update(secondary_toolbar_demo, fort.center:secondary_toolbar_frame(ir, secondary)) + update(secondary_toolbar_demo, fort.center:secondary_toolbar_frame(ir, secondary), + fort.center.secondary_toolbars[secondary].button_offsets) secondary_visible = true toolbar_demo_dy = toolbar_demo_dy - 2 * SECONDARY_TOOLBAR_HEIGHT else secondary_visible = false end - update(left_toolbar_demo, fort.left:frame(ir)) - update(right_toolbar_demo, fort.right:frame(ir)) - update(center_toolbar_demo, fort.center:frame(ir)) + update(left_toolbar_demo, fort.left:frame(ir), fort.left.button_offsets) + update(right_toolbar_demo, fort.right:frame(ir), fort.right.button_offsets) + update(center_toolbar_demo, fort.center:frame(ir), fort.center.button_offsets) end local tool_from_designation = { From 834c082c5ff096c5a5de50a7ca881702f1dbe70f Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 04:37:47 -0500 Subject: [PATCH 57/84] toolbars: expose button widths --- internal/df-bottom-toolbars.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index b3ef3f881..d4e9f6320 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -21,12 +21,15 @@ end ---@alias NamedWidth table -- single entry, value is width ---@alias NamedOffsets table -- multiple entries, values are offsets +---@alias Button { offset: integer, width: integer } +---@alias NamedButtons table -- multiple entries ---@class Toolbar ----@field button_offsets NamedOffsets +---@field button_offsets NamedOffsets deprecated, use buttons[name].offset +---@field buttons NamedButtons ---@field width integer ----@class Toolbar.Widget.frame +---@class Toolbar.Widget.frame: widgets.Widget.frame ---@field l integer Gap between the left edge of the frame and the parent. ---@field t integer Gap between the top edge of the frame and the parent. ---@field r integer Gap between the right edge of the frame and the parent. @@ -38,17 +41,19 @@ end ---@return Toolbar local function button_widths_to_toolbar(widths) local offsets = {} + local buttons = {} local offset = 0 for _, ww in ipairs(widths) do local name, w = next(ww) if name then if not name:startswith('_') then offsets[name] = offset + buttons[name] = { offset = offset, width = w } end offset = offset + w end end - return { button_offsets = offsets, width = offset } + return { button_offsets = offsets, buttons = buttons, width = offset } end ---@param buttons string[] @@ -146,10 +151,10 @@ function fort.center:secondary_toolbar_frame(interface_rect, toolbar_name) tool_name = toolbar_name --[[@as CenterToolbarToolNames]] end local toolbar_offset = self:frame(interface_rect).l - local toolbar_button_offset = self.button_offsets[tool_name] or dfhack.error('invalid tool name: ' .. tool_name) + local toolbar_button = self.buttons[tool_name] or dfhack.error('invalid tool name: ' .. tool_name) -- Ideally, the secondary toolbar is positioned directly above the (main) toolbar button - local ideal_offset = toolbar_offset + toolbar_button_offset + local ideal_offset = toolbar_offset + toolbar_button.offset -- In "narrow" interfaces conditions, a wide secondary toolbar (pretty much -- any tool that has "advanced" options) that was ideally positioned above From 767ebaaefc5e542cbcb800746eef4b492fae90a4 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 04:40:32 -0500 Subject: [PATCH 58/84] toolbars: (demo) show proper button widths The last couple "buttons" in the advanced traffic secondary toolbar are not 4 columns wide. Those are displayed more accurately in the demo now. --- internal/df-bottom-toolbars.lua | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index d4e9f6320..09d88cb11 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -330,6 +330,9 @@ function update_demonstrations(secondary) -- [l tool] [c tool] [r tool] (bottom of UI) local toolbar_demo_dy = -TOOLBAR_HEIGHT local ir = gui.get_interface_rect() + ---@param v widgets.Panel + ---@param frame widgets.Widget.frame + ---@param buttons NamedButtons local function update(v, frame, buttons) v.frame = { w = frame.w, @@ -338,15 +341,19 @@ function update_demonstrations(secondary) t = frame.t + ir.y1 + toolbar_demo_dy, } local sorted = {} - for _, offset in pairs(buttons) do - utils.insert_sorted(sorted, offset) + for _, button in pairs(buttons) do + utils.insert_sorted(sorted, button, 'offset') end local buttons = '' for i, o in ipairs(sorted) do - if o > #buttons then - buttons = buttons .. (' '):rep(o - #buttons) + if o.offset > #buttons then + buttons = buttons .. (' '):rep(o.offset - #buttons) + end + if o.width == 1 then + buttons = buttons .. '|' + elseif o.width > 1 then + buttons = buttons .. '/'..('-'):rep(o.width - 2)..'\\' end - buttons = buttons .. '/--\\' end v.subviews.buttons:setText( buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 @@ -360,16 +367,16 @@ function update_demonstrations(secondary) -- [s tool] -- [l tool] [c tool] [r tool] (bottom of UI) update(secondary_toolbar_demo, fort.center:secondary_toolbar_frame(ir, secondary), - fort.center.secondary_toolbars[secondary].button_offsets) + fort.center.secondary_toolbars[secondary].buttons) secondary_visible = true toolbar_demo_dy = toolbar_demo_dy - 2 * SECONDARY_TOOLBAR_HEIGHT else secondary_visible = false end - update(left_toolbar_demo, fort.left:frame(ir), fort.left.button_offsets) - update(right_toolbar_demo, fort.right:frame(ir), fort.right.button_offsets) - update(center_toolbar_demo, fort.center:frame(ir), fort.center.button_offsets) + update(left_toolbar_demo, fort.left:frame(ir), fort.left.buttons) + update(right_toolbar_demo, fort.right:frame(ir), fort.right.buttons) + update(center_toolbar_demo, fort.center:frame(ir), fort.center.buttons) end local tool_from_designation = { From a6a604612c7f2eb37e5302405d9fc870693ef0ee Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 06:30:39 -0500 Subject: [PATCH 59/84] toolbars: (demo) make update_demonstrations local --- internal/df-bottom-toolbars.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index 09d88cb11..efb3eb3a9 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -324,7 +324,7 @@ secondary_toolbar_demo = ToolbarDemoPanel{ } ---@param secondary? CenterToolbarSecondaryToolbarNames -function update_demonstrations(secondary) +local function update_demonstrations(secondary) -- by default, draw primary toolbar demonstrations right above the primary toolbars: -- {l demo} {c demo} {r demo} -- [l tool] [c tool] [r tool] (bottom of UI) From 9b362994dc8834d881629c978e58bd1599004571 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 1 Apr 2025 06:31:00 -0500 Subject: [PATCH 60/84] toolbars: some documentation --- internal/df-bottom-toolbars.lua | 131 ++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/internal/df-bottom-toolbars.lua b/internal/df-bottom-toolbars.lua index efb3eb3a9..59e78c58d 100644 --- a/internal/df-bottom-toolbars.lua +++ b/internal/df-bottom-toolbars.lua @@ -2,6 +2,137 @@ -- screen. Not quite as nice as getting the data from DF directly, but better -- than hand-rolling calculations for each "interesting" button. +--[[ + +DF bottom toolbars module +------------------------- + +This module provides a (black box) reproduction of how DF places its toolbar +buttons at the bottom of the screen. Several DFHack overlay UI elements are +placed with respect to (parts of) the toolbars that DF puts at the bottom of the +screen. + +The three primary toolbars are placed flush left, "centered", and flush right in +the interface area (which varies based on configured interface percentage and +the width (in UI tiles) of the DF window itself). + +In narrow interfaces areas (114-130 UI columns) the center toolbar isn't exactly +centered to avoid overlapping with the left toolbar. Furthermore the center +toolbar has secondary toolbars that can be opened above it. These secondary +toolbars are nominally opened directly above the button for the "tool" they +belong to. If there is not room for a secondary toolbar placed above its +"opening button", it will be pushed to the left to fit (even when most of the +width of the toolbar is currently hidden because its advanced options are +disabled). For example, the following secondary toolbars are "pushed left" when +the interface area is narrow enough: + +* dig: 114-175 UI columns +* chop, gather: 114-120 UI columns +* smooth, 114-151 UI columns +* traffic, 114-195 UI columns + +This nearly-centering and width-constrained placement can be derived manually +for specific toolbars, but this often results in using the above-mentioned +widths as "magic numbers". Instead of internalizing these values, this module +implicitly derives them based on the static toolbar widths and dynamic interface +sizes (combined with a few directly observable "padding" widths and a specific +rounding method used for the center toolbar). + +The module provides the following fields: + +* ``MINIMUM_INTERFACE_RECT`` + + The UI tile dimensions (from ``gui.mkdims_wh``) of the minimum-size DF window: + 114x46 UI tiles. + +* ``TOOLBAR_HEIGHT`` + + The UI tile height of the primary toolbars at the bottom of the DF window. + +* ``SECONDARY_TOOLBAR_HEIGHT`` + + The UI tile height of the secondary toolbars that are sometimes placed above + the primary toolbar. + +* ``fort`` + + The static "definitions" of the fortress mode toolbars. Each of + ``fort.right``, ``fort.center``, and ``fort.left`` has the following fields: + + * ``toolbar.width`` + + The overall width of the DF toolbar. + + * ``toolbar.buttons`` table that can be indexed by "button names" provided by + this module: + + * ``fort.left.buttons`` has ``citizens``, ``tasks``, ``places``, ``labor``, ``orders``, + ``nobles``, ``objects``, and ``justice`` buttons + + * ``fort.center.buttons`` has ``dig``, ``chop``, ``gather``, ``smooth``, ``erase``, + ``build``, ``stockpile``, ``zone``, ``burrow``, ``cart``, ``traffic``, and + ``mass_designation`` buttons + + * ``fort.right.buttons`` has ``squads``, and ``world`` buttons + + Each button (``toolbar.buttons[name]``) has two fields: + + * ``button.offset`` + + The offset of the button from the left-most column of the toolbar. + + * ``button.width`` + + The width of the button in UI tiles. + + * ``toolbar:frame(interface_rect)`` method + + The ``interface_rect`` parameter must have ``width`` and ``height`` fields, + and should represent the size of an interface area (e.g., from the + ``parent_rect`` parameter given to a non-fullscreen overlay's + ``updateLayout`` method, or directly from ``gui.get_interface_rect()``). + + Returns a Widget-style "frame" table that has fields for the offsets (``l``, + ``r``, ``t``, ``b``) and size (``w``, ``h``) of the toolbar when it is + displayed in an interface area of ``interface_rect`` size. + + The interface area left-offset of a specific button can be found by adding a + toolbar's frame's ``l`` field and the ``offset`` of one of its buttons:: + + local tb = reqscript('internal/df-bottom-toolbars') + ... + local right = tb.fort.right + local squads_offset = right:frame(interface_size) + right.buttons.squads.offset + + The ``center`` definition has an additional field and method: + + * ``fort.center.secondary_toolbars`` + + Provides the definitions of secondary toolbar for several "tools": ``dig``, + ``chop``, ``gather``, ``smooth``, ``erase``, ``stockpile``, + ``stockpile_paint``, ``burrow_paint``, ``traffic``, and + ``mass_designation``. See the complete list of secondary toolbar buttons in + the module's code. + + The definition of a secondary toolbar uses the same ``width`` & ``buttons`` + fields as the primary toolbars. + + * ``fort.center:secondary_toolbar_frame(interface_rect, secondary_name)`` + + Provides the frame (like ``toolbar:frame()``) for the specified secondary + toolbar when displayed in the specified interface size. + + Again, a button's ``offset`` can be combined with its secondary toolbar's + frame's ``l`` to find the location of a specific button:: + + local tb = reqscript('internal/df-bottom-toolbars') + ... + local center = tb.fort.center + local dig_advanced_offset = center:secondary_toolbar_frame(interface_size, 'dig') + + center.secondary_toolbars.dig.advanced_toggle.offset + +]] + --@module = true TOOLBAR_HEIGHT = 3 From 1a101c224f82046bec34f56ef9739278fdd942ad Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 2 Apr 2025 12:43:47 -0500 Subject: [PATCH 61/84] Fix the overlays being overwritten --- gui/design.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 9f409ebea..a106655e5 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -175,11 +175,6 @@ function RightClickOverlay:onInput(keys) end end -OVERLAY_WIDGETS = { - dimensions=DimensionsOverlay, - rightclick=RightClickOverlay, -} - --- --- HelpWindow --- @@ -1654,7 +1649,11 @@ function DesignToolbarOverlay:onInput(keys) return DesignToolbarOverlay.super.onInput(self, keys) end -OVERLAY_WIDGETS = {toolbar=DesignToolbarOverlay} +OVERLAY_WIDGETS = { + dimensions=DimensionsOverlay, + rightclick=RightClickOverlay, + toolbar=DesignToolbarOverlay +} if dfhack_flags.module then return end From f44a37ab6fa163b073b8eefd247364a5c3d33bb1 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Apr 2025 21:15:15 -0500 Subject: [PATCH 62/84] Update to use the Military Module --- autotraining.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/autotraining.lua b/autotraining.lua index d6ec90968..08ff3af19 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -203,9 +203,10 @@ function addTraining(unit) for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do if ( squad.positions[i].occupant == -1 ) then - squad.positions[i].occupant = unit.hist_figure_id - unit.military.squad_id = squad.id - unit.military.squad_position = i + dfhack.military.addToSquad(unit.id,squad.id) + -- squad.positions[i].occupant = unit.hist_figure_id + -- unit.military.squad_id = squad.id + -- unit.military.squad_position = i return true end end @@ -218,9 +219,10 @@ function removeTraining(unit) for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do if ( unit.hist_figure_id == squad.positions[i].occupant ) then - unit.military.squad_id = -1 - unit.military.squad_position = -1 - squad.positions[i].occupant = -1 + dfhack.military.removeFromSquad(unit.id) + -- unit.military.squad_id = -1 + -- unit.military.squad_position = -1 + -- squad.positions[i].occupant = -1 return true end end @@ -234,9 +236,7 @@ function removeAll() for i=1,9,1 do local dwarf = getByID(squad.positions[i].occupant) if (dwarf ~= nil) then - dwarf.military.squad_id = -1 - dwarf.military.squad_position = -1 - squad.positions[i].occupant = -1 + removeTraining(dwarf) end end end From 7c186d32149730944780566a94a503b997b3c23a Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Apr 2025 21:31:08 -0500 Subject: [PATCH 63/84] use the squad position --- autotraining.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotraining.lua b/autotraining.lua index 08ff3af19..d903d5600 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -203,7 +203,7 @@ function addTraining(unit) for _, squad in ipairs(getTrainingSquads()) do for i=1,9,1 do if ( squad.positions[i].occupant == -1 ) then - dfhack.military.addToSquad(unit.id,squad.id) + dfhack.military.addToSquad(unit.id,squad.id,i) -- squad.positions[i].occupant = unit.hist_figure_id -- unit.military.squad_id = squad.id -- unit.military.squad_position = i From f670df7212a73cec11fe2bb34f53f02a08c452da Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 6 Apr 2025 15:10:59 -0500 Subject: [PATCH 64/84] Remove all training dwarves when you disable --- autotraining.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/autotraining.lua b/autotraining.lua index d903d5600..f509091f7 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -277,6 +277,7 @@ function start() end function stop() + removeAll() repeatUtil.cancel(GLOBAL_KEY) dfhack.println(GLOBAL_KEY .. " | STOP") end From 13e50ccc268e09a63d0d0d5bfb93745c9401db45 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 7 Apr 2025 10:29:47 -0500 Subject: [PATCH 65/84] add prefer nicknamed --- gui/spectate.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 3e7bbd09b..53673166f 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -199,8 +199,9 @@ function Spectate:init() create_toggle_button({t=11}, 'include-wildlife', 'CUSTOM_ALT_W', rpad('Include wildlife', lWidth)), create_toggle_button({t=12}, 'prefer-conflict', 'CUSTOM_ALT_B', rpad('Prefer conflict', lWidth)), create_toggle_button({t=13}, 'prefer-new-arrivals', 'CUSTOM_ALT_N', rpad('Prefer new arrivals', lWidth)), + create_toggle_button({t=14}, 'prefer-nicknamed', 'CUSTOM_ALT_I', rpad('Prefer nicknamed', lWidth)), widgets.Divider{ - frame={t=15, h=1}, + frame={t=16, h=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, From 5f68243cbc24ca078e3e4df852a6a138290c6c42 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 7 Apr 2025 10:34:07 -0500 Subject: [PATCH 66/84] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 213e521fd..ce08b301a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/spectate`: added "Prefer nicknamed" to the list of options ## Fixes - `list-agreements`: fix date math when determining petition age From 3fb5645bd4df60c76403112f99cd4b37c507c93d Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 7 Apr 2025 10:38:10 -0500 Subject: [PATCH 67/84] make it look better --- gui/spectate.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 53673166f..558e281b5 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -46,7 +46,7 @@ end Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={l=5, t=5, w=36, h=41}, + frame={l=5, t=5, w=36, h=42}, } local function create_toggle_button(frame, cfg_elem, hotkey, label, cfg_elem_key) @@ -207,29 +207,29 @@ function Spectate:init() frame_style_r=false, }, widgets.Label{ - frame={t=17, l=0}, + frame={t=18, l=0}, text="Tooltips:" }, ToggleLabel{ - frame={t=17, l=12}, + frame={t=18, l=12}, initial_option=overlay.isOverlayEnabled(OVERLAY_NAME), on_change=function(val) dfhack.run_command('overlay', val and 'enable' or 'disable', OVERLAY_NAME) end, key='CUSTOM_ALT_O', label="Overlay ", }, widgets.Label{ - frame={t=19, l=colFollow}, + frame={t=20, l=colFollow}, text='Follow', }, widgets.Label{ - frame={t=19, l=colHover}, + frame={t=20, l=colHover}, text='Hover', }, - create_row({t=21}, 'Enabled', 'E', '', colFollow, colHover), + create_row({t=22}, 'Enabled', 'E', '', colFollow, colHover), - create_numeric_edit_field({t=23}, 'tooltip-follow-blink-milliseconds', 'CUSTOM_B', 'Blink period (ms): '), + create_numeric_edit_field({t=24}, 'tooltip-follow-blink-milliseconds', 'CUSTOM_B', 'Blink period (ms): '), widgets.CycleHotkeyLabel{ - frame={t=24}, + frame={t=25}, key='CUSTOM_C', label="Hold to show:", options={ @@ -242,11 +242,11 @@ function Spectate:init() on_change=function(new, _) dfhack.run_command('spectate', 'set', 'tooltip-follow-hold-to-show', new) end }, - create_row({t=26}, 'Job', 'J', 'job', colFollow, colHover), - create_row({t=27}, 'Activity', 'A', 'activity', colFollow, colHover), - create_row({t=28}, 'Name', 'N', 'name', colFollow, colHover), - create_row({t=29}, 'Stress', 'S', 'stress', colFollow, colHover), - create_stress_list({t=30}, colFollow, colHover), + create_row({t=27}, 'Job', 'J', 'job', colFollow, colHover), + create_row({t=28}, 'Activity', 'A', 'activity', colFollow, colHover), + create_row({t=29}, 'Name', 'N', 'name', colFollow, colHover), + create_row({t=30}, 'Stress', 'S', 'stress', colFollow, colHover), + create_stress_list({t=31}, colFollow, colHover), } end From ddae413059113621baa380e23bd1ccf35a2bf706 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 7 Apr 2025 13:00:20 -0500 Subject: [PATCH 68/84] 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 51df00d67..82c17c124 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 595a760495ff3dea9b11d6c1ecafb667d1f10a80 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 10 Apr 2025 16:34:16 -0500 Subject: [PATCH 69/84] disable autotraining on map unload --- autotraining.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/autotraining.lua b/autotraining.lua index f509091f7..2f6b03f64 100644 --- a/autotraining.lua +++ b/autotraining.lua @@ -75,6 +75,10 @@ local function load_state() end dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state.enabled = false + return + end -- the state changed, is a map loaded and is that map in fort mode? if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then -- no its isnt, so bail From eaa3fc986c9d51e340a8a457761240d0ccef4fca Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 11 Apr 2025 11:00:37 -0500 Subject: [PATCH 70/84] 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 8972fece7..7fa89bae4 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 82c17c124..6de6067e9 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 71/84] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 213e521fd..d03a2061f 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 72/84] 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 7fa89bae4..8972a13f6 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 73/84] 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 6de6067e9..edd6fd0bb 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 74/84] 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 8972a13f6..52f652efd 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 75/84] 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 edd6fd0bb..b4a36d0ff 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 76/84] 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 52f652efd..0b957cd2a 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 77/84] 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 b4a36d0ff..a3f2e767b 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 78/84] 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 a3f2e767b..33885416e 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 79/84] 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 33885416e..dcf25d31c 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 80/84] 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 dcf25d31c..f4891c106 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 81/84] 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 f4891c106..2edd503b5 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 82/84] 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 2edd503b5..7a7897c81 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 83/84] 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 7a7897c81..69cd2ae80 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 84/84] 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 69cd2ae80..6284eb1c7 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