From b8f53c55a534cd4c488495d4499fddc3b61d25ed Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 3 Apr 2026 17:04:22 -0600 Subject: [PATCH 01/18] Copy more imgui stuff from Metaforce --- CMakeLists.txt | 7 + extern/aurora | 2 +- files.cmake | 2 + res/NotoMono-Regular.ttf | Bin 0 -> 107848 bytes src/dusk/imgui/ImGuiCameraOverlay.cpp | 3 +- src/dusk/imgui/ImGuiConsole.cpp | 57 +++++- src/dusk/imgui/ImGuiConsole.hpp | 61 ++++--- src/dusk/imgui/ImGuiControllerOverlay.cpp | 4 +- src/dusk/imgui/ImGuiEngine.cpp | 201 ++++++++++++++++++++++ src/dusk/imgui/ImGuiEngine.hpp | 25 +++ src/dusk/imgui/ImGuiMapLoader.cpp | 3 +- src/dusk/imgui/ImGuiMenuGame.cpp | 35 ++-- src/dusk/imgui/ImGuiMenuTools.cpp | 3 + src/dusk/imgui/ImGuiMenuTools.hpp | 2 +- src/m_Do/m_Do_main.cpp | 6 + 15 files changed, 362 insertions(+), 49 deletions(-) create mode 100644 res/NotoMono-Regular.ttf create mode 100644 src/dusk/imgui/ImGuiEngine.cpp create mode 100644 src/dusk/imgui/ImGuiEngine.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e0cf678839..248dfcf6b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,13 @@ target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) target_include_directories(dusk PRIVATE include) target_link_libraries(dusk PRIVATE game aurora::main) +add_custom_command(TARGET dusk POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/res" + "$/res" + COMMENT "Copying resources" +) + include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake) aurora_copy_runtime_dlls(dusk game) diff --git a/extern/aurora b/extern/aurora index 33ec54347d..37631c3bf3 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 33ec54347df497c3f8c237e5c2b65590fb1807df +Subproject commit 37631c3bf382c1564d3f29c4c3f2dc6cf7617fa9 diff --git a/files.cmake b/files.cmake index 26cae0f1a9..e5b470dde2 100644 --- a/files.cmake +++ b/files.cmake @@ -1344,6 +1344,8 @@ set(DUSK_FILES #src/dusk/m_Do_ext_dusk.cpp src/dusk/imgui/ImGuiConsole.hpp src/dusk/imgui/ImGuiConsole.cpp + src/dusk/imgui/ImGuiEngine.cpp + src/dusk/imgui/ImGuiEngine.hpp src/dusk/imgui/ImGuiMenuGame.cpp src/dusk/imgui/ImGuiMenuGame.hpp src/dusk/imgui/ImGuiMenuTools.cpp diff --git a/res/NotoMono-Regular.ttf b/res/NotoMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3560a3a0c87b328a63c3c5dcd2020f3fc32dbbe9 GIT binary patch literal 107848 zcmb@udtgl0+6TPW+I#j~GxwR?$joFUkp_v3Ac{;~k_e5sHbNwbh+YIym*RvvN?eM% zwv<#IRg|GbTv}AM^?F-c)gGruC8v7PQt5>%nVs*q_9Uw3J@5N{e|$5uXZBuu@3o%w ztmpnbdk7_j7|A>$km7+ul2c!vzw0z1v<$7q{LHfP zV@iIT`1jXv?mD*4WoXbButgZJ4UXHCJvD76J#k|;jz2(unkUQ0jfwwgaXlg7=tC_& zHD=~y)%R=$&S&F#_oOjTji1+g&)*5%GKUb==E>z1)0!rd2^hZ>KR2!Euz<;fsm8$UmMDQKUu#mO#UEvQRUTC;805#Z^aAusGpP# zgoq!?A2jV$d6k|*=R)5CZxQB`mBdO06BX_>lVnnWTd&e_z%3<$_#s`1Geq^0%7^xt z(64Zwl+vF8onF!k8j&%vj91}~nV%DLR5kc< zbI8a^rR78ZO~0kBNqfT2^`H$jfhLlLWCN|Gt?4;(fvhI=WHVVndXU}ZC9;BSA}`^N zm&gX?y@R-jhb$uVv0Wm+lBe-oPpWX=H8KVJLrDPJ5we_=lDW8&ku-dl(oN(7^^o$W z2D%kjZARZ!7of^_qv&vqa)9&#+`DNxjl_8;@a!Nv0Lve5f>~T@e%E6b z{7sZO48G6%|%cDX=7M9k22p9mn9pqq(IZqqARMGyV9>G`JdO|Lf1X|gpvLw-{3yG{B4Go|QZC}w>D zZJg%HakL1lvjAs2BpMW#PI7SGLuQdPq?+yqjI$K%FT(Lw9IZy%=b-Rvg~}@+<2Xgm zCy%2gha>^E0xDpiuU-L03@5j7b~*awv`$Dbv|PYQp|xCuvl6Z%z)Kb07qBiNUf@v8 z2UO7tRGe~VVZ>efX5u%!#Qv+c?$4N z!DzK)Hm+DmMBKd_D-@zWf;oujVKDB!jWc{5S~k8OwM}>NzE0o9`wV>>lyMDQ+(5pj zvq>)cYJe12MOTp~vXk}z>@#prG0}kcA^~3xj;usaB1kJhl>`3G0Zic}6EZs_A2@^^ z@&1-9qCe8F@Q$N-;K7c#`+s|N0mO>{iw^U)Vs4{=jYaYYpojLrbT5T2L1T0)xLO8B zH)8f&dW7VIo!EiNYINQMzt9ipXB2*)24>T-lANkD05{(hCO^}9 zT2B)3t|I#(n|ok=HbW9}9{F4Wdnwk2^Aq15#ycFV(85KJlS*Y2-dic~5~BOTpu=KZ zXUBCLnztQIcbYEp4Rpj`T<(dObxTkD$|-=C({6~iIHz!VFtmB&cX3!YlbwLITG?9o zmjCtvT|{H1EgO2INuYyb^vmbNVd5L7Q@&1K%p(C<;jdW=)}{dBM6w-YaL8KV2>lNG zx8px#S<7|*F1tpM0Sf#r?Je@^L6~zuMGqeTr~UsVV_Mo;@Xdd#$cK<`$H-;sg-qdZ zFMXfxr~9!oOEE|2T5PaCh{%rMcN?r5iEs-ejQoX;q2p*JeSyA01K@-v7R`>a_t_bC znf)SAK@hZpN$?6wgk{1j!bidttn(>qN8ti)b2x~U&5l-2S=`LFVH$n@f- zqNV~;()391eDGZGqu_^fr94L-AKWO92)-nT2VW#a*2?)ZeWCvHnK=_x1Jlb@gA=|D*mw{fqU}>!%S?A5pKb*Vf5(_v(JF`=BnaF19YJ zF0#&AXQ~sfe0$~ml@I?p5mwj$mZz-0g3N#Wdj8*z|L?=H|NHcRILqzc|Myo^=n~qG z=Fm1Yj+N2d^fzHDYsEUTG*-Z1`}zBh$R9Rz$lyVF19Kl8&_AbNc2;KJKD~QA;?L;W zqx-|%(z|x)oYpZVxkLMQZ4=_-+IV9bv zpJvuws7ZM9Ue+Fn(rU%bEMP7kjaq{fFc) z35aq1O1yo6ZdCwKIlKGKPd=!52fvp47}zVDL!lG1>yIJyL!R_e}= z_5}PRu)rg{`Qt}$LIZ{p>o6Q%m{x%G9-7~Oh`0Zsf_$v1XM}QwpWxkzaee=-8*hGe zs2h-6Koh5N=d);G1THkA!JUa6Z?Eq74yfZa*vvq<(#lD-S9fcm(&)&Q( zgv$s{^X`1_c<%^rnLFUm%jYQK=uxoTj0^?mp;hTWH2=Y)2ap9c5f4r)m}^F6AT#Mc z4molIS;}z>u0b<@KHab7w0nuh+keOsz7pQ%4hbOd7a*L3{ar1XHh7)$vo~{$*KGzz zEBw4z5^ukfCEg+V-Ic2$LFPt3!^gIg{RTIV&m+ z%JH~bN|d(HMTh-Ro#Sd*sLiZghc<>bG1TT)+D71oRf;GBK|-+ib(iq5`5(k9>3F_7Tyv-?cw^zu@0*vSq%u zkM123>Du!~q-%Fhq-)pCNZ0GQb@R)SF4xO+@}_y4R&F}KscuulCdu?7U5{8w(;a`; z4O&yCd;PriE7v!y7hhj@cpdAyjy9~N$JbK#+LX2awe!}lTwA?XTJu7rYc;wzy+9Xc zMY>McQuRK%{RmZ)8mivUgn4{-B~|-(u<1`nx~Antx+<`hXGgjw;rb{VF)Y&7FmXyvHK$S|c(C-YvRYepmQ(!N~l5ls-RV;qv9ASL^nM*uxQ`GQ0(CNl64h6}S}P z2DDLrR=G&IBy^rCec)W^;-t{M{EnmuLL@tU;X#P9g_EaMH7X^#2C{5^i8=73R>1P* z@yrLlWxH%|x#s_mUky5D!^Tw!k0%`cI=AL8Rb_(y|R_0pI`f z3XsEOiE?fuyulTS1PuF+S57385ZOFItH>Ade?zZQ#AIfYb>sxOh-mDG)WO!nb5ABW zXfBxrE*?N{v4sEGThqUQ%XMTKAYKPg?k*uTf*OR@_r(wovX^#t|1>RXylnrAe7G{0#3Yrofx(Y>u}(s$99 z>-Xv#4L-vp!=|uSVZFlMF-94O8CM&BGz~OuGX1wnHjlStS>CmFww7BrS&vx1wK;9= zZ3AuFY(LrC*`KkWaabH%9G9Ju&O+y5=eOaHhCdg6BElR|6tOnqUy&Un%Oc;3dN%5I z^kdPhqHnZPwJL747VqP&O|8eaJ|FW~%xstF>gIaK?R1ZJ|JA+Blj!N`dEIk4wr%XX z*xJ~;-X7i}?ebQ0Iq+ zKJ?^6Hy&zAawerFWhWIU%}83Fv@Pkac4_VUw;S7TX1f*b{?_j8_S*K9?bo&6-Ts~S z|7hRP;gJp_I!x)XsKb^H2RnS!;YPBU9G%=fd2sT?Xg@0PNaO6@_mZj(a|xfW3P@QI!^1jwBy!}wH-g|c(vobRBLKUYE9~gsaI3)`K-P~ zUxsh6Z@h1gZ-?)jPW?NL?KG>?%1*DPiD|9Ud}%ppW73{ZTbi~d?NDcH=Q*8McYeL| ziO!#QzS%|9CAZ6tE)NmD*sjlXJ)hn$eN6g{^yky}q`#MbIsMmex^A9s zUApCTE9y4A+p=!2c01hde7A2NPI-9s!#f{7@$lat{;|8%y;b+L?z!D3bg%6GLib(W z-|2p-dqWSYhpR{D9s_!e>oKdx${yQ$eBLv;XIalpJ>Tqkre|Hx+Zl!oZ^mO8OEV5< z-0^qtzvMsvi1v|wk4$>x|UA^Ayb+p%ay~}&g=v~=+ zQSWbi-|YQcpYlF4`c(FLyU%-le(3W{-`u{#`;PAWQ{Ue+i!#q-{wwodmNlzwR?n=V zSx;rXpPiIFKl@+(()$JaeV6k{&XYOQaxV0@_kW`QodIOP=mD=iDn6R^=!!=_$bBgH z!a(1^qXRDtyq?GMBJw)qW##S7dvB0!Q1l@0pzeeE3_3PgGq`Z@w82XUzdQK9hiHdb zhC~jTH)Qcp$Ixj*PY=C3tjnGOznk*Nl$WRcFty9n=~F+ga8=}1JX^7~;#9@e zY01<6I&J^c@&EgK`ZLpio1vXynXzYP+nM`jelYXPnYU*dXZdC=nf2*2dC#1k-D&p9 z*;nUe&&ivUKc{%kt8-4x`C`s5b9Hk)a}WL{{V&V^@>S*F%H#8T%=={i;%8%@UA92I zVDVog|9WfTrG@niuRYi2xs}g-zo_4$af{|Gb}xRvYDm>5OO7sGzHHEP>++K2d!J8v zzWDi7&!2q$mlYdV_F4J%sz+CCS@qis6JEHpI%##`>H}*GYo@IEXifc^+iUaIu3Iaw z8@ul0`nKz*uRpm#wPC=9WgAYt*y_dkFSUEA{-s+RH5>bEoWAid8!v7ex#{f7885%{ z^1aPnHveVwnJw-ubGIDW@>_L9b;B#qY!$ZZx5jSmx^>G|`PEUczV_<9Z3DKg+4kAr zV*ggL-M0PU_P4fQ-2TIBs@Gb-7WZ2F*ZRIT^0i|-TJPw*BX>vHj`=$_?l|ze{`K*% z&w2fY*LS^s>h*uVe(Uw%PSwtson3Yg+WGj-`8(I|+_&@0&VTK^yUVsKY1gP-i+8=U z>+o*7jgRdVv zdGL#aztlKuI@b)S8Cx@}W<|~RnqxH=Yi=A;A95c`KQ!pj6NeTY+IHyOLth;Fsa9PZ zQ=3-Xzjkcx%-U78uh*Wc{b%j1!(oTx5BEGg_VBZZUp&0~@VkdE9scQv=7{@9+K~ZA z#vPe;WW|x~M~)r&^vJcN^l13eqOfVLr(nV#L5$|!W}q?#FP%2TtK!r!S7HN!zp1}kqDE8 zc?eI6f*PkXQj>i&xzK0v;j3ec&13O&!FE#U*LbLm%?d73)!fY~6Tcx0nV_%O*GhkS z(#hWnAzW!4)9DOh$vl+D93quODsI&5VCXiZzOc}eo?K|L()46seQJ7o#}w)nd_tBmUA7EiVMbdE+l4?g%pLr##B*v|t)>IjFumPEim+0s z2HXTEmhA|;VxglebcjA^YFrSM%5E{D$UXL*d7b#^I&u?WeoGfU^A8lqTb z)Rrg~RcB~0FoQ-k>nHq_>&qLEzZXoK$7U=f}^C7vd+`qx}|pwm&*Annfq7RPIT{u+mU%IByUQ;V~7a zc&|31wObM#R#TC=++1lE%x04**_2@_GKr=(nhHU}Fhqdo_@>U7Es*3SpD)?M@duIx zwtPM-M-``qdS9~n%fh78dQN*jALuSMwPOnCqvaLn@TSEkq&a<-`1rI=ox7y@9L{ic ze1gU4beO3{a@d{Wmd+s0-Bnks-hKV##@cOFN7wdR_U!ww$fn(cuGW@+b2)SOD7xSO z>XHS!?FRzdzN>obg29;lp`YBWT$pNew}uwT_}s+5WfRhKj7h~JD}OqzRbA6xWgzI4`>fgu&%ehW)-Xl zgtj#?huYLcM21U_2ty4yWUi6aB%}V<`V8>yuk|#!zVO%jLi49d^@R$}CKV5ay_>H)b3qSy}4@s)|VGAUAkPT3MOo3m%Puu z@<)^Tt?F$xN2<3To$*b5{WmQ6E-EMPc71CLZ+zwGbl!;60qN0nT|5gfFN(y0tH1V} zED7;i5fm9sAc53vYMUDTDMm}SzYXowhPKJF#h7t4For%HL#3FkNVhG;#!f_@k7Tw; zTcmLoQc77O$5tz?HCq(b>g2?1qhDvvHfBmli4Ui&3#HLKme56S%vJ``c=DY2TACkE zyC&o(u*3w~)|ng>~n?Hf=1ppYt{F!%a^ww z`RwR|Q>7=it=YY6?b$~L&dA9bHDb{8poXrRy?^3zxOcQc*v>91k~zX3MyiE6p#e$+ zr2-K4H@FxMNnm#QzTo9|CCJBwZMaLzHISrIp-C?^xHOfT6&gV!{Z8%xH=J=f5ctoP>ZF`Oj~|!&z~NgQychgUUPi~TC$ZCI%sUNre+_we*?)q*B_E>|OvfZTLtY>JK6n*9lZ9-Cn2nf%k+ktU zeue5$Y1N`m(9b>+2tAKCtJx!ifO6*QZdbJb+R0eI>y=niuH+GAt79TvsT(kP6+1(CTPhU zlJF)Jh9cGQjbS`#9!$mK@L0SoTAoW6tfdR&xog>?b#$RTd!1a#u~I8v5v)`~E;Yj+ z87U=7*QDPiQQAOXoGi{0&x>_pgQ!x7-J~^eV%G?pT~abUV3J&TLs8aAg+b2YG+YwA z)-D1wr&nzl;H6STo%}KF%`t@9gpY*)e1Czs{nmMGCA0ujxk(DtEICZyf*8S)a;&49 zG>?O}W~=GPJ9gmCb4~S3Mm>R=wDz0W`V=XEp(Tx$Y!Y{XjuZe2Z{W5TGof_DhV``l zcD#VC3t;~{3JtdM8w8YT!Jq+~QK;4o3k3yXOpYoDAmL7M`F>Wxtry$?z*a+Qq(f?G zQGe9gZw)Iyuc4Z7TR9QTf?E*OT)+4Yke7nFLZ#MKpwI$Li-{zSF06-A0RgFEnZ@i$ zwOY(^{EK<{v6Yp4eVu;%-3>Wu^^zs4S1np(N5#Y>)*#Q7R|icSX(a7IAEuG=HTjhM zj{F^8^?Cqw8(`^3lHaMN8VzDM)QsROr3SsI)=H@C$jJCGT|ozi=jyIwipOi|)J0OO z6R3~f4rmevuO34y*9ohhxtWwXYc^tG=y@}&oc5}-&^E*TtyhLuhX=xi@NlWT3F66b zvSnK$FFL<=-f{}i9^d)HK?6<$L7zjFRWo*f z`sR$gcjb#34&?%@g$q!|k|KXMy)D|th}lu@b_+#jYBp=a%C`u6!3Axi&uOk`ZfXRA zr6T5vGEBmvkBX>ht=3m)B`lB+D+DmX-W=3IA73ZM>hvYoTOl?JsYA5bMUSHL-B>&r zO{pPqc^`aMLnq4D<$FJ`s(qi1l@}~rSRGhAb^rEJM}Pn5+ie1qlI06$uMON;Og)sX z-!Qjy#;m#P-uv4U1n(FsRESur;x#i?GTa}bHfv4UdbKbKb%}nnEgKdsV>Y{$tE33M zDch>zdPPOWX46FkJ(B8tm^F<_8F2 zVMJBF#lF|h>`_LONw3xE2m*)ID76uZR$T!M3l%gx(l|{IAyN+%1EKYbkH9S|7y!UA z08Hwb(gk)DM{2A(!3Mb`fF4ALUG&E8Uio*LC+`^Xd_fnsKX^UiiPBG~AYZ>%_wepw z+Kt9XrL(|xxzPUg``@FSf{~X1r4RJrKwZl~zmsw}DYZH$9XC*eS87#GtQ0sr;dk02 zvaM3&)0JAP)w%$uUG1tcDV*x#Kou$&#P=USQU`P+6MP|J1Fhekt~18EU8LjS<+(?7B*Z;kOU|TrlAqEEfuIrL)dv%#~PT(RBRf~ z@ulYw7n(mxRV-F;SYe@ui+Ts8xy%=8`;s*=l>Zou#`*xF))0xx|ND z)TJX>v&agQ+FPLt2|bt`uuYPxEp zY4<3cmcSZmTI0FD|4gHPxVFFcs$BZrUnV_1cifXQwJ%=#m-T{FeeK{s&mEz3@e5a8 zdF$=1-*lO12vrUmSil*+oe*P-e$UEha8@CI;tlISIg!NzU0puaS zSuBOlGm-ZGXdPQ;Op%JA`Rb$wSSpg)qSk7o9q7Bk6Mt^3n(CbRx!moeFx0gs@0 z&=%EScq29WhNApi2IfXU=Pv9ijQP3IH@B*1eDlzY!%xfKH2uaoU<0`f8|Dl+V2r=V z1Zjq}ND|7#nW%0OrmLP)F%_Z$phl`@RHYK6Izq1y>L)Y<_wtYE7ETDv1T!2qNFjVX z#YD#1z_a689n1lV2-YBlg>f{Ef;B{6<9K0XFpZtPOiz{4Q`JEGPEI8o=n*8(u7Ow7 z#P3f~^LPivOQU*YvPcJ_HmqC}P+uvE+C6UyGvwU1d!Qq>a2u1P{&~M{xh;4(g zkq;*fUAtB;U56CJgL5wOcQ0cz*!7(j%n3N^om6N63vh( z3Z*59{l_UolQmBn<6D|%Y@^DQH?aQ-cII%1L zi(^i0Te$tn$DS;(ZflP45LZ)J|nu)t{05@7-K7merlw} zMjC0PqLE5Q$=F6~qqa{T>Lx|f$WPi>I$spp3mq?NGvH>e5Y7o#1XYUGuRX8jg@M{u z&d(BrR-eUq2*OSoPzp-C1)_@w3@E%>^JhFdptNGuE$L`4Ohx>F8>`JC0JcGc=kAlf^alA_%K&_*bc9-YL`{WPgUAYSf z)6H}$T~5!*GvznFm9McH$I;b|v@2HI19sW1ItJ=Ck{x~>GYT3?4uPb&dpZ_wZk9yj zCD`sDGBm27bkGQkBT+>oR8)%wy(s=Tz>w@n36tkI+t1p>mwW9a?-~H1fC{ z^??)O6C^lF*H_Ap7v82{ouXgMvlk|W+oM*})AGZrV|V+o$LWqarQ?Rm2-gYVCL&G& ztr%g?#=wF}wT-l^+o+jZ6pgJ$>fENt0%D}ZXf`$g5k_@H@fH&;Gf|U?L>3#Z;l=2K z8}ZyER7}Ltg;Tg~z)gj8ZqeI3ZXUT%b1PBuB7hbW_#TVJdV*Di6%NMYy9Cx#UI}Nl>JAYKz zNR>rbpvYS@>dXZV(H2udSIn1GadS9ofSHad@G(PFE?5-nZV}{74?0GEL4Iv&F|*4y zc;nB>Z^-NAd8+VxRjfDb#JjzO@w^yM54jts(Lm(;t5F8%5=ZMfRIEBLi2!f56}D1+7Yab&}T1!;4K1sskE zaWAE6>*`mI9Ff1haVcSY&n>HkTa96VN8L1!GAJ6#K+YVBH*_{+8U$FC4gM;u=N%or zq`RhLAL!^|9o?a$RXVEE7|hwT5DnDQNG;WA!H+OFILm8bZ+{1K+^lzIXW`dCuo6U7 zteUKvrxI0=uKM$UmQ`X@Sbu&}3?Ix)wrgrM?`eb?nnfBGuj#BAt|`@^QdnaURSzsz z$VjKjT{-rWyK>(LYK@yoFx-=PaFLtv%|9M5RVypm1;R_*b3yL8?8uQ>^z_HU-`J6< z!CI_m+z$8~3joPA%-;pvsz?XFOU1cZr4}GM@&Mjsa$Z3&o*xv1qLA}rlFF?sfpJJV1GCO8&HITxB>cT+pNee#=CW44Pnd# zqsPHicZp<{{8An`UQgA6l^I9CZIl#hPc2OCfNTtRYZYe^-`w^318aPbZyxHkIymNR zc;goiIE4`WUSnPV+a2j{U7u?*M915v6TZxj{5dE}=jZ{0VBV8;Rqne9EQCEbO4CxClzra!| z9sp4EMM5YhhGSM1yw04E`4;(l(;Kh6`nrs0!!PVdc@~t$dU@unG?uob$+UIwKyW`F z6^^tbv9!cOy82^{Be|Srl1&HUh3H~66BsGp0#4IWOL23&j|);ob13TvNGMhZS{u=x z1kU7$_0Y$U&^3(7Kgf5E?|g5Uyypx{555oeHS~+yn^(R_!<8A!W0e`hWFW&1BV~i+ z0cc7^w6>APCxtn9d_xc-UCz@^nbujP7tKX#Fa|sgprkn?r35~q$Ppe! z)C?d(yeGve877GMW=yH>BT5)O?44QHN406&g0J9>5V{f$EO>s`*HR18fZk@t)0(x~ObyOb^GL~Wb7o6U14k?m z+*TtNf%san!9eZ6td47Ccm=>MjHYJhNaFM0A_i`OLRsL8W@MXH)Jqj~Bc9@=Pamb@ znMS@smwzE|{d(K3t=0Rtuu(xI0OT=p{X}TcQJep|aycFZrerxNEd%<~g%x+1&8mVx z^a&&gJ&poB!NbZ_?@$?q;uxE)$jDt4#gYd=a0fwn+*xamKOy9hz;k(RV6($oa`Oxx zl6esD?&TNG($UL5KYx(Yy04G=PrfF9vEakof0KV`l7H*Ce|SUL$ z`~)6he$gK>0d<~G#u|`yqCg`Asuk!Vs3Yz(JdDP8M#JH;GCWaEzr%&!;+#e!X5;BR zcHc=rXgW*vn2N=zkX~_euO5=Q!Ee+fB8N~D6g@P&LOpO}3zI_5L9=fVat(Y+bgM-+ zfrfk_9v^jvckUv5{k|N2RE~U?CL|`idWNF*jbkd$tvbaq22qNPUk!<*_tLnqTUgz2&YKcce*<|c1Cwe$BydgV%-iMo2Z+mVGv0Axxqf`{YA4`WD#!=3bF1|vT20eHo0S;jsglquphqPPs4?o2l@Wa{q zffx1JuqHMX6?j7q8_G6pPOAfPAoIReJ@hOibLY{wceIUidp5m&c4vNgY(xj4A^6)D z`DwuR`nx%-&ytmIln392Q86(fKbf%p6|7+z)({#J66 zNK@1T*LyK42~#5oi+HPEgS?6kr34g-S@F4D9b(5Nn z;OWSaamfS8Y&N5L;8sS&g7Yf6RYg~$e*AeP7{>i%r;>y}b|65KXjLN?1(Jbu zhsBWmfW-PK{T&_xOHDGr$G<~1WHU)5@rgpW!h|THbI-_;;M2400{g0QLGXQ+EWq9P zLuP6be{37Cp=wRIhC$e9gb0WQwVGdpzj9!r2BSyD~N0StKoJinz-ZnC0cV09ti zp#;G_Vo4(tA{!fpi;dq3?N^DF+gIJ2kKX7e8DCKMaStmJiE?Q z%~W-Z-H~q5-&<@JCC|+3oZxdQz8%8rC=Zgi(*pUJba(CDj)3z|Sw@CVfIN{|c`V_6 zmXUYCsbC?i*dJWpl4YdS1E@MGP<1*)f0``KgBin}69-mM!oNpw=Y$b@OZuJNQr&3* zi%%Y|C#)%eg*2~+dH~RNUc0nMb#CqN5}+z)apGvzNkmzF{zybp-RKedbP-u}{=XYY zhBiksLC>i?hzUm)4nYrIErUP@))3&9(;cO>eyhAveuu>gn;J{ll^|3FZEBE@iC;AF z>K`kzr7B8~Fp><%34n2ArFq0J?m5ICx#sf5C#3nD;X=}^?T>#kJYT%@{NEh=X9EX|paq#Y8j&3j>E(~tQ%}~4 z@Gapi+}kSKLNsPr{+fuvdOeCa@@!@rldEzfKg9DTp_Cth8(rvAYyqAo^CWns=6pmK zUWU?~-m?g5#4Hh_>zpQ3#%UhiI`7rPR6hIb>Vp~Y9{uXyF7z6!c%OXMm1WuiK z7N;gEPj5zf9f$Ids7@+ki;5DD^QyTkZCCSDR49GSQw-cHjFsq6L)^j|jpm8Z&wTyO z2j?o5ZlGrQ$BoY|U5eU)#e-IDlP}7@%0J5&*u!6)q}%D{W1Ql7b`Mkmn>@)6V|E4H z8;2^JAOI!|qE@q3J;>k$jX_C-0uCP)rt;h6aq-bKgP~@% zcj*Qhp|uSQSHFhm=nd3J+gH!`&;!3;r*{)kCsH7f-N&u@g@7#^7_ks1@^S5K*(wt( zOh!NvBF=kc=68* z6i|SfRQJK@B+*P>O0qnyZ%VCUc%qPZ>uZ01G9DA+@2$ z5y%E#@<4!D@Af#DTB{28D&inp1Oa(F|@c;>Eo52~HZ1 z!g5bSiLzlHCozYUAO-}%Q0A0;60>orc{J7=o4{F?7nOlEVA}Iqr!Gpp9*@hnRGINo zGmje-BbL7V$i=t2xJq6*x~DdH_8(t06fYc6dhzsd|4Ucu>uKL(-yq4ev_#CA5zJthr`~Hdk*_4S}bGlp&lWWXMowTNoaSqVc>T(3sZAhESSC;q(kjm+Pw^<_@v)D>oHR3h4 z$OxWM*5_%R(WYFhNmR!E8e=Q6MJZXp;1(XwY)KF`N830I931xT6Hh!Y>z>>A^a6Pl zEBMz(+WK12vUcu3RzdS1npE!pM}fw4c9 z$u9q@tk5c&uUhDM=I(4|Y0&p5^j!g7b(1IjeIplY=yVMouc5;=w2LMS$}(%ImOjIW zvXHEm?rQf5_j$Kya#MG#-JIpO=x0G*DBm>MN~;4SI{dKO<~HGI9MoGqS$_1 zty&mmao11@q;aq_!UrvWb_&BZSV*_8AJ2ZQ1WwfsMbFZdK16ItN#!kI3b^EE{ zR!-krZU)~MbJpyrsX6?2PT$SEg}InfiRA%XnNOIR+4^U;(hGTEW)1T8|B0>s;3^Ks zeWqF+{U@g4gDNb=NCoslK1Sq~xUqh#FB&J;j0dR5Xw9TgA2c2|EL=L@BPU6nfrJ+0p#-{;TMr1hL5~Bz;nU=C1C|k9u6X*zAj2=w96kF-S&Vc zpQVXjU1KnW)Hye8LYiEtS@ZjU(Tj=m?`!nnru(WK0d&y2SD}%P{^*q;8e-L}vyzeS zR`epz4eKl*GngM$%r;8o8w?Di$ z7!Y&L%qT_IXF(m@7Ii_^$08P$i7-^A5{Amt7wa4Jq7sbCqcKifp2dhV4X%(wx~w@M z^*`fLoJTkqE%7J_z_Ta!QQO#k*JfQg{obzbHC5Ng*IZco{bzSNZXdoieW?GjRpSJYYBDa;AtjOy7mPB*D5gE_4I7GO)R|IoeXngh= zd7XTV>K~m*Yvi00h3l5MX+E1fx+8VTUsMGboZ@^8=oloN zRG%l_x7(E~*df5z=Z-$p|k+>n46J&#Jlp%9Akx~`;*e20=^F_hOpmzSs}B|txM4f zI$f9&^Kzh#CDRNzE19a{P!?Kh;$kMlmyVLBLKNWfC`wXuLc2LY;!(mPJ{}jL^k=#F zSi>-rq#k|qExB0C2~PWVbUPZ!mMM_pzrKk0F(8ekNry>fQ$r1WwbxL*zynATVb*Ld zo&!yE3W*MyV0p~KB#YC+lo(X6u!!sga~Ck08GCI_T}`a5G2h6v#z-S;8%E=Gc>2Pr zjn@j28GY`MD6cqsQ4-op*;2k#CW$IOhK`S?qVb>$e)tfEMAU#Nw-Amuq)n{Z8kEZ; z2W4WB#J)zqO@BZy=(&bXL9lzHeV6^HU9h{cuV;2U(+lcwa|DghsO|cZj1Glms+YpC z!US54hoKPlL(!NLg#z-LBT%7-2H@WE53C`mR^G%v%BKAA1K#Wr5OBhWho5|%yCC7x z{3{6U7xa-HS@hPGtz%=uC0E?Sla)yklB;#;=6W`i=E(awD+lY3oICsm>lJ+a)T=+Q zXD%!SMaGJVk1I6u`k{AqpX#oH3OGvIU|wf6Uub?n$vCfZI4gc3{vZk>-+z~V%C0g& z0|-A4{FtH4bRqTqqHm&N`s$hj|P?C5SWO#6$-XR{81&0LZ()SF{*PRfQ?j> zAW}Vw3qW2hK1pz;-?gQEyo;YNpo5SmoPk$`t|Y>E5pUMkBo&cPS+o|B)ZFMk9z zbLJfLv2eMRz7)I`{E!}y2PnJ&SO)Rx3k!D;5TzM`m6 za-~+z71{`siMmKK{*nzPQ7x0@fk-^d<8)f{%wf^IdH_}}d*&2B@lZFc)YK1$O+El-N!YX3 zvdS>HWF1xtERKc5 zq#?$sh&T?~12H6f<5spNxLimI&SEdl75Y@Y(D*UOT0NpQJa-*Q#`-ht_6KS(}sF42&94sfIV-_8QCGx1-Vr~%qJ5OCsOIAK{TMlDy! z{276@YbE;~v{LdF=6PM2XR*JhD)Of=q0+X38}-_orX~|JBVZ8i$P1aOn2^$ViI>qN zoA#QH<0&kY&Uwr6yY`kY6tYrm#QQbZp?ZnJ&@5@EE@^P)k(5ajEf%}S;dWTesx(xR z(XYP$e##1Zhz^$b$o*DKp${wbp#ugDplc;mt-OiZ%KPPdX&u@C^#+!MTD}5kmt=pG z*2X77tmZeg!hZD|*@{ho7Tz#!P05;P zv!0wWZf(ix1({E>oUKo0be+Y&Ku+JvOWD`Z|7z%#IKM@s)k*90j2XgEA3-*Xye<>& zw&G%8DV6e9I9ERSIchwE$4XK5b_|a*3fa6WbKAXdIjBl(2KlXUlIxEk#!QFXk^&pt zVq+@+bfh|)AgIqBXe%mIY^prsa5&W#?I5Gk#5FcbuJOfvt56B9LP-i8f!SbJaLbPe zaMXC9tqrIt&Bwmox~Vyc^Wu(Y_EpKbbbpm9LFkGY&RF@09$Ow3KKQ6F{f_>avm5Zu z0KW}aox$^vgZ&*PUAfb$v1&{tthJrTkF@rurDR8uqtda$vDb0jamAr_I1EAsjZ&Kp z(@ZFO0oR2}t^pL6YD%;bRdpd470ZVk6F?EZ(>kZ6q4eI;iQ8fcy_PSyLTAZ~ma*%N zo)6x8@6y2&s#zs7fh4TC7hE@mvA11P=~AeqgDy(1Ri~$h#!U=A-j_%&mUVke(Kb5^T*Ba z-Mezk{BcvKkEtxKe1y&{S-E2TxMjo9R5%&*mjWg}x7mW|;5cmD36i&8S3FsspOG&s*W=u!ur z##3GfSbp zZ)7H$%eKWP7;Q!y)mxnuMgBY^5UPiSmR5pG{7VTaAU}i!O-+Z{2oE`#%YFzruIrN# zeuC;kr3u$pcr2B_m1q>A#c60kwxHm+T-hLhLOaNx?3XXl_VR`A<%K7B?jT62Hdo1a zse~T^GKxJQ)6~S{H#2Fcz#sdCP8Ot)Y~y2cO5?(mOE?}e2Hy%;Me?N=wL+a@v_u7$i ze`exoZ=@?G8Gpnn#6-qKYE7qv4vtgWq%+YG(eY(X7}r*)z{0 zFa%st2@vfeh7Tm0^#c?|aqs+o(JqgG@(tCzDCP|{TYPYnX{c~f`KGA^&bv}QN#GEL zo|TjQYe7k35=UwaNw>a+Y zc)Dyko&dI^el^P<7elpmCu}s?R>`$*M4c%`RSo|m=4-7s%4oQ1`I1Im74W}V`|!uV ze@bN)2rfpf|Q6EDxcpXh*&#cGDq9BG2fQ?~8PoyX^Hn7lA?$tLRkZR*UjiF1Y4 z(-(X=ZGxoIE%Z(5_M_Z~X0T5E3nq=UbFMi}DnvHElk0sL0x$-u7TQ1!g{l_ksjmmS8_{Sj?X?IB23DQ+unG^mn z2t{shzkw_ic@w57%0YOti9e&p2|y7*qHk^EU7>aG8lAtcsj1~@MLgnYhNKbyM5Fwf z>~;Rp5wUGrpN@%&j7&Hk8y#C5LyKc+bS#a*|G#pIZ4Zi#igDm!;xnxxS|y!vHN?>J zn97(HF+x~O>lhZ>iWa*jyXLtB=CZn2Oe>eGRgCa&)T=z+&+TRYh%YB!#aa4;f_VkC zxgMg03tt{a{ojb$<`wuOVF>=%;}j|4g$eS1(em^g`_GNu{5|a}muK%BI->9Y7g}a{ za5#$PYW!QkZ?9UH=RAy6X}T`i#d@h4d~Q03lroBtz?hV40wqOQe@`#olI+Vs_esKV zVQaG-sPad>t~-o4EH99FU2_=0O1`&UtbwD!-3~malf9kQpvH_Jd2nRf#h(CdLgiJY zV;9l7wJF+_+G=e8wzyhrsHMzrASmv0E7>L$&#)fgHNDVwY@Z@6KIPh7iRN+X74oa# zSt-w{heUzx0hP{5=N~=t?1I|E&kmegkvnk8WYyWD^XJzdo;UxfEKSZGG_@ixcQUwS z1IQ=zw62LH`kh8Rup8F=#BSIdhKL8A+ErqS%{8qYH&7UgdZcL{$AeGr%5#N{?EVwH z@?GWW5;}@i3+1ZQh)F!^_cT9y&kA0UBMd`cRBa{bk94+rrK4?hp(+j3Q zn>l|GeS%e&zCV3@dc}a@6K0n6T0DO-_-!~_A>;z881h&DKw`Glg}0`yTQjp+ZKyLb z7aPcmSS4G*wxHmKsUsqYx>!9KRg1**^<6W)W~MvLwA@Vf=4eEF&AM<7bGYMb)R(%e zyp-r54mGMPP_s!8_8fR_YBYOO+_l3b(ANA5l55Ttg;c4RKiT;};@8eL2i7iIFmzJk z+SS>av&Uo=^eV4>YM$65VfD0msdFBwnp;*`o;kemsnPh~k$=l*OX}y?B0NmOUNE6% zy!p{+2$_22nJher53}1#w~o&HVsiB?@z;er9B(`anRNg@cqBYn2kGHA>r`1Q5U30{ z5o=b9rT_ns_9pOA6nJkFVAfg5^abt4l!3F2(JJc(kAd+gpA0xNqfW!5YvVUYPx>e^!M+- zYwP`4&c-*EkI>rMSd-D-c7S|itN!cKwU-#XwZRy}c}BZZAtEOiZdKMbs>g7;kX=&< z6mKtepbBqAh@vA-wocNW38$~t>GUE0n2+&7*Gi>`*3lf3MukEZ_dEei5J{*7gj4tH zgUj-jin;I1J?CO$(!tMpnbEGz{i#VV3xJJHMm{HFpYd9LRHpk>uSBRv`WbS)AA)^x zx>RpmqvXr!3PpWvMl32H3dSd$|GebsXi4O|g0;2*4sWd;KpvY@Qtxm#;Eg4>+wVjg z``uo?-RbD4-Fh(thEB!vHZ@fqQ=OUVX^!3JoB4M4Gn0f{W{;T8uI39Ny`ad6r+L(A!(YJYcsD&gc8 zmJVqyaoj@~PD7-@uU@&M^`U!rY+L=@b8WJ|{qYUko`3B3+va>q1Q|a&%enHE{AHE4 zLR+sr11Pl5;rc4!{BpfVT z>zmnN?lrC%g_<7|M;TXUo%$9`?{RuF`Gaj}AKym9aU7taZupTps9)qWZ@szjK z*YFOv7`x1;l$-A`3hyu^(Oc$4^bSSra1mQr#B|&z*njfbC;9BXeD-QSYXPRG>kE{N z@nf}~Fy`QfOM4|>nvkf-x7)SVf_I2>-+J8kD2vZmvp$na$ldcr<4`JgDHIqpAj+bd z1bkt?D8?gAY*^9sL;Hbtf+h?&72RW{pZ@gkrI zXx`87c$STo!mZq=7U4X9wiU5rTOlhcm+=47w4aTtG-IV;!o|b*z+_+Gms+z6@C>sx zbuWca*LQh(ENm>Szb<1jTJ_jzdM`=0N6ilEJ=P}kpjsqj^c}5X>k~BmF5_3M&u9?N z`C!A!l=-)r>`*34X0k*ki)S+3TrsnkK!*9;a!ZIIU*%YP{86^U1K@*ZSeh88+2L%~ z<)HFr@L=$B>^FFbG^e+of!LL?(Lh&GfahD*0Y58qxsa=;+NH39gdW|~;9Rfo0rK9- zbf@n01|1EW>}>$8mB@mm5HI0DDTf<~NAlH}=ID*4xrrDDU0j9N)KAHJWUPq)=%TG` zPt7t8HEw2O<*7yig@f2f?Q`0vi=;g(Ba6MxpTO!a!WoFXH_T2E=2DYxO9J-S#`;8h zX)YlIXd^Pkgv0;?BP0esK}Za9t3|-rHn;lROzI*rJ=@Qgs!SPR0eqm??fa$HkeC9i zHFfU_oUU(@0COzU5wjU9MHBGh>ku#|!9ya((z*UeaLxT(jQ$L3^!Ili{py~h@7aE~ zG^fkxZ|yPq9<5KwyO6eR5_{}fA5u98 z9-IMMG)c!#k{#bU=sO2}M{z1@B;e8^{9+*z?e_v=rsN@FK!6n~R!R|1ku+wKF_SwF zQKKl*^?^iY?Z(T~ko|eyRAU#bnr!SeUYX4Iv8rjt%WT*bW0$deGCRk3Z3<4|WJjyA zL~RG!filq>pO=CNB@S^!iP{W*g|dQ5Mtp+XftZ-;@)l%F;CX5(Owp`hW=M(TG)S`B zq1c!|7-?|A0VSMm2$2yD9!5w&PtsZRBm9mfC57aeB_Fu(*9uRM%*ZVsVhkr+z(0tU zPh2+l79QFDn~BTj&q5~Bi`Q{Ed8hFVzs-=6OaA!Sh}ENB+r}FCH`CUPdh-RNl~R+~ zL-O#;Q?2ju?DpN`8Pb&Ar87%pYzR3UV#(PKA!iJS5H!{foN-!Lx$N0~wlva|1coz? zjr0Y6sdbkE%8Ex?t-F|&QA#>&T@dQy!3Y%%&%qoMvUbhvHc zfH%))XowcHHnT^AK&|xFCwyz~z>OVF=Tpg8?nWutsN%u^u(`>;B+eJ}bTLsY!f8le ziZJ;py5(eTf4^H!76j-OFhQS6%3{(+co7%ChucrODRF@wVXo@9uO=%;LL8NijyJ@4 zPMiVSrj>|#j~>FfZnP#MQ4O+q;PY%C5)fqKH$%jtZ&4h%z)#Zd}J4le>xqp*OmoJ4{VOec0|#nSx~uAF!EqD3En z*T$ZB`QVaqXP$N2%FF+}wGE(-r^ie_XVh(%+%kQ7{l)_)M=33A#`^p3xMjq!{Nl66 zG(3FehNnp55yC^u$9fLO>S)sWPf4V~dilwEdb_o-mpzhRozt|QJr0Ez;nnYh=E+`c zcCSLw9A>L&ZT0A#T>w}OwWsbI`>=j}%CHt^MA2y&5>)+?pCp+cS9Ai`Zvn+`_Bu-$N z9xC&AVTm~dW_xIw{+O{1u$X&EeWfyKYU1oHXMw*)(#v+2@v^db@9L)me8V=i^*<_L!RC^{tm4OjL}W%2x~g`IarlzdpQi_tudc>W&`$<2u5n z-@Wl+;~SfQ??ny2zYkJ?yD{OuF=N$2W^Th_2`j5K6!)2G2yFxsTF6zcDOc|axrz$8 zdapGzicse%tGs9H*#O(ny62#Ia}*g-Op3Cx(M<6nY?k!8_xK#UK$nCqPmj@aPV)>Q zBj$*v2ds6@qILG?Ry+u*;NnyFY>M40e4tYPOTI<961JjwUm{Nc{P_jAjaTMiaGL< z!HC=Kp!id%$by6+-^q6>u6WR4P-Ji$!_z61u!^E$6^CPSMTE{qD+*FR2snEiyf-Gr zg*?}O*0v6*?%>qnzg{ur;}I_-IM;Dxzj6Fu|Fdt0@edYX*95*B>u1~*U%Kb*OvH6Wx7m?N9j5DLZ&>CZCrPx@_oO?g!UJYc~d|lQ_ zATfnpLd0lQ|0d)zB;@iSEH>Hk3$174K0$ZIOf(-8ivChtcEQOmJ-5SJ`*~|IW-$(& z&t}gw5GkB{nzp3f%rQ^@i6j7q8VT;Uj-OgHt@fQdYK?Rpp5o28)sxrfqC`c4P$NiS z_6D>hAxc2Yv^fvn#r258xu22rgg#SWsW@A;= zzl^-OgmFwq;^0wMt?&8OfKwZrzY-!9I);Wl06n z4bO#jN4PeJtVDbj3}J&IrrA3E70wAMnZ0T~PBPfszQW8}PC=NQPqjWh69@3Mb3J*CjAFFV_0D}HW2=$w&F~sw$!yf<23yW-C>SmI__EG zb{Qe%!7{SVo;!8kSZMaSV${HUW7M-pn&W^TW*5TW)44ZbsR?{Be7(d5H#E^Zv-VrH z0|s^6Tz9e=b=0M!%{Ng^>(dyH?`EP{fpw2*hVLP!4e>C@SZ*-s0 ztDu|ZB>J``I{O~gkuv*+69IcT)f(#VOEUZZrnB!+-5p2U7VJX8q9-ql4Iq*4toubaEc(AhVwSVTv;hSYNyrpg3)C9=T1YpvD0?)7 z7)6(z(4%jhjm_x07*4GS`rez(TC&-~Y&J8SC9~PB*%9$5jO&z5(EWKDDrK2ZgJxx8 zGef6&S_NiKya^jEZM5D5H*)ry?2MnkiFy>R=}j~?+Ui;C(#*(ddM3Z5+4D$gU6-D3 z$~6a0qDMEpo?{*fflcrK`t0swCLhH-G5f`0?M8O|h@LzB0w1+z=u%h9WOT<@Y613X zkyP_K=ARGYSaT^Crx;&AabyN+>TUea*4$n~8HkxA8NnZ7a6a`yey#wyc$wxM2v#kneOSgW zh3p3o1eBk`&OOrdfaL+2CtMar$oX{Xd3$eiyNxAmUfaVDr<3#^TnhAd9N=n&6|Dbl z!K2IC+LoOxVV@QMZJTj}F=rFaxA5tSo!&>x;f0b@%pvn)*|&9k*Xl-mqKi}tJ5%g* zVLP%5L*{ObVK)Y&8QpAI(_ctC-Rygmlw++t-FH=gcMqW1GwpM;=TVjgOBRMNySoKz z_MF+-^Jw@M%Y~efcght27unCoOwwxhJQ^B+Y!^~~(VSTKF+(4KJ*%_lF{qy_N#3oD zJ=f{;BbiuYTuS+kR-e!ts0f{hcAS6QX>C(GbEG8w<{XUd^n!|!hE6jFLPMA%9T__{ z(pX;iIS{;8Li(YVpt&xPlGg0TFb9|A#R&0XdJ`}Xkzpu$9)Z@-1<=spR=EO(6FgU~ z=TtbDa8_CKrph)Lg6iy1GmX55N_2%oSNaI0PCuo9PPXV&05^t<&zY{K0R-A58TLE=>#wDeiiQ%@vEvzWRW47`7xc3--rg@6u1jZlEW1P#JtH&6hUf>0oxO2khf!GdxE zLNpPZSW^@6iZ#j0Q(Ki+{*6DYPwpdzF{S@%WBDO+QY!RD(0c-@3b zU*U;|M0Hyuj}=9*P8)Go6?YGDqhw=K!ZtPUpP#k5tox+ZTJ$Y+rP=p|!pl!9O3D%_ zu0DoY^-psn%{FUCDarO0#d;X=dM}Og+Io z=v?+ma@SMuh*3${daGxRjj@~h%+XDsy#~F9Ojopmp$q*m20fBClVd^MEA$9yGjo)* zXU$QL?i?j)GuBn_LSDouMJ7RCRK1}r%<+^L>2xWMQOFk?MOkb>m{{Tyv zKn|(~9u*m%U4}y#8!HAV90+u+gh#7<5FtIqHxvOh;S>I10Ga5QteBFC`6u*uYBjJZ zC6`X#c!1sg9_c4!^NSdakY*|5 zwb{FzAC!^&I_XJv0cig;USUdeELsd{zPVM;?B*Na2Mr8vS;)z+NSb&hfWwe|9~AJm zj+a_JdN)mZ0IiW_jMgOeXpOrWTKh%cTU&hrXQwHjS$c^SKJf0v|W*1m~7VenKd z+dtNruZ)1dWjP47g z*m7}#=vaqyd-&2m$1H*^5Ur_q%p&$V`nHQX|KewTqct+5t-jIv3sXCZIsd4&XWtk} zp1RfQ8D-_g5jBGrGnEpIKoey?`z7IJ{ zvK@Z5W%jjp*=f(T_p{5h`BJ``%PJ4^LhxZaB#+PLs)vUKC>93PKZs%gK$n;k7;KaT zw!`HjyDXYT5n>Uc3xPYwZWV;FW)7AH|uS-+1A_z+iyo-Q4x5KZ{IxNCVkR!-c$y7&7FStVlX9=>~h14uZ8V(%y{Lrk_aoE~VKs zOsfqZY&HY=SzUdXO^{x!F=T>zhV;H8q&FaBeNSu8p3%C$u+>G+yz&##Iw)lQlh&Sn zcONwZIve`?$k?>L6Or$aWW;y^_r*H)8}>G#x> zdYM;a?Xu!`Cc`MXmEjG`DO#?tp^tDr0w@ALcqk+U%5XvoD)=C%=qPO$fjyB~jc-|& zci?ZAo!v)n{J{9Q?fy(>zduf4H>hn95X67SWYB?G92ZE(A5|~%@xhB}%oDpML%Q<0 z47)d@7Ia_75`&2x({{1#8k;-_RYh!tHjZi+!JwL9cc1iRqVUB@Nj(fuqpU_6LVjR? zeEO)^Yp;ORf_xO~`!WA>fm#9YGy zi}?N%m%qL*qd5cv1li6wgR!2N!?P0wUM~yQtaLVGM`S8e4RC6>@cNTJca{=K+K|A@ z=%!8XHp<}=lPF4)z~VIL4&F6#ubBf8lgh>;iC+*H7$1!pH29na;0ZA)4B8(sw#4oa zKPg{NY$2r61(HG7gaa`&8$4wePJ%<;$uU3=Ge{lu+c)WE~qwG5L``liM~L#=%{_6hjh&?<;LnY zf5TKry*KD?aLLgIyOIUI#h|5NVG|1UEBV@q!_!PGA?q+d-zJc+KpS)dqu>WLlkk+c zQZElE`<0QwrLna<$GC}2G1jtsQoGNpGu~w7)j*Jf`!s42SG$!<^G()MS*pZXS3{mC zO4L`nF45O1_4U{2IfPTluv!qvzK=fvS;8XP{`}WN$ zcIGABb{$UrWJZ`%@*ssWA$J4G#eLn|diRh3BWfm2J@}6!) zNOZ$jnJ>R|%YAJv>z=x7+@p)v8FljZ>l!bgcgYIllP8|6A2pgkJ8!=G&5w`)W``YD za`2$>{oxxyezc)(c0OCjo_y!8``+b2cBZk1q*kz&3e8MMCRrBbnStjlOUTwot>Fyb z>EVXl!IV%6#4Y(^l3QuE<&`7d+}|Ivch%$Dp>4s43OS^{p_|2>ih~#uC)kTUdX{#e zHP|K4ntHY&x_~g0R0K~0spkei1Jhd`V0V60)cONdwRlw8$@%j)fwumK`6h6J;CUNb zbHcy$CPG`omLO%lt>dHC7_PNl&LqK&D&}dhHBXJ!308!|pfO$`Wp!yS*ek>=jW=iM za`PlW=$&2Oqiv6#;T;HBJ;%{=s3`tRJ+li;4;G7~=V*_%cSAdZ&lNLDV+QgMmYtXC zw0zkbKf~-5^Y#{6foTJ+%(31Ee~_@#I)3bIh6O~+o+BM8?4QO`C=1>sMrHJRFyZtvbR_P;JHOzN4eR#+95)jFm%lwoyE;w*`taf18c z`7HyTkgCFNbbGyA+UsT;+-#K_w2yAkKDt5s=uV8?lOk$ye~!vLfUC9mWB~RzCvao*38I#P40GI_r?eS8B?OPzdlI^r5E7UbLP)f? zhO>IaIn8H~f7*P;MFJrwBBKNYXBYUan|+UG5E{gM#zjB^i1rBna5g%S)AI@NEP-X& zdICI4@CA@RJ*yK>M>of2v;s4n)hor}u-(wF7nJM*z%6BFTEREE=iw7r6y+_k$IlY- zazUpzjv`h(o*C%kzc86#gl?`!YxuuSPag%|DCQW?v}JVBANlCJ3sR3=(5XKhq(6L) zQ%F6Gq(`inU0}%zosM!&^i-@EZOxtuHF!3S8c6p}q_BtP8)@NY-<|V)N|&7@J~3OW zwDtx0+|F`#_d2#=R?pscsz z79M!fGaGHjX9#`sv$j3P+(rLV&r-no^PW$Qx$_1^J$t&gMNZJw@b8`gBGDtEDz&X< zw7tQ;3zZ2w?5f@FjYApw05fzmK}Q9HADRm~Okf}>`&;m*4T7a5pXfX7wmB2Gw!S-G zZC{byIEPSyJO6|!lMcYs^@B=$=wlQ8BDoP1F!I{Gws?*-!{hbpc}P|9#PdDUEqYFl zBQ`4kih%e0IqM}%yd9{YB#I;)7xCzuR(lbw8dQkqNpd6XQ7>&qG2VrDcm_NMs z*|O@PLncf_&+z{(JwnMX>Kn)??2#pb@R6A-gCI|N zl`y;TL=llks+x%Q(-SSZyd3q!Y7=KF#}e$@1Ur;qdlIZM!SWIyaN);#vBSOC-d+q~ zLM1nGc3}w~mfwjfq(Bt~TszZ!@ErV6wu>Epr}c#WrhIZjiT^zm?dC-kXhb1JPoOm< z|3)GCBK$OSP({3}|3F}*MbGQ3o~=D4_rdk2yjLMNlO)WTv_R{Kp0}Gl-6sPpV<6OqEdkzNBuSQ|hFAsYf1~o2xflO- z$*5any_OfgRJeSoa&XA)cMiveyQs<_=w#{;NO>34e__xaQY09)`J z^e?jePCW;?Og21AkPPEd@|IYyy9-__xO?cWV;9|a*<4M_DSC9|$Vbo58oXp7#y1iHtlDMN>Wvs=qGt#%n z^H)Ix>r(G`tnkSVpc2MgBIAypa*CNm!|hWp6L3>`m81DiycR#n$*$O`hafdnJzT9YUh3k@j z%D_)gDLWu-ar%z$K#Dhg=ObrNzaUDNFP4xT8AqB)NXL(%#f}WO9W&(qolm2(#0#`N zsF3KV<}Uo&|GQ3!J5uuDq$4E^t{(ACmmKdd36nD2V~JjFP!Yd!^4z(TuUWKU%9&L& z5d={V-h9Ub-=Q{pBmK zn}1^?o+TNW!?)FsB|kV`bEVrQwh~UH-6V^S+nvs!zQQ5jZ(AWD8;UrNIvI{AGnD*s z1w@=2!L_}NkXtxq*yO3P?PpDy+$)(mEo)ol)mQf!s~lwZAwzC5j#t9W;?fUz;(qL9 zq-0CyC;EAPOI;$@IU!SdCfV6E+STBifpFF#7s|x>o%}v;X9l#z2O<(vA)q>Cg$Au$ z=tAr350@ujpV@phn|{e9Z_I4I#!8LOn!S}0q%pgPl!N>$Jl}!3(YoE?ke4WmOTN#C z)0887*W?Yy`;h`6#X+fx1?*66uJetn&beLzCkGsrdoPk?Rsv3wW~~)(qQf6|NupA- z@g|u{1#o1X%R%Q7$*23`zQev_KGo-oE(!AcG|g{c6&~%G;aT8W?|H_v$8*SI^U&0O zJ+QJAHO$4#iW8HPhiMSjdljc+VQKmk9vay9ok_QiU3SHgzDQ}h;)+JKY59iG#ue91 zK69|ehLS!byWqmc8Ncp$P|9fI0hHDD!L=~F)fW_qHbfDskWl*nKXbaHqvIZEtpZ{B z_Q9mv*tpa)cxrya16ptv7bHO_l)5eTV5=LcurNQd*Tv)U*=z9GYVlbUYcMXg^t7K{ zg3pR$u*+v3rx>{StQn;pmwHvaEj?B|@U_kd(&O;7-{1y}YC|~D2U;V21fK{!%cC#| zQV-z{Y`HN->mbJVr08GRuz0UQqBV1))LUv$tHWcN$D;LZXx&78v)NYP51FlVsP!5^ zcDZ?gQfuj7s3&$@Yej1kRE=HyI=IHR$aH(DZ_UaxBY`5{SlB&SQ;jf+WR>7KH^Q5M zD-rYtlYYR@rMRi9U{FR$FRFou>RCx-VxsCOPJm~j$LMRqi7F5ZM=bfE;1wi>W^ec)yDy*-%fqyFOGyJ=PBvOz89!rb^TjV7UR^woWh}nSWjyoE0L^z2q z@=_u6Cwi+EQCbOpC)g;-YHsbG+Lcl~=8BTN*j4V=`PQJ)q-qBE2Xz-`o2hE?q zL_PEELpP+H{MbX7_k)-XLQH9daKg`%@iTdKG`dw->w-Kc{0vFPX)k=w@C-Wy*5!&s z;^y!jVO|=p3iElP-LK!qlE%i(LZ9i&gq6+PFTnC&Z`1l^k+*tAj;W zzKP8y0vZ_oBF&cY*{3UvMOCs-Qygm%d&s!vf3J|mpL`7Ob42VedLQgA{JanEOR;W^ zV%?BCQ!c6E&(7y18(YFAW|BDoM1OZwrcLQw$DN~HzT9#}Jqq>vy88qFNYr2};) zAqsFtqhKiHV4zrNt4t_SNS;Or@(UQ*jtsSdl+AA#A6Dh|FEI|j%0Am6uY2R}KQC=> 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

#cf7b!d>yWeyc2A*xN`Qh-!Gha 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 ke@Vs-7zxz!&}3uAWBWorkSize; ImVec2 windowPos; ImVec2 windowPosPivot; - constexpr float padding = 10.0f; + const float padding = 10.0f * ImGuiScale(); windowPos.x = (corner & 1) != 0 ? (workPos.x + workSize.x - padding) : (workPos.x + padding); windowPos.y = (corner & 2) != 0 ? (workPos.y + workSize.y - padding) : (workPos.y + padding); windowPosPivot.x = (corner & 1) != 0 ? 1.0f : 0.0f; @@ -173,23 +175,32 @@ namespace dusk { ImGuiConsole::ImGuiConsole() {} void ImGuiConsole::draw() { + if (!m_isLaunchInitialized) { + m_toasts.emplace_back("Press F1 to toggle menu"s, 5.f); + m_isLaunchInitialized = true; + } + if (CheckMenuViewToggle(ImGuiKey_F1, m_isHidden)) { + ShowToasts(); m_menuTools.afterDraw(); return; } + // TODO: we need to be able to render the menu bar & any overlays separately + // The code currently ties them all together, so hiding the menu hides all windows + if (ImGui::BeginMainMenuBar()) { m_menuGame.draw(); m_menuTools.draw(); m_menuEnhancements.draw(); - ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80.0f); + ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80.0f * ImGuiScale()); ImGuiIO& io = ImGui::GetIO(); ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); ImGui::EndMainMenuBar(); } - + ShowToasts(); m_menuTools.afterDraw(); } @@ -223,4 +234,44 @@ namespace dusk { return "Null"sv; } } + + void ImGuiConsole::ShowToasts() { + if (m_toasts.empty()) { + return; + } + auto& toast = m_toasts.front(); + const float dt = ImGui::GetIO().DeltaTime; + toast.remain -= dt; + toast.current += dt; + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 workPos = viewport->WorkPos; + const ImVec2 workSize = viewport->WorkSize; + constexpr float padding = 10.0f; + const ImVec2 windowPos{workPos.x + workSize.x / 2, workPos.y + workSize.y - padding}; + ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, ImVec2{0.5f, 1.f}); + + const float alpha = std::min({toast.remain, toast.current, 1.f}); + ImGui::SetNextWindowBgAlpha(alpha * 0.65f); + ImVec4 textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + textColor.w *= alpha; + ImVec4 borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border); + borderColor.w *= alpha; + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + ImGui::PushStyleColor(ImGuiCol_Border, borderColor); + if (ImGui::Begin("Toast", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoMove)) + { + ImGuiStringViewText(toast.message); + } + ImGui::End(); + ImGui::PopStyleColor(2); + + if (toast.remain <= 0.f) { + m_toasts.pop_front(); + } + } } diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 2cb887f904..db67c2a1cf 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -2,41 +2,56 @@ #define DUSK_IMGUI_HPP #include +#include #include -#include "imgui.h" +#include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" -#include "ImGuiMenuEnhancements.hpp" +#include "imgui.h" namespace dusk { - class ImGuiConsole { - public: - ImGuiConsole(); - void draw(); +class ImGuiConsole { +public: + ImGuiConsole(); + void draw(); - ImGuiMenuTools::CollisionViewSettings& getCollisionViewSettings() { return m_menuTools.getCollisionViewSettings(); } + ImGuiMenuTools::CollisionViewSettings& getCollisionViewSettings() { + return m_menuTools.getCollisionViewSettings(); + } - static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + static bool CheckMenuViewToggle(ImGuiKey key, bool& active); - private: - bool m_isHidden = false; +private: + struct Toast { + std::string message; + float remain; + float current = 0.f; + Toast(std::string message, float duration) noexcept : message(std::move(message)), + remain(duration) {} + }; - ImGuiMenuGame m_menuGame; - ImGuiMenuTools m_menuTools; - ImGuiMenuEnhancements m_menuEnhancements; - }; + bool m_isHidden = true; + bool m_isLaunchInitialized = false; + std::deque m_toasts; + ImGuiMenuGame m_menuGame; + ImGuiMenuTools m_menuTools; + ImGuiMenuEnhancements m_menuEnhancements; - extern ImGuiConsole g_imguiConsole; + void ShowToasts(); +}; - std::string_view backend_name(AuroraBackend backend); - std::string BytesToString(size_t bytes); - void SetOverlayWindowLocation(int corner); - bool ShowCornerContextMenu(int& corner, int avoidCorner); - void ImGuiStringViewText(std::string_view text); - void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); - void ImGuiEndGroupPanel(); -} +extern ImGuiConsole g_imguiConsole; + +std::string_view backend_name(AuroraBackend backend); +std::string BytesToString(size_t bytes); +void SetOverlayWindowLocation(int corner); +bool ShowCornerContextMenu(int& corner, int avoidCorner); +void ImGuiStringViewText(std::string_view text); +void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); +void ImGuiEndGroupPanel(); +float ImGuiScale(); +} // namespace dusk void DuskDebugPad(); diff --git a/src/dusk/imgui/ImGuiControllerOverlay.cpp b/src/dusk/imgui/ImGuiControllerOverlay.cpp index 0da82ac878..306bace22a 100644 --- a/src/dusk/imgui/ImGuiControllerOverlay.cpp +++ b/src/dusk/imgui/ImGuiControllerOverlay.cpp @@ -13,8 +13,6 @@ namespace dusk { ImGui::TextUnformatted(text.c_str()); } - static inline float GetScale() { return ImGui::GetCurrentContext()->CurrentDpiScale; } - void ImGuiMenuGame::windowInputViewer() { if (!m_showInputViewer) { return; @@ -35,7 +33,7 @@ namespace dusk { ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Input Viewer", nullptr, windowFlags)) { - float scale = GetScale(); + float scale = ImGuiScale(); if (!m_controllerName.empty()) { TextCenter(m_controllerName); ImGui::Separator(); diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp new file mode 100644 index 0000000000..4e2097cf3d --- /dev/null +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -0,0 +1,201 @@ +#include "ImGuiEngine.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "dusk/logging.h" + +#ifdef IMGUI_ENABLE_FREETYPE +#include "misc/freetype/imgui_freetype.h" +#endif + +namespace dusk { +namespace { +std::string GetAssetPath(const char* assetName) { + const char* basePath = SDL_GetBasePath(); + if (basePath != nullptr && basePath[0] != '\0') { + return std::string(basePath) + "res/" + assetName; + } + return std::string("res/") + assetName; +} + +bool AssetExists(const std::string& path) { + SDL_PathInfo pathInfo{}; + return SDL_GetPathInfo(path.c_str(), &pathInfo) && pathInfo.type == SDL_PATHTYPE_FILE; +} +} // namespace + +ImFont* ImGuiEngine::fontNormal; +ImFont* ImGuiEngine::fontLarge; +ImTextureID ImGuiEngine::duskIcon; + +void ImGuiEngine_Initialize(float scale) { + ImGui::GetCurrentContext(); + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); + + const std::string fontPath = GetAssetPath("NotoMono-Regular.ttf"); + const bool hasFontFile = AssetExists(fontPath); + + ImFontConfig fontConfig{}; + fontConfig.SizePixels = std::floor(15.f * scale); + snprintf(static_cast(fontConfig.Name), sizeof(fontConfig.Name), + "Noto Mono Regular, %dpx", static_cast(fontConfig.SizePixels)); + ImGuiEngine::fontNormal = + hasFontFile ? + io.Fonts->AddFontFromFileTTF(fontPath.c_str(), fontConfig.SizePixels, &fontConfig) : + nullptr; + if (ImGuiEngine::fontNormal == nullptr) { + if (hasFontFile) { + DuskLog.warn("Failed to load font '{}': {}", fontPath, SDL_GetError()); + } + ImGuiEngine::fontNormal = io.Fonts->AddFontDefault(&fontConfig); + } + + fontConfig.SizePixels = std::floor(26.f * scale); +#ifdef IMGUI_ENABLE_FREETYPE + fontConfig.FontBuilderFlags |= ImGuiFreeTypeBuilderFlags_Bold; + snprintf(static_cast(fontConfig.Name), sizeof(fontConfig.Name), "Noto Mono Bold, %dpx", + static_cast(fontConfig.SizePixels)); +#else + snprintf(static_cast(fontConfig.Name), sizeof(fontConfig.Name), + "Noto Mono Regular, %dpx", static_cast(fontConfig.SizePixels)); +#endif + ImGuiEngine::fontLarge = + hasFontFile ? + io.Fonts->AddFontFromFileTTF(fontPath.c_str(), fontConfig.SizePixels, &fontConfig) : + nullptr; + if (ImGuiEngine::fontLarge == nullptr) { + if (hasFontFile) { + DuskLog.warn("Failed to load font '{}': {}", fontPath, SDL_GetError()); + } + ImGuiEngine::fontLarge = io.Fonts->AddFontDefault(&fontConfig); + } + + auto& style = ImGui::GetStyle(); + style = {}; // Reset sizes + style.WindowPadding = ImVec2(15, 15); + style.WindowRounding = 5.0f; + style.FrameBorderSize = 1.f; + style.FramePadding = ImVec2(5, 5); + style.FrameRounding = 4.0f; + style.ItemSpacing = ImVec2(12, 8); + style.ItemInnerSpacing = ImVec2(8, 6); + style.IndentSpacing = 25.0f; + style.ScrollbarSize = 15.0f; + style.ScrollbarRounding = 9.0f; + style.GrabMinSize = 5.0f; + style.GrabRounding = 3.0f; + style.PopupBorderSize = 1.f; + style.PopupRounding = 7.0; + style.TabBorderSize = 1.f; + style.TabRounding = 3.f; + + auto* colors = style.Colors; + colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + colors[ImGuiCol_Border] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.12f, 0.20f, 0.28f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.09f, 0.12f, 0.14f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.12f, 0.14f, 0.65f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.39f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.18f, 0.22f, 0.25f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.09f, 0.21f, 0.31f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.37f, 0.61f, 1.00f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.29f, 0.55f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.25f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_Tab] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); + colors[ImGuiCol_TabUnfocused] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + + style.ScaleAllSizes(scale); +} + +Icon GetIcon() { + const std::string iconPath = GetAssetPath("icon.png"); + if (!AssetExists(iconPath)) { + return {}; + } + + SDL_Surface* loadedSurface = SDL_LoadPNG(iconPath.c_str()); + if (loadedSurface == nullptr) { + DuskLog.warn("Failed to load icon '{}': {}", iconPath, SDL_GetError()); + return {}; + } + + SDL_Surface* rgbaSurface = SDL_ConvertSurface(loadedSurface, SDL_PIXELFORMAT_RGBA32); + SDL_DestroySurface(loadedSurface); + if (rgbaSurface == nullptr) { + DuskLog.warn("Failed to convert icon '{}': {}", iconPath, SDL_GetError()); + return {}; + } + + const auto iconWidth = static_cast(rgbaSurface->w); + const auto iconHeight = static_cast(rgbaSurface->h); + const size_t rowSize = static_cast(iconWidth) * 4; + const size_t size = rowSize * static_cast(iconHeight); + auto ptr = std::make_unique(size); + for (uint32_t row = 0; row < iconHeight; ++row) { + const auto* src = static_cast(rgbaSurface->pixels) + + static_cast(row) * static_cast(rgbaSurface->pitch); + auto* dst = ptr.get() + static_cast(row) * rowSize; + std::memcpy(dst, src, rowSize); + } + + SDL_DestroySurface(rgbaSurface); + return Icon{ + std::move(ptr), + size, + iconWidth, + iconHeight, + }; +} + +void ImGuiEngine_AddTextures() { + auto icon = GetIcon(); + if (icon.data == nullptr || icon.width == 0 || icon.height == 0) { + ImGuiEngine::duskIcon = 0; + return; + } + + ImGuiEngine::duskIcon = aurora_imgui_add_texture(icon.width, icon.height, icon.data.get()); +} +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiEngine.hpp b/src/dusk/imgui/ImGuiEngine.hpp new file mode 100644 index 0000000000..18e49c2a47 --- /dev/null +++ b/src/dusk/imgui/ImGuiEngine.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "imgui.h" +#include "misc/cpp/imgui_stdlib.h" + +namespace dusk { +class ImGuiEngine { +public: + static ImFont* fontNormal; + static ImFont* fontLarge; + static ImTextureID duskIcon; +}; + +void ImGuiEngine_Initialize(float scale); +void ImGuiEngine_AddTextures(); + +struct Icon { + std::unique_ptr data; + size_t size; + uint32_t width; + uint32_t height; +}; +Icon GetIcon(); +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiMapLoader.cpp b/src/dusk/imgui/ImGuiMapLoader.cpp index 8d11c0673b..4baf48bdab 100644 --- a/src/dusk/imgui/ImGuiMapLoader.cpp +++ b/src/dusk/imgui/ImGuiMapLoader.cpp @@ -17,7 +17,8 @@ namespace dusk { ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::SetNextWindowBgAlpha(0.65f); - ImGui::SetNextWindowSizeConstraints(ImVec2(300, 0), ImVec2(FLT_MAX, FLT_MAX)); + ImGui::SetNextWindowSizeConstraints(ImVec2(300.f * ImGuiScale(), 0), + ImVec2(FLT_MAX, FLT_MAX)); if (!ImGui::Begin("Map Loader", &m_showMapLoader, windowFlags)) { ImGui::End(); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 00b287cc41..398e64de3a 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -84,13 +84,14 @@ namespace dusk { } static void drawVirtualStick(const char* id, const ImVec2& stick) { - ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 5, ImGui::GetCursorPos().y)); + float scale = ImGuiScale(); + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 5 * scale, ImGui::GetCursorPos().y)); - ImGui::BeginChild(id, ImVec2(80, 80)); + ImGui::BeginChild(id, ImVec2(80 * scale, 80 * scale)); ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 p = ImGui::GetCursorScreenPos(); - float radius = ImGui::GetCurrentContext()->CurrentDpiScale * 30.0f; + float radius = 30.0f * scale; ImVec2 pos = ImVec2(p.x + radius, p.y + radius); constexpr ImU32 stickGray = IM_COL32(150, 150, 150, 255); @@ -98,7 +99,7 @@ namespace dusk { constexpr ImU32 red = IM_COL32(230, 0, 0, 255); dl->AddCircleFilled(pos, radius, stickGray, 8); - dl->AddCircleFilled(ImVec2(pos.x + stick.x * (radius), pos.y + -stick.y * (radius)), 3, red); + dl->AddCircleFilled(ImVec2(pos.x + stick.x * (radius), pos.y + -stick.y * (radius)), 3 * scale, red); ImGui::EndChild(); } @@ -139,12 +140,14 @@ namespace dusk { } } + float scale = ImGuiScale(); ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize; ImGui::SetNextWindowBgAlpha(0.65f); - ImGui::SetNextWindowSizeConstraints(ImVec2(850, 400), ImVec2(850, 400)); + ImGui::SetNextWindowSizeConstraints(ImVec2(850 * scale, 400 * scale), + ImVec2(850 * scale, 400 * scale)); if (!ImGui::Begin("Controller Config", &m_showControllerConfig, windowFlags)) { ImGui::End(); @@ -225,9 +228,9 @@ namespace dusk { } // buttons panel - constexpr float uiButtonSize = 40; + const float uiButtonSize = 40 * scale; - ImGuiBeginGroupPanel("Buttons", ImVec2(150, 20)); + ImGuiBeginGroupPanel("Buttons", ImVec2(150 * scale, 20 * scale)); u32 buttonCount; PADButtonMapping* btnMappingList = PADGetButtonMappings(m_controllerConfig.m_selectedPort, &buttonCount); @@ -251,7 +254,7 @@ namespace dusk { dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(btnMappingList[i].nativeButton), i); } bool pressed = ImGui::Button(dispName.c_str(), - ImVec2(100.0f, 20.0f)); + ImVec2(100.0f * scale, 20.0f * scale)); if (pressed) { m_controllerConfig.m_isReading = true; @@ -268,7 +271,7 @@ namespace dusk { uint32_t axisCount; PADAxisMapping* axisMappingList = PADGetAxisMappings(m_controllerConfig.m_selectedPort, &axisCount); - ImGuiBeginGroupPanel("Triggers", ImVec2(150, 20)); + ImGuiBeginGroupPanel("Triggers", ImVec2(150 * scale, 20 * scale)); PADAxis triggers[] = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R}; if (axisMappingList != nullptr) { @@ -291,7 +294,7 @@ namespace dusk { dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger); } bool pressed = ImGui::Button(dispName.c_str(), - ImVec2(100.0f, 20.0f)); + ImVec2(100.0f * scale, 20.0f * scale)); if (pressed) { m_controllerConfig.m_isReading = true; @@ -308,7 +311,7 @@ namespace dusk { int port = m_controllerConfig.m_selectedPort; // main stick panel - ImGuiBeginGroupPanel("Control Stick", ImVec2(150, 20)); + ImGuiBeginGroupPanel("Control Stick", ImVec2(150 * scale, 20 * scale)); drawVirtualStick("##mainStick", ImVec2{ mDoCPd_c::getStickX(port), mDoCPd_c::getStickY(port) }); @@ -345,7 +348,7 @@ namespace dusk { dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis); } } - bool pressed = ImGui::Button(dispName.c_str(), ImVec2(100.0f, 20.0f)); + bool pressed = ImGui::Button(dispName.c_str(), ImVec2(100.0f * scale, 20.0f * scale)); if (pressed) { m_controllerConfig.m_isReading = true; @@ -372,7 +375,7 @@ namespace dusk { ImGui::SameLine(); // sub stick panel - ImGuiBeginGroupPanel("C Stick", ImVec2(150, 20)); + ImGuiBeginGroupPanel("C Stick", ImVec2(150 * scale, 20 * scale)); drawVirtualStick("##subStick", ImVec2{ mDoCPd_c::getSubStickX(port), mDoCPd_c::getSubStickY(port) }); @@ -409,7 +412,7 @@ namespace dusk { dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis); } } - bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), ImVec2(100.0f, 20.0f)); + bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), ImVec2(100.0f * scale, 20.0f * scale)); if (pressed) { m_controllerConfig.m_isReading = true; @@ -434,7 +437,7 @@ namespace dusk { ImGui::SameLine(); // Triggers Panel - ImGuiBeginGroupPanel("Triggers", ImVec2(150, 20)); + ImGuiBeginGroupPanel("Triggers", ImVec2(150 * scale, 20 * scale)); if (deadZones != nullptr) { ImGui::Text("L Threshold"); @@ -460,7 +463,7 @@ namespace dusk { ImGui::SameLine(); // Options panel - ImGuiBeginGroupPanel("Options", ImVec2(150, 20)); + ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, 20 * scale)); if (deadZones != nullptr) { ImGui::Checkbox("Enable Dead Zones", &deadZones->useDeadzones); diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 40bb3fa1ec..ca855a00cb 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -131,6 +131,9 @@ namespace dusk { BytesToString(stats->lastVertSize + stats->lastUniformSize + stats->lastIndexSize + stats->lastStorageSize + stats->lastTextureUploadSize))); + + // TODO: persist to config + ShowCornerContextMenu(m_debugOverlayCorner, m_cameraOverlayCorner); } ImGui::End(); } diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index 66c820dd78..42fcbdbe42 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -38,7 +38,7 @@ namespace dusk { private: bool m_showDebugOverlay = false; - int m_debugOverlayCorner = 0; // top-left + int m_debugOverlayCorner = 2; // bottom-left bool m_showCameraOverlay = false; int m_cameraOverlayCorner = 3; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 03ff6b8c0f..3c51875bd5 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -48,6 +48,7 @@ #include "dusk/logging.h" #include "dusk/time.h" #include "dusk/main.h" +#include "dusk/imgui/ImGuiEngine.hpp" #include #include @@ -225,6 +226,10 @@ static AuroraBackend ParseAuroraBackend(const std::string& value) { exit(1); } +static void aurora_imgui_init_callback(const AuroraWindowSize* size) { + dusk::ImGuiEngine_Initialize(size->scale); +} + // ========================================================================= // PC ENTRY POINT // ========================================================================= @@ -270,6 +275,7 @@ int game_main(int argc, char* argv[]) { config.mem1Size = 256 * 1024 * 1024; config.mem2Size = 24 * 1024 * 1024; config.allowJoystickBackgroundEvents = true; + config.imGuiInitCallback = &aurora_imgui_init_callback; auroraInfo = aurora_initialize(argc, argv, &config); From 5f0f3628cee82659ab822e128bd8e6746ef8713c Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 3 Apr 2026 17:09:29 -0600 Subject: [PATCH 02/18] Rescale imgui on display scale change --- src/m_Do/m_Do_main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 3c51875bd5..185ca8543c 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -157,6 +157,9 @@ void main01(void) { case AURORA_WINDOW_RESIZED: mDoGph_gInf_c::setWindowSize(event->windowSize); break; + case AURORA_DISPLAY_SCALE_CHANGED: + dusk::ImGuiEngine_Initialize(event->windowSize.scale); + break; case AURORA_EXIT: goto exit; } From d7644a294dfe14b94d54da228f012d732b11f1f6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 3 Apr 2026 17:28:32 -0600 Subject: [PATCH 03/18] Add pipeline progress overlay --- src/dusk/imgui/ImGuiConsole.cpp | 36 +++++++++++++++++++++++++++++++-- src/dusk/imgui/ImGuiConsole.hpp | 4 +++- src/m_Do/m_Do_graphic.cpp | 6 +++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 9bfdb71d38..ae7f87c681 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -174,7 +174,7 @@ namespace dusk { ImGuiConsole::ImGuiConsole() {} - void ImGuiConsole::draw() { + void ImGuiConsole::PreDraw() { if (!m_isLaunchInitialized) { m_toasts.emplace_back("Press F1 to toggle menu"s, 5.f); m_isLaunchInitialized = true; @@ -182,7 +182,6 @@ namespace dusk { if (CheckMenuViewToggle(ImGuiKey_F1, m_isHidden)) { ShowToasts(); - m_menuTools.afterDraw(); return; } @@ -200,8 +199,13 @@ namespace dusk { ImGui::EndMainMenuBar(); } + ShowToasts(); + } + + void ImGuiConsole::PostDraw() { m_menuTools.afterDraw(); + ShowPipelineProgress(); } bool ImGuiConsole::CheckMenuViewToggle(ImGuiKey key, bool& active) { @@ -274,4 +278,32 @@ namespace dusk { m_toasts.pop_front(); } } + + void ImGuiConsole::ShowPipelineProgress() { + const auto* stats = aurora_get_stats(); + const u32 queuedPipelines = stats->queuedPipelines; + if (queuedPipelines == 0) { + return; + } + const u32 createdPipelines = stats->createdPipelines; + const u32 totalPipelines = queuedPipelines + createdPipelines; + + const auto* viewport = ImGui::GetMainViewport(); + const auto padding = viewport->WorkPos.y + 10.f; + const auto halfWidth = viewport->GetWorkCenter().x; + ImGui::SetNextWindowPos(ImVec2{halfWidth, padding}, ImGuiCond_Always, ImVec2{0.5f, 0.f}); + ImGui::SetNextWindowSize(ImVec2{halfWidth, 0.f}, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.65f); + ImGui::Begin("Pipelines", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing); + const auto percent = static_cast(createdPipelines) / static_cast(totalPipelines); + const auto progressStr = fmt::format("Processing pipelines: {} / {}", createdPipelines, totalPipelines); + const auto textSize = ImGui::CalcTextSize(progressStr.data(), progressStr.data() + progressStr.size()); + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetWindowWidth() / 2.f - textSize.x + textSize.x / 2.f); + ImGuiStringViewText(progressStr); + ImGui::ProgressBar(percent); + ImGui::End(); + } } diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index db67c2a1cf..e12cc81844 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -14,7 +14,8 @@ namespace dusk { class ImGuiConsole { public: ImGuiConsole(); - void draw(); + void PreDraw(); + void PostDraw(); ImGuiMenuTools::CollisionViewSettings& getCollisionViewSettings() { return m_menuTools.getCollisionViewSettings(); @@ -39,6 +40,7 @@ private: ImGuiMenuEnhancements m_menuEnhancements; void ShowToasts(); + void ShowPipelineProgress(); }; extern ImGuiConsole g_imguiConsole; diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 06d6efd4eb..9bbbecbee8 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1634,6 +1634,10 @@ int mDoGph_Painter() { if (wn != 0) sDiagLoggedWindow = true; } +#if TARGET_PC + dusk::g_imguiConsole.PreDraw(); +#endif + #if DEBUG drawHeapMap(); #endif @@ -2244,7 +2248,7 @@ int mDoGph_Painter() { #endif #if TARGET_PC - dusk::g_imguiConsole.draw(); + dusk::g_imguiConsole.PostDraw(); #endif mDoGph_gInf_c::endRender(); From e04cd69851f6eb52b4f0b00999fbcc9940298df4 Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 3 Apr 2026 21:53:39 -0400 Subject: [PATCH 04/18] Fix Windows builds --- src/dusk/imgui/ImGuiConsole.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index ae7f87c681..6a06fce882 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -14,6 +15,7 @@ #include "JSystem/JUtility/JUTGamePad.h" #if _WIN32 +#define NOMINMAX #include "Windows.h" #endif From f3465b75063bdeb607201f0f819c5abfd0a55e58 Mon Sep 17 00:00:00 2001 From: madeline Date: Fri, 3 Apr 2026 19:54:57 -0700 Subject: [PATCH 05/18] better gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ae7fa75384..a381014599 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ compile_commands.json # ISOs *.iso + +*.ini +*.controller +pipeline_cache.bin From b9e16d33df177a162558551f5c0e06efb890921c Mon Sep 17 00:00:00 2001 From: Max Roncace Date: Thu, 2 Apr 2026 23:58:38 -0400 Subject: [PATCH 06/18] Refactor Dusk settings out to common interface used by game and UI code --- files.cmake | 1 + include/dusk/settings.h | 71 ++++++++++++++++++++++++ include/m_Do/m_Do_controller_pad.h | 7 +-- src/d/actor/d_a_alink.cpp | 6 +- src/d/actor/d_a_alink_demo.inc | 6 +- src/d/actor/d_a_alink_dusk.cpp | 5 +- src/d/actor/d_a_alink_hvyboots.inc | 4 +- src/d/d_bg_s.cpp | 21 +++---- src/d/d_camera.cpp | 4 +- src/d/d_cc_s.cpp | 17 +++--- src/d/d_kankyo.cpp | 4 +- src/dusk/imgui/ImGuiConsole.hpp | 6 +- src/dusk/imgui/ImGuiMenuEnhancements.cpp | 45 +++++++-------- src/dusk/imgui/ImGuiMenuEnhancements.hpp | 12 ---- src/dusk/imgui/ImGuiMenuGame.cpp | 24 ++++---- src/dusk/imgui/ImGuiMenuGame.hpp | 10 ---- src/dusk/imgui/ImGuiMenuTools.cpp | 17 +++--- src/dusk/imgui/ImGuiMenuTools.hpp | 15 ----- src/dusk/settings.cpp | 66 ++++++++++++++++++++++ src/m_Do/m_Do_graphic.cpp | 4 +- 20 files changed, 215 insertions(+), 130 deletions(-) create mode 100644 include/dusk/settings.h create mode 100644 src/dusk/settings.cpp diff --git a/files.cmake b/files.cmake index e5b470dde2..278ecdb815 100644 --- a/files.cmake +++ b/files.cmake @@ -1341,6 +1341,7 @@ set(DUSK_FILES src/dusk/extras.c src/dusk/extras.cpp src/dusk/globals.cpp + src/dusk/settings.cpp #src/dusk/m_Do_ext_dusk.cpp src/dusk/imgui/ImGuiConsole.hpp src/dusk/imgui/ImGuiConsole.cpp diff --git a/include/dusk/settings.h b/include/dusk/settings.h new file mode 100644 index 0000000000..daba1287d8 --- /dev/null +++ b/include/dusk/settings.h @@ -0,0 +1,71 @@ +#ifndef DUSK_CONFIG_H +#define DUSK_CONFIG_H + +namespace dusk { + +// Persistent user settings + +struct UserSettings { + // Program settings + + struct { + // Video + bool enableFullscreen; + } video; + + struct { + // Audio + float masterVolume; + float mainMusicVolume; + float subMusicVolume; + float soundEffectsVolume; + float fanfareVolume; + } audio; + + // Game settings + + struct { + // QoL + bool enableQuickTransform; + + // Preferences + bool enableMirrorMode; + bool invertCameraXAxis; + + // Graphics + bool enableBloom; + bool useWaterProjectionOffset; + + // Cheats + bool enableFastIronBoots; + + // Technical + bool restoreWiiGlitches; + } game; +}; + +UserSettings& getSettings(); + +// Transient settings + +struct CollisionViewSettings { + bool enableTerrainView; + bool enableWireframe; + bool enableAtView; + bool enableTgView; + bool enableCoView; + float terrainViewOpacity; + float colliderViewOpacity; + float drawRange; +}; + +struct TransientSettings { + CollisionViewSettings collisionView; +}; + +TransientSettings& getTransientSettings(); + +} + +#endif // DUSK_CONFIG_H + diff --git a/include/m_Do/m_Do_controller_pad.h b/include/m_Do/m_Do_controller_pad.h index b8cee23ca3..e187efd17d 100644 --- a/include/m_Do/m_Do_controller_pad.h +++ b/include/m_Do/m_Do_controller_pad.h @@ -3,8 +3,7 @@ #include "JSystem/JUtility/JUTGamePad.h" #include "SSystem/SComponent/c_API_controller_pad.h" - -#include "dusk/imgui/ImGuiMenuEnhancements.hpp" +#include "dusk/settings.h" // Controller Ports 1 - 4 enum { PAD_1, PAD_2, PAD_3, PAD_4 }; @@ -57,7 +56,7 @@ public: static s16 getStickAngle3D(u32 pad) { #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.mirrorMode) { + if (dusk::getSettings().game.enableMirrorMode) { return -getCpadInfo(pad).mMainStickAngle; } else { return getCpadInfo(pad).mMainStickAngle; @@ -69,7 +68,7 @@ public: static f32 getSubStickX3D(u32 pad) { #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.mirrorMode) { + if (dusk::getSettings().game.enableMirrorMode) { return -getCpadInfo(pad).mCStickPosX; } else { return getCpadInfo(pad).mCStickPosX; diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 2c5bcf46cd..48269ddc56 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -7510,7 +7510,7 @@ void daAlink_c::setBlendMoveAnime(f32 i_morf) { BOOL sp24 = checkEventRun(); BOOL sp20 = checkBootsMoveAnime(1); #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.fastIronBoots) { + if (dusk::getSettings().game.enableFastIronBoots) { sp20 = FALSE; } #endif @@ -9475,7 +9475,7 @@ void daAlink_c::setStickData() { mHeavySpeedMultiplier = mpHIO->mItem.mIronBoots.m.mInputFactor; } #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.fastIronBoots) { + if (dusk::getSettings().game.enableFastIronBoots) { mHeavySpeedMultiplier = 1.0f; } #endif @@ -9487,7 +9487,7 @@ void daAlink_c::setStickData() { mHeavySpeedMultiplier = mpHIO->mItem.mIronBoots.m.mWaterInputFactor; } #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.fastIronBoots) { + if (dusk::getSettings().game.enableFastIronBoots) { mHeavySpeedMultiplier = 1.0f; } #endif diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index 7acccbde13..39387582cb 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,8 +23,6 @@ #include "d/actor/d_a_npc_tkc.h" #include -#include "dusk/imgui/ImGuiMenuEnhancements.hpp" - BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); } @@ -4301,7 +4299,7 @@ bool daAlink_c::checkAcceptWarp() { */ if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) #if TARGET_PC - && (dusk::ImGuiMenuEnhancements::m_enhancements.restoreWiiGlitches || !checkNoResetFlg0(FLG0_WATER_IN_MOVE)) + && (dusk::getSettings().game.restoreWiiGlitches || !checkNoResetFlg0(FLG0_WATER_IN_MOVE)) #elif VERSION != VERSION_WII_USA_R0 && !checkNoResetFlg0(FLG0_WATER_IN_MOVE) #endif @@ -4312,7 +4310,7 @@ bool daAlink_c::checkAcceptWarp() { */ if ( #if TARGET_PC - (dusk::ImGuiMenuEnhancements::m_enhancements.restoreWiiGlitches || !getSlidePolygon(&plane)) && + (dusk::getSettings().game.restoreWiiGlitches || !getSlidePolygon(&plane)) && #elif VERSION != VERSION_WII_USA_R0 !getSlidePolygon(&plane) && #endif diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 47501d4f3d..83a5d592a5 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -3,10 +3,9 @@ #include "d/d_meter2.h" #include "d/d_meter2_draw.h" #include "d/d_meter2_info.h" -#include "dusk/imgui/ImGuiMenuEnhancements.hpp" void daAlink_c::handleQuickTransform() { - if (!dusk::ImGuiMenuEnhancements::m_enhancements.quickTransform) { + if (!dusk::getSettings().game.enableQuickTransform) { return; } @@ -70,4 +69,4 @@ void daAlink_c::handleQuickTransform() { OSReport("Running quick transform!"); procCoMetamorphoseInit(); -} \ No newline at end of file +} diff --git a/src/d/actor/d_a_alink_hvyboots.inc b/src/d/actor/d_a_alink_hvyboots.inc index ed52ffcb36..6f7fa8da40 100644 --- a/src/d/actor/d_a_alink_hvyboots.inc +++ b/src/d/actor/d_a_alink_hvyboots.inc @@ -6,8 +6,6 @@ #include "d/actor/d_a_alink.h" #include "d/actor/d_a_tag_magne.h" -#include "dusk/imgui/ImGuiMenuEnhancements.hpp" - void daAlink_c::concatMagneBootMtx() { if (checkMagneBootsOn()) { mDoMtx_stack_c::concat(mMagneBootMtx); @@ -351,7 +349,7 @@ int daAlink_c::procMagneBootsFly() { */ if (dComIfG_Bgsp().ChkPolySafe(mPolyInfo2) #if TARGET_PC - && (dusk::ImGuiMenuEnhancements::m_enhancements.restoreWiiGlitches || checkEquipHeavyBoots()) + && (dusk::getSettings().game.restoreWiiGlitches || checkEquipHeavyBoots()) #elif PLATFORM_GCN || VERSION == VERSION_WII_KOR && checkEquipHeavyBoots() #endif diff --git a/src/d/d_bg_s.cpp b/src/d/d_bg_s.cpp index f218424d30..f48c58fead 100644 --- a/src/d/d_bg_s.cpp +++ b/src/d/d_bg_s.cpp @@ -10,12 +10,14 @@ #include "d/d_bg_w.h" #include "d/d_com_inf_game.h" #include "f_op/f_op_actor_mng.h" -#include "dusk/offset_ptr.h" #include "d/d_debug_viewer.h" #include "d/d_bg_s_capt_poly.h" -#include "dusk/imgui/ImGuiConsole.hpp" +#if TARGET_PC +#include "dusk/offset_ptr.h" +#include "dusk/settings.h" +#endif #if DEBUG int g_ground_counter; @@ -619,8 +621,8 @@ static int poly_draw(dBgS_CaptPoly* capt, cBgD_Vtx_t* vtxList, int v0, int v1, i GXColor wall_color = {0, 0xFF, 0, 0xFF}; #if TARGET_PC - dusk::ImGuiMenuTools::CollisionViewSettings collisionViewSettings = dusk::g_imguiConsole.getCollisionViewSettings(); - f32 view_opacity = 255 * (collisionViewSettings.m_terrainViewOpacity / 100.0f); + const auto& collisionViewSettings = dusk::getTransientSettings().collisionView; + f32 view_opacity = 255 * (collisionViewSettings.terrainViewOpacity / 100.0f); ground_color.a = view_opacity; roof_color.a = view_opacity; wall_color.a = view_opacity; @@ -658,16 +660,16 @@ void dBgS::Draw() { cBgS::Draw(); #if TARGET_PC - #define IMGUI_TOGGLE_HIO_FLAG(status, flag) \ + #define DUSK_TOGGLE_HIO_FLAG(status, flag) \ if (status) { \ s_InsideHio.m_flags |= flag; \ } else { \ s_InsideHio.m_flags &= ~flag; \ } - dusk::ImGuiMenuTools::CollisionViewSettings collisionViewSettings = dusk::g_imguiConsole.getCollisionViewSettings(); - IMGUI_TOGGLE_HIO_FLAG(collisionViewSettings.m_enableTerrainView, dBgS_InsideHIO::FLAG_DISP_POLY_e); - IMGUI_TOGGLE_HIO_FLAG(collisionViewSettings.m_enableWireframe, dBgS_InsideHIO::FLAG_WHITE_WIRE_e); + const auto& collisionViewSettings = dusk::getTransientSettings().collisionView; + DUSK_TOGGLE_HIO_FLAG(collisionViewSettings.enableTerrainView, dBgS_InsideHIO::FLAG_DISP_POLY_e); + DUSK_TOGGLE_HIO_FLAG(collisionViewSettings.enableWireframe, dBgS_InsideHIO::FLAG_WHITE_WIRE_e); #endif if (s_InsideHio.ChkDispPoly()) { @@ -680,8 +682,7 @@ void dBgS::Draw() { f32 var_f31 = fabsf(s_InsideHio.m_p0.x); #if TARGET_PC - dusk::ImGuiMenuTools::CollisionViewSettings collisionViewSettings = dusk::g_imguiConsole.getCollisionViewSettings(); - var_f31 = collisionViewSettings.m_drawRange; + var_f31 = collisionViewSettings.drawRange; #endif min.x = player->current.pos.x - var_f31; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 59ed1d2c11..6540c74ec9 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -28,8 +28,6 @@ #include "d/d_debug_camera.h" #endif -#include "dusk/imgui/ImGuiMenuEnhancements.hpp" - namespace { static f32 limitf(f32 value, f32 min, f32 max) { @@ -766,7 +764,7 @@ void dCamera_c::updatePad() { var_f31 = mDoCPd_c::getSubStickX3D(mPadID); #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.invertCameraXAxis) { + if (dusk::getSettings().game.invertCameraXAxis) { var_f31 *= -1.0f; } #endif diff --git a/src/d/d_cc_s.cpp b/src/d/d_cc_s.cpp index ab71f08aaa..2a19de3dda 100644 --- a/src/d/d_cc_s.cpp +++ b/src/d/d_cc_s.cpp @@ -9,8 +9,9 @@ #include "d/d_com_inf_game.h" #include "d/d_jnt_col.h" #include "f_op/f_op_actor_mng.h" - -#include "dusk/imgui/ImGuiConsole.hpp" +#if TARGET_PC +#include "dusk/settings.h" +#endif class dCcS_HIO : public JORReflexible { public: @@ -770,19 +771,19 @@ void dCcS::Draw() { #endif #if TARGET_PC -#define IMGUI_TOGGLE_HIO_FLAG(status, flag) \ +#define DUSK_TOGGLE_HIO_FLAG(status, flag) \ if (status) { \ s_Hio.m_flags |= flag; \ } else { \ s_Hio.m_flags &= ~flag; \ } - dusk::ImGuiMenuTools::CollisionViewSettings collisionViewSettings = dusk::g_imguiConsole.getCollisionViewSettings(); - IMGUI_TOGGLE_HIO_FLAG(collisionViewSettings.m_enableAtView, dCcS_HIO::FLAG_AT_ON_e); - IMGUI_TOGGLE_HIO_FLAG(collisionViewSettings.m_enableTgView, dCcS_HIO::FLAG_TG_ON_e); - IMGUI_TOGGLE_HIO_FLAG(collisionViewSettings.m_enableCoView, dCcS_HIO::FLAG_CO_ON_e); + const auto& collisionViewSettings = dusk::getTransientSettings().collisionView; + DUSK_TOGGLE_HIO_FLAG(collisionViewSettings.enableAtView, dCcS_HIO::FLAG_AT_ON_e); + DUSK_TOGGLE_HIO_FLAG(collisionViewSettings.enableTgView, dCcS_HIO::FLAG_TG_ON_e); + DUSK_TOGGLE_HIO_FLAG(collisionViewSettings.enableCoView, dCcS_HIO::FLAG_CO_ON_e); - f32 view_opacity = 255 * (collisionViewSettings.m_colliderViewOpacity / 100.0f); + f32 view_opacity = 255 * (collisionViewSettings.colliderViewOpacity / 100.0f); #endif if (s_Hio.CheckAtOn()) { diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index a61219ffac..eb1900dc4e 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -32,7 +32,7 @@ #include #include #if TARGET_PC -#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/settings.h" #endif static void GxXFog_set(); @@ -11381,7 +11381,7 @@ void dKy_bg_MAxx_proc(void* bg_model_p) { C_MTXLightPerspective(sp1D8, dComIfGd_getView()->fovy, camera_p->view.aspect, 1.0f, 1.0f, #if TARGET_PC - dusk::ImGuiMenuEnhancements::m_enhancements.useWaterProjectionOffset ? -0.01f : 0.0f, 0.0f); + dusk::getSettings().game.useWaterProjectionOffset ? -0.01f : 0.0f, 0.0f); #else -0.01f, 0.0f); #endif diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index e12cc81844..a7c433be4d 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -17,11 +17,7 @@ public: void PreDraw(); void PostDraw(); - ImGuiMenuTools::CollisionViewSettings& getCollisionViewSettings() { - return m_menuTools.getCollisionViewSettings(); - } - - static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + static bool CheckMenuViewToggle(ImGuiKey key, bool& active); private: struct Toast { diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index 5ea315d8ec..eaf33a1908 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -1,46 +1,45 @@ -#include "fmt/format.h" #include "imgui.h" -#include "aurora/gfx.h" -#include "ImGuiConsole.hpp" #include "ImGuiMenuEnhancements.hpp" -#include + +#include "dusk/settings.h" namespace dusk { - EnhancementsSettings ImGuiMenuEnhancements::m_enhancements = { - .fastIronBoots = false, - .invertCameraXAxis = false, - .restoreWiiGlitches = false, - .enableBloom = true, - .useWaterProjectionOffset = false, - .mirrorMode = false, - }; - ImGuiMenuEnhancements::ImGuiMenuEnhancements() {} void ImGuiMenuEnhancements::draw() { if (ImGui::BeginMenu("Enhancements")) { if (ImGui::BeginMenu("Quality of Life")) { - ImGui::Checkbox("Fast Iron Boots", &m_enhancements.fastIronBoots); - ImGui::Checkbox("Invert Camera X Axis", &m_enhancements.invertCameraXAxis); - ImGui::Checkbox("Quick Transform (R+Y)", &m_enhancements.quickTransform); + ImGui::Checkbox("Quick Transform (R+Y)", &getSettings().game.enableQuickTransform); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Preferences")) { + ImGui::Checkbox("Mirror Mode", &getSettings().game.enableMirrorMode); + ImGui::Checkbox("Invert Camera X Axis", &getSettings().game.invertCameraXAxis); + ImGui::EndMenu(); } if (ImGui::BeginMenu("Graphics")) { - ImGui::Checkbox("Native Bloom", &m_enhancements.enableBloom); - ImGui::Checkbox("Water Projection Offset", &m_enhancements.useWaterProjectionOffset); + ImGui::Checkbox("Native Bloom", &getSettings().game.enableBloom); + ImGui::Checkbox("Water Projection Offset", &getSettings().game.useWaterProjectionOffset); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Adds GC-specific -0.01 transS offset\n" "that causes ~6px ghost artifacts in water reflections"); } - ImGui::Checkbox("Mirror Mode", &m_enhancements.mirrorMode); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Restorations")) { - ImGui::Checkbox("Restore Wii 1.0 Glitches", &m_enhancements.restoreWiiGlitches); + if (ImGui::BeginMenu("Cheats")) { + ImGui::Checkbox("Fast Iron Boots", &getSettings().game.enableFastIronBoots); + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Technical")) { + ImGui::Checkbox("Restore Wii 1.0 Glitches", &getSettings().game.restoreWiiGlitches); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0, the first released version"); } @@ -48,10 +47,6 @@ namespace dusk { ImGui::EndMenu(); } - if (ImGui::BeginMenu("Cheats")) { - ImGui::EndMenu(); - } - ImGui::EndMenu(); } } diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.hpp b/src/dusk/imgui/ImGuiMenuEnhancements.hpp index 0238317ce4..f40baaad65 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.hpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.hpp @@ -8,22 +8,10 @@ #include "imgui.h" namespace dusk { - struct EnhancementsSettings { - bool fastIronBoots; - bool invertCameraXAxis; - bool quickTransform; - bool restoreWiiGlitches; - bool enableBloom; - bool useWaterProjectionOffset; - bool mirrorMode; - }; - class ImGuiMenuEnhancements { public: ImGuiMenuEnhancements(); void draw(); - - static EnhancementsSettings m_enhancements; }; } diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 398e64de3a..e3aeb4e136 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -1,15 +1,13 @@ #include "fmt/format.h" #include "imgui.h" -#include "aurora/gfx.h" #include "ImGuiConsole.hpp" #include "ImGuiMenuGame.hpp" #include #include "JSystem/JUtility/JUTGamePad.h" -#include "d/actor/d_a_alink.h" #include "dusk/audio/DuskAudioSystem.h" -#include "m_Do/m_Do_audio.h" +#include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" namespace dusk { @@ -25,8 +23,8 @@ namespace dusk { if (ImGui::BeginMenu("Graphics")) { if (ImGui::MenuItem("Toggle Fullscreen", "F11")) { - m_fullscreen = !m_fullscreen; - VISetWindowFullscreen(m_fullscreen); + getSettings().video.enableFullscreen = !getSettings().video.enableFullscreen; + VISetWindowFullscreen(getSettings().video.enableFullscreen); } ImGui::EndMenu(); @@ -34,28 +32,28 @@ namespace dusk { if (ImGui::BeginMenu("Audio")) { ImGui::Text("Master Volume"); - ImGui::SliderFloat("##m_masterVolume", &m_audioSettings.m_masterVolume, 0.0f, 1.0f, ""); + ImGui::SliderFloat("##masterVolume", &getSettings().audio.masterVolume, 0.0f, 1.0f, ""); /* // TODO: implement additional settings ImGui::Text("Main Music Volume"); - ImGui::SliderFloat("##m_mainMusicVolume", &m_audioSettings.m_mainMusicVolume, 0.0f, 1.0f, ""); + ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0.0f, 1.0f, ""); ImGui::Text("Sub Music Volume"); - ImGui::SliderFloat("##m_subMusicVolume", &m_audioSettings.m_subMusicVolume, 0.0f, 1.0f, ""); + ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0.0f, 1.0f, ""); ImGui::Text("Sound Effects Volume"); - ImGui::SliderFloat("##m_soundEffectsVolume", &m_audioSettings.m_soundEffectsVolume, 0.0f, 1.0f, ""); + ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0.0f, 1.0f, ""); ImGui::Text("Fanfare Volume"); - ImGui::SliderFloat("##m_fanfareVolume", &m_audioSettings.m_fanfareVolume, 0.0f, 1.0f, ""); + ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0.0f, 1.0f, ""); Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); if (audioMgr != nullptr) { } */ - audio::SetMasterVolume(m_audioSettings.m_masterVolume); + audio::SetMasterVolume(getSettings().audio.masterVolume); ImGui::EndMenu(); } @@ -78,8 +76,8 @@ namespace dusk { } if (ImGui::IsKeyPressed(ImGuiKey_F11)) { - m_fullscreen = !m_fullscreen; - VISetWindowFullscreen(m_fullscreen); + getSettings().video.enableFullscreen = !getSettings().video.enableFullscreen; + VISetWindowFullscreen(getSettings().video.enableFullscreen); } } diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index 7df0a00973..6d7ed614aa 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -17,14 +17,6 @@ namespace dusk { void windowControllerConfig(); private: - struct { - float m_masterVolume = 1.0f; - float m_mainMusicVolume = 1.0f; - float m_subMusicVolume = 1.0f; - float m_soundEffectsVolume = 1.0f; - float m_fanfareVolume = 1.0f; - } m_audioSettings; - struct { int m_selectedPort = 0; bool m_isReading = false; @@ -33,8 +25,6 @@ namespace dusk { int m_pendingPort = -1; } m_controllerConfig; - bool m_fullscreen = false; - bool m_showControllerConfig = false; bool m_showInputViewer = false; diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index ca855a00cb..4fbe7745b7 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -23,16 +23,17 @@ namespace dusk { ImGui::Separator(); + auto& collisionView = getTransientSettings().collisionView; if (ImGui::BeginMenu("Collision View")) { - ImGui::Checkbox("Enable Terrain view", &m_collisionViewSettings.m_enableTerrainView); - ImGui::Checkbox("Enable wireframe view", &m_collisionViewSettings.m_enableWireframe); - ImGui::SliderFloat("Opacity##terrain", &m_collisionViewSettings.m_terrainViewOpacity, 0.0f, 100.0f); - ImGui::SliderFloat("Draw Range", &m_collisionViewSettings.m_drawRange, 0.0f, 1000.0f); + ImGui::Checkbox("Enable Terrain view", &collisionView.enableTerrainView); + ImGui::Checkbox("Enable wireframe view", &collisionView.enableWireframe); + ImGui::SliderFloat("Opacity##terrain", &collisionView.terrainViewOpacity, 0.0f, 100.0f); + ImGui::SliderFloat("Draw Range", &collisionView.drawRange, 0.0f, 1000.0f); ImGui::Separator(); - ImGui::Checkbox("Enable Attack Collider view", &m_collisionViewSettings.m_enableAtView); - ImGui::Checkbox("Enable Target Collider view", &m_collisionViewSettings.m_enableTgView); - ImGui::Checkbox("Enable Push Collider view", &m_collisionViewSettings.m_enableCoView); - ImGui::SliderFloat("Opacity##colliders", &m_collisionViewSettings.m_colliderViewOpacity, 0.0f, 100.0f); + ImGui::Checkbox("Enable Attack Collider view", &collisionView.enableAtView); + ImGui::Checkbox("Enable Target Collider view", &collisionView.enableTgView); + ImGui::Checkbox("Enable Push Collider view", &collisionView.enableCoView); + ImGui::SliderFloat("Opacity##colliders", &collisionView.colliderViewOpacity, 0.0f, 100.0f); ImGui::EndMenu(); } diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index 42fcbdbe42..35604e0c93 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -10,17 +10,6 @@ namespace dusk { class ImGuiMenuTools { public: - struct CollisionViewSettings { - bool m_enableTerrainView = false; - bool m_enableWireframe = false; - bool m_enableAtView = false; - bool m_enableTgView = false; - bool m_enableCoView = false; - float m_terrainViewOpacity = 50.0f; - float m_colliderViewOpacity = 50.0f; - float m_drawRange = 100.0f; - }; - ImGuiMenuTools(); void draw(); void afterDraw(); @@ -34,8 +23,6 @@ namespace dusk { void ShowPlayerInfo(); void ShowAudioDebug(); - CollisionViewSettings& getCollisionViewSettings() { return m_collisionViewSettings; } - private: bool m_showDebugOverlay = false; int m_debugOverlayCorner = 2; // bottom-left @@ -67,8 +54,6 @@ namespace dusk { bool m_isDevelopmentMode = false; bool m_showPlayerInfo = false; - CollisionViewSettings m_collisionViewSettings; - bool m_showSaveEditor = false; ImGuiSaveEditor m_saveEditor; }; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp new file mode 100644 index 0000000000..9a5bb20c8c --- /dev/null +++ b/src/dusk/settings.cpp @@ -0,0 +1,66 @@ +#include "dusk/settings.h" + +namespace dusk { + +UserSettings g_userSettings = { + // Program settings + + // Video + .video = { + .enableFullscreen = false, + }, + + // Audio + .audio = { + .masterVolume = 1.0f, + .mainMusicVolume = 1.0f, + .subMusicVolume = 1.0f, + .soundEffectsVolume = 1.0f, + .fanfareVolume = 1.0f, + }, + + // Game settings + .game = { + // Quality of Life + .enableQuickTransform = false, + + // Preferences + .enableMirrorMode = false, + .invertCameraXAxis = false, + + // Graphics + .enableBloom = true, + .useWaterProjectionOffset = false, + + // Cheats + .enableFastIronBoots = false, + + // Technical + .restoreWiiGlitches = false, + } +}; + +UserSettings& getSettings() { + return g_userSettings; +} + +// Transient settings + +static TransientSettings g_transientSettings = { + .collisionView = { + .enableTerrainView = false, + .enableWireframe = false, + .enableAtView = false, + .enableTgView = false, + .enableCoView = false, + .terrainViewOpacity = 50.0f, + .colliderViewOpacity = 50.0f, + .drawRange = 100.0f, + }, +}; + +TransientSettings& getTransientSettings() { + return g_transientSettings; +} + +} diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 9bbbecbee8..a7a09a8d87 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1163,7 +1163,7 @@ void mDoGph_gInf_c::bloom_c::remove() { void mDoGph_gInf_c::bloom_c::draw() { #if TARGET_PC - if (!dusk::ImGuiMenuEnhancements::m_enhancements.enableBloom) { + if (!dusk::getSettings().game.enableBloom) { return; } #endif @@ -2117,7 +2117,7 @@ int mDoGph_Painter() { #endif #if TARGET_PC - if (dusk::ImGuiMenuEnhancements::m_enhancements.mirrorMode) + if (dusk::getSettings().game.enableMirrorMode) #elif PLATFORM_WII if (data_8053a730) #endif From 45137f4838cc19c44af07dcfecff57434df7464c Mon Sep 17 00:00:00 2001 From: Max Roncace Date: Fri, 3 Apr 2026 00:23:09 -0400 Subject: [PATCH 07/18] Move hotkey bindings to separate header --- include/dusk/hotkeys.h | 19 +++++++++++++++++++ src/dusk/imgui/ImGuiMenuGame.cpp | 5 +++-- src/dusk/imgui/ImGuiMenuTools.cpp | 13 +++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 include/dusk/hotkeys.h diff --git a/include/dusk/hotkeys.h b/include/dusk/hotkeys.h new file mode 100644 index 0000000000..ba187a544c --- /dev/null +++ b/include/dusk/hotkeys.h @@ -0,0 +1,19 @@ +#ifndef DUSK_HOTKEYS_H +#define DUSK_HOTKEYS_H + +namespace dusk::hotkeys { + +constexpr const char* DO_RESET = "Ctrl+R"; + +constexpr const char* TOGGLE_FULLSCREEN = "F11"; + +constexpr const char* SHOW_PROCESS_MANAGEMENT = "F2"; +constexpr const char* SHOW_DEBUG_OVERLAY = "F3"; +constexpr const char* SHOW_HEAP_VIEWER = "F4"; +constexpr const char* SHOW_STUB_LOG = "F5"; +constexpr const char* SHOW_CAMERA_DEBUG = "F6"; +constexpr const char* SHOW_AUDIO_DEBUG = "F7"; + +} + +#endif // DUSK_HOTKEYS_H diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index e3aeb4e136..91aa0314df 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -7,6 +7,7 @@ #include "JSystem/JUtility/JUTGamePad.h" #include "dusk/audio/DuskAudioSystem.h" +#include "dusk/hotkeys.h" #include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" @@ -15,14 +16,14 @@ namespace dusk { void ImGuiMenuGame::draw() { if (ImGui::BeginMenu("Game")) { - if (ImGui::MenuItem("Reset", "Ctrl+R")) { + if (ImGui::MenuItem("Reset", hotkeys::DO_RESET)) { JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; } ImGui::Separator(); if (ImGui::BeginMenu("Graphics")) { - if (ImGui::MenuItem("Toggle Fullscreen", "F11")) { + if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { getSettings().video.enableFullscreen = !getSettings().video.enableFullscreen; VISetWindowFullscreen(getSettings().video.enableFullscreen); } diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 4fbe7745b7..20e92768a6 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -2,6 +2,7 @@ #include "imgui.h" #include "aurora/gfx.h" +#include "dusk/hotkeys.h" #include "ImGuiConsole.hpp" #include "ImGuiMenuTools.hpp" @@ -37,15 +38,15 @@ namespace dusk { ImGui::EndMenu(); } - ImGui::MenuItem("Process Management", "F2", &m_showProcessManagement); - ImGui::MenuItem("Debug Overlay", "F3", &m_showDebugOverlay); - ImGui::MenuItem("Heap Viewer", "F4", &m_showHeapOverlay); - ImGui::MenuItem("Stub Log", "F5", &m_showStubLog); - ImGui::MenuItem("Debug Camera", "F6", &m_showCameraOverlay); + ImGui::MenuItem("Process Management", hotkeys::SHOW_PROCESS_MANAGEMENT, &m_showProcessManagement); + ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay); + ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay); + ImGui::MenuItem("Stub Log", hotkeys::SHOW_STUB_LOG, &m_showStubLog); + ImGui::MenuItem("Debug Camera", hotkeys::SHOW_CAMERA_DEBUG, &m_showCameraOverlay); ImGui::MenuItem("Map Loader", nullptr, &m_showMapLoader); ImGui::MenuItem("Player Info", nullptr, &m_showPlayerInfo); ImGui::MenuItem("Save Editor", nullptr, &m_showSaveEditor); - ImGui::MenuItem("Audio Debug", "F7", &m_showAudioDebug); + ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); ImGui::MenuItem("OSReport Force", nullptr, &OSReportReallyForceEnable); ImGui::EndMenu(); } From 970664d7f9689901ccd62df00371f7f3d1fcce99 Mon Sep 17 00:00:00 2001 From: Max Roncace Date: Tue, 17 Mar 2026 01:07:22 -0400 Subject: [PATCH 08/18] Add tweak to enable/disable TV settings screen --- include/d/d_s_name.h | 4 ++ include/dusk/settings.h | 1 + src/d/d_s_name.cpp | 77 ++++++++++++++++-------- src/dusk/imgui/ImGuiMenuEnhancements.cpp | 4 ++ src/dusk/settings.cpp | 1 + 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/include/d/d_s_name.h b/include/d/d_s_name.h index 73d3d703be..72a44c02de 100644 --- a/include/d/d_s_name.h +++ b/include/d/d_s_name.h @@ -48,6 +48,7 @@ public: void FileSelectClose(); void brightCheckOpen(); void brightCheck(); + void doPreLoadSetup(); void changeGameScene(); #if VERSION == VERSION_GCN_PAL @@ -70,6 +71,9 @@ private: /* 0x41E */ u8 mWaitTimer; /* 0x41F */ u8 field_0x41f; /* 0x420 */ u8 field_0x420; +#if TARGET_PC + bool mShowTvSettingsScreen; +#endif }; #endif /* D_S_D_S_NAME_H */ diff --git a/include/dusk/settings.h b/include/dusk/settings.h index daba1287d8..db49560fff 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -27,6 +27,7 @@ struct UserSettings { struct { // QoL bool enableQuickTransform; + bool hideTvSettingsScreen; // Preferences bool enableMirrorMode; diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 5f22177654..e001d97c71 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -18,6 +18,12 @@ #include "f_op/f_op_overlap_mng.h" #include "dusk/memory.h" +#if TARGET_PC +#define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) +#else +#define SHOW_TV_SETTINGS_SCREEN (1) +#endif + static dSn_HIO_c g_snHIO; #if VERSION == VERSION_GCN_PAL @@ -293,13 +299,27 @@ void dScnName_c::FileSelectMain() { } void dScnName_c::FileSelectMainNormal() { +#if TARGET_PC + mShowTvSettingsScreen = !dusk::getSettings().game.hideTvSettingsScreen; +#endif + switch(dFs_c->isSelectEnd()) { case 1: - mWaitTimer = 15; - mDoGph_gInf_c::setFadeColor(*(JUtility::TColor*)&g_blackColor); - mDoGph_gInf_c::startFadeOut(15); + if (SHOW_TV_SETTINGS_SCREEN) { + mWaitTimer = 15; + mDoGph_gInf_c::setFadeColor(*(JUtility::TColor*)&g_blackColor); + mDoGph_gInf_c::startFadeOut(15); + } else { + mWaitTimer = 1; + } + mProc = dScnName_PROC_FileSelectClose; field_0x420 = 1; + + if (!SHOW_TV_SETTINGS_SCREEN) { + mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); + } + break; } } @@ -308,12 +328,17 @@ void dScnName_c::FileSelectClose() { mWaitTimer--; if (mWaitTimer == 0) { - mProc = dScnName_PROC_BrightCheckOpen; - mWaitTimer = 15; - mDrawProc = 1; - mDoGph_gInf_c::setFadeColor(*(JUtility::TColor*)&g_blackColor); - mDoGph_gInf_c::startFadeIn(15); - field_0x420 = 0; + if (SHOW_TV_SETTINGS_SCREEN) { + mProc = dScnName_PROC_BrightCheckOpen; + mWaitTimer = 15; + mDrawProc = 1; + mDoGph_gInf_c::setFadeColor(*(JUtility::TColor*)&g_blackColor); + mDoGph_gInf_c::startFadeIn(15); + field_0x420 = 0; + } else { + doPreLoadSetup(); + field_0x420 = 0; + } } } @@ -330,24 +355,28 @@ void dScnName_c::brightCheck() { mBrightCheck->_move(); if (mBrightCheck->isEnd()) { - dComIfGs_setSaveTotalTime(dComIfGs_getTotalTime()); - dComIfGs_setSaveStartTime(OSGetTime()); - mDoAud_bgmStop(45); - - field_0x41f = 0; - mProc = dScnName_PROC_ChangeGameScene; - - // Reset rupee "first-time collection" flags so the collection cutscene will play again - dComIfGs_offItemFirstBit(dItemNo_GREEN_RUPEE_e); - dComIfGs_offItemFirstBit(dItemNo_BLUE_RUPEE_e); - dComIfGs_offItemFirstBit(dItemNo_YELLOW_RUPEE_e); - dComIfGs_offItemFirstBit(dItemNo_RED_RUPEE_e); - dComIfGs_offItemFirstBit(dItemNo_PURPLE_RUPEE_e); - dComIfGs_offItemFirstBit(dItemNo_ORANGE_RUPEE_e); - dComIfGs_offItemFirstBit(dItemNo_SILVER_RUPEE_e); + doPreLoadSetup(); } } +void dScnName_c::doPreLoadSetup() { + dComIfGs_setSaveTotalTime(dComIfGs_getTotalTime()); + dComIfGs_setSaveStartTime(OSGetTime()); + mDoAud_bgmStop(45); + + field_0x41f = 0; + mProc = dScnName_PROC_ChangeGameScene; + + // Reset rupee "first-time collection" flags so the collection cutscene will play again + dComIfGs_offItemFirstBit(dItemNo_GREEN_RUPEE_e); + dComIfGs_offItemFirstBit(dItemNo_BLUE_RUPEE_e); + dComIfGs_offItemFirstBit(dItemNo_YELLOW_RUPEE_e); + dComIfGs_offItemFirstBit(dItemNo_RED_RUPEE_e); + dComIfGs_offItemFirstBit(dItemNo_PURPLE_RUPEE_e); + dComIfGs_offItemFirstBit(dItemNo_ORANGE_RUPEE_e); + dComIfGs_offItemFirstBit(dItemNo_SILVER_RUPEE_e); +} + void dScnName_c::changeGameScene() { if (!mDoRst::isReset() && !fopOvlpM_IsPeek()) { dComIfGs_gameStart(); diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index eaf33a1908..eed938de69 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -11,6 +11,10 @@ namespace dusk { if (ImGui::BeginMenu("Enhancements")) { if (ImGui::BeginMenu("Quality of Life")) { ImGui::Checkbox("Quick Transform (R+Y)", &getSettings().game.enableQuickTransform); + ImGui::Checkbox("Hide TV Settings Screen", &getSettings().game.hideTvSettingsScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hides the TV calibration screen shown when loading a save"); + } ImGui::EndMenu(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 9a5bb20c8c..d1ba67b585 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -23,6 +23,7 @@ UserSettings g_userSettings = { .game = { // Quality of Life .enableQuickTransform = false, + .hideTvSettingsScreen = false, // Preferences .enableMirrorMode = false, From 889129cacfa01a087ec4218d1259607acf482e56 Mon Sep 17 00:00:00 2001 From: madeline Date: Fri, 3 Apr 2026 21:51:04 -0700 Subject: [PATCH 09/18] do resampling manually --- src/dusk/audio/DuskDsp.cpp | 176 ++++++++++++++++------------------ src/dusk/audio/DuskDsp.hpp | 13 ++- src/dusk/imgui/ImGuiAudio.cpp | 7 +- 3 files changed, 98 insertions(+), 98 deletions(-) diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 7b9401d394..7cf1c44cd8 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -74,19 +74,6 @@ constexpr static int PitchToSampleRate(u16 value) { return static_cast(static_cast(SampleRate) * value / 4096); } -static void UpdateSampleRate(const JASDsp::TChannel& channel, ChannelAuxData& aux) { - auto sampleRate = PitchToSampleRate(channel.mPitch); - - const SDL_AudioSpec spec = { - SDL_AUDIO_S16, - 1, - sampleRate - }; - - SDL_SetAudioStreamFormat(aux.resampleStream, &spec, nullptr); - aux.prevPitch = channel.mPitch; -} - /** * Reset state for a DSP channel between independent playbacks. */ @@ -98,8 +85,9 @@ static void ResetChannel(JASDsp::TChannel& channel, ChannelAuxData& aux) { aux.hist0 = 0; aux.hist1 = 0; - SDL_ClearAudioStream(aux.resampleStream); - UpdateSampleRate(channel, aux); + aux.decodeBufCount = 0; + aux.resamplePos = 0.0; + aux.resamplePrev = 0; for (auto& volume : aux.prevVolume) { volume = NAN; @@ -196,15 +184,16 @@ static void ReadSampleData( } /** - * Read a single *contiguous* chunk of sample data from a channel, - * writes the samples to the channel's resampler stream. + * Read a single *contiguous* chunk of sample data from a channel into outBuf * - * @returns Amount of samples actually read. Can be greater than the amount requested. + * @returns Amount of samples written to outBuf. May be less than desiredSamples */ static int ReadChannelSamplesChunk( JASDsp::TChannel& channel, ChannelAuxData& aux, - int desiredSamples) { + int desiredSamples, + s16* outBuf, + int outBufSize) { assert(desiredSamples >= 0); @@ -249,61 +238,49 @@ static int ReadChannelSamplesChunk( channel.mSamplesLeft -= renderSamples; channel.mSamplePosition += renderSamples; - SDL_PutAudioStreamData( - aux.resampleStream, - renderData + skipSamples, - static_cast(renderSize - skipSamples * sizeof(u16))); + int outputCount = static_cast(renderSamples - skipSamples); + + // this should never be hit with the limits on pitch shift (i think) but just in case!! + outputCount = std::min(outputCount, outBufSize); + if (outputCount > 0) { + memcpy(outBuf, renderData + skipSamples, outputCount * sizeof(s16)); + } assert(curSamplePosition % channel.mSamplesPerBlock == 0 || channel.mSamplesLeft == 0); - return static_cast(renderSamples - skipSamples); + return outputCount; } /** - * Reads new audio channels from a DSP channel and writes them to the resampler stream. + * Fill decodeBuf with at least `needed` samples, fewer may be written if the channel has no loop and its data ends */ -static void SDLCALL ReadChannelSamples( - void *userdata, - SDL_AudioStream*, - int additional_amount, - int) { - - if (additional_amount == 0) { - return; - } - - const auto index = static_cast(reinterpret_cast(userdata)); - auto& channel = JASDsp::CH_BUF[index]; - auto& aux = ChannelAux[index]; - - if (channel.mSamplesLeft == 0 && !channel.mLoopFlag) { - // May get called when we're out of data to read. - // This is expected, as we need to drain the resampler channel before we mark the channel as finished. - return; - } - - auto samplesRead = ReadChannelSamplesChunk(channel, aux, additional_amount); - additional_amount -= samplesRead; - - if (channel.mSamplesLeft == 0) { - // Reached end of buffer. - if (!channel.mLoopFlag) { - return; +static void FillDecodeBuf(JASDsp::TChannel& channel, ChannelAuxData& aux, int needed) { + while (aux.decodeBufCount < needed) { + if (channel.mSamplesLeft == 0) { + if (!channel.mLoopFlag) { + // we aren't a looping channel and there's no samples left, we out of this fuckin loop + break; + } else { + // we are looping, handle loop logic + channel.mSamplesLeft = channel.mEndSample - channel.mLoopStartSample; + channel.mSamplePosition = channel.mLoopStartSample; + aux.hist1 = channel.mpPenult; + aux.hist0 = channel.mpLast; + } } - channel.mSamplesLeft = channel.mEndSample - channel.mLoopStartSample; - channel.mSamplePosition = channel.mLoopStartSample; + int remainingDecodeSpace = ChannelAuxData::DECODE_BUF_SIZE - aux.decodeBufCount; + if (remainingDecodeSpace == 0) { + break; + } - aux.hist1 = channel.mpPenult; - aux.hist0 = channel.mpLast; + aux.decodeBufCount += ReadChannelSamplesChunk( + channel, aux, std::min(remainingDecodeSpace, needed - aux.decodeBufCount), + aux.decodeBuf + aux.decodeBufCount, remainingDecodeSpace + ); } - if (additional_amount >= 0) { - ReadChannelSamplesChunk(channel, aux, additional_amount); - } - - channel.mAramStreamPosition = channel.mWaveAramAddress - + ConvertSamplesToDataLength(channel, channel.mSamplePosition); + channel.mAramStreamPosition = channel.mWaveAramAddress + ConvertSamplesToDataLength(channel, channel.mSamplePosition); } /** @@ -422,57 +399,70 @@ static void RenderOutputChannel( prevVolume = targetVolume; } +/** + * Fetch, decode, resample, output + */ static void RenderChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, OutputSubframe& subframe) { + if (channel.mResetFlag) { ResetChannel(channel, channelAux); - } else if (channelAux.prevPitch != channel.mPitch) { - UpdateSampleRate(channel, channelAux); } - DspSubframe audioLoadBuffer = {}; + // how many input samples we step per output sample, aka the resampling ratio + f32 step = (f32)PitchToSampleRate(channel.mPitch) / SampleRate; - int wantRead = sizeof(audioLoadBuffer); - auto read = SDL_GetAudioStreamData( - channelAux.resampleStream, - &audioLoadBuffer, - wantRead); + // how many input samples to resample to DSP_SUBFRAME_SIZE output samples + int needed = static_cast(channelAux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2; - if (read < wantRead) { + FillDecodeBuf(channel, channelAux, needed); + + // source ran dry, channel is finished + if(channelAux.decodeBufCount < needed) { channel.mIsFinished = true; } - auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, wantRead / sizeof(f32)); + DspSubframe audioLoadBuffer = {}; + f64 pos = channelAux.resamplePos; + s16 prev = channelAux.resamplePrev; + s16 next = channelAux.decodeBufCount > 0 ? channelAux.decodeBuf[0] : prev; + int srcIdx = 0; + + // linear resampling and f32 conversion + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + audioLoadBuffer[i] = static_cast(prev + pos * (next - prev)) / 32768.0f; + pos += step; + while (pos >= 1.0) { + pos -= 1.0; + prev = next; + srcIdx++; + next = srcIdx < channelAux.decodeBufCount ? channelAux.decodeBuf[srcIdx] : prev; + } + } + + // save resampler state for the next subframe, prevents popping on pitch change + channelAux.resamplePos = pos; + channelAux.resamplePrev = prev; + + // move any remaining samples in the decode buf to the beginning + int remainingDecodeBuf = channelAux.decodeBufCount - srcIdx; + if (remainingDecodeBuf > 0) { + memmove(channelAux.decodeBuf, channelAux.decodeBuf + srcIdx, remainingDecodeBuf * sizeof(s16)); + } + + channelAux.decodeBufCount = std::max(0, remainingDecodeBuf); + + auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, DSP_SUBFRAME_SIZE); static_assert(OutputSubframe::NUM_CHANNELS == 2, "Keep RenderChannel in sync!"); RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, hasReadSamples, subframe); - RenderOutputChannel(channel, channelAux,OutputChannel::RIGHT, hasReadSamples, subframe); + RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, hasReadSamples, subframe); } void dusk::audio::DspInit() { - constexpr SDL_AudioSpec srcSpec = { - SDL_AUDIO_S16, - 1, - SampleRate - }; - constexpr SDL_AudioSpec dstSpec = { - SDL_AUDIO_F32, - 1, - SampleRate - }; - - for (u32 i = 0; i < DSP_CHANNELS; i++) { - auto& aux = ChannelAux[i]; - aux.resampleStream = SDL_CreateAudioStream(&srcSpec, &dstSpec); - - SDL_SetAudioStreamGetCallback( - aux.resampleStream, - ReadChannelSamples, - reinterpret_cast(static_cast(i))); - } } void dusk::audio::ApplyVolume( diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index c84bb338db..35b6ccabc9 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -26,8 +26,6 @@ namespace dusk::audio { struct ChannelAuxData { s16 hist1; s16 hist0; - SDL_AudioStream* resampleStream; - u16 prevPitch; // Used for debugging tools. u32 resetCount; @@ -43,6 +41,17 @@ namespace dusk::audio { assert(channel < OutputChannel::OutputChannel_MAX); return prevVolume[static_cast(channel)]; } + + // buffer for decoding before resampling, size is chosen based on how many input samples we would need to fetch for the highest possible pitch + // to fill one subframe of output samples after resampling + static constexpr int DECODE_BUF_SIZE = 2048; + s16 decodeBuf[DECODE_BUF_SIZE]; + int decodeBufCount; + + // basically stores our position between resamplePrev and decodeBuf[0] so we don't lose that fractional resampler position next subframe + f32 resamplePos; + // last consumed sample from decodeBuf + s16 resamplePrev; }; extern ChannelAuxData ChannelAux[DSP_CHANNELS]; diff --git a/src/dusk/imgui/ImGuiAudio.cpp b/src/dusk/imgui/ImGuiAudio.cpp index 707091b158..ae01f119f4 100644 --- a/src/dusk/imgui/ImGuiAudio.cpp +++ b/src/dusk/imgui/ImGuiAudio.cpp @@ -50,9 +50,10 @@ static void DisplayDspChannel(int i) { auto dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.5f; auto fxMix = (channel.mAutoMixerFxMix >> 8) / 127.5f; auto volume = VolumeFromU16(channel.mAutoMixerVolume); + auto pitch = channel.mPitch / 4096.0f; ImGui::Text( - "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f)", - pan, dolby, fxMix, volume); + "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f, pitch %f)", + pan, dolby, fxMix, volume, pitch); } else { ImGui::Text( "Bus connect: %04X(%.2f),%04X(%.2f),%04X(%.2f),%04X(%.2f),%04X(%.2f),%04X(%.2f)", @@ -249,4 +250,4 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() { } ImGui::End(); -} \ No newline at end of file +} From 5bb36dfb20b2a2b2e5d773ee64478ac9f8b96b7c Mon Sep 17 00:00:00 2001 From: Max Roncace Date: Sat, 4 Apr 2026 01:30:24 -0400 Subject: [PATCH 10/18] Fix crashes when entering Henna's shop --- .../JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h | 4 ++-- .../JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp | 4 ++-- src/d/actor/d_a_npc_henna.cpp | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h index 6b70201532..7df974b854 100644 --- a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h +++ b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h @@ -82,7 +82,7 @@ public: /* 0x00 */ u16 mMaterialNum; /* 0x04 */ J3DMaterialInitData_v21* mpMaterialInitData; - /* 0x08 */ u16* mpMaterialID; + /* 0x08 */ BE(u16)* mpMaterialID; /* 0x0C */ GXColor* mpMatColor; /* 0x10 */ u8* mpColorChanNum; /* 0x14 */ J3DColorChanInfo* mpColorChanInfo; @@ -91,7 +91,7 @@ public: /* 0x20 */ J3DTexCoord2Info* mpTexCoord2Info; /* 0x24 */ J3DTexMtxInfo* mpTexMtxInfo; /* 0x28 */ J3DTexMtxInfo* field_0x28; - /* 0x2C */ u16* mpTexNo; + /* 0x2C */ BE(u16)* mpTexNo; /* 0x30 */ GXCullMode* mpCullMode; /* 0x34 */ J3DTevOrderInfo* mpTevOrderInfo; /* 0x38 */ GXColorS10* mpTevColor; diff --git a/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp b/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp index 229309545a..5cf98498ff 100644 --- a/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp +++ b/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp @@ -10,7 +10,7 @@ J3DMaterialFactory_v21::J3DMaterialFactory_v21(J3DMaterialBlock_v21 const& i_block) { mMaterialNum = i_block.mMaterialNum; mpMaterialInitData = JSUConvertOffsetToPtr(&i_block, i_block.mpMaterialInitData); - mpMaterialID = JSUConvertOffsetToPtr(&i_block, i_block.mpMaterialID); + mpMaterialID = JSUConvertOffsetToPtr(&i_block, i_block.mpMaterialID); mpCullMode = JSUConvertOffsetToPtr(&i_block, i_block.mpCullMode); mpMatColor = JSUConvertOffsetToPtr(&i_block, i_block.mpMatColor); mpColorChanNum = JSUConvertOffsetToPtr(&i_block, i_block.mpColorChanNum); @@ -20,7 +20,7 @@ J3DMaterialFactory_v21::J3DMaterialFactory_v21(J3DMaterialBlock_v21 const& i_blo mpTexCoord2Info = JSUConvertOffsetToPtr(&i_block, i_block.mpTexCoord2Info); mpTexMtxInfo = JSUConvertOffsetToPtr(&i_block, i_block.mpTexMtxInfo); field_0x28 = JSUConvertOffsetToPtr(&i_block, i_block.field_0x38); - mpTexNo = JSUConvertOffsetToPtr(&i_block, i_block.mpTexNo); + mpTexNo = JSUConvertOffsetToPtr(&i_block, i_block.mpTexNo); mpTevOrderInfo = JSUConvertOffsetToPtr(&i_block, i_block.mpTevOrderInfo); mpTevColor = JSUConvertOffsetToPtr(&i_block, i_block.mpTevColor); mpTevKColor = JSUConvertOffsetToPtr(&i_block, i_block.mpTevKColor); diff --git a/src/d/actor/d_a_npc_henna.cpp b/src/d/actor/d_a_npc_henna.cpp index 59f13ceabe..339d5ee82c 100644 --- a/src/d/actor/d_a_npc_henna.cpp +++ b/src/d/actor/d_a_npc_henna.cpp @@ -2709,6 +2709,14 @@ static int daNpc_Henna_Create(fopAc_ac_c* i_this) { } i_this->attention_info.flags = (fopAc_AttnFlag_SPEAK_e | fopAc_AttnFlag_TALK_e); a_this->action = 0; + +#if AVOID_UB + a_this->field_0x654 = 0; + a_this->field_0x658 = 0; + a_this->field_0x662 = 0; + a_this->field_0x664 = 0; +#endif + fopAcM_SetMtx(i_this, a_this->mpMorf->getModel()->getBaseTRMtx()); lbl_82_bss_90 = 0; if (a_this->arg0 == 1) { From 350e99e8ab8ac9ea1529f08d5116e3d7b7a8db50 Mon Sep 17 00:00:00 2001 From: madeline Date: Fri, 3 Apr 2026 22:33:38 -0700 Subject: [PATCH 11/18] fix getDopplerPower fixes #213 --- libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h | 2 +- src/Z2AudioLib/Z2Audience.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h b/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h index 50f5b45f09..44d4137d9b 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h @@ -10,7 +10,7 @@ */ struct JAUAudibleParam { f32 getDopplerPower() const { - return (u32)((*(u8*)&field_0x0.raw >> 4) & 0xf) * (1.0f / 15.0f); + return field_0x0.bytes.b0_0 * (1.0f / 15.0f); } union { diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp index 9adab4084f..5bb91eef31 100644 --- a/src/Z2AudioLib/Z2Audience.cpp +++ b/src/Z2AudioLib/Z2Audience.cpp @@ -802,7 +802,7 @@ f32 Z2Audience::calcFxMix_(f32 param_0, int distVolBit) const { f32 Z2Audience::calcPitch_(Z2AudibleChannel* channel, const Z2Audible* audible, const Z2AudioCamera* camera) const { JAUAudibleParam audParam = *audible->getAudibleParam(); - if ((*(u8*)&audParam.field_0x0.raw >> 4) & 0xf) { + if (audParam.field_0x0.bytes.b0_0) { JGeometry::TVec3 aTStack_4c; aTStack_4c.normalize(channel->field_0x14.field_0x00); JAUAudibleParam audParam = *audible->getAudibleParam(); From 479d444066b3da7f442cbddaa35d7cf41cef0d08 Mon Sep 17 00:00:00 2001 From: Max Roncace Date: Sat, 4 Apr 2026 01:36:16 -0400 Subject: [PATCH 12/18] Fix a couple more BE issues in J3DMaterialFactory_v21 --- .../include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h | 4 ++-- libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h index 7df974b854..93d4fb9b7d 100644 --- a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h +++ b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DMaterialFactory_v21.h @@ -92,9 +92,9 @@ public: /* 0x24 */ J3DTexMtxInfo* mpTexMtxInfo; /* 0x28 */ J3DTexMtxInfo* field_0x28; /* 0x2C */ BE(u16)* mpTexNo; - /* 0x30 */ GXCullMode* mpCullMode; + /* 0x30 */ BE(GXCullMode)* mpCullMode; /* 0x34 */ J3DTevOrderInfo* mpTevOrderInfo; - /* 0x38 */ GXColorS10* mpTevColor; + /* 0x38 */ BE(GXColorS10)* mpTevColor; /* 0x3C */ GXColor* mpTevKColor; /* 0x40 */ u8* mpTevStageNum; /* 0x44 */ J3DTevStageInfo* mpTevStageInfo; diff --git a/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp b/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp index 5cf98498ff..2566708f63 100644 --- a/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp +++ b/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp @@ -11,7 +11,7 @@ J3DMaterialFactory_v21::J3DMaterialFactory_v21(J3DMaterialBlock_v21 const& i_blo mMaterialNum = i_block.mMaterialNum; mpMaterialInitData = JSUConvertOffsetToPtr(&i_block, i_block.mpMaterialInitData); mpMaterialID = JSUConvertOffsetToPtr(&i_block, i_block.mpMaterialID); - mpCullMode = JSUConvertOffsetToPtr(&i_block, i_block.mpCullMode); + mpCullMode = JSUConvertOffsetToPtr(&i_block, i_block.mpCullMode); mpMatColor = JSUConvertOffsetToPtr(&i_block, i_block.mpMatColor); mpColorChanNum = JSUConvertOffsetToPtr(&i_block, i_block.mpColorChanNum); mpColorChanInfo = JSUConvertOffsetToPtr(&i_block, i_block.mpColorChanInfo); @@ -22,7 +22,7 @@ J3DMaterialFactory_v21::J3DMaterialFactory_v21(J3DMaterialBlock_v21 const& i_blo field_0x28 = JSUConvertOffsetToPtr(&i_block, i_block.field_0x38); mpTexNo = JSUConvertOffsetToPtr(&i_block, i_block.mpTexNo); mpTevOrderInfo = JSUConvertOffsetToPtr(&i_block, i_block.mpTevOrderInfo); - mpTevColor = JSUConvertOffsetToPtr(&i_block, i_block.mpTevColor); + mpTevColor = JSUConvertOffsetToPtr(&i_block, i_block.mpTevColor); mpTevKColor = JSUConvertOffsetToPtr(&i_block, i_block.mpTevKColor); mpTevStageNum = JSUConvertOffsetToPtr(&i_block, i_block.mpTevStageNum); mpTevStageInfo = JSUConvertOffsetToPtr(&i_block, i_block.mpTevStageInfo); From 932123bedbc28a793b551d3eacd8b5f23778bb63 Mon Sep 17 00:00:00 2001 From: Max Roncace Date: Sat, 4 Apr 2026 01:41:04 -0400 Subject: [PATCH 13/18] Fix compile error in J3DMaterialFactory_v21 --- libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp b/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp index 2566708f63..f6cca25525 100644 --- a/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp +++ b/libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp @@ -260,7 +260,7 @@ J3DGXColorS10 J3DMaterialFactory_v21::newTevColor(int i_idx, int i_no) const { J3DGXColorS10 dflt = defaultTevColor; J3DMaterialInitData_v21* mtl_init_data = &mpMaterialInitData[mpMaterialID[i_idx]]; if (mtl_init_data->mTevColorIdx[i_no] != 0xffff) { - return mpTevColor[mtl_init_data->mTevColorIdx[i_no]]; + return (GXColorS10)mpTevColor[mtl_init_data->mTevColorIdx[i_no]]; } else { return dflt; } From c94976cd412090d2474a6dab01c9d662694f4bc5 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sat, 4 Apr 2026 01:52:48 -0400 Subject: [PATCH 14/18] Remove AuroraConfig configPath --- src/m_Do/m_Do_main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 185ca8543c..b8eeba892e 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -272,7 +272,6 @@ int game_main(int argc, char* argv[]) { config.windowWidth = 608 * 2; config.windowHeight = 448 * 2; config.desiredBackend = ParseAuroraBackend(parsed_arg_options["backend"].as()); - config.configPath = "."; config.logCallback = &aurora_log_callback; config.logLevel = (AuroraLogLevel)parsed_arg_options["log-level"].as(); config.mem1Size = 256 * 1024 * 1024; From 7d41421702addb242bf5154bd1cf895bc3eb6b5a Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 4 Apr 2026 02:37:59 -0700 Subject: [PATCH 15/18] couple of hd qol toggles --- extern/aurora | 2 +- include/dusk/settings.h | 3 +++ src/d/actor/d_a_alink_demo.inc | 8 ++++++++ src/d/d_s_name.cpp | 7 +++++++ src/d/d_save.cpp | 14 ++++++++++++++ src/dusk/imgui/ImGuiMenuEnhancements.cpp | 16 ++++++++++++++++ src/dusk/settings.cpp | 3 +++ 7 files changed, 52 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 37631c3bf3..b17da31593 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 37631c3bf382c1564d3f29c4c3f2dc6cf7617fa9 +Subproject commit b17da315932b85d7d708eaf3e44914f70045a44e diff --git a/include/dusk/settings.h b/include/dusk/settings.h index db49560fff..fddcd3b0f0 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -28,6 +28,9 @@ struct UserSettings { // QoL bool enableQuickTransform; bool hideTvSettingsScreen; + bool biggerWallets; + bool noReturnRupees; + bool disableRupeeCutscenes; // Preferences bool enableMirrorMode; diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index 39387582cb..aa77b24129 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,6 +23,8 @@ #include "d/actor/d_a_npc_tkc.h" #include +#include "dusk/settings.h" + BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); } @@ -2230,6 +2232,12 @@ void daAlink_c::setGetSubBgm(int i_itemNo) { } BOOL daAlink_c::checkTreasureRupeeReturn(int i_itemNo) const { + #if TARGET_PC + if (dusk::getSettings().game.noReturnRupees) { + return FALSE; + } + #endif + static const int dummy = 0; if (i_itemNo == dItemNo_LINKS_SAVINGS_e) { diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index e001d97c71..735ce4d0ca 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -17,6 +17,7 @@ #include "m_Do/m_Do_main.h" #include "f_op/f_op_overlap_mng.h" #include "dusk/memory.h" +#include "dusk/settings.h" #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -367,6 +368,12 @@ void dScnName_c::doPreLoadSetup() { field_0x41f = 0; mProc = dScnName_PROC_ChangeGameScene; + #if TARGET_PC + if (dusk::getSettings().game.disableRupeeCutscenes) { + return; + } + #endif + // Reset rupee "first-time collection" flags so the collection cutscene will play again dComIfGs_offItemFirstBit(dItemNo_GREEN_RUPEE_e); dComIfGs_offItemFirstBit(dItemNo_BLUE_RUPEE_e); diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index 06b92994db..3289a49c64 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -25,6 +25,8 @@ #include "lingcod/lingcod.h" #endif +#include "dusk/settings.h" + static u8 dSv_item_rename(u8 i_itemNo) { switch (i_itemNo) { case dItemNo_OIL_BOTTLE_2_e: @@ -111,11 +113,23 @@ u16 dSv_player_status_a_c::getRupeeMax() const { if (mWalletSize < 3) { // if you make this a default, it wont match. Compiler, pls. switch (mWalletSize) { case WALLET: + #if TARGET_PC + return dusk::getSettings().game.biggerWallets ? 500 : 300; + #else return 300; + #endif case BIG_WALLET: + #if TARGET_PC + return dusk::getSettings().game.biggerWallets ? 1000 : 600; + #else return 600; + #endif case GIANT_WALLET: + #if TARGET_PC + return dusk::getSettings().game.biggerWallets ? 2000 : 1000; + #else return 1000; + #endif } } diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index eed938de69..8ed7b1d8c4 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -11,6 +11,22 @@ namespace dusk { if (ImGui::BeginMenu("Enhancements")) { if (ImGui::BeginMenu("Quality of Life")) { ImGui::Checkbox("Quick Transform (R+Y)", &getSettings().game.enableQuickTransform); + + ImGui::Checkbox("Bigger Wallets", &getSettings().game.biggerWallets); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Wallet sizes are like in the HD version (500, 1000, 2000)"); + } + + ImGui::Checkbox("No Rupee Returns", &getSettings().game.noReturnRupees); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full"); + } + + ImGui::Checkbox("Disable Rupee Cutscenes", &getSettings().game.disableRupeeCutscenes); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time"); + } + ImGui::Checkbox("Hide TV Settings Screen", &getSettings().game.hideTvSettingsScreen); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Hides the TV calibration screen shown when loading a save"); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index d1ba67b585..734188987a 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -24,6 +24,9 @@ UserSettings g_userSettings = { // Quality of Life .enableQuickTransform = false, .hideTvSettingsScreen = false, + .biggerWallets = false, + .noReturnRupees = false, + .disableRupeeCutscenes = false, // Preferences .enableMirrorMode = false, From 0d1e680ddd8bf8b41f963e20ce91c6bfe7c5c770 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 4 Apr 2026 02:59:53 -0700 Subject: [PATCH 16/18] no sword recoil enhancement --- include/dusk/settings.h | 1 + src/d/actor/d_a_alink_cut.inc | 8 ++++++++ src/dusk/imgui/ImGuiMenuEnhancements.cpp | 5 +++++ src/dusk/settings.cpp | 1 + 4 files changed, 15 insertions(+) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index fddcd3b0f0..cd9a1e0cf0 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -31,6 +31,7 @@ struct UserSettings { bool biggerWallets; bool noReturnRupees; bool disableRupeeCutscenes; + bool noSwordRecoil; // Preferences bool enableMirrorMode; diff --git a/src/d/actor/d_a_alink_cut.inc b/src/d/actor/d_a_alink_cut.inc index 4e9dd18aee..8e61a262cb 100644 --- a/src/d/actor/d_a_alink_cut.inc +++ b/src/d/actor/d_a_alink_cut.inc @@ -7,6 +7,8 @@ #include "d/actor/d_a_b_gnd.h" #include "SSystem/SComponent/c_math.h" +#include "dusk/settings.h" + enum daAlink_CutNmParamType { CUT_NM_PARAM_VERTICAL, CUT_NM_PARAM_LEFT, @@ -369,6 +371,12 @@ BOOL daAlink_c::changeCutReverseProc(daAlink_c::daAlink_ANM i_anmID) { return procCutReverseInit(i_anmID); } + #if TARGET_PC + if (dusk::getSettings().game.noSwordRecoil) { + return FALSE; + } + #endif + if (checkNoResetFlg0(FLG0_CUT_AT_FLG) || mEquipItem == dItemNo_COPY_ROD_e) { cXyz sp28; Vec sp1C; diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index 8ed7b1d8c4..d0285789cd 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -27,6 +27,11 @@ namespace dusk { ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time"); } + ImGui::Checkbox("No Sword Recoil", &getSettings().game.noSwordRecoil); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Link won't recoil when his sword hits walls"); + } + ImGui::Checkbox("Hide TV Settings Screen", &getSettings().game.hideTvSettingsScreen); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Hides the TV calibration screen shown when loading a save"); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 734188987a..7e658f6e5c 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -27,6 +27,7 @@ UserSettings g_userSettings = { .biggerWallets = false, .noReturnRupees = false, .disableRupeeCutscenes = false, + .noSwordRecoil = false, // Preferences .enableMirrorMode = false, From b45af183744f407d71231f5f7faf03e74b3d90f8 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 4 Apr 2026 03:52:28 -0700 Subject: [PATCH 17/18] quick climbing qol, difficulty options --- include/dusk/settings.h | 3 ++ src/d/actor/d_a_alink_damage.inc | 7 +++ src/d/actor/d_a_alink_hang.inc | 68 ++++++++++++++++++++---- src/dusk/imgui/ImGuiMenuEnhancements.cpp | 15 ++++++ src/dusk/settings.cpp | 3 ++ 5 files changed, 85 insertions(+), 11 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index cd9a1e0cf0..b822a2ffbb 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -32,6 +32,9 @@ struct UserSettings { bool noReturnRupees; bool disableRupeeCutscenes; bool noSwordRecoil; + int damageMultiplier; + bool instantDeath; + bool fastClimbing; // Preferences bool enableMirrorMode; diff --git a/src/d/actor/d_a_alink_damage.inc b/src/d/actor/d_a_alink_damage.inc index 122bf8f248..420011cfd6 100644 --- a/src/d/actor/d_a_alink_damage.inc +++ b/src/d/actor/d_a_alink_damage.inc @@ -152,6 +152,13 @@ f32 daAlink_c::damageMagnification(BOOL i_checkZoraMag, int param_1) { base_mag = 1.0f; } + #if TARGET_PC + base_mag *= dusk::getSettings().game.damageMultiplier; + if (dusk::getSettings().game.instantDeath) { + base_mag = 9999.0f; + } + #endif + if (checkWolf() && !checkCargoCarry() && param_1 == 0) { return 2.0f * base_mag; } diff --git a/src/d/actor/d_a_alink_hang.inc b/src/d/actor/d_a_alink_hang.inc index b99fe71c85..a98020ce4f 100644 --- a/src/d/actor/d_a_alink_hang.inc +++ b/src/d/actor/d_a_alink_hang.inc @@ -13,8 +13,15 @@ #include "d/actor/d_a_obj_swhang.h" f32 daAlink_c::getHangMoveAnmSpeed() { + #if TARGET_PC + return getAnmSpeedStickRate( + dusk::getSettings().game.fastClimbing ? 2.0f : mpHIO->mWallHang.mWallMove.m.mMinAnmSpeed, + dusk::getSettings().game.fastClimbing ? 2.0f : mpHIO->mWallHang.mWallMove.m.mMaxAnmSpeed + ); + #else return getAnmSpeedStickRate(mpHIO->mWallHang.mWallMove.m.mMinAnmSpeed, mpHIO->mWallHang.mWallMove.m.mMaxAnmSpeed); + #endif } int daAlink_c::getHangDirectionFromAngle() { @@ -1209,7 +1216,12 @@ void daAlink_c::setLadderPos(int param_0) { f32 daAlink_c::getLadderMoveAnmSpeed() { return getAnmSpeedStickRate(mpHIO->mLadder.m.mMoveMinASpeed, - mpHIO->mLadder.m.mMoveMaxSpeed); + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 1.575f : mpHIO->mLadder.m.mMoveMaxSpeed + #else + mpHIO->mLadder.m.mMoveMaxSpeed + #endif + ); } int daAlink_c::changeLadderMoveProc(int param_0) { @@ -1304,8 +1316,14 @@ int daAlink_c::procLadderUpStartInit() { mNormalSpeed = 0.0f; speedF = 0.0f; - setSingleAnimeBaseSpeed(ANM_LADDER_UP_START, mpHIO->mLadder.m.mClimbUpStartASpeed, - mpHIO->mLadder.m.mClimbUpStartInterp); + setSingleAnimeBaseSpeed(ANM_LADDER_UP_START, + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 1.8f : mpHIO->mLadder.m.mClimbUpStartASpeed, + #else + mpHIO->mLadder.m.mClimbUpStartASpeed, + #endif + mpHIO->mLadder.m.mClimbUpStartInterp); + field_0x2f99 = 0x10; field_0x3588 = l_waitBaseAnime; dComIfGp_setPlayerStatus0(0, 0x2000000); @@ -1354,8 +1372,14 @@ int daAlink_c::procLadderUpEndInit(BOOL param_0) { anm_id = ANM_LADDER_UP_END_RIGHT; } - setSingleAnimeBaseSpeed(anm_id, mpHIO->mLadder.m.mClimbUpEndASpeed, - mpHIO->mLadder.m.mClimbUpEndInterp); + setSingleAnimeBaseSpeed(anm_id, + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 1.765f : mpHIO->mLadder.m.mClimbUpEndASpeed, + #else + mpHIO->mLadder.m.mClimbUpEndASpeed, + #endif + mpHIO->mLadder.m.mClimbUpEndInterp); + field_0x2f99 = 14; setSpecialGravity(0.0f, maxFallSpeed, 0); @@ -1413,8 +1437,14 @@ int daAlink_c::procLadderDownStartInit() { shape_angle.y = field_0x306e + 0x8000; current.angle.y = field_0x306e; - setSingleAnimeBaseSpeed(ANM_LADDER_DOWN_START, mpHIO->mLadder.m.mClimbDownStartASpeed, - mpHIO->mLadder.m.mClimbDownStartInterp); + setSingleAnimeBaseSpeed(ANM_LADDER_DOWN_START, + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 1.8f : mpHIO->mLadder.m.mClimbDownStartASpeed, + #else + mpHIO->mLadder.m.mClimbDownStartASpeed, + #endif + mpHIO->mLadder.m.mClimbDownStartInterp); + field_0x2f99 = 0x10; field_0x3588 = l_waitBaseAnime; dComIfGp_setPlayerStatus0(0, 0x2000000); @@ -1469,8 +1499,14 @@ int daAlink_c::procLadderDownEndInit(BOOL param_0) { anm_id = ANM_LADDER_DOWN_END_RIGHT; } - setSingleAnimeBaseSpeed(anm_id, mpHIO->mLadder.m.mClimbDownEndASpeed, - mpHIO->mLadder.m.mClimbDownEndInterp); + setSingleAnimeBaseSpeed(anm_id, + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 1.8f : mpHIO->mLadder.m.mClimbDownEndASpeed, + #else + mpHIO->mLadder.m.mClimbDownEndASpeed, + #endif + mpHIO->mLadder.m.mClimbDownEndInterp); + field_0x2f99 = 14; setSpecialGravity(0.0f, maxFallSpeed, 0); @@ -1602,12 +1638,22 @@ int daAlink_c::procLadderMove() { f32 daAlink_c::getClimbMoveUpDownAnmSpeed() { return getAnmSpeedStickRate(mpHIO->mLadder.m.mWallVerticalMinAnmSpeed, - mpHIO->mLadder.m.mWallVerticalMaxAnmSpeed); + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 1.875f : mpHIO->mLadder.m.mWallVerticalMaxAnmSpeed + #else + mpHIO->mLadder.m.mWallVerticalMaxAnmSpeed + #endif + ); } f32 daAlink_c::getClimbMoveSideAnmSpeed() { return getAnmSpeedStickRate(mpHIO->mLadder.m.mWallHorizontalMinAnmSpeed, - mpHIO->mLadder.m.mWallHorizontalMaxAnmSpeed); + #if TARGET_PC + dusk::getSettings().game.fastClimbing ? 2.0f : mpHIO->mLadder.m.mWallHorizontalMaxAnmSpeed + #else + mpHIO->mLadder.m.mWallHorizontalMaxAnmSpeed + #endif + ); } BOOL daAlink_c::checkClimbCode(cBgS_PolyInfo& i_polyinfo) { diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index d0285789cd..1ccb06e202 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -32,6 +32,11 @@ namespace dusk { ImGui::SetTooltip("Link won't recoil when his sword hits walls"); } + ImGui::Checkbox("Faster Climbing", &getSettings().game.fastClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version"); + } + ImGui::Checkbox("Hide TV Settings Screen", &getSettings().game.hideTvSettingsScreen); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Hides the TV calibration screen shown when loading a save"); @@ -63,6 +68,16 @@ namespace dusk { ImGui::EndMenu(); } + if (ImGui::BeginMenu("Difficulty")) { + ImGui::SliderInt("Damage Multiplier", &getSettings().game.damageMultiplier, 1, 8, "x%d"); + ImGui::Checkbox("Instant Death", &getSettings().game.instantDeath); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Any hit will instantly kill you"); + } + + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Technical")) { ImGui::Checkbox("Restore Wii 1.0 Glitches", &getSettings().game.restoreWiiGlitches); if (ImGui::IsItemHovered()) { diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 7e658f6e5c..a8a4d219a9 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -28,6 +28,9 @@ UserSettings g_userSettings = { .noReturnRupees = false, .disableRupeeCutscenes = false, .noSwordRecoil = false, + .damageMultiplier = 1, + .instantDeath = false, + .fastClimbing = false, // Preferences .enableMirrorMode = false, From 015e7aceab3c5c18de440b3cf64e93b976a4220c Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 4 Apr 2026 06:02:23 -0700 Subject: [PATCH 18/18] hd faster tears toggle --- include/dusk/settings.h | 1 + src/d/actor/d_a_e_ym.cpp | 17 +++++++- src/d/actor/d_a_obj_drop.cpp | 53 ++++++++++++++++++++++++ src/dusk/imgui/ImGuiMenuEnhancements.cpp | 5 +++ src/dusk/imgui/ImGuiSaveEditor.cpp | 2 +- src/dusk/settings.cpp | 1 + 6 files changed, 77 insertions(+), 2 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index b822a2ffbb..582c5a1872 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -35,6 +35,7 @@ struct UserSettings { int damageMultiplier; bool instantDeath; bool fastClimbing; + bool fastTears; // Preferences bool enableMirrorMode; diff --git a/src/d/actor/d_a_e_ym.cpp b/src/d/actor/d_a_e_ym.cpp index a5544b4041..758bec6269 100644 --- a/src/d/actor/d_a_e_ym.cpp +++ b/src/d/actor/d_a_e_ym.cpp @@ -16,6 +16,8 @@ #include "f_op/f_op_camera_mng.h" #include +#include "dusk/settings.h" + class daE_YM_HIO_c: public JORReflexible { public: daE_YM_HIO_c(); @@ -1066,7 +1068,11 @@ void daE_YM_c::executeDown() { } if (mAcch.ChkGroundHit()) { if (mFlyType != 1) { + #if TARGET_PC + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + #else bckSet(6, 0, 0.0f, 1.0f); + #endif } if (mMode == 1) { mSound.startCreatureSound(Z2SE_EN_YM_LAND, 0, -1); @@ -1086,7 +1092,11 @@ void daE_YM_c::executeDown() { if (current.pos.y < gnd_cross) { mSound.startCreatureSound(Z2SE_CM_BODYFALL_WATER_M, 0, -1); mSound.startCreatureSound(Z2SE_EN_YM_MOGAKU, 0, -1); + #if TARGET_PC + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + #else bckSet(6, 0, 0.0f, 1.0f); + #endif speedF = 0.0f; speed.y = gravity = 0.0f; current.pos.y = gnd_cross; @@ -1102,8 +1112,13 @@ void daE_YM_c::executeDown() { f32 gnd_cross_2 = dComIfG_Bgsp().GroundCross(&gnd_chk); if (gnd_cross_2 == -G_CM3D_F_INF || std::abs(gnd_cross_2 - current.pos.y) > 1000.0f || dComIfG_Bgsp().GetGroundCode(gnd_chk) == 4 || dComIfG_Bgsp().GetGroundCode(gnd_chk) == 10 - || dComIfG_Bgsp().GetGroundCode(gnd_chk) == 5) { + || dComIfG_Bgsp().GetGroundCode(gnd_chk) == 5) + { + #if TARGET_PC + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + #else bckSet(6, 0, 0.0f, 1.0f); + #endif mMode = 3; speedF = 0.0f; shape_angle.x = -0x8000; diff --git a/src/d/actor/d_a_obj_drop.cpp b/src/d/actor/d_a_obj_drop.cpp index 64fc1f537d..275a3d6e12 100644 --- a/src/d/actor/d_a_obj_drop.cpp +++ b/src/d/actor/d_a_obj_drop.cpp @@ -20,6 +20,8 @@ #include "d/actor/d_a_e_ymb.h" #include "f_op/f_op_camera_mng.h" +#include "dusk/settings.h" + #if DEBUG daObjDrop_HIO_c l_HIO; #endif @@ -261,8 +263,18 @@ int daObjDrop_c::modeParentWait() { } mModeAction = 1; + +#if TARGET_PC + mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 20 : 40; + if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { + current.pos.y += 100.0f; + } else { + current.pos.y += 300.0f; + } +#else mModeTimer = 40; current.pos.y += 300.0f; +#endif mSound.startSound(Z2SE_OBJ_LIGHTDROP_APPEAR, 0, -1); break; case 1: @@ -272,7 +284,11 @@ int daObjDrop_c::modeParentWait() { break; case 2: createBodyEffect(); +#if TARGET_PC + mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 5 : 45; +#else mModeTimer = 45; +#endif mModeAction = 0; setMode(MODE_WAIT_e); break; @@ -281,6 +297,18 @@ int daObjDrop_c::modeParentWait() { return 1; } +#if TARGET_PC +static inline BOOL checkGetCargoRide() { + if ((daPy_getPlayerActorClass()->checkCargoCarry() && strcmp(dComIfGp_getStartStageName(), "F_SP112") == 0) || + dComIfGs_isLightDropGetFlag(dComIfGp_getStartStageDarkArea())) + { + return true; + } + + return false; +} +#endif + int daObjDrop_c::modeWait() { daPy_py_c* pplayer = daPy_getPlayerActorClass(); @@ -302,7 +330,32 @@ int daObjDrop_c::modeWait() { case 1: case 2: case 50: + #if TARGET_PC + if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { + f32 player_dist = current.pos.abs(daPy_getPlayerActorClass()->current.pos); + f32 home_dist = current.pos.abs(home.pos); + + if (checkGetCargoRide() && player_dist < 1000.0f) { + mTargetPos = pplayer->current.pos; + mTargetPos.y += 100.0f; + + f32 temp = 3000.0f - home_dist; + if (temp < 0.0f) { + temp = 0.0f; + } + + cLib_chaseF(&speedF, (temp / 3000.0f) * 10.0f * (temp / 3000.0f), 1.0f); + } else { + mTargetPos = home.pos; + cLib_chaseF(&speedF, 2.0f, 0.5f); + } + } else { + cLib_chaseF(&speedF, 7.5f, 0.4f); + } + #else cLib_chaseF(&speedF, 7.5f, 0.4f); + #endif + if (mModeAction == 1) { cLib_chasePos(¤t.pos, mTargetPos, speedF); } diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index 1ccb06e202..cf7fcc41d7 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -37,6 +37,11 @@ namespace dusk { ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version"); } + ImGui::Checkbox("Faster Tears of Light", &getSettings().game.fastTears); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version"); + } + ImGui::Checkbox("Hide TV Settings Screen", &getSettings().game.hideTvSettingsScreen); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Hides the TV calibration screen shown when loading a save"); diff --git a/src/dusk/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index ff04b1f1a2..a9cca636f8 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -823,7 +823,7 @@ namespace dusk { for (int e = 0; e < 255; e++) { ImGui::Text("%03d ", e); ImGui::SameLine(); - for (int i = 8; i >= 0; i--) { + for (int i = 7; i >= 0; i--) { bool flag = event.mEvent[e] & (1 << i); if (ImGui::Checkbox(fmt::format("##event{0}{1}", e, i).c_str(), &flag)) { if (flag) diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index a8a4d219a9..ea17eef374 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -31,6 +31,7 @@ UserSettings g_userSettings = { .damageMultiplier = 1, .instantDeath = false, .fastClimbing = false, + .fastTears = false, // Preferences .enableMirrorMode = false,