From e6bd52ce5390ee4e99bd3ef1039a617d7e571b84 Mon Sep 17 00:00:00 2001 From: Null Prism Date: Fri, 17 Jul 2026 17:25:34 -0400 Subject: [PATCH] chore: remove inherited Windows extras --- CMakeLists.txt | 5 - .../MapGenBP/Content/MapGen/MapCreator.uasset | Bin 120715 -> 0 bytes .../Content/MapGen/MapSpawnStruct.uasset | Bin 5734 -> 0 bytes .../MapGenBP/Content/MapGen/MeshStruct.uasset | Bin 3796 -> 0 bytes assets/MapGenBP/Readme.md | 11 - cmake/modules/IDEOrganization.cmake | 2 - cppmods/CMakeLists.txt | 12 - cppmods/EventViewerMod/CMakeLists.txt | 28 - cppmods/EventViewerMod/README.md | 128 -- cppmods/EventViewerMod/include/Client.hpp | 94 -- .../include/EntryCallStackRenderer.hpp | 49 - cppmods/EventViewerMod/include/Enums.hpp | 153 -- .../EventViewerMod/include/EventViewer.hpp | 22 - .../include/FilterCountRenderer.hpp | 16 - .../EventViewerMod/include/HelpStrings.hpp | 115 -- cppmods/EventViewerMod/include/Middleware.hpp | 195 --- .../EventViewerMod/include/QueueProfiler.hpp | 92 -- cppmods/EventViewerMod/include/StringPool.hpp | 65 - cppmods/EventViewerMod/include/Structs.hpp | 156 -- cppmods/EventViewerMod/src/Client.cpp | 1099 ------------- .../src/EntryCallStackRenderer.cpp | 144 -- cppmods/EventViewerMod/src/EventViewer.cpp | 44 - .../src/FilterCountRenderer.cpp | 29 - cppmods/EventViewerMod/src/Middleware.cpp | 279 ---- cppmods/EventViewerMod/src/StringPool.cpp | 87 - cppmods/EventViewerMod/src/Structs.cpp | 203 --- cppmods/EventViewerMod/src/dllmain.cpp | 19 - cppmods/KismetDebuggerMod/CMakeLists.txt | 14 - .../include/KismetDebugger.hpp | 127 -- .../KismetDebuggerMod/src/KismetDebugger.cpp | 1437 ----------------- cppmods/KismetDebuggerMod/src/dllmain.cpp | 43 - cppmods/KismetDebuggerMod/xmake.lua | 6 - cppmods/xmake.lua | 1 - docs/feature-overview/dumpers.md | 7 +- tools/buildscripts/build.ps1 | 353 ---- tools/buildscripts/build.sh | 305 ---- .../internal_build_tools/set_version.bat | 52 - tools/buildscripts/release.py | 382 ----- xmake.lua | 4 - 39 files changed, 4 insertions(+), 5774 deletions(-) delete mode 100644 assets/MapGenBP/Content/MapGen/MapCreator.uasset delete mode 100644 assets/MapGenBP/Content/MapGen/MapSpawnStruct.uasset delete mode 100644 assets/MapGenBP/Content/MapGen/MeshStruct.uasset delete mode 100644 assets/MapGenBP/Readme.md delete mode 100644 cppmods/CMakeLists.txt delete mode 100644 cppmods/EventViewerMod/CMakeLists.txt delete mode 100644 cppmods/EventViewerMod/README.md delete mode 100644 cppmods/EventViewerMod/include/Client.hpp delete mode 100644 cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp delete mode 100644 cppmods/EventViewerMod/include/Enums.hpp delete mode 100644 cppmods/EventViewerMod/include/EventViewer.hpp delete mode 100644 cppmods/EventViewerMod/include/FilterCountRenderer.hpp delete mode 100644 cppmods/EventViewerMod/include/HelpStrings.hpp delete mode 100644 cppmods/EventViewerMod/include/Middleware.hpp delete mode 100644 cppmods/EventViewerMod/include/QueueProfiler.hpp delete mode 100644 cppmods/EventViewerMod/include/StringPool.hpp delete mode 100644 cppmods/EventViewerMod/include/Structs.hpp delete mode 100644 cppmods/EventViewerMod/src/Client.cpp delete mode 100644 cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp delete mode 100644 cppmods/EventViewerMod/src/EventViewer.cpp delete mode 100644 cppmods/EventViewerMod/src/FilterCountRenderer.cpp delete mode 100644 cppmods/EventViewerMod/src/Middleware.cpp delete mode 100644 cppmods/EventViewerMod/src/StringPool.cpp delete mode 100644 cppmods/EventViewerMod/src/Structs.cpp delete mode 100644 cppmods/EventViewerMod/src/dllmain.cpp delete mode 100644 cppmods/KismetDebuggerMod/CMakeLists.txt delete mode 100644 cppmods/KismetDebuggerMod/include/KismetDebugger.hpp delete mode 100644 cppmods/KismetDebuggerMod/src/KismetDebugger.cpp delete mode 100644 cppmods/KismetDebuggerMod/src/dllmain.cpp delete mode 100644 cppmods/KismetDebuggerMod/xmake.lua delete mode 100644 cppmods/xmake.lua delete mode 100644 tools/buildscripts/build.ps1 delete mode 100644 tools/buildscripts/build.sh delete mode 100644 tools/buildscripts/internal_build_tools/set_version.bat delete mode 100644 tools/buildscripts/release.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 1282c1ad..75d537d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,11 +91,6 @@ setup_build_configuration() # Add subdirectories for dependencies and projects add_subdirectory("deps") -if(WIN32) - # cppmods link against the full UE4SS target; enable on Linux once libUE4SS.so links - add_subdirectory("cppmods") -endif() - option(UE4SS_BUILD_TESTS "Build native unit tests" OFF) if(UE4SS_BUILD_TESTS) enable_testing() diff --git a/assets/MapGenBP/Content/MapGen/MapCreator.uasset b/assets/MapGenBP/Content/MapGen/MapCreator.uasset deleted file mode 100644 index 6ed90bde53307a125c258052e43b2cb8f9b155b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120715 zcmeEv2VfM{_V=tb#R4LrK2aA15v6RB-DDSdPqv1VgpPes?u2=+$f2mSknu6=v<^Icbc{Q7`{QFeTLo6>=D{Hb z?Spc~;kwA+(r`m@q#hT4EE1*)l$-i~xvG~*`hZ!T4!AfO7`-^w(ND1BwG=AVS2Org+pUU&g^YR@omoJp(^g6uxfdZ%B zksmB{dcC270*BL)SD=jlaPc;v6UF_}x&~5=`h;@pUjMp&R|c1aXGCh1HG9X(1*o#J zd}Mus{wv2ny6k3XnzynNMdM0-{DBK0cW>qFaD7Fja%@d~peEi>8=j|}y!RKKMBduk zvS?)_uI%XXp>H?jZD^>SS4zAi*u&v&%Q^!Vi-qTv7e^AxP4^vq(e6n|Nu+*eqFTAh z^}TNo29AwI8zQm9JZ0p%_i8}O3~z01bWVVjE>RP$S9W~0$0x`E{r*M{S9ewcltR)dDZt}C83MwH8Dmb<=77++vd1%aHwZw!-Vse;zm9E6(XWxIX zFG`d|W`--~Q3r~|!ih+wploE^+nA`1#%dDbM9u7ol33Xo-cQS17Kv0&j1r=mkqO5| zYR5;a;>x)i@D>2H(v)k8^1BeD8$ z?WA~QOnvP<<@C3&8i#(FEH+_YJQ1mz9EruDDhm_49G+6^#CZ)7Ww$-ge~T=kGNUwH z8Sz$8jm%$S$@oZJbauE_IqTtNO!jHl+H-P7cRv!X(2$BrYroT6s#3Lv|CqNv4)Y65&8N5f0WTV)K;e zFKSo->oQ|}B+(eF_st_7Q~4TFMCEMH>vM#5O^DQ1k$xp2a}&ypcTT(&U312S>gXKk z;D*<~{ky1vRHQLnOKs9D!%b%tZWDygOGLQw%74lRZnjem=whI;oGgNBB+6wggbFD? zKhtwNdWS{6eC4QjhVGI>#scN@U#8XPkkP4pvi8Ht;F^Y-ZA|${QUT@4y{`Hmx|1P2 zUwQi918&HYp+K4U=*XwDWN<1cUh?I4Su(hkCl+6L-a+YYkXA2+k?%hF*ouR5$(gV8 z^?vtwPFV|-^N+dU=A5!Rm5X;f?7~8Q1FQ^QO7k!M=S!2|rjpDK)zsIJ_k@eBsh=tA zWlDiLFAkY@n7#n{hNMo$ee;CnNhxvpkFV}}sG$V<$V7Cf@zFW3#AOr#l#gjl(4+E? z9(^tzm`i@WK6l(+agVWCdL%#)S22itrT@;83t>!>l#h&4*QlvXY5OXJx2 zZq}bRYzbeGqA(dU<}0g)_HWE3XMu9qAtl{%$>~%E&;IXWDe6)hJ}xpb7Osz1MPqd- z6!l&3nkI3mCD_FTnE4AA5#`67a{kC@ALZ$2!hQB zMV-osb&(4YR{0w#QlPMvRPm=Vm3P9EOOL=lLr@rAyX>o-DG5q8Ixbs1KNUI$D=)@~UrID&|V{JlNxFae;Pzqo0TwX3? z%S)d;S}0o*oDs#>l~4R6+Q1bc)J7C2MIKkKJ?F3o5YHs58eJ2wizGzY$L%cr@tTLw zQw({e$L35M58cfHPnpc51@h&7<)~$6FGtXcZut z^OZZdT(Ubf2ElElG7zn3MA$mAzACERUEcL)G)E?>gACus@Y^DPNlg_+$#vx4>!~QB zi+kEvJ}UCd*bx$oC|mlLz0nDkh@qBphb{0Qbc!Gd<~WoZ8CN0;ck2zwr=<)Tg5aWF zxz+#B;$%@ZK9x95AO6}m&l^Eo$qz1$%EmX=C42U`msbsk?FhzV(O7AGrm||<7Y*nE0w5Th*x&IJ%u`T!AayKM zTT3H?X#EHpJ=T&Hp&+X|TB-DW`^lF@6J`^KrL>N{&L*V=5CvNi>c|xlWz9A3+$0E^ z8>whaL?+FM#AagnSFY)(^0)F4Uf81G+=eJ(RDoYv{lacXBmOe+ipuu

Lu@s5Tl- zBx(G4&6~eMzi3<#3|CZ_M57H#-}8>y3}%F4H4!3Vo>01WTOZ#Q%wVga#J=9N6spDm zK~h`^^gs0PkQ0M7vaXx@OAi=XW1qh2sp=W1$PlTYzVnoc=qqLpTX}oiNuv>zQDV}V z=zO!%ZF$ez)WZ67P!d-LZg}$|fe=hhM)b;W@BSi8tRSQq!-8v0ePVVpqejSWwX>du z=7>E0u|HJ<6OR};@D@z_`;N|nyOfyZ_;oK7jzX7J{kP)XF!T&`snPl96y-iL@W-jM z0e|~>*b5M+n#8VObhqy>Uj3x5m^Ar8D3bElh_^04U*ck7d?1okgFNV&%S14*W{{=g zi>K}fLR28jPxKh&@)tTBrfUhYU6@=E08jt-Meks2q!u)YMA=+E=zJ)Inmn>t88=g}wp!GRzbo^`k8_(0R zg{47_QVL_|uP{lXfI^8qy{AtkrF)az+?V|*nlF+{!-;BkA+_tev==-PDXH$|j>?fM zkX90oCsJ)|T-oh_ot_2Rx{yn`{JA&I9|%~qf?}I!eR*D9c|l=*UY@(C$muR9au+%r zA&1Z9E%N$(0l(K*6bvexC+GbLiz1jLEy~Abd)*AY8c0*yb;oX900WQ`Y$?Co@%4XY zCCQ1blh*%p8v4IxZpbA*`}oJ}(P{!FRoS&-;X?Eef(couYJ2s_L7#wS#Hi0(X(u>7<#apx+U2x`|Zf@6WF|UbHY$ zN0v}@y6?sx-$?_BuD0yJ+ct_ui(F&Fh>4Vwx1Tf&CZ|*YHCCLwanlip3pi$v%JW?1 z1&p*1L;gZXz*XpX2ONb3ZYRxtxr>4>zw-Py&xnaFgqhBg*)yZZAW*N~4vHG=hjw5J=M-V@Z>ghJ(%uqtT!s$<*U%X4en-^75i(fAZ)>(NVg(gBWAVxi9>2xw6x zu59}4@>&=&5n+DYZtL!F)vBkDjQ-39*~-Uc`3DHZL$xErkz9z7D97=^DCJs}h$mMRmk8Fm?1KQ>lVM^Vab-E_oH51;WY zWW_BH)rM!rmDi#NtcUqYLiH~2qtlP?uP*|g$CbP0bP#b57ZKX1?DNC9f+BT}aU#v` zBx)Lzp#$$-2Gcw~QW2r%h=4&1fHBciOHuvy{~mugA~iLGNE?f3(1j<>cpSV?6DHPF z9H)#P_si30?RbijYoy`O;<4!cXDmZ&DX}b?sHvI`FK{KfNSk$ALrcBJyin%c?= zn&TGH%p2`0mLR@CL@PC9@7;=zM28(8#h43riKd2~POm3W7;w70K~G^3UA%se$D8lK z%rJqKD~{ZL40J#gqPgWWYc{m5aFTs}v>JJ0Ja@DwWN^?BztD60sl zljAC7#6Fef5WfIVtZuBEQ6H`$b41bYLEAfSfVRs}SlMy+S;8aAX?L}MkM42W67-%4 z{s}Y)qv${Hh3B_I2IMl>f++*mU3?_ioH8y|t}5?77Hn20#wfZ{)uV>G-A>os^_^&K z&CL2R&9p_NW|v*xv5%;U!9tVX+hfEwMhbJy`MJUO_7^D}Ryg0a&QpqEot@gfSCF#^n@} zm&@6EF$6AW)2Ey;bV<>HR=M&O|EGSki94V_sfxb7 zq~JDmMp&SUWPxt~xYK>;21>$frgT4d`!!B zm1-wlba#i3Kp*Lg@PVi0J+uInwv$aJa-Cf6Eh=|*hH^+dCqvpBl!mYkhp|qi zEs{xTX|Gcn;(r{*a!7+0!(mK&jnWW_;V?`4r%WFys?YF$pBmC0QJ?h}rca-?QjFtFiY!XsPkR* z89v~(ENQ=FN!!JspIj&S`J|y7+i3K$8?vNr%aZn{n#OdtQ}N$Tb*{^hb{D1Hsiu8Q z_v)>ZM>o5x52&V(dXJ4*O*yE_WSv~@104)_j#3j)^P%ef>{h_@9|N9R6FgV60iKyA zcph$LJbxn`eiJ;Ow*sDzbbN#D3hD5$9`x+cay|G|hv#s@5!B&1M74kHOwXuRz+F_W;ceMhZ z&kS}aq{GAZ;LTRRW9&aeP4IN=)N;Kx%4ehw56fpzE8sEe+dp)8SUwY50gsWMQXL+q zXKpLtG1}`PIy|fgSF`~hj|rZKTLF(zKHuu_uzWsm1w6+7V-|;W+qva>ZIsWYI(k?> zLs|ikQQt=B@UXtkXazh*`TSjnhvl=R74R7Cj$4O^`L?zd@EGa&Mu&&#d9e-f6q(@p zp$+i-V1lO?59-u3s|&fBwDFITZ(p0>8QunXzB0j6*#>y9RTu{>0PDe$Ho)T&SW@w< zZ38^to8Wo24e)$tg6G>dz~eN*vrp%i+iRm<|JMZ1ur|Om&jio(R>1R#uJ2-=VxbOC ztfPE5trhU-{oFvRc4r+Pw%6ZvZMl4m^k5wy2Wn=Ycfg*lk7tU&BJps{Sk+2+o={~3 z`S5%&D8Mt0)vbU>?@v+hucBs256kD@t$^npq6Kr_YW2#$)EmsXk5$F;Yb)Sk-Y}ks zdIR_d^}ckkmg~X0I$BWe&1&7d($%XjF4?+kcyOmZshM>bq*0Yg{&3QpI#>Y#Pf-)l zkMh(CeFtbY<4${0GaD|s;9-3O9;PjRpjLBp@%*Ht2euk(!RR-@Q=@iTyS?Rj@V+rs zDGuPlx-!;AfM-N|`k`Z~qiyh9O!5bwo79Q^=bUYW=MojqsVY4uy4wcNxSb?DgVn`_ z--fjfp08Crt5rM$eQks18x_w>DxT+xTaM=kgT7rt@<*-EgBgSm$C}c%!2@>D9p4bcWA6!S1)dVM`rXP~j_1ERJg{#ob$HxBM#q%tEywe%0naL`m+3i4U07UR z-EutN81P&{c;K@^N=H>byEn8P&({V#S5p0~Z$s36x>urY@SLF1!+1((w+$ZFYsPcN z+_u33u|PhIXUqJy!Nc~A@%(&Z+u(sZgP!rK9=J|v8$4bE-_BUrHh4fwihLHe4W1DO zJU=gP8$6i%*VE%#(sDds8SKGTgadPmz_VSA3#dJ7tXgQuQ40x`gdgssyJfErd;MzOd2G3jr zp5=G74W4~fJQwdyH*}nIZ_Dv~Zs6OsR4?|Nf^YY$@pbnnT8?MC4i8nWJcb`SAe!DP zo)?~NIi8nvco2V70|_1d={!W-DayD1*6^sHtQV>>ITq!N&*@+o*S}3CY6ahRqZ7y9 zUefU3PJ0r`X+LIo{;k7<{fzIa^#IR*)EW-a;ZZ?ZFJ^dN)Zqc&w$sV4Yi8J@yb`pCHnq_qcXfuRYXjPxZRDdfkVvD2x6G)kPWXdBSx+_1cTB$g@A4 zT>k-T+JWlTu3meq*MrpS!Rob-dhM%Tp})YW(g|6i-Jl6_fs8;Gcnx{nM|tk0(@yu`19%SpgXcJa1N$uo(8;_VNcZ5wAoV(! zu4o7Phl5V!M;@dj4gE8pPVfM_P3U$&{>Tr$VVnmZ9ZgrHgU%!998RZ~&WUse=)@ZQ zI64dI4AS{`I{!iEP&%<6WCER|=q#agES-o$%IF+Jr-#lWI;YSXqH_eD_5vrKS%#n= zIKTsL59&c4w98K?^5PkAMLOz4n*M;ir_zb*G5Q4c9`Md|YM)Wqm7cUW$Cc?sEixIvsbzMj z47<^(f6=!WJ&PA3`|+m>q%!#;dYp1fTWR>{1jLf8G&QYXhVMQO(ughh_Y(%Wtg((?$)!&j%2$BJi=R}x zLzq$>q5P(F7fMW}R2cFy^=)yy&o18+wPH*UJq`NrzY$!BK~y8(cZRvo%Xd3nj-q^# zHZqRyWr%OyK}AAge5e7+8>;akMd|ZTI1-!qcPhCr+`X$lOQ?+;KHu%5-W4mel&BIR zb&gUutW#zyRm$;7TtFMC^UWam3=00L2xcQykmbkGQv>A#w2|^wDGAHeoz$`|Z9_-3 zo-x#}dctg@E+bo|L6{GFswqshc5Sbw1gVqS$f(GambQ({Pa^I#5I4b_n4M<}1Da|Uqlc8lA=lE`CHAjO|p&Unb zR}uyrX=LkglG`^{S9^dsI)gL~x@MKm{A12?tF`N{YDhlX3&_bA(my{1Klv0) z`{>z6&>#iZE(!?p=srXN7E*ka$4US4=qZnKJE{HMENK{^=78NyC^h02m3E_zvNf#;wSL;c-HX0sEnU5eD zLuYD;f5>~huw(NnJe3}7I&pB0s>xbj&D)x%?w!`6mU)Vx(1uECD|{9_7kZ`Ek29q+ zR!s^l<@9*MMnhG}6ZCnFR-?bu9T8O9Xeh3=i(!9LVcc_f zO4@DB&y`Rg;Cjp?Ha#h28wI_xCQY8bl@qC#%_cs>7D$e>R-8<^Ic8#A%aj+Nfag|f z{bDi=NHpJUu6yWtEJ0t=A`KN@1HNLG&`dwwRnvWxhK}&T@L`&i)6@1-Tcha&<7iqG z!#Fh0&)m`Qo2B(q@n^Qe%u&7k^!-rly)C7IV+q!h5hU+fq<1zNo~7!XmU4h)`wvoc z6p*(VLiRUE9szeQvgQGL4$-}zY&Gn+lTw59|^){o#v?lzBwK!L39E$3t3+5wFZAnyScKRnff`GiA%uM{Q4LDmgOK)289F(L`wL z>Y3JBL=km#>PM*NsfSonF<8yfpDf@%NE(Bw|7iaPlJ1#7{iaZ#2^4;;Un_Bfd#sG2 z5Yx%HC=>ipOX?@7IVMr8f=Hz|)hK;rZ}k~^OW&7QM@op{FhhXrTuP~azsqsx94*5qrb8e({L9<}IAtugxsj;CZ*QWvN>E=-RAUToqiOZlX*K&WU@#wpS?Ayp z0k>VYO#jPX3Q-ZHsL?S{r6Y4$=3*7uG|Uj#XemKoHzFL?*8i=TialE9m`Y30qN}kY z+KLmOF<#I{RJr73%avIg?5PUGs1oD30Qnv_#SDlOF#f}@o1%gMofvTzQcmP677>J# z?t=u!U+Ixj%#4e@!C2CHj5FiZ6ErGpWK*qbVE?Nxm9Lg!9r|C@9v(?+v>PMxNn`~p zm0IEl_c0BECjer@KDS8S;Be_49mGxrLeMcHu>+CrP4K`H|YT)8AL8KNfL3AMQ>WZY!DF- zkLGO@16uKkwK0zt+j~cpM;JbcxVU%SZJmWA* z^iXK9j9jHQjqSWj@Rx<`u?Wr@VqsUIkIqb2PcsP2N<9%mqR&dU-1D*GS4=$stCbi#xac0e+E0DMMLotNECSXgF)xjE zLi8ip1f-$oPgQZT^t4rormYaLwt}ay@mNP={%UJi`c#dJHsxqbih0c@cZnWjStt8M zO`mMEh0==m+`CitT%!g$G0n0Tjyhly;v}&dWFu=t)QbH)Hl^IM2IferR$)~fYljVD zClS`x5hKan2GcF;WWB=r#So!Mg`_R;C~ol#Z7U|7!srw08dx8P-T{u4a;$v73)z&g zB`#j?$TTX^D1k@Ob4eSU%&^45mMoN<`Kqui!+N3LDN<=!ihTw~9*|*g;s&C#`BV~q z1OGDH2-YeMXK&I9#Mat;a+PH*tX2A25O*OK#%jh)%Np9LIr8XQZJC>CM!bss186TI zV9;BmmX?&K&Mf9_d$f z*>0KT!)k73iSY`0_I)U`EP2>8UL0pxANOIOi1zZS@1UPzrwD95e1Mz&1qp_T5%Woy zO+aiYqb$roVLyva%xY!VS{|Kc>cg5W^!(NOZ=EGIyxu6ej2SE5?}K~2megC;#Su~Z z+(0zvvWOgDQ?-$JgJoH^C;IX7@s_1{P5~o5-XmmFVwUAtqq$rvm#{XMU8X$eiBTTL z(Y(flF`#k(8hjo8wKRI*zJ!>=PrF@VMP`sr%5fL`Gi(uKIf!0p;G98%H;Oi7v#z{~ zo4ykUYp-^|KuJ0sxLvX^I1;-h>g&wl9@gtj^B8OiU`=H-2j6NAU1-@h?%%1tcNEcy)n=*lb<}1=8>d><#@@zFo&{r! zeEJ8!f{_U3u`z=^M8uFo=vpMa5B9tuj?AN+Sku5L-liym?H6y}}9NEn-D&@10FK%d$S!vLezIx&IZTk^(W}z?c>z z5bV;$YH2>zjdfJOFw2G5CqQ>ablQ|}q=NV!yV6}GEA$6ELqgb1 zfO%n_C&HLOt~O$)fsfLP3C{{kTs&5ltv09ZLd#m%e<8YyQ+$V*&qkkLv(kB%ml@u4 z{@dt7YgRS#xL^#mVXm$R*_2Bx%d(|3YmH60R4w0pZR2@&yy-QI?56Zhm|aV!m30mF z19n74@9-@g)3BbU=eW$0Qr0l+RlvShtSZ0`K+Etx0<1CS6|wGsS)mZYU?X5lFv5h^ zVZFgdpP;gm2=`*y-t^Y4w5)+WP%eD4DXT2&W1UZ(lj$vH*?4y43d>qp)6yxr(y}ba zZN{?jLsHs^K;%r{RhG4}H0AgoGKIWgGax%y4ERi}8DZ`i)&cPw+k42hkW!q4=V}#~ z7R9pkQX|X~YA43d+KzFC=x1?QQ!(1aEGG8uVfP}>_}G+dENQdCf30N=%$)$yjJJVc zQ(-|}^ba$gh+yF2TI%ow=Hs$)9abZ>o@j@ZhjpzXw zprmV5VwV#^9M;d|=5uzSrztr1fy(j&rW$i4FY_%Kv zV1jDBL9My@ww|LS{Tk~;qAnr&Ks~Jvq^@p_q7=CnpZXpH;Js5)ZnUJDbp<;NAvxLi zVY3lY$hQ~dS~B(_VGmk9U19q}BsoM8*ujVh`zA|VgVh|`8ZX|R%iBz0;xpy2LN&!}^euu5vyAeIIiGn1_Zf1#X*itBQ+d)7;fa_5c&fe^d~5 z84snub)6}Fy(P^Z)f@@3&X^;zDYvPR=J)?zY7N>xAYQ>vkM$D7$N5B=_9iylEndMk zD}z1#u$KCA8&s^BX98JovF{M$Xy_DV3O@oZ$s_4;EC?^-CRxjsXGDrvnZ}qMk>Bl> zxOi1lQej4ek?xI_buwk?wWV_64$GSQs5u-op3~mYbP&zhe>O{u`Uabh_0nut*2mi4iI!)9S5j8!r@RzW0;`K4llz$?m_33e1Iu*T4I zSaIaDDfd|7;_;l`$7k9{Wu!>kM|H0yo^&6}o)lNj$07#ge{vrcd@TGu{~JiOr|+&h zj9PG?Wh=5v`hLsuEE!lB#2K(9h)dxy3+R+S5%U)CmzYV0WH18=e+IuN{iaRXoHH&& ztcZFcchrX11Qrc&-b)ABBhtk)R#auo58H?tlr1^qf+d9&#ST6>dx`NgJSXfb+JMm* zMrDZ85tC!q99Klyd4%VIoN<98@V2Ony)i~`^2BH!BRq@*fC2s(Rvr+<=6=HSV9vNu z9)2C%@d*Ban;3Z@qCo_LJ_YX~@6ih6Kpcrts7-k&XIzM15IQh150LYg`co& z1@jPZp2MEtYbJ=$VR_I8&~M<`fDLp1nC-y3X6Q@k$#TqqUiqYoi@m-!vnVC1jhRQO zV^;0mxlB0_&&g4&v9{jyUMY56SJFJt@stZb=P65SS&FIAK_=`r?L-*?ET($D5xL#7#! z=PYaBH7T=Mj_1|Vng;M_SkG_mI~d&4JE-OO`31{XFxTa*8~i`~2mED-_zTMpKL&e@ zXXrAb18`ctDG9ki+il8=mbms(bEHP3yoWjUjmdvo*2ZI)Otq!&KYFRfb*Ar2dfBp0 zE2{PTm0q!|jV*sBs#DkDURCSU^k^Ry(zJcYtgFUd)CsCBTiUiX{550&5=5f4@tnfSBWpC$#s#p zTZtZ?HNec~MB*||BW2nbpt9$B$C5tArcq>u?F8Dh=b&>4?e)ifp141VT{v`T%c*!9&~|T*(PuReL?c)SZZT>E1B|4oWiKBDQ!B5-gN-p{0wpd*JL`l zhPI+7G7KC^mnCxrkvW$ERk9vXAnW0l7O1!(f5wL~ugr(>uPlqP9m5z0{5``M2S$Pn zV;oLX`Y_&>bz|%)%dnJL_b{g*^W-LFqs+LzY#2CCt{9*jsrrjJlsU{C!Avc~m?IdC zO4wgqZz|V~9B1xkmMrrcIi+3W{@W5svrcF4UoN5X=yQKbx@D5axLo!bP12b2l`ysw z7<))a^JR;*Fra@WBC>(<hh`(OCdV+gU)aSUVcBgYRya5xT9?U2VqD7k z7zb1HN7+bsQdcv}y`^>-D@>B^w#3nN{bi{&ug73D$Vino3gdo?wJ({gxmZ|NGxILp z(l>Qni7}jUJb)EE*>bIyV4aO&tijl=C1GvfOR%0G`9bg=JVTwVrJ3nzI@i+eEHG)h&;0>V)_&}vlv)j_rt6Yh z=5t=i$-3M0Subvnc~pYBt#!7!UWx6AQTAGdn^_*MKh^@R*1ad4)|QU7uwHN7pUm*G z?3(8R^>*|)t_!+|+IVi=Smq=VJ;@xhucqhV8n`b5yH>Akd*)Iy9T>nRF2{53naW`& z2I#O#cj#(F{n<*$bbSf%KU)czt}kKEd5tQWjuIM7TK3G8$aKA=CC52>&0JE-T3=G) zl_fx%AowtN|#+_Agf*Fyhr#J)448Tj6US-lC+>)UdXeCpFV@ZS_&Jb>>>GtxEpBTCS~R z{-x!zKWkd9t>`vS+c4VUzN6Q-wwOoJR**GJY(F@n;pUa40W8+u z{C|I@(OMEbCI+|6#@DI#^v^T*XiZtu$0TO6kw)GAzcdr6?JUr|0_H-rxR_@z0Oe6$ zcE5m}JVVp`2)MaEz*_1}(`S}$OKh1H#&k;t9`ZOW7s+rd|B?)>RPJ7;>5|Fa$7s7) zvPI7)4t*rU)-%^gCY3hyebfd`ZcD3U#A@c-;P2V$ zbFccp<=;4})U2ras7Kq?mA$v-I=08k0b6iwcRBY1{peo5`%BJ>roQQbnpg%+(Fd(m z^Yl@NeV|Lxk-M{L0yqf!0jTddn zzf_IHw_!L=)Ue7g#z^nR@s>7MrCD`tFKVvROU0gxZ#Qa8`doX{M^B)S`=X{XfPz{? zy!us8ZAWT$dxi2GF z?(dPFQrq_;ZB*vnE3g{bcHhYKfn;6Kt6E^L(9jaBP!%;}uMpcSjRNU|GjlsaR8Y3N>Y}PTR+BWz+slv#6SfY%5D(^fpb$XcnI|O)~6{|B_6$S=82$OzyL& z+8YYlrSiW&i)!p4O^?S?W57T2N?pr$b(qa&ay+6%Jg{ca_2yYM%#K>eRZSmRw%n3& ztFrf*KXwkLwe^{$n>pHSImVVthFkf6TQXWigx+JlD}bqPiEONKwp=HI#@(cXsC&(&z}FY4!Nv}hu`bTER%2p4uE`&ZGhn_DHDLb| zTB5H59>{u@0do}?-(UpasP>C8glQE=snyU{_2s4bRmD^3=mck%=LnM31-pE zbI8bt)8&qMt(3GUnP!&c9ty1f{M94nI;b3zouIQEvd8|pdn9@pkF_)(2`ERrnvVoD zxA?Mk*1*hHGc@m=ITklt325p{b?RILXy@on!`l|Si-3vOf;3Dl4IcS(TeKCmmcGvc zyc|n34KJ6H7K1HRrg2sJ7zdb979}(tgoWXn^*SiWYBKtg{@;8Zwr$tcZ9=Hq2s*A%?X*%8Bj z7<8sbc+$K4S-n4c892;w1NyQV6ff=#(n>iUExw~AA+Tvu&r7_LLE$c-oZTGF-s!P4e#j5|2tBTE=3D3-LaKnsqlFQN`NaFkmhF;bzRnr)Z7|fgYleKORP2@^m!8ty zGvrCFX^#xK^fh(KkSDdKt{HOaYl1?n+y{e`deKpL4|q2^&7^d*fm?fJ$d}qO^cAz( z^eyX=Ax~;eJu~Eb)zGfJGvqVs^*$N$>07q1mM4X09SE+^_ctvc-Dzn?jomLpzEnzk zWyqz+xPOK`MkyVTA)mgk19Qk@&mm9m9P%8LL!N_k$kQi>JbiP>b4U((`sI-4&>Zsg zSMw~OD-MB09K{n+3?6o^`ZPW0>|vg1dVfYRZq*H)DHm40Nt?g~<>9|S<)My8(A zrj8MCJ${iqby`VZ>hDuK#_4z3)GpwQf}iS#xQ6{<4|?h-5$RJqdI_9!>(S#B{Oqfs zQ^6SFsn?T)#g4AtQ&t5Xz#$kRvPd4F_p4b_d6lGG@Kk{{={ap*1^MlC;@Izfd6jIJ zS*DhD5vcGZNrl!hskm3f57MW0fmK)VBO8g<<4N#d7~r1W6?g~9c2N-Ng6)74+tHCI z@Sy|A#P%ia!1i4RrVTqdi0G0IJisH!QTbI1Mh3|S!D_7u;_qA^(5EKee zy&k29y?xJeE>T+_+hIMMUimJ`%AvP->h%a!E6|8)iIgtr^$LDOCm_JV0N_Ugy1;=e z_>oLRRd|w}BiWruN{9xelti7#B%}nmzFvWiq%?&3^VE(@=$ASNjF^HSsWGBKYUdEO zi65j-?RZ%SZcD1VCfg!fFZuzs3h}_c_WOu#v>Z>pw$Ov`UT^~1CfW%2)M@+SA`URC z=hV_^U7%4yt(ZMR&+sDw9ls*+PAiF#K1}U+Iw=9sVn-SLNLqT+9cw@C>2LSZVW)Fy z7wf*;1tEcgAFYGnNp=vFCn6DANY;zGkuJyroLDbRo%liW)Q(luT3K5r4(g1fbra2c zmnbD_5$1(@7lIbec;IQj59kMn6o#Wz#8<8)& zEhLPi*JC7H*=^Cwdrs{*Smf?_2FbPkt`ihx0kVnXLB3J1Ho7iQ?>WsFyBw~j51=ay zI`CG)_R`>)$XbXyI`(f*9lEE$Cpw1!a-Db>C?N6&3_)YH()Uqgvz3PIo`4q9L0v+e z)20I5&O4#ci9?TWFhM|`Vz=Wr>2%jqCEEo=p}+bnkM`=7rT`S>H}xSc(Lo6UKV8!j z!KG)_lDDYWvFi06_4u3!Ad_uhX6R*kvf$=}K@T%)HOSs+5RmJiU!azaD9stFLu z6!jhq-bpY_p}+o&Jf|riQauSZy+FOidgb<7VR*o++Eir|HCd@e6y6fbVU;?i(~KNblmqOpP6F#T~gXPP9z zysQc$)k?-5sD!EInnb#j5|*eaV0dwuW^_@)y&_qwaVP4)IG8O;J9^+u419`w2@ZX~ zk`VQ`lPXPCKSob~E=?Zo)eKxCtHt>#)#x6KdbhKof?_pqy{=5kE$E8Qm=&pzT7@Z$ zlyuz#OI8O;-kz@Bv9wioDS}UdiS$<(g>H*OumCDCx+Y#1Nld7yj?{&PTmq4* zaAR#E<^E~4luacw$-oY3!prJ4jlE33)^^_fK@bc!=E zaI~5!NuK(gyd16GrM2%=HDQ{1Wk!+_mIo+I#2O>Xu0Bgm;G9X52{@CRQRBFuH(BG1 z{k>Tl7bi+ojl-0aN=e#b`Pj4x16i(S>Ze{;sMn;iqT!3OMg?+BhZ~%jnC$0F+zGx@ z3pJ?Mw9D}-goA)2N_3~!)kLn1-9Tp*by{r-rLg@7R>6#@5rZ@tb@xNc z0%eb{3S>n6)RO~}s3)uKx<$QqSFbD;7JvYQ*w54qxc^DLPh%O})>)Yn^C$r$QXGfL zi|)6#dT&g@Uv8EM%YZF-#$ad71@2?MFYD#Idz13@mH7|&ncb)c&3{MMmp&BunS9Ix zY9cGyGDBuT!Vv@zURhhH)2R+sYh>mbbIm&LQZF%(^KU2`9dCVUNAr3;qkd0o}&DMU`SUX`WgiOl!E$@DaIa?SyX#)hafCMGF1#f}1W7|Yni{I+=tw(c~+PMmr`AYNBdUSYr!a0Z-t9$%=i&|8#W=<~Q-d4>L< z+v!TJ^DmjG98C6*Z7RUnLsDciKALEXOp1b`{6fF0$XVnF_zV0_uh--EhdjZ6+wF0M z+yNnzWSxJ>#3++DGsq;xYE7sJ*GAGhiMiGC7Zo}Jg)YC-T}0i5L=f~jodq7ZC+Ktq zd``cxTC&2wq+yiC9T}v-vu0v}r!*3;j>OZ%fuJJwfX&MH=Q*9eB8M{+@C6F}-k>ut zKR@UWxqVJok>6RQukrs|d3>qzjboT(^p07G&1xct$jL+p-EGN~xVfF8+^;5bY|&MH z&KSx5@bj%}PU?52Z`DTS7su7(8+bfgpcZ6H+NeG&v(+1Rhj>Jofdhf*xZf7vy?xIi z!!LNLWLQz3r%p42b)kGT@j9U)Jg0twHUT4NUl0vtnU%3oG9UUjFbr3H9)|B=AR^$` zOBZgd{P%mqmppg>m77aEf>@+;9xkN6f{-_FDXLz0edX}g9YUv0`R37=&0vTQMa)&6 zU!IrmaJhV;Jg3*;%?}hf{f_)#AvugtL4m{R$YUl`iQaSug9Z8dzC4#R=yo`RK3AdB zl^67RT?Kx>FOT|>F8bP+k#rWQ?EcztJlQ4{(wgU<<2fsSHLpSHZ&ZBOu9JG0_;f`;(GH~mK?|fff^uyZW z8v=(M_dvS0jaa+p_Afub<8hwo z7hUc7ojW~t-1fu9tn7SV)u-nk^#pingn^eFi;y~&lWF#P0)+vm%Nz6*7SYA)_jtVd z4k#pnfmDs9;c5L5{ts@xsO20FX z+xwATzWc8Drs%F-*9CUPxkVE%pZ)coqlijEJg`*+Q(i?03Kp?dz^u;ST{vkzGl+_>cI z$sgVCae;m#>~Mm~P@+7~RbJo>IjMsNogqKPZ>~bWJK!iRa68FVx{HD?KQPM!4D{r? z+|EDIDuEEb+;k5<|7Y?sbPy)?!mWHQyz z#>iXjnNzU~^7$UuW*JRP#FT^&xcJD$i;BY*r&T=r z=rJcB+8Mk(5Ed) zvfu87dz-=FwBnK4D!KRyOFuflELs^6sx&bin;A*yZ}+AQ5~@1g@sX+sE$&oADCPXzm4$;HaC_{}ncn7?M${lZp_RK2E0z+;G&3HtUaOX~IaVVZ?`@j0@a8Xv7`wb=&veN1hI& z(rgSlP{o?wgN@cAq~^?MEpAscY*nwxp~*IqP)1X7p_-d1-mE^S8I0jH-q?~U@mcEN z_p6Cts8@DD83#DqPU!fnr`_q^)wY`^%sHoE8aDyusXlRFTH&q4rKO)gKWEhPRXx{t zEjVL^8LWv0L(~nkqSFVkNx)5h^i4BK79i~3!%ec1OmYVt2k1Cs+Rsl6Ipv4aYkzrS zL-pKy?lgm;ony4^7qWgs?JP+T&H7j@~Ei2OeSYOx z7};c1W~76FnW3xp_;}d-l2wJ9r|h~<;5svyY41h_Hi&XYG+K*?tVbrgxDdu#RpHvW zG#!8@sSDCW=M|mWZ~65%?@hzH{aUeC;c8^aBr>EKJLG8q(vh)4X6$p^Gj5X=Bm)A5 zXY4V(UjOi&o9~!0VqDyw8WuSkZwJgH=ETQY2EqDU9xoN1?`&AoqwBJ*XNyK zr}?kC;1mBvN4}NvHWb?;OiP`1>-MSp@BMz++J!GXu(`71jb<>+ZA9iqDjE|~cv82a z8Gx!ZWxg==nR93MeMYe1@fpoP0w%FFc_x|T7?=^cr z@Qk}jp&`&N6FOCm*IJV=Ik?Z%orR3(mrNwBt5r&30 zAs7?rb>=BqTqI=pX_Caaj$8=*`GqcT-|hUlWbJ;%4=;SZrUnEWVK5XTsu2~N+SgVO z8{y7REBUF1`@@ppir>z!{h-ISPXK*3nv52x`60nu4y(|w>>@L#{D;+zU=&i`|8yKdV~XO?G;D%>w4lB%Y64aCvW-q&XZp; zgZ&XU1nd4kR*X39_(A)5H(qx~w=*B95!18Tq{=iMxahYP)knMhS42kLy2m5-13;4z z_Pf~-G#JM?I%l1*Wbg62-Y{lq`O9w|z2K4k0nde?t3LYi+P8Wh?Z0}=2TO1Jz|#nV zjIg9}fV9MrF6%%KnM5vK+y-41aFZXs{+J{S5MHLiO|p_qv@uUW$H@a8du7eF0|NJ~ znRUw{U4Gru33KHbd9;I{GR>yJ8#H% zT|le zB!$?LDe+6{3HPgsIgg-%-^|(PiA{67r!8Ck*%yz$=H@1#9;#0q*uAiN;~|5_=lAq3 z`sA+3{>1e^n!(b{CDgZbP=s5ktM%g)RtzZUXFJ}0@3FzXM_sw!@?#Fa=1sA~Ih$;$ zh}c|p$|FyGcg%gu$J}(m(}#>bZpMS4%m_p4wZKawtBt}hCTjwrJ^JL?g?oS7dGzV+ z-kfsU%1^fu$g(Sfgf%roBi^63m8@QTchN;9H@nSXAgc*sSwG6I&XHM6m84l%{Y(kf zl5*%(E+xU_A=#U;rLFw1ad%$7^JgQjo-=U#@b(kmK>l1Le%boBcb>NF&XL!id(TJr z?tR`lV2u$5T`r5v!2~j@RdyUG0b+Kjv!49q{7@RvFSO9D2*kk6`rxOB)BinmM^TNZ z?4lFSTT}GVQnBAK7ow+qw%bKN?7m~vX`TN2)H}6{ZU)gtnCXbR2W3o|FZ0pJIufpL zrC~NYlN@4ecLWlLba-@|jlkBk(JyDS(eJBmVr|{5KBw7eESqh~ln5JrQl>;ud7_&5 zxq6+YUX?1k3C3cy=r*ysCT^d>pChp!iXPnGPO$oD!XCDdjn&lEqRY0XO;4>xoHn3Mgp5qj@D7|fLPsgRRZkPj_jWEQ7!P-b2z1K6czA`cwb*qs# zrl4B-ipWk>heQ3W5NgJeqz#I=rOR7yJ{J3Q^vZn}tlGL?-|fhd3vK^c_4CCW_I-Q! z#?ij)=SAmz*jS(svV2Q4=<+*EGv)C&IR5N^I7IvKfdT#0&tCg(^{Dg4EEw?msJp(n%3}t@!WfN<8X6OUNNr>$i3z3E zp%ElksRxTRlOOE4V@4QTXOdmjY)Z){3=~9@jq?&_u;0ztp}{yX#xzdm0B%K4Mm^#= z^v$S+jx;@*PNL1IN6i|A8E577dely~Lc9!e!LU&ahD>;S)Ghf}^%#D`UTd*(Y=pI* z9x)?~ty3G6a!FbYE4HJ_BRn@&mwoW6_n!B*oLqWu47;D|e%@PF4N)zbS+G_9(&21qn zRTkif4xXF{$;ZsgU$?6nwyIY+U~$08ZBXy;QLl&`#qpwgt=R_~(M z{yzQg+czzVs}-D}UYSvI)MsU`dczFEBYUqS=>E!&fBogkOE9oONcFxoIBKTs_H2J!z0ez-!o$6+@;TV-0|NR&0v4J4uwX( z`@<2j4{m?B4M@6JBxrNvy@c)#kLo zS}Z40{x_{fkISBavSFvq-gV1QiEYd)9MvLg;U!L=@xrvjoV~|7P zE9c2sEFY>R(&B5xVIQf9Y4J6GEbM+Y1BWJ-f$78*9Z!7o+jB#=c3bt>#k((Ft*6bV zR_3GYs?wraThHh_>b8Sce0S}YBlb0e{pmDiuu6b2fLD~wYD!wfw48|8k|}X4jloXJ zln8E}s3!7CK_3p;?Q|no3d*8(`LZ3o4PmdTw@+xC;iq|}nbFuhxf>Kf8nD>ya^k78 zEas0k($EE~2~{;T+zBT~F?OuZSK1FMqpzbMgtGm#FRb+&)VVZ#T*Mv+E>WDotuOWh0I_&c04h+5;X&#m)E$eFP zX{2SRkLZZC3VS?CU-GK1sh^4AR!VhtKx*n5YUfeo;|-CDnkw4YjhY9UVUon#+YnWS z`k|=V*oF@3D6pUv7)fG3Ap)Yark>JkDu<}>ntGZGjN1>bts%Dd6M6gf*RR^tQwhpK z@n9S+w%XcR%WOK=96A#`e14w+ZX? zeHVQZho8<%H+`}3&i;p%-CNeDw*1t}CIV5~IC^OH?E_}} z9Jt-!g;5!pcbrFmHIsoE%t?h*e)7swyxd)0kmsg%e!U)Np*s+870}z3PA9!P9rQZ_ zPB*>ztDyorJq3kDK6){jJ~0q1q=JQ>V3DiP4Uss#0e47z>p5AyC~IV1OwqjnD!IU#HcKr(d#ySPr*|i*FNaE_p{M=y?oO#B~QmwUIl>v zz!9Kx_d#!-H0y+~%dQ{z$@mBN+jFBC3=^K%J@9+J3V_{)QwGleXZ6s$lKT$0bx7?u zufI+pOMip*7aq0Ne^K#^rFVV2&ED6uY_1tB8UKpe+`Xxk-1S;cVHLz8q=N5K9!yCF zm=IPdN@0~9oAVHUh#my_3osR-mI6iFZMwi*`^Vir>+#dDvxZ-@q|ein_qs-O0d!8B znGP^xU0?s8hLT6U_r8AKzBdjo@|nTJUMAXO8ck>B7D>b+ z^w*8-VS9`OT@{PgrR~;e_07bSsmZISEW7)`qn8ISxPI6(-QGNND~QR3OJU#WL+@R> zV)WXg!3FPL_rO4K$q0k?liFe@F{)K|cwOz(9mIz52_dS*MIR_mWv%UVinxN0BoZ&TQ>?(;IIN zf4=PMb&WUH4cpld&KO}r|8?)aTG@&Hs1Bo@kd7Oy6mLaq>Djy`D9HH+>HQkoG4S^nM*Z7ktD>XgqGeR0^C{@dQ0y6g0!*Tf2BHui!n zwBPuiq!|qMjz;#Cv~`g_poc?PYq5c_#qV+=Vvw6mJJ6l@zBMr5Na_zlc;joQ$dC(x z#+Uv;pb;i8q?Iq#hQ-@IP;kN5QcgNMB#s`Qh(^)H@PbwO~+W$i!hxx;gt84P>EMq&bJ zEX8j)WQT!fwdJ0Y5mf)C74e}HmLBO}aNvRoTNb`|5U^+BuuuR>2MNa470qBU1k&-O z&}AIZHFqZp)t+)B`@h+1oyeJqE~IV74;^Wm(?C>jk!k6P*dyCy%{g`If0v$KHraRK z?5{6u=kA+lra8rg@#eQLSlHo+hXWhWUh!r8i0~RSSZhElnQuDSY{(0AZ4|edb zxa!dh`}P|tJYOabv*v&VU0gG_8GTM6R3HbWABtjbDCvyw)b5>>cUaS z55Mr2#N*LU#n+f=4mx$@gqn5jkJx|s{XYj^uJ2PZ&kUBE=Cpd>oJh_4biWnHcKrH> z(5+veuxQu$M~EdhE6o83Mq6bD%TaS$oh~!8PEBr}=4AbTWJAYh*qnDt-zw;_*Ixdm zV=sPe=zPyiGtC)E%{6|q#tfF5=IHw-gsVUF*!6I;`=nJbqEzE&fl$Cob3np*H^hhk z`05@bH$BXpY9uR7+9{k*r`bV)Y4khU2=bfkH! z1=(!Qy5?xkrZeo1opk$Gfy+z3Ui0~`#eMX$<2Yv&HRauTH(b8qvyP<~{(9yMw<||H zX$H$pbJ}Er_wB{OBlhnz*T4L}_E&yUHtkz*&q{MZLgVA>b{cW`urX&X+<57gs}>(> z2J1}ua)@(UovsOlcfm5xKKCl0lw90y^N#QLjNeJ1jC3JwGk)ku(;S$UEix@Vad>ky z=cj|`z5B}18-hz7I=txRW2c>Cra7YtW3aNk?4_mu9kcv^Ap<|N@7dQ3mYe42`=&}t z@}kF{1ZKEN+8p%F8;{!m#LdT#4F2niiPza)SbPl1SZNMOFuv1j2Fp=%T8%F5uvkAb zzE%vn5ZA3=zp0}Bmb#K#mybW`{YxjE0(d5Rk+vB>bfjt2^lTc{EL(K_`~Rwawd-}h z3m%x#Z~d6F=IUk2q?xVen89+>s8;W@e&UpI#S`4i#NnNhNC!wT+AuR%jvCeKbipnf zT>#phi7upV#t$87n)7Tn%~8n3HF?Z6aMx#Ci$7aC=7Rp0EB6+!JIYLR%#_~@mYe3Z zTAu|6ahTZ;z%y}}HOFkMW(Lbqb6TA)jQ@;7-E4FrZ8Lu8NYk7KnyuO*)6x?+H)o#b zqh~k2@N4Yjl7+wJcPV@5jY(#jgXJ9KM`O)kSv03Dcgz%0^Nip41&^#W2PB;L($>D4 z&-i)Fs!taVa%}kW1T$DR&1p-z#!&kj=AOIX?lnusoUyUs*QXpW{U;k;NZX7bI?^=f zs%$o=S(-CwmQ%>jv4Ha(i|*-Eb9Gqu;}3R<&0Q z6~WfxsF#=YU-GT3v3YN^T60=2%Q`34tkt*~W9R?!`0G!Px@ziG=Rbb!;ZvD(w18b8 z#65AZuW#J{fTKfKJ-g`2XFo1$ZwA9s2li2*tZvb&?bdE45}waa$oG77-M(duH$G>d zH}UDEz@1GGk&eb2_16qWHtl!cpJ~?5(dJxeGA>JkCL;`{up}C7@I@=j>$&$nqf1Xa;*AH6d)qNC6V1#kv%1V+m~2m>xf%O3 zZK8|sPBo?cIcRKwn9x9P>e9kfC{`1xudJPiEhM36td8E==ts#XP*R{K-cTDBDJR)a z7#oWwq6VmVd$tQ@PlYQ;Ss>8~l}xdQq0XT}QYPf2(R91gFN zF5d2_GaaF^;sRH|n@4|DE*L1xr@yt}qrZ((==D1Ng?SG8iysAl%Yqbb=nrfa(jNzOIb9AGdOt; z`nxR!4zHuxy5jKpfCV?*a`lYRP3RwfI`>1eKVzNxCwV+&Zh)|1Si(@Ygsp2D%9KO!oyEja4?6x)~_w zNDFhnB;Xd2WRlpdF!%2ttX%P3`O4zUzPkL{j+?rd>zjor4+l0P8o$qL2K(K^T)dxS z{CY7OY^8~4yzxV?W-zlbw^am;M(U5`!T=~Tej*ncbD_w%nH&@uVSh-F8=!_5cj4zs zlyL(rh%&;`qaf6-nLN|}h%EX;ulRbn*?m%6pv<`87zJ_>*BMt7H=KC+?9xpK>@j|# zZP9_C%n17v`yw>=>$?wnxAz{MN^U*0i8PmMcItqxkWJ*-Xcs6-CW!DFNYj!Gr(|1Yf1-%>Y+4+8bv)CJ9?bP@cE;HEg z?v3zCFXMO1z#S`NV75`i3})tyFotgh5u=&2X*DKM<&88<$lKrR9^z9J0TjIg%#NN6dh zjvTlVN0Kl2X*DKOAx{P;8q1 zG0Kpm%I@6y{12yiZa5Ky8DW1;KeZVo(vdbs$hvW+$=@izcv~F9=~sTEU_wK9PCb2! zh`z#V7x&`Bd-k$1WkI|CQ#f|<$7%UnKlGWPrjJg(Z=8GU5VYyy zZf6~^F~%xGOEV-SJD0;a;v`>g3VWN-cnS(@cPQ(t2=~F zo$}42F9U5ZB)NAPw*KPJ)|cE{{luhQUi$oLkYt2maZ>cBiLr2foW93ZhuWpIuvJpO zonr&-EfJ|EKYHCaNlpQYO4_L)G^%7r$vMw=dur4@j@p+u?sV1HNCN!KlRkdF-Zd_8 z@!GDlZ~4hHoZ-g5UZ05>fO8om4l}&_Q07!-l$S&+=x;%*FCzarmH=ID`bJL3>2&(@ zLmpqDqo|-T;P-j*{k}ZEhdwu?bY(K^nozqF%zD8%&*>`k$A8Gt^7z;mrpv)1> zCu89#$n$#hi(H|6x5w*p1^oH`pv%<)r6_Q_@?37e+gaqwE27;b|F69(kBzDbB`U@%idwIbQV|VDL=-thiyWzV zV??7t&_6^mqDB+Z7&Jk@Z{E!EI{VhPOO<-eX5YN`&AhqZZ?11A^w=_q0 zWR-M{2g`#;M5!$WcDUj&_>u6#JNBVFv=TecyKCmDZ=7y#Y~827a{A(7%u^|7c>iMa zDD~>pxRtxl-|*8zqo!dPJZ%uroce6G7A60U0{sA45J_1zCldsjjzkWSi1+a|D1Scy`Hm(#85k<`HwD?VEE|#j2qdD~?({=bn+*G<7ik=q1L)7D035EjckmIFx7i32du{#1u^kQ zsWJ9qIYT!zn2Vt0?55PN74CigHtzfAlqV*|(Qt|}x-QpP3o3%@>8HVy83$=)<8jiI z*GpIyJ_ngxLp_M6L}_g(fVC!leR>8eu$Y|#i9OfO1sO;ngET!20xd5P47H;~yh6G< zm@Z-TXj+tvS2>Rcv93cWZ_80H`>=$dLyRfSH z-2;`6E)}Sc3pt-hB-jXeGj(}{+OT>Snm^S#^*)S;H&KsWPTb_1+mg>dH{i0ByS_d1 zn?0BJXGy?t*w)O+aL;S;jicds+(nobsUAHp5e)Z0@T!k7_RgvYCVtdlY3ExDuW?_f zy5+kAeU>kbqhT|9h&t;;OivLLE$bp)^5jmzj(Prffn9(sZJ`BFSQDU5bQ|o?mknHB zed%ZR{JXXs-|XMhL1;Jw`xJ`; znca==d>=>q?}GgbEJiThZrNK}8Xmg&?gvJ<4w<^7dd2q_974;E=evr6{XrN#bsOv> zzZ(6>p8K~|whn&ny@8+4Z;7@xIPyX>75*=8zGTFN>EBkYS#K>LFk<1}I2w)uV)TFIV^2*HulQi(gG=|7^;y4q2lGGAytU7|4-Nb2#lGM4^BKsCAmQ`k zyo*&Tq@#&TQyfhJzXuC0ML5nWDJ4pRs@lG&Sb`-ZA^r z6w%jZS5!>P0Pc`0>#hMw)#1E>#{^Do`_={kG@xD@` zn35s8z+%xA0|bC;f&g-xG-B0C=NoJ3KUJRN}LN`hnRa4f+Q1VJif7xHaRsZ0R&8JV3QVIiRuEbwg( zhQZS~EHHi-GzckCQ7qHdpg=9p)O4^c$25TphfM;INjiH6798F39o-fbN%K7iczUYn z%aY~Dx(~Qeg%Yvg%bxAK9`FS%$pF@|qkD!cn2IOC$r4A7VG;6JD3wJ>$~JO2WGwLg z3}S(&sX&Xg3xv&;66KTz66_t779<0>Z?dV8boLG)sIu+409Qrz zY(sEOUl(;5))q`(^PpXDN(6$XD>@t;5*5kNp+=}G;0zQbIEt!jA{a{|*1;{QXw z6fC4BCr%4$8yXXB)cp)}&!%&^Om6r{l*tglrKvucIh-&Y%CAfOMt#1;hm<2B4I{_~ z!41k;ThBpVA+BOz$$r~@^7sMx)xK3Na=4*=ASvm46GDnJKfCW#^-5cjBJFX;ps z0J{xKiVXpr7SF3jEUUn3aD6f~j9)+>laruQW$FGI2sY}KO(&yX1G>wkJOL|lO*4!$p=l{@J^@jvD0@#RY=t zh`xiHSgo3xBpy=F#YPJrNa{O+gi#CNi4Omxhip9R7J68L7NCbY#C=V4L&PuybO$~7 zZWIY!K@a~<=jcJay@{`ZISc-_=@_yq!Bi6z^U0=Vs}fFSfII1_re}HyiFi#Uim%?H zhj!`GYFR|0W7aY`iiQ+qJA!~MgE;)%{g^1!NVwCeW7 zyn*>4w8%Ccye+y={gbx;xo!Qwu9|<$nqz0QwHnRz#e${q8Pp+~%TFlnBx++y(6Ee= iI(iil`V%soSD42BYoddnRbw*CPNy=is; diff --git a/assets/MapGenBP/Content/MapGen/MapSpawnStruct.uasset b/assets/MapGenBP/Content/MapGen/MapSpawnStruct.uasset deleted file mode 100644 index 7c0397f2ab0279d78c6b3b6918a5640b96e960d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5734 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 diff --git a/assets/MapGenBP/Readme.md b/assets/MapGenBP/Readme.md deleted file mode 100644 index 75ade956..00000000 --- a/assets/MapGenBP/Readme.md +++ /dev/null @@ -1,11 +0,0 @@ -## UE4SS Map Dumper - Map Gen Editor BP - -Extract to your project's content folder (Content/MapGen/) - -Import the dumped .csv file as a datatable based on "MapSpawnStruct". - -Drag the BP onto an empty map. - -Select the Actor within the map, and set the relevant datatable. - -Press "Create Map" under default in the details tab. \ No newline at end of file diff --git a/cmake/modules/IDEOrganization.cmake b/cmake/modules/IDEOrganization.cmake index af5d2cf5..6e772bb7 100644 --- a/cmake/modules/IDEOrganization.cmake +++ b/cmake/modules/IDEOrganization.cmake @@ -162,8 +162,6 @@ function(organize_targets_by_source_dir) set_target_properties(${target} PROPERTIES FOLDER "deps/first") elseif(REL_SOURCE_DIR MATCHES "^deps/third") set_target_properties(${target} PROPERTIES FOLDER "deps/third") - elseif(REL_SOURCE_DIR MATCHES "^cppmods") - set_target_properties(${target} PROPERTIES FOLDER "Mods") elseif(REL_SOURCE_DIR MATCHES "^UE4SS") set_target_properties(${target} PROPERTIES FOLDER "RE-UE4SS") endif() diff --git a/cppmods/CMakeLists.txt b/cppmods/CMakeLists.txt deleted file mode 100644 index b682e35b..00000000 --- a/cppmods/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Enable IDE organization with folders -set_property(GLOBAL PROPERTY USE_FOLDERS ON) - -# Add C++ mods -add_subdirectory("KismetDebuggerMod") -add_subdirectory("EventViewerMod") - -# Organize targets in the "mods" folder -get_property(TARGETS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS) -foreach(target ${TARGETS}) - set_target_properties(${target} PROPERTIES FOLDER "mods") -endforeach() diff --git a/cppmods/EventViewerMod/CMakeLists.txt b/cppmods/EventViewerMod/CMakeLists.txt deleted file mode 100644 index 71209af7..00000000 --- a/cppmods/EventViewerMod/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -cmake_minimum_required(VERSION 3.22) -set(TARGET EventViewerMod) -project(${TARGET}) - -include(FetchContent) - -FetchContent_Declare( - concurrentqueue - GIT_REPOSITORY https://github.com/cameron314/concurrentqueue.git - GIT_TAG c68072129c8a5b4025122ca5a0c82ab14b30cb03 -) -FetchContent_MakeAvailable(concurrentqueue) - -add_library(${TARGET} SHARED - src/dllmain.cpp - src/EventViewer.cpp - src/Middleware.cpp - src/Client.cpp - src/Structs.cpp - src/StringPool.cpp - src/EntryCallStackRenderer.cpp - src/FilterCountRenderer.cpp -) - -target_include_directories(${TARGET} PRIVATE "include") -target_link_libraries(${TARGET} PRIVATE ImGui) -target_link_libraries(${TARGET} PRIVATE concurrentqueue) -target_link_libraries(${TARGET} PUBLIC UE4SS) \ No newline at end of file diff --git a/cppmods/EventViewerMod/README.md b/cppmods/EventViewerMod/README.md deleted file mode 100644 index 2b5c3454..00000000 --- a/cppmods/EventViewerMod/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# EventViewerMod - -A UE4SS C++ mod that captures Unreal Engine call flow and renders it live in ImGui. - -It hooks **ProcessEvent**, **ProcessInternal**, and **ProcessLocalScriptFunction** concurrently and uses a **single -unified depth counter** so nested and recursive call chains keep a consistent indentation story (PE → PI → PLSF → …). - -## What you can do - -- Watch a **live call stack** with depth-indented entries. -- Switch between **Stack** and **Frequency** modes. -- Filter captures with **case-insensitive** whitelist/blacklist substring rules. -- Pause the stream and use right-click context menus to copy names, add filters, or open a focused call-stack modal. -- Save the current view (or everything) to a timestamped text file. - -## UI overview - -### Enable / Start / Pause - -- **Enable** toggles the mod on/off (and persists that setting). -- **Start/Stop** controls whether the middleware is actively capturing and dequeuing. -- **Pause** keeps capturing logic installed but stops dequeuing and UI growth. - -### Target filter - -The **Target** combo is a *view filter*, not a capture filter: - -- **All** shows the call stack exactly as the middleware reports it. -- **ProcessEvent / ProcessInternal / ProcessLocalScriptFunction** show only entries that originated from that hook. - -Important: depth is **not** recomputed when you filter. If you hide callers, the remaining entries keep their original -depth so you can still read the true nesting structure. - -### Modes - -- **Stack**: live call stack history (ordered by time, per thread). -- **Frequency**: aggregates by function and tracks how often it appears. - -### Thread picker - -Captures are grouped by the originating `std::thread::id`. The combo lets you switch which thread you’re viewing. The -game thread is labeled with `(Game)` when detected. - -### Performance knobs - -- **Max MS Read Time** and **Max Count Per Iteration** bound how much work `dequeue()` is allowed to do per ImGui frame. - -### Saving captures - -- **Save** writes the current thread + current mode to a timestamped file. -- **Save All** dumps both modes for all threads. - -## Filtering (case-insensitive) - -Whitelist and blacklist entries are **comma-separated tokens**. - -- Tokens are trimmed and converted to lowercase (ASCII-only lowercasing). -- Filtering is done against the entry’s cached **lower-cased** strings. -- The UI always displays the original (non-lowercased) names. - -Rules: - -- **Whitelist**: if empty, everything passes. If non-empty, an entry passes if **any** whitelist token is a substring - match. -- **Blacklist**: if any blacklist token is a substring match, the entry fails. -- **Show Tick Functions** is an additional filter gate applied on top. - -## Right-click menus - -Both stack entries and frequency entries have a right-click menu (when enabled by the current render flags) with helpers -such as: - -- Copy function/caller names to clipboard -- Add function/caller to whitelist/blacklist -- Open the call stack modal (when the stream is paused) - -## Call stack modal - -When the stream is paused, the context menu can open a modal window that shows an entry’s root call chain. - -Definitions: - -- The **root caller** of an entry is the depth `0` entry that began the call chain that ultimately led to the selected - entry. - -The modal provides: - -- **Show full context** - - Enabled: shows all calls produced by the root caller (the entire subtree under that root). - - Disabled: shows the path from the root → selected entry, plus the calls triggered by the selected entry. -- **Disable Indent Colors** - - Mirrors the main window’s behavior. - -## Architecture (high-level) - -- **Middleware** (`include/Middleware.hpp`, `src/Middleware.cpp`) - - Owns the UE hooks and pushes lightweight capture entries into a `moodycamel::ConcurrentQueue`. - - Uses thread-local producer tokens for low overhead under high call volume. - - Uses a one-time barrier/flag to prevent enqueuing until all hooks are installed (to keep depth sane). - -- **Client** (`include/Client.hpp`, `src/Client.cpp`) - - ImGui renderer + persistent UI state. - - Dequeues entries, groups by thread, maintains stack/frequency views, and applies filters. - -- **StringPool** (`include/StringPool.hpp`, `src/StringPool.cpp`) - - Interns function and caller strings and returns stable `std::string_view` pairs. - - Caches both original and lowercased variants. - - Produces a function hash (from Unreal’s `ComparisonIndex`) to avoid expensive string comparisons in hot paths. - -- **EntryCallStackRenderer** (`include/EntryCallStackRenderer.hpp`, `src/EntryCallStackRenderer.cpp`) - - Manages the call-stack modal’s state and rendering. - -## Files and persistence - -- UI state is stored as JSON at: - - `Mods/EventViewerMod/config/settings.json` -- Capture dumps are written to: - - `Mods/EventViewerMod/captures/` - -## Building - -This mod is intended to be compiled as a UE4SS C++ mod (MSVC, `/std:c++latest`). - -## Notes and gotchas - -- String views returned from `StringPool` are stable until the pool is cleared. The current implementation is designed - for “grow-only” usage during a session and never clears, but if later implementation does want to support clearing it, - they should also clear all threads. diff --git a/cppmods/EventViewerMod/include/Client.hpp b/cppmods/EventViewerMod/include/Client.hpp deleted file mode 100644 index 6a004412..00000000 --- a/cppmods/EventViewerMod/include/Client.hpp +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -// EventViewerMod: ImGui-facing front-end. -// -// Owns persistent UI state (filters, selected view, per-thread buffers) and pulls capture entries -// from the Middleware each frame. The hot path (hooking + enqueue) lives in Middleware; Client -// is deliberately written as a consumer that can be throttled (max ms / max count per frame). -// -// Threading notes: -// - render() is expected to be called only on the ImGui thread. -// - request_save_state() may be called from any thread (it uses an atomic flag). - -#include -#include - -#include -#include -#include - -#include - -namespace RC::EventViewerMod -{ - class EntryCallStackRenderer; - - class Client - { - public: - // [Thread-ImGui] - auto render() -> void; - - // [Thread-Any] Saves state on the next frame. - auto request_save_state() -> void; - - // [Thread-ImGui] - auto add_to_white_list(std::string_view item) -> void; - - // [Thread-ImGui] - auto add_to_black_list(std::string_view item) -> void; - - // [Thread-ImGui] - auto render_entry_stack_modal(const CallStackEntry* entry) -> void; - - // [Thread-Any] - static auto GetInstance() -> Client&; - - private: - Client(); - - auto render_cfg() -> void; - auto render_perf_opts() -> void; - auto render_view() -> void; - - static auto combo_with_flags(const char* label, int* current_item, const char* const items[], int items_count, ImGuiComboFlags_ flags = ImGuiComboFlags_None) - -> bool; - - auto save_state() -> void; - auto load_state() -> void; - auto check_save_request() -> bool; - - auto apply_filters_to_history(bool whitelist_changed, bool blacklist_changed, bool tick_changed) -> void; - auto dequeue() -> void; - - auto passes_filters(std::string_view test_str) const -> bool; - - enum class ESaveMode - { - none, - current, - all - }; - - auto save(ESaveMode mode) -> void; - auto serialize_view(ThreadInfo& info, EMode mode, EMiddlewareHookTarget hook_target, std::ofstream& out) const -> void; - auto serialize_all_views(std::ofstream& out) -> void; - - auto clear_threads() -> void; - - auto can_render_entry(const CallStackEntry& entry) const -> bool; - auto resize_render_set(ThreadInfo& thread, size_t max_size) const -> void; - - UIState m_state{}; - - Middleware& m_middleware; - - std::filesystem::path m_cfg_path{}; - std::filesystem::path m_dump_dir{}; - - std::unique_ptr m_entry_call_stack_renderer{}; - FilterCountRenderer m_filter_count_renderer{}; - - bool m_imgui_thread_id_set = false; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp b/cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp deleted file mode 100644 index d3e4d9f3..00000000 --- a/cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -// EventViewerMod: Call stack modal renderer. -// -// The main Stack view is a scrolling, time-ordered history. This helper renders a *focused slice*: -// given a selected CallStackEntry, it builds a context vector and renders it in an ImGui modal. -// -// Concepts: -// - "Root" caller: the depth==0 entry that started the call chain. An entry can be its own root. -// - Full context: show all entries produced by the root call chain. -// - Focused context: show the path leading to the selected entry, the entry itself, and then -// everything that happens underneath that entry. -// -// Note: ImGui modals require calling OpenPopup() on the frame you want the modal to begin opening. - -#include -#include - -namespace RC::EventViewerMod -{ - // Renders the call stack/context modal for a single selected entry. - // The context vector is prepared by the caller (Client) and passed in by value. - class EntryCallStackRenderer - { - public: - EntryCallStackRenderer() = delete; - EntryCallStackRenderer(const EntryCallStackRenderer& Other) = delete; - EntryCallStackRenderer(EntryCallStackRenderer&& Other) noexcept = delete; - EntryCallStackRenderer& operator=(const EntryCallStackRenderer& Other) = delete; - EntryCallStackRenderer& operator=(EntryCallStackRenderer&& Other) noexcept = delete; - - EntryCallStackRenderer(size_t target_idx, std::vector context); - - // Returns false when the modal is finished and can be destroyed. - auto render() -> bool; - - private: - size_t m_target_idx; - const CallStackEntry* m_target_ptr; - std::vector m_context; - std::string m_last_save_path; - bool m_disable_indent_colors = false; - bool m_show_full_context = false; - // ImGui popups need an explicit OpenPopup() call. We request it once so the modal can appear. - bool m_requested_open = false; - - auto save() -> void; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/Enums.hpp b/cppmods/EventViewerMod/include/Enums.hpp deleted file mode 100644 index b738c461..00000000 --- a/cppmods/EventViewerMod/include/Enums.hpp +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -// EventViewerMod: UI enums and small reflection helpers. -// -// We use X-macros to define enums once and auto-generate: -// - E_Size -// - E_NameArray (for ImGui combo boxes) -// - to_string(...) and to_prefix_string(...) -// -// MiddlewareHookTarget is a *bitmask* enum (powers of two). In the UI it is used as a view filter: -// selecting a target hides entries not produced by that hook, but DOES NOT change indentation depth. - -namespace RC::EventViewerMod -{ -#define EVM_MIDDLEWARE_HOOK_TARGET_FLAGS(X, EnumName) \ - X(EnumName, All, ((1 << 0) | (1 << 1) | (1 << 2))) \ - X(EnumName, ProcessEvent, (1 << 0)) \ - X(EnumName, ProcessInternal, (1 << 1)) \ - X(EnumName, ProcessLocalScriptFunction, (1 << 2)) - -#define EVM_MODE(X, EnumName) \ - X(EnumName, Stack) \ - X(EnumName, Frequency) - -#define EVM_STRINGIZE_1(x) #x -#define EVM_STRINGIZE(x) EVM_STRINGIZE_1(x) - -// Per-item emitters -#define EVM_ENUM_ELEM(EnumName, v) v, -#define EVM_ENUM_CASE(EnumName, v) \ - case E##EnumName::v: \ - return EVM_STRINGIZE(v); -#define EVM_ENUM_STR(EnumName, v) EVM_STRINGIZE(v), -#define EVM_ENUM_COUNT(EnumName, v) +1 - -// Prefix emitters -#define EVM_ENUM_PREFIX_CASE(EnumName, v) \ - case E##EnumName::v: \ - return "(" EVM_STRINGIZE(v) ") "; - -// Per-item emitters (with values) -#define EVM_ENUM_ELEM_V(EnumName, v, val) v = val, -#define EVM_ENUM_CASE_V(EnumName, v, val) \ - case E##EnumName::v: \ - return EVM_STRINGIZE(v); -#define EVM_ENUM_STR_V(EnumName, v, val) EVM_STRINGIZE(v), -#define EVM_ENUM_VAL_V(EnumName, v, val) E##EnumName::v, -#define EVM_ENUM_COUNT_V(EnumName, v, val) +1 - -// Prefix emitters (with values) -#define EVM_ENUM_PREFIX_CASE_V(EnumName, v, val) \ - case E##EnumName::v: \ - return "(" EVM_STRINGIZE(v) ") "; - -#define EVM_DECLARE_REFLECTED_ENUM(Name, LIST2) \ - enum class E##Name : int{LIST2(EVM_ENUM_ELEM, Name)}; \ - \ - inline static constexpr int E##Name##_Size = 0 LIST2(EVM_ENUM_COUNT, Name); \ - \ - inline static constexpr const char* E##Name##_NameArray[E##Name##_Size] = {LIST2(EVM_ENUM_STR, Name)}; \ - \ - inline constexpr const char* to_string(E##Name e) noexcept \ - { \ - switch (e) \ - { \ - LIST2(EVM_ENUM_CASE, Name) \ - default: \ - return ""; \ - } \ - } \ - \ - inline constexpr const char* to_prefix_string(E##Name e) noexcept \ - { \ - switch (e) \ - { \ - LIST2(EVM_ENUM_PREFIX_CASE, Name) \ - default: \ - return "() "; \ - } \ - } - -#define EVM_DECLARE_REFLECTED_ENUM_VALUES(Name, LIST3) \ - enum class E##Name : int{LIST3(EVM_ENUM_ELEM_V, Name)}; \ - \ - inline static constexpr int E##Name##_Size = 0 LIST3(EVM_ENUM_COUNT_V, Name); \ - \ - inline static constexpr E##Name E##Name##_ValueArray[E##Name##_Size] = {LIST3(EVM_ENUM_VAL_V, Name)}; \ - \ - inline static constexpr const char* E##Name##_NameArray[E##Name##_Size] = {LIST3(EVM_ENUM_STR_V, Name)}; \ - \ - inline constexpr const char* to_string(E##Name e) noexcept \ - { \ - switch (e) \ - { \ - LIST3(EVM_ENUM_CASE_V, Name) \ - default: \ - return ""; \ - } \ - } - - EVM_DECLARE_REFLECTED_ENUM_VALUES(MiddlewareHookTarget, EVM_MIDDLEWARE_HOOK_TARGET_FLAGS); - EVM_DECLARE_REFLECTED_ENUM(Mode, EVM_MODE); - -#undef EVM_DECLARE_REFLECTED_ENUM_VALUES -#undef EVM_DECLARE_REFLECTED_ENUM -#undef EVM_ENUM_PREFIX_CASE_V -#undef EVM_ENUM_PREFIX_CASE -#undef EVM_ENUM_COUNT_V -#undef EVM_ENUM_VAL_V -#undef EVM_ENUM_STR_V -#undef EVM_ENUM_CASE_V -#undef EVM_ENUM_ELEM_V -#undef EVM_ENUM_COUNT -#undef EVM_ENUM_STR -#undef EVM_ENUM_CASE -#undef EVM_ENUM_ELEM -#undef EVM_STRINGIZE -#undef EVM_STRINGIZE_1 -#undef EVM_MODE -#undef EVM_MIDDLEWARE_HOOK_TARGET_FLAGS - - enum ECallStackEntryRenderFlags_ : uint8_t - { - ECallStackEntryRenderFlags_None = 0, - ECallStackEntryRenderFlags_WithSupportMenus = 1, - ECallStackEntryRenderFlags_WithSupportMenusCallStackModal = 3, - ECallStackEntryRenderFlags_IndentColors = 4, - ECallStackEntryRenderFlags_Highlight = 8 - }; - - enum ECallFrequencyEntryRenderFlags_ : uint8_t - { - ECallFrequencyEntryRenderFlags_None = 0, - ECallFrequencyEntryRenderFlags_WithSupportMenus = 1 - }; - - inline constexpr const char* to_prefix_string(const EMiddlewareHookTarget e) noexcept - { - switch (e) - { - case EMiddlewareHookTarget::ProcessEvent: - return "(PE) "; - case EMiddlewareHookTarget::ProcessInternal: - return "(PI) "; - case EMiddlewareHookTarget::ProcessLocalScriptFunction: - return "(PLSF) "; - case EMiddlewareHookTarget::All: - return "(ALL) "; - default: - return "(UnknownTarget) "; - } - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/EventViewer.hpp b/cppmods/EventViewerMod/include/EventViewer.hpp deleted file mode 100644 index 3086d201..00000000 --- a/cppmods/EventViewerMod/include/EventViewer.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -// EventViewerMod: UE4SS mod entry + ImGui tab registration. -// -// UE4SS calls start_mod() (see dllmain.cpp) to create this mod instance. On Unreal init we register -// a new ImGui tab and route rendering to Client. - -#include -#include - -namespace RC::EventViewerMod -{ - class EventViewerMod : public CppUserModBase - { - public: - EventViewerMod(); - auto on_unreal_init() -> void override; - - private: - std::atomic_flag m_unreal_loaded{}; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/FilterCountRenderer.hpp b/cppmods/EventViewerMod/include/FilterCountRenderer.hpp deleted file mode 100644 index 2f23f21e..00000000 --- a/cppmods/EventViewerMod/include/FilterCountRenderer.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -namespace RC::EventViewerMod -{ - class FilterCountRenderer - { - public: - FilterCountRenderer() = default; - auto add() -> void; - auto render_and_reset(bool show_tooltip) -> void; - static auto render(size_t count, bool show_tooltip) -> void; - - private: - size_t m_count = 0; - }; -} // namespace RC::EventViewerMod \ No newline at end of file diff --git a/cppmods/EventViewerMod/include/HelpStrings.hpp b/cppmods/EventViewerMod/include/HelpStrings.hpp deleted file mode 100644 index 12df0dcb..00000000 --- a/cppmods/EventViewerMod/include/HelpStrings.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once - -// clang-format off - -// EventViewerMod: Centralized UI help strings. -// -// Keeping these in one place makes it easier to tweak copy without touching the main UI code. - - -// EventViewerMod: Small user-facing help strings shown in the UI. - -#include - -#define FILTER_NOTE "Note that filters don't affect the stack depth; they will always be shown as-is to indicate "\ - "callers that may be potentially filtered out. You can right click any function while paused "\ - "and select \"Show Call Stack\" to see the unfiltered call stack for that function.\n\n"\ - "Additionally, filters are applied to both incoming calls and the current history of all calls "\ - "in all threads." - -namespace RC::EventViewerMod -{ - // From ImGui demo - inline static void HelpMarker(const char* desc) - { - ImGui::SameLine(); - ImGui::TextDisabled("(?)"); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(desc); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } - } - - struct HelpStrings - { - inline static constexpr auto HELP_TARGET = "Filter by which target should be shown.\n\n" - "Calls are prefixed by the target that called them with their initials.\n\n" - "A common path that many function calls follow/cause is ProcessEvent->ProcessInternal->ProcessLocalScriptFunction.\n\n" - FILTER_NOTE; - - inline static constexpr auto HELP_MODE = "Select the mode that the view should run in.\n\n" - "Stack: View a filtered stream (see below) of the live call stack for the selected Target.\n\n" - "Frequency: View a table of all of the functions called so far with the number of times " - "each function has been called."; - - inline static constexpr auto HELP_LIST_FILTER = "Whitelist/Blacklist filters both accept comma-separate lists of strings with case-insensitive matching.\n\n" - "Note that you can press enter to apply them when focused as well as press the button.\n\n" - "If you don't have any filters, then all calls that match your Target and \"Show Builtin Tick Functions\" settings " - "will be shown.\n\n" - "The caller's name and the function's name make up a call's name, in the form of \"Caller.Function\".\n\n" - "For a call to pass the whitelist filter, the call must have at least one of the comma-separated strings as a substring of its name. " - "Only having a whitelist filter is effectively the same as a normal search.\n\n" - "For a call to pass the blacklist filter, the call must not have any of the comma-separated strings as a substring of its name.\n\n" - "The whitelist filter applies before the blacklist filter, and each call must pass them in that order to be visible.\n\n" - FILTER_NOTE; - - inline static constexpr auto HELP_THREAD = "Select the thread to monitor.\n\n" - "This generally tries to default to the game thread, marked with \"(Game)\"."; - - inline static constexpr auto HELP_CLEAR = "Clears this thread's call stack and frequency counter."; - - inline static constexpr auto HELP_CLEAR_ALL = "Clears all call stacks and frequency counters. This also removes all known threads."; - - inline static constexpr auto HELP_SAVE = "Save the current filtered view to a text file.\n\n" - "This means that if the Mode is \"Call Stack\", the current call stack view " - "is saved, with all of the same filters applied, and if the Mode is \"Frequency\", " - "the table of frequencies is saved.\n\n" - "The file is saved in Mods\\EventViewerMod\\captures from the ue4ss directory."; - - inline static constexpr auto HELP_SAVE_ALL = "Save all views to a text file.\n\n" - "The file is saved in Mods\\EventViewerMod\\captures from the ue4ss directory."; - - inline static constexpr auto HELP_SHOW_BUILTIN_TICK = "Attempts to automatically filter out \"Tick\" and \"ReceiveTick\" functions from the view.\n\n" - "This is inherently faster than blacklisting \"Tick\", but may not block all \"Tick\" functions."; - - inline static constexpr auto HELP_CEMODAL_SHOW_FULL_CONTEXT = "Shows all function calls associated with the selected call.\n\n" - "Note that this whole view may differ from what you see in the normal Call Stack Mode since " - "this has no filters applied at all."; - - inline static constexpr auto HELP_CEMODAL_SAVE = "Saves this entry's call stack to a text file, respecting the \"Show Full Context\" setting.\n\n" - "The file is saved in Mods\\EventViewerMod\\captures from the ue4ss directory."; - - inline static constexpr auto HELP_MAX_MS_READ_TIME = "The max amount of time the ImGui thread will spend dequeuing calls in milliseconds in a frame.\n\n" - "Basically, the ImGui thread will dequeue up to \"Max Count Per Iteration\", until the queue is empty, or " - "until it hits this time limit."; - inline static constexpr auto HELP_MAX_COUNT_PER_ITERATION = "The max amount of calls that will be dequeued in a frame.\n\n" - "Basically, the ImGui thread will dequeue up to \"Max Count Per Iteration\", until the queue is empty, or " - "until it hits the time limit in \"Max MS Read Time\"."; - inline static constexpr auto HELP_QUEUE_PROFILE_VALUES = "Enqueue Avg (microseconds): The average time it takes a hook to enqueue a call. It should be as low as possible.\n\n" - "Dequeue Avg (microseconds): The average time it takes for the ImGui thread to dequeue a single call. This should be as low " - "as possible, though it's likely to increase over time.\n\n" - "Pending Avg (calls): The average amount of calls that are left in the queue after the ImGui thread finishes an iteration of" - "dequeuing. Should be 0 or very close to it.\n\n" - "Time Slot Exceeded Count: Amount of times the ImGui thread exceeded the time set in \"Max MS Read Time\"." - "Should be 0, or at least rarely moving."; - - inline static constexpr auto HELP_TEXT_VIRTUALIZATION_COUNT = "The total amount of calls that can be streamed into ImGui while running.\n\n" - "Changing this value will clear all threads.\n\n" - "When scrolling up while paused, you'll eventually run into a \"Load More\" button, " - "where the number of calls loaded doubles every time from this number.\n\n" - "This doesn't affect filtering, nor does it erase calls, it just controls how much is in " - "ImGui at a time (apart from clearing the threads when changing this value).\n\n" - "Higher values means making the \"Load More\" button appear later, but can" - "come in at a hefty performance cost."; - - inline static constexpr auto HELP_ADD_CALLER_AND_FUNC_NAME_WARNING = "WARNING: Adding Caller or Caller + Function name to a filter may block output completely in " - "Frequency Mode, since the Frequency Mode is only aware of the function name!"; - }; -} - -#undef FILTER_NOTE - -// clang-format on \ No newline at end of file diff --git a/cppmods/EventViewerMod/include/Middleware.hpp b/cppmods/EventViewerMod/include/Middleware.hpp deleted file mode 100644 index 9682f5f5..00000000 --- a/cppmods/EventViewerMod/include/Middleware.hpp +++ /dev/null @@ -1,195 +0,0 @@ -#pragma once - -// EventViewerMod: Capture backend (UE hook installation + queueing). -// -// Hooks multiple Unreal call sites and enqueues CallStackEntry objects into a lock-free -// moodycamel::ConcurrentQueue. The queue is drained on the ImGui thread by Client. -// -// Design constraints: -// - Hooks are extremely hot (ProcessEvent/ProcessInternal/ProcessLocalScriptFunction), so this code -// avoids allocations where possible and uses thread_local producer tokens. -// - Depth is unified across the hooked functions so nested / recursive call flows keep consistent -// indentation regardless of which hook produced a given entry. - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -namespace RC::EventViewerMod -{ - class Middleware - { - public: - // By-reference singleton. - static auto GetInstance() -> Middleware&; - - // [Thread-Any] Enqueues info on a call. The hook_target is captured at enqueue-time. - auto enqueue(EMiddlewareHookTarget hook_target, RC::Unreal::UObject* context, RC::Unreal::UFunction* function) -> void; - - // [Thread-ImGui] Dequeues call info. - // max_ms - maximum wall time (ms) to spend dequeuing. - // max_count_per_iteration - max items pulled per bulk-dequeue. - // on_dequeue - receives the dequeued entry by rvalue-ref (move). - auto dequeue(uint16_t max_ms, uint16_t max_count_per_iteration, const std::function& on_dequeue) -> void; - - // [Thread-ImGui] Pauses stream, removes hooks. Also drains queue. - auto stop() -> bool; - - // [Thread-ImGui] - [[nodiscard]] auto is_paused() const -> bool; - - // [Thread-ImGui] Resumes stream by installing hooks. - auto start() -> bool; - - // [Thread-ImGui] - auto set_imgui_thread_id(std::thread::id id) -> void; - - // [Thread-ImGui] - [[nodiscard]] auto get_imgui_thread_id() const -> std::thread::id; - - // [Thread-ImGui] - [[nodiscard]] auto get_average_enqueue_time() const -> double; - - // [Thread-ImGui] - [[nodiscard]] auto get_average_dequeue_time() const -> double; - - ~Middleware(); - - private: - Middleware(); - - auto assert_on_imgui_thread() const -> void; - [[nodiscard]] auto is_tick_fn(const RC::Unreal::UFunction* fn) const -> bool; - auto stop_impl(bool do_assert) -> bool; - - template - struct HookController - { - Unreal::Hook::GlobalCallbackId (*register_prehook_fn)(CallbackType, Unreal::Hook::FCallbackOptions){}; - Unreal::Hook::GlobalCallbackId (*register_posthook_fn)(CallbackType, Unreal::Hook::FCallbackOptions){}; - CallbackType m_pre_callback{}; - CallbackType m_post_callback{}; - Unreal::Hook::GlobalCallbackId m_prehook_id = Unreal::Hook::ERROR_ID; - Unreal::Hook::GlobalCallbackId m_posthook_id = Unreal::Hook::ERROR_ID; - - auto install_prehook() -> bool; - auto install_posthook() -> bool; - auto unhook() -> void; - auto is_hooked() const -> bool; - - inline static Unreal::Hook::FCallbackOptions m_cb_options{false, true, STR("EventViewer"), STR("CallStackMonitor")}; - }; - - private: - HookController m_pe_controller{}; - HookController m_pi_controller{}; - HookController m_plsf_controller{}; - - bool m_paused = true; - - std::thread::id m_imgui_id{}; - - // Queue - moodycamel::ConcurrentQueue m_queue{}; - moodycamel::ConsumerToken m_imgui_consumer_token{m_queue}; - std::vector m_buffer{}; - - // Detected tick functions - std::unordered_set m_tick_fns{}; - - inline static thread_local uint32_t m_depth = 0; - std::atomic_uint64_t m_depth_reset_counter = 0; - - std::atomic_flag m_allow_queue; - }; - - template - auto Middleware::HookController::install_prehook() -> bool - { - if (m_prehook_id != RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install prehook because it's already installed!")); - return false; - } - - if (!register_prehook_fn) - { - Output::send(STR("[EventViewerMod] Failed to install prehook because register function is unknown! Is FName.toString known?")); - return false; - } - - m_prehook_id = register_prehook_fn(m_pre_callback, m_cb_options); - - if (m_prehook_id == RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install prehook!")); - return false; - } - - return true; - } - - template - auto Middleware::HookController::install_posthook() -> bool - { - if (m_posthook_id != RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install posthook because it's already installed!")); - return false; - } - - if (!register_posthook_fn) - { - Output::send(STR("[EventViewerMod] Failed to install posthook because register function is unknown! Is FName.toString known?")); - return false; - } - - m_posthook_id = register_posthook_fn(m_post_callback, m_cb_options); - - if (m_posthook_id == RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install posthook!")); - return false; - } - - return true; - } - - template - auto Middleware::HookController::unhook() -> void - { - if (m_prehook_id == RC::Unreal::Hook::ERROR_ID && m_posthook_id == RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to remove hooks because it's not active!")); - return; - } - - if (!RC::Unreal::Hook::UnregisterCallback(m_prehook_id)) - { - Output::send(STR("[EventViewerMod] Failed to unregister prehook!")); - } - - if (!RC::Unreal::Hook::UnregisterCallback(m_posthook_id)) - { - Output::send(STR("[EventViewerMod] Failed to unregister posthook!")); - } - - m_prehook_id = m_posthook_id = RC::Unreal::Hook::ERROR_ID; - } - - template - auto Middleware::HookController::is_hooked() const -> bool - { - return (m_prehook_id != RC::Unreal::Hook::ERROR_ID && m_posthook_id != RC::Unreal::Hook::ERROR_ID); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/QueueProfiler.hpp b/cppmods/EventViewerMod/include/QueueProfiler.hpp deleted file mode 100644 index 33e1337d..00000000 --- a/cppmods/EventViewerMod/include/QueueProfiler.hpp +++ /dev/null @@ -1,92 +0,0 @@ -#pragma once - -// EventViewerMod: Lightweight queue timing instrumentation. -// -// This is not a general-purpose profiler; it's a small helper to measure enqueue/dequeue costs -// and queue backlog under real game load. Numbers are aggregated and displayed in the UI. - -#include -#include - -class QueueProfiler -{ - public: - static void BeginEnqueue() - { - enqueue_count.fetch_add(1, std::memory_order_relaxed); - enqueue_start = std::chrono::high_resolution_clock::now(); - } - - static void EndEnqueue() - { - enqueue_total.fetch_add(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - enqueue_start).count(), - std::memory_order_relaxed); - } - - static double GetEnqueueAverage() - { - return (enqueue_total.load(std::memory_order_relaxed) / static_cast(enqueue_count.load(std::memory_order_relaxed))); - } - - static void BeginDequeue() - { - dequeue_count.fetch_add(1, std::memory_order_relaxed); - dequeue_start = std::chrono::high_resolution_clock::now(); - } - - static void EndDequeue() - { - const auto now = std::chrono::high_resolution_clock::now(); - dequeue_total.fetch_add(std::chrono::duration_cast(now - dequeue_start).count(), std::memory_order_relaxed); - } - - static double GetDequeueAverage() - { - return (dequeue_total.load(std::memory_order_relaxed) / static_cast(dequeue_count.load(std::memory_order_relaxed))); - } - - static void AddPendingCount(const int64_t pending) - { - pending_count.fetch_add(1, std::memory_order_relaxed); - pending_total.fetch_add(pending, std::memory_order_relaxed); - } - - static void AddTimeExceededCount() - { - time_exceeded_total.fetch_add(1, std::memory_order_relaxed); - } - - static uint64_t GetTimeExceededCount() - { - return time_exceeded_total.load(std::memory_order_relaxed); - } - - static double GetPendingAverage() - { - return (pending_total.load(std::memory_order_relaxed) / static_cast(pending_count.load(std::memory_order_relaxed))); - } - - static void Reset() - { - enqueue_count.store(0, std::memory_order_release); - enqueue_total.store(0, std::memory_order_release); - dequeue_count.store(0, std::memory_order_release); - dequeue_total.store(0, std::memory_order_release); - pending_total.store(0, std::memory_order_release); - pending_count.store(0, std::memory_order_release); - time_exceeded_total.store(0, std::memory_order_release); - } - - private: - inline static std::atomic_int64_t enqueue_total = 0; - inline static std::atomic_int64_t dequeue_total = 0; - inline static std::atomic_int64_t pending_total = 0; - inline static std::atomic_int64_t time_exceeded_total = 0; - - inline static std::atomic_int64_t enqueue_count = 0; - inline static std::atomic_int64_t dequeue_count = 0; - inline static std::atomic_int64_t pending_count = 0; - - inline static thread_local std::chrono::time_point enqueue_start; - inline static thread_local std::chrono::time_point dequeue_start; -}; \ No newline at end of file diff --git a/cppmods/EventViewerMod/include/StringPool.hpp b/cppmods/EventViewerMod/include/StringPool.hpp deleted file mode 100644 index 7d550974..00000000 --- a/cppmods/EventViewerMod/include/StringPool.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -// EventViewerMod: String interning + hashing. -// -// Unreal provides stable numeric identifiers for names (ComparisonIndex). StringPool uses those to: -// - Deduplicate storage for function/caller strings. -// - Provide fast comparisons via hashes (avoid expensive string comparisons in hot paths). -// - Cache both original and lowercased strings for case-insensitive filtering. -// -// The pool is designed for “grow-only” lifetime during a session; string_views returned from the pool -// stay valid as long as the underlying storage isn't cleared. - -#include -#include -#include -#include -#include - -#include - -#include - -namespace RC::EventViewerMod -{ - class StringPool - { - public: - // Returns string_views owned by the pool: - // - full_name / function_name for display - // - lower_cased_* for case-insensitive filtering - // - function_hash for fast equality on function identity - auto get_strings(RC::Unreal::UObject* caller, RC::Unreal::UFunction* function) -> AllNameStringViews; - - // Returns path name of a function, same as calling GetPathName but with a string view instead. - auto get_path_name(uint32_t function_hash) -> std::string_view; - - // Clears the string pool. Currently unused, but may be useful if games ever start recycling FName indices. - // Note that this will invalidate any string_views acquired through the getters, so the ImGui views should be cleared before doing this - // in the same frame. - auto clear() -> void; - - static auto GetInstance() -> StringPool&; - - StringPool(const StringPool& Other) = delete; - StringPool(StringPool&& Other) noexcept = delete; - StringPool& operator=(const StringPool& Other) = delete; - StringPool& operator=(StringPool&& Other) noexcept = delete; - - private: - StringPool() = default; - - struct StringInfo - { - size_t function_begin; - std::string full_name; - std::string lower_cased_full_name; - }; - - // TODO replace with better concurrent hash map solution, though not too important for now - std::unordered_map m_main_pool; - // hashed by function->GetComparisonIndex and caller->GetComparisonIndex - std::unordered_map m_path_pool; // hashed by function->GetComparisonIndex - std::shared_mutex m_mutex; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/Structs.hpp b/cppmods/EventViewerMod/include/Structs.hpp deleted file mode 100644 index 0c395822..00000000 --- a/cppmods/EventViewerMod/include/Structs.hpp +++ /dev/null @@ -1,156 +0,0 @@ -#pragma once - -// EventViewerMod: Data model + UI state. -// -// This header defines: -// - String-view “bundles” (FunctionNameStringViews / AllNameStringViews) returned by StringPool. -// - Capture entries (CallStackEntry, CallFrequencyEntry). -// - Per-thread buffers and persistent UI state. -// -// Many fields are intentionally plain and public: these objects are moved through the queue and -// stored in large vectors/lists, so keeping them trivially movable matters. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -namespace RC::EventViewerMod -{ - static_assert(std::is_same_v, - "EventViewerMod expects StringType to be std::wstring for ImGui encoding; needs refactor if that changes."); - - struct FunctionNameStringViews - { - // Stable identifier for the UFunction across frames. - // Used for fast frequency aggregation (no string compares in the hot path). - uint32_t function_hash; // FName.ComparisonIndex of UFunction - std::string_view function_name; - std::string_view lower_cased_function_name; - }; - - struct AllNameStringViews : FunctionNameStringViews - { - std::string_view full_name; - std::string_view lower_cased_full_name; - // Composite key for (caller, function). This avoids per-frame string concatenation. - // Layout: UFunction ComparisonIndex in upper 32 bits, caller UObject ComparisonIndex in lower 32 bits. - uint64_t full_hash; - }; - - // Note: these types are intentionally cheap-to-move so they can be passed through - // moodycamel::ConcurrentQueue by value (high throughput, minimal allocator churn). - struct EntryBase - { - EntryBase() = default; - explicit EntryBase(bool is_tick); - - bool is_tick = false; - - // Cached visibility bit (filters + tick toggle). - // Important: depth/indent is NOT recomputed when entries are hidden; callers may be hidden while - // deeper frames remain visible, to preserve the true call depth. - bool is_disabled = false; - }; - - // Stores both original-case and lower-cased string views (for case-insensitive filtering). - struct CallStackEntry : EntryBase, AllNameStringViews - { - CallStackEntry() = default; - CallStackEntry(EMiddlewareHookTarget hook_target, const AllNameStringViews& strings, uint32_t depth, std::thread::id thread_id, bool is_tick); - - auto render(int indent_delta, ECallStackEntryRenderFlags_ flags = ECallStackEntryRenderFlags_None) const -> void; - - // Provides a copy of the entry's key string fields. Don't use with ImGui, only use for logging/saving. - // Use the string_views for ImGui since they utilize the string pool. - auto to_string_with_prefix() const -> std::wstring; - - // Which hook produced this entry. This is captured at enqueue-time and later used as a *view filter* - // ("All" shows everything; other targets show only matching entries). - EMiddlewareHookTarget hook_target = EMiddlewareHookTarget::All; - - // Unified depth counter shared by all hooks; PE can call PI which can call PLSF, etc. - uint32_t depth = 0; - std::thread::id thread_id{}; - - private: - auto render_indents(int indent_delta) const -> void; - auto render_support_menus(ECallStackEntryRenderFlags_ flags) const -> void; - }; - - // Stores both original-case and lower-cased function name views (for case-insensitive filtering). - struct CallFrequencyEntry : EntryBase, FunctionNameStringViews - { - CallFrequencyEntry() = default; - CallFrequencyEntry(const FunctionNameStringViews& strings, bool is_tick); - auto render(ECallFrequencyEntryRenderFlags_ flags) const -> void; - uint64_t frequency = 1; - - // OR'd EMiddlewareHookTarget values that have invoked this function so far. - // This is used for filtering when a specific hook target is selected. - uint32_t source_flags = 0; - - private: - auto render_support_menus() const -> void; - }; - - struct ThreadInfo - { - explicit ThreadInfo(std::thread::id thread_id); - - const std::thread::id thread_id; - const bool is_game_thread; - - // High-throughput capture history (fast filtering/search due to contiguous storage). - std::vector call_stack; - - // Set of entries that should be rendered, respecting the client's text_temp_virtualization_count - std::set call_stack_render_set{}; - - // Aggregated frequency view; list allows O(1) reordering via splice without shifting elements. - std::list call_frequencies; - - auto id_string() -> const char*; - auto clear() -> void; - - private: - std::string m_id_string; - }; - - struct UIState - { - bool enabled = false; // [Savable] [Thread-ImGui] - bool started = false; // [Thread-ImGui] - bool show_tick = true; // [Savable] [Thread-ImGui] - bool disable_indent_colors = false; // [Savable] [Thread-ImGui] - bool thread_explicitly_chosen = false; // [Thread-ImGui] User selected a thread - bool thread_implicitly_set = false; // [Thread-ImGui] System set thread to game thread if not explicit - bool show_filter_counts = true; // [Savable] [Thread-ImGui] - EMiddlewareHookTarget hook_target = EMiddlewareHookTarget::All; // [Savable] [Thread-ImGui] - EMode mode = EMode::Stack; // [Savable] [Thread-ImGui] - uint16_t dequeue_max_ms = 10; // [Savable] [Thread-ImGui] - uint16_t text_virtualization_count = 100; // [Savable] [Thread-ImGui] - uint64_t text_temp_virtualization_count = text_virtualization_count; // [Thread-Imgui] - uint32_t dequeue_max_count = 100000; // [Savable] [Thread-ImGui] - std::string blacklist; // [Savable] [Thread-ImGui] - std::vector blacklist_tokens; // [Thread-ImGui] (lower-cased tokens) - std::string whitelist; // [Savable] [Thread-ImGui] - std::vector whitelist_tokens; // [Thread-ImGui] (lower-cased tokens) - std::vector threads{}; // [Thread-ImGui] - int current_thread = 0; // [Thread-ImGui] - std::atomic_flag needs_save = ATOMIC_FLAG_INIT; // [Thread-Any] - std::string last_save_path; // [Thread-ImGui] - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/Client.cpp b/cppmods/EventViewerMod/src/Client.cpp deleted file mode 100644 index 380f634f..00000000 --- a/cppmods/EventViewerMod/src/Client.cpp +++ /dev/null @@ -1,1099 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include - -#include -#include - -// EventViewerMod: UI renderer and history consumer. -// -// This file drives the ImGui tab. High-level flow: -// 1) Draw config + view controls. -// 2) Dequeue a bounded amount of entries from Middleware each frame. -// 3) Merge new entries into per-thread histories (stack + frequency). -// 4) Apply view filters (hook target selection, whitelist/blacklist, tick toggle). -// -// Important: -// - The hook target combo is a *display filter only*. Depth is always computed by Middleware and -// remains unchanged even if callers are hidden. -// - Filtering is case-insensitive by comparing lower-cased strings (see to_lower_ascii_copy()). -// - -// Returns lower-cased tokens (copied strings). -static std::vector split_string_by_comma(const std::string& string) -{ - std::vector result; - if (string.empty()) - { - return result; - } - - const std::string_view sv{string}; - size_t start = 0; - - auto trim = [](std::string_view v) -> std::string_view { - const auto leading = v.find_first_not_of(" \t\n\r\f\v"); - if (leading == std::string_view::npos) - { - return {}; - } - v.remove_prefix(leading); - - const auto trailing = v.find_last_not_of(" \t\n\r\f\v"); - if (trailing == std::string_view::npos) - { - return {}; - } - v = v.substr(0, trailing + 1); - return v; - }; - - while (start <= sv.size()) - { - size_t end = sv.find(',', start); - if (end == std::string_view::npos) - { - end = sv.size(); - } - - auto token = trim(sv.substr(start, end - start)); - if (!token.empty()) - { - result.emplace_back(to_lower_case(token)); - } - - if (end == sv.size()) - { - break; - } - start = end + 1; - } - - return result; -} - -namespace RC::EventViewerMod -{ - using namespace std::literals::string_literals; - - Client::Client() : m_middleware(Middleware::GetInstance()) - { - const auto wd = std::filesystem::path{StringType{UE4SSProgram::get_program().get_working_directory()}}; - const auto mod_root = wd / "Mods" / "EventViewerMod"; - - m_cfg_path = mod_root / "config" / "settings.json"; - m_dump_dir = mod_root / "captures"; - - std::error_code ec; - std::filesystem::create_directories(m_cfg_path.parent_path(), ec); - std::filesystem::create_directories(m_dump_dir, ec); - - load_state(); - } - - auto Client::render() -> void - { - // Ensure middleware knows the correct ImGui thread - if (!m_imgui_thread_id_set) - { - m_middleware.set_imgui_thread_id(std::this_thread::get_id()); - m_imgui_thread_id_set = true; - } - - const auto saved = check_save_request(); - - if (ImGui::Checkbox("Enable", &m_state.enabled)) - { - request_save_state(); - - if (m_state.enabled) - { - // enabling - m_middleware.set_imgui_thread_id(std::this_thread::get_id()); - m_imgui_thread_id_set = true; - QueueProfiler::Reset(); - } - else - { - // disabling - m_middleware.stop(); - m_state.started = false; - m_state.needs_save.clear(std::memory_order_release); - clear_threads(); - if (!saved) - { - save_state(); - } - QueueProfiler::Reset(); - return; - } - } - - if (!m_state.enabled) return; - - render_cfg(); - - if (ImGui::TreeNode("Performance Options")) - { - render_perf_opts(); - ImGui::TreePop(); - } - - dequeue(); - - render_view(); - - if (ImGui::BeginPopupModal("Saved File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::Text("Saved file to %s", m_state.last_save_path.c_str()); - ImGui::PopTextWrapPos(); - if (ImGui::Button("OK")) ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - } - - auto Client::render_cfg() -> void - { - int hook_target_idx = 0; - for (int i = 0; i < EMiddlewareHookTarget_Size; ++i) - { - if (EMiddlewareHookTarget_ValueArray[i] == m_state.hook_target) - { - hook_target_idx = i; - break; - } - } - - if (combo_with_flags("Target", &hook_target_idx, EMiddlewareHookTarget_NameArray, EMiddlewareHookTarget_Size, ImGuiComboFlags_WidthFitPreview)) - { - request_save_state(); - m_state.hook_target = EMiddlewareHookTarget_ValueArray[hook_target_idx]; - - // Hook target is an implicit filter for the stack view. - // Rebuild per-thread render sets immediately so the UI reflects the new selection. - // (Disabled state is controlled by whitelist/blacklist/tick and is handled elsewhere.) - for (auto& thread : m_state.threads) - { - thread.call_stack_render_set.clear(); - resize_render_set(thread, m_state.text_temp_virtualization_count); - } - } - HelpMarker(HelpStrings::HELP_TARGET); - ImGui::SameLine(); - - if (combo_with_flags("Mode", reinterpret_cast(&m_state.mode), EMode_NameArray, EMode_Size, ImGuiComboFlags_WidthFitPreview)) - { - request_save_state(); - } - HelpMarker(HelpStrings::HELP_MODE); - - bool whitelist_changed = false; - bool blacklist_changed = false; - bool tick_changed = false; - - // whitelist - const auto wl_input_changed = ImGui::InputText("Whitelist", &m_state.whitelist, ImGuiInputTextFlags_ElideLeft | ImGuiInputTextFlags_EnterReturnsTrue); - ImGui::SameLine(); - if (wl_input_changed || ImGui::Button("Apply##Whitelist")) - { - whitelist_changed = true; - request_save_state(); - } - ImGui::SameLine(); - if (ImGui::Button("Clear##Whitelist")) - { - if (!m_state.whitelist.empty()) - { - m_state.whitelist.clear(); - whitelist_changed = true; - request_save_state(); - } - } - HelpMarker(HelpStrings::HELP_LIST_FILTER); - - // blacklist - const auto bl_input_changed = ImGui::InputText("Blacklist", &m_state.blacklist, ImGuiInputTextFlags_ElideLeft | ImGuiInputTextFlags_EnterReturnsTrue); - ImGui::SameLine(); - if (bl_input_changed || ImGui::Button("Apply##Blacklist")) - { - blacklist_changed = true; - request_save_state(); - } - ImGui::SameLine(); - if (ImGui::Button("Clear##Blacklist")) - { - if (!m_state.blacklist.empty()) - { - m_state.blacklist.clear(); - blacklist_changed = true; - request_save_state(); - } - } - auto& threads = m_state.threads; - - if (!threads.empty()) - { - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - if (static_cast(m_state.current_thread) >= threads.size()) - { - m_state.current_thread = static_cast(threads.size() - 1); - } - - ThreadInfo* game_thread = nullptr; - if (ImGui::BeginCombo("Thread", threads[m_state.current_thread].id_string(), ImGuiComboFlags_WidthFitPreview)) - { - for (size_t idx = 0; idx < threads.size(); ++idx) - { - const bool selected = static_cast(idx) == m_state.current_thread; - auto& thread = threads[idx]; - if (ImGui::Selectable(thread.id_string(), selected)) - { - m_state.thread_explicitly_chosen = true; - m_state.current_thread = static_cast(idx); - } - if (selected) - { - ImGui::SetItemDefaultFocus(); - } - if (thread.is_game_thread) - { - game_thread = &thread; - } - } - ImGui::EndCombo(); - } - else if (!m_state.thread_implicitly_set) - { - for (auto& thread : threads) - { - if (thread.is_game_thread) game_thread = &thread; - } - - if (game_thread && !m_state.thread_explicitly_chosen) - { - m_state.current_thread = static_cast(game_thread - threads.data()); - ImGui::SetItemDefaultFocus(); - m_state.thread_implicitly_set = true; - } - } - HelpMarker(HelpStrings::HELP_THREAD); - } - - auto save_mode = ESaveMode::none; - - // controls - if (ImGui::Button(m_state.started ? "Stop" : "Start")) - { - m_state.started = !m_state.started; - if (m_state.started) - { - m_middleware.start(); - m_state.text_temp_virtualization_count = m_state.text_virtualization_count; - // shrink render sets - for (auto& thread : threads) - { - resize_render_set(thread, m_state.text_temp_virtualization_count); - } - } - else - { - m_middleware.stop(); - } - QueueProfiler::Reset(); - } - - ImGui::SameLine(); - if (ImGui::Button("Clear##CurrentThread") && !threads.empty()) - { - auto& thread = threads[m_state.current_thread]; - thread.clear(); - } - HelpMarker(HelpStrings::HELP_CLEAR); - ImGui::SameLine(); - if (ImGui::Button("Clear All##AllThreads") && !threads.empty()) - { - clear_threads(); - } - HelpMarker(HelpStrings::HELP_CLEAR_ALL); - ImGui::SameLine(); - if (ImGui::Button("Save##Current")) - { - save_mode = ESaveMode::current; - } - HelpMarker(HelpStrings::HELP_SAVE); - ImGui::SameLine(); - if (ImGui::Button("Save All##All")) - { - save_mode = ESaveMode::all; - } - HelpMarker(HelpStrings::HELP_SAVE_ALL); - ImGui::SameLine(); - if (ImGui::Checkbox("Show Builtin Tick Functions", &m_state.show_tick)) - { - tick_changed = true; - request_save_state(); - } - HelpMarker(HelpStrings::HELP_SHOW_BUILTIN_TICK); - ImGui::SameLine(); - if (ImGui::Checkbox("Disable Indent Colors", &m_state.disable_indent_colors)) - { - request_save_state(); - } - ImGui::SameLine(); - if (ImGui::Checkbox("Show Filter Counts", &m_state.show_filter_counts)) - { - request_save_state(); - } - - apply_filters_to_history(whitelist_changed, blacklist_changed, tick_changed); - save(save_mode); - } - - auto Client::render_perf_opts() -> void - { - static uint16_t step = 1; - if (ImGui::InputScalar("Max MS Read Time", ImGuiDataType_U16, &m_state.dequeue_max_ms, &step, 0, 0)) - { - request_save_state(); - if (!m_state.dequeue_max_ms) - { - m_state.dequeue_max_ms = 1; - } - } - HelpMarker(HelpStrings::HELP_MAX_MS_READ_TIME); - - if (ImGui::InputScalar("Max Count Per Iteration", ImGuiDataType_U32, &m_state.dequeue_max_count, &step)) - { - request_save_state(); - if (!m_state.dequeue_max_count) - { - m_state.dequeue_max_count = 1; - } - } - HelpMarker(HelpStrings::HELP_MAX_COUNT_PER_ITERATION); - - if (ImGui::InputScalar("Text Virtualization Count", ImGuiDataType_U16, &m_state.text_virtualization_count, &step)) - { - request_save_state(); - if (!m_state.text_virtualization_count) - { - m_state.text_virtualization_count = 1; - } - m_state.text_temp_virtualization_count = m_state.text_virtualization_count; - clear_threads(); - } - HelpMarker(HelpStrings::HELP_TEXT_VIRTUALIZATION_COUNT); - - ImGui::Text("Enqueue Avg: %f Dequeue Avg: %f Pending Avg: %f Time Slot Exceeded Count: %llu", - QueueProfiler::GetEnqueueAverage(), - QueueProfiler::GetDequeueAverage(), - QueueProfiler::GetPendingAverage(), - QueueProfiler::GetTimeExceededCount()); - HelpMarker(HelpStrings::HELP_QUEUE_PROFILE_VALUES); - } - - auto Client::render_view() -> void - { - auto& threads = m_state.threads; - if (threads.empty()) - { - return; - } - - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - if (static_cast(m_state.current_thread) >= threads.size()) - { - m_state.current_thread = static_cast(threads.size() - 1); - } - - auto& thread = threads[m_state.current_thread]; - - const auto selected_flags = static_cast(m_state.hook_target); - - auto area = ImGui::GetContentRegionAvail(); - auto& padding = ImGui::GetStyle().WindowPadding; - auto& scroll_size = ImGui::GetStyle().ScrollbarSize; - area.y -= ((padding.y + scroll_size) * 2); - area.x -= (padding.x + scroll_size); - ImGui::BeginChild("##view", area, ImGuiChildFlags_Borders | ImGuiChildFlags_FrameStyle | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_HorizontalScrollbar); - if (m_state.mode == EMode::Stack) - { - if (thread.call_stack.empty()) - { - ImGui::EndChild(); - return; - } - - int prev_depth = 0; - bool have_prev = false; - int current_indent = 0; - int id = 0; - uint8_t entry_flags = 0; - if (!m_state.disable_indent_colors) entry_flags |= ECallStackEntryRenderFlags_IndentColors; - if (!m_state.started) entry_flags |= ECallStackEntryRenderFlags_WithSupportMenusCallStackModal; - - bool needs_scroll_here = false; - if (!m_state.started && thread.call_stack_render_set.size() == m_state.text_temp_virtualization_count) - { - if (ImGui::Button("Load More...")) - { - m_state.text_temp_virtualization_count *= 2; - // expand set - resize_render_set(thread, m_state.text_temp_virtualization_count); - needs_scroll_here = true; - } - } - - const bool show_filter_counts = m_state.show_filter_counts; - const auto set_begin = thread.call_stack_render_set.begin(); - for (auto entry_it = set_begin; entry_it != thread.call_stack_render_set.end(); ++entry_it) - { - auto& entry = thread.call_stack[*entry_it]; - if (show_filter_counts && entry_it != set_begin) [[likely]] - { - auto before_entry_it = entry_it; - --before_entry_it; - const auto gap = (*entry_it) - (*before_entry_it); - if (gap > 1) - { - FilterCountRenderer::render(gap - 1, !m_state.started); - } - } - - const int depth = static_cast(entry.depth); - const int delta = have_prev ? (depth - prev_depth) : depth; - ImGui::PushID(id++); - entry.render(delta, static_cast(entry_flags)); - ImGui::PopID(); - current_indent += delta; - prev_depth = depth; - have_prev = true; - } - - // Reset indent state for safety. - while (current_indent > 0) - { - ImGui::Unindent(); - --current_indent; - } - - // if (show_filter_counts) m_filter_count_renderer.render_and_reset(!m_state.show_filter_counts); - - if (m_state.started || needs_scroll_here) ImGui::SetScrollHereY(1.0f); - } - - else - { - if (ImGui::BeginTable("##frequency", 2, ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersV)) - { - int id = 0; - for (const auto& entry : thread.call_frequencies) - { - if ((entry.source_flags & selected_flags) == 0) - { - continue; - } - - if (entry.is_disabled) - { - continue; - } - ImGui::PushID(id++); - entry.render(m_state.started ? ECallFrequencyEntryRenderFlags_None : ECallFrequencyEntryRenderFlags_WithSupportMenus); - ImGui::PopID(); - } - - ImGui::EndTable(); - } - } - - ImGui::EndChild(); - - if (m_entry_call_stack_renderer) - { - if (!m_entry_call_stack_renderer->render()) - { - m_entry_call_stack_renderer = nullptr; - } - } - } - - auto Client::combo_with_flags(const char* label, int* current_item, const char* const items[], const int items_count, const ImGuiComboFlags_ flags) -> bool - { - bool changed = false; - if (ImGui::BeginCombo(label, items[*current_item], flags)) - { - for (int i = 0; i < items_count; ++i) - { - const auto is_selected = i == *current_item; - if (ImGui::Selectable(items[i], is_selected)) - { - *current_item = i; - changed = true; - } - if (is_selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - return changed; - } - - auto Client::save_state() -> void - { - std::error_code ec; - std::filesystem::create_directories(m_cfg_path.parent_path(), ec); - - std::unordered_map state_map; - state_map.emplace("Enabled", std::to_string(m_state.enabled)); - state_map.emplace("ShowTick", std::to_string(m_state.show_tick)); - int hook_target_idx = 0; - for (int i = 0; i < EMiddlewareHookTarget_Size; ++i) - { - if (EMiddlewareHookTarget_ValueArray[i] == m_state.hook_target) - { - hook_target_idx = i; - break; - } - } - state_map.emplace("HookTarget", std::to_string(hook_target_idx)); - state_map.emplace("Mode", std::to_string(static_cast(m_state.mode))); - state_map.emplace("DequeueMaxMs", std::to_string(m_state.dequeue_max_ms)); - state_map.emplace("DequeueMaxCount", std::to_string(m_state.dequeue_max_count)); - state_map.emplace("Whitelist", m_state.whitelist); - state_map.emplace("Blacklist", m_state.blacklist); - state_map.emplace("DisableIndentColors", std::to_string(m_state.disable_indent_colors)); - state_map.emplace("TextVirtualizationCount", std::to_string(m_state.text_virtualization_count)); - state_map.emplace("ShowFilterCounts", std::to_string(m_state.show_filter_counts)); - (void)glz::write_file_json(state_map, m_cfg_path.string(), std::string{}); - } - - auto Client::load_state() -> void - { - if (!std::filesystem::exists(m_cfg_path) || !std::filesystem::is_regular_file(m_cfg_path)) - { - return; - } - - std::unordered_map state_map{}; - auto ec = glz::read_file_json(state_map, m_cfg_path.string(), std::string{}); - if (ec.ec != glz::error_code::none) - { - return; - } - - try - { - m_state.enabled = state_map.at("Enabled") != "0"; - m_state.show_tick = state_map.at("ShowTick") != "0"; - m_state.disable_indent_colors = state_map.at("DisableIndentColors") != "0"; - const int hook_target_idx = std::stoi(state_map.at("HookTarget")); - if (hook_target_idx >= 0 && hook_target_idx < EMiddlewareHookTarget_Size) - { - m_state.hook_target = EMiddlewareHookTarget_ValueArray[hook_target_idx]; - } - else - { - m_state.hook_target = EMiddlewareHookTarget::All; - } - m_state.mode = static_cast(std::stoi(state_map.at("Mode"))); - m_state.dequeue_max_ms = static_cast(std::stoi(state_map.at("DequeueMaxMs"))); - m_state.dequeue_max_count = static_cast(std::stoul(state_map.at("DequeueMaxCount"))); - m_state.whitelist = state_map.at("Whitelist"); - m_state.blacklist = state_map.at("Blacklist"); - m_state.whitelist_tokens = split_string_by_comma(m_state.whitelist); - m_state.blacklist_tokens = split_string_by_comma(m_state.blacklist); - m_state.text_virtualization_count = static_cast(std::stoi(state_map.at("TextVirtualizationCount"))); - m_state.text_temp_virtualization_count = m_state.text_virtualization_count; - m_state.show_filter_counts = state_map.at("ShowFilterCounts") != "0"; - clear_threads(); // just to be safe, since if text_virtualization_count changes while scrolling it could cause problems - } - catch (...) - { - Output::send(STR("[EventViewerMod] Failed to load state from file due to exception!")); - } - } - - auto Client::check_save_request() -> bool - { - if (m_state.needs_save.test(std::memory_order_acquire)) - { - save_state(); - m_state.needs_save.clear(std::memory_order_release); - return true; - } - return false; - } - - auto Client::apply_filters_to_history(const bool whitelist_changed, const bool blacklist_changed, const bool tick_changed) -> void - { - if (!(whitelist_changed || blacklist_changed || tick_changed)) - { - return; - } - if (whitelist_changed) - { - m_state.whitelist_tokens = split_string_by_comma(m_state.whitelist); - } - if (blacklist_changed) - { - m_state.blacklist_tokens = split_string_by_comma(m_state.blacklist); - } - - // Recompute disabled state for all history. This only runs when the user changes filters/tick setting. - const bool show_tick = m_state.show_tick; - - for (auto& thread : m_state.threads) - { - // Stack history can get very large; leverage parallel execution on random-access iterators. - std::for_each(std::execution::par_unseq, thread.call_stack.begin(), thread.call_stack.end(), [this, show_tick](CallStackEntry& entry) { - entry.is_disabled = (entry.is_tick && !show_tick) || !passes_filters(entry.lower_cased_full_name); - }); - - // Frequency view is smaller and is a list (non-random-access). - for (auto& entry : thread.call_frequencies) - { - entry.is_disabled = (entry.is_tick && !show_tick) || !passes_filters(entry.lower_cased_function_name); - } - - // Recalculate render set - thread.call_stack_render_set.clear(); - resize_render_set(thread, m_state.text_temp_virtualization_count); - } - } - - auto Client::dequeue() -> void - { - if (!m_state.enabled || !m_state.started) - { - return; - } - - m_middleware.dequeue(m_state.dequeue_max_ms, m_state.dequeue_max_count, [this](CallStackEntry&& entry) { - // Thread lookup/creation (unified across hook targets). - auto& threads = m_state.threads; - - const auto entry_thread = entry.thread_id; - auto thread_it = std::ranges::find_if(threads, [&entry_thread](const ThreadInfo& info) { - return info.thread_id == entry_thread; - }); - - ThreadInfo* thread_ptr = nullptr; - if (thread_it != threads.end()) - { - thread_ptr = &(*thread_it); - } - else - { - thread_ptr = &threads.emplace_back(entry_thread); - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - } - - auto& thread = *thread_ptr; - - // Determine disabled state under current filters. - // If it doesn't pass freq, it won't pass stack - const auto freq_disabled = (entry.is_tick && !m_state.show_tick) || !passes_filters(entry.lower_cased_function_name); - const auto stack_disabled = freq_disabled || ((entry.is_tick && !m_state.show_tick) || !passes_filters(entry.lower_cased_full_name)); - - // Frequency tracking: bump existing, or add. - auto freq_it = std::ranges::find_if(thread.call_frequencies, [&entry](const CallFrequencyEntry& freq_entry) -> bool { - return entry.function_hash == freq_entry.function_hash; - }); - - const auto entry_source_flags = static_cast(entry.hook_target); - - if (freq_it != thread.call_frequencies.end()) [[likely]] - { - auto& freq = *freq_it; - ++freq.frequency; - freq.is_disabled = freq_disabled; - freq.source_flags |= entry_source_flags; - - // Maintain descending order by frequency using list::splice (fast, no alloc). - auto new_pos = freq_it; - while (new_pos != thread.call_frequencies.begin()) - { - auto prev = std::prev(new_pos); - if (prev->frequency > freq.frequency) - { - break; - } - new_pos = prev; - } - if (new_pos != freq_it) - { - thread.call_frequencies.splice(new_pos, thread.call_frequencies, freq_it); - } - } - else - { - thread.call_frequencies.emplace_back(static_cast(entry), entry.is_tick); - auto& freq = thread.call_frequencies.back(); - freq.is_disabled = freq_disabled; - freq.source_flags = entry_source_flags; - } - - // Call stack history. - entry.is_disabled = stack_disabled; - if (can_render_entry(thread.call_stack.emplace_back(std::move(entry)))) - { - if (thread.call_stack_render_set.size() == m_state.text_temp_virtualization_count) [[likely]] - { - auto extracted = thread.call_stack_render_set.extract(thread.call_stack_render_set.begin()); - extracted.value() = thread.call_stack.size() - 1; - thread.call_stack_render_set.insert(thread.call_stack_render_set.end(), std::move(extracted)); - } - else [[unlikely]] - { - thread.call_stack_render_set.insert(thread.call_stack_render_set.end(), thread.call_stack.size() - 1); - } - } - }); - } - - // todo there's definitely room for improvement, like diffing white/blacklists to tell if test_str needs to be checked - // (would be done by callers probably) or keeping track of an 'enabled' and 'disabled' unordered_set of - // hashes (that the StringPool could be altered to provide) to skip string parsing, but this is good enough for now. - auto Client::passes_filters(const std::string_view test_str) const -> bool - { - bool passes_whitelist = m_state.whitelist_tokens.empty(); - for (const auto& token : m_state.whitelist_tokens) // any whitelist token present => pass - { - if (test_str.contains(token)) - { - passes_whitelist = true; - break; - } - } - if (!passes_whitelist) - { - return false; - } - - for (const auto& token : m_state.blacklist_tokens) // any blacklist token present => fail - { - if (test_str.contains(token)) - { - return false; - } - } - return true; - } - - auto Client::save(ESaveMode mode) -> void - { - if (mode == ESaveMode::none) - { - return; - } - - std::error_code ec; - std::filesystem::create_directories(m_dump_dir, ec); - - const auto now = std::chrono::system_clock::now(); - const std::time_t now_t = std::chrono::system_clock::to_time_t(now); - std::tm local_tm{}; - localtime_s(&local_tm, &now_t); - - std::ostringstream oss; - // Windows filenames cannot contain ':'. - oss << std::put_time(&local_tm, "%Y-%m-%d %H-%M-%S"); - - if (mode == ESaveMode::current) - { - auto& threads = m_state.threads; - if (threads.empty()) - { - return; - } - - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - if (static_cast(m_state.current_thread) >= threads.size()) - { - m_state.current_thread = static_cast(threads.size() - 1); - } - - const auto filename = "EventViewerMod Capture-"s + to_string(m_state.hook_target) + "-" + EMode_NameArray[static_cast(m_state.mode)] + " " + - oss.str() + ".txt"; - const auto path = m_dump_dir / filename; - std::ofstream file{path}; - if (!file.is_open()) - { - return; - } - - file << to_string(m_state.hook_target) << " "; - serialize_view(threads[m_state.current_thread], m_state.mode, m_state.hook_target, file); - file.close(); - m_state.last_save_path = path.string(); - ImGui::OpenPopup("Saved File"); - return; - } - - if (mode == ESaveMode::all) - { - if (m_state.threads.empty()) - { - return; - } - - const auto filename = "EventViewerMod Capture-All "s + oss.str() + ".txt"; - const auto path = m_dump_dir / filename; - std::ofstream file{path}; - if (!file.is_open()) - { - return; - } - - serialize_all_views(file); - file.close(); - m_state.last_save_path = path.string(); - ImGui::OpenPopup("Saved File"); - } - } - - auto Client::serialize_view(ThreadInfo& info, const EMode mode, const EMiddlewareHookTarget hook_target, std::ofstream& out) const -> void - { - out << fmt::format("Thread {} {}\n\n", info.id_string(), EMode_NameArray[static_cast(mode)]); - - const uint32_t selected_flags = static_cast(hook_target); - - if (mode == EMode::Stack) - { - if (info.call_stack.empty()) - { - out << "No captures.\n\n\n"; - return; - } - - for (const auto& entry : info.call_stack) - { - if ((static_cast(entry.hook_target) & selected_flags) == 0) - { - continue; - } - - if (entry.is_disabled) - { - continue; - } - for (auto i = 0u; i < entry.depth; ++i) - out << "\t"; - out << entry.full_name << '\n'; - } - - out << "\n\n\n"; - return; - } - - if (info.call_frequencies.empty()) - { - out << "No captures.\n\n\n"; - return; - } - - for (const auto& entry : info.call_frequencies) - { - if ((entry.source_flags & selected_flags) == 0) - { - continue; - } - - if (entry.is_disabled) - { - continue; - } - out << entry.function_name << '\t' << entry.frequency << '\n'; - } - - out << "\n\n\n"; - } - - auto Client::serialize_all_views(std::ofstream& out) -> void - { - for (auto& thread : m_state.threads) - { - out << to_string(EMiddlewareHookTarget::All) << " "; - serialize_view(thread, EMode::Stack, EMiddlewareHookTarget::All, out); - out << to_string(EMiddlewareHookTarget::All) << " "; - serialize_view(thread, EMode::Frequency, EMiddlewareHookTarget::All, out); - } - } - - auto Client::clear_threads() -> void - { - m_state.current_thread = 0; - m_state.threads.clear(); - m_state.thread_explicitly_chosen = false; - m_state.thread_implicitly_set = false; - } - - auto Client::can_render_entry(const CallStackEntry& entry) const -> bool - { - return !(entry.is_disabled || ((static_cast(entry.hook_target) & static_cast(m_state.hook_target)) == 0)); - } - - auto Client::resize_render_set(ThreadInfo& thread, const size_t max_size) const -> void - { - auto& stack = thread.call_stack; - auto& set = thread.call_stack_render_set; - - if (!max_size) return set.clear(); - if (set.size() == max_size) return; - - if (set.size() < max_size) - { - const auto cs_begin = stack.begin(); - for (auto entry_it = set.empty() ? stack.rbegin() : std::make_reverse_iterator(stack.begin() + *set.begin()); entry_it != stack.rend(); ++entry_it) - { - if (can_render_entry(*entry_it)) - { - set.insert(set.begin(), static_cast((entry_it.base() - cs_begin) - 1)); - if (set.size() == max_size) break; - } - } - - return; - } - - // set.size() > max_size - while (set.size() != max_size) - set.erase(set.begin()); - } - - auto Client::request_save_state() -> void - { - m_state.needs_save.test_and_set(std::memory_order_release); - } - - auto Client::add_to_white_list(const std::string_view item) -> void - { - if (m_state.whitelist.empty()) - { - m_state.whitelist += item; - } - else - { - m_state.whitelist += ", "; - m_state.whitelist += item; - } - request_save_state(); - apply_filters_to_history(true, false, false); - } - - auto Client::add_to_black_list(std::string_view item) -> void - { - if (m_state.blacklist.empty()) - { - m_state.blacklist += item; - } - else - { - m_state.blacklist += ", "; - m_state.blacklist += item; - } - request_save_state(); - apply_filters_to_history(false, true, false); - } - - auto Client::render_entry_stack_modal(const CallStackEntry* entry) -> void - { - if (m_entry_call_stack_renderer) return; - // find root entry and next root entry - // finding the root entry also reveals all callers, so go ahead and bookkeep it - const auto& stack = m_state.threads[m_state.current_thread].call_stack; - const size_t target_abs_idx = entry - stack.data(); - size_t root_abs_idx = target_abs_idx; - std::vector idxs_relevant_to_target{}; // added in reverse-view order - if (entry->depth) - { - uint32_t last_lowest_depth = entry->depth; - for (auto idx = target_abs_idx - 1; idx >= 0; --idx) - { - auto this_depth = stack[idx].depth; - if (this_depth < last_lowest_depth) - { - idxs_relevant_to_target.push_back(idx); - last_lowest_depth = this_depth; - if (this_depth == 0) - { - root_abs_idx = idx; - break; - } - } - } - } - - size_t next_root_abs_idx = target_abs_idx + 1; - bool out_of_target_scope = false; - for (; next_root_abs_idx < stack.size(); ++next_root_abs_idx) // find callers and callees of target - { - const auto this_entry_depth = stack[next_root_abs_idx].depth; - if (this_entry_depth == 0) break; - - if (this_entry_depth <= entry->depth) out_of_target_scope = true; - if (!out_of_target_scope) idxs_relevant_to_target.push_back(next_root_abs_idx); - } - - std::vector context{stack.begin() + root_abs_idx, stack.begin() + next_root_abs_idx}; // copy entries - for (auto& abs_idx : idxs_relevant_to_target) // make idxs_relevant_to_target relative to context - { - abs_idx -= root_abs_idx; - } - const auto target_rel_idx = target_abs_idx - root_abs_idx; // find context index for target - idxs_relevant_to_target.push_back(target_rel_idx); - - // make any irrelevant entry disabled by default, and assert that any relevant entry is enabled. the modal won't change them, but - // will use is_disabled as a flag to indicate its relevance for the checkbox. - for (size_t context_idx = 0; context_idx < context.size(); ++context_idx) - { - if (std::ranges::find(idxs_relevant_to_target, static_cast(context_idx)) != idxs_relevant_to_target.end()) - { - context[context_idx].is_disabled = false; - continue; - } - context[context_idx].is_disabled = true; - } - - m_entry_call_stack_renderer = std::make_unique(target_rel_idx, std::move(context)); - } - - auto Client::GetInstance() -> Client& - { - static Client client{}; - return client; - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp b/cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp deleted file mode 100644 index f7473ddf..00000000 --- a/cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include - -#include -#include -#include -#include - -// EventViewerMod: rendering for the call stack/context modal. -// -// The context vector is prepared by Client (based on the selected history entry). This renderer -// is intentionally simple: it only worries about ImGui state management and printing rows. -namespace RC::EventViewerMod -{ - using namespace std::literals::string_literals; - - EntryCallStackRenderer::EntryCallStackRenderer(const size_t target_idx, std::vector context) - : m_target_idx(target_idx), m_context(std::move(context)) - { - m_target_ptr = &m_context[m_target_idx]; - } - - auto EntryCallStackRenderer::render() -> bool - { - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - // BeginPopupModal() will *never* return true unless the popup has been opened. - // Because the renderer is created from a context-menu click (possibly in a different - // frame / popup), we request opening once here in the stable render path. - if (!m_requested_open) - { - ImGui::OpenPopup("Entry Call Stack##entrycallstackmodal"); - m_requested_open = true; - } - - if (ImGui::BeginPopupModal("Entry Call Stack##entrycallstackmodal", nullptr, ImGuiWindowFlags_HorizontalScrollbar)) - { - ImGui::Checkbox("Show Full Context", &m_show_full_context); - HelpMarker(HelpStrings::HELP_CEMODAL_SHOW_FULL_CONTEXT); - ImGui::Checkbox("Disable Indent Colors", &m_disable_indent_colors); - - const auto btn_height = ImGui::GetFrameHeightWithSpacing(); - auto view_area = ImGui::GetContentRegionAvail(); - view_area.y -= btn_height; - ImGui::BeginChild("##entrycallstackview", view_area, ImGuiChildFlags_Borders | ImGuiChildFlags_FrameStyle, ImGuiWindowFlags_HorizontalScrollbar); - - int prev_depth = 0; - bool have_prev = false; - int current_indent = 0; - int id = 0; - uint8_t flags = m_disable_indent_colors ? ECallStackEntryRenderFlags_None : ECallStackEntryRenderFlags_IndentColors; - flags |= ECallStackEntryRenderFlags_WithSupportMenus; - for (const auto& entry : m_context) - { - if (entry.is_disabled && !m_show_full_context) continue; - const int depth = static_cast(entry.depth); - const int delta = have_prev ? (depth - prev_depth) : depth; - ImGui::PushID(id++); - &entry != m_target_ptr ? entry.render(delta, static_cast(flags)) - : entry.render(delta, static_cast(flags | ECallStackEntryRenderFlags_Highlight)); - ImGui::PopID(); - current_indent += delta; - prev_depth = depth; - have_prev = true; - } - - // Reset indent state so the rest of the popup doesn't inherit the last depth. - while (current_indent > 0) - { - ImGui::Unindent(); - --current_indent; - } - - ImGui::EndChild(); - - if (ImGui::Button("Save##entrycallstacksave")) - { - save(); - } - HelpMarker(HelpStrings::HELP_CEMODAL_SAVE); - ImGui::SameLine(); - // NOTE: - // Do *not* early-return before EndPopup(). - // ImGui maintains multiple internal stacks (window stack, ID stack, etc.). - // Skipping EndPopup() will eventually trip assertions like "Calling PopId() too many times". - bool keep_open = true; - if (ImGui::Button("Close##entrycallstackclose")) - { - ImGui::CloseCurrentPopup(); - keep_open = false; - } - - if (ImGui::BeginPopupModal("Saved Entry File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::Text("Saved file to %s", m_last_save_path.c_str()); - ImGui::PopTextWrapPos(); - if (ImGui::Button("OK")) ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - - ImGui::EndPopup(); - - if (!keep_open) - { - return false; - } - } - - return true; - } - - auto EntryCallStackRenderer::save() -> void - { - static const auto wd = std::filesystem::path{StringType{UE4SSProgram::get_program().get_working_directory()}}; - static const auto captures_root = wd / "Mods" / "EventViewerMod" / "captures"; - std::error_code ec; - std::filesystem::create_directories(captures_root, ec); - - const auto now = std::chrono::system_clock::now(); - const std::time_t now_t = std::chrono::system_clock::to_time_t(now); - std::tm local_tm{}; - localtime_s(&local_tm, &now_t); - - std::ostringstream oss; - // Windows filenames cannot contain ':'. - oss << std::put_time(&local_tm, "%Y-%m-%d %H-%M-%S"); - - const auto filename = "EventViewerMod Capture-Entry "s + std::string(m_context[m_target_idx].function_name) + " " + oss.str() + ".txt"; - const auto path = captures_root / filename; - std::wofstream out{path}; - - for (const auto& entry : m_context) - { - if (entry.is_disabled && !m_show_full_context) continue; - auto str = entry.to_string_with_prefix(); - out << str; - } - - out.close(); - m_last_save_path = path.string(); - ImGui::OpenPopup("Saved Entry File"); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/EventViewer.cpp b/cppmods/EventViewerMod/src/EventViewer.cpp deleted file mode 100644 index 62b1077f..00000000 --- a/cppmods/EventViewerMod/src/EventViewer.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include - -// EventViewerMod: UE4SS mod entry point and ImGui tab wiring. -// -// This is the glue between UE4SS' mod lifecycle (start_mod/on_unreal_init) and our -// ImGui renderer + middleware. - -#include -#include -#include -#include - -namespace RC::EventViewerMod -{ - EventViewerMod::EventViewerMod() - { - ModName = STR("EventViewerMod"); - ModAuthors = STR("wildcherry"); - ModDescription = STR("Lets you view a live call stack of ProcessEvent and ProcessInternal, with additional call frequency tracking."); - ModVersion = STR("1.0.0"); - - register_tab(STR("EventViewer"), [](CppUserModBase* mod) { - UE4SS_ENABLE_IMGUI(); - - // Avoid unnecessary expensive dynamic_cast - ImGui::BeginDisabled(!static_cast(mod)->m_unreal_loaded.test()); // NOLINT(*-pro-type-static-cast-downcast) - auto& style = ImGui::GetStyle(); - const auto old_size = style.GrabMinSize; - style.GrabMinSize = 30.0f; - Client::GetInstance().render(); - style.GrabMinSize = old_size; - ImGui::EndDisabled(); - }); - } - - void EventViewerMod::on_unreal_init() - { - Unreal::Hook::RegisterEngineTickPreCallback( - [this](auto&, Unreal::UEngine*, float, bool) { - m_unreal_loaded.test_and_set(); - }, - {true, true, STR("EventViewerMod"), STR("InstallHook")}); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/FilterCountRenderer.cpp b/cppmods/EventViewerMod/src/FilterCountRenderer.cpp deleted file mode 100644 index 5c280cb8..00000000 --- a/cppmods/EventViewerMod/src/FilterCountRenderer.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include - -namespace RC::EventViewerMod -{ - auto FilterCountRenderer::add() -> void - { - ++m_count; - } - - auto FilterCountRenderer::render_and_reset(const bool show_tooltip) -> void - { - if (!m_count) return; - static ImVec4 text_color{1.0f, 1.0f, 0.0f, 1.0f}; - ImGui::TextColored(text_color, "<%llu Calls Filtered>", m_count); - if (show_tooltip) - ImGui::SetItemTooltip("The call above and the call below may not be related. Right click an entry -> Show Call Stack to see related calls."); - m_count = 0; - } - - auto FilterCountRenderer::render(const size_t count, const bool show_tooltip) -> void - { - if (!count) return; - static ImVec4 text_color{1.0f, 1.0f, 0.0f, 1.0f}; - ImGui::TextColored(text_color, "<%llu Calls Filtered>", count); - if (show_tooltip) - ImGui::SetItemTooltip("The call above and the call below may not be related. Right click an entry -> Show Call Stack to see related calls."); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/Middleware.cpp b/cppmods/EventViewerMod/src/Middleware.cpp deleted file mode 100644 index 670944d6..00000000 --- a/cppmods/EventViewerMod/src/Middleware.cpp +++ /dev/null @@ -1,279 +0,0 @@ -#include - -#include -#include - -#include -#include - -#include - -// note if using fname index as a hash, games that implement name recycling can be problematic/inaccurate for context objects, and right clicking entries -// FIXME trailing spaces on copy? -// EventViewerMod: hook interception + enqueue backend. -// -// Middleware installs the UE hooks and turns each callback into a lightweight CallStackEntry -// that can be consumed by the UI thread later. -// -// Key design points: -// - All supported hooks are installed and intercepted concurrently. -// - Depth is computed with a single counter so nested PE/PI/PLSF chains share indentation. -// - Enqueue is guarded by m_allow_queue to avoid capturing half-installed state. -// - The queue is moodycamel::ConcurrentQueue with per-thread ProducerTokens. -// - -namespace RC::EventViewerMod -{ - using RC::Unreal::FFrame; - using RC::Unreal::UFunction; - using RC::Unreal::UObject; - - using RC::Unreal::Hook::ERROR_ID; - using RC::Unreal::Hook::GlobalCallbackId; - using RC::Unreal::Hook::RegisterProcessEventPostCallback; - using RC::Unreal::Hook::RegisterProcessEventPreCallback; - using RC::Unreal::Hook::RegisterProcessInternalPostCallback; - using RC::Unreal::Hook::RegisterProcessInternalPreCallback; - using RC::Unreal::Hook::UnregisterCallback; - - using moodycamel::ConsumerToken; - using moodycamel::ProducerToken; - - auto Middleware::GetInstance() -> Middleware& - { - static Middleware s_instance; - return s_instance; - } - - Middleware::Middleware() - { - Unreal::UObjectGlobals::ForEachUObject([this](UObject* object, ...) -> LoopAction { - if (object && Unreal::Cast(object) && object->GetName().contains(STR("Tick"))) - { - m_tick_fns.insert(object); - } - return LoopAction::Continue; - }); - - Output::send(L"[EventViewerMod] Found {} engine tick functions!", m_tick_fns.size()); - - m_pe_controller = {.register_prehook_fn = &RC::Unreal::Hook::RegisterProcessEventPreCallback, - .register_posthook_fn = &RC::Unreal::Hook::RegisterProcessEventPostCallback, - .m_pre_callback = - [this](auto&, UObject* context, UFunction* function, void*) { - return enqueue(EMiddlewareHookTarget::ProcessEvent, context, function); - }, - .m_post_callback = - [](auto&, UObject*, UFunction*, void*) { - m_depth = (m_depth == 0) ? 0 : (m_depth - 1); - }}; - - m_pi_controller = {.register_prehook_fn = &RC::Unreal::Hook::RegisterProcessInternalPreCallback, - .register_posthook_fn = &RC::Unreal::Hook::RegisterProcessInternalPostCallback, - .m_pre_callback = - [this](auto&, UObject* context, FFrame& stack, void*) { - auto fn = stack.Node(); - if (!fn) fn = stack.CurrentNativeFunction(); - return enqueue(EMiddlewareHookTarget::ProcessInternal, context, fn); - }, - .m_post_callback = - [](auto&, UObject*, FFrame&, void*) { - m_depth = (m_depth == 0) ? 0 : (m_depth - 1); - }}; - - m_plsf_controller = {.register_prehook_fn = &RC::Unreal::Hook::RegisterProcessLocalScriptFunctionPreCallback, - .register_posthook_fn = &RC::Unreal::Hook::RegisterProcessLocalScriptFunctionPostCallback, - .m_pre_callback = - [this](auto&, UObject* context, FFrame& stack, void*) { - auto fn = stack.Node(); - if (!fn) fn = stack.CurrentNativeFunction(); - return enqueue(EMiddlewareHookTarget::ProcessLocalScriptFunction, context, fn); - }, - .m_post_callback = - [](auto&, UObject*, FFrame&, void*) { - m_depth = (m_depth == 0) ? 0 : (m_depth - 1); - }}; - QueueProfiler::Reset(); - } - - Middleware::~Middleware() - { - stop_impl(false); - } - - auto Middleware::assert_on_imgui_thread() const -> void - { - if (std::this_thread::get_id() != m_imgui_id) - { - throw std::runtime_error("EventViewerMod middleware: must be called from ImGui thread"); - } - } - - auto Middleware::is_tick_fn(const UFunction* fn) const -> bool - { - return fn && m_tick_fns.contains(const_cast(fn)); - } - - auto Middleware::set_imgui_thread_id(std::thread::id id) -> void - { - m_imgui_id = id; - } - - auto Middleware::get_imgui_thread_id() const -> std::thread::id - { - return m_imgui_id; - } - - auto Middleware::get_average_enqueue_time() const -> double - { - return QueueProfiler::GetEnqueueAverage(); - } - - auto Middleware::get_average_dequeue_time() const -> double - { - return QueueProfiler::GetDequeueAverage(); - } - - auto Middleware::stop_impl(const bool do_assert) -> bool - { - if (do_assert) - { - assert_on_imgui_thread(); - } - if (m_paused) - { - return true; - } - - m_pe_controller.unhook(); - m_pi_controller.unhook(); - m_plsf_controller.unhook(); - - // Causes all thread_local depths to be reset the next time the prehook runs. - m_depth_reset_counter.fetch_add(1, std::memory_order_release); - m_allow_queue.clear(std::memory_order_release); - m_paused = true; - return true; - } - - auto Middleware::stop() -> bool - { - if (!stop_impl(true)) - { - return false; - } - - // Drain remaining items (discard). - if (m_buffer.empty()) - { - m_buffer.resize(256); - } - - for (;;) - { - const auto count = m_queue.try_dequeue_bulk(m_imgui_consumer_token, m_buffer.data(), m_buffer.size()); - if (count == 0) - { - break; - } - } - - return true; - } - - auto Middleware::is_paused() const -> bool - { - assert_on_imgui_thread(); - return m_paused; - } - - auto Middleware::start() -> bool - { - assert_on_imgui_thread(); - if (!m_paused) - { - return true; - } - - if (!Unreal::FName::ToStringInternal.is_ready()) - { - Output::send(L"[EventViewerMod] Mod requires FName.toString to be known!"); - } - - if (!(m_plsf_controller.install_posthook() && m_pi_controller.install_posthook() && m_pe_controller.install_posthook() && - m_plsf_controller.install_prehook() && m_pi_controller.install_prehook() && m_pe_controller.install_prehook())) - { - m_pe_controller.unhook(); - m_pi_controller.unhook(); - m_plsf_controller.unhook(); - return false; - } - - m_paused = false; - m_allow_queue.test_and_set(std::memory_order_acq_rel); - return true; - } - - auto Middleware::enqueue(const EMiddlewareHookTarget hook_target, UObject* context, UFunction* function) -> void - { - thread_local std::thread::id thread_id = std::this_thread::get_id(); - thread_local uint64_t local_reset_counter = m_depth_reset_counter.load(std::memory_order_acquire); - if (!m_allow_queue.test(std::memory_order_acquire)) return; - const auto current_counter = m_depth_reset_counter.load(std::memory_order_acquire); - if (current_counter != local_reset_counter) - { - m_depth = 0; - local_reset_counter = current_counter; - } - - const auto is_tick = is_tick_fn(function); - - // Middleware is a singleton and lives for the mod lifetime, so a simple thread_local token is safe here. - thread_local ProducerToken tls_token{m_queue}; - - auto strings = StringPool::GetInstance().get_strings(context, function); - - QueueProfiler::BeginEnqueue(); - m_queue.enqueue(tls_token, CallStackEntry{hook_target, strings, m_depth++, thread_id, is_tick}); - QueueProfiler::EndEnqueue(); - } - - auto Middleware::dequeue(const uint16_t max_ms, const uint16_t max_count_per_iteration, const std::function& on_dequeue) -> void - { - assert_on_imgui_thread(); - - const auto start_time = std::chrono::steady_clock::now(); - if (m_buffer.size() < max_count_per_iteration) - { - m_buffer.resize(max_count_per_iteration); - } - auto now = std::chrono::steady_clock::now(); - for (; std::chrono::duration_cast(now - start_time).count() < max_ms; now = std::chrono::steady_clock::now()) - { - QueueProfiler::BeginDequeue(); - const auto amount = m_queue.try_dequeue_bulk(m_imgui_consumer_token, m_buffer.data(), max_count_per_iteration); - QueueProfiler::EndDequeue(); - - if (amount == 0) - { - break; - } - - for (size_t i = 0; i < amount; ++i) - { - on_dequeue(std::move(m_buffer[i])); - } - - if (m_queue.size_approx() == 0) - { - break; - } - } - - QueueProfiler::AddPendingCount(m_queue.size_approx()); - if (std::chrono::duration_cast(now - start_time).count() >= max_ms) - { - QueueProfiler::AddTimeExceededCount(); - } - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/StringPool.cpp b/cppmods/EventViewerMod/src/StringPool.cpp deleted file mode 100644 index aa0dfa4f..00000000 --- a/cppmods/EventViewerMod/src/StringPool.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include - -#include - -#include - -// EventViewerMod: string interning and cached lowercase views. -// -// Hooks run on game threads and must be cheap. Instead of allocating/formatting strings for -// every callback, we intern names and cache their lowercase forms once. -// -// The returned std::string_views remain valid until StringPool::clear() is called. - -auto RC::EventViewerMod::StringPool::get_strings(RC::Unreal::UObject* caller, RC::Unreal::UFunction* function) -> AllNameStringViews -{ - if (!caller || !function) return AllNameStringViews{}; - // StringPool combines the caller's ComparisonIndex and the function's ComparisonIndex - // to generate a unique hash for the full name string. - const uint32_t function_hash = function->GetNamePrivate().GetComparisonIndex(); - uint64_t hash = function_hash; - hash = hash << 32; - hash |= caller->GetNamePrivate().GetComparisonIndex(); - - { - std::shared_lock lock(m_mutex); - auto string_info_it = m_main_pool.find(hash); - if (string_info_it != m_main_pool.end()) - { - auto& string_info = string_info_it->second; - std::string_view full_name = string_info.full_name; - std::string_view lower_full = string_info.lower_cased_full_name; - - std::string_view func_name = full_name; - func_name.remove_prefix(string_info.function_begin); - - std::string_view lower_func_name = lower_full; - lower_func_name.remove_prefix(string_info.function_begin); - - return AllNameStringViews{FunctionNameStringViews{function_hash, func_name, lower_func_name}, full_name, lower_full, hash}; - } - } - - const auto caller_str = RC::to_string(caller->GetName()); - const auto func_str = RC::to_string(function->GetName()); - - // Build once, store both original-case and lower-cased versions. - auto full = caller_str + "." + func_str; - auto lower_full = to_lower_case(full); - auto path_str = RC::to_string(function->GetPathName()); - - { - std::unique_lock lock(m_mutex); - auto& string_info = m_main_pool.emplace(hash, StringInfo{caller_str.size() + 1, std::move(full), std::move(lower_full)}).first->second; - m_path_pool.emplace(function_hash, std::move(path_str)); - - std::string_view full_name = string_info.full_name; - std::string_view lower_full_name = string_info.lower_cased_full_name; - - std::string_view func_name = full_name; - func_name.remove_prefix(string_info.function_begin); - - std::string_view lower_func_name = lower_full_name; - lower_func_name.remove_prefix(string_info.function_begin); - - return AllNameStringViews{FunctionNameStringViews{function_hash, func_name, lower_func_name}, full_name, lower_full_name, hash}; - } -} - -auto RC::EventViewerMod::StringPool::get_path_name(const uint32_t function_hash) -> std::string_view -{ - std::shared_lock lock(m_mutex); - auto path_it = m_path_pool.find(function_hash); - if (path_it == m_path_pool.end()) return ""; - return path_it->second; -} - -auto RC::EventViewerMod::StringPool::clear() -> void -{ - std::unique_lock lock(m_mutex); - m_path_pool.clear(); -} - -auto RC::EventViewerMod::StringPool::GetInstance() -> StringPool& -{ - static StringPool string_pool; - return string_pool; -} diff --git a/cppmods/EventViewerMod/src/Structs.cpp b/cppmods/EventViewerMod/src/Structs.cpp deleted file mode 100644 index b27b98b0..00000000 --- a/cppmods/EventViewerMod/src/Structs.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include -#include -#include - -#include -#include - -#include -#include - -#include -#include - -inline constexpr static unsigned ALPHA = 200; -inline constexpr static std::array COLORS = { - IM_COL32(255, 0, 0, ALPHA), // red - IM_COL32(0, 0, 255, ALPHA), // blue - IM_COL32(0, 255, 0, ALPHA), // green - IM_COL32(255, 176, 0, ALPHA), // orange - IM_COL32(176, 255, 0, ALPHA), // lime - IM_COL32(0, 255, 255, ALPHA), // cyan - IM_COL32(255, 255, 0, ALPHA), // yellow -}; -inline constexpr static auto SELECTED_COLOR = ImVec4{1.0f, 1.0f, 0.0f, 1.0f}; - -// EventViewerMod: small UI-facing helpers for entries and context menus. -// -// The heavy lifting (hooks, queueing, string pooling) happens elsewhere. This file is mostly -// convenience methods used by the ImGui layer: context menu attachment management, clipboard -// helpers, and per-entry "support menu" behavior. -// -namespace RC::EventViewerMod -{ - auto copy_to_clipboard(const std::string_view& string) -> void - { - ImGui::LogToClipboard(); - ImVec2 dummy{0, 0}; - ImGui::LogRenderedText(&dummy, string.data(), string.data() + string.size()); - ImGui::LogFinish(); - }; - - EntryBase::EntryBase(const bool is_tick) : is_tick(is_tick) - { - } - - CallStackEntry::CallStackEntry( - const EMiddlewareHookTarget hook_target, const AllNameStringViews& strings, const uint32_t depth, const std::thread::id thread_id, const bool is_tick) - : EntryBase(is_tick), hook_target(hook_target), depth(depth), thread_id(thread_id) - { - // Inheritance is used to avoid extra indirection/caches misses. - // (StringPool owns the backing storage for these views.) - function_hash = strings.function_hash; - function_name = strings.function_name; - lower_cased_function_name = strings.lower_cased_function_name; - full_name = strings.full_name; - lower_cased_full_name = strings.lower_cased_full_name; - full_hash = strings.full_hash; - } - - auto CallStackEntry::render(const int indent_delta, const ECallStackEntryRenderFlags_ flags) const -> void - { - render_indents(indent_delta); - if (flags & ECallStackEntryRenderFlags_IndentColors) - { - const auto indent_width = ImGui::GetStyle().IndentSpacing * static_cast(depth); - auto min = ImGui::GetCursorScreenPos(); - auto max = min; - min.x -= indent_width; - max.y += ImGui::GetTextLineHeight(); - ImGui::GetWindowDrawList()->AddRectFilled(min, max, COLORS[depth % COLORS.size()]); - } - if (flags & ECallStackEntryRenderFlags_Highlight) [[unlikely]] - { - ImGui::TextColored(SELECTED_COLOR, to_prefix_string(hook_target)); - ImGui::SameLine(); - ImGui::TextColored(SELECTED_COLOR, full_name.data()); - } - else [[likely]] - { - ImGui::TextUnformatted(to_prefix_string(hook_target)); - ImGui::SameLine(); - ImGui::TextUnformatted(full_name.data()); - } - - if (flags & ECallStackEntryRenderFlags_WithSupportMenus) - { - render_support_menus(flags); - } - } - - auto CallStackEntry::to_string_with_prefix() const -> std::wstring - { - std::wstring out; - for (uint32_t i = 0; i < depth; ++i) - out += L"\t"; - out += ensure_str(to_prefix_string(hook_target)) + ensure_str(full_name) + L"\n"; - return out; - } - - auto CallStackEntry::render_indents(const int indent_delta) const -> void - { - if (indent_delta > 0) - { - for (int i = 0; i < indent_delta; ++i) - { - ImGui::Indent(); - } - } - else if (indent_delta < 0) - { - for (int i = indent_delta; i != 0; ++i) - { - ImGui::Unindent(); - } - } - } - - auto CallStackEntry::render_support_menus(const ECallStackEntryRenderFlags_ flags) const -> void - { - ImGui::SetItemTooltip("Right click for options"); - if (ImGui::BeginPopupContextItem("EntryPopup##ep", ImGuiPopupFlags_MouseButtonRight)) - { - if (flags & ECallStackEntryRenderFlags_WithSupportMenusCallStackModal & ~ECallStackEntryRenderFlags_WithSupportMenus) - { - if (ImGui::MenuItem("Show Call Stack")) Client::GetInstance().render_entry_stack_modal(this); // need to do this outside - ImGui::Separator(); - } - if (ImGui::MenuItem("Copy Function Full Name")) copy_to_clipboard(StringPool::GetInstance().get_path_name(function_hash)); - if (ImGui::MenuItem("Copy Function Name##cfn")) copy_to_clipboard(function_name); - if (ImGui::MenuItem("Add Function to Whitelist##fwl")) Client::GetInstance().add_to_white_list(function_name); - if (ImGui::MenuItem("Add Function to Blacklist##fbl")) Client::GetInstance().add_to_black_list(function_name); - ImGui::Separator(); - if (ImGui::MenuItem("Copy Caller Name##ccn")) copy_to_clipboard({full_name.begin(), function_name.begin() - 1}); - if (ImGui::MenuItem("Add Caller to Whitelist##cwl")) Client::GetInstance().add_to_white_list({full_name.begin(), function_name.begin() - 1}); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - if (ImGui::MenuItem("Add Caller to Blacklist##cbl")) Client::GetInstance().add_to_black_list({full_name.begin(), function_name.begin() - 1}); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - ImGui::Separator(); - if (ImGui::MenuItem("Copy Caller + Function Name##cfln")) copy_to_clipboard(full_name); - if (ImGui::MenuItem("Add Caller + Function to Whitelist##fnwl")) Client::GetInstance().add_to_white_list(full_name); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - if (ImGui::MenuItem("Add Caller + Function to Blacklist##fnb")) Client::GetInstance().add_to_black_list(full_name); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - ImGui::EndPopup(); - } - } - - CallFrequencyEntry::CallFrequencyEntry(const FunctionNameStringViews& strings, const bool is_tick) : EntryBase(is_tick) - { - function_hash = strings.function_hash; - function_name = strings.function_name; - lower_cased_function_name = strings.lower_cased_function_name; - } - - auto CallFrequencyEntry::render(const ECallFrequencyEntryRenderFlags_ flags) const -> void - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(function_name.data()); - if (flags & ECallFrequencyEntryRenderFlags_WithSupportMenus) render_support_menus(); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%llu", static_cast(frequency)); - } - - auto CallFrequencyEntry::render_support_menus() const -> void - { - ImGui::SetItemTooltip("Right click for options"); - if (ImGui::BeginPopupContextItem("EntryPopup##fep", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem("Copy Function Full Name")) copy_to_clipboard(StringPool::GetInstance().get_path_name(function_hash)); - if (ImGui::MenuItem("Copy Function Name##cfn")) copy_to_clipboard(function_name); - if (ImGui::MenuItem("Add Function to Whitelist##fwl")) Client::GetInstance().add_to_white_list(function_name); - if (ImGui::MenuItem("Add Function to Blacklist##fbl")) Client::GetInstance().add_to_black_list(function_name); - ImGui::EndPopup(); - } - } - - ThreadInfo::ThreadInfo(const std::thread::id thread_id) : thread_id(thread_id), is_game_thread(RC::Unreal::GetGameThreadId() == thread_id) - { - } - - auto ThreadInfo::id_string() -> const char* - { - if (m_id_string.empty()) - { - std::stringstream ss; - ss << thread_id; - m_id_string = ss.str(); - if (is_game_thread) - { - m_id_string += " (Game)"; - } - } - return m_id_string.c_str(); - } - - auto ThreadInfo::clear() -> void - { - call_frequencies.clear(); - call_stack.clear(); - call_stack_render_set.clear(); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/dllmain.cpp b/cppmods/EventViewerMod/src/dllmain.cpp deleted file mode 100644 index c86a94c0..00000000 --- a/cppmods/EventViewerMod/src/dllmain.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// EventViewerMod: Windows DLL entry point (UE4SS loads this module). -// -// The actual mod logic lives in EventViewerMod (see EventViewer.cpp). This file exists to -// satisfy the DLL entry requirements on Windows. - -#include - -extern "C" -{ - __declspec(dllexport) RC::CppUserModBase* start_mod() - { - return new EventViewerMod::EventViewerMod(); - } - - __declspec(dllexport) void uninstall_mod(RC::CppUserModBase* mod) - { - delete mod; - } -} \ No newline at end of file diff --git a/cppmods/KismetDebuggerMod/CMakeLists.txt b/cppmods/KismetDebuggerMod/CMakeLists.txt deleted file mode 100644 index c474290e..00000000 --- a/cppmods/KismetDebuggerMod/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -project(KismetDebuggerMod) - -set(TARGET KismetDebuggerMod) -project(${TARGET}) - -set(${TARGET}_Sources - "${CMAKE_CURRENT_SOURCE_DIR}/src/dllmain.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/KismetDebugger.cpp" - ) - -add_library(${TARGET} SHARED ${${TARGET}_Sources}) -target_include_directories(${TARGET} PRIVATE "include") -target_link_libraries(${TARGET} PRIVATE ImGui) -target_link_libraries(${TARGET} PUBLIC UE4SS) diff --git a/cppmods/KismetDebuggerMod/include/KismetDebugger.hpp b/cppmods/KismetDebuggerMod/include/KismetDebugger.hpp deleted file mode 100644 index 68a99983..00000000 --- a/cppmods/KismetDebuggerMod/include/KismetDebugger.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -namespace RC::GUI::KismetDebuggerMod -{ - using namespace RC::Unreal; - - auto expr_to_string(EExprToken expr) -> const char*; - - struct PausedContext - { - EExprToken expr{}; - UObject* context{}; - FFrame* stack{}; - }; - - class BreakpointStore - { - public: - BreakpointStore(); - ~BreakpointStore(); - - auto load(std::filesystem::path& path) -> void; - auto save() -> void; - - auto has_breakpoint(UFunction* fn, size_t index) -> bool; - auto add_breakpoint(UFunction* fn, size_t index) -> void; - auto add_breakpoint(const StringType& fn, size_t index) -> void; - auto remove_breakpoint(UFunction* fn, size_t index) -> void; - - private: - typedef std::unordered_set FunctionBreakpoints; - - std::unordered_map > m_breakpoints_by_function{}; - std::unordered_map > m_breakpoints_by_name{}; - }; - - class Debugger - { - public: - Debugger(); - ~Debugger(); - - auto enable() -> void; - auto disable() -> void; - - auto render() -> void; - auto render_nav_bar(float width) -> void; - - auto nav_to_function(UFunction* fn) -> void; - auto nav_to_function(std::string full_name) -> void; - - private: - bool m_paused{}; - - // std::string because it's used primarily with ImGui - std::unordered_map m_function_name_map{}; - std::vector m_nav_history{}; - int m_nav_history_index{-1}; - std::string m_nav_function{""}; - - // layout - float m_split_right{400.0}; - float m_split_left{400.0}; - - uint8_t* m_last_code{nullptr}; // pointer to last stack instruction, used to know if it's advanced since last frame - BreakpointStore& m_breakpoints; - - public: - static inline std::filesystem::path m_save_path; - }; - - class ScriptRenderContext - { - public: - ScriptRenderContext(std::optional& paused_context, UFunction* fn, bool scroll_to_active, BreakpointStore& breakpoints); - ~ScriptRenderContext(); - - auto render() -> void; - - private: - auto render_property(); - auto render_expr() -> EExprToken; - - template - auto read() -> T - { - T t; - memcpy(&t, &m_script[m_index], sizeof(T)); - m_index += sizeof(T); - return t; - } - - auto read_object() -> UObject* - { - return (UObject*)this->read(); - } - auto read_name() -> FName - { - FName n = read(); - read(); // not sure what this is for - return n; - } - - - private: - std::optional& m_paused_context; - UFunction* m_fn{}; - - bool m_scroll_to_active{}; - BreakpointStore& m_breakpoints; - - int m_indent{0}; - - uint8_t* m_script{}; - int m_script_size{}; - int m_index{}; - int m_cur{}; // or -1 - EExprToken m_current_expr{}; - }; -} diff --git a/cppmods/KismetDebuggerMod/src/KismetDebugger.cpp b/cppmods/KismetDebuggerMod/src/KismetDebugger.cpp deleted file mode 100644 index 67bdc500..00000000 --- a/cppmods/KismetDebuggerMod/src/KismetDebugger.cpp +++ /dev/null @@ -1,1437 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define IMGUI_DEFINE_MATH_OPERATORS -#include -#include -#include -#include - -#include "Profiler/Profiler.hpp" - -#include - -namespace RC::GUI::KismetDebuggerMod -{ - - using namespace RC::Unreal; - - FNativeFuncPtr GNativesOriginal[EExprToken::EX_Max]; - volatile bool is_hooked = false; // cannot hook *immediately* as GNatives is populated at runtime - - volatile bool should_pause = false; - volatile bool should_next = false; - std::optional context; - std::mutex context_mutex; - - BreakpointStore g_breakpoints; - - void hook_expr_internal(UObject* Context, FFrame& Stack, void* RESULT_DECL, EExprToken N) { - UFunction* fn = Stack.Node(); - StringType name = Stack.Node()->GetFullName(); - ProfilerTransientScopeNamed(scope, to_string(name).c_str(), true); - - size_t index = Stack.Code() - fn->GetScript().GetData() - 1; - if (should_pause || g_breakpoints.has_breakpoint(fn, index)) - { - should_pause = true; - std::unique_lock lock_a(context_mutex); - PausedContext ctx{ - .expr = N, - .context = Context, - .stack = &Stack, - }; - context = std::optional(ctx); - lock_a.unlock(); - while (should_pause && !should_next) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - std::unique_lock lock_b(context_mutex); - context = std::nullopt; - lock_b.unlock(); - should_next = false; - } - - GNativesOriginal[N](Context, Stack, RESULT_DECL); - } - - template - void hook_expr(UObject* Context, FFrame& Stack, void* RESULT_DECL) { - hook_expr_internal(Context, Stack, RESULT_DECL, static_cast(N)); - } - - template void hook_all() { - GNativesOriginal[N - 1] = GNatives_Internal[N - 1]; - GNatives_Internal[N - 1] = &hook_expr; - hook_all(); - } - template <> void hook_all<0>() {} - - typedef std::unordered_map> JsonBreakpoints; - - BreakpointStore::BreakpointStore() - { - } - BreakpointStore::~BreakpointStore() - { - } - auto BreakpointStore::load(std::filesystem::path& path) -> void - { - JsonBreakpoints breakpoints{}; - auto ec = glz::read_file_json(breakpoints, path.string(), std::string{}); - - for (const auto& [fn, bps] : breakpoints) - { - auto wfn = ensure_str(fn); - for (const auto& bp : bps) - { - add_breakpoint(wfn, bp); - } - } - } - auto BreakpointStore::save() -> void - { - JsonBreakpoints breakpoints{}; - for (const auto& [fn, bps] : m_breakpoints_by_name) { - if (bps) breakpoints[to_string(fn)] = *bps; - } - auto ec = glz::write_file_json(breakpoints, Debugger::m_save_path.string(), std::string{}); - - } - - auto BreakpointStore::has_breakpoint(UFunction* fn, size_t index) -> bool - { - auto [it_fn, inserted] = m_breakpoints_by_function.emplace(fn, nullptr); - if (!inserted) - { - if (it_fn->second) - return it_fn->second->contains(index); - } - else - { - // insert null so we know we've already inserted the fn ptr into the name map - auto [it_name, inserted] = m_breakpoints_by_name.emplace(fn->GetFullName(), nullptr); - if (!inserted) - { - if (it_name->second) - { - it_fn->second = it_name->second; - return it_name->second->contains(index); - } - } - } - return false; - } - auto BreakpointStore::add_breakpoint(UFunction* fn, size_t index) -> void - { - std::shared_ptr bps; - auto [it_fn, inserted_fn] = m_breakpoints_by_function.emplace(fn, nullptr); - auto [it_name, inserted_name] = m_breakpoints_by_name.emplace(fn->GetFullName(), nullptr); - if (!inserted_fn && it_fn->second) bps = it_fn->second; - if (!inserted_name && it_name->second) bps = it_name->second; - - if (!bps) - bps = it_fn->second = it_name->second = std::make_shared(); - - bps->emplace(index); - - save(); - - /* - std::cout << "all breakpoints by fn ptr:" << std::endl; - for (const auto& fns : m_breakpoints_by_function) { - std::cout << (void*) fns.first << " = "; - if (fns.second) - for (const auto& bps : *fns.second) { - std::cout << bps << " "; - } - else - std::cout << "null"; - std::cout << std::endl; - } - - std::cout << "all breakpoints by fn name:" << std::endl; - for (const auto& fns : m_breakpoints_by_name) { - std::wcout << fns.first << " = "; - if (fns.second) - for (const auto& bps : *fns.second) { - std::cout << bps << " "; - } - else - std::cout << "null"; - std::cout << std::endl; - } - - std::cout << "all non-null breakpoints by fn name:" << std::endl; - for (const auto& fns : m_breakpoints_by_name) { - if (fns.second) - { - std::wcout << fns.first << " = "; - for (const auto& bps : *fns.second) { - std::cout << bps << " "; - } - std::cout << std::endl; - } - } - */ - } - auto BreakpointStore::add_breakpoint(const StringType& fn, size_t index) -> void - { - std::shared_ptr bps; - auto [it_name, inserted_name] = m_breakpoints_by_name.emplace(fn, nullptr); - if (!inserted_name && it_name->second) bps = it_name->second; - - if (!bps) - bps = it_name->second = std::make_shared(); - - bps->emplace(index); - - save(); - } - auto BreakpointStore::remove_breakpoint(UFunction* fn, size_t index) -> void - { - std::shared_ptr bps; - auto [it_fn, inserted_fn] = m_breakpoints_by_function.emplace(fn, nullptr); - auto [it_name, inserted_name] = m_breakpoints_by_name.emplace(fn->GetFullName(), nullptr); - if (!inserted_fn && it_fn->second) bps = it_fn->second; - if (!inserted_name && it_fn->second) bps = it_name->second; - - if (bps) - bps->erase(index); - - save(); - } - - Debugger::Debugger() : m_breakpoints(g_breakpoints) - { - m_save_path = StringType{UE4SSProgram::get_program().get_working_directory()} + fmt::format(STR("\\Mods\\KismetDebugger\\config\\breakpoints.json")); - } - Debugger::~Debugger() - { - if (is_hooked) - { - for (int i = 0; i < EExprToken::EX_Max; i++) - { - GNatives_Internal[i] = GNativesOriginal[i]; - } - is_hooked = false; - should_pause = false; - should_next = false; - - // Give main thread some time to exit hooked function. This can - // possibly be called to unload the DLL, in which case bad things - // will happen if the main thread is still inside the hook when it - // disappears. - // TODO: Need to find a way to guarantee the main thread has exited - // the hook before continuing. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - } - - auto ImGui_Splitter(bool split_vertically, float thickness, float* size1, float* size2, float min_size1, float min_size2, float splitter_long_axis_size, const char* id_str = "##Splitter") -> bool - { - using namespace ImGui; - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImGuiID id = window->GetID(id_str); - ImRect bb; - bb.Min = window->DC.CursorPos + (split_vertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); - bb.Max = bb.Min + CalcItemSize(split_vertically ? ImVec2(thickness, splitter_long_axis_size) : ImVec2(splitter_long_axis_size, thickness), 0.0f, 0.0f); - return SplitterBehavior(bb, id, split_vertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, min_size1, min_size2, 0.0f); - } - - auto Debugger::enable() -> void - { - // do a bunch of setup on enable rather than mod init because a lot of things aren't ready at mod init - - // hack to delay breakpoint loading because working directory changes at some point during load - try - { - m_breakpoints.load(m_save_path); - } - catch (std::exception& e) - { - Output::send(STR("[KismetDebugger]: Failed to load breakpoints: {}\n"), ensure_str(e.what())); - } - - if (GNatives_Internal != nullptr) - { - // finally actually enable the debugger - hook_all(); - is_hooked = true; - return; - } - Output::send(STR("[KismetDebugger]: GNatives not found.\n")); - } - auto Debugger::disable() -> void - { - for (int i = 0; i < EExprToken::EX_Max; i++) - { - GNatives_Internal[i] = GNativesOriginal[i]; - } - is_hooked = false; - should_pause = false; - should_next = false; - } - - auto get_object_address(FProperty* property, EExprToken expr, auto in_context) -> void* - { - void* container{}; - if constexpr (std::is_same_v, std::optional>) - { - if (!in_context.has_value()) { return nullptr; }; - const auto& context = in_context.value(); - if (expr == EX_InstanceVariable) - { - container = context.context; - } - else if (expr == EX_LocalVariable) - { - container = context.stack->Locals(); - } - else - { - return nullptr; - } - } - else - { - container = in_context; - } - return *property->ContainerPtrToValuePtr(container); - } - - static auto try_rendering_property_context_menu(UFunction* fn, int index, FProperty* property, EExprToken expr, auto context) -> void - { - if (property->IsA()) - { - if (auto data_ptr = get_object_address(property, expr, context); data_ptr) - { - auto popup_context_name = to_string(fmt::format(STR("{}-{}-{}"), static_cast(fn), index, static_cast(property))); - if (ImGui::BeginPopupContextItem(popup_context_name.c_str())) - { - if (ImGui::MenuItem("Copy address")) - { - ImGui::SetClipboardText(fmt::format("{}", data_ptr).c_str()); - } - ImGui::EndPopup(); - } - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - ImGui::Text("0x%p", data_ptr); - ImGui::EndTooltip(); - } - } - } - } - - auto Debugger::render() -> void - { - std::scoped_lock lock(context_mutex); - - bool position_updated = context && m_last_code != context->stack->Code(); - if (position_updated) - nav_to_function(context->stack->Node()); - - UFunction* current_fn = nullptr; - if (m_nav_history_index >= 0) - { - auto name = m_nav_history[m_nav_history_index]; - if (auto it = m_function_name_map.find(name); it != m_function_name_map.end()) - current_fn = it->second; - } - - - float m_split_right = (ImGui::GetContentRegionMax().x - m_split_left); - - ImGui_Splitter(true, 4.0f, &m_split_left, &m_split_right, 200.0f, 200.0f, -24.0f, "KismetDebugger_split"); - - ImGui::BeginChild("KismetDebugger_Controls", {m_split_left - 2, -24.0f}, true); - - if (ImGui::Button(is_hooked ? "disable" : "enable")) - { - if (is_hooked) - { - disable(); - } - else - { - enable(); - } - } - if (is_hooked) - { - if (auto ctx = context) - { - if (ImGui::Button("continue")) - { - should_pause = false; - } - ImGui::SameLine(); - if (ImGui::Button("next")) - { - should_next = true; - } - - UFunction* node = ctx->stack->Node(); - size_t index = ctx->stack->Code() - node->GetScript().GetData() - 1; - ImGui::Text("paused @ %s", expr_to_string(ctx->expr)); - ImGui::Text("index @ %zi", index); - ImGui::Text("object context = %s", to_string(ctx->context->GetFullName()).c_str()); - - if (ImGui::CollapsingHeader("Call stack", ImGuiTreeNodeFlags_DefaultOpen)) - { - int lines = 0; - FFrame* current = ctx->stack; - while (current != nullptr) - { - current = current->PreviousFrame(); - lines++; - } - ImGui::BeginChild("KismetDebugger_CallStack", {0, ImGui::GetStyle().ScrollbarSize + lines * ImGui::GetTextLineHeightWithSpacing()}, false, ImGuiWindowFlags_HorizontalScrollbar); - current = ctx->stack; - for (int i = 0; current != nullptr; ++i) - { - ImGui::Text("%i: %s", i, to_string(current->Node()->GetFullName()).c_str()); - current = current->PreviousFrame(); - } - ImGui::EndChild(); - } - } - else - { - if (ImGui::Button("pause")) - { - should_pause = true; - } - } - } - - if (current_fn) - { - if (ImGui::CollapsingHeader("Locals", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::BeginChild("KismetDebugger_Locals", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar); - if (context && context->stack->Node() == current_fn) - { - UFunction* node = context->stack->Node(); - int property_count{}; - for (FProperty* property : TFieldRange(current_fn, EFieldIterationFlags::IncludeDeprecated)) - { - FString text{}; - auto container_ptr = property->ContainerPtrToValuePtr(context->stack->Locals()); - property->ExportTextItem(text, container_ptr, container_ptr, static_cast(node), NULL); - - ImGui::Text("%s = %S", to_string(property->GetName()).c_str(), *text); - try_rendering_property_context_menu(node, property_count, property, EX_Nothing, context->stack->Locals()); - ++property_count; - } - } - else - { - for (FProperty* property : TFieldRange(current_fn, EFieldIterationFlags::IncludeDeprecated)) - { - ImGui::Text("%s", to_string(property->GetName()).c_str()); - } - } - ImGui::EndChild(); - } - } - ImGui::EndChild(); - - // left pane - ImGui::SameLine(0, 8); - - ImGui::BeginGroup(); - - render_nav_bar(m_split_right - 20); - - ImGui::BeginChild("KismetDebugger_Disassembly", {m_split_right - 20 , -24.0f}, true); - - - if (current_fn) - { - ScriptRenderContext render_ctx{context, current_fn, position_updated, m_breakpoints}; - render_ctx.render(); - } - - ImGui::EndChild(); - - ImGui::EndGroup(); // left pane - - - if (is_hooked) - { - if (auto ctx = context) - { - m_last_code = ctx->stack->Code(); - } - } - - if (!is_hooked || !context) - m_last_code = nullptr; - } - - auto Debugger::render_nav_bar(float width) -> void - { - float start = ImGui::GetCursorPosX(); - - bool disable_back = m_nav_history_index < 0; - if (disable_back) - ImGui::BeginDisabled(); - if (ImGui::Button("back")) - { - m_nav_history_index--; - m_nav_function = m_nav_history_index < 0 ? "" : m_nav_history[m_nav_history_index]; - } - if (disable_back) - ImGui::EndDisabled(); - - ImGui::SameLine(); - bool disable_forward = m_nav_history_index + 1 >= m_nav_history.size(); - if (disable_forward) - ImGui::BeginDisabled(); - if (ImGui::Button("forward")) - { - m_nav_history_index++; - m_nav_function = m_nav_history[m_nav_history_index]; - } - if (disable_forward) - ImGui::EndDisabled(); - - - // https://github.com/ocornut/imgui/issues/718#issuecomment-1249822993 - ImGui::SameLine(); - ImGui::SetNextItemWidth(width - (ImGui::GetCursorPosX() - start)); - const bool is_input_text_enter_pressed = ImGui::InputText("##input", &m_nav_function, ImGuiInputTextFlags_EnterReturnsTrue); - const bool is_input_text_active = ImGui::IsItemActive(); - const bool is_input_text_activated = ImGui::IsItemActivated(); - - if (is_input_text_activated) - ImGui::OpenPopup("##popup"); - - { - ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y)); - //ImGui::SetNextWindowSize({ ImGui::GetItemRectSize().x, 0 }); - if (ImGui::BeginPopup("##popup", ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_ChildWindow)) - { - int n = 0; - - UObjectGlobals::ForEachUObject([&](UObject* object, ...) { - if (object->IsA()) - { - std::string full_name = to_string(object->GetFullName()); - - auto it = std::search( - full_name.begin(), full_name.end(), - m_nav_function.begin(), m_nav_function.end(), - [](auto ch1, auto ch2) { return std::toupper(ch1) == std::toupper(ch2); } - ); - - if (it != full_name.end()) - { - const char* name_c_str = full_name.c_str(); - - if (ImGui::Selectable(name_c_str)) - { - ImGui::ClearActiveID(); - m_function_name_map[full_name] = static_cast(object); - m_nav_function = full_name; - } - - if (n++ > 10) - { - ImGui::Text("..."); - return LoopAction::Break; - } - } - } - return LoopAction::Continue; - }); - - if (!n) - ImGui::Text("[nothing]"); - - if (is_input_text_enter_pressed || (!is_input_text_active && !ImGui::IsWindowFocused())) - { - ImGui::CloseCurrentPopup(); - // TODO check valid - - nav_to_function(m_nav_function); - } - - ImGui::EndPopup(); - } - } - } - - auto Debugger::nav_to_function(UFunction* fn) -> void - { - std::string full_name = to_string(fn->GetFullName()); - - m_function_name_map[full_name] = fn; - - nav_to_function(full_name); - } - - // function name must already be in name map - auto Debugger::nav_to_function(std::string full_name) -> void - { - if (m_nav_history_index >= 0 && m_nav_history[m_nav_history_index] == full_name) - return; // already at fn, do not add another entry to the stack - - m_nav_function = full_name; - - m_nav_history_index++; - if (m_nav_history_index >= m_nav_history.size()) - m_nav_history.push_back(m_nav_function); - else - m_nav_history[m_nav_history_index] = m_nav_function; - } - - - auto expr_to_string(EExprToken expr) -> const char* - { - switch (expr) - { - case EX_LocalVariable: - return "EX_LocalVariable"; - case EX_InstanceVariable: - return "EX_InstanceVariable"; - case EX_DefaultVariable: - return "EX_DefaultVariable"; - case EX_Return: - return "EX_Return"; - case EX_Jump: - return "EX_Jump"; - case EX_JumpIfNot: - return "EX_JumpIfNot"; - case EX_Assert: - return "EX_Assert"; - case EX_Nothing: - return "EX_Nothing"; - case EX_NothingInt32: - return "EX_NothingInt32"; - case EX_Let: - return "EX_Let"; - case EX_BitFieldConst: - return "EX_BitFieldConst"; - case EX_ClassContext: - return "EX_ClassContext"; - case EX_MetaCast: - return "EX_MetaCast"; - case EX_LetBool: - return "EX_LetBool"; - case EX_EndParmValue: - return "EX_EndParmValue"; - case EX_EndFunctionParms: - return "EX_EndFunctionParms"; - case EX_Self: - return "EX_Self"; - case EX_Skip: - return "EX_Skip"; - case EX_Context: - return "EX_Context"; - case EX_Context_FailSilent: - return "EX_Context_FailSilent"; - case EX_VirtualFunction: - return "EX_VirtualFunction"; - case EX_FinalFunction: - return "EX_FinalFunction"; - case EX_IntConst: - return "EX_IntConst"; - case EX_FloatConst: - return "EX_FloatConst"; - case EX_StringConst: - return "EX_StringConst"; - case EX_ObjectConst: - return "EX_ObjectConst"; - case EX_NameConst: - return "EX_NameConst"; - case EX_RotationConst: - return "EX_RotationConst"; - case EX_VectorConst: - return "EX_VectorConst"; - case EX_Vector3fConst: - return "EX_Vector3fConst"; - case EX_ByteConst: - return "EX_ByteConst"; - case EX_IntZero: - return "EX_IntZero"; - case EX_IntOne: - return "EX_IntOne"; - case EX_True: - return "EX_True"; - case EX_False: - return "EX_False"; - case EX_TextConst: - return "EX_TextConst"; - case EX_NoObject: - return "EX_NoObject"; - case EX_TransformConst: - return "EX_TransformConst"; - case EX_IntConstByte: - return "EX_IntConstByte"; - case EX_NoInterface: - return "EX_NoInterface"; - case EX_DynamicCast: - return "EX_DynamicCast"; - case EX_StructConst: - return "EX_StructConst"; - case EX_EndStructConst: - return "EX_EndStructConst"; - case EX_SetArray: - return "EX_SetArray"; - case EX_EndArray: - return "EX_EndArray"; - case EX_PropertyConst: - return "EX_PropertyConst"; - case EX_UnicodeStringConst: - return "EX_UnicodeStringConst"; - case EX_Int64Const: - return "EX_Int64Const"; - case EX_UInt64Const: - return "EX_UInt64Const"; - case EX_DoubleConst: - return "EX_DoubleConst"; - case EX_Cast: // EX_PrimitiveCast in 4.27 - return "EX_Cast"; - case EX_SetSet: - return "EX_SetSet"; - case EX_EndSet: - return "EX_EndSet"; - case EX_SetMap: - return "EX_SetMap"; - case EX_EndMap: - return "EX_EndMap"; - case EX_SetConst: - return "EX_SetConst"; - case EX_EndSetConst: - return "EX_EndSetConst"; - case EX_MapConst: - return "EX_MapConst"; - case EX_EndMapConst: - return "EX_EndMapConst"; - case EX_StructMemberContext: - return "EX_StructMemberContext"; - case EX_LetMulticastDelegate: - return "EX_LetMulticastDelegate"; - case EX_LetDelegate: - return "EX_LetDelegate"; - case EX_LocalVirtualFunction: - return "EX_LocalVirtualFunction"; - case EX_LocalFinalFunction: - return "EX_LocalFinalFunction"; - case EX_LocalOutVariable: - return "EX_LocalOutVariable"; - case EX_DeprecatedOp4A: - return "EX_DeprecatedOp4A"; - case EX_InstanceDelegate: - return "EX_InstanceDelegate"; - case EX_PushExecutionFlow: - return "EX_PushExecutionFlow"; - case EX_PopExecutionFlow: - return "EX_PopExecutionFlow"; - case EX_ComputedJump: - return "EX_ComputedJump"; - case EX_PopExecutionFlowIfNot: - return "EX_PopExecutionFlowIfNot"; - case EX_Breakpoint: - return "EX_Breakpoint"; - case EX_InterfaceContext: - return "EX_InterfaceContext"; - case EX_ObjToInterfaceCast: - return "EX_ObjToInterfaceCast"; - case EX_EndOfScript: - return "EX_EndOfScript"; - case EX_CrossInterfaceCast: - return "EX_CrossInterfaceCast"; - case EX_InterfaceToObjCast: - return "EX_InterfaceToObjCast"; - case EX_WireTracepoint: - return "EX_WireTracepoint"; - case EX_SkipOffsetConst: - return "EX_SkipOffsetConst"; - case EX_AddMulticastDelegate: - return "EX_AddMulticastDelegate"; - case EX_ClearMulticastDelegate: - return "EX_ClearMulticastDelegate"; - case EX_Tracepoint: - return "EX_Tracepoint"; - case EX_LetObj: - return "EX_LetObj"; - case EX_LetWeakObjPtr: - return "EX_LetWeakObjPtr"; - case EX_BindDelegate: - return "EX_BindDelegate"; - case EX_RemoveMulticastDelegate: - return "EX_RemoveMulticastDelegate"; - case EX_CallMulticastDelegate: - return "EX_CallMulticastDelegate"; - case EX_LetValueOnPersistentFrame: - return "EX_LetValueOnPersistentFrame"; - case EX_ArrayConst: - return "EX_ArrayConst"; - case EX_EndArrayConst: - return "EX_EndArrayConst"; - case EX_SoftObjectConst: - return "EX_SoftObjectConst"; - case EX_CallMath: - return "EX_CallMath"; - case EX_SwitchValue: - return "EX_SwitchValue"; - case EX_InstrumentationEvent: - return "EX_InstrumentationEvent"; - case EX_ArrayGetByRef: - return "EX_ArrayGetByRef"; - case EX_ClassSparseDataVariable: - return "EX_ClassSparseDataVariable"; - case EX_FieldPathConst: - return "EX_FieldPathConst"; - case EX_AutoRtfmTransact: - return "EX_AutoRtfmTransact"; - case EX_AutoRtfmStopTransact: - return "EX_AutoRtfmStopTransact"; - case EX_AutoRtfmAbortIfNot: - return "EX_AutoRtfmAbortIfNot"; - case EX_Max: - return "EX_Max"; - } - return "unknown"; - }; - - ScriptRenderContext::ScriptRenderContext(std::optional& paused_context, UFunction* fn, bool scroll_to_active, BreakpointStore& breakpoints) - : m_paused_context(paused_context), - m_fn(fn), - m_scroll_to_active(scroll_to_active), - m_breakpoints(breakpoints), - - m_script(fn->GetScript().GetData()), - m_script_size(fn->GetScript().Num()), - m_cur((paused_context && paused_context->stack->Node() == fn) ? paused_context->stack->Code() - paused_context->stack->Node()->GetScript().GetData() - 1 : -1) - { - } - ScriptRenderContext::~ScriptRenderContext() - { - } - - auto ScriptRenderContext::render() -> void - { - while (m_index < m_script_size) render_expr(); - } - - auto ScriptRenderContext::render_property() - { - FProperty* property = (FProperty*)read_object(); - if (property) - { - ImGui::Text("%s", to_string(property->GetName()).c_str()); - if (m_paused_context.has_value()) - { - try_rendering_property_context_menu(m_fn, m_index, property, m_current_expr, m_paused_context); - } - /* - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - - // TODO: this depends on context and isn't always a local - FString text{}; - auto container_ptr = property->ContainerPtrToValuePtr(stack->Locals()); - property->ExportTextItem(text, container_ptr, container_ptr, static_cast(stack->Node()), NULL); - - ImGui::Text("= %S", *text); - - ImGui::EndTooltip(); - } - */ - } - else - { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255,0,0,255)); - ImGui::Text("NULL property"); - ImGui::PopStyleColor(); - } - } - auto ScriptRenderContext::render_expr() -> EExprToken - { - bool active = m_index == m_cur; - size_t expr_index = m_index; - - m_current_expr = static_cast(read()); - - //std::cout << "rendering (" << std::hex << unsigned(m_current_expr) << std::dec << ") @ " << (index - 1) << " " << expr_to_string(m_current_expr) << std::endl; - - - ImGui::SetCursorPosX(ImGui::GetCursorStartPos().x); - auto label = std::format("{}", expr_index); - ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(1.0f, 0.5f)); - bool is_breakpoint = m_breakpoints.has_breakpoint(m_fn, expr_index); - if (ImGui::Selectable(label.c_str(), is_breakpoint, 0, {30, 0})) - { - if (is_breakpoint) - m_breakpoints.remove_breakpoint(m_fn, expr_index); - else - m_breakpoints.add_breakpoint(m_fn, expr_index); - } - ImGui::PopStyleVar(); - ImGui::SameLine(); - ImGui::SetCursorPosX(50 + m_indent * 20.0f); - - if (active) - { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 0, 255)); - if (m_scroll_to_active) - ImGui::SetScrollHereY(); - } - ImGui::Text("%s", expr_to_string(m_current_expr)); - if (active) - ImGui::PopStyleColor(); - - m_indent++; - // Note: we use the ImGui::Dummy() lines below to avoid a warning related to setting cursor pos without following with an imgui item - ImGui::SetCursorPosX(50 + m_indent * 20.0f); - - switch (m_current_expr) - { - case EX_Cast: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - render_expr(); - break; - } - case EX_ObjToInterfaceCast: - case EX_CrossInterfaceCast: - case EX_InterfaceToObjCast: - { - ImGui::Dummy(ImVec2(0, 0)); - (UClass*)read_object(); - render_expr(); - break; - } - case EX_Let: - { - render_property(); - } - case EX_LetObj: - case EX_LetWeakObjPtr: - case EX_LetBool: - case EX_LetDelegate: - case EX_LetMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - render_expr(); - break; - } - case EX_LetValueOnPersistentFrame: - { - render_property(); - render_expr(); - break; - } - case EX_StructMemberContext: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // struct member expr. - render_expr(); - break; - } - case EX_Jump: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_ComputedJump: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_LocalVariable: - case EX_InstanceVariable: - case EX_DefaultVariable: - case EX_LocalOutVariable: - case EX_ClassSparseDataVariable: - case EX_PropertyConst: - { - this->render_property(); - break; - } - case EX_InterfaceContext: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_PushExecutionFlow: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_NothingInt32: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_Nothing: - case EX_EndOfScript: - case EX_EndFunctionParms: - case EX_EndStructConst: - case EX_EndArray: - case EX_EndArrayConst: - case EX_EndSet: - case EX_EndMap: - case EX_EndSetConst: - case EX_EndMapConst: - case EX_IntZero: - case EX_IntOne: - case EX_True: - case EX_False: - case EX_NoObject: - case EX_NoInterface: - case EX_Self: - case EX_EndParmValue: - case EX_PopExecutionFlow: - case EX_DeprecatedOp4A: - { - ImGui::Dummy(ImVec2(0, 0)); - break; - } - case EX_WireTracepoint: - case EX_Tracepoint: - { - ImGui::Dummy(ImVec2(0, 0)); - break; - } - case EX_Breakpoint: - { - ImGui::Dummy(ImVec2(0, 0)); - break; - } - /* - case EX_InstrumentationEvent: TODO ?????? - { - if (Script[iCode] == EScriptInstrumentation::InlineEvent) - { - iCode += sizeof(FScriptName); - } - iCode += sizeof(uint8); - break; - } - */ - case EX_Return: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_CallMath: - case EX_LocalFinalFunction: - case EX_FinalFunction: - { - ImGui::Dummy(ImVec2(0, 0)); - (UFunction*)read_object(); - while (render_expr() != EX_EndFunctionParms); - break; - } - case EX_LocalVirtualFunction: - case EX_VirtualFunction: - { - //std::cout << "reading " << sizeof(FName) << " bytes @ " << index << std::endl; - - /* - for (int i = 0; i < script_size; ++i) - std::cout << script[i] << " "; - std::cout << std::endl; - */ - - /* - for (int i = 0; i < script_size; ++i) - std::cout << std::hex << std::setfill('0') << std::setw(2) << unsigned(script[i]) << " "; - std::cout << std::endl; - */ - - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - while (render_expr() != EX_EndFunctionParms); // Parms. - break; - } - case EX_CallMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - (UFunction*)read_object(); - while (render_expr() != EX_EndFunctionParms); // Parms. - break; - } - case EX_BitFieldConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); - read(); - break; - } - case EX_ClassContext: - case EX_Context: - case EX_Context_FailSilent: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Object expression. - read(); - (FField*)read_object(); // Property corresponding to the r-value data, in case the l-value needs to be mem-zero'd - render_expr(); // Context expression. - break; - } - case EX_AddMulticastDelegate: - case EX_RemoveMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Delegate property to assign to - render_expr(); // Delegate to add to the MC delegate for broadcast - break; - } - case EX_ClearMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Delegate property to clear - break; - } - case EX_IntConst: - { - ImGui::Text("%d", read()); - break; - } - case EX_Int64Const: - { - ImGui::Text("%lld", read()); - break; - } - case EX_UInt64Const: - { - ImGui::Text("%llu", read()); - break; - } - case EX_DoubleConst: - { - ImGui::Text("%f", read()); - break; - } - case EX_SkipOffsetConst: - { - ImGui::Text("%u", read()); // TODO jump - break; - } - case EX_FloatConst: - { - ImGui::Text("%f", read()); - break; - } - case EX_StringConst: - { - ImGui::Dummy(ImVec2(0, 0)); - while (read()); - break; - } - case EX_UnicodeStringConst: - { - ImGui::Dummy(ImVec2(0, 0)); - while (read()); - break; - } - case EX_TextConst: - { - ImGui::Dummy(ImVec2(0, 0)); - switch (read()) - { - case 0: // Empty - break; - case 1: // LocalizedText - render_expr(); - render_expr(); - render_expr(); - break; - case 2: // InvariantText - render_expr(); - break; - case 3: // LiteralString - render_expr(); - break; - case 4: // StringTableEntry - read_object(); - render_expr(); - render_expr(); - break; - } - break; - } - case EX_ObjectConst: - { - ImGui::Dummy(ImVec2(0, 0)); - read_object(); - break; - } - case EX_SoftObjectConst: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_FieldPathConst: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_NameConst: - { - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - break; - } - case EX_RotationConst: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - read(); - break; - } - case EX_Vector3fConst: - case EX_VectorConst: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - read(); - break; - } - case EX_TransformConst: - { - ImGui::Dummy(ImVec2(0, 0)); - // Rotation - read(); - read(); - read(); - read(); - // Translation - read(); - read(); - read(); - // Scale - read(); - read(); - read(); - break; - } - case EX_StructConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (UScriptStruct*)read_object(); // Struct. - read(); - while (render_expr() != EX_EndStructConst); - break; - } - case EX_SetArray: - { - ImGui::Dummy(ImVec2(0, 0)); - // If not loading, or its a newer version - //if((!GetLinker()) || !Ar.IsLoading() || (Ar.UE4Ver() >= VER_UE4_CHANGE_SETARRAY_BYTECODE)) - //{ - // Array property to assign to - EExprToken TargetToken = render_expr(); - //} - //else - //{ - // Array Inner Prop - //(FProperty*)read_object(); - //} - - while (render_expr() != EX_EndArray); - break; - } - case EX_SetSet: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // set property - read(); - while (render_expr() != EX_EndSet); - break; - } - case EX_SetMap: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // map property - read(); - while (render_expr() != EX_EndMap); - break; - } - case EX_ArrayConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // Inner property - read(); - while (render_expr() != EX_EndArrayConst); - break; - } - case EX_SetConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // Inner property - read(); - while (render_expr() != EX_EndSetConst); - break; - } - case EX_MapConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // Key property - (FProperty*)read_object(); // Val property - read(); - while (render_expr() != EX_EndMapConst); - break; - } - case EX_ByteConst: - case EX_IntConstByte: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_MetaCast: - { - ImGui::Dummy(ImVec2(0, 0)); - (UClass*)read_object(); - render_expr(); - break; - } - case EX_DynamicCast: - { - ImGui::Dummy(ImVec2(0, 0)); - (UClass*)read_object(); - render_expr(); - break; - } - case EX_JumpIfNot: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - render_expr(); // Boolean expr. - break; - } - case EX_PopExecutionFlowIfNot: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Boolean expr. - break; - } - case EX_Assert: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - render_expr(); // Assert expr. - break; - } - case EX_Skip: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - render_expr(); // Expression to possibly skip. - break; - } - case EX_InstanceDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - break; - } - case EX_BindDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - render_expr(); // Delegate property to assign to - render_expr(); - break; - } - case EX_SwitchValue: - { - ImGui::Dummy(ImVec2(0, 0)); - auto cases = read(); // number of cases, without default one - auto end = read(); // Code offset, go to it, when done. - render_expr(); //index term - - for (uint16 i = 0; i < cases; ++i) - { - render_expr(); // case index value term - auto next_case = read(); // offset to the next case - render_expr(); // case term - } - - render_expr(); //default term - break; - } - case EX_ArrayGetByRef: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - render_expr(); - break; - } - case EX_AutoRtfmTransact: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - while (render_expr() != EX_AutoRtfmStopTransact); - break; - } - case EX_AutoRtfmStopTransact: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - break; - } - case EX_AutoRtfmAbortIfNot: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - default: - { - // This should never occur. - //UE_LOG(LogScriptSerialization, Warning, TEXT("Error: Unknown bytecode 0x%02X; ignoring it"), (uint8)Expr ); - std::cout << "unknown expr (" << unsigned(m_current_expr) << ") " << expr_to_string(m_current_expr) << std::endl; - ImGui::Text("unknown expr (%i) %s", unsigned(m_current_expr), expr_to_string(m_current_expr)); - break; - } - } - m_indent--; - return m_current_expr; - } - -} diff --git a/cppmods/KismetDebuggerMod/src/dllmain.cpp b/cppmods/KismetDebuggerMod/src/dllmain.cpp deleted file mode 100644 index 8db1a204..00000000 --- a/cppmods/KismetDebuggerMod/src/dllmain.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include - -#include -#include -#include -#include - -class KismetDebuggerMod : public RC::CppUserModBase -{ -private: - RC::GUI::KismetDebuggerMod::Debugger m_debugger{}; - -public: - KismetDebuggerMod() : CppUserModBase() - { - ModName = STR("KismetDebugger"); - ModVersion = STR("1.0"); - ModDescription = STR("Debugging interface for kismet bytecode"); - ModAuthors = STR("truman"); - - register_tab(STR("Kismet Debugger"), [](CppUserModBase* mod) { - UE4SS_ENABLE_IMGUI() - dynamic_cast(mod)->m_debugger.render(); - }); - } - - ~KismetDebuggerMod() override = default; -}; - -#define KISMET_DEBUGGER_MOD_API __declspec(dllexport) -extern "C" -{ - KISMET_DEBUGGER_MOD_API RC::CppUserModBase* start_mod() - { - return new KismetDebuggerMod(); - } - - KISMET_DEBUGGER_MOD_API void uninstall_mod(RC::CppUserModBase* mod) - { - delete mod; - } -} - diff --git a/cppmods/KismetDebuggerMod/xmake.lua b/cppmods/KismetDebuggerMod/xmake.lua deleted file mode 100644 index 22c3289b..00000000 --- a/cppmods/KismetDebuggerMod/xmake.lua +++ /dev/null @@ -1,6 +0,0 @@ -local projectName = "KismetDebuggerMod" - -target(projectName) - add_rules("ue4ss.mod") - add_includedirs("include") - add_files("src/dllmain.cpp", "src/KismetDebugger.cpp") \ No newline at end of file diff --git a/cppmods/xmake.lua b/cppmods/xmake.lua deleted file mode 100644 index fa55abae..00000000 --- a/cppmods/xmake.lua +++ /dev/null @@ -1 +0,0 @@ -includes("KismetDebuggerMod") diff --git a/docs/feature-overview/dumpers.md b/docs/feature-overview/dumpers.md index 3f80d75c..876d5412 100644 --- a/docs/feature-overview/dumpers.md +++ b/docs/feature-overview/dumpers.md @@ -115,8 +115,9 @@ Thanks to [OutTheShade](https://github.com/OutTheShade/UnrealMappingsDumper) for Dump all loaded actors to the file `ue4ss_static_mesh_data.csv` to generate `.umaps` in-editor. -Two prerequisites are required to load the dumped actors in-editor to reconstruct the `.umap`: -- All dumped actors (static meshes, their materials/textures) must be reconstructed in the editor -- Download `zMapGenBP.zip` from the Releases page and follow the instructions in the Readme file inside of it +Reconstructing the resulting `.umap` still requires the dumped actors, meshes, +materials, and textures to be recreated in Unreal Editor. The editor-side +reconstruction assets previously bundled upstream are not distributed by this +Linux downstream. The keybind to dump mappings is by default `Ctrl` + `Numpad 7`, and can be changed in `Mods/Keybinds/Scripts/main.lua`. diff --git a/tools/buildscripts/build.ps1 b/tools/buildscripts/build.ps1 deleted file mode 100644 index 34ad7ded..00000000 --- a/tools/buildscripts/build.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -# Unified build script for RE-UE4SS on Windows -# Supports native and cross-compilation builds with various configurations - -param( - [Alias("g")] - [string]$Generator = "", # Default will be set based on what's available - [Alias("c")] - [string]$Compiler = "", - [Alias("t")] - [string]$Toolchain = "", - [Alias("b")] - [string]$BuildConfig = "Game__Shipping__Win64", - [string]$Target = "UE4SS", - [Alias("v")] - [switch]$Verbose, - [switch]$Clean, - [Alias("h")] - [switch]$Help -) - -# Colors for output -function Write-Error-Custom { Write-Host "Error: $args" -ForegroundColor Red } -function Write-Success { Write-Host $args -ForegroundColor Green } -function Write-Info { Write-Host $args -ForegroundColor Yellow } - -# Function to display usage -function Show-Usage { - @" -Usage: .\build.ps1 [OPTIONS] - -Options: - -g, --generator GENERATOR Build system generator (ninja, vs2022, vs2019) [default: auto-detect VS2022/VS2019, fallback to ninja] - -c, --compiler COMPILER Compiler to use (msvc, clang) [default: msvc when using ninja, VS default when using VS generator] - -t, --toolchain TOOLCHAIN CMake toolchain file name (without path/extension) - Example: windows-clang-cl - -b, --build-config CONFIG Build configuration [default: Game__Shipping__Win64] - --target TARGET Build target [default: UE4SS] - -v, --verbose Enable verbose output - --clean Clean build directory before building - -h, --help Display this help message - -Note: PowerShell also accepts single dash (-) prefix for all parameters - -Environment Variables: - CMAKE_PREFIX_PATH Additional CMake search paths - -Examples: - # Native build with system compiler - .\build.ps1 - - # Native build with Visual Studio 2022 - .\build.ps1 --generator vs2022 - - # Build with clang-cl - .\build.ps1 --compiler clang - - # Build specific configuration - .\build.ps1 --build-config Game__Debug__Win64 - - # Clean build with verbose output - .\build.ps1 --clean --verbose -"@ - exit 0 -} - -if ($Help) { Show-Usage } - -# Function to check if a tool is available -function Test-Tool { - param([string]$Tool) - - $command = Get-Command $Tool -ErrorAction SilentlyContinue - if (-not $command) { - Write-Error-Custom "$Tool is not installed or not in PATH" - return $false - } - return $true -} - -# Get script and project directories -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$ProjectRoot = (Get-Item "$ScriptDir\..\..").FullName - -# Check for CMake -if (-not (Test-Tool "cmake")) { exit 1 } - -# Set default generator if not specified -if (-not $Generator) { - # Check for Visual Studio installations first - $VSWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $VSWhere) { - $VSInstances = & $VSWhere -latest -property installationVersion - if ($VSInstances) { - $VSVersion = $VSInstances | Select-Object -First 1 - if ($VSVersion -like "17.*") { - $Generator = "vs2022" - Write-Info "Detected Visual Studio 2022, using as default generator" - } elseif ($VSVersion -like "16.*") { - $Generator = "vs2019" - Write-Info "Detected Visual Studio 2019, using as default generator" - } - } - } - - # Fall back to ninja if no VS found - if (-not $Generator) { - if (Get-Command "ninja" -ErrorAction SilentlyContinue) { - $Generator = "ninja" - Write-Info "Using Ninja as default generator" - # When using ninja on Windows without a compiler specified, default to MSVC - if (-not $Compiler -and -not $Toolchain) { - $Compiler = "msvc" - Write-Info "Using MSVC as default compiler with Ninja" - } - } else { - Write-Error-Custom "No suitable build generator found. Please install Visual Studio or Ninja." - exit 1 - } - } -} - -# Validate and convert generator to CMake format -$CMakeGenerator = switch ($Generator) { - "ninja" { - if (-not (Test-Tool "ninja")) { exit 1 } - "Ninja" - } - "vs2022" { "Visual Studio 17 2022" } - "vs2019" { "Visual Studio 16 2019" } - default { - Write-Error-Custom "Invalid generator: $Generator" - exit 1 - } -} - -# Warn if using VS generator with clang compiler -if ($Compiler -eq "clang" -and $Generator -match "vs") { - Write-Info "Note: Visual Studio generators work best with MSVC. For clang builds, consider using Ninja." - Write-Info "Example: .\build.ps1 -g ninja -c clang" -} - -# Set up build directory name -$BuildDirPrefix = "build" -if ($Toolchain) { - $BuildDirPrefix = "${BuildDirPrefix}_${Toolchain}" -} -if ($Compiler) { - $BuildDirPrefix = "${BuildDirPrefix}_${Compiler}" -} -$BuildDir = "${BuildDirPrefix}_${BuildConfig}" - -# Set up CMake arguments -$CMakeArgs = @( - $ProjectRoot, - "-G", $CMakeGenerator, - "-DCMAKE_BUILD_TYPE=$BuildConfig", - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" -) - -# Add architecture for Visual Studio generators -if ($Generator -like "vs*") { - $CMakeArgs += "-A", "x64" - - # For VS generators with clang, use ClangCL toolset - if ($Compiler -eq "clang") { - # Check if ClangCL toolset is available - $ClangCLAvailable = $false - $VSInstallDir = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath - if ($VSInstallDir) { - $ClangCLPath = Join-Path $VSInstallDir "VC\Tools\Llvm\x64\bin\clang-cl.exe" - if (Test-Path $ClangCLPath) { - $ClangCLAvailable = $true - } - } - - if ($ClangCLAvailable) { - $CMakeArgs += "-T", "ClangCL" - Write-Info "Using ClangCL toolset with Visual Studio generator" - } else { - Write-Info "ClangCL toolset not found in Visual Studio. Make sure you have installed the 'Clang compiler for Windows' component." - Write-Info "Attempting to use system clang-cl instead..." - # Don't set toolset, but specify compilers directly - if (Test-Tool "clang-cl") { - $CMakeArgs += "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl" - } else { - Write-Error-Custom "Neither VS ClangCL toolset nor system clang-cl found" - exit 1 - } - } - } -} - -# Set up compiler if specified -if ($Compiler) { - switch ($Compiler) { - "msvc" { - # Explicitly set MSVC compiler - if ($Generator -notlike "vs*") { - if (Test-Tool "cl") { - $CMakeArgs += "-DCMAKE_C_COMPILER=cl", "-DCMAKE_CXX_COMPILER=cl" - Write-Info "Using MSVC (cl) compiler" - } else { - # Try to find cl.exe in Visual Studio installation - $VSWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $VSWhere) { - $VSPath = & $VSWhere -latest -property installationPath - if ($VSPath) { - $CLPath = Get-ChildItem -Path "$VSPath\VC\Tools\MSVC" -Recurse -Filter "cl.exe" | - Where-Object { $_.FullName -like "*Hostx64\x64*" } | - Select-Object -First 1 - if ($CLPath) { - $CMakeArgs += "-DCMAKE_C_COMPILER=`"$($CLPath.FullName)`"", "-DCMAKE_CXX_COMPILER=`"$($CLPath.FullName)`"" - Write-Info "Using MSVC from: $($CLPath.DirectoryName)" - } else { - Write-Error-Custom "MSVC cl.exe not found in Visual Studio installation" - exit 1 - } - } - } else { - Write-Error-Custom "MSVC not found. Please ensure Visual Studio is installed." - exit 1 - } - } - } - } - "clang" { - # For non-VS generators, specify compiler directly - if ($Generator -notlike "vs*") { - if (Test-Tool "clang-cl") { - $CMakeArgs += "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl" - Write-Info "Using clang-cl for native Windows build" - } elseif (Test-Tool "clang") { - $CMakeArgs += "-DCMAKE_C_COMPILER=clang", "-DCMAKE_CXX_COMPILER=clang++" - Write-Info "Using clang/clang++ for build" - } else { - Write-Error-Custom "Clang not found" - exit 1 - } - } - } - default { - Write-Error-Custom "Invalid compiler: $Compiler" - exit 1 - } - } -} - -# Set up toolchain if specified -if ($Toolchain) { - $ToolchainFile = "$ProjectRoot\cmake\toolchains\${Toolchain}-toolchain.cmake" - if (-not (Test-Path $ToolchainFile)) { - Write-Error-Custom "Toolchain file not found: $ToolchainFile" - Write-Info "Available toolchains:" - Get-ChildItem "$ProjectRoot\cmake\toolchains\" -Filter "*.cmake" | - ForEach-Object { " " + $_.BaseName -replace '-toolchain$', '' } - exit 1 - } - $CMakeArgs += "-DCMAKE_TOOLCHAIN_FILE=$ToolchainFile" -} - -# Print build configuration -Write-Host "========================================" -Write-Info "Building RE-UE4SS" -Write-Host "========================================" -Write-Host "Generator: $Generator" -Write-Host "Compiler: $(if ($Compiler) { $Compiler } else { 'system default' })" -Write-Host "Toolchain: $(if ($Toolchain) { $Toolchain } else { 'none' })" -Write-Host "Configuration: $BuildConfig" -Write-Host "Target: $Target" -Write-Host "Build dir: $BuildDir" -Write-Host "========================================" - -# Clean build directory if requested -if ($Clean) { - Write-Info "Cleaning build directory..." - if (Test-Path $BuildDir) { - Remove-Item -Recurse -Force $BuildDir - Write-Success "Build directory cleaned successfully!" - } else { - Write-Info "Build directory does not exist, nothing to clean" - } -} - -# Create and enter build directory -New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null -Push-Location $BuildDir - -# Create .gitignore to prevent accidental commits of build artifacts -$GitIgnorePath = ".gitignore" -if (-not (Test-Path $GitIgnorePath)) { - @" -# Automatically generated .gitignore for build directory -# This prevents accidental commits of build artifacts - -# Ignore everything in this directory -* -"@ | Out-File -FilePath $GitIgnorePath -Encoding UTF8 - Write-Info "Created .gitignore in build directory" -} - -try { - # Configure with CMake - Write-Info "Configuring with CMake..." - if ($Verbose) { - Write-Host "CMake command: cmake $($CMakeArgs -join ' ')" - } - - & cmake $CMakeArgs - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "CMake configuration failed!" - exit 1 - } - - # Build - Write-Info "Building target: $Target..." - $BuildArgs = @("--build", ".", "--target", $Target) - - # For multi-configuration generators (Visual Studio), specify the configuration - if ($Generator -match "vs") { - $BuildArgs += "--config", $BuildConfig - } - - if ($Verbose) { - # The '--' argument tells cmake to pass subsequent flags to the native build tool (Ninja, MSBuild, etc.) - $BuildArgs += "--" - - # Add the generator-specific verbosity flag - $BuildArgs += switch ($Generator) { - "ninja" { "-v" } - default { "/verbosity:detailed" } - } - } - - & cmake $BuildArgs - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Build failed!" - exit 1 - } -} -finally { - # Return to original directory - Pop-Location -} - -Write-Host "" -Write-Success "========================================" -Write-Success "Build completed successfully!" -Write-Success "Configuration: $BuildConfig" -Write-Success "Output is in: $BuildDir" -if (Test-Path "$BuildDir\compile_commands.json") { - Write-Success "Compile commands: $BuildDir\compile_commands.json" -} -Write-Success "========================================" \ No newline at end of file diff --git a/tools/buildscripts/build.sh b/tools/buildscripts/build.sh deleted file mode 100644 index 2b7931e9..00000000 --- a/tools/buildscripts/build.sh +++ /dev/null @@ -1,305 +0,0 @@ -#!/bin/bash - -# Unified build script for RE-UE4SS -# Supports native and cross-compilation builds with various configurations - -set -e - -# Default values -GENERATOR="ninja" -COMPILER="" -TOOLCHAIN="" -BUILD_CONFIG="Game__Shipping__Win64" -TARGET="UE4SS" -VERBOSE=0 -CLEAN=0 - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_error() { echo -e "${RED}Error: $1${NC}" >&2; } -print_success() { echo -e "${GREEN}$1${NC}"; } -print_info() { echo -e "${YELLOW}$1${NC}"; } - -# Function to display usage -usage() { - cat << EOF -Usage: $0 [OPTIONS] - -Options: - -g, --generator GENERATOR Build system generator (ninja, make) [default: ninja] - -c, --compiler COMPILER Compiler to use (gcc, clang, msvc) [default: system default] - -t, --toolchain TOOLCHAIN CMake toolchain file name (without path/extension) - Examples: xwin-clang, xwin-clang-cl, msvc-wine, clang-wine - -b, --build-config CONFIG Build configuration [default: Game__Shipping__Win64] - --target TARGET Build target [default: UE4SS] - -v, --verbose Enable verbose output - --clean Clean build directory before building - -h, --help Display this help message - -Environment Variables: - XWIN_DIR Path to xwin directory (for xwin toolchains) - WINE_PREFIX Wine prefix path (for wine toolchains) - CMAKE_PREFIX_PATH Additional CMake search paths - -Examples: - # Native build with system compiler - $0 - - # Native build with clang - $0 --compiler clang - - # Cross-compile with xwin clang-cl - $0 --toolchain xwin-clang-cl - - # Cross-compile with MSVC under Wine - $0 --toolchain msvc-wine - - # Build specific configuration - $0 --build-config Game__Debug__Win64 - - # Clean build with make generator - $0 --generator make --clean -EOF - exit 0 -} - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - -g|--generator) - GENERATOR="$2" - shift 2 - ;; - -c|--compiler) - COMPILER="$2" - shift 2 - ;; - -t|--toolchain) - TOOLCHAIN="$2" - shift 2 - ;; - -b|--build-config) - BUILD_CONFIG="$2" - shift 2 - ;; - --target) - TARGET="$2" - shift 2 - ;; - -v|--verbose) - VERBOSE=1 - shift - ;; - --clean) - CLEAN=1 - shift - ;; - -h|--help) - usage - ;; - *) - print_error "Unknown option: $1" - usage - ;; - esac -done - -# Function to check if a tool is available -check_tool() { - if ! command -v "$1" &> /dev/null; then - print_error "$1 is not installed or not in PATH" - return 1 - fi - return 0 -} - -# Validate generator -case $GENERATOR in - ninja) - check_tool ninja || exit 1 - CMAKE_GENERATOR="Ninja" - ;; - make) - check_tool make || exit 1 - CMAKE_GENERATOR="Unix Makefiles" - ;; - *) - print_error "Invalid generator: $GENERATOR" - exit 1 - ;; -esac - -# Get script and project directories -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -# Use custom CMake if available -if [ -x "$HOME/.local/bin/cmake" ]; then - CMAKE_BIN="$HOME/.local/bin/cmake" -else - CMAKE_BIN="cmake" -fi -check_tool "$CMAKE_BIN" || exit 1 - -# Set up build directory name -BUILD_DIR_PREFIX="build" -if [ -n "$TOOLCHAIN" ]; then - BUILD_DIR_PREFIX="${BUILD_DIR_PREFIX}_${TOOLCHAIN}" -fi -if [ -n "$COMPILER" ]; then - BUILD_DIR_PREFIX="${BUILD_DIR_PREFIX}_${COMPILER}" -fi -BUILD_DIR="${BUILD_DIR_PREFIX}_${BUILD_CONFIG}" - -# Set up CMake arguments -CMAKE_ARGS=( - "$PROJECT_ROOT" - -G "$CMAKE_GENERATOR" - -DCMAKE_BUILD_TYPE="$BUILD_CONFIG" - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -) - -# Set up compiler if specified -if [ -n "$COMPILER" ]; then - case $COMPILER in - gcc) - check_tool gcc || exit 1 - check_tool g++ || exit 1 - export CC=gcc - export CXX=g++ - ;; - clang) - check_tool clang || exit 1 - check_tool clang++ || exit 1 - export CC=clang - export CXX=clang++ - ;; - msvc) - if [ -z "$TOOLCHAIN" ]; then - print_error "MSVC requires a toolchain (e.g., --toolchain msvc-wine)" - exit 1 - fi - ;; - *) - print_error "Invalid compiler: $COMPILER" - exit 1 - ;; - esac -fi - -# Set up toolchain if specified -if [ -n "$TOOLCHAIN" ]; then - TOOLCHAIN_FILE="$PROJECT_ROOT/cmake/toolchains/${TOOLCHAIN}-toolchain.cmake" - if [ ! -f "$TOOLCHAIN_FILE" ]; then - print_error "Toolchain file not found: $TOOLCHAIN_FILE" - print_info "Available toolchains:" - ls -1 "$PROJECT_ROOT/cmake/toolchains/" | grep -E '\.cmake$' | sed 's/-toolchain\.cmake$//' | sed 's/^/ /' - exit 1 - fi - CMAKE_ARGS+=(-DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE") - - # Handle toolchain-specific requirements - case $TOOLCHAIN in - xwin-*) - # Check xwin directory - XWIN_DIR="${XWIN_DIR:-$PROJECT_ROOT/xwin}" - if [ ! -d "$XWIN_DIR" ]; then - print_error "xwin directory not found at $XWIN_DIR" - print_info "Please run: xwin --accept-license splat --output $XWIN_DIR" - print_info "Or set XWIN_DIR environment variable" - exit 1 - fi - export XWIN_DIR - - # Don't set UE4SS_PROXY_PATH - let CMake use its default behavior - ;; - *wine*) - # Check Wine - check_tool wine || exit 1 - - # Set Wine prefix if not already set - export WINE_PREFIX="${WINE_PREFIX:-$HOME/.wine}" - - # Disable Wine debug output - export WINEDEBUG="-all" - ;; - esac -fi - -# Print build configuration -echo "========================================" -print_info "Building RE-UE4SS" -echo "========================================" -echo "Generator: $GENERATOR" -echo "Compiler: ${COMPILER:-system default}" -echo "Toolchain: ${TOOLCHAIN:-none}" -echo "Configuration: $BUILD_CONFIG" -echo "Target: $TARGET" -echo "Build dir: $BUILD_DIR" -echo "========================================" - -# Clean build directory if requested -if [ $CLEAN -eq 1 ]; then - print_info "Cleaning build directory..." - rm -rf "$BUILD_DIR" -fi - -# Create and enter build directory -mkdir -p "$BUILD_DIR" -cd "$BUILD_DIR" - -# Create .gitignore to prevent accidental commits of build artifacts -if [ ! -f ".gitignore" ]; then - cat > .gitignore << 'EOF' -# Automatically generated .gitignore for build directory -# This prevents accidental commits of build artifacts - -# Ignore everything in this directory -* -EOF - print_info "Created .gitignore in build directory" -fi - -# Configure with CMake -print_info "Configuring with CMake..." -if [ $VERBOSE -eq 1 ]; then - echo "CMake command: $CMAKE_BIN ${CMAKE_ARGS[@]}" -fi - -if ! "$CMAKE_BIN" "${CMAKE_ARGS[@]}"; then - print_error "CMake configuration failed!" - exit 1 -fi - -# Build -print_info "Building target: $TARGET..." -BUILD_ARGS=() -if [ $VERBOSE -eq 1 ]; then - case $GENERATOR in - ninja) BUILD_ARGS+=(-v) ;; - make) BUILD_ARGS+=(VERBOSE=1) ;; - esac -fi - -if ! "$CMAKE_BIN" --build . --target "$TARGET" -- "${BUILD_ARGS[@]}"; then - print_error "Build failed!" - exit 1 -fi - -# Return to original directory -cd "$PROJECT_ROOT" - -echo -print_success "========================================" -print_success "Build completed successfully!" -print_success "Configuration: $BUILD_CONFIG" -print_success "Output is in: $BUILD_DIR" -if [ -f "$BUILD_DIR/compile_commands.json" ]; then - print_success "Compile commands: $BUILD_DIR/compile_commands.json" -fi -print_success "========================================" \ No newline at end of file diff --git a/tools/buildscripts/internal_build_tools/set_version.bat b/tools/buildscripts/internal_build_tools/set_version.bat deleted file mode 100644 index c2da43e4..00000000 --- a/tools/buildscripts/internal_build_tools/set_version.bat +++ /dev/null @@ -1,52 +0,0 @@ -set set_version_param_major=%1% -if not defined set_version_param_major ( - echo Was unable to call 'set_version' because no major version was passed - pause - exit -) - -set set_version_param_minor=%2% -if not defined set_version_param_minor ( - echo Was unable to call 'set_version' because no minor version was passed - pause - exit -) - -set set_version_param_hotfix=%3% -if not defined set_version_param_hotfix ( - echo Was unable to call 'set_version' because no hotfix version was passed - pause - exit -) - -set set_version_param_prerelease=%4% -if not defined set_version_param_prerelease ( - echo Was unable to call 'set_version' because no pre-release version was passed - pause - exit -) - -set set_version_param_beta=%5% -if not defined set_version_param_beta ( - echo Was unable to call 'set_version' because no beta version was passed - pause - exit -) - -if not exist generated_src ( - echo Unable to find project directory, 'generated_src'. - pause - exit /b -) - -if not exist generated_src\version.cache ( - echo Unable to find version cache file, 'generated_src\version.cache'. - pause - exit /b -) - -echo Setting version to: %set_version_param_major%.%set_version_param_minor%.%set_version_param_hotfix%.%set_version_param_prerelease%.%set_version_param_beta% - -Rem Caching the incremented version -set full_version_string=%set_version_param_major%.%set_version_param_minor%.%set_version_param_hotfix%.%set_version_param_prerelease%.%set_version_param_beta% -echo %full_version_string%> generated_src\version.cache diff --git a/tools/buildscripts/release.py b/tools/buildscripts/release.py deleted file mode 100644 index 5544b1a7..00000000 --- a/tools/buildscripts/release.py +++ /dev/null @@ -1,382 +0,0 @@ -#!/usr/bin/env python3 - -import re -import os -import shutil -import subprocess -import argparse -from datetime import datetime -import sys -import json - -class ReleaseHandler: - def __init__(self, is_dev_release, is_experimental, release_output='release'): - self.is_dev_release = is_dev_release - self.is_experimental = is_experimental - self.release_output = release_output - self.staging_dir = os.path.join(self.release_output, 'StagingDev') if self.is_dev_release else os.path.join(self.release_output, 'StagingRelease') - self.ue4ss_dir = os.path.join(self.staging_dir, 'ue4ss') - - # TODO: Move all these hardcoded values into a release config file or similar to pass into the script - # List of CPP Mods with flags indicating if they need a config folder and if they should be included in release builds - self.cpp_mods = { - 'KismetDebuggerMod': {'create_config': True, 'include_in_release': False}, - 'EventViewerMod': {'create_config': False, 'include_in_release': False}, - } - - # Lua mods to exclude from the non-dev/release version of the zip - # And remove their entries from mods files - self.lua_mods_to_exclude_from_release = [ - 'ActorDumperMod', - 'jsbLuaProfilerMod', - ] - - # Lua mods to disable (but keep) in the non-dev/release version - self.lua_mods_to_disable_in_release = [ - 'LineTraceMod', - ] - - # Files in root/assets to exclude from the non-dev/release version of the zip - self.files_to_exclude_from_release = [ - 'Mods/shared/Types.lua', - 'Mods/shared/jsbProfiler', - 'UE4SS_Signatures', - 'VTableLayoutTemplates', - 'MemberVarLayoutTemplates', - 'CustomGameConfigs', - 'MapGenBP', - 'Changelog.md', - ] - - # Settings to change in the release. The default settings in assets/UE4SS-settings.ini are for dev - self.settings_to_modify_in_release = { - 'GuiConsoleVisible': 0, - 'GuiConsoleEnabled': 0, - 'ConsoleEnabled': 0, - 'EnableHotReloadSystem': 0, - 'MaxMemoryUsageDuringAssetLoading': 80, - 'bUseUObjectArrayCache': "false", - } - - def make_staging_dirs(self): - shutil.copytree('assets', self.ue4ss_dir) - shutil.copy('LICENSE', os.path.join(self.ue4ss_dir, 'LICENSE')) - - if not self.is_dev_release: - for file in self.files_to_exclude_from_release: - path = os.path.join(self.ue4ss_dir, file) - if os.path.exists(path): - if os.path.isfile(path): - os.remove(path) - elif os.path.isdir(path): - shutil.rmtree(path) - self.modify_settings(self.settings_to_modify_in_release) - - # Remove lua mods from the Mods directory - for mod in self.lua_mods_to_exclude_from_release: - mod_path = os.path.join(self.ue4ss_dir, 'Mods', mod) - if os.path.exists(mod_path): - if os.path.isfile(mod_path): - os.remove(mod_path) - elif os.path.isdir(mod_path): - shutil.rmtree(mod_path) - else: - # only include README file in dev releases - shutil.copy('README.md', os.path.join(self.ue4ss_dir, 'README.md')) - - ue4ss_dll_path, ue4ss_pdb_path, dwmapi_dll_path, cpp_mods_paths = self.scan_directories() - - self.copy_cpp_mods(cpp_mods_paths) - self.modify_mods_txt() # can only run this after copy mods just in case we are missing mod dlls - self.modify_mods_json() - self.copy_executables(dwmapi_dll_path, ue4ss_dll_path, ue4ss_pdb_path) - self.copy_docs() - - # needs to be run inside staging as we don't want to modify the original - # only run this if we are not doing an experimental release, as we don't want the date to be set in experimental releases - if not self.is_experimental: self.modify_changelog() - - def modify_settings(self, settings_to_modify): - config_path = os.path.join(self.ue4ss_dir, 'UE4SS-settings.ini') - with open(config_path, mode='r', encoding='utf-8') as file: - content = file.read() - - for key, value in settings_to_modify.items(): - pattern = rf'(^{key}\s*=).*?$' - content = re.sub(pattern, rf'\1 {value}', content, flags=re.MULTILINE) - - with open(config_path, mode='w', encoding='utf-8') as file: - file.write(content) - - def scan_directories(self): - ue4ss_dll_path = '' - ue4ss_pdb_path = '' - dwmapi_dll_path = '' - cpp_mods_paths = {mod: '' for mod in self.cpp_mods if self.is_dev_release or self.cpp_mods[mod]['include_in_release']} - scan_start_dir = '.' - - for root, _, files in os.walk(scan_start_dir): - for file in files: - if file.lower() == "ue4ss.dll": - ue4ss_dll_path = os.path.join(root, file) - if file.lower() == "ue4ss.pdb": - ue4ss_pdb_path = os.path.join(root, file) - if file.lower() == "dwmapi.dll": - dwmapi_dll_path = os.path.join(root, file) - for mod_name in cpp_mods_paths: - if file.lower() == mod_name.lower() + '.dll': - cpp_mods_paths[mod_name] = os.path.join(root, file) - - return ue4ss_dll_path, ue4ss_pdb_path, dwmapi_dll_path, cpp_mods_paths - - def copy_cpp_mods(self, cpp_mods_paths): - for mod_name, dll_path in cpp_mods_paths.items(): - if dll_path: - mod_dir = os.path.join(self.ue4ss_dir, 'Mods', mod_name, 'dlls') - os.makedirs(mod_dir, exist_ok=True) - shutil.copy(dll_path, os.path.join(mod_dir, 'main.dll')) - if self.is_dev_release: - pdb_path = dll_path.replace('.dll', '.pdb') - if os.path.exists(pdb_path): - shutil.copy(pdb_path, os.path.join(mod_dir, 'main.pdb')) - else: - print(f'Error: {mod_name}.dll not found, build has failed.') - sys.exit(1) - - for mod_name, mod_info in self.cpp_mods.items(): - if self.is_dev_release or mod_info['include_in_release']: - if mod_info['create_config']: - os.makedirs(os.path.join(self.ue4ss_dir, 'Mods', mod_name, 'config'), exist_ok=True) - - def modify_mods_txt(self): - mods_to_remove_from_release = self.lua_mods_to_exclude_from_release.copy() - for mod_name, mod_info in self.cpp_mods.items(): - if not mod_info['include_in_release']: - mods_to_remove_from_release.append(mod_name) - - mods_path = os.path.join(self.ue4ss_dir, 'Mods', 'mods.txt') - with open(mods_path, mode='r', encoding='utf-8') as file: - content = file.readlines() - - if self.cpp_mods: - content = [f'{mod} : 1\n' for mod in self.cpp_mods] + content - - if not self.is_dev_release: - content = [line for line in content if not any(mod in line for mod in mods_to_remove_from_release)] - # Disable mods that should be kept but disabled in release - new_content = [] - for line in content: - disabled = False - for mod in self.lua_mods_to_disable_in_release: - if mod in line: - new_content.append(f'{mod} : 0\n') - disabled = True - break - if not disabled: - new_content.append(line) - content = new_content - - with open(mods_path, mode='w', encoding='utf-8') as file: - file.writelines(content) - - def modify_mods_json(self): - mods_path = os.path.join(self.ue4ss_dir, 'Mods', 'mods.json') - with open(mods_path, mode='r', encoding='utf-8') as file: - content = json.load(file) - - if self.cpp_mods: - for mod_name, mod_info in self.cpp_mods.items(): - if mod_name not in [mod['mod_name'] for mod in content]: - if not self.is_dev_release: - content.append({'mod_name': mod_name, 'mod_enabled': mod_info['include_in_release']}) - else: - content.append({'mod_name': mod_name, 'mod_enabled': True}) - - if not self.is_dev_release: - content = [ - mod for mod in content - if mod['mod_name'] not in self.lua_mods_to_exclude_from_release - and self.cpp_mods.get(mod['mod_name'], {}).get('include_in_release', True) - ] - # Disable mods that should be kept but disabled in release - for mod in content: - if mod['mod_name'] in self.lua_mods_to_disable_in_release: - mod['mod_enabled'] = False - - with open(mods_path, mode='w', encoding='utf-8') as file: - json.dump(content, file, indent=4) - - def copy_executables(self, dwmapi_dll_path, ue4ss_dll_path, ue4ss_pdb_path): - shutil.copy(dwmapi_dll_path, self.staging_dir) - shutil.copy(ue4ss_dll_path, self.ue4ss_dir) - if self.is_dev_release: shutil.copy(ue4ss_pdb_path, self.ue4ss_dir) - - def copy_docs(self): - if self.is_dev_release and os.path.exists('docs'): - shutil.copytree('docs', os.path.join(self.ue4ss_dir, 'Docs')) - - def modify_changelog(self): - changelog_path = os.path.join(self.ue4ss_dir, 'Changelog.md') - if os.path.exists(changelog_path): ready_changelog_for_release(changelog_path) - - def package_release(self): - try: - version = subprocess.check_output(['git', 'describe', '--tags', '--exclude', '*experimental*']).decode('utf-8').strip() - except subprocess.CalledProcessError: - print('Error: git describe failed. Make sure the release has been tagged.') - sys.exit(1) - main_zip_name = f'zDEV-UE4SS_{version}' if self.is_dev_release else f'UE4SS_{version}' - output = os.path.join(self.release_output, main_zip_name) - shutil.make_archive(output, 'zip', self.staging_dir) - print(f'Created package {output}.zip') - - def cleanup(self): - shutil.rmtree(self.staging_dir) - -class Packager: - """ - "Release" refers to the zip that is distributed to the end users. It should be in the format UE4SS_.zip and contains only the necessary files. - "Dev" refers to the zip that is used for development. It should be in the format zDEV-UE4SS_.zip and contains all the files, pdbs and docs. - """ - - def __init__(self, args): - self.args = args - self.release_output = 'release' - self.setup() - - def setup(self): - if os.path.exists(self.release_output): shutil.rmtree(self.release_output) - os.mkdir(self.release_output) - - def run(self): - dev_handler = ReleaseHandler(is_dev_release=True, is_experimental=self.args.e, release_output=self.release_output) - release_handler = ReleaseHandler(is_dev_release=False, is_experimental=self.args.e, release_output=self.release_output) - - dev_handler.make_staging_dirs() - dev_handler.package_release() - dev_handler.cleanup() - - release_handler.make_staging_dirs() - release_handler.package_release() - release_handler.cleanup() - - shutil.make_archive(os.path.join(self.release_output, 'zCustomGameConfigs'), 'zip', 'assets/CustomGameConfigs') - shutil.make_archive(os.path.join(self.release_output, 'zMapGenBP'), 'zip', 'assets/MapGenBP') - - changelog = parse_changelog(self.args.changelog_path) - with open(os.path.join(self.release_output, 'release_notes.md'), 'w') as file: - file.write(changelog[0]['notes']) - - print('Done') - -def ready_changelog_for_release(changelog_path): - with open(changelog_path, 'r') as file: - lines = file.readlines() - version = lines[0].strip() - if lines[2] != 'TBD\n': - raise Exception('date is not "TBD"') - lines[2] = datetime.today().strftime('%Y-%m-%d') + '\n' - with open(changelog_path, 'w') as file: - file.writelines(lines) - return version - -def parse_changelog(changelog_path): - with open(changelog_path, 'r') as file: - lines = file.readlines() - delimeters = [index - 1 for index, value in enumerate(lines) if value == '==============\n'] - delimeters.append(len(lines) + 1) - return [{ - 'tag': lines[index[0]].strip(), - 'date': lines[index[0] + 2].strip(), - 'notes': ''.join(lines[index[0] + 3:index[1]]).strip(), - } for index in zip(delimeters, delimeters[1:])] - -def package(args): - packager = Packager(args) - packager.run() - -def release_commit(args): - version = ready_changelog_for_release(args.changelog_path) - message = f'Release {version}' - subprocess.run(['git', 'add', args.changelog_path], check=True) - if args.username: - subprocess.run(['git', '-c', f'user.name="{args.username}"', '-c', f'user.email="{args.username}@users.noreply.github.com"', 'commit', '-m', message], check=True) - else: - subprocess.run(['git', 'commit', '-m', message], check=True) - subprocess.run(['git', 'tag', version], check=True) - - # Outputs to GitHub env if present - def github_output(name, value): - if 'GITHUB_OUTPUT' in os.environ: - with open(os.environ['GITHUB_OUTPUT'], 'a') as env: - env.write(f'{name}={value}\n') - - github_output('release_tag', version) - -if __name__ == "__main__": - # Change dir to repo root - os.chdir(os.path.join(os.path.dirname(__file__), '..', '..')) - - parser = argparse.ArgumentParser() - parser.add_argument('--changelog_path', default='assets/Changelog.md', required=False) - - subparsers = parser.add_subparsers(dest='command', required=True) - - package_parser = subparsers.add_parser('package') - package_parser.add_argument('-e', action='store_true') - - release_commit_parser = subparsers.add_parser('release_commit') - release_commit_parser.add_argument('username', nargs='?') - - args = parser.parse_args() - commands = {f.__name__: f for f in [ - package, - release_commit, - ]} - commands[args.command](args) - -""" -How to run this script: -1. Running the 'package' command: - Usage: python release.py package - - --changelog_path : Optional argument to specify the path to the changelog file. Default is 'assets/Changelog.md' - -e : Argument used when running the script for experimental release - - Examples: - - python release.py package - - python release.py package -e --changelog_path custom/Changelog.md - -2. Running the 'release_commit' command: - Usage: python release.py release_commit [username] - - username : Optional argument to specify a github username - --changelog_path : Optional argument to specify the path to the changelog file. Default is 'assets/Changelog.md' - - Examples: - - python release.py release_commit - - python release.py release_commit ${{ github.actor }} - - python release.py release_commit ${{ github.actor }} custom/Changelog.md -""" - -""" -To locally debug this script in vscode, make launch.json in .vscode and add the following configurations: - -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Debug Release Script", - "type": "debugpy", - "request": "launch", - "program": "${workspaceFolder}/tools/buildscripts/release.py", - "console": "integratedTerminal", - "args": [ - "package", - "-e" - ], - "justMyCode": true - } - ] -} -""" diff --git a/xmake.lua b/xmake.lua index 73dac9ec..39cde7bd 100644 --- a/xmake.lua +++ b/xmake.lua @@ -56,10 +56,6 @@ on_install(function(target) end) includes("deps") includes("UE4SS") -if is_plat("windows") then - includes("cppmods") -end - -- TODO: Remove this before the next release. It only exists to maintain backwards compat -- warnings for older mod templates. set_config("scriptsRoot", path.join(os.scriptdir(), "tools/xmakescripts"))