From e09e15e7e196196c164e9a7a56c0d5222fb35674 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Tue, 25 Aug 2026 11:23:11 -0700 Subject: [PATCH] Gamepad Mapper (#7116) Also one more Torch VS fix. --- CMakeLists.txt | 2 + soh/assets/custom/textures/mapping-xbox.png | Bin 0 -> 79348 bytes soh/soh/Enhancements/controls/Mapper.cpp | 1711 +++++++++++++++++++ soh/soh/Enhancements/controls/Mapper.h | 181 ++ 4 files changed, 1894 insertions(+) create mode 100644 soh/assets/custom/textures/mapping-xbox.png create mode 100644 soh/soh/Enhancements/controls/Mapper.cpp create mode 100644 soh/soh/Enhancements/controls/Mapper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a0a23e16ca..7792172b2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -283,6 +283,8 @@ endfunction() add_subdirectory(torch SYSTEM) quiet_dependency_tree(${CMAKE_CURRENT_SOURCE_DIR}/torch) +target_compile_definitions(torch INTERFACE YAML_CPP_STATIC_DEFINE) + # Build-time ROM extraction. soh links torch as a static library, which compiles out torch's # own CLI, so this supplies an entry point around the same code the game runs. add_executable(soh-torch EXCLUDE_FROM_ALL diff --git a/soh/assets/custom/textures/mapping-xbox.png b/soh/assets/custom/textures/mapping-xbox.png new file mode 100644 index 0000000000000000000000000000000000000000..58db098481033eb933c8d4d8a5e43b06935963de GIT binary patch literal 79348 zcmb@t1yqz<`zStuc_dUoQt;paBHcN}QDR7?yN3>$p&L1hBB4V|Dy4LHDFP0SNSDG8 zQX_-RPy_#s=li~O|M&iGth?^4#o~Quc09G8-p?Ost5W^J{09UAp@OU3*M&eX%0M9J zZ(O^*G}0{-rg zz-S0WPQm|?jh!pP`<5-j(b)sWfopE(xaDjQ<1iA}64H8f58>pj7KlXX1#0Wt1-jZv z+jA($-;(o}0SmYzylrmzySsUyWc*3dp>($4En}?*x9)i& z5x1lTg!%1+goJNNN(k`b4X5tY34uMY><4r%Wo zqkCWFUz>q97>AR$_ahlWK|en~0Y6azPo$%uu(Y(appb~5hzLIz!H){?@V4>i_dwnL z_X_tBC_AL{BX4IZk9?4Bzc;tH6GXTn+z}q$C@@y|KVu&`d3t-IoIL+0OaJHR|KA3Hwpv>M+2g;2 z#ohfsTcEs^eE}N(BFKL^8l@lb2qCD8KzaHg?GVbo0LCAz zsR#cC`Id;7fW)mkM$R7go_;8v|7HT>zKu5m#sRn{!7n7qFCwllEG#1?DI+chUS))Y z{u!#}Y47Y1@ZSxU(iaw$5s{J+2AuwHh60(fxAC_5-ww97lX37wy4wJRo!xC55rU6A z964_Nhm11!Jl#BzU}FHC=x->rv}E8OC~q4NI|Tebj04~-;OuNKVd%iuGFK#1YD}_Kv@gv0Th4>MY zVmAEJk_ZQWI~yTkX(0zG8zCw2f6v!JI)kHSVKi zPj1e)eoKLjjoq1a!#M2D*ov^{_}ACY{{;p9&oKCBSwANPSoD9v?tg}%JRQ9KY>)^g zM?kIrtvCt(&wxkS`2Md!M@Wc@*(0PS`4I@fK_Q7V=1GZ2@=J*dNr{SziQ9=u+WZ^! z{}FT%nKQvV6WrgD`hNrZzsPFmWaHt80J>6;@rhr9elna+Zfs{CHK=&U>p9=0K3@<%HYRobYni8y!u`qeC| zUq?Q;y({|!C|G{Z<^tInR zq&|*Zym66q`f85iq`sl+`n9#qFl8aNs@K@wj1z(bnx!+^-ss6c5K>ZoiJMgB2px%M zaq*k**>qB_$g-EkxZVuH6(+Pum$dlmLw_$i%o0gA!{x!%D2{n?Gw8IQTRH`6`bn_K ztMjtA?^|8Qyw7d0*H||ig{9|o5koB3j?bM<3m0HKFK(oFPGVvEyQ5qhRu{cd8I6$j z{3~M6&{RX%^;}&W zN!LRcSX!=R<^gW~=YmQ!>0W(; zTzw9*6^1eE{Ns}r)o=L5RF;nG^Ngc?u5IgFYi>xiRqkO4Sd}_$sC&alChrq`4_+*z$^sSAO5|s{=gCX6Aes=T#l*V30X;4+q4H?AO<@R?HjApU6#*b=k z?t4&k+bSuJJbQ!3K`%oak4-$8Uxq3qVe4$p|Fd?Vhj>@Rfes>2X^>kl2IpDGh+AN( zXT+|oqut{y%==`QZn*L+3i%a2>th{{rSp0_FxxMOP`CFU9266gipM%Y^bT&G5F4j{ zi@C8xq^9IxMF~@MF1ECo^$&YC@d?%BF8qrU;Ke$0wSwD=k3t!w9SdGJGIxqF; zZi*hhx*WD(rHNGSPyUC?(v-R}FHXBIhn?K!>0XY{XDnooof1UNE~-cG4>&~o4<_&| z$}Tm$;)tvHek1dvmOw75Xj={6drpBnP@%09%D{DOvizGPirF_E#y;7>1@3-qOeAK) zEe(RBc>SqyPH39^fS0>^a5nWyu`Ts;b6j4+${l!nG&lNqolzm^Hl|b9>^FhJL#Nb) zPhi@^TSj^@)!CZD+XA1Z+cN2PvTpM%DrCFtqssH13EnjLJK@zxfPYUy2Fud%Z64k9 zw67lQHZ;@oeIqL>nsF79+X(8VNqt6t>85Z_Iv%%!BUR_7bo<1r|)(D;hk zOO%&DK`I8xh3R3*2$RN8WL^2aov8eU6FMHOP4%juK!-8X6P0sp7X%Ga~E-;Zpakv zN;MQBhEedL>hts{D&;;H1(3*z^XVxK5~gk%p18ud%cO}Yp2u(fcqIM-16xM7G~uH( z*sK>5MZ=*ut}>zbDM4FYide7v>HRqUOfzP!qz;W$iQHvV$2Z@7VQD$joN)Zc^*0tB zDP-Nv*1W5#)#wUxVUU*uU+vt-o$z7dXaevkD5-8XsOXxbY6vbC3V>+jPHub(A2%csz>hL9}L2? zinYWQCIoAE{XPLg+(c+V<%a;m_syt+;)g&X7B$#=CmS#WL}-P1-|DCw`Hc@e_V@DN ze)_Myo=WJ(sDiyzO)?&5kc3QQ@%IM;jC4O$Cub?HYDGfp#oviZBxLBBw|es^CvPuN z-F>Omb^Z*@4obHI`9C*y=$58Z=!^t2H9}YyyXd@1KZr0SDmvP{#P*Lb*kUH z`>1^jdJy{+hY1KUjyRhP>8P1awmoj_^^Nv&E7m5=jL~_$kGeY;z^@$a>KvI#SkoNW zV{ZwDVie-P;t(hebTw7mgF)WAzb8}w@LxAd1S*?fxmN!I{KOb7IG$1e3OijzKUn4H zU6v>RIe-I`!tuE=W?o&`Y|)N>HRAOK?)*sM_r#9SV)#iWG`mb{Rc)QPm3R8O1&6LMzREC$db%o9UaEX>0 z=T?tvM{`iuvC@oO7qDo~TEA8B%X7WWzeu-@T@!E6_1ml{#$H6rnRx|sKF&xip7Hg( zjj3&8_*jMTE`uImqL;WiKErG0pfmJ6*;z(r{*o~|!%%)nE9)W44&=filnS;*}s}{0yao-fp68-$?d^+|ZdZ~3D>6s=X zgL@}^^4Kv#cvcM=*0uAZ(u#Z7Bhp6J`gRvKX72LIoP=9ydC@MgzxN0_hEb#!GhCb5 zZd6B-yocP17uQ>gZmp88VeRF7c1mRfNs9b%l~vQCi9g5pZFCK2hPU9BM)wu3Fb^^L z5+-z8Jx;|kG+&K`!p4G_!Qtt1{i-1S8oL>DI{qqzMBArG{=(M`J#jbgg^|$vB3#3s ze!1?HN#E5q*1W)+y+LSs)2a0R&K4Knf#=XwI$EI~T+3b!s{Kdl_oM5;NeExALfeau zO=-NSO5>3J>nCk$$`;hqh}9~#l9tO)6>a3wmJmeDtf!0^G}DEN_HSOIF7|-=5E+H~ z$RcZ@-~f!gA%+A_zH860t$KDr4g&dA7+>|8;|2acnnCv0ybfpM)#6b|wE_LhDfLSA z8~y8SvT4{-c?tPLH~wX%!FWU?;SR!AfQqFPvHjqFM`C-kYS8ek+AGcjl^*%d7d+&d zMp4-|zXE~a6*fK!x76ON`UcMCVkuW6@7xuqxQ!3c1qw+)QMbYZ<$^DT5bEwlRT@;$uqLPlvR-ZQ`GgvyC+-k84(?2sz0)@MD zAJ#V+>I;aK)4Kzb?}|=HlgJc%yw4T=z4C6W@A{@0?@p>2T%9~9GS@!lCUYsa(<((YbHu()%kac^Krr-PVT20B9 zjcY^xBFX6&A>qo!W*5qPU8dkJ`0CseltsuOYS@`ovmF-*Y)Z<@$i;pioQ!i=Jy~o zEn%~rH&AxZ{c1LW}p&sFRfpY%5k2CrW4dgb>t>o?5MP(**vxMuDu;`$4X@4mU1C2ik2R+E< zWivihdhBfPNkB+%j)`R}ll0N;8#lDMWsE$RFk>+>f1B1 zL##g!Z^PU2{8J-yd92t1V51gu)_)!*GYPBhRpo$v``Y;eNJDwX>*tNxB5fiUN$^m@ z)y$K4Xn;^x$y(Ac$Dsw5&LI?ES5}6Bd^+}CrwT~(37Ktl68tP)?(V=yi7c>#O(ViD zJvLTC57!p%7zmEP_oTwP4&zuE4|7Pg>V}JsynqU$tI|o@AH91C*e>%+0|rx&gRt3XVNO@(PS9Q z#F$OkPGslFlFB)`ocN;dFiCnElp#I)(Wu4gqmbUUl3s{(GG6?dO>!?`{n#JfUgDoa zTu@kA?+cX})vXU(1wveq3LJ%^`E0B-Df#`LSvSdo@ayl;eO`FzcKF2B0OqY5XX9`W zE?fS*OtJiD5&i>Jzq|1unBn1Ai%Fsr-PYzjXoD)4ezA66|6$Wf7^6$@@^i4dOc>5XDy({`uHRJ@`phn+9S5!Hc=c!_^kHaBU8-!nef%-66eW$1OB$np3I3uQZRUBYa-ZFb_^_jYF= z^&v`@E4eU9AN8K}r?78Ti`!={bSlk#ue{YF#%IK4`H{uh3*gAJawpkNO^exzVH7 z#$HHT<|=7NiaOPb1kX?CZE_>Mjmw6%q)*#yLWBQMAt6Os~yzLD?h*4i;kIM9+i23mK-EQP~zpSQEXJ>YmW#~V&S9_hzfh~92q z`6TRZ;n4fdZUvv$1)Ec3Yh6qyh-)p?i>bG`-K*2F3cS+3@rwaQxuLf)tvp}W&P$ur zl+r2He5iP+@Ot3gE157L0(!{JXNzE~%B0%d?B};uIX0<}(o>$2hCWm%H03)PS1{N zXBg-%qiV$6-j%PcWM$`urEus~Xq|*&HtO#(Lm-SZrFM1AY3-Rd?cSaWJ~iAxe<-Du zGw8<5Y~?Zb(EjAh>RHu@3(?pa)KX~&A6pi}An zUw%M)booL;9|-N5@~O_8_e^dnI{`*>r&Y?KIo)^iCc&zF`P;3v!-~@&(}%)Khj!eL zw+xn=M3$V3tE8q!Eskzk%;#3SpscFZoT`gj1r4i$$&!`O#Ju3~(Y)~}M5C-j8ueXE z783FGOvBhXO4OLgdJ3maQra8^5#oSB>_bMYjq+Bqhjy%6b#3J|EwU=74eQf14v)5Q z8u(bg)k%jbslSE)nD(fD8#0{c*w_~_SS18gU5 zn~=re0O^`vkS_JnDe+yW@bnA2Um%%c>LF0sl!k4jU`{rQLWsH4Yj=`4BshY|g3Ndr5QM2JDPfsUM zsHufjn*B^8-5AN3P@J|$oKj48E+I~NQzE#a$(tR##O0|y*hh9|p9P!)$kWN8jR}`zfel8D{fbhBmBb zWgmp|z5ZkmtsCX^M$~{$TtwCF=19@Er4sGcq{q{}RdhVU$&;W&O^xJQUTPoaYUWvV zxoCpxkDMHd8I(o0AUie+ZF9Yw-rV)~gLYkK3aNXz%Y@U$SIwMzw-dqI5*!BunSmV3 zsmV_uk@VI-oY{{v;QivhL-cakk(+3jgvLc-2_>W1*VtGJ%XU#rv4!)n_Xvrz-0G3e zmus>-Kam#c| z0f@H3p6phaV*=VNlA7m5F^MI_pK-?JC3aOkE{#)ZLhd2{@D*Fwih>dT@E%;Ci8EZ- zoHu}!_!gXO)50^Fv0`m|iTg(=*vTwnsa~gnb1k*1VR|S(SOI7FW05VhnV!Cno(R3E zS9jvmr29O^8Mf@YTb>`hpd0QrbDrW1py@9i<|PGkyl%Hi8Hx`Lqq5ZT1!j-q-cNhh z?QK0SJ1=vQCtjdjUteI@(3RfmSBTrW&2hN}$Y_y~pl8SV3zrCIw0LEd=`xTVA+fRbKLIv)V9r z<`3i-3enZU0P&3VsiUH2X^5VPqHO9$DJ^IWI#UPLe>-f|y*Kiqw zr65<>Q3+zHF5QD7(BI0lmK5t(x}#eMZxS#6QB}0;d{k4?Ss867QWXJzuK-+yBW5Q* zl2?gpcC||j;q^>7HSde+OqU}ToN3N(2;jXJ&6gdO4poh~J?Fa7Eza@<+>Ns+GJKGi zWwp6gHM`QSg^(+3^0x<=*IK^i+2R^8IF!`lG3VN}UCmkUxwj^oEbcx3=pcfL=?*v@ z!`c^C3fwZvb)HirmTI$o4t_LLNzJU9E>uj-ZS_MVlM>5!Td!AhuGJ0bz`U+>yp%BLQCEn}l?Mgh9)Y@(a z>mJXK87vxzIWPUAsTjB?L0_KVy?vWSs>G9+lL<)uVmmj?Yy=YJ9rC!TP@|~ZxxDGv z+uR^yOND_&!rcx|mHg&9&rITxCXM_;Y?&cik-J4+=^_Z!lgAEIw_L&t4&^TxOK1o2 zp)1$g$Gd+VUR_HpRkAX_8$fEz3iF7Ce5;th8xW!~0;s&?i#LnDAeGk^znc_0YKi>u z<9U&Py5j1erTkvY&z~x$WEoK1vcA?!kP6lM*LZ|71*>j$&w*j`FVqqfo5{vv=M)zU zXSu@nwp>Rxo`4Vldt#C4NjIJds9F-@=E@X~L@ZUu-(e0c;M=Yn8JC<*Qe53ZPgp$~ z8WFVuN5t(geayL=_K~qHea|@=GDwcek-zE=B_vId#B3QpiMdRydcFIbv+veTv z!dI+xTEf|AxFhF1;xQmfos$LB`IdtsY&3tUCJWo7p*>WR7rd~YR3p@|@FZriHrnoS z#;*~oOzPj#_@tOQzV(g&+MA+=8B>;pM+_jqo-^W0D`L7<40EM~X!aFxfPq`iIb%&y z+QtS7rgh1P(O&;K^{6ZQ6fyF$diXKz4p7!qWx)@{%Nm)AA68`yAvdy?>Kwh3TifHC zde$0?`Um#nLT+SoD#C#5Om{&nild6UZAx2I7m~WZn=%-=1?5!p!3Y}L!hs%=m>+06 zKc@}|>zec4RR|j2h9VP4L;E*2yUQ(x_(P)e z*(BHXKY_o|9CiYb;cH-XDX#LGoG@2URy1Mt$aVLu*@_s<1(UvmNs0VnKN(~Q(uOkT z7s=-EE|?A1Xi$JspXn;U%WNHeD0ahGg4%V;G+D`nb=zrXe$CpievE0H1{y#JpODn& z;(I>mwr^%5idn9b`OJBtz)2P}v(5ti>jzU(AJ4KfG(u5_D{5%on+KGUP#|$RxbXyY z*tfll3Bk2m02I+XS^NQ{r$I5CetGwBK4Z(|E7<7YTyn~(Z4%S=|Fmq20mx>#&#<_o za;n!`w;FnkuXAfywx@3QvtD02x3V1AC0xx<1~GPsygV}k8U32%^-(E$XCgAgo5|$5 zfmefJ*Nqw0wZz)Xn1g6Liz(FLynjc_=D0WLV6ir#;&M}^(9~n^79DDi87|wOi{2p1 zEES6L!-~Fh3=f#vQgQ?xFAg^DAAQ9-#C#AL6)v*|k8iv)rq)uq(6{m{wVdLoM|$*` zT-iXFh1jk<(f+h#NXJ$)pUzo+n)8;-Kc$K+gBDOVoYU8~pCH;H!pq_pqBqjvSHl&@ zJ$!k~FCV+7F!hSqzI2@&<=u+iW1t5nRb#VDcxDJvJq)m^@|xfk_%k?4arJN91{0B* z8M3gmIvNWnL*P!YrQAp*JGuhQ9znFi7_k&;amkxnH>TNlkJMR{)U|9%Yvtyeg96Jg z`02bE{oY4!{ZR%)al@e(-c{ue{_S_?MPF(CGF6@Ua6k)o&uC7M@BLP^o%_%_U$G-! zJVk$c{tg*rnEFC$Yn4}fQgS=4=ny%BK)}J;acA>tZ36%T)!RJDU!O$VZSaTT5PSXN zDfCVcqd=y)CX%cm*KM{O$Xt=v_!R+OjdeH7MtKdGSZVt{P>11czd2}pGOO&^$dGp@ z;`~ZK1VkFsA0X8eV_xl5UgN5EK>myj%**J7yCp8j)ZdLWSw|xXzh#O^MO zBT&l~pNbZdtJ6O(7|9sseCafCFc9UIkTJbR-v2lRoNL8qCZ}wPcv{2Q^4=K7v|4$A z`bB0XL1uEJFY_=-v2)=PW3mKpa)@;r<*$i|xL*?t7Dkrur37A4n3lK$l)ER2`p#zt zW~tiSQn;^6rD4xQIz&v;D&{DKuDIXi7Bhs56)mDDXddm73%imR@g|lgx2;w=`VB0f zyD{t~;`Ik498o1>)6DcusJELmEz{jDZUg(H@9p{&{hHgsQG|ryouNe5=3>=M{_xIs z%oS7mAWg~lYT)wB03>Rp_KbdGjvQoH)=T(EER&9{Z~r3u!wT3Y3jUDxW;=Ntq}uyF zDDCEut_bM?*>TUbekf6P$eD7zZ7EGSo7m`Vmb3fzkMl0A`uIb~FOS7`D2$tKI3+Co zmUpZ0wDm7vY9r~EiY^9V^8d!&c`<5X{)@#`FQ@vh;%?OTpZjqd^gYv)#&^=+*DmXl zi|R|DNCCjJ>bM>d+D{Pa5KD|bsuc#Ih}<4pNgR5((x$ z>`G|c#~)+s4DVi?R72+9#?i0E*Fq=0!T_d?MO*e|ylmEkJ~}Kt2gT&j;!0KS82wn_ z$jLYe>+$mQ=7}&{J{r}I%Gzk^ChvH=uKwG-N-eL8r5%sWE#m!NP$V^{lFp6|XnOZo zM0b(IzQtGoxe(^ehfj$}cAk;48IxLX>N_8sz3^mvv~h7-k=tM(Yw~LV`2%2c!wDie z+wb+o<6M&9YtIDE$w*poNe%09mb2p?P;|=&ba1>20y}P>PYs(Jty6TIl3LdjxXt=5 zX{QMc9>@rf3<0M{R^}HI-#^tS(n$VJTb0+>@`jj4IfmUE7n++()9ybcs2JKxQ%QXW zUJhc&w{O0y$=JhSsnCdy27p7$G<|)|+$*eO(l_u++RiaWHBuJ{ zM!2ttkfa6kYB94y?c5FTwF6I$uIe9b;gx%wGXVqn=Nz!;-^$wF+T&1U&@lR~bb-8hJW$Zw6s#&^Qda3z&XC`@@AIx&_!q1u5I?*$qsh~eAM zqV7FIGlFGtw?=tm;)CM59)54#>4#fdbn%D6#4BP|qi+OPGod>EE5EUo?CsvmQadu# zD`FO`#nZBGr-dP|c@_*X=^M@Q4jYg4zpEQqJZ1o1BUS{6HAHbWF?NIWBDr;_$m@$i z$~9(JgX1!)An$3}D-K6|{Pq;zUaQ=ja_H(on|t#_aF(!;L98u9Jo~1}M{AO>f2khM zudAfS8E&0#TLZo|ja!&D%?;dhXg`?A2d-m(bqWXhBKoL1xc*lSAa zho}IjW@+Z1!|#lI0C}Qr5%5aZ*Tqcqy;=JVN-?)RwRBFEG%b@+!Dz7hLE4m-Q zPBWpx2n?;>TPkQ?cOJ^M22jLFVKsBM;a;tSmM5{!#DoM!Oi8p%8w-+y!X%|`8)u@h zq&0Ig@wKf1s(wv2 z;Urx7SN+WN>h>4Qx5LZ6-Lo6v$^sT?g$>Su1qIOD4Y6K+>`jsUss_glqx8B*04s`H8+^k7-o2@6B( zzf|?I20S=`fefiP8=+~>^AEoaOU8l-X1b}!+#oZR@pd@8Bu|buu&i%F0O_l2P-FCx zw%}B=-bZ4ri*AF!j=chBi<>Iw!0z=a8UA({tbO`@MHG{O5ATgo8k{jRZ};})89y$ zn)u{C%CAzEUtR^=!2p#64&WjMye+t6V+KCR%a+s44<8)Qs7}U*98O#FV`|ZMG+YNo zeU!At(}gG@0NEu2_6J2$tb@QksD<6etE{J#gVIDH+O6TM;`6b@q4DD;koB{Eo>&_; z)HT1S*ceN<!UjcJ1vdpWf+o1OvGP&ln;I+TaK)e;E7v_;joD zy`llktMV18SO`QHQw?pIW1H z`)3xzL;u83bapdU1kW3JZSF9(01`=WQO|TH)Z|8-nCb|}5w3u4i(sKHDT|cDaf2mL z9um5lPAea&SN$wzFcxZbSH4C`Ej5nF6>t(h(cDzNYyr1qUOGPOywbCF)~^**i7hLR z25G}B#1_IB842x4Xn}PPfpX6>b?A5%Nd;sKed@_IOSPv=?2l}(rCAsR{kW{w@hi9) zyLaU#NluV^iVgHeMbU2_jh6BmS_jwX2M(_g7;6BijTSfSUi6tW)fEF^;lkQ=2|JH7 zW_~!l6y;&RNUED5D=4^IRxcbcSpk3r*)d&0=q#;J>#FRsC{I(%1yb+dxH-10;i157 zwe{++rHnwG?gja9DHBE!TaIg)UW9M6uC+?7BZkHBvJ_TPZOEtrL2fP$J7#jaqiipo zDG)&VJBVyWx6zvvFvB!%u@w5WxKpH&lgmJHA~tzy3Lf$jrL_u9d7G$qg&}IN%SQI7 zp-uGHTU>D8&nb3E(CE;$KcgL`aQgi9;nh^qu5Tb)Sn#!oK~)gnELw)Y=bgE<4}+~` zWV#M3$$p-)x$Ul3NF_4`H)EIYTxHH^f4%Kc4%{ieS;o4AjSQB$=83(`HwhuD1K&l1 zeII9Av5b>h&|GLM#O`U;U(&QN3C4{y>IYsiZt|^vOD@iMdDNh`-!)t2?nY;KABy&P zGYu+OFeCak2WOZtn}B@+va@Kcm46w?5`0xhv#a5|gbxQ7qAO*cj!+?iF>Sk+MD7EX z!`dx96V|tVeLH`d8SvA?cBMYGfHY|knK1*)%2ED&4^x^M&w_o+;2h8Rm4$~4A3jn& z-FiH5IqOGH!tv8pP&;IiGBVT z%lg|VT_-Cy*GdlU$TNi&R?CK}Qw$R_)bYK4hW0pc&E(x=v@#$ZNNIBh=y?HKVCNmq0d@e%of1Cx#4su}n7D&f&ix?jHW3gnhs6D)#TG1|V#EAf3pnf*r&=E>Hm^M7s&>WFxQkea^fe`Or~znI;cqjDZK7UJu^DG z_9A|I-;ZjBq#8vEAA^NNMx2Pp%(YwU2+QxCd>55)e!1KB9TzB0vW#wWo89%Or|tjr z*HX}K|C+heeTL-8z+O?>6rO}8m&~8=uQ|;g4~{fV@$cfUB!J#`uxrXg3#f zm+yshX|PyT^752j{atDsN-_sW8{Vct(OlGk&(kLjD~*~@V;e^(8${vqGEWxj)3Fph z4n=J7B|1$LKiLuHW%v@eg_C^(=d%U=nrGaRjKbRO-4{wmE;ry!u zL(AWJzeqd$uES$f`|qTdR*@t?l?Q)4rLV@ZXAAL^(S40?uT&HTjhcxBxe&$$2Xr6o zP)_8&bEJubj^L5;5zrtq-}dw>m#O-7$Bd6a)vEfqa~IiiR_5t9G~vWYYFG-$@{lW~ z!?$(tl{_cg#hu!tc03MgaU$}n)fzVKk%a15wJLEp%2^q=l7)0z0{$X?kZIuPw3deg zb)8)YwCj!XkZ16M^fPD$Xb^FWXJ@J#%4V&bXkO2u93h>`3vK$%$!9 z+>-qRK{|fey#nLue(oze2U^^8V_e+}LXR2{4fshHk5zS&gbprq1#6ys-F&>BewQCv`2czR+%WZABMGHqZu+_Z{&1+{uT#-6!) z5{v^@I>wdeO0l&{=5;GPj<0wGoy`Jo9t*64}4)K{Vfv$>rbPL+{$Kk|N;;%-!K1&O@aHJ2; z%>_8h=^<;IKNw1>kCfASsub%JShXr~tG;MiJ^MP6`B0xj7-atkG%OXnXVbp6STUqv zdd+1uysbS{x4E0_LSrNO=m?Y>ouP?WMF;zKhn<3A^AXgFXWWVLyCpBuGG|ft-J{BZxU5jIW~+=dxwMLb4_O5zQ*T$ zetpzQmzLw1{~2`e+4`>SwV-^RsP{b?%0HZ(!X}BHzowm3cGiD3#X4AKX$t4=XF(Sd zPKLP3n!@Ip=VcxB`tQms9OIV1lSG^1eUU%;?0{2a^6N+?q$vqTYOJMOdTz!;IG$!E z;IyHQoEYwunilyiEK#qdWOuqE%+FaTY+Gf%_3hwEE1Vcl>mAZ>eLC4;Jrq(S%*xV$ z@W%xkMGI%?2>d)GEkPI5gu79~l07J=!F_JN@xQoO`gw;Q}Qk>C;7VUw zCf#dQ700o*@M7Vk%PSVIX5TqK8x_dS_uhOn&E_g22-fVkk+bGZo^?wYYe5w=P9y3U zo1QCm9P4-8a;UP}mJ^YY{0|8Oj8i~dfP zOePi?jgidN?3;rTTQxPny3=B8k8Up^;SPu?EVw`9b>copIA$K#a@5q zOPzul6st4i{yr!QT#*;G}p1nAmd!Oa8tv_ec(NV4{Cm?W>B}AT}3NTxH?0(l#|*#d|;X<&}|P zs@D6OwnAi`>MhkD9AO*G$c7bo|QMY5LXMYK((&2jQ(>b(#(EgzcdhdH9?{pYM)IcID zAp32^6P_2R1NQk7AGWvqxj!Y6A_fu#voH6LgPh03mJ?7sHIjoYyBESvGgsJd6<2yo zK@?d^?R|sVOACUtl_9VHRgg7Czlgqg!}c~N32mpB4UyP@$rOvIlsAu82&HIPo{oSk z6r?K2NaV|O(e()=O5!9Coy)^zb5l#0<<~AMG&}c0H4XQ{MR;3j={X3XC43MXa2Lb9 z^7TT=u6{J8mqCxO)8Bc&nn4eG&7AFos50NU$=xBcKZ2eR|q5WXQUp_*@S6}6H9L)+x%8^*TUDoZ<^>)^OK&+=F(8oLdVao9mjS84X zy0o_wy7o3PRrfZ!X&^T+`&|C=j?8N%ueEI0?;5uhg{av3kXMUM>P?gsTyn5{z!zK6 zgRX->MqLs@L8~*JvfT_%^5<5zsW*zU>s~t-oyr@#*bN=_|1<=`Z*RTh%M~0-wmy9W zG-V77e26EV`aV4^D>Qa6d|BP-3TI3AVxflQ;5?Y>RL@KIpgbl+7up%t))q#-`#*{vkk9oe=boei-}q>CRaBx zL#&S_HgCcQ??$IJq}qxs&EHz%hfC;{XjX|Fb6@>K^PWleKByU&N{;P+pQc<`6Q-xC znV$es2+X9Gbz|(~G*`F)Z=6b<9_8lj=K)SBUt;3wT0095VOB2Y1>opGq_G4gBxlft zYk~aR*9usRWI|;LsfD=UQeuhp?cbGTNfcGojj>|GZbV-&T^fP5`*duanS8A-K8j3G zeh3?@eKPJ}jNPMw9=u+Ru$*D>W}*7%md9x9ueF2Kk4D&*FFO>h&xbstmR&<~;%19~ zsYS!yX0h)QTZG)iPDU9okwfmFDS$#=>T?_G@G7VR?rZtdsiu=N;3!N+9f*`P4JaivtKo+={ZiTo;v zRs;g1-8SFje9bJ|OY=T|WtCiOWIlQcfozJa;oG1?u{)ymg^cyn&O46Nr|qh|m3(&T zVf^IT6(xH|ac#mi^QF@MLB-%oL#H$8mC5$V!2uBNhPq*9Sz0!VX~&vRn}*RLQEZw(m2ypxKN|t0(rGe&--U;Hps6 z7rD;|-)+ne_mYWGe#B#zT2D{PndV4}li0({s=D?%8F0KBx-bZtN!-31s1v<87dv@{ z?r<=b%ln>*+|S+7)Irb2(^mQBBxrmpEUw|!FmhxZ>DiVDmZ(<^5= z`3?_Q`C-jzwz;5PsZAEMDnl><2HvV*61BER*Jo(1iIkSC=1+mWGg)qRZcE%cYoquK ziaWJQE5*-Yudre2?c6llxjG_hujrfVb&y7ABJ>~&sxa|kXzx>2DCWSX-QLsp&afp5 zPj&_e0kY+)+8=EDk%K(bL0|DG_ztF!#(yrWNhmA?^F_=oPy!*N?eJKF0aD>MJ{nwA z_Pn)P_>UL@r$3()`IcVY>Vhad)MQ6-qhHRlL1LArX8rYFyd)3I#sYnWcB*nun0OcE zrxCZcAl=RP2bkn`qE6mtx?>e|N*KbOa?+TD8yZt|}LBC*uXxV3=ogt2`Mb z`NIeu?^7`^j>j3*BI~r2guMurpeG)jjWA3m95gPfqK`FSC=5B_S7_mFN!VICrp}4I zFl(DOa3U5A#D>+2({|3iGqLwJM7;gX7w+5SK50yx{pcopY~ddBx!*121LFi1=rTGc zNKQyz{7kOvyPBUP20ofu26_s%?su zW!9T*7F-*F*5{?>kmjKa>DW~>!o2Ebc6O~-4+*ibkP_Ed2bV|7cKb5)2Rl4ln$~|24wbwEH4t&5VTvdryOu2STQ6-de3d z;mxW1(yr#?;Cm>zcINhCes>RcdXx(fmBp~MWEKyBp;!Yl1&4F5|43){cf9QCF4uuT_4iP-;oSW~TdoXQonkep&ebnvY`rNP1|cgGJLBAb%2hYxr= z3A=ZWcDJE4)JK-!LcApC8hVKCc^Wd7n>Ju9;H^BhkL}_3eA(UT2D_Yetd%@KsmjW! zQOv2mDvjQ++y49^f^N2D9Dm{1CnZ|gE$;xy?$AsQvDkZr7IeW0EuAa|7E~n8Q{H4E zIL+xE9kYwwcis=aZT0T5V)CcU6W z%BR|1AFiY$D;oQLbaO!D2G-u(D2jMg2=KD=A0mTzzjr_F1Ua+WILOt6nVWT`8zPDf zKkyY=3hPU43YeYG`*{n!-Q4Z(opSaZnAtOYfqyD9MYtMCkbRA{#Rym1{CNCO)w_Dp z(lXDkllr~mbCYRpyf3(_q=RRv%wT~GQ-Sh(-G=}Wcw+}Ur;;`lb1Y)}P^Wv4Md*<*UF@X279!uD6z?BkRn z;b9&{@;Of_|2SujLNE78o8Vkd+=ji!1&Q9x*AI`v&yPi=Qv`%8FJ~>`*>9|!BfM9?osy=NzTFehEQ1)B&*fZ}`c>g%6+jo8Fct_}3#*7&E8Q-73A2)mjTS@6P_kD2aC$2+eN> zt(9WOlWZvV{Lw*AtwLW=H)Sxl6)K@q^!j~kS@|uHu&xd`?S$^8=M~8Cg&sY2?5q}k zv{RzFgVLf(w#BiWD?n5-rFl;KRc6Z_1{5m)?aWhBWRjY-O=VH{?eo$R?=SENe^&|% zZov9beA7IVUE9y1o{y!ANJhaeGj@ZdUvZ?ne~n-v1?lCcu{ETwOpT=IQPPn^_`Y;k zSUp%ajV4_PgQsU^O~R4#7$f{(t2~4FjkEBv!mkSFtNGaRq&F*qhL?FQR@>hDko<3t zgG}^)Ig>>wiM#DkM z=*1(kt}kf@WqFq_kZLa>znuCWkIkB&cScR7gm^3AR<~yR_gnlXJ&j|}F*+2x$>IIz zsJwgo7ylle%>_t%{ph1^nh5vr-0c@@a|9mftB2r9GtXn$+ItKraHKhH6+Ne;#Oqx1*?SzT%`>u$wwT1AS$<5mKP&UV_Hh!10mm_d_ zhDFRqy=_6qR-6Y5+y~8Nw}ZJaHH|!Ot8Yc7J*f&j(T`4Q2;Wv1iaI<=UHM{Za*93Fy7DnWJMc# z&<8(>gqlTDK-Qkk*`%NL!9(w2B!|`)fYK{awI%GWg6j?6q3tq-?d|E<$jW-|(bmFn51tncWo#=c z+8{e9+to1@I^O%#qv#Jd=Vt=td#tv;1AnIGUmPu8I<1ACTB5-vRCr~!0Hot>tcaxQ z(SW4}-Yt6B<#YoYc9r&~%?(ndvb@pZv3>}5gJX_@hgN0HK`6Jb+dYX_&f6o6pe0j$ zf;w>QrFDReG%rtbZ3_$o^hq`RCv< z!|}J_+l+NIT25q;8{bXQAeQm8R6~02FPsKNlQMo$S^JVAKl}v64Kv-%>H~_-(2qm? z`#5j~#u$AnjKOhBr*{cdYe;UJ7MtJw1%)~3!7C-LcP~hJDps$#z;uYGx)KZ9?r@XFBG*qG(Z^Mb5!%z5m9bw!!9`Bj#a`QJ&{-cX89KHXsT~-G zUX`0@tpmBYog>}Nq@Cx|#{=L7OCMB_Q(D2TX1zgCwxnP(h!~d15qZ>%Y=F&W4}PHK z?rpm#K?oj-*--Cn_ZnX4Yin;Nwzo^=SCM}Hgjq^|BUzUE>uB{YiRuN$`z#0L=d}=1 zW(r~2wL>ZfJ)Rl(pp|e}Wd4f$H0zS7)y>@lCK9ewUO|Wbr^&i}r_ITVXi(NjYj%Lx zl0PYYsCB9$Cam&9ElZ*Hq}AY3=*h@JNYK#2j6!wO`tCLfWG1uCzg3HrZe+b~3dq2r zBSYKVRu1QpWyKqk-2WdwQ=YG!Td~Uz=Nb*e9!h+Kz)zv%{hg za{J4_KfVMxHxiOVi;a6J`B1(J5Kpeb?1a(BnRJ}rZ66*Qi0N(}vdLad62y4U0RU(O zv2D5JZ8B6oWe3t%&W`Tu6>-|c-mI!uzL^uaoxYqh^jRgA`Qg3awi}gS^xod-1SJBQ zmO2h%*bvoA_p-+_w63J^)M_L$L$Kc9~r)f_~qX%u2~jRQv7t zMBo0~@42K*m@P7X<`^&2$KTf1YMG*>odDj{BCwiq+d>N-$>qW_Ffp=?K;2)qM~flK z{8H8#0z5u_r(jw<5c-&R8Mw;@_;m8thD0>NWlvxBI(WF}yEXa zipyhzuR%LRtLUBbqr!wG(=1cBHO5aB98w63Y*Kv43v21kUAG|)tClhY#obzX4z&3ASo_2lzGmrg&brK=+(K4kz3Fw zy>RXWn{NWtz$3MPLPUg8c;52ElT*HKjK6dqa|0QB5>ZPw*(a-E&=X1B;>ynZKPOD> zK(Y1v)B*Dw@`>hEX4-5nJ(WRy^%_o~R3uJe;Y5y#87~ey_T8NZ_$x`BH3v2!&LEnw zLzi!%9p|{2*TDd4AJfCJDhN_p3m*qs4IQ;1@@K-?kFdvbU>n_Wu$ zZx6i}TeR>hlP_ll>R~)rouBU6^(`}`n9EsACGf70))lWZfRu&7rXoI^?nqkk@Ta3X z4RNIm>R=zl$cIK%nE&!b{RAcZUDu~HsvF1Ck~gi2`3(eD=5uT)l035TJ1!{mRn$op z#2>nFUI$+MVd!rs1S&BIMRQIpIQ^Ie&TO73J3AEE3tcl?_-Fy58lc|ryMO0b1-~d} zE3_DpPltBiDNiUm*WyQYsH8aU83ua|a#>u6tac+&Dt3Os#M{!8 zgvV0&`Eg*|pzLQZBI*k$4S}F_PU2b21bLGoNgkue1v8g|Ey=U~zE6xR=3)KDy8$J8 z#5LNosvc%DGDZyMl~?B=hTxz6%1c#YpDoN(GS?GjNi!iy7BoL7e0Cb z*wA`;{F!dt_k?1dnBz=_Q+=ad_nWV>U3=Zp&1jH_SIHjL(?2)0Mx{Wzz@FX9vL~hS z6F!@zzC1T?^EqddOm)f{s2CsI6;4){vq1Yi!4mRMt5&Wj*{^z4QJ=07xdMy0sU$qL z%{O}DxVFc-o+*zd?S%4w-IAD4fgFSk96l@WX*x`bU)Wu~bcpvvpFQii<}8UuxX*MG z(|aT&midiG9^ng5(aaI0jz+bELtf4Ny;4HmKri4;afks#D7rpbfi_F-_ z;L{lV0hrERNO64>@%u&10O5qf&z>Kv-o8hr&in3fC1!>$rs1`dPaf|(ZFARk$XJfZ z>BS;J*WrKTJEqHO%(OQ>Wo>lUo5I4s0;7MVuYmfrf1cAf}P@o7F?h zD62b;Yr&lQUlNng>!1lskrBSDLUSh8MH|P(+VtZaljNX-29wyW70ffus9v4=N=)+(aaBGULt)H@zj?& z{nu^Jxs)h_@-&Q7gUE`@07Y&Oe)Ok({3dkutSQ+K9Gs9(LQMp*y7uQ& zJdNZWLMpHCZNrPqYpXpt8RZ-1+ync7e#nMc7m#mx$Im-Dm86UQE|-^H3BrfQF7Lpi z+ZjISr!!?05!s?Y{hImVr_#Pe9i<&BJbZ-*y9Vkd8<^8R1}>;tpVZTapAqIoK5s6y-AUN~NZ5MpwC zqM;v14_7Zjckeys-`?Dcg89!Ty>WmX|AbI;&H7##@E<{wS-y>Ofy8bCS zy<3G4FOl9rKn}t0LT21+iO-9)*rO{U5yIkUfY#M_qnih0%H|vlfS&kQ5tGM!sA=Ky2qevHdiZ;*861zE8wWr zakjPBHfk9iR+U<8MjzVZa&`U~#MZaHT*IPVirBntJ@55D7849Beav9yEJWcf$#8C( zNIJI3;e7()dxU`*39MC6T1r(21@aN&X|6@bk) zlDG{FQZh@s|Io?hLdQHnw-wtu)db)Z7^FSfa|0F7RvWv;TX}NYMyLCH2R>Fa2dyTn z_zFZjCJj}5KW(3Ki42csh4w`P648&}lbE)r({3}ZUzzR-_3a`>vBj>lCsLpAV`K#E zL!xevQM}L(ALc&EHT?rMxTH(ViZ1+dEw_;|8MLDnXaF?R=Q9^ol-xJGt6`~>cvoYc z`K7a_sn=H)JwRnu?4H@X4;D{0>)(~*s;@-z#&TYMpl|aS{yB`%z(b*BvM}p?vF}6K zhBe!^l!=!L8dA0y#ye1f*^RkrH7i6LD} zC@8TRo^1$0`(%=*RC7K2o?W1BSsw+dhIpBLO_{PAAh&Wc6Mj(^hmYj-TMwg?0b4tQ zt(F_>ZHu@*>8crj8OEGg@`d=F0a-=a+UA59HQN)iC*d+Ekuf{yyg8^zv|I@Fl|r)^ z2*2pA!q~dV5&XaiD9-KKW^3&o%k@ybwNgieYLM~8XaR+0e^6@~CIQNSLFs)&7Si3G z12!{<(-xW@&g0@;Dv6?F_yKC#qis9UdbTSeYpSXI&_H$@=x#UC?_2W`{`XUgfUo;F zGfmQ@e5Yf9AsSTpW>+89uIi+^ zoLxRnhTwYlHxd;~p?`MhWFn?6KF<3VV^*wKUm{QVb~~(jXY`nf_^s`u=P+EI=~(_?RsO=RcN^;JvC` zl=c3aVAzAT2#Ki|yc)iOvWd&9q0<|2bPMyG(0Ti$c*m0AAVzZ!&&Xz2gRyyO!CJ+e z!eT$dZx=nr>4x+KZwInp_DZl8mP!4sLx-v*3H$8Pg2uiNb1ncQ0w~d-DO(#3oQ?3k z;l5R6PHjLmX}gW(`W>c;1Y_6>nl3#Rngs96pcJnP@wG~2!x}0c{Rw=Qo=SOd0yNaT zM}VT>scNIl;V;OH^#O7+|6|xwVzY<&zcds<-FoA*l$B(<)~Fy`ITq3#vZGVvvq*ue zIF^#JgngV&1kdop0T(*`nU}n*_)|K5ea~tq&LIA_l-l^9nC-in8XGue`WI^(x{B-BlI){L>d3z!77|gKbnz(B6|RFv+(>>Y}s%P5igyk=;{9yR*GO zmPAyxs%c2`XT+}wgY~x)gwQ^xdo?o8kLyi0rpp2|KbFSfo!fZIWv!k4CsPnpPQ>n8 zrtAK&b!d8gS!|_aE&M<0u#aUPdLHslph|5z&az5exIo(CSCDwT(i0s+K&J9NwO4TF zD|=dxZ>1}zf1Ej5<55Aq93~O9Bcq*F&lYtgV%S$NfqX2428Wk&K|)c*Z23$SBI_lF zhs1D{_`-^z``8qKHpLTh-j%zDif$&(Ai3v_TJ38JD;M&zYcAVdZXCm;L(+yOC#9UZ zvES+P4J22bmP4~u6&*b#u(c3E2<~fTgUe+l`V478&m-tt+HrHUo^6iC5KsZU6)3d= zGUR)7#wVYp9L)`zQRKY6JZb}BaGlrM2oE2X;h{cI#Oo2{r8 zXT=)x>O^?6w)3T>Gu+aJ@+qifyqG!5KxE1iGpUy~`&O)~g8gIe_@1|FDwYd?$)O{o zs`mR>k50ePif;K}+}H$^56ZK+h>Fxy4V5rdq7iRSMv#8(``ujcWl{B0h7-=WhI*b_ zel{Xvp8tdyz$y8xdq<^nF~qYvi7R~IM6|v_e?S6m;A#~YBxGCL@wpHhI6a|MzSG!! zooat$nrrZTbJW~PMLf$jH<0#9|9)q71zPr69+_;ve!#(BIUyO3Z+^ksW)?{X_bX`i zQj!0=?d-idvA*!+V%UZpYI9WK?!MxTa_QlCa}1vLv6Dp@--F`S1L1MCt@+>0BOzyv zd=2u&RA)#D;c^!Fvt4s}_A*xJgLI6>OiEiy>uh4~$yBKD3jjtN{jxFurmtPH(mlNH`Qln$rVy*mpm6{kr?rb*~uL`{&?cMQ>it6__SA= zK+7&%@RkMtD9bu>F3ObXUL{6MEfuy|S{)T1hFonZbOUraQ6x(_GQe@*h;(OQOa1E5 z_{dHxa1Tzybb0CWTjkV`=T2R``+RSf zT#pZrVe9A#xL4}R-7l*mEJAok28s@eoN+xg$DL&4$9dAjr*a(qg4J1m4t;9y={IkP zKXXMoMVIJ*SW6DL2Vf6Ztod?KBHUTyHSw!UOpDmYtUcG)P}i0&f;TFF6#`@AWdx;o zfS>IO#impCA=$9fP8akoe(9ugg36>K7@f%_j&oFXVlJn{rhgGn(cOlflD%|`6)MbCAF&_nB5ottK`+t1|Ui)*^+YWP$dnH zer@n>kb7X7fp7VsS5Q}pFPJtK#sYBe>?O!=dGTcWF;S&PT@rmORG&RR8U{J5hU{?? zApjDQeiyi(Bz8t|%lunR&dCDm>JkeQyiG%0rV))lLN^ov)fzDIKc?V-GLxcM>Oqjv zP-_mOlg{s2x-`CAg)$idQ7gKf$o3{7v2IMn$6$^PP*Fdqw&XF_!RN7o-s!Gn<@sKm zR@~zJ`?ot|N?SOz4-`=PU78Z{@4k@MJx`^zOgEwLe9U%G3GmmSt!F>pyV{@UO7vu|C5-k`Xr;9P-EtkhDXTq0orLwMcU5pc-MU_fy)6x4=* zh>q`!1?wM=Pa%f9eflVhB~q|Rl$14?kP_J9tWlA>#DB$37H4Z^?e3`-WK_*G1DUAD z|A3Ks!`%4Vc~uVWKy^m!B1wn~H`W4ktcortXF7Y1`e}1PM+toL$1=e)4@^TGJ|)9P z``kVoi94_#xS|37^6)}uvF!B=mar(gezIx}rusxgbAX|Ov-0`cZ5j08$NDF4MGf{n zWJ&D>BO@!8yv1hYf%XvtwgIFKr2rp~ghX+`9(5w>&xVb<7%r7!s?U4Dj;)`9>xXw< zxxkF_Rs{Okqty1?X7zjpBhQMBadG_8Is~#B6qc-Sm5p8xhlFMDfwP}5_KX#~!TFfj zXl$GV5r!|$xZ< zk3@C_*iLp2`YC!QK(!xzCum3mea6>5O)^5yKy15b5-d;QV!YM z(G}`odGtd3`N(-9&gY_MItYHc4PjO+eLB%)j|QhiO}Jznevgj67?h8@&Y^bGrmcDUlHh@w1iZGaYas7LB(> zSNawks+3+Hl2X~KMjyxU7LjuWgen^iufGH*i`KvJmGp$(3ugti;rQO64yvaQcz#fu z0mBB*lP)UBaXeArgxe1$y0DNt*b*TCl8QD^4aL^!u4d{9@+6PY3;{6ocZ!~}O|@J2 z2B|026bOwD8JuN z!88oYWCRIX=kF#tv;y_(@h*U%7(l8Rm1*;~?JzlhLFNA&Uw#?nV*gy6H1+~RQj0N@ zlJ7riLg(A*k1j@iW*wBT$X-^&NOL@KI*ByzRsx2O__2Sc-O<%HecEuAhZhvX-Z&Yl za@d6XbN!u~60Sbr066Avw9sLfF0q%!c)RJq@%8iaDk5yx%Xnrwijjtw}1GE_^6hS4ZLV5aoZ9-qw%K27!V|tfQF!f zOP_f>Zj?AZB!?>(;Z1rj;TaUOuYdy+PzMD{fEq;nNqWvuP=w&UIz+(~7^hEi6Mp!qJhp4Cq5bAWXIuEKfg~47!09Oq?BNYk)Zlb z$EEKBT|x*E${-1~`E)omuM7~MFQm($%6fF=Ef^69toU!YDc0Dv3t@!z_l6{tIS&IO zWzakT*qA6aC59Iyjv`|4WH8+t?Ux2YQ1nSa_c2SU@cAlza<>`+)BWFGc*vSg`vPr3 z*$4X{4r3(R2WHO~MUlhOPXWPF4>Eypa^mj?#iElS34#Ri1cabyz(p`vf43nBKmYFM zCuVLks9uOBJ+0$EZgL$&8R@ym^T@`*ZOZy{1iEc+rW1r;`J&81?;2>`I*kPXh+G#a-Q z1|?lhUt$A-z@SF&A2{@vRGp7Z%l`1dy2RxXjGjlFWn7(6yBtbdvc)5|^?7yj1g<{>7>5P|&3S zP~df4ZKcT8T8!T%7$!kGnfbP# zWNbA%((M%9erc60oi=*F1n}9Mj!k<)iF-YnRI!Ht9~0~#K zy_OsQfR@YS@MRsue@dHh!u{@JZ)N{ET36=99_yfZW4}))W9|S6avc=+2T&hne05`| z2{Q9OfMeUm1}%I?9J3CWPNj{0HTBFviin&BB!qLog#Vp!5yf)GZMe$>(V0l>v3c$O z(@gkICu3kD*R#@8sPL6YHhO(w7rebp!{r!CsX=@cBCCP7XQy^fR-`f2gVWR;Xe=_~ zdw!o7Ds2tTA+?lBA!{{$Hv7{X-+EVH$Nuz1W-KPk)6ssq;`+7hU_E}p@YQqBObeP3 z0wOj|j^hJu~=Ef$P%1E_Iux%Uo<7 z-z7fI#|Swng`_0RS_rF|s<^Lh+e2!_TX6tf+K4s|j^|>u6Q_+%j1w*qUk*&PxkGd` zEjK0@$Y167_H>yOup^QVG-Y z?&7f}D8nZPy;`dB{C*k>-e|)YTjq6AULXuD&bZAZ3G2W$M&glEl0E%W#jw*1mcXB& zIJ|q#SFrPpQI@!lxpH;UdQ1z)fG6PO)fi${nt*WlUILU4##Y$~c1mGb z7#X!x!FZoeymU@mOsoVJ3OpT+`yP4=uUByJ8ly*L}(Vs(= zmzsg$jg|xdB`J9qo()7WZPDavrd*G8hr4#>2u(cL;^D-jUtk-VwiUL2)>k;WSLOje zQJGIPjezq+Cy^0|&vIed84#=qCW#XkoK}+Beq(%w+Q)%a1Dh^LDBxt*dJKBqynN3d=|bEO-CwZZG(TiocMb_W8_q*0c5gxmkHU#L zul7sbJF=Jl;H-N$`TzErMQV69QH=)AG@^`}bxdN+WiAe6e>}|O|CFZH8AKK*Z}yB5 zWXXv`@$o$TVa04a#+$l>1BC(#1+8JjYUB8y9FmeAn&mIB%H2N3UBtP+H|Z(*2nVT7?k zl^-FZ^|@58?ATyL;7<_6oY=JZ7tQaV5+AuA_q`6c?uy07*h znTJ#xVC8J6KQDn8?t_(Mkc&(mQEfInw-h5&(TDf5oX}%hR$4`zybn^al&Z}VBi6Qa zmlZZMFaBxUj9;j>X&_o;g?SUfPGLl%sfN31sMwimuNMa$eMm`yY58K}0E*&JC6)GY z!7v|on#DqFI=ZD0E~(>m?25KQS!KCnjynYjiD7KEuteY`UppQDOBa_S9RZw*_ZC;S z9|M`;GI&UO=I*&JH3&}gVi&()J%dF{fjoqJ*-VTaLb^46bd^q^IYvYhJy4O#+fmTemU{0_(q-rau$+ffjf+OV`-V zdUzFx6%E?|d$3;23UI$_MBph67!vQY#r?nQY^inuSfCFq)=>)Jy9(ZmysLv#@;qc& zVt%W9=c6ZmuX*kK>ZhaJ2y&Tk5ui{VV6xc zrY#*o@Dz9ooY{IU*ga>d??H0`$u@jNUaxs8hlOpz9iS}iuB_M(@h(l8z~Bg832;A% zj9(~*vJhWP5V8qRdHuZ5y>M|~AM~;2mCJEA>dN7>RN6W-32e4aU+p=D5s+M;zPR6w z${BJ8zC)VEsq1J343JjSP4B1RuO^@H*1fP@VsRMY!CwP@9{OciOx=`0etqr!|)mG6S4G<%w8rjLz2X}74w z`YEMAj||T{YeX@485ZW{-lmK`;tcQZpDWoL!Gg~+(+ksMQP$web(uVL;&i~ z*!`g+F-Z(QhF;Br2}oQodbef7o4VC?rjk>O_WQZX^!5HVH7A<4KJV+x4nkw7yc*NK zDWf^8`?S>*`|ZFAJR$rsffqd@f<45yH#HT$7d6XX+|Of~|KwM`xGyLRZnFS_RGPy1 zOB)Y~BWeU646{hqe4eCQ{d9~*_P+=EaA%8N7;tun6HiEPZiL4#%)I&QtaBJ~4Mp$0 zf7Y-VT{QsvFNP)XROos@`yG&%TV(1UCN84xFORpa2fvXLdSaATen~W~kqi!0 zmQFuF01B3jS-tn$!D6PF5qY}i>syUax&IJs8nMPNSE9dJ&|gpkQ27acXK|NVvRzte40KnqMhu%x*f)?`d#b!QS?AK~o^l;ykGQSH1V50*cJ!8`NN@Q!L20y?bpuF=}c{e2z!Q4!Bu-r4)&Y_q$@k2w`4UroZdMnZm; ztR39~-__J6a%Pvaj2ecOjM4%>m}GOc~v-=@_Xaiu16>t zx$9?*kh5Vmt_O`t_*$5MbmLDe1n7E;r#tI?GJ$+fbQZu8xXZY@ zewZ9^6U6Fi^ZU5~EMs4^6>R4#EpOQ1*{f;eyXrHI9=r6R+GNH6tha-~od?`Opo~b3 zGuJTQUGSr}{`K1nGGsS5kiY|EzK>0pK~L}{+$TJP^4%bZ@;x{bpD?eOT+4@+AvO}W zHl$+ka}asALtwetoONhO)XFd{tr-BGP5TM_t5|HUoG#FBE=$ZZ`Q`u0g^3edrj6^J z9oJWHAbRMjc!W(vo@C6aMfDr~;t0sWW!e8oH(H--a8$&HDqEhpc)q0JCC`UXC;S4^ z`!9iz{!8>n(YOuZY_Vfepcq|TGIIvt^cUembiBZXn1Is(qY_geiC|xe zgs3oWc}S$-L4LJJ3y3S++fB-|g?>@i2CF}w1wpl3MS6$`Legvf!;n;>buPtQW2W+)aWUh~1nYEbpdSJtYHJZ$G))#rU$Kx-{%d zxgxghH!;ROELd3kr8NW@DVOq8GL6GS7NP>=Ozdw9sYtvl>VDjP!aPu#VgBz~$o}0k zILgc)jB8y%kzsk>d(k-%?o!JBmpF8pm-)@}en~!2W8P)H6(RzHK)fX0%Jfdk#sL%U z=TC5qizC_>aC}K^170uq$+i4k1HLh9i+(=hQ}%_hQxps0C1rUhdqYYxJo54(5>;B6 z1fXj`(N3c9J4K*KYLr>QC#YM1^{{7KU3ancJ2P>n$j0falO8x=;E)TI> zmBCQ8rR3G(z5#WSESA;Ky0~C<(+FTY^efNZJuAHEVOHO+S&HJ84eGk;q!U3XitS;etUWzmS&oKYkjXUd&$v(vvWeoqSKn~<{XPCih zHC1VKb9Bb08VnXsj$y!Z$m>+v#k|K?Dj8N9_-g?QC)A3&t59meDsf=15;+CKP8URj zQZI5;DVVbO6W5V@t z10|Z4dm}%!I>0GoDIDmIsWUa-(AKnjbBjr|_`T)Zy3lzLWsS>#_pjhIn98ffFy0Pm z;f~`t{!%CJ8GXglLEHi$zPasySh@c}v%ZUuK27CI{n-Q7(!#52oBa#dIFS6iUDX(l3d|>$GnkVSJQ$D~ZBT8cb3c zd;;o2^r)iS;^Mhqz6pT8#dLez`y;fEJ})Vt8d6 ziXLpFerHbNQATgJ;O;%Jm9lMQuN4k6Nfo^}DkVRt{!MCFp5@`SY8=G7=)F(%{U62*Fysn^&ZVhZU3K+YSA=(vp zMf&@(!C@MO-}=Bh?`};rf?~m`Ark+Xcnq!3nzGjRX9=YF$oFDP0)TY@*Bf6^>~(Uw zsfCXT4ONbXt<>-rDz}32F5}P?aIuoq8`n3JYEsd77r^=>UdTi2h(ckO_pgET5$&C_ zgB#?-k${g)3;kF1bkPjE-vrJYZ}tSY;i@WGwA*i&)ihA+3a={v(r z`UTxg?w4VVFMxSnoiuP-T>xJ;ZjA=S-^%*@0zhD#;8LDBmE>nUC+fphP{H)H&E?#fgu;dl zk0gj_0toyq#7Ks7zDwl9u<+>bf?1!OZqYH-nEr0Ij!cLD+?@?p}jaK*Y zK98_cuWS{++jk_q-ETMM0J}9+$sKJAa?2NKRui*!Kq{>lrqYr3#&y7cxm;A`5qm5T zL(&sQ;Mw%ro%oRtUBGQi9U}gM3uZGiYDyMT8+KjInWMmwN9Qu8!_h|wh9N60CFjOg zFB_9x;WVY?4h2vk1roW7N3Tc+90t_|Q-#6xR0oZlGht4r*elL_Vq);BPLZ>-81wQ> za5`H2wWn%Z`}*er9c@J1DT(Q1!bG zFLV(j7NJEVEN_z79Ep!7hHyF=*d}i{%{!#NQ&{0Ilr-QDt2uqYNOYhwjTd`#<0pZ5OYW{^qZ(JPRZ4u#bUG!Wts;j#eWtWd=G?w}EzaYz+gBZ@67&)>IPf>fmTAmE}&^Dk1_S79?8}4*E2I-^+jD>za zhpaJ_Adc!t=1}lw)qr?cE!e+@1ZtabsjT-#l9xn{y@k*|wOJQ(UU&LYM>wsU$FF%^ zD0dWG;5I(WAl?F}v*dvhAOAh+LbEE~RS2K9_gwENk)rTb^Dy++p=tF2pm$dGx^Y?a z#iJ?oBGPU;8Sf_B@nwP%e)2FpM+em+H**A1UiZ2@!F`u} zr)4euZiWDhJwaVDSCHding-k`H(Wdd5kdaLrz*!*c^upbI$@HyVE|UkxGl&K^7=16 zN4p#%;b`FfYr?dif)cl?B82U2^W%R=LqOv7Gxonz?`rs`?U>-b@Ron<-28gBBhEjj zy$Aw}#?u0WI|0APxa)EFPuJ*+t=3RbRYD`996h4kW^pZ+h9Sw((~UZkL2T7c7iDI@ z46KGyyX?dSja3umJE*1Aj|5xj?(b;`PUW|Y{#1hVkDumY`YpW{XCQm1Q5VPn%xk%C zFyH{}YAKZ82YW)n>HCFx_`0p<`Yly1e_*Bi-Dh4nBT+-q9ZsN<16l>MKB`&y_S;u7 z5t#K9kV(ce*Qn2m;Z7F-9I``g4z>YP-{zZ*_LAsB!Ex<-N}PIolu6bKp<_^?AmGiH zZ2Yr1eN(6I`LSzNfTG_*k?9c$U*kDoC+Ct=@_4evShd=flq~Ed)|%2x+%W|l2Vubk zW=&HtIU$E)obdI&{jnrgF*q=U7vZ3wP;*AS8g}RUk!^M6sQ%D3j zf3f+{$#LI8SxBI#C7vbV!^v}EtSkpex(22=&6HtvDZyVfMlgP0LcL#sou>l&?X0l){vVR}R zaf$-Ptr&DeEPjYC&R8>KWI+zq(zV`%H2$1hn~W&#U4sBKrGjRLZ}F>n`ZEL0-=Ia_ z=DLL&xbuXZ{Da4?-pI*F%({-!&PzKnUMrF;!(ZQ)-hE)?Ql~uQhd1TM-u4DJj{r82 z#w~r}zLR`oAePx%V0x;XXq$DV>{oiUE-=xZDLB8tk6y}!|BU#8$m5I6_3grdx7a&K zb%6TZzLNnG0MNTHyq%l<^X6(9QNRLBn{f;8sU9n|`OSx2ZiwK7lFj=Y?;EVY;1$6s z39E({CcoHv9w9m63oUzo88&0bCM&cfLBuXp22^c@iI*YI#Al_XA*c@--9gt-&$|9+% z-uFyci83tQP~Y=ODDxN*^(*E^>O1(9tT3DE%X18aP+6i~ly9Fz~|87Xnd z2W5c&qh$~BRM({$&@A;lz zX4wad*){0^a-slILqo|v(G!>knu$^-(@%KMap798a{0X(fx_!YXDQvx^d;4DUa-4wz zZ|d0sbe9I7HARQir&2Yg0F1cXevZoiXK6rhrE6SIf%m5P`}+jHYW$*Pf`jWH{=^-o zs`WAdyhU#Y>bFo}5EgQEO{PLcn$()J#**32z15eHdkR}Xt<;XKHlqgYVXw2_rsj;& zV_m&Jxe^*u04QGE&F{(Mkv%%xDVe;Zp=!FPd?cy`%kzTiul%wQ!{B9cb5Extm$~~w zS=%b^=m0gw=sNNaMJBoU$5XklpBdU^9^9+sENuW>l@nK+ELuR>#~wg5on0b&tg6z; zYipbF--A!a*f$QSD|e{fB+woaf@}AFH%D!H3h|%JZmmJtHw}2ap)w9n~|NaT_T)-atH6{yCy9;S5BvxIe^%otq+io5X^E!-2ibmtV0PmP; zaq_iZ&Ge1Za?>2nP9v7!S77=2u;M#dH`8E>HfR+*KXRnc&ez}5oqw;w+-Kgoh<%7NHm-MEr%A~?hD0D za2)GfgruC+=o%MkoTJ+!;%}8L*Ddb2lH7;iv+|q}3*Z>H>(@eQYg- z-)<9~OsJ;rxaOo%DHq{BHWwUmqaYr#yEi^MX|fyU?f&|mn8hs}~`9;JPMxOHL3*M!)$t!Q(9Lu57c*FG;s3X<}?-Wq5f&=Q% zW+?M)gA{iA*Lqoz+Y#`?P9QZ6nEg)IAS%Mg zaVpuj`$tYKhJmitzrfXEAwUX9$(`-Cln&ys>o#t#>l!dNJKg!tB8IuJBFT4cLLC54 zDooFv_TT<%S>iSF%xy}!migxQbUwsQftPiYqkX(Ww?Tog*HEYXM$MQD2j3W9^D7JV z^_8a30bBDjGw+cfYaedeRV5QKe+}QDXF}2e*4Y5*%6mUL@X&sK_lk3wc*n1YW2`_b zvIvyXQVa=n9egl0{5|x?!+EB!L=ONKnPi0KB>dd%6w|tO`1XCm47}9GCWSrSkYTrw znR%N~l-<>5ei*NKI{ixV35+4>)rJMer)(Lf`usWoyr@G&&9SAn@#b#R$p|cnEiLP_ zkl#6EZxYIs(DzImS|a`U1{_s@q>)JTM*dZ$GWlQT%jMK@y_HWou7`NG1T0%*JnFK_ zqD3}l&v})>w1t$!H1-~WT#SyrAXX(4*!nhT2W7MqTh7#E&${jW8QsyKlgNe&=;@Sp zc%hoRs_9ip(GHv*oHsJ%m*K}cMGqhz^eI$1$07EJ^h@s9!*=KAPBSf|JGq35;O6yx zBQ#D}WP^xHX`(mFzJdG3D_TV2*RcntXhYo}e0MxHl(mPq&rUxnU84o~zB8j>n2iEg zS&B5(XPdf;a>7NOLCqJuPiiAs+U7@pH_KxRHei67@H3Z(dO|s7_jqHaZ9_hZYRTWX zRI-5C9_~C*((I~&HWZ&dj$rqCj0H8oXhcd=HGTZ&TuCl%1!!g#_IeScpIiB9KCq zl{D#9p1r<5f5Pa@FFR+cKUP7=-=WefM_2g0sU&7f8fG>YevEtxn@(YQF@GwN2wb=B zw@9eAO8NB~THXVr;suk>oqSxI6CmALN6qe26A*M_+3R%CZFMMv#yRCEn5_zf$A3;{ zGdlDQT%)@f;qMzfqb{s^9er$z4pB4p{S_fyU69o!w?8+xEuO!z_F8Bk$Td;vtN-B| zC0Atzn$To93HugMzHiINE2ZPthU|gB@b&^U`WcXD&fcaxBEYvSe5qA>l~L->d6Q|e zMm~I^b4G6bw&85GIkC5gGOdIj+7{Cc7WR_)_1ymVz)JL*U$inCY^65Dz$XOv!ySEX z`RdJC%eK?zsB0bDt%E!%fJ-1uPam>xz;}?^G+j46+W^i8b@TD@bycD8drw+pNPpYA zh22NWmNd^z{wFBgjZM*o?r4zjo=n2HhnOIl-At?fu|yBdC5V9}j~h5Xn{3 zw^VI6f7W}5Dhn?aUoZhIZgP*M{5wyn*$z-3bk9OETINWXbA{Gp8VYiZ%-0f`fo5@Z zGp@IS^~$wwYt|{P52mc_w0vSpn(L9g?jll2_*;w>hS!bwydR$-&?dsC**DU45S z<=CS1@Ea)7*Gl@5$8#xEMvkLlq7fP`4>i%aeI$`+9@If*n7Smc4|1{K;?A-!fw|zi z;o>RdFtRXro2C`AHka%nnQCFt9%I}4vabl>%F904L53Bp=txpGqC|%`Jwy9IVbF4i zwyDVGG}(-rJj)B$+q)2ZmB#G8AYOn)cpwWFYj~WgtZhd?uJBXFm{C6t1Y4*|I_<~> zG8pc<*l>O=!@JuVvA^_%6`VarHR|dJ1dZTk8=L+5PB-IyeVecU^mG)q0qNNLge10| zcH@=MUW%2E;A(bNL&q1KfC^kB`^hEVT`g(qGhHj^LTtB9W$WIc`z09liYj`7G0{Wk zwNmDEzHHXcxI>_Dgi5%MXQu^6R?CKFO)v*q?(Mum~D zVxouYcmK!}v%F3LQ^9+;`j6u_CtL7qaU04ze8)$2*>Fzdxi~Cy*z_(EwY5yK>#@At;!@INXvEv+=hqSwz9E?I=UV8|VOa2Blyv7pV*@!?e0hgYXZ)8YCFUs^C1xvX| zeq>0!ZDx0p{sw10y>aOHiX^tja-%0prf&dRCe&xd2^-3w5znC}85zLPEyWAQWN zAol_p!q}m_U?;b6z0cZ2^YCmkjcr0v(%c@Q)4ESAGlweEo-f+BGwlI5nGQ7O-n%XE zF*ryd_jyF-nE84M*RnbH`r4p%jx_&@Rq~W0JC?cP_kj-FS$CP=O0G(kE96?}UZUXI zz+@c0GTQH!u$@8YhInsak*?sBoqN)Ng# zVe%*Bj3h{t`*ScC>B6$>R>@_Q+0AOXJhQXHT-WF<%~?T>WT)zNvPxdZQoyum#!%v# zV-c_JSQ=;wtPLdImW;4VaJe?CYACH$3mwXT^B10(86-HQWvIDquAneV@uuSVE` z(mq}STzoNH*k#)A#}Pn{e)TMY-ghp)5?R+a4tC3tV{T9zkWo|Um1!&#=(ZN+J)U0v zdiA11X6!q6{~#A0%WN6eAjnw)?|2PC^mm*+s7kl0vYiGY(F>ZKFFoYo9Q4um0bwoX zPkGFAv%1Ui86zi2Z`%Q#_MrMO{Xp6{zk2ek=lum2FTTMgd%bM?@N773ojG(diSK_J;!MX_sBrYR%v zVpdw|&i!oF>jtD7Rng`O>3hbwME05L2#y1{>dQ<^C-7w5z>UeF&}#9f)_>%}vqe81 zyy%#qjsBI(SPay2ilJGV_3O)XRd%;cKEB${Bq*x%= zhMqoDf6sih|E%sy;6<@oxAMuOPuSWSxn=T%Y6JuS?cDB$7pPYJw#4x&^UKmf-6+~$ z9R?Xa!=S{hK&0s=Ql_eLYGB0MHVz1)+?e32bU9MBu+JFF;}4y&FQ2C$6u4;=H?)Q; z%X^>W_=cwCzi_~$tw97{oM2ZE@=bX`v%0b{iyIuhGE4ZYt+m<<9%;?& z#A*}8clb`5@N3SGreRwZwl2>X^rFN1>cE6$I9FoQ%w;d^$CYS%UDhb#}XU*pR1 z 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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "soh/OTRGlobals.h" +#include "soh/SohGui/SohGui.hpp" +#include "soh/SohGui/SohMenu.h" +#include "soh/SohGui/UIWidgets.hpp" +#include "soh/cvar_prefixes.h" + +#include + +namespace SohGui { +extern std::shared_ptr mSohMenu; +} + +namespace Mapper { + +using namespace UIWidgets; + +static const char* kUserMappingsFileName = "usergamepadmappings.txt"; + +static const char* kDiagramTextureName = "Gamepad-Mapper-Diagram"; +static const char* kDiagramTexturePath = "textures/mapping-xbox.png"; + +// --------------------------------------------------------------------------------------------------- +// Binding metadata +// --------------------------------------------------------------------------------------------------- + +static const char* kButtonNames[SDL_CONTROLLER_BUTTON_MAX] = { + "A", + "B", + "X", + "Y", + "Back", + "Guide", + "Start", + "Left Stick Click", + "Right Stick Click", + "Left Bumper", + "Right Bumper", + "D-Pad Up", + "D-Pad Down", + "D-Pad Left", + "D-Pad Right", + "Misc 1", + "Paddle 1", + "Paddle 2", + "Paddle 3", + "Paddle 4", + "Touchpad", +}; + +static const char* kAxisNames[AXIS_BINDING_MAX] = { + "Left Stick Left", "Left Stick Right", "Left Stick Up", "Left Stick Down", "Right Stick Left", + "Right Stick Right", "Right Stick Up", "Right Stick Down", "Left Trigger", "Right Trigger", +}; + +const char* BindingName(int32_t binding) { + if (binding < 0 || binding >= BINDING_COUNT) { + return "?"; + } + if (binding < SDL_CONTROLLER_BUTTON_MAX) { + return kButtonNames[binding]; + } + return kAxisNames[binding - SDL_CONTROLLER_BUTTON_MAX]; +} + +struct StickAxisPair { + SDL_GameControllerAxis axis; + int32_t negative; + int32_t positive; +}; + +static const StickAxisPair kStickAxes[] = { + { SDL_CONTROLLER_AXIS_LEFTX, AXIS_LEFTX_NEGATIVE, AXIS_LEFTX_POSITIVE }, + { SDL_CONTROLLER_AXIS_LEFTY, AXIS_LEFTY_NEGATIVE, AXIS_LEFTY_POSITIVE }, + { SDL_CONTROLLER_AXIS_RIGHTX, AXIS_RIGHTX_NEGATIVE, AXIS_RIGHTX_POSITIVE }, + { SDL_CONTROLLER_AXIS_RIGHTY, AXIS_RIGHTY_NEGATIVE, AXIS_RIGHTY_POSITIVE }, +}; + +static const int32_t kCoreBindingOrder[] = { + SDL_CONTROLLER_BUTTON_A, + SDL_CONTROLLER_BUTTON_B, + SDL_CONTROLLER_BUTTON_X, + SDL_CONTROLLER_BUTTON_Y, + SDL_CONTROLLER_BUTTON_BACK, + SDL_CONTROLLER_BUTTON_START, + SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SDL_CONTROLLER_BUTTON_LEFTSTICK, + SDL_CONTROLLER_BUTTON_RIGHTSTICK, + SDL_CONTROLLER_BUTTON_DPAD_UP, + SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SDL_CONTROLLER_BUTTON_DPAD_RIGHT, + SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTX_NEGATIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTX_POSITIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTY_NEGATIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTY_POSITIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTX_NEGATIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTX_POSITIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTY_NEGATIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTY_POSITIVE, + SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERLEFT, + SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERRIGHT, +}; + +static const int32_t kExtraBindings[] = { + SDL_CONTROLLER_BUTTON_GUIDE, SDL_CONTROLLER_BUTTON_MISC1, SDL_CONTROLLER_BUTTON_PADDLE1, + SDL_CONTROLLER_BUTTON_PADDLE2, SDL_CONTROLLER_BUTTON_PADDLE3, SDL_CONTROLLER_BUTTON_PADDLE4, + SDL_CONTROLLER_BUTTON_TOUCHPAD, +}; + +// --------------------------------------------------------------------------------------------------- +// Binding helpers +// --------------------------------------------------------------------------------------------------- + +static ExtendedBind NoBind() { + ExtendedBind bind = {}; + bind.bindType = SDL_CONTROLLER_BINDTYPE_NONE; + return bind; +} + +void ClearBindings(ExtendedBind (&bindings)[BINDING_COUNT]) { + for (int32_t i = 0; i < BINDING_COUNT; i++) { + bindings[i] = NoBind(); + } +} + +bool HasAnyBinding(const ExtendedBind (&bindings)[BINDING_COUNT]) { + for (int32_t i = 0; i < BINDING_COUNT; i++) { + if (bindings[i].bindType != SDL_CONTROLLER_BINDTYPE_NONE) { + return true; + } + } + return false; +} + +static bool IsFullRangeAxis(const ExtendedBind& bind) { + if (bind.bindType != SDL_CONTROLLER_BINDTYPE_AXIS) { + return false; + } + return (bind.value.axis.axisMin == SDL_JOYSTICK_AXIS_MIN && bind.value.axis.axisMax == SDL_JOYSTICK_AXIS_MAX) || + (bind.value.axis.axisMin == SDL_JOYSTICK_AXIS_MAX && bind.value.axis.axisMax == SDL_JOYSTICK_AXIS_MIN); +} + +static bool MergeAxisBindings(ExtendedBind (&bindings)[BINDING_COUNT], int32_t negative, int32_t positive) { + ExtendedBind& a = bindings[negative]; + ExtendedBind& b = bindings[positive]; + if (a.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && b.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && + a.value.axis.axis == b.value.axis.axis && a.value.axis.axisMin == b.value.axis.axisMin) { + a.value.axis.axisMin = a.value.axis.axisMax; + a.value.axis.axisMax = b.value.axis.axisMax; + b = NoBind(); + return true; + } + return false; +} + +static std::string EncodeBindElement(const ExtendedBind& bind) { + switch (bind.bindType) { + case SDL_CONTROLLER_BINDTYPE_BUTTON: + return StringHelper::Sprintf("b%d", bind.value.button); + case SDL_CONTROLLER_BINDTYPE_AXIS: + if (bind.value.axis.axisMin == 0 && bind.value.axis.axisMax == SDL_JOYSTICK_AXIS_MIN) { + return StringHelper::Sprintf("-a%d", bind.value.axis.axis); + } + if (bind.value.axis.axisMin == 0 && bind.value.axis.axisMax == SDL_JOYSTICK_AXIS_MAX) { + return StringHelper::Sprintf("+a%d", bind.value.axis.axis); + } + return StringHelper::Sprintf("a%d%s", bind.value.axis.axis, + bind.value.axis.axisMin > bind.value.axis.axisMax ? "~" : ""); + case SDL_CONTROLLER_BINDTYPE_HAT: + return StringHelper::Sprintf("h%d.%d", bind.value.hat.hat, bind.value.hat.hatMask); + default: + return ""; + } +} + +static const char* HatMaskName(int32_t mask) { + switch (mask) { + case SDL_HAT_UP: + return "Up"; + case SDL_HAT_DOWN: + return "Down"; + case SDL_HAT_LEFT: + return "Left"; + case SDL_HAT_RIGHT: + return "Right"; + case SDL_HAT_LEFTUP: + return "Up-Left"; + case SDL_HAT_RIGHTUP: + return "Up-Right"; + case SDL_HAT_LEFTDOWN: + return "Down-Left"; + case SDL_HAT_RIGHTDOWN: + return "Down-Right"; + default: + return "?"; + } +} + +std::string DescribeBind(const ExtendedBind& bind) { + switch (bind.bindType) { + case SDL_CONTROLLER_BINDTYPE_BUTTON: + return StringHelper::Sprintf("Button %d", bind.value.button); + case SDL_CONTROLLER_BINDTYPE_AXIS: + if (IsFullRangeAxis(bind)) { + return StringHelper::Sprintf("Axis %d%s", bind.value.axis.axis, + bind.value.axis.axisMin > bind.value.axis.axisMax ? " inv" : ""); + } + return StringHelper::Sprintf("Axis %d %s", bind.value.axis.axis, + bind.value.axis.axisMax < bind.value.axis.axisMin ? "-" : "+"); + case SDL_CONTROLLER_BINDTYPE_HAT: + return StringHelper::Sprintf("Hat %d %s", bind.value.hat.hat, HatMaskName(bind.value.hat.hatMask)); + default: + return ""; + } +} + +// --------------------------------------------------------------------------------------------------- +// Mapping string generation / parsing +// --------------------------------------------------------------------------------------------------- + +static std::string SanitizeName(const std::string& name) { + std::string result = name; + std::replace(result.begin(), result.end(), ',', ' '); + return result; +} + +std::string GenerateMappingString(SDL_Joystick* joystick, const ExtendedBind (&bindings)[BINDING_COUNT], + const std::string& nameOverride) { + if (joystick == nullptr) { + return ""; + } + + SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); + uint16_t crc = 0; +#if SDL_VERSION_ATLEAST(2, 26, 0) + SDL_GetJoystickGUIDInfo(guid, nullptr, nullptr, nullptr, &crc); + if (crc != 0) { + guid.data[2] = 0; + guid.data[3] = 0; + } +#endif + + char guidString[33] = ""; + SDL_JoystickGetGUIDString(guid, guidString, sizeof(guidString)); + + const char* rawName = SDL_JoystickName(joystick); + std::string name = SanitizeName(nameOverride); + if (name.find_first_not_of(' ') == std::string::npos) { + name = SanitizeName(rawName != nullptr ? rawName : guidString); + } + + std::string mapping = StringHelper::Sprintf("%s,%s,platform:%s,", guidString, name.c_str(), SDL_GetPlatform()); + if (crc != 0) { + mapping += StringHelper::Sprintf("crc:%.4x,", crc); + } + + ExtendedBind work[BINDING_COUNT]; + for (int32_t i = 0; i < BINDING_COUNT; i++) { + work[i] = bindings[i]; + } + + for (int32_t i = 0; i < SDL_CONTROLLER_BUTTON_MAX; i++) { + if (work[i].bindType == SDL_CONTROLLER_BINDTYPE_NONE) { + continue; + } + const char* name = SDL_GameControllerGetStringForButton((SDL_GameControllerButton)i); + if (name == nullptr) { + continue; + } + mapping += StringHelper::Sprintf("%s:%s,", name, EncodeBindElement(work[i]).c_str()); + } + + for (const auto& stick : kStickAxes) { + const int32_t negative = SDL_CONTROLLER_BUTTON_MAX + stick.negative; + const int32_t positive = SDL_CONTROLLER_BUTTON_MAX + stick.positive; + const char* name = SDL_GameControllerGetStringForAxis(stick.axis); + if (name == nullptr) { + continue; + } + + MergeAxisBindings(work, negative, positive); + + if (work[negative].bindType != SDL_CONTROLLER_BINDTYPE_NONE) { + const char* prefix = IsFullRangeAxis(work[negative]) ? "" : "-"; + mapping += StringHelper::Sprintf("%s%s:%s,", prefix, name, EncodeBindElement(work[negative]).c_str()); + } + if (work[positive].bindType != SDL_CONTROLLER_BINDTYPE_NONE) { + mapping += StringHelper::Sprintf("+%s:%s,", name, EncodeBindElement(work[positive]).c_str()); + } + } + + const std::pair triggers[] = { + { SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERLEFT, SDL_CONTROLLER_AXIS_TRIGGERLEFT }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERRIGHT, SDL_CONTROLLER_AXIS_TRIGGERRIGHT }, + }; + for (const auto& trigger : triggers) { + if (work[trigger.first].bindType == SDL_CONTROLLER_BINDTYPE_NONE) { + continue; + } + const char* name = SDL_GameControllerGetStringForAxis(trigger.second); + if (name == nullptr) { + continue; + } + mapping += StringHelper::Sprintf("%s:%s,", name, EncodeBindElement(work[trigger.first]).c_str()); + } + + return mapping; +} + +static int32_t BindingIndexForKey(const std::string& key) { + std::string name = key; + char sign = 0; + if (!name.empty() && (name.front() == '+' || name.front() == '-')) { + sign = name.front(); + name.erase(0, 1); + } + + const SDL_GameControllerAxis axis = SDL_GameControllerGetAxisFromString(name.c_str()); + if (axis != SDL_CONTROLLER_AXIS_INVALID) { + switch (axis) { + case SDL_CONTROLLER_AXIS_LEFTX: + return SDL_CONTROLLER_BUTTON_MAX + (sign == '+' ? AXIS_LEFTX_POSITIVE : AXIS_LEFTX_NEGATIVE); + case SDL_CONTROLLER_AXIS_LEFTY: + return SDL_CONTROLLER_BUTTON_MAX + (sign == '+' ? AXIS_LEFTY_POSITIVE : AXIS_LEFTY_NEGATIVE); + case SDL_CONTROLLER_AXIS_RIGHTX: + return SDL_CONTROLLER_BUTTON_MAX + (sign == '+' ? AXIS_RIGHTX_POSITIVE : AXIS_RIGHTX_NEGATIVE); + case SDL_CONTROLLER_AXIS_RIGHTY: + return SDL_CONTROLLER_BUTTON_MAX + (sign == '+' ? AXIS_RIGHTY_POSITIVE : AXIS_RIGHTY_NEGATIVE); + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERLEFT; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERRIGHT; + default: + return -1; + } + } + + if (sign != 0) { + return -1; + } + const SDL_GameControllerButton button = SDL_GameControllerGetButtonFromString(name.c_str()); + return button != SDL_CONTROLLER_BUTTON_INVALID ? (int32_t)button : -1; +} + +static bool DecodeBindElement(const std::string& value, ExtendedBind& out) { + std::string element = value; + bool inverted = false; + if (!element.empty() && element.back() == '~') { + inverted = true; + element.pop_back(); + } + + char sign = 0; + if (!element.empty() && (element.front() == '+' || element.front() == '-')) { + sign = element.front(); + element.erase(0, 1); + } + if (element.size() < 2) { + return false; + } + + out = {}; + out.committed = true; + + switch (element.front()) { + case 'b': + out.bindType = SDL_CONTROLLER_BINDTYPE_BUTTON; + out.value.button = std::atoi(element.c_str() + 1); + return true; + case 'a': + out.bindType = SDL_CONTROLLER_BINDTYPE_AXIS; + out.value.axis.axis = std::atoi(element.c_str() + 1); + if (sign == '-') { + out.value.axis.axisMin = 0; + out.value.axis.axisMax = SDL_JOYSTICK_AXIS_MIN; + } else if (sign == '+') { + out.value.axis.axisMin = 0; + out.value.axis.axisMax = SDL_JOYSTICK_AXIS_MAX; + } else { + out.value.axis.axisMin = inverted ? SDL_JOYSTICK_AXIS_MAX : SDL_JOYSTICK_AXIS_MIN; + out.value.axis.axisMax = inverted ? SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX; + } + return true; + case 'h': { + int32_t hat = 0; + int32_t mask = 0; + if (std::sscanf(element.c_str() + 1, "%d.%d", &hat, &mask) != 2) { + return false; + } + out.bindType = SDL_CONTROLLER_BINDTYPE_HAT; + out.value.hat.hat = hat; + out.value.hat.hatMask = mask; + return true; + } + default: + out = NoBind(); + return false; + } +} + +int32_t ParseMappingString(const std::string& mapping, ExtendedBind (&bindings)[BINDING_COUNT]) { + ClearBindings(bindings); + + int32_t recovered = 0; + size_t start = 0; + while (start <= mapping.size()) { + const size_t end = mapping.find(',', start); + const std::string field = mapping.substr(start, end == std::string::npos ? std::string::npos : end - start); + start = end == std::string::npos ? mapping.size() + 1 : end + 1; + + const size_t colon = field.find(':'); + if (colon == std::string::npos) { + continue; + } + + const int32_t index = BindingIndexForKey(field.substr(0, colon)); + if (index < 0) { + continue; + } + if (DecodeBindElement(field.substr(colon + 1), bindings[index])) { + recovered++; + } + } + + for (const auto& stick : kStickAxes) { + const int32_t negative = SDL_CONTROLLER_BUTTON_MAX + stick.negative; + const int32_t positive = SDL_CONTROLLER_BUTTON_MAX + stick.positive; + if (!IsFullRangeAxis(bindings[negative]) || bindings[positive].bindType != SDL_CONTROLLER_BINDTYPE_NONE) { + continue; + } + + const bool axisInverted = bindings[negative].value.axis.axisMin > bindings[negative].value.axis.axisMax; + bindings[positive] = bindings[negative]; + bindings[negative].value.axis.axisMin = 0; + bindings[negative].value.axis.axisMax = axisInverted ? SDL_JOYSTICK_AXIS_MAX : SDL_JOYSTICK_AXIS_MIN; + bindings[positive].value.axis.axisMin = 0; + bindings[positive].value.axis.axisMax = axisInverted ? SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX; + } + + return recovered; +} + +// --------------------------------------------------------------------------------------------------- +// MappingSession +// --------------------------------------------------------------------------------------------------- + +MappingSession::MappingSession() + : mBindings{}, mCurrentStep(0), mPendingAdvanceTime(0), mActive(false), mComplete(false) { + ClearBindings(mBindings); +} + +void MappingSession::Start(SDL_Joystick* joystick, const std::vector& order) { + mOrder = order; + mCurrentStep = 0; + mComplete = mOrder.empty(); + mActive = !mOrder.empty(); + mPendingAdvanceTime = 0; + + const int32_t axisCount = joystick != nullptr ? SDL_JoystickNumAxes(joystick) : 0; + mAxisState.assign(axisCount > 0 ? (size_t)axisCount : 0, AxisState{}); + for (int32_t i = 0; i < axisCount; i++) { + Sint16 initialValue = 0; + mAxisState[i].moving = SDL_JoystickGetAxisInitialState(joystick, i, &initialValue) == SDL_TRUE; + mAxisState[i].lastValue = initialValue; + mAxisState[i].startingValue = initialValue; + mAxisState[i].farthestValue = initialValue; + } + + if (mActive) { + mBindings[mOrder[0]] = NoBind(); + } +} + +void MappingSession::Stop() { + mActive = false; + mComplete = false; + mOrder.clear(); + mAxisState.clear(); + mPendingAdvanceTime = 0; +} + +bool MappingSession::IsActive() const { + return mActive; +} + +bool MappingSession::IsComplete() const { + return mComplete; +} + +int32_t MappingSession::CurrentStep() const { + return mCurrentStep; +} + +int32_t MappingSession::StepCount() const { + return (int32_t)mOrder.size(); +} + +int32_t MappingSession::CurrentBinding() const { + if (mCurrentStep < 0 || mCurrentStep >= (int32_t)mOrder.size()) { + return -1; + } + return mOrder[mCurrentStep]; +} + +void MappingSession::GoToStep(int32_t step) { + if (step < 0) { + return; + } + if (step >= (int32_t)mOrder.size()) { + mComplete = true; + mActive = false; + return; + } + + mCurrentStep = step; + + mBindings[mOrder[mCurrentStep]] = NoBind(); + for (auto& state : mAxisState) { + state.farthestValue = state.startingValue; + } + mPendingAdvanceTime = 0; +} + +void MappingSession::Advance() { + GoToStep(mCurrentStep + 1); +} + +void MappingSession::GoBack() { + GoToStep(mCurrentStep - 1); +} + +const ExtendedBind& MappingSession::GetBinding(int32_t index) const { + return mBindings[index]; +} + +void MappingSession::SetBindings(const ExtendedBind (&bindings)[BINDING_COUNT]) { + for (int32_t i = 0; i < BINDING_COUNT; i++) { + mBindings[i] = bindings[i]; + } +} + +void MappingSession::CopyBindingsTo(ExtendedBind (&out)[BINDING_COUNT]) const { + for (int32_t i = 0; i < BINDING_COUNT; i++) { + out[i] = mBindings[i]; + } +} + +bool MappingSession::BindingContainsBinding(const ExtendedBind& a, const ExtendedBind& b) const { + if (a.bindType != b.bindType) { + return false; + } + + if (a.bindType == SDL_CONTROLLER_BINDTYPE_AXIS) { + if (a.value.axis.axis != b.value.axis.axis || !a.committed) { + return false; + } + const int32_t minA = std::min(a.value.axis.axisMin, a.value.axis.axisMax); + const int32_t maxA = std::max(a.value.axis.axisMin, a.value.axis.axisMax); + const int32_t minB = std::min(b.value.axis.axisMin, b.value.axis.axisMax); + const int32_t maxB = std::max(b.value.axis.axisMin, b.value.axis.axisMax); + return minA <= minB && maxA >= maxB; + } + + return std::memcmp(&a, &b, sizeof(a)) == 0; +} + +void MappingSession::ConfigureBinding(const ExtendedBind& binding) { + const int32_t currentElement = CurrentBinding(); + if (currentElement < 0) { + return; + } + + for (int32_t i = 0; i < BINDING_COUNT; i++) { + if (BindingContainsBinding(mBindings[i], binding)) { + return; + } + } + + ExtendedBind& current = mBindings[currentElement]; + if (current.bindType != SDL_CONTROLLER_BINDTYPE_NONE) { + const bool nativeDPad = + currentElement == SDL_CONTROLLER_BUTTON_DPAD_UP || currentElement == SDL_CONTROLLER_BUTTON_DPAD_DOWN || + currentElement == SDL_CONTROLLER_BUTTON_DPAD_LEFT || currentElement == SDL_CONTROLLER_BUTTON_DPAD_RIGHT; + if (nativeDPad && current.bindType == SDL_CONTROLLER_BINDTYPE_HAT) { + return; + } + + const bool nativeAxis = currentElement >= SDL_CONTROLLER_BUTTON_MAX; + const bool currentIsAxis = current.bindType == SDL_CONTROLLER_BINDTYPE_AXIS; + if (nativeAxis == currentIsAxis && + (binding.bindType != SDL_CONTROLLER_BINDTYPE_AXIS || binding.value.axis.axis != current.value.axis.axis)) { + return; + } + } + + current = binding; + mPendingAdvanceTime = binding.committed ? SDL_GetTicks() : 0; +} + +void MappingSession::ProcessButtonDown(int32_t button) { + if (!mActive) { + return; + } + ExtendedBind binding = {}; + binding.bindType = SDL_CONTROLLER_BINDTYPE_BUTTON; + binding.value.button = button; + binding.committed = true; + ConfigureBinding(binding); +} + +void MappingSession::ProcessHatMotion(int32_t hat, int32_t value) { + if (!mActive || value == SDL_HAT_CENTERED) { + return; + } + ExtendedBind binding = {}; + binding.bindType = SDL_CONTROLLER_BINDTYPE_HAT; + binding.value.hat.hat = hat; + binding.value.hat.hatMask = value; + binding.committed = true; + ConfigureBinding(binding); +} + +static int32_t StandardizeAxisValue(int32_t value) { + if (value > SDL_JOYSTICK_AXIS_MAX / 2) { + return SDL_JOYSTICK_AXIS_MAX; + } + if (value < SDL_JOYSTICK_AXIS_MIN / 2) { + return SDL_JOYSTICK_AXIS_MIN; + } + return 0; +} + +void MappingSession::ProcessAxisMotion(int32_t axis, int32_t value, SDL_Joystick* joystick) { + if (!mActive || axis < 0 || axis >= (int32_t)mAxisState.size()) { + return; + } + + constexpr int32_t MAX_ALLOWED_JITTER = SDL_JOYSTICK_AXIS_MAX / 80; + AxisState& state = mAxisState[axis]; + + if (!state.moving) { + Sint16 initialValue = 0; + state.moving = SDL_JoystickGetAxisInitialState(joystick, axis, &initialValue) == SDL_TRUE; + state.lastValue = value; + state.startingValue = initialValue; + state.farthestValue = initialValue; + } else if (std::abs(value - state.lastValue) <= MAX_ALLOWED_JITTER) { + return; + } else { + state.lastValue = value; + } + + const int32_t currentDistance = std::abs(value - state.startingValue); + int32_t farthestDistance = std::abs(state.farthestValue - state.startingValue); + if (currentDistance > farthestDistance) { + state.farthestValue = value; + farthestDistance = std::abs(state.farthestValue - state.startingValue); + } + + if (farthestDistance < AXIS_COMMIT_DISTANCE) { + return; + } + + ExtendedBind binding = {}; + binding.bindType = SDL_CONTROLLER_BINDTYPE_AXIS; + binding.value.axis.axis = axis; + binding.value.axis.axisMin = StandardizeAxisValue(state.startingValue); + binding.value.axis.axisMax = StandardizeAxisValue(state.farthestValue); + binding.committed = currentDistance <= AXIS_RETURN_DISTANCE; + ConfigureBinding(binding); +} + +void MappingSession::CheckPendingAdvance() { + if (!mActive || mPendingAdvanceTime == 0) { + return; + } + if (SDL_GetTicks() - mPendingAdvanceTime >= PENDING_ADVANCE_DELAY_MS) { + Advance(); + } +} + +// --------------------------------------------------------------------------------------------------- +// usergamepadmappings.txt +// --------------------------------------------------------------------------------------------------- + +std::string GetUserMappingsPath() { + return Ship::Context::GetPathRelativeToAppDirectory(kUserMappingsFileName); +} + +static std::string StripCrcFromGuid(const std::string& guid) { + if (guid.length() < 32) { + return guid; + } + return guid.substr(0, 4) + "0000" + guid.substr(8); +} + +static std::string MappingKey(const std::string& mapping) { + const size_t comma = mapping.find(','); + if (comma == std::string::npos || comma == 0) { + return ""; + } + std::string key = StripCrcFromGuid(mapping.substr(0, comma)); + + const size_t crcPos = mapping.find(",crc:"); + if (crcPos != std::string::npos) { + const size_t valueStart = crcPos + 1; + size_t valueEnd = mapping.find(',', valueStart); + if (valueEnd == std::string::npos) { + valueEnd = mapping.length(); + } + key += "," + mapping.substr(valueStart, valueEnd - valueStart); + } + return key; +} + +static bool IsMappingLine(const std::string& line) { + return !line.empty() && line.front() != '#'; +} + +static std::vector ReadUserMappingLines() { + std::vector lines; + std::ifstream file(GetUserMappingsPath()); + if (!file.is_open()) { + return lines; + } + + std::string line; + while (std::getline(file, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + lines.push_back(line); + } + return lines; +} + +static bool WriteUserMappingLines(const std::vector& lines) { + const std::string path = GetUserMappingsPath(); + const std::string tempPath = path + ".tmp"; + + { + std::ofstream file(tempPath, std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + SPDLOG_ERROR("Failed to open \"{}\" for writing", tempPath); + return false; + } + file << "# SoH user gamepad mappings, written by the built-in gamepad mapper.\n"; + file << "# Loaded on top of gamecontrollerdb.txt, so entries here win.\n"; + for (const auto& line : lines) { + file << line << "\n"; + } + if (!file.good()) { + SPDLOG_ERROR("Failed while writing \"{}\"", tempPath); + return false; + } + } + + std::error_code error; + std::filesystem::rename(tempPath, path, error); + if (error) { + SPDLOG_ERROR("Failed to move \"{}\" into place: {}", tempPath, error.message()); + std::filesystem::remove(tempPath, error); + return false; + } + return true; +} + +std::string FindUserMapping(const std::string& guid) { + const std::string base = StripCrcFromGuid(guid); + for (const auto& line : ReadUserMappingLines()) { + if (!IsMappingLine(line)) { + continue; + } + const size_t comma = line.find(','); + if (comma != std::string::npos && StripCrcFromGuid(line.substr(0, comma)) == base) { + return line; + } + } + return ""; +} + +std::string MappingName(const std::string& mapping) { + const size_t first = mapping.find(','); + if (first == std::string::npos) { + return ""; + } + const size_t second = mapping.find(',', first + 1); + return mapping.substr(first + 1, second == std::string::npos ? std::string::npos : second - first - 1); +} + +bool LoadUserMappings() { + const std::string path = GetUserMappingsPath(); + + std::error_code error; + if (!std::filesystem::exists(path, error)) { + return false; + } + + const int32_t added = SDL_GameControllerAddMappingsFromFile(path.c_str()); + if (added < 0) { + SPDLOG_ERROR("Failed to add user gamepad mappings from \"{}\": {}", path, SDL_GetError()); + return false; + } + + SPDLOG_INFO("Added {} user gamepad mapping(s) from \"{}\"", added, path); + Ship::Context::GetRawInstance() + ->GetControlDeck() + ->GetConnectedPhysicalDeviceManager() + ->RefreshConnectedSDLGamepads(); + return true; +} + +bool SaveUserMapping(const std::string& mapping) { + const std::string key = MappingKey(mapping); + if (key.empty()) { + SPDLOG_ERROR("Refusing to save a gamepad mapping with no GUID"); + return false; + } + + std::vector lines; + for (const auto& line : ReadUserMappingLines()) { + if (!IsMappingLine(line)) { + continue; + } + if (MappingKey(line) == key) { + continue; + } + lines.push_back(line); + } + lines.push_back(mapping); + + if (!WriteUserMappingLines(lines)) { + return false; + } + + if (SDL_GameControllerAddMapping(mapping.c_str()) < 0) { + SPDLOG_ERROR("SDL rejected the generated mapping \"{}\": {}", mapping, SDL_GetError()); + return false; + } + + Ship::Context::GetRawInstance() + ->GetControlDeck() + ->GetConnectedPhysicalDeviceManager() + ->RefreshConnectedSDLGamepads(); + return true; +} + +bool DeleteUserMapping(const std::string& guid) { + const std::string base = StripCrcFromGuid(guid); + + std::vector lines; + bool found = false; + for (const auto& line : ReadUserMappingLines()) { + if (!IsMappingLine(line)) { + continue; + } + const size_t comma = line.find(','); + if (comma != std::string::npos && StripCrcFromGuid(line.substr(0, comma)) == base) { + found = true; + continue; + } + lines.push_back(line); + } + + if (!found) { + return false; + } + if (!WriteUserMappingLines(lines)) { + return false; + } + + Ship::Context::GetRawInstance() + ->GetControlDeck() + ->GetConnectedPhysicalDeviceManager() + ->RefreshConnectedSDLGamepads(); + return true; +} + +// --------------------------------------------------------------------------------------------------- +// Diagram layout +// --------------------------------------------------------------------------------------------------- + +struct DiagramEntry { + int32_t binding; + ImVec2 hotspot; + bool labelOnRight; +}; + +static constexpr float MAX_IMAGE_SPAN = 0.52f; +static constexpr float MIN_IMAGE_WIDTH = 360.0f; +static constexpr float FALLBACK_ART_ASPECT = 1850.0f / 1302.0f; +static constexpr ImU32 NAME_COLOR = IM_COL32(138, 144, 156, 255); + +static const DiagramEntry kDiagram[] = { + { SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERLEFT, ImVec2(0.081f, 0.110f), false }, + { SDL_CONTROLLER_BUTTON_LEFTSHOULDER, ImVec2(0.234f, 0.092f), false }, + { SDL_CONTROLLER_BUTTON_BACK, ImVec2(0.426f, 0.408f), false }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTY_NEGATIVE, ImVec2(0.241f, 0.315f), false }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTX_NEGATIVE, ImVec2(0.185f, 0.401f), false }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTX_POSITIVE, ImVec2(0.297f, 0.401f), false }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_LEFTY_POSITIVE, ImVec2(0.241f, 0.487f), false }, + { SDL_CONTROLLER_BUTTON_LEFTSTICK, ImVec2(0.241f, 0.401f), false }, + { SDL_CONTROLLER_BUTTON_DPAD_UP, ImVec2(0.368f, 0.540f), false }, + { SDL_CONTROLLER_BUTTON_DPAD_LEFT, ImVec2(0.310f, 0.614f), false }, + { SDL_CONTROLLER_BUTTON_DPAD_RIGHT, ImVec2(0.416f, 0.614f), false }, + { SDL_CONTROLLER_BUTTON_DPAD_DOWN, ImVec2(0.368f, 0.690f), false }, + + { SDL_CONTROLLER_BUTTON_MAX + AXIS_TRIGGERRIGHT, ImVec2(0.919f, 0.110f), true }, + { SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, ImVec2(0.766f, 0.092f), true }, + { SDL_CONTROLLER_BUTTON_START, ImVec2(0.570f, 0.408f), true }, + { SDL_CONTROLLER_BUTTON_Y, ImVec2(0.752f, 0.328f), true }, + { SDL_CONTROLLER_BUTTON_X, ImVec2(0.689f, 0.412f), true }, + { SDL_CONTROLLER_BUTTON_B, ImVec2(0.816f, 0.412f), true }, + { SDL_CONTROLLER_BUTTON_A, ImVec2(0.752f, 0.497f), true }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTY_NEGATIVE, ImVec2(0.628f, 0.518f), true }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTX_NEGATIVE, ImVec2(0.570f, 0.595f), true }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTX_POSITIVE, ImVec2(0.686f, 0.595f), true }, + { SDL_CONTROLLER_BUTTON_MAX + AXIS_RIGHTY_POSITIVE, ImVec2(0.628f, 0.682f), true }, + { SDL_CONTROLLER_BUTTON_RIGHTSTICK, ImVec2(0.628f, 0.595f), true }, +}; + +// --------------------------------------------------------------------------------------------------- +// MapperWindow +// --------------------------------------------------------------------------------------------------- + +#define MAPPER_WINDOW_GAME_INPUT_BLOCK_ID 95237930 + +static constexpr uint32_t DEVICE_SCAN_INTERVAL_MS = 500; +static constexpr uint32_t FILE_WATCH_INTERVAL_MS = 1000; + +MapperWindow::~MapperWindow() { + CloseJoystick(); +} + +void MapperWindow::InitElement() { + ClearBindings(mBindings); +} + +// Records the joystick's current digital state as the baseline for edge detection, so a button already +// held when a prompt appears has to be released and pressed again to count. +void MapperWindow::SnapshotDeviceState() { + mPreviousButtons.clear(); + mPreviousHats.clear(); + if (mJoystick == nullptr) { + return; + } + + SDL_JoystickUpdate(); + for (int32_t i = 0; i < SDL_JoystickNumButtons(mJoystick); i++) { + mPreviousButtons.push_back(SDL_JoystickGetButton(mJoystick, i)); + } + for (int32_t i = 0; i < SDL_JoystickNumHats(mJoystick); i++) { + mPreviousHats.push_back(SDL_JoystickGetHat(mJoystick, i)); + } +} + +void MapperWindow::PollDeviceForSession() { + if (mJoystick == nullptr) { + return; + } + + if (SDL_JoystickGetAttached(mJoystick) == SDL_FALSE) { + EndSession(false); + mStatusMessage = "Device disconnected during mapping."; + return; + } + + SDL_JoystickUpdate(); + + const int32_t buttonCount = SDL_JoystickNumButtons(mJoystick); + mPreviousButtons.resize((size_t)std::max(buttonCount, 0), 0); + for (int32_t i = 0; i < buttonCount; i++) { + const uint8_t state = SDL_JoystickGetButton(mJoystick, i); + if (state != 0 && mPreviousButtons[i] == 0) { + mSession.ProcessButtonDown(i); + } + mPreviousButtons[i] = state; + } + + const int32_t hatCount = SDL_JoystickNumHats(mJoystick); + mPreviousHats.resize((size_t)std::max(hatCount, 0), SDL_HAT_CENTERED); + for (int32_t i = 0; i < hatCount; i++) { + const uint8_t state = SDL_JoystickGetHat(mJoystick, i); + if (state != mPreviousHats[i] && state != SDL_HAT_CENTERED) { + mSession.ProcessHatMotion(i, state); + } + mPreviousHats[i] = state; + } + + for (int32_t i = 0; i < SDL_JoystickNumAxes(mJoystick); i++) { + mSession.ProcessAxisMotion(i, SDL_JoystickGetAxis(mJoystick, i), mJoystick); + } +} + +void MapperWindow::CloseJoystick() { + if (mJoystick != nullptr) { + SDL_JoystickClose(mJoystick); + mJoystick = nullptr; + } + mPreviousButtons.clear(); + mPreviousHats.clear(); +} + +void MapperWindow::RefreshDeviceList() { + const SDL_JoystickID previousInstanceId = + mSelectedDevice >= 0 && mSelectedDevice < (int32_t)mDevices.size() ? mDevices[mSelectedDevice].instanceId : -1; + + mDevices.clear(); + + const std::vector userLines = ReadUserMappingLines(); + + for (int32_t i = 0; i < SDL_NumJoysticks(); i++) { + char guidString[33] = ""; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), guidString, sizeof(guidString)); + + DeviceInfo info = {}; + info.deviceIndex = i; + info.instanceId = SDL_JoystickGetDeviceInstanceID(i); + const char* name = SDL_JoystickNameForIndex(i); + info.name = name != nullptr ? name : guidString; + info.guid = guidString; + info.hasMapping = SDL_IsGameController(i) == SDL_TRUE; + info.hasUserMapping = false; + + const std::string base = StripCrcFromGuid(info.guid); + for (const auto& line : userLines) { + if (!IsMappingLine(line)) { + continue; + } + const size_t comma = line.find(','); + if (comma != std::string::npos && StripCrcFromGuid(line.substr(0, comma)) == base) { + info.hasUserMapping = true; + break; + } + } + + mDevices.push_back(info); + } + + for (int32_t i = 0; i < (int32_t)mDevices.size(); i++) { + if (mDevices[i].instanceId == previousInstanceId) { + mSelectedDevice = i; + return; + } + } + + SelectDevice(mDevices.empty() ? -1 : 0); +} + +void MapperWindow::SelectDevice(int32_t listIndex) { + if (mSession.IsActive()) { + EndSession(false); + } + + CloseJoystick(); + ClearBindings(mBindings); + mDirty = false; + mStatusMessage.clear(); + mSelectedDevice = listIndex; + + if (listIndex < 0 || listIndex >= (int32_t)mDevices.size()) { + return; + } + + mJoystick = SDL_JoystickOpen(mDevices[listIndex].deviceIndex); + if (mJoystick == nullptr) { + mStatusMessage = StringHelper::Sprintf("Could not open device: %s", SDL_GetError()); + return; + } + + LoadBindingsForDevice(); +} + +void MapperWindow::LoadBindingsForDevice() { + ClearBindings(mBindings); + mCustomName.clear(); + if (mJoystick == nullptr) { + return; + } + + char guidString[33] = ""; + SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(mJoystick), guidString, sizeof(guidString)); + + const std::string userMapping = FindUserMapping(guidString); + if (!userMapping.empty()) { + ParseMappingString(userMapping, mBindings); + mCustomName = MappingName(userMapping); + return; + } + + const char* joystickName = SDL_JoystickName(mJoystick); + mCustomName = joystickName != nullptr ? joystickName : ""; + + char* existing = SDL_GameControllerMappingForGUID(SDL_JoystickGetGUID(mJoystick)); + if (existing == nullptr) { + return; + } + ParseMappingString(existing, mBindings); + SDL_free(existing); +} + +void MapperWindow::StartSession(const std::vector& order) { + if (mJoystick == nullptr || order.empty()) { + return; + } + + mStatusMessage.clear(); + mSession.SetBindings(mBindings); + mSession.Start(mJoystick, order); + SnapshotDeviceState(); + + Ship::Context::GetRawInstance()->GetControlDeck()->BlockGameInput(MAPPER_WINDOW_GAME_INPUT_BLOCK_ID); + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->BlockGamepadNavigation(); +} + +void MapperWindow::EndSession(bool applyResult) { + if (applyResult) { + mSession.CopyBindingsTo(mBindings); + mDirty = true; + } + mSession.Stop(); + mPreviousButtons.clear(); + mPreviousHats.clear(); + + mGameInputBlockTimer = 20; + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->UnblockGamepadNavigation(); +} + +void MapperWindow::UpdateElement() { + const uint32_t now = SDL_GetTicks(); + + if (now - mLastUserFileCheckTicks >= FILE_WATCH_INTERVAL_MS || mLastUserFileCheckTicks == 0) { + mLastUserFileCheckTicks = now; + + std::error_code error; + const std::filesystem::path path = GetUserMappingsPath(); + if (std::filesystem::exists(path, error)) { + const auto writeTime = std::filesystem::last_write_time(path, error); + const uint64_t stamp = (uint64_t)writeTime.time_since_epoch().count(); + if (!error && stamp != mLastUserFileWriteTime) { + const bool firstLoad = mLastUserFileWriteTime == 0; + mLastUserFileWriteTime = stamp; + LoadUserMappings(); + if (!firstLoad && !mSession.IsActive() && !mDirty) { + LoadBindingsForDevice(); + } + } + } else { + mLastUserFileWriteTime = 0; + } + } + + if (mGameInputBlockTimer != INT32_MAX && !mSession.IsActive()) { + mGameInputBlockTimer--; + if (mGameInputBlockTimer <= 0) { + Ship::Context::GetRawInstance()->GetControlDeck()->UnblockGameInput(MAPPER_WINDOW_GAME_INPUT_BLOCK_ID); + mGameInputBlockTimer = INT32_MAX; + } + } + + if (!IsVisible()) { + if (mSession.IsActive()) { + EndSession(false); + } + if (mJoystick != nullptr) { + CloseJoystick(); + mSelectedDevice = -1; + mDevices.clear(); + } + return; + } + + if (!mSession.IsActive() && (mDevices.empty() || now - mLastDeviceScanTicks >= DEVICE_SCAN_INTERVAL_MS)) { + mLastDeviceScanTicks = now; + RefreshDeviceList(); + } + + if (mSession.IsActive()) { + Ship::Context::GetRawInstance()->GetControlDeck()->BlockGameInput(MAPPER_WINDOW_GAME_INPUT_BLOCK_ID); + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->BlockGamepadNavigation(); + + PollDeviceForSession(); + mSession.CheckPendingAdvance(); + + if (mSession.IsComplete()) { + EndSession(true); + mStatusMessage = "Mapping captured. Check the diagram, then Save Mapping."; + } + } +} + +bool MapperWindow::IsBindingLive(int32_t binding) const { + if (mJoystick == nullptr || binding < 0 || binding >= BINDING_COUNT) { + return false; + } + + const ExtendedBind& bind = mSession.IsActive() ? mSession.GetBinding(binding) : mBindings[binding]; + + switch (bind.bindType) { + case SDL_CONTROLLER_BINDTYPE_BUTTON: + return SDL_JoystickGetButton(mJoystick, bind.value.button) != 0; + case SDL_CONTROLLER_BINDTYPE_AXIS: { + const int32_t value = SDL_JoystickGetAxis(mJoystick, bind.value.axis.axis); + if (IsFullRangeAxis(bind)) { + Sint16 rest = 0; + SDL_JoystickGetAxisInitialState(mJoystick, bind.value.axis.axis, &rest); + return std::abs(value - rest) > SDL_JOYSTICK_AXIS_MAX / 2; + } + // A half-axis counts as active once the reading is most of the way toward axisMax. + const int32_t threshold = bind.value.axis.axisMax / 2; + return bind.value.axis.axisMax < 0 ? value <= threshold : value >= threshold; + } + case SDL_CONTROLLER_BINDTYPE_HAT: + return (SDL_JoystickGetHat(mJoystick, bind.value.hat.hat) & bind.value.hat.hatMask) == + bind.value.hat.hatMask; + default: + return false; + } +} + +// --------------------------------------------------------------------------------------------------- +// Drawing +// --------------------------------------------------------------------------------------------------- + +void MapperWindow::DrawDeviceSelector() { + ImGui::AlignTextToFramePadding(); + ImGui::Text("Device"); + ImGui::SameLine(); + + std::string preview = "No joysticks detected"; + if (mSelectedDevice >= 0 && mSelectedDevice < (int32_t)mDevices.size()) { + preview = mDevices[mSelectedDevice].name; + } + + ImGui::BeginDisabled(mSession.IsActive() || mDevices.empty()); + PushStyleCombobox(THEME_COLOR); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.6f); + if (ImGui::BeginCombo("##mapperDevice", preview.c_str())) { + for (int32_t i = 0; i < (int32_t)mDevices.size(); i++) { + const DeviceInfo& device = mDevices[i]; + const char* status = device.hasUserMapping ? " [user mapping]" : (device.hasMapping ? "" : " [unmapped]"); + if (ImGui::Selectable(StringHelper::Sprintf("%s%s##dev%d", device.name.c_str(), status, i).c_str(), + i == mSelectedDevice)) { + SelectDevice(i); + } + } + ImGui::EndCombo(); + } + PopStyleCombobox(); + ImGui::EndDisabled(); + + if (mSelectedDevice < 0 || mSelectedDevice >= (int32_t)mDevices.size()) { + ImGui::TextDisabled("Connect a gamepad to get started."); + return; + } + + const DeviceInfo& device = mDevices[mSelectedDevice]; + ImGui::TextDisabled("GUID %s", device.guid.c_str()); + if (mJoystick != nullptr) { + ImGui::SameLine(); + ImGui::TextDisabled("| %d buttons, %d axes, %d hats", SDL_JoystickNumButtons(mJoystick), + SDL_JoystickNumAxes(mJoystick), SDL_JoystickNumHats(mJoystick)); + } + + if (!device.hasMapping) { + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), + "This device has no SDL mapping, so the game cannot see it yet. Map it below."); + } else if (device.hasUserMapping) { + ImGui::TextColored(ImVec4(0.4f, 0.85f, 0.4f, 1.0f), "Using a mapping from %s.", kUserMappingsFileName); + } +} + +void MapperWindow::DrawDiagram() { + auto gui = std::dynamic_pointer_cast(Ship::Context::GetRawInstance()->GetWindow()->GetGui()); + const bool haveArt = gui != nullptr && gui->HasTextureByName(kDiagramTextureName); + + float artAspect = FALLBACK_ART_ASPECT; + if (haveArt) { + const ImVec2 artSize = gui->GetTextureSize(kDiagramTextureName); + if (artSize.x > 0.0f && artSize.y > 0.0f) { + artAspect = artSize.x / artSize.y; + } + } + + std::string values[IM_ARRAYSIZE(kDiagram)]; + int32_t rowsPerGutter[2] = { 0, 0 }; + float widestName = 0.0f; + float widestValue = 0.0f; + for (int32_t i = 0; i < (int32_t)IM_ARRAYSIZE(kDiagram); i++) { + const DiagramEntry& entry = kDiagram[i]; + const ExtendedBind& bind = mSession.IsActive() ? mSession.GetBinding(entry.binding) : mBindings[entry.binding]; + values[i] = bind.bindType != SDL_CONTROLLER_BINDTYPE_NONE ? DescribeBind(bind) : "-"; + widestValue = std::max(widestValue, ImGui::CalcTextSize(values[i].c_str()).x); + widestName = std::max(widestName, ImGui::CalcTextSize(BindingName(entry.binding)).x); + rowsPerGutter[entry.labelOnRight ? 1 : 0]++; + } + const int32_t maxRows = std::max(rowsPerGutter[0], rowsPerGutter[1]); + + const float valueColumnWidth = std::max(widestValue, ImGui::GetFontSize() * 6.0f); + + const float valueGap = ImGui::CalcTextSize(" ").x; + + const float gutterWidth = widestName + valueColumnWidth + valueGap + ImGui::GetFontSize() * 3.0f; + const float width = std::max(ImGui::GetContentRegionAvail().x, gutterWidth * 2.0f + MIN_IMAGE_WIDTH); + const float imageWidth = std::min(width - gutterWidth * 2.0f, width * MAX_IMAGE_SPAN); + const float imageHeight = imageWidth / artAspect; + const float rowHeight = ImGui::GetTextLineHeightWithSpacing() * 1.35f; + const float height = std::max(imageHeight, rowHeight * maxRows); + + const ImVec2 origin = ImGui::GetCursorScreenPos(); + const ImVec2 canvas(width, height); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const ImVec2 imageMin(origin.x + (width - imageWidth) * 0.5f, origin.y + (height - imageHeight) * 0.5f); + const ImVec2 imageMax(imageMin.x + imageWidth, imageMin.y + imageHeight); + + const auto toArt = [&](const ImVec2& uv) { + return ImVec2(imageMin.x + uv.x * imageWidth, imageMin.y + uv.y * imageHeight); + }; + + if (haveArt) { + drawList->AddImage(gui->GetTextureByName(kDiagramTextureName), imageMin, imageMax); + } else { + drawList->AddRectFilled(imageMin, imageMax, IM_COL32(40, 44, 52, 255), 12.0f); + drawList->AddRect(imageMin, imageMax, IM_COL32(90, 96, 110, 255), 12.0f, 0, 2.0f); + const char* placeholder = "controller image"; + const ImVec2 textSize = ImGui::CalcTextSize(placeholder); + drawList->AddText( + ImVec2((imageMin.x + imageMax.x - textSize.x) * 0.5f, (imageMin.y + imageMax.y - textSize.y) * 0.5f), + IM_COL32(120, 126, 140, 255), placeholder); + } + + const int32_t currentBinding = mSession.IsActive() ? mSession.CurrentBinding() : -1; + const float pulse = 0.55f + 0.45f * std::sin((float)ImGui::GetTime() * 6.0f); + + int32_t rowUsed[2] = { 0, 0 }; + for (int32_t i = 0; i < (int32_t)IM_ARRAYSIZE(kDiagram); i++) { + const DiagramEntry& entry = kDiagram[i]; + const ExtendedBind& bind = mSession.IsActive() ? mSession.GetBinding(entry.binding) : mBindings[entry.binding]; + const bool bound = bind.bindType != SDL_CONTROLLER_BINDTYPE_NONE; + const bool active = entry.binding == currentBinding; + const bool live = IsBindingLive(entry.binding); + + const bool onRight = entry.labelOnRight; + const float lineHeight = ImGui::GetTextLineHeight(); + const ImVec2 valueSize = ImGui::CalcTextSize(values[i].c_str()); + + const int32_t gutter = onRight ? 1 : 0; + const int32_t row = rowUsed[gutter]++; + const float rowCenterY = origin.y + (row + 0.5f) * (height / rowsPerGutter[gutter]); + + const float run = ImGui::GetFontSize(); + const float elbowX = + onRight ? imageMax.x + ImGui::GetFontSize() * 0.6f : imageMin.x - ImGui::GetFontSize() * 0.6f; + const float valueEdgeX = onRight ? elbowX + run + valueGap : elbowX - run - valueGap; + + const ImVec2 cellMin(onRight ? valueEdgeX : valueEdgeX - valueColumnWidth, + rowCenterY - lineHeight * 0.5f - 2.0f); + const ImVec2 cellMax(cellMin.x + valueColumnWidth, cellMin.y + lineHeight + 4.0f); + + ImGui::SetCursorScreenPos(cellMin); + ImGui::InvisibleButton(StringHelper::Sprintf("##mapperRow%d", i).c_str(), + ImVec2(cellMax.x - cellMin.x, cellMax.y - cellMin.y), + ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + const bool hovered = ImGui::IsItemHovered(); + if (hovered && !mSession.IsActive() && mJoystick != nullptr) { + Tooltip("Click to map this control. Right-click to clear it."); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + StartSession({ entry.binding }); + } else if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && bound) { + mBindings[entry.binding] = NoBind(); + mDirty = true; + } + } + + ImU32 color; + if (active) { + color = IM_COL32(255, (int32_t)(200.0f * pulse), 60, 255); + } else if (live) { + color = IM_COL32(110, 220, 110, 255); + } else if (bound) { + color = IM_COL32(215, 220, 230, 255); + } else { + color = IM_COL32(130, 135, 145, 255); + } + + if (hovered) { + drawList->AddRectFilled(cellMin, cellMax, IM_COL32(255, 255, 255, 24), 3.0f); + } + + const ImVec2 hotspot = toArt(entry.hotspot); + const float lineStartX = onRight ? cellMin.x - valueGap : cellMax.x + valueGap; + const ImU32 lineColor = active ? color : (bound ? IM_COL32(150, 156, 168, 255) : IM_COL32(90, 94, 104, 255)); + drawList->AddLine(ImVec2(lineStartX, rowCenterY), ImVec2(elbowX, rowCenterY), lineColor, 1.5f); + drawList->AddLine(ImVec2(elbowX, rowCenterY), hotspot, lineColor, 1.5f); + drawList->AddCircleFilled(hotspot, active || live ? 5.0f : 3.5f, color); + + drawList->AddText(ImVec2(onRight ? cellMin.x : cellMax.x - valueSize.x, rowCenterY - valueSize.y * 0.5f), color, + values[i].c_str()); + + const char* name = BindingName(entry.binding); + const ImVec2 nameSize = ImGui::CalcTextSize(name); + drawList->AddText( + ImVec2(onRight ? origin.x + width - 4.0f - nameSize.x : origin.x + 4.0f, rowCenterY - nameSize.y * 0.5f), + NAME_COLOR, name); + } + + ImGui::SetCursorScreenPos(ImVec2(origin.x, origin.y + canvas.y)); + ImGui::Dummy(ImVec2(1.0f, 0.0f)); + + if (!mIncludeExtraButtons) { + return; + } + + ImGui::SeparatorText("Extra buttons"); + for (int32_t i = 0; i < (int32_t)IM_ARRAYSIZE(kExtraBindings); i++) { + const int32_t binding = kExtraBindings[i]; + const ExtendedBind& bind = mSession.IsActive() ? mSession.GetBinding(binding) : mBindings[binding]; + const bool bound = bind.bindType != SDL_CONTROLLER_BINDTYPE_NONE; + const bool active = mSession.IsActive() && mSession.CurrentBinding() == binding; + + ImGui::BeginDisabled(mSession.IsActive() || mJoystick == nullptr); + PushStyleButton(active ? Colors::Orange : THEME_COLOR); + if (ImGui::Button(StringHelper::Sprintf("%s: %s##extra%d", BindingName(binding), + bound ? DescribeBind(bind).c_str() : "-", i) + .c_str())) { + StartSession({ binding }); + } + PopStyleButton(); + ImGui::EndDisabled(); + if (i % 3 != 2 && i != (int32_t)IM_ARRAYSIZE(kExtraBindings) - 1) { + ImGui::SameLine(); + } + } +} + +void MapperWindow::DrawSessionPrompt() { + if (!mSession.IsActive()) { + return; + } + + const int32_t binding = mSession.CurrentBinding(); + const ImGuiStyle& style = ImGui::GetStyle(); + + PushStyleButton(THEME_COLOR); + const float buttonHeight = ImGui::GetFrameHeight(); + PopStyleButton(); + + const float promptHeight = + style.WindowPadding.y * 2.0f + ImGui::GetTextLineHeightWithSpacing() * 2.0f + buttonHeight; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.16f, 0.14f, 0.06f, 1.0f)); + ImGui::BeginChild("##mapperPrompt", ImVec2(0.0f, promptHeight), true, ImGuiWindowFlags_NoScrollbar); + + ImGui::Text("Press or move: %s", binding >= 0 ? BindingName(binding) : "?"); + if (mSession.StepCount() > 1) { + ImGui::SameLine(); + ImGui::TextDisabled("(%d / %d)", mSession.CurrentStep() + 1, mSession.StepCount()); + } + ImGui::TextDisabled("Sticks and triggers: push all the way, then let go. Space skips, Escape cancels."); + + PushStyleButton(THEME_COLOR); + const bool skip = ImGui::Button("Skip") || ImGui::IsKeyPressed(ImGuiKey_Space, false); + ImGui::SameLine(); + ImGui::BeginDisabled(mSession.CurrentStep() == 0); + const bool back = ImGui::Button("Back"); + ImGui::EndDisabled(); + ImGui::SameLine(); + const bool cancel = ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape, false); + PopStyleButton(); + + ImGui::EndChild(); + ImGui::PopStyleColor(); + + if (cancel) { + EndSession(false); + mStatusMessage = "Mapping cancelled; nothing was changed."; + } else if (back) { + mSession.GoBack(); + } else if (skip) { + mSession.Advance(); + if (mSession.IsComplete()) { + EndSession(true); + mStatusMessage = "Mapping captured. Check the diagram, then Save Mapping."; + } + } +} + +void MapperWindow::DrawActionButtons() { + const bool haveDevice = mJoystick != nullptr; + const bool busy = mSession.IsActive(); + + char nameBuffer[128] = ""; + SDL_strlcpy(nameBuffer, mCustomName.c_str(), sizeof(nameBuffer)); + ImGui::BeginDisabled(busy || !haveDevice); + PushStyleInput(THEME_COLOR); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.45f); + if (ImGui::InputText("Controller Name", nameBuffer, sizeof(nameBuffer))) { + mCustomName = nameBuffer; + mDirty = true; + } + PopStyleInput(); + ImGui::EndDisabled(); + Tooltip("Name saved into usergamepadmappings.txt for this device. Commas are replaced with spaces."); + + ImGui::BeginDisabled(busy || !haveDevice); + PushStyleButton(THEME_COLOR); + if (ImGui::Button("Map All")) { + std::vector order(std::begin(kCoreBindingOrder), std::end(kCoreBindingOrder)); + if (mIncludeExtraButtons) { + order.insert(order.end(), std::begin(kExtraBindings), std::end(kExtraBindings)); + } + StartSession(order); + } + PopStyleButton(); + ImGui::EndDisabled(); + Tooltip(haveDevice ? "Walks every control on the pad, asking for one input at a time." : "Select a device first."); + + ImGui::SameLine(); + ImGui::BeginDisabled(busy || !haveDevice || !mDirty); + PushStyleButton(THEME_COLOR); + if (ImGui::Button("Save Mapping")) { + char guidString[33] = ""; + SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(mJoystick), guidString, sizeof(guidString)); + + bool ok; + if (!HasAnyBinding(mBindings)) { + ok = DeleteUserMapping(guidString); + mStatusMessage = + ok ? StringHelper::Sprintf("Removed from %s. SDL keeps the device registered until you restart.", + kUserMappingsFileName) + : "Nothing to remove; this device has no saved mapping."; + ok = true; + } else { + const std::string mapping = GenerateMappingString(mJoystick, mBindings, mCustomName); + ok = !mapping.empty() && SaveUserMapping(mapping); + mStatusMessage = ok ? StringHelper::Sprintf("Saved to %s.", kUserMappingsFileName) + : "Failed to save the mapping; see the log for details."; + } + + if (ok) { + mDirty = false; + std::error_code error; + const auto writeTime = std::filesystem::last_write_time(GetUserMappingsPath(), error); + if (!error) { + mLastUserFileWriteTime = (uint64_t)writeTime.time_since_epoch().count(); + } + RefreshDeviceList(); + } + } + PopStyleButton(); + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::BeginDisabled(busy || !haveDevice); + PushStyleButton(THEME_COLOR); + if (ImGui::Button("Revert")) { + LoadBindingsForDevice(); + mDirty = false; + mStatusMessage = "Reverted to the mapping SDL is currently using."; + } + PopStyleButton(); + ImGui::EndDisabled(); + Tooltip("Discards unsaved changes and reloads the mapping SDL is using for this device."); + + ImGui::SameLine(); + ImGui::BeginDisabled(busy || !haveDevice); + PushStyleButton(THEME_COLOR); + if (ImGui::Button("Clear All Bindings")) { + ClearBindings(mBindings); + mDirty = true; + } + PopStyleButton(); + ImGui::EndDisabled(); + + const bool haveUserMapping = + mSelectedDevice >= 0 && mSelectedDevice < (int32_t)mDevices.size() && mDevices[mSelectedDevice].hasUserMapping; + + ImGui::BeginDisabled(busy || !haveUserMapping); + PushStyleButton(Colors::DarkRed); + if (ImGui::Button("Delete Saved Mapping")) { + if (DeleteUserMapping(mDevices[mSelectedDevice].guid)) { + mStatusMessage = StringHelper::Sprintf("Removed from %s. Restart to fall back to gamecontrollerdb.txt.", + kUserMappingsFileName); + RefreshDeviceList(); + } else { + mStatusMessage = "No saved mapping to remove."; + } + } + PopStyleButton(); + ImGui::EndDisabled(); + Tooltip("SDL keeps the mapping in memory until the game restarts, so the device stays usable this session."); + + ImGui::SameLine(); + ImGui::BeginDisabled(busy); + PushStyleButton(THEME_COLOR); + if (ImGui::Button("Reload From File")) { + if (LoadUserMappings()) { + LoadBindingsForDevice(); + mDirty = false; + RefreshDeviceList(); + mStatusMessage = StringHelper::Sprintf("Reloaded %s.", kUserMappingsFileName); + } else { + mStatusMessage = StringHelper::Sprintf("No %s to load.", kUserMappingsFileName); + } + } + PopStyleButton(); + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::BeginDisabled(busy); + Checkbox("Include extra buttons", &mIncludeExtraButtons, + CheckboxOptions() + .Color(THEME_COLOR) + .Tooltip("Adds Guide, Misc and paddle/touchpad buttons - which most pads do not have - to the " + "diagram and the Map All walk.")); + ImGui::EndDisabled(); +} + +void MapperWindow::DrawMappingStringPreview() { + if (mJoystick == nullptr || !ImGui::CollapsingHeader("Mapping string")) { + return; + } + + const std::string mapping = GenerateMappingString(mJoystick, mBindings, mCustomName); + ImGui::TextWrapped("%s", mapping.c_str()); + PushStyleButton(THEME_COLOR); + if (ImGui::Button("Copy to Clipboard")) { + ImGui::SetClipboardText(mapping.c_str()); + mStatusMessage = "Mapping string copied to the clipboard."; + } + PopStyleButton(); +} + +void MapperWindow::DrawElement() { + static bool sDiagramTextureRequested = false; + if (!sDiagramTextureRequested) { + sDiagramTextureRequested = true; + auto gui = std::dynamic_pointer_cast(Ship::Context::GetRawInstance()->GetWindow()->GetGui()); + auto archives = Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager(); + if (gui != nullptr && archives != nullptr && archives->HasFile(kDiagramTexturePath)) { + gui->LoadTextureFromRawImage(kDiagramTextureName, kDiagramTexturePath); + } + } + + ImGui::PushFont(OTRGlobals::Instance->fontMonoLarger); + + DrawDeviceSelector(); + Separator(); + DrawDiagram(); + DrawSessionPrompt(); + Separator(); + DrawActionButtons(); + + if (mDirty) { + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "Unsaved changes."); + } + if (!mStatusMessage.empty()) { + ImGui::TextWrapped("%s", mStatusMessage.c_str()); + } + + DrawMappingStringPreview(); + + ImGui::PopFont(); +} + +// --------------------------------------------------------------------------------------------------- +// Menu registration +// --------------------------------------------------------------------------------------------------- + +static void RegisterMapperWidgets() { + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->AddGuiWindow( + std::make_shared(CVAR_WINDOW("GamepadMapper"), "Gamepad Mapper", ImVec2(1280, 820))); + WidgetPath path = { "Settings", "Controls", SECTION_COLUMN_2 }; + SohGui::mSohMenu->AddWidget(path, "Gamepad Mapper", WIDGET_SEPARATOR_TEXT); + SohGui::mSohMenu->AddWidget(path, "Open Gamepad Mapper", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("GamepadMapper")) + .RaceDisable(false) + .WindowName("Gamepad Mapper") + .HideInSearch(true) + .Options(WindowButtonOptions() + .Tooltip("Build an SDL mapping for a gamepad the game does not recognize, or fix one that is " + "mapped wrong. Saves to usergamepadmappings.txt.") + .EmbedWindow(false)); +} + +static RegisterMenuInitFunc menuInitFunc(RegisterMapperWidgets); + +} // namespace Mapper diff --git a/soh/soh/Enhancements/controls/Mapper.h b/soh/soh/Enhancements/controls/Mapper.h new file mode 100644 index 0000000000..ba45d68cbd --- /dev/null +++ b/soh/soh/Enhancements/controls/Mapper.h @@ -0,0 +1,181 @@ +#pragma once + +// Built-in SDL gamepad mapper. +// +// Produces `gamecontrollerdb.txt`-style mapping strings for a raw SDL joystick and persists them to +// `usergamepadmappings.txt` in the app directory. That file is loaded at boot (and whenever it changes on +// disk) on top of the bundled `gamecontrollerdb.txt`, so a user-authored mapping always wins. +// +// The binding state machine (axis commit/return distances, binding override rules, mapping string layout) +// is adapted from https://github.com/General-Arcade/sdl2-gamepad-tool (MIT). + +#include +#include +#include + +#include +#include +#include + +namespace Mapper { +enum AxisBinding { + AXIS_LEFTX_NEGATIVE, + AXIS_LEFTX_POSITIVE, + AXIS_LEFTY_NEGATIVE, + AXIS_LEFTY_POSITIVE, + AXIS_RIGHTX_NEGATIVE, + AXIS_RIGHTX_POSITIVE, + AXIS_RIGHTY_NEGATIVE, + AXIS_RIGHTY_POSITIVE, + AXIS_TRIGGERLEFT, + AXIS_TRIGGERRIGHT, + AXIS_BINDING_MAX, +}; + +constexpr int32_t BINDING_COUNT = SDL_CONTROLLER_BUTTON_MAX + AXIS_BINDING_MAX; + +struct ExtendedBind { + SDL_GameControllerBindType bindType; + union { + int32_t button; + + struct { + int32_t axis; + int32_t axisMin; + int32_t axisMax; + } axis; + + struct { + int32_t hat; + int32_t hatMask; + } hat; + } value; + bool committed; +}; + +struct AxisState { + bool moving; + int32_t lastValue; + int32_t startingValue; + int32_t farthestValue; +}; + +class MappingSession { + public: + static constexpr int32_t AXIS_COMMIT_DISTANCE = 16000; + static constexpr int32_t AXIS_RETURN_DISTANCE = 10000; + static constexpr uint32_t PENDING_ADVANCE_DELAY_MS = 100; + + MappingSession(); + + void Start(SDL_Joystick* joystick, const std::vector& order); + void Stop(); + + bool IsActive() const; + bool IsComplete() const; + + int32_t CurrentStep() const; + int32_t StepCount() const; + int32_t CurrentBinding() const; + + void GoToStep(int32_t step); + void Advance(); + void GoBack(); + + void ProcessButtonDown(int32_t button); + void ProcessHatMotion(int32_t hat, int32_t value); + void ProcessAxisMotion(int32_t axis, int32_t value, SDL_Joystick* joystick); + + void CheckPendingAdvance(); + + const ExtendedBind& GetBinding(int32_t index) const; + void SetBindings(const ExtendedBind (&bindings)[BINDING_COUNT]); + void CopyBindingsTo(ExtendedBind (&out)[BINDING_COUNT]) const; + + private: + void ConfigureBinding(const ExtendedBind& binding); + bool BindingContainsBinding(const ExtendedBind& a, const ExtendedBind& b) const; + + ExtendedBind mBindings[BINDING_COUNT]; + std::vector mOrder; + std::vector mAxisState; + int32_t mCurrentStep; + uint32_t mPendingAdvanceTime; + bool mActive; + bool mComplete; +}; + +void ClearBindings(ExtendedBind (&bindings)[BINDING_COUNT]); +bool HasAnyBinding(const ExtendedBind (&bindings)[BINDING_COUNT]); +std::string GenerateMappingString(SDL_Joystick* joystick, const ExtendedBind (&bindings)[BINDING_COUNT], + const std::string& nameOverride = ""); +int32_t ParseMappingString(const std::string& mapping, ExtendedBind (&bindings)[BINDING_COUNT]); +std::string DescribeBind(const ExtendedBind& bind); +const char* BindingName(int32_t binding); +std::string GetUserMappingsPath(); +std::string FindUserMapping(const std::string& guid); +std::string MappingName(const std::string& mapping); +bool LoadUserMappings(); +bool SaveUserMapping(const std::string& mapping); +bool DeleteUserMapping(const std::string& guid); + +class MapperWindow final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + ~MapperWindow(); + + protected: + void InitElement() override; + void UpdateElement() override; + void DrawElement() override; + + private: + struct DeviceInfo { + int32_t deviceIndex; + SDL_JoystickID instanceId; + std::string name; + std::string guid; + bool hasMapping; + bool hasUserMapping; + }; + + void RefreshDeviceList(); + void SelectDevice(int32_t listIndex); + void CloseJoystick(); + void LoadBindingsForDevice(); + + void StartSession(const std::vector& order); + void EndSession(bool applyResult); + + void PollDeviceForSession(); + void SnapshotDeviceState(); + + void DrawDeviceSelector(); + void DrawDiagram(); + void DrawSessionPrompt(); + void DrawActionButtons(); + void DrawMappingStringPreview(); + + bool IsBindingLive(int32_t binding) const; + + std::vector mDevices; + int32_t mSelectedDevice = -1; + SDL_Joystick* mJoystick = nullptr; + + ExtendedBind mBindings[BINDING_COUNT] = {}; + MappingSession mSession; + bool mDirty = false; + bool mIncludeExtraButtons = false; + std::string mStatusMessage; + std::string mCustomName; + + std::vector mPreviousButtons; + std::vector mPreviousHats; + + int32_t mGameInputBlockTimer = INT32_MAX; + uint32_t mLastDeviceScanTicks = 0; + uint64_t mLastUserFileWriteTime = 0; + uint32_t mLastUserFileCheckTicks = 0; +}; + +} // namespace Mapper