From 3fc7d4ee1f505789622f1453940e74f06e5f31f3 Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Tue, 2 Jul 2024 04:29:11 +0300 Subject: [PATCH 001/919] =?UTF-8?q?=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about/Removed.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index f02dc27d18e..a80b1e637ee 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -10,6 +10,12 @@ work (e.g. links from the `changelog`). :local: :depth: 1 +.. _linger: + +linger +====== +Merged into `bodyswap` as `bodyswap linger`. + .. _adv-rumors: adv-rumors From b97455668d966231c36c48abd9a617a846414620 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 1 Jul 2024 18:43:06 -0700 Subject: [PATCH 002/919] Update docs/about/Removed.rst --- docs/about/Removed.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index a80b1e637ee..94cf58e5bad 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -14,7 +14,7 @@ work (e.g. links from the `changelog`). linger ====== -Merged into `bodyswap` as `bodyswap linger`. +Merged into `bodyswap` as ``bodyswap linger``. .. _adv-rumors: From d5b6f2044a90c8a6e648f155c72ff155d15f2d27 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:31:13 -0600 Subject: [PATCH 003/919] Update Stonesense docs (see description) Update the Stonesense docs to tell about using OpenGL on Windows for stability and reflect the changes made in https://github.com/DFHack/stonesense/pull/121 Also added a note about our need for some missing art in stonesense (we don't have sprites for bookcases and pedestals for example) --- docs/images/stonesense-yellowcubes.png | Bin 0 -> 178548 bytes docs/plugins/stonesense.rst | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png new file mode 100644 index 0000000000000000000000000000000000000000..83ae6ce5077a512ae28286bfa0748c62d86374e8 GIT binary patch literal 178548 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 z zdDB|>sxhn4<>NGl(w8p3tDaRi(xdxi%Dct%Tii2kqTBo^+=ahe0$_R21AIOP2frX3 z_r3_RzyD=5*3U`COG;zfDsNlo+Vx2X-3f2%qrYyxE-%{g`lavGoAzD2rVSje55K!5 z8(B=>qBN%Sy1!?h>fX()h-da>O2=~dq=-iFM*B$!fD zTLl=ocsjUJ}N7yM9(34MP5Y!nAPw5nXZ!l<=={%-C-~QK3`Q@QMJ$d$|$)7!( zrl$f;&D8)yoEvqj=>+KhW>Z__;kpwV z6S(~D6Zif$H*P0QCubiGUV@cj>A>8x+uHt5q`<#jIPN_qG&J;+(y4!>vV9FbK;5WU z+X=R(%i7Zg)&a|qhU1_M5Vo|`c|^yA9+>HOWMiRt3feekkLB0G<2f2Qw^{3g6~ zwGH>GM;ngvjFZ4R9&s@pB|hdDYYwr|E&dKm6^F<~8!uBg}{4)%_m!rF9w)jjcD&hj31W zbW0mMF{_aZTYs-Eb}wz@HH=)X!kWtNes8=!k5>end*CQH49G_!;*;=XXe&+}@*_xN z*~vq&8xi5zu!-k0nMpq9qAV1pAcc~s@SY96<2dVa!Xx2$=lwky=WUcvMslbxMl@pM z&1So*yNb*72%JyqmA#FuSJ6qD79ScXMg6OsGqV#hJ3XH7VWylNt_5itj<1U_O>N~l zV#pLux!EA}M?)*)!*ADH@(?Z*y>P)F8WfI@X`|eNn(GjD#;z0TT@N7h={F4{m0h1W zv5~<^aDSa-8 z=I11vZ|HeOy8E2U=Xq9o_(g@?QW-YpE(_X|s4N%yb8B+Oi=L*Zl}^9m4RxY?^6*i> z&%_5x|7V=aOOKSq@Vd$YpdpP{*4=SEJ>#m!qT1N=s`J;C|7)t#*Q1X*^*y79x_w>s zd0KI+4XqH;=oCdK9_r#x(xmQ5zo0RBe`Lz4tB?NRchi140KeQL1*h}bu_z$p!B1UE zHjFlU)N$lO%K^!ww@-D+`cMbgW1#O#`pxal$go>x;t<{F2xWjfw{q*x2G^rQCwHFC z*G4v`F$0Kcpd~cHQF*{4>Yl$kb)>D2%D#<`55&-jJC$mN`OXl|xAE#+m)Fe$B)-B4 z@3I`HAq+Z8W|Ud!0qcV4O5~&@UGmz};whT~5SQ}EGOSbg!in?3Ni*$Dr!gIo{>Fv} zI1U=@_Ut>62EKmQ!0fDpAP_!X7~c3+qgOalVW?T1Q0VZ+5c%TU~5FGd&Wg zr`T@U^G#2-*9TC&5_%+NAdZV39}GI`SxHB41q4)<5kn`B@Vhb4Yr*&)^YHXM^_j!y zCJ%t(GB@5Pit;G8jk68oGyO3+Cx!3piM7@Bc;$twVlwpE5F-qXoktv9MFXG%LAy>U zoCHRFoATfLBccp1_vhLSxptVkvCh~)6uK0sygK} zsv|EB9@$VXII{gTWfm|JAuW(jw~5EtKxf`zC0e1W_v&(HWw30r^v;xEi=WP`Ht_B(E&ECbpF1Z!h%#sgFm z#=&fu1cSW0!=A6gVU~Amc+jkYHz43t+`G*40ZMbAGIq9CRCmjdR|g{xj5pJ47>&U`Iwn;IK23Kusy2=2h&Y;b zRHH)9RaZJIIHuxH865EQz+W2z90GK9*CTjpZan#_c)lkesF%_foi$ZT7&{Z)$bF>= z(^D^)VjwCg~Rbl%e91KWb)X6t@i03H)DK5U{gO{g&xw1= z-`vJAHmDYX^{fI%mo%ekwsn55!OS3nU-) zm${U_(prv`4UOm-G@>WY(@x_~OX(u{Ih#U0)4*?ZEcqfJ4Ywwsd$bvFr*7+k#?qB? zByWA*Z(gmpvz6#yYN zPnH8V(NbXvS-xqgGAm8ddE`+F_FM04acEA)UBg_Qi=V+5kq2k)aAZ^lK!QkRYk|w4 zh=vZFV}=;Ewt4-fdgt75oHr*oy^=JdG-~y1_2gPa_F!d%5{$16(3=XG4@OclRb6>d zEbw5kFb<5QcrY5pbxLpJbmMp9jhwOs6=_iX2!(oUXhbrbj6{ z-}MxZqNgC~N`8|cV~4yLAz*wP*{duN(!qk?$b!)|PCVl&BYDy-ugc0gC=L9hj(O#b z^4ih3K2XugmuZ{&G`O?y=6ffdB9osaY5^3ZH{|EDrbg09Q4W8J)8$Y;I)&4wa|FAQ zZM!VWfafL6Za(x;23|ZN9fmRW%1AOSb)eyGW{STHZ-z=65LJ8hH+Av@_D5 z*E4a(jrK}8_h`urar;hdH9`Am037Qdk@0DMM#{Tawr>m zGEkqEQSwqe%(g9O;&#wvZi6z`c@Y-PNy8cmJF#;53MZsL;(1FV2-r!P3-kK3lcK{; zS;~K}_mQ;W_@#|+yU6w2R-VVtO?9r3=OtHHHL4+9O>G?(N;;H{-rnWp^j{-T2xqc7fD=g>goBRMuM~|r+PaZdZfN(Bt#E_aMdLT0N4NiTH zGna-nN^$;IU%U~k%j* zNM3<6-&di!1N?S5Zn*HidNID<+`Z*8+5l-h9nhsS!=x;^;N0i{Z)K$Ztf$a(rz+fV zJ0bcxnSJ}tGy3gqzOyqAYKVLy@vg7mj@7lB;^A&iU7eVFI5~q+1UK^G@~c-Yn0mU%hB~;O){XEgM`WC-Pv1LJvZ`3 zg$xtVm_97MVY-lZ;(D7;=wCW(=xxew1G+#OPfxk?0bcUfbQ;^ptUW#ZieYQe{4qa$lpL7@F6hT4doG5`Ddm_<2yh8 zmU!}UUeyqO_lO1q1=t*}OQ~(Y#IXp^hy^FYv=-A_beh---1np3`Ib*|b^M+(a{3YA zOm+^0Vms^y$%CW01u(5IFBph z#?2>{0*IsH{B}>AA-dyd5*h9iFbMdQ~1UV5DhaHFsY;aXsu@ptr(&-o!mk&W3>Av_O*$q9U_=Hv%c3 zj8op4D+A)jiy}MDj*9)|Bbf#oc=!6~=xpNH+**>-v4xd)v#TRA$U^$4(y5^+ZD&-c z?=@|d74v@r%b`n~dw1}I9zRnj$i@cLyz${tk4)Omn>c{qnO^za*xdmkZSjvf!Xuud z3B54y+&}^NSyL_P1d{L8AL_`aZ@oN1_=(r|qbQ%pG7w!53;`Boh;bruKvV@&lK8uF_o;2E6?MwJ|RMYf4q>D?}pLrG`iR$ZVC z74FDQpUDH_gfC^Myu?jR55>gnP>fC5K<~&ub2MRdoo%wz(R`s4ePq5GJpmng!O!Ne z8;8D_pVZv-7nRBnbm$X~M4k^h$Y1sVTV*@t^sH|x1vvE`l%yvubh$&sY(jVa6G+G3 z)SVmvdCVK+a46pQ#D(+Iaq0xGm1d(AZP7FXz1;%N*3;q?H7Qx?odq1%kK0Nv;d$k!Dr(BN;;RFHFk!U+^j zb}5FWT@8AD+1mqo#>Pfs?&MUQJTsfu6ny`pA=(EBAxsG&?`~{SohC+q8p}(pK^^M#4z*|Nqqa!C`P)-xt02aV8Oc-5c4e$vaH8t7>;s}8@Mtn3T z466*U2v}8cNx^pbpcYB6_#}9d&z_}&dAkPj5Psz8IevkLjt|m>i#rzJ4zL+zIX;WBYln7eNyg z9@kImVmj$SRy}RxQx}m{<}~mkJ=Q{ako)H9ezw&uIg;AaYo1uf-JAyWzVK7;GRy@o z)4Xj1DaZ%?0H0}3l($VMxLiNigPe}HGo<{~iF{Ol=^UfKTx%0A3LFowg*Ew(zB+&5 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 literal 0 HcmV?d00001 diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 69b305261d5..56461f1a952 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -19,7 +19,7 @@ or be moved independently. Configuration for stonesense can be set in the ``dfhack-config/stonesense/init.txt`` file in your DF game directory. If the window refresh rate is too low, change ``SEGMENTSIZE_Z`` to ``2`` in this file, and if you are unable to see the edges of the map with the overlay active, try decreasing the -value for ``SEGMENTSIZE_XY`` -- normal values are ``50`` to ``80``, depending +value for ``SEGMENTSIZE_X`` and ``SEGMENTSIZE_Y`` -- normal values are ``50`` to ``80``, depending on your screen resolution. .. figure:: ../images/stonesense-roadtruss.jpg @@ -52,7 +52,7 @@ See ``dfhack-config/stonesense/keybinds.txt`` to learn or set keybindings, inclu zooming, changing the dimensions of the rendered area, toggling various views, fog, and rotation. Here's the important section: -.. include:: ../../plugins/stonesense/resources/keybinds.txt +.. include:: ../../plugins/stonesense/configs/keybinds.txt :literal: :end-before: VALID ACTIONS: @@ -78,6 +78,20 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. +If you are on Windows, you may find that using OpenGL runs better and more stable. +Simply open :file:`dfhack-config/stonesense/init.txt` and set the renderer to ``[RENDERER:OPENGL]`` + +Yellow Cubes and Missing Sprites +-------------------------------- +If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. + +.. figure:: ../images/stonesense-yellowcubes.jpg + :align: center + + An example of the yellow cubes. + +If you would like to help us in fixing this, please see the wiki page on :wiki:`adding content ` + Useful links ------------ - Report issues on `Github `_ From 8a548e65b930eb708d6069848e4fe74991615710 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:38:02 -0600 Subject: [PATCH 004/919] my new pic is a png not a jpg --- docs/plugins/stonesense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 56461f1a952..efdcb7f230e 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -85,7 +85,7 @@ Yellow Cubes and Missing Sprites -------------------------------- If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. -.. figure:: ../images/stonesense-yellowcubes.jpg +.. figure:: ../images/stonesense-yellowcubes.png :align: center An example of the yellow cubes. From a6b97aab627c17d48da3130cab263402a314b4f7 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 19 Jan 2025 17:11:36 -0600 Subject: [PATCH 005/919] we fixed the resize crash. removing old info --- docs/plugins/stonesense.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index efdcb7f230e..12649c7add0 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,9 +78,6 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. -If you are on Windows, you may find that using OpenGL runs better and more stable. -Simply open :file:`dfhack-config/stonesense/init.txt` and set the renderer to ``[RENDERER:OPENGL]`` - Yellow Cubes and Missing Sprites -------------------------------- If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. From 46693058d93327f01427fb96ca608cf1b6981f97 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 19 Jan 2025 17:21:07 -0600 Subject: [PATCH 006/919] Update stonesense.rst --- docs/plugins/stonesense.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 12649c7add0..48f321cbde0 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -93,6 +93,7 @@ Useful links ------------ - Report issues on `Github `_ - `support` +- `Stonesense Subreddit `_ - :forums:`Official Stonesense thread <106497>` - :forums:`Screenshots thread <48172>` - :wiki:`Main wiki page ` From 130787901b46458ed8194973489a103215b836d7 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:40:58 -0600 Subject: [PATCH 007/919] Update the Yellow cubes blerb --- docs/plugins/stonesense.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 48f321cbde0..3864b69df4f 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -87,7 +87,9 @@ If you are seeing yellow cubes in Stonesense, that is simply things we don't hav An example of the yellow cubes. -If you would like to help us in fixing this, please see the wiki page on :wiki:`adding content ` +If you would like to help us in fixing this, there are 2 things you can do: +- Make an issue on Github with what item is missing and pictures of what it looks like in DF +- Create the art yourself. For help with this, please see the wiki page on :wiki:`adding content ` Useful links ------------ From 598aa1237f773a31d5243de20d4adb682b9ee45a Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:17:22 -0600 Subject: [PATCH 008/919] New Widget: Slider This is just like Range_Slider except that it has only one slider bar. --- library/lua/gui/widgets.lua | 1 + library/lua/gui/widgets/slider.lua | 132 +++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 library/lua/gui/widgets/slider.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 8a5daab7b23..e7870f88e86 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -27,6 +27,7 @@ List = require('gui.widgets.list') FilteredList = require('gui.widgets.filtered_list') TabBar = require('gui.widgets.tab_bar') RangeSlider = require('gui.widgets.range_slider') +Slider = require('gui.widgets.slider') DimensionsTooltip = require('gui.widgets.dimensions_tooltip') TextArea = require('gui.widgets.text_area') diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua new file mode 100644 index 00000000000..92263d2924f --- /dev/null +++ b/library/lua/gui/widgets/slider.lua @@ -0,0 +1,132 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field get_idx_fn? function +---@field on_change? fun(index: integer) + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, Widget) +Slider.ATTRS{ + num_stops=DEFAULT_NIL, + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, +} + +function Slider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function Slider:init() + if self.num_stops < 2 then error('too few Slider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function Slider_get_width_per_idx(self) + return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(left_idx-1) + 4 + if x < left_pos then + self.on_change(self.get_idx_fn() - 1) + else + self.is_dragging_target = 'both' + self.is_dragging_idx = x - right_pos + end + return true +end + +local function Slider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + if self.is_dragging_target == 'both' then + if new_idx > self.num_stops then + return + end + end + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function Slider:onRenderBody(dc, rect) + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx ~= stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif left_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if left_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tab + dc:seek(width_per_idx*(left_idx-1)+2) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + Slider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return Slider From 8779a7bd8f314bc07a8d472df9cb38a1799dc614 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:24:38 -0600 Subject: [PATCH 009/919] Add Lua API Docs for the new widget --- docs/dev/Lua API.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bfadfc68994..fbff70120ee 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6440,10 +6440,24 @@ change, the ``RangeSlider`` appearance will adjust automatically. :get_left_idx_fn: The function used by the RangeSlider to get the notch index on which to display the left handle. :get_right_idx_fn: The function used by the RangeSlider to get the notch index on which - to display the right handle. + to display the right handle. :on_left_change: Callback executed when moving the left handle. :on_right_change: Callback executed when moving the right handle. +Slider class +----------------- + +This widget implements a mouse-interactable slider. The player can move the handle to +set the value of the slider. The parent widget owns the slider value, and can control +it independently (e.g., with ``CycleHotkeyLabels``). If the value changes, the ``Slider`` +appearance will adjust automatically. + +:num_stops: Used to specify the number of "notches" in the slider, the places + where the handle can stop. (This should match the parents' number of options.) +:get_idx_fn: The function used by the Slider to get the notch index on which + to display the handle. +:on_change: Callback executed when moving the handle. + DimensionsTooltip class ----------------------- From 793f9cfc0d9b87446b8ed2d1fa72372f41589b31 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:02:17 -0600 Subject: [PATCH 010/919] Remove the Yellow Cubes Section --- docs/plugins/stonesense.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 3864b69df4f..b9f6c936685 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,19 +78,6 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. -Yellow Cubes and Missing Sprites --------------------------------- -If you are seeing yellow cubes in Stonesense, that is simply things we don't have sprites for. - -.. figure:: ../images/stonesense-yellowcubes.png - :align: center - - An example of the yellow cubes. - -If you would like to help us in fixing this, there are 2 things you can do: -- Make an issue on Github with what item is missing and pictures of what it looks like in DF -- Create the art yourself. For help with this, please see the wiki page on :wiki:`adding content ` - Useful links ------------ - Report issues on `Github `_ From a62cdf1fc5f6013a69ea50e7c15843e2141e114d Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:53:22 -0600 Subject: [PATCH 011/919] Delete stonesense-yellowcubes.png --- docs/images/stonesense-yellowcubes.png | Bin 178548 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png deleted file mode 100644 index 83ae6ce5077a512ae28286bfa0748c62d86374e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178548 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 z 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%0A3LFowg*Ew(zB+&5 zL}%IB%hVM&{o>fa8L$A)R$q2_;c!@Z=WZ`v1%jjL`5*q!w`HwgpI?i`HKrIMxA&f# z!C>^{NaNR@yA|L4_Ve+fM^DF>USo0A4fC}-G*%2v#Je7ti+5d;G6?yXUfYQO;%AS( zYBw38p3lxcJ#863F@Wnaq#9~yEb4TPJr!i7mf->Z<82Ny%Y)HX7jv==J4} zmt#OYO6Ps!N8_!GR`N6i!V3`Qp|pieL!mJsG>ih_6%W*ja|{#oD@(>tog9kE8P!3& zSy@_->zD7QM<*>tpq$s~=?5iLt|-{eofb^LG1M=^?Zbesd-gzOL?_3&i^)A*c4!nm zlk#|t?1{5uF{%*=uzXvcyk)P&$ducdcc@}?XSo7xdBGdn zUDiGEcc->~vt3)%2OZ5du+(YeY(qMQ%x_((sk!4g;RziiIt%D}wv(ijj&paqEa}cN zn;DvWlsP#$9N+f-2jX3CyFX^9CnU43P2})l?&4Z!+STorg?Fa5Hn;pDE%36d3&)b} z;6sm|`XuWhoLJ|~2p2bU86&6GIXgla(^+Sx$6{=_Kc}$(5bel|<2cAOtlkAvV~ki{ zy|bD3ZTTDtX;rCUxQv41u^Z;d%Q4@kugMe-T@mf^%6H>1B81!6*vqE@QIr*%Zy0`t zw}~~&BD}D>wC0!M_zq6;@KikcGiQcpic}_WcIj*A68LWaiiZ}{z``B|13ShP7ex$- zx9k$xmS8|Z8N)*uE~Sw|VK`7~6c2zN8Xn5Vca#(*V@F&zavD?93(fg!luPj_BLE)q z5%!1wpMOA&bSK{bf%nDI(o#J3*kkef>zCtTSB7MODJS)cWUSy7@&X6-szw1dl#fv* z+~w@00m0cM50dx#lQB6slwsKWkg!dufv>Caf!h2oex)49qu&Wl8hStINWM*xMSPq{ zLkIYa|K!cIYS?Myl&5;(7$V+KiJ@?(0f%abo1n6cNo5QF!NG${_b%alQt2KR?GFjZ zBl&%I??LhTVSRr@4W4;@-y+@C4>0kYt`l?_R0#hvT%8E%YAN*NZ&2Lsfoffz0w{Wd_d2;^gO0|sV)uu`F&6IJ9t?6PN?h+@lJ*m zT*z6d%o9^kGy+u~Q5a^@rM&q@Iz8Y(7Ns-fBoF`d54tgs;L7@72#L!2H|fZQ&MO{V z5oe`q?^a$$73)gj`er_T;}x`{vR_OA(#BXsN7PJRZEcA2yXJR+sZ!o5F27rzK-MVJ zQ&%3mP!{+tD+Bp$ z$5K8`r~*2_D0^78=b`&g#D%jn=?rpeRpE?K7-29ILK|Y?z1Bx)U$7!+*F(LyXY0=V za`)24`%a9-te8)oPfw1-1E)M+&tb!_U0oF74yE^md!20el8Mie>>h5$&VhR0`)20A zfalq%_|6WyR`g4HL|S^X7__Q5Mis{7Ji6lfP7fp-l^(vsc;aipnO+)-i2&%~Sb$P` zyu88yoWYx&BLNU7IEhE-%)6)X1@w3dXS*j;;4&M7(1MYPzL+_Ug7@T0`(JqGH7U8v zV}ZDgNG>eyaZe{U37l!Ep%@*W%J1;! zyT13^b7Az{`E$9gk4q>2;ZOg==slQFXJN)YL5(mNEz$cy4)hxPgvY%=dSJX)!y}SZ zK>cQsbYyt?4fW;*+s)0tCR)FtQOn=d?;9D85lN#i`soE5k4Gd0Tw^^bU4a(rhwPb# zK;dg~(RxtM!wnVSJ8u}I=bt@b>5&_w4c64yDRqLzQ&+O#>7h5cvGw~j(*8G z8O%)nNzwKx(d;*4B0G#K{DK~KZDm<4KtAX^^<3NZ^bK{xcn&JgC;Yud)i}{ndf(Jf zjaj;YzOQc1D~&I@93A9snk;^spTsjl$ns=x_xJy%c>5js`^{i*;OUqg`%5b0>q#?< z?M0_MP3|F=0RxaTyr;dv+clNRhzOmjX;4NG}ECs zyzbQ6a1)383y#KjN5`e3)CXW%w%V+12ko<~zg6esfG#I*o3b;k$zQY(&j>j$NLAec zWPyxJulQYU5}e>Dy#Ywa1C2!|X{M%zgpjdzxycs@sR*ejv*(#SlDz{BnCh?z{2^{A};lX2L#Q{gaI3PGPR z2!^9bH64L+(unxW^#}fhmxc_ZDTCjnhjFAuKRN;k?Y8WzBl@N=h~VafEA?eNWg&-c+Qxq zA}L&!wI8RZ9}q6}y7ZexO6X^b&5akwVFv>wo%;TccfU1Wdg;aZzz4W3S4t|6|K%@z zO)U{e?P0n5Px?RUmn>B_gyv{PjfW8ejwBbF6@T2392~q=&g$P)c_)NpQx57^WK>>K zUYv-hBB9ItW?E1UUp(@QtB|)F1Mjk7bmThwly1sh&w+FY!(ao$2uubIdTS7yyZfFN09aFDDzS_NK)DBp=auJdtgQdPWE3CC@HPw$eoA%yg%}+CqS7yQ>QeQw zUYL)i4}L)yy=r$1QWiaQKxmXTEyt8DZO{e_N8xaHz6eh`6^D@lG7pY0N54LV36Ic+ z?)uETr3W9CgZu%7pUZr0z`0W=?kH317sb@WQ?Jm0gQ{~iO>I&h=ts5S>7yOs7|F+U?O^{o;e1%(-=4BL)M$5l z=5sN7@>ezTzmp>lir?E6P8mdO3~v%NPu&izD>k~+2pW>dDm&?+?xKe{hmjZK>X8C< zFGph8VAE{O;RATVU-6Xfbu-0M&-|u8@v|N8{NA1BI+La=2k0*y0|x+oIs4G?ko@ov zoJE6m$2#Y_9mMq1NPPQ;ACIS=yqFGyHIj^HW~$i+%#0Cfz2NRecbo`oD@?@>$pPJ2 zSdOne`%3iIBKidH3lRbyks8aJyZN?6&XsWWNlstsnH(92sgc2aMpRhkX+Hx@StapR z0T^ndijw3y1xmzoXv37sn&CJpDV-Wo2@K)#4?MknVwCg@W1K@R{gp5hNJ^Rd^G|UDi;79#J56~r@|q|!X*x@u4_F;SlB4vG_W?Z#csIuy?nD7J#GZT zUiRoYEx}#Lrzh&}DR7|dJPimJ{HsN3=9e)T*#o3!FaE&~J6_S1Sx`Vv5q#z|pNTuS zZ^zI4lb?-W`1xOmU;UL|5iRO%C}ECtaD4hhNx==?l#SwDTSMZ_5cldLrlnx#lK%aJ zhlK0hdfu<+DaCK-k8W0YaPw;db zn;RASlZ{T3vYHBq9zQCPLIoVs9UPQB7#USv z##OJ0q>Fk`Z`wWlC!Yc`545J7^kA@thFDv{pt-YHvg8xpa)*jh8N2WnWC#c2?CFbf z{_Lf=c>Yvex_>MldSFCh!lC-JfWF09FLgE=Nn{<$xF+q?O%L&D2V6=0_8$N*%v=7- zIpjwN-jKe*%TtiCO`Y0hWkiNoem!DSZ@=vv)8$72d&lQd++7OVV4IP3RHS*@q~^EU zIh`@uJAA@6vDH7%iex;qFl_r)hldV-6UJvwd&&N@lWTHpw0p9h>rQU3(lGedh2vMG zwBfny%lEvr@$On*eD&&(8YOwyE55DXf{lNAx&6a5rd1Zk3WZ`SfNf|ndVOs2%R0broVzp=6B?GmoYjThSecixXmsN^WG_|^G}y4JH`o#YJqJc8)uRQR$GyD* z$9srZIKc&A?2@wr_a@)Sr#2l0Bji1hGC@Tt1RrL2*Dd^Q#?}Wu^>IM(tF;iO}U3f}}cns>)_$dWxI4wnDVLJ1x zT+U)Jod~^@Pj88EcvMqcG;kK#_xJxjmHh`5Hk&jahrxlrCxi3f%g7*GJANzoL9A^q ziVqqcC9fD!(Ac;hW-MtR%QqiloMdS9M;txYJV>cJvH5^H@ixi)lC^iEg(qp4Rp0Eu zh##c~@U3uCXX?JCk<`x4?^hWoGtHa9$nYN%P4wh#H26!o>JR=8#7!zGX zGTz&&$7bBQb3?sXHq>~NN?UIHA!oi5HZ?UBqoZT7y1FV_*^FZyM^4y=%qu+T-qy|s zVte~XC-kRQB8;FfqdvXvFQCi*e`XQtWKI zKFNC}f_LPh9^Y(g%is|t`du(PGZx?diMMyAwpd`VD^l2zMLX%7Y;VqyjL2yeKm)G| zs7o7re)d27H?mfB`655YhX-P26y?Xrb7>>HV@TyGET+4F7j7=cN;3sErSx3nNV=D9 z@5N7jj!XZ#LqR-jZ@(9Ca3nh%{^3H66ez59gJG^N%j%QSQ-$_4q&jo%OolriC9sfo z)uX3ZPx&Z`r?iYS1*Y^cw}ekQ1%x>djk6o>bcR%V;V7KWLzwe8e^)p-62jYIhAvK~ zU6}WTF943HjCNn&@0VhpRe206e@)Meis$g4of~N>6UD*6GXI>)L4)qp&)?n9d!!t^ z$AXO%c5;;C^#;?(`T=-mCeEo`ruW?Fax{?T)#!#Eq4_|Wp3d@NI~Iu99TbP}QvSE= zu`vbGaLYmY`?G1V4i8s4@k2uzB}f@_&1GAPyyZiB7!$&3#Ki%(T9?9Tip$C^%uxu^N*(0T4}JYd8E{ydy1*AffB!!d&ev6+ zG7es+Bi3?JN|i$%_??{v(K$3cB*QzIX~Dgz#=Et(nKWCs$l5EO%7}`$!j`40brex2%>Ps%< zv)(b2(Hr0W9U669Vx;@N_{jTj$J^iXx_ET7(|6<6UVQe8TXE~oo^*dde(7JV#!Ihy zdY8I_(`^Kx{O6-aNwrk)Ajn^1I`ZHcGs38>_N_F3v7=X{iq` zjN}{|kbK5Qh|inqw6CkP&)N@wP9$%o-_VlnLZ6nCJ4_15IKnB5^yu%*^jLh?cRZO> zTU>p@SrFDsJ$=R6-a#HxB#vnIrTFYkwiwQQ-Pq)iJmFf-A3yfwLrFX^e_e)n4a1v1 zJAlwI9B+1wJT*zf*o0v`%ODV5h83kn5rD#(0?=PB^j99?m+10)0md4kVTg~^Cvn=D z(c@j+xSNe7l}-(%aI)AwK+m}q2GFy_;G?i8aSs2RczWarxs;mtfq~)db>WZ}SFed4 z7TYlR7}KLC+rf=fIx%&h(tBzLKqmA6H*#AiBe0W$(|FsRGR%Q7gQ=Vwc=9o1k=jh)O-%As+ApE?eY4mG=T+=x;3 z74`7f*6fhrJC2CFP)xb#2Q+y-#ZDT%z~NAzj?N0;Ee@@W#F5d30N$|ZkFKPSG`vVz ziIy~87+CRh_rULvVG3t_Y>LfD(jz-=ne9x=cW{w^d~963#Swicowu!e*AbGgHI$~8 zoASWPbQgcAcXwoCfOqNSl-7KpW7d@&dS>}mJbG(cW0;^3@OEmCAgRmH;D|bY;+5!3 zqtozkFV}|LK^z~Kelz2scsqL0EmCs&SdXvip)NM+**+S64-NIkU;0lk#rJ$-Ch>kN zaQp6V{P<7Yh+qCSFUZ@;+X<$@)gNuCXodHb4^ZzKc_ELY%QSWK)H+r{#RHY60DRzW zw{6SYo!!FOhKbq!d{|puoL`O`*Y4y^#FPVmctbvSQjXG*qY{aZIP0%F!+ah;+1>_D zwBtjmd7P2^l5_-2I0 z?`LB#1!bxbfF(FGdW)2wy(>Fzjs)EBi0{xfId&?~CL}%L1vqe)ob>@w z85i^;&oDIfw2hRF8hE>n$A*n5Ge$QS98Rq|DZ6Tj@W>PKgur8QBpb+p=W#_VJu)22 z`qpyt9G*;!%*hF|*1`IRWk>yR5YUqR5S6%>q00(>WR&n%1W~Jg$rDlRBsLu8#qH)IYaYi_aBSj)?Sh$aeE{ zOT1r9o+KnMz55FNr|zVoQ-U66M@Ah&4!hZzQXdX~s`Iid9EaV}0fv|d8KHCRK%@>& zJTe`B@{ipY?|q7!uX|!*T%(%7u6%woVDz@OexMPOdH)mt?XCE^UtCqVN7evzrGCLr z@rH8XN%1=EiTZ#J9 zQ90uo*~h`Y*jV3+8`tl|#@cq$+QtP?kEBmkCyqK4PV{Gj_mO<)k$CK3-bd0CbJKL_ zhI28U5eOUu$Kw2AT)BKTZ#o|x8;$!fU5qh}K-Skc`_7IH z<$4GXg!3S4TN+C3(inE)^@X*Z_Ts$Bs2lWN|e9zFL4=LHaVr?8=pAN`t8Qo;8f#I#UO8!Kb0rLlE}nm1IToQ z&s(5Wl*vEvLTlno#j_G|uq~A*X$DLuMwfII#zT4CbOyFE6Xu4|7+*DR@VhrpoiVgF zOwi#Gmt{nCscY`EQQB-Y!j}&&Dtp*XFEx2%3W2JV>y=#0wB}cGr1Mw&%3qns>4Bn) z2cu0eC3vSBy;e7P0JNm1zL{2uF^I-t{+NF062@IRm&G*siBJBc7g@aN12KQz(p~j4 zuGFChDlPR%1XLt(Lq86kk&N@w8}=?L4(GX7SamI#7d^={oQMGac>AXf1-~&2IQG&h z99ee2!_h|T_`8Xc1rW3l^;mf=sV>Bq@kP~@yOt_YZ4u8%g%M` z)+OU50Y+9hr_@jVQ6D>Yh3WgL6aDel$H(HyMuhb?*BQ_J-8PWB~4(_W% z4G%dt!-yPv#4fdhq)~qWSdY-GcEmZ72k$?TSEL}LVRhcISORl0n&N}f*YT+>zP3jc7-$rU{Hq&_+1T64L1RIXlyR>SN&C`90EHt@ z;jG?ZH_xFQyn0v-?VO3gsF9A|A)ib^(4fEBVtY9Z6pGQp>6r%;A5Mq{Kw|(mbO0Wm z*cb!kbx*yS0??mn_UNVnz7t<{q?eWJLBbC~(96NWZkc2Goklc!Vj}5WSy@vrb6Iq6 zsqS91r&oMp>SD^gaFQQFGe`i!+qzFXF;AFQHk|-85pUzqc5=!y?~+f@tI<~0Sva^E zfH*~F96V&i2wl{PQ5HG}4vZbs)tu?s+EyAp=z+4Ptk{f0+o3MM>$}g#fAsG^5RX6n z=5LJzKKJUo;@O*zX!PPuF9m0&ZpHgAeK}4|+-XAY4gcf+Iv;;S&rR7X>TJGRe*u2i zEJKCEqtYkx0Pq`rILuB2UI5@g7p%kBm~NYK`>?K59?B^ooOtJTho^LLk1XUJ24EUO-ExYyLy%JNFQ`pWCExUeL9Js1x?{6Ngi&E#VH)6cyc zm#^K5S=sQ5=T64->|8wm`i;1K`)ojhV_+Wk=sA*)JbotE zMOYs>GczLP7ZQb^7$1tcW@_wfSGVJ@{PKc~m~$o5dsI0++>V>W@y$&UIUN&|o*JW# z?>2UqMY#1fuL)FsGuG1*#c;xtQTn7o7t_gS=iGmVPMAzvCo;o%lj$_9t&bY9ZC3{Q_yrXPPNoZi5s8lTTKaBhO0lFI>S z1b(}hXTW0D)iRYWUYcc@dT?fCc%cdKi4fL~HQ5^%-%Oetv@NV)(h38hl-Ku+c8q+?$KLTaU-q{!9$@@5B>lo{y6g^G$3Q zUca&vuggyH>V~gByA%J%FK@(^>o)d`@{mz)08IG^La3z-4Hqw@#nNd4aCv8om5?=DPFFLr{m z5^6S}<}c4E5K5fVIc=j1dJwjvEi8RXmKweJ($@&q?w zmKV54n`HH=^_v9zxdh8s#anuKzC;@u;wEQeP>IW?_f_kJr2pOEWJs!8#&T7QhuMX~# zY!;gWXz$3+b)TN(W3kcH+AiBDdbj*mRXmfJU8k@B@G zEV$VSeD6Ci#(i^sD`NY7Bro0Cj=%fa#UoQ&R>FZe|KMc4PzLTS-rk9gC5Fu0ao^`g z(}Ge~0D;kH$OvP+D{DXI&Ts|9UcCJBE4h8m0_++&j3lSOX-td~+z_}{Wht1Evci+XBJvkoFJ@;J9&(Ei_ zkiMYmG&^x#Lr*nI{nZrBamZaldY~6RGKEEcX!2saaZOL0OZh;L4O2~sfli^pNqV!PHaw0n~`$I6ya}D)NAjU3`o>WD?@U)ctva8-w9n7)&Q+ zVTcJo3QT{Ze+^CdD6>6^hi{HY2F;r##pCzs*}ziU>V$~VfoU}1eGKK;sjvbGner+fIi@!J@pZXl{fH}&$Y{!y!%DThP zd&}6obw|+cy#O7eBUd=x(E!jN^1y%iOq|=SIweh-j8|C2%PV9jB)>QDGpB}PYF2hZ z_Iu&(O5DD|ITW`IYyxRc&P~Se{r(Ted!BkI^KnTdXGs8dZLO@X$BnDkV`+i+-bCD8 z--#v8{^c(fE1h8t{oLsj@on#XEc)L6;E7K%5}BD8ii@*jaduk5A7!rjFt3njv@$l- zU0^3{L>_Bwm`WbT{MvRNM#Rs|XtY6^_3hqx`U=dFhwkgm`R&p1VU0$Vhj(^t@eTl0 zic8dzpJH4?TsG21u>p*vy}TPT@qFUErl)7(dw$>(>fLR{t(&*vxBVNR(CB6`&Ye4( z7ZGx^?SdFF)IXMX8zn}th1+*oy9}vACQ+?i}B<~a5ACh7EklF}Wn!H2dPl=w#B&TIL5*!W1 zSzogb{1)!S+b;Kbnd)$n&IS(}`7o_XPxI3lXm9$F$7=vt9dB5uLZjC%1i|_o{L44vPJu+V7@xk{@#Oaebrp(*{7R)a%T#Rc=XJct| zBu09#$EmRwdB$p^|i26JZjo;Q)hQlD5J}7Pk9pRvVoKpc?9J5Cw#V@=*GKk*j{w= z0vXoAnPu{S>M?B(U3EORgV74xgc-#T4)w|Ya$ZXvt~?XMRPji><ZvOqrpSvTcqaJwQVLbSRdK7Xf!0p%N`L^~%(2fM=sT2T% zjHd=*CetcirrD6S7rt{|SUAsI8jp{C?Azk=pZh{Olqa8jGNz^`)1mPB=YQhAiM~T$ zr46`8*6tw~5J)a;OF7EBSDD^r;}Hox{oHHMf{w_;D6?DQ_=to^lz@>5VO3^1Bd`|B)a0gW|zv+?~G@b8{!+h3B7-v**s`$=~}g-It4C|KtDoH#J=H^%zzhD>O7i zD>OUIz5lyQe@k>+RYU$yRR^y9I0_q^e@i@3W1IL1{koF0FrrA>B@Zu{(~G|M=plQV z%HuGXiIasg@-io1Ba(unJ^AkFpnzT;4GDdyh6GL9yC02}^&b^a-*{hC8z#s8vWD(I zC%p5CC+Tc-hwt#{jb*yGeY{Mn%*cs)r4F=4BmjBZJ$dSWG~T<|DvnYLU=-%M>aYg7)SgZN-9um1@dm3L&=aky}HA^!4z{@3H<-}dz;77s4skYVX?;q-{cd4S{bZ5W&C zi;1}bbwGP!b!9!SU%4#@u@NU`C*pT~?|b5{Ph61QWY&P=7=3wK{k2!G#EUP!l1DYp zo;@9pKmKqG$%$OMH6LI2>I=C|_=$(_i)UVZJ-+_J(4cRz& zB_{9Zp6;e`rx0woFh=D_fHre-$BGcqAleNc5BrYyaV7R{eC9KslSBDvT)BER-yrwQ zH=a`u{;l!(&wN=UD6SG|dIC_Ze@snTSO~;5LLu7m7AQN{JfBT^MjnLaqwm>J^}WCb zw%L`{(Co%XnChW=QKvLK%GYhEhw;&mek{&iJQ-j5l85h)J@#n4_`-|v=wpwhQ19No z8(;p?S7Ku5bS_+W6f#M!1%5Z~sz*=Uug3jUtZ)6GXr0WsHcXBGbvb3O#bB3<_}|cE zdBblxZ07wtb@cQO^dp}PNj{QSKU5}ZX>>S}`i(KpJUwhA5O10hT<)+s)&P(_INL^@ zun6jWGFCVKsD|MmRGfDz6$}h;yYSyoeV!3crt;it&NTpinkLZyVvK>Z<+< zEC(DWI@0J`hB5tjT8{AtVsq>Jg!5#)@0~BjfAEL?X*~Y$HPvBXBar179bre!QF!)- z^!(0)vNf_hW6#IKGrtkz19wHwo<pk_*V071w)~!WedRr|wKpE; zr*FoSXP=Ad(dCRQfZul)_BASDBzYMB(@!nLKl%C9Sl@839|_VHJOzy|)2><5WMSO8 z4UfuExJ|-)JB!qb)H7^?+fwRPVpP=U9cRbpy)xi zLQnRqlieE=V?*)oC(p-yXQ!3+Fn;6d7vuR?uH}y0aXFpok%4&Z(Fb$=h(LTv9`l2BiIDOlsPm?cmD9VKl(|C*v=ESOo#L+QD0uZNCCV)m)Gtx3SsemVV$0M zPt1<_;>JOxBmecK4LO|koC;gp>4`--6K>t4kq zhj}?%Y!=B76imVKu)q45XTBaUz4U6%0Y3lSi*f7bow$1WhDJTxarw2Y%Hgf6-~zAy z1B>b@JjrlqGHxK#D82+m<7aE48xDqmSkmZQ{_E1ruP)7sY-r{;;ou~l4Kt~6MBVal zUcVDhKmFC1zq6P_%jchcDem5xkLR9!AzpgnmAHP5d$)RGYAp7VtO)<@`FEV6}WNGDE-@z)a!%#7>}p9m}@^R1%9t|^}Ny@7h;dYTgchxYv8{Q0T>oj^aFzozzxPq~n$lL+8@^E>0ngKSQ! zUKdoK_lU;n3~xif>eAQ8ZOL--Js#yzm*i#DCsP8hTk_4nI&l?7X#TGcYEw9jk8otG zKWHONe_bbEIE<}uW`5QEP`!L))6;u4`Ui((w3g$-`4{6IZ~c-+)Yp`7RmO|gF?T60 zSlXOYCw44pm>szm=O?}qlS8*a4D@ctaQ}Kf>wBkSZTF1I>l4k}LJ8JFcNK07;Q*PP#*RE&f3SRep9uiIrEZgp)lUb=iUZr)jl z9@*6)iF`~Wle1?}NsfcDw7g=6p?sn)`qY7EOZScj89YGeWcx#>C*o9dr&fDv>p1-E zm#@SV=ceL^KJZ|?a(nfjmp0b>NTP=+jD9JP7jp}!Xzhy!>Qrw*#nULuU}Ah~YO8?W zrAz?z$nC!x@!;s%(4I1?#rCz0zaQWEv90(E|DV4V=T3X! ztPS7$@BF{x3r{c0X#KcGhIg`Vfa~cyu65I6e9V7!_{sd_>QmOym2_U})$krBnY%y!3l)0C)r4Npn+9ds9Grx}d9f zc3|$YQ%_GBb@H@EfB-V19MZa+qk4r7kcP5YY~R@W)6%2wQ@S@lGttv?Cq{<y0{Rprg;+1ope z|LSip$1nee7v5Ly`Zd~hzU?@m+a7%sZ;y^nt^Zy)&yK7BJ*g2(fk(f#7d2|JKKeH@ z0xz<;mtA`!PS5Vp5zejJ?66}!bXbFi5x>lkC5iE&{G}cPCdSG?W<;LZIzP7@3@(9TmYg62JD`&Ail+ySQGKBVms* z?S*t)@CaO<-;aOxbxz*eb7LH-OdDVh%u@edea0@4?2)Ia?13t#j1CP@K0d!!Lj|`R z_dup%TU+K4LG1=!(^NLRT_tGaB#syCKqX-~n8YQVoa7`W4X_$AP6b>(wM!2}VJ$~e z=Riv6l$ZxZr+TIOxt)0r}vS2kAJ4uQ;JD>I3j39K`4WG8!p%H6Cas~55J!m z1brgTo!N``zT@S4PAmMGjy!_ND~NgO23Q-m-csqcPk! z*3d)!ydX_l6dHXY4sN9+L_2zuGz*t9B#o)$s&eL2erk^+;ghhebIP%0)Rh0=;EZ_u zq{4k0bQ)`9pg%V#FsP?B@SJ3RRWkJZS%3i=9sFzLP0}{amMvlKh#)WUJ1OaC&~_$O zXCYS@yk{zdIwEKH2va}vQc~->Qu7K5wK{Ap3N8@{Hof} zsyi(WKhxOgv+Cn;@x3W4>z#bJzEECGcTgkJ5t#a_IumByq@Ha5Ze$~z+38VQ@R+`m zjq5W$H7J8LqwsgAkhSPNd|9DK9l?kU*8ATx5@$}^ApZ7`j>Z!Y${;o3=)j?LR5$`! z`M$5w*ntiCZyEYeNx)3+BIr>+9LQ+F{L+5>>Ss6OwQCsth|hgtGhV;!sWmpa?D2jR zRlDSbywShpJ3Lin@>_gErhxU+?VNlm>xOs4l?<&*%~aPh9V30_)dQs5qys?7vY?HV z^;|kFS)TrN=V06Q{zLir!rP$14hSXKjbtx@Hiv?Ff~H^5yx2X z7LB*&SK{WvN*)_Ia=eLa`&*AJw$oF0%2!*9?Wg6*I9Gz<;AHzhICip~ z&xGa!pmztu=sj2IEVAeHm7wieVQOdlujp!@8plx;<7x`34R$2%L6!i4Hcs*_u3dNI z6|_&W-wWD{s&#nph$Bfl9A9vJT?U0|pKRxjqUycp`GaiW@WH)*_bgq{awKg3rJUr| zVV9;K{X=Ks5C8D#n3^=cZv{U2UtfuT@G}dsy6)jMbz+TVKg65un=#rysb?z7LIyy| z5U3G{d*#p#J+9~Ro;*N9AP$)jmi$my)TG1Hce6pk(d)qYAB zJ9$J$$)%v;nX{@q1#O&+K>R-a?pAp|X|jXk(!YU&TnC>X8I04DLoqu#7%N+QadU;~ z{I31FFf$ToCp}};hV`BOSXfuv{mtG-ava*{NFF*hp^*&At%7F9^mydb-n9jDaU^97 z+u(-acMi0L3-jAkUo@g>EM>rP9x#vxXgdY+F|`G5Gqh>&%FAyW7ePbI5dLa5DK* zC-spHx`e@m|Im<)Mh~ymWv3gNkx$9cI)d}+Its^IZ;;J5o7y70oR8(j$jZiqi#k(x z^p_(Yb-%NBD0!i8q+v}GIy>!7bxR#mdicot+@^FCJm^flJ|H_!Nk>gHnI_Zc>9dkS z+ffj1BUaNdjWbtTJH#3#b{(Rf6;oI~`vT49?w`rS#K7H~8?m;?R``1x56UQh7tRys zMm3V54iP{6edF;vJ~0+EQ{PN)RhR7-)Ifa+>9+(u(#OGvMZmq^k5lj)U)hTP=^wAf zOPA%qxA$XS_1Rpzw_cW`=<>7WxWy;rTy~D%r7J+&QB<2wy=fc;%plL3>SFQ^=Y@<~ z&PSaOZ~??`Y%S+>BXTsIo&}(sqUlg0l7k_Y({CM_8ykw@{@z&F*vVa3L;C)x?EASX z%lP!UvvK~y**GyfC4J&;t<<|K@4nyl-uv?$$&yAEJG(~>AHYe*dRNKDj(Wgq42)8E z?wcErxryPN>SFKpwg>>%>+{^7z+hQ;BqjuS&q^eKhqM;w2#7uhkG^5mU9|XzCH;-jg|LVpyvnb(H0GEJVk@c z23HPcTR0OhDU&wRxy;O?Kj}1%Qv;~_Q#4flq?6i!k)K8`VEo%=2{UBnG%iz!sxn#L8C)v z)KwIMtC3w!BbsU`Y`(dS=1Z1s8r(1~n^u~HW*SIksB2&uCGgpNm&^*68+z%8OU}5Q zB7-7+JH`gZ^`H*sC+*2b$>I8r6G$2hEEn(`uMZ2e8K!*M=^>go;rmhX36VM2r+;OJzxZQ%yIJ;X18j16Ig~NF5I`0rU zi1AV0#nRAm1Z1#r4!vBpqB4%nJR}g`H?3z%kJ9z(ldxm(%9XwN(zDy~?903H^s`&> zwP!bDetuUSrTv`3^6PouNCRQiN8enZ$}A+JoEeChHxKk~Pq49WCw z#Mvziyq0epCmnS8VDnt#8Dgc`iC*M9>qP>r7A4NRN(4?gf6CkcCrITgL(6WzV>w6F@nthjAR* zmo}C`!jR@DM7)E3aB>kNpMx5C{KEhCL_Boq=r-8naCzZE{Q67p*D&;r=M}#1ZNH!) z?QPNj=4$ZFt2^-@{x=Kp(rfA&OOcir?#OUhE@P9t!+#*wSC(S9cOu67XF4)3p#IrY zmt3j);`R;o6c1zeG&`@T>wXLliQmnH`R>lP8uMnX%-@cIgW(wNn~-!_gJQ}@qsZ;$ ztpDWL$=s2bQ<%%90H%_PwUArgWUW{);yBCO# zxF?TuP)|*=DV_E771O33nP*tP^#o(es1E$#sxmtx9#se2r0b3%BG$HelO&@WM*3Vn z$w4#GFTDa7u~2XFLI)vnKpO>PYQ@{ z_x_QC_1bkj4r$nx4xn?&$jox7QOvhKwFQutILgg1edjMXqcB=;fIQ$skI)TwqTV|;vFnQ)QWh)8Bc~@jQ(HSCf~~?pITCh;q0lsR7*xg( z;5U^jWkOhlm4b1}Tb81-Jk4bxTF7Q(LH(w|W$#kISu95glrcTW^~VUOC%*lIPsKal z`k)kOIdyAz?u|GT>s}Z4;QTcL;PD5oz zBxMYDws~9NwtBi7v9-1qJDXcErk-W4If#K6x3=+vM~pteP2+*TJSskTJpuO*ZKxNO zJ*m|NIV0Yf$N8nfbRzkF6_t%VFwA8TkOO4^B`;**O&wl~fgrBdyBvKIp$9 zgdcuLAH)OFaq(T^)L-f@yYR%T%%gIM^#!2h^91B`XfExt$U`O^sIfrTVPBEV{%p_x&e)lr4n0fqF%O(eDz)-fV@ew zI#Z0q%8^tU^@bCs!A3MU4yax@9xfi_Oc;1hp6rkN9vFM!-DR~?ZFg-L}OiLg0{v}~H<3<Y3rkV{|9AJh@!Y3!j;=zljqwkM=$76YpWI>*XOe~~x7KD)q1Typ<=8y`j_Uet? zmV>FeF)nM|5fMy#F*M@p6dcdW=3#vOD!aC*gOwI!W;hIk!7$v77Y0=0%L-HRrgsWR zJPkuY*9`}^^COrhzi@)XqY%OmV@e}k-bXMoF&yuF@{xGU!w+Q7aAJ&GMn~hmb7%B9 z5-+}TSq`T^CiQ)Ny?PDMmW3s5fZvo`G!^jMjYah4^pfez3hO`DgYWQ_{GKAqM+!4K z++9#6E%eh%pH%1S+?ifEkG^>P;om}Le8Az0?aNAOku6>jK${eaNJ&A8XU(zEK>OW{t;sPXg;$x$$xTN7h24amr@!3Qc& z4Gaej-xv+*0g|^0+u7Mn$I^Ewy=fNH`2#wmJq2U1_=J~0HkeRnMZp-t!+jc%jXsOT zZe%pzt{gD()lmUR0}cFHFB^I3k7B8-fNvGl`+69!)n4-#L~(^{PL$a;|pKi ziW|3fWB%@*Mk3M>mC1QD_|mRT=|w_Q&$hmFszWY)r;cgIBs0<|AoCU0+Pz3(^P__{ zc|k~p^Xcs5wjDr@B)8*GsB8qAl>8g|Ekl4jg*-8X4_(OejhA*kESuOXesX@0Sth2m za2QN=F<4-k0%wsvKn`R;4xfEi_y`6K8yYEaWz+0b>s7n$OB;Dp;)OY1@6PDt|9s{~ ze9t>B#D^a{6JL65K0O%lFZHV37EozM8T9apZ|~XyV0e4#i&w{*)klY#H7tC?1&dOo z<@MW{mL9`nkDinvo{jgs?eTc)V-Ll}v%az=r?xaQIW;>Iqa&UkSzKOKuXQ#4%fIq> z;?`YX3%|A5FNL!ZF!#-R`Z8<3xkau=i&*M6n{@jA?-et2>OuEEk zd*YU^rel;=Qx4qb`_T6=93ItBzQI$y>{9cTFBfo*M81s?Jylv9B%2MO6WLS`8vc-naNxw5 ziI|xilk@1!-AXIDZs1OtM>lQ(E9#dg^5<5k~4Ld`gY<^&7kSZv9{X;&OcY%d0Xz+gb17Y3Y!h z1V(dwdM*tnYb(hM_#nry-8i59RW*b?8{Ym2$yK`4r{2!S+HyLQfqjjV#DCUEvT^Yz z{^sPFA>@SXbB?XK1_)6v`W`}I7L<)9m;(dCeD_T>K&Bcq)C zQ1~IE3fFT_qmSKf)ju70YIo)V%;V~XS9mwxR28^P(q@~G%v7>zZs<&&=sRVbPKAoZ zVL1^-lsMFneP}c;-De~H!FLSDTOP(aw#$jQetSPY^YoTFf%~zsc@Ure+-AIf#g3AG z$KDRTc{}R@ zfc>iVzA6u$Y3~%nhBA81_$EKU-H|~uu8(b0mCa1RVa&`9#MBf!9eZPXm^XN;^RTlY zi?ZuD4aN&NlRfc^jaH0A7${_05ZSp%uTM5)^*wQJ`jbpe&5rXvkzw`R`s4KENZzBx zyGC#hBm_X-W-r4X=F)f;)^_66+pBSNc{8pp?#A^c5kW=tP%$cqf>Q}9R}~~4rvbC6 zEa?Cb?Jc~#zFpxE@7|mjk4T?+V4$8le8UKb`9NjLd?!?wxtWQ0+Y=AV;BCj9`9%$N z*V3V!KXWoanId4_gcsZcPbuvK_nnWEbJOwa)oXEgVP51=4s)vLgop9a%PCI;T2m$k zX;4zA79#N|1>1L%7vcd1x9YyT`ya~?e21LV+vAV?zMJvGzjGsEfjBUVf`EF*qM5$6ik-w6Z-N-?;VuxVC&jy>@jh z=B@@W3OWw=o#~IaJu(#U`u~&mA7GYc*L@$lD(9SYbq9b8sdQbi&QL-dT3}UcJP#{Gji2#8CFnM~qd!|!$cU5&)&N+wo z|E+cQy|;QA5dCRapT75=v%^|zuf5U^=UlDLEf?9UnzGoei6`K51L6q= zDRXIf7=%jcYj_6XTOxXv$pS}du>2fF0dM3d#`Rof5wzq@-fQ&JC)LM4tu~&~^`M?T zA3dL zedbK|L@$gNUQu0MF2yXvhq@VroUD*|4Fv=L`QiZ4nY*-&VMRTM2miI&`)r0oAwKcF zz56|RAyZaeQ)WAxhqf+DesfLbWiYAcz)f|=yHi4m(x1?fXa1?b2=PTf1g>Ey@;kI@~#^XNOqOuk+)L-2Eo?T(bf-0Gp-9NV+2w|8xzr_4@DsobkbhV}gOU)`{m z-`uk6$~QG8FJyUF-Y$j&^+(&_kOVL76yg#v=bzJphWW6wkvmdO%5skCb5c@?L*t$atIjt$e z>kx57N?UeB$}De4oR2po0&ns_NKXv5xc72zV%64owrXp5?!f-=on4EUp~OBC?-&C( zET$b<{Tm`79m5NX*vOcO8N@-kFtE8uJTIK=CNWri5*j%DPWm5VpLr)r5!!08ryn_M z9jz@^SIfCRMY32nZDDEEl|_R*lg_qQ4fucotxIX(o}8Ie-Yq?Ev9G@ThW+lp|C)rI z@Q}x~y@}=zn0#I)qZJAQrt>hB7$Ai`=wO72hv2}OJgx}$?BD;|3-;g{Ubo7-rgx=0 zu+WaC+0_>N)1l{WeH)>>Z=X8#5ByvrQc3X+wxoamPfZ81Ob_Ilh%Ielv)8_L=P%mo zPK)SrXrF%Q%hp^YOW~fxX{lT&+c&T7+yD1>wry&j-v<&-3r^3H(lXoj`IJK|uc?vu zBHFGjSyNfJzlxRwr(kt0@6Nh%VI!nZw2S$Rb!~2;NF*3kVY0~J3uu_aUHo<{o&l6m z(Z7kel2AZ-V08HFhBdgjN7$^gR7$I)))rU)q3V0Y%NC~cvd>Gf{~Ph>xXMI-M;{`o zpjfm+7y>--qzN?&)8+2i9vPwC$0DWyd~l@q{^ zj*zW&DV(VtTbE&*=%5}K9E6xNh#}&J^C*clee$W zs)`ajHBf7{wPha4+L2};#I-1ey8%ySO{A7iw$)mH6Vn}%K{7HXSGHW?qy<(bKmVaq zFX4rh$$e%u1C_0A?f6NLI3j49rkV=(I@mkXTE{C}(V|82P2Jz>*k5HhqxX8!Fck+-RZ2F_Fe5Zyc_A-3;WS}NGu+`Gas?UY2ZLn~FHX(Yng~C|ZeXKw3QzyqPqaZnBhR_#+zE_@1Lw&6+ zEUy8H?jbONkd}k|2!WXA;6VtNP$XDt`V{_k4b@gv6>IY!lvTg6y>C-<+iL&NYN}vd zuPT=aJ8kY%%j4LzhN=}Ctba{{Z&d@BV>CW zxKGGze??B)ycHiV+xWDs!gmgAWnFZUfMO$~Po(=mgTX`a5pME+60gJnfwv?`^_6}t zmtbWMqN=LG`=mmj%Hn0cK1!%A%6NVH4R3lKSw8+M<=yaVfglllLVGa?P&f!SEPDFp z(2l7uA5>d)R#h`C%Wd??RZ=pewh5VN@}4%NEH@7Qb3=+G_O)cN19=8h67JrxU5YoAbIRkf77in2SMc@8GlcQcudj87g1E4es-i$xEF zxK<_eH(2Ij-$zoIu1zf8_t3_fxBjhMY14e zG4Bd!h*4gS8?NX2$EqBbl`?TtSJ^7yBYur$M^O--paTjgwn<5mxc8xYJj6io7@(B*sNJae239iiGdwqV0(@J-$Ts-t=GnY=eFD z11#+g9PXdQ6k8Yc@x3R+|C3X3jakJxf&9HEIyaWzv(4oz60%$N+SLa8-~P`*85gm% zc5`hdC-_UsVt$FRRT&!)%gStaY}AT&i>{fS$)GhUT0yNnfkO%j{TH8lfNsTds*egm+qCuL_R_XUS)YEmLmPl@i%Oi zjAfZImB^gg_PIfl_1E+-icA;eMK3@Ow5~cCQ7G$*}aPOB8h=p%R#8UTFak)h3E$?w8IEgf*G9-jM&eo-9kd?z2u#ZQbEDwHYu)^Gl=hub?lHhtr=txk_S zvDswU*%)Ci6iBVy>C{EnrflIY~r z0Sqb>$v{u5eeA`joe10VFfLvlvMqUD=~PSJ81~$g4_O6Ps}doD6w>2$lJ#{QMXv!Y z0^+?bEUnm@yq9GOwMBV2D{E`^TfhHxea2?N?JZ#O;(|YKA=K1XS!+k5uZr=Vy&HV` zqa*2o2db>AON^@yf98=&`}p%!@=)#_D1vXFH^yZZ_^GM<4!Hl5QgNdsU8jH-Loz5( zCc@+1w6XlI?QP9^AMEd~>snLu2lmGGCi{nlb*k#V7C|1`a&!`&JMHYJ*LLz6-ys#+4Vm9 z{~(;o$|JOeGVwVOUF>&)ws^lT(DWS^sB5gY`X=_3JY)x=?f%Y)Xn_YMR$&=$Hjceb zXrcB{g0z!_BKP30B+S#83YfSb!W`UEsFnmAWXj_a%8ju}U4BtcDHKV}d!}|g-BTxt zI%pGGagk^v<%u@nH~o2?jN?$WNeM(7w2jcu?(vtwM#|S8`3w6flxix&v#6gsfyHH% z{v4k)HkAtNWZsu|-3RScUHLkkGWzY`^#eSjF2)T?H~P)t$!M40aC+!|Qf5c{GmGvN zD_@l=F87vH7yTbf!95J+sn91A@$lN(mOPPdCjVIsz^Xvit)fiCd)d9ZcH5|IwxViT zg0-@mbk)>UTVr*(Uz`fbW<#_O*WAjMuiWC=)}X<)0Z}{`R(rKKRC+mXLSzV(5TMhr z*nA*>sLzowkhvH4$xjFjO9U$oCXS_+2n*`r3Pdy3p@)JvBz^c3fZ|z}cwSysDuL8s zyF#cS{lSup$m&BOpn>+v~;qJ(26 zsJBiJ^Gf7kU!JFw2G`!!ob7B*`nAt{O$r7gp-Xy`(3bavcaeHn%;$qfdHfE)+9?CMwWeZAN*wbcAw>;@7VKc#y~vNm1T)Cf(;r7 zk0QsV#Cff{`j&X{y6|T%L^y0n;pCY}3Hrx{z;U6YyR=2S-ja`JZ^eHHOnlG!QkQZ& zY?R@Te6DP=y8V6zdsd6*bBOlb}M@QPk#IGy=%AZ_&~35c&{qgIzf8!M-hHo~!Jhtdshg5iI zCcbgQZw{NxEKl$P#c5`@Bf@{r{g}*~Z7ZdIn4TQK3MQ^;wmBj^L3e4V; zw4w_dMA(VBg-jT$-+6Bf$DBZ`QY_ajpv+rBe?+xMO{ zwKP~~Hyb2NWu@=i%-EVAm2tmB-MJ%7l!#>$8sGpzgf;_iWpT$gR<~?Zif>N?kBxTT zmkekLmeq}U=UIMNGqEoiYAVIoQh=e{GG6wOhwf1d>s|@dD})k@RyZ6I0UO7%01M-$ z|5%4`3jSQ4m&dGY?>=Yfk;1sc!v{VIHnqbi@IrH^o9?;SmMO$Y5!wO{k{g0<*{hGl zZ{QJDITmY-XWF=Tp9vJnh45%gb&{wnS@Qv3zD#QKe(Bx`{1p01c!oU5FWF6rI{jG& zXFQL3Xp=f9kFY0SzRut;%F!O*gANGksFQr$rw=!G);xR#eeu9Z(29DYaiUFlm4Rn? z%;Y^P*P_j%p|3*PfCps-sy~Ci2|mgaGOpxgaCI~D+9(bVZcOhZNvZIN6rxjjEU1Et zsv7${|NY;xAN%Z&*y7@%mHf<4{m@Gscfi{8<&}s%bIEYAC9Af%of5l!XTmog@+#g_ zo%Pn&QfocUHFAAQeDa0a`<1O7_gF}a8wYlEf{yf`0w6jDM z{O<`ti-0IY*k~|f*`-d)UnnX%RhEMadWVUaUcW9#PuD%;cmgZ(Ldd|& z>`7?jrusU!;CPK>nv`EyUU6ZCq9E+WN|~6fUy-obvbvfYc^^`8mGUxFFE7H!%i)DD zOq^F#%1e;K+}=2pH7g+|YjAJZhpNLJIOLNvh8QRC1DCLL*jdaHNOt0?TDx|8*WMjs z8Dy!1(NQLgq27&DC3Kf82Jrrv37L z4&Nh9F4ou^H#=>9At{20ME|0EV8XFdDvA}DtJqa;>Z(1Kh+iWv5Jbr z8B3Y0&I>f^ViKM~1ia1z0-FICibR(SlMpBjU@SidRBc_AE0Bi9I_Cw#JoF8d&g`2Y zqLB;l((CPn_5Exu5ZxAXmBj@S5Wz zC6DrE%w+t>eM$;y|4s^i3=fKY@jNB!<_`C`pb>QqQ%bc0kN)GE?gb1ahr#O% z#ZDcRPckm?Irrc}j34h)eW%@ko9D^Eh`ipOl2cLNsXyS2I-}l@Ygw+`^C$|Ok@9u% zJ?crc2Ib#=7cCqDWy`{XA+ zY0p0MtUdYo<5p8$Z3_zvR`M5r{L?Ri)ZD_7P0!BT+Qycx3*W7ZMq4dj|M!7h7NZPUm{Wndn|ck z-WBESC7_}pTw*FxbVv{?SARAzJ&%xwfcH}*ghq&;Z%B#KnM}C%cefjF2BQC`-gctsJ2#b(dFkoTY-(9u!#kXu{k?3ABRltmfi*U1_VoI16tKFQmM@%4%h)b0RVm=@Al*C6d1BP6Lzo zbU;hGn)A8<9mT=Ya^@uv%vgf(gTHrQzbW6^-fSIR@=P0QoVF{n+SgZiT@es&%?pVT zGARKw{YHZ^X)X>tln1ZAVgCgRiNgo0Ys-wo4TUMC+`HxzB^bEG5?}TRxbkozL%53= zG)g2cj?QP#jMGXrxB!+_2;WCJ)myZ0ojonq(%N9P_0{rtW!T9pg4YiAfH&hc(MC|K zmV9zv)3ZWrHk2Pm8^S3ssGx&;8_EaXSjm)kG02$IUnqVkS5Lf+P>kdY+9f*EF9opbJ9Q!t@-XC;LX?U8`p@f& zi*^E!fGzqf^2V`OJ*|?7%@x)pUgaDu+8JNkwDILF$?#pjwlL_kxsv09gZ8sO^KU%u`QUOH;G0tgP7V?5q{N@|Az9_T-Yw8`<36w!!`$5dfgf-oA9jHr6+-rM^ar zLQF1eyXgA2ZEAkmF5X%2lYrU82>e}giMkr&eI)PP*|LA|r8&7wfe;^V^m2@(fP@cJN>KtbE%xx}C) z(JhfEL%LsR>+blJdk-jsnK{CB`_`jp5E(1u&cw7oCozY@1U+;)b7H_AI(xzv7FX0Z zE6V5~;U}f^#??EqcShdU#K^jBtie1eMd7Zz39^I>c>3Ox?0&fB(b<~v03}fi+8NPcE?`5Qg6TXTm3dVi5InPYs*U>(yFZHt!u3Q z5}kz)ieyh-UcD4a92Xx0lz!RW-*Af^`Yf-_=yx2B$VKvjdGdSjd0nn zV1|aIzQGQwm!O4~8_TlT*CZTN4fV52JmsUl6i$H0Qd@59s4RWveWU+fkn0 zsFe5YV|Pxpl1D_^;h@L;r07*zjrd*&!H^ebQD@MaC-7Q?O1OtWjxZDXLaFea@=gK2 z7dTQjg<^tsl+TAoNZfZkm5*QKNt4$Reni1UzLWx{D3j}N{?sloL2LSsoB?wPa#Fw) zUR=;KgnKBe>pt>B3nS1 z7hzQv-U-zI{QCRCV3jJt)pDy z5*bCaBAZ{Xu=NcDYUF|LK4~t&wg){;ic{7*JKLFuioOBEEPwXt1BgrXc57OF&+<-q z1c8|1;7$ke5{Y|wEIv?l-<7Nc1jpgvb#PG@<3u32SD5P?(Fh*oe^Njc&;Uo_K&DOq zrVqYjG{xXWSOlN35{fwqKT1a)4g#JFn98e4tg5zL$}}eDOEfTnGlX>r4RA>9!pF4B zYAlXe=QQ5h`YM%YWn+emE|&9x3m14Ko(WhTa{o=;K8AP#{ga2{_;?Qg5zi>Y#j~hK z&?!?UaI#tUPg8Cf4~`ca^-S&g1$MJvo<|tG>Y=1sNesx zb}5%@Tc0GJGnPo9AOk-Co&7ogW$p3|`Bey4f-~AtOZtcMi8=+f?^YU3fuTAxU2mkZj<*#7f6MRfXuz!S$!*{8Y zd62G-PJ7_odF$`&a3`X(t176^_Kzw8b8fMdpO07~&lyQWC;(Wo;uiDyqKf$_ndfYj&%J z`H@;F6_)zGd;PZEmB%tEPiA6%L)VH;&aT*$Jh7F99a~wFj)l0hUgSqW(B_T12llNCJN759Zd#3m)~NxG5P>J5 z(SJN`EM2zs#S6Bzdeio{XVqLD-jrarp;5D`Io@wA&qac4W2@XQ+~~36J?r-1$we{S zzTKTEwQpRgw3%snotvB1SlMAkQmE@2OI9p5Ie|n!ABu_nc$Ca)f^xu&av;o=UQShB z9xt!*5DPq@!BZm-q_MWsYW3;+jWXfLu_Aqg?bhDaBtxh{xJmKN1)T^%(FS~z=qc(8 zwvF-eDcY>=+p5|ut0=bm#tLg_5)M)%&ZEi$ zPoT&g9?{ml8GS_nxj@3RlGj?-P+{GDt-f@;s-nu)77jdIy0%34F7U|14h-M|=OkW> zEAY&4)#C&Q`I)!jIke@IXOtm4$aDIK8sRNgEmNlqU~)c?OO#X7hC>m=fHS z%hA#6j*C2zA#jQESsnUBfrK|m-ykq!tdK8xE@|A+CUlPHqPmO|hx0ydCVWEh3Loz$ z=a}4YuTy#XzO}RAdqJ2`r{4nCFaUG?3`}a%39pp+Jtc2DYu9nnZ)AddZ~9KTJZ+|Rn{)Z-qNKI1-KEvp7u=pdZS)i;u_C=s3$zT*1i$3AX<;m3c{s%_JnYAWsF z$DXpr=2k%T?vI|)7?a9Jjp4;~mMb}ogZGR_N zExZ@uM4d34;}neHOropsIrj;raG%W1D2@-zAFe-jg6~nr31-Ya-zzdUnQcmVaqRZB z(OvtRl*;t{z76&;-voR|)B4gy+gcg2?e$Sv&a-O!{gd)2i}7hQ=}jq8l{xC5+*gr| zOcdFdzb#lcIO0U!Y%wPypx7$q<;G!#=EZ+H)%70AtoU*o#Icsb=w3@mq(W1ze?Ar3Sdj-G@t-vqW zH~Jp;@_lpUIm>A?eiJw8p88E@5q-eD=+_Yb-YJ`{vDin`-HoD8)e)%g0ju8%n`ySpE^hUPMB>(pRwVF5^07!4~s>R(xuJ8A%0X?)RM z9$w+$SAlRZcn+><`~WofoR_=|_ykU10v&R^mU|v;`X@*$QHR&_Z2ekkjm;I;+geq-mDDy`V_mg1%N44Zb3Ai`VfUSeY&qNwEutjg?)$@M<8i zx9NFffCX$YZ-V>i^LQ4pQr;Ove<`Ip{h9vKAWA7ccR?j3!gX_a+g^TS(_VRJ+b&(( zG2X|pu}Sz&%!(JFfxxx9IVFKR>WXB4FTW22{zT_<(fv!SW(&)4N$>tg69xkt21~8n zDodC%c=O(L>hg>ZojpwnsvZub0XRHB4&pYjbPk4IW`lGc(|9hW9=fp>R z0#yd7p2TZ0 ziXuh*LC5<&_e8w~ZSohq%x5YaLd~BieDo{TSqQVk<@n_L4S4gkkN=}ign7VO_?(n` zo`W}o$**`0y?9P3U=Fy$fMqE+SHSxI6tEwqMQStZ$-@%xgIBbaXp>LW<2{%wfAo2l zl#OT(bEjoQu#5L4=ZdVUp-j#JD+XCPSn6{fwY8kLRcmL@oV5?W@S;8U>~mHpM`3LE zhOMmRB8r4U;#twP*Ze_}4^mV&JLFkf*XEQI5DnhALR+h`M zud%l;UA5KKbvtwNn0qj68f$8(w<}l2?4SJdANg)y2*MF5C=lGNiaLbTj$EQeAKWMq z!V}P%Hmc}W=lX5k(Ep{TG3=tQ4{%m^AQdqNnb#AN%izIOZc z_dhS1pCWB!*@C4R-nTyF}7!(GC1z<8(@L+|YR5_@U`-C(2OexV04uQD{ z+uiT^{n{i|*OggoSB)(8Vr#73wjX`|E$eRGu;aZZFD1)#PnwnYF*e2W=cYJOyHxZka ziOJ)%9{_sC^OVxwg2FZltHJj7zK^63Cd#C;3D2X?*_UwieILk<&AHYMd5QHDNQGZJJYZKUkGp%R}eu@xWp^)G{buz}1usr%atbaVp z`|(;V;SVnvp~&gO(Fcslqje_Q1)NNL2Mtn)E2U_YBVqFV?Qp6*b-8E-H+?UJ2^b<@ z$b%Rkd4{H3EZ}*6n<+s@;3sWvulk;yd-L)8-ty5mgw~=D{9Z;o?-Su4JcXi{^m5pv zJTL=$s*~?twm_~GwwazgeRYooJhIQMt-VG{B36^O$*^k3_J`aa6AtAq_P74d|I0q} z!=Lthq|28s*f0One`GfM#7nOH44+;SP{4*oN<1Smh`SpFGlc5?Y857{5^}guqzQ#66bEGGSjSYyZ@# zZaY5MX~QEEo|k%=$oDthiGbc7uDOQ_s9Ve>{RLr}P-fzXczR4&hjw=BL!H4VL~~+N zPJevzjLu1&M;k2vZEC2o)5rUzfP$EdGO^3`oeA}IDN2_6UcPqI>dOwS{BX zJoLVb=V<^!lN^n*defD{5e%Y$ET5cK>KkC&+1j_IrG5MID@FG8x9Y64qQQFGwym-} z-09V|@H}Ks_;+=?Cx`iar2B=o#-=OmTW>em+GgP7C}*%sl&F6QcQRqjaph9L`*gK-Ri8}qBKMNW_>!4-y z3n}qTzGRf8T);{47W`Z4FJvC$3Ou4M#=hE-cY=2=TVJx7c8jdKy4-rYn(c6J-Nx_S zb|p@j>dK`zY-M)J0CPZ$zvcMgsq@y>*(}Nms z5*~-g%(A%<0K+Sxay$}NcCnkCig?Kmih>XrFNJDosLQ;R3agiw!iK(<=6ZYlv6J@r zqbKAt_1YhP@l|;&!Bk-(@E(B*At`c5uJf*lWJ%Buw8JDk?mrjxCjv%zER;b~BtbN+ z9pZ=4S#E)5opnD&!afqdbJFh@zW7xuUmdlLjScH-skEn$H`oK+HP%^uXvdmMYC?rl`Q=OYil}GpJx@`C*4<(Oi7VQS*>lvl4fA^eH8M_cyl2GvqU_!wYgzi>#J5@ z-ewi0oRbxUf&@L1CB|Uh*?m}6;a|7*t~ca~e8HNUU)4Yuk(bDZ$xMj|_QL*w9PkNr ziNSlcZy1cUSJ-c)G&mDI3(L_5^qs?}dp`LO|9T%|(Fe?)SI=qJ>&oy-$qzKN68FA0 z)^==D{j;;lJQo`z?^PmEp9_m@tk8DhGp;0%_#V#?RvDjV_%OyIfBrcM?|||MBievA zflt1E>Lk5Sk*HGg;w}3QZjR7bpSig6GNQfL6K#53`b!=X-{1FKpMu8gPPizWx0~=G zaX%?_mjn6>c++Hiw2?gbgiEx6GK>10S+W#)l8IT(? zOzLV%tf{`t8uYolvn(YtWWz)6T60^cWOuFIzVfy$%}rWGWtE*c`>^hJ`Gh+2AY6P$ zfy~d(`zNy6J(X{L@z)Mf6m6}|9m7|Mx zc~$J?aJ?XgEzZFx-tJKe%fTpFWiG_ag60 z_X};^ni{Y_e(NK)w9zQ*ddJ>+<+`n|@QwvSol+kAo3_5YVAW*}R#jT>umK}|76t+| z$t%Xr{ss+*1{H6W(wMistyQNBa|M2gsQw|`DC+}Sb$JLyJV_l05O6N6GeS}fX7w5E z>5t@as2@nFPJ!nzQjZEn-@t2G{Un}8=!t$is??)S`X=KWr6h!sKqJLxd{6ZUKV&cp z9;jx`bfaS_=pMl{Qbf@sSkWmf&iK57EPq@p^SO%!dv~4t3y{PhLAr_*Nhja zpMu<{c=Tr~8%CtpnzciUKFsh@zwteIkg#)XVx?YQ(e3w@`Z_DEp|;e_A2+XW7-) z*IRdYw<`=13I)$(bad3ecXoDKF<~vjSHO-NgvSB!%z4lN%)ruD2!Y_mQ*p0ECMwHj zV+VRz*eDC;S-948p-{*_(BEOby=~Us-e|{V@UmDImo>Oeo`*bzb{P&Idt|`gxzcX$+^Dj<^1@bE zImOnS`@fZF2j1XBT=onV(grVy`G?Y?^0@Syeh216gQDWYIBKMd)g@S143~9%j@rTQ zk{cX5TdG^eN;;b>CF%%T$9<>AQBvl9Ca|DWj-IqtNPCr!I`cG-eo&q)8~IZi>U6qi z-}3cleUr<_U&L-`~%$JlJN02R0^LE-jomCEG$d9x9)orIl(=?2MzRJ?sqQbXdi8) zD{kvQie)?sd__epeP_$tFEs0%NJj%4wu)T`q&3w zVxFLy)l2d|Sb2m(-~~^;U2WFXSnr+)$EVyL8S|atZ1lnl;rt{B(yEJnTtf z!`{;JnoqnpHP(4~<{>7h=j_ve?pgcvr=GS?e*6iWon5h!kvX4ppk1uX>M8*#U@NL2 z3^dpMy%@A^nL%JkB7{A5smmEse;}2fCs&Wpww5}pud60_Nc`vyu4<+u@zzu5iP$By<+m9RnjS01jo0br2R6a64lP7FYqx zi|5npYmQ}%2!C+kSP=4*Ruo%zf0MOz)LLUpward!*y8M_tuEqX zhe1$?5A;m*La`$d3hC`uq3XwKbIVojx^O^i4T0&`wi@-tzOAk!ONy+yq0Fz=#xm<{ud_PI zzOmUQ8=25plxH%j@3J9XzB4_sv9)8vW7EQa$BrNClX16e!#6J4#n-=S7hn5^f3~%E z*vYdG3Qvs@DIb&wQvt|CR(+Lx?E9X3iKBzr!Ol`wlmc1`;ZFw<*5Vm+C(KnKi1}o0 zVOh$9XVulQG6xGEB|wM_0GTfVF;>xV{0faOudMn`dd{O_BCET*(FO-weKpnM0xx!f zz=%c-7Rf=zgdf6(;1*MP4~n=j92pVDsRL0e5E`Phstd+LK()5kim-L|;OXPAhr^Dd zq$~>MP)v6D#;BMQfxs#<1|~VZ37cFgF@dH8a|Xl2000&|IR+;J2snU0nAQLE2~)CD zp8LQb7{DO@KUH`>;HGY3=gyeAkHDl%xc@P@JXWi+u|g*5P0V_4n?87z*PAX#v;(|x zhEHZi`r(7-@%eRm_H`}g*3#Bu8{2i(T(=>b#(=&z-A|E>&s5s0@3h$Z7U6`1M|ilX zKyOcQg+K{mQI3VxBK~Qt@;63uqzGqVZLn`ba0#QpM=8-hC4?Ty2cTP#nIKIe18AM$ z%ySPXI?X}}M0KiFii1Sn=$n{(L*ej?`dC;3?6d{lsWbQjL5`4#IuQ=k;rRmVBW*?= zc;+ZB)Wb8%xPd}{Dxb&Uy$rY{fs~)4iGB|p9hdBnd*H!mf+6UiuaiU_eUxy53Oq|Z3^xZZ*)v@z_FUb5 zA6)2%7$Zpu1&*14k?Sw+MV)vc;6r#QPACpqL2K$wa0V{G5O?_wyiq6ZguqWYQMbdF zm2=AIcLx2D?y#rrz;*?Dm1I_5 zXM>$S)#)n$*SA?gT4Zgljn*xrzoWHTGQHAPCHsg%@wV6QjusgN+jimVh(CMaWWRN{ zH`}&kMEC|Yg({L9g;87lU zC#@|_PF%bbE|1m91>$8;>~=@7;3l!h0>uJRP$GCR?2_jbmj^{e8>-BKpPgT{F?NMd z&ANbp?6G6^@FN4(-=Dp>Nc@Qc*_RiBC&J}NZ4`JPklcx1aB-747nBWyk4b8l)^gFI zz=9_wFg)zGvbye_h9ddVANY`+edsZ}IJRjQ@2=VHnN6Ep+qKtjN#SiE=nzh7SM5;O zy@`8tI>`qD!qL5zxL~3LBQ)inGp%l5wejf!L-%}$(2oAdK|Q(heoueYsk-7`J~>n0 z(Y6Iu1K~Ij6bT*(*Wm#I0mUge?$ZY(>UX8+!i|BN@2@mygN7lX zQ$ILN;(ITz`ymv9ehxqPREA3ZURDnS2->;8(B*h~d4wJL@kgxVU0U&bGYegRxek2=6Ggp0=o++!5)1PN-gq6ozz2HB*%yHelK{9|6)!uj^K}c zv=K__eu?tVt4c0lqoRUTfCs!#fQMg#C(+g+gchQ?zW6=I)7uaJjq#aq58CNQ?p|Sk z@rhFOJs{A{X%Mt=VD6K=O|=m`5PaqBINZ_ily!Ks=UygjKc%2qzE2`AaK`V#i#jTPvt)<%ae``(hm z#ELFnnNak~SAOG=kP{mZ@kW923Q<4hzN^l=Wp!4tuD#~h{DbjNWD&?7Muc;=?P-M?O@sK=|WA2`zP-Z!2Ot|8O z@%@j#{vEr1Yt+uRSJ*Sh>+M*3wSDQb+09v5G2$_|a&@8H;0>SnCcdi9W}YF@1osn| zlUGC;3czsrPzqtN{40SAQ(80c`%Kial%XyL;pb^&KGbIKVFohSB38<{jLK-Gx0(yZnzcEr`K*w`nV&6_0@T7a& zgm#6`Q=)D9JJBaXMhKasa1^ElbK+xCgo=*tU7^1V`xyAsyGOt?=ob7Qutj(j-imvL zbjY`fFioMUdsB{A^&I};!VqJ*D+dr#&(S2BI)CQ-hI;cbsck*yp1%TF{T9#DLIm}P z(<*SMUp$ZgXq)!<4LtN=9?$&Pr>*cpkK&X1(CgF{^osl&+Z@}(qJVrKkWf&x39VD2 z{nVe-8PAjOPtk|(o}dl1PkfP2u|NR+BqjbOg+ki1UG=xnw%W$ZnA_Mtv`raz&DG_0 zqOaC!t2KtCC^+tYV>dtEc|q0I-o8Ee*janz^ne@rLn9M*>&}!teD;`C$?N#m+t+M- zdcpI9%Oht7?88r;m#5QccP8iTtFK(LvB_C$s;jaOJbKm!y4$1&*wj31H%FO62^#Ui z8C_@2K4?Gs+0R+YU-+?4zl0Fy94TJo$?2qMDiYoV32jBAT!e-2ESP|2WftBDVK9U; ziigvKW93#TBF^q2&#F8gR)bL&r~gtO!A_l*-n*ePW!BZ%=7%XV(a*$wcUOz`_O{r} zEUPGjU@#R76ih@4%h4HA&)oXb-O4g{16Q~(K=BXH}`CKuGrSL#pp1Xm^0c5SYQ+wjCK=4@Y@F&Za9C? zPJb|}3lcS&%JS3i>xz2yhvkF?tnbc*gj-a`-D$pLZ8)yfktND{JNip025goxCQu>2 z2VB5OUEIjQsNXyTSN{zjYTNfZZ0*a!*|(L=YMWiIv+Ex_>1f+M2Gk zSKev1mG!(~qM_p7D{D)vs)k+Noc1i?#nCRR*WcB$L;DV;5?}JT1kFVM>=qx62bbWb zoa4e1H{9?i`-$=dP81kz7AmHd!B}TPj&0A z|L?G6$#D@~a|qG4m(eZ#m+A?4B1iB=lqKc*&NDu4Tu!W;gaa#De8Ac;`iddf75TrA8h!+e zlsGY+203hYb$!dG=HhkHKvkxCTN>-+;ncWnZmg@Z2l#u8oo0w!N2G89jv;cc|4Lt zJOnsOfZucTi>_GkW&n!SUxWSK?p>ff2x;+()8Wwxo0y)piOCro=^rYs@sJvcxes6Q2(8 ze!w5^$ll#4v8|0_+uSG;u~uwicG5=1CS7T<#JRn-MP5{e&CD(NqJUja)twx-+6uD^ zqpP;ECB=9k3#hq#%ndmOzIU$l!t{DDQ)T z96kuMfvUfhU^?>ULlXfL-xITvl7fwa%RtKpYDPQeCn8@gCrxuQsSL}r_tm!;avQ5V z{)We;-3}X_>#*BXZFb>WmtDQxF2!GJ4@rr9H${R~P5%IMN4FTct;SvO9o5Te0UT_| zN-|;Lc!Ed3%m58qCYW+~3SMvlC+<1z1TWS%csm_MSCT6dB|Hi0VIImo0sZm2u+4Pw z+(VS~jniKJrTPo&OnIpzhN|~;z(Fa$pTVyD9-h%3DavMj<3)oy~9&vCoA%>=Oq}X*SKi6Q z%z_Q}blBq$oN%SlF7Ji;mD7XW*4-*NwwA2un}77HhX_*uLRg~^c%L|v>USh;Q6}9| z6hR#GiE@MuN`topa-V!00}oJ6XwPX!OkdK;wO*_~fuCkJ{R2j+49!&kp*XWNSHy?G6Q;x=1YB$g_MQ#l3* z%L8E$QV6Ixq!tPNf(M4++f5ALuqi!W1hcA?%W8r+EI&wHddg_PXKnx9or>LW^tHzq@ zDs60f-4<73?#V;g89a|X&|?psm*S|emfS4!P*-hrrDO&3)!ROKzO=ewm#&Qn?>#$n zyjMotybX`dm>-5J`HfdXpAB}m+H;Scaisv6nI8eiHp%~YZ;aUG>mz>p`Kkt#;x>xApWiTYGzhb#*o6UQ`6EwBz227?+DKqaE-`j6lb2$?DzM z*pqj%Z!5B}XXjU?ycTUj7cLz*-W(mbiqZp{SYks@n=3HdNdqPlpM@7GeUbzL1Sm)` z5E0aZg}S`Hd>*fxDpU@JjRAysBri|FB`&Jwd1CyiEAn|cZ=1mq_wul1b@SElN22aT zM?U?Y>P#t~N1JhcM}(pd)bB-x^}TCro5eP@P-PQyHMX!?YllS*R#Mh%rR9xQR#xeo zTT#@zQX<=%90{@?$IWCgGZ53{Xf!i-&%Habpfe(-do}|J$1D{l9_B8*TD)rqj zXqC|`|2!*CKK%hj2z>YfAw}M-41bhU9?Heg7FzOKL>#>hv_$s<6qpe#|0C>0m@_rpg;kG7H$cgY`Rj?gmQ z*Z1^1QuJx`jmkT|@jTs&KFI1~{BsG`3h9^nni3i1GK9-9EQ@7OS6XXJtzTW8jW*EN zVuSsy*3s4=CBjM28b`W_{P243+Nuhd!)i>zUPh~AD<_f6FRw{C@jidrI+S;|XG?3F z@;2ioeT26N^%K&Xnq9PoTa((9;*ztmBO@Of&+9kVrA%CJ z=qh^SYrlOsbbHjpRm9~8E#Wq?Uj!l$o?;g}8jW=7c)t`6akFKgE5R~bR|ABv;c6)fl9%wJJ zTeCa%i@)#}?K`iI*`+IY1>c6}PZmu|SZ^>dlkK4>@}Vsf`MH?H4u;CCj?8l@OkO|M z9?Yjd+}(VcIB=2}HhVD$kHuR9^KBt4s5=BiSak?KJdSwl9`BvtFv57A7@hUtJMV)z==%bkU29G~Wn}w6qd_vo> zsKa6{dWyx5Cv1`MLl8=kVqI+awiEwgdC}=AzT|-bDXI`GP*k_|f~$+r)t6 z9(dq+1nwm21`lw9e!O!(?uRExzo(S&;2!1OLoMi|9Bq`(>(pQ12_5qFfQQe26wred zeG4pwbq2pgoq=Zxh0-=K71qz#PB29{533wFqbQ?f8=*DfkduL2a@X%(~ zwrxR1;ImBXpBeBvOMdnzf9NHIzfbN+tBp_1==Py&JNAdL>G0Obn1`;IN9pbE5TOoi zX=T;pcDPeM;a*eiN2c)REtC?T1m2GCoRpR$M24ckHG&Z7SPQ|bjwp7oXic6C$WcFx zOL^Zlspohs)a9EerCeAU#wWYxAAR_QG2!{ou zBK#w-1SbmwHgv7YUDNN9nihYxg_l%^*MSluxq<*UeTR{f%kME& zXCM@jAiJj?F8$|q09!C~cnP6&vV8i4d1Fu(Ja?p^CwVEaAJhjRlmY>srw~H+obLe> zKQlO#p8-a_`3s9UTzj6U6oOObWw!Iy#?6gg+g{(doy|CSa7!0njH9NusEc~QBR?Ry z7cix^0=9g}hZL>StI+TM&x1B4`kv8CSL7?i1&=t|2z>L&=_!0z>dGn~t`LsFmrg{% znfi#jNInU#HpvrhmEohm_u(U4Xb*Z)Php}w zrQ!RXUEZG|V=Tjm{LvO5@IAv{S03hoHxE@I9{K|~;G;f`|I|J_fx!lRo}r*WQrr6C z^^w3SS^x&dz1q+{>LWk*{P#WeAe^a-@#VESv6Rcp&=-MEKpwwy{D05A)TdFN@Kl~R zoaWHN%Vo6jdNY~ezbUUT*>mb}_%fN~bvSNe7;t~L{$Ic#; zvTBh`X8*~ab+k12L^E@Bn=oIztlg9vb7SEY?OQUs5q zucylqoLg9SCE~|m=wbt6b7O-eho^zS=R%2~pg5j{$!&k%iRx$WB$z+uRtS}?tgX4m z^8qDTk`k&Hp_pIc>@GYMHqW5cIy;)}{Mi9Jcc#zE<*~f{?TcP79T%M)iPu9CM%&sB zp};_bVX+ACVxqHIkwomCN!m&)RvXQ7>)1FsKPQ30TlEBMFm6~b=}O7rR=Y5W-xmlG zONQBCs=Oj$z&WE9)RU{rnK5WncokYgAuNwRPvr}rMH@j=1`Tux!Ia8G>=#-vXcBJZ z15OuGS>8lb>Wl#xDR7cCEJoXs64~02aUqLkcPn1zw3{iBXglg6`M`I$Xe)$7t}XpZ zH018_Npd>z=jEd9T%D?4HmJTMSebhY%aAf@;HJD@r~ZOA`9363mb@Dr*0-}EzS{sq zZ!2Ae^>BGT&`y8x{3t%$CnS>khG&im<>Fh`cEFgPLuWh*+6R}^|Bg#RKa-zKslN*o z^9jJl&h0JXf*0$!^Ox}?Jxf{dBkv0dfA8-1_kjwz5>EG=FVtjGwkZLlD-wNrf2yt2 zAE_TB=~Hz^yFn}O1Nc;bDZkH)sI8zwz8?PklX**h0vB|Gx1C0`Prcq8V>IAqtR`CJ zanY69%=$B>C=+>Z>crbaokC{HA(K2 z${0tk-WZ*f$8g6+C+B=Vf+f1m4Yk%LWx%^aDyyobbYcZnPkXa3*IioMu&Ft=H5}TR z!ESr*(K9}|PJQ!|XUsb{stgL?_V|qFd-%+tb+$FyyEpIJyF+)q9WE5etYkg&Eaepy zc6_i`eP3!biz_y$wwR)L{oPwu^z}dZl|#Zsyr>E<yrSxSuOf|s!irIdeN&zJR__nn|%@M6ZL=~#x# zTNq&wI<>8($=xb`;|bjzo3ysprYzjn=3WE&d}Wvv8VZQI2zT-R6P6?szx$v6*Z)iK z#6faQ#M8kr#ok_Y?CQpxe!=u&8@1(s$`f-e<@TA+ecT$ z6hu7;bKun7EF_ve%b}^h(I0(7?Lp5_+$j_nbjXLK(g_A6Ur{BP0nPh3t3Qut(2JCw z({EnJTZ~j#C;KM25QvmPITZF=B+3TgBpx777#8AE-AjtV`8EfK?x;>^=Yl<>cM_7+ zk-}-v9=;&?O0t)tue6vjfnTCgekmsI{58=>WS79Bxn0!xt zO9-FywC2}$ZF~PvFyNgZ%81^#b;-yFPWIWur~9q0rb_JwZseJoU-Eq>qZ4y3Gb+nU zADe*a=Vk z4F;Z|m6dhX zMaR;$A&fZ`5O0Au^vTX}Wer!--j6@DsSQM|=bwg3f-=;SAaUA40$toA5*^99iKf z8U%cqcjf{m+6sDP@cEU8BU0c(KDX|bz$4L#fm-CAVJM4)jzjG@@thC-OAqsHQ>Rln z!zbVj0h$D|9~qP>E@~*GciiKmJ#Q%Jm%*Lzi29*zD4RSi6bo1qUg=)o8~G9+>1MQ* z75DlxSR5bqdB7AIQr&sn;I%|MJo1gG)XM>jO?4<+NZhoT+o!Mp{~0b?d)X zf|-R$@HukQYt|<+mPJe$2F2>%GR_37t+E!$)JAz3H8S=qB?J9XMH%x|)$&&KS+6`y zpd%+YBnJZ?UepEO$L{yV)peVgp7&#~ibT(<@)E1@!yhYru~bh-oKwi;J(KuC*eXSu za?ur;U-bH){Py8{*KXNyX~6cjm>_5QE1tl`_pZC5`2H84b>bo9*U%AeUQ{oiiK6xs56DT3}mY}E}ID10MB?jBI8^apR`}W|ulimht zcy!Fmb_yBha6lHKLk-}4=z)_`grQw;4&Su}b;hF)pUEk+zxm(&bGv+H#J!oYegsQp z7IHcy%oZ>10xtIilm{h-1>{R^GsY-Pg*%YH+hjZSAN4r$6XW421b%7oyT_tG>SHpU z=Y^B)B-&&*y&oH$JeDloJNe56I|oecT*u2{lKSWbehv@84IN^VIbe>wSh`0i#+`Ql zoH?naINzU`JWiG)79ei>Lf8h^BzZdd-*owKGrUu_txpcK!4!{g>_9Iq`rZ! zz=S2cv$J6vJ1cq`!if}c#zB&)&5E*WuZQyFg|O)IB#z=qan$Kdk)U<_8d+T z7aFQ{=et6I2z~(*um|lZSI9Te9)k#e1D>@_-t`i1B+b!Fc#;a+ByXZ^9Pgf|eM;a4 z%z2LlPa?{YP#Dm`Lsl6N02}!!M_XxUdrC)f|L&9R;FS3L{j>{1Ch*v?Z7F{Y0P5p@ zdVX)Bew0DL9Nt{v*iPjN@l3GZi+c`F`i1fDybwyA_v#S-ahS7v)YI2dW&K?hlA*;` zS6^rCT^-ia((E#GV{6N9j!f8%kx6^*@pFFe6|1X|uP71bJWwR0$*~Drn45R}TiRN! zL-laf6OI67m}f3bw9giYv8j3c53gLdk%`&tiP?$Xc4HSjOL^~%Pg`$emDOi&QY6(k zG+29QyWnh+x6XTuu35R{>;oryY;9@57U!~E^56LP|K^a5g*&q10T_V|Ab17r13`P8 z7?4XNjfPip;nFqTEAkap2yVQH)|N(J$;I+v&fubNPy%=`3?)tt<~t!T6boKWcW0Z= zwJ>+WyhuvCsgU-2dpeOO>YxMPXTm$yIhJKx`63wiMy7a!iz$S26e;3|n6lbM6YK-8C2biK&Buu(I$Z zLWse9Tnq?07iRS&vFRv>7URG!158A7cawKc*{dGoL%tt_@uv}emX6+z!+*(&#UO)LA|BQzEey<`y z(IGgz|Fd=q6*pnXu=Ij{(KkUClJ|%1(>K7+Z&&P@r3}1I3tfSG)W;9ti#noy5J7-N zD)M>12xp475XOQXuh?Nx9XumJBibd=rdOoIJ$_Or_XD<2VB{$*1HRBKe?QMNo^+4D zs0TwT>lghce&fE=A*+Mm@L2Fdz!bE|!*`^N{%F^^KzV{MQHFa5? z6DJ3KuN-p}%*9^2eb*Jq6AzxWb0>O*Q-r0EvB<&M**P1#I~ivps?Db6CTneP@kK|A z3yZcQ+2>n^C76t7VO0Et#4 z@j`i4R*{Er6-S;hH-eH$=hGw1lNM#e3~P<`)rkc$SgMQC0Wmxn!d0QT_PtzLS&1XT z+GMn=?`=_8>O;A4gaU&PH;y?QloRlhkF#a)mT=*g1RLHCunzRK+4*yQcJg?Kb#yeS z%__Nlgs~)K1se>2kia5wW`|Ph4Vc6?9;*Fp$T-~OcP7>!0)U6<9^WHR2r!s52?-a- zeg(6|V95`d$d?T+&y`Qnc3h;WR{{@f(61!W)VA`b=fFe>hsVqDn|c^95}Mrezx-hG zl)-0*tWDMB;Tq2wDRM_D#53>+dL?>#@;0fLb}63;ZDAC&38mq% z>z-%weEP*bU4cvd2{LbsBj7L-SGtU$3g~{>s`~J<_ zAU}eyux|qH5KLYl21L5=U`E^E7X780P7|(p;`fw~x*c966dDEx_`uNA<#lIeDHj)s z-_S6%3ocP-${%HTuGR{0P5FbSfv3~NpGDq)kKft7?2jAp8+la9%Rz$}570zyDo^lH z2z}?T498S=9>%~g@QF6^uyI@WQ=TkaAwGN$-g4;uI*Jc<1#P14lrIwS20zlb$s3M{ z2G)OFZKX9x&UAM+*>Ne7!TuI&ZmzWw8S2H7tr+3d8|4CS2UY(Ow#NfO=6Uo-LYzc+ zo_T2Gr7wP}t#P?aBxP%JOGfmvWZsJM@G9IjTUcDNnfYa#nqBfq^Mw^D0o8{SP*+`P zwUttS!gozFvP@01Hr2XfYiVth=1g<* zi!;J`-6ty>s@tuyykOV7lE1$>6TZm}zc+LRqcF%(PADlD5n+b)6c$Xl{0N0)ZKaf+ zBOnr!=0X%%-6Yz~GchY<-d|@tkC4y3iTZ(y^6sV!zQWMf{Zj0Q3EW8xI6_|xDi?6y zrA@3jo`V|+I$(K6=&OJ>f+gXRQem4B^2xj3DTVMRgck+@^rC(0Ti=q`y$h~+8k68b zj)?*0l)|V;FvWAWJqNCQ2}UujC3>e<}2nQm(%?dq zeI(pZ=_u~-O`=`mn=D)=7{R%)&K&<|a8O5KXv>YjY)pN(1P=i-{24r!$318hvdinD z9sQ*;w9hBBgQhIZ;aQ?L`8kK`)1P?A9(%Aq40Op%HfW<5e6CkY29My*_>5h>b=OYx zciGt!eS&}2-n%(w7jN9L6yLR;*J^p{&@!WpEm-niwNIK3CEpGE)* z(j#zFsEWyL$}>-a7l7wMqNAA$;giJjVU9r|l(Z<#iL0cCP6Pk~c|P%8*rJbSCp5*R zJooUB(6mg?6U(1EW5UhscvnP>U(x&ch_#p~g#8x3Ah;1sarVaz{|+P$oQkKg_+%ge7EE`C3h zP~a1F(ykwQ?t6!H?J|iTA-`B45V+Hh5Z3o-BMGAfXFOM*pxE^+ZDRGl)D`p%q2_Jt znU_-C2uR99r$o>AMbUz?+|S{Y<@4WJf2&RK^N^f}<+g-ZEJUH6gbP=+6m@tTf+zSV zyB81iPi?E8!Nbb}1$pkFP1K*!iK6`Zy`VL;i+%~3g~E$aQB$TX8%85CWDE7)+Y)P zg$@0=PdMDiCv^34hmvXKcJf$k$F6u|%b9rP1DCkp~ z*MHo@!yp30slbc{j#cURJt2~p-VZX|^qglH=v-;BLJlAQ&A4WYK$K^48tWf#1Vr!# zeEvZRjl`UXud0%QiPxrzxz!jUFT90%xjqPP?xTo6DkTuZ@}HSo@aHY6$5&=C|Dt=m z_ZQ8`?+95%S=_$n#(F7|I4p8mN{3~}gsfQg1w1G*j#A+{!X7VaZDW z0ADR8jPa6~gojYgtcch@w6?Z-`@Ro9Xg~1r$L+-ro>$AfJ7vQ!tZBTEn#yKx1EItf zv2`q6luVjb&EUp@k+td!lL^ya!QH&VI}74grVP&*1@-c9ex-Q{sN2H#O$)bz!34`Yf!IdngbNA7mFdedkJ%#|~Ta zo@g^0bb=diBhj7|{Uv-o1e5wBELlE-4o4~B@cUtiG3cOkVgER0CiA z@PT%DzMu}2$okfbznKs|P+=T#30HA z9+|Qhe(@cp9Uep|teBK$<0IBP0vMQm?4bNKZm6daci;u45X_;FsY5sQ55A?(qK|{# zQ6|CZJR-S)3Gztk@In0FyTw;d@3HZnf; zzzJ7QUPf?IFK2EEeXj$!k>B9MGn5ow4Q(O2aR7?`<^TS74>=l`eIE_=5k|tgrw}i& zA~Th<`pD$@?CJY78dmihHxt2qD5X~MMyE8uN$^~TvjA%s^!{pSQGc~Koz=h9K zCk7p15?Y`66Xt7Tfj~(dNSSpqduCX@_1atS`ZD4=Da3#FFTQMZ^Q^%S1|}a?U<_~= zG89-vY5rIi!bHH~J&+}yr2!BMCt($N6;`Z;NJv$_tgnJ~4Q|U*fpsXa!onB#`mzPyJACcfg;D81xL8lly5e)zi z?>jvsf6zF1-S?3o>=}RRZ-5J&V{Zy6Xo5V6=aKLX9nf%^mh1pT*xaZHJK9)m@h2}9LNY0wCo4KIY{zche1fRM*aLAw&_ zf|+8Vu=En9iOzt4K+IeSN`|eK~%kt-{o z{Y8rJ49Cco)lraK80bo$-j27Wqz}KZuL-WWt-q`cO4aY_d$g9d1zft^%g_~Nshf79 ze)6gXzwggA!1#+4@OX_GzNFl9uQ#4-wwa?I3@V<&%p!12tOquUzq<0%tun; z{iAJ?0v6Sg^-bK1`-KU*f@ky{chd8??>C(Wz$9bD=?s0MKmF1;i8|spW5>sj*Aewl zKE_R^TvToFe^8HRxS9K{JOOR|!E^8vkG)RMllw1HdY0;OzK-YohJW}ObPgrL_acXf zGQz{heZbY+RO1II9v^JA-mWIe3XUeJkzrls!renas+-B{h51EWSX}aV8?o&6&cwX$ zPWK(-@s zStzx#YU``3wx7+51a9ha$xh&cO3)o697RVPC>%06pYqQQ=XjS1aFCb!7v`62Uf1Nz zg53e1go`flD!TUiA39~HXXf1MXXkl}yOGdhC;)Fr39X0F#VdLH(p6hsUAHqQkGX>2 zDEcP3N+^Qcqho$pBb!-{5BAyI0u+85kXn0ttG`iwE38bIm*kd?TYy-=8k~BT4rMG%F)^!hIqsZSJgj*?UtGbVvIjypKXW6a2sfzAn`D zm->_PJa;*OOx%CgU7&?-#dF8)e@8E zyfY~Asq??ubpFctKWJOHNCTKqBGg4F2EiEbU(dsod20o;n&gghme<4lh4xR-)Q6m!MQv*Fzp`viQN6E93C7urFVPEJnSwQDzhfz!s$ zzKtzy+S2Cx`LpQqw}1bT-Q{k6BdPt8E62D)9DFqKw3a z-}Be5lFP31O(3*=+wiX572uh>5HeIK-sec~h5EF)4GTp5se zQDf`h{@U-@^{Ycphr$G32d#rv36A)k(JAA{gh%29>W;Y>UU8A&WZzME6VL=a5>3+c zLLMgtE{v_94Rtey|KiX6ls)~-6HbTDwmOekU})bR8L}H!-?qK&4VMvae5)*m>GZTb zj%*rWUGm5eWkfhjnBz^54;jXLKqhAw{dKt4M<=bjt;rP$p>_DOx}w5n7gv1mN3E0> zbJ)nOTVvCH?iTWTTgJOd8J_I#vd-2fZ?mbX!RL~}1^FCrvfZ;a$>gQwbfn@Nl!<_ISxXo!O&^r}m7n{umvVClssM|f1mU@dW@Nqs zLXu)N0MF3El@17&IsgzZLdm$Ik;lS4gg4#_b18T#XgV|^?}<8E_{r%9WysN4?~SkgO;R1}XM=5wHtRt7X~fN6P70BwtYf3n7ItA zTFmIeO6`PjQRg@_!`%4+&1XtUQ80(#il^W&CBDPx%#*lsQT}8-`H=I0grHadd0d)7 zbM#MzJio)A!RGMkK3)X0Be@VPc+M4VQZ{?0e6rcRjMuGnf}`3jhMid&;>UJCNW@gQC_yM(=UWu)SU!I`kjXfSo8NtE@<>j_Ky{n z=$=5pVs-%oeDtB?o2RR1@+25Cx{`~8U<|yXZ}imfsfWEC@}+v5F1qKFzYb5(o0O+_ zD0ZF)+}!ih`QQF5@JZHoO5D%Wp^!GTrFt^j#dFX_;D)E*uoMOVr8>&X<8{`I!;gOK z!}b$D^`rL8vrpN%^JnbL*^_?pUY|E!zhDy+Q$8Mp@ARp97~2>Pd;%LsC?#H^{u~dv z49H*={hV*~2TY7J5*SmK{Lne^P~gHnXoq(cGR1iSg`JH9`lY-Kxm4lDjr{yy`f2-- z&;5{{KY!Nx1_rIAz1y1Gx@==(-6qCHB*S-I#vwD1l}=I zVm-RLq%a6;5k7(|NV8HFL~A&B(LIGM?1aGQ9VEb<_SJFT7RSHOEwNZ>*J*fspvU@p zJFHgiA;(khLD4+lBLTloZ@ld@-D(yg#F`mc4QiRLNH#)18u;Ey&`3TjR+4* zjd>+GIP`S3I8H1MDEh`9{hLGfT(H-J$#z^bcC_D>Yt||ahd?F~vO>Vq8B^1k)2w|RYCoqy-plTIlMR$y@;P;?>*r3VfyStjAd+_-hel^HwmQ=Pr4 z3nfB&`_dIZa)reKct@?Wrhq->V|ZCttjA`)Ms)RrGP-ed!v592`V-$ZPb%EgvAIKs zg$sx!%PFW>qcAkj@g$OffO)xa^A4%HS15SdcO)r8>d=sIa5X ze5hn$b=*T2B+3BacT2zpd?Xim2yM}eb}1V|F_d{q+z0*?{vrM4zu#^9yG8b;f4*!> z6B0M7t1wiE=a>4BM4x%cQ-6i^6ej3RyXiS?ro{`SdqZ2~iM&A@tYlh(j+6nW_lNMp zK;fPXU;U9c;dsA9Uhf-yCLZK>dd~+!zr2iNV!}fxOh5OR{=9wi&wamy?Kue>HVCJ4 z_n5zojmnI>!nXhBfB8S!AO8NAJs<4=Cmu4q<}2((SK8!g8WOzo9}8`T(ukDm4E_%u z*xirt7s(H2E$GXXdV9O=XMW))-K*;E?)Em)JZ&Y0b9NL{k|U()@nL)In}2AFGk0As zA?%rCM{XbwF;v$jvoK^aP+8&&Dac2Z1EGx!!)qWB?Lpq%o30Iy+bi!3xtD_Qe({NO zek45d4uih7M)0ZJj$9z-sHuan*5b;V@B3r^g=qysap1)>6bj{h9$PSBh?lDz3!!Q; ztko{>BViR6G7~Omu0_=p&g}6K$1BW zev2(dl+d~m3E>Y>*vGLVR|t2AWy6HF+?`WCyadWKAAyyRwxl!Iuya?Q2>E>_fB@y- zM8a027>o?ilHB|T2_}UgJP(V2Hy-k?g?P@3ujniYx469Qgu|P0FGKhM3yRMd4G3pe zkFj6?Z4R+;O$mX43v)FPE-VG%tczX!9Ba?6{*x!W?c|9r8y}xjoiS+BazcbOpDtXa zU`Y}CWw^%uP#Rbc8VKCvzP=|xLmgbX-x41FJhOO1NQj=arRU@e0hd{h8bny_ZY^a4 zFcQy0FwnMJ#@=okkU*AiKk655qBZpcd)$|0$iT|lrT!dlSyK)dwfTdAPI#T|Wy3Z6_OP>bKstlqC-P_|I zue17A(M}}VT9s9|jso@4ULS4b=n_So-lWuC(2qU{TuI(GH1M+Ek}m_EF8mAeaa_O& zT*ylwM?dCq%j3iSd+|JaFX)vnZ-6I$Uwz9T&-9nW&lLf%#(d=W%ky~t#b@p8`O|j# z>UMkO*KRyu9va9Lel&Eb;6XK_*Y*Q~0h-s+p$19#;^Ng!mAIe?1L4j~?%A4_qO zqvYfKE0zM!Ew0+UjM~+8HX_I7L1YdI1u{0ZAOl`9r?1mm8|x$|2~l%uIS~mA_LxgX zNpSi(GR*gch;d{5V@NW#VpSP*j%^eeMsc5%Xuqp_?5KCSt9!hEggG1%o=h?lOMdW^ zAASip$TuTOn<9A8aJ==97rZmM&B`VYavU3<_M_Ypv{>)E(p2QfqhO;T6YA`2XNfNw z76pUiKwF{_neXWAY?ZdFaZiMabmmT8dE=rp#hDWWPBaL^Z{h46z@$9{xFdxF;R$J> zz)%EuK6p|nKn{TVBBz6xhCZyF`+*_ zGw%wD&cSO#(b2gafzLaXKl~*dq@gx3B)mMZuFvA+?(ZuM0(4=}B0oN!Abo z2;>lkSwa8qzf}hoR|uh4zDVM?=g9^KmX2HL40Qdu1?!YkXH(t>AHq7kAoosm2ik<- zLji;ppKCiSA09;vVBjSc&Rx(h&uP~OJeH$dzJf8si;MChlmd3>5c#6cL>ul=4o_%( zYstffX_D42FB^lAL|tK_hT`Nt21>M-hdIH^J@SEjVS3~fX8jKy+SJWMDU-N|BI!9> zVS^7<*!*qIR@Q@t|!%{jMY`u_CLJz z-`dao{7>2APd?%yAkL*j!O$L&hq*Zk#q64$owb>nncQ!_cSsIA@$@71p^rQ-p}S$X zZ{D#jS@4kpFFxV5pa)|i(VJ)REL<+duRO^+Y26nTu`jnMwSE0Fp5dB-E+!e_z_`BGL@Zyh}+t);!srY7&&{Opw1 zk7du?2GNb^uLv6*tq7CkRG@{h#i zaGG#cS9sYN9I+RIfk8?Fiq2x-6UK6(V+aCwLcq(toQb;>jxY>i6CV8%YHcm%p}c+Z*8#Q73g%oRwUU|G9EI`<$%LTFUO(J73hGo}(qf zjDXA932#(wgQKS)Y&?%ml)1iiSQ-3zzI7h(cEeC1@!gepK|4GP1r!1~{-NySIc=rH zX2BHtBQ)m?V5q`ayr$Du=YkQW-z=Ej=jz*dC2zx;*Yv^T%~HCL`dPqT%MU0dE_ zpAhiiQL*)5!v#Bu_kav^wfNzU%t0VSQJToby}bkXYJiDw8N1u51LGYJBdv;M{Npj; zskkSk@5m)4+le^gaTJEokgKG&=7`$hi6FO;>A+3s3yzqZnGNqT8(&+(VR=ik>9e2y z{+F=uQ6dQCR$2cv$^~G=k`bb4Q1}+c1W4~#H3H#0E{c|uCgjKz>Z2}%bA+N8+)*Cw zO=tK-c}2WM5eS^ftULmG`{Sm(g|zdHz!VWbu;_X3CJ=7c7oKRz{zqjOUBG@_2YVdg^)k({q29 zgoGHCpv>3_4Q*3iFQ>1A~%!Bf1Y;3fKh6WD@!S@JY z#uD=i@DuGVEG+nU@*$`Rg^`zcX0*tFY~^%s8E3OI^FHR7IA(<>V;g#)9Figl+(Zxk zx$?-AEqL(T@mB_vqOXip`JUb`;nQcGT^%0E`tV0yv}4By)D8#7a`MH38>A?QrY1I1 zb6{aC7~&J$zzG~sj^JM-!T$U*R_l*d+6D_Kz2U=w%m*t|TKvU27{Vs2|q7XIc= z$T8p~qJW%X?xaETiVcd)eRQ@rONLcAt&ufEjMz>=@ z%2)pFA>pVr*-oW2O5;qt=&Kk-UV*e4&n~`u%?ZkRRs2rr#?4_}qxQlF9``UAX?k|f z-K>+x`n^19VR6}oJ-f*hgMD3A2nevVougW4n|TZf1;NP2>IWv<6M`%yCLf7Op%iYz zy8&({+fg7S7?F;kJ~|_qqr_nu9h-Co#nJYK_!$Sr356{f&rC##da6k&N$|K^S?sof24qcG*@l;KW-5B$x94pYFK z%K09e45sgvQk@_9nI?PYhcwU|O6#eYF{7RWk z4Pm70+Nb}E7CZ51jeYqa@5vHW-eU_k^In5Bx7XSy{%WmVd-=dFepz0~QhuQUp%ej? z(H7W)HV8WIVJRc95sP>zcpjVEtJ&Zs?01xo_a^teM0;PHQGomRqKykQdX`>nk_7O&7Y<1*z-Ww6Mv zUAyL=D3Y$OE*I3~Pko&7a^V@gbK#Qx|Nj0zv&$E*I_(M?FljlRUhlJc>Wfu8Z#l@0bU_jou(3k9@PBtYXyErC=aMjf8 zqRq}P`4U{h^C%BwE%my=D|ttq7|97HWEO@y@(`uL&iAIqI(ab_Zp^cyYjJta+u?)| zXoyF{hD+oyP61mpxQ{Hwqd~#YZplx4_LDC)sxkyA!j(CJG_exigcvPDlnZS}xQPmw zJ3weNvCSL;uG6uBUe6D(zH&brmRmZo2-w^&#TU0o(BS=fWQUqOc)F+1z`#Omz ztpOMe6(tdDVST1LCFpc7=n}BeW^So2w2Br~Ix{v9I@FWFN8+9fC_tkwwV6UQOTq8L(Ghi8SKDIpI~4$14s>gSU@g%|ZPt`O{7+gnmNor38Z zc@Hnd{Dh2N61i>eHXJ#~(dyRV5qt!i&$@sk7%?;T0wGXtzCBf)PvU8+HM-(NoxY+ggNFpH-~sHEL4cDF#lk(j92nSL zc;!#H;A!CLzyNd@c+)OzaTq0gVXCU-s;jd|%*hb)0-w>l6Y{tg92XMAqz+&~dH7x$ zX>2^Gp6+IQ@`+RS?6ars^qFqCxltc*Fc?^d$iI+We-I6!rp)BR+u@4$ApM*cckiJqIzFtHpr;;WEHaZLO7I#u9o=Ef+Gl4x zdrFXBgsYgSXL6jYMly_Jp4iWVcaat}QKu@9lGt>QgOrpAZ9*&P7^}cI05#q(1GnnA6aBY6$)~H(u-R$X5#Uxf)IRgoJ!du*b`|?%y0N7Kq zzP=^U4jmYFL;?V-x8ixV4fglAM?vBlS{6e1ppoL5o+nNja~U12&AvnS&3E3_u#%-9 zrUogL#6#y#y7kXWGLChL@Y9~R!5jy>@kt;YljnQxDd63pm>?8wvuuxNC=(8%M3znLHYG90apB#1bb%3@;c(Z1ISB-4d~fm)hW>bt0AWzY-V`jju>OGU z-hCu6Q-nh9Q)h8_kE9e%C=8dSqf-duVBHdrrv22WACV#>VM9uT|Ij}F zd%tDB_>2F!HA=?(?yvu{z5MMPCOb~_%!jrx93Fn;0sG7UkDs+CpL)!8wm0qff8(Fo zdlz2wXTR{*|2zB8$A3ujfmKSO1mJDjBTPrQ4q0^i^l1-O6{dUfNuezg9t@#6%7RbA z18;=ijDNf~LV)B0e{Wy;$0sIiba=?FT==G~E=+`+lmUudCA2jf{0r*A%b{nx+9lJWiR%J3lkeKx z;)=C2a_V}$%S;SvCiRnCAeH>$&wlPDmimU(9&3>WrTt{Fd zFnLX6>;VCxT?q=gd-~2Nljpo#3Zcu&D}ePC1)1>00!uET$MYCi`i(ZmUB=U4b0l@4 z1n4Z>FP8VNuE@Pq+gSeImS8}5Lj(|xaMzA36L~Odi)XAt14eN133uVcC-tUDc9aK8 zlpz$)P^@@H{H9%B)+~I8XHt$%=NX*>9LdA+KivZtgbIof7zm9KegodM)h+AjXw<-J zx5cH{n?f0w24XSbp%K4PGB69YLI`lt2EQY`6P`}8f+-i)tCH#>pLakmF_6NO@aD35 zX+D*wNQQ~GEhq$22n@a_VHWLsO<6vKS*-8plZ*V`ka9(Rj;Q}mut%M7pE{#;m2>#; z076iD8yPJUp0&-zcH%J&BK5`2hVrOb;Gr_=Cu$-!_Y_&ji6X0M%?79Pl}WkD_EUeF zgjIz#)JS>PO6l$#iXV2Zv|Ng5@6aZ0u`vrpUSw6}RkppLK{qZsR_$43$DTEvD6zWM zN`C|A#Ek>HdwI{suga*{DD02uub>B4&<$Krn5myhsc*9!x$p5j@Bv3IvQXA(?R8{y zRDYqo-8<1=wFJ!vUwq!4e&$Jg;K6hD;)kBM{=q&UZ}2Y(;ZAv?3uywKDY+>dzcXUDuU}Ss`_|soZ|5F<%39jH zysZQqS5lt90!-iw%;1Md!FTe<7$-!&n+sR*oqI`%5bh#T2RNk9L`Ub%%u89$lqhW@ zhnid4tfRYM^{k3kGcxXZr9~Kh$XWW=H;B`%d>3s{JjfBD;h{0)F8NU`+$6e^!evYW zaQZLBORH!O>B1L}h4Zt}isXk$AWU+l@JbNcJR^LCW(I*fY9E5IJeZUOH!r6mbOk}tp6oYa zzYFs*)W<3^lpJ-!*whOigrxWcQO=}6si1HOwLu{AdU#BqFc~2`F6zf?;fyd83NTO? z`B6enY{9_=^D?+;GZYa=+}AqO03)XYpy7`lYqQ6nIB6ev`i#B#=1nn?ECU3C{t!Gd zaVS9~iz_^l1Ks1yoe&HN4)S@d*zFMI(7h0#A-Hlvmw=exN1y~lP>0{+)2}E)c{~h4 zR0t-o2ZAZ3@PS*+`h(WgNx~2y@oY=hHDNlw1DEFm=B&Paiu=Im7IZf{?tx zl)Q^Po(I-@rouk`*II37(`;r`0(BQb0Zl^iCiu_1P;8HUtVkBC;4hK_lGjnRrva*~ zS_+`CPDW9=`hLFLR%Z`wPP}qhBL0vkx4pM&TN})$RN3qev!#)JD?cc<`ZHTr|KOH& zoouq&`cP)y_|7ed^Pnuw%yuU5O-t2S2mC6`@LEWl|D? zseKd%LKEeHw}D_}e+9x69>?2Ab0GXiC{M`x<~f3t-{c<~8*@2;2hh{glUqtlIfOLz zUU~1byqPQZ<{Pit)psx2!L~e>>LPhUo%UD%tG{kP`}rDb;f)OmaG(GS?Evya(37cbdsufJh) z3nAO^C^&jvT`d`!&E4o9ujs(jz$-z>`*A9|wp`4K6clnZf*t-X8g zmY;yj`$;%11R?wKD{p&cSkVuiJMBBYW4A8&{eHx~5gsm(^_#qW4@IHA@H&D{&WtJvJj(6&QlboPLI4+l z%8HO8Toms?K^b8AvZo`29KQpmNLXlu&!S zlHuj~^Gb;JlVYo=DYB;SVte3)8tXn;p#f85H{aZ{3t!o=r+={8dd^Bn%9%PSUN`lE zZLP~&xOQMSUOBLu#sjOWEwP=sTI)PkWL;AMy@T|*tIR&SP>?NveqZ-Y<;HI=U{ryNs)a* z-h9!D%};FG^{-b+VPOc!`0*oBB3uZb;GF0H?m_SH%Dj(7=h$c(;{&A={T!Z%^Phfq zw%6N(=esqIE8Igrb?T(m%g`ge@X^o6l0W5nse`dPG&JNwk?#ohw89FZ3(pg}p>6m! zt-wN{B9OUHUcQr$JP2ZV9XW$y0tb`|!j!TIT~>aPkFt!NRL}VMn2n6ww%`1Xf30$6 zpZ&~_*zna0cIDEW*3{f)8%32ixg4+l9_a10=bwAtZoT)mU3m3RZFg(k&OY>{J^8^; zSWR7%!%(=wDk%|Qhc8ipC^Pb*01M#&7w#oRM1DeWDByr|+?5OTK#}ka+@V*Z8~MN+ z+H$d{2D$s%x4&+)lXpa$D&ab8(^GMJFkvc`2k>BM0}BE@MO#?0+E7>R%YV7gWIBB0 za?GPIthSQ3%>KI0ZYv%*Zk1g<8Y?xnUtVd&GZR*J>#A+cO#6G^RnlR_E1?{+j`2o5SB{;U=p8TMyHs3yWX2=fr`!Q7rl z7oTXj00878JciJuJoh*r1@8oAp2WQ*6e+_q?xi}Zi&YNfcgrSIK;Y*gGvULt zs1K_@@PP@zDJdrEjZ-+;Kv`-hj(6Glb8^AY_E~>_n-qDIESfqoBo<|?NI_5__>+dG$9ll>1<(jQSY>Y%(ok4pHa-!a&tWV%S? zE%;9T`bnL%?{#M>zU6;YF6Hw!GdL4Wex*{%v%P+36W8T6Y(|)?_guMkpDwr7-ZE=w zFZESez7nfWd{lu(kpkKgAFqr4mGZ*t#V5t;H+d<0Wl|*S(_JZ!lATK1m@Bn~i6UEF zkYd>{wyGN5vm=ku`9=z0Lzk4!(#)Zh!;a04Z)h-XTXDH~ty~^YDPEo|%GP3Q=`XU5 zlZV#ODG$6({85syND17MvW4EF1%2ZRKhZMi6LgQhCwCYPj7LJfm{m z8l;snXg6*6)(v?cZ`kW^yk@Vw_Noot8geDUDy169iN;2?)xep7p*URD6kLo+aNz~C`IsNsIwH8{CEpI+m`XeN-p+& z93nreYpt}o)rwl$#Qsuprm>}aCYl!2>(XbL=v+l41vw=nAK91if*+tGx&ht<2Bs>C zzWxWla>!nX8g(j#-jhdyaAu!}PYCPTt8ZTR@3=|F`g@$PBwo{tyL94MZ-kka!L&UV^nPK9GrSPIRi%(2O7A1Ek2 zj;{BG1Y$PYCe6+-QFA$zhS*oAS?@K#%r47N6D0eU|r9cS__VOa{FWl$0l|gVBOV z0-Pzdl~U9RPV{lYgSGZS)B7dbuBtDxpZ$BC)^jF!=~w^u9lQ4Gs_pF_+Q)ys-Jbe^ zX8XPWWzn{$4(&u=m34h^-L@;Y?VDc^Jj-R)D9dhob<1jHEuQVHv$?fxf3E~PlB>&0 ztf{)(`kJcjaJSS(#*6IO2iL6Yfh~LEcPnMFZCmT9RhzzEWh;{s8v88mj9B*P{!W!O z4{Y1~^rF4`J0&)Hq15)aWoY=Gl2U)aN`wL<(I@;)xD&>LK2BSVJqfCcvKpt!z3H)s zdv*0#cW1NnB8jt-);G58cfb0owKdnC`GRm17B~VDK7M!l^Y+fPgl;ZQU6jvS0sa|G+A18|u5#QK$T76Y@wFHpKTT z*HTkqUG){#CmHtRKl|6MtN)a@#oiFS4Ll!|813L?@C?t0=Xeq*2jEBveDNMoion&^ z*XPP3C6p`AZr;4<9tjH+$OB#}97Z`VzSGvb7vHd}?|jELR_9bhk>ts;hmugK_4RCD z2)-qO6Gj1ZxAO}ts#k(u#^>BZY;C~MW|0$ND-s?Iytcl5;Bo>l#UmY3CPaA}Kk%GY zKJ=)s{9>$@ORf{f=Y?3?>fe%E*RA5tO)HjyliB3`N>qr&M2t)fYGfZFHH>81E&0Wt z`P@sqMi)ila}xp(uLA)#JUZ@k0PGV%i9i&dGht2Vp{W3v$synSp+>Ooshsc-4U%Vm zana+^6hcXHA135n7+f#Tz+&w)mN_9N1S+tQj}E6^pEHp#X9+Mdyaf@F5EjS*JA@_= z&j~xRxsm!H9KxBrL&JC6Lt(Ch_V8-Z`Vfb~Nq#z-I(!XwCg5QJloRy>FY_zZ12Iuf zBoxfT;)-`F^B;IkFaY;RwB`6n%X1%GP%tQ8>RZq!c%ekt=?gwQr*9~Ge6ZUddH9(1 z_O#l>nzdEz?)3Px9czf_N&N1cQ^!W}1t zwHsa$^->mIpam32guUq7m{-XiX`y`7M@q1|x2x~e%@v9v!IMwaNB&Sq362Qwz(d@3 zII`#554hy><*)+K1J4v*XI zv1z+KGGcoh^M2HMPjA09G)72>L>pWP&V}_+8(aI1WJ8TL)^b{Rsm-ikwW+nswzVfA zSF&df)g7v*UHlwd2DnHFTtYn5J1cAa`sFw5#(NiRRn~aDya;xOFE1_Fr8i%(nW=GG zUYxT_*RK0GVSfcanOZS*r+DL7#qBP$a5F}RVV9EE~>CBi|JhcQQ9=MCk>80EJs4Z&W2 z`kYk`p0phq42N5rF5{3P$Tz}h#ceXkMc=)#J0`@fw6@OohwLvcTG`Z?mCa6C(Z-s* zC3&K1b5{z8h!N|=sRP*tY2ZJyI5+a~TSn|pmWQ0c&93%!P&k@Eb11{dg#e_V9nN?25 z270{R)FDjrbC@Dabf>20d^s{_ae*940|fv+OwOaUzz2BQKzQNORXy8ue8AN`5k2#H z2nj1ZA7H{u5|G{j;6oX_DexoAhxbMN(P7Hr6Cny}#}@JBRkx zmsV}?5zfbw0aH_CTa&W*Mk?)Ko1-`m{R|-lQn~mOL4^|Go*w1WlsuG)m33P?kntkJ za%;H4TB^zYskm{`m91VQ;?jr>;y2)AabL z6w1rCFgxvIW{0DYMBi!|t@O#Nln6(T&~GU%ZtU8S46ZMH`K$H^fAD#G>y@wB8{hnr zU3~o;u9Q*&w}cP4B{)gIN*If979JEdM$sgC@SF=HlRU@+!ghExD5Ju}y~B zL;Kd(zhpz#FWdTB7@7Ab`iaX8D7DGn4fc+(95~5fycgsi@(OPQnTsr~mJwQe;*?dL zf7nW703?bKLdR1f>U1c0ca~RdcXY(|C+{*?NT{p!0&=$iP zkJgW?SA8%X(I8~Chqe%OOibg2V5vg@tY?H8-@7DWIr0N#^0k-W@Ov;AiUG}Z_S7*K zws;sEe zrp+%bx|KkEC=facd{}PGZh2N*(W%g>r&Cs!eh1@mT4o%1&i5(70p8FH?Ts6V$HOrz zv>O5dh7=;g2N-A%yuk-=#l0|HbR+^CtC&?7gyVkex4vVGi`hPuAfczUdPg-oLn6H6 zBw#{Y`7l=O6M>KsjzT!65LhH?h=fAm;vTf1&JcXzBBw+N@bYk3u5>6KS1S5TFol7T zA@3$>)9IlQs7AXKJmsDT7yeM*d{SW-m+VZXMYCs`_AY$C^*AT1Ax< z#B`n2nFfM*;)eQ?@JM}WxpmdaY8M#qPA=Q31`ci1Rm#Fszq5jiu*tgaQ_lNVgKSSQ zHdL0`>GoQy*C#^m^2CZQX<#$=^y!~(wZU`cwj^U`>dvYy&FtFb+wHbGQ|5*jJPF-U z1}R*}uK5(&^0y^=Xfh5{WX}&g&F|RoDep*%;u9Zv!1}rgwG@dLn_XZqAxaUZ}JWzbYvSMPv5 z{o)VW7ysnz_WS?t^Y*cie9X?BJ8MNH2R5|y1=}l`w@*Fw-&jNWfXy#W+dGTDY$fs- z9`E>h>#UE19)aW4Z+^wT`1`+U6L&|grmM&XKU!{;ZF{!9ylvn4?G;b#`ZI z%Vt+Mt-|)CoJ*~)qQtIDt=i10j9&FI{XmEjWzShpy#5vC!uUba&^LH1r`zkSU42`k z`!{A)er?anE351mfBqNj=l{ZA)Ohc3Z-NjTN&^Lu=5hGW5??}JcqD}02=fufOOXl` zBIOBJ;f1h2CM_m_hd=*2zh>Y5##cNlQVl;*Mz5a@=-x_i`LVFcGz1H^p z3s$kQVh6XbS+V5g+WNM}$hvzP_5FQT`QW2gwzg~sH?G*$vOYVzt>U4_ZLgx*c4wx{ zhTgM^#ToGx-f(y%D6*1Y_zRzX31)K5B0yNy>ejH3Vg7^6cqlM|aP=2UflT}kfxOjp~+n2-+9#fOo#5*ciNor-%u|2*8T+3qgv7d+37@R~>rbefYkb3=x==_V6^k z4qea&MNyH#1FU#BC>ZKxA|9a$ZBV?FC#BN?@UB1=fk6E*4EQA4^68Ul8Z4}kpgm%w zMG{mf;f*!UE~=4dSm(+GI#4ePf$$C~JOv2jKlK3~zygfWEQL=(skv98gcVGGP%H#k zgjRqPj{>|R)Dnsz>I`?Ay6{Y*$@o813Z7il=WR#rUJuFXqx$?V-#w`$b@P|I7yVV+ zY*tYt!7dMEeOboBqVV34cU>jeHjCUF+1?aR)d*)P;dKdZhrP&VB)qnF0)A@U)_~j4 zZV$^~t<~4CGLjw4 zW!B$KEMA_o1jD9yfH0N!o8W`4vod^U7bK*`@AVpc7*h>R4c@z3G9C$Maq=v$YCSd3 zZ5=Ia#^pUet2QC8e_?6Kb`G|!wzt&A7OvRj!VM{`)d|7VWP3Zs z>IYe~hwLxGV=A?A)qiv7wvEr+lt3P}ja7MO6_O*ht9DqtV{6MhwluqE<2QC}V^Idc z?v70_t=OUziOT`;I`m(cbqw9<2SQlT*2j!~M}z>Lz=)zh*%@Bqb6tF2)A*TH->iu~ z2m8A+j@FHY)GqhD}Y3+2wcMkOw$y`+M7}HQ{f$ zA;jus#nFjlec1pvd+n{uPN?qAcDHcYOouQ>$a3x!ghVhi*$xsAf&(Buo~FcMU?>m{ zgv7hyo#;%qr-<16+DYk+3-5aP>)EFsb>%{0@*E{Z z=!o*v$&v7Q9&CmLpB1^DZ(g|Knw~il1{L>FD4g$wpvDs*Ka=fApaU~-0)q>2l||Wt zC;6Fd2X+z!jg=Ddvi)R*jZe7c6DlSg^(uU0eS8gEl$u46lgA z04O6o9Oz9yKs#W$eskEvVV!NQE^APDzy2Fvv+Fk|)x)glh%gD=jWCG`+YtDbvs#SWg09W}BvgSH?3^9^?9@oKyI`5jvw4+S;3EE-A>#c+&4utrEk1DE$u zBb=~c+v_TAOZS#!;bH9}tWrX`AdpfBrB>D@ilj#Qh|v>b;TiXtjHkYow%`jXse|=e zJJ=ma^5ns+u&)DePiXWr?k!K&V z4)H%_h|6ESb=Tg0_m*45t^K9;>`%(NKPLJglv?@3M{Htv*)H7v_qMb)X%zr}K!Cs1 z#nyYO(gvEIv(};)?2&WNT2cM94Q>8k_Q$_<$=>_Us@1fu+DBger2W7PzhH9}U$N`A zzHPU@xM?HTx80*ibS2Rz8=^HAiXGl+uMOT`+=ye)D>Uvhmh;5>(eXw|^a~yfeZ$CjcLlX)W{3c^OR6h@WD_0#82DDzW?La+!XuO@KEqH82@-0?6OCZFz!=gKL}wj z6bTCxm`nccU;F3w#;f1Vazw<$Um@=E%%w8Mmt0n~HaEEJTk$zz-aII}uM*Sn0CO#B|A2T9h!b&U{ z9E*UtlL!HEnBs;DQeL|6b0;%%E}VH~B!hx{2x9PQYiqK;-Y(x1NQcoF0w1_|gChCC zlea@6#QBbpAvrG6@`Ujk8ylol*r-*Wg}atKKk-uV#v7vDL{CBtOlqQJaBWdaC{AE+ zYi;rQ85oIoGSCL^7hvTKN5Ig*tn|VIB2e-T6(MTG_I?Dp~L1uU)~8yrKoHTKdebK~U07?hztE0HIJo zr6yAtt{9Z3RRV3WxyHKcD{~<&>L3J$5&>rBRS-}JK?E3|&}EaI{v|tBQn7E{XT>Mg zW*VZfBKmCzuqP_euiYkjfuCW zRBmx)!_J)Ov$KN(R<+w;dsDTxJ6U5zjSE&*BL#HO;<90Vd)+qH#ruaFwp%f7XO2H^ zb@kOYw|&`;cRy~=JoHIK6q ztG|8m9sAC!-}30pvB6_LwwOO*0-cZ*eZkxg7qml>4Br~E3vaw^7heCCUB2+Tl=B^p zy9m$v@_zBijuazf(Ro1f5G;{7C<=ZfGmwYmsjjZ@us7qJMK{RUO3915(bk{bM1<44qdaAt5>BEChTx~ z%jt#TO=1`Q%33VbmQ`VUQbfDTw=3nbEAOSaqEa4BmzA}3_z45$7?dYeKl@U|qs7Gg znS%fU)Wy=ra48)=+%nx~z@ZQj?1dc-KCH)fW`-WB%Ll6PsZ?IPGw!3r@Z1~^<>6D+dK)|s zh9CwH44eiK@l5I^4hyb9GvEv*jDlgs7kz^(?A{kJ=sCDJZPgBLIAJl`>FsT`vuFG5 zp@#>2f}U6^r+{K4;6!7=@I{TiFQ(;3wJq| zWXEO}wq>PDaqFT#@cLCG*PcX=3nqjRljA^(P*Ig`xl+bMCb|zVmMI^C3+2%&ub{EI z%!L>*lw$bM&(1I5fo#bV#u~;-rXG5BzgQMt*^ZS-kqA`VT`RHq+vT=4BgL^Fb0z7b zU#rwsZB2!>x7PXO`PoyQcIrf@b#=A*`#ZQAWdO10rMIh9UI_cDO8s@MC;|2mKr?6v zpTjqVl9!j+T*oaENwYn=^@P^`ETy{y}^6kw<-b?b7nB4c)$E^D-p& zHt9>Vs>VZW>OHWU7I_l2qNA>|^5~ngQhkc{gsq_=f9&0X#x9FQQ=XyZp*_l?rMAqP z)i-teUaE{9g3S7cx{?P%D3iV+Tn0~!j@_|0-+s&9c;hu+>BXKJWQfZ-8G_Ieny{pK zZEe+-78m?ln4htUyTf+l>Lt7S-UVA*mHf$!NX9iZX1vGdK#p8t>;%6%0{Rd6#dxL+ zp>v-KsH(DZT|^-&B*?2;TCMKXX)CF!aYg5fM8e&ws;yYcPL`(Qb|B-bM8Da=ekdGv zh1a(5*&eyA@1wT66sM0D3$J43L7^h^{MdSpS&2o#Bbh7s2s7?2%u67}PWIB)b}Mb~ zw6caqD{E=9aukacPtm{mr+@Dd&!SUS4Y7U@!SZH4fdsN3xg-W-K7rqacQ{`Cz3W51 zB8f?GmWsick3DkE1?zQLH5?~G7>V)-OXfxp^bm}W!F8jPE?v1U;m-R+#LQwIp7X{u z&e(@p`g^;5Zsgj~Z4dWQhWEoqCl%Y|RII`PevUlh)t1Kwdz}eL`z0Jm0q4HzVcsa< zObU^d>iphl6effi%T?i(CJDJdS=W%a{xx}xmsF>^wD#PF)yX62KG|$Fb#XxCAN^PDmWaME*^Z)S{Kvv3 zdBQUMTzT~kTHOmDv8CI?Hv8>w+1~oPH9ho*b$sNbwsZBeZHx|E@wtbsSVEo1L6zj< z_T4)+^Np`t{kikj`1q5yEn|KA`W4$Bx?-!62jJ7u*63amn-C8lc+6iUwt4X_D_UK$ z;^tP{KYmus&by*Qc9uy7Ro2(r-o-a;TWubmI&T$&@6Hss%$pz|e$q<*+F$y~ zmx%c@2oPde<%~a{@DcMfphNRx9mqA5WgHXC)xK;MucEEF(Fsgv*Gc%IgqW1~@Q%vk zrXj#td4!Td__0J64Tk{V6H|~lJQ7a@p~?xJSWo1|5~O43B-}jeMRCFKz`|>FQHIPb z5dNY)-~w)N05(Ev={vaU!*L}{=5rCcf$&Apv%-q9;L1D?UIB2SpjZKhcR|O;d1L8_ zg>5ez0-m+y@!osN(H@GE5FY{)7$^&&>&2Xm25_ZayazTA@schS6R?GXk8~|jS+izYf%IEhd9fWo)@1{I?KU$Tt45#!79(B=v3KeT1rl>y z^u33Z^he+F9R~B`=LIu@owC4#$CBiB&^YE;+>=q>xS#h@Vm>7EFbbbjKTx4y_ijk4sFYj%Cx1r=cu>Oz5V5en<5tMK;f2t5cyaHKw- zqvQ~L%bUA4v$o?)RkshNw2F4^+^KdueWFAC)NIEFTGW@-K8C1+v4W*buQ4vhrsi#U zY|2(7{8?B5z1Gz4gl&1Yx~Z`yxZoM+jv+HQzv4XG*V*Dm6CtqjE_o+q%QiByU^SI> z@@6}&t*zDhjdrMGw@6la`Giexj@#1uiu3u345Z~H88u=-#`7j&VESvjSo|&PTkVvT z9NO~ij*X6t+qEmVZAss^7E4UkU?o^(_qH{iEV72~3ahTKRNe!-d11?LU)-|YRg;ph z@V-O9bGbL7&QVeK0tqv=iPjBOFhWtTZ`ebEl1Jeb_6dSPAqKh*@V2Oo40S<;{2qoE=<|@*sx8FkJ!cQ z=4R|*e%7`LLovnB*kl!*-QM2%@UU$R-?H7sMJrM#r8m<0yo;3TUXu)kTFJgjDew9Q zJ8Y8IDG#GUZB#5wTj}z=l}f>stf)M2s?AdMDe|OL%B5Hy5IF3L|4RPWfBm1m#ByAY zQ)j;iRyYicP@(`#?3=Iz0D_WxOboG1n7IgE(b&}yZ##s+VRYsYU}j?aXnD@z!fNI{ z9*lmNlNBLYR@8^(ji6*-2y+sgT*-U|llv$Stg&5n7|5_$57;Pn#$@^DcXtD)%GiOCtCsAT>F%OBhkZcMm?6N(EOAl#WiXKx8G zv5E}Ez~nwmgD_`aXK``Gy&)793Xn6(P^efgTv(B;5F^Gxdu(LH8=`*ZOz=V|M=X|b z9k_r)>Kn?l$^#1~jx&gOF8#@z3Hx0-T3dvVghOw$J^aw14GwnLn{N)uOJc$@gc8<0 z^Ct-Ybc8v02d~)fI=TFaFajAayQbZ4S z;3JsGgBOCbOY=MgqELc|rE00-z~(ucWRm!`E?Ia8kizSDYmHm12%c>X{MBtX&=$$d zD78*03WOC(g|HQan*oAgsh6-h)?97PHPF7u7T0%eMaqLXyRQP0uu5|vTwu~7#dN%_ z);c8^Q7Yu!RKEikLX8zN_x!46P@+R z+wd5h9-flXcV*~~y?0|&LPCmPN@PW2YEFE|>l>CN*wb79AtJ`yj67Y&Q?>Z$fm8jy zy6dg?Zd=o%s}fpkcJsY8>u&9}b7zkE*n(GCHH6aGDPFbt>T5Q;F>ULs2aEuNe!lSi;>oAM${%J$^ZkJy!~S8RM}*Ou;<+T5_l)|L#+<^!ueziF+f zWeB#{%h)NkJJ)vX^*<6GclLbgHSBBdq zK<72yWZ;R9FdE@qPSwDmWc(u|5!z|46l0k&pOVWo^+~C4Kt3T0*@RhHU1K#*K5J!X z9`LZ_x@fmMJ>^QYOagvS3Zv@8DXZ)swB1|RZTIFi+n<`Wm7BLbq}BZ7Q&!U4VjI)b zwsP@;6v?o!%t8qf z$dh4?q`S{bVWUUP4BA3nAEI!knrQ1XvFIp#ceLv9|-O8_$P#gRq*59q1fkGBz<~7cO0M zuYyA(PoEsLsi`@eoWVU86Tm#G^Y}oIZ(vNP{$f?(Rd@_ro#8t~1s7#PVDGz^S<`TM_ZLpR6$dnko)X_Hd%`*=Gg zoI)I)LcBi%B@m$}R~FDm`IxUjNl>29mcLU)8IyAI&=!JE$(58oX@lp4uv~FSL7*t& zIXRf4NZ~H5(LE{4Qv1+zr)4}e_#ok!pQ*VeyCmVdwh>1~kcfdJB$zJQ^k^+(-r;iw4;R!?6XbXk#t+%f!UxoE|v{vZA=X?3x>ozjAU@NSKVmu0GVBwSVw?6*dBi1Xggc@Ic@3L)_E!gQ7Dy_XwM%8Yq zz43Wjx79^8&!` zye-(J(3f|%?d}S1lw<*^nQogerjvPehZHNyUvnYR}3>cw|L!PCTt=qfe_NqFofBZ?SJ8{zIM3|@8nVYqp>+jj# z#F!O{AM2lb*2~Sk_NwTyZq+@#*7C#?R<^!sd&5IkD#cTF@~oAp{LHt%Wh+BBtp5B1 zR@L2OhvK1~OK;ik@J)xc{Op5P-rH~ccW%o()Hv=xZWXQVRbk+T)@l=+q?K`Ar=tELJKZR8!~( zm|&U&6HMd6NQ0HhtQ|41Yk3VW@FCQO*MOq%mn$i6z$?pMpUhvd@{GEHDfbL`9TzMH z+~8;j#RANcFO-eL4Ne(8u0(}{d zNN@ooN{6^L_@FR?wn_QKxj0xx&?wz^I8{FuQcDXbAvaiCTfLn;(Pd{&_u9#m-9A8Q zKfDIPpn8+%;FIGn=%X)rmj3sS%cbmImf(;7e`D_%BUzT+`JJ#CnHlN5udFQF<=a-< z>F#NcA%||3nMlE7jiK)_-lK!5~&QGXC12ofX#3rJrU(g#5xNbPFHrMS~G7`8W4 z-PPr7W>tFcZA4^b82x|e=Ig2%PHTCqUq?o~_pUkj_;c>P)I~k~&A6hAN12OYwJ?t0 z_CugC4^Llu_3*ulwYJE*=xx;)WQj^^ZxZ8emT+xtthX*%vfX0Jonpu}8Z%5BM&nwn znt*}3gNur#xSb9 zo>jFT*hld6ku})U?y)PPBh8sz7K! zA6V&ZvWIJgj2bM`PAM)PMDJ{MO9lqUmM64Hdoa9=;DDdLKGk6LaS5!$}T|JW`+$h3p=8} zotPE&I3q^Cl|&OTO2MU|@g!@-#CJ=!I8n6CrF}cdl`UOOSY14AwNhAcWo28XX%EU0 zZxJjLf<-~|B3+RwU_Knyx_~uS1#{ytKb#wAlX*$&6ZjSMEJ^|?#>_c*Nl;$@X;2a- zvg%2R?P-@%s5z+<&DM!t>5KC!)dNj)@e(*2Hlq#q;X`<8;GkyLfBO21{Hq&qsTMpi?-rPZqN4rnHyv(nb46*o6SU|kFz)oXuk)pD~l zwl52&BA&*XBLu<|*#=9rb$C3*TG}k$)MCkoMxVdR_NJ93w90$CP@iXmsgq6tj(;hA zm$ibMO-x8aNn;XD#t7sei2vxlig44nk;huVI>4oZ#=N?=>4!;)zYVGvmCXm54K%H&Z9Ix~im;X(h-WR5VnBruHCw4o0~fxm%u$nnlgO{Q4kWIQs%<76u6ojFH$3))1~N zVnOr~?UnffVG)?uZr=0yn6KkUha7pC8-xn;NA5asfiW>2gb|r2|JT3%+kE26{wC&6|X4k`rW5e|Q!O&h}s{9yTDb%c{3!yG+be3kxT zUBCCkXYK6iK8+_VW@RS>gzeJ>m&^xW-M6)^-NRqZ1<28M&Cj9Wc)GCN35@@62wr^a z4KYaHe&gvIhlRKb42nXXXvGd+)dMqyp&`g<*9DrM{j=YD%LUz<@MTxlCPE@ulrS6S zui$e^nl7#ga0a(2iVQ(rEPC$eq%C5tg6mkQz;91@1MZQy!ttumA@F(cI#Tw4; z+1H=TvbYnoW-%QYIqs#iJ&l4%+-5fNwz`MHQ}sAgS7VK5RLyM4(rzl+nK#R}IvumA zTV@Sid%l3Mez(<<5?E2`yPUG#R}!|ly=K$5_ige@%2wuOJ!)PM@R3k|xl>C9PSA(F zjZIlSQfM`X!q>mB;^bpJEn#^2yd_VZw%Tmg(z6p58)IerrWoar)jt2SXKPhA)~tH- zD;IWl?6l=?U9-ad+umN{_kZ-=cWEFi(}h98yx12D9?jgIZo$bB2gS#a#sA z13Wf$ggLRl7SE7Uj8=>}2(#mT0L*<{Km3LOf`Jid@bxOHpI8do6zeTu@-R~f1V#xH zj55AFa1MZV4??1T>hiTANJjyQ>@5n`(nqL>xj|sCc7=sd z^qYEN*0>1W%2ON6S*Nt+=Pn(ymoJ~zct`HwYW$SGz9(!q&Pftv`;!Hi+@pk0$eDO5{B^0!GQEL76S!^ zrYNQt;D+^9br(nEEuaQp&m@|G!3Ap`ZBiEPq5G~yxWfJMxfOdfvt&z}Yv9B4)I~Zx z_nqDjBn)%HwM@~rSMxT1XWO<{_N-Af(%;l)&FvZM91#ri67JhEJJ?Rz!mQ|c3p@#y znfvnvKX#v8&j+SWL7=kg92l{9vRanFtZ3eLMS~U5e0E>&l%=RveOsHoc+uCFWCx{` zRtt9XvwfSmq495Nj1>egJ0Ax@fYnL=u`ZY|;wC*!8DD%zwjCBI^FoXZHzWPT!azYF z6RuZ+phwnWS4)GeS?DUPVT100GbO-3WJXdD`tT6Y72M-Flw1@XcXP>VM0h~ooFAV# zGGfV7XKh!wxVyUQI6$4?FqvDv0==!>b{|QZlVy<>?P8r2x3;W0K4vx}2A|utl*S6b zpdZXYqPYhmsn$y#Y;s?)?_Rpi^x`%to-1%?Jq7^d3VR+d&JdREY{j)Rg`KpYX}#V zi>wC_%sVPTz*Hepw7z9ARi5Fuo~{n}$wowvwZXnr&aA}n5hM%<1m!uHgl9Bp@&N$W z1e4A?oDC17gE{gn8k74GbwOZQEc6A&iys|gAvc`+VUc8DU})=+yUmYQRr?ee9TN>R zW-MH^2jjWN@{kY!Qd2+H ziziK}UWNkRL?B_+F&>yCL~wMt%iehXtiAo#1v~T1kPD>XS_(|wT~4|X zB2P~81M;{LB4Kjq2GhOhLKyf%hedZ?&|qcoe8|Fr#fEpov{CPqI{41m>5I>=ega1r z8_#=&jK;(K0UuHvkM?!iiJ=}F>F;#w4>!n)1jgd_zAbDQe(A!3z>@GRtiw7H0x%^k zj}ZF+7QwZYmbOi%P@A;tHZdyPWMp;mE{u~o;eGt>kQH%>!Jc-XV{i`z2$(Py{*QYz z+TsQm2krA4V?;Re8*>DHaJgGkI7B6(KS7B=u>*X(>-SaBP+y1B+sfvygvqi^FRXgv z$NmAk5A2I3ff;kBH+O2yR-43FQdSTXT@gMfG}n!?GLxH4*51%)Bj@X^zFByX(|G4) z`AC>BM*0XI11n+`>;izx3u}bJ|8p7-f}5>MiutM)=7oQ|HP$FTfiS=VT-97G?iOqv zA6C1p^V+;;A|72ZApMJ*$ zCZ0!JsJgad71p5aY+HGA-Kq;S?s`l14)_|D*#G?B{+CslpcARC3+GOGCn+#c5StRM zBkAqx)Hec-r64SXL2OL|fqz@UtVYm40G#tdoglq4F##HD2Er!;i*rzzluy36;mi_X z(KlRRtm%LWk^#0Mrig&xeG2X;GPN4S)FV@p05{u`0C?)e{S>)CSVw|D!)hRy3S(uw zQ5M*@jR!Y`3js|sRvHNep&w+Q;ZD#biO3Naxae>hjy;;P$CGnz(ST$WPGIER0W6*! zAqtug%phu)-+>G6xM++gjtv=Sm@qc7!GI@7OqwzC)6&%BFh!UmIM9yR{TNt(TLCU0 zHG-2fRu~hjdFczt3B#J7TerXZt82EodAK>`Q>AdQr!j}*27=?jBAr0EJ#lTJO>^vS z5C~Fs(+IhsabunQ>N=8dD<_Y2+xOo(D>#PqYz{WU#K)pU`C;6+RAIcp5dqJFSC}_* zUkI$89QuELblJ5fiA~Yw?s7O)I(dry5{$wKwwRS z6IiumA4YgX@plCqXez@F>Zhly+VsTjw>4F^$3KxUohL&xZKuziw4M06-Ce$EOXFf* zdv(@PU*kd-cU4nuTGp5twP5mib*F?y!2z1gi0@EkW^yMzeYOl&X0LS7W$s30(~B7q(Ahw=t(t!;3~ zRj^hlz&shj?mESpGKO*U(>i!WIzt+}~&#S;JIkALS~OgOeCM~fv+5*3h-!i#i-U><;jZanxa zf&uBUP!K9G)yQOa2B%5!c!r(@@c}5r4&f7jp*Yz{jLyG`rA-0p%lT}X@#$7<2yvKK#8|_ed7bb^=L;GBei@^jqN&6LC8IN01 zf-h|}$-<#+9)wxY7wUjv&}Ywj((fRzTVc9K_8m-$I1Y73%mp7V?n@uH`i030T!A;j z3s)VkFXA=KIsNJCYPO!9W?NrJ!3aKPw>S>RRDxI9;lP8uV-x)j4L|0U>fSSd4jtsOrA=6LZ3R)`fp8+RfY8>#x z`&rG`K{alR8+*1Y4R}xUK(Xi% zMu8Tni|@2a9sZJv?|+vO&ZJ_tzEre{`+GL`s9@WxWvk6ZzGL|v z!Mdz@(LDm&tk|c&xX$>SWCU8KFZ9XXf_k54fF1X6A@Mg*1WpJV?h{YpOqaasz^Z7d z7yas~twsGlTe0j&rE!qX3*3NZWI;fC5x(JJfd->e z-#=j4;Sm8s%{?nTMF@HPhE;*`p)g4r@!2QZ!@g2e`uC zTh--7D~ylX&K!$+4y{t}vuH_Uk9GB0Rr7c-KWp**py&pE5w5tNb$7h_mH5y9`JcUu zOQyTC&0Qh@kir0{FfyTtX`rc3&(1qSqq)Q2xELtE-+wsn8aRzGnAP=lw*uN)n_ZYh znlViD$UyJmg1f9ApoK}wxd1GpgYlxxQ!f+7CuSRM`E2SxG-vwA1yLc8Skdi8&yZ3K zBj(-k9EcpAMOa{o@Y~Yry1xs8*+!%d6Vm~?@qq(ZV94OZ z+=v0|o5!KlZ{~)zCTQQl6lTl3(LY>ToGs(#HZWAdtw$V)c|}+)f*bqvVPk<=!_-eSu!qo!Vb`A=AougV9?+*;|9i9 zi~Nm3l&Px|zPH=#FWB;S-h-`C*3YkA)Wc##Tq7)0o0SG0rS{gkrYlWcpCf%8 zc1#w;$)R5FC-<4#ATVU5>2DZNv}HIcmXUBL9z|>oMn=pMmjrH$RS9m~UOWdZ7&B?- zK}jh%4yb#s!4g}6Rzea&(U}o=ic*16g4L2qW-Og=wy|qD`{ZZycK1sXR4R7*yJ}Mv z?JSgSb8W{~m-lRIP2(;G_gu8)3)6)g(1m~)HgSsJ)(iNHAaRD~eI*e?^QW=vL;Vnr z=~>{pT`1X{;K;&3u!2}qyb!nLM+devCTnIoVH-;clfrG=dDeQAt);JOXQYS^U#MEP zy=n_HQt&q5-9f0~qGW9jbwhZ>YYSjDTMV5^#t)bfK+Jexu!}G+%t)T4g093OS4IZ2NO~N}$PHzG!lrfx#dIB}DddMM?BNEl4;&q_D7HN#R7><(I8a_h9VFo_;@|sx1Cp zoSU%<2U7`0oR8GiThp^ISj!79S@z^JzV-wqv~>TDfF-=mHdy7sZQH(c%a#{bETR6^ zAxLDI90_-pIy?XZ`gKW`J0^8$Q0+$mxb2rV=#k^o?+8TD^;eA0ys1*yp3Fg?k< zg3;4|Vi5SFVU_?9VF01hF5`jFh<(7o(3A3kU?kBS;`z{w52I zHn{JBIn@v2#*Ouht2gbn%a;VV275d?s~#Q()YV4>70;0+rNOydkllY2v_8hfgaBY8GAf!Lxa7>#(a!nLrfkk zlDUa2A_NFW0w8RFKf>jw?|)VSEXy5u9BBcE;k_glFl-2sq+?xAz&skYcN_j!-R+sBfx|)IpGM9TrQ3DG|#~l7$`uZc#J?mamlUsBH@9y z2=nE4BtHZ}d%R0#4ik#IE0uUcD4IW?ESQ?U`h@z?k6fx76CFeefJSo*YXxA|Fk$b< z;dm%K2NMBNqVe!sBt&?gL6ZT+c(7c^9*bb$b?I*im+|mPpLmY;z0azX^8jG{eij4c zIc%GYar$nZ`o}(Ez=*mTGzc2BbzB3XK8SWkMzhA?DaJ5{@zYII?R!;{WhY{!GTU2&^3s^Uw!d5Kt7*cjhwnQXb-{@$d8rW z1=HLuOZoT_>+bKiw(d@=>FBa*O-9U2%w4dk%VzC&e*6#Zo$vif+Ea}zjYid&lujo_ zL{?BWo|ZDhYW)B=#zf=57$(IU64sn_9R^yvTbuORXq{p@+(RyT%!By_Cc2o9tj5qN z!PFp}=R%-xL0n-Rw8cCG?Gok<9Dy-#U1HaXL+Wku4;`PeCipY;nreV+P&~!LL#ihS0BW{ zxuVSesb2o50h z0&JiMEIzCeAEPcFVxA*j!FYX+poNxJOExxJTr|#hK?yP3OlOzHHGjn|Y3ZVS)_t({ zBHiBM%seiojCCC~V&DnR_COhxg_572_e`kj`kKY_IUh)@KI>Ui!JWnUnpByco)7Tjn_&WQuuRw0Z2mRYF1Af6^G1E+DKsoA$xF8 z+kj}mFmK#F4Xg#@M`&cOaMxXHK|v#CjK)7yE6& z80j|xfa7{WDi{V^gfNb=@hP`%@c%QO$RdG3V=?r}+F`>*&NsoeLqVwcgb1Y<7T27h z84E5iVm!UlAOqtMS!0ZY!dMs^;|a{4V^D|Or8M?<$~d0@SmWO4QM;_0x%=R;6Czd@ zeW%VwX(Ib#_Uu!J2F*MCwy?D9sk|_47!8HAUfKi>Y&*|buid`yguq^8xL3G`fF@Ya z4zRe~u#nJry1Uyg95^OTOM-=YfT-vP<6$=e`u`XI_j{g-P5Ebk`Um#hg=bsnQ()dQ{G|T zgBusuS*@+79@|{}j(z&qd0TlLw_2Ew=s7A+uZKZkv}J2#!IeEm#5N*WA{5+xob655 z5dY=u8;sl{G+QVo^qycWr(v z?^Y03R2pSj9O-Ja9$6xc6-y0%gy3D5#U3rx{k8&(So?v3$Xu{a1m^ATRLyrnjI{Z? zziD~R&C-V-xC^L$aL9Up@I%|1p0+hv1lbFhtoG=L*T-gxRayUwKmVD>McQ9})ehAD z_QQv^bN#9nSC+kAw);oGZ*J$T_3huVwD5QJFVocI z+n}tg&HA(Ftm)i&KYN7t_9iE+`|WouRob`R>t9*@h37oA_u%@MW*lmzwl+3*EphaO zwZ8JY9Xz~k2ls9{4y9#X#g3k|%DtPOdYm6TX60<7a8=3>nW*!V79StA%E&1z_6%BH zw-)~6ERcj;bLcFOj*WRCw(0qpDp(i@w2d+3Fnuf=ei5L4>s^ zOepo!Hg2#;)1z?KIs%G-J!4_4WTdesgys^wdf!H1U~RU+TeNm)F@=5uLuj~&|@U(KlRXm zeq(I3NzKGwKK|@$PwQqYeh%T{J~+Tx79A4!2szs4$`@Gh80PiYm21}@8hbO*)(4+l zwHvn|sxMeg(pbf;j=%Gkb&L%A*)hIhpZHaLn$)4BRXe&Z&IWVRmiF1>CzF+>Ds4>o zbMyK&J9+Yy<~VN+jg5Bi?p-^f?+@=@we#f0RKEG;A z6tN`)48jM&K-w*5z_8wiJ`0t+;J(_BAi*t$1wrAHf4z<3A!?6;u%SN3NqwhZr@OP> zCpODm+}iiP@H^{2`r5&XkoW*oCSFBBn5gTn5Mlu{{-`iFSnR+kD#3arwLZ9(p%<)W zKknh^F-!ChS-hv;b|$B6?H8Y0YsBp>6I#>o2`Rbdq^!|a zSq#G6-A9jXcT6}hA>u301*2x+CNxQ$tKro*tnSFLuM62{^}701H!vt0TMFIWjOE#+ z69q^Dj}+W;Op0tFXX%y}t3Pqd4#YDG3v-q@5bvI!vz)A+n$B*kALzFXO7yCf8P$Ez z-fh*sAxpNl+s;>CSZVaGc%We1(`(k&>2Vg^ym|E@A^h^)N(7s@a15_NG21L&D+Nq_ zO8h5(_J{AX&lN!BBqH{V0+Df31x+{Ri}ymnsNgBJJj-`J2@vuLL z>jaJwGPr!!q(vi$m|%|^a2*m9fTT^2o#=Oj6P=%$SqM^F6GmI00{8JFBOIugNn^7` zVj?g;hz%_St&YGpZ9sHr)SUA{|8X&3Rk+(teWGrxEs%|jB@ieu-g@spu?Z3cuts3g z6f|@C76fI=+u;f;xN!rKO3PL(9Q#|ZwhF$MBfv){uG))s^(Fp5`;3J(C@?6}r6a#2 z0s^EbxC)GUj=AA?=7#>!57L1V0@Mvm=>zZZokH7TJax{jiRB;w*hh)D65D^{W{Kto zc>eWIKeR{V)1H?64}bgvJAGnUT7Nh#4u*;F=J_C$a8-W$HGHJko;fvaXHE~>=;KMd zKl)hx-*e5qQ<~(F<5l+KwM{q-Wb~4$qOq@a-Sc$4*(g|A-yTOj#jQYk&Lc zm-fX6@7vPcoISXI&mN79+TA<1Z9xQd=k_f(*#~+~Bs33?)&FpC7a5r4Q zxEKlGhAoO9P~WicrPnRf*=ecGWe=!Q2SSdtRG17xhyFD*kVuhm{A64(V=PSEWQ+lP zp?;J(o@bu0W>MtUq=|ldb4-fMsx8lzJ@W~-5*7lML3AM~5Eva2?i@QzP#l3lY=Fgn z?4FC;D#S>L7YJ@HF=E!=!e)vYzef@S8(k7BVNqb%Ofd)&E-VqUu7=y|J&l(&IUyU2 z3^57l!};Otb;dHip7(lyJ@58NsGl6{wno8~`DMP~OUy6XNx%;Ul*8qK6-puW10Db) zS0WAzbu#zxz~~wHMva(forFC5RIwM<-rT&H^sMhm#jY}BOl5leoRRO0KD7M9`GZFkY!bS>P&DY=Jq_k zlIR=o46W@)V|K8-w`xR5eXVUSx_*spMP}487Nsv7tfw@js4O1j885X#E7-P zBE@w{LkQ9*OdZG6W-?$j9&plk*RJ&)0%MY3E)d$<`i4)S6X4;b!%P7%R)ohcq-{Z* zff)dikV4Aj!_a2umpy>oEXj%njq7XVNWZ(t`un=<_T4dOh@34#V3{$FX)LTEq0l$3 zv!}(xq@CbW!WG9@Tu_Lh7?UrK6oT_N?b1H(Bl^uV9G6S_AY*|UL&&(;fDPlpQUQT+ zBf6HV=fZe`rsSDEg3;rNS%(=yb##2n<1qBSue;L)$Im|aTyMmDK_!8IkpJP>q%#nH zXKt`Ch6Z|^u!4q5o56<)Q85nSPoWEF<7}Y$!6#qp_nccmzxln_{ZvX^UkD27#%1ty zxq0WI-MTmC0udNQ+utC>IKKw``qkpDH4!OrS;{onf_5TYm(j0eHVeB#=` z^@&g*5rp{x_dV_S1r@mZ)quBoFNfgS+tt5JHCS}iicRCv~7a@oOco2 zSfmNlclw6)hk}8Splz5bv;~bYX8H{M@Gf}D`#i(n^!<}-4{c_7+2FOoEfMAwAr0QGu5USvDfAWo0=$3+f)~pUI^bTT1Tu<% z3t8O*hV;82>x;y%=GWh{#xrNEMsvd)7dJL+|HfC+!l!*sn%;azNZf3T@BiF#Gc%UV zHdxR1f8cy-_v$aK{`6U^JAcVzB&%P3>6A ze5;lS4uW(4fHl8x*&2q2rML(dntL{KtZ=06)`p*cSoi!ZmfYR4vXnP>S@jKC=Hl~~ zoS$~Qv@@4{+Z6h{_r=F{u(58bXD(XecYpZWyR74Y&>(oUPuIMqZNfBQTrgvZ1}zJV z1V%{uTQCXn9|nIv6M$w9<0E|$tl>A%4}@iK)WKx)8|yk4D8V$|gZY8rkrodl_SNS? zXzGcyWSFB8t-Roxy@g+~Zv)3Sy8dF`?63m5ucX;bv+Hgind%A!z^6x*2l@`P;Bgcj za2VVK2WSgCf^kRgEf1LLnY?gbeBZ55e9p!qPR&QEJ}V{^7Er2V{;URW#x@iUjh0X! zlWHrepTP&1BZWTGCq~4)@H_YEcfiXi`62}4Vanhe&-zgHzr2Jkg08tiG^D4k ze~bn9B3q2$=0bo(f%9mODhl-?5FD<;V_(ru1L_F$g5`nL6Xq~vWpT0cAA!XCSShT0 z=Uv9nT9l~hQEQiS*df8emM`sxDZKCj?lB%tlTvT04XmoSV_um48>S951mx?H!1KXSw5w;RX;DD_Q>!`y!QO@cef;s zIy*bWpxWF7M3aPp!1O^VTrm;xI|EaNXqvcGSlmUh8wL^1RTg^Sn)41xpz zgbd?(G&$>E%M5oazn3Lw>tt0NX>YROfo?HHEYn~S04HF~e5G@{(wd|#iHONy!79ld z8MgG%;}#n@V*9P#w%^!h3HBahr&JLb_{CVM2ONzAK6pgGBSG?X!4<-9z|q|=YM(d> zOb_qv?!Yc5Ly`T-vJDMtQx+30)(y>}h&KiCo4(N={Q;)TH#qM7kix5|rE8vUF>^HJmtUiOwENN-(8GFWX~dwl_Ix$?bIsnWUAa7;$2v zkj`CN4breWj~=ns*Wa|p7ca|d8L<5QI}%c>mO1~d^PtU%Nf%VaZk>0tjXk_1w{0m| zO&nqc9VF@-tY&ybv~$8@9o>Eo{yqYa9fZYie7(x*qQxaFD!qf2zjw>>Uth6<$;X!3 z&r67PT54d(V*m2L`A@3=m|zm;0yEJWN#Sf`i(4Wf+OZ=8K{|_v5U6!+P~Xu?Vc?vJ z0g*6~epb3B2gV8WW>TkS7knZ?o`L>u5nafznv)R08ag>LWFe}rd;2m74y!Q@y_ zhh~rK455a$jKB*FThEGl1+6>g+B8h@)bU}j4=aOrutFFQYi1B~XoN6e7&8ZOQ5X-w z_U-KuCt{o+7-PX@gxEDebm@O==`k0s-&f{S0?y^!p*+Ccu z0QexNfwdEh#!SB;BCIa1fBo-%Cb%rA{R3YQg!KloAESmEa&r zfVoElMxasnJDP{65Qk!pc-9$%#>M8>1k8szrEmyV z+~7Fh_U7PPU0dH4t%jW@oF8eN2o3(`cN86F8TTZ0GGE*y7C`|P;A2fj%2(fjrH`Mo z^@pRj`S76~h{oV$zz!O$(f#JjueckhxV&U-&pj`TqQ{Dbyj4fGxG*Q&E_+&XLPCcv=kxkKFY5+; zajWc*C)J$2sQw+d@{EL^=(X)IqO1-U8^R-@>(p~En|1YC<p)@-)$Co30@VU?6d1eW1np`VoCb7xnuY>#7q1Ak~#;p?#2s^$!dJSBz^_ zst;nts-bI=-8zv%-h!Q2Z0x4?YqCv8G35R$6$XMhWc#JCtMzG|37QBsDHu12bnVv`_Hb-6GP^vbbr)yqN&K8sMSvkb%W`AaY(#^pOJX{#I_EcpA>*Mg#sgeJe~2#y-@4bQcB!9$ITz`?8}5<{Cpltyan?PqEQALz zgP~9-ZY$D_fgw8$hh2nO%o%n*AZdX<02AO5UBou{bcQLIz&wEmNfKMC6Tu48WsI;Y zk4LE<;7Fz!`-gEMpkT1R-UKsV&#|dpRm2@%U$*Mbrd4F6RE5Lw{H`U0&t!IiHv+g! z`Fe|GnCE=Vrr+H2KCODhG8TL||a_r$g2|+nca^fR9PfyAV>pojL0}IL_Z(&@%2R#tz+pUnn5D7?(xB+6?+a{~>(GNm(JdjY_ggiel;qiwn~H*KA*Nv#-ypa4wDj(F>t{ zS#iQ!E*b=nxPn_(^sWBvOBfXw=B>K7OM0iT)o?{XbHP}W&COQR-Q(wmsFPNO_o#hv zFxAlLTb+owt)ll3Yz0=eb1sOKC^l(iT(v!YvTnpX)wc5h33BkJnBb^g@nCRkbtxnR zXbh#zO~fUhr5Czvb>OawnZ8d&rWrq+q0PplBu zrT{=nSa(rz%K_vFu7gcyFu2=5o?lmZ9|Q_QZr|5}(3VS7F(C>Jj0`LSk!+!YwLpLv zrWpi_d6EHXx=RwGSUx+NK>7&+QaFy7ctDiWOc|7wMMfEYqYW$$`U3(p9#Ww>Qnx5( zfQ1Ev=8O_dYx?->iW9sL87lxLjuqn8hQ@=yLfd7$xQJL!fR%Oi#ywkK-*Pxdu)T0r zX0zsNO@ar3fdKe-|Necilk#8ui+}2yVMcHTIf#`YB%*aOJp1Lf+deM(`<+*w_co%! zyWk24_NxV#Cc-u_&VYajQ(RUQ&Q`&?Vk{&JTz>w%zeD>7OJMM;C4c6k6{HF6eej-@ zA%w#MGi7~*U3|&PU47yHiZpqZx_5rdVm!BjOJXy^!I z@PdAWQ_F%IF%PU1+M%$4D9izx!>tI82jM`LPo3ZuVrDR{`sbxlo;h#ZfBT-hLBNfC z;lLMu_5c2lt!7t(@Ltd~(ViEU)`jyaYv^dTdi6b3*ppdy)Am**G+Wv&^U9l6uFYCe znr!9DN0!=H^3+&&FNuDjU$?f@C;G=YD2&B3|FB-wFA7#B3K<1BfL~Ga1qviMiW)Wgk=C<;_Nh5+NDjsadDiiSAn(BQk6OtzA zM^}4`;}2^<$SA^Mj0!Ze<=dD@V2PF*bul)!C}OUnf;JAV?P=_#Qq>xo+1xZ(&KZ=J z;Soz-eBPF?UbWp@H?1X`aeUm`&0FVh{2qaBctUFRUT!bRt? zJK=xVTW?!+dBO5GzqW+<5yBcCzPuus)i+tE?kD%QZ2#8RR?X!sIds%A&%R_OS#A~4 zQmw4x^1`h1IruZU?e5Y){L$<0dhZSk0E#qVm~OB*q|r-TS|>jmiF?REnFtsx>7*sJ zKUEC#Mbmb(UjU;aG+ne-7zr^07RtG1t^$JNcjh^?Q0iu~KzNAKHGDD8U=fk-y6o#m zK#b^p4vXRpcYwk>t}zP9h?9_djDMecV5ATi^}F_tdra>K2^+?Pka72h#*7eSlGKU| zS?Wb7QC|RHS^_pKOr&1=;ueH1v_lVy>Ur9L7=w$9-^o)am9b)zQ*+K#=o1A5BP-5f zt8s&T-Zzbl%q(1Fz!R+k7(qa)-uuZFo)5x_Pj(^3wRh#~+rE7ZLizNGqju)hG5)+%1!7L$e~7=qNyO*HS|ZfS4F5-n{O z6U?i*JxdLZc+i@73g=L#_qMGfLIR(qma0dZJK=C7xR_t&C*TM1tB~;qbLV$p4c|0r|&k{)kCd(tHe(i~2M(u!$ zSThxkhyHQYXFrg$K2KQ}cc%R`of;0C;& z)UO$)&eqm&N~O>#P0XO7$#JbbKkKec0^hvDYV_2=5qEvDR)kF+GX&nnU$HO z`b_us`#P42`eSRWZZ(3Z8iG%}Z(x}ee&{pa)M}}i3UyvZ>IQ(T-fLxX4x45`ueSQ7Lp;7>o0&=n)I_4(5@t~;?5dve&6PV_p zw0wjCE-BJLVFVy_w#ri5fe3xuh{Oj2V02#iRG@D8i(*_oFBI>^oYiz{oMkpz?B zIW%{EC(DQ!28{+zquQo+?F zX4lu<<(3E9Kk))G+Q^PW;9(6Sydohn~BUAl1EQ-~2F^o{xAa^jFy&nNxh;&)(4fw&nfzejM4+$#b88BaGS z$$`FcmJ104{LQ+X9>Es@hVTY&xLU;b+pKJznqSveH^OFhxMo=NhuL1;-n5!aFIk-J zRF)SlDaH^9lGKKXNP?s!oFO*}_W|xLg7V-&q-9~1&|k(&Opf_-_=-08{)CyBwaDUV zy7atlj6JrJ=7zb@t6qFwB}BHTXDlPDr0&!+zAJ0J7(%AI+lnIo>guv3w$^P^LZ5oE zGM#a%d#s)DEVNT+Jjp;Jz=^k<5JNl^B)B%dVd7%kxb<+e0ef7h&?5}gl@o-5`UH)` zM46YUFefNNj??;wTNio)*OOu<4eD28W20RkU$F6|O}8vqTk^C3i7v+P*@Eg1N8!$I z%hJq;x8|R1-Py(@+DsA4l66JkKgt#TBr_3VMw}9i1-G~t2}9-$TnxhH z$(XVu$F231?^sf>iU}`3ckqU6J5Sb;f|;A2bHR~4cfo35?t?+dOFmj&@F$as1%AIO zNQ$_(ggY(IK49 z!RpjTos^bTYnz7#$`Vj<;To18;{oTHZ!E_YuDZ5%U-6!nU=!Ok>+JEQ0@ju!L_5SK zBcWYxXtv7mi6^cteAP?{rcp%jta1e5$cK-9BjbnP_`9!9l2sCv{Ca}4Y-w_Dblj1T zLPI=<8A+g;XXr5ZSq&eA1S^S&Po&*FK;1AITpRq2xyyn{7=FJ@z_Ia3&&9uR_Jjif zhJ%#?vv$jbwl(;dU%2250O0a23l;z0pMI$KPT850BhJ7XQ^XJvbO@X%2E=wIFpjC& zd2dI<_=d@Fk*O91w_*G+buzfZxWn2F_dlyI1dW+sm@6yMIfSaOr(0uA`7y%ezYp|w zyAUQ;K{nLJ#LR!4~zlk&&H7V$0qD|fAE&zRkF!xA&15aqX0Qr4|Gdo zq|dCCdF4eRl;FTvS(y&Jqk=1tH8&4Nu|UGckid*Zk@O!y2P0uVD2#)8f&i7wJ!F_@ zcM!2Z6(04%eemGv!gt_Lol(7f2X@@!f*CL-#>_S>%nt?lU?rivAZ(cjk|%Kg-5(uu ztLq>pY4g$>mdfv0dScXy`JB6iYF>KHcRnRBy7%*c?;2qG;tQUCe=z-6z{q(L1%!!T znFI%z7^Xj4l%QZRl4v}2{e#x~gCBXu4a^$cC`~@L!mY2ZyuGdWIlD#-K{&3#*x*0< z6DemR`0HP}f1RvH@D|~~;=KsxiCgmCzTRu77oL$-Ba0{Y;I`S)ytt^8wDZqe>ewl- zH#RG)Gg-MeA&_AZO*d->`o7F(Qg;>T@P(AWcv!#EIFB*{<^o(LG+v2Im~ zhM@<3i{>jlN5DQX)79k_+tpaP&lV^M3j_&PLbUY>|NQe14Muu8+>J$b(Xqm~dcYg{ z104f@Vl-G(WW#ky*dRz&*R}$l=~?;@9+EJDG6DR_8ZC)omul*){_-1AA_g6opbHlD zm2O_Of&>H03s_S^-)O)2jki6Hh1+LmV#2)$O;VEM8przT`e8b4XfqlId@bAGZ`IT1 z{DeUp7ygNs5`)9m_R`CiZf^Ef=D4QVsIle3MOhm+Kz8f;&YNz1tbKLGl@DmL9);=h zD<0F^{rZZm*Iug~IpJAZczjqySgmh#SR#bk_%VOymL?<|2Mi8E8x=A5jv$ns)9ySuwSfxZEv z>H^siO4Ne}#{|Qe(9R(&5F;<b`*y%US43`3*>XtKO zU;{G^aAC1s<2RNW>6I{F-`ZQxL3p%H(~JiOOC}9WiRXYL_vkNhAd3wmM{w{i@bIk% zR2RRau@h4Po{W!95vyTdh9o2ivYfW8q&eqZ0?CZq*TK})TSfRAssv|&nd(m9RuJ*CT?La6hfWR`cVZ7#*c=cDh~6kzFGaN%6rznglS!8 zho!oEtlHG7{%FkVR|S01yd(5tQlhKyF3lT@8Z+WG72y;cJOX1s1reIlT$8C)6^}@_ zv{_QLim-`^QNug()03EBzTcK4ns2VixL6&}T9A0Q(XDTADMs=@YrB*wDJ-(4N=pk? z+1;|3-h)y~SaA~4tb<93Ckr9Ow{@vc6}@{Pr6Rw*WqVUntTdkb-hQjaN+GjLeemQI z^^J8Uu3YGQ;`e{>>bq$3NHUlJDRgKESO<}L2!bH3KtLv&iN@`!vZEpkMye#tnVn=;6S%)#ni!gcou667XZJ(wDU3tS%VTXcS67&3kv2A zoMC*pho)v&_$NzCFu+w+wPCYMm zYQ%6dC(k(1u{A{Uh1YG5ZL`-&M-Pb_;68to+aDK`ic<%I|2x&Ra?XX>V%M8dZPSci)kw9QRC{%JPEk$Vvu=-Lm9aSA;t>_0sEppipsn(NdKI z%SfnI&s?-hSHCktwk%Jme{9!LSy{Bg&1+UniJ`Q0T4G53o*B1C z4hx^f`(f%%|6*u*j{0$TLA&5*^IPAy#PBIwotU=Gr4`@Gr18`vt8 zw08J5JGIA7T22B3!QJ|uxBTAj%8G4`J+i{OtP2&aBd0mhXyc`4t@ZgALmKe;Vj>)9I(f=6C0T+G?pov7vzF=YwcOWVTBbvm znd&5&0yh~}SqWFH7<=~8i?*xZ7C-n~+nHamr236ZG(P>v%3}}2@TKeuA6Vm2|IAsp zen>vao<3vAqsQ$2Pv5i6TQ{s#Fe+?s$P(GMS}8iX8u#^X!wWBaOo+vgC@3-EZDr)N zZK?hB&)&D<%90eH_6FBD#6_^Z&2oX>hLXU}uv4CJ$K_Fy= z(O5Cjb}L9z5SD;BW+TjsoqGzI?dtpV%)G}S*rAtxu^13d9;S>C35bsisoRAQ zRs~~Aqf1wETW?Sj0X*jegj)93fiF1=AZwY zpLo_A8r-F`C%rz}Mu2y9wmJh~t`LCuM}f;%*YCJR#+=Qs^@T9tj@C`gC2)DbE6H{(ic^nDo-&x4&DQv1x7tj*yy8n;}@tm%#>q*`O^o|F;I*2wRA-l^2xN+pz{Nkiq#@Z$KWmDX1*bLy;FPWL9p-1qO+pkRtH zjugJaME7B+oFp~4Lw<_#PSyF2hMzwc-atlp1O>Tyqj+dRik#aJ%z5(EHkk{t$0 z5JU%eq5^$FA5WtTxBwo~p3}5St%N}B5eZN<^{zh4t*qMWCm;G5<7of&{rzH=y}m=h z&g`rNz?#Mb|H2ZL)rAGv+~zCGcgIF;@9~%mdIH?^r@5)#ad~GaXPFDnT2_K7CJO*p z)PZ2vTdcITVR3b};oJpFsXt}4zk#44=CZeCbuBHv zCMB+ZY|YQx`t93NOu`u>#KqRXzGhq3uUe`qC8@1Vf~V7i^lTfyK0fXnMAAV#Rz}ivIqN z6a*<`SR;a8?a;8V<}a_V1Y8o#D{Ns8y+A(*0*D*Lz~$iDrhW^P7|c`h<0F$Eq=GrY zNL{1*-{oMgjn#!?-D!wv2~< z(MJR${YS7d78oQ}3G?E@+zAuQ5MmG4)byN9>RMP>_OxZvecb|SA+sxN?E&!vXD^3v zUJ}90%`d4BvV7EE#>)JD+r@q0L|=hFjN4ryD!{}U(BTF1h;WCCaf2t|)YDhQw7{ir zyKt3K2LhZJ9O>EM!ou>Z3qtPEm&n3_$#B(6m~|(r)*mnXRkyxuvCVZqCjus%5))0x z)K7?^q$FI(pRY=jPs&2^y!z=$OGw*G_6@n)pmuQ3nogau`r%`~8rcaKhJofjGiB1o zEGfc_NdQ!3DX@J+L3Oa^fUPW8D0U#6fl2uqky1tY7Vr{R9h(#G9mvWPaiTf1(mBpS zN%~CG$eI_kui09&l!QZaMOyUyv?Un37*M(_wxIFA;Hv6tMc09}$?AqIV?9%rl~ra{ zwU|&GH(5iIk1Hli?ZAQpz}zy=;5zsP{X(-8rwxsh`%wu(CQ=ME&#qxqtxd|@@qsQI z>F=}wSslHd&DI5dHfKd^H7abk zGQB1%#Znq_&5>hP#iN1SO8rU+2iSQR?Y=C89r)hwiXfZtE??sadyfIQX~rFk6c-) zWgj)*$`OuI0Ad;OIaw8X;l%Ew6b|NEbHsX%gcx{A3>z0Hf-k9gr`@t}6u76wt4P<~ z7p?(&Yi#nhE&DUmuGHivCafr4!?C@bA5y}?05|n4UK98L$s~K~S586@*VVd&=E~TZ zZD`zhVCH8RYtY;w83go7KK93z8a7~oObo9-czAo3OM3@L67!AHtga~dd+KdpC zH6So|5EbUlbH0s-7%C-Xa_oN-yo(T;F(VLQeh>ib9{?`q{><#6t*YKr$B(#%j5YAd z7uTF&_w{xjo;=v*YgU+03S*%^r!`LEKxm^b)DSFcH~6EaBuf}UeDnkBjG+2o{@eFl zNb%c0`_q5m#}@l)eSQCR1wr%C=T~icd0o%M?bXYdTzLF_3k-w)xjXk37=LfXno=&p)*NN268|v&cMi z&gw2aXT>i+^*xSC$InTADYMKP7!m!DZ# z!V>0-G6lYZ|E>gfN?4w`XfQFm|Y6J>*4B_f4~^*Y&X zSO^VXp?ENkA{#>P%c6Pl6>B8JN?JI5CZ19F;v*|hjr;ozFTLtp=FI=>r?&OrzW+Ax zyT5M@eSNm0=V01dS)?RhY~8FlU%}@>| zADfMwv^480B*+R^KDFJux7>XVZ-@#?F5_-{`!_7pH(N`3-;e&1J(}ELYL0}x*x#3nNeQbQ=yRHx}efYkw^I#oG<162>?YTM6@ak0mS}#58 zb#8z9q2(tYTkVDCZDwN99tuAbvP?NAKNTx`OpDzHIMan~X~^E{YiqQQhFVMeeUzM( zbqH4)Wm!cfA*H46#aAsQd}S?Go-K%~Wcbxs;z#eiq;68 z2#c>i3aZi$43>j{U_7qLpc$md)(HFPfMg5`#D;iC9fjHPJnkIo&?BCy0Z|}4007LK zv&F;WH?*UWp@fjZa-j|g1Ivf&L(=c`fvrs%gj^6jTc1Dx2t5QcZL!5R zL1Pfk(;IbpW{c{=4aUlM>Wl;@1dXMGKqc-mJ~8Wo@qTIL$0U5Y5V$N3#NQnWI~JIc zeT8sdSX%KQI!poLCymx!L5D&ItA+8<*QYOFg(gUyZjQMy3&NUvzw|uga^Vng;6HeY zaexE#f%ik4l5)!V6D3kP z^?VgPS3Qo0dIp&CJB9UP&>1vB0hgnD6lk5kLMPBEpVR}6F;_IoCo~SDE)L~lu(Wi!W;s_>IXbrEQ;kyad2UwnNKI4H*Fntf-tmE%d0*VY+*uHjNl9Z*~|$) zRKJ?{^Iq7g8EcX?J*+E9f&aP*e7IQSR4aj;85|Cqqzd1wYCkT^2^>ub*PzYB?|kp_ zyJ!S3AP^5h!VP!dD^(UcKBWIzSqe!Xilw2!rdHGwt`pg}~05KJIq zhHPJg21Q&30wQS0*78^f4j>UZlS?81niKtgG%;-pi%3@?_+gzq7Z_N=if)FZ8HdF_ zP7ne&vq@EM&5NqeLL(`T31P~W}$8AABVCMh9%jYH3&7!`*aGuoX=d) z1z!xvUQ3m@V5Z+2kB(TQm_&Bum}exhW+lF|;8|MieYH0;b9kcNWm~&((^h}+g$VnB z?Mr)O^4gz!!QB@*DNXFTl+6M z*Kqc{+3b|%Z(Ozf_+#6UHL@`;OJ#D(<|ITG*S4hWNx>3~7PdG#Ashq1mN$Ev3sSUE z%h-S*L##>ym^cbCuS9pBwY~a=WzSx)xbSgbLdq4TQ)h!~c#9ZK%>F-Cu`|+;4y`Z% o000hUSV?A0O#mtY000O800000007cclK=n!07*qoM6N<$g2Y06Hvj+t From 91529fa5fcb6cd1da426e8e5a24169f25dd0edb7 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Fri, 31 Jan 2025 15:59:07 +0800 Subject: [PATCH 012/919] Edit military module --- library/include/modules/Military.h | 1 + library/modules/Military.cpp | 101 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 013f8044151..dc69dac52bf 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,6 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 0477c366b02..23fc15a8717 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -4,6 +4,7 @@ #include "MiscUtils.h" #include "modules/Military.h" #include "modules/Translation.h" +#include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" #include "df/historical_figure.h" @@ -289,3 +290,103 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } } + +static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +{ + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + auto link = strict_virtual_cast(hf->entity_links[i]); + if (link == nullptr) continue; + if (link->squad_id != squad->id) continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + delete link; + + start_year = link->start_year; + break; + } + if (start_year == -1) return false; + + auto former_squad = new df::histfig_entity_link_former_squadst(); + former_squad->squad_id = squad->id; + former_squad->entity_id = squad->entity_id; + former_squad->start_year = start_year; + former_squad->end_year = *df::global::cur_year; + former_squad->link_strength = 100; + + hf->entity_links.push_back(former_pos); + return true; +} + +static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squad) +{ + std::vector nps; + if (! Units::getNoblePositions(&nps, hf)) return false; + + int32_t assignment_id = -1; + for (auto& np : nps) + { + if (np.entity.id != squad->entity_id) continue; + if (np.assignment.squad_id != squad.id) continue; + + np.assignment->histfig = -1; + np.assignment->histfig2 = -1; + + assignment_id = np.assignment->id; + break; + } + if (assignment_id == -1) return false; + + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + auto link = strict_virtual_cast(hf->entity_links[i]); + if (link == nullptr) continue; + if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + delete link; + + start_year = link->start_year; + break; + } + if (start_year == -1) return false; + + auto former_pos = new df::histfig_entity_link_former_positionst(); + former_pos->assignment_id = assignment_id; + former_pos->entity_id = squad->entity_id; + former_pos->start_year = start_year; + former_pos->end_year = *df::global::cur_year; + former_pos->link_strength = 100; + + hf->entity_links.push_back(former_pos); + return true; +} + +bool Military::removeFromSquad(int32_t unit_id) +{ + df::unit* unit = df::unit::find(unit_id); + if (unit == nullptr) return false; + if (unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; + + int32_t squad_id = unit->military.squad_id; + df::squad* squad = df::squad::find(squad_id); + if (squad == nullptr) return false; + + // remove from squad information + int32_t squad_pos = unit->military.squad_position; + df::squad_position* pos = squad->positions.at(squad_pos); + pos->occupant = -1; + + // remove from unit information + unit->military.squad_id = -1; + unit->military.squad_position = -1; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) return false; + + return squad_pos == 0 // is unit a commander? + ? remove_captain_entity_link(hf, squad) + : remove_soldier_entity_link(hf, squad); +} From d88277fa3fe6f4ef7ca7991859ac0478126e9b94 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 31 Jan 2025 07:57:35 -0800 Subject: [PATCH 013/919] Update changelog.txt --- docs/changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc88..1bb7b42bcc4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -37,7 +37,6 @@ Template for new versions: ## API ## Lua -- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed @@ -58,6 +57,7 @@ Template for new versions: ## Fixes - `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command +- `createitem`: output items will now end up at look cursor if active ## Misc Improvements @@ -66,6 +66,7 @@ Template for new versions: ## API ## Lua +- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed From e6e1972fb9bd1b97c27c255351f314170d7a8689 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 31 Jan 2025 07:59:53 -0800 Subject: [PATCH 014/919] Support adv look cursor * Update dwarfmode.lua - Make cursor fns support adv mode look * Update Gui.cpp - Make cursor fns support adv mode look * Update changevein.cpp - Use getCursorPos * Update createitem.cpp - Use getCursorPos * Update cursecheck.cpp - Doesn't use cursor * Update stripcaged.cpp - Doesn't use cursor * Update zone.cpp - Use getCursorPos --- library/lua/gui/dwarfmode.lua | 25 +++++++++-- library/modules/Gui.cpp | 80 +++++++++++++++++++++++++++-------- plugins/changevein.cpp | 13 +++--- plugins/createitem.cpp | 10 +++-- plugins/cursecheck.cpp | 7 ++- plugins/devel/stripcaged.cpp | 1 - plugins/zone.cpp | 15 ++++--- 7 files changed, 109 insertions(+), 42 deletions(-) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd5202..80d30282cff 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -7,6 +7,7 @@ local utils = require('utils') local dscreen = dfhack.screen +local a_look = df.global.game.main_interface.adventure.look local g_cursor = df.global.cursor local g_sel_rect = df.global.selection_rect local world_map = df.global.world.map @@ -38,17 +39,35 @@ end ---@return df.coord|nil function getCursorPos() - if g_cursor.x >= 0 then + if dfhack.world.isAdventureMode() then + if a_look.open then + return copyall(a_look.cursor) + end + elseif g_cursor.x >= 0 then return copyall(g_cursor) end end function setCursorPos(cursor) - df.global.cursor = copyall(cursor) + if dfhack.world.isAdventureMode() then + a_look.cursor = copyall(cursor) + else + df.global.cursor = copyall(cursor) + end end function clearCursorPos() - df.global.cursor = xyz2pos(nil) + if dfhack.world.isAdventureMode() then + if not a_look.open then + return + end + local u = dfhack.world.getAdventurer() + if u and u.pos:isValid() then + a_look.cursor = copyall(u.pos) + end + else + df.global.cursor = xyz2pos(nil) + end end function getSelection() diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda99..ed6ddcba7cb 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2744,9 +2744,18 @@ df::coord Gui::getViewportPos() df::coord Gui::getCursorPos() { using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return df::coord(); + auto &look = game->main_interface.adventure.look; + if (!look.open) + return df::coord(); + return look.cursor; + } + if (!cursor) return df::coord(); - return df::coord(cursor->x, cursor->y, cursor->z); } @@ -2911,7 +2920,7 @@ bool Gui::inRenameBuilding() return false; } -bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getViewCoords(int32_t &x, int32_t &y, int32_t &z) { x = *df::global::window_x; y = *df::global::window_y; @@ -2919,7 +2928,7 @@ bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) return true; } -bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setViewCoords(const int32_t x, const int32_t y, const int32_t z) { (*df::global::window_x) = x; (*df::global::window_y) = y; @@ -2927,32 +2936,67 @@ bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) return true; } -bool Gui::getCursorCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) { - x = df::global::cursor->x; - y = df::global::cursor->y; - z = df::global::cursor->z; + using df::global::cursor; + bool is_adv = World::isAdventureMode(); + if (is_adv || !cursor) + { + df::coord p; + if (is_adv && game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + x = p.x; y = p.y; z = p.z; + return p.isValid(); + } + + x = cursor->x; y = cursor->y; z = cursor->z; return has_cursor(); } -bool Gui::getCursorCoords (df::coord &pos) +bool Gui::getCursorCoords(df::coord &pos) { - pos.x = df::global::cursor->x; - pos.y = df::global::cursor->y; - pos.z = df::global::cursor->z; - return has_cursor(); + using df::global::cursor; + df::coord p; + if (World::isAdventureMode()) + { + if (game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + } + else if (cursor) + p = df::coord(cursor->x, cursor->y, cursor->z); + + pos = p; + return p.isValid(); } //FIXME: confine writing of coords to map bounds? -bool Gui::setCursorCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setCursorCoords(const int32_t x, const int32_t y, const int32_t z) { - df::global::cursor->x = x; - df::global::cursor->y = y; - df::global::cursor->z = z; + using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return false; + auto &look = game->main_interface.adventure.look; + look.cursor = df::coord(x, y, z); + return true; + } + if (!cursor) + return false; + + cursor->x = x; cursor->y = y; cursor->z = z; return true; } -bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getDesignationCoords(int32_t &x, int32_t &y, int32_t &z) { x = selection_rect->start_x; y = selection_rect->start_y; @@ -2960,7 +3004,7 @@ bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) return (x >= 0) ? false : true; } -bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setDesignationCoords(const int32_t x, const int32_t y, const int32_t z) { selection_rect->start_x = x; selection_rect->start_y = y; diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e90..f143a25bc11 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -1,13 +1,14 @@ // Allow changing the material of a mineral inclusion #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" +#include "TileTypes.h" -#include "DataDefs.h" +#include "modules/Gui.h" #include "modules/Maps.h" #include "modules/Materials.h" -#include "TileTypes.h" #include "df/block_square_event.h" #include "df/block_square_event_mineralst.h" @@ -21,7 +22,6 @@ using namespace df::enums; DFHACK_PLUGIN("changevein"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); constexpr uint8_t NORTH = 0; constexpr uint8_t EAST = 1; @@ -212,7 +212,8 @@ command_result df_changevein (color_ostream &out, vector & parameters) out.printerr("Map is not available!\n"); return CR_FAILURE; } - if (!cursor || cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor detected - please place the cursor over a mineral vein.\n"); return CR_FAILURE; @@ -232,14 +233,14 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } - df::map_block *block = Maps::getTileBlock(cursor->x, cursor->y, cursor->z); + auto block = Maps::getTileBlock(pos); if (!block) { out.printerr("Invalid tile selected.\n"); return CR_FAILURE; } df::block_square_event_mineralst *mineral = NULL; - int tx = cursor->x % 16, ty = cursor->y % 16; + int tx = pos.x % 16, ty = pos.y % 16; for (auto evt : block->block_events) { if (evt->getType() != block_square_event_type::mineral) diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 1a720c3f4c5..1f4b5820472 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -30,7 +30,6 @@ using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("createitem"); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(gametype); REQUIRE_GLOBAL(cur_year_tick); @@ -84,7 +83,10 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t out_items[i]->moveToGround(building->centerx, building->centery, building->z); } else if (move_to_cursor) - out_items[i]->moveToGround(cursor->x, cursor->y, cursor->z); + { + auto pos = Gui::getCursorPos(); + out_items[i]->moveToGround(pos.x, pos.y, pos.z); + } // else createItem() already put it on the floor at the unit's feet, so we're good // Special logic for creating proper gloves in pairs @@ -395,11 +397,13 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { auto unit = Gui::getSelectedUnit(out, true); if (!unit) { + auto pos = Gui::getCursorPos(); if (*gametype == game_type::ADVENTURE_ARENA || World::isAdventureMode()) { // Use the adventurer unit unit = World::getAdventurer(); + move_to_cursor = pos.isValid(); } - else if (cursor->x >= 0) + else if (pos.isValid()) { // Use the first possible citizen if possible, otherwise the first unit for (auto u : Units::citizensRange(world->units.active)) { unit = u; diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d739125..958e5712717 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -1,8 +1,8 @@ // cursecheck plugin // -// check single tile or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set -// if a cursor is active only the selected tile will be observed -// without cursor the whole map will be checked +// check unit or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set +// if a unit is selected only the selected unit will be observed +// otherwise the whole map will be checked // by default cursed creatures will be only counted // // the tool was intended to help finding vampires but it will also list necromancers, werebeasts and zombies @@ -38,7 +38,6 @@ using namespace df::enums; DFHACK_PLUGIN("cursecheck"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); enum curses { None = 0, diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc309..25f764841f8 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -35,7 +35,6 @@ using std::string; using namespace DFHack; using namespace df::enums; using df::global::world; -using df::global::cursor; using df::global::plotinfo; using namespace DFHack::Gui; diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 612e042282d..1e69b7e193a 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -55,7 +55,6 @@ using std::vector; DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(ui_building_item_cursor); @@ -844,14 +843,15 @@ static void chainInfo(color_ostream & out, df::building* building, bool list_ref static df::building* getAssignableBuildingAtCursor(color_ostream& out) { // set building at cursor position to be new target building - if (cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor; place cursor over activity zone, pen," " pasture, pit, pond, chain, or cage.\n"); return NULL; } - auto building_at_tile = Buildings::findAtTile(Gui::getCursorPos()); + auto building_at_tile = Buildings::findAtTile(pos); // cagezone wants a pen/pit as starting point if (isCage(building_at_tile)) @@ -861,7 +861,7 @@ static df::building* getAssignableBuildingAtCursor(color_ostream& out) } else { - auto zone_at_tile = Buildings::findPenPitAt(Gui::getCursorPos()); + auto zone_at_tile = Buildings::findPenPitAt(pos); if(!zone_at_tile) { out << "No pen/pasture, pit, or cage under cursor!" << endl; @@ -1069,7 +1069,8 @@ static command_result df_zone(color_ostream &out, vector & parameters) } else if(p0 == "zinfo") { - if (cursor->x == -30000) { + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.color(COLOR_RED); out << "No cursor; place cursor over activity zone, chain, or cage." << endl; out.reset_color(); @@ -1081,10 +1082,10 @@ static command_result df_zone(color_ostream &out, vector & parameters) // (doesn't use the findXyzAtCursor() methods because zones might // overlap and contain a cage or chain) vector zones; - Buildings::findCivzonesAt(&zones, Gui::getCursorPos()); + Buildings::findCivzonesAt(&zones, pos); for (auto zone = zones.begin(); zone != zones.end(); ++zone) zoneInfo(out, *zone, verbose); - df::building* building = Buildings::findAtTile(Gui::getCursorPos()); + df::building* building = Buildings::findAtTile(pos); chainInfo(out, building, verbose); cageInfo(out, building, verbose); return CR_OK; From f72118ed26ef9487b16faba11c64f2d366b1bc9a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Feb 2025 04:13:35 +0000 Subject: [PATCH 015/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index b1aa3b365d6..6a952903e72 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b1aa3b365d611432195b80d1d064c942f54a78c7 +Subproject commit 6a952903e72199e469b44e0c74764c1c89f4b2e3 From bca91ec460503fdcf02ca4b0a6c60fff8932e521 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 20:47:11 -0800 Subject: [PATCH 016/919] increase the timeout and retry delay for steam --- .github/workflows/watch-df-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index d86f9934304..6e723330344 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -31,7 +31,8 @@ jobs: - name: Compare branch metadata uses: nick-fields/retry@v3 with: - timeout_minutes: 2 + timeout_minutes: 5 + retry_wait_seconds: 60 command: | blob=$(wget 'https://api.steamcmd.net/v1/info/975370?pretty=1' -O- | \ awk '/^ *"branches"/,0' | \ From deea1b0ffdfd84f4e5517b4216a6aff3f6bf4560 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:43:19 -0800 Subject: [PATCH 017/919] add sand example to changelayer docs --- docs/plugins/changelayer.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plugins/changelayer.rst b/docs/plugins/changelayer.rst index f986a1e3d19..44fa1a3279e 100644 --- a/docs/plugins/changelayer.rst +++ b/docs/plugins/changelayer.rst @@ -22,8 +22,8 @@ Usage When run without options, ``changelayer`` will: -- only affect the geology layer at the current cursor position -- only affect the biome that covers the current cursor position +- only affect the geology layer at the current keyboard cursor position +- only affect the biome that covers the current keyboard cursor position - not allow changing stone to soil and vice versa You can use the `probe` command on various tiles around your map to find valid @@ -34,8 +34,9 @@ Examples ``changelayer GRANITE`` Convert the layer at the cursor position into granite. -``changelayer SILTY_CLAY force`` - Convert the layer at the cursor position into clay, even if it's stone. +``changelayer SAND_RED force`` + Convert the layer at the cursor position into red sand, even if it's + currently stone. ``changelayer MARBLE all_biomes all_layers`` Convert all layers of all biomes which are not soil into marble. From 95b56d96f8bcdac0aca1338b8cb11e2248e4de77 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:44:16 -0800 Subject: [PATCH 018/919] add more boolean options to parser --- library/lua/argparse.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index d9ee57d139c..a76e9d3c6d8 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -237,8 +237,8 @@ function coords(arg, arg_name, skip_validation) return pos end -local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true, - ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false} +local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true,["enable"]=true,["enabled"]=true, + ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false,["disable"]=false,["disabled"]=false} ---@nodiscard ---@param arg string ---@param arg_name? string From 149f5905d29a05a04d7677fb64cdf3b811123ac5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 01:44:42 -0800 Subject: [PATCH 019/919] don't add suffix to focus string for non-difficulty tabs --- library/modules/Gui.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda99..951482d23f1 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -817,11 +817,12 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.settings.current_mode); - if (game->main_interface.settings.doing_custom_settings) - newFocusString += "/CustomSettings"; - else - newFocusString += "/Default"; - + if (game->main_interface.settings.current_mode == df::settings_tab_type::DIFFICULTY) { + if (game->main_interface.settings.doing_custom_settings) + newFocusString += "/CustomSettings"; + else + newFocusString += "/Default"; + } focusStrings.push_back(newFocusString); } if (game->main_interface.adventure.aim_projectile.open) { From 974e5ddc1f3d696278b18fa73d689544504ba8c3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:01:35 -0800 Subject: [PATCH 020/919] combine into one file --- plugins/spectate/CMakeLists.txt | 3 +- plugins/spectate/pause.cpp | 187 ------------------------ plugins/spectate/pause.h | 76 ---------- plugins/spectate/spectate.cpp | 242 +++++++++++++++++++++++++++++++- 4 files changed, 241 insertions(+), 267 deletions(-) delete mode 100644 plugins/spectate/pause.cpp delete mode 100644 plugins/spectate/pause.h diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt index d2de072d8bf..3f205171dca 100644 --- a/plugins/spectate/CMakeLists.txt +++ b/plugins/spectate/CMakeLists.txt @@ -2,7 +2,6 @@ project(spectate) SET(SOURCES - spectate.cpp - pause.cpp) + spectate.cpp) dfhack_plugin(${PROJECT_NAME} ${SOURCES}) diff --git a/plugins/spectate/pause.cpp b/plugins/spectate/pause.cpp deleted file mode 100644 index f94b1e73a91..00000000000 --- a/plugins/spectate/pause.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "pause.h" -#include -#include -#include -#include -#include -#include - -#include - -using namespace DFHack; -using namespace Pausing; -using namespace df::enums; - -// marked by REQUIRE_GLOBAL in spectate.cpp -using df::global::plotinfo; -using df::global::d_init; - -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; - - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; - -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); -} - -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); -} - -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } - } - return true; -} - -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } - } - out.reset_color(); - return true; -} - -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; - } - return false; -} - -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} - -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} - -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); -} - -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); - } - return true; - } - return false; -} - -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; -} - -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; - } - return true; - } - return false; -} - -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; - } - return allow_player_pause; -} - -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; - } - return !allow_player_pause; -} - -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; -} - -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } - } -} diff --git a/plugins/spectate/pause.h b/plugins/spectate/pause.h deleted file mode 100644 index ab736ed531e..00000000000 --- a/plugins/spectate/pause.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once -#include -#include -#include - -namespace DFHack { - //////////// - // Locking mechanisms for control over pausing - namespace Pausing - { - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; - } - namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) - - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not - - void Update(); - } -} diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 36bd2d94b29..29ec27718fb 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,5 +1,3 @@ -#include "pause.h" - #include "Debug.h" #include "Export.h" #include "PluginManager.h" @@ -40,6 +38,246 @@ using namespace DFHack; using namespace Pausing; using namespace df::enums; +//////////// +// Locking mechanisms for control over pausing +namespace Pausing +{ + class Lock + { + bool locked = false; + public: + const std::string name; + explicit Lock(const char* name) : name(name){} + virtual ~Lock()= default; + virtual bool isAnyLocked() const = 0; + virtual bool isOnlyLocked() const = 0; + bool isLocked() const { return locked; } + virtual void lock() { locked = true; } //simply locks the lock + void unlock() { locked = false; } + virtual void reportLocks(color_ostream &out) = 0; + }; + + // non-blocking lock resource used in conjunction with the announcement functions in World + class AnnouncementLock : public Lock + { + static std::unordered_set locks; + public: + explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } + ~AnnouncementLock() override { locks.erase(this); } + bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) + void lock() override; // locks and attempts to capture state + bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + void reportLocks(color_ostream &out) override; + }; + + // non-blocking lock resource used in conjunction with the Player pause functions in World + class PlayerLock : public Lock + { + static std::unordered_set locks; + public: + explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } + ~PlayerLock() override { locks.erase(this); } + bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + void reportLocks(color_ostream &out) override; + }; + + // non-blocking lock resource used in conjunction with the pause set state function in World +// todo: integrate with World::SetPauseState +// class PauseStateLock : public Lock +// { +// static std::unordered_set locks; +// public: +// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } +// ~PauseStateLock() override { locks.erase(this); } +// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked +// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked +// void reportLocks(color_ostream &out) override; +// }; +} +namespace World { + bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open + bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open + bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) + + bool EnablePlayerPausing(); // enable player pausing if all locks are open + bool DisablePlayerPausing(); // disable player pausing if all locks are open + bool IsPlayerPausingEnabled(); // returns whether the player can pause or not + + void Update(); +} + +std::unordered_set PlayerLock::locks; +std::unordered_set AnnouncementLock::locks; + +namespace pausing { + AnnouncementLock announcementLock("monitor"); + PlayerLock playerLock("monitor"); + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + bool state_saved = false; // indicates whether a restore state is ok + bool saved_states[announcement_flag_arr_size]; // state to restore + bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) + bool allow_player_pause = true; // toggles player pause ability + + using namespace df::enums; + struct player_pause_hook : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { + if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { + input->erase(interface_key::D_PAUSE); + } + INTERPOSE_NEXT(feed)(input); + } + }; + + IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); +} +using namespace pausing; + +template +inline bool any_lock(Locks locks) { + return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +} + +template +inline bool only_lock(Locks locks, LockT* this_lock) { + return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { + if (lock == this_lock) { + return lock->isLocked(); + } + return !lock->isLocked(); + }); +} + +template +inline bool only_or_none_locked(Locks locks, LockT* this_lock) { + for (auto &L: locks) { + if (L == this_lock) { + continue; + } + if (L->isLocked()) { + return false; + } + } + return true; +} + +template +inline bool reportLockedLocks(color_ostream &out, Locks locks) { + out.color(DFHack::COLOR_YELLOW); + for (auto &L: locks) { + if (L->isLocked()) { + out.print("Lock: '%s'\n", L->name.c_str()); + } + } + out.reset_color(); + return true; +} + +bool AnnouncementLock::captureState() { + if (only_or_none_locked(locks, this)) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; + } + return true; + } + return false; +} + +void AnnouncementLock::lock() { + Lock::lock(); + captureState(); +} + +bool AnnouncementLock::isAnyLocked() const { + return any_lock(locks); +} + +bool AnnouncementLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +void AnnouncementLock::reportLocks(color_ostream &out) { + reportLockedLocks(out, locks); +} + +bool PlayerLock::isAnyLocked() const { + return any_lock(locks); +} + +bool PlayerLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +void PlayerLock::reportLocks(color_ostream &out) { + reportLockedLocks(out, locks); +} + +bool World::DisableAnnouncementPausing() { + if (!announcementLock.isAnyLocked()) { + for (auto& flag : d_init->announcements.flags) { + flag.bits.PAUSE = false; + //out.print("pause: %d\n", flag.bits.PAUSE); + } + return true; + } + return false; +} + +bool World::SaveAnnouncementSettings() { + if (!announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; + } + state_saved = true; + return true; + } + return false; +} + +bool World::RestoreAnnouncementSettings() { + if (!announcementLock.isAnyLocked() && state_saved) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; + } + return true; + } + return false; +} + +bool World::EnablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = true; + } + return allow_player_pause; +} + +bool World::DisablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = false; + } + return !allow_player_pause; +} + +bool World::IsPlayerPausingEnabled() { + return allow_player_pause; +} + +void World::Update() { + static bool did_once = false; + if (!did_once) { + did_once = true; + INTERPOSE_HOOK(player_pause_hook, feed).apply(); + } + if (announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; + } + } +} + struct Configuration { bool unpause = false; bool disengage = false; From 1fc6737c1ecb701d90ecdec7aecfabe2f727e7a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:02:32 -0800 Subject: [PATCH 021/919] move spectate to main plugins dir --- plugins/CMakeLists.txt | 2 +- plugins/{spectate => }/spectate.cpp | 0 plugins/spectate/CMakeLists.txt | 7 ------- 3 files changed, 1 insertion(+), 8 deletions(-) rename plugins/{spectate => }/spectate.cpp (100%) delete mode 100644 plugins/spectate/CMakeLists.txt diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ff71159e5ea..8f230ff583e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -111,7 +111,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) #dfhack_plugin(steam-engine steam-engine.cpp) - add_subdirectory(spectate) + dfhack_plugin(spectate spectate.cpp LINK_LIBRARIES lua) #dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp LINK_LIBRARIES lua) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate.cpp similarity index 100% rename from plugins/spectate/spectate.cpp rename to plugins/spectate.cpp diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt deleted file mode 100644 index 3f205171dca..00000000000 --- a/plugins/spectate/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ - -project(spectate) - -SET(SOURCES - spectate.cpp) - -dfhack_plugin(${PROJECT_NAME} ${SOURCES}) From 2c27dc6b48753a1cf599d829d4c629138de8b65c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 14:03:12 -0800 Subject: [PATCH 022/919] update spectate docs to reflect update plan --- docs/plugins/spectate.rst | 152 +++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 36 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1e400a06e42..cd7a8b96c58 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -2,8 +2,31 @@ spectate ======== .. dfhack-tool:: - :summary: Automatically follow productive dwarves. - :tags: fort interface + :summary: Automated spectator mode. + :tags: fort inspection interface + +This tool is for those who like to watch their dwarves go about their business. + +When enabled, `spectate` will lock the camera to following the dwarves +scurrying around your fort. Every once in a while, it will automatically switch +to following a different dwarf. It can also switch to following animals, +hostiles, or visiting units. You can switch to the next target (or a previous +target) immediately with the left/right arrow keys. + +`spectate` will disengage and turn itself off when you move the map, just like +the vanilla follow mechanic. It will also disengage immediately if you open the +squads menu for military action. + +It can also annotate your dwarves on the map with their name, job, and other +information, either as floating tooltips or in a panel that comes up when you +hover the mouse over a target. + +Run `gui/spectate` to configure the plugin's settings. + +Settings are saved globally, so your preferences for `spectate` and its +overlays will apply to all forts, not just the currently loaded one. Follow +mode is automatically disabled when you load a fort so you can get your +bearings before re-enabling. Usage ----- @@ -11,52 +34,109 @@ Usage :: enable spectate - spectate + spectate [status] spectate set - spectate enable|disable - -When enabled, the plugin will lock the camera to following the dwarves -scurrying around your fort. Every once in a while, it will automatically switch -to following a different dwarf, preferring dwarves on z-levels with the highest -job activity. - -If you have the ``auto-disengage`` feature disabled, you can switch to a new -dwarf immediately by hitting one of the map movement keys (``wasd`` by -default). To stop following dwarves, bring up `gui/launcher` and run -``disable spectate``. - -Changes to settings will be saved with your fort, but if `spectate` is enabled -when you save the fort, it will disenable itself when you load so you can get -your bearings before re-enabling follow mode with ``enable spectate`` again. + spectate overlay enable|disable Examples -------- ``enable spectate`` - Starting following dwarves and observing life in your fort. + Start following dwarves and observing life in your fort. ``spectate`` The plugin reports its configured status. -``spectate enable auto-unpause`` - Enable the spectate plugin to automatically dismiss pause events caused - by the game. Siege events are one example of such a game event. +``spectate set auto-unpause true`` + Configure `spectate` to automatically dismiss popups and pause events, like + siege announcements. -``spectate set tick-threshold 1000`` - Set the tick interval between camera changes back to its default value. +``spectate set follow-seconds 30`` + Configure `spectate` to switch targets every 30 seconds when in follow mode. -Features --------- -:auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled) -:auto-disengage: Toggle auto-disengagement of plugin through player - intervention while unpaused. (default: disabled) -:animals: Toggle whether to sometimes follow animals. (default: disabled) -:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, - titans, invaders, etc.) (default: disabled) -:visiting: Toggle whether to sometimes follow visiting units (eg. - diplomats) +``spectate overlay follow enable`` + Show informative tooltips that follow each unit on the map. Settings -------- -:tick-threshold: Set the plugin's tick interval for changing the followed - dwarf. (default: 1000) + +``auto-disengage`` (default: enabled) + Toggle automatically disabling the plugin when the player moves the map or + opens the squad panel. If this is disabled, you will need to manually + disable the plugin to turn off follow mode. + +``auto-unpause`` (default: disabled) + Toggle auto-dismissal of announcements that pause the game, like sieges, + forgotten beasts, etc. + +``cinematic-action`` (default: enabled) + Toggle whether to switch targets more rapidly when there is conflict. + +``follow-seconds`` (default: 10) + Set the time interval for changing the followed unit. + +``include-animals`` (default: disabled) + Toggle whether to sometimes follow fort animals. + +``include-hostiles`` (default: disabled) + Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, + etc.) + +``include-visiting`` (default: disabled) + Toggle whether to sometimes follow visiting units, like diplomats. + +``include-wildlife`` (default: disabled) + Toggle whether to sometimes follow wildlife. + +``prefer-conflict`` (default: enabled) + Toggle whether to prefer following units in active conflict. + +``prefer-new-arrivals`` (default: enabled) + Toggle whether to prefer following (non-siege) units that have newly + arrived on the map. + +``tooltip-follow-job`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + job of the dwarf in the tooltip. + +``tooltip-follow-name`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + name of the dwarf in the tooltip. + +``tooltip-follow-stress`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the tooltip. + +``tooltip-hover-job`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + job of the dwarf in the hover panel. + +``tooltip-hover-name`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + name of the dwarf in the hover panel. + +``tooltip-hover-stress`` (default: enabled) + If the ``spectate.follow`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the hover panel. + +Overlays +-------- + +``spectate`` provides two overlays via the `overlay` framework to add +information and functionality to the main map. These overlays can be controlled +via the ``spectate overlay`` command or the ``Overlays`` tab in +`gui/control-panel`. + +The information displayed by these overlays can be configured via the +``spectate set`` command or the `gui/spectate` interface. + +``spectate.follow`` + Show informative tooltips that follow each unit on the map. You can enable + this overlay by running ``spectate overlay follow enable`` or, + equivalently, ``overlay enable spectate.follow``. + +``spectate.hover`` + Show a popup panel with selected information when your mouse cursor hovers + over a unit. You can enable this overlay by running + ``spectate overlay hover enable`` or, equivalently, + ``overlay enable spectate.hover``. From a065980a1fc65ba0096f00a43ce771d04704b40c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 15:32:09 -0800 Subject: [PATCH 023/919] initial implementation of most of the new behavior --- docs/plugins/spectate.rst | 10 +- plugins/lua/spectate.lua | 149 ++++++ plugins/spectate.cpp | 933 +++++++++++++------------------------- 3 files changed, 461 insertions(+), 631 deletions(-) create mode 100644 plugins/lua/spectate.lua diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index cd7a8b96c58..f49849c8506 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,6 +35,7 @@ Usage enable spectate spectate [status] + spectate toggle spectate set spectate overlay enable|disable @@ -44,6 +45,10 @@ Examples ``enable spectate`` Start following dwarves and observing life in your fort. +``spectate toggle`` + Toggle the plugin on or off. Intended for use with a keybinding. The + default is Ctrl-Shift-S. + ``spectate`` The plugin reports its configured status. @@ -73,7 +78,8 @@ Settings Toggle whether to switch targets more rapidly when there is conflict. ``follow-seconds`` (default: 10) - Set the time interval for changing the followed unit. + Set the time interval for changing the followed unit. The interval does not + include time that the game is paused. ``include-animals`` (default: disabled) Toggle whether to sometimes follow fort animals. @@ -82,7 +88,7 @@ Settings Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, etc.) -``include-visiting`` (default: disabled) +``include-visitors`` (default: disabled) Toggle whether to sometimes follow visiting units, like diplomats. ``include-wildlife`` (default: disabled) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua new file mode 100644 index 00000000000..ba7e9ba7ba1 --- /dev/null +++ b/plugins/lua/spectate.lua @@ -0,0 +1,149 @@ +local _ENV = mkmodule('plugins.spectate') + +local argparse = require('argparse') +local json = require('json') +local overlay = require('plugins.overlay') +local utils = require('utils') + +-- settings starting with 'tooltip-' are not passed to the C++ plugin +local lua_only_settings_prefix = 'tooltip-' + +local function get_default_state() + return { + ['auto-disengage']=true, + ['auto-unpause']=false, + ['cinematic-action']=true, + ['follow-seconds']=10, + ['include-animals']=false, + ['include-hostiles']=false, + ['include-visitors']=false, + ['include-wildlife']=false, + ['prefer-conflict']=true, + ['prefer-new-arrivals']=true, + ['tooltip-follow-job']=true, + ['tooltip-follow-name']=true, + ['tooltip-follow-stress']=true, + ['tooltip-hover-job']=true, + ['tooltip-hover-name']=true, + ['tooltip-hover-stress']=true, + } +end + +local function load_state() + local state = get_default_state() + local config = json.open('dfhack-config/spectate.json') + for key in pairs(config.data) do + if state[key] == nil then + config.data[key] = nil + end + end + utils.assign(state, config.data) + config.data = state + return config +end + +local config = load_state() + +function refresh_cpp_config() + for name,value in pairs(config.data) do + if not name:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(name, value) + end + end +end + +----------------------------- +-- commandline interface + +local function print_status() + print('spectate is:', isEnabled() and 'enabled' or 'disabled') + print() + print('settings:') + for key, value in pairs(config.data) do + print(' ' .. key .. ': ' .. tostring(value)) + end +end + +local function do_toggle() + if isEnabled() then + dfhack.run_command('disable', 'spectate') + else + dfhack.run_command('enable', 'spectate') + end +end + +local function set_setting(key, value) + if config.data[key] == nil then + qerror('unknown setting: ' .. key) + end + if key == 'follow-seconds' then + value = argparse.positiveInt(value, 'follow-seconds') + else + value = argparse.boolean(value, key) + end + config.data[key] = value + config:write() + if not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(key, value) + end +end + +local function set_overlay(name, value) + if not name:startswith('spectate.') then + name = 'spectate.' .. name + end + if name ~= 'spectate.follow' and name ~= 'spectate.hover' then + qerror('unknown overlay: ' .. name) + end + value = argparse.boolean(value, name) + dfhack.run_command('overlay', value and 'enable' or 'disable', name) +end + +function parse_commandline(args) + local command = table.remove(args, 1) + if not command or command == 'status' then + print_status() + elseif command == 'toggle' then + do_toggle() + elseif command == 'set' then + set_setting(args[1], args[2]) + elseif command == 'overlay' then + set_overlay(args[1], args[2]) + else + return false + end + + return true +end + +----------------------------- +-- overlays + +FollowOverlay = defclass(FollowOverlay, overlay.OverlayWidget) +FollowOverlay.ATTRS{ + desc='Adds info tooltips that follow units on the map.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +HoverOverlay = defclass(HoverOverlay, overlay.OverlayWidget) +HoverOverlay.ATTRS{ + desc='Shows info popup when hovering the mouse over units on the map.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +OVERLAY_WIDGETS = { + follow=FollowOverlay, + hover=HoverOverlay, +} + +return _ENV diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 29ec27718fb..ae33031b14b 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -1,716 +1,391 @@ #include "Debug.h" -#include "Export.h" +#include "LuaTools.h" +#include "PluginLua.h" #include "PluginManager.h" -#include "modules/EventManager.h" -#include "modules/World.h" -#include "modules/Maps.h" #include "modules/Gui.h" -#include "modules/Job.h" #include "modules/Units.h" +#include "modules/World.h" -#include "df/job.h" -#include "df/unit.h" -#include "df/historical_figure.h" -#include "df/global_objects.h" +#include "df/announcements.h" +#include "df/d_init.h" #include "df/plotinfost.h" +#include "df/unit.h" #include "df/world.h" -#include "df/viewscreen.h" -#include "df/creature_raw.h" -#include #include -#include - -// Debugging -namespace DFHack { - DBG_DECLARE(log, plugin, DebugCategory::LINFO); -} - -DFHACK_PLUGIN("spectate"); -DFHACK_PLUGIN_IS_ENABLED(enabled); - -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(d_init); // used in pause.cpp using namespace DFHack; -using namespace Pausing; -using namespace df::enums; -//////////// -// Locking mechanisms for control over pausing -namespace Pausing -{ - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; +using std::string; +using std::vector; - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; -} -namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) +DFHACK_PLUGIN("spectate"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not +REQUIRE_GLOBAL(d_init); +REQUIRE_GLOBAL(plotinfo); +REQUIRE_GLOBAL(world); - void Update(); +namespace DFHack { + DBG_DECLARE(spectate, control, DebugCategory::LINFO); + DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); } -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; +static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static bool was_in_settings = false; // whether we were in the vanilla settings screen last update - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; +static const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); +static std::unique_ptr saved_announcement_settings; -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +static void save_announcement_settings(color_ostream &out) { + if (!saved_announcement_settings) + saved_announcement_settings = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved_announcement_settings)[i] = d_init->announcements.flags[i].whole; } -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); +static void restore_announcement_settings(color_ostream &out) { + if (!saved_announcement_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved_announcement_settings)[i]; } -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } +static void scrub_announcements(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; } - return true; -} -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } + DEBUG(control,out).print("removing PAUSE from announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; } - out.reset_color(); - return true; } -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; +struct Configuration { + bool auto_disengage; + bool auto_unpause; + bool cinematic_action; + bool include_animals; + bool include_hostiles; + bool include_visitors; + bool include_wildlife; + bool prefer_conflict; + bool prefer_new_arrivals; + int32_t follow_ms; + + void reset() { + auto_disengage = true; + auto_unpause = false; + cinematic_action = true; + include_animals = false; + include_hostiles = false; + include_visitors = false; + include_wildlife = false; + prefer_conflict = true; + prefer_new_arrivals = true; + follow_ms = 10000; } - return false; -} +} config; -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} +static command_result do_command(color_ostream &out, vector ¶meters); +static void follow_a_dwarf(color_ostream &out); -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(control,out).print("initializing %s\n", plugin_name); -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} + commands.push_back(PluginCommand( + plugin_name, + "Automated spectator mode.", + do_command)); -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); + return CR_OK; } -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); +static void cleanup(color_ostream &out) { + if (saved_announcement_settings) { + restore_announcement_settings(out); + delete[] *saved_announcement_settings; + saved_announcement_settings.reset(); + } } -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + if (enable) { + INFO(control,out).print("Spectate mode enabled!\n"); + config.reset(); + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "refresh_cpp_config")) { + WARN(control,out).print("Failed to refresh config\n"); + } + follow_a_dwarf(out); + } else { + INFO(control,out).print("Spectate mode disabled!\n"); + plotinfo->follow_unit = -1; + cleanup(out); } - return true; + } else { + DEBUG(control,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } - return false; + return CR_OK; } -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(control,out).print("shutting down %s\n", plugin_name); + cleanup(out); + return CR_OK; } -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + switch (event) { + case SC_WORLD_LOADED: + next_cycle_unpaused_ms = 0; + break; + case SC_WORLD_UNLOADED: + if (is_enabled) { + DEBUG(control,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + cleanup(out); } - return true; + break; + default: + break; } - return false; + return CR_OK; } -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_announcement_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_announcement_settings(out); + scrub_announcements(out); + } } - return allow_player_pause; -} -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; + if (config.auto_disengage && plotinfo->follow_unit < 0) { + DEBUG(cycle,out).print("auto-disengage triggered\n"); + is_enabled = false; + cleanup(out); + return CR_OK; } - return !allow_player_pause; -} -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; + if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) + follow_a_dwarf(out); + return CR_OK; } -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } +static command_result do_command(color_ostream &out, vector ¶meters) { + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "parse_commandline", std::make_tuple(parameters), + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } -} - -struct Configuration { - bool unpause = false; - bool disengage = false; - bool animals = false; - bool hostiles = true; - bool visitors = false; - int32_t tick_threshold = 1000; -} config; - -Pausing::AnnouncementLock* pause_lock = nullptr; -bool lock_collision = false; -bool announcements_disabled = false; - -#define base 0.99 -static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; -enum ConfigData { - UNPAUSE, - DISENGAGE, - TICK_THRESHOLD, - ANIMALS, - HOSTILES, - VISITORS -}; + return show_help ? CR_WRONG_USAGE : CR_OK; +} -static PersistentDataItem pconfig; - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); -command_result spectate (color_ostream &out, std::vector & parameters); -#define COORDARGS(id) id.x, id.y, id.z - -namespace SP { - bool following_dwarf = false; - df::unit* our_dorf = nullptr; - int32_t timestamp = -1; - std::default_random_engine RNG; - - void DebugUnitVector(std::vector units) { - if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) { - for (auto unit: units) { - DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", - unit->id, - Units::isAnimal(unit), - Units::isDanger(unit), - Units::isVisiting(unit)); - } - } - } +///////////////////////////////////////////////////// +// cycle logic +// - void PrintStatus(color_ostream &out) { - out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); - out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); - out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off."); - out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off."); - out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off."); - out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); - if (following_dwarf) - out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", plotinfo->follow_unit); - } +static bool is_in_combat(df::unit *unit) { + return false; +} - void SetUnpauseState(bool state) { - // we don't need to do any of this yet if the plugin isn't enabled - if (enabled) { - // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] - // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete - // The onupdate function above ensure the procedure properly completes, thus we only care about - // state reversal here ergo `enabled != state` - if (lock_collision && config.unpause != state) { - WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n"); - // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, - // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) - lock_collision = false; - config.unpause = state; - if (config.unpause) { - // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock - pause_lock->lock(); - } - return; - } - // update the announcement settings if we can - if (state) { - if (World::SaveAnnouncementSettings()) { - World::DisableAnnouncementPausing(); - announcements_disabled = true; - pause_lock->lock(); - } else { - WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - lock_collision = true; - } - } else { - pause_lock->unlock(); - if (announcements_disabled) { - if (!World::RestoreAnnouncementSettings()) { - // this in theory shouldn't happen, if others use the lock like we do in spectate - WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n"); - lock_collision = true; - } else { - announcements_disabled = false; - } - } - } - if (lock_collision) { - ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - WARN(plugin).print( - " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - " The action you were attempting will complete when the following lock or locks lift.\n"); - pause_lock->reportLocks(Core::getInstance().getConsole()); - } - } - config.unpause = state; - } +static bool is_fleeing(df::unit *unit) { + return false; +} - void SaveSettings() { - if (pconfig.isValid()) { - pconfig.ival(UNPAUSE) = config.unpause; - pconfig.ival(DISENGAGE) = config.disengage; - pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; - pconfig.ival(ANIMALS) = config.animals; - pconfig.ival(HOSTILES) = config.hostiles; - pconfig.ival(VISITORS) = config.visitors; - } - } +static void get_dwarf_buckets(color_ostream &out, + vector &active_combat_units, + vector &passive_combat_units, + vector &job_units, + vector &other_units) +{ + static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, + }; - void LoadSettings() { - pconfig = World::GetPersistentSiteData(CONFIG_KEY); + for (auto unit : world->units.active) { + if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) + continue; + if (!config.include_animals && Units::isAnimal(unit)) + continue; + if (!config.include_hostiles && Units::isDanger(unit)) + continue; + if (!config.include_visitors && Units::isVisitor(unit)) + continue; + if (!config.include_wildlife && Units::isWildlife(unit)) + continue; - if (!pconfig.isValid()) { - pconfig = World::AddPersistentSiteData(CONFIG_KEY); - SaveSettings(); + if (is_in_combat(unit)) { + if (is_fleeing(unit)) + passive_combat_units.push_back(unit); + else + active_combat_units.push_back(unit); + } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { + job_units.push_back(unit); } else { - config.unpause = pconfig.ival(UNPAUSE); - config.disengage = pconfig.ival(DISENGAGE); - config.tick_threshold = pconfig.ival(TICK_THRESHOLD); - config.animals = pconfig.ival(ANIMALS); - config.hostiles = pconfig.ival(HOSTILES); - config.visitors = pconfig.ival(VISITORS); - pause_lock->unlock(); - SetUnpauseState(config.unpause); + other_units.push_back(unit); } } +} - bool FollowADwarf() { - if (enabled && !World::ReadPauseState()) { - df::coord viewMin = Gui::getViewportPos(); - df::coord viewMax{viewMin}; - const auto &dims = Gui::getDwarfmodeViewDims().map().second; - viewMax.x += dims.x - 1; - viewMax.y += dims.y - 1; - viewMax.z = viewMin.z; - std::vector units; - static auto add_if = [&](std::function check) { - for (auto unit : world->units.active) { - if (check(unit)) { - units.push_back(unit); - } - } - }; - static auto valid = [](df::unit* unit) { - if (Units::isAnimal(unit)) { - return config.animals; - } - if (Units::isVisiting(unit)) { - return config.visitors; - } - if (Units::isDanger(unit)) { - return config.hostiles; - } - return true; - }; - static auto calc_extra_weight = [](size_t idx, double r1, double r2) { - switch(idx) { - case 0: - return r2; - case 1: - return (r2-r1)/1.3; - case 2: - return (r2-r1)/2; - default: - return 0.0; - } - }; - /// Collecting our choice pool - /////////////////////////////// - std::array ranges{}; - std::array range_exists{}; - static auto build_range = [&](size_t idx){ - size_t first = idx * 2; - size_t second = idx * 2 + 1; - size_t previous = first - 1; - // first we get the end of the range - ranges[second] = units.size() - 1; - // then we calculate whether the range exists, and set the first index appropriately - if (idx == 0) { - range_exists[idx] = ranges[second] >= 0; - ranges[first] = 0; - } else { - range_exists[idx] = ranges[second] > ranges[previous]; - ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); - } - }; - - /// RANGE 0 (in view + working) - // grab valid working units - add_if([&](df::unit* unit) { - return valid(unit) && - Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && - Units::isCitizen(unit, true) && - unit->job.current_job; - }); - build_range(0); - - /// RANGE 1 (in view) - add_if([&](df::unit* unit) { - return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); - }); - build_range(1); - - /// RANGE 2 (working citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; - }); - build_range(2); - - /// RANGE 3 (citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true); - }); - build_range(3); - - /// RANGE 4 (any valid) - add_if(valid); - build_range(4); - - // selecting from our choice pool - if (!units.empty()) { - std::array bw{23,17,13,7,1}; // probability weights for each range - std::vector i; - std::vector w; - bool at_least_one = false; - // in one word, elegance - for(size_t idx = 0; idx < range_exists.size(); ++idx) { - if (range_exists[idx]) { - at_least_one = true; - const auto &r1 = ranges[idx*2]; - const auto &r2 = ranges[idx*2+1]; - double extra = calc_extra_weight(idx, r1, r2); - i.push_back(r1); - w.push_back(bw[idx] + extra); - if (r1 != r2) { - i.push_back(r2); - w.push_back(bw[idx] + extra); - } - } - } - if (!at_least_one) { - return false; - } - DebugUnitVector(units); - std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); - // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) - size_t idx = follow_any(RNG); - our_dorf = units[idx]; - plotinfo->follow_unit = our_dorf->id; - timestamp = world->frame_counter; - return true; - } else { - WARN(plugin).print("units vector is empty!\n"); - } - } - return false; - } - - void onUpdate(color_ostream &out) { - // keeps announcement pause settings locked - World::Update(); // from pause.h - - // Plugin Management - if (lock_collision) { - if (config.unpause) { - // player asked for auto-unpause enabled - World::SaveAnnouncementSettings(); - if (World::DisableAnnouncementPausing()) { - // now that we've got what we want, we can lock it down - lock_collision = false; - } - } else { - if (World::RestoreAnnouncementSettings()) { - lock_collision = false; - } - } - } - int failsafe = 0; - while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) { - // dismiss announcement popup(s) - Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); - if (World::ReadPauseState()) { - // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is - World::SetPauseState(false); - } - } - if (failsafe >= 10) { - out.printerr("spectate encountered a problem dismissing a popup!\n"); - } +static std::default_random_engine rng; - // plugin logic - static int32_t last_tick = -1; - int32_t tick = world->frame_counter; - if (!World::ReadPauseState() && tick - last_tick >= 1) { - last_tick = tick; - // validate follow state - if (!following_dwarf || !our_dorf || plotinfo->follow_unit < 0 || tick - timestamp >= config.tick_threshold) { - // we're not following anyone - following_dwarf = false; - if (!config.disengage) { - // try to - following_dwarf = FollowADwarf(); - } else if (!World::ReadPauseState()) { - plugin_enable(out, false); - } - } - } +static uint32_t get_next_cycle_unpaused_ms(bool has_active_combat) { + int32_t delay_ms = config.follow_ms; + if (has_active_combat) { + std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); + int32_t delay_ms = distribution(rng); + delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } -}; - -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { - commands.push_back(PluginCommand("spectate", - "Automated spectator mode.", - spectate, - false)); - pause_lock = new AnnouncementLock("spectate"); - return CR_OK; + return Core::getInstance().getUnpausedMs() + delay_ms; } -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - delete pause_lock; - return CR_OK; +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + intervals.push_back(units.size() + bucket.size()); + weights.push_back(weight); + units.insert(units.end(), bucket.begin(), bucket.end()); } -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - SP::LoadSettings(); - if (enabled) { - SP::following_dwarf = SP::FollowADwarf(); - SP::PrintStatus(out); +#define DUMP_BUCKET(name) \ + DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ + if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ + for (auto u : name) { \ + DEBUG(cycle,out).print(" unit %d: %s\n", u->id, DF2CONSOLE(Units::getReadableName(u)).c_str()); \ + } \ } - return DFHack::CR_OK; -} -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; +#define DUMP_FLOAT_VECTOR(name) \ + DEBUG(cycle,out).print(#name ":\n"); \ + for (float f : name) { \ + DEBUG(cycle,out).print(" %d\n", (int)f); \ } - if (enable && !enabled) { - out.print("Spectate mode enabled!\n"); - enabled = true; // enable_auto_unpause won't do anything without this set now - SP::SetUnpauseState(config.unpause); - } else if (!enable && enabled) { - // warp 8, engage! - out.print("Spectate mode disabled!\n"); - // we need to retain whether auto-unpause is enabled, but we also need to disable its effect - bool temp = config.unpause; - SP::SetUnpauseState(false); - config.unpause = temp; +static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; +static const float JOB_WEIGHT = 3.0f; +static const float OTHER_WEIGHT = 1.0f; + +static void follow_a_dwarf(color_ostream &out) { + DEBUG(cycle,out).print("choosing a unit to follow\n"); + + vector active_combat_units; + vector passive_combat_units; + vector job_units; + vector other_units; + get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); + + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(!active_combat_units.empty()); + + // coalesce the buckets and add weights + vector units; + vector intervals; + vector weights; + intervals.push_back(0); + add_bucket(active_combat_units, units, intervals, weights, config.prefer_conflict ? ACTIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(passive_combat_units, units, intervals, weights, config.prefer_conflict ? PASSIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); + add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); + + if (units.empty()) { + DEBUG(cycle,out).print("no units to follow\n"); + return; } - enabled = enable; - return DFHack::CR_OK; -} -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (enabled) { - switch (event) { - case SC_WORLD_UNLOADED: - SP::our_dorf = nullptr; - SP::following_dwarf = false; - enabled = false; - default: - break; - } + std::piecewise_constant_distribution distribution(intervals.begin(), intervals.end(), weights.begin()); + int unit_idx = distribution(rng); + df::unit *unit = units[unit_idx]; + + if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { + DUMP_BUCKET(active_combat_units); + DUMP_BUCKET(passive_combat_units); + DUMP_BUCKET(job_units); + DUMP_BUCKET(other_units); + DUMP_FLOAT_VECTOR(intervals); + DUMP_FLOAT_VECTOR(weights); + DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - return CR_OK; -} -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - SP::onUpdate(out); - return DFHack::CR_OK; + DEBUG(cycle,out).print("now following unit %d: %s\n", unit->id, Units::getReadableName(unit).c_str()); + plotinfo->follow_unit = unit->id; } -command_result spectate (color_ostream &out, std::vector & parameters) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (!parameters.empty()) { - if (parameters.size() >= 2 && parameters.size() <= 3) { - bool state =false; - bool set = false; - if (parameters[0] == "enable") { - state = true; - } else if (parameters[0] == "disable") { - state = false; - } else if (parameters[0] == "set") { - set = true; - } else { - return DFHack::CR_WRONG_USAGE; - } - if(parameters[1] == "auto-unpause"){ - SP::SetUnpauseState(state); - } else if (parameters[1] == "auto-disengage") { - config.disengage = state; - } else if (parameters[1] == "animals") { - config.animals = state; - } else if (parameters[1] == "hostiles") { - config.hostiles = state; - } else if (parameters[1] == "visiting") { - config.visitors = state; - } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { - try { - config.tick_threshold = std::abs(std::stol(parameters[2])); - } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); - } - } else { - return DFHack::CR_WRONG_USAGE; - } +///////////////////////////////////////////////////// +// Lua API +// + +static void spectate_setSetting(color_ostream &out, string name, int val) { + DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); + + if (name == "auto-disengage") { + config.auto_disengage = val; + } else if (name == "auto-unpause") { + if (val && !config.auto_unpause) { + save_announcement_settings(out); + scrub_announcements(out); + } else if (!val && config.auto_unpause) { + restore_announcement_settings(out); + } + config.auto_unpause = val; + } else if (name == "cinematic-action") { + config.cinematic_action = val; + } else if (name == "include-animals") { + config.include_animals = val; + } else if (name == "include-hostiles") { + config.include_hostiles = val; + } else if (name == "include-visitors") { + config.include_visitors = val; + } else if (name == "include-wildlife") { + config.include_wildlife = val; + } else if (name == "prefer-conflict") { + config.prefer_conflict = val; + } else if (name == "prefer-new-arrivals") { + config.prefer_new_arrivals = val; + } else if (name == "follow-seconds") { + if (val <= 0) { + WARN(control,out).print("follow-seconds must be a positive integer\n"); + return; } + config.follow_ms = val * 1000; } else { - SP::PrintStatus(out); + WARN(control,out).print("Unknown setting: %s\n", name.c_str()); } - SP::SaveSettings(); - return DFHack::CR_OK; } + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(spectate_setSetting), + DFHACK_LUA_END +}; From 516296d7c3351c778cfd042e12c20dbd52b25d2d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:14:04 -0800 Subject: [PATCH 024/919] add global keybinding to toggle spectate --- data/init/dfhack.keybindings.init | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index f4068a5cd10..1fa7b9facce 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -49,6 +49,9 @@ keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeo # quicksave keybinding add Ctrl-Alt-S@dwarfmode quicksave +# toggle spectate +keybinding add Ctrl-Shift-S@dwarfmode/Default "spectate toggle" + # designate the whole vein for digging keybinding add Ctrl-V@dwarfmode digv keybinding add Ctrl-Shift-V@dwarfmode "digv x" From 70a02dd9c6f17080dd2d90845de797d3b18c1b3e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:27:18 -0800 Subject: [PATCH 025/919] update changelog --- docs/changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3668383d2ba..80b8c76045d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,10 +55,14 @@ Template for new versions: ## New Tools ## New Features +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit +- `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict ## Fixes +- `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled ## Misc Improvements +- `spectate`: player-set configuration is now stored globally instead of per-fort ## Documentation From b934af10a892d05faf15e537a6a74113dbef5bfc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:33:58 -0800 Subject: [PATCH 026/919] add changelog for global keybinding --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 80b8c76045d..d521d967455 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict +- `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S ## Fixes - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled From c762108dade1d7b5a19bd29117b28aaf18bdd38a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:36:01 -0800 Subject: [PATCH 027/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6a952903e72..b6e8e30fb5f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6a952903e72199e469b44e0c74764c1c89f4b2e3 +Subproject commit b6e8e30fb5f98d1f1925f988675471a44a464375 From 41435de0ad02bcca9660797f7683481cdee5a37f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:18:40 +0000 Subject: [PATCH 028/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01094ed98db..ae54f7cf0cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From aa61289ab2d06fab8996ae1f868fadcb0fd89b21 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Feb 2025 04:13:51 -0800 Subject: [PATCH 029/919] Update Gui.cpp - Detect adv look focus string --- library/modules/Gui.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ed6ddcba7cb..6dfe7f920e3 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -848,6 +848,9 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.adventure.jump.open) { focusStrings.push_back(baseFocus + "/Jump"); } + if (game->main_interface.adventure.look.open) { + focusStrings.push_back(baseFocus + "/Look"); + } if (game->main_interface.adventure.movement_options.open) { focusStrings.push_back(baseFocus + "/MovementOptions"); } From d6b16e4d7fe5617c7b6d69ce2fe6a7ee46d66633 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Feb 2025 19:38:47 -0500 Subject: [PATCH 030/919] Disallow changing a vein into itself Prevents infinite recursion from occurring while searching for neighboring vein tiles, which would eventually result in a crash. Fixes #5231 --- docs/changelog.txt | 1 + plugins/changevein.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d521d967455..b421b7042ee 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled +- `changevein`: fix a crash that could occur when attempting to change a vein into itself ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e90..a8151f348b6 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -254,6 +254,12 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } + if (mineral->inorganic_mat == mi.index) + { + out.printerr("Selected tile is already of the target material.\n"); + return CR_FAILURE; + } + VeinEdgeBitmask mask = VeinEdgeBitmask(mineral); mineral->inorganic_mat = mi.index; ChangeSameBlockVeins(block, mineral, mask, mi.index); From 6b59c65d80fd67c68185905484155554b26db9ab Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 6 Feb 2025 23:01:06 -0800 Subject: [PATCH 031/919] fix incorrect doc comment --- library/include/LuaTools.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 53c17e1b21a..51ae098f493 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -513,9 +513,9 @@ namespace DFHack {namespace Lua { } } /** - * High-level wrappers for CallLuaModuleFunction that automatically suspends the - * core and pushes either an argument vector (i.e. single type variable number) or - * an argument tuple (i.e. fixed number of arguments of various types) + * High-level wrappers for CallLuaModuleFunction that pushes either an argument + * vector (i.e. single type variable number) or an argument tuple (i.e. fixed + * number of arguments of various types) */ template bool CallLuaModuleFunction( From d0e03a8dec53e3b8465636f9a65866ad4dc7ef5a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:16:48 +0000 Subject: [PATCH 032/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index ee9a53f73b4..b3202b06e04 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ee9a53f73b4d2bf918dca0fc9dcb450f53d6e7c3 +Subproject commit b3202b06e04c03433565e8248f2638dddde4ee22 diff --git a/scripts b/scripts index b6e8e30fb5f..ebb22f3adb6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b6e8e30fb5f98d1f1925f988675471a44a464375 +Subproject commit ebb22f3adb6070c931a82b5f6a1749e7bf3ced03 From 9dc6e22d6e714d959e076101b0c2a82e59ca8df2 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 7 Feb 2025 02:44:30 -0600 Subject: [PATCH 033/919] Docs: New Guide for adding art/sprites to Stonesense (#5207) guide for adding art to Stonesense --- docs/changelog.txt | 1 + docs/guides/index.rst | 1 + docs/guides/stonesense-art-guide.rst | 71 +++++++++++++++++++++ docs/images/stonesense-indexed-sprites.png | Bin 0 -> 10256 bytes docs/images/stonesense-sprite-sample.png | Bin 0 -> 694 bytes docs/images/stonesense-sprite-template.png | Bin 0 -> 515 bytes docs/images/stonesense-yellowcubes.png | Bin 0 -> 178548 bytes docs/plugins/stonesense.rst | 17 +++++ 8 files changed, 90 insertions(+) create mode 100644 docs/guides/stonesense-art-guide.rst create mode 100644 docs/images/stonesense-indexed-sprites.png create mode 100644 docs/images/stonesense-sprite-sample.png create mode 100644 docs/images/stonesense-sprite-template.png create mode 100644 docs/images/stonesense-yellowcubes.png diff --git a/docs/changelog.txt b/docs/changelog.txt index b421b7042ee..142447527e3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: - `spectate`: player-set configuration is now stored globally instead of per-fort ## Documentation +- `stonesense-art-guide`: new guide for making sprite art for Stonesense ## API diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 6660dfae51d..f47f9565ce6 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -10,3 +10,4 @@ These pages are detailed guides covering DFHack tools. /docs/guides/modding-guide /docs/guides/quickfort-library-guide /docs/guides/quickfort-user-guide + /docs/guides/stonesense-art-guide diff --git a/docs/guides/stonesense-art-guide.rst b/docs/guides/stonesense-art-guide.rst new file mode 100644 index 00000000000..c9e966dba3b --- /dev/null +++ b/docs/guides/stonesense-art-guide.rst @@ -0,0 +1,71 @@ +.. _stonesense-art-guide: + +Stonesense art creation guide +============================= + +Understanding isometric perspective +----------------------------------- + +Stonesense uses an isometric perspective, a form of pseudo-3D projection where objects are displayed at an +angle, typically with a 2:1 pixel ratio for diagonal lines. This perspective allows for a detailed and visually +appealing representation of a 3D world using 2D sprites. Unlike traditional top-down views, isometric projection +simulates depth while maintaining a consistent scale without vanishing points. + +Understanding sprites +--------------------- + +Understanding how Stonesense deals with sprites is central to anyone who wishes to modify the content. The +scheme is not very complicated, and this guide will give a short introduction to how they work. With the +exception of floors, which we will discuss later, all sprites are 32x32 pixels and come in groups known +as sprite sheets. All sprites are loaded and rendered in 32-bit full-color PNGs. The image files should have +a transparent background but pure magenta (RGB: 255,0,255) is also treated as transparent. + + +.. image:: ../images/stonesense-sprite-sample.png + :align: left + +Here's an example of a typical Stonesense sprite. + +When working with Stonesense sprites, it is important to understand how they fit into the isometric grid. +Each sprite is designed to align with the isometric perspective and must fit within a specific bounding area. +To illustrate this, here is a template for the area that should be used by Stonesense sprites: + +.. image:: ../images/stonesense-sprite-template.png + :align: left + +The solid area is the floor space taken up by a sprite, while the dotted box indicates the volume above this +area corresponding to one z-level. + +The way sprites are loaded is fairly generalized: the name of the sprite sheet, and the index of a sprite within that sheet. + +Sprite sheets +------------- +There can be an arbitrary number of sprite sheets for Stonesense, though there are 3 sheets that are +always present as they contain default sprites (see further down). Configuring the XML to use new sheets is +outside the scope of this guide but there may be a guide for such added in the future. + +Sprite index +------------ +The sprite index, or sheet index, is the zero-indexed offset of a sprite on its sprite sheet. +The index starts with the upper left sprite which has index zero. It then increments to the right. Stonesense +is hardcoded to 20 sprite-wide sheets, this means that anything past 20 "sprite slots" is ignored, though less +than 20 slots is fine. The first sprite on the second row always has index 20 (even if there are fewer sprites per row in the sheet), the next row is 40, and so on. This +boundary is hardcoded and changing the size of the sheet will not affect it. + +This image shows how sprites are indexed. Grid added for readability. + +.. figure:: ../images/stonesense-indexed-sprites.png + :align: left + + +Important sprite sheets +----------------------- +`objects.png `_ is the default sheet +for buildings and vegetation. Also used for all hard-coded content, like default plants, the cursor, default +walls and liquid. + +`creatures.png `_ is the default +sprite sheet for creatures. If no file is specified in a creature node, this is the sheet it will use. + +`floors.png `_ holds all the Stonesense +floors. Unlike the other sprite sheet, this sheet is hard-coded with sprite dimensions of 32x20 pixels. diff --git a/docs/images/stonesense-indexed-sprites.png b/docs/images/stonesense-indexed-sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..dc749a21965f0472cf189ecfe1083ec0892ccb87 GIT binary patch literal 10256 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 literal 0 HcmV?d00001 diff --git a/docs/images/stonesense-sprite-sample.png b/docs/images/stonesense-sprite-sample.png new file mode 100644 index 0000000000000000000000000000000000000000..35dcbefd55151f27e30aa656df873d0681d2b253 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCil&0(?ST8O&t`t2nauOECHJd(Bf=5zA0u#~7r?biB#Br15j!LI|B<)rGb&L0pkLQsURC!7eGv!0%U^# z6VO~Hu*x7y3m^-s%h138BrCW67yHpxt!7}bs(QLOhFJKwPHNoNtia=(xpL9}|A+NG zxt+`JMV7m@-bo9*{%-B8Rc4{Tx6R!B^`DE~>}z)po!xaidDXAGcWiZbl^WmnDO1oW z>wfW$JMu1%)pOQGUNd<=PGET6IDgT3o=LqwOW92X1Sij6V4umzl&4yLspw!|@-p^` zvP+Ug8iI{xEYVT6+r(-R<;S`Bbk9s~zm;iTv&#E*otgJNn{@c{DGttx2meaXw0z!e zy+EL8eV1HOvZqKZW0#|R%%S;znID9HZaMr*QEK7y4Ggm$xE$5{KkI5&mP63=A76I` znk@YB=e+GY#jlTLww}G!P$>H&=D7U<332ZueLe{ZzibrevtQ%YOh|ELK2|DN?eQ}v xeN*WtCJ!6qeT)M33`>myuy_`b4FaB$j0`V;o`TV6mNEdP zCa^QG096_o85=MzfS3xhk#zyYq$xl)2rvQ7Wdf@Vva|rQpt=kV3_!B_m-4?!{`z+k z$eiTq;uvD#-#al{s6l~;`RTvu+cY)5vF?z+mBRhF=RAkUnzQWA*Behu^j)>feBisd zac3{v26o;D^>;Qz2D)BfGsAFdSlHWNXJ7ai@lEA#I&x>p98Gf$8P7j#hd62$ghp^? zFN<7r>z=5wH1CUgTe~DWM4fSd50` literal 0 HcmV?d00001 diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png new file mode 100644 index 0000000000000000000000000000000000000000..83ae6ce5077a512ae28286bfa0748c62d86374e8 GIT binary patch literal 178548 zcmV)FK)=6Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D|D{PpK~#8Nr2Pl5 zrfGKHhn<^q&bf0>&vZ}N*`1xuiv>Ys5CR~H6e)H!glT+v0eRIy?yTSk8Iq$jmy?43?E92aL`}@B4eZqOp zbDoo)@K*hw{^s8}zVXa8d-mDuHZ(Y3RaI3sH9cduZ{M?{qa$l=ZLya>^@7#Z)Yy4= z>#cX~-h)Z|`~TqoYy12A#oZ^vr(U{cU;5(b>~pVv+S=M$y`GOgx@*%jvtAE%U%Phs ziS_*9AAQH(dG~$$=l|ljZDC>2(|q+SU$BM6Wqb5!%G%poRmQQcuWvZM_4W0(va;%* zt*y;AIr&K68@z7no1LAv&dv_2R1R!oW7Ce0kBgejT3VW|sj1QK-Fx8qhldBx;Iy-| zYg=2}XYxdyb;|S5^A8O4oypVN+ihK{@B44OWm>nD(+WO|U(=4Gu`CFQrti7el>gsBXy8)Ba^R~BN@$apA-XJ`oJ)cbt^|raa zYdd>;wz0Y6WmfB1M|+Fcvn5bEitfJ&vJ{@k*|!^7f^Sz}{^HLDFX>UY{SG}JG8H55-xb+lMtZ;w5A@W>X$Plu&4 zFE(shYxRMDAMI|j?$!qNMV0L~cGz}vx3zY*TYcSW-_hRnjcuzOL>{2Fw#Mn++8nfp zz6tY7tF|Ftf_IyQXH#Rn(}O(C`n|7y+YxQcWwwd-)iqVNrGBedzci{X@JT{TQ-jsj z(qF<;V{&h=VymL{zIZ*M%4)31I;^9)L*=#Da9^J_i+>WJ!Q#q_^99h`+3Eb1eMCFw z^_*Xw?V^F|1HYx!Rj)IlNw{_KTr{Fj7WEun?r3Whzifzib_@>Tcf0T?^Et4u{#+LC z>=t+cS?0TU@7f3V?%Uk*vgafJ+UBO?-l_7)v#qP6wbk z<~X$L9_>ij+}>8-Y}rAHmne(zKp%lS&wYF-A9d4C=m|bMySsjGL3E-lXhvF|*NT_v zH_9W=`o@5L>&?HQvHD_B(<%I=FZ^TsiI@MWeQ$ZiK3ECfy!Y_2&1wu#=BLNo?TeSX z?D?@y>uYQD=Vh2%-?2waJNC{-z3n#l+Qitf?jGBmWZ;KyzH4(+F(xl|x7$QlyS0l4 z8^nKy$kW|D(MYt>y*iEe%Hfgj?c1(s$nRT2y>_vu-SgK>OpLv@va+UzF_=ZHjt*yb zgebzcOH9<>-gai7Z;LR~)3f%iKmMM5@WDq?JhLuvZ#;9=UX`$Z;rZw6=FJ;6IM{DJ zJ>51xAC2DL-m&hkE|pN_0+%wI#Gp}6hY$F2J*#3ogvQr@=l5JlFD@>52houiE{uD5 z2%QEAM1%w7)YeveT?lN14+4H|ZC&4MZESQ{0&~+gH@75U8eI^f0LfohFgu?JP81RO z5oXj!nY$t^gZsPygI1)4jtpwbKxiF`@Z3KT;Ws5P*48$})U`G`&|$;6hWa|Jzq`#^ znj1Ax8#R~_CUMn@Iblf3P}N>8f(QW)+Bt~iObL0=)_fP zZSO4Fe(3qBo3g8`Yiwk6%r0KKqCQ&_Eq5GGE}%ogfi|L~p)o+eq++M61lq`8L~$St zfb1LUr40$t5u7*G@ALzGMSCzJ=o{L%qjs#JybkuQvwGj!Y7VTW`q0MPYplDa&RV7L z$9h_9u%p>Jn-TU^c6d;+ntk!2KCQ0aYSlLgq9grLJLT5b*LwX&ho|_^UeL?FAu(yW z$0u}V{P8>b{xmObMeu+(ICz?)pmUw>p;&+lMu6J1y|X7H;=t3w(_GN9S!1Q@NDA;s z{p&nh>->TeaOK0NXj`jtQ6OO8_jCcY0UYUD6bSeEq^$@@#v(MPOy?ca3qN=Y$UHm; z$V0nOT-zGoiHDb|eEIz0%#2OSAedWPvW3+ZH(&thp30NYRBR}sd7wSgIqIkH#$7Pf*A8*P>J@BHB4_&KLl1oxhTauIHLGhuexKJjP zSyz2*)fMsF-kPk#O>3{)w6U&xc4^>Ed-=lq_L)l`*ozbI*_FZXOR!JcPG!gHjt{J( zR##2M#yaZk#gPtsaje5Gh&Q`h>ecR`@7m73Ep6`F`cB2>)^==WZP%6#B;RVAtX@WE zU9Ain@fmI06R)V_)DFov(PB;G2xt_qINvB;ZNZc9AL)@9kO0`)JFvwq$=a5dme(c= ztDZqNF)`{w7Qx=t^>HNvEKA7VxqHuk>$iW`-h2Ne@06R*U6;lG8T-i}{}KE2%P+bz zK{z0Y-;gD~vbtuE9#7lI@PHaq?^ZjOeAbhSr3@&jhm()(pZt@5VUMS#qakWM9e7@m zkbiV!NCW&pY4-m|DH0SIWsoli+>;au-V4li9?FU&pvu!B)Ub#*l zrGC3G+ABrUDW;OwBt?Ri-^?Iadamj!gs?21tsRPUoLY1rM&&ocn(|;wMmmF{N*xag z5hw_gT?tkMlMi?aE*J31gF*sOKC~+bAipUS4~GH6y%&JzRw}-^)d#*&( zF2)LFB>iYZ)(>!>@2N=mPMh#JTnh$|aj$SahZEo4@%xLVC-(MBtZSy5$Ptd_C(T z+&6#s4{ckGqoWC5VbLN~X2nDdxbvYc6yEUApgnjv=^+XR5X$0dp)G*H&7dbQgLQRv z-S085@O~N_n`~qxLR&B!ilVPqj3Mvq_MQ7DA+B<0i?mYCUIt9Z=Z(vQc1enZI#P(W zs-qAHo9YA?GU}^g2~!vuL471k^H3eVrp~1U>CE=F7RP&2$|YUyvB6%q7`G)flF6Yb zWjbzBQnZo5&L`KllmP7k2K&0KMtEV}p)k-&F%Ts zk;k~`gL79mffP`Wc3kPOU2;ZJ*U{q@yao9b=6U*5C?^?`W(`&*5+Qjw*v zhy6WaC=gsCGwrz@x6F-uhaGw4iyPyGc9I6q1wl*52f^AZI@40ZJG2d)u*?y@@C5l0NaRJZ!o!5VKpcg%sdDDT zM})9we@j!de^NGzi27R8ui%3)$NHxp6o!Y7RA2I5jKgYMErqsHk-#Gfgv2l|)`c^Q zgnZykyUr^=6asDJKH)KxAkP^CC=T!dSK3AUJakuCv&z=I9UrPs8fvV!wNApiT6|Yy z7kXP=A&`E0b=y|9g0BzMU-UP?^C20cO-c`LwKdTnq%?SIR@1Ihf_AMF$bM03-ea9u`ayoZ42W{G6Ej&Y-+H<_7-cF zQpxcPjtMB8R>{6bx(qf%@*EmB&NQjR0a@0x0%@H^;w*h_;GRWr)41^5$kQadx_(uci57J}p zgE29F`X?`v;nsozf{6hDlky<*)zsEo|Imbuj9-=aan0H~d#zDccy+BTNztO7OM?Zv za3Q!6*j)TZND&T2kkhuF9(iL!!{VDkdB7L#+VvaqRIba2859m7xM?3e#K3fX1muPH zju`yWyfaXvfrV#T>jN$Bk>@}j1fTE@or&5#WhHPYej~IO!XG z@*5iSoN`gv**8E8-k=A#^GW038~T}xK1Ybb2ha+7Ggk0S@rw8)AN8depl`to!A_l2 z!aans^OwqTp3-+haNy2`0LDmgUQ#`L(gt`BT&aiqgnTHAdo@M7z|H$Z_en$DC>Zdf zTm*JS2F1+cf<2m>vB&eX_IPg2CTC{s;mouP_bjIjNjLI4PDMRF9(3`Y_VFAKCUHDz zdjZ)-#wEhvJ&d9}KFOaKblH^hKyjiFNlOTfe3b8b^jR&Qu!@Yqy>+WO+OcNBrnU0k zR>U58OXHH~Q#>O(yAAM(@gu3fR1NxQz z#ki_kUA%iTB2_e|i*)ByQ>qT0gPGc^F&Sl^~o?URogK{a;n=~+e`5yV{d_qbn7?^iW_=7tZ z8eyjc~Ek3U{Ha2#K9}<{oUz5f5#v5<=^MQc@yL|bw=c6p>me+6n)^FP1`@8?C_+n1h zW|LjNe&vLhfMt2X_wGOR5M@V4yUjZPoO&cJElqa$(gnZI+!}n85aG=ld6NUyQ?+f) zm36B=KD15^;^D4lx2&2pfUXU;TZ@zf@b2RkTic1yNErq?nru{_3wg?bW%$ALs>h~* zjkZBMl;;Ky?(av)m5F!yI{`(&ggXK?C)=L{t`yw6+4qo@d!?oJB@D<5!jr(OMd0C~ zz-#p7(#pE?N<&?P9d1kE*G1TdbXfbV;w5ANywk1mflxdH1>ewb{asxyoYKk%fA?I9 zJcM@e6o4*`3F@J3cp6N!Gr{f?_E3XsWT&kxThq>_jdiw)rqxm$ zyLL~sCv26_qVX}_)n=o3%90~^E?hv(-}vACd#~{YPXnR+JKy+y7qBoXjGvcJYzpBk zmoNCr7ZeR0Nk|9ALhv(KeF94tf*rxZ90;MG6khiw2=3gyZ{Pjin{K6IHLzj~>nJCy zu^bVYSYQk~zQcrAn+Se7>--!E0}`dh?;Kz#ubd-c!V|?p+tMN-4{>$ENZbdP1m;+% zA0?H8n?cwvr9*oNpX8)4z=YIiU$|t?T^+S6GAuzCr$VdA(qyVr4=senh70GvS zfq^rY&I%u&5OZ3b0t&(VMfJA$Kx%ZlDT@mNEOE*KFVgUdVqors`%LH~jMD31AOX-J z+Dm>Z)b%avAGv6s`NEIcvoCzsE?0ML$p zK$(yRMT_F$UZ&v_d|Fysr97_N^UuE^d7@QQhu1_qy&0TpAMfv@*9ET(X zu=L>1<;{ItR2tIdwYafocjq>2etp*mIQ_kaSG}~T!6~64zF{6J`vxHh-U#w0S)C~I zTu|Yjs;Z`a@C9}9Y-@{EauruBtl(nK13qJTL3?B|Y(eEP6aq%7@Q0^g|wuAi_T=3lEFweuk0| z?4*TfN&C^GhxVQK-?8`Z-?7T^zTLPsVOK7W+J&(pyMB4xUU}&```o8)+4TIPeR%(Y zg!r-)7M^{hhinNUA$Q!eF52SqLbx?44fVrs@ECZK-p8fVa-Zjv1+Br)7eNTJ?cj>{ z!Adq1Bq2CFE)*Z-d)d^fv_58cm*kZ$%S+w1{$>-e)yLeL0Pg`_+Y#L>l1UrlwPpR@ zQlHXq^y5HBqZA19a}Caa&Rgerjd_pB#Z8;jCuK6}O@4sTEWclWup}k2LJQ)dC^DmPNYPLk?gB|tMHG~0K39>Ti4#fk*&!yS&$(^NUmElh`E$% zhzBDuxs0_Bqa;CNkj9k)fr)VlJTo&VCXFSdKoC)yOqM(=V91Nr*w@$V9hmjZ%`f;8 zUOFP7t(arT$x?_(S>*dcfq_Yk9pUyQNLLgbV16Ll7lWxhQCqIJ{GEKvCyG7kE)3e0iGI5<+%0dU(FZ<^h3m_}WCaa_nTcvXVP*tG)P@R}I9*zZM*_X*qYPv5-RW}fLK$OFphTR049=o&Gfl$a;Gli+OJDZug%@A4RwVgMD=5=JPe=b{{h1J^c|>9UlrEZ2SUp1REYiM~%kO%qTA zDEGLS;0yr<&=UF+g-TzNpRq|l6Sl(W@QHNs03eNlvzFO$xP5&)EV%598^ zLsuS*4F^4PK2bkWAA%lQLSM=S@BIq*B0NWZcp%gbke<4HjHxZ|HAs;lut`s73f#SX zed2}S0WGes*xji~d-vWQd4L;saeUaGyLQQLT)AK`+_-9=eepT_{LAoQjV-OMO7U+< zupnC@ z1~P2+#S_RY!1+c0ad8i&vMR;1ps~lbuW|`-aaBFI^ZM}{Z@%MuG~RvhL$Bn8=byE& z{m_@}GoOCRl>vkJ&YgR{a_T?%JO71y0>IDx>`&N@8`tc!uYB4W`;&lqmwWf_+due+ z|DA`l=pa_oAQXQ5$9~vD6L;@D^hs2_o$;|zS8jM2taw5IeNt$PL54RH6W<@-M*?jK zNl^}vtEkRHt`cG&3EtGpFTY?99zOQ4N{3uJyt5QyT^-H#sT(8K-H9vw^pFu@q8(}R z&W_VPAy|DVASOG(9avr46wY-v(A#-pSpf(gguhetxX=H1_oGU zo#2UZAf(cUr=#y>JU2=c_m`jhf(=dFuq#)ux$p;|5oG`={NI0{RaYC{ch5ZYjEsf8 zlV?CVJ;Da1^X8jx`u%oUapU9Tu7J+d3lEhg-_z66-p44Go}M08RDb9H`5)V($p@Z} zuod3P(9nSQBYcTBLbc4PxuC6**IK`AP1Vu1i~aJ1Wc5?!dyiv){nA&iT3b`CJzU&6 ziBA(^*eR^ApM814M!IEE3hpm#DBY(0{)h8!U0xsV@L2Mu2JNGzO$jFnNR5G?d}-7t zM1kA0>uxh;)-g6_b&XBV19%qnXT;Y}X@V!RrvVP#5Po$s*+-%7MDuuSnK$>NFe)Jua;Y=hGswwwtkH!YV7wGQn zbPoj~Mq0v3;J|aNcD#o0W{#{?iiNP&ci(;2K78=dh4Q+T$5GXxl*dK8dHs@I);MK> z07_|TWzBiIQ)3)F!DCsn;vfI|H|$%lzvDEzFf?MTxmcE0+F7e;-qV@d*^l~05}XI8gm#2mQ7_`E@fsd7AuQ@b$=?^>V$=XA5tI&m$AXcUFZQbLI(xWrY`-^sXpc8T zsXv&UvL(qiU`#k%YH#+?R=W%o_;hk*!xqI)z~j{oTM_Mh+Z*ghZVlV#RQGSaHEX~7 z{+vIrdFjPluf6}lhyJ6lx7P*dbI)G4OP3}zh}rXDKBquP0XGPbWOQ^m<`_goR>vT~ zV|iuFIq*9#1p4>ic*{Pzea}~IA)rZzph6Jw{pyvAUN)gE!c+_t6j+wEzfWk3c<+-# zTL8j?uuodOXkHkBi%n`U2W2ui00a?}xwMhIDU7KbVGG_^Dy-0Am0Bv#He%}i9lAPw zA4s=^MhJB*VrgdmQKyHYRIYn0;4Y!Vceg5wXMDmq;ES+l0Am#q;y|GA3>uS`eC&}y zVTA=6-VPSHhPKy9-3$Q_CGbTTIC-c<`Dr_Ja6FVAVcpR;Ze9Ij);l<6oxLMg-xy1B zp>cWwIk56OVJ$2=`VS?N9!aJHGA$S7G4BCTW`?%tBUbQnp+E?kEiZ>xNjj7W3Wv0N zyF1Fqa`a9;SKrD3r~jB2VwE55t2(S$vj%f>^?|&OBO7U}woWMuya|@?5~FU@fFcBd z$H!zO17=$T6MBT0WR*>g=bZoGe4 z$y4x}`W&7PZzd+>X(Rl=#OmRm{Inyy$OvEQk(3qtPQqG8h*LIrXJ{0G?x6d*v>Cv9 zM=-&w060(=Jb^OdKI0o}p0E(%p@roY7slki?SQ_zN4s4a>Y^-H zILb%ceRZ1B^LurD$fo9>x0=IE>u8v>F2bfw(bpIs2ztH~zGAKdZ&qJzN4$}|CbD{X zh)|Snc{r3g4R{CT0Df5eq=(m7r3DYeYlK+g!@bIpZOQPVEO?eW)qF3%S-+W&Vd~}m zspoBS`HGapwvDv!$~)Kds)}_sRM~JxgS7}p{I&+s0#fX#4;D7JZAHe*zW8umUVByb zv6Nk#jIvfQ8zYJZE`8$r8Zpgl@4bK9=P8&J|M{Q$DPyO5Z*PweW;*lT_dYlgq(Af1 zKW102Uh*Ym-}?6J_QsoUOG(`IbTBUhn05Ji&Cbr*Kl<-~)!zT$BUz3!5<;8a5qJ?W z7uVAiiI?S`0~@uTu1K(W5sCn5Qjs9Q7!-tJ=##NefhzDY zhXeuTz;0Vghav$wREArx@@CZ6E<6|n2rTNNdZ7KWFs~vTk6Yg$1WE^BErh1s9b|d3?unfEFkK=0{pJ!1CFXF3MsxAfKe` z>gw|Q%$uM%xMpT%T-i`Jd`FqX!^6JdfwU7BF4@S)xV1@Q8 zml3kRTV?xu5dwt=5k2b?#xC5|xAYZ;4bQRk;X9NEb2qH4Nzfv z>V#Gp6)2Sr89*P-?Afos@vqtJ%IEDXmmk{a#+K|to4m?KDUo)Kjb6#7iu!+x`Qr%1 zqnu`?NLU8EE8f{wf3-B$s{U?o8w!!%W6H}_^-F*G-#Gra|H{8*-O}z1T0%SQp?Em? z$ey`=)h$8<@k6;mgrvUk`OkV~2z~@CisJwBfBjE997HGxB9{SXoK#)}Bxwj?k%ll4 zAqpnxfBeV3_QcTEJt+QCMA5p9LRwN8+D;T+*47T z8SK;nO#mP48tClRV8sqcVUUh66f{NykQR45Js!${;jvg?$_?cSkLAccBLopv9D{IY zCsqkj1|bgsLHyId@JrU#(c`$VAB4FDaK#fqp%R9IhJ=<NO9)v#Orw0!nSbu-N zD;)ZSc@WYL4GsBs%0%f927CMMx4nE66CMoZph)m&P(~;h+CZB0c))?a0N9&?@*)kZ z>?Qx=&aE zVPD?RmGIsaE3iUgv7%y`@y9kbyXZ>b>ez%I@d8eSycnaj5#@*yVJv~C z8Jxbv2j2i^zu!P_hpcsTVFD*uO6XNz6xeba7^d{f@R+YX+Hfcm1!y^l`foD#wi5~g9NFxEd4>L?QiFBi`d2Bhtg zAVvYv;dll!QYyHZoP)F=WrRn9mw`}#Ie$hr*TLPaQmGEldNeA=4Dykh9;BQYE+WW>z6ux1hF2=ZJdK}Rw;m`u-*mQeJo z3mMJX`w@HxmwdRH<DDF$r3tXJLI%F5XmnGx?1CwRH{FKR9XwL*v#r zKnQEty8DK$r+-9!8+$#VM^1`EH)w#Mryo!TT!b&VP&U+09_UVe)I&cb>@#FVxC$l0 zZ_-jWyhI-IbDzHC8OjC45PT>_p>*IuI?By*P%c%65}w;qHe0J!ceHDrk}ch`u6mkl ztXGVU``_KlV1UA|k9`gU?}H1v;9Vfl2?ykr8@)0Q|L1q+Kd``Bb&(c>f~%npPb0Xf zp!!K3x8#aCSqZkfU6DZ&o(-N1`Ri*%JB{GItxele8!KBIcC@`|$2+pTMN8KC!yE7e zW%7wqIN>#YhlXXK`^!AR7$*(<1P?(|_z%T|;Srtqr!Ih1UwBx) zS48)q8GV{Y4#Ju+6yKS{!^-E=J!byAg78*Rkl+GP7IPlVb0I_UkXY)@>Nn(y&l!m( zT#OsQy)en8!M+|F9_+KBfnHrb*5B7D!?x8lf}{kt z6b)CE(jU-vU3;R@?tzRgUHeDsmtz@y@(wXZFkHNUln%v^yasQ;XTfWAC%lFtppVGo zH1R+G;k*W)ag})mBO!S$!Ryrj%)4Zr>*6zv9{8WoDtygY+O1UC;`WhEZywl;ypJU* zj@5lBSX~&iJ@QV#iG?>rRZcG%sltPB9iC9Or#ybD{`G(LXO9VIaW>YU=Fmp0euT}R z`Qb0S)$zfHciggI;Ionos{+sA2k}U-bhuw0Ae56H2?FKf#fdXGA#6Cys+&L*RI(={m=fEegAiV!}cU>``Vj) zxg-ObUFetk!&89y-=AJPu?)*F*4^T9X7ZE)i|?b^bsvnt9eqzL@5|2&*!7_f3Evv` zG#)RS@5d z1!b;o*z7OycmRh0%0q>fCYIaC>8c+z{|`(u-XaBA1vor7k$G0 znJW|S@!Xl5@(>o`H11LVdAN3Q%tnU>Tv?)Qz>N?ZV}gYMd=hf|Pyef5v2VZmp67Y- z+A}uP+voKYy2|CqcqVutC|25;`$(YOBN;<0N_QTX=KqXMKmIrD-~HiVwXfXx7xrjr z{fYg~1o~h{lPmKw5UOGy2>i*$Lnh(zZ0WbJ-Wc>ra$ss@%Q)@?e&)EIvF>J9F67I7 zBHih0ofRD;MqB)ypoZ5$q6> z!QCfqpNt9MsSwiQtSvSoa&RND>L)1@gb>OBAw(RT)lZc5gD8@iFR1r&3BNo|kuZNj zozwx&31vmXpkpaAni)W;@f~YuSzZ%M<1UVLyMU(6BN9j~kz>hhx$O*MpTHCYvH*bj z4G(Y0yII?a0|)UKxKH_5X($TjKq7X{ntlmm=3Im`F>KN?AlNVnBfDFVr=oPE@k?d0 zDhro0>sMoa5ICKbpSCklQ9>HR?eE>Y=d>mC#d8!%Dj6>3I`CXjF6=YG%b*PEg|;XL=0ZOB-~(4M zq#=Y>RwUrVMLHDHH~xozXnPy1oa?Z)$@gu0c}87!;F|-7W1Rxv{UL+Q|ENKZ+@Z)~FhdSlO zol_)OaBi(B9jn1O#sZ}ShY|~?KJcN86f`I+tUh3FugX>rSY9pB?WvSs4gVV0AN@Unf3jsqPYPgtY2KvsjXhGNJzx@K%@` zpzkRMC4mA#;Aa0&9w9OC;5ocYm<*mKTr{ozL)kcGCH)RyXkcW}yKHVnSrE-wlh=TfzAZ*`3$_UUJLY_K=@y9^u44{c$7%BGff ztWjQmZ$m8qW-^@>XoS~TM1YZl$He$zMIv)BK+WLbz-tI}<_oY8CMIHsGS)hq3K6(4 zOc_>XC32L>H-Gn!d|7Z-ud9mV#V>3QJ+0OHf zjWVyoAfS_TvK=AE>M`yCgyE?BJm4`cY2xk-tk5C|0?Ky5vv);n~?ZoT-5??^|X5q6>;6htbL zynys1m;<5j@Hp@kGTa0`xj65P{LE|MWl&!9wFH=WhjOTga&yBX%7(n$L&4x_Faghf z%EnWnKg)&yX-T)Xwrb!0wg1K@-+SGr?tWm+wrgEtXeO?)_K4LYd{G%pb~CsbpwK#j zK}C$3!OB3*Hsf(j7w2ekv2>Do5hmhax!h-0?U0KAUw>7m|V-re~ zm0b~Pg3io)(4I11F;B+41U&9-)kS&)`hn^^K0qLe7WA7glAvhP`vu@Fc5VC8V;K?j zjgNKl8O9Alo;eii@J($ZKvRQo$AA<+A~0E2OPLr*^e=f?8I?vKb@-l;!-{W093ASn z-tG?frdheQsPV}XV3gsdaTd7rx*R~s@QkzQs0#(dl3~UI{ecn#?+c?t$~R>FyJs2&saYzRE> zk#M10P*%M20Y*gVJk29vU?iZ#5cnvVypIrqVJXvvx`5{}(e>+>ea{EG&Z(#Dk+9hh z#hA(m-~`@RZcRAzQ9iNQnD}lGgGv~B99F_mG-+;baAu}%fZg8+cYuL|!s5LT)C&v` z^th$S#CutwB@l*yzU49*6nGl|2mVp+ri4kB2_OiVl*hvXSA+x#g_Svf?$7@(Z0zE7 z?+ZLeRzqF5aKY(78T>}br}sb~LJ!K|IrK~~1SNq2Ng5}R=X>A#p66krJwspo#+%{d zIZA^t6iawfF61K(7nAvTEV(j^x>AW`y6FeE?H~L{f5T=b@93V{6_y;zivTVUw7CVw z0AygI2pCvcE2IUmR@L$)^m+rReRDJ{cvVY3XO^FpmhSu zY(&ZsVX|^qV{dOa+L}HQEJOW0qIWE?C@X@5kL@EljC~{=+L&~Ke>uz$fe*hvZ667i zzlXN;jQV(n0>ZdKC?nJX!euk^Bp*&s`>`l)c<@~OmF>#9%b~4{GCI3D+w9JRDVv^M zaDE5KgW`bCfvXqCY-BL@bofL(;kUw|;z%OSvV+GLg$Le0(4#bj;i&hY{r2zc`a|E7 z(%jN&T|K?N>9(u2#XfiAh6`7)%khywecj&9V%ND0ox4+0_U#Wpv>A=B*7_>@xoZvf ze1GuW1K)kFF|H88=RFcGya&9G43~lQ(>xNG0au`c=eX`4USuP z--u0IzF`fG&FZHW_dX~SkASce3IgGd&}U+sPXs)IpL+R?;^0EClLz`IfOq0PrGZ16 zQt@z~?>W(q$3k5w8Oq7QNIfWDJRb7n@ery44`69-%07DIk8Sd!ckS`*cWv&@8&*}} zD28fZDaA%WtRm>a$`qC$V$~Chg3c%pEPJdnCN^^tl)ACr5c&*4ln+9kuog5%fuKBC zfkIxw8kD`VjinrdnKXDn@Cq?)6bzOat7s_qNdlbb)B%2Y0fZ<}1eBFt6vs;lK^G?M zXF-^RQjAGjLJ6@;GWi_l2-&7eg+W$5US59ODX{2&W;?@rqcv#&h@&FANpW%e+t?C_nm_dKst9 ztVR+ZK4H!~k}eDl+fYxp4eI+vHo_L3Hu!>P0_72o;}Zo+8{uWj?P`b$@9A($jr6|kR-Fet zoQ|XB%l7~n5^o>}DM0uNreBs4W3Vxy>2DmAf@b+I1DHC>JKF)20`XwNXgS&bB*;29 zHMmfkgs#9Bh5JAGYp+>P|Bw$zggBNya}NvxEPf71L`j?n`j_kV*I#$q;)O7Ajv}F~ z6b@Vf_rQ()C%7X9~o(FZaQj3XkLRRd0M^Qj~F7P8ClkAlD+;h*JgtTbS zw}1Or?VtP?f7@2(XMB*c`3b=bJU`ZT;oI3L`&?jb)n?JuMy^h2zk6&43Y$dC&pgP`^BM+N0s13CZ}1p z3$VJ%C&qnAaCjGGzyl#{Xt!^5xcm&=5LXtP4M18uRK?ME=g8Q&&i9~+;YAdm1lw{!_g`TOG06cITXmV zS1(v!v9TH;?$6uUW5fL9nv4Ip>CW>mwdjf_o%-PFp!_ z5~WE!988HoChh<52Y=`mJb!V7v;#37aUg<2qy{foqnZ1pg{s235yF@0!_aFEQCle<=qDl>|ehA zeVdYZ*>JpX-N%y47Vj+i{I&j*MFA&@gk9&n$?!k;5C68$DIjEC`OHi1Io!QBDF!6| zUH?B?k&xdl5MAlPoL3~z-+I=CET+DR;{-cj65;hR_ zcDMROo{AG3c^C`N1Gkvn(J%z%g=JRf%tqaZ3C3sQD36vEH3Zc8J%`GYh zFL_yJjK$&>VIlmOL!m6_hda*2VBzUyy;)sX^4Xl>-9((mGKjp8p#cUiJ zy&!yJa+67J=)>f6PGaMIqyi`_D%!)vsl0f3xpE4@o>0z1`OKfd7vKHvcl{Y21>Oq^ z1z=*FE9XTBTV>b_AU}#G=U@nZeedhPYHxq*8@9VGCf;3_q*;)LJ&3TAgfP}MK$t2g zF!2_c^MaCi5m-f7`v_&$;y-E7F^Nt*T4=lFhlS2}ggXP5V^TKc9dhIZ0v=$+4d)&K zl$-M|?kUKdAYP2c$onIxn`eZ{V#j;zQ9%*XeD_Ysum0yg6PG-Lj(8%e6!E+=%*AiU}nkYS5RyR zQXWHgc~JZ*-eo0ZEW|~S%kxn=9Oc5?38x#UA|a&5edGqIQP?OGaL5&D&}*l#>VfaP z`<8w9;GTXT+O^IqyDGUmy}T|u9=KP^ycm=1gZfUtLl<}regQCWc-1Y2FLNt+((u}b zjDY)_wYDM;1bkk2=BAAgjd`9bXzSnPsfiA>2gLv*rXoSsv1yaNGV~$+?}n&&0L92N zwtmod_CLXc%WJFl{SV(&pUzsnMBs~k^5*LHeGg1EM8Ha9vmwX8b7Vq-zh^>>$AM(g z`K*MBl{e0zKLEhYD4d)ye-aQzN=AT?oW=`KXDSgon8xF+=&PsY9%*0{m=uA?diW}F z2Ac+X_eWbxqwhc;9_W%M(&G2<8W>nG18#im36V>$x?m27#-L=kEv*C}@S{`8ZGavw zkc<2PIDrctPhRkWRvwyC1{eyZ#o&ZCv;)hL&g7zv)Il4;4_puu^^L98($;02-2*l{ zamB_jUbnFeSJn1Ys}W@#p+8>pTJK&#;>*3f0O}zPeaQvhWyo|14Bqs30PsdhWIs?h ztG-Y)+^25p$bp`H34S}9YqqtzXiKw?ZTjx}Hu?VdeIk8n>VX|al98peW8AB5N=piy6t6i3^ymK z(rc9KW$QU*b3qOi5EsJ~&w~4u2Yz@b)W2ULv=bM7#|kf2y-*$}P;(yzz@{_sV#OHz zQm&IS!FPM_v^^_Qg4rIdS$KzZNGZV>xXsrch|lLgM@_ zF|I27B##7P^(XBk;m8#l0r0%+fh17Q({{FVKNME%BSC0?`O}wT4~MLI>U$Dk?O^f4 zjBMls+&d7HZm5k2dw@0VSioaLeLi3kP%cb{^PJUE)J^^jZP5;fM=r?(59-9Cb3q_s z)FF?EWxsA2mGE?rM(w1`p^0a0;`(Ro%GK+lX-t}(AAHUBsd2PZv&5qFc|3xX;D77pkVMcScyeAjj$T!K!Xqn z)$S?bHGTab{>L_VA6`53l}*e~AQV{=%NqEff8~Oo=X3vL65IUvhzvRX(6d9n;}>Dh zn+tLOfBT&owMSOcMEA)&2O+Su+JF3oQMdNJ9wytB&%GWA^73T+pS?AFGTB~!AIZ}~ zTUd^HXd}-q4V=Eu^pWTDqlS3QRBgx73R6b7v3`kaMF%DPOBFscogNc5;e;DGpMs$@S2K?NY zzG%;1yy$gvrWAWea(n=s)y3Wt+KEL@+Yr3vkPyGip)J~iV&pl>3qJ4Bb4D_UL;A5Q z#q;lc_>q18?p=AlJ9eX|)_(L-y^XhrC!OyjfrpS+@@Q@P~!3)sgx%`*B<+HZ<-K4>L zl_T6odTo5TM;bZ=G@+)j!eWvTCFcyGI=Gr8;5mbcXUt>Z_MX%!oL~X~4+RB*RtHD} z<5D*VC{hmfF%aoEI0kWl{68%2Ub$oYwU{-i5jT^z3nC9K87H~CQvEWJheqd-sq zcqG(|!Xacv9?BqpDjF0SWpEE%!D)SE*XVRCBG4n-h=68YKGr%eK7$NJ0Bz})*c2$Gwkv_6!HMEP5VQ0bVT>X| z;8GT698nMVse!yqd~eDiK;W~o3n7nEAsuzFESUT#FTzSf4uOWefd2`dAhf`h@X^t+Jbqn$JxsQ@xu-#-fHvlQ zBy}QB7&k1*&#}dQ(x%bHyw~>rk;;>`-_T?)T)k$`PFxU=b=zfD2)2cCpl;-Vw@vMb z)?Boma2R!-JXamiiBKN$fb`Id@y|VsK=M+*%Qk(256>x!_VOH>Fd2{6(B9JOOME|C z+O_xRTkX4dzhLh?{F>dF{j}Yl`Lw+?`J#RA{_}Qw*=%-i+2;4;QMbtJn7C>a1A{i& z+v6S;p*G3}M-(3Tf;(;W_NZ=f0I%|M?twe~jiRTmdf@jmFYRDFIgOPDh)`i{Ltq2+ zNN=xQm0>AkrQY|EAdnIGcnUBF#}qtGkzi4i2joEHX^Mn$5GVi)kcxzk;68$h<4kz< zF696`r=BbFd{}*vZz237MS?PFZ*8zo-<+_EqrHBxBH)wMg(C5RA$(94SSxfYOu^d( z8SpS=#E3b`l!>}nv4SFE@IY7?f^q-`O)3Et2ug;!P*@C1KFPyT7L*Nbd%AEr!vcb? zw3*m1>E-_*)N?{4l^n`0pf z)@hzM#b!dR)ZpEHS?Cxig|Q<*s30I&s6rZI=pF{vcdU7Mg1Hg|5^32-!VxJx*P`DD zXa>43F*py$j%X1p>{H@495f>goEdJhX@0K%*ljUQwb2O3Zby3@mky8zoLi(sD2LyK!zjbQ z`zFZ{hD@x}p(qbJ1w{-4lAj4f(hCjyi~rMq-}-#QOx}qfq!`MAPQ;oc3_G8ot^}PfpSnhlu18uFFhTU29xNl;$jk>XDEGWkB5>t4Zt(<;JgW_Np_>)#( z-7BryVvYfUisE2%BJXHGpkhfql(NZ*Zv-%2h}S74z|LQ+*K(aG6IM#)Wc%0Oopyl@ z1b+V26%d;n@hb3;2u0vAkS14C0of)1Wtm60lmR8fs;ykTLwGMITG?lCb%1wh#JebX z5fX}mx<-4NPwFHrg0cb1b>g|;9Z@GoaD4Z{imgidH8r={v$wt|Lu0^sk5CYEcZ7O4 z=6!Kx)r}#-A;W#W&buD&(D*?(`HC>{E(X{6BU0dR#vqgRY${|Q$<+M33tz@D!Wbb2 z?}H_Lw6MCUHgDM%UwOed5TejOxcks}%`S^fFzOh$3rnk>C-#ZNn?M&OFXmU|J&Qhs zf5ry}>}xN+ByYD-%4^&4BuqtE2W7?_210mOc=~2Z{Z4)+tb-B6{aF4V?=YbZgmel} zLPL010BuDuQzo>5Zsk|)QZBT~kPzkY34cOUXu|T~Z@u@(CgrM_H-(rZRr`v!LJCAvPUvF*zbyhl~AOIVPZEs zp%~)ru?K=lVnQ{fEr+(SK0Nj**A4F@_JG8|;ImbOn0+9_gS{F^gr2~`pL(aLsoq$h zOjmmD5sHHlcn3vMC*A{0f+FDJJ9)r~w3Nf#34#WkbBQhkl{Bt+sKY-v@?FfQ`e+Lb z>Iy++`sP7hsc>i`em~`;<)VMqFD1geS2os`eREWUTyydR)Qh5^p8)y;NQK~*zk~pv zC>`c6P&5E#kRH6iJ6B(!0LaVWLDA&i5uRf-;L%V91BEicfij#AinfrJGD$~SgvwAZ z_ile^@4orEZ_=c>m-{$QoqZs2!~&D!2t_P>ybnTIE|_JFBOo~nf_)=QJ~FR?a7O5o z2jPx4z%Fr~waNNs=`UUh3JThySg03|fcpFhm7-2URFp@ZC>p#Cb`Yaz2q}60svmF9 z#^yj24!EIcz=xG$108rO>_!j#D2vH<1U}veWu(_Zdr&@nryTf(*Ot;A+Rh|D_#gO~CYG2^at_i@=vU^_;v1$a>*IK7Nyz=Ro4iJ=#KEAnWFne6*YJ9yoa2U-|ME zjtOV|pdJYm=UFKLB4v-{(xrxxKSptISX_M^$f^B?khX+aWN?m^Kx`I2Q(r; z0k_D+4D5C%&-n>{UvjAd!9DUbk?!YmsYA@V7LFT83Eji*PrfB4~tCvy=9 zd4xZ-0S_(&IqC3LSc1#)TtZabM`@sB_)Zv#{zUlWaWFBCCjp?CzVn^$xbnb*;o^I_ zJphV``4EnhN4ed)b<2L^H-5u@bYOS|b_D_Fm z+>eGQWR@O9`Orq@S=j%904FRq+THAUa@LTa&n4k~9tf+UbW&&~uwv_Jhc^DyOJlz4 znJnNkFYgJ3foDOTC>n4odp>1&{r-|GouB*6ga&@KhyPHX_ZE)qn;&w_%g6ccC%*E$ zT^aB9KIYvY#N81XtT)`dM(56qCY{yGD4^JzT@E2v}#5noE( z_2Xrf%;nj+yj)-jev8On-;Q+Edk2fPD-7AXP_WJ42%0Nk^iR|Z|+v+}7^1BwF? zTMF+WfzH7q!g`{SC=}}9&_z~NL8PtiJ@+sm3hA&eeLXyu8%yh?AQ0{d#!b1KurSsT ziU2POjZE3#hTujyv4@0JS>S{<=a+DaeIm@UV13EL)!-(i#k>mnnJZyX0X~1EwB`1+ z_Y8Ung87ocu`71z8n67FbebUe2_K;d$ODbJfB*h{$9-aA!k^=nfG0R}p@0Zup#U-j zMW~DZgjPJm!yw$nedaX|jL;r*M``h$c9j(g zznRC$m0}$0!WD(5Pa=z9!_!)eGrH!kroN( z82b_Phpq@>@CXBudC%y3#x7}nd?`K32tYv*_My#gyeSV>d4{P{kx(Xi%J@nvOsGl@EqkaIqz?-=g(sLn`kdfr0 zZ)h83rXs;3&Esi+^JP|ZmOlg_(2ukui4MN$(O7wDBWIMml>V?$l)L{^%xvLd_*tp89la!Y0Pg9i-8z}Apahg7>8#(V=n z!#(CI0fkV9aTpl5=6rVosco*Q zyy#Gr4z4&f!J~pMwYtEE_RyAW&&KksP2PLoZomJAJ$`W8mKW!3b7M{T?8y^pbtS@; z;U_L|qW$0wjsR`Jqd*z(ocn~ZP%`A@JI~4E16)j6?$0bI`adp|4Pi7C66ctG_~C8) z<3Ik6-M#(5Hdc3SXHx_09uWNr*A|juL^+ zSk$}f3+6%Q*LVEFdS`_c7vZasu4eZV2p_RQ5#dTc1Ttk2N`g1J$K|02ABqwNQaA{C z(vl8k1}|`|1?3>1xllwX9bdWSp`yAIfu9HOk)F^F3I|2VL_YO%xv(kf$8%v12yKZ_ zRI~@YsGqiQIek?o7re_66+xR=Sx8wZqXfDKx+QLSP0ZWiy)l9R{?vwjFvAKr>Qj1y zHyDfbYg}_n>vr$af?xB?vNB~vvEv&B-B91)UJAkn-o+z8KrvPj>><=xWfsDZ@qr-X zEs8O2;-Xy`OYj*J-HZp)#`ufplTa zK;mH_EIo!_C_m~W4CFXE?h%#&2XI0t(ihav1)wxM+*P!VGVnU6hdex^Cr~1UyTIuL zH`1yc>Lw33QNBk(Fv=N|`c9jWE0jll5lxBkIb|mu2ot8_0%wdr@FFea)|Cc$C?B|T zB|X5yjsK!fS7xfu=SqsQQ*lC5=tDUuyi|I~)GUkkaM4!U!d3NG|H>~Ozx&=t-XM^A znn&Vs{Q{U%$(`*8e<0>aw$FGZcsU5{mtTI~<`w2`3NJu!PRGCi~GZJ?BCO z;GG%>X_o!*E{^nYSiM1r3g(8m0Q*89s_1YcoY@(hp{>5|4&TkrT6YHKc_7b|_K`4X zD3kY)FxeOJ+Ebc9c*0%f&=yQU2#Ye$?;{}%o>%V91;&Q^ymJ^lFcSE2kYl!u_mPZ@ zUA8~_=YGjr+Tv(}1j=TTosbh=1Ys?_1ZNo018;+)NeFel@WKo3k>FuwUas4>Z~Ghv zN+x&4XTKzW@cGyN+F$cME5Q8JmTj$z-cO(@l7 ztHX8HEUyJYJ1OCZ7lPH#5hGabz%RUV!L4kdd_)Sb&*c@P2=Ou?aG&-*k}{O{k@#eL z-|1xgbE928R{`7?E_g50S%&iaNX`S3=y)RZCFgrBDnEEJ@&CE21MU^kzVZ<%gyWds zI1ebG^kO~<%KJ#(pOzw-7N1+Q6dj8@LhztS8tbsC1LuQ7g!jkG{nU*M_9I_-USp=+ zg&83%gdV)kq&$KJAmr6Ae#2NqaOXY}!ae0f8wn}F%Xm7x&Nlav)Hl>vv)cUpjVmYn zNDyk2^TC~mey}2Vb3_Ug{lMbNx;>tmvu}Rub-O-($$svKzb0WAlk%NXdFP&Sh`G>BP;!Y<*p=HZj7xL&6P*sR$9V{1-t?N20k2Q=u{GXa)x! z3GN!{@lYT(9zt{$)^BQXXQB@4f?nr&qHfRomhqB<2M#5773v- z@^X3E((KR++*y!-VxUdn4t`jicq!@O5r!iT!itaxWy2t$hzOMhT~XQ#vy*;x_3(0^ zXcwCS32~8!-&`ndEENE4M~p?vfgbyBW-OiJr3 z*z}nm6Sz|k&+_{ZtVmEIgh2>>fY6^tk#LcovdGU$uYM_!OR@m_x?8QcyUlvL;;bKB ztxkCmAxLWrPvFoe^dKx=fr@jX%&IDcE})T1K0Rcn_QCi7Rus#T31!fRb}3D~9`MKN zg7H~KOvjKPNY5zS!sYnNin#O9dp7gvw$0BxuxmG8vBsuYs)r)ME2JOz4bW#Oj|@ja z?>qyG`njB0m5-&ve6lkguY_l`^YP=y_V@nY-?QKS-QTtEfBy}eyt`Pu^vZ9Z~ zoSgF{h2cVo&M1N37cPfKShCT!!~H7T-#Ipv(CUG#=>y?SKxYZ?gK8xr(v7_IWd z6t4|-`1x0lWR0^4k@!FROtO};B4OXf5&;MwG4YK;NF@SD@A#5qJQwn?C?Ijc(|K{C$IHii;uw3BOj(Hl zWktd=WtK-13L~_4UWou?#iK%jQWjyb*i%xkI`%{lymk7AzJza!OMiGFD3aP*_LeYD z$dX(YSk$wzwQCQi7wq=KSy`{UcKzb8yn;>_aLf~+2)H;(og?F8kxH!0!tzHLB4pqr zUST^uH|K9AM5qq;_1Tav=12~U4T+qQg<`=ACkg-_yhmiPuiN*RFxl>}deGQ-JTvbL zV|dT|#nBO^t#<315FGVUZz>W5BFc#I%Ws5eIYa|))C=yBuLw02!W3#?fxE!eeJp&h zM|_R)M9xt+N&}pIqFQB9CW6iuE9sYQ@%DW6 zD_=Z@5CDxUhqf+XzUYc2wIRH-t~Y@ubie&q;Sh<;=~Wp8lI~>)NnQ40roR zB$yk4=f^_p55_^Ep#-Qmgl(g(Z)~}s#zOH1={bysw*djluqQ@h;~*|M7S~W;ml_|j zUEe1nMZ=^b3LwtJDg?ML1}%hal>n!HewW`Z0z;!Pc~=QbTv1L;x;MxgY*J_8op?PO z5Lgf>0tN`m2}_TTW;5FAnmS4JLJ3j^3jjErj&H+nsEAWS9^;Oj_Uem(c#eB z+0Z#|&22q4GBWBby6`$s5=@Mv447OeWOeZ(eRR550fmkdLXp4=H*enbcZ&S-FaL7v z9g&c_|G}K?Nl-DWcK0G)4d9L8jnq^#KNW(B{zGt8>mHu~e1hO2yyYPl{h?f#Bf>BN zm*&=L>+Dil8XKMU$M(5_BU_ZPKp+!lB8&vEySrP09`6pNfER$0#Z$n0K~Uqd_z@{g z3<%gYj-aLc5Vkofk9V>pK}~u>TZElbkfS`ZzuAGGtF|~)4+TMC6klo!<$bCz%24L5ni|>_`Rn*zQ}6xb{drti_39(ese>>1{*}*Mw<{9^E)1Ec zAQS?vuxJVEr~(&k8QNkjp*)ZWq@{iYB?61dcHWX$Cwg#-@3U7g`n(L|oV{O+bJ8;j z&)7tmedAl-x4RD>+pT9V+eq(#RUg$^trQ`Gk}`M1qj>w|p`}a|$HJPhyb0-G!J`0J z<;4mn_>j5?{~*};&3%L|$^bfm2W>!jvI+}Jo^-Sk#e zT=W%+0lDPHx4uIg%Ay?jjC26MNmCAu)r^b`y@rdHQ1(bT!wMo%0GS~G&p?U`0Acb& zc!Ut%2cZ_eyMR%C4gw=a&zlU}G>BoycDc#pQX>65?fzS1#mi*yK};2^`f$slo?0l1vVAltwJS0>JLlL^6pI{BbS?u|M(P=k|7(mE#Ts%S@ zg}|H(eLz0af~T+iBD`2UtD&QW0{cNwKqxNi$~tKU?#_9q2=URUPh`dRlZ5KOFDsySz{-EyhJdL!;9cLbAh@GLg#Un0}hO71TMlDLzD5sea0X4fGe_q zHqai%D8N0^au0byew2xarc@tgQU>2M59vT7^MQw#U(~^cBA|Z8H}^?H8QgJ?n1_1M^n}Wu)*h@Oyq@y=NS^eDME6dF6X64xG9l|+({rp70Hp{cfpabj;GOPt zK3II}^FM0OfBLJ+d*G|99zA+=^5#RB4JH78a7M{bPEOjd|N5`nTW`JP9_ZGZtieTA zgFrtNdGtd9!o$N=`h)Sx&nprVt|SCZw!?T#=(=H1P2C8$2>ap5_?b=q94n0pQwf-Q zv%b+*pNIwiz0Wk+M0O07P=_^;IENXpq6V^Q! zyTlRJz|X!s;hqG*Wcx#TM!)~zyn6}PhdW#Wln-shyZFhMM%^m{{PS)^;6!rZqIw2KlZ|?{eS*o(SGOs6_ty!p}y#gc&#qsm{Y!b65uW3y`xcU<(bsi zk_Ul=!afa8mf%KcnXxC-UGl=UlindC{MOy+2`1R7br6=dCJ5~V2t2BVCB1^ zsQP&Zo}|SS0eFr#!Z^oUA}#qS1K9#@;7EJQ;}t*w@tiX0AMyk4fvFti3HwN>`#h8< z+mUw(S>_oGkw6?)UfsMp=t7x}YQw6O(8VK( zh90X^_T^b1v?{(z46A{>5S=R5K%!!o5!3_$QlDbY4g1m-zyFW1Svgrp60L%A^bLwP77R^&8dhG|!2#*s1B61?^F>7S& z#696r_EJ2YRnR^1#&ZM|iiF>UP5gBMO2=P3&kz}*Q(2SEvW8@lQ`9Th8m*%x+W%~q z*`U}`kBr^zthj<6Ok5-E5zf$?JsMc-0Gk7u_rNNz76Lmu!zRr1O7LuW{{!$?%8vR_ zB)Pm8d^iO1VsD!(lpQII|M%}avUz!oD80$$V_R641*;}RC=rD%qvwb@OhU6k8$wu5 zQYwLew3WW{FqeLFA7$jvL~G_Xk|zdwMOzs$jHeryNA3A*6B3{Z+*3tzB;#RgWy&gh zn~_Jdw@va2+OPnD@EmQNlLvZ#dDk}gkL0h^*t+=kSPBrMuU|$k;bQu;&>v#`8<@ARpPV2+$tVQ$BU(+z|VKphvcowt&00+4)e)zbLcj!o+y# zh?JNhjujIXNV%`MdOu7sMF*&W)cp4f_xxlWh$@Ewt4wkzs0x=5; zChNy5fgs?>TMY4>=oC7E=e~|!0~nW@&;*Q!(n{|E{7FlmaBsss<}oykb@;fSv4V^_ z60AuWnsQJ8-f4PHoB8A@i(rD7)Ynf}B;W}!Uqe}>h4zl0+Qnn~2BiTRP((YM8@4b% zWh+awZUh`wF@Mvjz6<3;IEy~}wO{)+d*_{ZZDwZH7G^i(iAXtb3-^QYG-x-JJ@Qz2 zD219L{E-)M@?3uiAcPdpTw&+}vOHHbDpf7;^7uP_9<=89LFHJ2C$23C(Yc*to8CCG zYrSMrU;8PH#Zpr~_d zh5a#7N>T(UInq)mb05na@(5Knix1eVLRo~%INytK8}Dkt3qvttJbeAVb$fq$*Cvt4I&N5!)Mrc|-S!=D$*0-34;yj_$}{j@co)5efJakL8Mp%lQ2 zI>}2q+Jan3B?CQJ?6kcb8!fq?Wq!;f0r+GY=#xAW-kP|&wsCT3V?s{0ac$*2I>y3}=Lra;w1&wa&ri$FFxk^W zDZL{t)+#{+jXI7ut@C(Y!s*ajC1l4X&=J;Hw?D`cDXhGjkVk-^J`d&hkubNx#6HU4 zxzSGh^u=B;yL?0nOPQY;?)142pd8vN0|GQ(waeVcZ@n|^rwN}2mOu9}kE3#*c0>vx ztiS(zkK~ErCAUd1v#S~&z?)>Vp9FY`wgC6nx8^_drm zMjr-0)l?BvueJHr`_|F8W1qg%B^ncDi1+U2f(Z_zWR42ng0?77ytOi*r1P*%mSX?c zZ>`!}vq7h87sve7xJDQwO?&Z%j?1;_K`3S5`NIo7U3vD&G5Oew3HG_S(U(V zLSIL}{vIf^QQvZ;f% zA^Zt9;cX<~3E`PUdr!|OOhDQ2JUtIy0!9aL4;;XSJt!z2lqxtSp45%fK;E#BAtB4G z86O*c4aUGqr6G712rDesvPZ&zrcjpZGM94x{H#`e?-RohN(k10hzL?y)3v^BN4V{( zgZx-Q)PwM2avAR@#iFNL(qYqrdu;Bb+*nqOM{=a{-9k}f zgeuA>1Ui#WVhGid0-WzW3l~&wr9PvcqVtuF@2SbT5MsEr2u$)FNEl+7kuL>1Wioei z;!>B|;~^=fB|Y`ik#HZMcrR*}<3Sp5$72HryedM0e8(G!&+ww~(vofoJl~YCnv-?C zw#ibyeVd(;QLv>lchv{mN1_$&jF3iHsS%1o0EK5kE8{sAWpdA-9rMglcUo}4EBQR` z6wfK&OiNd1G>2GGv_~W#m@IV_HW%;&>2OE!N*vMOuP(QAf&{aK``kHO9JzgI`9ncjzL2ts56pNyy zZ%`f_I!RjS3(Uz=6SG=ZW9;$pH%LlIA05PGJG@J|E(bJT7&}~8_57wy&eI40{iD7x z(&Dg2gfnBC34g{c<2vO@UOY$0Qa|HcMfi8O*!3F=9)av*U-wWPq{kECJ4OY3>f>F& zZ}y-;V9Ewh@Nv1TdyF-%^hQD^M0AKc(p%s{(5I3qD@tg>GXUi?J-=YHi;M1Ecu#|) z+Q0>byb8HbS;*(CgY@|vyh6q`Sk36j@N20=D6pf0^O6X%nmMmX z#>D_P)bS0n>TP+|cIS1?t=is_yp5h#m&SM+-Z?H9!?KReE=3`NP$Vq7jiXQ?9vAK@ zJI4{yL6}0}2tT|WpRh&9$|C?jLQDLn z7_>jz1I@@!Suic{i6HI5%9h=Eyk_&uJGQjCYm3t=Z(YX17D@yXA@onfN@Y)=+AYjb zF!7x5U9ozjNYD2SQ{`OA$#dmpfQ532$#$QpKSpp7ijxx5y)2sxd=OaY6-jzQdlI7C z5-Qu9;#&#BqvjUdscDwA)@+|058;adVks?_KFXkM?V=C}MPU&$$C2|Wtk&Y)AcUOA z!y6jeZI7V4DKF%u3q5vOmb9AY7Bur0C@WTAd1yw$&)>dS)R`-*04DJ{P570|eJ&^o zZJ}7^x1>lmn{4T**|w{i&Row8w%Bt6O)9&=6&7#G1E2EVlT;)tG7RVodXxUaOMqUX zT%fO0)br2}^%Cw4b!_x(R=asS-nQpq)fRnD|D6Z=kjsq>|BQN2NO(bnJX_kOoO|l5 zoAbNcJFQaN;%%v_YqHObwb<2O&N*xF5G3&1S`sjF@`*J86w5N#WTDU%0+G>{1LJ!4xT3t-1AG%-Dp?62!8Sdq@`S* zhtYi)lj$1ow9S_g>pmV6_p^RNQ`wI5iUd!F5aavz?%Nw5erWd}KlZYb8+=k1c%m#Q zi?*?ni@LxYe9F&BAN1rMF*2+^|G8IAMk*DSS8Z{02?YY|Nnj!zPlb-bE4ivZ?y0G= z;SM!K7E`li@j!cc2MO@|^vsW!*z46Vk%k;64n*eY7pYmNLof!d@N1Frr-2 zmj@NZV z&jV$;(oy++10B{XPYb?flPOntxu;N9S7jGQ+0+&W+vVmx>#66Qve+-v+sa7~7)Pgz z3Q#c63@;4J9VNfI6``D~LtQq~!_glyma2Mey?Ve}+IoB}AS>9vmFHU}w6Q5stYR2f%YW&O0@PVTiTx+EiQ`r_>m=})B&IB% zJg-E^DNh7G1@MonT0*_5uF)1X7N<52Ws&oakYhjAeS7=R`{v}@J@oc0tRC8f#Y1n; zV|`!Vt@5_)91+rp`pX^_Q0_}SzauFLmRo!1N`KImG-cW{mxaRc&4^MSWzW#dk@82e zkQboK>ZHUPq$Ig&WJJcu*SIbEqud@skc3dDm!mCQb6dx@wjX_SBpi5Ec80N%o}20$ z=B4NxmQGg*@9dlQ#u^WCb?a)Xm0@#8q##050ON)6%EcI>5B$(Z87Dpl5grAc@}5&i zWrU^eWnfsU3yCI zT!5lN$s8QMzt%Y&`fR50$8+$C6F_<4`LL`QT)~~Czc9dCcb05m!w2iNc4wo`Pd(n=*texQ)xY;~J_l%XwjDa);k9zk zn0uI_!T6;?Yb*B4EY3=huAHuDJP+x0!dn>*>m6>E7ZY=Vf968HeQ8V{dwrEJPGGeh z^kfc-iFoFOP?kS(bHpwWw0W5i7dJhGh~hqM9JIscF*~k_egm+yITsAxjmhVaPqrfq zILHwT8(>oTNt5kZ-N*(HH)+gK7UlAMbDIMWPZvsXpE7&9x=!Ajn1DcMBAzi$Svm2I zAZG4@yiBw+;f^o|2uX4Aj4@7rJOn%?pnSk0iikFR=iPVh-s34xM?JKM@~M}ysFMiB z1=tn{>L>TDl@z3ObWYV(AOgzsh5`f8e=115^GBhN2QHA&I?wl0x zb62~qq7I(LLeNbZ6ANuxoyvu!hoTr1lMJ`1!9s-NEc|E(VxcNS?Xrqo`b9D4x*E^q zXjOZQ9ZO)>$ik!HGzRM*%L{-=Smqvv65(ke!atP`N`d(fJQTDf9UjjQMBJELaSs8c z1%!)GK7L4?2#nI;33XD|4CjxDBtG{bC4vQjwnzA5A)w5XX3?fN{TJpW54ghoSVN>m z>EQjPBBKuS5rT6|M7TwnF&JqJb%K9bDpVP zOI!JOH|%hCOK2X6KgHk0W;g^Sdr#7nMX2RGM_G}qORy0V^SLuUN7?ZGJUra04U1c3 z!y2Dmg&*-`)jvB}(a;k*(Vo-3Nksx>;4x@c$Gb^1j^MwpmTJ2-UT+=E>{U3lU-`Wc zZBojN{tU0;G~AMq3IrO(m?iWYv>|;wqYHx{06dw>A%+kqcGp-t8$V?*IULd7k!MeB z$#ChDc+I^W=P5j-c;3?1U=5-@aCL}x-E)F~`fp!`mHOvcUIE@7p>jMBaAqQ&@E1TI zFxd{>%8CTRk8D6WF!nGu8Qbs@GJ#L5a_)gIR(#T)aR!eP+M*Ve0QHiVJQ+ox5BS8B zK=7s_K|mvzBa|KIT2W>KbyFUSjWT(Tu%{dr1R!6akIx$`eY477{)RSz6S9CZsDn=r zofY`=60+sBbytvc%gdoSlpZ`&L6j8<_c^f{WtX&}oKz%?>!c%TbAIFm^qhRO13btlbX82pl@s(OhTkviJyLEo|-DK5rP~SJL+MQH7!SI=z`rTquj?4M@LY& zwX8qBBkUqC{S;-E(ws-zV3_L$bgsyW6gSS^oRNM30}}#oPz;C7igMtD+L{RtRy? ze|RxWwzCootNBSDC-9Lx5R}Q%p2o*=#a5U1{Gdx5GI6%IM^tvYmIZInK0+v|z>g0_ z*ZnGQ&#sKHWo4P#R(nXx;j20ECX$nzlywP`^I@ge`Q{yJ(XSazS+{P zdC}z2rmS2k@5;`W%OGAjE=9khe|lm~u1=M&?W_K=45ffr@NL*R-cAuS4nXW15T^t~r4kFr1JkpRy~=zp%g z!7dIo8M|;FFYMU-#;&c(^19O3Dz{VCl@tj~$IjemhT3d)Qy$4iG?u|ND<#n&S5DFJnyt_D60GxVJLDx^ItGcda5}-t+1+evT1JfKChsL8zk% zIi2$K*a0TiQs}0~GBMomVWq{@b${z(qw*8R!V{t48QN-V&3Bx{d<}zyu+hremIg<) zwd(n{%7Kt0gMFUoJe=%@QJHuz?7GDg!X?JzKw&Xy&$~&I&fo$=FsNN2sY7TFod|P7 zN0beRRJN$yyaQ!!VMW%)roDG>N%e(AHTyvHk~Iia4wxW3t1vuFrO%Tv6AV-xgCK#y z&GS?wgpm;H9(vLr!Urz_fe_a2X^CgcG9G0JoCtB9@+E?e zJRa}Y757d5YI4 zUzJzMmG{^Hq=&{RoDiyVw~UX6`>1Q_=0j|E`hPeb1Zjc2jL;`K_MY*8OM}|+{p5MBcud9!W@H+un!6d zSwNTxYaSpZMn1~#?G|sfwyA7+6H-9blgi{tN0UT5;$TF|2k;oc9p&}bM<2QO0U%f4 zA?TiMPldvTCxXJ@yW^~S#|8(jw=>>rlzVR|pLtu_7fP5gVJs>j&zi~xf717Lb-A(u zYOY+l_!_U;#oOrAHK>6zAP=KcgN!$saY!9Y4tHb;LvSXd_k|$eQ6ehS9brua5lT@iwswKoL zYJ`Uybe~3WYHqHk(m%%qaN(#~bxQFub3UEu&-iIPK1jeFA>yj1X^BoTd4I3*z zm=F(-2MdC7*!W1g1nQy=lpTtsqKnX2?&>A%K!?)_SU=v`%14LO9=>}Wa@nbe_Hff% zqq4z^JbU}ZICpJ+dBc|Eeaz0X`BRpGtij!439sigoB;vwWWQm&=bb!(-tw&tktwloOU}D7q7%Y`1U&BERTV z)EWPCpDTU`uH*x!=p+6@IAawH57GQs9?SlozN=rc4HkDqM+vuQ23vgv5tHeJv6wr- z$YBKw;jbq3BcGJZ-iKv**7w&AZIOv^DUgR6*K6vNU0Kb$vamT+8g!i|s?%{Rz}w<9 z%X`s&|5H-W2i^39-+T!=DX-Bt`l8=>vO>@2T(D~E1#swsZ%79qkb1FbgF$tua{0~V*y2dkBOTi&mi>?pi-A*rHVRxtJmH4{E! z3_A`yOr!*)1CS5Y&GQ(uVfkZ;r$x_m1a4zJdp+Vv74lPuhdPxG`GK6^J11pR22lrJ zSVNzvv$!Y=MMbC_eCR8LHF)rwdQmv!N6FB(h2<4nUSIQ2*38196c=H&VsaiCgiHlb zr*E{0mAB-lO#shv5KvAi7uu62b!6MY3(tf0Arq0CoWaLoyxxb(Py4;EROiX^-)BV2 zVO>|{anLy^gnP^Dwxz;=URlK)dRI{)jbhvmRop5BHf7=eKmPb_A8=0sRyw>e(C44P zOCvqjEWx>7384xMw#h5lJCNHNyMVFu#$|crYLuyoAv;hWfJu2QeD(~`hy(;WVJ~MC zT`OyGehkZh%jwEM$Kb)ZD_q2W!#nu+8xjws{5TvkW9|T9CicSc98ZUO$xodd^3YH~ zw2?5|`8OoO)F=^@6QQv5ND^2?K+Kdf?>%0zckj%rV`DYX(zF;`{li`iV%_C@2m{}x zUQoAek>Yj4F7i`L{*X9}m<|gu!%Q&dsr3)z#H4yYPmA?DO^*l~KNN8utk_RVRO#0aS!nf=P7fL!G@tOk%5lgy+oEoK5Ru@RR-rIka(LxZAq>d6!6y z?=@Oq!eH4`=?Kgxc^BXmLeUpy6wk{O_~he0_$9CL{q%YC7hYTb_IAlY?-^{irWOfx zwdXHB*I>{0$2fghnrvS`Znx#5Zq5>Rp~oaP)^bj^mjS~LPXHl}kY!>Ve!}v{Lt&CV z#}l+>LYW9b0s@wI!V}s$4@5@7duZ`8vF;JF352E6%VEyp{A4?GC)Azqd5Lnh%)1y>(ottfcK>!PiPZLu1yMqGRolXP`ci(PWPZv$t0lYs2|0G z!XZqCCzCQ8x^iSq9-CkFfBRRycHA$<+uN$xfi!x1bB%iwk5_kWK^_K6@hU2;UP2FV zWWBOytqpa)gZ=j6s_kkpy>g+?4wa7M8s3>(ks_$EOWkcgKY~U1YwtdGMRcJ{3@Sn~ zi8iP~>6%y2>@iwgV=@0K$2%V>O|?z*G}~}@qdjCYuV}$$Z>JqfiF9=2q!H)Ygw{sz z=NtziAv7i~CY^&N%KnX5qF3=_Pp||SFj#RYB0tDcX}O3Uk{_jlP^bKE)tQWm3!8OP zA4&n>HYY?0R$&IqQQ%L0w-_{Vd>$PqD-Lt9D$TV7znF7o>M-G z!fCAg2&_}#aP~QSP*5T~Zf>l$YvY{w9PRtsnEI?a^5ywiBELP{sIuE@CXX70e{3`J zN51rzb}}ym$-$Yn2ENcTtWi)uZBO}mdUPnsQeIUbv=0luEV%W>ONte7k&n}oS&j?T z)E`>4t}2beEsf;`jZNkrNLyc;Z0{fLvX<5m2nX`44)zJLsy$MSM+bNcR2BRZHPW|? z?Wi*#agN`3C()j0XY_9x7)Ag4{S>rORy;?jiG!4#z99sO2T$MhbOg>X_Ep>EPPId6 z)~ow%xq8@^wh0TzMnu}k{)~~nJ{R5q^9*syMPaoo4PzIXfz^&cXIvqe5u^xDypj~M zC<*9?aAr|biDiY)aatKd>`Ut;qk8}ul(jdqm z&CSXCSoL;!WJGwQbWRkB%4=(mcm3nNke~7p@U+3_aMUhv&CnTT@r?Jw5ZaLeViKN7`h5tfi#MWGxUY-w zS4Gb_1_CdH+HqUe&r$!@p4DycTI04Xqpe-rm$kpkE^>7sv0yqXy$6&4LYxeDG@w-u>A6oR%XhDfc~Cwe zPJI!Mi#(L)N=oIii!j!K?eBzu zaY3uWituER`h_6XAA^y)z(FiBgcz<64uf;Jqj_>lK@pr9LBNGOcTV(=@1+pZ?jFN2Y!T)x_zv zEgx@B&=|bAz%1JnwBs-SkuS<5U(iwI@z6of^*_%E3DFiVJI0V|ww9Jw3BFi5TwAB{ zEhCNZc;Q?aO^p(aoMJ7i1U=w2dB5!MzMlY!8eZWL)^ys}D?XDAZn1WSj>$0|?8t&)ZO;kPczqD+J| zpQL4s=f1QA(lOq7xgH7ukJ0I&^Z>YGs3NQp?5k279BG0_f`<~cOo+ZgPLcnlnxZ_O z@y?LsGjKz(Aaj7E6-PYr9A$XYo>Cql+QM|kX+8P8>L3364?9*vU5)ApVqUVo~(9E@_J`x#kRz#^i^A9oVeOfSV-Opp|f6p*;Sx>Z*j{SWH}CXH2Sh* z&gW4T)^>Tw+OLgTmC~01;+;RV5#_MX8w$xN;OP16xF-QidFh;(Y)4CTP~_P;5{OC| z%Ll&7<U&$PN2E1&O>H;yE~-(zv{+fV>{oD?Rr;FHfj5fwr`FvkbP%Zj6oDr!HT%4q3Sm5&5s10;?I}OWWD7*mx4}f_FLOBRhLOFa!xGW~4`5kXujeR8L#Wz1lXp2x3 zp8ww1J@D@^NuxCTwunI7xsO<+7==Us z*mu&TPHK_HCl*R*Y_v=6nG_D29SL8UU{uQAun7x+)tPxFFJ{daz0^F+&LG{dIl4OXA=8d@Lpg%hz?H7xA2|LgISrp z^6(lf!K5UyY+MluIRq^h$HSR*TUy&yr|sF)qix$w!XC5(#Ri}(9G~)Y;TCNx@(R3GmB%OLlkWU;&m(*& zKid=O&bG%rQPmN6p2A6fJ!6-4c!ILzw&OkXT+&NkSC$Kqo;tzB%U4_SIrkhl+6mvg ziKRRKkN>~}D>l;dS3FVvFz`;=qrZ=}r{KA0FS$-3@~hu8m}7y4ykNC~HqjQoBj>$V zrQ*B|<*NAVwpeKOq=|9$_a|G$BlB7s`h6bIf`>HNKG<7<`1l z2)abr$wgafAuuEdR%I^2moiks-8?-{4PSq(|X#(H=r`StoNSIq%Bz!JZzc3;hvZ(%H7SY!iK; z5#>{t(^MW7#xeAxEYj9|acuZCHf5y};o|k23#{k%4O0-L$~P7EwKmv4`(V;~=nfI?ow;QXjlDeFFKvfa8I$XH zAuU1_qOtMl(m;#x_Bs^FF7Koe5f?V44Cti-4pq+n@`mj#ZCGVbjppSz68s2om;;93 zqEk2@s|+X-It5RGPC-#{vEKvB0_OA(naZOh2$QiS4nQHX&jn?|#gKxacs?)?X(uM2&BZ<7V~Rx2$j?ObUWFG+u>q7x?GPdmCEsC^ zWdXkDLv$lSD3a6X2q_oJ`CAtdZ3!U+oGDZI$%fW<=nM})uV@c#iT0eUqf}M`c}csw zv+w&p5LOi#d;7Z@G|bmk$aA)C%0ZDLVB-GS{^3584cRxOQA?u!_*T>%IOcQcj0YxE zy)CDTH`_W;;cD;@FZZUxnqo<_=5>bIQV4I?roo`zPy&^r0N1 zZ{h+@%M0AN&nLVRZHd0|GQHh@GOv+_wjao#jK1LvG~^8jbgsCt?2u*6{3kMim%K4~ z&GOg9l~tQrTy#ak_-E4G_smJSB3SWY5WaX7jB6p`UI*ho$1QjMO@{8@txq+UT1LFUe(G7o!o{;)GH((1^SPya?9W4|L=5B20yxgf8C5y`?P| z{_F~#+1OGC!u)dc#i6btRWX{4j{R_H#d@0??caRyx=++I=Q6b}cTET}XL6xe9oHb? zeZ;XVvIyA;EWyq0R^DsC?pGAsV8?0a0y`SqvvSYpH`EbQfPJ5T)*4$mL%&`f#YA<* z!%Zj-yby#w9td8BuhdXkOpJpg1dNbV2wuWa5Ia2`I)i603|XNhOr_KR%!_K1b?o- zay>v;q0r%!m$FU-aNxv$#dk36OJfD;QC$`Jez^?^sUvY9dv@_DFCN-(CxyHKt8mys- zaCw7${@EKg-q+{pmR2?HB=ne1AsoXbHouwN9vv8Pp-AKcVO% z$_1svSmVWA8Kwf@C#-x_Cwn<^E+Xm9r-#0jj(0pUKY>ype1xommiMQoJ+$R~s`Pv^ zPlAU+WCo>(r-4F5$uQyV=W3yBl$JC+r%dKYP)MmP!QB;%6fK7~_H}joJ@BS|p-foG z8F12eDb_3>$WR=HHuS3bnd=u{W20d#&#kh~<~n&2@%{$92`1i$yI4^m%_QN8S3+7E zx6MQ`8daW0b|j%EXDnwR1W1i<;A&w~Pl|@$Ovqp9>9l>yl#-xkyb>AMgTqa;r; zQBPS-b=AH<8%4$Efj3H(>VxIEyE|z2J!yQkr(KPc@NN(Rca=X5N;~uuj0rO#@F5Fq7KJPrr4)q(I-)rp&MZ;-154dnhye+tcWx zl=L>eHOh9TPVNZ;XX=T2l;cl4OSC`e0`5FH#V2SeoL{PdmXQ>H`vU@Jrzn zH>s0*1^x9IZHYEI=mX{EG~`FAPEXLeY4pZy=;Nt+Tk#xCC* zL=T#CLPP3Mnbbo`I0PsYjArWd=Xw@xk=#>zsH^Jl{`6OmAFs-yR0{p}o$JhN;H7Yn zW3a#UOph(g(qGxcnxY_Cc%s{*m2D4Gk$zpmn@=i&arY%$x*8g-P43X=FZM)(Brxk> znqUKY4}0=-_U%$Hak{woSY8k-2gXYKNSJJgeK?ARCB`fOSQT-4+v;tgqrqBbZK^q* zc5a>B@_}ckaPMntu!Yfn+miy}aLB&y4r^~?N9yUscp0z?p0tkykH*VvZ}DB`SRv&( z7diru!$V+VUhjM{1-serK3cV@1t#u}q&W6%ac0|<$CH3T6%&mi7zh_pHt?j$_A;DZ zyC0z(MkD><6W{D>(4dU@i_rKhMs39l*`ZaK8lJ@6dAOl9pMEf?P4{I zTlXh)IJ=Le{QP{K=Yh1);RKI!^v0?#Pqrrwfl#)IC-uj3Xy>N}@TW4MV+wxS7aj|^ z$3g}2g}}}-&+a2B?_DX^`6M`t$7$OgfAv?E30`M;4cdms0Z>Npn(Fla4m@c+?~3;_ zoz6_KpVAJzXrn6;>MA@Z%1W;&@cTHup9E(o@YN4B$G)$Ig$BS^Zr!wJMn_d!xH6bs%A3XBlg_~4*>B?)*cJO@wmJ#Dfb z4+yUg#Y6}Y@2={%{%1dN%#kMv8?usD<&B_7m=!jx@e|F85w)cHA7)q{79@Ledc0YWNv%gR>Ik&^S4>&wN5Mv4V?iLZK zx6=;VTdhHg1t!9KfpB;xKw1o(+c_)<_X zNcl#Y4BQlG`cPSgWvV*)gmJS>@Cc7WIk>NKQ2g8@of`p_t&_p+C-FLdSZd&e#dKDY zaKEm)!FcDr&owp5qB|g_%xg)*n4m0{cvcZxn<{JvZmNIX<$&_Fy;(81kn@KZZP zLC1r3k_Nh*;%d|k#HwNG|H?zC=tpmtm-1I zZzvR<@;CLK;6Z8^`k_p`EyYzXlOG7?C?jwO0TfI59{IqR^!&xMs1t}f5bCGxab6SM zpcNO-oX)hx`+|Rkhfvg+`J-Hwc|rsFCkz+*HTmtd%xxO!tJSvm<9Kb_>2QfB~mA(+6rLo2aI~#oY?@*^KY5gU%G-!ZJh|jN` z9%6^m!7CutMVN}yfqR5Ne`})+w(087cYSs@#fxePDNU_y`%>afDI1gyFS%iic;j?& zrk3wfQQ6c1i#9jteC6o#P&@;Ua$u zo=6)?Bk%`OhzAW%>rAD6u1yJ!3w1=h6Gv(ZVHY&aJ_wqFOVk~8axZ8ec?ul;PAE+; zQ#TM?w3EF2ar1nc0p&WyXGNZ9&-u1wSpwxD9~lCd;1y^WZ9A>=gidD920jKk|8g-6b9OPs#+8nA}+>mmU{w5q|ve5)<+}xI}p%!OFCq4S5|K z2R6O1YImk*bKPRmm4hV(I>(e;Y@iLRz9(>wsN1mTqqIBsC9W0ZzM$UQ$NoLQE?&E zsTU>TAvm>~y7=ulDn0j7c?BMEz+x&MaAJzWWw1W^%>~{lrIWB3^$RE$8JHWqS`Z99NRWf{45ao$s`v91g={ z;dIiWzHVDy6Hfa5$8XQd0}8FZ_*l%L`oR+`^t|Vh){}>>iu)f6@92-`FdUX6u07zB z-NnFRA4xth53F*X@jR63d7ymqg=I>f2yMCdu(zRm(VjAt!)*ZNmdgiE275zYlQp%~ zSZxElr8U@gnZMe##+o+Ob7bpVOG?u$dbe7854-vqRKn*#41LUL2N*U%5BGvx*k#Bk z7`zM!oe1G!%@bxi8@hTDVA;CoQSMVzu0NTlO7u0HJ`RE+dpFYRCkl-lM zfwly%iFQtVA`EBgO}+F@mYFoq(AJZHJhUhK002J^eK`$jr|9cA2c8L(Pa5jV-{cFg z$^0&67e~gp~+0A^blHHjC9*gun2b5>NYq6=x)6c` zH-LR3c*?h~Uh#t)6Hq+hN!#%>2#ui>p$CRFt6T}ip*T0?wSjl;Y_GZ5-}{=!>(vJ= ztzDL2#^b;>CxPfF8RSq3(0frA-7y zvr`t&$ckq%za>Srwj<@W<2&6ciwQGUdO=`TaqWsQA1toe-KBM#k+^Zj&y$8QEcpA1?bkVU#EFYAZ_ir_qyz(zo#pime*1M3K76_)GCz%RlU zK9ISWb>ouDf{{na0N#`G*xx4{Cgn`HOP6vvPIYxyT@BVEFP5d-xXEGNsvcclr~YVH z)D`t4F5Y?}nV}BamFePH<^fxks&-_a6Uau!e`oT0UGY&ojxRw6vc_`+KKDb}@EiP3 zpXc}DTl`UnuE?M5i8S#$7zp~teQ1!@csUJaW!V7Vv)14j6mph*ije*(pVPbG3vPP3 zqCM%UkA4W=%jeM_h%Z&7taf+AspZ>BQAZy@0aMGVAY4pSs zfm6}XMct%5`3+wbJnFxdhw-_;BQHQg40*Fvk#J|pt$riRzym8B;m=$Jzu9q)L5Z*> z)Wd2|EP0fLdkBRE4?r`34=+z?{e%mB@|#b*1?YemgwRI;pcu$|;>qA0C=I+z8oY({ zO59V?6@5ZqS9=I9J&M4MXiB}8#p*AVA4;RC0prbl4j4s28Q_I7%{+lO;bP=5{R-)Y zKLS1dR zr!}hZuGUjU(%D>Z{ql4;uP5h79xX}hO1QHNo_P++|NhjXG2y?oz3cl%uo5=ZusR`# zm(;C%Gn?D?Pv3vw2T5{V{fw?fDUvmHET;{#pQXLN(G>^#S(-&alm*_C+8sEF;}$pe z{e2U3RD&3*T^-Y;dh3;LtG(F+FLWl$c@c)GNVv+eOulbQVSM-9DOV=TYkRgZwP#yv zQc@dCg2W&Jg87N~vDlI97XwV8n8S9n-r24_735&ctTM?0N3O?_8ZB z2oM%wp)r`T%BU+RV!ceIV;S$BJdxdfj<+Tj%)mzoF(?Q*A#h?(h7VeO$NCKKAo?7j zZr?xC#(P<|Y-63>v^(OFdTXxl^!h~s@ffGL5(2HWR?*&%>d;j=lKXzF!6KZg4<$xB zy)LICh6s$0KnOb1{<7c=S_F*~pmFp;2r)jR?HMKu`tv*UlZJ8~5B*U#-=Tfrqq_B- zdyFCP8~uSh?WBI{^x7zszXE5W{ z!5P78k(c|l6ATMpQ`x}>(XV_`CMgofv(NR->x?!;8K=DF`LjKGM!kGb@8L9U-~&B? zD3>-znxIkC;jPepAo52U@ho^UXh(TLyGR$F3l|WyW$sEcuEJ*1V}DzvA4MXbn_p)` zC=+jovhYxR{|X90 z^-#|A{QSu)cTp02;{9ZKcrD=SN<_vuX$ZrClb`-8*~pXzo(T2wie8jq)vIFz$DbPN z6GC+5v0d(Gw|aDLqufH`XzXfVSl@SRp0E^eExR$?YBMVmoD$r0@kX&~i|`YZ-u;~o zwj@u+k9L-j=U%sz%;ciH4k6W8Uu}<8@htXij(sGmACCy-0kdx&>|0-R%(eW{qdA|0 z`QoJkA+Kk<6?=1L+4@_WY_zjQ<<{G*6wAMO|FK=_Y4@|fIB>3Ab+T%U_K2;lK?8v~ zA3{?1<_S~b_E*{SW9=e%y-jT%2nTPvEsgXsf#e%+2yaD4?Arb56}$6rNrG0^!IqTl zBhJbRA#I1@M#J+i*en=0U1~Zyq5M1sR#-9&h_YZ}RTbUgWy(`PK)|F?6!TQEJC@(v zPhj9Opd)OOI1x@jD4!mY!s}F7S;HXC{2sC?aP<&~`h?%4p`9KoDRAH(P!4BM4@;t3 zJDUAPOdM}c{?_&;>*#8=wIwO69cU(@p+CwW9O+R5IOhrGA@+9n zt?E#8G&W;J{nUdXguXF%6L_FxX(wR~$6ItrB^19?xP)ThnZBMt`FRQ?@C;!Wc=Ie* z2~nmi0|YEc=rL(S`Ni`fP0%O$Sc;~2xmDsw{s>z}7%GHq7$8n3;Xyr=&gv{rSkz6K zK!(03GvMrV%Ai~_!voP)cuLq11`?v)9(bj|J3ddJ)4sF37Uk0q`8;@ykl?8>cbs*X z@6Ip!12@KI(1E%_5kWud^wnZTnZTLn`t!c=_NY$od*5SOW6(yKQGa2?QHMW=_R;S? zho_5bI6|gppvjtB2ya%~=WaY>mx}OD%pLHC(oI(?9M}kL@K%QVdR+h`*a;osx$r$` z7d+17JVG7C1!BmS*oe6A-T_7{=^sqb*pvjmZ&XwrAci6XsGA9Xgg2AxVJF@V7sje;{ z(>FKP7;j5#Vj^F{h80}RSg(F%JQhY)SrQ(+FsngG`)f8eKnRg_szca|f>+boSnub2 zwc$Z19ZG9MIOi)lebb|G*@OYqfxB|MXb;LI+oS44=p#FjQWNE@PHCB--(MDSm)2}~ zX~X81Hf>(N7nY>_qzva5wrqI?0mmjL<&}bi$tf!a8Nc%ii^=ahT1r$rj~kKxH=l(tYlEgB;3jjnq*mgVsGK zvBI7dLzVibR>ldgy=T|OhE2j>guw_kcw5~Y;uE(XTv@dOKdE#o@Qd&2Pl|CGMVWz% zF8>p4_|tEHUVJa(dy-erbiEj~R>GSMc z-SB5oTeO|uj33gUV!-!kbLNTbw9EwVN4C?O{#^f2YCz=8GK2m_UWc>K%XRrxJO?5_ zm*~P0hKD@@>{aQwU4fHL5sT*f1d55NcILVH}1sUFG?+@K}x!LSI=l1+~_xB7cudwpiX zo*U}07e>46-MLkH2z!2HjyTj4H`p1OzaEB^YW$W7qzI=B@glE47E2vmug;&C- z=%Pji0RVruwek#gHTeDyUTZ3w!QW|8Cxkfcu}2;NyViZVul^pbZTgWafA#Y>T zcOR^H2SBh3-EIDoEI;S#2scuUaa3cI>l=1|dCh+E=BTu$*~67hn_Atq1-Y;rYReWl ziecD=-!A3DOSPEmU;=(}rP#EjXUmV5ZFhRvwjV9nTaQ-l&Bk`N4R8u zR-VW(Xu>npFI`yg+q>IR!gJQ!+huKSEgs8d=`VEIJJ2B9TGzEEHy%&8-i|6&ewr=b zC804S2JCP(4@o+W%Fi)c81&KaSj~Jt|J*OpGM2rv?$J)SO zSYJ194~(PSVKqDo6cBAhICBqWgwV#T$jN=m#5>?}Z$m;FCFHBPxQdlrl*@Az2MUJr zQ7{Pqsku21VNnJOt+Tz|C)J1g`rKoILQopd1CJy(V4_e)`};lI^;7h2B<7Jo^qJHqG`sr&IkFnO98nF7S?Phu-#e3N!RRBkvv^D5zl9n#=&#&#->JHjd!Wb)67a_0B z%D(-^hmUQjy~##8o9&u91utoObw^4H%Tz9?>R@RxVKNSUWQRO+EVKpBsI9q1%BRtW z+FET<0v|=j{ulO};MJhKdRo}m5*r_RPS_2FNSN*YgBp|V?0C*>5W0i$WI4<8sO&O8 zk&ymaA2168iGhw~5EI7vJYP^m7z%)aBitY%z2tKoF7whcb|#^7apTU2ue^5UPn&y7%0V*2e$TO&dH^BKp8k{3Z`dfpnyC{PimZ zHEjWWS!JUHlC@aPvFM>x2uW67jgAeMXhC_Fjv;J1JEvcWT+hf+eJ1Aj8Faj@a3!UecTdp`H+R{PSc zE!Ndh2yT_nv-Qnm`;}kYwA=R(q9tL@AHS70HsVHm^EvGaMG){Cp%CI??teaT~f z=c+Rl8RG!O#y$3w)I8hQ{aSl{gCFz$V0qoXbYa*#hLul*tPt3(YCMAwxH=@Hq!2i< z7cYQ8iwA@7P2e88*vl8ng}D|s3le_fV$z#4yd?K&yO64~K6xv^H}B2Z^??ri+(f^K zjhiQi6c4cCioGKldgFa4yAmoW81|Di%9?FbL*JfT@+0Z5_jM|N5QX*-KI0T(TEm9M zk?tm+;O7}Qaoh_y-IZWk6OpzwSXWn${B3be&K)Aa1TbJ;2WKC7Gz7J1pzrKzr=CnpnE*J%z&Qm33XIBJ2$SitB$o1HP*RR# zfG`o>C_;Y@P+zGn-~+x0u+w|0TLriP^R@(tQm!;u!doL+QWrpb+B#bexv;;#ZS5Vc zy4r1*mxqZ@2s7|`$iPs3RpP>xI^5Whu06!#RXpS*nIy@A=lrL$Q?LE^b{ZUVu24sr;$x?AX%Ay7F?Il#EbtlcCiq zVa?K56vg7oil5^}Xo)4fOnC1QvQ_b{h(cIXpKmf0pEGB{?sz;D0I!A#bp$qo97RI8 z$km*9_s|*wUC#+$u@Z~<6DIfZUeasglcVNItAA3A@`Pk$H%R6ksXegVIA zk0r@?ASey$o?l*ZoKPOTTOQ@Ht@b1y;92zxH?ACWoXMmV35PKL@4xaXY06_i!FOss zJb;~peH-pjv|oAfC351YmjCz1eNwp}x4eZ(|bNUCs42*4tFM+{|Fji%)3Ll=aBpHwB@!F;{0`n&OvX1maUWN#v zws834`wJ`f(r~Z+*p+c9oR}k-r9IM~Jb!MaL)Nm0dBAdEDUhQod$+#DDk=w9g-8ZK$b}!?mg{@6o5g)gzS;5nsoCM z%4KftauVuVF{r{~OCfL`NJE$^>cN_+cSc8OMLuxM=K=01=$Q+)LP;?27=S+79gbx^ zv2MP>+vD>bx(=#0)UJx{?Z)vDHFX-aV&WM5A&4oTm2rf%n7_hAV227O(`?fs!C8q&px_3~N8GTrM*L`T=4Joz7`3l+>eH3jEPnhzkx4i$N%oowl zP^@S35Vt?24Uo^_4X6JJ&`#l2l=*-7U-jC*@)I&Jn@`96p9KHgzd2>U`kQO+Sq5$5 z(8i=EV<3-Zp?}Y|Gn4`Cq;H@DP;Mvm&y`{c;EYG|@_UN6F^QEi~DtsAIKVp5bA`5K~!}+DZU^LYz zoaln^C<<#<%!cR(z?rlzz;(g!5%vk|%B?)|sE|`@Inq(EcW+Tg@k5Np+{7 z(JurWRy<|0CnUXuz=yn17EB%Oif36zq|+t(>!GKKeEd8ectDn!<;1;cFN1d(Rr_p&_X5V)i=;T~-{qbQ_w89;SmLHaq-N{_xb`) zKRwplskB~J;2d<$^3`tgMiU+Q>E%&m{O7;Ja;HBilRBdwfHp)33&qFl5K_^AGx?$% z7j{LSxL@>j>7sln=2aJIVhM2c8-f+Q1E)~Vz?r;&fABkLp8zdGnP>fg3u%>!W&4F! zTJ6=Bo89Yp_2niT8DdP8%lKqi+c>nI&RTo+T7-tyLQdx zC@UwsF_@4!$RSoy(f3&XZg^2Iyr7G}1$i_GYrFvfya<8$9umS=x{v1oExcS^?xCoB z_@KF2^18Lo8X7fH8iZ@@u{g%MW zbvJkL6I$awc+sA!|N75=&ClAxvS;W5gm&23&da4jftby2?w;&y&wV0Ot2;i~O=t+E z!IAGPYHYK#^o71=Ym)U=N2d$eEssZnkRdEKCJ%?-%#lFDv7RP-e!RnGmN_qm2GBWJ zCg!`)Sp~+)!b@BGK99k3LRp;c)z{o&{jCjlbGSo;R0>SWsNMxw0F&rs7*U;x``yJ& zn^lV+)Hd0HItqrEzQ6I*!xR~OwY6bc0|)H0K*$KvdisHOnTv7U3e+DRg2xf}&hDf> zYmp&LS&4RPtuo($M^g5JHg}d?nWRSp_@+YrrAI=YZec@o0cHBYJO>DHEVBQ9JQ6Hz z1}Qt+%Vo!N+R4kWb{t=X2>5#2lsA}`ehNmdDBV77P`*iuQ zThOA*$E{J8J;v&>GESkW)HeNbA4@-%vhou zUeL}Sdk?lEO;J8&lD}*XH`TNJo>wrCABD#EQ@#Kk&l8$OoeAZg?Ub9(QI=6=)CEpY zv*gK-*L%uqs$YLW+aqgeII{oye=uTS`$C&P|I@Y=1V3+eL{X6L?&HU24sB#V2v$F@ z+T{~rPrh;0!DKqijqzG?`R~w3n|1ozBYF3U6wvOzlvZpm#N$9|FwfK5)!}1`H00;L zKd&(ES>DU5MLMjdrc?T%-T+`gpTEO!Yr>Po0vK7jv>2%RcRqXbc%;4AhC7>muLmn- zmNxh7(aMg`kL274UJCm>zWPj`FA09Iur0(jxNC`FSJ}O#O}oFer9sagks3d4{$h8F zbqN_x=vBS$NqAXOIINOlZUsexfOiETAx`KD;2G~n;Wrx>L4)PJYw~tDx*i3>DZeYq z&wEk;e@{tUlP?Lr+}|cOA*)jSaC@FkiaDRT+Im|SBX9)%`X=!|h$vPRYDWiRjzabz zkYUgS1|y0jCc;Aiz<54b^+}3gc%>AwzWSz6G%y`$qReDQ@`Xi@;9*dFyiD+j@CRvw z(NP*1XJ)7EdW5)q&`8)(mKvQF*m*?)1fBs|CV8^G3>0PYGI*{y8o_qL1?7IB0!iF%+5UWm#R4p?s-&0jB%lGSpA#Yjha zszmujGyPE?!Xp)lD?AC$82EuRSrS~J=nC2uUW9N!h%=}I5BetRa)G1ge5W30mi-G5 zTJ?H$55*DTucD8|J198-oVW+AQiuVpN=x3LOrhYyD*G&-W&e9)&a@}Y!PBs(_}2SV zcfCEyYv34d&3ec~yZBAn)CnD_>+*#L`=!6oWmhjDL^%Ml-a6V&g?_^Q#RUY6+uDF$ld){W1!8mo=IxVRp>I~YVuwqOlaNoy=(zy|(yv*Hk zUKev6cqStfaC+uMku0yR z`Gsf0d`EX@r%$#sHhgj&4?=&vGTb{ln2(g~?P<3*$w3qX&wOdHF1(r5wM}n#Y>0&K zB7DmVER>5!nN&8HFJ{_fSw81!v0S*x>aE2z3jMiU)j%~RjO}?)V-9GR);IW3BMuJxi>c9_xx%|HWf)YUuev{VksGifoO$s^# z8gW0;`Q>SY@iUL8N*2%SgIiaYN&YgBk5xDxwo;oBGVYx)NIZSy^ExP>8XbYi8)=I6 zc-;wS>mnb|qpWOOUbKRGsG~^jWySscj5_j)x&o&Vw0KjlVDueE$74XS>4xL#_$VJ< zjxPz;bA*DpL+ynI@#(@E#f|Xrd;wl>@fSc_LMU-%KTt=uE9rKo%}Vp6c9d%J3tAvt z5WWmflnRDGDygXd^x}8m#&zzQcs}k2cq@wb=!@Trv}fBB=>l&rrf3fkxJO=!$o6D9 zx28^7Rb{O$)iyp_YgaEe*!9be_WX@Td+BDAz3^Pq86~n^X_7(MZY$dzwz*p`;fRo9 z718O%LYDR2CQrk)e-QC{V4$zgZmR4TZ#LRS$*}Tt9^E1;AT1R^xBRrNG3ELcbn*H_4${v?q(rx_IPR6W_iV|g#VA;8t_$OpLA$r ztB5uvjoTnXae6Szf-_9@>+eiOh)mxrY6}GTg-?z7s<69roA#~423wVX!t*s*Kbxxv z;TZI3K_+1FA>4>(CsW15Vpu^5gtq9H^2B%SG7p^6qrn}|4g?tRX~~i{C!)`Tdo&V^ zJVywThqAz#eIS%`9x_iEVrP7m@4~mp=T^7cdUjBJ93l(`T~0k3z^&_2JIa&Xv>hwB zxuGKzLjfUV!%9L~k|)melC{+y?{GMf;Xr$%%m|e+pc`aBP&|Wud3DjYw|R$72+=IF zzP8EQn|q78Vwp0$L%Ru|p}YWoGeGcwJcL-Zjq)O_5&d4Cj4y+?L;dafz*%|1imy2d zFP;bTfD?EoxOd_7AsiOZ8|qs8J`f?A2yG!WxexB}Yv2rSF*imT+{2?un!5rkXhS(s z-pBjXP>nQG%zzAKv?neKdDjeOYJE_I<;iANWmct3RDA z2DU0)_PY;%(H^fq>%YHp?GNlq-viMtX#FJk@Xo&dpa1rfef#?nJ^|06 zvJb=SV86}T(AJZHGQ$u%#U=WNd=r4_x36c?67683|z zKV?x00m3t1!!awZ4K+3{S1uLF{UuhMRcuYx0F(UKWqTcMwjnEMZL4CpSB~ZF5c|~g zUFJiSNr*JnGRaJ;Prsp8>=TLK<>#5-e-(9}!dbyW zIpp^;^aw@3%cb@z#96`hQ1p#(6=h}!2cSm2ktPHimdNQ~^LmTA$;(Ar0Ba`}PsH=c zPx%@sx{oK2?I4c}Cr>G$9^LP1Z?-PE)cWao`6f{zM7dkr2=tR2N)ve;e+iSZ3q!t# zWMOVWI1+M!v9aJ9WMwr-K(B?sRDry&8@IYB$UmYTv>u@xil{|QjsQ+Y!uO;tv`l5{ zLMB^Nlot?m1za%b8TCYdjD!##yA_VxiSXinVSv!K=m_(y4Z{?SEJe<(^7Px$0r8U@J~*#Ces%q!>%Wz7S=HGlsr9k z0r={ZDKpwQXHE!(XX|n*_i`kOOM?aCaW*(Qc9ZdTS=L){FsvmH4{&tCuO06Dci-{o zC%Il^Q3m&xMhHw5(eRlfdg;zeTwPkv-o6hD_Qi&fLdm%DutSQ5^7p^-N?cxA%Xg04 z)sr{!%BiGtk%KrfGMWy9(FIq>Gj+uX1XaKp^s7?Ka#r(lF05A&#@YzeT}+#?vx_wv zt~=pXyo{K*95Uz61x%MsXef5LFXJKnBYJ*iARrSR1kQU>IqLCkmQ%Y?|(V?gGB){}GW}g;C~p?t0A-WMkHd?843~a&8Y0<9XWTRr z)iG%|&145ZG@GA2aas-K;+g4~o8k}_y=M*=idX$|B%2s4k22bMpqCqe?|F*eR<8hyatxQ&vB zDJpPt;IXEu7=f_Z&S&a}(or9#97&(M$JDtmV9}is4vvI-1Zf(i;}a)BKG3i$tbY)z zp%L>>|L}Jno}V3#o6Fm=vcX}Np18HP8LusD$hmM}Mh!*10f>b8OS@Znuhsd9vH0S( z+xb$eKm375RZ;dL@5g5@-%exy6-s9dVO2DYZy;qdGJ-y?`Nc!w03s4Z|<%x%ho&qo(j1*GI z6vECAl(UVeyTgRvt|ySnrS!QT(8N<7NYhK#eD7y2z7eRfp zo*kU72Mz1^)E13_DJ4G32vH9f<2jUwQhG}0sF4CNN;REOQ(I3yd@fGTOenlJp15>> zJaPYhd3XxNhG&avGz-fs@he~al18Cx$%&V!tKrTIWPWXFbV|_ao7nU`2 z<~CHnHvz>T9N?V|u3I(!wlU7piU2+(Kg7fJ?WN=cJRTdK$*HfJ-l10JZG%UD)H$6b zdLiBpsFhF$!rXE2G!r|{aOlX#{PV)O^~y%t>A{J7alDpcyPVtvko>e>!0+zV79)fz zvpd~Z`bE5*o{b@Tk#y!;7YnC#o%-b?{pjA9z=+1Q{p6oH7oYg>gcpsOI_`(RiJ}AZ z>of7$SKl3XR!@koo%ptk|8tz1csAqudMWpf&f}ZG-T|FZ>+d)2d`G-6|6z3$C)BR5 z#5*p0J*G#OGp+zRY;7}*=lcKUzrGOv@Ml+IX_X5D)w)$5>ViCcQKa-U>*+GGeTXk^ zGj4P6#`5dRtN42~-s^P8(DLj$EH;#w-wrR^QFzRImkEBC!P$Zw{SOZ~p_2 zJr?($I+gg_Ba*4v!I+t29fB!vjpXDc(n06}eEs}RPC=1(WQ4`k7QVC4j~=np3`fE!A6z&h>^(Q2Ga>?*(gGi4)J`zIvqmyArS|`Oo_z3= zOf?b6*$}QMVNuA_5(uR(AH%TrxpM> zuH>i#q40`cuArx%&!~ilQ}N&=E!%vl9HSquGvSQN9XSt1D$vOp6`UJRXkF>%)yuuI zu^-pv^26pz>PzjFbBFopKycIy)o4ipX>#4$w{m86FH*;&NUj6 zt&|ek;&^5z#^a>o*EPbllT%&tpGK17BQne~{u}GL{=xUGy6ET0g?S8b^E+v2=rLW^ zA2;f(xyoge2fcf*Q>7Dc3;fa&$b4 z%UpZj(~O#A7uFAAPd1BsqRa5SWZ4Bcr~egJJcqX#*n|Ta#~BKJ5|*-b#wH$_TGoW+ zUlV6&;snZ_aW1pzFf{qWL+_s3G8j3qONw(STxZljaF{m{kBzB*Bb*hH6O{dZd8kg_K4#SW|mh27Ni{N2U) z1Mht}E=-NbuRMP{Z{X!NUM^o`$JLs=)|i+>Gx)-l`MiSV^2$p5k@r8AySKi6V^Jdy z_AW`BJs0&^Y-f?5=`w_JLus$7;juB3w-L@?7ehs- zrx#!t4v;ejj3hit$lipWHV}@$^Avfe^fpYG%baxX7LsyS53~RU^R?fmvwd*zc&aIgF|B59v|uk$0|z@U~vLUFtM?CHsR#8Eh@M|*oP zK$$fnp&sUnX>cP;MRxg$QyIEj8wO}SB|j6_F4D!%IE`hd4fI{@AA zfq3|^vd-N_8yP-4IH3{nXyT(B>ekka(wjC;Uvh~a9hIT^XxWC)FM=$A>QdZ#gaXe>lip3+7mN0oab0s+sx@E+7b)syy3MR z$#KAG9barmv&h4)$%2{^!~T+IQ*9ZiMO)t3kWWw;bh3a?I-ut-eAkB$aR7`&>SSsj zNL=2|^Ba`JsDMiyNA&qOKmURRJQVM_cp~4`4)ZQAs28b`1Y36*!EnXN_WquF6BxUm zxU0gMcjKVozJ|b4D*UOlbFsAH`?9zka#X}mF<+?soN9l|xhZ`&Julx9emM`e2fw7@ z@Uo0PqF7oxjD=NtUNU^by}ONabbAX3mBxfn5he{v0j+oowi;UY7BHatOr>bj)UK^| zd>U=aReV7yaRElX6q85T93ilw3JGZ~jZ5}Ynui8}vSeQIQD2fCpc5~_LleJ?uG*PI z7TMgednhQ=7{&r@42zZ?+pG`10qFdw>jO3LTn?r(eVyE1Hbb5orUH2{r9 zILVv6A<;z|rpTyJ>C%`8s?6ljy+!mr-X8rJO#8A4dl#>A&i?+gkx=L6$csgiD5}XEA2_zZ8SLM|WfW>xRvPi?O`- zme@UWNHsW(%2XBW8nUc40F za3s{V8!s72mdKtdEYg>3NMC8HgW-6$xjIv1{+h>@OP80#yG^)52xSGdVbuDtmjljp z687VxA9y(4@~96q0_0_(n?-GpE+t>tQ`%4-Cd^R>j>e9Jh4uF0IkYXWa>YGsEnE<( z&bm5%v>iIhOl{#Lz{Nv)MjM1f>&6yql zX>{WXy4S!bxDa>>GO?-z)uo+AO)bOuD+S;WW?DH6#CP-fPd`!^sZ?1`3$GsH$p=t+ z<`FnOw2Ak7%t^9<O3q#a@-P=8##9^5$v9Aal#k8?ikgdp z3LhC9mJ=C^>G8=_cpB}ddP?lZqUXlWqjK)9Z)5NT#ZSO-C9B3zWc^$5SwHv6aT>hZ zmoQ|&x|W?o@u+Kv%0YGT0^lwDdZ?>)#Jse#Zx|<_=qkVRn(x+&qMHu^1pugP1vfc#c@^w-7d>D`6p zJvlG_K^#4FIwD<}{FXs-F1#ayyK!dfZkBmw@=nKzP~FwFgSfl6pU?BJ?8fJw-iptB zc`IJI!l60WHq%mn@wj6LlC72@DTwEbWmfIgxwGcTEc21Ec{Kv)2G{xr#oGC zr7@n$s~`CXq>hQ-Oh0X|Yjm-^p3k;A*GyycVEAb_*ib%cM~p^1LLj`s3N1-4b*H1s zCKlONIRu;n;pE{nAN|p0c>x{qv{Aw+C)Zu%JhnAB;01npiK_AfjCODyp3ZJ$0fbo7P$-AbfjE(;k320#@4QJ^zf7aF*Svu{Q!!YwjTcPOGLa zntCzZ$ELOb4C2n#LOPM-Fg$uv!uPwU+mMDvG#d}oZA<~~i1+l`4`lGi;@wX?6uf^3G@Xf&rBK&eD>2-VI|n3cFBnG{@n;`k((%eB)J(LZm3U0N!i}$dQEj z^k)3X-jL|5X(ET&S;8wC@9`sGQy1^Er3tQ&yiHy&U?({0BhP0J*!bXKw9fk$e!??PBd^MXY}zue zc%bqWc*J9z=o1&KVyDQ1-dcZq;)j06d*egzeJJxVLSW>vy6&?yj5avWLA$D{Eei-?Ig)YNSVkw<@2zx9UXQ6OQgAd(O>sraabT*9JUQZ!Ee6=ZMgA(EPGT+QQ)b`y zKX&Ppj2-|+Ai3RkeK+o`sRA-wJbBW~K2O}Kx8ehxq zEG~I`=;TDqjSl5I+j&9N{nL|iZj#qb$`cC*C)-(^=V}w~{My>@jcYNG7b-64_tm96 z4O@KzeT%aqDgy!4Q$0b+BR>hF5z*jmFd(UVDegJ)&`41d8v{3f6oho8P+q4YH_jwW z$#|Pwd+}7-RJ@u-sD*D2lNohzg?lQh3{B46Dh&uT9bW_}WYsCLXE}VjXM^wo;`7=E zg+V{YVt-FgUxp66!$Xr&fC(wdxSSuo@X>t8w=d_wV@HD=MRS8Q4G;Wi9HeRE-7L$kMGBKet0h~oH=d4!4N7}yeH}cMenH*FZW}5jV4KgC1>j6%1 zB;N(J{heJOz@?*x9Ux88ob>S76K{R|e4IZ!lLjB-O~;*%8<)t^){rI32N--`qG$)h zC;>|PWV36{gDHQU2qTF;wMP;#0^v74ILpve>n2nWe3U^NzO(z4FQlm%2u4kO0yw|E z?`s?h4gjOhLN_CiJ8Lu?rXl*`@$(Z&psLyw!mg^`T=eE{Enr)pydBX@6@g;X_yE_T z49B}J%ybswADo$t^OG#1_T){$IIH_-#$r~EW>y|@ZSx?OWMp2DVlApst?0RVZ9O(t zb{i+sH4gQp!E1zwA)qG+pgc6p>XlXR!^%Tly1iKhm_7cM&;cJhBZbQ%og1}q=sSwW z=zwyJ(~a9s3CFUZyuArs2CRKjy9RA8{lya(jv2y_>1Y z3o)#o_0Zr%j-u>H#x%myNMd_MJZvsngT|yAJ;5O`&4aRfPM^bZC<)gkoSB;v4Trhr z@z|yN^U^sM+US)~FO(3)VpM{oVc{KGxTKDEGyH>J{&ald*%#y4*RCktU}q#!&YSv? zz60V<_UJ_yw2`Lxk#+7gLilJPe>oE5O+7mN1b@m0eCA=|75~{r9tEfbsy-Zkj2Bw9uMsZ{%(0qkeXB<`ZF*sj+p1-vi&9 z0i%$CbILC#((^_q0^GQ>7k~HX*5aT2`euCf`R#b^m5o?gP;Xgw;J6dv!2w$qZg<&X z6HUdNOfNp$S-X8&SJc63_{DFZ;jxXdeQ+Lr7Y?B0UpV+qx-6eRXc8XFoaKVev;#St zp*TaP%sk-X!I7LhGnHFz8SSG(I1&s#zm-Hfd6**s91VFmq2)|yyNq~fvs}5t2!>e#vg1VHCmUaw=E8x1AA3N)^$m>-l@6Sw?fKNd|NVz- z?*h>It9Li^Y7%b$eY2s%J_Q@QDifx zJ)KbC9ac8}C1eb!M5AvuDnK?Qj7utqeoMdzzHla!Q6fF12rPDklem7-$0wLDG(h&I zn?{fqB%)lxyRo^fUaaX!Mb{tn5s!l_yzQy2q7!hK^OP~_7#aQTY%Ry0925qXk;ll$ zY?e#TQhI@;aqTiZ*qv2izPlUC{9a5|hULx0&|q=e9jlD^}yi*fG6Ox`-m zf;mbCyz{X~C<@fe;doT<>F zr)bF$N~c$b-XNPUt(28GN!Q3Qb7M5bbK`QTQ%jfST&V|OU9@Ac^JM2x8K!O?fO-;7 zImutusm!R0}wQSxWMR0E~r(!vL#Pkpc&iL$3MRe+!j~k2AF{R|Za z(GmWf9usor(+9Yoge%>ZmYr8CQYsD=u5EE>)DJK5;d<*_Bas5rYssFGaPZFPr8QpS z#I+18{JC5j9Q4ym%erT!3CKW!e%Eu1Zyi3}1DvCkPWY&M7rOZP4xc66EH!1MZ;VVZ z<~)R9DAH(&Kjdp;BX`o>Dub|+C!twJ>5rbGv5U+9XYm=LjuSW&vd+?n9HmI-_BB%3U(%z|O7r^O1PpZ# zySkTC*taKgt&vr^~Fh42(WAKe-StBhM&;ocK(bFu(y&Hs!MTuk8679b>Aj9GXWeO??#yUYu6nxrxzFvV)43Lt*3> zRvATj5&Vhs6LDgatJ8UB`EJ}<-H!Wa)H4x5dEw$FQ&anSA|2d4OD+=-24^#s>E#Zpj807Ll> zLjZc#7^V~^f|fx{1<)To5qcOHffN*+5oCTnIbvyg6>hln4lHm4pFKgso1RJa0*M1> zjSf(jR6x~leQPOuA54vu5(1USUS;}(PL%IwYvWFAYb3I}vzF^1 zV`Has3X72hv@klV=?ypp4U|*$pllxMigOsyNM?9=D)a1!_PyOTJsZL|Cj8^-NX*8l z%Hh7NgTr+>g_U#|94cfg3*h%_H*UtO*Kg#S$%WIW^0lM$%d7F(Z#*9xTb#IOHx9e} zyc?@z0Z+^)47eOj(nurqaK2=5{3-dr5xRNwCHa9|`0j&!F4H|vPa{>C>GWmb8F`e$ z+uY&hLKuUt4#B&kzpEeC74R4=Pktjy@>DwLtGcBLN?fWlyLXg_>t5KjQF2pKFX&Py zLE_M->ruUA>oGVv*^WHH>#`X2IxO3V{-(wcN@haEu z6mJ()p96MzX~c0bFG42t9K9+&fv{ZvICb>tJE3Yez2IZ;PkwbH{)fN653EdJu#Lza4xxvOJs4&$lAI_Mvu(SesXc0d9njlem@pAI6q>K#Ci!^ zg0C)Wc(aW$JdD{fc9rx5ZyVgfgUL~FO-f$%E~dSF<0K91AG~*BTc34c5j?q&#*@l|LBWVP&W8!Rh9?b* zcxHul{i=L^?+Y)6evpH7GW6@OHRf^5W%Y>H3B^O^6kZ^!9j>L}AR zyalg25nY_rAKt-3zvH4u`o17tUrHyE`lK|hD|nQGv!gR3?x7d@ODyOK^oysGV&cuZ z=r^3MFw!8KE{)UyyqZSsmdqSyMUyedBBoaL62aoLoaX;nt1 zRXOP_)n+FrjyjUhzwm%$9r5Vdm(=M~M`c&WXPcY6akh47ZvaLr`;1QHR77+1dKCJt z!q5Ebdi=v*+=zei`K|cu7dGRySGQw%VcVTZ`dpAEblrMpw)~A;vu>r=S!ZPF-_#RG zr_jiXPxu3L;^9}L6Uoyouqi`v=BM$EB(`5A1-?C?UH>?;72< z+)M+cc4TVAgy!!DFP)6@XQ$KHGh!%UeFVeKPN_|%oAkRno&3g$VBpD$uQqZ#<*+d~+Uuy#Tujw5eA5gu^Zfq0~pI8`_B^>VM(wdIX`pZV>T z?HpmSBY+VGBZ!qv?ktk`6EOgVag_>}Dl&?JP@D{FAwGA4P%~*dj`P!FF*!1<^g}T% zPx#8lVLY?CAJ_K#VrlCzHuQaMnfG^hMcH|<}}2mFcT`C z#$O8NMsAG-TxLB=km(bD8U%u@5Dm60j}ZwwF;F55fy-2CeO7(|oX|#{%D@#k%;i?2 z1rG`UFPYNX)kvXFJymvN;YbEptj>3I*eHWM`H=Lr;pi`*ZBUb+GLVHAXL6t(@Qy}3 zo12T?t(1zzwm|Mv)5vjYHEu5~=~Kw-y#-d3`OzWXV)hUS`D| zX&$Y?8x%TePp9gha+_aeKnhCkT|P-h$xs`XcAOvcv3Q#zQMm>XV-PYhf6;)!#~o)I z+bj8GobY67#aTA`m3)Ry%ev?;*%!x49u7k;=x5Puz7WGYOpY*wgHN0adX-^nsHCfU z@!h}4Z`xAkSr3ewe^X}Fo%GO}G&VH98SJyeP)=lfO9pXgE6aC1+IjSy#rXX@e)1d& z#<_U%Mj$&|csW4G^R*XtdQ* zjpePFUu7yH;@r$|yzA0*+)xh!M?}H%>)ZJn+y~~y-)_g00fq!&0gMpng#j>=-d~%O2*P{xX)xR=)9nGh*}Fq9>>gSc+Z5jn@sC3a zM8*z-!ZZL-Wwx(K;cNxQaCcXhRmIc0&TVPTZH|Ih$Cro6)|ow9gKI_gqWGzB?lD>`d*(grtP*V)rJN8^zL@&p)VF?|GXpCeh& zD4eU{9S>iinavW&f-;BYXb1X6QqENl@`4{2ku*HYG__&PUVTUf7Sn@Hdy^i{DVY5iH)=iM16w&7zZbjf|>8 zcAPiB@pz`R$Pd8Vk}*6-hRDZxO1}*r4YV2;B;Erjio##l4jU^)w64YDq6^ks_Z z2|K%(%HlFn@1i_0Gnp@+I)07>=aO6W)wxd`bOseqS)6*uX|T}Fw3!`-9Sb|PSbWE^ zjA~H8SryjDiR*i4X6lo?!Ek1DAWlsV$CivDBaqciIhMVH+};cOw|DTo+>14shYwkt zzq7iNTYfn&!q#CP7`P_JUES}E*EbI0hI$__%`1+*-*PH!-D5XaIV%`S9zHugh30=g zQ5FnZ_M+u1++Z!7ZoGw;0AAvRv-`8FK;SJy<}%&W05@f%kem2St^7=B;bc%$-yZDV z2xaR-|DfAtq_XHqdQ~nDzX>lo$>%T|NB3@(t}%YlV?dduNEja}tka8sO?o(xbyGP1 zxh|H0jUhdKH@H+_%2e;}2sc1#1Oj5fr{BU_a?hJ=7@v;3lAqAvH8qS=Th^8rJQZXf=s2niHI)H= z%C#eMeJmq)Z1_z+HaDQ-=8JIXk95$K5)nH89InUhyPNU3ukFWIp4*FOUfhW< zd}S+c-Snt~Hwm(U?t1G<;>-in+sK??h4Ve@uJ*!+mU>lPD{Z#9ZVsb^2Te#*NCOq> zPs()udNMTQX)~n4N!SL^!SK{$8)NEf>IcOtUn3jR!GG(6!bnG$kRn%T%)Cw5Lzhm* z>64R+b2uY@Z~LVK&31L#l5dNsck?hJ;T)_@?Y`yc;R`x7EYvofGZ_ntpJ@6Y^KtK+$aH*Gj90UWvYBeff<%b0pB@r%a$| zf$M~oBiU20nc?$Z#;bJUa$0C^dTRP}FQ2QS2a2u&ZY z85wgp3_3{H9zA?3j)b&+;Ia*P#g_vB4|VZt06bL%E=JwR$xfC$awP1eYteu)8I;2Y zpcgzMDC^U4Fe(#ccbxVzW>p6s(*LVC5_r0&QTWb|M&o)02SsPUd&Nf}cX%=FwHPnF zx)FcpXM5tGeQH0x{Ppd)Dub}Pv=bX^%{wgw+4kY8+AwQb0Cd8O`d;;Dw<*`1^(5gD zZJmxs_KOMkXO>}(=~L-(L^zOo)h?r(dix^lFqDH1OD7HTLQ8)x+k`bp3IKSO^GQ1U!qkz$=Xb3}(hlfz%v@{&YgIkSN@r0o? zx&L2yfD~9#--Q&Y(zMe7q(yn!;k-9scpRZ?NWQQZ^S77PpnP+tjX&RDs^}B*O%oxa zS)-Zk^>QGqVP%pZCee!VV6hy`~PVr=|uPJ7hielC==D2|MjA0t!s z%_%P$g8t}vdx(Ej;R`62-`u(j9$;%*4F?AZwVcLP9XHqS#Kib$oI8C!9=~)!iaQX` zzV=!=5{BB~p(nJysrnt5rgRV*&A!v@JXZCGXOm1BARc1}ltBQt?T8R_A|6UqU%!_?5wC-FEp0qAdp^&|0;vTX?1 zp2@JE-<&3^+U0%blGB2Jv{Fk2EIu7kc_|dWH7#w2fU{{xbPk*T| z{>#5U6wkfX?2tU94Jb{+|2B=}#K|YlRy+nid6)7f4B41&fHb}(*-kC>s>uT`%gXpj zS2P=th2>f8tpIxAW$_OfpB{*@N%63+Csvl$W8v;5qZvK&X!obsZcp2kXj z7CB}Wx>gR%G}6oe{_lQgeDFOF<)L0i3$^5gs|>BybKG{Vcb^;i)g#5`AZJ zKCfAk6W}FPt`FtY33vSq*zHv>z^X02YW@lkW}TX5Bk$ zTQRC$#`wTM?zke3ylnBZ((+vu({Dx|J7UhZo(&nr{oaEZ7?x7&;XM=ETdIJZ44(+U z7?q&!R&FZP^c0%-5=aTWgh;gYGQ1wr7rugw)8~=AnZ~%=kV-`( z)Wh84=;)9d&u|P4qxj(9QQqTOn<^inliyi3v^5G?WrIl2rHS97AN(j_l1FbAdYbf2 z8jK-EzVq-~kIXFvHszDtjnW3oya(Fh^nx(3JhdO*J!;9hF|53&erI=GO12)WYiqfK zi^GUq@p>TV^;>tW};LLXJW@w8jn$3=GD|hz#o3P}%`{??%mk$8)+)SqD8u zg6XpUbn2_RfeS}*91U{DpfjpShd`~BH)na2AEVBc76-h+$w&sC+X1KyIR~5tQ)8a` z^0|=i*${B4oi_O(+k8=!9%y8=!f&@z^ORHaJ-_FPhYz`2@$w4qJ>Q8x^|5ye$zd$( z`?VDfXEX%el(L+g9*L6^!?C~9t48VDf|vU@VyD*&WWyGL-gVSJ`PCg-F~>XodmuE#4cToqHH&lf173>XKDIl^v3d%-yc zn}VU|&|=CFKp9v>HqF~x^T~(FiSr86sHUkWX=;%gBT7#mI)EIVq$Y41@0fdi`pZ#{ z9t)Cs2zl)*|+8a4*l5!*;3 zBhouvU+y$RXMrqJv~=P-x@bCCaKGJfeutVD|=7F^_2hs|MW>jK~w_R zc7UrZFJwWR&XHo12WaCgs4_irG|o1RPWHuw?7#q5MXhbbwad3+V{IpKk&klWeLDI4 zU0{8MAC@PMMO0Bv%8L;RC)@99(s#k5gRLBa_|PvqDR-hSZ5&tUmIeBq-Tgc)yw3Zi z6;EY=fVz{QYj8(WE=sWYxjArC2dAIXa9$aEwl5pU*GS0 z=fj^oAp$N=j>p5Z(|HdTqmC8zB)A)kHz4wosR31fclR)sVl#v?wt9ABdTJnMCkA6m zJq%6(6o87=Pf8ElcI!)pevj_ z2qhbK{`2&K5@kA+jNevLD+o$jN&`Klvtk=hyG*C=(n>gKG2pv9hq1n@o|r~u<8q$U z(-Sc#K(Ufi#Q|0z&+S56=4l>`3-N^WFx7IX9v?KbP1iKji=~G| zJ#b1Wqm76g5=tceq|7oY+s2yia*k}2pK{py?jE~)?$x`^kx0@Y8s&6&xQ3hs__5KG zQsC*NheM2cgK_Yi6Fm*F#SS+ab1ndgKzF~%EGK*%Cyj%WNDodN0(jj4A(rPb4^dJE`ROD$ zjSZb;V_8`~;+G5;{PdI>y-PP#jF+@4%SvOWywJuLW8O!TXOJ3N*+rJ;5pq3@#m(*5 z++NLxd%(8$a8&Fh^oT~tk_O6x2h_|PLXfd^B6*YAq&V|pa{M5^^CR6$8^5K+_HPl` z)x}l)w7DxcdgIey7>wKV=Bep}@7C#(d4ct~YYTu%3*EGNbQa#4_ug0nZfcUXkuJgC z{L^O!!3T7`aZKdRvf!V9wEQL=GH91se?=?tCDlFIfz37Sz=53Sa7@jN$qtN2Zqg|k z-LymewdzQ|Zdq=VNpHg&!Yz7#p)Q=H`r<2%5ZJl9c!NULmhrkq= z?SXki8zY`_SXUum9a5z3I2+y_#DT=5oly*(M^%*H>&>(nP7VjdXol}N96rI{%L(-R zXU5}gXXfHP=TAylM1bHhrbh*Mfshku8 z8IkbJ$}wf*hmr=e!8HoreIzh_VoVWg8a47DOvRM|GCmci@OUm5#sDewMku69Wz(;8 z6#AYVX`mFS8`6O4T!-R-qODCUg5>2{8_RpKwjkzi9mee3RLq^4irEu>=k>n|sJg=M zdewsh7C2tv`a2HU=u{uBI?2UkeaFyvml=k-_>7_u&NP*r0X<fb!M?$HsC4G+D3MikJ|G4jMVdTIG~jSF?p^D0|Q`EGgmPCT1BHZy$L zPEqyXkmQ;k(&12WG87LksI*R_2kO;}ma2=1#845Z_M^nvei|eBe?5pk@c}vuDAzjK zdiX7xlrZ_n^TxKVXxeoeTpMBJsK6$ix>-Kd+j7JX_zx#iI03%HljFJp*kQQLEaw=l z)FGY7>V9q>*xovj0~w1Gr>0|OZXyPUEc<_1C?|$t$Jq0v6Oip?n=Mmb7;zkcxBJrB zW2YaT7dEHJE^TlyQifXfvx8~bN$DUCfo;5N>mFe+Jq4be%4&c%!S-Jk@BK;pVA|C< zp@B;viw^Mg{Jy6iJ$zV|e}K`1Mj!jTJ$du*mF8U}`#t-yG_an`L-pQzkvF(mF&;lV z6*C&GK(;tSX!svCoMh40}8s9ye0uau8If`nU8mC1blv2;B`(@Y$od30# zZpP{|r`?ZEhtSwMQOt^gE2r(BvcF+A>RAD!AN zI>=9bFlgDB1@IOg0|)9Iv!et$Ml}NAN|xi!8Qx*gJ=y^WPRe1ctJgGKUd%Y04338b zuNZGnBXxNU|EZ_Xcz6n{=xSLo;6cj?3QcvC`VK1k8or3NP~V72kt7f4176I(k6Y4ynGgc zZI!Ds2Bu?JBRZU9LF#%={d!6ShhrzSnmmHGaq$)W$05%-q%7S7hYtAofA`eZGcVG? zvCg1l$bfofn-fhX*MzQGb>i*#ZNTBPdB~`rS%f#A+M=xNXf$DN7YCg(OJ1e-0J3p> z= zcii8EbODFK=mclL{dG8m+WppsxtYm)X9!LJ{A`B7(XimYu(Tq_+mlmaZM#udrj(^R zl!MsZ-p*@M$X9DBOnrfiX)UJJa868j^$y7?u--uhxDtgc<=5m9H#Op5)UnsUuLd<3 zGvh-stA`tX7v*Sp=2y4l&gxb^YaDu!VD+y{3<}T^0)S9qxV?U*BoDndrEMXA38e%X zPageI{}fu{a<9YjUPPs*LD?m^!c$HHKv9keF=?mw)1|4P3<9}fKEK;Q4;7`_T04l% z6+aAhXHHDU?8)gEl~7TdOe_3W{8rud8`@FYdQzcNIiZu(<|BD?_f_K@ely6q=B6=G zHlwaA1LKP!+uck<2V$j$={ zPEdaF87%`$llUcL)s_wAz*8P@rLN&T^*7W%nvRA0s*3l%zl#wg>8mW905ovui^HV8 zwx^sNRdjuDR;q(^8UrxQ-1YcGjE_xaX9=1;3R`WqrrPkq1J~a?NIFF$(_U={{eK6P zenV4%^~f~w6l9&V{>gw0!!~6d&;g}|CzU5hTg{X44bRXQ;waO0Ksq5Fl8!haHB>b4 zR2Y!>GtW^l%`S&O=;t#5av62fn_utR0q&`rnVXC`*@5v1ubDZI>1^|)Xs4&oR$q^d z9gf4`!N9Y9m+32}mN4SHCul^U7<%$Bx?q%nBjL@XI0JTgF{;2u8>c#qN`oWt6qRjS z@}$1B0Gb8|)oEgyO?n6owRq2o`M!_bf9{i9g>q?bJWfvy$8$GUV^PLssb@2`dw25+ zod-`(#EeEOoLGPPmg3j9V|l~p6F9ucZ*KN2Sll>>XRk9AVWHC-ff)$L4VPgUb8aO> zgg}G!{5J}a(^CjepQ%{Mc+ZJecs{}ih7azb=b*Eh7=oLdZ zy(xN*Y*Mh{Y3j$622QKVPucF(kfxv-l7aY~F32Q`6Ro)(lD%rr`&XICXFUfPc?_n) zxEPWtESv$;XOxw_ufz-Q;gNfa$!mB6eF{xE%22X?!WArBy~5S?J2HBC5u$J?QRa6q z)dnYxq3FQCrZW{z%7u67==8_$loNR{z{y)10L7S=h8~`s%Jc4Rt6^oSBGe*#YXu?lqqaaXpF#z+rpw)RXtcrHdzpi}s+>^|1EACnFL@ z2sj45Ps=%RX(Nt@cx)~>nQ~g+@Ky8V1y4?GX^?c6@DW5YhMh-}%@lPYKZ-DdMwBi*aXXBl<^r%uPrsROpvq zTZr>BWAWsL85y919HFqNz9?sMXJsoEG&*5N*W>4=NSQ<19scCFHo}Z>c(9qPV zPyTOZtNbWZQk3~&BEkR;k4amsK*ABshcqV|H>(cq4^K`SfrnM(1K@Yc*+@nY=Hl#kkdOEi1Si zl8vjU5QWZ9^ zg`Fa66E+e`ka8m}h98HCV}c6mz%&p}n%^FgFlw+vB2RXD_){GwM&@!io2L(HeBCuA z(o;8n<2<0*(_nVonXb`q^9xnQrCGD#{&HN=1Q!t zE$Ug(s7^dPU>asnWlu_`I4h=u-BDori_ul`f^x+l>Q7zfrUv7?K63QZ##e5hiC1r5 zQv9AejZ69YuLAdSB=ZYyyD1;>q1qRad>4H@@E|)|$uIO+pL&wlsu%oBJrXW-tLy+! z`E%2Q@?;sJp_~x$0C72**QdkMAav+)*|q~I^Q6^|rvAvoXE`j&$n_aLpd=4K{N8S@ zrT9?b3~!4)H5H>W%IXKGUcw|=Qjcj1lG)qdaxonVMj9YZUYw|+xuOJMWI#N5aUP69 zs1KtO);O>oj37ovIGl)%`X*tVh;5e-Co&pgnv3uP^5Upg*EXf2p1$HDUfw^ouF(xn z1TIh(&LrR6&gF~)13j@RK`rkpQ$Jp>H?NOBIqCPf^KSLbsKUmE2hM_1s>^5uAQbR``@M{!oQUUP78N}p%dY4Q}SJ9OQ8PLFNs5gmu4vAj7O z^Q&`lcV#YCHz!1cjoEJrC@ae|Z}>z_tSbVm3tS>9X|r6rJ@?e>Ob9fzH?kMgoIig% ze`3=Q^V@M<0F&c0jY3LKGQEi;lRuThSOkP+zDCFNms3y~O&rm-f&B4arR_}~)#ad9 zy5F(`GV;7&ip6>Es!Q7ff(DurAx`o|_vkcR=B!!Z1OURY!;n_fRJE%re?wn`w!-XC zoUS10mj2~*F8K6^17WlS4V2^SR6s^rBjuIaJ^$W!JeI%iJ295KttgWhQ$28s*Nk!l z@LqiFs(0OdGq}058Ml_bK!0boC%*JLd*g}mITDD&L*EPN85Y}UTo6gGu@fdwjgaX* z);v88uo^bsVJ`9PT%du`u-q%h>$(>SFq)vD0@-uaQ{@1pFFG+K;3OXA)k$)IMq4;@ zUYlM{3tZ+K8{x=AU(B4=uwD*)YGfpS_xs+fp0B4u0InmLUtW$+f9-4ejt_*lxz!hI z8+~!-&P%bq!_>oJ%uL;{k;9oZRvX*PvAljOc~E8M#e{k~4M3372^dmAmp5KuW2$L= zYgq=$YZvschX*G*bp#l}q_I*tEOIifk`4Krz`e?Zeq=}-#sZ_cwEVjG>jiUwwV9de z2h?ELg4-9jmR?m)$-S{rjcBMFxEQUVAIK47!zhhUWP5sAJ#3}pH+5s{ELZ-cGswq_ z`mUe1;39V#2s*GIC&gp<;P_@cvHte=e^}2Sjp>QMBuD=#rQJ$CQBQEdbLzH#h_2c3 z6~I5rK*w@tmhe)xbxlS_BOqw1X8ok~XVA4vJBeZwDcr7@@O;MJcO=-sg*KR_J$Y6~#BQ==csG;h1Q zel*^?N<4Ift2#9Bwe*`jrQZc5x8pE8)*F*224ZlyH@4OLzxw*ESXyMpp(lR$cYP>6 z@a~6l>WTJGSe_dZT6S^`%89fuZOqZbI@8`B;E(fp^{a$%&DHJOgrWW-OGoX-QY{l{dlR0E$CAVWn84rKbzRBSwJ& z5~sfKFVnUfhr;*`U#cG9;ydS0xWF=dDSZcVeQ7B!&(Ftz>aefjCVRTa)MGh!@Q(klB_7@xvMZWBJ;e?#k2FwT3>Hozc`Vv^k~-?s$cj(+2C@9{ zkGz9}qxyknGH;JgGZ!CF4V0IAD`f(JpbSDiQjy}r~HW=9gmY5mV@A2 zN?&#s`qQz%SL%{y2yl8Thlj=tec5xD(NTy#-K)p2jtdXY2!logq6e4vL-jC{rp}U$ z=(nLp*O0Af5j`%0Yf=2pc7E^df4j>2KGpqWqGwXHJ|`#hwBlD?m8LG%Jv(${5B+>E zeo!9e^Kc@#U%ffvQ(i2*Hx3~?ILRt<0npUwkpA-hKjKGCS6T+%;gwTl(pC6{J}|}6 z*N2`wruYxU=;-6|T_3$37tiuu_M^ZF`?E94@%Y7?(!rg$eD_SeaQ&gUy>c=pN7iJ( zy3YPyaQqy}r@k;4cUtF2%zNvjzq|cGzjR)G0z{NnJMjW&Ubf%`xa-m=8h{yRbDh4+WjcI%E_Whe5eDISP z|NG=f&Z;7law=TFcx8U0^DdG$Ja%>}9S7;!4+S!4;dQ%PyM6KEEzjM-@G{C&&P2i8#ytct!6J-m+%{r_SxJs4Kr(~{{BBPMya=6e$^w5n2!%R-x17pfG*yoV@JPhH z3>+)vw;c&|0PJC}det^tpyI1cdbs&1fuKCxfVw5#ZuD%$t(E1t!J7xi#%1WNh&Yi4 zKCNE&PTZQmnaVIc%saCu)FW7qwe>r)C-B|QfgMBi)TlG`W00V`o>E}19XSjo zB`@FEt4`03wF=Vu4S`Id_+rfj<%bU#fi!w52YeWFri_pqzxAII!)x(DPrFQLupHr$ z4FH!z;z(H3XT5|vZEniB3jd%CIyAv!oQF3hU@YAOKJ^ZBYFoN-e4B#1ao0$cFKPT59R-LdPx3+X>hc!7 z70%S3{FZs(8F*;RB|~J#?=C-G9x+u8Kw5C?KXrh+G`J0!w`eqJ61V<<#M6XlovQ46 z#Jladvzj}rw&+lzyN5A7I~Fr1)B&0ti}PouVp1LYPW1%v20KKDh|vRve`lAg-Pu0g ztYh@`rHz14J0Uv}9D)xqYGl2mQ8j!K?i_u{9I@0+!p)T&wU9EWww9Py zIE*`MeespcLpiktE{57~1TkWNLus~SY;e{gO6w^h8#oL*4W3>XaX6f__f5yxBvTSS zv9`FIujFLW@hF(~G8p_ukl;qp$K$(>NM#`C%+pQhcmx4oY`DM$FqM}0^?%}T_|ygJ zagQOdaPo6wXDN*%W&Y_u_ZvjdUE#NP3L-w?ZX>59I4ZZu4pRY}^BW219o_N2# zdx(SmV`+53z+phww+(H+px{zgIlgR|YV0gNSDqXN$yw}dF2&f`i5MF_t=w$N*h=RH zpBY)?PPy!XZ{>&unwjdF7(1!n^mHRr*n%Mut+Zu1igZBgU9D}X%yfV?b>%6SkzBl< zh@I`fs)X3G4DTwv9si|r844Gosh+T|BmTDcW4g~FISNgfR z^%F7}9~C`Jp`sTD;;BZs{pR<7`)B_njZl6?`MQP`*xTp&A`E;_T$`Vc|NS?fip#gT z-exy`&jT6jV|x?q}tl2J-X>AY@-{M>$!893`V z_>Kd0_c~zdvcVlvJBPFbZFzL!tITTW))S}j|K;F;b{=q%mb$qf9u;)Kw4~l8&Mxhl zR`q1J7VSU?;?%jZn40x;_l48raq-kdOihet@4o;Cz?lw42Zb%~0UY6~l#w*>#Q}6` zesLwQIe~Aa$*D2{PJ_Ggc-IN%Qkb&pOs9=7H7*;aFh(eKqcA`{M~Cc4fWCiABNE;_ zvLw&8xy>}vp&EFf4Afv=+&CenXDSQt1oFiu9ov1cF0AL)U)E7*OyuFZ6cq5~JK&;$ z*+aWlT?PUg-06Q0t?13UccS!}n6!NBQKK7l<f7lgfKT;>d=kL-gnaFdf1UrH_|v=;%n+pO5}sWS3{!SG^UET)6(}UFQ+d|mvLes z?XU=ULo*H@pQy(`&};KL7JOn+o>3M!2*c?z(r|v{W*JjV87ro?ytd&wKnwLW74D(t zC<@1@K+DwzfGx)(1B`%}-g{j1%2@Q?5U;Q5d0n(zmchEFw2TlxtFSxDYk6nA6Bm4_ z9xSC{C~V9y?#$?T+Mp+=4Ysznv!_UpPk~Jyjg)dx6vGd_JXs_Crc=b!A5#wW7HypP z%X)U|+SKjfA?4xR!4=W{y1u`rJU5j0cI@rmiTlrgRtD*Ayml3Ng+^O+Vvs3YpxK5k zBYe=SPUDz55&IgoY^dX~rQhRmXXT`D9>j1z>y%1=Y-)>jlG_WS0mKRBQyZ$j)Xuy^ ze=>q!d~2YIM~3?9cHPCB-xHU^vVHQ$XX=P`o#-a!Nct}RZkdHMzZ2gsgJc;;j%sJn zWd~?X?LR|DM$LoF^-mADq%G>KYXA_uHMU=}NB&GLt#ih2opld&Ahvh2V`O`2ePW9^ zfb9SW9Xk*+nqW%G3-Yu>QLhKQIBPzA!vpP+Hsnu6F*zcKIM+D&mm?hXvX3_D6ZSoS z^+!Hb_3J=;vHduF{*}Ax<-oMaGbO8=9+AA~!4vVG2Tq8vp7`Qx>+$D)=DO0^XaF?u zY(y~^075VgBcVl+qLU1u8@=G2= zyZ%Zx=m&gcV+TfOiIF^wh=?%7%BYL$}Q_^=P?t z>2)4=0L+JytKmogz-s)^_pQh5)KT4T!}e}}{MDcPcs%{e#r*vP@A)~k|1V1yw(q&v z{$?lJ3uqhgp1Ko%92|f2@=4nU4X4RS3gV6@JJA4r{>Ri9&!ISE| z_jfLBq{=yZ(8z?h7c%m|Asl~0B2Ecs!=;R@a~ucOUC5KC-J4Mm=TXRCi}gb)1LuTI zqRfsXIXgX?TYae_=Nm>e8sR(ZC9mK0)hEEym*=I>{{EKp)150)c&B@XKYwF8{`Rjg z#m+t~Gt*!~2^WFGz~M3Gzx!hT?z}4Jo)s@lVUP5M3l}70H^jAN&jA9vTWZ*rbq8uX zAsbUZ>K{n>2nGdBK_i4xE&*`}&S2`JTzUNudgX90Q~TbGO05TCFMij z<_rvcXgiXFT{%q2erB2zbF885r+%cY^yDa|F#u_tJ5GdMTw5_Uc3O^vYeUdy%aeMb zBj^C%>4Bw=h_*7O{PDZwk#!eMMHgvsBs7II_K7hg{HchXmP`K73*@A^<0b zSZ3}$yA4ox!mExC-FF!0&v1WJ#QWa96W{ak%{YDHsBvee;q_Z{v9iI`T*PyiXX5(Z zG1av<-ucMQc<*CZC4=pbBl-FZ@YMQ{b!&9P?X=dajye(!6K0QEP`pbTvtCZGiZto% zK;Q>}BS{|7?puJpwDue|+!KKwk3}nWg>+KeUpU z?}TT(zLz8E$^$)W_@cjpDq~=-mQjcb%U3?I=E3NKhk9byaxF%qW$4K*$ifTlbb#`Z zZ1P=t&Djv1@!|e-3QS`Si^w`3vMon)cXd-kSU=ml7yv)BV}o&C6*)gU8iOi;^%ADK zR^`oLPBAMLKuk*J!`Oyt{rz$z8U{0+z*PeIjt0qw1=3+THJ+i7zTDxoxw@ARFTYO# zv>ucuF{S{m6#h+_Rz^kScWB6TP$;@go4!kiDRkvA5N8GA4 z^H2uHWLrj)g;0!>GIZlxu(q`&r@^~DmgB`M*W>AzUXQQ5_*z`Mv#2aAnE9+i(b=UH zpguIX?gNlEyDCuEsqdSCu2e0HaAOm)D`9fS< zyf2p4XGEusRzZi~Dg%NfJx`OCPhkLy%r*GR8$ zlx~!~j9;;o-8eo)bn^HS@H-!Z8e9#|j=d3(I%}Bm1zDQ^60gDZw(T19Xv8CX%WH`@ zWc#)^_HxJPzHBdtn&+1`V`+6u`X#+mVjM?atAVN^pNbPbyeE$AeGqv4<;bX1u=U;QtBN4GW&+>*n1^|lxOHbvwMH&*gH@FO34 zRJ|S}eD3;ku9>v>k?_slM?&KQ*in)mO_d&FAxYkLbQ0l;(Cz)W{;JAsMmTRa4OP%y zjBliyVU?#1H5~xZ;4lE>E1WdI8k!PTIFrFGoESvDwnr2gN0f&As~d|=UAxT5X{L;? z-YA0T=67%1f*%>0%BhL$NjD63T8s;h+F?v>rP8QQ?rj}4hCnu`7M?a79g*M&%!e{a zZ63lm;KHK&y%~wgdbyiuX7|b6X)MBwbeZ*Qd2&5f*9kZqv0oD9q5<*x}zj| zxpp*Ne&i@hf5e%`M|qRimY}Z+XYrhArtEOQFF8=tzMa#ZTzOJcf;3ZV&f|}aO1$L&w+Wcq%3+O!T2CGXOaX4*2M^DU7 z#@O(1j)MBW|6TWel5M{2hl7RJ7dO;H-qoYR@puiKo*s!?@`PW%x|pMtB8EkIu1R5f zY-6W4zH%9Z@18mj41rNF)54JB7$1z$iNP3V`>_7F>}-cqpd0)7c0~mx*qFJ;1&(U; zW;Xb}{$NrZ2g6lpruUkI>7;yUNkvh9aAY2RrlRN%{H5?@b-JVVpbk9QFu|>Ve!-A> z4LmKUz^8lEU1J4e@bZAe@#YN*j1l(yHsy*I%1g>jf6_$V={012@(OSB{f$qn>^8LX zP?Lrp*H7Oyv~^F8c*vk!(UW=Lt@tQo51lv@9Be5*0DS-rC9i?Nuqq$%mY*GWmPbN; z@_u@N`?;Wr^fmQE8A(IYmvou_bP7tFuf^3U*Nwleb9NBf=##E2G4Yz*hGyYS{^l>Z zo*z_MAB_|eCs`d&Ipnvq;kRV3cz)YhqAM~AN(Ud9_S#~ruD-i-1pTb{Xr$7Fm2NSD z0e6-oJm5q>c!3!sL!3zJNC%+Ybpc3E54+)TH>#oEZNf4q&^&cpNS)VrwHz6zo_OKa zfq444!I+ylh^dLLVZ0aAIg+~z%mOreEXeW_C%lGV@Y6cR_u>^(g3xXoVmhI@!aDV7 zLsp@wgX)y&&=c_TmvvMriqCeezmhlkjKI83TAU(7t#?%q)9i5L1k%>%A+6In4lKso zFke6U=5T$7-X@NGIXbBPnZ&dZXDmv8(398M^J)u{?#S3Lt@`!5BeKWnYn}gqMs%J} zUc<{YmKW3EmCHrXa6sf|S~Xvq)!fa6-W-Q;`%}Wn=Hc8v{fL1UR2uIRoco_vC86_dZ6#j@KEye`l6A!r2 z`0~i2Efqw{k_Hn2DLxyY!mY%mJXH_YMckX9P6&hioG`}_w4De*o*D(E2par2473EA z+>2~_9Wt6MV&h2M!?p0hk&Q{vt6?T}B!AXf;cxor@6Di{mvWE`e1Konk8O$2=J-az zn=+vXK0+h>@KjE7-8ywvntV~2SkI`ToD6k98qm*3h0$s9M}c`lnbY<9>mL|JIQboZ zKoe6zlx-bS{gnsW=uLb7e>sxj>@DWKXL|Cmrbd+ML{gU42kTAKIE4q`H;?&FUAsmO z`KSe}4(#S~XM~<}@<)Hj6J#4i;mvl<%?-i< za9)T%Xh1(WE5jCLMpv$nA$3`gDAX^@-G;ZqsVA6RSLz7w`AvCzro8n>`fO8*U)wm0 zXI>nR&wOP#zVeO1c;7pA;?jkqw&6I`Ig+~z)oDYP@UA;D0>AYiy>OeST;c)C%W|;c zw%l{HKswhb)AGq5WjV}tfO3!*pX34DCL04Q{MZ&gyF7B6sJPCqg4 zFsfiZ!_!r2Z*m0b6o%|b0M<#s&vFI}?W~QEmIZdU1rHAm<;9RJ&eQ&xE@N7Y*(CBD zS!`znl4nBn>%mjw@y<(gnUJl%&)-}Y@vg{gw|Pq&hgA0Cw?B0uod+K5jy&PrRX6e% zZf?i_^63SoBM;L|9&*xqJ$?Up49WQrzP_~QQHTr-ml$$btN=Xi;U4*EkV%E&e3c49 z@{pDY8fq$z{%ROcMDA?Tx6aUA4@L%|kiG<7!U9Mub<6McC~^7KODvoy;`-L2!YK>G z*l6;kn~SOydqf%@{n!TwP~@(*D8q6{gGu7>@T34oh2;1>!BxJT3F zCy)k1@wJ2NI8+#QBpvlBdGa|0}Z4>%q8PP`p@{!F0W*1IyM)+v5lCrbyc*VMh%dPtVh#50v6KIAV2TArSQ z#fjLdGT*%1=+;3@kNX`clv{EjA38{RWq9(lp{e)+;CLvLyahP5iq8>b^1=?%b+tjW zp1T57PyaL@Y8r~LxfvX`%lM=J_F8=BM>k`1xLYRhFFxNB|Jh&ZiLX5m+HwGP3`u+b z!ze4?u`=^DyZQosq)8bm8FA!F-Zi>YkWL4^FPs9tg9F^Q4R-t~E8>L^dfYKo>imuK zvA(q1S@8~;dF`PqWAiA9ODN}P2W9>0*0bw0DHpoPpR!P#h|Kcg0dga}aHO6!@*;0` zYJ#a#(Jy;V{E5?}F>``f6CB2gX>Q0Ei_syqhvqCuUf?+B`dY{QwneU6;X9)eoDk_* z|H8f}Z&beN)N{a%<{Va5Cy)1?eESn;KFPwlQsyE%1 zt}fn;2>MMqm2DL)oMLQmUL*hXW%WRfHb&(k2bGrsSwJUG?rmOYSjGaZn3{4g1+e1s z9SX7%DcGW?(tycvB?t=+!BTi4%`nHS*QK-;yoEv9f-En~g27f$@{ymkHpGpQQ9K5r zwvZw~fV?Q1>(S)zl&P==KTiR{S$fh$qw!Q9=^ci#Q~jGPm6eZdc*z6(YU5XVHW(m3 zS{}3*Oylu*zE|1#TYoqv@L3M#HI0+^U@%paWoKe!>wBWuvRl9!Mj8;sXJe;Ml+(n! zd}^)6s~$PON7d`jgPQ{QTX`wlRW;50PebeLJ1J-KPSHx^-cgyRna6dpe94b4Za8_*BwC2@xbm!wkY9`X?>Pfcz&M@Z(-IL5XDrNg3DFC(Bp7>qA`rpYi^oSmCrp z+VOl*Cvc-DmR5UWS>ss2)8xwx2};dS`q{Qb@*BlrqeP3l1>s@ z&>2?x;<|R_Szrg%;B9c`U&^zUw!VW8^!#O97gv7Eu)^VXoOS>@B}0Cbrt+8UGaHcg z0ifGux*XDx^?12WI1k}^a1(^O{Z|#JR7{~&)RYiqw_$(( z{pwl$nHV4W84cfmPWa~&C-OuN(9U=GRSU5N)!T0O?kN-2vo4dpS#&_(D;^+E<%OOa zSrim+Fg_iAB4g!QS^qJ~?>m*=7en3)dVBB2%*3A+AHFC$Jf(_m!8^;1kuE*?Z(b&} zLuXCH8IMN+9pfY*9GvKk`E)#X3xV)?vu2+T$2ReWq zq|KrOD67kK$CK}s$NaQj!B^}+@=)mj_*dz{m*s%4fj5f>7je|RbhvOrAN(jd9^Zzs zslJ$)(`a^}M|w0Am(ESf@lVLvkK|%Jz-R;~!SvV2@Su32PQFGbT;|xmw2_nVTw{;( z7#$tXvnq7ViR=0MKl<_HG{jW(hUCE*aR7L>wM`6$VO}E=1tm-lb61cX_lg_}3-##S z&Gp{+^!349{NeZhc6T0hTcZ`^YoWQPfPo~g90`(hBk@qO8n+vO84bQP&|+TOk$~6A z!mtoE!kD9^t$6+AG?@~V5CN3$NW8)*)ARla4~K!$`jZ4jS2Y4?N(GZ(Fswy0b%73{ zRvE4zz+9)OP`WHF>r;+|ol7nc1!Z_^!%+tx!Oa{wG#gj9p{6lVIF1BRiTSC(jLR2f zW!>$V3kUQS-zyKYVNTz@Fm4-O$-IFybuFlEZVq@#~svhT?^j#Pk`X$xp zGpf&Lm1jXYU6vx06pZi#NvG=5hN3;& zgLvRDr4j$B&xNy(#-$4<(jYA?y&5mQ_RF!Njt6|jAyYhjE13XpA9h|DE?$8%!}M9a zYdaF4bmC||kJcS1oTNwZI`V4bC<7Ycp=I0Xb+!W=XYv$3_yiZ{a2z4CqL-{<8?e?iC54+$X#=yOI05qAB@wd z$6{iV=`A^x^D{9!$)*(R6J^lh$DuHisdu2TPSW1Jg>&(aFzJi(;COl;J~J7Q=wT$m zU0d^OBvx;*d3n{iMkIF?Ua!>MlNSLwjq=>gXgqw{`H{^g&@!$0O>sM?zAwM&w^w! zxI5qd+jO+!-SEhdB6y(~hvsyJH|a=h$AEA>ZNOphe=4+aL%y9&H-FOA`BvpPebTDB z zdDB|>sxhn4<>NGl(w8p3tDaRi(xdxi%Dct%Tii2kqTBo^+=ahe0$_R21AIOP2frX3 z_r3_RzyD=5*3U`COG;zfDsNlo+Vx2X-3f2%qrYyxE-%{g`lavGoAzD2rVSje55K!5 z8(B=>qBN%Sy1!?h>fX()h-da>O2=~dq=-iFM*B$!fD zTLl=ocsjUJ}N7yM9(34MP5Y!nAPw5nXZ!l<=={%-C-~QK3`Q@QMJ$d$|$)7!( zrl$f;&D8)yoEvqj=>+KhW>Z__;kpwV z6S(~D6Zif$H*P0QCubiGUV@cj>A>8x+uHt5q`<#jIPN_qG&J;+(y4!>vV9FbK;5WU z+X=R(%i7Zg)&a|qhU1_M5Vo|`c|^yA9+>HOWMiRt3feekkLB0G<2f2Qw^{3g6~ zwGH>GM;ngvjFZ4R9&s@pB|hdDYYwr|E&dKm6^F<~8!uBg}{4)%_m!rF9w)jjcD&hj31W zbW0mMF{_aZTYs-Eb}wz@HH=)X!kWtNes8=!k5>end*CQH49G_!;*;=XXe&+}@*_xN z*~vq&8xi5zu!-k0nMpq9qAV1pAcc~s@SY96<2dVa!Xx2$=lwky=WUcvMslbxMl@pM z&1So*yNb*72%JyqmA#FuSJ6qD79ScXMg6OsGqV#hJ3XH7VWylNt_5itj<1U_O>N~l zV#pLux!EA}M?)*)!*ADH@(?Z*y>P)F8WfI@X`|eNn(GjD#;z0TT@N7h={F4{m0h1W zv5~<^aDSa-8 z=I11vZ|HeOy8E2U=Xq9o_(g@?QW-YpE(_X|s4N%yb8B+Oi=L*Zl}^9m4RxY?^6*i> z&%_5x|7V=aOOKSq@Vd$YpdpP{*4=SEJ>#m!qT1N=s`J;C|7)t#*Q1X*^*y79x_w>s zd0KI+4XqH;=oCdK9_r#x(xmQ5zo0RBe`Lz4tB?NRchi140KeQL1*h}bu_z$p!B1UE zHjFlU)N$lO%K^!ww@-D+`cMbgW1#O#`pxal$go>x;t<{F2xWjfw{q*x2G^rQCwHFC z*G4v`F$0Kcpd~cHQF*{4>Yl$kb)>D2%D#<`55&-jJC$mN`OXl|xAE#+m)Fe$B)-B4 z@3I`HAq+Z8W|Ud!0qcV4O5~&@UGmz};whT~5SQ}EGOSbg!in?3Ni*$Dr!gIo{>Fv} zI1U=@_Ut>62EKmQ!0fDpAP_!X7~c3+qgOalVW?T1Q0VZ+5c%TU~5FGd&Wg zr`T@U^G#2-*9TC&5_%+NAdZV39}GI`SxHB41q4)<5kn`B@Vhb4Yr*&)^YHXM^_j!y zCJ%t(GB@5Pit;G8jk68oGyO3+Cx!3piM7@Bc;$twVlwpE5F-qXoktv9MFXG%LAy>U zoCHRFoATfLBccp1_vhLSxptVkvCh~)6uK0sygK} zsv|EB9@$VXII{gTWfm|JAuW(jw~5EtKxf`zC0e1W_v&(HWw30r^v;xEi=WP`Ht_B(E&ECbpF1Z!h%#sgFm z#=&fu1cSW0!=A6gVU~Amc+jkYHz43t+`G*40ZMbAGIq9CRCmjdR|g{xj5pJ47>&U`Iwn;IK23Kusy2=2h&Y;b zRHH)9RaZJIIHuxH865EQz+W2z90GK9*CTjpZan#_c)lkesF%_foi$ZT7&{Z)$bF>= z(^D^)VjwCg~Rbl%e91KWb)X6t@i03H)DK5U{gO{g&xw1= z-`vJAHmDYX^{fI%mo%ekwsn55!OS3nU-) zm${U_(prv`4UOm-G@>WY(@x_~OX(u{Ih#U0)4*?ZEcqfJ4Ywwsd$bvFr*7+k#?qB? zByWA*Z(gmpvz6#yYN zPnH8V(NbXvS-xqgGAm8ddE`+F_FM04acEA)UBg_Qi=V+5kq2k)aAZ^lK!QkRYk|w4 zh=vZFV}=;Ewt4-fdgt75oHr*oy^=JdG-~y1_2gPa_F!d%5{$16(3=XG4@OclRb6>d zEbw5kFb<5QcrY5pbxLpJbmMp9jhwOs6=_iX2!(oUXhbrbj6{ z-}MxZqNgC~N`8|cV~4yLAz*wP*{duN(!qk?$b!)|PCVl&BYDy-ugc0gC=L9hj(O#b z^4ih3K2XugmuZ{&G`O?y=6ffdB9osaY5^3ZH{|EDrbg09Q4W8J)8$Y;I)&4wa|FAQ zZM!VWfafL6Za(x;23|ZN9fmRW%1AOSb)eyGW{STHZ-z=65LJ8hH+Av@_D5 z*E4a(jrK}8_h`urar;hdH9`Am037Qdk@0DMM#{Tawr>m zGEkqEQSwqe%(g9O;&#wvZi6z`c@Y-PNy8cmJF#;53MZsL;(1FV2-r!P3-kK3lcK{; zS;~K}_mQ;W_@#|+yU6w2R-VVtO?9r3=OtHHHL4+9O>G?(N;;H{-rnWp^j{-T2xqc7fD=g>goBRMuM~|r+PaZdZfN(Bt#E_aMdLT0N4NiTH zGna-nN^$;IU%U~k%j* zNM3<6-&di!1N?S5Zn*HidNID<+`Z*8+5l-h9nhsS!=x;^;N0i{Z)K$Ztf$a(rz+fV zJ0bcxnSJ}tGy3gqzOyqAYKVLy@vg7mj@7lB;^A&iU7eVFI5~q+1UK^G@~c-Yn0mU%hB~;O){XEgM`WC-Pv1LJvZ`3 zg$xtVm_97MVY-lZ;(D7;=wCW(=xxew1G+#OPfxk?0bcUfbQ;^ptUW#ZieYQe{4qa$lpL7@F6hT4doG5`Ddm_<2yh8 zmU!}UUeyqO_lO1q1=t*}OQ~(Y#IXp^hy^FYv=-A_beh---1np3`Ib*|b^M+(a{3YA zOm+^0Vms^y$%CW01u(5IFBph z#?2>{0*IsH{B}>AA-dyd5*h9iFbMdQ~1UV5DhaHFsY;aXsu@ptr(&-o!mk&W3>Av_O*$q9U_=Hv%c3 zj8op4D+A)jiy}MDj*9)|Bbf#oc=!6~=xpNH+**>-v4xd)v#TRA$U^$4(y5^+ZD&-c z?=@|d74v@r%b`n~dw1}I9zRnj$i@cLyz${tk4)Omn>c{qnO^za*xdmkZSjvf!Xuud z3B54y+&}^NSyL_P1d{L8AL_`aZ@oN1_=(r|qbQ%pG7w!53;`Boh;bruKvV@&lK8uF_o;2E6?MwJ|RMYf4q>D?}pLrG`iR$ZVC z74FDQpUDH_gfC^Myu?jR55>gnP>fC5K<~&ub2MRdoo%wz(R`s4ePq5GJpmng!O!Ne z8;8D_pVZv-7nRBnbm$X~M4k^h$Y1sVTV*@t^sH|x1vvE`l%yvubh$&sY(jVa6G+G3 z)SVmvdCVK+a46pQ#D(+Iaq0xGm1d(AZP7FXz1;%N*3;q?H7Qx?odq1%kK0Nv;d$k!Dr(BN;;RFHFk!U+^j zb}5FWT@8AD+1mqo#>Pfs?&MUQJTsfu6ny`pA=(EBAxsG&?`~{SohC+q8p}(pK^^M#4z*|Nqqa!C`P)-xt02aV8Oc-5c4e$vaH8t7>;s}8@Mtn3T z466*U2v}8cNx^pbpcYB6_#}9d&z_}&dAkPj5Psz8IevkLjt|m>i#rzJ4zL+zIX;WBYln7eNyg z9@kImVmj$SRy}RxQx}m{<}~mkJ=Q{ako)H9ezw&uIg;AaYo1uf-JAyWzVK7;GRy@o z)4Xj1DaZ%?0H0}3l($VMxLiNigPe}HGo<{~iF{Ol=^UfKTx%0A3LFowg*Ew(zB+&5 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 literal 0 HcmV?d00001 diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 5d8dbb69640..9b0a7c27927 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -78,10 +78,27 @@ line containing that folder, or :dffd:`use these smaller sprites <6096>`. Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. +Yellow cubes and missing sprites +-------------------------------- +If you are seeing yellow cubes in Stonesense, then there is something on the map that +Stonesense does not have a sprite for. + +.. figure:: ../images/stonesense-yellowcubes.png + :align: center + + An example of the yellow cubes. + +If you would like to help us in fixing this, there are two things you can do: + +* Make an issue on `GitHub `_ with what + item is missing and pictures of what it looks like in DF. +* Create the art yourself. For help with this, please see the `stonesense-art-guide`. + Useful links ------------ - Report issues on `Github `_ - `support` +- `Stonesense Subreddit `_ - :forums:`Official Stonesense thread <106497>` - :forums:`Screenshots thread <48172>` - :wiki:`Main wiki page ` From aa6fdb41fc4ee5330ba907a9960dde05cb34d2b1 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 22:48:31 +0800 Subject: [PATCH 034/919] Add Military::removeFromSquad --- library/modules/Military.cpp | 64 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 23fc15a8717..929285a761c 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -7,6 +7,10 @@ #include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/histfig_entity_link_former_positionst.h" +#include "df/histfig_entity_link_former_squadst.h" +#include "df/histfig_entity_link_positionst.h" +#include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" #include "df/entity_position.h" @@ -17,6 +21,7 @@ #include "df/squad_schedule_order.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/unit.h" #include "df/world.h" using namespace DFHack; @@ -297,46 +302,51 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ for (size_t i = 0; i < hf->entity_links.size(); i++) { auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) continue; - if (link->squad_id != squad->id) continue; - + if (link == nullptr || link->squad_id != squad->id) + continue; + hf->entity_links.erase(hf->entity_links.begin() + i); - delete link; - start_year = link->start_year; + + delete link; break; } - if (start_year == -1) return false; + + if (start_year == -1) + return false; - auto former_squad = new df::histfig_entity_link_former_squadst(); + auto former_squad = df::allocate(); former_squad->squad_id = squad->id; former_squad->entity_id = squad->entity_id; former_squad->start_year = start_year; former_squad->end_year = *df::global::cur_year; former_squad->link_strength = 100; - hf->entity_links.push_back(former_pos); + hf->entity_links.push_back(former_squad); return true; } -static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squad) +static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; - if (! Units::getNoblePositions(&nps, hf)) return false; + if (! Units::getNoblePositions(&nps, hf)) + return false; int32_t assignment_id = -1; for (auto& np : nps) { - if (np.entity.id != squad->entity_id) continue; - if (np.assignment.squad_id != squad.id) continue; - + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + continue; + np.assignment->histfig = -1; np.assignment->histfig2 = -1; assignment_id = np.assignment->id; break; } - if (assignment_id == -1) return false; + + if (assignment_id == -1) + return false; int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -346,14 +356,16 @@ static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squ if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - delete link; - start_year = link->start_year; + + delete link; break; } - if (start_year == -1) return false; + + if (start_year == -1) + return false; - auto former_pos = new df::histfig_entity_link_former_positionst(); + auto former_pos = df::allocate(); former_pos->assignment_id = assignment_id; former_pos->entity_id = squad->entity_id; former_pos->start_year = start_year; @@ -366,13 +378,16 @@ static bool remove_captain_entity_link(df::historical_figure* hf, df::squad* squ bool Military::removeFromSquad(int32_t unit_id) { - df::unit* unit = df::unit::find(unit_id); - if (unit == nullptr) return false; - if (unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; + df::unit *unit = df::unit::find(unit_id); + if (unit == nullptr) + return false; + if (unit->military.squad_id == -1 || unit->military.squad_position == -1) + return false; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); - if (squad == nullptr) return false; + if (squad == nullptr) + return false; // remove from squad information int32_t squad_pos = unit->military.squad_position; @@ -384,9 +399,10 @@ bool Military::removeFromSquad(int32_t unit_id) unit->military.squad_position = -1; df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) return false; + if (hf == nullptr) + return false; return squad_pos == 0 // is unit a commander? - ? remove_captain_entity_link(hf, squad) + ? remove_officer_entity_link(hf, squad) : remove_soldier_entity_link(hf, squad); } From 872cc7cba5a2e599c6e2f081b4ee2a8333d5d9ff Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:01:49 +0800 Subject: [PATCH 035/919] Fix style --- library/modules/Military.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 929285a761c..d5692501675 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -312,7 +312,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ break; } - if (start_year == -1) + if (start_year == -1) return false; auto former_squad = df::allocate(); @@ -335,7 +335,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ int32_t assignment_id = -1; for (auto& np : nps) { - if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) continue; np.assignment->histfig = -1; @@ -352,8 +352,10 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ for (size_t i = 0; i < hf->entity_links.size(); i++) { auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) continue; - if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) continue; + if (link == nullptr) + continue; + if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) + continue; hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; From 84370629900dcfca95a984cae8284d11e3d9678b Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:09:19 +0800 Subject: [PATCH 036/919] Attempt to fix trailing whitespace --- library/modules/Military.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index d5692501675..d1322735db6 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -329,7 +329,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; - if (! Units::getNoblePositions(&nps, hf)) + if (!Units::getNoblePositions(&nps, hf)) return false; int32_t assignment_id = -1; From 7eb27ef350963ebdac9a136bcc291ef989df99aa Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Fri, 7 Feb 2025 23:12:44 +0800 Subject: [PATCH 037/919] Remove trailing whitespace --- library/modules/Military.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index d1322735db6..8292c6227b0 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -304,14 +304,14 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ auto link = strict_virtual_cast(hf->entity_links[i]); if (link == nullptr || link->squad_id != squad->id) continue; - + hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; delete link; break; } - + if (start_year == -1) return false; @@ -337,14 +337,14 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ { if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) continue; - + np.assignment->histfig = -1; np.assignment->histfig2 = -1; assignment_id = np.assignment->id; break; } - + if (assignment_id == -1) return false; @@ -359,11 +359,11 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.erase(hf->entity_links.begin() + i); start_year = link->start_year; - + delete link; break; } - + if (start_year == -1) return false; From 9a06a27222538bf4cd7536453e0d9e851858a8c2 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 02:43:23 +0800 Subject: [PATCH 038/919] Add Lua bindings --- docs/dev/Lua API.rst | 8 ++++++++ library/LuaApi.cpp | 1 + 2 files changed, 9 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 000840c76b3..4cc2c35d066 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1986,6 +1986,14 @@ Military module Returns the name of a squad as a string. +* ``dfhack.military.removeFromSquad(unit_id)`` + + Removes a unit from its squad and returns true if successful. Unsets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links + to indicate former squad membership or command. + Items module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4b82dddc17f..6e251dbfcfb 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2341,6 +2341,7 @@ static const LuaWrapper::FunctionReg dfhack_military_module[] = { WRAPM(Military, makeSquad), WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), + WRAPM(Military, removeFromSquad), { NULL, NULL } }; From c9e2dcc2812247cb4f9d1f3a4e3410f70d2172b3 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 02:44:26 +0800 Subject: [PATCH 039/919] Modify Military.cpp --- library/modules/Military.cpp | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 8292c6227b0..62d189c4d75 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -301,14 +301,18 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) { - auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr || link->squad_id != squad->id) + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::SQUAD) + continue; + + auto squad_link = strict_virtual_cast(link); + if (squad_link == nullptr || squad_link->squad_id != squad->id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - start_year = link->start_year; + start_year = squad_link->start_year; - delete link; + delete squad_link; break; } @@ -351,16 +355,20 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) { - auto link = strict_virtual_cast(hf->entity_links[i]); - if (link == nullptr) + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::POSITION) + continue; + + auto pos_link = strict_virtual_cast(link); + if (pos_link == nullptr) continue; - if (link->assignment_id != assignment_id && link->entity_id != squad->entity_id) + if (pos_link->assignment_id != assignment_id && pos_link->entity_id != squad->entity_id) continue; hf->entity_links.erase(hf->entity_links.begin() + i); - start_year = link->start_year; + start_year = pos_link->start_year; - delete link; + delete pos_link; break; } @@ -393,9 +401,12 @@ bool Military::removeFromSquad(int32_t unit_id) // remove from squad information int32_t squad_pos = unit->military.squad_position; - df::squad_position* pos = squad->positions.at(squad_pos); - pos->occupant = -1; + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos == nullptr) + return false; + // remove from squad information + pos->occupant = -1; // remove from unit information unit->military.squad_id = -1; unit->military.squad_position = -1; From 76d2af495407cd6cdad1ac9a18854ef3e207e171 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 12:56:43 -0600 Subject: [PATCH 040/919] Update Authors.rst --- docs/about/Authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 2908cae9d67..bfb8aa02a84 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -242,6 +242,7 @@ thurin thurin Tim Siegel softmoth Tim Walberg twalberg Timothy Collett danaris +Timothy Torres timothymtorres Timur Kelman TymurGubayev Tom Jobbins TheBloke Tom Prince From cfe1dbb79b6b53e3d1f4e03bf92a6b2807b6a9de Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 03:14:43 +0800 Subject: [PATCH 041/919] Update changelog --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d28decc88..e8340baeaca 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,9 +35,11 @@ Template for new versions: ## Documentation ## API +- ``Military::removeFromSquad``: removes unit from any squad assignments ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings +- ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` ## Removed From 71f92c6085625497940d1788f60687228d3b9e12 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Fri, 7 Feb 2025 16:32:29 -0800 Subject: [PATCH 042/919] Update Gui.cpp - Redundant code --- library/modules/Gui.cpp | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 6dfe7f920e3..1d60c335b6b 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2962,22 +2962,8 @@ bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) bool Gui::getCursorCoords(df::coord &pos) { - using df::global::cursor; - df::coord p; - if (World::isAdventureMode()) - { - if (game) - { - auto &look = game->main_interface.adventure.look; - if (look.open) - p = look.cursor; - } - } - else if (cursor) - p = df::coord(cursor->x, cursor->y, cursor->z); - - pos = p; - return p.isValid(); + pos = getCursorPos(); + return pos.isValid(); } //FIXME: confine writing of coords to map bounds? From 194bde01576571e023ea11255c3aa5a0f80a066a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:15:59 +0000 Subject: [PATCH 043/919] Auto-update submodules scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 5b70cd886f6..5bd392890e9 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5b70cd886f619b503aa770008fb79aa39a3be6fa +Subproject commit 5bd392890e92a70622878c0513ef50e72e215435 diff --git a/plugins/stonesense b/plugins/stonesense index 9d851db0800..1756e2f3f70 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9d851db080012add38631af37ea5c5b60a893e79 +Subproject commit 1756e2f3f704a0d01b24feccd0089972c245c164 diff --git a/scripts b/scripts index ebb22f3adb6..fc67fdc5b67 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ebb22f3adb6070c931a82b5f6a1749e7bf3ced03 +Subproject commit fc67fdc5b67a25a776eebfa769b289801679e364 From 5a6de1fc928ecc067267064fab2fd443a36643c8 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sat, 8 Feb 2025 19:15:03 +0800 Subject: [PATCH 044/919] Remove return val --- docs/dev/Lua API.rst | 2 +- library/include/modules/Military.h | 2 +- library/modules/Military.cpp | 35 ++++++++++++++---------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 4cc2c35d066..66516642a96 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1988,7 +1988,7 @@ Military module * ``dfhack.military.removeFromSquad(unit_id)`` - Removes a unit from its squad and returns true if successful. Unsets the unit's + Removes a unit from its squad. Unsets the unit's military information (i.e., ``unit.military.squad_id`` and ``unit.military.squad_pos``), the squad's position information (i.e., ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index dc69dac52bf..37855d94dba 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,7 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); -DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); +DFHACK_EXPORT void removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 62d189c4d75..9e507a99aac 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -296,7 +296,7 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } -static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) { int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -317,7 +317,7 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ } if (start_year == -1) - return false; + return; auto former_squad = df::allocate(); former_squad->squad_id = squad->id; @@ -327,14 +327,13 @@ static bool remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ former_squad->link_strength = 100; hf->entity_links.push_back(former_squad); - return true; } -static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) +static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) { std::vector nps; if (!Units::getNoblePositions(&nps, hf)) - return false; + return; int32_t assignment_id = -1; for (auto& np : nps) @@ -350,7 +349,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ } if (assignment_id == -1) - return false; + return; int32_t start_year = -1; for (size_t i = 0; i < hf->entity_links.size(); i++) @@ -373,7 +372,7 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ } if (start_year == -1) - return false; + return; auto former_pos = df::allocate(); former_pos->assignment_id = assignment_id; @@ -383,27 +382,24 @@ static bool remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->link_strength = 100; hf->entity_links.push_back(former_pos); - return true; } -bool Military::removeFromSquad(int32_t unit_id) +void Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); - if (unit == nullptr) - return false; - if (unit->military.squad_id == -1 || unit->military.squad_position == -1) - return false; + if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) + return; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); if (squad == nullptr) - return false; + return; // remove from squad information int32_t squad_pos = unit->military.squad_position; df::squad_position* pos = vector_get(squad->positions, squad_pos); if (pos == nullptr) - return false; + return; // remove from squad information pos->occupant = -1; @@ -413,9 +409,10 @@ bool Military::removeFromSquad(int32_t unit_id) df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); if (hf == nullptr) - return false; + return;; - return squad_pos == 0 // is unit a commander? - ? remove_officer_entity_link(hf, squad) - : remove_soldier_entity_link(hf, squad); + if (squad_pos == 0) // is unit a commander? + remove_officer_entity_link(hf, squad); + else + remove_soldier_entity_link(hf, squad); } From 6d44e8f8f0e1eb0cd7b9a42c29dbfa8309d081f5 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 9 Feb 2025 12:51:12 +0800 Subject: [PATCH 045/919] Change return type of Military::removeFromSquad --- library/include/modules/Military.h | 2 +- library/modules/Military.cpp | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 37855d94dba..dc69dac52bf 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,7 +17,7 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); -DFHACK_EXPORT void removeFromSquad(int32_t unit_id); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 9e507a99aac..3c5708d75e1 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -384,22 +384,25 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.push_back(former_pos); } -void Military::removeFromSquad(int32_t unit_id) +bool Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) - return; + return false; int32_t squad_id = unit->military.squad_id; df::squad* squad = df::squad::find(squad_id); if (squad == nullptr) - return; + return false; - // remove from squad information int32_t squad_pos = unit->military.squad_position; df::squad_position* pos = vector_get(squad->positions, squad_pos); if (pos == nullptr) - return; + return false; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) + return false; // remove from squad information pos->occupant = -1; @@ -407,12 +410,10 @@ void Military::removeFromSquad(int32_t unit_id) unit->military.squad_id = -1; unit->military.squad_position = -1; - df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) - return;; - if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); else remove_soldier_entity_link(hf, squad); + + return true; } From c21740929a56691be26401c56cf5ea0ba6df04a7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 9 Feb 2025 07:15:07 +0000 Subject: [PATCH 046/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 1756e2f3f70..92794aec776 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 1756e2f3f704a0d01b24feccd0089972c245c164 +Subproject commit 92794aec7768bdafd0f5fc23fed8b30655805b32 From 3844957820893c92bd1cf97cac617f7ef5e451bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 00:55:35 -0800 Subject: [PATCH 047/919] refactor announcement settings logic --- plugins/spectate.cpp | 141 +++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index ae33031b14b..0871e4754ce 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -33,42 +33,8 @@ namespace DFHack { } static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle -static bool was_in_settings = false; // whether we were in the vanilla settings screen last update -static const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); -static std::unique_ptr saved_announcement_settings; - -static void save_announcement_settings(color_ostream &out) { - if (!saved_announcement_settings) - saved_announcement_settings = std::make_unique(new uint32_t[announcement_flag_arr_size]); - DEBUG(control,out).print("saving announcement settings\n"); - for (size_t i = 0; i < announcement_flag_arr_size; ++i) - (*saved_announcement_settings)[i] = d_init->announcements.flags[i].whole; -} - -static void restore_announcement_settings(color_ostream &out) { - if (!saved_announcement_settings) - return; - DEBUG(control,out).print("restoring saved announcement settings\n"); - for (size_t i = 0; i < announcement_flag_arr_size; ++i) - d_init->announcements.flags[i].whole = (*saved_announcement_settings)[i]; -} - -static void scrub_announcements(color_ostream &out) { - if (Gui::matchFocusString("dwarfmode/Settings")) { - DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); - return; - } - - DEBUG(control,out).print("removing PAUSE from announcement settings\n"); - for (auto& flag : d_init->announcements.flags) { - flag.bits.DO_MEGA = false; - flag.bits.PAUSE = false; - flag.bits.RECENTER = false; - } -} - -struct Configuration { +static struct Configuration { bool auto_disengage; bool auto_unpause; bool cinematic_action; @@ -94,6 +60,75 @@ struct Configuration { } } config; +static class AnnouncementSettings { + bool was_in_settings = false; // whether we were in the vanilla settings screen last update + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + std::unique_ptr saved; + + void save_settings(color_ostream &out) { + if (!saved) + saved = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved)[i] = d_init->announcements.flags[i].whole; + } + +public: + void reset(color_ostream &out, bool skip_restore = false) { + was_in_settings = false; + + if (saved) { + if (!skip_restore) + restore_settings(out); + delete[] *saved; + saved.reset(); + } + } + + void on_update(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_and_scrub_settings(out); + } + } + } + + void restore_settings(color_ostream &out) { + if (!saved || was_in_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved)[i]; + } + + // remove pausing, popups, and recentering from all announcements + // saves first so the original settings can be restored + void save_and_scrub_settings(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; + } + + save_settings(out); + + DEBUG(control,out).print("scrubbing announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; + } + } +} announcement_settings; + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -108,14 +143,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit = -1; - cleanup(out); + announcement_settings.reset(out); } } else { DEBUG(control,out).print("%s from the API, but already %s; no action\n", @@ -148,7 +175,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); - cleanup(out); + announcement_settings.reset(out); return CR_OK; } @@ -162,7 +189,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DEBUG(control,out).print("world unloaded; disabling %s\n", plugin_name); is_enabled = false; - cleanup(out); + announcement_settings.reset(out, true); } break; default: @@ -172,25 +199,12 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (Gui::matchFocusString("dwarfmode/Settings")) { - if (!was_in_settings) { - DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); - restore_announcement_settings(out); - was_in_settings = true; - } - } else if (was_in_settings) { - was_in_settings = false; - if (config.auto_unpause) { - DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); - save_announcement_settings(out); - scrub_announcements(out); - } - } + announcement_settings.on_update(out); if (config.auto_disengage && plotinfo->follow_unit < 0) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; - cleanup(out); + announcement_settings.reset(out); return CR_OK; } @@ -354,10 +368,9 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { config.auto_disengage = val; } else if (name == "auto-unpause") { if (val && !config.auto_unpause) { - save_announcement_settings(out); - scrub_announcements(out); + announcement_settings.save_and_scrub_settings(out); } else if (!val && config.auto_unpause) { - restore_announcement_settings(out); + announcement_settings.restore_settings(out); } config.auto_unpause = val; } else if (name == "cinematic-action") { From 2daa28790b5b3dc3e7a052ec61d4768d5350c380 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 02:28:45 -0800 Subject: [PATCH 048/919] implement follow history and zoom to/highlight unit when switching targets when paused --- plugins/spectate.cpp | 92 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 0871e4754ce..a3e3c86193a 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -2,6 +2,7 @@ #include "LuaTools.h" #include "PluginLua.h" #include "PluginManager.h" +#include "VTableInterpose.h" #include "modules/Gui.h" #include "modules/Units.h" @@ -11,6 +12,7 @@ #include "df/d_init.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/viewscreen_dwarfmodest.h" #include "df/world.h" #include @@ -34,6 +36,8 @@ namespace DFHack { static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static const size_t MAX_HISTORY = 200; + static struct Configuration { bool auto_disengage; bool auto_unpause; @@ -129,6 +133,77 @@ static class AnnouncementSettings { } } announcement_settings; +static void follow_a_dwarf(color_ostream &out); + +static class UnitHistory { + std::deque history; + size_t offset = 0; + +public: + void reset() { + history.clear(); + offset = 0; + } + + void add(color_ostream &out, df::unit *unit) { + int32_t id = unit->id; + DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = id; + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + history.push_back(id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + } + + void scan_back(color_ostream &out) { + if (history.empty() || offset >= history.size()-1) + return; + int unit_id = history[history.size() - (1 + ++offset)]; + DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = unit_id; + } + + void scan_forward(color_ostream &out) { + if (history.empty() || offset == 0) { + DEBUG(cycle,out).print("already at most recent unit; following new unit\n"); + follow_a_dwarf(out); + return; + } + + int unit_id = history[history.size() - (1 + --offset)]; + DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_unit = unit_id; + } +} unit_history; + +struct forward_back_interceptor : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { + bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default", Gui::getDFViewscreen()); + if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) + unit_history.scan_back(Core::getInstance().getConsole()); + else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) + unit_history.scan_forward(Core::getInstance().getConsole()); + else { + INTERPOSE_NEXT(feed)(input); + } + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -143,6 +218,11 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit = -1; - announcement_settings.reset(out); + on_disable(out); + // don't reset the unit history since we may want to re-enable } } else { DEBUG(control,out).print("%s from the API, but already %s; no action\n", @@ -175,7 +257,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); - announcement_settings.reset(out); + on_disable(out); return CR_OK; } @@ -190,6 +272,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan plugin_name); is_enabled = false; announcement_settings.reset(out, true); + unit_history.reset(); } break; default: @@ -204,7 +287,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (config.auto_disengage && plotinfo->follow_unit < 0) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; - announcement_settings.reset(out); + on_disable(out); return CR_OK; } @@ -353,8 +436,7 @@ static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - DEBUG(cycle,out).print("now following unit %d: %s\n", unit->id, Units::getReadableName(unit).c_str()); - plotinfo->follow_unit = unit->id; + unit_history.add(out, unit); } ///////////////////////////////////////////////////// From 82e014dd6791d2e8046ea9a15c84f258876a407b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 02:55:03 -0800 Subject: [PATCH 049/919] auto-disengage when the squads menu is opened --- docs/plugins/spectate.rst | 6 +++--- plugins/lua/spectate.lua | 15 +++++++++++++++ plugins/spectate.cpp | 23 +++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index f49849c8506..d0b5c8f04ee 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -13,9 +13,9 @@ to following a different dwarf. It can also switch to following animals, hostiles, or visiting units. You can switch to the next target (or a previous target) immediately with the left/right arrow keys. -`spectate` will disengage and turn itself off when you move the map, just like -the vanilla follow mechanic. It will also disengage immediately if you open the -squads menu for military action. +By default, `spectate` will disengage and turn itself off when you move the +map, just like the vanilla follow mechanic. It will also disengage immediately +if you open the squads menu for military action. It can also annotate your dwarves on the map with their name, job, and other information, either as floating tooltips or in a panel that comes up when you diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index ba7e9ba7ba1..e21016329f2 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.spectate') local argparse = require('argparse') +local dlg = require('gui.dialogs') local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') @@ -55,6 +56,20 @@ function refresh_cpp_config() end end +function show_squads_warning() + local message = { + 'Cannot start spectate mode while auto-disengage is enabled and', + 'the squads panel is open. The auto-disengage feature automatically', + 'stops spectate mode when you open the squads panel.', + '', + 'Please either close the squads panel or disable auto-disengage by', + 'running the following command:', + '', + 'spectate set auto-disengage false', + } + dlg.showMessage("Spectate", table.concat(message, '\n')) +end + ----------------------------- -- commandline interface diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index a3e3c86193a..67a841ed6af 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -79,7 +79,7 @@ static class AnnouncementSettings { } public: - void reset(color_ostream &out, bool skip_restore = false) { + void reset(color_ostream &out, bool skip_restore) { was_in_settings = false; if (saved) { @@ -218,9 +218,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit < 0) { + if (config.auto_disengage && (plotinfo->follow_unit < 0 || is_squads_open())) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; + plotinfo->follow_unit = -1; on_disable(out); return CR_OK; } From 8b28be61602c7c86baff4960024069253b9f2744 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:29:50 -0800 Subject: [PATCH 050/919] first part of prefer recent units feature --- plugins/spectate.cpp | 80 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 67a841ed6af..c7f07b94ce2 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -38,6 +38,17 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; +static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; +static const float JOB_WEIGHT = 3.0f; +static const float OTHER_WEIGHT = 1.0f; + +static const float RECENT_UNIT_MULTIPLIER = 2.0f; +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +///////////////////////////////////////////////////// +// Configuration + static struct Configuration { bool auto_disengage; bool auto_unpause; @@ -64,6 +75,9 @@ static struct Configuration { } } config; +///////////////////////////////////////////////////// +// AnnouncementSettings + static class AnnouncementSettings { bool was_in_settings = false; // whether we were in the vanilla settings screen last update @@ -133,6 +147,9 @@ static class AnnouncementSettings { } } announcement_settings; +///////////////////////////////////////////////////// +// UnitHistory + static void follow_a_dwarf(color_ostream &out); static class UnitHistory { @@ -204,6 +221,37 @@ struct forward_back_interceptor : df::viewscreen_dwarfmodest { }; IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); +///////////////////////////////////////////////////// +// RecentUnits + +static class RecentUnits { + std::unordered_map units; // unit id -> time seen +public: + void add(int32_t unit_id) { + units[unit_id] = Core::getInstance().getUnpausedMs(); + } + + bool contains(int32_t unit_id) { + return units.contains(unit_id); + } + + void trim() { + uint32_t unpaused_ms = Core::getInstance().getUnpausedMs(); + if (unpaused_ms < RECENT_UNIT_MS) + return; + uint32_t cutoff = unpaused_ms - RECENT_UNIT_MS; + for (auto it = units.begin(); it != units.end();) { + if (it->second < cutoff) + it = units.erase(it); + else + ++it; + } + } +} recent_units; + +///////////////////////////////////////////////////// +// plugin API + static command_result do_command(color_ostream &out, vector ¶meters); static void follow_a_dwarf(color_ostream &out); @@ -321,7 +369,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) ///////////////////////////////////////////////////// // cycle logic -// static bool is_in_combat(df::unit *unit) { return false; @@ -370,17 +417,18 @@ static void get_dwarf_buckets(color_ostream &out, static std::default_random_engine rng; -static uint32_t get_next_cycle_unpaused_ms(bool has_active_combat) { +static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { int32_t delay_ms = config.follow_ms; if (has_active_combat) { std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); int32_t delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } + DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); return Core::getInstance().getUnpausedMs() + delay_ms; } -static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { +static void add_bucket_to_vectors(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { if (bucket.empty()) return; intervals.push_back(units.size() + bucket.size()); @@ -388,6 +436,24 @@ static void add_bucket(const vector &bucket, vector &units units.insert(units.end(), bucket.begin(), bucket.end()); } +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + if (config.prefer_new_arrivals) { + vector new_bucket, old_bucket; + for (auto unit : bucket) { + if (recent_units.contains(unit->id)) + new_bucket.push_back(unit); + else + old_bucket.push_back(unit); + } + add_bucket_to_vectors(new_bucket, units, intervals, weights, weight * RECENT_UNIT_MULTIPLIER); + add_bucket_to_vectors(old_bucket, units, intervals, weights, weight); + } else { + add_bucket_to_vectors(bucket, units, intervals, weights, weight); + } +} + #define DUMP_BUCKET(name) \ DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ @@ -402,11 +468,6 @@ static void add_bucket(const vector &bucket, vector &units DEBUG(cycle,out).print(" %d\n", (int)f); \ } -static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; -static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; -static const float JOB_WEIGHT = 3.0f; -static const float OTHER_WEIGHT = 1.0f; - static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("choosing a unit to follow\n"); @@ -416,7 +477,7 @@ static void follow_a_dwarf(color_ostream &out) { vector other_units; get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(!active_combat_units.empty()); + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !active_combat_units.empty()); // coalesce the buckets and add weights vector units; @@ -452,7 +513,6 @@ static void follow_a_dwarf(color_ostream &out) { ///////////////////////////////////////////////////// // Lua API -// static void spectate_setSetting(color_ostream &out, string name, int val) { DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); From ae305971aad0eba85af8f5760e1a39d8ab7c0e85 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:31:04 -0800 Subject: [PATCH 051/919] only intercept arrow keys when the map has focus --- plugins/spectate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index c7f07b94ce2..94c5adb23f0 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -209,7 +209,7 @@ struct forward_back_interceptor : df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { - bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default", Gui::getDFViewscreen()); + bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default"); if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) unit_history.scan_back(Core::getInstance().getConsole()); else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) From ea4b2a2e7a3fd498b986019ba21d219688328fe3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:39:55 -0800 Subject: [PATCH 052/919] collapse two overlays to one since we don't have a good method to control which one gets rendered first --- docs/plugins/spectate.rst | 43 ++++++++++++++++----------------------- plugins/lua/spectate.lua | 29 +++++++------------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index d0b5c8f04ee..3d20b1e6653 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -37,7 +37,7 @@ Usage spectate [status] spectate toggle spectate set - spectate overlay enable|disable + spectate overlay enable|disable Examples -------- @@ -59,8 +59,9 @@ Examples ``spectate set follow-seconds 30`` Configure `spectate` to switch targets every 30 seconds when in follow mode. -``spectate overlay follow enable`` - Show informative tooltips that follow each unit on the map. +``spectate overlay enable`` + Show informative tooltips that follow each unit on the map. Note that this + can be enabled independently of `spectate` itself. Settings -------- @@ -68,7 +69,8 @@ Settings ``auto-disengage`` (default: enabled) Toggle automatically disabling the plugin when the player moves the map or opens the squad panel. If this is disabled, you will need to manually - disable the plugin to turn off follow mode. + disable the plugin to turn off follow mode. You can still interact normally + with the DF UI. ``auto-unpause`` (default: disabled) Toggle auto-dismissal of announcements that pause the game, like sieges, @@ -102,47 +104,36 @@ Settings arrived on the map. ``tooltip-follow-job`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. ``tooltip-follow-name`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the tooltip. ``tooltip-follow-stress`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the tooltip. ``tooltip-hover-job`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. ``tooltip-hover-name`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the hover panel. ``tooltip-hover-stress`` (default: enabled) - If the ``spectate.follow`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the hover panel. Overlays -------- -``spectate`` provides two overlays via the `overlay` framework to add -information and functionality to the main map. These overlays can be controlled -via the ``spectate overlay`` command or the ``Overlays`` tab in -`gui/control-panel`. +``spectate.tooltip`` -The information displayed by these overlays can be configured via the -``spectate set`` command or the `gui/spectate` interface. +``spectate`` can show informative tooltips that follow each unit on the map +and/or a popup panel with information when your mouse cursor hovers over a unit. -``spectate.follow`` - Show informative tooltips that follow each unit on the map. You can enable - this overlay by running ``spectate overlay follow enable`` or, - equivalently, ``overlay enable spectate.follow``. - -``spectate.hover`` - Show a popup panel with selected information when your mouse cursor hovers - over a unit. You can enable this overlay by running - ``spectate overlay hover enable`` or, equivalently, - ``overlay enable spectate.hover``. +This overlay is managed via the `overlay` framework. It can be controlled via +the ``spectate overlay`` command or the ``Overlays`` tab in `gui/control-panel`. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index e21016329f2..d44957dcb19 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -109,15 +109,9 @@ local function set_setting(key, value) end end -local function set_overlay(name, value) - if not name:startswith('spectate.') then - name = 'spectate.' .. name - end - if name ~= 'spectate.follow' and name ~= 'spectate.hover' then - qerror('unknown overlay: ' .. name) - end +local function set_overlay(value) value = argparse.boolean(value, name) - dfhack.run_command('overlay', value and 'enable' or 'disable', name) + dfhack.run_command('overlay', value and 'enable' or 'disable', 'spectate.tooltip') end function parse_commandline(args) @@ -129,7 +123,7 @@ function parse_commandline(args) elseif command == 'set' then set_setting(args[1], args[2]) elseif command == 'overlay' then - set_overlay(args[1], args[2]) + set_overlay(args[1]) else return false end @@ -140,25 +134,16 @@ end ----------------------------- -- overlays -FollowOverlay = defclass(FollowOverlay, overlay.OverlayWidget) -FollowOverlay.ATTRS{ - desc='Adds info tooltips that follow units on the map.', - default_pos={x=1,y=1}, - fullscreen=true, - viewscreens='dwarfmode/Default', -} - -HoverOverlay = defclass(HoverOverlay, overlay.OverlayWidget) -HoverOverlay.ATTRS{ - desc='Shows info popup when hovering the mouse over units on the map.', +TooltipOverlay = defclass(TooltipOverlay, overlay.OverlayWidget) +TooltipOverlay.ATTRS{ + desc='Adds info tooltips that follow units or appear when you hover the mouse.', default_pos={x=1,y=1}, fullscreen=true, viewscreens='dwarfmode/Default', } OVERLAY_WIDGETS = { - follow=FollowOverlay, - hover=HoverOverlay, + tooltip=TooltipOverlay, } return _ENV From 77bbe8c87815e218d5f9bdc2f175572c9c27bc72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 03:59:30 -0800 Subject: [PATCH 053/919] implement recent units preference --- plugins/spectate.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 94c5adb23f0..abe0fffaf18 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -4,6 +4,7 @@ #include "PluginManager.h" #include "VTableInterpose.h" +#include "modules/EventManager.h" #include "modules/Gui.h" #include "modules/Units.h" #include "modules/World.h" @@ -32,6 +33,7 @@ REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(spectate, control, DebugCategory::LINFO); DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); + DBG_DECLARE(spectate, event, DebugCategory::LINFO); } static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle @@ -43,6 +45,7 @@ static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; static const float JOB_WEIGHT = 3.0f; static const float OTHER_WEIGHT = 1.0f; +static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; static const float RECENT_UNIT_MULTIPLIER = 2.0f; static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes @@ -226,7 +229,12 @@ IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); static class RecentUnits { std::unordered_map units; // unit id -> time seen + public: + void reset() { + units.clear(); + } + void add(int32_t unit_id) { units[unit_id] = Core::getInstance().getUnpausedMs(); } @@ -249,6 +257,14 @@ static class RecentUnits { } } recent_units; +static void on_new_active_unit(color_ostream& out, void* data) { + int32_t unit_id = reinterpret_cast(data); + DEBUG(event,out).print("unit %d has arrived on map\n", unit_id); + recent_units.add(unit_id); +} + +static EventManager::EventHandler new_unit_handler(plugin_self, on_new_active_unit, RECENT_UNITS_SCAN_CYCLE); + ///////////////////////////////////////////////////// // plugin API @@ -267,6 +283,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) + if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { + recent_units.trim(); follow_a_dwarf(out); + } return CR_OK; } From a8ebea3c96af3a4909b6a34e817cb01cedb6cf93 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 04:43:10 -0800 Subject: [PATCH 054/919] implement conflict detection and cinematic camera --- plugins/spectate.cpp | 67 ++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index abe0fffaf18..b27db514603 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -13,6 +13,7 @@ #include "df/d_init.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/activity_entry.h" #include "df/viewscreen_dwarfmodest.h" #include "df/world.h" @@ -40,14 +41,22 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; -static const float ACTIVE_COMBAT_PREFERRED_WEIGHT = 25.0f; -static const float PASSIVE_COMBAT_PREFERRED_WEIGHT = 8.0f; -static const float JOB_WEIGHT = 3.0f; +static const float CITIZEN_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float OTHER_COMBAT_PREFERRED_WEIGHT = 10.0f; +static const float JOB_WEIGHT = 5.0f; static const float OTHER_WEIGHT = 1.0f; static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; -static const float RECENT_UNIT_MULTIPLIER = 2.0f; -static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes +static const float RECENT_UNIT_MULTIPLIER = 2.0f; // weight multiplier for recent units +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +// jobs that get "other" weight instad of "job" weight +static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, +}; + ///////////////////////////////////////////////////// // Configuration @@ -392,25 +401,22 @@ static command_result do_command(color_ostream &out, vector ¶meters) // cycle logic static bool is_in_combat(df::unit *unit) { - return false; -} - -static bool is_fleeing(df::unit *unit) { + if (Units::isCrazed(unit) || unit->mood == df::mood_type::Berserk) + return true; + for (auto activity_id : unit->activities) { + auto activity = df::activity_entry::find(activity_id); + if (activity && activity->type == df::activity_entry_type::Conflict) + return true; + } return false; } static void get_dwarf_buckets(color_ostream &out, - vector &active_combat_units, - vector &passive_combat_units, + vector &citizen_combat_units, + vector &other_combat_units, vector &job_units, vector &other_units) { - static const std::unordered_set boring_jobs = { - df::job_type::Eat, - df::job_type::Drink, - df::job_type::Sleep, - }; - for (auto unit : world->units.active) { if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) continue; @@ -424,10 +430,11 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - if (is_fleeing(unit)) - passive_combat_units.push_back(unit); + INFO(cycle).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) + citizen_combat_units.push_back(unit); else - active_combat_units.push_back(unit); + other_combat_units.push_back(unit); } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { job_units.push_back(unit); } else { @@ -440,9 +447,9 @@ static std::default_random_engine rng; static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { int32_t delay_ms = config.follow_ms; - if (has_active_combat) { + if (config.cinematic_action && has_active_combat) { std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); - int32_t delay_ms = distribution(rng); + delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); @@ -492,21 +499,21 @@ static void add_bucket(const vector &bucket, vector &units static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("choosing a unit to follow\n"); - vector active_combat_units; - vector passive_combat_units; + vector citizen_combat_units; + vector other_combat_units; vector job_units; vector other_units; - get_dwarf_buckets(out, active_combat_units, passive_combat_units, job_units, other_units); + get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !active_combat_units.empty()); + next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); // coalesce the buckets and add weights vector units; vector intervals; vector weights; intervals.push_back(0); - add_bucket(active_combat_units, units, intervals, weights, config.prefer_conflict ? ACTIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); - add_bucket(passive_combat_units, units, intervals, weights, config.prefer_conflict ? PASSIVE_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(citizen_combat_units, units, intervals, weights, config.prefer_conflict ? CITIZEN_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(other_combat_units, units, intervals, weights, config.prefer_conflict ? OTHER_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); @@ -520,8 +527,8 @@ static void follow_a_dwarf(color_ostream &out) { df::unit *unit = units[unit_idx]; if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { - DUMP_BUCKET(active_combat_units); - DUMP_BUCKET(passive_combat_units); + DUMP_BUCKET(citizen_combat_units); + DUMP_BUCKET(other_combat_units); DUMP_BUCKET(job_units); DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); From ab263e4755bd39aafa8ed5795860f689dd7f2e0f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 04:47:47 -0800 Subject: [PATCH 055/919] clarify function of include-animals setting --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 3d20b1e6653..b4d913d9e0b 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -84,7 +84,7 @@ Settings include time that the game is paused. ``include-animals`` (default: disabled) - Toggle whether to sometimes follow fort animals. + Toggle whether to sometimes follow fort animals and wildlife. ``include-hostiles`` (default: disabled) Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, From fa8550427d3516bffe2f646c204938022db64326 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 12:30:24 -0800 Subject: [PATCH 056/919] protect chained units from autobutcher --- docs/changelog.txt | 1 + docs/plugins/autobutcher.rst | 1 + plugins/autobutcher.cpp | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 36315052f76..3e5fa6ad905 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort +- `autobutcher`: treat animals on restraints as unavailable for slaughter ## Documentation - `stonesense-art-guide`: new guide for making sprite art for Stonesense diff --git a/docs/plugins/autobutcher.rst b/docs/plugins/autobutcher.rst index b429f575425..f7bea4d6d9f 100644 --- a/docs/plugins/autobutcher.rst +++ b/docs/plugins/autobutcher.rst @@ -15,6 +15,7 @@ Units are protected from being automatically butchered if they are: * Untamed * Named or nicknamed * Caged, if and only if the cage is in a zone (to protect zoos) +* On a restraint * Trained for war or hunting * Females who are pregnant or brooding a clutch of fertile eggs diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 9e93018cc93..8fde1cc3479 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -666,9 +666,10 @@ static bool isProtectedUnit(df::unit *unit) { || Units::isHunter(unit) // ignore hunting dogs etc || Units::isMarkedForWarTraining(unit) // ignore units marked for any kind of training || Units::isMarkedForHuntTraining(unit) - // ignore creatures in built cages which are defined as rooms to leave zoos alone + || unit->flags1.bits.chained // ignore chained animals + // ignore creatures in built cages which are members of zones to leave zoos alone // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) || (unit->pregnancy_timer != 0) // do not butcher pregnant animals (which includes brooding female egglayers) || Units::isAvailableForAdoption(unit) || unit->name.has_name From eed96caeeea7f8ce1729c41ab7c3b4f89d1199f4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 07:34:55 -0800 Subject: [PATCH 057/919] update to great reorg structures --- library/LuaApi.cpp | 5 +- library/RemoteTools.cpp | 2 +- .../df/custom/creature_handler.methods.inc | 2 +- library/include/modules/Buildings.h | 5 +- library/include/modules/Items.h | 2 +- library/include/modules/Job.h | 2 +- library/include/modules/MapCache.h | 2 +- library/modules/Buildings.cpp | 119 +++++++++--------- library/modules/Gui.cpp | 5 +- library/modules/Items.cpp | 21 ++-- library/modules/Job.cpp | 10 +- library/modules/Maps.cpp | 16 +-- library/modules/Materials.cpp | 18 +-- library/modules/Military.cpp | 35 +++--- library/modules/Units.cpp | 13 +- plugins/3dveins.cpp | 2 +- plugins/blueprint.cpp | 2 +- plugins/buildingplan/buildingplan.cpp | 4 +- plugins/buildingplan/buildingplan_cycle.cpp | 2 +- plugins/cursecheck.cpp | 1 - plugins/devel/eventExample.cpp | 1 + plugins/devel/stripcaged.cpp | 1 - plugins/dwarfmonitor.cpp | 3 +- plugins/embark-assistant/embark-assistant.cpp | 1 - plugins/embark-assistant/finder_ui.cpp | 2 - plugins/embark-assistant/matcher.cpp | 1 - plugins/embark-assistant/overlay.cpp | 1 - plugins/embark-assistant/survey.cpp | 2 - plugins/forceequip.cpp | 2 +- plugins/generated-creature-renamer.cpp | 1 - plugins/infinite-sky.cpp | 4 +- plugins/logistics.cpp | 1 + plugins/nestboxes.cpp | 1 + plugins/orders.cpp | 10 +- plugins/probe.cpp | 2 +- plugins/prospector.cpp | 14 ++- .../remotefortressreader/building_reader.cpp | 4 +- plugins/remotefortressreader/item_reader.cpp | 19 ++- .../remotefortressreader.cpp | 70 +++++------ plugins/stockpiles/OrganicMatLookup.cpp | 6 +- plugins/stockpiles/StockpileSerializer.cpp | 22 ++-- plugins/strangemood.cpp | 20 +-- plugins/tailor.cpp | 4 +- plugins/timestream.cpp | 5 +- 44 files changed, 236 insertions(+), 229 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6e251dbfcfb..637df32211b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -99,6 +99,7 @@ distribution. #include "df/report_zoom_type.h" #include "df/specific_ref.h" #include "df/specific_ref_type.h" +#include "df/squad_use_flags.h" #include "df/squad.h" #include "df/unit.h" #include "df/unit_misc_trait.h" @@ -2443,7 +2444,7 @@ static int items_moveToBuilding(lua_State *state) static int items_moveToInventory(lua_State *state) { auto item = Lua::CheckDFObject(state, 1); auto unit = Lua::CheckDFObject(state, 2); - auto use_mode = (df::unit_inventory_item::T_mode)luaL_optint(state, 3, 0); + auto use_mode = (df::inv_item_role_type)luaL_optint(state, 3, 0); int body_part = luaL_optint(state, 4, -1); lua_pushboolean(state, Items::moveToInventory(item, unit, use_mode, body_part)); return 1; @@ -2816,7 +2817,7 @@ int buildings_setSize(lua_State *state) lua_pushinteger(state, size.x); lua_pushinteger(state, size.y); lua_pushinteger(state, area); - lua_pushinteger(state, Buildings::countExtentTiles(&bld->room, area)); + lua_pushinteger(state, Buildings::countExtentTiles(bld, area)); return 5; } else diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index fbace1ae2ec..3df55b847a9 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -458,7 +458,7 @@ static command_result ListEnums(color_ostream &stream, BITFIELD(cie_add_tag_mask1); BITFIELD(cie_add_tag_mask2); - describe_bitfield(out->mutable_death_info_flags()); + describe_bitfield(out->mutable_death_info_flags()); ENUM(profession); diff --git a/library/include/df/custom/creature_handler.methods.inc b/library/include/df/custom/creature_handler.methods.inc index 811128d058d..14639bd8891 100644 --- a/library/include/df/custom/creature_handler.methods.inc +++ b/library/include/df/custom/creature_handler.methods.inc @@ -1 +1 @@ -friend struct world_raws; +friend struct world; diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index ee7fcdf6982..80fe4433965 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -44,7 +44,6 @@ namespace df { struct building; struct building_cagest; struct building_civzonest; - struct building_extents; struct building_stockpilest; struct item; struct job_item; @@ -112,7 +111,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext = NULL, + df::building *bld, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, @@ -121,7 +120,7 @@ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, /** * Returns the number of tiles included by the extent, or defval. */ -DFHACK_EXPORT int countExtentTiles(df::building_extents *ext, int defval = -1); +DFHACK_EXPORT int countExtentTiles(df::building *bld, int defval = -1); /** * Checks if the building contains the specified tile. If the building has diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 79aed92c36e..6d068e8786d 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -157,7 +157,7 @@ DFHACK_EXPORT bool moveToContainer(df::item *item, df::item *container); DFHACK_EXPORT bool moveToBuilding(df::item *item, df::building_actual *building, df::building_item_role_type use_mode = df::building_item_role_type::TEMP, bool force_in_building = false); DFHACK_EXPORT bool moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Hauled, int body_part = -1); + df::inv_item_role_type mode = df::inv_item_role_type::Hauled, int body_part = -1); /// Remove item from jobs and inventories, hide and forbid. /// Unless no_uncat, item is marked for garbage collection. diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index f0d3e89b970..a0754c4781f 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -105,7 +105,7 @@ namespace DFHack DFHACK_EXPORT bool listNewlyCreated(std::vector *pvec, int *id_var); DFHACK_EXPORT bool attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx = -1, int insert_idx = -1); DFHACK_EXPORT bool isSuitableItem(const df::job_item *item, df::item_type itype, int isubtype); diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h index 04fddd38bd7..0d636273894 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -84,7 +84,7 @@ class BlockInfo }; static GroundType getGroundType(int material); - typedef df::block_square_event_mineralst::T_flags DFVeinFlags; + typedef df::mineral_event_flag DFVeinFlags; t_veintype veintype; t_blockmaterials veinmats; diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index a560a6fc994..672464ceba0 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -48,18 +48,21 @@ distribution. #include "df/building_coffinst.h" #include "df/building_def.h" #include "df/building_design.h" +#include "df/building_extents_type.h" #include "df/building_floodgatest.h" #include "df/building_furnacest.h" #include "df/building_grate_floorst.h" #include "df/building_grate_wallst.h" #include "df/building_rollersst.h" #include "df/building_screw_pumpst.h" +#include "df/building_squad_infost.h" #include "df/building_stockpilest.h" #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_weaponst.h" #include "df/building_wellst.h" #include "df/building_workshopst.h" +#include "df/buildingitemst.h" #include "df/buildings_other_id.h" #include "df/d_init.h" #include "df/dfhack_room_quality_level.h" @@ -77,6 +80,7 @@ distribution. #include "df/plotinfost.h" #include "df/punishment.h" #include "df/squad.h" +#include "df/squad_barracks_infost.h" #include "df/unit.h" #include "df/unit_relationship_type.h" #include "df/world.h" @@ -112,15 +116,15 @@ struct CoordHash { static unordered_map locationToBuilding; -static df::building_extents_type *getExtentTile(df::building_extents &extent, df::coord2d tile) +static df::building_extents_type *getExtentTile(const df::building::T_room &room, df::coord2d tile) { - if (!extent.extents) + if (!room.extents) return NULL; - int dx = tile.x - extent.x; - int dy = tile.y - extent.y; - if (dx < 0 || dy < 0 || dx >= extent.width || dy >= extent.height) + int dx = tile.x - room.x; + int dy = tile.y - room.y; + if (dx < 0 || dy < 0 || dx >= room.width || dy >= room.height) return NULL; - return &extent.extents[dx + dy*extent.width]; + return &room.extents[dx + dy*room.width]; } /* @@ -162,14 +166,14 @@ void buildings_onUpdate(color_ostream &out) for (size_t i = 0; i < job->items.size(); i++) { df::job_item_ref *iref = job->items[i]; - if (iref->role != df::job_item_ref::Reagent) + if (iref->role != df::job_role_type::Reagent) continue; df::job_item *item = vector_get(job->job_items.elements, iref->job_item_idx); if (!item) continue; // Convert Reagent to Hauled, while decrementing quantity item->quantity = std::max(0, item->quantity-1); - iref->role = df::job_item_ref::Hauled; + iref->role = df::job_role_type::Hauled; iref->job_item_idx = -1; } } @@ -219,7 +223,7 @@ static void add_building_to_all_zones(df::building* bld) static void add_zone_to_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -259,7 +263,7 @@ static void remove_building_from_zone(df::building* bld, df::building_civzonest* static void remove_zone_from_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -478,19 +482,20 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in // Type specific init switch (type) { - case building_type::Well: + using namespace df::enums::building_type; + case Well: { if (VIRTUAL_CAST_VAR(obj, df::building_wellst, bld)) obj->bucket_z = bld->z; break; } - case building_type::Workshop: + case Workshop: { if (VIRTUAL_CAST_VAR(obj, df::building_workshopst, bld)) obj->profile.max_general_orders = 5; break; } - case building_type::Furnace: + case Furnace: { if (VIRTUAL_CAST_VAR(obj, df::building_furnacest, bld)) { @@ -507,7 +512,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in break; } */ - case building_type::Trap: + case Trap: { if (VIRTUAL_CAST_VAR(obj, df::building_trapst, bld)) { @@ -516,46 +521,46 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in } break; } - case building_type::Floodgate: + case Floodgate: { if (VIRTUAL_CAST_VAR(obj, df::building_floodgatest, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateWall: + case GrateWall: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_wallst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateFloor: + case GrateFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsVertical: + case BarsVertical: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_verticalst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsFloor: + case BarsFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::Bridge: + case Bridge: { if (VIRTUAL_CAST_VAR(obj, df::building_bridgest, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.raised = false; break; } - case building_type::Weapon: + case Weapon: { if (VIRTUAL_CAST_VAR(obj, df::building_weaponst, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.retracted = false; break; } default: @@ -711,26 +716,29 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, } } -static void init_extents(df::building_extents *ext, const df::coord &pos, +static void init_extents(df::building::T_room &room, const df::coord &pos, const df::coord2d &size) { - ext->extents = new df::building_extents_type[size.x*size.y]; - ext->x = pos.x; - ext->y = pos.y; - ext->width = size.x; - ext->height = size.y; + room.extents = new df::building_extents_type[size.x*size.y]; + room.x = pos.x; + room.y = pos.y; + room.width = size.x; + room.height = size.y; - memset(ext->extents, 1, size.x*size.y); + memset(room.extents, 1, size.x*size.y); } bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext, + df::building *bld, bool create_ext, bool allow_occupied, bool allow_wall, bool allow_flow) { + CHECK_NULL_POINTER(bld); + bool found_any = false; + auto & room = bld->room; for (int dx = 0; dx < size.x; dx++) { @@ -740,9 +748,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (ext && ext->extents) + if (room.extents) { - etile = getExtentTile(*ext, tile); + etile = getExtentTile(room, tile); if (!etile || !*etile) continue; } @@ -776,13 +784,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!ext || !create_ext) + if (!create_ext) return false; - if (!ext->extents) + if (!room.extents) { - init_extents(ext, pos, size); - etile = getExtentTile(*ext, tile); + init_extents(room, pos, size); + etile = getExtentTile(room, tile); } if (!etile) @@ -813,10 +821,10 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, if (force_extents && !bld->room.extents) { // populate the room structure if it's not set already - init_extents(&bld->room, psize.first, psize.second); + init_extents(bld->room, psize.first, psize.second); } - return Buildings::checkFreeTiles(psize.first, psize.second, &bld->room, + return Buildings::checkFreeTiles(psize.first, psize.second, bld, can_change && bld->isExtentShaped(), !bld->isSettingOccupancy(), bld->getType() == @@ -824,14 +832,18 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, !bld->isActual()); } -int Buildings::countExtentTiles(df::building_extents *ext, int defval) +int Buildings::countExtentTiles(df::building *bld, int defval) { - if (!ext || !ext->extents) + CHECK_NULL_POINTER(bld); + + auto & room = bld->room; + + if (!room.extents) return defval; int cnt = 0; - for (int i = 0; i < ext->width * ext->height; i++) - if (ext->extents[i]) + for (int i = 0; i < room.width * room.height; i++) + if (room.extents[i]) cnt++; return cnt; } @@ -881,7 +893,7 @@ static int computeMaterialAmount(df::building *bld) int cnt = size.x * size.y; if (bld->room.extents && bld->isExtentShaped()) - cnt = Buildings::countExtentTiles(&bld->room, cnt); + cnt = Buildings::countExtentTiles(bld, cnt); return cnt/4 + 1; } @@ -911,10 +923,9 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) auto type = bld->getType(); - using namespace df::enums::building_type; - switch (type) { + using namespace df::enums::building_type; case WaterWheel: { auto obj = (df::building_water_wheelst*)bld; @@ -953,7 +964,7 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) bool ok = checkBuildingTiles(bld, true); - if (type != Construction) + if (type != df::building_type::Construction) bld->setMaterialAmount(computeMaterialAmount(bld)); return ok; @@ -1158,7 +1169,7 @@ bool Buildings::constructWithItems(df::building *bld, std::vector ite for (size_t i = 0; i < items.size(); i++) { - Job::attachJobItem(job, items[i], df::job_item_ref::Hauled); + Job::attachJobItem(job, items[i], df::job_role_type::Hauled); if (items[i]->getType() == item_type::BOULDER) rough = true; @@ -1224,15 +1235,9 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectorsquad_room_info) + for (auto room_info : zone->squad_room_info) { - int32_t squad_id = room_info->squad_id; - - df::squad* squad = df::squad::find(squad_id); - - //if this is null, something has gone just *terribly* wrong - if (squad) - { + if (auto squad = df::squad::find(room_info->squad_id)) { for (int i=(int)squad->rooms.size() - 1; i >= 0; i--) { if (squad->rooms[i]->building_id == zone->id) @@ -1406,7 +1411,7 @@ bool Buildings::deconstruct(df::building *bld) for (int i = ui_look_list->size()-1; i >= 0; --i) { auto item = (*ui_look_list)[i]; - if (item->type == df::lookinfost::Building && + if (item->type == df::look_info_type::Building && item->data.building.bld_id == id) { vector_erase_at(*ui_look_list, i); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 951482d23f1..853d42f3b83 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -84,7 +84,6 @@ distribution. #include "df/route_stockpile_link.h" #include "df/soundst.h" #include "df/stop_depart_condition.h" -#include "df/ui_unit_view_mode.h" #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/viewscreen_choose_start_sitest.h" @@ -1877,7 +1876,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce new_report->text = message; new_report->color = color; new_report->bright = bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->pos = pos; new_report->id = world->status.next_report_id++; new_report->year = *df::global::cur_year; @@ -2197,7 +2196,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) new_report->text = message; new_report->color = info.color; new_report->bright = info.bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->zoom_type = info.zoom_type; new_report->pos = info.pos; new_report->zoom_type2 = info.zoom_type2; diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 47d68b16548..a99c2bb7619 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -44,6 +44,7 @@ distribution. #include "df/building.h" #include "df/building_actual.h" #include "df/building_tradedepotst.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/caravan_state.h" #include "df/creature_raw.h" @@ -765,13 +766,13 @@ static bool removeItemOnGround(df::item *item) } static void resetUnitInvFlags(df::unit *unit, df::unit_inventory_item *inv_item) { - if (inv_item->mode == df::unit_inventory_item::Worn || - inv_item->mode == df::unit_inventory_item::WrappedAround) + if (inv_item->mode == df::inv_item_role_type::Worn || + inv_item->mode == df::inv_item_role_type::WrappedAround) { unit->flags2.bits.calculated_inventory = false; unit->flags2.bits.calculated_insulation = false; } - else if (inv_item->mode == df::unit_inventory_item::StuckIn) + else if (inv_item->mode == df::inv_item_role_type::StuckIn) unit->flags3.bits.stuck_weapon_computed = false; } @@ -815,7 +816,7 @@ static bool detachItem(df::item *item) } if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) - return linked_list_remove(&world->proj_list, ref->projectile_id) + return linked_list_remove(&world->projectiles.all, ref->projectile_id) && removeRef(item->general_refs, general_ref_type::PROJECTILE, ref->getID()); if (item->flags.bits.on_ground) { @@ -960,7 +961,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building ref->building_id = building->id; item->general_refs.push_back(ref); - auto con = new df::building_actual::T_contained_items; + auto con = new df::buildingitemst; con->item = item; con->use_mode = use_mode; building->contained_items.push_back(con); @@ -969,7 +970,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building } bool DFHack::Items::moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode, int body_part) + df::inv_item_role_type mode, int body_part) { CHECK_NULL_POINTER(item); CHECK_NULL_POINTER(unit); @@ -1063,7 +1064,7 @@ df::proj_itemst *Items::makeProjectile(df::item *item) ref->projectile_id = proj->id; item->general_refs.push_back(ref); - linked_list_append(&world->proj_list, proj->link); + linked_list_append(&world->projectiles.all, proj->link); return proj; } @@ -1799,8 +1800,8 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t bool Items::checkMandates(df::item *item) { CHECK_NULL_POINTER(item); - for (auto mandate : world->mandates) { - if ((mandate->mode == df::mandate::Export) && + for (auto mandate : world->mandates.all) { + if ((mandate->mode == df::mandate_type::Export) && (item->getType() == mandate->item_type) && (mandate->item_subtype == -1 || item->getSubtype() == mandate->item_subtype) && (mandate->mat_type == -1 || item->getMaterial() == mandate->mat_type) && @@ -1881,7 +1882,7 @@ bool Items::markForTrade(df::item *item, df::building_tradedepotst *depot) { job->pos = df::coord(depot->centerx, depot->centery, depot->z); // job <-> item link - if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { + if (!Job::attachJobItem(job, item, df::job_role_type::Hauled)) { delete job; delete href; return false; diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 86809ba5ba7..96344ddd6f0 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -46,6 +46,8 @@ distribution. #include "df/job.h" #include "df/job_item.h" #include "df/job_list_link.h" +#include "df/job_postingst.h" +#include "df/job_restrictionst.h" #include "df/plotinfost.h" #include "df/specific_ref.h" #include "df/unit.h" @@ -323,11 +325,11 @@ void DFHack::Job::setJobCooldown(df::building *workshop, df::unit *worker, int c if (cooldown <= 0) return; - int idx = linear_index(workshop->job_claim_suppress, &df::building::T_job_claim_suppress::unit, worker); + int idx = linear_index(workshop->job_claim_suppress, &df::job_restrictionst::unit, worker); if (idx < 0) { - auto obj = new df::building::T_job_claim_suppress; + auto obj = new df::job_restrictionst; obj->unit = worker; obj->timer = cooldown; workshop->job_claim_suppress.push_back(obj); @@ -575,7 +577,7 @@ bool DFHack::Job::listNewlyCreated(std::vector *pvec, int *id_var) } bool DFHack::Job::attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx, int insert_idx) { CHECK_NULL_POINTER(job); @@ -585,7 +587,7 @@ bool DFHack::Job::attachJobItem(df::job *job, df::item *item, * Functionality 100% reverse-engineered from DF code. */ - if (role != df::job_item_ref::TargetContainer) + if (role != df::job_role_type::TargetContainer) { if (item->flags.bits.in_job) return false; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index c207360ac48..54fb43c810c 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -47,6 +47,8 @@ distribution. #include "df/builtin_mats.h" #include "df/burrow.h" #include "df/feature_init.h" +#include "df/feature_map_shellst.h" +#include "df/feature_mapst.h" #include "df/flow_info.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -515,7 +517,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index) df::coord2d sub = rgn_pos & 15; - vector &features = fptr->feature_init[sub.x][sub.y]; + vector &features = fptr->feature_init[sub.x][sub.y]; return vector_get(features, index); } @@ -947,13 +949,13 @@ static int16_t basic_wet_dry_effect(int16_t region_y, int16_t rain) auto dimy = world->world_data->world_height; auto pole = world->world_data->flip_latitude; - if (dimy > 65 && pole != df::world_data::T_flip_latitude::None) + if (dimy > 65 && pole != df::pole_type::None) { // Medium and Large worlds with at least one pole auto latitude = region_y; - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_y < dimy / 2) latitude = region_y * 2; @@ -993,7 +995,7 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in bool tropical; // Determine tropicality - if (pole == df::world_data::T_flip_latitude::None) + if (pole == df::pole_type::None) { potential_tropical = region->temperature > 74; tropical = region->temperature > 84; @@ -1002,9 +1004,9 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in { auto latitude = region_ref_y; // DF just uses region_y, but embark assistant needs region_ref_y - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_ref_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_ref_y < dimy / 2) latitude = region_ref_y * 2; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 341ddeabc46..18dd73b09a2 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -99,7 +99,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; } - df::world_raws &raws = world->raws; + auto &raws = world->raws; if (size_t(type) >= sizeof(raws.mat_table.builtin)/sizeof(void*)) return false; @@ -216,7 +216,7 @@ bool MaterialInfo::findBuiltin(const std::string &token) return true; } - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 0; i < NUM_BUILTIN; i++) { auto obj = raws.mat_table.builtin[i]; @@ -236,10 +236,10 @@ bool MaterialInfo::findInorganic(const std::string &token) return true; } - df::world_raws &raws = world->raws; - for (size_t i = 0; i < raws.inorganics.size(); i++) + auto &raws = world->raws; + for (size_t i = 0; i < raws.inorganics.all.size(); i++) { - df::inorganic_raw *p = raws.inorganics[i]; + df::inorganic_raw *p = raws.inorganics.all[i]; if (p->id == token) return decode(0, i); } @@ -250,7 +250,7 @@ bool MaterialInfo::findPlant(const std::string &token, const std::string &subtok { if (token.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (size_t i = 0; i < raws.plants.all.size(); i++) { df::plant_raw *p = raws.plants.all[i]; @@ -274,7 +274,7 @@ bool MaterialInfo::findCreature(const std::string &token, const std::string &sub { if (token.empty() || subtoken.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (size_t i = 0; i < raws.creatures.all.size(); i++) { df::creature_raw *p = raws.creatures.all[i]; @@ -645,12 +645,12 @@ bool t_matglossInorganic::isGem() bool Materials::CopyInorganicMaterials (std::vector & inorganic) { - size_t size = world->raws.inorganics.size(); + size_t size = world->raws.inorganics.all.size(); inorganic.clear(); inorganic.reserve (size); for (size_t i = 0; i < size;i++) { - df::inorganic_raw *orig = world->raws.inorganics[i]; + df::inorganic_raw *orig = world->raws.inorganics.all[i]; t_matglossInorganic mat; mat.id = orig->id; mat.name = orig->material.stone_name; diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 3c5708d75e1..cbb42955624 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -7,6 +7,7 @@ #include "modules/Units.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/building_squad_infost.h" #include "df/histfig_entity_link_former_positionst.h" #include "df/histfig_entity_link_former_squadst.h" #include "df/histfig_entity_link_positionst.h" @@ -16,11 +17,16 @@ #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/plotinfost.h" +#include "df/military_routinest.h" #include "df/squad.h" -#include "df/squad_position.h" -#include "df/squad_schedule_order.h" +#include "df/squad_barracks_infost.h" +#include "df/squad_month_positionst.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/squad_position.h" +#include "df/squad_routine_schedulest.h" +#include "df/squad_schedule_entry.h" +#include "df/squad_schedule_order.h" #include "df/unit.h" #include "df/world.h" @@ -96,7 +102,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) result->id = *df::global::squad_next_id; result->uniform_priority = result->id + 1; //no idea why, but seems to hold result->supplies.carry_food = 2; - result->supplies.carry_water = df::squad::T_supplies::Water; + result->supplies.carry_water = df::squad_water_level_type::Water; result->entity_id = df::global::plotinfo->group_id; result->leader_position = corresponding_position->id; result->leader_assignment = found_assignment->id; @@ -116,7 +122,8 @@ df::squad* Military::makeSquad(int32_t assignment_id) for (const auto& routine : routines) { - df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest[12]; + auto &asched = schedule->month; for(int kk=0; kk < 12; kk++) { @@ -124,14 +131,14 @@ df::squad* Military::makeSquad(int32_t assignment_id) for(int jj=0; jj < squad_size; jj++) { - int32_t* order_assignments = new int32_t(); - *order_assignments = -1; + df::squad_month_positionst* order_assignments = new df::squad_month_positionst(); + order_assignments->assigned_order_idx = -1; asched[kk].order_assignments.push_back(order_assignments); } } - auto insert_training_order = [asched, squad_size](int month) + auto insert_training_order = [&](int month) { df::squad_schedule_order* order = new df::squad_schedule_order(); order->min_count = squad_size; @@ -212,7 +219,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) } } - result->schedule.push_back(reinterpret_cast(asched)); + result->schedule.routine.push_back(schedule); } //Modify necessary world state @@ -234,8 +241,8 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (squad == nullptr || zone == nullptr) return; - df::squad::T_rooms* room_from_squad = nullptr; - df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + df::squad_barracks_infost* room_from_squad = nullptr; + df::building_squad_infost* room_from_building = nullptr; for (auto room : squad->rooms) { @@ -263,18 +270,18 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (!avoiding_squad_roundtrip && room_from_squad == nullptr) { - room_from_squad = new df::squad::T_rooms(); + room_from_squad = new df::squad_barracks_infost(); room_from_squad->building_id = civzone_id; - insert_into_vector(squad->rooms, &df::squad::T_rooms::building_id, room_from_squad); + insert_into_vector(squad->rooms, &df::squad_barracks_infost::building_id, room_from_squad); } if (room_from_building == nullptr) { - room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building = new df::building_squad_infost(); room_from_building->squad_id = squad_id; - insert_into_vector(zone->squad_room_info, &df::building_civzonest::T_squad_room_info::squad_id, room_from_building); + insert_into_vector(zone->squad_room_info, &df::building_squad_infost::squad_id, room_from_building); } if (room_from_squad) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 933ea537547..1af93c3b7d8 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -61,16 +61,20 @@ distribution. #include "df/historical_kills.h" #include "df/history_event_hist_figure_diedst.h" #include "df/identity.h" +#include "df/interaction_profilest.h" #include "df/item.h" #include "df/job.h" #include "df/nemesis_record.h" +#include "df/personality_goalst.h" #include "df/plotinfost.h" +#include "df/reputation_profilest.h" #include "df/syndrome.h" #include "df/tile_occupancy.h" #include "df/training_assignment.h" #include "df/unit.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" +#include "df/unit_active_animationst.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" #include "df/unit_path_goal.h" @@ -79,6 +83,7 @@ distribution. #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include "df/world_site.h" @@ -325,7 +330,7 @@ bool Units::isNaked(df::unit *unit, bool no_items) { for (auto inv_item : unit->inventory) { // TODO: Check for proper coverage (bad thought) - if (inv_item->mode == df::unit_inventory_item::Worn) + if (inv_item->mode == df::inv_item_role_type::Worn) return false; } return true; @@ -1247,7 +1252,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { // This is 100% reverse-engineered from DF code int rating = getNominalSkill(unit, skill_id, true); // Apply special states - if (unit->counters.soldier_mood == df::unit::T_counters::None) { + if (unit->counters.soldier_mood == df::soldier_mood_type::None) { if (unit->counters.nausea > 0) rating >>= 1; if (unit->counters.winded > 0) @@ -1260,7 +1265,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { rating >>= 1; } - if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) { + if (unit->counters.soldier_mood != df::soldier_mood_type::MartialTrance) { if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle && !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged && !hasExtravision(unit)) @@ -1608,7 +1613,7 @@ float Units::computeSlowdownFactor(df::unit *unit) { if (!unit->flags1.bits.marauder && casteFlagSet(unit->race, unit->caste, caste_raw_flags::MEANDERER) && !(unit->following && isCitizen(unit)) && - linear_index(unit->inventory, &df::unit_inventory_item::mode, df::unit_inventory_item::Hauled) < 0) + linear_index(unit->inventory, &df::unit_inventory_item::mode, df::inv_item_role_type::Hauled) < 0) { coeff *= 4.0f; } diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 3ed5455d0f4..8f75e0549e2 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -568,7 +568,7 @@ struct VeinGenerator bool VeinGenerator::init_biomes() { auto &mats = df::inorganic_raw::get_vector(); - materials.resize(world->raws.inorganics.size()); + materials.resize(world->raws.inorganics.all.size()); for (size_t i = 0; i < mats.size(); i++) { diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index c7ce4114e7b..b82442a66da 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -679,7 +679,7 @@ static const char * if_pretty(const tile_context &ctx, const char *c) { } static bool is_rectangular(const df::building *bld) { - const df::building_extents &room = bld->room; + const df::building::T_room &room = bld->room; if (!room.extents) return true; for (int32_t y = 0; y < room.height; ++y) { diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index fe317e4705b..64ddf89e34f 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -166,12 +166,12 @@ static void load_organic_material_cache(df::organic_mat_category cat) { } static void load_material_cache() { - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) if (raws.mat_table.builtin[i]) cache_matched(i, -1); - for (size_t i = 0; i < raws.inorganics.size(); i++) + for (size_t i = 0; i < raws.inorganics.all.size(); i++) cache_matched(0, i); load_organic_material_cache(df::organic_mat_category::Wood); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index d55b5f5da46..1a97a801de1 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -337,7 +337,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, } auto item = closest->second; // some item must be closest. - if (Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { + if (Job::attachJobItem(job, item, df::job_role_type::Hauled, filter_idx)) { MaterialInfo material; material.decode(item); ItemTypeInfo item_type; diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d739125..50b0c7f1389 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -29,7 +29,6 @@ #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/world.h" -#include "df/world_raws.h" using std::vector; using std::string; diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index ceaec299219..c0944249f1e 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -13,6 +13,7 @@ #include "df/job.h" #include "df/unit.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc309..fdb11741ca0 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -21,7 +21,6 @@ using namespace std; #include #include "df/world.h" -#include "df/world_raws.h" #include "df/building_def.h" #include "df/unit_inventory_item.h" #include diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index d4fddb48467..35f02381236 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -46,7 +46,6 @@ #include "df/unit_preference.h" #include "df/unit_soul.h" #include "df/viewscreen_unitst.h" -#include "df/world_raws.h" using std::deque; @@ -1184,7 +1183,7 @@ struct preference_map label = ""; typedef df::unit_preference::T_type T_type; - df::world_raws &raws = world->raws; + auto &raws = world->raws; switch (pref.type) { case (T_type::LikeCreature): diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index ee418363e7d..db04308d0b2 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -18,7 +18,6 @@ #include "df/world.h" #include "df/world_data.h" #include "df/world_geo_biome.h" -#include "df/world_raws.h" #include "defs.h" #include "embark-assistant.h" diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index b4c55dd8235..bb90435343b 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -14,9 +14,7 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region_type.h" -#include "df/world_raws.h" #include "embark-assistant.h" #include "finder_ui.h" diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index b540ae80b4d..9f3900f0c9e 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -10,7 +10,6 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_type.h" diff --git a/plugins/embark-assistant/overlay.cpp b/plugins/embark-assistant/overlay.cpp index d5b50294a97..37130c6897b 100644 --- a/plugins/embark-assistant/overlay.cpp +++ b/plugins/embark-assistant/overlay.cpp @@ -8,7 +8,6 @@ #include "df/viewscreen.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" -#include "df/world_raws.h" #include "finder_ui.h" #include "help_ui.h" diff --git a/plugins/embark-assistant/survey.cpp b/plugins/embark-assistant/survey.cpp index 84994b28691..ddfc6d8d498 100644 --- a/plugins/embark-assistant/survey.cpp +++ b/plugins/embark-assistant/survey.cpp @@ -30,7 +30,6 @@ #include "df/interaction_source_type.h" #include "df/interaction_target.h" #include "df/interaction_target_materialst.h" -#include "df/material_common.h" #include "df/reaction.h" #include "df/reaction_product_itemst.h" #include "df/reaction_product_type.h" @@ -42,7 +41,6 @@ #include "df/world_data.h" #include "df/world_geo_biome.h" #include "df/world_geo_layer.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_feature.h" diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index df716831db8..fa8cb8ef12d 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -238,7 +238,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * return false; } - if (!Items::moveToInventory(item, unit, df::unit_inventory_item::Worn, bpIndex)) + if (!Items::moveToInventory(item, unit, df::inv_item_role_type::Worn, bpIndex)) { if (verbose) { WARN(log).print("\nEquipping failed - failed to retrieve item from its current location/container/inventory. Please move it to the ground and try again.\n"); } return false; diff --git a/plugins/generated-creature-renamer.cpp b/plugins/generated-creature-renamer.cpp index b0a00b3f212..0ea3227b1b8 100644 --- a/plugins/generated-creature-renamer.cpp +++ b/plugins/generated-creature-renamer.cpp @@ -3,7 +3,6 @@ #include "Export.h" #include "PluginManager.h" #include "df/world.h" -#include "df/world_raws.h" #include "df/creature_raw.h" #include "df/caste_raw.h" #include "modules/World.h" diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 9b41c2e2ca2..ffa62ed8304 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -8,6 +8,7 @@ #include "modules/Maps.h" #include "modules/World.h" +#include "df/block_column_print_infost.h" #include "df/construction.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -190,8 +191,7 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { __LINE__, bpos.x, bpos.y); continue; } - df::map_block_column::T_unmined_glyphs *glyphs = - new df::map_block_column::T_unmined_glyphs; + df::block_column_print_infost *glyphs = new df::block_column_print_infost; glyphs->x[0] = 0; glyphs->x[1] = 1; glyphs->x[2] = 2; diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 52b9bf43a44..bcb59b7ad5c 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -10,6 +10,7 @@ #include "modules/World.h" #include "df/building.h" +#include "df/buildingitemst.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" #include "df/caravan_state.h" diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index 64cfcc301bc..b935c34f814 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -6,6 +6,7 @@ #include "modules/Persistence.h" #include "modules/World.h" +#include "df/buildingitemst.h" #include "df/building_nest_boxst.h" #include "df/item.h" #include "df/item_eggst.h" diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 89df7832453..49558c1b88e 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -765,8 +765,8 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) if (it2.isMember("bearing")) { std::string bearing(it2["bearing"].asString()); - auto found = std::find_if(world->raws.inorganics.begin(), world->raws.inorganics.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); - if (found == world->raws.inorganics.end()) + auto found = std::find_if(world->raws.inorganics.all.begin(), world->raws.inorganics.all.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); + if (found == world->raws.inorganics.all.end()) { delete condition; @@ -774,7 +774,7 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) continue; } - condition->metal_ore = found - world->raws.inorganics.begin(); + condition->metal_ore = found - world->raws.inorganics.all.begin(); } if (it2.isMember("reaction_class")) @@ -1008,8 +1008,8 @@ static bool orders_compare(df::manager_order *a, df::manager_order *b) return a->workshop_id >= 0; } - if (a->frequency == df::manager_order::T_frequency::OneTime - || b->frequency == df::manager_order::T_frequency::OneTime) + if (a->frequency == df::workquota_frequency_type::OneTime + || b->frequency == df::workquota_frequency_type::OneTime) return a->frequency < b->frequency; return a->frequency > b->frequency; } diff --git a/plugins/probe.cpp b/plugins/probe.cpp index 31c2df5edca..eff0c98c863 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -46,7 +46,7 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) for (auto inv_item : unit->inventory) { df::item* item = inv_item->item; - if (inv_item->mode == df::unit_inventory_item::T_mode::Worn) { + if (inv_item->mode == df::inv_item_role_type::Worn) { out << " wears item: #" << item->id; if (item->flags.bits.owned) out << " (owned)"; diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 159b557e4fb..e8883fde590 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -223,9 +223,11 @@ void printVeins(color_ostream &con, MatMap &mat_map, MatMap gems; MatMap rest; + auto & inorganics = world->raws.inorganics.all; + for (const auto &kv : mat_map) { - df::inorganic_raw *gloss = vector_get(world->raws.inorganics, kv.first); + df::inorganic_raw *gloss = vector_get(inorganics, kv.first); if (!gloss) { con.printerr("invalid material gloss: %hi\n", kv.first); @@ -242,17 +244,17 @@ void printVeins(color_ostream &con, MatMap &mat_map, if (options.ores) { con << "Ores:" << std::endl; - printMats(con, ores, world->raws.inorganics, options); + printMats(con, ores, inorganics, options); } if (options.gems) { con << "Gems:" << std::endl; - printMats(con, gems, world->raws.inorganics, options); + printMats(con, gems, inorganics, options); } if (options.veins) { con << "Other vein stone:" << std::endl; - printMats(con, rest, world->raws.inorganics, options); + printMats(con, rest, inorganics, options); } } @@ -604,7 +606,7 @@ static command_result embark_prospector(color_ostream &out, // Print the report if (options.layers) { out << "Layer materials:" << std::endl; - printMats(out, layerMats, world->raws.inorganics, options); + printMats(out, layerMats, world->raws.inorganics.all, options); } if (options.hidden) { @@ -837,7 +839,7 @@ static command_result map_prospector(color_ostream &con, if (options.layers) { con << "Layer materials:" << std::endl; - printMats(con, layerMats, world->raws.inorganics, options); + printMats(con, layerMats, world->raws.inorganics.all, options); } if (options.features) { diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index 7cc715147b8..ebf0359afdc 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -529,7 +529,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re default: break; } - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.raised) remote_build->set_active(1); else remote_build->set_active(0); @@ -671,7 +671,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto actual = strict_virtual_cast(local_build); if (actual) { - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.retracted) remote_build->set_active(1); else remote_build->set_active(0); diff --git a/plugins/remotefortressreader/item_reader.cpp b/plugins/remotefortressreader/item_reader.cpp index 1353243b2ba..58c84b90755 100644 --- a/plugins/remotefortressreader/item_reader.cpp +++ b/plugins/remotefortressreader/item_reader.cpp @@ -14,7 +14,6 @@ #include "df/art_image_property.h" #include "df/art_image_property_intransitive_verbst.h" #include "df/art_image_property_transitive_verbst.h" -#include "df/art_image_ref.h" #include "df/descriptor_shape.h" #include "df/instrument_piece.h" #include "df/instrument_register.h" @@ -240,19 +239,19 @@ void CopyItem(RemoteFortressReader::Item * NetItem, df::item * DfItem) GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), statue->image.id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), statue->image.id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == statue->image.id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == statue->image.id) + chunk = world->art_image_chunks.all[i]; } } - if (chunk && chunk->images[statue->image.subid]) + if (chunk && chunk->images[statue->image.subid].art_image) { - CopyImage(chunk->images[statue->image.subid], NetItem->mutable_image()); + CopyImage(chunk->images[statue->image.subid].art_image, NetItem->mutable_image()); } @@ -663,11 +662,11 @@ DFHack::command_result GetItemList(DFHack::color_ostream &stream, const DFHack:: { send_instrument->add_tuning_parm(*(instrument->tuning_parm[j])); } - for (size_t j = 0; j < instrument->registers.size(); j++) + for (size_t j = 0; j < instrument->timbre.registers.size(); j++) { auto reg = send_instrument->add_registers(); - reg->set_pitch_range_min(instrument->registers[j]->pitch_range_min); - reg->set_pitch_range_max(instrument->registers[j]->pitch_range_max); + reg->set_pitch_range_min(instrument->timbre.registers[j]->pitch_range_min); + reg->set_pitch_range_max(instrument->timbre.registers[j]->pitch_range_max); } send_instrument->set_description(DF2UTF(instrument->description)); } diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index ae6683dee95..697eb3fe921 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -42,6 +42,7 @@ #include "df/body_part_layer_raw.h" #include "df/body_part_raw.h" #include "df/bp_appearance_modifier.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/building_wellst.h" @@ -64,6 +65,7 @@ #include "df/inorganic_raw.h" #include "df/item.h" #include "df/job.h" +#include "df/job_postingst.h" #include "df/job_type.h" #include "df/job_item.h" #include "df/job_material_category.h" @@ -97,6 +99,7 @@ #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_savegamest.h" @@ -194,7 +197,6 @@ const char* growth_locations[] = { #include "df/art_image.h" #include "df/art_image_chunk.h" -#include "df/art_image_ref.h" command_result loadArtImageChunk(color_ostream &out, std::vector & parameters) { if (parameters.size() != 1) @@ -210,7 +212,7 @@ command_result loadArtImageChunk(color_ostream &out, std::vector & if (GetArtImageChunk) { int index = atoi(parameters[0].c_str()); - auto chunk = GetArtImageChunk(&(world->art_image_chunks), index); + auto chunk = GetArtImageChunk(&(world->art_image_chunks.all), index); out.print("Loaded chunk id: %d\n", chunk->id); } return CR_OK; @@ -809,10 +811,10 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage return CR_OK; } - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; // df::world_history *history = &world->history; MaterialInfo mat; - for (size_t i = 0; i < raws->inorganics.size(); i++) + for (size_t i = 0; i < raws->inorganics.all.size(); i++) { mat.decode(0, i); MaterialDefinition *mat_def = out->add_material_list(); @@ -820,9 +822,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; - if (size_t(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)]) < raws->descriptors.colors.size()) + if (size_t(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)]) < raws->descriptors.colors.size()) { - ConvertDFColorDescriptor(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)], mat_def->mutable_state_color()); + ConvertDFColorDescriptor(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)], mat_def->mutable_state_color()); } } for (int i = 0; i < 19; i++) @@ -890,7 +892,7 @@ static command_result GetGrowthList(color_ostream &stream, const EmptyMessage *i - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; if (!raws) return CR_OK;//'. @@ -1154,7 +1156,7 @@ void CopyDesignation(df::map_block * DfBlock, RemoteFortressReader::MapBlock * N void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (projectile == NULL) @@ -1187,7 +1189,7 @@ void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { bool isProj = false; auto vehicle = world->vehicles.active[i]; - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (!projectile) @@ -1515,14 +1517,14 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), engraving->art_id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), engraving->art_id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == engraving->art_id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == engraving->art_id) + chunk = world->art_image_chunks.all[i]; } } if (!chunk) @@ -1534,8 +1536,8 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in ConvertDFCoord(engraving->pos, netEngraving->mutable_pos()); netEngraving->set_quality(engraving->quality); netEngraving->set_tile(engraving->tile); - if (chunk->images[engraving->art_subid]) { - CopyImage(chunk->images[engraving->art_subid], netEngraving->mutable_image()); + if (chunk->images[engraving->art_subid].art_image) { + CopyImage(chunk->images[engraving->art_subid].art_image, netEngraving->mutable_image()); } netEngraving->set_floor(engraving->flags.bits.floor); netEngraving->set_west(engraving->flags.bits.west); @@ -1772,7 +1774,7 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques if (unit->flags1.bits.projectile) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(item, df::proj_unitst, proj->item); if (item == NULL) @@ -1985,27 +1987,23 @@ static command_result GetWorldMap(color_ostream &stream, const EmptyMessage *in, out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); auto poles = data->flip_latitude; -#if DF_VERSION_INT > 34011 switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2133,28 +2131,24 @@ static command_result GetWorldMapNew(color_ostream &stream, const EmptyMessage * out->set_world_height(height); out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); -#if DF_VERSION_INT > 34011 auto poles = data->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2272,28 +2266,24 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w sprintf(name, "Region %d, %d", pos_x, pos_y); out->set_name_english(name); out->set_name(name); -#if DF_VERSION_INT > 34011 auto poles = worldData->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif df::world_region_details * south = NULL; df::world_region_details * east = NULL; diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 1efdd4df605..05cb7764877 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -23,7 +23,7 @@ DBG_EXTERN(stockpiles, log); void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx); - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; int16_t type = table.organic_types[mat_category][food_idx]; @@ -58,7 +58,7 @@ size_t OrganicMatLookup::food_max_size(organic_mat_category::organic_mat_categor void OrganicMatLookup::food_build_map() { if (index_built) return; - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; @@ -73,7 +73,7 @@ void OrganicMatLookup::food_build_map() { } int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_category::organic_mat_category mat_category, const std::string& token) { - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; DEBUG(log, out).print("food_idx_by_token:\n"); if (mat_category == organic_mat_category::Fish || diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index f315e7d674f..1d11db41e57 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -566,7 +566,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo vector& pile_list) { // we initialize all disallowed values to 1 // why? because that's how the memory is in DF before we muck with it. - size_t num_elems = world->raws.inorganics.size(); + size_t num_elems = world->raws.inorganics.all.size(); pile_list.resize(num_elems, 0); for (size_t i = 0; i < pile_list.size(); ++i) { MaterialInfo mi(0, i); @@ -814,9 +814,9 @@ void StockpileSerializer::read(color_ostream &out, DeserializeMode mode, const v void StockpileSerializer::write_containers(color_ostream& out) { DEBUG(log, out).print("writing container settings\n"); - mBuffer.set_max_bins(mPile->max_bins); - mBuffer.set_max_barrels(mPile->max_barrels); - mBuffer.set_max_wheelbarrows(mPile->max_wheelbarrows); + mBuffer.set_max_bins(mPile->storage.max_bins); + mBuffer.set_max_barrels(mPile->storage.max_barrels); + mBuffer.set_max_wheelbarrows(mPile->storage.max_wheelbarrows); } template @@ -869,21 +869,21 @@ void StockpileSerializer::read_containers(color_ostream& out, DeserializeMode mo read_elem(out, "max_bins", mode, std::bind(&StockpileSettings::has_max_bins, mBuffer), std::bind(&StockpileSettings::max_bins, mBuffer), - mPile->max_bins); + mPile->storage.max_bins); read_elem(out, "max_barrels", mode, std::bind(&StockpileSettings::has_max_barrels, mBuffer), std::bind(&StockpileSettings::max_barrels, mBuffer), - mPile->max_barrels); + mPile->storage.max_barrels); read_elem(out, "max_wheelbarrows", mode, std::bind(&StockpileSettings::has_max_wheelbarrows, mBuffer), std::bind(&StockpileSettings::max_wheelbarrows, mBuffer), - mPile->max_wheelbarrows); + mPile->storage.max_wheelbarrows); } void StockpileSettingsSerializer::write_general(color_ostream& out) { DEBUG(log, out).print("writing general settings\n"); - mBuffer.set_allow_inorganic(mSettings->allow_inorganic); - mBuffer.set_allow_organic(mSettings->allow_organic); + mBuffer.set_allow_inorganic(mSettings->misc.allow_inorganic); + mBuffer.set_allow_organic(mSettings->misc.allow_organic); } void StockpileSerializer::write_general(color_ostream& out) { @@ -895,11 +895,11 @@ void StockpileSettingsSerializer::read_general(color_ostream& out, DeserializeMo read_elem(out, "allow_inorganic", mode, std::bind(&StockpileSettings::has_allow_inorganic, mBuffer), std::bind(&StockpileSettings::allow_inorganic, mBuffer), - mSettings->allow_inorganic); + mSettings->misc.allow_inorganic); read_elem(out, "allow_organic", mode, std::bind(&StockpileSettings::has_allow_organic, mBuffer), std::bind(&StockpileSettings::allow_organic, mBuffer), - mSettings->allow_organic); + mSettings->misc.allow_organic); } void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) { diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 4955d49e960..98b8b4b7256 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -644,8 +644,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == builtin_mats::INORGANIC) { item->mat_type = pref->mattype; @@ -722,8 +722,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::SILK)) @@ -805,8 +805,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == 0 && getCreatedMetalBars(pref->matindex) > 0) mats.push_back(pref->matindex); } @@ -835,8 +835,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && ((pref->mattype == builtin_mats::GLASS_GREEN) || (pref->mattype == builtin_mats::GLASS_CLEAR && have_glass[1]) || (pref->mattype == builtin_mats::GLASS_CRYSTAL && have_glass[2]))) @@ -867,8 +867,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::BONE)) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index ad96302887e..ef9b77c67aa 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -276,7 +276,7 @@ class Tailor { for (auto inv : u->inventory) { - if (inv->mode != df::unit_inventory_item::Worn) + if (inv->mode != df::inv_item_role_type::Worn) continue; // skip non-clothing if (!inv->item->isClothing()) @@ -456,7 +456,7 @@ class Tailor { order->mat_index == -1 && order->specdata.hist_figure_id == hfid && order->material_category.whole == mcat.whole && - order->frequency == df::manager_order::T_frequency::OneTime) + order->frequency == df::workquota_frequency_type::OneTime) return order; } return NULL; diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index a472c2efec3..ea75ced3598 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -32,6 +32,7 @@ #include "df/activity_event_worshipst.h" #include "df/building_nest_boxst.h" #include "df/building_trapst.h" +#include "df/buildingitemst.h" #include "df/init.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -409,7 +410,9 @@ static void adjust_activities(color_ostream &out, int32_t timeskip) { for (auto act : world->activities.all) { for (auto ev : act->events) { switch (ev->getType()) { - using namespace df::enums::activity_event_type; + using namespace df::enums::activity_event_type; + case NONE: + break; case TrainingSession: // no counters From 3583334b7aa0039f78dd852f32c2482ec5886672 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 14:55:38 -0800 Subject: [PATCH 058/919] update core/plugin lua for great reorg --- library/lua/gui/dwarfmode.lua | 2 +- library/lua/syndrome-util.lua | 2 +- plugins/lua/buildingplan/planneroverlay.lua | 2 +- plugins/lua/sort/diplomacy.lua | 2 +- test/library/print.lua | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd5202..61317f9ebc2 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -249,7 +249,7 @@ end function get_hotkey_target(key) local hk = HOTKEY_KEYS[key] - if hk and hk.cmd == df.ui_hotkey.T_cmd.Zoom then + if hk and hk.cmd == df.hotkey_type.Zoom then return xyz2pos(hk.x, hk.y, hk.z) end end diff --git a/library/lua/syndrome-util.lua b/library/lua/syndrome-util.lua index efbaa5deaae..c0af9f8a726 100644 --- a/library/lua/syndrome-util.lua +++ b/library/lua/syndrome-util.lua @@ -98,7 +98,7 @@ function infectWithSyndrome(target,syndrome,resetPolicy) unitSyndrome.ticks = 0 unitSyndrome.wound_id = -1 for k,v in ipairs(syndrome.ce) do - local symptom = df.unit_syndrome.T_symptoms:new() + local symptom = df.active_creature_interaction_effectst:new() symptom.quantity = 0 symptom.delay = 0 symptom.ticks = 0 diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 62e1a6c49c5..f92dc95d842 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -1348,7 +1348,7 @@ function PlannerOverlay:place_building(placement_data, chosen_items) dfhack.printerr(('item no longer available: %d'):format(item_id)) break end - if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + if not dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, idx-1, -1) then dfhack.printerr(('cannot attach item: %d'):format(item_id)) break end diff --git a/plugins/lua/sort/diplomacy.lua b/plugins/lua/sort/diplomacy.lua index c0ec8185057..3e785938623 100644 --- a/plugins/lua/sort/diplomacy.lua +++ b/plugins/lua/sort/diplomacy.lua @@ -64,7 +64,7 @@ local function get_preferences(unit) if not unit then return {} end local preferences = {} for _, pref in ipairs(unit.status.current_soul.preferences) do - if pref.type == df.unit_preference.T_type.LikeItem and pref.active then + if pref.type == df.unitpref_type.LikeItem and pref.active then table.insert(preferences, make_item_description(pref.item_type, pref.item_subtype)) end end diff --git a/test/library/print.lua b/test/library/print.lua index 28a2e703704..79243d8989c 100644 --- a/test/library/print.lua +++ b/test/library/print.lua @@ -47,7 +47,7 @@ end local function new_int_vector() -- create a vector of integers by cloning one from world. we do it this way -- because we can't allocate typed vectors from lua directly. - local vector = df.global.world.busy_buildings:new() + local vector = df.global.world.building_uses.buildings:new() vector:resize(0) return vector end From 17be93cb594379622ec8309ef1490c4d35ce2889 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 16:58:00 -0800 Subject: [PATCH 059/919] ignore missing viewscreens unused viewscreens are optimized out of windows builds but not linux builds, so this is necessary to get this test to pass on Windows --- test/structures/misc.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 13febff566b..33d386b8327 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -28,8 +28,10 @@ function test.viewscreenDtors() for name, type in pairs(df) do if name:startswith('viewscreen') then print('testing', name) - local v = type:new() - expect.true_(v:delete(), "destructor returned false: " .. name) + local ok, v = pcall(function() return type:new() end) + if ok then + expect.true_(v:delete(), "destructor returned false: " .. name) + end end end end From df46a55a523606ba005ac61639cbac3b31974989 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 23:13:44 -0800 Subject: [PATCH 060/919] add unit test for allocating and deallocating all types --- test/structures/misc.lua | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 33d386b8327..ab58187b31e 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -24,14 +24,39 @@ function test.overlappingGlobals() end end -function test.viewscreenDtors() +local known_bad_types = { + -- renderer base class has non-destructible padding declared + renderer_2d_base=true, + renderer_2d=true, + renderer_offscreen=true, + + -- abstract base classes that aren't instantiable + active_script_varst=true, + widget_sheet_button=true, +} + +if dfhack.getOSType() == 'linux' then + -- empty destructors are declared inline for these types, + -- and gcc appears to optimize them out + known_bad_types.mental_picture_propertyst = true + known_bad_types.region_block_eventst = true +end + +function test.destructors() + local count = 1 for name, type in pairs(df) do - if name:startswith('viewscreen') then - print('testing', name) - local ok, v = pcall(function() return type:new() end) - if ok then - expect.true_(v:delete(), "destructor returned false: " .. name) - end + if known_bad_types[name] then + goto continue + end + print(('testing constructor %5d: %s'):format(count, name)) + local ok, v = pcall(function() return type:new() end) + if not ok then + print(' constructor failed; skipping destructor test') + else + print(' destructor ok') + expect.true_(v:delete(), "destructor returned false: " .. name) end + count = count + 1 + ::continue:: end end From 6518abd6d6384105d5d534e3a56c464ddc0c03aa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 07:34:29 -0800 Subject: [PATCH 061/919] update submodule refs --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index b3202b06e04..2a84adf2fe1 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b3202b06e04c03433565e8248f2638dddde4ee22 +Subproject commit 2a84adf2fe1e76f5244f51cd4536442ebce10b5d diff --git a/plugins/stonesense b/plugins/stonesense index 92794aec776..8d445c72d5b 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 92794aec7768bdafd0f5fc23fed8b30655805b32 +Subproject commit 8d445c72d5bc797bc2e8f6d0c42091fc068d0893 diff --git a/scripts b/scripts index fc67fdc5b67..fc3f7d044b1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fc67fdc5b67a25a776eebfa769b289801679e364 +Subproject commit fc3f7d044b1b45f3ad7b9f7ba3bcc5a4bb24b09b From aedfa33bc99c918eab848f5ddac5d9414933f97c Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 9 Feb 2025 22:01:35 +0100 Subject: [PATCH 062/919] spectate.lua: implement unit banners and mouse tooltips --- plugins/lua/spectate.lua | 373 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 360 insertions(+), 13 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d44957dcb19..36045c7a48a 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -6,9 +6,15 @@ local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') +local gui = require('gui') +local widgets = require('gui.widgets') + -- settings starting with 'tooltip-' are not passed to the C++ plugin local lua_only_settings_prefix = 'tooltip-' +-- how many lines the text following unit is allowed to be moved down to avoid overlapping +local max_banner_y_offset = 4 + local function get_default_state() return { ['auto-disengage']=true, @@ -21,32 +27,69 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, + ['tooltip-follow-blink-milliseconds']=2000, ['tooltip-follow-job']=true, - ['tooltip-follow-name']=true, + ['tooltip-follow-job-shortenings'] = { + ["Store item in stockpile"] = "Store item", + }, + ['tooltip-follow-name']=false, ['tooltip-follow-stress']=true, + ['tooltip-follow-stress-levels']={ + [0] = + true, -- Miserable + true, + false, + false, + false, + true, + true, -- Ecstatic + }, ['tooltip-hover-job']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, + ['tooltip-hover-stress-levels']={ + [0] = + true, -- Miserable + true, + false, + false, + false, + true, + true, -- Ecstatic + }, + ['tooltip-stress-levels']={ + -- keep in mind, the text will look differently with game's font + -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE + [0] = + {text = "=C", pen = COLOR_RED, name = "Miserable"}, + {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, + {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, + {text = ":]", pen = COLOR_GREY, name = "Content"}, + {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, + {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, + {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, + } } end local function load_state() local state = get_default_state() - local config = json.open('dfhack-config/spectate.json') - for key in pairs(config.data) do + local config_file = json.open('dfhack-config/spectate.json') + for key in pairs(config_file.data) do if state[key] == nil then - config.data[key] = nil + config_file.data[key] = nil end end - utils.assign(state, config.data) - config.data = state - return config + utils.assign(state, config_file.data) + config_file.data = state + return config_file.data, + function() config_file:write() end end -local config = load_state() +local config, save_state = load_state() function refresh_cpp_config() - for name,value in pairs(config.data) do + for name,value in pairs(config) do if not name:startswith(lua_only_settings_prefix) then if type(value) == 'boolean' then value = value and 1 or 0 @@ -77,7 +120,7 @@ local function print_status() print('spectate is:', isEnabled() and 'enabled' or 'disabled') print() print('settings:') - for key, value in pairs(config.data) do + for key, value in pairs(config) do print(' ' .. key .. ': ' .. tostring(value)) end end @@ -91,7 +134,7 @@ local function do_toggle() end local function set_setting(key, value) - if config.data[key] == nil then + if config[key] == nil then qerror('unknown setting: ' .. key) end if key == 'follow-seconds' then @@ -99,8 +142,8 @@ local function set_setting(key, value) else value = argparse.boolean(value, key) end - config.data[key] = value - config:write() + config[key] = value + save_state() if not key:startswith(lua_only_settings_prefix) then if type(value) == 'boolean' then value = value and 1 or 0 @@ -131,6 +174,80 @@ function parse_commandline(args) return true end +----------------------------- +-- info functions + +local function GetUnitStress(unit, stress_levels) + local stressCat = dfhack.units.getStressCategory(unit) + if stressCat > 6 then stressCat = 6 end + if not stress_levels[stressCat] then return end + + local level_cfg = config['tooltip-stress-levels'][stressCat] + return {text=level_cfg.text, pen=level_cfg.pen} +end + +local function GetUnitName(unit) + return dfhack.units.getReadableName(unit) +end + +local function GetUnitJob(unit) + local job = unit.job.current_job + return job and dfhack.job.getName(job) +end + +local function GetRelevantSettings(key) + return config['tooltip-' .. key .. '-name'], + config['tooltip-' .. key .. '-job'], + config['tooltip-' .. key .. '-stress'], + config['tooltip-' .. key .. '-stress-levels'], + config['tooltip-' .. key .. '-job-shortenings'] +end + +local function GetUnitInfoText(unit, settings_group_name) + local show_name, show_job, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) + + local stress = show_stress and GetUnitStress(unit, stress_levels) or nil + local name = show_name and GetUnitName(unit) or nil + local job = show_job and GetUnitJob(unit) or nil + if job_shortenings then job = job_shortenings[job] or job end + + local txt = {} + if stress then + txt[#txt+1] = stress + if name or job then txt[#txt+1] = ' ' end + end + if name then + txt[#txt+1] = name + end + if job then + if name then txt[#txt+1] = ": " end + txt[#txt+1] = job + end + + return txt +end + +local function GetHoverText(pos) + if not pos then return end + + local txt = {} + local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? + + for _,unit in ipairs(units) do + local info = GetUnitInfoText(unit, 'hover') + if not next(info) then goto continue end + + for _,t in ipairs(info) do + txt[#txt+1] = t + end + txt[#txt+1] = NEWLINE + + ::continue:: + end + + return txt +end + ----------------------------- -- overlays @@ -142,6 +259,236 @@ TooltipOverlay.ATTRS{ viewscreens='dwarfmode/Default', } +function TooltipOverlay:init() + self:addviews{MouseTooltip{view_id = 'tooltip'}} +end + +function TooltipOverlay:preUpdateLayout(parent_rect) + -- this is required, otherwise there is no room to draw child widgets in + self.frame.w = parent_rect.width + self.frame.h = parent_rect.height +end + +function TooltipOverlay:render(dc) + self:render_unit_banners(dc) + TooltipOverlay.super.render(self, dc) +end + +local function AnyFollowOptionOn() + return config['tooltip-follow-job'] + or config['tooltip-follow-name'] + or config['tooltip-follow-stress'] +end + +-- map coordinates -> interface layer coordinates +local function GetScreenCoordinates(map_coord) + -- -> map viewport offset + local vp = df.global.world.viewport + local vp_Coord = vp.corner + local map_offset_by_vp = { + x = map_coord.x - vp_Coord.x, + y = map_coord.y - vp_Coord.y, + z = map_coord.z - vp_Coord.z, + } + + if not dfhack.screen.inGraphicsMode() then + return map_offset_by_vp + else + -- -> pixel offset + local gps = df.global.gps + local map_tile_pixels = gps.viewport_zoom_factor // 4; + local screen_coord_px = { + x = map_tile_pixels * map_offset_by_vp.x, + y = map_tile_pixels * map_offset_by_vp.y, + } + -- -> interface layer coordinates + local screen_coord_text = { + x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), + y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), + } + + return screen_coord_text + end +end + +local function GetString(tokens) + local sb = {} + for _, tok in ipairs(tokens) do + if type(tok) == "string" then + sb[#sb+1] = tok + else -- must be a table token + sb[#sb+1] = tok.text + end + end + if not next(sb) then return nil end + return table.concat(sb) +end + +local once = false +function TooltipOverlay:render_unit_banners(dc) + if not AnyFollowOptionOn() then return end + + local blink_duration = config['tooltip-follow-blink-milliseconds'] + if blink_duration > 0 and not gui.blink_visible(blink_duration) then + return + end + + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end + + local vp = df.global.world.viewport + local topleft = vp.corner + local width = vp.max_x + local height = vp.max_y + local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} + + local units = dfhack.units.getUnitsInBox(topleft, bottomright) + if not units or #units == 0 then return end + + local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) + local pen = COLOR_WHITE + + local used_tiles = {} + -- reverse order yields better offsets for overlapping texts + for i = #units, 1, -1 do + local unit = units[i] + + local posX, posY, posZ = dfhack.units.getPosition(unit) + if not posX then goto continue end + local pos = xyz2pos(posX, posY, posZ) + + local info = GetUnitInfoText(unit, 'follow') + if not info or not next(info) then goto continue end + + local str = GetString(info) + if not str then goto continue end + + local scrPos = GetScreenCoordinates(pos) + local y = scrPos.y - 1 -- subtract 1 to move the text over the heads + local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + + -- to resolve overlaps, we'll mark every coordinate we write anything in, + -- and then check if the new tooltip will overwrite any used coordinate. + -- if it will, try the next row, to a maximum offset of 4. + local row + local dy = 0 + -- todo: search for the "best" offset instead, f.e. max `usedAt` value, with `-1` the best + local usedAt = -1 + for yOffset = 0, max_banner_y_offset do + dy = yOffset + + row = used_tiles[y + dy] + if not row then + row = {} + used_tiles[y + dy] = row + end + + usedAt = -1 + for j = 0, #str - 1 do + if row[x + j] then + usedAt = j + break + end + end + + if usedAt == -1 then break end + end -- for dy + -- if other text starts at the same position, or even 2 to the right, + -- we can't place any useful information, and will ignore it instead. + if 0 <= usedAt and usedAt <= 2 then goto continue end + + local writer = dc:seek(x, y + dy) + local ix = 0 + for _, tok in ipairs(info) do + local s + if type(tok) == "string" then + writer = writer:pen(pen) + s = tok + else + writer = writer:pen(tok.pen) + s = tok.text + end + + -- in case there isn't enough space, cut the text off + local len = #s + if usedAt > 0 and ix + len + 1 >= usedAt then + -- last position we can write is `usedAt - len - ix - 1` + -- we want to replace it with an `_`, so we need another `- 1` + s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' + + writer = writer:string(s) + break -- nothing more will fit + else + writer = writer:string(s) + end + + ix = ix + len + end + + -- mark coordinates as used + for j = 0, #str - 1 do + row[x + j] = true + end + + ::continue:: + end +end + +-- MouseTooltip is an almost copy&paste of the DimensionsTooltip +MouseTooltip = defclass(MouseTooltip, widgets.ResizingPanel) + +MouseTooltip.ATTRS{ + frame_style=gui.FRAME_THIN, + frame_background=gui.CLEAR_PEN, + no_force_pause_badge=true, + auto_width=true, + display_offset={x=3, y=3}, +} + +function MouseTooltip:init() + ensure_key(self, 'frame').w = 17 + self.frame.h = 4 + + self.label = widgets.Label{ + frame={t=0}, + auto_width=true, + } + + self:addviews{ + widgets.Panel{ + -- set minimum size for tooltip frame so the DFHack frame badge fits + frame={t=0, l=0, w=7, h=2}, + }, + self.label, + } +end + +local function AnyHoverOptionOn() + return config['tooltip-hover-job'] + or config['tooltip-hover-name'] + or config['tooltip-hover-stress'] +end + +function MouseTooltip:render(dc) + if not AnyHoverOptionOn() then return end + + local x, y = dfhack.screen.getMousePos() + if not x then return end + + local pos = dfhack.gui.getMousePos() + local text = GetHoverText(pos) + if not text or not next(text) then return end + self.label:setText(text) + + local sw, sh = dfhack.screen.getWindowSize() + local frame_width = math.max(9, self.label:getTextWidth() + 2) + self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) + self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) + self:updateLayout() + MouseTooltip.super.render(self, dc) +end + OVERLAY_WIDGETS = { tooltip=TooltipOverlay, } From 5a016ceacd5e4d9f7eb6c37277188d648ed1dbe9 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 9 Feb 2025 22:36:00 +0100 Subject: [PATCH 063/919] remove debug remnant --- plugins/lua/spectate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 36045c7a48a..96fc0770c44 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -324,7 +324,6 @@ local function GetString(tokens) return table.concat(sb) end -local once = false function TooltipOverlay:render_unit_banners(dc) if not AnyFollowOptionOn() then return end From 47792ce67181d476c14123232e61d4de2192890a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 17:06:51 -0800 Subject: [PATCH 064/919] finish updating modules for the Great Reorg --- library/lua/custom-raw-tokens.lua | 2 +- library/lua/dfhack/workshops.lua | 2 +- library/lua/gui/materials.lua | 6 +++--- plugins/lua/sort.lua | 4 ++-- plugins/lua/stockflow.lua | 2 +- plugins/rendermax/rendermax.lua | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/lua/custom-raw-tokens.lua b/library/lua/custom-raw-tokens.lua index 6c19deb72aa..7975ba5e394 100644 --- a/library/lua/custom-raw-tokens.lua +++ b/library/lua/custom-raw-tokens.lua @@ -300,7 +300,7 @@ local function getTokenArg1Else(userdata, token) elseif df.is_instance(df.building_workshopst, userdata) or df.is_instance(df.building_furnacest, userdata) then rawStruct = df.building_def.find(userdata.custom_type) elseif df.is_instance(df.interaction_instance, userdata) then - rawStruct = df.global.world.raws.interactions[userdata.interaction_id] + rawStruct = df.global.world.raws.interactions.all[userdata.interaction_id] else -- Assume raw struct *is* argument 1 rawStruct = userdata diff --git a/library/lua/dfhack/workshops.lua b/library/lua/dfhack/workshops.lua index a09849a57df..f43429642f3 100644 --- a/library/lua/dfhack/workshops.lua +++ b/library/lua/dfhack/workshops.lua @@ -509,7 +509,7 @@ local function addReactionJobs(ret,bid,wid,cid) end local function scanRawsOres() local ret={} - for idx,ore in ipairs(df.global.world.raws.inorganics) do + for idx,ore in ipairs(df.global.world.raws.inorganics.all) do if #ore.metal_ore.mat_index~=0 then ret[idx]=ore end diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index d3a9408a511..7429a78237a 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -116,7 +116,7 @@ end function MaterialDialog:initInorganicMode() local choices = {} - for i,mat in ipairs(df.global.world.raws.inorganics) do + for i,mat in ipairs(df.global.world.raws.inorganics.all) do self:addMaterial(choices, mat.material, 0, i, false, mat) end @@ -378,14 +378,14 @@ function ItemTraitsDialog(args) end -------------------------------------- local set_ore_ix = {} - for i, raw in ipairs(df.global.world.raws.inorganics) do + for i, raw in ipairs(df.global.world.raws.inorganics.all) do for _, ix in ipairs(raw.metal_ore.mat_index) do set_ore_ix[ix] = true end end local ores = {} for ix in pairs(set_ore_ix) do - local raw = df.global.world.raws.inorganics[ix] + local raw = df.global.world.raws.inorganics.all[ix] ores[#ores+1] = {mat_index = ix, name = raw.material.state_name.Solid} end table.sort(ores, function(a,b) return a.name < b.name end) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index fe42cbb1ed6..5777d2b174e 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -43,7 +43,7 @@ local function get_active_idx_cache() end local function is_original_dwarf(unit) - return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 + return df.global.plotinfo.fortress_age == unit.curse.interaction.time_on_site // 10 end local WAVE_END_GAP = 10000 @@ -53,7 +53,7 @@ local function get_most_recent_wave_oldest_active_idx(cache) for idx=#active_units-1,0,-1 do local unit = active_units[idx] if not dfhack.units.isCitizen(unit) then goto continue end - if oldest_unit and unit.curse.time_on_site - oldest_unit.curse.time_on_site > WAVE_END_GAP then + if oldest_unit and unit.curse.interaction.time_on_site - oldest_unit.curse.interaction.time_on_site > WAVE_END_GAP then return cache[oldest_unit.id] else oldest_unit = unit diff --git a/plugins/lua/stockflow.lua b/plugins/lua/stockflow.lua index 3b2b0fe6698..dc44d23045f 100644 --- a/plugins/lua/stockflow.lua +++ b/plugins/lua/stockflow.lua @@ -283,7 +283,7 @@ function collect_reactions() reaction_entry(result, job_types.CatchLiveFish) -- Cutting, encrusting, and metal extraction. - local rock_types = df.global.world.raws.inorganics + local rock_types = df.global.world.raws.inorganics.all for rock_id = #rock_types-1, 0, -1 do local material = rock_types[rock_id].material local rock_name = material.state_adj.Solid diff --git a/plugins/rendermax/rendermax.lua b/plugins/rendermax/rendermax.lua index f1cfa90203d..6aacbde64f5 100644 --- a/plugins/rendermax/rendermax.lua +++ b/plugins/rendermax/rendermax.lua @@ -164,7 +164,7 @@ function colorFrom16(col16) return {col[0],col[1],col[2]} end function addGems() - for k,v in pairs(df.global.world.raws.inorganics) do + for k,v in pairs(df.global.world.raws.inorganics.all) do if v.material.flags.IS_GEM then addMaterial("INORGANIC:"..v.id,colorFrom16(v.material.tile_color[0]+v.material.tile_color[2]*8)) end From 82c6551ebcb135f73ddb49e7d0d4533b2f794ccf Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 01:35:43 +0000 Subject: [PATCH 065/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 2a84adf2fe1..9bf00722a73 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2a84adf2fe1e76f5244f51cd4536442ebce10b5d +Subproject commit 9bf00722a739e6c00fd2a2c19ac7208e6f337ed3 diff --git a/plugins/stonesense b/plugins/stonesense index 8d445c72d5b..9f5aa0f22e7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8d445c72d5bc797bc2e8f6d0c42091fc068d0893 +Subproject commit 9f5aa0f22e70d3c9c0145fe2c4b4047a54dddca9 diff --git a/scripts b/scripts index fc3f7d044b1..795cba2474b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fc3f7d044b1b45f3ad7b9f7ba3bcc5a4bb24b09b +Subproject commit 795cba2474b928771cf59d6fc3690f4f5622bf99 From db49f6d5873e543ad0f72c0050bd34175de3b7a4 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 9 Feb 2025 21:57:50 -0800 Subject: [PATCH 066/919] Update docs/changelog.txt Co-authored-by: Myk --- docs/changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index aee713f373b..6d930a30451 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,6 @@ Template for new versions: - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S ## Fixes -- `gui/launcher`: ensure commandline is fully visible when searching through history and switching from a very long command to a short command - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself From b9f7882a3e310ef8b966ec39284bc5f44fbb91ef Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 9 Feb 2025 22:56:01 -0800 Subject: [PATCH 067/919] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6d930a30451..b36fad72840 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -76,6 +76,7 @@ Template for new versions: ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` +- ``gui.dwarfmode`` module: ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` now operate on the adventure mode look cursor, if active. Clearing the cursor sets it to the active adventurer's position. ## Removed From 57d850f2653af01c1bd32bc3733d6e820fe4b473 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:18:09 +0000 Subject: [PATCH 068/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 9f5aa0f22e7..9bc40021b36 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9f5aa0f22e70d3c9c0145fe2c4b4047a54dddca9 +Subproject commit 9bc40021b3673f59393e4c718949b5575599f01f From 928f2435fec9b0d80f84cc9dd482c30253e3da81 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 02:46:14 -0800 Subject: [PATCH 069/919] add property filters for brewable, millable, and processable --- docs/changelog.txt | 1 + docs/plugins/stockpiles.rst | 6 +++++ plugins/stockpiles/OrganicMatLookup.cpp | 8 +++--- plugins/stockpiles/OrganicMatLookup.h | 2 +- plugins/stockpiles/StockpileSerializer.cpp | 31 +++++++++++++++++++--- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3e5fa6ad905..e8bfa535c45 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter +- `stockpiles`: add property filters for brewable, millable, and processable organic materials ## Documentation - `stonesense-art-guide`: new guide for making sprite art for Stonesense diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 739cac5b8d4..506b9d783cd 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -378,6 +378,12 @@ Flags and subcategory prefixes:: paste/ pressed/ +Properties:: + + brewable + millable + processable + Settings files:: preparedmeals diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 05cb7764877..879353f3b03 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -39,9 +39,7 @@ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category: DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); } } -std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx) { - FoodMat food_mat; - food_mat_by_idx(out, mat_category, idx, food_mat); +std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, const FoodMat& food_mat) { if (food_mat.material.isValid()) { return food_mat.material.getToken(); } @@ -62,11 +60,11 @@ void OrganicMatLookup::food_build_map() { df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; - for (int32_t mat_category = traits::first_item_value; mat_category <= traits::last_item_value; ++mat_category) { + for (int32_t mat_category = 0; mat_category <= traits::last_item_value; ++mat_category) { for (size_t i = 0; i < table.organic_indexes[mat_category].size(); ++i) { int16_t type = table.organic_types[mat_category].at(i); int32_t index = table.organic_indexes[mat_category].at(i); - food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); // wtf.. only in c++ + food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); } } index_built = true; diff --git a/plugins/stockpiles/OrganicMatLookup.h b/plugins/stockpiles/OrganicMatLookup.h index f585ef2de4c..15f9838df26 100644 --- a/plugins/stockpiles/OrganicMatLookup.h +++ b/plugins/stockpiles/OrganicMatLookup.h @@ -31,7 +31,7 @@ class OrganicMatLookup { }; static void food_mat_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat); - static std::string food_token_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx); + static std::string food_token_by_idx(DFHack::color_ostream& out, const FoodMat& food_mat); static size_t food_max_size(df::enums::organic_mat_category::organic_mat_category mat_category); static void food_build_map(); diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 1d11db41e57..afd0a6dbc8c 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -449,7 +449,9 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v all = false; continue; } - string token = OrganicMatLookup::food_token_by_idx(out, cat, i); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, i, food_mat); + string token = OrganicMatLookup::food_token_by_idx(out, food_mat); if (token.empty()) { DEBUG(log, out).print("food mat invalid :(\n"); continue; @@ -460,6 +462,24 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v return all; } +static string get_filter_string(color_ostream& out, const OrganicMatLookup::FoodMat& food_mat) { + auto str = OrganicMatLookup::food_token_by_idx(out, food_mat); + if (auto plant = food_mat.material.plant) { + if (plant->flags.is_set(df::plant_raw_flags::DRINK)) + str += "/brewable"; + if (plant->flags.is_set(df::plant_raw_flags::MILL)) + str += "/millable"; + if (auto mat = food_mat.material.material) { + if (mat->flags.is_set(df::material_flags::STRUCTURAL_PLANT_MAT) && + (plant->flags.is_set(df::plant_raw_flags::THREAD) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_VIAL) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_BARREL))) + str += "/processable"; + } + } + return str; +} + static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, bool all, char val, const vector& filters, FuncReadImport read_value, size_t list_size, vector& pile_list, organic_mat_category::organic_mat_category cat) { @@ -467,8 +487,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, pile_list.resize(num_elems, '\0'); if (all) { for (size_t idx = 0; idx < num_elems; ++idx) { - string token = OrganicMatLookup::food_token_by_idx(out, cat, idx); - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } return; } @@ -480,7 +501,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, WARN(log, out).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); continue; } - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } } From 803cddce9ff40e5b8d17568d719a7bc377f0bfd4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:53:22 +0000 Subject: [PATCH 070/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 795cba2474b..58909686480 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 795cba2474b928771cf59d6fc3690f4f5622bf99 +Subproject commit 589096864804e5d6758c555851ef12228003f51c From ef3c9785c560057e26bf6d908c2a2411435c1f55 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 08:04:05 -0800 Subject: [PATCH 071/919] changelog editing pass --- docs/changelog.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9586c36d6a5..aba2146d483 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,9 +54,10 @@ Template for new versions: ## New Tools ## New Features -- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit unit +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S +- `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units ## Fixes - `createitem`: output items will now end up at look cursor if active @@ -66,10 +67,10 @@ Template for new versions: ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter -- `stockpiles`: add property filters for brewable, millable, and processable organic materials +- `stockpiles`: add property filters for brewable, millable, and processable (e.g. at a Farmer's workshop) organic materials ## Documentation -- `stonesense-art-guide`: new guide for making sprite art for Stonesense +- `stonesense-art-guide`: guide for making sprite art for Stonesense ## API - ``Military::removeFromSquad``: removes unit from any squad assignments @@ -77,7 +78,7 @@ Template for new versions: ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` -- ``gui.dwarfmode`` module: ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` now operate on the adventure mode look cursor, if active. Clearing the cursor sets it to the active adventurer's position. +- ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons ## Removed From 07b43723b7bc39a43faebae3b9faa555b529d34c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:30:55 +0000 Subject: [PATCH 072/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 9bf00722a73..13cd875ce99 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9bf00722a739e6c00fd2a2c19ac7208e6f337ed3 +Subproject commit 13cd875ce99fec984b7d654d68c31ef265d05bdd diff --git a/plugins/stonesense b/plugins/stonesense index 9bc40021b36..760d36ec92d 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9bc40021b3673f59393e4c718949b5575599f01f +Subproject commit 760d36ec92d8d250ea6a08e588ae8dcc1bbd2367 diff --git a/scripts b/scripts index 58909686480..273494caa52 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 589096864804e5d6758c555851ef12228003f51c +Subproject commit 273494caa52ddff73f8eeaed3c037166c70796ca From 537727be276ffd1cc4ac2fe99be245113594ddb4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 11 Feb 2025 07:16:50 +0000 Subject: [PATCH 073/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 760d36ec92d..17c6be1669b 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 760d36ec92d8d250ea6a08e588ae8dcc1bbd2367 +Subproject commit 17c6be1669b2bce22864892892722c625e1d66e3 diff --git a/scripts b/scripts index 273494caa52..70c54c4e48c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 273494caa52ddff73f8eeaed3c037166c70796ca +Subproject commit 70c54c4e48c8005f2a07b68646e1f5b3855a0373 From 3f817a04ef07698c8b0b700466ead859a25555f2 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:03:57 +0800 Subject: [PATCH 074/919] Add missing history event --- library/modules/Military.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index cbb42955624..356c6066812 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -14,6 +14,7 @@ #include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" +#include "df/history_event_remove_hf_entity_linkst.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/plotinfost.h" @@ -334,6 +335,8 @@ static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squ former_squad->link_strength = 100; hf->entity_links.push_back(former_squad); + + // Unassigning a normal soldier does not seem to generate an event record. } static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) @@ -343,6 +346,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ return; int32_t assignment_id = -1; + int32_t pos_id = -1; for (auto& np : nps) { if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) @@ -352,6 +356,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ np.assignment->histfig2 = -1; assignment_id = np.assignment->id; + pos_id = np.position->id; break; } @@ -387,8 +392,21 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->start_year = start_year; former_pos->end_year = *df::global::cur_year; former_pos->link_strength = 100; - + hf->entity_links.push_back(former_pos); + + int32_t event_id = *df::global::hist_event_next_id; + auto former_pos_event = df::allocate(); + former_pos_event->year = *df::global::cur_year; + former_pos_event->seconds = *df::global::cur_year_tick; + former_pos_event->id = event_id; + former_pos_event->civ = squad->entity_id; + former_pos_event->histfig = hf->id; + former_pos_event->position_id = pos_id; + former_pos_event->link_type = df::histfig_entity_link_type::POSITION; + + df::global::world->history.events.push_back(former_pos_event); + *df::global::hist_event_next_id = event_id + 1; } bool Military::removeFromSquad(int32_t unit_id) From 25196c0281487f35b606a197676e94cd95ea6bc9 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:07:38 +0800 Subject: [PATCH 075/919] Trim whitespace --- library/modules/Military.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 356c6066812..2f44e048241 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -392,7 +392,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos->start_year = start_year; former_pos->end_year = *df::global::cur_year; former_pos->link_strength = 100; - + hf->entity_links.push_back(former_pos); int32_t event_id = *df::global::hist_event_next_id; @@ -404,7 +404,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos_event->histfig = hf->id; former_pos_event->position_id = pos_id; former_pos_event->link_type = df::histfig_entity_link_type::POSITION; - + df::global::world->history.events.push_back(former_pos_event); *df::global::hist_event_next_id = event_id + 1; } From 72819ad3a290ee399d8f86462793e4370c5b2817 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:16:14 +0800 Subject: [PATCH 076/919] Update docs/dev/Lua API.rst --- docs/dev/Lua API.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 66516642a96..bbab5a6c38e 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1991,8 +1991,9 @@ Military module Removes a unit from its squad. Unsets the unit's military information (i.e., ``unit.military.squad_id`` and ``unit.military.squad_pos``), the squad's position information (i.e., - ``squad.positions[squad_pos].occupant``), and modifies the unit's entity links - to indicate former squad membership or command. + ``squad.positions[squad_pos].occupant``), modifies the unit's entity links + to indicate former squad membership or command, and creates a corresponding + world history event. Items module ------------ From 4aa4fe9468a6f4175cde36861caa520df94c5d12 Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Wed, 12 Feb 2025 00:48:16 +0800 Subject: [PATCH 077/919] Change increment style --- docs/about/Authors.rst | 1 + library/modules/Military.cpp | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index bfb8aa02a84..d806d92aaa4 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -158,6 +158,7 @@ NotRexButCaesar NotRexButCaesar Nuno Fernandes UnknowableCoder nuvu vallode Omniclasm +Ong Ying Gao ong-yinggao98 oorzkws oorzkws OwnageIsMagic OwnageIsMagic palenerd dlmarquis diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 2f44e048241..a5946acca5d 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -395,7 +395,7 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ hf->entity_links.push_back(former_pos); - int32_t event_id = *df::global::hist_event_next_id; + int32_t event_id = (*df::global::hist_event_next_id)++; auto former_pos_event = df::allocate(); former_pos_event->year = *df::global::cur_year; former_pos_event->seconds = *df::global::cur_year_tick; @@ -406,7 +406,6 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ former_pos_event->link_type = df::histfig_entity_link_type::POSITION; df::global::world->history.events.push_back(former_pos_event); - *df::global::hist_event_next_id = event_id + 1; } bool Military::removeFromSquad(int32_t unit_id) From 3752608ebd00c426aaad634cbaf7ac81ee9d2ea9 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:34:09 -0600 Subject: [PATCH 078/919] Vastly Improved Slider --- library/lua/gui/widgets/range_slider.lua | 35 ++++-- library/lua/gui/widgets/slider.lua | 138 +++-------------------- 2 files changed, 38 insertions(+), 135 deletions(-) diff --git a/library/lua/gui/widgets/range_slider.lua b/library/lua/gui/widgets/range_slider.lua index bd1f9cb3de4..f55f0e00f05 100644 --- a/library/lua/gui/widgets/range_slider.lua +++ b/library/lua/gui/widgets/range_slider.lua @@ -29,6 +29,7 @@ RangeSlider.ATTRS{ get_right_idx_fn=DEFAULT_NIL, on_left_change=DEFAULT_NIL, on_right_change=DEFAULT_NIL, + is_single=DEFAULT_NIL } function RangeSlider:preinit(init_table) @@ -43,7 +44,8 @@ function RangeSlider:init() end local function rangeslider_get_width_per_idx(self) - return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) + local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 + return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) end function RangeSlider:onInput(keys) @@ -142,16 +144,27 @@ function RangeSlider:onRenderBody(dc, rect) end dc:char(nil, SLIDER_TRACK) dc:char(nil, SLIDER_RIGHT_END) - -- draw tabs - dc:seek(width_per_idx*(left_idx-1)) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - dc:seek(width_per_idx*(right_idx-1)+4) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging + + -- Draw tab(s) + if self.is_single then + -- Single slider: Draw one centered tab + dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + else + -- Dual slider: Draw left and right tabs separately + dc:seek(width_per_idx * (left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + end + + -- Manage dragging if self.is_dragging_target then rangeslider_do_drag(self, width_per_idx) end diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua index 92263d2924f..23bf6f62029 100644 --- a/library/lua/gui/widgets/slider.lua +++ b/library/lua/gui/widgets/slider.lua @@ -1,131 +1,21 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse - --------------------------------- --- Slider --------------------------------- - ----@class widgets.Slider.attrs: widgets.Widget.attrs ----@field num_stops integer ----@field get_idx_fn? function ----@field on_change? fun(index: integer) - ----@class widgets.Slider.attrs.partial: widgets.Slider.attrs - ----@class widgets.Slider.initTable: widgets.Slider.attrs ----@field num_stops integer - ----@class widgets.Slider: widgets.Widget, widgets.Slider.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) ----@overload fun(init_table: widgets.Slider.initTable): self -Slider = defclass(Slider, Widget) -Slider.ATTRS{ - num_stops=DEFAULT_NIL, - get_idx_fn=DEFAULT_NIL, - on_change=DEFAULT_NIL, +-- slider.lua +local RangeSlider = require('gui.widgets.range_slider') + +local Slider = defclass(Slider, RangeSlider) +Slider.ATTRS { + get_idx_fn = DEFAULT_NIL, -- Function to get the current index + on_change = DEFAULT_NIL, -- Function to handle index change + is_single = true } -function Slider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - function Slider:init() - if self.num_stops < 2 then error('too few Slider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function Slider_get_width_per_idx(self) - return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) -end - -function Slider:onInput(keys) - if not keys._MOUSE_L then return false end - local x = self:getMousePos() - if not x then return false end - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - local left_pos = width_per_idx*(left_idx-1) - local right_pos = width_per_idx*(left_idx-1) + 4 - if x < left_pos then - self.on_change(self.get_idx_fn() - 1) - else - self.is_dragging_target = 'both' - self.is_dragging_idx = x - right_pos - end - return true -end - -local function Slider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - if self.is_dragging_target == 'both' then - if new_idx > self.num_stops then - return - end - end - if new_idx and new_idx ~= self.get_idx_fn() then - self.on_change(new_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function Slider:onRenderBody(dc, rect) - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx ~= stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif left_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if left_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - -- draw tab - dc:seek(width_per_idx*(left_idx-1)+2) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging - if self.is_dragging_target then - Slider_do_drag(self, width_per_idx) + self.get_left_idx_fn = self.get_idx_fn + self.get_right_idx_fn = self.get_idx_fn + self.on_left_change = function(index) + if self.on_change then self.on_change(index) end end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil + self.on_right_change = function(index) + if self.on_change then self.on_change(index) end end end From 2d33265846786767c942e26789752c5623aac80f Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:38:27 -0600 Subject: [PATCH 079/919] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index aba2146d483..f68ca870d2d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,6 +79,7 @@ Template for new versions: - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons +- ``widgets.Slider``: new mouse-controlled single-headed slider widget ## Removed From 2a00e78d7addb83cf400bcca9e8c37edf4d48946 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 12 Feb 2025 07:16:57 +0000 Subject: [PATCH 080/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 70c54c4e48c..a76a39a2048 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 70c54c4e48c8005f2a07b68646e1f5b3855a0373 +Subproject commit a76a39a2048ea47607794c1ba2f5f0807f5d7a92 From cfb2cac22246972bc6e3ec703d65e03e3af3db1b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 12 Feb 2025 14:15:33 -0500 Subject: [PATCH 081/919] Fix regression in checkFreeTiles --- library/include/modules/Buildings.h | 2 +- library/modules/Buildings.cpp | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index 80fe4433965..c323958e17d 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -111,7 +111,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building *bld, + df::building *bld = nullptr, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 672464ceba0..1135fb5307f 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -735,10 +735,7 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, bool allow_wall, bool allow_flow) { - CHECK_NULL_POINTER(bld); - bool found_any = false; - auto & room = bld->room; for (int dx = 0; dx < size.x; dx++) { @@ -748,9 +745,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (room.extents) + if (bld && bld->room.extents) { - etile = getExtentTile(room, tile); + etile = getExtentTile(bld->room, tile); if (!etile || !*etile) continue; } @@ -784,13 +781,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!create_ext) + if (!bld || !create_ext) return false; - if (!room.extents) + if (!bld->room.extents) { - init_extents(room, pos, size); - etile = getExtentTile(room, tile); + init_extents(bld->room, pos, size); + etile = getExtentTile(bld->room, tile); } if (!etile) From 52a6b891104fff68ac4d2e08f6de7b66e5d81a9f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 12 Feb 2025 14:57:14 -0600 Subject: [PATCH 082/919] do it right this time --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 345d8967db8..1e61d7f5f36 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 345d8967db88ff33283111fdfecd0eaebbf09584 +Subproject commit 1e61d7f5f36a0bc937985c5b859d8ee152789bde From ba448949ffac1d933ce4089871f19ce0f1c54ee3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Feb 2025 18:44:14 -0800 Subject: [PATCH 083/919] update docs for checkFreeTiles --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20e65538d2b..20d11103bcb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -74,11 +74,13 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments +- ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons +- ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bbab5a6c38e..f8108e8b2ca 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2477,14 +2477,15 @@ General using width and height for flexible dimensions. Returns *is_flexible, width, height, center_x, center_y*. -* ``dfhack.buildings.checkFreeTiles(pos,size[,extents[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` - - Checks if the rectangle defined by ``pos`` and ``size``, and possibly extents, - can be used for placing a building. If ``change_extents`` is true, bad tiles - are removed from extents. If ``allow_occupied``, the occupancy test is skipped. - Set ``allow_wall`` to true if the building is unhindered by walls (such as an - activity zone). Set ``allow_flow`` to true if the building can be built even - if there is deep water or any magma on the tile (such as abstract buildings). +* ``dfhack.buildings.checkFreeTiles(pos,size[,bld[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` + + Checks if the rectangle defined by ``pos`` and ``size``, and possibly the + extents associated with bld, can be used for placing a building. If + ``change_extents`` is true, bad tiles are removed from extents. If + ``allow_occupied``, the occupancy test is skipped. Set ``allow_wall`` to true + if the building is unhindered by walls (such as an activity zone). Set + ``allow_flow`` to true if the building can be built even if there is deep + water or any magma on the tile (such as abstract buildings). * ``dfhack.buildings.countExtentTiles(extents,defval)`` From 66c2af8677505069471d6d60aa3c979cbecaa214 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 13 Feb 2025 15:07:48 -0600 Subject: [PATCH 084/919] Remove the segment size and overlay stuff --- docs/plugins/stonesense.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index f0b9c3f9ce1..57eccee1a2c 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -16,11 +16,7 @@ Usage The viewer window has read-only access to the game, and can follow the game view or be moved independently. Configuration for stonesense can be set in the -``dfhack-config/stonesense/init.txt`` file in your DF game directory. If the window refresh -rate is too low, change ``SEGMENTSIZE_Z`` to ``2`` in this file, and if you are -unable to see the edges of the map with the overlay active, try decreasing the -value for ``SEGMENTSIZE_X`` and ``SEGMENTSIZE_Y`` -- normal values are ``50`` to ``80``, depending -on your screen resolution. +``dfhack-config/stonesense/init.txt`` file in your DF game directory. .. figure:: ../images/stonesense-roadtruss.jpg :align: center From f944852c209cc0bc46b73f09422fafa80b772eb6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 14 Feb 2025 20:01:41 -0800 Subject: [PATCH 085/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index a76a39a2048..4af65c03bf6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a76a39a2048ea47607794c1ba2f5f0807f5d7a92 +Subproject commit 4af65c03bf6f0028e5f9e03af125bee0266688b0 From 02652911d3e4e6c7b75c0d22b62c1092f8c213bb Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 12:51:57 +0100 Subject: [PATCH 086/919] make it possible to set all settings Also: * print settings in key order * use string keys to simplify using json for persistence * change default tooltip-follow-blink-milliseconds to 3000ms --- plugins/lua/spectate.lua | 135 +++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 40 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 96fc0770c44..647f022bf20 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -27,7 +27,7 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, - ['tooltip-follow-blink-milliseconds']=2000, + ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-job']=true, ['tooltip-follow-job-shortenings'] = { ["Store item in stockpile"] = "Store item", @@ -35,39 +35,36 @@ local function get_default_state() ['tooltip-follow-name']=false, ['tooltip-follow-stress']=true, ['tooltip-follow-stress-levels']={ - [0] = - true, -- Miserable - true, - false, - false, - false, - true, - true, -- Ecstatic + ["0"] = true, -- Miserable + ["1"] = true, + ["2"] = false, + ["3"] = false, + ["4"] = false, + ["5"] = true, + ["6"] = true, -- Ecstatic }, ['tooltip-hover-job']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, ['tooltip-hover-stress-levels']={ - [0] = - true, -- Miserable - true, - false, - false, - false, - true, - true, -- Ecstatic + ["0"] = true, -- Miserable + ["1"] = true, + ["2"] = false, + ["3"] = false, + ["4"] = false, + ["5"] = true, + ["6"] = true, -- Ecstatic }, ['tooltip-stress-levels']={ -- keep in mind, the text will look differently with game's font -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE - [0] = - {text = "=C", pen = COLOR_RED, name = "Miserable"}, - {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, - {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, - {text = ":]", pen = COLOR_GREY, name = "Content"}, - {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, - {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, - {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, + ["0"] = {text = "=C", pen = COLOR_RED, name = "Miserable"}, + ["1"] = {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, + ["2"] = {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, + ["3"] = {text = ":]", pen = COLOR_GREY, name = "Content"}, + ["4"] = {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, + ["5"] = {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, + ["6"] = {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, } } end @@ -116,13 +113,39 @@ end ----------------------------- -- commandline interface +local function pairsByKeys(t, f) + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function () -- iterator function + i = i + 1 + if a[i] == nil then return nil + else return a[i], t[a[i]] + end + end + return iter +end + +-- no recursion protection, but it shouldn't be needed for a config... +local function print_table(t, indent) + indent = indent or '' + for key, value in pairsByKeys(t) do + if type(value) == 'table' then + print(indent .. key .. ':') + print_table(value, indent .. ' ') + else + print(indent .. key .. ': ' .. tostring(value)) + end + end +end + local function print_status() print('spectate is:', isEnabled() and 'enabled' or 'disabled') print() print('settings:') - for key, value in pairs(config) do - print(' ' .. key .. ': ' .. tostring(value)) - end + + print_table(config, ' ') end local function do_toggle() @@ -133,23 +156,54 @@ local function do_toggle() end end -local function set_setting(key, value) +local function set_setting(args) + local key = table.remove(args, 1) if config[key] == nil then qerror('unknown setting: ' .. key) end - if key == 'follow-seconds' then - value = argparse.positiveInt(value, 'follow-seconds') - else - value = argparse.boolean(value, key) + local n = #args + if n == 0 then + qerror('missing value') end - config[key] = value - save_state() - if not key:startswith(lua_only_settings_prefix) then - if type(value) == 'boolean' then - value = value and 1 or 0 + + if n == 1 then + local value = args[1] + if key == 'follow-seconds' then + value = argparse.positiveInt(value, 'follow-seconds') + elseif key == 'tooltip-follow-blink-milliseconds' then + value = argparse.nonnegativeInt(value, 'tooltip-follow-blink-milliseconds') + else + value = argparse.boolean(value, key) end - spectate_setSetting(key, value) + + config[key] = value + + if not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(key, value) + end + else + local t = config[key] + for i = 1, n - 2 do + t = t[args[i]] + end + local k = args[n-1] + local v = args[n] + if key ~= 'tooltip-follow-job-shortenings' then + -- user should be able to add new shortenings, but not other things + if t[k] == nil then + table.remove(args) + qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) + elseif key:endswith('-stress-levels') then + v = argparse.boolean(v, key .. '/' .. k) + end + end + t[k] = v end + + save_state() end local function set_overlay(value) @@ -164,7 +218,7 @@ function parse_commandline(args) elseif command == 'toggle' then do_toggle() elseif command == 'set' then - set_setting(args[1], args[2]) + set_setting(args) elseif command == 'overlay' then set_overlay(args[1]) else @@ -180,6 +234,7 @@ end local function GetUnitStress(unit, stress_levels) local stressCat = dfhack.units.getStressCategory(unit) if stressCat > 6 then stressCat = 6 end + stressCat = tostring(stressCat) if not stress_levels[stressCat] then return end local level_cfg = config['tooltip-stress-levels'][stressCat] From 82ce1be6c97dabceefd9ac10aa2d1debbb0b73bc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 12:59:33 +0100 Subject: [PATCH 087/919] add options to enable/disable tooltips at once --- plugins/lua/spectate.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 647f022bf20..a1d23c1f0d4 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -27,6 +27,7 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, + ['tooltip-follow']=true, ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-job']=true, ['tooltip-follow-job-shortenings'] = { @@ -43,6 +44,7 @@ local function get_default_state() ["5"] = true, ["6"] = true, -- Ecstatic }, + ['tooltip-hover']=true, ['tooltip-hover-job']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, @@ -380,7 +382,7 @@ local function GetString(tokens) end function TooltipOverlay:render_unit_banners(dc) - if not AnyFollowOptionOn() then return end + if not (config['tooltip-follow'] and AnyFollowOptionOn()) then return end local blink_duration = config['tooltip-follow-blink-milliseconds'] if blink_duration > 0 and not gui.blink_visible(blink_duration) then @@ -525,7 +527,7 @@ local function AnyHoverOptionOn() end function MouseTooltip:render(dc) - if not AnyHoverOptionOn() then return end + if not (config['tooltip-hover'] and AnyHoverOptionOn()) then return end local x, y = dfhack.screen.getMousePos() if not x then return end From 9f103d1bc4c477c822caaf5fa265ecb7af455aaa Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 16:25:47 +0100 Subject: [PATCH 088/919] add documentation for new options Also, make it possible to set `tooltip-stress-levels`. --- docs/plugins/spectate.rst | 31 +++++++++++++++++++++++++++++++ plugins/lua/spectate.lua | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index b4d913d9e0b..c93e2140b64 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -103,10 +103,22 @@ Settings Toggle whether to prefer following (non-siege) units that have newly arrived on the map. +``tooltip-follow`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + tooltip. + +``tooltip-follow-blink-milliseconds`` (default: 3000) + If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration + in milliseconds. Set to 0 to always show. + ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. +``tooltip-follow-job-shortenings`` + If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to + shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the tooltip. @@ -115,6 +127,15 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the tooltip. +``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + specific happiness level (stress) of the dwarf in the tooltip. F.e. + ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. + +``tooltip-hover`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + hover panel. + ``tooltip-hover-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. @@ -127,6 +148,16 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the happiness level (stress) of the dwarf in the hover panel. +``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + specific happiness level (stress) of the dwarf in the hover panel. F.e. + ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. + +``tooltip-stress-levels`` + Controls how happiness levels (stress) are displayed (emoticon and color). + F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for + Ecstatic dwarves to ``XD``. + Overlays -------- diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index a1d23c1f0d4..2f8ade03cf8 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -198,7 +198,7 @@ local function set_setting(args) if t[k] == nil then table.remove(args) qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - elseif key:endswith('-stress-levels') then + elseif key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then v = argparse.boolean(v, key .. '/' .. k) end end From 1d79fd05e4290584c9deee349a3ecbf2ca1f80f2 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Feb 2025 16:28:44 +0100 Subject: [PATCH 089/919] remove tabs --- docs/plugins/spectate.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index c93e2140b64..8cd40276b66 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -104,20 +104,20 @@ Settings arrived on the map. ``tooltip-follow`` (default: enabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the tooltip. ``tooltip-follow-blink-milliseconds`` (default: 3000) - If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration - in milliseconds. Set to 0 to always show. + If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration + in milliseconds. Set to 0 to always show. ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. ``tooltip-follow-job-shortenings`` - If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to - shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to + shorten some job names, f.e. "Store item in stockpile" becomes "Store item". ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -128,12 +128,12 @@ Settings happiness level (stress) of the dwarf in the tooltip. ``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the tooltip. F.e. - ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. ``tooltip-hover`` (default: enabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the hover panel. ``tooltip-hover-job`` (default: enabled) @@ -149,14 +149,14 @@ Settings happiness level (stress) of the dwarf in the hover panel. ``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) - If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the hover panel. F.e. - ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. ``tooltip-stress-levels`` - Controls how happiness levels (stress) are displayed (emoticon and color). - F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for - Ecstatic dwarves to ``XD``. + Controls how happiness levels (stress) are displayed (emoticon and color). + F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for + Ecstatic dwarves to ``XD``. Overlays -------- From 4f016d4874a7daf21eac415741eba433ca3393c6 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 19:43:58 -0500 Subject: [PATCH 090/919] Binpatches to remove the vanilla dimensions tooltip --- CMakeLists.txt | 1 + patches/CMakeLists.txt | 3 +++ .../disable-vanilla-dimensions.dif | 6 ++++++ .../v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif | 7 +++++++ .../disable-vanilla-dimensions.dif | 9 +++++++++ .../disable-vanilla-dimensions.dif | 6 ++++++ .../v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif | 6 ++++++ .../v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif | 6 ++++++ 8 files changed, 44 insertions(+) create mode 100644 patches/CMakeLists.txt create mode 100644 patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif create mode 100644 patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c58b36bf23..2d73cd996bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,6 +451,7 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) diff --git a/patches/CMakeLists.txt b/patches/CMakeLists.txt new file mode 100644 index 00000000000..37ee7c94405 --- /dev/null +++ b/patches/CMakeLists.txt @@ -0,0 +1,3 @@ +install(DIRECTORY ${patchdirectories} + DESTINATION ${DFHACK_DATA_DESTINATION}/patches +) diff --git a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif new file mode 100644 index 00000000000..f8c95a2af6d --- /dev/null +++ b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +011ba9eb: e8 90 +011ba9ec: c0 90 +011ba9ed: 4a 90 +011ba9ee: e6 90 +011ba9ef: fe 90 diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif new file mode 100644 index 00000000000..0c8cd61ad64 --- /dev/null +++ b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif @@ -0,0 +1,7 @@ +Dwarf_Fortress +011c605b: e8 90 +011c605c: 50 90 +011c605d: a5 90 +011c605e: e5 90 +011c605f: fe 90 + diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif new file mode 100644 index 00000000000..59af3301c79 --- /dev/null +++ b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif @@ -0,0 +1,9 @@ +---8<--- +This difference file is created by The Interactive Disassembler + +Dwarf_Fortress +011C9C3B: e8 90 +011c9c3c: 80 90 +011c9c3d: 6a 90 +011c9c3e: e5 90 +011c9c3f: fe 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif new file mode 100644 index 00000000000..b097959e804 --- /dev/null +++ b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +377d5d: e8 90 +377d5e: ee 90 +377d5f: 93 90 +377d60: 68 90 +377d61: 00 90 \ No newline at end of file diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif new file mode 100644 index 00000000000..b0eea337933 --- /dev/null +++ b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +37af8d: e8 90 +37af8e: 1e 90 +37af8f: b7 90 +37af90: 68 90 +37af91: 00 90 \ No newline at end of file diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif new file mode 100644 index 00000000000..22fc2673690 --- /dev/null +++ b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif @@ -0,0 +1,6 @@ +Dwarf_Fortress +37d77d: e8 90 +37d77e: 6e 90 +37d77f: bb 90 +37d780: 68 90 +37d781: 00 90 \ No newline at end of file From db459e41da2489c8383e157ebac25334fec51c9c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 19:47:46 -0500 Subject: [PATCH 091/919] Changelog for new binpatches --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20d11103bcb..700a776ed8e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units +- disable-vanilla-dimensions: new binpatch to disable the vanilla dimensions tooltip ## Fixes - `createitem`: output items will now end up at look cursor if active From fb9fb5f235db45fe1dd0252f680f5cf0fc08f2db Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 19:51:48 -0500 Subject: [PATCH 092/919] Cleanup line endings and formatting --- patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif | 1 - .../v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif | 5 +---- .../v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif | 2 +- patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif | 2 +- patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif index 0c8cd61ad64..17845768819 100644 --- a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif @@ -4,4 +4,3 @@ Dwarf_Fortress 011c605d: a5 90 011c605e: e5 90 011c605f: fe 90 - diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif index 59af3301c79..142e53e4fa8 100644 --- a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif @@ -1,8 +1,5 @@ ----8<--- -This difference file is created by The Interactive Disassembler - Dwarf_Fortress -011C9C3B: e8 90 +011c9c3b: e8 90 011c9c3c: 80 90 011c9c3d: 6a 90 011c9c3e: e5 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif index b097959e804..2cd08dfd0e5 100644 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif @@ -3,4 +3,4 @@ Dwarf_Fortress 377d5e: ee 90 377d5f: 93 90 377d60: 68 90 -377d61: 00 90 \ No newline at end of file +377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif index b0eea337933..f1d36354d35 100644 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif @@ -3,4 +3,4 @@ Dwarf_Fortress 37af8e: 1e 90 37af8f: b7 90 37af90: 68 90 -37af91: 00 90 \ No newline at end of file +37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif index 22fc2673690..82d103ad185 100644 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif @@ -3,4 +3,4 @@ Dwarf_Fortress 37d77e: 6e 90 37d77f: bb 90 37d780: 68 90 -37d781: 00 90 \ No newline at end of file +37d781: 00 90 From c64f8969b0e3323f484462cf9d4a38311e0c6d19 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 20:07:31 -0500 Subject: [PATCH 093/919] Fix lineendings for real this time --- .../disable-vanilla-dimensions.dif | 12 ++++++------ .../disable-vanilla-dimensions.dif | 12 ++++++------ .../disable-vanilla-dimensions.dif | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif index 2cd08dfd0e5..2f5cf9e0ae0 100644 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif @@ -1,6 +1,6 @@ -Dwarf_Fortress -377d5d: e8 90 -377d5e: ee 90 -377d5f: 93 90 -377d60: 68 90 -377d61: 00 90 +Dwarf_Fortress +377d5d: e8 90 +377d5e: ee 90 +377d5f: 93 90 +377d60: 68 90 +377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif index f1d36354d35..e2546507617 100644 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif @@ -1,6 +1,6 @@ -Dwarf_Fortress -37af8d: e8 90 -37af8e: 1e 90 -37af8f: b7 90 -37af90: 68 90 -37af91: 00 90 +Dwarf_Fortress +37af8d: e8 90 +37af8e: 1e 90 +37af8f: b7 90 +37af90: 68 90 +37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif index 82d103ad185..44dd5ebb81d 100644 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif @@ -1,6 +1,6 @@ -Dwarf_Fortress -37d77d: e8 90 -37d77e: 6e 90 -37d77f: bb 90 -37d780: 68 90 -37d781: 00 90 +Dwarf_Fortress +37d77d: e8 90 +37d77e: 6e 90 +37d77f: bb 90 +37d780: 68 90 +37d781: 00 90 From 87bfb088399bc07a8fd1c52a744e1aec1e73fe68 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 20:09:56 -0500 Subject: [PATCH 094/919] Improve formatting of changelog entry Co-authored-by: Myk --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 700a776ed8e..9ee8fdb5134 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,7 @@ Template for new versions: - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units -- disable-vanilla-dimensions: new binpatch to disable the vanilla dimensions tooltip +- ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip ## Fixes - `createitem`: output items will now end up at look cursor if active From db1039718d5c4a59c9b64fccf3add2e766d416c8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Feb 2025 21:31:47 -0500 Subject: [PATCH 095/919] Correct path to not create "hack/patches/patches" --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d73cd996bd..e133c87acb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,7 +451,7 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches DESTINATION ${DFHACK_DATA_DESTINATION}/patches) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) From c1380835b70397a5eb05bab9a95a4b6872de94cc Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 16 Feb 2025 21:55:37 +0000 Subject: [PATCH 096/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 17c6be1669b..f312d353240 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 17c6be1669b2bce22864892892722c625e1d66e3 +Subproject commit f312d353240f45922084f901ade84f7479fde53d diff --git a/scripts b/scripts index 4af65c03bf6..654720a2d60 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4af65c03bf6f0028e5f9e03af125bee0266688b0 +Subproject commit 654720a2d608dfd26a9596182a70ca1ad1ea3053 From bed64583b05293452af78dc3c335b2225ad91666 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 16 Feb 2025 19:05:21 -0600 Subject: [PATCH 097/919] fix bad logic in windows console handler prompt refresh logic would underflow and infinite-loop if window size was smaller than the prompt size replaced with direct math instead of while loops --- library/Console-windows.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 85934b2f371..83ed0f64ee5 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -36,6 +36,7 @@ POSSIBILITY OF SUCH DAMAGE. */ +#define NOMINMAX #include #include #include @@ -232,16 +233,13 @@ namespace DFHack size_t len = raw_buffer.size(); int cooked_cursor = raw_cursor; - while ((plen + cooked_cursor) >= cols) - { - buf++; - len--; - cooked_cursor--; - } - while (plen + len > cols) - { - len--; - } + int adj = std::min(plen + cooked_cursor - cols, len); + buf += adj; + len -= adj; + cooked_cursor -= adj; + + int adj2 = std::min(plen + len - cols, len); + len -= adj2; CONSOLE_SCREEN_BUFFER_INFO inf = { 0 }; GetConsoleScreenBufferInfo(console_out, &inf); From d7100be8f73dbc5e1255d2e585b574f59e84f15d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 16 Feb 2025 19:07:03 -0600 Subject: [PATCH 098/919] add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 20d11103bcb..ad953a7e81d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: - `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units ## Fixes +- Windows console: fix possible hang if the console returns a too-small window width (for any reason) - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself From 6011cb375e769e3bb317ec0a2f7f98500fe63339 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 17 Feb 2025 20:51:22 +0100 Subject: [PATCH 099/919] Update docs/plugins/spectate.rst Co-authored-by: Myk --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 8cd40276b66..8eb38729e9c 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -105,7 +105,7 @@ Settings ``tooltip-follow`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the - tooltip. + tooltips that follow onscreen dwarves around the map. ``tooltip-follow-blink-milliseconds`` (default: 3000) If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration From 594e1debb8eae45be4b7939d6b77bbc8417e2c5e Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 17 Feb 2025 20:51:37 +0100 Subject: [PATCH 100/919] Update docs/plugins/spectate.rst Co-authored-by: Myk --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 8eb38729e9c..5a9ba0b2e54 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -134,7 +134,7 @@ Settings ``tooltip-hover`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the - hover panel. + hover popup panel when your mouse cursor is over a unit. ``tooltip-hover-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the From 3093a3f59d5796e7e1fd6a7bc4d173c770fa0185 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 17 Feb 2025 22:10:34 +0100 Subject: [PATCH 101/919] expand documentation --- docs/plugins/spectate.rst | 54 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 5a9ba0b2e54..69c626d56c7 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -63,6 +63,9 @@ Examples Show informative tooltips that follow each unit on the map. Note that this can be enabled independently of `spectate` itself. +``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store item"`` + Set a (see ``tooltip-follow-job-shortenings`` for details). + Settings -------- @@ -108,8 +111,8 @@ Settings tooltips that follow onscreen dwarves around the map. ``tooltip-follow-blink-milliseconds`` (default: 3000) - If the ``spectate.tooltip`` overlay is enabled, set tooltip's blink duration - in milliseconds. Set to 0 to always show. + If the ``spectate.tooltip`` overlay is enabled, set the tooltip's blink + duration in milliseconds. Set to 0 to always show. ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -118,6 +121,7 @@ Settings ``tooltip-follow-job-shortenings`` If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + See examples for an example. ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -130,7 +134,8 @@ Settings ``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the tooltip. F.e. - ``tooltip-follow-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-follow-stress-levels 2 true`` would show the Displeased emoticon. + See ``tooltip-stress-levels`` for details. ``tooltip-hover`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -151,12 +156,53 @@ Settings ``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the hover panel. F.e. - ``tooltip-hover-stress-levels 2 true`` would show Displeased emoticon. + ``tooltip-hover-stress-levels 2 true`` would show the Displeased emoticon. + See ``tooltip-stress-levels`` for details. ``tooltip-stress-levels`` Controls how happiness levels (stress) are displayed (emoticon and color). F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for Ecstatic dwarves to ``XD``. + Default values are: + +.. list-table:: + :widths: 25 25 25 25 + :header-rows: 1 + + * - Level + - name + - text + - pen + * - 0 + - Miserable + - =C + - COLOR_RED + * - 1 + - Unhappy + - :C + - COLOR_LIGHTRED + * - 2 + - Displeased + - :( + - COLOR_YELLOW + * - 3 + - Content + - :] + - COLOR_GREY + * - 4 + - Pleased + - :) + - COLOR_GREEN + * - 5 + - Happy + - :D + - COLOR_LIGHTGREEN + * - 6 + - Ecstatic + - =D + - COLOR_LIGHTCYAN + +Keep in mind that the text will look different with the game's font. Overlays -------- From 2c87cf82d393c4c8e01eaa4617e496508348382e Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 17 Feb 2025 18:56:50 -0800 Subject: [PATCH 102/919] doc clarifications --- docs/plugins/spectate.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 69c626d56c7..65d7cefe281 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -63,8 +63,10 @@ Examples Show informative tooltips that follow each unit on the map. Note that this can be enabled independently of `spectate` itself. -``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store item"`` - Set a (see ``tooltip-follow-job-shortenings`` for details). +``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store"`` + Abbreviate the names of "Store item in stockpile" jobs to just "Store" when the + job is displayed in the `spectate` tooltips. See the + ``tooltip-follow-job-shortenings`` setting below for details. Settings -------- @@ -118,10 +120,11 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. -``tooltip-follow-job-shortenings`` +``tooltip-follow-job-shortenings`` (default: "Store item in stockpile" -> "Store item") If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to shorten some job names, f.e. "Store item in stockpile" becomes "Store item". - See examples for an example. + You can pass two parameters to ``spectate set tooltip-follow-job-shortenings`` to + add or change elements in the dictionary. See the Examples section for an example. ``tooltip-follow-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -135,7 +138,7 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the tooltip. F.e. ``tooltip-follow-stress-levels 2 true`` would show the Displeased emoticon. - See ``tooltip-stress-levels`` for details. + See ``tooltip-stress-levels`` below for details. ``tooltip-hover`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the @@ -157,7 +160,7 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the specific happiness level (stress) of the dwarf in the hover panel. F.e. ``tooltip-hover-stress-levels 2 true`` would show the Displeased emoticon. - See ``tooltip-stress-levels`` for details. + See ``tooltip-stress-levels`` below for details. ``tooltip-stress-levels`` Controls how happiness levels (stress) are displayed (emoticon and color). @@ -202,7 +205,7 @@ Settings - =D - COLOR_LIGHTCYAN -Keep in mind that the text will look different with the game's font. +Keep in mind that the text may look different when rendered in the game's font. Overlays -------- From ca3af5e9318838cc6b5ff3b3ded07292926f8aee Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Feb 2025 07:17:09 +0000 Subject: [PATCH 103/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 654720a2d60..8639a8f17e4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 654720a2d608dfd26a9596182a70ca1ad1ea3053 +Subproject commit 8639a8f17e43e0caba48137a407c2ac60abc3d15 From 7b6bfc856a1ed982408aaea23d898b8eac65bb10 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Feb 2025 17:25:31 -0600 Subject: [PATCH 104/919] add type identity for `std::filesystem::path` --- library/DataIdentity.cpp | 1 + library/LuaTypes.cpp | 18 ++++++++++++++++++ library/LuaWrapper.cpp | 2 +- library/include/DataIdentity.h | 24 ++++++++++++++++++++++++ library/include/LuaWrapper.h | 2 +- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index e6b4889cf88..a385915d920 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -38,6 +38,7 @@ namespace df { const bool_identity identity_traits::identity; const stl_string_identity identity_traits::identity; + const path_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const pointer_identity identity_traits::identity; diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 1aa7711e689..81914a71855 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -188,6 +188,24 @@ void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *p *(std::string*)ptr = std::string(bytes, size); } +void df::path_identity::lua_read(lua_State* state, int fname_idx, void* ptr) const +{ + auto ppath = (std::filesystem::path*)ptr; + auto str = ppath->u8string(); + lua_pushlstring(state, (char*)str.data(), str.size()); +} + +void df::path_identity::lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const +{ + size_t size; + const char* bytes = lua_tolstring(state, val_index, &size); + if (!bytes) + field_error(state, fname_idx, "path expected", "write"); + + std::u8string str((char8_t*)bytes, size); + *(std::filesystem::path*)ptr = std::filesystem::path(str); +} + void df::pointer_identity::lua_read(lua_State *state, int fname_idx, void *ptr, const type_identity *target) { push_object_internal(state, target, *(void**)ptr); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 608ef956837..892a6784cff 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -49,7 +49,7 @@ using namespace DFHack::LuaWrapper; /** * Report an error while accessing a field (index = field name). */ -void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) +[[noreturn]] void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) { if (lua_islightuserdata(state, UPVAL_METATABLE)) lua_pushstring(state, "(global)"); diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 02bc8b32018..7e1f8eaaea7 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -34,6 +34,7 @@ distribution. #include #include #include +#include #include "DataDefs.h" @@ -298,6 +299,24 @@ namespace df virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; }; + class DFHACK_EXPORT path_identity : public DFHack::constructed_identity { + public: + path_identity() + : constructed_identity(sizeof(std::filesystem::path), &allocator_fn) + { + }; + + const std::string getFullName() const { return "path"; } + + virtual DFHack::identity_type type() const { return DFHack::IDTYPE_PRIMITIVE; } + + virtual bool isPrimitive() const { return true; } + + virtual void lua_read(lua_State* state, int fname_idx, void* ptr) const; + virtual void lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const; + }; + + class DFHACK_EXPORT stl_ptr_vector_identity : public ptr_container_identity { public: typedef std::vector container; @@ -616,6 +635,11 @@ namespace df static const stl_string_identity *get() { return &identity; } }; + template<> struct DFHACK_EXPORT identity_traits { + static const bool is_primitive = true; + static const path_identity identity; + static const path_identity* get() { return &identity; } + }; template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; static const ptr_string_identity identity; diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 347945e8966..44bc19938c6 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -142,7 +142,7 @@ namespace LuaWrapper { /** * Report an error while accessing a field (index = field name). */ - void field_error(lua_State *state, int index, const char *err, const char *mode); + [[noreturn]] void field_error(lua_State *state, int index, const char *err, const char *mode); /* * If is_method is true, these use UPVAL_TYPETABLE to save a hash lookup. From cb7b4cdc484d2a20c6147c2a415fade252d14122 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Feb 2025 17:26:43 -0600 Subject: [PATCH 105/919] really fix windows console --- library/Console-windows.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 83ed0f64ee5..12a3e0c2eb0 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -233,13 +233,16 @@ namespace DFHack size_t len = raw_buffer.size(); int cooked_cursor = raw_cursor; - int adj = std::min(plen + cooked_cursor - cols, len); - buf += adj; - len -= adj; - cooked_cursor -= adj; + if (plen + cooked_cursor > cols) + { + int adj = std::min(plen + cooked_cursor - cols, len); + buf += adj; + len -= adj; + cooked_cursor -= adj; + } - int adj2 = std::min(plen + len - cols, len); - len -= adj2; + if (len + plen > cols) + len = cols - plen; CONSOLE_SCREEN_BUFFER_INFO inf = { 0 }; GetConsoleScreenBufferInfo(console_out, &inf); From b7424cc5223b7b05a4d815404797c2cd082f48ec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 14:37:11 -0800 Subject: [PATCH 106/919] add spectate features to vanilla follow mode --- docs/changelog.txt | 2 +- docs/plugins/spectate.rst | 24 +++++--- plugins/lua/spectate.lua | 65 ++++++++++++++++++++- plugins/spectate.cpp | 117 +++++++++++++++++++++++--------------- 4 files changed, 150 insertions(+), 58 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index eb90bcf8619..3d6da9045c2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S -- `spectate`: when spectate mode is enabled, left/right arrow will cycle through following next/prevous units +- `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) - ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip ## Fixes diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 65d7cefe281..a09d0170b90 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -178,31 +178,31 @@ Settings - pen * - 0 - Miserable - - =C + - ``=C`` - COLOR_RED * - 1 - Unhappy - - :C + - ``:C`` - COLOR_LIGHTRED * - 2 - Displeased - - :( + - ``:(`` - COLOR_YELLOW * - 3 - Content - - :] + - ``:]`` - COLOR_GREY * - 4 - Pleased - - :) + - ``:)`` - COLOR_GREEN * - 5 - Happy - - :D + - ``:D`` - COLOR_LIGHTGREEN * - 6 - Ecstatic - - =D + - ``=D`` - COLOR_LIGHTCYAN Keep in mind that the text may look different when rendered in the game's font. @@ -217,3 +217,13 @@ and/or a popup panel with information when your mouse cursor hovers over a unit. This overlay is managed via the `overlay` framework. It can be controlled via the ``spectate overlay`` command or the ``Overlays`` tab in `gui/control-panel`. + +``spectate.followpanel`` + +This overlay adds widgets to the vanilla follow panel -- the one that appears +in the lower left corner of the screen when you are following a unit. When you +are following a unit, regardless of whether the `spectate` plugin is enabled, +you can use the keyboard cursor left/right keys to switch which unit you are +following. There is also an indicator for whether spectate mode is enabled +(that is, whether the `spectate` plugin is enabled), and there is a button for +launching the `gui/spectate` configuration UI. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 2f8ade03cf8..d90eb3e70f5 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -2,11 +2,10 @@ local _ENV = mkmodule('plugins.spectate') local argparse = require('argparse') local dlg = require('gui.dialogs') +local gui = require('gui') local json = require('json') local overlay = require('plugins.overlay') local utils = require('utils') - -local gui = require('gui') local widgets = require('gui.widgets') -- settings starting with 'tooltip-' are not passed to the C++ plugin @@ -306,7 +305,7 @@ local function GetHoverText(pos) end ----------------------------- --- overlays +-- TooltipOverlay TooltipOverlay = defclass(TooltipOverlay, overlay.OverlayWidget) TooltipOverlay.ATTRS{ @@ -545,7 +544,67 @@ function MouseTooltip:render(dc) MouseTooltip.super.render(self, dc) end +----------------------------- +-- FollowPanelOverlay + +local plotinfo = df.global.plotinfo +local mi = df.global.game.main_interface + +local function follow_panel_is_visible() + return plotinfo.follow_unit > -1 and + mi.current_hover == -1 and + not mi.hover_instructions_on and + not mi.current_hover_alert +end + +FollowPanelOverlay = defclass(FollowPanelOverlay, overlay.OverlayWidget) +FollowPanelOverlay.ATTRS{ + desc='Adds spectate widgets to the vanilla follow panel.', + default_pos={x=6,y=-5}, + viewscreens='dwarfmode/Default', + default_enabled=true, + frame={w=39, h=1}, + visible=follow_panel_is_visible, +} + +function FollowPanelOverlay:init() + self:addviews{ + widgets.Label{ + frame={l=0, t=0, w=3}, + text=(' %s '):format(string.char(27)), + on_click=spectate_followPrev, + }, + widgets.Label{ + frame={l=5, t=0, w=3}, + text=(' %s '):format(string.char(26)), + on_click=spectate_followNext, + }, + widgets.Label{ + frame={l=10, t=0, w=25}, + text={ + ' spectate mode: ', + {text=function() return isEnabled() and 'enabled ' or 'disabled ' end}, + }, + on_click=function() dfhack.run_command(isEnabled() and 'disable' or 'enable', 'spectate') end, + }, + widgets.ConfigureButton{ + frame={l=36, t=0}, + on_click=function() dfhack.run_script('gui/spectate') end, + } + } +end + +function FollowPanelOverlay:onInput(keys) + if keys.KEYBOARD_CURSOR_LEFT then + spectate_followPrev() + elseif keys.KEYBOARD_CURSOR_RIGHT then + spectate_followNext() + end + return FollowPanelOverlay.super.onInput(self, keys) +end + OVERLAY_WIDGETS = { + followpanel=FollowPanelOverlay, tooltip=TooltipOverlay, } diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index b27db514603..7b2b24f946c 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -37,6 +37,7 @@ namespace DFHack { DBG_DECLARE(spectate, event, DebugCategory::LINFO); } +static std::default_random_engine rng; static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; @@ -174,28 +175,27 @@ static class UnitHistory { offset = 0; } - void add(color_ostream &out, df::unit *unit) { + void add_and_follow(color_ostream &out, df::unit *unit) { + // if we're currently following a unit, add it to the history if it's not already there + if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { + DEBUG(cycle,out).print("currently following unit %d that is not in history; adding\n", plotinfo->follow_unit); + update_history(out, plotinfo->follow_unit); + } + int32_t id = unit->id; + update_history(out, id); DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_unit = id; - if (offset > 0) { - DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); - history.resize(history.size() - offset); - offset = 0; - } - history.push_back(id); - if (history.size() > MAX_HISTORY) { - DEBUG(cycle,out).print("history full, truncating\n"); - history.pop_front(); - } - DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); } void scan_back(color_ostream &out) { - if (history.empty() || offset >= history.size()-1) + if (history.empty() || offset >= history.size()-1) { + DEBUG(cycle,out).print("already at beginning of history\n"); return; - int unit_id = history[history.size() - (1 + ++offset)]; + } + ++offset; + int unit_id = get_cur_unit_id(); DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); @@ -209,29 +209,39 @@ static class UnitHistory { return; } - int unit_id = history[history.size() - (1 + --offset)]; + --offset; + int unit_id = get_cur_unit_id(); DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_unit = unit_id; } -} unit_history; -struct forward_back_interceptor : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { - bool is_at_default_view = Gui::matchFocusString("dwarfmode/Default"); - if (is_at_default_view && input->count(df::interface_key::CUSTOM_LEFT)) - unit_history.scan_back(Core::getInstance().getConsole()); - else if (is_at_default_view && input->count(df::interface_key::CUSTOM_RIGHT)) - unit_history.scan_forward(Core::getInstance().getConsole()); - else { - INTERPOSE_NEXT(feed)(input); + int32_t get_cur_unit_id() { + if (offset >= history.size()) + return -1; + return history[history.size() - (1 + offset)]; + } + +private: + void update_history(color_ostream &out, int32_t unit_id) { + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + if (history.size() && history.back() == unit_id) { + DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); + } else { + history.push_back(unit_id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); } -}; -IMPLEMENT_VMETHOD_INTERPOSE(forward_back_interceptor, feed); +} unit_history; ///////////////////////////////////////////////////// // RecentUnits @@ -293,7 +303,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector distribution(config.follow_ms / 2, config.follow_ms / 6); + delay_ms = distribution(rng); + delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); + } + DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); + next_cycle_unpaused_ms = Core::getInstance().getUnpausedMs() + delay_ms; +} + DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); @@ -323,9 +343,11 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { return CR_FAILURE; } INFO(control,out).print("Spectate mode enabled!\n"); - INTERPOSE_HOOK(forward_back_interceptor, feed).apply(); EventManager::registerListener(EventManager::EventType::UNIT_NEW_ACTIVE, new_unit_handler); - follow_a_dwarf(out); + if (plotinfo->follow_unit > -1) + set_next_cycle_unpaused_ms(out); + else + follow_a_dwarf(out); } else { INFO(control,out).print("Spectate mode disabled!\n"); plotinfo->follow_unit = -1; @@ -443,19 +465,6 @@ static void get_dwarf_buckets(color_ostream &out, } } -static std::default_random_engine rng; - -static uint32_t get_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat) { - int32_t delay_ms = config.follow_ms; - if (config.cinematic_action && has_active_combat) { - std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); - delay_ms = distribution(rng); - delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); - } - DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); - return Core::getInstance().getUnpausedMs() + delay_ms; -} - static void add_bucket_to_vectors(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { if (bucket.empty()) return; @@ -505,7 +514,7 @@ static void follow_a_dwarf(color_ostream &out) { vector other_units; get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); - next_cycle_unpaused_ms = get_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); + set_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); // coalesce the buckets and add weights vector units; @@ -536,7 +545,7 @@ static void follow_a_dwarf(color_ostream &out) { DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); } - unit_history.add(out, unit); + unit_history.add_and_follow(out, unit); } ///////////////////////////////////////////////////// @@ -579,7 +588,21 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { } } +static void spectate_followPrev(color_ostream &out) { + DEBUG(control,out).print("entering spectate_followPrev\n"); + unit_history.scan_back(out); + set_next_cycle_unpaused_ms(out); +}; + +static void spectate_followNext(color_ostream &out) { + DEBUG(control,out).print("entering spectate_followNext\n"); + unit_history.scan_forward(out); + set_next_cycle_unpaused_ms(out); +}; + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(spectate_setSetting), + DFHACK_LUA_FUNCTION(spectate_followPrev), + DFHACK_LUA_FUNCTION(spectate_followNext), DFHACK_LUA_END }; From d32c52bc2c8f4bdd4334c07b7b8a455f089f2c0c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 15:37:02 -0800 Subject: [PATCH 107/919] add global keybinding for gui/sitemap --- data/init/dfhack.keybindings.init | 1 + docs/changelog.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 1fa7b9facce..d7ce5c86629 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -41,6 +41,7 @@ keybinding add Ctrl-A@choose_start_site gui/embark-anywhere # in the game root directory keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeonmode/ViewSheets/UNIT|dungeonmode/ViewSheets/ITEM markdown +keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap ###################### # dwarfmode bindings # diff --git a/docs/changelog.txt b/docs/changelog.txt index 3d6da9045c2..166f55c45f6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) - ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip +- `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes - Windows console: fix possible hang if the console returns a too-small window width (for any reason) From 7d92f181f3935be0859de0ab926358b9b7e1e85a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 15:37:14 -0800 Subject: [PATCH 108/919] add infrastructure for use by gui/sitemap --- plugins/lua/spectate.lua | 9 ++++--- plugins/spectate.cpp | 56 ++++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d90eb3e70f5..4b36bee6938 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -563,7 +563,7 @@ FollowPanelOverlay.ATTRS{ default_pos={x=6,y=-5}, viewscreens='dwarfmode/Default', default_enabled=true, - frame={w=39, h=1}, + frame={w=29, h=1}, visible=follow_panel_is_visible, } @@ -582,13 +582,14 @@ function FollowPanelOverlay:init() widgets.Label{ frame={l=10, t=0, w=25}, text={ - ' spectate mode: ', - {text=function() return isEnabled() and 'enabled ' or 'disabled ' end}, + ' spectate:', + {text=function() return isEnabled() and ' on ' or 'off ' end, + pen=function() return isEnabled() and COLOR_GREEN or COLOR_LIGHTRED end}, }, on_click=function() dfhack.run_command(isEnabled() and 'disable' or 'enable', 'spectate') end, }, widgets.ConfigureButton{ - frame={l=36, t=0}, + frame={l=26, t=0}, on_click=function() dfhack.run_script('gui/spectate') end, } } diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 7b2b24f946c..45512a2042f 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -175,17 +175,36 @@ static class UnitHistory { offset = 0; } + void add_to_history(color_ostream &out, int32_t unit_id) { + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + if (history.size() && history.back() == unit_id) { + DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); + } else { + history.push_back(unit_id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } + } + DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + } + void add_and_follow(color_ostream &out, df::unit *unit) { // if we're currently following a unit, add it to the history if it's not already there if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { DEBUG(cycle,out).print("currently following unit %d that is not in history; adding\n", plotinfo->follow_unit); - update_history(out, plotinfo->follow_unit); + add_to_history(out, plotinfo->follow_unit); } int32_t id = unit->id; - update_history(out, id); + add_to_history(out, id); DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; plotinfo->follow_unit = id; } @@ -199,6 +218,7 @@ static class UnitHistory { DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; plotinfo->follow_unit = unit_id; } @@ -214,6 +234,7 @@ static class UnitHistory { DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; plotinfo->follow_unit = unit_id; } @@ -222,25 +243,6 @@ static class UnitHistory { return -1; return history[history.size() - (1 + offset)]; } - -private: - void update_history(color_ostream &out, int32_t unit_id) { - if (offset > 0) { - DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); - history.resize(history.size() - offset); - offset = 0; - } - if (history.size() && history.back() == unit_id) { - DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); - } else { - history.push_back(unit_id); - if (history.size() > MAX_HISTORY) { - DEBUG(cycle,out).print("history full, truncating\n"); - history.pop_front(); - } - } - DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); - } } unit_history; ///////////////////////////////////////////////////// @@ -392,7 +394,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_onupdate(color_ostream &out) { announcement_settings.on_update(out); - if (config.auto_disengage && (plotinfo->follow_unit < 0 || is_squads_open())) { + if (config.auto_disengage && (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open())) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; plotinfo->follow_unit = -1; @@ -600,9 +602,19 @@ static void spectate_followNext(color_ostream &out) { set_next_cycle_unpaused_ms(out); }; +static void spectate_addToHistory(color_ostream &out, int32_t unit_id) { + DEBUG(control,out).print("entering spectate_addToHistory; unit_id=%d\n", unit_id); + if (!df::unit::find(unit_id)) { + WARN(control,out).print("unit with id %d not found; not adding to history\n", unit_id); + return; + } + unit_history.add_to_history(out, unit_id); +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(spectate_setSetting), DFHACK_LUA_FUNCTION(spectate_followPrev), DFHACK_LUA_FUNCTION(spectate_followNext), + DFHACK_LUA_FUNCTION(spectate_addToHistory), DFHACK_LUA_END }; From 872ba28e02e842a6a8fc6409cc4c504fd969f857 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 21 Feb 2025 00:04:04 +0000 Subject: [PATCH 109/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8639a8f17e4..cc421f03eeb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8639a8f17e43e0caba48137a407c2ac60abc3d15 +Subproject commit cc421f03eebed861cdf3a5cd925dd01887373020 From fa911ff0152fb49979ddbf9087c7592f6c3bf0e4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 09:52:39 -0800 Subject: [PATCH 110/919] exclude inactive units from inBox functions --- docs/changelog.txt | 1 + library/modules/Units.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 166f55c45f6..7954d9c6a0f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -78,6 +78,7 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments - ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 1af93c3b7d8..21f39932fe1 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -665,6 +665,8 @@ bool Units::isGreatDanger(df::unit *unit) { bool Units::isUnitInBox(df::unit *u, const cuboid &box) { CHECK_NULL_POINTER(u); + if (!isActive(u)) + return false; return box.containsPos(getPosition(u)); } @@ -674,7 +676,7 @@ bool Units::getUnitsInBox(vector &units, const cuboid &box, std::fun units.clear(); for (auto unit : world->units.active) - if (filter(unit) && isUnitInBox(unit, box)) + if (isUnitInBox(unit, box) && filter(unit)) units.push_back(unit); return true; } From ea65827025ac39ad02163c34ae55dcfbdf47fd3d Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 21 Feb 2025 21:04:49 +0100 Subject: [PATCH 111/919] spectate.lua: make config public Also improve error messages when missing command arguments --- plugins/lua/spectate.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 4b36bee6938..0bd2d66920b 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -84,7 +84,8 @@ local function load_state() function() config_file:write() end end -local config, save_state = load_state() +-- global variables to make the UI script simpler +_ENV.config, _ENV.save_state = load_state() function refresh_cpp_config() for name,value in pairs(config) do @@ -158,11 +159,15 @@ local function do_toggle() end local function set_setting(args) + local n = #args + if n == 0 then + qerror('missing key') + end local key = table.remove(args, 1) if config[key] == nil then qerror('unknown setting: ' .. key) end - local n = #args + n = #args if n == 0 then qerror('missing value') end @@ -221,6 +226,7 @@ function parse_commandline(args) elseif command == 'set' then set_setting(args) elseif command == 'overlay' then + if #args == 0 then qerror('missing option') end set_overlay(args[1]) else return false From 20f7fd346aaa63c97b02b47777fe9613f2dc22c7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Feb 2025 17:18:54 -0600 Subject: [PATCH 112/919] flush cache after mempatch This adds code to flush cache after applying a hotpatch. While it's unlikely that a patched location will be in cache at the time we apply a patch, it is better to be safe than sorry Also fixed the spelling error in `setPermissions` while I was here --- library/Core.cpp | 6 ++++-- library/Process-linux.cpp | 9 ++++++++- library/Process-windows.cpp | 7 ++++++- library/include/MemAccess.h | 5 ++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 9dc2d2e68a4..be53fe58fdb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2987,7 +2987,7 @@ bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) save.push_back(perms); perms.write = perms.read = true; - if (!p->setPermisions(perms, perms)) + if (!p->setPermissions(perms, perms)) return false; } @@ -3000,13 +3000,15 @@ bool MemoryPatcher::write(void *target, const void *src, size_t size) return false; memmove(target, src, size); + + p->flushCache(target, size); return true; } void MemoryPatcher::close() { for (size_t i = 0; i < save.size(); i++) - p->setPermisions(save[i], save[i]); + p->setPermissions(save[i], save[i]); save.clear(); ranges.clear(); diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 85e83f54f61..321db805506 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -32,6 +32,7 @@ distribution. #include #include +#include #include #include #include @@ -205,7 +206,7 @@ int Process::getPID() return getpid(); } -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) { int result; int protect=0; @@ -217,6 +218,12 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) return result==0; } +bool Process::flushCache(const void* target, size_t count) +{ + return cacheflush(target, count, BCACHE); +} + + // returns -1 on error void* Process::memAlloc(const int length) { diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 2450eb87bd2..0098c8c167e 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -399,7 +399,7 @@ int Process::getPID() } -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) { DWORD newprotect=0; if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; @@ -414,6 +414,11 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) return result; } +bool Process::flushCache(const void* target, size_t count) +{ + return 0 == FlushInstructionCache(d->my_handle, (LPCVOID)target, count); +} + void* Process::memAlloc(const int length) { void *ret; diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 083d94434ca..bcbab75bd3a 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -270,11 +270,14 @@ namespace DFHack uint32_t getTickCount(); /// modify permisions of memory range - bool setPermisions(const t_memrange & range,const t_memrange &trgrange); + bool setPermissions(const t_memrange & range,const t_memrange &trgrange); /// write a possibly read-only memory area bool patchMemory(void *target, const void* src, size_t count); + /// flush cache + bool flushCache(const void* target, size_t count); + /// allocate new memory pages for code or stuff /// returns -1 on error (0 is a valid address) void* memAlloc(const int length); From 3b0d1ea40817e72ebf1ce3ea0728630f7eca6385 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Feb 2025 17:32:08 -0600 Subject: [PATCH 113/919] use __builtin___clear_cache on linux apparently the documentation i found was... incomplete --- library/Process-linux.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 321db805506..1583b9d619e 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -32,7 +32,6 @@ distribution. #include #include -#include #include #include #include @@ -220,7 +219,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { - return cacheflush(target, count, BCACHE); + __builtin___clear_cache((char*)target, (char*)target + count - 1); } From a78f2a70e5e89a95a9569233cbbead221e19a500 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Feb 2025 17:37:05 -0600 Subject: [PATCH 114/919] assume the intrinsic always succeeds --- library/Process-linux.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 1583b9d619e..5e2458ec473 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -220,6 +220,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { __builtin___clear_cache((char*)target, (char*)target + count - 1); + return false; /* assume always succeeds, as the builtin has no return type */ } From 970bfc6e5ba9a4f42278b27055508b4f918e4aa7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:15:20 +0000 Subject: [PATCH 115/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index f312d353240..7badeff963b 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f312d353240f45922084f901ade84f7479fde53d +Subproject commit 7badeff963b0494e2e79e49a0dd3555961df5f04 From acd6ec97fd359969369c37ba8c4b9855eb0bc9e2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 23:22:12 -0800 Subject: [PATCH 116/919] actually disable timestream when unloading world --- plugins/timestream.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index ea75ced3598..eeb4d1115c2 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -157,6 +157,7 @@ static void reset_ephemeral_state() { static void do_disable() { EventManager::unregisterAll(plugin_self); + is_enabled = false; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { From 0676850f670ee0a3d0673f8a684087478b1b88da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 23:58:52 -0800 Subject: [PATCH 117/919] add API for querying whether an overlay is enabled --- docs/changelog.txt | 1 + docs/dev/overlay-dev-guide.rst | 17 +++++++++++++++++ plugins/lua/overlay.lua | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7954d9c6a0f..7c2b587bdaa 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -85,6 +85,7 @@ Template for new versions: - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons - ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled ## Removed diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 7b8198c78aa..4302d5d4c8c 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -228,6 +228,23 @@ If you need to improve performance, here are some potential options: 3. Move hotspots into C++ code, either in a new core library function or in a dedicated plugin +Overlay framework API +--------------------- + +The overlay plugin Lua interface provides a few functions for interacting with +the framework. You can get a reference to the API via:: + + local overlay = require('plugins.overlay') + +* ``overlay.rescan()`` + + Rescans all module-loadable Lua scripts for registered overlays and loads + updated widget definitions. + +* ``overlay.isOverlayEnabled(name)`` + + Returns whether the overlay with the given name is enabled. + Development workflows --------------------- diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 8d7c9a7e2d2..77df7d47a4e 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -24,6 +24,7 @@ local overlay_config = {} -- map of widget name to persisted state local active_hotspot_widgets = {} -- map of widget names to the db entry local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db +-- for use by gui/overlay function get_state() return {index=widget_index, config=overlay_config, db=widget_db} end @@ -73,6 +74,11 @@ local function save_config() end end +function isOverlayEnabled(name) + if not overlay_config[name] then return false end + return overlay_config[name].enabled +end + -- ----------- -- -- utility fns -- -- ----------- -- From 7cf972279bab990bf549c36fc05775c9bb8c3d77 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 22 Feb 2025 00:22:59 -0800 Subject: [PATCH 118/919] add state query function intending to modify state via the public API so it gets synced to cpp --- plugins/lua/spectate.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 0bd2d66920b..d7a20818021 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -84,8 +84,18 @@ local function load_state() function() config_file:write() end end --- global variables to make the UI script simpler -_ENV.config, _ENV.save_state = load_state() +local config, save_state = load_state() + +-- called by gui/spectate +function get_config_elem(name, key) + local elem = config[name] + if not elem then return end + if type(elem) == 'table' then + return elem[key] + end + return elem +end + function refresh_cpp_config() for name,value in pairs(config) do From c0252824359d27cd230147a21c9b111eb477a2d7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 00:25:50 -0800 Subject: [PATCH 119/919] improve error message when boolean arg is missing --- library/lua/argparse.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index a76e9d3c6d8..e5337afa11d 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -244,6 +244,9 @@ local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true,["ena ---@param arg_name? string ---@return boolean function boolean(arg, arg_name) + if arg == nil then + arg_error(arg_name, 'missing value; expected "true", "yes", "false", or "no"') + end local arg_lower = string.lower(arg) if toBool[arg_lower] == nil then arg_error(arg_name, From 5e835fa27ca881bdef47229cefca0c192cef1544 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 00:38:20 -0800 Subject: [PATCH 120/919] fix typo --- plugins/lua/spectate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d7a20818021..b50ab399dc5 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -96,7 +96,6 @@ function get_config_elem(name, key) return elem end - function refresh_cpp_config() for name,value in pairs(config) do if not name:startswith(lua_only_settings_prefix) then From 2b7b1b1a25d90a8a5256924c55792bd34fcbb067 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 02:37:29 -0800 Subject: [PATCH 121/919] get rid of auto-disengage option and make it always on otherwise the UI can fight with the player --- docs/plugins/spectate.rst | 10 ++-------- plugins/lua/spectate.lua | 13 ++++--------- plugins/spectate.cpp | 14 +++++--------- scripts | 2 +- 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index a09d0170b90..89aef2b9f46 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -13,7 +13,7 @@ to following a different dwarf. It can also switch to following animals, hostiles, or visiting units. You can switch to the next target (or a previous target) immediately with the left/right arrow keys. -By default, `spectate` will disengage and turn itself off when you move the +`spectate` will automatically disengage and turn itself off when you move the map, just like the vanilla follow mechanic. It will also disengage immediately if you open the squads menu for military action. @@ -36,7 +36,7 @@ Usage enable spectate spectate [status] spectate toggle - spectate set + spectate set [] spectate overlay enable|disable Examples @@ -71,12 +71,6 @@ Examples Settings -------- -``auto-disengage`` (default: enabled) - Toggle automatically disabling the plugin when the player moves the map or - opens the squad panel. If this is disabled, you will need to manually - disable the plugin to turn off follow mode. You can still interact normally - with the DF UI. - ``auto-unpause`` (default: disabled) Toggle auto-dismissal of announcements that pause the game, like sieges, forgotten beasts, etc. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index b50ab399dc5..d9e5f5e5669 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -16,7 +16,6 @@ local max_banner_y_offset = 4 local function get_default_state() return { - ['auto-disengage']=true, ['auto-unpause']=false, ['cinematic-action']=true, ['follow-seconds']=10, @@ -89,7 +88,7 @@ local config, save_state = load_state() -- called by gui/spectate function get_config_elem(name, key) local elem = config[name] - if not elem then return end + if elem == nil then return end if type(elem) == 'table' then return elem[key] end @@ -109,14 +108,10 @@ end function show_squads_warning() local message = { - 'Cannot start spectate mode while auto-disengage is enabled and', - 'the squads panel is open. The auto-disengage feature automatically', - 'stops spectate mode when you open the squads panel.', + 'Cannot start spectate mode while the squads panel is open. Spectate', + 'automatically disengages when you open the squads panel.', '', - 'Please either close the squads panel or disable auto-disengage by', - 'running the following command:', - '', - 'spectate set auto-disengage false', + 'Please close the squads panel before enabling spectate mode.', } dlg.showMessage("Spectate", table.concat(message, '\n')) end diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 45512a2042f..517d8a29db7 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -63,7 +63,6 @@ static const std::unordered_set boring_jobs = { // Configuration static struct Configuration { - bool auto_disengage; bool auto_unpause; bool cinematic_action; bool include_animals; @@ -75,7 +74,6 @@ static struct Configuration { int32_t follow_ms; void reset() { - auto_disengage = true; auto_unpause = false; cinematic_action = true; include_animals = false; @@ -338,8 +336,8 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "refresh_cpp_config")) { WARN(control,out).print("Failed to refresh config\n"); } - if (config.auto_disengage && is_squads_open()) { - out.printerr("Cannot enable %s while auto-disengage is enabled and the squads screen is open.\n", plugin_name); + if (is_squads_open()) { + out.printerr("Cannot enable %s while the squads screen is open.\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.spectate", "show_squads_warning"); is_enabled = false; return CR_FAILURE; @@ -394,7 +392,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_onupdate(color_ostream &out) { announcement_settings.on_update(out); - if (config.auto_disengage && (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open())) { + if (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open()) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; plotinfo->follow_unit = -1; @@ -402,7 +400,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } - if ((!config.auto_disengage && plotinfo->follow_unit < 0) || Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { + if (Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { recent_units.trim(); follow_a_dwarf(out); } @@ -556,9 +554,7 @@ static void follow_a_dwarf(color_ostream &out) { static void spectate_setSetting(color_ostream &out, string name, int val) { DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); - if (name == "auto-disengage") { - config.auto_disengage = val; - } else if (name == "auto-unpause") { + if (name == "auto-unpause") { if (val && !config.auto_unpause) { announcement_settings.save_and_scrub_settings(out); } else if (!val && config.auto_unpause) { diff --git a/scripts b/scripts index cc421f03eeb..637eb47ca98 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit cc421f03eebed861cdf3a5cd925dd01887373020 +Subproject commit 637eb47ca98e9188fe7cdd73c73b0d994c4bada8 From a4d2e0809f516ae31553c197629d58de6dd98675 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Feb 2025 10:55:34 +0000 Subject: [PATCH 122/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 637eb47ca98..bbf75a749c5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 637eb47ca98e9188fe7cdd73c73b0d994c4bada8 +Subproject commit bbf75a749c510a7baf5bafbcd4bb8f2bfc4fa46d From 83dbb4dd3fdd15b47d3f14055f44188720d81632 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 03:25:48 -0800 Subject: [PATCH 123/919] remove leftover debug log --- plugins/spectate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 517d8a29db7..54bd4944a0f 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -452,7 +452,7 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - INFO(cycle).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + TRACE(cycle,out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) citizen_combat_units.push_back(unit); else From c9a7537ecc6a1f6684942832dba12dfb6e8628e5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 03:53:59 -0800 Subject: [PATCH 124/919] don't modify state with bad coords --- library/modules/Gui.cpp | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 6b1976d34f3..ff635ac4188 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2831,33 +2831,31 @@ bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center, boo unfollow(); + if (!Maps::isValidTilePos(x, y, z)) + return false; + auto dims = getDwarfmodeViewDims(); int32_t w = dims.map_x2 - dims.map_x1 + 1; int32_t h = dims.map_y2 - dims.map_y1 + 1; int32_t new_win_x, new_win_y, new_win_z; getViewCoords(new_win_x, new_win_y, new_win_z); - if (Maps::isValidTilePos(x, y, z)) - { - if (center) - { - new_win_x = x - w / 2; - new_win_y = y - h / 2; - } - else // just bring it on screen - { - if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" - new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; - if (new_win_y > (y - 5)) - new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; - if (new_win_x < (x + 5 - w)) - new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; - if (new_win_y < (y + 5 - h)) - new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; - } - - new_win_z = z; - } + if (center) { + new_win_x = x - w / 2; + new_win_y = y - h / 2; + } else { + // just bring it on screen + if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" + new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; + if (new_win_y > (y - 5)) + new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; + if (new_win_x < (x + 5 - w)) + new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; + if (new_win_y < (y + 5 - h)) + new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; + } + + new_win_z = z; *window_x = new_win_x; *window_y = new_win_y; From 4c5a461759fddd10a2bb2184c5c3f95f245abf99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 03:54:16 -0800 Subject: [PATCH 125/919] ensure highlight appears when scanning through history --- plugins/lua/spectate.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d9e5f5e5669..c0858ee5364 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -608,8 +608,10 @@ end function FollowPanelOverlay:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then spectate_followPrev() + return true elseif keys.KEYBOARD_CURSOR_RIGHT then spectate_followNext() + return true end return FollowPanelOverlay.super.onInput(self, keys) end From d5167e19daac69ce039ae3db5e2bbf79dc5c4aae Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Feb 2025 14:12:15 -0600 Subject: [PATCH 126/919] switch `flushCache` to return `true` on success --- library/Process-linux.cpp | 2 +- library/Process-windows.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 5e2458ec473..a3fa9f2d67d 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -220,7 +220,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { __builtin___clear_cache((char*)target, (char*)target + count - 1); - return false; /* assume always succeeds, as the builtin has no return type */ + return true; /* assume always succeeds, as the builtin has no return type */ } diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 0098c8c167e..79bbea80025 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -416,7 +416,7 @@ bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange bool Process::flushCache(const void* target, size_t count) { - return 0 == FlushInstructionCache(d->my_handle, (LPCVOID)target, count); + return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); } void* Process::memAlloc(const int length) From 0d4912f3c2679fd02a319ed51ec277b649879370 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:57:14 +0000 Subject: [PATCH 127/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index bbf75a749c5..03fc4b2348e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit bbf75a749c510a7baf5bafbcd4bb8f2bfc4fa46d +Subproject commit 03fc4b2348e71b929a90cff429be151e9a7c1b49 From 65595afe4a580220d5e1832fa1b729073d034907 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 23 Feb 2025 07:15:49 +0000 Subject: [PATCH 128/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 03fc4b2348e..557ac5732e8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 03fc4b2348e71b929a90cff429be151e9a7c1b49 +Subproject commit 557ac5732e87d62f3b7970295322ae0c5f7eae2c From 941c2762a52c779e1238487b3a52abf929311c11 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 23 Feb 2025 09:20:07 +0100 Subject: [PATCH 129/919] spectate.lua: prevent users from destroying their config --- plugins/lua/spectate.lua | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index c0858ee5364..b3b81ef79a9 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -172,7 +172,9 @@ local function set_setting(args) qerror('unknown setting: ' .. key) end n = #args - if n == 0 then + if n == 0 + or (n == 1 and type(config[key]) == 'table') + then qerror('missing value') end @@ -195,21 +197,30 @@ local function set_setting(args) spectate_setSetting(key, value) end else + local errorUnknownSettingIfNil = function(t) + if t == nil then + table.remove(args) + qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) + end + end + local t = config[key] for i = 1, n - 2 do + errorUnknownSettingIfNil(t) t = t[args[i]] end local k = args[n-1] local v = args[n] if key ~= 'tooltip-follow-job-shortenings' then -- user should be able to add new shortenings, but not other things - if t[k] == nil then - table.remove(args) - qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - elseif key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then + errorUnknownSettingIfNil(t[k]) + if key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then v = argparse.boolean(v, key .. '/' .. k) end end + if type(t[k]) == 'table' then + qerror('missing value') + end t[k] = v end From d05b2df74862d3eae54552f29ec5f5c537820768 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 23 Feb 2025 13:33:36 -0600 Subject: [PATCH 130/919] Fix spectate.lua (#5300) fix the follow panel hitbox --- plugins/lua/spectate.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index b3b81ef79a9..dbe80ac9589 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -601,7 +601,7 @@ function FollowPanelOverlay:init() on_click=spectate_followNext, }, widgets.Label{ - frame={l=10, t=0, w=25}, + frame={l=10, t=0, w=14}, text={ ' spectate:', {text=function() return isEnabled() and ' on ' or 'off ' end, From a7a55f4b0eb3765296ea0dbe67c51bd650d5d9bf Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 23 Feb 2025 23:10:33 +0100 Subject: [PATCH 131/919] spectate.lua: refactor `set_setting()` --- plugins/lua/spectate.lua | 97 +++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index dbe80ac9589..d70efc62386 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -167,61 +167,64 @@ local function set_setting(args) if n == 0 then qerror('missing key') end - local key = table.remove(args, 1) - if config[key] == nil then - qerror('unknown setting: ' .. key) - end - n = #args - if n == 0 - or (n == 1 and type(config[key]) == 'table') - then - qerror('missing value') - end - if n == 1 then - local value = args[1] - if key == 'follow-seconds' then - value = argparse.positiveInt(value, 'follow-seconds') - elseif key == 'tooltip-follow-blink-milliseconds' then - value = argparse.nonnegativeInt(value, 'tooltip-follow-blink-milliseconds') + local cfg = config + local v + for i = 1, n do + v = cfg[args[i]] + if v == nil then + -- probably an unknown option, but we may allow adding new keys + break + elseif type(v) == 'table' then + if i == n then + -- arrived at the very last argument, but have a table + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + end + cfg = v else - value = argparse.boolean(value, key) - end - - config[key] = value - - if not key:startswith(lua_only_settings_prefix) then - if type(value) == 'boolean' then - value = value and 1 or 0 + -- arrived at something that's not a table + if i == n-1 then + -- if there is exactly 1 argument left, we're good + break + elseif i == n then + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + else -- i < n-1 then + qerror('too many arguments for ' .. table.concat(args, '/', 1, i)) end - spectate_setSetting(key, value) end - else - local errorUnknownSettingIfNil = function(t) - if t == nil then - table.remove(args) - qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - end + end + if v == nil then + if n == 3 and args[1] == 'tooltip-follow-job-shortenings' then + -- user should be able to add new shortenings, but not other things + else + qerror('unknown option: ' .. table.concat(args, '/', 1, i)) end + end - local t = config[key] - for i = 1, n - 2 do - errorUnknownSettingIfNil(t) - t = t[args[i]] - end - local k = args[n-1] - local v = args[n] - if key ~= 'tooltip-follow-job-shortenings' then - -- user should be able to add new shortenings, but not other things - errorUnknownSettingIfNil(t[k]) - if key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then - v = argparse.boolean(v, key .. '/' .. k) - end + local path = table.concat(args, '/', 1, n-1) + local key = args[n-1] + local value = args[n] + local entry_type = type(cfg[key]) + if entry_type == 'table' then + -- here just in case, is already checked in the loop above + qerror('missing value for ' .. path) + elseif entry_type == 'boolean' then + value = argparse.boolean(value, path) + elseif entry_type == 'number' then + if path == 'follow-seconds' then + value = argparse.positiveInt(value, path) + else + value = argparse.nonnegativeInt(value, path) end - if type(t[k]) == 'table' then - qerror('missing value') + end + + cfg[key] = value + + if n == 2 and not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 end - t[k] = v + spectate_setSetting(key, value) end save_state() From 7652b346370b1c167111af4eae18ea9ca9681181 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:17:39 +0000 Subject: [PATCH 132/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 7badeff963b..af8cf85f0c2 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7badeff963b0494e2e79e49a0dd3555961df5f04 +Subproject commit af8cf85f0c2a772439a441120eac246123afdc6a From e71649449edb2b470cf15199d126db44233ff42f Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Wed, 26 Feb 2025 17:14:47 +0100 Subject: [PATCH 133/919] spectate.lua: minor simplification of `render_unit_banners()` --- plugins/lua/spectate.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index d70efc62386..e255989117a 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -482,10 +482,10 @@ function TooltipOverlay:render_unit_banners(dc) for _, tok in ipairs(info) do local s if type(tok) == "string" then - writer = writer:pen(pen) + writer:pen(pen) s = tok else - writer = writer:pen(tok.pen) + writer:pen(tok.pen) s = tok.text end @@ -496,14 +496,15 @@ function TooltipOverlay:render_unit_banners(dc) -- we want to replace it with an `_`, so we need another `- 1` s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' - writer = writer:string(s) + writer:string(s) break -- nothing more will fit else - writer = writer:string(s) + writer:string(s) end ix = ix + len end + writer:pen(pen) -- just in case a different dc isn't resetting it -- mark coordinates as used for j = 0, #str - 1 do From 28cb1a0d19cce49b62b1862b9a1b169fd058fd46 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Wed, 26 Feb 2025 17:17:31 +0100 Subject: [PATCH 134/919] Update spectate.lua --- plugins/lua/spectate.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index e255989117a..7be670802ff 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -504,7 +504,6 @@ function TooltipOverlay:render_unit_banners(dc) ix = ix + len end - writer:pen(pen) -- just in case a different dc isn't resetting it -- mark coordinates as used for j = 0, #str - 1 do From c96f1080f3ceb94af14afa94b0591f29d674abf3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 26 Feb 2025 11:46:35 -0600 Subject: [PATCH 135/919] add `std::abort` falllthrough to `field_error` this should not be required, but `luaL_error` is not marked `[[noreturn]]` even though it reliably does not return, and i don't want to alter the lua module any more than absolutely necessary, so we do this here instead --- library/LuaWrapper.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 892a6784cff..f9fd0e78327 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -59,6 +59,7 @@ using namespace DFHack::LuaWrapper; const char *fname = index ? lua_tostring(state, index) : "*"; luaL_error(state, "Cannot %s field %s.%s: %s.", mode, (cname ? cname : "?"), (fname ? fname : "?"), err); + std::abort(); // should never be reached but makes gcc happy } /* */ From f3e3570b990b6298fc760802434aa1f5484483c4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Feb 2025 11:00:42 -0800 Subject: [PATCH 136/919] add experimental as a symbols target --- .github/workflows/generate-symbols.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 85a0067c854..9f4e32b1462 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -36,6 +36,7 @@ on: default: default options: - default + - experimental - testing - adventure_test - beta From 4de298941b2904cee57e590212313951bcabbeca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Feb 2025 12:27:00 -0800 Subject: [PATCH 137/919] turn on autopush for df experimental branch --- .github/workflows/watch-df-release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 6e723330344..77f49477813 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -15,13 +15,17 @@ jobs: fail-fast: false matrix: # df_steam_branch: which DF Steam branch to watch - # platform: leave blank to default to all + # platform: for symbols generation; leave blank to default to all # structures_ref: leave blank to default to master # dfhack_ref: leave blank if no structures update is desired # steam_branch: leave blank if no DFHack steam push is desired include: - df_steam_branch: public - df_steam_branch: beta + - df_steam_branch: experimental + structures_ref: experimental + dfhack_ref: experimental + steam_branch: experimental steps: - name: Fetch state uses: actions/cache/restore@v4 From 15b72512252d00eee2c22b26a73ce0394f5095c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Feb 2025 18:35:54 -0800 Subject: [PATCH 138/919] update xml ref --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 36117efc6b2..79154f3e8f7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 36117efc6b233ade089dcf053fe996a04e06e7ef +Subproject commit 79154f3e8f7699f1630c9b9601ebd22f96116024 From 66f58df8fad5b7fc38e6d2a6ddb3a167560e4bcf Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:17:49 +0000 Subject: [PATCH 139/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 79154f3e8f7..f5ca9135f09 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 79154f3e8f7699f1630c9b9601ebd22f96116024 +Subproject commit f5ca9135f09d0b13a23e5394b563609417d568c5 From 12cb12506d0857fc1dda0ffaf8e78352e69fa56b Mon Sep 17 00:00:00 2001 From: Quietust Date: Fri, 28 Feb 2025 10:30:48 -0600 Subject: [PATCH 140/919] Update item value functions. Fixes #5313 --- docs/changelog.txt | 2 ++ library/modules/Items.cpp | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1995e9d1d3b..612d5f9f707 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,6 +79,8 @@ Template for new versions: - ``Military::removeFromSquad``: removes unit from any squad assignments - ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer - ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units +- ``Items::getItemBaseValue``: DF 51.06 reduced the base value of prepared meals +- ``Items::getValue``: magical powers now correctly contribute to item value ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index a99c2bb7619..ff92b3cece7 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -64,6 +64,7 @@ distribution. #include "df/historical_entity.h" #include "df/item.h" #include "df/item_bookst.h" +#include "df/item_magicalst.h" #include "df/item_plant_growthst.h" #include "df/item_toolst.h" #include "df/item_type.h" @@ -1238,7 +1239,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, value = craw->misc.petvalue; return value; case FOOD: - return 10; + return 1; case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1645,7 +1646,10 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { case 3: value = value / 4; break; // (XX) tattered } - // Ignore value bonuses from magic, since that never actually happens + // Magical powers have 500 value each + auto magic = item->getMagic(); + if (magic != NULL) + value += magic->power.size() * 500; // Artifacts have 10x value if (item->flags.bits.artifact_mood) From 88f23737dfcd9a11d12f9ff977f22f7d3529d393 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:10:10 -0800 Subject: [PATCH 141/919] remove now-unused binpatches but update docs and keep structure for future binpatches --- CMakeLists.txt | 1 - data/CMakeLists.txt | 4 ++++ data/patches/README.md | 13 +++++++++++++ docs/changelog.txt | 1 - docs/dev/Binpatches.rst | 14 ++++++++++++-- patches/CMakeLists.txt | 3 --- .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ .../disable-vanilla-dimensions.dif | 6 ------ 12 files changed, 29 insertions(+), 43 deletions(-) create mode 100644 data/patches/README.md delete mode 100644 patches/CMakeLists.txt delete mode 100644 patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif delete mode 100644 patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e927a295d9..f102c355549 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,7 +451,6 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 355182bfb76..75146071b88 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -24,6 +24,10 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/ FILES_MATCHING PATTERN "*" PATTERN blueprints/test EXCLUDE) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ + DESTINATION ${DFHACK_DATA_DESTINATION}/patches +) + if(BUILD_TESTS) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/test/ DESTINATION "${DFHACK_DATA_DESTINATION}/data/blueprints/test") diff --git a/data/patches/README.md b/data/patches/README.md new file mode 100644 index 00000000000..599bbf47fbc --- /dev/null +++ b/data/patches/README.md @@ -0,0 +1,13 @@ +Place IDA-exported `.dif` files for use by `binpatch` in subdirectories of this +directory. Each `.dif` file must be in a subdirectory named after the full +symbol table version string. For example, for DF version 51.05, you would use +these subdirectories: + +- "v0.51.05 linux64 CLASSIC" +- "v0.51.05 linux64 ITCH" +- "v0.51.05 linux64 STEAM" +- "v0.51.05 win64 CLASSIC" +- "v0.51.05 win64 ITCH" +- "v0.51.05 win64 STEAM" + +See https://docs.dfhack.org/en/stable/docs/dev/Binpatches.html for more details. diff --git a/docs/changelog.txt b/docs/changelog.txt index 612d5f9f707..f67755852f3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,6 @@ Template for new versions: - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S - `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) -- ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip - `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes diff --git a/docs/dev/Binpatches.rst b/docs/dev/Binpatches.rst index a5665761689..d59b1f84820 100644 --- a/docs/dev/Binpatches.rst +++ b/docs/dev/Binpatches.rst @@ -50,10 +50,20 @@ directly in memory at runtime:: If the name of the patch has no extension or directory separators, the script uses :file:`hack/patches//.dif`, thus auto-selecting -the version appropriate for the currently loaded executable. +the version appropriate for the currently loaded executable. The ``df-version`` +is the version string in the loaded symbol table. For example, if you want to +make a patch for all distributed verisons of DF 51.05, you'd provide a ``dif`` +file in each of the following directories: + +- :file:`hack/patches/v0.51.05 linux64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 STEAM/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 STEAM/mypatch.dif` This is the preferred method; it's easier to debug, does not cause persistent -problems, and leaves file checksums alone. As with many other commands, users +problems, and leaves file checksums alone. As with many other commands, users can simply add it to `dfhack.init` to reapply the patch every time DF is run. diff --git a/patches/CMakeLists.txt b/patches/CMakeLists.txt deleted file mode 100644 index 37ee7c94405..00000000000 --- a/patches/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -install(DIRECTORY ${patchdirectories} - DESTINATION ${DFHACK_DATA_DESTINATION}/patches -) diff --git a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif deleted file mode 100644 index f8c95a2af6d..00000000000 --- a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011ba9eb: e8 90 -011ba9ec: c0 90 -011ba9ed: 4a 90 -011ba9ee: e6 90 -011ba9ef: fe 90 diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif deleted file mode 100644 index 17845768819..00000000000 --- a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011c605b: e8 90 -011c605c: 50 90 -011c605d: a5 90 -011c605e: e5 90 -011c605f: fe 90 diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif deleted file mode 100644 index 142e53e4fa8..00000000000 --- a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011c9c3b: e8 90 -011c9c3c: 80 90 -011c9c3d: 6a 90 -011c9c3e: e5 90 -011c9c3f: fe 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif deleted file mode 100644 index 2f5cf9e0ae0..00000000000 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -377d5d: e8 90 -377d5e: ee 90 -377d5f: 93 90 -377d60: 68 90 -377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif deleted file mode 100644 index e2546507617..00000000000 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -37af8d: e8 90 -37af8e: 1e 90 -37af8f: b7 90 -37af90: 68 90 -37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif deleted file mode 100644 index 44dd5ebb81d..00000000000 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -37d77d: e8 90 -37d77e: 6e 90 -37d77f: bb 90 -37d780: 68 90 -37d781: 00 90 From f4ff0cea2fabd579d182d6a6fb32164cd4e683bf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:30:40 -0800 Subject: [PATCH 142/919] add enable/disable hooks for widgets and update docs --- docs/changelog.txt | 1 + docs/dev/overlay-dev-guide.rst | 5 +++++ plugins/lua/overlay.lua | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f67755852f3..254f3ea9e51 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -87,6 +87,7 @@ Template for new versions: - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons - ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer - ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled +- ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable ## Removed diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 4302d5d4c8c..d87178bfff9 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -86,6 +86,11 @@ beyond your everyday `widgets.Widget `: end This allows for dynamic updates to UI overlays directly from the CLI. +- If an ``overlay_onenable()`` function is defined, it is called when the + overlay is enabled (including when the persisted state is reloaded at DF + startup). +- If an ``overlay_ondisable()`` function is defined, it is called when the + overlay is disabled. If the widget can take up a variable amount of space on the screen, and you want the widget to adjust its position according to the size of its contents, you can diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 77df7d47a4e..c93f8ed7bdd 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -171,6 +171,9 @@ local function do_enable(args, quiet, skip_save) vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry end + if db_entry.widget.overlay_onenable then + db_entry.widget.overlay_onenable() + end if not quiet then print(('enabled widget %s'):format(name)) end @@ -202,6 +205,9 @@ local function do_disable(args, quiet) active_viewscreen_widgets[vs_name] = nil end end + if db_entry.widget.overlay_ondisable then + db_entry.widget.overlay_ondisable() + end if not quiet then print(('disabled widget %s'):format(name)) end From a224c5958a7effae46e8c2dfb04e8bd823e5d5fe Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:47:41 -0800 Subject: [PATCH 143/919] reset draw context between rendering widgets --- docs/changelog.txt | 1 + plugins/lua/overlay.lua | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 254f3ea9e51..2352db036b0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,6 +65,7 @@ Template for new versions: - `createitem`: output items will now end up at look cursor if active - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself +- `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index c93f8ed7bdd..c1d1bcb4346 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -538,16 +538,16 @@ function feed_viewscreen_widgets(vs_name, vs, keys) return true end -local function _render_viewscreen_widgets(vs_name, vs, full_dc, scaled_dc) +local function _render_viewscreen_widgets(vs_name, vs) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return end local full, scaled = get_interface_rects() - full_dc = full_dc or gui.Painter.new(full) - scaled_dc = scaled_dc or gui.Painter.new(scaled) for _,db_entry in pairs(vs_widgets) do local w = db_entry.widget if (not vs or matches_focus_strings(db_entry, vs_name, vs)) and utils.getval(w.visible) then - detect_frame_change(w, function() w:render(w.fullscreen and full_dc or scaled_dc) end) + detect_frame_change(w, function() + w:render(w.fullscreen and gui.Painter.new(full) or gui.Painter.new(scaled)) + end) end end return full_dc, scaled_dc @@ -556,8 +556,8 @@ end local force_refresh function render_viewscreen_widgets(vs_name, vs) - local full_dc, scaled_dc = _render_viewscreen_widgets(vs_name, vs, nil, nil) - _render_viewscreen_widgets('all', nil, full_dc, scaled_dc) + _render_viewscreen_widgets(vs_name, vs) + _render_viewscreen_widgets('all', nil) if force_refresh then force_refresh = nil df.global.gps.force_full_display_count = 1 From eee505b4a556a7459f94016a015ddf2c156eadea Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 28 Feb 2025 18:57:14 +0000 Subject: [PATCH 144/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index af8cf85f0c2..30b3d1f1cef 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit af8cf85f0c2a772439a441120eac246123afdc6a +Subproject commit 30b3d1f1cefd5773fb9e10c49c89dba937c0f58c diff --git a/scripts b/scripts index 557ac5732e8..72b6578e90f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 557ac5732e87d62f3b7970295322ae0c5f7eae2c +Subproject commit 72b6578e90f9abc91d31faf92d7aa9d4c6f978ae From b300afcb07dc9633f8f4d45add1d9a4528068d1d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 11:21:39 -0800 Subject: [PATCH 145/919] changelog editing pass and update refs --- docs/changelog.txt | 8 ++++---- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2352db036b0..496ef9febed 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,7 +62,7 @@ Template for new versions: ## Fixes - Windows console: fix possible hang if the console returns a too-small window width (for any reason) -- `createitem`: output items will now end up at look cursor if active +- `createitem`: produced items will now end up at the look cursor position (if it is active) - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself - `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget @@ -77,16 +77,16 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments -- ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``Buildings::checkFreeTiles``: now takes a building instead of a pointer to the building extents - ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units -- ``Items::getItemBaseValue``: DF 51.06 reduced the base value of prepared meals +- ``Items::getItemBaseValue``: adjust to the reduced value of prepared meals (changed in DF 51.06) - ``Items::getValue``: magical powers now correctly contribute to item value ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons -- ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``dfhack.buildings.checkFreeTiles``: now takes a building pointer instead of an extents parameter - ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled - ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable diff --git a/library/xml b/library/xml index f5ca9135f09..f5a63e87b3d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f5ca9135f09d0b13a23e5394b563609417d568c5 +Subproject commit f5a63e87b3d96c5c435d8737e95a70151bd218c1 diff --git a/plugins/stonesense b/plugins/stonesense index 30b3d1f1cef..8fdc2fdb493 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 30b3d1f1cefd5773fb9e10c49c89dba937c0f58c +Subproject commit 8fdc2fdb4939fda074b50626381a43581b0b8479 diff --git a/scripts b/scripts index 72b6578e90f..108e5e9a377 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 72b6578e90f9abc91d31faf92d7aa9d4c6f978ae +Subproject commit 108e5e9a3774a57758404704c706361d2b1745d9 From 10e680794350bde96c4bf1a32e4193f2ccc813dc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 11:28:29 -0800 Subject: [PATCH 146/919] bump version to 51.06-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f102c355549..22f04fe0853 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.06") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 8eb563a889d7bb4117d105003a47e13f9332c7cb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 13:00:39 -0800 Subject: [PATCH 147/919] remove MakeCheese job from basic orders list defer to autocheese --- data/orders/basic.json | 21 --------------------- docs/changelog.txt | 1 + docs/plugins/orders.rst | 11 +++++++++-- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/data/orders/basic.json b/data/orders/basic.json index 688bdaa7ab0..e1cd5ac8fff 100644 --- a/data/orders/basic.json +++ b/data/orders/basic.json @@ -114,27 +114,6 @@ "job" : "CustomReaction", "reaction" : "BREW_DRINK_FROM_PLANT_GROWTH" }, - { - "amount_left" : 1, - "amount_total" : 1, - "frequency" : "Daily", - "id" : 3, - "is_active" : false, - "is_validated" : false, - "item_conditions" : - [ - { - "condition" : "AtLeast", - "flags" : - [ - "unrotten", - "milk" - ], - "value" : 2 - } - ], - "job" : "MakeCheese" - }, { "amount_left" : 1, "amount_total" : 1, diff --git a/docs/changelog.txt b/docs/changelog.txt index 496ef9febed..b4a39ba3893 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -91,6 +91,7 @@ Template for new versions: - ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable ## Removed +- `orders`: MakeCheese job removed from library/basic orders set. Please use `autocheese` instead! # 51.06-r1 diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 9ac3009af81..a56e5274669 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -145,7 +145,14 @@ This collection of orders handles basic fort necessities: You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. -Note that the jugs are specifically made out of wood. This is so, as long as you don't may any other "Tools" out of wood, you can have a stockpile just for jugs by restricting a finished goods stockpile to only take wooden tools. +These orders do not contain milking, shearing, or cheesemaking jobs since the +game does not provide sufficient order conditions. Please enable `automilk`, +`autoshear`, and `autocheese` on the DFHack `gui/control-panel` for these types +of jobs. + +Note that the jugs are specifically made out of wood. This is so, as long as +you don't may any other "Tools" out of wood, you can have a stockpile just for +jugs by restricting a finished goods stockpile to only take wooden tools. Armok's additional note: "shleggings? Yes, `shleggings `__." @@ -155,7 +162,7 @@ Armok's additional note: "shleggings? Yes, This collection creates basic items that require heat. It is separated out from ``library/basic`` to give players the opportunity to set up magma furnaces first -in order to save resources. It handles: +(if desired) in order to save resources. It handles: - charcoal (including smelting of bituminous coal and lignite) - pearlash From 989359fb41e9a160fb5a163ba70c123f7c7a4e66 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 28 Feb 2025 22:33:50 +0100 Subject: [PATCH 148/919] spectate.lua: do not write into top and bottom 4 rows over DF's UI This is a quick workaround. A proper solution would involve calculating sizes and locations of the DF's widgets, as well as visibility for some of them. Also, remove `local writer = dc`, just use `dc`. --- plugins/lua/spectate.lua | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 7be670802ff..0bf46cef2be 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -428,6 +428,8 @@ function TooltipOverlay:render_unit_banners(dc) local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) local pen = COLOR_WHITE + local _, screenHeight = dfhack.screen:getWindowSize() + local used_tiles = {} -- reverse order yields better offsets for overlapping texts for i = #units, 1, -1 do @@ -447,6 +449,10 @@ function TooltipOverlay:render_unit_banners(dc) local y = scrPos.y - 1 -- subtract 1 to move the text over the heads local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + -- do not write anything in the top rows, where DF's interface is. + -- todo: use precise rectangles + if y < 4 then goto continue end + -- to resolve overlaps, we'll mark every coordinate we write anything in, -- and then check if the new tooltip will overwrite any used coordinate. -- if it will, try the next row, to a maximum offset of 4. @@ -477,15 +483,19 @@ function TooltipOverlay:render_unit_banners(dc) -- we can't place any useful information, and will ignore it instead. if 0 <= usedAt and usedAt <= 2 then goto continue end - local writer = dc:seek(x, y + dy) + -- do not write anything over DF's interface + -- todo: use precise rectangles + if y + dy > screenHeight - 4 then goto continue end + + dc:seek(x, y + dy) local ix = 0 for _, tok in ipairs(info) do local s if type(tok) == "string" then - writer:pen(pen) + dc:pen(pen) s = tok else - writer:pen(tok.pen) + dc:pen(tok.pen) s = tok.text end @@ -496,10 +506,10 @@ function TooltipOverlay:render_unit_banners(dc) -- we want to replace it with an `_`, so we need another `- 1` s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' - writer:string(s) + dc:string(s) break -- nothing more will fit else - writer:string(s) + dc:string(s) end ix = ix + len From d161360944b8b73ffb2f2581036e34949b0d9804 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:54:15 +0000 Subject: [PATCH 149/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 108e5e9a377..e927734288e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 108e5e9a3774a57758404704c706361d2b1745d9 +Subproject commit e927734288e1676ccb6f25b96228719b79b48f13 From edbce6a8ff93375c50f62a331e3af059b3123b9f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 16:56:12 -0800 Subject: [PATCH 150/919] double quote not-commands --- docs/plugins/orders.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index a56e5274669..378881d12c7 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -146,9 +146,9 @@ You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. These orders do not contain milking, shearing, or cheesemaking jobs since the -game does not provide sufficient order conditions. Please enable `automilk`, -`autoshear`, and `autocheese` on the DFHack `gui/control-panel` for these types -of jobs. +game does not provide sufficient order conditions. Please enable ``automilk``, +``autoshear``, and `autocheese` on the DFHack `gui/control-panel` for these +types of jobs. Note that the jugs are specifically made out of wood. This is so, as long as you don't may any other "Tools" out of wood, you can have a stockpile just for From e0889266c44f8d53ebec616056b24811de593dae Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 18:17:09 -0800 Subject: [PATCH 151/919] fix changelog typo --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b4a39ba3893..c883d4868bc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S -- `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) +- `spectate`: new overlay panel that allows you to cycle through following next/previous units (regardless of whether spectate mode is enabled) - `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes From 3e96c601553689a4b9d789a60ea4b57ecfadd0e3 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 11:21:13 +0100 Subject: [PATCH 152/919] spectate.lua: add "hold-to-show" option (f.e. `ctrl`) --- plugins/lua/spectate.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 0bf46cef2be..01e4cf32ac4 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -27,6 +27,7 @@ local function get_default_state() ['prefer-new-arrivals']=true, ['tooltip-follow']=true, ['tooltip-follow-blink-milliseconds']=3000, + ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift ['tooltip-follow-job']=true, ['tooltip-follow-job-shortenings'] = { ["Store item in stockpile"] = "Store item", @@ -407,13 +408,20 @@ end function TooltipOverlay:render_unit_banners(dc) if not (config['tooltip-follow'] and AnyFollowOptionOn()) then return end - local blink_duration = config['tooltip-follow-blink-milliseconds'] - if blink_duration > 0 and not gui.blink_visible(blink_duration) then - return - end + local hold_to_show = config['tooltip-follow-hold-to-show'] + if hold_to_show and hold_to_show ~= 'none' then + if not dfhack.internal.getModifiers()[hold_to_show] then + return + end + else + local blink_duration = config['tooltip-follow-blink-milliseconds'] + if blink_duration > 0 and not gui.blink_visible(blink_duration) then + return + end - if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then - return + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end end local vp = df.global.world.viewport From 768b3d6b173f1a389cd43d8913094956a374e45f Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 11:44:34 +0100 Subject: [PATCH 153/919] add a 'toggle' command line option for any boolean value --- plugins/lua/spectate.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 01e4cf32ac4..de005ae7849 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -210,7 +210,11 @@ local function set_setting(args) -- here just in case, is already checked in the loop above qerror('missing value for ' .. path) elseif entry_type == 'boolean' then - value = argparse.boolean(value, path) + if value == 'toggle' then + value = not cfg[key] + else + value = argparse.boolean(value, path) + end elseif entry_type == 'number' then if path == 'follow-seconds' then value = argparse.positiveInt(value, path) @@ -241,7 +245,12 @@ function parse_commandline(args) if not command or command == 'status' then print_status() elseif command == 'toggle' then - do_toggle() + if #args == 0 then + do_toggle() + else + args[#args+1] = 'toggle' + set_setting(args) + end elseif command == 'set' then set_setting(args) elseif command == 'overlay' then From 0ecda6088a2d845c14a9e91c0a57267ebea7a921 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 11:53:35 +0100 Subject: [PATCH 154/919] update docs --- docs/plugins/spectate.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 89aef2b9f46..bf1349b8ad4 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -37,6 +37,7 @@ Usage spectate [status] spectate toggle spectate set [] + spectate toggle spectate overlay enable|disable Examples @@ -68,6 +69,9 @@ Examples job is displayed in the `spectate` tooltips. See the ``tooltip-follow-job-shortenings`` setting below for details. +``spectate toggle tooltip-follow`` + Toggle the follow tooltips on or off. + Settings -------- @@ -110,6 +114,12 @@ Settings If the ``spectate.tooltip`` overlay is enabled, set the tooltip's blink duration in milliseconds. Set to 0 to always show. +``tooltip-follow-hold-to-show`` (default: none) + If the ``spectate.tooltip`` overlay is enabled, specifies a modifier key + (one of none, ctrl, alt, or shift) that has to be hold to show the tooltips + that follow onscreen dwarves around the map. This supersedes the + ``tooltip-follow-blink-milliseconds`` option. + ``tooltip-follow-job`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. From ccdfaa9924fd3a4fcedc75b1cc2cae209e3f02a2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Mar 2025 07:27:27 -0800 Subject: [PATCH 155/919] document dfhack.internal.getModstate function --- docs/dev/Lua API.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index f8108e8b2ca..5cc5024abcf 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3428,6 +3428,11 @@ and are only documented here for completeness: Sets the system clipboard text from a CP437 string. Character 0x10 is interpreted as a newline instead of the usual CP437 glyph. +* ``dfhack.internal.getModifiers()`` + + Returns the state of the keyboard modifier keys in a table of string -> + boolean. The keys are ``ctrl``, ``shift``, and ``alt``. + * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` From d6dd79c95d122f5dd0313b569b02fc4497f6470b Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 23:23:40 +0100 Subject: [PATCH 156/919] Update spectate.rst --- docs/plugins/spectate.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index bf1349b8ad4..181c85a5213 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,9 +35,8 @@ Usage enable spectate spectate [status] - spectate toggle - spectate set [] spectate toggle + spectate set [] spectate overlay enable|disable Examples @@ -116,7 +115,7 @@ Settings ``tooltip-follow-hold-to-show`` (default: none) If the ``spectate.tooltip`` overlay is enabled, specifies a modifier key - (one of none, ctrl, alt, or shift) that has to be hold to show the tooltips + (one of none, ctrl, alt, or shift) that has to be held to show the tooltips that follow onscreen dwarves around the map. This supersedes the ``tooltip-follow-blink-milliseconds`` option. From 4ef8d2240f95fdc0f0be2cc23a47e45e4f95216b Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 23:24:14 +0100 Subject: [PATCH 157/919] Update spectate.rst --- docs/plugins/spectate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 181c85a5213..e57456ac832 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -35,7 +35,7 @@ Usage enable spectate spectate [status] - spectate toggle + spectate toggle [] spectate set [] spectate overlay enable|disable From 2a116f9794ba3ae6f400506c41948529f9f94bc5 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 2 Mar 2025 22:32:54 +0000 Subject: [PATCH 158/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e927734288e..eb4ea8175df 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e927734288e1676ccb6f25b96228719b79b48f13 +Subproject commit eb4ea8175df8ade8494ba8d63189021680949e5c From e944a38c3365c608b77925f4322e012a2f6e4a45 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 2 Mar 2025 17:10:18 -0600 Subject: [PATCH 159/919] fix make squad from the great reorg --- library/modules/Military.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index a5946acca5d..2e86fb958d9 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -123,7 +123,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) for (const auto& routine : routines) { - df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest[12]; + df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest(); auto &asched = schedule->month; for(int kk=0; kk < 12; kk++) From 0f782ce4a00a058c6f0f5e821b0ed6848cd7b66c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:32:45 +0000 Subject: [PATCH 160/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.31.1 → 0.31.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.1...0.31.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae54f7cf0cf..6904eb760c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.1 + rev: 0.31.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 2668082ef84f17cfa71cc096f5b3cd8e9d6a3b5a Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Mar 2025 18:15:58 -0800 Subject: [PATCH 161/919] Update Lua API.rst - Fix outdated enum names --- docs/dev/Lua API.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 5cc5024abcf..0a2fdc894ad 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1395,9 +1395,9 @@ Job module Attach a real item to this job. If the item is intended to satisfy a job_item filter, the index of that filter should be passed in ``filter_idx``; otherwise, pass ``-1``. Similarly, if you don't care where the item is inserted, pass - ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_item_ref.T_role``. + ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_role_type``. If the item needs to be brought to the job site, then the value should be - ``df.job_item_ref.T_role.Hauled``. + ``df.job_role_type.Hauled``. * ``dfhack.job.isSuitableItem(job_item, item_type, item_subtype)`` @@ -2108,7 +2108,7 @@ Items module * ``dfhack.items.moveToInventory(item,unit[,use_mode[,body_part]])`` Move the item to the unit inventory. Returns *false* if impossible. - ``use_mode`` defaults to ``df.unit_inventory_item.T_mode.Hauled``. + ``use_mode`` defaults to ``df.inv_item_role_type.Hauled``. ``body_part`` defaults to ``-1``. * ``dfhack.items.remove(item[,no_uncat])`` From 9d44e1f67e6ddaa8df3dc8ce967067a7bda8ee4c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:18:03 +0000 Subject: [PATCH 162/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index eb4ea8175df..dcd3612f4a0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit eb4ea8175df8ade8494ba8d63189021680949e5c +Subproject commit dcd3612f4a08020436fb62cdc0339fb9498e695d From 0d9f62bc4ac861fec1c6b6a8e4a5977ca0ed2c5f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:28:35 +0000 Subject: [PATCH 163/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 5bd392890e9..364f71f7454 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5bd392890e92a70622878c0513ef50e72e215435 +Subproject commit 364f71f74544c37f47a4ddfebcb9d6b9e4cdfcce diff --git a/library/xml b/library/xml index f5a63e87b3d..e97f8d46feb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f5a63e87b3d96c5c435d8737e95a70151bd218c1 +Subproject commit e97f8d46febe9156ef737eff63ffc8c09a0ad26c diff --git a/plugins/stonesense b/plugins/stonesense index 8fdc2fdb493..675aee7a6de 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8fdc2fdb4939fda074b50626381a43581b0b8479 +Subproject commit 675aee7a6de151261f83e45049bad0fcfa7bb92c diff --git a/scripts b/scripts index dcd3612f4a0..e9987efcb84 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit dcd3612f4a08020436fb62cdc0339fb9498e695d +Subproject commit e9987efcb8444dab606edea50f245166c4417b90 From 2259cd3e7f04496a0ef0b84299b14057aac4d104 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:14:55 -0800 Subject: [PATCH 164/919] fix dead link --- docs/guides/quickfort-user-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 03b26d56a54..a5b39c529b6 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1919,7 +1919,7 @@ Links DF maps. - DFHack's `buildingplan plugin ` sets material and quality constraints for quickfort-placed buildings. -- `Python Quickfort `__ is the previous, +- `Python Quickfort `__ is the previous, Python-based implementation that DFHack's quickfort script was inspired by. .. _quickfort_guide_appendix: From e40cd40b86044577ca3b05d2a3e124ea4b2fb276 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:18:20 -0800 Subject: [PATCH 165/919] check for null in fprintf --- library/VersionInfoFactory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index b1f0472c617..776362855ac 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -209,7 +209,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value); + fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +217,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value); + fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); From b01fa4d0bd78070ed1951197d579284e9df9e564 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:41:07 -0800 Subject: [PATCH 166/919] add toggle-kbd-cursor keybindings for adv mode --- data/init/dfhack.keybindings.init | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index d7ce5c86629..776a8425b36 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -41,8 +41,13 @@ keybinding add Ctrl-A@choose_start_site gui/embark-anywhere # in the game root directory keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeonmode/ViewSheets/UNIT|dungeonmode/ViewSheets/ITEM markdown +# gui/sitemap keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap +# toggle keyboard cursor +keybinding add Alt-K@dwarfmode|dungeonmode/Default|dungeonmode/Look toggle-kbd-cursor + + ###################### # dwarfmode bindings # ###################### @@ -72,9 +77,6 @@ keybinding add Ctrl-Shift-T@dwarfmode gui/teleport # apply blueprints to the map keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort -# toggle keyboard cursor -keybinding add Alt-K@dwarfmode toggle-kbd-cursor - # Stocks plugin #keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" From 94a26e1a810c3d415e3b8d1a98d9daca2ac03317 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Mar 2025 01:57:32 +0000 Subject: [PATCH 167/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e9987efcb84..43a4e221ed1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e9987efcb8444dab606edea50f245166c4417b90 +Subproject commit 43a4e221ed1144b918a3c37ee44994cd55a8c6a6 From 346b3f9a2cde40c46b8b2b0699092efb51178b03 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 01:15:06 -0800 Subject: [PATCH 168/919] add autocheese to the dreamfort recommended list --- data/blueprints/dreamfort.csv | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/blueprints/dreamfort.csv b/data/blueprints/dreamfort.csv index 72013fd487d..c1266edb6e4 100644 --- a/data/blueprints/dreamfort.csv +++ b/data/blueprints/dreamfort.csv @@ -113,6 +113,7 @@ You can save some time by setting up your settings in gui/control-panel before y - autobutcher - autobutcher target 10 10 14 2 BIRD_GOOSE - autochop +- autocheese - autofarm - autofarm threshold 150 grass_tail_pig - autofish @@ -134,6 +135,7 @@ You can save some time by setting up your settings in gui/control-panel before y """Gameplay"" subtab:" - combine - dwarfvet +- immortal-cravings - timestream - work-now "Note that if you've already started your fort and have missed the ""new fort"" trigger, you can enable these tools on the ""Enabled"" tabs instead. You can run the one-time commands (like ban-cooking all) manually from gui/launcher." @@ -283,11 +285,11 @@ Surface Walkthrough: Sieges and Prisoner Processing: Here are some tips and procedures for handling seiges -- including how to clean up afterwards! "" -"- Your ""Inside+"" burrow will automatically grow with your fort and should include only safe areas of your fort. In particular, it should not include the ""atrium"" area (where the ""siege bait"" pasture is) or the trapped hallways." +"- Your ""Inside+"" burrow will automatically grow with your fort and should include only safe areas. In particular, it does not include the ""atrium"" area (where the ""siege bait"" pasture is) or the trapped hallways." "" "- When a siege begins, set your civilian alert (attach the alert to your ""Inside+"" burrow if it isn't already) to ensure all your civilians stay out of danger. Immediately pull the lever to close the outer main gate. It is also wise to close the trade depot and inner main gate as well. That way, if enemies get past the traps, they'll have to go through the soldiers in your barracks (assuming you have a military)." "" -"- During a siege, you can use the levers to control how attackers path through the trapped corridors. If there are more enemies than cage traps, time your lever pulling so that the inner gates snap closed before your last cage trap is sprung. Then the remaining attackers will have to backtrack and go through the other trap-filled hallway. You can also choose *not* to use the trap hallways and meet the siege with your military. It's up to you!" +"- During a siege, you can use the levers to control how attackers path through the trapped corridors. If there are more enemies than cage traps, time your lever pulling so that the inner gates snap closed before your last cage trap is sprung. Then the remaining attackers will have to backtrack and go through the other trap-filled hallway. You can also choose *not* to use the trap hallways and instead meet the siege head-on with your military. It's up to you!" "" "- If your cage traps fill up, ensure your hallways are free of uncaged attackers, then close the trap hallway outer gates and open the inner gates. Clear the civilian alert and allow your dwarves to reset all the traps -- make some extra cages in preparation for this! Then re-enable the civilian alert and open the trap hallway outer gates." "" From 1e8790dd9bb6baf478b961ed435847e76b6ab129 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 8 Mar 2025 09:31:27 +0000 Subject: [PATCH 169/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 43a4e221ed1..2f1873d55fb 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 43a4e221ed1144b918a3c37ee44994cd55a8c6a6 +Subproject commit 2f1873d55fb4b7780fa02acdb0ff30569551b0d1 From 210f1c6362653a511b8b463f2d07adaad807a35d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 18:21:15 -0800 Subject: [PATCH 170/919] document github workflows and release process --- docs/dev/github-workflows.rst | 204 ++++++++++++++++++++++++++++++++++ docs/dev/index.rst | 3 + docs/dev/release-process.rst | 192 ++++++++++++++++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 docs/dev/github-workflows.rst create mode 100644 docs/dev/release-process.rst diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst new file mode 100644 index 00000000000..06249f3f180 --- /dev/null +++ b/docs/dev/github-workflows.rst @@ -0,0 +1,204 @@ +GitHub workflows +================ + +We run our continuous integration (CI) validation and our release automation +via GitHub workflows. This allows us to merge PRs with confidence that they +won't catestrophically break DFHack functionality. GitHub workflows also allow +us to quickly produce stable release builds with fewer manual steps. Reducing +manual steps for releases is important since it is easy for a person to forget +a small but impactful step and therefore produce a bad release that causes +trouble for our users. + +Background +---------- + +`GitHub workflows `_ run +on provisioned VMs in the cloud with stable environments that we specify. They +are free to use since DFHack is an open source project. They have proven to be +reliably available within a few seconds when our workflows are triggered. The +logic for the workflows is written in yaml, and the files that control our +workflows are stored in the :file:`.github/workflows/` directory in each of our +repos. Example: :source:`.github/workflows`. + +Each workflow contains metadata that specifies: +- when it `triggers `_ +- what `base environment `_ it uses (OS, pre-installed dependencies, etc.) +- what additional dependencies should be installed (if any) +- custom business logic + +Workflows run in the context of a single repo, but workflows defined in one +repo can inherit logic from workflows in other repos. All our common CI logic +is in the main DFHack/dfhack repo, but our submodules, like our ``scripts`` and +``df-structures`` repos, have CI workflows defined that inherit from the logic +in DFHack/dfhack. That way we can fix bugs and extend functionality in one +place and have it benefit the entire org tree. + +Caches +~~~~~~ + +GitHub also provides 10GB per repository for `caches `_. +We utilize the cache system to keep state between workflow runs, cache +downloads, and keep compiler output to speed up subsequent builds. Efficient +use of the cache system is a critical part of our workflow design. It allows us +to iterate on test failures in PRs in one minute instead of 20. It allows us to +put out an entire emergency release build in 5 minutes instead of 45. We have +tuned our build and test workflows to minimize spurious cache misses and keep +the fast path fast. + +Caches are namespaced by key prefixes, and we have one key prefix per build +context. For example, release builds on gcc-10 are kept in one cache namespace, +whereas test builds on gcc-10 are kept separate. MSVC release and test builds +similarly have their own namespaces. Each cache has a maximum size that is +enforced by the business logic that writes the cache data. + +In order to maintain consistency in a distributed environment, caches are +versioned. A workflow will read the latest version of the cache with its key +prefix, maybe modify the cache with new data, then write back a new version. +Caches that are not used for 2 weeks are purged from GitHub storage, but if a +repo goes over the 10GB limit, caches are deleted in LRU order until the repo +is under the storage limit again. + +CI workflows +------------ + +Build +~~~~~ + +The Build workflow is the main CI workflow. It runs on every PR and push to a +branch. The ``build.yml`` file is essentially an orchestration layer for the +logic in several other .yml files: + +- ``test.yml`` builds DFHack with the test suite enabled (but stonesense and + windows pdb files disabled) and runs the test suite. It is optimized for + speed and is intended to give PR authors quick feedback on their changes. + The test suite is executed in a real running DF game on both Linux and + Windows. The ``test`` job populates the ``test`` cache, which is used by many + other workflows for non-distributed builds. +- ``package.yml`` builds DFHack as it would be released: test suite disabled + but stonesense and windows pdb files enabled. The ``package`` job populates the ``release`` cache, which is used to build all distributed binaries. +- The ``docs`` target does a docs-only build of DFHack and reports any errors. + Doc errors would show up in the ``test`` and ``package`` builds anyway, but + the ``docs`` target runs very fast and can identify doc errors in less than + 1m. +- ``lint.yml`` runs the verification scripts in the ``ci`` directory. These + scripts check for common errors in the codebase that are not caught by the + compiler. The lint scripts are written in Python and shell script and are + intended to be run quickly and catch common errors. + +Check type sizes +~~~~~~~~~~~~~~~~ + +``check-type-sizes.yml`` is a df-structures-only workflow that checks for +changes in the sizes of types in the xml structures. It builds the +``xml-dump-type-sizes`` binary on both Linux and Windows for both the +structures in this PR and for the structures in the target merge branch. It +then runs the built binary on its native OS and compares the output. If any +type sizes have changed, the workflow generates a PR comment (via the +``comment-pr.yml`` workflow) with details. + +.. _workflows-release-automation: + +Release automation workflows +---------------------------- + +Watch DF Releases +~~~~~~~~~~~~~~~~~ + +This workflow runs every 8 minutes and checks the Steam metadata, the Itch +website, and the Bay 12 website for evidence of new releases. If a new release +is found, it generates an announcement in a private channel on the DFHack +Discord server. + +Inside the ``watch-df-releases.yml`` workflow, there are separate jobs for +watching Steam branches and watching the websites. For the Steam watcher, it +takes configuration for: + +- which branches to watch +- whether to kick off the Generate symbols workflow when a new release is found +- whether to autodeploy to Steam when the Generate symbols workflow completes + +The workflow has protections against concurrent runs, so if you suspect a new +release is out, you can manually trigger the workflow to check. If the cron +trigger happens to run the workflow at the same time, the second run will be +paused while the first run completes. + +Generate symbols +~~~~~~~~~~~~~~~~ + +This workflow can be triggered manually or by the Watch DF Releases workflow. +It downloads the specified DF version for the selected distribution platform(s) +and OS target(s), then updates the ``symbol-table`` entries in ``symbols.xml``. +If the distribution platform is Steam, it can also autodetect the DF version by +extracting the version string from the DF title screen data. + +For Linux, it always builds DFHack -- just the core library (no plugins) -- and +generates symbols via the `devel/dump-offsets` and `devel/scan-vtables` scripts. + +For Windows, we extract symbol data via static analysis, so the workflow only +builds DFHack if it needs to autodetect the DF version. + +Once the symbols.xml file is updated, the workflow commits the changes to the +specified df-structures branch and updates the xml submodule ref in the +specified DFHack/dfhack branch. If a deploy Steam branch is specified, it also +launches the Deploy to Steam workflow. + +Deploy to GitHub +~~~~~~~~~~~~~~~~ + +`github-release.yml `_ +can be triggered manually or automatically by creating a new release version +tag in git. It builds DFHack with the release configuration, packages the +aritifacts for GitHub, creates a new GitHub release, and uploads the packages +to the GitHub release page. + +It uses text in :source:`.github/release_template.md` to generate the release +notes, and appends the changelog contents for the tagged version. + +If you need to re-tag the release to fix a mistake, it will automatically run +again and replace the binaries attached to the GitHub release for the tagged +version. It will not overwrite the release notes, though, to preserve any edits +you may have made in the GitHub UI. If you *want* it to completely regenerate +the release notes, you can delete the release before you re-tag the version. + +GitHub releases end up here: https://github.com/DFHack/dfhack/releases. + +Deploy to Steam +~~~~~~~~~~~~~~~ + +`steam-deploy.yml `_ +can be triggered manually or automatically by creating a new release version +tag in git. It builds DFHack with the release configuration, packages the +aritifacts for Steam, and uploads them to the specified Steam branch. + +The workflow caches steamcmd to speed the deployment up by 30s or so. +Otherwise, steamcmd would have to be downloaded and updated every time the +workflow runs. + +Steam releases end up here: +https://partner.steamgames.com/apps/builds/2346660. The "version" you +specified for the workflow is used as the "description" for the build. + +Maintenance workflows +--------------------- + +Update submodules +~~~~~~~~~~~~~~~~~ + +`update-submodules.yml `_ +runs daily, or can be run manually as needed. It checks DFHack submodules for +new commits on the main branches and updates the submodule refs in the DFHack +develop branch. + +You generally should not run this workflow for anything other than the develop +branch, as it will overwrite any changes you have made to the submodule refs in +other branches. + +Clean up PR caches +~~~~~~~~~~~~~~~~~~ + +This workflow runs automatically whenever a PR is closed or merged. It removes +caches created for the PR so they don't take up quota. + +Note that if you merge a PR before all the workflows have completed, the caches +may be created after this workflow runs. In that case, the caches will be +orphaned and will be purged by GitHub's cache eviction policy after 2 weeks. diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 4992015d863..006fe06da14 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -18,7 +18,10 @@ These are pages relevant to people developing for DFHack. /docs/dev/overlay-dev-guide /docs/dev/Structures-intro /docs/dev/data-identity + /docs/dev/github-workflows + /docs/dev/release-process /docs/dev/Memory-research /docs/dev/Binpatches /docs/dev/Remote /docs/NEWS-dev + diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst new file mode 100644 index 00000000000..51c0ac61b03 --- /dev/null +++ b/docs/dev/release-process.rst @@ -0,0 +1,192 @@ +Release process +=============== + +This page details the process we follow for beta and stable releases. + +For documentation on the related GitHub workflows, see +`workflows-release-automation`. + +Beta release +------------ + +This process pushes a pre-release build to GitHub and Steam. It is intended to +be lower-toil than the stable release process and allows us to facilitate +frequent public testing and feedback without compromising the stability of our +"stable" releases. + +1. Run the `Update submodules `_ GitHub action on the ``develop`` branch to ensure that all submodules are up to date. +1. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". + + - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``TRUE``. + - Commit and push to ``develop`` + - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r2rc1`` for bash) + +1. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` + + - This will automatically trigger `Deploy to Steam `_ and `Deploy to GitHub `_ build workflows. + +1. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. +1. Associate release notes with the build on Steam + + - Go to the `announcement creation page `_ + - Select "A game update" + - Select "Small update / Patch notes" + - Set the "Event title" to the release version (e.g. "DFHack 51.07-r2rc1") + - Set the "Subtitle" to "DFHack pre-release (beta channel)" + - Mention release highlights in the "Summary" field + - Transcode release notes from the GitHub release into the "Event description" field + - patch notes must be in BBcode; see `converting-markdown-to-bbcode` below for how to convert our release notes to BBcode + - Click "Link to build" and select the staging branch (be sure the build has been deployed to the branch first. By the time you're done with the patch notes the build will likely be ready for you) + - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on dfhack_logo.png. Click "Upload" (even though it has already been uploaded). + - Switch to the "Publish" tab and publish! + - `Promote `_ the build to the "beta" branch (and the "testing" branch if it's newer than what is on the "testing" branch) + +1. Monitor for beta channel subscriber feedback on the Steam `community page `_ +1. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. + +Stable release +-------------- + +This process creates a stable DFHack release meant for widespread distribution. +Stable releases come in two forms: straight from ``develop`` or from a point +release branch. + +During "normal" times, we will test out new features in beta releases until we +reach a point of stability. Then, after the ``develop`` branch is feature frozen +while we polish and fix bugs, we tag a release directly from ``develop`` +``HEAD``. + +However, if we have already started committing beta features to ``develop`` and +it becomes necessary to put out a bugfix release for a problem in an +already-released stable release, then we will create a new branch from the +stable tag, cherry-pick fixes from ``develop`` onto that branch, and spin a +release from there. After the point release is published, we'll merge the +branch back into ``develop`` and remove the release branch to clean up. + +1. Triage remaining issues/PRs in the `release project `_ + + - Don't feel pressure to merge anything risky just before a stable release. That's what beta releases are for. + +1. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. +1. Ensure that CI has not failed unexpectedly on the latest online changes: + + - https://github.com/DFHack/dfhack/commits/develop + - https://github.com/DFHack/scripts/commits/master + - https://github.com/DFHack/df-structures/commits/master + +1. Update version strings in :source:`CMakeLists.txt` as appropriate + + - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``FALSE``. + - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r1``) + +1. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): + + - ``docs/changelog.txt`` + - ``scripts/changelog.txt`` + - ``library/xml/changelog.txt`` + - ``plugins/stonesense/docs/changelog.txt`` + +1. Do a top-level build to ensure the docs build cleanly +1. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) + + - ``scripts`` + - ``library/xml`` + - ``plugins/stonesense`` + +1. Commit and push changes to ``develop`` + + - Ensure that any updates you pushed to submodules are tracked in the commit to ``DFHack/develop`` + +1. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` + + - This will automatically trigger a `Deploy to Steam `_ GitHub action to the "staging" Steam branch and a `Deploy to GitHub `_ GitHub action to create a draft `release `_ from a template and attach the built artifacts. + +1. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. + + - Ensure DFHack starts DF when run from the Steam client + - Ensure the DFHack version string is accurate on the title page (should just be the release number, e.g. ``DFHack 51.07-r1``, with no git hash or warnings) + - Run `devel/check-release` + - If something goes wrong with this step, fix it, delete the tag (both from `GitHub `_ and locally (``git tag -d $RELEASE``)), re-tag, re-push, and re-test. Note that you do *not* need to remove the GitHub draft release -- the existing one will just get updated with the new tag and binaries. You *can* remove the draft release, though, if you want the release notes to get regenerated. + +1. Prep release on GitHub + + - Go to the draft `release `_ on GitHub + - Add announcements, highlights (with demo videos), etc. to the description + +1. Push develop to master (``git push origin develop:master``) + + - This will start the documentation build process and update the published "stable" docs + - Note that if this is a -r1 release, you won't be able to complete this step until a classic build is available on the Bay 12 website so the DFHack Test workflow can pass, which is a prerequisite for being able to push to ``master``. + +1. Post release notes on Steam + + - Go to the `announcement creation page `_ + - Select "A game update" + - Select "Regular update" + - Set the "Event title" to the release version (e.g. "DFHack 51.07-r1") + - Set the "Subtitle" to "DFHack stable release" + - Add list of highlights (and maybe announcements, if significant) to the "Summary" field + - Upload screenshots and demo videos via the button at the bottom of the "Previously uploaded videos" area + - Add release notes to the "Event description" field (must be in BBcode; see `converting-markdown-to-bbcode` below for how to convert our release notes to BBcode) + - Drag uploaded images/videos into their appropriate places in the announcement text (replace the GitHub URL tags, which won't work from Steam) + - If the generated release notes exceed the announcement length limits, add a link to the GitHub release page at the bottom of the announcement instead + - Click "Link to build" and select the staging branch (be sure the build has been deployed to the branch first. by the time you're done with the patch notes the build will likely be ready for you) + - the release notes will travel with the build when we promote it to other branches + - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on STABLEannouncement6.png. Click "Upload" (even though it has already been uploaded). + - Switch to the "Publish" tab and publish! + +1. Go to the `Steam builds page `_ and promote the build to the "default" branch + + - For the build that you just pushed to "staging", click the "-- Select an app branch --" drop-down and select "default" + - Click on "Preview Change" + - Commit the change (you may need to verify with 2FA) + - If the release is newer than what's on the ``beta`` and/or ``testing`` branches, set it live on those branches as well + +1. Publish the prepped GitHub release +1. Send out release announcements + + - Announce new version in r/dwarffortress. Example: https://www.reddit.com/r/dwarffortress/comments/1i3l5xl/dfhack_5015r2_released_highlights_stonesense/ + - Create the post in the Reddit web interface; the mobile app is extremely painful to use for posting + - Do an "Images & Video" post, sample title: "DFHack 51.07-r1 released! Hilights: Open legends mode directly from an active fort, Dig through warm or damp tiles without interruption, Unlink buildings from levers" + - Add the animated gifs to the post (with appropriate captions naming the relevant tool and what is being demonstrated) + - Add the "DFHack Official" flair to the post. If you're not a r/dwarffortress mod, ask Myk to do this after posting. + - After posting, add each section of the release notes as its own comment, splitting out individual announcements and highlights. This gives people the opportunity to respond directly to the portion of the release notes that interests them; it also helps us avoid size limits for comments. You can include a single still shot (.png file) per comment, but you have to switch to "Fancy Pants Editor" to do it. You can only switch editors once, or the image will get messed up (that is, the image will turn into a hyperlink to an image). Suggested procedure is to prepare the comment in markdown, switch to Fancy Pants Editor, and add images just before submitting the comment. + - Announce new version in forum thread. Example: http://www.bay12forums.com/smf/index.php?topic=164123.msg8567134#msg8567134 + - Update latest version text and link in `first post `_ (if you are not Lethosor, ping Lethosor for this) + - Announce in `#announcements `_ on DFHack Discord + - Announce in `#mod-releases `_ on Kitfox Discord + - Change the name of the release thread on Kitfox Discord to match the release version (if you are not Myk, ping Myk for this) + +1. Monitor all announcement channels for feedback and respond to questions/complaints +1. Create a `project `_ on GitHub in the DFHack org for the next release + + - Copy the previous project (3 dot menu, "Make a copy"), set DFHack as the owner + - In the new project, select settings and set the visibility to Public + - Move any remaining To Do or In Progress items from last release project to next release project + - Close project for last release + +1. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. + +.. _converting-markdown-to-bbcode: + +Converting Markdown to BBcode +----------------------------- + +Hopefully we can `automate `_ this in the future, but for now, here is the procedure: + +1. Get the markdown that you want to convert into some field on GitHub (can be a temporary text field that you then preview without saving) +1. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) +1. Right click on the rendered text and inspect the DOM +1. Copy the HTML element that contains the release notes +1. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" +1. Copy the generated BBCode out from the description field and into a text editor +1. Fix it up: + + - Remove the "How do I download DFHack?" section -- people on Steam don't need it + - Some ``

`` elements aren't converted properly and need to be rewritten with square brackets + - Any monospaced text gets HTML tags instead of BBCode ``[code]`` tags, but you can't use them either since they force newlines. ``[tt]`` isn't supported. Any ```` tags just need to be removed entirely. + - Any ``
`` and ```` tags need to be removed + +1. Copy it all back into the description field for the announcement +1. Click on "Preview event" to double check that it renders sanely +1. You're done. From c953144c94ec21cab8cf40b9085a84c136ac80b0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 03:59:13 -0800 Subject: [PATCH 171/919] formatting --- docs/dev/index.rst | 1 - docs/dev/release-process.rst | 81 +++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 006fe06da14..931b9bb006e 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -24,4 +24,3 @@ These are pages relevant to people developing for DFHack. /docs/dev/Binpatches /docs/dev/Remote /docs/NEWS-dev - diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst index 51c0ac61b03..c2aa7b1cc16 100644 --- a/docs/dev/release-process.rst +++ b/docs/dev/release-process.rst @@ -15,18 +15,20 @@ frequent public testing and feedback without compromising the stability of our "stable" releases. 1. Run the `Update submodules `_ GitHub action on the ``develop`` branch to ensure that all submodules are up to date. -1. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". + +2. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``TRUE``. - Commit and push to ``develop`` - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r2rc1`` for bash) -1. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` +3. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` - This will automatically trigger `Deploy to Steam `_ and `Deploy to GitHub `_ build workflows. -1. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. -1. Associate release notes with the build on Steam +4. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. + +5. Associate release notes with the build on Steam - Go to the `announcement creation page `_ - Select "A game update" @@ -41,8 +43,9 @@ frequent public testing and feedback without compromising the stability of our - Switch to the "Publish" tab and publish! - `Promote `_ the build to the "beta" branch (and the "testing" branch if it's newer than what is on the "testing" branch) -1. Monitor for beta channel subscriber feedback on the Steam `community page `_ -1. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. +6. Monitor for beta channel subscriber feedback on the Steam `community page `_ + +7. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. Stable release -------------- @@ -67,58 +70,60 @@ branch back into ``develop`` and remove the release branch to clean up. - Don't feel pressure to merge anything risky just before a stable release. That's what beta releases are for. -1. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. -1. Ensure that CI has not failed unexpectedly on the latest online changes: +2. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. + +3. Ensure that CI has not failed unexpectedly on the latest online changes: - https://github.com/DFHack/dfhack/commits/develop - https://github.com/DFHack/scripts/commits/master - https://github.com/DFHack/df-structures/commits/master -1. Update version strings in :source:`CMakeLists.txt` as appropriate +4. Update version strings in :source:`CMakeLists.txt` as appropriate - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``FALSE``. - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r1``) -1. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): +5. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): - ``docs/changelog.txt`` - ``scripts/changelog.txt`` - ``library/xml/changelog.txt`` - ``plugins/stonesense/docs/changelog.txt`` -1. Do a top-level build to ensure the docs build cleanly -1. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) +6. Do a top-level build to ensure the docs build cleanly + +7. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) - ``scripts`` - ``library/xml`` - ``plugins/stonesense`` -1. Commit and push changes to ``develop`` +8. Commit and push changes to ``develop`` - Ensure that any updates you pushed to submodules are tracked in the commit to ``DFHack/develop`` -1. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` +9. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` - This will automatically trigger a `Deploy to Steam `_ GitHub action to the "staging" Steam branch and a `Deploy to GitHub `_ GitHub action to create a draft `release `_ from a template and attach the built artifacts. -1. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. +10. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. - Ensure DFHack starts DF when run from the Steam client - Ensure the DFHack version string is accurate on the title page (should just be the release number, e.g. ``DFHack 51.07-r1``, with no git hash or warnings) - Run `devel/check-release` - If something goes wrong with this step, fix it, delete the tag (both from `GitHub `_ and locally (``git tag -d $RELEASE``)), re-tag, re-push, and re-test. Note that you do *not* need to remove the GitHub draft release -- the existing one will just get updated with the new tag and binaries. You *can* remove the draft release, though, if you want the release notes to get regenerated. -1. Prep release on GitHub +11. Prep release on GitHub - Go to the draft `release `_ on GitHub - Add announcements, highlights (with demo videos), etc. to the description -1. Push develop to master (``git push origin develop:master``) +12. Push develop to master (``git push origin develop:master``) - This will start the documentation build process and update the published "stable" docs - Note that if this is a -r1 release, you won't be able to complete this step until a classic build is available on the Bay 12 website so the DFHack Test workflow can pass, which is a prerequisite for being able to push to ``master``. -1. Post release notes on Steam +13. Post release notes on Steam - Go to the `announcement creation page `_ - Select "A game update" @@ -135,15 +140,16 @@ branch back into ``develop`` and remove the release branch to clean up. - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on STABLEannouncement6.png. Click "Upload" (even though it has already been uploaded). - Switch to the "Publish" tab and publish! -1. Go to the `Steam builds page `_ and promote the build to the "default" branch +14. Go to the `Steam builds page `_ and promote the build to the "default" branch - For the build that you just pushed to "staging", click the "-- Select an app branch --" drop-down and select "default" - Click on "Preview Change" - Commit the change (you may need to verify with 2FA) - If the release is newer than what's on the ``beta`` and/or ``testing`` branches, set it live on those branches as well -1. Publish the prepped GitHub release -1. Send out release announcements +15. Publish the prepped GitHub release + +16. Send out release announcements - Announce new version in r/dwarffortress. Example: https://www.reddit.com/r/dwarffortress/comments/1i3l5xl/dfhack_5015r2_released_highlights_stonesense/ - Create the post in the Reddit web interface; the mobile app is extremely painful to use for posting @@ -157,15 +163,16 @@ branch back into ``develop`` and remove the release branch to clean up. - Announce in `#mod-releases `_ on Kitfox Discord - Change the name of the release thread on Kitfox Discord to match the release version (if you are not Myk, ping Myk for this) -1. Monitor all announcement channels for feedback and respond to questions/complaints -1. Create a `project `_ on GitHub in the DFHack org for the next release +17. Monitor all announcement channels for feedback and respond to questions/complaints + +18. Create a `project `_ on GitHub in the DFHack org for the next release - Copy the previous project (3 dot menu, "Make a copy"), set DFHack as the owner - In the new project, select settings and set the visibility to Public - Move any remaining To Do or In Progress items from last release project to next release project - Close project for last release -1. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. +19. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. .. _converting-markdown-to-bbcode: @@ -175,18 +182,26 @@ Converting Markdown to BBcode Hopefully we can `automate `_ this in the future, but for now, here is the procedure: 1. Get the markdown that you want to convert into some field on GitHub (can be a temporary text field that you then preview without saving) -1. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) -1. Right click on the rendered text and inspect the DOM -1. Copy the HTML element that contains the release notes -1. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" -1. Copy the generated BBCode out from the description field and into a text editor -1. Fix it up: + +2. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) + +3. Right click on the rendered text and inspect the DOM + +4. Copy the HTML element that contains the release notes + +5. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" + +6. Copy the generated BBCode out from the description field and into a text editor + +7. Fix it up: - Remove the "How do I download DFHack?" section -- people on Steam don't need it - Some ``

`` elements aren't converted properly and need to be rewritten with square brackets - Any monospaced text gets HTML tags instead of BBCode ``[code]`` tags, but you can't use them either since they force newlines. ``[tt]`` isn't supported. Any ```` tags just need to be removed entirely. - Any ``
`` and ```` tags need to be removed -1. Copy it all back into the description field for the announcement -1. Click on "Preview event" to double check that it renders sanely -1. You're done. +8. Copy it all back into the description field for the announcement + +9. Click on "Preview event" to double check that it renders sanely + +10. You're done. From e2c3bca2695c41000d504d86a7279cc9abcb0977 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 8 Mar 2025 07:18:19 -0800 Subject: [PATCH 172/919] spelling --- docs/dev/github-workflows.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst index 06249f3f180..fa1c36b66c6 100644 --- a/docs/dev/github-workflows.rst +++ b/docs/dev/github-workflows.rst @@ -3,7 +3,7 @@ GitHub workflows We run our continuous integration (CI) validation and our release automation via GitHub workflows. This allows us to merge PRs with confidence that they -won't catestrophically break DFHack functionality. GitHub workflows also allow +won't catastrophically break DFHack functionality. GitHub workflows also allow us to quickly produce stable release builds with fewer manual steps. Reducing manual steps for releases is important since it is easy for a person to forget a small but impactful step and therefore produce a bad release that causes From 9936e584b6899b316ef4ec4d0890b02e287ec254 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 08:44:49 -0800 Subject: [PATCH 173/919] use template for project creation --- docs/dev/release-process.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst index c2aa7b1cc16..bef09be412e 100644 --- a/docs/dev/release-process.rst +++ b/docs/dev/release-process.rst @@ -167,7 +167,9 @@ branch back into ``develop`` and remove the release branch to clean up. 18. Create a `project `_ on GitHub in the DFHack org for the next release - - Copy the previous project (3 dot menu, "Make a copy"), set DFHack as the owner + - Open the `project template `_ + - Click "Use this template" + - Name the project according to the version, e.g. "51.07-r2" and click "Use template" - In the new project, select settings and set the visibility to Public - Move any remaining To Do or In Progress items from last release project to next release project - Close project for last release From 03c8001f7749b3b22e407c1761063299594953e7 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 8 Mar 2025 13:43:47 -0600 Subject: [PATCH 174/919] Fix capitalization The proper name of Stonesense should be capitalized when its usage does not refer to the case-sensitive command `stonesense` --- docs/plugins/stonesense.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 57eccee1a2c..23fbaf2b6fe 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -15,7 +15,7 @@ Usage Open the visualiser in a new window. The viewer window has read-only access to the game, and can follow the game view -or be moved independently. Configuration for stonesense can be set in the +or be moved independently. Configuration for Stonesense can be set in the ``dfhack-config/stonesense/init.txt`` file in your DF game directory. .. figure:: ../images/stonesense-roadtruss.jpg @@ -53,14 +53,14 @@ views, fog, and rotation. Here's the important section: :end-before: VALID ACTIONS: -Streaming stonesense on Windows +Streaming Stonesense on Windows ------------------------------- -If you wish to stream stonesense thru a broadcasting software such as `OBS `_ -then you may find that opening stonesense causes your main DF window to flicker -between DF and stonesense. While it is unknown exactly what causes this, a fix -does exist. Simply make sure that both DF and stonesense are using ``Window Capture`` +If you wish to stream Stonesense thru a broadcasting software such as `OBS `_ +then you may find that opening Stonesense causes your main DF window to flicker +between DF and Stonesense. While it is unknown exactly what causes this, a fix +does exist. Simply make sure that both DF and Stonesense are using ``Window Capture`` and NOT ``Game Capture``. This will stop the flickering from happening and enable -you to stream stonesense for all to enjoy. This has been tested in OBS on Windows 10 but +you to stream Stonesense for all to enjoy. This has been tested in OBS on Windows 10 but should work on Windows 11 and in `Streamlabs `_. Linux, having no ``Game Capture`` option should be unaffected by this issue. From 83e476d6542c7797dfd62d4986ecb64b6ba1f604 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 8 Mar 2025 13:53:36 -0600 Subject: [PATCH 175/919] Update follow mode stuff --- docs/plugins/stonesense.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 23fbaf2b6fe..88fc2f40c7f 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -33,10 +33,9 @@ Mouse controls are hard-coded and cannot be changed. :Scrollwheel: Move up and down :Ctrl-Scroll: Increase/decrease Z depth shown -Follow mode makes the Stonesense view follow the location of the DF -window. The offset can be adjusted by holding :kbd:`Ctrl` while using the -keyboard window movement keys. When you turn on cursor follow mode, the -Stonesense debug cursor will follow the DF cursor when the latter exists. +Follow mode makes the Stonesense view follow the location and zoom level of the DF +window. The offset can be adjusted by holding :kbd:`Alt` while using the +keyboard window movement keys. You can take screenshots with :kbd:`F5`, larger screenshots with :kbd:`Ctrl`:kbd:`F5`, and screenshot the whole map at full resolution with From da2762982ca3ae7ca69505af3cd02518f14b02f8 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 8 Mar 2025 13:59:01 -0600 Subject: [PATCH 176/919] Add some more known issues --- docs/plugins/stonesense.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 88fc2f40c7f..7127dc71b4c 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -70,6 +70,13 @@ If Stonesense gives an error saying that it can't load detail sprites used. Either open :file:`creatures/init.txt` and remove the line containing that folder, or :dffd:`use these smaller sprites <6096>`. +Sometimes if you have opened Stonesense and then resize the DF window, DF will appear to be +unresponsive. This bug is graphical only and if you hit :kbd:`Ctrl`:kbd:`Alt`:kbd:`S` and wait +a minute or so (since you can't see when the game finishes saving) the game should quicksave. + +If you have Stonesense open in a fort and want to load a new fort, you MUST close Stonesense before +loading the new fort or the game will crash. + Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. From 7bbca5f3915816d5404aa3c404dd97478cc2b108 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 01:06:49 -0800 Subject: [PATCH 177/919] redesign aquifer tap for better fill rate and update walkthrough --- data/blueprints/aquifer_tap.csv | 96 +++++++++++++++------------------ 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/data/blueprints/aquifer_tap.csv b/data/blueprints/aquifer_tap.csv index b6675cee287..dc79e523d44 100644 --- a/data/blueprints/aquifer_tap.csv +++ b/data/blueprints/aquifer_tap.csv @@ -7,9 +7,9 @@ Here's the procedure: "" 2) Dig a one-tile-wide tunnel from where you want the water to end up (e.g. your well cistern) to an area on the same z-level directly below the target light aquifer. Dig a one-tile-wide diagonal segment in this tunnel near the cistern side so water that will eventually flow through the tunnel is depressurized. "" -"3) Pause the game. From the end of that tunnel, go down one z-level, enable damp dig mode in the dig toolbar, then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). If you only have one layer of aquifer, you should end the staircase one level below the aquifer so when we dig the tap, it will extend up into the aquifer level. Your tunnel should connect to the staircase one z-level above the bottom of the staircase." +"3) Pause the game. From the end of that tunnel, go down one z-level then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). Your original tunnel should connect to the staircase one z-level above the bottom of the staircase." "" -"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint designates ramps for digging so two layers of aquifer can contribute to the water collector. It also changes the staircase tile below the tap to a ""blueprint"" tile so your miners don't dig the tap before your drainage tunnel is ready." +"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint also changes the staircase tile below the tap to a vanilla ""blueprint"" tile (shaded in blue) so your miners don't dig the tap before your drainage tunnel is ready." "" "5) You can now unpause the game. From the bottom of the staircase (the z-level below where the water will flow to your cisterns), dig a straight, one-tile wide tunnel to the closest edge of the map. This is your emergency drainage tunnel. Smooth the map edge tile and carve a fortification. The water can flow through the fortification and off the map, allowing the dwarves to dig out the aquifer tap without drowning." "" @@ -17,7 +17,7 @@ Here's the procedure: "" "7) If you want, haul away any boulders in the tunnels and/or smooth the tiles (e.g. mark them for dumping -- hotkey i-p -- and wait for them to be dumped). Enable prioritize in gui/control-panel to focus dwarves on dumping tasks and make it go faster. You won't be able to access any of this area once it fills up with water!" "" -"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders and remove the ramps if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." +"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." "8b) Sometimes, DF gets into a bad state with mining designations and miners will refuse to dig the stairway tile. If this happens to you, enter mining mode, enable the keyboard cursor if it's not already enabled (hotkey: Alt-k), highlight the undug stair designation, and run dig-now here in gui/launcher. You might also have to do this for the down stair designation in the center of the aquifer tap. Your miners should be able to handle the rest without assistance." "9) Once everything is dug out and all dwarves are out of the waterways, close the floodgate. Your cisterns will fill with water. Since the waterway to your cisterns is depressurized (due to the diagonal tunnel you dug), the cisterns will stay forever full, but will not flood." @@ -32,54 +32,42 @@ i <- cistern outlet level with diagonal tunnel to depressurize "u <- up stairs, drainage level" "" "Good luck! If done right, this method is the safest way to supply your fort with clean water." -#dig label(dig) start(13 13 center of tap) light aquifer water collector -,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, -,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdj3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3, -,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,, -#>,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, -,`,`,`,`,`,`,`,`,`,`,`,mbmdi3,`,`,`,`,`,`,`,`,`,`,`, -,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, +#dig label(dig) start(10 10 center of tap) light aquifer water collector +,,,,,,,,,,,,,,,,,, +,,,,,,,,mdd3,mdd3,mdd3,,,,,,,, +,,,,,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,,,,, +,,,,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,,,, +,,,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,,, +,,mdd3,,,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,,,mdd3,, +,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,, +,,,mdd3,mdd3,,,mdd3,mdd3,,mdd3,mdd3,,,mdd3,mdd3,,, +,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3, +,mdd3,mdd3,,,mdd3,mdd3,,,mdj3,,,mdd3,mdd3,,,mdd3,mdd3, +,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3, +,,,mdd3,mdd3,,,mdd3,mdd3,,mdd3,mdd3,,,mdd3,mdd3,,, +,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,, +,,mdd3,,,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,,,mdd3,, +,,,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,,, +,,,,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,,,, +,,,,,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,,,,, +,,,,,,,,mdd3,mdd3,mdd3,,,,,,,, +,,,,,,,,,,,,,,,,,, +#>,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,, +,,,,,,,,`,`,`,,,,,,,, +,,,,,,`,`,`,,`,`,`,,,,,, +,,,,`,,`,`,`,,`,`,`,,`,,,, +,,,`,`,`,`,,`,`,`,,`,`,`,`,,, +,,`,,,`,`,,`,`,`,,`,`,,,`,, +,,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,, +,,,`,`,,,`,`,,`,`,,,`,`,,, +,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`, +,`,`,,,`,`,,,mbmdi3,,,`,`,,,`,`, +,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`, +,,,`,`,,,`,`,,`,`,,,`,`,,, +,,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,, +,,`,,,`,`,,`,`,`,,`,`,,,`,, +,,,`,`,`,`,,`,`,`,,`,`,`,`,,, +,,,,`,,`,`,`,,`,`,`,,`,,,, +,,,,,,`,`,`,,`,`,`,,,,,, +,,,,,,,,`,`,`,,,,,,,, From 72f8afcff89a52f1d43a4423d7eee9ebdcc08308 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 01:07:08 -0800 Subject: [PATCH 178/919] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index c883d4868bc..84cf4e6eaf7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -71,6 +71,7 @@ Template for new versions: - `spectate`: player-set configuration is now stored globally instead of per-fort - `autobutcher`: treat animals on restraints as unavailable for slaughter - `stockpiles`: add property filters for brewable, millable, and processable (e.g. at a Farmer's workshop) organic materials +- `quickfort`: redesigned ``library/aquifer_tap.cav`` to improve the water fill rate ## Documentation - `stonesense-art-guide`: guide for making sprite art for Stonesense From 0c5484e5a0ff56648fa7272227f03a000ad496ff Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 9 Mar 2025 07:13:54 +0000 Subject: [PATCH 179/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 675aee7a6de..82088731a78 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 675aee7a6de151261f83e45049bad0fcfa7bb92c +Subproject commit 82088731a7843973582fd1e1fd7d8ceb6f21b2a6 From 48fdd07c4a5565daf83bd300758143784969ce9f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 11 Mar 2025 01:13:01 +0000 Subject: [PATCH 180/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 82088731a78..8087647e3da 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 82088731a7843973582fd1e1fd7d8ceb6f21b2a6 +Subproject commit 8087647e3daf50ddb479fcfba51dd2702525673f From 2b1a36e4e9761c07b0b420afd7965593284df9f0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Mar 2025 19:07:41 -0700 Subject: [PATCH 181/919] don't show suspend overlay when other ui panels are up --- docs/changelog.txt | 1 + plugins/lua/suspendmanager.lua | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 84cf4e6eaf7..73329f7d31c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself - `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget +- `suspendmanager`: in ASCII mode, building planning mode overlay now only displays when viewing the default map, reducing issues with showing through the UI ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index 594fb31155f..c15be19402b 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -140,12 +140,18 @@ if not ok then buildingplan = nil end +local function show_suspend_overlay() + return dfhack.screen.inGraphicsMode() or + dfhack.gui.matchFocusString('dwarfmode/Default', dfhack.gui.getDFViewscreen(true)) +end + SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ desc='Annotates suspended buildings with a visible marker.', viewscreens='dwarfmode', default_enabled=true, frame={w=0, h=0}, + visible=show_suspend_overlay, overlay_onupdate_max_freq_seconds=30, } From b719eefe28cdcc488ed73a2f9397ae1c9dd16f7e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 11 Mar 2025 11:05:00 -0700 Subject: [PATCH 182/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 2f1873d55fb..d399dd32278 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 2f1873d55fb4b7780fa02acdb0ff30569551b0d1 +Subproject commit d399dd322781b3f3c453d6d6c7f8cc196cdbffc5 From beb1b23e89cc66ac169d7e7fd8975f849c341910 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:11:55 +0000 Subject: [PATCH 183/919] Auto-update structures ref for 51.07 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index e97f8d46feb..19dbe08f627 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e97f8d46febe9156ef737eff63ffc8c09a0ad26c +Subproject commit 19dbe08f627aa1d0143b81b12f308873b34c6ec6 From 85d385c70cb5e8e2b427598441f75e329f2a41f3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Mar 2025 10:04:25 -0700 Subject: [PATCH 184/919] update version to 51.07 --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22f04fe0853..e67b695b4dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.06") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "51.07") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 7a449436d52b32ce33c95b89a29c8f8aae6daaab Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Mar 2025 10:10:12 -0700 Subject: [PATCH 185/919] update changelog and refs --- docs/changelog.txt | 16 ++++++++++++++++ library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 73329f7d31c..bf4eb7d112e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -53,6 +53,22 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 51.07-r1 + ## New Features - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict diff --git a/library/xml b/library/xml index 19dbe08f627..a5286b3d702 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 19dbe08f627aa1d0143b81b12f308873b34c6ec6 +Subproject commit a5286b3d70249abe4d45d9f707b826ec306a1c33 diff --git a/plugins/stonesense b/plugins/stonesense index 8087647e3da..93496b510fc 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 8087647e3daf50ddb479fcfba51dd2702525673f +Subproject commit 93496b510fcfe691f8baeb54d885a8ac811cae1e diff --git a/scripts b/scripts index d399dd32278..90431556c4c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d399dd322781b3f3c453d6d6c7f8cc196cdbffc5 +Subproject commit 90431556c4c2924c206403232ea1c24491355d07 From 7cf6799bbb919e2196e57dc06cfd13e71c0dc9e1 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:28:12 +0000 Subject: [PATCH 186/919] Auto-update structures ref for 51.08 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a5286b3d702..860017b193c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a5286b3d70249abe4d45d9f707b826ec306a1c33 +Subproject commit 860017b193c5cce1ee6c16321301b0a1d4788569 From 487df2ce4ad051015ea75e764f59372b1738b54c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 13 Mar 2025 07:55:32 -0700 Subject: [PATCH 187/919] update version --- CMakeLists.txt | 2 +- docs/changelog.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e67b695b4dd..11e95c6d8d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.07") +set(DF_VERSION "51.08") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf4eb7d112e..6327a843b74 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,11 @@ Template for new versions: ## Removed +# 51.08-r1 + +## Misc Improvements +- Compatibility update for DF 51.08 + # 51.07-r1 ## New Features From 05a35ed66a89ef2bf843c56fda75d1145fe85af0 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Mar 2025 14:46:14 +0100 Subject: [PATCH 188/919] spectate.lua: display unit activity if no job --- plugins/lua/spectate.lua | 52 ++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index de005ae7849..fd2f736d0e3 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -29,6 +29,7 @@ local function get_default_state() ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift ['tooltip-follow-job']=true, + ['tooltip-follow-activity']=true, ['tooltip-follow-job-shortenings'] = { ["Store item in stockpile"] = "Store item", }, @@ -45,6 +46,7 @@ local function get_default_state() }, ['tooltip-hover']=true, ['tooltip-hover-job']=true, + ['tooltip-hover-activity']=true, ['tooltip-hover-name']=true, ['tooltip-hover-stress']=true, ['tooltip-hover-stress-levels']={ @@ -285,33 +287,59 @@ local function GetUnitJob(unit) return job and dfhack.job.getName(job) end +-- there is no equivalent of `dfhack.job.GetName` yet +-- this function is by no means a replacement, it's +-- here to show something at least +local activityNames = { + [df.activity_entry_type.TrainingSession] = "Training Session", + [df.activity_entry_type.IndividualSkillDrill] = "Individual Skill Drill", + [df.activity_entry_type.FillServiceOrder] = "Fill Service Order", + [df.activity_entry_type.StoreObject] = "Store Object", + -- other types are single words +} +local function activity_GetName(activity) + local t = activity.type + local n = df.activity_entry_type[t] + n = activityNames[n] or n + return {text = n, pen = COLOR_LIGHTGREEN} +end + +local function GetUnitActivity(unit) + local activity = dfhack.units.getMainSocialActivity(unit) + return activity and activity_GetName(activity) +end + local function GetRelevantSettings(key) return config['tooltip-' .. key .. '-name'], config['tooltip-' .. key .. '-job'], + config['tooltip-' .. key .. '-activity'], config['tooltip-' .. key .. '-stress'], config['tooltip-' .. key .. '-stress-levels'], config['tooltip-' .. key .. '-job-shortenings'] end local function GetUnitInfoText(unit, settings_group_name) - local show_name, show_job, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) + local show_name, show_job, show_activity, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) local stress = show_stress and GetUnitStress(unit, stress_levels) or nil local name = show_name and GetUnitName(unit) or nil local job = show_job and GetUnitJob(unit) or nil if job_shortenings then job = job_shortenings[job] or job end + local activity = show_activity and GetUnitActivity(unit) or nil + + local job_or_activity = job or activity local txt = {} if stress then txt[#txt+1] = stress - if name or job then txt[#txt+1] = ' ' end + if name or job_or_activity then txt[#txt+1] = ' ' end end if name then txt[#txt+1] = name end - if job then + if job_or_activity then if name then txt[#txt+1] = ": " end - txt[#txt+1] = job + txt[#txt+1] = job_or_activity end return txt @@ -364,12 +392,6 @@ function TooltipOverlay:render(dc) TooltipOverlay.super.render(self, dc) end -local function AnyFollowOptionOn() - return config['tooltip-follow-job'] - or config['tooltip-follow-name'] - or config['tooltip-follow-stress'] -end - -- map coordinates -> interface layer coordinates local function GetScreenCoordinates(map_coord) -- -> map viewport offset @@ -415,7 +437,7 @@ local function GetString(tokens) end function TooltipOverlay:render_unit_banners(dc) - if not (config['tooltip-follow'] and AnyFollowOptionOn()) then return end + if not config['tooltip-follow'] then return end local hold_to_show = config['tooltip-follow-hold-to-show'] if hold_to_show and hold_to_show ~= 'none' then @@ -570,14 +592,8 @@ function MouseTooltip:init() } end -local function AnyHoverOptionOn() - return config['tooltip-hover-job'] - or config['tooltip-hover-name'] - or config['tooltip-hover-stress'] -end - function MouseTooltip:render(dc) - if not (config['tooltip-hover'] and AnyHoverOptionOn()) then return end + if not config['tooltip-hover'] then return end local x, y = dfhack.screen.getMousePos() if not x then return end From 3a89f6120ea724073584d52ed9e4e8e16057c038 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Mar 2025 15:35:23 +0100 Subject: [PATCH 189/919] update docs --- docs/plugins/spectate.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index e57456ac832..68d1013f644 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -123,6 +123,10 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the tooltip. +``tooltip-follow-activity`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + activity of the dwarf in the tooltip. + ``tooltip-follow-job-shortenings`` (default: "Store item in stockpile" -> "Store item") If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to shorten some job names, f.e. "Store item in stockpile" becomes "Store item". @@ -151,6 +155,10 @@ Settings If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the job of the dwarf in the hover panel. +``tooltip-hover-activity`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + activity of the dwarf in the hover panel. + ``tooltip-hover-name`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the name of the dwarf in the hover panel. From 1058f839e5cb71c7cd1961328c8fc5f177387ba0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 09:27:42 -0500 Subject: [PATCH 190/919] use `std::filesystem::path` Recode most uses of `std::string` to represent a path name to use `std::filesystem::path` instead. Eliminate most of old crunchy filesystem module with `std::filesystem` equivalent methods some modules have also cleaned to remove `using filesystem std` and/or missing or unnecessary `#include`s note: the `atime`, `ctime`, and `filetype` methods of the `Filesystem` module have been removed because they're POSIX-specific, not supported by `std::filesystem`, and unused in DFHack --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 6 - library/Core.cpp | 129 +++++++------- library/LuaApi.cpp | 25 ++- library/PlugLoad.cpp | 6 +- library/PluginManager.cpp | 60 +++---- library/Process-linux.cpp | 2 +- library/Process-windows.cpp | 4 +- library/Types.cpp | 14 +- library/include/Console.h | 5 +- library/include/Core.h | 8 +- library/include/MemAccess.h | 3 +- library/include/PluginManager.h | 6 +- library/include/Types.h | 4 +- library/include/modules/Filesystem.h | 122 ++----------- library/modules/Filesystem.cpp | 251 ++++++++++----------------- library/modules/Persistence.cpp | 20 +-- plugins/orders.cpp | 24 +-- 18 files changed, 253 insertions(+), 437 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6327a843b74..ed655242070 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: ## Documentation ## API +- `Filesystem` module: rewritten to use C++ standard library components, for better portability ## Lua diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 0a2fdc894ad..d449c8810bb 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3168,12 +3168,6 @@ unless otherwise noted. specified by ``path``, or -1 if ``path`` does not exist. This depends on the system clock and should only be used locally. -* ``dfhack.filesystem.atime(path)`` -* ``dfhack.filesystem.ctime(path)`` - - Return values vary across operating systems - return the ``st_atime`` and - ``st_ctime`` fields of a C++ stat struct, respectively. - * ``dfhack.filesystem.listdir(path)`` Lists files/directories in a directory. Returns ``{}`` if ``path`` does not exist. diff --git a/library/Core.cpp b/library/Core.cpp index be53fe58fdb..c4c611ee9d7 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -83,8 +83,14 @@ distribution. #include #include #include +#include #include +#ifdef _WIN32 +#define NOMINMAX +#include +#endif + #ifdef LINUX_BUILD #include #endif @@ -98,15 +104,15 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder); +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder); namespace DFHack { DBG_DECLARE(core,keybinding,DebugCategory::LINFO); DBG_DECLARE(core,script,DebugCategory::LINFO); -static const std::string CONFIG_PATH = "dfhack-config/"; -static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/"; +static const std::filesystem::path CONFIG_PATH{ std::filesystem::path{} / "dfhack-config" }; +static const std::filesystem::path CONFIG_DEFAULTS_PATH{ std::filesystem::path{} / "hack" / "data" / "dfhack-config-defaults" }; class MainThread { public: @@ -511,33 +517,33 @@ bool Core::removeScriptPath(std::string path) return found; } -void Core::getScriptPaths(std::vector *dest) +void Core::getScriptPaths(std::vector *dest) { std::lock_guard lock(script_path_mutex); dest->clear(); - std::string df_path = this->p->getPath() + "/"; + std::filesystem::path df_path = this->p->getPath(); for (auto & path : script_paths[0]) dest->emplace_back(path); - dest->push_back(df_path + CONFIG_PATH + "scripts"); + dest->push_back(df_path / CONFIG_PATH / "scripts"); if (df::global::world && isWorldLoaded()) { std::string save = World::ReadWorldFolder(); if (save.size()) - dest->emplace_back(df_path + "save/" + save + "/scripts"); + dest->emplace_back(df_path / "save" / save / "scripts"); } - dest->emplace_back(df_path + "hack/scripts"); + dest->emplace_back(df_path / "hack" / "scripts"); for (auto & path : script_paths[2]) dest->emplace_back(path); for (auto & path : script_paths[1]) dest->emplace_back(path); } -std::string Core::findScript(std::string name) +std::filesystem::path Core::findScript(std::string name) { - std::vector paths; + std::vector paths; getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - std::string path = *it + "/" + name; + std::filesystem::path path = *it / name; if (Filesystem::isfile(path)) return path; } @@ -546,9 +552,8 @@ std::string Core::findScript(std::string name) bool loadScriptPaths(color_ostream &out, bool silent = false) { - using namespace std; - std::string filename(CONFIG_PATH + "script-paths.txt"); - ifstream file(filename); + std::filesystem::path filename{ CONFIG_PATH / "script-paths.txt" }; + std::ifstream file(filename); if (!file) { if (!silent) @@ -560,12 +565,12 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) while (getline(file, raw)) { ++line; - istringstream ss(raw); + std::istringstream ss(raw); char ch; - ss >> skipws; + ss >> std::skipws; if (!(ss >> ch) || ch == '#') continue; - ss >> ws; // discard whitespace + ss >> std::ws; // discard whitespace std::string path; getline(ss, path); if (ch == '+' || ch == '-') @@ -857,8 +862,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if(!plug) { - std::string lua = findScript(part + ".lua"); - if (lua.size()) + std::filesystem::path lua = findScript(part + ".lua"); + if (!lua.empty()) { res = enableLuaScript(con, part, enable); } @@ -961,7 +966,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } con << parts[0]; bool builtin = is_builtin(con, parts[0]); - std::string lua_path = findScript(parts[0] + ".lua"); + std::filesystem::path lua_path = findScript(parts[0] + ".lua"); Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); if (builtin) { @@ -976,7 +981,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { con << " is a command implemented by the plugin " << plug->getName() << std::endl; } - else if (lua_path.size()) + else if (!lua_path.empty()) { con << " is a Lua script: " << lua_path << std::endl; } @@ -1123,7 +1128,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if(parts.size() == 1) { - loadScriptFile(con, parts[0], false); + loadScriptFile(con, std::filesystem::canonical(std::filesystem::path{parts[0]}), false); } else { @@ -1286,8 +1291,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (res == CR_NOT_IMPLEMENTED) { std::string completed; - std::string filename = findScript(first + ".lua"); - bool lua = filename != ""; + std::filesystem::path filename = findScript(first + ".lua"); + bool lua = !filename.empty(); if ( !lua ) { filename = findScript(first + ".rb"); } @@ -1330,17 +1335,17 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s return CR_OK; } -bool Core::loadScriptFile(color_ostream &out, std::string fname, bool silent) +bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent) { if(!silent) { INFO(script,out) << "Running script: " << fname << std::endl; std::cerr << "Running script: " << fname << std::endl; } - std::ifstream script(fname.c_str()); + std::ifstream script(fname); if ( !script.good() ) { if(!silent) - out.printerr("Error loading script: %s\n", fname.c_str()); + out.printerr("Error loading script: %s\n", fname.string().c_str()); return false; } std::string command; @@ -1379,11 +1384,11 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // load baseline defaults - core->loadScriptFile(out, CONFIG_PATH + "init/default.dfhack.init", false); + core->loadScriptFile(out, CONFIG_PATH / "init" / "default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); - loadScriptFiles(core, out, prefixes, CONFIG_PATH + "init"); + loadScriptFiles(core, out, prefixes, CONFIG_PATH / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); @@ -1407,7 +1412,7 @@ void fInitthread(void * iodata) // A thread function... for the interactive console. void fIOthread(void * iodata) { - static const std::string HISTORY_FILE = CONFIG_PATH + "dfhack.history"; + static const std::filesystem::path HISTORY_FILE = CONFIG_PATH / "dfhack.history"; IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; @@ -1455,7 +1460,7 @@ void fIOthread(void * iodata) { // a proper, non-empty command was entered main_history.add(command); - main_history.save(HISTORY_FILE.c_str()); + main_history.save(HISTORY_FILE); } auto rv = core->runCommand(con, command); @@ -1534,13 +1539,9 @@ void Core::fatal (std::string output, const char * title) } } -std::string Core::getHackPath() +std::filesystem::path Core::getHackPath() { -#ifdef LINUX_BUILD - return p->getPath() + "/hack/"; -#else - return p->getPath() + "\\hack\\"; -#endif + return p->getPath() / "hack"; } df::viewscreen * Core::getTopViewscreen() { @@ -1781,8 +1782,8 @@ bool Core::InitSimulationThread() con.printerr("Failed to create config directory: '%s'\n", CONFIG_PATH.c_str()); // copy over default config files if necessary - std::map config_files; - std::map default_config_files; + std::map config_files; + std::map default_config_files; if (Filesystem::listdir_recursive(CONFIG_PATH, config_files, 10, false) != 0) con.printerr("Failed to list directory: '%s'\n", CONFIG_PATH.c_str()); else if (Filesystem::listdir_recursive(CONFIG_DEFAULTS_PATH, default_config_files, 10, false) != 0) @@ -1794,7 +1795,7 @@ bool Core::InitSimulationThread() // skip over files if (!entry.second) continue; - std::string dirname = CONFIG_PATH + entry.first; + std::filesystem::path dirname = CONFIG_PATH / entry.first; if (!Filesystem::mkdir_recursive(dirname)) con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); } @@ -1804,12 +1805,12 @@ bool Core::InitSimulationThread() // skip over directories if (entry.second) continue; - std::string filename = entry.first; + std::filesystem::path filename = entry.first; if (!config_files.count(filename)) { - std::string src_file = CONFIG_DEFAULTS_PATH + filename; + std::filesystem::path src_file = CONFIG_DEFAULTS_PATH / filename; if (!Filesystem::isfile(src_file)) continue; - std::string dest_file = CONFIG_PATH + filename; + std::filesystem::path dest_file = CONFIG_PATH / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); if (!src.good() || !dest.good()) { @@ -2185,42 +2186,30 @@ void Core::onUpdate(color_ostream &out) perf_counters.incCounter(perf_counters.update_lua_ms, step_start_ms); } -void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { - std::vector files; +void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { + std::vector files; DFHack::Filesystem::listdir(folder, files); - for ( size_t a = 0; a < files.size(); a++ ) { - if ( prefix.length() > files[a].length() ) - continue; - if ( suffix.length() > files[a].length() ) - continue; - if ( files[a].compare(0, prefix.length(), prefix) != 0 ) - continue; - if ( files[a].compare(files[a].length()-suffix.length(), suffix.length(), suffix) != 0 ) - continue; - result.push_back(files[a]); + for ( auto f : files) { + if (f.stem().string().starts_with(prefix) && f.extension() == suffix) + result.push_back(f); } return; } -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder) { +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; - std::vector scriptFiles; + std::vector scriptFiles; for ( size_t a = 0; a < prefix.size(); a++ ) { getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); } std::sort(scriptFiles.begin(), scriptFiles.end(), - [&](const std::string &a, const std::string &b) { - std::string a_base = a.substr(0, a.size() - suffix.size()); - std::string b_base = b.substr(0, b.size() - suffix.size()); - return a_base < b_base; + [&](const std::filesystem::path &a, const std::filesystem::path &b) { + return a < b; }); size_t result = 0; for ( size_t a = 0; a < scriptFiles.size(); a++ ) { result++; - std::string path = ""; - if (folder != ".") - path = folder + "/"; - core->loadScriptFile(out, path + scriptFiles[a], false); + core->loadScriptFile(out, folder / scriptFiles[a], false); } return result; } @@ -2272,16 +2261,16 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve if (!df::global::world) return; - std::string rawFolder = !isWorldLoaded() ? "" : "save/" + World::ReadWorldFolder() + "/init"; + std::filesystem::path rawFolder = !isWorldLoaded() ? std::filesystem::path{} : std::filesystem::path{} / "save" / World::ReadWorldFolder() / "init"; auto i = table.find(event); if ( i != table.end() ) { const std::vector& set = i->second; // load baseline defaults - this->loadScriptFile(out, CONFIG_PATH + "init/default." + set[0] + ".init", false); + this->loadScriptFile(out, CONFIG_PATH / "init" / ("default." + set[0] + ".init"), false); - loadScriptFiles(this, out, set, CONFIG_PATH + "init"); + loadScriptFiles(this, out, set, CONFIG_PATH / "init"); loadScriptFiles(this, out, set, rawFolder); } @@ -2295,7 +2284,7 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve } else if (it->save_specific && isWorldLoaded()) { - loadScriptFile(out, rawFolder + it->path, false); + loadScriptFile(out, rawFolder / it->path, false); } } } @@ -2366,7 +2355,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) strftime(timebuf, sizeof(timebuf), "[%Y-%m-%dT%H:%M:%S%z] ", timeinfo); evtlog << timebuf; evtlog << "DFHack " << Version::git_description() << " on " << ostype << "; "; - evtlog << "cwd md5: " << md5w.getHashFromString(getHackPath()).substr(0, 10) << "; "; + evtlog << "cwd md5: " << md5w.getHashFromString(getHackPath().string().c_str()).substr(0, 10) << "; "; evtlog << "save: " << world->cur_savegame.save_dir << "; "; evtlog << sc_event_name(event) << "; "; if (gametype) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 637df32211b..d2162a33d14 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -115,6 +115,7 @@ distribution. #include #include #include +#include namespace DFHack { DBG_DECLARE(core, luaapi, DebugCategory::LINFO); @@ -1351,8 +1352,8 @@ static string getArchitectureName() static string getDFVersion() { return Core::getInstance().vinfo->getVersion(); } static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); } -static string getDFPath() { return Core::getInstance().p->getPath(); } -static string getHackPath() { return Core::getInstance().getHackPath(); } +static std::filesystem::path getDFPath() { return Core::getInstance().p->getPath(); } +static std::filesystem::path getHackPath() { return Core::getInstance().getHackPath(); } static bool isWorldLoaded() { return Core::getInstance().isWorldLoaded(); } static bool isMapLoaded() { return Core::getInstance().isMapLoaded(); } @@ -3153,8 +3154,6 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, exists), WRAPM(Filesystem, isfile), WRAPM(Filesystem, isdir), - WRAPM(Filesystem, atime), - WRAPM(Filesystem, ctime), WRAPM(Filesystem, mtime), {NULL, NULL} }; @@ -3163,7 +3162,7 @@ static int filesystem_listdir(lua_State *L) { luaL_checktype(L,1,LUA_TSTRING); string dir=lua_tostring(L,1); - vector files; + vector files; int err = DFHack::Filesystem::listdir(dir, files); if (err) { @@ -3176,7 +3175,7 @@ static int filesystem_listdir(lua_State *L) for(size_t i=0;i= 3 && !lua_isnil(L, 3)) include_prefix = lua_toboolean(L, 3); - std::map files; + std::map files; int err = DFHack::Filesystem::listdir_recursive(dir, files, depth, include_prefix); if (err != 0 && err != -1) { lua_pushnil(L); @@ -3207,7 +3206,7 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - lua_pushstring(L, (it->first).c_str()); + lua_pushstring(L, (it->first).string().c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); @@ -3947,12 +3946,12 @@ static int internal_getScriptPaths(lua_State *L) { int i = 1; lua_newtable(L); - vector paths; + vector paths; Core::getInstance().getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { lua_pushinteger(L, i++); - lua_pushstring(L, it->c_str()); + lua_pushstring(L, it->string().c_str()); lua_settable(L, -3); } return 1; @@ -3961,9 +3960,9 @@ static int internal_getScriptPaths(lua_State *L) static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); - string path = Core::getInstance().findScript(name); - if (path.size()) - lua_pushstring(L, path.c_str()); + std::filesystem::path path = Core::getInstance().findScript(name); + if (!path.empty()) + lua_pushstring(L, path.string().c_str()); else lua_pushnil(L); return 1; diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 059e6aef953..1cafc791f53 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -18,7 +18,7 @@ #define global_search_handle() GetModuleHandle(nullptr) #define get_function_address(plugin, function) GetProcAddress((HMODULE)plugin, function) #define clear_error() -#define load_library(fn) LoadLibrary(fn) +#define load_library(fn) LoadLibraryW(fn.c_str()) #define close_library(handle) (!(FreeLibrary((HMODULE)handle))) #else #include @@ -33,7 +33,7 @@ #define global_search_handle() (RTLD_DEFAULT) #define get_function_address(plugin, function) dlsym((void*)plugin, function) #define clear_error() dlerror() -#define load_library(fn) dlopen(fn, RTLD_NOW | RTLD_LOCAL); +#define load_library(fn) dlopen(fn.c_str(), RTLD_NOW | RTLD_LOCAL); #define close_library(handle) dlclose((void*)handle) #endif @@ -71,7 +71,7 @@ namespace DFHack } } - DFLibrary * OpenPlugin (const char * filename) + DFLibrary * OpenPlugin (std::filesystem::path filename) { clear_error(); DFLibrary* ret = (DFLibrary*)load_library(filename); diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 458c0377c24..98a9eaeec40 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -48,7 +48,8 @@ using namespace DFHack; #include #include #include -using namespace std; + +using std::string; #include @@ -60,14 +61,14 @@ using namespace std; static const string plugin_suffix = ".plug.dll"; #endif -static string getPluginPath() +static std::filesystem::path getPluginPath() { - return Core::getInstance().getHackPath() + "plugins/"; + return Core::getInstance().getHackPath() / "plugins"; } -static string getPluginPath (std::string name) +static std::filesystem::path getPluginPath (std::string name) { - return getPluginPath() + name + plugin_suffix; + return getPluginPath() / (name + plugin_suffix); } struct Plugin::RefLock @@ -175,7 +176,7 @@ struct Plugin::LuaEvent : public Lua::Event::Owner { } }; -Plugin::Plugin(Core * core, const std::string & path, +Plugin::Plugin(Core * core, const std::filesystem::path& path, const std::string &name, PluginManager * pm) :path(path), name(name), @@ -246,7 +247,7 @@ bool Plugin::load(color_ostream &con) CoreSuspender suspend; // open the library, etc fprintf(stderr, "loading plugin %s\n", name.c_str()); - DFLibrary * plug = OpenPlugin(path.c_str()); + DFLibrary * plug = OpenPlugin(path); if(!plug) { RefAutolock lock(access); @@ -311,8 +312,8 @@ bool Plugin::load(color_ostream &con) { std::string msg = stl_sprintf("Warning: Plugin %s compiled for DFHack %s, running DFHack %s\n", *plug_name, plug_git_desc, dfhack_git_desc); - con << msg << flush; - cerr << msg << flush; + con << msg << std::flush; + std::cerr << msg << std::flush; } } else @@ -895,7 +896,7 @@ bool PluginManager::addPlugin(string name) Core::printerr("Plugin already exists: %s\n", name.c_str()); return false; } - string path = getPluginPath(name); + std::filesystem::path path = getPluginPath(name); if (!Filesystem::isfile(path)) { Core::printerr("Plugin does not exist: %s\n", name.c_str()); @@ -906,16 +907,17 @@ bool PluginManager::addPlugin(string name) return true; } -vector PluginManager::listPlugins() +std::vector PluginManager::listPlugins() { - vector results; - vector files; + std::vector results; + std::vector files; Filesystem::listdir(getPluginPath(), files); for (auto file = files.begin(); file != files.end(); ++file) { - if (hasEnding(*file, plugin_suffix)) + string fname = file->filename().string(); + if (hasEnding(file->filename().string(), plugin_suffix)) { - string shortname = file->substr(0, file->find(plugin_suffix)); + string shortname = fname.substr(0, fname.find(plugin_suffix)); results.push_back(shortname); } } @@ -924,7 +926,7 @@ vector PluginManager::listPlugins() void PluginManager::refresh() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; auto files = listPlugins(); for (auto f = files.begin(); f != files.end(); ++f) { @@ -935,7 +937,7 @@ void PluginManager::refresh() bool PluginManager::load (const string &name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name] && !addPlugin(name)) return false; Plugin *p = (*this)[name]; @@ -949,7 +951,7 @@ bool PluginManager::load (const string &name) bool PluginManager::loadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; auto files = listPlugins(); bool ok = true; // load all plugins in hack/plugins @@ -963,7 +965,7 @@ bool PluginManager::loadAll() bool PluginManager::unload (const string &name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) { Core::printerr("Plugin does not exist: %s\n", name.c_str()); @@ -974,7 +976,7 @@ bool PluginManager::unload (const string &name) bool PluginManager::unloadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; bool ok = true; // only try to unload plugins that are in all_plugins for (auto it = begin(); it != end(); ++it) @@ -989,7 +991,7 @@ bool PluginManager::reload (const string &name) { // equivalent to "unload(name); load(name);" if plugin is recognized, // "load(name);" otherwise - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) return load(name); if (!unload(name)) @@ -999,7 +1001,7 @@ bool PluginManager::reload (const string &name) bool PluginManager::reloadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; bool ok = true; if (!unloadAll()) ok = false; @@ -1010,8 +1012,8 @@ bool PluginManager::reloadAll() Plugin *PluginManager::getPluginByCommand(const std::string &command) { - lock_guard lock{*cmdlist_mutex}; - map ::iterator iter = command_map.find(command); + std::lock_guard lock{*cmdlist_mutex}; + std::map ::iterator iter = command_map.find(command); if (iter != command_map.end()) return iter->second; else @@ -1059,8 +1061,8 @@ void PluginManager::OnStateChange(color_ostream &out, state_change_event event) void PluginManager::registerCommands( Plugin * p ) { - lock_guard lock{*cmdlist_mutex}; - vector & cmds = p->commands; + std::lock_guard lock{*cmdlist_mutex}; + std::vector & cmds = p->commands; for (size_t i = 0; i < cmds.size();i++) { std::string name = cmds[i].name; @@ -1076,8 +1078,8 @@ void PluginManager::registerCommands( Plugin * p ) void PluginManager::unregisterCommands( Plugin * p ) { - lock_guard lock{*cmdlist_mutex}; - vector & cmds = p->commands; + std::lock_guard lock{*cmdlist_mutex}; + std::vector & cmds = p->commands; for(size_t i = 0; i < cmds.size();i++) { command_map.erase(cmds[i].name); @@ -1126,7 +1128,7 @@ void PluginManager::doLoadSiteData(color_ostream &out) Plugin *PluginManager::operator[] (std::string name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (all_plugins.find(name) == all_plugins.end()) { if (Filesystem::isfile(getPluginPath(name))) diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index a3fa9f2d67d..ce64cc113b5 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -195,7 +195,7 @@ uint32_t Process::getTickCount() return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); } -string Process::getPath() +std::filesystem::path Process::getPath() { return Filesystem::get_initial_cwd(); } diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 79bbea80025..fe8c5d591ab 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -29,6 +29,7 @@ distribution. #include #include #include +#include #include "Error.h" #include "Internal.h" @@ -53,7 +54,6 @@ using std::vector; using std::endl; using std::cerr; - namespace DFHack { class PlatformSpecific @@ -382,7 +382,7 @@ uint32_t Process::getTickCount() return GetTickCount(); } -string Process::getPath() +std::filesystem::path Process::getPath() { HMODULE hmod; DWORD junk; diff --git a/library/Types.cpp b/library/Types.cpp index ffed86eb591..1dab657c1c3 100644 --- a/library/Types.cpp +++ b/library/Types.cpp @@ -33,24 +33,16 @@ distribution. #include "df/general_ref.h" #include "df/specific_ref.h" -#ifndef LINUX_BUILD - #include - #include "wdirent.h" -#else - #include - #include - #include - #include -#endif - #include #include +#include #include #include +#include -int DFHack::getdir(std::string dir, std::vector &files) +int DFHack::getdir(std::filesystem::path dir, std::vector &files) { return DFHack::Filesystem::listdir(dir, files); } diff --git a/library/include/Console.h b/library/include/Console.h index a4ca8c5452c..4397c759545 100644 --- a/library/include/Console.h +++ b/library/include/Console.h @@ -33,6 +33,7 @@ distribution. #include #include #include +#include namespace DFHack { @@ -43,7 +44,7 @@ namespace DFHack { this->capacity = capacity; } - bool load (const char * filename) + bool load (std::filesystem::path filename) { std::string reader; std::ifstream infile(filename); @@ -58,7 +59,7 @@ namespace DFHack } return true; } - bool save (const char * filename) + bool save (std::filesystem::path filename) { if (!history.size()) return true; diff --git a/library/include/Core.h b/library/include/Core.h index 556e5e1b03d..6794a6b28fd 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -175,13 +175,13 @@ namespace DFHack command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream &out, const std::string &command); - bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false); + bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); bool addScriptPath(std::string path, bool search_before = false); bool setModScriptPaths(const std::vector &mod_script_paths); bool removeScriptPath(std::string path); - std::string findScript(std::string name); - void getScriptPaths(std::vector *dest); + std::filesystem::path findScript(std::string name); + void getScriptPaths(std::vector *dest); bool getSuppressDuplicateKeyboardEvents(); void setSuppressDuplicateKeyboardEvents(bool suppress); @@ -201,7 +201,7 @@ namespace DFHack std::map> ListAliases(); std::string GetAliasCommand(const std::string &name, bool ignore_params = false); - std::string getHackPath(); + std::filesystem::path getHackPath(); bool isWorldLoaded() { return (last_world_data_ptr != NULL); } bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index bcbab75bd3a..95f7e256d17 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -33,6 +33,7 @@ distribution. #include #include #include +#include #include "VersionInfo.h" @@ -262,7 +263,7 @@ namespace DFHack /// get the DF Process ID int getPID(); /// get the DF Process FilePath - std::string getPath(); + std::filesystem::path getPath(); /// Adjust between in-memory and in-file image offset int adjustOffset(int offset, bool to_file = false); diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 3749ff95489..50b9b0447d1 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -64,7 +64,7 @@ namespace DFHack // DFLibrary* that can be used to resolve global names extern DFLibrary* GLOBAL_NAMES; // Open a plugin library - DFHACK_EXPORT DFLibrary * OpenPlugin (const char * filename); + DFHACK_EXPORT DFLibrary * OpenPlugin (std::filesystem::path filename); // find a symbol inside plugin DFHACK_EXPORT void * LookupPlugin (DFLibrary * plugin ,const char * function); // Close a plugin library. returns true on success, false on failure @@ -143,7 +143,7 @@ namespace DFHack struct RefAutoinc; friend class PluginManager; friend class RPCService; - Plugin(DFHack::Core* core, const std::string& filepath, + Plugin(DFHack::Core* core, const std::filesystem::path& filepath, const std::string &plug_name, PluginManager * pm); ~Plugin(); command_result on_update(color_ostream &out); @@ -202,7 +202,7 @@ namespace DFHack RefLock * access; std::vector commands; std::vector services; - std::string path; + std::filesystem::path path; std::string name; DFLibrary * plugin_lib; PluginManager * parent; diff --git a/library/include/Types.h b/library/include/Types.h index 53b6aee18ae..84408ecf2ab 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -26,6 +26,8 @@ distribution. #pragma once #include +#include +#include #include "Export.h" @@ -118,7 +120,7 @@ namespace DFHack return rect.second - rect.first + df::coord2d(1,1); } - DFHACK_EXPORT int getdir(std::string dir, std::vector &files); + DFHACK_EXPORT int getdir(std::filesystem::path dir, std::vector &files); DFHACK_EXPORT bool hasEnding (std::string const &fullString, std::string const &ending); DFHACK_EXPORT df::general_ref *findRef(std::vector &vec, df::general_ref_type type); diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 8e7bab9b200..8c376a7a01b 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -50,123 +50,29 @@ SOFTWARE. #include #include -#ifndef _WIN32 - #ifndef _AIX - #define _FILE_OFFSET_BITS 64 /* Linux, Solaris and HP-UX */ - #else - #define _LARGE_FILES 1 /* AIX */ - #endif -#endif +#include -#ifndef _LARGEFILE64_SOURCE - #define _LARGEFILE64_SOURCE -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 - #include - #define NOMINMAX - #include - #include - #include - #ifdef __BORLANDC__ - #include - #else - #include - #endif - #include - #include "wdirent.h" -#else - #include - #include - #include - #include - #include -#endif - -#ifdef _WIN32 - #ifdef __BORLANDC__ - #define lfs_setmode(L,file,m) ((void)L, setmode(_fileno(file), m)) - #define STAT_STRUCT struct stati64 - #else - #define lfs_setmode(L,file,m) ((void)L, _setmode(_fileno(file), m)) - #define STAT_STRUCT struct _stati64 - #endif - #define STAT_FUNC _stati64 - #define LSTAT_FUNC STAT_FUNC -#else - #define _O_TEXT 0 - #define _O_BINARY 0 - #define lfs_setmode(L,file,m) ((void)L, (void)file, (void)m, 0) - #define STAT_STRUCT struct stat - #define STAT_FUNC stat - #define LSTAT_FUNC lstat -#endif - -#ifdef _WIN32 - #ifndef S_ISDIR - #define S_ISDIR(mode) (mode&_S_IFDIR) - #endif - #ifndef S_ISREG - #define S_ISREG(mode) (mode&_S_IFREG) - #endif - #ifndef S_ISLNK - #define S_ISLNK(mode) (0) - #endif - #ifndef S_ISSOCK - #define S_ISSOCK(mode) (0) - #endif - #ifndef S_ISCHR - #define S_ISCHR(mode) (mode&_S_IFCHR) - #endif - #ifndef S_ISBLK - #define S_ISBLK(mode) (0) - #endif -#endif - -enum _filetype { - FILETYPE_NONE = -2, - FILETYPE_UNKNOWN = -1, - FILETYPE_FILE = 1, - FILETYPE_DIRECTORY, - FILETYPE_LINK, - FILETYPE_SOCKET, - FILETYPE_NAMEDPIPE, - FILETYPE_CHAR_DEVICE, - FILETYPE_BLOCK_DEVICE -}; namespace DFHack { namespace Filesystem { DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::string path); - DFHACK_EXPORT std::string getcwd (); + DFHACK_EXPORT bool chdir (std::filesystem::path path); + DFHACK_EXPORT std::filesystem::path getcwd (); DFHACK_EXPORT bool restore_cwd (); - DFHACK_EXPORT std::string get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::string path); + DFHACK_EXPORT std::filesystem::path get_initial_cwd (); + DFHACK_EXPORT bool mkdir (std::filesystem::path path); // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::string path); - DFHACK_EXPORT bool rmdir (std::string path); - DFHACK_EXPORT bool stat (std::string path, STAT_STRUCT &info); - DFHACK_EXPORT bool exists (std::string path); - DFHACK_EXPORT _filetype filetype (std::string path); - DFHACK_EXPORT bool isfile (std::string path); - DFHACK_EXPORT bool isdir (std::string path); - DFHACK_EXPORT int64_t atime (std::string path); - DFHACK_EXPORT int64_t ctime (std::string path); - DFHACK_EXPORT int64_t mtime (std::string path); - DFHACK_EXPORT int listdir (std::string dir, std::vector &files); + DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path); + DFHACK_EXPORT bool rmdir (std::filesystem::path path); + DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info); + DFHACK_EXPORT bool exists (std::filesystem::path path); + DFHACK_EXPORT bool isfile (std::filesystem::path path); + DFHACK_EXPORT bool isdir (std::filesystem::path path); + DFHACK_EXPORT std::time_t mtime (std::filesystem::path path); + DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files); // set include_prefix to false to prevent dir from being prepended to // paths returned in files - DFHACK_EXPORT int listdir_recursive (std::string dir, std::map &files, + DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, int depth = 10, bool include_prefix = true); } } diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 2c275ceba84..8b316d34c85 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -47,13 +47,15 @@ SOFTWARE. #include #include +#include +#include #include "modules/Filesystem.h" using namespace DFHack; static bool initialized = false; -static std::string initial_cwd; +static std::filesystem::path initial_cwd; void Filesystem::init () { @@ -64,24 +66,23 @@ void Filesystem::init () } } -bool Filesystem::chdir (std::string path) +bool Filesystem::chdir (std::filesystem::path path) { Filesystem::init(); - return ::chdir(path.c_str()) == 0; + try + { + std::filesystem::current_path(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -std::string Filesystem::getcwd () +std::filesystem::path Filesystem::getcwd () { - char *path; - char buf[FILENAME_MAX]; - std::string result = ""; -#ifdef _WIN32 - if ((path = ::_getcwd(buf, FILENAME_MAX)) != NULL) -#else - if ((path = ::getcwd(buf, FILENAME_MAX)) != NULL) -#endif - result = buf; - return result; + return std::filesystem::current_path(); } bool Filesystem::restore_cwd () @@ -89,196 +90,124 @@ bool Filesystem::restore_cwd () return Filesystem::chdir(initial_cwd); } -std::string Filesystem::get_initial_cwd () +std::filesystem::path Filesystem::get_initial_cwd () { Filesystem::init(); return initial_cwd; } -bool Filesystem::mkdir (std::string path) +bool Filesystem::mkdir (std::filesystem::path path) { - int fail; -#ifdef _WIN32 - fail = ::_mkdir(path.c_str()); -#else - fail = ::mkdir(path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | - S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); -#endif - return fail == 0; -} - -static bool mkdir_recursive_impl (std::string path) -{ - size_t last_slash = path.find_last_of("/"); - if (last_slash != std::string::npos) + try { - std::string parent_path = path.substr(0, last_slash); - bool parent_exists = mkdir_recursive_impl(parent_path); - if (!parent_exists) - { - return false; - } + std::filesystem::create_directory(path); + return true; } - return (Filesystem::mkdir(path) || errno == EEXIST) && Filesystem::isdir(path); -} - -bool Filesystem::mkdir_recursive (std::string path) -{ -#ifdef _WIN32 - // normalize to forward slashes - std::replace(path.begin(), path.end(), '\\', '/'); -#endif - - if (path.size() > FILENAME_MAX) + catch (std::filesystem::filesystem_error&) { - // path too long return false; } - - return mkdir_recursive_impl(path); } -bool Filesystem::rmdir (std::string path) +bool Filesystem::mkdir_recursive (std::filesystem::path path) { - int fail; -#ifdef _WIN32 - fail = ::_rmdir(path.c_str()); -#else - fail = ::rmdir(path.c_str()); -#endif - return fail == 0; -} - -#ifdef _WIN32 -_filetype mode2type (unsigned short mode) { -#else -_filetype mode2type (mode_t mode) { -#endif - if (S_ISREG(mode)) - return FILETYPE_FILE; - else if (S_ISDIR(mode)) - return FILETYPE_DIRECTORY; - else if (S_ISLNK(mode)) - return FILETYPE_LINK; - else if (S_ISSOCK(mode)) - return FILETYPE_SOCKET; - else if (S_ISCHR(mode)) - return FILETYPE_CHAR_DEVICE; - else if (S_ISBLK(mode)) - return FILETYPE_BLOCK_DEVICE; - else - return FILETYPE_UNKNOWN; + try + { + std::filesystem::create_directories(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -bool Filesystem::stat (std::string path, STAT_STRUCT &info) +bool Filesystem::rmdir (std::filesystem::path path) { - return (STAT_FUNC(path.c_str(), &info)) == 0; + try + { + std::filesystem::remove(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -bool Filesystem::exists (std::string path) +bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) { - STAT_STRUCT info; - return Filesystem::stat(path, info); + try + { + info = std::filesystem::status(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -_filetype Filesystem::filetype (std::string path) +bool Filesystem::exists (std::filesystem::path path) { - STAT_STRUCT info; - if (!Filesystem::stat(path, info)) - return FILETYPE_NONE; - return mode2type(info.st_mode); + return std::filesystem::exists(path); } -bool Filesystem::isfile (std::string path) +bool Filesystem::isfile(std::filesystem::path path) { - return Filesystem::exists(path) && Filesystem::filetype(path) == FILETYPE_FILE; + return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); } -bool Filesystem::isdir (std::string path) +bool Filesystem::isdir (std::filesystem::path path) { - return Filesystem::exists(path) && Filesystem::filetype(path) == FILETYPE_DIRECTORY; -} - -#define DEFINE_STAT_TIME_WRAPPER(attr) \ -int64_t Filesystem::attr (std::string path) \ -{ \ - STAT_STRUCT info; \ - if (!Filesystem::stat(path, info)) \ - return -1; \ - return (int64_t)info.st_##attr; \ + return std::filesystem::exists(path) && std::filesystem::is_directory(path); } -DEFINE_STAT_TIME_WRAPPER(atime) -DEFINE_STAT_TIME_WRAPPER(ctime) -DEFINE_STAT_TIME_WRAPPER(mtime) - -#undef DEFINE_STAT_TIME_WRAPPER - -int Filesystem::listdir (std::string dir, std::vector &files) +std::time_t Filesystem::mtime (std::filesystem::path path) { - DIR *dp; - struct dirent *dirp; - if((dp = opendir(dir.c_str())) == NULL) + try { - return errno; + auto ftime = std::filesystem::last_write_time(path); + return ftime.time_since_epoch().count(); } - while ((dirp = readdir(dp)) != NULL) { - files.push_back(std::string(dirp->d_name)); + catch (std::filesystem::filesystem_error&) + { + return 0; } - closedir(dp); - return 0; } -// prefix is the top-level dir where we started recursing -// path is the relative path under the prefix; must be empty or end in a '/' -// files is the output list of files and directories (bool == true for dir) -// depth is the remaining dir depth to recurse into. function returns -1 if -// we haven't finished recursing when we run out of depth. -// include_prefix controls whether the directory where we started recursing is -// included in the filenames returned in files. -static int listdir_recursive_impl (std::string prefix, std::string path, - std::map &files, int depth, bool include_prefix) +int Filesystem::listdir (std::filesystem::path dir, std::vector &files) { - if (depth < 0) - return -1; - std::string prefixed_path = prefix + "/" + path; - std::vector curdir_files; - int err = Filesystem::listdir(prefixed_path, curdir_files); - if (err) - return err; - bool out_of_depth = false; - for (auto file = curdir_files.begin(); file != curdir_files.end(); ++file) - { - if (*file == "." || *file == "..") - continue; - std::string prefixed_file = prefixed_path + *file; - std::string path_file = path + *file; - if (Filesystem::isdir(prefixed_file)) - { - files.insert(std::pair(include_prefix ? prefixed_file : path_file, true)); - - if (depth == 0) - { - out_of_depth = true; - continue; - } - - err = listdir_recursive_impl(prefix, path_file + "/", files, depth - 1, include_prefix); - if (err) - return err; - } - else + try { + for (auto const& dirent : std::filesystem::directory_iterator(dir)) { - files.insert(std::pair(include_prefix ? prefixed_file : path_file, false)); + files.push_back(dirent.path().filename()); } + return 0; + } + catch (std::filesystem::filesystem_error&) + { + return 1; } - return out_of_depth ? -1 : 0; } -int Filesystem::listdir_recursive (std::string dir, std::map &files, +int Filesystem::listdir_recursive (std::filesystem::path dir, std::map &files, int depth /* = 10 */, bool include_prefix /* = true */) { - if (dir.size() && dir[dir.size()-1] == '/') - dir.resize(dir.size()-1); - return listdir_recursive_impl(dir, "", files, depth, include_prefix); + try { + for (auto i = std::filesystem::recursive_directory_iterator(dir); + i != std::filesystem::recursive_directory_iterator(); + ++i) + { + if (i->is_directory() && i.depth() >= depth) + i.disable_recursion_pending(); + auto dirent = include_prefix ? *i : std::filesystem::relative(dir, *i); + files.emplace(dirent, i->is_directory()); + } + return 0; + } + catch (std::filesystem::filesystem_error&) + { + return 1; + } } diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index 0a6f1c3772d..9e1dd4d0181 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -183,12 +183,12 @@ static std::string filterSaveFileName(std::string s) { return s; } -static std::string getSavePath(const std::string &world) { - return "save/" + world; +static std::filesystem::path getSavePath(const std::string &world) { + return std::filesystem::path{} / "save" / world; } -static std::string getSaveFilePath(const std::string &world, const std::string &name) { - return getSavePath(world) + "/dfhack-" + filterSaveFileName(name) + ".dat"; +static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { + return getSavePath(world) / ("dfhack-" + filterSaveFileName(name) + ".dat"); } struct LastLoadSaveTickCountUpdater { @@ -266,7 +266,7 @@ static void add_entry(int entity_id, std::shared_ptr ent add_entry(store[entity_id], entry); } -static bool load_file(const std::string & path, int entity_id) { +static bool load_file(const std::filesystem::path & path, int entity_id) { Json::Value json; try { std::ifstream file(path); @@ -298,8 +298,8 @@ void Persistence::Internal::load(color_ostream& out) { clear(out); std::string world_name = World::ReadWorldFolder(); - std::string save_path = getSavePath(world_name); - std::vector files; + std::filesystem::path save_path = getSavePath(world_name); + std::vector files; if (0 != Filesystem::listdir(save_path, files)) { DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '%s'\n", save_path.c_str()); return; @@ -308,11 +308,11 @@ void Persistence::Internal::load(color_ostream& out) { bool found = false; for (auto & fname : files) { int entity_id = Persistence::WORLD_ENTITY_ID; - if (fname != "dfhack-world.dat" && !get_entity_id(fname, entity_id)) + if (fname != "dfhack-world.dat" && !get_entity_id(fname.string(), entity_id)) continue; found = true; - std::string path = save_path + "/" + fname; + std::filesystem::path path = save_path / fname; if (!load_file(path, entity_id)) out.printerr("Cannot load data from: '%s'\n", path.c_str()); } @@ -321,7 +321,7 @@ void Persistence::Internal::load(color_ostream& out) { return; // new file formats not found; attempt to load legacy file - const std::string legacy_fname = getSaveFilePath(world_name, "legacy-data"); + const std::filesystem::path legacy_fname = getSaveFilePath(world_name, "legacy-data"); if (Filesystem::exists(legacy_fname)) { int synthesized_entity_id = Persistence::WORLD_ENTITY_ID; if (World::IsSiteLoaded()) diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 49558c1b88e..a2da1f0ae94 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -134,7 +134,7 @@ static command_result orders_command(color_ostream & out, std::vector files; + std::map files; if (0 < Filesystem::listdir_recursive(ORDERS_LIBRARY_DIR, files, 0, false)) { // if the library directory doesn't exist, just skip it return; @@ -145,15 +145,15 @@ static void list_library(color_ostream &out) { return; } - for (auto it : files) + for (auto& it : files) { if (it.second) continue; // skip directories - std::string name = it.first; - if (name.length() <= 5 || name.rfind(".json") != name.length() - 5) + std::filesystem::path name = it.first; + if (name.extension() != ".json") continue; // skip non-.json files - name.resize(name.length() - 5); - out << "library/" << name << std::endl; + auto sname = name.stem(); + out << "library/" << sname << std::endl; } } @@ -162,17 +162,17 @@ static command_result orders_list_command(color_ostream & out) // use listdir_recursive instead of listdir even though orders doesn't // support subdirs so we can identify and ignore subdirs with ".json" names. // also listdir_recursive will alphabetize the list for us. - std::map files; + std::map files; Filesystem::listdir_recursive(ORDERS_DIR, files, 0, false); - for (auto it : files) { + for (auto& it : files) { if (it.second) continue; // skip directories - std::string name = it.first; - if (name.length() <= 5 || name.rfind(".json") != name.length() - 5) + std::filesystem::path name = it.first; + if (name.extension() != ".json") continue; // skip non-.json files - name.resize(name.length() - 5); - out << name << std::endl; + auto sname = name.stem(); + out << sname << std::endl; } list_library(out); From e8274319e92c82c45a8d549b751ba8d30cfd2038 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 09:34:24 -0500 Subject: [PATCH 191/919] fix docs --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ed655242070..ee2551a94ec 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,7 +62,7 @@ Template for new versions: ## Documentation ## API -- `Filesystem` module: rewritten to use C++ standard library components, for better portability +- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability ## Lua From a17c0ca56bc4481e8e306aaa29a3d921872763a7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 09:41:31 -0500 Subject: [PATCH 192/919] transform argument to printf really wish we could use `std::format` --- library/PlugLoad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 1cafc791f53..ec72e36fb9e 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -77,7 +77,7 @@ namespace DFHack DFLibrary* ret = (DFLibrary*)load_library(filename); if (!ret) { auto error = get_error(); - WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename, error.c_str()); + WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename.string().c_str(), error.c_str()); } return ret; } From 1945981b945046182e4a99e65fb3b2111471741d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 10:12:23 -0500 Subject: [PATCH 193/919] add `canonicalize` method to `Filesystem` and use in tests instead of hacky regexp --- library/LuaApi.cpp | 1 + library/include/modules/Filesystem.h | 1 + library/modules/Filesystem.cpp | 6 ++++++ test/core.lua | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d2162a33d14..cc8c643b74c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3155,6 +3155,7 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, isfile), WRAPM(Filesystem, isdir), WRAPM(Filesystem, mtime), + WRAPM(Filesystem, canonicalize), {NULL, NULL} }; diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 8c376a7a01b..d2b6f7a9b86 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -74,5 +74,6 @@ namespace DFHack { // paths returned in files DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, int depth = 10, bool include_prefix = true); + DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p); } } diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 8b316d34c85..9e81ba8d10b 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -211,3 +211,9 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::map Date: Mon, 17 Mar 2025 10:29:59 -0500 Subject: [PATCH 194/919] eradicate extra blank line in `Filesystem.cpp` --- library/modules/Filesystem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 9e81ba8d10b..b626319928c 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -216,4 +216,3 @@ std::filesystem::path Filesystem::canonicalize(std::filesystem::path p) { return std::filesystem::canonical(p); } - From 951621d5cae7fb1346e12c77f1250b1e8259b5a2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 11:24:50 -0500 Subject: [PATCH 195/919] don't error on an existing but empty script --- library/Core.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index c4c611ee9d7..cb310a47777 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1341,8 +1341,8 @@ bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool INFO(script,out) << "Running script: " << fname << std::endl; std::cerr << "Running script: " << fname << std::endl; } - std::ifstream script(fname); - if ( !script.good() ) + std::ifstream script{ fname.c_str() }; + if ( !script ) { if(!silent) out.printerr("Error loading script: %s\n", fname.string().c_str()); @@ -2203,9 +2203,9 @@ size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector Date: Mon, 17 Mar 2025 12:17:31 -0500 Subject: [PATCH 196/919] fix stupidly reversed arguments to `relative` --- library/modules/Filesystem.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index b626319928c..d95afecf8bd 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -49,6 +49,7 @@ SOFTWARE. #include #include #include +#include #include "modules/Filesystem.h" @@ -201,8 +202,9 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::mapis_directory() && i.depth() >= depth) i.disable_recursion_pending(); - auto dirent = include_prefix ? *i : std::filesystem::relative(dir, *i); - files.emplace(dirent, i->is_directory()); + auto p = i->path(); + auto pp = include_prefix ? p : std::filesystem::relative(p, dir); + files.emplace(pp, std::filesystem::is_directory(p)); } return 0; } From 0f83cac96f6fd711ec845ac5dc23105ef5962df6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 12:37:06 -0500 Subject: [PATCH 197/919] fix mtime to return -1 (not 0) on failure --- library/modules/Filesystem.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index d95afecf8bd..14f1ec63413 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -169,11 +169,12 @@ std::time_t Filesystem::mtime (std::filesystem::path path) try { auto ftime = std::filesystem::last_write_time(path); - return ftime.time_since_epoch().count(); + auto t = ftime.time_since_epoch().count(); + return t; } catch (std::filesystem::filesystem_error&) { - return 0; + return -1; } } From c4de1207ec8573884a96a4a6dc511d375125f198 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 13:50:57 -0500 Subject: [PATCH 198/919] use filesystem::path for mod script paths also make testing slightly noisier --- ci/test.lua | 1 + library/Core.cpp | 24 ++++++++++++++---------- library/include/Core.h | 8 ++++---- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/ci/test.lua b/ci/test.lua index e57facf14bb..32466a4f9ae 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -289,6 +289,7 @@ local MODES = { local function load_test_config(config_file) local config = {} + print ("loading test config from " .. config_file) if dfhack.filesystem.isfile(config_file) then config = json.decode_file(config_file) end diff --git a/library/Core.cpp b/library/Core.cpp index cb310a47777..642ff7d7a65 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -480,10 +480,10 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: return false; } -bool Core::addScriptPath(std::string path, bool search_before) +bool Core::addScriptPath(std::filesystem::path path, bool search_before) { std::lock_guard lock(script_path_mutex); - std::vector &vec = script_paths[search_before ? 0 : 1]; + auto &vec = script_paths[search_before ? 0 : 1]; if (std::find(vec.begin(), vec.end(), path) != vec.end()) return false; if (!Filesystem::isdir(path)) @@ -492,19 +492,19 @@ bool Core::addScriptPath(std::string path, bool search_before) return true; } -bool Core::setModScriptPaths(const std::vector &mod_script_paths) { +bool Core::setModScriptPaths(const std::vector &mod_script_paths) { std::lock_guard lock(script_path_mutex); script_paths[2] = mod_script_paths; return true; } -bool Core::removeScriptPath(std::string path) +bool Core::removeScriptPath(std::filesystem::path path) { std::lock_guard lock(script_path_mutex); bool found = false; for (int i = 0; i < 2; i++) { - std::vector &vec = script_paths[i]; + auto &vec = script_paths[i]; while (1) { auto it = std::find(vec.begin(), vec.end(), path); @@ -547,7 +547,7 @@ std::filesystem::path Core::findScript(std::string name) if (Filesystem::isfile(path)) return path; } - return ""; + return {}; } bool loadScriptPaths(color_ostream &out, bool silent = false) @@ -585,14 +585,18 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) } static void loadModScriptPaths(color_ostream &out) { - std::vector mod_script_paths; + std::vector mod_script_paths_str; + std::vector mod_script_paths; Lua::CallLuaModuleFunction(out, "script-manager", "get_mod_script_paths", {}, 1, [&](lua_State *L) { - Lua::GetVector(L, mod_script_paths); + Lua::GetVector(L, mod_script_paths_str); }); DEBUG(script,out).print("final mod script paths:\n"); - for (auto & path : mod_script_paths) - DEBUG(script,out).print(" %s\n", path.c_str()); + for (auto& path : mod_script_paths_str) + { + DEBUG(script, out).print(" %s\n", path.c_str()); + mod_script_paths.push_back(std::filesystem::canonical(std::filesystem::path{ path })); + } Core::getInstance().setModScriptPaths(mod_script_paths); } diff --git a/library/include/Core.h b/library/include/Core.h index 6794a6b28fd..9ef8fb8f0e1 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -177,9 +177,9 @@ namespace DFHack command_result runCommand(color_ostream &out, const std::string &command); bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); - bool addScriptPath(std::string path, bool search_before = false); - bool setModScriptPaths(const std::vector &mod_script_paths); - bool removeScriptPath(std::string path); + bool addScriptPath(std::filesystem::path path, bool search_before = false); + bool setModScriptPaths(const std::vector & mod_script_paths); + bool removeScriptPath(std::filesystem::path path); std::filesystem::path findScript(std::string name); void getScriptPaths(std::vector *dest); @@ -273,7 +273,7 @@ namespace DFHack std::vector> allModules; DFHack::PluginManager * plug_mgr; - std::vector script_paths[3]; + std::vector script_paths[3]; std::mutex script_path_mutex; // hotkey-related stuff From 9c8688aea987ce3245365c5a87caec275e40aa46 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 15:12:02 -0500 Subject: [PATCH 199/919] add noisy logging to try to debug ci --- library/Core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Core.cpp b/library/Core.cpp index 642ff7d7a65..0fa04d1d08e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -544,6 +544,7 @@ std::filesystem::path Core::findScript(std::string name) for (auto it = paths.begin(); it != paths.end(); ++it) { std::filesystem::path path = *it / name; + std::cerr << "findScript " << name << " | " << path << " | " << Filesystem::isfile(path) << std::endl; if (Filesystem::isfile(path)) return path; } From be1c8e95290ed5445e92b20b152fc8e028b52d8b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 15:29:12 -0500 Subject: [PATCH 200/919] different noisy crickets --- library/Core.cpp | 1 - library/LuaApi.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index 0fa04d1d08e..642ff7d7a65 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -544,7 +544,6 @@ std::filesystem::path Core::findScript(std::string name) for (auto it = paths.begin(); it != paths.end(); ++it) { std::filesystem::path path = *it / name; - std::cerr << "findScript " << name << " | " << path << " | " << Filesystem::isfile(path) << std::endl; if (Filesystem::isfile(path)) return path; } diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index cc8c643b74c..c37fd946e78 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3962,6 +3962,7 @@ static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); std::filesystem::path path = Core::getInstance().findScript(name); + std::cerr << "findScript " << name << " -> " << path << std::endl; if (!path.empty()) lua_pushstring(L, path.string().c_str()); else From 3ee3edf0b2f9f31b7c53a6469fb4894871d24c95 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 15:36:45 -0500 Subject: [PATCH 201/919] switch to weak canonicalization --- library/Core.cpp | 6 +++--- library/modules/Filesystem.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 642ff7d7a65..227c4378f02 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -543,7 +543,7 @@ std::filesystem::path Core::findScript(std::string name) getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - std::filesystem::path path = *it / name; + std::filesystem::path path = std::filesystem::weakly_canonical(*it / name); if (Filesystem::isfile(path)) return path; } @@ -595,7 +595,7 @@ static void loadModScriptPaths(color_ostream &out) { for (auto& path : mod_script_paths_str) { DEBUG(script, out).print(" %s\n", path.c_str()); - mod_script_paths.push_back(std::filesystem::canonical(std::filesystem::path{ path })); + mod_script_paths.push_back(std::filesystem::weakly_canonical(std::filesystem::path{ path })); } Core::getInstance().setModScriptPaths(mod_script_paths); } @@ -1132,7 +1132,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if(parts.size() == 1) { - loadScriptFile(con, std::filesystem::canonical(std::filesystem::path{parts[0]}), false); + loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); } else { diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 14f1ec63413..670e0640ef6 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -217,5 +217,5 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::map Date: Mon, 17 Mar 2025 16:02:26 -0500 Subject: [PATCH 202/919] make most Filesystem modules noexcept --- library/LuaApi.cpp | 1 - library/include/DataDefs.h | 19 +++++++++++++++ library/include/DataFuncs.h | 35 ++++++++++++++++++++++++++++ library/include/modules/Filesystem.h | 24 +++++++++---------- library/lua/dfhack.lua | 1 + library/modules/Filesystem.cpp | 33 +++++++++++++++----------- 6 files changed, 87 insertions(+), 26 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index c37fd946e78..cc8c643b74c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3962,7 +3962,6 @@ static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); std::filesystem::path path = Core::getInstance().findScript(name); - std::cerr << "findScript " << name << " -> " << path << std::endl; if (!path.empty()) lua_pushstring(L, path.string().c_str()); else diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index c6a7eb8272c..67380544235 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -615,6 +615,25 @@ namespace df static const bool is_method = true; }; + template + struct return_type { + using type = RT; + static const bool is_method = false; + }; + + template + struct return_type { + using type = RT; + using class_type = CT; + static const bool is_method = true; + }; + + template + struct return_type { + using type = RT; + using class_type = CT; + static const bool is_method = true; + }; } diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index e8bfc7e2848..a8d2caca51c 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -133,6 +133,41 @@ namespace df { }; }; + template + struct function_wrapper { + static const int num_args = sizeof...(AT); + static void execute(lua_State* L, int base, RT(fun)(DFHack::color_ostream& out, AT...)) { + cur_lua_ostream_argument out(L); + call_and_push(L, base, fun, out); + } + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT); + static void execute(lua_State* L, int base, RT(fun)(AT...)) { + call_and_push(L, base, fun); + } + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT) + 1; + static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...)) { + CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); + call_and_push(L, base, mem_fun, self); + }; + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT) + 1; + static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...) const) { + CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); + call_and_push(L, base, mem_fun, self); + }; + }; + template class function_identity : public function_identity_base { T ptr; diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index d2b6f7a9b86..c4a1c8ad94d 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -56,24 +56,24 @@ SOFTWARE. namespace DFHack { namespace Filesystem { DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::filesystem::path path); + DFHACK_EXPORT bool chdir (std::filesystem::path path) noexcept; DFHACK_EXPORT std::filesystem::path getcwd (); DFHACK_EXPORT bool restore_cwd (); DFHACK_EXPORT std::filesystem::path get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::filesystem::path path); + DFHACK_EXPORT bool mkdir (std::filesystem::path path) noexcept; // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path); - DFHACK_EXPORT bool rmdir (std::filesystem::path path); - DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info); - DFHACK_EXPORT bool exists (std::filesystem::path path); - DFHACK_EXPORT bool isfile (std::filesystem::path path); - DFHACK_EXPORT bool isdir (std::filesystem::path path); - DFHACK_EXPORT std::time_t mtime (std::filesystem::path path); - DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files); + DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool rmdir (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept; + DFHACK_EXPORT bool exists (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isfile (std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isdir (std::filesystem::path path) noexcept; + DFHACK_EXPORT std::time_t mtime (std::filesystem::path path) noexcept; + DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files) noexcept; // set include_prefix to false to prevent dir from being prepended to // paths returned in files DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, - int depth = 10, bool include_prefix = true); - DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p); + int depth = 10, bool include_prefix = true) noexcept; + DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; } } diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ef08fb7f4b8..88327a419e9 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -1074,6 +1074,7 @@ function dfhack.run_script_with_env(envVars, name, flags, ...) end end + print ( "run_script_with_env, name="..name..", file="..file ) local env = scripts[file].env if env == nil then env = {} diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 670e0640ef6..55a7fd21945 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -67,7 +67,7 @@ void Filesystem::init () } } -bool Filesystem::chdir (std::filesystem::path path) +bool Filesystem::chdir (std::filesystem::path path) noexcept { Filesystem::init(); try @@ -97,7 +97,7 @@ std::filesystem::path Filesystem::get_initial_cwd () return initial_cwd; } -bool Filesystem::mkdir (std::filesystem::path path) +bool Filesystem::mkdir (std::filesystem::path path) noexcept { try { @@ -110,7 +110,7 @@ bool Filesystem::mkdir (std::filesystem::path path) } } -bool Filesystem::mkdir_recursive (std::filesystem::path path) +bool Filesystem::mkdir_recursive (std::filesystem::path path) noexcept { try { @@ -123,7 +123,7 @@ bool Filesystem::mkdir_recursive (std::filesystem::path path) } } -bool Filesystem::rmdir (std::filesystem::path path) +bool Filesystem::rmdir (std::filesystem::path path) noexcept { try { @@ -136,7 +136,7 @@ bool Filesystem::rmdir (std::filesystem::path path) } } -bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) +bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept { try { @@ -149,22 +149,22 @@ bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status } } -bool Filesystem::exists (std::filesystem::path path) +bool Filesystem::exists (std::filesystem::path path) noexcept { return std::filesystem::exists(path); } -bool Filesystem::isfile(std::filesystem::path path) +bool Filesystem::isfile(std::filesystem::path path) noexcept { return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); } -bool Filesystem::isdir (std::filesystem::path path) +bool Filesystem::isdir (std::filesystem::path path) noexcept { return std::filesystem::exists(path) && std::filesystem::is_directory(path); } -std::time_t Filesystem::mtime (std::filesystem::path path) +std::time_t Filesystem::mtime (std::filesystem::path path) noexcept { try { @@ -178,7 +178,7 @@ std::time_t Filesystem::mtime (std::filesystem::path path) } } -int Filesystem::listdir (std::filesystem::path dir, std::vector &files) +int Filesystem::listdir (std::filesystem::path dir, std::vector &files) noexcept { try { for (auto const& dirent : std::filesystem::directory_iterator(dir)) @@ -194,7 +194,7 @@ int Filesystem::listdir (std::filesystem::path dir, std::vector &files, - int depth /* = 10 */, bool include_prefix /* = true */) + int depth /* = 10 */, bool include_prefix /* = true */) noexcept { try { for (auto i = std::filesystem::recursive_directory_iterator(dir); @@ -215,7 +215,14 @@ int Filesystem::listdir_recursive (std::filesystem::path dir, std::map Date: Mon, 17 Mar 2025 16:20:45 -0500 Subject: [PATCH 203/919] more cricket shuffling --- library/lua/dfhack.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 88327a419e9..97351e88254 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -1042,7 +1042,9 @@ end function dfhack.run_script_with_env(envVars, name, flags, ...) if type(flags) ~= 'table' then flags = {} end + dfhack.printerr ( "run_script_with_env, name="..name..", file=" ) local file = dfhack.findScript(name) + dfhack.printerr ( "run_script_with_env, name="..name..", file="..file ) if not file then error('Could not find script ' .. name) end @@ -1074,7 +1076,6 @@ function dfhack.run_script_with_env(envVars, name, flags, ...) end end - print ( "run_script_with_env, name="..name..", file="..file ) local env = scripts[file].env if env == nil then env = {} From 5beb9405bfb570b6c3f51ab27ffcc9b95ba4dd8e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 16:34:31 -0500 Subject: [PATCH 204/919] more cricket shuffling --- library/Core.cpp | 1 + library/lua/dfhack.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/library/Core.cpp b/library/Core.cpp index 227c4378f02..4d0a505d06d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -350,6 +350,7 @@ static bool init_run_script(color_ostream &out, lua_State *state, void *info) static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { + std::cerr << "runLuaScript " << name << std::endl; ScriptArgs data; data.pcmd = &name; data.pargs = &args; diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 97351e88254..888a266d14b 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -997,6 +997,7 @@ local valid_script_flags = { local warned_scripts = {} function dfhack.run_script(name,...) + dfhack.printerr ( "run_script, name="..name ) if not warned_scripts[name] then local helpdb = require('helpdb') if helpdb.has_tag(name, 'unavailable') then From 6ab84505d402f6eaf7b0fceda08cd6f783c01ce8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 16:48:31 -0500 Subject: [PATCH 205/919] better type jenga (???) --- library/LuaTools.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index c0f8bd1d2ec..084ba4be3ef 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1240,13 +1240,13 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, if (histfile != histname) { if (!histname.empty()) - hist.save(histname.c_str()); + hist.save(std::filesystem::path{ histname }); hist.clear(); histname = histfile; if (!histname.empty()) - hist.load(histname.c_str()); + hist.load(std::filesystem::path{ histname }); } if (prompt.empty()) @@ -1269,7 +1269,7 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, } if (!histname.empty()) - hist.save(histname.c_str()); + hist.save(std::filesystem::path{ histname }); { CoreSuspender suspend; From 8979fe2535e95b320f05841bf5a14b8c1276941f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:04:13 -0500 Subject: [PATCH 206/919] fix a couple more type jengas --- library/Core.cpp | 2 ++ library/LuaApi.cpp | 2 +- library/LuaTools.cpp | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 4d0a505d06d..a1cdcbac41a 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -357,6 +357,8 @@ static command_result runLuaScript(color_ostream &out, std::string name, std::ve bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); + std::cerr << "runLuaScript " << name << " res=" << ok << std::endl; + return ok ? CR_OK : CR_FAILURE; } diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index cc8c643b74c..e082076ae63 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1290,7 +1290,7 @@ static void addCommandToHistory(string id, string src_file, string command) { CommandHistory *history = ensureCommandHistory(id, src_file); history->add(command); - history->save(src_file.c_str()); + history->save(std::filesystem::path{ src_file }); } /************************ diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 084ba4be3ef..2d9df61ab44 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -426,7 +426,7 @@ static int dfhack_lineedit_sync(lua_State *S, Console *pstream) DFHack::CommandHistory hist; if (hfile) - hist.load(hfile); + hist.load(std::filesystem::path{ hfile }); std::string ret; int rv = pstream->lineedit(prompt, ret, hist); @@ -446,7 +446,7 @@ static int dfhack_lineedit_sync(lua_State *S, Console *pstream) else { if (hfile) - hist.save(hfile); + hist.save(std::filesystem::path{ hfile }); lua_pushlstring(S, ret.data(), ret.size()); return 1; } From b98913b6ed6c0b0525f12d49ce8a295a2c86ab59 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:15:17 -0500 Subject: [PATCH 207/919] add missing slash --- ci/test.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test.lua b/ci/test.lua index 32466a4f9ae..c0d021d0a60 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -295,7 +295,7 @@ local function load_test_config(config_file) end if not config.test_dir then - config.test_dir = dfhack.getHackPath() .. 'scripts/test' + config.test_dir = dfhack.getHackPath() .. '/scripts/test' end if not config.save_dir then From 5683c7aee513fe3aec1276748083612734a655e4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:24:35 -0500 Subject: [PATCH 208/919] silence at least some of the crickets --- library/Core.cpp | 3 --- library/lua/dfhack.lua | 3 --- 2 files changed, 6 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index a1cdcbac41a..227c4378f02 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -350,15 +350,12 @@ static bool init_run_script(color_ostream &out, lua_State *state, void *info) static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { - std::cerr << "runLuaScript " << name << std::endl; ScriptArgs data; data.pcmd = &name; data.pargs = &args; bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); - std::cerr << "runLuaScript " << name << " res=" << ok << std::endl; - return ok ? CR_OK : CR_FAILURE; } diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 888a266d14b..ef08fb7f4b8 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -997,7 +997,6 @@ local valid_script_flags = { local warned_scripts = {} function dfhack.run_script(name,...) - dfhack.printerr ( "run_script, name="..name ) if not warned_scripts[name] then local helpdb = require('helpdb') if helpdb.has_tag(name, 'unavailable') then @@ -1043,9 +1042,7 @@ end function dfhack.run_script_with_env(envVars, name, flags, ...) if type(flags) ~= 'table' then flags = {} end - dfhack.printerr ( "run_script_with_env, name="..name..", file=" ) local file = dfhack.findScript(name) - dfhack.printerr ( "run_script_with_env, name="..name..", file="..file ) if not file then error('Could not find script ' .. name) end From 4a8b292438dc36e94967639523bcf0980e42259f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Mar 2025 17:35:57 -0500 Subject: [PATCH 209/919] use same cwd mechanics in both linux and windows --- library/Process-windows.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index fe8c5d591ab..603636176db 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -33,11 +33,13 @@ distribution. #include "Error.h" #include "Internal.h" + #include "MemAccess.h" #include "Memory.h" #include "MiscUtils.h" #include "VersionInfo.h" #include "VersionInfoFactory.h" +#include "modules/Filesystem.h" #define _WIN32_WINNT 0x0600 #define WINVER 0x0600 @@ -384,13 +386,7 @@ uint32_t Process::getTickCount() std::filesystem::path Process::getPath() { - HMODULE hmod; - DWORD junk; - char String[255]; - EnumProcessModules(d->my_handle, &hmod, 1 * sizeof(HMODULE), &junk); //get the module from the handle - GetModuleFileNameEx(d->my_handle,hmod,String,sizeof(String)); //get the filename from the module - string out(String); - return(out.substr(0,out.find_last_of("\\"))); + return Filesystem::get_initial_cwd(); } int Process::getPID() From 4d0b66124f9b9af59d9dd0a82baf52691549f31a Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 11:37:16 -0500 Subject: [PATCH 210/919] create toolbar icon for mass remove --- data/art/mass_remove_toolbar.png | Bin 0 -> 1400 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/art/mass_remove_toolbar.png diff --git a/data/art/mass_remove_toolbar.png b/data/art/mass_remove_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..d1b09ac93bb28ea29db7767139378f90ba2f6207 GIT binary patch literal 1400 zcmV-;1&8{HP)Px)GD$>1RA_P*NzCmO|1)GLw04 zXW!fGd&yGzpLXZXoA3Ysnfd>j|J5wss#)ZJEH!~ZU`-rgnkE3r!C}(50;!=Sfk0p_ zP+A$AybZv_$T;a-fka=Nj_{tMe$Um_)qUzW+%!#+gTwrl`<+ZpBR9YMY~78Y7#Zj5 z56+UQY2?D$qgvwuYW;?rrioz~{Cw$001k#W`HgUtvB}%aPR*3f_uT;inQR__x8jHB z@9kOF*|+l}vG$U)53r$9h}h#g8BNba+nc0w1=qfnI!EkrH-5yWuhs#(JG;4)y5g$D z(GO3vYjZ8lO+ne#e&}7!bnRr|+5=KUNxBZ~=Wtg{K7aCS4uIySAY*qQapR{SD&~Ll z<+nxrEw8;U`H8+b7kiF7@+)2e0J}EV%0W*i1^_S&gG@Hhy{o4!#tX23i}!E%#19`n z2Egpp44G`6OXs^S#+Tw3#em(N-SYQ|j}ndwOwTO zU7KrVf8DH9i?59VViq#lyhWi>ik#`%2_Vy+s+|3IN87fdS@DZ6(%x$L+LPdp?Tv(j z4Wx4gGTA&w_JyTVsuCjlt7#9MgO7;5Iw!1_FQUJi_EyU;ItK`^3(u!>1^Rn?w1XuF zhZ(r`plsvI5gk7gYp1@og^1|X7YCMZdZX$2vW?e?#R0f<-ymjUS1%Gdne-xB$&<>*s;BlXf!H~uWxMu z;K;HPp{cV5&u_ymo{Fkm&X*dfDI_(NBsn-NXF-LFoO>{| ziRPF8l<)lUI1N~%ta|2F4Q_4JDL zWeuvc;0D+A^(xiiTO4pFbwv&=a`0;5ZUN4zZ#DQuMbW~;`@HeaNm4^eUV5d44U5*I zu{~CZ6u5C~NVY$Hw#d_Gi)?wZhUvKl`CmK}?Eu+dW>BT@@IH^{{~^&A=i2pKj(qjx zS1I4IEN+@6ou~S>&q^5&EBV)+mh5eSwjv~CZ1Ogre{o6std#MqX1W Date: Thu, 20 Mar 2025 08:38:37 -0500 Subject: [PATCH 211/919] Create sitemap_toolbar.png --- data/art/sitemap_toolbar.png | Bin 0 -> 483 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/art/sitemap_toolbar.png diff --git a/data/art/sitemap_toolbar.png b/data/art/sitemap_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..07871884a64a746cd5aba7b1d6a1c72a00d6ad7c GIT binary patch literal 483 zcmV<90UZ8`P)Px$ok>JNRA_?kF~10=g6^XaV23FF-R**RtcM{XzVVs({D&$&PO>wdQFCA@JRG zD;)0u@FG-}fCO%54iQb)ir{wc$9^CoT7V62Lrxu!2?u{a_HUoL zX)f&(fPfjGy{YjD2X8`!yb$pcuw?kqI6ns02oE?4SSvi>AYje#z=VLc!viA%N`MCh z0j0nLjDV8h0ZTw>@PHwpM0mguK+vh-f%pA=N_b#l{|+}@n-m^cn1CGlycErS0l8DW ZfFGZ36}hoadiekV002ovPDHLkV1f<2((3>K literal 0 HcmV?d00001 From 6dc491464ceed1acefc2a4a395d806748f0a747a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 16:27:35 -0700 Subject: [PATCH 212/919] remove obsolete comment --- test/core.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core.lua b/test/core.lua index 3a0a1d5ee92..7270f570676 100644 --- a/test/core.lua +++ b/test/core.lua @@ -1,7 +1,6 @@ config.target = 'core' local function clean_path(p) - -- todo: replace with dfhack.filesystem call? return dfhack.filesystem.canonicalize(p) end From dc4b40e554d2905694c103c79cbc3696c0cddf43 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 23 Mar 2025 02:19:24 +0000 Subject: [PATCH 213/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 93496b510fc..c783692f43a 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 93496b510fcfe691f8baeb54d885a8ac811cae1e +Subproject commit c783692f43a205cef821e669adc26bc725f5c159 diff --git a/scripts b/scripts index 90431556c4c..9fe2a467fe5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 90431556c4c2924c206403232ea1c24491355d07 +Subproject commit 9fe2a467fe5c2c51a74db02c6a317453904e095f From a801e96901af3db8706370d185ea7d7479dce86b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 20:55:49 -0700 Subject: [PATCH 214/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 9fe2a467fe5..ccdc48f2dd4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9fe2a467fe5c2c51a74db02c6a317453904e095f +Subproject commit ccdc48f2dd4d1a777dc9d1002c707202ffdffff3 From 8b8b70b6e6d5bd2dea05ce9e113c6e19acdcbabb Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Mar 2025 00:45:34 -0500 Subject: [PATCH 215/919] deal with directory separators cleanly dfhack assumes that scripts are in a namespace in which directories are separated by slashes, and also that pathnames will be slash-separated this enforces that when the transition is made from a `std::filesystem::path` to dfhack's internal script name space, backslashes are replaced with slashes on systems where the preferred separator is not already '/' --- library/LuaApi.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e082076ae63..e5ce40ac182 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3207,7 +3207,12 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - lua_pushstring(L, (it->first).string().c_str()); + auto p = (it->first).string(); + if constexpr (std::filesystem::path::preferred_separator != '/') + { + std::ranges::replace(p, std::filesystem::path::preferred_separator, '/'); + } + lua_pushstring(L, p.c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); From 1fdc92866e92cabca6f44cce1dc5f363249898fc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 23 Mar 2025 10:18:56 +0100 Subject: [PATCH 216/919] add a changelog entry --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94ec..8f5f60f7134 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `spectate`: show dwarves' activities (like prayer) ## Documentation From 19023334bce9046f0f546237ca831f7882660497 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Mar 2025 08:09:20 -0500 Subject: [PATCH 217/919] address another path/string confusion added `Filesystem::as_string` to provide namespace recanonicalization and change use in LuaApi to use this exported API use `as_string` in `orders` --- library/LuaApi.cpp | 7 +---- library/include/modules/Filesystem.h | 39 +++++++++++++++++----------- plugins/orders.cpp | 4 +-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e5ce40ac182..0ce74f9f063 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3207,12 +3207,7 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - auto p = (it->first).string(); - if constexpr (std::filesystem::path::preferred_separator != '/') - { - std::ranges::replace(p, std::filesystem::path::preferred_separator, '/'); - } - lua_pushstring(L, p.c_str()); + lua_pushstring(L, Filesystem::as_string(it->first).c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index c4a1c8ad94d..7c68983a3a4 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -55,25 +55,34 @@ SOFTWARE. namespace DFHack { namespace Filesystem { - DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::filesystem::path path) noexcept; - DFHACK_EXPORT std::filesystem::path getcwd (); - DFHACK_EXPORT bool restore_cwd (); - DFHACK_EXPORT std::filesystem::path get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::filesystem::path path) noexcept; + DFHACK_EXPORT void init(); + DFHACK_EXPORT bool chdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT std::filesystem::path getcwd(); + DFHACK_EXPORT bool restore_cwd(); + DFHACK_EXPORT std::filesystem::path get_initial_cwd(); + DFHACK_EXPORT bool mkdir(std::filesystem::path path) noexcept; // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool rmdir (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept; - DFHACK_EXPORT bool exists (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool isfile (std::filesystem::path path) noexcept; - DFHACK_EXPORT bool isdir (std::filesystem::path path) noexcept; - DFHACK_EXPORT std::time_t mtime (std::filesystem::path path) noexcept; - DFHACK_EXPORT int listdir (std::filesystem::path dir, std::vector &files) noexcept; + DFHACK_EXPORT bool mkdir_recursive(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool rmdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool stat(std::filesystem::path path, std::filesystem::file_status& info) noexcept; + DFHACK_EXPORT bool exists(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isfile(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT std::time_t mtime(std::filesystem::path path) noexcept; + DFHACK_EXPORT int listdir(std::filesystem::path dir, std::vector& files) noexcept; // set include_prefix to false to prevent dir from being prepended to // paths returned in files - DFHACK_EXPORT int listdir_recursive (std::filesystem::path dir, std::map &files, + DFHACK_EXPORT int listdir_recursive(std::filesystem::path dir, std::map& files, int depth = 10, bool include_prefix = true) noexcept; DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; + inline std::string as_string(std::filesystem::path path) noexcept + { + auto pStr = path.string(); + if constexpr (std::filesystem::path::preferred_separator != '/') + { + std::ranges::replace(pStr, std::filesystem::path::preferred_separator, '/'); + } + return pStr; + } } } diff --git a/plugins/orders.cpp b/plugins/orders.cpp index a2da1f0ae94..5891f924a38 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -153,7 +153,7 @@ static void list_library(color_ostream &out) { if (name.extension() != ".json") continue; // skip non-.json files auto sname = name.stem(); - out << "library/" << sname << std::endl; + out << Filesystem::as_string("library" / sname) << std::endl; } } @@ -172,7 +172,7 @@ static command_result orders_list_command(color_ostream & out) if (name.extension() != ".json") continue; // skip non-.json files auto sname = name.stem(); - out << sname << std::endl; + out << sname.string() << std::endl; } list_library(out); From da4b53adc4dae165007ff4f71d769ad3d317ec8e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 26 Mar 2025 17:31:32 -0500 Subject: [PATCH 218/919] add `Military::addToSquad` does not handle setting a squad leader yet so will just fail if you try to add to position 0 --- docs/changelog.txt | 1 + library/include/modules/Military.h | 1 + library/modules/Military.cpp | 55 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94ec..ebb3905391a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## API - ``Filesystem`` module: rewritten to use C++ standard library components, for better portability +- ''Military`` module: added `addToSquad` function ## Lua diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index dc69dac52bf..3b51a921e2a 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -18,6 +18,7 @@ DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); +DFHACK_EXPORT bool addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos = -1); } } diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 2e86fb958d9..83cf240d19b 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -408,6 +408,61 @@ static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squ df::global::world->history.events.push_back(former_pos_event); } +static void add_soldier_entity_link(df::historical_figure* hf, df::squad* squad, int32_t squad_pos) +{ + auto squad_link = df::allocate(); + squad_link->squad_id = squad->id; + squad_link->squad_position = squad_pos; + squad_link->entity_id = squad->entity_id; + squad_link->start_year = *df::global::cur_year; + squad_link->link_strength = 100; + + hf->entity_links.push_back(squad_link); +} + +bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) +{ + df::unit* unit = df::unit::find(unit_id); + if (unit == nullptr || unit->military.squad_id != -1) return false; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) + return false; + + df::squad* squad = df::squad::find(squad_id); + if (squad == nullptr) return false; + + if (squad_pos == -1) + { + for (int p = 0; p < 10; p++) + { + auto pp = vector_get(squad->positions, p); + if (pp == nullptr || pp->occupant == -1) + { + squad_pos = p; + break; + } + } + } + if (squad_pos == -1) return false; + + // this function cannot (currently) change the squad commander + if (squad_pos == 0) return false; + + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos == nullptr) + pos = squad->positions[squad_pos] = df::allocate(); + + pos->occupant = hf->id; + // does anything else need to be set here? + + unit->military.squad_id = squad->id; + unit->military.squad_position = squad_pos; + + add_soldier_entity_link(hf, squad, squad_pos); + return true; +} + bool Military::removeFromSquad(int32_t unit_id) { df::unit *unit = df::unit::find(unit_id); From 7eb83f1322cf64a54c16a55cc08042d96871bdd5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 26 Mar 2025 18:34:33 -0500 Subject: [PATCH 219/919] fix markup in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ebb3905391a..187b49a0a79 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,7 +63,7 @@ Template for new versions: ## API - ``Filesystem`` module: rewritten to use C++ standard library components, for better portability -- ''Military`` module: added `addToSquad` function +- ``Military`` module: added ``addToSquad`` function ## Lua From 95461eab8fabb6a2a5f6016c11dc5755bdba545a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 27 Mar 2025 20:20:50 -0500 Subject: [PATCH 220/919] bump msvc version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11e95c6d8d5..c8f8d90d437 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1942) + set(MSVC_MAX_VER 1943) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) From e7fa72ef9d52f8334643945a1fdf9ae457b1c064 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 28 Mar 2025 15:23:41 -0500 Subject: [PATCH 221/919] Create design_toolbar.png --- data/art/design_toolbar.png | Bin 0 -> 599 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/art/design_toolbar.png diff --git a/data/art/design_toolbar.png b/data/art/design_toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..935a332f00b512ca61b0fb57f229dc6ffff63285 GIT binary patch literal 599 zcmV-d0;v6oP)Px%5lKWrRA_!aYoU~`q&62o4nQxPw!b$ zgfeVD!v=4RVI0S|HsI^*0M5q8fU^ULLeAOr+;>y}>zv^-rl{9HffIWj0G2I7N^%Ul zG{JSQyB%Qy6?`4)&n_E?CNyP$Bsda9xFpyMmjM>x)BTxS=grsBdT;)!0h-teol~;g zm|&VS+{WbU1z5Bu*2=H-f+qudZ@8RI4JDFu?!Ms`{)YjQc+TzOuyl^!EoFfwzL#Ih zf`Y~q5&YatQ2iCqQpb{BEzR&~es&UE=d9MC^%n4%r36YAMDV*ZfD&`{X1N6# zhzCdTtCL_M!7rku)N$OXdJ)ZE{TON~1Fj< Date: Mon, 31 Mar 2025 18:23:16 -0700 Subject: [PATCH 222/919] add hotkey for gui/journal in adv mode --- data/init/dfhack.keybindings.init | 6 +++--- docs/changelog.txt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 776a8425b36..018c66ccde9 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -47,6 +47,9 @@ keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap # toggle keyboard cursor keybinding add Alt-K@dwarfmode|dungeonmode/Default|dungeonmode/Look toggle-kbd-cursor +# gui/journal +keybinding add Ctrl-J@dwarfmode|dungeonmode gui/journal + ###################### # dwarfmode bindings # @@ -151,9 +154,6 @@ keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort keybinding add Ctrl-D@dwarfmode/Default gui/design keybinding add Ctrl-M@dwarfmode/Default gui/mass-remove -# gui/journal -keybinding add Ctrl-J@dwarfmode gui/journal - ######################## # dungeonmode bindings # diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94ec..ce9d7f80d69 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! ## Fixes From 9b00c08c6b117fa44a01d82153ed221e4de6d0c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:31:30 -0700 Subject: [PATCH 223/919] adapt to structure changes --- docs/changelog.txt | 1 + plugins/lua/sort/diplomacy.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee2551a94ec..b6ba2ba2f5f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- Fix processing error in the overlay that displays unit preferences in the baron selection list ## Misc Improvements diff --git a/plugins/lua/sort/diplomacy.lua b/plugins/lua/sort/diplomacy.lua index 3e785938623..505e02b7431 100644 --- a/plugins/lua/sort/diplomacy.lua +++ b/plugins/lua/sort/diplomacy.lua @@ -64,7 +64,7 @@ local function get_preferences(unit) if not unit then return {} end local preferences = {} for _, pref in ipairs(unit.status.current_soul.preferences) do - if pref.type == df.unitpref_type.LikeItem and pref.active then + if pref.type == df.unitpref_type.LikeItem and pref.flags.visible then table.insert(preferences, make_item_description(pref.item_type, pref.item_subtype)) end end From 739a5eeabc916a05c9a9bdd7bfc33e9724ce5595 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 1 Apr 2025 01:33:45 +0000 Subject: [PATCH 224/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index c783692f43a..d0c14fa0a67 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit c783692f43a205cef821e669adc26bc725f5c159 +Subproject commit d0c14fa0a6702f396e147bbd3c9b325c0677aa2b diff --git a/scripts b/scripts index ccdc48f2dd4..a9f4502faf2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ccdc48f2dd4d1a777dc9d1002c707202ffdffff3 +Subproject commit a9f4502faf2e2e026361aa9e84a138077d1dff7b From e4359781d7d5034b4e6000f5967be750e1610ccd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:51:37 -0700 Subject: [PATCH 225/919] make toolbar icon fit in with others in the group --- data/art/sitemap_toolbar.png | Bin 483 -> 4833 bytes scripts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/art/sitemap_toolbar.png b/data/art/sitemap_toolbar.png index 07871884a64a746cd5aba7b1d6a1c72a00d6ad7c..a29fa9b9004532e764504ea6e758236029de8e65 100644 GIT binary patch literal 4833 zcmeHLeN+=y7M~y@Mny$zJ)$+F+7)mzlgT7xvI(C-NY@|>6j`;dlbIJXm3)v45TI6M ztJRg&YU?*zJXpU$Xzj68D{zF>-6B$ZdiK=agOsYU)m2b;b&=J5GlZ|RJ!g;S)c;7% zn|J5E`+o1<-@WfnCL2>zk|s%KN)ZH^WJ)%s!9EtYxq$)jN=?*ngIz1gGFeTg2qX~R zKuw81B7ssYY!aV57s`H;F~}I$uZJxd%7Tv2W=h72eHoNPee#D;p6}BU)+i-@$Rya8 z!$v?k1op>ZpYZ&PoVgX@u{A$rIabPI+D_< zNw~kHV#p!^NzzgGGQ7UaB_yPp5)x!Cx6{hy00i-tuik1%zK|NSZt*uSB}N40PszQM zI=fL3(UEmL^$ zSoo<8J?oRATh4ZNx2-N|JwaTS&O^+nFE5G+zjgn(qerM(>aX7is9E!3N5rK|VdUsrxHq7!Xd`dQ1h&Kpr>ArH5M>ThPo zNjGJc`cIYqc%(uan%jH(VAiG9cdlNHxzjt(9e(3j$yP?9w!Qym%;V6${Z((2?|HaQ zZj$wNXy;VMUQx8ZrEPgs`@^Y}l6IBrqPqLd7#Xca@T@|b9;-m8NEcEHmzuRX`%(RmGW5myIg zVHlIkL=}FOL7oNkD8cClGEzY*a5T}&g)Klo-X)o&VyedHSV;F&ladR%7b2?-KCT(%%@dmjZ&dUbu zv%AdZL3oG9#{%R7^U^L%slYM29UH#F!zboL5T8Lmxx$lC=mJ<8@Hq3_3`ooe4*u2Q z5G*se-j(Oh5#3=K4CDYiRQ14Jl_RzkPTb%Mfr4z#?h;o)vPWq0oOOt-5w;04qC3M4 zfy)PRM`#cDE-FJUvsq_!GI@e|CZj1G%SOv zD5XZj0-7Qy?Jy{l!^6`K1_)3PT){ycwN*vpR=}bbrA331ERLfzV^yN06%Y(fYgItY z41-wg=3rLRIm4|IpjZe+5Tu4y0UD)ol^P|rj0)980V@hLswkSEDJ^MHi%=}1Tj+G# zX&6q7hOhc{1mL`xlBs{U<6N z>J*;aA(i(h{m3jPwB@!i8<@=@0beSGz#z_N|;UmlZ9FPwAd z?IcRuxx(6B^T!#VR6|+FesD*}e0Z)YB)Pt5e$x4?KA6 z&5LNiej2m3C9SKes_%JwLfEaFEu{(dedoMs{*mLigkAV|Czw8cZRwhF!}!T{;Zsle z>&FHB;HvsCaKv6WD7FM{65?BRk&^5x=)i!_LOEWo5+lZQYRqWl1`AenIJFe93yQTkK)!NAi z$ZK@^^~1)_>qv=Kt)qdRw>C4>_gL-@SjT{ZAoXk}Zj?rPptShM6Cmr4_TEeHnXlOGwel eH8cJ*ZFh`qQ|O(xqqcQ0a)>E0#aJD`vh=^*5SmW_ delta 457 zcmV;)0XF{OCF28-B!2;OQb$4nuFf3k0004ZusIAwNyR5Wk&43*IAXI#|7K)e8lus_VLF0pGbVKr>C( zvg4=yLHv!XfXDgCj&Clt=4l2Y@ZEJQ9Pa_}B2<=u1a4;z5r0kBir{wc$9^CoT7V62 zLrxu!2?u{a_HUoLHA33?odxcVkQuej5CNuah0PEkiLl2h zYOQd{9o2#lZG^abl@W6zCwZj7=0!n}f1OcVM1B`%@;6edQKxy!RA)rKfzz{&tso{b5 z{e4P!U}66bH(i?)9$1)w9QnKy&3ys6Q@ns5pKcYou}*sV00000NkvXXu0mjf2(Z%V diff --git a/scripts b/scripts index a9f4502faf2..20d201a8ffe 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a9f4502faf2e2e026361aa9e84a138077d1dff7b +Subproject commit 20d201a8ffe5df52563a37fdde88986973faca4e From a8bd0f84dcfeafb820c91abdcf716c51a6bd4288 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:12:10 +0000 Subject: [PATCH 226/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 20d201a8ffe..31e31bf1872 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 20d201a8ffe5df52563a37fdde88986973faca4e +Subproject commit 31e31bf18728c9913deb234ef47a6f11a7208a1d From 7b6da0043b66d0d1addacf95b5c00fdeec260203 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:19:19 +0000 Subject: [PATCH 227/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 31e31bf1872..3a919bc098a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 31e31bf18728c9913deb234ef47a6f11a7208a1d +Subproject commit 3a919bc098a08e0e1c259f82d03ecf268bd10a41 From c5eff407cb4ca054578b0150d520f0a1050197c5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 27 Mar 2025 20:20:50 -0500 Subject: [PATCH 228/919] bump msvc version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11e95c6d8d5..c8f8d90d437 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1942) + set(MSVC_MAX_VER 1943) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) From f589397542d6f3e9f6c6185f530b58f9a5f2ba39 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 1 Apr 2025 14:37:07 -0500 Subject: [PATCH 229/919] add `addToSquad` to Lua API --- library/LuaApi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 0ce74f9f063..f169650f90c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2344,6 +2344,7 @@ static const LuaWrapper::FunctionReg dfhack_military_module[] = { WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), WRAPM(Military, removeFromSquad), + WRAPM(Military, addToSquad), { NULL, NULL } }; From 0560b0089a999c06476d3c9c8b251ea007228dac Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 1 Apr 2025 14:43:18 -0500 Subject: [PATCH 230/919] add documentation --- docs/dev/Lua API.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d449c8810bb..e2d9eaa7865 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1995,6 +1995,23 @@ Military module to indicate former squad membership or command, and creates a corresponding world history event. + * ``dfhack.military.addToSquad(unit_id, squad_id, squad_pos)`` + + Adds a unit to a squad. Sets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), adds a unit's entity links to + indicate squad membership. Does not currently add world history events. + If ``squad_pos`` is -1, the unit will be added to the first open slot in + the squad. + + This API cannot be used to set or change the leader of a squad and will fail + if ``squad_pos`` is specified as 0 or if ``squad_pos`` is specified as -1 and + the squad leader position is currently vacant. It will also fail if + the requested squad position is already occupied, the squad does not exist, + the unit does not exist, or the requested unit is already a member of another + squad. + Items module ------------ From 2302b256b969e06c9631e94a90cf1062a2930f29 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:43:46 +0000 Subject: [PATCH 231/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 860017b193c..1732e543952 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 860017b193c5cce1ee6c16321301b0a1d4788569 +Subproject commit 1732e54395247019ff21aa77a09b0fe3869a5c87 From b040c783f46fcc7b6cef2c92fa0ee9cc092205f7 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:52:01 +0000 Subject: [PATCH 232/919] Auto-update structures ref for 51.09 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 1732e543952..94f95a3dd53 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1732e54395247019ff21aa77a09b0fe3869a5c87 +Subproject commit 94f95a3dd53a2a4a35ed645e2afea9109079269a From 4d988aa1b600d4ba5632ec1305a64ded9ced251c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 2 Apr 2025 09:12:18 -0700 Subject: [PATCH 233/919] update for 51.09 --- CMakeLists.txt | 2 +- docs/changelog.txt | 14 +++++++++++--- scripts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c8f8d90d437..fcc15168ba1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.08") +set(DF_VERSION "51.09") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index f384939d400..7c4babf881e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,22 +54,30 @@ Template for new versions: ## New Tools ## New Features -- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! ## Fixes -- Fix processing error in the overlay that displays unit preferences in the baron selection list ## Misc Improvements ## Documentation ## API -- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability ## Lua ## Removed +# 51.09-r1 + +## New Features +- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! + +## Fixes +- Fix processing error in the overlay that displays unit preferences in the baron selection list + +## API +- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability + # 51.08-r1 ## Misc Improvements diff --git a/scripts b/scripts index 3a919bc098a..d8912409bb3 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3a919bc098a08e0e1c259f82d03ecf268bd10a41 +Subproject commit d8912409bb3c7398a2859aa0c0092d792c9211e7 From 7fcfecc9e61091618f6e0f694c95ab24f7ef4e02 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:57:23 +0000 Subject: [PATCH 234/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 94f95a3dd53..eccd051abb7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 94f95a3dd53a2a4a35ed645e2afea9109079269a +Subproject commit eccd051abb7dcbecceac06b861347304578e897e From f6850514a17fa9f22fa92e27f1e0cb7ff6593168 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:37:37 +0000 Subject: [PATCH 235/919] Auto-update structures ref for 51.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index eccd051abb7..ec3ab067aef 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit eccd051abb7dcbecceac06b861347304578e897e +Subproject commit ec3ab067aef716817d9c5b84a6fb85bc22516caa From f324abb20546fcf21b4b29ab55921bf02cf5f618 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Apr 2025 07:51:48 -0700 Subject: [PATCH 236/919] bump version to 51.10 --- CMakeLists.txt | 2 +- docs/changelog.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fcc15168ba1..017edd4460a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.09") +set(DF_VERSION "51.10") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7c4babf881e..7c25e650f59 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,11 @@ Template for new versions: ## Removed +# 51.10-r1 + +## Misc Improvements +- Compatibility with DF 51.10 + # 51.09-r1 ## New Features From 69e72e4541c66bd89696cf7486db41c1803f84bf Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Apr 2025 13:18:35 -0500 Subject: [PATCH 237/919] Update the DFHack Logo stuff Make it one image file and clean up the references to it. --- data/art/logo.png | Bin 997 -> 1213 bytes data/art/logo_hovered.png | Bin 5124 -> 0 bytes docs/dev/Lua API.rst | 12 ++++++++---- plugins/lua/hotkeys.lua | 7 +++++-- 4 files changed, 13 insertions(+), 6 deletions(-) delete mode 100644 data/art/logo_hovered.png diff --git a/data/art/logo.png b/data/art/logo.png index 133dc5aaf8c98921bcc0203ebd986d4915c726cd..c34407b372cdb138314827e163ec38686cc42678 100644 GIT binary patch delta 1193 zcmV;a1Xlay2fYb@iBL{Q4GJ0x0000DNk~Le0000$0000a2m$~A0Jd(c#Q*>R0drDE zLIAGL9O(c61YJo)K~z|Uh#*Sn1`iPr0T%^HKnVT=HsV1L z-9!4ClijDBbIJs=B!bs;t|A5xfskZ#5sac@YpZ*ZF^v#^G2NFvZ0+G`r|859YHHYJ zzeA{c_3C@|x~jUq>bM9Yz!HDn_A%w}1AuSeerLokNxuQW?602~@mK%&yN{04ThPzt z`}eq*ES^Akc6iBu>Qa{Um3An3YI z2vJqFtFz00<*LmOu+cSv5AtIIt+$XM2=v~6uq+DzCMPGO(Wq_IG_6!BwQr2x-m4F-b%Aj@*cfimK6odfsx_ZBRc6D=e69D@A`)MCX^-cI7fA<7p zu^3%lU0sE?1&wmxefS{%r`MdYn$PFy$0N%!E>N6P8-s&`T?fjDzqJQWPEP0sMNx{y zBF)b8{MyN25{0FkV|2J?D2O&}cO1 zHJ8ul3xxszq|<2(e|~-r05dZ)=oX1YCMG7(+2`|Rv)R{%&WOL203H^}WD-w7P17vP z+TGm+09lsD#>N~yIgX1&BD6Rul}f>XU~pq&e*X4a49#4%s=5L;vaN>8Yuy_MtQ7cdAi5$u&&_0L!v)eyi2$&CLz+ z1_A*9C=?2oWjQ{CSeE5>yP@qm!j#{+2S!Fl?2K(ZK0abk0Py?$c(K^q+p`^iQ2jat zOh@#bzm>q<-5mftJUr0N7-?>9E*uWqJ}WCL0FX+huCK4@zIK3)(uEQK^Af65D#cl8@LN6$xX6F}FQ$J0(ergMw65}5 HlfMHdlqf{a delta 975 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 Z2o#j-Nb=lLw87Bo0{&|6lI80+{TEB3t%Cpn diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d449c8810bb..071ab48ecb5 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5945,6 +5945,8 @@ common text token lists that you can then pass as ``text`` to a ``Label``: Example 2: The DFHack logo - a graphical button in graphics mode and a text button in ASCII mode. The ASCII colors use the default for hovering:: + local logo_textures=dfhack.textures.loadTileset( + 'hack/data/art/logo.png', 8, 12, true), widgets.Label{ text=widgets.makeButtonLabelText{ chars={ @@ -5952,10 +5954,12 @@ common text token lists that you can then pass as ``text`` to a ``Label``: {179, 'H', 'a', 179}, {179, 'c', 'k', 179}, }, - tileset=dfhack.textures.loadTileset( - 'hack/data/art/logo.png', 8, 12, true), - tileset_hover=dfhack.textures.loadTileset( - 'hack/data/art/logo_hovered.png', 8, 12, true), + tileset=logo_textures, + tileset_offset=1, + tileset_stride=8, + tileset_hover=logo_textures, + tileset_hover_offset=5, + tileset_hover_stride=8, }, on_click=function() dfhack.run_command{'hotkeys', 'menu', self.name} diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 74d0b96231f..d0cecb5ba01 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -6,7 +6,6 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true) -local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12, true) local function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] @@ -41,7 +40,11 @@ function HotspotMenuWidget:init() {VERT_BAR, 'c', 'k', VERT_BAR}, }, tileset=logo_textures, - tileset_hover=logo_hovered_textures, + tileset_offset=1, + tileset_stride=8, + tileset_hover=logo_textures, + tileset_hover_offset=5, + tileset_hover_stride=8, }, on_click=function() dfhack.run_command{'hotkeys', 'menu', self.name} end, }, From aa803c6cf971444a7da391941166ebd1bec36ec8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:33:46 +0000 Subject: [PATCH 238/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index ec3ab067aef..4740f8967f4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ec3ab067aef716817d9c5b84a6fb85bc22516caa +Subproject commit 4740f8967f4a77c1228d8c98acb9996412a36f29 diff --git a/scripts b/scripts index d8912409bb3..43948f3cb10 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d8912409bb3c7398a2859aa0c0092d792c9211e7 +Subproject commit 43948f3cb10848b107ddf0e97cfbcc6c8c2bd0d9 From 12c07638ae77b4ab2d3e63fe0285257fc5a31ab1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Apr 2025 23:14:39 +0000 Subject: [PATCH 239/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 43948f3cb10..139ee179b93 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 43948f3cb10848b107ddf0e97cfbcc6c8c2bd0d9 +Subproject commit 139ee179b93471fa4e3d37fd5333c11a87f461cf From 6d9cfda41f4d85297169c287ea14c036ffe9653c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Apr 2025 18:03:53 -0700 Subject: [PATCH 240/919] fix conversion of enum token to value was not correctly handling enums that did not start at 0 --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 30 +++++++--------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8a037da96a0..65e7edcb491 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `stockpiles`: fix one-off error in item type when importing furniture stockpile settings ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index afd0a6dbc8c..6abab93baf3 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -210,24 +210,15 @@ bool StockpileSettingsSerializer::unserialize_from_file(color_ostream &out, cons /** * Find an enum's value based off the string label. - * @param traits the enum's trait struct * @param token the string value in key_table - * @return the enum's value, -1 if not found + * @return the enum's value, -1 if not found */ template -static typename df::enum_traits::base_type linear_index(df::enum_traits traits, const string& token) { - auto j = traits.first_item_value; - auto limit = traits.last_item_value; - // sometimes enums start at -1, which is bad news for array indexing - if (j < 0) { - j += abs(traits.first_item_value); - limit += abs(traits.first_item_value); - } - for (; j <= limit; ++j) { - if (token.compare(traits.key_table[j]) == 0) - return j; - } - return -1; +static typename df::enum_traits::base_type token_to_enum_val(const string& token) { + E val; + if (!find_enum_item(&val, token)) + return -1; + return val; } static bool matches_filter(color_ostream& out, const vector& filters, const string& name) { @@ -360,10 +351,9 @@ static void unserialize_list_quality(color_ostream& out, const char* subcat, boo } using df::enums::item_quality::item_quality; - df::enum_traits quality_traits; for (int i = 0; i < list_size; ++i) { const string quality = read_value(i); - df::enum_traits::base_type idx = linear_index(quality_traits, quality); + df::enum_traits::base_type idx = token_to_enum_val(quality); if (idx < 0) { WARN(log, out).print("invalid quality token: %s\n", quality.c_str()); continue; @@ -551,11 +541,9 @@ static void unserialize_list_item_type(color_ostream& out, const char* subcat, b } using df::enums::item_type::item_type; - df::enum_traits type_traits; for (int i = 0; i < list_size; ++i) { const string token = read_value(i); - // subtract one because item_type starts at -1 - const df::enum_traits::base_type idx = linear_index(type_traits, token) - 1; + const df::enum_traits::base_type idx = token_to_enum_val(token); if (!is_allowed((item_type)idx)) continue; if (idx < 0 || size_t(idx) >= num_elems) { @@ -1806,7 +1794,7 @@ void StockpileSettingsSerializer::read_furniture(color_ostream& out, Deserialize } else { for (int i = 0; i < bfurniture.type_size(); ++i) { const string token = bfurniture.type(i); - df::enum_traits::base_type idx = linear_index(type_traits, token); + df::enum_traits::base_type idx = token_to_enum_val(token); if (idx < 0 || size_t(idx) >= pfurniture.type.size()) { WARN(log, out).print("furniture type index invalid %s, idx=%d\n", token.c_str(), idx); continue; From aecadd7b1f8d45621f5d51f866ec7759c17b025d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 18:09:24 -0700 Subject: [PATCH 241/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 139ee179b93..1f7663b57bd 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 139ee179b93471fa4e3d37fd5333c11a87f461cf +Subproject commit 1f7663b57bdda6bb02dc2f950f3465df832b2fe6 From 6b642281f73d77f5f36ac0911304b7675cc3e7d7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 19:26:57 -0700 Subject: [PATCH 242/919] fix output boulder/rough gem material calculation --- docs/changelog.txt | 1 + plugins/dig-now.cpp | 58 +++++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 65e7edcb491..d880426aa7d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings +- `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index c3c79122fcc..16fe585157f 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -20,9 +20,11 @@ #include "modules/World.h" #include "df/building.h" +#include "df/builtin_mats.h" #include "df/historical_entity.h" #include "df/item.h" #include "df/map_block.h" +#include "df/material.h" #include "df/plotinfost.h" #include "df/reaction_product_itemst.h" #include "df/tile_designation.h" @@ -391,7 +393,7 @@ struct dug_tile_info { DFCoord pos; df::tiletype_material tmat; df::item_type itype; - int32_t imat; // mat idx of boulder/gem potentially generated at this pos + t_matpair imat; // matpair of boulder/gem potentially generated at this pos dug_tile_info(MapExtras::MapCache &map, const DFCoord &pos) { this->pos = pos; @@ -403,32 +405,32 @@ struct dug_tile_info { imat = -1; df::tiletype_shape shape = tileShape(tt); - if (shape == df::tiletype_shape::WALL || shape == df::tiletype_shape::FORTIFICATION) { - switch (tmat) { - case df::tiletype_material::STONE: - case df::tiletype_material::MINERAL: - case df::tiletype_material::FEATURE: - imat = map.baseMaterialAt(pos).mat_index; - break; - case df::tiletype_material::LAVA_STONE: - { - MaterialInfo mi; - if (mi.findInorganic("OBSIDIAN")) - imat = mi.index; - return; // itype should always be BOULDER, regardless of vein - } - default: - break; - } - } - - switch (map.BlockAtTile(pos)->veinTypeAt(pos)) { - case df::inclusion_type::CLUSTER_ONE: - case df::inclusion_type::CLUSTER_SMALL: - itype = df::item_type::ROUGH; + if (shape != df::tiletype_shape::WALL && shape != df::tiletype_shape::FORTIFICATION) + return; + + switch (tmat) { + case df::tiletype_material::STONE: + case df::tiletype_material::MINERAL: + case df::tiletype_material::FEATURE: + case df::tiletype_material::LAVA_STONE: + imat = map.baseMaterialAt(pos); break; - default: + case df::tiletype_material::FROZEN_LIQUID: + // assume frozen water + // we can't use baseMaterialAt here because it will return the underlying river bed material + imat = t_matpair(df::builtin_mats::WATER, -1); break; + default: + return; + } + + MaterialInfo mi; + mi.decode(imat); + if (mi.type == -1 || !mi.material) + return; + + if (mi.material->isGem()) { + itype = df::item_type::ROUGH; } } }; @@ -737,7 +739,7 @@ static bool produces_item(const boulder_percent_options &options, return rng.random(100) < probability; } -typedef std::map, std::vector> +typedef std::map, std::vector> item_coords_t; static void do_dig(color_ostream &out, std::vector &dug_coords, @@ -876,8 +878,8 @@ static void create_boulders(color_ostream &out, prod->item_type = entry.first.first; prod->item_subtype = -1; - prod->mat_type = 0; - prod->mat_index = entry.first.second; + prod->mat_type = entry.first.second.mat_type; + prod->mat_index = entry.first.second.mat_index; prod->probability = 100; prod->product_dimension = 1; From 06a038fefe050db9193e93931621fd907d0ce5a0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 19:27:40 -0700 Subject: [PATCH 243/919] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d880426aa7d..cd43392b786 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls +- `dig-now`: properly generate ice boulders when digging through ice walls ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From d9d2c7a60a3c2867ba06a9b68e7b57c89df3d626 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:12:18 -0700 Subject: [PATCH 244/919] define -> operator for DF linked lists --- library/include/BitArray.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 3679323eb8f..bad77d79355 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -318,6 +318,22 @@ namespace DFHack return cur->item; } + I * operator->() + { + CHECK_NULL_POINTER(root); + CHECK_NULL_POINTER(cur); + + return cur->item; + } + + I * operator->() const + { + CHECK_NULL_POINTER(root); + CHECK_NULL_POINTER(cur); + + return cur->item; + } + operator const_iterator() const { return const_iterator(*this); From 3565121aebe9376abff451cf013d9074ead321f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:15:56 -0700 Subject: [PATCH 245/919] generalize linked_list_remove so it can take a predicate --- library/include/MiscUtils.h | 53 +++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 8568a55c0a2..c916a6a9fbf 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -309,24 +309,43 @@ Link *linked_list_insert_after(Link *pos, Link *link) } /** - * Returns true if the item with id idToRemove was found, deleted, and removed - * from the list. Otherwise returns false. + * Returns true if an item that matches the given function was found, deleted, + * and removed from the list. Otherwise returns false. Only removes the first + * match. + * + * Example usage: + * + * linked_list_remove(&world->projectiles.all, [&](df::projectile *proj) { + * if (proj->getType() != df::enums::projectile_type::Unit) + * return false; + * if (auto unit_proj = virtual_cast(proj)) + * return unit_proj->unit == unit; + * return false; + * }); */ -template -bool linked_list_remove(Link *head, int32_t idToRemove) -{ - for (Link *link = head; link; link = link->next) - { - if (!link->item || link->item->id != idToRemove) - continue; - - link->prev->next = link->next; - if (link->next) - link->next->prev = link->prev; - delete(link); - return true; - } - return false; +template F> +bool linked_list_remove(L *list, F matches) { + auto matches_wrapper = [&](L::iterator::value_type item) { + return item && matches(item); + }; + auto it = std::find_if(list->begin(), list->end(), matches_wrapper); + if (it == list->end()) + return false; + auto item = *it; + list->erase(it); + delete item; + return true; +} + +/** + * Returns true if the item with id idToRemove was found, deleted, and + * removed from the list. Otherwise returns false. + */ +template +bool linked_list_remove(L *list, int32_t idToRemove) { + return linked_list_remove(list, [&](L::iterator::value_type item) { + return item->id == idToRemove; + }); } template From 0b7c04bb156d835547405a5088d7ae06c01cf9bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:16:17 -0700 Subject: [PATCH 246/919] remove projectile info for teleported units otherwise they will be teleported back to their next projectile targeted tile on the next tick --- docs/changelog.txt | 2 ++ library/modules/Units.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index cd43392b786..5d64c50a789 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls +- `gui/teleport`: now properly handles teleporting units that are currently falling or being flung ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) @@ -67,6 +68,7 @@ Template for new versions: ## API - ``Military`` module: added ``addToSquad`` function +- ``Units::teleport``: projectile information is now cleared for teleported units ## Lua - ``dfhack.military.addToSquad``: expose Military API function diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 21f39932fe1..e604562c77c 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -67,6 +67,7 @@ distribution. #include "df/nemesis_record.h" #include "df/personality_goalst.h" #include "df/plotinfost.h" +#include "df/proj_unitst.h" #include "df/reputation_profilest.h" #include "df/syndrome.h" #include "df/tile_occupancy.h" @@ -773,6 +774,18 @@ bool Units::teleport(df::unit *unit, df::coord target_pos) else old_occ->bits.unit = false; + // Clear unit projectile info + if (unit->flags1.bits.projectile) { + unit->flags1.bits.projectile = false; + linked_list_remove(&world->projectiles.all, [&](df::projectile *proj) { + if (proj->getType() != df::enums::projectile_type::Unit) + return false; + if (auto unit_proj = virtual_cast(proj)) + return unit_proj->unit == unit; + return false; + }); + } + // If there's already somebody standing at the destination, then force the unit to lay down if (new_occ->bits.unit) unit->flags1.bits.on_ground = true; From 3b9d731f71bd49123b09177f877fce99ccda6646 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:31:21 -0700 Subject: [PATCH 247/919] don't show hover tooltip for hidden creatures --- docs/changelog.txt | 1 + plugins/lua/spectate.lua | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index cd43392b786..e311e6441d7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `spectate`: don't show a hover tooltip for hidden units (e.g. invisible snatchers) - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index fd2f736d0e3..8c7e653a7b5 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -345,11 +345,15 @@ local function GetUnitInfoText(unit, settings_group_name) return txt end +local function unit_filter(unit) + return not dfhack.units.isHidden(unit) +end + local function GetHoverText(pos) if not pos then return end local txt = {} - local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? + local units = dfhack.units.getUnitsInBox(pos, pos, unit_filter) or {} for _,unit in ipairs(units) do local info = GetUnitInfoText(unit, 'hover') From 4f75aa4a6e59f37daff9b94f51f6a185b2dbc8c0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 6 Apr 2025 14:36:09 -0700 Subject: [PATCH 248/919] try to make msvc happy --- library/include/MiscUtils.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index c916a6a9fbf..2b9426202d1 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -328,8 +328,8 @@ bool linked_list_remove(L *list, F matches) { auto matches_wrapper = [&](L::iterator::value_type item) { return item && matches(item); }; - auto it = std::find_if(list->begin(), list->end(), matches_wrapper); - if (it == list->end()) + typename L::const_iterator it = std::find_if(list->cbegin(), list->cend(), matches_wrapper); + if (it == list->cend()) return false; auto item = *it; list->erase(it); From 118861d67943b8f2f32b10970f3608b65f88cb60 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:32:50 +0000 Subject: [PATCH 249/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.31.2 → 0.32.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.2...0.32.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6904eb760c2..8348cb4d85a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.2 + rev: 0.32.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 4eb3ec703ebe2acda71457a68ccc09d444bd9b5d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 14:58:46 -0500 Subject: [PATCH 250/919] add `army-controller-sanity` plugin --- plugins/CMakeLists.txt | 1 + plugins/army-controller-sanity.cpp | 133 +++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 plugins/army-controller-sanity.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8f230ff583e..3ef551a49e5 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -35,6 +35,7 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) + dfhack_plugin(army-controller-sanity army-controller-sanity.cpp) dfhack_plugin(add-spatter add-spatter.cpp) dfhack_plugin(aquifer aquifer.cpp LINK_LIBRARIES lua) dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp new file mode 100644 index 00000000000..6e5efceb39c --- /dev/null +++ b/plugins/army-controller-sanity.cpp @@ -0,0 +1,133 @@ +#include "Debug.h" +#include "PluginManager.h" + +#include "df/world.h" +#include "df/army_controller.h" +#include "df/army.h" +#include "df/historical_entity.h" +#include "df/unit.h" +#include "df/global_objects.h" + +#include + +DFHACK_PLUGIN("army-controller-sanity"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(army_controller_next_id); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(pause_state); + +namespace DFHack { + DBG_DECLARE(army_controller_sanity, log, DebugCategory::LWARNING); +} + +namespace { + bool checkArmyControllerSanity() + { + // army controllers are found: + // in viewscreen_worldst (army_controller (list), last_hover_ac) (not checked) + // in entitst (army_controller (list)) + // in armyst (controller) + // in unitst (army_controller) + // + // master list is in army_controller_handlerst + + static size_t last_ac_vec_size = 0; + static int last_army_controller_next_id = 0; + + if (last_army_controller_next_id == *df::global::army_controller_next_id && + last_ac_vec_size == df::global::world->army_controllers.all.size()) + return true; + + std::unordered_set ac_set{}; + + for (auto ac : df::global::world->army_controllers.all) + { + ac_set.insert(ac); + } + + bool ok = true; + + for (auto ent : df::global::world->entities.all) + { + for (auto ac : ent->army_controllers) + { + if (ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller %08lx found in entity id %d\n", ac, ent->id); + ok = false; + } + } + } + + for (auto ar : df::global::world->armies.all) + { + auto ac = ar->controller; + if (ac && ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller %08lx found in army id %d\n", ac, ar->id); + ok = false; + } + else if (ac && ac->id != ar->controller_id) + { + WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); + ok = false; + } + else if (!ac && ar->controller_id != -1) + { + WARN(log).print("acValidationError: army %d has nonzero controller %d but controller pointer is null\n", ar->id, ar->controller_id); + ok = false; + } + } + + for (auto un : df::global::world->units.all) + { + auto ac = un->enemy.army_controller; + if (ac && ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller %08lx found in unit id %d\n", ac, un->id); + ok = false; + } + else if (ac && ac->id != un->enemy.army_controller_id) + { + WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); + ok = false; + } + else if (!ac && un->enemy.army_controller_id != -1) + { + WARN(log).print("acValidationError: unit %d has has nonzero controller %d but controller pointer is null\n", un->id, un->enemy.army_controller_id); + ok = false; + } + } + + last_army_controller_next_id = *df::global::army_controller_next_id; + last_ac_vec_size = df::global::world->army_controllers.all.size(); + + INFO(log).print("acValidation: controller count = %d, next id = %d, season tick count = %d\n", + last_ac_vec_size, last_army_controller_next_id, *df::global::cur_year_tick); + + return ok; + } +} + +DFhackCExport DFHack::command_result plugin_init(DFHack::color_ostream& out, std::vector & commands) +{ + return DFHack::CR_OK; +} + +DFhackCExport DFHack::command_result plugin_enable(DFHack::color_ostream& out, bool enable) +{ + is_enabled = enable; + return DFHack::CR_OK; +} + +DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) +{ + if (is_enabled) + { + bool ok = checkArmyControllerSanity(); + if (!ok) { + ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); + *df::global::pause_state = true; + is_enabled = false; + } + } + return DFHack::CR_OK; +} \ No newline at end of file From ab1922baf67f82e65bd44bc80441e6d9901f336b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 15:23:56 -0500 Subject: [PATCH 251/919] make gcc happy --- plugins/army-controller-sanity.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 6e5efceb39c..7dd3abf92d8 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -53,7 +53,7 @@ namespace { for (auto ac : ent->army_controllers) { if (ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %08lx found in entity id %d\n", ac, ent->id); + WARN(log).print("acValidationError: Bad controller %p found in entity id %d\n", ac, ent->id); ok = false; } } @@ -63,12 +63,12 @@ namespace { { auto ac = ar->controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %08lx found in army id %d\n", ac, ar->id); + WARN(log).print("acValidationError: Bad controller %p found in army id %d\n", ac, ar->id); ok = false; } else if (ac && ac->id != ar->controller_id) { - WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); + WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); ok = false; } else if (!ac && ar->controller_id != -1) @@ -82,12 +82,12 @@ namespace { { auto ac = un->enemy.army_controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %08lx found in unit id %d\n", ac, un->id); + WARN(log).print("acValidationError: Bad controller %p found in unit id %d\n", ac, un->id); ok = false; } else if (ac && ac->id != un->enemy.army_controller_id) { - WARN(log).print("acValidationError: controller %08lx id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); + WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); ok = false; } else if (!ac && un->enemy.army_controller_id != -1) @@ -100,7 +100,7 @@ namespace { last_army_controller_next_id = *df::global::army_controller_next_id; last_ac_vec_size = df::global::world->army_controllers.all.size(); - INFO(log).print("acValidation: controller count = %d, next id = %d, season tick count = %d\n", + INFO(log).print("acValidation: controller count = %ld, next id = %d, season tick count = %d\n", last_ac_vec_size, last_army_controller_next_id, *df::global::cur_year_tick); return ok; From 8946176318f7479754d184f0257a07d7acc9c1a0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 15:33:03 -0500 Subject: [PATCH 252/919] eof --- plugins/army-controller-sanity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 7dd3abf92d8..19f8d74a5b8 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -130,4 +130,4 @@ DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) } } return DFHack::CR_OK; -} \ No newline at end of file +} From 174018f1ccb149b917afd649e910b5619636aae8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 7 Apr 2025 21:13:54 -0500 Subject: [PATCH 253/919] yay globalism --- plugins/army-controller-sanity.cpp | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 19f8d74a5b8..3f4ac0b1c32 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -16,6 +16,7 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(army_controller_next_id); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(pause_state); +REQUIRE_GLOBAL(cur_year_tick); namespace DFHack { DBG_DECLARE(army_controller_sanity, log, DebugCategory::LWARNING); @@ -35,20 +36,20 @@ namespace { static size_t last_ac_vec_size = 0; static int last_army_controller_next_id = 0; - if (last_army_controller_next_id == *df::global::army_controller_next_id && - last_ac_vec_size == df::global::world->army_controllers.all.size()) + if (last_army_controller_next_id == *army_controller_next_id && + last_ac_vec_size == world->army_controllers.all.size()) return true; std::unordered_set ac_set{}; - for (auto ac : df::global::world->army_controllers.all) + for (auto ac : world->army_controllers.all) { ac_set.insert(ac); } bool ok = true; - for (auto ent : df::global::world->entities.all) + for (auto ent : world->entities.all) { for (auto ac : ent->army_controllers) { @@ -59,7 +60,7 @@ namespace { } } - for (auto ar : df::global::world->armies.all) + for (auto ar : world->armies.all) { auto ac = ar->controller; if (ac && ac_set.count(ac) == 0) { @@ -78,7 +79,7 @@ namespace { } } - for (auto un : df::global::world->units.all) + for (auto un : world->units.all) { auto ac = un->enemy.army_controller; if (ac && ac_set.count(ac) == 0) { @@ -97,11 +98,11 @@ namespace { } } - last_army_controller_next_id = *df::global::army_controller_next_id; - last_ac_vec_size = df::global::world->army_controllers.all.size(); + last_army_controller_next_id = *army_controller_next_id; + last_ac_vec_size = world->army_controllers.all.size(); INFO(log).print("acValidation: controller count = %ld, next id = %d, season tick count = %d\n", - last_ac_vec_size, last_army_controller_next_id, *df::global::cur_year_tick); + last_ac_vec_size, last_army_controller_next_id, *cur_year_tick); return ok; } @@ -120,14 +121,11 @@ DFhackCExport DFHack::command_result plugin_enable(DFHack::color_ostream& out, b DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) { - if (is_enabled) - { - bool ok = checkArmyControllerSanity(); - if (!ok) { - ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); - *df::global::pause_state = true; - is_enabled = false; - } + bool ok = checkArmyControllerSanity(); + if (!ok) { + ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); + *pause_state = true; + is_enabled = false; } return DFHack::CR_OK; } From e66973008ca3bcce8eef1e6179c2e86843ab6af2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 8 Apr 2025 07:19:23 +0000 Subject: [PATCH 254/919] Auto-update submodules library/xml: master scripts: master depends/dfhooks: main --- depends/dfhooks | 2 +- library/xml | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 364f71f7454..ca30a90f8a3 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 364f71f74544c37f47a4ddfebcb9d6b9e4cdfcce +Subproject commit ca30a90f8a3f9cc61531ac24430fd2434fff40ec diff --git a/library/xml b/library/xml index 4740f8967f4..8f4b816a52f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4740f8967f4a77c1228d8c98acb9996412a36f29 +Subproject commit 8f4b816a52f94c95772100cd4652a5a43697059e diff --git a/scripts b/scripts index 1f7663b57bd..bbba0a3b3c0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1f7663b57bdda6bb02dc2f950f3465df832b2fe6 +Subproject commit bbba0a3b3c0697b6760f308e22fb5b300bf84826 From 2ded1754efd2c7cb196abb4e58b1866cfb3a21eb Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 8 Apr 2025 19:02:02 -0700 Subject: [PATCH 255/919] Also set filter for follow tooltip --- plugins/lua/spectate.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 8c7e653a7b5..0cb2d3bd33b 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -465,7 +465,7 @@ function TooltipOverlay:render_unit_banners(dc) local height = vp.max_y local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} - local units = dfhack.units.getUnitsInBox(topleft, bottomright) + local units = dfhack.units.getUnitsInBox(topleft, bottomright, unit_filter) if not units or #units == 0 then return end local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) From 77ea136b09ccb516b44bdd2ff5dcd13aac0332b4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 10 Apr 2025 18:26:19 -0700 Subject: [PATCH 256/919] fix docs for `dfhack.gui.getCurFocus()` --- docs/dev/Lua API.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 795df5b3773..b11c8862b21 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1073,7 +1073,8 @@ Screens * ``dfhack.gui.getCurFocus([skip_dismissed])`` - Returns the focus string of the current viewscreen. + Returns a list of focus strings for the current viewscreen. Equivalent to + ``dfhack.gui.getFocusStrings(dfhack.gui.getCurViewscreen(skip_dismissed))``. * ``dfhack.gui.getViewscreenByType(type[, depth])`` From 3745107f98390cfaab0df0db4006f58f5ab29fbf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 11 Apr 2025 15:38:01 -0500 Subject: [PATCH 257/919] code modernization this commit changes several templates to have more precisely constrained match patterns to yield more clear compilation errors when a mistake is made certain `Lua::Push` overloads have been made either more or less general; a side effect is that more "string-like" objects (notably, `std::string_view`) will be converted to lua strings on push a chunk of ancient dead code bracketed by `#f 0` was removed a declaration for the undefined and unused `Lua::Core::Init` was removed it also replaces several uses of `enable_if` with `if constexpr` or templates constrained by appropriate types, to make that code more readable some other minor cleanups, mostly around namespaces blocks,, are also included --- library/include/DataDefs.h | 151 +++++++++++++++++-------------------- library/include/LuaTools.h | 63 +++++++--------- plugins/debug.cpp | 34 ++++----- 3 files changed, 113 insertions(+), 135 deletions(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 67380544235..59f47abcb65 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -494,22 +494,18 @@ namespace df using DFHack::DfLinkedList; using DFHack::DfOtherVectors; - template - typename std::enable_if< - std::is_copy_assignable::value, - void* - >::type allocator_try_assign(void *out, const void *in) { - *(T*)out = *(const T*)in; - return out; - } + template concept copy_assignable = std::assignable_from && std::assignable_from; - template - typename std::enable_if< - !std::is_copy_assignable::value, - void* - >::type allocator_try_assign(void *out, const void *in) { - // assignment is not possible; do nothing - return NULL; + template + void* allocator_try_assign(void *out, const void *in) { + if constexpr (copy_assignable) { + *(T*)out = *(const T*)in; + return out; + } + else { + // assignment is not possible; do nothing + return nullptr; + } } #pragma GCC diagnostic push @@ -651,98 +647,87 @@ namespace DFHack { /** * Return the next item in the enum, wrapping to the first one at the end if 'wrap' is true (otherwise an invalid item). */ - template - inline typename std::enable_if< - !df::enum_traits::is_complex, - typename df::enum_traits::enum_type - >::type next_enum_item(T v, bool wrap = true) + template concept complex_enum = (df::enum_traits::is_complex); + + template + inline auto next_enum_item(T v, bool wrap = true) -> typename df::enum_traits::enum_type { - typedef df::enum_traits traits; - typedef typename traits::base_type base_type; - base_type iv = base_type(v); - if (iv < traits::last_item_value) - { - return T(iv + 1); - } - else + using traits = df::enum_traits; + + if constexpr (complex_enum) { - if (wrap) - return traits::first_item; + const auto& complex = traits::complex; + const auto it = complex.value_index_map.find(v); + if (it != complex.value_index_map.end()) + { + if (!wrap && it->second + 1 == complex.size()) + { + return T(traits::last_item_value + 1); + } + size_t next_index = (it->second + 1) % complex.size(); + return T(complex.index_value_map[next_index]); + } else return T(traits::last_item_value + 1); } - } - - template - inline typename std::enable_if< - df::enum_traits::is_complex, - typename df::enum_traits::enum_type - >::type next_enum_item(T v, bool wrap = true) - { - typedef df::enum_traits traits; - const auto &complex = traits::complex; - const auto it = complex.value_index_map.find(v); - if (it != complex.value_index_map.end()) + else { - if (!wrap && it->second + 1 == complex.size()) + using base_type = typename traits::base_type; + base_type iv = base_type(v); + if (iv < traits::last_item_value) { - return T(traits::last_item_value + 1); + return T(iv + 1); + } + else + { + if (wrap) + return traits::first_item; + else + return T(traits::last_item_value + 1); } - size_t next_index = (it->second + 1) % complex.size(); - return T(complex.index_value_map[next_index]); } - else - return T(traits::last_item_value + 1); } /** * Check if the value is valid for its enum type. */ - template - inline typename std::enable_if< - !df::enum_traits::is_complex, - bool - >::type is_valid_enum_item(T v) + template + inline bool is_valid_enum_item(T v) { - return df::enum_traits::is_valid(v); + if constexpr (complex_enum) + { + const auto& complex = df::enum_traits::complex; + return complex.value_index_map.find(v) != complex.value_index_map.end(); + } + else + { + return df::enum_traits::is_valid(v); + } } - template - inline typename std::enable_if< - df::enum_traits::is_complex, - bool - >::type is_valid_enum_item(T v) - { - const auto &complex = df::enum_traits::complex; - return complex.value_index_map.find(v) != complex.value_index_map.end(); - } /** * Return the enum item key string pointer, or NULL if none. */ template - inline typename std::enable_if< - !df::enum_traits::is_complex, - const char * - >::type enum_item_raw_key(T val) { - typedef df::enum_traits traits; - return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL; - } - - template - inline typename std::enable_if< - df::enum_traits::is_complex, - const char * - >::type enum_item_raw_key(T val) { - typedef df::enum_traits traits; - const auto &value_index_map = traits::complex.value_index_map; - auto it = value_index_map.find(val); - if (it != value_index_map.end()) - return traits::key_table[it->second]; + const char* enum_item_raw_key(T val) { + using traits = df::enum_traits; + if constexpr (complex_enum) + { + const auto& value_index_map = traits::complex.value_index_map; + auto it = value_index_map.find(val); + if (it != value_index_map.end()) + return traits::key_table[it->second]; + else + return nullptr; + } else - return NULL; + { + return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : nullptr; + } } + /** * Return the enum item key string pointer, or "?" if none. */ @@ -773,7 +758,7 @@ namespace DFHack { */ template inline bool find_enum_item(T *var, const std::string &name) { - typedef df::enum_traits traits; + using traits = df::enum_traits; int size = traits::last_item_value-traits::first_item_value+1; int idx = findEnumItem(name, size, traits::key_table); if (idx < 0) return false; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 51ae098f493..8b71862ec3d 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -32,6 +32,7 @@ distribution. #include #include #include +#include #include "Core.h" #include "ColorText.h" @@ -60,7 +61,7 @@ namespace DFHack { }; } -namespace DFHack {namespace Lua { +namespace DFHack::Lua { /** * Create or initialize a lua interpreter with access to DFHack tools. */ @@ -140,10 +141,15 @@ namespace DFHack {namespace Lua { DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, void *target, int val_index, bool exact_type = false); + template concept df_object = requires(T x) + { + { df::identity_traits::get() } -> std::convertible_to; + }; + /** * Push the pointer onto the stack as a wrapped DF object of a specific type. */ - template + template void PushDFObject(lua_State *state, T *ptr) { PushDFObject(state, df::identity_traits::get(), ptr); } @@ -151,7 +157,7 @@ namespace DFHack {namespace Lua { /** * Check that the value is a wrapped DF object of the correct type, and if so return the pointer. */ - template + template T *GetDFObject(lua_State *state, int val_index, bool exact_type = false) { return (T*)GetDFObject(state, df::identity_traits::get(), val_index, exact_type); } @@ -159,7 +165,7 @@ namespace DFHack {namespace Lua { /** * Check that the value is a wrapped DF object of the correct type, and if so return the pointer. Otherwise throw an argument type error. */ - template + template T *CheckDFObject(lua_State *state, int val_index, bool exact_type = false) { return (T*)CheckDFObject(state, df::identity_traits::get(), val_index, exact_type); } @@ -167,7 +173,7 @@ namespace DFHack {namespace Lua { /** * Assign the value at val_index to the target using df.assign(). */ - template + template bool AssignDFObject(color_ostream &out, lua_State *state, T *target, int val_index, bool exact_type = false, bool perr = true) { return AssignDFObject(out, state, df::identity_traits::get(), @@ -178,7 +184,7 @@ namespace DFHack {namespace Lua { * Assign the value at val_index to the target using df.assign(). * Throws in case of an error. */ - template + template void CheckDFAssign(lua_State *state, T *target, int val_index, bool exact_type = false) { CheckDFAssign(state, df::identity_traits::get(), target, val_index, exact_type); } @@ -298,42 +304,33 @@ namespace DFHack {namespace Lua { /** * Push utility functions */ -#if 0 -#define NUMBER_PUSH(type) inline void Push(lua_State *state, type value) { lua_pushnumber(state, value); } - NUMBER_PUSH(char) - NUMBER_PUSH(int8_t) NUMBER_PUSH(uint8_t) - NUMBER_PUSH(int16_t) NUMBER_PUSH(uint16_t) - NUMBER_PUSH(int32_t) NUMBER_PUSH(uint32_t) - NUMBER_PUSH(int64_t) NUMBER_PUSH(uint64_t) - NUMBER_PUSH(float) NUMBER_PUSH(double) -#undef NUMBER_PUSH -#else - template - inline typename std::enable_if::value || std::is_enum::value>::type - Push(lua_State *state, T value) { + template concept lua_integral = (std::is_integral_v || std::is_enum_v); + + inline void Push(lua_State *state, lua_integral auto value) { lua_pushinteger(state, value); } - template - inline typename std::enable_if::value>::type - Push(lua_State *state, T value) { + inline void Push(lua_State* state, std::floating_point auto value) { lua_pushnumber(state, lua_Number(value)); } -#endif inline void Push(lua_State *state, bool value) { lua_pushboolean(state, value); } - inline void Push(lua_State *state, const char *str) { - lua_pushstring(state, str); - } - inline void Push(lua_State *state, const std::string &str) { - lua_pushlstring(state, str.data(), str.size()); + + template concept lua_string = (std::convertible_to); + + inline void Push(lua_State *state, lua_string auto str) { + std::string_view sv{ str }; + lua_pushlstring(state, sv.data(), sv.size()); } + DFHACK_EXPORT void Push(lua_State *state, const df::coord &obj); DFHACK_EXPORT void Push(lua_State *state, const df::coord2d &obj); void Push(lua_State *state, const Units::NoblePosition &pos); DFHACK_EXPORT void Push(lua_State *state, const MaterialInfo &info); DFHACK_EXPORT void Push(lua_State *state, const Screen::Pen &info); - template inline void Push(lua_State *state, T *ptr) { + + template inline void Push(lua_State *state, T *ptr) + { PushDFObject(state, ptr); } @@ -472,10 +469,6 @@ namespace DFHack {namespace Lua { * All accesses must be done under CoreSuspender. */ namespace Core { -// DFHACK_EXPORT extern lua_State *State; - - // Not exported; for use by the Core class - lua_State* Init(color_ostream &out); DFHACK_EXPORT void Reset(color_ostream &out, const char *where); // Events signalled by the core @@ -559,7 +552,7 @@ namespace DFHack {namespace Lua { Notification(function_identity_base *handler = NULL) : state(NULL), key(NULL), handler(handler), count(0) {} - int get_listener_count() { return count; } + int get_listener_count() const { return count; } lua_State *get_state() { return state; } function_identity_base *get_handler() { return handler; } @@ -572,7 +565,7 @@ namespace DFHack {namespace Lua { void bind(lua_State *state, const char *name); void bind(lua_State *state, void *key); }; -}} +} #define DEFINE_LUA_EVENT_0(name, handler) \ static DFHack::Lua::Notification name##_event(df::wrap_function(handler, true)); \ diff --git a/plugins/debug.cpp b/plugins/debug.cpp index c3f336afc05..eb2fc4c1c7a 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -48,30 +48,30 @@ DBG_DECLARE(debug,example,DebugCategory::LINFO); namespace serialization { -template -struct nvp : public std::pair { - using parent_t = std::pair; - nvp(const char* name, T& value) : - parent_t{name, &value} - {} -}; + template + struct nvp : public std::pair { + using parent_t = std::pair; + nvp(const char* name, T& value) : + parent_t{name, &value} + {} + }; -template -nvp make_nvp(const char* name, T& value) { - return {name, value}; -} + template + nvp make_nvp(const char* name, T& value) { + return {name, value}; + } } #define NVP(variable) serialization::make_nvp(#variable, variable) namespace Json { -template -typename std::enable_if::value, ET>::type -get(Json::Value& ar, const std::string &key, const ET& default_) -{ - return static_cast(as(ar.get(key, static_cast(default_)))); -} + template + requires (std::is_enum_v) + ET get(Value& ar, const std::string &key, const ET& default_) + { + return static_cast(as(ar.get(key, static_cast(default_)))); + } } namespace DFHack { namespace debugPlugin { From f6e4ed493562df0eabead5fdd6b96bde98b0dd44 Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Sat, 12 Apr 2025 02:04:30 -0400 Subject: [PATCH 258/919] L803: Added reload bool to catch whether the reload command was given in Core::runCommand ---- L831: Added reload to else if statement to short circuit the PluginManager::reload statement so that it only runs if reload was given as an argument to Core::runCommand --- library/Core.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index 227c4378f02..2b02c46fe44 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -800,6 +800,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s bool all = false; bool load = (first == "load"); bool unload = (first == "unload"); + bool reload = (first == "reload"); if (parts.size()) { for (auto p = parts.begin(); p != parts.end(); p++) @@ -828,7 +829,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s ret = CR_FAILURE; else if (unload && !plug_mgr->unload(*p)) ret = CR_FAILURE; - else if (!plug_mgr->reload(*p)) + else if (reload && !plug_mgr->reload(*p)) ret = CR_FAILURE; } if (ret != CR_OK) From 426733615ff8cf8b707cabcc449da1a4f577213c Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Sat, 12 Apr 2025 02:10:09 -0400 Subject: [PATCH 259/919] Updated Changelog to Core.cpp module reload fixes --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d65aeace62d..01a59a83814 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung +- `Core.cpp`: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From 8ebf0362eb78b66f07eda6012bbccad18de77150 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 12 Apr 2025 05:10:40 -0700 Subject: [PATCH 260/919] Fix format in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 01a59a83814..23839e179a2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,7 +61,7 @@ Template for new versions: - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung -- `Core.cpp`: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() +- ``Core.cpp``: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From 8cde8f7f36703d391510bf9f9bb8810d0922f42e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 12 Apr 2025 14:16:26 -0700 Subject: [PATCH 261/919] also unbreak unload -a --- docs/changelog.txt | 2 +- library/Core.cpp | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23839e179a2..310c5386524 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,7 +61,7 @@ Template for new versions: - `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung -- ``Core.cpp``: now properly handles "load", "unload", and "reload" plugin commands given to Core::runCommand() +- `unload`: fix recent regression where `unload` would immediately `reload` the target ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/library/Core.cpp b/library/Core.cpp index 2b02c46fe44..6e875648fbd 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -818,19 +818,22 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s ret = CR_FAILURE; else if (unload && !plug_mgr->unloadAll()) ret = CR_FAILURE; - else if (!plug_mgr->reloadAll()) + else if (reload && !plug_mgr->reloadAll()) ret = CR_FAILURE; } - for (auto p = parts.begin(); p != parts.end(); p++) + else { - if (!p->size() || (*p)[0] == '-') - continue; - if (load && !plug_mgr->load(*p)) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(*p)) - ret = CR_FAILURE; - else if (reload && !plug_mgr->reload(*p)) - ret = CR_FAILURE; + for (auto p = parts.begin(); p != parts.end(); p++) + { + if (!p->size() || (*p)[0] == '-') + continue; + if (load && !plug_mgr->load(*p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(*p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(*p)) + ret = CR_FAILURE; + } } if (ret != CR_OK) con.printerr("%s failed\n", first.c_str()); From e7860b4c83ffb404c8928bb76807ab07e302c9ea Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 12 Apr 2025 14:32:37 -0700 Subject: [PATCH 262/919] restore overlays after running unit tests --- ci/test.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/test.lua b/ci/test.lua index c0d021d0a60..a4846520ec7 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -6,6 +6,7 @@ local gui = require('gui') local helpdb = require('helpdb') local json = require('json') local mock = require('test_util.mock') +local overlay = require('plugins.overlay') local script = require('gui.script') local utils = require('utils') @@ -427,6 +428,7 @@ end local function finish_tests(done_command) dfhack.internal.IN_TEST = false + overlay.rescan() if done_command and #done_command > 0 then dfhack.run_command(done_command) end From 436b6408134c014efbfe2ae668f030d59dc3de25 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:59:37 +0000 Subject: [PATCH 263/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index bbba0a3b3c0..c0ea8aa176b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit bbba0a3b3c0697b6760f308e22fb5b300bf84826 +Subproject commit c0ea8aa176ba93aafe52eb8592d5aafa1c323ba8 From 090aef032531196a37eadb9e44feb35c6ec81d5a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 13 Apr 2025 15:54:40 +0000 Subject: [PATCH 264/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index c0ea8aa176b..bb096b27333 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit c0ea8aa176ba93aafe52eb8592d5aafa1c323ba8 +Subproject commit bb096b27333207c65974ed90bf6a9884d6d287c8 From 02d117c7d50dd8d23c8270bdd6d2e7fabd81745e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 13 Apr 2025 12:11:36 -0500 Subject: [PATCH 265/919] add safety check to stockpile iterator the stockpile iterator will crash if a `map_block` contains an item pointer to an item that is not on the master item list. this should never happen if DF was working correctly, but DF often does not work correctly related discord discussion: https://discord.com/channels/329272032778780672/1049402643342168114/1360977656820273322 --- docs/changelog.txt | 1 + library/modules/Buildings.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 310c5386524..ee422916ef4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: - `dig-now`: properly generate ice boulders when digging through ice walls - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung - `unload`: fix recent regression where `unload` would immediately `reload` the target +- ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 1135fb5307f..df31b1de7b3 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -1654,7 +1654,7 @@ StockpileIterator& StockpileIterator::operator++() { // If the current item isn't properly stored, move on to the next. item = df::item::find(block->items[current]); - if (!item->flags.bits.on_ground) { + if (!item || !item->flags.bits.on_ground) { continue; } From 72260531287669d446c3d66477d895d23b9d1714 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:41:51 +0000 Subject: [PATCH 266/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index bb096b27333..32203322174 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit bb096b27333207c65974ed90bf6a9884d6d287c8 +Subproject commit 322033221745e9cbf537d59152138df385c505b7 From f21aa92a76039b6eb218bb68bdc03e9e8d91bfef Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 14 Apr 2025 15:58:54 -0500 Subject: [PATCH 267/919] add another test to army controller sanity --- plugins/army-controller-sanity.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 3f4ac0b1c32..089b626b5f8 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -57,6 +57,10 @@ namespace { WARN(log).print("acValidationError: Bad controller %p found in entity id %d\n", ac, ent->id); ok = false; } + if (ac_set.count(ac) != 0 && ac->entity_id != ent->id) + { + WARN(log).print("acValidationError: Army controller %d has entity id %d but is linked from entity with id %d\n", ac->id, ac->entity_id, ent->id); + } } } From eb1ae67b2c7ef706b9ba008d1be48cb402dd4fae Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 15 Apr 2025 07:21:20 +0000 Subject: [PATCH 268/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index d0c14fa0a67..d65c4ac5602 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit d0c14fa0a6702f396e147bbd3c9b325c0677aa2b +Subproject commit d65c4ac560264c4a0d69a802bc31b636c3da55fc From 9e99ca4dc560c3fda29ee0b318dfbd79bbf1f3a6 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Fri, 18 Apr 2025 12:34:57 +0100 Subject: [PATCH 269/919] Add star icon to gui design --- data/art/design.png | Bin 1491 -> 1732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/art/design.png b/data/art/design.png index 787b9962a1bc68d6d5d46bfa9c9d402a4f8cc4a1..38fed7e0885f20a9ee9e1b23cd6c90dde2c7cad0 100644 GIT binary patch literal 1732 zcmV;#20QtQP)11hLtOkivgpOG#z3|AH36vXymOkrWREQ}_e4FolgED6GZC5EL|tMAw9W z-`;uuqticsRsp?zbn@*FUqyw$2R;LT;nAIQ%U51gL3hr*`NrFk_rM1}1HXy>eEG{y z(h4><6MqFh@U!r*6torRKfe1+J_7!IaqgUZ_WMy-2z=l>=T--+T>K`gL&YNRDfnI2 zcOL90;;Z}LI^1=VQm*d5k6s~s#rUrdR*9bN;y2aPw4qTo)YG}hAp)gD?lVt0c+F=Z z7m9ZAEMDGht!|!p;3BzVms6@p=uH$o`A9a48%0FxE9hu^!w0gO)cx#ee=vqYId;lJ z$41MIb2SBJk9bAhZOd&2x6omAcJ4gbNv~zKNFRD=5NbxB)=^M;&-uV*4el@yCDxz@ z+F6>KLmn0q(TJ4vS{es4T#u@Ba3OZ8h$tOl=*g&ci|KUgsFqA^x}Os}OE1`>k&s^3 zK|!nK@(g-evQtGwLl6pI(+@ZEW>29kp1sqgo1B$HBhJ5+%pvh9%GkqoJ0tBB%BR`x zk5)zDJl;$9uxW`(=c%9uS^RZpXCjZLpWkLlX*cW?Oem;G8w|1~b#|6G@XM`~?!%%= z2WPe1ewv#?E^n}9kyS;l;6p*D$j;KVusI`))&{GCmH%<%Q>8j)qN8=(A2OBr*`|7!D2L9o$>k;YQ3`2<4FSToWSz)Y%i^N|rQKy*ws0o*K}+m| z8jsA;>g4l4F9LBEZ7QyaDI;luiIL4ds6oRGZZM@=K_Ni>eb6B<usjnM<;ia-zX*8Uw>cpmIEL74E)BUx4+!o*zEnz zp3r~~e363sH;rha|90|g_)p*ipND@7?+1llk2NR^27<( z#8>yfb-3#!rCi;AAH72Oit)ey@HEl0UHtkNmVIzH@7xQ^w|6hkpI?kNW)wL@pp?*& zBZnE~SWJJpS5wd-(hQj= z6Qd7?YH7=P$WaID>gdq=Qjwj9d&{=r-4BL*l9MU)kR9sWIW}v4)>(s|KfhosuqYw+ zfy)})Z6LaROevA{u?jnfA`=Ryiq1tIww+i=L?cqtXZdPL5}nYw>`#1J$HCDgeou#>%y zW5!M`5k1^nM*Fg9E~X!z=NpzwXYuTvCdqhKTpA|S-LFKW5RDS9m!s)n8FtgqGqe3) zKFVeB2K%7w<&a9E-HNDi$_f+q-js-v21zKT-2*&| zB+vQ6a(9xVlu&|hz)syACB`%zwt^9*bft73DiAa`E4QEKrjX0?PMfoaEhIep>A|2B@zPVmQN)3RoF>A-mn~g55NCAd-?Y1k84ce1D}U~rJ(*zBPF~CKJeN2ct<_#nyf)#Fc1_513_Uk avi|_7^f7$z*D;v@0000D!@9mqN zKf8O&OmSQ$cfXyT{q4M&+4pW?Wo2cgqJotLC6I~|NJR;xGVcVsa^vpyj}I)Mi_@)_ zZ=X3z#V->7kU$o_`kOw6MX%m8=b-qd;v47R2Or;42UuG6{8#Y{!yh+^`Tf;HdVKq{ zziVH=e493vieDw8 z|4M`+HxVEjy3KHXknvo|yxum9W^pY8FOB3F?^qS8hYVrg^nJ_m9(||b#AFh9`eOc| z9|ytP-s#f>4I#%Ky6ezrp?)495MswL#;r=v6`~o%!*wq9*w6cODK1hrEyTInqzylV zE~qCKv^kpOlgNQ0#2#tt^jR1hgvKM4i1`r9g+_=yIM{K{;m7${ZkMx?*n>_;Gy=t| z#c&%SCVSY*_+nO9s0u?@^1R8r99o~j?QdxW%CQe5+6CaHX_*lyLKQae?D14v@bejs zKuC7R>>{$Whxfo>oGLuPGlTeHJcn8=Ldh94MBgkl0%cvv3+)-kGln!d5WK&PTZsr~ zVWRAb*6{-Nojsp1$)^E1^eLtmqny_Y?O`GTl2u{O1pKxl4kJfV?gx_S14Z6BgTwiq zW8!nabD5Lu!kqg+5hjj4fzPWC)JG$_2O+I`^eFHqO3o!D-6~VeOp=?*MV&cv#gQ_8 zak_QF`$Py(e*W6))GL0G_@+j$-q4Pk#CMsiyw&&4b;)dmb7A zk(&q*4IJ4q7@-d`KFE7?UjfAc;{il)%ix*3)e~vdMuupB?0J0e7)1{m!cM>MTaNeO z;PCx`ClH;{p+>joZ++LKQILb08=#_KJGN1xohW+-{m)1pXCXrUJa7qvW7Ug|@KN6= zl^%!eEC6pD>dS8!*<**OCf@#HbG?bwaV*5SnkNUc+ZSSfVwTPiXDA}{|aKbs4n;=>vG&!)xGmsE_a2a$O0>Q(NGcok#OgfE= z#vYc-mdCRak^R5c+aYm%0E1I;2bjNmpSp*~6GiIYHOoKxwyWIzM0XFO&&YVV0c{*uz_~ zQxY9J3y+t^slo$1+m9c{bEw5|8F$duf>&FfS&TD@4ZKxb8I`=y#VaTX4e~fm4pe7q zgMCk2_>r!{q}d}Hew=D+&`AG8j6ctkYrxc}AAO5Qyiq}}3Zv9wl=E7lJxt`WNp Date: Sat, 19 Apr 2025 17:24:36 +0200 Subject: [PATCH 270/919] fix walls being treated as potential suitable access if another wall is built beneath them --- docs/changelog.txt | 1 + plugins/suspendmanager.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee422916ef4..fb0c1175a95 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: - `gui/teleport`: now properly handles teleporting units that are currently falling or being flung - `unload`: fix recent regression where `unload` would immediately `reload` the target - ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector +- `suspendmanager`: fix walls being treated as potential suitable access if another wall is built underneath ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index 10c8d241f0a..d63d6a99c9a 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -387,7 +387,12 @@ class SuspendManager { if (isSuitableAccess(pos)) return true; - // if a wall is being constructed below, the tile will be suitable + // wall-type tiles can never become suitable + auto tile_type = Maps::getTileType(pos); + if (!tile_type || isWallTerrain(*tile_type)) + return false; + + // other tiles can become suitable if a wall is being constructed below auto below = Buildings::findAtTile(coord(pos.x,pos.y,pos.z-1)); if (below && below->getType() == df::building_type::Construction && below->getSubtype() == construction_type::Wall) From e4389a28ec464d5ecb88844cbcfb6e55acc46215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 21 Apr 2025 11:45:57 +0200 Subject: [PATCH 271/919] Fix selection tests for TextArea They are now closer to real behaviour. In real TextArea cursor dissapears when there is a selection. On tests id did not - now it do. --- .../lua/gui/widgets/text_area/text_area_content.lua | 10 ++++------ test/library/gui/widgets.TextArea.lua | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index cfa790a355a..928cf7dc662 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -240,12 +240,10 @@ function TextAreaContent:onRenderBody(dc) dc:newline() end - local show_focus = not self.enable_cursor_blink - or ( - not self:hasSelection() - and self.parent_view:hasFocus() - and gui.blink_visible(530) - ) + local show_focus = not self:hasSelection() and ( + not self.enable_cursor_blink + or (self.parent_view:hasFocus() and gui.blink_visible(530)) + ) if show_focus then local x, y = self.wrapped_text:indexToCoords(self.cursor) diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index a5748afd5f1..c9cd805782a 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1726,7 +1726,7 @@ function test.arrows_reset_selection() simulate_input_keys('CUSTOM_CTRL_A') - expect.eq(read_rendered_text(text_area), text .. '_'); + expect.eq(read_rendered_text(text_area), text); expect.eq(read_selected_text(text_area), text); @@ -1768,7 +1768,7 @@ function test.click_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1803,7 +1803,7 @@ function test.line_navigation_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1836,7 +1836,7 @@ function test.jump_begin_or_end_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -3057,7 +3057,7 @@ function test.fast_rewind_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ From eb0723f0b67406010d5a6805d95f2a53b22646f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 21 Apr 2025 11:47:17 +0200 Subject: [PATCH 272/919] Fix TextArea cursor dissapearing on Select All when there is no text --- .../lua/gui/widgets/text_area/text_area_content.lua | 5 +++++ test/library/gui/widgets.TextArea.lua | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 928cf7dc662..83099fac765 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -98,7 +98,12 @@ function TextAreaContent:setCursor(cursor_offset) end function TextAreaContent:setSelection(from_offset, to_offset) + if #self.text == 0 then + return + end + -- text selection is always start on self.cursor and end on self.sel_end + self:setCursor(from_offset) self.sel_end = to_offset diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index c9cd805782a..5a7d006d599 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1655,6 +1655,18 @@ function test.select_all() screen:dismiss() end +function test.ignore_select_all_for_empty_text() + local text_area, screen, window = arrange_textarea({w=65}) + + expect.eq(read_rendered_text(text_area), '_'); + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), '_'); + + screen:dismiss() +end + function test.text_key_replace_selection() local text_area, screen, window = arrange_textarea({w=65}) From 7c75baa0e7d92210a14b30a4d0c054c1156a1fa4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:09:22 -0500 Subject: [PATCH 273/919] update `Buildings` for 51.11 Also added `Units::get_cached_unit_by_global_id` as a utility function to support compatible updating of `buildingst::owner_unit_cached_index` --- library/include/modules/Units.h | 2 ++ library/modules/Buildings.cpp | 29 ++++++++++++++++------------- library/modules/Units.cpp | 21 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 2e0f188b5cb..86238308d70 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -350,5 +350,7 @@ DFHACK_EXPORT void multiplyActionTimers(color_ostream &out, df::unit *unit, floa DFHACK_EXPORT void multiplyGroupActionTimers(color_ostream &out, df::unit *unit, float amount, df::unit_action_type_group affectedActionTypeGroup); DFHACK_EXPORT void setActionTimers(color_ostream &out, df::unit *unit, int32_t amount, df::unit_action_type affectedActionType); DFHACK_EXPORT void setGroupActionTimers(color_ostream &out, df::unit *unit, int32_t amount, df::unit_action_type_group affectedActionTypeGroup); + +DFHACK_EXPORT df::unit* get_cached_unit_by_global_id(int32_t id, int32_t& index); } } diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index df31b1de7b3..5fe8bdc5e36 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -38,6 +38,7 @@ distribution. #include "modules/Buildings.h" #include "modules/Maps.h" #include "modules/Job.h" +#include "modules/Units.h" #include "df/building_axle_horizontalst.h" #include "df/building_bars_floorst.h" @@ -321,26 +322,30 @@ bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) { CHECK_NULL_POINTER(bld); - if (bld->assigned_unit == unit) + auto unit_id = unit ? unit->id : -1; + + if (bld->assigned_unit_id == unit_id) return true; - if (bld->assigned_unit) + if (bld->assigned_unit_id != -1) { - auto &blist = bld->assigned_unit->owned_buildings; - vector_erase_at(blist, linear_index(blist, bld)); - - if (auto spouse = df::unit::find(bld->assigned_unit->relationship_ids[df::unit_relationship_type::Spouse])) + if (auto old_unit = df::unit::find(bld->assigned_unit_id)) { - auto &blist = spouse->owned_buildings; + auto& blist = old_unit->owned_buildings; vector_erase_at(blist, linear_index(blist, bld)); + + if (auto spouse = df::unit::find(old_unit->relationship_ids[df::unit_relationship_type::Spouse])) + { + auto& blist = spouse->owned_buildings; + vector_erase_at(blist, linear_index(blist, bld)); + } } } - bld->assigned_unit = unit; + bld->assigned_unit_id = unit_id; if (unit) { - bld->assigned_unit_id = unit->id; unit->owned_buildings.push_back(bld); if (auto spouse = df::unit::find(unit->relationship_ids[df::unit_relationship_type::Spouse])) @@ -350,10 +355,8 @@ bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) blist.push_back(bld); } } - else - { - bld->assigned_unit_id = -1; - } + + Units::get_cached_unit_by_global_id(unit_id, bld->owner_unit_cached_index); return true; } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index e604562c77c..726e2343c0b 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2188,3 +2188,24 @@ void Units::setGroupActionTimers(color_ostream &out, df::unit *unit, } } } + +// this is a (loose) reimplementation of df's `unit_handlerst::get_cached_unit_by_global_id` +df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) +{ + auto& vector = df::unit::get_vector(); + auto len = vector.size(); + + if (len == 0 || id == -1) + return nullptr; + + if (index > -1 && index < len) + { + auto unit = vector[index]; + if (index == unit->id) + return unit; + index = -1; + } + index = binsearch_index(vector, &df::unit::id, id); + return index != -1 ? vector[index] : nullptr; +} + From 29b9965a146834aca193354efe6f7349395ab806 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:16:51 -0500 Subject: [PATCH 274/919] add `Buildings::getOwner` this correctly leverages the `get_cached_unit_by_global_id` mechanic for civzones --- library/include/modules/Buildings.h | 5 +++++ library/modules/Buildings.cpp | 5 +++++ plugins/preserve-tombs.cpp | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index c323958e17d..1a37cb8304a 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -73,6 +73,11 @@ DFHACK_EXPORT bool ReadCustomWorkshopTypes(std::map & bt DFHACK_EXPORT df::general_ref *getGeneralRef(df::building *building, df::general_ref_type type); DFHACK_EXPORT df::specific_ref *getSpecificRef(df::building *building, df::specific_ref_type type); +/** + * Gets the owner unit for the zone. Uses the cached index in the civzone if valid, updates if not + */ +DFHACK_EXPORT df::unit* getOwner(df::building_civzonest* building); + /** * Sets the owner unit for the zone. */ diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 5fe8bdc5e36..b7aeb559965 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -318,6 +318,11 @@ std::string Buildings::getName(df::building* building) return tmp; } +df::unit* Buildings::getOwner(df::building_civzonest* bld) +{ + return Units::get_cached_unit_by_global_id(bld->assigned_unit_id, bld->owner_unit_cached_index); +} + bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) { CHECK_NULL_POINTER(bld); diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 17f06c6c929..5a20d653718 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -198,8 +198,9 @@ static void update_tomb_assignments(color_ostream &out) { // check tomb civzones for assigned units for (auto* tomb : world->buildings.other.ZONE_TOMB) { if (!tomb || !tomb->flags.bits.exists) continue; - if (!tomb->assigned_unit) continue; - if (Units::isDead(tomb->assigned_unit)) continue; // we only care about living units + if (tomb->assigned_unit_id == -1) continue; + auto unit = Buildings::getOwner(tomb); + if (Units::isDead(unit)) continue; // we only care about living units auto it = tomb_assignments.find(tomb->assigned_unit_id); From 11e5369bdd44a520614c21e98201edd987090ea5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:22:25 -0500 Subject: [PATCH 275/919] fix typo --- library/modules/Units.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 726e2343c0b..ad442145013 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2201,7 +2201,7 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) if (index > -1 && index < len) { auto unit = vector[index]; - if (index == unit->id) + if (id == unit->id) return unit; index = -1; } From 97ae50ab9de6cea3b939664a046e0e9fefe9a727 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:24:55 -0500 Subject: [PATCH 276/919] updates to `preserve-rooms` and `preserve-tombs` for 51.11 --- plugins/preserve-rooms.cpp | 12 +++++++----- plugins/preserve-tombs.cpp | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 52f61516373..a34273f7f23 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -315,13 +315,14 @@ static void assign_nobles(color_ostream &out) { auto zone = virtual_cast(df::building::find(zone_id)); if (!zone) continue; + auto owner = Buildings::getOwner(zone); bool assigned = false; for (auto it = group_codes.rbegin(); it != group_codes.rend(); it++) { auto code = *it; vector units; Units::getUnitsByNobleRole(units, code); // if zone is already assigned to a proper unit (or their spouse), skip - if (linear_index(units, zone->assigned_unit) >= 0 || + if (linear_index(units, owner) >= 0 || linear_index(get_spouse_ids(out, units), zone->assigned_unit_id) >= 0) { assigned = true; @@ -352,7 +353,7 @@ static void assign_nobles(color_ostream &out) { if (assigned) break; } - if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit)) { + if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit_id != -1)) { DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: %d\n", zone_id); zone->spec_sub_flag.bits.active = false; Buildings::setOwner(zone, NULL); @@ -506,11 +507,12 @@ static void process_rooms(color_ostream &out, WARN(cycle, out).print("invalid building pointer %p in building vector\n", zone); continue; } - if (!zone->assigned_unit) { + if (zone->assigned_unit_id == -1) { handle_missing_assignments(out, active_unit_ids, &it, it_end, share_with_spouse, zone->id); continue; } - auto hf = df::historical_figure::find(zone->assigned_unit->hist_figure_id); + auto owner = Buildings::getOwner(zone); + auto hf = df::historical_figure::find(owner->hist_figure_id); if (!hf) continue; int32_t spouse_hfid = share_with_spouse ? get_spouse_hfid(out, hf) : -1; @@ -593,7 +595,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { if (!zone) continue; zone->spec_sub_flag.bits.active = true; - if (zone->assigned_unit || spouse_has_sharable_room(out, hfid, zone->type)) + if (zone->assigned_unit_id != -1 || spouse_has_sharable_room(out, hfid, zone->type)) continue; DEBUG(event,out).print("reassigning zone %d\n", zone->id); Buildings::setOwner(zone, unit); diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 5a20d653718..b7f7be28b66 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -249,7 +249,7 @@ static bool assign_to_tomb(df::unit * unit, int32_t building_id) { if (!bld) return false; auto tomb = virtual_cast(bld); - if (!tomb || tomb->assigned_unit) return false; + if (!tomb || tomb->assigned_unit_id != -1) return false; Buildings::setOwner(tomb, unit); return true; From ea882fe39204a9a12c5b1f776eea8c028acf36d8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:29:33 -0500 Subject: [PATCH 277/919] add changelog --- docs/changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fb0c1175a95..2f4e1a15ee8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,7 +72,10 @@ Template for new versions: ## API - ``Military`` module: added ``addToSquad`` function +- ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) +- ``Buildings`` module: add `getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units +- ``Buildings::setOwner``: updated for changes in 51.11 ## Lua - ``dfhack.military.addToSquad``: expose Military API function From c285e49c2d536a38ee0efa63ed224d4624c598d5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:32:17 -0500 Subject: [PATCH 278/919] trim eof --- library/modules/Units.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index ad442145013..06018f75773 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2208,4 +2208,3 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) index = binsearch_index(vector, &df::unit::id, id); return index != -1 ? vector[index] : nullptr; } - From 5d4b014379300edaa1ef66bc527b9ec980dda14c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:34:06 -0500 Subject: [PATCH 279/919] fix markup in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2f4e1a15ee8..fe6184aa7b7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,7 +73,7 @@ Template for new versions: ## API - ``Military`` module: added ``addToSquad`` function - ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) -- ``Buildings`` module: add `getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 +- ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units - ``Buildings::setOwner``: updated for changes in 51.11 From c8011f47ab4c612e3947bb927aa4c3cc9b612e00 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 10:40:59 -0500 Subject: [PATCH 280/919] minor tweek --- library/modules/Units.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 06018f75773..e049bf53ac9 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2203,7 +2203,6 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) auto unit = vector[index]; if (id == unit->id) return unit; - index = -1; } index = binsearch_index(vector, &df::unit::id, id); return index != -1 ? vector[index] : nullptr; From d6d21519a40a05989b94692a75246ff1a4744682 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:24:02 -0700 Subject: [PATCH 281/919] fix signed/unsigned comparison --- library/modules/Units.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index e049bf53ac9..e68318fa1bf 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2198,7 +2198,7 @@ df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) if (len == 0 || id == -1) return nullptr; - if (index > -1 && index < len) + if (index > -1 && (size_t)index < len) { auto unit = vector[index]; if (id == unit->id) From 980b95c40135bc181c96be97bfca33e92461e0ce Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:24:12 -0700 Subject: [PATCH 282/919] update xml ref --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 8f4b816a52f..e86f7763ef8 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8f4b816a52f94c95772100cd4652a5a43697059e +Subproject commit e86f7763ef81c3265b4842a620ef9345ee75c2c7 From 315252026f885387b78c7202e30b229c1f58b423 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:29:14 -0700 Subject: [PATCH 283/919] expose getOwner to Lua --- docs/dev/Lua API.rst | 4 ++++ library/LuaApi.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index b11c8862b21..c1c2f78e76c 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2465,6 +2465,10 @@ General Searches for a specific_ref with the given type. +* ``dfhack.buildings.getOwner(civzone)`` + + Returns the owner of the zone or *nil* if there isn't one. + * ``dfhack.buildings.setOwner(civzone,unit)`` Replaces the owner of the civzone. If unit is *nil*, removes ownership. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index f169650f90c..c80fecffc26 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2736,6 +2736,7 @@ static bool buildings_containsTile(df::building *bld, int x, int y) { static const LuaWrapper::FunctionReg dfhack_buildings_module[] = { WRAPM(Buildings, getGeneralRef), WRAPM(Buildings, getSpecificRef), + WRAPM(Buildings, getOwner), WRAPM(Buildings, setOwner), WRAPM(Buildings, allocInstance), WRAPM(Buildings, checkFreeTiles), From 8253a9c25d82238d2753f5726cb9ab2cb7dc5297 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:39:39 -0700 Subject: [PATCH 284/919] use new assigned_unit API for 51.11 --- plugins/lua/sort/places.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index 35a55694932..d58f306584b 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -79,8 +79,9 @@ local function get_zone_search_key(zone) end -- allow zones w/ assignments to be searchable by their assigned unit - if zone.assigned_unit ~= nil then - table.insert(result, sortoverlay.get_unit_search_key(zone.assigned_unit)) + local owner = dfhack.buildings.getOwner(zone) + if owner then + table.insert(result, sortoverlay.get_unit_search_key(owner)) end -- allow zones to be searchable by type From b4f37ccd6f5328e243df013e3efd6410c5570c23 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:41:55 -0700 Subject: [PATCH 285/919] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fb0c1175a95..e54cb327b9f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: - `unload`: fix recent regression where `unload` would immediately `reload` the target - ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector - `suspendmanager`: fix walls being treated as potential suitable access if another wall is built underneath +- text widgets no longer lose their cursor when the Ctrl-a (select all) hotkey is pressed when there is no text to select ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) From 279f54222df2d22569eb9dfa81199c088aa8157d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:45:17 +0000 Subject: [PATCH 286/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 32203322174..6cf7d0adb49 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 322033221745e9cbf537d59152138df385c505b7 +Subproject commit 6cf7d0adb49f508811edf940898c53b56d5d9bb3 From e2ac17f408dd715a581c9ba008111cf197028726 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:48:48 -0700 Subject: [PATCH 287/919] bump to 51.11 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++---- scripts | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 017edd4460a..df9e4ea767a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.10") +set(DF_VERSION "51.11") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index 52c2f194a80..23fc4e3028f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,20 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 51.11-r1 + ## Fixes - `spectate`: don't show a hover tooltip for hidden units (e.g. invisible snatchers) - `stockpiles`: fix one-off error in item type when importing furniture stockpile settings @@ -69,8 +83,6 @@ Template for new versions: ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) -## Documentation - ## API - ``Military`` module: added ``addToSquad`` function - ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) @@ -81,8 +93,6 @@ Template for new versions: ## Lua - ``dfhack.military.addToSquad``: expose Military API function -## Removed - # 51.10-r1 ## Misc Improvements diff --git a/scripts b/scripts index 6cf7d0adb49..b5178d671f9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6cf7d0adb49f508811edf940898c53b56d5d9bb3 +Subproject commit b5178d671f9e6142c7c3fccadf025e7eb07d9b04 From c806420fcb4be0449c3cba214019b3c8726b0346 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:06:51 +0000 Subject: [PATCH 288/919] Auto-update structures ref for 51.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index e86f7763ef8..6502bdefd17 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e86f7763ef81c3265b4842a620ef9345ee75c2c7 +Subproject commit 6502bdefd1796315365a7bb58e30e0ce34359bea From adf6217bbce04774840ad2cc4a15c6521d0d4845 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 10:26:04 -0700 Subject: [PATCH 289/919] add missing changelog entry --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23fc4e3028f..76e9510890b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -92,6 +92,7 @@ Template for new versions: ## Lua - ``dfhack.military.addToSquad``: expose Military API function +- ``dfhack.buildings.getOwner``: make new Buildings API available to Lua # 51.10-r1 From 503d2f2595b032675dac44780e72f5ea8e4514b3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Apr 2025 14:17:14 -0500 Subject: [PATCH 290/919] `preserve-rooms`: validate assigned unit id before using --- docs/changelog.txt | 1 + plugins/preserve-rooms.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 76e9510890b..510050aee67 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist ## Misc Improvements diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index a34273f7f23..805e10c8370 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -512,6 +512,10 @@ static void process_rooms(color_ostream &out, continue; } auto owner = Buildings::getOwner(zone); + if (!owner) { + WARN(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); + continue; + } auto hf = df::historical_figure::find(owner->hist_figure_id); if (!hf) continue; From 453d0088c685ce955c5d76b1f82f6135fe9ac915 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 22 Apr 2025 18:27:42 -0700 Subject: [PATCH 291/919] Revert "Add star icon to gui/design" --- data/art/design.png | Bin 1732 -> 1491 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/art/design.png b/data/art/design.png index 38fed7e0885f20a9ee9e1b23cd6c90dde2c7cad0..787b9962a1bc68d6d5d46bfa9c9d402a4f8cc4a1 100644 GIT binary patch literal 1491 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 avi|_7^f7$z*D;v@0000 Date: Tue, 22 Apr 2025 18:28:25 -0700 Subject: [PATCH 292/919] Revert "Revert "Add star icon to gui/design"" --- data/art/design.png | Bin 1491 -> 1732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/art/design.png b/data/art/design.png index 787b9962a1bc68d6d5d46bfa9c9d402a4f8cc4a1..38fed7e0885f20a9ee9e1b23cd6c90dde2c7cad0 100644 GIT binary patch literal 1732 zcmV;#20QtQP)11hLtOkivgpOG#z3|AH36vXymOkrWREQ}_e4FolgED6GZC5EL|tMAw9W z-`;uuqticsRsp?zbn@*FUqyw$2R;LT;nAIQ%U51gL3hr*`NrFk_rM1}1HXy>eEG{y z(h4><6MqFh@U!r*6torRKfe1+J_7!IaqgUZ_WMy-2z=l>=T--+T>K`gL&YNRDfnI2 zcOL90;;Z}LI^1=VQm*d5k6s~s#rUrdR*9bN;y2aPw4qTo)YG}hAp)gD?lVt0c+F=Z z7m9ZAEMDGht!|!p;3BzVms6@p=uH$o`A9a48%0FxE9hu^!w0gO)cx#ee=vqYId;lJ z$41MIb2SBJk9bAhZOd&2x6omAcJ4gbNv~zKNFRD=5NbxB)=^M;&-uV*4el@yCDxz@ z+F6>KLmn0q(TJ4vS{es4T#u@Ba3OZ8h$tOl=*g&ci|KUgsFqA^x}Os}OE1`>k&s^3 zK|!nK@(g-evQtGwLl6pI(+@ZEW>29kp1sqgo1B$HBhJ5+%pvh9%GkqoJ0tBB%BR`x zk5)zDJl;$9uxW`(=c%9uS^RZpXCjZLpWkLlX*cW?Oem;G8w|1~b#|6G@XM`~?!%%= z2WPe1ewv#?E^n}9kyS;l;6p*D$j;KVusI`))&{GCmH%<%Q>8j)qN8=(A2OBr*`|7!D2L9o$>k;YQ3`2<4FSToWSz)Y%i^N|rQKy*ws0o*K}+m| z8jsA;>g4l4F9LBEZ7QyaDI;luiIL4ds6oRGZZM@=K_Ni>eb6B<usjnM<;ia-zX*8Uw>cpmIEL74E)BUx4+!o*zEnz zp3r~~e363sH;rha|90|g_)p*ipND@7?+1llk2NR^27<( z#8>yfb-3#!rCi;AAH72Oit)ey@HEl0UHtkNmVIzH@7xQ^w|6hkpI?kNW)wL@pp?*& zBZnE~SWJJpS5wd-(hQj= z6Qd7?YH7=P$WaID>gdq=Qjwj9d&{=r-4BL*l9MU)kR9sWIW}v4)>(s|KfhosuqYw+ zfy)})Z6LaROevA{u?jnfA`=Ryiq1tIww+i=L?cqtXZdPL5}nYw>`#1J$HCDgeou#>%y zW5!M`5k1^nM*Fg9E~X!z=NpzwXYuTvCdqhKTpA|S-LFKW5RDS9m!s)n8FtgqGqe3) zKFVeB2K%7w<&a9E-HNDi$_f+q-js-v21zKT-2*&| zB+vQ6a(9xVlu&|hz)syACB`%zwt^9*bft73DiAa`E4QEKrjX0?PMfoaEhIep>A|2B@zPVmQN)3RoF>A-mn~g55NCAd-?Y1k84ce1D}U~rJ(*zBPF~CKJeN2ct<_#nyf)#Fc1_513_Uk avi|_7^f7$z*D;v@0000D!@9mqN zKf8O&OmSQ$cfXyT{q4M&+4pW?Wo2cgqJotLC6I~|NJR;xGVcVsa^vpyj}I)Mi_@)_ zZ=X3z#V->7kU$o_`kOw6MX%m8=b-qd;v47R2Or;42UuG6{8#Y{!yh+^`Tf;HdVKq{ zziVH=e493vieDw8 z|4M`+HxVEjy3KHXknvo|yxum9W^pY8FOB3F?^qS8hYVrg^nJ_m9(||b#AFh9`eOc| z9|ytP-s#f>4I#%Ky6ezrp?)495MswL#;r=v6`~o%!*wq9*w6cODK1hrEyTInqzylV zE~qCKv^kpOlgNQ0#2#tt^jR1hgvKM4i1`r9g+_=yIM{K{;m7${ZkMx?*n>_;Gy=t| z#c&%SCVSY*_+nO9s0u?@^1R8r99o~j?QdxW%CQe5+6CaHX_*lyLKQae?D14v@bejs zKuC7R>>{$Whxfo>oGLuPGlTeHJcn8=Ldh94MBgkl0%cvv3+)-kGln!d5WK&PTZsr~ zVWRAb*6{-Nojsp1$)^E1^eLtmqny_Y?O`GTl2u{O1pKxl4kJfV?gx_S14Z6BgTwiq zW8!nabD5Lu!kqg+5hjj4fzPWC)JG$_2O+I`^eFHqO3o!D-6~VeOp=?*MV&cv#gQ_8 zak_QF`$Py(e*W6))GL0G_@+j$-q4Pk#CMsiyw&&4b;)dmb7A zk(&q*4IJ4q7@-d`KFE7?UjfAc;{il)%ix*3)e~vdMuupB?0J0e7)1{m!cM>MTaNeO z;PCx`ClH;{p+>joZ++LKQILb08=#_KJGN1xohW+-{m)1pXCXrUJa7qvW7Ug|@KN6= zl^%!eEC6pD>dS8!*<**OCf@#HbG?bwaV*5SnkNUc+ZSSfVwTPiXDA}{|aKbs4n;=>vG&!)xGmsE_a2a$O0>Q(NGcok#OgfE= z#vYc-mdCRak^R5c+aYm%0E1I;2bjNmpSp*~6GiIYHOoKxwyWIzM0XFO&&YVV0c{*uz_~ zQxY9J3y+t^slo$1+m9c{bEw5|8F$duf>&FfS&TD@4ZKxb8I`=y#VaTX4e~fm4pe7q zgMCk2_>r!{q}d}Hew=D+&`AG8j6ctkYrxc}AAO5Qyiq}}3Zv9wl=E7lJxt`WNp Date: Tue, 22 Apr 2025 18:31:28 -0700 Subject: [PATCH 293/919] bump version to 51.11-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index df9e4ea767a..fdf638bb29c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.11") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 510050aee67..94223c2326a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,6 @@ Template for new versions: ## New Features ## Fixes -- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist ## Misc Improvements @@ -68,6 +67,12 @@ Template for new versions: ## Removed +# 51.11-r1.1 + +## Fixes +- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist +- `gui/design`: fix misaligned shape icons + # 51.11-r1 ## Fixes From 2310f0d05718dd88a5b83715b29cc46c7280b41d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 22 Apr 2025 19:28:16 -0700 Subject: [PATCH 294/919] remove out of date reference to "k" mode --- docs/plugins/changeitem.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plugins/changeitem.rst b/docs/plugins/changeitem.rst index f452f6a7425..cf3fb83d941 100644 --- a/docs/plugins/changeitem.rst +++ b/docs/plugins/changeitem.rst @@ -18,9 +18,10 @@ Usage Show details about the selected item. Does not change the item. You can use this command to discover RAW ids for existing items. ``changeitem []`` - Change the item selected in the ``k`` list or inside a container/inventory. + Change the item that is selected in the UI. ``changeitem here []`` - Change all items at the cursor position. Requires in-game cursor. + Change all items at the keyboard cursor position. Requires the vanilla + keyboard cursor to be enabled. Examples -------- From 9cc6851cc157c5931b9e449673869ee61c8105f2 Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Wed, 23 Apr 2025 02:52:34 -0400 Subject: [PATCH 295/919] Fix for mods not being properly reloaded on Windows --- docs/about/Authors.rst | 1 + docs/changelog.txt | 1 + library/lua/script-manager.lua | 3 +++ 3 files changed, 5 insertions(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index d806d92aaa4..e8030e881a2 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -63,6 +63,7 @@ DoctorVanGogh DoctorVanGogh Donald Ruegsegger hashaash doomchild doomchild Droseran Droseran +dvantwisk dvantwisk DwarvenM DwarvenM Eamon Bode eamondo2 Baron Von Munchhausen EarthPulseAcademy EarthPulseAcademy diff --git a/docs/changelog.txt b/docs/changelog.txt index 94223c2326a..30e64c2b99d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,6 +72,7 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons +- `scripts/script-manager`: fix lua scripts in mods not being reloaded properly on Windows. # 51.11-r1 diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index c2647fbe2d5..6e9da549c5c 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -52,6 +52,9 @@ function reload(refresh_active_mod_scripts) local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name) if script_path:find('scripts_modactive') then local full_path = script_path..'/'..script_name + if dfhack.getOSType() == 'windows' then + full_path = script_path..'\\'..script_name + end internal_script = dfhack.internal.scripts[full_path] if internal_script then dfhack.internal.scripts[full_path] = nil From 8b0a4825bc22753af3b296abca27741dd4477bec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 23 Apr 2025 08:55:26 -0500 Subject: [PATCH 296/919] don't crash on a tomb owned by a unit that doesn't exist --- docs/changelog.txt | 1 + plugins/preserve-tombs.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 94223c2326a..df96baaa1e7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist ## Misc Improvements diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index b7f7be28b66..40b9eb56fd6 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -200,7 +200,7 @@ static void update_tomb_assignments(color_ostream &out) { if (!tomb || !tomb->flags.bits.exists) continue; if (tomb->assigned_unit_id == -1) continue; auto unit = Buildings::getOwner(tomb); - if (Units::isDead(unit)) continue; // we only care about living units + if (!unit || Units::isDead(unit)) continue; // we only care about living units auto it = tomb_assignments.find(tomb->assigned_unit_id); From 3d0f2969e3cee738646e3a2038558fee679367c1 Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Wed, 23 Apr 2025 13:28:48 -0400 Subject: [PATCH 297/919] Changelog entry modifications --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index dac194aa2fa..6f0cd82c8bb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,7 +73,7 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons -- `scripts/script-manager`: fix lua scripts in mods not being reloaded properly on Windows. +- `script-manager`: fix lua scripts in mods not being reloaded properly on Windows. # 51.11-r1 From 6816b676927ab300e7fdf99aa02157b0b235442e Mon Sep 17 00:00:00 2001 From: Dan Van Twisk Date: Wed, 23 Apr 2025 13:33:55 -0400 Subject: [PATCH 298/919] Modified changelog log --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6f0cd82c8bb..12a8fcbe618 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,7 +73,7 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons -- `script-manager`: fix lua scripts in mods not being reloaded properly on Windows. +- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows. # 51.11-r1 From bbb5ec4d356a0b918df0b8fa6afdeeaffa1c6d84 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 23 Apr 2025 17:44:40 -0700 Subject: [PATCH 299/919] bump version to 51.11-r1.2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdf638bb29c..d2d1c8ec9e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.11") -set(DFHACK_RELEASE "r1.1") +set(DFHACK_RELEASE "r1.2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index df96baaa1e7..35018ebce59 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,6 @@ Template for new versions: ## New Features ## Fixes -- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist ## Misc Improvements @@ -68,6 +67,11 @@ Template for new versions: ## Removed +# 51.11-r1.2 + +## Fixes +- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist + # 51.11-r1.1 ## Fixes From a2cda9bb3b468718ae88fdc55074864987f5a73c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Apr 2025 20:17:52 -0500 Subject: [PATCH 300/919] unset "fail randomly' flag on `getplants` specifically when handling plants that have growths that do not drop seeds when processed --- docs/changelog.txt | 1 + plugins/getplants.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 35018ebce59..9a37f81c960 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed ## Misc Improvements diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 9bd3f6f0d44..ab8333e51a7 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -154,9 +154,10 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo bool seedSource = plant->growths[i]->item_type == df::item_type::SEEDS; if (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) { - for (size_t k = 0; growth_mat.material->reaction_product.material.mat_type.size(); k++) { - if (growth_mat.material->reaction_product.material.mat_type[k] == plant->material_defs.type[plant_material_def::seed] && - growth_mat.material->reaction_product.material.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) { + auto& mat = growth_mat.material->reaction_product.material; + for (size_t k = 0; k < mat.mat_type.size(); k++) { + if (mat.mat_type[k] == plant->material_defs.type[plant_material_def::seed] && + mat.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) { seedSource = true; break; } From c0b1bf886b7f62040ffe8a4acca902375d82b746 Mon Sep 17 00:00:00 2001 From: Quietust Date: Fri, 25 Apr 2025 17:09:52 -0600 Subject: [PATCH 301/919] Fix getplants "ripe" and "picked" logic Added Splitmix64 RNG to Random module; not yet exposed from Lua, but that can be added later if deemed necessary. --- docs/changelog.txt | 3 +++ library/include/modules/Random.h | 31 ++++++++++++++++++++++ plugins/getplants.cpp | 44 ++++++++++++++++---------------- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9a37f81c960..9d035c598c4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,12 +57,15 @@ Template for new versions: ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed +- `getplants`: use updated formula for calculating whether plant growths are ripe +- `getplants`: fix logic for determining whether plant growths have been picked ## Misc Improvements ## Documentation ## API +- ``Random`` module: added SplitmixRNG class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness ## Lua diff --git a/library/include/modules/Random.h b/library/include/modules/Random.h index 38fbdd0f894..8ed703be790 100644 --- a/library/include/modules/Random.h +++ b/library/include/modules/Random.h @@ -106,6 +106,37 @@ namespace Random extern template void DFHACK_IMPORT MersenneRNG::unitvector(double *p, int size); #endif + // Standard Splitmix64 RNG, as used by Dwarf Fortress's "hash_rngst" class + class SplitmixRNG + { + uint64_t state; + + public: + SplitmixRNG(uint64_t seed) { + init(seed); + } + + void init(uint64_t seed) { + state = seed; + } + + uint64_t next() { + state += 0x9e3779b97f4a7c15; + uint64_t z = state; + z ^= z >> 30; + z *= 0xbf58476d1ce4e5b9; + z ^= z >> 27; + z *= 0x94d049bb133111eb; + z ^= z >> 31; + return z; + } + + int32_t df_trandom(uint32_t max) { + uint32_t val = next() >> 32; + return (int32_t)(val % max); + } + }; + /* * Classical Perlin noise function in template form. * http://mrl.nyu.edu/~perlin/doc/oscar.html#noise diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index ab8333e51a7..29c6655fc98 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -5,6 +5,7 @@ #include "modules/Designations.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/Random.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -211,32 +212,31 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } // Formula for determination of the variance in plant growth maturation time, determined via disassembly. -// The x and y parameters are in tiles relative to the embark. -bool ripe(int32_t x, int32_t y, int32_t start, int32_t end) { - int32_t time = (((435522653 - (((y + 3) * x + 5) * ((y + 7) * y * 400181475 + 289700012))) & 0x3FFFFFFF) % 2000 + *cur_year_tick) % 403200; +// The coordinates are relative to the embark region. +bool ripe(int32_t x, int32_t y, int32_t z, int32_t start, int32_t end) { + DFHack::Random::SplitmixRNG rng((world->map.region_x * 48 + x) + (world->map.region_y * 48 + y) * 10000 + (world->map.region_z + z) * 100000000); + int32_t time = (rng.df_trandom(2000) + *cur_year_tick) % 403200; return time >= start && (end == -1 || time <= end); } -// Looks in the picked growths vector to see if a matching growth has been marked as picked. -bool picked(const df::plant* plant, int32_t growth_subtype) { - df::world_data* world_data = world->world_data; - df::world_site* site = df::world_site::find(plotinfo->site_id); - int32_t pos_x = site->global_min_x + plant->pos.x / 48; - int32_t pos_y = site->global_min_y + plant->pos.y / 48; - size_t id = pos_x + pos_y * 16 * world_data->world_width; - df::world_object_data* object_data = df::world_object_data::find(id); - if (!object_data) { +// Looks in the local creation zone's picked growths vector to see if a matching growth has been marked as picked. +bool picked(const df::plant* plant, int32_t growth_subtype, int32_t growth_density) { + int32_t pos_x = plant->pos.x / 48 + world->map.region_x; + int32_t pos_y = plant->pos.y / 48 + world->map.region_y; + size_t cz_id = pos_x + pos_y * 16 * world->world_data->world_width; + auto cz = df::world_object_data::find(cz_id); + if (!cz) { return false; } - df::map_block_column* column = world->map.map_block_columns[(plant->pos.x / 16) * world->map.x_count_block + (plant->pos.y / 16)]; - - for (size_t i = 0; i < object_data->picked_growths.x.size(); i++) { - if (object_data->picked_growths.x[i] == plant->pos.x && - object_data->picked_growths.y[i] == plant->pos.y && - object_data->picked_growths.z[i] - column->z_base == plant->pos.z && - object_data->picked_growths.subtype[i] == growth_subtype && - object_data->picked_growths.year[i] == *cur_year) { + + for (size_t i = 0; i < cz->picked_growths.x.size(); i++) { + if (cz->picked_growths.x[i] == (plant->pos.x % 48) && + cz->picked_growths.y[i] == (plant->pos.y % 48) && + cz->picked_growths.z[i] == (plant->pos.z + world->map.region_z) && + cz->picked_growths.density[i] >= growth_density && + cz->picked_growths.subtype[i] == growth_subtype && + cz->picked_growths.year[i] == *cur_year) { return true; } } @@ -310,8 +310,8 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { } if ((!farming || seedSource) && - ripe(plant->pos.x, plant->pos.y, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2) && - !picked(plant, i)) + ripe(plant->pos.x, plant->pos.y, plant->pos.z, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2) && + !picked(plant, i, plant_raw->growths[i]->density)) return Designations::markPlant(plant); } From 170b5443d656a55a849115505502748640b5f9c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:41:39 -0700 Subject: [PATCH 302/919] add more mod utility functions --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 23 ++++++++++ library/lua/script-manager.lua | 82 ++++++++++++++++++++++++++++------ 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 35018ebce59..545ca09f5a0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: ## API ## Lua +- ``script-manager``: new ``get_active_mods()`` function for getting information on active mods ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index c1c2f78e76c..aaf5731b541 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3783,6 +3783,29 @@ paths will be relative to the top level game directory and will end in a slash Which would open ``dfhack-config/mods/my_awesome_mod/settings.json``. After calling ``getModStatePath``, the returned directory is guaranteed to exist. +* ``get_active_mods()`` + + Returns a list of all active mods in the current world. The list elements are + tables containing the following fields: + + - id: mod id + - name: mod display name + - version: mod display version + - numeric_version: numeric mod version + - path: path to the mod directory + - vanilla: true if this is a vanilla mod + +* ``get_mod_info_metadata(mod_path, tags)`` + + Returns a table with the values of the given tags from the ``info.txt`` file + in the given mod directory. The ``mod_path`` argument must be a path to a mod + directory (retrieved, say, from ``get_active_mods()``). The ``tags`` argument + is a string or a list of strings representing the tags to retrieve. The + function will return a table with the tag names as keys and their values as + values. If a requested tag includes the string ``NUMERIC_``, it will return + the numeric value for that tag (e.g., ``NUMERIC_VERSION`` will return the + numeric version of the mod as a number instead of a string). + utils ===== diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index c2647fbe2d5..9a1ecb1df00 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -88,25 +88,52 @@ local INSTALLED_MODS_PATH = 'data/installed_mods/' -- changes to the files) local MOD_PATH_ROOTS = {WORKSHOP_MODS_PATH, MODS_PATH, INSTALLED_MODS_PATH} -local function get_mod_id_and_version(path) - local idfile = path .. '/info.txt' +-- returns the values of the given list of tags from the info.txt file in the given mod directory +-- if a requested tag includes the string 'NUMERIC_', it will return the numeric value for that tag +-- (e.g. NUMERIC_VERSION will return the numeric version of the mod as a number instead of a string). +function get_mod_info_metadata(mod_path, tags) + local idfile = mod_path .. '/info.txt' local ok, lines = pcall(io.lines, idfile) if not ok then return end - local id, version + + if type(tags) ~= 'table' then + tags = {tags} + end + + local tag_regexes = {} + for _,tag in ipairs(tags) do + local tag_regex = ('^%%[%s:'):format(tag) + if tag:find('NUMERIC_') then + -- note this doesn't go all the way to the closing brace since some people put + -- non-number characters in here, and DF only reads the leading digits for + -- numeric fields + tag_regex = tag_regex .. '(%d+)' + else + tag_regex = tag_regex .. '([^%]]+)' + end + tag_regexes[tag] = tag_regex + end + + local values = {} for line in lines do - if not id then - _,_,id = line:find('^%[ID:([^%]]+)%]') + local _,_,info_tag = line:find('^%[([^:]+):') + if not info_tag or not tag_regexes[info_tag] then + goto continue 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+)') + local _,_,value = line:find(tag_regexes[info_tag]) + if value then + values[info_tag] = value end - -- note that we do *not* want to break out of this loop early since - -- lines has to hit EOF to close the file + ::continue:: end - return id, version + + return values +end + +local function get_mod_id_and_version(path) + local values = get_mod_info_metadata(path, {'ID', 'NUMERIC_VERSION'}) + if not values then return end + return values.ID, values.NUMERIC_VERSION end local function add_mod_paths(mod_paths, id, base_path, subdir) @@ -172,6 +199,35 @@ function get_mod_paths(installed_subdir, active_subdir) return mod_paths end +-- returns a list of tables in load order with the following fields: +-- id: mod id +-- name: mod display name +-- version: mod display version +-- numeric_version: numeric mod version +-- path: path to the mod directory +-- vanilla: true if this is a vanilla mod +function get_active_mods() + if not dfhack.isWorldLoaded() then return {} end + + local mods = {} + + local ol = df.global.world.object_loader + + for idx=0,#ol.object_load_order_id-1 do + local path = ol.object_load_order_src_dir[idx].value + table.insert(mods, { + id=ol.object_load_order_id[idx].value, + name=ol.object_load_order_name[idx].value, + version=ol.object_load_order_displayed_version[idx].value, + numeric_version=ol.object_load_order_numeric_version[idx], + path=path, + vanilla=path:startswith('data/vanilla/'), -- windows also uses forward slashes + }) + end + + return mods +end + function get_mod_script_paths() local paths = {} for _,v in ipairs(get_mod_paths('scripts_modinstalled', 'scripts_modactive')) do From 02e2e4387007ae6dba98314c982035783ec87488 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:51:05 -0700 Subject: [PATCH 303/919] add missing changelog for new script-manager function --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 508f3ccfad0..bf4f38e5db8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## Lua - ``script-manager``: new ``get_active_mods()`` function for getting information on active mods +- ``script-manager``: new ``get_mod_info_metadata()`` function for getting information out of mod ``info.txt`` files ## Removed From 6a90b6976be41ded4e16cbcae5dd594cf8bfe285 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 18:52:34 -0700 Subject: [PATCH 304/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index b5178d671f9..3ea6b8811c1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b5178d671f9e6142c7c3fccadf025e7eb07d9b04 +Subproject commit 3ea6b8811c13e6084ca92230bd5b789d428dfb88 From c0ed5302e89497ea76c897746e211c8a04defce0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 26 Apr 2025 11:27:04 -0700 Subject: [PATCH 305/919] add translated english name to getReadableName output --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 10 +++++----- library/modules/Units.cpp | 13 +++++++++++-- plugins/lua/sort/sortoverlay.lua | 5 +---- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf4f38e5db8..187467ac26a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed ## Misc Improvements +- All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. ## Documentation diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index aaf5731b541..2c7f9cacbce 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1775,11 +1775,11 @@ Units module * ``dfhack.units.getReadableName(unit or historical_figure)`` - Returns a string that includes the language name of the unit (if any), the - race of the unit (if different from fort), whether it is trained for war or - hunting, any syndrome-given descriptions (such as "necromancer"), the - training level (if tame), and profession or noble role. If a - ``historical_figure`` is passed instead of a unit, some information + Returns a string that includes the native and english language name of the + unit (if any), the race of the unit (if different from fort), whether it is + trained for war or hunting, any syndrome-given descriptions (such as + "necromancer"), the training level (if tame), and profession or noble role. + If a ``historical_figure`` is passed instead of a unit, some information (e.g., agitation status) is not available, and the profession may be different (e.g., "Monk") from what is displayed in fort mode. diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index e68318fa1bf..4ccce422778 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1190,8 +1190,17 @@ string Units::getReadableName(df::unit *unit) { if (isTame(unit)) prof_name += " (" + getTameTag(unit) + ")"; - string name = Translation::translateName(getVisibleName(unit)); - return name.empty() ? prof_name : name + ", " + prof_name; + auto visible_name = getVisibleName(unit); + string native_name = Translation::translateName(visible_name); + + if (native_name.empty()) + return prof_name; + + string english_name = Translation::translateName(visible_name, true, true); + if (english_name.empty()) + return native_name + ", " + prof_name; + + return native_name + " \"" + english_name + "\", " + prof_name; } double Units::getAge(df::unit *unit, bool true_age) { diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index c823c4c34e4..53a591251d8 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -4,10 +4,7 @@ local overlay = require('plugins.overlay') local utils = require('utils') function get_unit_search_key(unit) - return ('%s %s %s'):format( - dfhack.units.getReadableName(unit), - dfhack.units.getProfessionName(unit), - dfhack.translation.translateName(unit.name, true, true)) -- get English last name + return dfhack.units.getReadableName(unit) end local function copy_to_lua_table(vec) From 58e29a5e6bf5a242fd9cd56aee6e76e2e3d97329 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Apr 2025 18:37:34 +0000 Subject: [PATCH 306/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3ea6b8811c1..ed4a0144008 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3ea6b8811c13e6084ca92230bd5b789d428dfb88 +Subproject commit ed4a0144008979492f92175de66a322ed8fb0629 From 38699891ed0cee43686a8c8f49b1ab78c5f6bf67 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 26 Apr 2025 12:21:28 -0700 Subject: [PATCH 307/919] change order of operations to avoid a use after free --- docs/changelog.txt | 1 + library/modules/Items.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 187467ac26a..e1ddf8d47c2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed +- `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index ff92b3cece7..36d1748c5bb 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -816,9 +816,12 @@ static bool detachItem(df::item *item) } } - if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) - return linked_list_remove(&world->projectiles.all, ref->projectile_id) - && removeRef(item->general_refs, general_ref_type::PROJECTILE, ref->getID()); + if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) { + auto proj_id = ref->projectile_id; + bool isRefRemoved = removeRef(item->general_refs, general_ref_type::PROJECTILE, proj_id); + bool isLinkRemoved = linked_list_remove(&world->projectiles.all, proj_id); + return isRefRemoved && isLinkRemoved; + } if (item->flags.bits.on_ground) { if (!removeItemOnGround(item)) From 2d0649f398dcec516504d2cb6090c149e29f316a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:33:50 +0000 Subject: [PATCH 308/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ed4a0144008..6c24dc15f32 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ed4a0144008979492f92175de66a322ed8fb0629 +Subproject commit 6c24dc15f3268ee6216af3b8da565348888d2393 From 6762009487ad04762e9fc7872ea0220a3ac34d32 Mon Sep 17 00:00:00 2001 From: Quietust Date: Thu, 17 Apr 2025 19:28:40 -0600 Subject: [PATCH 309/919] Fix Items::getDescription * Display correct quality level for items with "non-displayed" improvements * Add ALL item designations here (and remove "XXwearXX" from other spot) * Obey SHOW_IMP_QUALITY setting --- docs/changelog.txt | 1 + library/modules/Items.cpp | 58 +++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e1ddf8d47c2..e9713f8b9f0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -105,6 +105,7 @@ Template for new versions: - ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units - ``Buildings::setOwner``: updated for changes in 51.11 +- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys SHOW_IMP_QUALITY ## Lua - ``dfhack.military.addToSquad``: expose Military API function diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 36d1748c5bb..a161bea8a2d 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -49,6 +49,7 @@ distribution. #include "df/caravan_state.h" #include "df/creature_raw.h" #include "df/dfhack_material_category.h" +#include "df/d_init.h" #include "df/entity_buy_prices.h" #include "df/entity_buy_requests.h" #include "df/entity_raw.h" @@ -678,14 +679,54 @@ string Items::getDescription(df::item *item, int type, bool decorate) { item->getItemDescription(&tmp, type); if (decorate) { - addQuality(tmp, item->getQuality()); + // Special indicators get added in a specific order + // Innermost is at the top, and outermost is at the bottom - if (item->isImproved()) { + // First, figure out the quality levels we're going to display + int craftquality = item->getQuality(); + int craftquality_only_imps = item->getImprovementQuality(); + bool has_displayed_item_improvements = item->isImproved(); + if (!has_displayed_item_improvements && (craftquality < craftquality_only_imps)) + craftquality = craftquality_only_imps; + + // First, actual item quality + addQuality(tmp, craftquality); + + // Next, magic enchants + if (item->getMagic() != NULL) + tmp = '\x11' + tmp + '\x10'; // <| |> + + // Next, improvements + if (has_displayed_item_improvements) { tmp = '\xAE' + tmp + '\xAF'; // («) + tmp + (») - addQuality(tmp, item->getImprovementQuality()); + if (df::global::d_init->display.flags.is_set(d_init_flags1::SHOW_IMP_QUALITY)) + addQuality(tmp, craftquality_only_imps); + } + + // Dwarf mode only, forbid/foreign + if (*df::global::gamemode == game_mode::DWARF) { + if (item->flags.bits.forbid) + tmp = '{' + tmp + '}'; + if (item->flags.bits.foreign) + tmp = '(' + tmp + ')'; + } + + // Wear + switch (item->getWear()) + { + case 1: tmp = 'x' + tmp + 'x'; break; + case 2: tmp = 'X' + tmp + 'X'; break; + case 3: tmp = "XX" + tmp + "XX"; break; } - if (item->flags.bits.foreign) - tmp = "(" + tmp + ")"; + + // Fire + if (item->flags.bits.on_fire) + tmp = '\x13' + tmp + '\x13'; // !! !! + + // Finally, Adventure civzone + if ((*df::global::gamemode == game_mode::ADVENTURE) && + Items::getGeneralRef(item, general_ref_type::BUILDING_CIVZONE_ASSIGNED) != NULL) + tmp = '$' + tmp + '$'; } return tmp; } @@ -721,13 +762,6 @@ string Items::getReadableDescription(df::item *item) { CHECK_NULL_POINTER(item); auto desc = get_base_desc(item); - switch (item->getWear()) - { - case 1: desc = "x" + desc + "x"; break; // Worn - case 2: desc = "X" + desc + "X"; break; // Threadbare - case 3: desc = "XX" + desc + "XX"; break; // Tattered - } - if (auto gref = Items::getGeneralRef(item, general_ref_type::CONTAINS_UNIT)) { if (auto unit = gref->getUnit()) { From 2392d41c3f46e3bf4773bc2463872c6d280ba4a0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:01:56 -0700 Subject: [PATCH 310/919] only check scripts for the unavailable tag once --- library/lua/dfhack.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ef08fb7f4b8..15910b46f1b 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -994,13 +994,13 @@ local valid_script_flags = { scripts = {required = false}, } -local warned_scripts = {} +local checked_scripts = {} function dfhack.run_script(name,...) - if not warned_scripts[name] then + if not checked_scripts[name] then + checked_scripts[name] = true local helpdb = require('helpdb') if helpdb.has_tag(name, 'unavailable') then - warned_scripts[name] = true dfhack.printerr(('UNTESTED WARNING: the "%s" script has not been validated to work well with this version of DF.'):format(name)) dfhack.printerr('It may not work as expected, or it may corrupt your game.') qerror('Please run the command again to ignore this warning and proceed.') From 74004612d88d8419af237226bb1329df6ac12589 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:02:45 -0700 Subject: [PATCH 311/919] revert windows-specific change to script-manager --- library/lua/script-manager.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 71bbd3449e1..9a1ecb1df00 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -52,9 +52,6 @@ function reload(refresh_active_mod_scripts) local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name) if script_path:find('scripts_modactive') then local full_path = script_path..'/'..script_name - if dfhack.getOSType() == 'windows' then - full_path = script_path..'\\'..script_name - end internal_script = dfhack.internal.scripts[full_path] if internal_script then dfhack.internal.scripts[full_path] = nil From b260041a03f93a931dfefe070ec0f12a57e72f1b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:02:54 -0700 Subject: [PATCH 312/919] canonicalize slashes in path returned from findScript --- docs/dev/Lua API.rst | 10 ++++++---- library/LuaApi.cpp | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 2c7f9cacbce..bf7ee35eff2 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3338,12 +3338,14 @@ and are only documented here for completeness: * ``dfhack.internal.findScript(name)`` - Searches `script paths ` for the script ``name`` and returns the - path of the first file found, or ``nil`` on failure. + Searches `script paths ` for the script ``name`` (which + includes the ``.lua`` extension) and returns the absolute path of the first + file found, or ``nil`` on failure. Slashes in the path are canonicalized to + forward slashes. .. note:: - This requires an extension to be specified (``.lua`` or ``.rb``) - use - ``dfhack.findScript()`` to include the ``.lua`` extension automatically. + You can use the ``dfhack.findScript()`` wrapper if you want to specify the + script name without the ``.lua`` extension. * ``dfhack.internal.runCommand(command[, use_console])`` diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index c80fecffc26..04b7741828b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3965,7 +3965,7 @@ static int internal_findScript(lua_State *L) const char *name = luaL_checkstring(L, 1); std::filesystem::path path = Core::getInstance().findScript(name); if (!path.empty()) - lua_pushstring(L, path.string().c_str()); + lua_pushstring(L, Filesystem::as_string(path).c_str()); else lua_pushnil(L); return 1; From 833da7d329af22540af1649b598291d6520f6b7d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:37:58 -0700 Subject: [PATCH 313/919] make english name in readable names optional --- docs/dev/Lua API.rst | 23 ++++++++++---------- library/LuaApi.cpp | 5 +++-- library/include/modules/Units.h | 4 ++-- library/modules/Units.cpp | 37 ++++++++++++++++++++------------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index bf7ee35eff2..e810a184403 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1773,17 +1773,18 @@ Units module Get human-readable baby or child name (e.g., "dwarven baby" or "dwarven child"). -* ``dfhack.units.getReadableName(unit or historical_figure)`` - - Returns a string that includes the native and english language name of the - unit (if any), the race of the unit (if different from fort), whether it is - trained for war or hunting, any syndrome-given descriptions (such as - "necromancer"), the training level (if tame), and profession or noble role. - If a ``historical_figure`` is passed instead of a unit, some information - (e.g., agitation status) is not available, and the profession may be - different (e.g., "Monk") from what is displayed in fort mode. - -* ``dfhack.units.getAge(unit[,true_age])`` +* ``dfhack.units.getReadableName(unit or historical_figure[, skip_english])`` + + Returns a string that includes the native and english language name (if + ``skip_english`` is not ``true``) of the unit (if any), the race of the unit + (if different from fort), whether it is trained for war or hunting, any + syndrome-given descriptions (such as "necromancer"), the training level (if + tame), and profession or noble role. If a ``historical_figure`` is passed + instead of a unit, some information (e.g., agitation status) is not + available, and the profession may be different (e.g., "Monk") from what is + displayed in fort mode. + +* ``dfhack.units.getAge(unit[, true_age])`` Returns the age of the unit in years as a floating-point value. If ``true_age`` is true, ignores false identities. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 04b7741828b..228399c84fd 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2291,10 +2291,11 @@ static int units_assignTrainer(lua_State *L) { } static int units_getReadablename(lua_State *L) { + bool skip_english = lua_toboolean(L, 2); // defaults to false if (auto unit = Lua::GetDFObject(L, 1)) - Lua::Push(L, Units::getReadableName(unit)); + Lua::Push(L, Units::getReadableName(unit, skip_english)); else if (auto hf = Lua::GetDFObject(L, 1)) - Lua::Push(L, Units::getReadableName(hf)); + Lua::Push(L, Units::getReadableName(hf, skip_english)); else luaL_argerror(L, 1, "Expected a unit or a historical figure"); return 1; diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 86238308d70..0cba2516445 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -281,9 +281,9 @@ DFHACK_EXPORT std::string getRaceBabyName(df::unit *unit, bool plural = false); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id, bool plural = false); DFHACK_EXPORT std::string getRaceChildName(df::unit *unit, bool plural = false); // Full readable name including profession. HF profession might be different from unit profession. -DFHACK_EXPORT std::string getReadableName(df::historical_figure *hf); +DFHACK_EXPORT std::string getReadableName(df::historical_figure *hf, bool skip_english = false); // Full readable name including profession, curse name, and tame description. -DFHACK_EXPORT std::string getReadableName(df::unit *unit); +DFHACK_EXPORT std::string getReadableName(df::unit *unit, bool skip_english = false); // Unit's age (in non-integer years). Ignore false identities if true_age. DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 4ccce422778..ce135b477d3 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1153,7 +1153,25 @@ static string getTameTag(df::unit *unit) { } } -string Units::getReadableName(df::historical_figure *hf) { +template +static string formatReadableName(T *unit_or_hf, const string &prof_name, bool skip_english) { + auto visible_name = Units::getVisibleName(unit_or_hf); + string native_name = Translation::translateName(visible_name); + + if (native_name.empty()) + return prof_name; + + if (skip_english) + return native_name + ", " + prof_name; + + string english_name = Translation::translateName(visible_name, true, true); + if (english_name.empty()) + return native_name + ", " + prof_name; + + return native_name + " \"" + english_name + "\", " + prof_name; +} + +string Units::getReadableName(df::historical_figure *hf, bool skip_english) { CHECK_NULL_POINTER(hf); string prof_name = getProfessionName(hf, false, false, true); @@ -1167,11 +1185,10 @@ string Units::getReadableName(df::historical_figure *hf) { prof_name = "Ghostly " + prof_name; } - string name = Translation::translateName(getVisibleName(hf)); - return name.empty() ? prof_name : name + ", " + prof_name; + return formatReadableName(hf, prof_name, skip_english); } -string Units::getReadableName(df::unit *unit) { +string Units::getReadableName(df::unit *unit, bool skip_english) { CHECK_NULL_POINTER(unit); string prof_name = getProfessionName(unit, false, false, true); @@ -1190,17 +1207,7 @@ string Units::getReadableName(df::unit *unit) { if (isTame(unit)) prof_name += " (" + getTameTag(unit) + ")"; - auto visible_name = getVisibleName(unit); - string native_name = Translation::translateName(visible_name); - - if (native_name.empty()) - return prof_name; - - string english_name = Translation::translateName(visible_name, true, true); - if (english_name.empty()) - return native_name + ", " + prof_name; - - return native_name + " \"" + english_name + "\", " + prof_name; + return formatReadableName(unit, prof_name, skip_english); } double Units::getAge(df::unit *unit, bool true_age) { From ee319b17d1d980cb7e6144dd83b06b85726e50a7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:21:40 -0700 Subject: [PATCH 314/919] update scripts ref --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6c24dc15f32..fdd597e7590 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6c24dc15f3268ee6216af3b8da565348888d2393 +Subproject commit fdd597e75904f54c089bec114b3c8f5fa34ce520 From bfc584c6b57507d80c7a20fc336f20c396a8304d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:41:59 -0700 Subject: [PATCH 315/919] fix changelog entry --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index bb2f01a8e3d..7111999ecb8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed - `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air +- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. @@ -82,7 +83,6 @@ Template for new versions: ## Fixes - `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist - `gui/design`: fix misaligned shape icons -- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows. # 51.11-r1 From b1dbe0139917d031cc663de02cb847cba669422e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:43:23 -0700 Subject: [PATCH 316/919] fix changelog entry --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7111999ecb8..0bd39e0cbd2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## Documentation ## API +- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting ## Lua - ``script-manager``: new ``get_active_mods()`` function for getting information on active mods @@ -106,7 +107,6 @@ Template for new versions: - ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 - ``Units::teleport``: projectile information is now cleared for teleported units - ``Buildings::setOwner``: updated for changes in 51.11 -- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys SHOW_IMP_QUALITY ## Lua - ``dfhack.military.addToSquad``: expose Military API function From 5eb5f83ad114e617ede10794317201896555b052 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 10:38:02 -0500 Subject: [PATCH 317/919] replace some `#define`s with `constexpr` --- library/PluginManager.cpp | 1 + library/include/DataFuncs.h | 7 +++++++ library/include/LuaWrapper.h | 34 +++++++++++++++++----------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 98a9eaeec40..575ebdc7f51 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -772,6 +772,7 @@ int Plugin::lua_cmd_wrapper(lua_State *state) int Plugin::lua_fun_wrapper(lua_State *state) { + using DFHack::LuaWrapper::UPVAL_CONTAINER_ID; auto cmd = (LuaFunction*)lua_touserdata(state, UPVAL_CONTAINER_ID); RefAutoinc lock(cmd->owner->access); diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index a8d2caca51c..0b297a68e4a 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -54,6 +54,7 @@ namespace df { T get_from_lua_state(lua_State* L, int idx) { using DFHack::LuaWrapper::field_error; + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; using Ptr = std::add_pointer_t>; Ptr ptr{}; df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &ptr, idx); @@ -69,6 +70,7 @@ namespace df { template T get_from_lua_state(lua_State* L, int idx) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; T val{}; df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &val, idx); return val; @@ -78,6 +80,7 @@ namespace df { requires std::is_invocable_r_v void call_and_push_impl(lua_State* L, int base, std::index_sequence, FT fun, ET... extra) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; if constexpr (std::is_same_v) { std::invoke(fun, extra..., (get_from_lua_state(L, base+I))...); lua_pushnil(L); @@ -119,6 +122,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT)+1; static void execute(lua_State *L, int base, RT(CT::*mem_fun)(AT...)) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT *self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -128,6 +132,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT)+1; static void execute(lua_State *L, int base, RT(CT::*mem_fun)(AT...) const) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT *self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -154,6 +159,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT) + 1; static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...)) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -163,6 +169,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT) + 1; static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...) const) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 44bc19938c6..b95713e5727 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -70,51 +70,51 @@ namespace LuaWrapper { extern LuaToken DFHACK_PTR_IDTABLE_TOKEN; // Function registry names -#define DFHACK_CHANGEERROR_NAME "DFHack::ChangeError" -#define DFHACK_COMPARE_NAME "DFHack::ComparePtrs" -#define DFHACK_TYPE_TOSTRING_NAME "DFHack::TypeToString" -#define DFHACK_SIZEOF_NAME "DFHack::Sizeof" -#define DFHACK_DISPLACE_NAME "DFHack::Displace" -#define DFHACK_NEW_NAME "DFHack::New" -#define DFHACK_ASSIGN_NAME "DFHack::Assign" -#define DFHACK_IS_INSTANCE_NAME "DFHack::IsInstance" -#define DFHACK_DELETE_NAME "DFHack::Delete" -#define DFHACK_CAST_NAME "DFHack::Cast" + constexpr auto DFHACK_CHANGEERROR_NAME = "DFHack::ChangeError"; + constexpr auto DFHACK_COMPARE_NAME = "DFHack::ComparePtrs"; + constexpr auto DFHACK_TYPE_TOSTRING_NAME = "DFHack::TypeToString"; + constexpr auto DFHACK_SIZEOF_NAME = "DFHack::Sizeof"; + constexpr auto DFHACK_DISPLACE_NAME = "DFHack::Displace"; + constexpr auto DFHACK_NEW_NAME = "DFHack::New"; + constexpr auto DFHACK_ASSIGN_NAME = "DFHack::Assign"; + constexpr auto DFHACK_IS_INSTANCE_NAME = "DFHack::IsInstance"; + constexpr auto DFHACK_DELETE_NAME = "DFHack::Delete"; + constexpr auto DFHACK_CAST_NAME = "DFHack::Cast"; extern LuaToken DFHACK_EMPTY_TABLE_TOKEN; /* * Upvalue: contents of DFHACK_TYPETABLE_NAME */ -#define UPVAL_TYPETABLE lua_upvalueindex(1) + constexpr auto UPVAL_TYPETABLE = lua_upvalueindex(1); /* * Expected metatable of the current object. */ -#define UPVAL_METATABLE lua_upvalueindex(2) + constexpr auto UPVAL_METATABLE = lua_upvalueindex(2); /* * Table mapping field names to indices or data structure pointers. * Enum index table is linked into here via getmetatable($).__index. * Fields that are actually in UPVAL_METATABLE are marked with NULL light udata. */ -#define UPVAL_FIELDTABLE lua_upvalueindex(3) -#define UPVAL_METHOD_NAME lua_upvalueindex(3) + constexpr auto UPVAL_FIELDTABLE = lua_upvalueindex(3); + constexpr auto UPVAL_METHOD_NAME = lua_upvalueindex(3); /* * Only for containers: light udata with container identity. */ -#define UPVAL_CONTAINER_ID lua_upvalueindex(4) + constexpr auto UPVAL_CONTAINER_ID = lua_upvalueindex(4); /* * Only for containers: light udata with item identity. */ -#define UPVAL_ITEM_ID lua_upvalueindex(5) + constexpr auto UPVAL_ITEM_ID = lua_upvalueindex(5); /* * Only for containers: if not nil, overrides the item count. */ -#define UPVAL_ITEM_COUNT lua_upvalueindex(6) + constexpr auto UPVAL_ITEM_COUNT = lua_upvalueindex(6); inline void lua_dup(lua_State *state) { lua_pushvalue(state, -1); } inline void lua_swap(lua_State *state) { lua_insert(state, -2); } From c21478c256df11735aad4fed61e3faaf9d2ed99f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 10:56:52 -0500 Subject: [PATCH 318/919] minor quality issues `using` instead of `typedef` `[[noreturn]]` on a nonreturning function explicit `[[fallthrough]]` --- library/LuaWrapper.cpp | 6 +++--- library/include/LuaWrapper.h | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index f9fd0e78327..8c4a17f8dbe 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -64,10 +64,10 @@ using namespace DFHack::LuaWrapper; /* */ -static int change_error(lua_State *state) +[[noreturn]] static int change_error(lua_State *state) { luaL_error(state, "Attempt to change a read-only table.\n"); - return 0; + std::abort(); // should never be reached but makes gcc happy } /** @@ -1642,7 +1642,7 @@ static void RenderType(lua_State *state, const compound_identity *node) case IDTYPE_UNION: // TODO: change this to union-type? what relies on this? lua_pushboolean(state, true); lua_setfield(state, ftable, "_union"); - // fall through + [[fallthrough]]; case IDTYPE_STRUCT: lua_pushstring(state, "struct-type"); lua_setfield(state, ftable, "_kind"); diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index b95713e5727..6d77af87dbb 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -231,7 +231,8 @@ namespace LuaWrapper { /** * Wrap functions and add them to the table on the top of the stack. */ - typedef DFHack::FunctionReg FunctionReg; + using DFHack::FunctionReg; + void SetFunctionWrappers(lua_State *state, const FunctionReg *reg); int method_wrapper_core(lua_State *state, function_identity_base *id); From 00597e778e3c50976dab7b695565d9e2842de17c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 11:06:05 -0500 Subject: [PATCH 319/919] ColorText: `typedef` -> `using` --- library/include/ColorText.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f484e61d4b6..f5768c0ffa3 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -65,7 +65,7 @@ namespace DFHack class DFHACK_EXPORT color_ostream : public std::ostream { public: - typedef DFHack::color_value color_value; + using color_value = DFHack::color_value; private: color_value cur_color; From a012347210769b58ada362fd7fd77dc6bc734f2e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 28 Apr 2025 11:39:08 -0500 Subject: [PATCH 320/919] Core.h: `typedef` -> `using` also this is C++, not C, so `typedef struct T T` doesn't belong here --- library/include/Core.h | 8 ++++---- library/include/DataDefs.h | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/library/include/Core.h b/library/include/Core.h index 9ef8fb8f0e1..205b130b91c 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -44,10 +44,6 @@ distribution. #include #include -#define DFH_MOD_SHIFT 1 -#define DFH_MOD_CTRL 2 -#define DFH_MOD_ALT 4 - struct WINDOW; struct lua_State; @@ -58,6 +54,10 @@ namespace df namespace DFHack { + constexpr auto DFH_MOD_SHIFT = 1; + constexpr auto DFH_MOD_CTRL = 2; + constexpr auto DFH_MOD_ALT = 4; + class Process; class Module; class Materials; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 59f47abcb65..e8e54fac412 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -35,7 +35,7 @@ distribution. #include "BitArray.h" #include "Export.h" -typedef struct lua_State lua_State; +struct lua_State; /* * Definitions of DFHack namespace structs used by generated headers. @@ -352,9 +352,9 @@ namespace DFHack }; #ifdef _MSC_VER - typedef void *virtual_ptr; + using virtual_ptr = void*; #else - typedef virtual_class *virtual_ptr; + using virtual_ptr = virtual_class*; #endif class DFHACK_EXPORT VMethodInterposeLinkBase; From 8067b6ce4598a413203c123328d814779a1addc1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:21:05 +0000 Subject: [PATCH 321/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index fdd597e7590..15b3be7fa93 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit fdd597e75904f54c089bec114b3c8f5fa34ce520 +Subproject commit 15b3be7fa9353b01ccc1b3e1fd4ff7913e428f08 From aae16bd6578d1c9c2e24affd7e24ccfd5ba50a12 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 29 Apr 2025 10:59:06 -0500 Subject: [PATCH 322/919] DataIdentity: `typedef` -> `using` --- library/include/DataIdentity.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 7e1f8eaaea7..5aa256809a1 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -319,7 +319,7 @@ namespace df class DFHACK_EXPORT stl_ptr_vector_identity : public ptr_container_identity { public: - typedef std::vector container; + using container = std::vector; /* * This class assumes that std::vector is equivalent @@ -486,7 +486,7 @@ namespace df * in layout and behavior to BitArray for any T. */ - typedef BitArray container; + using container = BitArray; bit_array_identity(const enum_identity *ienum = NULL) : bit_container_identity(sizeof(container), &allocator_fn, ienum) @@ -516,7 +516,7 @@ namespace df class DFHACK_EXPORT stl_bit_vector_identity : public bit_container_identity { public: - typedef std::vector container; + using container = std::vector; stl_bit_vector_identity(const enum_identity *ienum = NULL) : bit_container_identity(sizeof(container), &df::allocator_fn, ienum) @@ -547,7 +547,7 @@ namespace df template class enum_list_attr_identity : public container_identity { public: - typedef enum_list_attr container; + using container = enum_list_attr; enum_list_attr_identity(const type_identity *item) : container_identity(sizeof(container), NULL, item, NULL) @@ -615,7 +615,7 @@ namespace df template struct DFHACK_EXPORT identity_traits> { static opaque_identity *get() { - typedef std::shared_ptr type; + using type = std::shared_ptr; static std::string name = std::string("shared_ptr<") + typeid(T).name() + ">"; static opaque_identity identity(sizeof(type), allocator_noassign_fn, name); return &identity; @@ -772,7 +772,7 @@ namespace df template inline const container_identity *identity_traits >::get() { - typedef std::vector container; + using container = std::vector; static const stl_container_identity identity("vector", identity_traits::get()); return &identity; } @@ -800,28 +800,28 @@ namespace df #ifdef BUILD_DFHACK_LIB template inline const container_identity *identity_traits >::get() { - typedef std::deque container; + using container = std::deque; static const stl_container_identity identity("deque", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - typedef std::set container; + using container = std::set; static const ro_stl_container_identity identity("set", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::map container; + using container = std::map; static const ro_stl_assoc_container_identity identity("map", identity_traits::get(), identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::unordered_map container; + using container = std::unordered_map; static const ro_stl_assoc_container_identity identity("unordered_map", identity_traits::get(), identity_traits::get()); return &identity; } @@ -834,7 +834,7 @@ namespace df template inline const container_identity *identity_traits >::get() { - typedef DfArray container; + using container = DfArray; static const stl_container_identity identity("DfArray", identity_traits::get()); return &identity; } From 5b5c5c6449cf983b92d9321ded86b09d3cc191a2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 29 Apr 2025 11:00:33 -0500 Subject: [PATCH 323/919] LuaApi: template isntead of ad hoc macro can't get rid of the second one until c++26 --- library/LuaApi.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 228399c84fd..077d8cd9c5a 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2667,20 +2667,17 @@ static const LuaWrapper::FunctionReg dfhack_world_module[] = { { NULL, NULL } }; -#define WORLD_GAMEMODE_WRAPPER(func) \ - static int world_gamemode_##func(lua_State *L) \ - { \ - int gametype = luaL_optint(L, 1, -1); \ - lua_pushboolean(L, World::func((df::game_type)gametype)); \ - return 1;\ - } -#define WORLD_GAMEMODE_FUNC(func) \ - {#func, world_gamemode_##func} +using gamemode_func = auto (df::game_type t) -> bool; +template +static int world_gamemode(lua_State* L) +{ + int gametype = luaL_optint(L, 1, -1); + lua_pushboolean(L, gmf((df::game_type)gametype)); + return 1; +} -WORLD_GAMEMODE_WRAPPER(isFortressMode); -WORLD_GAMEMODE_WRAPPER(isAdventureMode); -WORLD_GAMEMODE_WRAPPER(isArena); -WORLD_GAMEMODE_WRAPPER(isLegends); +#define WORLD_GAMEMODE_FUNC(func) \ + {#func, world_gamemode} static const luaL_Reg dfhack_world_funcs[] = { WORLD_GAMEMODE_FUNC(isFortressMode), From e803731cd14dbc8be19a473b09a8185c5b9dfabf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 1 May 2025 20:33:55 -0500 Subject: [PATCH 324/919] LuaApi: templated lambda instead of macro --- library/LuaApi.cpp | 53 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 077d8cd9c5a..927e2f6d721 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3786,32 +3786,39 @@ static int internal_diffscan(lua_State *L) bool has_newv = !lua_isnil(L, 7); bool has_diffv = !lua_isnil(L, 8); -#define LOOP(esz, etype) \ - case esz: { \ - etype *pold = (etype*)old_data; \ - etype *pnew = (etype*)new_data; \ - etype oldv = (etype)luaL_optint(L, 6, 0); \ - etype newv = (etype)luaL_optint(L, 7, 0); \ - etype diffv = (etype)luaL_optint(L, 8, 0); \ - for (int i = start_idx; i < end_idx; i++) { \ - if (pold[i] == pnew[i]) continue; \ - if (has_oldv && pold[i] != oldv) continue; \ - if (has_newv && pnew[i] != newv) continue; \ - if (has_diffv && etype(pnew[i]-pold[i]) != diffv) continue; \ - lua_pushinteger(L, i); return 1; \ - } \ - break; \ - } + auto loop = [&]() -> std::optional + { + etype* pold = (etype*)old_data; + etype* pnew = (etype*)new_data; + etype oldv = (etype)luaL_optint(L, 6, 0); + etype newv = (etype)luaL_optint(L, 7, 0); + etype diffv = (etype)luaL_optint(L, 8, 0); + for (int i = start_idx; i < end_idx; i++) { + if (pold[i] == pnew[i]) continue; + if (has_oldv && pold[i] != oldv) continue; + if (has_newv && pnew[i] != newv) continue; + if (has_diffv && etype(pnew[i] - pold[i]) != diffv) continue; + return i; + } + return std::nullopt; + }; + + std::optional res; switch (eltsize) { - LOOP(1, uint8_t); - LOOP(2, uint16_t); - LOOP(4, uint32_t); - default: - luaL_argerror(L, 5, "invalid element size"); + case 1: + res = loop.operator()(); break; + case 2: + res = loop.operator()(); break; + case 4: + res = loop.operator()(); break; + default: + luaL_argerror(L, 5, "invalid element size"); } -#undef LOOP + if (res) + lua_pushinteger(L,*res); + else + lua_pushnil(L); - lua_pushnil(L); return 1; } From 2fb9a275c95b3c80e4395dd3581baaabea79ed81 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 2 May 2025 15:37:18 -0700 Subject: [PATCH 325/919] downgrade warning to debug DF now routinely has zones assigned to non-existent units --- docs/changelog.txt | 1 + plugins/preserve-rooms.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b94cdce955d..816f75469a2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: - `getplants`: fix logic for determining whether plant growths have been picked - `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air - `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows +- `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 805e10c8370..d5b4b5b7a5a 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -513,7 +513,7 @@ static void process_rooms(color_ostream &out, } auto owner = Buildings::getOwner(zone); if (!owner) { - WARN(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); + DEBUG(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); continue; } auto hf = df::historical_figure::find(owner->hist_figure_id); From 94cdaf713329fbf25f9fcf7e85bc2ff6cbc26ac6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 10:57:12 -0500 Subject: [PATCH 326/919] Remove unused `RegisterData` API from Core as discussed in #5438 and on discord --- docs/changelog.txt | 1 + library/Core.cpp | 23 ----------------------- library/include/Core.h | 8 -------- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 816f75469a2..923e53b4823 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -77,6 +77,7 @@ Template for new versions: - ``script-manager``: new ``get_mod_info_metadata()`` function for getting information out of mod ``info.txt`` files ## Removed +- removed historically unused ``Core::RegisterData``/``Core::GetData`` API and associated internal data structures # 51.11-r1.2 diff --git a/library/Core.cpp b/library/Core.cpp index 6e875648fbd..d949db688c5 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1496,7 +1496,6 @@ Core::Core() : HotkeyCond{}, alias_mutex{}, started{false}, - misc_data_mutex{}, CoreSuspendMutex{}, CoreWakeup{}, ownerThread{}, @@ -1995,28 +1994,6 @@ void Core::printerr(const char *format, ...) va_end(args); } -void Core::RegisterData( void *p, std::string key ) -{ - std::lock_guard lock(misc_data_mutex); - misc_data_map[key] = p; -} - -void *Core::GetData( std::string key ) -{ - std::lock_guard lock(misc_data_mutex); - std::map::iterator it=misc_data_map.find(key); - - if ( it != misc_data_map.end() ) - { - void *p=it->second; - return p; - } - else - { - return 0;// or throw an error. - } -} - Core& Core::getInstance() { static Core instance; return instance; diff --git a/library/include/Core.h b/library/include/Core.h index 205b130b91c..dd84d49df03 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -168,11 +168,6 @@ namespace DFHack /// removes the hotkey command and gives it to the caller thread std::string getHotkeyCmd( bool &keep_going ); - /// adds a named pointer (for later or between plugins) - void RegisterData(void *p,std::string key); - /// returns a named pointer. - void *GetData(std::string key); - command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream &out, const std::string &command); bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); @@ -316,9 +311,6 @@ namespace DFHack // Additional state change scripts std::vector state_change_scripts; - std::mutex misc_data_mutex; - std::map misc_data_map; - /*! * \defgroup core_suspend CoreSuspender state handling serialization to * DF memory. From 6ad91b66ed2504f81d2f5699218761687b41744f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 3 May 2025 18:27:20 +0000 Subject: [PATCH 327/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 15b3be7fa93..def13b7c6f1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 15b3be7fa9353b01ccc1b3e1fd4ff7913e428f08 +Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc From 90cac3d574cda6e8fd1b7c1a8dfb3b6f93a0c5b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 13:28:19 -0500 Subject: [PATCH 328/919] merge `Process.cpp` into a single source file --- library/CMakeLists.txt | 4 +- library/Process-darwin.cpp | 341 --------------- library/Process-linux.cpp | 250 ----------- library/Process-windows.cpp | 455 -------------------- library/Process.cpp | 801 ++++++++++++++++++++++++++++++++++++ 5 files changed, 802 insertions(+), 1049 deletions(-) delete mode 100644 library/Process-darwin.cpp delete mode 100644 library/Process-linux.cpp delete mode 100644 library/Process-windows.cpp create mode 100644 library/Process.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 37f5b12d5ec..895ae6eb860 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -105,6 +105,7 @@ set(MAIN_SOURCES PluginManager.cpp PluginStatics.cpp PlugLoad.cpp + Process.cpp TileTypes.cpp VersionInfoFactory.cpp RemoteClient.cpp @@ -126,7 +127,6 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} Hooks.cpp - Process-windows.cpp ) if(WIN32) @@ -136,13 +136,11 @@ endif() set(MAIN_SOURCES_LINUX ${CONSOLE_SOURCES} - Process-linux.cpp ) set(MAIN_SOURCES_DARWIN ${CONSOLE_SOURCES} PlugLoad-posix.cpp - Process-darwin.cpp ) set(MODULE_HEADERS diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp deleted file mode 100644 index f8c0833a782..00000000000 --- a/library/Process-darwin.cpp +++ /dev/null @@ -1,341 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "md5wrapper.h" -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - - -Process::Process(const VersionInfoFactory& known_versions) : identified(false), my_pe(0) -{ - char path[1024]; - char *real_path; - uint32_t size = sizeof(path); - if (_NSGetExecutablePath(path, &size) == 0) { - real_path = realpath(path, NULL); - } - - md5wrapper md5; - uint32_t length; - uint8_t first_kb [1024]; - memset(first_kb, 0, sizeof(first_kb)); - // get hash of the running DF process - my_md5 = md5.getHashFromFile(real_path, length, (char *) first_kb); - // create linux process, add it to the vector - auto vinfo = known_versions.getVersionInfoByMD5(my_md5); - if(vinfo) - { - identified = true; - my_descriptor = std::make_shared(*vinfo); - } - else - { - char * wd = getcwd(NULL, 0); - cerr << "Unable to retrieve version information.\n"; - cerr << "File: " << real_path << endl; - cerr << "MD5: " << my_md5 << endl; - cerr << "working dir: " << wd << endl; - cerr << "length:" << length << endl; - cerr << "1KB hexdump follows:" << endl; - for(int i = 0; i < 64; i++) - { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - first_kb[i*16], - first_kb[i*16+1], - first_kb[i*16+2], - first_kb[i*16+3], - first_kb[i*16+4], - first_kb[i*16+5], - first_kb[i*16+6], - first_kb[i*16+7], - first_kb[i*16+8], - first_kb[i*16+9], - first_kb[i*16+10], - first_kb[i*16+11], - first_kb[i*16+12], - first_kb[i*16+13], - first_kb[i*16+14], - first_kb[i*16+15] - ); - } - free(wd); - } -} - -Process::~Process() -{ - // Nothing to do here -} - -string Process::doReadClassName (void * vptr) -{ - char * typeinfo = Process::readPtr(((char *)vptr - sizeof(void*))); - char * typestring = Process::readPtr(typeinfo + sizeof(void*)); - string raw = readCString(typestring); - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -const char * -inheritance_strings[] = { - "SHARE", "COPY", "NONE", "DONATE_COPY", -}; - -const char * -behavior_strings[] = { - "DEFAULT", "RANDOM", "SEQUENTIAL", "RESQNTL", "WILLNEED", "DONTNEED", -}; - -void Process::getMemRanges( vector & ranges ) -{ - static bool log_ranges = (getenv("DFHACK_LOG_MEM_RANGES") != NULL); - - kern_return_t kr; - task_t the_task; - - the_task = mach_task_self(); - -#ifdef DFHACK64 - mach_vm_size_t vmsize; - mach_vm_address_t address; - vm_region_basic_info_data_64_t info; -#else - vm_size_t vmsize; - vm_address_t address; - vm_region_basic_info_data_t info; -#endif - mach_msg_type_number_t info_count; - vm_region_flavor_t flavor; - memory_object_name_t object; - - kr = KERN_SUCCESS; - address = 0; - - string cur_name; - void * cur_base = nullptr; - - do { -#ifdef DFHACK64 - flavor = VM_REGION_BASIC_INFO_64; - info_count = VM_REGION_BASIC_INFO_COUNT_64; - kr = mach_vm_region(the_task, &address, &vmsize, flavor, - (vm_region_info_64_t)&info, &info_count, &object); -#else - flavor = VM_REGION_BASIC_INFO; - info_count = VM_REGION_BASIC_INFO_COUNT; - kr = vm_region(the_task, &address, &vmsize, flavor, - (vm_region_info_t)&info, &info_count, &object); -#endif - - if (kr == KERN_SUCCESS) { - if (info.reserved==1) { - address += vmsize; - continue; - } - Dl_info dlinfo; - int dlcheck; - dlcheck = dladdr((const void*)address, &dlinfo); - if (dlcheck==0) { - dlinfo.dli_fname = ""; - } - - t_memrange temp; - strncpy( temp.name, dlinfo.dli_fname, 1023 ); - temp.name[1023] = 0; - if (cur_name != temp.name) { - cur_name = temp.name; - cur_base = (void *) address; - } - temp.base = cur_base; - temp.start = (void *) address; - temp.end = (void *) (address+vmsize); - temp.read = (info.protection & VM_PROT_READ); - temp.write = (info.protection & VM_PROT_WRITE); - temp.execute = (info.protection & VM_PROT_EXECUTE); - temp.shared = info.shared; - temp.valid = true; - ranges.push_back(temp); - - if (log_ranges) - { - fprintf(stderr, - "%p-%p %8zuK %c%c%c/%c%c%c %11s %6s %10s uwir=%hu sub=%u dlname: %s\n", - (void*)address, - (void*)(address + vmsize), - size_t(vmsize >> 10), - (info.protection & VM_PROT_READ) ? 'r' : '-', - (info.protection & VM_PROT_WRITE) ? 'w' : '-', - (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', - (info.max_protection & VM_PROT_READ) ? 'r' : '-', - (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', - (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-', - inheritance_strings[info.inheritance], - (info.shared) ? "shared" : "-", - behavior_strings[info.behavior], - info.user_wired_count, - info.reserved, - dlinfo.dli_fname); - } - - address += vmsize; - } else if (kr != KERN_INVALID_ADDRESS) { - - /*if (the_task != MACH_PORT_NULL) { - mach_port_deallocate(mach_task_self(), the_task); - }*/ - return; - } - } while (kr != KERN_INVALID_ADDRESS); - - -/* if (the_task != MACH_PORT_NULL) { - mach_port_deallocate(mach_task_self(), the_task); - }*/ -} - -uintptr_t Process::getBase() -{ - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool /*to_file*/) -{ - return offset; -} - -uint32_t Process::getTickCount() -{ - struct timeval tp; - gettimeofday(&tp, NULL); - return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); -} - -string Process::getPath() -{ - static string cached_path = ""; - if (cached_path.size()) - return cached_path; - char path[1024]; - char *real_path; - uint32_t size = sizeof(path); - if (getcwd(path, size)) - { - cached_path = string(path); - return cached_path; - } - if (_NSGetExecutablePath(path, &size) == 0) { - real_path = realpath(path, NULL); - } - else { - fprintf(stderr, "_NSGetExecutablePath failed!\n"); - cached_path = "."; - return cached_path; - } - std::string path_string(real_path); - int last_slash = path_string.find_last_of("/"); - cached_path = path_string.substr(0,last_slash); - return cached_path; -} - -int Process::getPID() -{ - return getpid(); -} - -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) -{ - int result; - int protect=0; - if(trgrange.read)protect|=PROT_READ; - if(trgrange.write)protect|=PROT_WRITE; - if(trgrange.execute)protect|=PROT_EXEC; - result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); - - return result==0; -} - -// returns -1 on error -void* Process::memAlloc(const int length) -{ - return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); -} - -int Process::memDealloc(void *ptr, const int length) -{ - return munmap(ptr, length); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - - if (prot & Process::MemProt::READ) - prot_native |= PROT_READ; - if (prot & Process::MemProt::WRITE) - prot_native |= PROT_WRITE; - if (prot & Process::MemProt::EXEC) - prot_native |= PROT_EXEC; - - return mprotect(ptr, length, prot_native); -} diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp deleted file mode 100644 index ce64cc113b5..00000000000 --- a/library/Process-linux.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" -#include "md5wrapper.h" - -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - -Process::Process(const VersionInfoFactory& known_versions) : identified(false), my_pe(0) -{ - const char * exe_link_name = "/proc/self/exe"; - - // valgrind replaces readlink for /proc/self/exe, but not open. - char self_exe[1024]; - memset(self_exe, 0, sizeof(self_exe)); - std::string self_exe_name; - if (readlink(exe_link_name, self_exe, sizeof(self_exe) - 1) < 0) - self_exe_name = exe_link_name; - else - self_exe_name = self_exe; - - md5wrapper md5; - uint32_t length; - uint8_t first_kb [1024]; - memset(first_kb, 0, sizeof(first_kb)); - // get hash of the running DF process - my_md5 = md5.getHashFromFile(self_exe_name, length, (char *) first_kb); - // create linux process, add it to the vector - auto vinfo = known_versions.getVersionInfoByMD5(my_md5); - if(vinfo) - { - identified = true; - my_descriptor = std::make_shared(*vinfo); - } - else - { - char * wd = getcwd(NULL, 0); - cerr << "Unable to retrieve version information.\n"; - cerr << "File: " << exe_link_name << endl; - cerr << "MD5: " << my_md5 << endl; - cerr << "working dir: " << wd << endl; - cerr << "length:" << length << endl; - cerr << "1KB hexdump follows:" << endl; - for(int i = 0; i < 64; i++) - { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - first_kb[i*16], - first_kb[i*16+1], - first_kb[i*16+2], - first_kb[i*16+3], - first_kb[i*16+4], - first_kb[i*16+5], - first_kb[i*16+6], - first_kb[i*16+7], - first_kb[i*16+8], - first_kb[i*16+9], - first_kb[i*16+10], - first_kb[i*16+11], - first_kb[i*16+12], - first_kb[i*16+13], - first_kb[i*16+14], - first_kb[i*16+15] - ); - } - free(wd); - } -} - -Process::~Process() -{ - // Nothing to do here -} - -string Process::doReadClassName (void * vptr) -{ - char* typeinfo = Process::readPtr(((char *)vptr - sizeof(void*))); - char* typestring = Process::readPtr(typeinfo + sizeof(void*)); - string raw = readCString(typestring); - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -//FIXME: cross-reference with ELF segment entries? -void Process::getMemRanges( vector & ranges ) -{ - char buffer[1024]; - char permissions[5]; // r/-, w/-, x/-, p/s, 0 - - FILE *mapFile = ::fopen("/proc/self/maps", "r"); - if (!mapFile) - return; - - size_t start, end, offset, device1, device2, node; - - string cur_name; - void * cur_base = nullptr; - - while (fgets(buffer, 1024, mapFile)) - { - t_memrange temp; - temp.name[0] = 0; - sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", - &start, - &end, - (char*)&permissions, - &offset, &device1, &device2, &node, - (char*)temp.name); - if (cur_name != temp.name) { - cur_name = temp.name; - cur_base = (void *) start; - } - temp.base = cur_base; - temp.start = (void *) start; - temp.end = (void *) end; - temp.read = permissions[0] == 'r'; - temp.write = permissions[1] == 'w'; - temp.execute = permissions[2] == 'x'; - temp.shared = permissions[3] == 's'; - temp.valid = true; - ranges.push_back(temp); - } - - fclose(mapFile); -} - -uintptr_t Process::getBase() -{ - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool /*to_file*/) -{ - return offset; -} - -uint32_t Process::getTickCount() -{ - struct timeval tp; - gettimeofday(&tp, NULL); - return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); -} - -std::filesystem::path Process::getPath() -{ - return Filesystem::get_initial_cwd(); -} - -int Process::getPID() -{ - return getpid(); -} - -bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) -{ - int result; - int protect=0; - if(trgrange.read)protect|=PROT_READ; - if(trgrange.write)protect|=PROT_WRITE; - if(trgrange.execute)protect|=PROT_EXEC; - result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); - - return result==0; -} - -bool Process::flushCache(const void* target, size_t count) -{ - __builtin___clear_cache((char*)target, (char*)target + count - 1); - return true; /* assume always succeeds, as the builtin has no return type */ -} - - -// returns -1 on error -void* Process::memAlloc(const int length) -{ - return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); -} - -int Process::memDealloc(void *ptr, const int length) -{ - return munmap(ptr, length); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - - if (prot & Process::MemProt::READ) - prot_native |= PROT_READ; - if (prot & Process::MemProt::WRITE) - prot_native |= PROT_WRITE; - if (prot & Process::MemProt::EXEC) - prot_native |= PROT_EXEC; - - return mprotect(ptr, length, prot_native); -} diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp deleted file mode 100644 index 603636176db..00000000000 --- a/library/Process-windows.cpp +++ /dev/null @@ -1,455 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Error.h" -#include "Internal.h" - -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" - -#define _WIN32_WINNT 0x0600 -#define WINVER 0x0600 - -#define WIN32_LEAN_AND_MEAN -#include -#include - -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - -namespace DFHack -{ - class PlatformSpecific - { - public: - PlatformSpecific() - { - base = 0; - sections = 0; - }; - HANDLE my_handle; - uint32_t my_pid; - IMAGE_NT_HEADERS pe_header; - IMAGE_SECTION_HEADER * sections; - char * base; - }; -} - -Process::Process(const VersionInfoFactory& factory) : identified(false) -{ - HMODULE hmod = NULL; - DWORD needed; - bool found = false; - - d = new PlatformSpecific(); - // open process - d->my_pid = GetCurrentProcessId(); - d->my_handle = GetCurrentProcess(); - // try getting the first module of the process - if(EnumProcessModules(d->my_handle, &hmod, sizeof(hmod), &needed) == 0) - { - return; //if enumprocessModules fails, give up - } - - // got base ;) - d->base = (char *)hmod; - - // read from this process - try - { - uint32_t pe_offset = readDWord(d->base+0x3C); - read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t *)&(d->pe_header)); - const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; - d->sections = (IMAGE_SECTION_HEADER *) malloc(sectionsSize); - read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t *)(d->sections)); - } - catch (std::exception &) - { - return; - } - my_pe = d->pe_header.FileHeader.TimeDateStamp; - auto vinfo = factory.getVersionInfoByPETimestamp(my_pe); - if(vinfo) - { - identified = true; - // give the process a data model and memory layout fixed for the base of first module - my_descriptor = std::make_shared(*vinfo); - my_descriptor->rebaseTo(getBase()); - } - else - { - fprintf(stderr, "Unable to retrieve version information.\nPE timestamp: 0x%x\n", my_pe); - fflush(stderr); - } -} - -Process::~Process() -{ - // destroy our rebased copy of the memory descriptor - if(d->sections != NULL) - free(d->sections); -} - -string Process::doReadClassName (void * vptr) -{ - char* rtti = readPtr((char *)vptr - sizeof(void*)); -#ifdef DFHACK64 - void* base; - if (!RtlPcToFileHeader(rtti, &base)) - return "dummy"; - char* typeinfo = (char *)base + readDWord(rtti + 0xC); - std::string raw = readCString(typeinfo + 0x10); -#else - char* typeinfo = readPtr(rtti + 0xC); - std::string raw = readCString(typeinfo + 0x8); -#endif - if (raw.length() == 0) - return "dummy"; - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -/* -typedef struct _MEMORY_BASIC_INFORMATION -{ - void * BaseAddress; - void * AllocationBase; - uint32_t AllocationProtect; - size_t RegionSize; - uint32_t State; - uint32_t Protect; - uint32_t Type; -} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; -*/ -/* -//Internal structure used to store heap block information. -struct HeapBlock -{ - PVOID dwAddress; - DWORD dwSize; - DWORD dwFlags; - ULONG reserved; -}; -*/ - -static void GetDosNames(std::map &table) -{ - // Partially based on example from msdn: - // Translate path with device name to drive letters. - TCHAR szTemp[512]; - szTemp[0] = '\0'; - - if (GetLogicalDriveStrings(sizeof(szTemp)-1, szTemp)) - { - TCHAR szName[MAX_PATH]; - TCHAR szDrive[3] = " :"; - BOOL bFound = FALSE; - TCHAR* p = szTemp; - - do - { - // Copy the drive letter to the template string - *szDrive = *p; - - // Look up each device name - if (QueryDosDevice(szDrive, szName, MAX_PATH)) - table[szName] = szDrive; - - // Go to the next NULL character. - while (*p++); - } while (*p); // end of string - } -} - -void Process::getMemRanges( vector & ranges ) -{ - MEMORY_BASIC_INFORMATION MBI; - //map heaps; - uint64_t movingStart = 0; - PVOID LastAllocationBase = 0; - map nameMap; - map dosDrives; - - // get page size - SYSTEM_INFO si; - GetSystemInfo(&si); - uint64_t PageSize = si.dwPageSize; - - // get dos drive names - GetDosNames(dosDrives); - - ranges.clear(); - - HANDLE my_handle = GetCurrentProcess(); - - // enumerate heaps - // HeapNodes(d->my_pid, heaps); - // go through all the VM regions, convert them to our internal format - while (VirtualQueryEx(my_handle, (const void*) (movingStart), &MBI, sizeof(MBI)) == sizeof(MBI)) - { - movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize); - if(movingStart % PageSize != 0) - movingStart = (movingStart / PageSize + 1) * PageSize; - - // Skip unallocated address space - if (MBI.State & MEM_FREE) - continue; - - // Find range and permissions - t_memrange temp; - memset(&temp, 0, sizeof(temp)); - - temp.start = (char *) MBI.BaseAddress; - temp.end = ((char *)MBI.BaseAddress + (uint64_t)MBI.RegionSize); - temp.valid = true; - - if (!(MBI.State & MEM_COMMIT)) - temp.valid = false; // reserved address space - else if (MBI.Protect & PAGE_EXECUTE) - temp.execute = true; - else if (MBI.Protect & PAGE_EXECUTE_READ) - temp.execute = temp.read = true; - else if (MBI.Protect & PAGE_EXECUTE_READWRITE) - temp.execute = temp.read = temp.write = true; - else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY) - temp.execute = temp.read = temp.write = true; - else if (MBI.Protect & PAGE_READONLY) - temp.read = true; - else if (MBI.Protect & PAGE_READWRITE) - temp.read = temp.write = true; - else if (MBI.Protect & PAGE_WRITECOPY) - temp.read = temp.write = true; - - // Merge areas with the same properties - if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase) - { - auto &last = ranges.back(); - - if (last.end == temp.start && - last.valid == temp.valid && last.execute == temp.execute && - last.read == temp.read && last.write == temp.write) - { - last.end = temp.end; - continue; - } - } - -#if 1 - // Find the mapped file name - if (GetMappedFileName(my_handle, temp.start, temp.name, 1024)) - { - int vsize = strlen(temp.name); - - // Translate NT name to DOS name - for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it) - { - int ksize = it->first.size(); - if (strncmp(temp.name, it->first.data(), ksize) != 0) - continue; - - memcpy(temp.name, it->second.data(), it->second.size()); - memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize); - break; - } - } - else - temp.name[0] = 0; -#else - // Find the executable name - char *base = (char*)MBI.AllocationBase; - - if(nameMap.count(base)) - { - strncpy(temp.name, nameMap[base].c_str(), 1023); - } - else if(GetModuleBaseName(d->my_handle, (HMODULE)base, temp.name, 1024)) - { - std::string nm(temp.name); - - nameMap[base] = nm; - - // this is our executable! (could be generalized to pull segments from libs, but whatever) - if(d->base == base) - { - for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) - { - /*char sectionName[9]; - memcpy(sectionName,d->sections[i].Name,8); - sectionName[8] = 0; - string nm; - nm.append(temp.name); - nm.append(" : "); - nm.append(sectionName);*/ - nameMap[base + d->sections[i].VirtualAddress] = nm; - } - } - } - else - temp.name[0] = 0; -#endif - - // Push the entry - LastAllocationBase = MBI.AllocationBase; - ranges.push_back(temp); - } -} - -uintptr_t Process::getBase() -{ - if(d) - return (uintptr_t) d->base; - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool to_file) -{ - if (!d) - return -1; - - for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) - { - auto §ion = d->sections[i]; - - if (to_file) - { - unsigned delta = offset - section.VirtualAddress; - if (delta >= section.Misc.VirtualSize) - continue; - if (!section.PointerToRawData || delta >= section.SizeOfRawData) - return -1; - return (int)(section.PointerToRawData + delta); - } - else - { - unsigned delta = offset - section.PointerToRawData; - if (!section.PointerToRawData || delta >= section.SizeOfRawData) - continue; - if (delta >= section.Misc.VirtualSize) - return -1; - return (int)(section.VirtualAddress + delta); - } - } - - return -1; -} - -uint32_t Process::getTickCount() -{ - return GetTickCount(); -} - -std::filesystem::path Process::getPath() -{ - return Filesystem::get_initial_cwd(); -} - -int Process::getPID() -{ - return (int) GetCurrentProcessId(); -} - - -bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) -{ - DWORD newprotect=0; - if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; - if(trgrange.read && trgrange.write && !trgrange.execute)newprotect=PAGE_READWRITE; - if(!trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE; - if(trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READ; - if(trgrange.read && trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READWRITE; - DWORD oldprotect=0; - bool result; - result=VirtualProtect((LPVOID)range.start,(char *)range.end-(char *)range.start,newprotect,&oldprotect); - - return result; -} - -bool Process::flushCache(const void* target, size_t count) -{ - return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); -} - -void* Process::memAlloc(const int length) -{ - void *ret; - // returns 0 on error - ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); - if (!ret) - ret = (void*)-1; - return ret; -} - -int Process::memDealloc(void *ptr, const int length) -{ - // can only free the whole region at once - // vfree returns 0 on error - return !VirtualFree(ptr, 0, MEM_RELEASE); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - DWORD old_prot = 0; - - // only support a few constant combinations - if (prot == 0) - prot_native = PAGE_NOACCESS; - else if (prot == Process::MemProt::READ) - prot_native = PAGE_READONLY; - else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE)) - prot_native = PAGE_READWRITE; - else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC)) - prot_native = PAGE_EXECUTE_READWRITE; - else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC)) - prot_native = PAGE_EXECUTE_READ; - else - return -1; - - return !VirtualProtect(ptr, length, prot_native, &old_prot); -} diff --git a/library/Process.cpp b/library/Process.cpp new file mode 100644 index 00000000000..cc31b0a056e --- /dev/null +++ b/library/Process.cpp @@ -0,0 +1,801 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. +*/ + +#ifndef WIN32 +#ifndef _DARWIN +#endif /* ! WIN32 */ +#include +#ifndef WIN32 +#endif /* ! _DARWIN */ +#endif /* ! WIN32 */ +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#include +#include +#include +#include +#ifdef _DARWIN +#include + +#include +#include +#include +#include +#include +#include + +#endif /* _DARWIN */ + +#endif /* ! WIN32 */ +#include "Error.h" +#include "Internal.h" +#include "MemAccess.h" +#include "Memory.h" +#include "MiscUtils.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" +#include "modules/Filesystem.h" +#ifndef WIN32 +#include "md5wrapper.h" +#else /* WIN32 */ + +#include + +#define _WIN32_WINNT 0x0600 +#define WINVER 0x0600 +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif /* WIN32 */ + +using namespace DFHack; + +using std::string; +using std::map; +using std::vector; +using std::endl; +using std::cerr; + +#ifdef WIN32 +namespace DFHack +{ + class PlatformSpecific + { + public: + PlatformSpecific() + { + base = 0; + sections = 0; + }; + HANDLE my_handle; + uint32_t my_pid; + IMAGE_NT_HEADERS pe_header; + IMAGE_SECTION_HEADER * sections; + char * base; + }; +} + +#endif /* WIN32 */ +Process::Process(const VersionInfoFactory& known_versions) : identified(false) +{ +#ifndef WIN32 + std::string self_exe_name; +#ifndef _DARWIN + const char * exe_link_name = "/proc/self/exe"; + + // valgrind replaces readlink for /proc/self/exe, but not open. + char self_exe[1024]; + memset(self_exe, 0, sizeof(self_exe)); + if (readlink(exe_link_name, self_exe, sizeof(self_exe) - 1) < 0) + self_exe_name = exe_link_name; + else + self_exe_name = self_exe; +#else /* _DARWIN */ + char path[1024]; + char *exec_link_name; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + exec_link_name = realpath(path, NULL); + self_exe_name = std::string(exec_link_name); +#endif /* _DARWIN */ + } +#else /* WIN32 */ + HMODULE hmod = NULL; + DWORD needed; + bool found = false; + + d = new PlatformSpecific(); + // open process + d->my_pid = GetCurrentProcessId(); + d->my_handle = GetCurrentProcess(); + // try getting the first module of the process + if (EnumProcessModules(d->my_handle, &hmod, sizeof(hmod), &needed) == 0) + { + return; //if enumprocessModules fails, give up + } + // got base ;) + d->base = (char*)hmod; + + // read from this process + try + { + uint32_t pe_offset = readDWord(d->base + 0x3C); + read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t*)&(d->pe_header)); + const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; + d->sections = (IMAGE_SECTION_HEADER*)malloc(sectionsSize); + read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t*)(d->sections)); + } + catch (std::exception&) + { + return; + } +#endif /* WIN32 */ + +#ifndef WIN32 + my_pe = 0; + md5wrapper md5; + uint32_t length; + uint8_t first_kb [1024]; + memset(first_kb, 0, sizeof(first_kb)); + // get hash of the running DF process + my_md5 = md5.getHashFromFile(self_exe_name, length, (char *) first_kb); + // create linux process, add it to the vector + auto vinfo = known_versions.getVersionInfoByMD5(my_md5); +#else /* WIN32 */ + my_pe = d->pe_header.FileHeader.TimeDateStamp; + auto vinfo = known_versions.getVersionInfoByPETimestamp(my_pe); +#endif /* WIN32 */ + if(vinfo) + { + identified = true; + my_descriptor = std::make_shared(*vinfo); +#ifdef WIN32 + // give the process a data model and memory layout fixed for the base of first module + my_descriptor->rebaseTo(getBase()); +#endif /* WIN32 */ + } + else + { +#ifndef WIN32 + char * wd = getcwd(NULL, 0); +#endif /* ! WIN32 */ + cerr << "Unable to retrieve version information.\n"; +#ifndef WIN32 + cerr << "File: " << exe_link_name << endl; + cerr << "MD5: " << my_md5 << endl; + cerr << "working dir: " << wd << endl; + cerr << "length:" << length << endl; + cerr << "1KB hexdump follows:" << endl; + for(int i = 0; i < 64; i++) + { + fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + first_kb[i*16], + first_kb[i*16+1], + first_kb[i*16+2], + first_kb[i*16+3], + first_kb[i*16+4], + first_kb[i*16+5], + first_kb[i*16+6], + first_kb[i*16+7], + first_kb[i*16+8], + first_kb[i*16+9], + first_kb[i*16+10], + first_kb[i*16+11], + first_kb[i*16+12], + first_kb[i*16+13], + first_kb[i*16+14], + first_kb[i*16+15] + ); + } + free(wd); +#else /* WIN32 */ + cerr << "PE timestamp: " << std::format("{:#0x}", my_pe) << endl; +#endif /* WIN32 */ + } +} + +Process::~Process() +{ +#ifndef WIN32 + // Nothing to do here +#else /* WIN32 */ + // destroy our rebased copy of the memory descriptor + if(d->sections != NULL) + free(d->sections); +#endif /* WIN32 */ +} + +string Process::doReadClassName (void * vptr) +{ + char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); +#ifndef WIN32 + char* typestring = Process::readPtr(rtti + sizeof(void*)); +#else /* WIN32 */ +#ifdef DFHACK64 + void* base; + if (!RtlPcToFileHeader(rtti, &base)) + return "dummy"; + char* typeinfo = (char *)base + readDWord(rtti + 0xC); + char* typestring = typeinfo + 0x10; +#else + char* typeinfo = readPtr(rtti + 0xC); + char* typestring = typeinfo + 0x8; +#endif +#endif /* WIN32 */ + std::string raw = readCString(typestring); + if (raw.length() == 0) + return "dummy"; + + string status; + string demangled = cxx_demangle(raw, &status); + + if (demangled.length() == 0) { + return "dummy"; + } + + return demangled; +} + +#ifndef WIN32 +#ifndef _DARWIN +//FIXME: cross-reference with ELF segment entries? +#else /* _DARWIN */ +const char* +inheritance_strings[] = { + "SHARE", "COPY", "NONE", "DONATE_COPY", +}; + +const char* +behavior_strings[] = { + "DEFAULT", "RANDOM", "SEQUENTIAL", "RESQNTL", "WILLNEED", "DONTNEED", +}; + +#endif /* _DARWIN */ +void Process::getMemRanges(vector& ranges) +{ +#ifndef _DARWIN + char buffer[1024]; + char permissions[5]; // r/-, w/-, x/-, p/s, 0 +#else /* _DARWIN */ + static bool log_ranges = (getenv("DFHACK_LOG_MEM_RANGES") != NULL); + + kern_return_t kr; + task_t the_task; +#endif /* _DARWIN */ + +#ifndef _DARWIN + FILE* mapFile = ::fopen("/proc/self/maps", "r"); + if (!mapFile) + return; +#else /* _DARWIN */ + the_task = mach_task_self(); +#endif /* _DARWIN */ + +#ifndef _DARWIN + size_t start, end, offset, device1, device2, node; +#else /* _DARWIN */ +#ifdef DFHACK64 + mach_vm_size_t vmsize; + mach_vm_address_t address; + vm_region_basic_info_data_64_t info; +#else + vm_size_t vmsize; + vm_address_t address; + vm_region_basic_info_data_t info; +#endif + mach_msg_type_number_t info_count; + vm_region_flavor_t flavor; + memory_object_name_t object; + + kr = KERN_SUCCESS; + address = 0; +#endif /* _DARWIN */ + + string cur_name; + void* cur_base = nullptr; + +#ifndef _DARWIN + while (fgets(buffer, 1024, mapFile)) + { +#else /* _DARWIN */ + do { +#ifdef DFHACK64 + flavor = VM_REGION_BASIC_INFO_64; + info_count = VM_REGION_BASIC_INFO_COUNT_64; + kr = mach_vm_region(the_task, &address, &vmsize, flavor, + (vm_region_info_64_t)&info, &info_count, &object); +#else + flavor = VM_REGION_BASIC_INFO; + info_count = VM_REGION_BASIC_INFO_COUNT; + kr = vm_region(the_task, &address, &vmsize, flavor, + (vm_region_info_t)&info, &info_count, &object); +#endif + + if (kr == KERN_INVALID_ADDRESS) + break; + if (kr != KERN_SUCCESS) + { + + /*if (the_task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), the_task); + }*/ + return; + } + if (info.reserved == 1) { + address += vmsize; + continue; + } + Dl_info dlinfo; + int dlcheck; + dlcheck = dladdr((const void*)address, &dlinfo); + if (dlcheck == 0) { + dlinfo.dli_fname = ""; + } + +#endif /* _DARWIN */ + t_memrange temp; +#ifndef _DARWIN + temp.name[0] = 0; + sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", + &start, + &end, + (char*)&permissions, + &offset, &device1, &device2, &node, + (char*)temp.name); +#else /* _DARWIN */ + strncpy(temp.name, dlinfo.dli_fname, 1023); + temp.name[1023] = 0; +#endif /* _DARWIN */ + if (cur_name != temp.name) { + cur_name = temp.name; +#ifndef _DARWIN + cur_base = (void*)start; +#else /* _DARWIN */ + cur_base = (void*)address; +#endif /* _DARWIN */ + } + temp.base = cur_base; +#ifndef _DARWIN + temp.start = (void*)start; + temp.end = (void*)end; + temp.read = permissions[0] == 'r'; + temp.write = permissions[1] == 'w'; + temp.execute = permissions[2] == 'x'; + temp.shared = permissions[3] == 's'; +#else /* _DARWIN */ + temp.start = (void*)address; + temp.end = (void*)(address + vmsize); + temp.read = (info.protection & VM_PROT_READ); + temp.write = (info.protection & VM_PROT_WRITE); + temp.execute = (info.protection & VM_PROT_EXECUTE); + temp.shared = info.shared; +#endif /* _DARWIN */ + temp.valid = true; + ranges.push_back(temp); +#ifndef _DARWIN + } +#endif /* ! _DARWIN */ + +#ifndef _DARWIN + fclose(mapFile); +#else /* _DARWIN */ + if (log_ranges) + { + fprintf(stderr, + "%p-%p %8zuK %c%c%c/%c%c%c %11s %6s %10s uwir=%hu sub=%u dlname: %s\n", + (void*)address, + (void*)(address + vmsize), + size_t(vmsize >> 10), + (info.protection & VM_PROT_READ) ? 'r' : '-', + (info.protection & VM_PROT_WRITE) ? 'w' : '-', + (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', + (info.max_protection & VM_PROT_READ) ? 'r' : '-', + (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', + (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-', + inheritance_strings[info.inheritance], + (info.shared) ? "shared" : "-", + behavior_strings[info.behavior], + info.user_wired_count, + info.reserved, + dlinfo.dli_fname); + } + + address += vmsize; + } while (kr != KERN_INVALID_ADDRESS); + + +/* if (the_task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), the_task); + }*/ +#endif /* _DARWIN */ +} +#else /* WIN32 */ +/* +typedef struct _MEMORY_BASIC_INFORMATION +{ + void * BaseAddress; + void * AllocationBase; + uint32_t AllocationProtect; + size_t RegionSize; + uint32_t State; + uint32_t Protect; + uint32_t Type; +} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; +*/ +/* +//Internal structure used to store heap block information. +struct HeapBlock +{ + PVOID dwAddress; + DWORD dwSize; + DWORD dwFlags; + ULONG reserved; +}; +*/ + +static void GetDosNames(std::map& table) +{ + // Partially based on example from msdn: + // Translate path with device name to drive letters. + TCHAR szTemp[512]; + szTemp[0] = '\0'; + + if (GetLogicalDriveStrings(sizeof(szTemp) - 1, szTemp)) + { + TCHAR szName[MAX_PATH]; + TCHAR szDrive[3] = " :"; + BOOL bFound = FALSE; + TCHAR* p = szTemp; + + do + { + // Copy the drive letter to the template string + *szDrive = *p; + + // Look up each device name + if (QueryDosDevice(szDrive, szName, MAX_PATH)) + table[szName] = szDrive; + + // Go to the next NULL character. + while (*p++); + } while (*p); // end of string + } +} + +void Process::getMemRanges(vector& ranges) +{ + MEMORY_BASIC_INFORMATION MBI; + //map heaps; + uint64_t movingStart = 0; + PVOID LastAllocationBase = 0; + map nameMap; + map dosDrives; + + // get page size + SYSTEM_INFO si; + GetSystemInfo(&si); + uint64_t PageSize = si.dwPageSize; + + // get dos drive names + GetDosNames(dosDrives); + + ranges.clear(); + + HANDLE my_handle = GetCurrentProcess(); + + // enumerate heaps + // HeapNodes(d->my_pid, heaps); + // go through all the VM regions, convert them to our internal format + while (VirtualQueryEx(my_handle, (const void*)(movingStart), &MBI, sizeof(MBI)) == sizeof(MBI)) + { + t_memrange temp; + movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize); + if (movingStart % PageSize != 0) + movingStart = (movingStart / PageSize + 1) * PageSize; + + // Skip unallocated address space + if (MBI.State & MEM_FREE) + continue; + + // Find range and permissions + memset(&temp, 0, sizeof(temp)); + + temp.start = (char*)MBI.BaseAddress; + temp.end = ((char*)MBI.BaseAddress + (uint64_t)MBI.RegionSize); + temp.valid = true; + + if (!(MBI.State & MEM_COMMIT)) + temp.valid = false; // reserved address space + else if (MBI.Protect & PAGE_EXECUTE) + temp.execute = true; + else if (MBI.Protect & PAGE_EXECUTE_READ) + temp.execute = temp.read = true; + else if (MBI.Protect & PAGE_EXECUTE_READWRITE) + temp.execute = temp.read = temp.write = true; + else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY) + temp.execute = temp.read = temp.write = true; + else if (MBI.Protect & PAGE_READONLY) + temp.read = true; + else if (MBI.Protect & PAGE_READWRITE) + temp.read = temp.write = true; + else if (MBI.Protect & PAGE_WRITECOPY) + temp.read = temp.write = true; + + // Merge areas with the same properties + if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase) + { + auto& last = ranges.back(); + + if (last.end == temp.start && + last.valid == temp.valid && last.execute == temp.execute && + last.read == temp.read && last.write == temp.write) + { + last.end = temp.end; + continue; + } + } + + // Find the mapped file name + if (GetMappedFileName(my_handle, temp.start, temp.name, 1024)) + { + int vsize = strlen(temp.name); + + // Translate NT name to DOS name + for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it) + { + int ksize = it->first.size(); + if (strncmp(temp.name, it->first.data(), ksize) != 0) + continue; + + memcpy(temp.name, it->second.data(), it->second.size()); + memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize); + break; + } + } + else + temp.name[0] = 0; + + // Push the entry + LastAllocationBase = MBI.AllocationBase; + ranges.push_back(temp); + } +} +#endif + +uintptr_t Process::getBase() +{ +#if WIN32 + if(d) + return (uintptr_t) d->base; +#endif /* WIN32 */ + return DEFAULT_BASE_ADDR; // Memory.h +} + +int Process::adjustOffset(int offset, [[maybe_unused]] bool to_file) +{ +#ifndef WIN32 + return offset; +#else /* WIN32 */ + if (!d) + return -1; + + for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) + { + auto §ion = d->sections[i]; + + if (to_file) + { + unsigned delta = offset - section.VirtualAddress; + if (delta >= section.Misc.VirtualSize) + continue; + if (!section.PointerToRawData || delta >= section.SizeOfRawData) + return -1; + return (int)(section.PointerToRawData + delta); + } + else + { + unsigned delta = offset - section.PointerToRawData; + if (!section.PointerToRawData || delta >= section.SizeOfRawData) + continue; + if (delta >= section.Misc.VirtualSize) + return -1; + return (int)(section.VirtualAddress + delta); + } + } + + return -1; +#endif /* WIN32 */ +} + +uint32_t Process::getTickCount() +{ +#ifndef WIN32 + struct timeval tp; + gettimeofday(&tp, NULL); + return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); +#else /* WIN32 */ + return GetTickCount(); +#endif /* WIN32 */ +} + +std::filesystem::path Process::getPath() +{ +#if defined(WIN32) || !defined(_DARWIN) + return Filesystem::get_initial_cwd(); +#else /* _DARWIN */ + static string cached_path = ""; + if (cached_path.size()) + return cached_path; + char path[1024]; + char *real_path; + uint32_t size = sizeof(path); + if (getcwd(path, size)) + { + cached_path = string(path); + return cached_path; + } + if (_NSGetExecutablePath(path, &size) == 0) { + real_path = realpath(path, NULL); + } + else { + fprintf(stderr, "_NSGetExecutablePath failed!\n"); + cached_path = "."; + return cached_path; + } + std::string path_string(real_path); + int last_slash = path_string.find_last_of("/"); + cached_path = path_string.substr(0,last_slash); + return cached_path; +#endif /* _DARWIN */ +} + +int Process::getPID() +{ +#ifndef WIN32 + return getpid(); +#else /* WIN32 */ + return (int) GetCurrentProcessId(); +#endif /* WIN32 */ +} + +#ifdef WIN32 + +#endif /* WIN32 */ +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) +{ +#ifndef WIN32 + int result; + int protect=0; + if(trgrange.read)protect|=PROT_READ; + if(trgrange.write)protect|=PROT_WRITE; + if(trgrange.execute)protect|=PROT_EXEC; + result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); +#else /* WIN32 */ + DWORD newprotect=0; + if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; + if(trgrange.read && trgrange.write && !trgrange.execute)newprotect=PAGE_READWRITE; + if(!trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE; + if(trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READ; + if(trgrange.read && trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READWRITE; + DWORD oldprotect=0; + bool result; + result=VirtualProtect((LPVOID)range.start,(char *)range.end-(char *)range.start,newprotect,&oldprotect); +#endif /* WIN32 */ + +#ifndef WIN32 + return result==0; +#else /* WIN32 */ + return result; +#endif /* WIN32 */ +} + +bool Process::flushCache(const void* target, size_t count) +{ +#ifndef WIN32 +#ifndef _DARWIN + __builtin___clear_cache((char*)target, (char*)target + count - 1); + return true; /* assume always succeeds, as the builtin has no return type */ +#else /* _DARWIN */ + // FIXME: implement cache flush for MacOS (???) + return false; +#endif /* _DARWIN */ +#else /* WIN32 */ + return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); +#endif /* WIN32 */ +} + +#ifndef WIN32 +// returns -1 on error +#endif /* ! WIN32 */ +void* Process::memAlloc(const int length) +{ +#ifndef WIN32 +#ifndef _DARWIN + return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); +#else /* _DARWIN */ + return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); +#endif /* _DARWIN */ +#else /* WIN32 */ + void *ret; + // returns 0 on error + ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + if (!ret) + ret = (void*)-1; + return ret; +#endif /* WIN32 */ +} + +int Process::memDealloc(void *ptr, const int length) +{ +#ifndef WIN32 + return munmap(ptr, length); +#else /* WIN32 */ + // can only free the whole region at once + // vfree returns 0 on error + return !VirtualFree(ptr, 0, MEM_RELEASE); +#endif /* WIN32 */ +} + +int Process::memProtect(void *ptr, const int length, const int prot) +{ + int prot_native = 0; + +#ifndef WIN32 + if (prot & Process::MemProt::READ) + prot_native |= PROT_READ; + if (prot & Process::MemProt::WRITE) + prot_native |= PROT_WRITE; + if (prot & Process::MemProt::EXEC) + prot_native |= PROT_EXEC; +#else /* WIN32 */ + // only support a few constant combinations + if (prot == 0) + prot_native = PAGE_NOACCESS; + else if (prot == Process::MemProt::READ) + prot_native = PAGE_READONLY; + else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE)) + prot_native = PAGE_READWRITE; + else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC)) + prot_native = PAGE_EXECUTE_READWRITE; + else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC)) + prot_native = PAGE_EXECUTE_READ; + else + return -1; +#endif /* WIN32 */ + +#ifndef WIN32 + return mprotect(ptr, length, prot_native); +#else /* WIN32 */ + DWORD old_prot = 0; + return !VirtualProtect(ptr, length, prot_native, &old_prot); +#endif /* WIN32 */ +} From 21ccb6acd71164f3e0b2529c82560cb8aca7a4cd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 14:40:20 -0500 Subject: [PATCH 329/919] off by just a smidgen! --- library/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Process.cpp b/library/Process.cpp index cc31b0a056e..7a6413c5dd1 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -126,8 +126,8 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) if (_NSGetExecutablePath(path, &size) == 0) { exec_link_name = realpath(path, NULL); self_exe_name = std::string(exec_link_name); -#endif /* _DARWIN */ } +#endif /* _DARWIN */ #else /* WIN32 */ HMODULE hmod = NULL; DWORD needed; From 09fa4a64288ff0323c6657fae11c0481cb3026ee Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 15:38:09 -0500 Subject: [PATCH 330/919] rejuggle `#ifdef` --- library/Process.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/library/Process.cpp b/library/Process.cpp index 7a6413c5dd1..101eecbc710 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -24,9 +24,7 @@ distribution. #ifndef WIN32 #ifndef _DARWIN -#endif /* ! WIN32 */ #include -#ifndef WIN32 #endif /* ! _DARWIN */ #endif /* ! WIN32 */ #include @@ -43,19 +41,18 @@ distribution. #include #include #include + #ifdef _DARWIN #include - #include #include #include #include #include #include - #endif /* _DARWIN */ - #endif /* ! WIN32 */ + #include "Error.h" #include "Internal.h" #include "MemAccess.h" @@ -64,17 +61,17 @@ distribution. #include "VersionInfo.h" #include "VersionInfoFactory.h" #include "modules/Filesystem.h" + #ifndef WIN32 #include "md5wrapper.h" #else /* WIN32 */ - -#include - #define _WIN32_WINNT 0x0600 #define WINVER 0x0600 #define WIN32_LEAN_AND_MEAN #include #include + +#include #endif /* WIN32 */ using namespace DFHack; From 269eef80e31b897376846171da5f845a602ce639 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 3 May 2025 17:29:37 -0500 Subject: [PATCH 331/919] Bring back zones in `gui/blueprint` (WIP) (#5222) * Restore zones to blueprints * Begin refactor of get_zone_keys * more refactor * Make zones almost completely work * Update the zone keys * Update blueprint.cpp --- docs/plugins/blueprint.rst | 12 -- plugins/blueprint.cpp | 219 +++++++++++-------------------------- plugins/lua/blueprint.lua | 7 +- 3 files changed, 66 insertions(+), 172 deletions(-) diff --git a/docs/plugins/blueprint.rst b/docs/plugins/blueprint.rst index 1a5071b6be1..2d84a7ca69c 100644 --- a/docs/plugins/blueprint.rst +++ b/docs/plugins/blueprint.rst @@ -15,13 +15,6 @@ selected interactively with the `gui/blueprint` command or, if the GUI is not used, starts at the active cursor location and extends right and down for the requested width and height. -.. admonition:: Note - - blueprint is still in the process of being updated for the new version of - DF. Stockpiles (the "place" phase), zones (the "zone" phase), building - configuration (the "query" phase), and game configuration (the "config" - phase) are not yet supported. - Usage ----- @@ -83,11 +76,6 @@ phases; just separate them with a space. Generate quickfort ``#place`` blueprints for placing stockpiles. ``zone`` Generate quickfort ``#zone`` blueprints for designating zones. -``query`` - Generate quickfort ``#query`` blueprints for configuring stockpiles and - naming buildings. -``rooms`` - Generate quickfort ``#query`` blueprints for defining rooms. If no phases are specified, phases are autodetected. For example, a ``#place`` blueprint will be created only if there are stockpiles in the blueprint area. diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index b82442a66da..d9bff5f329d 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -106,9 +106,8 @@ struct blueprint_options { bool construct = false; bool build = false; bool place = false; - // bool zone = false; - // bool query = false; - // bool rooms = false; + bool zone = false; + static struct_identity _identity; }; @@ -132,9 +131,7 @@ static const struct_field_info blueprint_options_fields[] = { { struct_field_info::PRIMITIVE, "construct", offsetof(blueprint_options, construct), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "zone", offsetof(blueprint_options, zone), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "rooms", offsetof(blueprint_options, rooms), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "zone", offsetof(blueprint_options, zone), &df::identity_traits::identity, 0, 0 }, { struct_field_info::END } }; struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::allocator_fn, NULL, "blueprint_options", NULL, blueprint_options_fields); @@ -1107,78 +1104,70 @@ static const char * get_tile_place(const df::coord &pos, return add_expansion_syntax(ctx, get_place_keys(ctx)); } -/* TODO: understand how this changes for v50 -static bool hospital_maximums_eq(const df::hospital_supplies &a, - const df::hospital_supplies &b) { - return a.max_thread == b.max_thread && - a.max_cloth == b.max_cloth && - a.max_splints == b.max_splints && - a.max_crutches == b.max_crutches && - a.max_plaster == b.max_plaster && - a.max_buckets == b.max_buckets && - a.max_soap == b.max_soap; -} - static const char * get_zone_keys(const df::building_civzonest *zone) { static const uint32_t DEFAULT_GATHER_FLAGS = - df::building_civzonest::T_gather_flags::mask_pick_trees | - df::building_civzonest::T_gather_flags::mask_pick_shrubs | - df::building_civzonest::T_gather_flags::mask_gather_fallen; - static const df::hospital_supplies DEFAULT_HOSPITAL; + df::civzone_gather_flag::mask_pick_trees | + df::civzone_gather_flag::mask_pick_shrubs | + df::civzone_gather_flag::mask_gather_fallen; ostringstream keys; - const df::building_civzonest::T_zone_flags &flags = zone->zone_flags; - - // inverted logic for Active since it's on by default - if (!flags.bits.active) keys << 'a'; - - // in UI order - if (flags.bits.water_source) keys << 'w'; - if (flags.bits.fishing) keys << 'f'; - if (flags.bits.gather) { - keys << 'g'; - if (zone->gather_flags.whole != DEFAULT_GATHER_FLAGS) { - keys << 'G'; - // logic is inverted since they're all on by default - if (!zone->gather_flags.bits.pick_trees) keys << 't'; - if (!zone->gather_flags.bits.pick_shrubs) keys << 's'; - if (!zone->gather_flags.bits.gather_fallen) keys << 'f'; - keys << '^'; - } + const df::civzone_type type = zone->type; + + // in DFHack docs order + if (type == df::civzone_type::MeetingHall) keys << 'm'; + if (type == df::civzone_type::Bedroom) keys << 'b'; + if (type == df::civzone_type::DiningHall) keys << 'h'; + if (type == df::civzone_type::Pen) keys << 'n'; + if (type == df::civzone_type::Pond) keys << 'p'; + if (type == df::civzone_type::WaterSource) keys << 'w'; + if (type == df::civzone_type::Dungeon) keys << 'j'; + if (type == df::civzone_type::FishingArea) keys << 'f'; + if (type == df::civzone_type::SandCollection) keys << 's'; + if (type == df::civzone_type::Office) keys << 'o'; + if (type == df::civzone_type::Dormitory) keys << 'D'; + if (type == df::civzone_type::Barracks) keys << 'B'; + if (type == df::civzone_type::ArcheryRange) keys << 'a'; + if (type == df::civzone_type::Dump) keys << 'd'; + if (type == df::civzone_type::AnimalTraining) keys << 't'; + if (type == df::civzone_type::Tomb) keys << 'T'; + if (type == df::civzone_type::PlantGathering) keys << 'g'; + if (type == df::civzone_type::ClayCollection) keys << 'c'; + + keys << '{'; + if (!zone->name.empty()) { + keys << "name=" << zone->name << ' '; } - if (flags.bits.garbage_dump) keys << 'd'; - if (flags.bits.pen_pasture) keys << 'n'; - if (flags.bits.pit_pond) { - keys << 'p'; - if (zone->pit_flags.bits.is_pond) - keys << "Pf^"; + if (!zone->spec_sub_flag.bits.active) { + keys << "active=false "; } - if (flags.bits.sand) keys << 's'; - if (flags.bits.clay) keys << 'c'; - if (flags.bits.meeting_area) keys << 'm'; - if (flags.bits.hospital) { - keys << 'h'; - const df::hospital_supplies &hospital = zone->hospital; - if (!hospital_maximums_eq(hospital, DEFAULT_HOSPITAL)) { - keys << "H{hospital"; - if (hospital.max_thread != DEFAULT_HOSPITAL.max_thread) - keys << " thread=" << hospital.max_thread; - if (hospital.max_cloth != DEFAULT_HOSPITAL.max_cloth) - keys << " cloth=" << hospital.max_cloth; - if (hospital.max_splints != DEFAULT_HOSPITAL.max_splints) - keys << " splints=" << hospital.max_splints; - if (hospital.max_crutches != DEFAULT_HOSPITAL.max_crutches) - keys << " crutches=" << hospital.max_crutches; - if (hospital.max_plaster != DEFAULT_HOSPITAL.max_plaster) - keys << " plaster=" << hospital.max_plaster; - if (hospital.max_buckets != DEFAULT_HOSPITAL.max_buckets) - keys << " buckets=" << hospital.max_buckets; - if (hospital.max_soap != DEFAULT_HOSPITAL.max_soap) - keys << " soap=" << hospital.max_soap; - keys << "}^"; - } + if (zone->assigned_unit) { + keys << "assigned_unit=" << zone->assigned_unit << ' '; + } + if (!zone->zone_settings.pond.flag.bits.keep_filled) { + keys << "pond=false "; + } + auto archery_settings = zone->zone_settings.archery; // need this to get the `shoot_from` direction + if (archery_settings.dir_y == 0) { + if (archery_settings.dir_x == 1) keys << "shoot_from=west "; + if (archery_settings.dir_x == -1) keys << "shoot_from=east "; + } + if (archery_settings.dir_x == 0) { + if (archery_settings.dir_y == 1) keys << "shoot_from=north "; + if (archery_settings.dir_y == -1) keys << "shoot_from=south "; + } + //FIXEME (Squid): Need to know how to get the location data here + if (!zone->zone_settings.tomb.flags.bits.no_pets) { + keys << "pets=true "; + } + if (zone->zone_settings.tomb.flags.bits.no_citizens) { + keys << "citizens=false "; + } + if (zone->zone_settings.gather.flags.whole != DEFAULT_GATHER_FLAGS) { + // logic is inverted since they're all on by default + if (!zone->zone_settings.gather.flags.bits.pick_trees) keys << "pick_trees=false "; + if (!zone->zone_settings.gather.flags.bits.pick_shrubs) keys << "pick_shrubs=false "; + if (!zone->zone_settings.gather.flags.bits.gather_fallen) keys << "gather_fallen=false "; } - if (flags.bits.animal_training) keys << 't'; string keys_str = keys.str(); @@ -1187,8 +1176,11 @@ static const char * get_zone_keys(const df::building_civzonest *zone) { return NULL; // remove final '^' character if there is one - if (keys_str.back() == '^') + if (keys_str.back() == '{') { keys_str.pop_back(); + } else { + keys << '}'; + } return cache(keys_str); } @@ -1215,84 +1207,7 @@ static const char * get_tile_zone(const df::coord &pos, return add_expansion_syntax(zone, get_zone_keys(zone)); } -// surrounds the given string in quotes and replaces internal double quotes (") -// with double double quotes ("") (as per the csv spec) -static string csv_quote(const string &str) { - ostringstream outstr; - outstr << "\""; - - size_t start = 0; - auto end = str.find('"'); - while (end != string::npos) { - outstr << str.substr(start, end - start); - outstr << "\"\""; - start = end + 1; - end = str.find('"', start); - } - outstr << str.substr(start, end) << "\""; - - return outstr.str(); -} - -static const char * get_tile_query(const df::coord &pos, - const tile_context &ctx) { - string bld_name, zone_name; - auto & seen = ctx.processor->seen; - - if (ctx.b && !seen.count(ctx.b)) { - bld_name = ctx.b->name; - seen.emplace(ctx.b); - } - - vector civzones; - if (Buildings::findCivzonesAt(&civzones, pos)) { - auto civzone = civzones.back(); - if (!seen.count(civzone)) { - zone_name = civzone->name; - seen.emplace(civzone); - } - } - - if (!bld_name.size() && !zone_name.size()) - return NULL; - - ostringstream str; - if (bld_name.size()) - str << "{givename name=" + csv_quote(bld_name) + "}"; - if (zone_name.size()) - str << "{namezone name=" + csv_quote(zone_name) + "}"; - - return cache(csv_quote(str.str())); -} - -static const char * get_tile_rooms(const df::coord &, const tile_context &ctx) { - if (!ctx.b || !ctx.b->is_room) - return NULL; - // get the maximum distance from the center of the building - df::building_extents &room = ctx.b->room; - int32_t x1 = room.x; - int32_t x2 = room.x + room.width - 1; - int32_t y1 = room.y; - int32_t y2 = room.y + room.height - 1; - - int32_t dimx = std::max(ctx.b->centerx - x1, x2 - ctx.b->centerx); - int32_t dimy = std::max(ctx.b->centery - y1, y2 - ctx.b->centery); - int32_t max_dim = std::max(dimx, dimy); - - switch (max_dim) { - case 0: return "r---&"; - case 1: return "r--&"; - case 2: return "r-&"; - case 3: return "r&"; - case 4: return "r+&"; - } - - ostringstream str; - str << "r{+ " << (max_dim - 3) << "}&"; - return cache(str); -} -*/ static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { @@ -1503,13 +1418,7 @@ static bool do_transform(color_ostream &out, get_tile_build, ensure_building); add_processor(processors, opts, "place", "place", opts.place, get_tile_place, ensure_building); -/* TODO: understand how this changes for v50 add_processor(processors, opts, "zone", "zone", opts.zone, get_tile_zone); - add_processor(processors, opts, "query", "query", opts.query, - get_tile_query, ensure_building); - add_processor(processors, opts, "query", "rooms", opts.rooms, - get_tile_rooms, ensure_building); -*/ if (processors.empty()) { out.printerr("no phases requested! nothing to do!\n"); return false; diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index dfe8f4eae69..e43ce5f5f68 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -9,17 +9,14 @@ local valid_phase_list = { 'construct', 'build', 'place', - -- 'zone', - -- 'query', - -- 'rooms', + 'zone', } valid_phases = utils.invert(valid_phase_list) local meta_phase_list = { 'build', 'place', - -- 'zone', - -- 'query', + 'zone', } meta_phases = utils.invert(meta_phase_list) From f8af00b7870c0fb1cb7d9d13da9f78aa4a681713 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 15:30:24 -0700 Subject: [PATCH 332/919] record zone information in blueprints can't handle disjoint zones or overlapping non-rectangular zones yet disjoint zone support coming in a follow-up PR need to think about overlapping non-rectangular zones more, though --- plugins/blueprint.cpp | 256 +++++++++++++++++++++---------------- plugins/preserve-rooms.cpp | 15 ++- 2 files changed, 161 insertions(+), 110 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index d9bff5f329d..6ec9b9c3c44 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -21,6 +21,7 @@ #include "modules/Buildings.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Maps.h" #include "modules/World.h" #include "df/building_axle_horizontalst.h" @@ -36,6 +37,7 @@ #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" #include "df/engraving.h" +#include "df/entity_position.h" #include "df/tile_bitmask.h" #include "df/tile_designation.h" #include "df/tile_occupancy.h" @@ -161,8 +163,7 @@ typedef vector bp_row; // index is x coordinate typedef map bp_area; // key is y coordinate typedef map bp_volume; // key is z coordinate -typedef const char * (get_tile_fn)(const df::coord &pos, - const tile_context &ctx); +typedef const char * (get_tile_fn)(color_ostream &out, const df::coord &pos, const tile_context &ctx); typedef void (init_ctx_fn)(const df::coord &pos, tile_context &ctx); struct blueprint_processor { @@ -334,7 +335,7 @@ static const char * get_tile_dig_job(df::tile_designation *td, df::job *job) { } } -static const char * get_tile_dig(const df::coord &pos, const tile_context &) { +static const char * get_tile_dig(color_ostream &out, const df::coord &pos, const tile_context &) { df::tile_designation *td = Maps::getTileDesignation(pos); if (td && td->bits.dig != df::tile_dig_designation::No) return add_markers(pos, get_tile_dig_designation(pos, td->bits.dig)); @@ -373,8 +374,7 @@ static const char * get_tile_dig(const df::coord &pos, const tile_context &) { } } -static const char * get_tile_smooth_minimal(const df::coord &pos, - const tile_context &) { +static const char * get_tile_smooth_minimal(color_ostream &out, const df::coord &pos, const tile_context &) { if (dig_job_cache.contains(pos) && dig_job_cache[pos]->job_type == df::job_type::CarveFortification) return "s"; @@ -398,9 +398,8 @@ static const char * get_tile_smooth_minimal(const df::coord &pos, return NULL; } -static const char * get_tile_smooth_with_engravings(const df::coord &pos, - const tile_context &tc) { - const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); +static const char * get_tile_smooth_with_engravings(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * smooth_minimal = get_tile_smooth_minimal(out, pos, tc); if (smooth_minimal) return smooth_minimal; @@ -431,9 +430,8 @@ static const char * get_tile_smooth_with_engravings(const df::coord &pos, return NULL; } -static const char * get_tile_smooth_all(const df::coord &pos, - const tile_context &tc) { - const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); +static const char * get_tile_smooth_all(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * smooth_minimal = get_tile_smooth_minimal(out, pos, tc); if (smooth_minimal) return smooth_minimal; @@ -481,8 +479,7 @@ static const char * get_track_str(const char *prefix, df::tiletype tt, df::tile_ return cache(prefix + dir); } -static const char * get_tile_carve_minimal(const df::coord &pos, - const tile_context &) { +static const char * get_tile_carve_minimal(color_ostream &out, const df::coord &pos, const tile_context &) { df::tiletype *tt = Maps::getTileType(pos); if (!tt) return NULL; @@ -533,8 +530,8 @@ static const char * get_tile_carve_minimal(const df::coord &pos, return NULL; } -static const char * get_tile_carve(const df::coord &pos, const tile_context &tc) { - const char * tile_carve_minimal = get_tile_carve_minimal(pos, tc); +static const char * get_tile_carve(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * tile_carve_minimal = get_tile_carve_minimal(out, pos, tc); if (tile_carve_minimal) return tile_carve_minimal; @@ -643,8 +640,7 @@ static const char * get_constructed_ramp_str(df::tiletype *tt) { return get_constructed_track_str(tt, "trackramp"); } -static const char * get_tile_construct(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_construct(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (ctx.b && ctx.b->getType() == building_type::Construction) return get_construction_str(ctx.b); @@ -1042,8 +1038,7 @@ static const char * add_label(const tile_context &ctx, const char *keys) { return cache(s); } -static const char * get_tile_build(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_build(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { return NULL; } @@ -1088,8 +1083,7 @@ static const char * get_place_keys(const tile_context &ctx) { return cache(keys); } -static const char * get_tile_place(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_place(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() != building_type::Stockpile) return NULL; @@ -1104,111 +1098,159 @@ static const char * get_tile_place(const df::coord &pos, return add_expansion_syntax(ctx, get_place_keys(ctx)); } -static const char * get_zone_keys(const df::building_civzonest *zone) { - static const uint32_t DEFAULT_GATHER_FLAGS = - df::civzone_gather_flag::mask_pick_trees | - df::civzone_gather_flag::mask_pick_shrubs | - df::civzone_gather_flag::mask_gather_fallen; +static string get_reservation(color_ostream &out, df::building_civzonest *zone) { + string res; + Lua::CallLuaModuleFunction(out, "plugins.preserve-rooms", "preserve_rooms_getRoleAssignmentForZone", + std::make_tuple(zone), 1, [&](lua_State *L) { + if (lua_isstring(L, -1)) + res = lua_tostring(L, -1); + }); + return res; +} - ostringstream keys; - const df::civzone_type type = zone->type; +// TODO: handle locations +static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_properties) { + const char * symbol = NULL; + vector properties; + + if (!zone->name.empty()) + properties.push_back("name=" + zone->name); + if (!zone->spec_sub_flag.bits.active) + properties.push_back("active=false"); + if (auto reserved_for = get_reservation(out, zone); !reserved_for.empty()) { + properties.push_back("assigned_unit=" + reserved_for); + } // in DFHack docs order - if (type == df::civzone_type::MeetingHall) keys << 'm'; - if (type == df::civzone_type::Bedroom) keys << 'b'; - if (type == df::civzone_type::DiningHall) keys << 'h'; - if (type == df::civzone_type::Pen) keys << 'n'; - if (type == df::civzone_type::Pond) keys << 'p'; - if (type == df::civzone_type::WaterSource) keys << 'w'; - if (type == df::civzone_type::Dungeon) keys << 'j'; - if (type == df::civzone_type::FishingArea) keys << 'f'; - if (type == df::civzone_type::SandCollection) keys << 's'; - if (type == df::civzone_type::Office) keys << 'o'; - if (type == df::civzone_type::Dormitory) keys << 'D'; - if (type == df::civzone_type::Barracks) keys << 'B'; - if (type == df::civzone_type::ArcheryRange) keys << 'a'; - if (type == df::civzone_type::Dump) keys << 'd'; - if (type == df::civzone_type::AnimalTraining) keys << 't'; - if (type == df::civzone_type::Tomb) keys << 'T'; - if (type == df::civzone_type::PlantGathering) keys << 'g'; - if (type == df::civzone_type::ClayCollection) keys << 'c'; - - keys << '{'; - if (!zone->name.empty()) { - keys << "name=" << zone->name << ' '; - } - if (!zone->spec_sub_flag.bits.active) { - keys << "active=false "; - } - if (zone->assigned_unit) { - keys << "assigned_unit=" << zone->assigned_unit << ' '; - } - if (!zone->zone_settings.pond.flag.bits.keep_filled) { - keys << "pond=false "; - } - auto archery_settings = zone->zone_settings.archery; // need this to get the `shoot_from` direction - if (archery_settings.dir_y == 0) { - if (archery_settings.dir_x == 1) keys << "shoot_from=west "; - if (archery_settings.dir_x == -1) keys << "shoot_from=east "; - } - if (archery_settings.dir_x == 0) { - if (archery_settings.dir_y == 1) keys << "shoot_from=north "; - if (archery_settings.dir_y == -1) keys << "shoot_from=south "; - } - //FIXEME (Squid): Need to know how to get the location data here - if (!zone->zone_settings.tomb.flags.bits.no_pets) { - keys << "pets=true "; - } - if (zone->zone_settings.tomb.flags.bits.no_citizens) { - keys << "citizens=false "; - } - if (zone->zone_settings.gather.flags.whole != DEFAULT_GATHER_FLAGS) { - // logic is inverted since they're all on by default - if (!zone->zone_settings.gather.flags.bits.pick_trees) keys << "pick_trees=false "; - if (!zone->zone_settings.gather.flags.bits.pick_shrubs) keys << "pick_shrubs=false "; - if (!zone->zone_settings.gather.flags.bits.gather_fallen) keys << "gather_fallen=false "; + switch (zone->type) { + using namespace df::enums::civzone_type; + case MeetingHall: symbol = "m"; break; + case Bedroom: symbol = "b"; break; + case DiningHall: symbol = "h"; break; + case Pen: symbol = "n"; break; + case Pond: + symbol = "p"; + { + if (zone->zone_settings.pond.flag.bits.keep_filled) + properties.push_back("pond=true"); + } + break; + case WaterSource: symbol = "w"; break; + case Dungeon: symbol = "j"; break; + case FishingArea: symbol = "f"; break; + case SandCollection: symbol = "s"; break; + case Office: symbol = "o"; break; + case Dormitory: symbol = "D"; break; + case Barracks: symbol = "B"; break; + case ArcheryRange: + symbol = "a"; + { + auto & archery = zone->zone_settings.archery; + if (archery.dir_x == 1 && archery.dir_y == 0) + properties.push_back("shoot_from=west"); + else if (archery.dir_x == -1 && archery.dir_y == 0) + properties.push_back("shoot_from=east"); + else if (archery.dir_x == 0 && archery.dir_y == 1) + properties.push_back("shoot_from=north"); + else if (archery.dir_x == 0 && archery.dir_y == -1) + properties.push_back("shoot_from=south"); + else + return NULL; // invalid direction + } + break; + case Dump: symbol = "d"; break; + case AnimalTraining: symbol = "t"; break; + case Tomb: + symbol = "T"; + { + auto & tomb = zone->zone_settings.tomb; + if (!tomb.flags.bits.no_pets) + properties.push_back("pets=true"); + if (tomb.flags.bits.no_citizens) + properties.push_back("citizens=false"); + } + break; + case PlantGathering: + symbol = "g"; + { + auto & gather = zone->zone_settings.gather; + if (!gather.flags.bits.pick_trees) + properties.push_back("pick_trees=false"); + if (!gather.flags.bits.pick_shrubs) + properties.push_back("pick_shrubs=false"); + if (!gather.flags.bits.gather_fallen) + properties.push_back("gather_fallen=false"); + } + break; + case ClayCollection: symbol = "c"; break; + default: + return NULL; } - string keys_str = keys.str(); + if (!add_properties || properties.empty()) + return symbol; - // there is no way to represent an active, but empty zone in quickfort - if (keys_str.empty()) - return NULL; + ostringstream keys; + keys << symbol << '{' << join_strings(" ", properties) << '}'; + return cache(keys.str()); +} - // remove final '^' character if there is one - if (keys_str.back() == '{') { - keys_str.pop_back(); - } else { - keys << '}'; - } +static df::coord get_first_tile(df::building_civzonest *zone) { + df::coord first_pos; + cuboid zone_area(zone->x1, zone->y1, zone->z, zone->x2, zone->y2, zone->z); + zone_area.forCoord([&](const df::coord &pos) { + if (Buildings::containsTile(zone, pos)) { + first_pos = pos; + return false; + } + return true; + }); - return cache(keys_str); + return first_pos; } -static const char * get_tile_zone(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { vector civzones; if (!Buildings::findCivzonesAt(&civzones, pos)) return NULL; - // we only have one "zone" blueprint, so use the "topmost" zone (that is, - // the one that is highlighted when the cursor is over this tile). - // overlapping zones are outside the scope of this plugin, I think. - df::building_civzonest *zone = civzones.back(); + // we can handle overlapping zones in a single blueprint, but only if one of + // the following is true: + // -- they exactly overlap (even if they aren't rectangular) + // -- no two non-rectangular zones overlap + + // for a first implementation, we will only handle overlapping zones if they + // are rectangular. if this is the upper left corner of a rectangular zone, + // we will output for that zone. otherwise, if this pos is interior to + // all zones, then it doesn't matter which we choose. + + df::building_civzonest * primary_zone = civzones[0]; + df::coord upper_left_corner; + + if (civzones.size() == 1) { + upper_left_corner = get_first_tile(primary_zone); + } else if (civzones.size() > 1) { + for (auto zone : civzones) { + if (!is_rectangular(zone)) + continue; + primary_zone = zone; + upper_left_corner = get_first_tile(zone); + if (pos == upper_left_corner) + break; + } + } + + bool is_first_tile = pos == upper_left_corner; - if (!is_rectangular(zone)) - return get_zone_keys(zone); + if (!is_rectangular(primary_zone)) + return get_zone_keys(out, primary_zone, is_first_tile); - if (zone->x1 != static_cast(pos.x) - || zone->y1 != static_cast(pos.y)) { + if (!is_first_tile) return if_pretty(ctx, "`"); - } - return add_expansion_syntax(zone, get_zone_keys(zone)); + return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, true)); } - - static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { string basename = BLUEPRINT_USER_DIR + opts.name; @@ -1439,7 +1481,7 @@ static bool do_transform(color_ostream &out, ctx.processor = &processor; if (processor.init_ctx) processor.init_ctx(pos, ctx); - const char *tile_str = processor.get_tile(pos, ctx); + const char *tile_str = processor.get_tile(out, pos, ctx); if (tile_str) { // ensure our z-index is in the order we want to write auto area = processor.mapdata.emplace(abs(z - start.z), diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index d5b4b5b7a5a..ae8337859d3 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -686,17 +686,25 @@ static int preserve_rooms_assignToRole(lua_State *L) { return 0; } -static string preserve_rooms_getRoleAssignment(color_ostream &out) { - auto zone = Gui::getSelectedCivZone(out, true); +static string get_role_assignment(color_ostream &out, df::building_civzonest * zone) { if (!zone) return ""; - TRACE(control,out).print("preserve_rooms_getRoleAssignment: zone_id=%d\n", zone->id); + TRACE(control,out).print("get_role_assignment: zone_id=%d\n", zone->id); auto it = noble_zones.find(zone->id); if (it == noble_zones.end() || it->second.empty()) return ""; return it->second[0]; } + +static string preserve_rooms_getRoleAssignmentForZone(color_ostream &out, df::building_civzonest * zone) { + return get_role_assignment(out, zone); +} + +static string preserve_rooms_getRoleAssignment(color_ostream &out) { + return get_role_assignment(out, Gui::getSelectedCivZone(out, true)); +} + static bool preserve_rooms_isReserved(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) @@ -774,6 +782,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(preserve_rooms_getFeature), DFHACK_LUA_FUNCTION(preserve_rooms_resetFeatureState), DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignment), + DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignmentForZone), DFHACK_LUA_FUNCTION(preserve_rooms_isReserved), DFHACK_LUA_FUNCTION(preserve_rooms_getReservationName), DFHACK_LUA_FUNCTION(preserve_rooms_clearReservation), From af5dccf7890af59899d90a880a25f713ffaccdbe Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 3 May 2025 17:39:41 -0500 Subject: [PATCH 333/919] Add prefer nicknamed to spectate (#5386) * Add prefer nicknamed * Actually add to the nicknamed bucket * Use other not job weight * Update changelog.txt * weight change * add prefer nicknamed default * fix my misunderstanding of the code in my testing turning off the prefer nicknamed used the jobs to categorize them . (i went from 16 nicknamed to 2 job units and 14 other units) so I'm pretty sure this works as i meant it to now * Update spectate.rst --- docs/changelog.txt | 1 + docs/plugins/spectate.rst | 3 +++ plugins/lua/spectate.lua | 1 + plugins/spectate.cpp | 15 +++++++++++++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 923e53b4823..9625cf119de 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -105,6 +105,7 @@ Template for new versions: ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) +- `spectate`: added prefer nicknamed units ## API - ``Military`` module: added ``addToSquad`` function diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 68d1013f644..dd0174f3206 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -105,6 +105,9 @@ Settings Toggle whether to prefer following (non-siege) units that have newly arrived on the map. +``prefer-nicknamed`` (default: enabled) + Toggle whether to prefer following nicknamed units. + ``tooltip-follow`` (default: enabled) If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the tooltips that follow onscreen dwarves around the map. diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index 0cb2d3bd33b..953895eab02 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -25,6 +25,7 @@ local function get_default_state() ['include-wildlife']=false, ['prefer-conflict']=true, ['prefer-new-arrivals']=true, + ['prefer-nicknamed']=true, ['tooltip-follow']=true, ['tooltip-follow-blink-milliseconds']=3000, ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 54bd4944a0f..ae287f60003 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -43,6 +43,7 @@ static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle static const size_t MAX_HISTORY = 200; static const float CITIZEN_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float NICKNAMED_CITIZEN_PREFERRED_WEIGHT = 15.0f; static const float OTHER_COMBAT_PREFERRED_WEIGHT = 10.0f; static const float JOB_WEIGHT = 5.0f; static const float OTHER_WEIGHT = 1.0f; @@ -71,6 +72,7 @@ static struct Configuration { bool include_wildlife; bool prefer_conflict; bool prefer_new_arrivals; + bool prefer_nicknamed; int32_t follow_ms; void reset() { @@ -82,6 +84,7 @@ static struct Configuration { include_wildlife = false; prefer_conflict = true; prefer_new_arrivals = true; + prefer_nicknamed = true; follow_ms = 10000; } } config; @@ -436,6 +439,7 @@ static bool is_in_combat(df::unit *unit) { static void get_dwarf_buckets(color_ostream &out, vector &citizen_combat_units, vector &other_combat_units, + vector &nicknamed_units, vector &job_units, vector &other_units) { @@ -452,11 +456,13 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - TRACE(cycle,out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + TRACE(cycle, out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) citizen_combat_units.push_back(unit); else other_combat_units.push_back(unit); + } else if (config.prefer_nicknamed && !unit->name.nickname.empty()) { + nicknamed_units.push_back(unit); } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { job_units.push_back(unit); } else { @@ -510,9 +516,10 @@ static void follow_a_dwarf(color_ostream &out) { vector citizen_combat_units; vector other_combat_units; + vector nicknamed_units; vector job_units; vector other_units; - get_dwarf_buckets(out, citizen_combat_units, other_combat_units, job_units, other_units); + get_dwarf_buckets(out, citizen_combat_units, other_combat_units, nicknamed_units, job_units, other_units); set_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); @@ -523,6 +530,7 @@ static void follow_a_dwarf(color_ostream &out) { intervals.push_back(0); add_bucket(citizen_combat_units, units, intervals, weights, config.prefer_conflict ? CITIZEN_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); add_bucket(other_combat_units, units, intervals, weights, config.prefer_conflict ? OTHER_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(nicknamed_units, units, intervals, weights, NICKNAMED_CITIZEN_PREFERRED_WEIGHT); add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); @@ -538,6 +546,7 @@ static void follow_a_dwarf(color_ostream &out) { if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { DUMP_BUCKET(citizen_combat_units); DUMP_BUCKET(other_combat_units); + DUMP_BUCKET(nicknamed_units); DUMP_BUCKET(job_units); DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); @@ -575,6 +584,8 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { config.prefer_conflict = val; } else if (name == "prefer-new-arrivals") { config.prefer_new_arrivals = val; + } else if (name == "prefer-nicknamed") { + config.prefer_nicknamed = val; } else if (name == "follow-seconds") { if (val <= 0) { WARN(control,out).print("follow-seconds must be a positive integer\n"); From b83426c0a24ebc8c6be39d3d9a2bec6930d8d405 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 3 May 2025 22:41:02 +0000 Subject: [PATCH 334/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index def13b7c6f1..f1e74867f22 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc +Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 From 4a8d6b51f45fb0bbd25b48b66414b19ebe20105e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 18:50:10 -0700 Subject: [PATCH 335/919] add option for forCoord to iterate in row-major order --- docs/changelog.txt | 1 + library/include/modules/Maps.h | 13 ++++++++----- library/modules/Maps.cpp | 27 ++++++++++++++++++--------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9625cf119de..b8d67523428 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -71,6 +71,7 @@ Template for new versions: ## API - ``Random`` module: added ``SplitmixRNG`` class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness - ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting +- ``cuboid::forCoord``, ``Maps::forCoord``: take additional parameter to control whether iteration goes in column major or row major order ## Lua - ``script-manager``: new ``get_active_mods()`` function for getting information on active mods diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c6..839989237e6 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -229,9 +229,11 @@ class cuboid { DFHACK_EXPORT bool containsPos(int16_t x, int16_t y, int16_t z) const; DFHACK_EXPORT bool containsPos(const df::coord &pos) const { return containsPos(pos.x, pos.y, pos.z); } - /// Iterate over every point in the cuboid from top-down, N-S, then W-E. Doesn't guarantee valid map tile! + /// Iterate over every point in the cuboid. Doesn't guarantee valid map tile! /// "fn" should return true to keep iterating. Won't iterate if cuboid invalid. - DFHACK_EXPORT void forCoord(std::function fn) const; + /// If row_major is false, iterates from top-down (z), N-S (y), then W-E (x). + /// If row_major is true, iterates from top-down (z), W-E (x), then N-S (y). + DFHACK_EXPORT void forCoord(std::function fn, bool row_major = false) const; /// Iterate over every non-NULL map block intersecting the tile cuboid from top-down, N-S, then W-E. /// Will also supply the intersection of this cuboid and block to your "fn" for use with cuboid::forCoord. @@ -250,12 +252,13 @@ namespace Maps extern DFHACK_EXPORT bool IsValid(); /// Iterate over points in a cuboid from z1:z2, y1:y2, then x1:x2. +/// If row_major is true, iterates from z1:z2, x1:x2, then y1:y2. /// Doesn't guarantee valid map tile! Can be used to iterate over blocks, etc. /// "fn" should return true to keep iterating. DFHACK_EXPORT void forCoord(std::function fn, - int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2); -inline void forCoord(std::function fn, const df::coord &p1, const df::coord &p2) { - forCoord(fn, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z); + int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2, bool row_major = false); +inline void forCoord(std::function fn, const df::coord &p1, const df::coord &p2, bool row_major = false) { + forCoord(fn, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, row_major); } /** diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 54fb43c810c..7e5707fccbf 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -197,10 +197,10 @@ bool cuboid::containsPos(int16_t x, int16_t y, int16_t z) const { x <= x_max && y <= y_max && z <= z_max; } -void cuboid::forCoord(std::function fn) const +void cuboid::forCoord(std::function fn, bool row_major) const { if (isValid()) // Only iterate if valid cuboid. - Maps::forCoord(fn, x_min, y_min, z_max, x_max, y_max, z_min); + Maps::forCoord(fn, x_min, y_min, z_max, x_max, y_max, z_min, row_major); } void cuboid::forBlock(std::function fn, bool ensure_block) const @@ -230,18 +230,27 @@ bool Maps::IsValid() { } void Maps::forCoord(std::function fn, int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) + int16_t x2, int16_t y2, int16_t z2, bool row_major) { int16_t dx = x1 > x2 ? -1 : 1; int16_t dy = y1 > y2 ? -1 : 1; int16_t dz = z1 > z2 ? -1 : 1; - // Process z, y, then x. - for (int16_t x = x1; x != x2 + dx; x += dx) - for (int16_t y = y1; y != y2 + dy; y += dy) - for (int16_t z = z1; z != z2 + dz; z += dz) - if (!fn(df::coord(x, y, z))) - return; // Break iterator. + if (row_major) { + // Process z, y, then x. + for (int16_t z = z1; z != z2 + dz; z += dz) + for (int16_t y = y1; y != y2 + dy; y += dy) + for (int16_t x = x1; x != x2 + dx; x += dx) + if (!fn(df::coord(x, y, z))) + return; // Break iterator. + } else { + // Process z, x, then y. + for (int16_t z = z1; z != z2 + dz; z += dz) + for (int16_t x = x1; x != x2 + dx; x += dx) + for (int16_t y = y1; y != y2 + dy; y += dy) + if (!fn(df::coord(x, y, z))) + return; // Break iterator. + } } // getter for map size in blocks From 4d49a5217a545ac159003ef5f042a064a66921b1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 18:52:06 -0700 Subject: [PATCH 336/919] handle disjoint zones --- plugins/blueprint.cpp | 62 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 6ec9b9c3c44..7d2b6568867 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -5,9 +5,6 @@ * Written by cdombroski. */ -#include -#include - #include "Console.h" #include "DataDefs.h" #include "DataFuncs.h" @@ -43,6 +40,10 @@ #include "df/tile_occupancy.h" #include "df/world.h" +#include +#include +#include + using std::endl; using std::map; using std::ofstream; @@ -1109,7 +1110,7 @@ static string get_reservation(color_ostream &out, df::building_civzonest *zone) } // TODO: handle locations -static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_properties) { +static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_label, bool add_properties) { const char * symbol = NULL; vector properties; @@ -1187,11 +1188,15 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo return NULL; } - if (!add_properties || properties.empty()) + if (!add_label && (!add_properties || properties.empty())) return symbol; ostringstream keys; - keys << symbol << '{' << join_strings(" ", properties) << '}'; + keys << symbol; + if (add_label) + keys << "/" << "zone_" << zone->id; + if (add_properties) + keys << '{' << join_strings(" ", properties) << '}'; return cache(keys.str()); } @@ -1204,11 +1209,49 @@ static df::coord get_first_tile(df::building_civzonest *zone) { return false; } return true; - }); + }, true); return first_pos; } +static int32_t get_flood_size(const df::building::T_room &room, int32_t start_x, int32_t start_y) { + if (!room.extents) + return 0; + std::unordered_set visited; + std::queue to_visit; + to_visit.push(df::coord(start_x, start_y, 0)); + while (!to_visit.empty()) { + df::coord pos = to_visit.front(); + to_visit.pop(); + if (visited.count(pos)) + continue; + visited.insert(pos); + for (int32_t y = -1; y <= 1; ++y) { + for (int32_t x = -1; x <= 1; ++x) { + if (x == 0 && y == 0) + continue; + int32_t nx = pos.x + x; + int32_t ny = pos.y + y; + if (nx < 0 || ny < 0 || nx >= room.width || ny >= room.height) + continue; + if (room.extents[ny * room.width + nx]) + to_visit.push(df::coord(nx, ny, 0)); + } + } + } + // flood size is the number of tiles we visited + return (int32_t)visited.size(); +} + +static bool is_disjoint(df::building *bld, const df::coord &first_tile) { + const df::building::T_room &room = bld->room; + if (!room.extents) + return false; + + int32_t flood_size = get_flood_size(room, first_tile.x - bld->x1, first_tile.y - bld->y1); + return flood_size != Buildings::countExtentTiles(bld); +} + static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { vector civzones; if (!Buildings::findCivzonesAt(&civzones, pos)) @@ -1240,15 +1283,16 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons } } + bool disjoint = is_disjoint(primary_zone, upper_left_corner); bool is_first_tile = pos == upper_left_corner; if (!is_rectangular(primary_zone)) - return get_zone_keys(out, primary_zone, is_first_tile); + return get_zone_keys(out, primary_zone, disjoint, is_first_tile); if (!is_first_tile) return if_pretty(ctx, "`"); - return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, true)); + return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, disjoint, true)); } static bool create_output_dir(color_ostream &out, From 7a057fba91ab06424c616605a3ea2a9e8a235c12 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 2 May 2025 18:10:59 -0500 Subject: [PATCH 337/919] `Core.cpp`: remove unnecessary casts `std::thread`'s constructor is a template, no reason to type-erase here --- library/Core.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index d949db688c5..bfaa30a034d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -269,10 +269,10 @@ struct IODATA // A thread function... for handling hotkeys. This is needed because // all the plugin commands are expected to be run from foreign threads. // Running them from one of the main DF threads will result in deadlock! -void fHKthread(void * iodata) +static void fHKthread(IODATA * iodata) { - Core * core = ((IODATA*) iodata)->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + Core * core = iodata->core; + PluginManager * plug_mgr = iodata->plug_mgr; if(plug_mgr == 0 || core == 0) { std::cerr << "Hotkey thread has croaked." << std::endl; @@ -1874,7 +1874,7 @@ bool Core::InitSimulationThread() std::cerr << "Starting DF input capture thread.\n"; // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, (void *) temp); + d->hotkeythread = std::thread(fHKthread, temp); started = true; modstate = 0; From 5040c012f3f90a78183708c508dc8fb8bdd5f30a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 01:59:06 -0500 Subject: [PATCH 338/919] remove type erasure from `RunCoreQueryLoop` --- library/Core.cpp | 45 ++++++++++++-------------------------- library/LuaTools.cpp | 27 +++++++---------------- library/include/LuaTools.h | 6 ++--- 3 files changed, 25 insertions(+), 53 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index bfaa30a034d..f2f965a7de2 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -323,62 +323,45 @@ static std::string dfhack_version_desc() return s.str(); } -namespace { - struct ScriptArgs { - const std::string *pcmd; - std::vector *pargs; - }; - struct ScriptEnableState { - const std::string *pcmd; - bool pstate; - }; -} - -static bool init_run_script(color_ostream &out, lua_State *state, void *info) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::vector& pargs) { - auto args = (ScriptArgs*)info; - if (!lua_checkstack(state, args->pargs->size()+10)) + if (!lua_checkstack(state, pargs.size()+10)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "run_script"); lua_remove(state, -2); - lua_pushstring(state, args->pcmd->c_str()); - for (size_t i = 0; i < args->pargs->size(); i++) - lua_pushstring(state, (*args->pargs)[i].c_str()); + lua_pushstring(state, pcmd.c_str()); + for (auto& arg : pargs) + lua_pushstring(state, arg.c_str()); return true; } static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { - ScriptArgs data; - data.pcmd = &name; - data.pargs = &args; - - bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); + using namespace std::placeholders; + auto init_fn = std::bind(init_run_script, _1, _2, name, args); + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, void *info) +static bool init_enable_script(color_ostream &out, lua_State *state, std::string& name, bool enable) { - auto args = (ScriptEnableState*)info; if (!lua_checkstack(state, 4)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "enable_script"); lua_remove(state, -2); - lua_pushstring(state, args->pcmd->c_str()); - lua_pushboolean(state, args->pstate); + lua_pushstring(state, name.c_str()); + lua_pushboolean(state, enable); return true; } static command_result enableLuaScript(color_ostream &out, std::string name, bool state) { - ScriptEnableState data; - data.pcmd = &name; - data.pstate = state; - - bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_enable_script, &data); + using namespace std::placeholders; + auto init_fn = std::bind(init_enable_script, _1, _2, name, state); + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); return ok ? CR_OK : CR_FAILURE; } diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 2d9df61ab44..95f8017dd8a 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1193,9 +1193,7 @@ static int resume_query_loop(color_ostream &out, return rv; } -bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, - bool (*init)(color_ostream&, lua_State*, void*), - void *arg) +bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, DFHack::Lua::init_fn init) { if (!lua_checkstack(state, 20)) return false; @@ -1213,7 +1211,7 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, int base = lua_gettop(state); - if (!init(out, state, arg)) + if (!init(out, state)) { lua_settop(state, base); return false; @@ -1283,21 +1281,13 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, return (rv == LUA_OK); } -namespace { - struct InterpreterArgs { - const char *prompt; - const char *hfile; - }; -} - -static bool init_interpreter(color_ostream &out, lua_State *state, void *info) +static bool init_interpreter(color_ostream &out, lua_State *state, const char* prompt, const char* hfile) { - auto args = (InterpreterArgs*)info; lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_DFHACK_TOKEN); lua_getfield(state, -1, "interpreter"); lua_remove(state, -2); - lua_pushstring(state, args->prompt); - lua_pushstring(state, args->hfile); + lua_pushstring(state, prompt); + lua_pushstring(state, hfile); return true; } @@ -1312,11 +1302,10 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, if (!prompt) prompt = "lua"; - InterpreterArgs args; - args.prompt = prompt; - args.hfile = hfile; + using namespace std::placeholders; + auto init_fn = std::bind(init_interpreter, _1, _2, prompt, hfile); - return RunCoreQueryLoop(out, state, init_interpreter, &args); + return RunCoreQueryLoop(out, state, init_fn); } static bool do_invoke_cleanup(lua_State *L, int nargs, int errorfun, bool success) diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 8b71862ec3d..e5a83721cc0 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -286,9 +286,9 @@ namespace DFHack::Lua { * is done inside CoreSuspender, while waiting for input happens * without the suspend lock. */ - DFHACK_EXPORT bool RunCoreQueryLoop(color_ostream &out, lua_State *state, - bool (*init)(color_ostream&, lua_State*, void*), - void *arg); + using init_fn = std::function; + + DFHACK_EXPORT bool RunCoreQueryLoop(color_ostream &out, lua_State *state, init_fn init); /** * Attempt to interrupt the currently-executing lua function by raising a lua error From 53f7af165230661fe39baf7f98f432019e412406 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 02:03:04 -0500 Subject: [PATCH 339/919] `Core`: remove more unneeded type erasures --- library/Core.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index f2f965a7de2..48da47ef13e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1391,9 +1391,8 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // Load dfhack.init in a dedicated thread (non-interactive console mode) -void fInitthread(void * iodata) +static void fInitthread(IODATA * iod) { - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; color_ostream_proxy out(core->getConsole()); @@ -1401,13 +1400,12 @@ void fInitthread(void * iodata) } // A thread function... for the interactive console. -void fIOthread(void * iodata) +static void fIOthread(IODATA * iod) { static const std::filesystem::path HISTORY_FILE = CONFIG_PATH / "dfhack.history"; - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + PluginManager * plug_mgr = iod->plug_mgr; CommandHistory main_history; main_history.load(HISTORY_FILE.c_str()); @@ -1847,12 +1845,12 @@ bool Core::InitSimulationThread() { std::cerr << "Starting IO thread.\n"; // create IO thread - d->iothread = std::thread{fIOthread, (void*)temp}; + d->iothread = std::thread{fIOthread, temp}; } else { std::cerr << "Starting dfhack.init thread.\n"; - d->iothread = std::thread{fInitthread, (void*)temp}; + d->iothread = std::thread{fInitthread, temp}; } std::cerr << "Starting DF input capture thread.\n"; From 608ae95a36bde75b868cf44fc7588e2595ff1722 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 11:30:03 -0500 Subject: [PATCH 340/919] eliminate another type erasure in `Core` --- library/Core.cpp | 4 ++-- library/include/Core.h | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 48da47ef13e..b7a4bc5227b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2022,8 +2022,8 @@ void Core::doUpdate(color_ostream &out) } // detect if the game was loaded or unloaded in the meantime - void *new_wdata = NULL; - void *new_mapdata = NULL; + df::world_data* new_wdata = nullptr; + df::map_block**** new_mapdata = nullptr; if (df::global::world && !is_load_save) { df::world_data *wdata = df::global::world->world_data; diff --git a/library/include/Core.h b/library/include/Core.h index dd84d49df03..383a0a9c954 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -50,6 +50,8 @@ struct lua_State; namespace df { struct viewscreen; + struct world_data; + struct map_block; } namespace DFHack @@ -300,9 +302,9 @@ namespace DFHack bool SelectHotkey(int key, int modifiers); // for state change tracking - void *last_world_data_ptr; + df::world_data *last_world_data_ptr; // for state change tracking - void *last_local_map_ptr; + df::map_block**** last_local_map_ptr; friend struct Screen::Hide; df::viewscreen *top_viewscreen; bool last_pause_state; From 5e50664b8f0477656c96d49946105652cbf2359a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 3 May 2025 13:28:19 -0500 Subject: [PATCH 341/919] merge `Process.cpp` into a single source file --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index f1e74867f22..def13b7c6f1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 +Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc From bb332c91677afc118cc4d33e40ea551526392944 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 4 May 2025 12:06:56 -0500 Subject: [PATCH 342/919] Revert "merge `Process.cpp` into a single source file" This reverts commit 5e50664b8f0477656c96d49946105652cbf2359a. --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index def13b7c6f1..f1e74867f22 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit def13b7c6f1bb8b036968490cae696e18cb25bbc +Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 From bfb62c4530a4071b36e08529dad947ff85fc36b5 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 5 May 2025 07:20:53 +0000 Subject: [PATCH 343/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index f1e74867f22..9f76bc9d2a1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f1e74867f22c5f51eb4bfe380744b43f062d0b02 +Subproject commit 9f76bc9d2a1c1e1cb237543f320acda352f40881 From 5b063b59f2b08c2ef9cf3c7da63bfafa6c5bef6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 19:15:53 +0000 Subject: [PATCH 344/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.32.1 → 0.33.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.32.1...0.33.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8348cb4d85a..937a9de80d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.32.1 + rev: 0.33.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 724ba25bc9461372b9d9b2a54f0bfbd179a34541 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 6 May 2025 02:02:41 +0000 Subject: [PATCH 345/919] Auto-update submodules library/xml: master scripts: master depends/dfhooks: main --- depends/dfhooks | 2 +- library/xml | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index ca30a90f8a3..481dc1a12b1 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit ca30a90f8a3f9cc61531ac24430fd2434fff40ec +Subproject commit 481dc1a12b1264ef06ce95e331ef35cbfa0e6ace diff --git a/library/xml b/library/xml index 6502bdefd17..f48fd9ef436 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 6502bdefd1796315365a7bb58e30e0ce34359bea +Subproject commit f48fd9ef4369027499da28441adaf9c572431891 diff --git a/scripts b/scripts index 9f76bc9d2a1..e50474efbfa 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9f76bc9d2a1c1e1cb237543f320acda352f40881 +Subproject commit e50474efbfa514fd0e4b63cd9a8c114ccf0bb392 From 2ba3c0d61000a1dea8d1d3ada0f15f6e36982e34 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 May 2025 11:05:01 -0500 Subject: [PATCH 346/919] adjust `df_object` constraint some (most, actually) `get` methods return a pointer to a `const` instance of a subclass of `type_identity` which isn't convertible to a pointer of non-`const` `type_identity`. adding `const` fixes this fixes #5450 --- docs/changelog.txt | 2 ++ library/include/LuaTools.h | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b8d67523428..d3484c86a68 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,8 @@ Template for new versions: - `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air - `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows - `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit +- fixed an overly restrictive type constraint that resulted in some object types being glossed as a boolean when passed as an argument from C++ to Lua +- `plants`: will no longer generate a traceback when a filter is used ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index e5a83721cc0..429a8bfc6bb 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -141,9 +141,9 @@ namespace DFHack::Lua { DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, void *target, int val_index, bool exact_type = false); - template concept df_object = requires(T x) + template concept df_object = requires() { - { df::identity_traits::get() } -> std::convertible_to; + { df::identity_traits::get() } -> std::convertible_to; }; /** From d915fe192d84f63eb61c1abe751e12a857b56264 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 13:07:27 -0700 Subject: [PATCH 347/919] show priorities for digging designations in ascii overlay --- docs/changelog.txt | 1 + docs/plugins/dig.rst | 12 ++++++------ plugins/dig.cpp | 23 ++++++++++++++--------- plugins/lua/dig.lua | 17 +++++++++++------ 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index d3484c86a68..ff76e3d6b7f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. +- `dig`: ASCII overlay now displays priority of digging designations ## Documentation diff --git a/docs/plugins/dig.rst b/docs/plugins/dig.rst index 0a6007f598e..d8d9c4ff400 100644 --- a/docs/plugins/dig.rst +++ b/docs/plugins/dig.rst @@ -197,13 +197,13 @@ Overlay This tool also provides three overlays that are managed by the `overlay` framework. -asciicarve -~~~~~~~~~~ +asciidesignated +~~~~~~~~~~~~~~~ -The ``dig.asciicarve`` overlay makes carving designations visible in ASCII -mode. It highlights tiles that are designated for smoothing, engraving, track -carving, or fortification carving. The designations blink (slowly) so you can -still see what is underneath them. +The ``dig.asciidesignated`` overlay makes designations visible in ASCII mode. +It highlights tiles that are designated for digging, smoothing, engraving, +track carving, or fortification carving. The designations blink (slowly) so you +can still see what is underneath them. Due to the limitations of the ASCII mode screen buffer, the designation highlights may show through other interface elements that overlap the diff --git a/plugins/dig.cpp b/plugins/dig.cpp index bc9089a654a..8bdcca59d67 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -2388,6 +2388,10 @@ static bool is_designated_for_track_carving(const designation &designation) { return occ.bits.carve_track_east || occ.bits.carve_track_north || occ.bits.carve_track_south || occ.bits.carve_track_west; } +static bool is_designated_for_digging(const designation &designation) { + return designation.td.bits.dig != df::tile_dig_designation::No; +} + static char get_track_char(const designation &designation) { const df::tile_occupancy &occ = designation.to; if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_south && occ.bits.carve_track_west) @@ -2444,8 +2448,8 @@ static char get_tile_char(const df::coord &pos, char desig_char, bool draw_prior } } -static void paintScreenCarve() { - TRACE(log).print("entering paintScreenCarve\n"); +static void paintScreenDesignated() { + TRACE(log).print("entering paintScreenDesignated\n"); if (Screen::inGraphicsMode() || blink(500)) return; @@ -2461,11 +2465,6 @@ static void paintScreenCarve() { if (!Maps::isValidTilePos(map_pos)) continue; - if (!Maps::isTileVisible(map_pos)) { - TRACE(log).print("skipping hidden tile\n"); - continue; - } - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", map_pos.x, map_pos.y, map_pos.z, x, y); @@ -2486,8 +2485,14 @@ static void paintScreenCarve() { else if (is_designated_for_track_carving(des)) { cur_tile.ch = get_tile_char(map_pos, get_track_char(des), draw_priority); // directional track } + else if (is_designated_for_digging(des)) { + static char empty_char = (char)0x00; + cur_tile.ch = get_tile_char(map_pos, empty_char, draw_priority); + if (cur_tile.ch == empty_char) + continue; + } else { - TRACE(log).print("skipping tile with no carving designation\n"); + TRACE(log).print("skipping tile with no designation\n"); continue; } @@ -2515,6 +2520,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(toggleCurLevelWarmDig), DFHACK_LUA_FUNCTION(toggleCurLevelDampDig), DFHACK_LUA_FUNCTION(paintScreenWarmDamp), - DFHACK_LUA_FUNCTION(paintScreenCarve), + DFHACK_LUA_FUNCTION(paintScreenDesignated), DFHACK_LUA_END }; diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua index b988e9d42eb..babdb3b7ab5 100644 --- a/plugins/lua/dig.lua +++ b/plugins/lua/dig.lua @@ -348,13 +348,18 @@ function WarmDampOverlay:onRenderFrame(dc, rect) end -- -------------------------------- --- CarveOverlay +-- DesignatedOverlay -- -CarveOverlay = defclass(CarveOverlay, overlay.OverlayWidget) -CarveOverlay.ATTRS{ +DesignatedOverlay = defclass(DesignatedOverlay, overlay.OverlayWidget) +DesignatedOverlay.ATTRS{ desc='Makes existing carving designations visible when in ASCII mode.', viewscreens={ + 'dwarfmode/Designate/DIG_DIG', + 'dwarfmode/Designate/DIG_REMOVE_STAIRS_RAMPS', + 'dwarfmode/Designate/DIG_STAIR_UPDOWN', + 'dwarfmode/Designate/DIG_RAMP', + 'dwarfmode/Designate/DIG_CHANNEL', 'dwarfmode/Designate/SMOOTH', 'dwarfmode/Designate/ENGRAVE', 'dwarfmode/Designate/TRACK', @@ -365,8 +370,8 @@ CarveOverlay.ATTRS{ frame={w=0, h=0}, } -function CarveOverlay:onRenderFrame() - paintScreenCarve() +function DesignatedOverlay:onRenderFrame() + paintScreenDesignated() end -- -------------------------------- @@ -374,7 +379,7 @@ end -- OVERLAY_WIDGETS = { - asciicarve=CarveOverlay, + asciidesignated=DesignatedOverlay, warmdamp=WarmDampOverlay, warmdamptoolbar=WarmDampToolbarOverlay, } From 9933825d9d27041624770c66cf0ee9942beb0180 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 13:30:37 -0700 Subject: [PATCH 348/919] canonicalize slashes in output of dfhack.filesystem.listdir --- library/LuaApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 927e2f6d721..1d77ebf15a1 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3176,7 +3176,7 @@ static int filesystem_listdir(lua_State *L) for(size_t i=0;i Date: Fri, 9 May 2025 13:31:59 -0700 Subject: [PATCH 349/919] fix misplaced line in changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ff76e3d6b7f..0b824ae90d0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ Template for new versions: ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. - `dig`: ASCII overlay now displays priority of digging designations +- `spectate`: added prefer nicknamed units ## Documentation @@ -109,7 +110,6 @@ Template for new versions: ## Misc Improvements - `spectate`: show dwarves' activities (like prayer) -- `spectate`: added prefer nicknamed units ## API - ``Military`` module: added ``addToSquad`` function From e7f17ca5f81b4c894795b69b07fd3c3658cd6936 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 13:42:01 -0700 Subject: [PATCH 350/919] remove obsolete reference to ruby scripts --- library/Core.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b7a4bc5227b..1208650579d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1284,9 +1284,6 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::string completed; std::filesystem::path filename = findScript(first + ".lua"); bool lua = !filename.empty(); - if ( !lua ) { - filename = findScript(first + ".rb"); - } if ( lua ) res = runLuaScript(con, first, parts); else if (!no_autocomplete && try_autocomplete(con, first, completed)) From 382191cce80e4bb2a0735bc50e8385b6622127df Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 15:43:39 -0700 Subject: [PATCH 351/919] record stockpile properties in blueprint --- docs/changelog.txt | 2 + plugins/blueprint.cpp | 347 ++++++++++++++++++++++---------------- plugins/logistics.cpp | 22 ++- plugins/lua/blueprint.lua | 16 ++ 4 files changed, 231 insertions(+), 156 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0b824ae90d0..abd1b845fc0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -69,6 +69,8 @@ Template for new versions: - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. - `dig`: ASCII overlay now displays priority of digging designations - `spectate`: added prefer nicknamed units +- `blueprint`: support for recording zones +- `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration ## Documentation diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 7d2b6568867..26caf258169 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -182,7 +182,12 @@ struct blueprint_processor { get_tile(get_tile), init_ctx(init_ctx) { } }; -// global caches, cleared when the string cache is cleared +// global caches, lazily initialized and cleared at the end of each blueprint +// this assumes that no two blueprints are being generated at the same time, +// which is currently ensured by the higher-level DFHack command handling code. +// if this assumption ever becomes untrue, we'll need to protect the caches +// with thread synchronization primitives or make the caches per-blueprint. +static std::set string_cache; static std::unordered_map engravings_cache; static std::unordered_map dig_job_cache; static PersistentDataItem warm_config, damp_config; @@ -214,6 +219,14 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { }); } +static void clear_caches() { + string_cache.clear(); + engravings_cache.clear(); + dig_job_cache.clear(); + warm_config = PersistentDataItem(); + damp_config = PersistentDataItem(); +} + // We use const char * throughout this code instead of std::string to avoid // having to allocate memory for all the small string literals. This // significantly speeds up processing and allows us to handle very large maps @@ -222,19 +235,9 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { // allocated until we write out the blueprints at the end. // If NULL is passed as the str, the cache is cleared. static const char * cache(const char *str) { - // this local static assumes that no two blueprints are being generated at - // the same time, which is currently ensured by the higher-level DFHack - // command handling code. if this assumption ever becomes untrue, we'll - // need to protect the cache with thread synchronization primitives or make - // the cache per-blueprint. - static std::set _cache; - if (!str) { - _cache.clear(); - engravings_cache.clear(); - dig_job_cache.clear(); + if (!str) return NULL; - } - return _cache.emplace(str).first->c_str(); + return string_cache.emplace(str).first->c_str(); } // Convenience wrapper for std::string. @@ -1024,21 +1027,18 @@ static const char * add_expansion_syntax(const df::building *bld, return cache(s); } +static void add_expansion_syntax(const df::building *bld, ostringstream &keys) { + if (keys.str().empty()) + return; + pair size = get_building_size(bld); + keys << "(" << size.first << "x" << size.second << ")"; +} + static const char * add_expansion_syntax(const tile_context &ctx, const char *keys) { return add_expansion_syntax(ctx.b, keys); } -static const char * add_label(const tile_context &ctx, const char *keys) { - if (!keys) - return "~"; - auto bld = ctx.b; - ostringstream s; - // use building's id as the unique label - s << keys << "/" << "bld_" << bld->id; - return cache(s); -} - static const char * get_tile_build(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { return NULL; @@ -1052,51 +1052,152 @@ static const char * get_tile_build(color_ostream &out, const df::coord &pos, con return add_expansion_syntax(ctx, keys); } -static const char * get_place_keys(const tile_context &ctx) { - df::building_stockpilest* sp = - virtual_cast(ctx.b); - if (!sp) { - return NULL; - } +static string quotify_inner(const string &s) { + if (s.find_first_of(" ,") == string::npos) + return s; + ostringstream buf; + buf << "\"\"" << s << "\"\""; + return buf.str(); +} + +static string quotify_outer(const string &s) { + if (s.find_first_of("\",") == string::npos) + return s; + ostringstream buf; + buf << "\"" << s << "\""; + return buf.str(); +} - string keys; +static string quotify_outer(const ostringstream &s) { + return quotify_outer(s.str()); +} + +static void get_place_keys(color_ostream &out, ostringstream &keys, df::building_stockpilest* sp, bool add_label, bool add_properties) { df::stockpile_group_set &flags = sp->settings.flags; - if (flags.bits.animals) keys += 'a'; - if (flags.bits.food) keys += 'f'; - if (flags.bits.furniture) keys += 'u'; - if (flags.bits.coins) keys += 'n'; - if (flags.bits.corpses) keys += 'y'; - if (flags.bits.refuse) keys += 'r'; - if (flags.bits.stone) keys += 's'; - if (flags.bits.wood) keys += 'w'; - if (flags.bits.gems) keys += 'e'; - if (flags.bits.bars_blocks) keys += 'b'; - if (flags.bits.cloth) keys += 'h'; - if (flags.bits.leather) keys += 'l'; - if (flags.bits.ammo) keys += 'z'; - if (flags.bits.sheet) keys += 'S'; - if (flags.bits.finished_goods) keys += 'g'; - if (flags.bits.weapons) keys += 'p'; - if (flags.bits.armor) keys += 'd'; - - if (keys.empty()) - return "c"; - return cache(keys); + if (flags.bits.animals) keys << 'a'; + if (flags.bits.food) keys << 'f'; + if (flags.bits.furniture) keys << 'u'; + if (flags.bits.coins) keys << 'n'; + if (flags.bits.corpses) keys << 'y'; + if (flags.bits.refuse) keys << 'r'; + if (flags.bits.stone) keys << 's'; + if (flags.bits.wood) keys << 'w'; + if (flags.bits.gems) keys << 'e'; + if (flags.bits.bars_blocks) keys << 'b'; + if (flags.bits.cloth) keys << 'h'; + if (flags.bits.leather) keys << 'l'; + if (flags.bits.ammo) keys << 'z'; + if (flags.bits.sheet) keys << 'S'; + if (flags.bits.finished_goods) keys << 'g'; + if (flags.bits.weapons) keys << 'p'; + if (flags.bits.armor) keys << 'd'; + + if (keys.str().empty()) + keys << 'c'; + + if (!add_label && !add_properties) + return; + + if (add_label) + keys << "/" << "sp_" << sp->id; + + if (!add_properties) + return; + + vector properties; + + if (!sp->name.empty()) + properties.push_back("name=" + quotify_inner(sp->name)); + + // only include take_from and give_to targets if they are named + vector take_from, give_to; + for (auto & target : sp->links.take_from_pile) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.take_from_workshop) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.give_to_pile) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + for (auto & target : sp->links.give_to_workshop) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + if (!take_from.empty()) + properties.push_back("take_from=" + quotify_inner(join_strings(",", take_from))); + if (!give_to.empty()) + properties.push_back("give_to=" + quotify_inner(join_strings(",", give_to))); + + if (sp->stockpile_flag.bits.use_links_only) + properties.push_back("links_only=true"); + + // simplify implementation; always record container counts, even if they are set to default values + if (!sp->storage.max_barrels && !sp->storage.max_bins && !sp->storage.max_wheelbarrows) + properties.push_back("containers=0"); + else { + properties.push_back("barrels=" + int_to_string(sp->storage.max_barrels)); + properties.push_back("bins=" + int_to_string(sp->storage.max_bins)); + properties.push_back("wheelbarrows=" + int_to_string(sp->storage.max_wheelbarrows)); + } + + // logistics features + Lua::CallLuaModuleFunction(out, "plugins.blueprint", "get_logistics_settings", + std::make_tuple(sp->stockpile_number), 6, [&](lua_State *L) { + if (lua_toboolean(L, -6)) properties.push_back("automelt=true"); + if (lua_toboolean(L, -5)) properties.push_back("autotrade=true"); + if (lua_toboolean(L, -4)) properties.push_back("autodump=true"); + if (lua_toboolean(L, -3)) properties.push_back("autotrain=true"); + if (lua_toboolean(L, -2)) properties.push_back("autoforbid=true"); + if (lua_toboolean(L, -1)) properties.push_back("autoclaim=true"); + }); + + if (!properties.empty()) + keys << '{' << join_strings(" ", properties) << '}'; +} + +static df::coord get_first_tile(df::building *bld) { + df::coord first_pos; + cuboid bld_area(bld->x1, bld->y1, bld->z, bld->x2, bld->y2, bld->z); + bld_area.forCoord([&](const df::coord &pos) { + if (Buildings::containsTile(bld, pos)) { + first_pos = pos; + return false; + } + return true; + }, true); + + return first_pos; } static const char * get_tile_place(color_ostream &out, const df::coord &pos, const tile_context &ctx) { - if (!ctx.b || ctx.b->getType() != building_type::Stockpile) + df::building_stockpilest* sp = virtual_cast(ctx.b); + + if (!sp || sp->getType() != building_type::Stockpile) return NULL; - if (!is_rectangular(ctx)) - return add_label(ctx, get_place_keys(ctx)); + bool rectangular = is_rectangular(sp); + bool is_first_tile = pos == get_first_tile(sp); + ostringstream keys; - if (ctx.b->x1 != static_cast(pos.x) - || ctx.b->y1 != static_cast(pos.y)) { - return if_pretty(ctx, "`"); + if (!rectangular){ + get_place_keys(out, keys, sp, true, is_first_tile); + return cache(quotify_outer(keys)); } - return add_expansion_syntax(ctx, get_place_keys(ctx)); + if (!is_first_tile) + return if_pretty(ctx, "`"); + + get_place_keys(out, keys, sp, false, true); + add_expansion_syntax(sp, keys); + return cache(quotify_outer(keys)); } static string get_reservation(color_ostream &out, df::building_civzonest *zone) { @@ -1110,12 +1211,11 @@ static string get_reservation(color_ostream &out, df::building_civzonest *zone) } // TODO: handle locations -static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_label, bool add_properties) { - const char * symbol = NULL; +static void get_zone_keys(color_ostream &out, ostringstream &keys, df::building_civzonest *zone, bool add_label, bool add_properties) { vector properties; if (!zone->name.empty()) - properties.push_back("name=" + zone->name); + properties.push_back(quotify_inner("name=" + zone->name)); if (!zone->spec_sub_flag.bits.active) properties.push_back("active=false"); if (auto reserved_for = get_reservation(out, zone); !reserved_for.empty()) { @@ -1125,26 +1225,26 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo // in DFHack docs order switch (zone->type) { using namespace df::enums::civzone_type; - case MeetingHall: symbol = "m"; break; - case Bedroom: symbol = "b"; break; - case DiningHall: symbol = "h"; break; - case Pen: symbol = "n"; break; + case MeetingHall: keys << "m"; break; + case Bedroom: keys << "b"; break; + case DiningHall: keys << "h"; break; + case Pen: keys << "n"; break; case Pond: - symbol = "p"; + keys << "p"; { if (zone->zone_settings.pond.flag.bits.keep_filled) properties.push_back("pond=true"); } break; - case WaterSource: symbol = "w"; break; - case Dungeon: symbol = "j"; break; - case FishingArea: symbol = "f"; break; - case SandCollection: symbol = "s"; break; - case Office: symbol = "o"; break; - case Dormitory: symbol = "D"; break; - case Barracks: symbol = "B"; break; + case WaterSource: keys << "w"; break; + case Dungeon: keys << "j"; break; + case FishingArea: keys << "f"; break; + case SandCollection: keys << "s"; break; + case Office: keys << "o"; break; + case Dormitory: keys << "D"; break; + case Barracks: keys << "B"; break; case ArcheryRange: - symbol = "a"; + keys << "a"; { auto & archery = zone->zone_settings.archery; if (archery.dir_x == 1 && archery.dir_y == 0) @@ -1155,14 +1255,16 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo properties.push_back("shoot_from=north"); else if (archery.dir_x == 0 && archery.dir_y == -1) properties.push_back("shoot_from=south"); - else - return NULL; // invalid direction + else { + keys.clear(); + return; // invalid direction + } } break; - case Dump: symbol = "d"; break; - case AnimalTraining: symbol = "t"; break; + case Dump: keys << "d"; break; + case AnimalTraining: keys << "t"; break; case Tomb: - symbol = "T"; + keys << "T"; { auto & tomb = zone->zone_settings.tomb; if (!tomb.flags.bits.no_pets) @@ -1172,7 +1274,7 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo } break; case PlantGathering: - symbol = "g"; + keys << "g"; { auto & gather = zone->zone_settings.gather; if (!gather.flags.bits.pick_trees) @@ -1183,73 +1285,18 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo properties.push_back("gather_fallen=false"); } break; - case ClayCollection: symbol = "c"; break; + case ClayCollection: keys << "c"; break; default: - return NULL; + return; } if (!add_label && (!add_properties || properties.empty())) - return symbol; + return; - ostringstream keys; - keys << symbol; if (add_label) keys << "/" << "zone_" << zone->id; - if (add_properties) + if (add_properties && !properties.empty()) keys << '{' << join_strings(" ", properties) << '}'; - return cache(keys.str()); -} - -static df::coord get_first_tile(df::building_civzonest *zone) { - df::coord first_pos; - cuboid zone_area(zone->x1, zone->y1, zone->z, zone->x2, zone->y2, zone->z); - zone_area.forCoord([&](const df::coord &pos) { - if (Buildings::containsTile(zone, pos)) { - first_pos = pos; - return false; - } - return true; - }, true); - - return first_pos; -} - -static int32_t get_flood_size(const df::building::T_room &room, int32_t start_x, int32_t start_y) { - if (!room.extents) - return 0; - std::unordered_set visited; - std::queue to_visit; - to_visit.push(df::coord(start_x, start_y, 0)); - while (!to_visit.empty()) { - df::coord pos = to_visit.front(); - to_visit.pop(); - if (visited.count(pos)) - continue; - visited.insert(pos); - for (int32_t y = -1; y <= 1; ++y) { - for (int32_t x = -1; x <= 1; ++x) { - if (x == 0 && y == 0) - continue; - int32_t nx = pos.x + x; - int32_t ny = pos.y + y; - if (nx < 0 || ny < 0 || nx >= room.width || ny >= room.height) - continue; - if (room.extents[ny * room.width + nx]) - to_visit.push(df::coord(nx, ny, 0)); - } - } - } - // flood size is the number of tiles we visited - return (int32_t)visited.size(); -} - -static bool is_disjoint(df::building *bld, const df::coord &first_tile) { - const df::building::T_room &room = bld->room; - if (!room.extents) - return false; - - int32_t flood_size = get_flood_size(room, first_tile.x - bld->x1, first_tile.y - bld->y1); - return flood_size != Buildings::countExtentTiles(bld); } static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { @@ -1263,9 +1310,9 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons // -- no two non-rectangular zones overlap // for a first implementation, we will only handle overlapping zones if they - // are rectangular. if this is the upper left corner of a rectangular zone, - // we will output for that zone. otherwise, if this pos is interior to - // all zones, then it doesn't matter which we choose. + // are rectangular and have different upper-left corners. if this is the upper + // left corner of a rectangular zone, we will output for that zone. otherwise, + // if this pos is interior to all zones, then it doesn't matter which we choose. df::building_civzonest * primary_zone = civzones[0]; df::coord upper_left_corner; @@ -1283,16 +1330,21 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons } } - bool disjoint = is_disjoint(primary_zone, upper_left_corner); + bool rectangular = is_rectangular(primary_zone); bool is_first_tile = pos == upper_left_corner; + ostringstream keys; - if (!is_rectangular(primary_zone)) - return get_zone_keys(out, primary_zone, disjoint, is_first_tile); + if (!rectangular) { + get_zone_keys(out, keys, primary_zone, true, is_first_tile); + return cache(quotify_outer(keys)); + } if (!is_first_tile) return if_pretty(ctx, "`"); - return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, disjoint, true)); + get_zone_keys(out, keys, primary_zone, false, true); + add_expansion_syntax(primary_zone, keys); + return cache(quotify_outer(keys)); } static bool create_output_dir(color_ostream &out, @@ -1647,8 +1699,7 @@ static command_result do_blueprint(color_ostream &out, bool ok = do_transform(out, start, end, options, files); - // clear caches - cache(NULL); + clear_caches(); return ok ? CR_OK : CR_FAILURE; } diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index bcb59b7ad5c..90b31b3509b 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -59,6 +59,12 @@ enum StockpileConfigValues { STOCKPILE_CONFIG_FORBID = 6, }; +enum StockpileConfigForbidValues { + STOCKPILE_CONFIG_FORBID_OFF = 0, + STOCKPILE_CONFIG_FORBID_FORBID = 1, + STOCKPILE_CONFIG_FORBID_CLAIM = 2, +}; + static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { TRACE(control, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); if (watched_stockpiles.count(stockpile_number)) { @@ -76,7 +82,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock c.set_bool(STOCKPILE_CONFIG_DUMP, false); c.set_bool(STOCKPILE_CONFIG_TRAIN, false); c.set_bool(STOCKPILE_CONFIG_MELT_MASTERWORKS, false); - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); return c; } @@ -132,7 +138,7 @@ static void validate_stockpile_configs(color_ostream& out, !c.get_bool(STOCKPILE_CONFIG_TRADE) && !c.get_bool(STOCKPILE_CONFIG_DUMP) && !c.get_bool(STOCKPILE_CONFIG_TRAIN) && - !(c.get_int(STOCKPILE_CONFIG_FORBID) > 0))) { + !(c.get_int(STOCKPILE_CONFIG_FORBID) > STOCKPILE_CONFIG_FORBID_OFF))) { to_remove.push_back(stockpile_number); continue; } @@ -180,7 +186,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (c.key() == CONFIG_KEY) continue; if (c.get_int(STOCKPILE_CONFIG_FORBID) == -1) // remove this once saves from 51.01 are no longer compatible - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); watched_stockpiles.emplace(c.get_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER), c); } migrate_old_keys(out); @@ -560,8 +566,8 @@ static void do_cycle(color_ostream& out, bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats, melt_masterworks); TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); @@ -645,8 +651,8 @@ static int logistics_getStockpileData(lua_State *L) { bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); unordered_map sconfig; sconfig.emplace("melt", melt ? "true" : "false"); @@ -719,7 +725,7 @@ static unordered_map get_stockpile_config(int32_t stockpile_number) stockpile_config.emplace("trade", false); stockpile_config.emplace("dump", false); stockpile_config.emplace("train", false); - stockpile_config.emplace("forbid", 0); + stockpile_config.emplace("forbid", STOCKPILE_CONFIG_FORBID_OFF); } return stockpile_config; } diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index e43ce5f5f68..8c765d563f8 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.blueprint') local argparse = require('argparse') +local logistics = require('plugins.logistics') local utils = require('utils') local valid_phase_list = { @@ -221,6 +222,21 @@ function get_filename(opts, phase, ordinal) return ('%s-%d-%s.csv'):format(fullname, ordinal, phase) end +function get_logistics_settings(stockpile_number) + local automelt, autotrade, autodump, autotrain, autoforbid, autoclaim = false, false, false, false, false, false + local configs = logistics.logistics_getStockpileConfigs(stockpile_number) + if configs and #configs == 1 then + local config = configs[1] + automelt = config.melt ~= 0 + autotrade = config.trade ~= 0 + autodump = config.dump ~= 0 + autotrain = config.train ~= 0 + autoforbid = config.forbid == 1 + autoclaim = config.forbid == 2 + end + return automelt, autotrade, autodump, autotrain, autoforbid, autoclaim +end + -- compatibility with old exported API. local function do_phase(start_pos, end_pos, name, phase) local width = math.abs(start_pos.x - end_pos.x) + 1 From 1400be4c00f8fd43d7501e8731af9aa2f3d7aa1f Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 21 May 2025 13:42:37 -0500 Subject: [PATCH 352/919] Add id option --- docs/plugins/strangemood.rst | 4 ++++ plugins/strangemood.cpp | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/plugins/strangemood.rst b/docs/plugins/strangemood.rst index b7d87196e23..e9f00337831 100644 --- a/docs/plugins/strangemood.rst +++ b/docs/plugins/strangemood.rst @@ -29,6 +29,10 @@ Options Make the strange mood strike the selected unit instead of picking one randomly. Unit eligibility is still enforced (unless ``--force`` is also specified). +``--id`` + Make the strange mood strike the unit with the given id instead of picking one + randomly. Unit eligibility is still enforced (unless ``--force`` is also + specified). ``--type `` Force the mood to be of a particular type instead of choosing randomly based on happiness. Valid values are "fey", "secretive", "possessed", "fell", and diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 98b8b4b7256..3bdd9431df5 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -164,6 +164,18 @@ command_result df_strangemood (color_ostream &out, vector & parameters) return CR_WRONG_USAGE; else if(parameters[i] == "--force") force = true; + else if(parameters[i] == "--id") + { + i++; + if (i == parameters.size()) + { + out.printerr("No unit id specified!\n"); + return CR_WRONG_USAGE; + } + unit = df::unit::find(std::stoi(parameters[i])); + if (!unit) + return CR_FAILURE; + } else if(parameters[i] == "--unit") { unit = DFHack::Gui::getSelectedUnit(out); From 23a9e3c9d424c75f574345d42b4f4a69e79425ec Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 21 May 2025 13:45:44 -0500 Subject: [PATCH 353/919] Update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index abd1b845fc0..1cf3d0d0701 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -71,6 +71,7 @@ Template for new versions: - `spectate`: added prefer nicknamed units - `blueprint`: support for recording zones - `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration +- `strangemood`: support added for specifying unit id instead of selected unit or random one. ## Documentation From a8661d674f5b0d1696be52b8892999dd6f66b4ec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 10 May 2025 00:49:39 -0500 Subject: [PATCH 354/919] lua/c++ interface code cleanup replace weird `operator new` override with `make_lua_userdata` template use placement `new` and a constructor for `DFRefHeader` --- library/LuaApi.cpp | 10 +++++----- library/LuaWrapper.cpp | 15 +++++++++------ library/include/LuaTools.h | 19 ++++++++++++++----- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1d77ebf15a1..be010a359c1 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -629,7 +629,7 @@ void Lua::Push(lua_State *L, const Screen::Pen &info) return; } - new (L) Pen(info); + Lua::make_lua_userdata(L, info); lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN); lua_setmetatable(L, -2); @@ -1128,7 +1128,7 @@ static int dfhack_random_init(lua_State *L) static int dfhack_random_new(lua_State *L) { - new (L) MersenneRNG(); + Lua::make_lua_userdata(L); lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_RANDOM_TOKEN); lua_setmetatable(L, -2); @@ -1212,19 +1212,19 @@ static int dfhack_random_perlin(lua_State *L) switch (size) { case 1: { - auto pdata = new (L) PerlinNoise1D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_1, 1); break; } case 2: { - auto pdata = new (L) PerlinNoise2D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_2, 1); break; } case 3: { - auto pdata = new (L) PerlinNoise3D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_3, 1); break; diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 8c4a17f8dbe..8a46e992f9f 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -168,12 +168,15 @@ static void BuildTypeMetatable(lua_State *state, const type_identity *type); void LuaWrapper::push_object_ref(lua_State *state, void *ptr) { // stack: [metatable] - auto ref = (DFRefHeader*)lua_newuserdata(state, sizeof(DFRefHeader)); - ref->ptr = ptr; - ref->field_info = NULL; - ref->tag_ptr = NULL; - ref->tag_identity = NULL; - ref->tag_attr = NULL; + void* stg = lua_newuserdata(state, sizeof(DFRefHeader)); + auto ref = new (stg) DFRefHeader + { + .ptr = ptr, + .field_info = NULL, + .tag_ptr = NULL, + .tag_identity = NULL, + .tag_attr = NULL, + }; lua_swap(state); lua_setmetatable(state, -2); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 429a8bfc6bb..db40e76810d 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -44,11 +44,6 @@ distribution. #include #include -/// Allocate a new user data object and push it on the stack -inline void *operator new (std::size_t size, lua_State *L) { - return lua_newuserdata(L, size); -} - namespace DFHack { class function_identity_base; struct MaterialInfo; @@ -67,6 +62,20 @@ namespace DFHack::Lua { */ DFHACK_EXPORT lua_State *Open(color_ostream &out, lua_State *state = NULL); + /** + * Allocate a lua userdata and construct a C++ object in that userdata's storage space + * The C++ object must be trivially destructible as lua GC will _not_ call the object's destructor + * be aware that the created userdata is left on the Lua stack as well as returned to the caller + */ + template + requires (std::is_trivially_destructible_v) + T* make_lua_userdata(lua_State* L, Args&&... args) + { + void* stg = lua_newuserdata(L, sizeof(T)); + T * obj = ::new (stg) T(std::forward(args)...); + return obj; + } + DFHACK_EXPORT void PushDFHack(lua_State *state); DFHACK_EXPORT void PushBaseGlobals(lua_State *state); From 776b4d421a5b4f54d73e6fd66a76034b4d31fd81 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 7 Jun 2025 14:34:04 -0500 Subject: [PATCH 355/919] remove unused variable --- library/LuaWrapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 8a46e992f9f..a0069fbbdc4 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -169,7 +169,7 @@ void LuaWrapper::push_object_ref(lua_State *state, void *ptr) { // stack: [metatable] void* stg = lua_newuserdata(state, sizeof(DFRefHeader)); - auto ref = new (stg) DFRefHeader + new (stg) DFRefHeader { .ptr = ptr, .field_info = NULL, From 7c62106aac7b98432f4183cbf9ca642a1d26f2fc Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 25 Jun 2025 07:21:39 +0000 Subject: [PATCH 356/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index d65c4ac5602..eeb00caa9bb 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit d65c4ac560264c4a0d69a802bc31b636c3da55fc +Subproject commit eeb00caa9bba4655e5095e1a767f4beee52bac8f From c38138192bdd06b6811754e4a6887d2ad50a834e Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 15 Jun 2025 19:53:27 -0700 Subject: [PATCH 357/919] Update plant.rst - Link to regrass tool --- docs/plugins/plant.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plugins/plant.rst b/docs/plugins/plant.rst index 51eedaf9999..2a2d6327e0f 100644 --- a/docs/plugins/plant.rst +++ b/docs/plugins/plant.rst @@ -9,7 +9,8 @@ Grow and remove shrubs or trees. Modes are ``list``, ``create``, ``grow``, and ``remove``. ``list`` prints a list of all valid shrub and sapling raw IDs. ``create`` allows the creation of new shrubs and saplings. ``grow`` adjusts the age of saplings and trees, allowing them to grow instantly. ``remove`` can -remove existing shrubs and saplings. +remove existing shrubs and saplings. For operating on grasses (such as bamboo) +use `regrass` instead. Usage ----- From dca9aded12c2b02bae4d57422050dd01cf6aae16 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 04:54:57 -0700 Subject: [PATCH 358/919] remove reference to unused SquadSupplies gui state --- library/modules/Gui.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ff635ac4188..f71654c024c 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -762,11 +762,6 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.squad_supplies.open) { - newFocusString = baseFocus; - newFocusString += "/SquadSupplies"; - focusStrings.push_back(newFocusString); - } if (game->main_interface.squads.open) { newFocusString = baseFocus; newFocusString += "/Squads"; From 95dc5f083e7c83910319eed81fe8b81b5c146874 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:57:39 +0000 Subject: [PATCH 359/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f48fd9ef436..9c0222d1c17 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f48fd9ef4369027499da28441adaf9c572431891 +Subproject commit 9c0222d1c17b66599ee57362c847ab78ee99723f From d7e65b07edad89df75d0a457aba5cc5d2c9f6efd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:12:08 -0700 Subject: [PATCH 360/919] add missing cases to timer switch --- library/modules/Units.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index ce135b477d3..744a3979af2 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2078,6 +2078,14 @@ static int32_t *getActionTimerPointer(df::unit_action *action) { return &action->data.dismount.timer; case unit_action_type::HoldItem: return &action->data.holditem.timer; + case unit_action_type::LoadRangedWeapon: + return &action->data.loadrangedweapon.movewait; + case unit_action_type::ShootRangedWeapon: + return &action->data.shootrangedweapon.movewait; + case unit_action_type::ThrowItem: + return &action->data.throwitem.movewait; + case unit_action_type::PostShootRecovery: + return &action->data.postshootrecovery.movewait; case unit_action_type::LeadAnimal: case unit_action_type::StopLeadAnimal: case unit_action_type::Jump: From fb6f8bb5d4e6d4f54d4e9ded1e2bf7a7ae39b14f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:22:27 -0700 Subject: [PATCH 361/919] remove reference to removed think_counter field --- plugins/timestream.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index eeb4d1115c2..a5e5b04c808 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -341,7 +341,6 @@ static void decrement_counter(T *obj, FT T::*field, int32_t timeskip) { static void adjust_unit_counters(df::unit * unit, int32_t timeskip) { auto * c1 = &unit->counters; - decrement_counter(c1, &df::unit::T_counters::think_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::job_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::swap_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::winded, timeskip); From 055930a83ebc00b99481159dbbeecc62cb1b805b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:31:45 -0700 Subject: [PATCH 362/919] bump version to 51.12 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 16 ++++++++++++++-- library/xml | 2 +- scripts | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2d1c8ec9e0..6c2e619ad3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.11") -set(DFHACK_RELEASE "r1.2") +set(DF_VERSION "51.12") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index abd1b845fc0..9c4a419513d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,20 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 51.12-r1 + ## Fixes - `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed - `getplants`: use updated formula for calculating whether plant growths are ripe @@ -72,8 +86,6 @@ Template for new versions: - `blueprint`: support for recording zones - `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration -## Documentation - ## API - ``Random`` module: added ``SplitmixRNG`` class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness - ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting diff --git a/library/xml b/library/xml index 9c0222d1c17..9dac571e64b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9c0222d1c17b66599ee57362c847ab78ee99723f +Subproject commit 9dac571e64bb99663f753ba5fe625fc79d0b2aeb diff --git a/scripts b/scripts index e50474efbfa..8a2c8e7b13d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e50474efbfa514fd0e4b63cd9a8c114ccf0bb392 +Subproject commit 8a2c8e7b13d703da672532d408e4250dd11a86c9 From cbbbde6c6ac20dfe793e2b5496fda8711794b319 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:40:10 +0000 Subject: [PATCH 363/919] Auto-update structures ref for 51.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 9dac571e64b..ece08d36c4f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9dac571e64bb99663f753ba5fe625fc79d0b2aeb +Subproject commit ece08d36c4f08dac9822a4ededa78a2f8b5532a9 From e3165854e80411eec584c74949c73000f2aa0dcb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:44:39 -0700 Subject: [PATCH 364/919] ignore "dest" install target --- build/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/build/.gitignore b/build/.gitignore index f25d10ca7b9..11f87d8c3eb 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -7,3 +7,4 @@ _CPack_Packages *.tar.* .cmake win64-cross +dest From a10a0888e79c0b4f826d0676d62fd8af88a77089 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:14:02 +0000 Subject: [PATCH 365/919] Auto-update structures ref for 51.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ece08d36c4f..b2996207313 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ece08d36c4f08dac9822a4ededa78a2f8b5532a9 +Subproject commit b299620731342affa32414de855673fa54fabe6f From ecb68285e4e463dd2650be7e7857d94f85d46825 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:29:51 +0000 Subject: [PATCH 366/919] Auto-update structures ref for 51.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index b2996207313..f18388f097c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b299620731342affa32414de855673fa54fabe6f +Subproject commit f18388f097c752cbd54ac791d687237e0b88cb5b From 5a635afc2e76c9f5d2a08cdc656531504bc02dd3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Jun 2025 14:44:02 -0500 Subject: [PATCH 367/919] bump visual studio version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c2e619ad3c..361965522fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1943) + set(MSVC_MAX_VER 1944) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) From 64222555f1834a99e5d575cf2b308a4c25a33287 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 12:45:45 -0700 Subject: [PATCH 368/919] update to 51.12-r1.1 for itch support --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 361965522fe..fafa5bdcc02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "51.12") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 2d61c2083a785fde02d219da466e67f361a34004 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 13:15:43 -0700 Subject: [PATCH 369/919] add missing changelog entry for 1.1 --- docs/changelog.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9c4a419513d..486cb911b8f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,12 @@ Template for new versions: ## Removed +# 51.12-r1.1 + +## New Features + +- Compatibility with Itch release of DF 51.12 + # 51.12-r1 ## Fixes From 00aeceb937f0ba50d9342d05928b675af3feb98d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 16:56:58 -0700 Subject: [PATCH 370/919] bump version to 51.13-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fafa5bdcc02..d8c4144655b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.12") -set(DFHACK_RELEASE "r1.1") +set(DF_VERSION "51.13") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 486cb911b8f..b90b39a3320 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,12 @@ Template for new versions: ## Removed +# 51.13-r1 + +## New Features + +- Compatibility with DF 51.13 + # 51.12-r1.1 ## New Features From fac6d9b4b00baf4046bf3087134c09972f116469 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:01:40 +0000 Subject: [PATCH 371/919] Auto-update structures ref for 51.13 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f18388f097c..309450b08ad 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f18388f097c752cbd54ac791d687237e0b88cb5b +Subproject commit 309450b08ad4c7b20470a9833b7b251ea64d8639 From 8cc84672058102afe14e6196ee343d1c792ca4a4 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:09:43 +0000 Subject: [PATCH 372/919] Auto-update structures ref for 51.13 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 309450b08ad..ae7424d3f40 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 309450b08ad4c7b20470a9833b7b251ea64d8639 +Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 From 4457a8cc3ed967bb4e393224df103a054f894048 Mon Sep 17 00:00:00 2001 From: Quietust Date: Sat, 31 May 2025 14:48:08 -0600 Subject: [PATCH 373/919] Add "animaltrap-reuse" tweak Makes it so animal traps generate "Release Small Creature" jobs instead of "Destroy Building" jobs, so a dwarf will transfer the caught vermin into an empty stockpiled trap and leave the built trap ready to be automatically filled with new bait. --- docs/changelog.txt | 1 + docs/plugins/tweak.rst | 3 + plugins/tweak/tweak.cpp | 3 + plugins/tweak/tweaks/animaltrap-reuse.h | 84 +++++++++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 plugins/tweak/tweaks/animaltrap-reuse.h diff --git a/docs/changelog.txt b/docs/changelog.txt index b90b39a3320..fa559df8eaa 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: ## New Tools ## New Features +- `tweak`: ``animaltrap-reuse``: make it so built animal traps automatically unload the vermin they catch into stockpiled animal traps, so that they can be automatically re-baited and reused ## Fixes diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 87361ef670e..ae42aecac78 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -38,6 +38,9 @@ Commands ``adamantine-cloth-wear`` Prevents adamantine clothing from wearing out while being worn (:bug:`6481`). +``animaltrap-reuse`` + Makes built animal traps unload caught vermin into stockpiled animal traps + so that they can be automatically re-baited and reused. ``craft-age-wear`` Fixes crafted items not wearing out over time (:bug:`6003`). With this tweak, items made from cloth and leather will gain a level of wear every 20 diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 74276bba1cf..dd7edbf3e1d 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -14,6 +14,7 @@ using std::string; using namespace DFHack; #include "tweaks/adamantine-cloth-wear.h" +#include "tweaks/animaltrap-reuse.h" #include "tweaks/craft-age-wear.h" #include "tweaks/eggs-fertile.h" #include "tweaks/fast-heat.h" @@ -53,6 +54,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector 1) && (contained_items[1]->item->getType() == df::item_type::VERMIN)) + { + auto trap = contained_items[0]->item; + auto vermin = contained_items[1]->item; + + // Make sure we don't already have a "Release Small Creature" job + for (size_t j = 0; j < jobs.size(); j++) + { + if (jobs[j]->job_type == df::job_type::ReleaseSmallCreature) + return; + // Also bail out if the player marked the building for destruction + if (jobs[j]->job_type == df::job_type::DestroyBuilding) + return; + } + + // Create the job + auto job = new df::job(); + Job::linkIntoWorld(job, true); + + job->job_type = df::job_type::ReleaseSmallCreature; + job->pos.x = centerx; + job->pos.y = centery; + job->pos.z = z; + + // Attach the vermin to the job + Job::attachJobItem(job, vermin, df::job_role_type::Hauled, -1, -1); + + // Link the job to the building + df::general_ref *ref = df::allocate(); + ref->setID(id); + job->general_refs.push_back(ref); + + jobs.push_back(job); + + // Hack: put the vermin inside the trap ITEM right away, otherwise the job will cancel. + // Normally, this doesn't happen until after the trap is deconstructed, but the game still + // seems to handle everything correctly and doesn't leave any bad references afterwards. + if (!Items::getContainer(vermin)) + { + // We can't use Items::moveToContainer, because that would remove it from the Building + // (and cause the game to no longer recognize the trap as being "full"). + // Instead, manually add the references and set the necessary bits. + + ref = df::allocate(); + ref->setID(trap->id); + vermin->general_refs.push_back(ref); + + ref = df::allocate(); + ref->setID(vermin->id); + trap->general_refs.push_back(ref); + + vermin->flags.bits.in_inventory = true; + trap->flags.bits.weight_computed = false; + // Don't set this flag here (even though it would normally get set), + // since the game doesn't clear it after the vermin gets taken out + // trap->flags.bits.container = true; + } + return; + } + INTERPOSE_NEXT(updateAction)(); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(animaltrap_reuse_hook, updateAction); From a94eb538bf3273609163491e5f7bb79c1357fc4c Mon Sep 17 00:00:00 2001 From: Quietust Date: Sat, 21 Jun 2025 10:09:00 -0600 Subject: [PATCH 374/919] Add "count" parameter to Items::createItem and update the "createitem" plugin to specify it instead of creating a bunch of individual items in a loop. --- docs/changelog.txt | 1 + library/LuaApi.cpp | 5 ++++- library/include/modules/Items.h | 2 +- library/modules/Items.cpp | 4 ++-- plugins/createitem.cpp | 16 +++++++--------- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b90b39a3320..539b2c22403 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -90,6 +90,7 @@ Template for new versions: - `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit - fixed an overly restrictive type constraint that resulted in some object types being glossed as a boolean when passed as an argument from C++ to Lua - `plants`: will no longer generate a traceback when a filter is used +- `createitem`: multiple items should be properly created in stacks again ## Misc Improvements - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1d77ebf15a1..1d3f4075a28 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2461,8 +2461,11 @@ static int items_createItem(lua_State *state) auto mat_type = lua_tointeger(state, 4); auto mat_index = lua_tointeger(state, 5); bool no_floor = lua_toboolean(state, 6); + int count = lua_tointeger(state, 7); + if (count < 1) + count = 1; vector out_items; - Items::createItem(out_items, unit, item_type, item_subtype, mat_type, mat_index, no_floor); + Items::createItem(out_items, unit, item_type, item_subtype, mat_type, mat_index, no_floor, count); Lua::PushVector(state, out_items); return 1; } diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 6d068e8786d..1189fd8bc28 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -173,7 +173,7 @@ DFHACK_EXPORT int getItemBaseValue(int16_t item_type, int16_t item_subtype, int1 DFHACK_EXPORT int getValue(df::item *item, df::caravan_state *caravan = NULL); DFHACK_EXPORT bool createItem(std::vector &out_items, df::unit *creator, df::item_type type, - int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false); + int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false, int32_t count = 1); // Returns true if the item is free from mandates, or false if mandates prevent trading the item. DFHACK_EXPORT bool checkMandates(df::item *item); diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index a161bea8a2d..5b9e781721a 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1752,7 +1752,7 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { } bool Items::createItem(vector &out_items, df::unit *unit, df::item_type item_type, - int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor) + int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor, int32_t count) { // Based on Quietust's plugins/createitem.cpp CHECK_NULL_POINTER(unit); auto pos = Units::getPosition(unit); @@ -1765,7 +1765,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t prod->mat_type = mat_type; prod->mat_index = mat_index; prod->probability = 100; - prod->count = 1; + prod->count = count; switch(item_type) { using namespace df::enums::item_type; diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 1f4b5820472..fba821b2703 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -50,7 +50,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } -bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_type, int32_t mat_index, +bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_type, int32_t mat_index, int32_t count, bool move_to_cursor = false, bool second_item = false) { // Special logic for making Gloves and Shoes in pairs bool is_gloves = (type == item_type::GLOVES); @@ -66,7 +66,7 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t bool on_floor = (container == NULL) && (building == NULL) && !move_to_cursor; vector out_items; - if (!Items::createItem(out_items, unit, type, subtype, mat_type, mat_index, !on_floor)) + if (!Items::createItem(out_items, unit, type, subtype, mat_type, mat_index, !on_floor, count)) return false; for (size_t i = 0; i < out_items.size(); i++) { @@ -105,7 +105,7 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t is_shoes = false; // If we asked for gloves/shoes and only got one (and we're making the first one), make another if ((is_gloves || is_shoes) && !second_item) - return makeItem(unit, type, subtype, mat_type, mat_index, move_to_cursor, true); + return makeItem(unit, type, subtype, mat_type, mat_index, count, move_to_cursor, true); return true; } @@ -435,12 +435,10 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { out.printerr("Previously selected building no longer exists - item will be placed on the floor.\n"); } - for (int i = 0; i < count; i++) { - if (!makeItem(unit, item_type, item_subtype, mat_type, mat_index, move_to_cursor, false)) - { - out.printerr("Failed to create item!\n"); - return CR_FAILURE; - } + if (!makeItem(unit, item_type, item_subtype, mat_type, mat_index, count, move_to_cursor, false)) + { + out.printerr("Failed to create item!\n"); + return CR_FAILURE; } return CR_OK; } From 910d0d48d91e740057c59f755986e0729e157408 Mon Sep 17 00:00:00 2001 From: myk002 <977482+myk002@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:30:45 +0000 Subject: [PATCH 375/919] Auto-update structures ref for 51.13-lua+filesystem-test-1 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ae7424d3f40..f3068890126 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 +Subproject commit f30688901262aabef2c9dec0306e78a5b7350434 From c2d74714b6c5fb0c682c6f78a8f6c442af625a31 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 29 Jun 2025 07:19:34 +0000 Subject: [PATCH 376/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f3068890126..ae7424d3f40 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f30688901262aabef2c9dec0306e78a5b7350434 +Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 From 3ea3cb806040800ac838b571e0c142ce50bf55e7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:00:07 +0000 Subject: [PATCH 377/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8a2c8e7b13d..6f348f7d926 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8a2c8e7b13d703da672532d408e4250dd11a86c9 +Subproject commit 6f348f7d9267559e48ca6703d68eb234b4f8a8f1 From e76b960fc49a21f6e069822c4a21d9ea0abcf4e7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 2 Jul 2025 00:47:23 +0000 Subject: [PATCH 378/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6f348f7d926..ba62d8e5519 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6f348f7d9267559e48ca6703d68eb234b4f8a8f1 +Subproject commit ba62d8e5519e43fd777bca426ec62eb5bcd5ec9e From e89efb4f156aa1ded28b7759f1a7528ea530bf2c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Jul 2025 07:21:03 +0000 Subject: [PATCH 379/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ba62d8e5519..10a04ab91a5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ba62d8e5519e43fd777bca426ec62eb5bcd5ec9e +Subproject commit 10a04ab91a50b357574b106df2cff96389e61ac3 From 62343fbb9c2ff9d0dce2c43e2f8a2cb88106d5ac Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 07:53:57 -0500 Subject: [PATCH 380/919] `Units::getReadableName`: do not add commas to histfigs with no profession fixes #5474 --- docs/changelog.txt | 1 + library/modules/Units.cpp | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 539b2c22403..25e4e639b3b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession ## Misc Improvements diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 744a3979af2..90fb8b593a0 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1161,14 +1161,16 @@ static string formatReadableName(T *unit_or_hf, const string &prof_name, bool sk if (native_name.empty()) return prof_name; + std::string prof_suffix{ (prof_name.size() > 0) ? (", " + prof_name) : "" }; + if (skip_english) - return native_name + ", " + prof_name; + return native_name + prof_suffix; string english_name = Translation::translateName(visible_name, true, true); if (english_name.empty()) - return native_name + ", " + prof_name; + return native_name + prof_suffix; - return native_name + " \"" + english_name + "\", " + prof_name; + return native_name + " \"" + english_name + "\"" + prof_suffix; } string Units::getReadableName(df::historical_figure *hf, bool skip_english) { From d458d8ac6d0c12df74de194e524cd13092a704ba Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Jul 2025 09:08:12 -0500 Subject: [PATCH 381/919] Update changelog.txt --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a5e4a3fcab6..e4ea864383c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,7 +54,6 @@ Template for new versions: ## New Tools ## New Features -- ``widgets.Slider``: new mouse-controlled single-headed slider widget ## Fixes @@ -65,6 +64,7 @@ Template for new versions: ## API ## Lua +- ``widgets.Slider``: new mouse-controlled single-headed slider widget ## Removed From 6707e20d9062483aa314c798fc21251dce9d2d81 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Thu, 3 Jul 2025 11:43:30 -0500 Subject: [PATCH 382/919] clean up the sliders and how they work. i have split out as much of the logic as possible. couldnt split out the `get_width_per_odx` bc we need the `frame_body` to not be nil. --- library/lua/gui/widgets/range_slider.lua | 128 +----------------- library/lua/gui/widgets/slide_core.lua | 163 +++++++++++++++++++++++ library/lua/gui/widgets/slider.lua | 70 +++++++--- 3 files changed, 224 insertions(+), 137 deletions(-) create mode 100644 library/lua/gui/widgets/slide_core.lua diff --git a/library/lua/gui/widgets/range_slider.lua b/library/lua/gui/widgets/range_slider.lua index f55f0e00f05..9b44e0fd54d 100644 --- a/library/lua/gui/widgets/range_slider.lua +++ b/library/lua/gui/widgets/range_slider.lua @@ -1,17 +1,15 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse +local core = require('gui.widgets.slide_core') -------------------------------- -- RangeSlider -------------------------------- ---@class widgets.RangeSlider.attrs: widgets.Widget.attrs ----@field num_stops integer ---@field get_left_idx_fn? function ---@field get_right_idx_fn? function ---@field on_left_change? fun(index: integer) ---@field on_right_change? fun(index: integer) +---@field is_single boolean ---@class widgets.RangeSlider.attrs.partial: widgets.RangeSlider.attrs @@ -22,28 +20,17 @@ local to_pen = dfhack.pen.parse ---@field super widgets.Widget ---@field ATTRS widgets.RangeSlider.attrs|fun(attributes: widgets.RangeSlider.attrs.partial) ---@overload fun(init_table: widgets.RangeSlider.initTable): self -RangeSlider = defclass(RangeSlider, Widget) +RangeSlider = defclass(RangeSlider, slide_core) RangeSlider.ATTRS{ - num_stops=DEFAULT_NIL, get_left_idx_fn=DEFAULT_NIL, get_right_idx_fn=DEFAULT_NIL, on_left_change=DEFAULT_NIL, on_right_change=DEFAULT_NIL, - is_single=DEFAULT_NIL + num_stops=DEFAULT_NIL, + is_single=false, } -function RangeSlider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function RangeSlider:init() - if self.num_stops < 2 then error('too few RangeSlider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function rangeslider_get_width_per_idx(self) +function RangeSlider:get_width_per_idx() local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) end @@ -53,7 +40,7 @@ function RangeSlider:onInput(keys) local x = self:getMousePos() if not x then return false end local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = rangeslider_get_width_per_idx(self) + local width_per_idx = self:get_width_per_idx() local left_pos = width_per_idx*(left_idx-1) local right_pos = width_per_idx*(right_idx-1) + 4 if x < left_pos then @@ -73,105 +60,4 @@ function RangeSlider:onInput(keys) return true end -local function rangeslider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = self.is_dragging_target == 'right' and -2 or 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - local new_left_idx, new_right_idx - if self.is_dragging_target == 'right' then - new_right_idx = new_idx - else - new_left_idx = new_idx - if self.is_dragging_target == 'both' then - new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() - if new_right_idx > self.num_stops then - return - end - end - end - if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then - if not new_right_idx and new_left_idx > self.get_right_idx_fn() then - self.on_right_change(new_left_idx) - end - self.on_left_change(new_left_idx) - end - if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then - if new_right_idx < self.get_left_idx_fn() then - self.on_left_change(new_right_idx) - end - self.on_right_change(new_right_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function RangeSlider:onRenderBody(dc, rect) - local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = rangeslider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx > stop_idx or right_idx < stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif right_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if right_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - - -- Draw tab(s) - if self.is_single then - -- Single slider: Draw one centered tab - dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - else - -- Dual slider: Draw left and right tabs separately - dc:seek(width_per_idx * (left_idx-1)) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - dc:seek(width_per_idx*(right_idx-1)+4) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - end - - -- Manage dragging - if self.is_dragging_target then - rangeslider_do_drag(self, width_per_idx) - end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - return RangeSlider diff --git a/library/lua/gui/widgets/slide_core.lua b/library/lua/gui/widgets/slide_core.lua new file mode 100644 index 00000000000..450954b9c12 --- /dev/null +++ b/library/lua/gui/widgets/slide_core.lua @@ -0,0 +1,163 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- slide_core +-------------------------------- + +---@class widgets.slide_core.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field is_single boolean +---@field w integer + +---@class widgets.slide_core.attrs.partial: widgets.slide_core.attrs + +---@class widgets.slide_core.initTable: widgets.slide_core.attrs +---@field num_stops integer + +---@class widgets.slide_core: widgets.Widget, widgets.slide_core.attrs +---@field super widgets.Widget +---@field ATTRS widgets.slide_core.attrs|fun(attributes: widgets.slide_core.attrs.partial) +---@overload fun(init_table: widgets.slide_core.initTable): self +slide_core = defclass(slide_core, Widget) +slide_core.ATTRS{ + num_stops=DEFAULT_NIL, + is_single=DEFAULT_NIL, + w=DEFAULT_NIL +} + +function slide_core:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function slide_core:init() + local min_stops = self:get_min_stops() + if self.num_stops < min_stops then error(('too few stops, expected at least %s'):format(min_stops)) end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +function slide_core:get_min_stops() + return self.is_single and 1 or 2 +end + +local function do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = self.is_dragging_target == 'right' and -2 or 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + local new_left_idx, new_right_idx + if self.is_dragging_target == 'right' then + new_right_idx = new_idx + else + new_left_idx = new_idx + if self.is_dragging_target == 'both' then + if self.get_left_idx_fn ~= nil then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + else + new_right_idx = new_left_idx + end + if new_right_idx > self.num_stops then + return + end + end + end + if self.get_idx_fn == nil then + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + if not new_right_idx and new_left_idx > self.get_right_idx_fn() then + self.on_right_change(new_left_idx) + end + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + if new_right_idx < self.get_left_idx_fn() then + self.on_left_change(new_right_idx) + end + self.on_right_change(new_right_idx) + end + else + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function slide_core:onRenderBody(dc, rect) + local left_idx, right_idx + if self.get_idx_fn ~= nil then + left_idx = self.get_idx_fn() + right_idx = left_idx + else + left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + end + local width_per_idx = self:get_width_per_idx() + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx > stop_idx or right_idx < stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif right_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if right_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + + -- Draw tab(s) + if self.is_single then + -- Single slider: Draw one centered tab + dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + else + -- Dual slider: Draw left and right tabs separately + dc:seek(width_per_idx * (left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + end + + -- Manage dragging + if self.is_dragging_target then + do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return slide_core diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua index 23bf6f62029..65ce28a5c71 100644 --- a/library/lua/gui/widgets/slider.lua +++ b/library/lua/gui/widgets/slider.lua @@ -1,22 +1,60 @@ --- slider.lua -local RangeSlider = require('gui.widgets.range_slider') - -local Slider = defclass(Slider, RangeSlider) -Slider.ATTRS { - get_idx_fn = DEFAULT_NIL, -- Function to get the current index - on_change = DEFAULT_NIL, -- Function to handle index change - is_single = true +local core = require('gui.widgets.slide_core') + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field get_idx_fn? function +---@field on_change? fun(index: integer) +---@field is_single boolean + + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, slide_core) +Slider.ATTRS{ + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, + num_stops=DEFAULT_NIL, + is_single=true, } -function Slider:init() - self.get_left_idx_fn = self.get_idx_fn - self.get_right_idx_fn = self.get_idx_fn - self.on_left_change = function(index) - if self.on_change then self.on_change(index) end - end - self.on_right_change = function(index) - if self.on_change then self.on_change(index) end +function Slider:get_width_per_idx() + local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 + return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_idx_fn(), self.get_idx_fn() + local width_per_idx = self:get_width_per_idx() + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(right_idx-1) + 4 + if x < left_pos then + self.on_change(self.get_idx_fn() - 1) + elseif x < left_pos+3 then + self.is_dragging_target = 'left' + self.is_dragging_idx = x - left_pos + elseif x < right_pos then + self.is_dragging_target = 'both' + self.is_dragging_idx = x - left_pos + elseif x < right_pos+3 then + self.is_dragging_target = 'right' + self.is_dragging_idx = x - right_pos + else + self.on_change(self.get_idx_fn() + 1) end + return true end return Slider From f4057eee5745cf4972b95f248689d2ca741502f1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 16:26:58 -0500 Subject: [PATCH 383/919] remove fake `curse` compound in `unitst` resolves an alignment issue in `unitst` --- library/RemoteTools.cpp | 24 +++++++++---------- library/modules/Units.cpp | 16 ++++++------- library/xml | 2 +- plugins/cursecheck.cpp | 50 +++++++++++++++++++-------------------- plugins/lua/sort.lua | 4 ++-- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 3df55b847a9..56b45eba015 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -336,22 +336,22 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit, } } - if (unit->curse.add_tags1.whole || - unit->curse.add_tags2.whole || - unit->curse.rem_tags1.whole || - unit->curse.rem_tags2.whole || - unit->curse.name_visible) + if (unit->uwss_add_caste_flag.whole || + unit->uwss_remove_caste_flag.whole || + unit->uwss_add_property.whole || + unit->uwss_remove_property.whole || + unit->uwss_use_display_name) { auto curse = info->mutable_curse(); - curse->set_add_tags1(unit->curse.add_tags1.whole); - curse->set_rem_tags1(unit->curse.rem_tags1.whole); - curse->set_add_tags2(unit->curse.add_tags2.whole); - curse->set_rem_tags2(unit->curse.rem_tags2.whole); + curse->set_add_tags1(unit->uwss_add_caste_flag.whole); + curse->set_rem_tags1(unit->uwss_add_property.whole); + curse->set_add_tags2(unit->uwss_remove_caste_flag.whole); + curse->set_rem_tags2(unit->uwss_remove_property.whole); - if (unit->curse.name_visible) - describeNameTriple(curse->mutable_name(), unit->curse.name, - unit->curse.name_plural, unit->curse.name_adjective); + if (unit->uwss_use_display_name) + describeNameTriple(curse->mutable_name(), unit->uwss_display_name_sing, + unit->uwss_display_name_plur, unit->uwss_display_name_adj); } for (size_t i = 0; i < unit->burrows.size(); i++) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 744a3979af2..89a1cbf0262 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -108,8 +108,8 @@ using df::global::gametype; using df::global::plotinfo; using df::global::world; -#define IS_ACTIVE_CASTE_FLAG(cf) !unit->curse.rem_tags1.bits.cf && \ -(unit->curse.add_tags1.bits.cf || casteFlagSet(unit->race, unit->caste, caste_raw_flags::cf)) +#define IS_ACTIVE_CASTE_FLAG(cf) !unit->uwss_remove_caste_flag.bits.cf && \ +(unit->uwss_add_caste_flag.bits.cf || casteFlagSet(unit->race, unit->caste, caste_raw_flags::cf)) // Common flags to exclude for fort members constexpr uint32_t exclude_flags1 = ( @@ -208,7 +208,7 @@ bool Units::isOwnRace(df::unit *unit) { bool Units::isAlive(df::unit *unit) { CHECK_NULL_POINTER(unit); - return !isDead(unit) && !unit->curse.add_tags1.bits.NOT_LIVING; + return !isDead(unit) && !unit->uwss_add_caste_flag.bits.NOT_LIVING; } bool Units::isDead(df::unit *unit) { @@ -606,7 +606,7 @@ bool Units::isInvader(df::unit *unit) { bool Units::isUndead(df::unit *unit, bool hiding_curse) { CHECK_NULL_POINTER(unit); - const auto &cb = unit->curse.add_tags1.bits; + const auto &cb = unit->uwss_add_caste_flag.bits; return unit->flags3.bits.ghostly || ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (hiding_curse || !isHidingCurse(unit))); } @@ -1014,7 +1014,7 @@ int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr auto &aobj = unit->body.physical_attrs[attr]; int value = max(0, aobj.value - aobj.soft_demotion); - if (auto mod = unit->curse.attr_change) + if (auto mod = unit->uwss_att_change) { int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; if (isHidingCurse(unit)) @@ -1033,7 +1033,7 @@ int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) { auto &aobj = soul->mental_attrs[attr]; int value = max(0, aobj.value - aobj.soft_demotion); - if (auto mod = unit->curse.attr_change) + if (auto mod = unit->uwss_att_change) { int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; if (isHidingCurse(unit)) @@ -1192,8 +1192,8 @@ string Units::getReadableName(df::unit *unit, bool skip_english) { CHECK_NULL_POINTER(unit); string prof_name = getProfessionName(unit, false, false, true); - if (unit->curse.name_visible) // Necromancer, etc. - prof_name += " " + unit->curse.name; + if (unit->uwss_use_display_name) // Necromancer, etc. + prof_name += " " + unit->uwss_display_name_sing; else if (isGhost(unit)) // TODO: Should be "Ghost" instead of "Ghostly Peasant" prof_name = "Ghostly " + prof_name; // TODO: impersonating deity/force diff --git a/library/xml b/library/xml index ae7424d3f40..672894f464e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ae7424d3f40bf60b3906b5b9b472cb9a7209a3a8 +Subproject commit 672894f464e84e1b3f8cfb911bf0ffe2af11695a diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index a8ec36f9bab..0762ad9c36e 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -90,15 +90,15 @@ curses determineCurse(df::unit * unit) cursetype = curses::Ghost; // zombies: undead or hate life (according to ag), not bloodsuckers - if( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING) - && !unit->curse.add_tags1.bits.BLOODSUCKER ) + if( (unit->uwss_add_caste_flag.bits.OPPOSED_TO_LIFE || unit->uwss_add_caste_flag.bits.NOT_LIVING) + && !unit->uwss_add_caste_flag.bits.BLOODSUCKER) cursetype = curses::Zombie; // necromancers: alive, don't eat, don't drink, don't age - if(!unit->curse.add_tags1.bits.NOT_LIVING - && unit->curse.add_tags1.bits.NO_EAT - && unit->curse.add_tags1.bits.NO_DRINK - && unit->curse.add_tags2.bits.NO_AGING + if (!unit->uwss_add_caste_flag.bits.NOT_LIVING + && unit->uwss_add_caste_flag.bits.NO_EAT + && unit->uwss_add_caste_flag.bits.NO_DRINK + && unit->uwss_add_property.bits.NO_AGING ) cursetype = curses::Necromancer; @@ -108,21 +108,21 @@ curses determineCurse(df::unit * unit) auto syndrome = df::syndrome::find(active_syndrome->type); if (syndrome) { for (auto classname : syndrome->syn_class) - if (classname && *classname == "WERECURSE") { - cursetype = curses::Werebeast; - break; - } + if (classname && *classname == "WERECURSE") { + cursetype = curses::Werebeast; + break; + } } } // vampires: bloodsucker (obvious enough) - if(unit->curse.add_tags1.bits.BLOODSUCKER) + if (unit->uwss_add_caste_flag.bits.BLOODSUCKER) cursetype = curses::Vampire; return cursetype; } -command_result cursecheck (color_ostream &out, vector & parameters) +command_result cursecheck(color_ostream& out, vector & parameters) { df::unit* selected_unit = Gui::getSelectedUnit(out, true); @@ -133,19 +133,19 @@ command_result cursecheck (color_ostream &out, vector & parameters) bool verbose = false; size_t cursecount = 0; - for(auto parameter : parameters) + for (auto parameter : parameters) { - if(parameter == "help" || parameter == "?") + if (parameter == "help" || parameter == "?") return CR_WRONG_USAGE; - if(parameter == "detail") + if (parameter == "detail") giveDetails = true; - if(parameter == "ids") + if (parameter == "ids") giveUnitID = true; - if(parameter == "nick") + if (parameter == "nick") giveNick = true; - if(parameter == "all") + if (parameter == "all") ignoreDead = false; - if(parameter == "verbose") + if (parameter == "verbose") { // verbose makes no sense without enabling details giveDetails = true; @@ -157,11 +157,11 @@ command_result cursecheck (color_ostream &out, vector & parameters) vector to_check; if (selected_unit) to_check.push_back(selected_unit); - for(df::unit *unit : to_check.size() ? to_check : world->units.all) + for (df::unit* unit : to_check.size() ? to_check : world->units.all) { // filter out all "living" units that are currently removed from play // don't spam all completely dead creatures if not explicitly wanted - if((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead)) + if ((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead)) { continue; } @@ -170,9 +170,9 @@ command_result cursecheck (color_ostream &out, vector & parameters) if (cursetype != curses::None) { - cursecount++; + cursecount++; - if(giveNick) + if (giveNick) { Units::setNickname(unit, curse_names[cursetype]); //"CURSED"); } @@ -196,8 +196,8 @@ command_result cursecheck (color_ostream &out, vector & parameters) if (verbose) { out << "Curse flags: " - << bitfield_to_string(unit->curse.add_tags1) << std::endl - << bitfield_to_string(unit->curse.add_tags2) << std::endl; + << bitfield_to_string(unit->uwss_add_caste_flag) << std::endl + << bitfield_to_string(unit->uwss_add_property) << std::endl; } } diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 5777d2b174e..bdad4ae90e8 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -43,7 +43,7 @@ local function get_active_idx_cache() end local function is_original_dwarf(unit) - return df.global.plotinfo.fortress_age == unit.curse.interaction.time_on_site // 10 + return df.global.plotinfo.fortress_age == unit.usable_interaction.time_on_site // 10 end local WAVE_END_GAP = 10000 @@ -53,7 +53,7 @@ local function get_most_recent_wave_oldest_active_idx(cache) for idx=#active_units-1,0,-1 do local unit = active_units[idx] if not dfhack.units.isCitizen(unit) then goto continue end - if oldest_unit and unit.curse.interaction.time_on_site - oldest_unit.curse.interaction.time_on_site > WAVE_END_GAP then + if oldest_unit and unit.usable_interaction.time_on_site - oldest_unit.usable_interaction.time_on_site > WAVE_END_GAP then return cache[oldest_unit.id] else oldest_unit = unit From 985e2832d93386ce02df29901f9a27b53a9695a6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 16:59:38 -0500 Subject: [PATCH 384/919] repoint scripts --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 10a04ab91a5..1b95334b2ee 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 10a04ab91a50b357574b106df2cff96389e61ac3 +Subproject commit 1b95334b2ee7d40a686be17bd343ecbf55504e4c From 4bf1b130be86acbc05e1ce15438dfe9c6720dfeb Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:34:01 +0000 Subject: [PATCH 385/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 672894f464e..ff2e0604057 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 672894f464e84e1b3f8cfb911bf0ffe2af11695a +Subproject commit ff2e06040572a76afce7311ecffb61f403cc2fbb diff --git a/scripts b/scripts index 1b95334b2ee..8a3b92bc433 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1b95334b2ee7d40a686be17bd343ecbf55504e4c +Subproject commit 8a3b92bc433722d1416b4b18ccd43bdad9307a8d From a561ea62eb7c5cc706d8d9154e5924bd117a8700 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:07:38 -0500 Subject: [PATCH 386/919] Update docs/dev/Lua API.rst Co-authored-by: Myk --- docs/dev/Lua API.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 665b069c666..639c0a9caca 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6514,7 +6514,7 @@ Slider class This widget implements a mouse-interactable slider. The player can move the handle to set the value of the slider. The parent widget owns the slider value, and can control -it independently (e.g., with ``CycleHotkeyLabels``). If the value changes, the ``Slider`` +it independently (e.g., with a ``CycleHotkeyLabel``). If the value changes, the ``Slider`` appearance will adjust automatically. :num_stops: Used to specify the number of "notches" in the slider, the places From c5715533a09372ba6bdc96ea560ba5aad344fb74 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 7 Jul 2025 07:22:23 +0000 Subject: [PATCH 387/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index eeb00caa9bb..15add1726c7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit eeb00caa9bba4655e5095e1a767f4beee52bac8f +Subproject commit 15add1726c711b5b8c4d0ae5afe42b8f841d3af6 From 656e7c385409b15b6ecabc73fbdf6d35603c017c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:40:40 +0000 Subject: [PATCH 388/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.33.0 → 0.33.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.0...0.33.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 937a9de80d5..d79077b318d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.0 + rev: 0.33.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 058e036a554d822814ec510f2cc9a552747f8cae Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 13 Jul 2025 16:31:27 +0800 Subject: [PATCH 389/919] Add arena mode focus strings for widgets --- library/modules/Gui.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index f71654c024c..dedcc6cdbcc 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -90,6 +90,7 @@ distribution. #include "df/viewscreen_dungeonmodest.h" #include "df/viewscreen_dwarfmodest.h" #include "df/viewscreen_legendsst.h" +#include "df/viewscreen_new_arenast.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_setupdwarfgamest.h" #include "df/viewscreen_titlest.h" @@ -188,6 +189,18 @@ DEFINE_GET_FOCUS_STRING_HANDLER(new_region) focusStrings.push_back(baseFocus); } +DEFINE_GET_FOCUS_STRING_HANDLER(new_arena) +{ + if (screen->raw_load) + focusStrings.push_back(baseFocus + "/Loading"); + else if (screen->doing_mods) + focusStrings.push_back(baseFocus + "/Mods"); + + if (focusStrings.empty()) + focusStrings.push_back(baseFocus); + +} + DEFINE_GET_FOCUS_STRING_HANDLER(choose_start_site) { if (screen->doing_site_finder) From 5d7968bdfa1fcc3fdd8736a90ef35de44f0f723f Mon Sep 17 00:00:00 2001 From: Ying Gao Date: Sun, 13 Jul 2025 16:42:17 +0800 Subject: [PATCH 390/919] Update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fa2e9535af..49887c54ad7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Documentation ## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua From af44b489e5084eff25298a3a1b29a694af995628 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:12:32 +0000 Subject: [PATCH 391/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8a3b92bc433..349321c9f16 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8a3b92bc433722d1416b4b18ccd43bdad9307a8d +Subproject commit 349321c9f166670e317d53b7bcdfe5541493da1c From f5312cf6e417060fd3f62e582c64609b03741e09 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 11:40:03 -0500 Subject: [PATCH 392/919] update structures to 52.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ff2e0604057..4c8d4cc8577 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ff2e06040572a76afce7311ecffb61f403cc2fbb +Subproject commit 4c8d4cc8577fcb8fdd3ac99e6156d9d719419bdc From 4034e8d0380091fb9a4d10e5d659fe230f660b61 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 11:42:11 -0500 Subject: [PATCH 393/919] bump version to 52.01 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d8c4144655b..0e1226cd81e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.13") +set(DF_VERSION "52.01") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 101d25a44f0fe17236f81ce00b43e816a8d41698 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:50:06 +0000 Subject: [PATCH 394/919] Auto-update structures ref for 52.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 4c8d4cc8577..1407dfd6076 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4c8d4cc8577fcb8fdd3ac99e6156d9d719419bdc +Subproject commit 1407dfd6076b6cc3ebb64aef17105042fd66e2ca From 749d3b036790daa8218e2b818234d90b7f667b15 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 12:30:46 -0500 Subject: [PATCH 395/919] update changelogs for 52.01-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ library/xml | 2 +- scripts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fa2e9535af..0fe8c6e9f49 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -53,6 +53,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.01-r1 + +## New Tools + ## New Features - `tweak`: ``animaltrap-reuse``: make it so built animal traps automatically unload the vermin they catch into stockpiled animal traps, so that they can be automatically re-baited and reused diff --git a/library/xml b/library/xml index 1407dfd6076..15442adf5c0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1407dfd6076b6cc3ebb64aef17105042fd66e2ca +Subproject commit 15442adf5c0a3cf3c455a888553ff8da30f9b0cd diff --git a/scripts b/scripts index 349321c9f16..9a3cf1a1c13 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 349321c9f166670e317d53b7bcdfe5541493da1c +Subproject commit 9a3cf1a1c13c0def58979f0c6b59aa3939af7099 From 242fee748aea55f45cbb048376b2228e7c111224 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Wed, 23 Jul 2025 23:27:27 -0500 Subject: [PATCH 396/919] Add clear option to autoclothing that will unset a previosly set order --- docs/plugins/autoclothing.rst | 4 ++++ plugins/autoclothing.cpp | 32 +++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/plugins/autoclothing.rst b/docs/plugins/autoclothing.rst index 1ea1a9094f1..02d13c83a73 100644 --- a/docs/plugins/autoclothing.rst +++ b/docs/plugins/autoclothing.rst @@ -22,6 +22,7 @@ Usage autoclothing autoclothing autoclothing + autoclothing clear ```` can be "cloth", "silk", "yarn", or "leather". The ```` can be anything your civilization can produce, such as "dress" or "mitten". @@ -43,6 +44,9 @@ Examples long as there is cloth available to make them out of). ``autoclothing cloth dress`` Displays the currently set number of cloth dresses chosen per citizen. +``autoclothing clear cloth "short skirt"`` + Unsets cloth short skirts from being made if previously enabled such as + in the first example Which should I enable: autoclothing or tailor? ---------------------------------------------- diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index fac721a5c5b..96f81ecd2eb 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -129,6 +129,15 @@ struct ClothingRequirement { bool SetFromParameters(color_ostream &out, vector ¶meters) { + if (parameters[0] == "clear") { // handle the clear case + if (!set_bitfield_field(&material_category, parameters[1], 1)) + out << "Unrecognized material type: " << parameters[1] << endl; + if (!setItem(parameters[2], this)) { + out << "Unrecognized item name or token: " << parameters[2] << endl; + return false; + } + return true; + } if (!set_bitfield_field(&material_category, parameters[0], 1)) out << "Unrecognized material type: " << parameters[0] << endl; if (!setItem(parameters[1], this)) { @@ -442,14 +451,20 @@ command_result autoclothing(color_ostream &out, vector ¶meters) bool settingSize = false; bool matchedExisting = false; if (parameters.size() > 2) { - try { - newRequirement.needed_per_citizen = std::stoi(parameters[2]); + if (parameters[0] == "clear") { + newRequirement.needed_per_citizen = 0; + settingSize = true; } - catch (const std::exception&) { - out << parameters[2] << " is not a valid number." << endl; - return CR_WRONG_USAGE; + else { + try { + newRequirement.needed_per_citizen = std::stoi(parameters[2]); + } + catch (const std::exception&) { + out << parameters[2] << " is not a valid number." << endl; + return CR_WRONG_USAGE; + } + settingSize = true; } - settingSize = true; } for (size_t i = clothingOrders.size(); i-- > 0;) { @@ -459,7 +474,10 @@ command_result autoclothing(color_ostream &out, vector ¶meters) if (settingSize) { if (newRequirement.needed_per_citizen == 0) { clothingOrders.erase(clothingOrders.begin() + i); - out << "Unset " << parameters[0] << " " << parameters[1] << endl; + if (parameters[0] == "clear") + out << "Unset " << parameters[1] << " " << parameters[2] << endl; + else + out << "Unset " << parameters[0] << " " << parameters[1] << endl; } else { clothingOrders[i] = newRequirement; From f8663942191f4b7856c2885dc237c8fc3055dd5b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 11:41:02 -0500 Subject: [PATCH 397/919] use proper save base path for cosaves fixes #5494 --- docs/changelog.txt | 1 + library/include/modules/DFSDL.h | 72 ++++++++++++++++++--------------- library/modules/DFSDL.cpp | 14 +++++++ library/modules/Persistence.cpp | 11 ++++- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fe8c6e9f49..1726e6b9a6d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working. ## Misc Improvements diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 0fcd17b3226..8d12de19186 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -21,6 +21,7 @@ namespace DFHack SDL_Rect* rect; // from which coords (NULL to draw whole surface) SDL_Rect* dstResize; // if not NULL dst rect will be resized (x/y/w/h will be added to original dst) }; +} /** * The DFSDL module - provides access to SDL functions without actually @@ -28,47 +29,52 @@ namespace DFHack * \ingroup grp_modules * \ingroup grp_dfsdl */ -namespace DFSDL +namespace DFHack::DFSDL { + /** + * Call this on DFHack init so we can load the SDL functions. Returns false on + * failure. + */ + bool init(DFHack::color_ostream& out); -/** - * Call this on DFHack init so we can load the SDL functions. Returns false on - * failure. - */ -bool init(DFHack::color_ostream &out); + /** + * Call this when DFHack is being unloaded. + */ + void cleanup(); -/** - * Call this when DFHack is being unloaded. - */ -void cleanup(); + DFHACK_EXPORT SDL_Surface* DFIMG_Load(const char* file); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceFrom(void* pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); + DFHACK_EXPORT int DFSDL_UpperBlit(SDL_Surface* src, const SDL_Rect* srcrect, SDL_Surface* dst, SDL_Rect* dstrect); + DFHACK_EXPORT SDL_Surface* DFSDL_ConvertSurface(SDL_Surface* src, const SDL_PixelFormat* fmt, uint32_t flags); + DFHACK_EXPORT void DFSDL_FreeSurface(SDL_Surface* surface); + // DFHACK_EXPORT int DFSDL_SemWait(SDL_sem *sem); + // DFHACK_EXPORT int DFSDL_SemPost(SDL_sem *sem); + DFHACK_EXPORT int DFSDL_PushEvent(SDL_Event* event); + DFHACK_EXPORT void DFSDL_free(void* ptr); + DFHACK_EXPORT SDL_PixelFormat* DFSDL_AllocFormat(uint32_t pixel_format); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); + DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char* title, const char* message, SDL_Window* window); -DFHACK_EXPORT SDL_Surface * DFIMG_Load(const char *file); -DFHACK_EXPORT SDL_Surface * DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFHACK_EXPORT SDL_Surface * DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFHACK_EXPORT int DFSDL_UpperBlit(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect); -DFHACK_EXPORT SDL_Surface * DFSDL_ConvertSurface(SDL_Surface *src, const SDL_PixelFormat *fmt, uint32_t flags); -DFHACK_EXPORT void DFSDL_FreeSurface(SDL_Surface *surface); -// DFHACK_EXPORT int DFSDL_SemWait(SDL_sem *sem); -// DFHACK_EXPORT int DFSDL_SemPost(SDL_sem *sem); -DFHACK_EXPORT int DFSDL_PushEvent(SDL_Event *event); -DFHACK_EXPORT void DFSDL_free(void *ptr); -DFHACK_EXPORT SDL_PixelFormat* DFSDL_AllocFormat(uint32_t pixel_format); -DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); -DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window); + // submitted and returned text is UTF-8 + // see wrapper functions below for cp-437 variants + DFHACK_EXPORT char* DFSDL_GetClipboardText(); + DFHACK_EXPORT int DFSDL_SetClipboardText(const char* text); -// submitted and returned text is UTF-8 -// see wrapper functions below for cp-437 variants -DFHACK_EXPORT char * DFSDL_GetClipboardText(); -DFHACK_EXPORT int DFSDL_SetClipboardText(const char *text); + DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); + DFHACK_EXPORT char* DFSDL_GetBasePath(); } -// System clipboard -- submitted and returned text must be in CP437 -DFHACK_EXPORT std::string getClipboardTextCp437(); -DFHACK_EXPORT bool setClipboardTextCp437(std::string text); +namespace DFHack +{ + + // System clipboard -- submitted and returned text must be in CP437 + DFHACK_EXPORT std::string getClipboardTextCp437(); + DFHACK_EXPORT bool setClipboardTextCp437(std::string text); -// interprets 0xa as newline instead of usual CP437 char -DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); -DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // interprets 0xa as newline instead of usual CP437 char + DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); + DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 832ffeb0be7..536d1a5881b 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -58,6 +58,8 @@ void (*g_SDL_free)(void *); SDL_PixelFormat* (*g_SDL_AllocFormat)(uint32_t pixel_format) = nullptr; SDL_Surface* (*g_SDL_CreateRGBSurfaceWithFormat)(uint32_t flags, int width, int height, int depth, uint32_t format) = nullptr; int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char *message, SDL_Window *window) = nullptr; +char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; +char* (*g_SDL_GetBasePath)() = nullptr; bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -101,6 +103,8 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_AllocFormat); bind(g_sdl_handle, SDL_CreateRGBSurfaceWithFormat); bind(g_sdl_handle, SDL_ShowSimpleMessageBox); + bind(g_sdl_handle, SDL_GetPrefPath); + bind(g_sdl_handle, SDL_GetBasePath); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -175,6 +179,16 @@ SDL_Surface* DFSDL::DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, return g_SDL_CreateRGBSurfaceWithFormat(flags, width, height, depth, format); } +char* DFSDL::DFSDL_GetPrefPath(const char* org, const char* app) +{ + return g_SDL_GetPrefPath(org, app); +} + +char* DFSDL::DFSDL_GetBasePath() +{ + return g_SDL_GetBasePath(); +} + int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window) { if (!g_SDL_ShowSimpleMessageBox) return -1; diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index 9e1dd4d0181..e2db24d5fd0 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -29,12 +29,14 @@ distribution. #include "LuaTools.h" #include "MemAccess.h" +#include "modules/DFSDL.h" #include "modules/Filesystem.h" #include "modules/Gui.h" #include "modules/Persistence.h" #include "modules/World.h" #include "df/world.h" +#include "df/init.h" #include @@ -184,7 +186,14 @@ static std::string filterSaveFileName(std::string s) { } static std::filesystem::path getSavePath(const std::string &world) { - return std::filesystem::path{} / "save" / world; + auto getsavebase = []() { + if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); + else + return DFSDL::DFSDL_GetBasePath(); + }; + std::filesystem::path base{ getsavebase() }; + return base / "save" / world; } static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { From 4f38870514b35c924ccdc74e8b9cc2a1334e2a18 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:07:51 +0000 Subject: [PATCH 398/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 9a3cf1a1c13..1c6ef8eaae0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9a3cf1a1c13c0def58979f0c6b59aa3939af7099 +Subproject commit 1c6ef8eaae0b942722c6ad869153fabd8d299069 From 8f745d58d69c379998f9ecab8af0f68d79b7895e Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:14:24 -0500 Subject: [PATCH 399/919] Add missing function `validateMaterialCategory` to the `clear` case for parameters --- plugins/autoclothing.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 96f81ecd2eb..a4516f4dce2 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -136,6 +136,10 @@ struct ClothingRequirement { out << "Unrecognized item name or token: " << parameters[2] << endl; return false; } + else if (!validateMaterialCategory(this)) { + out << parameters[1] << " is not a valid material category for " << parameters[2] << endl; + return false; + } return true; } if (!set_bitfield_field(&material_category, parameters[0], 1)) From 30aa982b31ce465af8b5b18ab6790b945d8b0076 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:35:36 -0500 Subject: [PATCH 400/919] Update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0fe8c6e9f49..f9b4d82f9bb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `autoclothing`: Added a `clear` option to unset previously set orders ## Documentation From 83f7b908613a6adcf51ea0445d1e2eecf7b8cb7f Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:38:38 -0500 Subject: [PATCH 401/919] Update changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f9b4d82f9bb..ae86e466674 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements -- `autoclothing`: Added a `clear` option to unset previously set orders +- `autoclothing`: Added a ``clear`` option to unset previously set orders ## Documentation From 2239953e16e524571a105f8b821f3af5a19ac9b0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 12:38:48 -0500 Subject: [PATCH 402/919] update version to 52.02 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e1226cd81e..4901c0363a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.01") +set(DF_VERSION "52.02") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 1f31ec73e135a8179d3905453330ad95b3563697 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 24 Jul 2025 12:43:41 -0500 Subject: [PATCH 403/919] realized all others start lowercase changing to conform --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ae86e466674..2acfe5c922b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,7 +58,7 @@ Template for new versions: ## Fixes ## Misc Improvements -- `autoclothing`: Added a ``clear`` option to unset previously set orders +- `autoclothing`: added a ``clear`` option to unset previously set orders ## Documentation From b7056fb287802b468f81594e580b4b9bba2d4e86 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:47:21 +0000 Subject: [PATCH 404/919] Auto-update structures ref for 52.02 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 15442adf5c0..d2ebbc986d2 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 15442adf5c0a3cf3c455a888553ff8da30f9b0cd +Subproject commit d2ebbc986d275f3c68af30b63b024f376fd33f9e From 7a5ebe1a82894a496d4f6bf43d5d1b5781d08c6c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:48:44 +0000 Subject: [PATCH 405/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index d2ebbc986d2..fb886909596 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d2ebbc986d275f3c68af30b63b024f376fd33f9e +Subproject commit fb886909596f58612af71a41fe75b711b20a4120 From 849839b149b2fe0c41b25aa78be051c7e5992530 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:58:02 +0000 Subject: [PATCH 406/919] Auto-update structures ref for 52.02 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index fb886909596..ac168003b0f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit fb886909596f58612af71a41fe75b711b20a4120 +Subproject commit ac168003b0f44775373e8a437e4db434619ed1d4 From eabfac36b0ef0800ab974030031189d18ea3b7ce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 13:32:43 -0500 Subject: [PATCH 407/919] changelog and modules for 52.02-r1 --- docs/changelog.txt | 20 +++++++++++++++++++- library/xml | 2 +- scripts | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1726e6b9a6d..98f6f7e4876 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,25 @@ Template for new versions: ## New Features ## Fixes -- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working. + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.02-r1 + +## New Tools + +## New Features + +## Fixes +- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working in most cases. ## Misc Improvements diff --git a/library/xml b/library/xml index ac168003b0f..e8c52ec34dc 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ac168003b0f44775373e8a437e4db434619ed1d4 +Subproject commit e8c52ec34dc7ea5979c3b655902b6816028c504a diff --git a/scripts b/scripts index 1c6ef8eaae0..166b2378a95 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1c6ef8eaae0b942722c6ad869153fabd8d299069 +Subproject commit 166b2378a95721f5bdf6d7575541772ac420b48a From 39cc8548ff4db3e18470cccea630da7144563d8f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 16:56:06 -0500 Subject: [PATCH 408/919] fix mod manager in appdata mode these changes allow the mod manager to work, even in appdata mode, accounting for structure changes in 52.01 --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 10 ++++++++++ library/LuaApi.cpp | 2 ++ library/LuaTypes.cpp | 3 ++- library/include/modules/Filesystem.h | 5 ++++- library/lua/script-manager.lua | 6 +++--- library/modules/Filesystem.cpp | 19 +++++++++++++++++++ library/modules/Persistence.cpp | 8 +------- 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 750670a79ed..24cfa47f03e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- several fixes related to changes in file system handling in DF 52.01 ## Misc Improvements - `autoclothing`: added a ``clear`` option to unset previously set orders diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index e810a184403..e2d65d44654 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3206,6 +3206,16 @@ unless otherwise noted. following it for each entry. Set ``include_prefix`` to false if you don't want the ``path`` string prepended to the returned filenames. +* ``dfhack.filesystem.getBaseDir()`` + + Returns a directory to which DF (and thus DFHack) can save files. This will either + be DF's install directory, or the path returned by ``SDLGetPrefDir``, depending on whether + DF is in "portable mode" or not. + +* ``dfhack.filesystem.getInstallDir()`` + + Returns the the directory in which DF is installed. + Console API ----------- diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1d3f4075a28..10568f7ea0e 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3159,6 +3159,8 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, isdir), WRAPM(Filesystem, mtime), WRAPM(Filesystem, canonicalize), + WRAPM(Filesystem, getInstallDir), + WRAPM(Filesystem, getBaseDir), {NULL, NULL} }; diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 81914a71855..cdacd5ebfeb 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -41,6 +41,7 @@ distribution. #include "PluginManager.h" #include "MiscUtils.h" +#include "modules/Filesystem.h" #include #include @@ -191,7 +192,7 @@ void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *p void df::path_identity::lua_read(lua_State* state, int fname_idx, void* ptr) const { auto ppath = (std::filesystem::path*)ptr; - auto str = ppath->u8string(); + auto str = DFHack::Filesystem::as_string(*ppath); lua_pushlstring(state, (char*)str.data(), str.size()); } diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 7c68983a3a4..407579926fd 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -75,7 +75,7 @@ namespace DFHack { DFHACK_EXPORT int listdir_recursive(std::filesystem::path dir, std::map& files, int depth = 10, bool include_prefix = true) noexcept; DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; - inline std::string as_string(std::filesystem::path path) noexcept + inline std::string as_string(const std::filesystem::path path) noexcept { auto pStr = path.string(); if constexpr (std::filesystem::path::preferred_separator != '/') @@ -84,5 +84,8 @@ namespace DFHack { } return pStr; } + DFHACK_EXPORT std::filesystem::path getInstallDir() noexcept; + DFHACK_EXPORT std::filesystem::path getBaseDir() noexcept; + } } diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 9a1ecb1df00..72b0aae951b 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -80,8 +80,8 @@ end -- this perhaps could/should be queried from the Steam API -- are there any installation configurations where this will be wrong, though? local WORKSHOP_MODS_PATH = '../../workshop/content/975370/' -local MODS_PATH = 'mods/' -local INSTALLED_MODS_PATH = 'data/installed_mods/' +local MODS_PATH = dfhack.filesystem.getBaseDir() .. 'mods/' +local INSTALLED_MODS_PATH = dfhack.filesystem.getBaseDir() .. 'data/installed_mods/' -- last instance of the same version of the same mod wins, so read them in this -- order (in increasing order of liklihood that players may have made custom @@ -214,7 +214,7 @@ function get_active_mods() local ol = df.global.world.object_loader for idx=0,#ol.object_load_order_id-1 do - local path = ol.object_load_order_src_dir[idx].value + local path = ol.object_load_order_src_dir[idx] table.insert(mods, { id=ol.object_load_order_id[idx].value, name=ol.object_load_order_name[idx].value, diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 55a7fd21945..3b1deb67ed3 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -51,8 +51,11 @@ SOFTWARE. #include #include +#include "modules/DFSDL.h" #include "modules/Filesystem.h" +#include "df/init.h" + using namespace DFHack; static bool initialized = false; @@ -226,3 +229,19 @@ std::filesystem::path Filesystem::canonicalize(std::filesystem::path p) noexcept return p; } } + +std::filesystem::path Filesystem::getInstallDir() noexcept +{ + return std::filesystem::path{ DFSDL::DFSDL_GetBasePath() }; +} + +std::filesystem::path Filesystem::getBaseDir() noexcept +{ + auto getsavebase = []() { + if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + return DFSDL::DFSDL_GetBasePath(); + else + return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); + }; + return std::filesystem::path{ getsavebase() }; +} diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index e2db24d5fd0..cd67ff59dc3 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -186,13 +186,7 @@ static std::string filterSaveFileName(std::string s) { } static std::filesystem::path getSavePath(const std::string &world) { - auto getsavebase = []() { - if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) - return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); - else - return DFSDL::DFSDL_GetBasePath(); - }; - std::filesystem::path base{ getsavebase() }; + auto base{ Filesystem::getBaseDir() }; return base / "save" / world; } From b8bbca83cadb13060c7a351c24385f7adfae421d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 09:45:39 -0500 Subject: [PATCH 409/919] second half of fixing mod manager --- library/include/DataIdentity.h | 21 +++++++++++++++++++++ library/include/LuaWrapper.h | 5 +++-- plugins/CMakeLists.txt | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 5aa256809a1..e9fb5a9aa35 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -37,6 +37,7 @@ distribution. #include #include "DataDefs.h" +#include "LuaWrapper.h" namespace std { class condition_variable; @@ -113,6 +114,7 @@ namespace DFHack }; class DFHACK_EXPORT container_identity : public constructed_identity { + protected: const type_identity *item; const enum_identity *ienum; @@ -418,6 +420,25 @@ namespace df ct.insert(ct.begin()+idx, *(typename T::value_type*)item); return true; } + virtual bool lua_insert2(lua_State* state, int fname_idx, void* ptr, int idx, int val_index) const + { + using VT = typename T::value_type; + VT tmp{}; + auto id = (type_identity*)lua_touserdata(state, DFHack::LuaWrapper::UPVAL_ITEM_ID); + auto pitem = DFHack::LuaWrapper::get_object_internal(state, id, val_index, false); + bool useTemporary = (!pitem && id->isPrimitive()); + + if (useTemporary) + { + pitem = &tmp; + id->lua_write(state, fname_idx, pitem, val_index); + } + + if (id != item || !pitem) + DFHack::LuaWrapper::field_error(state, fname_idx, "incompatible object type", "insert"); + + return insert(ptr, idx, pitem); + } protected: virtual int item_count(void *ptr, CountMode) const { return (int)((T*)ptr)->size(); } diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 6d77af87dbb..67301b52cc6 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -29,8 +29,6 @@ distribution. #include #include -#include "DataDefs.h" - #include #include @@ -41,6 +39,9 @@ distribution. namespace DFHack { struct FunctionReg; + + class function_identity_base; + namespace LuaWrapper { struct LuaToken; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 3ef551a49e5..405a11baa0c 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -36,7 +36,7 @@ option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) dfhack_plugin(army-controller-sanity army-controller-sanity.cpp) - dfhack_plugin(add-spatter add-spatter.cpp) + dfhack_plugin(add-spatter add-spatter.cpp LINK_LIBRARIES lua) dfhack_plugin(aquifer aquifer.cpp LINK_LIBRARIES lua) dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) dfhack_plugin(autochop autochop.cpp LINK_LIBRARIES lua) From 36a0b1f8aa4d41526978bd21518de223250d8191 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 09:56:38 -0500 Subject: [PATCH 410/919] add comment to plugin CMakeLists.txt --- plugins/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 405a11baa0c..1f721ec7133 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -32,6 +32,10 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) # proto file must be in the proto/ folder # dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) +# warning: for complicated reasons, if a plugin uses a vmethod interpose, it must +# link to lua even if it doesn't otherwise use lua. we'll fix this someday, we promise. +# we apologize for the inconvenience. + option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) From bcea2c9b6021c5a212316879f443bcdab9dc1d5d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 10:02:09 -0500 Subject: [PATCH 411/919] didn't know about this one --- plugins/devel/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 210669a75de..c044fa10447 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -23,7 +23,7 @@ dfhack_plugin(tilesieve tilesieve.cpp) # dfhack_plugin(zoom zoom.cpp) if(UNIX) - dfhack_plugin(ref-index ref-index.cpp) + dfhack_plugin(ref-index ref-index.cpp LINK_LIBRARIES lua) endif() add_subdirectory(check-structures-sanity) From 1c0663c4bc04b53c4632b4395a370ec145038c1c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 10:18:33 -0500 Subject: [PATCH 412/919] add changelog also included a bit i forgot from #5514 --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 24cfa47f03e..483e0b0570e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,8 +64,10 @@ Template for new versions: ## Documentation ## API +- ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) ## Lua +- ``insert``ing values into STL containers containing nonprimitive types is now supported ## Removed From 772b637ca7ce63e9c24c365145bdcc728163fde3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 25 Jul 2025 10:33:51 -0500 Subject: [PATCH 413/919] Update changelog.txt --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 483e0b0570e..bf01965a3c7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,7 +67,7 @@ Template for new versions: - ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) ## Lua -- ``insert``ing values into STL containers containing nonprimitive types is now supported +- inserting values into STL containers containing nonprimitive types is now supported ## Removed From 253101b334bbf0683132ce90e53e15709f316629 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:12:07 +0000 Subject: [PATCH 414/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 166b2378a95..ca3ab763c5f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 166b2378a95721f5bdf6d7575541772ac420b48a +Subproject commit ca3ab763c5f5d9c6d340bf4ee5db9ba213c718f0 From 81821ffdb23f67ca8939c881483a52ba6e708c30 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:07:47 +0000 Subject: [PATCH 415/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ca3ab763c5f..3cec9d6f116 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ca3ab763c5f5d9c6d340bf4ee5db9ba213c718f0 +Subproject commit 3cec9d6f116a7e4246723f0aaeb2dfe6b124a697 From aee124c3d3380979580f43e78c986f5bb49f7871 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 26 Jul 2025 13:49:43 -0500 Subject: [PATCH 416/919] use consistent base and install path determination for 52.xx compatibility when in appdata mode --- library/Core.cpp | 57 +++++++++++++++++++-------------- library/modules/Persistence.cpp | 3 +- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 1208650579d..a2b48c992d1 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -108,11 +108,18 @@ size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector *dest) { std::lock_guard lock(script_path_mutex); dest->clear(); - std::filesystem::path df_path = this->p->getPath(); + std::filesystem::path df_pref_path = Filesystem::getBaseDir(); + std::filesystem::path df_install_path = Filesystem::getInstallDir(); for (auto & path : script_paths[0]) dest->emplace_back(path); - dest->push_back(df_path / CONFIG_PATH / "scripts"); + // should this be df_pref_path? probably + dest->push_back(getConfigPath() / "scripts"); if (df::global::world && isWorldLoaded()) { std::string save = World::ReadWorldFolder(); if (save.size()) - dest->emplace_back(df_path / "save" / save / "scripts"); + dest->emplace_back(df_pref_path / "save" / save / "scripts"); } - dest->emplace_back(df_path / "hack" / "scripts"); + dest->emplace_back(df_install_path / "hack" / "scripts"); for (auto & path : script_paths[2]) dest->emplace_back(path); for (auto & path : script_paths[1]) @@ -535,7 +544,7 @@ std::filesystem::path Core::findScript(std::string name) bool loadScriptPaths(color_ostream &out, bool silent = false) { - std::filesystem::path filename{ CONFIG_PATH / "script-paths.txt" }; + std::filesystem::path filename{ getConfigPath() / "script-paths.txt" }; std::ifstream file(filename); if (!file) { @@ -1372,11 +1381,11 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // load baseline defaults - core->loadScriptFile(out, CONFIG_PATH / "init" / "default.dfhack.init", false); + core->loadScriptFile(out, getConfigPath() / "init" / "default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); - loadScriptFiles(core, out, prefixes, CONFIG_PATH / "init"); + loadScriptFiles(core, out, prefixes, getConfigPath() / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); @@ -1399,7 +1408,7 @@ static void fInitthread(IODATA * iod) // A thread function... for the interactive console. static void fIOthread(IODATA * iod) { - static const std::filesystem::path HISTORY_FILE = CONFIG_PATH / "dfhack.history"; + static const std::filesystem::path HISTORY_FILE = getConfigPath() / "dfhack.history"; Core * core = iod->core; PluginManager * plug_mgr = iod->plug_mgr; @@ -1763,16 +1772,16 @@ bool Core::InitSimulationThread() virtual_identity::Init(this); // create config directory if it doesn't already exist - if (!Filesystem::mkdir_recursive(CONFIG_PATH)) - con.printerr("Failed to create config directory: '%s'\n", CONFIG_PATH.c_str()); + if (!Filesystem::mkdir_recursive(getConfigPath())) + con.printerr("Failed to create config directory: '%s'\n", getConfigPath().c_str()); // copy over default config files if necessary std::map config_files; std::map default_config_files; - if (Filesystem::listdir_recursive(CONFIG_PATH, config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", CONFIG_PATH.c_str()); - else if (Filesystem::listdir_recursive(CONFIG_DEFAULTS_PATH, default_config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", CONFIG_DEFAULTS_PATH.c_str()); + if (Filesystem::listdir_recursive(getConfigPath(), config_files, 10, false) != 0) + con.printerr("Failed to list directory: '%s'\n", getConfigPath().c_str()); + else if (Filesystem::listdir_recursive(getConfigDefaultsPath(), default_config_files, 10, false) != 0) + con.printerr("Failed to list directory: '%s'\n", getConfigDefaultsPath().c_str()); else { // ensure all config file directories exist before we start copying files @@ -1780,7 +1789,7 @@ bool Core::InitSimulationThread() // skip over files if (!entry.second) continue; - std::filesystem::path dirname = CONFIG_PATH / entry.first; + std::filesystem::path dirname = getConfigPath() / entry.first; if (!Filesystem::mkdir_recursive(dirname)) con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); } @@ -1792,10 +1801,10 @@ bool Core::InitSimulationThread() continue; std::filesystem::path filename = entry.first; if (!config_files.count(filename)) { - std::filesystem::path src_file = CONFIG_DEFAULTS_PATH / filename; + std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; - std::filesystem::path dest_file = CONFIG_PATH / filename; + std::filesystem::path dest_file = getConfigPath() / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); if (!src.good() || !dest.good()) { @@ -2224,16 +2233,16 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve if (!df::global::world) return; - std::filesystem::path rawFolder = !isWorldLoaded() ? std::filesystem::path{} : std::filesystem::path{} / "save" / World::ReadWorldFolder() / "init"; + std::filesystem::path rawFolder = !isWorldLoaded() ? Filesystem::getInstallDir() : Filesystem::getBaseDir() / "save" / World::ReadWorldFolder() / "init"; auto i = table.find(event); if ( i != table.end() ) { const std::vector& set = i->second; // load baseline defaults - this->loadScriptFile(out, CONFIG_PATH / "init" / ("default." + set[0] + ".init"), false); + this->loadScriptFile(out, getConfigPath() / "init" / ("default." + set[0] + ".init"), false); - loadScriptFiles(this, out, set, CONFIG_PATH / "init"); + loadScriptFiles(this, out, set, getConfigPath() / "init"); loadScriptFiles(this, out, set, rawFolder); } diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index cd67ff59dc3..e6a0d501269 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -186,8 +186,7 @@ static std::string filterSaveFileName(std::string s) { } static std::filesystem::path getSavePath(const std::string &world) { - auto base{ Filesystem::getBaseDir() }; - return base / "save" / world; + return Filesystem::getBaseDir() / "save" / world; } static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { From c6f3ebde5c10fa3f5a1d5b6139ec14e05baa7331 Mon Sep 17 00:00:00 2001 From: Quietust Date: Sat, 26 Jul 2025 13:15:44 -0600 Subject: [PATCH 417/919] dig-now - properly handle UNDIGGABLE tiles Fixes #5512 --- docs/changelog.txt | 1 + plugins/dig-now.cpp | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ef0b1b00da7..730508f2d91 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## Fixes - several fixes related to changes in file system handling in DF 52.01 +- `dig-now`: don't allow UNDIGGABLE stones to be excavated ## Misc Improvements - `autoclothing`: added a ``clear`` option to unset previously set orders diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index 16fe585157f..48984e1b9fc 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -450,12 +450,10 @@ static bool is_diggable(MapExtras::MapCache &map, const DFCoord &pos, break; } - if (mat == df::tiletype_material::FEATURE) { - // adamantine is the only is diggable feature - t_feature feature; - return map.BlockAtTile(pos)->GetLocalFeature(&feature) - && feature.type == feature_type::deep_special_tube; - } + MaterialInfo mi; + mi.decode(map.baseMaterialAt(pos)); + if (mi.material != nullptr && mi.material->flags.is_set(df::material_flags::UNDIGGABLE)) + return false; return true; } From 2a43c80b945050c34395df7e46772e4cf1070e6d Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 26 Jul 2025 14:18:17 -0700 Subject: [PATCH 418/919] script-manager.lua One last use of a as a --- library/lua/script-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 72b0aae951b..74a31fd1d97 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -152,7 +152,6 @@ function get_mod_paths(installed_subdir, active_subdir) -- 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) From 4d7544735b32c6ef65fc2381469236fd846e8a8a Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 27 Jul 2025 13:35:31 +0200 Subject: [PATCH 419/919] extend implementations of `addToSquad` and `removeFromSquad` --- docs/changelog.txt | 1 + library/modules/Military.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ef0b1b00da7..071d6352ef5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## API - ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) +- expand the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` ## Lua - inserting values into STL containers containing nonprimitive types is now supported diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 83cf240d19b..b0c671af2da 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -5,6 +5,8 @@ #include "modules/Military.h" #include "modules/Translation.h" #include "modules/Units.h" +#include "df/activity_entry.h" +#include "df/activity_event.h" #include "df/building.h" #include "df/building_civzonest.h" #include "df/building_squad_infost.h" @@ -17,6 +19,7 @@ #include "df/history_event_remove_hf_entity_linkst.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" +#include "df/equipment_update.h" #include "df/plotinfost.h" #include "df/military_routinest.h" #include "df/squad.h" @@ -460,6 +463,15 @@ bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) unit->military.squad_position = squad_pos; add_soldier_entity_link(hf, squad, squad_pos); + + #define F(flag) df::equipment_update::mask_##flag + auto constexpr update_flags = F(weapon) | F(armor) | F(shoes) | F(shield) | F(helm) | F(gloves) + | F(ammo) | F(pants) | F(backpack) | F(quiver) | F(flask); + #undef F + + squad->ammo.update.whole |= update_flags; + df::global::plotinfo->equipment.update.whole |= update_flags; + return true; } @@ -489,6 +501,19 @@ bool Military::removeFromSquad(int32_t unit_id) unit->military.squad_id = -1; unit->military.squad_position = -1; + // abort individual training + if (unit->individual_drills.size()) { + unit->flags3.bits.verify_personal_training = true; + } + + // remove unit from squad activities + auto activity_entry = binsearch_in_vector(df::global::world->activities.all,&df::activity_entry::id,squad->activity); + if (activity_entry) { + for (auto const activity_event : activity_entry->events) { + activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); + } + } + if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); else From 9c51ad173ca702d3038b6cfcac640c3d40a14df1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:10:08 +0000 Subject: [PATCH 420/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3cec9d6f116..33159eee46f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3cec9d6f116a7e4246723f0aaeb2dfe6b124a697 +Subproject commit 33159eee46f0eca08a728893a3f7476d7e6d42d8 From 2264abb8ba91f1c408a68a860e66d02f159a32c0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 27 Jul 2025 12:25:18 -0500 Subject: [PATCH 421/919] 52.02-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 25 ++++++++++++++++++++++--- library/xml | 2 +- scripts | 2 +- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4901c0363a6..ecdf3f68e81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.02") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 6c71e7027a3..3807f7ffc8f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,25 @@ Template for new versions: ## New Features ## Fixes -- several fixes related to changes in file system handling in DF 52.01 + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.02-r2 + +## New Tools + +## New Features + +## Fixes +- Several fixes related to changes in file system handling in DF 52.01 - `dig-now`: don't allow UNDIGGABLE stones to be excavated ## Misc Improvements @@ -66,11 +84,12 @@ Template for new versions: ## Documentation ## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` - ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) -- expand the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` +- Expanded the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` ## Lua -- inserting values into STL containers containing nonprimitive types is now supported +- Inserting values into STL containers containing nonprimitive types is now supported ## Removed diff --git a/library/xml b/library/xml index e8c52ec34dc..3eb07ab8abf 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e8c52ec34dc7ea5979c3b655902b6816028c504a +Subproject commit 3eb07ab8abfb0d6a00f0758917c52736effbeb9b diff --git a/scripts b/scripts index 33159eee46f..30993eef1c6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 33159eee46f0eca08a728893a3f7476d7e6d42d8 +Subproject commit 30993eef1c6ad88279886b6863ad56bbc82f669b From 9a4d21225052bf6f81d21c2c2ea647e942e0543f Mon Sep 17 00:00:00 2001 From: Quietust Date: Sun, 27 Jul 2025 13:58:42 -0600 Subject: [PATCH 422/919] Update removeFromSquad to match in-game logic aside from the link removal stuff at the end, which might still be slightly different --- docs/changelog.txt | 1 + library/modules/Military.cpp | 59 +++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3807f7ffc8f..a52fb495e54 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## Documentation ## API +- Adjusted the logic inside ``Military::removeFromSquad`` to more closely match the game's own behavior ## Lua diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index b0c671af2da..d3fb4368265 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -477,42 +477,53 @@ bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) bool Military::removeFromSquad(int32_t unit_id) { + // based on unitst::remove_squad_info df::unit *unit = df::unit::find(unit_id); if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) return false; - int32_t squad_id = unit->military.squad_id; - df::squad* squad = df::squad::find(squad_id); - if (squad == nullptr) - return false; + // abort individual training + if (unit->individual_drills.size()) + unit->flags3.bits.verify_personal_training = true; + int32_t squad_id = unit->military.squad_id; int32_t squad_pos = unit->military.squad_position; - df::squad_position* pos = vector_get(squad->positions, squad_pos); - if (pos == nullptr) - return false; - df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); - if (hf == nullptr) - return false; + df::squad* squad = df::squad::find(squad_id); + + if (squad) + { + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos) + { + // based on unitst::remove_squad_activity_info + FOR_ENUM_ITEMS(squad_event_type, i) + { + auto activity_entry = df::activity_entry::find(pos->activities[i]); + if (activity_entry) + { + auto activity_event = binsearch_in_vector(activity_entry->events, &df::activity_event::event_id, pos->events[i]); + if (activity_event) + { + activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); + pos->activities[i] = -1; + pos->events[i] = -1; + } + } + } - // remove from squad information - pos->occupant = -1; + // remove from squad position + pos->occupant = -1; + } + } // remove from unit information unit->military.squad_id = -1; unit->military.squad_position = -1; - // abort individual training - if (unit->individual_drills.size()) { - unit->flags3.bits.verify_personal_training = true; - } - - // remove unit from squad activities - auto activity_entry = binsearch_in_vector(df::global::world->activities.all,&df::activity_entry::id,squad->activity); - if (activity_entry) { - for (auto const activity_event : activity_entry->events) { - activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); - } - } + // remove entity squad link + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr || squad == nullptr) + return false; if (squad_pos == 0) // is unit a commander? remove_officer_entity_link(hf, squad); From d1f30167b403912a3a2349cf6fcbf0a1880f033d Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 3 Aug 2025 17:16:33 +0200 Subject: [PATCH 423/919] helper functions for creating jobs and checking whether units are ready to take jobs --- docs/changelog.txt | 2 + library/include/modules/Job.h | 7 +++ library/include/modules/Units.h | 13 +++++ library/modules/Job.cpp | 22 ++++++++ library/modules/Units.cpp | 90 +++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3807f7ffc8f..99b9c6a9bfd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,8 @@ Template for new versions: ## Documentation ## API +- ``Job``: new functions ``createLinked`` and ``assignToWorkshop`` +- ``Units``: new functions ``getFocusPenalty``, ``unbailableSocialActivity``, ``isJobAvailable`` ## Lua diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index a0754c4781f..5f7ddcb11a2 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -31,6 +31,7 @@ distribution. #include "Types.h" #include "DataDefs.h" +#include "df/building_workshopst.h" #include "df/item_type.h" #include "df/job_item_ref.h" @@ -94,7 +95,13 @@ namespace DFHack DFHACK_EXPORT void checkBuildingsNow(); DFHACK_EXPORT void checkDesignationsNow(); + // link the job into the global job list, passing ownership to DF DFHACK_EXPORT bool linkIntoWorld(df::job *job, bool new_id = true); + // create a job and immediately link it into the global job list + DFHACK_EXPORT df::job* createLinked(); + + // assign job to workshop, returns false if workshop already has the maximum of ten jobs + DFHACK_EXPORT bool assignToWorkshop(df::job *job, df::building_workshopst *workshop); // Flag this job's posting as "dead" and set its posting_index to -1 // If remove_all is true, flag all postings pointing to this job diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 0cba2516445..fc33fd3b0a4 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -39,12 +39,14 @@ distribution. #include "df/job_skill.h" #include "df/mental_attribute_type.h" #include "df/misc_trait_type.h" +#include "df/need_type.h" #include "df/physical_attribute_type.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" #include "df/unit_path_goal.h" #include +#include namespace df { struct activity_entry; @@ -339,6 +341,17 @@ DFHACK_EXPORT bool isGoalAchieved(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); +// get largest (i.e. most negative) focus penalty for a set of needs +using need_types_set = std::bitset; +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_types_set need_types); +// get focused penalty for a single need +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); + +// unit has an unbailable social activity (e.g. "Socialize!") +DFHACK_EXPORT bool unbailableSocialActivity(df::unit *unit); +// unit can be assigned a job +DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool interrupt_social); + // Stress categories. 0 is highest stress, 6 is lowest. DFHACK_EXPORT extern const std::vector stress_cutoffs; DFHACK_EXPORT int getStressCategory(df::unit *unit); diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 96344ddd6f0..5a34a19e01a 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -38,6 +38,7 @@ distribution. #include "modules/References.h" #include "df/building.h" +#include "df/building_workshopst.h" #include "df/general_ref.h" #include "df/general_ref_unit_workerst.h" #include "df/general_ref_building_holderst.h" @@ -517,6 +518,27 @@ bool DFHack::Job::linkIntoWorld(df::job *job, bool new_id) } } +df::job* DFHack::Job::createLinked() +{ + auto job = new df::job(); + DFHack::Job::linkIntoWorld(job, true); + return job; +} + +bool assignToWorkshop(df::job *job, df::building_workshopst *workshop) +{ + CHECK_NULL_POINTER(job); + CHECK_NULL_POINTER(workshop); + + if (workshop->jobs.size() >= 10) { + return false; + } + job->pos = df::coord(workshop->centerx, workshop->centery, workshop->z); + DFHack::Job::addGeneralRef(job, df::general_ref_type::BUILDING_HOLDER, workshop->id); + workshop->jobs.push_back(job); + return true; +} + bool DFHack::Job::removePostings(df::job *job, bool remove_all) { using df::global::world; diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 89a1cbf0262..4a5e3f5a0db 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -64,12 +64,15 @@ distribution. #include "df/interaction_profilest.h" #include "df/item.h" #include "df/job.h" +#include "df/need_type.h" #include "df/nemesis_record.h" #include "df/personality_goalst.h" +#include "df/personality_needst.h" #include "df/plotinfost.h" #include "df/proj_unitst.h" #include "df/reputation_profilest.h" #include "df/syndrome.h" +#include "df/squad.h" #include "df/tile_occupancy.h" #include "df/training_assignment.h" #include "df/unit.h" @@ -89,6 +92,7 @@ distribution. #include "df/world_site.h" #include +#include #include #include #include @@ -2017,6 +2021,92 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) { return entry->events[entry->events.size() - 1]; } +int32_t Units::getFocusPenalty(df::unit* unit, need_types_set need_types) { + CHECK_NULL_POINTER(unit); + + int max_penalty = INT_MAX; + auto& needs = unit->status.current_soul->personality.needs; + for (auto const need : needs) { + if (need_types.test(need->id)) { + max_penalty = min(max_penalty, need->focus_level); + } + } + return max_penalty; +} + +int32_t Units::getFocusPenalty(df::unit* unit, df::need_type need_type) { + auto need_types = need_types_set().set(need_type); + return getFocusPenalty(unit, need_types); +} + +// reverse engineered from unitst::have_unbailable_sp_activities (partial implementation) +bool Units::unbailableSocialActivity(df::unit *unit) +{ + // these can become constexpr with C++23 + static const need_types_set pray_needs = need_types_set() + .set(df::need_type::PrayOrMeditate); + + static const need_types_set socialize_needs = need_types_set() + .set(df::need_type::Socialize) + .set(df::need_type::BeCreative) + .set(df::need_type::Excitement) + .set(df::need_type::AdmireArt); + + static const need_types_set read_needs = need_types_set() + .set(df::need_type::ThinkAbstractly) + .set(df::need_type::LearnSomething); + + CHECK_NULL_POINTER(unit); + + if (unit->social_activities.empty()) { + return false; + } else if (unit->social_activities.size() > 1) { + return true; // is this even possible? + } + + auto activity = df::activity_entry::find(unit->social_activities[0]); + if (activity) { + using df::activity_entry_type; + switch (activity->type) { + case activity_entry_type::Socialize: + return getFocusPenalty(unit, socialize_needs) <= -10000; + case activity_entry_type::Prayer: + return getFocusPenalty(unit, pray_needs) <= -10000; + case activity_entry_type::Read: + return getFocusPenalty(unit, read_needs) <= -10000; + default: + // consider unhandled activities as uninterruptible + return true; + } + } + // this should never happen + return false; +} + +bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ + if (unit->job.current_job) + return false; + if (unit->flags1.bits.caged || unit->flags1.bits.chained) + return false; + if (unit->individual_drills.size() > 0) { + if (unit->individual_drills.size() > 1) + return false; // this is even possible + auto activity = df::activity_entry::find(unit->individual_drills[0]); + if (activity && (activity->type == df::activity_entry_type::FillServiceOrder)) + return false; + } + if (unbailableSocialActivity(unit)) + return false; + if (preserve_social && unit->social_activities.size() > 0) + return false; + if (unit->military.squad_id != -1) { + auto squad = df::squad::find(unit->military.squad_id); + if (squad) + return squad->orders.size() == 0 && squad->activity == -1; + } + return true; +} + // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; From e6757c36a395e577741c2c303c14463bd811cd19 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 6 Aug 2025 18:42:25 -0500 Subject: [PATCH 424/919] cheap mitigation of #5538 this doesn't really resolve #5538 but it does mitigate it from a hang to potential misoperation --- docs/changelog.txt | 1 + plugins/lua/preserve-rooms.lua | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3807f7ffc8f..fbf9959a382 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features ## Fixes +- `preserve-rooms` will no longer hang on startup in the presence of a cycle in the replacement relationship of noble positions ## Misc Improvements diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index 8dd4e507af9..3851920dafa 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -320,9 +320,13 @@ local function get_codes(positions) if position.replaced_by == -1 then ensure_key(grouped, id)[id] = position else + -- replaced-by links may be cyclic. this code will behave "incorrectly" in the event + -- of a cycle but will not hang. a proper fix is still needed. see issue DFHack/dfhack#5538 local parent = positions[position.replaced_by] - while parent.replaced_by ~= -1 do + local counter = 0 + while parent.replaced_by ~= -1 and counter < 10 do parent = positions[parent.replaced_by] + counter = counter + 1 end ensure_key(grouped, parent.id)[id] = position end From bd6c6bd9139e3116957db3e620c3f49b83c63f1d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 7 Aug 2025 07:26:46 +0000 Subject: [PATCH 425/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 30993eef1c6..244ddec5095 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 30993eef1c6ad88279886b6863ad56bbc82f669b +Subproject commit 244ddec5095cf8ea3e5553a96cd285dd13a33942 From 3a5eaa1a33166236fe258eb831316b99773b2b9a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 12:54:13 -0500 Subject: [PATCH 426/919] Bump to 52.03-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 19 +++++++++++++++++++ library/xml | 2 +- scripts | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ecdf3f68e81..fa6ac565c2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.02") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "52.03") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index fbf9959a382..2c4aa5c9f29 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,25 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` + +## Lua + +## Removed + +# 52.03-1 + +## New Tools + +## New Features + ## Fixes - `preserve-rooms` will no longer hang on startup in the presence of a cycle in the replacement relationship of noble positions diff --git a/library/xml b/library/xml index 3eb07ab8abf..5ee7e9d47d8 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3eb07ab8abfb0d6a00f0758917c52736effbeb9b +Subproject commit 5ee7e9d47d86bb7b27f540197d4916ae971439e5 diff --git a/scripts b/scripts index 244ddec5095..61e0e181953 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 244ddec5095cf8ea3e5553a96cd285dd13a33942 +Subproject commit 61e0e181953e70679d0ed4dfc7e9e64922ad0c07 From 0cc8ad1edb16ac98a74176d727bcda25bd78d2de Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:09:42 +0000 Subject: [PATCH 427/919] Auto-update structures ref for 52.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 5ee7e9d47d8..23880e1a19c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5ee7e9d47d86bb7b27f540197d4916ae971439e5 +Subproject commit 23880e1a19cc77e3aafdb642b291015390ccc01c From 26b65e0591a02645d513bc04dbe47a46e23698d3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 13:19:29 -0500 Subject: [PATCH 428/919] fix generate symbols CI --- .github/workflows/generate-symbols.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 9f4e32b1462..f78da19e748 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -134,7 +134,7 @@ jobs: +quit tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_steam xml/symbols_gen_linux.sh ${{ inputs.version == 'auto' && '50.0' || inputs.version }} STEAM DF_steam - if [ "${{ inputs.version }}" = "auto" ]; then + if [ "${{ inputs.version }}" == "auto" ]; then while pgrep dwarfort; do echo "waiting for DF to exit" sleep 0.5 From 76a1ce24a6e388be63140143c74dc9bce4bffefe Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 14:48:50 -0500 Subject: [PATCH 429/919] update structures --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 23880e1a19c..64ce0f1ced2 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 23880e1a19cc77e3aafdb642b291015390ccc01c +Subproject commit 64ce0f1ced2b224168d7d898b4afa8f3b47405b3 From 82a0acec3aacd2f13031c72c9728032d6e166da0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 15:42:45 -0500 Subject: [PATCH 430/919] make `Filesystem::getBaseDir` fall back if df::global::init is missing --- library/modules/Filesystem.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 3b1deb67ed3..7a6b09a5004 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -238,7 +238,8 @@ std::filesystem::path Filesystem::getInstallDir() noexcept std::filesystem::path Filesystem::getBaseDir() noexcept { auto getsavebase = []() { - if (df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + // assume portable mode is _on_ if init is missing + if (!df::global::init || df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) return DFSDL::DFSDL_GetBasePath(); else return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); From ad2625074503df7a3beea3bd411e73990e9a5443 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:56:33 +0000 Subject: [PATCH 431/919] Auto-update structures ref for 52.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 64ce0f1ced2..7c4f3e32cd9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 64ce0f1ced2b224168d7d898b4afa8f3b47405b3 +Subproject commit 7c4f3e32cd9c03f39aefed4b6ad7f6adb8eea95b From c2435b0a571c8733e3f11b352a5c62d61d4a1b98 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 8 Aug 2025 21:49:24 -0500 Subject: [PATCH 432/919] fix `Job::getName` add fields added to `job` in the dye update --- docs/changelog.txt | 5 +++-- library/modules/Job.cpp | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2c4aa5c9f29..064e7181066 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,19 +57,19 @@ Template for new versions: ## New Features ## Fixes +- job descriptions of mix dye job will display proper dye names ## Misc Improvements ## Documentation ## API -- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua ## Removed -# 52.03-1 +# 52.03-r1 ## New Tools @@ -83,6 +83,7 @@ Template for new versions: ## Documentation ## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 96344ddd6f0..39ca701c23c 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -655,6 +655,9 @@ std::string Job::getName(df::job *job) button->matgloss = job->mat_index; button->specflag = job->specflag; button->job_item_flag = job->material_category; + button->specdata = job->specdata; + button->art_specifier_id1 = job->art_spec.id; + button->art_specifier_id2 = job->art_spec.subid; button->text(&desc); delete button; From 24985eb3753aaa7ddb4919a75a6c467c00d304aa Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 9 Aug 2025 03:10:55 +0000 Subject: [PATCH 433/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 7c4f3e32cd9..462eeeb505c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7c4f3e32cd9c03f39aefed4b6ad7f6adb8eea95b +Subproject commit 462eeeb505c453ecbc7d2062fd4575feca34c34e From 02bd21828ea5e7b5622bafc22d9b56cab9d5d25a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 9 Aug 2025 03:01:07 -0500 Subject: [PATCH 434/919] update for 52.03-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 20 ++++++++++++++++++-- library/xml | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fa6ac565c2e..a0875d2c321 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.03") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 064e7181066..76a203fc725 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,7 +35,6 @@ Template for new versions: ## Documentation ## API -- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua @@ -56,6 +55,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.03-r1.1 + +## New Tools + +## New Features + ## Fixes - job descriptions of mix dye job will display proper dye names @@ -83,7 +100,6 @@ Template for new versions: ## Documentation ## API -- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` ## Lua diff --git a/library/xml b/library/xml index 462eeeb505c..fc6dcd5aa07 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 462eeeb505c453ecbc7d2062fd4575feca34c34e +Subproject commit fc6dcd5aa077a0d96d81bd2dfe89e24bed375472 From f031a14a4e622b7ba9ef6ad3af24923e4e324994 Mon Sep 17 00:00:00 2001 From: beofhelp <17787716+beofhelp@users.noreply.github.com> Date: Sat, 9 Aug 2025 13:43:05 +0000 Subject: [PATCH 435/919] fixed a typo in orders.rst --- docs/plugins/orders.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 378881d12c7..d89475a3280 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -151,7 +151,7 @@ game does not provide sufficient order conditions. Please enable ``automilk``, types of jobs. Note that the jugs are specifically made out of wood. This is so, as long as -you don't may any other "Tools" out of wood, you can have a stockpile just for +you don't make any other "Tools" out of wood, you can have a stockpile just for jugs by restricting a finished goods stockpile to only take wooden tools. Armok's additional note: "shleggings? Yes, From 46d8213f16058623a3c790b9026a67facce58750 Mon Sep 17 00:00:00 2001 From: definability <5962274+definability@users.noreply.github.com> Date: Sat, 16 Aug 2025 09:59:54 +0100 Subject: [PATCH 436/919] Fix #5554: Typo in plugins/lua/sort/places.lua Typo in the word "Barracks" --- plugins/lua/sort/places.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index d58f306584b..f2588cc0ff3 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -21,7 +21,7 @@ local zone_names = { [df.civzone_type.SandCollection] = 'Sand', [df.civzone_type.Office] = 'Office', [df.civzone_type.Dormitory] = 'Dormitory', - [df.civzone_type.Barracks] = 'Barrachs', + [df.civzone_type.Barracks] = 'Barracks', [df.civzone_type.ArcheryRange] = 'Archery Range', [df.civzone_type.Dump] = 'Garbage Dump', [df.civzone_type.AnimalTraining] = 'Animal Training', From b258565046908a18b69c376d225d450cde5251da Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 16 Aug 2025 18:04:39 +0200 Subject: [PATCH 437/919] export job helpers and provide documentation --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 28 +++++++++++++++++++++++++++- library/LuaApi.cpp | 26 ++++++++++++++++++++++++++ library/include/modules/Units.h | 6 +++--- library/modules/Job.cpp | 2 +- library/modules/Units.cpp | 14 +++++++------- 6 files changed, 66 insertions(+), 12 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 667bbe04254..6a9ee5ec0d6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,8 @@ Template for new versions: ## Lua +- New functions: ``dfhack.jobs.createLinked``, ``dfhack.jobs.assignToWorkshop``, ``dfhack.units.getFocusPenalty``, ``dfhack.units.unbailableSocialActivity``, and ``dfhack.units.isJobAvailable`` + ## Removed # 52.03-r1.1 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index e2d65d44654..ea6a23183d3 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1313,6 +1313,10 @@ Job module Creates a deep copy of the given job. +* ``dfhack.job.createLinked()`` + + Create a job and immediately link it into the global job list. + * ``dfhack.job.printJobDetails(job)`` Prints info about the job. @@ -1338,6 +1342,12 @@ Job module Searches for a specific_ref with the given type. +* ``dfhack.job.assignToWorkshop(job, workshop)`` + + Assign job to workshop (i.e. establish the bidirectional link between the job + and the workshop). Does nothing and returns ``false`` if the workshop already + has the maximum of ten jobs. + * ``dfhack.job.getHolder(job)`` Returns the building holding the job. @@ -1628,7 +1638,7 @@ Units module Returns true if the unit is within a box defined by the specified coordinates. -``dfhack.units.getUnitsInBox(pos1, pos2[, filter])`` +* ``dfhack.units.getUnitsInBox(pos1, pos2[, filter])`` * ``dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2[,filter])`` Returns a table of all units within the specified coordinates. @@ -1894,6 +1904,22 @@ Units module Return the ``df.activity_entry`` or ``df.activity_event`` representing the unit's current social activity. +* ``dfhack.units.hasUnbailableSocialActivity(unit)`` + + Unit has an uninterruptible social activity (e.g. a purple "Socialize!"). + +* ``dfhack.units.isJobAvailable(unit [, interrupt_social])`` + + Check whether a unit can be assigned to (i.e. is looking for) a job. Will + return ``true`` if the unit is engaged in "green" social activities, unless + the boolean ``interrupt_social`` is true. + +* ``dfhack.units.getFocusPenalty(unit, need_type [, need_type, ...])`` + + Get largest (i.e. most negative) focus penalty associated to a collection of + ``df.need_type`` arguments. Returns a number strictly greater than 400 if the + unit does not have any of the requested needs. + * ``dfhack.units.getStressCategory(unit)`` Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least. diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index a34439a2aea..192fc7beb5f 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -69,6 +69,7 @@ distribution. #include "df/building_civzonest.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" +#include "df/building_workshopst.h" #include "df/burrow.h" #include "df/caravan_state.h" #include "df/construction.h" @@ -116,6 +117,7 @@ distribution. #include #include #include +#include namespace DFHack { DBG_DECLARE(core, luaapi, DebugCategory::LINFO); @@ -1902,6 +1904,8 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = { WRAPM(Job,disconnectJobItem), WRAPM(Job,disconnectJobGeneralRef), WRAPM(Job,removeJob), + WRAPM(Job,createLinked), + WRAPM(Job,assignToWorkshop), WRAPN(is_equal, jobEqual), WRAPN(is_item_equal, jobItemEqual), { NULL, NULL } @@ -2128,6 +2132,8 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, setGroupActionTimers), WRAPM(Units, getUnitByNobleRole), WRAPM(Units, unassignTrainer), + WRAPM(Units, hasUnbailableSocialActivity), + WRAPM(Units, isJobAvailable), { NULL, NULL } }; @@ -2321,6 +2327,25 @@ static int units_getProfessionName(lua_State *L) { return 1; } +int32_t units_getFocusPenalty(lua_State *L) { + auto unit = Lua::GetDFObject(L, 1); + Units::need_type_set needs; + auto top = lua_gettop(L); + if (top < 2) { + luaL_argerror(L, 2, "Expected at least one need type"); + } else { + for (int i = 2; i <= top; ++i) { + try { + needs.set(luaL_checkint(L, i)); + } catch (const std::out_of_range &e) { + luaL_argerror(L, i, "Expected a need type"); + } + } + Lua::Push(L, Units::getFocusPenalty(unit, needs)); + } + return 1; +} + static const luaL_Reg dfhack_units_funcs[] = { { "getPosition", units_getPosition }, { "getOuterContainerRef", units_getOuterContainerRef }, @@ -2335,6 +2360,7 @@ static const luaL_Reg dfhack_units_funcs[] = { { "getReadableName", units_getReadablename }, { "getVisibleName", units_getVisibleName }, { "getProfessionName", units_getProfessionName }, + { "getFocusPenalty", units_getFocusPenalty }, { NULL, NULL } }; diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index fc33fd3b0a4..a105930438b 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -342,13 +342,13 @@ DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); // get largest (i.e. most negative) focus penalty for a set of needs -using need_types_set = std::bitset; -DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_types_set need_types); +using need_type_set = std::bitset; +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_type_set need_types); // get focused penalty for a single need DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); // unit has an unbailable social activity (e.g. "Socialize!") -DFHACK_EXPORT bool unbailableSocialActivity(df::unit *unit); +DFHACK_EXPORT bool hasUnbailableSocialActivity(df::unit *unit); // unit can be assigned a job DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool interrupt_social); diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index e0bb857ad80..3c70807325c 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -525,7 +525,7 @@ df::job* DFHack::Job::createLinked() return job; } -bool assignToWorkshop(df::job *job, df::building_workshopst *workshop) +bool DFHack::Job::assignToWorkshop(df::job *job, df::building_workshopst *workshop) { CHECK_NULL_POINTER(job); CHECK_NULL_POINTER(workshop); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index d42087f7c85..086a2532f7e 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2023,7 +2023,7 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) { return entry->events[entry->events.size() - 1]; } -int32_t Units::getFocusPenalty(df::unit* unit, need_types_set need_types) { +int32_t Units::getFocusPenalty(df::unit* unit, need_type_set need_types) { CHECK_NULL_POINTER(unit); int max_penalty = INT_MAX; @@ -2037,24 +2037,24 @@ int32_t Units::getFocusPenalty(df::unit* unit, need_types_set need_types) { } int32_t Units::getFocusPenalty(df::unit* unit, df::need_type need_type) { - auto need_types = need_types_set().set(need_type); + auto need_types = need_type_set().set(need_type); return getFocusPenalty(unit, need_types); } // reverse engineered from unitst::have_unbailable_sp_activities (partial implementation) -bool Units::unbailableSocialActivity(df::unit *unit) +bool Units::hasUnbailableSocialActivity(df::unit *unit) { // these can become constexpr with C++23 - static const need_types_set pray_needs = need_types_set() + static const need_type_set pray_needs = need_type_set() .set(df::need_type::PrayOrMeditate); - static const need_types_set socialize_needs = need_types_set() + static const need_type_set socialize_needs = need_type_set() .set(df::need_type::Socialize) .set(df::need_type::BeCreative) .set(df::need_type::Excitement) .set(df::need_type::AdmireArt); - static const need_types_set read_needs = need_types_set() + static const need_type_set read_needs = need_type_set() .set(df::need_type::ThinkAbstractly) .set(df::need_type::LearnSomething); @@ -2097,7 +2097,7 @@ bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ if (activity && (activity->type == df::activity_entry_type::FillServiceOrder)) return false; } - if (unbailableSocialActivity(unit)) + if (hasUnbailableSocialActivity(unit)) return false; if (preserve_social && unit->social_activities.size() > 0) return false; From b307ef62ca09f5572d733c6d42d482b67b0338c7 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:21:49 +0200 Subject: [PATCH 438/919] fix unused variable warning --- library/LuaApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 192fc7beb5f..1989b9a78b5 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2337,7 +2337,7 @@ int32_t units_getFocusPenalty(lua_State *L) { for (int i = 2; i <= top; ++i) { try { needs.set(luaL_checkint(L, i)); - } catch (const std::out_of_range &e) { + } catch ([[maybe_unused]] const std::out_of_range &e) { luaL_argerror(L, i, "Expected a need type"); } } From 80f084b44ad1206cee529244a8f1017f5dc1ab4a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 17 Aug 2025 07:19:07 +0000 Subject: [PATCH 439/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 61e0e181953..b36c7396196 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 61e0e181953e70679d0ed4dfc7e9e64922ad0c07 +Subproject commit b36c7396196beeaaacdf518e96e23ea106d425d6 From 00faba712abd1b9687974d653a505b0f75da2b24 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:06:10 +0200 Subject: [PATCH 440/919] Update Authors.rst --- docs/about/Authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index e8030e881a2..8743b75a196 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -43,6 +43,7 @@ Cameron Ewell Ozzatron Carter Bray Qartar Chris Dombroski cdombroski Chris Parsons chrismdp +Christian Doczkal chdoc cjhammel cjhammel Clayton Hughes Clément Vuchener cvuchener From 7af372a3254e55354e899690f48c92858e3f6d4f Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:32:22 +0200 Subject: [PATCH 441/919] allow limiting egg protection to nestboxes inside a designated burrow --- docs/changelog.txt | 2 + docs/plugins/nestboxes.rst | 7 ++++ plugins/nestboxes.cpp | 75 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6a9ee5ec0d6..96a7887d92e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,8 @@ Template for new versions: ## New Features +- `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow + ## Fixes - ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession diff --git a/docs/plugins/nestboxes.rst b/docs/plugins/nestboxes.rst index 3c07cc30cab..a8c67dda333 100644 --- a/docs/plugins/nestboxes.rst +++ b/docs/plugins/nestboxes.rst @@ -10,9 +10,16 @@ This plugin will automatically scan for and forbid fertile eggs incubating in a nestbox so that dwarves won't come to collect them for eating. The eggs will hatch normally, even when forbidden. +You can control which eggs are collected and which eggs are protected by placing +the nestboxes whose contents should be protected inside a burrow and running +``nestboxes burrow ``. The default behavior of protecting all +fertile eggs in nest boxes can be reestablished by running ``nestboxes all``. + Usage ----- :: enable nestboxes + nestboxes burrow + nestbox all diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index b935c34f814..bce36f91034 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -1,6 +1,7 @@ #include "Debug.h" #include "PluginManager.h" +#include "modules/Burrows.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Persistence.h" @@ -8,6 +9,7 @@ #include "df/buildingitemst.h" #include "df/building_nest_boxst.h" +#include "df/burrow.h" #include "df/item.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -34,6 +36,7 @@ static PersistentDataItem config; enum ConfigValues { CONFIG_IS_ENABLED = 0, + CONFIG_BURROW = 1 }; static const int32_t CYCLE_TICKS = 7; // need to react quickly when eggs are laid/unforbidden @@ -41,9 +44,16 @@ static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static void do_cycle(color_ostream &out); +static command_result do_command(color_ostream &out, std::vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(control,out).print("initializing %s\n", plugin_name); + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Protect fertile eggs incubating in a nestbox.", + do_command)); + return CR_OK; } @@ -108,6 +118,65 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } +///////////////////////////////////////////////////// +// configuration logic +// + +static void setBurrow(color_ostream &out, const string &burrow_name){ + auto burrow = Burrows::findByName(burrow_name); + if (burrow) { + config.set_int(CONFIG_BURROW, burrow->id); + } else { + config.set_int(CONFIG_BURROW, -1); + } +} + +static df::burrow* getBurrow(color_ostream &out){ + int id = config.get_int(CONFIG_BURROW); + auto burrow = df::burrow::find(id); + if (!burrow) { + config.set_int(CONFIG_BURROW, -1); + } + return burrow; +} + +static void printStatus(color_ostream &out){ + if (!is_enabled) + out.print("%s is disabled\n", plugin_name); + else { + out.print("%s is enabled\n", plugin_name); + auto burrow = getBurrow(out); + if (burrow) + { + out.print("only protecting eggs inside burrow: %s\n", burrow->name.c_str()); + } + else + { + out.print("protecting all fertile eggs\n"); + } + } +} + +static command_result do_command(color_ostream &out, std::vector ¶meters){ + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (parameters.size() == 0 || parameters[0] == "status") + { + printStatus(out); + return CR_OK; + } else if (parameters.size() > 1 && parameters[0] == "burrow") { + setBurrow(out, parameters[1]); + return CR_OK; + } else if (parameters[0] == "all") { + config.set_int(CONFIG_BURROW, -1); + return CR_OK; + } else { + return CR_WRONG_USAGE; + } +} ///////////////////////////////////////////////////// // cycle logic // @@ -118,6 +187,9 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; + // see if egg protection is limited to a burrow + auto burrow = getBurrow(out); + for (df::building_nest_boxst *nb : world->buildings.other.NEST_BOX) { for (auto &contained_item : nb->contained_items) { if (contained_item->use_mode == df::building_item_role_type::PERM) @@ -126,6 +198,9 @@ static void do_cycle(color_ostream &out) { bool fertile = item->egg_flags.bits.fertile; if (item->flags.bits.forbid == fertile) continue; + if (burrow && !Burrows::isAssignedTile(burrow, Items::getPosition(item))) { + continue; + } item->flags.bits.forbid = fertile; if (fertile && item->flags.bits.in_job) { // cancel any job involving the egg From bc9290d78d1c944b1948247e176132b0f4cdc572 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 15:08:31 -0500 Subject: [PATCH 442/919] fix off-by-one in furniture stockpile export fixes #5557 --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b30979b7812..ce71b3edb8b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## Fixes - ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession +- `stockpiles`: fixed off-by-one error in exporting furniture stockpiles ## Misc Improvements diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 6abab93baf3..1be2ae550cd 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -1741,7 +1741,7 @@ bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileS all = false; continue; } - string f_type(type_traits::key_table[i]); + string f_type{ENUM_KEY_STR(furniture_type, furniture_type(i))}; furniture->add_type(f_type); DEBUG(log, out).print("furniture_type %zd is %s\n", i, f_type.c_str()); } From cb2f9a71ebca247132e9d4cae58f720ff90c89ef Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:02:50 -0500 Subject: [PATCH 443/919] add dye automation to tailor --- docs/plugins/tailor.rst | 14 ++ plugins/lua/tailor.lua | 8 + plugins/tailor.cpp | 333 ++++++++++++++++++++++++++++++++++------ 3 files changed, 308 insertions(+), 47 deletions(-) diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index c1732a741dd..b915afb264a 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -28,6 +28,7 @@ Usage tailor now tailor materials [ ...] tailor confiscate [true|false] + tailor dye [true|false] By default, ``tailor`` will prefer using materials in this order:: @@ -48,6 +49,11 @@ too precious to routinely make into cloth. ``tailor`` does not support modded "cloth" types which utilize custom reactions for making clothing out of those cloth types. +If dye management is enabled, ``tailor`` will also issue orders to dye cloth +as needed to fulfill fortress clothing requirements, and will issue orders to +manufacture dyes required to fulfill the dye cloth orders. Dye management is +disabled by default. + Examples -------- @@ -69,3 +75,11 @@ Caveats Modded cloth-like materials are not supported because custom reactions do not support being sized for non-dwarf races. The game only supports sizing the built-in default make-clothing or make-armor reactions. + +Dye automation will not not issue orders to mill dyes that are made at a +millstone or quern because Dwarf Fortress does not currently support +"mill plant" orders for a specified material. + +At present, dye automation uses any available dye and cloth will be dyed +with whatever color dye can be found or made. The ability to specify color +preferences may be added in the future. \ No newline at end of file diff --git a/plugins/lua/tailor.lua b/plugins/lua/tailor.lua index 46f3e712d9e..d32a4b75703 100644 --- a/plugins/lua/tailor.lua +++ b/plugins/lua/tailor.lua @@ -17,6 +17,7 @@ end function status() dfhack.print(('tailor is %s'):format(isEnabled() and "enabled" or "disabled")) print((' %s confiscating tattered clothing'):format(tailor_getConfiscate() and "and" or "but not")) + print(('tailor %s automating dye'):format(tailor_getAutomateDye() and "is" or "is not")) print('materials preference order:') for _,name in ipairs(tailor_getMaterialPreferences()) do print((' %s'):format(name)) @@ -38,6 +39,11 @@ function setConfiscate(opt) tailor_setConfiscate(fl) end +function setAutomateDye(opt) + local fl = argparse.boolean(opt[1], "set dye") + tailor_setAutomateDye(fl) +end + function parse_commandline(...) local args, opts = {...}, {} local positionals = process_args(opts, args) @@ -55,6 +61,8 @@ function parse_commandline(...) setMaterials(positionals) elseif command == 'confiscate' then setConfiscate(positionals) + elseif command == 'dye' then + setAutomateDye(positionals) else return false end diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index ef9b77c67aa..dde351f9dc8 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -2,6 +2,8 @@ * Tailor plugin. Automatically manages keeping your dorfs clothed. */ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -12,7 +14,9 @@ #include "modules/Units.h" #include "modules/World.h" +#include "df/building.h" #include "df/creature_raw.h" +#include "df/descriptor_color.h" #include "df/historical_entity.h" #include "df/item.h" #include "df/item_flags.h" @@ -25,6 +29,10 @@ #include "df/manager_order.h" #include "df/material.h" #include "df/plotinfost.h" +#include "df/reaction.h" +#include "df/reaction_reagent.h" +#include "df/reaction_reagent_itemst.h" +#include "df/reaction_product_itemst.h" #include "df/unit.h" #include "df/world.h" @@ -46,7 +54,9 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; +static const string CONFIG_KEY_2 = string(plugin_name) + "/config1"; static PersistentDataItem config; +static PersistentDataItem config2; enum ConfigValues { CONFIG_IS_ENABLED = 0, @@ -58,6 +68,11 @@ enum ConfigValues { CONFIG_CONFISCATE = 6 }; +enum Config2Values +{ + CONFIG_AUTOMATE_DYE = 0 +}; + static const int32_t CYCLE_TICKS = 1231; // one day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle @@ -144,11 +159,17 @@ class Tailor { bool confiscate = true; + bool automate_dye = false; + public: void set_confiscate(bool f){ confiscate = f; } bool get_confiscate() { return confiscate; } + void set_automate_dye(bool f) { automate_dye = f; } + + bool get_automate_dye() { return automate_dye; } + void sync_material_order() { material_order.clear(); @@ -426,45 +447,232 @@ class Tailor { DF2CONSOLE(m.name).c_str(), o->amount_left); } } + } + } - if (race == -1) - continue; // -1 means that the race of the worker will determine the size made; we must ignore these jobs + using jiqt = decltype(df::reaction_reagent_itemst::quantity); - int size = world->raws.creatures.all[race]->adultsize; + int get_reaction_max_count(df::reaction* r, jiqt lim = std::numeric_limits::max()) + { + jiqt max = lim; - auto tt = jobTypeMap.find(o->job_type); - if (tt == jobTypeMap.end()) + for (auto rr : r->reagents) + { + if (rr->getType() == df::reaction_reagent_type::item) { - continue; + max = std::min(get_reagent_max_count(virtual_cast(rr), max), max); + } + } + + return max; + } + + int get_reagent_max_count(df::reaction_reagent_itemst* r, jiqt lim = std::numeric_limits::max()) + { + df::reaction_reagent_itemst* t = df::allocate(); + *t = *r; + jiqt orig = t->quantity; + + std::vector tbc; + std::vector toc; + std::vector trc; + std::vector tcc; + + auto test = [&] (jiqt m) { + t->quantity = orig * m; + return t->have_enough_from_precalc_info(NULL, &tbc, &toc, &trc, &tcc, -1); + }; + + jiqt max = lim / orig; + + jiqt lo = 0; + jiqt hi = max; + + // exit fast when 0 + if (test(1)) + { + lo = 1; + + while (hi > lo + 1) + { + // can't do (hi+lo)/2 because hi+lo may overflow! + jiqt mult = hi / 2 + lo / 2; + if (mult == lo) mult += 1; + if (test(mult)) + lo = mult; + else + hi = mult; + } + } + + delete t; + + return lo; + } + + int count_dyeables(df::item_type ty) + { + auto reagent = df::allocate(); + reagent->item_subtype = -1; + reagent->mat_type = -1; + reagent->mat_index = -1; + reagent->flags2.bits.dyeable = true; + reagent->has_tool_use = df::tool_uses::NONE; + if (ty == df::item_type::SKIN_TANNED) + { + reagent->quantity = 1; + reagent->min_dimension = 1; + } + else + { + reagent->quantity = 10000; + reagent->min_dimension = 10000; + } + reagent->item_type = ty; + return get_reagent_max_count(reagent); + } + + int count_dyes() + { + auto reagent = df::allocate(); + reagent->item_type = df::item_type::NONE; + reagent->item_subtype = -1; + reagent->mat_type = -1; + reagent->mat_index = -1; + reagent->flags1.bits.unrotten = true; + reagent->flags2.bits.dye = true; + reagent->has_tool_use = df::tool_uses::NONE; + reagent->quantity = 1; + int count = get_reagent_max_count(reagent); + delete reagent; + return count; + } + + using color_type = decltype(MaterialInfo::material->powder_dye); + + static auto product_is_dye (df::reaction_product* r) -> bool + { + if (r->getType() == df::reaction_product_type::item) + { + auto rr = virtual_cast (r); + auto mat = MaterialInfo(rr->mat_type, rr->mat_index); + return mat.material && mat.material->flags.is_set(df::enums::material_flags::IS_DYE); + } + return false; + }; + + void order_dye_from_reaction(df::reaction* r, int c = 1) + { + std::string descr; + auto dye = std::ranges::find_if(r->products, product_is_dye); + assert(dye != r->products.end()); + auto pp = virtual_cast(*dye); + assert(pp != nullptr); + auto mat = MaterialInfo(pp->mat_type, pp->mat_index); + color_type color = mat.material->powder_dye; + + auto& color_name = world->raws.descriptors.colors[color]->name; + pp->getDescription(&descr); + + get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); + INFO(cycle).print("tailor: ordered %d %s\n", c, descr); + } + + void order_dye_cloth(int c = 1) + { + get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + } + + void make_dyes(int count) + { + auto reaction_produces_dye = [] (df::reaction* r) { + return std::ranges::any_of(r->products, product_is_dye); + }; + + for (auto r : std::ranges::views::filter(world->raws.reactions.reactions, reaction_produces_dye)) + { + auto& reaction_name = r->name; + + int max = get_reaction_max_count(r,1); + + if (max > 0) + { + order_dye_from_reaction(r, max); } - needed[std::make_pair(tt->second, size)] -= o->amount_left; - TRACE(cycle).print("tailor: existing order for %d %s of size %d detected\n", - o->amount_left, - ENUM_KEY_STR(job_type, o->job_type).c_str(), - size); + count = std::max(0, count - max); + if (count <= 0) + break; } + } + + int count_dye_cloth_orders() + { + auto f = [] (df::manager_order* o) { + return o->job_type == df::job_type::DyeCloth; + }; + int sum = 0; + for (auto o : std::ranges::views::filter(world->manager_orders.all, f)) + { + sum += o->amount_left; + } + return sum; } - static df::manager_order * get_existing_order(df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat) { - for (auto order : world->manager_orders.all) { - if (order->job_type == ty && - order->item_type == df::item_type::NONE && - order->item_subtype == sub && - order->mat_type == -1 && - order->mat_index == -1 && - order->specdata.hist_figure_id == hfid && - order->material_category.whole == mcat.whole && - order->frequency == df::workquota_frequency_type::OneTime) - return order; + using oqt = decltype(df::manager_order::amount_total); + + static df::manager_order* get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") + { + auto f = [&] (df::manager_order* order) { + return order->job_type == ty && + order->item_type == df::item_type::NONE && + order->item_subtype == sub && + order->mat_type == -1 && + order->mat_index == -1 && + order->specdata.race == hfid && + order->material_category.whole == mcat.whole && + order->frequency == df::workquota_frequency_type::OneTime && + order->reaction_name == custom_reaction; + }; + + auto orderIt = std::ranges::find_if(world->manager_orders.all, f); + + if (orderIt != world->manager_orders.all.end()) + { + auto o = *orderIt; + if (o->amount_left > 0) + { + o->amount_left = std::max(c, o->amount_left); + o->amount_total = std::max(c, o->amount_total); + } + return o; } - return NULL; + + auto order = new df::manager_order; + order->job_type = ty; + order->item_type = df::item_type::NONE; + order->item_subtype = sub; + order->reaction_name = custom_reaction; + order->specdata.race = hfid; + order->material_category = mcat; + order->mat_type = -1; + order->mat_index = -1; + order->amount_left = c; + order->amount_total = c; + order->status.bits.validated = false; + order->status.bits.active = false; + order->id = world->manager_orders.manager_order_next_id++; + + world->manager_orders.all.push_back(order); + + return order; } int place_orders() { int ordered = 0; + int skipped = 0; auto entity = world->entities.all[plotinfo->civ_id]; for (auto& o : orders) @@ -550,32 +758,11 @@ class Tailor { c = supply[m] - res; TRACE(cycle).print("tailor: order reduced from %d to %d to protect reserves of %s\n", count, c, DF2CONSOLE(m.name).c_str()); + skipped += (count - c); } supply[m] -= c; - auto order = get_existing_order(ty, sub, sizes[size], m.job_material); - if (order) { - if (order->amount_total > 0) { - order->amount_left += c; - order->amount_total += c; - } - } else { - order = new df::manager_order; - order->job_type = ty; - order->item_type = df::item_type::NONE; - order->item_subtype = sub; - order->mat_type = -1; - order->mat_index = -1; - order->amount_left = c; - order->amount_total = c; - order->status.bits.validated = false; - order->status.bits.active = false; - order->id = world->manager_orders.manager_order_next_id++; - order->specdata.hist_figure_id = sizes[size]; - order->material_category = m.job_material; - - world->manager_orders.all.push_back(order); - } + auto order = get_or_create_order(c, ty, sub, sizes[size], m.job_material); INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", order->id, @@ -590,12 +777,39 @@ class Tailor { } else { - TRACE(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + skipped += count; + INFO(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); } } } } + + if (skipped > 0) + { + INFO(cycle).print("tailor: %d item%s not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); + + if (automate_dye) + { + int available_dyes = count_dyes(); + int available_dyable_cloth = count_dyeables(df::item_type::CLOTH); + + int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), count_dyeables(df::item_type::CLOTH))) - count_dye_cloth_orders()); + if (to_dye > 0) + { + INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); + order_dye_cloth(to_dye); + } + + int dyes_to_make = std::max(0, skipped - available_dyes - to_dye); + if (dyes_to_make > 0) + { + INFO(cycle).print("tailor: ordering up to %d dyes\n", dyes_to_make); + make_dyes(dyes_to_make); + } + } + } + return ordered; } @@ -671,16 +885,27 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CONFISCATE, true); + config.set_bool(CONFIG_AUTOMATE_DYE, false); } // transition existing saves to CONFIG_CONFISCATE=true if (config.get_int(CONFIG_CONFISCATE) < 0) { DEBUG(control,out).print("found existing configuration with CONFIG_CONFISCATE unset, initializing to true\n"); config.set_bool(CONFIG_CONFISCATE, true); } + + config2 = World::GetPersistentSiteData(CONFIG_KEY_2); + // transition existing saves to CONFIG_AUTOMATE_DYE=false + if (config2.get_int(CONFIG_AUTOMATE_DYE) < 0) + { + DEBUG(control, out).print("found existing configuration with CONFIG_AUTOMATE_DYE unset, initializing to false\n"); + config2.set_bool(CONFIG_AUTOMATE_DYE, false); + } + is_enabled = config.get_bool(CONFIG_IS_ENABLED); DEBUG(control,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); tailor_instance->set_confiscate(config.get_bool(CONFIG_CONFISCATE)); + tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); DEBUG(control,out).print("loading persisted confiscation state: %s\n", tailor_instance->get_confiscate() ? "true" : "false"); tailor_instance->sync_material_order(); @@ -776,6 +1001,18 @@ static bool tailor_getConfiscate(color_ostream& out) return tailor_instance->get_confiscate(); } +static void tailor_setAutomateDye(color_ostream& out, bool enable) +{ + DEBUG(control, out).print("%s automation of dye\n", enable ? "enabling" : "disabling"); + config2.set_bool(CONFIG_AUTOMATE_DYE, enable); + tailor_instance->set_automate_dye(enable); +} + +static bool tailor_getAutomateDye(color_ostream& out) +{ + return tailor_instance->get_automate_dye(); +} + static int tailor_getMaterialPreferences(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) @@ -793,6 +1030,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences), DFHACK_LUA_FUNCTION(tailor_setConfiscate), DFHACK_LUA_FUNCTION(tailor_getConfiscate), + DFHACK_LUA_FUNCTION(tailor_setAutomateDye), + DFHACK_LUA_FUNCTION(tailor_getAutomateDye), DFHACK_LUA_END }; From 078741e21040f7a193ebc10688d4df9bcd87e226 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:09:15 -0500 Subject: [PATCH 444/919] Update StockpileSerializer.cpp remove no longer used type alias --- plugins/stockpiles/StockpileSerializer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 1be2ae550cd..efac3e8bad1 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -1731,7 +1731,6 @@ static bool furniture_mat_is_allowed(const MaterialInfo& mi) { bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileSettings::FurnitureSet* furniture) { using df::enums::furniture_type::furniture_type; - using type_traits = df::enum_traits; auto & pfurniture = mSettings->furniture; bool all = true; From 59238a31d07542fe074cfac9343adb3eff216236 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:13:25 -0500 Subject: [PATCH 445/919] Update tailor.cpp Clean up unused variables, fix type error --- plugins/tailor.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index dde351f9dc8..f5e4a272071 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -436,8 +436,6 @@ class Tailor { if (f == jobTypeMap.end()) continue; - int race = o->specdata.hist_figure_id; - for (auto& m : all_materials) { if (o->material_category.whole == m.job_material.whole) @@ -571,11 +569,10 @@ class Tailor { auto mat = MaterialInfo(pp->mat_type, pp->mat_index); color_type color = mat.material->powder_dye; - auto& color_name = world->raws.descriptors.colors[color]->name; pp->getDescription(&descr); get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); - INFO(cycle).print("tailor: ordered %d %s\n", c, descr); + INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr.c_str())); } void order_dye_cloth(int c = 1) @@ -591,8 +588,6 @@ class Tailor { for (auto r : std::ranges::views::filter(world->raws.reactions.reactions, reaction_produces_dye)) { - auto& reaction_name = r->name; - int max = get_reaction_max_count(r,1); if (max > 0) @@ -792,9 +787,9 @@ class Tailor { if (automate_dye) { int available_dyes = count_dyes(); - int available_dyable_cloth = count_dyeables(df::item_type::CLOTH); + int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); - int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), count_dyeables(df::item_type::CLOTH))) - count_dye_cloth_orders()); + int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), available_dyeable_cloth)) - count_dye_cloth_orders()); if (to_dye > 0) { INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); From 734623bec5afc314805e82fc3ce1c12066705656 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:14:28 -0500 Subject: [PATCH 446/919] whitespace --- plugins/tailor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index f5e4a272071..0780667170c 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -1033,4 +1033,4 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(tailor_getMaterialPreferences), DFHACK_LUA_END -}; +}; \ No newline at end of file From 28dabce6082f31301f8c9ca6fbfb13a5f53040dd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:15:26 -0500 Subject: [PATCH 447/919] actual whitespace --- docs/plugins/tailor.rst | 2 +- plugins/tailor.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index b915afb264a..9e59957810f 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -82,4 +82,4 @@ millstone or quern because Dwarf Fortress does not currently support At present, dye automation uses any available dye and cloth will be dyed with whatever color dye can be found or made. The ability to specify color -preferences may be added in the future. \ No newline at end of file +preferences may be added in the future. diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 0780667170c..f5e4a272071 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -1033,4 +1033,4 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(tailor_getMaterialPreferences), DFHACK_LUA_END -}; \ No newline at end of file +}; From e53224d2570c897e9fe5074c3707a4ff6050190a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:18:43 -0500 Subject: [PATCH 448/919] More minor cleanup --- plugins/tailor.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index f5e4a272071..6bb63f6ee82 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -567,12 +567,11 @@ class Tailor { auto pp = virtual_cast(*dye); assert(pp != nullptr); auto mat = MaterialInfo(pp->mat_type, pp->mat_index); - color_type color = mat.material->powder_dye; pp->getDescription(&descr); get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); - INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr.c_str())); + INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); } void order_dye_cloth(int c = 1) From a93ace4e87ab17c6686a3312cc481668ee3a0af0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 16:21:50 -0500 Subject: [PATCH 449/919] one more unused variable gcc, why can't you tell me all of them at once? --- plugins/tailor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 6bb63f6ee82..513e1a95432 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -566,7 +566,6 @@ class Tailor { assert(dye != r->products.end()); auto pp = virtual_cast(*dye); assert(pp != nullptr); - auto mat = MaterialInfo(pp->mat_type, pp->mat_index); pp->getDescription(&descr); From ee34213f72eea6610f16f2a8ec7d53a39d701a64 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:23:49 +0000 Subject: [PATCH 450/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index fc6dcd5aa07..6af6484430d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit fc6dcd5aa077a0d96d81bd2dfe89e24bed375472 +Subproject commit 6af6484430d633f059a40998d4e8b3a2c0cc863f diff --git a/plugins/stonesense b/plugins/stonesense index 15add1726c7..fba957f2699 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 15add1726c711b5b8c4d0ae5afe42b8f841d3af6 +Subproject commit fba957f2699987cfec89cc1f18552bb7fc07f1eb diff --git a/scripts b/scripts index b36c7396196..6749ada0aa9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b36c7396196beeaaacdf518e96e23ea106d425d6 +Subproject commit 6749ada0aa923935d2997f0ea8c3ca58d532a4d0 From 0f5b65b8af3bc22d05507e17bd29bbbdb972ca98 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:45:02 +0000 Subject: [PATCH 451/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 6af6484430d..d84e0708515 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 6af6484430d633f059a40998d4e8b3a2c0cc863f +Subproject commit d84e07085151f3b177fe257232322d7010c5a2e5 diff --git a/scripts b/scripts index 6749ada0aa9..e2e976e3f4a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6749ada0aa923935d2997f0ea8c3ca58d532a4d0 +Subproject commit e2e976e3f4a17f16d7d0e8d0c8fb09adaf4aa5dc From a2d0f94f60e070f776a44642376cb7790e4405ed Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 18 Aug 2025 10:00:08 -0500 Subject: [PATCH 452/919] bump to 52.03-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a0875d2c321..e150f7255a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.03") -set(DFHACK_RELEASE "r1.1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 3347184e0b297ef7aec4e588163a8c6019c8d403 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 18 Aug 2025 11:13:48 -0500 Subject: [PATCH 453/919] update changelog to conform to prerelease --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ce71b3edb8b..dc0f1d296e4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,8 +54,8 @@ Template for new versions: ## New Tools ## New Features - - `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow +- `tailor`: tailor now provides optional dye automation ## Fixes - ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession From 6febec7b0bab9e098a2a6ff6cd371651cf802ef8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:19:37 +0000 Subject: [PATCH 454/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index fba957f2699..78ea0e4906a 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit fba957f2699987cfec89cc1f18552bb7fc07f1eb +Subproject commit 78ea0e4906af869c7b6b2ab3b64d833953e2ab2f From 867da3e0b63f63e2a25dd8757aac3fd2bbcf61ec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 09:32:51 -0500 Subject: [PATCH 455/919] tailor: fixes from testing add missing binding for second config persistence item improve logging --- plugins/tailor.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 513e1a95432..bfcd4f09345 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -771,7 +771,7 @@ class Tailor { else { skipped += count; - INFO(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + DEBUG(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); } } @@ -786,15 +786,19 @@ class Tailor { { int available_dyes = count_dyes(); int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); + int dye_cloth_orders = count_dye_cloth_orders(); - int to_dye = std::max(0, std::min(skipped, std::min(count_dyes(), available_dyeable_cloth)) - count_dye_cloth_orders()); + DEBUG(cycle).print("tailor: available dyes = %d, available dyeable cloth = %d, dye cloth orders = %d\n", + available_dyes, available_dyeable_cloth, dye_cloth_orders); + int to_dye = std::min(skipped, std::min(available_dyes, available_dyeable_cloth) - dye_cloth_orders); + DEBUG(cycle).print("tailor: to dye = %d\n", to_dye); if (to_dye > 0) { INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); order_dye_cloth(to_dye); } - int dyes_to_make = std::max(0, skipped - available_dyes - to_dye); + int dyes_to_make = available_dyes - to_dye; if (dyes_to_make > 0) { INFO(cycle).print("tailor: ordering up to %d dyes\n", dyes_to_make); @@ -872,6 +876,7 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { DFhackCExport command_result plugin_load_site_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentSiteData(CONFIG_KEY); + config2 = World::GetPersistentSiteData(CONFIG_KEY_2); if (!config.isValid()) { DEBUG(control,out).print("no config found in this save; initializing\n"); From 0f389d195f35aa02bbccaf3d0c485392132054b2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:20:37 +0000 Subject: [PATCH 456/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e2e976e3f4a..4a950bd5b2b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e2e976e3f4a17f16d7d0e8d0c8fb09adaf4aa5dc +Subproject commit 4a950bd5b2b9dae26fdfed14e24afda7b7f2a4b1 From 56f9a23def0803be26dcb30ba803c967e116cbe7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 19:16:12 -0500 Subject: [PATCH 457/919] tailor: logging tweaks, configuration --- plugins/tailor.cpp | 65 ++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index bfcd4f09345..9f658fff135 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -546,6 +546,8 @@ class Tailor { return count; } + using oqt = decltype(df::manager_order::amount_total); + using color_type = decltype(MaterialInfo::material->powder_dye); static auto product_is_dye (df::reaction_product* r) -> bool @@ -559,7 +561,7 @@ class Tailor { return false; }; - void order_dye_from_reaction(df::reaction* r, int c = 1) + oqt order_dye_from_reaction(df::reaction* r, int c = 1) { std::string descr; auto dye = std::ranges::find_if(r->products, product_is_dye); @@ -569,13 +571,16 @@ class Tailor { pp->getDescription(&descr); - get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); - INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); + if (n > 0) + INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + return n; } - void order_dye_cloth(int c = 1) + oqt order_dye_cloth(int c = 1) { - get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + auto [_, n] = get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + return n; } void make_dyes(int count) @@ -590,7 +595,7 @@ class Tailor { if (max > 0) { - order_dye_from_reaction(r, max); + max = order_dye_from_reaction(r, max); } count = std::max(0, count - max); @@ -613,9 +618,7 @@ class Tailor { return sum; } - using oqt = decltype(df::manager_order::amount_total); - - static df::manager_order* get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") + static std::pair get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") { auto f = [&] (df::manager_order* order) { return order->job_type == ty && @@ -634,12 +637,15 @@ class Tailor { if (orderIt != world->manager_orders.all.end()) { auto o = *orderIt; + int chg = 0; if (o->amount_left > 0) { + int prev = o->amount_left; o->amount_left = std::max(c, o->amount_left); o->amount_total = std::max(c, o->amount_total); + chg = o->amount_left - prev; } - return o; + return {o, chg}; } auto order = new df::manager_order; @@ -659,7 +665,7 @@ class Tailor { world->manager_orders.all.push_back(order); - return order; + return {order, c}; } int place_orders() @@ -755,18 +761,21 @@ class Tailor { } supply[m] -= c; - auto order = get_or_create_order(c, ty, sub, sizes[size], m.job_material); - - INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", - order->id, - c, - bitfield_to_string(order->material_category).c_str(), - DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), - DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() - ); + auto [order,n] = get_or_create_order(c, ty, sub, sizes[size], m.job_material); - count -= c; - ordered += c; + if (n > 0) + { + INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", + order->id, + n, + bitfield_to_string(order->material_category).c_str(), + DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), + DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() + ); + + count -= n; + ordered += n; + } } else { @@ -794,8 +803,9 @@ class Tailor { DEBUG(cycle).print("tailor: to dye = %d\n", to_dye); if (to_dye > 0) { - INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); - order_dye_cloth(to_dye); + int dyed = order_dye_cloth(to_dye); + if (dyed > 0) + INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); } int dyes_to_make = available_dyes - to_dye; @@ -883,7 +893,12 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CONFISCATE, true); - config.set_bool(CONFIG_AUTOMATE_DYE, false); + } + if (!config2.isValid()) + { + DEBUG(control, out).print("no extended config found in this save; initializing\n"); + config2 = World::AddPersistentSiteData(CONFIG_KEY_2); + config2.set_bool(CONFIG_AUTOMATE_DYE, false); } // transition existing saves to CONFIG_CONFISCATE=true if (config.get_int(CONFIG_CONFISCATE) < 0) { From 2aa6ab03b42b47fd632506d4f0ba8dc97b9adeed Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 19:19:13 -0500 Subject: [PATCH 458/919] make gcc happier --- plugins/tailor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 9f658fff135..7409c0eb7ca 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -573,7 +573,9 @@ class Tailor { auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); if (n > 0) + { INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + } return n; } @@ -805,7 +807,9 @@ class Tailor { { int dyed = order_dye_cloth(to_dye); if (dyed > 0) + { INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); + } } int dyes_to_make = available_dyes - to_dye; From c7e066eee49b532d9a1559d929ac7185ceb5a4e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 20 Aug 2025 18:48:12 -0500 Subject: [PATCH 459/919] tailor: fix configuration transition --- plugins/tailor.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 7409c0eb7ca..c66d314431f 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -54,7 +54,7 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string CONFIG_KEY_2 = string(plugin_name) + "/config1"; +static const string CONFIG_KEY_2 = string(plugin_name) + "/config_2"; static PersistentDataItem config; static PersistentDataItem config2; @@ -885,8 +885,6 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { return CR_OK; } - - DFhackCExport command_result plugin_load_site_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentSiteData(CONFIG_KEY); @@ -910,7 +908,6 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { config.set_bool(CONFIG_CONFISCATE, true); } - config2 = World::GetPersistentSiteData(CONFIG_KEY_2); // transition existing saves to CONFIG_AUTOMATE_DYE=false if (config2.get_int(CONFIG_AUTOMATE_DYE) < 0) { @@ -922,9 +919,12 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DEBUG(control,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); tailor_instance->set_confiscate(config.get_bool(CONFIG_CONFISCATE)); - tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); DEBUG(control,out).print("loading persisted confiscation state: %s\n", tailor_instance->get_confiscate() ? "true" : "false"); + tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); + DEBUG(control, out).print("loading persisted dye automation state: %s\n", + tailor_instance->get_automate_dye() ? "true" : "false"); + tailor_instance->sync_material_order(); return CR_OK; From a2a768c1df35f533eb6db2c9f375171962682de5 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:49:43 +0200 Subject: [PATCH 460/919] two minor fixes for #5535 --- docs/dev/Lua API.rst | 5 +++-- library/include/modules/Units.h | 2 +- library/modules/Units.cpp | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d31a8296aa4..86e90da9c17 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1908,11 +1908,12 @@ Units module Unit has an uninterruptible social activity (e.g. a purple "Socialize!"). -* ``dfhack.units.isJobAvailable(unit [, interrupt_social])`` +* ``dfhack.units.isJobAvailable(unit [, preserve_social])`` Check whether a unit can be assigned to (i.e. is looking for) a job. Will return ``true`` if the unit is engaged in "green" social activities, unless - the boolean ``interrupt_social`` is true. + the boolean ``preserve_social`` is true. Will never interrupt uninterruptible + social activities (e.g. a purple "Socialize!"). * ``dfhack.units.getFocusPenalty(unit, need_type [, need_type, ...])`` diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index a105930438b..b13a75d6a3b 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -350,7 +350,7 @@ DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); // unit has an unbailable social activity (e.g. "Socialize!") DFHACK_EXPORT bool hasUnbailableSocialActivity(df::unit *unit); // unit can be assigned a job -DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool interrupt_social); +DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool preserve_social); // Stress categories. 0 is highest stress, 6 is lowest. DFHACK_EXPORT extern const std::vector stress_cutoffs; diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 086a2532f7e..d8bfebdc976 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2090,6 +2090,8 @@ bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ return false; if (unit->flags1.bits.caged || unit->flags1.bits.chained) return false; + if (Units::getSpecificRef(unit, df::specific_ref_type::ACTIVITY)) + return false; if (unit->individual_drills.size() > 0) { if (unit->individual_drills.size() > 1) return false; // this is even possible From 7ca63173c3730257c83d89d51cca0294a6ba1c5b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 22 Aug 2025 03:01:14 +0000 Subject: [PATCH 461/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4a950bd5b2b..93bdca9d4d2 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4a950bd5b2b9dae26fdfed14e24afda7b7f2a4b1 +Subproject commit 93bdca9d4d234ab982867b8e71ad052a82fb9421 From 430678d7a46ce3c0ceb5a158eb0411eb92b8a0f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 Aug 2025 00:36:55 -0500 Subject: [PATCH 462/919] update for 52.03-r2 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 19 ++++++++++++++++++- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e150f7255a4..9ed54be78a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.03") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DFHACK_RELEASE "r2") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) diff --git a/docs/changelog.txt b/docs/changelog.txt index dc0f1d296e4..7e27d7f4c9b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -53,6 +53,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.03-r2 + +## New Tools + ## New Features - `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow - `tailor`: tailor now provides optional dye automation @@ -70,7 +88,6 @@ Template for new versions: - ``Units``: new functions ``getFocusPenalty``, ``unbailableSocialActivity``, ``isJobAvailable`` ## Lua - - New functions: ``dfhack.jobs.createLinked``, ``dfhack.jobs.assignToWorkshop``, ``dfhack.units.getFocusPenalty``, ``dfhack.units.unbailableSocialActivity``, and ``dfhack.units.isJobAvailable`` ## Removed diff --git a/library/xml b/library/xml index d84e0708515..e4993ceef79 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d84e07085151f3b177fe257232322d7010c5a2e5 +Subproject commit e4993ceef7907351fd2ac6f9bd3a9e5ff24699db diff --git a/plugins/stonesense b/plugins/stonesense index 78ea0e4906a..08102c59ef1 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 78ea0e4906af869c7b6b2ab3b64d833953e2ab2f +Subproject commit 08102c59ef1c76b1cebe3e582be51f0ac763de0e diff --git a/scripts b/scripts index 93bdca9d4d2..539120522a8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 93bdca9d4d234ab982867b8e71ad052a82fb9421 +Subproject commit 539120522a808220e4ab23538bad5cfcd979211e From 95e41a0bf612398dc84b0148876258c52ed75bf7 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 01:18:41 -0400 Subject: [PATCH 463/919] Update changelog syntax reference, and move out the changelog template --- docs/changelog.txt | 29 ++++++++++++++++------------- docs/dev/Documentation.rst | 4 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7e27d7f4c9b..fc0031b4fe1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,26 +1,30 @@ === Scroll down for changes ===[[[ -The text below is included in docs/dev/Documentation.rst - see that file for more details on the changelog setup. -This is kept in this file as a quick syntax reference. +For information on how to edit/build the changelogs, see `docs/html/docs/dev/Documentation.html` or `docs/dev/Documentation.rst`. -===help +The text between the `syntax-reference` markers is included in `docs/dev/Documentation.rst`, so it must be valid RST. +It is kept in this file as a quick syntax reference. -changelog.txt uses a syntax similar to RST, with a few special sequences: +===syntax-reference-start + +The changelogs use a syntax similar to RST, with a few special sequences: - ``===`` indicates the start of a comment - ``#`` indicates the start of a release name (do not include "DFHack") -- ``##`` indicates the start of a section name (this must be listed in ``gen_changelog.py``) +- ``##`` indicates the start of a section name (which must be listed in ``docs/sphinx_extensions/dfhack/changelog.py``) - ``-`` indicates the start of a changelog entry. **Note:** an entry currently must be only one line. -- ``:`` (colon followed by space) separates the name of a feature from a description of a change to that feature. - Changes made to the same feature are grouped if they end up in the same section. -- ``:\`` (colon, backslash, space) avoids the above behavior -- ``- @`` (the space is optional) indicates the start of an entry that should only be displayed in NEWS-dev.rst. - Use this sparingly, e.g. for immediate fixes to one development build in another development build that - are not of interest to users of stable builds only. +- ``:`` (followed by space) separates the name of a feature from a description of a change to that feature. + - Changes made to the same feature are grouped if they end up in the same section. +- ``:\`` (followed by space) avoids the above behavior +- ``- @`` (the space is optional) indicates the start of an entry that should only be displayed in ``NEWS-dev.rst``. + - Use this sparingly, e.g. for immediate fixes to one development build in another development build that + are not of interest to users of stable builds only. - Three ``[`` characters indicate the start of a block (possibly a comment) that spans multiple lines. Three ``]`` characters indicate the end of such a block. -- ``!`` immediately before a phrase set up to be replaced (see gen_changelog.py) stops that occurrence from being replaced. +- ``!`` immediately before a configured replacement (see ``docs/sphinx_extensions/dfhack/changelog.py``) stops that occurrence from being replaced. + +===syntax-reference-end Template for new versions: @@ -40,7 +44,6 @@ Template for new versions: ## Removed -===end ]]] ================================================================================ diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 13eaf42da04..19a243f31cd 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -515,8 +515,8 @@ Changelog syntax ---------------- .. include:: /docs/changelog.txt - :start-after: ===help - :end-before: ===end + :start-after: ===syntax-reference-start + :end-before: ===syntax-reference-end .. _docs-ci: From a308c260a6634711cfe03012d91cc85fa1e271f7 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 01:24:09 -0400 Subject: [PATCH 464/919] Remove unnecessary blank lines from output when building docs under CMake --- CMakeLists.txt | 2 +- docs/build.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ed54be78a3..eeb548f7f0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -523,7 +523,7 @@ if(BUILD_DOCS) add_custom_command(OUTPUT ${SPHINX_OUTPUT} COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/docs/build.py" - ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" -- -q -W + ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" --quiet -- -W DEPENDS ${SPHINX_DEPS} COMMENT "Building documentation with Sphinx" ) diff --git a/docs/build.py b/docs/build.py index bfb6780b23c..bf0dd9e4883 100755 --- a/docs/build.py +++ b/docs/build.py @@ -62,6 +62,8 @@ def output_format(s): help='Sphinx executable to run [environment variable: SPHINX; default: "sphinx-build"]') parser.add_argument('-j', '--jobs', type=str, default=os.environ.get('JOBS', 'auto'), help='Number of Sphinx threads to run [environment variable: JOBS; default: "auto"]') + parser.add_argument('-q', '--quiet', action='store_true', + help='Disable most output on stdout (also passed to sphinx-build)') parser.add_argument('--debug', action='store_true', help='Log commands that are run, etc.') parser.add_argument('--offline', action='store_true', @@ -91,6 +93,8 @@ def output_format(s): command = [args.sphinx] + OUTPUT_FORMATS[format_name].args + ['-j', args.jobs] if args.clean: command += ['-E'] + if args.quiet: + command += ['-q'] command += forward_args if args.debug: @@ -98,4 +102,5 @@ def output_format(s): print('Running:', command) subprocess.run(command, check=True, env=sphinx_env) - print('') + if not args.quiet: + print('') From 548087db149e4b206a7719ea60725b1e71743b01 Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 01:52:03 -0400 Subject: [PATCH 465/919] Rearrange/clarify use of docs/build.py --- docs/dev/Documentation.rst | 67 ++++++++++++++++++++++-------------- docs/dev/compile/Options.rst | 18 +++++++--- 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 19a243f31cd..12637af0bf9 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -418,6 +418,8 @@ Once you have pip available, you can install Sphinx with the following command:: Note that this may require opening a new (admin) command prompt if you just installed pip from the same command prompt. +.. _docs-build: + Building the documentation ========================== @@ -427,16 +429,18 @@ Sphinx to build the docs: Using CMake ----------- -See our page on `build options ` +See our page on `build options `. -Running Sphinx manually ------------------------ +Using the documentation build script +------------------------------------ You can also build the documentation without running CMake - this is faster if -you only want to rebuild the documentation regardless of any code changes. The -``docs/build.py`` script will build the documentation in any specified formats -(HTML only by default) using the same command that CMake runs when building the -docs. Run the script with ``--help`` to see additional options. +you only want to rebuild the documentation regardless of any code changes. + +The recommended approach is the ``docs/build.py`` script. This is the same +script that CMake uses internally, which wraps Sphinx with a few additional +options to handle common cases and can build multiple documentation formats with +a single invocation. Examples: @@ -449,37 +453,50 @@ Examples: * ``docs/build.py --clean`` Build HTML and force a clean build (all source files are re-read) -The resulting documentation will be stored in ``docs/html`` and/or ``docs/text``. - -Alternatively, you can run Sphinx manually with:: - - sphinx-build . docs/html - -or, to build plain-text output:: +* ``docs/build.py --help`` + Display a full list of available options - sphinx-build -b text . docs/text - -Sphinx has many options to enable clean builds, parallel builds, logging, and -more - run ``sphinx-build --help`` for details. If you specify a different -output path, be warned that Sphinx may overwrite existing files in the output -folder. Also be aware that when running ``sphinx-build`` directly, the -``docs/html`` folder may be polluted with intermediate build files that normally -get written in the cmake ``build`` directory. +The resulting documentation will be stored in ``docs/html`` and/or ``docs/text`` +(or generally, a subfolder of ``docs/`` named after the requested output format(s)). Building a PDF version ---------------------- ReadTheDocs automatically builds a PDF version of the documentation (available under the "Downloads" section when clicking on the release selector). If you -want to build a PDF version locally, you will need ``pdflatex``, which is part +want to build a PDF version locally, you will need the ``pdflatex`` command, which is part of a TeX distribution. The following command will then build a PDF, located in ``docs/pdf/latex/DFHack.pdf``, with default options:: docs/build.py pdf -Alternatively, you can run Sphinx manually with:: +Running Sphinx manually +----------------------- + +If ``docs/build.py`` does not support what you need, you can also run Sphinx +manually. This is primarily useful for low-level debugging. + +For a good starting point, add the ``--debug`` argument to your call to +``docs/build.py``. This will cause the script to print out the Sphinx command(s) +that it is running. + +Some examples: + +* ``sphinx-build . docs/html`` + Build the HTML docs (equivalent to ``docs/build.py``) + +* ``sphinx-build -b text . docs/text`` + Build the plain text docs (equivalent to ``docs/build.py text``) - sphinx-build -M latexpdf . docs/pdf +* ``sphinx-build -M latexpdf . docs/pdf`` + Build the PDF docs + +Sphinx has many options to enable clean builds, parallel builds, logging, and +more - run ``sphinx-build --help`` for details. If you specify a different +output path, be warned that Sphinx may overwrite existing files in the output +folder. Also be aware that when running ``sphinx-build`` directly, the +``docs/html`` folder may be polluted with intermediate build files that normally +get written in the cmake ``build`` directory. .. _build-changelog: diff --git a/docs/dev/compile/Options.rst b/docs/dev/compile/Options.rst index b314e9db279..462fd1f1ca0 100644 --- a/docs/dev/compile/Options.rst +++ b/docs/dev/compile/Options.rst @@ -136,16 +136,26 @@ Usage:: Documentation ============= -If you need to build documentation. Documentation can be built as HTML, and PDF, -but there are also plain text files generated for in-game. +If you need to build `documentation `. -Variable: ``BUILD_DOCS`` +.. note:: + + These options are primarily useful for verifying that the end-to-end process + for building and packaging the documentation is working as expected. For + iterating on documentation changes, `faster alternatives ` are + available. + +Variables: + +* ``BUILD_DOCS``: enables the default documentation build +* ``BUILD_DOCS_NO_HTML``: disables the HTML documentation build (only builds the text documentation used in-game) Usage:: cmake .. -DBUILD_DOCS:bool=ON cmake .. -DBUILD_DOCS=1 - + cmake .. -DBUILD_DOCS_NO_HTML:bool=ON + cmake .. -DBUILD_DOCS_NO_HTML=1 The generated documentation is stored in ``docs/html`` and ``docs/text`` (respectively) in the root DFHack folder, and they will both be installed to ``hack/docs`` when you From 26fcf3694595bae7cf13304a42243aa75979fe4c Mon Sep 17 00:00:00 2001 From: lethosor Date: Sun, 24 Aug 2025 02:11:41 -0400 Subject: [PATCH 466/919] Add some basic troubleshooting tips for the docs build --- docs/dev/Documentation.rst | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 12637af0bf9..0ba61c404d8 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -67,6 +67,8 @@ with relevant tags. These are used to compile indices and generate cross-links b commands, both in the HTML documents and in-game. See the list of available `tag-list` and think about which categories your new tool belongs in. +.. _docs-links: + Links ----- @@ -498,6 +500,48 @@ folder. Also be aware that when running ``sphinx-build`` directly, the ``docs/html`` folder may be polluted with intermediate build files that normally get written in the cmake ``build`` directory. +Troubleshooting +=============== + +Sphinx errors are typically printed by Sphinx, so ensure that you are not silencing Sphinx output. + +When built with ``docs/build.py`` or CMake, errors are also logged to +``build/docs//sphinx-warnings.txt`` (for instance, if you are building +the HTML docs, ``build/docs/html/sphinx-warnings.txt``). + + +"undefined label" +----------------- + +Typical causes: + +* You have used single backticks for an inline code snippet, where double backticks should be used instead (see `docs-links`):: + + `this is an invalid inline code snippet (actually a link)` + ``this is a valid inline code snippet`` + +* You are attempting to link to a section/label, but either it does not have a label defined or you have spelled it incorrectly:: + + .. my-label: + + This is where the link should go to. + + ... + + This is `a valid link to the earlier label `. So is `my-label`. + +"toctree contains reference to document that doesn't have a title" +------------------------------------------------------------------ + +Due to the nature of our autogenerated documentation, this can sometimes occur +when switching between branches that have different autogenerated files, and can +result in autogenerated documentation (e.g. for individual tools) being missing +from the table of contents, or links failing to generate. + +The quickest resolution is a clean docs build:: + + docs/build.py --clean + .. _build-changelog: Building the changelogs From 67d4d915d6f5c18116eb4b8870db6aea7b97af49 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:05:49 +0000 Subject: [PATCH 467/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index e4993ceef79..bd52dfb23f2 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e4993ceef7907351fd2ac6f9bd3a9e5ff24699db +Subproject commit bd52dfb23f28e40350f88fe3c81e63f5edc38f89 From e83fce914640ab93d2dbaf18eedf7926950dc8f2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 28 Aug 2025 15:05:52 -0500 Subject: [PATCH 468/919] bump version to 52.04 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ed54be78a3..cab5c6a8ce9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.03") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "52.04") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From a7d8acafa8d9afb5ff91a34e45d36a688b1d03b3 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:15:41 +0000 Subject: [PATCH 469/919] Auto-update structures ref for 52.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index bd52dfb23f2..7b691d256f9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit bd52dfb23f28e40350f88fe3c81e63f5edc38f89 +Subproject commit 7b691d256f9427036e7ff24fa795a0f9334739e7 From f37f004fa673d5e1219fdb560a12acb84902c5c5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 28 Aug 2025 15:56:59 -0500 Subject: [PATCH 470/919] changelog for 52.04-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7e27d7f4c9b..a2e558aeb23 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,24 @@ Template for new versions: ## Removed +# 52.04-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 52.03-r2 ## New Tools From 5d5288aa2b1bcb6cf72d56b2baed91d16a5ebcf5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 28 Aug 2025 16:09:17 -0500 Subject: [PATCH 471/919] make changelog nonempty --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index a2e558aeb23..f6421c7d89e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,6 +72,7 @@ Template for new versions: ## New Tools ## New Features +- Compatibility with DF 52.04 ## Fixes From cba221330e1e6e580b8c4cb47af95ed394c9e40f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:38:28 +0000 Subject: [PATCH 472/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/python-jsonschema/check-jsonschema: 0.33.2 → 0.33.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.2...0.33.3) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d79077b318d..28599a14ae9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.33.3 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 34f5bf8217b1eeb447d7788654242e634731ceca Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Mon, 1 Sep 2025 20:00:31 -0700 Subject: [PATCH 473/919] Remove unused declaration in Burrows.cpp --- library/modules/Burrows.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index 3c641e79b74..6c8802469c2 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -99,8 +99,6 @@ bool Burrows::isAssignedUnit(df::burrow *burrow, df::unit *unit) void Burrows::setAssignedUnit(df::burrow *burrow, df::unit *unit, bool enable) { - using df::global::plotinfo; - CHECK_NULL_POINTER(unit); CHECK_NULL_POINTER(burrow); From d5ffeb3e9181450fff3775f3c84c5b82e70348b3 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Tue, 2 Sep 2025 11:57:24 -0700 Subject: [PATCH 474/919] Comment out unused declarations in remotefortressreader.cpp --- plugins/remotefortressreader/remotefortressreader.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index 697eb3fe921..a7bfccd7ce5 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -1679,8 +1679,8 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques continue; } - using df::global::cur_year; - using df::global::cur_year_tick; + //using df::global::cur_year; + //using df::global::cur_year_tick; send_unit->set_age(Units::getAge(unit, false)); From 2ef698c24de7f5ebec81da4d10a2821d81229575 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 7 Sep 2025 12:02:32 -0700 Subject: [PATCH 475/919] zone.lua pits, ponds, and cages: no trader-owned small pets. --- docs/changelog.txt | 1 + plugins/lua/zone.lua | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f6421c7d89e..d07fb869bcb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- small animals owned by traders can no longer be assigned to pits, ponds, and cages. ## Misc Improvements diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 92940ef48ed..7dce4aee148 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -602,7 +602,9 @@ local function is_assignable_unit(unit) end local function is_assignable_item(item) - -- all vermin/small pets are assignable + -- small pets owned by traders are not assignable + if item.flags.trader then return false end + -- other vermin/small pets are assignable return true end From be8594a6bfd4a900ec3d4b04cd1d98db1b1fae38 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 7 Sep 2025 16:53:47 -0500 Subject: [PATCH 476/919] Revert "Prevent assignment of trader-owned small pets (items, not units) to pits, ponds, and cages." --- docs/changelog.txt | 1 - plugins/lua/zone.lua | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index d07fb869bcb..f6421c7d89e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,7 +56,6 @@ Template for new versions: ## New Features ## Fixes -- small animals owned by traders can no longer be assigned to pits, ponds, and cages. ## Misc Improvements diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 7dce4aee148..92940ef48ed 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -602,9 +602,7 @@ local function is_assignable_unit(unit) end local function is_assignable_item(item) - -- small pets owned by traders are not assignable - if item.flags.trader then return false end - -- other vermin/small pets are assignable + -- all vermin/small pets are assignable return true end From 9a0455e1280bce6f4b58081a38aaa59c39da94bd Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 10 Sep 2025 11:58:57 -0700 Subject: [PATCH 477/919] plugins/lua/zone.lua fix: overlay doesn't run when invalid pasture While the overlay is processing all animals to see where they're assigned, if it encounters an animal assigned to a nonexistent civzone or to a building that isn't a civzone, it errors out. This patch makes the overlay more robust in these cases. Note that the overlay will not display the relevant animal, nor will it attempt to repair the invalid assignment. --- plugins/lua/zone.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 92940ef48ed..2923f8e3331 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -989,7 +989,9 @@ local function get_zone_status(unit_or_vermin, bld_assignments) return PASTURE_STATUS.ASSIGNED_HERE.value else local civzone = df.building.find(assigned_zone_ref.building_id) - if civzone.type == df.civzone_type.Pen then + if not civzone or not df.building_civzonest:is_instance(civzone) then + return PASTURE_STATUS.NONE.value + elseif civzone.type == df.civzone_type.Pen then return PASTURE_STATUS.PASTURED.value elseif civzone.type == df.civzone_type.Pond then return PASTURE_STATUS.PITTED.value @@ -1150,7 +1152,9 @@ local function get_cage_status(unit_or_vermin, bld_assignments) local assigned_zone_ref = get_general_ref(unit_or_vermin, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) if assigned_zone_ref then local civzone = df.building.find(assigned_zone_ref.building_id) - if civzone.type == df.civzone_type.Pen then + if not civzone or not df.building_civzonest:is_instance(civzone) then + return CAGE_STATUS.NONE.value + elseif civzone.type == df.civzone_type.Pen then return CAGE_STATUS.PASTURED.value elseif civzone.type == df.civzone_type.Pond then return CAGE_STATUS.PITTED.value From 6fce14efc4469b9451a8b09442a6551b07655d69 Mon Sep 17 00:00:00 2001 From: SilasD Date: Thu, 11 Sep 2025 15:11:10 -0700 Subject: [PATCH 478/919] Update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f6421c7d89e..fd85c18af5f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. ## Misc Improvements From 516ade1ef354fc15b3c5a558c2cc830b2871eeef Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 15 Sep 2025 07:19:48 +0000 Subject: [PATCH 479/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 7b691d256f9..4358ea5e4d9 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7b691d256f9427036e7ff24fa795a0f9334739e7 +Subproject commit 4358ea5e4d92a8b13b17adb58da773ab66db8cf6 diff --git a/scripts b/scripts index 539120522a8..a91c23c51a5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 539120522a808220e4ab23538bad5cfcd979211e +Subproject commit a91c23c51a5d7be64b89d35e915677f2319026d6 From 8df7c427551c850e882481b5b7ecaa5555db1b34 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 23 Sep 2025 07:19:36 +0000 Subject: [PATCH 480/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 08102c59ef1..525024cf722 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 08102c59ef1c76b1cebe3e582be51f0ac763de0e +Subproject commit 525024cf72244cd6c384b723a0b7a34a50b336ae From fdb1db08ccf068c0e19fe8a40eb146e220ac055d Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 06:57:38 -0400 Subject: [PATCH 481/919] use noexcept std::filesystem overloads --- library/Core.cpp | 9 ++++++++- library/modules/Filesystem.cpp | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index a2b48c992d1..bbe6195351b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -535,7 +535,14 @@ std::filesystem::path Core::findScript(std::string name) getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - std::filesystem::path path = std::filesystem::weakly_canonical(*it / name); + std::error_code ec; + std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); + if (ec) + { + con.printerr("Error loading ''%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); + continue; + } + if (Filesystem::isfile(path)) return path; } diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 7a6b09a5004..8f0f86cb8bb 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -154,17 +154,20 @@ bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status bool Filesystem::exists (std::filesystem::path path) noexcept { - return std::filesystem::exists(path); + std::error_code ec; + return std::filesystem::exists(path, ec); } bool Filesystem::isfile(std::filesystem::path path) noexcept { - return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); + std::error_code ec; + return std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); } bool Filesystem::isdir (std::filesystem::path path) noexcept { - return std::filesystem::exists(path) && std::filesystem::is_directory(path); + std::error_code ec; + return std::filesystem::exists(path, ec) && std::filesystem::is_directory(path, ec); } std::time_t Filesystem::mtime (std::filesystem::path path) noexcept From 838c3786ee7748c7afba8b2dac24961893fd4b7d Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:49:18 -0400 Subject: [PATCH 482/919] fix typo --- library/Core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index bbe6195351b..a6fc71fb2bc 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -539,7 +539,7 @@ std::filesystem::path Core::findScript(std::string name) std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); if (ec) { - con.printerr("Error loading ''%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); + con.printerr("Error loading '%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); continue; } From 06fd987d8e00f5b2081668352ad721bb4f28161f Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:27:50 -0400 Subject: [PATCH 483/919] implement suggested changes --- library/modules/Filesystem.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 8f0f86cb8bb..6d49c6a3b74 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -155,19 +155,30 @@ bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status bool Filesystem::exists (std::filesystem::path path) noexcept { std::error_code ec; - return std::filesystem::exists(path, ec); + auto r = std::filesystem::exists(path, ec); + if (ec) + return false; + return r; } bool Filesystem::isfile(std::filesystem::path path) noexcept { std::error_code ec; - return std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); + // is_regular_file() also checks for existence. + auto r = std::filesystem::is_regular_file(path, ec); + if (ec) + return false; + return r; } bool Filesystem::isdir (std::filesystem::path path) noexcept { std::error_code ec; - return std::filesystem::exists(path, ec) && std::filesystem::is_directory(path, ec); + // is_directory() also checks for existence. + auto r = std::filesystem::is_directory(path, ec); + if (ec) + return false; + return r; } std::time_t Filesystem::mtime (std::filesystem::path path) noexcept From 2f54afa068c751f98496aac2a36aa323ea10462b Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:43:11 -0400 Subject: [PATCH 484/919] add changelog entry --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index fd85c18af5f..8ff5b73e6ca 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. +- improved file system handling: gracefully handle errors from operations, preventing crashes. ## Misc Improvements From ba349ba66dcb3a3fe3589a63ecac190b5b0b3620 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 26 Sep 2025 07:19:11 +0000 Subject: [PATCH 485/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 4358ea5e4d9..d8ebde11e7d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4358ea5e4d92a8b13b17adb58da773ab66db8cf6 +Subproject commit d8ebde11e7d66f4f033e801d3737e49f94d5cf80 diff --git a/scripts b/scripts index a91c23c51a5..046b99aa87e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a91c23c51a5d7be64b89d35e915677f2319026d6 +Subproject commit 046b99aa87ed69561050ace3f6273a63f2a23ee1 From 77455eeadb47782186c85435b8fc8cbfa9d76eef Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:27:21 +0000 Subject: [PATCH 486/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 046b99aa87e..14e4a34e158 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 046b99aa87ed69561050ace3f6273a63f2a23ee1 +Subproject commit 14e4a34e158071a2796669e63d8eb02bb2b9f408 From 6cdf702e8119fc89b48acaf3784cdfac1e834d4a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 26 Sep 2025 11:28:15 -0500 Subject: [PATCH 487/919] prep for prerelease --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 463fa229da1..d50c8ee7229 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.04") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) diff --git a/docs/changelog.txt b/docs/changelog.txt index fb59ad39124..b1a11493eae 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,8 @@ Template for new versions: ## New Features ## Fixes -- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. - improved file system handling: gracefully handle errors from operations, preventing crashes. +- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. ## Misc Improvements From 3e2c2a5445eb76418ffddc3f7aa17d2c6124e4f7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:28:12 +0000 Subject: [PATCH 488/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index d8ebde11e7d..57443cdc32a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d8ebde11e7d66f4f033e801d3737e49f94d5cf80 +Subproject commit 57443cdc32a48a8a2e11b91be5200346b9d7c7e8 From 8825c9824db89efc0c89ba12be83f4c8498314e9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 30 Sep 2025 14:07:44 -0500 Subject: [PATCH 489/919] Revert "Merge pull request #4781 from Crystalwarrior/tombstone-linger" This reverts commit 345abc1404ca6849434221dee26c8493d989ac1f, reversing changes made to ba349ba66dcb3a3fe3589a63ecac190b5b0b3620. --- docs/about/Removed.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 24e05626425..5f73b86be38 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -10,12 +10,6 @@ work (e.g. links from the `changelog`). :local: :depth: 1 -.. _linger: - -linger -====== -Merged into `bodyswap` as ``bodyswap linger``. - .. _adv-rumors: adv-rumors From c406ff971b20cff92a664dae5a4d32f70f719792 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Oct 2025 10:42:10 -0500 Subject: [PATCH 490/919] Update watch-df-release.yml remove beta and experimental branches as they currently do not exist --- .github/workflows/watch-df-release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 77f49477813..50950140d1f 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -21,11 +21,11 @@ jobs: # steam_branch: leave blank if no DFHack steam push is desired include: - df_steam_branch: public - - df_steam_branch: beta - - df_steam_branch: experimental - structures_ref: experimental - dfhack_ref: experimental - steam_branch: experimental +# - df_steam_branch: beta +# - df_steam_branch: experimental +# structures_ref: experimental +# dfhack_ref: experimental +# steam_branch: experimental steps: - name: Fetch state uses: actions/cache/restore@v4 From e16df6f94112c31b8e5958354c1a9dbd1fc1372e Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:00:55 +0000 Subject: [PATCH 491/919] Auto-update structures ref for 52.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 57443cdc32a..df0ec27701a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 57443cdc32a48a8a2e11b91be5200346b9d7c7e8 +Subproject commit df0ec27701a2aeaa1ccfb81323f4be55b78e1c10 From 7c268fd33500ef0685b21b12d1f623dedd7c04ba Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Oct 2025 11:02:53 -0500 Subject: [PATCH 492/919] Update CMakeLists.txt for 52.05-r1 --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d50c8ee7229..3446e247848 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.04") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "52.05") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From d4122bed934eccf7e49961f2872e0772c7bf71c4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:23:31 +0000 Subject: [PATCH 493/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 14e4a34e158..9b325f16fd6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 14e4a34e158071a2796669e63d8eb02bb2b9f408 +Subproject commit 9b325f16fd64838a032ccf168303cfc08cc59d24 From b6872194f9f1ca4643064d8defd9137f8d981959 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Oct 2025 11:43:43 -0500 Subject: [PATCH 494/919] Update changelog and scripts for 52.05-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index b1a11493eae..f09a12f7e4a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.05-r1 + +## New Tools + +## New Features + ## Fixes - improved file system handling: gracefully handle errors from operations, preventing crashes. - `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. diff --git a/scripts b/scripts index 9b325f16fd6..c6e4e62404f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 9b325f16fd64838a032ccf168303cfc08cc59d24 +Subproject commit c6e4e62404fbff15783d1e94393177119fd2beb7 From e003f20fe19b74f3fdf695f4fac2f879846d39d6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 2 Oct 2025 16:23:52 -0500 Subject: [PATCH 495/919] add link to DF wiki for DF Lua API --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f09a12f7e4a..897dc98d1f5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## Misc Improvements ## Documentation +- added a clarification link to DF's Lua API documentation to the DFHack Lua API documentation, as a way to reduce end-user confusion ## API diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 86e90da9c17..a4a039f4097 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -25,6 +25,13 @@ It does not describe all of the utility functions implemented by Lua files located in :file:`hack/lua/*` (:file:`library/lua/*` in the git repo). +.. admonition:: Is this the DF or DFHack Lua API? + :class: warning + + This document describes the Lua API provided by DFHack, not + the Lua API provided by Dwarf Fortress. For information about DF's Lua API, see + :wiki:`Lua scripting` + on the Dwarf Fortress Wiki. .. contents:: Contents :local: From a82d35fd90e42aa9bbd1bacf23f10f6287918348 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Oct 2025 22:16:55 -0500 Subject: [PATCH 496/919] add missing `const` qualifiers this is basically catching some cases missed in #4702; in addition, these changes are necessary to allow for the type identities defined by codegen to be `const` --- library/LuaTools.cpp | 6 +++--- library/LuaWrapper.cpp | 2 +- library/include/DataDefs.h | 4 ++-- library/include/LuaTools.h | 4 ++-- library/include/LuaWrapper.h | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 95f8017dd8a..81bcdd50ca3 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1077,7 +1077,7 @@ bool DFHack::Lua::Require(color_ostream &out, lua_State *state, } static bool doAssignDFObject(color_ostream *out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact, bool perr, bool signal) { if (signal) @@ -1124,13 +1124,13 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, } bool DFHack::Lua::AssignDFObject(color_ostream &out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact_type, bool perr) { return doAssignDFObject(&out, state, type, target, val_index, exact_type, perr, false); } -void DFHack::Lua::CheckDFAssign(lua_State *state, type_identity *type, +void DFHack::Lua::CheckDFAssign(lua_State *state, const type_identity *type, void *target, int val_index, bool exact_type) { doAssignDFObject(NULL, state, type, target, val_index, exact_type, false, true); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index a0069fbbdc4..ef37bd61ee7 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -132,7 +132,7 @@ bool LuaWrapper::LookupTypeInfo(lua_State *state, bool in_method) return true; } -void LuaWrapper::LookupInTable(lua_State *state, void *id, LuaToken *tname) +void LuaWrapper::LookupInTable(lua_State *state, const void *id, LuaToken *tname) { lua_rawgetp(state, LUA_REGISTRYINDEX, tname); lua_rawgetp(state, -1, id); diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index e8e54fac412..76e74c97915 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -257,8 +257,8 @@ namespace DFHack }; struct struct_field_info_extra { - enum_identity *index_enum; - type_identity *ref_target; + const enum_identity *index_enum; + const type_identity *ref_target; const char *union_tag_field; const char *union_tag_attr; const char *original_name; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index db40e76810d..9831841ca2a 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -140,14 +140,14 @@ namespace DFHack::Lua { * Return behavior is of SafeCall below. */ DFHACK_EXPORT bool AssignDFObject(color_ostream &out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact_type = false, bool perr = true); /** * Assign the value at val_index to the target of given identity using df.assign(). * Otherwise throws an error. */ - DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, + DFHACK_EXPORT void CheckDFAssign(lua_State *state, const type_identity *type, void *target, int val_index, bool exact_type = false); template concept df_object = requires() diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 67301b52cc6..1b8be705ed6 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -167,7 +167,7 @@ namespace LuaWrapper { const char *ctx, bool allow_type = false, bool keep_metatable = false); - void LookupInTable(lua_State *state, void *id, LuaToken *tname); + void LookupInTable(lua_State *state, const void *id, LuaToken *tname); void SaveInTable(lua_State *state, void *node, LuaToken *tname); void SaveTypeInfo(lua_State *state, void *node); From c31f28f6f2f0da3887fd682f709059cbe4122fe8 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 5 Oct 2025 15:48:52 -0700 Subject: [PATCH 497/919] script-manager.lua mitigate #5591 not searching scripts_modactive --- library/lua/script-manager.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 74a31fd1d97..80774c53e85 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -152,6 +152,7 @@ function get_mod_paths(installed_subdir, active_subdir) -- 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 = dfhack.filesystem.getBaseDir() .. path -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end local id = get_mod_id_and_version(path) From ca827d332f00bf63d70aa086b7bb2f2eb84b8a62 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 5 Oct 2025 16:13:32 -0700 Subject: [PATCH 498/919] update changelog.txt --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f09a12f7e4a..c3d50b9e7f8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- modding: the ``scripts_modactive`` and ``scripts_modinstalled`` subdirectories of mods will be searched for scripts as per the modding-guide documentation ## Misc Improvements From 96936a7466597d3c407809c69013171d708f1167 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 5 Oct 2025 18:26:18 -0700 Subject: [PATCH 499/919] update changelog.txt again --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index c3d50b9e7f8..28b6b42fadb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,7 @@ Template for new versions: ## New Features ## Fixes -- modding: the ``scripts_modactive`` and ``scripts_modinstalled`` subdirectories of mods will be searched for scripts as per the modding-guide documentation +- `script-manager`: the ``scripts_modactive`` and ``scripts_modinstalled`` folders of a script-enabled mod will be properly added to the script path search list ## Misc Improvements From 6b1ede0b0335f85e5ab14bfd5b023cf3495df8c2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Oct 2025 07:20:06 +0000 Subject: [PATCH 500/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index c6e4e62404f..6cd5936717c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit c6e4e62404fbff15783d1e94393177119fd2beb7 +Subproject commit 6cd5936717c24455b42944d31b27b0bbe9a6533d From 43ff7893aa6307a693c98ec9742095ccc13dd269 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:32:25 +0000 Subject: [PATCH 501/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.33.3 → 0.34.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.3...0.34.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28599a14ae9..1bca6a9289e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.3 + rev: 0.34.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 48ae6a4b98183ad9ed40f00b5e61fdc10f1592b1 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:37:50 +0000 Subject: [PATCH 502/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6cd5936717c..2cc976a0295 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6cd5936717c24455b42944d31b27b0bbe9a6533d +Subproject commit 2cc976a0295071ba2668a89738d55db24f7820f8 From cd0a4146af8658ced20edc5f8c87b0cd0947f492 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 6 Oct 2025 11:22:16 -0500 Subject: [PATCH 503/919] Update CMakeLists.txt for 52.05-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3446e247848..b145db0bf87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.05") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From e3bf5f73c334f2b039bf9fa9769f60e9147ff50e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Oct 2025 10:08:43 -0500 Subject: [PATCH 504/919] Changelogs for 52.05-r2 --- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e2253c7f43c..14a9ba0ca3f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.05-r2 + +## New Tools + +## New Features + ## Fixes - `script-manager`: the ``scripts_modactive`` and ``scripts_modinstalled`` folders of a script-enabled mod will be properly added to the script path search list diff --git a/scripts b/scripts index 2cc976a0295..ae0ca6eefb1 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 2cc976a0295071ba2668a89738d55db24f7820f8 +Subproject commit ae0ca6eefb1297ca909eebb58b7706e95e223d98 From ec428d988ac4bbb1a81083faada4576ae1c8a5db Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Oct 2025 11:25:11 -0500 Subject: [PATCH 505/919] CMakeLists.txt for 52.05-r2 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b145db0bf87..8e63238f89d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.05") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DFHACK_RELEASE "r2") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 7b9347d0b78298bd705f9c47cca14d888da37e85 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 10 Oct 2025 20:13:59 -0500 Subject: [PATCH 506/919] add more missing `const` qualifiers --- library/DataDefs.cpp | 6 +++--- library/include/DataDefs.h | 14 +++++++------- library/include/MiscUtils.h | 2 +- library/include/modules/Gui.h | 2 +- library/modules/Gui.cpp | 5 +++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 5d2386282e5..50be5834594 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -232,8 +232,8 @@ const std::string df::buffer_container_identity::getFullName(const type_identity } union_identity::union_identity(size_t size, const TAllocateFn alloc, - compound_identity *scope_parent, const char *dfhack_name, - struct_identity *parent, const struct_field_info *fields) + const compound_identity *scope_parent, const char *dfhack_name, + const struct_identity *parent, const struct_field_info *fields) : struct_identity(size, alloc, scope_parent, dfhack_name, parent, fields) { } @@ -350,7 +350,7 @@ virtual_identity *virtual_identity::find(void *vtable) return NULL; } -void virtual_identity::adjust_vtable(virtual_ptr obj, virtual_identity *main) +void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *main) const { if (vtable_ptr) { *(void**)obj = vtable_ptr; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 76e74c97915..6a5695809d7 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -326,8 +326,8 @@ namespace DFHack class DFHACK_EXPORT union_identity : public struct_identity { public: union_identity(size_t size, TAllocateFn alloc, - compound_identity *scope_parent, const char *dfhack_name, - struct_identity *parent, const struct_field_info *fields); + const compound_identity *scope_parent, const char *dfhack_name, + const struct_identity *parent, const struct_field_info *fields); virtual identity_type type() const { return IDTYPE_UNION; } @@ -418,13 +418,13 @@ namespace DFHack template static P get_vmethod_ptr(P selector); public: - bool can_instantiate() { return can_allocate(); } - virtual_ptr instantiate() { return can_instantiate() ? (virtual_ptr)do_allocate() : NULL; } + bool can_instantiate() const { return can_allocate(); } + virtual_ptr instantiate() const { return can_instantiate() ? (virtual_ptr)do_allocate() : NULL; } static virtual_ptr clone(virtual_ptr obj); public: // Strictly for use in virtual class constructors - void adjust_vtable(virtual_ptr obj, virtual_identity *main); + void adjust_vtable(virtual_ptr obj, const virtual_identity *main) const; }; template @@ -536,10 +536,10 @@ namespace df struct identity_traits {}; template - requires requires () { { &T::_identity } -> std::convertible_to; } + requires requires () { { &T::_identity } -> std::convertible_to; } struct identity_traits { static const bool is_primitive = false; - static compound_identity *get() { return &T::_identity; } + static const compound_identity *get() { return &T::_identity; } }; template diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 2b9426202d1..2ff04282e4c 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -400,7 +400,7 @@ typename T::mapped_type findPrefixInMap( #endif template -inline bool static_add_to_map(CT *pmap, typename CT::key_type key, typename CT::mapped_type value) { +inline bool static_add_to_map(CT *pmap, const typename CT::key_type key, const typename CT::mapped_type value) { (*pmap)[key] = value; return true; } diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 222779a8eee..63939fc247a 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -211,7 +211,7 @@ namespace DFHack /// Get the current top-level view-screen DFHACK_EXPORT df::viewscreen *getCurViewscreen(bool skip_dismissed = false); - DFHACK_EXPORT df::viewscreen *getViewscreenByIdentity(virtual_identity &id, int n = 1); + DFHACK_EXPORT df::viewscreen *getViewscreenByIdentity(const virtual_identity &id, int n = 1); /// Get the top-most underlying DF viewscreen (not owned by DFHack) DFHACK_EXPORT df::viewscreen *getDFViewscreen(bool skip_dismissed = false, df::viewscreen *top = NULL); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index dedcc6cdbcc..b5df3ca990b 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -152,9 +152,10 @@ static std::string getNameChunk(virtual_identity *id, int start, int end) */ typedef void (*getFocusStringsHandler)(std::string &str, std::vector &strList, df::viewscreen *screen); -static std::map getFocusStringsHandlers; +static std::map getFocusStringsHandlers; #define VIEWSCREEN(name) df::viewscreen_##name##st + #define DEFINE_GET_FOCUS_STRING_HANDLER(screen_type) \ static void getFocusStrings_##screen_type(const std::string &baseFocus, std::vector &focusStrings, VIEWSCREEN(screen_type) *screen);\ DFHACK_STATIC_ADD_TO_MAP(\ @@ -2718,7 +2719,7 @@ df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed) return ws; } -df::viewscreen *Gui::getViewscreenByIdentity (virtual_identity &id, int n) +df::viewscreen *Gui::getViewscreenByIdentity (const virtual_identity &id, int n) { bool limit = (n > 0); df::viewscreen *screen = Gui::getCurViewscreen(); From 73c103bf20958eff9d56f90ad1a451a3249903e7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 13 Oct 2025 07:20:41 +0000 Subject: [PATCH 507/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index df0ec27701a..27c83f81f16 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit df0ec27701a2aeaa1ccfb81323f4be55b78e1c10 +Subproject commit 27c83f81f16f904c626cf80f581d658e85206a0b diff --git a/scripts b/scripts index ae0ca6eefb1..80d61d16d46 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ae0ca6eefb1297ca909eebb58b7706e95e223d98 +Subproject commit 80d61d16d467a4d2bb1c301b22cd067bf4b4992e From 0a2a35343fbfc77a4d2f1ab98754ff61c2c2eb75 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 16 Oct 2025 07:20:33 +0000 Subject: [PATCH 508/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 27c83f81f16..9e3924fa796 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 27c83f81f16f904c626cf80f581d658e85206a0b +Subproject commit 9e3924fa7967078cebdf5890908df909569f7f5f From 60b5ce3824c14627e71487f50998f333ded0b813 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Oct 2025 06:06:45 -0500 Subject: [PATCH 509/919] more `const`ifying! this gets rid of the `mutable`s in `compound_identity`, `struct_identity`, and `virtual_identity` and makes, I think, finally, all `type_identity` objects `const` everywhere --- library/DataDefs.cpp | 183 +++++++++++++++++++++++------------- library/LuaWrapper.cpp | 2 +- library/VTableInterpose.cpp | 39 ++++---- library/include/DataDefs.h | 90 ++++++++++++------ library/modules/Gui.cpp | 4 +- 5 files changed, 200 insertions(+), 118 deletions(-) diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 50be5834594..5c187290f7e 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -27,6 +27,7 @@ distribution. #include #include #include +#include #include "MemAccess.h" #include "Core.h" @@ -86,27 +87,43 @@ void *enum_identity::do_allocate() const { return p; } -/* The order of global object constructor calls is - * undefined between compilation units. Therefore, - * this list has to be plain data, so that it gets - * initialized by the loader in the initial mmap. - */ -compound_identity *compound_identity::list = NULL; -std::vector compound_identity::top_scope; +/****** COMPOUND IDENTITIES ******/ -compound_identity::compound_identity(size_t size, TAllocateFn alloc, - const compound_identity *scope_parent, const char *dfhack_name) - : constructed_identity(size, alloc), dfhack_name(dfhack_name), scope_parent(const_cast(scope_parent)) // fixme +decltype(compound_identity::list) compound_identity::list = nullptr; +decltype(compound_identity::parent_map) compound_identity::parent_map = nullptr; +decltype(compound_identity::children_map) compound_identity::children_map = nullptr; +decltype(compound_identity::top_scope) compound_identity::top_scope = nullptr; + +void compound_identity::ensure_compound_identity_init() { - next = list; list = this; + static std::once_flag compound_identity_init_flag; + std::call_once(compound_identity_init_flag, [] () { + list = new (std::remove_pointer::type)(); + parent_map = new (std::remove_pointer::type)(); + children_map = new (std::remove_pointer::type)(); + top_scope = new std::vector(); + }); } -void compound_identity::doInit(Core *) +compound_identity::compound_identity(size_t size, TAllocateFn alloc, + const compound_identity* scope_parent, const char* dfhack_name) + : constructed_identity(size, alloc), dfhack_name(dfhack_name), scope_parent(scope_parent) { + ensure_compound_identity_init(); + if (scope_parent) - scope_parent->scope_children.push_back(this); + { + (*parent_map)[this] = scope_parent; + (*children_map)[scope_parent].push_back(this); + } else - top_scope.push_back(this); + (*top_scope).push_back(this); + + list->push_back(this); +} + +void compound_identity::doInit(Core *) const +{ } const std::string compound_identity::getFullName() const @@ -124,10 +141,10 @@ void compound_identity::Init(Core *core) if (!known_mutex) known_mutex = new std::mutex(); - // This cannot be done in the constructors, because - // they are called in an undefined order. - for (compound_identity *p = list; p; p = p->next) - p->doInit(core); + // do any late initialization required for compound identities + + for (auto ci : *list) + ci->doInit(core); } bitfield_identity::bitfield_identity(size_t size, @@ -176,27 +193,44 @@ enum_identity::ComplexData::ComplexData(std::initializer_list values) } } +/****** STRUCT IDENTITIES ******/ + +decltype(struct_identity::parent_map) struct_identity::parent_map = nullptr; +decltype(struct_identity::children_map) struct_identity::children_map = nullptr; + +void struct_identity::ensure_struct_identity_init() +{ + static std::once_flag struct_identity_init_flag; + std::call_once(struct_identity_init_flag, []() { + parent_map = new (std::remove_pointer::type)(); + children_map = new (std::remove_pointer::type)(); + }); +} + + + struct_identity::struct_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name, const struct_identity *parent, const struct_field_info *fields) : compound_identity(size, alloc, scope_parent, dfhack_name), - parent(const_cast(parent)), has_children(false), fields(fields) + fields(fields) { + ensure_struct_identity_init(); + if (parent) + { + (*parent_map)[this] = parent; + (*children_map)[parent].push_back(this); + } } -void struct_identity::doInit(Core *core) +void struct_identity::doInit(Core *core) const { compound_identity::doInit(core); - - if (parent) { - parent->children.push_back(this); - parent->has_children = true; - } } bool struct_identity::is_subclass(const struct_identity *actual) const { - if (!has_children && actual != this) + if (!hasChildren() && actual != this) return false; for (; actual; actual = actual->getParent()) @@ -238,13 +272,32 @@ union_identity::union_identity(size_t size, const TAllocateFn alloc, { } +/****** VIRTUAL IDENTITIES ******/ + +decltype(virtual_identity::name_lookup) virtual_identity::name_lookup = nullptr; +decltype(virtual_identity::known) virtual_identity::known = nullptr; +decltype(virtual_identity::vtable_ptr_map) virtual_identity::vtable_ptr_map = nullptr; +decltype(virtual_identity::interpose_list_map) virtual_identity::interpose_list_map = nullptr; + +void virtual_identity::ensure_virtual_identity_init() +{ + static std::once_flag virtual_identity_init_flag; + std::call_once(virtual_identity_init_flag, []() { + name_lookup = new (std::remove_pointer::type)(); + known = new (std::remove_pointer::type)(); + vtable_ptr_map = new (std::remove_pointer::type)(); + interpose_list_map = new (std::remove_pointer::type)(); + }); +} + virtual_identity::virtual_identity(size_t size, const TAllocateFn alloc, const char *dfhack_name, const char *original_name, const virtual_identity *parent, const struct_field_info *fields, bool is_plugin) : struct_identity(size, alloc, NULL, dfhack_name, parent, fields), original_name(original_name), - vtable_ptr(NULL), is_plugin(is_plugin) + is_plugin(is_plugin) { + ensure_virtual_identity_init(); // Plugins are initialized after Init was called, so they need to be added to the name table here if (is_plugin) { @@ -252,15 +305,10 @@ virtual_identity::virtual_identity(size_t size, const TAllocateFn alloc, } } -/* Vtable name to identity lookup. */ -static std::map name_lookup; - -/* Vtable pointer to identity lookup. */ -std::map virtual_identity::known; - virtual_identity::~virtual_identity() { // Remove interpose entries, so that they don't try accessing this object later + auto& interpose_list = (*interpose_list_map)[this]; for (auto it = interpose_list.begin(); it != interpose_list.end(); ++it) if (it->second) it->second->on_host_delete(this); @@ -269,75 +317,79 @@ virtual_identity::~virtual_identity() // Remove global lookup table entries if we're from a plugin if (is_plugin) { - name_lookup.erase(getOriginalName()); + (*name_lookup).erase(getOriginalName()); - if (vtable_ptr) - known.erase(vtable_ptr); + if (vtable_ptr()) + (*known).erase(vtable_ptr()); } } -void virtual_identity::doInit(Core *core) +void virtual_identity::doInit(Core *core) const { struct_identity::doInit(core); auto vtname = getOriginalName(); - name_lookup[vtname] = this; + (*name_lookup)[vtname] = this; - vtable_ptr = core->vinfo->getVTable(vtname); + auto vtable_ptr = core->vinfo->getVTable(vtname); if (vtable_ptr) - known[vtable_ptr] = this; + { + (*known)[vtable_ptr] = this; + (*vtable_ptr_map)[this] = vtable_ptr; + } } -virtual_identity *virtual_identity::find(const std::string &name) +const virtual_identity *virtual_identity::find(std::string_view name) { - auto name_it = name_lookup.find(name); + auto name_it = (*name_lookup).find(name); - return (name_it != name_lookup.end()) ? name_it->second : NULL; + return (name_it != (*name_lookup).end()) ? name_it->second : nullptr; } -virtual_identity *virtual_identity::get(virtual_ptr instance_ptr) +const virtual_identity *virtual_identity::get(virtual_ptr instance_ptr) { - if (!instance_ptr) return NULL; + if (!instance_ptr) return nullptr; return find(get_vtable(instance_ptr)); } -virtual_identity *virtual_identity::find(void *vtable) +const virtual_identity *virtual_identity::find(void *vtable) { if (!vtable || !known_mutex) - return NULL; + return nullptr; + + ensure_virtual_identity_init(); // Actually, a reader/writer lock would be sufficient, // since the table is only written once per class. std::lock_guard lock(*known_mutex); - std::map::iterator it = known.find(vtable); - - if (it != known.end()) + auto it = (*known).find(vtable); + if (it != (*known).end()) return it->second; // If using a reader/writer lock, re-grab as write here, and recheck Core &core = Core::getInstance(); std::string name = core.p->doReadClassName(vtable); - auto name_it = name_lookup.find(name); - if (name_it != name_lookup.end()) { - virtual_identity *p = name_it->second; + auto name_it = (*name_lookup).find(name); + if (name_it != (*name_lookup).end()) { + const virtual_identity *p = name_it->second; - if (p->vtable_ptr && p->vtable_ptr != vtable) { + if (p->vtable_ptr() && p->vtable_ptr() != vtable) { std::cerr << "Conflicting vtable ptr for class '" << p->getName() << "': found 0x" << std::hex << uintptr_t(vtable) - << ", previous 0x" << uintptr_t(p->vtable_ptr) << std::dec << std::endl; + << ", previous 0x" << uintptr_t(p->vtable_ptr()) << std::dec << std::endl; abort(); - } else if (!p->vtable_ptr) { + } else if (!p->vtable_ptr()) { uintptr_t pv = uintptr_t(vtable); pv -= Core::getInstance().vinfo->getRebaseDelta(); std::cerr << "" << std::endl; } - known[vtable] = p; - p->vtable_ptr = vtable; + (*known)[vtable] = p; + (*vtable_ptr_map)[p] = vtable; return p; } @@ -346,14 +398,15 @@ virtual_identity *virtual_identity::find(void *vtable) << std::hex << uintptr_t(vtable) << std::dec << std::endl; } - known[vtable] = NULL; - return NULL; + (*known).erase(vtable); + return nullptr; } void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *main) const { - if (vtable_ptr) { - *(void**)obj = vtable_ptr; + auto vp = vtable_ptr(); + if (vp) { + *(void**)obj = vp; return; } @@ -366,10 +419,10 @@ void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *ma virtual_ptr virtual_identity::clone(virtual_ptr obj) { - virtual_identity *id = get(obj); - if (!id) return NULL; + const virtual_identity *id = get(obj); + if (!id) return nullptr; virtual_ptr copy = id->instantiate(); - if (!copy) return NULL; + if (!copy) return nullptr; id->do_copy(copy, obj); return copy; } diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index ef37bd61ee7..823d542b797 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -221,7 +221,7 @@ void LuaWrapper::push_object_internal(lua_State *state, const type_identity *typ if (type->type() == IDTYPE_CLASS) { - virtual_identity *class_vid = virtual_identity::get(virtual_ptr(ptr)); + const virtual_identity *class_vid = virtual_identity::get(virtual_ptr(ptr)); if (class_vid) type = class_vid; } diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 797161240e8..3c2a4a50fc6 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -210,7 +210,7 @@ void DFHack::addr_to_method_pointer_(void *pptr, void *addr) void *virtual_identity::get_vmethod_ptr(int idx) const { assert(idx >= 0); - void **vtable = (void**)vtable_ptr; + void **vtable = (void**)vtable_ptr(); if (!vtable) return NULL; return vtable[idx]; } @@ -218,7 +218,7 @@ void *virtual_identity::get_vmethod_ptr(int idx) const bool virtual_identity::set_vmethod_ptr(MemoryPatcher &patcher, int idx, void *ptr) const { assert(idx >= 0); - void **vtable = (void**)vtable_ptr; + void **vtable = (void**)vtable_ptr(); if (!vtable) return NULL; return patcher.write(&vtable[idx], &ptr, sizeof(void*)); } @@ -326,16 +326,13 @@ VMethodInterposeLinkBase::~VMethodInterposeLinkBase() VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(const virtual_identity *id) { - auto pitem = id->interpose_list.find(vmethod_idx); - if (pitem == id->interpose_list.end()) - return NULL; - auto item = pitem->second; + auto item = id->get_interpose(vmethod_idx); if (!item) - return NULL; + return nullptr; if (item->host != id) - return NULL; + return nullptr; while (item->prev && item->prev->host == id) item = item->prev; @@ -364,7 +361,7 @@ bool VMethodInterposeLinkBase::find_child_hosts(const virtual_identity *cur, voi child_next.insert(base); found = true; } - else if (child->vtable_ptr) + else if (child->vtable_ptr()) { void *cptr = child->get_vmethod_ptr(vmethod_idx); if (cptr != vmptr) @@ -401,7 +398,7 @@ void VMethodInterposeLinkBase::on_host_delete(const virtual_identity *from) { // Otherwise, drop the link to that child: assert(child_hosts.count(from) != 0 && - from->interpose_list[vmethod_idx] == this); // while mutating this gets cleaned up below so machts nichts + from->get_interpose(vmethod_idx) == this); // while mutating this gets cleaned up below so machts nichts // Find and restore the original vmethod ptr auto last = this; @@ -413,7 +410,7 @@ void VMethodInterposeLinkBase::on_host_delete(const virtual_identity *from) // Unlink the chains child_hosts.erase(from); - from->interpose_list.erase(vmethod_idx); + from->set_interpose(vmethod_idx,nullptr); } } @@ -427,7 +424,7 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (is_applied()) return true; - if (!host->vtable_ptr) + if (!host->vtable_ptr()) { std::cerr << "VMethodInterposeLinkBase::apply(" << enable << "): " << name() << ": no vtable pointer: " << host->getName() << endl; @@ -435,10 +432,10 @@ bool VMethodInterposeLinkBase::apply(bool enable) } // Retrieve the current vtable entry - auto l = host->interpose_list.find(vmethod_idx); + auto l = host->get_interpose(vmethod_idx); - VMethodInterposeLinkBase* old_link = (l != host->interpose_list.end()) ? (l->second) : nullptr; - VMethodInterposeLinkBase* next_link = NULL; + VMethodInterposeLinkBase* old_link = (l != nullptr) ? l : nullptr; + VMethodInterposeLinkBase* next_link = nullptr; while (old_link && old_link->host == host && old_link->priority > priority) { @@ -473,7 +470,7 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (next_link) next_link->prev = this; else - host->interpose_list[vmethod_idx] = this; + host->set_interpose(vmethod_idx,this); child_hosts.clear(); child_next.clear(); @@ -532,9 +529,9 @@ bool VMethodInterposeLinkBase::apply(bool enable) for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it) { auto nhost = *it; - assert(nhost->interpose_list[vmethod_idx] == old_link); // acceptable due to assign below + assert(nhost->get_interpose(vmethod_idx) == old_link); // acceptable due to assign below nhost->set_vmethod_ptr(patcher, vmethod_idx, interpose_method); - nhost->interpose_list[vmethod_idx] = this; + nhost->set_interpose(vmethod_idx, this); } return true; @@ -573,7 +570,7 @@ void VMethodInterposeLinkBase::remove() MemoryPatcher patcher; // Remove from the list in the identity and vtable - host->interpose_list[vmethod_idx] = prev; + host->set_interpose(vmethod_idx, prev); host->set_vmethod_ptr(patcher, vmethod_idx, saved_chain); for (auto it = child_next.begin(); it != child_next.end(); ++it) @@ -589,8 +586,8 @@ void VMethodInterposeLinkBase::remove() for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it) { auto nhost = *it; - assert(nhost->interpose_list[vmethod_idx] == this); // acceptable due to assign below - nhost->interpose_list[vmethod_idx] = prev; + assert(nhost->get_interpose(vmethod_idx) == this); // acceptable due to assign below + nhost->set_interpose(vmethod_idx,prev); nhost->set_vmethod_ptr(patcher, vmethod_idx, saved_chain); if (prev) prev->child_hosts.insert(nhost); diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 6a5695809d7..9d1eb4c664a 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -24,6 +24,7 @@ distribution. #pragma once +#include #include #include #include @@ -134,28 +135,30 @@ namespace DFHack }; class DFHACK_EXPORT compound_identity : public constructed_identity { - static compound_identity *list; - mutable compound_identity *next; + static std::list* list; + static std::map* parent_map; + static std::map>* children_map; + static std::vector* top_scope; const char *dfhack_name; - mutable compound_identity *scope_parent; - mutable std::vector scope_children; - static std::vector top_scope; + const compound_identity *const scope_parent; + + static void ensure_compound_identity_init(); protected: compound_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name); - virtual void doInit(Core *core); + virtual void doInit(Core *core) const; public: const char *getName() const { return dfhack_name; } virtual const std::string getFullName() const; - const compound_identity *getScopeParent() const { return scope_parent; } - const std::vector &getScopeChildren() const { return scope_children; } - static const std::vector &getTopScope() { return top_scope; } + const compound_identity *getScopeParent() const { return (*parent_map)[this]; } + const std::vector &getScopeChildren() const { return (*children_map)[this]; } + static const std::vector &getTopScope() { return *top_scope; } static void Init(Core *core); }; @@ -286,14 +289,15 @@ namespace DFHack }; class DFHACK_EXPORT struct_identity : public compound_identity { - mutable struct_identity *parent; - mutable std::vector children; - bool has_children; + static std::map* parent_map; + static std::map>* children_map; const struct_field_info *fields; + static void ensure_struct_identity_init(); + protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; public: struct_identity(size_t size, TAllocateFn alloc, @@ -302,9 +306,9 @@ namespace DFHack virtual identity_type type() const { return IDTYPE_STRUCT; } - const struct_identity *getParent() const { return parent; } - const std::vector &getChildren() const { return children; } - bool hasChildren() const { return has_children; } + const struct_identity *getParent() const { return (*parent_map)[this]; } + const std::vector &getChildren() const { return (*children_map)[this]; } + bool hasChildren() const { return (*children_map)[this].size() > 0; } const struct_field_info *getFields() const { return fields; } @@ -361,27 +365,55 @@ namespace DFHack class MemoryPatcher; class DFHACK_EXPORT virtual_identity : public struct_identity { - static std::map known; + public: + using interpose_t = VMethodInterposeLinkBase*; + using interpose_list_t = std::map; - const char *original_name; + private: + static std::map> *name_lookup; + static std::map* known; + static std::map* vtable_ptr_map; + static std::map* interpose_list_map; - mutable void *vtable_ptr; + const char *original_name; bool is_plugin; friend class VMethodInterposeLinkBase; - mutable std::map interpose_list; + + static void ensure_virtual_identity_init(); protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; static void *get_vtable(virtual_ptr instance_ptr) { return *(void**)instance_ptr; } - bool can_allocate() const { return struct_identity::can_allocate() && (vtable_ptr != NULL); } + void* vtable_ptr() const + { + auto& lst = (*vtable_ptr_map); + auto it = lst.find(this); + return it != lst.end() ? it->second : nullptr; + } + + bool can_allocate() const { return struct_identity::can_allocate() && (vtable_ptr() != nullptr); } void *get_vmethod_ptr(int index) const; bool set_vmethod_ptr(MemoryPatcher &patcher, int index, void *ptr) const; + interpose_list_t& get_interpose_list() const { return (*interpose_list_map)[this]; } + interpose_t get_interpose(int index) const { + auto &lst = get_interpose_list(); + auto it = lst.find(index); + return it != lst.end() ? it->second : nullptr; + } + void set_interpose(int index, interpose_t link) const { + auto &lst = get_interpose_list(); + if (link) + lst[index] = link; + else + lst.erase(index); + } + public: virtual_identity(size_t size, const TAllocateFn alloc, const char *dfhack_name, const char *original_name, @@ -394,16 +426,16 @@ namespace DFHack const char *getOriginalName() const { return original_name ? original_name : getName(); } public: - static virtual_identity *get(virtual_ptr instance_ptr); + static const virtual_identity *get(virtual_ptr instance_ptr); - static virtual_identity *find(void *vtable); - static virtual_identity *find(const std::string &name); + static const virtual_identity *find(void *vtable); + static const virtual_identity *find(std::string_view name); bool is_instance(virtual_ptr instance_ptr) const { if (!instance_ptr) return false; - if (vtable_ptr) { + if (vtable_ptr()) { void *vtable = get_vtable(instance_ptr); - if (vtable == vtable_ptr) return true; + if (vtable == vtable_ptr()) return true; if (!hasChildren()) return false; } return is_subclass(get(instance_ptr)); @@ -411,8 +443,8 @@ namespace DFHack bool is_direct_instance(virtual_ptr instance_ptr) const { if (!instance_ptr) return false; - return vtable_ptr ? (vtable_ptr == get_vtable(instance_ptr)) - : (this == get(instance_ptr)); + auto vp = vtable_ptr(); + return vp ? (vp == get_vtable(instance_ptr)) : (this == get(instance_ptr)); } template static P get_vmethod_ptr(P selector); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index b5df3ca990b..3808fa02712 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -135,7 +135,7 @@ static df::layer_object_listst *getLayerList(df::viewscreen_layer *layer, int id } */ -static std::string getNameChunk(virtual_identity *id, int start, int end) +static std::string getNameChunk(const virtual_identity *id, int start, int end) { if (!id) return "UNKNOWN"; @@ -970,7 +970,7 @@ std::vector Gui::getFocusStrings(df::viewscreen* top) } } - if (virtual_identity *id = virtual_identity::get(top)) + if (const virtual_identity *id = virtual_identity::get(top)) { std::string name = getNameChunk(id, 11, 2); From 2a8c7897418427f94afbb5e348dea27ab3020b75 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Oct 2025 06:24:36 -0500 Subject: [PATCH 510/919] update docs --- docs/dev/data-identity.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/dev/data-identity.rst b/docs/dev/data-identity.rst index 855a6db26ce..ae7076941d1 100644 --- a/docs/dev/data-identity.rst +++ b/docs/dev/data-identity.rst @@ -123,18 +123,12 @@ Type identity object lifetime and mutability ============================================ *Most* instances of ``type_identity`` are statically constructed and immutable and are thus ``const static`` when constructed. -All ``type_identity`` pointers should be declared ``const``. -Due to the use of lazy initialization, ``compound_identity`` and its subclasses have a few fields that are marked as ``mutable`` -(so that the ``parent`` and ``child`` members can be updated as additional instances are constructed). -In addition, ``virtual_identity`` has two fields that are ``mutable`` due to the dual use of this type to implement -DFHack's vmethod interpose system. -Due to this dual purpose, it is important that there be at most one ``virtual_identity`` object per virtual class, -although this is not enforced at present. +All ``type_identity`` pointers should be declared ``const``. Due to ``virtual_identity``'s role in implementing +DFHack's vmethod interpose system, it is important that there be at most one ``virtual_identity`` object per virtual class. Having more than one ``struct_identity`` object for the same type might also potentially lead to misoperation. In general, there should be a one to one correspondence between ``type_identity`` objects and C++ types -(with the special case that ``global_identity`` has no corresponding type). -As far as we know, for any type other than ``virtual_identity``, +(with the special case that ``global_identity`` has no corresponding type). As far as we know, for any type other than ``virtual_identity``, violations of this constraint will not lead to misoperation, but this constraint should not be lightly violated. The Lua/C++ interface does, in a handful of places, assume that it can compare ``type_identity`` pointers to determine if they reference the same type, but as far as we know all of these instances will fall From c6b55d4da33a2ae21ea2fda4339735d331962080 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 22 Oct 2025 20:34:21 -0500 Subject: [PATCH 511/919] simplify the allocator current code has three different allocator functions, the generic one, one labeled "nodel" and one labeled "noassign" the `noassign` allocator just mimics the behavior of the basic one when the type in question is not copy-assignable and is only being used for types that are not copy-assignable so the default allocator will work _exactly_ the same anyway the `nodel` version is entirely unused thus, this PR removes the `nodel` and `noassign` versions and folds `try_assign` into the allocator so that all the logic is in one place also add in-code documentation --- library/DataIdentity.cpp | 2 +- library/include/DataDefs.h | 77 +++++++++++++++++++--------------- library/include/DataIdentity.h | 2 +- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index a385915d920..1bbeb64e77a 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -19,7 +19,7 @@ namespace df { #define INTEGER_IDENTITY_TRAITS(type, name) NUMBER_IDENTITY_TRAITS(integer, type, name) #define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type, #type) #define OPAQUE_IDENTITY_TRAITS_NAME(name, ...) \ - const opaque_identity identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_noassign_fn<__VA_ARGS__ >, name) + const opaque_identity identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_fn<__VA_ARGS__ >, name) #define OPAQUE_IDENTITY_TRAITS(...) OPAQUE_IDENTITY_TRAITS_NAME(#__VA_ARGS__, __VA_ARGS__ ) INTEGER_IDENTITY_TRAITS(char, "char"); diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 9d1eb4c664a..7fac3c0c5ed 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -72,7 +72,7 @@ namespace DFHack PTRFLAG_HAS_BAD_POINTERS = 2, }; - typedef void *(*TAllocateFn)(void*,const void*); + using TAllocateFn = void *(*)(void*, const void*); class DFHACK_EXPORT type_identity { const size_t size; @@ -122,10 +122,10 @@ namespace DFHack constructed_identity(size_t size, const TAllocateFn alloc) : type_identity(size), allocator(alloc) {}; - virtual bool can_allocate() const { return (allocator != NULL); } - virtual void *do_allocate() const { return allocator(NULL,NULL); } + virtual bool can_allocate() const { return (allocator != nullptr); } + virtual void *do_allocate() const { return allocator(nullptr,nullptr); } virtual bool do_copy(void *tgt, const void *src) const { return allocator(tgt,src) == tgt; } - virtual bool do_destroy(void *obj) const { return allocator(NULL,obj) == obj; } + virtual bool do_destroy(void *obj) const { return allocator(nullptr,obj) == obj; } public: virtual bool isPrimitive() const { return false; } virtual bool isConstructed() const { return true; } @@ -526,42 +526,53 @@ namespace df using DFHack::DfLinkedList; using DFHack::DfOtherVectors; + /* + * + * Allocator functions are used to allocate, deallocate, and copy-assign objects + * + * When out is non-null, the object pointed to by in is copy-assigned over the object + * pointed to by out, if possible. When assignment is possible, out is returned. + * When assignment is not possible, nothing is done and nullptr is returned. + * The type must be copy-assignable for this to work; move-assignment is not + * supported. Callers can determine if the assignment succeeded by checking for the + * return value matching out (or simply being not null). + * + * When only in is non-null, the object pointed to by in is destroyed and deallocated, + * and in is returned. Note that the return value points to deallocated memory + * and should not be dereferenced. + * + * When both out and in are null, a new object is constructed and returned. + * + * Calling an allocator function with out non-null and in null is undefined behavior. + * + */ + template concept copy_assignable = std::assignable_from && std::assignable_from; template - void* allocator_try_assign(void *out, const void *in) { - if constexpr (copy_assignable) { - *(T*)out = *(const T*)in; - return out; - } - else { - // assignment is not possible; do nothing - return nullptr; + void *allocator_fn(void *out, const void *in) { + if (out) + { + if constexpr (copy_assignable) + { + *(T*)out = *(const T*)in; + return out; + } + else + { + return nullptr; + } } - } - + else if (in) + { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - template - void *allocator_fn(void *out, const void *in) { - if (out) { return allocator_try_assign(out, in); } - else if (in) { delete (T*)in; return (T*)in; } - else return new T(); - } + delete (T*)in; #pragma GCC diagnostic pop - - template - void *allocator_nodel_fn(void *out, const void *in) { - if (out) { *(T*)out = *(const T*)in; return out; } - else if (in) { return NULL; } - else return new T(); - } - - template - void *allocator_noassign_fn(void *out, const void *in) { - if (out) { return NULL; } - else if (in) { delete (T*)in; return (T*)in; } - else return new T(); + return (T*)in; + } + else + return new T(); } template diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index e9fb5a9aa35..2c302264d93 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -638,7 +638,7 @@ namespace df static opaque_identity *get() { using type = std::shared_ptr; static std::string name = std::string("shared_ptr<") + typeid(T).name() + ">"; - static opaque_identity identity(sizeof(type), allocator_noassign_fn, name); + static opaque_identity identity(sizeof(type), allocator_fn, name); return &identity; } }; From 321173c6617999c29364af61d759449d80daf2a7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:20:30 +0000 Subject: [PATCH 512/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 80d61d16d46..f72ccf6f3b8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 80d61d16d467a4d2bb1c301b22cd067bf4b4992e +Subproject commit f72ccf6f3b83cdaa8d424d68b7f2b0cdd552f242 From df1bdc3dcb4bbd9bcba9e805aea92262e0c66b49 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 24 Oct 2025 07:20:00 +0000 Subject: [PATCH 513/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index f72ccf6f3b8..e80d3301030 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f72ccf6f3b83cdaa8d424d68b7f2b0cdd552f242 +Subproject commit e80d33010309fcbad0b10ff05218f21600f62751 From 3911fca9e7a7f47bca1c4b6ffdf312607cf92c15 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 24 Oct 2025 09:22:31 -0400 Subject: [PATCH 514/919] Add stockpiles plugin support for dyes Enables support for "dyed", "undyed", and "color" fields for armor, cloth, leather, and finished goods. --- docs/changelog.txt | 1 + plugins/stockpiles/StockpileSerializer.cpp | 128 ++++++++++++++++++++- plugins/stockpiles/proto/stockpiles.proto | 12 ++ 3 files changed, 136 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 14a9ba0ca3f..fcd16dc0025 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `stockpiles`: add support for managing the dyed, undyed, and color filter settings. ## Documentation diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index efac3e8bad1..756421d9d46 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -12,6 +12,7 @@ #include "df/building_stockpilest.h" #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/descriptor_color.h" #include "df/inorganic_raw.h" #include "df/item_quality.h" #include @@ -497,6 +498,57 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, } } +static bool serialize_list_color(color_ostream& out, FuncWriteExport add_value, const vector* colors) { + bool all = true; + if (!colors) { + DEBUG(log, out).print("serialize_list_color: list null\n"); + return all; + } + for (size_t i = 0; i < colors->size(); ++i) { + if (!colors->at(i)) { + all = false; + continue; + } + add_value(world->raws.descriptors.colors[i]->id); + } + return all; +} + +// Find the index of a named color, returning SIZE_MAX on unknown. +static size_t find_color_by_token(const string& token) { + auto& colors = world->raws.descriptors.colors; + size_t num_colors = colors.size(); + for (size_t idx = 0; idx < num_colors; ++ idx) { + if (colors[idx]->id == token) { + return idx; + } + } + return SIZE_MAX; +} + +static void unserialize_list_color(color_ostream& out, const char* subcat, bool all, char val, const vector& filters, + FuncReadImport read_value, int32_t list_size, vector& pile_list) { + size_t num_elems = world->raws.descriptors.colors.size(); + pile_list.resize(num_elems, '\0'); + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + set_filter_elem(out, subcat, filters, val, world->raws.descriptors.colors[idx]->id, idx, pile_list[idx]); + } + return; + } + + for (int32_t idx = 0; idx < list_size; ++idx) { + const string& value = read_value(idx); + + size_t color = find_color_by_token(value); + if (color == SIZE_MAX) { + WARN(log, out).print("unknown color %s", value.c_str()); + continue; + } + set_filter_elem(out, subcat, filters, val, value, color, pile_list[color]); + } +} + static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allowed, FuncWriteExport add_value, const vector& list) { using df::enums::item_type::item_type; @@ -1059,10 +1111,12 @@ static bool armor_mat_is_allowed(const MaterialInfo& mi) { bool StockpileSettingsSerializer::write_armor(color_ostream& out, StockpileSettings::ArmorSet* armor) { auto & parmor = mSettings->armor; - bool all = parmor.unusable && parmor.usable; + bool all = parmor.unusable && parmor.usable && parmor.dyed && parmor.undyed; armor->set_unusable(parmor.unusable); armor->set_usable(parmor.usable); + armor->set_dyed(parmor.dyed); + armor->set_undyed(parmor.undyed); // armor type all = serialize_list_itemdef(out, @@ -1125,6 +1179,9 @@ bool StockpileSettingsSerializer::write_armor(color_ostream& out, StockpileSetti all = serialize_list_quality(out, [&](const string& token) { armor->add_quality_total(token); }, parmor.quality_total) && all; + all = serialize_list_color(out, [&](const string& token) { armor->add_color(token); }, + &parmor.color) && all; + return all; } @@ -1148,6 +1205,9 @@ void StockpileSettingsSerializer::read_armor(color_ostream& out, DeserializeMode parmor.mats.clear(); quality_clear(parmor.quality_core); quality_clear(parmor.quality_total); + parmor.dyed = false; + parmor.undyed = false; + parmor.color.clear(); }, [&](bool all, char val) { auto & barmor = mBuffer.armor(); @@ -1195,6 +1255,13 @@ void StockpileSettingsSerializer::read_armor(color_ostream& out, DeserializeMode unserialize_list_quality(out, "total", all, val, filters, [&](const size_t& idx) -> const string& { return barmor.quality_total(idx); }, barmor.quality_total_size(), parmor.quality_total); + + unserialize_list_color(out, "color", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.color(idx); }, + barmor.color_size(), parmor.color); + + set_flag(out, "dyed", filters, all, val, barmor.dyed(), parmor.dyed); + set_flag(out, "undyed", filters, all, val, barmor.undyed(), parmor.undyed); }); } @@ -1266,7 +1333,9 @@ void StockpileSettingsSerializer::read_bars_blocks(color_ostream& out, Deseriali } bool StockpileSettingsSerializer::write_cloth(color_ostream& out, StockpileSettings::ClothSet* cloth) { - bool all = true; + bool all = mSettings->cloth.dyed && mSettings->cloth.undyed; + cloth->set_dyed(mSettings->cloth.dyed); + cloth->set_undyed(mSettings->cloth.undyed); all = serialize_list_organic_mat(out, [&](const string& token) { cloth->add_thread_silk(token); }, @@ -1300,6 +1369,10 @@ bool StockpileSettingsSerializer::write_cloth(color_ostream& out, StockpileSetti [&](const string& token) { cloth->add_cloth_metal(token); }, &mSettings->cloth.cloth_metal, organic_mat_category::MetalThread) && all; + all = serialize_list_color(out, + [&](const string& token) { cloth->add_color(token); }, + &mSettings->cloth.color) && all; + return all; } @@ -1319,6 +1392,9 @@ void StockpileSettingsSerializer::read_cloth(color_ostream& out, DeserializeMode pcloth.cloth_plant.clear(); pcloth.cloth_yarn.clear(); pcloth.cloth_metal.clear(); + pcloth.dyed = false; + pcloth.undyed = false; + pcloth.color.clear(); }, [&](bool all, char val) { auto & bcloth = mBuffer.cloth(); @@ -1354,6 +1430,13 @@ void StockpileSettingsSerializer::read_cloth(color_ostream& out, DeserializeMode unserialize_list_organic_mat(out, "cloth/metal", all, val, filters, [&](size_t idx) -> string { return bcloth.cloth_metal(idx); }, bcloth.cloth_metal_size(), pcloth.cloth_metal, organic_mat_category::MetalThread); + + unserialize_list_color(out, "cloth/color", all, val, filters, + [&](size_t idx) -> string { return bcloth.color(idx); }, + bcloth.color_size(), pcloth.color); + + set_flag(out, "dyed", filters, all, val, bcloth.dyed(), pcloth.dyed); + set_flag(out, "undyed", filters, all, val, bcloth.undyed(), pcloth.undyed); }); } @@ -1425,7 +1508,11 @@ static bool finished_goods_mat_is_allowed(const MaterialInfo& mi) { } bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, StockpileSettings::FinishedGoodsSet* finished_goods) { - bool all = serialize_list_item_type(out, + bool all = mSettings->leather.dyed && mSettings->leather.undyed; + finished_goods->set_dyed(mSettings->finished_goods.dyed); + finished_goods->set_undyed(mSettings->finished_goods.undyed); + + all = serialize_list_item_type(out, finished_goods_type_is_allowed, [&](const string& token) { finished_goods->add_type(token); }, mSettings->finished_goods.type); @@ -1447,6 +1534,10 @@ bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, Stock [&](const string& token) { finished_goods->add_quality_total(token); }, mSettings->finished_goods.quality_total) && all; + all = serialize_list_color(out, + [&](const string& token) { finished_goods->add_color(token); }, + &mSettings->finished_goods.color) && all; + return all; } @@ -1463,6 +1554,9 @@ void StockpileSettingsSerializer::read_finished_goods(color_ostream& out, Deseri pfinished_goods.mats.clear(); quality_clear(pfinished_goods.quality_core); quality_clear(pfinished_goods.quality_total); + pfinished_goods.dyed = false; + pfinished_goods.undyed = false; + pfinished_goods.color.clear(); }, [&](bool all, char val) { auto & bfinished_goods = mBuffer.finished_goods(); @@ -1486,6 +1580,13 @@ void StockpileSettingsSerializer::read_finished_goods(color_ostream& out, Deseri unserialize_list_quality(out, "total", all, val, filters, [&](const size_t& idx) -> const string& { return bfinished_goods.quality_total(idx); }, bfinished_goods.quality_total_size(), pfinished_goods.quality_total); + + unserialize_list_color(out, "color", all, val, filters, + [&](const size_t& idx) -> const string& { return bfinished_goods.color(idx); }, + bfinished_goods.color_size(), pfinished_goods.color); + + set_flag(out, "dyed", filters, all, val, bfinished_goods.dyed(), pfinished_goods.dyed); + set_flag(out, "undyed", filters, all, val, bfinished_goods.undyed(), pfinished_goods.undyed); }); } @@ -1934,9 +2035,17 @@ void StockpileSettingsSerializer::read_gems(color_ostream& out, DeserializeMode } bool StockpileSettingsSerializer::write_leather(color_ostream& out, StockpileSettings::LeatherSet* leather) { - return serialize_list_organic_mat(out, + bool all = mSettings->leather.dyed && mSettings->leather.undyed; + leather->set_dyed(mSettings->leather.dyed); + leather->set_undyed(mSettings->leather.undyed); + + all = serialize_list_organic_mat(out, [&](const string& id) { leather->add_mats(id); }, - &mSettings->leather.mats, organic_mat_category::Leather); + &mSettings->leather.mats, organic_mat_category::Leather) && all; + all = serialize_list_color(out, + [&](const string& id) { leather->add_color(id); }, + &mSettings->leather.color) && all; + return all; } void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMode mode, const vector& filters) { @@ -1948,6 +2057,9 @@ void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMo mSettings->flags.mask_leather, [&]() { pleather.mats.clear(); + pleather.color.clear(); + pleather.dyed = false; + pleather.undyed = false; }, [&](bool all, char val) { auto & bleather = mBuffer.leather(); @@ -1955,6 +2067,12 @@ void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMo unserialize_list_organic_mat(out, "", all, val, filters, [&](size_t idx) -> string { return bleather.mats(idx); }, bleather.mats_size(), pleather.mats, organic_mat_category::Leather); + unserialize_list_color(out, "", all, val, filters, + [&](size_t idx) -> string { return bleather.color(idx); }, + bleather.color_size(), pleather.color); + + set_flag(out, "dyed", filters, all, val, bleather.dyed(), pleather.dyed); + set_flag(out, "undyed", filters, all, val, bleather.undyed(), pleather.undyed); }); } diff --git a/plugins/stockpiles/proto/stockpiles.proto b/plugins/stockpiles/proto/stockpiles.proto index 681e7d9279f..49a6d1c42e5 100644 --- a/plugins/stockpiles/proto/stockpiles.proto +++ b/plugins/stockpiles/proto/stockpiles.proto @@ -100,10 +100,16 @@ message StockpileSettings { repeated string mats = 3; repeated string quality_core = 4; repeated string quality_total = 5; + optional bool dyed = 7; + optional bool undyed = 8; + repeated string color = 9; } message LeatherSet { optional bool all = 2; repeated string mats = 1; + optional bool dyed = 3; + optional bool undyed = 4; + repeated string color = 5; } message ClothSet { optional bool all = 9; @@ -115,6 +121,9 @@ message StockpileSettings { repeated string cloth_plant = 6; repeated string cloth_yarn = 7; repeated string cloth_metal = 8; + optional bool dyed = 10; + optional bool undyed = 11; + repeated string color = 12; } message WoodSet { optional bool all = 2; @@ -145,6 +154,9 @@ message StockpileSettings { repeated string quality_total = 10; optional bool usable = 11; optional bool unusable = 12; + optional bool dyed = 14; + optional bool undyed = 15; + repeated string color = 16; } message CorpsesSet { optional bool all = 1; From 2a332ebbfe3ec2f28e1a2a2aa40937945b1a4fda Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 24 Oct 2025 21:36:31 -0400 Subject: [PATCH 515/919] Correct logic errors --- plugins/stockpiles/StockpileSerializer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 756421d9d46..ad76977f1a9 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -499,7 +499,7 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, } static bool serialize_list_color(color_ostream& out, FuncWriteExport add_value, const vector* colors) { - bool all = true; + bool all = world->raws.descriptors.colors.size() == colors->size(); if (!colors) { DEBUG(log, out).print("serialize_list_color: list null\n"); return all; @@ -1508,14 +1508,14 @@ static bool finished_goods_mat_is_allowed(const MaterialInfo& mi) { } bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, StockpileSettings::FinishedGoodsSet* finished_goods) { - bool all = mSettings->leather.dyed && mSettings->leather.undyed; + bool all = mSettings->finished_goods.dyed && mSettings->finished_goods.undyed; finished_goods->set_dyed(mSettings->finished_goods.dyed); finished_goods->set_undyed(mSettings->finished_goods.undyed); all = serialize_list_item_type(out, finished_goods_type_is_allowed, [&](const string& token) { finished_goods->add_type(token); }, - mSettings->finished_goods.type); + mSettings->finished_goods.type) && all; all = serialize_list_material(out, finished_goods_mat_is_allowed, From 374df0e264407ad733dee93ad4bc915e0bf357d1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 27 Oct 2025 16:03:02 -0500 Subject: [PATCH 516/919] 52.05-r3rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e63238f89d..e9686d6b5c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "52.05") -set(DFHACK_RELEASE "r2") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r3rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 5180882301238b164ba87f4222598a3b4f8db382 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:20:01 +0000 Subject: [PATCH 517/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e80d3301030..412ae1a979f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e80d33010309fcbad0b10ff05218f21600f62751 +Subproject commit 412ae1a979fb1e634ce29d7b87c17bb38fb22c6c From 576e8396dae8914d3bca321ab48dfdf9de3b1a7e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:36:25 +0000 Subject: [PATCH 518/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 412ae1a979f..ede81fd3a00 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 412ae1a979fb1e634ce29d7b87c17bb38fb22c6c +Subproject commit ede81fd3a00f4104b16a4b913b17f3019af89c44 From b372657cfe2a0dd41b2189decfd1388ab35372e3 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:21:07 +0000 Subject: [PATCH 519/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 9e3924fa796..d7630adb7b5 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9e3924fa7967078cebdf5890908df909569f7f5f +Subproject commit d7630adb7b548ce7bce8cf1a0d3a7648b870d5b2 From 77683efb41d2815e55c9207140873523501a7a0c Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:53:19 +0000 Subject: [PATCH 520/919] Auto-update structures ref for 53.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index d7630adb7b5..014e2e43d14 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d7630adb7b548ce7bce8cf1a0d3a7648b870d5b2 +Subproject commit 014e2e43d148fd04f309a47ab8d866539aced726 From 2a3906b7d0906089e01b2b3949c25ab4e4779500 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:00:24 +0000 Subject: [PATCH 521/919] Auto-update structures ref for 53.01 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 014e2e43d14..812cec185f5 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 014e2e43d148fd04f309a47ab8d866539aced726 +Subproject commit 812cec185f5666d40049958257368e624c7c01ec From 76d7bde153fac9eec0264d084ca573dbcd98696e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Nov 2025 11:51:25 -0600 Subject: [PATCH 522/919] update version to 53.01-r1 --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9686d6b5c8..9f471490c6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "52.05") -set(DFHACK_RELEASE "r3rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "53.01") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 9a73da3d1604abc24ce02e382aaa420b47557c01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:00:28 +0000 Subject: [PATCH 523/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.34.0 → 0.34.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.0...0.34.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bca6a9289e..70277354adb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.0 + rev: 0.34.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From dec140ef815ea8bfd5f0ec44e6463ddcd2a9d95a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:08:26 +0000 Subject: [PATCH 524/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 812cec185f5..fa6fb6e5159 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 812cec185f5666d40049958257368e624c7c01ec +Subproject commit fa6fb6e5159344c383736f9f5819d9e48c97d356 From 7fc09e34daff5b2c39098cbf12841faf953bb65a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:38:41 +0000 Subject: [PATCH 525/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index fa6fb6e5159..a2604299ce4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit fa6fb6e5159344c383736f9f5819d9e48c97d356 +Subproject commit a2604299ce4321dd5111b374c69af783db8969e9 From 1dead6c15d913eecd22938f031ae619a009a6216 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Nov 2025 15:00:12 -0600 Subject: [PATCH 526/919] Changelog for 53.01-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fcd16dc0025..bc72a8e33ac 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,24 @@ Template for new versions: ## Fixes +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.01-r1 + +## New Tools + +## New Features + +## Fixes + ## Misc Improvements - `stockpiles`: add support for managing the dyed, undyed, and color filter settings. diff --git a/scripts b/scripts index ede81fd3a00..370778c997c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ede81fd3a00f4104b16a4b913b17f3019af89c44 +Subproject commit 370778c997cafc1721a4ae635159c21b84d95e8c From f1dc866ccf3c1b96860897fdea3085a43e86e9b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 3 Nov 2025 15:24:58 -0600 Subject: [PATCH 527/919] update global_table --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a2604299ce4..c7da8a73765 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a2604299ce4321dd5111b374c69af783db8969e9 +Subproject commit c7da8a737657117ff5c8752b7ca08bb5392e02e0 From c9af9f44f297b7b70c08b8a77a7aa7004ac22072 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Nov 2025 07:20:31 +0000 Subject: [PATCH 528/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index c7da8a73765..8808adbdf5b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit c7da8a737657117ff5c8752b7ca08bb5392e02e0 +Subproject commit 8808adbdf5b1073e4eb7c55de0eb96a5b512cfed diff --git a/scripts b/scripts index 370778c997c..f71c1568f3a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 370778c997cafc1721a4ae635159c21b84d95e8c +Subproject commit f71c1568f3af618fc542f1af2028dc89ba383f7a From 7123b8c8b49bb3bca8733733904a4f16532a88af Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:03:32 +0000 Subject: [PATCH 529/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 8808adbdf5b..3833a114451 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8808adbdf5b1073e4eb7c55de0eb96a5b512cfed +Subproject commit 3833a11445168f67cffd35b678e47ec86c35e091 From e0951fdafd18636f6170574b82b12b2ad50f6940 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Nov 2025 11:04:36 -0600 Subject: [PATCH 530/919] add `gps` to checked globals --- docs/changelog.txt | 1 + library/Core.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bc72a8e33ac..04b6fd6fd19 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Core: added ``gps`` (``graphicst``) to the set of globals whose sizes must agree for DFHack to pass initialization checks ## Documentation diff --git a/library/Core.cpp b/library/Core.cpp index a6fc71fb2bc..b927d49cc46 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1682,6 +1682,7 @@ bool Core::InitMainThread() { { "world", sizeof(df::world) }, { "game", sizeof(df::gamest) }, { "plotinfo", sizeof(df::plotinfost) }, + { "gps", sizeof(df::graphic) }, }; for (auto& gte : *df::global::global_table) From dd94cff18e91c8d022656cd6924a82a26b7db804 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:23:03 +0000 Subject: [PATCH 531/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 3833a114451..0c035273128 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3833a11445168f67cffd35b678e47ec86c35e091 +Subproject commit 0c035273128c300aa77014e92315b09136e65de9 From 0c2902ab12ecf301ea1674a376690cadc678d82e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Nov 2025 11:23:58 -0600 Subject: [PATCH 532/919] cmake for 53.02-r1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f471490c6b..e394d7d3495 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.01") +set(DF_VERSION "53.02") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From e8bba788f855909679186c912a73757a52233234 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:31:01 +0000 Subject: [PATCH 533/919] Auto-update structures ref for 53.02 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 0c035273128..99cb51ac849 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0c035273128c300aa77014e92315b09136e65de9 +Subproject commit 99cb51ac849a17652dbb74efa757df5b5716a9ba From 8ef275bc2c7a6eb8f0a516138292df2bc7918d43 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 4 Nov 2025 11:50:23 -0600 Subject: [PATCH 534/919] changelog for 53.02-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 04b6fd6fd19..537686fb9ac 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,24 @@ Template for new versions: ## Fixes +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.02-r1 + +## New Tools + +## New Features + +## Fixes + ## Misc Improvements - Core: added ``gps`` (``graphicst``) to the set of globals whose sizes must agree for DFHack to pass initialization checks From a8e4cb173842ea23d631107d804c2568631f4c75 Mon Sep 17 00:00:00 2001 From: Quietust Date: Tue, 4 Nov 2025 15:29:34 -0600 Subject: [PATCH 535/919] Update cleanconst to skip reinforced constructions since those are built with 2 items plus reinforcements, and it's unclear whether the game would properly restore those during deconstruction. --- docs/changelog.txt | 1 + plugins/cleanconst.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 537686fb9ac..3f338221a6b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features ## Fixes +- `cleanconst`: do not attempt to clean Reinforced constructions ## Misc Improvements diff --git a/plugins/cleanconst.cpp b/plugins/cleanconst.cpp index b72806cd9b5..895f182e760 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -45,6 +45,10 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) if (cons->flags.bits.no_build_item) continue; + // Skip reinforced constructions as well + if (cons->flags.bits.reinforced) + continue; + // only destroy the item if the construction claims to be made of the exact same thing if (item->getType() != cons->item_type || item->getSubtype() != cons->item_subtype || From 8f517802a6ce36860d954790a783797e0928153b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 00:17:58 -0500 Subject: [PATCH 536/919] Add support for new siege update buildings --- docs/changelog.txt | 2 ++ library/lua/dfhack/buildings.lua | 26 +++++++++++++++- plugins/lua/buildingplan/planneroverlay.lua | 34 +++++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 537686fb9ac..0037d2515a4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,10 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: Building costs for reinforced walls are now correct. ## Misc Improvements +- `buildingplan`: Added support for bolt throwers and siege engine rotation. ## Documentation diff --git a/library/lua/dfhack/buildings.lua b/library/lua/dfhack/buildings.lua index 7fff00dcf2b..5ea3b1f7581 100644 --- a/library/lua/dfhack/buildings.lua +++ b/library/lua/dfhack/buildings.lua @@ -130,7 +130,6 @@ local building_inputs = { vector_id=df.job_item_vector_id.PIPE_SECTION } }, - [df.building_type.Construction] = { { flags2={ building_material=true, non_economic=true } } }, [df.building_type.Hatch] = { { item_type=df.item_type.HATCH_COVER, @@ -353,6 +352,26 @@ local siegeengine_input = { quantity=3 } }, + [df.siegeengine_type.BoltThrower] = { + { + item_type=df.item_type.BOLT_THROWER_PARTS, + vector_id=df.job_item_vector_id.BOLT_THROWER_PARTS, + }, + { + item_type=df.item_type.BIN, + vector_id=df.job_item_vector_id.BIN, + }, + { + name='mechanism', + item_type=df.item_type.TRAPPARTS, + vector_id=df.job_item_vector_id.TRAPPARTS, + }, + { + name='chain', + item_type=df.item_type.CHAIN, + vector_id=df.job_item_vector_id.CHAIN + }, + }, } --[[ Functions for lookup in tables. ]] @@ -380,6 +399,11 @@ local function get_inputs_by_type(type,subtype,custom) return trap_inputs[subtype] elseif type == df.building_type.SiegeEngine then return siegeengine_input[subtype] + elseif type == df.building_type.Construction then + if subtype == df.construction_type.ReinforcedWall then + return { { flags2={ building_material=true, non_economic=true }, quantity=2 }, { flags3={ metal=true }, item_type=df.item_type.BAR, vector_id=df.job_item_vector_id.BAR } } + end + return { { flags2={ building_material=true, non_economic=true } } } else return building_inputs[type] end diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index f92dc95d842..72987590fbe 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -119,6 +119,10 @@ local function is_construction() return uibs.building_type == df.building_type.Construction end +local function is_siege_engine() + return uibs.building_type == df.building_type.SiegeEngine +end + local function tile_is_construction(pos) local tt = dfhack.maps.getTileType(pos) if not tt then return false end @@ -605,7 +609,7 @@ function PlannerOverlay:init() local main_panel = widgets.Panel{ view_id='main', - frame={t=1, l=0, r=0, h=14}, + frame={t=1, l=0, r=0, h=15}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), @@ -743,6 +747,24 @@ function PlannerOverlay:init() buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, + widgets.CycleHotkeyLabel { + view_id='siege_facing', + frame = {b=4, l=1, w=28}, + key='CUSTOM_T', + key_back='CUSTOM_SHIFT_T', + label='Facing:', + visible=is_siege_engine, + options={ + {label='North',value=0}, + {label='Northeast',value=1}, + {label='East',value=2}, + {label='Southeast',value=3}, + {label='South',value=4}, + {label='Southwest',value=5}, + {label='West',value=6}, + {label='Northwest',value=7}, + }, + }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, @@ -829,7 +851,7 @@ function PlannerOverlay:init() } local divider_widget = widgets.Divider{ - frame={t=10, l=0, r=0, h=1}, + frame={t=11, l=0, r=0, h=1}, frame_style=gui.FRAME_INTERIOR_MEDIUM, visible=self:callback('is_not_minimized'), } @@ -1307,10 +1329,16 @@ function PlannerOverlay:place_building(placement_data, chosen_items) if is_stairs() then subtype = self:get_stairs_subtype(pos, pd) end + local fields = {} + if is_siege_engine() then + local val = self.subviews.siege_facing:getOptionValue() + fields.facing = val + fields.resting_orientation = val + end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=pd.width, height=pd.height, - direction=uibs.direction, filters=filters} + direction=uibs.direction, filters=filters, fields=fields} if err then -- it's ok if some buildings fail to build goto continue From dc7057fb22722fe06be5bdd4742064ac1ea0449d Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 08:42:18 -0500 Subject: [PATCH 537/919] Use vanilla direction interface for siege engine facing direction --- plugins/lua/buildingplan/planneroverlay.lua | 29 +++++---------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 72987590fbe..f0fbe17de45 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -239,6 +239,7 @@ local direction_panel_types = utils.invert{ df.building_type.WaterWheel, df.building_type.AxleHorizontal, df.building_type.Rollers, + df.building_type.SiegeEngine, } local function has_direction_panel() @@ -609,7 +610,7 @@ function PlannerOverlay:init() local main_panel = widgets.Panel{ view_id='main', - frame={t=1, l=0, r=0, h=15}, + frame={t=1, l=0, r=0, h=14}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), @@ -747,24 +748,6 @@ function PlannerOverlay:init() buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, - widgets.CycleHotkeyLabel { - view_id='siege_facing', - frame = {b=4, l=1, w=28}, - key='CUSTOM_T', - key_back='CUSTOM_SHIFT_T', - label='Facing:', - visible=is_siege_engine, - options={ - {label='North',value=0}, - {label='Northeast',value=1}, - {label='East',value=2}, - {label='Southeast',value=3}, - {label='South',value=4}, - {label='Southwest',value=5}, - {label='West',value=6}, - {label='Northwest',value=7}, - }, - }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, @@ -851,7 +834,7 @@ function PlannerOverlay:init() } local divider_widget = widgets.Divider{ - frame={t=11, l=0, r=0, h=1}, + frame={t=10, l=0, r=0, h=1}, frame_style=gui.FRAME_INTERIOR_MEDIUM, visible=self:callback('is_not_minimized'), } @@ -1331,9 +1314,9 @@ function PlannerOverlay:place_building(placement_data, chosen_items) end local fields = {} if is_siege_engine() then - local val = self.subviews.siege_facing:getOptionValue() - fields.facing = val - fields.resting_orientation = val + local facing = df.global.buildreq.direction + fields.facing = facing + fields.resting_orientation = facing end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, From a939fc06fafb05c54b4c31c8b5f96eceb4f69660 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Nov 2025 11:28:05 -0500 Subject: [PATCH 538/919] Fix boltthrower building size --- library/modules/Buildings.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index b7aeb559965..e5a0af116b3 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -615,6 +615,23 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, return false; case SiegeEngine: + { + using namespace df::enums::siegeengine_type; + + switch((df::siegeengine_type)subtype) + { + case df::siegeengine_type::BoltThrower: + size = df::coord2d(1, 1); + center = df::coord2d(0, 0); + break; + default: + size = df::coord2d(3,3); + center = df::coord2d(1,1); + break; + } + return false; + } + case Windmill: case Wagon: size = df::coord2d(3,3); From 86cf16bd17a4be6a2c843ae2e171a4471db1151a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Nov 2025 12:00:46 -0600 Subject: [PATCH 539/919] 53.02-r2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e394d7d3495..8edb346fe1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.02") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 8c8e39fb5a0dd6afe088827ebc52923f8fbb7ee1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 7 Nov 2025 12:16:03 -0600 Subject: [PATCH 540/919] changelog for 53.02-r2 --- docs/changelog.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 888f6350c5b..bb3c7b769c2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,7 +33,6 @@ Template for new versions: ## New Features ## Fixes -- `cleanconst`: do not attempt to clean Reinforced constructions ## Misc Improvements @@ -59,8 +58,27 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.02-r2 + +## New Tools + +## New Features + ## Fixes - `buildingplan`: Building costs for reinforced walls are now correct. +- `cleanconst`: do not attempt to clean Reinforced constructions ## Misc Improvements - `buildingplan`: Added support for bolt throwers and siege engine rotation. From bd9fc845861c7673d84791844bcea27afab14328 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 6 Nov 2025 14:38:59 -0600 Subject: [PATCH 541/919] update workflows to use gcc 11/13/15 instead of gcc 10/12, because it's not 2023 anymore --- .github/workflows/build-linux.yml | 2 +- .github/workflows/test.yml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3bc931305a7..c2d946e300c 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -66,7 +66,7 @@ on: default: false gcc-ver: type: string - default: "10" + default: "11" jobs: build-linux64: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a73bb3f82d5..323d1a9883a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,9 +59,11 @@ jobs: fail-fast: false matrix: include: - - gcc: 10 + - gcc: 11 # baseline compatibility with ubuntu LTS 22.04 plugins: "default" - - gcc: 12 + - gcc: 13 # future compatibility with ubuntu LTS 24.04 + plugins: "all" + - gcc: 15 # cutting-edge plugins: "all" test-windows: From b30d606a2fc5670fcbf7040e104942f2568dbc7b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 6 Nov 2025 14:41:32 -0600 Subject: [PATCH 542/919] reduce due to limitations of LTS 22.04 --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 323d1a9883a..a4284359418 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,9 +61,7 @@ jobs: include: - gcc: 11 # baseline compatibility with ubuntu LTS 22.04 plugins: "default" - - gcc: 13 # future compatibility with ubuntu LTS 24.04 - plugins: "all" - - gcc: 15 # cutting-edge + - gcc: 12 # highest available in ubuntu 22.04 plugins: "all" test-windows: From 66807805d2148016aef06246cee338e538643296 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 6 Nov 2025 15:12:19 -0600 Subject: [PATCH 543/919] correct references to gcc-10 --- .github/workflows/test.yml | 2 +- docs/dev/github-workflows.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a4284359418..175a1adf48c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: dfhack_repo: ${{ inputs.dfhack_repo }} dfhack_ref: ${{ inputs.dfhack_ref }} os: ubuntu - compiler: gcc-10 + compiler: gcc-11 plugins: default config: default diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst index fa1c36b66c6..ea71e1a2b6e 100644 --- a/docs/dev/github-workflows.rst +++ b/docs/dev/github-workflows.rst @@ -46,8 +46,8 @@ tuned our build and test workflows to minimize spurious cache misses and keep the fast path fast. Caches are namespaced by key prefixes, and we have one key prefix per build -context. For example, release builds on gcc-10 are kept in one cache namespace, -whereas test builds on gcc-10 are kept separate. MSVC release and test builds +context. For example, release builds on gcc-11 are kept in one cache namespace, +whereas test builds on gcc-11 are kept separate. MSVC release and test builds similarly have their own namespaces. Each cache has a maximum size that is enforced by the business logic that writes the cache data. From 324281d194621748a126b1e0d64a640463a13ed0 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:02:41 +0000 Subject: [PATCH 544/919] Auto-update structures ref for 53.03 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 99cb51ac849..1bc08a85ea0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 99cb51ac849a17652dbb74efa757df5b5716a9ba +Subproject commit 1bc08a85ea0038ac8ba2ca5d6203dddf7d840036 From 240414cc9e863007016340bef043cf03613bee88 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:10:32 +0000 Subject: [PATCH 545/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 1bc08a85ea0..145f317340a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1bc08a85ea0038ac8ba2ca5d6203dddf7d840036 +Subproject commit 145f317340ac9327854318c7ba758ee2af5e1928 From 30374d04db2d8591491703750628aa0ddb7c76d8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 10 Nov 2025 12:25:34 -0600 Subject: [PATCH 546/919] 53.03-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8edb346fe1a..f14ed888788 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.02") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.03") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index bb3c7b769c2..9ea08534840 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,25 @@ Template for new versions: ## Removed +# 53.03-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements +- Release builds for Linux are now compiled with gcc 11 + +## Documentation + +## API + +## Lua + +## Removed + # 53.02-r2 ## New Tools From fb2a14001cdbc1803d6be63cc3059dfc32bb3499 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:20:39 -0500 Subject: [PATCH 547/919] use nullptr in Core --- library/Core.cpp | 30 +++++++++++++++--------------- library/include/Core.h | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b927d49cc46..0a47430d3b7 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -103,7 +103,7 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); +static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder); namespace DFHack { @@ -210,7 +210,7 @@ thread_local int CommandDepthCounter::depth = 0; void Core::cheap_tokenise(std::string const& input, std::vector& output) { - std::string *cur = NULL; + std::string *cur = nullptr; size_t i = 0; // Check the first non-space character @@ -241,7 +241,7 @@ void Core::cheap_tokenise(std::string const& input, std::vector& ou { unsigned char c = input[i]; if (isspace(c)) { - cur = NULL; + cur = nullptr; } else { if (!cur) { output.push_back(""); @@ -280,7 +280,7 @@ static void fHKthread(IODATA * iodata) { Core * core = iodata->core; PluginManager * plug_mgr = iodata->plug_mgr; - if(plug_mgr == 0 || core == 0) + if(!plug_mgr || !core) { std::cerr << "Hotkey thread has croaked." << std::endl; return; @@ -1424,7 +1424,7 @@ static void fIOthread(IODATA * iod) main_history.load(HISTORY_FILE.c_str()); Console & con = core->getConsole(); - if (plug_mgr == 0) + if (plug_mgr == nullptr) { con.printerr("Something horrible happened in Core's constructor...\n"); return; @@ -1496,7 +1496,7 @@ Core::Core() : toolCount{0} { // init the console. This must be always the first step! - plug_mgr = 0; + plug_mgr = nullptr; errorstate = false; vinfo = 0; memset(&(s_mods), 0, sizeof(s_mods)); @@ -1504,10 +1504,10 @@ Core::Core() : // set up hotkey capture suppress_duplicate_keyboard_events = true; hotkey_set = NO; - last_world_data_ptr = NULL; - last_local_map_ptr = NULL; + last_world_data_ptr = nullptr; + last_local_map_ptr = nullptr; last_pause_state = false; - top_viewscreen = NULL; + top_viewscreen = nullptr; color_ostream::log_errors_to_stderr = true; }; @@ -1531,7 +1531,7 @@ void Core::fatal (std::string output, const char * title) std::cout << "DFHack fatal error: " << out.str() << std::endl; if (!title) title = "DFHack error!"; - DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, title, out.str().c_str(), NULL); + DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, title, out.str().c_str(), nullptr); bool is_headless = bool(getenv("DFHACK_HEADLESS")); if (is_headless) @@ -2004,7 +2004,7 @@ void Core::doUpdate(color_ostream &out) Lua::Core::Reset(out, "DF code execution"); // find the current viewscreen - df::viewscreen *screen = NULL; + df::viewscreen *screen = nullptr; if (df::global::gview) { screen = &df::global::gview->view; @@ -2230,7 +2230,7 @@ namespace DFHack { } void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event event) { - static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(0, + static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(nullptr, (int)SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", "", (int)SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", "", (int)SC_MAP_LOADED, "onMapLoad", "onLoadMap", "", @@ -2330,7 +2330,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) else { char timebuf[30]; - time_t rawtime = time(NULL); + time_t rawtime = time(nullptr); struct tm * timeinfo = localtime(&rawtime); strftime(timebuf, sizeof(timebuf), "[%Y-%m-%dT%H:%M:%S%z] ", timeinfo); evtlog << timebuf; @@ -2416,7 +2416,7 @@ int Core::Shutdown ( void ) if(plug_mgr) { delete plug_mgr; - plug_mgr = 0; + plug_mgr = nullptr; } // invalidate all modules allModules.clear(); @@ -2998,7 +2998,7 @@ bool Process::patchMemory(void *target, const void* src, size_t count) #define MODULE_GETTER(TYPE) \ TYPE * Core::get##TYPE() \ { \ - if(errorstate) return NULL;\ + if(errorstate) return nullptr;\ if(!s_mods.p##TYPE)\ {\ std::unique_ptr mod = create##TYPE();\ diff --git a/library/include/Core.h b/library/include/Core.h index 383a0a9c954..49aaa504306 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -200,8 +200,8 @@ namespace DFHack std::filesystem::path getHackPath(); - bool isWorldLoaded() { return (last_world_data_ptr != NULL); } - bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } + bool isWorldLoaded() { return (last_world_data_ptr != nullptr); } + bool isMapLoaded() { return (last_local_map_ptr != nullptr && last_world_data_ptr != nullptr); } static df::viewscreen *getTopViewscreen(); @@ -252,7 +252,7 @@ namespace DFHack void operator=(Core const&); // Don't implement // report error to user while failing - void fatal (std::string output, const char * title = NULL); + void fatal (std::string output, const char * title = nullptr); // 1 = fatal failure bool errorstate; From e94b51918463e3636e075e3d60a0a3af29fe875b Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:37:52 -0500 Subject: [PATCH 548/919] Remove unused sortable struct --- library/Core.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b927d49cc46..253f84683ff 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -301,21 +301,6 @@ static void fHKthread(IODATA * iodata) } } -struct sortable -{ - bool recolor; - std::string name; - std::string description; - //FIXME: Nuke when MSVC stops failing at being C++11 compliant - sortable(bool recolor_,const std::string& name_,const std::string & description_): recolor(recolor_), name(name_), description(description_){}; - bool operator <(const sortable & rhs) const - { - if( name < rhs.name ) - return true; - return false; - }; -}; - static std::string dfhack_version_desc() { std::stringstream s; From 31244ad82a6d83c9a91f5ffaa6fd6adaa3534cff Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 00:17:45 -0500 Subject: [PATCH 549/919] prefer lambdas over std::bind --- library/Core.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b927d49cc46..17754e6884d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -330,7 +330,7 @@ static std::string dfhack_version_desc() return s.str(); } -static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::vector& pargs) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::span pargs) { if (!lua_checkstack(state, pargs.size()+10)) return false; @@ -338,21 +338,23 @@ static bool init_run_script(color_ostream &out, lua_State *state, const std::str lua_getfield(state, -1, "run_script"); lua_remove(state, -2); lua_pushstring(state, pcmd.c_str()); - for (auto& arg : pargs) + for (const auto& arg : pargs) lua_pushstring(state, arg.c_str()); return true; } -static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) +static command_result runLuaScript(color_ostream &out, std::string name, std::span args) { - using namespace std::placeholders; - auto init_fn = std::bind(init_run_script, _1, _2, name, args); + auto init_fn = [n = std::move(name), args](color_ostream& out, lua_State* state) -> bool { + return init_run_script(out, state, n, args); + }; + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, std::string& name, bool enable) +static bool init_enable_script(color_ostream &out, lua_State *state, const std::string& name, bool enable) { if (!lua_checkstack(state, 4)) return false; @@ -364,10 +366,12 @@ static bool init_enable_script(color_ostream &out, lua_State *state, std::string return true; } -static command_result enableLuaScript(color_ostream &out, std::string name, bool state) +static command_result enableLuaScript(color_ostream &out, std::string name, bool enabled) { - using namespace std::placeholders; - auto init_fn = std::bind(init_enable_script, _1, _2, name, state); + auto init_fn = [n = std::move(name), enabled](color_ostream& out, lua_State* state) -> bool { + return init_enable_script(out, state, n, enabled); + }; + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); return ok ? CR_OK : CR_FAILURE; From 31e3caecabd83c1f9b081aef461a7f5207935bff Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:02:55 -0500 Subject: [PATCH 550/919] attempt to use more modern c++20 --- library/Core.cpp | 184 +++++++++++++++++++++-------------------- library/include/Core.h | 2 +- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 17754e6884d..0fa91ccee49 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -74,6 +74,7 @@ distribution. #include #include #include +#include #include #include #include @@ -806,11 +807,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s bool reload = (first == "reload"); if (parts.size()) { - for (auto p = parts.begin(); p != parts.end(); p++) + for (const auto& p : parts) { - if (p->size() && (*p)[0] == '-') + if (p.empty() && p[0] == '-') { - if (p->find('a') != std::string::npos) + if (p.find('a') != std::string::npos) all = true; } } @@ -826,15 +827,15 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else { - for (auto p = parts.begin(); p != parts.end(); p++) + for (auto& p : parts) { - if (!p->size() || (*p)[0] == '-') + if (p.empty() || p[0] == '-') continue; - if (load && !plug_mgr->load(*p)) + if (load && !plug_mgr->load(p)) ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(*p)) + else if (unload && !plug_mgr->unload(p)) ret = CR_FAILURE; - else if (reload && !plug_mgr->reload(*p)) + else if (reload && !plug_mgr->reload(p)) ret = CR_FAILURE; } } @@ -854,16 +855,15 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if(parts.size()) { - for (size_t i = 0; i < parts.size(); i++) + for (auto& part : parts) { - std::string part = parts[i]; if (part.find('\\') != std::string::npos) { con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - for (size_t j = 0; j < part.size(); j++) + for (char& c : part) { - if (part[j] == '\\') - part[j] = '/'; + if (c == '\\') + c = '/'; } } @@ -902,9 +902,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else { - for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it) + for (auto& [key, plug] : *plug_mgr) { - Plugin * plug = it->second; + if (!plug) + continue; if (!plug->can_be_enabled()) continue; con.print( @@ -929,13 +930,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s con.print(header_format, "Name", "State", "Cmds", "Enabled"); plug_mgr->refresh(); - for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it) + for (auto& [key, plug] : *plug_mgr) { - Plugin * plug = it->second; if (!plug) continue; - if (parts.size() && std::find(parts.begin(), parts.end(), plug->getName()) == parts.end()) + + if (std::ranges::find(parts, plug->getName()) == parts.end()) continue; + color_value color; switch (plug->getState()) { @@ -1012,9 +1014,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::string keystr = parts[1]; if (parts[0] == "set") ClearKeyBindings(keystr); - for (int i = parts.size()-1; i >= 2; i--) + // for (int i = parts.size()-1; i >= 2; i--) + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!AddKeyBinding(keystr, parts[i])) { + if (!AddKeyBinding(keystr, part)) { con.printerr("Invalid key spec: %s\n", keystr.c_str()); break; } @@ -1022,10 +1025,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (parts.size() >= 2 && parts[0] == "clear") { - for (size_t i = 1; i < parts.size(); i++) + // for (size_t i = 1; i < parts.size(); i++) + for (const auto& part : parts | std::views::drop(1)) { - if (!ClearKeyBindings(parts[i])) { - con.printerr("Invalid key spec: %s\n", parts[i].c_str()); + if (!ClearKeyBindings(part)) { + con.printerr("Invalid key spec: %s\n", part.c_str()); break; } } @@ -1035,8 +1039,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::vector list = ListKeyBindings(parts[1]); if (list.empty()) con << "No bindings." << std::endl; - for (size_t i = 0; i < list.size(); i++) - con << " " << list[i] << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; } else { @@ -1121,9 +1125,9 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (first == "kill-lua") { bool force = false; - for (auto it = parts.begin(); it != parts.end(); ++it) + for (const auto& part : parts) { - if (*it == "force") + if (part == "force") force = true; } if (!Lua::Interrupt(force)) @@ -1189,14 +1193,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s con << "Unrecognized event name: " << parts[1] << std::endl; return CR_WRONG_USAGE; } - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& state_script : state_change_scripts) { - if (!parts[1].size() || (it->event == sc_event_id(parts[1]))) + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) { - con.print("%s (%s): %s%s\n", sc_event_name(it->event).c_str(), - it->save_specific ? "save-specific" : "global", - it->save_specific ? "/raw/" : "/", - it->path.c_str()); + con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path.c_str()); } } return CR_OK; @@ -1216,9 +1220,9 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); StateChangeScript script(evt, parts[2], save_specific); - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& state_script : state_change_scripts) { - if (script == *it) + if (script == state_script) { con << "Script already registered" << std::endl; return CR_FAILURE; @@ -1448,7 +1452,7 @@ static void fIOthread(IODATA * iod) while (true) { - std::string command = ""; + std::string command; int ret; while ((ret = con.lineedit("[DFHack]# ",command, main_history)) == Console::RETRY); @@ -1812,7 +1816,7 @@ bool Core::InitSimulationThread() if (entry.second) continue; std::filesystem::path filename = entry.first; - if (!config_files.count(filename)) { + if (!config_files.contains(filename)) { std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; @@ -1855,7 +1859,7 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); - IODATA *temp = new IODATA; + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1890,13 +1894,13 @@ bool Core::InitSimulationThread() if (raw[offset] == '"') { offset++; - size_t next = raw.find("\"", offset); + size_t next = raw.find('"', offset); args.push_back(raw.substr(offset, next - offset)); offset = next + 2; } else { - size_t next = raw.find(" ", offset); + size_t next = raw.find(' ', offset); if (next == std::string::npos) { args.push_back(raw.substr(offset)); @@ -1915,9 +1919,9 @@ bool Core::InitSimulationThread() if (first.length() > 0 && first[0] == '+') { std::vector cmd; - for (it++; it != args.end(); it++) { - const std::string & arg = *it; - if (arg.length() > 0 && arg[0] == '+') + for (const auto& arg : args) + { + if (!arg.empty() && arg[0] == '+') { break; } @@ -1927,9 +1931,9 @@ bool Core::InitSimulationThread() if (runCommand(con, first.substr(1), cmd) != CR_OK) { std::cerr << "Error running command: " << first.substr(1); - for (auto it2 = cmd.begin(); it2 != cmd.end(); it2++) + for (const auto& arg : cmd) { - std::cerr << " \"" << *it2 << "\""; + std::cerr << " \"" << arg << "\""; } std::cerr << "\n"; } @@ -1953,7 +1957,7 @@ bool Core::setHotkeyCmd( std::string cmd ) // access command std::lock_guard lock(HotkeyMutex); hotkey_set = SET; - hotkey_cmd = cmd; + hotkey_cmd = std::move(cmd); HotkeyCond.notify_all(); return true; } @@ -2170,43 +2174,44 @@ void Core::onUpdate(color_ostream &out) perf_counters.incCounter(perf_counters.update_lua_ms, step_start_ms); } -void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { +static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { std::vector files; DFHack::Filesystem::listdir(folder, files); - for ( auto f : files) { + for ( const auto& f : files) { if (f.stem().string().starts_with(prefix) && f.extension() == suffix) result.push_back(f); } - return; } size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; std::vector scriptFiles; - for ( size_t a = 0; a < prefix.size(); a++ ) { - getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); + for ( const auto& p : prefix ) { + getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); } - std::sort(scriptFiles.begin(), scriptFiles.end(), - [](const std::filesystem::path& a, const std::filesystem::path& b) { - return a.stem() < b.stem(); - }); + + std::ranges::sort(scriptFiles, + [](const std::filesystem::path& a, const std::filesystem::path& b) { + return a.stem() < b.stem(); + }); + size_t result = 0; - for ( size_t a = 0; a < scriptFiles.size(); a++ ) { + for ( const auto& file : scriptFiles) { result++; - core->loadScriptFile(out, folder / scriptFiles[a], false); + core->loadScriptFile(out, folder / file, false); } return result; } namespace DFHack { namespace X { - typedef state_change_event Key; - typedef std::vector Val; - typedef std::pair Entry; - typedef std::vector EntryVector; - typedef std::map InitVariationTable; + using Key = state_change_event; + using Val = std::vector; + using Entry = std::pair; + using EntryVector = std::vector; + using InitVariationTable = std::map; - EntryVector computeInitVariationTable(void* none, ...) { + static EntryVector computeInitVariationTable(void* none, ...) { va_list list; va_start(list,none); EntryVector result; @@ -2227,7 +2232,7 @@ namespace DFHack { return result; } - InitVariationTable getTable(const EntryVector& vec) { + static InitVariationTable getTable(const EntryVector& vec) { return InitVariationTable(vec.begin(),vec.end()); } } @@ -2258,17 +2263,17 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve loadScriptFiles(this, out, set, rawFolder); } - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& script : state_change_scripts) { - if (it->event == event) + if (script.event == event) { - if (!it->save_specific) + if (!script.save_specific) { - loadScriptFile(out, it->path, false); + loadScriptFile(out, script.path, false); } - else if (it->save_specific && isWorldLoaded()) + else if (script.save_specific && isWorldLoaded()) { - loadScriptFile(out, rawFolder / it->path, false); + loadScriptFile(out, rawFolder / script.path, false); } } } @@ -2278,7 +2283,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) { using df::global::gametype; static md5wrapper md5w; - static std::string ostype = ""; + static std::string ostype; if (!ostype.size()) { @@ -2480,7 +2485,7 @@ bool Core::DFH_ncurses_key(int key) return ncurses_wgetch(key, dummy); } -bool Core::getSuppressDuplicateKeyboardEvents() { +bool Core::getSuppressDuplicateKeyboardEvents() const { return suppress_duplicate_keyboard_events; } @@ -2605,8 +2610,8 @@ bool Core::SelectHotkey(int sym, int modifiers) // Check the internal keybindings std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - auto &binding = bindings[i]; + //for (int i = bindings.size()-1; i >= 0; --i) { + for (const auto& binding : bindings | std::views::reverse) { DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); if (binding.modifiers != modifiers) { @@ -2662,8 +2667,8 @@ bool Core::SelectHotkey(int sym, int modifiers) setHotkeyCmd(cmd); return true; } - else - return false; + + return false; } static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) @@ -2684,13 +2689,13 @@ static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string // ugh, ugly for (;;) { - if (keyspec.size() > 6 && keyspec.substr(0, 6) == "Shift-") { + if (keyspec.size() > 6 && keyspec.starts_with("Shift-")) { *pmod |= 1; keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.substr(0, 5) == "Ctrl-") { + } else if (keyspec.size() > 5 && keyspec.starts_with("Ctrl-")) { *pmod |= 2; keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.substr(0, 4) == "Alt-") { + } else if (keyspec.size() > 4 && keyspec.starts_with("Alt-")) { *pmod |= 4; keyspec = keyspec.substr(4); } else @@ -2709,13 +2714,13 @@ static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { *psym = SDLK_F1 + (keyspec[1]-'1'); return true; - } else if (keyspec.size() == 3 && keyspec.substr(0, 2) == "F1" && keyspec[2] >= '0' && keyspec[2] <= '2') { + } else if (keyspec.size() == 3 && keyspec.starts_with("F1") && keyspec[2] >= '0' && keyspec[2] <= '2') { *psym = SDLK_F10 + (keyspec[2]-'0'); return true; - } else if (keyspec.size() == 6 && keyspec.substr(0, 5) == "MOUSE" && keyspec[5] >= '4' && keyspec[5] <= '9') { + } else if (keyspec.size() == 6 && keyspec.starts_with("MOUSE") && keyspec[5] >= '4' && keyspec[5] <= '9') { *psym = SDLK_F13 + (keyspec[5]-'4'); return true; - } else if (keyspec.size() == 7 && keyspec.substr(0, 6) == "MOUSE1" && keyspec[5] >= '0' && keyspec[5] <= '5') { + } else if (keyspec.size() == 7 && keyspec.starts_with("MOUSE1") && keyspec[5] >= '0' && keyspec[5] <= '5') { *psym = SDLK_F19 + (keyspec[5]-'0'); return true; } else if (keyspec == "Enter") { @@ -2754,9 +2759,9 @@ bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) { std::vector focus_strings; split_string(&focus_strings, raw_focus, "|"); - for (size_t i = 0; i < focus_strings.size(); i++) + for (const auto& fs : focus_strings) { - if (!AddKeyBinding(raw_spec + "@" + focus_strings[i], cmdline)) + if (!AddKeyBinding(raw_spec + "@" + fs, cmdline)) return false; } return true; @@ -2838,7 +2843,7 @@ bool Core::RemoveAlias(const std::string &name) bool Core::IsAlias(const std::string &name) { std::lock_guard lock(alias_mutex); - return aliases.find(name) != aliases.end(); + return aliases.contains(name); } bool Core::RunAlias(color_ostream &out, const std::string &name, @@ -2903,10 +2908,9 @@ bool ClassNameCheck::operator() (Process *p, void * ptr) const { void ClassNameCheck::getKnownClassNames(std::vector &names) { - std::set::iterator it = known_class_names.begin(); - - for (; it != known_class_names.end(); it++) - names.push_back(*it); + for(const auto& kcn : known_class_names) { + names.push_back(kcn); + } } MemoryPatcher::MemoryPatcher(Process *p_) : p(p_) @@ -2922,7 +2926,7 @@ MemoryPatcher::~MemoryPatcher() bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) { - uint8_t *sptr = (uint8_t*)target; + auto *sptr = (uint8_t*)target; uint8_t *eptr = sptr + count; // Find the valid memory ranges diff --git a/library/include/Core.h b/library/include/Core.h index 383a0a9c954..50cfd8909b8 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -180,7 +180,7 @@ namespace DFHack std::filesystem::path findScript(std::string name); void getScriptPaths(std::vector *dest); - bool getSuppressDuplicateKeyboardEvents(); + bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); void setArmokTools(const std::vector &tool_names); From a5717a66d6d0a5bf18079ce129511f1a37e6c363 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:21:26 -0500 Subject: [PATCH 551/919] add missing include --- library/Core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Core.cpp b/library/Core.cpp index 0fa91ccee49..1fee8000e69 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -75,6 +75,7 @@ distribution. #include #include #include +#include #include #include #include From f9fae1391af320e690cdce0ab1edcbac577ddde5 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:43:41 -0500 Subject: [PATCH 552/919] fix typo --- library/Core.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 1fee8000e69..ef271b980d0 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -80,7 +80,6 @@ distribution. #include #include #include -#include #include #include #include @@ -810,7 +809,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { for (const auto& p : parts) { - if (p.empty() && p[0] == '-') + if (p.empty()) + continue; + + if (p[0] == '-') { if (p.find('a') != std::string::npos) all = true; From c454de10d85e36d7e377e010d3bc274ab17225b1 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:12:25 -0500 Subject: [PATCH 553/919] use size() as before --- library/Core.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 33717469950..7bd11562e31 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -794,10 +794,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { for (const auto& p : parts) { - if (p.empty()) - continue; - - if (p[0] == '-') + if (p.size() && p[0] == '-') { if (p.find('a') != std::string::npos) all = true; From 16d7e985a3d60c61b7f1bc8d90a242e4786b8686 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:59:57 -0500 Subject: [PATCH 554/919] typo: missing parts.size() test for 'plug' without arguments. use std::ranges::end() iterator. --- library/Core.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 7bd11562e31..14d82a30aa1 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -895,7 +895,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s con.print( "%21s %-3s%s\n", - (plug->getName()+":").c_str(), + (key+":").c_str(), plug->is_enabled() ? "on" : "off", plug->can_set_enabled() ? "" : " (controlled internally)" ); @@ -920,7 +920,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (!plug) continue; - if (std::ranges::find(parts, plug->getName()) == parts.end()) + if (parts.size() && std::ranges::find(parts, key) == std::ranges::end(parts)) continue; color_value color; From a52f36b895c89a3d2221ecf1ad54b4147ca4cc38 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:49:30 -0500 Subject: [PATCH 555/919] mostly add utility functions for backslash handling to MiscUtils.h --- library/Core.cpp | 32 ++++++++++++-------------------- library/include/MiscUtils.h | 13 +++++++++++++ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 14d82a30aa1..b49784db4b9 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -105,7 +105,7 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder); +static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { @@ -316,7 +316,7 @@ static std::string dfhack_version_desc() return s.str(); } -static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, std::span pargs) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, const std::span pargs) { if (!lua_checkstack(state, pargs.size()+10)) return false; @@ -329,7 +329,7 @@ static bool init_run_script(color_ostream &out, lua_State *state, const std::str return true; } -static command_result runLuaScript(color_ostream &out, std::string name, std::span args) +static command_result runLuaScript(color_ostream &out, std::string name, const std::span args) { auto init_fn = [n = std::move(name), args](color_ostream& out, lua_State* state) -> bool { return init_run_script(out, state, n, args); @@ -673,14 +673,14 @@ void tags_helper(color_ostream &con, const std::string &tag) { } } -void ls_helper(color_ostream &con, const std::vector ¶ms) { +static void ls_helper(color_ostream &con, const std::span params) { std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - std::string exclude_strs = ""; + std::string exclude_strs; bool in_exclude = false; - for (auto str : params) { + for (const auto& str : params) { if (in_exclude) exclude_strs = str; else if (str == "--notags") @@ -733,14 +733,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (first.empty()) return CR_NOT_IMPLEMENTED; - if (first.find('\\') != std::string::npos) + if (has_backslashes(first)) { con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", first.c_str()); - for (size_t i = 0; i < first.size(); i++) - { - if (first[i] == '\\') - first[i] = '/'; - } + replace_backslashes_with_forwardslashes(first); } // let's see what we actually got @@ -842,14 +838,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { for (auto& part : parts) { - if (part.find('\\') != std::string::npos) + if (has_backslashes(part)) { con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - for (char& c : part) - { - if (c == '\\') - c = '/'; - } + replace_backslashes_with_forwardslashes(part); } part = GetAliasCommand(part, true); @@ -920,7 +912,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (!plug) continue; - if (parts.size() && std::ranges::find(parts, key) == std::ranges::end(parts)) + if (parts.size() && std::ranges::find(parts, key) == parts.end()) continue; color_value color; @@ -2168,7 +2160,7 @@ static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, con } } -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::filesystem::path& folder) { +size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; std::vector scriptFiles; for ( const auto& p : prefix ) { diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 2ff04282e4c..1d703b2f83d 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -475,6 +475,19 @@ static inline std::string &trim(std::string &s) { return ltrim(rtrim(s)); } +static inline bool has_backslashes(const std::string_view str) +{ + return (str.find('\\') != std::string::npos); +} + +static inline void replace_backslashes_with_forwardslashes(std::string& str) +{ + for (auto& c : str) { + if (c == '\\') + c = '/'; + } +} + enum struct NumberFormatType : int32_t { DEFAULT = 0, ENGLISH, From 6ffc9d87129d5b1cce7ecf749e7167c41a86e3b2 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:00:44 -0500 Subject: [PATCH 556/919] try to cleanup unused includes from headers --- library/include/BitArray.h | 3 --- library/include/Core.h | 1 - library/include/DataDefs.h | 2 -- library/include/DataFuncs.h | 4 ---- library/include/DataIdentity.h | 4 +--- library/include/Error.h | 1 - library/include/LuaTools.h | 3 +-- library/include/LuaWrapper.h | 5 ----- library/include/MemAccess.h | 1 - library/include/MiscUtils.h | 1 - library/include/PluginManager.h | 1 - library/include/Types.h | 2 -- library/include/VTableInterpose.h | 3 ++- library/include/modules/DFSDL.h | 1 - library/include/modules/EventManager.h | 2 -- library/include/modules/Gui.h | 3 --- library/include/modules/Items.h | 2 -- library/include/modules/Job.h | 3 --- library/include/modules/Maps.h | 2 -- library/include/modules/Materials.h | 2 -- library/include/modules/Random.h | 3 --- library/include/modules/Screen.h | 3 --- library/include/modules/Translation.h | 4 ++-- library/include/modules/World.h | 4 +--- library/modules/DFSDL.cpp | 1 + library/modules/EventManager.cpp | 1 + plugins/autofarm.cpp | 1 + plugins/buildingplan/itemfilter.h | 2 ++ plugins/burrow.cpp | 2 ++ plugins/eventful.cpp | 1 + plugins/filltraffic.cpp | 1 + plugins/getplants.cpp | 2 ++ plugins/pathable.cpp | 2 ++ plugins/reveal.cpp | 1 + 34 files changed, 21 insertions(+), 53 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index bad77d79355..15ec84a62f6 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -23,14 +23,11 @@ distribution. */ #pragma once -#include "Export.h" #include "Error.h" #include #include #include #include -#include -#include #include namespace DFHack { diff --git a/library/include/Core.h b/library/include/Core.h index 49aaa504306..bced326fd12 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -37,7 +37,6 @@ distribution. #include #include #include -#include #include #include #include diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 7fac3c0c5ed..477496673f2 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -26,10 +26,8 @@ distribution. #include #include -#include #include #include -#include #include #include diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index 0b297a68e4a..ab9bb78a261 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -24,10 +24,6 @@ distribution. #pragma once -#include -#include -#include -#include #include #include "ColorText.h" diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 2c302264d93..8a9e3c406b5 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -26,11 +26,9 @@ distribution. #include #include -#include #include -#include #include -#include +#include #include #include #include diff --git a/library/include/Error.h b/library/include/Error.h index a4624d5f727..550e1b01983 100644 --- a/library/include/Error.h +++ b/library/include/Error.h @@ -25,7 +25,6 @@ distribution. #pragma once #include -#include #include #include "Export.h" diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 9831841ca2a..93853468e4e 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -26,8 +26,8 @@ distribution. #include #include -#include #include +#include #include #include #include @@ -39,7 +39,6 @@ distribution. #include "DataDefs.h" #include "df/interface_key.h" -#include "df/interfacest.h" #include #include diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 1b8be705ed6..7576be7a114 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -24,11 +24,6 @@ distribution. #pragma once -#include -#include -#include -#include - #include #include diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 95f7e256d17..5ef352a99e0 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -29,7 +29,6 @@ distribution. #define PROCESS_H_INCLUDED #include "Export.h" -#include #include #include #include diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 2ff04282e4c..fd785232109 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -33,7 +33,6 @@ distribution. #include #include #include -#include #include #include #include diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 50b9b0447d1..1eccc7a79a8 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -25,7 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Hooks.h" #include "ColorText.h" #include "MiscUtils.h" #include diff --git a/library/include/Types.h b/library/include/Types.h index 84408ecf2ab..f367946e561 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -30,12 +30,10 @@ distribution. #include #include "Export.h" - #include "DataDefs.h" #include "df/general_ref_type.h" #include "df/specific_ref_type.h" -#include "df/language_name_type.h" namespace df { struct building; diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h index 7575ee18c28..9dff74d7e59 100644 --- a/library/include/VTableInterpose.h +++ b/library/include/VTableInterpose.h @@ -25,7 +25,8 @@ distribution. #pragma once #include "DataDefs.h" -#include "DataIdentity.h" + +#include namespace DFHack { diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 8d12de19186..ff8e81ab2c8 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -1,6 +1,5 @@ #pragma once -#include "Error.h" #include "Export.h" #include "ColorText.h" diff --git a/library/include/modules/EventManager.h b/library/include/modules/EventManager.h index e0a74bed929..a2c546a7555 100644 --- a/library/include/modules/EventManager.h +++ b/library/include/modules/EventManager.h @@ -1,10 +1,8 @@ #pragma once -#include "Core.h" #include "Export.h" #include "ColorText.h" #include "PluginManager.h" -#include "Console.h" #include "DataDefs.h" #include "df/unit_inventory_item.h" diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 63939fc247a..761e948bb0b 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -25,8 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "ColorText.h" #include "Types.h" #include "DataDefs.h" @@ -34,7 +32,6 @@ distribution. #include "modules/GuiHooks.h" #include "df/announcement_type.h" -#include "df/report_zoom_type.h" #include "df/unit_report_type.h" namespace df { diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 1189fd8bc28..b4235df11d2 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -28,8 +28,6 @@ distribution. */ #include "DataDefs.h" #include "Export.h" -#include "MemAccess.h" -#include "Module.h" #include "Types.h" #include "modules/Materials.h" diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index 5f7ddcb11a2..25c357bec7e 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -27,7 +27,6 @@ distribution. #define CL_MOD_JOB #include "Export.h" -#include "Module.h" #include "Types.h" #include "DataDefs.h" @@ -35,8 +34,6 @@ distribution. #include "df/item_type.h" #include "df/job_item_ref.h" -#include - namespace df { struct job; diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 839989237e6..052dbe3aab1 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -31,8 +31,6 @@ distribution. #define CL_MOD_MAPS #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "modules/Materials.h" diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 90907b8a414..2c3be4e7318 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -31,8 +31,6 @@ distribution. */ #include "Export.h" #include "Module.h" -#include "Types.h" -#include "BitArray.h" #include "DataDefs.h" #include "df/craft_material_class.h" diff --git a/library/include/modules/Random.h b/library/include/modules/Random.h index 8ed703be790..b8e5de26bff 100644 --- a/library/include/modules/Random.h +++ b/library/include/modules/Random.h @@ -31,9 +31,6 @@ distribution. */ #include "Export.h" -#include "Module.h" -#include "Types.h" - #include "DataDefs.h" namespace DFHack diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index 3989460bb6f..1a6ab569ada 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -25,9 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Module.h" -#include "BitArray.h" -#include "ColorText.h" #include "Types.h" #include "DataDefs.h" diff --git a/library/include/modules/Translation.h b/library/include/modules/Translation.h index 2709dbf6fae..2860c3db879 100644 --- a/library/include/modules/Translation.h +++ b/library/include/modules/Translation.h @@ -31,9 +31,9 @@ distribution. */ #include "Export.h" -#include "Module.h" -#include "Types.h" #include "DataDefs.h" +#include "Types.h" +#include "df/language_name_type.h" namespace df { struct language_name; diff --git a/library/include/modules/World.h b/library/include/modules/World.h index b03170b5fa4..a82ceef9a40 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -32,12 +32,10 @@ distribution. */ #include "Export.h" -#include "Module.h" #include "modules/Persistence.h" -#include - #include "DataDefs.h" + namespace df { struct tile_bitmask; diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 536d1a5881b..9da2bd80570 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -1,3 +1,4 @@ +#include "Error.h" #include "Internal.h" #include "modules/DFSDL.h" diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 5a24f1174a2..d504e74c3dc 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -2,6 +2,7 @@ #include "Console.h" #include "Debug.h" #include "VTableInterpose.h" +#include "MemAccess.h" #include "modules/Buildings.h" #include "modules/Constructions.h" diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 571cd136085..f961bc13a7a 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -22,6 +22,7 @@ #include "df/unit.h" #include "df/world.h" +#include #include using namespace DFHack; diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 8a1f67d3cf9..95a216c302c 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -7,6 +7,8 @@ #include "df/dfhack_material_category.h" #include "df/item_quality.h" +#include + class ItemFilter { public: ItemFilter(); diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index d06e95efb64..b016014d0c2 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" diff --git a/plugins/eventful.cpp b/plugins/eventful.cpp index 0ed06d9284e..8dd3bbc85dd 100644 --- a/plugins/eventful.cpp +++ b/plugins/eventful.cpp @@ -22,6 +22,7 @@ #include "df/unit_wound.h" #include "df/world.h" +#include #include #include #include diff --git a/plugins/filltraffic.cpp b/plugins/filltraffic.cpp index c8b0a209e37..f16e8bdd838 100644 --- a/plugins/filltraffic.cpp +++ b/plugins/filltraffic.cpp @@ -1,6 +1,7 @@ // Wide-area traffic designation utility. // Flood-fill from cursor or fill entire map. +#include #include //For toupper(). #include //for min(). #include diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 29c6655fc98..18570fda95c 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -21,6 +21,8 @@ #include "df/world_object_data.h" #include "df/world_site.h" +#include + using std::string; using std::vector; using std::set; diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index 3bf9e2c0106..45a5827df6e 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "Error.h" #include "PluginManager.h" diff --git a/plugins/reveal.cpp b/plugins/reveal.cpp index 1feb72bbc6d..e5bd2479f57 100644 --- a/plugins/reveal.cpp +++ b/plugins/reveal.cpp @@ -16,6 +16,7 @@ #include "df/map_block.h" #include "df/world.h" +#include #include using std::string; From 3156695bbd48bab2af6089129f5a26679c96684a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 13 Nov 2025 13:05:35 -0600 Subject: [PATCH 557/919] use `unordered_map` instead of `map` for lookasides some of these are used in fairly hot code, and so using `unordered_map` should improve performance --- library/include/DataDefs.h | 46 +++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 7fac3c0c5ed..a067b8e24a9 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -31,7 +31,9 @@ distribution. #include #include #include +#include #include +#include #include "BitArray.h" #include "Export.h" @@ -136,8 +138,8 @@ namespace DFHack class DFHACK_EXPORT compound_identity : public constructed_identity { static std::list* list; - static std::map* parent_map; - static std::map>* children_map; + static std::unordered_map* parent_map; + static std::unordered_map>* children_map; static std::vector* top_scope; const char *dfhack_name; @@ -204,7 +206,7 @@ namespace DFHack class DFHACK_EXPORT enum_identity : public compound_identity { public: struct ComplexData { - std::map value_index_map; + std::unordered_map value_index_map; std::vector index_value_map; ComplexData(std::initializer_list values); size_t size() const { @@ -289,8 +291,8 @@ namespace DFHack }; class DFHACK_EXPORT struct_identity : public compound_identity { - static std::map* parent_map; - static std::map>* children_map; + static std::unordered_map* parent_map; + static std::unordered_map>* children_map; const struct_field_info *fields; @@ -355,6 +357,30 @@ namespace DFHack virtual void build_metatable(lua_State *state) const; }; + namespace + { + template + struct overload : Bases ... + { + using is_transparent = void; + using Bases::operator() ...; + }; + + struct char_pointer_hash + { + auto operator()(const char* ptr) const noexcept + { + return std::hash{}(ptr); + } + }; + + using transparent_string_hash = overload< + std::hash, + std::hash, + char_pointer_hash + >; + } + #ifdef _MSC_VER using virtual_ptr = void*; #else @@ -367,13 +393,13 @@ namespace DFHack class DFHACK_EXPORT virtual_identity : public struct_identity { public: using interpose_t = VMethodInterposeLinkBase*; - using interpose_list_t = std::map; + using interpose_list_t = std::unordered_map; private: - static std::map> *name_lookup; - static std::map* known; - static std::map* vtable_ptr_map; - static std::map* interpose_list_map; + static std::unordered_map> *name_lookup; + static std::unordered_map* known; + static std::unordered_map* vtable_ptr_map; + static std::unordered_map* interpose_list_map; const char *original_name; From c94555901e2a845f126e1eb0d62f7d0fb6e23e0f Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 21:27:28 -0500 Subject: [PATCH 558/919] Require empty bins when constructing bolt throwers --- docs/changelog.txt | 1 + library/lua/dfhack/buildings.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ea08534840..f91fe08e445 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: Bolt throwers will no longer be constructed using populated bins. ## Misc Improvements diff --git a/library/lua/dfhack/buildings.lua b/library/lua/dfhack/buildings.lua index 5ea3b1f7581..ae4d6243606 100644 --- a/library/lua/dfhack/buildings.lua +++ b/library/lua/dfhack/buildings.lua @@ -358,6 +358,7 @@ local siegeengine_input = { vector_id=df.job_item_vector_id.BOLT_THROWER_PARTS, }, { + flags1={ empty=true }, item_type=df.item_type.BIN, vector_id=df.job_item_vector_id.BIN, }, From 0871519d7c9af8a9b31fa5327a05c550f742218e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 21:11:49 -0500 Subject: [PATCH 559/919] Initial implementation for a linux crashlog --- library/CMakeLists.txt | 1 + library/Core.cpp | 10 ++++++ library/Crashlog.cpp | 81 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 library/Crashlog.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 895ae6eb860..fa5c482bf62 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -136,6 +136,7 @@ endif() set(MAIN_SOURCES_LINUX ${CONSOLE_SOURCES} + Crashlog.cpp ) set(MAIN_SOURCES_DARWIN diff --git a/library/Core.cpp b/library/Core.cpp index faec04db61d..42c7dda2cbf 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1540,6 +1540,11 @@ bool Core::InitMainThread() { Filesystem::init(); + #ifdef LINUX_BUILD + extern void dfhack_crashlog_init(); + dfhack_crashlog_init(); + #endif + // Re-route stdout and stderr again - DF seems to set up stdout and // stderr.txt on Windows as of 0.43.05. Also, log before switching files to // make it obvious what's going on if someone checks the *.txt files. @@ -2375,6 +2380,11 @@ void Core::onStateChange(color_ostream &out, state_change_event event) int Core::Shutdown ( void ) { + #ifdef LINUX_BUILD + extern void dfhack_crashlog_shutdown(); + dfhack_crashlog_shutdown(); + #endif + if(errorstate) return true; errorstate = 1; diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp new file mode 100644 index 00000000000..fa2ec6dad5d --- /dev/null +++ b/library/Crashlog.cpp @@ -0,0 +1,81 @@ +#include "DFHackVersion.h" +#include +#include +#include +#include +#include + +#include + +const int BT_ENTRY_MAX = 25; +int bt_entries = 0; +void* bt[BT_ENTRY_MAX]; +int crash_signal = 0; + +std::binary_semaphore crashlog_ready{0}; +std::binary_semaphore crashlog_complete{0}; + +std::thread crashlog_thread; +volatile bool shutdown = false; + +extern "C" void dfhack_crashlog_handle_signal(int sig) { + crash_signal = sig; + bt_entries = backtrace(bt, BT_ENTRY_MAX); + + // Signal saving of crashlog and wait for completion + crashlog_ready.release(); + crashlog_complete.acquire(); + std::quick_exit(1); +} + +void dfhack_save_crashlog() { + char** backtrace_strings = backtrace_symbols(bt, bt_entries); + if (!backtrace_strings) { + // Something has gone terribly wrong + return; + } + std::filesystem::path crashlog_path = "./crash.txt"; + std::ofstream crashlog(crashlog_path); + + crashlog << "Dwarf Fortress has crashed!" << "\n"; + crashlog << "DwarfFortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + + for (int i = 0; i < bt_entries; i++) { + crashlog << i << "> " << backtrace_strings[i] << "\n"; + } + + free(backtrace_strings); +} + +void dfhack_crashlog_thread() { + // Wait for crash or shutdown signal + crashlog_ready.acquire(); + if (shutdown) + return; + + dfhack_save_crashlog(); + crashlog_complete.release(); + std::quick_exit(1); +} + +const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; +namespace DFHack { +void dfhack_crashlog_init() { + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + + // Ensure the library is initialized to avoid AsyncSignal-Unsafe init during crash + int _ = backtrace(bt, 1); + + crashlog_thread = std::thread(dfhack_crashlog_thread); +} + +void dfhack_crashlog_shutdown() { + shutdown = true; + crashlog_ready.release(); + crashlog_thread.join(); + return; +} +} From 093896d2d8e21be084a951465192c16392a10cd9 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Nov 2025 11:11:32 -0500 Subject: [PATCH 560/919] Improve linux crashlog functionality and safety --- library/Crashlog.cpp | 119 ++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index fa2ec6dad5d..2c8ac0f17f8 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,47 +1,94 @@ #include "DFHackVersion.h" #include #include -#include #include #include #include const int BT_ENTRY_MAX = 25; -int bt_entries = 0; -void* bt[BT_ENTRY_MAX]; -int crash_signal = 0; +struct CrashInfo { + int backtrace_entries = 0; + void* backtrace[BT_ENTRY_MAX]; + int signal = 0; +}; -std::binary_semaphore crashlog_ready{0}; -std::binary_semaphore crashlog_complete{0}; +CrashInfo crash_info; + +/* + * As of c++17 the only safe stdc++ methods are plain lock-free atomic methods + * This sadly means that using std::semaphore *could* cause issues according to the standard. + */ +std::atomic_bool crashed = false; +std::atomic_bool crashlog_ready = false; +std::atomic_bool crashlog_complete = false; + +void flag_set(std::atomic_bool &atom) { + atom.store(true); + atom.notify_all(); +} +void flag_wait(std::atomic_bool &atom) { + atom.wait(false); +} std::thread crashlog_thread; -volatile bool shutdown = false; +bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - crash_signal = sig; - bt_entries = backtrace(bt, BT_ENTRY_MAX); + if (crashed.exchange(true)) { + // Crashlog already produced, bail thread. + std::quick_exit(1); + } + crash_info.signal = sig; + crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); // Signal saving of crashlog and wait for completion - crashlog_ready.release(); - crashlog_complete.acquire(); + flag_set(crashlog_ready); + flag_wait(crashlog_complete); std::quick_exit(1); } +void dfhack_crashlog_handle_terminate() { + dfhack_crashlog_handle_signal(0); +} + +std::string signal_name(int sig) { + switch (sig) { + case SIGINT: + return "SIGINT"; + case SIGILL: + return "SIGILL"; + case SIGABRT: + return "SIGABRT"; + case SIGFPE: + return "SIGFPE"; + case SIGSEGV: + return "SIGSEGV"; + case SIGTERM: + return "SIGTERM"; + } + return ""; +} + void dfhack_save_crashlog() { - char** backtrace_strings = backtrace_symbols(bt, bt_entries); + char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); if (!backtrace_strings) { - // Something has gone terribly wrong + // Allocation failed, give up return; } std::filesystem::path crashlog_path = "./crash.txt"; std::ofstream crashlog(crashlog_path); - crashlog << "Dwarf Fortress has crashed!" << "\n"; - crashlog << "DwarfFortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; - for (int i = 0; i < bt_entries; i++) { + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal << "\n"; + } + + for (int i = 0; i < crash_info.backtrace_entries; i++) { crashlog << i << "> " << backtrace_strings[i] << "\n"; } @@ -49,33 +96,37 @@ void dfhack_save_crashlog() { } void dfhack_crashlog_thread() { - // Wait for crash or shutdown signal - crashlog_ready.acquire(); - if (shutdown) + // Wait for activation signal + flag_wait(crashlog_ready); + if (shutdown) // Shutting down gracefully, end thread. return; dfhack_save_crashlog(); - crashlog_complete.release(); + + flag_set(crashlog_complete); std::quick_exit(1); } const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { -void dfhack_crashlog_init() { - for (int signal : desired_signals) { - std::signal(signal, dfhack_crashlog_handle_signal); - } + void dfhack_crashlog_init() { + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + std::set_terminate(dfhack_crashlog_handle_terminate); - // Ensure the library is initialized to avoid AsyncSignal-Unsafe init during crash - int _ = backtrace(bt, 1); + // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 + // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s + // Using it here ensures it is loaded before use in the signal handler. + int _ = backtrace(crash_info.backtrace, 1); - crashlog_thread = std::thread(dfhack_crashlog_thread); -} + crashlog_thread = std::thread(dfhack_crashlog_thread); + } -void dfhack_crashlog_shutdown() { - shutdown = true; - crashlog_ready.release(); - crashlog_thread.join(); - return; -} + void dfhack_crashlog_shutdown() { + shutdown = true; + flag_set(crashlog_ready); + crashlog_thread.join(); + return; + } } From 6dabe6adb454f550ca30849494ce21ad095fea3d Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 22:21:28 -0500 Subject: [PATCH 561/919] Generate filepath using timestamp --- library/Crashlog.cpp | 53 +++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 2c8ac0f17f8..e73a5e8b481 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,8 +1,9 @@ #include "DFHackVersion.h" #include -#include +#include #include #include +#include #include @@ -41,7 +42,7 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { } crash_info.signal = sig; crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); - + // Signal saving of crashlog and wait for completion flag_set(crashlog_ready); flag_wait(crashlog_complete); @@ -70,27 +71,49 @@ std::string signal_name(int sig) { return ""; } +std::filesystem::path get_crashlog_path() { + std::time_t time = std::time(nullptr); + std::tm* tm = std::localtime(&time); + + std::string timestamp = "unknown"; + if (tm) { + char stamp[64]; + std::size_t out = strftime(&stamp[0], 63, "%Y-%m-%d-%H-%M-%S", tm); + if (out != 0) + timestamp = stamp; + } + + std::filesystem::path dir = "crashlog"; + std::error_code err; + std::filesystem::create_directories(dir, err); + + std::filesystem::path log_path = dir / ("crash_" + timestamp + ".txt"); + return log_path; +} + void dfhack_save_crashlog() { char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); if (!backtrace_strings) { // Allocation failed, give up return; } - std::filesystem::path crashlog_path = "./crash.txt"; - std::ofstream crashlog(crashlog_path); + try { + std::filesystem::path crashlog_path = get_crashlog_path(); + std::ofstream crashlog(crashlog_path); - crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; - crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; - crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; - std::string signal = signal_name(crash_info.signal); - if (!signal.empty()) { - crashlog << "Signal " << signal << "\n"; - } + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal << "\n"; + } - for (int i = 0; i < crash_info.backtrace_entries; i++) { - crashlog << i << "> " << backtrace_strings[i] << "\n"; - } + for (int i = 0; i < crash_info.backtrace_entries; i++) { + crashlog << i << "> " << backtrace_strings[i] << "\n"; + } + } catch (...) {} free(backtrace_strings); } @@ -118,7 +141,7 @@ namespace DFHack { // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s // Using it here ensures it is loaded before use in the signal handler. - int _ = backtrace(crash_info.backtrace, 1); + [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); crashlog_thread = std::thread(dfhack_crashlog_thread); } From 0d4bc8ac4e092e7f21481c69179d189c0c0cfbeb Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 23:45:59 -0500 Subject: [PATCH 562/919] Remove signal and terminate handlers on shutdown --- library/Crashlog.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index e73a5e8b481..c3ab5d4d679 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -130,13 +130,15 @@ void dfhack_crashlog_thread() { std::quick_exit(1); } +std::terminate_handler term_handler = nullptr; + const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { for (int signal : desired_signals) { std::signal(signal, dfhack_crashlog_handle_signal); } - std::set_terminate(dfhack_crashlog_handle_terminate); + term_handler = std::set_terminate(dfhack_crashlog_handle_terminate); // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s @@ -147,6 +149,11 @@ namespace DFHack { } void dfhack_crashlog_shutdown() { + for (int signal : desired_signals) { + std::signal(signal, SIG_DFL); + } + std::set_terminate(term_handler); + shutdown = true; flag_set(crashlog_ready); crashlog_thread.join(); From 25ac663f5a9327a1cac825fab5069e3f263783e7 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 09:20:06 -0500 Subject: [PATCH 563/919] Use eventfd to wait instead of cvar for as-safety --- library/Crashlog.cpp | 54 ++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index c3ab5d4d679..7286d832321 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include const int BT_ENTRY_MAX = 25; struct CrashInfo { @@ -16,13 +18,11 @@ struct CrashInfo { CrashInfo crash_info; -/* - * As of c++17 the only safe stdc++ methods are plain lock-free atomic methods - * This sadly means that using std::semaphore *could* cause issues according to the standard. - */ -std::atomic_bool crashed = false; -std::atomic_bool crashlog_ready = false; -std::atomic_bool crashlog_complete = false; +std::atomic crashed = false; +std::atomic crashlog_ready = false; + +// Use eventfd for async-signal safe waiting +int crashlog_complete = -1; void flag_set(std::atomic_bool &atom) { atom.store(true); @@ -32,20 +32,32 @@ void flag_wait(std::atomic_bool &atom) { atom.wait(false); } +void signal_crashlog_complete() { + if (crashlog_complete == -1) + return; + uint64_t v = 1; + write(crashlog_complete, &v, sizeof(v)); +} + std::thread crashlog_thread; -bool shutdown = false; +volatile bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - if (crashed.exchange(true)) { - // Crashlog already produced, bail thread. + if (shutdown || crashed.exchange(true) || crashlog_ready.load()) { + // Ensure the signal handler doesn't try to write a crashlog + // whilst the crashlog thread is unavailable. std::quick_exit(1); } crash_info.signal = sig; crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); - // Signal saving of crashlog and wait for completion + // Signal saving of crashlog flag_set(crashlog_ready); - flag_wait(crashlog_complete); + // Wait for completion via eventfd read, if fd isn't valid, bail + if (crashlog_complete != -1) { + [[maybe_unused]] uint64_t _; + read(crashlog_complete, &_, sizeof(_)); + } std::quick_exit(1); } @@ -125,8 +137,7 @@ void dfhack_crashlog_thread() { return; dfhack_save_crashlog(); - - flag_set(crashlog_complete); + signal_crashlog_complete(); std::quick_exit(1); } @@ -135,6 +146,11 @@ std::terminate_handler term_handler = nullptr; const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { + // Initialize eventfd flag + crashlog_complete = eventfd(0, EFD_CLOEXEC); + + crashlog_thread = std::thread(dfhack_crashlog_thread); + for (int signal : desired_signals) { std::signal(signal, dfhack_crashlog_handle_signal); } @@ -144,19 +160,23 @@ namespace DFHack { // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s // Using it here ensures it is loaded before use in the signal handler. [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); - - crashlog_thread = std::thread(dfhack_crashlog_thread); } void dfhack_crashlog_shutdown() { + shutdown = true; for (int signal : desired_signals) { std::signal(signal, SIG_DFL); } std::set_terminate(term_handler); - shutdown = true; + // Shutdown the crashlog thread. flag_set(crashlog_ready); crashlog_thread.join(); + + // If the signal handler is somehow running whilst here, let it terminate + signal_crashlog_complete(); + if (crashlog_complete != -1) + close(crashlog_complete); // Close fd return; } } From cf28bd362121eb87bcaa8ea69463ae43039fe0e2 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 09:41:38 -0500 Subject: [PATCH 564/919] More maybe_unused --- library/Crashlog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 7286d832321..22bd40cf312 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -36,7 +36,7 @@ void signal_crashlog_complete() { if (crashlog_complete == -1) return; uint64_t v = 1; - write(crashlog_complete, &v, sizeof(v)); + [[maybe_unused]] auto _ = write(crashlog_complete, &v, sizeof(v)); } std::thread crashlog_thread; @@ -55,8 +55,8 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { flag_set(crashlog_ready); // Wait for completion via eventfd read, if fd isn't valid, bail if (crashlog_complete != -1) { - [[maybe_unused]] uint64_t _; - read(crashlog_complete, &_, sizeof(_)); + [[maybe_unused]] uint64_t v; + [[maybe_unused]] auto _ = read(crashlog_complete, &v, sizeof(v)); } std::quick_exit(1); } From 8fbaedf20932c1710e267fcbd0a5a5bf61985ff1 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 21:48:12 -0500 Subject: [PATCH 565/919] Use atomic bool for shutdown flag --- library/Crashlog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 22bd40cf312..c560666f99b 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -20,6 +20,7 @@ CrashInfo crash_info; std::atomic crashed = false; std::atomic crashlog_ready = false; +std::atomic shutdown = false; // Use eventfd for async-signal safe waiting int crashlog_complete = -1; @@ -40,10 +41,9 @@ void signal_crashlog_complete() { } std::thread crashlog_thread; -volatile bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - if (shutdown || crashed.exchange(true) || crashlog_ready.load()) { + if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { // Ensure the signal handler doesn't try to write a crashlog // whilst the crashlog thread is unavailable. std::quick_exit(1); @@ -133,7 +133,7 @@ void dfhack_save_crashlog() { void dfhack_crashlog_thread() { // Wait for activation signal flag_wait(crashlog_ready); - if (shutdown) // Shutting down gracefully, end thread. + if (shutdown.load()) // Shutting down gracefully, end thread. return; dfhack_save_crashlog(); @@ -163,7 +163,7 @@ namespace DFHack { } void dfhack_crashlog_shutdown() { - shutdown = true; + shutdown.exchange(true); for (int signal : desired_signals) { std::signal(signal, SIG_DFL); } From c6736649a493b37463e81ac8379c4ce1b2629652 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 19:01:52 -0600 Subject: [PATCH 566/919] df structure support implement `operator==`, `operator<`, and `std::hash` for `labor_kitchen_interface_food_key` --- library/include/df/custom/.gitignore | 1 + .../hash/labor_kitchen_interface_food_key.h | 16 ++++++++++++++++ .../labor_kitchen_interface_food_key.custom.inc | 11 +++++++++++ 3 files changed, 28 insertions(+) create mode 100644 library/include/df/custom/.gitignore create mode 100644 library/include/df/custom/hash/labor_kitchen_interface_food_key.h create mode 100644 library/include/df/custom/labor_kitchen_interface_food_key.custom.inc diff --git a/library/include/df/custom/.gitignore b/library/include/df/custom/.gitignore new file mode 100644 index 00000000000..96e79c167db --- /dev/null +++ b/library/include/df/custom/.gitignore @@ -0,0 +1 @@ +!*.h diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h new file mode 100644 index 00000000000..3ca96767253 --- /dev/null +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -0,0 +1,16 @@ +template<> +struct std::hash { + auto operator()(const labor_kitchen_interface_food_key &a) const -> size_t { + struct thing { + int16_t t; + int16_t st; + int32_t x; + } thing{ + .t = a.type; + .st = a.subtype, + .x = static_cast(a.mat) ^ (static_cast(a.matg)) + }; + + return hash()(bit_cast(thing)); + } + }; diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc b/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc new file mode 100644 index 00000000000..59796a69ba9 --- /dev/null +++ b/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc @@ -0,0 +1,11 @@ + bool operator<(const labor_kitchen_interface_food_key &b) const { + if (type Date: Sat, 15 Nov 2025 19:27:07 -0600 Subject: [PATCH 567/919] Rename labor_kitchen_interface_food_key.custom.inc to labor_kitchen_interface_food_key.methods.inc correct file name --- ...ey.custom.inc => labor_kitchen_interface_food_key.methods.inc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename library/include/df/custom/{labor_kitchen_interface_food_key.custom.inc => labor_kitchen_interface_food_key.methods.inc} (100%) diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.custom.inc b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc similarity index 100% rename from library/include/df/custom/labor_kitchen_interface_food_key.custom.inc rename to library/include/df/custom/labor_kitchen_interface_food_key.methods.inc From e2eef85656df1621799b27fecafc40a03be59bdf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 19:35:40 -0600 Subject: [PATCH 568/919] add missing namespace --- .../include/df/custom/hash/labor_kitchen_interface_food_key.h | 2 +- .../df/custom/labor_kitchen_interface_food_key.methods.inc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index 3ca96767253..4880f385fbc 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,5 +1,5 @@ template<> -struct std::hash { +struct std::hash<> { auto operator()(const labor_kitchen_interface_food_key &a) const -> size_t { struct thing { int16_t t; diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc index 59796a69ba9..a1de2fec13d 100644 --- a/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc +++ b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc @@ -1,4 +1,4 @@ - bool operator<(const labor_kitchen_interface_food_key &b) const { + bool operator<(const df::labor_kitchen_interface_food_key &b) const { if (type Date: Sat, 15 Nov 2025 19:41:31 -0600 Subject: [PATCH 569/919] Update labor_kitchen_interface_food_key.h grr forgot to save --- .../include/df/custom/hash/labor_kitchen_interface_food_key.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index 4880f385fbc..4386621a6a8 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,6 +1,6 @@ template<> struct std::hash<> { - auto operator()(const labor_kitchen_interface_food_key &a) const -> size_t { + auto operator()(const df::labor_kitchen_interface_food_key &a) const -> size_t { struct thing { int16_t t; int16_t st; From 1df6c49c3b815d70e1b4bee3bafc8d80ff111364 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 15 Nov 2025 21:33:20 -0500 Subject: [PATCH 570/919] Various updates for v53 items/buildings --- docs/changelog.txt | 3 +++ docs/guides/quickfort-user-guide.rst | 2 ++ library/modules/Items.cpp | 1 + plugins/autolabor/laborstatemap.h | 6 ++++++ plugins/blueprint.cpp | 24 ++++++++++++++++++++++-- plugins/suspendmanager.cpp | 5 ++++- 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ea08534840..d235191e3e3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,11 @@ Template for new versions: ## New Features ## Fixes +- `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform ## Misc Improvements +- `blueprint`: support for reinforced walls and bolt throwers +- `autolabor`: support for new dying and siege-related labors ## Documentation diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index a5b39c529b6..dae03c3aba5 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -2298,8 +2298,10 @@ Symbol Type Properties ``ek`` kiln ``en`` magma kiln ``ib`` ballista +``it`` bolt thrower ``ic`` catapult ``Cw`` wall +``CW`` reinforced wall ``Cf`` floor ``Cr`` ramp ``Cu`` up stair diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 5b9e781721a..b3d327881ee 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1200,6 +1200,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, break; case CATAPULTPARTS: case BALLISTAPARTS: + case BOLT_THROWER_PARTS: case TRAPPARTS: value = 30; break; diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h index a9472bcc2ea..3757011d639 100644 --- a/plugins/autolabor/laborstatemap.h +++ b/plugins/autolabor/laborstatemap.h @@ -282,6 +282,12 @@ const dwarf_state dwarf_states[] = { dwarf_state::OTHER /* HeistItem */, dwarf_state::OTHER /* InterrogateSubject */, dwarf_state::OTHER /* AcceptHeistItem */, + dwarf_state::BUSY /* StoreSquadEquipmentItem */, + dwarf_state::BUSY /* MixDye */, + dwarf_state::BUSY /* DyeLeather */, + dwarf_state::BUSY /* ConstructBoltThrowerParts */, + dwarf_state::BUSY /* LoadBoltThrower */, + dwarf_state::BUSY /* FireBoltThrower */, }; #define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 26caf258169..98b4c1c1552 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -16,6 +16,7 @@ #include "TileTypes.h" #include "modules/Buildings.h" +#include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" #include "modules/Maps.h" @@ -33,6 +34,7 @@ #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" +#include "df/construction.h" #include "df/engraving.h" #include "df/entity_position.h" #include "df/tile_bitmask.h" @@ -610,6 +612,7 @@ static const char * get_construction_str(df::building *b) { case construction_type::TrackRampNEW: return "trackrampNEW"; case construction_type::TrackRampSEW: return "trackrampSEW"; case construction_type::TrackRampNSEW: return "trackrampNSEW"; + case construction_type::ReinforcedWall: return "CW"; case construction_type::NONE: default: return "~"; @@ -632,6 +635,13 @@ static const char * get_constructed_track_str(df::tiletype *tt, return cache(str); } +static const char * get_constructed_wall_str(df::coord pos) { + df::construction *c = Constructions::findAtTile(pos); + if (!c || !(c->flags.bits.reinforced)) + return "Cw"; + return "CW"; +} + static const char * get_constructed_floor_str(df::tiletype *tt) { if (tileSpecial(*tt) != df::tiletype_special::TRACK) return "Cf"; @@ -653,7 +663,7 @@ static const char * get_tile_construct(color_ostream &out, const df::coord &pos, return NULL; switch (tileShape(*tt)) { - case tiletype_shape::WALL: return "Cw"; + case tiletype_shape::WALL: return get_constructed_wall_str(pos); case tiletype_shape::FLOOR: return get_constructed_floor_str(tt); case tiletype_shape::RAMP: return get_constructed_ramp_str(tt); case tiletype_shape::FORTIFICATION: return "CF"; @@ -732,7 +742,17 @@ static const char * get_bridge_str(df::building *b) { static const char * get_siege_str(df::building *b) { df::building_siegeenginest *se = virtual_cast(b); - return !se || se->type == df::siegeengine_type::Catapult ? "ic" : "ib"; + if (!se) + return "ic"; + + switch(se->type) { + case df::siegeengine_type::Catapult: return "ic"; + case df::siegeengine_type::Ballista: return "ib"; + case df::siegeengine_type::BoltThrower: return "it"; + default: + return "ic"; + } + } static const char * get_workshop_str(df::building *b) { diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index d63d6a99c9a..0756e108321 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -113,11 +113,13 @@ using df::coord; // impassible constructions static const std::bitset<64> construction_impassible = std::bitset<64>() .set(construction_type::Wall) + .set(construction_type::ReinforcedWall) .set(construction_type::Fortification); // constructions requiring same support as walls static const std::bitset<64> construction_wall_support = std::bitset<64>() .set(construction_type::Wall) + .set(construction_type::ReinforcedWall) .set(construction_type::Fortification) .set(construction_type::UpStair) .set(construction_type::UpDownStair); @@ -395,7 +397,8 @@ class SuspendManager { // other tiles can become suitable if a wall is being constructed below auto below = Buildings::findAtTile(coord(pos.x,pos.y,pos.z-1)); if (below && below->getType() == df::building_type::Construction && - below->getSubtype() == construction_type::Wall) + (below->getSubtype() == construction_type::Wall || + below->getSubtype() == construction_type::ReinforcedWall)) return true; return false; From b91188208eb7a00f87253da60b26242f6589bde0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 20:49:08 -0600 Subject: [PATCH 571/919] take three? --- .../hash/labor_kitchen_interface_food_key.h | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index 4386621a6a8..b1fdd73fdbd 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,16 +1,26 @@ -template<> -struct std::hash<> { - auto operator()(const df::labor_kitchen_interface_food_key &a) const -> size_t { - struct thing { - int16_t t; - int16_t st; - int32_t x; - } thing{ - .t = a.type; - .st = a.subtype, - .x = static_cast(a.mat) ^ (static_cast(a.matg)) - }; +#include +#include +#include "df/labor_kitchen_interface_food_key.h" - return hash()(bit_cast(thing)); +namespace std +{ + template<> + struct hash + { + auto operator()(const df::labor_kitchen_interface_food_key& a) const -> size_t + { + struct thing + { + int16_t t; + int16_t st; + int32_t x; + } thing{ + .t = a.type, + .st = a.subtype, + .x = static_cast(a.mat) ^ (static_cast(a.matg)) + }; + + return hash()(std::bit_cast(thing)); } }; +} From 7b3e3157963dfc981b43efbc64d7dfab4b249b8d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 15 Nov 2025 21:01:51 -0600 Subject: [PATCH 572/919] add include guard to labor_kitchen_interface_food_key.h --- .../include/df/custom/hash/labor_kitchen_interface_food_key.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h index b1fdd73fdbd..5f2d0d2b339 100644 --- a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include "df/labor_kitchen_interface_food_key.h" From 1edf08d4ec6fa2b275f14b4ea24d104d4f99b136 Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:00:22 -0500 Subject: [PATCH 573/919] use std::string_view. eliminate C-varargs when building state_change_event table. --- library/Core.cpp | 71 ++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index b49784db4b9..685703eb335 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -316,23 +316,23 @@ static std::string dfhack_version_desc() return s.str(); } -static bool init_run_script(color_ostream &out, lua_State *state, const std::string& pcmd, const std::span pargs) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string_view pcmd, const std::span pargs) { if (!lua_checkstack(state, pargs.size()+10)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "run_script"); lua_remove(state, -2); - lua_pushstring(state, pcmd.c_str()); + lua_pushlstring(state, pcmd.data(), pcmd.size()); for (const auto& arg : pargs) lua_pushstring(state, arg.c_str()); return true; } -static command_result runLuaScript(color_ostream &out, std::string name, const std::span args) +static command_result runLuaScript(color_ostream &out, const std::string_view name, const std::span args) { - auto init_fn = [n = std::move(name), args](color_ostream& out, lua_State* state) -> bool { - return init_run_script(out, state, n, args); + auto init_fn = [name, args](color_ostream& out, lua_State* state) -> bool { + return init_run_script(out, state, name, args); }; bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); @@ -340,22 +340,22 @@ static command_result runLuaScript(color_ostream &out, std::string name, const s return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, const std::string& name, bool enable) +static bool init_enable_script(color_ostream &out, lua_State *state, const std::string_view name, bool enable) { if (!lua_checkstack(state, 4)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "enable_script"); lua_remove(state, -2); - lua_pushstring(state, name.c_str()); + lua_pushlstring(state, name.data(), name.size()); lua_pushboolean(state, enable); return true; } -static command_result enableLuaScript(color_ostream &out, std::string name, bool enabled) +static command_result enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) { - auto init_fn = [n = std::move(name), enabled](color_ostream& out, lua_State* state) -> bool { - return init_enable_script(out, state, n, enabled); + auto init_fn = [name, enabled](color_ostream& out, lua_State* state) -> bool { + return init_enable_script(out, state, name, enabled); }; bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); @@ -677,7 +677,7 @@ static void ls_helper(color_ostream &con, const std::span par std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - std::string exclude_strs; + std::string_view exclude_strs; bool in_exclude = false; for (const auto& str : params) { @@ -2188,24 +2188,31 @@ namespace DFHack { using EntryVector = std::vector; using InitVariationTable = std::map; - static EntryVector computeInitVariationTable(void* none, ...) { - va_list list; - va_start(list,none); + template + concept VariationTableTypes = std::same_as || std::convertible_to; + + template + static EntryVector computeInitVariationTable(Ts&&... ts) { + using Arg = std::variant; + std::vector args{ Arg{std::forward(ts)}... }; + + Key current_key = SC_UNKNOWN; EntryVector result; - while(true) { - Key key = (Key)va_arg(list,int); - if ( key == SC_UNKNOWN ) - break; - Val val; - while (true) { - const char *v = va_arg(list, const char *); - if (!v || !v[0]) - break; - val.emplace_back(v); + Val val; + for (auto& arg : args) { + if (std::holds_alternative(arg)) { + if (current_key != SC_UNKNOWN && !val.empty()) { + result.emplace_back(current_key, val); + val.clear(); + } + current_key = std::get(arg); + } else if (std::holds_alternative(arg)) { + auto str = std::get(arg); + if (!str.empty()) + val.emplace_back(str); } - result.push_back(Entry(key,val)); } - va_end(list); + return result; } @@ -2216,12 +2223,12 @@ namespace DFHack { } void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event event) { - static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(nullptr, - (int)SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", "", - (int)SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", "", - (int)SC_MAP_LOADED, "onMapLoad", "onLoadMap", "", - (int)SC_MAP_UNLOADED, "onMapUnload", "onUnloadMap", "", - (int)SC_UNKNOWN + static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable( + SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", + SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", + SC_MAP_LOADED, "onMapLoad", "onLoadMap", + SC_MAP_UNLOADED, "onMapUnload", "onUnloadMap", + SC_UNKNOWN )); if (!df::global::world) From a6ed266f701b929f80ef5d8d6669739ee878743e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:21:06 +0000 Subject: [PATCH 574/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 525024cf722..f69c2c91821 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 525024cf72244cd6c384b723a0b7a34a50b336ae +Subproject commit f69c2c91821330899d795e52edd6655f1dc61477 diff --git a/scripts b/scripts index f71c1568f3a..d286ffc6f27 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f71c1568f3af618fc542f1af2028dc89ba383f7a +Subproject commit d286ffc6f27de862e0298fded0f4464b4842bf18 From 648045289e97a89c459d0cfb79edf9bfe2f3f032 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 12:32:37 -0600 Subject: [PATCH 575/919] adjustments for change in `announcement_alert_type` enum for compatibility with DFHack/df-structures#859 --- library/modules/Gui.cpp | 2 ++ library/xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3808fa02712..1e079da598c 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1957,6 +1957,8 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, df::report auto alert_type = announcement_alert_type::NONE; switch (slot) { + case unit_report_type::NONE: /* should never happen? */ + return false; case unit_report_type::Combat: world->status.flags.bits.combat = true; alert_type = announcement_alert_type::COMBAT; diff --git a/library/xml b/library/xml index 145f317340a..01c3774730e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 145f317340ac9327854318c7ba758ee2af5e1928 +Subproject commit 01c3774730e2c89b2a4a7894c03f43a91b8db0a7 From bde3891297ee9459c400b156b8fb9f69591662fd Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:09:23 +0000 Subject: [PATCH 576/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 01c3774730e..7ba825f763f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 01c3774730e2c89b2a4a7894c03f43a91b8db0a7 +Subproject commit 7ba825f763f414d90314d4e5b6d4563d8389a98a diff --git a/scripts b/scripts index d286ffc6f27..0936c5369e3 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d286ffc6f27de862e0298fded0f4464b4842bf18 +Subproject commit 0936c5369e389f36811d71094c3a0f0b4435637b From 48fd7dba0104e3be6f031dc57aa7c9df42f2b109 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 13:31:37 -0600 Subject: [PATCH 577/919] update RFR for changes to siegeengines for compatibility with DFHack/df-structures#860 --- docs/changelog.txt | 1 + library/xml | 2 +- .../remotefortressreader/building_reader.cpp | 25 ++++++++++++++----- .../proto/RemoteFortressReader.proto | 6 ++++- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7d5a3e6db3f..dfdce58d2b0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## Fixes - `buildingplan`: Bolt throwers will no longer be constructed using populated bins. +- `RemoteFortressReader`: updated siege engine facing enums for new diagonal directions - `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform ## Misc Improvements diff --git a/library/xml b/library/xml index 7ba825f763f..33bb9b6c08c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7ba825f763f414d90314d4e5b6d4563d8389a98a +Subproject commit 33bb9b6c08c661ef30b9bcc4ae652ec2e09bbd8f diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index ebf0359afdc..62475ce1aa4 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -560,18 +560,31 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto facing = actual->facing; switch (facing) { - case df::building_siegeenginest::Left: - remote_build->set_direction(WEST); - break; - case df::building_siegeenginest::Up: + using enum df::enums::siegeengine_orientation::siegeengine_orientation; + case North: remote_build->set_direction(NORTH); break; - case df::building_siegeenginest::Right: + case Northeast: + remote_build->set_direction(NORTHEAST); + break; + case East: remote_build->set_direction(EAST); break; - case df::building_siegeenginest::Down: + case Southeast: + remote_build->set_direction(SOUTHEAST); + break; + case South: remote_build->set_direction(SOUTH); break; + case Southwest: + remote_build->set_direction(SOUTHWEST); + break; + case West: + remote_build->set_direction(WEST); + break; + case Northwest: + remote_build->set_direction(NORTHWEST); + break; default: break; } diff --git a/plugins/remotefortressreader/proto/RemoteFortressReader.proto b/plugins/remotefortressreader/proto/RemoteFortressReader.proto index a092a120973..b7dc7bdc883 100644 --- a/plugins/remotefortressreader/proto/RemoteFortressReader.proto +++ b/plugins/remotefortressreader/proto/RemoteFortressReader.proto @@ -141,7 +141,11 @@ enum BuildingDirection EAST = 1; SOUTH = 2; WEST = 3; - NONE = 4; + NORTHEAST = 4; + SOUTHEAST = 5; + SOUTHWEST = 6; + NORTHWEST = 7; + NONE = 8; } enum TileDigDesignation From f8cff802562bb7ce004ad2c42df5db2ad38eee0e Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:44:40 +0000 Subject: [PATCH 578/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 33bb9b6c08c..07bd0e8e160 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 33bb9b6c08c661ef30b9bcc4ae652ec2e09bbd8f +Subproject commit 07bd0e8e160bf5e4b62cd6dc3ba643fcd7dac836 diff --git a/plugins/stonesense b/plugins/stonesense index f69c2c91821..7a82390f42f 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f69c2c91821330899d795e52edd6655f1dc61477 +Subproject commit 7a82390f42f017dc961afa1a2d0376896aac7829 diff --git a/scripts b/scripts index 0936c5369e3..5dc42f99852 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 0936c5369e389f36811d71094c3a0f0b4435637b +Subproject commit 5dc42f99852a295e03ca5fc538dd2b139c1e0453 From 6ec91c7e0724b3aa684b3b1118e2f491058b4f38 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 15:40:28 -0600 Subject: [PATCH 579/919] make itch downloader more permissive on matching this allows it to match adownloadable if there's crud after `windows` or `linux` (e.g. if a file is called `dwarf_fortress_53_04_windows(1).zip `) --- .github/workflows/generate-symbols.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index f78da19e748..18a99b9db8f 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -164,7 +164,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_linux.tar.bz2" + fname="dwarf_fortress_${minor}_${patch}_linux*.tar.bz2" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" tar xjf "kitfoxgames/dwarf-fortress/files/${fname}" -C DF_itch tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_itch @@ -272,7 +272,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_windows.zip" + fname="dwarf_fortress_${minor}_${patch}_windows*.zip" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" unzip -d DF_itch "kitfoxgames/dwarf-fortress/files/${fname}" xml/symbols_gen_windows.sh ${{ inputs.version }} ITCH DF_itch From a829c98a28207314f648c06ada0d4232864ea2a4 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:49:00 +0000 Subject: [PATCH 580/919] Auto-update structures ref for 53.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 07bd0e8e160..20b33a7d9e4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 07bd0e8e160bf5e4b62cd6dc3ba643fcd7dac836 +Subproject commit 20b33a7d9e4ec2224a6877ce3eb0a568299d9567 From 64df95f2dac24e30bdae7fd377e902ff5d1683d5 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:54:35 +0000 Subject: [PATCH 581/919] Auto-update structures ref for 53.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 20b33a7d9e4..ec38149428a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 20b33a7d9e4ec2224a6877ce3eb0a568299d9567 +Subproject commit ec38149428af0b5780c81a3fe77706b346c8c2d0 From 0701719515d6119bda78fb3e03c769a4fc8ad02f Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:58:04 +0000 Subject: [PATCH 582/919] Auto-update structures ref for 53.04 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ec38149428a..757f87b85df 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ec38149428af0b5780c81a3fe77706b346c8c2d0 +Subproject commit 757f87b85df2a22aa342a986b9609febd60ae734 From fb163fe3a3871d059b077151392a03fc64700e97 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 16:14:24 -0600 Subject: [PATCH 583/919] updates for 53.04-r1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 20 +++++++++++++++++++- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f14ed888788..4530bfb2f5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.03") +set(DF_VERSION "53.04") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index dfdce58d2b0..ac68d3b1df4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,14 +58,32 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.04-r1 + +## New Tools + +## New Features + ## Fixes - `buildingplan`: Bolt throwers will no longer be constructed using populated bins. - `RemoteFortressReader`: updated siege engine facing enums for new diagonal directions - `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform ## Misc Improvements -- `blueprint`: support for reinforced walls and bolt throwers - `autolabor`: support for new dying and siege-related labors +- `blueprint`: support for reinforced walls and bolt throwers ## Documentation diff --git a/library/xml b/library/xml index 757f87b85df..6c70c75ed08 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 757f87b85df2a22aa342a986b9609febd60ae734 +Subproject commit 6c70c75ed08975d963ecbb669d946f403831d3c9 diff --git a/plugins/stonesense b/plugins/stonesense index 7a82390f42f..9213b475463 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7a82390f42f017dc961afa1a2d0376896aac7829 +Subproject commit 9213b475463c39251e93a5d7c93d1b0f38a0f537 diff --git a/scripts b/scripts index 5dc42f99852..72f90ad5824 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 5dc42f99852a295e03ca5fc538dd2b139c1e0453 +Subproject commit 72f90ad5824fa532525ab123eda8fe91851178ff From f628ce8850547e6744a646392f0377240d4d5715 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:42:13 +0000 Subject: [PATCH 584/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 6c70c75ed08..48077a042df 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 6c70c75ed08975d963ecbb669d946f403831d3c9 +Subproject commit 48077a042dff14de22d682de3afc6d9157203c81 From 7f647fca3e34666c21b3a8130a8440f522e75726 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 18:46:11 -0600 Subject: [PATCH 585/919] 53.04-r1.1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4530bfb2f5a..787d58a75ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.04") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r1.1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index ac68d3b1df4..99903dadbbe 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,25 @@ Template for new versions: ## Removed +# 53.04-r1.1 + +## New Tools + +## New Features + +## Fixes +- fixed misalignment in ``widgets::unit_list`` + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.04-r1 ## New Tools From 7e865e9ff7939c7e2205b2943fa3955e8c129728 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 23:09:43 -0600 Subject: [PATCH 586/919] changes needed to implement unordered maps and sets see DFHack/df-structures#862 --- library/include/DataIdentity.h | 16 ++++++++- .../include/df/custom/hash/texture_fullid.h | 34 +++++++++++++++++++ .../df/custom/texture_fullid.methods.inc | 26 ++++++++++++++ library/modules/Gui.cpp | 7 ++-- library/xml | 2 +- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 library/include/df/custom/hash/texture_fullid.h create mode 100644 library/include/df/custom/texture_fullid.methods.inc diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 8a9e3c406b5..ed4ca06ecd5 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -29,9 +29,10 @@ distribution. #include #include #include +#include +#include #include #include -#include #include #include "DataDefs.h" @@ -748,6 +749,11 @@ namespace df static const container_identity *get(); }; + template struct identity_traits > + { + static const container_identity* get(); + }; + template<> struct identity_traits > { static const bit_array_identity identity; static const bit_container_identity *get() { return &identity; } @@ -831,6 +837,14 @@ namespace df return &identity; } + template + inline const container_identity* identity_traits >::get() + { + using container = std::set; + static const ro_stl_container_identity identity("unordered_set", identity_traits::get()); + return &identity; + } + template inline const container_identity *identity_traits>::get() { using container = std::map; diff --git a/library/include/df/custom/hash/texture_fullid.h b/library/include/df/custom/hash/texture_fullid.h new file mode 100644 index 00000000000..25eff39cf3c --- /dev/null +++ b/library/include/df/custom/hash/texture_fullid.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include "df/texture_fullid.h" + +template<> +struct std::hash +{ + size_t operator()(df::texture_fullid const &t) const noexcept + { +// for some reason, bay12 used different hash methods on windows vs linux +#ifdef WIN32 + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); + h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; + h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; + h^=std::hash{}(t.flag.whole); + return h; +#else + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(t.r); + h^=u_hash(t.g)<<1; + h^=u_hash(t.b)<<2; + h^=u_hash(t.br)<<3; + h^=u_hash(t.bg)<<4; + h^=u_hash(t.bb)<<5; + h^=std::hash{}(t.flag.whole)<<6; + return h; +#endif + } +}; \ No newline at end of file diff --git a/library/include/df/custom/texture_fullid.methods.inc b/library/include/df/custom/texture_fullid.methods.inc new file mode 100644 index 00000000000..ff57b2f4c5e --- /dev/null +++ b/library/include/df/custom/texture_fullid.methods.inc @@ -0,0 +1,26 @@ +auto operator== (const texture_fullid &other) const { + return ( + this->texpos == other.texpos && + this->r == other.r && + this->g == other.g && + this->b == other.b && + this->br == other.br && + this->bg == other.bg && + this->bb == other.bb && + this->flag.whole == other.flag.whole + ); +} + +auto operator< (const texture_fullid &other) const { + if (this->texpos < other.texpos) return true; + if (this->r < other.r) return true; + if (this->g < other.g) return true; + if (this->b < other.b) return true; + if (this->br < other.br) return true; + if (this->bg < other.bg) return true; + if (this->bb < other.bb) return true; + if (this->flag.whole < other.flag.whole) return true; + return false; +} + + diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 1e079da598c..fae55ce9d7a 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -2686,10 +2686,9 @@ void Gui::MTB_set_width(df::markup_text_boxst *mtb, int32_t n_width) df::widget * Gui::getWidget(df::widget_container *container, string name) { CHECK_NULL_POINTER(container); // ensure the compiler catches the change if we ever fix the template parameters - std::map & orig_field = container->children_by_name; - auto children_by_name = reinterpret_cast> *>(&orig_field); - if (children_by_name->contains(name)) - return (*children_by_name)[name].get(); + auto & children_by_name = container->children_by_name; + if (children_by_name.contains(name)) + return (children_by_name)[name].get(); return NULL; } diff --git a/library/xml b/library/xml index 48077a042df..461cd5a7e67 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 48077a042dff14de22d682de3afc6d9157203c81 +Subproject commit 461cd5a7e6773ea89e2795e6ef6cef1d3abdffc9 From c628dde44670d0a2948bfefab3a80e26cc70d9bd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 23:18:47 -0600 Subject: [PATCH 587/919] whitespacery --- .../include/df/custom/hash/texture_fullid.h | 38 +++++++++---------- .../df/custom/texture_fullid.methods.inc | 2 - 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/library/include/df/custom/hash/texture_fullid.h b/library/include/df/custom/hash/texture_fullid.h index 25eff39cf3c..a1e4606e33d 100644 --- a/library/include/df/custom/hash/texture_fullid.h +++ b/library/include/df/custom/hash/texture_fullid.h @@ -7,28 +7,28 @@ template<> struct std::hash { - size_t operator()(df::texture_fullid const &t) const noexcept + size_t operator()(df::texture_fullid const &t) const noexcept { // for some reason, bay12 used different hash methods on windows vs linux #ifdef WIN32 - size_t h=std::hash{}(t.texpos); - auto u_hash=std::hash{}; - h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); - h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; - h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; - h^=std::hash{}(t.flag.whole); - return h; + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); + h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; + h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; + h^=std::hash{}(t.flag.whole); + return h; #else - size_t h=std::hash{}(t.texpos); - auto u_hash=std::hash{}; - h^=u_hash(t.r); - h^=u_hash(t.g)<<1; - h^=u_hash(t.b)<<2; - h^=u_hash(t.br)<<3; - h^=u_hash(t.bg)<<4; - h^=u_hash(t.bb)<<5; - h^=std::hash{}(t.flag.whole)<<6; - return h; + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(t.r); + h^=u_hash(t.g)<<1; + h^=u_hash(t.b)<<2; + h^=u_hash(t.br)<<3; + h^=u_hash(t.bg)<<4; + h^=u_hash(t.bb)<<5; + h^=std::hash{}(t.flag.whole)<<6; + return h; #endif } -}; \ No newline at end of file +}; diff --git a/library/include/df/custom/texture_fullid.methods.inc b/library/include/df/custom/texture_fullid.methods.inc index ff57b2f4c5e..12e21f67357 100644 --- a/library/include/df/custom/texture_fullid.methods.inc +++ b/library/include/df/custom/texture_fullid.methods.inc @@ -22,5 +22,3 @@ auto operator< (const texture_fullid &other) const { if (this->flag.whole < other.flag.whole) return true; return false; } - - From 353f16395a627133b0a3c8a7942fe87cb2fef2eb Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 17 Nov 2025 23:31:20 -0600 Subject: [PATCH 588/919] Update xml --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 461cd5a7e67..00e1c5ea007 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 461cd5a7e6773ea89e2795e6ef6cef1d3abdffc9 +Subproject commit 00e1c5ea0071b716afad5a4140dc43e0ed7a8fbb From ade165ada438092b89c77040f02495304445de00 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 18 Nov 2025 07:43:06 +0000 Subject: [PATCH 589/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 00e1c5ea007..40c9ae5c13b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 00e1c5ea0071b716afad5a4140dc43e0ed7a8fbb +Subproject commit 40c9ae5c13bc0b63dfb3cbf36efb95d3b20faabe From b033dc13d907c7bb1cae59cdd25afef0ace9a499 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:32:13 +0000 Subject: [PATCH 590/919] Auto-update structures ref for 53.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 40c9ae5c13b..f8f7002cea6 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 40c9ae5c13bc0b63dfb3cbf36efb95d3b20faabe +Subproject commit f8f7002cea696ebab9ee1c83d2da23150e6cc93c From 61b725faebe2aa78065baff7cbad582b69b5aed5 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:34:53 +0000 Subject: [PATCH 591/919] Auto-update structures ref for 53.05 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f8f7002cea6..2bd33eba052 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f8f7002cea696ebab9ee1c83d2da23150e6cc93c +Subproject commit 2bd33eba052f6ea3f65f4a22670029f2311b07b4 From bd1b453a05c7fc4cdc3ab3b6399a3ac91deb92b4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 07:59:08 -0600 Subject: [PATCH 592/919] changes for 53.05-r1 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 787d58a75ad..4824ad4ed1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.04") -set(DFHACK_RELEASE "r1.1") +set(DF_VERSION "53.05") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index 99903dadbbe..13d6f1bc4dd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,24 @@ Template for new versions: ## Removed +# 53.05-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.04-r1.1 ## New Tools From dca7d8d48bfd28d6293f253e48ffe24836467853 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 08:10:42 -0600 Subject: [PATCH 593/919] changes for 53.05-r1 --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 13d6f1bc4dd..8551676b26b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -75,6 +75,7 @@ Template for new versions: ## New Tools ## New Features +- compatibility with 53.05 ## Fixes From 04cd9718235af5e3f2538c4c415881041f54dcb1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 10:55:08 -0600 Subject: [PATCH 594/919] update `sort` for changes in df `unit_list` --- library/include/DataIdentity.h | 2 +- plugins/sort.cpp | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index ed4ca06ecd5..4bd49762501 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -840,7 +840,7 @@ namespace df template inline const container_identity* identity_traits >::get() { - using container = std::set; + using container = std::unordered_set; static const ro_stl_container_identity identity("unordered_set", identity_traits::get()); return &identity; } diff --git a/plugins/sort.cpp b/plugins/sort.cpp index add54688c49..9b91f1118c2 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -25,7 +27,7 @@ namespace DFHack { DBG_DECLARE(sort, log, DebugCategory::LINFO); } -using item_or_unit = std::pair; +using item_or_unit = std::variant; using filter_vec_type = std::vector>; // recreated here since our autogenerated df::sort_entry lacks template params @@ -44,8 +46,8 @@ static bool probing = false; static bool probe_result = false; static bool do_filter(const char *module_name, const char *fn_name, const item_or_unit &elem) { - if (elem.second) return true; - auto unit = (df::unit *)elem.first; + if (std::holds_alternative(elem)) return true; + auto unit = std::get(elem); if (probing) { TRACE(log).print("probe successful\n"); @@ -78,11 +80,11 @@ static bool do_work_animal_assignment_filter(item_or_unit elem) { static int32_t our_filter_idx(df::widget_unit_list *unitlist) { if (world->units.active.empty()) - return true; + return -1; df::unit *u = world->units.active[0]; // any unit will do; we just need a sentinel if (!u) - return true; + return -1; probing = true; probe_result = false; @@ -91,8 +93,8 @@ static int32_t our_filter_idx(df::widget_unit_list *unitlist) { filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); TRACE(log).print("probing for our filter function\n"); - for (auto fn : *filter_vec) { - fn(std::make_pair(u, false)); + for (auto& fn : *filter_vec) { + fn(item_or_unit(u)); if (probe_result) { TRACE(log).print("found our filter function at idx %d\n", idx); break; @@ -144,13 +146,13 @@ static df::widget_unit_list * get_work_animal_assignment_unit_list() { // static bool sort_proxy(const item_or_unit &a, const item_or_unit &b) { - if (a.second || b.second) + if (std::holds_alternative(a) || std::holds_alternative(b)) return true; bool ret = true; color_ostream &out = Core::getInstance().getConsole(); Lua::CallLuaModuleFunction(out, "plugins.sort", "do_sort", - std::make_tuple((df::unit *)a.first, (df::unit *)b.first), + std::make_tuple(std::get(a), std::get(b)), 1, [&](lua_State *L){ ret = lua_toboolean(L, 1); } From ab4bb52ebdcab82f876d82a7e2132b4c29aac9e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 11:37:57 -0600 Subject: [PATCH 595/919] enable msvc c4062 fixes #5641 --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4824ad4ed1d..a9bb70aa153 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,9 @@ if(MSVC) # Enable C5038 - This is equivalent to gcc's -Werror=reorder, which is enabled by default by gcc -Wall add_compile_options("/w15038") + # Enable C4062 - Warns about missing enum case in switch statement, equivalent to gcc -Wswitch + add_compile_options("/w14062") + # MSVC panics if an object file contains more than 65,279 sections. this # happens quite frequently with code that uses templates, such as vectors. add_compile_options("/bigobj") From 6ca9bab763b7eed4582b0ffd2f6fa30b7ac4cabf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 18 Nov 2025 17:19:47 -0600 Subject: [PATCH 596/919] `sort`: make removal of our filter functions more reliable avoids a crash on df exit caused by a dangled pointer left in the df data structure Co-Authored-By: Nicholas McDaniel --- docs/changelog.txt | 1 + plugins/sort.cpp | 67 +++++++++++++++++++--------------------------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8551676b26b..46f1a718e69 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements diff --git a/plugins/sort.cpp b/plugins/sort.cpp index 9b91f1118c2..7706750fefd 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -1,4 +1,5 @@ #include +#include #include "Debug.h" #include "LuaTools.h" @@ -13,6 +14,7 @@ #include "df/widget_unit_list.h" #include "df/world.h" + using std::vector; using std::string; @@ -28,7 +30,8 @@ namespace DFHack { } using item_or_unit = std::variant; -using filter_vec_type = std::vector>; +using filter_func = bool(item_or_unit); +using filter_vec_type = std::vector>; // recreated here since our autogenerated df::sort_entry lacks template params struct sort_entry { @@ -42,19 +45,10 @@ static const string DFHACK_SORT_IDENT = "dfhack_sort"; // filter logic // -static bool probing = false; -static bool probe_result = false; - static bool do_filter(const char *module_name, const char *fn_name, const item_or_unit &elem) { if (std::holds_alternative(elem)) return true; auto unit = std::get(elem); - if (probing) { - TRACE(log).print("probe successful\n"); - probe_result = true; - return false; - } - bool ret = true; color_ostream &out = Core::getInstance().getConsole(); Lua::CallLuaModuleFunction(out, module_name, fn_name, std::make_tuple(unit), @@ -78,35 +72,28 @@ static bool do_work_animal_assignment_filter(item_or_unit elem) { return do_filter("plugins.sort.info", "do_work_animal_assignment_filter", elem); } -static int32_t our_filter_idx(df::widget_unit_list *unitlist) { - if (world->units.active.empty()) - return -1; - - df::unit *u = world->units.active[0]; // any unit will do; we just need a sentinel - if (!u) - return -1; - - probing = true; - probe_result = false; +static int32_t our_filter_idx(filter_func* filter, df::widget_unit_list* unitlist) +{ int32_t idx = 0; - filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); + filter_vec_type* filter_vec = reinterpret_cast(&unitlist->filter_func); TRACE(log).print("probing for our filter function\n"); - for (auto& fn : *filter_vec) { - fn(item_or_unit(u)); - if (probe_result) { + + for (auto& fn : *filter_vec) + { + auto t = fn.target(); + if (t && *t == filter) + { TRACE(log).print("found our filter function at idx %d\n", idx); - break; + return idx; } ++idx; } - - probing = false; - return probe_result ? idx : -1; + return -1; } -static df::widget_unit_list * get_squad_unit_list() { +static df::widget_unit_list* get_squad_unit_list() { return virtual_cast( Gui::getWidget(&game->main_interface.unit_selector, "Unit selector")); } @@ -182,8 +169,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector= 0) { DEBUG(log,out).print("removing %s filter function\n", which); filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); @@ -202,22 +189,22 @@ static void remove_sort_function(color_ostream &out, const char *which, df::widg DFhackCExport command_result plugin_shutdown(color_ostream &out) { if (auto unitlist = get_squad_unit_list()) { - remove_filter_function(out, "squad", unitlist); + remove_filter_function(out, do_squad_filter, "squad", unitlist); remove_sort_function(out, "squad", unitlist); } if (auto unitlist = get_interrogate_unit_list("Open cases")) - remove_filter_function(out, "open cases interrogate", unitlist); + remove_filter_function(out, do_justice_filter, "open cases interrogate", unitlist); if (auto unitlist = get_interrogate_unit_list("Cold cases")) - remove_filter_function(out, "cold cases interrogate", unitlist); + remove_filter_function(out, do_justice_filter, "cold cases interrogate", unitlist); if (auto unitlist = get_convict_unit_list("Open cases")) - remove_filter_function(out, "open cases convict", unitlist); + remove_filter_function(out, do_justice_filter, "open cases convict", unitlist); if (auto unitlist = get_convict_unit_list("Cold cases")) - remove_filter_function(out, "cold cases convict", unitlist); + remove_filter_function(out, do_justice_filter, "cold cases convict", unitlist); if (auto unitlist = get_work_animal_assignment_unit_list()) - remove_filter_function(out, "work animal assignment", unitlist); + remove_filter_function(out, do_work_animal_assignment_filter, "work animal assignment", unitlist); return CR_OK; } @@ -228,7 +215,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { static void sort_set_squad_filter_fn(color_ostream &out) { auto unitlist = get_squad_unit_list(); - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_squad_filter, unitlist) == -1) { DEBUG(log).print("adding squad filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_squad_filter); @@ -240,7 +227,7 @@ static void sort_set_squad_filter_fn(color_ostream &out) { } static void sort_set_justice_filter_fn(color_ostream &out, df::widget_unit_list *unitlist) { - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_justice_filter, unitlist) == -1) { DEBUG(log).print("adding justice filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_justice_filter); @@ -249,7 +236,7 @@ static void sort_set_justice_filter_fn(color_ostream &out, df::widget_unit_list } static void sort_set_work_animal_assignment_filter_fn(color_ostream &out, df::widget_unit_list *unitlist) { - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_work_animal_assignment_filter, unitlist) == -1) { DEBUG(log).print("adding work animal assignment filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_work_animal_assignment_filter); From 7cd54dd3cca3d5edcc0a2078f6db63b1a3dea666 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 19 Nov 2025 19:08:09 -0600 Subject: [PATCH 597/919] rewrite `BitArray` to be a little less crusty and no malloc/free/realloc neither --- library/include/BitArray.h | 216 ++++++++++++++++++++------------- library/include/DataDefs.h | 2 +- library/include/DataIdentity.h | 2 +- library/include/RemoteTools.h | 2 +- 4 files changed, 135 insertions(+), 87 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 15ec84a62f6..dae604f659f 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -27,60 +27,100 @@ distribution. #include #include #include + +#include #include #include + namespace DFHack { template class BitArray { + private: + // note that these are mandated by the implementation of flagarrayst in DF code, and must be exactly as below + using buffer_type = unsigned char; + using size_type = int32_t; + + buffer_type* _bits; + size_type _size; + + void resize(size_type newsize, const BitArray* replacement) + { + if (newsize == _size) + return; + + if (newsize == 0) + { + delete[] _bits; + _bits = nullptr; + _size = 0; + return; + } + + buffer_type* old_data = _bits; + + _bits = new buffer_type[newsize]; + + buffer_type* copysrc = replacement ? replacement->_bits : old_data; + size_type copysize = replacement ? replacement->_size : _size; + + if (copysrc) + std::memcpy(_bits, copysrc, std::min(copysize, newsize)); + + if (newsize > _size) + std::memset(_bits + _size, 0, newsize - _size); + + delete[] old_data; + + _size = newsize; + } + + void extend(T index) + { + size_type newsize = (index / 8) + 1; + if (newsize > _size) + resize(newsize); + } + public: - BitArray() : bits(NULL), size(0) {} - BitArray(const BitArray &other) : bits(NULL), size(0) + BitArray() : _bits(nullptr), _size(0) {} + BitArray(const BitArray &other) : _bits(nullptr), _size(0) { - *this = other; + resize(other._size, &other); } ~BitArray() { - free(bits); + delete [] _bits; } - explicit BitArray(T last) : bits(NULL), size(0) { + explicit BitArray(T last) : _bits(nullptr), _size(0) { extend(last); } - explicit BitArray(unsigned bytes) : bits(NULL), size(0) { + explicit BitArray(unsigned bytes) : _bits(nullptr), _size(0) { resize(bytes); } - void clear_all ( void ) + size_type size() const { return _size; } + buffer_type* bits() const { return _bits; } + + void resize( size_type newsize ) { - if(bits) - memset(bits, 0, size); + resize(newsize, nullptr); } - void resize (unsigned newsize) + + void clear_all ( void ) { - if (newsize == size) - return; - uint8_t* mem = (uint8_t *) realloc(bits, newsize); - if(!mem && newsize != 0) - throw std::bad_alloc(); - bits = mem; - if (newsize > size) - memset(bits+size, 0, newsize-size); - size = newsize; - } - BitArray &operator= (const BitArray &other) - { - resize(other.size); - memcpy(bits, other.bits, size); - return *this; + if(_bits) + memset(_bits, 0, _size); } - void extend (T index) + + BitArray& operator= (const BitArray& other) { - unsigned newsize = (index / 8) + 1; - if (newsize > size) - resize(newsize); + resize(other._size, &other); + return *this; } + void set (T index, bool value = true) { if(!value) @@ -88,71 +128,74 @@ namespace DFHack clear(index); return; } - uint32_t byte = index / 8; + size_type byte = index / 8; extend(index); - //if(byte < size) - { - uint8_t bit = 1 << (index % 8); - bits[byte] |= bit; - } + uint8_t bit = 1 << (index % 8); + _bits[byte] |= bit; } + void clear (T index) { - uint32_t byte = index / 8; - if(byte < size) + size_type byte = index / 8; + if(byte < _size) { uint8_t bit = 1 << (index % 8); - bits[byte] &= ~bit; + _bits[byte] &= ~bit; } } + void toggle (T index) { - uint32_t byte = index / 8; + size_type byte = index / 8; extend(index); - //if(byte < size) - { - uint8_t bit = 1 << (index % 8); - bits[byte] ^= bit; - } + uint8_t bit = 1 << (index % 8); + _bits[byte] ^= bit; } + bool is_set (T index) const { - uint32_t byte = index / 8; - if(byte < size) + size_type byte = index / 8; + if(byte < _size) { uint8_t bit = 1 << (index % 8); - return bit & bits[byte]; + return bit & _bits[byte]; } else return false; } + /// WARNING: this can truncate long bit arrays - uint32_t as_int () + template + I as_int () const { - if(!bits) + if(!_bits) return 0; - if(size >= 4) - return *(uint32_t *)bits; - uint32_t target = 0; - memcpy (&target, bits,size); + if (_size >= sizeof(I)) + // FIXME (C++23): should be std::start_lifetime_as + return *reinterpret_cast(_bits); + I target = 0; + std::memcpy(&target, _bits, _size); return target; } + /// WARNING: this can be truncated / only overwrite part of the data - bool operator =(uint32_t data) + template + bool operator =(I data) { - if(!bits) + if(!_bits) return false; - if (size >= 4) + if (_size >= sizeof(I)) { - (*(uint32_t *)bits) = data; + *reinterpret_cast(_bits) = data; return true; } - memcpy(bits, &data, size); + std::memcpy(_bits, &data, _size); return true; } + friend std::ostream& operator<< (std::ostream &out, BitArray &ba) { std::stringstream sstr; - for (int i = 0; i < ba.size * 8; i++) + for (int i = 0; i < ba._size * 8; i++) { if(ba.is_set((T)i)) sstr << "1 "; @@ -162,23 +205,44 @@ namespace DFHack out << sstr.str(); return out; } - uint8_t * bits; - uint32_t size; }; template class DfArray { + private: T *m_data; unsigned short m_size; + + void resize(unsigned new_size, const DfArray* replacement) + { + if (new_size == m_size) + return; + + T* old_data = m_data; + + m_data = (T*) new T[new_size]; + + T* copysrc = replacement ? replacement->m_data : m_data; + unsigned short copysize = replacement ? replacement->m_size : m_size; + + if (copysrc) + std::memcpy(m_data, copysrc, sizeof(T) * copysize); + + if (new_size > m_size) + std::memset(m_data + m_size, 0, sizeof(T) * (new_size - m_size)); + + delete[] old_data; + + m_size = new_size; + } public: - DfArray() : m_data(NULL), m_size(0) {} - ~DfArray() { free(m_data); } + DfArray() : m_data(nullptr), m_size(0) {} + ~DfArray() { delete[] m_data; } - DfArray(const DfArray &other) : m_data(NULL), m_size(0) + DfArray(const DfArray &other) : m_data(nullptr), m_size(0) { - resize(other.m_size); - memcpy(m_data, other.m_data,m_size*sizeof(T)); + resize(other.m_size, &other); } typedef T value_type; @@ -195,28 +259,12 @@ namespace DFHack void resize(unsigned new_size) { - if (new_size == m_size) - return; - if(!m_data) - { - m_data = (T*) malloc(sizeof(T)*new_size); - } - else - { - T* mem = (T*) realloc(m_data, sizeof(T)*new_size); - if(!mem && new_size != 0) - throw std::bad_alloc(); - m_data = mem; - } - if (new_size > m_size) - memset(m_data+sizeof(T)*m_size, 0, sizeof(T)*(new_size - m_size)); - m_size = new_size; + resize(new_size, nullptr); } DfArray &operator= (const DfArray &other) { - resize(other.size()); - memcpy(data(), other.data(), sizeof(T)*size()); + resize(other.size(), &other); return *this; } diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 74ffbe251ae..9ce2cd4dacf 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -948,7 +948,7 @@ namespace DFHack { inline void flagarray_to_string(std::vector *pvec, const BitArray &val) { typedef df::enum_traits traits; int size = traits::last_item_value-traits::first_item_value+1; - flagarrayToString(pvec, val.bits, val.size, + flagarrayToString(pvec, val.bits(), val.size(), (int)traits::first_item_value, size, traits::key_table); } diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 4bd49762501..f8fd3c6fd7f 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -523,7 +523,7 @@ namespace df protected: virtual int item_count(void *ptr, CountMode cnt) const { - return cnt == COUNT_LEN ? ((container*)ptr)->size * 8 : -1; + return cnt == COUNT_LEN ? ((container*)ptr)->size() * 8 : -1; } virtual bool get_item(void *ptr, int idx) const { return ((container*)ptr)->is_set(idx); diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index 38c234c5fbc..f041c5d7b8c 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -74,7 +74,7 @@ namespace DFHack */ template void flagarray_to_ints(RepeatedField *pf, const BitArray &val) { - for (size_t i = 0; i < val.size*8; i++) + for (size_t i = 0; i < val.size()*8; i++) if (val.is_set(T(i))) pf->Add(i); } From 6d21dcc379d1286f02f2858086a39197e841a702 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 19 Nov 2025 19:11:43 -0600 Subject: [PATCH 598/919] add missing header --- library/include/BitArray.h | 1 + 1 file changed, 1 insertion(+) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index dae604f659f..19b226f7a10 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -29,6 +29,7 @@ distribution. #include #include +#include #include #include From 207e443b82d3161cd4d8e9e4961dd50ae215a5ac Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 20:52:33 -0500 Subject: [PATCH 599/919] Refactor hotkey code into a new Hotkey module --- library/CMakeLists.txt | 2 + library/Core.cpp | 333 ++---------------- library/include/Core.h | 39 +- library/include/modules/DFSDL.h | 4 + library/include/modules/Hotkey.h | 57 +++ library/modules/DFSDL.cpp | 18 + library/modules/Hotkey.cpp | 300 ++++++++++++++++ plugins/blueprint.cpp | 3 +- plugins/embark-assistant/embark-assistant.cpp | 3 +- plugins/hotkeys.cpp | 68 ++-- 10 files changed, 442 insertions(+), 385 deletions(-) create mode 100644 library/include/modules/Hotkey.h create mode 100644 library/modules/Hotkey.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf62..6f91c6c08ba 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -156,6 +156,7 @@ set(MODULE_HEADERS include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h + include/modules/Hotkey.h include/modules/Items.h include/modules/Job.h include/modules/Kitchen.h @@ -186,6 +187,7 @@ set(MODULE_SOURCES modules/Filesystem.cpp modules/Graphic.cpp modules/Gui.cpp + modules/Hotkey.cpp modules/Items.cpp modules/Job.cpp modules/Kitchen.cpp diff --git a/library/Core.cpp b/library/Core.cpp index 33f66d767eb..6cc60aaf85b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -47,6 +47,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Textures.h" #include "modules/World.h" #include "modules/Persistence.h" @@ -104,11 +105,9 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { - DBG_DECLARE(core, keybinding, DebugCategory::LINFO); DBG_DECLARE(core, script, DebugCategory::LINFO); @@ -274,34 +273,6 @@ struct IODATA PluginManager * plug_mgr; }; -// A thread function... for handling hotkeys. This is needed because -// all the plugin commands are expected to be run from foreign threads. -// Running them from one of the main DF threads will result in deadlock! -static void fHKthread(IODATA * iodata) -{ - Core * core = iodata->core; - PluginManager * plug_mgr = iodata->plug_mgr; - if(!plug_mgr || !core) - { - std::cerr << "Hotkey thread has croaked." << std::endl; - return; - } - bool keep_going = true; - while(keep_going) - { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! - if(!stuff.empty()) - { - color_ostream_proxy out(core->getConsole()); - - auto rv = core->runCommand(out, stuff); - - if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); - } - } -} - static std::string dfhack_version_desc() { std::stringstream s; @@ -990,11 +961,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { std::string keystr = parts[1]; if (parts[0] == "set") - ClearKeyBindings(keystr); + hotkey_mgr->clearKeybind(keystr); // for (int i = parts.size()-1; i >= 2; i--) for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!AddKeyBinding(keystr, part)) { + if (!hotkey_mgr->addKeybind(keystr, part)) { con.printerr("Invalid key spec: %s\n", keystr.c_str()); break; } @@ -1005,7 +976,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s // for (size_t i = 1; i < parts.size(); i++) for (const auto& part : parts | std::views::drop(1)) { - if (!ClearKeyBindings(part)) { + if (!hotkey_mgr->clearKeybind(part)) { con.printerr("Invalid key spec: %s\n", part.c_str()); break; } @@ -1013,7 +984,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = ListKeyBindings(parts[1]); + std::vector list = hotkey_mgr->listKeybinds(parts[1]); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) @@ -1471,8 +1442,7 @@ Core::~Core() Core::Core() : d(std::make_unique()), script_path_mutex{}, - HotkeyMutex{}, - HotkeyCond{}, + armok_mutex{}, alias_mutex{}, started{false}, CoreSuspendMutex{}, @@ -1488,7 +1458,6 @@ Core::Core() : // set up hotkey capture suppress_duplicate_keyboard_events = true; - hotkey_set = NO; last_world_data_ptr = nullptr; last_local_map_ptr = nullptr; last_pause_state = false; @@ -1841,6 +1810,8 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); + this->hotkey_mgr = new HotkeyManager(); + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1858,8 +1829,6 @@ bool Core::InitSimulationThread() } std::cerr << "Starting DF input capture thread.\n"; - // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, temp); started = true; modstate = 0; @@ -1933,31 +1902,6 @@ bool Core::InitSimulationThread() return true; } -/// sets the current hotkey command -bool Core::setHotkeyCmd( std::string cmd ) -{ - // access command - std::lock_guard lock(HotkeyMutex); - hotkey_set = SET; - hotkey_cmd = std::move(cmd); - HotkeyCond.notify_all(); - return true; -} -/// removes the hotkey command and gives it to the caller thread -std::string Core::getHotkeyCmd( bool &keep_going ) -{ - std::string returner; - std::unique_lock lock(HotkeyMutex); - HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); - if (hotkey_set == SHUTDOWN) { - keep_going = false; - return returner; - } - hotkey_set = NO; - returner = hotkey_cmd; - hotkey_cmd.clear(); - return returner; -} void Core::print(const char *format, ...) { @@ -2405,17 +2349,14 @@ int Core::Shutdown ( void ) con.shutdown(); } - if (d->hotkeythread.joinable()) { - std::unique_lock hot_lock(HotkeyMutex); - hotkey_set = SHUTDOWN; - HotkeyCond.notify_one(); - } - ServerMain::block(); - d->hotkeythread.join(); d->iothread.join(); + if (hotkey_mgr) { + delete hotkey_mgr; + } + if(plug_mgr) { delete plug_mgr; @@ -2490,16 +2431,24 @@ void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { } void Core::setMortalMode(bool value) { - std::lock_guard lock(HotkeyMutex); - mortal_mode = value; + mortal_mode.exchange(value); +} + +bool Core::getMortalMode() { + return mortal_mode.load(); } void Core::setArmokTools(const std::vector &tool_names) { - std::lock_guard lock(HotkeyMutex); + std::lock_guard lock(armok_mutex); armok_tools.clear(); armok_tools.insert(tool_names.begin(), tool_names.end()); } +bool Core::isArmokTool(const std::string& tool) { + std::lock_guard lock(armok_mutex); + return armok_tools.contains(tool); +} + // returns true if the event is handled bool Core::DFH_SDL_Event(SDL_Event* ev) { uint32_t start_ms = p->getTickCount(); @@ -2540,7 +2489,7 @@ bool Core::doSdlInputEvent(SDL_Event* ev) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); - if (SelectHotkey(sym, modstate)) { + if (hotkey_mgr->handleKeybind(sym, modstate)) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); @@ -2561,8 +2510,8 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { - SDL_Keycode sym = SDLK_F13 + but.button - 4; - if (sym <= SDLK_F24 && SelectHotkey(sym, modstate)) + SDL_Keycode sym = -but.button; + if (sym >= -15 && sym <= -1 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { @@ -2582,236 +2531,6 @@ bool Core::doSdlInputEvent(SDL_Event* ev) return false; } -bool Core::SelectHotkey(int sym, int modifiers) -{ - // Find the topmost viewscreen - if (!df::global::gview || !df::global::plotinfo) - return false; - - df::viewscreen *screen = &df::global::gview->view; - while (screen->child) - screen = screen->child; - - if (sym == SDLK_KP_ENTER) - sym = SDLK_RETURN; - - std::string cmd; - - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); - - { - std::lock_guard lock(HotkeyMutex); - - // Check the internal keybindings - std::vector &bindings = key_bindings[sym]; - //for (int i = bindings.size()-1; i >= 0; --i) { - for (const auto& binding : bindings | std::views::reverse) { - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); - - if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", - binding.modifiers, modifiers); - continue; - } - if (!binding.focus.empty()) { - if (!Gui::matchFocusString(binding.focus)) { - std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); - continue; - } - } - if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - - cmd = binding.cmdline; - DEBUG(keybinding).print("matched hotkey\n"); - break; - } - - if (cmd.empty()) { - // Check the hotkey keybindings - int idx = sym - SDLK_F1; - if(idx >= 0 && idx < 8) - { -/* TODO: understand how this changes for v50 - if (modifiers & 1) - idx += 8; - - if (strict_virtual_cast(screen) && - df::global::plotinfo->main.mode != ui_sidebar_mode::Hotkeys && - df::global::plotinfo->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) - { - cmd = df::global::plotinfo->main.hotkeys[idx].name; - } -*/ - } - } - } - - if (!cmd.empty()) { - setHotkeyCmd(cmd); - return true; - } - - return false; -} - -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) -{ - *pmod = 0; - - if (pfocus) - { - *pfocus = ""; - - size_t idx = keyspec.find('@'); - if (idx != std::string::npos) - { - *pfocus = keyspec.substr(idx+1); - keyspec = keyspec.substr(0, idx); - } - } - - // ugh, ugly - for (;;) { - if (keyspec.size() > 6 && keyspec.starts_with("Shift-")) { - *pmod |= 1; - keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.starts_with("Ctrl-")) { - *pmod |= 2; - keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.starts_with("Alt-")) { - *pmod |= 4; - keyspec = keyspec.substr(4); - } else - break; - } - - if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { - *psym = SDLK_a + (keyspec[0]-'A'); - return true; - } else if (keyspec.size() == 1 && keyspec[0] == '`') { - *psym = SDLK_BACKQUOTE; - return true; - } else if (keyspec.size() == 1 && keyspec[0] >= '0' && keyspec[0] <= '9') { - *psym = SDLK_0 + (keyspec[0]-'0'); - return true; - } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { - *psym = SDLK_F1 + (keyspec[1]-'1'); - return true; - } else if (keyspec.size() == 3 && keyspec.starts_with("F1") && keyspec[2] >= '0' && keyspec[2] <= '2') { - *psym = SDLK_F10 + (keyspec[2]-'0'); - return true; - } else if (keyspec.size() == 6 && keyspec.starts_with("MOUSE") && keyspec[5] >= '4' && keyspec[5] <= '9') { - *psym = SDLK_F13 + (keyspec[5]-'4'); - return true; - } else if (keyspec.size() == 7 && keyspec.starts_with("MOUSE1") && keyspec[5] >= '0' && keyspec[5] <= '5') { - *psym = SDLK_F19 + (keyspec[5]-'0'); - return true; - } else if (keyspec == "Enter") { - *psym = SDLK_RETURN; - return true; - } else - return false; -} - -bool Core::ClearKeyBindings(std::string keyspec) -{ - int sym, mod; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return false; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == mod && prefix_matches(focus, bindings[i].focus)) - bindings.erase(bindings.begin()+i); - } - - return true; -} - -bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) -{ - size_t at_pos = keyspec.find('@'); - if (at_pos != std::string::npos) - { - std::string raw_spec = keyspec.substr(0, at_pos); - std::string raw_focus = keyspec.substr(at_pos + 1); - if (raw_focus.find('|') != std::string::npos) - { - std::vector focus_strings; - split_string(&focus_strings, raw_focus, "|"); - for (const auto& fs : focus_strings) - { - if (!AddKeyBinding(raw_spec + "@" + fs, cmdline)) - return false; - } - return true; - } - } - int sym; - KeyBinding binding; - if (!parseKeySpec(keyspec, &sym, &binding.modifiers, &binding.focus)) - return false; - - cheap_tokenise(cmdline, binding.command); - if (binding.command.empty()) - return false; - - std::lock_guard lock(HotkeyMutex); - - // Don't add duplicates - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == binding.modifiers && - bindings[i].cmdline == cmdline && - bindings[i].focus == binding.focus) - return true; - } - - binding.cmdline = cmdline; - bindings.push_back(binding); - return true; -} - -std::vector Core::ListKeyBindings(std::string keyspec) -{ - int sym, mod; - std::vector rv; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return rv; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (focus.size() && focus != bindings[i].focus) - continue; - if (bindings[i].modifiers == mod) - { - std::string cmd = bindings[i].cmdline; - if (!bindings[i].focus.empty()) - cmd = "@" + bindings[i].focus + ": " + cmd; - rv.push_back(cmd); - } - } - - return rv; -} - bool Core::AddAlias(const std::string &name, const std::vector &command, bool replace) { std::lock_guard lock(alias_mutex); diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef12..08c7ca3d2e5 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -68,6 +68,7 @@ namespace DFHack class Core; class ServerMain; class CoreSuspender; + class HotkeyManager; namespace Lua { namespace Core { DFHACK_EXPORT void Reset(color_ostream &out, const char *where); @@ -164,10 +165,6 @@ namespace DFHack Materials * getMaterials(); /// get the graphic module Graphic * getGraphic(); - /// sets the current hotkey command - bool setHotkeyCmd( std::string cmd ); - /// removes the hotkey command and gives it to the caller thread - std::string getHotkeyCmd( bool &keep_going ); command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream &out, const std::string &command); @@ -182,11 +179,10 @@ namespace DFHack bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); + bool getMortalMode(); void setArmokTools(const std::vector &tool_names); + bool isArmokTool(const std::string& name); - bool ClearKeyBindings(std::string keyspec); - bool AddKeyBinding(std::string keyspec, std::string cmdline); - std::vector ListKeyBindings(std::string keyspec); int8_t getModstate() { return modstate; } bool AddAlias(const std::string &name, const std::vector &command, bool replace = false); @@ -213,6 +209,7 @@ namespace DFHack static void printerr(const char *format, ...) Wformat(printf,1,2); PluginManager *getPluginManager() { return plug_mgr; } + HotkeyManager *getHotkeyManager() { return hotkey_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -267,39 +264,25 @@ namespace DFHack Graphic * pGraphic; } s_mods; std::vector> allModules; - DFHack::PluginManager * plug_mgr; + DFHack::PluginManager *plug_mgr; + // Hotkey Manager + DFHack::HotkeyManager *hotkey_mgr; + + // FIXME: remove all this junk (hotkey related) std::vector script_paths[3]; std::mutex script_path_mutex; - // hotkey-related stuff - struct KeyBinding { - int modifiers; - std::vector command; - std::string cmdline; - std::string focus; - }; int8_t modstate; bool suppress_duplicate_keyboard_events; - bool mortal_mode; + std::atomic mortal_mode; std::unordered_set armok_tools; - std::map > key_bindings; - std::string hotkey_cmd; - enum hotkey_set_t { - NO, - SET, - SHUTDOWN, - }; - hotkey_set_t hotkey_set; - std::mutex HotkeyMutex; - std::condition_variable HotkeyCond; + std::mutex armok_mutex; std::map> aliases; std::recursive_mutex alias_mutex; - bool SelectHotkey(int key, int modifiers); - // for state change tracking df::world_data *last_world_data_ptr; // for state change tracking diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index ff8e81ab2c8..161cc351710 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -4,12 +4,14 @@ #include "ColorText.h" #include +#include struct SDL_Surface; struct SDL_Rect; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; +typedef int32_t SDL_Keycode; namespace DFHack { @@ -63,6 +65,8 @@ namespace DFHack::DFSDL DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); DFHACK_EXPORT char* DFSDL_GetBasePath(); + DFHACK_EXPORT SDL_Keycode DFSDL_GetKeyFromName(const char* name); + DFHACK_EXPORT const char* DFSDL_GetKeyName(SDL_Keycode key); } namespace DFHack diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h new file mode 100644 index 00000000000..581ebe43e08 --- /dev/null +++ b/library/include/modules/Hotkey.h @@ -0,0 +1,57 @@ +#pragma once + +#include "Export.h" + +#include +#include +#include +#include +#include + +namespace DFHack { + class DFHACK_EXPORT HotkeyManager { + public: + HotkeyManager(); + ~HotkeyManager(); + + struct KeySpec { + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + }; + + struct KeyBinding { + HotkeyManager::KeySpec spec; + std::string command; + std::string cmdline; + }; + + bool addKeybind(std::string keyspec, std::string cmd); + bool addKeybind(KeySpec spec, std::string cmd); + bool clearKeybind(std::string keyspec, bool any_focus=false); + bool clearKeybind(const KeySpec& spec, bool any_focus=false); + + std::vector listKeybinds(std::string keyspec); + std::vector listKeybinds(const KeySpec& spec); + + std::vector listActiveKeybinds(); + + bool handleKeybind(int sym, int modifiers); + + void setHotkeyCommand(std::string cmd); + + std::optional parseKeySpec(std::string spec); + private: + std::thread hotkey_thread; + std::mutex lock {}; + std::condition_variable cond {}; + + int hotkey_sig = 0; + std::string queued_command = ""; + + std::map> bindings; + + void hotkey_thread_fn(); + }; +} diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9da2bd80570..028422d75ad 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -7,6 +7,7 @@ #include "PluginManager.h" #include +#include #ifdef WIN32 # include @@ -62,6 +63,9 @@ int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; char* (*g_SDL_GetBasePath)() = nullptr; +SDL_Keycode (*g_SDL_GetKeyFromName)(const char* name) = nullptr; +const char* (*g_SDL_GetKeyName)(SDL_Keycode key) = nullptr; + bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { if ((g_sdl_handle = OpenPlugin(lib_str.c_str()))) @@ -106,6 +110,8 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetKeyFromName); + bind(g_sdl_handle, SDL_GetKeyName); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -196,6 +202,18 @@ int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const c return g_SDL_ShowSimpleMessageBox(flags, title, message, window); } +SDL_Keycode DFSDL::DFSDL_GetKeyFromName(const char* name) { + if (!g_SDL_GetKeyFromName) + return SDLK_UNKNOWN; + return g_SDL_GetKeyFromName(name); +} + +const char* DFSDL::DFSDL_GetKeyName(SDL_Keycode key) { + if (!g_SDL_GetKeyName) + return ""; + return g_SDL_GetKeyName(key); +} + // convert tabs to spaces so they don't get converted to '?' static char * tabs_to_spaces(char *str) { for (char *c = str; *c; ++c) { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp new file mode 100644 index 00000000000..2cf87129d4a --- /dev/null +++ b/library/modules/Hotkey.cpp @@ -0,0 +1,300 @@ +#include "modules/Hotkey.h" +#include "Core.h" +#include "CoreDefs.h" +#include "ColorText.h" +#include "MiscUtils.h" +#include "modules/DFSDL.h" +#include "modules/Gui.h" +#include "PluginManager.h" +#include "df/global_objects.h" +#include "df/viewscreen.h" +#include "df/interfacest.h" + +#include + +#include "SDL_keycode.h" + +using namespace DFHack; + +enum HotkeySignal { + None = 0, + CmdReady, + Shutdown, +}; + +bool operator==(const HotkeyManager::KeySpec& a, const HotkeyManager::KeySpec& b) { + return a.modifiers == b.modifiers && a.sym == b.sym && + a.focus.size() == b.focus.size() && + std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); +} + +// Equality operator for key bindings +bool operator==(const HotkeyManager::KeyBinding& a, const HotkeyManager::KeyBinding& b) { + return a.spec == b.spec && + a.command == b.command && + a.cmdline == b.cmdline; +} + +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + +std::optional HotkeyManager::parseKeySpec(std::string spec) { + KeySpec out; + + // Determine focus string, if present + size_t focus_idx = spec.find('@'); + if (focus_idx != std::string::npos) { + split_string(&out.focus, spec.substr(focus_idx + 1), "|"); + spec.erase(focus_idx); + } + + // Determine modifier flags + auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { + bool found = spec.starts_with(prefix); + if (found) { + out.modifiers |= mod; + spec.erase(0, prefix.size()); + } + return found; + }; + while (match_modifier("Shift-", DFH_MOD_SHIFT) || match_modifier("Ctrl-", DFH_MOD_CTRL) || match_modifier("Alt-", DFH_MOD_ALT)) {} + + out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); + if (out.sym != SDLK_UNKNOWN) + return out; + + // Attempt to parse as a mouse binding + if (spec.starts_with("MOUSE")) { + spec.erase(0, 5); + // Read button number, ensuring between 1 and 15 inclusive + try { + int mbutton = std::stoi(spec); + if (mbutton >= 1 && mbutton <= 15) { + out.sym = -mbutton; + return out; + } + } catch (...) { + // If integer parsing fails, it isn't valid + } + } + + // Invalid key binding + return std::nullopt; +} + +bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { + // No point in a hotkey with no action + if (cmd.empty()) + return false; + + KeyBinding binding; + binding.spec = spec; + binding.cmdline = cmd; + + std::lock_guard l(lock); + auto& bindings = this->bindings[binding.spec.sym]; + for (auto& bind : bindings) { + // Don't set a keybind twice, but return true as there isn't an issue + if (bind == binding) + return true; + } + + bindings.emplace_back(binding); + return true; +} + +bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return false; + return this->addKeybind(spec_opt.value(), cmd); +} + +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return false; + auto& binds = bindings[spec.sym]; + + auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec](const auto& v) { + return any_focus + ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers + : v.spec == spec; + }); + if (new_end == binds.end()) + return false; // No bindings removed + + binds.erase(new_end, binds.end()); + return true; +} + +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return false; + return this->clearKeybind(spec_opt.value(), any_focus); +} + +std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return {}; + + std::vector out; + + auto& binds = bindings[spec.sym]; + for (const auto& bind : binds) { + if (bind.spec.modifiers != spec.modifiers) + continue; + + // If no focus is required, it is always active + if (spec.focus.empty() || bind.spec.focus.empty()) { + out.push_back(bind.cmdline); + continue; + } + + // If a focus is required, determine if search spec if the same or more specific + for (const auto& requested : spec.focus) { + for (const auto& to_match : bind.spec.focus) { + if (prefix_matches(to_match, requested)) + out.push_back("@" + to_match + ":" + bind.cmdline); + } + } + } + + return out; +} + +std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return {}; + return this->listKeybinds(spec_opt.value()); +} + +std::vector HotkeyManager::listActiveKeybinds() { + std::vector out; + + for(const auto& [_, bind_set] : bindings) { + for (const auto& binding : bind_set) { + if (binding.spec.focus.empty()) { + // Binding always active + out.emplace_back(binding); + continue; + } + for (const auto& focus : binding.spec.focus) { + // Determine if focus string allows this binding + if (Gui::matchFocusString(focus)) { + out.emplace_back(binding); + break; + } + } + } + } + + return out; +} + +bool HotkeyManager::handleKeybind(int sym, int modifiers) { + // Ensure gamestate is ready + if (!df::global::gview || !df::global::plotinfo) + return false; + + // Get bottommost active screen + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + if (sym == SDLK_KP_ENTER) + sym = SDLK_RETURN; + + std::unique_lock l(lock); + if (!bindings.contains(sym)) + return false; + auto& binds = bindings[sym]; + + auto& core = Core::getInstance(); + bool mortal_mode = core.getMortalMode(); + + for (const auto& bind : binds | std::views::reverse) { + if (bind.spec.modifiers != modifiers) + continue; + + if (!bind.spec.focus.empty()) { + bool matched = false; + for (const auto& focus : bind.spec.focus) { + printf("Focus check for: %s", focus.c_str()); + if (Gui::matchFocusString(focus)) { + printf("Matched\n"); + matched = true; + break; + } + } + if (!matched) + continue; + } + + if (!Core::getInstance().getPluginManager()->CanInvokeHotkey(bind.command, screen)) + continue; + + if (mortal_mode && core.isArmokTool(bind.command)) + continue; + + queued_command = bind.cmdline; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); + return true; + } + + return false; +} + +void HotkeyManager::setHotkeyCommand(std::string cmd) { + std::unique_lock l(lock); + queued_command = cmd; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); +} + +HotkeyManager::HotkeyManager() { + this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); +} + +HotkeyManager::~HotkeyManager() { + // Set shutdown signal and notify thread + { + std::lock_guard l(lock); + this->hotkey_sig = HotkeySignal::Shutdown; + } + cond.notify_all(); + + if (this->hotkey_thread.joinable()) + this->hotkey_thread.join(); +} diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 98b4c1c1552..330a0938f93 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -19,6 +19,7 @@ #include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Maps.h" #include "modules/World.h" @@ -1668,7 +1669,7 @@ static command_result do_blueprint(color_ostream &out, string command_str = command.str(); out.print("launching %s\n", command_str.c_str()); - Core::getInstance().setHotkeyCmd(command_str); + Core::getInstance().getHotkeyManager()->setHotkeyCommand(command_str); return CR_OK; } diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index db04308d0b2..5b9d11f78ad 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Screen.h" #include "../uicommon.h" @@ -161,7 +162,7 @@ struct start_site_hook : df::viewscreen_choose_start_sitest { { if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A)) { - Core::getInstance().setHotkeyCmd("embark-assistant"); + Core::getInstance().getHotkeyManager()->setHotkeyCommand("embark-assistant"); return; } INTERPOSE_NEXT(feed)(input); diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f14..197b0d685da 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -2,8 +2,10 @@ #include #include +#include "modules/DFSDL.h" #include "modules/Gui.h" #include "modules/Screen.h" +#include "modules/Hotkey.h" #include "Debug.h" #include "LuaTools.h" @@ -44,7 +46,7 @@ static int cleanupHotkeys(lua_State *) { std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().ClearKeyBindings(keyspec); + Core::getInstance().getHotkeyManager()->clearKeybind(keyspec); }); valid = false; sorted_keys.clear(); @@ -83,60 +85,30 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); - Core::getInstance().AddKeyBinding(keyspec, binding); + Core::getInstance().getHotkeyManager()->addKeybind(keyspec, binding); } static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, bool filtermenu) { - DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); - vector valid_keys; - - for (char c = '0'; c <= '9'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (char c = 'A'; c <= 'Z'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (int i = 1; i <= 12; i++) { - valid_keys.push_back('F' + int_to_string(i)); - } - - valid_keys.push_back("`"); - - for (int shifted = 0; shifted < 2; shifted++) { - for (int alt = 0; alt < 2; alt++) { - for (int ctrl = 0; ctrl < 2; ctrl++) { - for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { - string sym; - if (ctrl) sym += "Ctrl-"; - if (alt) sym += "Alt-"; - if (shifted) sym += "Shift-"; - sym += *it; - - auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - string::size_type colon_pos = invoke_cmd->find(":"); - // colons at location 0 are for commands like ":lua" - if (colon_pos == string::npos || colon_pos == 0) { - add_binding_if_valid(out, sym, *invoke_cmd, screen, filtermenu); - } - else { - vector tokens; - split_string(&tokens, *invoke_cmd, ":"); - string focus = tokens[0].substr(1); - if(Gui::matchFocusString(focus)) { - auto cmdline = trim(tokens[1]); - add_binding_if_valid(out, sym, cmdline, screen, filtermenu); - } - } - } - } - } + auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); + for (const auto& bind : active_binds) { + string sym; + if (bind.spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; + if (bind.spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (bind.spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; + + std::string key_name; + if (bind.spec.sym < 0) { + key_name = "MOUSE" + std::to_string(-bind.spec.sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(bind.spec.sym); } + if (key_name.empty()) continue; + sym += key_name; + + add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } valid = true; From 835966798226fcf69d5fa7142d96853572cfb76b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 19 Nov 2025 19:56:07 -0600 Subject: [PATCH 600/919] add explicit conversion --- library/include/RemoteTools.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index f041c5d7b8c..8731ad3474b 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -74,7 +74,7 @@ namespace DFHack */ template void flagarray_to_ints(RepeatedField *pf, const BitArray &val) { - for (size_t i = 0; i < val.size()*8; i++) + for (size_t i = 0; i < size_t(val.size()*8); i++) if (val.is_set(T(i))) pf->Add(i); } From 1554b55fb8ba91ddd0717c2eec8d9b0d0bd92b89 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 20:59:57 -0500 Subject: [PATCH 601/919] Reorder imports --- library/include/modules/Hotkey.h | 2 +- library/modules/Hotkey.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 581ebe43e08..d6dbf591d80 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -4,9 +4,9 @@ #include #include +#include #include #include -#include namespace DFHack { class DFHACK_EXPORT HotkeyManager { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 2cf87129d4a..329db01fb73 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -1,18 +1,19 @@ #include "modules/Hotkey.h" + #include "Core.h" -#include "CoreDefs.h" #include "ColorText.h" #include "MiscUtils.h" +#include "PluginManager.h" + #include "modules/DFSDL.h" #include "modules/Gui.h" -#include "PluginManager.h" + #include "df/global_objects.h" #include "df/viewscreen.h" #include "df/interfacest.h" #include - -#include "SDL_keycode.h" +#include using namespace DFHack; From ec2b30972541130ddecc664dc5108939b9982b81 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 21:12:03 -0500 Subject: [PATCH 602/919] Move keybinding command handling into Hotkey module --- library/Core.cpp | 48 +------------------------------- library/include/modules/Hotkey.h | 3 ++ library/modules/Hotkey.cpp | 43 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 6cc60aaf85b..93b5f93da6d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -957,53 +957,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "keybinding") { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; - if (parts[0] == "set") - hotkey_mgr->clearKeybind(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!hotkey_mgr->addKeybind(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!hotkey_mgr->clearKeybind(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = hotkey_mgr->listKeybinds(parts[1]); - if (list.empty()) - con << "No bindings." << std::endl; - for (const auto& kb : list) - con << " " << kb << std::endl; - } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; - } + this->hotkey_mgr->handleKeybindingCommand(con, parts); } else if (first == "alias") { diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index d6dbf591d80..6c4eefaf255 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -1,6 +1,7 @@ #pragma once #include "Export.h" +#include "ColorText.h" #include #include @@ -10,6 +11,7 @@ namespace DFHack { class DFHACK_EXPORT HotkeyManager { + friend class Core; public: HotkeyManager(); ~HotkeyManager(); @@ -53,5 +55,6 @@ namespace DFHack { std::map> bindings; void hotkey_thread_fn(); + void handleKeybindingCommand(color_ostream& out, const std::vector& parts); }; } diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 329db01fb73..800bafa674e 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -284,6 +284,49 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } + +void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + std::string keystr = parts[1]; + if (parts[0] == "set") + clearKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + if (!addKeybind(keystr, part)) { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + break; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + if (!clearKeybind(part)) { + con.printerr("Invalid key spec: %s\n", part.c_str()); + break; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") { + std::vector list = listKeybinds(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } +} + HotkeyManager::HotkeyManager() { this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); } From 3c2dc3cfa04d10e110d26999a7ff9a53391f9989 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 11:39:15 -0500 Subject: [PATCH 603/919] Properly populate the command field on keybinds --- library/modules/Hotkey.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 800bafa674e..52657ac4f28 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -116,6 +116,8 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { KeyBinding binding; binding.spec = spec; binding.cmdline = cmd; + size_t space_idx = cmd.find(' '); + binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); std::lock_guard l(lock); auto& bindings = this->bindings[binding.spec.sym]; From b5d8899a8bb72d50b2fb0e1480e25f4a294e16db Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 13:32:12 -0500 Subject: [PATCH 604/919] Lua api for hotkeys and library support for future keybinding gui --- library/LuaApi.cpp | 73 +++++++++++++++++++++++++++ library/include/modules/Hotkey.h | 54 ++++++++++++-------- library/modules/Hotkey.cpp | 84 ++++++++++++++++++++++++++++---- plugins/hotkeys.cpp | 15 +----- 4 files changed, 182 insertions(+), 44 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1989b9a78b5..6ae5c071e8d 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -47,6 +47,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Kitchen.h" @@ -1866,6 +1867,77 @@ static const luaL_Reg dfhack_gui_funcs[] = { { NULL, NULL } }; +/***** Hotkey module *****/ +static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->addKeybind(spec, cmd); +} + +static bool hotkey_clearKeybind(const std::string spec, bool any_focus, std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->clearKeybind(spec, any_focus, cmd); +} + +static void hotkey_requestKeybindingInput() { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); +} + +static std::string hotkey_readKeybindInput() { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return ""; + return hotkey_mgr->readKeybindInput(); +} + +void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { + lua_createtable(L, binds.size(), 0); + int i = 1; + for (const auto& bind : binds) { + lua_createtable(L, 0, 2); + + lua_pushstring(L, "spec"); + lua_pushstring(L, Hotkey::keyspec_to_string(bind.spec, true).c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "command"); + lua_pushstring(L, bind.cmdline.c_str()); + lua_settable(L, -3); + lua_rawseti(L, -2, i++); + } +} + +static int hotkey_listActiveKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listActiveKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static int hotkey_listAllKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listAllKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static const luaL_Reg dfhack_hotkey_funcs[] = { + { "listActiveKeybinds", hotkey_listActiveKeybinds }, + { "listAllKeybinds", hotkey_listAllKeybinds }, + { NULL, NULL } +}; + +static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { + WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(clearKeybind, hotkey_clearKeybind), + WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), + WRAPN(readKeybindInput, hotkey_readKeybindInput), + { NULL, NULL } +}; + /***** Job module *****/ static bool jobEqual(const df::job *job1, const df::job *job2) @@ -4304,6 +4376,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "translation", dfhack_translation_module); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); + OpenModule(state, "hotkey", dfhack_hotkey_module, dfhack_hotkey_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_funcs); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 6c4eefaf255..7eed1200be8 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -10,49 +10,63 @@ #include namespace DFHack { + namespace Hotkey { + struct KeySpec { + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + }; + + struct KeyBinding { + KeySpec spec; + std::string command; + std::string cmdline; + }; + + DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); + } class DFHACK_EXPORT HotkeyManager { friend class Core; public: HotkeyManager(); ~HotkeyManager(); - struct KeySpec { - int modifiers = 0; - // Negative numbers denote mouse buttons - int sym = 0; - std::vector focus; - }; - - struct KeyBinding { - HotkeyManager::KeySpec spec; - std::string command; - std::string cmdline; - }; bool addKeybind(std::string keyspec, std::string cmd); - bool addKeybind(KeySpec spec, std::string cmd); - bool clearKeybind(std::string keyspec, bool any_focus=false); - bool clearKeybind(const KeySpec& spec, bool any_focus=false); + bool addKeybind(Hotkey::KeySpec spec, std::string cmd); + // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command + bool clearKeybind(std::string keyspec, bool any_focus=false, std::string cmdline=""); + bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string cmdline=""); std::vector listKeybinds(std::string keyspec); - std::vector listKeybinds(const KeySpec& spec); + std::vector listKeybinds(const Hotkey::KeySpec& spec); - std::vector listActiveKeybinds(); + std::vector listActiveKeybinds(); + std::vector listAllKeybinds(); bool handleKeybind(int sym, int modifiers); - void setHotkeyCommand(std::string cmd); - std::optional parseKeySpec(std::string spec); + // Used to request the next keybind input is saved. + // This is to allow for graphical keybinding menus. + void requestKeybindInput(); + // Returns the latest requested keybind input + std::string readKeybindInput(); + + std::optional parseKeySpec(std::string spec); private: std::thread hotkey_thread; std::mutex lock {}; std::condition_variable cond {}; + bool keybind_save_requested = false; + std::string requested_keybind; + int hotkey_sig = 0; std::string queued_command = ""; - std::map> bindings; + std::map> bindings; void hotkey_thread_fn(); void handleKeybindingCommand(color_ostream& out, const std::vector& parts); diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 52657ac4f28..7b57e032bd9 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -16,6 +16,8 @@ #include using namespace DFHack; +using Hotkey::KeySpec; +using Hotkey::KeyBinding; enum HotkeySignal { None = 0, @@ -23,19 +25,49 @@ enum HotkeySignal { Shutdown, }; -bool operator==(const HotkeyManager::KeySpec& a, const HotkeyManager::KeySpec& b) { +bool operator==(const KeySpec& a, const KeySpec& b) { return a.modifiers == b.modifiers && a.sym == b.sym && a.focus.size() == b.focus.size() && std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); } // Equality operator for key bindings -bool operator==(const HotkeyManager::KeyBinding& a, const HotkeyManager::KeyBinding& b) { +bool operator==(const KeyBinding& a, const KeyBinding& b) { return a.spec == b.spec && a.command == b.command && a.cmdline == b.cmdline; } +std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { + std::string sym; + if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; + if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; + + std::string key_name; + if (spec.sym < 0) { + key_name = "MOUSE" + std::to_string(-spec.sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(spec.sym); + } + sym += key_name; + + if (include_focus && !spec.focus.empty()) { + sym += "@"; + bool first = true; + for (const auto& focus : spec.focus) { + if (first) { + first = false; + sym += focus; + } else { + sym += "|" + focus; + } + } + } + + return sym; +} + // Hotkeys actions are executed from an external thread to avoid deadlocks // that may occur if running commands from the render or simulation threads. void HotkeyManager::hotkey_thread_fn() { @@ -64,7 +96,7 @@ void HotkeyManager::hotkey_thread_fn() { } } -std::optional HotkeyManager::parseKeySpec(std::string spec) { +std::optional HotkeyManager::parseKeySpec(std::string spec) { KeySpec out; // Determine focus string, if present @@ -138,16 +170,16 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; auto& binds = bindings[spec.sym]; - auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec](const auto& v) { - return any_focus + auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec, &cmdline](const auto& v) { + return (any_focus ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers - : v.spec == spec; + : v.spec == spec) && (cmdline.empty() || v.cmdline == cmdline); }); if (new_end == binds.end()) return false; // No bindings removed @@ -156,11 +188,11 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus) { +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { std::optional spec_opt = parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; - return this->clearKeybind(spec_opt.value(), any_focus); + return this->clearKeybind(spec_opt.value(), any_focus, cmdline); } std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { @@ -200,7 +232,7 @@ std::vector HotkeyManager::listKeybinds(std::string keyspec) { return this->listKeybinds(spec_opt.value()); } -std::vector HotkeyManager::listActiveKeybinds() { +std::vector HotkeyManager::listActiveKeybinds() { std::vector out; for(const auto& [_, bind_set] : bindings) { @@ -223,6 +255,17 @@ std::vector HotkeyManager::listActiveKeybinds() { return out; } +std::vector HotkeyManager::listAllKeybinds() { + std::vector out; + + for (const auto& [_, bind_set] : bindings) { + for (const auto& bind : bind_set) { + out.emplace_back(bind); + } + } + return out; +} + bool HotkeyManager::handleKeybind(int sym, int modifiers) { // Ensure gamestate is ready if (!df::global::gview || !df::global::plotinfo) @@ -237,6 +280,17 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { sym = SDLK_RETURN; std::unique_lock l(lock); + + // If reading input for a keybinding screen, save the input and exit early + if (keybind_save_requested) { + KeySpec spec; + spec.sym = sym; + spec.modifiers = modifiers; + requested_keybind = Hotkey::keyspec_to_string(spec); + keybind_save_requested = false; + return true; + } + if (!bindings.contains(sym)) return false; auto& binds = bindings[sym]; @@ -286,6 +340,16 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } +void HotkeyManager::requestKeybindInput() { + std::lock_guard l(lock); + keybind_save_requested = true; + requested_keybind = ""; +} + +std::string HotkeyManager::readKeybindInput() { + std::lock_guard l(lock); + return requested_keybind; +} void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 197b0d685da..1e87d7296d9 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -94,20 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); for (const auto& bind : active_binds) { - string sym; - if (bind.spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; - if (bind.spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; - if (bind.spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; - - std::string key_name; - if (bind.spec.sym < 0) { - key_name = "MOUSE" + std::to_string(-bind.spec.sym); - } else { - key_name = DFSDL::DFSDL_GetKeyName(bind.spec.sym); - } - if (key_name.empty()) continue; - sym += key_name; - + string sym = Hotkey::keyspec_to_string(bind.spec); add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } From 74737b415d21b2781e3d77b54ebf7623fe408f8b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 16:18:59 -0500 Subject: [PATCH 605/919] Prefer using to typedef --- library/include/modules/DFSDL.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 161cc351710..9bba7eb4abb 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -11,7 +11,7 @@ struct SDL_Rect; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; -typedef int32_t SDL_Keycode; +using SDL_Keycode = int32_t; namespace DFHack { From 1fcee14fe8c18d542814edbcb5d967a40247adc8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 16:44:05 -0500 Subject: [PATCH 606/919] Improve keybinding command error messages for invalid keyspecs --- library/include/modules/Hotkey.h | 2 +- library/modules/Hotkey.cpp | 92 +++++++++++++++++++------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 7eed1200be8..75f1af1604b 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -24,6 +24,7 @@ namespace DFHack { std::string cmdline; }; + DFHACK_EXPORT std::optional parseKeySpec(std::string spec, std::string* err = nullptr); DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); } class DFHACK_EXPORT HotkeyManager { @@ -54,7 +55,6 @@ namespace DFHack { // Returns the latest requested keybind input std::string readKeybindInput(); - std::optional parseKeySpec(std::string spec); private: std::thread hotkey_thread; std::mutex lock {}; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 7b57e032bd9..ab64f2e1419 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -68,35 +68,8 @@ std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { return sym; } -// Hotkeys actions are executed from an external thread to avoid deadlocks -// that may occur if running commands from the render or simulation threads. -void HotkeyManager::hotkey_thread_fn() { - auto& core = DFHack::Core::getInstance(); - - std::unique_lock l(lock); - while (true) { - cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); - if (hotkey_sig == HotkeySignal::Shutdown) - return; - if (hotkey_sig != HotkeySignal::CmdReady) - continue; - // Copy and reset important data, then release the lock - this->hotkey_sig = HotkeySignal::None; - std::string cmd = this->queued_command; - this->queued_command.clear(); - l.unlock(); - - // Attempt execution of command - DFHack::color_ostream_proxy out(core.getConsole()); - auto res = core.runCommand(out, cmd); - if (res == DFHack::CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); - l.lock(); - } -} - -std::optional HotkeyManager::parseKeySpec(std::string spec) { +std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) { KeySpec out; // Determine focus string, if present @@ -136,10 +109,42 @@ std::optional HotkeyManager::parseKeySpec(std::string spec) { } } + if (err) + *err = "Unknown key '" + spec + "'"; + // Invalid key binding return std::nullopt; } +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + + bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { // No point in a hotkey with no action if (cmd.empty()) @@ -164,7 +169,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { } bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -189,7 +194,7 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin } bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; return this->clearKeybind(spec_opt.value(), any_focus, cmdline); @@ -226,7 +231,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -352,27 +357,42 @@ std::string HotkeyManager::readKeybindInput() { } void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { + std::string parse_error; if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { std::string keystr = parts[1]; if (parts[0] == "set") clearKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!addKeybind(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); + auto spec = Hotkey::parseKeySpec(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); break; } } } else if (parts.size() >= 2 && parts[0] == "clear") { for (const auto& part : parts | std::views::drop(1)) { - if (!clearKeybind(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); + auto spec = Hotkey::parseKeySpec(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!clearKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); break; } } } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = listKeybinds(parts[1]); + auto spec = Hotkey::parseKeySpec(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return; + } + std::vector list = listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) From e49d2204cc393b03b7d0f71144f54dbddf89013f Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 17:20:53 -0500 Subject: [PATCH 607/919] Make keyspecs case-insensitive (excluding focus strings) --- library/modules/Hotkey.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index ab64f2e1419..aa38773865d 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -79,6 +79,9 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) spec.erase(focus_idx); } + // Treat remaining keyspec as lowercase for case-insensitivity. + std::transform(spec.begin(), spec.end(), spec.begin(), tolower); + // Determine modifier flags auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { bool found = spec.starts_with(prefix); @@ -88,25 +91,28 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) } return found; }; - while (match_modifier("Shift-", DFH_MOD_SHIFT) || match_modifier("Ctrl-", DFH_MOD_CTRL) || match_modifier("Alt-", DFH_MOD_ALT)) {} + while (match_modifier("shift-", DFH_MOD_SHIFT) || match_modifier("ctrl-", DFH_MOD_CTRL) || match_modifier("alt-", DFH_MOD_ALT)) {} out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); if (out.sym != SDLK_UNKNOWN) return out; // Attempt to parse as a mouse binding - if (spec.starts_with("MOUSE")) { + if (spec.starts_with("mouse")) { spec.erase(0, 5); // Read button number, ensuring between 1 and 15 inclusive try { int mbutton = std::stoi(spec); - if (mbutton >= 1 && mbutton <= 15) { + if (mbutton >= 4 && mbutton <= 15) { out.sym = -mbutton; return out; } } catch (...) { // If integer parsing fails, it isn't valid } + if (err) + *err = "Invalid mouse button '" + spec + "', only 4-15 are valid"; + return std::nullopt; } if (err) From 43ecdad98912e6a20882603033bd9215e2a6c44c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 20:48:52 -0500 Subject: [PATCH 608/919] Address code review concerns --- library/Core.cpp | 3 ++- library/LuaApi.cpp | 9 +++++---- library/include/modules/Hotkey.h | 4 ++-- library/modules/Hotkey.cpp | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 93b5f93da6d..12ee89ed15f 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2464,8 +2464,9 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { + // We represent mouse buttons as a negative number, permitting buttons 4-15 SDL_Keycode sym = -but.button; - if (sym >= -15 && sym <= -1 && hotkey_mgr->handleKeybind(sym, modstate)) + if (sym >= -15 && sym <= -4 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6ae5c071e8d..de6ffd78640 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1897,12 +1897,13 @@ void hotkey_pushBindArray(lua_State *L, const std::vector& b for (const auto& bind : binds) { lua_createtable(L, 0, 2); - lua_pushstring(L, "spec"); - lua_pushstring(L, Hotkey::keyspec_to_string(bind.spec, true).c_str()); + lua_pushlstring(L, "spec", 4); + auto spec_str = Hotkey::keyspec_to_string(bind.spec, true); + lua_pushlstring(L, spec_str.data(), spec_str.size()); lua_settable(L, -3); - lua_pushstring(L, "command"); - lua_pushstring(L, bind.cmdline.c_str()); + lua_pushlstring(L, "command", 7); + lua_pushlstring(L, bind.cmdline.data(), bind.cmdline.size()); lua_settable(L, -3); lua_rawseti(L, -2, i++); } diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 75f1af1604b..8f3690b8460 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -37,8 +37,8 @@ namespace DFHack { bool addKeybind(std::string keyspec, std::string cmd); bool addKeybind(Hotkey::KeySpec spec, std::string cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command - bool clearKeybind(std::string keyspec, bool any_focus=false, std::string cmdline=""); - bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string cmdline=""); + bool clearKeybind(std::string keyspec, bool any_focus=false, std::string_view cmdline=""); + bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string_view cmdline=""); std::vector listKeybinds(std::string keyspec); std::vector listKeybinds(const Hotkey::KeySpec& spec); diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index aa38773865d..e3b301e8f49 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -181,7 +181,7 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string cmdline) { +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string_view cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; @@ -199,7 +199,7 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string_view cmdline) { std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; From 2ee9d7e08afad751808de4af956fe1917237aa0c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 21:31:32 -0500 Subject: [PATCH 609/919] Initial documentation pass, and improvements to lua api ergonomics --- docs/builtins/keybinding.rst | 8 ++++---- docs/dev/Lua API.rst | 40 ++++++++++++++++++++++++++++++++++++ library/LuaApi.cpp | 14 +++++++++---- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 3e6970b6fc2..ea27b6bcf37 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,9 +9,9 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or ` -(the key below the :kbd:`Esc` key on most keyboards). You can also represent -mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. +Hotkeys can be any combinations of Ctrl/Alt/Shift with any key recognized by SDL. +You can also represent mouse buttons beyond the first three with ``MOUSE4`` +through ``MOUSE15``. Usage ----- @@ -27,7 +27,7 @@ Usage ``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. -The ```` parameter above has the following **case-sensitive** syntax:: +The ```` parameter above has the following case-insensitive syntax:: [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index a4a039f4097..582e0b7e53b 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1430,6 +1430,46 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +Hotkey module +------------- + +* ``dfhack.hotkey.addKeybind(keyspec, command)`` + + Creates a new keybind with the provided keyspec (see the `keybinding` documentation + for details on format). + Returns false on failure to create keybind. + +* ``dfhack.hotkey.clearKeybind(keyspec, any_focus, command)`` + + Removes keybinds matching the provided keyspec. + If any_focus is true, the focus portion of the keyspec is ignored. + If command is not an empty string, the command is matched against as well. + Returns false if no keybinds were removed. + +* ``dfhack.hotkey.listActiveKeybinds()`` + + Returns a list of keybinds active within the current context. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.listAllKeybinds()`` + + Returns a list of all keybinds currently registered. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.requestKeybindInput()`` + + Requests that the next hotkey-compatible input is saved and not processed, + retrievable with ``dfhack.hotkey.readKeybindInput()``. + +* ``dfhack.hotkey.readKeybindInput()`` + + Reads the latest saved keybind input that was requested. + Returns a keyspec string for the input, or nil if no input is saved. + Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index de6ffd78640..319e1f33759 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1885,10 +1885,16 @@ static void hotkey_requestKeybindingInput() { if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); } -static std::string hotkey_readKeybindInput() { +static int hotkey_readKeybindInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (!hotkey_mgr) return ""; - return hotkey_mgr->readKeybindInput(); + auto input = hotkey_mgr->readKeybindInput(); + + if (input.empty()) { + lua_pushnil(L); + } else { + lua_pushlstring(L, input.data(), input.size()); + } + return 1; } void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { @@ -1928,6 +1934,7 @@ static int hotkey_listAllKeybinds(lua_State *L) { static const luaL_Reg dfhack_hotkey_funcs[] = { { "listActiveKeybinds", hotkey_listActiveKeybinds }, { "listAllKeybinds", hotkey_listAllKeybinds }, + { "readKeybindInput", hotkey_readKeybindInput }, { NULL, NULL } }; @@ -1935,7 +1942,6 @@ static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), WRAPN(clearKeybind, hotkey_clearKeybind), WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), - WRAPN(readKeybindInput, hotkey_readKeybindInput), { NULL, NULL } }; From 37dbb46c05b62e92cd186c91e635c4ca073717c2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 20 Nov 2025 23:51:24 -0600 Subject: [PATCH 610/919] fixes don't code when tired, kids, you make mistakes Co-Authored-By: Nicholas McDaniel --- library/include/BitArray.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 19b226f7a10..c258add590a 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -215,7 +215,7 @@ namespace DFHack T *m_data; unsigned short m_size; - void resize(unsigned new_size, const DfArray* replacement) + void resize(unsigned short new_size, const DfArray* replacement) { if (new_size == m_size) return; @@ -224,11 +224,11 @@ namespace DFHack m_data = (T*) new T[new_size]; - T* copysrc = replacement ? replacement->m_data : m_data; + T* copysrc = replacement ? replacement->m_data : old_data; unsigned short copysize = replacement ? replacement->m_size : m_size; if (copysrc) - std::memcpy(m_data, copysrc, sizeof(T) * copysize); + std::memcpy(m_data, copysrc, sizeof(T) * std::min(copysize, new_size)); if (new_size > m_size) std::memset(m_data + m_size, 0, sizeof(T) * (new_size - m_size)); From de45ea4d2eb2f159a660f9830747514e1d41512c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Nov 2025 11:02:30 -0600 Subject: [PATCH 611/919] move fencepost Co-Authored-By: Nicholas McDaniel --- library/include/BitArray.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index c258add590a..360281c6b14 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -79,7 +79,7 @@ namespace DFHack void extend(T index) { - size_type newsize = (index / 8) + 1; + size_type newsize = (index + 7 ) / 8; if (newsize > _size) resize(newsize); } From fec896577ec7477c2db821254478d35aea283a5a Mon Sep 17 00:00:00 2001 From: dhthwy <302825+dhthwy@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:09:20 -0500 Subject: [PATCH 612/919] The default Filter() constructor fails to initialize some members. Provide safe defaults. --- plugins/debug.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/debug.cpp b/plugins/debug.cpp index eb2fc4c1c7a..90e5c783d6d 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -254,10 +254,10 @@ struct Filter { private: std::regex category_; std::regex plugin_; - DebugCategory::level level_; - size_t matches_; - bool persistent_; - bool enabled_; + DebugCategory::level level_{DebugCategory::level::LTRACE}; + size_t matches_{0}; + bool persistent_{false}; + bool enabled_{false}; std::string categoryText_; std::string pluginText_; }; From 69e734fe43768d2e10984b3fa2be05157198adce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 21 Nov 2025 20:56:43 -0600 Subject: [PATCH 613/919] add missing break --- library/LuaApi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1989b9a78b5..4edf60000af 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2637,6 +2637,7 @@ static int maps_setTileAquifer(lua_State* L) case 1: Lua::CheckDFAssign(L, &p, 1); rv = Maps::setTileAquifer(p); + break; case 2: Lua::CheckDFAssign(L, &p, 1); rv = Maps::setTileAquifer(p, lua_toboolean(L, 2)); From 7ce44a55da0ff9a5dc6c17b6a97842aae31f75fc Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Nov 2025 19:48:17 -0500 Subject: [PATCH 614/919] Improve api ergonomics and add missing locks --- docs/dev/Lua API.rst | 17 +++++----- library/LuaApi.cpp | 57 ++++++++++++++++++++++++-------- library/include/modules/Hotkey.h | 12 +++---- library/modules/Hotkey.cpp | 30 +++++++++-------- plugins/hotkeys.cpp | 2 +- 5 files changed, 77 insertions(+), 41 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 582e0b7e53b..c7c35c5441c 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1439,11 +1439,11 @@ Hotkey module for details on format). Returns false on failure to create keybind. -* ``dfhack.hotkey.clearKeybind(keyspec, any_focus, command)`` +* ``dfhack.hotkey.removeKeybind(keyspec, [match_focus=true, command])`` Removes keybinds matching the provided keyspec. - If any_focus is true, the focus portion of the keyspec is ignored. - If command is not an empty string, the command is matched against as well. + If match_focus is set, the focus portion of the keyspec is matched against. + If command is provided and not an empty string, the command is matched against. Returns false if no keybinds were removed. * ``dfhack.hotkey.listActiveKeybinds()`` @@ -1460,15 +1460,16 @@ Hotkey module :spec: The keyspec for the hotkey :command: The command the hotkey runs when pressed -* ``dfhack.hotkey.requestKeybindInput()`` +* ``dfhack.hotkey.requestKeybindingInput([cancel=false])`` - Requests that the next hotkey-compatible input is saved and not processed, - retrievable with ``dfhack.hotkey.readKeybindInput()``. + Enqueues or cancels a request that the next hotkey-compatible input is saved + and not processed, retrievable with ``dfhack.hotkey.getKeybindingInput()``. + If cancel is true, any current request is cancelled. -* ``dfhack.hotkey.readKeybindInput()`` +* ``dfhack.hotkey.getKeybindingInput()`` Reads the latest saved keybind input that was requested. - Returns a keyspec string for the input, or nil if no input is saved. + Returns a keyspec string for the input, or nil if no input has been saved. Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 319e1f33759..e7b0d1197b2 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1874,20 +1874,19 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { return hotkey_mgr->addKeybind(spec, cmd); } -static bool hotkey_clearKeybind(const std::string spec, bool any_focus, std::string cmd) { +static int hotkey_requestKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (!hotkey_mgr) return false; - return hotkey_mgr->clearKeybind(spec, any_focus, cmd); -} - -static void hotkey_requestKeybindingInput() { - auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); + if (!hotkey_mgr) return 0; + bool cancel = false; + if (lua_gettop(L) == 1) + cancel = lua_toboolean(L, -1); + hotkey_mgr->requestKeybindingInput(cancel); + return 0; } -static int hotkey_readKeybindInput(lua_State *L) { +static int hotkey_getKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - auto input = hotkey_mgr->readKeybindInput(); + auto input = hotkey_mgr->getKeybindingInput(); if (input.empty()) { lua_pushnil(L); @@ -1897,6 +1896,38 @@ static int hotkey_readKeybindInput(lua_State *L) { return 1; } +static int hotkey_removeKeybind(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) { + lua_pushboolean(L, false); + return 1; + } + + bool res = false; + switch (lua_gettop(L)) { + case 1: + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -1)); + break; + case 2: + luaL_checkstring(L, -2); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -2), lua_toboolean(L, -1)); + break; + case 3: + luaL_checkstring(L, -3); + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind( + lua_tostring(L, -3), + lua_toboolean(L, -2), + lua_tostring(L, -1) + ); + break; + } + + lua_pushboolean(L, res); + return 1; +} + void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { lua_createtable(L, binds.size(), 0); int i = 1; @@ -1932,16 +1963,16 @@ static int hotkey_listAllKeybinds(lua_State *L) { } static const luaL_Reg dfhack_hotkey_funcs[] = { + { "removeKeybind", hotkey_removeKeybind }, { "listActiveKeybinds", hotkey_listActiveKeybinds }, { "listAllKeybinds", hotkey_listAllKeybinds }, - { "readKeybindInput", hotkey_readKeybindInput }, + { "requestKeybindingInput", hotkey_requestKeybindingInput }, + { "getKeybindingInput", hotkey_getKeybindingInput }, { NULL, NULL } }; static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), - WRAPN(clearKeybind, hotkey_clearKeybind), - WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), { NULL, NULL } }; diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 8f3690b8460..6414ec92048 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -37,8 +37,8 @@ namespace DFHack { bool addKeybind(std::string keyspec, std::string cmd); bool addKeybind(Hotkey::KeySpec spec, std::string cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command - bool clearKeybind(std::string keyspec, bool any_focus=false, std::string_view cmdline=""); - bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string_view cmdline=""); + bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); + bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); std::vector listKeybinds(std::string keyspec); std::vector listKeybinds(const Hotkey::KeySpec& spec); @@ -49,11 +49,11 @@ namespace DFHack { bool handleKeybind(int sym, int modifiers); void setHotkeyCommand(std::string cmd); - // Used to request the next keybind input is saved. + // Used to request the next hotkey-compatible input is saved. // This is to allow for graphical keybinding menus. - void requestKeybindInput(); + void requestKeybindingInput(bool cancel=false); // Returns the latest requested keybind input - std::string readKeybindInput(); + std::string getKeybindingInput(); private: std::thread hotkey_thread; @@ -64,7 +64,7 @@ namespace DFHack { std::string requested_keybind; int hotkey_sig = 0; - std::string queued_command = ""; + std::string queued_command; std::map> bindings; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index e3b301e8f49..761a2376720 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -181,16 +181,17 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string_view cmdline) { +bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::string_view cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; auto& binds = bindings[spec.sym]; - auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec, &cmdline](const auto& v) { - return (any_focus - ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers - : v.spec == spec) && (cmdline.empty() || v.cmdline == cmdline); + auto new_end = std::remove_if(binds.begin(), binds.end(), [match_focus, spec, &cmdline](const auto& v) { + return v.spec.sym == spec.sym + && v.spec.modifiers == spec.modifiers + && (!match_focus || v.spec.focus == spec.focus) + && (cmdline.empty() || v.cmdline == cmdline); }); if (new_end == binds.end()) return false; // No bindings removed @@ -199,11 +200,11 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string_view cmdline) { +bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; - return this->clearKeybind(spec_opt.value(), any_focus, cmdline); + return this->removeKeybind(spec_opt.value(), match_focus, cmdline); } std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { @@ -237,6 +238,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::lock_guard l(lock); std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return {}; @@ -244,6 +246,7 @@ std::vector HotkeyManager::listKeybinds(std::string keyspec) { } std::vector HotkeyManager::listActiveKeybinds() { + std::lock_guard l(lock); std::vector out; for(const auto& [_, bind_set] : bindings) { @@ -267,6 +270,7 @@ std::vector HotkeyManager::listActiveKeybinds() { } std::vector HotkeyManager::listAllKeybinds() { + std::lock_guard l(lock); std::vector out; for (const auto& [_, bind_set] : bindings) { @@ -351,13 +355,13 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } -void HotkeyManager::requestKeybindInput() { +void HotkeyManager::requestKeybindingInput(bool cancel) { std::lock_guard l(lock); - keybind_save_requested = true; - requested_keybind = ""; + keybind_save_requested = !cancel; + requested_keybind.clear(); } -std::string HotkeyManager::readKeybindInput() { +std::string HotkeyManager::getKeybindingInput() { std::lock_guard l(lock); return requested_keybind; } @@ -367,7 +371,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { std::string keystr = parts[1]; if (parts[0] == "set") - clearKeybind(keystr); + removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { auto spec = Hotkey::parseKeySpec(keystr, &parse_error); if (!spec.has_value()) { @@ -386,7 +390,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); } - if (!clearKeybind(spec.value())) { + if (!removeKeybind(spec.value())) { con.printerr("No matching keybinds to remove\n"); break; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 1e87d7296d9..c0a5f4fccda 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -46,7 +46,7 @@ static int cleanupHotkeys(lua_State *) { std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().getHotkeyManager()->clearKeybind(keyspec); + Core::getInstance().getHotkeyManager()->removeKeybind(keyspec); }); valid = false; sorted_keys.clear(); From 68e0d5b59e8ed6cee9c69c6f27f5c8c8eb7abc4e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Nov 2025 20:34:38 -0500 Subject: [PATCH 615/919] First pass at changelog --- docs/changelog.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8551676b26b..6cfa16e4bf2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,8 +65,15 @@ Template for new versions: ## Documentation ## API +- ``Hotkey``: New module for hotkey functionality ## Lua +- ``dfhack.hotkey.addKeybind``: Creates new keybindings +- ``dfhack.hotkey.removeKeybind``: Removes existing keybindings +- ``dfhack.hotkey.listActiveKeybinds``: Lists all keybinds for the current context +- ``dfhack.hotkey.listAllKeybinds``: Lists all keybinds for all contexts +- ``dfhack.hotkey.requestKeybindingInput``: Requests the next keybind-compatible input is saved +- ``dfhack.hotkey.getKeybindingInput``: Reads the input saved in response to a request. ## Removed From 9c87028411cad69ee1102513485d3563c573d7de Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 23 Nov 2025 17:28:57 -0500 Subject: [PATCH 616/919] Allow "Super" as a hotkey modifier --- .gitignore | 2 +- docs/builtins/keybinding.rst | 4 ++-- docs/dev/Lua API.rst | 2 +- library/Core.cpp | 2 ++ library/LuaApi.cpp | 3 +++ library/include/Core.h | 1 + library/modules/Hotkey.cpp | 6 +++++- 7 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 60a0e1b6b2b..6142448b7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # linux backup files *~ - +.cache # Kdevelop project files *.kdev4 .kdev4 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index ea27b6bcf37..f77a0816507 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,7 +9,7 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with any key recognized by SDL. +Hotkeys can be any combinations of Ctrl/Alt/Super/Shift with any key recognized by SDL. You can also represent mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. @@ -29,7 +29,7 @@ Usage The ```` parameter above has the following case-insensitive syntax:: - [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] + [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote optional parts. diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index c7c35c5441c..27dcf62558b 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3535,7 +3535,7 @@ and are only documented here for completeness: * ``dfhack.internal.getModifiers()`` Returns the state of the keyboard modifier keys in a table of string -> - boolean. The keys are ``ctrl``, ``shift``, and ``alt``. + boolean. The keys are ``ctrl``, ``shift``, ``super``, and ``alt``. * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` diff --git a/library/Core.cpp b/library/Core.cpp index 12ee89ed15f..e8ddf47270e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2439,6 +2439,8 @@ bool Core::doSdlInputEvent(SDL_Event* ev) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_CTRL : modstate & ~DFH_MOD_CTRL; else if (sym == SDLK_LALT || sym == SDLK_RALT) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_ALT : modstate & ~DFH_MOD_ALT; + else if (sym == SDLK_LGUI || sym == SDLK_RGUI) // Renamed to LMETA/RMETA in SDL3 + modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_SUPER : modstate & ~DFH_MOD_SUPER; else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e7b0d1197b2..3021a70b1de 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -4073,6 +4073,9 @@ static int internal_getModifiers(lua_State *L) lua_pushstring(L, "alt"); lua_pushboolean(L, modstate & DFH_MOD_ALT); lua_settable(L, -3); + lua_pushstring(L, "super"); + lua_pushboolean(L, modstate & DFH_MOD_SUPER); + lua_settable(L, -3); return 1; } diff --git a/library/include/Core.h b/library/include/Core.h index 08c7ca3d2e5..bf0378b05dd 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -58,6 +58,7 @@ namespace DFHack constexpr auto DFH_MOD_SHIFT = 1; constexpr auto DFH_MOD_CTRL = 2; constexpr auto DFH_MOD_ALT = 4; + constexpr auto DFH_MOD_SUPER = 8; class Process; class Module; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 761a2376720..11a462dec91 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -42,6 +42,7 @@ std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { std::string sym; if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-"; if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; std::string key_name; @@ -91,7 +92,10 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) } return found; }; - while (match_modifier("shift-", DFH_MOD_SHIFT) || match_modifier("ctrl-", DFH_MOD_CTRL) || match_modifier("alt-", DFH_MOD_ALT)) {} + while (match_modifier("shift-", DFH_MOD_SHIFT) + || match_modifier("ctrl-", DFH_MOD_CTRL) + || match_modifier("alt-", DFH_MOD_ALT) + || match_modifier("super-", DFH_MOD_SUPER)) {} out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); if (out.sym != SDLK_UNKNOWN) From 05f6440c3aab8a036024d6140c804774b0afb794 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 23 Nov 2025 18:51:38 -0500 Subject: [PATCH 617/919] Add heuristic method to catch potentially disruptive keybinds --- docs/dev/Lua API.rst | 6 +++ library/LuaApi.cpp | 10 ++++- library/include/modules/Hotkey.h | 21 ++++++---- library/modules/Hotkey.cpp | 68 ++++++++++++++++++++------------ plugins/hotkeys.cpp | 2 +- 5 files changed, 71 insertions(+), 36 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 27dcf62558b..27103b2d9da 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1471,6 +1471,12 @@ Hotkey module Reads the latest saved keybind input that was requested. Returns a keyspec string for the input, or nil if no input has been saved. +* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)`` + + Determines if the provided keyspec could be disruptive to the game experience. + This includes the majority of standard characters and special keys such as escape, + backspace, and return when lacking modifiers other than Shift. + Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 3021a70b1de..6353f5c07fd 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1874,6 +1874,13 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { return hotkey_mgr->addKeybind(spec, cmd); } +static bool hotkey_isDisruptiveKeybind(const std::string spec) { + auto key = Hotkey::KeySpec::parse(spec); + if (!key.has_value()) + return true; + return key.value().isDisruptive(); +} + static int hotkey_requestKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); if (!hotkey_mgr) return 0; @@ -1935,7 +1942,7 @@ void hotkey_pushBindArray(lua_State *L, const std::vector& b lua_createtable(L, 0, 2); lua_pushlstring(L, "spec", 4); - auto spec_str = Hotkey::keyspec_to_string(bind.spec, true); + auto spec_str = bind.spec.toString(true); lua_pushlstring(L, spec_str.data(), spec_str.size()); lua_settable(L, -3); @@ -1973,6 +1980,7 @@ static const luaL_Reg dfhack_hotkey_funcs[] = { static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(isDisruptiveKeybind, hotkey_isDisruptiveKeybind), { NULL, NULL } }; diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 6414ec92048..2e9a400cd11 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -11,11 +11,19 @@ namespace DFHack { namespace Hotkey { - struct KeySpec { - int modifiers = 0; - // Negative numbers denote mouse buttons - int sym = 0; - std::vector focus; + class DFHACK_EXPORT KeySpec { + public: + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + + static std::optional parse(std::string spec, std::string* err = nullptr); + std::string toString(bool include_focus=true) const; + + // Determines if a keybind could be disruptive to normal gameplay, + // including typing and navigating the UI. + bool isDisruptive() const; }; struct KeyBinding { @@ -23,9 +31,6 @@ namespace DFHack { std::string command; std::string cmdline; }; - - DFHACK_EXPORT std::optional parseKeySpec(std::string spec, std::string* err = nullptr); - DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); } class DFHACK_EXPORT HotkeyManager { friend class Core; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 11a462dec91..eb7b483af07 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -38,39 +38,38 @@ bool operator==(const KeyBinding& a, const KeyBinding& b) { a.cmdline == b.cmdline; } -std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { - std::string sym; - if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; - if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; - if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-"; - if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; +std::string KeySpec::toString(bool include_focus) const { + std::string out; + if (modifiers & DFH_MOD_CTRL) out += "Ctrl-"; + if (modifiers & DFH_MOD_ALT) out += "Alt-"; + if (modifiers & DFH_MOD_SUPER) out += "Super-"; + if (modifiers & DFH_MOD_SHIFT) out += "Shift-"; std::string key_name; - if (spec.sym < 0) { - key_name = "MOUSE" + std::to_string(-spec.sym); + if (this->sym < 0) { + key_name = "MOUSE" + std::to_string(-this->sym); } else { - key_name = DFSDL::DFSDL_GetKeyName(spec.sym); + key_name = DFSDL::DFSDL_GetKeyName(this->sym); } - sym += key_name; + out += key_name; - if (include_focus && !spec.focus.empty()) { - sym += "@"; + if (include_focus && !this->focus.empty()) { + out += "@"; bool first = true; - for (const auto& focus : spec.focus) { + for (const auto& fc : this->focus) { if (first) { first = false; - sym += focus; + out += fc; } else { - sym += "|" + focus; + out += "|" + fc; } } } - return sym; + return out; } - -std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) { +std::optional KeySpec::parse(std::string spec, std::string* err) { KeySpec out; // Determine focus string, if present @@ -126,6 +125,23 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) return std::nullopt; } +bool KeySpec::isDisruptive() const { + // Miscellaneous essential keys + const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; + + // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) + || essential_key_set.find(this->sym) != std::string::npos + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); + + // Essential keys are safe, so long as they have a modifier that isn't Shift + if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) + return true; + + return false; +} + // Hotkeys actions are executed from an external thread to avoid deadlocks // that may occur if running commands from the render or simulation threads. void HotkeyManager::hotkey_thread_fn() { @@ -179,7 +195,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { } bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -205,7 +221,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st } bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return false; return this->removeKeybind(spec_opt.value(), match_focus, cmdline); @@ -243,7 +259,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { std::vector HotkeyManager::listKeybinds(std::string keyspec) { std::lock_guard l(lock); - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -305,7 +321,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { KeySpec spec; spec.sym = sym; spec.modifiers = modifiers; - requested_keybind = Hotkey::keyspec_to_string(spec); + requested_keybind = spec.toString(false); keybind_save_requested = false; return true; } @@ -377,7 +393,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (parts[0] == "set") removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - auto spec = Hotkey::parseKeySpec(keystr, &parse_error); + auto spec = KeySpec::parse(keystr, &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); break; @@ -390,7 +406,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } else if (parts.size() >= 2 && parts[0] == "clear") { for (const auto& part : parts | std::views::drop(1)) { - auto spec = Hotkey::parseKeySpec(part, &parse_error); + auto spec = KeySpec::parse(part, &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); } @@ -401,7 +417,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } } else if (parts.size() == 2 && parts[0] == "list") { - auto spec = Hotkey::parseKeySpec(parts[1], &parse_error); + auto spec = KeySpec::parse(parts[1], &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); return; @@ -419,7 +435,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl << "Context may be used to limit the scope of the binding, by" << std::endl << "requiring the current context to have a certain prefix." << std::endl << "Current UI context is: " << std::endl diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index c0a5f4fccda..448d2a1c10b 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -94,7 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); for (const auto& bind : active_binds) { - string sym = Hotkey::keyspec_to_string(bind.spec); + string sym = bind.spec.toString(false); add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } From 5ee8b3c5f17f1704278f6be9292a0c4ade7a1437 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 19:00:37 -0600 Subject: [PATCH 618/919] disable infinite-sky changes in DF make infinite-sky unsafe to use --- docs/changelog.txt | 1 + plugins/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 46f1a718e69..617a5849c8c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,7 @@ Template for new versions: ## Lua ## Removed +- `infiniteSky`: Temporarily disabled due to incompatibility with changes made as part of DF's siege update # 53.05-r1 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1f721ec7133..a49fdf7a9fe 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -86,7 +86,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) dfhack_plugin(getplants getplants.cpp) dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) - dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) + #dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) #dfhack_plugin(jobutils jobutils.cpp) dfhack_plugin(lair lair.cpp) dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) From 12a311e287719abac8b8690d726a6cba6d635113 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 00:12:43 -0600 Subject: [PATCH 619/919] use `constexpr` in `Core` where possible --- library/Core.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 33f66d767eb..99919099db8 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -201,7 +201,7 @@ uint32_t PerfCounters::getUnpausedFps() { struct CommandDepthCounter { - static const int MAX_DEPTH = 20; + static constexpr int MAX_DEPTH = 20; static thread_local int depth; CommandDepthCounter() { depth++; } ~CommandDepthCounter() { depth--; } @@ -902,8 +902,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "plug") { - const char *header_format = "%30s %10s %4s %8s\n"; - const char *row_format = "%30s %10s %4i %8s\n"; + constexpr auto header_format = "%30s %10s %4s %8s\n"; + constexpr auto row_format = "%30s %10s %4i %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); plug_mgr->refresh(); @@ -1601,7 +1601,7 @@ bool Core::InitMainThread() { { if (!Version::git_xml_match()) { - const char *msg = ( + constexpr auto msg = ( "*******************************************************\n" "* BIG, UGLY ERROR MESSAGE *\n" "*******************************************************\n" @@ -2166,7 +2166,7 @@ static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, con } size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { - static const std::string suffix = ".init"; + static constexpr std::string suffix = ".init"; std::vector scriptFiles; for ( const auto& p : prefix ) { getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); From 61d8540fc6e041e4f78a99f8544746525457c020 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 00:42:54 -0600 Subject: [PATCH 620/919] fix row format, roll back one `constexpr` --- library/Core.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 99919099db8..bb631414c6e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -903,7 +903,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (first == "plug") { constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4i %8s\n"; + constexpr auto row_format = "%30s %10s %4zu %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); plug_mgr->refresh(); @@ -2166,7 +2166,7 @@ static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, con } size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { - static constexpr std::string suffix = ".init"; + static const std::string suffix = ".init"; std::vector scriptFiles; for ( const auto& p : prefix ) { getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); From c6a0181b1d6408cb548c2a229e6aa8a44007e978 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:21:28 +0000 Subject: [PATCH 621/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 72f90ad5824..6a369da9321 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 72f90ad5824fa532525ab123eda8fe91851178ff +Subproject commit 6a369da93210d260a66f39fddd5d95c85962d053 From fce63b7a713b955ad094853642f316daef653c43 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 22 Nov 2025 05:12:44 -0600 Subject: [PATCH 622/919] info sort overlay: support new "Siege engines" subtab Move the Places tab search widget: - in wide interfaces it was being drawn over the new "Siege engines" subtab; move it down so it is drawn in the blank line between the subtab group and the content area (like the Tasks search widget) - in narrow interfaces it was being drawn too far right to show its text field inside the bounds of DF's Info panel; move it left into the space made available now that DF is wrapping the subtab group due to the addition of the new subtab Register a handler for the new "Siege engine" subtab; generate search keys that match the text that DF displays. --- docs/changelog.txt | 2 + plugins/lua/sort/info.lua | 3 +- plugins/lua/sort/places.lua | 115 +++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 617a5849c8c..a18405fd9b4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,9 +57,11 @@ Template for new versions: ## New Tools ## New Features +- `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit +- `sort`: Places search widget moved to account for DF's new "Siege engines" subtab ## Misc Improvements diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index feadee99c0d..e2c728e0e69 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -299,8 +299,7 @@ function get_panel_offsets() if tabs_in_two_rows then t_offset = shift_right and 0 or 3 end - if info.current_mode == df.info_interface_mode_type.JOBS or - info.current_mode == df.info_interface_mode_type.BUILDINGS then + if info.current_mode == df.info_interface_mode_type.JOBS then t_offset = t_offset - 1 end return l_offset, t_offset diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index f2588cc0ff3..c353d5a9480 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -170,6 +170,117 @@ local function get_farmplot_search_key(farmplot) return table.concat(result, ' ') end +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_type(siege_engine) + if siege_engine.type == df.siegeengine_type.BoltThrower then + return 'Bolt Thrower' + end + return df.siegeengine_type[siege_engine.type] +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_status(siege_engine) + -- portions of return value with with underscores are to allow easier + -- word-anchored matching even when the DFHack full-text search mode is + -- enabled; e.g. + -- - "loaded" would match "Loaded" and "Unloaded", + -- - but "_loaded" would only match "_Loaded" + local count = 0 + local count_all = siege_engine.type == df.siegeengine_type.BoltThrower + for _, building_item in ipairs(siege_engine.contained_items) do + if building_item.use_mode == df.building_item_role_type.TEMP then + if not count_all then + return 'Loaded _Loaded' + end + count = count + building_item.item:getStackSize() + end + end + if count_all and count > 0 then + return ('%d bolts _%d_bolts'):format(count, count) + end + return 'Unloaded' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_job_status(siege_engine) + for _, job in ipairs(siege_engine.jobs) do + if job.job_type == df.job_type.LoadCatapult + or job.job_type == df.job_type.LoadBallista + or job.job_type == df.job_type.LoadBoltThrower + then + if dfhack.job.getWorker(job) ~= nil then + return 'Loading' + else + return 'Inactive load task' + end + end + local firing_bolt_thrower = job.job_type == df.job_type.FireBoltThrower + local firing = job.job_type == df.job_type.FireCatapult + or job.job_type == df.job_type.FireBallista + or firing_bolt_thrower + if firing then + local unit = dfhack.job.getWorker(job) + if unit == nil then + return 'No operator' + else + ---@type integer?, integer?, integer? + local x, y, z = dfhack.units.getPosition(unit) + -- DF shows "present" when the unit is inside the building's + -- footprint (or, for bolt throwers, next to it); the unit does + -- not need to be at the exact firing position tile (which + -- varies based on siege engine type and direction) + if x ~= nil and z == siege_engine.z then + ---@cast y integer + if firing_bolt_thrower then + if siege_engine.x1 - 1 <= x and x <= siege_engine.x2 + 1 + and siege_engine.y1 - 1 <= y and y <= siege_engine.y2 + 1 + then + return 'Operator present' + end + elseif dfhack.buildings.containsTile(siege_engine, x, y) then + return 'Operator present' + end + end + return 'Operator assigned' + end + end + end + return '' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function get_siege_engine_search_key(siege_engine) + -- DF 53.05 Info window, Places tab, Siege Engines subtab shows this info: + -- name: assigned name or siege engine type name + -- status: "Unloaded", "Loaded", " bolts" + -- job status: + -- - "Inactive load task" (load job unassigned), + -- - "Loading" (load job assigned), + -- - "No operator" (fire job unassigned), + -- - "Operator present" (fire job assigned), + -- - "Operator assigned" (fire job assigned, but not in position), + -- - blank + -- action: (icons) fire-at-will, practice, prepare-to-fire, keep-loaded, not-in-use + -- These have associated text blurbs that are shown in the + -- building info window, but those texts are not discoverable + -- from the Info > Places > Siege engine list view. + local result = {} + + if #siege_engine.name ~= 0 then table.insert(result, siege_engine.name) end + + table.insert(result, siege_engine_type(siege_engine)) + + table.insert(result, siege_engine_status(siege_engine)) + + table.insert(result, siege_engine_job_status(siege_engine)) + + return table.concat(result, ' ') +end + -- ---------------------- -- PlacesOverlay -- @@ -177,7 +288,8 @@ end PlacesOverlay = defclass(PlacesOverlay, sortoverlay.SortOverlay) PlacesOverlay.ATTRS{ desc='Adds search functionality to the places overview screens.', - default_pos={x=71, y=9}, + default_pos={x=52, y=9}, + version=2, viewscreens='dwarfmode/Info', frame={w=40, h=6} } @@ -205,6 +317,7 @@ function PlacesOverlay:init() self:register_handler('STOCKPILES', buildings.list[df.buildings_mode_type.STOCKPILES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_stockpile_search_key})) self:register_handler('WORKSHOPS', buildings.list[df.buildings_mode_type.WORKSHOPS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_workshop_search_key})) self:register_handler('FARMPLOTS', buildings.list[df.buildings_mode_type.FARMPLOTS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_farmplot_search_key})) + self:register_handler('SIEGE_ENGINES', buildings.list[df.buildings_mode_type.SIEGE_ENGINES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_siege_engine_search_key})) end function PlacesOverlay:get_key() From 6eaf98b1d07c6735d1cd7e9f163ff9fa1fd79c63 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 22 Nov 2025 05:12:44 -0600 Subject: [PATCH 623/919] info sort overlay: use interface width in resize_overlay The DF info window is based on the interface area, not the full window area. So the sort info overlays should also work with the interface width instead of the window width. If DF is configured to use an interface percentage smaller than 100, then the interface area will be narrower than the full window once the window is resized beyond the minimum width. Basing the info sort overlay size on the window size can let some UI elements extend outside the footprint of the info window. For example, when the DF interface percentage is set to 90, the search box for the Task tab - is too close to the info window border at window widths 115 and 147 - shares a edge with the info window border at window widths 116 and 146 - extends beyond the info window border for window widths 117-145 --- plugins/lua/sort/info.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index e2c728e0e69..0b261da2d7f 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -278,8 +278,8 @@ function InfoOverlay:get_key() end function resize_overlay(self) - local sw = dfhack.screen.getWindowSize() - local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) + local iw = gui.get_interface_rect().width + local overlay_width = math.min(40, iw - (self.frame_rect.x1 + 30)) if overlay_width ~= self.frame.w then self.frame.w = overlay_width return true From 30536a65bb2d59a6d0bcd1e83ea1278ac6818646 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Nov 2025 06:30:09 -0600 Subject: [PATCH 624/919] info sort overlay: remove check for unused Labor tab There hasn't been a LABOR tab sort overlay since 50.12. The WORK_DETAIL overlay was removed in 4c928766. That commit was part of 50.12-r1~34. The DF 50.12 release notes say that work detail sorting and searching was added, and the DFHack 50.12-r1 release notes say that search widgets were removed for screens that gained native search. https://store.steampowered.com/news/app/975370/view/4136064865380703752 https://docs.dfhack.org/en/stable/docs/NEWS.html#dfhack-50-12-r1 --- plugins/lua/sort/info.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 0b261da2d7f..4f0462dfb78 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -292,8 +292,7 @@ end function get_panel_offsets() local tabs_in_two_rows = is_tabs_in_two_rows() - local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS or - info.current_mode == df.info_interface_mode_type.LABOR + local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS local l_offset = (not tabs_in_two_rows and shift_right) and 4 or 0 local t_offset = 1 if tabs_in_two_rows then From c5c820cb1ca9d809769fd95d159902ffae4133c2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 10:58:26 -0600 Subject: [PATCH 625/919] revert permissive matching for itch --- .github/workflows/generate-symbols.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 18a99b9db8f..f78da19e748 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -164,7 +164,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_linux*.tar.bz2" + fname="dwarf_fortress_${minor}_${patch}_linux.tar.bz2" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" tar xjf "kitfoxgames/dwarf-fortress/files/${fname}" -C DF_itch tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_itch @@ -272,7 +272,7 @@ jobs: pip install itch-dl minor=$(echo "${{ inputs.version }}" | cut -d. -f1) patch=$(echo "${{ inputs.version }}" | cut -d. -f2) - fname="dwarf_fortress_${minor}_${patch}_windows*.zip" + fname="dwarf_fortress_${minor}_${patch}_windows.zip" itch-dl https://kitfoxgames.itch.io/dwarf-fortress --download-to . --api-key $ITCH_API_KEY --filter-files-glob "${fname}" unzip -d DF_itch "kitfoxgames/dwarf-fortress/files/${fname}" xml/symbols_gen_windows.sh ${{ inputs.version }} ITCH DF_itch From 30ec1420d4ba5ab48d19b50cc9ddcacb37f4e8d1 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:27:37 +0000 Subject: [PATCH 626/919] Auto-update structures ref for 53.06 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 2bd33eba052..a451f62ba8f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2bd33eba052f6ea3f65f4a22670029f2311b07b4 +Subproject commit a451f62ba8fe66aaa748ad85a709fe4d4de4f486 From cab99e8017e3221f848394fc7404604a7f01a98d Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:32:31 +0000 Subject: [PATCH 627/919] Auto-update structures ref for 53.06 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index a451f62ba8f..3e60b0bad1c 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a451f62ba8fe66aaa748ad85a709fe4d4de4f486 +Subproject commit 3e60b0bad1c1d2b72d4ff145ce29056c4de2d247 From 237a424226dd47dd89bec5733cb4a9c4124c646b Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:35:26 +0000 Subject: [PATCH 628/919] Auto-update structures ref for 53.06 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 3e60b0bad1c..2b631c0e55a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3e60b0bad1c1d2b72d4ff145ce29056c4de2d247 +Subproject commit 2b631c0e55abc5009d95b4f291081d267e8dadb9 From 72c30832ad9b069d520e2614908b720dba0d32f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 11:44:21 -0600 Subject: [PATCH 629/919] update version to 53.06 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a9bb70aa153..e78a562626f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.05") +set(DF_VERSION "53.06") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 31af7ac67242ea0610d3f412435dd37c3a1544e8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 11:56:09 -0600 Subject: [PATCH 630/919] 53.06-r1 --- docs/changelog.txt | 18 ++++++++++++++++++ library/xml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 617a5849c8c..91c50db23a4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.06-r1 + +## New Tools + +## New Features + ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit diff --git a/library/xml b/library/xml index 2b631c0e55a..24322fe4a30 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2b631c0e55abc5009d95b4f291081d267e8dadb9 +Subproject commit 24322fe4a30209399c1c5fe903726828edd9032b From 858b05f83d745ebad8a4d85fce82a973e88ca294 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 12:46:19 -0600 Subject: [PATCH 631/919] 53.06-r1 again --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 6a369da9321..dd60bf218e6 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 6a369da93210d260a66f39fddd5d95c85962d053 +Subproject commit dd60bf218e6f4a9c421327d65094998fb6459995 From 06695c9d8e5585d1efe4298efeec372a1bc1eb48 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 01:59:25 -0600 Subject: [PATCH 632/919] refactor Core - part 1 of ? --- library/CMakeLists.txt | 4 + library/Commands.cpp | 49 ++++ library/Core.cpp | 176 +----------- library/LuaApi.cpp | 1 + library/MemoryPatcher.cpp | 84 ++++++ library/Process.cpp | 8 + library/VTableInterpose.cpp | 1 + library/include/Commands.h | 16 ++ library/include/Core.h | 12 +- library/include/MemAccess.h | 485 ++++++++++++++++---------------- library/include/MemoryPatcher.h | 35 +++ 11 files changed, 459 insertions(+), 412 deletions(-) create mode 100644 library/Commands.cpp create mode 100644 library/MemoryPatcher.cpp create mode 100644 library/include/Commands.h create mode 100644 library/include/MemoryPatcher.h diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf62..d10978e97bb 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -48,6 +48,7 @@ set(MAIN_HEADERS include/DFHackVersion.h include/BitArray.h include/ColorText.h + include/Commands.h include/Console.h include/Core.h include/CoreDefs.h @@ -66,6 +67,7 @@ set(MAIN_HEADERS include/MiscUtils.h include/Module.h include/MemAccess.h + include/MemoryPatcher.h include/ModuleFactory.h include/PluginLua.h include/PluginManager.h @@ -88,6 +90,7 @@ set(MAIN_HEADERS_WINDOWS set(MAIN_SOURCES Core.cpp ColorText.cpp + Commands.cpp CompilerWorkAround.cpp DataDefs.cpp DataIdentity.cpp @@ -100,6 +103,7 @@ set(MAIN_SOURCES LuaApi.cpp DataStatics.cpp DataStaticsCtor.cpp + MemoryPatcher.cpp MiscUtils.cpp Types.cpp PluginManager.cpp diff --git a/library/Commands.cpp b/library/Commands.cpp new file mode 100644 index 00000000000..a71c628d02c --- /dev/null +++ b/library/Commands.cpp @@ -0,0 +1,49 @@ + +#include "ColorText.h" +#include "Commands.h" +#include "Core.h" +#include "CoreDefs.h" + +#include +#include + +namespace DFHack +{ + command_result Commands::help(color_ostream& con, const std::string& first, const std::vector& parts) + { + if (!parts.size()) + { + if (con.is_console()) + { + con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" + "Some basic editing capabilities are included (single-line text editing).\n" + "The console also has a command history - you can navigate it with Up and Down keys.\n" + "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" + "by clicking on the program icon in the top bar of the window.\n\n"); + } + con.print("Here are some basic commands to get you started:\n" + " help|?|man - This text.\n" + " help - Usage help for the given plugin, command, or script.\n" + " tags - List the tags that the DFHack tools are grouped by.\n" + " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" + " Optional parameters:\n" + " --notags: skip printing tags for each command.\n" + " --dev: include commands intended for developers and modders.\n" + " cls|clear - Clear the console.\n" + " fpause - Force DF to pause.\n" + " die - Force DF to close immediately, without saving.\n" + " keybinding - Modify bindings of commands to in-game key shortcuts.\n" + "\n" + "See more commands by running 'ls'.\n\n" + ); + + con.print("DFHack version %s\n", dfhack_version_desc().c_str()); + } + else + { + DFHack::help_helper(con, parts[0]); + } + return CR_OK; + } + +} \ No newline at end of file diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6e..5c1abdb41c4 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -30,6 +30,7 @@ distribution. #include "DataDefs.h" #include "Debug.h" #include "Console.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "Module.h" #include "VersionInfoFactory.h" @@ -302,7 +303,7 @@ static void fHKthread(IODATA * iodata) } } -static std::string dfhack_version_desc() +std::string DFHack::dfhack_version_desc() { std::stringstream s; s << Version::dfhack_version() << " "; @@ -363,13 +364,13 @@ static command_result enableLuaScript(color_ostream &out, const std::string_view return ok ? CR_OK : CR_FAILURE; } -command_result Core::runCommand(color_ostream &out, const std::string &command) +command_result Core::runCommand(color_ostream& out, const std::string& command) { if (!command.empty()) { std::vector parts; - Core::cheap_tokenise(command,parts); - if(parts.size() == 0) + Core::cheap_tokenise(command, parts); + if (parts.size() == 0) return CR_NOT_IMPLEMENTED; std::string first = parts[0]; @@ -385,20 +386,23 @@ command_result Core::runCommand(color_ostream &out, const std::string &command) return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream &con, const std::string &command) { +bool is_builtin(color_ostream& con, const std::string& command) +{ CoreSuspender suspend; auto L = DFHack::Core::getInstance().getLuaState(); Lua::StackUnwinder top(L); if (!lua_checkstack(L, 1) || - !Lua::PushModulePublic(con, L, "helpdb", "is_builtin")) { + !Lua::PushModulePublic(con, L, "helpdb", "is_builtin")) + { con.printerr("Failed to load helpdb Lua code\n"); return false; } Lua::Push(L, command); - if (!Lua::SafeCall(con, L, 1, 1)) { + if (!Lua::SafeCall(con, L, 1, 1)) + { con.printerr("Failed Lua call to helpdb.is_builtin.\n"); return false; } @@ -625,7 +629,7 @@ static std::string sc_event_name (state_change_event id) { return "SC_UNKNOWN"; } -void help_helper(color_ostream &con, const std::string &entry_name) { +void DFHack::help_helper(color_ostream &con, const std::string &entry_name) { ConditionalCoreSuspender suspend{}; if (!suspend) { @@ -743,38 +747,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s command_result res; if (first == "help" || first == "man" || first == "?") { - if(!parts.size()) - { - if (con.is_console()) - { - con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" - "Some basic editing capabilities are included (single-line text editing).\n" - "The console also has a command history - you can navigate it with Up and Down keys.\n" - "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" - "by clicking on the program icon in the top bar of the window.\n\n"); - } - con.print("Here are some basic commands to get you started:\n" - " help|?|man - This text.\n" - " help - Usage help for the given plugin, command, or script.\n" - " tags - List the tags that the DFHack tools are grouped by.\n" - " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" - " Optional parameters:\n" - " --notags: skip printing tags for each command.\n" - " --dev: include commands intended for developers and modders.\n" - " cls|clear - Clear the console.\n" - " fpause - Force DF to pause.\n" - " die - Force DF to close immediately, without saving.\n" - " keybinding - Modify bindings of commands to in-game key shortcuts.\n" - "\n" - "See more commands by running 'ls'.\n\n" - ); - - con.print("DFHack version %s\n", dfhack_version_desc().c_str()); - } - else - { - help_helper(con, parts[0]); - } + Commands::help(con, first, parts); } else if (first == "tags") { @@ -1278,7 +1251,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s res = plug_mgr->InvokeCommand(con, first, parts); if (res == CR_WRONG_USAGE) { - help_helper(con, first); + DFHack::help_helper(con, first); } else if (res == CR_NOT_IMPLEMENTED) { @@ -2872,127 +2845,6 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return join_strings(" ", aliases[name]); } -///////////////// -// ClassNameCheck -///////////////// - -// Since there is no Process.cpp, put ClassNameCheck stuff in Core.cpp - -static std::set known_class_names; -static std::map known_vptrs; - -ClassNameCheck::ClassNameCheck(std::string _name) : name(_name), vptr(0) -{ - known_class_names.insert(name); -} - -ClassNameCheck &ClassNameCheck::operator= (const ClassNameCheck &b) -{ - name = b.name; vptr = b.vptr; return *this; -} - -bool ClassNameCheck::operator() (Process *p, void * ptr) const { - if (vptr == 0 && p->readClassName(ptr) == name) - { - vptr = ptr; - known_vptrs[name] = ptr; - } - return (vptr && vptr == ptr); -} - -void ClassNameCheck::getKnownClassNames(std::vector &names) -{ - for(const auto& kcn : known_class_names) { - names.push_back(kcn); - } -} - -MemoryPatcher::MemoryPatcher(Process *p_) : p(p_) -{ - if (!p) - p = Core::getInstance().p.get(); -} - -MemoryPatcher::~MemoryPatcher() -{ - close(); -} - -bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) -{ - auto *sptr = (uint8_t*)target; - uint8_t *eptr = sptr + count; - - // Find the valid memory ranges - if (ranges.empty()) - p->getMemRanges(ranges); - - // Find the ranges that this area spans - unsigned start = 0; - while (start < ranges.size() && ranges[start].end <= sptr) - start++; - if (start >= ranges.size() || ranges[start].start > sptr) - return false; - - unsigned end = start+1; - while (end < ranges.size() && ranges[end].start < eptr) - { - if (ranges[end].start != ranges[end-1].end) - return false; - end++; - } - if (ranges[end-1].end < eptr) - return false; - - // Verify current permissions - for (unsigned i = start; i < end; i++) - if (!ranges[i].valid || !(ranges[i].read || ranges[i].execute) || ranges[i].shared) - return false; - - // Apply writable permissions & update - for (unsigned i = start; i < end; i++) - { - auto &perms = ranges[i]; - if ((perms.write || !write) && perms.read) - continue; - - save.push_back(perms); - perms.write = perms.read = true; - if (!p->setPermissions(perms, perms)) - return false; - } - - return true; -} - -bool MemoryPatcher::write(void *target, const void *src, size_t size) -{ - if (!makeWritable(target, size)) - return false; - - memmove(target, src, size); - - p->flushCache(target, size); - return true; -} - -void MemoryPatcher::close() -{ - for (size_t i = 0; i < save.size(); i++) - p->setPermissions(save[i], save[i]); - - save.clear(); - ranges.clear(); -}; - - -bool Process::patchMemory(void *target, const void* src, size_t count) -{ - MemoryPatcher patcher(this); - - return patcher.write(target, src, count); -} - /******************************************************************************* M O D U L E S *******************************************************************************/ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4edf60000af..450d03e95e0 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -36,6 +36,7 @@ distribution. #include "LuaTools.h" #include "LuaWrapper.h" #include "md5wrapper.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "PluginManager.h" diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp new file mode 100644 index 00000000000..8fcf1b21c3e --- /dev/null +++ b/library/MemoryPatcher.cpp @@ -0,0 +1,84 @@ +#include "Core.h" +#include "MemoryPatcher.h" +#include "Process.h" + +namespace DFHack +{ + MemoryPatcher::MemoryPatcher(Process* p_) : p(p_) + { + if (!p) + p = Core::getInstance().p.get(); + } + + MemoryPatcher::~MemoryPatcher() + { + close(); + } + + bool MemoryPatcher::verifyAccess(void* target, size_t count, bool write) + { + auto* sptr = (uint8_t*)target; + uint8_t* eptr = sptr + count; + + // Find the valid memory ranges + if (ranges.empty()) + p->getMemRanges(ranges); + + // Find the ranges that this area spans + unsigned start = 0; + while (start < ranges.size() && ranges[start].end <= sptr) + start++; + if (start >= ranges.size() || ranges[start].start > sptr) + return false; + + unsigned end = start + 1; + while (end < ranges.size() && ranges[end].start < eptr) + { + if (ranges[end].start != ranges[end - 1].end) + return false; + end++; + } + if (ranges[end - 1].end < eptr) + return false; + + // Verify current permissions + for (unsigned i = start; i < end; i++) + if (!ranges[i].valid || !(ranges[i].read || ranges[i].execute) || ranges[i].shared) + return false; + + // Apply writable permissions & update + for (unsigned i = start; i < end; i++) + { + auto& perms = ranges[i]; + if ((perms.write || !write) && perms.read) + continue; + + save.push_back(perms); + perms.write = perms.read = true; + if (!p->setPermissions(perms, perms)) + return false; + } + + return true; + } + + bool MemoryPatcher::write(void* target, const void* src, size_t size) + { + if (!makeWritable(target, size)) + return false; + + memmove(target, src, size); + + p->flushCache(target, size); + return true; + } + + void MemoryPatcher::close() + { + for (size_t i = 0; i < save.size(); i++) + p->setPermissions(save[i], save[i]); + + save.clear(); + ranges.clear(); + }; +} \ No newline at end of file diff --git a/library/Process.cpp b/library/Process.cpp index 101eecbc710..beec599ef84 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -57,6 +57,7 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "Memory.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "VersionInfo.h" #include "VersionInfoFactory.h" @@ -796,3 +797,10 @@ int Process::memProtect(void *ptr, const int length, const int prot) return !VirtualProtect(ptr, length, prot_native, &old_prot); #endif /* WIN32 */ } + +bool Process::patchMemory(void* target, const void* src, size_t count) +{ + MemoryPatcher patcher(this); + + return patcher.write(target, src, count); +} diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 3c2a4a50fc6..212d6e7dca1 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -32,6 +32,7 @@ distribution. #include "Core.h" #include "DataFuncs.h" #include "MemAccess.h" +#include "MemoryPatcher.h" #include "VersionInfo.h" #include "VTableInterpose.h" diff --git a/library/include/Commands.h b/library/include/Commands.h new file mode 100644 index 00000000000..bcd8cd0a41f --- /dev/null +++ b/library/include/Commands.h @@ -0,0 +1,16 @@ +#pragma once + +#include "ColorText.h" +#include "CoreDefs.h" +#include "Core.h" + +#include +#include + +namespace DFHack +{ + namespace Commands + { + command_result help(color_ostream& con, const std::string& first, const std::vector& parts); + } +} diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef12..777d6881d66 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -25,6 +25,7 @@ distribution. #pragma once #include "Console.h" +#include "Commands.h" #include "CoreDefs.h" #include "Export.h" #include "Hooks.h" @@ -170,7 +171,8 @@ namespace DFHack std::string getHotkeyCmd( bool &keep_going ); command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); - command_result runCommand(color_ostream &out, const std::string &command); + command_result runCommand(color_ostream& out, const std::string& command); + bool loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent = false); bool addScriptPath(std::filesystem::path path, bool search_before = false); @@ -502,4 +504,12 @@ namespace DFHack operator bool() const { return owns_lock(); } }; + // unclassified functions related to core + + void help_helper(color_ostream& con, const std::string& entry_name); + + std::string dfhack_version_desc(); + } + + diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 5ef352a99e0..19d65468edb 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -50,9 +50,9 @@ namespace DFHack */ struct DFHACK_EXPORT t_memrange { - void * base; - void * start; - void * end; + void* base; + void* start; + void* end; // memory range name (if any) char name[1024]; // permission to read @@ -63,7 +63,7 @@ namespace DFHack bool execute : 1; // is a shared region bool shared : 1; - inline bool isInRange( void * address) + inline bool isInRange(void* address) { if (address >= start && address < end) return true; return false; @@ -77,237 +77,239 @@ namespace DFHack */ class DFHACK_EXPORT Process { - public: - /// this is the single most important destructor ever. ~px - Process(const VersionInfoFactory& known_versions); - ~Process(); - /// read a 8-byte integer - uint64_t readQuad(const void * address) - { - return *(uint64_t *)address; - } - /// read a 8-byte integer - void readQuad(const void * address, uint64_t & value) - { - value = *(uint64_t *)address; - }; - /// write a 8-byte integer - void writeQuad(const void * address, const uint64_t value) - { - (*(uint64_t *)address) = value; - }; - - /// read a 4-byte integer - uint32_t readDWord(const void * address) - { - return *(uint32_t *)address; - } - /// read a 4-byte integer - void readDWord(const void * address, uint32_t & value) - { - value = *(uint32_t *)address; - }; - /// write a 4-byte integer - void writeDWord(const void * address, const uint32_t value) - { - (*(uint32_t *)address) = value; - }; - - /// read a pointer - char * readPtr(const void * address) - { - return *(char **)address; - } - /// read a pointer - void readPtr(const void * address, char * & value) - { - value = *(char **)address; - }; - - /// read a float - float readFloat(const void * address) - { - return *(float*)address; - } - /// write a float - void readFloat(const void * address, float & value) - { - value = *(float*)address; - }; - - /// read a 2-byte integer - uint16_t readWord(const void * address) - { - return *(uint16_t *)address; - } - /// read a 2-byte integer - void readWord(const void * address, uint16_t & value) - { - value = *(uint16_t *)address; - }; - /// write a 2-byte integer - void writeWord(const void * address, const uint16_t value) - { - (*(uint16_t *)address) = value; - }; - - /// read a byte - uint8_t readByte(const void * address) - { - return *(uint8_t *)address; - } - /// read a byte - void readByte(const void * address, uint8_t & value) - { - value = *(uint8_t *)address; - }; - /// write a byte - void writeByte(const void * address, const uint8_t value) - { - (*(uint8_t *)address) = value; - }; - - /// read an arbitrary amount of bytes - void read(void * address, uint32_t length, uint8_t* buffer) - { - memcpy(buffer, (void *) address, length); - }; - /// write an arbitrary amount of bytes - void write(void * address, uint32_t length, uint8_t* buffer) - { - memcpy((void *) address, buffer, length); - }; - - /// read an STL string - const std::string readSTLString (void * offset) - { - std::string * str = (std::string *) offset; - return *str; - }; - /// read an STL string - size_t readSTLString (void * offset, char * buffer, size_t bufcapacity) - { - if(!bufcapacity || bufcapacity == 1) - return 0; - std::string * str = (std::string *) offset; - size_t copied = str->copy(buffer,bufcapacity-1); - buffer[copied] = 0; - return copied; - }; - /** - * write an STL string - * @return length written - */ - size_t writeSTLString(const void * address, const std::string writeString) - { - std::string * str = (std::string *) address; - str->assign(writeString); - return writeString.size(); - }; - /** - * attempt to copy a string from source address to target address. may truncate or leak, depending on platform - * @return length copied - */ - size_t copySTLString(const void * address, const uintptr_t target) - { - std::string * strsrc = (std::string *) address; - std::string * str = (std::string *) target; - str->assign(*strsrc); - return str->size(); - } - - /// get class name of an object with rtti/type info - std::string doReadClassName(void * vptr); - - std::string readClassName(void * vptr) - { - std::map::iterator it = classNameCache.find(vptr); - if (it != classNameCache.end()) - return it->second; - return classNameCache[vptr] = doReadClassName(vptr); - } - - /// read a null-terminated C string - const std::string readCString (void * offset) - { - return std::string((char *) offset); - }; - - /// @return true if the process is suspended - bool isSuspended() - { - return true; - }; - /// @return true if the process is identified -- has a symbol table extension - bool isIdentified() - { - return identified; - }; - - /// get virtual memory ranges of the process (what is mapped where) - static void getMemRanges(std::vector & ranges); - - /// get the symbol table extension of this process - std::shared_ptr getDescriptor() - { - return my_descriptor; - }; - - void ValidateDescriptionOS() { - if (my_descriptor) - my_descriptor->ValidateOS(); - }; - - uintptr_t getBase(); - /// get the DF Process ID - int getPID(); - /// get the DF Process FilePath - std::filesystem::path getPath(); - /// Adjust between in-memory and in-file image offset - int adjustOffset(int offset, bool to_file = false); - - /// millisecond tick count, exactly as DF uses - uint32_t getTickCount(); - - /// modify permisions of memory range - bool setPermissions(const t_memrange & range,const t_memrange &trgrange); - - /// write a possibly read-only memory area - bool patchMemory(void *target, const void* src, size_t count); - - /// flush cache - bool flushCache(const void* target, size_t count); - - /// allocate new memory pages for code or stuff - /// returns -1 on error (0 is a valid address) - void* memAlloc(const int length); - - /// free memory pages from memAlloc - /// should have length = alloced length for portability - /// returns 0 on success - int memDealloc(void *ptr, const int length); - - /// change memory page permissions - /// prot is a bitwise OR of the MemProt enum - /// returns 0 on success - int memProtect(void *ptr, const int length, const int prot); - - enum MemProt { - READ = 1, - WRITE = 2, - EXEC = 4 - }; - - uint32_t getPE() { return my_pe; } - std::string getMD5() { return my_md5; } + public: + /// this is the single most important destructor ever. ~px + Process(const VersionInfoFactory& known_versions); + ~Process(); + /// read a 8-byte integer + uint64_t readQuad(const void* address) + { + return *(uint64_t*)address; + } + /// read a 8-byte integer + void readQuad(const void* address, uint64_t& value) + { + value = *(uint64_t*)address; + }; + /// write a 8-byte integer + void writeQuad(const void* address, const uint64_t value) + { + (*(uint64_t*)address) = value; + }; + + /// read a 4-byte integer + uint32_t readDWord(const void* address) + { + return *(uint32_t*)address; + } + /// read a 4-byte integer + void readDWord(const void* address, uint32_t& value) + { + value = *(uint32_t*)address; + }; + /// write a 4-byte integer + void writeDWord(const void* address, const uint32_t value) + { + (*(uint32_t*)address) = value; + }; + + /// read a pointer + char* readPtr(const void* address) + { + return *(char**)address; + } + /// read a pointer + void readPtr(const void* address, char*& value) + { + value = *(char**)address; + }; + + /// read a float + float readFloat(const void* address) + { + return *(float*)address; + } + /// write a float + void readFloat(const void* address, float& value) + { + value = *(float*)address; + }; + + /// read a 2-byte integer + uint16_t readWord(const void* address) + { + return *(uint16_t*)address; + } + /// read a 2-byte integer + void readWord(const void* address, uint16_t& value) + { + value = *(uint16_t*)address; + }; + /// write a 2-byte integer + void writeWord(const void* address, const uint16_t value) + { + (*(uint16_t*)address) = value; + }; + + /// read a byte + uint8_t readByte(const void* address) + { + return *(uint8_t*)address; + } + /// read a byte + void readByte(const void* address, uint8_t& value) + { + value = *(uint8_t*)address; + }; + /// write a byte + void writeByte(const void* address, const uint8_t value) + { + (*(uint8_t*)address) = value; + }; + + /// read an arbitrary amount of bytes + void read(void* address, uint32_t length, uint8_t* buffer) + { + memcpy(buffer, (void*)address, length); + }; + /// write an arbitrary amount of bytes + void write(void* address, uint32_t length, uint8_t* buffer) + { + memcpy((void*)address, buffer, length); + }; + + /// read an STL string + const std::string readSTLString(void* offset) + { + std::string* str = (std::string*)offset; + return *str; + }; + /// read an STL string + size_t readSTLString(void* offset, char* buffer, size_t bufcapacity) + { + if (!bufcapacity || bufcapacity == 1) + return 0; + std::string* str = (std::string*)offset; + size_t copied = str->copy(buffer, bufcapacity - 1); + buffer[copied] = 0; + return copied; + }; + /** + * write an STL string + * @return length written + */ + size_t writeSTLString(const void* address, const std::string writeString) + { + std::string* str = (std::string*)address; + str->assign(writeString); + return writeString.size(); + }; + /** + * attempt to copy a string from source address to target address. may truncate or leak, depending on platform + * @return length copied + */ + size_t copySTLString(const void* address, const uintptr_t target) + { + std::string* strsrc = (std::string*)address; + std::string* str = (std::string*)target; + str->assign(*strsrc); + return str->size(); + } + + /// get class name of an object with rtti/type info + std::string doReadClassName(void* vptr); + + std::string readClassName(void* vptr) + { + std::map::iterator it = classNameCache.find(vptr); + if (it != classNameCache.end()) + return it->second; + return classNameCache[vptr] = doReadClassName(vptr); + } + + /// read a null-terminated C string + const std::string readCString(void* offset) + { + return std::string((char*)offset); + }; + + /// @return true if the process is suspended + bool isSuspended() + { + return true; + }; + /// @return true if the process is identified -- has a symbol table extension + bool isIdentified() + { + return identified; + }; + + /// get virtual memory ranges of the process (what is mapped where) + static void getMemRanges(std::vector& ranges); + + /// get the symbol table extension of this process + std::shared_ptr getDescriptor() + { + return my_descriptor; + }; + + void ValidateDescriptionOS() + { + if (my_descriptor) + my_descriptor->ValidateOS(); + }; + + uintptr_t getBase(); + /// get the DF Process ID + int getPID(); + /// get the DF Process FilePath + std::filesystem::path getPath(); + /// Adjust between in-memory and in-file image offset + int adjustOffset(int offset, bool to_file = false); + + /// millisecond tick count, exactly as DF uses + uint32_t getTickCount(); + + /// modify permisions of memory range + bool setPermissions(const t_memrange& range, const t_memrange& trgrange); + + /// write a possibly read-only memory area + bool patchMemory(void* target, const void* src, size_t count); + + /// flush cache + bool flushCache(const void* target, size_t count); + + /// allocate new memory pages for code or stuff + /// returns -1 on error (0 is a valid address) + void* memAlloc(const int length); + + /// free memory pages from memAlloc + /// should have length = alloced length for portability + /// returns 0 on success + int memDealloc(void* ptr, const int length); + + /// change memory page permissions + /// prot is a bitwise OR of the MemProt enum + /// returns 0 on success + int memProtect(void* ptr, const int length, const int prot); + + enum MemProt + { + READ = 1, + WRITE = 2, + EXEC = 4 + }; + + uint32_t getPE() { return my_pe; } + std::string getMD5() { return my_md5; } private: std::shared_ptr my_descriptor; - PlatformSpecific *d; + PlatformSpecific* d; bool identified; uint32_t my_pid; uint32_t base; - std::map classNameCache; + std::map classNameCache; uint32_t my_pe; std::string my_md5; }; @@ -315,36 +317,21 @@ namespace DFHack class DFHACK_EXPORT ClassNameCheck { std::string name; - mutable void * vptr; + mutable void* vptr; public: ClassNameCheck() : vptr(0) {} ClassNameCheck(std::string _name); - ClassNameCheck &operator= (const ClassNameCheck &b); + ClassNameCheck& operator= (const ClassNameCheck& b); // Is the class name of the given virtual table pointer the same as the // name for thei ClassNameCheck object? - bool operator() (Process *p, void * ptr) const; + bool operator() (Process* p, void* ptr) const; // Get list of names given to ClassNameCheck constructors. - static void getKnownClassNames(std::vector &names); + static void getKnownClassNames(std::vector& names); }; - class DFHACK_EXPORT MemoryPatcher - { - Process *p; - std::vector ranges, save; - public: - MemoryPatcher(Process *p = NULL); - ~MemoryPatcher(); - - bool verifyAccess(void *target, size_t size, bool write = false); - bool makeWritable(void *target, size_t size) { - return verifyAccess(target, size, true); - } - bool write(void *target, const void *src, size_t size); - - void close(); - }; } + #endif diff --git a/library/include/MemoryPatcher.h b/library/include/MemoryPatcher.h new file mode 100644 index 00000000000..8577a762eff --- /dev/null +++ b/library/include/MemoryPatcher.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "MemAccess.h" + +namespace DFHack { + + class Process; + + class MemoryPatcher { + public: + explicit MemoryPatcher(Process *p_ = nullptr); + ~MemoryPatcher(); + + // Ensure the target memory region is accessible and optionally writable. + // If `write` is true this will attempt to make the pages writable. + bool verifyAccess(void *target, size_t count, bool write); + + // Write `size` bytes from `src` to `target`. Returns true on success. + bool write(void *target, const void *src, size_t size); + + // Restore any modified permissions and clear internal state. + void close(); + + bool makeWritable(void* target, size_t size) + { + return verifyAccess(target, size, true); + } + private: + Process* p; + std::vector ranges, save; + }; + +} From 2a220df662cb36b1c91149ec5dca062b567238bc Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 02:02:49 -0600 Subject: [PATCH 633/919] whitespace --- library/Commands.cpp | 2 +- library/MemoryPatcher.cpp | 2 +- library/include/Core.h | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index a71c628d02c..15dd38374ab 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -46,4 +46,4 @@ namespace DFHack return CR_OK; } -} \ No newline at end of file +} diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp index 8fcf1b21c3e..e05b5b15754 100644 --- a/library/MemoryPatcher.cpp +++ b/library/MemoryPatcher.cpp @@ -81,4 +81,4 @@ namespace DFHack save.clear(); ranges.clear(); }; -} \ No newline at end of file +} diff --git a/library/include/Core.h b/library/include/Core.h index 777d6881d66..2c4d37b7834 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -511,5 +511,3 @@ namespace DFHack std::string dfhack_version_desc(); } - - From 8205a60f8b75b0218a064a1806d7805c6b0168c6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 10:24:21 -0600 Subject: [PATCH 634/919] remove unneeded include there is a "Process.h" in the winapi, so on windows this includes an entirely unneeded windows API header and on linux it simply fails --- library/MemoryPatcher.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp index e05b5b15754..44aa865c420 100644 --- a/library/MemoryPatcher.cpp +++ b/library/MemoryPatcher.cpp @@ -1,6 +1,5 @@ #include "Core.h" #include "MemoryPatcher.h" -#include "Process.h" namespace DFHack { From fe85497448fd1ff6937fe9a0be5183a9eace40a8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 12:29:05 -0600 Subject: [PATCH 635/919] push more commands into `Commands` and assorted other adjustments appertaining thereto --- library/Commands.cpp | 328 ++++++++++++++++++++++++++++++++++++- library/Core.cpp | 285 ++------------------------------ library/include/Commands.h | 9 +- library/include/Core.h | 12 +- 4 files changed, 353 insertions(+), 281 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 15dd38374ab..724c407d8c4 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -1,15 +1,22 @@ -#include "ColorText.h" #include "Commands.h" + +#include "ColorText.h" #include "Core.h" #include "CoreDefs.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "Modules/Gui.h" #include #include +#include + namespace DFHack { - command_result Commands::help(color_ostream& con, const std::string& first, const std::vector& parts) + command_result Commands::help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { if (!parts.size()) { @@ -46,4 +53,321 @@ namespace DFHack return CR_OK; } + command_result Commands::load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool all = false; + bool load = (first == "load"); + bool unload = (first == "unload"); + bool reload = (first == "reload"); + auto plug_mgr = core.getPluginManager(); + if (parts.size()) + { + for (const auto& p : parts) + { + if (p.size() && p[0] == '-') + { + if (p.find('a') != std::string::npos) + all = true; + } + } + auto ret = CR_OK; + if (all) + { + if (load && !plug_mgr->loadAll()) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unloadAll()) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reloadAll()) + ret = CR_FAILURE; + } + else + { + for (auto& p : parts) + { + if (p.empty() || p[0] == '-') + continue; + if (load && !plug_mgr->load(p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(p)) + ret = CR_FAILURE; + } + } + if (ret != CR_OK) + con.printerr("%s failed\n", first.c_str()); + return ret; + } + else + { + con.printerr("%s: no arguments\n", first.c_str()); + return CR_FAILURE; + } + + } + + command_result Commands::enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + CoreSuspender suspend; + bool enable = (first == "enable"); + auto plug_mgr = core.getPluginManager(); + + if (parts.size()) + { + command_result res{CR_FAILURE}; + + for (auto& part_ : parts) + { + // have to copy to modify as passed argument is const + std::string part(part_); + + if (has_backslashes(part)) + { + con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); + replace_backslashes_with_forwardslashes(part); + } + + auto alias = core.GetAliasCommand(part, true); + + Plugin* plug = (*plug_mgr)[alias]; + + if (!plug) + { + std::filesystem::path lua = core.findScript(part + ".lua"); + if (!lua.empty()) + { + res = core.enableLuaScript(con, part, enable); + } + else + { + res = CR_NOT_FOUND; + con.printerr("No such plugin or Lua script: %s\n", part.c_str()); + } + } + else if (!plug->can_set_enabled()) + { + res = CR_NOT_IMPLEMENTED; + con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); + } + else + { + res = plug->set_enabled(con, enable); + + if (res != CR_OK || plug->is_enabled() != enable) + con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); + } + } + + return res; + } + else + { + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + if (!plug->can_be_enabled()) continue; + + con.print( + "%21s %-3s%s\n", + (key + ":").c_str(), + plug->is_enabled() ? "on" : "off", + plug->can_set_enabled() ? "" : " (controlled internally)" + ); + } + + Lua::CallLuaModuleFunction(con, "script-manager", "list"); + + return CR_OK; + } + } + + command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + constexpr auto header_format = "%30s %10s %4s %8s\n"; + constexpr auto row_format = "%30s %10s %4i %8s\n"; + + con.print(header_format, "Name", "State", "Cmds", "Enabled"); + + auto plug_mgr = core.getPluginManager(); + + plug_mgr->refresh(); + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + + if (parts.size() && std::ranges::find(parts, key) == parts.end()) + continue; + + color_value color; + switch (plug->getState()) + { + case Plugin::PS_LOADED: + color = COLOR_RESET; + break; + case Plugin::PS_UNLOADED: + case Plugin::PS_UNLOADING: + color = COLOR_YELLOW; + break; + case Plugin::PS_LOADING: + color = COLOR_LIGHTBLUE; + break; + case Plugin::PS_BROKEN: + color = COLOR_LIGHTRED; + break; + default: + color = COLOR_LIGHTMAGENTA; + break; + } + con.color(color); + con.print(row_format, + plug->getName().c_str(), + Plugin::getStateDescription(plug->getState()), + plug->size(), + (plug->can_be_enabled() + ? (plug->is_enabled() ? "enabled" : "disabled") + : "n/a") + ); + con.color(COLOR_RESET); + } + + return CR_OK; + } + + command_result Commands::type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + auto plug_mgr = core.getPluginManager(); + + if (!parts.size()) + { + con.printerr("type: no argument\n"); + return CR_WRONG_USAGE; + } + con << parts[0]; + bool builtin = is_builtin(con, parts[0]); + std::filesystem::path lua_path = core.findScript(parts[0] + ".lua"); + Plugin* plug = plug_mgr->getPluginByCommand(parts[0]); + if (builtin) + { + con << " is a built-in command"; + con << std::endl; + } + else if (core.IsAlias(parts[0])) + { + con << " is an alias: " << core.GetAliasCommand(parts[0]) << std::endl; + } + else if (plug) + { + con << " is a command implemented by the plugin " << plug->getName() << std::endl; + } + else if (!lua_path.empty()) + { + con << " is a Lua script: " << lua_path << std::endl; + } + else + { + con << " is not a recognized command." << std::endl; + plug = plug_mgr->getPluginByName(parts[0]); + if (plug) + con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) + { + std::string keystr = parts[1]; + if (parts[0] == "set") + core.ClearKeyBindings(keystr); + // for (int i = parts.size()-1; i >= 2; i--) + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) + { + if (!core.AddKeyBinding(keystr, part)) + { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + return CR_FAILURE; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") + { + // for (size_t i = 1; i < parts.size(); i++) + for (const auto& part : parts | std::views::drop(1)) + { + if (!core.ClearKeyBindings(part)) + { + con.printerr("Invalid key spec: %s\n", part.c_str()); + return CR_FAILURE; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") + { + std::vector list = core.ListKeyBindings(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else + { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } + + return CR_OK; + } + + command_result Commands::alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) + { + const std::string& name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); + if (!core.AddAlias(name, cmd, parts[0] == "replace")) + { + con.printerr("Could not add alias %s - already exists\n", name.c_str()); + return CR_FAILURE; + } + } + else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) + { + if (!core.RemoveAlias(parts[1])) + { + con.printerr("Could not remove alias %s\n", parts[1].c_str()); + return CR_FAILURE; + } + } + else if (parts.size() >= 1 && (parts[0] == "list")) + { + auto aliases = core.ListAliases(); + for (auto p : aliases) + { + con << p.first << ": " << join_strings(" ", p.second) << std::endl; + } + } + else + { + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; + } + + return CR_OK; + } + } diff --git a/library/Core.cpp b/library/Core.cpp index 5c1abdb41c4..62b88606099 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -22,11 +22,12 @@ must not be misrepresented as being the original software. distribution. */ +#include "Core.h" + #include "Internal.h" #include "Error.h" #include "MemAccess.h" -#include "Core.h" #include "DataDefs.h" #include "Debug.h" #include "Console.h" @@ -43,6 +44,8 @@ distribution. #include "DFHackVersion.h" #include "md5wrapper.h" +#include "Commands.h" + #include "modules/DFSDL.h" #include "modules/DFSteam.h" #include "modules/EventManager.h" @@ -353,7 +356,7 @@ static bool init_enable_script(color_ostream &out, lua_State *state, const std:: return true; } -static command_result enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) +command_result Core::enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) { auto init_fn = [name, enabled](color_ostream& out, lua_State* state) -> bool { return init_enable_script(out, state, name, enabled); @@ -386,7 +389,7 @@ command_result Core::runCommand(color_ostream& out, const std::string& command) return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream& con, const std::string& command) +bool DFHack::is_builtin(color_ostream& con, const std::string& command) { CoreSuspender suspend; auto L = DFHack::Core::getInstance().getLuaState(); @@ -747,7 +750,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s command_result res; if (first == "help" || first == "man" || first == "?") { - Commands::help(con, first, parts); + return Commands::help(con, *this, first, parts); } else if (first == "tags") { @@ -755,119 +758,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "load" || first == "unload" || first == "reload") { - bool all = false; - bool load = (first == "load"); - bool unload = (first == "unload"); - bool reload = (first == "reload"); - if (parts.size()) - { - for (const auto& p : parts) - { - if (p.size() && p[0] == '-') - { - if (p.find('a') != std::string::npos) - all = true; - } - } - auto ret = CR_OK; - if (all) - { - if (load && !plug_mgr->loadAll()) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unloadAll()) - ret = CR_FAILURE; - else if (reload && !plug_mgr->reloadAll()) - ret = CR_FAILURE; - } - else - { - for (auto& p : parts) - { - if (p.empty() || p[0] == '-') - continue; - if (load && !plug_mgr->load(p)) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(p)) - ret = CR_FAILURE; - else if (reload && !plug_mgr->reload(p)) - ret = CR_FAILURE; - } - } - if (ret != CR_OK) - con.printerr("%s failed\n", first.c_str()); - return ret; - } - else { - con.printerr("%s: no arguments\n", first.c_str()); - return CR_FAILURE; - } + return Commands::load(con, *this, first, parts); } else if( first == "enable" || first == "disable" ) { - CoreSuspender suspend; - bool enable = (first == "enable"); - - if(parts.size()) - { - for (auto& part : parts) - { - if (has_backslashes(part)) - { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - replace_backslashes_with_forwardslashes(part); - } - - part = GetAliasCommand(part, true); - - Plugin * plug = (*plug_mgr)[part]; - - if(!plug) - { - std::filesystem::path lua = findScript(part + ".lua"); - if (!lua.empty()) - { - res = enableLuaScript(con, part, enable); - } - else - { - res = CR_NOT_FOUND; - con.printerr("No such plugin or Lua script: %s\n", part.c_str()); - } - } - else if (!plug->can_set_enabled()) - { - res = CR_NOT_IMPLEMENTED; - con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); - } - else - { - res = plug->set_enabled(con, enable); - - if (res != CR_OK || plug->is_enabled() != enable) - con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); - } - } - - return res; - } - else - { - for (auto& [key, plug] : *plug_mgr) - { - if (!plug) - continue; - if (!plug->can_be_enabled()) continue; - - con.print( - "%21s %-3s%s\n", - (key+":").c_str(), - plug->is_enabled() ? "on" : "off", - plug->can_set_enabled() ? "" : " (controlled internally)" - ); - } - - Lua::CallLuaModuleFunction(con, "script-manager", "list"); - } + return Commands::enable(con, *this, first, parts); } else if (first == "ls" || first == "dir") { @@ -875,173 +770,19 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "plug") { - constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4zu %8s\n"; - con.print(header_format, "Name", "State", "Cmds", "Enabled"); - - plug_mgr->refresh(); - for (auto& [key, plug] : *plug_mgr) - { - if (!plug) - continue; - - if (parts.size() && std::ranges::find(parts, key) == parts.end()) - continue; - - color_value color; - switch (plug->getState()) - { - case Plugin::PS_LOADED: - color = COLOR_RESET; - break; - case Plugin::PS_UNLOADED: - case Plugin::PS_UNLOADING: - color = COLOR_YELLOW; - break; - case Plugin::PS_LOADING: - color = COLOR_LIGHTBLUE; - break; - case Plugin::PS_BROKEN: - color = COLOR_LIGHTRED; - break; - default: - color = COLOR_LIGHTMAGENTA; - break; - } - con.color(color); - con.print(row_format, - plug->getName().c_str(), - Plugin::getStateDescription(plug->getState()), - plug->size(), - (plug->can_be_enabled() - ? (plug->is_enabled() ? "enabled" : "disabled") - : "n/a") - ); - con.color(COLOR_RESET); - } + return Commands::plug(con, *this, first, parts); } else if (first == "type") { - if (!parts.size()) - { - con.printerr("type: no argument\n"); - return CR_WRONG_USAGE; - } - con << parts[0]; - bool builtin = is_builtin(con, parts[0]); - std::filesystem::path lua_path = findScript(parts[0] + ".lua"); - Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); - if (builtin) - { - con << " is a built-in command"; - con << std::endl; - } - else if (IsAlias(parts[0])) - { - con << " is an alias: " << GetAliasCommand(parts[0]) << std::endl; - } - else if (plug) - { - con << " is a command implemented by the plugin " << plug->getName() << std::endl; - } - else if (!lua_path.empty()) - { - con << " is a Lua script: " << lua_path << std::endl; - } - else - { - con << " is not a recognized command." << std::endl; - plug = plug_mgr->getPluginByName(parts[0]); - if (plug) - con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; - return CR_FAILURE; - } + return Commands::type(con, *this, first, parts); } else if (first == "keybinding") { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; - if (parts[0] == "set") - ClearKeyBindings(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!AddKeyBinding(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!ClearKeyBindings(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = ListKeyBindings(parts[1]); - if (list.empty()) - con << "No bindings." << std::endl; - for (const auto& kb : list) - con << " " << kb << std::endl; - } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; - } + return Commands::keybinding(con, *this, first, parts); } else if (first == "alias") { - if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) - { - const std::string &name = parts[1]; - std::vector cmd(parts.begin() + 2, parts.end()); - if (!AddAlias(name, cmd, parts[0] == "replace")) - { - con.printerr("Could not add alias %s - already exists\n", name.c_str()); - return CR_FAILURE; - } - } - else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) - { - if (!RemoveAlias(parts[1])) - { - con.printerr("Could not remove alias %s\n", parts[1].c_str()); - return CR_FAILURE; - } - } - else if (parts.size() >= 1 && (parts[0] == "list")) - { - auto aliases = ListAliases(); - for (auto p : aliases) - { - con << p.first << ": " << join_strings(" ", p.second) << std::endl; - } - } - else - { - con << "Usage: " << std::endl - << " alias add|replace " << std::endl - << " alias delete|clear " << std::endl - << " alias list" << std::endl; - } + return Commands::alias(con, *this, first, parts); } else if (first == "fpause") { diff --git a/library/include/Commands.h b/library/include/Commands.h index bcd8cd0a41f..66f20342631 100644 --- a/library/include/Commands.h +++ b/library/include/Commands.h @@ -11,6 +11,13 @@ namespace DFHack { namespace Commands { - command_result help(color_ostream& con, const std::string& first, const std::vector& parts); + command_result help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + } } diff --git a/library/include/Core.h b/library/include/Core.h index 2c4d37b7834..649fc9fea24 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -25,7 +25,6 @@ distribution. #pragma once #include "Console.h" -#include "Commands.h" #include "CoreDefs.h" #include "Export.h" #include "Hooks.h" @@ -214,7 +213,7 @@ namespace DFHack static void print(const char *format, ...) Wformat(printf,1,2); static void printerr(const char *format, ...) Wformat(printf,1,2); - PluginManager *getPluginManager() { return plug_mgr; } + PluginManager* getPluginManager() const { return plug_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -226,6 +225,8 @@ namespace DFHack return State; } + static command_result enableLuaScript(color_ostream& out, const std::string_view name, bool enabled); + private: DFHack::Console con; @@ -249,8 +250,8 @@ namespace DFHack void onStateChange(color_ostream &out, state_change_event event); void handleLoadAndUnloadScripts(color_ostream &out, state_change_event event); - Core(Core const&); // Don't Implement - void operator=(Core const&); // Don't implement + Core(Core const&) = delete; + void operator=(Core const&) = delete; // report error to user while failing void fatal (std::string output, const char * title = nullptr); @@ -507,7 +508,6 @@ namespace DFHack // unclassified functions related to core void help_helper(color_ostream& con, const std::string& entry_name); - std::string dfhack_version_desc(); - + bool is_builtin(color_ostream& con, const std::string& command); } From 771c4ae05bf9e7e63b5d4d058c5b4b314e0eb04f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 12:36:13 -0600 Subject: [PATCH 636/919] silly case-dependent file systems --- library/Commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 724c407d8c4..9b033f853ff 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -7,7 +7,7 @@ #include "LuaTools.h" #include "PluginManager.h" -#include "Modules/Gui.h" +#include "modules/Gui.h" #include #include From f447711167b29458e3fce2b4abb5121277ede466 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 13:04:50 -0600 Subject: [PATCH 637/919] fix printf --- library/Commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 9b033f853ff..e9ee4a8a944 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -185,7 +185,7 @@ namespace DFHack command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4i %8s\n"; + constexpr auto row_format = "%30s %10s %4llu %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); From 023b78c211134966a8a3de5a55aacfdd865741d3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 13:53:42 -0600 Subject: [PATCH 638/919] really fix printf --- library/Commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index e9ee4a8a944..1c67d5ebe42 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -185,7 +185,7 @@ namespace DFHack command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { constexpr auto header_format = "%30s %10s %4s %8s\n"; - constexpr auto row_format = "%30s %10s %4llu %8s\n"; + constexpr auto row_format = "%30s %10s %4zu %8s\n"; con.print(header_format, "Name", "State", "Cmds", "Enabled"); From 89dd6e70fde682f236151fd389e94ffcc2126e2f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 14:57:43 -0600 Subject: [PATCH 639/919] move `fpause` to `Commands.cpp` incidentally also reenable using `fpause` during worldgen --- library/Commands.cpp | 22 ++++++++++++++++++++++ library/Core.cpp | 9 +-------- library/include/Commands.h | 2 +- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 1c67d5ebe42..aae3ecc9c53 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -8,6 +8,9 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/World.h" + +#include "df/viewscreen_new_regionst.h" #include #include @@ -370,4 +373,23 @@ namespace DFHack return CR_OK; } + command_result Commands::fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (auto scr = Gui::getViewscreenByType()) + { + if (scr->doing_mods || scr->doing_simple_params || scr->doing_params) + { + con.printerr("Cannot pause now.\n"); + return CR_FAILURE; + } + scr->abort_world_gen_dialogue = true; + } + else + { + World::SetPauseState(true); + } + con.print("The game was forced to pause!\n"); + return CR_OK; + } + } diff --git a/library/Core.cpp b/library/Core.cpp index 62b88606099..00370ee1926 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -786,14 +786,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "fpause") { - World::SetPauseState(true); -/* TODO: understand how this changes for v50 - if (auto scr = Gui::getViewscreenByType()) - { - scr->worldgen_paused = true; - } -*/ - con.print("The game was forced to pause!\n"); + return Commands::fpause(con, *this, first, parts); } else if (first == "cls" || first == "clear") { diff --git a/library/include/Commands.h b/library/include/Commands.h index 66f20342631..984b34a637f 100644 --- a/library/include/Commands.h +++ b/library/include/Commands.h @@ -18,6 +18,6 @@ namespace DFHack command_result type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); - + command_result fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); } } From df4af1464ea0f7695ebe3f1b99f2ede569e56077 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 18:03:36 -0600 Subject: [PATCH 640/919] move rest of builtin commands to `Commands.cpp` --- library/Commands.cpp | 186 +++++++++++++++++++++++++++++++++ library/Core.cpp | 170 ++---------------------------- library/include/ColorText.h | 3 + library/include/Commands.h | 7 ++ library/include/Console.h | 3 +- library/include/Core.h | 24 +++++ library/include/RemoteServer.h | 1 + 7 files changed, 232 insertions(+), 162 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index aae3ecc9c53..796a1f27015 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -6,6 +6,7 @@ #include "CoreDefs.h" #include "LuaTools.h" #include "PluginManager.h" +#include "RemoteTools.h" #include "modules/Gui.h" #include "modules/World.h" @@ -392,4 +393,189 @@ namespace DFHack return CR_OK; } + command_result Commands::clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (con.can_clear()) + { + con.clear(); + return CR_OK; + } + else + { + con.printerr("No console to clear, or this console does not support clearing.\n"); + return CR_NEEDS_CONSOLE; + } + } + + command_result Commands::kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool force = std::ranges::any_of(parts, [] (const std::string& part) { return part == "force"; }); + if (!Lua::Interrupt(force)) + { + con.printerr( + "Failed to register hook. This can happen if you have" + " lua profiling or coverage monitoring enabled. Use" + " 'kill-lua force' to force, but this may disable" + " profiling and coverage monitoring.\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + core.loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); + return CR_OK; + } + else + { + con << "Usage:" << std::endl + << " script " << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().show()) + { + con.printerr("Could not show console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().hide()) + { + con.printerr("Could not hide console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.empty() || parts[0] == "help" || parts[0] == "?") + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; + for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) + { + std::string name = sc_event_name((state_change_event)i); + if (name != "SC_UNKNOWN") + con << " " << name << std::endl; + } + return CR_OK; + } + else if (parts[0] == "list") + { + std::string event_name = parts.size() >= 2 ? parts[1] : ""; + if (event_name.size() && sc_event_id(event_name) == SC_UNKNOWN) + { + con << "Unrecognized event name: " << parts[1] << std::endl; + return CR_WRONG_USAGE; + } + for (const auto& state_script : core.getStateChangeScripts()) + { + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) + { + con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path.c_str()); + } + } + return CR_OK; + } + else if (parts[0] == "add") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript script(evt, parts[2], save_specific); + for (const auto& state_script : core.getStateChangeScripts()) + { + if (script == state_script) + { + con << "Script already registered" << std::endl; + return CR_FAILURE; + } + } + core.addStateChangeScript(script); + return CR_OK; + } + else if (parts[0] == "remove") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript tmp(evt, parts[2], save_specific); + if (core.removeStateChangeScript(tmp)) + { + return CR_OK; + } + else + { + con << "Unrecognized script" << std::endl; + return CR_FAILURE; + } + } + else + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + std::ofstream file(parts[0]); + CoreService coreSvc; + coreSvc.dumpMethods(file); + + for (auto& it : *core.getPluginManager()) + { + Plugin* plug = it.second; + if (!plug) + continue; + + std::unique_ptr svc(plug->rpc_connect(con)); + if (!svc) + continue; + + file << "// Plugin: " << plug->getName() << std::endl; + svc->dumpMethods(file); + } + } + else + { + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; + return CR_WRONG_USAGE; + } + return CR_OK; + } } diff --git a/library/Core.cpp b/library/Core.cpp index 00370ee1926..298bb1edfa7 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -612,7 +612,7 @@ static void sc_event_map_init() { } } -static state_change_event sc_event_id (std::string name) { +state_change_event DFHack::sc_event_id (std::string name) { sc_event_map_init(); auto it = state_change_event_map.find(name); if (it != state_change_event_map.end()) @@ -622,7 +622,7 @@ static state_change_event sc_event_id (std::string name) { return SC_UNKNOWN; } -static std::string sc_event_name (state_change_event id) { +std::string DFHack::sc_event_name (state_change_event id) { sc_event_map_init(); for (auto it = state_change_event_map.begin(); it != state_change_event_map.end(); ++it) { @@ -790,13 +790,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "cls" || first == "clear") { - if (con.is_console()) - ((Console&)con).clear(); - else - { - con.printerr("No console to clear.\n"); - return CR_NEEDS_CONSOLE; - } + return Commands::clear(con, *this, first, parts); } else if (first == "die") { @@ -808,173 +802,27 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "kill-lua") { - bool force = false; - for (const auto& part : parts) - { - if (part == "force") - force = true; - } - if (!Lua::Interrupt(force)) - { - con.printerr( - "Failed to register hook. This can happen if you have" - " lua profiling or coverage monitoring enabled. Use" - " 'kill-lua force' to force, but this may disable" - " profiling and coverage monitoring.\n"); - } + return Commands::kill_lua(con, *this, first, parts); } else if (first == "script") { - if(parts.size() == 1) - { - loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); - } - else - { - con << "Usage:" << std::endl - << " script " << std::endl; - return CR_WRONG_USAGE; - } + return Commands::script(con, *this, first, parts); } else if (first == "hide") { - if (!getConsole().hide()) - { - con.printerr("Could not hide console\n"); - return CR_FAILURE; - } - return CR_OK; + return Commands::hide(con, *this, first, parts); } else if (first == "show") { - if (!getConsole().show()) - { - con.printerr("Could not show console\n"); - return CR_FAILURE; - } - return CR_OK; + return Commands::show(con, *this, first, parts); } else if (first == "sc-script") { - if (parts.empty() || parts[0] == "help" || parts[0] == "?") - { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; - con << "Valid event names (SC_ prefix is optional):" << std::endl; - for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) - { - std::string name = sc_event_name((state_change_event)i); - if (name != "SC_UNKNOWN") - con << " " << name << std::endl; - } - return CR_OK; - } - else if (parts[0] == "list") - { - if(parts.size() < 2) - parts.push_back(""); - if (parts[1].size() && sc_event_id(parts[1]) == SC_UNKNOWN) - { - con << "Unrecognized event name: " << parts[1] << std::endl; - return CR_WRONG_USAGE; - } - for (const auto& state_script : state_change_scripts) - { - if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) - { - con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), - state_script.save_specific ? "save-specific" : "global", - state_script.save_specific ? "/raw/" : "/", - state_script.path.c_str()); - } - } - return CR_OK; - } - else if (parts[0] == "add") - { - if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) - { - con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; - return CR_WRONG_USAGE; - } - state_change_event evt = sc_event_id(parts[1]); - if (evt == SC_UNKNOWN) - { - con << "Unrecognized event: " << parts[1] << std::endl; - return CR_FAILURE; - } - bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); - StateChangeScript script(evt, parts[2], save_specific); - for (const auto& state_script : state_change_scripts) - { - if (script == state_script) - { - con << "Script already registered" << std::endl; - return CR_FAILURE; - } - } - state_change_scripts.push_back(script); - return CR_OK; - } - else if (parts[0] == "remove") - { - if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) - { - con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; - return CR_WRONG_USAGE; - } - state_change_event evt = sc_event_id(parts[1]); - if (evt == SC_UNKNOWN) - { - con << "Unrecognized event: " << parts[1] << std::endl; - return CR_FAILURE; - } - bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); - StateChangeScript tmp(evt, parts[2], save_specific); - auto it = std::find(state_change_scripts.begin(), state_change_scripts.end(), tmp); - if (it != state_change_scripts.end()) - { - state_change_scripts.erase(it); - return CR_OK; - } - else - { - con << "Unrecognized script" << std::endl; - return CR_FAILURE; - } - } - else - { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; - return CR_WRONG_USAGE; - } + return Commands::sc_script(con, *this, first, parts); } else if (first == "devel/dump-rpc") { - if (parts.size() == 1) - { - std::ofstream file(parts[0]); - CoreService core; - core.dumpMethods(file); - - for (auto & it : *plug_mgr) - { - Plugin * plug = it.second; - if (!plug) - continue; - - std::unique_ptr svc(plug->rpc_connect(con)); - if (!svc) - continue; - - file << "// Plugin: " << plug->getName() << std::endl; - svc->dumpMethods(file); - } - } - else - { - con << "Usage: devel/dump-rpc \"filename\"" << std::endl; - return CR_WRONG_USAGE; - } + return Commands::dump_rpc(con, *this, first, parts); } else if (RunAlias(con, first, parts, res)) { diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f5768c0ffa3..d8cd23c480c 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -122,6 +122,9 @@ namespace DFHack virtual bool is_console() { return false; } virtual color_ostream *proxy_target() { return NULL; } + virtual bool can_clear() const { return false; } + virtual void clear() {} + static bool log_errors_to_stderr; }; diff --git a/library/include/Commands.h b/library/include/Commands.h index 984b34a637f..163d2e164f4 100644 --- a/library/include/Commands.h +++ b/library/include/Commands.h @@ -19,5 +19,12 @@ namespace DFHack command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); command_result fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); } } diff --git a/library/include/Console.h b/library/include/Console.h index 4397c759545..cbf7d585119 100644 --- a/library/include/Console.h +++ b/library/include/Console.h @@ -141,8 +141,9 @@ namespace DFHack /// shutdown the console. NOT thread-safe bool shutdown( void ); + bool can_clear() const { return true; } /// Clear the console, along with its scrollback - void clear(); + void clear() override; /// Position cursor at x,y. 1,1 = top left corner void gotoxy(int x, int y); /// Enable or disable the caret/cursor diff --git a/library/include/Core.h b/library/include/Core.h index 649fc9fea24..da44fea04ad 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -227,6 +227,27 @@ namespace DFHack static command_result enableLuaScript(color_ostream& out, const std::string_view name, bool enabled); + const std::vector getStateChangeScripts() const + { + return state_change_scripts; + } + + void addStateChangeScript(const StateChangeScript& script) + { + state_change_scripts.push_back(script); + } + + bool removeStateChangeScript(const StateChangeScript& script) + { + auto it = std::find(state_change_scripts.begin(), state_change_scripts.end(), script); + if (it != state_change_scripts.end()) + { + state_change_scripts.erase(it); + return true; + } + return false; + } + private: DFHack::Console con; @@ -510,4 +531,7 @@ namespace DFHack void help_helper(color_ostream& con, const std::string& entry_name); std::string dfhack_version_desc(); bool is_builtin(color_ostream& con, const std::string& command); + std::string sc_event_name(state_change_event id); + state_change_event sc_event_id(std::string name); + } diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h index 8a5bee86f74..bade25b5219 100644 --- a/library/include/RemoteServer.h +++ b/library/include/RemoteServer.h @@ -211,6 +211,7 @@ namespace DFHack functions.push_back(new VoidServerMethod(this, name, flags, fptr)); } + public: void dumpMethods(std::ostream & out) const; }; From b5744f3b0c11f7a2fcb27f3fca12c3e5900c50d2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 18:13:14 -0600 Subject: [PATCH 641/919] add missing `` --- library/include/Core.h | 1 + 1 file changed, 1 insertion(+) diff --git a/library/include/Core.h b/library/include/Core.h index da44fea04ad..5b85d972520 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -31,6 +31,7 @@ distribution. #include "modules/Graphic.h" +#include #include #include #include From 5f3314bba8e73fd16a1b76b66f09b2eeee50cd10 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 18:22:19 -0600 Subject: [PATCH 642/919] add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a4..ba30756d5b9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -80,6 +80,7 @@ Template for new versions: - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements +-- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation From 017d569edee4c0f1b019b589f1350caa3f6cc38b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 18:03:45 -0600 Subject: [PATCH 643/919] move changelog to correct section --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ba30756d5b9..f7d146dc456 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation @@ -80,7 +81,6 @@ Template for new versions: - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements --- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation From b89d747492fcc7eb33970f0605cedac5e58d54cc Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Tue, 25 Nov 2025 11:01:46 -0500 Subject: [PATCH 644/919] Cleanup pass --- .gitignore | 2 +- docs/builtins/keybinding.rst | 3 ++- docs/changelog.txt | 1 + library/include/Core.h | 1 - library/modules/Hotkey.cpp | 26 ++++++++++++++------------ 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 6142448b7a4..60a0e1b6b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # linux backup files *~ -.cache + # Kdevelop project files *.kdev4 .kdev4 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index f77a0816507..e8f206848d3 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -32,7 +32,8 @@ The ```` parameter above has the following case-insensitive syntax:: [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote -optional parts. +optional parts. It is important to note that the key is the non-shifted version +of the key. For example ``!`` would be defined as ``Shift-0``. DFHack commands can advertise the contexts in which they can be usefully run. For example, a command that acts on a selected unit can tell `keybinding` that diff --git a/docs/changelog.txt b/docs/changelog.txt index 6cfa16e4bf2..05ebf4907bd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. ## Documentation diff --git a/library/include/Core.h b/library/include/Core.h index bf0378b05dd..c250eef2e2f 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -270,7 +270,6 @@ namespace DFHack // Hotkey Manager DFHack::HotkeyManager *hotkey_mgr; - // FIXME: remove all this junk (hotkey related) std::vector script_paths[3]; std::mutex script_path_mutex; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index eb7b483af07..d6c72e60b92 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -1,5 +1,8 @@ #include "modules/Hotkey.h" +#include +#include + #include "Core.h" #include "ColorText.h" #include "MiscUtils.h" @@ -12,8 +15,6 @@ #include "df/viewscreen.h" #include "df/interfacest.h" -#include -#include using namespace DFHack; using Hotkey::KeySpec; @@ -103,7 +104,7 @@ std::optional KeySpec::parse(std::string spec, std::string* err) { // Attempt to parse as a mouse binding if (spec.starts_with("mouse")) { spec.erase(0, 5); - // Read button number, ensuring between 1 and 15 inclusive + // Read button number, ensuring between 4 and 15 inclusive try { int mbutton = std::stoi(spec); if (mbutton >= 4 && mbutton <= 15) { @@ -126,14 +127,15 @@ std::optional KeySpec::parse(std::string spec, std::string* err) { } bool KeySpec::isDisruptive() const { - // Miscellaneous essential keys + // SDLK enum uses the actual characters for a key as its value. + // Escaped values included are Return, Escape, Backspace, and Tab const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys - bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) - || (this->sym >= SDLK_0 && this->sym <= SDLK_9) + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 || essential_key_set.find(this->sym) != std::string::npos - || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys // Essential keys are safe, so long as they have a modifier that isn't Shift if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) @@ -306,11 +308,12 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { if (!df::global::gview || !df::global::plotinfo) return false; - // Get bottommost active screen + // Get topmost active screen df::viewscreen *screen = &df::global::gview->view; while (screen->child) screen = screen->child; + // Map keypad return to return if (sym == SDLK_KP_ENTER) sym = SDLK_RETURN; @@ -333,6 +336,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { auto& core = Core::getInstance(); bool mortal_mode = core.getMortalMode(); + // Iterate in reverse, prioritizing the last added keybinds for (const auto& bind : binds | std::views::reverse) { if (bind.spec.modifiers != modifiers) continue; @@ -340,9 +344,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { if (!bind.spec.focus.empty()) { bool matched = false; for (const auto& focus : bind.spec.focus) { - printf("Focus check for: %s", focus.c_str()); if (Gui::matchFocusString(focus)) { - printf("Matched\n"); matched = true; break; } @@ -351,7 +353,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { continue; } - if (!Core::getInstance().getPluginManager()->CanInvokeHotkey(bind.command, screen)) + if (!core.getPluginManager()->CanInvokeHotkey(bind.command, screen)) continue; if (mortal_mode && core.isArmokTool(bind.command)) @@ -435,7 +437,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl << "Context may be used to limit the scope of the binding, by" << std::endl << "requiring the current context to have a certain prefix." << std::endl << "Current UI context is: " << std::endl From f0e98b5ab51cf5712670f8d995b7b50fb1309c00 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Tue, 25 Nov 2025 12:10:31 -0500 Subject: [PATCH 645/919] Address tidy checks --- library/include/modules/Hotkey.h | 10 +++---- library/modules/Hotkey.cpp | 50 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 2e9a400cd11..78978f29f6b 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -39,8 +39,8 @@ namespace DFHack { ~HotkeyManager(); - bool addKeybind(std::string keyspec, std::string cmd); - bool addKeybind(Hotkey::KeySpec spec, std::string cmd); + bool addKeybind(std::string keyspec, std::string_view cmd); + bool addKeybind(Hotkey::KeySpec spec, std::string_view cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); @@ -62,13 +62,13 @@ namespace DFHack { private: std::thread hotkey_thread; - std::mutex lock {}; - std::condition_variable cond {}; + std::mutex lock; + std::condition_variable cond; bool keybind_save_requested = false; std::string requested_keybind; - int hotkey_sig = 0; + uint8_t hotkey_sig = 0; std::string queued_command; std::map> bindings; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index d6c72e60b92..d7ef91ed107 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -20,20 +20,20 @@ using namespace DFHack; using Hotkey::KeySpec; using Hotkey::KeyBinding; -enum HotkeySignal { +enum HotkeySignal : uint8_t { None = 0, CmdReady, Shutdown, }; -bool operator==(const KeySpec& a, const KeySpec& b) { +static bool operator==(const KeySpec& a, const KeySpec& b) { return a.modifiers == b.modifiers && a.sym == b.sym && a.focus.size() == b.focus.size() && std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); } // Equality operator for key bindings -bool operator==(const KeyBinding& a, const KeyBinding& b) { +static bool operator==(const KeyBinding& a, const KeyBinding& b) { return a.spec == b.spec && a.command == b.command && a.cmdline == b.cmdline; @@ -134,7 +134,7 @@ bool KeySpec::isDisruptive() const { // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 - || essential_key_set.find(this->sym) != std::string::npos + || (this->sym < CHAR_MAX && essential_key_set.find((char)this->sym) != std::string::npos) || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys // Essential keys are safe, so long as they have a modifier that isn't Shift @@ -173,13 +173,13 @@ void HotkeyManager::hotkey_thread_fn() { } -bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { +bool HotkeyManager::addKeybind(KeySpec spec, std::string_view cmd) { // No point in a hotkey with no action if (cmd.empty()) return false; KeyBinding binding; - binding.spec = spec; + binding.spec = std::move(spec); binding.cmdline = cmd; size_t space_idx = cmd.find(' '); binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); @@ -196,8 +196,8 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { return true; } -bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = KeySpec::parse(keyspec); +bool HotkeyManager::addKeybind(std::string keyspec, std::string_view cmd) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -223,7 +223,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st } bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { - std::optional spec_opt = KeySpec::parse(keyspec); + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return false; return this->removeKeybind(spec_opt.value(), match_focus, cmdline); @@ -261,7 +261,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { std::vector HotkeyManager::listKeybinds(std::string keyspec) { std::lock_guard l(lock); - std::optional spec_opt = KeySpec::parse(keyspec); + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -371,7 +371,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { void HotkeyManager::setHotkeyCommand(std::string cmd) { std::unique_lock l(lock); - queued_command = cmd; + queued_command = std::move(cmd); hotkey_sig = HotkeySignal::CmdReady; l.unlock(); cond.notify_all(); @@ -391,7 +391,7 @@ std::string HotkeyManager::getKeybindingInput() { void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { std::string parse_error; if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { - std::string keystr = parts[1]; + const std::string& keystr = parts[1]; if (parts[0] == "set") removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { @@ -426,22 +426,22 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } std::vector list = listKeybinds(spec.value()); if (list.empty()) - con << "No bindings." << std::endl; + con << "No bindings.\n"; for (const auto& kb : list) - con << " " << kb << std::endl; + con << " " << kb << "\n"; } else { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" + << join_strings("\n", Gui::getCurFocus(true)) << "\n"; } } From d8316fc68a8a237c95e21660edba67a2b2f59be4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 23:30:42 -0600 Subject: [PATCH 646/919] add fmtlib adds fmtlib as a dependency adds `format` and `format_err` template functions to `color_ostream` --- CMakeLists.txt | 8 ++++++++ library/CMakeLists.txt | 4 ++-- library/include/ColorText.h | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e78a562626f..e8fe082b7fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,6 +347,14 @@ if(BUILD_LIBRARY) endif() endif() +INCLUDE(FetchContent) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG master +) +FetchContent_MakeAvailable(fmt) + if(APPLE) # libstdc++ (GCC 4.8.5 for OS X 10.6) # fixes crash-on-unwind bug in DF's libstdc++ diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf62..48e2af51995 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -320,12 +320,12 @@ if(UNIX) endif() if(APPLE) - set(PROJECT_LIBS dl dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS dl dfhack-md5 fmt ${DFHACK_TINYXML}) elseif(UNIX) set(PROJECT_LIBS rt dl dfhack-md5 ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? - set(PROJECT_LIBS psapi dbghelp dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS psapi dbghelp dfhack-md5 fmt ${DFHACK_TINYXML}) endif() set(VERSION_SRCS DFHackVersion.cpp) diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f5768c0ffa3..05e38363bdc 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -33,6 +33,8 @@ distribution. #include #include +#include + namespace dfproto { class CoreTextNotification; @@ -104,6 +106,25 @@ namespace DFHack color_ostream(); virtual ~color_ostream(); + template + void format(const char* format, Args&& ... args) + { + auto str = fmt::format(format, std::forward(args)...); + flush_buffer(false); + add_text(cur_color, str); + } + + template + void format_err(const char* format, Args&& ... args) + { + auto str = fmt::format(format, std::forward(args)...); + if (log_errors_to_stderr) { + std::cerr << str; + } + flush_buffer(false); + add_text(COLOR_LIGHTRED, str); + } + /// Print a formatted string, like printf void print(const char *format, ...) Wformat(printf,2,3); void vprint(const char *format, va_list args) Wformat(printf,2,0); From 4859b8dc7ca7a1464f60c9807152b71a44db6e0d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 22 Nov 2025 23:34:04 -0600 Subject: [PATCH 647/919] forgot to add `fmt` to "UNIX" lol --- library/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 48e2af51995..05f6d2166c1 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -322,7 +322,7 @@ endif() if(APPLE) set(PROJECT_LIBS dl dfhack-md5 fmt ${DFHACK_TINYXML}) elseif(UNIX) - set(PROJECT_LIBS rt dl dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS rt dl dfhack-md5 fmt ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? set(PROJECT_LIBS psapi dbghelp dfhack-md5 fmt ${DFHACK_TINYXML}) From c6eaf6df724387b6de63f846204994196fc42b62 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 00:13:36 -0600 Subject: [PATCH 648/919] add `fmt` to library lists for `dfhack-client` and plugins --- library/CMakeLists.txt | 2 +- plugins/Plugins.cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 05f6d2166c1..6d1d46141d5 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -420,7 +420,7 @@ endif() target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) set_target_properties(dfhack PROPERTIES INTERFACE_LINK_LIBRARIES "") -target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static) +target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static fmt) if(WIN32) target_link_libraries(dfhack-client dbghelp) endif() diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 3726b86c7c4..107f9a80682 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -124,7 +124,7 @@ macro(dfhack_plugin) target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") target_link_libraries(${PLUGIN_NAME} protobuf-lite) endif() - target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${PLUGIN_LINK_LIBRARIES}) + target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version fmt ${PLUGIN_LINK_LIBRARIES}) if(UNIX) set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") From 3ff667ff9493802ef9a083ed06ab85c03e7ba8d4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 00:19:53 -0600 Subject: [PATCH 649/919] missed one? --- library/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 6d1d46141d5..471d2f808aa 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -391,7 +391,7 @@ else() set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "-include Export.h" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "-include Export.h" ) add_library(dfhooks_dfhack SHARED Hooks.cpp) - target_link_libraries(dfhooks_dfhack dfhack) + target_link_libraries(dfhooks_dfhack dfhack fmt) endif() # effectively disables debug builds... From bc9224dcbcb263fb1e1c0a8f9cb3b78fad390e43 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 17:40:03 -0600 Subject: [PATCH 650/919] convert over to fmtlib This also adds custom formatters for `df::coord` and `df::coord2d` --- library/ColorText.cpp | 48 ---- library/Core.cpp | 146 +++++------ library/DataDefs.cpp | 4 +- library/Error.cpp | 5 +- library/LuaApi.cpp | 4 +- library/LuaTools.cpp | 34 +-- library/LuaTypes.cpp | 2 +- library/LuaWrapper.cpp | 13 +- library/PlugLoad.cpp | 4 +- library/PluginManager.cpp | 72 +++--- library/Process.cpp | 2 +- library/RemoteClient.cpp | 42 ++-- library/RemoteServer.cpp | 18 +- library/RemoteTools.cpp | 8 +- library/TileTypes.cpp | 37 ++- library/VTableInterpose.cpp | 2 +- library/VersionInfoFactory.cpp | 4 +- library/dfhack-run.cpp | 4 +- library/include/ColorText.h | 19 +- library/include/Core.h | 15 +- library/include/DataDefs.h | 26 +- library/modules/DFSteam.cpp | 8 +- library/modules/EventManager.cpp | 72 +++--- library/modules/Gui.cpp | 36 +-- library/modules/Items.cpp | 6 +- library/modules/Kitchen.cpp | 6 +- library/modules/Materials.cpp | 8 +- library/modules/Persistence.cpp | 4 +- library/modules/Screen.cpp | 4 +- library/modules/Textures.cpp | 25 +- library/modules/World.cpp | 8 +- plugins/3dveins.cpp | 78 +++--- plugins/add-spatter.cpp | 4 +- plugins/aquifer.cpp | 47 ++-- plugins/army-controller-sanity.cpp | 18 +- plugins/autobutcher.cpp | 56 ++--- plugins/autochop.cpp | 72 +++--- plugins/autoclothing.cpp | 14 +- plugins/autodump.cpp | 4 +- plugins/autofarm.cpp | 14 +- plugins/autogems.cpp | 6 +- plugins/autolabor/autolabor.cpp | 30 +-- plugins/autonestbox.cpp | 38 +-- plugins/autoslab.cpp | 16 +- plugins/blueprint.cpp | 10 +- plugins/buildingplan/buildingplan.cpp | 136 +++++----- plugins/buildingplan/buildingplan_cycle.cpp | 50 ++-- plugins/buildingplan/buildingtypekey.cpp | 2 +- plugins/buildingplan/buildingtypekey.h | 11 + plugins/buildingplan/defaultitemfilters.cpp | 20 +- plugins/buildingplan/itemfilter.cpp | 10 +- plugins/buildingplan/plannedbuilding.cpp | 8 +- plugins/burrow.cpp | 22 +- plugins/changeitem.cpp | 2 +- plugins/channel-safely/channel-groups.cpp | 20 +- plugins/channel-safely/channel-manager.cpp | 22 +- .../channel-safely/channel-safely-plugin.cpp | 42 ++-- plugins/channel-safely/include/inlines.h | 2 +- plugins/cleanconst.cpp | 4 +- plugins/cleaners.cpp | 8 +- plugins/cleanowned.cpp | 8 +- plugins/createitem.cpp | 10 +- plugins/cursecheck.cpp | 10 +- plugins/cxxrandom.cpp | 2 +- plugins/debug.cpp | 10 +- plugins/deramp.cpp | 4 +- plugins/devel/buildprobe.cpp | 2 +- .../check-structures-sanity/dispatch.cpp | 4 +- .../devel/check-structures-sanity/main.cpp | 2 +- .../devel/check-structures-sanity/types.cpp | 2 +- .../check-structures-sanity/validate.cpp | 4 +- plugins/devel/color-dfhack-text.cpp | 2 +- plugins/devel/counters.cpp | 2 +- plugins/devel/dumpmats.cpp | 94 +++---- plugins/devel/eventExample.cpp | 28 +-- plugins/devel/frozen.cpp | 4 +- plugins/devel/kittens.cpp | 22 +- plugins/devel/memutils.cpp | 4 +- plugins/devel/memview.cpp | 10 +- plugins/devel/onceExample.cpp | 2 +- plugins/devel/stepBetween.cpp | 2 +- plugins/devel/tilesieve.cpp | 2 +- plugins/devel/vectors.cpp | 8 +- plugins/devel/zoom.cpp | 2 +- plugins/dig-now.cpp | 23 +- plugins/dig.cpp | 40 +-- plugins/digFlood.cpp | 2 +- plugins/diggingInvaders/assignJob.cpp | 6 +- plugins/diggingInvaders/diggingInvaders.cpp | 16 +- plugins/dwarfvet.cpp | 14 +- plugins/embark-assistant/finder_ui.cpp | 10 +- plugins/embark-assistant/matcher.cpp | 234 +++++++++--------- .../examples/persistent_per_save_example.cpp | 22 +- plugins/examples/simple_command_example.cpp | 2 +- plugins/examples/skeleton.cpp | 10 +- plugins/examples/ui_addition_example.cpp | 4 +- plugins/fastdwarf.cpp | 28 +-- plugins/filltraffic.cpp | 2 +- plugins/fix-occupancy.cpp | 32 +-- plugins/fixveins.cpp | 4 +- plugins/flows.cpp | 10 +- plugins/follow.cpp | 2 +- plugins/forceequip.cpp | 38 +-- plugins/generated-creature-renamer.cpp | 4 +- plugins/getplants.cpp | 46 ++-- plugins/hotkeys.cpp | 18 +- plugins/infinite-sky.cpp | 19 +- plugins/jobutils.cpp | 30 +-- plugins/logistics.cpp | 46 ++-- plugins/misery.cpp | 18 +- plugins/nestboxes.cpp | 26 +- plugins/orders.cpp | 2 +- plugins/overlay.cpp | 4 +- plugins/pathable.cpp | 18 +- plugins/pet-uncapper.cpp | 22 +- plugins/plant.cpp | 58 ++--- plugins/preserve-rooms.cpp | 82 +++--- plugins/preserve-tombs.cpp | 36 +-- plugins/probe.cpp | 109 ++++---- plugins/prospector.cpp | 6 +- plugins/regrass.cpp | 46 ++-- .../remotefortressreader.cpp | 10 +- plugins/rendermax/renderer_light.cpp | 16 +- plugins/rendermax/rendermax.cpp | 2 +- plugins/seedwatch.cpp | 38 +-- plugins/showmood.cpp | 77 +++--- plugins/siege-engine.cpp | 2 +- plugins/sort.cpp | 8 +- plugins/spectate.cpp | 50 ++-- plugins/steam-engine.cpp | 2 +- plugins/stockflow.cpp | 4 +- plugins/stockpiles/OrganicMatLookup.cpp | 18 +- plugins/stockpiles/StockpileSerializer.cpp | 82 +++--- plugins/stockpiles/stockpiles.cpp | 30 +-- plugins/strangemood.cpp | 10 +- plugins/suspendmanager.cpp | 40 +-- plugins/tailor.cpp | 97 ++++---- plugins/tiletypes.cpp | 32 +-- plugins/timestream.cpp | 26 +- plugins/tubefill.cpp | 2 +- plugins/tweak/tweak.cpp | 12 +- plugins/work-now.cpp | 14 +- plugins/workflow.cpp | 34 +-- 143 files changed, 1686 insertions(+), 1701 deletions(-) diff --git a/library/ColorText.cpp b/library/ColorText.cpp index 8dc5cc5dcfe..bdd3e98f447 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -91,54 +91,6 @@ color_ostream::~color_ostream() delete buf(); } -void color_ostream::print(const char *format, ...) -{ - va_list args; - va_start(args, format); - vprint(format, args); - va_end(args); -} - -void color_ostream::vprint(const char *format, va_list args) -{ - std::string str = stl_vsprintf(format, args); - - if (!str.empty()) { - flush_buffer(false); - add_text(cur_color, str); - if (str[str.size()-1] == '\n') - flush_proxy(); - } -} - -void color_ostream::printerr(const char * format, ...) -{ - va_list args; - va_start(args, format); - vprinterr(format, args); - va_end(args); -} - -void color_ostream::vprinterr(const char *format, va_list args) -{ - color_value save = cur_color; - - if (log_errors_to_stderr) - { - va_list args1; - va_copy(args1, args); - vfprintf(stderr, format, args1); - va_end(args1); - } - - color(COLOR_LIGHTRED); - va_list args2; - va_copy(args2, args); - vprint(format, args2); - va_end(args2); - color(save); -} - void color_ostream::color(color_value c) { if (c == cur_color) diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6e..6960c12eb9d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -87,6 +87,8 @@ distribution. #include #include +#include + #ifdef _WIN32 #define NOMINMAX #include @@ -297,7 +299,7 @@ static void fHKthread(IODATA * iodata) auto rv = core->runCommand(out, stuff); if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); + out.printerr("Invalid hotkey command: '{}'\n", stuff); } } } @@ -444,7 +446,7 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: { completed = possible[0]; //fprintf(stderr, "Autocompleted %s to %s\n", , ); - con.printerr("%s is not recognized. Did you mean %s?\n", first.c_str(), completed.c_str()); + con.printerr("{} is not recognized. Did you mean {}?\n", first, completed); return true; } @@ -453,7 +455,7 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: std::string out; for (size_t i = 0; i < possible.size(); i++) out += " " + possible[i]; - con.printerr("%s is not recognized. Possible completions:%s\n", first.c_str(), out.c_str()); + con.printerr("{} is not recognized. Possible completions:{}\n", first, out); return true; } @@ -529,7 +531,7 @@ std::filesystem::path Core::findScript(std::string name) std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); if (ec) { - con.printerr("Error loading '%s' (%s)\n", (*it / name).string().c_str(), ec.message().c_str()); + con.printerr("Error loading '{}' ({})\n", *it / name, ec.message()); continue; } @@ -546,7 +548,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) if (!file) { if (!silent) - out.printerr("Could not load %s\n", filename.c_str()); + out.printerr("Could not load {}\n", filename); return false; } std::string raw; @@ -565,10 +567,10 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) if (ch == '+' || ch == '-') { if (!Core::getInstance().addScriptPath(path, ch == '+') && !silent) - out.printerr("%s:%i: Failed to add path: %s\n", filename.c_str(), line, path.c_str()); + out.printerr("{}:{}: Failed to add path: {}\n", filename, line, path); } else if (!silent) - out.printerr("%s:%i: Illegal character: %c\n", filename.c_str(), line, ch); + out.printerr("{}:{}: Illegal character: {}\n", filename, line, ch); } return true; } @@ -583,7 +585,7 @@ static void loadModScriptPaths(color_ostream &out) { DEBUG(script,out).print("final mod script paths:\n"); for (auto& path : mod_script_paths_str) { - DEBUG(script, out).print(" %s\n", path.c_str()); + DEBUG(script, out).print(" {}\n", path); mod_script_paths.push_back(std::filesystem::weakly_canonical(std::filesystem::path{ path })); } Core::getInstance().setModScriptPaths(mod_script_paths); @@ -725,8 +727,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s CommandDepthCounter counter; if (!counter.ok()) { - con.printerr("Cannot invoke \"%s\": maximum command depth exceeded (%i)\n", - first.c_str(), CommandDepthCounter::MAX_DEPTH); + con.printerr("Cannot invoke \"{}\": maximum command depth exceeded ({})\n", + first, CommandDepthCounter::MAX_DEPTH); return CR_FAILURE; } @@ -735,7 +737,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (has_backslashes(first)) { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", first.c_str()); + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", first); replace_backslashes_with_forwardslashes(first); } @@ -769,7 +771,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s "See more commands by running 'ls'.\n\n" ); - con.print("DFHack version %s\n", dfhack_version_desc().c_str()); + con.print("DFHack version {}\n", dfhack_version_desc()); } else { @@ -821,11 +823,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } } if (ret != CR_OK) - con.printerr("%s failed\n", first.c_str()); + con.printerr("{} failed\n", first); return ret; } else { - con.printerr("%s: no arguments\n", first.c_str()); + con.printerr("{}: no arguments\n", first); return CR_FAILURE; } } @@ -840,7 +842,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if (has_backslashes(part)) { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", part); replace_backslashes_with_forwardslashes(part); } @@ -858,20 +860,20 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else { res = CR_NOT_FOUND; - con.printerr("No such plugin or Lua script: %s\n", part.c_str()); + con.printerr("No such plugin or Lua script: {}\n", part); } } else if (!plug->can_set_enabled()) { res = CR_NOT_IMPLEMENTED; - con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); + con.printerr("Cannot {} plugin: {}\n", first, part); } else { res = plug->set_enabled(con, enable); if (res != CR_OK || plug->is_enabled() != enable) - con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); + con.printerr("Could not {} plugin: {}\n", first, part); } } @@ -886,8 +888,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s if (!plug->can_be_enabled()) continue; con.print( - "%21s %-3s%s\n", - (key+":").c_str(), + "{:21} {:<3}{}\n", + (key+":"), plug->is_enabled() ? "on" : "off", plug->can_set_enabled() ? "" : " (controlled internally)" ); @@ -995,7 +997,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { if (!AddKeyBinding(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); + con.printerr("Invalid key spec: {}\n", keystr); break; } } @@ -1006,7 +1008,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s for (const auto& part : parts | std::views::drop(1)) { if (!ClearKeyBindings(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); + con.printerr("Invalid key spec: {}\n", part); break; } } @@ -1042,7 +1044,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s std::vector cmd(parts.begin() + 2, parts.end()); if (!AddAlias(name, cmd, parts[0] == "replace")) { - con.printerr("Could not add alias %s - already exists\n", name.c_str()); + con.printerr("Could not add alias {} - already exists\n", name); return CR_FAILURE; } } @@ -1050,7 +1052,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if (!RemoveAlias(parts[1])) { - con.printerr("Could not remove alias %s\n", parts[1].c_str()); + con.printerr("Could not remove alias {}\n", parts[1]); return CR_FAILURE; } } @@ -1174,10 +1176,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) { - con.print("%s (%s): %s%s\n", sc_event_name(state_script.event).c_str(), + con.print("{} ({}): {}{}\n", sc_event_name(state_script.event), state_script.save_specific ? "save-specific" : "global", state_script.save_specific ? "/raw/" : "/", - state_script.path.c_str()); + state_script.path); } } return CR_OK; @@ -1290,26 +1292,26 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s else if (!no_autocomplete && try_autocomplete(con, first, completed)) res = CR_NOT_IMPLEMENTED; else - con.printerr("%s is not a recognized command.\n", first.c_str()); + con.printerr("{} is not a recognized command.\n", first); if (res == CR_NOT_IMPLEMENTED) { Plugin *p = plug_mgr->getPluginByName(first); if (p) { - con.printerr("%s is a plugin ", first.c_str()); + con.printerr("{} is a plugin ", first); if (p->getState() == Plugin::PS_UNLOADED) - con.printerr("that is not loaded - try \"load %s\" or check stderr.log\n", - first.c_str()); + con.printerr("that is not loaded - try \"load {}\" or check stderr.log\n", + first); else if (p->size()) - con.printerr("that implements %zi commands - see \"help %s\" for details\n", - p->size(), first.c_str()); + con.printerr("that implements {} commands - see \"help {}\" for details\n", + p->size(), first); else con.printerr("but does not implement any commands\n"); } } } else if (res == CR_NEEDS_CONSOLE) - con.printerr("%s needs an interactive console to work.\n" + con.printerr("{} needs an interactive console to work.\n" "Please run this command from the DFHack console.\n\n" #ifdef WIN32 "You can show the console with the 'show' command." @@ -1317,7 +1319,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s "The console is accessible when you run DF from the commandline\n" "via the './dfhack' script." #endif - "\n", first.c_str()); + "\n", first); return res; } @@ -1334,7 +1336,7 @@ bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool if ( !script ) { if(!silent) - out.printerr("Error loading script: %s\n", fname.string().c_str()); + out.printerr("Error loading script: {}\n", fname); return false; } std::string command; @@ -1418,9 +1420,9 @@ static void fIOthread(IODATA * iod) run_dfhack_init(con, core); con.print("DFHack is ready. Have a nice day!\n" - "DFHack version %s\n" + "DFHack version {}\n" "Type in '?' or 'help' for general help, 'ls' to see all commands.\n", - dfhack_version_desc().c_str()); + dfhack_version_desc()); int clueless_counter = 0; @@ -1507,11 +1509,11 @@ void Core::fatal (std::string output, const char * title) out << "DFHack will now deactivate.\n"; if(con.isInited()) { - con.printerr("%s", out.str().c_str()); + con.printerr("{}", out.str()); con.reset_color(); con.print("\n"); } - fprintf(stderr, "%s\n", out.str().c_str()); + fmt::print(stderr, "{}\n", out.str()); out << "Check file stderr.log for details.\n"; std::cout << "DFHack fatal error: " << out.str() << std::endl; if (!title) @@ -1771,15 +1773,15 @@ bool Core::InitSimulationThread() // create config directory if it doesn't already exist if (!Filesystem::mkdir_recursive(getConfigPath())) - con.printerr("Failed to create config directory: '%s'\n", getConfigPath().c_str()); + con.printerr("Failed to create config directory: '{}'\n", getConfigPath()); // copy over default config files if necessary std::map config_files; std::map default_config_files; if (Filesystem::listdir_recursive(getConfigPath(), config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", getConfigPath().c_str()); + con.printerr("Failed to list directory: '{}'\n", getConfigPath()); else if (Filesystem::listdir_recursive(getConfigDefaultsPath(), default_config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", getConfigDefaultsPath().c_str()); + con.printerr("Failed to list directory: '{}'\n", getConfigDefaultsPath()); else { // ensure all config file directories exist before we start copying files @@ -1789,7 +1791,7 @@ bool Core::InitSimulationThread() continue; std::filesystem::path dirname = getConfigPath() / entry.first; if (!Filesystem::mkdir_recursive(dirname)) - con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); + con.printerr("Failed to create config directory: '{}'\n", dirname); } // copy files from the default tree that don't already exist in the config tree @@ -1806,7 +1808,7 @@ bool Core::InitSimulationThread() std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); if (!src.good() || !dest.good()) { - con.printerr("Copy failed: '%s'\n", filename.c_str()); + con.printerr("Copy failed: '{}'\n", filename); continue; } dest << src.rdbuf(); @@ -1959,26 +1961,6 @@ std::string Core::getHotkeyCmd( bool &keep_going ) return returner; } -void Core::print(const char *format, ...) -{ - color_ostream_proxy proxy(getInstance().con); - - va_list args; - va_start(args,format); - proxy.vprint(format,args); - va_end(args); -} - -void Core::printerr(const char *format, ...) -{ - color_ostream_proxy proxy(getInstance().con); - - va_list args; - va_start(args,format); - proxy.vprinterr(format,args); - va_end(args); -} - Core& Core::getInstance() { static Core instance; return instance; @@ -2323,7 +2305,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) if (evtlog.fail()) { if (DFHack::Filesystem::isdir(save_dir)) - out.printerr("Could not append to %s\n", evtlogpath.c_str()); + out.printerr("Could not append to {}\n", evtlogpath); } else { @@ -2484,8 +2466,8 @@ bool Core::getSuppressDuplicateKeyboardEvents() const { } void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { - DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to %s\n", - suppress ? "true" : "false"); + DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to {}\n", + suppress); suppress_duplicate_keyboard_events = suppress; } @@ -2539,26 +2521,26 @@ bool Core::doSdlInputEvent(SDL_Event* ev) else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress - DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); + DEBUG(keybinding).print("key down: sym={}, char={}\n", sym, static_cast(sym)); if (SelectHotkey(sym, modstate)) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); return false; } - DEBUG(keybinding).print("%sinhibiting SDL key down event\n", + DEBUG(keybinding).print("{}inhibiting SDL key down event\n", suppress_duplicate_keyboard_events ? "" : "not "); return suppress_duplicate_keyboard_events; } } else if (ke.state == SDL_RELEASED) { - DEBUG(keybinding).print("key up: sym=%d (%c)\n", sym, sym); + DEBUG(keybinding).print("key up: sym={}, char={}\n", sym, static_cast(sym)); hotkey_states[sym] = false; } } else if (ev->type == SDL_MOUSEBUTTONDOWN) { auto &but = ev->button; - DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); + DEBUG(keybinding).print("mouse button down: button={}\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { SDL_Keycode sym = SDLK_F13 + but.button - 4; @@ -2567,13 +2549,13 @@ bool Core::doSdlInputEvent(SDL_Event* ev) } } else if (ev->type == SDL_TEXTINPUT) { auto &te = ev->text; - DEBUG(keybinding).print("text input: '%s' (modifiers: %s%s%s)\n", + DEBUG(keybinding).print("text input: '{}' (modifiers: {}{}{})\n", te.text, modstate & DFH_MOD_SHIFT ? "Shift" : "", modstate & DFH_MOD_CTRL ? "Ctrl" : "", modstate & DFH_MOD_ALT ? "Alt" : ""); if (strlen(te.text) == 1 && hotkey_states[te.text[0]]) { - DEBUG(keybinding).print("%sinhibiting SDL text event\n", + DEBUG(keybinding).print("{}inhibiting SDL text event\n", suppress_duplicate_keyboard_events ? "" : "not "); return suppress_duplicate_keyboard_events; } @@ -2597,7 +2579,7 @@ bool Core::SelectHotkey(int sym, int modifiers) std::string cmd; - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); + DEBUG(keybinding).print("checking hotkeys for sym={}, char={}, modifiers={:x}\n", sym, static_cast(sym), modifiers); { std::lock_guard lock(HotkeyMutex); @@ -2606,29 +2588,29 @@ bool Core::SelectHotkey(int sym, int modifiers) std::vector &bindings = key_bindings[sym]; //for (int i = bindings.size()-1; i >= 0; --i) { for (const auto& binding : bindings | std::views::reverse) { - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); + DEBUG(keybinding).print("examining hotkey with commandline: '{}'\n", binding.cmdline); if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", + DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x{:x} != 0x{:x}\n", binding.modifiers, modifiers); continue; } if (!binding.focus.empty()) { if (!Gui::matchFocusString(binding.focus)) { std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); + DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '{}' != '{}'\n", + join_strings(", ", focusStrings), binding.focus); continue; } } if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); + DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '{}')\n", + binding.command[0]); continue; } if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); + DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '{}')\n", + binding.command[0]); continue; } diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 5c187290f7e..7a9ae4ae358 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -262,7 +262,7 @@ const std::string bit_container_identity::getFullName(const type_identity *) con const std::string df::buffer_container_identity::getFullName(const type_identity *item) const { return (item ? item->getFullName() : std::string("void")) + - (size > 0 ? stl_sprintf("[%d]", size) : std::string("[]")); + (size > 0 ? std::format("[{}]", size) : std::string("[]")); } union_identity::union_identity(size_t size, const TAllocateFn alloc, @@ -482,7 +482,7 @@ void DFHack::bitfieldToString(std::vector *pvec, const void *p, std::string name = format_key(items[i].name, i); if (items[i].size > 1) - name += stl_sprintf("=%u", value); + name += fmt::format("={}", value); pvec->push_back(name); } diff --git a/library/Error.cpp b/library/Error.cpp index 3cd0eb31d6c..fa32daf90e3 100644 --- a/library/Error.cpp +++ b/library/Error.cpp @@ -1,6 +1,9 @@ #include "Error.h" #include "MiscUtils.h" +#include +#include + using namespace DFHack::Error; inline std::string safe_str(const char *s) @@ -24,7 +27,7 @@ VTableMissing::VTableMissing(const char *name) {} SymbolsXmlParse::SymbolsXmlParse(const char* desc, int id, int row, int col) - :AllSymbols(stl_sprintf("error %d: %s, at row %d col %d", id, desc, row, col)), + :AllSymbols(fmt::format("error {}: {}, at row {} col {}", id, desc, row, col)), desc(safe_str(desc)), id(id), row(row), col(col) {} diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4edf60000af..36e47f7c1bc 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3524,7 +3524,7 @@ static void setPreferredNumberFormat(color_ostream & out, int32_t type_int) { set_preferred_number_format_type(type); break; default: - WARN(luaapi, out).print("invalid number format enum value: %d\n", type_int); + WARN(luaapi, out).print("invalid number format enum value: {}\n", type_int); } } @@ -3608,7 +3608,7 @@ static int internal_setAddress(lua_State *L) // Print via printerr, so that it is definitely logged to stderr.log. uintptr_t iaddr = addr - Core::getInstance().vinfo->getRebaseDelta(); - fprintf(stderr, "Setting global '%s' to %p (%p)\n", name.c_str(), (void*)addr, (void*)iaddr); + fmt::print(stderr, "Setting global '{}' to {} ({})\n", name, (addr), (void*)iaddr); fflush(stderr); return 1; diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 81bcdd50ca3..e6083b0a4e5 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -244,12 +244,12 @@ static void check_valid_ptr_index(lua_State *state, int val_index) } } -static void signal_typeid_error(color_ostream *out, lua_State *state, - const type_identity *type, const char *msg, +static void signal_typeid_error(color_ostream* out, lua_State* state, + const type_identity* type, std::string_view msg, int val_index, bool perr, bool signal) { std::string typestr = type ? type->getFullName() : "any pointer"; - std::string error = stl_sprintf(msg, typestr.c_str()); + std::string error = fmt::format(fmt::runtime(msg), typestr); if (signal) { @@ -261,7 +261,7 @@ static void signal_typeid_error(color_ostream *out, lua_State *state, else if (perr) { if (out) - out->printerr("%s", error.c_str()); + out->printerr("{}", error); else dfhack_printerr(state, error); } @@ -282,7 +282,7 @@ void *DFHack::Lua::CheckDFObject(lua_State *state, const type_identity *type, in void *rv = get_object_internal(state, type, val_index, exact_type, false); if (!rv) - signal_typeid_error(NULL, state, type, "invalid pointer type; expected: %s", + signal_typeid_error(NULL, state, type, "invalid pointer type; expected: {}", val_index, false, true); return rv; @@ -366,9 +366,9 @@ static int lua_dfhack_print(lua_State *S) { std::string str = lua_print_fmt(S); if (color_ostream *out = Lua::GetOutput(S)) - out->print("%s", str.c_str());//*out << str; + out->print("{}", str); else - Core::print("%s", str.c_str()); + Core::print("{}", str); return 0; } @@ -378,16 +378,16 @@ static int lua_dfhack_println(lua_State *S) if (color_ostream *out = Lua::GetOutput(S)) *out << str << std::endl; else - Core::print("%s\n", str.c_str()); + Core::print("{}\n", str); return 0; } void dfhack_printerr(lua_State *S, const std::string &str) { if (color_ostream *out = Lua::GetOutput(S)) - out->printerr("%s\n", str.c_str()); + out->printerr("{}\n", str); else - Core::printerr("%s\n", str.c_str()); + Core::printerr("{}\n", str); } static int lua_dfhack_printerr(lua_State *S) @@ -563,7 +563,7 @@ static void report_error(lua_State *L, color_ostream *out = NULL, bool pop = fal assert(msg); if (out) - out->printerr("%s\n", msg); + out->printerr("{}\n", msg); else dfhack_printerr(L, msg); @@ -842,7 +842,7 @@ bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, if (!lua_checkstack(L, 1 + nargs) || !Lua::PushModulePublic(out, L, module_name, fn_name)) { if (perr) - out.printerr("Failed to load %s Lua code\n", module_name); + out.printerr("Failed to load {} Lua code\n", module_name); return false; } @@ -850,7 +850,7 @@ bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, if (!Lua::SafeCall(out, L, nargs, nres, perr)) { if (perr) - out.printerr("Failed Lua call to '%s.%s'\n", module_name, fn_name); + out.printerr("Failed Lua call to '{}.{}'\n", module_name, fn_name); return false; } @@ -1100,7 +1100,7 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, } else if (!lua_isuserdata(state, val_index)) { - signal_typeid_error(out, state, type, "pointer to %s expected", + signal_typeid_error(out, state, type, "pointer to {} expected", val_index, perr, signal); return false; } @@ -1109,13 +1109,13 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, void *in_ptr = Lua::GetDFObject(state, type, val_index, exact); if (!in_ptr) { - signal_typeid_error(out, state, type, "incompatible pointer type: %s expected", + signal_typeid_error(out, state, type, "incompatible pointer type: {} expected", val_index, perr, signal); return false; } if (!type->copy(target, in_ptr)) { - signal_typeid_error(out, state, type, "no copy support for %s", + signal_typeid_error(out, state, type, "no copy support for {}", val_index, perr, signal); return false; } @@ -2134,7 +2134,7 @@ void DFHack::Lua::Core::Reset(color_ostream &out, const char *where) if (top != 0) { - out.printerr("Common lua context stack top left at %d after %s.\n", top, where); + out.printerr("Common lua context stack top left at {} after {}.\n", top, where); lua_settop(State, 0); } diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index cdacd5ebfeb..0f990a6b844 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -1253,7 +1253,7 @@ int LuaWrapper::method_wrapper_core(lua_State *state, function_identity_base *id field_error(state, UPVAL_METHOD_NAME, e.what(), "invoke"); } catch (std::exception &e) { - std::string tmp = stl_sprintf("C++ exception: %s", e.what()); + std::string tmp = fmt::format("C++ exception: {}", e.what()); field_error(state, UPVAL_METHOD_NAME, tmp.c_str(), "invoke"); } diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 823d542b797..9f0ecab7381 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -1009,7 +1009,8 @@ static int meta_type_tostring(lua_State *state) lua_getfield(state, -1, "__metatable"); const char *cname = lua_tostring(state, -1); - lua_pushstring(state, stl_sprintf("", cname).c_str()); + auto str = fmt::format("", cname); + lua_pushlstring(state, str.data(), str.size()); return 1; } @@ -1033,10 +1034,12 @@ static int meta_ptr_tostring(lua_State *state) lua_getfield(state, UPVAL_METATABLE, "__metatable"); const char *cname = lua_tostring(state, -1); - if (has_length) - lua_pushstring(state, stl_sprintf("<%s[%" PRIu64 "]: %p>", cname, length, (void*)ptr).c_str()); - else - lua_pushstring(state, stl_sprintf("<%s: %p>", cname, (void*)ptr).c_str()); + auto str = has_length ? + fmt::format("<{}[{}]: {}>", cname, length, static_cast(ptr)) : + fmt::format("<{}: {}>", cname, static_cast(ptr)); + + lua_pushlstring(state, str.data(), str.size()); + return 1; } diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index ec72e36fb9e..336e7f50e71 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -77,7 +77,7 @@ namespace DFHack DFLibrary* ret = (DFLibrary*)load_library(filename); if (!ret) { auto error = get_error(); - WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename.string().c_str(), error.c_str()); + WARN(plugins).print("OpenPlugin on {} failed: {}\n", filename.string(), error); } return ret; } @@ -91,7 +91,7 @@ namespace DFHack if (res != 0) { auto error = get_error(); - WARN(plugins).print("ClosePlugin failed: %s\n", error.c_str()); + WARN(plugins).print("ClosePlugin failed: {}\n", error); } return (res == 0); diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 575ebdc7f51..b7e0b2c3c42 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -238,7 +238,7 @@ bool Plugin::load(color_ostream &con) else if(state != PS_UNLOADED && state != PS_DELETED) { if (state == PS_BROKEN) - con.printerr("Plugin %s is broken - cannot be loaded\n", name.c_str()); + con.printerr("Plugin {} is broken - cannot be loaded\n", name); return false; } state = PS_LOADING; @@ -246,19 +246,19 @@ bool Plugin::load(color_ostream &con) // enter suspend CoreSuspender suspend; // open the library, etc - fprintf(stderr, "loading plugin %s\n", name.c_str()); + fmt::print(stderr, "loading plugin {}\n", name); DFLibrary * plug = OpenPlugin(path); if(!plug) { RefAutolock lock(access); if (!Filesystem::isfile(path)) { - con.printerr("Plugin %s does not exist on disk\n", name.c_str()); + con.printerr("Plugin {} does not exist on disk\n", name); state = PS_DELETED; return false; } else { - con.printerr("Can't load plugin %s\n", name.c_str()); + con.printerr("Can't load plugin {}\n", name); state = PS_UNLOADED; return false; } @@ -267,7 +267,7 @@ bool Plugin::load(color_ostream &con) #define plugin_check_symbol(sym) \ if (!LookupPlugin(plug, sym)) \ { \ - con.printerr("Plugin %s: missing symbol: %s\n", name.c_str(), sym); \ + con.printerr("Plugin {}: missing symbol: {}\n", name, sym); \ plugin_abort_load; \ return false; \ } @@ -281,7 +281,7 @@ bool Plugin::load(color_ostream &con) const char ** plug_name =(const char ** ) LookupPlugin(plug, "plugin_name"); if (name != *plug_name) { - con.printerr("Plugin %s: name mismatch, claims to be %s\n", name.c_str(), *plug_name); + con.printerr("Plugin {}: name mismatch, claims to be {}\n", name, *plug_name); plugin_abort_load; return false; } @@ -294,15 +294,15 @@ bool Plugin::load(color_ostream &con) const char *plug_git_desc = plug_git_desc_ptr ? *plug_git_desc_ptr : "unknown"; if (*plugin_abi_version != Version::dfhack_abi_version()) { - con.printerr("Plugin %s: ABI version mismatch (Plugin: %i, DFHack: %i)\n", + con.printerr("Plugin {}: ABI version mismatch (Plugin: {}, DFHack: {})\n", *plug_name, *plugin_abi_version, Version::dfhack_abi_version()); plugin_abort_load; return false; } if (strcmp(dfhack_version, *plug_version) != 0) { - con.printerr("Plugin %s was not built for this version of DFHack.\n" - "Plugin: %s, DFHack: %s\n", *plug_name, *plug_version, dfhack_version); + con.printerr("Plugin {} was not built for this version of DFHack.\n" + "Plugin: {}, DFHack: {}\n", *plug_name, *plug_version, dfhack_version); plugin_abort_load; return false; } @@ -310,18 +310,18 @@ bool Plugin::load(color_ostream &con) { if (strcmp(dfhack_git_desc, plug_git_desc) != 0) { - std::string msg = stl_sprintf("Warning: Plugin %s compiled for DFHack %s, running DFHack %s\n", + std::string msg = fmt::format("Warning: Plugin {} compiled for DFHack {}, running DFHack {}\n", *plug_name, plug_git_desc, dfhack_git_desc); con << msg << std::flush; std::cerr << msg << std::flush; } } else - con.printerr("Warning: Plugin %s missing git information\n", *plug_name); + con.printerr("Warning: Plugin {} missing git information\n", *plug_name); bool *plug_dev = (bool*)LookupPlugin(plug, "plugin_dev"); if (plug_dev && *plug_dev && getenv("DFHACK_NO_DEV_PLUGINS")) { - con.print("Skipping dev plugin: %s\n", *plug_name); + con.print("Skipping dev plugin: {}\n", *plug_name); plugin_abort_load; return false; } @@ -338,7 +338,7 @@ bool Plugin::load(color_ostream &con) } if (missing_globals.size()) { - con.printerr("Plugin %s is missing required globals: %s\n", + con.printerr("Plugin {} is missing required globals: {}\n", *plug_name, join_strings(", ", missing_globals).c_str()); plugin_abort_load; return false; @@ -364,18 +364,18 @@ bool Plugin::load(color_ostream &con) state = PS_LOADED; parent->registerCommands(this); if ((plugin_onupdate || plugin_enable) && !plugin_is_enabled) - con.printerr("Plugin %s has no enabled var!\n", name.c_str()); + con.printerr("Plugin {} has no enabled var!\n", name); if (Core::getInstance().isWorldLoaded() && plugin_load_world_data && plugin_load_world_data(con) != CR_OK) - con.printerr("Plugin %s has failed to load saved world data.\n", name.c_str()); + con.printerr("Plugin {} has failed to load saved world data.\n", name); if (Core::getInstance().isMapLoaded() && plugin_load_site_data && World::IsSiteLoaded() && plugin_load_site_data(con) != CR_OK) - con.printerr("Plugin %s has failed to load saved site data.\n", name.c_str()); - fprintf(stderr, "loaded plugin %s; DFHack build %s\n", name.c_str(), plug_git_desc); - fflush(stderr); + con.printerr("Plugin {} has failed to load saved site data.\n", name); + + fmt::print(stderr, "loaded plugin {}; DFHack build {}\n", name, plug_git_desc); return true; } else { - con.printerr("Plugin %s has failed to initialize properly.\n", name.c_str()); + con.printerr("Plugin {} has failed to initialize properly.\n", name); plugin_is_enabled = 0; plugin_onupdate = 0; reset_lua(); @@ -393,7 +393,7 @@ bool Plugin::unload(color_ostream &con) { if (Screen::hasActiveScreens(this)) { - con.printerr("Cannot unload plugin %s: has active viewscreens\n", name.c_str()); + con.printerr("Cannot unload plugin {}: has active viewscreens\n", name); access->unlock(); return false; } @@ -402,7 +402,7 @@ bool Plugin::unload(color_ostream &con) if (plugin_onstatechange && plugin_onstatechange(con, SC_BEGIN_UNLOAD) != CR_OK) { - con.printerr("Plugin %s has refused to be unloaded.\n", name.c_str()); + con.printerr("Plugin {} has refused to be unloaded.\n", name); access->unlock(); return false; } @@ -418,9 +418,9 @@ bool Plugin::unload(color_ostream &con) CoreSuspender suspend; access->lock(); if (Core::getInstance().isMapLoaded() && plugin_save_site_data && World::IsSiteLoaded() && plugin_save_site_data(con) != CR_OK) - con.printerr("Plugin %s has failed to save site data.\n", name.c_str()); + con.printerr("Plugin {} has failed to save site data.\n", name); if (Core::getInstance().isWorldLoaded() && plugin_save_world_data && plugin_save_world_data(con) != CR_OK) - con.printerr("Plugin %s has failed to save world data.\n", name.c_str()); + con.printerr("Plugin {} has failed to save world data.\n", name); // holding the access lock while releasing the CoreSuspender creates a deadlock risk access->unlock(); } @@ -448,7 +448,7 @@ bool Plugin::unload(color_ostream &con) } else { - con.printerr("Plugin %s has failed to shutdown!\n",name.c_str()); + con.printerr("Plugin {} has failed to shutdown!\n",name); state = PS_BROKEN; } access->unlock(); @@ -460,7 +460,7 @@ bool Plugin::unload(color_ostream &con) return true; } else if (state == PS_BROKEN) - con.printerr("Plugin %s is broken - cannot be unloaded\n", name.c_str()); + con.printerr("Plugin {} is broken - cannot be unloaded\n", name); access->unlock(); return false; } @@ -490,7 +490,7 @@ command_result Plugin::invoke(color_ostream &out, const std::string & command, s else if (cmdIt->guard) { CoreSuspender suspend; if (!cmdIt->guard(Core::getInstance().getTopViewscreen())) { - out.printerr("Could not invoke %s: unsuitable UI state.\n", command.c_str()); + out.printerr("Could not invoke {}: unsuitable UI state.\n", command); cr = CR_WRONG_USAGE; } else { @@ -894,13 +894,13 @@ bool PluginManager::addPlugin(string name) { if (all_plugins.find(name) != all_plugins.end()) { - Core::printerr("Plugin already exists: %s\n", name.c_str()); + Core::printerr("Plugin already exists: {}\n", name); return false; } std::filesystem::path path = getPluginPath(name); if (!Filesystem::isfile(path)) { - Core::printerr("Plugin does not exist: %s\n", name.c_str()); + Core::printerr("Plugin does not exist: {}\n", name); return false; } Plugin * p = new Plugin(core, path, name, this); @@ -944,7 +944,7 @@ bool PluginManager::load (const string &name) Plugin *p = (*this)[name]; if (!p) { - Core::printerr("Plugin failed to register: %s\n", name.c_str()); + Core::printerr("Plugin failed to register: {}\n", name); return false; } return p->load(core->getConsole()); @@ -969,7 +969,7 @@ bool PluginManager::unload (const string &name) std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) { - Core::printerr("Plugin does not exist: %s\n", name.c_str()); + Core::printerr("Plugin does not exist: {}\n", name); return false; } return (*this)[name]->unload(core->getConsole()); @@ -1069,8 +1069,8 @@ void PluginManager::registerCommands( Plugin * p ) std::string name = cmds[i].name; if (command_map.find(name) != command_map.end()) { - core->printerr("Plugin %s re-implements command \"%s\" (from plugin %s)\n", - p->getName().c_str(), name.c_str(), command_map[name]->getName().c_str()); + core->printerr("Plugin {} re-implements command \"{}\" (from plugin {})\n", + p->getName(), name, command_map[name]->getName()); continue; } command_map[name] = p; @@ -1096,12 +1096,12 @@ void PluginManager::doSaveData(color_ostream &out) if (World::IsSiteLoaded()) { cr = it->second->save_site_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to save site data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to save site data.\n", it->first); } cr = it->second->save_world_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to save world data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to save world data.\n", it->first); } } @@ -1112,7 +1112,7 @@ void PluginManager::doLoadWorldData(color_ostream &out) command_result cr = it->second->load_world_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to load saved world data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to load saved world data.\n", it->first); } } @@ -1123,7 +1123,7 @@ void PluginManager::doLoadSiteData(color_ostream &out) command_result cr = it->second->load_site_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to load saved site data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to load saved site data.\n", it->first); } } diff --git a/library/Process.cpp b/library/Process.cpp index 101eecbc710..47d110ba920 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -194,7 +194,7 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) cerr << "1KB hexdump follows:" << endl; for(int i = 0; i < 64; i++) { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + fmt::print(std::cerr, "{:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}\n", first_kb[i*16], first_kb[i*16+1], first_kb[i*16+2], diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index 20990428dca..b159843761b 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -180,7 +180,7 @@ bool RemoteClient::connect(int port) if (!socket->Open("localhost", port)) { - default_output().printerr("Could not connect to localhost:%d\n", port); + default_output().printerr("Could not connect to localhost:{}\n", port); return false; } @@ -335,8 +335,8 @@ bool RemoteFunctionBase::bind(color_ostream &out, RemoteClient *client, if (p_client == client && this->name == name && this->plugin == plugin) return true; - out.printerr("Function already bound to %s::%s\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("Function already bound to {}::{}\n", + this->plugin, this->name); return false; } @@ -372,15 +372,15 @@ command_result RemoteFunctionBase::execute(color_ostream &out, { if (!isValid()) { - out.printerr("Calling an unbound RPC function %s::%s.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("Calling an unbound RPC function {}:{}.\n", + this->plugin, this->name); return CR_NOT_IMPLEMENTED; } if (!p_client->socket->IsSocketValid()) { - out.printerr("In call to %s::%s: invalid socket.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: invalid socket.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -388,15 +388,15 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (send_size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In call to %s::%s: message too large: %d.\n", - this->plugin.c_str(), this->name.c_str(), send_size); + out.printerr("In call to {}:{}: message too large: {}.\n", + this->plugin, this->name, send_size); return CR_LINK_FAILURE; } if (!sendRemoteMessage(p_client->socket, id, input, true)) { - out.printerr("In call to %s::%s: I/O error in send.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: I/O error in send.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -410,8 +410,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (!readFullBuffer(p_client->socket, &header, sizeof(header))) { - out.printerr("In call to %s::%s: I/O error in receive header.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: I/O error in receive header.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -422,8 +422,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In call to %s::%s: invalid received size %d.\n", - this->plugin.c_str(), this->name.c_str(), header.size); + out.printerr("In call to {}:{}: invalid received size {}.\n", + this->plugin, this->name, header.size); return CR_LINK_FAILURE; } @@ -431,8 +431,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (!readFullBuffer(p_client->socket, buf, header.size)) { - out.printerr("In call to %s::%s: I/O error in receive %d bytes of data.\n", - this->plugin.c_str(), this->name.c_str(), header.size); + out.printerr("In call to {}:{}: I/O error in receive {} bytes of data.\n", + this->plugin, this->name, header.size); return CR_LINK_FAILURE; } @@ -440,8 +440,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, case RPC_REPLY_RESULT: if (!output->ParseFromArray(buf, header.size)) { - out.printerr("In call to %s::%s: error parsing received result.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: error parsing received result.\n", + this->plugin, this->name); delete[] buf; return CR_LINK_FAILURE; } @@ -454,8 +454,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (text_data.ParseFromArray(buf, header.size)) text_decoder.decode(&text_data); else - out.printerr("In call to %s::%s: received invalid text data.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: received invalid text data.\n", + this->plugin, this->name); break; default: diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 4e26b064973..9557a15862f 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -189,14 +189,14 @@ ServerFunctionBase *ServerConnection::findFunction(color_ostream &out, const std Plugin *plug = Core::getInstance().plug_mgr->getPluginByName(plugin); if (!plug) { - out.printerr("No such plugin: %s\n", plugin.c_str()); + out.printerr("No such plugin: {}\n", plugin); return NULL; } svc = plug->rpc_connect(out); if (!svc) { - out.printerr("Plugin %s doesn't export any RPC methods.\n", plugin.c_str()); + out.printerr("Plugin {} doesn't export any RPC methods.\n", plugin); return NULL; } @@ -299,7 +299,7 @@ void ServerConnection::threadFn() if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In RPC server: invalid received size %d.\n", header.size); + out.printerr("In RPC server: invalid received size {}.\n", header.size); break; } @@ -307,7 +307,7 @@ void ServerConnection::threadFn() if (!readFullBuffer(socket, buf.get(), header.size)) { - out.printerr("In RPC server: I/O error in receive %d bytes of data.\n", header.size); + out.printerr("In RPC server: I/O error in receive {} bytes of data.\n", header.size); break; } @@ -323,17 +323,17 @@ void ServerConnection::threadFn() if (!fn) { - stream.printerr("RPC call of invalid id %d\n", header.id); + stream.printerr("RPC call of invalid id {}\n", header.id); } else { if (((fn->flags & SF_ALLOW_REMOTE) != SF_ALLOW_REMOTE) && strcmp(socket->GetClientAddr(), "127.0.0.1") != 0) { - stream.printerr("In call to %s: forbidden host: %s\n", fn->name, socket->GetClientAddr()); + stream.printerr("In call to {}: forbidden host: {}\n", fn->name, socket->GetClientAddr()); } else if (!fn->in()->ParseFromArray(buf.get(), header.size)) { - stream.printerr("In call to %s: could not decode input args.\n", fn->name); + stream.printerr("In call to {}: could not decode input args.\n", fn->name); } else { @@ -364,7 +364,7 @@ void ServerConnection::threadFn() if (out_size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - stream.printerr("In call to %s: reply too large: %d.\n", + stream.printerr("In call to {}: reply too large: {}.\n", (fn ? fn->name : "UNKNOWN"), out_size); res = CR_LINK_FAILURE; } @@ -489,7 +489,7 @@ void ServerMainImpl::threadFn(std::promise promise, int port) break; case CSimpleSocket::SocketFirewallError: case CSimpleSocket::SocketProtocolError: - WARN(socket).print("Connection failed: %s\n", server.socket.DescribeError()); + WARN(socket).print("Connection failed: {}\n", server.socket.DescribeError()); break; default: break; diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 56b45eba015..dbef7a99060 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -696,16 +696,16 @@ command_result CoreService::BindMethod(color_ostream &stream, if (!fn) { - stream.printerr("RPC method not found: %s::%s\n", - in->plugin().c_str(), in->method().c_str()); + stream.printerr("RPC method not found: {}::{}\n", + in->plugin(), in->method()); return CR_FAILURE; } if (fn->p_in_template->GetTypeName() != in->input_msg() || fn->p_out_template->GetTypeName() != in->output_msg()) { - stream.printerr("Requested wrong signature for RPC method: %s::%s\n", - in->plugin().c_str(), in->method().c_str()); + stream.printerr("Requested wrong signature for RPC method: {}::{}\n", + in->plugin(), in->method()); return CR_FAILURE; } diff --git a/library/TileTypes.cpp b/library/TileTypes.cpp index 7636d5bab2c..9f8a72ade60 100644 --- a/library/TileTypes.cpp +++ b/library/TileTypes.cpp @@ -61,9 +61,9 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No shape %s in %s.\n", - enum_item_key(shape).c_str(), enum_item_key(mat).c_str() + fmt::print( + stderr, "NOTE: No shape {} in {}.\n", + enum_item_key(shape), enum_item_key(mat) ); } @@ -93,10 +93,10 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No special %s in %s:%s.\n", - enum_item_key(special).c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str() + fmt::print( + stderr, "NOTE: No special {} in {}:{}.\n", + enum_item_key(special), enum_item_key(mat), + enum_item_key(shape) ); } @@ -139,10 +139,9 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No direction '%s' in %s:%s:%s.\n", - dir.c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str(), enum_item_key(special).c_str() + fmt::print( + stderr, "NOTE: No direction '{}' in {}:{}:{}.\n", + dir, enum_item_key(mat), enum_item_key(shape), enum_item_key(special) ); } @@ -162,10 +161,10 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No variant '%s' in %s:%s:%s:%s.\n", - enum_item_key(variant).c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str(), enum_item_key(special).c_str(), dir.c_str() + fmt::print( + stderr, "NOTE: No variant '{}' in {}:{}:{}:{}.\n", + enum_item_key(variant), enum_item_key(mat), + enum_item_key(shape), enum_item_key(special), dir ); } @@ -214,8 +213,8 @@ static void init_tables() tile_to_mat[tiletype_material::STONE][tt] = ttm; if (ttm == tiletype::Void) - fprintf(stderr, "No match for tile %s in STONE.\n", - enum_item_key(tt).c_str()); + fmt::print(stderr, "No match for tile {} in STONE.\n", + enum_item_key(tt)); } else { @@ -233,8 +232,8 @@ static void init_tables() tile_to_mat[mat][tt] = ttm; if (ttm == tiletype::Void) - fprintf(stderr, "No match for tile %s in %s.\n", - enum_item_key(tt).c_str(), enum_item_key(mat).c_str()); + fmt::print(stderr, "No match for tile {} in {}.\n", + enum_item_key(tt), enum_item_key(mat)); } } } diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 3c2a4a50fc6..1d3635a74a2 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -311,7 +311,7 @@ VMethodInterposeLinkBase::VMethodInterposeLinkBase(const virtual_identity *host, * - interpose_method comes from method_pointer_to_addr_ */ - fprintf(stderr, "Bad VMethodInterposeLinkBase arguments: %d %p (%s)\n", + fmt::print(stderr, "Bad VMethodInterposeLinkBase arguments: {} {} ({})\n", vmethod_idx, interpose_method, name_str); fflush(stderr); abort(); diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index 776362855ac..6b2c1e62f6c 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -209,7 +209,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + fmt::print(stderr, "{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +217,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + fmt::print(stderr, "{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); diff --git a/library/dfhack-run.cpp b/library/dfhack-run.cpp index df10bf1485b..171032a39c7 100644 --- a/library/dfhack-run.cpp +++ b/library/dfhack-run.cpp @@ -119,8 +119,8 @@ int main (int argc, char *argv[]) if (rv == CR_OK) { for (int i = 0; i < run_call.out()->value_size(); i++) - printf("%s%s", (i>0?"\t":""), run_call.out()->value(i).c_str()); - printf("\n"); + fmt::print("{}{}", (i>0?"\t":""), run_call.out()->value(i)); + fmt::print("\n"); } } else diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 05e38363bdc..3fce782ecf6 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -34,6 +34,7 @@ distribution. #include #include +#include namespace dfproto { @@ -106,16 +107,16 @@ namespace DFHack color_ostream(); virtual ~color_ostream(); - template - void format(const char* format, Args&& ... args) + template + void print(fmt::format_string format, Args&& ... args) { auto str = fmt::format(format, std::forward(args)...); flush_buffer(false); add_text(cur_color, str); } - template - void format_err(const char* format, Args&& ... args) + template + void printerr(fmt::format_string format, Args&& ... args) { auto str = fmt::format(format, std::forward(args)...); if (log_errors_to_stderr) { @@ -125,14 +126,6 @@ namespace DFHack add_text(COLOR_LIGHTRED, str); } - /// Print a formatted string, like printf - void print(const char *format, ...) Wformat(printf,2,3); - void vprint(const char *format, va_list args) Wformat(printf,2,0); - - /// Print a formatted string, like printf, in red - void printerr(const char *format, ...) Wformat(printf,2,3); - void vprinterr(const char *format, va_list args) Wformat(printf,2,0); - /// Get color color_value color() { return cur_color; } /// Set color (ANSI color number) @@ -196,4 +189,6 @@ namespace DFHack void decode(dfproto::CoreTextNotification *data); }; + } + diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef12..e762703c5a3 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -209,8 +209,19 @@ namespace DFHack std::unique_ptr p; std::shared_ptr vinfo; - static void print(const char *format, ...) Wformat(printf,1,2); - static void printerr(const char *format, ...) Wformat(printf,1,2); + template + static void print(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.print(format, std::forward(args)...); + } + + template + static void printerr(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.printerr(format, std::forward(args)...); + } PluginManager *getPluginManager() { return plug_mgr; } diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 9ce2cd4dacf..182148d6354 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -36,6 +36,8 @@ distribution. #include "BitArray.h" #include "Export.h" +#include + struct lua_State; /* @@ -981,7 +983,7 @@ namespace DFHack { #define ENUM_KEY_STR(enum,val) (DFHack::enum_item_key(val)) #define ENUM_FIRST_ITEM(enum) (df::enum_traits::first_item) #define ENUM_LAST_ITEM(enum) (df::enum_traits::last_item) - +#define ENUM_AS_STR(val) (DFHack::enum_item_key(val)) #define ENUM_NEXT_ITEM(enum,val) \ (DFHack::next_enum_item(val)) #define FOR_ENUM_ITEMS(enum,iter) \ @@ -1009,3 +1011,25 @@ namespace std { } }; } + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", c.x, c.y, c.z), ctx); + } +}; + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord2d& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {})", c.x, c.y), ctx); + } +}; diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index b5892bbb16a..10074eef294 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -179,8 +179,8 @@ static bool findProcess(color_ostream& out, std::string name, pid_t &pid) { command += name; FILE *cmd_pipe = popen(command.c_str(), "r"); if (!cmd_pipe) { - WARN(dfsteam, out).print("failed to exec '%s' (error: %d)\n", - command.c_str(), errno); + WARN(dfsteam, out).print("failed to exec '{}' (error: {})\n", + command, errno); return false; } @@ -204,7 +204,7 @@ static bool launchDFHack(color_ostream& out) { pid = fork(); if (pid == -1) { - WARN(dfsteam, out).print("failed to fork (error: %d)\n", errno); + WARN(dfsteam, out).print("failed to fork (error: {})\n", errno); return false; } else if (pid == 0) { // child process @@ -248,5 +248,5 @@ void DFSteam::launchSteamDFHackIfNecessary(color_ostream& out) { } bool ret = launchDFHack(out); - DEBUG(dfsteam, out).print("launching DFHack via Steam: %s\n", ret ? "successful" : "unsuccessful"); + DEBUG(dfsteam, out).print("launching DFHack via Steam: {}\n", ret ? "successful" : "unsuccessful"); } diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index d504e74c3dc..4a4d47e7659 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -71,7 +71,10 @@ static int32_t eventLastTick[EventType::EVENT_MAX]; static const int32_t ticksPerYear = 403200; void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler) { - DEBUG(log).print("registering handler %p from plugin %s for event %d\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str(), e); + DEBUG(log).print("registering handler {} from plugin {} for event {}\n", + static_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : "", + static_cast(e)); handlers[e].insert(pair(handler.plugin, handler)); } @@ -87,7 +90,9 @@ int32_t DFHack::EventManager::registerTick(EventHandler handler, int32_t when, b } handler.freq = when; tickQueue.insert(pair(handler.freq, handler)); - DEBUG(log).print("registering handler %p from plugin %s for event TICK\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str()); + DEBUG(log).print("registering handler {} from plugin {} for event TICK\n", + static_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : ""); handlers[EventType::TICK].insert(pair(handler.plugin,handler)); return when; } @@ -113,7 +118,10 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl i++; continue; } - DEBUG(log).print("unregistering handler %p from plugin %s for event %d\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str(), e); + DEBUG(log).print("unregistering handler {} from plugin {} for event {}\n", + static_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : "", + static_cast(e)); i = handlers[e].erase(i); if ( e == EventType::TICK ) removeFromTickQueue(handler); @@ -121,7 +129,8 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl } void DFHack::EventManager::unregisterAll(Plugin* plugin) { - DEBUG(log).print("unregistering all handlers for plugin %s\n", !plugin ? "" : plugin->getName().c_str()); + DEBUG(log).print("unregistering all handlers for plugin {}\n", + plugin ? plugin->getName() : ""); for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); i++ ) { if ( (*i).first != plugin ) break; @@ -408,7 +417,7 @@ void DFHack::EventManager::manageEvents(color_ostream& out) { CoreSuspender suspender; int32_t tick = df::global::world->frame_counter; - TRACE(log,out).print("processing events at tick %d\n", tick); + TRACE(log,out).print("processing events at tick {}\n", tick); auto &core = Core::getInstance(); auto &counters = core.perf_counters; @@ -599,13 +608,13 @@ static void manageJobCompletedEvent(color_ostream& out) { df::job& job1 = *(*j).second; out.print("new job\n" - " location : 0x%X\n" - " id : %d\n" - " type : %d %s\n" - " working : %d\n" - " completion_timer : %d\n" - " workerID : %d\n" - " time : %d -> %d\n" + " location : {:#X}\n" + " id : {}\n" + " type : {} {}\n" + " working : {}\n" + " completion_timer : {}\n" + " workerID : {}\n" + " time : {} -> {}\n" "\n", job1.list_link->item, job1.id, job1.job_type, ENUM_ATTR(job_type, caption, job1.job_type), job1.flags.bits.working, job1.completion_timer, getWorkerID(&job1), tick0, tick1); } for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { @@ -613,13 +622,13 @@ static void manageJobCompletedEvent(color_ostream& out) { auto j = nowJobs.find((*i).first); if ( j == nowJobs.end() ) { out.print("job deallocated\n" - " location : 0x%X\n" - " id : %d\n" - " type : %d %s\n" - " working : %d\n" - " completion_timer : %d\n" - " workerID : %d\n" - " time : %d -> %d\n" + " location : {:#X}\n" + " id : {}\n" + " type : {} {}\n" + " working : {}\n" + " completion_timer : {}\n" + " workerID : {}\n" + " time : {} -> {}\n" ,job0.list_link == NULL ? 0 : job0.list_link->item, job0.id, job0.job_type, ENUM_ATTR(job_type, caption, job0.job_type), job0.flags.bits.working, job0.completion_timer, getWorkerID(&job0), tick0, tick1); continue; } @@ -631,14 +640,14 @@ static void manageJobCompletedEvent(color_ostream& out) { continue; out.print("job change\n" - " location : 0x%X -> 0x%X\n" - " id : %d -> %d\n" - " type : %d -> %d\n" - " type : %s -> %s\n" - " working : %d -> %d\n" - " completion timer : %d -> %d\n" - " workerID : %d -> %d\n" - " time : %d -> %d\n" + " location : {:#X} -> {:#X}\n" + " id : {} -> {}\n" + " type : {} -> {}\n" + " type : {} -> {}\n" + " working : {} -> {}\n" + " completion timer : {} -> {}\n" + " workerID : {} -> {}\n" + " time : {} -> {}\n" "\n", job0.list_link->item, job1.list_link->item, job0.id, job1.id, @@ -1229,7 +1238,7 @@ static void manageUnitAttackEvent(color_ostream& out) { if ( reportStr.find("severed part") ) continue; if ( Once::doOnce("EventManager neither wound") ) { - out.print("%s, %d: neither wound: %s\n", __FILE__, __LINE__, reportStr.c_str()); + out.print("{}, {}: neither wound: {}\n", __FILE__, __LINE__, reportStr.c_str()); } } } @@ -1352,9 +1361,8 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, //if trying attack-defend pair and it fails to find attacker, try defend only InteractionData result = /*(InteractionData)*/ { std::string(), std::string(), -1, -1, -1, -1 }; if ( attackers.size() > 1 ) { -//out.print("%s,%d\n",__FILE__,__LINE__); if ( Once::doOnce("EventManager interaction ambiguous attacker") ) { - out.print("%s,%d: ambiguous attacker on report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + out.print("{},{}: ambiguous attacker on report\n \'{}\'\n \'{}\'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text : "", defendEvent ? defendEvent->text : ""); } } else if ( attackers.empty() ) { //out.print("%s,%d\n",__FILE__,__LINE__); @@ -1368,7 +1376,7 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, result.defender = defenders[0]->id; if ( defenders.size() > 1 ) { if ( Once::doOnce("EventManager interaction ambiguous defender") ) { - out.print("%s,%d: ambiguous defender: shouldn't happen. On report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + out.print("{}, {}: ambiguous defender: shouldn't happen. On report\n \'{}\'\n \'{}\'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text : "", defendEvent ? defendEvent->text : ""); } } result.attackVerb = attackVerb; @@ -1395,7 +1403,7 @@ static vector gatherRelevantUnits(color_ostream& out, df::report* r1, vector& units = reportToRelevantUnits[report->id]; if ( units.size() > 2 ) { if ( Once::doOnce("EventManager interaction too many relevant units") ) { - out.print("%s,%d: too many relevant units. On report\n \'%s\'\n", __FILE__, __LINE__, report->text.c_str()); + out.print("{},{}: too many relevant units. On report\n \'{}\'\n", __FILE__, __LINE__, report->text); } } for (int & unit_id : units) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index fae55ce9d7a..68d63e68de1 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1842,7 +1842,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce return -1; else if (message.empty()) { - Core::printerr("Empty announcement %u\n", type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement {}\n", ENUM_AS_STR(type)); // DF would print this to errorlog.txt return -1; } @@ -2070,17 +2070,17 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { // Based on reverse-engineering of "make_announcement" FUN_1400574e0 (v50.11 win64 Steam) if (!world->allow_announcements) { - DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n{}\n", message); return false; } else if (!is_valid_enum_item(info.type) || info.type == df::announcement_type::NONE) { - WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); + WARN(gui).print("Invalid announcement type:\n{}\n", message); return false; } else if (message.empty()) { - Core::printerr("Empty announcement %u\n", info.type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement {}\n", ENUM_AS_STR(info.type)); // DF would print this to errorlog.txt return false; } @@ -2091,7 +2091,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!a_flags.bits.A_DISPLAY && !a_flags.bits.DO_MEGA) { - DEBUG(gui).print("Skipped announcement not enabled at all for adventure mode:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement not enabled at all for adventure mode:\n{}\n", message); return false; } @@ -2105,7 +2105,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { // Adventure mode reuses a dwarf mode digging designation bit to determine current visibility if (!Maps::isValidTilePos(info.pos) || (Maps::getTileDesignation(info.pos)->whole & 0x10) == 0x0) { - DEBUG(gui).print("Adventure mode announcement not detected:\n%s\n", message.c_str()); + DEBUG(gui).print("Adventure mode announcement not detected:\n{}\n", message); return false; } } @@ -2115,7 +2115,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if ((info.unit_a || info.unit_d) && (!info.unit_a || Units::isHidden(info.unit_a)) && (!info.unit_d || Units::isHidden(info.unit_d))) { - DEBUG(gui).print("Dwarf mode announcement not detected:\n%s\n", message.c_str()); + DEBUG(gui).print("Dwarf mode announcement not detected:\n{}\n", message); return false; } @@ -2125,7 +2125,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!info.unit_a && !info.unit_d) { - DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n{}\n", message); return false; } } @@ -2133,12 +2133,12 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) { - DEBUG(gui).print("Skipped announcement not enabled at all for dwarf mode:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement not enabled at all for dwarf mode:\n{}\n", message); return false; } else if (!recent_report_any(info.unit_a) && !recent_report_any(info.unit_d)) { - DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n{}\n", message); return false; } } @@ -2171,7 +2171,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) if (samp_index >= 0) { - DEBUG(gui).print("Playing sound #%d for announcement.\n", samp_index); + DEBUG(gui).print("Playing sound #{} for announcement.\n", samp_index); //play_sound(musicsound_info, samp_index, 255, true); // g_src/music_and_sound_g.h // TODO: implement sounds } } @@ -2198,7 +2198,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) if (a_flags.bits.D_DISPLAY) world->status.display_timer = info.display_timer; - DEBUG(gui).print("Announcement succeeded as repeat:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement succeeded as repeat:\n{}\n", message); return true; } } @@ -2284,10 +2284,10 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) (*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || // Did adventure announcement (a_flags.bits.DO_MEGA && !adv_unconscious)) // Did popup { - DEBUG(gui).print("Announcement succeeded and displayed:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement succeeded and displayed:\n{}\n", message); } else - DEBUG(gui).print("Announcement added internally and to gamelog.txt but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement added internally and to gamelog.txt but didn't qualify to be displayed anywhere:\n{}\n", message); return true; } @@ -2493,8 +2493,8 @@ void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) if (buff1 == "VAR") // Color from dipscript var { - DEBUG(gui).print("MTB_parse received:\n[C:VAR:%s:%s]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: %s\n", - buff2.c_str(), buff3.c_str(), mtb->environment ? "Active" : "NULL"); + DEBUG(gui).print("MTB_parse received:\n[C:VAR:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", + buff2, buff3, mtb->environment ? "Active" : "NULL"); //MTB_set_color_on_var(mtb, buff2, buff3); } else @@ -2548,8 +2548,8 @@ void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) string buff_var_name = grab_token_string_pos(parse_text, i, ':'); i += buff_var_name.size(); - DEBUG(gui).print("MTB_parse received:\n[VAR:%s:%s:%s]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: %s\n", - buff_format.c_str(), buff_var_type.c_str(), buff_var_name.c_str(), mtb->environment ? "Active" : "NULL"); + DEBUG(gui).print("MTB_parse received:\n[VAR:{}:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", + buff_format, buff_var_type, buff_var_name, mtb->environment ? "Active" : "NULL"); //MTB_append_variable(mtb, str, buff_format, buff_var_type, buff_var_name); } else if (token_buffer == "R" || token_buffer == "B" || token_buffer == "P") diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index b3d327881ee..6acd3b9401c 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -155,7 +155,7 @@ string ItemTypeInfo::getToken() { if (custom) rv += ":" + custom->id; else if (subtype != -1 && type != item_type::PLANT_GROWTH) - rv += stl_sprintf(":%d", subtype); + rv += fmt::format(":{}", subtype); return rv; } @@ -859,7 +859,7 @@ static bool detachItem(df::item *item) if (item->flags.bits.on_ground) { if (!removeItemOnGround(item)) - Core::printerr("Item was marked on_ground, but not in block: %d (%d,%d,%d)\n", + Core::printerr("Item was marked on_ground, but not in block: {} ({},{},{})\n", item->id, item->pos.x, item->pos.y, item->pos.z); item->flags.bits.on_ground = false; return true; @@ -1797,7 +1797,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t World::isFortressMode() ? df::world_site::find(World::GetCurrentSiteId()) : NULL, NULL); delete prod; - DEBUG(items).print("produced %zd items\n", out_items.size()); + DEBUG(items).print("produced {} items\n", out_items.size()); for (auto out_item : out_items) { // Plant growths need a valid "growth print", otherwise they behave oddly diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index d6c2b657f50..be1415d90d5 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -33,14 +33,14 @@ void Kitchen::debug_print(color_ostream &out) out.print("Kitchen Exclusions\n"); for(std::size_t i = 0; i < size(); ++i) { - out.print("%2zu: IT:%2i IS:%i MT:%3i MI:%2i ET:%i %s\n", + out.print("{:2}: IT:{:2} IS:{:} MT:{:3} MI:{:2} ET:{:} {}\n", i, - plotinfo->kitchen.item_types[i], + ENUM_KEY_STR(item_type,plotinfo->kitchen.item_types[i]), plotinfo->kitchen.item_subtypes[i], plotinfo->kitchen.mat_types[i], plotinfo->kitchen.mat_indices[i], plotinfo->kitchen.exc_types[i].whole, - (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id.c_str() : "n/a" + (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" ); } out.print("\n"); diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 18dd73b09a2..d5358f6139f 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -309,7 +309,7 @@ std::string MaterialInfo::getToken() const return "NONE"; if (!material) - return stl_sprintf("INVALID:%d:%d", type, index); + return fmt::format("INVALID:{}:{}", type, index); switch (mode) { case Builtin: @@ -327,7 +327,7 @@ std::string MaterialInfo::getToken() const case Plant: return "PLANT:" + plant->id + ":" + material->id; default: - return stl_sprintf("INVALID_MODE:%d:%d", type, index); + return fmt::format("INVALID_MODE:{}:{}", type, index); } } @@ -337,7 +337,7 @@ std::string MaterialInfo::toString(uint16_t temp, bool named) const return "any"; if (!material) - return stl_sprintf("INVALID:%d:%d", type, index); + return fmt::format("INVALID:{}:{}", type, index); df::matter_state state = matter_state::Solid; if (temp >= material->heat.melting_point) @@ -350,7 +350,7 @@ std::string MaterialInfo::toString(uint16_t temp, bool named) const name = material->prefix + " " + name; if (named && figure) - name += stl_sprintf(" of HF %d", index); + name += fmt::format(" of HF {}", index); return name; } diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index e6a0d501269..b9c3df577b3 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -303,7 +303,7 @@ void Persistence::Internal::load(color_ostream& out) { std::filesystem::path save_path = getSavePath(world_name); std::vector files; if (0 != Filesystem::listdir(save_path, files)) { - DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '%s'\n", save_path.c_str()); + DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '{}'\n", save_path); return; } @@ -316,7 +316,7 @@ void Persistence::Internal::load(color_ostream& out) { found = true; std::filesystem::path path = save_path / fname; if (!load_file(path, entity_id)) - out.printerr("Cannot load data from: '%s'\n", path.c_str()); + out.printerr("Cannot load data from: '{}'\n", path); } if (found) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 1a988377399..d629c4b8cf4 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -621,7 +621,7 @@ std::set Screen::normalize_text_keys(const std::setlast_text_input[0]) { char c = df::global::enabler->last_text_input[0]; df::interface_key key = charToKey(c); - DEBUG(screen).print("adding character %c as interface key %ld\n", c, key); + DEBUG(screen).print("adding character {} as interface key {}\n", c, ENUM_AS_STR(key)); combined_keys.emplace(key); } return combined_keys; @@ -780,7 +780,7 @@ void dfhack_viewscreen::logic() bool is_df_screen = !is_instance(p); auto *next_p = p->parent; if (is_df_screen && Screen::isDismissed(p)) { - DEBUG(screen).print("raising dismissed DF viewscreen %p\n", p); + DEBUG(screen).print("raising dismissed DF viewscreen {}\n", static_cast(p)); Screen::raise(p); } if (is_df_screen) diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 8b485a3a625..21642a9bbed 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -204,14 +204,14 @@ std::vector Textures::loadTileset(const std::string& file, int til SDL_Surface* surface = DFIMG_Load(file.c_str()); if (!surface) { - ERR(textures).printerr("unable to load textures from '%s'\n", file.c_str()); + ERR(textures).printerr("unable to load textures from '{}'\n", file); return std::vector{}; } surface = canonicalize_format(surface); auto handles = slice_tileset(surface, tile_px_w, tile_px_h, reserved); - DEBUG(textures).print("loaded %zd textures from '%s'\n", handles.size(), file.c_str()); + DEBUG(textures).print("loaded {} textures from '{}'\n", handles.size(), file); g_tileset_to_handles[file] = handles; return handles; @@ -308,7 +308,7 @@ static void reset_surface() { } static void register_delayed_handles() { - DEBUG(textures).print("register delayed handles, size %zd\n", g_delayed_regs.size()); + DEBUG(textures).print("register delayed handles, size {}\n", g_delayed_regs.size()); for (auto& handle : g_delayed_regs) { auto texpos = add_texture(g_handle_to_surface[handle]); g_handle_to_texpos.emplace(handle, texpos); @@ -322,8 +322,9 @@ struct tracking_stage_new_region : df::viewscreen_new_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_raw_load_stage != this->raw_load_stage) { - TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage, - this->raw_load_stage); + TRACE(textures).print("raw_load_stage {} -> {}\n", + this->m_raw_load_stage, + static_cast(this->raw_load_stage)); bool tmp_state = loading_state; loading_state = this->raw_load_stage >= 0 && this->raw_load_stage < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -346,7 +347,9 @@ struct tracking_stage_adopt_region : df::viewscreen_adopt_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -369,7 +372,9 @@ struct tracking_stage_load_region : df::viewscreen_loadgamest { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -392,7 +397,9 @@ struct tracking_stage_new_arena : df::viewscreen_new_arenast { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -446,7 +453,7 @@ void Textures::init(color_ostream& out) { reserve_static_range(); install_reset_point(); DEBUG(textures, out) - .print("dynamic texture loading ready, reserved range %d-%d\n", reserved_range.start, + .print("dynamic texture loading ready, reserved range {}-{}\n", reserved_range.start, reserved_range.end); } diff --git a/library/modules/World.cpp b/library/modules/World.cpp index b41c625f174..d19f61fcee3 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -223,16 +223,16 @@ int32_t World::GetCurrentSiteId() { DEBUG(world).print("searching for adventure site\n"); auto & world_map = world->map; auto adv_pos = Units::getPosition(adv); - DEBUG(world).print("adv_pos: (%d, %d, %d)\n", adv_pos.x, adv_pos.y, adv_pos.z); + DEBUG(world).print("adv_pos: ({}, {}, {})\n", adv_pos.x, adv_pos.y, adv_pos.z); df::coord2d rgn_pos(world_map.region_x + adv_pos.x/48, world_map.region_y + adv_pos.y/48); for (auto site : world->world_data->sites) { - DEBUG(world).print("scanning site %d: %s\n", site->id, Translation::translateName(&site->name, true).c_str()); - DEBUG(world).print(" rgn_pos: (%d, %d); site bounds: (%d, %d), (%d, %d) \n", + DEBUG(world).print("scanning site {}: {}\n", site->id, Translation::translateName(&site->name, true)); + DEBUG(world).print(" rgn_pos: ({}, {}); site bounds: ({}, {}), ({}, {}) \n", rgn_pos.x, rgn_pos.y, site->global_min_x, site->global_min_y, site->global_max_x, site->global_max_y); if (rgn_pos.x >= site->global_min_x && rgn_pos.x <= site->global_max_x && rgn_pos.y >= site->global_min_y && rgn_pos.y <= site->global_max_y) { - DEBUG(world).print("found site: %d\n", site->id); + DEBUG(world).print("found site: {}\n", site->id); return site->id; } } diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 8f75e0549e2..07c1ca795d6 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -433,13 +433,13 @@ struct GeoLayer void print_mineral_stats(color_ostream &out) { for (auto it = mineral_count.begin(); it != mineral_count.end(); ++it) - INFO(process, out).print("3dveins: %s %s: %d (%f)\n", - MaterialInfo(0, it->first.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, it->first.second).c_str(), + INFO(process, out).print("3dveins: {} {}: {} ({})\n", + MaterialInfo(0, it->first.first).getToken(), + ENUM_KEY_STR(inclusion_type, it->first.second), it->second, (float(it->second) / unmined_tiles)); - INFO(process, out).print ("3dveins: Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); + INFO(process, out).print ("3dveins: Total tiles: {} ({} unmined)\n", tiles, unmined_tiles); } bool form_veins(color_ostream &out); @@ -471,12 +471,12 @@ struct GeoBiome void print_mineral_stats(color_ostream &out) { - INFO(process,out).print("3dveins: Geological biome %d:\n", info.geo_index); + INFO(process,out).print("3dveins: Geological biome {}:\n", info.geo_index); for (size_t i = 0; i < layers.size(); i++) if (layers[i]) { - INFO(process, out).print("3dveins: Layer %ld\n", i); + INFO(process, out).print("3dveins: Layer {}\n", i); layers[i]->print_mineral_stats(out); } } @@ -590,7 +590,7 @@ bool VeinGenerator::init_biomes() if (info.geo_index < 0 || !info.geobiome) { - WARN(process, out).print("Biome %zd is not defined.\n", i); + WARN(process, out).print("Biome {} is not defined.\n", i); return false; } @@ -801,7 +801,7 @@ bool VeinGenerator::scan_layer_depth(Block *b, df::coord2d column, int z) { if (z != min_level[idx]-1 && min_level[idx] <= top_solid) { - WARN(process, out).print("Discontinuous layer %d at (%d,%d,%d).\n", + WARN(process, out).print("Discontinuous layer {} at ({} {} {}).\n", layer->index, x+column.x*16, y+column.y*16, z ); return false; @@ -852,7 +852,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) if (max_level[i+1] != min_level[i]-1) { WARN(process, out).print( - "Gap or overlap with next layer %d at (%d,%d,%d-%d).\n", + "Gap or overlap with next layer {} at ({} {} {}-{}).\n", i+1, x+column.x*16, y+column.y*16, max_level[i+1], min_level[i] ); return false; @@ -895,7 +895,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) } WARN(process, out).print( - "Layer height change in layer %d at (%d,%d,%d): %d instead of %d.\n", + "Layer height change in layer {} at ({} {} {}): {} instead of {}.\n", i, x+column.x*16, y+column.y*16, max_level[i], size, biome->layers[i]->thickness ); @@ -936,7 +936,7 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) if (unsigned(key.first) >= materials.size() || unsigned(key.second) >= NUM_INCLUSIONS) { - WARN(process, out).print("Invalid vein code: %d %d - aborting.\n",key.first,key.second); + WARN(process, out).print("Invalid vein code: {} {} - aborting.\n",key.first,ENUM_AS_STR(key.second)); return false; } @@ -946,9 +946,9 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) { // Report first occurence of unreasonable vein spec WARN(process, out).print( - "Unexpected vein %s %s - ", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str() + "Unexpected vein {} {} - ", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second) ); status = materials[key.first].default_type; @@ -956,8 +956,8 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) WARN(process, out).print("will be left in place.\n"); else WARN(process, out).print( - "correcting to %s.\n", - ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)).c_str() + "correcting to {}.\n", + ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)) ); } @@ -1095,7 +1095,7 @@ void VeinGenerator::write_block_tiles(Block *b, df::coord2d column, int z) if (!ok) { WARN(process, out).print( - "Couldn't write %d vein at (%d,%d,%d)\n", + "Couldn't write {} vein at ({} {} {})\n", mat, x+column.x*16, y+column.y*16, z ); } @@ -1285,7 +1285,7 @@ bool GeoLayer::form_veins(color_ostream &out) if (parent_id >= (int)refs.size()) { - WARN(process, out).print("Forward vein reference in biome %d.\n", biome->info.geo_index); + WARN(process, out).print("Forward vein reference in biome {}.\n", biome->info.geo_index); return false; } @@ -1306,10 +1306,10 @@ bool GeoLayer::form_veins(color_ostream &out) ctx = "only be in "+MaterialInfo(0,vptr->parent_mat()).getToken(); WARN(process, out).print( - "Duplicate vein %s %s in biome %d layer %d - will %s.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str(), - biome->info.geo_index, index, ctx.c_str() + "Duplicate vein {} {} in biome {} layer {} - will {}.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second), + biome->info.geo_index, index, ctx ); } @@ -1362,9 +1362,9 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (best.empty()) { WARN(process,out).print( - "Could not place orphaned vein %s %s anywhere.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str() + "Could not place orphaned vein {} {} anywhere.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second) ); return true; @@ -1396,9 +1396,9 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (size > 0) { WARN(process, out).print( - "Could not place all of orphaned vein %s %s: %d left.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str(), + "Could not place all of orphaned vein {} {}: {} left.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second), size ); } @@ -1546,8 +1546,8 @@ bool VeinGenerator::place_veins(bool verbose) if (!isStoneInorganic(key.first)) { WARN(process, out).print( - "Invalid vein material: %s\n", - MaterialInfo(0, key.first).getToken().c_str() + "Invalid vein material: {}\n", + MaterialInfo(0, key.first).getToken() ); return false; @@ -1555,7 +1555,7 @@ bool VeinGenerator::place_veins(bool verbose) if (!is_valid_enum_item(key.second)) { - WARN(process, out).print("Invalid vein type: %d\n", key.second); + WARN(process, out).print("Invalid vein type: {}\n", ENUM_AS_STR(key.second)); return false; } @@ -1568,16 +1568,16 @@ bool VeinGenerator::place_veins(bool verbose) sort(queue.begin(), queue.end(), vein_cmp); // Place tiles - TRACE(process,out).print("Processing... (%zu)", queue.size()); + TRACE(process,out).print("Processing... ({})", queue.size()); for (size_t j = 0; j < queue.size(); j++) { if (queue[j]->parent && !queue[j]->parent->placed) { WARN(process, out).print( - "\nParent vein not placed for %s %s.\n", - MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str() + "\nParent vein not placed for {} {}.\n", + MaterialInfo(0,queue[j]->vein.first).getToken(), + ENUM_KEY_STR(inclusion_type, queue[j]->vein.second) ); return false; @@ -1591,16 +1591,16 @@ bool VeinGenerator::place_veins(bool verbose) } TRACE(process, out).print( - "\nVein layer %zu of %zu: %s %s (%.2f%%)... ", + "\nVein layer {} of {}: {} {} ({:.2})... ", j+1, queue.size(), - MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str(), + MaterialInfo(0,queue[j]->vein.first).getToken(), + ENUM_KEY_STR(inclusion_type, queue[j]->vein.second), queue[j]->density() * 100 ); } else { - TRACE(process, out).print("\rVein layer %zu of %zu... ", j+1, queue.size()); + TRACE(process, out).print("\rVein layer {} of {}... ", j+1, queue.size()); } queue[j]->place_tiles(); diff --git a/plugins/add-spatter.cpp b/plugins/add-spatter.cpp index 0ece73d982d..a2685983fb2 100644 --- a/plugins/add-spatter.cpp +++ b/plugins/add-spatter.cpp @@ -83,7 +83,7 @@ static void find_material(int *type, int *index, df::item *input, MaterialSource if (!info.findProduct(info, mat.product_name)) { color_ostream_proxy out(Core::getInstance().getConsole()); - out.printerr("Cannot find product '%s'\n", mat.product_name.c_str()); + out.printerr("Cannot find product '{}'\n", mat.product_name); } } @@ -293,7 +293,7 @@ static void find_reagent( return; } - out.printerr("Invalid reagent name '%s' in '%s'\n", name.c_str(), react->code.c_str()); + out.printerr("Invalid reagent name '{}' in '{}'\n", name, react->code); } static void parse_product( diff --git a/plugins/aquifer.cpp b/plugins/aquifer.cpp index 124fa29838a..afb8bbe54fb 100644 --- a/plugins/aquifer.cpp +++ b/plugins/aquifer.cpp @@ -35,7 +35,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded()) { - out.printerr("Cannot run %s without a loaded map.\n", plugin_name); + out.printerr("Cannot run {} without a loaded map.\n", plugin_name); return CR_FAILURE; } @@ -142,7 +142,7 @@ static int get_max_aq_z(color_ostream &out, const df::coord &pos1, const df::coo static void get_z_range(color_ostream &out, int & minz, int & maxz, const df::coord & pos1, const df::coord & pos2, int levels, bool top_is_aq = false, int skip_top = 0) { - DEBUG(log,out).print("get_z_range: top_is_aq=%d, skip_top=%d\n", top_is_aq, skip_top); + DEBUG(log,out).print("get_z_range: top_is_aq={}, skip_top={}\n", top_is_aq, skip_top); if (!top_is_aq) maxz = get_max_ground_z(out, pos1, pos2) - skip_top; @@ -151,12 +151,12 @@ static void get_z_range(color_ostream &out, int & minz, int & maxz, const df::co minz = std::max((int)pos1.z, maxz - levels + 1); - DEBUG(log,out).print("calculated z range: minz=%d, maxz=%d\n", minz, maxz); + DEBUG(log,out).print("calculated z range: minz={}, maxz={}\n", minz, maxz); } static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_list: pos1=%d,%d,%d, pos2=%d,%d,%d, levels=%d, leaky=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, levels, leaky); + DEBUG(log,out).print("entering aquifer_list: pos1={}, pos2={}, levels={}, leaky={}\n", + pos1, pos2, levels, leaky); std::map> light_tiles, heavy_tiles; @@ -173,14 +173,14 @@ static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int }); if (light_tiles.empty() && heavy_tiles.empty()) { - out.print("No %saquifer tiles in the specified range.\n", leaky ? "leaking " : ""); + out.print("No {}aquifer tiles in the specified range.\n", leaky ? "leaking " : ""); } else { int elev_off = world->map.region_z - 100; for (int z = maxz; z >= minz; --z) { int lcount = light_tiles.contains(z) ? light_tiles[z] : 0; int hcount = heavy_tiles.contains(z) ? heavy_tiles[z] : 0; if (lcount || hcount) - out.print("z-level %3d (elevation %4d) has %6d %slight aquifer tile(s) and %6d %sheavy aquifer tile(s)\n", + out.print("z-level {} (elevation {}) has {} {}light aquifer tile(s) and {} {}heavy aquifer tile(s)\n", z, z+elev_off, lcount, leaky ? "leaking " : "", hcount, leaky ? "leaking " : ""); } } @@ -189,22 +189,21 @@ static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int static int aquifer_drain(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_drain: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_drain: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); const bool all = aq_type == "all"; const bool heavy_state = aq_type == "heavy"; int modified = Maps::removeAreaAquifer(pos1, pos2, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return Maps::isTileAquifer(pos) && (all || Maps::isTileHeavyAquifer(pos) == heavy_state) && (!leaky || is_leaky(pos)); }); - DEBUG(log, out).print("drained aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, all=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, all, modified); + DEBUG(log, out).print("drained aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, all={}, count={}\n", + pos1, pos2, heavy_state, all, modified); return modified; } @@ -212,21 +211,20 @@ static int aquifer_drain(color_ostream &out, string aq_type, static int aquifer_convert(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_convert: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_convert: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); const bool heavy_state = aq_type == "heavy"; int modified = Maps::setAreaAquifer(pos1, pos2, heavy_state, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return Maps::isTileAquifer(pos) && Maps::isTileHeavyAquifer(pos) != heavy_state && (!leaky || is_leaky(pos)); }); - DEBUG(log, out).print("converted aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, modified); + DEBUG(log, out).print("converted aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, count={}\n", + pos1, pos2, heavy_state, modified); return modified; } @@ -234,20 +232,19 @@ static int aquifer_convert(color_ostream &out, string aq_type, static int aquifer_add(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_add: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_add: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); const bool heavy_state = aq_type == "heavy"; int modified = Maps::setAreaAquifer(pos1, pos2, heavy_state, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return (leaky || !is_leaky(pos)) && (!Maps::isTileAquifer(pos) || Maps::isTileHeavyAquifer(pos) != heavy_state); }); - DEBUG(log, out).print("added aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, modified); + DEBUG(log, out).print("added aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, count={}\n", + pos1, pos2, heavy_state, modified); return modified; } diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp index 089b626b5f8..1924cfc9e96 100644 --- a/plugins/army-controller-sanity.cpp +++ b/plugins/army-controller-sanity.cpp @@ -54,12 +54,12 @@ namespace { for (auto ac : ent->army_controllers) { if (ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %p found in entity id %d\n", ac, ent->id); + WARN(log).print("acValidationError: Bad controller {} found in entity id {}\n", static_cast(ac), ent->id); ok = false; } if (ac_set.count(ac) != 0 && ac->entity_id != ent->id) { - WARN(log).print("acValidationError: Army controller %d has entity id %d but is linked from entity with id %d\n", ac->id, ac->entity_id, ent->id); + WARN(log).print("acValidationError: Army controller {} has entity id {} but is linked from entity with id {}\n", ac->id, ac->entity_id, ent->id); } } } @@ -68,17 +68,17 @@ namespace { { auto ac = ar->controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %p found in army id %d\n", ac, ar->id); + WARN(log).print("acValidationError: Bad controller {} found in army id {}\n", static_cast(ac), ar->id); ok = false; } else if (ac && ac->id != ar->controller_id) { - WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in army %d\n", ac, ar->controller_id, ac->id, ar->id); + WARN(log).print("acValidationError: controller {} id mismatch ({} != {}) in army {}\n", static_cast(ac), ar->controller_id, ac->id, ar->id); ok = false; } else if (!ac && ar->controller_id != -1) { - WARN(log).print("acValidationError: army %d has nonzero controller %d but controller pointer is null\n", ar->id, ar->controller_id); + WARN(log).print("acValidationError: army {} has nonzero controller {} but controller pointer is null\n", ar->id, ar->controller_id); ok = false; } } @@ -87,17 +87,17 @@ namespace { { auto ac = un->enemy.army_controller; if (ac && ac_set.count(ac) == 0) { - WARN(log).print("acValidationError: Bad controller %p found in unit id %d\n", ac, un->id); + WARN(log).print("acValidationError: Bad controller {} found in unit id {}\n", static_cast(ac), un->id); ok = false; } else if (ac && ac->id != un->enemy.army_controller_id) { - WARN(log).print("acValidationError: controller %p id mismatch (%d != %d) in unit %d\n", ac, un->enemy.army_controller_id, ac->id, un->id); + WARN(log).print("acValidationError: controller {} id mismatch ({} != {}) in unit {}\n", static_cast(ac), un->enemy.army_controller_id, ac->id, un->id); ok = false; } else if (!ac && un->enemy.army_controller_id != -1) { - WARN(log).print("acValidationError: unit %d has has nonzero controller %d but controller pointer is null\n", un->id, un->enemy.army_controller_id); + WARN(log).print("acValidationError: unit {} has has nonzero controller {} but controller pointer is null\n", un->id, un->enemy.army_controller_id); ok = false; } } @@ -105,7 +105,7 @@ namespace { last_army_controller_next_id = *army_controller_next_id; last_ac_vec_size = world->army_controllers.all.size(); - INFO(log).print("acValidation: controller count = %ld, next id = %d, season tick count = %d\n", + INFO(log).print("acValidation: controller count = {}, next id = {}, season tick count = {}\n", last_ac_vec_size, last_army_controller_next_id, *cur_year_tick); return ok; diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 8fde1cc3479..8f7bbb16faf 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -82,13 +82,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector fortress_age > 0) autobutcher_cycle(out); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -104,7 +104,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); cleanup_autobutcher(out); return CR_OK; } @@ -128,7 +128,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { // all the other state we can directly read/modify from the persistent // data structure. is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); // load the persisted watchlist @@ -140,7 +140,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -209,8 +209,8 @@ static bool isHighPriority(df::unit *unit) { } static void doMarkForSlaughter(df::unit *unit) { - DEBUG(cycle).print("marking unit %d for slaughter: %s, %s, high priority: %s, age: %.2f\n", - unit->id, Units::getReadableName(unit).c_str(), + DEBUG(cycle).print("marking unit {} for slaughter: {}, {}, high priority: {}, age: {:.2f}\n", + unit->id, Units::getReadableName(unit), Units::isFemale(unit) ? "female" : "male", isHighPriority(unit) ? "yes" : "no", Units::getAge(unit)); @@ -266,7 +266,7 @@ struct WatchedRace { fk_prot(0), fa_prot(0), mk_prot(0), ma_prot(0), fk_units(compareKids), mk_units(compareKids), fa_units(compareAdults), ma_units(compareAdults) { - TRACE(control,out).print("creating new WatchedRace: id=%d, watched=%s, fk=%u, mk=%u, fa=%u, ma=%u\n", + TRACE(control,out).print("creating new WatchedRace: id={}, watched={}, fk={}, mk={}, fa={}, ma={}\n", id, watch ? "true" : "false", fk, mk, fa, ma); } @@ -293,8 +293,8 @@ struct WatchedRace { rconfig.ival(5) = ma; } else { - ERR(control,out).print("could not create persistent key for race: %s", - Units::getRaceNameById(raceId).c_str()); + ERR(control,out).print("could not create persistent key for race: {}", + Units::getRaceNameById(raceId)); } } @@ -375,14 +375,14 @@ static void init_autobutcher(color_ostream &out) { vector watchlist; World::GetPersistentSiteData(&watchlist, WATCHLIST_CONFIG_KEY_PREFIX, true); for (auto & p : watchlist) { - DEBUG(control,out).print("Reading from save: %s\n", p.key().c_str()); + DEBUG(control,out).print("Reading from save: {}\n", p.key()); WatchedRace *w = new WatchedRace(out, p); watched_races.emplace(w->raceId, w); } } static void cleanup_autobutcher(color_ostream &out) { - DEBUG(control,out).print("cleaning %s state\n", plugin_name); + DEBUG(control,out).print("cleaning {} state\n", plugin_name); race_to_id.clear(); for (auto w : watched_races) delete w.second; @@ -396,7 +396,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o static command_result df_autobutcher(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -508,7 +508,7 @@ static void autobutcher_control(color_ostream &out) { static void autobutcher_target(color_ostream &out, const autobutcher_options &opts) { if (opts.races_new) { - DEBUG(control,out).print("setting targets for new races to fk=%u, mk=%u, fa=%u, ma=%u\n", + DEBUG(control,out).print("setting targets for new races to fk={}, mk={}, fa={}, ma={}\n", opts.fk, opts.mk, opts.fa, opts.ma); config.set_int(CONFIG_DEFAULT_FK, opts.fk); config.set_int(CONFIG_DEFAULT_MK, opts.mk); @@ -517,7 +517,7 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op } if (opts.races_all) { - DEBUG(control,out).print("setting targets for all races on watchlist to fk=%u, mk=%u, fa=%u, ma=%u\n", + DEBUG(control,out).print("setting targets for all races on watchlist to fk={}, mk={}, fa={}, ma={}\n", opts.fk, opts.mk, opts.fa, opts.ma); for (auto w : watched_races) { w.second->fk = opts.fk; @@ -530,7 +530,7 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op for (auto race : opts.races) { if (!race_to_id.count(*race)) { - out.printerr("race not found: '%s'", race->c_str()); + out.printerr("race not found: '{}'", race->c_str()); continue; } int id = race_to_id[*race]; @@ -559,7 +559,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o for (auto race : opts.races) { if (!race_to_id.count(*race)) { - out.printerr("race not found: '%s'", race->c_str()); + out.printerr("race not found: '{}'", race->c_str()); continue; } ids.emplace(race_to_id[*race]); @@ -576,7 +576,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o config.get_int(CONFIG_DEFAULT_MA))); } else if (!watched_races[id]->isWatched) { - DEBUG(control,out).print("watching: %s\n", opts.command.c_str()); + DEBUG(control,out).print("watching: {}\n", opts.command); watched_races[id]->isWatched = true; } } @@ -590,13 +590,13 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o config.get_int(CONFIG_DEFAULT_MA))); } else if (watched_races[id]->isWatched) { - DEBUG(control,out).print("unwatching: %s\n", opts.command.c_str()); + DEBUG(control,out).print("unwatching: {}\n", opts.command); watched_races[id]->isWatched = false; } } else if (opts.command == "forget") { if (watched_races.count(id)) { - DEBUG(control,out).print("forgetting: %s\n", opts.command.c_str()); + DEBUG(control,out).print("forgetting: {}\n", opts.command); watched_races[id]->RemoveConfig(out); delete watched_races[id]; watched_races.erase(id); @@ -680,7 +680,7 @@ static void autobutcher_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // check if there is anything to watch before walking through units vector if (!config.get_bool(CONFIG_AUTOWATCH)) { @@ -720,8 +720,8 @@ static void autobutcher_cycle(color_ostream &out) { w->UpdateConfig(out); watched_races.emplace(unit->race, w); - INFO(cycle,out).print("New race added to autobutcher watchlist: %s\n", - Units::getRaceNamePluralById(unit->race).c_str()); + INFO(cycle,out).print("New race added to autobutcher watchlist: {}\n", + Units::getRaceNamePluralById(unit->race)); } if (w->isWatched) { @@ -740,8 +740,8 @@ static void autobutcher_cycle(color_ostream &out) { if (slaughter_count) { std::stringstream ss; ss << slaughter_count; - INFO(cycle,out).print("%s marked for slaughter: %s\n", - Units::getRaceNamePluralById(w.first).c_str(), ss.str().c_str()); + INFO(cycle,out).print("{} marked for slaughter: {}\n", + Units::getRaceNamePluralById(w.first), ss.str()); } } } @@ -817,7 +817,7 @@ static bool autowatch_isEnabled() { } static void autowatch_setEnabled(color_ostream &out, bool enable) { - DEBUG(control,out).print("auto-adding to watchlist %s\n", enable ? "started" : "stopped"); + DEBUG(control,out).print("auto-adding to watchlist {}\n", enable ? "started" : "stopped"); config.set_bool(CONFIG_AUTOWATCH, enable); if (config.get_bool(CONFIG_IS_ENABLED)) autobutcher_cycle(out); @@ -843,7 +843,7 @@ static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsign WatchedRace * w = new WatchedRace(out, id, watched, fk, mk, fa, ma); w->UpdateConfig(out); watched_races.emplace(id, w); - INFO(control,out).print("New race added to autobutcher watchlist: %s\n", + INFO(control,out).print("New race added to autobutcher watchlist: {}\n", Units::getRaceNamePluralById(id).c_str()); } diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 7c2cfc9a5ed..6b98d0347a4 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -79,7 +79,7 @@ static PersistentDataItem & ensure_burrow_config(color_ostream &out, int id) { if (watched_burrows_indices.count(id)) return watched_burrows[watched_burrows_indices[id]]; string keyname = BURROW_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for burrow %d\n", id); + DEBUG(control,out).print("creating new persistent key for burrow {}\n", id); watched_burrows.emplace_back(World::GetPersistentSiteData(keyname, true)); size_t idx = watched_burrows.size()-1; watched_burrows_indices.emplace(id, idx); @@ -88,7 +88,7 @@ static PersistentDataItem & ensure_burrow_config(color_ostream &out, int id) { static void remove_burrow_config(color_ostream &out, int id) { if (!watched_burrows_indices.count(id)) return; - DEBUG(control,out).print("removing persistent key for burrow %d\n", id); + DEBUG(control,out).print("removing persistent key for burrow {}\n", id); size_t idx = watched_burrows_indices[id]; World::DeletePersistentData(watched_burrows[idx]); watched_burrows.erase(watched_burrows.begin()+idx); @@ -110,7 +110,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static int32_t do_cycle(color_ostream &out, bool force_designate = false); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -123,19 +123,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector frame_counter - cycle_timestamp >= CYCLE_TICKS) { int32_t designated = do_cycle(out); if (0 < designated) - out.print("autochop: designated %d tree(s) for chopping\n", designated); + out.print("autochop: designated {} tree(s) for chopping\n", designated); } return CR_OK; } static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -371,7 +371,7 @@ static int32_t scan_tree(color_ostream & out, df::plant *plant, int32_t *expecte map *designated_tree_counts, map &clearcut_burrows, map &chop_burrows) { - TRACE(cycle,out).print(" scanning tree at %d,%d,%d\n", + TRACE(cycle,out).print(" scanning tree at {},{},{}\n", plant->pos.x, plant->pos.y, plant->pos.z); if (!is_valid_tree(plant)) @@ -518,7 +518,7 @@ static void scan_logs(color_ostream &out, int32_t *usable_logs, *inaccessible_logs = 0; for (auto &item : world->items.other[items_other_id::IN_PLAY]) { - TRACE(cycle,out).print(" scanning log %d\n", item->id); + TRACE(cycle,out).print(" scanning log {}\n", item->id); if (item->flags.whole & bad_flags.whole) continue; @@ -538,7 +538,7 @@ static void scan_logs(color_ostream &out, int32_t *usable_logs, } static int32_t do_cycle(color_ostream &out, bool force_designate) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -580,7 +580,7 @@ static int32_t do_cycle(color_ostream &out, bool force_designate) { // of accessible trees int32_t needed = config.get_int(CONFIG_MAX_LOGS) - (usable_logs + expected_yield); - DEBUG(cycle,out).print("needed logs for this cycle: %d\n", needed); + DEBUG(cycle,out).print("needed logs for this cycle: {}\n", needed); for (auto & entry : designatable_trees_by_size) { if (!Designations::markPlant(entry.second)) continue; @@ -590,7 +590,7 @@ static int32_t do_cycle(color_ostream &out, bool force_designate) { return newly_marked; } } - out.print("autochop: insufficient accessible trees to reach log target! Still need %d logs!\n", + out.print("autochop: insufficient accessible trees to reach log target! Still need {} logs!\n", needed); return newly_marked; } @@ -623,8 +623,8 @@ static const char * get_protect_str(bool protect_brewable, bool protect_edible, static void autochop_printStatus(color_ostream &out) { DEBUG(control,out).print("entering autochop_printStatus\n"); validate_burrow_configs(out); - out.print("autochop is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" keeping log counts between %d and %d\n", + out.print("autochop is {}\n\n", is_enabled ? "enabled" : "disabled"); + out.print(" keeping log counts between {} and {}\n", config.get_int(CONFIG_MIN_LOGS), config.get_int(CONFIG_MAX_LOGS)); if (config.get_bool(CONFIG_WAITING_FOR_MIN)) out.print(" currently waiting for min threshold to be crossed before designating more trees\n"); @@ -643,19 +643,19 @@ static void autochop_printStatus(color_ostream &out) { &designated_trees, &accessible_yield, &tree_counts, &designated_tree_counts); out.print("summary:\n"); - out.print(" accessible logs (usable stock): %d\n", usable_logs); - out.print(" inaccessible logs: %d\n", inaccessible_logs); - out.print(" total visible logs: %d\n", usable_logs + inaccessible_logs); + out.print(" accessible logs (usable stock): {}\n", usable_logs); + out.print(" inaccessible logs: {}\n", inaccessible_logs); + out.print(" total visible logs: {}\n", usable_logs + inaccessible_logs); out.print("\n"); - out.print(" accessible trees: %d\n", accessible_trees); - out.print(" inaccessible trees: %d\n", inaccessible_trees); - out.print(" total visible trees: %d\n", accessible_trees + inaccessible_trees); + out.print(" accessible trees: {}\n", accessible_trees); + out.print(" inaccessible trees: {}\n", inaccessible_trees); + out.print(" total visible trees: {}\n", accessible_trees + inaccessible_trees); out.print("\n"); - out.print(" designated trees: %d\n", designated_trees); - out.print(" expected logs from designated trees: %d\n", expected_yield); - out.print(" expected logs from all accessible trees: %d\n", accessible_yield); + out.print(" designated trees: {}\n", designated_trees); + out.print(" expected logs from designated trees: {}\n", expected_yield); + out.print(" expected logs from all accessible trees: {}\n", accessible_yield); out.print("\n"); - out.print(" total trees harvested: %d\n", plotinfo->trees_removed); + out.print(" total trees harvested: {}\n", plotinfo->trees_removed); out.print("\n"); if (!plotinfo->burrows.list.size()) { @@ -671,9 +671,9 @@ static void autochop_printStatus(color_ostream &out) { } name_width = -name_width; // left justify - const char *fmt = "%*s %4s %4s %8s %5s %6s %7s\n"; - out.print(fmt, name_width, "burrow name", " id ", "chop", "clearcut", "trees", "marked", "protect"); - out.print(fmt, name_width, "-----------", "----", "----", "--------", "-----", "------", "-------"); + constexpr auto fmt = "{:{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; + out.print(fmt, "burrow name", name_width, " id ", "chop", "clearcut", "trees", "marked", "protect"); + out.print(fmt, "-----------", name_width, "----", "----", "--------", "-----", "------", "-------"); for (auto &burrow : plotinfo->burrows.list) { bool chop = false; @@ -689,17 +689,17 @@ static void autochop_printStatus(color_ostream &out) { protect_edible = c.get_bool(BURROW_CONFIG_PROTECT_EDIBLE); protect_cookable = c.get_bool(BURROW_CONFIG_PROTECT_COOKABLE); } - out.print(fmt, name_width, burrow->name.c_str(), int_to_string(burrow->id).c_str(), + out.print(fmt, burrow->name, name_width, burrow->id, chop ? "[x]" : "[ ]", clearcut ? "[x]" : "[ ]", - int_to_string(tree_counts[burrow->id]).c_str(), - int_to_string(designated_tree_counts[burrow->id]).c_str(), + tree_counts[burrow->id], + designated_tree_counts[burrow->id], get_protect_str(protect_brewable, protect_edible, protect_cookable)); } } static void autochop_designate(color_ostream &out) { DEBUG(control,out).print("entering autochop_designate\n"); - out.print("designated %d tree(s) for chopping\n", do_cycle(out, true)); + out.print("designated {} tree(s) for chopping\n", do_cycle(out, true)); } static void autochop_undesignate(color_ostream &out) { @@ -709,7 +709,7 @@ static void autochop_undesignate(color_ostream &out) { if (is_valid_tree(plant) && Designations::unmarkPlant(plant)) ++count; } - out.print("undesignated %d tree(s)\n", count); + out.print("undesignated {} tree(s)\n", count); } static void autochop_setTargets(color_ostream &out, int32_t max_logs, int32_t min_logs) { diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index a4516f4dce2..89a45ee15f3 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -237,7 +237,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { } is_enabled = enabled.get_bool(CONFIG_IS_ENABLED); - DEBUG(control, out).print("loading persisted enabled state: %s\n", + DEBUG(control, out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); // Parse constraints @@ -282,21 +282,21 @@ DFhackCExport command_result plugin_save_site_data(color_ostream &out) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { auto enabled = World::GetPersistentSiteData(CONFIG_KEY); is_enabled = enable; - DEBUG(control, out).print("%s from the API; persisting\n", + DEBUG(control, out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); enabled.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_autoclothing(); } else { - DEBUG(control, out).print("%s from the API, but already %s; no action\n", + DEBUG(control, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -410,7 +410,7 @@ static bool validateMaterialCategory(ClothingRequirement *requirement) { command_result autoclothing(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -527,7 +527,7 @@ static void find_needed_clothing_items() { { auto item = Items::findItemByID(ownedItem); if (!item) { - DEBUG(cycle).print("autoclothing: Invalid inventory item ID: %d\n", ownedItem); + DEBUG(cycle).print("autoclothing: Invalid inventory item ID: {}\n", ownedItem); continue; } @@ -683,7 +683,7 @@ static void generate_control(color_ostream &out) { { auto item = Items::findItemByID(itemId); if (!item) { - DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: %d\n", itemId); + DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: {}\n", itemId); continue; } else if (item->getWear() >= 1) diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index fbf3ff8a82e..4376a888d0c 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -170,7 +170,7 @@ static command_result autodump_main(color_ostream &out, vector ¶mete } } else - out.print("Could not move item: %s\n", Items::getDescription(itm, 0, true).c_str()); + out.print("Could not move item: {}\n", Items::getDescription(itm, 0, true)); } } else { // Destroy @@ -184,7 +184,7 @@ static command_result autodump_main(color_ostream &out, vector ¶mete dumped_total++; } - out.print("Done. %d items %s.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); + out.print("Done. {} items {}.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); return CR_OK; } diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index f961bc13a7a..65a08123d9d 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -200,9 +200,9 @@ class AutoFarm { if (old_plant_id != new_plant_id) { farm->plant_id[season] = new_plant_id; - INFO(cycle, out).print("autofarm: changing farm #%d from %s to %s\n", farm->id, - get_plant_name(old_plant_id).c_str(), - get_plant_name(new_plant_id).c_str()); + INFO(cycle, out).print("autofarm: changing farm #{} from {} to {}\n", farm->id, + get_plant_name(old_plant_id), + get_plant_name(new_plant_id)); } } @@ -390,11 +390,11 @@ class AutoFarm { if (plant != std::end(allPlants)) { setThreshold((*plant)->index, val); - INFO(control, out).print("threshold of %d for plant %s in saved configuration loaded\n", val, id.c_str()); + INFO(control, out).print("threshold of {} for plant {} in saved configuration loaded\n", val, id); } else { - WARN(control, out).print("threshold for unknown plant %s in saved configuration ignored\n", id.c_str()); + WARN(control, out).print("threshold for unknown plant {} in saved configuration ignored\n", id); } } } @@ -454,7 +454,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream& out) DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -513,7 +513,7 @@ static command_result setThresholds(color_ostream& out, std::vector static command_result autofarm(color_ostream& out, std::vector& parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } diff --git a/plugins/autogems.cpp b/plugins/autogems.cpp index 593e64670a2..249552a0069 100644 --- a/plugins/autogems.cpp +++ b/plugins/autogems.cpp @@ -314,7 +314,7 @@ bool read_config(color_ostream &out) { } } catch (Json::Exception &e) { - out.printerr("autogems: failed to read autogems.json: %s\n", e.what()); + out.printerr("autogems: failed to read autogems.json: {}\n", e.what()); return false; } @@ -326,7 +326,7 @@ bool read_config(color_ostream &out) { blacklist.insert(mat_index(item.asInt())); } else { - out.printerr("autogems: illegal item at position %i in blacklist\n", i); + out.printerr("autogems: illegal item at position {} in blacklist\n", i); } } } @@ -355,7 +355,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (enable != enabled) { if (!INTERPOSE_HOOK(autogem_hook, feed).apply(enable) || !INTERPOSE_HOOK(autogem_hook, render).apply(enable)) { - out.printerr("Could not %s autogem hooks!\n", enable? "insert": "remove"); + out.printerr("Could not {} autogem hooks!\n", enable? "insert": "remove"); return CR_FAILURE; } diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index d755202de63..d0cec796fba 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -697,8 +697,8 @@ static void assign_labor(unit_labor::unit_labor labor, dwarfs[dwarf]->uniform.pickup_flags.bits.update = 1; } - TRACE(cycle, out).print("Dwarf % i \"%s\" assigned %s: value %i %s %s\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, labor).c_str(), values[dwarf], + TRACE(cycle, out).print("Dwarf {} \"{}\" assigned {}: value {} {} {}\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor), values[dwarf], dwarf_info[dwarf].trader ? "(trader)" : "", dwarf_info[dwarf].diplomacy ? "(diplomacy)" : ""); @@ -766,7 +766,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { df::building_tradedepotst* depot = (df::building_tradedepotst*) build; trader_requested = trader_requested || depot->trade_flags.bits.trader_requested; - TRACE(cycle,out).print(trader_requested + TRACE(cycle,out).print("{}", trader_requested ? "Trade depot found and trader requested, trader will be excluded from all labors.\n" : "Trade depot found but trader is not requested.\n" ); @@ -846,8 +846,8 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (p1 || p2) { dwarf_info[dwarf].diplomacy = true; - DEBUG(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str()); + DEBUG(cycle, out).print("Dwarf {} \"{}\" has a meeting, will be cleared of all labors\n", + dwarf, dwarfs[dwarf]->name.first_name); break; } } @@ -919,15 +919,15 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) dwarf_info[dwarf].state = dwarf_states[job]; else { - WARN(cycle, out).print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job); + WARN(cycle, out).print("Dwarf {} \"{}\" has unknown job {}\n", dwarf, dwarfs[dwarf]->name.first_name, job); dwarf_info[dwarf].state = OTHER; } } state_count[dwarf_info[dwarf].state]++; - TRACE(cycle, out).print("Dwarf %i \"%s\": penalty %i, state %s\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); + TRACE(cycle, out).print("Dwarf {} \"{}\": penalty {}, state {}\n", + dwarf, dwarfs[dwarf]->name.first_name, dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); } std::vector labors; @@ -1034,8 +1034,8 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) labor_infos[labor].active_dwarfs++; - TRACE(cycle, out).print("Dwarf %i \"%s\" assigned %s: hauler\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, labor).c_str()); + TRACE(cycle, out).print("Dwarf {} \"{}\" assigned {}: hauler\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor)); } for (size_t i = num_haulers; i < hauler_ids.size(); i++) @@ -1076,7 +1076,7 @@ void print_labor (df::unit_labor labor, color_ostream &out) DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -1095,7 +1095,7 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) command_result autolabor (color_ostream &out, std::vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -1137,7 +1137,7 @@ command_result autolabor (color_ostream &out, std::vector & parame if (labor == unit_labor::NONE) { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); + out.printerr("Could not find labor {}.\n", parameters[0]); return CR_WRONG_USAGE; } @@ -1171,7 +1171,7 @@ command_result autolabor (color_ostream &out, std::vector & parame if (maximum < minimum || maximum < 0 || minimum < 0) { - out.printerr("Syntax: autolabor [] [], %d > %d\n", maximum, minimum); + out.printerr("Syntax: autolabor [] [], {} > {}\n", maximum, minimum); return CR_WRONG_USAGE; } @@ -1235,7 +1235,7 @@ command_result autolabor (color_ostream &out, std::vector & parame { out.print("Automatically assigns labors to dwarves.\n" "Activate with 'enable autolabor', deactivate with 'disable autolabor'.\n" - "Current state: %d.\n", enable_autolabor); + "Current state: {}.\n", enable_autolabor); return CR_OK; } diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index f7f1bdd84b5..cc45507b2b4 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -63,17 +63,17 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -235,9 +235,9 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building_ci unit->general_refs.push_back(ref); zone->assigned_units.push_back(unit->id); - INFO(cycle,out).print("Unit %d (%s) assigned to nestbox zone %d (%s)\n", - unit->id, Units::getRaceName(unit).c_str(), - zone->id, zone->name.c_str()); + INFO(cycle,out).print("Unit {} ({}) assigned to nestbox zone {} ({})\n", + unit->id, Units::getRaceName(unit), + zone->id, zone->name); return true; } @@ -247,13 +247,13 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building_ci static size_t getFreeNestboxZones(color_ostream &out, vector &free_zones) { size_t assigned = 0; for (auto zone : world->buildings.other.ZONE_PEN) { - TRACE(cycle,out).print("scanning pasture %d (%s)\n", zone->id, zone->name.c_str()); + TRACE(cycle,out).print("scanning pasture {} ({})\n", zone->id, zone->name); if (!Buildings::isActive(zone)) { - TRACE(cycle,out).print("pasture %d is inactive\n", zone->id); + TRACE(cycle,out).print("pasture {} is inactive\n", zone->id); continue; } if (!isEmptyPasture(zone)) { - TRACE(cycle,out).print("pasture %d is not empty\n", zone->id); + TRACE(cycle,out).print("pasture {} is not empty\n", zone->id); continue; } @@ -262,23 +262,23 @@ static size_t getFreeNestboxZones(color_ostream &out, vectorx1, zone->y1, zone->z); auto bld = Buildings::findAtTile(pos); if (!bld || bld->getType() != df::building_type::NestBox) { - TRACE(cycle,out).print("pasture %d does not have nestbox in upper left corner\n", zone->id); + TRACE(cycle,out).print("pasture {} does not have nestbox in upper left corner\n", zone->id); continue; } - TRACE(cycle,out).print("found nestbox %d in pasture %d\n", bld->id, zone->id); + TRACE(cycle,out).print("found nestbox {} in pasture {}\n", bld->id, zone->id); df::building_nest_boxst *nestbox = virtual_cast(bld); if (!nestbox) { - TRACE(cycle,out).print("nestbox %d is somehow not a nestbox\n", bld->id); + TRACE(cycle,out).print("nestbox {} is somehow not a nestbox\n", bld->id); continue; } if (nestbox->claimed_by >= 0) { if (auto unit = df::unit::find(nestbox->claimed_by)) { - TRACE(cycle,out).print("nestbox %d is claimed by unit %d (%s)\n", bld->id, - nestbox->claimed_by, Units::getReadableName(unit).c_str()); + TRACE(cycle,out).print("nestbox {} is claimed by unit {} ({})\n", bld->id, + nestbox->claimed_by, Units::getReadableName(unit)); if (!isFreeEgglayer(unit)) { - DEBUG(cycle,out).print("cannot assign unit %d to nestbox %d: not a free egg layer\n", unit->id, bld->id); + DEBUG(cycle,out).print("cannot assign unit {} to nestbox {}: not a free egg layer\n", unit->id, bld->id); } else { // if the nestbox is claimed by a free egg layer, attempt to assign that unit to the zone if (assignUnitToZone(out, unit, zone)) @@ -327,7 +327,7 @@ static size_t assign_nestboxes(color_ostream &out) { DEBUG(cycle,out).print("Failed to assign unit to building.\n"); return assigned; } - DEBUG(cycle,out).print("assigned unit %d to zone %d\n", + DEBUG(cycle,out).print("assigned unit {} to zone {}\n", free_units[idx]->id, free_zones[idx]->id); ++assigned; } diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp index f8f93ac7476..08c4c837d1f 100644 --- a/plugins/autoslab.cpp +++ b/plugins/autoslab.cpp @@ -53,7 +53,7 @@ static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control, out).print("initializing %s\n", plugin_name); + DEBUG(control, out).print("initializing {}\n", plugin_name); return CR_OK; } @@ -62,28 +62,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control, out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_cycle(out); } else { - DEBUG(control, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control, out).print("shutting down %s\n", plugin_name); + DEBUG(control, out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -104,7 +104,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) // all the other state we can directly read/modify from the persistent // data structure. is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control, out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); + DEBUG(control, out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; } @@ -114,7 +114,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan { if (is_enabled) { - DEBUG(control, out).print("world unloaded; disabling %s\n", plugin_name); + DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } } @@ -174,7 +174,7 @@ static void checkslabs(color_ostream &out) { createSlabJob(ghost); auto fullName = Units::getReadableName(ghost); - out.print("Added slab order for %s\n", DF2CONSOLE(fullName).c_str()); + out.print("Added slab order for {}\n", DF2CONSOLE(fullName)); } } } diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 98b4c1c1552..e3a728e2d8e 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -1375,8 +1375,8 @@ static bool create_output_dir(color_ostream &out, // create output directory if it doesn't already exist if (!Filesystem::mkdir_recursive(parent_path)) { - out.printerr("could not create output directory: '%s'\n", - parent_path.c_str()); + out.printerr("could not create output directory: '{}'\n", + parent_path); return false; } return true; @@ -1666,7 +1666,7 @@ static command_result do_blueprint(color_ostream &out, command << " " << parameters[i]; } string command_str = command.str(); - out.print("launching %s\n", command_str.c_str()); + out.print("launching {}\n", command_str); Core::getInstance().setHotkeyCmd(command_str); return CR_OK; @@ -1694,7 +1694,7 @@ static command_result do_blueprint(color_ostream &out, } } if (!Maps::isValidTilePos(start)) { - out.printerr("Invalid start position: %d,%d,%d\n", + out.printerr("Invalid start position: {},{},{},\n", start.x, start.y, start.z); return CR_FAILURE; } @@ -1757,7 +1757,7 @@ command_result blueprint(color_ostream &out, vector ¶meters) { else { out.print("Generated blueprint file(s):\n"); for (string &fname : files) - out.print(" %s\n", fname.c_str()); + out.print(" {}\n", fname); } } return cr; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 64ddf89e34f..6f5a13a8c7c 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -64,7 +64,7 @@ static Tasks tasks; // planned_buildings, then it has either been built or desroyed. therefore there is // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { - DEBUG(control,out).print("removing persistent data for building %d\n", id); + DEBUG(control,out).print("removing persistent data for building {}\n", id); World::DeletePersistentData(bld_config); planned_buildings.erase(id); } @@ -97,8 +97,8 @@ static const vector & get_job_items(color_ostream &out, Bu std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), index+1), 1, [&](lua_State *L) { df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(control,out).print("retrieving job_item for (%d, %d, %d) index=%d: 0x%p\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + DEBUG(control,out).print("retrieving job_item for {} index={}: {}\n", + key, index, static_cast(jitem)); if (!jitem) failed = true; else @@ -125,35 +125,35 @@ static void cache_matched(int16_t type, int32_t index) { MaterialInfo mi; mi.decode(type, index); if (mi.matches(stone_cat)) { - DEBUG(control).print("cached stone material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached stone material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "stone")); } else if (mi.matches(wood_cat)) { - DEBUG(control).print("cached wood material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached wood material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "wood")); } else if (mi.matches(metal_cat)) { - DEBUG(control).print("cached metal material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached metal material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "metal")); } else if (mi.matches(glass_cat)) { - DEBUG(control).print("cached glass material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached glass material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "glass")); } else if (mi.matches(gem_cat)) { - DEBUG(control).print("cached gem material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached gem material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "gem")); } else if (mi.matches(clay_cat)) { - DEBUG(control).print("cached clay material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached clay material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "clay")); } else if (mi.matches(cloth_cat)) { - DEBUG(control).print("cached cloth material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached cloth material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "cloth")); } else if (mi.matches(silk_cat)) { - DEBUG(control).print("cached silk material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached silk material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "silk")); } else if (mi.matches(yarn_cat)) { - DEBUG(control).print("cached yarn material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached yarn material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "yarn")); } else - TRACE(control).print("not matched: %s\n", mi.toString().c_str()); + TRACE(control).print("not matched: {}\n", mi.toString()); } static void load_organic_material_cache(df::organic_mat_category cat) { @@ -201,7 +201,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bool unsuspend_on_finalize); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -214,12 +214,12 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector getType(), get_subtype(bld), bld->getCustomType()); if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { - WARN(control).print("loaded state for building %d doesn't match world\n", pb.id); + WARN(control).print("loaded state for building {} doesn't match world\n", pb.id); pb.remove(out); continue; } @@ -354,7 +354,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot configure %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot configure {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -428,8 +428,8 @@ vector getVectorIds(color_ostream &out, const df::job_it // if the filter already has the vector_id set to something specific, use it if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) { - DEBUG(control,out).print("using vector_id from job_item: %s\n", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); + DEBUG(control,out).print("using vector_id from job_item: {}\n", + ENUM_KEY_STR(job_item_vector_id, job_item->vector_id)); ret.push_back(job_item->vector_id); return ret; } @@ -460,7 +460,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bo return false; if (bld->jobs.size() != 1) { - DEBUG(control,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); + DEBUG(control,out).print("unexpected number of jobs: want 1, got {}\n", bld->jobs.size()); return false; } @@ -489,10 +489,10 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bo for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { tasks[vector_id][bucket].emplace_back(id, rev_jitem_index); - DEBUG(control,out).print("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, rev_jitem_index, tasks.size(), + DEBUG(control,out).print("added task: {}/{}/{}, {}; " + "{} vector(s), {} filter bucket(s), {} task(s) in bucket\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket, id, rev_jitem_index, tasks.size(), tasks[vector_id].size(), tasks[vector_id][bucket].size()); } } @@ -520,16 +520,16 @@ static string get_desc_string(color_ostream &out, df::job_item *jitem, static void printStatus(color_ostream &out) { DEBUG(control,out).print("entering buildingplan_printStatus\n"); - out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); + out.print("buildingplan is {}\n\n", is_enabled ? "enabled" : "disabled"); out.print("Current settings:\n"); - out.print(" use blocks: %s\n", config.get_bool(CONFIG_BLOCKS) ? "yes" : "no"); - out.print(" use boulders: %s\n", config.get_bool(CONFIG_BOULDERS) ? "yes" : "no"); - out.print(" use logs: %s\n", config.get_bool(CONFIG_LOGS) ? "yes" : "no"); - out.print(" use bars: %s\n", config.get_bool(CONFIG_BARS) ? "yes" : "no"); - out.print(" plan constructions on tiles with existing constructed floors/ramps when using box select: %s\n", + out.print(" use blocks: {}\n", config.get_bool(CONFIG_BLOCKS) ? "yes" : "no"); + out.print(" use boulders: {}\n", config.get_bool(CONFIG_BOULDERS) ? "yes" : "no"); + out.print(" use logs: {}\n", config.get_bool(CONFIG_LOGS) ? "yes" : "no"); + out.print(" use bars: {}\n", config.get_bool(CONFIG_BARS) ? "yes" : "no"); + out.print(" plan constructions on tiles with existing constructed floors/ramps when using box select: {}\n", config.get_bool(CONFIG_RECONSTRUCT) ? "yes" : "no"); auto burrow = df::burrow::find(config.get_int(CONFIG_BURROW)); - out.print(" ignore building materials in burrow: %s\n", burrow ? burrow->name.c_str() : "none"); + out.print(" ignore building materials in burrow: {}\n", burrow ? burrow->name.c_str() : "none"); out.print("\n"); size_t bld_count = 0; @@ -560,10 +560,10 @@ static void printStatus(color_ostream &out) { } if (bld_count) { - out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + out.print("Waiting for {} item(s) to be produced for {} building(s):\n", total, bld_count); for (auto &count : counts) - out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); + out.print(" {:3} {}{}\n", count.second, count.first, count.second == 1 ? "" : "s"); } else { out.print("Currently no planned buildings\n"); } @@ -571,7 +571,7 @@ static void printStatus(color_ostream &out) { } static bool setSetting(color_ostream &out, string name, bool value) { - DEBUG(control,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); + DEBUG(control,out).print("entering setSetting ({} -> {})\n", name, value ? "true" : "false"); if (name == "blocks") config.set_bool(CONFIG_BLOCKS, value); else if (name == "boulders") @@ -583,7 +583,7 @@ static bool setSetting(color_ostream &out, string name, bool value) { else if (name == "reconstruct") config.set_bool(CONFIG_RECONSTRUCT, value); else { - out.printerr("unrecognized setting: '%s'\n", name.c_str()); + out.printerr("unrecognized setting: '{}'\n", name); return false; } @@ -654,8 +654,8 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ vector *item_ids = NULL, map *counts = NULL) { DEBUG(control,out).print( - "entering scanAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering scanAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = heat_override ? *heat_override : get_heat_safety_filter(key); auto &job_items = get_job_items(out, key); @@ -700,7 +700,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ } } - DEBUG(control,out).print("found matches %d\n", count); + DEBUG(control,out).print("found matches {}\n", count); return count; } @@ -713,8 +713,8 @@ static int getAvailableItems(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); vector item_ids; scanAvailableItems(*out, type, subtype, custom, index, true, false, NULL, &item_ids); Lua::PushVector(L, item_ids); @@ -731,8 +731,8 @@ static int getAvailableItemsByHeat(lua_State *L) { int index = luaL_checkint(L, 4); HeatSafety heat = (HeatSafety)luaL_checkint(L, 5); DEBUG(control,*out).print( - "entering getAvailableItemsByHeat building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getAvailableItemsByHeat building_type={} subtype={} custom={} index={} heat={}\n", + ENUM_AS_STR(type), subtype, custom, index, static_cast(heat)); vector item_ids; scanAvailableItems(*out, type, subtype, custom, index, true, true, &heat, &item_ids); Lua::PushVector(L, item_ids); @@ -756,8 +756,8 @@ static int getGlobalSettings(lua_State *L) { static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(control,out).print( - "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering countAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); int count = scanAvailableItems(out, type, subtype, custom, index, false, false); if (count) return count; @@ -816,8 +816,8 @@ static int setMaterialMaskFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering setMaterialMaskFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering setMaterialMaskFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -846,8 +846,8 @@ static int setMaterialMaskFilter(lua_State *L) { mask |= yarn_cat.whole; } DEBUG(control,*out).print( - "setting material mask filter for building_type=%d subtype=%d custom=%d index=%d to %x\n", - type, subtype, custom, index, mask); + "setting material mask filter for building_type={} subtype={} custom={} index={} to {:x}\n", + ENUM_AS_STR(type), subtype, custom, index, mask); ItemFilter filter = filters[index]; filter.setMaterialMask(mask); set new_mats; @@ -875,8 +875,8 @@ static int getMaterialMaskFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key); if (index < 0 || filters.getItemFilters().size() <= (size_t)index) @@ -906,8 +906,8 @@ static int setMaterialFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering setMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering setMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -920,8 +920,8 @@ static int setMaterialFilter(lua_State *L) { mats.emplace(mat_cache.at(mat).first); } DEBUG(control,*out).print( - "setting material filter for building_type=%d subtype=%d custom=%d index=%d to %zd materials\n", - type, subtype, custom, index, mats.size()); + "setting material filter for building_type={} subtype={} custom={} index={} to {} materials\n", + ENUM_AS_STR(type), subtype, custom, index, mats.size()); ItemFilter filter = filters[index]; filter.setMaterials(mats); // ensure relevant masks are explicitly enabled @@ -963,8 +963,8 @@ static int getMaterialFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -1004,8 +1004,8 @@ static int getMaterialFilter(lua_State *L) { static void setChooseItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int choose) { DEBUG(control,out).print( - "entering setChooseItems building_type=%d subtype=%d custom=%d choose=%d\n", - type, subtype, custom, choose); + "entering setChooseItems building_type={} subtype={} custom={} choose={}\n", + ENUM_AS_STR(type), subtype, custom, choose); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); filters.setChooseItems(choose); @@ -1020,8 +1020,8 @@ static int getChooseItems(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getChooseItems building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getChooseItems building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); Lua::Push(L, get_item_filters(*out, key).getChooseItems()); return 1; @@ -1045,8 +1045,8 @@ static int getHeatSafetyFilter(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getHeatSafetyFilter building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); Lua::Push(L, heat); @@ -1069,8 +1069,8 @@ static int getSpecials(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getSpecials building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getSpecials building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); Lua::Push(L, get_item_filters(*out, key).getSpecials()); return 1; @@ -1100,8 +1100,8 @@ static int getQualityFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getQualityFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getQualityFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 1a97a801de1..7e8187dfce4 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -77,7 +77,7 @@ static bool isAccessible(color_ostream& out, df::item* item) { df::coord item_pos = Items::getPosition(item); uint16_t walkability_group = Maps::getWalkableGroup(item_pos); bool is_walkable = accessible_walkability_groups.contains(walkability_group); - TRACE(cycle, out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + TRACE(cycle, out).print("item {} in walkability_group {} at ({},{},{}) is {}accessible from job site\n", item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "(probably) " : "not "); return is_walkable; } @@ -192,7 +192,7 @@ bool isJobReady(color_ostream &out, const std::vector &jitems) { int needed_items = 0; for (auto job_item : jitems) { needed_items += job_item->quantity; } if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + DEBUG(cycle,out).print("building needs {} more item(s)\n", needed_items); return false; } return true; @@ -208,7 +208,7 @@ static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { // remove them to keep the "finalize with buildingplan active" path as similar // as possible to the "finalize with buildingplan disabled" path. void finalizeBuilding(color_ostream &out, df::building *bld, bool unsuspend_on_finalize) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + DEBUG(cycle,out).print("finalizing building {}\n", bld->id); auto job = bld->jobs[0]; // sort the items so they get added to the structure in the correct order @@ -255,7 +255,7 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, if (bld && bld->jobs[0]->job_items.elements[task.second]->quantity) return bld; } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + DEBUG(cycle,out).print("discarding invalid task: bld={}, job_item_idx={}\n", id, task.second); task_queue.pop_front(); } return NULL; @@ -273,9 +273,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); const auto item_vector = df::global::world->items.other[other_id]; - DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + DEBUG(cycle,out).print("matching {} item(s) in vector {} against {} filter bucket(s)\n", item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + ENUM_KEY_STR(job_item_vector_id, vector_id), buckets.size()); // items we might want to attach (and their positions) @@ -287,12 +287,12 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, std::vector> matching; size_t num_matching = 0; - DEBUG(cycle,out).print("%zu items available for assignment\n", available.size()); + DEBUG(cycle,out).print("{} items available for assignment\n", available.size()); for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - TRACE(cycle,out).print("scanning bucket: %s/%s\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), bucket_it->first.c_str()); + TRACE(cycle,out).print("scanning bucket: {}/{}\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), bucket_it->first); auto & task_queue = bucket_it->second; bool first_task = true; @@ -322,7 +322,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, num_matching = matching.size(); first_task = false; - TRACE(cycle,out).print("first task in bucket: found %zu matching items\n", + TRACE(cycle,out).print("first task in bucket: found {} matching items\n", num_matching); } // every task: find and attach closest matching item (if any) @@ -342,15 +342,15 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, material.decode(item); ItemTypeInfo item_type; item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s (distance %d) to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), + DEBUG(cycle,out).print("attached {} {} (distance {}) to filter {} for {}({}): {}/{}\n", + material.toString(), + item_type.toString(), distance(closest->first, jpos), filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), + ENUM_KEY_STR(building_type, bld->getType()), id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket_it->first); // clean up fulfilled task task_queue.pop_front(); // keep quantity aligned with the actual number of remaining @@ -373,10 +373,10 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, if (task_queue.empty()) { DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); + "removing empty item bucket: {}/{}; {} left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket_it->first, + buckets.size() - 1); bucket_it = buckets.erase(bucket_it); } else { ++bucket_it; @@ -420,8 +420,8 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, auto & buckets = it->second; doVector(out, vector_id, buckets, planned_buildings, unsuspend_on_finalize); if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + DEBUG(cycle,out).print("removing empty vector: {}; {} vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), tasks.size() - 1); it = tasks.erase(it); } @@ -434,12 +434,12 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, auto & buckets = tasks[vector_id]; doVector(out, vector_id, buckets, planned_buildings, unsuspend_on_finalize); if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + DEBUG(cycle,out).print("removing empty vector: {}; {} vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), tasks.size() - 1); tasks.erase(vector_id); } } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + DEBUG(cycle,out).print("cycle done; {} registered building(s) left\n", planned_buildings.size()); } diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp index 710878d643c..e8136b88960 100644 --- a/plugins/buildingplan/buildingtypekey.cpp +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -21,7 +21,7 @@ static BuildingTypeKey deserialize(color_ostream &out, const std::string &serial vector key_parts; split_string(&key_parts, serialized, ","); if (key_parts.size() != 3) { - WARN(control,out).print("invalid key_str: '%s'\n", serialized.c_str()); + WARN(control,out).print("invalid key_str: '{}'\n", serialized); return BuildingTypeKey(df::building_type::NONE, -1, -1); } return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h index 81bb043c556..5b21b6cc2c2 100644 --- a/plugins/buildingplan/buildingtypekey.h +++ b/plugins/buildingplan/buildingtypekey.h @@ -17,6 +17,17 @@ struct BuildingTypeKey : public std::tuple std::string serialize() const; }; +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const BuildingTypeKey& key, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key)), ctx); + } +}; + struct BuildingTypeKeyHash { std::size_t operator() (const BuildingTypeKey & key) const; }; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index eaa11335d09..0e1cab8548a 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -40,8 +40,8 @@ static string serialize(const std::vector &item_filters, const std:: DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) : key(key), choose_items(ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER) { - DEBUG(control,out).print("creating persistent data for filter key %d,%d,%d\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key)); + DEBUG(control,out).print("creating persistent data for filter key {},{},{}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key)); filter_config = World::AddPersistentSiteData(FILTER_CONFIG_KEY); filter_config.set_int(FILTER_CONFIG_TYPE, std::get<0>(key)); filter_config.set_int(FILTER_CONFIG_SUBTYPE, std::get<1>(key)); @@ -61,16 +61,16 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &f choose_items > ItemSelectionChoice::ITEM_SELECTION_CHOICE_AUTOMATERIAL) choose_items = ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER; auto &serialized = filter_config.get_str(); - DEBUG(control,out).print("deserializing default item filters for key %d,%d,%d: %s\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + DEBUG(control,out).print("deserializing default item filters for key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), serialized); if (!jitems.size()) return; std::vector elems; split_string(&elems, serialized, "|"); std::vector filters = deserialize_item_filters(out, elems[0]); if (filters.size() != jitems.size()) { - WARN(control,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + WARN(control,out).print("ignoring invalid filters_str for key {},{},{}: '{}'\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), serialized); item_filters.resize(jitems.size()); } else item_filters = filters; @@ -99,13 +99,13 @@ void DefaultItemFilters::setSpecial(const std::string &special, bool val) { void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { if (index < 0 || item_filters.size() <= (size_t)index) { - WARN(control,out).print("invalid index for filter key %d,%d,%d: %d\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + WARN(control,out).print("invalid index for filter key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), index); return; } item_filters[index] = filter; filter_config.set_str( serialize(item_filters, specials)); - DEBUG(control,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.get_str().c_str()); + DEBUG(control,out).print("updated item filter and persisted for key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), filter_config.get_str()); } diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index 0e4569f156a..09a4a15c8e7 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -41,7 +41,7 @@ static bool deserializeMaterialMask(const string& ser, df::dfhack_material_categ return true; if (!parseJobMaterialCategory(&mat_mask, ser)) { - DEBUG(control).print("invalid job material category serialization: '%s'", ser.c_str()); + DEBUG(control).print("invalid job material category serialization: '{}'", ser); return false; } return true; @@ -56,7 +56,7 @@ static bool deserializeMaterials(const string& ser, set &m for (auto m = mat_names.begin(); m != mat_names.end(); m++) { DFHack::MaterialInfo material; if (!material.find(*m) || !material.isValid()) { - DEBUG(control).print("invalid material name serialization: '%s'", ser.c_str()); + DEBUG(control).print("invalid material name serialization: '{}'", ser); return false; } materials.emplace(material); @@ -68,7 +68,7 @@ ItemFilter::ItemFilter(color_ostream &out, const string& serialized) : ItemFilte vector tokens; split_string(&tokens, serialized, "/"); if (tokens.size() < 5) { - DEBUG(control,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + DEBUG(control,out).print("invalid ItemFilter serialization: '{}'", serialized); return; } @@ -155,8 +155,8 @@ bool ItemFilter::matches(DFHack::MaterialInfo &material) const { bool ItemFilter::matches(df::item *item) const { int16_t quality = (item->flags.bits.artifact ? df::item_quality::Artifact : item->getQuality()); if (quality < min_quality || quality > max_quality) { - TRACE(cycle).print("item outside of quality range (%d not between %d and %d)\n", - quality, min_quality, max_quality); + TRACE(cycle).print("item outside of quality range ({} not between {} and {})\n", + ENUM_AS_STR(static_cast(quality)), ENUM_AS_STR(min_quality), ENUM_AS_STR(max_quality)); return false; } diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index 5888d169e76..e0f5a8984e9 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -41,8 +41,8 @@ static vector> deserialize_vector_ids(color_ostre split_string(&rawstrs, bld_config.get_str(), "|"); const string &serialized = rawstrs[0]; - DEBUG(control,out).print("deserializing vector ids for building %d: %s\n", - bld_config.get_int(BLD_CONFIG_ID), serialized.c_str()); + DEBUG(control,out).print("deserializing vector ids for building {}: {}\n", + bld_config.get_int(BLD_CONFIG_ID), serialized); vector joined; split_string(&joined, serialized, ";"); @@ -100,12 +100,12 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafe : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), item_filters(item_filters.getItemFilters()), specials(item_filters.getSpecials()) { - DEBUG(control,out).print("creating persistent data for building %d\n", id); + DEBUG(control,out).print("creating persistent data for building {}\n", id); bld_config = World::AddPersistentSiteData(BLD_CONFIG_KEY); bld_config.set_int(BLD_CONFIG_ID, id); bld_config.set_int(BLD_CONFIG_HEAT, heat_safety); bld_config.set_str(serialize(vector_ids, item_filters)); - DEBUG(control,out).print("serialized state for building %d: %s\n", id, bld_config.get_str().c_str()); + DEBUG(control,out).print("serialized state for building {}: {}\n", id, bld_config.get_str()); } PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index b016014d0c2..172a7d37d20 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -47,7 +47,7 @@ static void jobStartedHandler(color_ostream& out, void* ptr); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status, out).print("initializing %s\n", plugin_name); + DEBUG(status, out).print("initializing {}\n", plugin_name); commands.push_back( PluginCommand("burrow", "Quickly adjust burrow tiles and units.", @@ -62,7 +62,7 @@ static void reset() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(status, out).print("%s from the API\n", is_enabled ? "enabled" : "disabled"); + DEBUG(status, out).print("{} from the API\n", is_enabled ? "enabled" : "disabled"); reset(); if (enable) { init_diggers(out); @@ -73,13 +73,13 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } } else { - DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + DEBUG(status, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(status, out).print("shutting down %s\n", plugin_name); + DEBUG(status, out).print("shutting down {}\n", plugin_name); reset(); return CR_OK; } @@ -92,7 +92,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -136,8 +136,8 @@ static void jobStartedHandler(color_ostream& out, void* ptr) { return; const df::coord &pos = job->pos; - DEBUG(event, out).print("dig job started: id=%d, pos=(%d,%d,%d), type=%s\n", - job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); + DEBUG(event, out).print("dig job started: id={}, pos=({}, {}, {}), type={}\n", + job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type)); df::tiletype *tt = Maps::getTileType(pos); if (tt) active_dig_jobs[pos] = *tt; @@ -199,8 +199,8 @@ static void jobCompletedHandler(color_ostream& out, void* ptr) { return; const df::coord &pos = job->pos; - DEBUG(event, out).print("dig job completed: id=%d, pos=(%d,%d,%d), type=%s\n", - job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); + DEBUG(event, out).print("dig job completed: id={}, pos=({}, {}, {}), type={}\n", + job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type)); df::tiletype prev_tt = active_dig_jobs[pos]; df::tiletype *tt = Maps::getTileType(pos); @@ -544,7 +544,7 @@ static void flood_fill(lua_State *L, bool enable) { bool start_outside = is_outside(start_pos, start_des); bool start_hidden = start_des->bits.hidden; uint16_t start_walk = Maps::getWalkableGroup(start_pos); - DEBUG(status).print("starting pos: (%d,%d,%d); outside: %d; hidden: %d\n", + DEBUG(status).print("starting pos: ({},{},{}); outside: {}; hidden: {}\n", start_pos.x, start_pos.y, start_pos.z, start_outside, start_hidden); std::stack flood; @@ -554,7 +554,7 @@ static void flood_fill(lua_State *L, bool enable) { const df::coord pos = flood.top(); flood.pop(); - TRACE(status).print("pos: (%d,%d,%d)\n", pos.x, pos.y, pos.z); + TRACE(status).print("pos: ({},{},{})\n", pos.x, pos.y, pos.z); df::tile_designation *des = Maps::getTileDesignation(pos); if (!des || diff --git a/plugins/changeitem.cpp b/plugins/changeitem.cpp index 33387aa1417..e07db445706 100644 --- a/plugins/changeitem.cpp +++ b/plugins/changeitem.cpp @@ -236,7 +236,7 @@ command_result df_changeitem(color_ostream &out, vector & parameters) changeitem_execute(out, item, info, force, change_material, new_material, change_quality, new_quality, change_subtype, new_subtype); processed_total++; } - out.print("Done. %d items processed.\n", processed_total); + out.print("Done. {} items processed.\n", processed_total); } else { diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index c1a5b5953f1..71cfdddb8d7 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -111,7 +111,7 @@ void ChannelGroups::add(const df::coord &map_pos) { // we already have group "prime" if you will, so we're going to merge the new find into prime Group &group2 = groups.at(index2); // merge - TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(), + TRACE(groups).print(" -> merging two groups. group 1 size: {}. group 2 size: {}\n", group->size(), group2.size()); for (auto pos2: group2) { group->emplace(pos2); @@ -119,7 +119,7 @@ void ChannelGroups::add(const df::coord &map_pos) { } group2.clear(); free_spots.emplace(index2); - TRACE(groups).print(" merged size: %zu\n", group->size()); + TRACE(groups).print(" merged size: {}\n", group->size()); } } } @@ -143,13 +143,13 @@ void ChannelGroups::add(const df::coord &map_pos) { } // puts the "add" in "ChannelGroups::add" group->emplace(map_pos); - DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size()); + DEBUG(groups).print(" = group[{}] of (" COORD ") is size: {}\n", group_index, COORDARGS(map_pos), group->size()); // we may have performed a merge, so we update all the `coord -> group index` mappings for (auto &wpos: *group) { groups_map[wpos] = group_index; } - DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size()); + DEBUG(groups).print(" <- add() exits, there are {} mappings\n", groups_map.size()); } // scans a single tile for channel designations @@ -192,7 +192,7 @@ void ChannelGroups::scan(bool full_scan) { std::set gone_jobs; set_difference(last_jobs, jobs, gone_jobs); set_difference(jobs, last_jobs, new_jobs); - INFO(groups).print("gone jobs: %zd\nnew jobs: %zd\n",gone_jobs.size(), new_jobs.size()); + INFO(groups).print("gone jobs: {}\nnew jobs: {}\n",gone_jobs.size(), new_jobs.size()); for (auto &pos : new_jobs) { add(pos); } @@ -236,7 +236,7 @@ void ChannelGroups::scan(bool full_scan) { for (df::block_square_event* event: block->block_events) { if (auto evT = virtual_cast(event)) { // we want to let the user keep some designations free of being managed - TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]); + TRACE(groups).print(" tile designation priority: {}\n", evT->priority[lx][ly]); if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { if (empty_group) { group_blocks.emplace(block); @@ -338,9 +338,9 @@ void ChannelGroups::debug_groups() { int idx = 0; DEBUG(groups).print(" debugging group data\n"); for (auto &group: groups) { - DEBUG(groups).print(" group %d (size: %zu)\n", idx, group.size()); + DEBUG(groups).print(" group {} (size: {})\n", idx, group.size()); for (auto &pos: group) { - DEBUG(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(groups).print(" ({},{},{})\n", pos.x, pos.y, pos.z); } idx++; } @@ -350,9 +350,9 @@ void ChannelGroups::debug_groups() { // prints debug info group mappings void ChannelGroups::debug_map() { if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) { - INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); + INFO(groups).print("Group Mappings: {}\n", groups_map.size()); for (auto &pair: groups_map) { - TRACE(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second); + TRACE(groups).print(" map[" COORD "] = {}\n", COORDARGS(pair.first), pair.second); } } } diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 67f8742b381..6c85fda951b 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -89,7 +89,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // the tile below is not solid earth // we're counting accessibility then dealing with 0 access DEBUG(manager).print("analysis: cave-in condition found\n"); - INFO(manager).print("(%d) checking accessibility of (" COORD ") from (" COORD ")\n", cavein_possible,COORDARGS(pos),COORDARGS(miner_pos)); + INFO(manager).print("({}) checking accessibility of ({}) from ({})\n", cavein_possible,pos,miner_pos); auto access = count_accessibility(miner_pos, pos); if (access == 0) { TRACE(groups).print(" has 0 access\n"); @@ -100,7 +100,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // todo: engage dig management, swap channel designations for dig } } else { - WARN(manager).print(" has %d access\n", access); + WARN(manager).print(" has {} access\n", access); cavein_possible = config.riskaverse; cavein_candidates.emplace(pos, access); least_access = std::min(access, least_access); @@ -110,13 +110,13 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool dig_now(DFHack::Core::getInstance().getConsole(), pos); } } - DEBUG(manager).print("cavein possible(%d)\n" - "%zu candidates\n" - "least access %d\n", cavein_possible, cavein_candidates.size(), least_access); + DEBUG(manager).print("cavein possible({})\n" + "{} candidates\n" + "least access {}\n", cavein_possible, cavein_candidates.size(), least_access); } // managing designations if (!group.empty()) { - DEBUG(manager).print("managing group #%d\n", groups.debugGIndex(*group.begin())); + DEBUG(manager).print("managing group #{}\n", groups.debugGIndex(*group.begin())); } for (auto &pos: group) { // if no cave-in is possible [or we don't check for], we'll just execute normally and move on @@ -132,7 +132,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool if (CSP::dignow_queue.count(pos) || (cavein_candidates.count(pos) && least_access < MAX && cavein_candidates[pos] <= least_access+OFFSET)) { - TRACE(manager).print("cave-in evaluated true and either of dignow or (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + TRACE(manager).print("cave-in evaluated true and either of dignow or ({} <= {})\n", cavein_candidates[pos], least_access+OFFSET); df::coord local(pos); local.x %= 16; local.y %= 16; @@ -143,7 +143,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // we want to let the user keep some designations free of being managed auto b = std::max(0, cavein_candidates[pos] - least_access); auto v = 1000 + (b * 1700); - DEBUG(manager).print("(" COORD ") 1000+1000(%d) -> %d {least-access: %d}\n",COORDARGS(pos), b, v, least_access); + DEBUG(manager).print("({}) 1000+1000({}) -> {} {least-access: {}}\n", pos, b, v, least_access); evT->priority[Coord(local)] = v; } } @@ -152,7 +152,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool } // cavein possible, but we failed to meet the criteria for activation if (cavein_candidates.count(pos)) { - DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as ({} <= {})\n", cavein_candidates[pos], least_access+OFFSET); } else { DEBUG(manager).print("cave-in evaluated true and the position was not a candidate, nor was it set for dignow\n"); } @@ -163,7 +163,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) const { if (Maps::isValidTilePos(map_pos)) { - TRACE(manager).print("manage_one((" COORD "), %d, %d)\n", COORDARGS(map_pos), set_marker_mode, marker_mode); + TRACE(manager).print("manage_one(({}), {}, {})\n", map_pos, set_marker_mode, marker_mode); df::map_block* block = Maps::getTileBlock(map_pos); // we calculate the position inside the block* df::coord local(map_pos); @@ -188,7 +188,7 @@ bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, block->flags.bits.designated = true; } tile_occupancy.bits.dig_marked = marker_mode; - TRACE(manager).print("marker mode %s\n", marker_mode ? "ENABLED" : "DISABLED"); + TRACE(manager).print("marker mode {}\n", marker_mode ? "ENABLED" : "DISABLED"); } else { // if we are though, it should be totally safe to dig tile_occupancy.bits.dig_marked = false; diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 4aa16484535..ac69ffdc8e4 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -182,7 +182,7 @@ namespace CSP { psetting.ival(IGNORE_THRESH) = config.ignore_threshold; psetting.ival(FALL_THRESH) = config.fall_threshold; } catch (std::exception &e) { - ERR(plugin).print("%s\n", e.what()); + ERR(plugin).print("{}\n", e.what()); } } } @@ -208,7 +208,7 @@ namespace CSP { config.refresh_freq = psetting.ival(REFRESH_RATE); config.monitor_freq = psetting.ival(MONITOR_RATE); } catch (std::exception &e) { - ERR(plugin).print("%s\n", e.what()); + ERR(plugin).print("{}\n", e.what()); } } active_workers.clear(); @@ -320,14 +320,14 @@ namespace CSP { dignow_queue.emplace(report->pos); } - DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), - COORDARGS(report->pos2), report->text.c_str()); + DEBUG(plugin).print("{}, pos: {} , pos2: {}\n{}\n", report_id, report->pos, + report->pos2, report->text.c_str()); } break; case announcement_type::CAVE_COLLAPSE: if (config.resurrect) { - DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), - COORDARGS(report->pos2), report->text.c_str()); + DEBUG(plugin).print("CAVE IN\n{}, pos: {} , pos2: {}\n{}\n", report_id, report->pos, + report->pos2, report->text.c_str()); df::coord below = report->pos; below.z -= 1; @@ -344,12 +344,12 @@ namespace CSP { Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax)); for (auto unit: units) { endangered_units[unit] = tick; - DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id); + DEBUG(plugin).print(" [id {}] was near a cave in.\n", unit->id); } for (auto unit : world->units.all) { if (last_safe.count(unit->id)) { endangered_units[unit] = tick; - DEBUG(plugin).print(" [id %d] is/was a worker, we'll track them too.\n", unit->id); + DEBUG(plugin).print(" [id {}] is/was a worker, we'll track them too.\n", unit->id); } } } @@ -472,7 +472,7 @@ namespace CSP { // clean up any "endangered" workers that have been tracked 100 ticks or more for (auto iter = endangered_units.begin(); iter != endangered_units.end();) { if (tick - iter->second >= 1200) { //keep watch 1 day - DEBUG(plugin).print("It has been one day since [id %d]'s last incident.\n", iter->first->id); + DEBUG(plugin).print("It has been one day since [id {}]'s last incident.\n", iter->first->id); iter = endangered_units.erase(iter); continue; } @@ -525,7 +525,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + outs without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -582,7 +582,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_ev command_result channel_safely(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + outs without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -650,23 +650,23 @@ command_result channel_safely(color_ostream &out, std::vector ¶ return DFHack::CR_WRONG_USAGE; } } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); + out.printerr("{}\n", e.what()); return DFHack::CR_FAILURE; } } } else { - out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED."); + out.print("Channel-Safely is {}\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "risk-averse: ", config.riskaverse ? "on." : "off."); - out.print(" %-20s\t%s\n", "monitoring: ", config.monitoring ? "on." : "off."); - out.print(" %-20s\t%s\n", "require-vision: ", config.require_vision ? "on." : "off."); + out.print(" {:<20}\t{}\n", "risk-averse: ", config.riskaverse ? "on." : "off."); + out.print(" {:<20}\t{}\n", "monitoring: ", config.monitoring ? "on." : "off."); + out.print(" {:<20}\t{}\n", "require-vision: ", config.require_vision ? "on." : "off."); //out.print(" %-20s\t%s\n", "insta-dig: ", config.insta_dig ? "on." : "off."); - out.print(" %-20s\t%s\n", "resurrect: ", config.resurrect ? "on." : "off."); + out.print(" {:<20}\t{}\n", "resurrect: ", config.resurrect ? "on." : "off."); out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "refresh-freq: ", config.refresh_freq); - out.print(" %-20s\t%" PRIi32 "\n", "monitor-freq: ", config.monitor_freq); - out.print(" %-20s\t%" PRIu8 "\n", "ignore-threshold: ", config.ignore_threshold); - out.print(" %-20s\t%" PRIu8 "\n", "fall-threshold: ", config.fall_threshold); + out.print(" {:<20}\t{}\n", "refresh-freq: ", config.refresh_freq); + out.print(" {:<20}\t{}\n", "monitor-freq: ", config.monitor_freq); + out.print(" {:<20}\t{}\n", "ignore-threshold: ", config.ignore_threshold); + out.print(" {:<20}\t{}\n", "fall-threshold: ", config.fall_threshold); } CSP::SaveSettings(); return DFHack::CR_OK; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 362fd927a34..16db1ff7d26 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -311,7 +311,7 @@ inline bool dig_now(color_ostream &out, const df::coord &map_pos) { // fully heals the unit specified, resurrecting if need be inline void resurrect(color_ostream &out, const int32_t &unit) { out.color(DFHack::COLOR_RED); - out.print("channel-safely: resurrecting [id: %d]\n", unit); + out.print("channel-safely: resurrecting [id: {}]\n", unit); std::vector params{"-r", "--unit", std::to_string(unit)}; Core::getInstance().runCommand(out,"full-heal", params); } diff --git a/plugins/cleanconst.cpp b/plugins/cleanconst.cpp index 895f182e760..9e05ecae465 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -38,7 +38,7 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) df::construction *cons = df::construction::find(pos); if (!cons) { - out.printerr("Item at %i,%i,%i marked as construction but no construction is present!\n", pos.x, pos.y, pos.z); + out.printerr("Item at {} marked as construction but no construction is present!\n", pos); continue; } // if the construction is already labeled as "no build item", then leave it alone @@ -62,7 +62,7 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) cleaned_total++; } - out.print("Done. %d construction items cleaned up.\n", cleaned_total); + out.print("Done. {} construction items cleaned up.\n", cleaned_total); return CR_OK; } diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp index 6706bc92d82..74bc40e6ff2 100644 --- a/plugins/cleaners.cpp +++ b/plugins/cleaners.cpp @@ -95,7 +95,7 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud, bool item_spat } if(num_blocks) - out.print("Cleaned %d of %zd map blocks.\n", num_blocks, world->map.map_blocks.size()); + out.print("Cleaned {} of {} map blocks.\n", num_blocks, world->map.map_blocks.size()); return CR_OK; } @@ -123,7 +123,7 @@ command_result cleanitems (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d items.\n", cleaned_total, cleaned_items); + out.print("Removed {} contaminants from {} items.\n", cleaned_total, cleaned_items); return CR_OK; } @@ -143,7 +143,7 @@ command_result cleanunits (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d creatures.\n", cleaned_total, cleaned_units); + out.print("Removed {} contaminants from {} creatures.\n", cleaned_total, cleaned_units); return CR_OK; } @@ -163,7 +163,7 @@ command_result cleanplants (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d plants.\n", cleaned_total, cleaned_plants); + out.print("Removed {} contaminants from {} plants.\n", cleaned_total, cleaned_plants); return CR_OK; } diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 9e6ddb98492..c8d67d92ad0 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -47,7 +47,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) command_result df_cleanowned (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -155,16 +155,16 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) std::string description; item->getItemDescription(&description, 0); out.print( - "[%d] %s (wear level %d)", + "[{}] {} (wear level {})", item->id, - DF2CONSOLE(description).c_str(), + DF2CONSOLE(description), item->getWear() ); df::unit *owner = Items::getOwner(item); if (owner) - out.print(", owner %s", DF2CONSOLE(Units::getReadableName(owner)).c_str()); + out.print(", owner {}", DF2CONSOLE(Units::getReadableName(owner))); if (!dry_run) { diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index fba821b2703..36ecdacf4ad 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -144,7 +144,7 @@ static inline bool select_caste_mat(color_ostream &out, vector &tokens, out.printerr("You must also specify a caste.\n"); else out.printerr("The creature you specified has no such caste!\n"); - out.printerr("Valid castes:%s\n", castes.c_str()); + out.printerr("Valid castes:{}\n", castes); return false; } } @@ -198,7 +198,7 @@ static inline bool select_plant_growth(color_ostream &out, vector &token out.printerr("You must also specify a growth ID.\n"); else out.printerr("The plant you specified has no such growth!\n"); - out.printerr("Valid growths:%s\n", growths.c_str()); + out.printerr("Valid growths:{}\n", growths); return false; } } @@ -226,7 +226,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { ItemTypeInfo iinfo(item->getType(), item->getSubtype()); MaterialInfo minfo(item->getMaterial(), item->getMaterialIndex()); - out.print("%s %s\n", iinfo.getToken().c_str(), minfo.getToken().c_str()); + out.print("{} {}\n", iinfo.getToken(), minfo.getToken()); return CR_OK; } else if (parameters[0] == "floor") { @@ -268,7 +268,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { dest_container = item->id; string name; item->getItemDescription(&name, 0); - out.print("Items created will be placed inside %s.\n", name.c_str()); + out.print("Items created will be placed inside {}.\n", name); return CR_OK; } else if (parameters[0] == "building") { @@ -308,7 +308,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { dest_building = building->id; string name; building->getName(&name); - out.print("Items created will be placed inside %s.\n", name.c_str()); + out.print("Items created will be placed inside {}.\n", name); return CR_OK; } else diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 0762ad9c36e..f0651889a63 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -182,10 +182,10 @@ command_result cursecheck(color_ostream& out, vector & parameters) out << DF2CONSOLE(Units::getReadableName(unit)); auto death = df::incident::find(unit->counters.death_id); - out.print(", born in %d, cursed in %d to be a %s. (%s%s)\n", + out.print(", born in {}, cursed in {} to be a {}. ({}{})\n", unit->birth_year, unit->curse_year, - curse_names[cursetype].c_str(), + curse_names[cursetype], // technically most cursed creatures are undead, // therefore output 'active' if they are not completely dead unit->flags2.bits.killed ? "deceased" : "active", @@ -203,15 +203,15 @@ command_result cursecheck(color_ostream& out, vector & parameters) if (giveUnitID) { - out.print("Creature %d, race %d (%x)\n", unit->id, unit->race, unit->race); + out.print("Creature {}, race {} (0x{:x})\n", unit->id, unit->race, unit->race); } } } if (selected_unit && !giveDetails) - out.print("Selected unit is %scursed\n", cursecount == 0 ? "not " : ""); + out.print("Selected unit is {}cursed\n", cursecount == 0 ? "not " : ""); else if (!selected_unit) - out.print("%zd cursed creatures on map\n", cursecount); + out.print("{} cursed creatures on map\n", cursecount); return CR_OK; } diff --git a/plugins/cxxrandom.cpp b/plugins/cxxrandom.cpp index 3f6a4055985..a471cda6be5 100644 --- a/plugins/cxxrandom.cpp +++ b/plugins/cxxrandom.cpp @@ -140,7 +140,7 @@ class NumberSequence } void Print() { for( auto v : m_numbers ) { - cout->print( "%" PRId64 " ", v ); + cout->print("{} ", v); } } }; diff --git a/plugins/debug.cpp b/plugins/debug.cpp index 90e5c783d6d..51e4a8300b7 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -804,9 +804,9 @@ static command_result setFilter(color_ostream& out, return v.match(level); }); if (iter == levelNames.end()) { - ERR(command,out).print("level ('%s') parameter must be one of " + ERR(command,out).print("level ('{}') parameter must be one of " "trace, debug, info, warning, error.\n", - parameters[pos].c_str()); + parameters[pos]); return CR_WRONG_USAGE; } @@ -1062,8 +1062,8 @@ CommandDispatch::dispatch_t CommandDispatch::dispatch { static command_result commandDebugFilter(color_ostream& out, std::vector& parameters) { - DEBUG(command,out).print("debugfilter %s, parameter count %zu\n", - parameters.size() > 0 ? parameters[0].c_str() : "", + DEBUG(command,out).print("debugfilter {}, parameter count {}\n", + parameters.size() > 0 ? parameters[0] : "", parameters.size()); auto iter = CommandDispatch::dispatch.end(); if (0u < parameters.size()) @@ -1096,7 +1096,7 @@ DFhackCExport DFHack::command_result plugin_init(DFHack::color_ostream& out, filter.apply(*cat); } } - INFO(init,out).print("plugin_init with %zu commands, %zu filters and %zu categories\n", + INFO(init,out).print("plugin_init with {}, {} filters and {} categories\n", commands.size(), filMan.size(), catMan.size()); filMan.connectTo(catMan.categorySignal); return rv; diff --git a/plugins/deramp.cpp b/plugins/deramp.cpp index 726693b416b..6b41701225d 100644 --- a/plugins/deramp.cpp +++ b/plugins/deramp.cpp @@ -98,9 +98,9 @@ command_result df_deramp (color_ostream &out, vector & parameters) } } if (count) - out.print("Found and changed %d tiles.\n", count); + out.print("Found and changed {} tiles.\n", count); if (countbad) - out.print("Fixed %d bad down ramps.\n", countbad); + out.print("Fixed {} bad down ramps.\n", countbad); return CR_OK; } diff --git a/plugins/devel/buildprobe.cpp b/plugins/devel/buildprobe.cpp index 333f1d3bc4e..3b4df00e28c 100644 --- a/plugins/devel/buildprobe.cpp +++ b/plugins/devel/buildprobe.cpp @@ -55,7 +55,7 @@ command_result readFlag (color_ostream &out, vector & parameters) MapExtras::MapCache * MCache = new MapExtras::MapCache(); t_occupancy oc = MCache->occupancyAt(cursor); - out.print("Current Value: %d\n", oc.bits.building); + out.print("Current Value: {}\n", ENUM_AS_STR(oc.bits.building)); return CR_OK; } diff --git a/plugins/devel/check-structures-sanity/dispatch.cpp b/plugins/devel/check-structures-sanity/dispatch.cpp index 0c0b3453b4a..162ea7bbd45 100644 --- a/plugins/devel/check-structures-sanity/dispatch.cpp +++ b/plugins/devel/check-structures-sanity/dispatch.cpp @@ -118,7 +118,7 @@ void Checker::queue_globals() // offset is the position of the DFHack pointer to this global. auto ptr = *reinterpret_cast(field->offset); - QueueItem item(stl_sprintf("df.global.%s", field->name), ptr); + QueueItem item(fmt::format("df.global.{}", field->name), ptr); CheckedStructure cs(field); if (!ptr) @@ -886,7 +886,7 @@ void Checker::check_stl_string(const QueueItem & item) else if (is_gcc && length > 0 && !is_valid_dereference(QueueItem(item, "?start?", reinterpret_cast(string->start)), 1)) { // nullptr is NOT okay here - FAIL("invalid string pointer " << stl_sprintf("0x%" PRIxPTR, string->start)); + FAIL("invalid string pointer " << fmt::format("{:#x}", string->start)); return; } else if (is_local && length >= 16) diff --git a/plugins/devel/check-structures-sanity/main.cpp b/plugins/devel/check-structures-sanity/main.cpp index 5bc7c09d6db..6110afe1c07 100644 --- a/plugins/devel/check-structures-sanity/main.cpp +++ b/plugins/devel/check-structures-sanity/main.cpp @@ -77,7 +77,7 @@ static command_result command(color_ostream & out, std::vector & pa } \ catch (std::exception & ex) \ { \ - out.printerr("check-structures-sanity: argument to -%s: %s\n", #name, ex.what()); \ + out.printerr("check-structures-sanity: argument to -{}: {}\n", #name, ex.what()); \ return CR_WRONG_USAGE; \ } \ } diff --git a/plugins/devel/check-structures-sanity/types.cpp b/plugins/devel/check-structures-sanity/types.cpp index 647fa6141ec..4c8a8f6ae26 100644 --- a/plugins/devel/check-structures-sanity/types.cpp +++ b/plugins/devel/check-structures-sanity/types.cpp @@ -10,7 +10,7 @@ QueueItem::QueueItem(const QueueItem & parent, const std::string & member, const { } QueueItem::QueueItem(const QueueItem & parent, size_t index, const void *ptr) : - QueueItem(parent.path + stl_sprintf("[%zu]", index), ptr) + QueueItem(parent.path + fmt::format("[{}]", index), ptr) { } diff --git a/plugins/devel/check-structures-sanity/validate.cpp b/plugins/devel/check-structures-sanity/validate.cpp index 24d5327f92c..d6137eff1f3 100644 --- a/plugins/devel/check-structures-sanity/validate.cpp +++ b/plugins/devel/check-structures-sanity/validate.cpp @@ -35,10 +35,10 @@ bool Checker::is_valid_dereference(const QueueItem & item, const CheckedStructur // assumes MALLOC_PERTURB_=45 #ifdef DFHACK64 #define UNINIT_PTR 0xd2d2d2d2d2d2d2d2 -#define FAIL_PTR(message) FAIL(stl_sprintf("0x%016zx: ", reinterpret_cast(base)) << message) +#define FAIL_PTR(message) FAIL(fmt::format("{:#016x} ", reinterpret_cast(base)) << message) #else #define UNINIT_PTR 0xd2d2d2d2 -#define FAIL_PTR(message) FAIL(stl_sprintf("0x%08zx: ", reinterpret_cast(base)) << message) +#define FAIL_PTR(message) FAIL(fmt::format("{:#016x} ", reinterpret_cast(base)) << message) #endif if (uintptr_t(base) == UNINIT_PTR) { diff --git a/plugins/devel/color-dfhack-text.cpp b/plugins/devel/color-dfhack-text.cpp index a19a475bb3b..a17ad9d0725 100644 --- a/plugins/devel/color-dfhack-text.cpp +++ b/plugins/devel/color-dfhack-text.cpp @@ -114,7 +114,7 @@ command_result color(color_ostream &out, std::vector ¶ms) } else if (p != "enable") { - out.printerr("Unrecognized option: %s\n", p.c_str()); + out.printerr("Unrecognized option: {}\n", p); return CR_WRONG_USAGE; } } diff --git a/plugins/devel/counters.cpp b/plugins/devel/counters.cpp index d3672d8d487..05c2024acc3 100644 --- a/plugins/devel/counters.cpp +++ b/plugins/devel/counters.cpp @@ -22,7 +22,7 @@ command_result df_counters (color_ostream &out, vector & parameters) for (size_t i = 0; i < counters.size(); i++) { auto counter = counters[i]; - out.print("%i (%s): %i\n", (int)counter->id, ENUM_KEY_STR(misc_trait_type, counter->id).c_str(), counter->value); + out.print("{} ({}) : {}\n", (int)counter->id, ENUM_KEY_STR(misc_trait_type, counter->id), counter->value); } return CR_OK; diff --git a/plugins/devel/dumpmats.cpp b/plugins/devel/dumpmats.cpp index b910b6546be..91b70a26767 100644 --- a/plugins/devel/dumpmats.cpp +++ b/plugins/devel/dumpmats.cpp @@ -32,7 +32,7 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) df::material *mat = world->raws.mat_table.builtin[mat_num]; if (!mat) continue; - out.print("\n[MATERIAL:%s] - reconstructed from data extracted from memory\n", mat->id.c_str()); + out.print("\n[MATERIAL:{}] - reconstructed from data extracted from memory\n", mat->id); int32_t def_color[6] = {-1,-1,-1,-1,-1,-1}; string def_name[6]; @@ -52,10 +52,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_color[matter_state::Liquid] = solid_color; def_color[matter_state::Gas] = solid_color; - out.print("\t[STATE_COLOR:ALL:%s]\n", world->raws.descriptors.colors[solid_color]->id.c_str()); + out.print("\t[STATE_COLOR:ALL:{}]\n", world->raws.descriptors.colors[solid_color]->id); } else - out.print("\t[STATE_COLOR:ALL_SOLID:%s]\n", world->raws.descriptors.colors[solid_color]->id.c_str()); + out.print("\t[STATE_COLOR:ALL_SOLID:{}]\n", world->raws.descriptors.colors[solid_color]->id); } string solid_name = mat->state_name[matter_state::Solid]; @@ -82,10 +82,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) def_name[matter_state::Gas] = solid_name; def_adj[matter_state::Liquid] = solid_name; def_adj[matter_state::Gas] = solid_name; - out.print("\t[STATE_NAME_ADJ:ALL:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME_ADJ:ALL:{}]\n", solid_name); } else - out.print("\t[STATE_NAME_ADJ:ALL_SOLID:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME_ADJ:ALL_SOLID:{}]\n", solid_name); } } else @@ -104,10 +104,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_name[matter_state::Liquid] = solid_name; def_name[matter_state::Gas] = solid_name; - out.print("\t[STATE_NAME:ALL:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME:ALL:{}]\n", solid_name); } else - out.print("\t[STATE_NAME:ALL_SOLID:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME:ALL_SOLID:{}]\n", solid_name); } if (solid_adj == mat->state_adj[matter_state::Powder] || solid_adj == mat->state_adj[matter_state::Paste] || @@ -123,10 +123,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_adj[matter_state::Liquid] = solid_adj; def_adj[matter_state::Gas] = solid_adj; - out.print("\t[STATE_ADJ:ALL:%s]\n", solid_adj.c_str()); + out.print("\t[STATE_ADJ:ALL:{}]\n", solid_adj); } else - out.print("\t[STATE_ADJ:ALL_SOLID:%s]\n", solid_adj.c_str()); + out.print("\t[STATE_ADJ:ALL_SOLID:{}]\n", solid_adj); } } const char *state_names[6] = {"SOLID", "LIQUID", "GAS", "SOLID_POWDER", "SOLID_PASTE", "SOLID_PRESSED"}; @@ -134,110 +134,110 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) FOR_ENUM_ITEMS(matter_state, state) { if (mat->state_color[state] != -1 && mat->state_color[state] != def_color[state]) - out.print("\t[STATE_COLOR:%s:%s]\n", state_names[state], world->raws.descriptors.colors[mat->state_color[state]]->id.c_str()); + out.print("\t[STATE_COLOR:{}:{}]\n", state_names[state], world->raws.descriptors.colors[mat->state_color[state]]->id); if (mat->state_name[state] == mat->state_adj[state]) { if ((mat->state_name[state].size() && mat->state_name[state] != def_name[state]) || (mat->state_adj[state].size() && mat->state_adj[state] != def_adj[state])) - out.print("\t[STATE_NAME_ADJ:%s:%s]\n", state_names[state], mat->state_name[state].c_str()); + out.print("\t[STATE_NAME_ADJ:{}:{}]\n", state_names[state], mat->state_name[state]); } else { if (mat->state_name[state].size() && mat->state_name[state] != def_name[state]) - out.print("\t[STATE_NAME:%s:%s]\n", state_names[state], mat->state_name[state].c_str()); + out.print("\t[STATE_NAME:{}:{}]\n", state_names[state], mat->state_name[state]); if (mat->state_adj[state].size() && mat->state_adj[state] != def_adj[state]) - out.print("\t[STATE_ADJ:%s:%s]\n", state_names[state], mat->state_adj[state].c_str()); + out.print("\t[STATE_ADJ:{}:{}]\n", state_names[state], mat->state_adj[state]); } } if (mat->basic_color[0] != 7 || mat->basic_color[1] != 0) - out.print("\t[BASIC_COLOR:%i:%i]\n", mat->basic_color[0], mat->basic_color[1]); + out.print("\t[BASIC_COLOR:{}:{}]\n", mat->basic_color[0], mat->basic_color[1]); if (mat->build_color[0] != 7 || mat->build_color[1] != 7 || mat->build_color[2] != 0) - out.print("\t[BUILD_COLOR:%i:%i:%i]\n", mat->build_color[0], mat->build_color[1], mat->build_color[2]); + out.print("\t[BUILD_COLOR:{}:{}:{}]\n", mat->build_color[0], mat->build_color[1], mat->build_color[2]); if (mat->tile_color[0] != 7 || mat->tile_color[1] != 7 || mat->tile_color[2] != 0) - out.print("\t[TILE_COLOR:%i:%i:%i]\n", mat->tile_color[0], mat->tile_color[1], mat->tile_color[2]); + out.print("\t[TILE_COLOR:{}:{}:{}]\n", mat->tile_color[0], mat->tile_color[1], mat->tile_color[2]); if (mat->tile != 0xdb) - out.print("\t[TILE:%i]\n", mat->tile); + out.print("\t[TILE:{}]\n", mat->tile); if (mat->item_symbol != 0x07) - out.print("\t[ITEM_SYMBOL:%i]\n", mat->item_symbol); + out.print("\t[ITEM_SYMBOL:{}]\n", mat->item_symbol); if (mat->material_value != 1) - out.print("\t[MATERIAL_VALUE:%i]\n", mat->material_value); + out.print("\t[MATERIAL_VALUE:{}]\n", mat->material_value); if (mat->gem_name1.size()) - out.print("\t[IS_GEM:%s:%s]\n", mat->gem_name1.c_str(), mat->gem_name2.c_str()); + out.print("\t[IS_GEM:{}:{}]\n", mat->gem_name1.c_str(), mat->gem_name2.c_str()); if (mat->stone_name.size()) - out.print("\t[STONE_NAME:%s]\n", mat->stone_name.c_str()); + out.print("\t[STONE_NAME:{}]\n", mat->stone_name.c_str()); if (mat->heat.spec_heat != 60001) - out.print("\t[SPEC_HEAT:%i]\n", mat->heat.spec_heat); + out.print("\t[SPEC_HEAT:{}]\n", mat->heat.spec_heat); if (mat->heat.heatdam_point != 60001) - out.print("\t[HEATDAM_POINT:%i]\n", mat->heat.heatdam_point); + out.print("\t[HEATDAM_POINT:{}]\n", mat->heat.heatdam_point); if (mat->heat.colddam_point != 60001) - out.print("\t[COLDDAM_POINT:%i]\n", mat->heat.colddam_point); + out.print("\t[COLDDAM_POINT:{}]\n", mat->heat.colddam_point); if (mat->heat.ignite_point != 60001) - out.print("\t[IGNITE_POINT:%i]\n", mat->heat.ignite_point); + out.print("\t[IGNITE_POINT:{}]\n", mat->heat.ignite_point); if (mat->heat.melting_point != 60001) - out.print("\t[MELTING_POINT:%i]\n", mat->heat.melting_point); + out.print("\t[MELTING_POINT:{}]\n", mat->heat.melting_point); if (mat->heat.boiling_point != 60001) - out.print("\t[BOILING_POINT:%i]\n", mat->heat.boiling_point); + out.print("\t[BOILING_POINT:{}]\n", mat->heat.boiling_point); if (mat->heat.mat_fixed_temp != 60001) - out.print("\t[MAT_FIXED_TEMP:%i]\n", mat->heat.mat_fixed_temp); + out.print("\t[MAT_FIXED_TEMP:{}]\n", mat->heat.mat_fixed_temp); if (uint32_t(mat->solid_density) != 0xFBBC7818) - out.print("\t[SOLID_DENSITY:%i]\n", mat->solid_density); + out.print("\t[SOLID_DENSITY:{}]\n", mat->solid_density); if (uint32_t(mat->liquid_density) != 0xFBBC7818) - out.print("\t[LIQUID_DENSITY:%i]\n", mat->liquid_density); + out.print("\t[LIQUID_DENSITY:{}]\n", mat->liquid_density); if (uint32_t(mat->molar_mass) != 0xFBBC7818) - out.print("\t[MOLAR_MASS:%i]\n", mat->molar_mass); + out.print("\t[MOLAR_MASS:{}]\n", mat->molar_mass); FOR_ENUM_ITEMS(strain_type, strain) { auto name = ENUM_KEY_STR(strain_type,strain); if (mat->strength.yield[strain] != 10000) - out.print("\t[%s_YIELD:%i]\n", name.c_str(), mat->strength.yield[strain]); + out.print("\t[{}_YIELD:{}]\n", name, mat->strength.yield[strain]); if (mat->strength.fracture[strain] != 10000) - out.print("\t[%s_FRACTURE:%i]\n", name.c_str(), mat->strength.fracture[strain]); + out.print("\t[{}_FRACTURE:{}]\n", name, mat->strength.fracture[strain]); if (mat->strength.strain_at_yield[strain] != 0) - out.print("\t[%s_STRAIN_AT_YIELD:%i]\n", name.c_str(), mat->strength.strain_at_yield[strain]); + out.print("\t[{}_STRAIN_AT_YIELD:{}]\n", name, mat->strength.strain_at_yield[strain]); } if (mat->strength.max_edge != 0) - out.print("\t[MAX_EDGE:%i]\n", mat->strength.max_edge); + out.print("\t[MAX_EDGE:{}]\n", mat->strength.max_edge); if (mat->strength.absorption != 0) - out.print("\t[ABSORPTION:%i]\n", mat->strength.absorption); + out.print("\t[ABSORPTION:{}]\n", mat->strength.absorption); FOR_ENUM_ITEMS(material_flags, i) { if (mat->flags.is_set(i)) - out.print("\t[%s]\n", ENUM_KEY_STR(material_flags, i).c_str()); + out.print("\t[{}]\n", ENUM_KEY_STR(material_flags, i)); } if (mat->extract_storage != item_type::BARREL) - out.print("\t[EXTRACT_STORAGE:%s]\n", ENUM_KEY_STR(item_type, mat->extract_storage).c_str()); + out.print("\t[EXTRACT_STORAGE:{}]\n", ENUM_KEY_STR(item_type, mat->extract_storage)); if (mat->butcher_special_type != item_type::NONE || mat->butcher_special_subtype != -1) - out.print("\t[BUTCHER_SPECIAL:%s:%s]\n", ENUM_KEY_STR(item_type, mat->butcher_special_type).c_str(), (mat->butcher_special_subtype == -1) ? "NONE" : "?"); + out.print("\t[BUTCHER_SPECIAL:{}:{}]\n", ENUM_KEY_STR(item_type, mat->butcher_special_type), (mat->butcher_special_subtype == -1) ? "NONE" : "?"); if (mat->meat_name[0].size() || mat->meat_name[1].size() || mat->meat_name[2].size()) - out.print("\t[MEAT_NAME:%s:%s:%s]\n", mat->meat_name[0].c_str(), mat->meat_name[1].c_str(), mat->meat_name[2].c_str()); + out.print("\t[MEAT_NAME:{}:{}:{}]\n", mat->meat_name[0], mat->meat_name[1], mat->meat_name[2]); if (mat->block_name[0].size() || mat->block_name[1].size()) - out.print("\t[BLOCK_NAME:%s:%s]\n", mat->block_name[0].c_str(), mat->block_name[1].c_str()); + out.print("\t[BLOCK_NAME:{}:{}]\n", mat->block_name[0], mat->block_name[1]); for (std::string *s : mat->reaction_class) - out.print("\t[REACTION_CLASS:%s]\n", s->c_str()); + out.print("\t[REACTION_CLASS:{}]\n", *s); for (size_t i = 0; i < mat->reaction_product.id.size(); i++) { if ((*mat->reaction_product.str[0][i] == "NONE") && (*mat->reaction_product.str[1][i] == "NONE")) - out.print("\t[MATERIAL_REACTION_PRODUCT:%s:%s:%s%s%s]\n", mat->reaction_product.id[i]->c_str(), mat->reaction_product.str[2][i]->c_str(), mat->reaction_product.str[3][i]->c_str(), mat->reaction_product.str[4][i]->size() ? ":" : "", mat->reaction_product.str[4][i]->c_str()); + out.print("\t[MATERIAL_REACTION_PRODUCT:{}:{}:{}{}{}]\n", *mat->reaction_product.id[i], *mat->reaction_product.str[2][i], *mat->reaction_product.str[3][i], mat->reaction_product.str[4][i]->size() ? ":" : "", *mat->reaction_product.str[4][i]); else - out.print("\t[ITEM_REACTION_PRODUCT:%s:%s:%s:%s:%s%s%s]\n", mat->reaction_product.id[i]->c_str(), mat->reaction_product.str[0][i]->c_str(), mat->reaction_product.str[1][i]->c_str(), mat->reaction_product.str[2][i]->c_str(), mat->reaction_product.str[3][i]->c_str(), mat->reaction_product.str[4][i]->size() ? ":" : "", mat->reaction_product.str[4][i]->c_str()); + out.print("\t[ITEM_REACTION_PRODUCT:{}:{}:{}:{}:{}{}]\n", *mat->reaction_product.id[i], *mat->reaction_product.str[0][i], *mat->reaction_product.str[1][i], *mat->reaction_product.str[2][i], *mat->reaction_product.str[3][i], *mat->reaction_product.str[4][i]); } if (mat->hardens_with_water.mat_type != -1) - out.print("\t[HARDENS_WITH_WATER:%s:%s%s%s]\n", mat->hardens_with_water.str[0].c_str(), mat->hardens_with_water.str[1].c_str(), mat->hardens_with_water.str[2].size() ? ":" : "", mat->hardens_with_water.str[2].c_str()); + out.print("\t[HARDENS_WITH_WATER:{}:{}{}{}]\n", mat->hardens_with_water.str[0], mat->hardens_with_water.str[1], mat->hardens_with_water.str[2].size() ? ":" : "", mat->hardens_with_water.str[2]); if (mat->powder_dye != -1) - out.print("\t[POWDER_DYE:%s]\n", world->raws.descriptors.colors[mat->powder_dye]->id.c_str()); + out.print("\t[POWDER_DYE:{}]\n", world->raws.descriptors.colors[mat->powder_dye]->id); if (mat->soap_level != -0) - out.print("\t[SOAP_LEVEL:%o]\n", mat->soap_level); + out.print("\t[SOAP_LEVEL:{}]\n", mat->soap_level); for (size_t i = 0; i < mat->syndrome.syndrome.size(); i++) out.print("\t[SYNDROME] ...\n"); diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index c0944249f1e..3b0bd860d4f 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -109,7 +109,7 @@ command_result eventExample(color_ostream& out, vector& parameters) { //static int timerCount=0; //static int timerDenom=0; void jobInitiated(color_ostream& out, void* job_) { - out.print("Job initiated! %p\n", job_); + out.print("Job initiated! {}\n", static_cast(job_)); /* df::job* job = (df::job*)job_; out.print(" completion_timer = %d\n", job->completion_timer); @@ -120,37 +120,37 @@ void jobInitiated(color_ostream& out, void* job_) { } void jobCompleted(color_ostream& out, void* job) { - out.print("Job completed! %p\n", job); + out.print("Job completed! {}\n", static_cast(job)); } void timePassed(color_ostream& out, void* ptr) { - out.print("Time: %zi\n", (intptr_t)(ptr)); + out.print("Time: {}\n", (intptr_t)(ptr)); } void unitDeath(color_ostream& out, void* ptr) { - out.print("Death: %zi\n", (intptr_t)(ptr)); + out.print("Death: {}\n", (intptr_t)(ptr)); } void itemCreate(color_ostream& out, void* ptr) { int32_t item_index = df::item::binsearch_index(df::global::world->items.all, (intptr_t)ptr); if ( item_index == -1 ) { - out.print("%s, %d: Error.\n", __FILE__, __LINE__); + out.print("{}: Error.\n", __FILE__, __LINE__); } df::item* item = df::global::world->items.all[item_index]; df::item_type type = item->getType(); df::coord pos = item->pos; - out.print("Item created: %zi, %s, at (%d,%d,%d)\n", (intptr_t)(ptr), ENUM_KEY_STR(item_type, type).c_str(), pos.x, pos.y, pos.z); + out.print("Item created: {}, {}, at ({},{},{})\n", (intptr_t)(ptr), ENUM_KEY_STR(item_type, type).c_str(), pos.x, pos.y, pos.z); } void building(color_ostream& out, void* ptr) { - out.print("Building created/destroyed: %zi\n", (intptr_t)ptr); + out.print("Building created/destroyed: {}\n", (intptr_t)ptr); } void construction(color_ostream& out, void* ptr) { - out.print("Construction created/destroyed: %p\n", ptr); + out.print("Construction created/destroyed: {}\n", ptr); df::construction* constr = (df::construction*)ptr; df::coord pos = constr->pos; - out.print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + out.print(" ({},{},{})\n", pos.x, pos.y, pos.z); if ( df::construction::find(pos) == NULL ) out.print(" construction destroyed\n"); else @@ -160,19 +160,19 @@ void construction(color_ostream& out, void* ptr) { void syndrome(color_ostream& out, void* ptr) { EventManager::SyndromeData* data = (EventManager::SyndromeData*)ptr; - out.print("Syndrome started: unit %d, syndrome %d.\n", data->unitId, data->syndromeIndex); + out.print("Syndrome started: unit {}, syndrome {}.\n", data->unitId, data->syndromeIndex); } void invasion(color_ostream& out, void* ptr) { - out.print("New invasion! %zi\n", (intptr_t)ptr); + out.print("New invasion! {}\n", (intptr_t)ptr); } void unitAttack(color_ostream& out, void* ptr) { EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr; - out.print("unit %d attacks unit %d\n", data->attacker, data->defender); + out.print("unit {} attacks unit {}\n", data->attacker, data->defender); df::unit* defender = df::unit::find(data->defender); if (!defender) { - out.printerr("defender %d does not exist\n", data->defender); + out.printerr("defender {} does not exist\n", data->defender); return; } int32_t woundIndex = df::unit_wound::binsearch_index(defender->body.wounds, data->wound); @@ -187,6 +187,6 @@ void unitAttack(color_ostream& out, void* ptr) { for ( auto a = parts.begin(); a != parts.end(); a++ ) { int32_t body_part_id = (*a); df::body_part_raw* part = defender->body.body_plan->body_parts[body_part_id]; - out.print(" %s\n", part->name_singular[0]->c_str()); + out.print(" {}\n", *part->name_singular[0]); } } diff --git a/plugins/devel/frozen.cpp b/plugins/devel/frozen.cpp index 6c4264783e6..a6d862a5a50 100644 --- a/plugins/devel/frozen.cpp +++ b/plugins/devel/frozen.cpp @@ -55,7 +55,7 @@ command_result df_frozenlava (color_ostream &out, vector & parameters) int tiles = changeLiquid(tile_liquid::Magma); if (tiles) - out.print("Changed %i tiles of ice into frozen lava.\n", tiles); + out.print("Changed {} tiles of ice into frozen lava.\n", tiles); return CR_OK; } @@ -72,7 +72,7 @@ command_result df_frozenwater (color_ostream &out, vector & parameters) int tiles = changeLiquid(tile_liquid::Water); if (tiles) - out.print("Changed %i tiles of ice into frozen water.\n", tiles); + out.print("Changed {} tiles of ice into frozen water.\n", tiles); return CR_OK; } diff --git a/plugins/devel/kittens.cpp b/plugins/devel/kittens.cpp index 69430e69daf..d9dbf27015f 100644 --- a/plugins/devel/kittens.cpp +++ b/plugins/devel/kittens.cpp @@ -115,14 +115,14 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) uint64_t time2 = GetTimeMs64(); uint64_t delta = time2-timeLast; timeLast = time2; - out.print("Time delta = %d ms\n", int(delta)); + out.print("Time delta = {} ms\n", int(delta)); } if(trackmenu_flg) { if (last_menu != plotinfo->main.mode) { last_menu = plotinfo->main.mode; - out.print("Menu: %d\n",last_menu); + out.print("Menu: {}\n", ENUM_AS_STR(last_menu)); } } if(trackpos_flg) @@ -134,14 +134,14 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) last_designation[0] = desig_x; last_designation[1] = desig_y; last_designation[2] = desig_z; - out.print("Designation: %d %d %d\n",desig_x, desig_y, desig_z); + out.print("Designation: {} {} {}\n", desig_x, desig_y, desig_z); } df::coord mousePos = Gui::getMousePos(); if(mousePos.x != last_mouse[0] || mousePos.y != last_mouse[1]) { last_mouse[0] = mousePos.x; last_mouse[1] = mousePos.y; - out.print("Mouse: %d %d\n",mousePos.x, mousePos.y); + out.print("Mouse: {} {}\n", mousePos.x, mousePos.y); } } return CR_OK; @@ -158,7 +158,7 @@ command_result trackmenu (color_ostream &out, vector & parameters) { is_enabled = true; last_menu = plotinfo->main.mode; - out.print("Menu: %d\n",last_menu); + out.print("Menu: {}\n", ENUM_AS_STR(last_menu)); trackmenu_flg = true; return CR_OK; } @@ -182,10 +182,10 @@ command_result colormods (color_ostream &out, vector & parameters) for(df::creature_raw* rawlion : vec) { df::caste_raw * caste = rawlion->caste[0]; - out.print("%s\nCaste addr %p\n",rawlion->creature_id.c_str(), &caste->color_modifiers); + out.print("{}\nCaste addr {}\n",rawlion->creature_id, static_cast(&caste->color_modifiers)); for(size_t j = 0; j < caste->color_modifiers.size();j++) { - out.print("mod %zd: %p\n", j, caste->color_modifiers[j]); + out.print("mod {}: {}\n", j, static_cast(caste->color_modifiers[j])); } } return CR_OK; @@ -203,7 +203,7 @@ command_result ktimer (color_ostream &out, vector & parameters) uint64_t timeend = GetTimeMs64(); timeLast = timeend; timering = true; - out.print("Time to suspend = %d ms\n", int(timeend - timestart)); + out.print("Time to suspend = {} ms\n", int(timeend - timestart)); } is_enabled = true; return CR_OK; @@ -300,9 +300,9 @@ struct Connected : public ClearMem { return this; } ~Connected() { - INFO(command,*out).print("Connected %d had %d count. " - "It was caller %d times. " - "It was callee %d times.\n", + INFO(command,*out).print("Connected {} had {} count. " + "It was caller {} times. " + "It was callee {} times.\n", id, count, caller, callee.load()); } }; diff --git a/plugins/devel/memutils.cpp b/plugins/devel/memutils.cpp index c2a7387fdd7..b7ce98ea730 100644 --- a/plugins/devel/memutils.cpp +++ b/plugins/devel/memutils.cpp @@ -52,7 +52,7 @@ namespace memutils { lua_pushstring(state, expr); if (!Lua::SafeCall(*out, state, 1, 1)) { - out->printerr("Failed to evaluate %s\n", expr); + out->printerr("Failed to evaluate {}\n", expr); return NULL; } @@ -60,7 +60,7 @@ namespace memutils { lua_swap(state); if (!Lua::SafeCall(*out, state, 1, 1) || !lua_isinteger(state, -1)) { - out->printerr("Failed to get address: %s\n", expr); + out->printerr("Failed to get address: {}\n", expr); return NULL; } diff --git a/plugins/devel/memview.cpp b/plugins/devel/memview.cpp index a9fba897e13..01397ef7d21 100644 --- a/plugins/devel/memview.cpp +++ b/plugins/devel/memview.cpp @@ -75,7 +75,7 @@ void outputHex(uint8_t *buf,uint8_t *lbuf,size_t len,size_t start,color_ostream for(size_t i=0;i8X} ",i+start); for(size_t j=0;(j2X}",static_cast(buf[j+i])); //if modfied show a star else - con.print(" %02X",buf[j+i]); + con.print(" {:0>2X}",static_cast(buf[j+i])); } con.reset_color(); con.print(" | "); for(size_t j=0;(j31)&&(buf[j+i]<128)) //only printable ascii - con.print("%c",buf[j+i]); + con.print("{}",static_cast(buf[j+i])); else con.print("."); con.print("\n"); @@ -187,7 +187,7 @@ command_result memview (color_ostream &out, vector & parameters) isValid=true; if(!isValid) { - out.printerr("Invalid address: %p\n",memdata.addr); + out.printerr("Invalid address: {}\n",memdata.addr); mymutex->unlock(); return CR_OK; } diff --git a/plugins/devel/onceExample.cpp b/plugins/devel/onceExample.cpp index ebc03e32b7b..73361c53e78 100644 --- a/plugins/devel/onceExample.cpp +++ b/plugins/devel/onceExample.cpp @@ -25,7 +25,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector & parameters) { - out.print("Already done = %d.\n", DFHack::Once::alreadyDone("onceExample_1")); + out.print("Already done = {}.\n", DFHack::Once::alreadyDone("onceExample_1")); if ( DFHack::Once::doOnce("onceExample_1") ) { out.print("Printing this message once!\n"); } diff --git a/plugins/devel/stepBetween.cpp b/plugins/devel/stepBetween.cpp index 0eca4ea8c9e..9acbce904b5 100644 --- a/plugins/devel/stepBetween.cpp +++ b/plugins/devel/stepBetween.cpp @@ -79,7 +79,7 @@ df::coord prev; command_result stepBetween (color_ostream &out, std::vector & parameters) { df::coord bob = Gui::getCursorPos(); - out.print("(%d,%d,%d), (%d,%d,%d): canStepBetween = %d, canWalkBetween = %d\n", prev.x, prev.y, prev.z, bob.x, bob.y, bob.z, Maps::canStepBetween(prev, bob), Maps::canWalkBetween(prev,bob)); + out.print("({},{},{}), ({},{},{}): canStepBetween = {}, canWalkBetween = {}\n", prev.x, prev.y, prev.z, bob.x, bob.y, bob.z, Maps::canStepBetween(prev, bob), Maps::canWalkBetween(prev,bob)); prev = bob; return CR_OK; diff --git a/plugins/devel/tilesieve.cpp b/plugins/devel/tilesieve.cpp index 6b36b8dd0d9..9f21d98368b 100644 --- a/plugins/devel/tilesieve.cpp +++ b/plugins/devel/tilesieve.cpp @@ -73,7 +73,7 @@ command_result tilesieve(color_ostream &out, std::vector & params) if(seen.count(tt)) continue; seen.insert(tt); - out.print("Found tile %x @ %d %d %d\n", tt, block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); + out.print("Found tile {} @ {} {} {}\n", ENUM_AS_STR(tt), block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); } } return CR_OK; diff --git a/plugins/devel/vectors.cpp b/plugins/devel/vectors.cpp index 279fe8a0d5e..ac72de30740 100644 --- a/plugins/devel/vectors.cpp +++ b/plugins/devel/vectors.cpp @@ -134,7 +134,7 @@ static void printVec(color_ostream &con, const char* msg, t_vecTriplet *vec, uintptr_t length = (intptr_t)vec->end - (intptr_t)vec->start; uintptr_t offset = pos - start; - con.print("%8s offset 0x%06zx, addr 0x%01zx, start 0x%01zx, length %zi", + con.print("{:8} offset 0x{:06x}, addr 0x{:01x}, start 0x{:01x}, length {:}", msg, offset, pos, intptr_t(vec->start), length); if (length >= 4 && length % 4 == 0) { @@ -146,7 +146,7 @@ static void printVec(color_ostream &con, const char* msg, t_vecTriplet *vec, } std::string classname; if (Core::getInstance().vinfo->getVTableName(ptr, classname)) - con.print(", 1st item: %s", classname.c_str()); + con.print(", 1st item: {}", classname); } con.print("\n"); } @@ -197,10 +197,10 @@ command_result df_vectors (color_ostream &con, vector & parameters) // Found the range containing the start if (!range.isInRange((void *)end)) { - con.print("Scanning %zi bytes would read past end of memory " + con.print("Scanning {} bytes would read past end of memory " "range.\n", bytes); size_t diff = end - (intptr_t)range.end; - con.print("Cutting bytes down by %zi.\n", diff); + con.print("Cutting bytes down by {}.\n", diff); end = (uintptr_t) range.end; } diff --git a/plugins/devel/zoom.cpp b/plugins/devel/zoom.cpp index 18e8d62e32b..d37ed3418a1 100644 --- a/plugins/devel/zoom.cpp +++ b/plugins/devel/zoom.cpp @@ -42,7 +42,7 @@ command_result df_zoom (color_ostream &out, std::vector & paramete return CR_WRONG_USAGE; if (zcmap.find(parameters[0]) == zcmap.end()) { - out.printerr("Unrecognized zoom command: %s\n", parameters[0].c_str()); + out.printerr("Unrecognized zoom command: {}\n", parameters[0]); out.print("Valid commands:"); for (auto it = zcmap.begin(); it != zcmap.end(); ++it) { diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index 48984e1b9fc..259978f9bff 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -48,9 +48,6 @@ namespace DFHack { DBG_DECLARE(dignow, channels, DebugCategory::LINFO); } -#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16 -#define COORDARGS(id) id.x, id.y, id.z - using namespace DFHack; struct designation{ @@ -486,7 +483,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, DFCoord pos_below(pos.x, pos.y, pos.z-1); if (can_dig_channel(tt) && map.ensureBlockAt(pos_below) && is_diggable(map, pos_below, map.tiletypeAt(pos_below))) { - TRACE(channels).print("dig_tile: channeling at (" COORD ") [can_dig_channel: true]\n",COORDARGS(pos_below)); + TRACE(channels).print("dig_tile: channeling at ({}) [can_dig_channel: true]\n", pos_below); target_type = df::tiletype::OpenSpace; DFCoord pos_above(pos.x, pos.y, pos.z+1); if (map.ensureBlockAt(pos_above)) { @@ -503,7 +500,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, return true; } } else { - DEBUG(channels).print("dig_tile: failed to channel at (" COORD ") [can_dig_channel: false]\n", COORDARGS(pos_below)); + DEBUG(channels).print("dig_tile: failed to channel at ({}) [can_dig_channel: false]\n", pos_below); } break; } @@ -550,8 +547,8 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, case df::tile_dig_designation::No: default: out.printerr( - "unhandled dig designation for tile (%d, %d, %d): %d\n", - pos.x, pos.y, pos.z, designation); + "unhandled dig designation for tile ({}, {}, {}): {}\n", + pos.x, pos.y, pos.z, ENUM_AS_STR(designation)); } // fail if unhandled or no change to tile @@ -559,7 +556,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, return false; dug_tiles.emplace_back(map, pos); - TRACE(general).print("dig_tile: digging the designation tile at (" COORD ")\n",COORDARGS(pos)); + TRACE(general).print("dig_tile: digging the designation tile at ({})\n",pos); dig_type(map, pos, target_type); clean_ramps(map, pos); @@ -898,10 +895,8 @@ static void create_boulders(color_ostream &out, if (num_items != coords.size()) { MaterialInfo material; material.decode(prod->mat_type, prod->mat_index); - out.printerr("unexpected number of %s %s produced: expected %zd," - " got %zd.\n", - material.toString().c_str(), - ENUM_KEY_STR(item_type, prod->item_type).c_str(), + out.printerr("unexpected number of {} {} produced: expected {}, got {}.\n", + material.toString(), ENUM_KEY_STR(item_type, prod->item_type), coords.size(), num_items); num_items = std::min(num_items, entry.second.size()); } @@ -911,7 +906,7 @@ static void create_boulders(color_ostream &out, dump_pos : simulate_fall(coords[i]); if (!Maps::ensureTileBlock(pos)) { out.printerr( - "unable to place boulder generated at (%d, %d, %d)\n", + "unable to place boulder generated at ({}, {}, {})\n", coords[i].x, coords[i].y, coords[i].z); continue; } @@ -956,7 +951,7 @@ static void post_process_dug_tiles(color_ostream &out, continue; if (!Maps::ensureTileBlock(resting_pos)) { - out.printerr("No valid tile beneath (%d, %d, %d); can't move" + out.printerr("No valid tile beneath ({},{},{}) can't move" " units and items to floor", pos.x, pos.y, pos.z); continue; diff --git a/plugins/dig.cpp b/plugins/dig.cpp index 8bdcca59d67..1be2a9a1169 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -108,7 +108,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector getassignment(pos), is_warm(pos)); if (warm_mask->getassignment(pos) && is_warm(pos)) { - DEBUG(log,out).print("revealing warm dig tile at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(log,out).print("revealing warm dig tile at ({},{},{})\n", pos.x, pos.y, pos.z); block->designation[pos.x&15][pos.y&15].bits.hidden = false; } } if (auto damp_mask = World::getPersistentTilemask(damp_config, block)) { - TRACE(log,out).print("testing tile at (%d,%d,%d); mask:%d, damp:%d\n", pos.x, pos.y, pos.z, + TRACE(log,out).print("testing tile at ({},{},{}); mask:{}, damp:{}\n", pos.x, pos.y, pos.z, damp_mask->getassignment(pos), is_damp(pos)); if (damp_mask->getassignment(pos) && is_damp(pos)) { - DEBUG(log,out).print("revealing damp dig tile at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(log,out).print("revealing damp dig tile at ({},{},{})\n", pos.x, pos.y, pos.z); block->designation[pos.x&15][pos.y&15].bits.hidden = false; } } @@ -418,7 +418,7 @@ static void unhide_surrounding_tagged_tiles(color_ostream& out, void* job_ptr) { return; const auto & pos = job->pos; - TRACE(log,out).print("handing dig job at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + TRACE(log,out).print("handing dig job at ({},{},{})\n", pos.x, pos.y, pos.z); process_taken_dig_job(out, pos); @@ -626,7 +626,7 @@ int32_t parse_priority(color_ostream &out, vector ¶meters) } else { - out.printerr("invalid priority specified; reverting to %i\n", default_priority); + out.printerr("invalid priority specified; reverting to {}\n", default_priority); break; } } @@ -1470,7 +1470,7 @@ command_result digv (color_ostream &out, vector & parameters) con.printerr("This tile is not a vein.\n"); return CR_FAILURE; } - con.print("%d/%d/%d tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, des.whole); + con.print("{} tiletype: {}, veinmat: {}, designation: 0x{:x} ... DIGGING!\n", xy, ENUM_AS_STR(tt), veinmat, des.whole); stack flood; flood.push(xy); @@ -1654,7 +1654,7 @@ command_result digl (color_ostream &out, vector & parameters) con.printerr("This is a vein. Use digv instead!\n"); return CR_FAILURE; } - con.print("%d/%d/%d tiletype: %d, basemat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, basemat, des.whole); + con.print("{}/{}/{}/ tiletype: {}, basemat: {}, designation: 0x{:x} ... DIGGING!\n", cx,cy,cz, ENUM_AS_STR(tt), basemat, des.whole); stack flood; flood.push(xy); @@ -1841,7 +1841,7 @@ command_result digtype (color_ostream &out, vector & parameters) automine = false; else { - out.printerr("Invalid parameter: '%s'.\n", parameter.c_str()); + out.printerr("Invalid parameter: '{}'.\n", parameter); return CR_FAILURE; } } @@ -1873,7 +1873,7 @@ command_result digtype (color_ostream &out, vector & parameters) out.printerr("This tile is not a vein.\n"); return CR_FAILURE; } - out.print("(%d,%d,%d) tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, baseDes.whole); + out.print("({},{},{}) tiletype: {}, veinmat: {}, designation: 0x{:x} ... DIGGING!\n", cx,cy,cz, ENUM_AS_STR(tt), veinmat, baseDes.whole); if ( targetDigType != -1 ) { @@ -1910,7 +1910,7 @@ command_result digtype (color_ostream &out, vector & parameters) //designate it for digging if ( !mCache->testCoord(current) ) { - out.printerr("testCoord failed at (%d,%d,%d)\n", x, y, z); + out.printerr("testCoord failed at ({},{},{})\n", x, y, z); return CR_FAILURE; } @@ -2158,7 +2158,7 @@ static bool blink(int delay) { } static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = false) { - TRACE(log).print("entering paintScreenDampWarm aquifer_mode=%d, show_damp=%d\n", aquifer_mode, show_damp); + TRACE(log).print("entering paintScreenDampWarm aquifer_mode={}, show_damp={}\n", aquifer_mode, show_damp); static Screen::Pen empty_pen; @@ -2220,7 +2220,7 @@ static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = fals bump_layers(*pen, x, y); } } else { - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", + TRACE(log).print("scanning map tile at ({},{},{}) screen offset ({},{})\n", pos.x, pos.y, pos.z, x, y); auto des = Maps::getTileDesignation(pos); @@ -2231,7 +2231,7 @@ static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = fals Screen::Pen cur_tile = Screen::readTile(x, y, true); if (!cur_tile.valid()) { - DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); + DEBUG(log).print("cannot read tile at offset {}, {}\n", x, y); continue; } @@ -2465,7 +2465,7 @@ static void paintScreenDesignated() { if (!Maps::isValidTilePos(map_pos)) continue; - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", + TRACE(log).print("scanning map tile at ({},{},{}) screen offset ({},{})\n", map_pos.x, map_pos.y, map_pos.z, x, y); Screen::Pen cur_tile; diff --git a/plugins/digFlood.cpp b/plugins/digFlood.cpp index 045e6c61d63..c1d0eccbc5a 100644 --- a/plugins/digFlood.cpp +++ b/plugins/digFlood.cpp @@ -181,7 +181,7 @@ command_result digFlood (color_ostream &out, std::vector & paramet } } - out.print("Could not find material \"%s\".\n", parameters[a].c_str()); + out.print("Could not find material \"{}\".\n", parameters[a]); return CR_WRONG_USAGE; loop: continue; diff --git a/plugins/diggingInvaders/assignJob.cpp b/plugins/diggingInvaders/assignJob.cpp index eb74433b863..1bd5c97e4dd 100644 --- a/plugins/diggingInvaders/assignJob.cpp +++ b/plugins/diggingInvaders/assignJob.cpp @@ -231,13 +231,13 @@ int32_t assignJob(color_ostream& out, Edge firstImportantEdge, unordered_mapmat_type = material.type; @@ -255,7 +255,7 @@ int32_t assignJob(color_ostream& out, Edge firstImportantEdge, unordered_mapsite_id), NULL); if ( out_items.size() != 1 ) { - out.print("%s, %d: wrong size: %zu.\n", __FILE__, __LINE__, out_items.size()); + out.print("{}, {}: wrong size: {}.\n", __FILE__, __LINE__, out_items.size()); return -1; } out_items[0]->moveToGround(firstInvader->pos.x, firstInvader->pos.y, firstInvader->pos.z); diff --git a/plugins/diggingInvaders/diggingInvaders.cpp b/plugins/diggingInvaders/diggingInvaders.cpp index d883ef4dba8..fff7f033eaf 100644 --- a/plugins/diggingInvaders/diggingInvaders.cpp +++ b/plugins/diggingInvaders/diggingInvaders.cpp @@ -276,13 +276,13 @@ command_result diggingInvadersCommand(color_ostream& out, std::vectorjob.current_job && lastDigger->job.current_job->id == lastInvasionJob ) { return; } - //out.print("%s,%d: lastDigger = %d, last job = %d, last digger's job = %d\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); + //out.print("{},{}: lastDigger = {}, last job = {}, last digger's job = {}\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); lastInvasionDigger = lastInvasionJob = -1; clearDijkstra(); @@ -382,7 +382,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { } else if ( unit->flags1.bits.active_invader ) { df::creature_raw* raw = df::creature_raw::find(unit->race); if ( raw == NULL ) { - out.print("%s,%d: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); + out.print("{},{}: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); continue; } /* @@ -464,7 +464,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { fringe.erase(fringe.begin()); //out.print("line %d: fringe size = %d, localPtsFound = %d / %d, closedSetSize = %d, pt = %d,%d,%d\n", __LINE__, fringe.size(), localPtsFound, localPts.size(), closedSet.size(), pt.x,pt.y,pt.z); if ( closedSet.find(pt) != closedSet.end() ) { - out.print("%s, line %d: Double closure! Bad!\n", __FILE__, __LINE__); + out.print("{},{}: Double closure! Bad!\n", __FILE__, __LINE__); break; } closedSet.insert(pt); @@ -511,7 +511,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { delete myEdges; } // clock_t time = clock() - t0; - //out.print("tickTime = %d, time = %d, totalEdgeTime = %d, total points = %d, total edges = %d, time per point = %.3f, time per edge = %.3f, clocks/sec = %d\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); + //out.print("tickTime = {}, time = {}, totalEdgeTime = {}, total points = {}, total edges = {}, time per point = {:.3f}, time per edge = {:.3f}, clocks/sec = {}\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); fringe.clear(); if ( !foundTarget ) @@ -590,7 +590,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { //cancel it job->flags.bits.item_lost = 1; - out.print("%s,%d: cancelling job %d.\n", __FILE__,__LINE__, job->id); + out.print("{},{}: cancelling job {}.\n", __FILE__,__LINE__, job->id); //invaderJobs.remove(job->id); } invaderJobs.erase(lastInvasionJob); diff --git a/plugins/dwarfvet.cpp b/plugins/dwarfvet.cpp index 6f3e32d00f0..7847dfa8e8e 100644 --- a/plugins/dwarfvet.cpp +++ b/plugins/dwarfvet.cpp @@ -69,17 +69,17 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -140,7 +140,7 @@ static void dwarfvet_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.dwarfvet", "checkup"); } diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index bb90435343b..8e695bd7487 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -198,7 +198,7 @@ namespace embark_assist { size_t civ = 0; if (!infile) { - out.printerr("No profile file found at %s\n", profile_file_name); + out.printerr("No profile file found at {}\n", profile_file_name); return; } @@ -209,7 +209,7 @@ namespace embark_assist { while (true) { if (!fgets(line, count, infile) || line[0] != '[') { - out.printerr("Failed to find token start '[' at line %i\n", static_cast(i)); + out.printerr("Failed to find token start '[' at line {}\n", static_cast(i)); fclose(infile); return; } @@ -218,7 +218,7 @@ namespace embark_assist { if (line[k] == ':') { for (int l = 1; l < k; l++) { if (state->finder_list[static_cast(i) + civ].text.c_str()[l - 1] != line[l]) { - out.printerr("Token mismatch of %s vs %s\n", line, state->finder_list[static_cast(i) + civ].text.c_str()); + out.printerr("Token mismatch of {} vs {}\n", line, state->finder_list[static_cast(i) + civ].text.c_str()); fclose(infile); return; } @@ -242,7 +242,7 @@ namespace embark_assist { } if (!found) { - out.printerr("Value extraction failure from %s\n", line); + out.printerr("Value extraction failure from {}\n", line); fclose(infile); return; } @@ -252,7 +252,7 @@ namespace embark_assist { } if (!found) { - out.printerr("Value delimiter not found in %s\n", line); + out.printerr("Value delimiter not found in {}\n", line); fclose(infile); return; } diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index 9f3900f0c9e..61da64959b5 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -1450,21 +1450,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->savagery_count[i] < embark_size) { - if (trace) out.print("matcher::world_tile_match: Savagery All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->savagery_count[i] == 0 && !tile->neighboring_savagery[i]) { - if (trace) out.print("matcher::world_tile_match: Savagery Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->savagery_count[i] > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Savagery Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery Absent ({}, {})\n", x, y); return false; } break; @@ -1480,21 +1480,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->evilness_count[i] < embark_size) { - if (trace) out.print("matcher::world_tile_match: Evil All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->evilness_count[i] == 0 && !tile->neighboring_evilness[i]) { - if (trace) out.print("matcher::world_tile_match: Evil Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->evilness_count[i] > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Evil Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil Absent ({}, {})\n", x, y); return false; } break; @@ -1511,14 +1511,14 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::At_Most_Light: if (tile->aquifer == embark_assist::defs::Heavy_Aquifer_Bit) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light ({}, {})\n", x, y); return false; } break; @@ -1526,7 +1526,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_Light: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Light ({}, {})\n", x, y); return false; } break; @@ -1534,21 +1534,21 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_At_Least_Light: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || (tile->aquifer == embark_assist::defs::None_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_At_Least_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_At_Least_Light ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::Light: if (!(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Light ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::At_Least_Light: if (tile->aquifer == embark_assist::defs::None_Aquifer_Bit) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Least_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Least_Light ({}, {})\n", x, y); return false; } break; @@ -1556,7 +1556,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_Heavy: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1564,7 +1564,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::At_Most_Light_Plus_Heavy: if (tile->aquifer == embark_assist::defs::Heavy_Aquifer_Bit || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1572,7 +1572,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::Light_Plus_Heavy: if (!(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Light_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Light_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1580,14 +1580,14 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Light_Heavy: if (tile->aquifer != (embark_assist::defs::None_Aquifer_Bit | embark_assist::defs::Light_Aquifer_Bit | embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Light_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Light_Heavy ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::Heavy: if (!(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Heavy ({}, {})\n", x, y); return false; } break; @@ -1597,42 +1597,42 @@ namespace embark_assist { switch (tile->max_river_size) { case embark_assist::defs::river_sizes::None: if (finder->min_river > embark_assist::defs::river_ranges::None) { - if (trace) out.print("matcher::world_tile_match: River_Size None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Brook: if (finder->min_river > embark_assist::defs::river_ranges::Brook) { - if (trace) out.print("matcher::world_tile_match: River_Size Brook (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Brook ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Stream: if (finder->min_river > embark_assist::defs::river_ranges::Stream) { - if (trace) out.print("matcher::world_tile_match: River_Size Stream (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Stream ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Minor: if (finder->min_river > embark_assist::defs::river_ranges::Minor) { - if (trace) out.print("matcher::world_tile_match: River_Size Mino (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Mino ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Medium: if (finder->min_river > embark_assist::defs::river_ranges::Medium) { - if (trace) out.print("matcher::world_tile_match: River_Size Medium (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Medium ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Major: if (finder->max_river != embark_assist::defs::river_ranges::NA) { - if (trace) out.print("matcher::world_tile_match: River_Size Major (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Major ({}, {})\n", x, y); return false; } break; @@ -1640,13 +1640,13 @@ namespace embark_assist { // Waterfall if (finder->min_waterfall > tile->max_waterfall) { // N/A = -1 is always smaller - if (trace) out.print("matcher::world_tile_match: Waterfall (%i, %i), finder: %i, tile: %i\n", x, y, finder->min_waterfall, tile->max_waterfall); + if (trace) out.print("matcher::world_tile_match: Waterfall ({}, {}), finder: {}, tile: {}\n", x, y, finder->min_waterfall, tile->max_waterfall); return false; } if (finder->min_waterfall == 0 && // Absent embark_size == 256 && tile->max_waterfall > 0) { - if (trace) out.print("matcher::world_tile_match: Waterfall 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Waterfall 2 ({}, {})\n", x, y); return false; } @@ -1660,14 +1660,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->clay_count == 0 && !tile->neighboring_clay) { - if (trace) out.print("matcher::world_tile_match: Clay Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Clay Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->clay_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Clay Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Clay Absent ({}, {})\n", x, y); return false; } break; @@ -1681,14 +1681,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->sand_count == 0 && !tile->neighboring_sand) { - if (trace) out.print("matcher::world_tile_match: Sand Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Sand Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->sand_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Sand Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Sand Absent ({}, {})\n", x, y); return false; } break; @@ -1701,14 +1701,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->flux_count == 0) { - if (trace) out.print("matcher::world_tile_match: Flux Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Flux Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->flux_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Flux Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Flux Absent ({}, {})\n", x, y); return false; } break; @@ -1721,14 +1721,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->coal_count == 0) { - if (trace) out.print("matcher::world_tile_match: Coal Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Coal Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->coal_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Coal Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Coal Absent ({}, {})\n", x, y); return false; } break; @@ -1742,28 +1742,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->max_region_soil < 1) { - if (trace) out.print("matcher::world_tile_match: Soil Min Very Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Very Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->max_region_soil < 2) { - if (trace) out.print("matcher::world_tile_match: Soil Min Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->max_region_soil < 3) { - if (trace) out.print("matcher::world_tile_match: Soil Min Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Deep ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Deep: if (tile->max_region_soil < 4) { - if (trace) out.print("matcher::world_tile_match: Soil Min Very Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Very Deep ({}, {})\n", x, y); return false; } break; @@ -1779,28 +1779,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::None: if (tile->min_region_soil > 0) { - if (trace) out.print("matcher::world_tile_match: Soil_Max None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->min_region_soil > 1) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Very_Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Very_Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->min_region_soil > 2) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->min_region_soil > 3) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Deep ({}, {})\n", x, y); return false; } break; @@ -1840,14 +1840,14 @@ namespace embark_assist { case embark_assist::defs::freezing_ranges::Permanent: if (min_max_temperature > 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::At_Least_Partial: if (min_min_temperature > 0) { - if (trace) out.print("matcher::world_tile_match: Freezing At_Lest_Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing At_Lest_Partial ({}, {})\n", x, y); return false; } break; @@ -1855,21 +1855,21 @@ namespace embark_assist { case embark_assist::defs::freezing_ranges::Partial: if (min_min_temperature > 0 || max_max_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Partial ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::At_Most_Partial: if (max_max_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing At Most_Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing At Most_Partial ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::Never: if (max_min_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Never (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Never ({}, {})\n", x, y); return false; } break; @@ -1884,28 +1884,28 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::Very_Scarce: if (tile->max_tree_level < embark_assist::defs::tree_levels::Very_Scarce) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Very_Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Very_Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Scarce: if (tile->max_tree_level < embark_assist::defs::tree_levels::Scarce) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Woodland: if (tile->max_tree_level < embark_assist::defs::tree_levels::Woodland) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Woodland (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Woodland ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Heavily_Forested: if (tile->max_tree_level < embark_assist::defs::tree_levels::Heavily_Forested) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Heavily_Forested (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Heavily_Forested ({}, {})\n", x, y); return false; } break; @@ -1918,7 +1918,7 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::None: if (tile->min_tree_level > embark_assist::defs::tree_levels::None) { - if (trace) out.print("matcher::world_tile_match: Max_Trees None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees None ({}, {})\n", x, y); return false; } break; @@ -1926,21 +1926,21 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::Very_Scarce: if (tile->min_tree_level > embark_assist::defs::tree_levels::Very_Scarce) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Very_Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Very_Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Scarce: if (tile->min_tree_level > embark_assist::defs::tree_levels::Scarce) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Woodland: if (tile->min_tree_level > embark_assist::defs::tree_levels::Woodland) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Woodland (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Woodland ({}, {})\n", x, y); return false; } break; @@ -1953,14 +1953,14 @@ namespace embark_assist { case embark_assist::defs::yes_no_ranges::Yes: if (!tile->blood_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Blood_Rain Yes (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Blood_Rain Yes ({}, {})\n", x, y); return false; } break; case embark_assist::defs::yes_no_ranges::No: if (tile->blood_rain_full) { - if (trace) out.print("matcher::world_tile_match: Blood_Rain No (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Blood_Rain No ({}, {})\n", x, y); return false; } break; @@ -1973,35 +1973,35 @@ namespace embark_assist { case embark_assist::defs::syndrome_rain_ranges::Any: if (!tile->permanent_syndrome_rain_possible && !tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Permanent: if (!tile->permanent_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Temporary: if (!tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Temporary (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Temporary ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Not_Permanent: if (tile->permanent_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Not_Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Not_Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::None: if (tile->permanent_syndrome_rain_full || tile->temporary_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain None ({}, {})\n", x, y); return false; } break; @@ -2014,42 +2014,42 @@ namespace embark_assist { case embark_assist::defs::reanimation_ranges::Both: if (!tile->reanimating_possible || !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Both (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Both ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Any: if (!tile->reanimating_possible && !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Thralling: if (!tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Reanimation: if (!tile->reanimating_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Reanimation (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Reanimation ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Not_Thralling: if (tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: Reanimation Not_Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Not_Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::None: if (tile->reanimating_full || tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: Reanimation None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation None ({}, {})\n", x, y); return false; } break; @@ -2063,7 +2063,7 @@ namespace embark_assist { // Region Type 1 if (finder->region_type_1 != -1) { if (!tile->neighboring_region_types[finder->region_type_1]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_1 ({}, {})\n", x, y); return false; } } @@ -2071,7 +2071,7 @@ namespace embark_assist { // Region Type 2 if (finder->region_type_2 != -1) { if (!tile->neighboring_region_types[finder->region_type_2]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_2 ({}, {})\n", x, y); return false; } } @@ -2079,7 +2079,7 @@ namespace embark_assist { // Region Type 3 if (finder->region_type_3 != -1) { if (!tile->neighboring_region_types[finder->region_type_3]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_3 ({}, {})\n", x, y); return false; } } @@ -2087,7 +2087,7 @@ namespace embark_assist { // Biome 1 if (finder->biome_1 != -1) { if (!tile->neighboring_biomes[finder->biome_1]) { - if (trace) out.print("matcher::world_tile_match: Biome_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_1 ({}, {})\n", x, y); return false; } } @@ -2095,7 +2095,7 @@ namespace embark_assist { // Biome 2 if (finder->biome_2 != -1) { if (!tile->neighboring_biomes[finder->biome_2]) { - if (trace) out.print("matcher::world_tile_match: Biome_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_2 ({}, {})\n", x, y); return false; } } @@ -2103,7 +2103,7 @@ namespace embark_assist { // Biome 3 if (finder->biome_3 != -1) { if (!tile->neighboring_biomes[finder->biome_3]) { - if (trace) out.print("matcher::world_tile_match: Biome_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_3 ({}, {})\n", x, y); return false; } } @@ -2146,30 +2146,30 @@ namespace embark_assist { !mineral_1 || !mineral_2 || !mineral_3) { - if (trace) out.print("matcher::world_tile_match: Metal/Economic/Mineral (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Metal/Economic/Mineral ({}, {})\n", x, y); return false; } } // Necro Neighbors if (finder->min_necro_neighbors > tile->necro_neighbors) { - if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 1 ({}, {})\n", x, y); return false; } if (finder->max_necro_neighbors < tile->necro_neighbors && finder->max_necro_neighbors != -1) { - if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 2 ({}, {})\n", x, y); return false; } // Civ Neighbors if (finder->min_civ_neighbors > (int16_t)tile->neighbors.size()) { - if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 1 (%i, %i), %i, %i\n", x, y, finder->min_civ_neighbors, (int)tile->neighbors.size()); + if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 1 ({}, {}), {}, {}\n", x, y, finder->min_civ_neighbors, (int)tile->neighbors.size()); return false; } if (finder->max_civ_neighbors < (int8_t)tile->neighbors.size() && finder->max_civ_neighbors != -1) { - if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 2 ({}, {})\n", x, y); return false; } @@ -2191,7 +2191,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: Specific Neighbors Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Specific Neighbors Present ({}, {})\n", x, y); return false; } @@ -2201,7 +2201,7 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Absent: for (uint16_t k = 0; k < tile->neighbors.size(); k++) { if (finder->neighbors[i].entity_raw == tile->neighbors[k]) { - if (trace) out.print("matcher::world_tile_match: Specific Neighbors Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Specific Neighbors Absent ({}, {})\n", x, y); return false; } } @@ -2223,21 +2223,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->savagery_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Savagery All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->savagery_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Savagery Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->savagery_count[i] == 256) { - if (trace) out.print("matcher::world_tile_match: NS Savagery Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery Absent ({}, {})\n", x, y); return false; } break; @@ -2253,21 +2253,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->evilness_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Evil All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->evilness_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Evil Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->evilness_count[i] == 256) { - if (trace) out.print("matcher::world_tile_match: NS Evil Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil Absent ({}, {})\n", x, y); return false; } break; @@ -2295,14 +2295,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->flux_count == 0) { - if (trace) out.print("matcher::world_tile_match: NS Flux Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Flux Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->flux_count == 256) { - if (trace) out.print("matcher::world_tile_match: NS Flux Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Flux Absent ({}, {})\n", x, y); return false; } break; @@ -2315,14 +2315,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->coal_count == 0) { - if (trace) out.print("matcher::world_tile_match: NS Coal Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Coal Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->coal_count == 256) { - if (trace) out.print("matcher::world_tile_match: NS Coal Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Coal Absent ({}, {})\n", x, y); return false; } break; @@ -2336,28 +2336,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->max_region_soil < 1) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->max_region_soil < 2) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->max_region_soil < 3) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Deep ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Deep: if (tile->max_region_soil < 4) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Deep ({}, {})\n", x, y); return false; } break; @@ -2375,14 +2375,14 @@ namespace embark_assist { case embark_assist::defs::yes_no_ranges::Yes: if (!tile->blood_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Blood_Rain Yes (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Blood_Rain Yes ({}, {})\n", x, y); return false; } break; case embark_assist::defs::yes_no_ranges::No: if (tile->blood_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Blood_Rain No (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Blood_Rain No ({}, {})\n", x, y); return false; } break; @@ -2397,35 +2397,35 @@ namespace embark_assist { case embark_assist::defs::syndrome_rain_ranges::Any: if (!tile->permanent_syndrome_rain_possible && !tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Permanent: if (!tile->permanent_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Temporary: if (!tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Temporary (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Temporary ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Not_Permanent: if (tile->permanent_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Not_Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Not_Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::None: if (tile->permanent_syndrome_rain_full || tile->temporary_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain None ({}, {})\n", x, y); return false; } break; @@ -2438,42 +2438,42 @@ namespace embark_assist { case embark_assist::defs::reanimation_ranges::Both: if (!tile->reanimating_possible || !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Both (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Both ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Any: if (!tile->reanimating_possible && !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Thralling: if (!tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Reanimation: if (!tile->reanimating_possible) { - if (trace) out.print("matcher::world_tile_match:NS Reanimating Reanimating (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match:NS Reanimating Reanimating ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Not_Thralling: if (tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Not_Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Not_Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::None: if (tile->reanimating_full || tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating None ({}, {})\n", x, y); return false; } break; @@ -2483,7 +2483,7 @@ namespace embark_assist { // Magma Min/Max // Biome Count Min (Can't do anything with Max at this level) if (finder->biome_count_min > tile->biome_count) { - if (trace) out.print("matcher::world_tile_match: NS Biome_Count (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_Count ({}, {})\n", x, y); return false; } @@ -2503,7 +2503,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_1 ({}, {})\n", x, y); return false; } } @@ -2524,7 +2524,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_2 ({}, {})\n", x, y); return false; } } @@ -2545,7 +2545,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_3 ({}, {})\n", x, y); return false; } } @@ -2562,7 +2562,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_1 ({}, {})\n", x, y); return false; } } @@ -2579,7 +2579,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_2 ({}, {})\n", x, y); return false; } } @@ -2596,7 +2596,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_3 ({}, {})\n", x, y); return false; } } @@ -2639,7 +2639,7 @@ namespace embark_assist { !mineral_1 || !mineral_2 || !mineral_3) { - if (trace) out.print("matcher::world_tile_match: NS Metal/Economic/Mineral (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Metal/Economic/Mineral ({}, {})\n", x, y); return false; } } @@ -2948,11 +2948,11 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter preliminary_matches = preliminary_world_match(survey_results, &iterator->finder, match_results); if (preliminary_matches == 0) { - out.printerr("matcher::find: Preliminarily matching World Tiles: %i\n", preliminary_matches); + out.printerr("matcher::find: Preliminarily matching World Tiles: {}\n", preliminary_matches); return 0; } else { - out.print("matcher::find: Preliminarily matching World Tiles: %i\n", preliminary_matches); + out.print("matcher::find: Preliminarily matching World Tiles: {}\n", preliminary_matches); } while (screen->location.region_pos.x != 0 || screen->location.region_pos.y != 0) { diff --git a/plugins/examples/persistent_per_save_example.cpp b/plugins/examples/persistent_per_save_example.cpp index 3ca26fc1d3a..4067e1d161e 100644 --- a/plugins/examples/persistent_per_save_example.cpp +++ b/plugins/examples/persistent_per_save_example.cpp @@ -56,14 +56,14 @@ static PersistentDataItem & ensure_elem_config(color_ostream &out, int id) { if (elems.count(id)) return elems[id]; string keyname = ELEM_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for elem id %d\n", id); + DEBUG(control,out).print("creating new persistent key for elem id {}\n", id); elems.emplace(id, World::GetPersistentSiteData(keyname, true)); return elems[id]; } static void remove_elem_config(color_ostream &out, int id) { if (!elems.count(id)) return; - DEBUG(control,out).print("removing persistent key for elem id %d\n", id); + DEBUG(control,out).print("removing persistent key for elem id {}\n", id); World::DeletePersistentData(elems[id]); elems.erase(id); } @@ -82,7 +82,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -95,19 +95,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -190,7 +190,7 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // TODO: logic that runs every CYCLE_TICKS ticks } diff --git a/plugins/examples/simple_command_example.cpp b/plugins/examples/simple_command_example.cpp index ea45b18d475..d708bc222fe 100644 --- a/plugins/examples/simple_command_example.cpp +++ b/plugins/examples/simple_command_example.cpp @@ -21,7 +21,7 @@ namespace DFHack { static command_result do_command(color_ostream &out, vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(log,out).print("initializing %s\n", plugin_name); + DEBUG(log,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, diff --git a/plugins/examples/skeleton.cpp b/plugins/examples/skeleton.cpp index fd77ab691a5..a6b9e234a3c 100644 --- a/plugins/examples/skeleton.cpp +++ b/plugins/examples/skeleton.cpp @@ -66,7 +66,7 @@ static command_result command_callback1(color_ostream &out, vector ¶ // run when the plugin is loaded DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status,out).print("initializing %s\n", plugin_name); + DEBUG(status,out).print("initializing {}\n", plugin_name); // For in-tree plugins, don't use the "usage" parameter of PluginCommand. // Instead, add an .rst file with the same name as the plugin to the @@ -80,7 +80,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { - DEBUG(command,out).print("%s command called with %zu parameters\n", + DEBUG(command,out).print("{} command called with {} parameters\n", plugin_name, parameters.size()); // Return CR_WRONG_USAGE to print out your help text. The help text is diff --git a/plugins/examples/ui_addition_example.cpp b/plugins/examples/ui_addition_example.cpp index bbd3af3deb7..95bcd164558 100644 --- a/plugins/examples/ui_addition_example.cpp +++ b/plugins/examples/ui_addition_example.cpp @@ -39,14 +39,14 @@ struct title_version_hook : df::viewscreen_titlest { IMPLEMENT_VMETHOD_INTERPOSE(title_version_hook, render); DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(log,out).print("shutting down %s\n", plugin_name); + DEBUG(log,out).print("shutting down {}\n", plugin_name); INTERPOSE_HOOK(title_version_hook, render).remove(); return CR_OK; } DFhackCExport command_result plugin_enable (color_ostream &out, bool enable) { if (enable != is_enabled) { - DEBUG(log,out).print("%s %s\n", plugin_name, + DEBUG(log,out).print("{} {}\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (!INTERPOSE_HOOK(title_version_hook, render).apply(enable)) return CR_FAILURE; diff --git a/plugins/fastdwarf.cpp b/plugins/fastdwarf.cpp index 0d4808db8c0..d1a6750a5d1 100644 --- a/plugins/fastdwarf.cpp +++ b/plugins/fastdwarf.cpp @@ -37,7 +37,7 @@ enum ConfigValues { static command_result do_command(color_ostream &out, vector & parameters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector & commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( "fastdwarf", @@ -48,7 +48,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vectorpath.dest)) return; - DEBUG(cycle,out).print("teleporting unit %d\n", unit->id); + DEBUG(cycle,out).print("teleporting unit {}\n", unit->id); if (!Units::teleport(unit, unit->path.dest)) return; @@ -150,7 +150,7 @@ static void do_tele(color_ostream &out, df::unit * unit) { } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // fast mode 2 is handled by DF itself bool is_fast = config.get_int(CONFIG_FAST) == 1; @@ -161,7 +161,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { do_tele(out, unit); if (is_fast) { - DEBUG(cycle,out).print("fastifying unit %d\n", unit->id); + DEBUG(cycle,out).print("fastifying unit {}\n", unit->id); Units::setGroupActionTimers(out, unit, 1, df::unit_action_type_group::All); } }); @@ -176,7 +176,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -186,7 +186,7 @@ static command_result do_command(color_ostream &out, vector & parameter return CR_WRONG_USAGE; if (num_params == 0 || parameters[0] == "status") { - out.print("Current state: fast = %d, teleport = %d.\n", + out.print("Current state: fast = {}, teleport = {}.\n", config.get_int(CONFIG_FAST), config.get_int(CONFIG_TELE)); return CR_OK; @@ -196,11 +196,11 @@ static command_result do_command(color_ostream &out, vector & parameter int tele = num_params >= 2 ? string_to_int(parameters[1]) : 0; if (fast < 0 || fast > 2) { - out.printerr("Invalid value for fast: '%s'", parameters[0].c_str()); + out.printerr("Invalid value for fast: '{}'", parameters[0]); return CR_WRONG_USAGE; } if (tele < 0 || tele > 1) { - out.printerr("Invalid value for tele: '%s'", parameters[1].c_str()); + out.printerr("Invalid value for tele: '{}'", parameters[1]); return CR_WRONG_USAGE; } diff --git a/plugins/filltraffic.cpp b/plugins/filltraffic.cpp index f16e8bdd838..23952d8ac25 100644 --- a/plugins/filltraffic.cpp +++ b/plugins/filltraffic.cpp @@ -158,7 +158,7 @@ command_result filltraffic(color_ostream &out, std::vector & params return CR_FAILURE; } - out.print("%d/%d/%d ... FILLING!\n", cx,cy,cz); + out.print("{}/{}/{} ... FILLING!\n", cx,cy,cz); //Naive four-way or six-way flood fill with possible tiles on a stack. stack flood; diff --git a/plugins/fix-occupancy.cpp b/plugins/fix-occupancy.cpp index 7b5a416dbb0..5ed962e4d47 100644 --- a/plugins/fix-occupancy.cpp +++ b/plugins/fix-occupancy.cpp @@ -107,8 +107,8 @@ static void scan_building(color_ostream &out, df::building * bld, Expected & exp auto expected_bld = expected.bld(x, y, bld->z); if (bld->isSettingOccupancy() && expected_bld) { if (*expected_bld) { - WARN(log,out).print("Buildings overlap at (%d, %d, %d); please manually remove overlapping building." - " Run ':lua dfhack.gui.revealInDwarfmodeMap(%d, %d, %d, true, true)' to zoom to the tile.\n", + WARN(log,out).print("Buildings overlap at ({}, {}, {}); please manually remove overlapping building." + " Run ':lua dfhack.gui.revealInDwarfmodeMap({}, {}, {}, true, true)' to zoom to the tile.\n", x, y, bld->z, x, y, bld->z); } *expected_bld = bld; @@ -163,7 +163,7 @@ static void normalize_item_vector(color_ostream &out, df::map_block *block, bool prev_id = item_id; } if (needs_sorting) { - INFO(log,out).print("%s item list for map block at (%d, %d, %d)\n", + INFO(log,out).print("{} item list for map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) std::sort(block->items.begin(), block->items.end()); @@ -175,7 +175,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: { // clear building occupancy if there is no building there if (expected_occ.bits.building == df::tile_building_occ::None && block_occ.bits.building != df::tile_building_occ::None) { - INFO(log,out).print("%s building occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} building occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.building = df::tile_building_occ::None; @@ -189,7 +189,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: if (block_occ.bits.building == df::tile_building_occ::Dynamic) block_occ.bits.building = prev_occ; else if (prev_occ != block_occ.bits.building) { - INFO(log,out).print("%s building occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} building occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (dry_run) block_occ.bits.building = prev_occ; @@ -198,13 +198,13 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: // clear unit occupancy if there are no units there if (!expected_occ.bits.unit && block_occ.bits.unit) { - INFO(log,out).print("%s standing unit occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} standing unit occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.unit = false; } if (!expected_occ.bits.unit_grounded && block_occ.bits.unit_grounded) { - INFO(log,out).print("%s grounded unit occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} grounded unit occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.unit_grounded = false; @@ -212,7 +212,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: // clear item occupancy if there are no items there if (!expected_occ.bits.item && block_occ.bits.item) { - INFO(log,out).print("%s item occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} item occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.item = false; @@ -223,7 +223,7 @@ static void fix_tile(color_ostream &out, df::coord pos, bool dry_run) { auto occ = Maps::getTileOccupancy(pos); auto block = Maps::getTileBlock(pos); if (!occ || !block) { - WARN(log,out).print("invalid tile: (%d, %d, %d)\n", pos.x, pos.y, pos.z); + WARN(log,out).print("invalid tile: ({}, {}, {})\n", pos.x, pos.y, pos.z); return; } @@ -264,7 +264,7 @@ static void fix_tile(color_ostream &out, df::coord pos, bool dry_run) { if (expected_bld && expected_occ) reconcile_map_tile(out, *expected_bld, *expected_occ, block->occupancy[pos.x&15][pos.y&15], dry_run, pos.x, pos.y, pos.z); - INFO(log,out).print("verified %zd building(s), %zd unit(s), %zd item(s), 1 map block(s), and 1 map tile(s)\n", + INFO(log,out).print("verified {} building(s), {} unit(s), {} item(s), 1 map block(s), and 1 map tile(s)\n", num_buildings, num_units, num_items); } @@ -273,7 +273,7 @@ static void reconcile_block_items(color_ostream &out, std::set * expect if (!expected_items) { if (block_items.size()) { - INFO(log,out).print("%s stale item references in map block at (%d, %d, %d)\n", + INFO(log,out).print("{} stale item references in map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) block_items.resize(0); @@ -282,7 +282,7 @@ static void reconcile_block_items(color_ostream &out, std::set * expect } if (!std::equal(expected_items->begin(), expected_items->end(), block_items.begin(), block_items.end())) { - INFO(log,out).print("%s stale item references in map block at (%d, %d, %d)\n", + INFO(log,out).print("{} stale item references in map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) { block_items.resize(expected_items->size()); @@ -324,20 +324,20 @@ static void fix_map(color_ostream &out, bool dry_run) { auto expected_occ = expected.occ(x, y, z); auto expected_bld = expected.bld(x, y, z); if (!expected_occ || !expected_bld) { - TRACE(log,out).print("pos out of bounds (%d, %d, %d)\n", x, y, z); + TRACE(log,out).print("pos out of bounds ({}, {}, {})\n", x, y, z); continue; } df::tile_occupancy &block_occ = block->occupancy[xoff][yoff]; if (*expected_bld || (expected_occ->whole & occ_mask) != (block_occ.whole & occ_mask)) { - DEBUG(log,out).print("reconciling occupancy at (%d, %d, %d) (bld=%p, 0x%x ?= 0x%x)\n", - x, y, z, *expected_bld, expected_occ->whole & occ_mask, block_occ.whole & occ_mask); + DEBUG(log,out).print("reconciling occupancy at ({}, {}, {}) (bld={}, 0x{:x} ?= 0x{:x})\n", + x, y, z, static_cast(*expected_bld), expected_occ->whole & occ_mask, block_occ.whole & occ_mask); reconcile_map_tile(out, *expected_bld, *expected_occ, block_occ, dry_run, x, y, z); } } } } - INFO(log,out).print("verified %zd buildings, %zd units, %zd items, %zd map blocks, and %zd map tiles\n", + INFO(log,out).print("verified {} buildings, {} units, {} items, {} map blocks, and {} map tiles\n", world->buildings.all.size(), world->units.active.size(), world->items.other.IN_PLAY.size(), world->map.map_blocks.size(), expected.get_size()); } diff --git a/plugins/fixveins.cpp b/plugins/fixveins.cpp index f87701e0bc4..1fc003972ec 100644 --- a/plugins/fixveins.cpp +++ b/plugins/fixveins.cpp @@ -92,9 +92,9 @@ command_result df_fixveins (color_ostream &out, vector & parameters) } } if (mineral_removed || feature_removed) - out.print("Removed invalid references from %i mineral inclusion and %i map feature tiles.\n", mineral_removed, feature_removed); + out.print("Removed invalid references from {} mineral inclusion and {} map feature tiles.\n", mineral_removed, feature_removed); if (mineral_added || feature_added) - out.print("Restored missing references to %i mineral inclusion and %i map feature tiles.\n", mineral_added, feature_added); + out.print("Restored missing references to {} mineral inclusion and {} map feature tiles.\n", mineral_added, feature_added); return CR_OK; } diff --git a/plugins/flows.cpp b/plugins/flows.cpp index 8ebf968a0bf..c13e355b6ce 100644 --- a/plugins/flows.cpp +++ b/plugins/flows.cpp @@ -46,11 +46,11 @@ command_result df_flows (color_ostream &out, vector & parameters) } } - out.print("Blocks with liquid_1=true: %d\n", flow1); - out.print("Blocks with liquid_2=true: %d\n", flow2); - out.print("Blocks with both: %d\n", flowboth); - out.print("Water tiles: %d\n", water); - out.print("Magma tiles: %d\n", magma); + out.print("Blocks with liquid_1=true: {}\n", flow1); + out.print("Blocks with liquid_2=true: {}\n", flow2); + out.print("Blocks with both: {}\n", flowboth); + out.print("Water tiles: {}\n", water); + out.print("Magma tiles: {}\n", magma); return CR_OK; } diff --git a/plugins/follow.cpp b/plugins/follow.cpp index 5649d6bdfbe..109c37823b4 100644 --- a/plugins/follow.cpp +++ b/plugins/follow.cpp @@ -150,7 +150,7 @@ command_result follow (color_ostream &out, std::vector & parameter ss << "Unpause to begin following " << world->raws.creatures.all[followedUnit->race]->name[0]; if (followedUnit->name.has_name) ss << " " << followedUnit->name.first_name; ss << ". Simply manually move the view to break the following.\n"; - out.print("%s", ss.str().c_str()); + out.print("{}", ss.str()); } else followedUnit = 0; is_enabled = (followedUnit != NULL); diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index fa8cb8ef12d..290772e87ba 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -80,7 +80,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * } else if(!item->isClothing() && !item->isArmorNotClothing()) { - if (verbose) { WARN(log).print("Item %d is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); } return false; } else if (item->getType() != df::enums::item_type::GLOVES && @@ -90,22 +90,22 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * item->getType() != df::enums::item_type::SHOES && !targetBodyPart) { - if (verbose) { WARN(log).print("Item %d is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); } return false; } else if (itemOwner && itemOwner->id != unit->id) { - if (verbose) { WARN(log).print("Item %d is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); } + if (verbose) { WARN(log).print("Item {} is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); } return false; } else if (item->flags.bits.in_inventory) { - if (verbose) { WARN(log).print("Item %d is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); } return false; } else if (item->flags.bits.in_job) { - if (verbose) { WARN(log).print("Item %d is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); } + if (verbose) { WARN(log).print("Item {} is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); } return false; } @@ -132,45 +132,45 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * else if (bpIndex < unit->body.body_plan->body_parts.size()) { // The current body part is not the one that was specified in the function call, but we can keep searching - if (verbose) { WARN(log).print("Found bodypart %s; not a match; continuing search.\n", currPart->token.c_str()); } + if (verbose) { WARN(log).print("Found bodypart {}; not a match; continuing search.\n", currPart->token); } continue; } else { // The specified body part has not been found, and we've reached the end of the list. Report failure. - if (verbose) { WARN(log).print("The specified body part (%s) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token.c_str()); } + if (verbose) { WARN(log).print("The specified body part ({}) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token); } return false; } - if (verbose) { DEBUG(log).print("Inspecting bodypart %s.\n", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Inspecting bodypart {}.\n", currPart->token); } // Inspect the current bodypart if (item->getType() == df::enums::item_type::GLOVES && currPart->flags.is_set(df::body_part_raw_flags::GRASP) && ((item->getGloveHandedness() == const_GloveLeftHandedness && currPart->flags.is_set(df::body_part_raw_flags::LEFT)) || (item->getGloveHandedness() == const_GloveRightHandedness && currPart->flags.is_set(df::body_part_raw_flags::RIGHT)))) { - if (verbose) { DEBUG(log).print("Hand found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Hand found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::HELM && currPart->flags.is_set(df::body_part_raw_flags::HEAD)) { - if (verbose) { DEBUG(log).print("Head found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Head found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::ARMOR && currPart->flags.is_set(df::body_part_raw_flags::UPPERBODY)) { - if (verbose) { DEBUG(log).print("Upper body found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Upper body found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::PANTS && currPart->flags.is_set(df::body_part_raw_flags::LOWERBODY)) { - if (verbose) { DEBUG(log).print("Lower body found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Lower body found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::SHOES && currPart->flags.is_set(df::body_part_raw_flags::STANCE)) { - if (verbose) { DEBUG(log).print("Foot found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Foot found ({}).", currPart->token); } } else if (targetBodyPart && ignoreRestrictions) { // The BP in question would normally be considered ineligible for equipment. But since it was deliberately specified by the user, we'll proceed anyways. - if (verbose) { DEBUG(log).print("Non-standard bodypart found (%s)...", targetBodyPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Non-standard bodypart found ({}).", targetBodyPart->token); } } else if (targetBodyPart) { @@ -205,7 +205,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * // Collision detected; have we reached the limit? if (++collisions >= multiEquipLimit) { - if (verbose) { WARN(log).print(" but it already carries %d piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); } + if (verbose) { WARN(log).print(" but it already carries {} piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); } confirmedBodyPart = NULL; break; } @@ -382,13 +382,13 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (targetBodyPart->token.compare(targetBodyPartCode) == 0) { // It is indeed a match; exit the loop (while leaving the variable populated) - if (verbose) { INFO(log).print("Matching bodypart (%s) found.\n", targetBodyPart->token.c_str()); } + if (verbose) { INFO(log).print("Matching bodypart ({}) found.\n", targetBodyPart->token); } break; } else { // Not a match; nullify the variable (it will get re-populated on the next pass through the loop) - if (verbose) { WARN(log).print("Bodypart \"%s\" does not match \"%s\".\n", targetBodyPart->token.c_str(), targetBodyPartCode.c_str()); } + if (verbose) { WARN(log).print("Bodypart ({}) does not match ({}).\n", targetBodyPart->token, targetBodyPartCode); } targetBodyPart = NULL; } } @@ -396,7 +396,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (!targetBodyPart) { // Loop iteration is complete but no match was found. - WARN(log).print("The unit does not possess a bodypart of type \"%s\". Please check the spelling or choose a different unit.\n", targetBodyPartCode.c_str()); + WARN(log).print("The unit does not possess a bodypart of type ({}). Please check the spelling or choose a different unit.\n", targetBodyPartCode); return CR_FAILURE; } } @@ -466,7 +466,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (itemsEquipped == 0 && !verbose) { WARN(log).print("Some items were found but no equipment changes could be made. Use the /verbose switch to display the reasons for failure.\n"); } - if (itemsEquipped > 0) { INFO(log).print("%d items equipped.\n", itemsEquipped); } + if (itemsEquipped > 0) { INFO(log).print("{} items equipped.\n", itemsEquipped); } // Note: we might expect to recalculate the unit's weight at this point, in order to account for the // added items. In fact, this recalculation occurs automatically during each dwarf's "turn". diff --git a/plugins/generated-creature-renamer.cpp b/plugins/generated-creature-renamer.cpp index 0ea3227b1b8..7c330e00561 100644 --- a/plugins/generated-creature-renamer.cpp +++ b/plugins/generated-creature-renamer.cpp @@ -190,10 +190,10 @@ command_result list_creatures(color_ostream &out, std::vector & pa auto creatureRaw = world->raws.creatures.all[i]; if (!creatureRaw->flags.is_set(df::enums::creature_raw_flags::GENERATED)) continue; - out.print("%s",creatureRaw->creature_id.c_str()); + out.print("{}",creatureRaw->creature_id); if (detailed) { - out.print("\t%s",creatureRaw->caste[0]->description.c_str()); + out.print("\t{}",creatureRaw->caste[0]->description); } out.print("\n"); } diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 18570fda95c..69eba4b1d1c 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -79,13 +79,13 @@ enum class selectability { // result in the plants not being usable for farming or even collectable at all). selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bool farming) { - TRACE(log, out).print("analyzing %s\n", plant->id.c_str()); + TRACE(log, out).print("analyzing {}\n", plant->id); const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant->material_defs.type[plant_material_def::basic_mat], plant->material_defs.idx[plant_material_def::basic_mat]); bool outOfSeason = false; selectability result = selectability::Nonselectable; if (plant->flags.is_set(plant_raw_flags::TREE)) { - DEBUG(log, out).print("%s is a selectable tree\n", plant->id.c_str()); + DEBUG(log, out).print("{} is a selectable tree\n", plant->id); if (farming) { return selectability::Nonselectable; } @@ -94,7 +94,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } } else if (plant->flags.is_set(plant_raw_flags::GRASS)) { - DEBUG(log, out).print("%s is a non selectable Grass\n", plant->id.c_str()); + DEBUG(log, out).print("{} is a non selectable Grass\n", plant->id); return selectability::Grass; } @@ -106,7 +106,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))) { - DEBUG(log, out).print("%s is edible\n", plant->id.c_str()); + DEBUG(log, out).print("{} is edible\n", plant->id); if (farming) { if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW)) { result = selectability::Selectable; @@ -122,7 +122,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo plant->flags.is_set(plant_raw_flags::EXTRACT_VIAL) || plant->flags.is_set(plant_raw_flags::EXTRACT_BARREL) || plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) { - DEBUG(log, out).print("%s is thread/mill/extract\n", plant->id.c_str()); + DEBUG(log, out).print("{} is thread/mill/extract\n", plant->id); if (farming) { result = selectability::Selectable; } @@ -135,7 +135,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo (basic_mat.material->reaction_product.id.size() > 0 || basic_mat.material->reaction_class.size() > 0)) { - DEBUG(log, out).print("%s has a reaction\n", plant->id.c_str()); + DEBUG(log, out).print("{} has a reaction\n", plant->id); if (farming) { result = selectability::Selectable; } @@ -170,7 +170,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo if (*cur_year_tick >= plant->growths[i]->timing_1 && (plant->growths[i]->timing_2 == -1 || *cur_year_tick <= plant->growths[i]->timing_2)) { - DEBUG(log, out).print("%s has an edible seed or a stockpile growth\n", plant->id.c_str()); + DEBUG(log, out).print("{} has an edible seed or a stockpile growth\n", plant->id); if (!farming || seedSource) { return selectability::Selectable; } @@ -204,11 +204,11 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } if (outOfSeason) { - DEBUG(log, out).print("%s has an out of season growth\n", plant->id.c_str()); + DEBUG(log, out).print("{} has an out of season growth\n", plant->id); return selectability::OutOfSeason; } else { - DEBUG(log, out).print("%s cannot be gathered\n", plant->id.c_str()); + DEBUG(log, out).print("{} cannot be gathered\n", plant->id); return result; } } @@ -247,7 +247,7 @@ bool picked(const df::plant* plant, int32_t growth_subtype, int32_t growth_densi } bool designate(color_ostream& out, const df::plant* plant, bool farming) { - TRACE(log, out).print("Attempting to designate %s at (%i, %i, %i)\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z); + TRACE(log, out).print("Attempting to designate {} at ({}, {}, {})\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z); if (!farming) { bool istree = (tileMaterial(Maps::getTileBlock(plant->pos)->tiletype[plant->pos.x % 16][plant->pos.y % 16]) == tiletype_material::TREE); @@ -281,14 +281,14 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { } for (size_t i = 0; i < plant_raw->growths.size(); i++) { - TRACE(log, out).print("growth item type=%d\n", plant_raw->growths[i]->item_type); + TRACE(log, out).print("growth item type={}\n", ENUM_AS_STR(plant_raw->growths[i]->item_type)); // Only trees have seed growths in vanilla, but raws can be modded... if (plant_raw->growths[i]->item_type != df::item_type::SEEDS && plant_raw->growths[i]->item_type != df::item_type::PLANT_GROWTH) continue; const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant_raw->growths[i]->mat_type, plant_raw->growths[i]->mat_index); - TRACE(log, out).print("edible_cooked=%d edible_raw=%d leaf_mat=%d\n", + TRACE(log, out).print("edible_cooked={} edible_raw={} leaf_mat={}\n", growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED), growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW), growth_mat.material->flags.is_set(material_flags::STOCKPILE_PLANT_GROWTH)); @@ -402,20 +402,20 @@ command_result df_getplants(color_ostream& out, vector & parameters) { plantSelections[i] = selectablePlant(out, plant, farming); switch (plantSelections[i]) { case selectability::Grass: - out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str()); + out.printerr("{} is a grass and cannot be gathered\n", plant->id); break; case selectability::Nonselectable: if (farming) { - out.printerr("%s does not have any parts that can be gathered for seeds for farming\n", plant->id.c_str()); + out.printerr("{} does not have any parts that can be gathered for seeds for farming\n", plant->id); } else { - out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str()); + out.printerr("{} does not have any parts that can be gathered\n", plant->id); } break; case selectability::OutOfSeason: - out.printerr("%s is out of season, with nothing that can be gathered now\n", plant->id.c_str()); + out.printerr("{} is out of season, with nothing that can be gathered now\n", plant->id); break; case selectability::Selectable: @@ -429,7 +429,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { if (plantNames.size() > 0) { out.printerr("Invalid plant ID(s):"); for (set::const_iterator it = plantNames.begin(); it != plantNames.end(); it++) - out.printerr(" %s", it->c_str()); + out.printerr(" {}", *it); out.printerr("\n"); return CR_FAILURE; } @@ -454,7 +454,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { case selectability::OutOfSeason: { if (!treesonly) { - out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str()); + out.print("* (shrub) {} - {} is out of season\n", plant->id, plant->name); } break; } @@ -465,7 +465,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { (shrubsonly && !plant->flags.is_set(plant_raw_flags::TREE)) || (!treesonly && !shrubsonly)) // 'farming' weeds out trees when determining selectability, so no need to test that explicitly { - out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str()); + out.print("* ({}) {} - {}\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id, plant->name); } break; } @@ -482,7 +482,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { const df::plant* plant = world->plants.all[i]; df::map_block* cur = Maps::getTileBlock(plant->pos); - TRACE(log, out).print("Examining %s at (%i, %i, %i) [index=%d]\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + TRACE(log, out).print("Examining {} at ({}, {}, {}) [index={}]\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); int x = plant->pos.x % 16; int y = plant->pos.y % 16; @@ -511,7 +511,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { ++count; } if (!deselect && designate(out, plant, farming)) { - DEBUG(log, out).print("Designated %s at (%i, %i, %i), %d\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + DEBUG(log, out).print("Designated {} at ({}, {}, {}), {}\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); collectionCount[plant->material]++; ++count; } @@ -519,11 +519,11 @@ command_result df_getplants(color_ostream& out, vector & parameters) { if (count && verbose) { for (size_t i = 0; i < plantSelections.size(); i++) { if (collectionCount[i] > 0) - out.print("Updated %d %s designations.\n", (int)collectionCount[i], world->raws.plants.all[i]->id.c_str()); + out.print("Updated {} {} designations.\n", collectionCount[i], world->raws.plants.all[i]->id); } out.print("\n"); } - out.print("Updated %d plant designations.\n", (int)count); + out.print("Updated {} plant designations.\n", count); return CR_OK; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f14..58794311521 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -40,10 +40,10 @@ static bool can_invoke(const string &cmdline, df::viewscreen *screen) { } static int cleanupHotkeys(lua_State *) { - DEBUG(log).print("cleaning up old stub keybindings for: %s\n", join_strings(", ", Gui::getCurFocus(true)).c_str()); + DEBUG(log).print("cleaning up old stub keybindings for: {}\n", join_strings(", ", Gui::getCurFocus(true))); std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); + DEBUG(log).print("clearing keybinding: {}\n", keyspec); Core::getInstance().ClearKeyBindings(keyspec); }); valid = false; @@ -82,7 +82,7 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); - DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); + DEBUG(log).print("adding keybinding: {} -> {}\n", keyspec, binding); Core::getInstance().AddKeyBinding(keyspec, binding); } @@ -164,10 +164,10 @@ static void list(color_ostream &out) { if (!valid) find_active_keybindings(out, Gui::getCurViewscreen(true), false); - out.print("Valid keybindings for the current focus:\n %s\n", - join_strings("\n", Gui::getCurFocus(true)).c_str()); + out.print("Valid keybindings for the current focus:\n {}\n", + join_strings("\n", Gui::getCurFocus(true))); std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) { - out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str()); + out.print("{}: {}\n", sym, current_bindings[sym]); }); if (!was_valid) @@ -181,7 +181,7 @@ static bool invoke_command(color_ostream &out, const size_t index) { return false; auto cmd = current_bindings[sorted_keys[index]]; - DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + DEBUG(log).print("invoking command: '{}'\n", cmd); { Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); @@ -194,11 +194,11 @@ static bool invoke_command(color_ostream &out, const size_t index) { static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { if (!parameters.size()) { - DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_DEFAULT_COMMAND.c_str()); + DEBUG(log).print("invoking command: '{}'\n", INVOKE_MENU_DEFAULT_COMMAND); return Core::getInstance().runCommand(out, INVOKE_MENU_DEFAULT_COMMAND); } else if (parameters.size() == 2 && parameters[0] == "menu") { string cmd = INVOKE_MENU_BASE_COMMAND + parameters[1]; - DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + DEBUG(log).print("invoking command: '{}'\n", cmd); return Core::getInstance().runCommand(out, cmd); } diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index ffa62ed8304..158dea2a421 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -62,13 +62,13 @@ void cleanup() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; DEBUG(control, out) - .print("%s from the API; persisting\n", + .print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); @@ -80,7 +80,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } } else { DEBUG(control, out) - .print("%s from the API, but already %s; no action\n", + .print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -103,7 +103,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { plugin_enable(out, true); } DEBUG(control, out) - .print("loading persisted enabled state: %s\n", + .print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; } @@ -113,7 +113,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { DEBUG(control, out) - .print("world unloaded; disabling %s\n", plugin_name); + .print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; cleanup(); } @@ -187,8 +187,7 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { world->map.column_index[bpos.x][bpos.y]; if (!column) { DEBUG(cycle, out) - .print("%s, line %d: column is null (%d, %d).\n", __FILE__, - __LINE__, bpos.x, bpos.y); + .print("{}, line {}: column is null ({}).\n", __FILE__, __LINE__, bpos); continue; } df::block_column_print_infost *glyphs = new df::block_column_print_infost; @@ -242,7 +241,7 @@ struct_identity infinitesky_options::_identity{sizeof(infinitesky_options), &df: command_result infiniteSky(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -254,11 +253,11 @@ command_result infiniteSky(color_ostream &out, return CR_WRONG_USAGE; if (opts.n > 0) { - out.print("Infinite-sky: creating %d new z-level%s of sky.\n", opts.n, + out.print("Infinite-sky: creating {} new z-level{} of sky.\n", opts.n, opts.n == 1 ? "" : "s"); doInfiniteSky(out, opts.n); } else { - out.print("Construction monitoring is %s.\n", + out.print("Construction monitoring is {}.\n", is_enabled ? "enabled" : "disabled"); } return CR_OK; diff --git a/plugins/jobutils.cpp b/plugins/jobutils.cpp index 37912d8429d..c32b9adf22c 100644 --- a/plugins/jobutils.cpp +++ b/plugins/jobutils.cpp @@ -100,8 +100,8 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (!new_mat.isValid() || new_mat.type != 0) { - out.printerr("New job material isn't inorganic: %s\n", - new_mat.toString().c_str()); + out.printerr("New job material isn't inorganic: {}\n", + new_mat.toString()); return CR_FAILURE; } @@ -109,22 +109,22 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (!cur_mat.isValid() || cur_mat.type != 0) { - out.printerr("Current job material isn't inorganic: %s\n", - cur_mat.toString().c_str()); + out.printerr("Current job material isn't inorganic: {}\n", + cur_mat.toString()); return CR_FAILURE; } df::craft_material_class old_class = cur_mat.getCraftClass(); if (old_class == craft_material_class::None) { - out.printerr("Unexpected current material type: %s\n", - cur_mat.toString().c_str()); + out.printerr("Unexpected current material type: {}\n", + cur_mat.toString()); return CR_FAILURE; } if (new_mat.getCraftClass() != old_class) { - out.printerr("New material %s does not satisfy requirement: %s\n", - new_mat.toString().c_str(), ENUM_KEY_STR(craft_material_class, old_class).c_str()); + out.printerr("New material {} does not satisfy requirement: {}\n", + new_mat.toString(), ENUM_KEY_STR(craft_material_class, old_class)); return CR_FAILURE; } @@ -135,15 +135,15 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (item_mat != cur_mat) { - out.printerr("Job item %zu has different material: %s\n", - i, item_mat.toString().c_str()); + out.printerr("Job item {} has different material: {}\n", + i, item_mat.toString()); return CR_FAILURE; } if (!new_mat.matches(*item)) { - out.printerr("Job item %zu requirements not satisfied by %s.\n", - i, new_mat.toString().c_str()); + out.printerr("Job item {} requirements not satisfied by {}.\n", + i, new_mat.toString()); return CR_FAILURE; } } @@ -212,7 +212,7 @@ static command_result job_material_in_build(color_ostream &out, MaterialInfo &ne } } - out.printerr("Could not find material in list: %s\n", new_mat.toString().c_str()); + out.printerr("Could not find material in list: {}\n", new_mat.toString()); return CR_FAILURE; } @@ -222,7 +222,7 @@ static command_result job_material(color_ostream &out, vector & paramet if (parameters.size() == 1) { if (!new_mat.find(parameters[0])) { - out.printerr("Could not find material: %s\n", parameters[0].c_str()); + out.printerr("Could not find material: {}\n", parameters[0]); return CR_WRONG_USAGE; } } @@ -253,7 +253,7 @@ static command_result job_duplicate(color_ostream &out, vector & parame job->job_type != job_type::CollectSand && job->job_type != job_type::CollectClay)) { - out.printerr("Cannot duplicate job %s\n", ENUM_KEY_STR(job_type,job->job_type).c_str()); + out.printerr("Cannot duplicate job {}\n", ENUM_KEY_STR(job_type,job->job_type)); return CR_FAILURE; } diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 90b31b3509b..23585356219 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -66,14 +66,14 @@ enum StockpileConfigForbidValues { }; static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { - TRACE(control, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); + TRACE(control, out).print("ensuring stockpile config stockpile_number={}\n", stockpile_number); if (watched_stockpiles.count(stockpile_number)) { TRACE(control, out).print("stockpile exists in watched_stockpiles\n"); return watched_stockpiles[stockpile_number]; } string keyname = CONFIG_KEY_PREFIX + int_to_string(stockpile_number); - DEBUG(control, out).print("creating new persistent key for stockpile %d\n", stockpile_number); + DEBUG(control, out).print("creating new persistent key for stockpile {}\n", stockpile_number); watched_stockpiles.emplace(stockpile_number, World::GetPersistentSiteData(keyname, true)); PersistentDataItem& c = watched_stockpiles[stockpile_number]; c.set_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER, stockpile_number); @@ -89,7 +89,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock static void remove_stockpile_config(color_ostream& out, int stockpile_number) { if (!watched_stockpiles.count(stockpile_number)) return; - DEBUG(control, out).print("removing persistent key for stockpile %d\n", stockpile_number); + DEBUG(control, out).print("removing persistent key for stockpile {}\n", stockpile_number); World::DeletePersistentData(watched_stockpiles[stockpile_number]); watched_stockpiles.erase(stockpile_number); } @@ -105,7 +105,7 @@ static void do_cycle(color_ostream& out, static void logistics_cycle(color_ostream &out, bool quiet); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { - DEBUG(control, out).print("initializing %s\n", plugin_name); + DEBUG(control, out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -117,7 +117,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -445,12 +445,12 @@ static const struct BadFlags { } bad_flags; static void scan_item(color_ostream &out, df::item *item, StockProcessor &processor) { - DEBUG(cycle,out).print("scan_item [%s] item_id=%d\n", processor.name.c_str(), item->id); + DEBUG(cycle,out).print("scan_item [{}] item_id={}\n", processor.name, item->id); if (DBG_NAME(cycle).isEnabled(DebugCategory::LTRACE)) { string name = ""; item->getItemDescription(&name, 0); - TRACE(cycle,out).print("item: %s\n", name.c_str()); + TRACE(cycle,out).print("item: {}\n", name); } if (processor.is_designated(out, item)) { @@ -539,7 +539,7 @@ static void train_partials(color_ostream& out, int32_t& train_count) { continue; if (Units::assignTrainer(unit)) { - DEBUG(cycle,out).print("assigned trainer to unit %d\n", unit->id); + DEBUG(cycle,out).print("assigned trainer to unit {}\n", unit->id); ++train_count; } } @@ -549,7 +549,7 @@ static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_count, int32_t& dump_count, int32_t& train_count, int32_t& forbid_count, int32_t& claim_count) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); cycle_timestamp = world->frame_counter; ProcessorStats melt_stats, trade_stats, dump_stats, train_stats, forbid_stats, claim_stats; @@ -593,7 +593,7 @@ static void do_cycle(color_ostream& out, train_partials(out, train_count); } - TRACE(cycle,out).print("exit %s do_cycle\n", plugin_name); + TRACE(cycle,out).print("exit {} do_cycle\n", plugin_name); } ///////////////////////////////////////////////////// @@ -673,21 +673,21 @@ static int logistics_getStockpileData(lua_State *L) { } static void logistics_cycle(color_ostream &out, bool quiet = false) { - DEBUG(control, out).print("entering logistics_cycle%s\n", quiet ? " (quiet)" : ""); + DEBUG(control, out).print("entering logistics_cycle{}\n", quiet ? " (quiet)" : ""); int32_t melt_count = 0, trade_count = 0, dump_count = 0, train_count = 0, forbid_count = 0, claim_count = 0; do_cycle(out, melt_count, trade_count, dump_count, train_count, forbid_count, claim_count); if (0 < melt_count || !quiet) - out.print("logistics: designated %d item%s for melting\n", melt_count, (melt_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for melting\n", melt_count, (melt_count == 1) ? "" : "s"); if (0 < trade_count || !quiet) - out.print("logistics: designated %d item%s for trading\n", trade_count, (trade_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for trading\n", trade_count, (trade_count == 1) ? "" : "s"); if (0 < dump_count || !quiet) - out.print("logistics: designated %d item%s for dumping\n", dump_count, (dump_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for dumping\n", dump_count, (dump_count == 1) ? "" : "s"); if (0 < train_count || !quiet) - out.print("logistics: designated %d animal%s for training\n", train_count, (train_count == 1) ? "" : "s"); + out.print("logistics: designated {} animal{} for training\n", train_count, (train_count == 1) ? "" : "s"); if (0 < forbid_count || !quiet) - out.print("logistics: designated %d item%s forbidden\n", forbid_count, (forbid_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} forbidden\n", forbid_count, (forbid_count == 1) ? "" : "s"); if (0 < claim_count || !quiet) - out.print("logistics: claimed %d forbidden item%s\n", claim_count, (claim_count == 1) ? "" : "s"); + out.print("logistics: claimed {} forbidden item{} \n", claim_count, (claim_count == 1) ? "" : "s"); } static void find_stockpiles(lua_State *L, int idx, @@ -756,11 +756,11 @@ static void logistics_setStockpileConfig(color_ostream& out, int stockpile_numbe bool dump, bool train, int forbid, bool melt_masterworks) { DEBUG(control, out).print("entering logistics_setStockpileConfig\n"); - DEBUG(control, out).print("stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d, forbid=%d, melt_masterworks=%d\n", + DEBUG(control, out).print("stockpile_number={}, melt={}, trade={}, dump={}, train={}, forbid={}, melt_masterworks={}\n", stockpile_number, melt, trade, dump, train, forbid, melt_masterworks); if (!find_stockpile(stockpile_number)) { - out.printerr("invalid stockpile number: %d\n", stockpile_number); + out.printerr("invalid stockpile number: {}\n", stockpile_number); return; } @@ -843,8 +843,8 @@ static int logistics_getGlobalCounts(lua_State *L) { } static bool logistics_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control, out).print("entering logistics_setFeature (enabled=%d, feature=%s)\n", - enabled, feature.c_str()); + DEBUG(control, out).print("entering logistics_setFeature (enabled={}, feature={})\n", + enabled, feature); if (feature != "autoretrain") return false; config.set_bool(CONFIG_TRAIN_PARTIAL, enabled); @@ -854,7 +854,7 @@ static bool logistics_setFeature(color_ostream &out, bool enabled, string featur } static bool logistics_getFeature(color_ostream &out, string feature) { - DEBUG(control, out).print("entering logistics_getFeature (feature=%s)\n", feature.c_str()); + DEBUG(control, out).print("entering logistics_getFeature (feature={})\n", feature); if (feature != "autoretrain") return false; return config.get_bool(CONFIG_TRAIN_PARTIAL); diff --git a/plugins/misery.cpp b/plugins/misery.cpp index b81a4f1daa2..7ecdb0d344a 100644 --- a/plugins/misery.cpp +++ b/plugins/misery.cpp @@ -48,7 +48,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -61,19 +61,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -179,7 +179,7 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); int strength = STRENGTH_MULTIPLIER * config.get_int(CONFIG_FACTOR); diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index bce36f91034..e471ac20d37 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -46,7 +46,7 @@ static void do_cycle(color_ostream &out); static command_result do_command(color_ostream &out, std::vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -59,19 +59,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector name.c_str()); + out.print("only protecting eggs inside burrow: {}\n", burrow->name); } else { @@ -159,7 +159,7 @@ static void printStatus(color_ostream &out){ static command_result do_command(color_ostream &out, std::vector ¶meters){ if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -182,7 +182,7 @@ static command_result do_command(color_ostream &out, std::vector ¶me // static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -209,7 +209,7 @@ static void do_cycle(color_ostream &out) { if (sref && sref->data.job) Job::removeJob(sref->data.job); } - out.print("nestboxes: %d eggs %s\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); + out.print("nestboxes: {} eggs {}\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); } } } diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 5891f924a38..4bdff21fe24 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -93,7 +93,7 @@ static command_result orders_command(color_ostream & out, std::vector & continue; } - DEBUG(log).print("scanning map tile at offset %d, %d\n", x, y); + DEBUG(log).print("scanning map tile at offset {}, {}\n", x, y); Screen::Pen cur_tile = Screen::readTile(x, y, true); - DEBUG(log).print("tile data: ch=%d, fg=%d, bg=%d, bold=%s\n", + DEBUG(log).print("tile data: ch={}, fg={}, bg={}, bold={}\n", cur_tile.ch, cur_tile.fg, cur_tile.bg, cur_tile.bold ? "true" : "false"); - DEBUG(log).print("tile data: tile=%d, tile_mode=%d, tile_fg=%d, tile_bg=%d\n", - cur_tile.tile, cur_tile.tile_mode, cur_tile.tile_fg, cur_tile.tile_bg); + DEBUG(log).print("tile data: tile={}, tile_mode={}, tile_fg={}, tile_bg={}\n", + cur_tile.tile, static_cast(cur_tile.tile_mode), cur_tile.tile_fg, cur_tile.tile_bg); if (!cur_tile.valid()) { - DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); + DEBUG(log).print("cannot read tile at offset {}, {}\n", x, y); continue; } bool can_walk = get_can_walk(map_pos); - DEBUG(log).print("tile is %swalkable at offset %d, %d\n", + DEBUG(log).print("tile is {}walkable at offset {}, {}\n", can_walk ? "" : "not ", x, y); if (ctx.use_graphics) { @@ -148,7 +148,7 @@ static bool get_depot_coords(color_ostream &out, unordered_set * depo depot_coords->clear(); for (auto bld : world->buildings.other.TRADE_DEPOT){ - DEBUG(log,out).print("found depot at (%d, %d, %d)\n", bld->centerx, bld->centery, bld->z); + DEBUG(log,out).print("found depot at ({}, {}, {})\n", bld->centerx, bld->centery, bld->z); depot_coords->emplace(bld->centerx, bld->centery, bld->z); } @@ -163,7 +163,7 @@ static bool get_pathability_groups(color_ostream &out, unordered_set * for (auto pos : depot_coords) { auto wgroup = Maps::getWalkableGroup(pos); if (wgroup) { - DEBUG(log,out).print("walkability group at (%d, %d, %d) is %d\n", pos.x, pos.y, pos.z, wgroup); + DEBUG(log,out).print("walkability group at ({}, {}, {}) is {}\n", pos.x, pos.y, pos.z, wgroup); depot_pathability_groups->emplace(wgroup); } } @@ -386,7 +386,7 @@ static bool wagon_flood(color_ostream &out, unordered_set * wagon_pat df::coord pos = ctx.search_edge.top(); ctx.search_edge.pop(); - TRACE(log,out).print("checking tile: (%d, %d, %d); pathability group: %d\n", pos.x, pos.y, pos.z, + TRACE(log,out).print("checking tile: ({}, {}, {}); pathability group: {}\n", pos.x, pos.y, pos.z, Maps::getWalkableGroup(pos)); if (entry_tiles.contains(pos)) { diff --git a/plugins/pet-uncapper.cpp b/plugins/pet-uncapper.cpp index 168c5f0bfd9..0370015dcb2 100644 --- a/plugins/pet-uncapper.cpp +++ b/plugins/pet-uncapper.cpp @@ -119,22 +119,22 @@ void impregnateMany(color_ostream &out, bool verbose = false) { } if (pregnancies || verbose) { - INFO(cycle, out).print("%d pet pregnanc%s initiated\n", + INFO(cycle, out).print("{} pet pregnanc{} initiated\n", pregnancies, pregnancies == 1 ? "y" : "ies"); } } command_result do_command(color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.size() == 0 || parameters[0] == "status") { - out.print("%s is %s\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); - out.print("population cap per species: %d\n", config.get_int(CONFIG_POP_CAP)); - out.print("updating pregnancies every %d ticks\n", config.get_int(CONFIG_FREQ)); - out.print("pregancies last %d ticks\n", config.get_int(CONFIG_PREG_TIME)); + out.print("{} is {}\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); + out.print("population cap per species: {}\n", config.get_int(CONFIG_POP_CAP)); + out.print("updating pregnancies every {} ticks\n", config.get_int(CONFIG_FREQ)); + out.print("pregancies last {} ticks\n", config.get_int(CONFIG_PREG_TIME)); } else if (parameters[0] == "now") { impregnateMany(out, true); } else { @@ -165,19 +165,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, vectorflags.is_set(plant_raw_flags::WET)) || (!is_watery && !p_raw->flags.is_set(plant_raw_flags::DRY))) { - out.printerr("Can't create plant: Plant type can't grow this %s water feature!\n" + out.printerr("Can't create plant: Plant type can't grow this {} water feature!\n" "Override with --force\n", is_watery ? "close to" : "far from"); return CR_FAILURE; } @@ -297,7 +297,7 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt { if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } @@ -332,9 +332,9 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt auto tt = Maps::getTileType(plant->pos); if (!tt || tileShape(*tt) != tiletype_shape::SAPLING) { - out.printerr("Invalid sapling tiletype at (%d, %d, %d): %s!\n", + out.printerr("Invalid sapling tiletype at ({}, {}, {}): {}\n", plant->pos.x, plant->pos.y, plant->pos.z, - tt ? ENUM_KEY_STR(tiletype, *tt).c_str() : "No map block!"); + tt ? ENUM_KEY_STR(tiletype, *tt) : "No map block!"); continue; // Bad tiletype } else if (*tt == tiletype::SaplingDead) @@ -349,9 +349,9 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt } if (do_trees) - out.print("%d saplings and %d trees%s set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); + out.print("{} saplings and {} trees{} set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); else - out.print("%d saplings%s set to grow.\n", grown, options.dry_run ? " would be" : ""); + out.print("{} saplings{} set to grow.\n", grown, options.dry_run ? " would be" : ""); return CR_OK; } @@ -426,7 +426,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } @@ -473,9 +473,9 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (tileShape(*tt) != tiletype_shape::SHRUB) { - out.printerr("Bad shrub tiletype at (%d, %d, %d): %s\n", + out.printerr("Bad shrub tiletype at ({}, {}, {}): {}\n", plant.pos.x, plant.pos.y, plant.pos.z, - ENUM_KEY_STR(tiletype, *tt).c_str()); + ENUM_KEY_STR(tiletype, *tt)); bad_tt = true; } } @@ -483,9 +483,9 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (tileShape(*tt) != tiletype_shape::SAPLING) { - out.printerr("Bad sapling tiletype at (%d, %d, %d): %s\n", + out.printerr("Bad sapling tiletype at ({}, {}, {}): {}\n", plant.pos.x, plant.pos.y, plant.pos.z, - ENUM_KEY_STR(tiletype, *tt).c_str()); + ENUM_KEY_STR(tiletype, *tt)); bad_tt = true; } } @@ -493,7 +493,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl } else { - out.printerr("Bad plant tiletype at (%d, %d, %d): No map block!\n", + out.printerr("Bad plant tiletype at ({}, {}, {}): No map block!\n", plant.pos.x, plant.pos.y, plant.pos.z); bad_tt = true; } @@ -508,7 +508,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl if (!options.dry_run) { if (!uncat_plant(&plant)) - out.printerr("Remove plant: No block column at (%d, %d)!\n", plant.pos.x, plant.pos.y); + out.printerr("Remove plant: No block column at ({}, {})!\n", plant.pos.x, plant.pos.y); if (!bad_tt) // TODO: trees set_tt(plant.pos); @@ -518,7 +518,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl } } - out.print("Plants%s removed: %d (%d bad)\n", options.dry_run ? " that would be" : "", count, count_bad); + out.print("Plants{} removed: {} ({} bad)\n", options.dry_run ? " that would be" : "", count, count_bad); return CR_OK; } @@ -538,11 +538,11 @@ command_result df_plant(color_ostream &out, vector ¶meters) { // Print all non-grass raw IDs ("plant list") out.print("--- Shrubs ---\n"); for (auto p_raw : world->raws.plants.bushes) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); out.print("\n--- Saplings ---\n"); for (auto p_raw : world->raws.plants.trees) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); return CR_OK; } @@ -574,7 +574,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) return CR_FAILURE; } - DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + DEBUG(log, out).print("pos_1 = ({}, {}, {})\npos_2 = ({}, {}, {})\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); if (!Core::getInstance().isMapLoaded()) @@ -599,7 +599,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) if (!pos_1.isValid()) { // Attempt to use cursor for pos if active Gui::getCursorCoords(pos_1); - DEBUG(log, out).print("Try to use cursor (%d, %d, %d) for pos_1.\n", + DEBUG(log, out).print("Try to use cursor ({}, {}, {}) for pos_1.\n", pos_1.x, pos_1.y, pos_1.z); if (!pos_1.isValid()) @@ -609,20 +609,20 @@ command_result df_plant(color_ostream &out, vector ¶meters) } } - DEBUG(log, out).print("plant_idx = %d\n", options.plant_idx); + DEBUG(log, out).print("plant_idx = {}\n", options.plant_idx); auto p_raw = vector_get(world->raws.plants.all, options.plant_idx); if (p_raw) { - DEBUG(log, out).print("Plant raw: %s\n", p_raw->id.c_str()); + DEBUG(log, out).print("Plant raw: {}\n", p_raw->id); if (p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Plant raw was grass: %d (%s)\n", options.plant_idx, p_raw->id.c_str()); + out.printerr("Plant raw was grass: {} ({})\n", options.plant_idx, p_raw->id); return CR_FAILURE; } } else { - out.printerr("Plant raw not found for create: %d\n", options.plant_idx); + out.printerr("Plant raw not found for create: {}\n", options.plant_idx); return CR_FAILURE; } } @@ -638,24 +638,24 @@ command_result df_plant(color_ostream &out, vector ¶meters) for (auto idx : filter) { - DEBUG(log, out).print("Filter/exclude test idx: %d\n", idx); + DEBUG(log, out).print("Filter/exclude test idx: {}\n", idx); auto p_raw = vector_get(world->raws.plants.all, idx); if (p_raw) { - DEBUG(log, out).print("Filter/exclude raw: %s\n", p_raw->id.c_str()); + DEBUG(log, out).print("Filter/exclude raw: {}\n", p_raw->id); if (p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Filter/exclude plant raw was grass: %d (%s)\n", idx, p_raw->id.c_str()); + out.printerr("Filter/exclude plant raw was grass: {} ({})\n", idx, p_raw->id); return CR_FAILURE; } else if (options.grow && !p_raw->flags.is_set(plant_raw_flags::TREE)) { // User might copy-paste filters between grow and remove, so just log this - DEBUG(log, out).print("Filter/exclude shrub with grow: %d (%s)\n", idx, p_raw->id.c_str()); + DEBUG(log, out).print("Filter/exclude shrub with grow: {} ({})\n", idx, p_raw->id); } } else { - out.printerr("Plant raw not found for filter/exclude: %d\n", idx); + out.printerr("Plant raw not found for filter/exclude: {}\n", idx); return CR_FAILURE; } } @@ -689,12 +689,12 @@ command_result df_plant(color_ostream &out, vector ¶meters) bounds.addPos(world->map.x_count-1, world->map.y_count-1, 0); } - DEBUG(log, out).print("bounds = (%d:%d, %d:%d, %d:%d)\n", + DEBUG(log, out).print("bounds = ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index ae8337859d3..eca7b888de7 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -73,7 +73,7 @@ static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, "Manage room assignments for off-map units and noble roles.", @@ -92,12 +92,12 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { EventManager::unregisterAll(plugin_self); } - DEBUG(control, out).print("now %s\n", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("now {}\n", is_enabled ? "enabled" : "disabled"); return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -265,7 +265,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!World::isFortressMode() || !Core::getInstance().isMapLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -344,17 +344,17 @@ static void assign_nobles(color_ostream &out) { zone->spec_sub_flag.bits.active = true; Buildings::setOwner(zone, unit); assigned = true; - INFO(cycle,out).print("preserve-rooms: assigning %s to a %s-associated %s\n", - DF2CONSOLE(Units::getReadableName(unit)).c_str(), - toLower_cp437(code).c_str(), - ENUM_KEY_STR(civzone_type, zone->type).c_str()); + INFO(cycle,out).print("preserve-rooms: assigning {} to a {}-associated {}\n", + DF2CONSOLE(Units::getReadableName(unit)), + toLower_cp437(code), + ENUM_KEY_STR(civzone_type, zone->type)); break; } if (assigned) break; } if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit_id != -1)) { - DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: %d\n", zone_id); + DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: {}\n", zone_id); zone->spec_sub_flag.bits.active = false; Buildings::setOwner(zone, NULL); } @@ -403,8 +403,8 @@ static void scrub_reservations(color_ostream &out) { continue; } } - DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid %d: %s\n", hfid, - hf ? DF2CONSOLE(Units::getReadableName(hf)).c_str() : "culled"); + DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid {}: {}\n", hfid, + hf ? DF2CONSOLE(Units::getReadableName(hf)) : "culled"); hfids_to_scrub.push_back(hfid); for (int32_t zone_id : zone_ids) { if (scrub_id_from_entries(hfid, zone_id, reserved_zones)) { @@ -461,9 +461,9 @@ static void handle_missing_assignments(color_ostream &out, if (spouse_hf && share_with_spouse) { if (spouse && Units::isActive(spouse) && !Units::isDead(spouse) && active_unit_ids.contains(spouse->id)) { - DEBUG(cycle,out).print("assigning zone %d (%s) to spouse %s\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), - DF2CONSOLE(Units::getReadableName(spouse)).c_str()); + DEBUG(cycle,out).print("assigning zone {} ({}) to spouse {}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), + DF2CONSOLE(Units::getReadableName(spouse))); Buildings::setOwner(zone, spouse); continue; } @@ -471,22 +471,22 @@ static void handle_missing_assignments(color_ostream &out, if (hf->died_year > -1) continue; // register the hf ids for reassignment and reserve the room - DEBUG(cycle,out).print("registering primary unit for reassignment to zone %d (%s): %d %s\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), hf->unit_id, - DF2CONSOLE(Units::getReadableName(hf)).c_str()); + DEBUG(cycle,out).print("registering primary unit for reassignment to zone {} ({}): {} {}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), hf->unit_id, + DF2CONSOLE(Units::getReadableName(hf))); pending_reassignment[hfid].push_back(zone_id); reserved_zones[zone_id].push_back(hfid); if (share_with_spouse && spouse) { - DEBUG(cycle,out).print("registering spouse unit for reassignment to zone %d (%s): hfid=%d\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), spouse_hfid); + DEBUG(cycle,out).print("registering spouse unit for reassignment to zone {} ({}): hfid={}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), spouse_hfid); pending_reassignment[spouse_hfid].push_back(zone_id); reserved_zones[zone_id].push_back(spouse_hfid); } - INFO(cycle,out).print("preserve-rooms: reserving %s for the return of %s%s%s\n", - toLower_cp437(ENUM_KEY_STR(civzone_type, zone->type)).c_str(), - DF2CONSOLE(Units::getReadableName(hf)).c_str(), + INFO(cycle,out).print("preserve-rooms: reserving {} for the return of {}{}\n", + toLower_cp437(ENUM_KEY_STR(civzone_type, zone->type)), + DF2CONSOLE(Units::getReadableName(hf)), spouse_hf ? " or their spouse, " : "", - spouse_hf ? DF2CONSOLE(Units::getReadableName(spouse_hf)).c_str() : ""); + spouse_hf ? DF2CONSOLE(Units::getReadableName(spouse_hf)) : ""); zone->spec_sub_flag.bits.active = false; } @@ -504,7 +504,7 @@ static void process_rooms(color_ostream &out, for (auto zone : vec) { auto idx = linear_index(df::global::world->buildings.all, (df::building*)(zone)); if (idx == -1) { - WARN(cycle, out).print("invalid building pointer %p in building vector\n", zone); + WARN(cycle, out).print("invalid building pointer {} in building vector\n", static_cast(zone)); continue; } if (zone->assigned_unit_id == -1) { @@ -513,7 +513,7 @@ static void process_rooms(color_ostream &out, } auto owner = Buildings::getOwner(zone); if (!owner) { - DEBUG(cycle, out).print("building %d has owner id %d but no such unit exists\n", zone->id, zone->assigned_unit_id); + DEBUG(cycle, out).print("building {} has owner id {} but no such unit exists\n", zone->id, zone->assigned_unit_id); continue; } auto hf = df::historical_figure::find(owner->hist_figure_id); @@ -530,7 +530,7 @@ static void process_rooms(color_ostream &out, static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; - TRACE(cycle,out).print("running %s cycle\n", plugin_name); + TRACE(cycle,out).print("running {} cycle\n", plugin_name); if (config.get_bool(CONFIG_TRACK_MISSIONS)) { unordered_set active_unit_ids; @@ -542,7 +542,7 @@ static void do_cycle(color_ostream &out) { process_rooms(out, active_unit_ids, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); process_rooms(out, active_unit_ids, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); - DEBUG(cycle,out).print("tracking zone assignments: bedrooms: %zd, offices: %zd, dining halls: %zd, tombs: %zd\n", + DEBUG(cycle,out).print("tracking zone assignments: bedrooms: {}, offices: {}, dining halls: {}, tombs: {}\n", last_known_assignments_bedroom.size(), last_known_assignments_office.size(), last_known_assignments_dining.size(), last_known_assignments_tomb.size()); @@ -584,15 +584,15 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto unit = df::unit::find(unit_id); if (!unit || unit->hist_figure_id < 0) return; - TRACE(event,out).print("unit %d (%s) arrived on map (hfid: %d, in pending: %d)\n", - unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str(), unit->hist_figure_id, + TRACE(event,out).print("unit {} ({}) arrived on map (hfid: {}, in pending: {})\n", + unit->id, DF2CONSOLE(Units::getReadableName(unit)), unit->hist_figure_id, pending_reassignment.contains(unit->hist_figure_id)); auto hfid = unit->hist_figure_id; auto it = pending_reassignment.find(hfid); if (it == pending_reassignment.end()) return; - INFO(event,out).print("preserve-rooms: restoring room ownership for %s\n", - DF2CONSOLE(Units::getReadableName(unit)).c_str()); + INFO(event,out).print("preserve-rooms: restoring room ownership for {}\n", + DF2CONSOLE(Units::getReadableName(unit))); for (auto zone_id : it->second) { reserved_zones.erase(zone_id); auto zone = virtual_cast(df::building::find(zone_id)); @@ -601,7 +601,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { zone->spec_sub_flag.bits.active = true; if (zone->assigned_unit_id != -1 || spouse_has_sharable_room(out, hfid, zone->type)) continue; - DEBUG(event,out).print("reassigning zone %d\n", zone->id); + DEBUG(event,out).print("reassigning zone {}\n", zone->id); Buildings::setOwner(zone, unit); } pending_reassignment.erase(it); @@ -617,8 +617,8 @@ static void preserve_rooms_cycle(color_ostream &out) { } static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control,out).print("preserve_rooms_setFeature: enabled=%d, feature=%s\n", - enabled, feature.c_str()); + DEBUG(control,out).print("preserve_rooms_setFeature: enabled={}, feature={}\n", + enabled, feature); if (feature == "track-missions") { config.set_bool(CONFIG_TRACK_MISSIONS, enabled); if (is_enabled && enabled) @@ -633,7 +633,7 @@ static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string f } static bool preserve_rooms_getFeature(color_ostream &out, string feature) { - TRACE(control,out).print("preserve_rooms_getFeature: feature=%s\n", feature.c_str()); + TRACE(control,out).print("preserve_rooms_getFeature: feature={}\n", feature); if (feature == "track-missions") return config.get_bool(CONFIG_TRACK_MISSIONS); if (feature == "track-roles") @@ -642,7 +642,7 @@ static bool preserve_rooms_getFeature(color_ostream &out, string feature) { } static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { - DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature=%s\n", feature.c_str()); + DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature={}\n", feature); if (feature == "track-missions") { vector zone_ids; std::transform(reserved_zones.begin(), reserved_zones.end(), std::back_inserter(zone_ids), [](auto & elem){ return elem.first; }); @@ -667,7 +667,7 @@ static int preserve_rooms_assignToRole(lua_State *L) { virtual_cast(df::building::find(zone_id)); if (!zone) return 0; - DEBUG(control,*out).print("preserve_rooms_assignToRole: zone_id=%d\n", zone->id); + DEBUG(control,*out).print("preserve_rooms_assignToRole: zone_id={}\n", zone->id); vector group_codes; Lua::GetVector(L, group_codes); @@ -689,7 +689,7 @@ static int preserve_rooms_assignToRole(lua_State *L) { static string get_role_assignment(color_ostream &out, df::building_civzonest * zone) { if (!zone) return ""; - TRACE(control,out).print("get_role_assignment: zone_id=%d\n", zone->id); + TRACE(control,out).print("get_role_assignment: zone_id={}\n", zone->id); auto it = noble_zones.find(zone->id); if (it == noble_zones.end() || it->second.empty()) return ""; @@ -709,7 +709,7 @@ static bool preserve_rooms_isReserved(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; - TRACE(control,out).print("preserve_rooms_isReserved: zone_id=%d\n", zone->id); + TRACE(control,out).print("preserve_rooms_isReserved: zone_id={}\n", zone->id); auto it = reserved_zones.find(zone->id); return it != reserved_zones.end() && it->second.size() > 0; } @@ -718,7 +718,7 @@ static string preserve_rooms_getReservationName(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return ""; - TRACE(control,out).print("preserve_rooms_getReservationName: zone_id=%d\n", zone->id); + TRACE(control,out).print("preserve_rooms_getReservationName: zone_id={}\n", zone->id); auto it = reserved_zones.find(zone->id); if (it != reserved_zones.end() && it->second.size() > 0) { if (auto hf = df::historical_figure::find(it->second.front())) { @@ -732,7 +732,7 @@ static bool preserve_rooms_clearReservation(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; - DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id=%d\n", zone->id); + DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id={}\n", zone->id); clear_reservation(out, zone->id, zone); return true; } diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 40b9eb56fd6..a695b22e318 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -53,25 +53,25 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & params) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot use %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (params.size() == 0 || params[0] == "status") { - out.print("%s is currently %s\n", plugin_name, is_enabled ? "enabled" : "disabled"); + out.print("{} is currently {}\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (is_enabled) { out.print("tracked tomb assignments:\n"); std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){ auto& [unit_id, building_id] = p; auto* unit = df::unit::find(unit_id); std::string name = unit ? DF2CONSOLE(Units::getReadableName(unit)) : "UNKNOWN UNIT" ; - out.print("%s (id %d) -> building %d\n", name.c_str(), unit_id, building_id); + out.print("{} (id {}) -> building {}\n", name, unit_id, building_id); }); } return CR_OK; } if (params[0] == "now") { if (!is_enabled) { - out.printerr("Cannot update %s when not enabled", plugin_name); + out.printerr("Cannot update {} when not enabled", plugin_name); return CR_FAILURE; } update_tomb_assignments(out); @@ -96,13 +96,13 @@ static void do_disable() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) @@ -110,7 +110,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { else do_disable(); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -118,7 +118,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); // PluginManager handles unregistering our handler from EventManager, // so we don't have to do that here @@ -136,7 +136,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); if (is_enabled) do_enable(out); @@ -146,7 +146,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; do_disable(); @@ -176,16 +176,16 @@ void onUnitDeath(color_ostream& out, void* ptr) { int32_t building_id = it->second; if (!assign_to_tomb(unit, building_id)) { if (unit) { - WARN(event, out).print("%s died - but failed to assign them back to their tomb %d\n", + WARN(event, out).print("{} died - but failed to assign them back to their tomb {}\n", DF2CONSOLE(Units::getReadableName(unit)).c_str(), building_id); } else { - WARN(event, out).print("Unit %d died - but failed to assign them back to their tomb %d\n", unit_id, building_id); + WARN(event, out).print("Unit {} died - but failed to assign them back to their tomb {}\n", unit_id, building_id); } return; } // success, print status update and remove assignment from our memo-list - INFO(event, out).print("%s died - assigning them back to their tomb\n", + INFO(event, out).print("{} died - assigning them back to their tomb\n", DF2CONSOLE(Units::getReadableName(unit)).c_str()); tomb_assignments.erase(it); } @@ -206,10 +206,10 @@ static void update_tomb_assignments(color_ostream &out) { if (it == tomb_assignments.end()) { tomb_assignments.emplace(tomb->assigned_unit_id, tomb->id); - DEBUG(cycle, out).print("%s new tomb assignment, unit %d to tomb %d\n", + DEBUG(cycle, out).print("{} new tomb assignment, unit {} to tomb {}\n", plugin_name, tomb->assigned_unit_id, tomb->id); } else if (it->second != tomb->id) { - DEBUG(cycle, out).print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", + DEBUG(cycle, out).print("{} tomb assignment to {} changed, (old: {}, new: {})\n", plugin_name, tomb->assigned_unit_id, it->second, tomb->id); it->second = tomb->id; } @@ -221,17 +221,17 @@ static void update_tomb_assignments(color_ostream &out) { auto bld = df::building::find(building_id); if (!bld) { - DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("{} tomb missing: {} - removing\n", plugin_name, building_id); return true; } auto tomb = virtual_cast(bld); if (!tomb || !tomb->flags.bits.exists) { - DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("{} tomb missing: {} - removing\n", plugin_name, building_id); return true; } if (tomb->assigned_unit_id != unit_id) { - DEBUG(cycle, out).print("%s unit %d unassigned from tomb %d - removing\n", plugin_name, unit_id, building_id); + DEBUG(cycle, out).print("{} unit {} unassigned from tomb {} - removing\n", plugin_name, unit_id, building_id); return true; } diff --git a/plugins/probe.cpp b/plugins/probe.cpp index eff0c98c863..f49e167867b 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -41,7 +41,7 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) if (!unit) return CR_FAILURE; - out.print("Creature %d, race %d (0x%x), civ %d (0x%x)\n", + out.print("Creature {}, race {} (0x{:x}), civ {} (0x{:x})\n", unit->id, unit->race, unit->race, unit->civ_id, unit->civ_id); for (auto inv_item : unit->inventory) { @@ -62,25 +62,25 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) } static void describeTile(color_ostream &out, df::tiletype tiletype) { - out.print("%d", tiletype); + out.print("{}", static_cast(tiletype)); if (tileName(tiletype)) - out.print(" = %s", tileName(tiletype)); - out.print(" (%s)", ENUM_KEY_STR(tiletype, tiletype).c_str()); + out.print(" = {}", tileName(tiletype)); + out.print(" ({})", ENUM_KEY_STR(tiletype, tiletype).c_str()); out.print("\n"); df::tiletype_shape shape = tileShape(tiletype); df::tiletype_material material = tileMaterial(tiletype); df::tiletype_special special = tileSpecial(tiletype); df::tiletype_variant variant = tileVariant(tiletype); - out.print("%-10s: %4d %s\n","Class" ,shape, - ENUM_KEY_STR(tiletype_shape, shape).c_str()); - out.print("%-10s: %4d %s\n","Material" , - material, ENUM_KEY_STR(tiletype_material, material).c_str()); - out.print("%-10s: %4d %s\n","Special" , - special, ENUM_KEY_STR(tiletype_special, special).c_str()); - out.print("%-10s: %4d %s\n" ,"Variant" , - variant, ENUM_KEY_STR(tiletype_variant, variant).c_str()); - out.print("%-10s: %s\n" ,"Direction", + out.print("{:>10}: {:4} {}\n","Class" ,static_cast(shape), + ENUM_KEY_STR(tiletype_shape, shape)); + out.print("{:>10}: {:4} {}\n","Material" , static_cast(material), + ENUM_KEY_STR(tiletype_material, material)); + out.print("{:>10}: {:4} {}\n","Special" , static_cast(special), + ENUM_KEY_STR(tiletype_special, special)); + out.print("{:>10}: {:4} {}\n","Variant" , static_cast(variant), + ENUM_KEY_STR(tiletype_variant, variant)); + out.print("{:>10}: {}\n" ,"Direction", tileDirection(tiletype).getStr()); out.print("\n"); } @@ -129,7 +129,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) } auto &block = *b->getRaw(); - out.print("block addr: %p\n\n", &block); + out.print("block addr: {}\n\n", static_cast(&block)); df::tiletype tiletype = mc.tiletypeAt(cursor); df::tile_designation &des = block.designation[tileX][tileY]; @@ -143,8 +143,8 @@ static command_result df_probe(color_ostream &out, vector & parameters) out.print("base: "); describeTile(out, mc.baseTiletypeAt(cursor)); - out.print("temperature1: %d U\n", mc.temperature1At(cursor)); - out.print("temperature2: %d U\n", mc.temperature2At(cursor)); + out.print("temperature1: {} U\n", mc.temperature1At(cursor)); + out.print("temperature2: {} U\n", mc.temperature2At(cursor)); int offset = block.region_offset[des.bits.biome]; int bx = clip_range(block.region_pos.x + (offset % 3) - 1, 0, world->world_data->world_width-1); @@ -223,12 +223,12 @@ static command_result df_probe(color_ostream &out, vector & parameters) if (des.bits.water_stagnant) out << "stagnant" << std::endl; - #define PRINT_FLAG( FIELD, BIT ) out.print("%-16s= %c\n", #BIT , ( FIELD.bits.BIT ? 'Y' : ' ' ) ) + #define PRINT_FLAG( FIELD, BIT ) out.print("{:<16} = {}\n", #BIT , ( FIELD.bits.BIT ? 'Y' : ' ' ) ) - out.print("%-16s= %s\n", "dig", enum_item_key(des.bits.dig).c_str()); + out.print("{:<16} = {}\n", "dig", enum_item_key(des.bits.dig)); PRINT_FLAG(occ, dig_marked); PRINT_FLAG(occ, dig_auto); - out.print("%-16s= %s\n", "traffic", enum_item_key(des.bits.traffic).c_str()); + out.print("{:<16} = {}\n", "traffic", enum_item_key(des.bits.traffic)); PRINT_FLAG(occ, carve_track_north); PRINT_FLAG(occ, carve_track_south); PRINT_FLAG(occ, carve_track_east); @@ -241,7 +241,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) PRINT_FLAG( des, water_table ); PRINT_FLAG( des, rained ); PRINT_FLAG( occ, monster_lair); - out.print("%-16s= %d\n", "fog_of_war", fog_of_war); + out.print("{:<16} = {}\n", "fog_of_war", fog_of_war); df::coord2d pc(blockX, blockY); @@ -251,19 +251,20 @@ static command_result df_probe(color_ostream &out, vector & parameters) PRINT_FLAG( des, feature_local ); if(local.type != -1) { - out.print("%-16s", ""); - out.print(" %4d", block.local_feature); - out.print(" (%2d)", local.type); - out.print(" addr %p ", local.origin); - out.print(" %s\n", sa_feature(local.type)); + out.print("{:<16}", ""); + out.print(" {:4}", block.local_feature); + out.print(" ({:2})", static_cast(local.type)); + out.print(" addr {}", static_cast(local.origin)); + out.print(" {}", sa_feature(local.type)); } PRINT_FLAG( des, feature_global ); if(global.type != -1) { - out.print("%-16s", ""); - out.print(" %4d", block.global_feature); - out.print(" (%2d)", global.type); - out.print(" %s\n", sa_feature(global.type)); + out.print("{:<16}", ""); + out.print(" {:4}", block.global_feature); + out.print(" ({:2})", static_cast(global.type)); + out.print(" {}", static_cast(global.origin)); + out.print(" {}", sa_feature(global.type)); } out << "local feature idx: " << block.local_feature << std::endl; @@ -272,7 +273,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) out << std::endl; out << "Occupancy:" << std::endl; - out.print("%-16s= %s\n", "building", enum_item_key(occ.bits.building).c_str()); + out.print("{:<16} = {}\n", "building", enum_item_key(occ.bits.building)); PRINT_FLAG(occ, unit); PRINT_FLAG(occ, unit_grounded); PRINT_FLAG(occ, item); @@ -331,67 +332,67 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) Subtype subtype{bld->getSubtype()}; int32_t custom = bld->getCustomType(); - out.print("Building %i, \"%s\", type %s (%i)", + out.print("Building {:<4}, \"{}\", type {} ({})", bld->id, - name.c_str(), - ENUM_KEY_STR(building_type, bld_type).c_str(), - bld_type); + name, + ENUM_KEY_STR(building_type, bld_type), + static_cast(bld_type)); switch (bld_type) { case df::building_type::Civzone: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(civzone_type, subtype.civzone_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(civzone_type, subtype.civzone_type), subtype.subtype); break; case df::building_type::Furnace: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(furnace_type, subtype.furnace_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(furnace_type, subtype.furnace_type), subtype.subtype); if (subtype.furnace_type == df::furnace_type::Custom) - out.print(", custom type %s (%i)", - world->raws.buildings.all[custom]->code.c_str(), + out.print(", custom type {} ({})", + world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Workshop: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(workshop_type, subtype.workshop_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(workshop_type, subtype.workshop_type), subtype.subtype); if (subtype.workshop_type == df::workshop_type::Custom) - out.print(", custom type %s (%i)", - world->raws.buildings.all[custom]->code.c_str(), + out.print(", custom type {} ({})", + world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Construction: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(construction_type, subtype.construction_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(construction_type, subtype.construction_type), subtype.subtype); break; case df::building_type::Shop: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(shop_type, subtype.shop_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(shop_type, subtype.shop_type), subtype.subtype); break; case df::building_type::SiegeEngine: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type), subtype.subtype); break; case df::building_type::Trap: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(trap_type, subtype.trap_type).c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(trap_type, subtype.trap_type), subtype.subtype); break; case df::building_type::NestBox: { df::building_nest_boxst* nestbox = virtual_cast(bld); if (nestbox) - out.print(", claimed:(%i), items:%zu", nestbox->claimed_by, nestbox->contained_items.size()); + out.print(", claimed:({}), items:({})", nestbox->claimed_by, nestbox->contained_items.size()); break; } default: if (subtype.subtype != -1) - out.print(", subtype %i", subtype.subtype); + out.print(", subtype {}", subtype.subtype); break; } diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index e8883fde590..035d6cdee12 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -230,7 +230,7 @@ void printVeins(color_ostream &con, MatMap &mat_map, df::inorganic_raw *gloss = vector_get(inorganics, kv.first); if (!gloss) { - con.printerr("invalid material gloss: %hi\n", kv.first); + con.printerr("invalid material gloss: {}\n", kv.first); continue; } @@ -298,7 +298,7 @@ static df::world_region_details *get_details(df::world_data *data, df::coord2d p bool estimate_underground(color_ostream &out, EmbarkTileLayout &tile, df::world_region_details *details, int x, int y) { if (x < 0 || y < 0 || x > 15 || y > 15) { - out.printerr("Invalid embark coordinates: x=%i, y=%i\n", x, y); + out.printerr("Invalid embark coordinates: x={}, y={}\n", x, y); return false; } // Find actual biome @@ -428,7 +428,7 @@ bool estimate_materials(color_ostream &out, EmbarkTileLayout &tile, MatMap &laye if (!geo_biome) { - out.printerr("Region geo-biome not found: (%d,%d)\n", + out.printerr("Region geo-biome not found: ({}, {})\n", tile.biome_pos.x, tile.biome_pos.y); return false; } diff --git a/plugins/regrass.cpp b/plugins/regrass.cpp index d40e1bc6dcc..a17f5fb7e27 100644 --- a/plugins/regrass.cpp +++ b/plugins/regrass.cpp @@ -111,7 +111,7 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc else if (block->occupancy[tx][ty].bits.building > (options.buildings ? tile_building_occ::Passable : tile_building_occ::None)) { // Avoid stockpiles and planned/passable buildings unless enabled. - TRACE(log, out).print("Invalid tile: Building (%s)\n", + TRACE(log, out).print("Invalid tile: Building ({})\n", ENUM_KEY_STR(tile_building_occ, block->occupancy[tx][ty].bits.building).c_str()); return false; } @@ -180,7 +180,7 @@ static void grasses_for_tile(color_ostream &out, vector &vec, df::map_b for (auto p_raw : world->raws.plants.grasses) { // Sorted by df::plant_raw::index if (p_raw->flags.is_set(plant_raw_flags::BIOME_SUBTERRANEAN_WATER)) { - TRACE(log, out).print("Cave grass: %s\n", p_raw->id.c_str()); + TRACE(log, out).print("Cave grass: {}\n", p_raw->id); vec.push_back(p_raw->index); } } @@ -209,7 +209,7 @@ static void grasses_for_tile(color_ostream &out, vector &vec, df::map_b (evil || !flags.is_set(plant_raw_flags::EVIL)) && (savage || !flags.is_set(plant_raw_flags::SAVAGE))) { - TRACE(log, out).print("Surface grass: %s\n", p_raw->id.c_str()); + TRACE(log, out).print("Surface grass: {}\n", p_raw->id); vec.push_back(p_raw->index); } } @@ -241,7 +241,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d } else if (gr_ev->amount[tx][ty] > 0) { // Refill first non-zero grass. - DEBUG(log, out).print("Refilling existing grass (ID %d).\n", gr_ev->plant_index); + DEBUG(log, out).print("Refilling existing grass (ID {}).\n", gr_ev->plant_index); gr_ev->amount[tx][ty] = 100; return true; } @@ -263,12 +263,12 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d for (auto gr_ev : consider_grev) { // These grass events are already present. if (erase_from_vector(new_grasses, gr_ev->plant_index)) { - TRACE(log, out).print("Grass (ID %d) already present.\n", gr_ev->plant_index); + TRACE(log, out).print("Grass (ID {}) already present.\n", gr_ev->plant_index); } } for (auto id : new_grasses) { - DEBUG(log, out).print("Allocating new grass event (ID %d).\n", id); + DEBUG(log, out).print("Allocating new grass event (ID {}).\n", id); auto gr_ev = df::allocate(); if (!gr_ev) { out.printerr("Failed to allocate new grass event! Aborting loop.\n"); @@ -297,7 +297,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d for (size_t i = consider_grev.size(); i-- > 0;) { // Iterate backwards and remove invalid events from consideration. if (!vector_contains(valid_grasses, consider_grev[i]->plant_index)) { - TRACE(log, out).print("Removing grass (ID %d) from consideration.\n", + TRACE(log, out).print("Removing grass (ID {}) from consideration.\n", consider_grev[i]->plant_index); consider_grev.erase(consider_grev.begin() + i); } @@ -327,14 +327,14 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d if (options.max_grass) { // Don't need random choice. - DEBUG(log, out).print("Done with max regrass, %s.\n", + DEBUG(log, out).print("Done with max regrass, {}.\n", success ? "success" : "failure"); return success; } TRACE(log, out).print("Selecting random valid grass event...\n"); if (auto rand_grev = vector_get_random(consider_grev)) { - DEBUG(log, out).print("Refilling random grass (ID %d).\n", rand_grev->plant_index); + DEBUG(log, out).print("Refilling random grass (ID {}).\n", rand_grev->plant_index); rand_grev->amount[tx][ty] = 100; return true; } @@ -346,11 +346,11 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo { // Regrass single tile. Return 1 if tile success, else 0. CHECK_NULL_POINTER(block); if (!is_valid_tile_coord(df::coord2d(tx, ty))) { - out.printerr("(%d, %d) not in range 0-15!\n", tx, ty); + out.printerr("({}, {}) not in range 0-15!\n", tx, ty); return 0; } - DEBUG(log, out).print("Regrass tile (%d, %d, %d)\n", + DEBUG(log, out).print("Regrass tile ({}, {}, {})\n", block->map_pos.x + tx, block->map_pos.y + ty, block->map_pos.z); if (!regrass_events(out, options, block, tx, ty)) return 0; @@ -377,7 +377,7 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo { // Ramp or stairs. auto new_mat = (rand() & 1) ? tiletype_material::GRASS_LIGHT : tiletype_material::GRASS_DARK; auto new_tt = findTileType(shape, new_mat, tiletype_variant::NONE, tiletype_special::NONE, nullptr); - DEBUG(log, out).print("Tiletype to %s.\n", ENUM_KEY_STR(tiletype, new_tt).c_str()); + DEBUG(log, out).print("Tiletype to {}.\n", ENUM_KEY_STR(tiletype, new_tt)); block->tiletype[tx][ty] = new_tt; } return 1; @@ -385,12 +385,12 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo int regrass_cuboid(color_ostream &out, const regrass_options &options, const cuboid &bounds) { // Regrass all tiles in the defined cuboid. - DEBUG(log, out).print("Regrassing cuboid (%d:%d, %d:%d, %d:%d), clipped to map.\n", + DEBUG(log, out).print("Regrassing cuboid ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); int count = 0; bounds.forBlock([&](df::map_block *block, cuboid intersect) { - DEBUG(log, out).print("Cuboid regrass block at (%d, %d, %d)\n", + DEBUG(log, out).print("Cuboid regrass block at ({}, {}, {})\n", block->map_pos.x, block->map_pos.y, block->map_pos.z); intersect.forCoord([&](df::coord pos) { count += regrass_tile(out, options, block, pos.x&15, pos.y&15); @@ -404,7 +404,7 @@ int regrass_cuboid(color_ostream &out, const regrass_options &options, const cub int regrass_block(color_ostream &out, const regrass_options &options, df::map_block *block) { // Regrass all tiles in a single block. CHECK_NULL_POINTER(block); - DEBUG(log, out).print("Regrass block at (%d, %d, %d)\n", + DEBUG(log, out).print("Regrass block at ({}, {}, {})\n", block->map_pos.x, block->map_pos.y, block->map_pos.z); int count = 0; @@ -443,11 +443,11 @@ command_result df_regrass(color_ostream &out, vector ¶meters) else if (options.forced_plant == -2) { // Print all grass raw IDs. for (auto p_raw : world->raws.plants.grasses) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); return CR_OK; } - DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + DEBUG(log, out).print("pos_1 = ({}, {}, {})\npos_2 = ({}, {}, {})\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); if (options.block && options.zlevel) { @@ -464,17 +464,17 @@ command_result df_regrass(color_ostream &out, vector ¶meters) } if (options.force) { - DEBUG(log, out).print("forced_plant = %d\n", options.forced_plant); + DEBUG(log, out).print("forced_plant = {}\n", options.forced_plant); auto p_raw = vector_get(world->raws.plants.all, options.forced_plant); if (p_raw) { if (!p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Plant raw wasn't grass: %d (%s)\n", options.forced_plant, p_raw->id.c_str()); + out.printerr("Plant raw wasn't grass: {} ({})\n", options.forced_plant, p_raw->id); return CR_FAILURE; } - out.print("Forced grass: %s\n", p_raw->id.c_str()); + out.print("Forced grass: {}\n", p_raw->id); } else { - out.printerr("Plant raw not found for --force: %d\n", options.forced_plant); + out.printerr("Plant raw not found for --force: {}\n", options.forced_plant); return CR_FAILURE; } } @@ -484,7 +484,7 @@ command_result df_regrass(color_ostream &out, vector ¶meters) { // Specified z-levels or viewport z. auto z1 = pos_1.isValid() ? pos_1.z : Gui::getViewportPos().z; auto z2 = pos_2.isValid() ? pos_2.z : z1; - DEBUG(log, out).print("Regrassing z-levels %d to %d...\n", z1, z2); + DEBUG(log, out).print("Regrassing z-levels {} to {}...\n", z1, z2); count = regrass_cuboid(out, options, cuboid(0, 0, z1, world->map.x_count-1, world->map.y_count-1, z2)); } @@ -519,6 +519,6 @@ command_result df_regrass(color_ostream &out, vector ¶meters) DEBUG(log, out).print("Regrassing map...\n"); count = regrass_map(out, options); } - out.print("Regrew %d tiles of grass.\n", count); + out.print("Regrew {} tiles of grass.\n", count); return CR_OK; } diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index a7bfccd7ce5..60c04ac25b7 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -213,7 +213,7 @@ command_result loadArtImageChunk(color_ostream &out, std::vector & { int index = atoi(parameters[0].c_str()); auto chunk = GetArtImageChunk(&(world->art_image_chunks.all), index); - out.print("Loaded chunk id: %d\n", chunk->id); + out.print("Loaded chunk id: {}\n", chunk->id); } return CR_OK; } @@ -627,7 +627,7 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) } clock_t end = clock(); double elapsed_secs = double(end - start) / CLOCKS_PER_SEC; - stream.print("Checking all hashes took %f seconds.", elapsed_secs); + stream.print("Checking all hashes took {} seconds.", elapsed_secs); return CR_OK; } @@ -2262,8 +2262,7 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w out->set_map_y(pos_y); out->set_world_width(17); out->set_world_height(17); - char name[256]; - sprintf(name, "Region %d, %d", pos_x, pos_y); + std::string name = fmt::format("Region {}, {}", pos_x, pos_y); out->set_name_english(name); out->set_name(name); auto poles = worldData->flip_latitude; @@ -2366,8 +2365,7 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w int pos_y = worldRegionDetails->pos.y; out->set_map_x(pos_x); out->set_map_y(pos_y); - char name[256]; - sprintf(name, "Region %d, %d", pos_x, pos_y); + std::string name = fmt::format("Region {}, {}", pos_x, pos_y); out->set_name_english(name); out->set_name(name); diff --git a/plugins/rendermax/renderer_light.cpp b/plugins/rendermax/renderer_light.cpp index 469c849091f..b905cd93c58 100644 --- a/plugins/rendermax/renderer_light.cpp +++ b/plugins/rendermax/renderer_light.cpp @@ -1185,12 +1185,12 @@ void lightingEngineViewscreen::loadSettings() int ret=luaL_loadfile(s,settingsfile.c_str()); if(ret==LUA_ERRFILE) { - out.printerr("File not found:%s\n",settingsfile.c_str()); + out.printerr("File not found:{}\n",settingsfile); lua_pop(s,1); } else if(ret==LUA_ERRSYNTAX) { - out.printerr("Syntax error:\n\t%s\n",lua_tostring(s,-1)); + out.printerr("Syntax error:\n\t{}\n",lua_tostring(s,-1)); } else { @@ -1202,38 +1202,38 @@ void lightingEngineViewscreen::loadSettings() lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu materials loaded\n",matDefs.size()); + out.print("{} materials loaded\n",matDefs.size()); lua_pushcfunction(s, parseSpecial); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu day light colors loaded\n",dayColors.size()); + out.print("{} day light colors loaded\n",dayColors.size()); lua_pushcfunction(s, parseBuildings); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu buildings loaded\n",buildingDefs.size()); + out.print("{} buildings loaded\n",buildingDefs.size()); lua_pushcfunction(s, parseCreatures); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu creatures loaded\n",creatureDefs.size()); + out.print("{} creatures loaded\n",creatureDefs.size()); lua_pushcfunction(s, parseItems); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu items loaded\n",itemDefs.size()); + out.print("{} items loaded\n",itemDefs.size()); } } } catch(std::exception& e) { - out.printerr("%s",e.what()); + out.printerr("{}\n",e.what()); } lua_pop(s,1); } diff --git a/plugins/rendermax/rendermax.cpp b/plugins/rendermax/rendermax.cpp index 5515bb39a2c..39e59e4f6b9 100644 --- a/plugins/rendermax/rendermax.cpp +++ b/plugins/rendermax/rendermax.cpp @@ -433,7 +433,7 @@ static command_result rendermax(color_ostream &out, vector & parameters else if(cmd=="disable") { if(current_mode==MODE_DEFAULT) - out.print("%s\n","Not installed, doing nothing."); + out.print("{}\n","Not installed, doing nothing."); else removeOld(); gps->force_full_display_count++; diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 994ac067ace..7eecabd2c56 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -69,7 +69,7 @@ static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { if (watched_seeds.count(id)) return watched_seeds[id]; string keyname = SEED_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for seed type %d\n", id); + DEBUG(control,out).print("creating new persistent key for seed type {}\n", id); watched_seeds.emplace(id, World::GetPersistentSiteData(keyname, true)); watched_seeds[id].set_int(SEED_CONFIG_ID, id); return watched_seeds[id]; @@ -77,7 +77,7 @@ static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { static void remove_seed_config(color_ostream &out, int id) { if (!watched_seeds.count(id)) return; - DEBUG(control,out).print("removing persistent key for seed type %d\n", id); + DEBUG(control,out).print("removing persistent key for seed type {}\n", id); World::DeletePersistentData(watched_seeds[id]); watched_seeds.erase(id); } @@ -90,12 +90,12 @@ static bool validate_seed_config(color_ostream& out, PersistentDataItem c) int seed_id = c.get_int(SEED_CONFIG_ID); auto plant = df::plant_raw::find(seed_id); if (!plant) { - WARN(control,out).print("discarded invalid seed id: %d\n", seed_id); + WARN(control,out).print("discarded invalid seed id: {}\n", seed_id); return false; } bool valid = (!plant->flags.is_set(df::enums::plant_raw_flags::TREE)); if (!valid) { - DEBUG(control, out).print("invalid configuration for %s discarded\n", plant->id.c_str()); + DEBUG(control, out).print("invalid configuration for {} discarded\n", plant->id); } return valid; } @@ -108,7 +108,7 @@ static void do_cycle(color_ostream &out, int32_t *num_enabled_seeds = NULL, int3 static void seedwatch_setTarget(color_ostream &out, string name, int32_t num); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -144,19 +144,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -304,7 +304,7 @@ static void scan_seeds(color_ostream &out, unordered_map *acce } static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_t *num_disabled_seed_types) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -325,14 +325,14 @@ static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_ if (accessible_counts[id] <= target && (Kitchen::isPlantCookeryAllowed(id) || Kitchen::isSeedCookeryAllowed(id))) { - DEBUG(cycle,out).print("disabling seed mat: %d\n", id); + DEBUG(cycle,out).print("disabling seed mat: {}\n", id); if (num_disabled_seed_types) ++*num_disabled_seed_types; Kitchen::denyPlantSeedCookery(id); } else if (target + TARGET_BUFFER < accessible_counts[id] && (!Kitchen::isPlantCookeryAllowed(id) || !Kitchen::isSeedCookeryAllowed(id))) { - DEBUG(cycle,out).print("enabling seed mat: %d\n", id); + DEBUG(cycle,out).print("enabling seed mat: {}\n", id); if (num_enabled_seed_types) ++*num_enabled_seed_types; Kitchen::allowPlantSeedCookery(id); @@ -351,7 +351,7 @@ static void set_target(color_ostream &out, int32_t id, int32_t target) { else { if (id < 0 || (size_t)id >= world->raws.plants.all.size()) { WARN(control,out).print( - "cannot set target for unknown plant id: %d\n", id); + "cannot set target for unknown plant id: {}\n", id); return; } PersistentDataItem &c = ensure_seed_config(out, id); @@ -385,7 +385,7 @@ static void seedwatch_setTarget(color_ostream &out, string name, int32_t num) { if (!world_plant_ids.count(token)) { token = toUpper_cp437(token); if (!world_plant_ids.count(token)) { - out.printerr("%s has not been found as a material.\n", token.c_str()); + out.printerr("{} has not been found as a material.\n", token); return; } } diff --git a/plugins/showmood.cpp b/plugins/showmood.cpp index 198dd190e0b..977c767aebc 100644 --- a/plugins/showmood.cpp +++ b/plugins/showmood.cpp @@ -31,7 +31,7 @@ REQUIRE_GLOBAL(world); command_result df_showmood (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -65,7 +65,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) out.printerr("Dwarf with strange mood does not have a mood type!\n"); continue; } - out.print("%s is currently ", DF2CONSOLE(out, Units::getReadableName(unit)).c_str()); + out.print("{} is currently ", DF2CONSOLE(out, Units::getReadableName(unit))); switch (unit->mood) { case mood_type::Macabre: @@ -143,7 +143,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) out.print("do something else..."); break; } - out.print(" and become a legendary %s", ENUM_ATTR_STR(job_skill, caption_noun, unit->job.mood_skill)); + out.print(" and become a legendary {}", ENUM_ATTR_STR(job_skill, caption_noun, unit->job.mood_skill)); if (unit->mood == mood_type::Possessed) out.print(" (but not really)"); break; @@ -160,7 +160,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) { string name; building->getName(&name); - out.print("claimed a %s and wants", name.c_str()); + out.print("claimed a {} and wants", name.c_str()); } else out.print("not yet claimed a workshop but will want"); @@ -169,7 +169,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) for (size_t i = 0; i < job->job_items.elements.size(); i++) { df::job_item *item = job->job_items.elements[i]; - out.print("Item %zu: ", i + 1); + out.print("Item {}: ", i + 1); MaterialInfo matinfo(item->mat_type, item->mat_index); @@ -178,37 +178,37 @@ command_result df_showmood (color_ostream &out, vector & parameters) switch (item->item_type) { case item_type::BOULDER: - out.print("%s boulder", mat_name.c_str()); + out.print("{} boulder", mat_name); break; case item_type::BLOCKS: - out.print("%s blocks", mat_name.c_str()); + out.print("{} blocks", mat_name); break; case item_type::WOOD: - out.print("%s logs", mat_name.c_str()); + out.print("{} logs", mat_name); break; case item_type::BAR: if (matinfo.isInorganicWildcard()) mat_name = "metal"; if (matinfo.inorganic && matinfo.inorganic->flags.is_set(inorganic_flags::WAFERS)) - out.print("%s wafers", mat_name.c_str()); + out.print("{} wafers", mat_name); else - out.print("%s bars", mat_name.c_str()); + out.print("{} bars", mat_name); break; case item_type::SMALLGEM: - out.print("%s cut gems", mat_name.c_str()); + out.print("{} cut gems", mat_name); break; case item_type::ROUGH: if (matinfo.isAnyInorganic()) { if (matinfo.isInorganicWildcard()) mat_name = "any"; - out.print("%s rough gems", mat_name.c_str()); + out.print("{} rough gems", mat_name); } else - out.print("raw %s", mat_name.c_str()); + out.print("raw {}", mat_name); break; case item_type::SKIN_TANNED: - out.print("%s leather", mat_name.c_str()); + out.print("{} leather", mat_name); break; case item_type::CLOTH: if (matinfo.isNone()) @@ -220,50 +220,51 @@ command_result df_showmood (color_ostream &out, vector & parameters) else if (item->flags2.bits.yarn) mat_name = "any yarn"; } - out.print("%s cloth", mat_name.c_str()); + out.print("{} cloth", mat_name); break; case item_type::REMAINS: - out.print("%s remains", mat_name.c_str()); + out.print("{} remains", mat_name); break; case item_type::CORPSE: - out.print("%s %scorpse", mat_name.c_str(), (item->flags1.bits.murdered ? "murdered " : "")); + out.print("{} {}corpse", mat_name, (item->flags1.bits.murdered ? "murdered " : "")); break; case item_type::NONE: if (item->flags2.bits.body_part) { if (item->flags2.bits.bone) - out.print("%s bones", mat_name.c_str()); + out.print("{} bones", mat_name); else if (item->flags2.bits.shell) - out.print("%s shells", mat_name.c_str()); + out.print("{} shells", mat_name); else if (item->flags2.bits.horn) - out.print("%s horns", mat_name.c_str()); + out.print("{} horns", mat_name); else if (item->flags2.bits.pearl) - out.print("%s pearls", mat_name.c_str()); + out.print("{} pearls", mat_name); else if (item->flags2.bits.ivory_tooth) - out.print("%s ivory/teeth", mat_name.c_str()); + out.print("{} ivory/teeth", mat_name); else - out.print("%s unknown body parts (%s:%s:%s)", - mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("{} unknown body parts ({}:{}:{})", + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); } else - out.print("indeterminate %s item (%s:%s:%s)", - mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("indeterminate {} item ({}:{}:{})", + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); break; default: { ItemTypeInfo itinfo(item->item_type, item->item_subtype); - out.print("item %s material %s flags (%s:%s:%s)", - itinfo.toString().c_str(), mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("item {} material {} flags ({}:{}:{})", + itinfo.toString(), + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); break; } } @@ -280,7 +281,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) if (job->items[j]->job_item_idx == int32_t(i)) count_got += 1; } - out.print(", got %i of %i\n", count_got, + out.print(", got {} of {}\n", count_got, item->quantity < divisor ? item->quantity : item->quantity/divisor); } } diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index ca30fc76b41..3d9fbd537d4 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -1503,7 +1503,7 @@ static void releaseTiredWorker(EngineInfo *engine, df::job *job, df::unit *worke if (Job::removeWorker(job)) { color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("Released tired operator %d from siege engine.\n", worker->id); + out.print("Released tired operator {} from siege engine.\n", worker->id); if (process_jobs) *process_jobs = true; diff --git a/plugins/sort.cpp b/plugins/sort.cpp index 7706750fefd..f1b700c5afc 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -56,7 +56,7 @@ static bool do_filter(const char *module_name, const char *fn_name, const item_o ret = lua_toboolean(L, 1); } ); - TRACE(log).print("filter result for %s: %d\n", Units::getReadableName(unit).c_str(), ret); + TRACE(log).print("filter result for {}: {}\n", Units::getReadableName(unit), ret); return !ret; } @@ -85,7 +85,7 @@ static int32_t our_filter_idx(filter_func* filter, df::widget_unit_list* unitlis auto t = fn.target(); if (t && *t == filter) { - TRACE(log).print("found our filter function at idx %d\n", idx); + TRACE(log).print("found our filter function at idx {}\n", idx); return idx; } ++idx; @@ -172,7 +172,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector= 0) { - DEBUG(log,out).print("removing %s filter function\n", which); + DEBUG(log,out).print("removing {} filter function\n", which); filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); vector_erase_at(*filter_vec, idx); } @@ -182,7 +182,7 @@ static void remove_sort_function(color_ostream &out, const char *which, df::widg std::vector *sorting_by = reinterpret_cast *>(&unitlist->sorting_by); int32_t idx = our_sort_idx(*sorting_by); if (idx >= 0) { - DEBUG(log).print("removing %s sort function\n", which); + DEBUG(log).print("removing {} sort function\n", which); vector_erase_at(*sorting_by, idx); } } diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index ae287f60003..072ad813c74 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -178,12 +178,12 @@ static class UnitHistory { void add_to_history(color_ostream &out, int32_t unit_id) { if (offset > 0) { - DEBUG(cycle,out).print("trimming history forward of offset %zd\n", offset); + DEBUG(cycle,out).print("trimming history forward of offset {}\n", offset); history.resize(history.size() - offset); offset = 0; } if (history.size() && history.back() == unit_id) { - DEBUG(cycle,out).print("unit %d is already current unit; not adding to history\n", unit_id); + DEBUG(cycle,out).print("unit {} is already current unit; not adding to history\n", unit_id); } else { history.push_back(unit_id); if (history.size() > MAX_HISTORY) { @@ -191,19 +191,19 @@ static class UnitHistory { history.pop_front(); } } - DEBUG(cycle,out).print("history now has %zd entries\n", history.size()); + DEBUG(cycle,out).print("history now has {} entries\n", history.size()); } void add_and_follow(color_ostream &out, df::unit *unit) { // if we're currently following a unit, add it to the history if it's not already there if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { - DEBUG(cycle,out).print("currently following unit %d that is not in history; adding\n", plotinfo->follow_unit); + DEBUG(cycle,out).print("currently following unit {} that is not in history; adding\n", plotinfo->follow_unit); add_to_history(out, plotinfo->follow_unit); } int32_t id = unit->id; add_to_history(out, id); - DEBUG(cycle,out).print("now following unit %d: %s\n", id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + DEBUG(cycle,out).print("now following unit {}: {}\n", id, DF2CONSOLE(Units::getReadableName(unit))); Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_item = -1; plotinfo->follow_unit = id; @@ -216,7 +216,7 @@ static class UnitHistory { } ++offset; int unit_id = get_cur_unit_id(); - DEBUG(cycle,out).print("scanning back to unit %d at offset %zd\n", unit_id, offset); + DEBUG(cycle,out).print("scanning back to unit {} at offset {}\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_item = -1; @@ -232,7 +232,7 @@ static class UnitHistory { --offset; int unit_id = get_cur_unit_id(); - DEBUG(cycle,out).print("scanning forward to unit %d at offset %zd\n", unit_id, offset); + DEBUG(cycle,out).print("scanning forward to unit {} at offset {}\n", unit_id, offset); if (auto unit = df::unit::find(unit_id)) Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); plotinfo->follow_item = -1; @@ -281,7 +281,7 @@ static class RecentUnits { static void on_new_active_unit(color_ostream& out, void* data) { int32_t unit_id = reinterpret_cast(data); - DEBUG(event,out).print("unit %d has arrived on map\n", unit_id); + DEBUG(event,out).print("unit {} has arrived on map\n", unit_id); recent_units.add(unit_id); } @@ -294,7 +294,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void follow_a_dwarf(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -320,19 +320,19 @@ static void set_next_cycle_unpaused_ms(color_ostream &out, bool has_active_comba delay_ms = distribution(rng); delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); } - DEBUG(cycle,out).print("next cycle in %d ms\n", delay_ms); + DEBUG(cycle,out).print("next cycle in {} ms\n", delay_ms); next_cycle_unpaused_ms = Core::getInstance().getUnpausedMs() + delay_ms; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); if (enable) { config.reset(); @@ -340,7 +340,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { WARN(control,out).print("Failed to refresh config\n"); } if (is_squads_open()) { - out.printerr("Cannot enable %s while the squads screen is open.\n", plugin_name); + out.printerr("Cannot enable {} while the squads screen is open.\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.spectate", "show_squads_warning"); is_enabled = false; return CR_FAILURE; @@ -358,7 +358,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { // don't reset the unit history since we may want to re-enable } } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -366,7 +366,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); on_disable(out); return CR_OK; } @@ -378,7 +378,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan break; case SC_WORLD_UNLOADED: if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; on_disable(out, true); @@ -456,7 +456,7 @@ static void get_dwarf_buckets(color_ostream &out, continue; if (is_in_combat(unit)) { - TRACE(cycle, out).print("unit %d is in combat: %s\n", unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str()); + TRACE(cycle, out).print("unit {} is in combat: {}\n", unit->id, DF2CONSOLE(Units::getReadableName(unit))); if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) citizen_combat_units.push_back(unit); else @@ -498,17 +498,17 @@ static void add_bucket(const vector &bucket, vector &units } #define DUMP_BUCKET(name) \ - DEBUG(cycle,out).print("bucket: " #name ", size: %zd\n", name.size()); \ + DEBUG(cycle,out).print("bucket: " #name ", size: {}\n", name.size()); \ if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ for (auto u : name) { \ - DEBUG(cycle,out).print(" unit %d: %s\n", u->id, DF2CONSOLE(Units::getReadableName(u)).c_str()); \ + DEBUG(cycle,out).print(" unit {}: {}\n", u->id, DF2CONSOLE(Units::getReadableName(u))); \ } \ } #define DUMP_FLOAT_VECTOR(name) \ DEBUG(cycle,out).print(#name ":\n"); \ for (float f : name) { \ - DEBUG(cycle,out).print(" %d\n", (int)f); \ + DEBUG(cycle,out).print(" {}\n", (int)f); \ } static void follow_a_dwarf(color_ostream &out) { @@ -551,7 +551,7 @@ static void follow_a_dwarf(color_ostream &out) { DUMP_BUCKET(other_units); DUMP_FLOAT_VECTOR(intervals); DUMP_FLOAT_VECTOR(weights); - DEBUG(cycle,out).print("selected unit idx %d\n", unit_idx); + DEBUG(cycle,out).print("selected unit idx {}\n", unit_idx); } unit_history.add_and_follow(out, unit); @@ -561,7 +561,7 @@ static void follow_a_dwarf(color_ostream &out) { // Lua API static void spectate_setSetting(color_ostream &out, string name, int val) { - DEBUG(control,out).print("entering spectate_setSetting %s = %d\n", name.c_str(), val); + DEBUG(control,out).print("entering spectate_setSetting {} = {}\n", name, val); if (name == "auto-unpause") { if (val && !config.auto_unpause) { @@ -593,7 +593,7 @@ static void spectate_setSetting(color_ostream &out, string name, int val) { } config.follow_ms = val * 1000; } else { - WARN(control,out).print("Unknown setting: %s\n", name.c_str()); + WARN(control,out).print("Unknown setting: {}\n", name); } } @@ -610,9 +610,9 @@ static void spectate_followNext(color_ostream &out) { }; static void spectate_addToHistory(color_ostream &out, int32_t unit_id) { - DEBUG(control,out).print("entering spectate_addToHistory; unit_id=%d\n", unit_id); + DEBUG(control,out).print("entering spectate_addToHistory; unit_id={}\n", unit_id); if (!df::unit::find(unit_id)) { - WARN(control,out).print("unit with id %d not found; not adding to history\n", unit_id); + WARN(control,out).print("unit with id {} not found; not adding to history\n", unit_id); return; } unit_history.add_to_history(out, unit_id); diff --git a/plugins/steam-engine.cpp b/plugins/steam-engine.cpp index befd53f10a0..06b14aa6c01 100644 --- a/plugins/steam-engine.cpp +++ b/plugins/steam-engine.cpp @@ -870,7 +870,7 @@ static bool find_engines(color_ostream &out) if (!ws.gear_tiles.empty()) engines.push_back(ws); else - out.printerr("%s has no gear tiles - ignoring.\n", wslist[i]->code.c_str()); + out.printerr("{} has no gear tiles - ignoring.\n", wslist[i]->code); } return !engines.empty(); diff --git a/plugins/stockflow.cpp b/plugins/stockflow.cpp index 1671d08991a..524bd16896a 100644 --- a/plugins/stockflow.cpp +++ b/plugins/stockflow.cpp @@ -284,7 +284,7 @@ static bool apply_hooks(color_ostream &out, bool enabling) { } if (!INTERPOSE_HOOK(stockflow_hook, feed).apply(enabling) || !INTERPOSE_HOOK(stockflow_hook, render).apply(enabling)) { - out.printerr("Could not %s stockflow hooks!\n", enabling? "insert": "remove"); + out.printerr("Could not {} stockflow hooks!\n", enabling? "insert": "remove"); return false; } @@ -336,7 +336,7 @@ static command_result stockflow_cmd(color_ostream &out, vector & parame } } - out.print("Stockflow is %s %s%s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled", fast? ", in fast mode": ""); + out.print("Stockflow is {} {}{}.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled", fast? ", in fast mode": ""); enabled = desired; return CR_OK; } diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 879353f3b03..1392e8c12c4 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -22,7 +22,7 @@ DBG_EXTERN(stockpiles, log); */ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { - DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx); + DEBUG(log, out).print("food_lookup: food_idx({})\n", food_idx); auto& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; @@ -32,11 +32,11 @@ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category: mat_category == organic_mat_category::Eggs) { food_mat.creature = raws.creatures.all[type]; food_mat.caste = food_mat.creature->caste[main_idx]; - DEBUG(log, out).print("special creature type(%d) caste(%d)\n", type, main_idx); + DEBUG(log, out).print("special creature type({}) caste({})\n", type, main_idx); } else { food_mat.material.decode(type, main_idx); - DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); + DEBUG(log, out).print("type({}) index({}) token({})\n", type, main_idx, food_mat.material.getToken()); } } std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, const FoodMat& food_mat) { @@ -80,22 +80,22 @@ int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_cate std::vector tokens; split_string(&tokens, token, ":"); if (tokens.size() != 2) { - WARN(log, out).print("creature invalid CREATURE:CASTE token: %s\n", token.c_str()); + WARN(log, out).print("creature invalid CREATURE:CASTE token: {}\n", token); return -1; } int16_t creature_idx = find_creature(tokens[0]); if (creature_idx < 0) { - WARN(log, out).print("creature invalid token %s\n", tokens[0].c_str()); + WARN(log, out).print("creature invalid token {}\n", tokens[0]); return -1; } int16_t food_idx = linear_index(table.organic_types[mat_category], creature_idx); if (tokens[1] == "MALE") food_idx += 1; if (table.organic_types[mat_category][food_idx] == creature_idx) { - DEBUG(log, out).print("creature %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx); + DEBUG(log, out).print("creature {} caste {} creature_idx({}) food_idx({})\n", token, tokens[1], creature_idx, food_idx); return food_idx; } - WARN(log, out).print("creature caste not found: %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx); + WARN(log, out).print("creature caste not found: {} caste {} creature_idx({}) food_idx({})\n", token, tokens[1], creature_idx, food_idx); return -1; } @@ -106,12 +106,12 @@ int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_cate int32_t index = mat_info.index; auto it = food_index[mat_category].find(std::make_pair(type, index)); if (it != food_index[mat_category].end()) { - DEBUG(log, out).print("matinfo: %s type(%d) idx(%d) food_idx(%zd)\n", token.c_str(), mat_info.type, mat_info.index, it->second); + DEBUG(log, out).print("matinfo: {} type({}) idx({}) food_idx({})\n", token, mat_info.type, mat_info.index, it->second); return it->second; } - WARN(log, out).print("matinfo: %s type(%d) idx(%d) food_idx not found :(\n", token.c_str(), mat_info.type, mat_info.index); + WARN(log, out).print("matinfo: {} type({}) idx({}) food_idx not found :(\n", token, mat_info.type, mat_info.index); return -1; } diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index ad76977f1a9..46efd95044c 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -180,8 +180,8 @@ bool StockpileSettingsSerializer::serialize_to_ostream(color_ostream& out, std:: bool StockpileSettingsSerializer::serialize_to_file(color_ostream& out, const string& file, uint32_t includedElements) { std::fstream output(file, std::ios::out | std::ios::binary | std::ios::trunc); if (output.fail()) { - WARN(log, out).print("ERROR: failed to open file for writing: '%s'\n", - file.c_str()); + WARN(log, out).print("ERROR: failed to open file for writing: '{}'\n", + file); return false; } return serialize_to_ostream(out, &output, includedElements); @@ -202,8 +202,8 @@ bool StockpileSettingsSerializer::parse_from_istream(color_ostream &out, std::is bool StockpileSettingsSerializer::unserialize_from_file(color_ostream &out, const string& file, DeserializeMode mode, const vector& filters) { std::fstream input(file, std::ios::in | std::ios::binary); if (input.fail()) { - WARN(log, out).print("failed to open file for reading: '%s'\n", - file.c_str()); + WARN(log, out).print("failed to open file for reading: '{}'\n", + file); return false; } return parse_from_istream(out, &input, mode, filters); @@ -224,7 +224,7 @@ static typename df::enum_traits::base_type token_to_enum_val(const string& to static bool matches_filter(color_ostream& out, const vector& filters, const string& name) { for (auto & filter : filters) { - DEBUG(log, out).print("searching for '%s' in '%s'\n", filter.c_str(), name.c_str()); + DEBUG(log, out).print("searching for '{}' in '{}'\n", filter, name); if (std::search(name.begin(), name.end(), filter.begin(), filter.end(), [](unsigned char ch1, unsigned char ch2) { return std::toupper(ch1) == std::toupper(ch2); } ) != name.end()) @@ -235,7 +235,7 @@ static bool matches_filter(color_ostream& out, const vector& filters, co static void set_flag(color_ostream& out, const char* name, const vector& filters, bool all, char val, bool enabled, bool& elem) { if ((all || enabled) && matches_filter(out, filters, name)) { - DEBUG(log, out).print("setting %s to %d\n", name, val); + DEBUG(log, out).print("setting {} to {}\n", name, val); elem = val; } } @@ -243,7 +243,7 @@ static void set_flag(color_ostream& out, const char* name, const vector& static void set_filter_elem(color_ostream& out, const char* subcat, const vector& filters, char val, const string& name, const string& id, char& elem) { if (matches_filter(out, filters, subcat + ((*subcat ? "/" : "") + name))) { - DEBUG(log, out).print("setting %s (%s) to %d\n", name.c_str(), id.c_str(), val); + DEBUG(log, out).print("setting {} ({}) to {}\n", name, id, val); elem = val; } } @@ -252,7 +252,7 @@ template static void set_filter_elem(color_ostream& out, const char* subcat, const vector& filters, T_val val, const string& name, T_id id, T_val& elem) { if (matches_filter(out, filters, subcat + ((*subcat ? "/" : "") + name))) { - DEBUG(log, out).print("setting %s (%d) to %d\n", name.c_str(), (int32_t)id, val); + DEBUG(log, out).print("setting {} ({}) to {}\n", name, id, val); elem = val; } } @@ -287,7 +287,7 @@ static bool serialize_list_itemdef(color_ostream& out, FuncWriteExport add_value ItemTypeInfo ii; if (!ii.decode(type, i)) continue; - DEBUG(log, out).print("adding itemdef type %s\n", ii.getToken().c_str()); + DEBUG(log, out).print("adding itemdef type {}\n", ii.getToken()); add_value(ii.getToken()); } return all; @@ -312,7 +312,7 @@ static void unserialize_list_itemdef(color_ostream& out, const char* subcat, boo if (!ii.find(id)) continue; if (ii.subtype < 0 || size_t(ii.subtype) >= pile_list.size()) { - WARN(log, out).print("item type index invalid: %d\n", ii.subtype); + WARN(log, out).print("item type index invalid: {}\n", ii.subtype); continue; } set_filter_elem(out, subcat, filters, val, id, ii.subtype, pile_list.at(ii.subtype)); @@ -332,7 +332,7 @@ static bool serialize_list_quality(color_ostream& out, FuncWriteExport add_value } const string f_type(quality_traits::key_table[i]); add_value(f_type); - DEBUG(log, out).print("adding quality %s\n", f_type.c_str()); + DEBUG(log, out).print("adding quality {}\n", f_type); } return all; } @@ -356,7 +356,7 @@ static void unserialize_list_quality(color_ostream& out, const char* subcat, boo const string quality = read_value(i); df::enum_traits::base_type idx = token_to_enum_val(quality); if (idx < 0) { - WARN(log, out).print("invalid quality token: %s\n", quality.c_str()); + WARN(log, out).print("invalid quality token: {}\n", quality); continue; } set_filter_elem(out, subcat, filters, val, quality, idx, pile_list[idx]); @@ -392,11 +392,11 @@ static bool serialize_list_other_mats(color_ostream& out, } const string token = other_mats_index(other_mats, i); if (token.empty()) { - WARN(log, out).print("invalid other material with index %zd\n", i); + WARN(log, out).print("invalid other material with index {}\n", i); continue; } add_value(token); - DEBUG(log, out).print("other mats %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("other mats {} is {}\n", i, token); } return all; } @@ -416,11 +416,11 @@ static void unserialize_list_other_mats(color_ostream& out, const char* subcat, const string token = read_value(i); size_t idx = other_mats_token(other_mats, token); if (idx < 0) { - WARN(log, out).print("invalid other mat with token %s\n", token.c_str()); + WARN(log, out).print("invalid other mat with token {}\n", token); continue; } if (idx >= num_elems) { - WARN(log, out).print("other_mats index too large! idx[%zd] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("other_mats index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); @@ -447,7 +447,7 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v DEBUG(log, out).print("food mat invalid :(\n"); continue; } - DEBUG(log, out).print("organic_material %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("organic_material {} is {}\n", i, token); add_value(token); } return all; @@ -489,7 +489,7 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, const string token = read_value(i); int16_t idx = OrganicMatLookup::food_idx_by_token(out, cat, token); if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("organic mat index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } OrganicMatLookup::FoodMat food_mat; @@ -542,7 +542,7 @@ static void unserialize_list_color(color_ostream& out, const char* subcat, bool size_t color = find_color_by_token(value); if (color == SIZE_MAX) { - WARN(log, out).print("unknown color %s", value.c_str()); + WARN(log, out).print("unknown color {}\n", value); continue; } set_filter_elem(out, subcat, filters, val, value, color, pile_list[color]); @@ -556,7 +556,7 @@ static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allo bool all = true; size_t num_item_types = list.size(); - DEBUG(log, out).print("item_type size = %zd size limit = %d typecasted: %zd\n", + DEBUG(log, out).print("item_type size = {} size limit = {} typecasted: {}\n", num_item_types, type_traits::last_item_value, (size_t)type_traits::last_item_value); for (size_t i = 0; i <= (size_t)type_traits::last_item_value; ++i) { @@ -569,7 +569,7 @@ static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allo if (!is_allowed(type)) continue; add_value(r_type); - DEBUG(log, out).print("item_type key_table[%zd] type[%d] is %s\n", i + 1, (int16_t)type, r_type.c_str()); + DEBUG(log, out).print("item_type key_table[{}] type[{}] is {}\n", i + 1, (int16_t)type, r_type); } return all; } @@ -599,7 +599,7 @@ static void unserialize_list_item_type(color_ostream& out, const char* subcat, b if (!is_allowed((item_type)idx)) continue; if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("error item_type index too large! idx[%d] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("error item_type index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); @@ -618,7 +618,7 @@ static bool serialize_list_material(color_ostream& out, FuncMaterialAllowed is_a mi.decode(0, i); if (!is_allowed(mi)) continue; - DEBUG(log, out).print("adding material %s\n", mi.getToken().c_str()); + DEBUG(log, out).print("adding material {}\n", mi.getToken()); add_value(mi.getToken()); } return all; @@ -652,7 +652,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo if (!mi.find(id) || !is_allowed(mi)) continue; if (mi.index < 0 || size_t(mi.index) >= pile_list.size()) { - WARN(log, out).print("material type index invalid: %d\n", mi.index); + WARN(log, out).print("material type index invalid: {}\n", mi.index); continue; } set_filter_elem(out, subcat, filters, val, id, mi.index, pile_list.at(mi.index)); @@ -671,7 +671,7 @@ static bool serialize_list_creature(color_ostream& out, FuncWriteExport add_valu if (r->flags.is_set(creature_raw_flags::GENERATED) || r->creature_id == "EQUIPMENT_WAGON") continue; - DEBUG(log, out).print("adding creature %s\n", r->creature_id.c_str()); + DEBUG(log, out).print("adding creature {}\n", r->creature_id); add_value(r->creature_id); } return all; @@ -701,7 +701,7 @@ static void unserialize_list_creature(color_ostream& out, const char* subcat, bo string id = read_value(i); int idx = find_creature(id); if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("animal index invalid: %d\n", idx); + WARN(log, out).print("animal index invalid: {}\n", idx); continue; } auto r = find_creature(idx); @@ -721,7 +721,7 @@ static void write_cat(color_ostream& out, const char* name, bool include_types, T_cat_set* cat_set = mutable_cat_fn(); if (!include_types) { - DEBUG(log, out).print("including all for %s since only category is being recorded\n", name); + DEBUG(log, out).print("including all for {} since only category is being recorded\n", name); cat_set->set_all(true); return; } @@ -729,7 +729,7 @@ static void write_cat(color_ostream& out, const char* name, bool include_types, if (write_cat_fn(out, cat_set)) { // all fields were set. clear them and use the "all" flag instead so "all" can be applied // to other worlds with other generated types - DEBUG(log, out).print("including all for %s since all fields were enabled\n", name); + DEBUG(log, out).print("including all for {} since all fields were enabled\n", name); cat_set->Clear(); cat_set->set_all(true); } @@ -742,7 +742,7 @@ void StockpileSettingsSerializer::write(color_ostream& out, uint32_t includedEle if (!(includedElements & INCLUDED_ELEMENTS_CATEGORIES)) return; - DEBUG(log, out).print("GROUP SET %s\n", + DEBUG(log, out).print("GROUP SET {}\n", bitfield_to_string(mSettings->flags).c_str()); bool include_types = 0 != (includedElements & INCLUDED_ELEMENTS_TYPES); @@ -893,7 +893,7 @@ static void read_elem(color_ostream& out, const char* name, DeserializeMode mode bool is_set = elem_fn() != 0; if (mode == DESERIALIZE_MODE_SET || is_set) { T_elem val = (mode == DESERIALIZE_MODE_DISABLE) ? 0 : elem_fn(); - DEBUG(log, out).print("setting %s to %d\n", name, val); + DEBUG(log, out).print("setting {} to {}\n", name, val); setting = val; } } @@ -907,7 +907,7 @@ static void read_category(color_ostream& out, const char* name, DeserializeMode std::function clear_fn, std::function set_fn) { if (mode == DESERIALIZE_MODE_SET) { - DEBUG(log, out).print("clearing %s\n", name); + DEBUG(log, out).print("clearing {}\n", name); cat_flags &= ~cat_mask; clear_fn(); } @@ -923,7 +923,7 @@ static void read_category(color_ostream& out, const char* name, DeserializeMode bool all = cat_fn().all(); char val = (mode == DESERIALIZE_MODE_DISABLE) ? (char)0 : (char)1; - DEBUG(log, out).print("setting %s %s elements to %d\n", + DEBUG(log, out).print("setting {} {} elements to {}\n", all ? "all" : "marked", name, val); set_fn(all, val); } @@ -970,7 +970,7 @@ void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) if (!mBuffer.has_use_links_only()) return; bool use_links_only = mBuffer.use_links_only(); - DEBUG(log, out).print("setting use_links_only to %d\n", use_links_only); + DEBUG(log, out).print("setting use_links_only to {}\n", use_links_only); mPile->stockpile_flag.bits.use_links_only = use_links_only; } @@ -991,7 +991,7 @@ bool StockpileSettingsSerializer::write_ammo(color_ostream& out, StockpileSettin mSettings->ammo.mats) && all; if (mSettings->ammo.other_mats.size() > 2) { - WARN(log, out).print("ammo other materials > 2: %zd\n", + WARN(log, out).print("ammo other materials > 2: {}\n", mSettings->ammo.other_mats.size()); } @@ -1004,7 +1004,7 @@ bool StockpileSettingsSerializer::write_ammo(color_ostream& out, StockpileSettin } const string token = i == 0 ? "WOOD" : "BONE"; ammo->add_other_mats(token); - DEBUG(log, out).print("other mats %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("other mats {} is {}\n", i, token); } all = serialize_list_quality(out, @@ -1843,7 +1843,7 @@ bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileS } string f_type{ENUM_KEY_STR(furniture_type, furniture_type(i))}; furniture->add_type(f_type); - DEBUG(log, out).print("furniture_type %zd is %s\n", i, f_type.c_str()); + DEBUG(log, out).print("furniture_type {} is {}\n", i, f_type); } all = serialize_list_material(out, @@ -1896,7 +1896,7 @@ void StockpileSettingsSerializer::read_furniture(color_ostream& out, Deserialize const string token = bfurniture.type(i); df::enum_traits::base_type idx = token_to_enum_val(token); if (idx < 0 || size_t(idx) >= pfurniture.type.size()) { - WARN(log, out).print("furniture type index invalid %s, idx=%d\n", token.c_str(), idx); + WARN(log, out).print("furniture type index invalid {}, idx={}\n", token, idx); continue; } set_filter_elem(out, "type", filters, val, token, idx, pfurniture.type.at(idx)); @@ -1956,7 +1956,7 @@ bool StockpileSettingsSerializer::write_gems(color_ostream& out, StockpileSettin mi.decode(i, -1); if (!gem_other_mat_is_allowed(mi)) continue; - DEBUG(log, out).print("gem rough_other mat %zd is %s\n", i, mi.getToken().c_str()); + DEBUG(log, out).print("gem rough_other mat {} is {}\n", i, mi.getToken()); gems->add_rough_other_mats(mi.getToken()); } @@ -1970,7 +1970,7 @@ bool StockpileSettingsSerializer::write_gems(color_ostream& out, StockpileSettin mi.decode(0, i); if (!gem_other_mat_is_allowed(mi)) continue; - DEBUG(log, out).print("gem cut_other mat %zd is %s\n", i, mi.getToken().c_str()); + DEBUG(log, out).print("gem cut_other mat {} is {}\n", i, mi.getToken()); gems->add_cut_other_mats(mi.getToken()); } @@ -2384,7 +2384,7 @@ bool StockpileSettingsSerializer::write_wood(color_ostream& out, StockpileSettin if (!wood_mat_is_allowed(plant)) continue; wood->add_mats(plant->id); - DEBUG(log, out).print("plant %zd is %s\n", i, plant->id.c_str()); + DEBUG(log, out).print("plant {} is {}\n", i, plant->id); } return all; } @@ -2415,7 +2415,7 @@ void StockpileSettingsSerializer::read_wood(color_ostream& out, DeserializeMode const string token = bwood.mats(i); const size_t idx = find_plant(token); if (idx < 0 || (size_t)idx >= num_elems) { - WARN(log, out).print("wood mat index invalid %s idx=%zd\n", token.c_str(), idx); + WARN(log, out).print("wood mat index invalid {}, idx={}\n", token, idx); continue; } set_filter_elem(out, "", filters, val, token, idx, pwood.mats.at(idx)); diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index 33ce855b42b..7165449ccaf 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -29,7 +29,7 @@ DBG_DECLARE(stockpiles, log, DebugCategory::LINFO); static command_result do_command(color_ostream& out, vector& parameters); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { - DEBUG(log, out).print("initializing %s\n", plugin_name); + DEBUG(log, out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -62,7 +62,7 @@ static df::building_stockpilest* get_stockpile(int id) { static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t includedElements) { df::building_stockpilest* sp = get_stockpile(id); if (!sp) { - out.printerr("Specified building isn't a stockpile: %d.\n", id); + out.printerr("Specified building isn't a stockpile: {}\n", id); return false; } @@ -72,12 +72,12 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t try { StockpileSerializer cereal(sp); if (!cereal.serialize_to_file(out, fname, includedElements)) { - out.printerr("could not save to '%s'\n", fname.c_str()); + out.printerr("could not save to '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("serialization failed: protobuf exception: %s\n", e.what()); + out.printerr("serialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -87,7 +87,7 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t static bool stockpiles_import(color_ostream& out, string fname, int id, string mode_str, string filter) { df::building_stockpilest* sp = get_stockpile(id); if (!sp) { - out.printerr("Specified building isn't a stockpile: %d.\n", id); + out.printerr("Specified building isn't a stockpile: {}\n", id); return false; } @@ -95,7 +95,7 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m fname += ".dfstock"; if (!Filesystem::exists(fname)) { - out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + out.printerr("ERROR: file doesn't exist: '{}'\n", fname); return false; } @@ -111,12 +111,12 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m try { StockpileSerializer cereal(sp); if (!cereal.unserialize_from_file(out, fname, mode, filters)) { - out.printerr("deserialization failed: '%s'\n", fname.c_str()); + out.printerr("deserialization failed: '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + out.printerr("deserialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -126,13 +126,13 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m static df::stockpile_settings * get_stop_settings(color_ostream& out, int route_id, int stop_id) { auto route = df::hauling_route::find(route_id); if (!route) { - out.printerr("Specified hauling route not found: %d.\n", route_id); + out.printerr("Specified hauling route not found: {}\n", route_id); return NULL; } df::hauling_stop *stop = binsearch_in_vector(route->stops, &df::hauling_stop::id, stop_id); if (!stop) { - out.printerr("Specified hauling stop not found in route %d: %d.\n", route_id, stop_id); + out.printerr("Specified hauling stop not found in route {}: {}.\n", route_id, stop_id); return NULL; } @@ -150,12 +150,12 @@ static bool stockpiles_route_export(color_ostream& out, string fname, int route_ try { StockpileSettingsSerializer cereal(settings); if (!cereal.serialize_to_file(out, fname, includedElements)) { - out.printerr("could not save to '%s'\n", fname.c_str()); + out.printerr("could not save to '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("serialization failed: protobuf exception: %s\n", e.what()); + out.printerr("serialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -171,7 +171,7 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_ fname += ".dfstock"; if (!Filesystem::exists(fname)) { - out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + out.printerr("ERROR: file doesn't exist: '{}'\n", fname); return false; } @@ -187,12 +187,12 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_ try { StockpileSettingsSerializer cereal(settings); if (!cereal.unserialize_from_file(out, fname, mode, filters)) { - out.printerr("deserialization failed: '%s'\n", fname.c_str()); + out.printerr("deserialization failed: '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + out.printerr("deserialization failed: protobuf exception: {}\n", e.what()); return false; } diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 3bdd9431df5..2ab221195df 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -149,7 +149,7 @@ int getCreatedMetalBars (int32_t idx) command_result df_strangemood (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -202,7 +202,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) type = mood_type::Macabre; else { - out.printerr("Mood type '%s' not recognized!\n", parameters[i].c_str()); + out.printerr("Mood type '{}' not recognized!\n", parameters[i]); return CR_WRONG_USAGE; } } @@ -260,13 +260,13 @@ command_result df_strangemood (color_ostream &out, vector & parameters) skill = job_skill::MECHANICS; else { - out.printerr("Mood skill '%s' not recognized!\n", parameters[i].c_str()); + out.printerr("Mood skill '{}' not recognized!\n", parameters[i]); return CR_WRONG_USAGE; } } else { - out.printerr("Unrecognized parameter: %s\n", parameters[i].c_str()); + out.printerr("Unrecognized parameter: {}\n", parameters[i]); return CR_WRONG_USAGE; } } @@ -428,7 +428,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) if (unit->job.current_job) { // TODO: cancel job - out.printerr("Chosen unit '%s' has active job, cannot start mood!\n", + out.printerr("Chosen unit '{}' has active job, cannot start mood!\n", DF2CONSOLE(Units::getReadableName(unit)).c_str()); return CR_FAILURE; } diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index 0756e108321..13a37d1901f 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -471,7 +471,7 @@ class SuspendManager { static bool riskBlocking(color_ostream &out, df::job* job) { if (job->job_type != job_type::ConstructBuilding) return false; - TRACE(cycle,out).print("risk blocking: check construction job %d\n", job->id); + TRACE(cycle,out).print("risk blocking: check construction job {}\n", job->id); auto building = Job::getHolder(job); if (!building || !isImpassable(building)) @@ -484,7 +484,7 @@ class SuspendManager { return false; auto risk = riskOfStuckConstructionAt(pos); - TRACE(cycle,out).print(" risk is %d\n", risk); + TRACE(cycle,out).print(" risk is {}\n", risk); // TOTHINK: on a large grid, this will compute the same risk up to 5 times for (auto npos : neighbors | transform(around(pos))) { @@ -504,7 +504,7 @@ class SuspendManager { if (!building || building->getType() != df::building_type::Construction) return false; - TRACE(cycle,out).print("check (construction) construction job %d for support\n", job->id); + TRACE(cycle,out).print("check (construction) construction job {} for support\n", job->id); coord pos(building->centerx,building->centery,building->z); @@ -647,7 +647,7 @@ class SuspendManager { void refresh(color_ostream &out) { - DEBUG(cycle,out).print("starting refresh, prevent blocking is %s\n", + DEBUG(cycle,out).print("starting refresh, prevent blocking is {}\n", prevent_blocking ? "true" : "false"); suspensions.clear(); leadsToDeadend.clear(); @@ -693,7 +693,7 @@ class SuspendManager { if (building && buildingOnDesignation(building)) suspensions[job->id]=Reason::ERASE_DESIGNATION; } - DEBUG(cycle,out).print("finished refresh: found %zu reasons for suspension\n",suspensions.size()); + DEBUG(cycle,out).print("finished refresh: found {} reasons for suspension\n",suspensions.size()); } void do_cycle (color_ostream &out, bool unsuspend_everything = false) @@ -720,7 +720,7 @@ class SuspendManager { } } } - DEBUG(cycle,out).print("suspended %zu constructions and unsuspended %zu constructions\n", + DEBUG(cycle,out).print("suspended {} constructions and unsuspended {} constructions\n", num_suspend, num_unsuspend); } @@ -772,7 +772,7 @@ static void do_cycle(color_ostream &out); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); suspendmanager_instance = std::make_unique(); eventhandler_instance = std::make_unique(plugin_self,jobCompletedHandler,1); @@ -793,13 +793,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector prevent_blocking = config.get_bool(CONFIG_PREVENT_BLOCKING); - DEBUG(control,out).print("loading persisted state: enabled is %s / prevent_blocking is %s\n", + DEBUG(control,out).print("loading persisted state: enabled is {} / prevent_blocking is {}\n", is_enabled ? "true" : "false", suspendmanager_instance->prevent_blocking ? "true" : "false"); if(is_enabled) { @@ -854,7 +854,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -870,16 +870,16 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.size() == 0) { if (!is_enabled) { - out.print("%s is disabled\n", plugin_name); + out.print("{} is disabled\n", plugin_name); } else { out.print( - "%s is enabled %s supending blocking jobs\n", plugin_name, + "{} is enabled {} suspending blocking jobs\n", plugin_name, suspendmanager_instance->prevent_blocking ? "and" : "but not"); } return CR_OK; @@ -896,7 +896,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) config.set_bool(CONFIG_PREVENT_BLOCKING, true); if (is_enabled) { do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); + out.print("{}", suspendmanager_instance->getStatus(out)); } return CR_OK; } else if (parameters[2] == "false") { @@ -904,7 +904,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) config.set_bool(CONFIG_PREVENT_BLOCKING, false); if (is_enabled) { do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); + out.print("{}", suspendmanager_instance->getStatus(out)); } return CR_OK; } else @@ -923,7 +923,7 @@ static void jobCompletedHandler(color_ostream& out, void* ptr) { TRACE(cycle,out).print("job completed/initiated handler called\n"); df::job* job = static_cast(ptr); if (SuspendManager::isConstructionJob(job)) { - DEBUG(cycle,out).print("construction job initiated/completed (tick: %d)\n", world->frame_counter); + DEBUG(cycle,out).print("construction job initiated/completed (tick: {})\n", world->frame_counter); cycle_needed = true; } @@ -938,7 +938,7 @@ static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; cycle_needed = false; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); suspendmanager_instance->do_cycle(out); } diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index c66d314431f..efd90440240 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -228,8 +228,8 @@ class Tailor { df::item_type t; int size; std::tie(t, size) = i.first; - DEBUG(cycle).print("tailor: %d %s of size %d found\n", - i.second, ENUM_KEY_STR(item_type, t).c_str(), size); + DEBUG(cycle).print("tailor: {} {} of size {} found\n", + i.second, ENUM_KEY_STR(item_type, t), size); } } } @@ -248,7 +248,7 @@ class Tailor { // only count dyed std::string d; i->getItemDescription(&d, 0); - TRACE(cycle).print("tailor: skipping undyed %s\n", DF2CONSOLE(d).c_str()); + TRACE(cycle).print("tailor: skipping undyed {}\n", DF2CONSOLE(d)); continue; } MaterialInfo mat(i); @@ -268,7 +268,7 @@ class Tailor { { std::string d; i->getItemDescription(&d, 0); - DEBUG(cycle).print("tailor: weird cloth item found: %s (%d)\n", DF2CONSOLE(d).c_str(), i->id); + DEBUG(cycle).print("tailor: weird cloth item found: {} ({})\n", DF2CONSOLE(d), i->id); } } } @@ -280,7 +280,7 @@ class Tailor { supply[M_LEATHER] += i->getStackSize(); } - DEBUG(cycle).print("tailor: available silk %d yarn %d cloth %d leather %d adamantine %d\n", + DEBUG(cycle).print("tailor: available silk {} yarn {} cloth {} leather {} adamantine {}\n", supply[M_SILK], supply[M_YARN], supply[M_CLOTH], supply[M_LEATHER], supply[M_ADAMANTINE]); } @@ -331,9 +331,9 @@ class Tailor { { if (ordered.count(ty) == 0) { - DEBUG(cycle).print ("tailor: %s (size %d) worn by %s (size %d) needs replacement\n", - DF2CONSOLE(description).c_str(), isize, - DF2CONSOLE(Units::getReadableName(u)).c_str(), usize); + DEBUG(cycle).print ("tailor: {} (size {}) worn by {} (size {}) needs replacement\n", + DF2CONSOLE(description), isize, + DF2CONSOLE(Units::getReadableName(u)), usize); needed[std::make_pair(ty, usize)] += 1; ordered.insert(ty); } @@ -346,10 +346,10 @@ class Tailor { bool confiscated = Items::setOwner(w, NULL); INFO(cycle).print( - "tailor: %s %s from %s.\n", + "tailor: {} {} from {}.\n", (confiscated ? "confiscated" : "could not confiscate"), - DF2CONSOLE(description).c_str(), - DF2CONSOLE(Units::getReadableName(u)).c_str() + DF2CONSOLE(description), + DF2CONSOLE(Units::getReadableName(u)) ); } @@ -363,10 +363,10 @@ class Tailor { { if (equipped.count(ty) == 0 && ordered.count(ty) == 0) { - TRACE(cycle).print("tailor: one %s of size %d needed to cover %s\n", - ENUM_KEY_STR(item_type, ty).c_str(), + TRACE(cycle).print("tailor: one {} of size {} needed to cover {}\n", + ENUM_KEY_STR(item_type, ty), usize, - DF2CONSOLE(Units::getReadableName(u)).c_str()); + DF2CONSOLE(Units::getReadableName(u))); needed[std::make_pair(ty, usize)] += 1; } } @@ -423,7 +423,7 @@ class Tailor { { const df::job_type j = jj->second; orders[std::make_tuple(j, sub, size)] += count; - DEBUG(cycle).print("tailor: %s times %d of size %d ordered\n", ENUM_KEY_STR(job_type, j).c_str(), count, size); + DEBUG(cycle).print("tailor: {} times {} of size {} ordered\n", ENUM_KEY_STR(job_type, j), count, size); } } } @@ -441,8 +441,8 @@ class Tailor { if (o->material_category.whole == m.job_material.whole) { supply[m] -= o->amount_left; - TRACE(cycle).print("tailor: supply of %s reduced by %d due to being required for an existing order\n", - DF2CONSOLE(m.name).c_str(), o->amount_left); + TRACE(cycle).print("tailor: supply of {} reduced by {} due to being required for an existing order\n", + DF2CONSOLE(m.name), o->amount_left); } } } @@ -574,7 +574,7 @@ class Tailor { auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); if (n > 0) { - INFO(cycle).print("tailor: ordered %d %s\n", c, DF2CONSOLE(descr).c_str()); + INFO(cycle).print("tailor: ordered {} {}\n", c, DF2CONSOLE(descr)); } return n; } @@ -687,7 +687,7 @@ class Tailor { if (sizes.count(size) == 0) { - WARN(cycle).print("tailor: cannot determine race for clothing of size %d, skipped\n", + WARN(cycle).print("tailor: cannot determine race for clothing of size {}, skipped\n", size); continue; } @@ -738,11 +738,11 @@ class Tailor { if (!can_make) { - INFO(cycle).print("tailor: civilization cannot make %s, skipped\n", DF2CONSOLE(name_p).c_str()); + INFO(cycle).print("tailor: civilization cannot make {}, skipped\n", DF2CONSOLE(name_p)); continue; } - DEBUG(cycle).print("tailor: ordering %d %s\n", count, DF2CONSOLE(name_p).c_str()); + DEBUG(cycle).print("tailor: ordering {} {}\n", count, DF2CONSOLE(name_p)); for (auto& m : material_order) { @@ -757,8 +757,8 @@ class Tailor { if (supply[m] < count + res) { c = supply[m] - res; - TRACE(cycle).print("tailor: order reduced from %d to %d to protect reserves of %s\n", - count, c, DF2CONSOLE(m.name).c_str()); + TRACE(cycle).print("tailor: order reduced from {} to {} to protect reserves of {}\n", + count, c, DF2CONSOLE(m.name)); skipped += (count - c); } supply[m] -= c; @@ -767,13 +767,12 @@ class Tailor { if (n > 0) { - INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", + INFO(cycle).print("tailor: added order #{} for {} {} {}, sized for {}\n", order->id, n, - bitfield_to_string(order->material_category).c_str(), - DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), - DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() - ); + bitfield_to_string(order->material_category), + DF2CONSOLE((c > 1) ? name_p : name_s), + DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1])); count -= n; ordered += n; @@ -782,7 +781,7 @@ class Tailor { else { skipped += count; - DEBUG(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + DEBUG(cycle).print("tailor: material {} skipped due to lack of reserves, {} available\n", DF2CONSOLE(m.name), supply[m]); } } @@ -791,7 +790,7 @@ class Tailor { if (skipped > 0) { - INFO(cycle).print("tailor: %d item%s not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); + INFO(cycle).print("tailor: {} item{} not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); if (automate_dye) { @@ -799,23 +798,23 @@ class Tailor { int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); int dye_cloth_orders = count_dye_cloth_orders(); - DEBUG(cycle).print("tailor: available dyes = %d, available dyeable cloth = %d, dye cloth orders = %d\n", + DEBUG(cycle).print("tailor: available dyes = {}, available dyeable cloth = {}, dye cloth orders = {}\n", available_dyes, available_dyeable_cloth, dye_cloth_orders); int to_dye = std::min(skipped, std::min(available_dyes, available_dyeable_cloth) - dye_cloth_orders); - DEBUG(cycle).print("tailor: to dye = %d\n", to_dye); + DEBUG(cycle).print("tailor: to dye = {}\n", to_dye); if (to_dye > 0) { int dyed = order_dye_cloth(to_dye); if (dyed > 0) { - INFO(cycle).print("tailor: dyeing %d cloth\n", to_dye); + INFO(cycle).print("tailor: dyeing {} cloth\n", to_dye); } } int dyes_to_make = available_dyes - to_dye; if (dyes_to_make > 0) { - INFO(cycle).print("tailor: ordering up to %d dyes\n", dyes_to_make); + INFO(cycle).print("tailor: ordering up to {} dyes\n", dyes_to_make); make_dyes(dyes_to_make); } } @@ -843,7 +842,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static int do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); tailor_instance = std::make_unique(); @@ -858,19 +857,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector set_confiscate(config.get_bool(CONFIG_CONFISCATE)); - DEBUG(control,out).print("loading persisted confiscation state: %s\n", + DEBUG(control,out).print("loading persisted confiscation state: {}\n", tailor_instance->get_confiscate() ? "true" : "false"); tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); - DEBUG(control, out).print("loading persisted dye automation state: %s\n", + DEBUG(control, out).print("loading persisted dye automation state: {}\n", tailor_instance->get_automate_dye() ? "true" : "false"); tailor_instance->sync_material_order(); @@ -933,7 +932,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -945,14 +944,14 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { int ordered = do_cycle(out); if (0 < ordered) - out.print("tailor: ordered %d items of clothing\n", ordered); + out.print("tailor: ordered {} items of clothing\n", ordered); } return CR_OK; } static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -975,7 +974,7 @@ static int do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); return tailor_instance->do_cycle(); } @@ -986,7 +985,7 @@ static int do_cycle(color_ostream &out) { static void tailor_doCycle(color_ostream &out) { DEBUG(control,out).print("entering tailor_doCycle\n"); - out.print("ordered %d items of clothing\n", do_cycle(out)); + out.print("ordered {} items of clothing\n", do_cycle(out)); } // remember, these are ONE-based indices from Lua @@ -1008,7 +1007,7 @@ static void tailor_setMaterialPreferences(color_ostream &out, int32_t silkIdx, static void tailor_setConfiscate(color_ostream& out, bool enable) { - DEBUG(control,out).print("%s confiscation of tattered clothing \n", enable ? "enabling" : "disabling"); + DEBUG(control,out).print("{} confiscation of tattered clothing \n", enable ? "enabling" : "disabling"); config.set_bool(CONFIG_CONFISCATE, enable); tailor_instance->set_confiscate(enable); } @@ -1020,7 +1019,7 @@ static bool tailor_getConfiscate(color_ostream& out) static void tailor_setAutomateDye(color_ostream& out, bool enable) { - DEBUG(control, out).print("%s automation of dye\n", enable ? "enabling" : "disabling"); + DEBUG(control, out).print("{} automation of dye\n", enable ? "enabling" : "disabling"); config2.set_bool(CONFIG_AUTOMATE_DYE, enable); tailor_instance->set_automate_dye(enable); } diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp index 95c5bd44243..8c8407d1893 100644 --- a/plugins/tiletypes.cpp +++ b/plugins/tiletypes.cpp @@ -1156,7 +1156,7 @@ command_result executePaintJob(color_ostream &out, } if (!opts.quiet) - out.print("Cursor coords: (%d, %d, %d)\n", + out.print("Cursor coords: ({}, {}, {})\n", cursor.x, cursor.y, cursor.z); MapExtras::MapCache map; @@ -1196,9 +1196,9 @@ command_result executePaintJob(color_ostream &out, } if (failures > 0) - out.printerr("Could not update %d tiles of %zu.\n", failures, all_tiles.size()); + out.printerr("Could not update {} tiles of {}.\n", failures, all_tiles.size()); else if (!opts.quiet) - out.print("Processed %zu tiles.\n", all_tiles.size()); + out.print("Processed {} tiles.\n", all_tiles.size()); if (map.WriteAll()) { @@ -1399,57 +1399,57 @@ command_result df_tiletypes_here_point (color_ostream &out, vector & pa static bool setTile(color_ostream& out, df::coord pos, TileType target) { if (!Maps::isValidTilePos(pos)) { - out.printerr("Invalid map position: %d, %d, %d\n", pos.x, pos.y, pos.z); + out.printerr("Invalid map position: ({}, {}, {})\n", pos.x, pos.y, pos.z); return false; } if (!is_valid_enum_item(target.shape)) { - out.printerr("Invalid shape type: %d\n", target.shape); + out.printerr("Invalid shape type: {}\n", ENUM_AS_STR(target.shape)); return false; } if (!is_valid_enum_item(target.material)) { - out.printerr("Invalid material type: %d\n", target.material); + out.printerr("Invalid material type: {}\n", ENUM_AS_STR(target.material)); return false; } if (!is_valid_enum_item(target.special)) { - out.printerr("Invalid special type: %d\n", target.special); + out.printerr("Invalid special type: {}\n", ENUM_AS_STR(target.special)); return false; } if (!is_valid_enum_item(target.variant)) { - out.printerr("Invalid variant type: %d\n", target.variant); + out.printerr("Invalid variant type: {}\n", ENUM_AS_STR(target.variant)); return false; } if (target.hidden < -1 || target.hidden > 1) { - out.printerr("Invalid hidden value: %d\n", target.hidden); + out.printerr("Invalid hidden value: {}\n", target.hidden); return false; } if (target.light < -1 || target.light > 1) { - out.printerr("Invalid light value: %d\n", target.light); + out.printerr("Invalid light value: {}\n", target.light); return false; } if (target.subterranean < -1 || target.subterranean > 1) { - out.printerr("Invalid subterranean value: %d\n", target.subterranean); + out.printerr("Invalid subterranean value: {}\n", target.subterranean); return false; } if (target.skyview < -1 || target.skyview > 1) { - out.printerr("Invalid skyview value: %d\n", target.skyview); + out.printerr("Invalid skyview value: {}\n", target.skyview); return false; } if (target.aquifer < -1 || target.aquifer > 2) { - out.printerr("Invalid aquifer value: %d\n", target.aquifer); + out.printerr("Invalid aquifer value: {}\n", target.aquifer); return false; } if (target.autocorrect < 0 || target.autocorrect > 1) { - out.printerr("Invalid autocorrect value: %d\n", target.autocorrect); + out.printerr("Invalid autocorrect value: {}\n", target.autocorrect); return false; } if (target.material == df::tiletype_material::STONE) { if (target.stone_material != -1 && !isStoneInorganic(target.stone_material)) { - out.printerr("Invalid stone material: %d\n", target.stone_material); + out.printerr("Invalid stone material: {}\n", target.stone_material); return false; } if (!is_valid_enum_item(target.vein_type)) { - out.printerr("Invalid vein type: %d\n", target.vein_type); + out.printerr("Invalid vein type: {}\n", ENUM_AS_STR(target.vein_type)); return false; } } diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index a5e5b04c808..deb6c45e4df 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -78,7 +78,7 @@ static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -162,7 +162,7 @@ static void do_disable() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -170,7 +170,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) { @@ -181,7 +181,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { do_disable(); } } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -189,7 +189,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -221,7 +221,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (config.get_bool(CONFIG_IS_ENABLED)) { plugin_enable(out, true); } - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -230,7 +230,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); do_disable(); } @@ -246,7 +246,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -270,7 +270,7 @@ static void record_coverage(color_ostream &out) { if (coverage_slot >= NUM_COVERAGE_TICKS) return; if (!tick_coverage[coverage_slot]) { - DEBUG(cycle,out).print("recording coverage for slot: %u", coverage_slot); + DEBUG(cycle,out).print("recording coverage for slot: {}", coverage_slot); } tick_coverage[coverage_slot] = true; } @@ -616,7 +616,7 @@ static void adjust_items(color_ostream &out, int32_t timeskip) { } static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -647,7 +647,7 @@ static void do_cycle(color_ostream &out) { // don't let our deficit grow unbounded if we can never catch up timeskip_deficit = std::min(desired_timeskip - float(timeskip), 100.0F); - DEBUG(cycle,out).print("cur_year_tick: %d, real_fps: %d, timeskip: (%d, +%.2f)\n", *cur_year_tick, real_fps, timeskip, timeskip_deficit); + DEBUG(cycle,out).print("cur_year_tick: {}, real_fps: {}, timeskip: ({}, +{:.2f})\n", *cur_year_tick, real_fps, timeskip, timeskip_deficit); if (timeskip <= 0) return; @@ -669,7 +669,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto unit = df::unit::find(unit_id); if (!unit) return; - DEBUG(event,out).print("registering new unit %d (%s)\n", unit->id, Units::getReadableName(unit).c_str()); + DEBUG(event,out).print("registering new unit {} ({})\n", unit->id, Units::getReadableName(unit)); register_birthday(unit); } @@ -678,7 +678,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { // static void timestream_setFps(color_ostream &out, int fps) { - DEBUG(cycle,out).print("timestream_setFps: %d\n", fps); + DEBUG(cycle,out).print("timestream_setFps: {}\n", fps); config.set_int(CONFIG_TARGET_FPS, clamp_fps_to_valid(fps)); } diff --git a/plugins/tubefill.cpp b/plugins/tubefill.cpp index 46f7101fab6..cab0241bd41 100644 --- a/plugins/tubefill.cpp +++ b/plugins/tubefill.cpp @@ -116,6 +116,6 @@ command_result tubefill(color_ostream &out, std::vector & params) } } } - out.print("Found and changed %" PRId64 " tiles.\n", count); + out.print("Found and changed {} tiles.\n", count); return CR_OK; } diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index dd7edbf3e1d..0e69da9df60 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -107,14 +107,14 @@ static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vect if (disable) { hook.remove(); if (!quiet) - out.print("Disabled tweak %s (%s)\n", parameters[0].c_str(), hook.name()); + out.print("Disabled tweak {} ({})\n", parameters[0], hook.name()); } else { if (hook.apply()) { if (!quiet) - out.print("Enabled tweak %s (%s)\n", parameters[0].c_str(), hook.name()); + out.print("Enabled tweak {} ({})\n", parameters[0], hook.name()); } else - out.printerr("Could not activate tweak %s (%s)\n", parameters[0].c_str(), hook.name()); + out.printerr("Could not activate tweak {} ({})\n", parameters[0], hook.name()); } } @@ -134,13 +134,13 @@ static command_result enable_tweak(string tweak, color_ostream &out, vector ¶meters) { if (parameters.empty() || parameters[0] == "list") { out.print("tweaks:\n"); for (auto & entry : get_status()) - out.print(" %25s: %s\n", entry.first.c_str(), entry.second ? "enabled" : "disabled"); + out.print(" {:25}: {}\n", entry.first, entry.second ? "enabled" : "disabled"); return CR_OK; } diff --git a/plugins/work-now.cpp b/plugins/work-now.cpp index ca9f1ffe060..6844a23623d 100644 --- a/plugins/work-now.cpp +++ b/plugins/work-now.cpp @@ -44,13 +44,13 @@ static void cleanup() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(log,out).print("%s from the API; persisting\n", + DEBUG(log,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) @@ -58,7 +58,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { else cleanup(); } else { - DEBUG(log,out).print("%s from the API, but already %s; no action\n", + DEBUG(log,out).print("{} from the API, but already {} ; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -81,7 +81,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(log,out).print("loading persisted enabled state: %s\n", + DEBUG(log,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -98,7 +98,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan poke_idlers(); } else if (e == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(log,out).print("world unloaded; disabling %s\n", + DEBUG(log,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -109,12 +109,12 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static command_result work_now(color_ostream& out, vector& parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.empty() || parameters[0] == "status") { - out.print("work_now is %sactively poking idle dwarves.\n", is_enabled ? "" : "not "); + out.print("work_now is {}actively poking idle dwarves.\n", is_enabled ? "" : "not "); return CR_OK; } diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 2b1d91b8264..24c3a67a3d9 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -410,7 +410,7 @@ static void stop_protect(color_ostream &out) pending_recover.clear(); if (!known_jobs.empty()) - out.print("Unprotecting %zd jobs.\n", known_jobs.size()); + out.print("Unprotecting {} jobs.\n", known_jobs.size()); for (TKnownJobs::iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) delete it->second; @@ -437,7 +437,7 @@ static void start_protect(color_ostream &out) check_lost_jobs(out, 0); if (!known_jobs.empty()) - out.print("Protecting %zd jobs.\n", known_jobs.size()); + out.print("Protecting {} jobs.\n", known_jobs.size()); } static void init_state(color_ostream &out) @@ -456,7 +456,7 @@ static void init_state(color_ostream &out) if (get_constraint(out, items[i].val(), &items[i])) continue; - out.printerr("Lost constraint %s\n", items[i].val().c_str()); + out.printerr("Lost constraint {}\n", items[i].val()); World::DeletePersistentData(items[i]); } @@ -503,8 +503,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) pj->holder = df::building::find(pj->building_id); if (!pj->holder) { - out.printerr("Forgetting job %d (%s): holder building lost.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Forgetting job {} ({}): holder building lost.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); forget_job(out, pj); return true; } @@ -512,8 +512,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) // Check its state and postpone or cancel if invalid if (pj->holder->jobs.size() >= 10) { - out.printerr("Forgetting job %d (%s): holder building has too many jobs.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Forgetting job {} ({}): holder building has too many jobs.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); forget_job(out, pj); return true; } @@ -535,8 +535,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) { Job::deleteJobStruct(recovered); - out.printerr("Inconsistency: job %d (%s) already in list.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Inconsistency: job {} ({}) already in list.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); return true; } @@ -664,7 +664,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (tokens[0] == "ANY_CRAFT" || tokens[0] == "CRAFTS") { is_craft = true; } else if (!item.find(tokens[0]) || !item.isValid()) { - out.printerr("Cannot find item type: %s\n", tokens[0].c_str()); + out.printerr("Cannot find item type: {}\n", tokens[0]); return NULL; } @@ -674,7 +674,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str df::dfhack_material_category mat_mask; std::string maskstr = vector_get(tokens,1); if (!maskstr.empty() && !parseJobMaterialCategory(&mat_mask, maskstr)) { - out.printerr("Cannot decode material mask: %s\n", maskstr.c_str()); + out.printerr("Cannot decode material mask: {}\n", maskstr); return NULL; } @@ -684,7 +684,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str MaterialInfo material; std::string matstr = vector_get(tokens,2); if (!matstr.empty() && (!material.find(matstr) || !material.isValid())) { - out.printerr("Cannot find material: %s\n", matstr.c_str()); + out.printerr("Cannot find material: {}\n", matstr); return NULL; } @@ -692,7 +692,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str weight += (material.index >= 0 ? 5000 : 1000); if (mat_mask.whole && material.isValid() && !material.matches(mat_mask)) { - out.printerr("Material %s doesn't match mask %s\n", matstr.c_str(), maskstr.c_str()); + out.printerr("Material {} doesn't match mask {}\n", matstr, maskstr); return NULL; } @@ -724,7 +724,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (!found) { - out.printerr("Cannot parse token: %s\n", token.c_str()); + out.printerr("Cannot parse token: {}\n", token); return NULL; } } @@ -1184,9 +1184,9 @@ static void setJobResumed(color_ostream &out, ProtectedJob *pj, bool goal) if (goal != current) { - out.print("%s %s%s\n", + out.print("{} {}{}\n", (goal ? "Resuming" : "Suspending"), - shortJobDescription(pj->actual_job).c_str(), + shortJobDescription(pj->actual_job), (!goal || pj->isActuallyResumed() ? "" : " (delayed)")); } } @@ -1817,7 +1817,7 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (deleteConstraint(parameters[1])) return CR_OK; - out.printerr("Constraint not found: %s\n", parameters[1].c_str()); + out.printerr("Constraint not found: {}\n", parameters[1]); return CR_FAILURE; } else if (cmd == "unlimit-all") From d5518b988a93eabe5063ca75f2f6b94b14c5bc0f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 17:46:24 -0600 Subject: [PATCH 651/919] corrections whitespace; `std::format` -> `fmt::format` --- library/DataDefs.cpp | 2 +- library/Process.cpp | 2 +- library/include/ColorText.h | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 7a9ae4ae358..54cdfff982c 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -262,7 +262,7 @@ const std::string bit_container_identity::getFullName(const type_identity *) con const std::string df::buffer_container_identity::getFullName(const type_identity *item) const { return (item ? item->getFullName() : std::string("void")) + - (size > 0 ? std::format("[{}]", size) : std::string("[]")); + (size > 0 ? fmt::format("[{}]", size) : std::string("[]")); } union_identity::union_identity(size_t size, const TAllocateFn alloc, diff --git a/library/Process.cpp b/library/Process.cpp index 47d110ba920..00664c74de4 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -215,7 +215,7 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) } free(wd); #else /* WIN32 */ - cerr << "PE timestamp: " << std::format("{:#0x}", my_pe) << endl; + cerr << "PE timestamp: " << fmt::format("{:#0x}", my_pe) << endl; #endif /* WIN32 */ } } diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 3fce782ecf6..15aca0cf590 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -191,4 +191,3 @@ namespace DFHack }; } - From 31dcd41624c24fb4c6ca83b0fde276fcd893de5c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 17:54:09 -0600 Subject: [PATCH 652/919] add missing include --- library/Process.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/Process.cpp b/library/Process.cpp index 00664c74de4..2f3471d4a53 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -35,6 +35,8 @@ distribution. #include #include +#include + #ifndef WIN32 #include #include From 4fd76d64e61728d44fe1f74f2c4ab5fb85620a11 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:02:23 -0600 Subject: [PATCH 653/919] add another missing include --- library/Process.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Process.cpp b/library/Process.cpp index 2f3471d4a53..57ea6a7c0ce 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -36,6 +36,7 @@ distribution. #include #include +#include #ifndef WIN32 #include From 809b3adb75ab8a2155cddb2a547c19ee7242a7ce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:12:51 -0600 Subject: [PATCH 654/919] use `reinterpret_cast` to print function pointers c++ standard does not allow static cast of a function pointer to `void*` --- library/modules/EventManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 4a4d47e7659..fad6b776780 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -72,7 +72,7 @@ static const int32_t ticksPerYear = 403200; void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler) { DEBUG(log).print("registering handler {} from plugin {} for event {}\n", - static_cast(handler.eventHandler), + reinterpret_cast(handler.eventHandler), handler.plugin ? handler.plugin->getName() : "", static_cast(e)); handlers[e].insert(pair(handler.plugin, handler)); @@ -91,7 +91,7 @@ int32_t DFHack::EventManager::registerTick(EventHandler handler, int32_t when, b handler.freq = when; tickQueue.insert(pair(handler.freq, handler)); DEBUG(log).print("registering handler {} from plugin {} for event TICK\n", - static_cast(handler.eventHandler), + reinterpret_cast(handler.eventHandler), handler.plugin ? handler.plugin->getName() : ""); handlers[EventType::TICK].insert(pair(handler.plugin,handler)); return when; From dda124b7f08088fcafd2a75d3da64f0a243293c5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:16:58 -0600 Subject: [PATCH 655/919] missed one :( --- library/modules/EventManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index fad6b776780..3c926645a26 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -119,7 +119,7 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl continue; } DEBUG(log).print("unregistering handler {} from plugin {} for event {}\n", - static_cast(handler.eventHandler), + reinterpret_cast(handler.eventHandler), handler.plugin ? handler.plugin->getName() : "", static_cast(e)); i = handlers[e].erase(i); From 5bd91da0452bf1ceeda5b6ad7bb3f560dae8b6e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 18:32:59 -0600 Subject: [PATCH 656/919] add `fmt` to `dfhack_test` --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8fe082b7fa..caaf20c3e83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -436,7 +436,7 @@ macro(dfhack_test name files) if(BUILD_LIBRARY AND UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated add_executable(${name} ${files}) target_include_directories(${name} PUBLIC depends/googletest/googletest/include) - target_link_libraries(${name} dfhack gtest) + target_link_libraries(${name} dfhack fmt gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() From 5224d655ed5e19ebc65ba82be8150b6d2abc2df9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 22:45:20 -0600 Subject: [PATCH 657/919] pin fmtlib to 12.1.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index caaf20c3e83..2cbe4fd6866 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,7 +351,7 @@ INCLUDE(FetchContent) FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git - GIT_TAG master + GIT_TAG 12.1.0 ) FetchContent_MakeAvailable(fmt) From 41c85e77060f4e0135346a2a7accfb1fb917fc22 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 23 Nov 2025 23:45:30 -0600 Subject: [PATCH 658/919] change fmtlib pin 12.1.0 generates a warning when built with msvc also there are build time improvements after 12.1.0 was released --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2cbe4fd6866..de3e0f3132b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,7 +351,7 @@ INCLUDE(FetchContent) FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git - GIT_TAG 12.1.0 + GIT_TAG 790b9389ae99c4ddebdd2736a8602eca1fec684e # 12.1.0 + bugfix for MSVC warning + build time improvements ) FetchContent_MakeAvailable(fmt) From 47d6dd25d4f5f5558756a2ca1f61f4777ab0f1ad Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 01:22:05 -0600 Subject: [PATCH 659/919] adjustments to facilitating to std::format when it's ready, which it's not... --- CMakeLists.txt | 9 ++++++--- library/CMakeLists.txt | 11 ++++++----- library/Core.cpp | 13 +++++++------ library/Error.cpp | 2 +- library/LuaTools.cpp | 2 +- library/Process.cpp | 3 +-- library/VersionInfoFactory.cpp | 4 ++-- library/include/ColorText.h | 4 +--- library/include/DataDefs.h | 3 +-- library/include/Format.h | 15 +++++++++++++++ 10 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 library/include/Format.h diff --git a/CMakeLists.txt b/CMakeLists.txt index de3e0f3132b..528f6f0d934 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,8 +46,8 @@ option(REMOVE_SYMBOLS_FROM_DF_STUBS "Remove debug symbols from DF stubs. (Reduce macro(CHECK_GCC compiler_path) execute_process(COMMAND ${compiler_path} -dumpversion OUTPUT_VARIABLE GCC_VERSION_OUT) string(STRIP "${GCC_VERSION_OUT}" GCC_VERSION_OUT) - if(${GCC_VERSION_OUT} VERSION_LESS "10") - message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 10 or later") + if(${GCC_VERSION_OUT} VERSION_LESS "11") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 11 or later") endif() endmacro() @@ -347,6 +347,7 @@ if(BUILD_LIBRARY) endif() endif() +# this can be made conditional once we get to better platform support for std::format INCLUDE(FetchContent) FetchContent_Declare( fmt @@ -354,6 +355,8 @@ FetchContent_Declare( GIT_TAG 790b9389ae99c4ddebdd2736a8602eca1fec684e # 12.1.0 + bugfix for MSVC warning + build time improvements ) FetchContent_MakeAvailable(fmt) +set(FMTLIB fmt) +add_definitions("-DUSE_FMTLIB") if(APPLE) # libstdc++ (GCC 4.8.5 for OS X 10.6) @@ -436,7 +439,7 @@ macro(dfhack_test name files) if(BUILD_LIBRARY AND UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated add_executable(${name} ${files}) target_include_directories(${name} PUBLIC depends/googletest/googletest/include) - target_link_libraries(${name} dfhack fmt gtest) + target_link_libraries(${name} dfhack ${FMTLIB} gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 471d2f808aa..9e9babb2412 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -58,6 +58,7 @@ set(MAIN_HEADERS include/DebugManager.h include/Error.h include/Export.h + include/Format.h include/Hooks.h include/LuaTools.h include/LuaWrapper.h @@ -320,12 +321,12 @@ if(UNIX) endif() if(APPLE) - set(PROJECT_LIBS dl dfhack-md5 fmt ${DFHACK_TINYXML}) + set(PROJECT_LIBS dl dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) elseif(UNIX) - set(PROJECT_LIBS rt dl dfhack-md5 fmt ${DFHACK_TINYXML}) + set(PROJECT_LIBS rt dl dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? - set(PROJECT_LIBS psapi dbghelp dfhack-md5 fmt ${DFHACK_TINYXML}) + set(PROJECT_LIBS psapi dbghelp dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) endif() set(VERSION_SRCS DFHackVersion.cpp) @@ -391,7 +392,7 @@ else() set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "-include Export.h" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "-include Export.h" ) add_library(dfhooks_dfhack SHARED Hooks.cpp) - target_link_libraries(dfhooks_dfhack dfhack fmt) + target_link_libraries(dfhooks_dfhack dfhack ${FMTLIB}) endif() # effectively disables debug builds... @@ -420,7 +421,7 @@ endif() target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) set_target_properties(dfhack PROPERTIES INTERFACE_LINK_LIBRARIES "") -target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static fmt) +target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static ${FMTLIB}) if(WIN32) target_link_libraries(dfhack-client dbghelp) endif() diff --git a/library/Core.cpp b/library/Core.cpp index 6960c12eb9d..5e32f1b0d5d 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -41,6 +41,7 @@ distribution. #include "LuaTools.h" #include "DFHackVersion.h" #include "md5wrapper.h" +#include "Format.h" #include "modules/DFSDL.h" #include "modules/DFSteam.h" @@ -87,7 +88,6 @@ distribution. #include #include -#include #ifdef _WIN32 #define NOMINMAX @@ -525,18 +525,19 @@ std::filesystem::path Core::findScript(std::string name) { std::vector paths; getScriptPaths(&paths); - for (auto it = paths.begin(); it != paths.end(); ++it) + for (auto& path : paths) { std::error_code ec; - std::filesystem::path path = std::filesystem::weakly_canonical(*it / name, ec); + auto raw_path = path / name; + std::filesystem::path load_path = std::filesystem::weakly_canonical(raw_path, ec); if (ec) { - con.printerr("Error loading '{}' ({})\n", *it / name, ec.message()); + con.printerr("Error loading '{}' ({})\n", raw_path, ec.message()); continue; } - if (Filesystem::isfile(path)) - return path; + if (Filesystem::isfile(load_path)) + return load_path; } return {}; } diff --git a/library/Error.cpp b/library/Error.cpp index fa32daf90e3..9abb9ea7c9e 100644 --- a/library/Error.cpp +++ b/library/Error.cpp @@ -1,7 +1,7 @@ #include "Error.h" +#include "Format.h" #include "MiscUtils.h" -#include #include using namespace DFHack::Error; diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index e6083b0a4e5..69242ede5f6 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -249,7 +249,7 @@ static void signal_typeid_error(color_ostream* out, lua_State* state, int val_index, bool perr, bool signal) { std::string typestr = type ? type->getFullName() : "any pointer"; - std::string error = fmt::format(fmt::runtime(msg), typestr); + std::string error = fmt::vformat(msg, fmt::make_format_args(typestr)); //FIXME: C++26 if (signal) { diff --git a/library/Process.cpp b/library/Process.cpp index 57ea6a7c0ce..c5e28b179eb 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -35,8 +35,7 @@ distribution. #include #include -#include -#include +#include "Format.h" #ifndef WIN32 #include diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index 6b2c1e62f6c..b4f6eefc782 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -209,7 +209,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fmt::print(stderr, "{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + std::cerr << fmt::format("{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +217,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fmt::print(stderr, "{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); + std::cerr << fmt::format("{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 15aca0cf590..330a2d50bed 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -24,6 +24,7 @@ distribution. #pragma once #include "Export.h" +#include "Format.h" #include #include @@ -33,9 +34,6 @@ distribution. #include #include -#include -#include - namespace dfproto { class CoreTextNotification; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 182148d6354..08d8d020975 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -35,8 +35,7 @@ distribution. #include "BitArray.h" #include "Export.h" - -#include +#include "Format.h" struct lua_State; diff --git a/library/include/Format.h b/library/include/Format.h new file mode 100644 index 00000000000..52d4e0e3c6a --- /dev/null +++ b/library/include/Format.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef USE_FMTLIB + +#include +#include +#include + +#else + +#include + +namespace fmt = std; + +#endif From b1fd0b98b8915ff28c50e962da71b7ae31d51a2d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 24 Nov 2025 01:42:37 -0600 Subject: [PATCH 660/919] missed a file on last commit --- plugins/Plugins.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 107f9a80682..82439f69a5b 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -124,7 +124,7 @@ macro(dfhack_plugin) target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") target_link_libraries(${PLUGIN_NAME} protobuf-lite) endif() - target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version fmt ${PLUGIN_LINK_LIBRARIES}) + target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${FMTLIB} ${PLUGIN_LINK_LIBRARIES}) if(UNIX) set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") From 59a262ba7bbb64669ea7b7d97b7586d2460d609e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 25 Nov 2025 15:16:41 -0600 Subject: [PATCH 661/919] Add Commands.cpp (from #5659) --- library/Commands.cpp | 581 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 library/Commands.cpp diff --git a/library/Commands.cpp b/library/Commands.cpp new file mode 100644 index 00000000000..b7eb0e424d1 --- /dev/null +++ b/library/Commands.cpp @@ -0,0 +1,581 @@ + +#include "Commands.h" + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "RemoteTools.h" + +#include "modules/Gui.h" +#include "modules/World.h" + +#include "df/viewscreen_new_regionst.h" + +#include +#include +#include + + +namespace DFHack +{ + command_result Commands::help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!parts.size()) + { + if (con.is_console()) + { + con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" + "Some basic editing capabilities are included (single-line text editing).\n" + "The console also has a command history - you can navigate it with Up and Down keys.\n" + "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" + "by clicking on the program icon in the top bar of the window.\n\n"); + } + con.print("Here are some basic commands to get you started:\n" + " help|?|man - This text.\n" + " help - Usage help for the given plugin, command, or script.\n" + " tags - List the tags that the DFHack tools are grouped by.\n" + " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" + " Optional parameters:\n" + " --notags: skip printing tags for each command.\n" + " --dev: include commands intended for developers and modders.\n" + " cls|clear - Clear the console.\n" + " fpause - Force DF to pause.\n" + " die - Force DF to close immediately, without saving.\n" + " keybinding - Modify bindings of commands to in-game key shortcuts.\n" + "\n" + "See more commands by running 'ls'.\n\n" + ); + + con.print("DFHack version {}\n", dfhack_version_desc()); + } + else + { + DFHack::help_helper(con, parts[0]); + } + return CR_OK; + } + + command_result Commands::load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool all = false; + bool load = (first == "load"); + bool unload = (first == "unload"); + bool reload = (first == "reload"); + auto plug_mgr = core.getPluginManager(); + if (parts.size()) + { + for (const auto& p : parts) + { + if (p.size() && p[0] == '-') + { + if (p.find('a') != std::string::npos) + all = true; + } + } + auto ret = CR_OK; + if (all) + { + if (load && !plug_mgr->loadAll()) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unloadAll()) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reloadAll()) + ret = CR_FAILURE; + } + else + { + for (auto& p : parts) + { + if (p.empty() || p[0] == '-') + continue; + if (load && !plug_mgr->load(p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(p)) + ret = CR_FAILURE; + } + } + if (ret != CR_OK) + con.printerr("{} failed\n", first.c_str()); + return ret; + } + else + { + con.printerr("{}: no arguments\n", first.c_str()); + return CR_FAILURE; + } + + } + + command_result Commands::enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + CoreSuspender suspend; + bool enable = (first == "enable"); + auto plug_mgr = core.getPluginManager(); + + if (parts.size()) + { + command_result res{CR_FAILURE}; + + for (auto& part_ : parts) + { + // have to copy to modify as passed argument is const + std::string part(part_); + + if (has_backslashes(part)) + { + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", part); + replace_backslashes_with_forwardslashes(part); + } + + auto alias = core.GetAliasCommand(part, true); + + Plugin* plug = (*plug_mgr)[alias]; + + if (!plug) + { + std::filesystem::path lua = core.findScript(part + ".lua"); + if (!lua.empty()) + { + res = core.enableLuaScript(con, part, enable); + } + else + { + res = CR_NOT_FOUND; + con.printerr("No such plugin or Lua script: {}\n", part); + } + } + else if (!plug->can_set_enabled()) + { + res = CR_NOT_IMPLEMENTED; + con.printerr("Cannot {} plugin: {}\n", first, part); + } + else + { + res = plug->set_enabled(con, enable); + + if (res != CR_OK || plug->is_enabled() != enable) + con.printerr("Could not {} plugin: {}\n", first, part); + } + } + + return res; + } + else + { + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + if (!plug->can_be_enabled()) continue; + + con.print( + "{:>21} {:<3}{}\n", + (key + ":").c_str(), + plug->is_enabled() ? "on" : "off", + plug->can_set_enabled() ? "" : " (controlled internally)" + ); + } + + Lua::CallLuaModuleFunction(con, "script-manager", "list"); + + return CR_OK; + } + } + + command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + constexpr auto header_format = "{:30} {:10} {:4} {:8}\n"; + constexpr auto row_format = header_format; + + con.print(header_format, "Name", "State", "Cmds", "Enabled"); + + auto plug_mgr = core.getPluginManager(); + + plug_mgr->refresh(); + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + + if (parts.size() && std::ranges::find(parts, key) == parts.end()) + continue; + + color_value color; + switch (plug->getState()) + { + case Plugin::PS_LOADED: + color = COLOR_RESET; + break; + case Plugin::PS_UNLOADED: + case Plugin::PS_UNLOADING: + color = COLOR_YELLOW; + break; + case Plugin::PS_LOADING: + color = COLOR_LIGHTBLUE; + break; + case Plugin::PS_BROKEN: + color = COLOR_LIGHTRED; + break; + default: + color = COLOR_LIGHTMAGENTA; + break; + } + con.color(color); + con.print(row_format, + plug->getName(), + Plugin::getStateDescription(plug->getState()), + plug->size(), + (plug->can_be_enabled() + ? (plug->is_enabled() ? "enabled" : "disabled") + : "n/a") + ); + con.color(COLOR_RESET); + } + + return CR_OK; + } + + command_result Commands::type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + auto plug_mgr = core.getPluginManager(); + + if (!parts.size()) + { + con.printerr("type: no argument\n"); + return CR_WRONG_USAGE; + } + con << parts[0]; + bool builtin = is_builtin(con, parts[0]); + std::filesystem::path lua_path = core.findScript(parts[0] + ".lua"); + Plugin* plug = plug_mgr->getPluginByCommand(parts[0]); + if (builtin) + { + con << " is a built-in command"; + con << std::endl; + } + else if (core.IsAlias(parts[0])) + { + con << " is an alias: " << core.GetAliasCommand(parts[0]) << std::endl; + } + else if (plug) + { + con << " is a command implemented by the plugin " << plug->getName() << std::endl; + } + else if (!lua_path.empty()) + { + con << " is a Lua script: " << lua_path << std::endl; + } + else + { + con << " is not a recognized command." << std::endl; + plug = plug_mgr->getPluginByName(parts[0]); + if (plug) + con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) + { + std::string keystr = parts[1]; + if (parts[0] == "set") + core.ClearKeyBindings(keystr); + // for (int i = parts.size()-1; i >= 2; i--) + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) + { + if (!core.AddKeyBinding(keystr, part)) + { + con.printerr("Invalid key spec: {}\n", keystr); + return CR_FAILURE; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") + { + // for (size_t i = 1; i < parts.size(); i++) + for (const auto& part : parts | std::views::drop(1)) + { + if (!core.ClearKeyBindings(part)) + { + con.printerr("Invalid key spec: {}\n", part); + return CR_FAILURE; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") + { + std::vector list = core.ListKeyBindings(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else + { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } + + return CR_OK; + } + + command_result Commands::alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) + { + const std::string& name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); + if (!core.AddAlias(name, cmd, parts[0] == "replace")) + { + con.printerr("Could not add alias {} - already exists\n", name); + return CR_FAILURE; + } + } + else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) + { + if (!core.RemoveAlias(parts[1])) + { + con.printerr("Could not remove alias {}\n", parts[1]); + return CR_FAILURE; + } + } + else if (parts.size() >= 1 && (parts[0] == "list")) + { + auto aliases = core.ListAliases(); + for (auto p : aliases) + { + con << p.first << ": " << join_strings(" ", p.second) << std::endl; + } + } + else + { + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; + } + + return CR_OK; + } + + command_result Commands::fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (auto scr = Gui::getViewscreenByType()) + { + if (scr->doing_mods || scr->doing_simple_params || scr->doing_params) + { + con.printerr("Cannot pause now.\n"); + return CR_FAILURE; + } + scr->abort_world_gen_dialogue = true; + } + else + { + World::SetPauseState(true); + } + con.print("The game was forced to pause!\n"); + return CR_OK; + } + + command_result Commands::clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (con.can_clear()) + { + con.clear(); + return CR_OK; + } + else + { + con.printerr("No console to clear, or this console does not support clearing.\n"); + return CR_NEEDS_CONSOLE; + } + } + + command_result Commands::kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool force = std::ranges::any_of(parts, [] (const std::string& part) { return part == "force"; }); + if (!Lua::Interrupt(force)) + { + con.printerr( + "Failed to register hook. This can happen if you have" + " lua profiling or coverage monitoring enabled. Use" + " 'kill-lua force' to force, but this may disable" + " profiling and coverage monitoring.\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + core.loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); + return CR_OK; + } + else + { + con << "Usage:" << std::endl + << " script " << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().show()) + { + con.printerr("Could not show console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().hide()) + { + con.printerr("Could not hide console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.empty() || parts[0] == "help" || parts[0] == "?") + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; + for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) + { + std::string name = sc_event_name((state_change_event)i); + if (name != "SC_UNKNOWN") + con << " " << name << std::endl; + } + return CR_OK; + } + else if (parts[0] == "list") + { + std::string event_name = parts.size() >= 2 ? parts[1] : ""; + if (event_name.size() && sc_event_id(event_name) == SC_UNKNOWN) + { + con << "Unrecognized event name: " << parts[1] << std::endl; + return CR_WRONG_USAGE; + } + for (const auto& state_script : core.getStateChangeScripts()) + { + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) + { + con.print("{} ({}): {}{}\n", sc_event_name(state_script.event), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path); + } + } + return CR_OK; + } + else if (parts[0] == "add") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript script(evt, parts[2], save_specific); + for (const auto& state_script : core.getStateChangeScripts()) + { + if (script == state_script) + { + con << "Script already registered" << std::endl; + return CR_FAILURE; + } + } + core.addStateChangeScript(script); + return CR_OK; + } + else if (parts[0] == "remove") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript tmp(evt, parts[2], save_specific); + if (core.removeStateChangeScript(tmp)) + { + return CR_OK; + } + else + { + con << "Unrecognized script" << std::endl; + return CR_FAILURE; + } + } + else + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + std::ofstream file(parts[0]); + CoreService coreSvc; + coreSvc.dumpMethods(file); + + for (auto& it : *core.getPluginManager()) + { + Plugin* plug = it.second; + if (!plug) + continue; + + std::unique_ptr svc(plug->rpc_connect(con)); + if (!svc) + continue; + + file << "// Plugin: " << plug->getName() << std::endl; + svc->dumpMethods(file); + } + } + else + { + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; + return CR_WRONG_USAGE; + } + return CR_OK; + } +} From c6914c227cdd94f880415d2d70c704206dfdfabf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 25 Nov 2025 15:32:24 -0600 Subject: [PATCH 662/919] Include information about `FetchContent` in offline build section --- docs/dev/compile/Compile.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/dev/compile/Compile.rst b/docs/dev/compile/Compile.rst index 3eea95d880f..2ad2c52efb4 100644 --- a/docs/dev/compile/Compile.rst +++ b/docs/dev/compile/Compile.rst @@ -442,3 +442,14 @@ a command starting with ``cmake .. -G Ninja`` on Linux and macOS, following the instructions in the sections above. CMake should automatically locate files that you placed in ``CMake/downloads``, and use them instead of attempting to download them. + +In addition, some packages used by DFHack are managed using CMake's ``FetchContent`` +feature, which requires an online connection during builds. The simplest way to address +this is to have a connection during the first build (during which CMake will download the +dependencies), and then to use CMake's ``FETCHCONTENT_FULLY_DISCONNECTED`` or +``FETCHCONTENT_UPDATES_DISCONNECTED`` defines to control how CMake manages cached +dependencies. If you need even the first-time build be an offline build, you will need +to provide a CMake dependency provider. We do not provide one, but CMake's own documentation +includes a simple provider. For more information about CMake's ``FetchContent`` feature +and how to use it in offline builds, see the +`CMake documentation `_. From 0f3726c74f2f577274254b5ae6fb4a6e4d21a2fe Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 26 Nov 2025 07:20:43 +0000 Subject: [PATCH 663/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 24322fe4a30..76b912392d4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 24322fe4a30209399c1c5fe903726828edd9032b +Subproject commit 76b912392d461138726bb0c0d82fcb47a5638cd3 From fc1b58899e0592e873d97a71982b76254c3584fd Mon Sep 17 00:00:00 2001 From: Optical Disc <88067218+opticaldisc@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:36:50 -0800 Subject: [PATCH 664/919] Add more detail about --@ lines in the Lua API doc. Includes a block of examples showing what will work and what will not. --- docs/dev/Lua API.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index a4a039f4097..cc7a90bb3f1 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -7468,6 +7468,15 @@ Importing scripts --@ module = true + In order to be recognized, this line **must** begin with ``--@`` with no + whitespace characters before it:: + + --@ module = true OK + --@module = true OK + -- @module = true NOT OK (no --@ found due to space after --) + --@module = true NOT OK (leading space, --@ is not at the beginning of the line) + ---@module = true NOT OK (leading dash, --@ is not at the beginning of the line) + 2. Include a check for ``dfhack_flags.module``, and avoid running any code that has side-effects if this flag is true. For instance:: From 60cd9b713f2f7980c8ce7133003019b11b6e7248 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 14:47:39 -0500 Subject: [PATCH 665/919] Add new plugin edgescroll to automate panning of game/region maps --- docs/changelog.txt | 1 + docs/plugins/edgescroll.rst | 13 +++ plugins/CMakeLists.txt | 1 + plugins/edgescroll.cpp | 197 ++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 docs/plugins/edgescroll.rst create mode 100644 plugins/edgescroll.cpp diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a4..45cbc951372 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. ## New Features diff --git a/docs/plugins/edgescroll.rst b/docs/plugins/edgescroll.rst new file mode 100644 index 00000000000..4522ff36bfc --- /dev/null +++ b/docs/plugins/edgescroll.rst @@ -0,0 +1,13 @@ +edgescroll +========== + +.. dfhack-tool:: + :summary: Scroll the game world and region maps when the mouse reaches the window border. + :tags: interface + +Usage +----- + +:: + + enable edgescroll diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a49fdf7a9fe..0ac4fe9b57f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -75,6 +75,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) + dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp new file mode 100644 index 00000000000..7c25287c82f --- /dev/null +++ b/plugins/edgescroll.cpp @@ -0,0 +1,197 @@ +#include "ColorText.h" +#include "PluginManager.h" +#include "MemAccess.h" + +#include "df/world_generatorst.h" +#include "modules/Gui.h" + +#include "df/enabler.h" +#include "df/gamest.h" +#include "df/graphic.h" +#include "df/graphic_viewportst.h" +#include "df/renderer_2d.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/viewscreen_worldst.h" +#include "df/viewscreen_new_regionst.h" +#include "df/world.h" +#include "df/world_data.h" + +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("edgescroll"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(enabler); +REQUIRE_GLOBAL(game); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(gps); + +// Cooldown between edge scroll actions +constexpr uint32_t cooldown_ms = 100; +// Number of pixels from border to trigger edgescroll +constexpr int border_range = 5; + +// Controls how much edge scroll moves +constexpr int map_scroll_pixels = 100; +constexpr int world_scroll_tiles = 3; +constexpr int world_scroll_tiles_zoomed = 6; + +DFhackCExport command_result plugin_init([[maybe_unused]]color_ostream &out, [[maybe_unused]] std::vector &commands) { + return CR_OK; +} + +DFhackCExport command_result plugin_enable([[maybe_unused]]color_ostream &out, bool enable) { + is_enabled = enable; + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out) { + return CR_OK; +} + +template +static void apply_scroll(T* out, T diff, T min, T max) { + *out = std::min(std::max(*out + diff, min), max); +} + +static void scroll_dwarfmode(int xdiff, int ydiff) { + using df::global::window_x; + using df::global::window_y; + using df::global::game; + // Scale the movement by pixels, to keep scroll speeds visually consistent + int tilesize = gps->viewport_zoom_factor / 4; + int width = gps->main_viewport->dim_x; + int height = gps->main_viewport->dim_y; + + // Ensure the map doesn't go fully off-screen + int min_x = -width / 2; + int min_y = -height / 2; + int max_x = world->map.x_count - (width / 2); + int max_y = world->map.y_count - (height / 2); + apply_scroll(window_x, xdiff * std::max(1, map_scroll_pixels / tilesize), min_x, max_x); + apply_scroll(window_y, ydiff * std::max(1, map_scroll_pixels / tilesize), min_y, max_y); + + // Force a minimap update + game->minimap.update = 1; + game->minimap.mustmake = 1; +} + +template +static void scroll_world(T* screen, int xdiff, int ydiff) { + if constexpr(std::is_same_v) { + if (screen->zoomed_in) { + int max_x = (world->world_data->world_width * 16)-1; + int max_y = (world->world_data->world_height * 16)-1; + apply_scroll(&screen->zoom_cent_x, xdiff * world_scroll_tiles_zoomed, 0, max_x); + apply_scroll(&screen->zoom_cent_y, ydiff * world_scroll_tiles_zoomed, 0, max_y); + return; + } + } + + int32_t *x, *y; + if constexpr(std::is_same_v) { + x = &world->worldgen_status.cursor_x; + y = &world->worldgen_status.cursor_y; + } else { + x = &screen->region_cent_x; + y = &screen->region_cent_y; + } + int max_x = world->world_data->world_width-1; + int max_y = world->world_data->world_height-1; + apply_scroll(x, xdiff * world_scroll_tiles, 0, max_x); + apply_scroll(y, ydiff * world_scroll_tiles, 0, max_y); +} + +template +struct overloads : Ts... { using Ts::operator()...; }; + +using world_map = std::variant; +static std::optional get_map() { + df::viewscreen* screen = Gui::getCurViewscreen(true); + screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if (auto start_site = virtual_cast(screen)) + return start_site; + if (auto world_map = virtual_cast(screen)) + return world_map; + if (auto worldgen_map = virtual_cast(screen)) { + if (!world || world->worldgen_status.state <= df::world_generatorst::Initializing) + return {}; // Map isn't displayed yet + return worldgen_map; + } + return {}; +} + +static void scroll_world(world_map screen, int xdiff, int ydiff) { + const auto visitor = overloads { + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world(s, xdiff, ydiff);}, + }; + std::visit(visitor, screen); +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; + + // FIXME: Once dfhooks_sdl_loop is hooked up in Core, use SDL_GetMouseState + // to determine the correct mouse position without forcing the screen to + // render slightly un-centered. + // origin_x/y are already zero if not fitting the interface to the grid + + // Force the origin_x/y values to zero to workaround df marking any + // mouse position within the margin register as invalid + auto renderer = virtual_cast(enabler->renderer); + if (renderer && (renderer->origin_x != 0 || renderer->origin_y != 0)) { + renderer->origin_x = 0; + renderer->origin_y = 0; + } + + auto dim_x = gps->screen_pixel_x; + auto dim_y = gps->screen_pixel_y; + auto x = gps->precise_mouse_x; + auto y = gps->precise_mouse_y; + if (x == -1 || y == -1) + return CR_OK; // Invalid mouse position + + // Apply a cooldown to any potential edgescrolls + auto& core = Core::getInstance(); + static uint32_t last_action = 0; + uint32_t now = core.p->getTickCount(); + if (now < last_action + cooldown_ms) + return CR_OK; + + + int xdiff = 0; + int ydiff = 0; + if (x <= border_range) { + xdiff--; + } else if (x >= dim_x - border_range) { + xdiff++; + } + if (y <= border_range) { + ydiff--; + } else if (y >= dim_y - border_range) { + ydiff++; + } + + if (xdiff == 0 && ydiff == 0) + return CR_OK; // No work to do + + // Dispatch scrolling to active scrollables + if (worldmap.has_value()) + scroll_world(worldmap.value(), xdiff, ydiff); + else if (gps->main_viewport->flag.bits.active) + scroll_dwarfmode(xdiff, ydiff); + + // Update cooldown + last_action = now; + + return CR_OK; +} From 04da99d53970be81bdcc6ec434eb8facd6be67fb Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 21:27:23 -0500 Subject: [PATCH 666/919] Setup dfhooks_sdl_loop and use it to read exact mouse position for edgescroll --- library/Core.cpp | 4 ++ library/Hooks.cpp | 1 + library/include/Core.h | 2 + library/include/modules/DFSDL.h | 10 +++ library/modules/DFSDL.cpp | 36 +++++++++++ plugins/edgescroll.cpp | 108 ++++++++++++++++++++------------ 6 files changed, 120 insertions(+), 41 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6e..3be676644eb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2508,6 +2508,10 @@ bool Core::DFH_SDL_Event(SDL_Event* ev) { return ret; } +void Core::DFH_SDL_Loop() { + DFHack::runRenderThreadCallbacks(); +} + bool Core::doSdlInputEvent(SDL_Event* ev) { // this should only ever be called from the render thread diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 951472eba66..0f957fea063 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -68,6 +68,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged + DFHack::Core::getInstance().DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef12..9791464c28d 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -151,6 +151,7 @@ namespace DFHack friend void ::dfhooks_update(); friend void ::dfhooks_prerender(); friend bool ::dfhooks_sdl_event(SDL_Event* event); + friend void ::dfhooks_sdl_loop(); friend bool ::dfhooks_ncurses_key(int key); public: /// Get the single Core instance or make one. @@ -238,6 +239,7 @@ namespace DFHack int Update (void); int Shutdown (void); bool DFH_SDL_Event(SDL_Event* event); + void DFH_SDL_Loop(); bool ncurses_wgetch(int in, int & out); bool DFH_ncurses_key(int key); diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index ff8e81ab2c8..3d371d3ea5a 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -3,10 +3,13 @@ #include "Export.h" #include "ColorText.h" +#include +#include #include struct SDL_Surface; struct SDL_Rect; +struct SDL_Renderer; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; @@ -55,6 +58,10 @@ namespace DFHack::DFSDL DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char* title, const char* message, SDL_Window* window); + DFHACK_EXPORT uint32_t DFSDL_GetMouseState(int* x, int* y); + DFHACK_EXPORT void DFSDL_RenderWindowToLogical(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); + DFHACK_EXPORT void DFSDL_RenderLogicalToWindow(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); + // submitted and returned text is UTF-8 // see wrapper functions below for cp-437 variants DFHACK_EXPORT char* DFSDL_GetClipboardText(); @@ -76,4 +83,7 @@ namespace DFHack DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // Queue a cb to be run on the render thread, with optional userdata + DFHACK_EXPORT void runOnRenderThread(std::function cb, void* userdata); + DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9da2bd80570..666844f6cb7 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -8,6 +8,8 @@ #include +#include + #ifdef WIN32 # include #endif @@ -61,6 +63,9 @@ SDL_Surface* (*g_SDL_CreateRGBSurfaceWithFormat)(uint32_t flags, int width, int int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char *message, SDL_Window *window) = nullptr; char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; char* (*g_SDL_GetBasePath)() = nullptr; +uint32_t (*g_SDL_GetMouseState)(int* x, int* y) = nullptr; +void (*g_SDL_RenderWindowToLogical)(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); +void (*g_SDL_RenderLogicalToWindow)(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -106,6 +111,9 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetMouseState); + bind(g_sdl_handle, SDL_RenderWindowToLogical); + bind(g_sdl_handle, SDL_RenderLogicalToWindow); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -190,6 +198,18 @@ char* DFSDL::DFSDL_GetBasePath() return g_SDL_GetBasePath(); } +uint32_t DFSDL::DFSDL_GetMouseState(int* x, int* y) { + return g_SDL_GetMouseState(x, y); +} + +void DFSDL::DFSDL_RenderWindowToLogical(SDL_Renderer *renderer, int windowX, int windowY, float *logicalX, float *logicalY) { + g_SDL_RenderWindowToLogical(renderer, windowX, windowY, logicalX, logicalY); +} + +void DFSDL::DFSDL_RenderLogicalToWindow(SDL_Renderer *renderer, float logicalX, float logicalY, int *windowX, int *windowY) { + g_SDL_RenderLogicalToWindow(renderer, logicalX, logicalY, windowX, windowY); +} + int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window) { if (!g_SDL_ShowSimpleMessageBox) return -1; @@ -266,3 +286,19 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { } return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } + +static std::recursive_mutex render_cb_lock; +static std::vector, void*>> render_cb_queue; + +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { + std::lock_guard l(render_cb_lock); + render_cb_queue.push_back({cb, userdata}); +} + +DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { + std::lock_guard l(render_cb_lock); + for (auto& cb : render_cb_queue) { + std::get<0>(cb)(std::get<1>(cb)); + } + render_cb_queue.clear(); +} diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 7c25287c82f..2a26797281f 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -4,6 +4,7 @@ #include "df/world_generatorst.h" #include "modules/Gui.h" +#include "modules/DFSDL.h" #include "df/enabler.h" #include "df/gamest.h" @@ -52,6 +53,62 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out return CR_OK; } +static std::atomic_bool request_queued = false; + +struct scroll_state { + int8_t xdiff; + int8_t ydiff; +}; + +static const scroll_state state_default(0, 0); + +static scroll_state state = state_default; +static scroll_state queued = state_default; + +static void render_thread_cb([[maybe_unused]] void* _) { + queued = state_default; + // Ignore the mouse if outside the window + if (!enabler->mouse_focus) { + request_queued.store(false); + return; + } + + // Determine window border location in window coordinates + auto* renderer = virtual_cast(enabler->renderer); + int origin_x, origin_y = 0; + int end_x, end_y; + DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->origin_x, renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->cur_w - renderer->origin_x, renderer->cur_h - renderer->origin_y, &end_x, &end_y); + + int mx, my; + DFSDL::DFSDL_GetMouseState(&mx, &my); + + if (mx <= origin_x + border_range) { + queued.xdiff--; + } else if (mx >= end_x - border_range) { + queued.xdiff++; + } + if (my <= origin_y + border_range) { + queued.ydiff--; + } else if (my >= end_y - border_range) { + queued.ydiff++; + } + + request_queued.store(false); +} + +static bool update_mouse_pos() { + if (request_queued.load()) + return false; // No new inputs, and a request for more is already placed + + state = queued; + queued = state_default; + DFHack::runOnRenderThread(render_thread_cb, nullptr); + request_queued.store(true); + return true; +} + +// Scrolling behavior template static void apply_scroll(T* out, T diff, T min, T max) { *out = std::min(std::max(*out + diff, min), max); @@ -134,32 +191,6 @@ static void scroll_world(world_map screen, int xdiff, int ydiff) { } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - - // Ensure either a map viewscreen or the main viewport are visible - auto worldmap = get_map(); - if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) - return CR_OK; - - // FIXME: Once dfhooks_sdl_loop is hooked up in Core, use SDL_GetMouseState - // to determine the correct mouse position without forcing the screen to - // render slightly un-centered. - // origin_x/y are already zero if not fitting the interface to the grid - - // Force the origin_x/y values to zero to workaround df marking any - // mouse position within the margin register as invalid - auto renderer = virtual_cast(enabler->renderer); - if (renderer && (renderer->origin_x != 0 || renderer->origin_y != 0)) { - renderer->origin_x = 0; - renderer->origin_y = 0; - } - - auto dim_x = gps->screen_pixel_x; - auto dim_y = gps->screen_pixel_y; - auto x = gps->precise_mouse_x; - auto y = gps->precise_mouse_y; - if (x == -1 || y == -1) - return CR_OK; // Invalid mouse position - // Apply a cooldown to any potential edgescrolls auto& core = Core::getInstance(); static uint32_t last_action = 0; @@ -167,28 +198,23 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (now < last_action + cooldown_ms) return CR_OK; + // Update mouse_x/y to values from the render thread + if (!update_mouse_pos()) + return CR_OK; - int xdiff = 0; - int ydiff = 0; - if (x <= border_range) { - xdiff--; - } else if (x >= dim_x - border_range) { - xdiff++; - } - if (y <= border_range) { - ydiff--; - } else if (y >= dim_y - border_range) { - ydiff++; - } + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; - if (xdiff == 0 && ydiff == 0) + if (state.xdiff == 0 && state.ydiff == 0) return CR_OK; // No work to do // Dispatch scrolling to active scrollables if (worldmap.has_value()) - scroll_world(worldmap.value(), xdiff, ydiff); + scroll_world(worldmap.value(), state.xdiff, state.ydiff); else if (gps->main_viewport->flag.bits.active) - scroll_dwarfmode(xdiff, ydiff); + scroll_dwarfmode(state.xdiff, state.ydiff); // Update cooldown last_action = now; From bfd9bc4a46a27ddda326292dfa679722c734f5c3 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 23:52:02 -0500 Subject: [PATCH 667/919] Release render_cb_lock earlier --- library/modules/DFSDL.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 666844f6cb7..2a67351d14c 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -287,18 +287,24 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } +// Queue to run callbacks on the render thread. +// Semantics loosely based on SDL3's SDL_RunOnMainThread static std::recursive_mutex render_cb_lock; static std::vector, void*>> render_cb_queue; DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { std::lock_guard l(render_cb_lock); - render_cb_queue.push_back({cb, userdata}); + render_cb_queue.push_back({std::move(cb), userdata}); } DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { - std::lock_guard l(render_cb_lock); - for (auto& cb : render_cb_queue) { + static decltype(render_cb_queue) local_queue; + { + std::lock_guard l(render_cb_lock); + std::swap(local_queue, render_cb_queue); + } + for (auto& cb : local_queue) { std::get<0>(cb)(std::get<1>(cb)); } - render_cb_queue.clear(); + local_queue.clear(); } From 8f9688b439b4a5a7cd636aa979763feb699933ab Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 28 Nov 2025 22:54:07 -0500 Subject: [PATCH 668/919] Code cleanup and implement review suggestions --- library/include/modules/DFSDL.h | 4 +- library/modules/DFSDL.cpp | 8 ++-- plugins/edgescroll.cpp | 71 +++++++++++++++++++-------------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 3d371d3ea5a..be40eff2c54 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -83,7 +83,7 @@ namespace DFHack DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); - // Queue a cb to be run on the render thread, with optional userdata - DFHACK_EXPORT void runOnRenderThread(std::function cb, void* userdata); + // Queue a cb to be run on the render thread + DFHACK_EXPORT void runOnRenderThread(std::function cb); DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 2a67351d14c..9543f813870 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -290,11 +290,11 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { // Queue to run callbacks on the render thread. // Semantics loosely based on SDL3's SDL_RunOnMainThread static std::recursive_mutex render_cb_lock; -static std::vector, void*>> render_cb_queue; +static std::vector> render_cb_queue; -DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb) { std::lock_guard l(render_cb_lock); - render_cb_queue.push_back({std::move(cb), userdata}); + render_cb_queue.push_back(std::move(cb)); } DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { @@ -304,7 +304,7 @@ DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { std::swap(local_queue, render_cb_queue); } for (auto& cb : local_queue) { - std::get<0>(cb)(std::get<1>(cb)); + cb(); } local_queue.clear(); } diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 2a26797281f..08a50430f88 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -1,8 +1,7 @@ #include "ColorText.h" -#include "PluginManager.h" #include "MemAccess.h" +#include "PluginManager.h" -#include "df/world_generatorst.h" #include "modules/Gui.h" #include "modules/DFSDL.h" @@ -16,6 +15,7 @@ #include "df/viewscreen_new_regionst.h" #include "df/world.h" #include "df/world_data.h" +#include "df/world_generatorst.h" #include #include @@ -53,33 +53,37 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out return CR_OK; } -static std::atomic_bool request_queued = false; +static std::atomic_bool callback_queued = false; struct scroll_state { int8_t xdiff; int8_t ydiff; }; -static const scroll_state state_default(0, 0); +static scroll_state state; +static scroll_state queued; -static scroll_state state = state_default; -static scroll_state queued = state_default; - -static void render_thread_cb([[maybe_unused]] void* _) { - queued = state_default; +static void render_thread_cb() { + queued = {0}; // Ignore the mouse if outside the window if (!enabler->mouse_focus) { - request_queued.store(false); + callback_queued.store(false); return; } - // Determine window border location in window coordinates + // Calculate the render rect in window coordinates auto* renderer = virtual_cast(enabler->renderer); - int origin_x, origin_y = 0; + int origin_x, origin_y; int end_x, end_y; - DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->origin_x, renderer->origin_y, &origin_x, &origin_y); - DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->cur_w - renderer->origin_x, renderer->cur_h - renderer->origin_y, &end_x, &end_y); - + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, (float)renderer->origin_x, + (float)renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, + (float)renderer->cur_w - (float)renderer->origin_x, + (float)(renderer->cur_h - renderer->origin_y), &end_x, &end_y); + + // Get the mouse location in window coordinates int mx, my; DFSDL::DFSDL_GetMouseState(&mx, &my); @@ -94,26 +98,28 @@ static void render_thread_cb([[maybe_unused]] void* _) { queued.ydiff++; } - request_queued.store(false); + callback_queued.store(false); } static bool update_mouse_pos() { - if (request_queued.load()) - return false; // No new inputs, and a request for more is already placed + if (callback_queued.load()) + return false; // Queued callback not complete, check back later + // Queued callback complete, save the results and enqueue again state = queued; - queued = state_default; - DFHack::runOnRenderThread(render_thread_cb, nullptr); - request_queued.store(true); + queued = {0}; + DFHack::runOnRenderThread(render_thread_cb); + callback_queued.store(true); return true; } -// Scrolling behavior +// Apply scroll whilst maintaining boundaries template static void apply_scroll(T* out, T diff, T min, T max) { *out = std::min(std::max(*out + diff, min), max); } +// Scroll main fortress/adventure world views static void scroll_dwarfmode(int xdiff, int ydiff) { using df::global::window_x; using df::global::window_y; @@ -137,7 +143,7 @@ static void scroll_dwarfmode(int xdiff, int ydiff) { } template -static void scroll_world(T* screen, int xdiff, int ydiff) { +static void scroll_world_internal(T* screen, int xdiff, int ydiff) { if constexpr(std::is_same_v) { if (screen->zoomed_in) { int max_x = (world->world_data->world_width * 16)-1; @@ -169,6 +175,9 @@ using world_map = std::variant get_map() { df::viewscreen* screen = Gui::getCurViewscreen(true); screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if(!screen) + return {}; + if (auto start_site = virtual_cast(screen)) return start_site; if (auto world_map = virtual_cast(screen)) @@ -183,9 +192,9 @@ static std::optional get_map() { static void scroll_world(world_map screen, int xdiff, int ydiff) { const auto visitor = overloads { - [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world(s, xdiff, ydiff);}, - [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world(s, xdiff, ydiff);}, - [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world_internal(s, xdiff, ydiff);}, }; std::visit(visitor, screen); } @@ -198,18 +207,18 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (now < last_action + cooldown_ms) return CR_OK; - // Update mouse_x/y to values from the render thread + // Update mouse_x/y from values read in render thread callback if (!update_mouse_pos()) - return CR_OK; + return CR_OK; // No new input to process + + if (state.xdiff == 0 && state.ydiff == 0) + return CR_OK; // No work to do // Ensure either a map viewscreen or the main viewport are visible auto worldmap = get_map(); if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) return CR_OK; - if (state.xdiff == 0 && state.ydiff == 0) - return CR_OK; // No work to do - // Dispatch scrolling to active scrollables if (worldmap.has_value()) scroll_world(worldmap.value(), state.xdiff, state.ydiff); From 7e6f7f9c801a5e2b84e6ba28a3fb2e0a0a48d30c Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 29 Nov 2025 10:15:49 -0800 Subject: [PATCH 669/919] library/lua/dfhack.lua interactive interpreter banner: list shortcuts When the `lua` interactive interpreter is started, the help banner now lists the shortcuts (that is, variable names with special handling) already implemented in `library/lua/utils.lua` `df_shortcut_var()`. These keywords allow the interpreter to use the currently-selected object in the DF UI. They are currently undocumented, as far as I can tell. (The `lua` interpreter documentation does contain two hints in the Examples.) --- library/lua/dfhack.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 15910b46f1b..24fb10e2267 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -855,6 +855,8 @@ function dfhack.interpreter(prompt,hfile,env) " '^ foo' => 'printall_recurse(foo)'\n".. " '@ foo' => 'printall_ipairs(foo)'\n".. "All of these save the first result as '_'.") + print("These keywords refer to the currently-selected object in the game:") + print(" unit item plant building bld job workshop_job wsjob screen scr") print_banner = false end From bcb22b54de666f814b5587a7df4c3e03d025905f Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 29 Nov 2025 17:20:06 -0800 Subject: [PATCH 670/919] update changelog (Is there a better way to word this?) --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a4..8cacfc9c569 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## API ## Lua +- The ``Lua interactive interpreter`` banner now documents keywords such as ``unit`` and ``item`` which reference the currently-selected object in the DF UI. ## Removed From d6eaea10e438026bdc72a9b1ead7cc3b0bc58621 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 29 Nov 2025 18:41:45 -0500 Subject: [PATCH 671/919] Ensure the dfhack signal handler doesn't appear in crashlogs --- library/Crashlog.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index c560666f99b..681e4fd4c5a 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,4 +1,5 @@ #include "DFHackVersion.h" + #include #include #include @@ -42,7 +43,8 @@ void signal_crashlog_complete() { std::thread crashlog_thread; -extern "C" void dfhack_crashlog_handle_signal(int sig) { +// Force this method to be inlined so that it doesn't create a stack frame +[[gnu::always_inline]] inline void handle_signal_internal(int sig) { if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { // Ensure the signal handler doesn't try to write a crashlog // whilst the crashlog thread is unavailable. @@ -61,8 +63,12 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { std::quick_exit(1); } +extern "C" void dfhack_crashlog_handle_signal(int sig) { + handle_signal_internal(sig); +} + void dfhack_crashlog_handle_terminate() { - dfhack_crashlog_handle_signal(0); + handle_signal_internal(0); } std::string signal_name(int sig) { @@ -122,8 +128,9 @@ void dfhack_save_crashlog() { crashlog << "Signal " << signal << "\n"; } - for (int i = 0; i < crash_info.backtrace_entries; i++) { - crashlog << i << "> " << backtrace_strings[i] << "\n"; + // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) + for (int i = 1; i < crash_info.backtrace_entries; i++) { + crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; } } catch (...) {} From 0bce84e60caff072285f268122d2fe1a6e68b2b7 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 30 Nov 2025 09:47:50 -0800 Subject: [PATCH 672/919] library/lua/dfhack.lua interactive interpreter banner: highlight keywords When the interactive interpreter is started, highlight the various commands and keywords in yellow. This includes banner and the 'Type quit to exit interactive lua interpreter.' message. --- library/lua/dfhack.lua | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 24fb10e2267..0dadc4fa9c6 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -845,18 +845,26 @@ function dfhack.interpreter(prompt,hfile,env) return nil, 'not interactive' end - print("Type quit to exit interactive lua interpreter.") + local function print_keyword(pre, keyword, post) + dfhack.color(COLOR_RESET) + if pre ~= nil then dfhack.print(pre) end + dfhack.color(COLOR_YELLOW) + dfhack.print(keyword) + dfhack.color(COLOR_RESET) + if post ~= nil then print(post) end + end + print_keyword("Type ", "quit", " to exit interactive lua interpreter.") if print_banner then - print("Shortcuts:\n".. - " '= foo' => '_1,_2,... = foo'\n".. - " '! foo' => 'print(foo)'\n".. - " '~ foo' => 'printall(foo)'\n".. - " '^ foo' => 'printall_recurse(foo)'\n".. - " '@ foo' => 'printall_ipairs(foo)'\n".. - "All of these save the first result as '_'.") + print("Shortcuts:") + print_keyword(" '", "= foo", "' => '_1,_2,... = foo'") + print_keyword(" '", "! foo", "' => 'print(foo)'") + print_keyword(" '", "~ foo", "' => 'printall(foo)'") + print_keyword(" '", "^ foo", "' => 'printall_recurse(foo)'") + print_keyword(" '", "@ foo", "' => 'printall_ipairs(foo)'") + print_keyword("All of these save the first result as '", "_", "'.") print("These keywords refer to the currently-selected object in the game:") - print(" unit item plant building bld job workshop_job wsjob screen scr") + print_keyword(" ", "unit item plant building bld job workshop_job wsjob screen scr", "") print_banner = false end From fe7b67fe02134ae24a23e10f027d048541011b4f Mon Sep 17 00:00:00 2001 From: Quietust Date: Sun, 30 Nov 2025 14:19:32 -0600 Subject: [PATCH 673/919] createitem - add a few more building targets (table, nest, bookcase, display furniture, and offering place) --- docs/changelog.txt | 1 + plugins/createitem.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a4..53e2ace9acb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars ## Documentation diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index fba821b2703..7f8ef6e2ced 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -280,6 +280,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { } switch (building->getType()) { using namespace df::enums::building_type; + case Table: case Coffin: case Furnace: case TradeDepot: @@ -294,8 +295,12 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { case AnimalTrap: case Cage: case Wagon: + case Nest: case NestBox: case Hive: + case Bookcase: + case DisplayFurniture: + case OfferingPlace: break; default: out.printerr("The selected building cannot be used for item storage!\n"); From 124aa4590b348ca2e9d1429a9e2bb790e23c2b2e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:13:07 +0000 Subject: [PATCH 674/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.34.1 → 0.35.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.1...0.35.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70277354adb..cc38b7e9abe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.1 + rev: 0.35.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 71a1a32972a92fd97e7487cf40f18f07d15a43e6 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 20:55:00 +0100 Subject: [PATCH 675/919] orders: add search overlay for manager orders Adds search overlay to find and navigate manager orders with arrow indicators showing current search result. Search uses Alt+S to focus, Alt+P/N for prev/next navigation. Overlays are disabled by default. --- docs/changelog.txt | 1 + plugins/lua/orders.lua | 398 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 398 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a4..54e33d712db 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators ## Fixes diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 2774bd80ee0..c99e24fd213 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -6,6 +6,11 @@ local overlay = require('plugins.overlay') local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') +local stockflow = reqscript('internal/quickfort/stockflow') + +-- Shared state for search cursor visibility +local search_cursor_visible = false +local search_last_scroll_position = -1 -- -- OrdersOverlay @@ -74,7 +79,7 @@ local mi = df.global.game.main_interface OrdersOverlay = defclass(OrdersOverlay, overlay.OverlayWidget) OrdersOverlay.ATTRS{ desc='Adds import, export, and other functions to the manager orders screen.', - default_pos={x=53,y=-6}, + default_pos={x=41,y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=43, h=4}, @@ -709,11 +714,401 @@ function QuantityRightClickOverlay:onInput(keys) end end +-- +-- OrdersSearchOverlay +-- + +local search_cursor_visible = false +local search_last_scroll_position = -1 + +local function make_order_key(order) + local mat_cat_str = '' + if order.material_category then + local keys = {} + for k in pairs(order.material_category) do + if type(k) == 'string' then + table.insert(keys, k) + end + end + table.sort(keys) + for _, k in ipairs(keys) do + mat_cat_str = mat_cat_str .. k .. '=' .. tostring(order.material_category[k]) .. ';' + end + end + + local encrust_str = '' + if order.specflag and order.specflag.encrust_flags then + local flags = {'finished_goods', 'furniture', 'ammo'} + for _, flag in ipairs(flags) do + if order.specflag.encrust_flags[flag] then + encrust_str = encrust_str .. flag .. ';' + end + end + end + + return string.format('%d:%d:%d:%d:%d:%s:%s:%s', + order.job_type, + order.item_type, + order.item_subtype, + order.mat_type, + order.mat_index, + order.reaction_name or '', + mat_cat_str, + encrust_str) +end + +local function build_reaction_map() + local map = {} + local reactions = stockflow.collect_reactions() + + for _, reaction in ipairs(reactions) do + local key = make_order_key(reaction.order) + map[key] = reaction.name:lower() + end + + return map +end + +local reaction_map_cache = nil + +local function get_cached_reaction_map() + if not reaction_map_cache then + reaction_map_cache = build_reaction_map() + end + return reaction_map_cache +end + +local function get_order_search_key(order) + local reaction_map = get_cached_reaction_map() + local key = make_order_key(order) + if reaction_map[key] then + return reaction_map[key] + end + return "" +end + +local function matches_all_search_words(search_key, filter_text) + local search_words = {} + for word in filter_text:gmatch('%S+') do + table.insert(search_words, word) + end + + -- Check if all search words are found in search_key (order-independent) + for _, search_word in ipairs(search_words) do + if not search_key:find(search_word, 1, true) then + return false + end + end + return true +end + +OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) +OrdersSearchOverlay.ATTRS{ + desc='Adds a search box to find and navigate to matching manager orders.', + default_pos={x=85, y=-6}, + default_enabled=false, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + frame={w=34, h=4}, +} + +function OrdersSearchOverlay:init() + local main_panel = widgets.Panel{ + view_id='main_panel', + frame={t=0, l=0, r=0, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + frame_title='Search', + visible=function() return not self.minimized end, + subviews={ + widgets.EditField{ + view_id='filter', + frame={t=0, l=0}, + key='CUSTOM_ALT_S', + on_change=self:callback('update_filter'), + }, + widgets.HotkeyLabel{ + view_id='prev_match', + frame={t=1, l=0}, + label='prev', + key='CUSTOM_ALT_P', + auto_width=true, + on_activate=self:callback('jump_to_previous_match'), + enabled=function() return self:has_matches() end, + }, + widgets.HotkeyLabel{ + view_id='next_match', + frame={t=1, l=17}, + label='next', + key='CUSTOM_ALT_N', + auto_width=true, + on_activate=self:callback('jump_to_next_match'), + enabled=function() return self:has_matches() end, + }, + }, + } + + local minimized_panel = widgets.Panel{ + frame={t=0, r=0, w=3, h=1}, + subviews={ + widgets.Label{ + frame={t=0, l=0, w=1, h=1}, + text='[', + text_pen=COLOR_RED, + visible=function() return self.minimized end, + }, + widgets.Label{ + frame={t=0, l=1, w=1, h=1}, + text={{text=function() return self.minimized and string.char(31) or string.char(30) end}}, + text_pen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_GREY}, + text_hpen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE}, + on_click=function() self.minimized = not self.minimized end, + }, + widgets.Label{ + frame={t=0, r=0, w=1, h=1}, + text=']', + text_pen=COLOR_RED, + visible=function() return self.minimized end, + }, + }, + } + + self:addviews{ + main_panel, + minimized_panel, + } + + -- Initialize search state + self.filter_text = '' + self.matched_indices = {} + self.current_match_idx = 0 + self.minimized = false +end + +function OrdersSearchOverlay:update_filter(text) + self.filter_text = text:lower() + self.matched_indices = {} + self.current_match_idx = 0 + + if self.filter_text == '' then + self.subviews.main_panel.frame_title = 'Search' + return + end + + local orders = df.global.world.manager_orders.all + for i = 0, #orders - 1 do + local order = orders[i] + local search_key = get_order_search_key(order) + if matches_all_search_words(search_key, self.filter_text) then + table.insert(self.matched_indices, i) + end + end + + self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +end + +function OrdersSearchOverlay:jump_to_next_match() + if #self.matched_indices == 0 then return end + + self.current_match_idx = self.current_match_idx + 1 + if self.current_match_idx > #self.matched_indices then + self.current_match_idx = 1 + end + + local order_idx = self.matched_indices[self.current_match_idx] + mi.info.work_orders.scroll_position_work_orders = order_idx + search_last_scroll_position = order_idx + search_cursor_visible = true + + self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +end + +function OrdersSearchOverlay:jump_to_previous_match() + if #self.matched_indices == 0 then return end + + self.current_match_idx = self.current_match_idx - 1 + if self.current_match_idx < 1 then + self.current_match_idx = #self.matched_indices + end + + local order_idx = self.matched_indices[self.current_match_idx] + mi.info.work_orders.scroll_position_work_orders = order_idx + search_last_scroll_position = order_idx + search_cursor_visible = true + + self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +end + +function OrdersSearchOverlay:get_match_text() + if self.filter_text == '' then + return '' + end + + local total_matches = #self.matched_indices + + if self.current_match_idx == 0 then + return string.format('%d matches', total_matches) + end + + return string.format('%d of %d', self.current_match_idx, total_matches) +end + +function OrdersSearchOverlay:has_matches() + return #self.matched_indices > 0 +end + +local function is_mouse_key(keys) + return keys._MOUSE_L + or keys._MOUSE_R + or keys._MOUSE_M + or keys.CONTEXT_SCROLL_UP + or keys.CONTEXT_SCROLL_DOWN + or keys.CONTEXT_SCROLL_PAGEUP + or keys.CONTEXT_SCROLL_PAGEDOWN +end + +function OrdersSearchOverlay:onInput(keys) + local filter_field = self.subviews.filter + if not filter_field then return false end + + -- Unfocus search on right-click + if keys._MOUSE_R and filter_field.focus then + filter_field:setFocus(false) + return true + end + + -- Let parent handle input first (for HotkeyLabel clicks and widget interactions) + if OrdersSearchOverlay.super.onInput(self, keys) then + return true + end + + -- Unfocus search on left-click when focused (for workshop and number of times changes) + -- And let the click pass through + if keys._MOUSE_L and filter_field.focus then + filter_field:setFocus(false) + return false + end + + -- Only consume input if search field has focus and it's not a mouse key + -- This allows scrolling, navigation, and mouse interaction in the orders list + if filter_field.focus and not is_mouse_key(keys) then + return true + end + + return false +end + +-- ------------------- +-- OrderHighlightOverlay +-- ------------------- + +OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) +OrderHighlightOverlay.ATTRS{ + desc='Shows arrows next to the work order found by orders.search', + default_enabled=false, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + frame={w=80, h=3}, +} + +function OrderHighlightOverlay:init() + self.ORDER_HEIGHT = 3 + self.TABS_WIDTH_THRESHOLD = 155 + self.LIST_START_Y_ONE_TABS_ROW = 8 + self.LIST_START_Y_TWO_TABS_ROWS = 10 + self.BOTTOM_MARGIN = 9 + self.ARROW_X_FIRST = 5 + self.ARROW_X_SECOND = 6 + self.ARROW_CHAR = '>' + + self.cached_list_start_y = nil + self.cached_viewport_size = nil + self.cached_screen_width = nil + self.cached_screen_height = nil +end + +function OrderHighlightOverlay:getListStartY() + local rect = gui.get_interface_rect() + + if rect.width >= self.TABS_WIDTH_THRESHOLD then + return self.LIST_START_Y_ONE_TABS_ROW + else + return self.LIST_START_Y_TWO_TABS_ROWS + end +end + +function OrderHighlightOverlay:getViewportSize() + local rect = gui.get_interface_rect() + local list_start_y = self:getListStartY() + + local available_height = rect.height - list_start_y - self.BOTTOM_MARGIN + return math.floor(available_height / self.ORDER_HEIGHT) +end + +function OrderHighlightOverlay:calculateSelectedOrderY() + local orders = df.global.world.manager_orders.all + local scroll_pos = mi.info.work_orders.scroll_position_work_orders + + if #orders == 0 or scroll_pos < 0 or scroll_pos >= #orders then + return nil + end + + local list_start_y = self:getListStartY() + local viewport_size = self:getViewportSize() + + local viewport_start = scroll_pos + local viewport_end = scroll_pos + viewport_size - 1 + + -- Selected order tries to be at the top unless we're at the end of the list + if viewport_end >= #orders then + viewport_end = #orders - 1 + viewport_start = math.max(0, viewport_end - viewport_size + 1) + end + + local pos_in_viewport = scroll_pos - viewport_start + + local selected_y = list_start_y + (pos_in_viewport * self.ORDER_HEIGHT) + + return selected_y +end + +function OrderHighlightOverlay:render(dc) + if search_cursor_visible then + local current_scroll = mi.info.work_orders.scroll_position_work_orders + + -- Hide cursor when user manually scrolls + if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then + search_cursor_visible = false + end + search_last_scroll_position = current_scroll + end + + OrderHighlightOverlay.super.render(self, dc) +end + +function OrderHighlightOverlay:onRenderFrame(dc, rect) + OrderHighlightOverlay.super.onRenderFrame(self, dc, rect) + + if not search_cursor_visible then return end + + local selected_y = self:calculateSelectedOrderY() + if not selected_y then return end + + local highlight_pen = dfhack.pen.parse{ + fg=COLOR_LIGHTGREEN, + bold=true, + } + + local y = selected_y + 1 -- Middle line of the 3-line order + dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) + dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) +end + -- ------------------- OVERLAY_WIDGETS = { recheck=RecheckOverlay, importexport=OrdersOverlay, + search=OrdersSearchOverlay, + highlight=OrderHighlightOverlay, skillrestrictions=SkillRestrictionOverlay, laborrestrictions=LaborRestrictionsOverlay, conditionsrightclick=ConditionsRightClickOverlay, @@ -722,3 +1117,4 @@ OVERLAY_WIDGETS = { } return _ENV + From cfaa14062102150eb59e6be6d5b338c3941896db Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 21:39:56 +0100 Subject: [PATCH 676/919] Fix trailing whitespace --- plugins/lua/orders.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index c99e24fd213..0095fe8c273 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1056,7 +1056,7 @@ function OrderHighlightOverlay:calculateSelectedOrderY() local viewport_start = scroll_pos local viewport_end = scroll_pos + viewport_size - 1 - + -- Selected order tries to be at the top unless we're at the end of the list if viewport_end >= #orders then viewport_end = #orders - 1 @@ -1116,5 +1116,4 @@ OVERLAY_WIDGETS = { quantityrightclick=QuantityRightClickOverlay, } -return _ENV - +return _ENV \ No newline at end of file From 16ab556bb04bfb1558a70ad48d16e069b4f31a35 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 2 Dec 2025 21:45:39 +0100 Subject: [PATCH 677/919] Newline at the end --- plugins/lua/orders.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 0095fe8c273..22236e47696 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1116,4 +1116,4 @@ OVERLAY_WIDGETS = { quantityrightclick=QuantityRightClickOverlay, } -return _ENV \ No newline at end of file +return _ENV From 8ad478055629bce381623f4c39a39ed141f7f465 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 3 Dec 2025 11:56:10 -0500 Subject: [PATCH 678/919] Convert build-windows workflow to run on windows natively This should resolve the majority of spurious failures that occurred when building the windows release under Wine. --- .github/workflows/build-windows.yml | 81 +++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 9257a210816..41eb7a94e5c 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -66,12 +66,15 @@ on: jobs: build-win64: name: Build win64 - runs-on: ubuntu-22.04 + runs-on: windows-2022 steps: - - name: Install dependencies + - name: Install build dependencies run: | - sudo apt-get update - sudo apt-get install ccache + choco install sccache + - name: Install doc dependencies + if: inputs.docs + run: | + pip install sphinx - name: Clone DFHack uses: actions/checkout@v4 with: @@ -108,49 +111,80 @@ jobs: ref: main ssh-key: ${{ secrets.DFHACK_3RDPARTY_TOKEN }} path: depends/steam + - name: Prepare output directories + run: | + mkdir output + mkdir pdb - name: Fetch ccache if: inputs.platform-files uses: actions/cache/restore@v4 with: - path: build/win64-cross/ccache + path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} restore-keys: | win-msvc-${{ inputs.cache-id }} win-msvc + - uses: ilammy/msvc-dev-cmd@v1 + - name: Configure DFHack + run: | + cmake ` + -S . ` + -B build ` + -GNinja ` + -DDFHACK_BUILD_ARCH=64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX=output ` + -DCMAKE_C_COMPILER_LAUNCHER=sccache ` + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ` + -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} ` + -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id}}' ` + -DBUILD_LIBRARY=${{ inputs.platform-files }} ` + -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} ` + -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} ` + -DBUILD_DOCS:BOOL=${{ inputs.docs }} ` + -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} ` + -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} ` + -DBUILD_DFLAUNCH:BOOL=${{ inputs.launchdf }} ` + -DBUILD_TESTS:BOOL=${{ inputs.tests }} ` + -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ` + ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} - name: Cross-compile env: - CMAKE_EXTRA_ARGS: -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' -DBUILD_LIBRARY=${{ inputs.platform-files }} -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} -DBUILD_DOCS:BOOL=${{ inputs.docs }} -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} -DINSTALL_SCRIPTS:BOOL=${{ inputs.common-files }} -DBUILD_DFLAUNCH:BOOL=${{ inputs.launchdf }} -DBUILD_TESTS:BOOL=${{ inputs.tests }} -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} + SCCACHE_CACHE_SIZE: 500M run: | - cd build - bash -x build-win64-from-linux.sh + ninja install -C build - name: Finalize cache run: | cd build - ccache -d win64-cross/ccache --show-stats --verbose - ccache -d win64-cross/ccache --max-size ${{ inputs.cache-id == 'release' && '500M' || '150M' }} - ccache -d win64-cross/ccache --cleanup - ccache -d win64-cross/ccache --max-size ${{ inputs.cache-id == 'release' && '2G' || '500M' }} - ccache -d win64-cross/ccache --zero-stats + sccache --show-stats + sccache --zero-stats - name: Save ccache if: inputs.platform-files && !inputs.cache-readonly uses: actions/cache/save@v4 with: - path: build/win64-cross/ccache + path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} - name: Format artifact name if: inputs.artifact-name id: artifactname run: | - if test "false" = "${{ inputs.append-date-and-hash }}"; then - echo name=${{ inputs.artifact-name }} >> $GITHUB_OUTPUT - else - echo name=${{ inputs.artifact-name }}-$(date +%Y%m%d)-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT - fi + if ("${{ inputs.append-date-and-hash }}" -eq "false") { + "name=${{ inputs.artifact-name }}" | Out-File -Append $env:GITHUB_OUTPUT + } else { + $date = Get-Date -Format "yyyMMdd" + $hash = git rev-parse --short HEAD + "name=${{ inputs.artifact-name}}-$date-$hash" | Out-File -Append $env:GITHUB_OUTPUT + } + - name: Prep pdbs + if: inputs.artifact-name && inputs.cache-id == 'release' + run: | + Get-ChildItem -Recurse -File -Path "build" -Filter *.pdb | + Copy-Item -Destination "pdb" - name: Prep artifact - if: inputs.artifact-name run: | - cd build/win64-cross/output - tar cjf ../../../${{ steps.artifactname.outputs.name }}.tar.bz2 . + cd output + 7z a -ttar -so -an . | + 7z a -si -tbzip2 ../${{ steps.artifactname.outputs.name }}.tar.bz2 - name: Upload artifact if: inputs.artifact-name uses: actions/upload-artifact@v4 @@ -162,4 +196,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ steps.artifactname.outputs.name }}_pdb - path: build/win64-cross/pdb + path: pdb + From 3071d397265ce65dc1a7ad43bde8978bd8a7b3c6 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 3 Dec 2025 14:28:18 -0500 Subject: [PATCH 679/919] Fix line ending --- .github/workflows/build-windows.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 41eb7a94e5c..0a467378091 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -197,4 +197,3 @@ jobs: with: name: ${{ steps.artifactname.outputs.name }}_pdb path: pdb - From 911f949bbfc548cfdb0644b65503b3c0daabf00f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 4 Dec 2025 12:04:29 -0600 Subject: [PATCH 680/919] correct changelog.txt moved a 53.06 change to correct section --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f7d146dc456..29c6ef5365c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -78,7 +78,6 @@ Template for new versions: ## New Features ## Fixes -- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements @@ -99,6 +98,7 @@ Template for new versions: - compatibility with 53.05 ## Fixes +- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit ## Misc Improvements From 5687f5c0f0ee465d31a6a9b82e1920919b911422 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 15:01:47 -0500 Subject: [PATCH 681/919] Make a failure to obtain relevant crash backtrace obvious --- library/Crashlog.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 681e4fd4c5a..3860e0106e2 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -111,10 +111,6 @@ std::filesystem::path get_crashlog_path() { void dfhack_save_crashlog() { char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); - if (!backtrace_strings) { - // Allocation failed, give up - return; - } try { std::filesystem::path crashlog_path = get_crashlog_path(); std::ofstream crashlog(crashlog_path); @@ -128,9 +124,14 @@ void dfhack_save_crashlog() { crashlog << "Signal " << signal << "\n"; } - // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) - for (int i = 1; i < crash_info.backtrace_entries; i++) { - crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; + if (crash_info.backtrace_entries >= 2 && backtrace_strings != nullptr) { + // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) + for (int i = 1; i < crash_info.backtrace_entries; i++) { + crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; + } + } else { + // Make it clear if no relevant backtrace was able to be obtained + crashlog << "Failed to obtain relevant backtrace\n"; } } catch (...) {} From cd825e0f0b672ef997ffe1ae460fd4d969a82015 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 15:27:01 -0500 Subject: [PATCH 682/919] Default initialize scroll_state properly --- plugins/edgescroll.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 08a50430f88..cdc0c349d22 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -56,15 +56,15 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out static std::atomic_bool callback_queued = false; struct scroll_state { - int8_t xdiff; - int8_t ydiff; + int8_t xdiff = 0; + int8_t ydiff = 0; }; static scroll_state state; static scroll_state queued; static void render_thread_cb() { - queued = {0}; + queued = {}; // Ignore the mouse if outside the window if (!enabler->mouse_focus) { callback_queued.store(false); @@ -107,7 +107,7 @@ static bool update_mouse_pos() { // Queued callback complete, save the results and enqueue again state = queued; - queued = {0}; + queued = {}; DFHack::runOnRenderThread(render_thread_cb); callback_queued.store(true); return true; From 71d830ac7161a1e3c712b7fe387e35086e9bdfb8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:14:31 -0500 Subject: [PATCH 683/919] Determine LOCALAPPDATA path dynamically --- .github/workflows/build-windows.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 0a467378091..0864b716f97 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -115,11 +115,13 @@ jobs: run: | mkdir output mkdir pdb + - name: Get sccache path + run: echo ("SCCACHE_DIR=" + $env:LOCALAPPDATA + "\Mozilla\sccache\cache") >> $env:GITHUB_ENV - name: Fetch ccache if: inputs.platform-files uses: actions/cache/restore@v4 with: - path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache + path: ${{ env.SCCACHE_DIR }} key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} restore-keys: | win-msvc-${{ inputs.cache-id }} @@ -137,7 +139,7 @@ jobs: -DCMAKE_C_COMPILER_LAUNCHER=sccache ` -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ` -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} ` - -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id}}' ` + -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' ` -DBUILD_LIBRARY=${{ inputs.platform-files }} ` -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} ` -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} ` @@ -148,7 +150,7 @@ jobs: -DBUILD_TESTS:BOOL=${{ inputs.tests }} ` -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ` ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} - - name: Cross-compile + - name: Build DFHack env: SCCACHE_CACHE_SIZE: 500M run: | @@ -162,7 +164,7 @@ jobs: if: inputs.platform-files && !inputs.cache-readonly uses: actions/cache/save@v4 with: - path: C:\\Users\\runneradmin\\AppData\\Local\\Mozilla\\sccache\\cache + path: ${{ env.SCCACHE_DIR }} key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} - name: Format artifact name if: inputs.artifact-name From f65e4bdcc49de55979bc576f68158bae50c392ff Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:17:45 -0500 Subject: [PATCH 684/919] Remove redundant lock causing deadlock when listing keybinds --- library/modules/Hotkey.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index d7ef91ed107..e5fbf77f781 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -260,7 +260,6 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { - std::lock_guard l(lock); std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return {}; From 0e341b76b5b96d7b27616c77bace2df69728e4b7 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:33:33 -0500 Subject: [PATCH 685/919] Relocate keybinding command handling --- library/Commands.cpp | 79 +++++++++++++++++--------------- library/include/modules/Hotkey.h | 2 - library/modules/Hotkey.cpp | 57 ----------------------- 3 files changed, 43 insertions(+), 95 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 796a1f27015..289219c5946 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -9,6 +9,7 @@ #include "RemoteTools.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/World.h" #include "df/viewscreen_new_regionst.h" @@ -282,56 +283,62 @@ namespace DFHack command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; + using Hotkey::KeySpec; + auto hotkey_mgr = core.getHotkeyManager(); + std::string parse_error; + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + const std::string& keystr = parts[1]; if (parts[0] == "set") - core.ClearKeyBindings(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!core.AddKeyBinding(keystr, part)) - { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - return CR_FAILURE; + hotkey_mgr->removeKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + auto spec = KeySpec::parse(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!hotkey_mgr->addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); + break; } } } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!core.ClearKeyBindings(part)) - { - con.printerr("Invalid key spec: %s\n", part.c_str()); - return CR_FAILURE; + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + auto spec = KeySpec::parse(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!hotkey_mgr->removeKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); + break; } } } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = core.ListKeyBindings(parts[1]); + else if (parts.size() == 2 && parts[0] == "list") { + auto spec = KeySpec::parse(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return CR_FAILURE; + } + std::vector list = hotkey_mgr->listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) con << " " << kb << std::endl; } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl + else { + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" << join_strings("\n", Gui::getCurFocus(true)) << std::endl; } - return CR_OK; } diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 78978f29f6b..25129b2ff5a 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -33,7 +33,6 @@ namespace DFHack { }; } class DFHACK_EXPORT HotkeyManager { - friend class Core; public: HotkeyManager(); ~HotkeyManager(); @@ -74,6 +73,5 @@ namespace DFHack { std::map> bindings; void hotkey_thread_fn(); - void handleKeybindingCommand(color_ostream& out, const std::vector& parts); }; } diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index e5fbf77f781..6433879d859 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -387,63 +387,6 @@ std::string HotkeyManager::getKeybindingInput() { return requested_keybind; } -void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { - std::string parse_error; - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { - const std::string& keystr = parts[1]; - if (parts[0] == "set") - removeKeybind(keystr); - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - auto spec = KeySpec::parse(keystr, &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - break; - } - if (!addKeybind(spec.value(), part)) { - con.printerr("Invalid command: '%s'\n", part.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") { - for (const auto& part : parts | std::views::drop(1)) { - auto spec = KeySpec::parse(part, &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - } - if (!removeKeybind(spec.value())) { - con.printerr("No matching keybinds to remove\n"); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") { - auto spec = KeySpec::parse(parts[1], &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - return; - } - std::vector list = listKeybinds(spec.value()); - if (list.empty()) - con << "No bindings.\n"; - for (const auto& kb : list) - con << " " << kb << "\n"; - } - else { - con << "Usage:\n" - << " keybinding list \n" - << " keybinding clear [@context]...\n" - << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" - << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" - << "Later adds, and earlier items within one command have priority.\n" - << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" - << "Context may be used to limit the scope of the binding, by\n" - << "requiring the current context to have a certain prefix.\n" - << "Current UI context is: \n" - << join_strings("\n", Gui::getCurFocus(true)) << "\n"; - } -} - HotkeyManager::HotkeyManager() { this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); } From 522eb7b443b1a6a06975de2705300b9d63453433 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 5 Dec 2025 07:22:40 +0000 Subject: [PATCH 686/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 76b912392d4..40ac7ee39f6 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 76b912392d461138726bb0c0d82fcb47a5638cd3 +Subproject commit 40ac7ee39f67c1de8bb63ef307e8218e3d3fd14a diff --git a/plugins/stonesense b/plugins/stonesense index 9213b475463..50190a34de7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9213b475463c39251e93a5d7c93d1b0f38a0f537 +Subproject commit 50190a34de760732a4d8f926f28df9048fd0a5e2 diff --git a/scripts b/scripts index dd60bf218e6..163f2c46eec 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit dd60bf218e6f4a9c421327d65094998fb6459995 +Subproject commit 163f2c46eec0d10f722b7ea178b76730ba52b611 From e09948a20f892f5f804b8795be1c282936a314ab Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 6 Dec 2025 07:19:23 +0000 Subject: [PATCH 687/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 163f2c46eec..db609669d40 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 163f2c46eec0d10f722b7ea178b76730ba52b611 +Subproject commit db609669d4076a4d165d913ab98df487899acb20 From e36860793be04b18d5612cbdff1b9d495b34cacf Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 17:49:00 +0100 Subject: [PATCH 688/919] orders: load cache reaction map on init --- plugins/lua/orders.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 22236e47696..ef78d5b3033 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -758,6 +758,11 @@ local function make_order_key(order) end local function build_reaction_map() + local can_read_stockflow = dfhack.isWorldLoaded() and dfhack.isMapLoaded() + if not can_read_stockflow then + return nil + end + local map = {} local reactions = stockflow.collect_reactions() @@ -812,6 +817,8 @@ OrdersSearchOverlay.ATTRS{ } function OrdersSearchOverlay:init() + get_cached_reaction_map() + local main_panel = widgets.Panel{ view_id='main_panel', frame={t=0, l=0, r=0, h=4}, From b46eb7fa34925f3691df03faa26fefbf3e25485d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:05:07 +0100 Subject: [PATCH 689/919] Add Enter/Shift+Enter navigation and refactor jump to match --- plugins/lua/orders.lua | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index ef78d5b3033..31b9f76cbb0 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -832,23 +832,23 @@ function OrdersSearchOverlay:init() frame={t=0, l=0}, key='CUSTOM_ALT_S', on_change=self:callback('update_filter'), + on_submit=self:callback('on_submit'), + on_submit2=self:callback('on_submit2'), }, widgets.HotkeyLabel{ - view_id='prev_match', frame={t=1, l=0}, label='prev', key='CUSTOM_ALT_P', auto_width=true, - on_activate=self:callback('jump_to_previous_match'), + on_activate=self:callback('cycle_match', -1), enabled=function() return self:has_matches() end, }, widgets.HotkeyLabel{ - view_id='next_match', frame={t=1, l=17}, label='next', key='CUSTOM_ALT_N', auto_width=true, - on_activate=self:callback('jump_to_next_match'), + on_activate=self:callback('cycle_match', 1), enabled=function() return self:has_matches() end, }, }, @@ -913,27 +913,23 @@ function OrdersSearchOverlay:update_filter(text) self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() end -function OrdersSearchOverlay:jump_to_next_match() - if #self.matched_indices == 0 then return end - - self.current_match_idx = self.current_match_idx + 1 - if self.current_match_idx > #self.matched_indices then - self.current_match_idx = 1 - end - - local order_idx = self.matched_indices[self.current_match_idx] - mi.info.work_orders.scroll_position_work_orders = order_idx - search_last_scroll_position = order_idx - search_cursor_visible = true +function OrdersSearchOverlay:on_submit() + self:cycle_match(1) + self.subviews.filter:setFocus(true) +end - self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() +function OrdersSearchOverlay:on_submit2() + self:cycle_match(-1) + self.subviews.filter:setFocus(true) end -function OrdersSearchOverlay:jump_to_previous_match() +function OrdersSearchOverlay:cycle_match(direction) if #self.matched_indices == 0 then return end - self.current_match_idx = self.current_match_idx - 1 - if self.current_match_idx < 1 then + self.current_match_idx = self.current_match_idx + direction + if direction > 0 and self.current_match_idx > #self.matched_indices then + self.current_match_idx = 1 + elseif direction < 0 and self.current_match_idx < 1 then self.current_match_idx = #self.matched_indices end From cde147713f745c1130f2eb135b9a23e60422dfd5 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:09:30 +0100 Subject: [PATCH 690/919] Hide search highlight when filter text changes --- plugins/lua/orders.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 31b9f76cbb0..a4672eeeda1 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -895,6 +895,7 @@ function OrdersSearchOverlay:update_filter(text) self.filter_text = text:lower() self.matched_indices = {} self.current_match_idx = 0 + search_cursor_visible = false if self.filter_text == '' then self.subviews.main_panel.frame_title = 'Search' From 7fa03fb4c200ded3cee46089e8cbf2bee3945bdc Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:18:06 +0100 Subject: [PATCH 691/919] Use full_interface for OrderHighlightOverlay to prevent repositioning --- plugins/lua/orders.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index a4672eeeda1..e4cb70e0f68 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1010,7 +1010,7 @@ OrderHighlightOverlay.ATTRS{ desc='Shows arrows next to the work order found by orders.search', default_enabled=false, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - frame={w=80, h=3}, + full_interface=true, } function OrderHighlightOverlay:init() From c163d3c159137f8584355e0dbc13cbc7aeec392c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:35:18 +0100 Subject: [PATCH 692/919] Hide highlight when order list changes --- plugins/lua/orders.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index e4cb70e0f68..7692529440b 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -11,6 +11,7 @@ local stockflow = reqscript('internal/quickfort/stockflow') -- Shared state for search cursor visibility local search_cursor_visible = false local search_last_scroll_position = -1 +local order_count_at_highlight = 0 -- -- OrdersOverlay @@ -938,6 +939,7 @@ function OrdersSearchOverlay:cycle_match(direction) mi.info.work_orders.scroll_position_work_orders = order_idx search_last_scroll_position = order_idx search_cursor_visible = true + order_count_at_highlight = #df.global.world.manager_orders.all self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() end @@ -1077,12 +1079,18 @@ end function OrderHighlightOverlay:render(dc) if search_cursor_visible then local current_scroll = mi.info.work_orders.scroll_position_work_orders + local current_order_count = #df.global.world.manager_orders.all -- Hide cursor when user manually scrolls if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then search_cursor_visible = false end search_last_scroll_position = current_scroll + + -- Hide cursor when order list changes (orders added or removed) + if order_count_at_highlight ~= current_order_count then + search_cursor_visible = false + end end OrderHighlightOverlay.super.render(self, dc) From f37e46adec9964b48bc8ada72e298c538b90be5a Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 19:47:35 +0100 Subject: [PATCH 693/919] Consolidate onRenderFrame into render method --- plugins/lua/orders.lua | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 7692529440b..eb2b4207b5a 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1085,33 +1085,25 @@ function OrderHighlightOverlay:render(dc) if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then search_cursor_visible = false end - search_last_scroll_position = current_scroll -- Hide cursor when order list changes (orders added or removed) if order_count_at_highlight ~= current_order_count then search_cursor_visible = false end - end - - OrderHighlightOverlay.super.render(self, dc) -end - -function OrderHighlightOverlay:onRenderFrame(dc, rect) - OrderHighlightOverlay.super.onRenderFrame(self, dc, rect) - - if not search_cursor_visible then return end - local selected_y = self:calculateSelectedOrderY() - if not selected_y then return end - - local highlight_pen = dfhack.pen.parse{ - fg=COLOR_LIGHTGREEN, - bold=true, - } + -- Draw highlight arrows + local selected_y = self:calculateSelectedOrderY() + if selected_y then + local highlight_pen = dfhack.pen.parse{ + fg=COLOR_LIGHTGREEN, + bold=true, + } - local y = selected_y + 1 -- Middle line of the 3-line order - dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) - dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) + local y = selected_y + 1 -- Middle line of the 3-line order + dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) + dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) + end + end end -- ------------------- From 506ca40b1166657cff24c07e46305bdf198ece40 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 20:09:09 +0100 Subject: [PATCH 694/919] Use utils.search_text instead of custom search --- plugins/lua/orders.lua | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index eb2b4207b5a..28d16068d8c 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -793,21 +793,6 @@ local function get_order_search_key(order) return "" end -local function matches_all_search_words(search_key, filter_text) - local search_words = {} - for word in filter_text:gmatch('%S+') do - table.insert(search_words, word) - end - - -- Check if all search words are found in search_key (order-independent) - for _, search_word in ipairs(search_words) do - if not search_key:find(search_word, 1, true) then - return false - end - end - return true -end - OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -886,19 +871,17 @@ function OrdersSearchOverlay:init() } -- Initialize search state - self.filter_text = '' self.matched_indices = {} self.current_match_idx = 0 self.minimized = false end function OrdersSearchOverlay:update_filter(text) - self.filter_text = text:lower() self.matched_indices = {} self.current_match_idx = 0 search_cursor_visible = false - if self.filter_text == '' then + if text == '' then self.subviews.main_panel.frame_title = 'Search' return end @@ -907,7 +890,7 @@ function OrdersSearchOverlay:update_filter(text) for i = 0, #orders - 1 do local order = orders[i] local search_key = get_order_search_key(order) - if matches_all_search_words(search_key, self.filter_text) then + if search_key and utils.search_text(search_key, text) then table.insert(self.matched_indices, i) end end @@ -945,12 +928,12 @@ function OrdersSearchOverlay:cycle_match(direction) end function OrdersSearchOverlay:get_match_text() - if self.filter_text == '' then + local total_matches = #self.matched_indices + + if total_matches == 0 then return '' end - local total_matches = #self.matched_indices - if self.current_match_idx == 0 then return string.format('%d matches', total_matches) end From 7fabf2b8b887d795ca2f1c6d3c3e5d057e29e325 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 20:13:46 +0100 Subject: [PATCH 695/919] Force new version of overlay position --- plugins/lua/orders.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 28d16068d8c..db2a187f42e 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -84,6 +84,7 @@ OrdersOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=43, h=4}, + version=1, } function OrdersOverlay:init() From 2cbf2d7860b09b730f9853c88c50ec55e18452db Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 20:24:51 +0100 Subject: [PATCH 696/919] Narrow search overlay and adjust button positions Now there is 16 visible chars in search --- plugins/lua/orders.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index db2a187f42e..c2600ce5b0c 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -800,7 +800,7 @@ OrdersSearchOverlay.ATTRS{ default_pos={x=85, y=-6}, default_enabled=false, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - frame={w=34, h=4}, + frame={w=26, h=4}, } function OrdersSearchOverlay:init() @@ -831,7 +831,7 @@ function OrdersSearchOverlay:init() enabled=function() return self:has_matches() end, }, widgets.HotkeyLabel{ - frame={t=1, l=17}, + frame={t=1, l=12}, label='next', key='CUSTOM_ALT_N', auto_width=true, From 1f2705d500ba045cc2aef8d3f3080e81bd8b540b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 6 Dec 2025 21:31:49 +0100 Subject: [PATCH 697/919] Reshape arrow and contrasting colors --- plugins/lua/orders.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index c2600ce5b0c..0df4e06e988 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1005,9 +1005,9 @@ function OrderHighlightOverlay:init() self.LIST_START_Y_ONE_TABS_ROW = 8 self.LIST_START_Y_TWO_TABS_ROWS = 10 self.BOTTOM_MARGIN = 9 - self.ARROW_X_FIRST = 5 - self.ARROW_X_SECOND = 6 - self.ARROW_CHAR = '>' + self.ARROW_X = 10 + self.ARROW_FG = COLOR_BLACK + self.ARROW_BG = COLOR_WHITE self.cached_list_start_y = nil self.cached_viewport_size = nil @@ -1079,13 +1079,14 @@ function OrderHighlightOverlay:render(dc) local selected_y = self:calculateSelectedOrderY() if selected_y then local highlight_pen = dfhack.pen.parse{ - fg=COLOR_LIGHTGREEN, + fg=self.ARROW_FG, + bg=self.ARROW_BG, bold=true, } - local y = selected_y + 1 -- Middle line of the 3-line order - dc:seek(self.ARROW_X_FIRST, y):string(self.ARROW_CHAR, highlight_pen) - dc:seek(self.ARROW_X_SECOND, y):string(self.ARROW_CHAR, highlight_pen) + dc:seek(self.ARROW_X, selected_y):string('|', highlight_pen) + dc:seek(self.ARROW_X, selected_y + 1):string('>', highlight_pen) + dc:seek(self.ARROW_X, selected_y + 2):string('|', highlight_pen) end end end From 93f6b1c28d51be3d6b9858367e1bd1b46312c69f Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 7 Dec 2025 10:27:47 +0100 Subject: [PATCH 698/919] Hide overlay when job_details is open. Add author --- docs/about/Authors.rst | 1 + plugins/lua/orders.lua | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 8743b75a196..a154d314b63 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -163,6 +163,7 @@ Omniclasm Ong Ying Gao ong-yinggao98 oorzkws oorzkws OwnageIsMagic OwnageIsMagic +pajawojciech pajawojciech palenerd dlmarquis PassionateAngler PassionateAngler Patrik Lundell PatrikLundell diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 0df4e06e988..8cde607826f 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -957,6 +957,8 @@ local function is_mouse_key(keys) end function OrdersSearchOverlay:onInput(keys) + if mi.job_details.open then return end + local filter_field = self.subviews.filter if not filter_field then return false end @@ -987,6 +989,11 @@ function OrdersSearchOverlay:onInput(keys) return false end +function OrdersSearchOverlay:render(dc) + if mi.job_details.open then return end + OrdersSearchOverlay.super.render(self, dc) +end + -- ------------------- -- OrderHighlightOverlay -- ------------------- @@ -1061,6 +1068,8 @@ function OrderHighlightOverlay:calculateSelectedOrderY() end function OrderHighlightOverlay:render(dc) + if mi.job_details.open then return end + if search_cursor_visible then local current_scroll = mi.info.work_orders.scroll_position_work_orders local current_order_count = #df.global.world.manager_orders.all From 6bd16adfcff2d9b064e4d901de5d3151fb173205 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 7 Dec 2025 10:31:48 +0100 Subject: [PATCH 699/919] Remove trailing spaces --- plugins/lua/orders.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 8cde607826f..0960bfd83d1 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -958,7 +958,7 @@ end function OrdersSearchOverlay:onInput(keys) if mi.job_details.open then return end - + local filter_field = self.subviews.filter if not filter_field then return false end From 76477ae446ef4f2e0f0094f460ec5b5054982fda Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Mon, 8 Dec 2025 23:23:15 -0500 Subject: [PATCH 700/919] infinite-sky: Add support for invasion maps --- docs/changelog.txt | 1 + plugins/CMakeLists.txt | 2 +- plugins/infinite-sky.cpp | 56 +++++++++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e9b68fb94a6..baf28404bed 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Tools - ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. +- `infinite-sky`: Re-enabled with compatibility with new siege map data. ## New Features - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 0ac4fe9b57f..355cc0ac4c0 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -87,7 +87,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) dfhack_plugin(getplants getplants.cpp) dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) - #dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) + dfhack_plugin(infinite-sky infinite-sky.cpp LINK_LIBRARIES lua) #dfhack_plugin(jobutils jobutils.cpp) dfhack_plugin(lair lair.cpp) dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 158dea2a421..402a4a7a7b7 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -10,8 +10,13 @@ #include "df/block_column_print_infost.h" #include "df/construction.h" +#include "df/entity_plot_invasion_mapst.h" +#include "df/historical_entity.h" +#include "df/invasion_info.h" #include "df/map_block.h" #include "df/map_block_column.h" +#include "df/plotinfost.h" +#include "df/plot_invasion_mapst.h" #include "df/world.h" #include "df/z_level_flags.h" @@ -28,6 +33,7 @@ using namespace df::enums; DFHACK_PLUGIN("infinite-sky"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); +REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); namespace DFHack { @@ -130,7 +136,7 @@ static void constructionEventHandler(color_ostream &out, void *ptr) { doInfiniteSky(out, 1); } -void doInfiniteSky(color_ostream& out, int32_t howMany) { +void addBlockColumns(color_ostream& out, int32_t quantity) { int32_t z_count_block = world->map.z_count_block; df::map_block ****block_index = world->map.block_index; @@ -141,14 +147,14 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { last_air_layer.forCoord([&](df::coord bpos) { // Allocate a new block column and copy over data from the old df::map_block **blockColumn = - new df::map_block *[z_count_block + howMany]; + new df::map_block *[z_count_block + quantity]; memcpy(blockColumn, block_index[bpos.x][bpos.y], z_count_block * sizeof(df::map_block *)); delete[] block_index[bpos.x][bpos.y]; block_index[bpos.x][bpos.y] = blockColumn; df::map_block *last_air_block = blockColumn[bpos.z]; - for (int32_t count = 0; count < howMany; count++) { + for (int32_t count = 0; count < quantity; count++) { df::map_block *air_block = new df::map_block(); std::fill(&air_block->tiletype[0][0], &air_block->tiletype[0][0] + (16 * 16), @@ -209,19 +215,55 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { }); // Update global z level flags - df::z_level_flags *flags = new df::z_level_flags[z_count_block + howMany]; + df::z_level_flags *flags = new df::z_level_flags[z_count_block + quantity]; memcpy(flags, world->map_extras.z_level_flags, z_count_block * sizeof(df::z_level_flags)); - for (int32_t count = 0; count < howMany; count++) { + for (int32_t count = 0; count < quantity; count++) { flags[z_count_block + count].whole = 0; flags[z_count_block + count].bits.update = 1; } - world->map.z_count_block += howMany; - world->map.z_count += howMany; + world->map.z_count_block += quantity; + world->map.z_count += quantity; delete[] world->map_extras.z_level_flags; world->map_extras.z_level_flags = flags; } +void updateInvasionMap(color_ostream &out, int32_t new_height, df::plot_invasion_mapst& map) { + if (map.blockz == 0) + return; // Unused invasion map + if (map.blockz >= new_height) + return; // No change required + + cuboid blocks(0, 0, 0, map.blockx - 1, map.blocky - 1, 0); + blocks.forCoord([&](df::coord bpos) { + // Create new vertical block + df::pim_blockst **new_block = new df::pim_blockst *[new_height](); + memcpy(new_block, map.block_index[bpos.x][bpos.y], map.blockz * sizeof(df::pim_blockst*)); + // Fill new block with nullptr (no information) + std::fill_n(&new_block[map.blockz], new_height - map.blockz, nullptr); + delete[] map.block_index[bpos.x][bpos.y]; + map.block_index[bpos.x][bpos.y] = new_block; + return true; + }); + + map.blockz = new_height; +} + +void doInfiniteSky(color_ostream &out, int32_t quantity) { + addBlockColumns(out, quantity); + + for (auto& invasion : plotinfo->invasions.list) { + updateInvasionMap(out, world->map.z_count, invasion->map); + } + for (auto& entity : world->entities.all) { + for (auto& map : entity->plot_invasion_map) { + if (map->site_id != plotinfo->site_id) + continue; + updateInvasionMap(out, world->map.z_count, map->map); + } + } +} + struct infinitesky_options { // whether to display help bool help = false; From d8eb61e1b8eb0401f00c5a64428137d70867432d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:19:38 +0100 Subject: [PATCH 701/919] Rebuild manager order search results on every navigation to fix stale results after sorting/clearing/importing orders --- plugins/lua/orders.lua | 58 +++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 0960bfd83d1..8641757de6a 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -877,14 +877,11 @@ function OrdersSearchOverlay:init() self.minimized = false end -function OrdersSearchOverlay:update_filter(text) - self.matched_indices = {} - self.current_match_idx = 0 - search_cursor_visible = false +function OrdersSearchOverlay:perform_search(text) + local matches = {} if text == '' then - self.subviews.main_panel.frame_title = 'Search' - return + return matches end local orders = df.global.world.manager_orders.all @@ -892,11 +889,23 @@ function OrdersSearchOverlay:update_filter(text) local order = orders[i] local search_key = get_order_search_key(order) if search_key and utils.search_text(search_key, text) then - table.insert(self.matched_indices, i) + table.insert(matches, i) end end - self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() + return matches +end + +function OrdersSearchOverlay:update_filter(text) + self.matched_indices = self:perform_search(text) + self.current_match_idx = 0 + search_cursor_visible = false + + if text == '' then + self.subviews.main_panel.frame_title = 'Search' + else + self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() + end end function OrdersSearchOverlay:on_submit() @@ -910,22 +919,37 @@ function OrdersSearchOverlay:on_submit2() end function OrdersSearchOverlay:cycle_match(direction) - if #self.matched_indices == 0 then return end + local search_text = self.subviews.filter.text + + local new_matches = self:perform_search(search_text) - self.current_match_idx = self.current_match_idx + direction - if direction > 0 and self.current_match_idx > #self.matched_indices then - self.current_match_idx = 1 - elseif direction < 0 and self.current_match_idx < 1 then - self.current_match_idx = #self.matched_indices + if #new_matches == 0 then + self.matched_indices = {} + self.current_match_idx = 0 + search_cursor_visible = false + self.subviews.main_panel.frame_title = 'Search' + return end + local new_match_idx = self.current_match_idx + direction + + if new_match_idx > #new_matches then + new_match_idx = 1 + elseif new_match_idx < 1 then + new_match_idx = #new_matches + end + + self.matched_indices = new_matches + self.current_match_idx = new_match_idx + + -- Scroll to the selected match local order_idx = self.matched_indices[self.current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx search_last_scroll_position = order_idx search_cursor_visible = true order_count_at_highlight = #df.global.world.manager_orders.all - self.subviews.main_panel.frame_title = 'Search: ' .. self:get_match_text() + self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end function OrdersSearchOverlay:get_match_text() @@ -936,10 +960,10 @@ function OrdersSearchOverlay:get_match_text() end if self.current_match_idx == 0 then - return string.format('%d matches', total_matches) + return string.format(': %d matches', total_matches) end - return string.format('%d of %d', self.current_match_idx, total_matches) + return string.format(': %d of %d', self.current_match_idx, total_matches) end function OrdersSearchOverlay:has_matches() From 860a2a1c9d5b50e85d02d6791bfd43f82bb3ab22 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:23:36 +0100 Subject: [PATCH 702/919] Consolidate search state variables in OrdersSearchOverlay section to remove shadowing --- plugins/lua/orders.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 8641757de6a..1d9e8659d06 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -8,11 +8,6 @@ local utils = require('utils') local widgets = require('gui.widgets') local stockflow = reqscript('internal/quickfort/stockflow') --- Shared state for search cursor visibility -local search_cursor_visible = false -local search_last_scroll_position = -1 -local order_count_at_highlight = 0 - -- -- OrdersOverlay -- @@ -722,6 +717,7 @@ end local search_cursor_visible = false local search_last_scroll_position = -1 +local order_count_at_highlight = 0 local function make_order_key(order) local mat_cat_str = '' From ab257aa54b8424bb288c153e5b78ba0e89eb581e Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:27:26 +0100 Subject: [PATCH 703/919] Guard get_order_search_key against nil reaction map and return nil for missing entries --- plugins/lua/orders.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 1d9e8659d06..a47574c96b0 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -783,11 +783,11 @@ end local function get_order_search_key(order) local reaction_map = get_cached_reaction_map() - local key = make_order_key(order) - if reaction_map[key] then - return reaction_map[key] + if not reaction_map then + return nil end - return "" + local key = make_order_key(order) + return reaction_map[key] end OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) From 3a727a097a97294cbf1301059946fabaf7bf3a3f Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:29:37 +0100 Subject: [PATCH 704/919] Remove unused cached variables from OrderHighlightOverlay init --- plugins/lua/orders.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index a47574c96b0..f8da92677ed 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1035,11 +1035,6 @@ function OrderHighlightOverlay:init() self.ARROW_X = 10 self.ARROW_FG = COLOR_BLACK self.ARROW_BG = COLOR_WHITE - - self.cached_list_start_y = nil - self.cached_viewport_size = nil - self.cached_screen_width = nil - self.cached_screen_height = nil end function OrderHighlightOverlay:getListStartY() From d3d7067d1378e0e924849d7c8678c2fdcf5a95f4 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:33:42 +0100 Subject: [PATCH 705/919] Convert OrderHighlightOverlay constants from instance fields to module-locals and inline arrow colors --- plugins/lua/orders.lua | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index f8da92677ed..dcfd92d4e57 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1018,6 +1018,13 @@ end -- OrderHighlightOverlay -- ------------------- +local ORDER_HEIGHT = 3 +local TABS_WIDTH_THRESHOLD = 155 +local LIST_START_Y_ONE_TABS_ROW = 8 +local LIST_START_Y_TWO_TABS_ROWS = 10 +local BOTTOM_MARGIN = 9 +local ARROW_X = 10 + OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) OrderHighlightOverlay.ATTRS{ desc='Shows arrows next to the work order found by orders.search', @@ -1026,24 +1033,13 @@ OrderHighlightOverlay.ATTRS{ full_interface=true, } -function OrderHighlightOverlay:init() - self.ORDER_HEIGHT = 3 - self.TABS_WIDTH_THRESHOLD = 155 - self.LIST_START_Y_ONE_TABS_ROW = 8 - self.LIST_START_Y_TWO_TABS_ROWS = 10 - self.BOTTOM_MARGIN = 9 - self.ARROW_X = 10 - self.ARROW_FG = COLOR_BLACK - self.ARROW_BG = COLOR_WHITE -end - function OrderHighlightOverlay:getListStartY() local rect = gui.get_interface_rect() - if rect.width >= self.TABS_WIDTH_THRESHOLD then - return self.LIST_START_Y_ONE_TABS_ROW + if rect.width >= TABS_WIDTH_THRESHOLD then + return LIST_START_Y_ONE_TABS_ROW else - return self.LIST_START_Y_TWO_TABS_ROWS + return LIST_START_Y_TWO_TABS_ROWS end end @@ -1051,8 +1047,8 @@ function OrderHighlightOverlay:getViewportSize() local rect = gui.get_interface_rect() local list_start_y = self:getListStartY() - local available_height = rect.height - list_start_y - self.BOTTOM_MARGIN - return math.floor(available_height / self.ORDER_HEIGHT) + local available_height = rect.height - list_start_y - BOTTOM_MARGIN + return math.floor(available_height / ORDER_HEIGHT) end function OrderHighlightOverlay:calculateSelectedOrderY() @@ -1077,7 +1073,7 @@ function OrderHighlightOverlay:calculateSelectedOrderY() local pos_in_viewport = scroll_pos - viewport_start - local selected_y = list_start_y + (pos_in_viewport * self.ORDER_HEIGHT) + local selected_y = list_start_y + (pos_in_viewport * ORDER_HEIGHT) return selected_y end @@ -1103,14 +1099,14 @@ function OrderHighlightOverlay:render(dc) local selected_y = self:calculateSelectedOrderY() if selected_y then local highlight_pen = dfhack.pen.parse{ - fg=self.ARROW_FG, - bg=self.ARROW_BG, + fg=COLOR_BLACK, + bg=COLOR_WHITE, bold=true, } - dc:seek(self.ARROW_X, selected_y):string('|', highlight_pen) - dc:seek(self.ARROW_X, selected_y + 1):string('>', highlight_pen) - dc:seek(self.ARROW_X, selected_y + 2):string('|', highlight_pen) + dc:seek(ARROW_X, selected_y):string('|', highlight_pen) + dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) + dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) end end end From 2cd1c6efa7986852aa5e760dbc82283221955493 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:42:01 +0100 Subject: [PATCH 706/919] Use early return guard for search cursor visibility in OrderHighlightOverlay render. Return when disable cursor --- plugins/lua/orders.lua | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index dcfd92d4e57..e11d11c9efe 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1079,35 +1079,35 @@ function OrderHighlightOverlay:calculateSelectedOrderY() end function OrderHighlightOverlay:render(dc) - if mi.job_details.open then return end - - if search_cursor_visible then - local current_scroll = mi.info.work_orders.scroll_position_work_orders - local current_order_count = #df.global.world.manager_orders.all + if mi.job_details.open or not search_cursor_visible then return end - -- Hide cursor when user manually scrolls - if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then - search_cursor_visible = false - end + local current_scroll = mi.info.work_orders.scroll_position_work_orders + local current_order_count = #df.global.world.manager_orders.all - -- Hide cursor when order list changes (orders added or removed) - if order_count_at_highlight ~= current_order_count then - search_cursor_visible = false - end + -- Hide cursor when user manually scrolls + if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then + search_cursor_visible = false + return + end - -- Draw highlight arrows - local selected_y = self:calculateSelectedOrderY() - if selected_y then - local highlight_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, - bold=true, - } + -- Hide cursor when order list changes (orders added or removed) + if order_count_at_highlight ~= current_order_count then + search_cursor_visible = false + return + end - dc:seek(ARROW_X, selected_y):string('|', highlight_pen) - dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) - dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) - end + -- Draw highlight arrows + local selected_y = self:calculateSelectedOrderY() + if selected_y then + local highlight_pen = dfhack.pen.parse{ + fg=COLOR_BLACK, + bg=COLOR_WHITE, + bold=true, + } + + dc:seek(ARROW_X, selected_y):string('|', highlight_pen) + dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) + dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) end end From d605165a83d8de5253e48655fac480ee59cfa70c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:46:02 +0100 Subject: [PATCH 707/919] Add unconditional super render call to OrderHighlightOverlay for future compatibility --- plugins/lua/orders.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index e11d11c9efe..67ea8ab8b00 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -1079,6 +1079,8 @@ function OrderHighlightOverlay:calculateSelectedOrderY() end function OrderHighlightOverlay:render(dc) + OrderHighlightOverlay.super.render(self, dc) + if mi.job_details.open or not search_cursor_visible then return end local current_scroll = mi.info.work_orders.scroll_position_work_orders From 14f46c897bf3a9c7b8acb2f2a6986bc2af78a59b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 09:48:54 +0100 Subject: [PATCH 708/919] Inline build_reaction_map into get_cached_reaction_map with early return pattern --- plugins/lua/orders.lua | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 67ea8ab8b00..14e79d9f25d 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -755,7 +755,13 @@ local function make_order_key(order) encrust_str) end -local function build_reaction_map() +local reaction_map_cache = nil + +local function get_cached_reaction_map() + if reaction_map_cache then + return reaction_map_cache + end + local can_read_stockflow = dfhack.isWorldLoaded() and dfhack.isMapLoaded() if not can_read_stockflow then return nil @@ -769,15 +775,7 @@ local function build_reaction_map() map[key] = reaction.name:lower() end - return map -end - -local reaction_map_cache = nil - -local function get_cached_reaction_map() - if not reaction_map_cache then - reaction_map_cache = build_reaction_map() - end + reaction_map_cache = map return reaction_map_cache end From 1bdd533b3038fa3fbd189b711b18e840f6e5d8db Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 13 Dec 2025 10:00:37 +0100 Subject: [PATCH 709/919] Free C++ manager_order objects allocated by collect_reactions to prevent memory leak --- plugins/lua/orders.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 14e79d9f25d..f0e7bc502eb 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -773,7 +773,9 @@ local function get_cached_reaction_map() for _, reaction in ipairs(reactions) do local key = make_order_key(reaction.order) map[key] = reaction.name:lower() + df.delete(reaction.order) end + reactions = nil reaction_map_cache = map return reaction_map_cache From 501d01a09761354f9f173a996dd746cabe8d9bb0 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:47:36 +0000 Subject: [PATCH 710/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index db609669d40..578f6fe8fae 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit db609669d4076a4d165d913ab98df487899acb20 +Subproject commit 578f6fe8faed4d8e156787c25eadc39cdd0bc358 From 1b42dfadb8973133107f9a0395f09df330b71bed Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 15 Dec 2025 08:01:54 -0600 Subject: [PATCH 711/919] 53.06-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 528f6f0d934..ea49529bc83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.06") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 0645b1241d12bdee3265a6357c3f0388f265824e Mon Sep 17 00:00:00 2001 From: Quietust Date: Wed, 17 Dec 2025 09:27:20 -0600 Subject: [PATCH 712/919] Add drawbridge-tiles tweak for ASCII mode --- docs/changelog.txt | 1 + docs/plugins/tweak.rst | 3 ++ plugins/tweak/tweak.cpp | 3 ++ plugins/tweak/tweaks/drawbridge-tiles.h | 60 +++++++++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 plugins/tweak/tweaks/drawbridge-tiles.h diff --git a/docs/changelog.txt b/docs/changelog.txt index baf28404bed..c71c0115c0f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## New Features - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status +- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index ae42aecac78..c3699603443 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -45,6 +45,9 @@ Commands Fixes crafted items not wearing out over time (:bug:`6003`). With this tweak, items made from cloth and leather will gain a level of wear every 20 in-game years. +``drawbridge-tiles`` + Makes raising bridges in ASCII mode render with different tiles when they + are raised. ``eggs-fertile`` Displays an indicator on fertile eggs. ``fast-heat`` diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 0e69da9df60..eeed5027a86 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -16,6 +16,7 @@ using namespace DFHack; #include "tweaks/adamantine-cloth-wear.h" #include "tweaks/animaltrap-reuse.h" #include "tweaks/craft-age-wear.h" +#include "tweaks/drawbridge-tiles.h" #include "tweaks/eggs-fertile.h" #include "tweaks/fast-heat.h" #include "tweaks/flask-contents.h" @@ -58,6 +59,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, vectory1; p2 = buf->y2; iy = true; + break; + case 2: // Up + case 3: // Down + p1 = buf->x1; p2 = buf->x2; + break; + default: + // Already ignoring retracting above + return; + } + + int x = 0, y = 0; + if (p1 == p2) + buf->tile[0][0] = tiles[direction][1]; + else for (int p = p1; p <= p2; p++) + { + if (p == p1) + buf->tile[x][y] = tiles[direction][0]; + else if (p == p2) + buf->tile[x][y] = tiles[direction][2]; + else + buf->tile[x][y] = tiles[direction][1]; + if (iy) + y++; + else + x++; + } + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(drawbridge_tiles_hook, drawBuilding); From c4e84d744181f78cc12a6e176888eab8451a5adf Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Wed, 17 Dec 2025 19:23:51 +0100 Subject: [PATCH 713/919] Refactor overlay helper functions to local scope in orders.lua --- plugins/lua/orders.lua | 72 +++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index f0e7bc502eb..5d96dcfc354 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -790,6 +790,25 @@ local function get_order_search_key(order) return reaction_map[key] end +local function perform_search(text) + local matches = {} + + if text == '' then + return matches + end + + local orders = df.global.world.manager_orders.all + for i = 0, #orders - 1 do + local order = orders[i] + local search_key = get_order_search_key(order) + if search_key and utils.search_text(search_key, text) then + table.insert(matches, i) + end + end + + return matches +end + OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -873,27 +892,8 @@ function OrdersSearchOverlay:init() self.minimized = false end -function OrdersSearchOverlay:perform_search(text) - local matches = {} - - if text == '' then - return matches - end - - local orders = df.global.world.manager_orders.all - for i = 0, #orders - 1 do - local order = orders[i] - local search_key = get_order_search_key(order) - if search_key and utils.search_text(search_key, text) then - table.insert(matches, i) - end - end - - return matches -end - function OrdersSearchOverlay:update_filter(text) - self.matched_indices = self:perform_search(text) + self.matched_indices = perform_search(text) self.current_match_idx = 0 search_cursor_visible = false @@ -917,7 +917,7 @@ end function OrdersSearchOverlay:cycle_match(direction) local search_text = self.subviews.filter.text - local new_matches = self:perform_search(search_text) + local new_matches = perform_search(search_text) if #new_matches == 0 then self.matched_indices = {} @@ -1025,15 +1025,7 @@ local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 -OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) -OrderHighlightOverlay.ATTRS{ - desc='Shows arrows next to the work order found by orders.search', - default_enabled=false, - viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - full_interface=true, -} - -function OrderHighlightOverlay:getListStartY() +local function getListStartY() local rect = gui.get_interface_rect() if rect.width >= TABS_WIDTH_THRESHOLD then @@ -1043,15 +1035,15 @@ function OrderHighlightOverlay:getListStartY() end end -function OrderHighlightOverlay:getViewportSize() +local function getViewportSize() local rect = gui.get_interface_rect() - local list_start_y = self:getListStartY() + local list_start_y = getListStartY() local available_height = rect.height - list_start_y - BOTTOM_MARGIN return math.floor(available_height / ORDER_HEIGHT) end -function OrderHighlightOverlay:calculateSelectedOrderY() +local function calculateSelectedOrderY() local orders = df.global.world.manager_orders.all local scroll_pos = mi.info.work_orders.scroll_position_work_orders @@ -1059,8 +1051,8 @@ function OrderHighlightOverlay:calculateSelectedOrderY() return nil end - local list_start_y = self:getListStartY() - local viewport_size = self:getViewportSize() + local list_start_y = getListStartY() + local viewport_size = getViewportSize() local viewport_start = scroll_pos local viewport_end = scroll_pos + viewport_size - 1 @@ -1078,6 +1070,14 @@ function OrderHighlightOverlay:calculateSelectedOrderY() return selected_y end +OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) +OrderHighlightOverlay.ATTRS{ + desc='Shows arrows next to the work order found by orders.search', + default_enabled=false, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + full_interface=true, +} + function OrderHighlightOverlay:render(dc) OrderHighlightOverlay.super.render(self, dc) @@ -1099,7 +1099,7 @@ function OrderHighlightOverlay:render(dc) end -- Draw highlight arrows - local selected_y = self:calculateSelectedOrderY() + local selected_y = calculateSelectedOrderY() if selected_y then local highlight_pen = dfhack.pen.parse{ fg=COLOR_BLACK, From a98e9ccca44fa8c811d5ac75cf8de69a71c2f5fd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 18 Dec 2025 12:49:54 -0600 Subject: [PATCH 714/919] updates for 53.07-r1 --- CMakeLists.txt | 6 +++--- library/xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ea49529bc83..33289b2c984 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.06") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DF_VERSION "53.07") +set(DFHACK_RELEASE "r1") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) diff --git a/library/xml b/library/xml index 40ac7ee39f6..58afa78efbd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 40ac7ee39f67c1de8bb63ef307e8218e3d3fd14a +Subproject commit 58afa78efbd6d5ca9790b9591db9df045a9020a5 From f8f665c6eca2d8a040f57f3b2d43a3059948e22a Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:14:19 +0000 Subject: [PATCH 715/919] Auto-update structures ref for 53.07 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 58afa78efbd..30c1a178365 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 58afa78efbd6d5ca9790b9591db9df045a9020a5 +Subproject commit 30c1a1783652a8d24fd6d24e2aa0aaf62ac98e4e From c0d6e5f823ff9b382307ba22dda2d0e480f07a3a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 18 Dec 2025 13:26:19 -0600 Subject: [PATCH 716/919] changelog for 53.07-r1 --- docs/changelog.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index baf28404bed..51a0d7fb5da 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,24 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.07-r1 + ## New Tools - ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. - `infinite-sky`: Re-enabled with compatibility with new siege map data. @@ -67,8 +85,8 @@ Template for new versions: ## Misc Improvements - `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars -- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. - The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. ## Documentation From b1c7abab5c52b54fca1cf137ed50df56741ae61a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 18 Dec 2025 13:27:43 -0600 Subject: [PATCH 717/919] update submodules --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 30c1a178365..0e1e5cc8537 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 30c1a1783652a8d24fd6d24e2aa0aaf62ac98e4e +Subproject commit 0e1e5cc8537cfb1a6580125dc5e0a2541e4db777 From 95fb7dd962f82203af6da1ed22e651aa812a8a05 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:42:19 +0000 Subject: [PATCH 718/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 578f6fe8fae..00d826f0eb8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 578f6fe8faed4d8e156787c25eadc39cdd0bc358 +Subproject commit 00d826f0eb81e837912c841438eba522149d93fc From 8319aa31acbe96204165e645048b1458046d03a6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 19 Dec 2025 10:51:03 -0600 Subject: [PATCH 719/919] update CMakeLists for 53.08-r1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 33289b2c984..6aafbe107fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.07") +set(DF_VERSION "53.08") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From f8d2b95d78c9c27584c8a08d1e4b4b2f95feffee Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:07:14 +0000 Subject: [PATCH 720/919] Auto-update structures ref for 53.08 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 0e1e5cc8537..e97044d60e6 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0e1e5cc8537cfb1a6580125dc5e0a2541e4db777 +Subproject commit e97044d60e6facf1d5e27be979e061573d78cbc5 From 43100e7fd48e765603e779bd1487dbaff4ab760c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:14:02 +0000 Subject: [PATCH 721/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index e97044d60e6..da0b52eb3ad 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e97044d60e6facf1d5e27be979e061573d78cbc5 +Subproject commit da0b52eb3ad79866b1228a880be3b734cfac7b55 From 67c591cfae28f766089de48412237e766ed3cd9f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 19 Dec 2025 11:28:13 -0600 Subject: [PATCH 722/919] add changelog for 53.08-r1 --- docs/changelog.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 51a0d7fb5da..2eb584644fc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,25 @@ Template for new versions: ## Removed +# 53.08-r1 + +## New Tools + +## New Features +- compatibility with DF 53.08 + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.07-r1 ## New Tools From c474f7555a9afff7a23e627e40385b3069011440 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 11:19:02 +0100 Subject: [PATCH 723/919] Remove order name generation from Lua and use DF button trick to get names --- library/LuaApi.cpp | 2 + library/include/modules/Job.h | 2 + library/modules/Job.cpp | 27 +++++++++++++ plugins/lua/orders.lua | 76 +---------------------------------- 4 files changed, 32 insertions(+), 75 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4875b934c67..d46da3fc912 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -92,6 +92,7 @@ distribution. #include "df/job_item.h" #include "df/job_material_category.h" #include "df/language_word_table.h" +#include "df/manager_order.h" #include "df/material.h" #include "df/map_block.h" #include "df/nemesis_record.h" @@ -2018,6 +2019,7 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = { WRAPM(Job,isSuitableItem), WRAPM(Job,isSuitableMaterial), WRAPM(Job,getName), + WRAPM(Job,getManagerOrderName), WRAPM(Job,linkIntoWorld), WRAPM(Job,removePostings), WRAPM(Job,disconnectJobItem), diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index 25c357bec7e..acb37169c3a 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -41,6 +41,7 @@ namespace df struct job_item_filter; struct building; struct unit; + struct manager_order; } namespace DFHack @@ -117,6 +118,7 @@ namespace DFHack int mat_index, df::item_type itype); DFHACK_EXPORT std::string getName(df::job *job); + DFHACK_EXPORT std::string getManagerOrderName(df::manager_order *order); struct JobDeleter { void operator()(df::job *ptr) const { diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 3c70807325c..91c16dd37a4 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -49,6 +49,7 @@ distribution. #include "df/job_list_link.h" #include "df/job_postingst.h" #include "df/job_restrictionst.h" +#include "df/manager_order.h" #include "df/plotinfost.h" #include "df/specific_ref.h" #include "df/unit.h" @@ -686,3 +687,29 @@ std::string Job::getName(df::job *job) return desc; } + +std::string Job::getManagerOrderName(df::manager_order *order) +{ + CHECK_NULL_POINTER(order); + + std::string desc; + auto button = df::allocate(); + button->mstring = order->reaction_name; + button->specdata.hist_figure_id = order->specdata.hist_figure_id; + button->jobtype = order->job_type; + button->itemtype = order->item_type; + button->subtype = order->item_subtype; + button->material = order->mat_type; + button->matgloss = order->mat_index; + button->specflag = order->specflag; + button->job_item_flag = order->material_category; + button->specdata = order->specdata; + button->art_specifier = order->art_spec.type; + button->art_specifier_id1 = order->art_spec.id; + button->art_specifier_id2 = order->art_spec.subid; + + button->text(&desc); + delete button; + + return desc; +} diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 5d96dcfc354..b78096d32e3 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -6,7 +6,6 @@ local overlay = require('plugins.overlay') local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') -local stockflow = reqscript('internal/quickfort/stockflow') -- -- OrdersOverlay @@ -719,77 +718,6 @@ local search_cursor_visible = false local search_last_scroll_position = -1 local order_count_at_highlight = 0 -local function make_order_key(order) - local mat_cat_str = '' - if order.material_category then - local keys = {} - for k in pairs(order.material_category) do - if type(k) == 'string' then - table.insert(keys, k) - end - end - table.sort(keys) - for _, k in ipairs(keys) do - mat_cat_str = mat_cat_str .. k .. '=' .. tostring(order.material_category[k]) .. ';' - end - end - - local encrust_str = '' - if order.specflag and order.specflag.encrust_flags then - local flags = {'finished_goods', 'furniture', 'ammo'} - for _, flag in ipairs(flags) do - if order.specflag.encrust_flags[flag] then - encrust_str = encrust_str .. flag .. ';' - end - end - end - - return string.format('%d:%d:%d:%d:%d:%s:%s:%s', - order.job_type, - order.item_type, - order.item_subtype, - order.mat_type, - order.mat_index, - order.reaction_name or '', - mat_cat_str, - encrust_str) -end - -local reaction_map_cache = nil - -local function get_cached_reaction_map() - if reaction_map_cache then - return reaction_map_cache - end - - local can_read_stockflow = dfhack.isWorldLoaded() and dfhack.isMapLoaded() - if not can_read_stockflow then - return nil - end - - local map = {} - local reactions = stockflow.collect_reactions() - - for _, reaction in ipairs(reactions) do - local key = make_order_key(reaction.order) - map[key] = reaction.name:lower() - df.delete(reaction.order) - end - reactions = nil - - reaction_map_cache = map - return reaction_map_cache -end - -local function get_order_search_key(order) - local reaction_map = get_cached_reaction_map() - if not reaction_map then - return nil - end - local key = make_order_key(order) - return reaction_map[key] -end - local function perform_search(text) local matches = {} @@ -800,7 +728,7 @@ local function perform_search(text) local orders = df.global.world.manager_orders.all for i = 0, #orders - 1 do local order = orders[i] - local search_key = get_order_search_key(order) + local search_key = dfhack.job.getManagerOrderName(order) if search_key and utils.search_text(search_key, text) then table.insert(matches, i) end @@ -819,8 +747,6 @@ OrdersSearchOverlay.ATTRS{ } function OrdersSearchOverlay:init() - get_cached_reaction_map() - local main_panel = widgets.Panel{ view_id='main_panel', frame={t=0, l=0, r=0, h=4}, From 99d01265965de91713e23719c7a038ecf35f0f0b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 12:19:50 +0100 Subject: [PATCH 724/919] Enable orders search overlay by default --- plugins/lua/orders.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index b78096d32e3..846027ffd54 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -741,7 +741,7 @@ OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', default_pos={x=85, y=-6}, - default_enabled=false, + default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=26, h=4}, } @@ -999,7 +999,7 @@ end OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) OrderHighlightOverlay.ATTRS{ desc='Shows arrows next to the work order found by orders.search', - default_enabled=false, + default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', full_interface=true, } From b9c82f68881e55758ea3ce8e9f9eb750da57fc54 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 30 Dec 2025 13:00:20 +0100 Subject: [PATCH 725/919] Move change in changelog to future --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 67890c55d0c..9ca880c3086 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators ## Fixes @@ -96,7 +97,6 @@ Template for new versions: - `infinite-sky`: Re-enabled with compatibility with new siege map data. ## New Features -- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status ## Fixes From 4b5be796af479619eb13419c18c9f760076bf2cc Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 17:55:41 +0100 Subject: [PATCH 726/919] Highlight all --- plugins/lua/orders.lua | 104 ++++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 846027ffd54..d6b78501541 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -717,6 +717,8 @@ end local search_cursor_visible = false local search_last_scroll_position = -1 local order_count_at_highlight = 0 +local search_matched_indices = {} +local search_current_match_idx = 0 local function perform_search(text) local matches = {} @@ -812,15 +814,12 @@ function OrdersSearchOverlay:init() minimized_panel, } - -- Initialize search state - self.matched_indices = {} - self.current_match_idx = 0 self.minimized = false end function OrdersSearchOverlay:update_filter(text) - self.matched_indices = perform_search(text) - self.current_match_idx = 0 + search_matched_indices = perform_search(text) + search_current_match_idx = 0 search_cursor_visible = false if text == '' then @@ -846,14 +845,14 @@ function OrdersSearchOverlay:cycle_match(direction) local new_matches = perform_search(search_text) if #new_matches == 0 then - self.matched_indices = {} - self.current_match_idx = 0 + search_matched_indices = {} + search_current_match_idx = 0 search_cursor_visible = false self.subviews.main_panel.frame_title = 'Search' return end - local new_match_idx = self.current_match_idx + direction + local new_match_idx = search_current_match_idx + direction if new_match_idx > #new_matches then new_match_idx = 1 @@ -861,11 +860,11 @@ function OrdersSearchOverlay:cycle_match(direction) new_match_idx = #new_matches end - self.matched_indices = new_matches - self.current_match_idx = new_match_idx + search_matched_indices = new_matches + search_current_match_idx = new_match_idx -- Scroll to the selected match - local order_idx = self.matched_indices[self.current_match_idx] + local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx search_last_scroll_position = order_idx search_cursor_visible = true @@ -875,21 +874,21 @@ function OrdersSearchOverlay:cycle_match(direction) end function OrdersSearchOverlay:get_match_text() - local total_matches = #self.matched_indices + local total_matches = #search_matched_indices if total_matches == 0 then return '' end - if self.current_match_idx == 0 then + if search_current_match_idx == 0 then return string.format(': %d matches', total_matches) end - return string.format(': %d of %d', self.current_match_idx, total_matches) + return string.format(': %d of %d', search_current_match_idx, total_matches) end function OrdersSearchOverlay:has_matches() - return #self.matched_indices > 0 + return #search_matched_indices > 0 end local function is_mouse_key(keys) @@ -969,31 +968,43 @@ local function getViewportSize() return math.floor(available_height / ORDER_HEIGHT) end -local function calculateSelectedOrderY() +local function getVisibleOrderIndices() local orders = df.global.world.manager_orders.all local scroll_pos = mi.info.work_orders.scroll_position_work_orders - if #orders == 0 or scroll_pos < 0 or scroll_pos >= #orders then - return nil - end + if #orders == 0 then return 0, -1 end - local list_start_y = getListStartY() local viewport_size = getViewportSize() - local viewport_start = scroll_pos local viewport_end = scroll_pos + viewport_size - 1 - -- Selected order tries to be at the top unless we're at the end of the list + -- Handle end-of-list case if viewport_end >= #orders then viewport_end = #orders - 1 viewport_start = math.max(0, viewport_end - viewport_size + 1) end - local pos_in_viewport = scroll_pos - viewport_start + return viewport_start, viewport_end +end + +local function calculateOrderY(order_idx) + local orders = df.global.world.manager_orders.all + + if #orders == 0 or order_idx < 0 or order_idx >= #orders then + return nil + end + + local viewport_start, viewport_end = getVisibleOrderIndices() + + -- Check if order is in viewport + if order_idx < viewport_start or order_idx > viewport_end then + return nil + end - local selected_y = list_start_y + (pos_in_viewport * ORDER_HEIGHT) + local list_start_y = getListStartY() + local pos_in_viewport = order_idx - viewport_start - return selected_y + return list_start_y + (pos_in_viewport * ORDER_HEIGHT) end OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) @@ -1024,18 +1035,37 @@ function OrderHighlightOverlay:render(dc) return end - -- Draw highlight arrows - local selected_y = calculateSelectedOrderY() - if selected_y then - local highlight_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, - bold=true, - } - - dc:seek(ARROW_X, selected_y):string('|', highlight_pen) - dc:seek(ARROW_X, selected_y + 1):string('>', highlight_pen) - dc:seek(ARROW_X, selected_y + 2):string('|', highlight_pen) + -- Draw highlight arrows for all matches in viewport + if #search_matched_indices == 0 then return end + + local selected_pen = dfhack.pen.parse{ + fg=COLOR_BLACK, + bg=COLOR_RED, + bold=true, + } + + local match_pen = dfhack.pen.parse{ + fg=COLOR_BLACK, + bg=COLOR_WHITE, + bold=true, + } + + -- Get the order index of the currently selected match + local selected_order_idx = search_current_match_idx > 0 and + search_matched_indices[search_current_match_idx] or nil + + -- Draw highlights for all matching orders in viewport + for _, match_order_idx in ipairs(search_matched_indices) do + local match_y = calculateOrderY(match_order_idx) + + if match_y then + -- Use red pen for selected match, white for others + local pen = (match_order_idx == selected_order_idx) and selected_pen or match_pen + + dc:seek(ARROW_X, match_y):string('|', pen) + dc:seek(ARROW_X, match_y + 1):string('>', pen) + dc:seek(ARROW_X, match_y + 2):string('|', pen) + end end end From 5cfb0c7a6b4fce18d9bdea8aa75a9303d5706ac0 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 18:14:03 +0100 Subject: [PATCH 727/919] Stop hiding on scroll --- plugins/lua/orders.lua | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index d6b78501541..135043562c7 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -715,7 +715,6 @@ end -- local search_cursor_visible = false -local search_last_scroll_position = -1 local order_count_at_highlight = 0 local search_matched_indices = {} local search_current_match_idx = 0 @@ -820,7 +819,13 @@ end function OrdersSearchOverlay:update_filter(text) search_matched_indices = perform_search(text) search_current_match_idx = 0 - search_cursor_visible = false + + if #search_matched_indices > 0 then + search_cursor_visible = true + order_count_at_highlight = #df.global.world.manager_orders.all + else + search_cursor_visible = false + end if text == '' then self.subviews.main_panel.frame_title = 'Search' @@ -866,7 +871,6 @@ function OrdersSearchOverlay:cycle_match(direction) -- Scroll to the selected match local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx - search_last_scroll_position = order_idx search_cursor_visible = true order_count_at_highlight = #df.global.world.manager_orders.all @@ -1020,16 +1024,8 @@ function OrderHighlightOverlay:render(dc) if mi.job_details.open or not search_cursor_visible then return end - local current_scroll = mi.info.work_orders.scroll_position_work_orders - local current_order_count = #df.global.world.manager_orders.all - - -- Hide cursor when user manually scrolls - if search_last_scroll_position ~= -1 and current_scroll ~= search_last_scroll_position then - search_cursor_visible = false - return - end - -- Hide cursor when order list changes (orders added or removed) + local current_order_count = #df.global.world.manager_orders.all if order_count_at_highlight ~= current_order_count then search_cursor_visible = false return @@ -1040,13 +1036,13 @@ function OrderHighlightOverlay:render(dc) local selected_pen = dfhack.pen.parse{ fg=COLOR_BLACK, - bg=COLOR_RED, + bg=COLOR_WHITE, bold=true, } local match_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, + fg=COLOR_WHITE, + bg=COLOR_BLACK, bold=true, } From 0a6378da7542d36c7559d58438cee63682b3ad93 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 19:08:33 +0100 Subject: [PATCH 728/919] Periodic orders change detection --- plugins/lua/orders.lua | 43 +++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 135043562c7..fe3f13736bc 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -715,9 +715,10 @@ end -- local search_cursor_visible = false -local order_count_at_highlight = 0 local search_matched_indices = {} local search_current_match_idx = 0 +local order_names_checksum = nil +local search_overlay_instance = nil local function perform_search(text) local matches = {} @@ -738,6 +739,19 @@ local function perform_search(text) return matches end +local function calculate_order_names_checksum() + local orders = df.global.world.manager_orders.all + if #orders == 0 then return "" end + + local names = {} + for i = 0, #orders - 1 do + local name = dfhack.job.getManagerOrderName(orders[i]) + table.insert(names, name or "") + end + + return table.concat(names, "|") +end + OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -814,15 +828,16 @@ function OrdersSearchOverlay:init() } self.minimized = false + search_overlay_instance = self end -function OrdersSearchOverlay:update_filter(text) +function OrdersSearchOverlay:update_filter() + local text = self.subviews.filter.text search_matched_indices = perform_search(text) search_current_match_idx = 0 if #search_matched_indices > 0 then search_cursor_visible = true - order_count_at_highlight = #df.global.world.manager_orders.all else search_cursor_visible = false end @@ -872,7 +887,6 @@ function OrdersSearchOverlay:cycle_match(direction) local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx search_cursor_visible = true - order_count_at_highlight = #df.global.world.manager_orders.all self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end @@ -953,6 +967,8 @@ local LIST_START_Y_ONE_TABS_ROW = 8 local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 +local CHECK_FRAME_INTERVAL = 50 +local check_frame_counter = 0 local function getListStartY() local rect = gui.get_interface_rect() @@ -1024,11 +1040,20 @@ function OrderHighlightOverlay:render(dc) if mi.job_details.open or not search_cursor_visible then return end - -- Hide cursor when order list changes (orders added or removed) - local current_order_count = #df.global.world.manager_orders.all - if order_count_at_highlight ~= current_order_count then - search_cursor_visible = false - return + -- Periodic check for order name changes + check_frame_counter = check_frame_counter + 1 + if check_frame_counter >= CHECK_FRAME_INTERVAL then + check_frame_counter = 0 + + local new_checksum = calculate_order_names_checksum() + if new_checksum ~= order_names_checksum then + order_names_checksum = new_checksum + + -- Auto re-run search if active + if search_overlay_instance and not search_overlay_instance.minimized then + search_overlay_instance:update_filter() + end + end end -- Draw highlight arrows for all matches in viewport From 9b1becbd70a5a3e63c22ebf423ea2b991efa751b Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 20:06:52 +0100 Subject: [PATCH 729/919] Remove unnecessary field assignments --- library/modules/Job.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 91c16dd37a4..cbcfebcc23f 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -695,7 +695,6 @@ std::string Job::getManagerOrderName(df::manager_order *order) std::string desc; auto button = df::allocate(); button->mstring = order->reaction_name; - button->specdata.hist_figure_id = order->specdata.hist_figure_id; button->jobtype = order->job_type; button->itemtype = order->item_type; button->subtype = order->item_subtype; @@ -704,7 +703,6 @@ std::string Job::getManagerOrderName(df::manager_order *order) button->specflag = order->specflag; button->job_item_flag = order->material_category; button->specdata = order->specdata; - button->art_specifier = order->art_spec.type; button->art_specifier_id1 = order->art_spec.id; button->art_specifier_id2 = order->art_spec.subid; From 2f1f78a15ad04b8a9c2aa004d68410d13f174d15 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 2 Jan 2026 20:10:17 +0100 Subject: [PATCH 730/919] Add documentation entry to lua api --- docs/dev/Lua API.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 3ba63890880..bb6f82748fe 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1430,6 +1430,10 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +* ``dfhack.job.getManagerOrderName(manager_order)`` + + Returns the manager order's description, as seen in the Work orders screen. + Hotkey module ------------- From 8c8d3fcbfa26a846852140bb2c47273c9ee2a674 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 13:38:03 +0100 Subject: [PATCH 731/919] Remove redundant search_cursor_visible variable --- plugins/lua/orders.lua | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index fe3f13736bc..061e421287a 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -714,7 +714,6 @@ end -- OrdersSearchOverlay -- -local search_cursor_visible = false local search_matched_indices = {} local search_current_match_idx = 0 local order_names_checksum = nil @@ -836,12 +835,6 @@ function OrdersSearchOverlay:update_filter() search_matched_indices = perform_search(text) search_current_match_idx = 0 - if #search_matched_indices > 0 then - search_cursor_visible = true - else - search_cursor_visible = false - end - if text == '' then self.subviews.main_panel.frame_title = 'Search' else @@ -867,7 +860,6 @@ function OrdersSearchOverlay:cycle_match(direction) if #new_matches == 0 then search_matched_indices = {} search_current_match_idx = 0 - search_cursor_visible = false self.subviews.main_panel.frame_title = 'Search' return end @@ -886,7 +878,6 @@ function OrdersSearchOverlay:cycle_match(direction) -- Scroll to the selected match local order_idx = search_matched_indices[search_current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx - search_cursor_visible = true self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end @@ -1038,7 +1029,7 @@ OrderHighlightOverlay.ATTRS{ function OrderHighlightOverlay:render(dc) OrderHighlightOverlay.super.render(self, dc) - if mi.job_details.open or not search_cursor_visible then return end + if mi.job_details.open or #search_matched_indices == 0 then return end -- Periodic check for order name changes check_frame_counter = check_frame_counter + 1 From f8e9af512676ebcab21b6d8f0e39cd3c9a6cc6bb Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 13:55:52 +0100 Subject: [PATCH 732/919] Move periodic order change detection to SearchOverlay using overlay_onupdate --- plugins/lua/orders.lua | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 061e421287a..05c47e1979c 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -717,7 +717,6 @@ end local search_matched_indices = {} local search_current_match_idx = 0 local order_names_checksum = nil -local search_overlay_instance = nil local function perform_search(text) local matches = {} @@ -758,6 +757,7 @@ OrdersSearchOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=26, h=4}, + overlay_onupdate_max_freq_seconds=1, } function OrdersSearchOverlay:init() @@ -827,7 +827,16 @@ function OrdersSearchOverlay:init() } self.minimized = false - search_overlay_instance = self +end + +function OrdersSearchOverlay:overlay_onupdate() + if self.minimized then return end + + local new_checksum = calculate_order_names_checksum() + if new_checksum ~= order_names_checksum then + order_names_checksum = new_checksum + self:update_filter() + end end function OrdersSearchOverlay:update_filter() @@ -958,8 +967,6 @@ local LIST_START_Y_ONE_TABS_ROW = 8 local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 -local CHECK_FRAME_INTERVAL = 50 -local check_frame_counter = 0 local function getListStartY() local rect = gui.get_interface_rect() @@ -1031,25 +1038,6 @@ function OrderHighlightOverlay:render(dc) if mi.job_details.open or #search_matched_indices == 0 then return end - -- Periodic check for order name changes - check_frame_counter = check_frame_counter + 1 - if check_frame_counter >= CHECK_FRAME_INTERVAL then - check_frame_counter = 0 - - local new_checksum = calculate_order_names_checksum() - if new_checksum ~= order_names_checksum then - order_names_checksum = new_checksum - - -- Auto re-run search if active - if search_overlay_instance and not search_overlay_instance.minimized then - search_overlay_instance:update_filter() - end - end - end - - -- Draw highlight arrows for all matches in viewport - if #search_matched_indices == 0 then return end - local selected_pen = dfhack.pen.parse{ fg=COLOR_BLACK, bg=COLOR_WHITE, From 3e2b7e2665d6302fc43215e8fc69b30071dcc925 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:16:08 +0100 Subject: [PATCH 733/919] Consolidate search and highlight into single overlay --- plugins/lua/orders.lua | 190 +++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 104 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 05c47e1979c..19295f5aef5 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -714,9 +714,12 @@ end -- OrdersSearchOverlay -- -local search_matched_indices = {} -local search_current_match_idx = 0 -local order_names_checksum = nil +local ORDER_HEIGHT = 3 +local TABS_WIDTH_THRESHOLD = 155 +local LIST_START_Y_ONE_TABS_ROW = 8 +local LIST_START_Y_TWO_TABS_ROWS = 10 +local BOTTOM_MARGIN = 9 +local ARROW_X = 10 local function perform_search(text) local matches = {} @@ -750,6 +753,63 @@ local function calculate_order_names_checksum() return table.concat(names, "|") end +local function getListStartY() + local rect = gui.get_interface_rect() + + if rect.width >= TABS_WIDTH_THRESHOLD then + return LIST_START_Y_ONE_TABS_ROW + else + return LIST_START_Y_TWO_TABS_ROWS + end +end + +local function getViewportSize() + local rect = gui.get_interface_rect() + local list_start_y = getListStartY() + + local available_height = rect.height - list_start_y - BOTTOM_MARGIN + return math.floor(available_height / ORDER_HEIGHT) +end + +local function getVisibleOrderIndices() + local orders = df.global.world.manager_orders.all + local scroll_pos = mi.info.work_orders.scroll_position_work_orders + + if #orders == 0 then return 0, -1 end + + local viewport_size = getViewportSize() + local viewport_start = scroll_pos + local viewport_end = scroll_pos + viewport_size - 1 + + -- Handle end-of-list case + if viewport_end >= #orders then + viewport_end = #orders - 1 + viewport_start = math.max(0, viewport_end - viewport_size + 1) + end + + return viewport_start, viewport_end +end + +local function calculateOrderY(order_idx) + local orders = df.global.world.manager_orders.all + + if #orders == 0 or order_idx < 0 or order_idx >= #orders then + return nil + end + + local viewport_start, viewport_end = getVisibleOrderIndices() + + -- Check if order is in viewport + if order_idx < viewport_start or order_idx > viewport_end then + return nil + end + + local list_start_y = getListStartY() + local pos_in_viewport = order_idx - viewport_start + + return list_start_y + (pos_in_viewport * ORDER_HEIGHT) +end + OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) OrdersSearchOverlay.ATTRS{ desc='Adds a search box to find and navigate to matching manager orders.', @@ -827,22 +887,25 @@ function OrdersSearchOverlay:init() } self.minimized = false + self.matched_indices = {} + self.current_match_idx = 0 + self.order_names_checksum = nil end function OrdersSearchOverlay:overlay_onupdate() if self.minimized then return end local new_checksum = calculate_order_names_checksum() - if new_checksum ~= order_names_checksum then - order_names_checksum = new_checksum + if new_checksum ~= self.order_names_checksum then + self.order_names_checksum = new_checksum self:update_filter() end end function OrdersSearchOverlay:update_filter() local text = self.subviews.filter.text - search_matched_indices = perform_search(text) - search_current_match_idx = 0 + self.matched_indices = perform_search(text) + self.current_match_idx = 0 if text == '' then self.subviews.main_panel.frame_title = 'Search' @@ -867,13 +930,13 @@ function OrdersSearchOverlay:cycle_match(direction) local new_matches = perform_search(search_text) if #new_matches == 0 then - search_matched_indices = {} - search_current_match_idx = 0 + self.matched_indices = {} + self.current_match_idx = 0 self.subviews.main_panel.frame_title = 'Search' return end - local new_match_idx = search_current_match_idx + direction + local new_match_idx = self.current_match_idx + direction if new_match_idx > #new_matches then new_match_idx = 1 @@ -881,32 +944,32 @@ function OrdersSearchOverlay:cycle_match(direction) new_match_idx = #new_matches end - search_matched_indices = new_matches - search_current_match_idx = new_match_idx + self.matched_indices = new_matches + self.current_match_idx = new_match_idx -- Scroll to the selected match - local order_idx = search_matched_indices[search_current_match_idx] + local order_idx = self.matched_indices[self.current_match_idx] mi.info.work_orders.scroll_position_work_orders = order_idx self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end function OrdersSearchOverlay:get_match_text() - local total_matches = #search_matched_indices + local total_matches = #self.matched_indices if total_matches == 0 then return '' end - if search_current_match_idx == 0 then + if self.current_match_idx == 0 then return string.format(': %d matches', total_matches) end - return string.format(': %d of %d', search_current_match_idx, total_matches) + return string.format(': %d of %d', self.current_match_idx, total_matches) end function OrdersSearchOverlay:has_matches() - return #search_matched_indices > 0 + return #self.matched_indices > 0 end local function is_mouse_key(keys) @@ -955,88 +1018,11 @@ end function OrdersSearchOverlay:render(dc) if mi.job_details.open then return end OrdersSearchOverlay.super.render(self, dc) + self:render_highlights(dc) end --- ------------------- --- OrderHighlightOverlay --- ------------------- - -local ORDER_HEIGHT = 3 -local TABS_WIDTH_THRESHOLD = 155 -local LIST_START_Y_ONE_TABS_ROW = 8 -local LIST_START_Y_TWO_TABS_ROWS = 10 -local BOTTOM_MARGIN = 9 -local ARROW_X = 10 - -local function getListStartY() - local rect = gui.get_interface_rect() - - if rect.width >= TABS_WIDTH_THRESHOLD then - return LIST_START_Y_ONE_TABS_ROW - else - return LIST_START_Y_TWO_TABS_ROWS - end -end - -local function getViewportSize() - local rect = gui.get_interface_rect() - local list_start_y = getListStartY() - - local available_height = rect.height - list_start_y - BOTTOM_MARGIN - return math.floor(available_height / ORDER_HEIGHT) -end - -local function getVisibleOrderIndices() - local orders = df.global.world.manager_orders.all - local scroll_pos = mi.info.work_orders.scroll_position_work_orders - - if #orders == 0 then return 0, -1 end - - local viewport_size = getViewportSize() - local viewport_start = scroll_pos - local viewport_end = scroll_pos + viewport_size - 1 - - -- Handle end-of-list case - if viewport_end >= #orders then - viewport_end = #orders - 1 - viewport_start = math.max(0, viewport_end - viewport_size + 1) - end - - return viewport_start, viewport_end -end - -local function calculateOrderY(order_idx) - local orders = df.global.world.manager_orders.all - - if #orders == 0 or order_idx < 0 or order_idx >= #orders then - return nil - end - - local viewport_start, viewport_end = getVisibleOrderIndices() - - -- Check if order is in viewport - if order_idx < viewport_start or order_idx > viewport_end then - return nil - end - - local list_start_y = getListStartY() - local pos_in_viewport = order_idx - viewport_start - - return list_start_y + (pos_in_viewport * ORDER_HEIGHT) -end - -OrderHighlightOverlay = defclass(OrderHighlightOverlay, overlay.OverlayWidget) -OrderHighlightOverlay.ATTRS{ - desc='Shows arrows next to the work order found by orders.search', - default_enabled=true, - viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - full_interface=true, -} - -function OrderHighlightOverlay:render(dc) - OrderHighlightOverlay.super.render(self, dc) - - if mi.job_details.open or #search_matched_indices == 0 then return end +function OrdersSearchOverlay:render_highlights(dc) + if #self.matched_indices == 0 then return end local selected_pen = dfhack.pen.parse{ fg=COLOR_BLACK, @@ -1050,16 +1036,13 @@ function OrderHighlightOverlay:render(dc) bold=true, } - -- Get the order index of the currently selected match - local selected_order_idx = search_current_match_idx > 0 and - search_matched_indices[search_current_match_idx] or nil + local selected_order_idx = self.current_match_idx > 0 and + self.matched_indices[self.current_match_idx] or nil - -- Draw highlights for all matching orders in viewport - for _, match_order_idx in ipairs(search_matched_indices) do + for _, match_order_idx in ipairs(self.matched_indices) do local match_y = calculateOrderY(match_order_idx) if match_y then - -- Use red pen for selected match, white for others local pen = (match_order_idx == selected_order_idx) and selected_pen or match_pen dc:seek(ARROW_X, match_y):string('|', pen) @@ -1075,7 +1058,6 @@ OVERLAY_WIDGETS = { recheck=RecheckOverlay, importexport=OrdersOverlay, search=OrdersSearchOverlay, - highlight=OrderHighlightOverlay, skillrestrictions=SkillRestrictionOverlay, laborrestrictions=LaborRestrictionsOverlay, conditionsrightclick=ConditionsRightClickOverlay, From e6cd89514c37832b2a4c95e4e884aa383ddd5df1 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:22:23 +0100 Subject: [PATCH 734/919] Rename checksum to concat_order_names and cached_order_names --- plugins/lua/orders.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 19295f5aef5..a64ce81684c 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -740,7 +740,7 @@ local function perform_search(text) return matches end -local function calculate_order_names_checksum() +local function concat_order_names() local orders = df.global.world.manager_orders.all if #orders == 0 then return "" end @@ -889,15 +889,15 @@ function OrdersSearchOverlay:init() self.minimized = false self.matched_indices = {} self.current_match_idx = 0 - self.order_names_checksum = nil + self.cached_order_names = nil end function OrdersSearchOverlay:overlay_onupdate() if self.minimized then return end - local new_checksum = calculate_order_names_checksum() - if new_checksum ~= self.order_names_checksum then - self.order_names_checksum = new_checksum + local current_order_names = concat_order_names() + if current_order_names ~= self.cached_order_names then + self.cached_order_names = current_order_names self:update_filter() end end From 0f7174dcdac9931cecf85dcf554b67ff9992787e Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:37:45 +0100 Subject: [PATCH 735/919] Hoist pens to module-level constants and inline has_matches --- plugins/lua/orders.lua | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index a64ce81684c..19e6e0d9bb3 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -721,6 +721,9 @@ local LIST_START_Y_TWO_TABS_ROWS = 10 local BOTTOM_MARGIN = 9 local ARROW_X = 10 +local SELECTED_PEN = dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE, bold=true} +local MATCH_PEN = dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_BLACK, bold=true} + local function perform_search(text) local matches = {} @@ -843,7 +846,7 @@ function OrdersSearchOverlay:init() key='CUSTOM_ALT_P', auto_width=true, on_activate=self:callback('cycle_match', -1), - enabled=function() return self:has_matches() end, + enabled=function() return #self.matched_indices > 0 end, }, widgets.HotkeyLabel{ frame={t=1, l=12}, @@ -851,7 +854,7 @@ function OrdersSearchOverlay:init() key='CUSTOM_ALT_N', auto_width=true, on_activate=self:callback('cycle_match', 1), - enabled=function() return self:has_matches() end, + enabled=function() return #self.matched_indices > 0 end, }, }, } @@ -968,10 +971,6 @@ function OrdersSearchOverlay:get_match_text() return string.format(': %d of %d', self.current_match_idx, total_matches) end -function OrdersSearchOverlay:has_matches() - return #self.matched_indices > 0 -end - local function is_mouse_key(keys) return keys._MOUSE_L or keys._MOUSE_R @@ -1024,18 +1023,6 @@ end function OrdersSearchOverlay:render_highlights(dc) if #self.matched_indices == 0 then return end - local selected_pen = dfhack.pen.parse{ - fg=COLOR_BLACK, - bg=COLOR_WHITE, - bold=true, - } - - local match_pen = dfhack.pen.parse{ - fg=COLOR_WHITE, - bg=COLOR_BLACK, - bold=true, - } - local selected_order_idx = self.current_match_idx > 0 and self.matched_indices[self.current_match_idx] or nil @@ -1043,7 +1030,7 @@ function OrdersSearchOverlay:render_highlights(dc) local match_y = calculateOrderY(match_order_idx) if match_y then - local pen = (match_order_idx == selected_order_idx) and selected_pen or match_pen + local pen = (match_order_idx == selected_order_idx) and SELECTED_PEN or MATCH_PEN dc:seek(ARROW_X, match_y):string('|', pen) dc:seek(ARROW_X, match_y + 1):string('>', pen) From 806ff7b1f3d35ba8ab8d0d9221f134509f444c5c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sun, 4 Jan 2026 14:48:56 +0100 Subject: [PATCH 736/919] Only scroll to selected match when outside visible viewport --- plugins/lua/orders.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 19e6e0d9bb3..80a6196e130 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -950,9 +950,12 @@ function OrdersSearchOverlay:cycle_match(direction) self.matched_indices = new_matches self.current_match_idx = new_match_idx - -- Scroll to the selected match + -- Scroll to the selected match only if not already visible local order_idx = self.matched_indices[self.current_match_idx] - mi.info.work_orders.scroll_position_work_orders = order_idx + local viewport_start, viewport_end = getVisibleOrderIndices() + if order_idx < viewport_start or order_idx > viewport_end then + mi.info.work_orders.scroll_position_work_orders = order_idx + end self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() end From 9560cef37f664929ea1fc5f486e78f16b1bd4311 Mon Sep 17 00:00:00 2001 From: plule <630159+plule@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:38:55 +0100 Subject: [PATCH 737/919] Fix the suspendmanager overlay appearing for followed units --- docs/changelog.txt | 1 + plugins/lua/suspendmanager.lua | 32 +++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2eb584644fc..cc0acf0217f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `suspendmanager`: Fix the overlay appearing where it should not when following a unit ## Misc Improvements diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index c15be19402b..a7de42e5a07 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -26,6 +26,25 @@ function isBuildingPlanJob(job) return suspendmanager_isBuildingPlanJob(job) end +--- Return the selected construction job +local function getSelectedBuildingJob() + -- This is not relying on dfhack.gui.getSelectedJob() because we don't want + -- the job of a selected or followed unit, only of a selected building + local building = dfhack.gui.getSelectedBuilding(true) + if not building then + return nil + end + + -- Find if the building is being constructed + for _, job in ipairs(building.jobs) do + if job.job_type == df.job_type.ConstructBuilding then + return job + end + end + + return nil +end + function runOnce(prevent_blocking, quiet, unsuspend_everything) suspendmanager_runOnce(prevent_blocking, unsuspend_everything) if (not quiet) then @@ -69,7 +88,7 @@ function StatusOverlay:init() end function StatusOverlay:get_status_string() - local job = dfhack.gui.getSelectedJob(true) + local job = getSelectedBuildingJob() if job and job.flags.suspend then return "Suspended because: " .. suspendmanager_suspensionDescription(job) .. "." end @@ -77,8 +96,11 @@ function StatusOverlay:get_status_string() end function StatusOverlay:render(dc) - local job = dfhack.gui.getSelectedJob(true) - if not job or job.job_type ~= df.job_type.ConstructBuilding or not isEnabled() or isBuildingPlanJob(job) then + if not isEnabled() then + return + end + local job = getSelectedBuildingJob() + if not job or isBuildingPlanJob(job) then return end StatusOverlay.super.render(self, dc) @@ -110,8 +132,8 @@ function ToggleOverlay:init() end function ToggleOverlay:shouldRender() - local job = dfhack.gui.getSelectedJob(true) - return job and job.job_type == df.job_type.ConstructBuilding and not isBuildingPlanJob(job) + local job = getSelectedBuildingJob() + return job and not isBuildingPlanJob(job) end function ToggleOverlay:render(dc) From ec0440ff8dac686e16bbe14fa9af23e72a702aa1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:58:32 +0000 Subject: [PATCH 738/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.35.0 → 0.36.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.35.0...0.36.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc38b7e9abe..5d77a667a4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.35.0 + rev: 0.36.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From fb0e17caa52098e980a93868a041cc7077962243 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 5 Jan 2026 14:10:22 -0600 Subject: [PATCH 739/919] change `Filesystem::as_string` to always use utf-8 our policy is that strings are to be encoded in utf-8 as much as possible. however, this function was using the system locale encoding, which was causing a transcoding error when a pathname was round-tripped through lua fixes #5688 --- docs/changelog.txt | 1 + library/include/modules/Filesystem.h | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2eb584644fc..1f4d389c732 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +# ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding ## Misc Improvements diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 407579926fd..68da9ec7f61 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -77,12 +77,13 @@ namespace DFHack { DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; inline std::string as_string(const std::filesystem::path path) noexcept { - auto pStr = path.string(); - if constexpr (std::filesystem::path::preferred_separator != '/') - { - std::ranges::replace(pStr, std::filesystem::path::preferred_separator, '/'); - } - return pStr; + // this just mashes the utf-8 into a std::string without any conversion + // this is largely because we use utf-8 everywhere internally as much as we can + // but we should ultimately convert to using u8strings for strings that are utf-8 + // and use a different string type for strings encoded in cp437 or in the locale codepage + std::u8string pstr = path.generic_u8string(); + return std::string((char*)pstr.c_str()); + } DFHACK_EXPORT std::filesystem::path getInstallDir() noexcept; DFHACK_EXPORT std::filesystem::path getBaseDir() noexcept; From a0f37eef886569ecba9eca8a15b4ff534bf825c2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:27:04 +0000 Subject: [PATCH 740/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index da0b52eb3ad..c9c7f4e5286 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit da0b52eb3ad79866b1228a880be3b734cfac7b55 +Subproject commit c9c7f4e52866c528f63a82c774aa602e48cf1dc7 diff --git a/scripts b/scripts index 00d826f0eb8..8359ac67595 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 00d826f0eb81e837912c841438eba522149d93fc +Subproject commit 8359ac67595ebdb2b83ba27225856834732b43f3 From d04b116626177a9d7ae50f6894e24abd7f16a9c9 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 6 Jan 2026 20:35:23 +0100 Subject: [PATCH 741/919] Add death cause button to dead/missing tab --- docs/changelog.txt | 1 + plugins/lua/sort.lua | 1 + plugins/lua/sort/deathcause_button.lua | 78 ++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 plugins/lua/sort/deathcause_button.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index 2eb584644fc..4dfb1419fe6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `sort`: Add death cause button to dead/missing tab in the creatures screen ## Fixes diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index bdad4ae90e8..a81bff5beb5 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1294,6 +1294,7 @@ OVERLAY_WIDGETS = { candidates=require('plugins.sort.info').CandidatesOverlay, interrogation=require('plugins.sort.info').InterrogationOverlay, conviction=require('plugins.sort.info').ConvictionOverlay, + deathcause_button=require('plugins.sort.deathcause_button').DeathCauseOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, -- TODO: maybe rewrite for 50.12 -- burrow_assignment=require('plugins.sort.unitselector').BurrowAssignmentOverlay, diff --git a/plugins/lua/sort/deathcause_button.lua b/plugins/lua/sort/deathcause_button.lua new file mode 100644 index 00000000000..e5f424c2a91 --- /dev/null +++ b/plugins/lua/sort/deathcause_button.lua @@ -0,0 +1,78 @@ +local _ENV = mkmodule('plugins.sort.deathcause_button') + +local dialogs = require('gui.dialogs') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +DeathCauseOverlay = defclass(DeathCauseOverlay, overlay.OverlayWidget) +DeathCauseOverlay.ATTRS{ + desc='Adds a button to view death cause on the dead/missing tab.', + default_pos={x=50, y=-7}, + default_enabled=true, + viewscreens='dwarfmode/Info/CREATURES/DECEASED', + frame={w=21, h=1}, +} + +function DeathCauseOverlay:init() + local deathcause = reqscript('deathcause') + + local function get_selected_unit() + -- Navigate to the creatures/deceased widget hierarchy: + -- list_widget - the main deceased list + -- ├─ children[0]: scrollbar widget + -- └─ children[1]: container widget (list_container) + -- ├─ grandchildren[0]: header + -- ├─ grandchildren[1]: header or other UI + -- └─ grandchildren[2]: scrollable rows container (scrollable_list) + -- └─ rows: row widgets (each row = one unit in the list) + -- └─ row.children[x]: unit widget + -- └─ unit_widget.u: pointer to the df.unit object + + local creatures = df.global.game.main_interface.info.creatures + local list_widget = dfhack.gui.getWidget(creatures, 'Tabs', 'Dead/Missing') + if not list_widget then return nil end + + local children = dfhack.gui.getWidgetChildren(list_widget) + local list_container = children[1] + local grandchildren = dfhack.gui.getWidgetChildren(list_container) + + local scrollable_list = grandchildren[2] + if not scrollable_list then + return nil + end + + local rows = dfhack.gui.getWidgetChildren(scrollable_list) + + local cursor_idx = list_widget.cursor_idx or 0 + + if cursor_idx >= 0 and cursor_idx < #rows then + local row = rows[cursor_idx + 1] + + local ok, unit = pcall(function() return dfhack.gui.getWidget(row, 0).u end) + if ok and unit then + return unit + end + end + + return nil + end + + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='Show death cause', + key='CUSTOM_D', + on_activate=function() + local unit = get_selected_unit() + if not unit then + dialogs.showMessage('Death Cause', 'No unit selected.') + return + end + local cause = deathcause.getDeathCause(unit) + dialogs.showMessage('Death Cause', dfhack.df2console(cause)) + end, + }, + } +end + +return _ENV From 9e916367761ca33cc6a1eabe08997710888a949c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 6 Jan 2026 20:54:12 +0100 Subject: [PATCH 742/919] Trim trailing whitespace --- plugins/lua/sort/deathcause_button.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/lua/sort/deathcause_button.lua b/plugins/lua/sort/deathcause_button.lua index e5f424c2a91..b149601a7a9 100644 --- a/plugins/lua/sort/deathcause_button.lua +++ b/plugins/lua/sort/deathcause_button.lua @@ -31,20 +31,20 @@ function DeathCauseOverlay:init() local creatures = df.global.game.main_interface.info.creatures local list_widget = dfhack.gui.getWidget(creatures, 'Tabs', 'Dead/Missing') if not list_widget then return nil end - + local children = dfhack.gui.getWidgetChildren(list_widget) local list_container = children[1] local grandchildren = dfhack.gui.getWidgetChildren(list_container) - + local scrollable_list = grandchildren[2] if not scrollable_list then return nil end - + local rows = dfhack.gui.getWidgetChildren(scrollable_list) - + local cursor_idx = list_widget.cursor_idx or 0 - + if cursor_idx >= 0 and cursor_idx < #rows then local row = rows[cursor_idx + 1] From 563262ae8b8b828f5094a7b318086eaa90f02935 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 10:59:57 -0600 Subject: [PATCH 743/919] Update build-windows.yml add jinja2 dependency for windows builds --- .github/workflows/build-windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 0864b716f97..4ad823e696b 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -71,6 +71,7 @@ jobs: - name: Install build dependencies run: | choco install sccache + pip install Jinja2 - name: Install doc dependencies if: inputs.docs run: | From 47eab6c1ad4013d6c1637f29cf4c22c8495a6dce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 11:16:52 -0600 Subject: [PATCH 744/919] update version to 53.09 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6aafbe107fc..cd9d70f567a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.08") +set(DF_VERSION "53.09") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 11f8580a07c0cddf74420035f82aa8122e7f154c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:18:00 +0000 Subject: [PATCH 745/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index c9c7f4e5286..cffd2fa02cd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit c9c7f4e52866c528f63a82c774aa602e48cf1dc7 +Subproject commit cffd2fa02cdd753843e5336ac140f9753a3fc9ce From 99a69c81770064baee1e19502f35cf735b5abe61 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:27:04 +0000 Subject: [PATCH 746/919] Auto-update structures ref for 53.09 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index cffd2fa02cd..2b9e59ea390 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit cffd2fa02cdd753843e5336ac140f9753a3fc9ce +Subproject commit 2b9e59ea390c9e6ee234c7da7fc7832f8ccb161b From 6e0ae2dfde9bc901f5ae4d08234087d083a87af4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 11:41:19 -0600 Subject: [PATCH 747/919] changelog for 53.09 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index e63253551f0..907e037428b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.09-r1 + +## New Tools + +## New Features + ## Fixes # ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding From 587a85c149dbc753a516888831da7f9a8f0f9216 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 11:57:14 -0600 Subject: [PATCH 748/919] FIx changelog.txt move one entry to the correct release --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 907e037428b..742fd54ecc1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -75,6 +75,7 @@ Template for new versions: ## New Tools ## New Features +- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes # ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding @@ -116,7 +117,6 @@ Template for new versions: ## New Features - `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status -- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit From 652b79254729a670ef7816751b2ca0e202a084e4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 7 Jan 2026 12:39:44 -0600 Subject: [PATCH 749/919] Update changelog.txt fix markup error --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 742fd54ecc1..da2aa1ef4ea 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -78,7 +78,7 @@ Template for new versions: - `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) ## Fixes -# ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding +- ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding ## Misc Improvements From 5e1fb2d5515bd52e20d63d8d7acbd2240bb927a5 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Wed, 7 Jan 2026 22:10:05 +0100 Subject: [PATCH 750/919] Add Uniformed filter to squad selection screen --- plugins/lua/sort.lua | 73 +++++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index bdad4ae90e8..19413be269d 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1029,12 +1029,29 @@ function SquadFilterOverlay:init() local left_panel = widgets.Panel{ view_id='left_panel', - frame={t=1, b=0, l=0, w=NARROW_WIDTH-4}, + frame={t=0, b=0, l=0, w=NARROW_WIDTH-4}, visible=true, subviews={ + widgets.HotkeyLabel{ + view_id='toggle_all', + frame={t=0, l=0}, + key='CUSTOM_SHIFT_A', + label='Toggle all', + on_activate=function() + local target = self.subviews.military:getOptionValue() == 'exclude' and 'include' or 'exclude' + self.subviews.military:setOption(target) + self.subviews.officials:setOption(target) + self.subviews.nobles:setOption(target) + self.subviews.infant:setOption(target) + self.subviews.unstable:setOption(target) + self.subviews.maimed:setOption(target) + self.subviews.labor_conflict:setOption(target) + poke_list() + end, + }, widgets.CycleHotkeyLabel{ view_id='military', - frame={t=0, l=0}, + frame={t=1, l=0}, key='CUSTOM_SHIFT_Q', label='Other squads:', options={ @@ -1047,7 +1064,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='officials', - frame={t=1, l=0}, + frame={t=2, l=0}, key='CUSTOM_SHIFT_O', label=' Officials:', options={ @@ -1060,7 +1077,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='nobles', - frame={t=2, l=0}, + frame={t=3, l=0}, key='CUSTOM_SHIFT_N', label=' Nobility:', options={ @@ -1076,12 +1093,25 @@ function SquadFilterOverlay:init() local right_panel = widgets.Panel{ view_id='right_panel', - frame={t=1, b=0, r=2, w=NARROW_WIDTH-4}, + frame={t=0, b=0, r=2, w=NARROW_WIDTH-4}, visible=false, subviews={ widgets.CycleHotkeyLabel{ - view_id='infant', + view_id='labor_conflict', frame={t=0, l=0}, + key='CUSTOM_SHIFT_U', + label=' Uniformed:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_LIGHTRED}, + }, + initial_option='include', + on_change=poke_list, + }, + widgets.CycleHotkeyLabel{ + view_id='infant', + frame={t=1, l=0}, key='CUSTOM_SHIFT_M', label='With infants:', options={ @@ -1094,7 +1124,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='unstable', - frame={t=1, l=0}, + frame={t=2, l=0}, key='CUSTOM_SHIFT_D', label='Hates combat:', options={ @@ -1107,7 +1137,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='maimed', - frame={t=2, l=0}, + frame={t=3, l=0}, key='CUSTOM_SHIFT_I', label=' Maimed:', options={ @@ -1125,21 +1155,6 @@ function SquadFilterOverlay:init() frame_style=gui.FRAME_MEDIUM, frame_background=gui.CLEAR_PEN, subviews={ - widgets.HotkeyLabel{ - frame={t=0, w=NARROW_WIDTH-3}, - key='CUSTOM_SHIFT_A', - label='Toggle all filters', - on_activate=function() - local target = self.subviews.military:getOptionValue() == 'exclude' and 'include' or 'exclude' - self.subviews.military:setOption(target) - self.subviews.officials:setOption(target) - self.subviews.nobles:setOption(target) - self.subviews.infant:setOption(target) - self.subviews.unstable:setOption(target) - self.subviews.maimed:setOption(target) - poke_list() - end, - }, left_panel, widgets.Label{ view_id='shifter', @@ -1167,9 +1182,8 @@ function SquadFilterOverlay:init() main_panel, widgets.Divider{ view_id='divider', - frame={l=NARROW_WIDTH-1, w=1, t=2}, + frame={l=NARROW_WIDTH-1, w=1, t=0}, frame_style=gui.FRAME_MEDIUM, - frame_style_t=false, visible=false, }, widgets.HelpButton{ @@ -1253,6 +1267,12 @@ local function is_maimed(unit) unit.status2.limbs_stand_count == 0 end +local function has_labor_conflict(unit) + return unit.status.labors[df.unit_labor.MINE] or + unit.status.labors[df.unit_labor.CUTWOOD] or + unit.status.labors[df.unit_labor.HUNT] +end + local function filter_matches(unit, filter) if filter.military == 'only' and not is_in_military(unit) then return false end if filter.military == 'exclude' and is_in_military(unit) then return false end @@ -1266,6 +1286,8 @@ local function filter_matches(unit, filter) if filter.unstable == 'exclude' and is_unstable(unit) then return false end if filter.maimed == 'only' and not is_maimed(unit) then return false end if filter.maimed == 'exclude' and is_maimed(unit) then return false end + if filter.labor_conflict == 'only' and not has_labor_conflict(unit) then return false end + if filter.labor_conflict == 'exclude' and has_labor_conflict(unit) then return false end return true end @@ -1281,6 +1303,7 @@ function do_squad_filter(unit) infant=self.subviews.infant:getOptionValue(), unstable=self.subviews.unstable:getOptionValue(), maimed=self.subviews.maimed:getOptionValue(), + labor_conflict=self.subviews.labor_conflict:getOptionValue(), } return filter_matches(unit, filter) end From 7daf2e7fa66d6ee9679b3a7ca1afdc0f8272da59 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Wed, 7 Jan 2026 22:12:44 +0100 Subject: [PATCH 751/919] Changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4ea..0786cbaa0db 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Tools ## New Features +- `sort`: added ``Uniformed`` filter to squad assignment screen to filter dwarves with mining, woodcutting, or hunting labors ## Fixes From 821123c558cca83d71b347cac147e96b319f95f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 8 Jan 2026 20:10:15 -0600 Subject: [PATCH 752/919] fix autochop report bug fix format string specification in `autochop` fixes #5701 --- docs/changelog.txt | 3 +++ docs/dev/Lua API.rst | 5 +++++ library/include/modules/Burrows.h | 2 ++ library/modules/Burrows.cpp | 7 +++++++ plugins/autochop.cpp | 5 ++--- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4ea..63ed45501ec 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,14 +59,17 @@ Template for new versions: ## New Features ## Fixes +- `autochop`: the report will no longer throw a C++ exception when burrows are defined. ## Misc Improvements ## Documentation ## API +- Added ``Burrows::getName``: obtains the name of a burrow, or the same placeholder name that DF would show if the burrow is unnamed. ## Lua +- Added ``Burrows::getName`` as ``dfhack.burrows.getName``. ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 3ba63890880..52fb7c778fc 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2489,6 +2489,11 @@ Maps module Burrows module -------------- +* ``dfhack.burrows.getName(burrow)`` + + Returns the name of the burrow. + If the burrow has no set name, returns the same placeholder name that DF would show in the UI. + * ``dfhack.burrows.findByName(name[, ignore_final_plus])`` Returns the burrow pointer or *nil*. if ``ignore_final_plus`` is ``true``, diff --git a/library/include/modules/Burrows.h b/library/include/modules/Burrows.h index b2e3e56b4ee..dbd2e51dd6b 100644 --- a/library/include/modules/Burrows.h +++ b/library/include/modules/Burrows.h @@ -45,6 +45,8 @@ namespace DFHack { namespace Burrows { + DFHACK_EXPORT std::string getName(df::burrow* burrow); + DFHACK_EXPORT df::burrow *findByName(std::string name, bool ignore_final_plus = false); // Units diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index 6c8802469c2..17ab0389d4b 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -52,6 +52,13 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; +std::string Burrows::getName(df::burrow* burrow) +{ + CHECK_NULL_POINTER(burrow); + return burrow->name.empty() ? fmt::format("Burrow {}", burrow->id + 1) : burrow->name; +} + + df::burrow *Burrows::findByName(std::string name, bool ignore_final_plus) { auto &vec = df::burrow::get_vector(); diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 6b98d0347a4..811a3d1cb08 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -669,9 +669,8 @@ static void autochop_printStatus(color_ostream &out) { for (auto &burrow : plotinfo->burrows.list) { name_width = std::max(name_width, (int)burrow->name.size()); } - name_width = -name_width; // left justify - constexpr auto fmt = "{:{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; + constexpr auto fmt = "{:<{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; out.print(fmt, "burrow name", name_width, " id ", "chop", "clearcut", "trees", "marked", "protect"); out.print(fmt, "-----------", name_width, "----", "----", "--------", "-----", "------", "-------"); @@ -689,7 +688,7 @@ static void autochop_printStatus(color_ostream &out) { protect_edible = c.get_bool(BURROW_CONFIG_PROTECT_EDIBLE); protect_cookable = c.get_bool(BURROW_CONFIG_PROTECT_COOKABLE); } - out.print(fmt, burrow->name, name_width, burrow->id, + out.print(fmt, Burrows::getName(burrow), name_width, burrow->id, chop ? "[x]" : "[ ]", clearcut ? "[x]" : "[ ]", tree_counts[burrow->id], designated_tree_counts[burrow->id], From c050add7bc472ec9e6daa8b0e5752120c20dc03d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 10 Jan 2026 12:57:04 +0100 Subject: [PATCH 753/919] Create plugin for clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. --- docs/changelog.txt | 1 + docs/plugins/logcleaner.rst | 62 ++++++++ plugins/CMakeLists.txt | 1 + plugins/logcleaner/logcleaner.cpp | 239 ++++++++++++++++++++++++++++++ plugins/lua/logcleaner.lua | 66 +++++++++ 5 files changed, 369 insertions(+) create mode 100644 docs/plugins/logcleaner.rst create mode 100644 plugins/logcleaner/logcleaner.cpp create mode 100644 plugins/lua/logcleaner.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4ea..377577da84b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. ## New Features diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst new file mode 100644 index 00000000000..d9d0930d82e --- /dev/null +++ b/docs/plugins/logcleaner.rst @@ -0,0 +1,62 @@ +logcleaner +========== +.. dfhack-tool:: + :summary: Automatically clear combat, sparring, and hunting reports. + :tags: fort auto units + +This plugin prevents spam from cluttering your announcement history and filling +the 3000-item reports buffer. It runs every 100 ticks and clears selected report +types from both the global reports buffer and per-unit logs. + +Usage +----- + +Basic commands +~~~~~~~~~~~~~~ + +``logcleaner`` + Show the current status of the plugin. +``logcleaner enable`` + Enable the plugin (persists per save). +``logcleaner disable`` + Disable the plugin. + +Configuring filters +~~~~~~~~~~~~~~~~~~~ + +``logcleaner combat`` + Clear combat reports (also enables the plugin if disabled). +``logcleaner sparring`` + Clear sparring reports. +``logcleaner hunting`` + Clear hunting reports. +``logcleaner combat,sparring`` + Clear multiple report types (comma-separated). +``logcleaner all`` + Enable all three filter types. +``logcleaner none`` + Disable all filter types. + +Examples +~~~~~~~~ + +Clear only sparring reports:: + + logcleaner sparring + +Clear combat and hunting, but not sparring:: + + logcleaner combat,hunting + +Overlay UI +---------- + +Run ``gui/logcleaner`` to open the settings overlay, or access it from the +control panel under the Gameplay tab. + +The overlay provides: + +- **Enable toggle**: Turn the plugin on or off (``Shift+E``) +- **Combat toggle**: Clear combat reports (``Shift+C``) +- **Sparring toggle**: Clear sparring reports (``Shift+S``) +- **Hunting toggle**: Clear hunting reports (``Shift+H``) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 355cc0ac4c0..4a4423f48f2 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -66,6 +66,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(createitem createitem.cpp) dfhack_plugin(cursecheck cursecheck.cpp) dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) + dfhack_plugin(logcleaner logcleaner/logcleaner.cpp LINK_LIBRARIES lua) dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(dig dig.cpp LINK_LIBRARIES lua) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp new file mode 100644 index 00000000000..b6be95a70e2 --- /dev/null +++ b/plugins/logcleaner/logcleaner.cpp @@ -0,0 +1,239 @@ +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "PluginLua.h" + +#include "modules/Persistence.h" +#include "modules/World.h" + +#include +#include +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("logcleaner"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CLEAR_COMBAT = 1, + CONFIG_CLEAR_SPARING = 2, + CONFIG_CLEAR_HUNTING = 3, +}; + +static bool clear_combat = false; +static bool clear_sparring = true; +static bool clear_hunting = false; + +namespace DFHack { + DBG_DECLARE(logcleaner, control, DebugCategory::LINFO); + DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO); +} + +static void cleanupLogs(color_ostream& out); +static command_result do_command(color_ostream& out, std::vector& params); +static void do_enable(); +static void do_disable(); + +// Getter functions for Lua +static bool logcleaner_getCombat() { return clear_combat; } +static bool logcleaner_getSparring() { return clear_sparring; } +static bool logcleaner_getHunting() { return clear_hunting; } + +// Setter functions for Lua (also persist to config) +static void logcleaner_setCombat(color_ostream& out, bool val) { + clear_combat = val; + config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); +} + +static void logcleaner_setSparring(color_ostream& out, bool val) { + clear_sparring = val; + config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); +} + +static void logcleaner_setHunting(color_ostream& out, bool val) { + clear_hunting = val; + config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); +} + +DFhackCExport command_result plugin_init(color_ostream& out, std::vector& commands) { + commands.push_back(PluginCommand( + plugin_name, + "Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).", + do_command)); + + return CR_OK; +} + +static command_result do_command(color_ostream& out, std::vector& params) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params, + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +static void do_enable() { +} + +static void do_disable() { +} + +DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control, out).print("{} from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_enable(); + else + do_disable(); + } else { + DEBUG(control, out).print("{} from the API, but already {}; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream& out) { + DEBUG(control, out).print("shutting down {}\n", plugin_name); + return CR_OK; +} + +DFhackCExport command_result plugin_load_site_data(color_ostream& out) { + config = World::GetPersistentSiteData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(control, out).print("no config found in this save; initializing\n"); + config = World::AddPersistentSiteData(CONFIG_KEY); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); + config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); + config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); + } + + is_enabled = config.get_bool(CONFIG_IS_ENABLED); + clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT); + clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); + clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); + + DEBUG(control, out).print("loading persisted enabled state: {}\n", + is_enabled ? "true" : "false"); + if (is_enabled) + do_enable(); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { + DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); + is_enabled = false; + do_disable(); + } + return CR_OK; +} + +static void cleanupLogs(color_ostream& out) { + if (!is_enabled || !world) + return; + + // Collect all report IDs from unit combat/sparring/hunting logs + std::unordered_set report_ids_to_remove; + + for (auto unit : world->units.all) { + // Combat logs (index 0) + if (clear_combat) { + auto& log = unit->reports.log[0]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + // Sparring logs (index 1) + if (clear_sparring) { + auto& log = unit->reports.log[1]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + // Hunting logs (index 2) + if (clear_hunting) { + auto& log = unit->reports.log[2]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + } + + if (report_ids_to_remove.empty()) + return; + + // Remove collected reports from global buffers + auto& reports = world->status.reports; + + int reports_erased = 0; + + for (auto report_id : report_ids_to_remove) { + df::report* report = df::report::find(report_id); + if (!report) + continue; + + auto it = std::find(reports.begin(), reports.end(), report); + if (it != reports.end()) { + delete report; + reports.erase(it); + reports_erased++; + } + } +} + +DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { + static int32_t tick_counter = 0; + + if (!is_enabled || !world) + return CR_OK; + + tick_counter++; + if (tick_counter >= 100) { + tick_counter = 0; + cleanupLogs(out); + } + + return CR_OK; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(logcleaner_getCombat), + DFHACK_LUA_FUNCTION(logcleaner_getSparring), + DFHACK_LUA_FUNCTION(logcleaner_getHunting), + DFHACK_LUA_FUNCTION(logcleaner_setCombat), + DFHACK_LUA_FUNCTION(logcleaner_setSparring), + DFHACK_LUA_FUNCTION(logcleaner_setHunting), + DFHACK_LUA_END +}; diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua new file mode 100644 index 00000000000..f7fa328ea62 --- /dev/null +++ b/plugins/lua/logcleaner.lua @@ -0,0 +1,66 @@ +local _ENV = mkmodule('plugins.logcleaner') + +local function print_status() + print(('logcleaner is %s'):format(isEnabled() and "enabled" or "disabled")) + print(' Combat: ' .. (logcleaner_getCombat() and 'enabled' or 'disabled')) + print(' Sparring: ' .. (logcleaner_getSparring() and 'enabled' or 'disabled')) + print(' Hunting: ' .. (logcleaner_getHunting() and 'enabled' or 'disabled')) +end + +function parse_commandline(...) + local args = {...} + local command = args[1] + + -- Show status if no command or "status" + if not command or command == 'status' then + print_status() + return true + end + + -- Start with all disabled, enable only what's specified + local new_combat, new_sparring, new_hunting = false, false, false + local has_filter = false + + for _, param in ipairs(args) do + if param == 'all' then + new_combat, new_sparring, new_hunting = true, true, true + has_filter = true + elseif param == 'none' then + new_combat, new_sparring, new_hunting = false, false, false + else + -- Split by comma for multiple options in one parameter + for token in param:gmatch('([^,]+)') do + if token == 'combat' then + new_combat = true + has_filter = true + elseif token == 'sparring' then + new_sparring = true + has_filter = true + elseif token == 'hunting' then + new_hunting = true + has_filter = true + else + dfhack.printerr('Unknown option: ' .. token) + return false + end + end + end + end + + -- Auto-enable plugin when filters are being configured + if has_filter and not isEnabled() then + dfhack.run_command('enable', 'logcleaner') + print('logcleaner enabled') + end + + logcleaner_setCombat(new_combat) + logcleaner_setSparring(new_sparring) + logcleaner_setHunting(new_hunting) + + print('Log cleaning config updated:') + print_status() + + return true +end + +return _ENV From 67b26213c5c7db21db12f93914c87e53b8150700 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 10 Jan 2026 15:08:31 +0100 Subject: [PATCH 754/919] Remove debugs, add enable / disable, refactor --- plugins/logcleaner/logcleaner.cpp | 72 +++++-------------------------- plugins/lua/logcleaner.lua | 63 +++++++++++++++++---------- 2 files changed, 52 insertions(+), 83 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index b6be95a70e2..56804613ad2 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -1,4 +1,3 @@ -#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" #include "PluginLua.h" @@ -32,15 +31,8 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -namespace DFHack { - DBG_DECLARE(logcleaner, control, DebugCategory::LINFO); - DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO); -} - static void cleanupLogs(color_ostream& out); static command_result do_command(color_ostream& out, std::vector& params); -static void do_enable(); -static void do_disable(); // Getter functions for Lua static bool logcleaner_getCombat() { return clear_combat; } @@ -48,17 +40,17 @@ static bool logcleaner_getSparring() { return clear_sparring; } static bool logcleaner_getHunting() { return clear_hunting; } // Setter functions for Lua (also persist to config) -static void logcleaner_setCombat(color_ostream& out, bool val) { +static void logcleaner_setCombat(bool val) { clear_combat = val; config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); } -static void logcleaner_setSparring(color_ostream& out, bool val) { +static void logcleaner_setSparring(bool val) { clear_sparring = val; config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); } -static void logcleaner_setHunting(color_ostream& out, bool val) { +static void logcleaner_setHunting(bool val) { clear_hunting = val; config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); } @@ -89,12 +81,6 @@ static command_result do_command(color_ostream& out, std::vector& p return show_help ? CR_WRONG_USAGE : CR_OK; } -static void do_enable() { -} - -static void do_disable() { -} - DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); @@ -103,23 +89,12 @@ DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(control, out).print("{} from the API; persisting\n", - is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); - if (enable) - do_enable(); - else - do_disable(); - } else { - DEBUG(control, out).print("{} from the API, but already {}; no action\n", - is_enabled ? "enabled" : "disabled", - is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream& out) { - DEBUG(control, out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -127,7 +102,6 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { config = World::GetPersistentSiteData(CONFIG_KEY); if (!config.isValid()) { - DEBUG(control, out).print("no config found in this save; initializing\n"); config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); @@ -140,19 +114,12 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); - DEBUG(control, out).print("loading persisted enabled state: {}\n", - is_enabled ? "true" : "false"); - if (is_enabled) - do_enable(); - return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; - do_disable(); } return CR_OK; } @@ -163,31 +130,17 @@ static void cleanupLogs(color_ostream& out) { // Collect all report IDs from unit combat/sparring/hunting logs std::unordered_set report_ids_to_remove; + bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; for (auto unit : world->units.all) { - // Combat logs (index 0) - if (clear_combat) { - auto& log = unit->reports.log[0]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); - } - // Sparring logs (index 1) - if (clear_sparring) { - auto& log = unit->reports.log[1]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); + for (int log_idx = 0; log_idx < 3; log_idx++) { + if (log_types[log_idx]) { + auto& log = unit->reports.log[log_idx]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); } - log.clear(); - } - // Hunting logs (index 2) - if (clear_hunting) { - auto& log = unit->reports.log[2]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); } } @@ -197,8 +150,6 @@ static void cleanupLogs(color_ostream& out) { // Remove collected reports from global buffers auto& reports = world->status.reports; - int reports_erased = 0; - for (auto report_id : report_ids_to_remove) { df::report* report = df::report::find(report_id); if (!report) @@ -208,7 +159,6 @@ static void cleanupLogs(color_ostream& out) { if (it != reports.end()) { delete report; reports.erase(it); - reports_erased++; } } } diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua index f7fa328ea62..104c23ab88f 100644 --- a/plugins/lua/logcleaner.lua +++ b/plugins/lua/logcleaner.lua @@ -17,32 +17,51 @@ function parse_commandline(...) return true end + -- Handle enable/disable commands + if command == 'enable' then + if isEnabled() then + print('logcleaner is already enabled') + else + dfhack.run_command('enable', 'logcleaner') + print('logcleaner enabled') + end + return true + end + + if command == 'disable' then + if not isEnabled() then + print('logcleaner is already disabled') + else + dfhack.run_command('disable', 'logcleaner') + print('logcleaner disabled') + end + return true + end + -- Start with all disabled, enable only what's specified local new_combat, new_sparring, new_hunting = false, false, false local has_filter = false - for _, param in ipairs(args) do - if param == 'all' then - new_combat, new_sparring, new_hunting = true, true, true - has_filter = true - elseif param == 'none' then - new_combat, new_sparring, new_hunting = false, false, false - else - -- Split by comma for multiple options in one parameter - for token in param:gmatch('([^,]+)') do - if token == 'combat' then - new_combat = true - has_filter = true - elseif token == 'sparring' then - new_sparring = true - has_filter = true - elseif token == 'hunting' then - new_hunting = true - has_filter = true - else - dfhack.printerr('Unknown option: ' .. token) - return false - end + if command == 'all' then + new_combat, new_sparring, new_hunting = true, true, true + has_filter = true + elseif command == 'none' then + new_combat, new_sparring, new_hunting = false, false, false + else + -- Split by comma for multiple options in one parameter + for token in command:gmatch('([^,]+)') do + if token == 'combat' then + new_combat = true + has_filter = true + elseif token == 'sparring' then + new_sparring = true + has_filter = true + elseif token == 'hunting' then + new_hunting = true + has_filter = true + else + dfhack.printerr('Unknown option: ' .. token) + return false end end end From 47e9d9b956facc5529cc50de245190b4e9cb2d73 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:52:55 +0000 Subject: [PATCH 755/919] Auto-update structures ref for 53.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 2b9e59ea390..3d1ee6456c7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 2b9e59ea390c9e6ee234c7da7fc7832f8ccb161b +Subproject commit 3d1ee6456c7739612c6f920f4e165866a299275b From b1185f65255f9b0b764b45a298c1c9a3bf5d24c2 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:55:40 +0000 Subject: [PATCH 756/919] Auto-update structures ref for 53.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 3d1ee6456c7..56511218a5b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3d1ee6456c7739612c6f920f4e165866a299275b +Subproject commit 56511218a5b1eb1cb494a22a7f5e9753cbf7cd87 From 281cfefd6436cebf8d02adf0e0ebaba7f52d811b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:59:53 +0000 Subject: [PATCH 757/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 8359ac67595..0b7816249ff 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8359ac67595ebdb2b83ba27225856834732b43f3 +Subproject commit 0b7816249ff0461c7d171e0cece44311c3015f61 From a62e75b4c99f03e5b34b6a52427300192a4f5afc Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:15:29 +0000 Subject: [PATCH 758/919] Auto-update structures ref for 53.10 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 56511218a5b..3826f45ef0f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 56511218a5b1eb1cb494a22a7f5e9753cbf7cd87 +Subproject commit 3826f45ef0fad7bd3357a6d55d5c9d28b56614c2 From 20c4278faf98f2ec2a1b8c1959ca95f4d4819452 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 12 Jan 2026 09:33:38 -0600 Subject: [PATCH 759/919] 53.10-r1 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd9d70f567a..ea1e6135564 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.09") +set(DF_VERSION "53.10") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) diff --git a/docs/changelog.txt b/docs/changelog.txt index 77478e3f32e..9481bbbbd83 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.10-r1 + +## New Tools + +## New Features + ## Fixes - `autochop`: the report will no longer throw a C++ exception when burrows are defined. - `suspendmanager`: Fix the overlay appearing where it should not when following a unit From 28129e3a23697f679d55bd7e6bd353c2e01279a9 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 12 Jan 2026 09:35:07 -0600 Subject: [PATCH 760/919] Update scripts --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 0b7816249ff..ff1b95a7b4e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 0b7816249ff0461c7d171e0cece44311c3015f61 +Subproject commit ff1b95a7b4e97be9a94218162c099dd95eaf4680 From 9c28ca8a5e6314f1dfda36a4a5d41b1960f4a75c Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Mon, 12 Jan 2026 17:46:06 +0100 Subject: [PATCH 761/919] Add constant for frequency. Remove unused variables --- plugins/logcleaner/logcleaner.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index 56804613ad2..fc00a9cb135 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -31,7 +31,9 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -static void cleanupLogs(color_ostream& out); +static const int32_t CLEANUP_TICK_INTERVAL = 97; + +static void cleanupLogs(); static command_result do_command(color_ostream& out, std::vector& params); // Getter functions for Lua @@ -124,7 +126,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_chan return CR_OK; } -static void cleanupLogs(color_ostream& out) { +static void cleanupLogs() { if (!is_enabled || !world) return; @@ -170,9 +172,9 @@ DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_ev return CR_OK; tick_counter++; - if (tick_counter >= 100) { + if (tick_counter >= CLEANUP_TICK_INTERVAL) { tick_counter = 0; - cleanupLogs(out); + cleanupLogs(); } return CR_OK; From e2cc7351f6288190a8136667a5f6e329f3c22062 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 16 Jan 2026 14:10:07 -0600 Subject: [PATCH 762/919] add vtable validation doReadClassName will now first validate that the vtable pointer points to mapped memory before attempting to read it, and throws an exception if it does not --- docs/changelog.txt | 1 + library/DataDefs.cpp | 2 +- library/Process.cpp | 17 +++++++++++++++++ library/include/MemAccess.h | 5 ++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9481bbbbd83..e1b0e51eec2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Core: DFHack now validates vtable pointers in objects read from memory and will throw an exception instead of crashing when an invalid vtable pointer is encountered. This makes it easier to identify which DF data structure contains corrupted data when this manifests in the form of a bad vtable pointer, and shifts blame for such crashes from DFHack to DF. ## Documentation diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 54cdfff982c..880a8b93a58 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -370,7 +370,7 @@ const virtual_identity *virtual_identity::find(void *vtable) // If using a reader/writer lock, re-grab as write here, and recheck Core &core = Core::getInstance(); - std::string name = core.p->doReadClassName(vtable); + std::string name = core.p->readClassName(vtable); auto name_it = (*name_lookup).find(name); if (name_it != (*name_lookup).end()) { diff --git a/library/Process.cpp b/library/Process.cpp index 3e8ae8de8d5..c60f3c8038c 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -236,6 +236,9 @@ Process::~Process() string Process::doReadClassName (void * vptr) { + if (!checkValidAddress(vptr)) + throw std::runtime_error(std::format("invalid vtable ptr {}", vptr)); + char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); #ifndef WIN32 char* typestring = Process::readPtr(rtti + sizeof(void*)); @@ -591,6 +594,20 @@ void Process::getMemRanges(vector& ranges) } #endif +bool Process::checkValidAddress(void* ptr) +{ + uintptr_t addr = reinterpret_cast(ptr); + auto validate = [&] (t_memrange& r) { + uintptr_t lo = reinterpret_cast(r.start); + uintptr_t hi = reinterpret_cast(r.end); + return addr >= lo && addr < hi; + }; + std::vector mr; + getMemRanges(mr); + bool valid = std::any_of(mr.begin(), mr.end(), validate); + return valid; +} + uintptr_t Process::getBase() { #if WIN32 diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 19d65468edb..5115348ba27 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -221,7 +221,7 @@ namespace DFHack std::string readClassName(void* vptr) { - std::map::iterator it = classNameCache.find(vptr); + auto it = classNameCache.find(vptr); if (it != classNameCache.end()) return it->second; return classNameCache[vptr] = doReadClassName(vptr); @@ -247,6 +247,9 @@ namespace DFHack /// get virtual memory ranges of the process (what is mapped where) static void getMemRanges(std::vector& ranges); + /// check if an address has a mapping + bool checkValidAddress(void* ptr); + /// get the symbol table extension of this process std::shared_ptr getDescriptor() { From 9020877ab253642727e10c018668e536c4686af0 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 16 Jan 2026 14:19:28 -0600 Subject: [PATCH 763/919] duh forgot to shift brain into dfhack gear --- library/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Process.cpp b/library/Process.cpp index c60f3c8038c..3e3ba6db805 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -237,7 +237,7 @@ Process::~Process() string Process::doReadClassName (void * vptr) { if (!checkValidAddress(vptr)) - throw std::runtime_error(std::format("invalid vtable ptr {}", vptr)); + throw std::runtime_error(fmt::format("invalid vtable ptr {}", vptr)); char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); #ifndef WIN32 From 09d3789689c92830c88a1cbbe4ee507f90e935e5 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:29:23 +0000 Subject: [PATCH 764/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 50190a34de7..7f8db9d9c12 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 50190a34de760732a4d8f926f28df9048fd0a5e2 +Subproject commit 7f8db9d9c1225a396678ca4057e9caaae267666f From 58af0b9d4cf064043d3ff2d8ae29508f5d007b42 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:29:27 +0000 Subject: [PATCH 765/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 7f8db9d9c12..efe712b62d5 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7f8db9d9c1225a396678ca4057e9caaae267666f +Subproject commit efe712b62d50b1f537558168701af8e6ed2bdece From 153da7d9a783728f9e2f71e50bdfd24807a3cb7b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:39:49 +0000 Subject: [PATCH 766/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.36.0 → 0.36.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.0...0.36.1) - [github.com/Lucas-C/pre-commit-hooks: v1.5.5 → v1.5.6](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.5...v1.5.6) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d77a667a4b..c47c9ccfdfd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.0 + rev: 0.36.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 + rev: v1.5.6 hooks: - id: forbid-tabs exclude_types: From edda03064e9769fd92c96d2595fdc98e2d9ef9b5 Mon Sep 17 00:00:00 2001 From: Quietust Date: Wed, 14 Jan 2026 17:11:20 -0600 Subject: [PATCH 767/919] Add library functions for placing spatters Required for #5703 --- docs/changelog.txt | 6 + docs/dev/Lua API.rst | 16 +++ library/LuaApi.cpp | 36 +++++ library/include/modules/Items.h | 6 + library/include/modules/Maps.h | 5 + library/modules/Items.cpp | 87 ++++++----- library/modules/Maps.cpp | 246 ++++++++++++++++++++++++++++++++ 7 files changed, 363 insertions(+), 39 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9481bbbbd83..b568d027cf5 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,8 +65,14 @@ Template for new versions: ## Documentation ## API +- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Items::pickGrowthPrint``: given a plant material and a growth index, returns the print variant corresponding to the current in-game time. +- Added ``Items::useStandardMaterial``: given an item type, returns true if the item is made of a specific material and false if it has a race and caste instead. ## Lua +- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. +- Added ``Maps::addItemSpatter`` as ``dfhack.maps.addItemSpatter``. ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 52fb7c778fc..4995e420c7a 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2486,6 +2486,22 @@ Maps module Removes an aquifer from the given tile position. Returns *true* or *false* depending on success. +* ``dfhack.maps.addMaterialSpatter(pos, mat, matg, state, amount)`` + + Adds a material spatter to the specified map tile. If the tile is already + full of that spatter, returns the amount left over. + + Specifying a state of -1 (None) will automatically choose either Solid, + Liquid, or Gas based on the material properties and the tile temperature. + +* ``dfhack.maps.addItemSpatter(pos, i_type, i_subtype, subcat1, subcat2, print_variant, amount)`` + + Adds an item spatter to the specified map tile. If the tile is already + full of that spatter, returns the amount left over. + + For plant growths, specifying a print_variant of -1 will automatically + choose an appropriate value. For other item types, this field is ignored. + Burrows module -------------- diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4875b934c67..ce2a8d2481a 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2781,6 +2781,40 @@ static int maps_removeTileAquifer(lua_State* L) return 1; } +static int maps_addMaterialSpatter(lua_State *L) +{ + int32_t rv; + df::coord pos; + + Lua::CheckDFAssign(L, &pos, 1); + int16_t mat = lua_tointeger(L, 2); + int32_t matg = lua_tointeger(L, 3); + df::matter_state state = (df::matter_state)lua_tointeger(L, 4); + int32_t amount = lua_tointeger(L, 5); + rv = Maps::addMaterialSpatter(pos, mat, matg, state, amount); + + lua_pushinteger(L, rv); + return 1; +} + +static int maps_addItemSpatter(lua_State *L) +{ + int32_t rv; + df::coord pos; + + Lua::CheckDFAssign(L, &pos, 1); + df::item_type i_type = (df::item_type)lua_tointeger(L, 2); + int16_t i_subtype = lua_tointeger(L, 3); + int16_t i_subcat1 = lua_tointeger(L, 4); + int32_t i_subcat2 = lua_tointeger(L, 5); + int32_t print_variant = lua_tointeger(L, 6); + int32_t amount = lua_tointeger(L, 7); + rv = Maps::addItemSpatter(pos, i_type, i_subtype, i_subcat1, i_subcat2, print_variant, amount); + + lua_pushinteger(L, rv); + return 1; +} + static const luaL_Reg dfhack_maps_funcs[] = { { "isValidTilePos", maps_isValidTilePos }, { "isTileVisible", maps_isTileVisible }, @@ -2796,6 +2830,8 @@ static const luaL_Reg dfhack_maps_funcs[] = { { "isTileHeavyAquifer", maps_isTileHeavyAquifer }, { "setTileAquifer", maps_setTileAquifer }, { "removeTileAquifer", maps_removeTileAquifer }, + { "addMaterialSpatter", maps_addMaterialSpatter }, + { "addItemSpatter", maps_addItemSpatter }, { NULL, NULL } }; diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index b4235df11d2..52af19e680c 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -170,6 +170,9 @@ DFHACK_EXPORT int getItemBaseValue(int16_t item_type, int16_t item_subtype, int1 // Gets the value of a specific item, taking into account civ values and trade agreements if a caravan is given. DFHACK_EXPORT int getValue(df::item *item, df::caravan_state *caravan = NULL); +// Automatically choose a growth print variant for the specified plant growth subtype+material +DFHACK_EXPORT int32_t pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg); + DFHACK_EXPORT bool createItem(std::vector &out_items, df::unit *creator, df::item_type type, int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false, int32_t count = 1); @@ -186,6 +189,9 @@ DFHACK_EXPORT bool markForTrade(df::item *item, df::building_tradedepotst *depot // Returns true if an active caravan will pay extra for the given item. DFHACK_EXPORT bool isRequestedTradeGood(df::item *item, df::caravan_state *caravan = NULL); +// DF standard_material_itemtype - returns true if item has material/matgloss, false if race+caste +DFHACK_EXPORT bool usesStandardMaterial(df::item_type item_type); + // Returns true if the item can currently be melted. If game_ui, then able to be marked is enough. DFHACK_EXPORT bool canMelt(df::item *item, bool game_ui = false); // Marks the item for melting. diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 052dbe3aab1..1e7eac89e16 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -38,6 +38,7 @@ distribution. #include "df/block_flags.h" #include "df/feature_type.h" #include "df/flow_type.h" +#include "df/matter_state.h" #include "df/tile_dig_designation.h" #include "df/tiletype.h" @@ -372,6 +373,10 @@ extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block, std::vector *priorities = 0 ); +// Add spatters at the specified location, returning the amount that couldn't be placed (e.g. due to overflow) +extern DFHACK_EXPORT int32_t addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df::matter_state state, int32_t amount); +extern DFHACK_EXPORT int32_t addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_subtype, int16_t i_subcat1, int32_t i_subcat2, int32_t print_variant, int32_t amount); + // Remove a block event from the block by address. extern DFHACK_EXPORT bool RemoveBlockEvent(int32_t x, int32_t y, int32_t z, df::block_square_event *which ); extern DFHACK_EXPORT bool RemoveBlockEvent(uint32_t x, uint32_t y, uint32_t z, df::block_square_event *which ); // TODO: deprecate me diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 6acd3b9401c..adce8ba8a6d 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1752,6 +1752,37 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { return value; } +// Automatically choose a growth print variant for the specified plant growth subtype+material +int32_t Items::pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg) +{ + int growth_print = -1; + // Make sure it's made of a valid plant material, then grab its definition + if (mat >= 419 && mat <= 618 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) + { + auto plant_def = world->raws.plants.all[matg]; + // Make sure it subtype is also valid + if (subtype >= 0 && (unsigned)subtype < plant_def->growths.size()) + { + auto growth_def = plant_def->growths[subtype]; + // Try and find a growth print matching the current time + // (in practice, only tree leaves use this for autumn color changes) + for (size_t i = 0; i < growth_def->prints.size(); i++) + { + auto print_def = growth_def->prints[i]; + if (print_def->timing_start <= *df::global::cur_year_tick && *df::global::cur_year_tick <= print_def->timing_end) + { + growth_print = i; + break; + } + } + // If we didn't find one, then pick the first one (if it exists) + if (growth_print == -1 && !growth_def->prints.empty()) + growth_print = 0; + } + } + return growth_print; +} + bool Items::createItem(vector &out_items, df::unit *unit, df::item_type item_type, int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor, int32_t count) { // Based on Quietust's plugins/createitem.cpp @@ -1802,34 +1833,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t for (auto out_item : out_items) { // Plant growths need a valid "growth print", otherwise they behave oddly if (auto growth = virtual_cast(out_item)) - { - int growth_print = -1; - // Make sure it's made of a valid plant material, then grab its definition - if (growth->mat_type >= 419 && growth->mat_type <= 618 && growth->mat_index >= 0 && (unsigned)growth->mat_index < world->raws.plants.all.size()) - { - auto plant_def = world->raws.plants.all[growth->mat_index]; - // Make sure it subtype is also valid - if (growth->subtype >= 0 && (unsigned)growth->subtype < plant_def->growths.size()) - { - auto growth_def = plant_def->growths[growth->subtype]; - // Try and find a growth print matching the current time - // (in practice, only tree leaves use this for autumn color changes) - for (size_t i = 0; i < growth_def->prints.size(); i++) - { - auto print_def = growth_def->prints[i]; - if (print_def->timing_start <= *df::global::cur_year_tick && *df::global::cur_year_tick <= print_def->timing_end) - { - growth_print = i; - break; - } - } - // If we didn't find one, then pick the first one (if it exists) - if (growth_print == -1 && !growth_def->prints.empty()) - growth_print = 0; - } - } - growth->growth_print = growth_print; - } + growth->growth_print = pickGrowthPrint(growth->subtype, growth->mat_type, growth->mat_index); if (!no_floor) out_item->moveToGround(pos.x, pos.y, pos.z); } @@ -1962,17 +1966,10 @@ bool Items::isRequestedTradeGood(df::item *item, df::caravan_state *caravan) { return false; } -/// When called with game_ui = true, this is equivalent to Bay12's itemst::meltable() -/// (i.e., returning true if and only if the item has a "designate for melting" button in game) -bool Items::canMelt(df::item *item, bool game_ui) { - CHECK_NULL_POINTER(item); - MaterialInfo mat(item); - if (mat.getCraftClass() != craft_material_class::Metal) - return false; - - switch(item->getType()) +bool Items::usesStandardMaterial(df::item_type item_type) +{ + switch(item_type) { using namespace df::enums::item_type; - // These are not meltable, even if made from metal case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1984,8 +1981,20 @@ bool Items::canMelt(df::item *item, bool game_ui) { case EGG: return false; default: - break; + return true; } +} + +/// When called with game_ui = true, this is equivalent to Bay12's itemst::meltable() +/// (i.e., returning true if and only if the item has a "designate for melting" button in game) +bool Items::canMelt(df::item *item, bool game_ui) { + CHECK_NULL_POINTER(item); + MaterialInfo mat(item); + if (mat.getCraftClass() != craft_material_class::Metal) + return false; + + if (!usesStandardMaterial(item->getType())) + return false; if (item->flags.bits.artifact) return false; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 7e5707fccbf..6f449dc2fc1 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -42,6 +42,9 @@ distribution. #include "df/block_burrow.h" #include "df/block_burrow_link.h" #include "df/block_square_event_grassst.h" +#include "df/block_square_event_item_spatterst.h" +#include "df/block_square_event_material_spatterst.h" +#include "df/block_square_event_spoorst.h" #include "df/building.h" #include "df/building_type.h" #include "df/builtin_mats.h" @@ -52,6 +55,7 @@ distribution. #include "df/flow_info.h" #include "df/map_block.h" #include "df/map_block_column.h" +#include "df/material.h" #include "df/plant.h" #include "df/plant_root_tile.h" #include "df/plant_tree_info.h" @@ -656,6 +660,248 @@ bool Maps::SortBlockEvents(df::map_block *block, return true; } +// Based on worldst::add_material_spatter_tile_capped +int32_t Maps::addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df::matter_state state, int32_t amount) +{ + // Hardcoded maximum + int32_t cap = 255; + + // Sanity checks + if (amount > cap) + amount = cap; + // DF doesn't handle negative numbers, so disallow them + if (amount < 0) + amount = 0; + + // DF rejects materials of NONE:* + if (mat == -1) + return amount; + + // Extra check: make sure the material correctly exists + MaterialInfo matinfo(mat, matg); + if (!matinfo.isValid()) + return amount; + + df::map_block *block = Maps::getTileBlock(pos); + if (!block) + return amount; + + int16_t bx = pos.x & 0xF, by = pos.y & 0xF; + + // Extra check: specify state == NONE to auto-pick based on tile temperature + // Note that this won't choose POWDER/PASTE/PRESSED + if (state == df::matter_state::None) + { + uint16_t tile = block->temperature_1[bx][by]; + uint16_t melt = matinfo.material->heat.melting_point; + uint16_t boil = matinfo.material->heat.boiling_point; + if (boil != 60001 && tile >= boil) + state = df::matter_state::Gas; + else if (melt != 60001 && tile >= melt) + state = df::matter_state::Liquid; + else + state = df::matter_state::Solid; + } + + if (amount > 0) + { + // scan all SPOOR events and clear the PRESENT flag if the type is HFID_COMBINEDCASTE_BP, ITEMT_ITEMST_ORIENT, or MESS + for (size_t i = 0; i < block->block_events.size(); i++) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::spoor) + continue; + auto spoor = (df::block_square_event_spoorst *)evt; + if (!spoor->info.flags[bx][by].bits.present) + continue; + if (spoor->info.type[bx][by] == df::spoor_type::HFID_COMBINEDCASTE_BP || + spoor->info.type[bx][by] == df::spoor_type::ITEMT_ITEMST_ORIENT || + spoor->info.type[bx][by] == df::spoor_type::MESS) + spoor->info.flags[bx][by].bits.present = false; + } + } + + // Find existing matching material spatter + df::block_square_event_material_spatterst *spatter = nullptr; + // DF: get_material_spatter_event_even_if_empty(...) + for (int i = block->block_events.size() - 1; i >= 0; i--) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::material_spatter) + continue; + auto spt = (df::block_square_event_material_spatterst *)evt; + if (spt->mat_type == mat && spt->mat_index == matg && + spt->mat_state == state) + { + spatter = spt; + break; + } + } + + // If we didn't find one, make a new one + if (!spatter) + { + spatter = df::allocate(); + spatter->mat_type = mat; + spatter->mat_index = matg; + spatter->mat_state = state; + memset(spatter->amount, 0, sizeof(spatter->amount)); + spatter->min_temperature = spatter->max_temperature = 60001; + + uint16_t melt = matinfo.material->heat.melting_point; + uint16_t boil = matinfo.material->heat.boiling_point; + + switch (state) + { using namespace df::enums::matter_state; + case Solid: + case Powder: + case Paste: + case Pressed: + if (melt != 60001) + boil = melt; + spatter->max_temperature = boil; + break; + case Liquid: + if (melt != 60001 && melt != 0) + spatter->min_temperature = melt - 1; + spatter->max_temperature = boil; + break; + // Can't really have gas spatters, but DF has this check + // presumably, DF could convert this into a flow + case Gas: + if (boil != 60001 && boil != 0) + spatter->min_temperature = boil - 1; + else if (melt != 60001 && melt != 0) + spatter->min_temperature = melt - 1; + break; + case None: + // impossible + break; + } + // DF doesn't check heatdam/colddam/ignite points here + block->block_events.push_back(spatter); + } + + int32_t newamount = spatter->amount[bx][by] + amount; + if (newamount > cap) + { + amount = newamount - cap; + newamount = cap; + } + else + amount = 0; + + spatter->amount[bx][by] = (uint8_t)newamount; + block->flags.bits.may_have_material_spatter = 1; + + return amount; +} + +// Based on worldst::add_item_spatter_tile_capped +int32_t Maps::addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_subtype, int16_t i_subcat1, int32_t i_subcat2, int32_t print_variant, int32_t amount) +{ + // DF passes this as a parameter, but it's always the same + int32_t cap = 10000; + + // Sanity checks + if (amount > cap) + amount = cap; + // DF doesn't handle negative numbers, so disallow them + if (amount < 0) + amount = 0; + + df::map_block *block = Maps::getTileBlock(pos); + if (!block) + return amount; + + int16_t bx = pos.x & 0xF, by = pos.y & 0xF; + + if (amount > 0) + { + // scan all SPOOR events and clear the PRESENT flag if the type is HFID_COMBINEDCASTE_BP, ITEMT_ITEMST_ORIENT, or MESS + for (size_t i = 0; i < block->block_events.size(); i++) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::spoor) + continue; + auto spoor = (df::block_square_event_spoorst *)evt; + if (!spoor->info.flags[bx][by].bits.present) + continue; + if (spoor->info.type[bx][by] == df::spoor_type::HFID_COMBINEDCASTE_BP || + spoor->info.type[bx][by] == df::spoor_type::ITEMT_ITEMST_ORIENT || + spoor->info.type[bx][by] == df::spoor_type::MESS) + spoor->info.flags[bx][by].bits.present = false; + } + } + + // Allow auto-selecting growth print for plant growths + if (i_type == df::item_type::PLANT_GROWTH && print_variant == -1) + print_variant = Items::pickGrowthPrint(i_subtype, i_subcat1, i_subcat2); + + // Find existing matching item spatter + df::block_square_event_item_spatterst *spatter = nullptr; + // DF: get_item_spatter_event_even_if_empty(...) + for (int i = block->block_events.size() - 1; i >= 0; i--) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::item_spatter) + continue; + auto spt = (df::block_square_event_item_spatterst *)evt; + if (spt->item_type == i_type && spt->item_subtype == i_subtype && + spt->mattype == i_subcat1 && spt->matindex == i_subcat2 && + spt->print_variant == print_variant) + { + spatter = spt; + break; + } + } + + // If we didn't find one, make a new one + if (!spatter) + { + spatter = df::allocate(); + spatter->item_type = i_type; + spatter->item_subtype = i_subtype; + spatter->mattype = i_subcat1; + spatter->matindex = i_subcat2; + spatter->print_variant = print_variant; + memset(spatter->amount, 0, sizeof(spatter->amount)); + memset(spatter->flag, 0, sizeof(spatter->flag)); + spatter->min_temperature = spatter->max_temperature = 60001; + + if (Items::usesStandardMaterial(i_type)) + { + MaterialInfo info(i_subcat1, i_subcat2); + if (info.isValid()) + { + uint16_t melt = info.material->heat.melting_point; + uint16_t boil = info.material->heat.melting_point; + if (melt != 60001) + spatter->max_temperature = melt; + else + spatter->max_temperature = boil; + // DF doesn't look at the heatdam/colddam/ignite temperatures + } + } + block->block_events.push_back(spatter); + } + + int32_t newamount = spatter->amount[bx][by] + amount; + if (newamount > cap) + { + amount = newamount - cap; + newamount = cap; + } + else + amount = 0; + + spatter->amount[bx][by] = newamount; + spatter->flag[bx][by].bits.season_full_timer = 7; + block->flags.bits.may_have_item_spatter = 1; + + return amount; +} + inline bool RemoveBlockEventInline(int32_t x, int32_t y, int32_t z, df::block_square_event * which) { df::map_block *block = Maps::getBlock(x, y, z); From a2e058bd0f4905ff321777cf0cbcc5af52094c03 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Feb 2026 00:59:29 -0600 Subject: [PATCH 768/919] add python setup to build-windows.yml so that jinja2 has somewhere to install to (may be necessary in the windows runner, unclear?) --- .github/workflows/build-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 4ad823e696b..25e42808dc2 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -68,6 +68,8 @@ jobs: name: Build win64 runs-on: windows-2022 steps: + - name: Set up Python + uses: actions/setup-python@v5 - name: Install build dependencies run: | choco install sccache From ef54ef2cc8d744125fcfe69c6d03d0eecf5fd2c4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Feb 2026 01:17:48 -0600 Subject: [PATCH 769/919] python: explicitly require 3.x (we're not that picky) --- .github/workflows/build-windows.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 25e42808dc2..1dec2111175 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -69,7 +69,9 @@ jobs: runs-on: windows-2022 steps: - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 + with: + python-version: '3.x' - name: Install build dependencies run: | choco install sccache From 92bd3642c51b3fe015fd2167530af12b6149728f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 6 Feb 2026 07:48:34 +0000 Subject: [PATCH 770/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 3826f45ef0f..8042f0a3574 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3826f45ef0fad7bd3357a6d55d5c9d28b56614c2 +Subproject commit 8042f0a357469d247d515893c8a01483c6c385c7 From 20240e17b4c208afad7d98d9b49b9f63ebc5cea0 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:57:25 +0000 Subject: [PATCH 771/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ff1b95a7b4e..ca71e41da49 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ff1b95a7b4e97be9a94218162c099dd95eaf4680 +Subproject commit ca71e41da49d6b7dbc767fab262a939b4172bfb6 From 59c900e5ae1a1cd3fa750e00d3ff627ba27bce75 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Mon, 9 Feb 2026 22:49:13 +0100 Subject: [PATCH 772/919] Add changelog entry in api section --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9ca880c3086..fe7bd82320f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## Documentation ## API +- `dfhack.job.getManagerOrderName()`: New function to get the display name of a manager order ## Lua From aa4d3962cdcf3e3b4e86bab107a6c7477dee8de3 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Mon, 9 Feb 2026 22:59:36 +0100 Subject: [PATCH 773/919] Fix changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7257bd62a1a..04321422d8b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -116,7 +116,7 @@ Template for new versions: ## Documentation ## API -- `dfhack.job.getManagerOrderName()`: New function to get the display name of a manager order +- ``dfhack.job.getManagerOrderName``: New function to get the display name of a manager order ## Lua From 7b2090017d9b338fb28efa737afecd57a86d2cee Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:58:45 +0000 Subject: [PATCH 774/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index ca71e41da49..1b9f8c61f76 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit ca71e41da49d6b7dbc767fab262a939b4172bfb6 +Subproject commit 1b9f8c61f7615bfa361da83ed49288b305b9d3a6 From c011d31bfafd565c03d9769516755e6e28c9b4af Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Tue, 10 Feb 2026 21:13:46 +0100 Subject: [PATCH 775/919] constExpr. erase_if --- plugins/logcleaner/logcleaner.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index fc00a9cb135..770bb2f26fc 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -31,7 +31,7 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -static const int32_t CLEANUP_TICK_INTERVAL = 97; +static constexpr int32_t CLEANUP_TICK_INTERVAL = 97; static void cleanupLogs(); static command_result do_command(color_ostream& out, std::vector& params); @@ -130,6 +130,9 @@ static void cleanupLogs() { if (!is_enabled || !world) return; + if (!clear_combat && !clear_sparring && !clear_hunting) + return; + // Collect all report IDs from unit combat/sparring/hunting logs std::unordered_set report_ids_to_remove; bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; @@ -152,17 +155,12 @@ static void cleanupLogs() { // Remove collected reports from global buffers auto& reports = world->status.reports; - for (auto report_id : report_ids_to_remove) { - df::report* report = df::report::find(report_id); - if (!report) - continue; - - auto it = std::find(reports.begin(), reports.end(), report); - if (it != reports.end()) { - delete report; - reports.erase(it); - } - } + std::erase_if(reports, [&](df::report* report) { + if (!report || !report_ids_to_remove.contains(report->id)) + return false; + delete report; + return true; + }); } DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { From bbfabe6ab00d8736bb97d349ac2f1951a6b8a329 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 11 Feb 2026 07:55:28 +0000 Subject: [PATCH 776/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 1b9f8c61f76..103e1b394f9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1b9f8c61f7615bfa361da83ed49288b305b9d3a6 +Subproject commit 103e1b394f9ba889b7909aeef45fa8281f59950b From 97e2fc5fcc6a1aae3f8a2e43c9f3c4baf6d4153a Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:51:07 +0000 Subject: [PATCH 777/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 103e1b394f9..16bd197d6ca 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 103e1b394f9ba889b7909aeef45fa8281f59950b +Subproject commit 16bd197d6ca5ca9adbe2f8cf0856448f3f5ac86b From 38ed2723d04baeb1b9f6b132a35c6bac77c88967 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Feb 2026 08:51:28 -0600 Subject: [PATCH 778/919] add covering default case to `Maps::SortBlockEvents` temporary - replace with `case block_square_event_type::NONE` after dfhack/df-structures#2293 is merged --- library/modules/Maps.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 6f449dc2fc1..11499a42701 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -655,6 +655,9 @@ bool Maps::SortBlockEvents(df::map_block *block, if (priorities) priorities->push_back((df::block_square_event_designation_priorityst *)evt); break; + default: + assert("Unhandled block event type" && false); // FIXME temporary - replace with NONE case after structure are updated + break; } } return true; From 36d0ace6ba809902d43293fe72d6612b5a4fe85b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:51:15 +0000 Subject: [PATCH 779/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index efe712b62d5..e86849a674a 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit efe712b62d50b1f537558168701af8e6ed2bdece +Subproject commit e86849a674a83ff7169330e65d18857c5dbe11cb From 380c0498fc452a47da975e8d0fd810ace83bb1f6 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:07:57 +0000 Subject: [PATCH 780/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index e86849a674a..2667acf1a8c 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit e86849a674a83ff7169330e65d18857c5dbe11cb +Subproject commit 2667acf1a8c7b2be8a37e2ecd68545f733094305 From c3ccb430c2cb7dd8e0e3f8ba546c029d3fbadb51 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:32:20 +0000 Subject: [PATCH 781/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 2667acf1a8c..f393d6bfc1d 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 2667acf1a8c7b2be8a37e2ecd68545f733094305 +Subproject commit f393d6bfc1d30aec6beae543d3cf6a04e700e821 From 3468c748624fe369d70f931c882fa4aa73691046 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:33:51 +0000 Subject: [PATCH 782/919] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 8042f0a3574..3100ca32f12 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8042f0a357469d247d515893c8a01483c6c385c7 +Subproject commit 3100ca32f12313bcd74437f381b72a14bf291b8d diff --git a/scripts b/scripts index 16bd197d6ca..e88bc68286d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 16bd197d6ca5ca9adbe2f8cf0856448f3f5ac86b +Subproject commit e88bc68286d6a15b86594822d114e21d197542d8 From 6d2b752f6b69780261bc88741a87ff5ef25d8aa2 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:20:55 +0000 Subject: [PATCH 783/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index f393d6bfc1d..a951081b3f9 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f393d6bfc1d30aec6beae543d3cf6a04e700e821 +Subproject commit a951081b3f9edce7704841a91f1b14bb13dc27a8 diff --git a/scripts b/scripts index e88bc68286d..32739c4c760 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e88bc68286d6a15b86594822d114e21d197542d8 +Subproject commit 32739c4c7607b75e6ef2fb18ddefec34b4191740 From 465833f0365ceb0e034a9b16e945ef0f72180e8a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 24 Feb 2026 11:17:40 -0600 Subject: [PATCH 784/919] up version to 53.10-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ea1e6135564..f0cbce0acb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.10") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From 8a682c84d246683338dfba6f38f257e38fedec15 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 Feb 2026 15:21:46 -0800 Subject: [PATCH 785/919] Properly delete announcements --- plugins/logcleaner/logcleaner.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index 770bb2f26fc..d9c0b8dce3c 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -158,6 +158,8 @@ static void cleanupLogs() { std::erase_if(reports, [&](df::report* report) { if (!report || !report_ids_to_remove.contains(report->id)) return false; + if (report->flags.bits.announcement) + erase_from_vector(world->status.announcements, &df::report::id, report->id); delete report; return true; }); From aee495573408594857201a27d51ef112828d50ef Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 1 Mar 2026 23:43:37 -0800 Subject: [PATCH 786/919] Fix logcleaner to use ticks --- plugins/logcleaner/logcleaner.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index d9c0b8dce3c..4b59a88d1e9 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -31,7 +31,8 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -static constexpr int32_t CLEANUP_TICK_INTERVAL = 97; +static constexpr int32_t CYCLE_TICKS = 97; +static int32_t cycle_timestamp = 0; static void cleanupLogs(); static command_result do_command(color_ostream& out, std::vector& params); @@ -116,6 +117,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); + cycle_timestamp = 0; return CR_OK; } @@ -166,16 +168,13 @@ static void cleanupLogs() { } DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { - static int32_t tick_counter = 0; - if (!is_enabled || !world) return CR_OK; + else if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) + return CR_OK; - tick_counter++; - if (tick_counter >= CLEANUP_TICK_INTERVAL) { - tick_counter = 0; - cleanupLogs(); - } + cycle_timestamp = world->frame_counter; + cleanupLogs(); return CR_OK; } From a12d60c432a2bbcc047577cfc6bbc5f8aa835679 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 1 Mar 2026 23:47:01 -0800 Subject: [PATCH 787/919] Update logcleaner.rst - not exactly 100 ticks --- docs/plugins/logcleaner.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst index d9d0930d82e..6e4cba07d6b 100644 --- a/docs/plugins/logcleaner.rst +++ b/docs/plugins/logcleaner.rst @@ -5,8 +5,8 @@ logcleaner :tags: fort auto units This plugin prevents spam from cluttering your announcement history and filling -the 3000-item reports buffer. It runs every 100 ticks and clears selected report -types from both the global reports buffer and per-unit logs. +the 3000-item reports buffer. It runs approximately every 100 ticks and clears +selected report types from both the global reports buffer and per-unit logs. Usage ----- From 3d808ccf0e3214fb937390ca298cf251afdf251d Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Mar 2026 00:21:07 -0800 Subject: [PATCH 788/919] Fix logcleaner and autolabor ticks offset by +1 * Update logcleaner.cpp * Update autolabor.cpp --- plugins/autolabor/autolabor.cpp | 2 +- plugins/logcleaner/logcleaner.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index d0cec796fba..4fd73ce1814 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -740,7 +740,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) return CR_OK; } - if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) return CR_OK; cycle_timestamp = world->frame_counter; diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index 4b59a88d1e9..89f84cf32f8 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -170,7 +170,7 @@ static void cleanupLogs() { DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { if (!is_enabled || !world) return CR_OK; - else if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) + else if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) return CR_OK; cycle_timestamp = world->frame_counter; From a5c5a87ae40620cdad9d686002acb7a11c31c87d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 06:00:02 -0600 Subject: [PATCH 789/919] revert change to autolabor out of scope for this PR --- plugins/autolabor/autolabor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 4fd73ce1814..a95d6636d48 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -1,4 +1,4 @@ -#include "laborstatemap.h" +g#include "laborstatemap.h" #include "Debug.h" #include "PluginManager.h" @@ -740,7 +740,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) return CR_OK; } - if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) return CR_OK; cycle_timestamp = world->frame_counter; From 80b4446849f1eed4452087200b05d16e466e19bd Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 06:03:27 -0600 Subject: [PATCH 790/919] gah remove line noise --- plugins/autolabor/autolabor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index a95d6636d48..d0cec796fba 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -1,4 +1,4 @@ -g#include "laborstatemap.h" +#include "laborstatemap.h" #include "Debug.h" #include "PluginManager.h" From 7c9ec563663fbdae3c4ddabb318736abe095ae13 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 07:03:25 -0600 Subject: [PATCH 791/919] cmakelist/changelog/submodules for 53.10-r2 --- CMakeLists.txt | 4 ++-- docs/changelog.txt | 24 +++++++++++++++++++++--- library/xml | 2 +- scripts | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f0cbce0acb0..36e18160c7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.10") -set(DFHACK_RELEASE "r2rc1") -set(DFHACK_PRERELEASE TRUE) +set(DFHACK_RELEASE "r2") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3d78dd83356..23204c72187 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,24 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.10-r2 + ## New Tools - ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. @@ -70,14 +88,14 @@ Template for new versions: ## Documentation ## API -- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. -- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. - Added ``Items::pickGrowthPrint``: given a plant material and a growth index, returns the print variant corresponding to the current in-game time. - Added ``Items::useStandardMaterial``: given an item type, returns true if the item is made of a specific material and false if it has a race and caste instead. +- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. ## Lua -- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. - Added ``Maps::addItemSpatter`` as ``dfhack.maps.addItemSpatter``. +- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. ## Removed diff --git a/library/xml b/library/xml index 3100ca32f12..b3364bb752f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 3100ca32f12313bcd74437f381b72a14bf291b8d +Subproject commit b3364bb752fce026a1e217cd1871125731d6224e diff --git a/scripts b/scripts index 32739c4c760..edf2d1f92f9 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 32739c4c7607b75e6ef2fb18ddefec34b4191740 +Subproject commit edf2d1f92f9b68cdef590d299a3a47690ce80442 From 23d425376a7b92f091370488c3f4c2882bc950b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:18:22 +0000 Subject: [PATCH 792/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.36.1 → 0.37.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.1...0.37.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c47c9ccfdfd..90b37493a7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.37.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 191e25296621271e9eb23bed3961dd68b8c52881 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 2 Mar 2026 18:49:39 -0600 Subject: [PATCH 793/919] add type identity support for `static-wstring` as an opaque type only --- library/DataIdentity.cpp | 2 ++ library/include/DataIdentity.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index 1bbeb64e77a..346565c62fe 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -33,6 +33,7 @@ namespace df { INTEGER_IDENTITY_TRAITS(unsigned long, "unsigned long"); INTEGER_IDENTITY_TRAITS(long long, "int64_t"); INTEGER_IDENTITY_TRAITS(unsigned long long, "uint64_t"); + INTEGER_IDENTITY_TRAITS(wchar_t, "wchar_t"); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); @@ -57,6 +58,7 @@ namespace df { OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(wchar_t*); const buffer_container_identity buffer_container_identity::base_instance; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index f8fd3c6fd7f..e58247fdaae 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -617,8 +617,10 @@ namespace df INTEGER_IDENTITY_TRAITS(unsigned long); INTEGER_IDENTITY_TRAITS(long long); INTEGER_IDENTITY_TRAITS(unsigned long long); + INTEGER_IDENTITY_TRAITS(wchar_t); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); + OPAQUE_IDENTITY_TRAITS(wchar_t*); OPAQUE_IDENTITY_TRAITS(std::condition_variable); OPAQUE_IDENTITY_TRAITS(std::fstream); OPAQUE_IDENTITY_TRAITS(std::mutex); From 9a81dc37f87ecfd2985e63461f4969b14b8beb69 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:43:40 +0000 Subject: [PATCH 794/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index b3364bb752f..228ee8d1364 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b3364bb752fce026a1e217cd1871125731d6224e +Subproject commit 228ee8d13642ac65f737837e31ed6b2142cb188c diff --git a/plugins/stonesense b/plugins/stonesense index a951081b3f9..4760027eee7 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit a951081b3f9edce7704841a91f1b14bb13dc27a8 +Subproject commit 4760027eee745d8c35cca843a2fcc46c21be326a diff --git a/scripts b/scripts index edf2d1f92f9..46da47f64ef 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit edf2d1f92f9b68cdef590d299a3a47690ce80442 +Subproject commit 46da47f64ef1c8e7d49e257b36c59963770b6d53 From 6f5af720424dece4d7da4abe10684250488f11cb Mon Sep 17 00:00:00 2001 From: Halavus Nenuli Date: Wed, 4 Mar 2026 04:57:32 +0100 Subject: [PATCH 795/919] Fix spelling of 'perseverance' in sort.lua changed e to a --- plugins/lua/sort.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 978e165354f..af066fb1758 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -293,7 +293,7 @@ local function get_mental_stability(unit) local emotionally_obsessive = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE local humor = unit.status.current_soul.personality.traits.HUMOR local love_propensity = unit.status.current_soul.personality.traits.LOVE_PROPENSITY - local perseverence = unit.status.current_soul.personality.traits.PERSEVERENCE + local perseverance = unit.status.current_soul.personality.traits.PERSEVERANCE local politeness = unit.status.current_soul.personality.traits.POLITENESS local privacy = unit.status.current_soul.personality.traits.PRIVACY local stress_vulnerability = unit.status.current_soul.personality.traits.STRESS_VULNERABILITY @@ -315,7 +315,7 @@ local function get_mental_stability(unit) + (anxiety_propensity * -0.06) + (bravery * 0.06) + (cheer_propensity * 0.41) + (curious * -0.06) + (discord * 0.14) + (dutifulness * -0.03) + (emotionally_obsessive * -0.13) - + (humor * -0.05) + (love_propensity * 0.15) + (perseverence * -0.07) + + (humor * -0.05) + (love_propensity * 0.15) + (perseverance * -0.07) + (politeness * -0.14) + (privacy * 0.03) + (stress_vulnerability * -0.20) + (tolerant * -0.11) From 3a2dfaa09e859ee6957ba162f4db64c3252163aa Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 3 Mar 2026 22:19:42 -0600 Subject: [PATCH 796/919] add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 23204c72187..96207045207 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features ## Fixes +- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen ## Misc Improvements From 5cd74c711bc5190653cd521c332279acba0596a5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Mar 2026 10:16:17 -0600 Subject: [PATCH 797/919] Update version to 53.11 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 36e18160c7a..9d6e55bd12f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.10") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.11") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 85cdf29e27ff0b0660fc52a4eb4c3b33109c864f Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:30:02 +0000 Subject: [PATCH 798/919] Auto-update structures ref for 53.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 228ee8d1364..491bec74402 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 228ee8d13642ac65f737837e31ed6b2142cb188c +Subproject commit 491bec744029a33abc021e893ffceac541fc89e2 From 012306d028582e01774f8529fb1fc887ff37979a Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:34:33 +0000 Subject: [PATCH 799/919] Auto-update structures ref for 53.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 491bec74402..228549676e0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 491bec744029a33abc021e893ffceac541fc89e2 +Subproject commit 228549676e0cc03746d6f23da66562a94f3bf59d From 7ab06c4ef523ff2322bb7ceab246036a8417a146 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:47:05 +0000 Subject: [PATCH 800/919] Auto-update structures ref for 53.11 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 228549676e0..435e433fd68 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 228549676e0cc03746d6f23da66562a94f3bf59d +Subproject commit 435e433fd686c842fd53868c1648938280afeea3 From ce08f11bd689454bb32aeba16adbb8a963e3f849 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Mar 2026 11:02:25 -0600 Subject: [PATCH 801/919] changelog for 53.11-r1 --- docs/changelog.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 96207045207..79db3273136 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,7 +33,6 @@ Template for new versions: ## New Features ## Fixes -- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen ## Misc Improvements @@ -71,6 +70,25 @@ Template for new versions: ## Removed +# 53.11-r1 + +## New Tools + +## New Features + +## Fixes +- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + # 53.10-r2 ## New Tools From 64587d365681afedc01a046f93b8eaa77ea14dd1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Mar 2026 11:02:39 -0600 Subject: [PATCH 802/919] update xml --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 435e433fd68..d7928837480 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 435e433fd686c842fd53868c1648938280afeea3 +Subproject commit d79288374802cc63b1507900030b32231ffd244b From f766031a2bd8d33815e991e98641fc430acbf662 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Mar 2026 18:59:10 -0600 Subject: [PATCH 803/919] fix windows console (#5737) always use utf-8 when writing to the Windows console --- docs/changelog.txt | 1 + library/MiscUtils.cpp | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 79db3273136..09878b51103 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- General: DFHack will unconditionally use UTF-8 for the console on Windows, now that DF forces the process effective system code page to 65001 during startup ## Documentation diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index 082657ea34a..dc1d4950ac2 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -653,20 +653,31 @@ std::string UTF2DF(const std::string &in) out.resize(pos); return out; } - -DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in) +static bool console_is_utf8() { - bool is_utf = false; #ifdef LINUX_BUILD + static bool checked = false; + static bool is_utf = false; + if (checked) + return is_utf; + std::string locale = ""; if (getenv("LANG")) locale += getenv("LANG"); if (getenv("LC_CTYPE")) locale += getenv("LC_CTYPE"); locale = toUpper_cp437(locale); - is_utf = (locale.find("UTF-8") != std::string::npos) || - (locale.find("UTF8") != std::string::npos); + is_utf = (locale.find("UTF-8") != std::string::npos) || (locale.find("UTF8") != std::string::npos); + checked = true; + return is_utf; +#else + return true; // Since DF 53.11, Windows console is always UTF-8 #endif +} + +DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in) +{ + bool is_utf = console_is_utf8(); return is_utf ? DF2UTF(in) : in; } From d1db2f79e6fce3e4eb8f180ae0a4415e827d6c8c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 5 Mar 2026 23:19:42 -0600 Subject: [PATCH 804/919] update for changed `manager_order` default constructor default-initialized value for `frequency` is now `NONE`, was previously `OneTime` --- docs/changelog.txt | 1 + plugins/autoclothing.cpp | 1 + plugins/autoslab.cpp | 1 + plugins/tailor.cpp | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 79db3273136..7174ed16493 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- `autoclothing`, `autoslab`, `tailor`: orders will no longer be created with a repetition frequency of ``NONE`` ## Misc Improvements diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index 89a45ee15f3..076892f9bd6 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -613,6 +613,7 @@ static void add_clothing_orders() { newOrder->material_category = clothingOrder.material_category; newOrder->amount_left = amount; newOrder->amount_total = amount; + newOrder->frequency = df::workquota_frequency_type::OneTime; world->manager_orders.all.push_back(newOrder); } } diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp index 08c4c837d1f..e8c06d575d9 100644 --- a/plugins/autoslab.cpp +++ b/plugins/autoslab.cpp @@ -141,6 +141,7 @@ static void createSlabJob(df::unit *unit) order->specdata.hist_figure_id = unit->hist_figure_id; order->amount_left = 1; order->amount_total = 1; + order->frequency = df::workquota_frequency_type::OneTime; world->manager_orders.all.push_back(order); } diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index efd90440240..1018999ec4b 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -663,6 +663,7 @@ class Tailor { order->amount_total = c; order->status.bits.validated = false; order->status.bits.active = false; + order->frequency = df::workquota_frequency_type::OneTime; order->id = world->manager_orders.manager_order_next_id++; world->manager_orders.all.push_back(order); From 1ba56e61ea46186c4c719498de58fd4bb5d5ab1d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:47:57 +0000 Subject: [PATCH 805/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index d7928837480..0f9e05ac64a 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit d79288374802cc63b1507900030b32231ffd244b +Subproject commit 0f9e05ac64a2ac15714172361c86f28ae9ebf821 From 850b8ac51d8faa72a501ec06ed70293ecda498f9 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:44:05 +0000 Subject: [PATCH 806/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 46da47f64ef..eb772d3b49f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 46da47f64ef1c8e7d49e257b36c59963770b6d53 +Subproject commit eb772d3b49f2019cf27ba5d6af5a3917dc285712 From c92ca2af021a752752bb3527660892aafc38df78 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 13 Mar 2026 09:10:26 -0500 Subject: [PATCH 807/919] Bump version to 53.11-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ library/xml | 2 +- scripts | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d6e55bd12f..93df1f3576c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index fdde2f1639d..bf2ca7d998b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,24 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.11-r2 + +## New Tools + +## New Features + ## Fixes - `autoclothing`, `autoslab`, `tailor`: orders will no longer be created with a repetition frequency of ``NONE`` diff --git a/library/xml b/library/xml index 0f9e05ac64a..114321c4802 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 0f9e05ac64a2ac15714172361c86f28ae9ebf821 +Subproject commit 114321c4802fb7e16e9d1092c8f2c057422a1c82 diff --git a/scripts b/scripts index eb772d3b49f..70460d491c0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit eb772d3b49f2019cf27ba5d6af5a3917dc285712 +Subproject commit 70460d491c0fc3e3cb03bc0907bf2fda381eaf42 From c2e2cae4c5e64751a29780ba7ed654c646b0ba92 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:46:07 +0000 Subject: [PATCH 808/919] Auto-update submodules depends/dfhooks: main --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 481dc1a12b1..4c48e25a2a3 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 481dc1a12b1264ef06ce95e331ef35cbfa0e6ace +Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 From 0b56dc8a8f1905965172b0a13b02ff3f0f690f56 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 22:26:23 -0500 Subject: [PATCH 809/919] replace hardcoded `hack` path with `getHackPath()` removes dependency on `hack` being in the DF CWD at runtime also use `std::filesystem::path` in `Textures` instead of `string` for pathnames --- library/include/modules/Textures.h | 2 +- library/lua/gui/textures.lua | 20 ++++++++++---------- library/modules/Textures.cpp | 6 +++--- plugins/dig.cpp | 2 +- plugins/lua/dig.lua | 2 +- plugins/lua/hotkeys.lua | 2 +- plugins/lua/suspendmanager.lua | 2 +- plugins/pathable.cpp | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index b820c3332d7..c2038c20b33 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -32,7 +32,7 @@ DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface, bool reserved = fal * Load tileset from image file. * Return vector of handles to obtain valid texposes. */ -DFHACK_EXPORT std::vector loadTileset(const std::string& file, +DFHACK_EXPORT std::vector loadTileset(const std::filesystem::path file, int tile_px_w = TILE_WIDTH_PX, int tile_px_h = TILE_HEIGHT_PX, bool reserved = false); diff --git a/library/lua/gui/textures.lua b/library/lua/gui/textures.lua index 44d4f0de30f..b97a10e439a 100644 --- a/library/lua/gui/textures.lua +++ b/library/lua/gui/textures.lua @@ -8,16 +8,16 @@ local _ENV = mkmodule('gui.textures') -- Use these handles if you need to get dfhack standard textures. ---@type table local texpos_handles = { - green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true), - red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12, true), - icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12, true), - on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true), - control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12, true), - border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12, true), - border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12, true), - border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12, true), - border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12, true), - border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12, true), + green_pin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/green-pin.png', 8, 12, true), + red_pin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/red-pin.png', 8, 12, true), + icons = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/icons.png', 8, 12, true), + on_off = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/on-off.png', 8, 12, true), + control_panel = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/control-panel.png', 8, 12, true), + border_thin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-thin.png', 8, 12, true), + border_medium = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-medium.png', 8, 12, true), + border_bold = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-bold.png', 8, 12, true), + border_panel = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-panel.png', 8, 12, true), + border_window = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-window.png', 8, 12, true), } -- Get valid texpos for preloaded texture in tileset diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 21642a9bbed..4a1e29aeab5 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -54,7 +54,7 @@ static ReservedRange reserved_range{}; static std::unordered_map g_handle_to_texpos; static std::unordered_map g_handle_to_reserved_texpos; static std::unordered_map g_handle_to_surface; -static std::unordered_map> g_tileset_to_handles; +static std::unordered_map> g_tileset_to_handles; static std::vector g_delayed_regs; static std::mutex g_adding_mutex; static std::atomic loading_state = false; @@ -195,14 +195,14 @@ TexposHandle Textures::loadTexture(SDL_Surface* surface, bool reserved) { return handle; } -std::vector Textures::loadTileset(const std::string& file, int tile_px_w, +std::vector Textures::loadTileset(const std::filesystem::path file, int tile_px_w, int tile_px_h, bool reserved) { if (g_tileset_to_handles.contains(file)) return g_tileset_to_handles[file]; if (!enabler) return std::vector{}; - SDL_Surface* surface = DFIMG_Load(file.c_str()); + SDL_Surface* surface = DFIMG_Load(file.string().c_str()); if (!surface) { ERR(textures).printerr("unable to load textures from '{}'\n", file); return std::vector{}; diff --git a/plugins/dig.cpp b/plugins/dig.cpp index 1be2a9a1169..5fbf23b251f 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -68,7 +68,7 @@ static bool is_painting_warm = false; static bool is_painting_damp = false; DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/damp_dig_map.png", 32, 32, true); + textures = Textures::loadTileset(Core::getInstance().getHackPath() / "data" / "art" / "damp_dig_map.png", 32, 32, true); commands.push_back(PluginCommand( "digv", diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua index babdb3b7ab5..192989d8a16 100644 --- a/plugins/lua/dig.lua +++ b/plugins/lua/dig.lua @@ -4,7 +4,7 @@ local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/damp_dig_toolbar.png', 8, 12, true) +local toolbar_textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/damp_dig_toolbar.png', 8, 12, true) local main_if = df.global.game.main_interface local selection_rect = df.global.selection_rect diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index d0cecb5ba01..5c04fba2df6 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -5,7 +5,7 @@ local helpdb = require('helpdb') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true) +local logo_textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/logo.png', 8, 12, true) local function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index a7de42e5a07..86f0504ed39 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -155,7 +155,7 @@ end -- suspend overlay (formerly in unsuspend.lua) -local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, 32, true) +local textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/unsuspend.png', 32, 32, true) local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index d5983bb0197..3fd1eaaad1a 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -42,7 +42,7 @@ namespace DFHack { static std::vector textures; DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32, true); + textures = Textures::loadTileset(Core::getInstance().getHackPath() / "data" / "art" / "pathable.png", 32, 32, true); return CR_OK; } From 5af6036e0804b535bb60061c629bf40ce5b11147 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:48:08 +0000 Subject: [PATCH 810/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 70460d491c0..56c934eb5d4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 70460d491c0fc3e3cb03bc0907bf2fda381eaf42 +Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c From 056dd45c1603da99e163bff64845a26370b3986e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Mar 2026 15:09:36 -0500 Subject: [PATCH 811/919] fix more instances of `hack` hardcoding also in some places switch to using pathnames instead of strings for paths --- library/Core.cpp | 14 +++++--------- library/Process.cpp | 2 +- library/VersionInfoFactory.cpp | 5 +++-- library/include/VersionInfoFactory.h | 3 ++- library/lua/helpdb.lua | 4 ++-- library/modules/DFSteam.cpp | 6 ++++-- plugins/orders.cpp | 9 ++++----- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index a5cd9966676..8979cb3eee0 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -124,7 +124,7 @@ namespace DFHack { static const std::filesystem::path getConfigDefaultsPath() { - return Filesystem::getInstallDir() / "hack" / "data" / "dfhack-config-defaults"; + return Core::getInstance().getHackPath() / "data" / "dfhack-config-defaults"; }; class MainThread { @@ -492,7 +492,7 @@ void Core::getScriptPaths(std::vector *dest) if (save.size()) dest->emplace_back(df_pref_path / "save" / save / "scripts"); } - dest->emplace_back(df_install_path / "hack" / "scripts"); + dest->emplace_back(getHackPath() / "scripts"); for (auto & path : script_paths[2]) dest->emplace_back(path); for (auto & path : script_paths[1]) @@ -1054,7 +1054,7 @@ void Core::fatal (std::string output, const char * title) std::filesystem::path Core::getHackPath() { - return p->getPath() / "hack"; + return Filesystem::get_initial_cwd() / "hack"; } df::viewscreen * Core::getTopViewscreen() { @@ -1099,16 +1099,12 @@ bool Core::InitMainThread() { } // find out what we are... - #ifdef LINUX_BUILD - const char * path = "hack/symbols.xml"; - #else - const char * path = "hack\\symbols.xml"; - #endif + std::filesystem::path symbols_path = getHackPath() / "symbols.xml"; auto local_vif = std::make_unique(); std::cerr << "Identifying DF version.\n"; try { - local_vif->loadFile(path); + local_vif->loadFile(symbols_path); } catch(Error::All & err) { diff --git a/library/Process.cpp b/library/Process.cpp index 3e3ba6db805..c45e5aea456 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -664,7 +664,7 @@ uint32_t Process::getTickCount() #endif /* WIN32 */ } -std::filesystem::path Process::getPath() +[[deprecated]] std::filesystem::path Process::getPath() { #if defined(WIN32) || !defined(_DARWIN) return Filesystem::get_initial_cwd(); diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index b4f6eefc782..94e2560e37d 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -29,6 +29,7 @@ distribution. #include #include #include +#include #include "VersionInfoFactory.h" #include "VersionInfo.h" @@ -226,9 +227,9 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) } // method // load the XML file with offsets -bool VersionInfoFactory::loadFile(string path_to_xml) +bool VersionInfoFactory::loadFile(std::filesystem::path path_to_xml) { - TiXmlDocument doc( path_to_xml.c_str() ); + TiXmlDocument doc( path_to_xml.string().c_str() ); std::cerr << "Loading " << path_to_xml << " ... "; //bool loadOkay = doc.LoadFile(); if (!doc.LoadFile()) diff --git a/library/include/VersionInfoFactory.h b/library/include/VersionInfoFactory.h index 060d622ecd9..92a5f94e103 100644 --- a/library/include/VersionInfoFactory.h +++ b/library/include/VersionInfoFactory.h @@ -26,6 +26,7 @@ distribution. #pragma once #include +#include #include "Export.h" @@ -38,7 +39,7 @@ namespace DFHack public: VersionInfoFactory(); ~VersionInfoFactory(); - bool loadFile( std::string path_to_xml); + bool loadFile( std::filesystem::path path_to_xml); bool isInErrorState() const {return error;}; std::shared_ptr getVersionInfoByMD5(std::string md5string) const; std::shared_ptr getVersionInfoByPETimestamp(uintptr_t timestamp) const; diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index 0bd64d29ccb..5af55f5697c 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -5,8 +5,8 @@ local _ENV = mkmodule('helpdb') local argparse = require('argparse') -- paths -local RENDERED_PATH = 'hack/docs/docs/tools/' -local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt' +local RENDERED_PATH = dfhack.getHackPath() .. '/docs/docs/tools/' +local TAG_DEFINITIONS = dfhack.getHackPath() .. '/docs/docs/Tags.txt' -- used when reading help text embedded in script sources local SCRIPT_DOC_BEGIN = '[====[' diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 10074eef294..b7453a0db57 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -157,7 +157,8 @@ static bool launchDFHack(color_ostream& out) { si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - static LPCWSTR procname = L"hack/launchdf.exe"; + auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; + LPCWSTR procname = procpath.wstring().c_str(); static const char * env = "\0"; // note that the environment must be explicitly zeroed out and not NULL, @@ -208,7 +209,8 @@ static bool launchDFHack(color_ostream& out) { return false; } else if (pid == 0) { // child process - static const char * command = "hack/launchdf"; + auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; + char * command = procpath.string().c_str(); unsetenv("SteamAppId"); execl(command, command, NULL); _exit(EXIT_FAILURE); diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 4bdff21fe24..95932acb520 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -44,8 +44,8 @@ DFHACK_PLUGIN("orders"); REQUIRE_GLOBAL(world); -static const std::string ORDERS_DIR = "dfhack-config/orders"; -static const std::string ORDERS_LIBRARY_DIR = "hack/data/orders"; +static std::filesystem::path ORDERS_DIR = std::filesystem::path("dfhack-config") / "orders"; +static std::filesystem::path ORDERS_LIBRARY_DIR = Core::getInstance().getHackPath() / "data" / "orders"; static command_result orders_command(color_ostream & out, std::vector & parameters); @@ -506,7 +506,7 @@ static command_result orders_export_command(color_ostream & out, const std::stri Filesystem::mkdir(ORDERS_DIR); - std::ofstream file(ORDERS_DIR + "/" + name + ".json"); + std::ofstream file(ORDERS_DIR / ( name + ".json")); file << orders << std::endl; @@ -924,8 +924,7 @@ static command_result orders_import_command(color_ostream & out, const std::stri return CR_WRONG_USAGE; } - const std::string filename((is_library ? ORDERS_LIBRARY_DIR : ORDERS_DIR) + - "/" + fname + ".json"); + auto filename((is_library ? ORDERS_LIBRARY_DIR : ORDERS_DIR) / (fname + ".json")); Json::Value orders; { From d959609a64e5d5c281dd956cc6e53add461f4a14 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Mar 2026 15:15:02 -0500 Subject: [PATCH 812/919] fix typo --- library/modules/DFSteam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index b7453a0db57..7e42414c729 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -209,7 +209,7 @@ static bool launchDFHack(color_ostream& out) { return false; } else if (pid == 0) { // child process - auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; + auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; char * command = procpath.string().c_str(); unsetenv("SteamAppId"); execl(command, command, NULL); From 8afcf1b5141333c41b95ada60d3dc9314b4d8f2b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 17 Mar 2026 15:22:52 -0500 Subject: [PATCH 813/919] dangle me not --- library/modules/DFSteam.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 7e42414c729..400c489232a 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -157,14 +157,13 @@ static bool launchDFHack(color_ostream& out) { si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - auto procpath = Core.getInstance().getHackPath() / "launchdf.exe"; - LPCWSTR procname = procpath.wstring().c_str(); + auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; static const char * env = "\0"; // note that the environment must be explicitly zeroed out and not NULL, // otherwise the launched process will inherit this process's environment, // and the Steam API in the launchdf process will think it is in DF's context. - BOOL res = CreateProcessW(procname, + BOOL res = CreateProcessW(procpath.wstring().c_str(), NULL, NULL, NULL, FALSE, 0, (LPVOID)env, NULL, &si, &pi); return !!res; @@ -210,9 +209,9 @@ static bool launchDFHack(color_ostream& out) { } else if (pid == 0) { // child process auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; - char * command = procpath.string().c_str(); + auto command = procpath.string(); unsetenv("SteamAppId"); - execl(command, command, NULL); + execl(command.c_str(), command.c_str(), NULL); _exit(EXIT_FAILURE); } From 6efba41ebbc6af5e840d85b561ac3cd0a4bc854d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 24 Mar 2026 08:45:59 -0500 Subject: [PATCH 814/919] correct executable name on linux --- library/modules/DFSteam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 400c489232a..61e50a61b8e 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -208,7 +208,7 @@ static bool launchDFHack(color_ostream& out) { return false; } else if (pid == 0) { // child process - auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; + auto procpath = Core::getInstance().getHackPath() / "launchdf"; auto command = procpath.string(); unsetenv("SteamAppId"); execl(command.c_str(), command.c_str(), NULL); From 93c91007da87dc9433f733b9080b7541be6b6e73 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:44:01 +0000 Subject: [PATCH 815/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 56c934eb5d4..b672e4afc62 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c +Subproject commit b672e4afc6225d4e5e414beeb13f530b8cf66b39 From 97c7eecb272aa8a625aef81d741b6cc8f1e3681c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 14 Mar 2026 08:16:18 -0500 Subject: [PATCH 816/919] split dfhack.dll out of dfhooks_dfhack.dll removes disparallelism with linux, and makes dealing with cross-folder hooking mechanics easier --- library/CMakeLists.txt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index b405469b0e8..3982a1a37c8 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -318,8 +318,6 @@ endif() # Compilation -add_definitions(-DBUILD_DFHACK_LIB) - if(UNIX) if(CONSOLE_NO_CATCH) add_definitions(-DCONSOLE_NO_CATCH) @@ -373,6 +371,7 @@ if(EXISTS ${dfhack_SOURCE_DIR}/.git/index AND EXISTS ${dfhack_SOURCE_DIR}/.git/m endif() add_library(dfhack SHARED ${PROJECT_SOURCES}) +target_compile_definitions(dfhack PRIVATE BUILD_DFHACK_LIB) target_include_directories(dfhack PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/proto) get_target_property(xlsxio_INCLUDES xlsxio_read_STATIC INTERFACE_INCLUDE_DIRECTORIES) @@ -381,6 +380,7 @@ add_dependencies(dfhack generate_proto_core) add_dependencies(dfhack generate_headers) add_library(dfhack-client SHARED RemoteClient.cpp ColorText.cpp MiscUtils.cpp Error.cpp ${PROJECT_PROTO_SRCS} ${CONSOLE_SOURCES}) +target_compile_definitions(dfhack-client PRIVATE BUILD_DFHACK_LIB) target_include_directories(dfhack-client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/proto) add_dependencies(dfhack-client dfhack) @@ -391,16 +391,16 @@ add_executable(binpatch binpatch.cpp) target_link_libraries(binpatch dfhack-md5) if(WIN32) - set_target_properties(dfhack PROPERTIES OUTPUT_NAME "dfhooks_dfhack" ) set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) else() set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "-include Export.h" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "-include Export.h" ) - add_library(dfhooks_dfhack SHARED Hooks.cpp) - target_link_libraries(dfhooks_dfhack dfhack ${FMTLIB}) endif() +add_library(dfhooks_dfhack SHARED Hooks.cpp) +target_link_libraries(dfhooks_dfhack PUBLIC dfhack ${FMTLIB}) + # effectively disables debug builds... set_target_properties(dfhack PROPERTIES DEBUG_POSTFIX "-debug" ) @@ -450,10 +450,11 @@ if(UNIX) install(PROGRAMS ${dfhack_SOURCE_DIR}/package/linux/dfhack-run DESTINATION .) endif() - install(TARGETS dfhooks_dfhack +endif() + +install(TARGETS dfhooks_dfhack LIBRARY DESTINATION . RUNTIME DESTINATION .) -endif() # install the main lib install(TARGETS dfhack From a3a26bda97160b1809afa213070533cb9ceb943c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 14 Mar 2026 11:07:24 -0500 Subject: [PATCH 817/919] update dfhooks to resteam branch --- depends/dfhooks | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 4c48e25a2a3..2d84a5826c5 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 +Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 diff --git a/scripts b/scripts index b672e4afc62..56c934eb5d4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b672e4afc6225d4e5e414beeb13f530b8cf66b39 +Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c From e2bbfc9fe8a987035622503b9d8c476b8c50d164 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 14:46:29 -0500 Subject: [PATCH 818/919] Further work on Steam compatibility 1. Adopt changes to dfhooks 2. Install dfhooks_dfhack the `hack` folder, and generate and install an appropriate `dfhooks_dfhack.ini` into the DF folder 3. Adjust RPATH (on Linux) so dfhooks_dfhack.dll can find dfhack.dll 4. Use dfhooks preinit to get the hack base path. Use this path when initializing Core 5. in `getHackPath()`, use the collected hack base path instead of hardcoding `./hack`. 6. Fix at one instance where `hack` was hardcoded outside of `getHackPath` (note: there are others this commit does not fix) 7. Update VersionInfoFactory to use a fs `path` instead of a `string` as its argument --- CMakeLists.txt | 13 +++++-------- library/CMakeLists.txt | 12 +++++++++--- library/Core.cpp | 5 +++-- library/Hooks.cpp | 10 +++++++++- library/include/Core.h | 4 +++- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 93df1f3576c..5694d4bd9f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,13 +226,10 @@ set(DFHACK_DATA_DESTINATION hack) ## where to install things (after the build is done, classic 'make install' or package structure) # the dfhack libraries will be installed here: -if(UNIX) - # put the lib into DF/hack - set(DFHACK_LIBRARY_DESTINATION ${DFHACK_DATA_DESTINATION}) -else() - # windows is crap, therefore we can't do nice things with it. leave the libs on a nasty pile... - set(DFHACK_LIBRARY_DESTINATION .) -endif() + +# put the lib into DF/hack +# windows will find it because dfhooks will `AddDllDirectory` the hack folder at runtime +set(DFHACK_LIBRARY_DESTINATION ${DFHACK_DATA_DESTINATION}) # external tools will be installed here: set(DFHACK_BINARY_DESTINATION .) @@ -267,7 +264,7 @@ if(UNIX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32 -march=i686") endif() string(REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") - set(CMAKE_INSTALL_RPATH ${DFHACK_LIBRARY_DESTINATION}) + set(CMAKE_INSTALL_RPATH "$ORIGIN/${DFHACK_LIBRARY_DESTINATION}") elseif(MSVC) # for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP") diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 3982a1a37c8..f91afef2e2a 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -131,7 +131,6 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} - Hooks.cpp ) if(WIN32) @@ -453,8 +452,9 @@ if(UNIX) endif() install(TARGETS dfhooks_dfhack - LIBRARY DESTINATION . - RUNTIME DESTINATION .) + LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} + RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) + # install the main lib install(TARGETS dfhack @@ -465,6 +465,12 @@ install(TARGETS dfhack-run dfhack-client binpatch LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/dfhooks_dfhack.ini + CONTENT "${DFHACK_DATA_DESTINATION}/$") + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dfhooks_dfhack.ini + DESTINATION .) + endif(BUILD_LIBRARY) # install the offset file diff --git a/library/Core.cpp b/library/Core.cpp index 8979cb3eee0..9fe3cf9bc61 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1054,16 +1054,17 @@ void Core::fatal (std::string output, const char * title) std::filesystem::path Core::getHackPath() { - return Filesystem::get_initial_cwd() / "hack"; + return hack_path; } df::viewscreen * Core::getTopViewscreen() { return getInstance().top_viewscreen; } -bool Core::InitMainThread() { +bool Core::InitMainThread(std::filesystem::path path) { // this hook is always called from DF's main (render) thread, so capture this thread id df_render_thread = std::this_thread::get_id(); + hack_path = path; Filesystem::init(); diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 0f957fea063..31d711056bd 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -7,6 +7,14 @@ static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; +static std::filesystem::path basepath{"./hack"}; + +// called by the chainloader before the main thread is initialized and before any other hooks are called. +DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath) +{ + basepath = dllpath.parent_path(); +} + // called from the main thread before the simulation thread is started // and the main event loop is initiated DFhackCExport void dfhooks_init() { @@ -17,7 +25,7 @@ DFhackCExport void dfhooks_init() { } // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread() || !df::global::game) { + if (!DFHack::Core::getInstance().InitMainThread(basepath) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } diff --git a/library/include/Core.h b/library/include/Core.h index 1f3cea5837f..9ccf0ca8710 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -268,7 +268,7 @@ namespace DFHack struct Private; std::unique_ptr d; - bool InitMainThread(); + bool InitMainThread(std::filesystem::path path); bool InitSimulationThread(); int Update (void); int Shutdown (void); @@ -353,6 +353,8 @@ namespace DFHack uint32_t unpaused_ms; // reset to 0 on map load + std::filesystem::path hack_path; + friend class CoreService; friend class ServerConnection; friend class CoreSuspender; From 8e20fbd4eb3ffe9bbaacc4d7429fcc8ebdc4c40d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 15:23:06 -0500 Subject: [PATCH 819/919] canonicalize hack path at init --- library/Hooks.cpp | 2 +- library/include/Hooks.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 31d711056bd..3f5ca36c329 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -25,7 +25,7 @@ DFhackCExport void dfhooks_init() { } // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread(basepath) || !df::global::game) { + if (!DFHack::Core::getInstance().InitMainThread(std::filesystem::canonical(basepath)) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } diff --git a/library/include/Hooks.h b/library/include/Hooks.h index 6945de2ea68..5f49d8dabaa 100644 --- a/library/include/Hooks.h +++ b/library/include/Hooks.h @@ -26,6 +26,7 @@ distribution. union SDL_Event; +DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath); DFhackCExport void dfhooks_init(); DFhackCExport void dfhooks_shutdown(); DFhackCExport void dfhooks_update(); From a546399f150bb352fb57f39adbb0dcc44f3a6b31 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 15:54:22 -0500 Subject: [PATCH 820/919] test: use RPATH `$ORIGIN` on Linux --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5694d4bd9f3..9be91932106 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -264,7 +264,7 @@ if(UNIX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32 -march=i686") endif() string(REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") - set(CMAKE_INSTALL_RPATH "$ORIGIN/${DFHACK_LIBRARY_DESTINATION}") + set(CMAKE_INSTALL_RPATH "$ORIGIN") elseif(MSVC) # for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP") From 02f64aa48c7712f54e03e8e575f671836680c0be Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 21:34:16 -0500 Subject: [PATCH 821/919] add path autodetection --- library/Hooks.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 3f5ca36c329..9865ca3276c 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -3,11 +3,36 @@ #include "df/gamest.h" +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif + static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; -static std::filesystem::path basepath{"./hack"}; +static std::filesystem::path getModulePath() +{ +#ifdef _WIN32 + HMODULE module = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)getModulePath, &module); + if (!module) return std::filesystem::path(); // should never happen, but just in case, return an empty path instead of crashing + + wchar_t path[MAX_PATH]; + GetModuleFileNameW(module, path, MAX_PATH); + return std::filesystem::path(path); +#else + DL_info info; + dladdr(getModulePath, &info); + return std::filesystem::path(info.dli_fname); +#endif +} + +static std::filesystem::path basepath{getModulePath()}; // called by the chainloader before the main thread is initialized and before any other hooks are called. DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath) From 0413b712ca06206943e0013a84363b07927b9cff Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 21:34:38 -0500 Subject: [PATCH 822/919] fix lua default path --- library/Core.cpp | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 9fe3cf9bc61..d7fc3f0d7bb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1092,6 +1092,7 @@ bool Core::InitMainThread(std::filesystem::path path) { std::cerr << "Build url: " << Version::dfhack_run_url() << std::endl; } std::cerr << "Starting with working directory: " << Filesystem::getcwd() << std::endl; + std::cerr << "Hack path: " << getHackPath() << std::endl; std::cerr << "Binding to SDL.\n"; if (!DFSDL::init(con)) { @@ -1233,9 +1234,9 @@ bool Core::InitSimulationThread() { // the update hook is only called from the simulation thread, so capture this thread id df_simulation_thread = std::this_thread::get_id(); - if(started) + if (started) return true; - if(errorstate) + if (errorstate) return false; // Lock the CoreSuspendMutex until the thread exits or call Core::Shutdown @@ -1277,20 +1278,20 @@ bool Core::InitSimulationThread() std::cout << "Console disabled.\n"; } } - else if(con.init(false)) + else if (con.init(false)) std::cerr << "Console is running.\n"; else std::cerr << "Console has failed to initialize!\n"; -/* - // dump offsets to a file - std::ofstream dump("offsets.log"); - if(!dump.fail()) - { - //dump << vinfo->PrintOffsets(); - dump.close(); - } - */ - // initialize data defs + /* + // dump offsets to a file + std::ofstream dump("offsets.log"); + if(!dump.fail()) + { + //dump << vinfo->PrintOffsets(); + dump.close(); + } + */ + // initialize data defs virtual_identity::Init(this); // create config directory if it doesn't already exist @@ -1307,7 +1308,8 @@ bool Core::InitSimulationThread() else { // ensure all config file directories exist before we start copying files - for (auto &entry : default_config_files) { + for (auto& entry : default_config_files) + { // skip over files if (!entry.second) continue; @@ -1317,19 +1319,22 @@ bool Core::InitSimulationThread() } // copy files from the default tree that don't already exist in the config tree - for (auto &entry : default_config_files) { + for (auto& entry : default_config_files) + { // skip over directories if (entry.second) continue; std::filesystem::path filename = entry.first; - if (!config_files.contains(filename)) { + if (!config_files.contains(filename)) + { std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; std::filesystem::path dest_file = getConfigPath() / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); - if (!src.good() || !dest.good()) { + if (!src.good() || !dest.good()) + { con.printerr("Copy failed: '{}'\n", filename); continue; } @@ -1340,6 +1345,17 @@ bool Core::InitSimulationThread() } } + // set lua default path if not already set + if (std::getenv("DFHACK_LUA_PATH") == nullptr) + { + std::filesystem::path lua_path = getHackPath() / "lua" / "?.lua"; +#ifdef WIN32 + _putenv_s("DFHACK_LUA_PATH", lua_path.string().c_str()); +#else + setenv("DFHACK_LUA_PATH", lua_path.string().c_str(), 1); +#endif + } + loadScriptPaths(con); // initialize common lua context From 0b1ea135a6bdecc8ab4f9735b3ddfda8a6b904f7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 22:51:37 -0500 Subject: [PATCH 823/919] `script`: try multiple locatons specifically, try in the hack path, in the hack path parent directory, and in the CWD, in that order --- library/Core.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index d7fc3f0d7bb..2deba390ff4 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -859,7 +859,22 @@ bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool INFO(script,out) << "Running script: " << fname << std::endl; std::cerr << "Running script: " << fname << std::endl; } - std::ifstream script{ fname.c_str() }; + + auto pathlist = {getHackPath(), getHackPath().parent_path(), std::filesystem::current_path()}; + + std::filesystem::path path; + + for (auto& p : pathlist) + { + auto candidate = fname.is_relative() ? p / fname : fname; + if (std::filesystem::exists(candidate)) + { + path = candidate; + break; + } + } + + std::ifstream script{ path }; if ( !script ) { if(!silent) From 0e25a2fd28d93988c46e619244b635eba1e8cd60 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 23:04:09 -0500 Subject: [PATCH 824/919] fix typo --- library/Hooks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 9865ca3276c..d37f9fad082 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -26,7 +26,7 @@ static std::filesystem::path getModulePath() GetModuleFileNameW(module, path, MAX_PATH); return std::filesystem::path(path); #else - DL_info info; + Dl_info info; dladdr(getModulePath, &info); return std::filesystem::path(info.dli_fname); #endif From 56462222b00ca499d4881b9a03652a7799b8d908 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 16 Mar 2026 23:10:24 -0500 Subject: [PATCH 825/919] gcc wants an explicit cast here --- library/Hooks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Hooks.cpp b/library/Hooks.cpp index d37f9fad082..42e9f859af4 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -27,7 +27,7 @@ static std::filesystem::path getModulePath() return std::filesystem::path(path); #else Dl_info info; - dladdr(getModulePath, &info); + dladdr((const void*)getModulePath, &info); return std::filesystem::path(info.dli_fname); #endif } From 1ec0bc211a9ad372c8149a5114ca0f7aee70ec07 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Mar 2026 11:47:07 -0500 Subject: [PATCH 826/919] Make stonesense loadable in a relocated installation --- library/PlugLoad.cpp | 3 ++- plugins/stonesense | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 336e7f50e71..fa58d39514f 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -15,10 +15,11 @@ #ifdef WIN32 #define NOMINMAX #include +#include #define global_search_handle() GetModuleHandle(nullptr) #define get_function_address(plugin, function) GetProcAddress((HMODULE)plugin, function) #define clear_error() -#define load_library(fn) LoadLibraryW(fn.c_str()) +#define load_library(fn) LoadLibraryExW(fn.wstring().c_str(), NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); #define close_library(handle) (!(FreeLibrary((HMODULE)handle))) #else #include diff --git a/plugins/stonesense b/plugins/stonesense index 4760027eee7..581f8ff7317 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 4760027eee745d8c35cca843a2fcc46c21be326a +Subproject commit 581f8ff7317fd15723fb31632650746afede7b6c From 2fee311c0c1f2142bb9c07cf1e21a73599b61290 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Mar 2026 13:57:23 -0500 Subject: [PATCH 827/919] incorporate stonesense updates --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 581f8ff7317..64cc02b1232 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 581f8ff7317fd15723fb31632650746afede7b6c +Subproject commit 64cc02b12321abf9b99e41c4e5f931f71bc81a45 From 8efc6726301f084c70e66b3d702945257f04150d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 26 Mar 2026 14:48:54 -0500 Subject: [PATCH 828/919] sync stonesense --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 64cc02b1232..b1b676bcf1d 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 64cc02b12321abf9b99e41c4e5f931f71bc81a45 +Subproject commit b1b676bcf1dc810e7210aceef89c1626069136f5 From 1ba2130b4c9a94930989ea82094f047e87efe054 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:59:50 +0000 Subject: [PATCH 829/919] Auto-update submodules scripts: master plugins/stonesense: master --- plugins/stonesense | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/stonesense b/plugins/stonesense index 4760027eee7..9dfa9d731f6 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 4760027eee745d8c35cca843a2fcc46c21be326a +Subproject commit 9dfa9d731f6b84b6deaba42364168b7374157e6e diff --git a/scripts b/scripts index b672e4afc62..af457f9b5c8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b672e4afc6225d4e5e414beeb13f530b8cf66b39 +Subproject commit af457f9b5c86fc041a064ef5bdfbcab38f38a50f From 2d6d34ef1c7b47cd8a2915e700fe0f10c73cc79b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 27 Mar 2026 15:26:00 -0500 Subject: [PATCH 830/919] on linux, use proper rpath for plugins Co-Authored-By: Christian Doczkal <20443222+chdoc@users.noreply.github.com> --- plugins/Plugins.cmake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 82439f69a5b..192662bccc4 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -141,6 +141,10 @@ macro(dfhack_plugin) set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dll) endif() + if (UNIX) + set_target_properties(${PLUGIN_NAME} PROPERTIES INSTALL_RPATH "$ORIGIN/..") + endif() + install(TARGETS ${PLUGIN_NAME} LIBRARY DESTINATION ${DFHACK_PLUGIN_DESTINATION} RUNTIME DESTINATION ${DFHACK_PLUGIN_DESTINATION}) From 4166d211fb21db8f16c40c66e859269d4640912b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 1 Apr 2026 11:36:15 -0500 Subject: [PATCH 831/919] changes to launchdf to handle relocatable install instead of assuming colocation, this version will (when both DF and DFHack are installed in Steam) automatically inject DFHack into DF environments where not both apps are installed in Steam are (hopefully) unaffected --- package/launchdf.cpp | 74 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 5a850c6cf94..c7f9b58c894 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -10,6 +10,8 @@ #include "steam_api.h" #include +#include +#include #define xstr(s) str(s) #define str(s) #s @@ -245,14 +247,19 @@ int main(int argc, char* argv[]) { } bool nowait = false; + bool installmode = false; #ifdef WIN32 std::wstring cmdline(lpCmdLine); if (cmdline.find(L"--nowait") != std::wstring::npos) nowait = true; + if (cmdline.find(L"--install") != std::wstring::npos) + installmode = true; #else for (int idx = 0; idx < argc; ++idx) { if (strcmp(argv[idx], "--nowait") == 0) nowait = true; + if (strcmp(argv[idx], "--install") == 0) + installmode = true; } #endif @@ -294,22 +301,75 @@ int main(int argc, char* argv[]) { char buf[2048] = ""; int b1 = SteamApps()->GetAppInstallDir(DFHACK_STEAM_APPID, (char*)&buf, 2048); - std::string dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; + std::filesystem::path dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; int b2 = SteamApps()->GetAppInstallDir(DF_STEAM_APPID, (char*)&buf, 2048); - std::string df_install_folder = (b2 != -1) ? std::string(buf) : ""; + std::filesystem::path df_install_folder = (b2 != -1) ? std::string(buf) : ""; + if (df_install_folder != dfhack_install_folder) + { +#ifdef WIN32 + constexpr auto dfhooks_dll_name = "dfhooks.dll"; + constexpr auto dfhook_dfhack_dll_name = "dfhooks_dfhack.dll"; +#else + constexpr auto dfhooks_dll_name = "libdfhooks.so"; + constexpr auto dfhook_dfhack_dll_name = "libdfhooks_dfhack.so"; +#endif + // DF and DFHack are not co-installed (modern case) + // inject dfhooks.dll and dfhooks_dfhack.ini into DF install folder + std::filesystem::path dfhooks_dll_src = dfhack_install_folder / dfhooks_dll_name; + std::filesystem::path dfhooks_dll_dst = df_install_folder / dfhooks_dll_name; + std::filesystem::path dfhooks_ini_dst = df_install_folder / "dfhooks_dfhack.ini"; + std::filesystem::path dfhooks_dfhack_dll_src = dfhack_install_folder / "hack" / dfhook_dfhack_dll_name; - if (df_install_folder != dfhack_install_folder) { - // DF and DFHack are not installed in the same library + std::error_code ec; + + std::filesystem::copy(dfhooks_dll_src, dfhooks_dll_dst, std::filesystem::copy_options::update_existing, ec); + if (!ec) + { + std::string indirection; + if (std::filesystem::exists(dfhooks_ini_dst)) + { + std::ifstream ini(dfhooks_ini_dst); + std::getline(ini, indirection); + } + + if (indirection != dfhooks_dfhack_dll_src.string()) + { + std::ofstream ini(dfhooks_ini_dst); + ini << dfhooks_dfhack_dll_src.string() << std::endl; + } + } + else + { #ifdef WIN32 - MessageBoxW(NULL, L"DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting.", NULL, 0); + std::wstring message{ + L"Failed to inject DFHack into Dwarf Fortress\n\n" + L"Details:\n" + std::filesystem::relative(dfhooks_dll_src).wstring() + + L" -> " + std::filesystem::relative(dfhooks_dll_dst).wstring() + + L"\n\nError code: " + std::to_wstring(ec.value()) + + L"\nError message: " + std::filesystem::relative(ec.message()).wstring() + }; + + MessageBoxW(NULL, message.c_str(), NULL, 0); #else - notify("DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting."); + std::string message{ + "Failed to inject DFHack into Dwarf Fortress\n\n" + "Details:\n" + std::filesystem::relative(dfhooks_dll_src).string() + + " -> " + std::filesystem::relative(dfhooks_dll_dst).string() + + "\n\nError code: " + std::to_string(ec.value()) + + "\nError message: " + std::filesystem::relative(ec.message()).string() + }; + + notify(message.c_str()); #endif - exit(1); + exit(1); + } } + if (installmode) + exit(0); + if (!wrap_launch(launch_via_steam)) exit(1); From e718a021e349db0f55f3b79bd31d990888361aad Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 12:33:46 -0500 Subject: [PATCH 832/919] add code to detect/clean legacy install on windows, will prompt user on linux, will advise user on how to manually clean also removed install mode - can't seem to make work with steam API rn --- package/launchdf.cpp | 106 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index c7f9b58c894..03085ed6d54 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -236,6 +236,80 @@ bool waitForDF(bool nowait) { #endif +constexpr const char* old_filelist[] { + "hack", + "stonesense", +#ifdef WIN32 + "binpatch.exe", + "dfhack-run.exe", + "allegro-5.2.dll", + "allegro_color-5.2.dll", + "allegro_font-5.2.dll", + "allegro_image-5.2.dll", + "allegro_primitives-5.2.dll", + "allegro_ttf-5.2.dll", + "allegro-5.2.dll", + "dfhack-client.dll", + "dfhooks_dfhack.dll", + "lua53.dll", + "protobuf-lite.dll" +#else + "binpatch", + "dfhack-run", + "liballegro-5.2.so", + "liballegro_color-5.2.so", + "liballegro_font-5.2.so", + "liballegro_image-5.2.so", + "liballegro_primitives-5.2.so", + "liballegro_ttf-5.2.so", + "liballegro-5.2.so", + "libdfhack-client.so", + "libdfhooks_dfhack.so", + "liblua53.so", + "libprotobuf-lite.so" +#endif +}; + +bool check_for_old_install(std::filesystem::path df_path) +{ + for (auto file : old_filelist) + { + std::filesystem::path p = df_path / file; + bool exists = std::filesystem::exists(p); +// std::wstring message = L"Checking for legacy files:\n" + p.wstring() + L": " + (exists ? L"found" : L"not found"); +// MessageBoxW(NULL, message.c_str(), L"Checking for legacy files", 0); + if (exists) + return true; + } + return false; +} + +void remove_old_install(std::filesystem::path df_path) +{ + std::string message{ + "Removing legacy files:" + }; + + for (auto file : old_filelist) + { + std::error_code ec; + + std::filesystem::path p = df_path / file; + + if (std::filesystem::is_directory(p)) + std::filesystem::remove_all(p, ec); + else if (std::filesystem::is_regular_file(p)) + std::filesystem::remove(p, ec); + else + continue; + + message += "\n" + p.string() + ": " + (ec ? "failed to remove - " + ec.message() : "removed successfully"); + } +#ifdef WIN32 + MessageBoxW(NULL, std::wstring(message.begin(), message.end()).c_str(), L"Legacy Install Cleanup", 0); +#endif +} + #ifdef WIN32 int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { #else @@ -247,19 +321,14 @@ int main(int argc, char* argv[]) { } bool nowait = false; - bool installmode = false; #ifdef WIN32 std::wstring cmdline(lpCmdLine); if (cmdline.find(L"--nowait") != std::wstring::npos) nowait = true; - if (cmdline.find(L"--install") != std::wstring::npos) - installmode = true; #else for (int idx = 0; idx < argc; ++idx) { if (strcmp(argv[idx], "--nowait") == 0) nowait = true; - if (strcmp(argv[idx], "--install") == 0) - installmode = true; } #endif @@ -365,10 +434,30 @@ int main(int argc, char* argv[]) { #endif exit(1); } - } + bool dirty = check_for_old_install(df_install_folder); + if (dirty) + { +#ifdef WIN32 + int ok = MessageBoxW(NULL, L"A legacy install of DFHack has been detected in the Dwarf Fortress folder. This likely means that you have installed DFHack with the old Steam client (or manually). This legacy installation will almost certainly interfere with using DFHack. Do you want to remove the old files now? (recommended)", L"Legacy DFHack Install Detected", MB_OKCANCEL); - if (installmode) - exit(0); + if (ok == IDOK) + remove_old_install(df_install_folder); +#else + int response = 0; + std::string filelist; + for (auto file : old_filelist) + if (std::filesystem::exists(df_install_folder / file)) + filelist += (filelist.empty() ? "" : std::string(",")) + file; + + std::string message{ + "A legacy install of DFHack has been detected in the Dwarf Fortress directory.This likely means that you have installed DFHack with the old Steam client (or manually).This installation will almost certainly interfere with using DFHack. \n\n" + "To remove these files, run the following command: rm -r " + df_install_folder.string() + "/{ " + filelist + "}\n\n" + }; + + notify(message.c_str()); +#endif + } + } if (!wrap_launch(launch_via_steam)) exit(1); @@ -389,6 +478,5 @@ int main(int argc, char* argv[]) { usleep(1000000); #endif } - exit(0); } From 5ecdca159200723f9455d10cf62ce93125db0633 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 13:34:44 -0500 Subject: [PATCH 833/919] changes to hopefully make injection work in wine changed injection/legacy check code to before instead of after wine-specific launch code also made path detection hopefully more robust (and less wet) --- package/launchdf.cpp | 79 ++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 03085ed6d54..a2e3cbaf3e5 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #define xstr(s) str(s) #define str(s) #s @@ -275,10 +276,7 @@ bool check_for_old_install(std::filesystem::path df_path) for (auto file : old_filelist) { std::filesystem::path p = df_path / file; - bool exists = std::filesystem::exists(p); -// std::wstring message = L"Checking for legacy files:\n" + p.wstring() + L": " + (exists ? L"found" : L"not found"); -// MessageBoxW(NULL, message.c_str(), L"Checking for legacy files", 0); - if (exists) + if (std::filesystem::exists(p)) return true; } return false; @@ -341,42 +339,38 @@ int main(int argc, char* argv[]) { } #ifdef WIN32 - if (is_running_on_wine()) { - // attempt launch via steam client - LPCWSTR err = launch_via_steam_posix(); - - if (err != NULL) - // steam client launch failed, attempt fallback launch - err = launch_direct(); - - if (err != NULL) - { - MessageBoxW(NULL, err, NULL, 0); - exit(1); - } - exit(0); - } + bool wine_detected = is_running_on_wine(); +#else + bool wine_detected = false; #endif - // steam detected and not running in wine + bool df_detected = SteamApps()->BIsAppInstalled(DF_STEAM_APPID); - if (!SteamApps()->BIsAppInstalled(DF_STEAM_APPID)) { + if (!df_detected) { // Steam DF is not installed. Assume DF is installed in same directory as DFHack and do a fallback launch exit(wrap_launch(launch_direct) ? 0 : 1); } - // obtain DF app path - - char buf[2048] = ""; + // obtain DF and DFHack app paths - int b1 = SteamApps()->GetAppInstallDir(DFHACK_STEAM_APPID, (char*)&buf, 2048); - std::filesystem::path dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; + auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { + char buf[2048] = ""; + int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, 2048); + if (bytes == -1) + return std::nullopt; + // steam API counts the null terminator in the byte count returned + if (buf[bytes] == '\0') bytes--; + return std::string(buf, bytes); + }; - int b2 = SteamApps()->GetAppInstallDir(DF_STEAM_APPID, (char*)&buf, 2048); - std::filesystem::path df_install_folder = (b2 != -1) ? std::string(buf) : ""; + auto opt_dfhack_install_folder = get_app_path_from_steam(DFHACK_STEAM_APPID); + auto opt_df_install_folder = get_app_path_from_steam(DF_STEAM_APPID); - if (df_install_folder != dfhack_install_folder) + if (opt_dfhack_install_folder && opt_df_install_folder && (*opt_df_install_folder != *opt_dfhack_install_folder)) { + auto& dfhack_install_folder = *opt_dfhack_install_folder; + auto& df_install_folder = *opt_df_install_folder; + #ifdef WIN32 constexpr auto dfhooks_dll_name = "dfhooks.dll"; constexpr auto dfhook_dfhack_dll_name = "dfhooks_dfhack.dll"; @@ -414,8 +408,8 @@ int main(int argc, char* argv[]) { #ifdef WIN32 std::wstring message{ L"Failed to inject DFHack into Dwarf Fortress\n\n" - L"Details:\n" + std::filesystem::relative(dfhooks_dll_src).wstring() + - L" -> " + std::filesystem::relative(dfhooks_dll_dst).wstring() + + L"Details:\n" + dfhooks_dll_src.wstring() + + L" -> " + dfhooks_dll_dst.wstring() + L"\n\nError code: " + std::to_wstring(ec.value()) + L"\nError message: " + std::filesystem::relative(ec.message()).wstring() }; @@ -424,8 +418,8 @@ int main(int argc, char* argv[]) { #else std::string message{ "Failed to inject DFHack into Dwarf Fortress\n\n" - "Details:\n" + std::filesystem::relative(dfhooks_dll_src).string() + - " -> " + std::filesystem::relative(dfhooks_dll_dst).string() + + "Details:\n" + dfhooks_dll_src.string() + + " -> " + dfhooks_dll_dst.string() + "\n\nError code: " + std::to_string(ec.value()) + "\nError message: " + std::filesystem::relative(ec.message()).string() }; @@ -459,6 +453,25 @@ int main(int argc, char* argv[]) { } } +#ifdef WIN32 + if (wine_detected) + { + // attempt launch via steam client + LPCWSTR err = launch_via_steam_posix(); + + if (err != NULL) + // steam client launch failed, attempt fallback launch + err = launch_direct(); + + if (err != NULL) + { + MessageBoxW(NULL, err, NULL, 0); + exit(1); + } + exit(0); + } +#endif + if (!wrap_launch(launch_via_steam)) exit(1); From 7afeddc100614dec6adfab8048c400260f4ccc88 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 13:36:27 -0500 Subject: [PATCH 834/919] slightly safer return check for `GetAppInstallDir` --- package/launchdf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index a2e3cbaf3e5..96875e3b685 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -356,7 +356,7 @@ int main(int argc, char* argv[]) { auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { char buf[2048] = ""; int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, 2048); - if (bytes == -1) + if (bytes <= 0) return std::nullopt; // steam API counts the null terminator in the byte count returned if (buf[bytes] == '\0') bytes--; From 3483d778db665fabb764264375f03ba5763d0fba Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 13:41:13 -0500 Subject: [PATCH 835/919] move toward release candidate status --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9be91932106..083ea5bc38e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r2") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r3rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From cef0742f571de6bd66d711db56c52a15488cc05c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 3 Apr 2026 14:02:15 -0500 Subject: [PATCH 836/919] make gcc happy --- package/launchdf.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 96875e3b685..5ffc939e15b 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -340,8 +340,6 @@ int main(int argc, char* argv[]) { #ifdef WIN32 bool wine_detected = is_running_on_wine(); -#else - bool wine_detected = false; #endif bool df_detected = SteamApps()->BIsAppInstalled(DF_STEAM_APPID); @@ -437,7 +435,6 @@ int main(int argc, char* argv[]) { if (ok == IDOK) remove_old_install(df_install_folder); #else - int response = 0; std::string filelist; for (auto file : old_filelist) if (std::filesystem::exists(df_install_folder / file)) From 6a0f9c89d5a928b930d370aaf5c2fa90fb732aa7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:59:45 +0000 Subject: [PATCH 837/919] Auto-update submodules scripts: master plugins/stonesense: master depends/dfhooks: main --- depends/dfhooks | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/dfhooks b/depends/dfhooks index 2d84a5826c5..4c48e25a2a3 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 +Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 diff --git a/plugins/stonesense b/plugins/stonesense index b1b676bcf1d..9dfa9d731f6 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit b1b676bcf1dc810e7210aceef89c1626069136f5 +Subproject commit 9dfa9d731f6b84b6deaba42364168b7374157e6e diff --git a/scripts b/scripts index 56c934eb5d4..af457f9b5c8 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 56c934eb5d4c957ca731705783a0a43397a9ba2c +Subproject commit af457f9b5c86fc041a064ef5bdfbcab38f38a50f From e88c8c16d747fa5c413be8f008d43b3e3de79ff4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 4 Apr 2026 12:24:36 -0500 Subject: [PATCH 838/919] revert inadvertent dfhooks submodule change in ff1b068 --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 4c48e25a2a3..2d84a5826c5 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 +Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 From 95e89e9156ed9a1fb16233bfe6c7a0efc1e4e191 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 4 Apr 2026 12:21:20 -0500 Subject: [PATCH 839/919] use utf8 in windows console been wanting to do this for a very long time ref #1474 --- docs/changelog.txt | 1 + library/Console-windows.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf2ca7d998b..36aba6f46ea 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## New Features ## Fixes +- Core: Windows console will always use UTF-8 regardless of system code page settings ## Misc Improvements diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 12a3e0c2eb0..058cedebe09 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -474,6 +474,10 @@ bool Console::init(bool) HMENU hm = GetSystemMenu(d->ConsoleWindow,false); DeleteMenu(hm, SC_CLOSE, MF_BYCOMMAND); + // force console code pages to utf-8 + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); + // set the screen buffer to be big enough to let us scroll text GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); d->default_attributes = coninfo.wAttributes; From 731662185e022b8abbbf7d20b3beb4ab3bc01cc6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 6 Apr 2026 08:17:51 -0500 Subject: [PATCH 840/919] Update changelog.txt --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf2ca7d998b..8feb15325f2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,8 +59,10 @@ Template for new versions: ## New Features ## Fixes +- Steam launcher: Switch to injection strategy, allowing Dwarf Fortress and DFHack to be installed in disparate locations ## Misc Improvements +- Make DFHack relocatable so that it doesn't depend on being fully co-installed with Dwarf Fortress ## Documentation From 35735053c3ae1b19e7c2a9f1856700ab52e131c0 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:55:10 +0000 Subject: [PATCH 841/919] Auto-update submodules depends/dfhooks: main --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 2d84a5826c5..4c48e25a2a3 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 2d84a5826c51e99a6ff2c7d4c530680b366044c1 +Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 From 688200286b9bb7f72f7b6454fc7fe506a53b706d Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:01:17 +0000 Subject: [PATCH 842/919] Auto-update submodules depends/dfhooks: main --- depends/dfhooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/dfhooks b/depends/dfhooks index 4c48e25a2a3..8a578206fb9 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 4c48e25a2a33538bf0c522f69987fd28c1525503 +Subproject commit 8a578206fb9b1dd32b04c8c7c35217e2b83e369e From 1680442d630365d9f3c99effd9f26ae46d6412e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:17:08 +0000 Subject: [PATCH 843/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.37.0 → 0.37.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.0...0.37.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90b37493a7c..b991bfbb742 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.0 + rev: 0.37.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From bafc832f218e706bc33bb986f783ab303fe685a1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 01:16:36 -0500 Subject: [PATCH 844/919] more aggressive traiiling null trimming seems to be needed on Steam Deck --- package/launchdf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 5ffc939e15b..58906af30d2 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -357,7 +357,7 @@ int main(int argc, char* argv[]) { if (bytes <= 0) return std::nullopt; // steam API counts the null terminator in the byte count returned - if (buf[bytes] == '\0') bytes--; + while (bytes && buf[bytes] == '\0') bytes--; return std::string(buf, bytes); }; From 4981e7ee09d885ed6876d75e7637865c200f981e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 01:54:23 -0500 Subject: [PATCH 845/919] improve `get_app_path_from_steam`; force UTF-8 on windows --- package/launchdf.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 58906af30d2..e2596331f0e 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -312,6 +312,10 @@ void remove_old_install(std::filesystem::path df_path) int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { #else int main(int argc, char* argv[]) { +#endif +#ifdef WIN32 + // force UTF-8 + std::setlocale(LC_ALL, ".utf8"); #endif // initialize steam context if (SteamAPI_RestartAppIfNecessary(DFHACK_STEAM_APPID)) { @@ -352,11 +356,12 @@ int main(int argc, char* argv[]) { // obtain DF and DFHack app paths auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { - char buf[2048] = ""; - int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, 2048); + constexpr auto BUFSIZE = 2048; + char buf[BUFSIZE] = ""; + int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, BUFSIZE); if (bytes <= 0) return std::nullopt; - // steam API counts the null terminator in the byte count returned + // steam API includes one or more null terminators after the path, so trim those off while (bytes && buf[bytes] == '\0') bytes--; return std::string(buf, bytes); }; From ba555de43675f4ec48768275c8aef0a325cc4539 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:48:10 +0000 Subject: [PATCH 846/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 9dfa9d731f6..f910ace3abe 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9dfa9d731f6b84b6deaba42364168b7374157e6e +Subproject commit f910ace3abea09feef79df5c211c62c91146aa37 From 174ebd48826e3bad73336cd7c24e89807d67cd00 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 20:50:44 -0500 Subject: [PATCH 847/919] fix changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 039b2acbb50..88c7546a61c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,7 +33,6 @@ Template for new versions: ## New Features ## Fixes -- Core: Windows console will always use UTF-8 regardless of system code page settings ## Misc Improvements @@ -60,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- Core: Windows console will always use UTF-8 regardless of system code page settings - Steam launcher: Switch to injection strategy, allowing Dwarf Fortress and DFHack to be installed in disparate locations ## Misc Improvements From de7fa493073d025776c2536c39b1342f96bf15c1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 21:23:51 -0500 Subject: [PATCH 848/919] Update stonesense --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index f910ace3abe..963dfd7c73c 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit f910ace3abea09feef79df5c211c62c91146aa37 +Subproject commit 963dfd7c73c78be921c15d86e80fea9c7910faf9 From 5d4e181ba737dcb4235beff17592d82ef1736281 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 22:04:51 -0500 Subject: [PATCH 849/919] fix stupid in launchdf --- package/launchdf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/launchdf.cpp b/package/launchdf.cpp index e2596331f0e..3b07abf99a4 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -362,7 +362,7 @@ int main(int argc, char* argv[]) { if (bytes <= 0) return std::nullopt; // steam API includes one or more null terminators after the path, so trim those off - while (bytes && buf[bytes] == '\0') bytes--; + for (; bytes > 0 && buf[bytes-1] == '\0'; bytes--); return std::string(buf, bytes); }; From 3f6aaeb123d24472288716b755a9cb81126ec50f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 8 Apr 2026 22:06:03 -0500 Subject: [PATCH 850/919] Update stonesense --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 963dfd7c73c..c8ddd2c5238 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 963dfd7c73c78be921c15d86e80fea9c7910faf9 +Subproject commit c8ddd2c52387d32f06d7c99d83b9303e3038b47b From 07e81d05751787c05ca1cab02a143016498096e6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Apr 2026 08:46:11 -0500 Subject: [PATCH 851/919] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 083ea5bc38e..b40fd0a451b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r3rc1") +set(DFHACK_RELEASE "r3rc2") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From ccb0aa186fe8737800b64ef1a86eef19809bb622 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 9 Apr 2026 11:22:16 -0500 Subject: [PATCH 852/919] make `stockpiles.lua` relocatable --- plugins/lua/stockpiles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index ac48ed6fc9b..1b2955459dc 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -8,7 +8,7 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local STOCKPILES_DIR = 'dfhack-config/stockpiles' -local STOCKPILES_LIBRARY_DIR = 'hack/data/stockpiles' +local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. 'data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' From 8d602bc1eeada61a8b43b4a55c348aafe81c7869 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Apr 2026 09:42:29 -0500 Subject: [PATCH 853/919] Change build version to 53.11-r3 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b40fd0a451b..307208ddf28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r3rc2") -set(DFHACK_PRERELEASE TRUE) +set(DFHACK_RELEASE "r3") +set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) From eacfddfb2185e5ee90dd7950ef7f686973d67d64 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 13 Apr 2026 09:42:43 -0500 Subject: [PATCH 854/919] Update changelogs --- docs/changelog.txt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 88c7546a61c..7fb648d8db1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,7 +52,23 @@ Template for new versions: ======== were in submodules with their own changelogs! ======== ================================================================================ -# Future +# Future## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.11-r3 ## New Tools From a835c5baa9019b62236c6b695a3aecc1a0a16ad6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 14 Apr 2026 15:44:49 -0500 Subject: [PATCH 855/919] Update CMakeLists.txt for 53.12-r1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 307208ddf28..ebc0ffe3102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.11") -set(DFHACK_RELEASE "r3") +set(DF_VERSION "53.12") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From 848dbe8f7dbe747e7a0304d84696832ae0da3a9c Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:54:31 +0000 Subject: [PATCH 856/919] Auto-update structures ref for 53.12 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 114321c4802..f1cc38c163d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 114321c4802fb7e16e9d1092c8f2c057422a1c82 +Subproject commit f1cc38c163d90cafc414f9dffd120d19d45da113 From 424140be9da9102759d0ecd27dafe6ab7d25525c Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:31:26 +0000 Subject: [PATCH 857/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index f1cc38c163d..5c432a10a9e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit f1cc38c163d90cafc414f9dffd120d19d45da113 +Subproject commit 5c432a10a9e50ee7c95bee61bee0564149b1d2bc From e96c220eb9e93a8a94f5bc76318e8909889e02d8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 14 Apr 2026 17:15:28 -0500 Subject: [PATCH 858/919] fix missing `/` in stockpiles --- plugins/lua/stockpiles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index 1b2955459dc..33f6e375ada 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -8,7 +8,7 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local STOCKPILES_DIR = 'dfhack-config/stockpiles' -local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. 'data/stockpiles' +local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. '/data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' From 7e7886e698847195b8c03c539537c3b31d155d8e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 14 Apr 2026 17:15:43 -0500 Subject: [PATCH 859/919] Update changelog for 53.12-r1 --- docs/changelog.txt | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7fb648d8db1..495a5ecc928 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,11 +52,33 @@ Template for new versions: ======== were in submodules with their own changelogs! ======== ================================================================================ -# Future## New Tools +# Future + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.12-r1 + +## New Tools ## New Features +- Compatibility with Dwarf Fortress 53.12 ## Fixes +- Stockpile definitions in the default library will be correctly found and used (fixed missing path separator) ## Misc Improvements From ab5b43742761a2daa65011633e66ee7a63ed24a6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 15 Apr 2026 12:24:42 -0500 Subject: [PATCH 860/919] improve `enum_field` with more flexible casting mostly so `RemoteFortressReader` will behave with updated structures --- docs/changelog.txt | 1 + library/include/DataDefs.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 495a5ecc928..cfff16e5a46 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,6 +65,7 @@ Template for new versions: ## Documentation ## API +- add flexible casting to ``enum_field`` to enable explicit casting to more types ## Lua diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 08d8d020975..88c4a93651d 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -632,6 +632,10 @@ namespace df enum_field &operator=(EnumType ev) { value = IntType(ev); return *this; } + explicit operator IntType () const { return IntType(value); } + template + explicit operator T () const { return static_cast(IntType(value)); } + }; template From 6cb697cc66dd46d07582faa8691074a7c8cf7002 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:19:18 +0000 Subject: [PATCH 861/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 5c432a10a9e..cab90ab30c4 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5c432a10a9e50ee7c95bee61bee0564149b1d2bc +Subproject commit cab90ab30c4f52796aec9f83ce21c2fa5974494b From 97e7aa75a399f62f45ac9e6554059e0a1fd13cda Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 00:34:24 -0500 Subject: [PATCH 862/919] `ColorText.cpp`: remove unneeded headers --- library/ColorText.cpp | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/library/ColorText.cpp b/library/ColorText.cpp index bdd3e98f447..a101d795a98 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -35,24 +35,12 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include -#include -#include -#include #include +#include +#include #include "ColorText.h" -#include "MiscUtils.h" - -#include -#include -#include -#include -using namespace std; using namespace DFHack; bool color_ostream::log_errors_to_stderr = false; @@ -81,7 +69,7 @@ void color_ostream::end_batch() flush_proxy(); } -color_ostream::color_ostream() : ostream(new buffer(this)), cur_color(COLOR_RESET) +color_ostream::color_ostream() : std::ostream(new buffer(this)), cur_color(COLOR_RESET) { // } From d5b1fcb4d6a0e673c8e8aaa4fdbb0fb437d136a1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:21:29 -0500 Subject: [PATCH 863/919] dedup/sort/clean `Core.cpp` includes --- library/Core.cpp | 84 ++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 2deba390ff4..d7824165c28 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -26,40 +26,43 @@ distribution. #include "Internal.h" -#include "Error.h" -#include "MemAccess.h" +#include "ColorText.h" +#include "Commands.h" +#include "Console.h" +#include "CoreDefs.h" #include "DataDefs.h" #include "Debug.h" -#include "Console.h" +#include "DFHackVersion.h" +#include "Error.h" +#include "Format.h" +#include "LuaTools.h" +#include "MemAccess.h" #include "MemoryPatcher.h" #include "MiscUtils.h" +#include "MiscUtils.h" #include "Module.h" -#include "VersionInfoFactory.h" -#include "VersionInfo.h" -#include "PluginManager.h" #include "ModuleFactory.h" +#include "PluginManager.h" #include "RemoteServer.h" #include "RemoteTools.h" -#include "LuaTools.h" -#include "DFHackVersion.h" -#include "md5wrapper.h" -#include "Format.h" - -#include "Commands.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" #include "modules/DFSDL.h" #include "modules/DFSteam.h" #include "modules/EventManager.h" #include "modules/Filesystem.h" +#include "modules/Graphic.h" #include "modules/Gui.h" #include "modules/Hotkey.h" +#include "modules/Persistence.h" #include "modules/Textures.h" #include "modules/World.h" -#include "modules/Persistence.h" -#include "df/init.h" #include "df/gamest.h" +#include "df/global_objects.h" #include "df/graphic.h" +#include "df/init.h" #include "df/interfacest.h" #include "df/plotinfost.h" #include "df/viewscreen_dwarfmodest.h" @@ -68,35 +71,51 @@ distribution. #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_savegamest.h" -#include "df/world.h" #include "df/world_data.h" +#include "df/world.h" -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include #include -#include -#include #include -#include -#include +#include #include -#include +#include +#include +#include +#include #include -#include -#include +#include +#include + +#include "md5wrapper.h" + +#include + #include +#include +#include +#include -#ifdef _WIN32 -#define NOMINMAX -#include +#ifdef WIN32 +#include #endif #ifdef LINUX_BUILD @@ -105,6 +124,7 @@ distribution. using namespace DFHack; using namespace df::enums; + using df::global::init; using df::global::world; using std::string; From 3d80246bc94cf2929766dbd73560144c1c9d7907 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:25:03 -0500 Subject: [PATCH 864/919] update includes in `DataIdentity.cpp` --- library/DataIdentity.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index 346565c62fe..0a797c20694 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -1,14 +1,21 @@ -#include +#include "DataIdentity.h" + +#include "BitArray.h" +#include "DataDefs.h" #include +#include +#include #include +#include +#include +#include #include +#include #include -#include #include +#include -#include "DataFuncs.h" -#include "DataIdentity.h" // the space after the uses of "type" in OPAQUE_IDENTITY_TRAITS_NAME is _required_ // without it the macro generates a syntax error when type is a template specification From 7278c55a47c9b567316c42d162909d45161d5a01 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:37:39 -0500 Subject: [PATCH 865/919] update includes in `MiscUtils.cpp` --- library/MiscUtils.cpp | 64 +++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index dc1d4950ac2..44f93524eea 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -22,39 +22,49 @@ must not be misrepresented as being the original software. distribution. */ -#include "Internal.h" #include "Export.h" #include "MiscUtils.h" #include "ColorText.h" -#include "modules/DFSDL.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#ifndef LINUX_BUILD -// We don't want min and max macros +#ifdef WIN32 +// Suppress warning which occurs in header on some WinSDK versions +// See dfhack/dfhack#5147 for more information #define NOMINMAX - #include - // Suppress warning which occurs in header on some WinSDK versions - // See dfhack/dfhack#5147 for more information - #pragma warning(push) - #pragma warning(disable:4091) - #include - #pragma warning(pop) -#else - #include - #include - #include +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma warning(push) +#pragma warning(disable:4091) +#include +#pragma warning(pop) #endif -#include -#include -#include -#include -#include - -#include -#include -#include -#include +#ifdef LINUX_BUILD +#include +#include +#include +#endif NumberFormatType preferred_number_format_type = NumberFormatType::DEFAULT; @@ -499,8 +509,8 @@ uint64_t GetTimeMs64() /* Character decoding */ // See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. -#define UTF8_ACCEPT 0 -#define UTF8_REJECT 12 +constexpr auto UTF8_ACCEPT = 0; +constexpr auto UTF8_REJECT = 12; static const uint8_t utf8d[] = { // The first part of the table maps bytes to character classes that From fcb02402c711c1edf442fe2be9f0cbb5aa2a6022 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:50:03 -0500 Subject: [PATCH 866/919] correction on `Core.cpp` --- library/Core.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/Core.cpp b/library/Core.cpp index d7824165c28..793824ac8ed 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -115,6 +115,9 @@ distribution. #include #ifdef WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include #include #endif From 208d550ab7385a085df291c91bb4e26985a210b7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:50:32 -0500 Subject: [PATCH 867/919] add missing include in LuaWrapper.h --- library/include/LuaWrapper.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 7576be7a114..70f36d3f957 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -25,7 +25,8 @@ distribution. #pragma once #include -#include + +#include "DataDefs.h" /** * Internal header file of the lua wrapper. From 5be4f9549252db21cb936a6ae0da7ca472c53060 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:50:53 -0500 Subject: [PATCH 868/919] remove C include from `Graphic.h` --- library/include/modules/Graphic.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h index 9fa498a7cf8..5e30d898054 100644 --- a/library/include/modules/Graphic.h +++ b/library/include/modules/Graphic.h @@ -30,9 +30,9 @@ distribution. #ifndef CL_MOD_GRAPHIC #define CL_MOD_GRAPHIC -#include #include "Export.h" #include "Module.h" + #include namespace DFHack From 371be6a8addd444b893a4813158ecda225d26dd8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:51:55 -0500 Subject: [PATCH 869/919] update includes in `PluginManager.cpp` --- library/PluginManager.cpp | 50 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index b7e0b2c3c42..05486e885d1 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -22,36 +22,48 @@ must not be misrepresented as being the original software. distribution. */ -#include "modules/EventManager.h" -#include "modules/Filesystem.h" -#include "modules/Screen.h" -#include "modules/World.h" -#include "Internal.h" +#include "PluginManager.h" + +#include "ColorText.h" #include "Core.h" +#include "CoreDefs.h" +#include "LuaWrapper.h" +#include "LuaTools.h" #include "MemAccess.h" -#include "PluginManager.h" +#include "MiscUtils.h" #include "RemoteServer.h" -#include "Console.h" #include "Types.h" #include "VersionInfo.h" -#include "DataDefs.h" -#include "MiscUtils.h" -#include "DFHackVersion.h" - -#include "LuaWrapper.h" -#include "LuaTools.h" - -using namespace DFHack; +#include "modules/EventManager.h" +#include "modules/Filesystem.h" +#include "modules/Screen.h" +#include "modules/World.h" +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include -using std::string; +#include +#include + +#include "df/viewscreen.h" -#include +#include +#include + +using namespace DFHack; +using std::string; #if defined(_LINUX) static const string plugin_suffix = ".plug.so"; @@ -871,7 +883,7 @@ void PluginManager::init() loadAll(); bool any_loaded = false; - for (auto p : all_plugins) + for (auto& p : all_plugins) { if (p.second->getState() == Plugin::PS_LOADED) { From 21d865acb19b3f2b2dd2bed168ac118788455530 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 01:53:47 -0500 Subject: [PATCH 870/919] clean headers in `PlugLoad.cpp` --- library/PlugLoad.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index fa58d39514f..6b4dc9d4512 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -1,19 +1,15 @@ -#include "Core.h" #include "Debug.h" -#include "Export.h" #include "PluginManager.h" -#include "Hooks.h" #include #include #include #include -#include -#include #ifdef WIN32 #define NOMINMAX +#define WIN32_LEAN_AND_MEAN #include #include #define global_search_handle() GetModuleHandle(nullptr) From e363b443701886b1e89a6c0af9ddfc23d1a8a520 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:05:31 -0500 Subject: [PATCH 871/919] adjust includes in `PlugLoad.cpp` --- library/PlugLoad.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 6b4dc9d4512..d3b75607739 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -2,10 +2,7 @@ #include "PluginManager.h" #include -#include #include -#include - #ifdef WIN32 #define NOMINMAX From b14b82dc0e0824e175b7fc16167fd0b7f52b0bfc Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:06:22 -0500 Subject: [PATCH 872/919] includes in `Process.cpp` --- library/Process.cpp | 56 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/library/Process.cpp b/library/Process.cpp index c45e5aea456..1ff63622f01 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -22,22 +22,30 @@ must not be misrepresented as being the original software. distribution. */ -#ifndef WIN32 -#ifndef _DARWIN -#include -#endif /* ! _DARWIN */ -#endif /* ! WIN32 */ +#include "Format.h" +#include "MemAccess.h" +#include "Memory.h" +#include "MemoryPatcher.h" +#include "MiscUtils.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" + +#include "modules/Filesystem.h" + +#include +#include +#include #include -#include +#include +#include +#include #include -#include +#include +#include #include #include -#include - -#include "Format.h" -#ifndef WIN32 +#ifdef LINUX_BUILD #include #include #include @@ -53,28 +61,24 @@ distribution. #include #include #endif /* _DARWIN */ -#endif /* ! WIN32 */ -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MemoryPatcher.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" - -#ifndef WIN32 #include "md5wrapper.h" -#else /* WIN32 */ +#endif /* LINUX_BUILD */ + +#ifdef WIN32 #define _WIN32_WINNT 0x0600 #define WINVER 0x0600 + +#define NOMINMAX #define WIN32_LEAN_AND_MEAN #include #include +#include +#include +#include + +#include -#include #endif /* WIN32 */ using namespace DFHack; @@ -151,7 +155,7 @@ Process::Process(const VersionInfoFactory& known_versions) : identified(false) uint32_t pe_offset = readDWord(d->base + 0x3C); read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t*)&(d->pe_header)); const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; - d->sections = (IMAGE_SECTION_HEADER*)malloc(sectionsSize); + d->sections = (IMAGE_SECTION_HEADER*)std::malloc(sectionsSize); read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t*)(d->sections)); } catch (std::exception&) From e3605d6834c8e001c3db7976a6505c10cdaa7112 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:11:21 -0500 Subject: [PATCH 873/919] headers in `RemoteClient.cpp` --- library/RemoteClient.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index b159843761b..c917a3232ae 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -36,24 +36,25 @@ POSSIBILITY OF SUCH DAMAGE. */ -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include -#include #include -#include +#include +#include "ColorText.h" +#include "CoreDefs.h" #include "RemoteClient.h" -#include -#include "MiscUtils.h" -#include -#include -#include +#include "ActiveSocket.h" +#include "Host.h" +#include "SimpleSocket.h" -#include #include "json/json.h" From 6a641dbef43b3181bec0681e795c6dd85d41f662 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:17:24 -0500 Subject: [PATCH 874/919] includes in `RemoteServer.cpp` --- library/RemoteServer.cpp | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 9557a15862f..0e6129b9261 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -34,28 +34,33 @@ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - - -#include -#include -#include +#include +#include +#include +#include +#include +#include #include +#include #include -#include #include -#include +#include +#include + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "Debug.h" +#include "MiscUtils.h" +#include "PluginManager.h" +#include "ActiveSocket.h" +#include "Host.h" +#include "RemoteClient.h" #include "RemoteServer.h" #include "RemoteTools.h" - #include "PassiveSocket.h" -#include "PluginManager.h" -#include "MiscUtils.h" -#include "Debug.h" - -#include -#include -#include +#include "SimpleSocket.h" #include #include From 9b15e57e6dfe0c0211e6c7d7bdd8eb501a72ffec Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:19:14 -0500 Subject: [PATCH 875/919] includes in `Types.cpp` --- library/Types.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/library/Types.cpp b/library/Types.cpp index 1dab657c1c3..21437723f1f 100644 --- a/library/Types.cpp +++ b/library/Types.cpp @@ -22,24 +22,20 @@ must not be misrepresented as being the original software. distribution. */ -#include "Internal.h" -#include "Export.h" #include "MiscUtils.h" -#include "Error.h" #include "Types.h" #include "modules/Filesystem.h" #include "df/general_ref.h" +#include "df/general_ref_type.h" +#include "df/global_objects.h" #include "df/specific_ref.h" +#include "df/specific_ref_type.h" -#include -#include - -#include -#include -#include #include +#include +#include int DFHack::getdir(std::filesystem::path dir, std::vector &files) From d223a385486ed7b7c4b91612110e58cb03d2637a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:25:33 -0500 Subject: [PATCH 876/919] Includes in `RemoteTools.cpp` --- library/RemoteTools.cpp | 63 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index dbef7a99060..5dab76e8cb0 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -35,57 +35,58 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include -#include -#include -#include -#include -#include - #include "RemoteTools.h" -#include "PluginManager.h" + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "DataDefs.h" +#include "DFHackVersion.h" +#include "LuaTools.h" #include "MiscUtils.h" +#include "PluginManager.h" #include "VersionInfo.h" -#include "DFHackVersion.h" #include "modules/Materials.h" #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" -#include "LuaTools.h" - -#include "DataDefs.h" -#include "df/plotinfost.h" #include "df/adventurest.h" -#include "df/world.h" -#include "df/world_data.h" -#include "df/unit.h" -#include "df/unit_misc_trait.h" -#include "df/unit_soul.h" -#include "df/unit_skill.h" +#include "df/creature_raw.h" +#include "df/global_objects.h" +#include "df/historical_entity.h" +#include "df/historical_figure.h" +#include "df/incident.h" +#include "df/inorganic_raw.h" +#include "df/language_name.h" #include "df/material.h" #include "df/matter_state.h" -#include "df/inorganic_raw.h" -#include "df/creature_raw.h" -#include "df/plant_raw.h" #include "df/nemesis_record.h" -#include "df/historical_figure.h" -#include "df/historical_entity.h" -#include "df/squad.h" +#include "df/plant_raw.h" +#include "df/plotinfost.h" +#include "df/profession.h" #include "df/squad_position.h" -#include "df/incident.h" +#include "df/squad.h" +#include "df/unit_misc_trait.h" +#include "df/unit_skill.h" +#include "df/unit_soul.h" +#include "df/unit.h" +#include "df/world_data.h" +#include "df/world.h" #include "BasicApi.pb.h" +#include #include #include -#include - +#include +#include +#include #include +#include +#include +#include using namespace DFHack; using namespace df::enums; From 7c6a6c114465809ab31199d922669d16b7abb67f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:36:00 -0500 Subject: [PATCH 877/919] constrain winsock leakage --- library/RemoteServer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 0e6129b9261..aa3deb201d7 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -34,6 +34,10 @@ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#ifdef WIN32 +#define NOMINMAX +#endif #include #include #include From a1a4ab5b6af94585c4f8266b506b7456c4ad8831 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:36:18 -0500 Subject: [PATCH 878/919] BitArray.h includes --- library/include/BitArray.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 360281c6b14..7658e331020 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -24,14 +24,14 @@ distribution. #pragma once #include "Error.h" -#include -#include -#include #include +#include #include -#include #include +#include +#include +#include namespace DFHack { From 01b36494cb9f5e140175a1ef7ae7cc3c1ba169ab Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:37:16 -0500 Subject: [PATCH 879/919] includes in `DFSDL.h` --- library/include/modules/DFSDL.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 393877e0951..953686deed6 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -1,12 +1,12 @@ #pragma once -#include "Export.h" #include "ColorText.h" +#include "Export.h" #include #include +#include #include -#include struct SDL_Surface; struct SDL_Rect; From ec58ce6a8bc4e51ed8d8549c3fd1092308783952 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Apr 2026 02:37:28 -0500 Subject: [PATCH 880/919] includes in `DFSteam.cpp` --- library/modules/DFSteam.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 61e50a61b8e..31cacfea632 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -1,11 +1,30 @@ -#include "Internal.h" - #include "modules/DFSteam.h" #include "Debug.h" #include "PluginManager.h" +#include "ColorText.h" +#include "Core.h" + +#include +#include +#include +#include +#include + #include "df/gamest.h" +#include + +#ifdef WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#endif namespace DFHack { @@ -100,9 +119,6 @@ void DFSteam::cleanup(color_ostream& out) { #ifdef WIN32 -#include -#include -#include static bool is_running_on_wine() { typedef const char* (CDECL wine_get_version)(void); From 7a4c94417b2f12fe2a106d9805989e5bfc7959a5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 1 May 2026 13:50:18 -0500 Subject: [PATCH 881/919] a few minor adjustments --- library/Core.cpp | 2 -- library/MiscUtils.cpp | 4 ++-- library/PluginManager.cpp | 6 ++---- library/Process.cpp | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 793824ac8ed..c1ab8812b33 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -106,8 +106,6 @@ distribution. #include "md5wrapper.h" -#include - #include #include #include diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index 44f93524eea..d91471803b0 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -48,12 +48,12 @@ distribution. #include #ifdef WIN32 -// Suppress warning which occurs in header on some WinSDK versions -// See dfhack/dfhack#5147 for more information #define NOMINMAX #define WIN32_LEAN_AND_MEAN #include #include +// Suppress warning which occurs in header on some WinSDK versions +// See dfhack/dfhack#5147 for more information #pragma warning(push) #pragma warning(disable:4091) #include diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 05486e885d1..34095898db9 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -27,6 +27,7 @@ distribution. #include "ColorText.h" #include "Core.h" #include "CoreDefs.h" +#include "Format.h" #include "LuaWrapper.h" #include "LuaTools.h" #include "MemAccess.h" @@ -54,13 +55,10 @@ distribution. #include #include -#include -#include - #include "df/viewscreen.h" -#include #include +#include using namespace DFHack; using std::string; diff --git a/library/Process.cpp b/library/Process.cpp index 1ff63622f01..e6b82ccabda 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -30,7 +30,7 @@ distribution. #include "VersionInfo.h" #include "VersionInfoFactory.h" -#include "modules/Filesystem.h" +#include "Modules/Filesystem.h" #include #include From 397d95ade374048c0bbaba0df82cde7f0923e5d4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 1 May 2026 13:54:49 -0500 Subject: [PATCH 882/919] case dependent filesystems --- library/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Process.cpp b/library/Process.cpp index e6b82ccabda..1ff63622f01 100644 --- a/library/Process.cpp +++ b/library/Process.cpp @@ -30,7 +30,7 @@ distribution. #include "VersionInfo.h" #include "VersionInfoFactory.h" -#include "Modules/Filesystem.h" +#include "modules/Filesystem.h" #include #include From 3c2c40db54703d5cd58310c011c25887f9797327 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 19:45:32 +0000 Subject: [PATCH 883/919] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.37.1 → 0.37.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.1...0.37.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b991bfbb742..91a34bdee25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.1 + rev: 0.37.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From ebb18ee3afe1ea81325b3490ef389a46eeab3b79 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 10 May 2026 09:54:51 +0200 Subject: [PATCH 884/919] Handle units without current soul in `Units::getFocusPenalty` --- docs/changelog.txt | 1 + library/modules/Units.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index cfff16e5a46..84b897421d7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,6 +66,7 @@ Template for new versions: ## API - add flexible casting to ``enum_field`` to enable explicit casting to more types +- Handle units without current soul in ``Units::getFocusPenalty`` ## Lua diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index d8bfebdc976..707437ea347 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -2027,6 +2027,9 @@ int32_t Units::getFocusPenalty(df::unit* unit, need_type_set need_types) { CHECK_NULL_POINTER(unit); int max_penalty = INT_MAX; + if (!unit->status.current_soul) { + return max_penalty; + } auto& needs = unit->status.current_soul->personality.needs; for (auto const need : needs) { if (need_types.test(need->id)) { From 6b65dce6a7e26d7cef2bad497945190a85694563 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 10 May 2026 11:58:30 +0200 Subject: [PATCH 885/919] exclude `fmtlib` build artifacts --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 60a0e1b6b2b..8ee4eb5ff19 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ build/compile_commands.json build/dfhack_setarch.txt build/ImportExecutables.cmake build/Testing +build/_deps # Python binding binaries *.pyc From a35a0c333262868144b35f6d0f23baa1f24029c4 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 10 May 2026 15:09:57 +0200 Subject: [PATCH 886/919] improve `autofarm` documentation --- docs/plugins/autofarm.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/plugins/autofarm.rst b/docs/plugins/autofarm.rst index 8896c3117c9..0fe98f2b8b8 100644 --- a/docs/plugins/autofarm.rst +++ b/docs/plugins/autofarm.rst @@ -24,9 +24,7 @@ Usage Sets thresholds of individual plant types. You can find the identifiers for the crop types in your world by running the -following command:: - - lua "for _,plant in ipairs(df.global.world.raws.plants.all) do if plant.flags.SEED then print(plant.id) end end" +following command: ``getplants -f`` Examples -------- From 9a1d5b86fdcaf329dbc5be81356e15eb419cd968 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 13 May 2026 16:07:06 +0000 Subject: [PATCH 887/919] Auto-update structures ref for 53.13 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index cab90ab30c4..ab0b7648862 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit cab90ab30c4f52796aec9f83ce21c2fa5974494b +Subproject commit ab0b7648862b4718de07bc1e30a5ac0d9d1b1c89 From eec022051369f45e227c371bed9ac8ea2ea8c6af Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 13 May 2026 11:19:18 -0500 Subject: [PATCH 888/919] update CMakeLists for 53.13 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ebc0ffe3102..815a2df89a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.12") +set(DF_VERSION "53.13") set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) From 3c9e10d46e4738e26068c4a74a70fd67924a3470 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 13 May 2026 17:09:09 +0000 Subject: [PATCH 889/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index ab0b7648862..392d6522cd2 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ab0b7648862b4718de07bc1e30a5ac0d9d1b1c89 +Subproject commit 392d6522cd2f88fdc36c978944e508f17cb43ad3 From 5611803aaf10b32fdd13187026ea994d863c9f68 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 13 May 2026 17:50:45 +0000 Subject: [PATCH 890/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 392d6522cd2..ee2b7c73806 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 392d6522cd2f88fdc36c978944e508f17cb43ad3 +Subproject commit ee2b7c73806f9652d6524d3324673600c04f6481 From 66dddaaea5832149bc3f4a21ba7b00ecc2dc352e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 13 May 2026 13:55:17 -0500 Subject: [PATCH 891/919] Changelog and structures for 53.13-r1 --- docs/changelog.txt | 19 +++++++++++++++++++ library/xml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 84b897421d7..6b87efe40d6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,25 @@ Template for new versions: ## Documentation +## API + +## Lua + +## Removed + +# 53.13-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation +- updated documentation for ``autofarm`` for more clarity + ## API - add flexible casting to ``enum_field`` to enable explicit casting to more types - Handle units without current soul in ``Units::getFocusPenalty`` diff --git a/library/xml b/library/xml index ee2b7c73806..82e62e90f0f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ee2b7c73806f9652d6524d3324673600c04f6481 +Subproject commit 82e62e90f0f50f143dcb3eca1698344ff39c45dd From 4a48d49429941ece57085a814ac9b202554f1429 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 14 May 2026 19:08:10 -0500 Subject: [PATCH 892/919] remove announcement culling from `Gui` module --- docs/changelog.txt | 1 + library/modules/Gui.cpp | 39 +++------------------------------------ 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6b87efe40d6..fa85b98865e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: ## New Features ## Fixes +- ``Gui::makeAnnoucement``, ``Gui::showPopupAnnouncement`, and ``Gui::autoDFAnnouncement`` will no longer attempt to cull the DF announcement vector ## Misc Improvements diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 68d63e68de1..06de785a390 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -105,10 +105,9 @@ using std::string; using std::vector; using namespace DFHack; -const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size -const int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE -const int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 2 sec at 50 GFPS -const int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 40 sec at 50 GFPS +static constexpr int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE +static constexpr int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 2 sec at 50 GFPS +static constexpr int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 40 sec at 50 GFPS namespace DFHack { @@ -1928,18 +1927,6 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce world->status.display_timer = ANNOUNCE_DISPLAY_TIME; } - // Delete excess reports - while (reports.size() > MAX_REPORTS_SIZE) - { // Report destructor - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - return world->status.reports.size() - 1; } @@ -2032,14 +2019,6 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright) auto &popups = world->status.popups; popups.push_back(popup); - // Delete excess popups - while (popups.size() > MAX_REPORTS_SIZE) - { - if (popups[0] != NULL) - delete popups[0]; - popups.erase(popups.begin()); - } - Gui::MTB_clean(&world->status.mega_text); Gui::MTB_parse(&world->status.mega_text, popups[0]->text); Gui::MTB_set_width(&world->status.mega_text); @@ -2268,18 +2247,6 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) world->status.display_timer = info.display_timer; } - // Delete excess reports - while (reports.size() > MAX_REPORTS_SIZE) - { // Report destructor - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - if (*gamemode == game_mode::DWARF || // Did dwarf announcement or UCR (*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || // Did adventure announcement (a_flags.bits.DO_MEGA && !adv_unconscious)) // Did popup From 2d5ae9a7c012d5c66b60ae55bce3877be37bcc9b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 15 May 2026 09:48:30 +0000 Subject: [PATCH 893/919] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 82e62e90f0f..79749f5bb15 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 82e62e90f0f50f143dcb3eca1698344ff39c45dd +Subproject commit 79749f5bb15a50f7b4a065aae05a1fc53a6ab462 From e4dc45e36ebc6de37dbed02b40a7ed1e26f5b96a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 16 May 2026 07:54:48 -0500 Subject: [PATCH 894/919] CMakelist & changelog for 53.13-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 815a2df89a2..6120219faed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.13") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index fa85b98865e..bf0171b3184 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,25 @@ Template for new versions: # Future + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.13-r2 + ## New Tools ## New Features From e53187c8b975f71c5c356d9a587563009684bc89 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 16 May 2026 07:57:00 -0500 Subject: [PATCH 895/919] structures for 53.13-r2 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 79749f5bb15..5ac7a801297 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 79749f5bb15a50f7b4a065aae05a1fc53a6ab462 +Subproject commit 5ac7a801297a604ff2a58d7235291b33535a64a0 From 6c7a24c910fea5220ee462a238040a06c8822bd7 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 20 May 2026 10:24:50 +0000 Subject: [PATCH 896/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index af457f9b5c8..e320f138ffa 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit af457f9b5c86fc041a064ef5bdfbcab38f38a50f +Subproject commit e320f138ffa762f6f7533451d8adcdfb769cf1a1 From fa1165bce8c0c5630d93a32efea5f2fb29c903ad Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 20 May 2026 13:18:43 +0000 Subject: [PATCH 897/919] Auto-update submodules library/xml: master scripts: master plugins/stonesense: master --- library/xml | 2 +- plugins/stonesense | 2 +- scripts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/xml b/library/xml index 5ac7a801297..37bd2498d42 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 5ac7a801297a604ff2a58d7235291b33535a64a0 +Subproject commit 37bd2498d42f26ba904abe04b38341c625f5f3b3 diff --git a/plugins/stonesense b/plugins/stonesense index c8ddd2c5238..32471bc1e76 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit c8ddd2c52387d32f06d7c99d83b9303e3038b47b +Subproject commit 32471bc1e76d29ca0051d61a1fba4fc104f240e9 diff --git a/scripts b/scripts index e320f138ffa..66c1ff627b4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e320f138ffa762f6f7533451d8adcdfb769cf1a1 +Subproject commit 66c1ff627b4bf195e0576eaa2e4dbe2146ba6fb8 From 108dd8c1e842f1c3e97f9d1ab9a1d37fe38ebdc9 Mon Sep 17 00:00:00 2001 From: ab9rf <1445859+ab9rf@users.noreply.github.com> Date: Wed, 20 May 2026 16:07:56 +0000 Subject: [PATCH 898/919] Auto-update structures ref for 53.14 --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 37bd2498d42..01aae95cacd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 37bd2498d42f26ba904abe04b38341c625f5f3b3 +Subproject commit 01aae95cacd98850e4f477c45a4b75f800bacecc From ac13e275a2b7da55566118972762798dcae64d79 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 20 May 2026 11:16:18 -0500 Subject: [PATCH 899/919] Update version to 53.14-r1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6120219faed..62f55d0b4f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "53.13") -set(DFHACK_RELEASE "r2") +set(DF_VERSION "53.14") +set(DFHACK_RELEASE "r1") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") From a58f695ff3b521b94fba64ddd6fdeddd60542e32 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 20 May 2026 11:22:14 -0500 Subject: [PATCH 900/919] Changelog for 53.14 --- docs/changelog.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bf0171b3184..113fc16ba9c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,10 +54,28 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.14-r1 ## New Tools ## New Features +- Compatibility with Dwarf Fortress 53.14 ## Fixes From 128014165b230b7b7e437d5e3435fbf6c6cc9137 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 20 May 2026 17:03:39 +0000 Subject: [PATCH 901/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 66c1ff627b4..3ce62a5ade3 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 66c1ff627b4bf195e0576eaa2e4dbe2146ba6fb8 +Subproject commit 3ce62a5ade38c3ddb266294017e9a151772a09c9 From c2f6e92abc1a18b9edf377a180b768d0d4eff76c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 21 May 2026 11:26:37 -0500 Subject: [PATCH 902/919] protect DF pooled objects from being deleted --- docs/changelog.txt | 1 + library/include/DataDefs.h | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 113fc16ba9c..735ddddf4ed 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap ## Documentation diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 88c4a93651d..aabcfdd9dbf 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -24,14 +24,15 @@ distribution. #pragma once +#include #include #include #include +#include #include #include #include #include -#include #include "BitArray.h" #include "Export.h" @@ -572,15 +573,21 @@ namespace df * */ + template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; + template concept copy_assignable = std::assignable_from && std::assignable_from; template void *allocator_fn(void *out, const void *in) { - if (out) + // unerase type + T* _out = out ? reinterpret_cast(out) : nullptr; + const T* _in = in ? reinterpret_cast(in) : nullptr; + + if (_out) { if constexpr (copy_assignable) { - *(T*)out = *(const T*)in; + *_out = *_in; return out; } else @@ -588,13 +595,20 @@ namespace df return nullptr; } } - else if (in) + else if (_in) { + if constexpr (pooled_object) + { + if (_in->pool_id != -1) + { + throw std::runtime_error("Pool-allocated type cannot be deallocated with allocator_fn"); + } + } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - delete (T*)in; + delete _in; #pragma GCC diagnostic pop - return (T*)in; + return const_cast(in); } else return new T(); From ab59c374923cba0ccba5c0a32154d37cf49f6c4f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 21 May 2026 11:39:03 -0500 Subject: [PATCH 903/919] `pool_id`s are `size_t` --- library/include/DataDefs.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index aabcfdd9dbf..37892391a48 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -573,12 +573,14 @@ namespace df * */ - template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; + using df_pool_id_t = size_t; + template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; template concept copy_assignable = std::assignable_from && std::assignable_from; template void *allocator_fn(void *out, const void *in) { + constexpr df_pool_id_t invalid_pool_id = static_cast(-1); // unerase type T* _out = out ? reinterpret_cast(out) : nullptr; const T* _in = in ? reinterpret_cast(in) : nullptr; @@ -599,7 +601,7 @@ namespace df { if constexpr (pooled_object) { - if (_in->pool_id != -1) + if (_in->pool_id != invalid_pool_id) { throw std::runtime_error("Pool-allocated type cannot be deallocated with allocator_fn"); } From d86cf1928ae2a3c4574eb50531ca939dfc102bb8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 10:51:08 -0500 Subject: [PATCH 904/919] Remove ``logcleaner`` --- docs/changelog.txt | 1 + docs/plugins/logcleaner.rst | 62 ---------- plugins/CMakeLists.txt | 1 - plugins/logcleaner/logcleaner.cpp | 190 ------------------------------ plugins/lua/logcleaner.lua | 85 ------------- 5 files changed, 1 insertion(+), 338 deletions(-) delete mode 100644 docs/plugins/logcleaner.rst delete mode 100644 plugins/logcleaner/logcleaner.cpp delete mode 100644 plugins/lua/logcleaner.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index 735ddddf4ed..8d723a4c014 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,6 +70,7 @@ Template for new versions: ## Lua ## Removed +- ``logcleaner``: Removed (cannot be safely implemented at this time) # 53.14-r1 diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst deleted file mode 100644 index 6e4cba07d6b..00000000000 --- a/docs/plugins/logcleaner.rst +++ /dev/null @@ -1,62 +0,0 @@ -logcleaner -========== -.. dfhack-tool:: - :summary: Automatically clear combat, sparring, and hunting reports. - :tags: fort auto units - -This plugin prevents spam from cluttering your announcement history and filling -the 3000-item reports buffer. It runs approximately every 100 ticks and clears -selected report types from both the global reports buffer and per-unit logs. - -Usage ------ - -Basic commands -~~~~~~~~~~~~~~ - -``logcleaner`` - Show the current status of the plugin. -``logcleaner enable`` - Enable the plugin (persists per save). -``logcleaner disable`` - Disable the plugin. - -Configuring filters -~~~~~~~~~~~~~~~~~~~ - -``logcleaner combat`` - Clear combat reports (also enables the plugin if disabled). -``logcleaner sparring`` - Clear sparring reports. -``logcleaner hunting`` - Clear hunting reports. -``logcleaner combat,sparring`` - Clear multiple report types (comma-separated). -``logcleaner all`` - Enable all three filter types. -``logcleaner none`` - Disable all filter types. - -Examples -~~~~~~~~ - -Clear only sparring reports:: - - logcleaner sparring - -Clear combat and hunting, but not sparring:: - - logcleaner combat,hunting - -Overlay UI ----------- - -Run ``gui/logcleaner`` to open the settings overlay, or access it from the -control panel under the Gameplay tab. - -The overlay provides: - -- **Enable toggle**: Turn the plugin on or off (``Shift+E``) -- **Combat toggle**: Clear combat reports (``Shift+C``) -- **Sparring toggle**: Clear sparring reports (``Shift+S``) -- **Hunting toggle**: Clear hunting reports (``Shift+H``) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4a4423f48f2..355cc0ac4c0 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -66,7 +66,6 @@ if(BUILD_SUPPORTED) dfhack_plugin(createitem createitem.cpp) dfhack_plugin(cursecheck cursecheck.cpp) dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) - dfhack_plugin(logcleaner logcleaner/logcleaner.cpp LINK_LIBRARIES lua) dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(dig dig.cpp LINK_LIBRARIES lua) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp deleted file mode 100644 index 89f84cf32f8..00000000000 --- a/plugins/logcleaner/logcleaner.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "LuaTools.h" -#include "PluginManager.h" -#include "PluginLua.h" - -#include "modules/Persistence.h" -#include "modules/World.h" - -#include -#include -#include -#include - -using namespace DFHack; - -DFHACK_PLUGIN("logcleaner"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -REQUIRE_GLOBAL(world); - -static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; -static PersistentDataItem config; - -enum ConfigValues { - CONFIG_IS_ENABLED = 0, - CONFIG_CLEAR_COMBAT = 1, - CONFIG_CLEAR_SPARING = 2, - CONFIG_CLEAR_HUNTING = 3, -}; - -static bool clear_combat = false; -static bool clear_sparring = true; -static bool clear_hunting = false; - -static constexpr int32_t CYCLE_TICKS = 97; -static int32_t cycle_timestamp = 0; - -static void cleanupLogs(); -static command_result do_command(color_ostream& out, std::vector& params); - -// Getter functions for Lua -static bool logcleaner_getCombat() { return clear_combat; } -static bool logcleaner_getSparring() { return clear_sparring; } -static bool logcleaner_getHunting() { return clear_hunting; } - -// Setter functions for Lua (also persist to config) -static void logcleaner_setCombat(bool val) { - clear_combat = val; - config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); -} - -static void logcleaner_setSparring(bool val) { - clear_sparring = val; - config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); -} - -static void logcleaner_setHunting(bool val) { - clear_hunting = val; - config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); -} - -DFhackCExport command_result plugin_init(color_ostream& out, std::vector& commands) { - commands.push_back(PluginCommand( - plugin_name, - "Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).", - do_command)); - - return CR_OK; -} - -static command_result do_command(color_ostream& out, std::vector& params) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - bool show_help = false; - if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params, - 1, [&](lua_State *L) { - show_help = !lua_toboolean(L, -1); - })) { - return CR_FAILURE; - } - - return show_help ? CR_WRONG_USAGE : CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (enable != is_enabled) { - is_enabled = enable; - config.set_bool(CONFIG_IS_ENABLED, is_enabled); - } - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream& out) { - return CR_OK; -} - -DFhackCExport command_result plugin_load_site_data(color_ostream& out) { - config = World::GetPersistentSiteData(CONFIG_KEY); - - if (!config.isValid()) { - config = World::AddPersistentSiteData(CONFIG_KEY); - config.set_bool(CONFIG_IS_ENABLED, is_enabled); - config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); - config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); - config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); - } - - is_enabled = config.get_bool(CONFIG_IS_ENABLED); - clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT); - clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); - clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); - - cycle_timestamp = 0; - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - is_enabled = false; - } - return CR_OK; -} - -static void cleanupLogs() { - if (!is_enabled || !world) - return; - - if (!clear_combat && !clear_sparring && !clear_hunting) - return; - - // Collect all report IDs from unit combat/sparring/hunting logs - std::unordered_set report_ids_to_remove; - bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; - - for (auto unit : world->units.all) { - for (int log_idx = 0; log_idx < 3; log_idx++) { - if (log_types[log_idx]) { - auto& log = unit->reports.log[log_idx]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); - } - } - } - - if (report_ids_to_remove.empty()) - return; - - // Remove collected reports from global buffers - auto& reports = world->status.reports; - - std::erase_if(reports, [&](df::report* report) { - if (!report || !report_ids_to_remove.contains(report->id)) - return false; - if (report->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, report->id); - delete report; - return true; - }); -} - -DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { - if (!is_enabled || !world) - return CR_OK; - else if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) - return CR_OK; - - cycle_timestamp = world->frame_counter; - cleanupLogs(); - - return CR_OK; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(logcleaner_getCombat), - DFHACK_LUA_FUNCTION(logcleaner_getSparring), - DFHACK_LUA_FUNCTION(logcleaner_getHunting), - DFHACK_LUA_FUNCTION(logcleaner_setCombat), - DFHACK_LUA_FUNCTION(logcleaner_setSparring), - DFHACK_LUA_FUNCTION(logcleaner_setHunting), - DFHACK_LUA_END -}; diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua deleted file mode 100644 index 104c23ab88f..00000000000 --- a/plugins/lua/logcleaner.lua +++ /dev/null @@ -1,85 +0,0 @@ -local _ENV = mkmodule('plugins.logcleaner') - -local function print_status() - print(('logcleaner is %s'):format(isEnabled() and "enabled" or "disabled")) - print(' Combat: ' .. (logcleaner_getCombat() and 'enabled' or 'disabled')) - print(' Sparring: ' .. (logcleaner_getSparring() and 'enabled' or 'disabled')) - print(' Hunting: ' .. (logcleaner_getHunting() and 'enabled' or 'disabled')) -end - -function parse_commandline(...) - local args = {...} - local command = args[1] - - -- Show status if no command or "status" - if not command or command == 'status' then - print_status() - return true - end - - -- Handle enable/disable commands - if command == 'enable' then - if isEnabled() then - print('logcleaner is already enabled') - else - dfhack.run_command('enable', 'logcleaner') - print('logcleaner enabled') - end - return true - end - - if command == 'disable' then - if not isEnabled() then - print('logcleaner is already disabled') - else - dfhack.run_command('disable', 'logcleaner') - print('logcleaner disabled') - end - return true - end - - -- Start with all disabled, enable only what's specified - local new_combat, new_sparring, new_hunting = false, false, false - local has_filter = false - - if command == 'all' then - new_combat, new_sparring, new_hunting = true, true, true - has_filter = true - elseif command == 'none' then - new_combat, new_sparring, new_hunting = false, false, false - else - -- Split by comma for multiple options in one parameter - for token in command:gmatch('([^,]+)') do - if token == 'combat' then - new_combat = true - has_filter = true - elseif token == 'sparring' then - new_sparring = true - has_filter = true - elseif token == 'hunting' then - new_hunting = true - has_filter = true - else - dfhack.printerr('Unknown option: ' .. token) - return false - end - end - end - - -- Auto-enable plugin when filters are being configured - if has_filter and not isEnabled() then - dfhack.run_command('enable', 'logcleaner') - print('logcleaner enabled') - end - - logcleaner_setCombat(new_combat) - logcleaner_setSparring(new_sparring) - logcleaner_setHunting(new_hunting) - - print('Log cleaning config updated:') - print_status() - - return true -end - -return _ENV From bdbb52f45f88f0895b55f72de677216114260869 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 11:14:21 -0500 Subject: [PATCH 905/919] tombstone `logcleaner` documentation --- docs/about/Removed.rst | 12 ++++++++++++ docs/changelog.txt | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 5f73b86be38..9ef7781ebf1 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,6 +258,11 @@ gui/hack-wish ============= Replaced by `gui/create-item`. +gui/log-cleaner +=============== +Removed because changes to Dwarf Fortress internals made the functionality +impossible to implement safely. + .. _gui/manager-quantity: gui/manager-quantity @@ -278,6 +283,13 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that ``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no longer necessary. +.. _logcleaner: + +logcleaner +=============== +Removed because changes to Dwarf Fortress internals made the functionality +impossible to implement safely. + .. _masspit: masspit diff --git a/docs/changelog.txt b/docs/changelog.txt index 8d723a4c014..cddef2d75c3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -70,7 +70,7 @@ Template for new versions: ## Lua ## Removed -- ``logcleaner``: Removed (cannot be safely implemented at this time) +- `logcleaner`: Removed (cannot be safely implemented at this time) # 53.14-r1 From 4f31fff8290487c3f5bbcf93c8d741ea83f1587d Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 11:53:38 -0500 Subject: [PATCH 906/919] Update Removed.rst --- docs/about/Removed.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 9ef7781ebf1..b0a7f469a18 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,7 +258,7 @@ gui/hack-wish ============= Replaced by `gui/create-item`. -gui/log-cleaner +gui/logcleaner =============== Removed because changes to Dwarf Fortress internals made the functionality impossible to implement safely. From 6e85d8551316c8bd826b1665c2d8afb8f8d32120 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 12:23:59 -0500 Subject: [PATCH 907/919] Update Removed.rst --- docs/about/Removed.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index b0a7f469a18..cf9bf95b090 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,6 +258,7 @@ gui/hack-wish ============= Replaced by `gui/create-item`. +.. _gui/logcleaner gui/logcleaner =============== Removed because changes to Dwarf Fortress internals made the functionality From 360d0f76d79b111bc755c27a784e75c3018adaf6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 12:39:10 -0500 Subject: [PATCH 908/919] Update Removed.rst sigh --- docs/about/Removed.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index cf9bf95b090..96eaa3c1423 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,7 +258,8 @@ gui/hack-wish ============= Replaced by `gui/create-item`. -.. _gui/logcleaner +.. _gui/logcleaner: + gui/logcleaner =============== Removed because changes to Dwarf Fortress internals made the functionality From 7db509ea6529b078b3615d58f8f31050896703df Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 22 May 2026 18:17:30 +0000 Subject: [PATCH 909/919] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 3ce62a5ade3..364dd46ab03 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 3ce62a5ade38c3ddb266294017e9a151772a09c9 +Subproject commit 364dd46ab037521a34ade84bb7bf3ce3638723f4 From 2e2b92130d9f0c1bddbdd20cec262ae5cdb050e7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 May 2026 13:58:23 -0500 Subject: [PATCH 910/919] update version, changelog and scripts for 53.14-r2 --- CMakeLists.txt | 2 +- docs/changelog.txt | 18 ++++++++++++++++++ scripts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 62f55d0b4f8..dff1b7fac32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW) # set up versioning. set(DF_VERSION "53.14") -set(DFHACK_RELEASE "r1") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/docs/changelog.txt b/docs/changelog.txt index cddef2d75c3..70e55aff58b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,24 @@ Template for new versions: ## Fixes +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.14-r2 + +## New Tools + +## New Features + +## Fixes + ## Misc Improvements - Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap diff --git a/scripts b/scripts index 364dd46ab03..92aec15dd7e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 364dd46ab037521a34ade84bb7bf3ce3638723f4 +Subproject commit 92aec15dd7eff76b76ff74efb3164e31b7f0e5bd From 91458a222a7adee32901f05422535c6733f85f26 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 23 May 2026 00:16:26 -0500 Subject: [PATCH 911/919] remove dead `Graphic` module This module has been without purpose for at least a decade; nothing uses it I don't think it's had a purpose since DFHack became an in-process toolset --- library/CMakeLists.txt | 2 - library/Core.cpp | 2 - library/include/Core.h | 5 -- library/include/modules/DFSDL.h | 11 ---- library/include/modules/Graphic.h | 59 -------------------- library/modules/Graphic.cpp | 93 ------------------------------- 6 files changed, 172 deletions(-) delete mode 100644 library/include/modules/Graphic.h delete mode 100644 library/modules/Graphic.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index f91afef2e2a..6da49099681 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -157,7 +157,6 @@ set(MODULE_HEADERS include/modules/Designations.h include/modules/EventManager.h include/modules/Filesystem.h - include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h include/modules/Hotkey.h @@ -189,7 +188,6 @@ set(MODULE_SOURCES modules/Designations.cpp modules/EventManager.cpp modules/Filesystem.cpp - modules/Graphic.cpp modules/Gui.cpp modules/Hotkey.cpp modules/Items.cpp diff --git a/library/Core.cpp b/library/Core.cpp index c1ab8812b33..62a280a187b 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -52,7 +52,6 @@ distribution. #include "modules/DFSteam.h" #include "modules/EventManager.h" #include "modules/Filesystem.h" -#include "modules/Graphic.h" #include "modules/Gui.h" #include "modules/Hotkey.h" #include "modules/Persistence.h" @@ -2203,4 +2202,3 @@ TYPE * Core::get##TYPE() \ } MODULE_GETTER(Materials); -MODULE_GETTER(Graphic); diff --git a/library/include/Core.h b/library/include/Core.h index 9ccf0ca8710..ee50a83080a 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -29,8 +29,6 @@ distribution. #include "Export.h" #include "Hooks.h" -#include "modules/Graphic.h" - #include #include #include @@ -166,8 +164,6 @@ namespace DFHack /// get the materials module Materials * getMaterials(); - /// get the graphic module - Graphic * getGraphic(); command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream& out, const std::string& command); @@ -300,7 +296,6 @@ namespace DFHack struct { Materials * pMaterials; - Graphic * pGraphic; } s_mods; std::vector> allModules; DFHack::PluginManager *plug_mgr; diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 953686deed6..7d53242ad03 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -16,17 +16,6 @@ struct SDL_Window; union SDL_Event; using SDL_Keycode = int32_t; -namespace DFHack -{ - struct DFTileSurface - { - bool paintOver; // draw over original tile? - SDL_Surface* surface; // from where it should be drawn - SDL_Rect* rect; // from which coords (NULL to draw whole surface) - SDL_Rect* dstResize; // if not NULL dst rect will be resized (x/y/w/h will be added to original dst) - }; -} - /** * The DFSDL module - provides access to SDL functions without actually * requiring build-time linkage to SDL diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h deleted file mode 100644 index 5e30d898054..00000000000 --- a/library/include/modules/Graphic.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mr�zek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -/******************************************************************************* - GRAPHIC - Changing tile cache -*******************************************************************************/ -#pragma once -#ifndef CL_MOD_GRAPHIC -#define CL_MOD_GRAPHIC - -#include "Export.h" -#include "Module.h" - -#include - -namespace DFHack -{ - // forward declaration used here instead of including DFSDL.h to reduce inclusion loading - struct DFTileSurface; - - class DFHACK_EXPORT Graphic : public Module - { - public: - bool Finish() - { - return true; - } - bool Register(DFTileSurface* (*func)(int,int)); - bool Unregister(DFTileSurface* (*func)(int,int)); - DFTileSurface* Call(int x, int y); - - private: - std::vector funcs; - }; -} - -#endif diff --git a/library/modules/Graphic.cpp b/library/modules/Graphic.cpp deleted file mode 100644 index b55ee83ed4e..00000000000 --- a/library/modules/Graphic.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mr�zek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - - -#include "Internal.h" - -#include -#include -#include -#include -#include -using namespace std; - -#include "modules/Graphic.h" -#include "Error.h" -#include "VersionInfo.h" -#include "MemAccess.h" -#include "MiscUtils.h" -#include "ModuleFactory.h" -#include "Core.h" - -using namespace DFHack; - -std::unique_ptr DFHack::createGraphic() -{ - return std::make_unique(); -} - -bool Graphic::Register(DFTileSurface* (*func)(int,int)) -{ - funcs.push_back(func); - return true; -} - -bool Graphic::Unregister(DFTileSurface* (*func)(int,int)) -{ - if ( funcs.empty() ) return false; - - vector::iterator it = funcs.begin(); - while ( it != funcs.end() ) - { - if ( *it == func ) - { - funcs.erase(it); - return true; - } - it++; - } - - return false; -} - -// This will return first DFTileSurface it can get (or NULL if theres none) -DFTileSurface* Graphic::Call(int x, int y) -{ - if ( funcs.empty() ) return NULL; - - DFTileSurface* temp = NULL; - - vector::iterator it = funcs.begin(); - while ( it != funcs.end() ) - { - temp = (*it)(x,y); - if ( temp != NULL ) - { - return temp; - } - it++; - } - - return NULL; -} From b799dd6366a4dc5a61ed3d51a2ce4079c94758de Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 23 May 2026 02:42:46 -0500 Subject: [PATCH 912/919] remove use of `Materials` module from `probe` also remove UB use of a union and clean up headers (at least somewhat) --- plugins/probe.cpp | 96 +++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/plugins/probe.cpp b/plugins/probe.cpp index f49e167867b..9df22004063 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -1,11 +1,16 @@ +#include +#include +#include + #include "LuaTools.h" +#include "MiscUtils.h" #include "PluginManager.h" #include "TileTypes.h" #include "modules/Gui.h" -#include "modules/Materials.h" #include "modules/MapCache.h" #include "modules/Maps.h" +#include "modules/Materials.h" #include "df/block_square_event_grassst.h" #include "df/block_square_event_world_constructionst.h" @@ -15,6 +20,8 @@ #include "df/civzone_type.h" #include "df/construction_type.h" #include "df/furnace_type.h" +#include "df/global_objects.h" +#include "df/inorganic_raw.h" #include "df/item.h" #include "df/map_block.h" #include "df/region_map_entry.h" @@ -86,10 +93,8 @@ static void describeTile(color_ostream &out, df::tiletype tiletype) { } static command_result df_probe(color_ostream &out, vector & parameters) { - DFHack::Materials *Materials = Core::getInstance().getMaterials(); - vector inorganic; - bool hasmats = Materials->CopyInorganicMaterials(inorganic); + auto& inorganic = world->raws.inorganics.all; if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); @@ -173,29 +178,25 @@ static command_result df_probe(color_ostream &out, vector & parameters) out << "geolayer: " << des.bits.geolayer_index << std::endl; int16_t base_rock = mc.layerMaterialAt(cursor); - if (base_rock != -1) { + if (base_rock != -1) + { out << "Layer material: " << std::dec << base_rock; - if(hasmats) - out << " / " << inorganic[base_rock].id - << " / " - << inorganic[base_rock].name - << std::endl; - else - out << std::endl; + out << " / " << inorganic[base_rock]->id + << " / " + << inorganic[base_rock]->material.stone_name + << std::endl; } int16_t vein_rock = mc.veinMaterialAt(cursor); - if (vein_rock != -1) { + if (vein_rock != -1) + { out << "Vein material (final): " << std::dec << vein_rock; - if(hasmats) - out << " / " << inorganic[vein_rock].id - << " / " - << inorganic[vein_rock].name - << " (" - << ENUM_KEY_STR(inclusion_type,b->veinTypeAt(cursor)) - << ")" - << std::endl; - else - out << std::endl; + out << " / " << inorganic[vein_rock]->id + << " / " + << inorganic[vein_rock]->material.stone_name + << " (" + << ENUM_KEY_STR(inclusion_type, b->veinTypeAt(cursor)) + << ")" + << std::endl; } MaterialInfo minfo(mc.baseMaterialAt(cursor)); if (minfo.isValid()) @@ -309,17 +310,6 @@ static command_result df_probe(color_ostream &out, vector & parameters) return CR_OK; } -union Subtype { - int16_t subtype; - df::civzone_type civzone_type; - df::furnace_type furnace_type; - df::workshop_type workshop_type; - df::construction_type construction_type; - df::shop_type shop_type; - df::siegeengine_type siegeengine_type; - df::trap_type trap_type; -}; - static command_result df_bprobe(color_ostream &out, vector & parameters) { auto bld = Gui::getSelectedBuilding(out); if (!bld) @@ -329,7 +319,7 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) bld->getName(&name); auto bld_type = bld->getType(); - Subtype subtype{bld->getSubtype()}; + int16_t subtype{bld->getSubtype()}; int32_t custom = bld->getCustomType(); out.print("Building {:<4}, \"{}\", type {} ({})", @@ -342,46 +332,46 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) switch (bld_type) { case df::building_type::Civzone: out.print(", subtype {} ({})", - ENUM_KEY_STR(civzone_type, subtype.civzone_type), - subtype.subtype); + ENUM_KEY_STR(civzone_type, static_cast(subtype)), + subtype); break; case df::building_type::Furnace: out.print(", subtype {} ({})", - ENUM_KEY_STR(furnace_type, subtype.furnace_type), - subtype.subtype); - if (subtype.furnace_type == df::furnace_type::Custom) + ENUM_KEY_STR(furnace_type, static_cast(subtype)), + subtype); + if (static_cast(subtype) == df::furnace_type::Custom) out.print(", custom type {} ({})", world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Workshop: out.print(", subtype {} ({})", - ENUM_KEY_STR(workshop_type, subtype.workshop_type), - subtype.subtype); - if (subtype.workshop_type == df::workshop_type::Custom) + ENUM_KEY_STR(workshop_type, static_cast(subtype)), + subtype); + if (subtype == static_cast(df::workshop_type::Custom)) out.print(", custom type {} ({})", world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Construction: out.print(", subtype {} ({})", - ENUM_KEY_STR(construction_type, subtype.construction_type), - subtype.subtype); + ENUM_KEY_STR(construction_type, static_cast(subtype)), + subtype); break; case df::building_type::Shop: out.print(", subtype {} ({})", - ENUM_KEY_STR(shop_type, subtype.shop_type), - subtype.subtype); + ENUM_KEY_STR(shop_type, static_cast(subtype)), + subtype); break; case df::building_type::SiegeEngine: out.print(", subtype {} ({})", - ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type), - subtype.subtype); + ENUM_KEY_STR(siegeengine_type, static_cast(subtype)), + subtype); break; case df::building_type::Trap: out.print(", subtype {} ({})", - ENUM_KEY_STR(trap_type, subtype.trap_type), - subtype.subtype); + ENUM_KEY_STR(trap_type, static_cast(subtype)), + subtype); break; case df::building_type::NestBox: { @@ -391,8 +381,8 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) break; } default: - if (subtype.subtype != -1) - out.print(", subtype {}", subtype.subtype); + if (subtype != -1) + out.print(", subtype {}", subtype); break; } From faea23b361913eefc25c10f1bcf7be98768bf575 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 24 May 2026 04:17:39 +0000 Subject: [PATCH 913/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 32471bc1e76..992957670fd 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 32471bc1e76d29ca0051d61a1fba4fc104f240e9 +Subproject commit 992957670fd99b55d10196cf2850a4554bb15374 From d29682533d2f7a9d8cebc1d1a8c63a7046542bc1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 23 May 2026 23:37:31 -0500 Subject: [PATCH 914/919] Remove `Materials` module from `prospector` was unused - the plugin only obtained the handle and then disposed it without using it for anything --- plugins/prospector.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 035d6cdee12..5dd712e872e 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -610,9 +610,7 @@ static command_result embark_prospector(color_ostream &out, } if (options.hidden) { - DFHack::Materials *mats = Core::getInstance().getMaterials(); printVeins(out, veinMats, options); - mats->Finish(); } out << "Embark depth: " << (world_bottom.upper_z-world_bottom.lower_z+1) << " "; @@ -635,8 +633,6 @@ static command_result map_prospector(color_ostream &con, Maps::getSize(x_max, y_max, z_max); MapExtras::MapCache map; - DFHack::Materials *mats = Core::getInstance().getMaterials(); - DFHack::t_feature blockFeatureGlobal; DFHack::t_feature blockFeatureLocal; @@ -894,9 +890,6 @@ static command_result map_prospector(color_ostream &con, printMats(con, treeMats, world->raws.plants.all, options); } - // Cleanup - mats->Finish(); - return CR_OK; } From e82a2e1c8f45f027e8b7b77ba35f8a1f652d4bb2 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 00:30:19 -0500 Subject: [PATCH 915/919] Remove `Materials` module --- library/Core.cpp | 2 - library/include/Core.h | 5 - library/include/modules/Materials.h | 134 +++++----- library/modules/Materials.cpp | 383 +++++----------------------- 4 files changed, 131 insertions(+), 393 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 62a280a187b..50c6fe5f8bd 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2200,5 +2200,3 @@ TYPE * Core::get##TYPE() \ }\ return s_mods.p##TYPE;\ } - -MODULE_GETTER(Materials); diff --git a/library/include/Core.h b/library/include/Core.h index ee50a83080a..085dd33cf1a 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -61,7 +61,6 @@ namespace DFHack class Process; class Module; - class Materials; struct VersionInfo; class VersionInfoFactory; class PluginManager; @@ -162,9 +161,6 @@ namespace DFHack /// Is everything OK? bool isValid(void) { return !errorstate; } - /// get the materials module - Materials * getMaterials(); - command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream& out, const std::string& command); @@ -295,7 +291,6 @@ namespace DFHack // Module storage struct { - Materials * pMaterials; } s_mods; std::vector> allModules; DFHack::PluginManager *plug_mgr; diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 2c3be4e7318..9689c7790cc 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -35,7 +35,8 @@ distribution. #include "df/craft_material_class.h" -namespace df { +namespace df +{ struct item; struct plant_raw; struct creature_raw; @@ -54,28 +55,32 @@ namespace df { namespace DFHack { - struct t_matpair { + struct t_matpair + { int16_t mat_type; int32_t mat_index; t_matpair(int16_t type = -1, int32_t index = -1) - : mat_type(type), mat_index(index) {} + : mat_type(type), mat_index(index) + {} }; - struct DFHACK_EXPORT MaterialInfo { + struct DFHACK_EXPORT MaterialInfo + { static const int NUM_BUILTIN = 19; static const int GROUP_SIZE = 200; static const int CREATURE_BASE = NUM_BUILTIN; static const int FIGURE_BASE = NUM_BUILTIN + GROUP_SIZE; - static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE*2; - static const int END_BASE = NUM_BUILTIN + GROUP_SIZE*3; + static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE * 2; + static const int END_BASE = NUM_BUILTIN + GROUP_SIZE * 3; int16_t type; int32_t index; - df::material *material; + df::material* material; - enum Mode { + enum Mode + { None, Builtin, Inorganic, @@ -85,16 +90,16 @@ namespace DFHack Mode mode; int16_t subtype; - df::inorganic_raw *inorganic; - df::creature_raw *creature; - df::plant_raw *plant; + df::inorganic_raw* inorganic; + df::creature_raw* creature; + df::plant_raw* plant; - df::historical_figure *figure; + df::historical_figure* figure; public: MaterialInfo(int16_t type = -1, int32_t index = -1) { decode(type, index); } - MaterialInfo(const t_matpair &mp) { decode(mp.mat_type, mp.mat_index); } - template MaterialInfo(T *ptr) { decode(ptr); } + MaterialInfo(const t_matpair& mp) { decode(mp.mat_type, mp.mat_index); } + template MaterialInfo(T* ptr) { decode(ptr); } bool isValid() const { return material != NULL; } @@ -108,25 +113,27 @@ namespace DFHack bool isInorganicWildcard() const { return isAnyInorganic() && isBuiltin(); } bool decode(int16_t type, int32_t index = -1); - bool decode(df::item *item); - bool decode(const df::material_vec_ref &vr, int idx); - bool decode(const t_matpair &mp) { return decode(mp.mat_type, mp.mat_index); } + bool decode(df::item* item); + bool decode(const df::material_vec_ref& vr, int idx); + bool decode(const t_matpair& mp) { return decode(mp.mat_type, mp.mat_index); } - template bool decode(T *ptr) { + template bool decode(T* ptr) + { // Assume and exploit a certain naming convention return ptr ? decode(ptr->mat_type, ptr->mat_index) : decode(-1); } - bool find(const std::string &token); - bool find(const std::vector &tokens); + bool find(const std::string& token); + bool find(const std::vector& tokens); - bool findBuiltin(const std::string &token); - bool findInorganic(const std::string &token); - bool findPlant(const std::string &token, const std::string &subtoken); - bool findCreature(const std::string &token, const std::string &subtoken); + bool findBuiltin(const std::string& token); + bool findInorganic(const std::string& token); + bool findPlant(const std::string& token, const std::string& subtoken); + bool findCreature(const std::string& token, const std::string& subtoken); - bool findProduct(df::material *material, const std::string &name); - bool findProduct(const MaterialInfo &info, const std::string &name) { + bool findProduct(df::material* material, const std::string& name); + bool findProduct(const MaterialInfo& info, const std::string& name) + { return findProduct(info.material, name); } @@ -135,35 +142,38 @@ namespace DFHack bool isAnyCloth() const; - void getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const; - void getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const; - void getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const; + void getMatchBits(df::job_item_flags1& ok, df::job_item_flags1& mask) const; + void getMatchBits(df::job_item_flags2& ok, df::job_item_flags2& mask) const; + void getMatchBits(df::job_item_flags3& ok, df::job_item_flags3& mask) const; df::craft_material_class getCraftClass(); - bool matches(const MaterialInfo &mat) const + bool matches(const MaterialInfo& mat) const { if (!mat.isValid()) return true; return (type == mat.type) && - (mat.index == -1 || index == mat.index); + (mat.index == -1 || index == mat.index); } - bool matches(const df::job_material_category &cat) const; - bool matches(const df::dfhack_material_category &cat) const; - bool matches(const df::job_item &jitem, - df::item_type itype = df::item_type::NONE) const; + bool matches(const df::job_material_category& cat) const; + bool matches(const df::dfhack_material_category& cat) const; + bool matches(const df::job_item& jitem, + df::item_type itype = df::item_type::NONE) const; }; - DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category *cat, const std::string &token); - DFHACK_EXPORT bool parseJobMaterialCategory(df::dfhack_material_category *cat, const std::string &token); + DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category* cat, const std::string& token); + DFHACK_EXPORT bool parseJobMaterialCategory(df::dfhack_material_category* cat, const std::string& token); - inline bool operator== (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator== (const MaterialInfo& a, const MaterialInfo& b) + { return a.type == b.type && a.index == b.index; } - inline bool operator!= (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator!= (const MaterialInfo& a, const MaterialInfo& b) + { return a.type != b.type || a.index != b.index; } - inline bool operator< (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator< (const MaterialInfo& a, const MaterialInfo& b) + { return a.type < b.type || (a.type == b.type && a.index < b.index); } @@ -345,38 +355,18 @@ namespace DFHack t_materialIndex mat_index; uint32_t flags; }; - /** - * The Materials module - * \ingroup grp_modules - * \ingroup grp_materials - */ - class DFHACK_EXPORT Materials : public Module + + + namespace Materials { - public: - Materials(); - ~Materials(); - bool Finish(); - - std::vector race; - std::vector raceEx; - std::vector color; - std::vector other; - std::vector alldesc; - - bool CopyInorganicMaterials (std::vector & inorganic); - bool CopyOrganicMaterials (std::vector & organic); - bool CopyWoodMaterials (std::vector & tree); - bool CopyPlantMaterials (std::vector & plant); - - bool ReadCreatureTypes (void); - bool ReadCreatureTypesEx (void); - bool ReadDescriptorColors(void); - bool ReadOthers (void); - - bool ReadAllMaterials(void); - - std::string getType(const t_material & mat); - std::string getDescription(const t_material & mat); - }; + /** + * The Materials module + * \ingroup grp_modules + * \ingroup grp_materials + */ + + std::string getType(const t_material& mat); + std::string getDescription(const t_material& mat); + } } #endif diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index d5358f6139f..3351726d57e 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -68,7 +68,7 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; -bool MaterialInfo::decode(df::item *item) +bool MaterialInfo::decode(df::item* item) { if (!item) return decode(-1); @@ -76,7 +76,7 @@ bool MaterialInfo::decode(df::item *item) return decode(item->getActualMaterial(), item->getActualMaterialIndex()); } -bool MaterialInfo::decode(const df::material_vec_ref &vr, int idx) +bool MaterialInfo::decode(const df::material_vec_ref& vr, int idx) { if (size_t(idx) >= vr.mat_type.size() || size_t(idx) >= vr.mat_index.size()) return decode(-1); @@ -94,14 +94,15 @@ bool MaterialInfo::decode(int16_t type, int32_t index) inorganic = NULL; plant = NULL; creature = NULL; figure = NULL; - if (type < 0) { + if (type < 0) + { mode = None; return false; } - auto &raws = world->raws; + auto& raws = world->raws; - if (size_t(type) >= sizeof(raws.mat_table.builtin)/sizeof(void*)) + if (size_t(type) >= sizeof(raws.mat_table.builtin) / sizeof(void*)) return false; if (index < 0) @@ -123,7 +124,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < FIGURE_BASE) { mode = Creature; - subtype = type-CREATURE_BASE; + subtype = type - CREATURE_BASE; creature = df::creature_raw::find(index); if (!creature || size_t(subtype) >= creature->material.size()) return false; @@ -132,7 +133,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < PLANT_BASE) { mode = Creature; - subtype = type-FIGURE_BASE; + subtype = type - FIGURE_BASE; figure = df::historical_figure::find(index); if (!figure) return false; @@ -144,7 +145,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < END_BASE) { mode = Plant; - subtype = type-PLANT_BASE; + subtype = type - PLANT_BASE; plant = df::plant_raw::find(index); if (!plant || size_t(subtype) >= plant->material.size()) return false; @@ -158,24 +159,24 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return (material != NULL); } -bool MaterialInfo::find(const std::string &token) +bool MaterialInfo::find(const std::string& token) { std::vector items; split_string(&items, token, ":"); return find(items); } -bool MaterialInfo::find(const std::vector &items) +bool MaterialInfo::find(const std::vector& items) { if (items.empty()) return false; if (items[0] == "INORGANIC" && items.size() > 1) - return findInorganic(vector_get(items,1)); + return findInorganic(vector_get(items, 1)); if (items[0] == "CREATURE_MAT" || items[0] == "CREATURE") - return findCreature(vector_get(items,1), vector_get(items,2)); + return findCreature(vector_get(items, 1), vector_get(items, 2)); if (items[0] == "PLANT_MAT" || items[0] == "PLANT") - return findPlant(vector_get(items,1), vector_get(items,2)); + return findPlant(vector_get(items, 1), vector_get(items, 2)); if (items.size() == 1) { @@ -188,7 +189,8 @@ bool MaterialInfo::find(const std::vector &items) } else if (items.size() == 2) { - if (items[0] == "COAL" && findBuiltin(items[0])) { + if (items[0] == "COAL" && findBuiltin(items[0])) + { if (items[1] == "COKE") this->index = 0; else if (items[1] == "CHARCOAL") @@ -206,17 +208,18 @@ bool MaterialInfo::find(const std::vector &items) return false; } -bool MaterialInfo::findBuiltin(const std::string &token) +bool MaterialInfo::findBuiltin(const std::string& token) { if (token.empty()) return decode(-1); - if (token == "NONE") { + if (token == "NONE") + { decode(-1); return true; } - auto &raws = world->raws; + auto& raws = world->raws; for (int i = 0; i < NUM_BUILTIN; i++) { auto obj = raws.mat_table.builtin[i]; @@ -226,34 +229,35 @@ bool MaterialInfo::findBuiltin(const std::string &token) return decode(-1); } -bool MaterialInfo::findInorganic(const std::string &token) +bool MaterialInfo::findInorganic(const std::string& token) { if (token.empty()) return decode(-1); - if (token == "NONE") { + if (token == "NONE") + { decode(0, -1); return true; } - auto &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.inorganics.all.size(); i++) { - df::inorganic_raw *p = raws.inorganics.all[i]; + df::inorganic_raw* p = raws.inorganics.all[i]; if (p->id == token) return decode(0, i); } return decode(-1); } -bool MaterialInfo::findPlant(const std::string &token, const std::string &subtoken) +bool MaterialInfo::findPlant(const std::string& token, const std::string& subtoken) { if (token.empty()) return decode(-1); - auto &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.plants.all.size(); i++) { - df::plant_raw *p = raws.plants.all[i]; + df::plant_raw* p = raws.plants.all[i]; if (p->id != token) continue; @@ -263,39 +267,39 @@ bool MaterialInfo::findPlant(const std::string &token, const std::string &subtok for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(PLANT_BASE+j, i); + return decode(PLANT_BASE + j, i); break; } return decode(-1); } -bool MaterialInfo::findCreature(const std::string &token, const std::string &subtoken) +bool MaterialInfo::findCreature(const std::string& token, const std::string& subtoken) { if (token.empty() || subtoken.empty()) return decode(-1); - auto &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.creatures.all.size(); i++) { - df::creature_raw *p = raws.creatures.all[i]; + df::creature_raw* p = raws.creatures.all[i]; if (p->creature_id != token) continue; for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(CREATURE_BASE+j, i); + return decode(CREATURE_BASE + j, i); break; } return decode(-1); } -bool MaterialInfo::findProduct(df::material *material, const std::string &name) +bool MaterialInfo::findProduct(df::material* material, const std::string& name) { if (!material || name.empty()) return decode(-1); - auto &pids = material->reaction_product.id; + auto& pids = material->reaction_product.id; for (size_t i = 0; i < pids.size(); i++) if ((*pids[i]) == name) return decode(material->reaction_product.material, i); @@ -311,9 +315,11 @@ std::string MaterialInfo::getToken() const if (!material) return fmt::format("INVALID:{}:{}", type, index); - switch (mode) { + switch (mode) + { case Builtin: - if (material->id == "COAL") { + if (material->id == "COAL") + { if (index == 0) return "COAL:COKE"; else if (index == 1) @@ -382,10 +388,10 @@ bool MaterialInfo::isAnyCloth() const material->flags.is_set(THREAD_PLANT) || material->flags.is_set(SILK) || material->flags.is_set(YARN) - ); + ); } -bool MaterialInfo::matches(const df::job_material_category &cat) const +bool MaterialInfo::matches(const df::job_material_category& cat) const { if (!material) return false; @@ -414,7 +420,7 @@ bool MaterialInfo::matches(const df::job_material_category &cat) const return false; } -bool MaterialInfo::matches(const df::dfhack_material_category &cat) const +bool MaterialInfo::matches(const df::dfhack_material_category& cat) const { if (!material) return false; @@ -443,7 +449,7 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat) const #undef TEST -bool MaterialInfo::matches(const df::job_item &jitem, df::item_type itype) const +bool MaterialInfo::matches(const df::job_item& jitem, df::item_type itype) const { if (!isValid()) return false; @@ -460,11 +466,11 @@ bool MaterialInfo::matches(const df::job_item &jitem, df::item_type itype) const mask2.whole &= ~xmask2.whole; return bits_match(jitem.flags1.whole, ok1.whole, mask1.whole) && - bits_match(jitem.flags2.whole, ok2.whole, mask2.whole) && - bits_match(jitem.flags3.whole, ok3.whole, mask3.whole); + bits_match(jitem.flags2.whole, ok2.whole, mask2.whole) && + bits_match(jitem.flags3.whole, ok3.whole, mask3.whole); } -void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags1& ok, df::job_item_flags1& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -486,10 +492,10 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma TEST(processable_to_vial, structural && FLAG(plant, plant_raw_flags::EXTRACT_VIAL)); TEST(processable_to_barrel, structural && FLAG(plant, plant_raw_flags::EXTRACT_BARREL)); TEST(solid, !(MAT_FLAG(ALCOHOL_PLANT) || - MAT_FLAG(ALCOHOL_CREATURE) || - MAT_FLAG(LIQUID_MISC_PLANT) || - MAT_FLAG(LIQUID_MISC_CREATURE) || - MAT_FLAG(LIQUID_MISC_OTHER))); + MAT_FLAG(ALCOHOL_CREATURE) || + MAT_FLAG(LIQUID_MISC_PLANT) || + MAT_FLAG(LIQUID_MISC_CREATURE) || + MAT_FLAG(LIQUID_MISC_OTHER))); TEST(tameable_vermin, false); TEST(sharpenable, MAT_FLAG(IS_STONE)); TEST(milk, linear_index(material->reaction_product.id, std::string("CHEESE_MAT")) >= 0); @@ -497,7 +503,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma //04000000 - "milkable" - vtable[107],1,1 } -void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags2& ok, df::job_item_flags2& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -511,15 +517,15 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(glass_making, MAT_FLAG(CRYSTAL_GLASSABLE)); TEST(fire_safe, material->heat.melting_point > 11000 - && material->heat.boiling_point > 11000 - && material->heat.ignite_point > 11000 - && material->heat.heatdam_point > 11000 - && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 11000)); + && material->heat.boiling_point > 11000 + && material->heat.ignite_point > 11000 + && material->heat.heatdam_point > 11000 + && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 11000)); TEST(magma_safe, material->heat.melting_point > 12000 - && material->heat.boiling_point > 12000 - && material->heat.ignite_point > 12000 - && material->heat.heatdam_point > 12000 - && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 12000)); + && material->heat.boiling_point > 12000 + && material->heat.ignite_point > 12000 + && material->heat.heatdam_point > 12000 + && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 12000)); TEST(deep_material, FLAG(inorganic, inorganic_flags::SPECIAL)); TEST(non_economic, !inorganic || !(plotinfo && vector_get(plotinfo->economic_stone, index))); @@ -537,7 +543,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(yarn, MAT_FLAG(YARN)); } -void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags3& ok, df::job_item_flags3& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -549,7 +555,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &ma #undef FLAG #undef TEST -bool DFHack::parseJobMaterialCategory(df::job_material_category *cat, const std::string &token) +bool DFHack::parseJobMaterialCategory(df::job_material_category* cat, const std::string& token) { cat->whole = 0; @@ -565,7 +571,7 @@ bool DFHack::parseJobMaterialCategory(df::job_material_category *cat, const std: return true; } -bool DFHack::parseJobMaterialCategory(df::dfhack_material_category *cat, const std::string &token) +bool DFHack::parseJobMaterialCategory(df::dfhack_material_category* cat, const std::string& token) { cat->whole = 0; @@ -600,32 +606,14 @@ bool DFHack::isStoneInorganic(int material) return true; } -std::unique_ptr DFHack::createMaterials() -{ - return std::make_unique(); -} - -Materials::Materials() -{ -} - -Materials::~Materials() -{ -} - -bool Materials::Finish() -{ - return true; -} - t_matgloss::t_matgloss() { - fore = 0; - back = 0; - bright = 0; + fore = 0; + back = 0; + bright = 0; - value = 0; - wall_tile = 0; + value = 0; + wall_tile = 0; boulder_tile = 0; } @@ -643,240 +631,7 @@ bool t_matglossInorganic::isGem() return is_gem; } -bool Materials::CopyInorganicMaterials (std::vector & inorganic) -{ - size_t size = world->raws.inorganics.all.size(); - inorganic.clear(); - inorganic.reserve (size); - for (size_t i = 0; i < size;i++) - { - df::inorganic_raw *orig = world->raws.inorganics.all[i]; - t_matglossInorganic mat; - mat.id = orig->id; - mat.name = orig->material.stone_name; - - mat.ore_types = orig->metal_ore.mat_index; - mat.ore_chances = orig->metal_ore.probability; - mat.strand_types = orig->thread_metal.mat_index; - mat.strand_chances = orig->thread_metal.probability; - mat.value = orig->material.material_value; - mat.wall_tile = orig->material.tile; - mat.boulder_tile = orig->material.item_symbol; - mat.fore = orig->material.basic_color[0]; - mat.bright = orig->material.basic_color[1]; - mat.is_gem = orig->material.flags.is_set(material_flags::IS_GEM); - inorganic.push_back(mat); - } - return true; -} - -bool Materials::CopyOrganicMaterials (std::vector & organic) -{ - size_t size = world->raws.plants.all.size(); - organic.clear(); - organic.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.all[i]->id; - organic.push_back(mat); - } - return true; -} - -bool Materials::CopyWoodMaterials (std::vector & tree) -{ - size_t size = world->raws.plants.trees.size(); - tree.clear(); - tree.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.trees[i]->id; - tree.push_back(mat); - } - return true; -} - -bool Materials::CopyPlantMaterials (std::vector & plant) -{ - size_t size = world->raws.plants.bushes.size(); - plant.clear(); - plant.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.bushes[i]->id; - plant.push_back(mat); - } - return true; -} - -bool Materials::ReadCreatureTypes (void) -{ - size_t size = world->raws.creatures.all.size(); - race.clear(); - race.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.creatures.all[i]->creature_id; - race.push_back(mat); - } - return true; -} - -bool Materials::ReadOthers(void) -{ - other.clear(); - FOR_ENUM_ITEMS(builtin_mats, i) - { - t_matglossOther mat; - mat.id = world->raws.mat_table.builtin[i]->id; - other.push_back(mat); - } - return true; -} - -bool Materials::ReadDescriptorColors (void) -{ - size_t size = world->raws.descriptors.colors.size(); - - color.clear(); - if(size == 0) - return false; - color.reserve(size); - for (size_t i = 0; i < size;i++) - { - df::descriptor_color *c = world->raws.descriptors.colors[i]; - t_descriptor_color col; - col.id = c->id; - col.name = c->name; - col.red = c->red; - col.green = c->green; - col.blue = c->blue; - color.push_back(col); - } - - size = world->raws.descriptors.patterns.size(); - alldesc.clear(); - alldesc.reserve(size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.descriptors.patterns[i]->id; - alldesc.push_back(mat); - } - return true; -} - -bool Materials::ReadCreatureTypesEx (void) -{ - size_t size = world->raws.creatures.all.size(); - raceEx.clear(); - raceEx.reserve (size); - for (size_t i = 0; i < size; i++) - { - df::creature_raw *cr = world->raws.creatures.all[i]; - t_creaturetype mat; - mat.id = cr->creature_id; - mat.tile_character = cr->creature_tile; - mat.tilecolor.fore = cr->color[0]; - mat.tilecolor.back = cr->color[1]; - mat.tilecolor.bright = cr->color[2]; - - size_t sizecas = cr->caste.size(); - for (size_t j = 0; j < sizecas;j++) - { - df::caste_raw *ca = cr->caste[j]; - /* caste name */ - t_creaturecaste caste; - caste.id = ca->caste_id; - caste.singular = ca->caste_name[0]; - caste.plural = ca->caste_name[1]; - caste.adjective = ca->caste_name[2]; - - // color mod reading - // Caste + offset > color mod vector - auto & colorings = ca->color_modifiers; - size_t sizecolormod = colorings.size(); - caste.ColorModifier.resize(sizecolormod); - for(size_t k = 0; k < sizecolormod;k++) - { - // color mod [0] -> color list - auto & indexes = colorings[k]->pattern_index; - size_t sizecolorlist = indexes.size(); - caste.ColorModifier[k].colorlist.resize(sizecolorlist); - for(size_t l = 0; l < sizecolorlist; l++) - caste.ColorModifier[k].colorlist[l] = indexes[l]; - // color mod [color_modifier_part_offset] = string part - caste.ColorModifier[k].part = colorings[k]->part; - caste.ColorModifier[k].startdate = colorings[k]->start_date; - caste.ColorModifier[k].enddate = colorings[k]->end_date; - } - - // body parts - caste.bodypart.clear(); - size_t sizebp = ca->body_info.body_parts.size(); - for (size_t k = 0; k < sizebp; k++) - { - df::body_part_raw *bp = ca->body_info.body_parts[k]; - t_bodypart part; - part.id = bp->token; - part.category = bp->category; - caste.bodypart.push_back(part); - } - using namespace df::enums::mental_attribute_type; - using namespace df::enums::physical_attribute_type; - for (int32_t k = 0; k < 7; k++) - { - auto & physical = ca->attributes.phys_att_range; - caste.strength[k] = physical[STRENGTH][k]; - caste.agility[k] = physical[AGILITY][k]; - caste.toughness[k] = physical[TOUGHNESS][k]; - caste.endurance[k] = physical[ENDURANCE][k]; - caste.recuperation[k] = physical[RECUPERATION][k]; - caste.disease_resistance[k] = physical[DISEASE_RESISTANCE][k]; - - auto & mental = ca->attributes.ment_att_range; - caste.analytical_ability[k] = mental[ANALYTICAL_ABILITY][k]; - caste.focus[k] = mental[FOCUS][k]; - caste.willpower[k] = mental[WILLPOWER][k]; - caste.creativity[k] = mental[CREATIVITY][k]; - caste.intuition[k] = mental[INTUITION][k]; - caste.patience[k] = mental[PATIENCE][k]; - caste.memory[k] = mental[MEMORY][k]; - caste.linguistic_ability[k] = mental[LINGUISTIC_ABILITY][k]; - caste.spatial_sense[k] = mental[SPATIAL_SENSE][k]; - caste.musicality[k] = mental[MUSICALITY][k]; - caste.kinesthetic_sense[k] = mental[KINESTHETIC_SENSE][k]; - caste.empathy[k] = mental[EMPATHY][k]; - caste.social_awareness[k] = mental[SOCIAL_AWARENESS][k]; - } - mat.castes.push_back(caste); - } - for (size_t j = 0; j < world->raws.creatures.all[i]->material.size(); j++) - { - t_creatureextract extract; - extract.id = world->raws.creatures.all[i]->material[j]->id; - mat.extract.push_back(extract); - } - raceEx.push_back(mat); - } - return true; -} - -bool Materials::ReadAllMaterials(void) -{ - bool ok = true; - ok &= this->ReadCreatureTypes(); - ok &= this->ReadCreatureTypesEx(); - ok &= this->ReadDescriptorColors(); - ok &= this->ReadOthers(); - return ok; -} - -std::string Materials::getDescription(const t_material & mat) +std::string Materials::getDescription(const t_material& mat) { MaterialInfo mi(mat.mat_type, mat.mat_index); if (mi.creature) @@ -889,7 +644,7 @@ std::string Materials::getDescription(const t_material & mat) // type of material only so we know which vector to retrieve // This is completely worthless now -std::string Materials::getType(const t_material & mat) +std::string Materials::getType(const t_material& mat) { MaterialInfo mi(mat.mat_type, mat.mat_index); switch (mi.mode) From a527e8cdcbd954ff1c66cfd58c9f7f957eefe1a4 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 03:26:59 -0500 Subject: [PATCH 916/919] sunset the old Modules implementation not relevant since we move to in-process --- library/CMakeLists.txt | 2 - library/Core.cpp | 21 ---------- library/include/Core.h | 5 --- library/include/Module.h | 59 ----------------------------- library/include/ModuleFactory.h | 38 ------------------- library/include/modules/Materials.h | 1 - library/modules/Buildings.cpp | 1 - library/modules/Gui.cpp | 1 - library/modules/Items.cpp | 1 - library/modules/Kitchen.cpp | 1 - library/modules/MapCache.cpp | 1 - library/modules/Maps.cpp | 1 - library/modules/Materials.cpp | 1 - library/modules/Random.cpp | 1 - library/modules/Screen.cpp | 1 - library/modules/Units.cpp | 1 - 16 files changed, 136 deletions(-) delete mode 100644 library/include/Module.h delete mode 100644 library/include/ModuleFactory.h diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 6da49099681..d87e20531e0 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -66,10 +66,8 @@ set(MAIN_HEADERS include/MemAccess.h include/Memory.h include/MiscUtils.h - include/Module.h include/MemAccess.h include/MemoryPatcher.h - include/ModuleFactory.h include/PluginLua.h include/PluginManager.h include/PluginStatics.h diff --git a/library/Core.cpp b/library/Core.cpp index 50c6fe5f8bd..ec105a6c6bd 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -40,8 +40,6 @@ distribution. #include "MemoryPatcher.h" #include "MiscUtils.h" #include "MiscUtils.h" -#include "Module.h" -#include "ModuleFactory.h" #include "PluginManager.h" #include "RemoteServer.h" #include "RemoteTools.h" @@ -1047,7 +1045,6 @@ Core::Core() : plug_mgr = nullptr; errorstate = false; vinfo = 0; - memset(&(s_mods), 0, sizeof(s_mods)); // set up hotkey capture suppress_duplicate_keyboard_events = true; @@ -1949,11 +1946,9 @@ int Core::Shutdown ( void ) plug_mgr = nullptr; } // invalidate all modules - allModules.clear(); Textures::cleanup(); DFSDL::cleanup(); DFSteam::cleanup(getConsole()); - memset(&(s_mods), 0, sizeof(s_mods)); d.reset(); return -1; } @@ -2184,19 +2179,3 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return join_strings(" ", aliases[name]); } -/******************************************************************************* - M O D U L E S -*******************************************************************************/ - -#define MODULE_GETTER(TYPE) \ -TYPE * Core::get##TYPE() \ -{ \ - if(errorstate) return nullptr;\ - if(!s_mods.p##TYPE)\ - {\ - std::unique_ptr mod = create##TYPE();\ - s_mods.p##TYPE = (TYPE *) mod.get();\ - allModules.push_back(std::move(mod));\ - }\ - return s_mods.p##TYPE;\ -} diff --git a/library/include/Core.h b/library/include/Core.h index 085dd33cf1a..8b78e580970 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -288,11 +288,6 @@ namespace DFHack // FIXME: shouldn't be kept around like this std::unique_ptr vif; - // Module storage - struct - { - } s_mods; - std::vector> allModules; DFHack::PluginManager *plug_mgr; // Hotkey Manager diff --git a/library/include/Module.h b/library/include/Module.h deleted file mode 100644 index 7770e13970b..00000000000 --- a/library/include/Module.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once - -#ifndef MODULE_H_INCLUDED -#define MODULE_H_INCLUDED - -#include "Export.h" -namespace DFHack -{ - /** - * The parent class for all DFHack modules - * \ingroup grp_modules - */ - class DFHACK_EXPORT Module - { - public: - virtual ~Module(){}; - virtual bool Start(){return true;};// default start... - virtual bool Finish() = 0;// everything should have a Finish() - /* - // should Context call Finish when Resume is called? - virtual bool OnResume() - { - Finish(); - return true; - }; - // Finish when map change is detected? - // TODO: implement - virtual bool OnMapChange() - { - return false; - }; - */ - }; -} -#endif //MODULE_H_INCLUDED diff --git a/library/include/ModuleFactory.h b/library/include/ModuleFactory.h deleted file mode 100644 index c99e7b32898..00000000000 --- a/library/include/ModuleFactory.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once - -#ifndef MODULE_FACTORY_H_INCLUDED -#define MODULE_FACTORY_H_INCLUDED - -#include - -namespace DFHack -{ - class Module; - std::unique_ptr createMaterials(); - std::unique_ptr createGraphic(); -} -#endif diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 9689c7790cc..1f87f548ffc 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -30,7 +30,6 @@ distribution. * @ingroup grp_modules */ #include "Export.h" -#include "Module.h" #include "DataDefs.h" #include "df/craft_material_class.h" diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index e5a0af116b3..ddedbbc1b66 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -29,7 +29,6 @@ distribution. #include "MemAccess.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "TileTypes.h" #include "MiscUtils.h" diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 06de785a390..e7c8233ba17 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -30,7 +30,6 @@ distribution. #include "VersionInfo.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "Debug.h" #include "PluginManager.h" diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index adce8ba8a6d..6b5b3ee1d28 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -28,7 +28,6 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "Types.h" #include "VersionInfo.h" diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index be1415d90d5..c9d4c7d5c67 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -14,7 +14,6 @@ using namespace std; #include "Types.h" #include "Error.h" #include "modules/Kitchen.h" -#include "ModuleFactory.h" #include "Core.h" using namespace DFHack; diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index c01f5bf5769..603d1d0da29 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -30,7 +30,6 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "VersionInfo.h" #include "modules/Buildings.h" diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 11499a42701..2376936054b 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -31,7 +31,6 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "VersionInfo.h" #include "modules/Buildings.h" diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 3351726d57e..dba8a781324 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -28,7 +28,6 @@ distribution. #include "VersionInfo.h" #include "MemAccess.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "MiscUtils.h" diff --git a/library/modules/Random.cpp b/library/modules/Random.cpp index f0d2054c9e3..d6d682d9216 100644 --- a/library/modules/Random.cpp +++ b/library/modules/Random.cpp @@ -35,7 +35,6 @@ using namespace std; #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" -#include "ModuleFactory.h" #include "Core.h" #include "Error.h" #include "VTableInterpose.h" diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index d629c4b8cf4..b16a3e9b0e3 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -28,7 +28,6 @@ distribution. #include "VersionInfo.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "PluginManager.h" #include "LuaTools.h" diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 707437ea347..00821c11c0c 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -27,7 +27,6 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "Types.h" #include "VersionInfo.h" From 2c164932af0c2a401e10dec54086db6c13b1a716 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 03:44:51 -0500 Subject: [PATCH 917/919] missed one --- library/modules/Translation.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Translation.cpp b/library/modules/Translation.cpp index 38ae2c51ba3..097de85cc9f 100644 --- a/library/modules/Translation.cpp +++ b/library/modules/Translation.cpp @@ -27,7 +27,6 @@ distribution. #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" -#include "ModuleFactory.h" #include "Core.h" #include "Error.h" #include "DataDefs.h" From 5a8c9f3559122b0719c3d3ecc4ae9b9508a677a8 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 24 May 2026 03:45:45 -0500 Subject: [PATCH 918/919] Update Core.cpp --- library/Core.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/Core.cpp b/library/Core.cpp index ec105a6c6bd..00419bd7aef 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2178,4 +2178,3 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return aliases[name][0]; return join_strings(" ", aliases[name]); } - From 9d86dc2f5a8959fb298cc0f6c80187c6d92596a3 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:08:51 +0000 Subject: [PATCH 919/919] Auto-update submodules plugins/stonesense: master --- plugins/stonesense | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stonesense b/plugins/stonesense index 992957670fd..8791e2c2669 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 992957670fd99b55d10196cf2850a4554bb15374 +Subproject commit 8791e2c26693cea552c42700e38c87503b7ac7da