From 9007092afdaf92421c0b5d68ceafbfe08c3701fc Mon Sep 17 00:00:00 2001 From: Jackz Date: Thu, 28 Mar 2024 12:13:03 -0500 Subject: [PATCH] New socket system --- plugins/adminpanel.smx | Bin 15570 -> 20068 bytes scripting/adminpanel.sp | 863 ++++++++++++++++++++++++++++++---------- 2 files changed, 647 insertions(+), 216 deletions(-) diff --git a/plugins/adminpanel.smx b/plugins/adminpanel.smx index 037e2726aa3d480c17c72caa178b2a8476dd707c..4367f5089b50bdfd3540dde26e06f65058d80d06 100644 GIT binary patch literal 20068 zcmb@Nbx>SQ)94ouo)9Fsy9IX(4#C~s-Q9u(cXxMpCn30d(8V2=#hts)^Sy6f`TK5F zSO0pZr>A%J>`a|LyAl$LD$r0+rt$#5i69gJn*<5~AcF?{`}#+pc>)0N5DU!*0FW{V z08k*Tz6}6qLi(^ET)PVZ)I-J)L0Equ0HA>|HH5!149E1&Mp#UEtdL1GT5GH`+ za17xndMLnmH~`=d!pmQw0H4ty`I|VHngN(hja-cYOpb2GwpJ!C|AM=bGX(67T&>*A z{slXTa&~pK`XAqa$ ze|MT&nc4m~+`kcAo!w0S>-)bO4F2QCWNK`|WNu~qPk#TPt(ErB{`S5c;W2i0MNz`YOo>=W z$v84n+!38zU3r^$rL+(LI^sw-X3nWgK**nNJ`c49DE&^aTeJ^fK{ zqEc@HPj7-(cfwnDVvfliL2m-BE1l4Vh40wdeQ5BkJ8Fdzr>Tm`oJ((FNN)nEhiU)9 z_`rju%ALi=l?Ayy9knz4+=GSh%-H?+;92#*-lt6FkVyyOIwr^djnCPUe%qd20*Tz2 zPUytKw{7gcGQeWy!w{!j{4afxIdJ}hD=`sH^$Az)`*uVL};WUjinJ?>2 zQ0Y$iivN?n`k`^_=wS8QAOj?knZdL2s1;hArfVj14c!U*j`TQ2aarssIDbV1(fz@Y8=;Dp8*Ynn4ll?%(-p7GMTajH8DA7pt~2hW_* znl$#isQNsVUZMQHC5$5#PpFduWs^Pf$eKJUL2!yQzw-1V`KaIj@OS+6zTf*$COj4g zC4J{OvcdgQQN}A^V8zzNAcTbVKIyE!!&304$T<}&E@IgrVB^vx?56D7DD`5uRG7%I zKFK7C&#q_LLyg*w2G=;!JT24S;rHV=4$8QL@f)TRuS&+cLmH>%GJ1_S?D6J(I-#z- zH=dVXo5(jM-)VB<1DO|!m5ibcg$QNlP+PKB_A|av4%t!S$Z1yG<w=jSAxFHPWq(Zgbb$y>8*rV#X;84+hHO8{JqpV`!Y z*0gWQ#{JL%J?7y6GTZpAcLqlsQ_4Pz!K%u}!`?b0cc>?kR{HycJ%}&n9`6J=Zu%w= zpD_)hdM6r*9f$~hY!lBMR`z1&esDQJdvw``h*{(t_vG48ikD7-=ohX{LtP;GDLZ67uKA-X5%;fue;!gwRs!L*O5kT58@wjbu~?k~&= z>vsEGI2X+^ewtnPUrzUO{uY4>yV=dQ2h%*eW?&OKosEPiu%cM@ybRlVlOjhd=B-wO5U$%D$_a5LUh#YCJ>N& zM+X_2QPxx9wOTbbudkPThj-Q7#;M>Fa{60F zUbFfV*s*lwJ7pa@|J8edKF(go@+MdM@^#7!L7n1Wj`+$|EZ8sZ1>3}GFd(nnuj=aD zJ(T19>%-#^i$-r6&EsZ^skPN+N|ElMcbl^PJsxXLvQW%rEOW+s9-m&SgPD-=$d8UY zR@IL3qV*jwiVd16eHG2J21*s`eE#_bF zUVYy-ELiS>X$;~t%rCUglXpXCRhXU8uhmAG7dF@N5)&L$nI+o}n06KD-zFCt=Vibh z3aK2AZk9H8{=W&#>VA=*f3XL7{AFfcpBNdKWTiFni!^P`Z{gZad5FiDMo4beEFXdT zlC!)r-!e{G<5E0*z$-Shlc%;h4OjDZ-0X#BB6cw2)|wUbxiGTNWbD~9_A44g>)dT} zV!$7+Ga;JmVJidhmazp_lD>qOZ}1m(=Z(!zVz>dR6u3C7vwUIfYxj@MJ35yW%Cd%& z@BeWrZ=F^2x5uUj?ena6&%SA19||q=D4$nPZ)qGn9iP}=NIO*dl->H3@+Pxk?^J|-FQ9g4(GMjRXx$j!^ zLoAsfPB3}G2VWEI_pJSzR$+3HZvgf3xJ+3wmuV>vmr16&Y1zHm(--@6#ZDx#3L>{u zJnlCW2}lmfE;1K_f?I6wQe@L|ORoe~vv`EO)H-Qh;~TQCtlm>A_EmNxUpC|^^RB@3 z8iF-r={|A-#0PXPhaIgQ-?S@yWRp3K)69rctco~tb2~?01ri5+E(mOWCb>cvT65ncaM~BX99(BVJ1o}h#@uz5bmDXdwBGm-jI~fFPj<~@O|IpQ91yNp zhCUKy2zr-Um{Nv&I8k0loz^(>w{tWR0_!t)j-Myf%_4)!iXjOyKdL) zZ_gj-#0`9%?sWBEY73V;DbaP!;G-ZA=3qj9V94)m02_?laxH9;MBIBOgpM8kr=#TCG~> zRDkyCZboN*(t33+20Kgg=hr+%L#Me*au@D8dEoZunwBjZ6y7+BZ*>n$4mEWH73Q<+d8E>{{@EiAW{kigbouVoBYSfYSMD@MAyUSt% zjMq}a=a?}E?Q5wtUNk1vvjv{w4QY7(4T}z~ifA}{=iwa9frjZaiOj3IBKzwRz1Di? z(cXjHEZJ<|NtLYK^Gq?WE8Ezs>NNgm5CPR1e%I<5LwF9zC6{l1HQ4+6YC$tWWrlBW za)H4c{5-|fn5X^KE&DUknd*-FzT83EmjRG)Mum4!n|97A9ysU9j6d#GtL*Y@|0Ifg znX;?LaiPP1Eg)+@6~wie& zT?+8N#u0h>e}OaRXbR-HsT>r=MaeA|LKiY z!JE1}RjbI;$)6ZN7fP!T1+^WwB!>cZ{VV%aIA6G4^bZu(@Av~&seEb`*ZC8|@t=u? zPYAA`bZxXM`=)75z4dQBgw9v)7Ts>xS{`F=_qX}@$^<%AIuxc-(5Rp9T)&%5X*z|^ zyBLly%$fWJx*;4K!*OoNYU_skEfxM3{9 zsaFAT&icXF;Tjs4!RaUUWLCv%D)&R(Qqx%a^WVY?xU}{&r&TvmdxXny9Vn!`iBI45TiXMh!GB3FeXg(uVx6!S0yLPKr4>o~@r-DsbCTspG-mC{%5d{jg3kE8IP+~ zp_fXqQ=qS>YCNi4Fh?(2G>&ZojGyYnaVp2{1I(S%YMs{$S3PqJ$#JV+w#u+LEqBp> zCfMFV8=tbpA-wlH8}7-zjTFYVvA%%;K_0iQd|E`vviHV8-_8YK=6A+4XyZr$u!P>{57s@tJhwmwKkKo z%8!mYCv#Wkul`yF^wiq{HB79B#Z+3Evwx5_mA^n=ldkW#x?4{brXKR!?)1?eP%EcB zdY^d4d~8p$-Dc&4s0=W5d9u34V6TF1fVZKF}3 zTKBenkFo*>W8$vN6C;3z=`@9($mwzblz5@*gr{d%qos?)`)g3)ZSWpz6P%9P#!>P-4gKPO?b=A%$lJSOODM~$DL8W~ z{L^ZF>V%8na-5-*C4iG?foZo7nkSXrR6XoH{I+t6Xl&ob(RI<>XB@g#I~Ck!J`UQT zwOAP)wI9^?&>FUqZ-Qrvw2fqqd53U2PJD`Wxm2K7?>zZX&*7qg#vPPWaC8C8VcFrSy$(-t(%4-5O zViP*Bvy4fvP03?1OI}az=in7P%4Pq!g1)3UJ?#Mlqj8|F&kr~m-|Vxsdv2X^XbM2R<&vLU$ZIt5Jkerfs5bn_D^Gy6C-v z&!^t_klWl7swU8YS?}#zs%UAYifAekf*RU!eeAa#l2aU2I2YLlIGt4Y z^gv*F`T2<@yGNJu^sp!WFg&$iLRDu|?@yLG#p!2~_gY#DW$fYVhWE~M2DRr+b*7CZ z8=f0QB(wE#qH(l=8U>Bl=ur<*hf*UNm+U)*!X~v#6`CYnJFgeC=6(F68=TNr52k3{ z^W(jjihHg~AA{pPZ~R5XLsq5XTi@}J6Y0Od`t(+_Kse5$^=)_ZZ(U;p-6t+q+dVYejl^}FJ8r;U_hhaQ(j1kufa?@y1J1FQZO=H~`!6m0j^ z3p}B-!z&wAhZngDywm$AdZ1+sXH6aULq6Fd!aHaSdY=&q|BmD3RBE>8n&14_j2ds# z%1dTe=u}K@8|!l>%xih(e|HFYX0G33cEReQS_Kn+F>GTxrProoURUbFC3}5Jx?>l@ zdP=YJkxhpP^5EPQa{o#u;Xw+M0fx1Qnzz9%68^O~qTF#G)SkH`3SAU;23+`FQpEfZR&_W9{C*zVIHcP{Qlkp7kQA+kk z?TvWIqxD4Pa7X=7A%t5=IPq#npkOoG8?Tg4JU1*Dktp_>A<3{s?Fjr z>8;{#nX-7YT70H!I#ESEBFiQ0`Qs2(GU*`?-RBaz<5A^*?IFvLr!eLb%iBjzckDUW zI1Fs5xruq|l2gbho*Td?^TzZ?8K$Y07noGq^2mtV(Oa?tGZm3+@_72E@lbD?U5@d= z#W>t}n}6}-8tCuBh2Na~D#lFvU!3s3g}*Pvm0tf8iFPWXZ@Z*YF%xKi5M!jWKcH}` zaw;`658m8j@K*c0K5yZ*h!htpxk6t#IL zCq1@>iz>T6N?~>xtzCI#n#k%YVWQ$CF*$hfoLN$NHYymV?j-MF%mQj6vxI&R=zCia6y6>>p$TC4>=# zi>50dqc8$A7*^4O0xM0_58EB)go>$(0)+d7ah%n zcQr%U53NALLE(6`Rh`S<*wa!fj1h(^fARn3E@ksn}9oXv0CZj2DiR;;lmn0`okpla>SmCVd*XsMwJGdlL77fuS zu`mww`+Jq63}Dbm{FI$7cgBK=RdFw(|B#9HsqO-yB03EcFMt=C7m635lRqthix-M6 zr_3rPf5@vE1|L2qP(ox_xJ=|wBu#`%p|arhM7_)BLRbwR|v!J}d)#O5m8V|BUjC_Kdzj^3UNP4W8e* zHb2L!iGYGz*m69nDQWA1{ zooUoNe8t5f%sru&!ERx*Ta6LN@Mh)4jF6Zs*b|(*kfi%0gWLTk#hQW^Mz+0mA*CHL z@G&gR)i*9h!V~vQd|c^FQ*C?)HALC3Al;Z6S0Tih_Z}$`hj@i6kav(U^iwX;;v&Cr z8%9LQHy^nGC1g<}fiI{9P$xqgr4W07$`T zLGr;&KkR?_-WP(P#6={9Erc~hJ{P0ce|*I+$7)7gKsy1z3G3zCL#{d$JXmoNL`6Jy z=v8#c1^59^Z{%3F7srmjT2f`4M>0^t?6d|CNz!GdAd>6l?q~guGAAeC(l(_Xtu|>xYp)-pHuakz)`l9oK?lo=G=Uj$9v9hy+yf>t$ zk*~Z~Y3&Jw7i}xbXnYH4DjKwIbw{g{+)4_$NWJ_7YdzU~k0&R18A_EIXO~9u zr#{k}WA&m{ArYiEZ{sye{qULL^K(?}9#0XpiFTZxj&?FhygD9C zCp;VOc5aKxTLL8Ico^q9ImyDrsd%NsAargfSkv>l{F%oK7vqvLiL>!*=$>xASQ5OM z>8CgkMyStrQ^cL2vU&cp2Wif%5!$@{@g9-LL}6oyLDlp3Q|}O z4IQcI)hG#*Xr5!8@EhL)B?lGdi{_K(o0$2gfpBNEAfJr*4Au;LoH_*cr>cN$UmB;VU_xh#u17bI0@P^Q-c0@=wt(^+vfw z+JxIgO!A#(U}@>n$oqr`Nr=l~Rp~&?cVn-U(4pp1e*7jvEt(hPsUv>Bd7K>_YF~Ob zeRaINn~%Ny%ZTDoffLtHguW9E)Stee78JnNgt>*?!Og5KZdNp}ncWu{(&elZ$jo4P z8L%cwIcyKEP||KTah4}ZaJhEU2CYNoX%<_RVQbws((+TNtC@JK`d|}2tXVN~^YR1C zc`xFGF$hVN*7~>-iEhF}_Cx9!?)s~$q|yn$mwm3_7*9j*pK^3iGw`*19N$}11YTL{ zwYz6HwlWaNpc?Cn{#_Q<5Dtx+^~dgOxh*Yq#~N&`l<=Q_L~~eFdHeXJugGO7boAwj z;0VtWTJ`PggvbVDyEe&pfvgUq;v@N21h?S3;Mkz4pxPiR;eD+C$CfojJApq(AcV<* z(Swl-93u|pDaUUnc13YTb46u`q6v@ zZ`fYH|Eq4PUtssR6(6KYXu2^!ATh)I_^l?a=*jaDbBN*wo+es)6IJmG8wfxD5{ZOH z7Xda@16pL;yZ4m78|ecQ7eHit5#o*d5z`-H*1`SwtpFpkZ4sD%$xA}B{sA^LD=^=Y z?IWf=)Q{ga5J!QJn419DP%cOeIbp?i)DK8P$iD8KME{e^e^UDYsjlBapTEcty@1n& zs_)UzAC76r1jmIqFfk=Deg>ob_^pbJa`Kj(yIw^7YFFcWBSMxd^Bn$pTTxr#@%i!V z07Kg+r(CWMyVJ{XW21JN(or`9e(d?@Bex2J+|Hf2;+HBsJUsTA($3Dx($0cPb2B?N zNa2=aqUBlO=Lg4h(Fl^9QhJQbJ45a{An|c<_zu4}>jw$rooYdF?XXz(k#C-1o#4la z(fb8QgFL2K@f`==RniOs`C>Ptyf`L8`Pno#OB}Z7Cc@(hV&1K93S-v{Du_;aAVr!_ z->F3#RK*DNtp6g;cQlacfwxVG#SO^&VqmrRLtLxGjr^!@HNxDGs0quOunaK6)qXb7 z>0e!MW530Hr@B9xnqG?rtG!=mU2v{fFCC0M4zG+!3=MzXyc3hr)*n{ zPfw#I*b%q$Ex4H(?0ZU}>e!EJ7~m(Va2l@+H*FpIO~qYkS9i13K19k!3G>jQL*Nr1 zxx;ekEtFR;`*Kclt3WFsHZaO~*CZP9?44aaR6I+T?6YtMdnMB%;24upKbxz}DyN(i z(Nt1REu}Q>vZLy!O(Ls^s(i?I5C7sYAA`~xJoBNp9HEtYd% z!cPLJwYrV_;Ob6WcYeQMR~d{&+^Np!YgyLQIHGTGo1YWUJ>**R8osK*xf+sIAz6$5 z67UA13E+XEitl?gUlVDj1G&XBk*8Bj3k)4jzg8-0c1tSTHS{Wy!49(;?$~|H{RYadjy}R>{Rgoy=lCPA3lOlXtYZ@NOG-IAp*L$u zJz0F{4#DwH{zdOA$(E@5$%CN39aqGkPD`TjXk=8e@qkSa)cSIKA>SD3XPVhsGF!1c z?L$j@2-C~wo0*|?G^|z|zscDQAAJPm8q`?zp_~aDy>nddsbq4(ch}3$6Xg_GS>SWn z@m*4VMdV*7w(3LYM}48H@79_($Fy3tGvv5bj4Fi-3O(gGhv_%zNA3^CXFr$hC&%Zg zF`_zav00VhvfG-5o_1JjR_QMq+3tgPw-tY8km%-PKPT)DJLEY3+;1s7|JI!jdhE7I zEnPhS=4}opEaa_+ZGjX7z^}yy@S7-CJZAzU#o-p$uW^i!IWz(n%Mm354}$`Dvp{b> zK4`$x=NE$B@O!`h*DnnI9%w)V4_r@-hg}|DYTu4HkcjU2?lY#rCD`wWkDqVZez>o% z)p?xo0loytda%T|pBcL#Di>*NhHdk9)hVBk1O3FJB2eQz9VCvy!1k zM(L{|Q69K05e=~m{h&YAS?-zpQl_NzmGJ9RdFdpR$Wwv=`V6Vbnj%8rhTTYjA)Mov z^vzi(DnDS{b*amL0lg{ZkR-#l;q6*oxV0jcST$Pcm%)@h~xUFodMpl z{>oXL!c3D1y71FT?ullEgN(h?(F(3?R+Z6#gzqSqu=SGz_iPt{Sb%6h;1X*KYO5sfmrD6M%hc=1>X{f z1_}?(@59Y=K679%j5T5{FgT%!QJjm;Sbbi;pxd1(tRUEHzDdW$^n?Xeqvni5tq^s#UpezME4(`mEJ!n*gQ4lsMT zNr^tflYk-GQ&~+tKR^F9)!*96i2vv}{FK_iWT*Z011NZ>70w?k+yoDl5aP>LTU#$U zh>ANzO?-xpLRI+7QqS|FL%Pt(ri26;>jy6ot5lrR!J&Pl>o_MRRs!l&TmD&@wLk_oaA>%V6KF*2UOO_n{?2}3t zHCu_747dy9u}crd2U;N!H|lc~_r!Q-(ub!+l zy6t4=C#r`PX>2I0;QYNcirZ4YTZ$nCt)XcVbuo-B(!P{DR7I_+u(h_<+9rDN1F5Z5 zhQU?*FGX8opm?J=CVXEMpOopZfvQgJaeA%R%vT=;F<=$-qcLeFFo;+otWCN?lI;&M zDp{z+JZ%D7<98h^Z#6X)4V7iXFmf}j$v0FRH;$H44q3c;0!ywFxxUbSRm@$dS@!RhHFg45kuD zOead3rBQYaMTscIJYYjVGe1$Ro~DNa+PbgRbrp3O8@1_1*ZiAc9pqt?;{%FSbK=$N zQqXp#Rv(hvgmb+h_7ZEKVaYEPn4Y9LCvHdpj>KV!^|`@8c5TkGN^`YIqyY$iNShm^NKx< ztw=4jnBoBu*rV$0UVDa;-*NG#DkS15j9p_+eAFgSQmRg8VcXY^A$RSYxihwT-FKqy zryBTLW7yhOJTy^IQB_qj#ux(9V-ugRn<`s7;WMC|zBc36xYMxbd(2pCV3RZGKZmIt zKg7QP^5|g$?0-@}yFM~$v@Y7VhunQm2EmQfD{-lQ zmJyd3Kj=T9)13Tx$0i=2Sy6$LBaa-mfQ^0IGQoTU&G$`OERTwT#g7?yip9nB@U0Dc z{PIJNC?P6sV?e{E;X>dTI((Xo#kcUb)@cD}DB-9tvQUm~g{7^(oFz5fX5pZ3Ojw$EYCV`icozil&mi61{q`Ej~f?2a-_^T+_ zfVlAyCW9<4P--IZGOiLHg{LHSKN>`FlC2vOQM_u|{nJE@3S|1)fZbpD^{OduD0%d1 zeu!m>uldb$t~$I*-DwZCs}RY;rvOqOoDEXDe)W7M!%p?XM^M)&X#1EGql7o+qjr8` z-7Ji9!h36cqcPR%jl1*u`sX{kgz|6Jm%_o4Qd8Dug{IB0Z{()B;iD;TSe3U+l40~3 z3v?c3+73QqEXv7rDfqoo{qPaZADc!gRj3&ayq|YuxR{PZn-IgztdRK zP%+u=lB&UfCeR#dDJQ}gK3u{vi=DzrSFb6rt83zC(edJxBo0CeNtJc+%5*`!iRk$I zQ7k&h6R)LN=m+NVY;~G_338^KZbzQ{xE*vF>TgP16+T0Ho#!l;`^jIi$H#)(>DWe9 zkOD+?e$1GaK2+L#zBT1RS1Di7r;$@a=lUx3+Hb>TCW9EfxvkfURX~%5JrCY9ng~P-u0l_6;=_izX(T#b#F0 z1%eJH_WA368-^U))~hI>Qx~t-tO)Yd6DogS986nEUY!h&NwUWy#*n*mLv5cZW%T|vhr=M)XbBbxV@iv{D`N~vOya0lY`K5SJgqtQ z7%3c;e~}LVF0=@9!be=ZA-nj@p)F`JyX5c0Y@!K9CCMH~j56!zD%r^@q=9w(bxd%P zl9WXUW;AB7XTr|Q3NKpaoG=&&@{A&iXbiB9S)noI;>KyK-H9@$aG(?a^GCzFKU3~9 zyE*C4KB{Y0V*{2tB^(2M!guj(J1fqbV=E?>(RkuZgW?KGX|D3IKjW^mYITKVbG#|0 z=2NHT&891fgsFLd47UIHu6>s!v!k8zb?KViYaaYlD>?7TE8mhtPp8*K#z~?bCgun& z#^A;A|u$Yi07ZI;mF29PDV9L2?64`_c%J-Rpkm_P@mB z^VMuEfvcDw>``EYwS9br^_a?)GmXp%no(Fb7|=TS4gGa4eic@-V3vB?&3~TtD;Hq^ zY2Pk?Ob>bJB9+c4^KT9*oYn8_32Pd3WQr`ZCO5ENPhAD9Kq8Nr*l0}(vXUhIvcq!wjM6~tb$DiXJtg95a z7+2=oF++qS=loDxM){0OXuo>Q6hUh_KP8Q=k{BtJLLDX!vySk-SVcXr-&XW0Lc5SR zze6>&TS_S2;Y(mE*$~N-27gw{Sxw5m$ZSFG*z73P(G{EBzysk^d66$}&|-d&qPUPp zg|qp=!SU;9k^ZEUyrK8VQ4@rXgSIO=W%|c1y_kKbjju#tt;0>$(M3h0-cI)Pi4lr% z|DvZ|0fxu&sYZ8?q6d>y=|_9i3a*00z=B~Bk_sk|AWJsl^x%CK5-*1MB&G;DEcrS| zVxgGz*Iq6f_a9bBT(A{1bJW-+UAUI}EhXd?_mUEqt80yqPgPPn zh~!${Sir-Tr+E4bGMA@DW|^St{Ph2n(TdrO*{b*wi6o{*>nXnH6Hzv@TeFDKk}DR$ zHG9HXAch^8KZ@O0{>U_T&8kULsEHYY^J!VTQeeo2id;*TPZaR>c&SwFOQAHcIP2Te zM3C+{yx7qCSDn$O>4cV5CAo9*o@|^^AGw0nOUzc0RL1X2sORS&#i)u~vd>!@*Bdo( zp{$2CV(^Oglr&-sxJb&F73D9BRVsle89NRxw@~P>x6@UEti;0YJ;gozEw0eAnsn8UW!Mf=w{|R&$2gNAQ{zzsx@)4ucjcN>ACyx16?){JCjRkHX8Jgr6ivrtkbq(~qF1STQj z7gvMX_XAk^lIGy|mp@hXi;Uslyz*3xh{$|U6$B0YN?r^KlOg1k*^RvmYmdR2-9MuOz3B2&H|7| z$qpiIPB$_zFByrZ*!N1`H$aZAFdhRb^j@GM-WkjF12DIvI{ljrZ8-Qf*_V1h8GZLz z?)+OxrYb|MK`E zFTL|Z>Pt$Qs$f<9@XdU|;-z;8>8t8; zd2G(S=BKa|vPBWPyc}hguvLH`x-1;-PQrVgjCcmp6!EvVa4d=>uMj`^^|QLu!Tq)t zXUW1!22k?Q++WDNtK}-H6$D!84Ny&d?C}vf;L;yQh%C=hVK@8+-s+B}Y6Ik!D+xM}kK zs@(;wPh^m*Ibt#r8_M6So96Wp&e|u4Tv;rbJA{e1%#Ic}l>VA8RJ!ovO^l8w@dx?s zV}OjE+^$o>mxxE`MYj_^X;O>_ab;In|A+%`q6>pq$e-#IR19hxFm?=PxS?NUV^Ywh zof4B#`k-}@U+4{SF01;R(*^`lUTiXVq5C9Ul(v?oNk4mrULoz@pjO89J0y~sL(v<< z2WnLGLBBCxe!K()QX_l5bOK)T*fkLRk-4E>5Kcf!6?I8JsN<1JisFYmEJ^9zW1$@Y z3V?T<{ZE&~R#Rxjjl>9>#o$jVZ$ACjTS7@cXfF-QYbLh3`MDuS@NPdy1sCwnHMtSk zR&hSP1#X6oc?4RbA-s^rRQ0nMet7^Dk08AWLCTcx`5jb_1fFmYdHrks@3OacRP=@F z&;j4~(O(c#-oN*Lb$kC_TP_(ygcld2zqOyi?+=8=PAsIQJ3vu3gu|ZoM9xM$V>;+$ zfzICF=DD@qIyP4K?SZ|S=$iP$!LJqXr(j&T<2f^oN4qk^v z_4@>J1>SqNvyp_ZI&b;)!*9J7e?|5MqytjtAB3_F@u`EN$}gaCVCrCA`w_Q9`x|+x z({;GQ4=D*<+G{0)WTlL-A800Z0^??VLl36 zaqkF0<3Yngi^TwxYAF3%aJL0Y`wvXF*@II{e;8fFb=Yd^L0|_zF)u(@?vXw1P@Fo@ z75FGk^6GemeL1YJKIc#PC4Kn3!G@@vPt4Trox2sJp}}i?D)P{U?|Smk zz13Z?t*Z8MJy(A6@X6C#aCrc6$ESVw5xXAa0bNxga9Myq?*)yP!UW}&sl)|}o=SsZ z>v55EYdW8i5S|D?1oy^pNq0%&NbsP)1$*h&j}fg^m#~iPEA$oWg~u=xGcc&Af1p3A zA6*e2&i|t)@`EgUyixTtmLWOqE3{7_F??819})r78{Q?ur3ohbmeUr8@<;g1-mK>k zjOdg;1O~WVsMI8kul4wz*zLp*T9E1|F;pBt2)!HnW%nD=^?S&i#+Y9ex=BA=;(d?- ztVECj-JaLqv>=3_0BNBRfPP^w;scfp@ns!NK%fCk?cf)%TO`6IQVIG4>KO5sS|oy_ z;8S){f0tA*!X_%e3-i61M&P|75VjkK8(vomhHoexAy6<{bidzz%iIv%k^U0%Qlmm2 z5U2Ex`a-kFyj7?4t}}#m7aE`DpED$|g=olk$rY&A52O&kde#i`g?*6TqPd)BmslS* z5nXvr*Mac~?1MppQJCld^3c_fjDb(wpZ(!4(u*jw0p`2b7Rwf~mzVMNF#9{xA#o); z;63>)#l`QO?(j)Qh~~jyfmn(k_C@jpG}iH^&v@7%@AGjNuo$opQantloE_kDfpGvy=K)RQ3CL`knIQ{An(~4nY})|LGri5e@ldh?qj` z2z5zz$#gOt=#kVVDXibi{zOCvl6(k9MhG<9lixwydS%y$_r!V#)X=9xtu7b-q~up+ z*g=n~d$x~UYGv07G2l-4>F-lwVu?1m~N5!su9$##~IrW3ReH zYKS%HfcRd&16v>V0=K06lv>}daYu8N_ySXVb2Ye#+7Ii9I(V&=ihvHCNA*sbo9cO> z#A7k+TR%IFqnqS=TE+p7CmCo_t}hi0{T_Jo`I3#O_a-x*Dmp;wwjeP;^Bay~K60gOicpJ;y$6`&4*;t`bp`(r~iuW(WZJ5^vnI2Fca}UkQ z`?I3`IOW9JW6qCihFmcYvRbhfsG_AgI{4dcURBr5`5QK4Q1uW1in1 zZb$=fI0(T?AK90^*i}CU`nb@tHGY$R?IwkJUYrg@7S7Ge( zNiDRH@FRtiPJ~nX90GqKSiGd~`$Z5j*?qr3!Hec-pYf8R6zRrRqX|)POe`e$!CkUGOr&9i32Qs0pyl? za(%^y4oEwpyu2?b;;27s#(u!1Q`pj~oJM5`Ahb1nw5bl-uj^cl&Qfn8NCo)ko*Y}J z4l`Ao^PKHirw-Ulb+=^hH;i#GgxBZ|IBOcBj_c{!c;=?Xds?E78S6fdQsX)Y zX~V~yKft7(GOta~9jnxVBWJC|fsn|iy2hiBBy6?rvr+ub5uAUAbB~trpIU8TxAlR;s`i)3&7cq;XI++a9M^Nn}@kMr^hRcaR#XT!Ri=*}b+WD-dG6^Cp`RmOfzqXxyagAeC27?%T!;X^~_|&RcTL_f@xyE3&HRP!>CO6raNNt1XYRq=~_<-55 zHgY;M%z1^*%`DaZL09sT8N4K+PoMF3r2TKG(n+Tu!UF8E7DhyWB=5Ee(msI z0@BCHo?ATnaY85rr8j_kNxcCE|f6vrp;_o~Y0p@ZvRQu*IzO1|5(w*OqY)6UlP?fHfu=AsaM2LcAq8DsX9{ zdeI-J;t%;6ojj@!Id8q^zd*fVm%u#m?fKK!MG&S+2^GKU$qcLK=51{LuK;2Uo%71` zv62p+k5zQ={Hwcz=U-{yA5nRA@ciqv#52HiR4+PsK6VTEC*&TJrSr7D@;v__xDj}P z>Vd>4@QailiHCq+B7cEjCOHInk>d3{?|&3-^4iVqEU#7WV0o=wV#f}a+m7yFx$TU^i#u3uo7ll}n|~+EYnvpt?_{}c|4x?M zj!HbXljXNlJ6V3axRd3!+Y-livi#=jV)?C7;?gdb_*S>E7U!?p3c2j&HjJ!nhDe!HS4vEKrL6T=B-Ujwi zIfocqL;M5%aERB^2SfY={nHZ90rvo}0r!%e2q9;L;tAYGxu#!tLcB_MZX1 zi|PUJ-4t)&dno^bKTGQ;5Rwj3JpjI!!71@K>o`0zX3O zlh^}120RS>D3v?#*MR4NAEWx$&sYpRPUQ#uI2~xv&+AjifS;iB_wzbc^#HF^wMyJH z!0S~B;HN0w1H5ikI>_r)tr9l@f1B(L^7_?@L0-SQBJt`VuU}07&j3pzypB~G;dQK~ z5*s6Yz`|&R*RzgEJP!OEl{@hBlwT6ZBD~&J`ZBL~bpy|n`mu;a$bSf}U!Zyf zyg>B;_(dvj2(MqF@&bOD%%i+V69xV`^&=9G0{?>AHSjOV9faLWdqX8}K_+?||PWe-pg-wDuK@E7Xqy|Ao>IyhY^){8vgZ z@cY!hfw!st0)Ig51^91Np1>bcc}gsSus=rSAaSk4M&Q5Gx*704sGb356kxmo76IE+ zyjR%+oCOR6X9KSQ=a4<%T;MI>JYaWP)8-Q&SOS~?-bMTH!>5EH1EauN@_&T&;b`CYmkP4? zuA7y;UYahZ9VdiP9=SAE5 zkIB8!CqcBGzg+rR$6Ep<<_o{q%I)`Oq%Olc85W6ar24a7hKcF1c0a4@Xb;E(Vkizf zcqv${Nl{#A4T^O4y7>$kvGzo9qqU}oxf|6U(Rht%iN;07z>@q!afJP_4#mLn}013!*gtEHBB3HK2Np^|4lIyyioB z@pBsMj2)H-Z_yggQyQ<`P#)2m&C_xpneyr*^3a@PGOs=+%b38DVnTNlr z@%rFNnU|lE<#&(tCyK)LJ}o!EiKxSEM9F!N$lY+FhY;l+IP-qe#-!~(?^(k+nC}Mj zFbdK;e<3qyBi(<3HxRt{Eo6pZlJ?aig-KiO#9T_ur{&%|kyda+tk-)tDNL$lM~V58 z+?^-VR`iIOoj6;E7^LFXQ^;b07{65}$qYgWFqq^go%ADS_6E~m$jADK<$2X6o!ui2 z?=fxG@xeS|pfZ@+BNcUYYL6IIRVEz;B&Ki{Gc3*@5(f~eOgcnJEXEtmZ?EDLfy9V! zFq<0q7$UKaVJ1`KGm69cxoA#HZGO^uMq)i*WzwNbLRdE= zkAkC(5i!$IZPM{ZV%lWc{I)o&NgUQ>=4XHpL=q<=SvJ2TPHhs$HmNr0XeJ>_sR7Y> zPvXEQ)g~R@B!nQD^W`#I=OB}vM-G$*N9rz^{5UFD^oYQGwVs%nw(ve_76+>mx`C?-Ine==FbAZPV zu{cYHhE1`laz@+L$7sGd8C1^N$rK^^oI~_FDzK!Ekn^>?Mdfhj#g4t8x`e9hDDK`Uz^Pex|U4?V2 zZO#p*8PH!8^8P{l8pY;U6($V?*C`-MCav_JU&oycr)Y-7%qSz3Y1*WP^D~V$n<`Ue z-N$C|Qut^SJ1sFPn8`Gnr+Tars~{al5vnS2V2!3{P)X{Gd=ks+#c8o&!4F3x0%{&ShaJa0t4&Sq0Y{uvunM1G@_P0A2j zPLFojx;2AI-I{5W7R>9Ox7jq9WYa87S|Xo#UMbBV7W4XU$xbEbe^kti%-vm_$s!={ ztW2ixbB@CF2h?5>jOtzN=WPmT8*E+#Bk%%`;3D)FXYpz(F*9nN${aVGS3;oly~LR; zoPT&0FR~KTA6A?j%nUzi`SaLIO4`3ai`PJJ1Z_4|rilE5v*oH~h^NOsW9XqJ#PcA3 zS~~ySY+mO(7iQ;U-QmXDqXM`Hz@pWtcUGnZ*&??80E0 zDS8GrTfu0Ls*{sByp+}xwIx$!UL)r;;i1KD*)*bDKZ#@nX^y)%oWo^X6Wq z9g%Z`NzP3stu=*5Y)o2cx-z1~bM{=;ioG#c=_<=qrV-GCHl}dCWiBhxw$D@I*`+XP zN-UiB*ldc@`~Vn-!04E-)Z~xPWyM=!`VWGM8sjAmUeeAzF`w(wI5AHtOtX6TD9p@> z85m=dd5!`Szbt^egA15>o|qN$I2ptw$yYdErZBB0Q_PCE#ih1@RHj+yN%i&6z4msb zFon&pBcMV^Qh_pQla_0~HIFq=iJ8$=8q5qoNyqd=nUcvAdI#jV_HrelkIZMSQiEwa zFSgtK`h3>!3_Qw9Kw{*&J)f1@##K>hTw$7`&`(sR5l}&i#iUWjoL!K@Ug={>U81=+ z?ZTZ`$Bxw{7Be&Q+h7n-x}e=9yxD%^Ij%OQP1@-nZMV&BNF8mYhME5lAOM%1r(2TM literal 15570 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 diff --git a/scripting/adminpanel.sp b/scripting/adminpanel.sp index acfb590..e862d11 100644 --- a/scripting/adminpanel.sp +++ b/scripting/adminpanel.sp @@ -2,12 +2,10 @@ #define DEBUG -// Update intervals (only sends when > 0 players) -// The update interval when there are active viewers -#define UPDATE_INTERVAL 5.0 -// The update interval when there are no viewers on. -// We still need to poll to know how many viewers are watching -#define UPDATE_INTERVAL_SLOW 20.0 +// Every attempt waits exponentionally longer, up to this value. +#define MAX_ATTEMPT_TIMEOUT 120.0 +#define DEFAULT_SERVER_PORT 7888 +#define SOCKET_TIMEOUT_DURATION 90.0 #include #include @@ -15,6 +13,7 @@ #include #include #include +#include #pragma newdecls required @@ -27,102 +26,277 @@ public Plugin myinfo = url = "https://github.com/jackzmc/l4d2-admin-dash" }; +int LIVESTATUS_VERSION = 0; + + ConVar cvar_debug; -ConVar cvar_postAddress; char postAddress[128]; -ConVar cvar_authKey; char authKey[512]; ConVar cvar_gamemode; char gamemode[32]; +ConVar cvar_difficulty; int gameDifficulty; +ConVar cvar_id; char serverId[32]; +ConVar cvar_address; char serverIp[16] = "127.0.0.1"; int serverPort = DEFAULT_SERVER_PORT; char currentMap[64]; int numberOfPlayers = 0; -int lastSuccessTime; int campaignStartTime; -int lastErrorCode; int uptime; -bool fastUpdateMode = false; - -Handle updateTimer = null; +bool g_inTransition; +bool isL4D1Survivors; +int lastReceiveTime; char steamidCache[MAXPLAYERS+1][32]; char nameCache[MAXPLAYERS+1][MAX_NAME_LENGTH]; int g_icBeingHealed[MAXPLAYERS+1]; int playerJoinTime[MAXPLAYERS+1]; +Handle updateHealthTimer[MAXPLAYERS+1]; +Handle updateItemTimer[MAXPLAYERS+1]; +Handle receiveTimeoutTimer = null; +bool lateLoaded; + +Socket g_socket; +bool g_isPaused; +#define BUFFER_SIZE 2048 +Buffer sendBuffer; +Buffer receiveBuffer; // Unfortunately there's no easy way to have this not be the same as BUFFER_SIZE + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + lateLoaded = late; + return APLRes_Success; +} public void OnPluginStart() { + // TODO: periodic reconnect + g_socket = new Socket(SOCKET_TCP, OnSocketError); + g_socket.SetOption(SocketKeepAlive, 1); + g_socket.SetOption(SocketReuseAddr, 1); + uptime = GetTime(); cvar_debug = CreateConVar("sm_adminpanel_debug", "0", "Turn on debug mode", FCVAR_DONTRECORD, true, 0.0, true, 1.0); - cvar_postAddress = CreateConVar("sm_adminpanel_url", "", "The base address to post updates to", FCVAR_NONE); - cvar_postAddress.AddChangeHook(OnCvarChanged); - cvar_postAddress.GetString(postAddress, sizeof(postAddress)); - cvar_authKey = CreateConVar("sm_adminpanel_key", "", "The authentication key", FCVAR_NONE); - cvar_authKey.AddChangeHook(OnCvarChanged); - cvar_authKey.GetString(authKey, sizeof(authKey)); + cvar_id = CreateConVar("sm_adminpanel_id", "", "The server ID to post updates for", FCVAR_NONE); + cvar_id.AddChangeHook(OnCvarChanged); + cvar_id.GetString(serverId, sizeof(serverId)); + + cvar_address = CreateConVar("sm_adminpanel_host", "100.108.152.125:7888", "The IP and port to connect to, default is 7888", FCVAR_NONE); + cvar_address.AddChangeHook(OnCvarChanged); + cvar_address.GetString(serverIp, sizeof(serverIp)); + OnCvarChanged(cvar_address, "", serverIp); cvar_gamemode = FindConVar("mp_gamemode"); cvar_gamemode.AddChangeHook(OnCvarChanged); cvar_gamemode.GetString(gamemode, sizeof(gamemode)); + cvar_difficulty = FindConVar("z_difficulty"); + cvar_difficulty.AddChangeHook(OnCvarChanged); + gameDifficulty = GetDifficultyInt(); + HookEvent("game_init", Event_GameStart); HookEvent("game_end", Event_GameEnd); - HookEvent("heal_success", Event_HealStop); - HookEvent("heal_interrupted", Event_HealStop); + HookEvent("heal_begin", Event_HealStart); + HookEvent("heal_success", Event_HealSuccess); + HookEvent("heal_interrupted", Event_HealInterrupted); + HookEvent("pills_used", Event_ItemUsed); + HookEvent("adrenaline_used", Event_ItemUsed); + HookEvent("weapon_drop", Event_WeaponDrop); HookEvent("player_first_spawn", Event_PlayerFirstSpawn); + HookEvent("map_transition", Event_MapTransition); + HookEvent("player_death", Event_PlayerDeath); + campaignStartTime = GetTime(); for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i)) { + playerJoinTime[i] = GetTime(); OnClientPutInServer(i); } } - TryStartTimer(true); - AutoExecConfig(true, "adminpanel"); - RegAdminCmd("sm_panel_status", Command_PanelStatus, ADMFLAG_GENERIC); - + RegAdminCmd("sm_panel_debug", Command_PanelDebug, ADMFLAG_GENERIC); + RegAdminCmd("sm_panel_request_stop", Command_RequestStop, ADMFLAG_GENERIC); } -#define DATE_FORMAT "%F at %I:%M %p" -Action Command_PanelStatus(int client, int args) { - ReplyToCommand(client, "Active: %b", updateTimer != null); - ReplyToCommand(client, "#Players: %d", numberOfPlayers); - ReplyToCommand(client, "Update Interval: %0f s", fastUpdateMode ? UPDATE_INTERVAL : UPDATE_INTERVAL_SLOW); - char buffer[32]; - ReplyToCommand(client, "Last Error Code: %d", lastErrorCode); - if(lastSuccessTime > 0) - FormatTime(buffer, sizeof(buffer), DATE_FORMAT, lastSuccessTime); - else - Format(buffer, sizeof(buffer), "(none)"); - ReplyToCommand(client, "Last Success: %s", buffer); - return Plugin_Handled; +void TriggerHealthUpdate(int client, bool instant = false) { + if(updateHealthTimer[client] != null) { + delete updateHealthTimer[client]; + } + updateHealthTimer[client] = CreateTimer(instant ? 0.1 : 1.0, Timer_UpdateHealth, client); } -void TryStartTimer(bool fast = true) { - if(numberOfPlayers > 0 && updateTimer == null && postAddress[0] != '\0' && authKey[0] != 0) { - fastUpdateMode = fast; - float interval = fast ? UPDATE_INTERVAL : UPDATE_INTERVAL_SLOW; - updateTimer = CreateTimer(interval, Timer_PostStatus, _, TIMER_REPEAT); - PrintToServer("[AdminPanel] Updating every %.1f seconds", interval); +void TriggerItemUpdate(int client) { + if(updateItemTimer[client] != null) { + delete updateItemTimer[client]; + } + updateItemTimer[client] = CreateTimer(1.0, Timer_UpdateItems, client); +} + +void OnSocketError(Socket socket, int errorType, int errorNumber, int any) { + PrintToServer("[AdminPanel] Socket Error %d %d", errorType, errorNumber); + if(!socket.Connected) { + PrintToServer("[AdminPanel] Lost connection to socket, reconnecting", errorType, errorNumber); + ConnectSocket(); } } +void OnSocketReceive(Socket socket, const char[] receiveData, int dataSize, int arg) { + receiveBuffer.FromArray(receiveData, dataSize); + LiveRecordResponse response = view_as(receiveBuffer.ReadByte()); + if(cvar_debug.BoolValue) { + PrintToServer("[AdminPanel] Received: %d", response); + } + lastReceiveTime = GetTime(); + switch(response) { + case Live_OK: { + int viewerCount = receiveBuffer.ReadByte(); + g_isPaused = viewerCount == 0; + } + case Live_Reconnect: + CreateTimer(5.0, Timer_Reconnect); + case Live_Refresh: { + PrintToServer("[AdminPanel] Refresh requested, performing"); + StartPayload(); + AddGameRecord(); + SendPayload(); + + SendPlayers(); + } + } + if(receiveTimeoutTimer != null) { + delete receiveTimeoutTimer; + } + receiveTimeoutTimer = CreateTimer(SOCKET_TIMEOUT_DURATION, Timer_Reconnect, 1); +} + +void OnSocketConnect(Socket socket, int any) { + if(cvar_debug.BoolValue) + PrintToServer("[AdminPanel] Connected to %s:%d", serverIp, serverPort); + g_socket.SetArg(0); + // Late loads / first setup we can't send + if(currentMap[0] != '\0' && StartPayload()) { + AddGameRecord(); + SendPayload(); + // Resend all players + SendPlayers(); + } +} + +void OnSocketDisconnect(Socket socket, int attempt) { + g_socket.SetArg(attempt + 1); + float nextAttempt = Exponential(float(attempt) / 2.0) + 2.0; + if(nextAttempt > MAX_ATTEMPT_TIMEOUT) nextAttempt = MAX_ATTEMPT_TIMEOUT; + PrintToServer("[AdminPanel] Disconnected, retrying in %.0f seconds", nextAttempt); + CreateTimer(nextAttempt, Timer_Reconnect); +} + +Action Timer_Reconnect(Handle h, int type) { + if(type == 1) { + PrintToServer("[AdminPanel] No response after %f seconds, attempting reconnect", SOCKET_TIMEOUT_DURATION); + } + ConnectSocket(); + return Plugin_Handled; +} + +void ConnectSocket() { + if(g_socket == null) LogError("Socket is invalid"); + if(g_socket.Connected) + g_socket.Disconnect(); + if(serverId[0] == '\0') return; + g_socket.SetOption(DebugMode, cvar_debug.BoolValue); + g_socket.Connect(OnSocketConnect, OnSocketReceive, OnSocketDisconnect, serverIp, serverPort); +} + +#define DATE_FORMAT "%F at %I:%M %p" +Action Command_PanelDebug(int client, int args) { + char arg[32]; + GetCmdArg(1, arg, sizeof(arg)); + if(StrEqual(arg, "connect")) { + if(serverId[0] == '\0') + ReplyToCommand(client, "No server id."); + else + ConnectSocket(); + } else if(StrEqual(arg, "info")) { + ReplyToCommand(client, "Connected: %b\tPaused: %b\t#Player: %d", g_socket.Connected, g_isPaused, numberOfPlayers); + ReplyToCommand(client, "ID: %s", serverId); + ReplyToCommand(client, "Target Host: %s:%d", serverIp, serverPort); + ReplyToCommand(client, "Buffer Size: %d", BUFFER_SIZE); + } else if(g_socket.Connected) { + if(StrEqual(arg, "game")) { + StartPayload(); + AddGameRecord(); + SendPayload(); + } else if(StrEqual(arg, "players")) { + SendPlayers(); + } else { + ReplyToCommand(client, "Unknown type"); + return Plugin_Handled; + } + } else { + ReplyToCommand(client, "Not connected"); + } + return Plugin_Handled; +} + +Action Command_RequestStop(int client, int args) { + if(GetClientCount(false) > 0) { + ReplyToCommand(client, "There are still %d players online.", GetClientCount(false)); + } else { + ReplyToCommand(client, "Stopping..."); + RequestFrame(StopServer); + } + return Plugin_Handled; +} +void StopServer() { + ServerCommand("exit"); +} void Event_GameStart(Event event, const char[] name, bool dontBroadcast) { campaignStartTime = GetTime(); + if(StartPayload()) { + AddGameRecord(); + SendPayload(); + } } void Event_GameEnd(Event event, const char[] name, bool dontBroadcast) { campaignStartTime = 0; } -void Event_HealStart(Event event, const char[] name, bool dontBroadcast) { - int healing = GetClientOfUserId(event.GetInt("subject")); - g_icBeingHealed[healing] = true; +void Event_MapTransition(Event event, const char[] name, bool dontBroadcast) { + g_inTransition = true; + if(StartPayload()) { + AddGameRecord(); + SendPayload(); + } } -void Event_HealStop(Event event, const char[] name, bool dontBroadcast) { - int healing = GetClientOfUserId(event.GetInt("subject")); - g_icBeingHealed[healing] = false; + +void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0) { + PrintToServer("death: %N", client); + TriggerHealthUpdate(client, true); + } +} + +void Event_HealStart(Event event, const char[] name, bool dontBroadcast) { + int subject = GetClientOfUserId(event.GetInt("subject")); + g_icBeingHealed[subject] = true; +} +void Event_HealSuccess(Event event, const char[] name, bool dontBroadcast) { + int healer = GetClientOfUserId(event.GetInt("userid")); + int subject = GetClientOfUserId(event.GetInt("subject")); + if(subject > 0 && StartPayload()) { + g_icBeingHealed[subject] = false; + // Update the subject's health: + AddSurvivorRecord(subject); + // Update the teammate who healed subject: + AddSurvivorItemsRecord(healer); + SendPayload(); + } +} +void Event_HealInterrupted(Event event, const char[] name, bool dontBroadcast) { + int subject = GetClientOfUserId(event.GetInt("subject")); + g_icBeingHealed[subject] = false; } void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) { @@ -141,24 +315,155 @@ void RecalculatePlayerCount() { numberOfPlayers = players; } +void SendPlayers() { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientInGame(i)) { + StartPayload(); + AddPlayerRecord(i); + SendPayload(); + } + } +} + +// public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) { +// float time = GetGameTime(); +// // if(time - lastUpdateTime[client] > 7.0) { +// // if(StartPayload()) { +// // lastUpdateTime[client] = time; +// // AddSurvivorRecord(client); +// // SendPayload(); +// // } +// // } +// } + public void OnMapStart() { GetCurrentMap(currentMap, sizeof(currentMap)); numberOfPlayers = 0; + if(lateLoaded) { + StartPayload(); + AddGameRecord(); + SendPayload(); + + SendPlayers(); + } } +public void OnConfigsExecuted() { + isL4D1Survivors = L4D2_GetSurvivorSetMap() == 1; +} // Player counts public void OnClientPutInServer(int client) { + if(g_inTransition) { + g_inTransition = false; + if(StartPayload()) { + AddGameRecord(); + SendPayload(); + } + } GetClientName(client, nameCache[client], MAX_NAME_LENGTH); if(!IsFakeClient(client)) { GetClientAuthId(client, AuthId_SteamID64, steamidCache[client], 32); numberOfPlayers++; - TryStartTimer(true); } else { + // Check if they are not a bot, such as ABMBot or EPIBot, etc + char classname[32]; + GetEntityClassname(client, classname, sizeof(classname)); + if(StrContains(classname, "bot", false) > -1) { + return; + } strcopy(steamidCache[client], 32, "BOT"); } + SDKHook(client, SDKHook_WeaponEquipPost, OnWeaponPickUp); + SDKHook(client, SDKHook_OnTakeDamageAlivePost, OnTakeDamagePost); + // We wait a frame because Event_PlayerFirstSpawn sets their join time + RequestFrame(SendNewClient, client); +} + +void OnWeaponPickUp(int client, int weapon) { + // float time = GetGameTime(); + // if(time - lastUpdateTime[client] > 3.0 && StartPayload()) { + // lastUpdateTime[client] = time; + // AddSurvivorItemsRecord(client); + // SendPayload(); + // } + if(GetClientTeam(client) == 2) + TriggerItemUpdate(client); +} + +// Tracks the inventories for pills/adr used, kit used, ammo pack used, etc +void Event_WeaponDrop(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0) { + if(GetClientTeam(client) == 2) + TriggerItemUpdate(client); + // if(StartPayload()) { + // AddSurvivorItemsRecord(client); + // SendPayload(); + // } + } +} + +void Event_ItemUsed(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0) { + // if(StartPayload()) { + // AddSurvivorRecord(client); + // SendPayload(); + // } + if(GetClientTeam(client) == 2) + TriggerHealthUpdate(client); + } +} + +void OnTakeDamagePost(int victim, int attacker, int inflictor, float damage, int damagetype, int weapon, const float damageForce[3], const float damagePosition[3], int damagecustom) { + if(damage > 1.0 && victim > 0 && victim <= MaxClients) { + TriggerHealthUpdate(victim); + // if(GetGameTime() - lastUpdateTime[victim] > 0.3 && StartPayload()) { + // lastUpdateTime[victim] = GetGameTime(); + // if(GetClientTeam(victim) == 2) + // AddSurvivorRecord(victim); + // else + // AddInfectedRecord(victim); + // SendPayload(); + // } + } +} + +Action Timer_UpdateHealth(Handle h, int client) { + if(IsClientInGame(client) && StartPayload()) { + if(GetClientTeam(client) == 2) + AddSurvivorRecord(client); + else + AddInfectedRecord(client); + SendPayload(); + } + updateHealthTimer[client] = null; + return Plugin_Handled; +} +Action Timer_UpdateItems(Handle h, int client) { + if(IsClientInGame(client) && StartPayload()) { + AddSurvivorItemsRecord(client); + SendPayload(); + } + updateItemTimer[client] = null; + return Plugin_Handled; +} + +void SendNewClient(int client) { + if(!IsClientInGame(client)) return; + if(StartPayload()) { + PrintToServer("SendNewClient(%N)", client); + AddPlayerRecord(client); + SendPayload(); + } } public void OnClientDisconnect(int client) { + if(StartPayload()) { + // hopefully userid is valid here? + AddPlayerRecord(client, false); + SendPayload(); + } steamidCache[client][0] = '\0'; nameCache[client][0] = '\0'; if(!IsFakeClient(client)) { @@ -167,134 +472,58 @@ public void OnClientDisconnect(int client) { if(numberOfPlayers < 0) { numberOfPlayers = 0; } - if(numberOfPlayers == 0 && updateTimer != null) { - delete updateTimer; - } + } + if(updateHealthTimer[client] != null) { + delete updateHealthTimer[client]; + } + if(updateItemTimer[client] != null) { + delete updateItemTimer[client]; } } // Cvar updates void OnCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) { - if(cvar_postAddress == convar) { - strcopy(postAddress, sizeof(postAddress), newValue); - PrintToServer("[AdminPanel] Update Url has updated"); - } else if(cvar_authKey == convar) { - strcopy(authKey, sizeof(authKey), newValue); - PrintToServer("[AdminPanel] Auth key has been updated"); + if(cvar_id == convar) { + strcopy(serverId, sizeof(serverId), newValue); + PrintToServer("[AdminPanel] Server ID changed to: %s", serverId); + } else if(cvar_address == convar) { + if(newValue[0] == '\0') { + if(g_socket.Connected) + g_socket.Disconnect(); + serverPort = DEFAULT_SERVER_PORT; + serverIp = "127.0.0.1"; + PrintToServer("[AdminPanel] Deactivated"); + } else { + int index = SplitString(newValue, ":", serverIp, sizeof(serverIp)); + if(index > -1) { + serverPort = StringToInt(newValue[index]); + if(serverPort == 0) serverPort = DEFAULT_SERVER_PORT; + } + PrintToServer("[AdminPanel] Sending data to %s:%d", serverIp, serverPort); + ConnectSocket(); + } } else if(cvar_gamemode == convar) { strcopy(gamemode, sizeof(gamemode), newValue); - } - TryStartTimer(true); -} - -bool isSubmitting; -Action Timer_PostStatus(Handle h) { - if(isSubmitting) return Plugin_Continue; - isSubmitting = true; - // TODO: optimize only if someone is requesting live - HTTPRequest req = new HTTPRequest(postAddress); - JSONObject obj = GetObject(); - req.SetHeader("x-authtoken", authKey); - // req.AppendFormParam("playerCount", "%d", numberOfPlayers); - // req.AppendFormParam("map", currentMap); - if(cvar_debug.BoolValue) PrintToServer("[AdminPanel] Submitting"); - req.Post(obj, Callback_PostStatus); - delete obj; - // req.PostForm(Callback_PostStatus); - return Plugin_Continue; -} - -void Callback_PostStatus(HTTPResponse response, any value, const char[] error) { - isSubmitting = false; - if(response.Status == HTTPStatus_NoContent || response.Status == HTTPStatus_OK) { - lastErrorCode = 0; - lastSuccessTime = GetTime(); - if(cvar_debug.BoolValue) - PrintToServer("[AdminPanel] Response: OK/204"); - // We have subscribers, kill timer and recreate it in fast mode (if not already): - if(!fastUpdateMode) { - PrintToServer("[AdminPanel] Switching to fast update interval for active viewers."); - if(updateTimer != null) - delete updateTimer; - TryStartTimer(true); + if(StartPayload()) { + AddGameRecord(); + SendPayload(); } - - } else if(response.Status == HTTPStatus_Gone) { - lastErrorCode = 0; - // We have no subscribers, kill timer and recreate it in slow mode (if not already): - if(fastUpdateMode) { - PrintToServer("[AdminPanel] Switching to slow update interval, no viewers"); - if(updateTimer != null) - delete updateTimer; - TryStartTimer(false); - } - } else { - lastErrorCode = view_as(response.Status); - lastSuccessTime = 0; - // TODO: backoff - PrintToServer("[AdminPanel] Getting response: %d", response.Status); - if(cvar_debug.BoolValue) { - char buffer[64]; - JSONObject json = view_as(response.Data); - if(json.GetString("error", buffer, sizeof(buffer))) { - PrintToServer("[AdminPanel] Got %d response from server: \"%s\"", view_as(response.Status), buffer); - json.GetString("message", buffer, sizeof(buffer)); - PrintToServer("[AdminPanel] Error message: \"%s\"", buffer); - } else { - PrintToServer("[AdminPanel] Got %d response from server: \n%s", view_as(response.Status), error); - } - } - if(view_as(response.Status) == 0 || response.Status == HTTPStatus_Unauthorized || response.Status == HTTPStatus_Forbidden) { - PrintToServer("[AdminPanel] API Key seems to be invalid, killing timer."); - if(updateTimer != null) - delete updateTimer; + } else if(cvar_difficulty == convar) { + gameDifficulty = GetDifficultyInt(); + if(StartPayload()) { + AddGameRecord(); + SendPayload(); } } } -JSONObject GetObject() { - JSONObject obj = new JSONObject(); - obj.SetInt("playerCount", numberOfPlayers); - obj.SetString("map", currentMap); - obj.SetString("gamemode", gamemode); - obj.SetInt("startTime", uptime); - obj.SetFloat("fps", 1.0 / GetGameFrameTime()); - AddFinaleInfo(obj); - JSONArray players = GetPlayers(); - obj.Set("players", players); - delete players; - obj.SetFloat("refreshInterval", UPDATE_INTERVAL); - obj.SetInt("lastUpdateTime", GetTime()); - obj.SetInt("campaignStartTime", campaignStartTime); - return obj; -} - -void AddFinaleInfo(JSONObject parentObj) { - if(L4D_IsMissionFinalMap()) { - JSONObject obj = new JSONObject(); - obj.SetBool("escapeLeaving", L4D_IsFinaleEscapeInProgress()); - obj.SetInt("finaleStage", L4D2_GetCurrentFinaleStage()); - parentObj.Set("finaleInfo", obj); - delete obj; +public void L4D2_OnChangeFinaleStage_Post(int finaleType, const char[] arg) { + if(StartPayload()) { + AddFinaleRecord(finaleType); + SendPayload(); } } -JSONArray GetPlayers() { - JSONArray players = new JSONArray(); - for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i)) { - int team = GetClientTeam(i); - if( team == 2 || team == 3) { - JSONObject player = GetPlayer(i); - players.Push(player); - delete player; - } - } - } - return players; -} - - enum { Action_BeingHealed = -1, Action_None = 0, // No use action active @@ -351,7 +580,9 @@ enum { pState_BlackAndWhite = 1, pState_InSaferoom = 2, pState_IsCalm = 4, - pState_IsBoomed = 8 + pState_IsBoomed = 8, + pState_IsPinned = 16, + pState_IsAlive = 32, } stock bool IsPlayerBoomed(int client) { @@ -365,62 +596,262 @@ int GetPlayerStates(int client) { if(GetEntProp(client, Prop_Send, "m_bIsOnThirdStrike", 1)) state |= pState_BlackAndWhite; if(GetEntProp(client, Prop_Send, "m_isCalm")) state |= pState_IsCalm; if(IsPlayerBoomed(client)) state |= pState_IsBoomed; + if(IsPlayerAlive(client)) state |= pState_IsAlive; + if(L4D2_GetInfectedAttacker(client) > 0) state |= pState_IsPinned; return state; } -JSONObject GetPlayer(int client) { - int team = GetClientTeam(client); - JSONObject player = new JSONObject(); - player.SetString("steamid", steamidCache[client]); - player.SetInt("userId", GetClientUserId(client)); - player.SetString("name", nameCache[client]); - player.SetInt("team", team); - player.SetBool("isAlive", IsPlayerAlive(client)); - player.SetInt("joinTime", playerJoinTime[client]); - player.SetInt("permHealth", GetEntProp(client, Prop_Send, "m_iHealth")); - if(team == 2) { - // Include idle players (player here is their idle bot) - if(IsFakeClient(client)) { - int idlePlayer = L4D_GetIdlePlayerOfBot(client); - if(idlePlayer > 0) { - player.SetString("idlePlayerId", steamidCache[idlePlayer]); - if(IsClientInGame(idlePlayer)) { - JSONObject idlePlayerObj = GetPlayer(idlePlayer); - player.Set("idlePlayer", idlePlayerObj); - delete idlePlayerObj; - } - } - } - player.SetInt("action", GetAction(client)); - player.SetInt("flowProgress", L4D2_GetVersusCompletionPlayer(client)); - player.SetFloat("flow", L4D2Direct_GetFlowDistance(client)); - player.SetBool("isPinned", L4D2_GetInfectedAttacker(client) > 0); - player.SetInt("tempHealth", L4D_GetPlayerTempHealth(client)); - player.SetInt("states", GetPlayerStates(client)); - player.SetInt("move", GetPlayerMovement(client)); - player.SetInt("survivor", GetEntProp(client, Prop_Send, "m_survivorCharacter")); - JSONArray weapons = GetPlayerWeapons(client); - player.Set("weapons", weapons); - delete weapons; - } else if(team == 3) { - player.SetInt("class", L4D2_GetPlayerZombieClass(client)); - player.SetInt("maxHealth", GetEntProp(client, Prop_Send, "m_iMaxHealth")); - int victim = L4D2_GetSurvivorVictim(client); - if(victim > 0) - player.SetString("pinnedSurvivorId", steamidCache[victim]); - } - return player; +stock int GetDifficultyInt() { + char diff[16]; + cvar_difficulty.GetString(diff, sizeof(diff)); + if(StrEqual(diff, "easy", false)) return 0; + else if(StrEqual(diff, "hard", false)) return 2; + else if(StrEqual(diff, "impossible", false)) return 3; + else return 1; } -JSONArray GetPlayerWeapons(int client) { - JSONArray weapons = new JSONArray(); - static char buffer[64]; - for(int slot = 0; slot < 6; slot++) { - if(GetClientWeaponNameSmart(client, slot, buffer, sizeof(buffer))) { - weapons.PushString(buffer); - } else { - weapons.PushNull(); +enum LiveRecordType { + Live_Game, + Live_Player, + Live_Survivor, + Live_Infected, + Live_Finale, + Live_SurvivorItems, + Live_CommandResponse, + Live_Auth +} + +enum LiveRecordResponse { + Live_OK, + Live_Reconnect, + Live_Error, + Live_Refresh, + Live_RunComand +} + +bool StartPayload() { + if(!cvar_debug.BoolValue && (g_isPaused || numberOfPlayers == 0)) return false; + sendBuffer.Reset(); + sendBuffer.WriteByte(LIVESTATUS_VERSION); + sendBuffer.WriteString(serverId); + return g_socket.Connected; +} + +void StartRecord(LiveRecordType type) { + sendBuffer.WriteChar('\x1e'); // record separator + sendBuffer.WriteByte(view_as(type)); +} + +void AddGameRecord() { + PrintToServer("pushing Live_Game"); + StartRecord(Live_Game); + sendBuffer.WriteInt(uptime); + sendBuffer.WriteInt(campaignStartTime); + sendBuffer.WriteByte(gameDifficulty); + sendBuffer.WriteByte(g_inTransition); + sendBuffer.WriteString(gamemode); + sendBuffer.WriteString(currentMap); +} + +void AddFinaleRecord(int stage) { + StartRecord(Live_Finale); + sendBuffer.WriteByte(stage); // finale stage + sendBuffer.WriteByte(L4D_IsFinaleEscapeInProgress()); // escape or not +} + +void AddPlayerRecord(int client, bool connected = true) { + // fake bots are ignored: + + int originalClient = client; + bool isIdle = false; + if(connected) { + // If this is an idle player's bot, then we use the real player's info instead. + if(IsFakeClient(client)) { + int realPlayer = L4D_GetIdlePlayerOfBot(client); + if(realPlayer > 0) { + PrintToServer("%d is idle bot of %N", client, realPlayer); + isIdle = true; + client = realPlayer; + } else if(steamidCache[client][0] == '\0') { + PrintToServer("skipping %N %s", client, steamidCache[client]); + return; + } } } - return weapons; + StartRecord(Live_Player); + sendBuffer.WriteInt(GetClientUserId(client)); + sendBuffer.WriteString(steamidCache[client]); + if(connected) { + sendBuffer.WriteByte(isIdle); + sendBuffer.WriteInt(playerJoinTime[client]); + sendBuffer.WriteString(nameCache[client]); + + if(GetClientTeam(originalClient) == 2) { + AddSurvivorRecord(originalClient, client); + AddSurvivorItemsRecord(originalClient, client); + } else if(GetClientTeam(client) == 3) { + AddInfectedRecord(client); + } + } +} + +void AddSurvivorRecord(int client, int forClient = 0) { + if(forClient == 0) forClient = client; + int survivor = GetEntProp(client, Prop_Send, "m_survivorCharacter"); + // The icons are mapped for survivors as 4,5,6,7; so inc to that for L4D1 survivors + if(isL4D1Survivors) { + survivor += 4; + } + if(survivor >= 8) return; + StartRecord(Live_Survivor); + sendBuffer.WriteInt(GetClientUserId(forClient)); + sendBuffer.WriteByte(survivor); + sendBuffer.WriteByte(L4D_GetPlayerTempHealth(client)); //temp health + sendBuffer.WriteByte(GetEntProp(client, Prop_Send, "m_iHealth")); //perm health + sendBuffer.WriteByte(L4D2_GetVersusCompletionPlayer(client)); // flow% + sendBuffer.WriteInt(GetPlayerStates(client)); // state (incl. alive) + sendBuffer.WriteInt(GetPlayerMovement(client)); // move + sendBuffer.WriteInt(GetAction(client)); // action +} +void AddSurvivorItemsRecord(int client, int forClient = 0) { + if(forClient == 0) forClient = client; + StartRecord(Live_SurvivorItems); + sendBuffer.WriteInt(GetClientUserId(client)); + char name[32]; + for(int slot = 0; slot < 6; slot++) { + name[0] = '\0'; + GetClientWeaponNameSmart2(client, slot, name, sizeof(name)); + sendBuffer.WriteString(name); + } +} +void AddInfectedRecord(int client) { + StartRecord(Live_Infected); + sendBuffer.WriteInt(GetClientUserId(client)); + sendBuffer.WriteShort(GetEntProp(client, Prop_Send, "m_iHealth")); //cur health + sendBuffer.WriteShort(GetEntProp(client, Prop_Send, "m_iMaxHealth")); //max health + sendBuffer.WriteByte(L4D2_GetPlayerZombieClass(client)); // class + int victim = L4D2_GetSurvivorVictim(client); + if(victim > 0) + sendBuffer.WriteInt(GetClientUserId(victim)); + else + sendBuffer.WriteInt(0); +} + +void SendPayload() { + sendBuffer.Finish(); + if(cvar_debug.BoolValue) + PrintToServer("[AdminPanel] Sending %d bytes of data", sendBuffer.offset); + g_socket.Send(sendBuffer.buffer, sendBuffer.offset); +} + +enum struct Buffer { + char buffer[BUFFER_SIZE]; + int offset; + + void Reset() { + this.buffer[0] = '\0'; + this.offset = 0; + } + + void FromArray(const char[] input, int size) { + this.Reset(); + int max = BUFFER_SIZE; + if(size < max) max = size; + for(int i = 0; i < max; i++) { + this.buffer[i] = input[i]; + } + } + + void Print() { + char[] output = new char[BUFFER_SIZE+100]; + for(int i = 0; i < BUFFER_SIZE; i++) { + if(this.buffer[i] == '\0') { + Format(output, BUFFER_SIZE, "%s \\0", output); + } else { + Format(output, BUFFER_SIZE, "%s %c", output, this.buffer[i]); + } + } + PrintToServer("%s", output); + } + + void WriteChar(char c) { + this.buffer[this.offset++] = c; + } + + void WriteByte(int value) { + this.buffer[this.offset++] = value & 0xFF; + } + + void WriteShort(int value) { + this.buffer[this.offset++] = value & 0xFF; + this.buffer[this.offset++] = (value >> 8) & 0xFF; + } + + void WriteInt(int value, int bytes = 4) { + this.buffer[this.offset++] = value & 0xFF; + this.buffer[this.offset++] = (value >> 8) & 0xFF; + this.buffer[this.offset++] = (value >> 16) & 0xFF; + this.buffer[this.offset++] = (value >> 24) & 0xFF; + } + + void WriteFloat(float value) { + this.WriteInt(view_as(value)); + } + + /// Writes a variable-width string, with the size being prepended. Only supports strings up to 2^15 in size + /// @param lenHint - optional, but the length of the string, to avoid strlen() twice + void WriteVarString(const char[] string, int lenHint = -1) { + if(lenHint < 0) lenHint = strlen(string); + this.WriteShort(lenHint); + // null term written will just get overwritten + strcopy(this.buffer[this.offset], BUFFER_SIZE, string); + this.offset += lenHint; + } + + // Writes a null-terminated length string, strlen > size is truncated. + void WriteString(const char[] string) { + int written = strcopy(this.buffer[this.offset], BUFFER_SIZE, string); + this.offset += written + 1; + } + + void Finish() { + // Set newline + this.buffer[this.offset++] = '\n'; + } + + int ReadByte() { + return this.buffer[this.offset++] & 0xFF; + } + + int ReadShort() { + int value = this.buffer[this.offset++]; + value += this.buffer[this.offset++] << 8; + return value; + } + + int ReadInt() { + int value = this.buffer[this.offset++]; + value += this.buffer[this.offset++] << 8; + value += this.buffer[this.offset++] << 16; + value += this.buffer[this.offset++] << 32; + return value; + } + + float ReadFloat() { + return view_as(this.ReadInt()); + } + + int ReadString(char[] output, int maxlen) { + int len = strcopy(output, maxlen, this.buffer[this.offset]) + 1; + this.offset += len; + return len; + } + + char ReadChar() { + return this.buffer[this.offset++]; + } + + bool EOF() { + return this.offset >= BUFFER_SIZE; + } } \ No newline at end of file